Python-智能项目-全-
Python 智能项目(全)
零、前言
Python 人工智能项目将帮助您利用深度学习和强化学习构建智能实用的基于 AI 的系统。本书中展示的项目涵盖了与医疗保健、电子商务、专家系统、监控时尚行业、基于移动的应用以及自动驾驶汽车相关的广泛领域问题,这些领域使用了卷积神经网络、深度强化学习、基于 LSTM 的神经网络、受限玻尔兹曼机器、生成对抗网络、机器翻译和迁移学习等技术。本书中阐述的构建智能应用程序的理论方面将使读者能够以有趣的方式扩展项目,并使它们在构建有影响力的人工智能应用程序时跟上速度。到本书结束时,你将足够熟练地构建自己的智能模型来解决任何类型的问题,而没有任何麻烦。
这本书是给谁的
这本书是为数据科学家、机器学习专业人员和深度学习实践者准备的,他们准备扩展他们的人工智能知识。如果你想构建现实生活中的智能系统,在每个复杂的领域发挥至关重要的作用,那么这本书就是你所需要的。
这本书涵盖了什么
第 1 章,基于人工智能的系统基础,涵盖了如何使用机器学习、深度学习和强化学习构建智能人工系统的基础知识。我们将讨论各种人工神经网络,包括用于图像处理的中枢神经系统和用于自然语言处理的神经网络。
第二章、迁移学习,讲述了如何利用迁移学习检测人眼的糖尿病视网膜病变情况,并判断视网膜病变的严重程度。我们将探索中枢神经系统,并学习如何用中枢神经系统训练一个能够在人眼眼底图像中检测糖尿病视网膜病变的模型。
第 3 章、神经机器翻译,涵盖了递归神经网络 ( RNN )架构的基础知识。我们还将学习三种不同的机器翻译系统:基于规则的机器翻译、统计机器翻译和神经机器翻译。
第 4 章、时尚产业中的风格转移使用 GANs ,解释了如何创建智能 AI 模型来生成与给定手提包风格相似的鞋子,反之亦然。我们将使用香草 GAN 来实现项目,使用定制版本的 GAN,如 DiscoGAN 和 CycleGAN。
第 5 章、视频字幕应用,讨论了 CNNs 和 LSTMs 在视频字幕中的作用,并解释了如何利用序列到序列——视频到文本架构来构建视频字幕系统。
第六章、智能推荐系统讨论推荐系统,它是一种信息过滤系统,处理数字数据过载的问题,根据需要拉出项目或信息。我们将使用潜在因子分解进行协同过滤,并使用受限的玻尔兹曼机器来构建推荐系统。
第 7 章电影评论情感分析手机应用解释了机器学习作为一种服务是如何让手机应用受益的。我们将使用 TensorFlow 创建一个安卓移动应用程序,该应用程序将电影评论作为输入,并根据情感分析提供评级。
第 8 章面向客户服务的对话式 AI 聊天机器人,解释了聊天机器人在期间是如何发展的,并探讨了拥有对话式聊天机器人的好处。我们还将研究如何创建聊天机器人,以及什么是 LSTM 序列到序列模型。我们还将为推特支持的聊天机器人建立一个序列到序列的模型。
第九章、通过强化学习的自动驾驶汽车,讲解强化学习和 Q 学习。我们还将使用深度学习和强化学习来制造自动驾驶汽车。
第 10 章、验证码从深度学习的角度,我们讨论什么是验证码以及为什么需要验证码。我们还将创建一个模型,使用深度学习来打破验证码,然后如何使用对抗性学习来生成验证码。
充分利用这本书
读者应该有 Python 和人工智能的先前知识,才能浏览书中的项目。
下载示例代码文件
你可以从你在www.packt.com的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packt.com/support并注册将文件直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
- 登录或注册www.packt.com。
- 选择“支持”选项卡。
- 点击代码下载和勘误表。
- 在搜索框中输入图书的名称,并按照屏幕指示进行操作。
下载文件后,请确保使用最新版本的解压缩文件夹:
- 视窗系统的 WinRAR/7-Zip
- zipeg/izp/un ARX for MAC
- 适用于 Linux 的 7-Zip/PeaZip
这本书的代码包也在 GitHub 上托管在https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python。如果代码有更新,它将在现有的 GitHub 存储库中更新。
我们还有来自丰富的图书和视频目录的其他代码包,可在【https://github.com/PacktPublishing/】获得。看看他们!
下载彩色图像
我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:https://www . packtpub . com/sites/default/files/downloads/9781788996921 _ color images . pdf。
行动中的代码
访问以下链接查看正在运行的代码的视频:
http://bit.ly/2Ru8rlU
使用的约定
本书通篇使用了许多文本约定。
CodeInText:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。下面是一个例子:“将下载的WebStorm-10*.dmg磁盘镜像文件作为另一个磁盘挂载到系统中。”
代码块设置如下:
def get_im_cv2(path,dim=224):
img = cv2.imread(path)
resized = cv2.resize(img, (dim,dim), cv2.INTER_LINEAR)
return resized
当我们希望将您的注意力吸引到代码块的特定部分时,相关的行或项目以粗体显示:
adam = optimizers.Adam(lr=0.00001, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.0)
任何命令行输入或输出都编写如下:
Cross Validation Accuracy: 0.6383708345200797
Validation Quadratic Kappa Score: 0.47422998110380984
粗体:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如,菜单或对话框中的单词像这样出现在文本中。下面是一个示例:“从管理面板中选择系统信息。”
Warnings or important notes appear like this. Tips and tricks appear like this.
取得联系
我们随时欢迎读者的反馈。
一般反馈:如果你对这本书的任何方面有疑问,在你的信息主题中提到书名,发邮件给我们customercare@packtpub.com。
勘误表:虽然我们已经尽了最大的努力来保证内容的准确性,但是错误还是会发生。如果你在这本书里发现了一个错误,如果你能向我们报告,我们将不胜感激。请访问www.packt.com/submit-errata,选择您的图书,点击勘误表提交链接,并输入详细信息。
盗版:如果您在互联网上遇到任何形式的我们作品的非法拷贝,如果您能提供我们的位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com联系我们,并提供材料链接。
如果你有兴趣成为一名作者:如果有一个你有专长的话题,你有兴趣写或者投稿一本书,请访问authors.packtpub.com。
复习
请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在的读者可以看到并使用您不带偏见的意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢大家!
更多关于 Packt 的信息,请访问packt.com。
一、基于人工智能的系统基础
人工智能 ( AI )在过去几年中一直走在技术的前沿,并已进入主流应用,如专家系统、移动设备上的个性化应用、自然语言处理中的机器翻译、聊天机器人、自动驾驶汽车等。然而,人工智能的定义一直是争议的话题。这主要是因为所谓的人工智能效应将过去已经通过人工智能解决的工作归类为非人工智能。根据一位著名的计算机科学家的说法:
Intelligence is whatever machines haven't done yet. – Larry Tesler
构建一个可以下棋的智能系统被认为是人工智能,直到 1996 年 IBM 计算机深蓝击败加里·卡斯帕罗夫。同样,处理视觉、语音和自然语言的问题曾经被认为是复杂的,但由于人工智能的影响,它们现在只会被认为是计算,而不是真正的人工智能。最近,人工智能已经能够解决复杂的数学问题、创作音乐和创作抽象画,人工智能的这些能力正在不断增加。未来人工智能系统与人类智力水平相当的点被科学家称为人工智能奇点。机器是否真的能达到人类的智力水平这个问题非常有趣。
许多人会认为,机器永远不会达到人类的智能水平,因为它们学习或执行智能任务的人工智能逻辑是由人类编程的,它们缺乏人类拥有的意识和自我意识。然而,一些研究人员提出了另一种观点,即人类意识和自我意识就像通过反馈从周围环境中学习的无限循环程序。因此,也有可能将意识和自我意识编程到机器中。然而,就目前而言,我们将把人工智能的这一哲学方面留到另一天,并将简单地讨论我们所知道的人工智能。
简而言之,人工智能可以被定义为机器(通常是计算机或机器人)以类似人类的智能执行任务的能力,拥有诸如推理、从经验中学习、归纳、破译含义和拥有视觉感知的能力等属性。我们将坚持这个更实际的定义,而不是看人工智能效应提出的哲学内涵和人工智能奇点的前景。虽然可能会有关于人工智能能实现什么和不能实现什么的争论,但最近基于人工智能的系统的成功故事已经铺天盖地。下图描述了人工智能最近的一些主流应用:

Figure 1.1: Applications of AI
这本书将涵盖人工智能所有核心学科项目的详细实施,概述如下:
- 基于迁移学习的人工智能系统
- 基于自然语言的人工智能系统
- 基于生成性对抗网络的应用
- 专家系统
- 视频到文本的翻译应用
- 基于人工智能的推荐系统
- 基于人工智能的移动应用
- 基于人工智能的聊天机器人
- 强化学习应用
在这一章中,我们将简要地涉及到机器学习和深度学习的概念,这些概念是实现将在以下章节中涵盖的项目所需要的。
神经网络
神经网络是受人脑启发的机器学习模型。它们由神经处理单元组成,并以分层的方式相互连接。这些神经处理单元被称为人工神经元,它们执行与人脑中轴突相同的功能。在人脑中,树突接收来自邻近神经元的输入,并在将输入传递到神经元的胞体之前衰减或放大输入。在神经元的胞体中,这些被修饰的信号被加在一起并传递到神经元的轴突上。如果轴突的输入超过特定的阈值,信号就会传递到邻近神经元的树突。
人工神经元的工作原理可能与生物神经元的工作原理相同。它接收来自邻近神经元的输入。输入通过神经元的输入连接进行缩放,然后相加在一起。最后,相加的输入通过激活函数,激活函数的输出传递给下一层的神经元。
为了进行比较,生物神经元和人工神经元在下图中进行了说明:

Figure 1.2: Biological neuron
下图显示了一个人工神经元:

Figure 1.3: Artificial neuron
现在,让我们看看人工神经网络的结构,如下图所示:

Figure 1.4: Artificial neural network
输入,即 x ∈ R N ,通过连续的神经单元层,以分层的方式排列。特定层中的每个神经元接收来自前一层神经元的输入,通过它们之间连接的权重进行衰减或放大。重量
对应于层 l 中的IthT8】神经元与层 (l+1) 中的jthT14】神经元之间的重量连接。此外,特定层中的每个神经元单元I**l都伴有偏差
。神经网络预测输出
,对于输入向量, x ∈ R N 。如果数据的实际标签是 y ,其中 y 取连续值,那么神经元网络通过最小化预测误差来学习权重和偏差,
。当然,对于所有标记的数据点,误差必须最小化: ( x i ,y i )∀ i ∈ 1,2,。。。m 。**
如果我们用一个共同的向量 W 来表示权重和偏差的集合,并且预测中的总误差用 C 来表示,那么通过训练过程,估计的 W 可以表示如下:

同样,预测输出
可以由输入的函数 x 表示,该函数由权重向量 W 参数化,如下所示:

这样一个预测输出连续值的公式叫做回归问题。
对于两类二进制分类,交叉熵损失被最小化而不是平方误差损失,并且网络输出正类的概率而不是输出。交叉熵损失可以表示如下:

这里, p i 是输出类的预测概率,给定输入 x ,并且可以表示为输入的函数, x ,由权重向量参数化,如下所示:

一般来说,对于多类分类问题(比如说 n 个类),交叉熵损失由下式给出:

这里,
是 j 第T4】类的输出标签,用于 i 第T8】数据点。
神经激活单位
神经网络中使用几种神经激活单元,这取决于体系结构和手头的问题。我们将讨论最常用的激活函数,因为这些函数在确定网络架构和性能方面起着重要作用。线性和 sigmoid 单位激活函数主要用于人工神经网络,直到 Hinton 等人发明的校正线性单位 ( ReLUs )彻底改变了神经网络的性能。
线性激活单元
一个线性激活单元输出被衰减的神经元的总输入,如下图所示:

Figure 1.5: Linear neuron
如果 x 是线性激活单元的总输入,那么输出 y 可以表示如下:

乙状结肠激活单位
sigmoid 激活单元、 y 的输出,作为其总输入的函数, x ,表示如下:

由于 sigmoid 激活单元响应是一个非线性函数,如下图所示,它用于在神经网络中引入非线性:

Figure 1.6: Sigmoid activation function
自然界中任何复杂过程的输入输出关系一般都是非线性的,因此,我们需要非线性激活函数通过神经网络对其进行建模。用于两类分类的神经网络的输出概率通常由 sigmoid 神经单元的输出给出,因为它输出从 0 到 1 的值。输出概率可以表示如下:

这里, x 表示输出层中 sigmoid 单元的总输入。
双曲正切激活函数
双曲正切激活函数 ( tanh )的输出 y 是其总输入 x 的函数,如下所示:

tanh 激活功能输出范围为[ -1 、 1 的值,如下图所示:

Figure 1.7: Tanh activation function
需要注意的是,sigmoid 和 tanh 激活函数在输入的小范围内都是线性的,超过这个范围,输出就会饱和。在饱和区,激活函数的梯度(相对于输入)非常小或接近于零;这意味着它们非常容易出现消失梯度问题。正如您稍后将看到的,神经网络从反向传播方法中学习,其中一层的梯度取决于后续层中激活单元的梯度,直到最终输出层。因此,如果激活单元中的单元工作在饱和区域,那么很少的误差被反向传播到神经网络的早期层。神经网络最小化预测误差,以便通过利用梯度来学习权重和偏差。这意味着,如果梯度很小或消失为零,那么神经网络将无法正确学习这些权重。
整流线性单元
当神经元的总输入大于零时,ReLU 的输出是线性的,当神经元的总输入为负时,输出为零。这个简单的激活函数为神经网络提供了非线性,同时,它提供了一个相对于总输入的恒定梯度。这种恒定的梯度有助于防止神经网络出现饱和或消失的梯度问题,如激活函数,如 sigmoid 和 tanh 激活单元。ReLU 功能输出(如图图 1.8 所示)可以表示如下:

ReLU 激活函数可以绘制如下:

Figure 1.8: ReLU activation function
ReLU 的一个限制是输入负值的零梯度。这可能会减慢训练速度,尤其是在初始阶段。漏 ReLU 激活函数(如图图 1.9 所示)在这种情况下非常有用,在这种情况下,输出和梯度都是非零的,即使输入为负值。泄漏 ReLU 输出函数可以表示如下:


参数是为泄漏 ReLU 激活函数提供的,而对于参数 ReLU,
是神经网络将通过训练学习的参数。下图显示了泄漏 ReLU 激活函数的输出:

Figure 1.9: Leaky ReLU activation function
softmax 激活单元
在多类分类问题的情况下, softmax 激活单元通常用于输出类概率。假设我们正在处理一个 n 类分类问题,对应于这些类的总输入由下式给出:

在这种情况下,软最大激活单元的 k 第 类的输出概率由以下公式给出:

还有其他几个激活功能,大部分是这些基本版本的变体。我们将在接下来的章节中讨论不同项目中遇到的问题。
训练神经网络的反向传播方法
在反向传播方法中,通过梯度下降技术训练神经网络,其中组合权重向量 W 迭代更新,如下所示:

这里,η是学习率, W (t+1) 和 W (t) 分别是迭代时的权重向量 (t+1) 和 (t) , ∇C(W (t) ) 是成本函数或误差函数相对于权重向量的梯度, W 由 w ∈ W 概括的个体权重或偏差的先前算法可以表示如下:

从前面的表达式中可以看出,梯度下降学习方法的核心依赖于计算成本函数或误差函数相对于每个权重的梯度。
从微分的链式法则我们知道,如果我们有 y = f(x),z = f(y) ,那么以下是正确的:

这个表达式可以推广到任何数量的变量。现在,让我们看看一个非常简单的神经网络,如下图所示,以便理解反向传播算法:

Figure 1.10: A network illustrating backpropagation
让网络的输入为二维向量,x =【x1x2T,对应的输出标签和预测分别为
和
。此外,让我们假设神经网络中的所有激活单元都是乙状结肠。让层 (l-1) 中连接任意单元 i 到层 l 中单元 j 的广义权重用
表示,层 l 中任意单元 i 中的偏差用
表示。让我们导出一个数据点的梯度;总梯度可以计算为训练(或小批量)中使用的所有数据点的总和。如果输出是连续的,那么可以选择损失函数 C 作为预测误差的平方:

由集合 W 累积表示的网络的权重和偏差可以通过相对于 W 向量最小化成本函数来确定,如下所示:

为了通过梯度下降迭代地执行成本函数的最小化,我们需要计算成本函数相对于每个权重的梯度 w ∈ W ,如下所示:

现在我们已经有了我们需要的一切,让我们计算成本函数的梯度, C ,相对于权重,
。利用微分的链式法则,我们得到如下结果:

现在让我们看看下面的公式:

正如你在前面的表达式中看到的,导数只不过是预测中的误差。通常,在回归问题的情况下,输出单元激活函数是线性的,因此以下表达式适用:

因此,如果我们计算成本函数相对于输出单位的总输入的梯度,它将是
。这仍然等于输出预测的误差。
作为传入权重和激活的函数,输出单元的总输入可以表示如下:

这意味着,
和成本函数相对于权重的导数,
,对输出层的输入有贡献是通过以下给出的:

如您所见,在计算成本函数的梯度时,相对于最终输出层之前的层中的权重,误差是反向传播的。当我们计算成本函数相对于广义权重的梯度时,这变得更加明显。我们取 j=1 和 k=2 对应的权重;也就是
。成本函数 C 相对于该权重的梯度可以表示如下:

现在,
,也就是说,
。
因此,一旦我们计算出成本函数相对于神经元总输入的梯度
,任何权重的梯度 w ,对总输入的贡献 s ,可以通过简单地乘以与权重相关的激活 z 来获得。
现在,成本函数相对于总输入的梯度
可以通过链式法则再次导出,如下所示:

由于神经网络的所有单元(输出单元除外)都是 sigmoid 激活函数,因此情况如下:


结合 (1) 、 (2) 、 (3) ,我们得到如下结果:

在前面导出的梯度表达式中,您可以看到预测中的误差
通过将其与相关激活和权重(根据微分链规则)相结合进行反向传播,以计算每一层权重的梯度,因此在人工智能术语中称为反向传播。
卷积神经网络
卷积神经网络 ( 中枢神经系统)利用卷积运算从具有相关拓扑结构的数据中提取有用信息。这最适合图像和音频数据。当输入图像通过卷积层时,产生几个输出图像,称为输出特征图。输出要素图检测要素。初始卷积层中的输出特征映射可以学习检测基本特征,例如边缘和颜色成分变化。
第二卷积层可以检测稍微复杂的特征,例如正方形、圆形和其他几何结构。当我们通过神经网络前进时,卷积层学习检测越来越复杂的特征。例如,如果我们有一个分类图像是猫还是狗的美国有线电视新闻网,神经网络底部的卷积层可能会学习检测头部、腿部等特征。
图 1.11 展示了一个 CNN 的架构图,它处理猫和狗的图像,以便对它们进行分类。图像通过一个卷积层,帮助检测相关特征,如边缘和颜色组成。ReLU 激活增加了非线性。跟随激活层的汇集层总结了局部邻域信息,以便提供一定量的平移不变性。在理想的有线电视新闻网中,这种卷积-激活-汇集操作在网络到达密集连接之前要执行几次:

Figure 1.11: CNN architecture
当我们通过这样一个具有几个卷积-激活-汇集操作的网络时,图像的空间分辨率降低,而输出特征图的数量在每个层中增加。卷积层中的每个输出特征图都与一个滤波器内核相关联,其权重通过 CNN 训练过程来学习。
在卷积运算中,滤波器核的翻转版本被放置在整个图像或特征图上,并且为输入图像或特征图上的每个位置计算滤波器核输入值与相应图像像素或特征图值的点积。已经习惯于普通图像处理的读者可能已经使用了不同的滤波器核,例如高斯滤波器、索贝尔边缘检测滤波器等,其中滤波器的权重是预先定义的。卷积神经网络的优点是通过训练过程确定不同的滤波器权重;这意味着,针对卷积神经网络正在处理的问题,滤波器被更好地定制。
当卷积运算涉及在输入的每个位置覆盖滤波器内核时,卷积被称为具有一个步长。如果我们选择跳过一个位置,同时覆盖滤波器内核,那么卷积是以两个步长执行的。一般来说,如果在将滤波器内核覆盖在输入上时跳过了 n 个位置,则卷积被认为是以 (n+1) 的步长执行的。大于 1 的步幅减小了卷积输出的空间维度。
通常,卷积层后面是汇集层,它基本上总结了邻域中的输出特征图激活,由汇集的感受野决定。例如,一个 2×2 感受野将收集四个相邻输出特征图激活的局部信息。对于最大池操作,选择四次激活的最大值作为输出,而对于平均池操作,选择四次激活的平均值。池化会降低要素地图的空间分辨率。例如,对于具有 2×2 感受野的 224×224 大小的特征图汇集操作,特征图的空间维度将减少到 112×112。
需要注意的一点是,卷积运算减少了每一层要学习的权重数量。例如,如果我们有一个空间维度为 224 x 224 的输入图像,并且下一层的期望输出是 224 x 224 的维度,那么对于具有全连接的传统神经网络,要学习的权重数是 224 x 224 x 224 x 224。对于输入输出维数相同的卷积层,我们需要学习的只是滤波器核的权重。因此,如果我们使用 3×3 的滤波器内核,我们只需要学习 9 个权重,而不是 224×224×224×224权重。这种简化是可行的,因为像图像和音频这样的结构在局部空间邻域中具有很高的相关性。
*输入图像经过几层卷积和汇集操作。随着网络的发展,要素地图的数量会增加,而图像的空间分辨率会降低。在卷积汇集层的末端,特征映射的输出被馈送到完全连接的层,随后是输出层。
输出单位取决于手头的任务。如果我们执行回归,则输出激活单元是线性的,而如果是二元分类问题,则输出单元是 sigmoid。对于多类分类,输出层是 softmax 单元。
在本书的所有图像处理项目中,我们将以某种形式使用卷积神经网络。
递归神经网络
递归神经网络 ( RNNs )在处理顺序或时间数据时非常有用,其中给定实例或位置的数据与先前时间步长或位置的数据高度相关。RNNs 在处理文本数据方面已经非常成功,因为给定实例中的一个单词与它前面的单词高度相关。在 RNN,在每个时间步长,网络执行相同的功能,因此,术语在其名称中重复出现。下图展示了 RNN 的架构:

Figure 1.12: RNN architecture
在每个给定的时间步, t ,一个记忆状态,hT5【t】是基于先前的状态, h t-1 ,在步骤 (t-1) 和输入, x t ,在时间步 t 。新状态下, h t , 用于预测输出, o t , at step t 。控制 RNNs 的方程式如下:


如果我们在预测一个句子中的下一个单词,那么函数 f 2 通常是词汇中单词的软最大函数。功能 f 1 可以是基于手边问题的任何激活功能。
在 RNN 中,步骤 t 中的输出误差试图校正由 k ∈ 1,2,.。。t-1 ,通过传播先前时间步骤中的错误。这有助于 RNN 了解彼此相距甚远的单词之间的长期依存关系。实际上,由于渐变问题的消失和爆发,通过 RNN 学习如此长的依赖关系并不总是可能的。
众所周知,神经网络是通过梯度下降来学习的,时间步长 t 中的单词与先前序列步长 k 中的单词之间的关系可以通过记忆状态
相对于记忆状态
∀ i 的梯度来学习。这用下面的公式表示:

如果从序列步骤 k 处的记忆状态
到序列步骤 (k+1) 处的记忆状态
的权重连接是由uii∈Whh给出的,则以下情况成立:

在上式中,
是在时间步长(k+1)时对记忆状态 i 的总输入,因此情况如下:


现在我们已经准备好了一切,很容易理解为什么消失梯度问题会出现在 RNN。从前面的等式 (3) 和(4)中,我们得到以下结果:

对于 RNNs,函数 f 2 通常为 sigmoid 或 tanh,其存在饱和问题,即低梯度超出输入值的指定范围。现在,由于 f 2 导数彼此相乘,如果激活函数的输入在饱和区操作,梯度
可以变为零,即使对于相对适中的值 (t-k) 。即使 f 2 函数不在饱和区运行,乙状结肠的 f 2 函数的梯度始终小于 1 ,因此很难学习序列中单词之间的遥远依赖关系。类似地,由于
因素,可能会出现爆炸性的梯度问题。假设台阶 t 和 k 之间的距离在 10 左右,而重量 u ii ,在 2 左右。在这种情况下,梯度将被放大两倍,2 10 = 1024,导致爆炸梯度问题。
长短期记忆(LSTM)细胞
消失的梯度问题在很大程度上由 RNNs 的修改版本处理,称为长短期记忆 ( LSTM )细胞。长短期存储单元的架构图如下:

Figure 1.13: LSTM architecture
LSTM 介绍了细胞状态,CtT3,除了记忆状态,htT7】,这些你在学习 RNNs 的时候就已经看到了。单元状态由三个门调节:忘记门、更新门和输出门。遗忘门决定从先前的单元状态 C t-1 保留多少信息,其输出表示如下:**

更新门的输出表示如下:

潜在的新候选小区状态
表示如下:

基于先前的单元状态和当前的潜在单元状态,通过以下方式给出更新的单元状态输出:

单元状态的信息并非全部传递到下一步,应释放多少单元状态到下一步由输出门决定。输出门的输出通过下式给出:

基于当前单元状态和输出门,通过以下方式给出传递到下一步的更新的存储器状态:

现在来了一个大问题:LSTM 如何避免消失的梯度问题?在 LSTM
的等价物由
给出,可以用乘积形式表示如下:

现在,单元状态单位的循环由下式给出:

由此,我们得出以下结论:

结果,梯度表达式
变成如下:

正如你所看到的,如果我们能保持遗忘细胞状态在 1 附近,梯度几乎不会减弱,LSTM 不会受到消失梯度问题的影响。
我们将在本书中看到的大多数文本处理应用程序都将使用 LSTM 版本的 RNNs。
生成性对抗网络
生成性对抗网络,俗称 GANs ,是通过生成器 G 学习特定概率分布的生成模型。生成器 G 用鉴别器 D 玩零和极小极大游戏,两者都在达到纳什均衡之前随着时间演变。生成器尝试生成与给定概率分布 P(x) 生成的样本相似的样本,而鉴别器 D 尝试将生成器 G 生成的假数据样本与原始分布的数据样本区分开来。生成器 G 试图通过转换从噪声分布 P(z) 提取的样本 z 来生成与来自 P(x) 、的样本相似的样本。鉴别器 D 在造假时学习将生成器 G 生成的样本标记为G(z); x 原属 P(x) 。在极小极大博弈的均衡状态下,生成器将学会产生与原始分布 P(x) 生成的样本相似的样本,从而出现以下情况:

下图说明了学习 MNIST 数字概率分布的 GAN 网络:

Figure 1.14: GAN architecture
鉴别器最小化的代价函数是二进制交叉熵,用于区分属于概率分布 P(x) 的真实数据点和生成器生成的虚假数据点(即 G(z) ):

生成器将尝试最大化由(1)给出的相同成本函数。这意味着,优化问题可以用效用函数 U(G,D) 公式化为一个极小极大玩家,如下图所示:

通常,为了测量给定概率分布与给定分布的匹配程度,使用了f-散度度量,例如库尔巴克-莱布勒 ( KL )散度、詹森香农散度和巴特查亚距离。例如,两个概率分布 P 和 Q 之间的 KL 散度由下式给出,其中期望是关于分布 P :

类似地, P 和 Q 之间的詹森香农散度如下:

现在回到 (2) ,表达式可以写成:

这里, G(x) 是发生器的概率分布。将期望扩展成它的积分形式,我们得到如下结果:

对于固定的发电机分布,G(x)**如果下列条件成立,效用函数相对于鉴别器将处于最小值:
*
将 (3) 中的 (5) 代入 D(x) ,得到如下结果:

现在,发电机的任务是最大化效用,
,或者最小化效用,
。
的表达可以重新安排如下:



因此,我们可以看到生成器最小化
相当于最小化真实分布 P(x) 和生成器生成的样本分布 G (即 G(x) )之间的詹森香农散度。
训练一个 GAN 不是一个简单的过程,在训练这样一个网络时,我们需要考虑几个技术因素。我们将使用先进的 g an 网络在第 4 章、使用 GAN构建时尚行业的跨域风格转移应用。
强化学习
强化学习是机器学习的一个分支,通过采取特定的行动,使机器和/或代理能够在特定的环境中最大化某种形式的奖励。强化学习不同于有监督和无监督学习。强化学习广泛应用于博弈论、控制系统、机器人学和其他新兴的人工智能领域。下图说明了强化学习问题中代理和环境之间的交互作用:

Figure 1.15: Agent-environment interaction in a reinforcement learning model
q 学习
我们现在来看一个流行的强化学习算法,叫做 Q 学习。q 学习用于为给定的有限马尔可夫决策过程确定最优动作选择策略。一个马尔可夫决策过程由一个状态空间 S 定义;一个动作空间,A;一个立竿见影的奖励集,R;下一个状态的概率, S (t+1) ,给定当前状态,S(t);一个电流动作,一个(t)T21;P(S(t+1)/S(t);r(t));和折扣系数
。下图说明了一个马尔可夫决策过程,其中下一个状态取决于当前状态和在当前状态下采取的任何操作:

Figure 1.16: A Markov decision process
假设我们有一系列状态、动作和相应的奖励,如下所示:

如果考虑长期奖励, R t ,在第 t 步,等于每一步的即时奖励之和,从 t 一直到结束,如下:

现在,一个马尔可夫决策过程是一个随机过程,不可能每次都得到同样的下一步, S (t+1) ,基于 S (t) 和a(t);所以,我们对未来的奖励应用一个折扣因子
。这意味着,长期奖励可以更好地表示如下:

既然在时间步 t 上,眼前的奖励已经实现了,要使长期奖励最大化,我们需要通过选择一个最优的动作,使时间步 t+1 (即 R t+1 上的长期奖励最大化。通过采取动作 a (t) 在状态 aS(t)预期的最大长期奖励由以下 Q 函数表示:

在每个状态下, s ∈ S ,Q-learning 中的代理试图采取一个动作,
,使其长期回报最大化。Q 学习算法是一个迭代过程,其更新规则如下:

如您所见,该算法的灵感来自长期奖励的概念,如 (1) 所示。
状态 s (t) 采取行动a(t)的整体累计奖励 Q(s (t) 、a (t) ) ,取决于眼前的奖励 r (t) ,以及我们在新的一步所能期望的最大长期奖励 s在马尔可夫决策过程中,新状态 s (t+1) 随机依赖于当前状态 s (t) ,通过形式的概率密度/质量函数P(S(t+1)/S(t)采取的行动;r (t) )* 。*
*该算法基于
的值,通过对旧的期望和新的长期奖励进行加权平均,不断更新期望的长期累积奖励。
一旦我们通过迭代算法构建了 Q(s,a) 函数,在基于给定状态 s 玩游戏时,我们可以采取最佳动作
,作为最大化 Q 函数的策略:

深度学习
在 Q 学习中,我们通常使用有限的一组状态和动作;这意味着,表格足以保存 Q 值和奖励。然而,在实际应用中,状态和适用动作的数量大多是无限的,需要更好的 Q 函数逼近器来表示和学习 Q 函数。这就是深层神经网络的拯救之处,因为它们是通用函数逼近器。我们可以用一个神经网络来表示 Q 函数,该网络将状态和动作作为输入,并提供相应的 Q 值作为输出。或者,我们可以仅使用状态来训练神经网络,并将输出作为对应于所有动作的 Q 值。下图说明了这两种情况。由于 Q 值是奖励,我们正在处理这些网络中的回归:

Figure 1.17: Deep Q-learning function approximator network
在本书中,我们将通过深度 Q 学习,使用强化学习来训练一辆赛车自己驾驶。
迁移学习
一般来说,迁移学习是指利用一个领域获得的知识来解决另一个领域的相关问题的概念。然而,在深度学习中,它具体指的是将为特定任务训练的神经网络重新用于不同领域中类似任务的过程。新任务使用从以前任务中学习的特征检测器,因此我们不必训练模型来学习它们。
由于不同层的单元之间的连接模式的性质,深度学习模型往往具有大量的参数。训练这么大的模型,需要相当大的数据量;否则,模型可能会因过度拟合而受损。对于许多需要深度学习解决方案的问题,大量数据将不可用。例如,在用于对象识别的图像处理中,深度学习模型提供了最先进的解决方案。在这种情况下,基于从现有的训练深度学习模型中学习的特征检测器,迁移学习可以用于创建特征。然后,这些特征可以用来用可用的数据建立一个简单的模型,以解决手头的新问题。因此,新模型需要学习的唯一参数是与构建简单模型相关的参数,从而减少过度拟合的机会。预处理后的模型通常是在一个庞大的数据集上训练的,因此,它们具有可靠的参数作为特征检测器。
当我们在中枢神经系统中处理图像时,初始层学习检测非常一般的特征,例如卷曲、边缘、颜色组成等。随着网络越来越深,更深层次的卷积层学习检测与特定类型数据集相关的更复杂的特征。我们可以使用一个预训练的网络,选择不训练前几层,因为它们学习非常通用的特性。相反,我们可以只专注于训练最后几层的参数,因为这些参数将学习到特定于当前问题的复杂特征。这将确保我们需要训练的参数更少,并且我们明智地使用数据,只训练所需的复杂参数,而不训练通用特征。
迁移学习广泛应用于通过中枢神经系统的图像处理,其中滤波器充当特征检测器。用于迁移学习的最常见的预处理中枢神经系统是AlexNet、VGG16、VGG19、Inception V3和ResNet等。下图说明了用于迁移学习的预处理VGG16网络:

Figure 1.18: Transfer learning with a pretrained VGG 16 network
由 x 表示的输入图像被馈送到预训练的 VGG 16 网络,并且从最后一个完全连接的层提取 4096 维输出特征向量x’、。提取的特征x’与对应的类标签 y 一起用于训练一个简单的分类网络,减少了解决问题所需的数据。
我们将通过使用第 2 章、迁移学习中的迁移学习来解决医疗保健领域的图像分类问题。
受限玻尔兹曼机器
受限玻尔兹曼机器 ( RBMs )是一类无监督的机器学习算法,学习数据的内部表示。一个 RBM 有一个可见层 v ∈ R m ,还有一个隐藏层 h ∈ R n 。RBMs 学习将可见层中的输入呈现为隐藏层中的低维表示。给定可见层输入,所有隐藏层单元都是有条件独立的。同样,给定隐藏层输入,所有可见层都是有条件独立的。这允许 RBM 独立地采样可见单元的输出,给定隐藏层输入,反之亦然。
下图展示了 RBM 的架构:

Figure 1.19: Restricted Boltzmann machines
重量 w ij ∈ W 将可见单位 i、连接到隐藏单位 j ,其中 W ∈ R m x n 是所有此类重量的集合,从可见单位到隐藏单位。可见单元中的偏差用 b i ∈ b 表示,而隐藏单元中的偏差用 c j ∈ c 表示。
受统计物理学中玻尔兹曼分布的启发,可见层矢量 v、和隐藏层矢量 h、的联合分布与组态的负能量指数成正比:
(1)
构型的能量由下式给出:
(2)
给定可见输入向量, v,,隐藏单元的概率, j,可以表示如下:
(2)
类似地,给定隐藏输入向量, h,的可见单位, i,的概率由下式给出:
(3)
因此,一旦我们通过训练了解了 RBM 的权重和偏差,给定隐藏状态,就可以对可见表示进行采样,而给定可见状态,就可以对隐藏状态进行采样。
类似于主成分分析 ( 主成分分析),径向基函数是一种在一个维度上表示数据的方式,由可见层 v、提供,进入不同的维度,由隐藏层 h 提供。当隐藏层的维数小于可见层的维数时,径向基函数网络执行降维任务。成果管理制通常以二进制数据为基础。
通过最大化训练数据的可能性来训练成果管理制。在成本函数相对于权重和偏差的梯度下降的每次迭代中,采样进入画面,这使得训练过程昂贵并且在计算上有些棘手。一种聪明的采样方法,叫做对比发散——使用吉布斯采样——被用来训练径向基函数。
我们将在第 6 章、智能推荐系统中使用 RBMs 来构建推荐系统。
自动编码器
很像 RBMs,自动编码器是一类无监督学习算法,旨在揭示数据中的隐藏结构。在主成分分析 ( 主成分分析)中,我们试图捕捉输入变量之间的线性关系,并试图通过采用(输入变量的)线性组合来表示降维空间中的数据,这些线性组合占了数据中的大部分方差。然而,主成分分析无法捕捉输入变量之间的非线性关系。
自动编码器是一种神经网络,可以捕捉输入变量之间的非线性相互作用,同时在隐藏层中以不同的维度表示输入。大多数情况下,隐藏层的尺寸小于输入层的尺寸。我们跳过了这一点,假设高维数据有一个固有的低维结构。例如,高维图像可以由低维流形表示,自动编码器通常用于发现该结构。下图说明了自动编码器的神经架构:

Figure 1.20: Autoencoder architecture
自动编码器有两个部分:编码器和解码器。编码器试图将输入数据 x、投影到隐藏层 h 中。解码器试图从隐藏层 h 重建输入。伴随这种网络的权重是通过最小化重构误差来训练的,即来自解码器的重构输入
和原始输入之间的误差。如果输入是连续的,则最小化重构误差的平方和,以便学习自动编码器的权重。
如果我们用一个函数来表示编码器, f W (x) ,用 f U (x) 来表示解码器,其中 W 和 U 是与编码器和解码器相关联的权重矩阵,那么情况如下:
(1)
(2)
重建误差, C,过训练集, x i ,i = 1,2,3,...m ,可以表示为:
(3)
自动编码器最佳权重
可以通过最小化 (3) 的成本函数来学习,如下所示:
(4)
自动编码器用于各种目的,例如学习数据的潜在表示、降噪和特征检测。降噪自动编码器将实际输入的噪声版本作为输入。他们试图构建实际的输入,作为重建的标签。同样,自动编码器可以用作创成式模型。一类可以作为创成式模型工作的自动编码器叫做变分自动编码器。目前,变分自动编码器和 GANs 作为图像处理的生成模型非常流行。
摘要
我们现在已经到了这一章的结尾。我们研究了人工神经网络的几种变体,包括用于图像处理的中枢神经系统和用于自然语言处理的神经网络。此外,我们将 RBMs 和 GANs 视为生成模型,将自动编码器视为无监督方法,以解决许多问题,例如降噪或破译数据的内部结构。此外,我们还谈到了强化学习,它对机器人和人工智能产生了巨大的影响。
现在,您应该已经熟悉了本书其余章节中构建智能人工智能应用程序时我们将使用的核心技术。在构建应用程序时,我们会在需要时进行一些小的技术上的离题。建议刚接触深度学习的读者更多地探索本章中涉及的核心技术,以获得更透彻的理解。
在后续章节中,我们将讨论实际的人工智能项目,并将使用本章中讨论的技术来实现它们。在第 2 章、迁移学习中,我们将从使用迁移学习实现医学图像分析的医疗保健应用开始。我们希望您能参与进来。***
二、迁移学习
迁移学习是将在特定领域的一项任务中获得的知识转移到相似领域的相关任务中的过程。在深度学习范式中,迁移学习一般指的是重用一个预先训练好的模型作为另一个问题的起点。计算机视觉和自然语言处理中的问题需要大量的数据和计算资源,来训练有意义的深度学习模型。迁移学习在视觉和文本领域已经变得非常重要,因为它减少了对大量训练数据和训练时间的需求。在本章中,我们将使用迁移学习来解决医疗保健问题。
我们将在本章中涉及的与迁移学习相关的一些关键主题如下:
- 使用迁移学习来检测人眼中的糖尿病视网膜病变状况,并确定视网膜病变的严重程度
- 探索先进的预训练卷积神经架构,该架构可用于训练能够检测人眼眼底图像中糖尿病视网膜病变的卷积神经网络 ( CNN )
- 查看实际实现 CNN 所需的不同图像预处理步骤
- 学习制定适合当前问题的成本函数
- 定义适当的度量标准来衡量训练模型的性能
- 使用仿射变换生成附加数据
- 训练的复杂性与适当的学习速率、优化器的选择等有关
- 浏览端到端 Python 实现
技术要求
你将需要具备 Python 3、TensorFlow、Keras 和 OpenCV 的基本知识。
本章代码文件可在 GitHub:
https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 02上找到
查看以下视频,查看正在运行的代码:
http://bit.ly/2t6LLyB
迁移学习导论
在传统的机器学习范例中(见图 2.1 ),每个用例或任务都是基于手头的数据独立建模的。在迁移学习中,我们使用从特定任务中获得的知识(以架构和模型参数的形式)来解决不同的(但相关的)任务,如下图所示:

Figure 2.1: Traditional machine learning versus transfer learning
吴恩达在他的 2016 NIPS 教程中指出,迁移学习将是机器学习商业成功的下一个大驱动力(在监督学习之后);这种说法一天比一天正确。迁移学习现在广泛应用于需要用人工神经网络解决的问题。因此,最大的问题是为什么会这样。
从头开始训练人工神经网络是一项艰巨的任务,主要有以下两个原因:
- 人工神经网络的成本面是非凸的;因此,它需要一组良好的初始权重来实现合理的收敛。
- 人工神经网络有很多参数,因此需要大量数据进行训练。不幸的是,对于许多项目来说,可用于训练神经网络的具体数据不足,而项目旨在解决的问题足够复杂,需要神经网络解决方案。
在这两种情况下,迁移学习都有帮助。如果我们使用预先训练好的模型,这些模型是在一个巨大的标记数据语料库上训练的,比如 ImageNet 或 CIFAR,那么涉及迁移学习的问题将有一组很好的初始权重来开始训练;然后可以根据手头的数据对这些权重进行微调。类似地,为了避免在少量数据上训练复杂模型,我们可能希望从预先训练的神经网络中提取复杂特征,然后使用这些特征训练相对简单的模型,例如 SVM 或逻辑回归模型。举个例子,如果我们正在处理一个图像分类问题,并且我们已经有了一个预先训练好的模型——比如说,一个在 1000 类 ImageNet 上的VGG16网络——我们可以通过VGG16的权重传递训练数据,并从最后一个池层提取特征。如果我们有 m 训练数据点,可以使用方程
,其中 x 为特征向量, y 为输出类。然后,我们可以从预先训练的VGG16网络中导出复杂的特征,例如向量 h ,如下所示:

这里, W 是预先训练好的VGG16网络的一组权重,一直到最后一个汇聚层。
然后,我们可以使用转换后的训练数据点集合
,来构建一个相对简单的模型。
迁移学习与糖尿病视网膜病变的检测
在本章中,我们将使用迁移学习来构建一个模型来检测人眼中的糖尿病视网膜病变。糖尿病视网膜病变通常见于糖尿病患者,高血糖水平会导致视网膜血管受损。下图左侧为正常视网膜,右侧为糖尿病视网膜病变:

Figure 2.2: A normal human retina versus a retina with diabetic retinopathy
在医疗保健中,糖尿病视网膜病变检测通常是一个手动过程,包括训练有素的医生检查彩色眼底视网膜图像。这在诊断过程中引入了延迟,经常导致延迟治疗。作为我们项目的一部分,我们将构建一个强大的人工智能系统,该系统可以拍摄视网膜的彩色眼底图像,并根据糖尿病视网膜病变对视网膜状况的严重程度进行分类。我们将视网膜图像分类到的不同条件如下:
- 0 :无糖尿病视网膜病变
- 1 :轻度糖尿病视网膜病变
- 2 :中度糖尿病视网膜病变
- 3 :重度糖尿病视网膜病变
- 4 :增生性糖尿病视网膜病变
糖尿病视网膜病变数据集
构建糖尿病视网膜病变检测应用程序的数据集从 Kaggle 获得,可从以下链接下载:https://www.kaggle.com/c/教室-糖尿病-视网膜病变-检测-竞赛/数据。
训练和保持测试数据集都存在于train_dataset.zip文件中,该文件在前面的链接中可用。
我们将使用标记的训练数据通过交叉验证来构建模型。我们将在保持数据集上评估模型。
由于我们正在处理类预测,准确性将是一个有用的验证指标。精确度定义如下:

这里, c 为正确分类样本数, N 为评估样本总数。
我们还将使用二次加权 kappa 统计数据来确定模型的质量,并与 Kaggle 标准相比,确定模型有多好的基准。二次加权 kappa 定义如下:

二次加权 kappa 表达式中的权重 (w i,j ) 如下:

在上式中,以下公式适用:
- N 代表班级数量
- O ij 表示已经被预测具有类别 i 的图像的数量,并且其中图像的实际类别是 j
- E ij 代表预测类的预期观测数,即 i ,实际类为 j ,假设预测类和实际类之间是独立的
为了更好地理解 kappa 指标的组成部分,让我们看看苹果和橘子的二元分类。我们假设预测类和实际类的混淆矩阵如下图所示:

Figure 2.3: Kappa metric terms
当真实标签为橙,假设标签之间独立时,预测苹果的预期计数由以下公式给出:

鉴于没有模型,这个预期计数是你能犯的最大错误。
如果您熟悉两个分类变量之间独立性的卡方检验,列联表每个单元格中的预期计数是基于相同的公式计算的,假设分类变量之间是独立的。
真实标签为橙时预测苹果的模型的观测计数可以从混淆矩阵中直接追溯,等于 5 ,如下:

因此,我们可以看到模型在预测橙色为苹果时产生的误差小于我们在不使用模型的情况下获得的误差。Kappa 通常衡量与没有模型的预测相比,我们做得有多好。
如果我们观察二次权重的表达式,( w i,j ),我们可以看到当实际标签和预测标签之间的差异较大时,权重的值较高。这是有道理的,因为类是有序的。例如,让我们用类标签零来表示处于完美状态的眼睛;轻度糖尿病视网膜病变伴有一种;中度糖尿病视网膜病变,伴有两种;和严重的糖尿病视网膜病变。当轻度糖尿病视网膜病变被错误地归类为重度糖尿病视网膜病变,而不是中度糖尿病视网膜病变时,这个二次项权重( w i,j )将会更高。这是有道理的,因为我们希望预测一个尽可能接近实际情况的条件,即使我们无法预测实际的类。
我们将使用sklearn.metrics.cohen_kappa_score与weights= "quadratic" 来计算卡帕分数。权重越高,kappa 分数越低。
建立损失函数
此使用案例的数据分为五类,分别为无糖尿病视网膜病变、轻度糖尿病视网膜病变、中度糖尿病视网膜病变、重度糖尿病视网膜病变和增生性糖尿病视网膜病变。因此,我们可以把它当作一个分类问题。对于我们的分类问题,输出标签需要进行一次热编码,如下所示:
- 无糖尿病视网膜病变:【1 0 0 0】T
- 轻度糖尿病视网膜病变:【0 1 0 0】T
- 中度糖尿病视网膜病变:【0 0 1 0】T
- 严重糖尿病视网膜病变:【0 0 1 0】T
- 增生性糖尿病视网膜病变:【0 0 0 0 1】T
Softmax 将是呈现输出层中不同类别概率的最佳激活函数,而每个数据点的分类交叉熵损失之和将是最佳优化损失。对于具有输出标签向量 y 和预测概率 p 的单个数据点,交叉熵损失由以下等式给出:

这里,
和
。
同样,训练数据点上的平均损失 M 可以表示如下:

在训练过程中,小批量的梯度基于 (2) 给出的平均对数损失,其中 M 是选择的批量大小。对于我们将结合验证精度监控的验证日志丢失, M 是验证集数据点的数量。因为我们将在每个折叠中进行 K 折叠交叉验证,所以我们将在每个折叠中有不同的验证数据集。
现在我们已经定义了训练方法、损失函数和验证度量,让我们继续进行数据探索和建模。
请注意,输出类中的分类是序数性质的,因为严重性随着类的不同而增加。由于这个原因,回归可能会派上用场。我们也会用回归代替分类来试试运气,看看结果如何。回归面临的挑战之一是将原始分数转换为类别。我们将使用一个简单的方案,将分数散列到最接近的整数严重性级别。
考虑到阶级不平衡
阶级不平衡是分类的一个主要问题。下图描述了五个严重性级别的级别密度:

Figure 2.4: Class densities of the five severity classes
从上图可以看出,近 73%的训练数据属于0 类,代表无糖尿病视网膜病变情况。因此,如果我们碰巧将所有数据点标记为0 级,那么我们将有 73%的准确率。这在患者健康状况下是不可取的。我们宁愿让一个测试说一个病人有某种健康状况,当它没有的时候(假阳性),也不愿让一个测试在它有健康状况的时候没有检测到某种健康状况(假阴性)。如果模型学会将所有点分类为属于0 级,那么 73%的准确率可能毫无意义。
检测更高的严重性等级比在无严重性等级上做得好更重要。使用对数损失或交叉熵代价函数的分类模型的问题是它偏向多数类。这是因为交叉熵误差源自最大似然原理,该原理倾向于将较高概率分配给多数类。我们可以做两件事:
- 丢弃样本较多的类中的数据,或者对低频类进行上采样,以保持样本在类之间的分布均匀。
- 在损失函数中,以与密度成反比的方式为类分配权重。这将确保当模型未能对低频类别进行分类时,低频类别对成本函数施加更高的惩罚。
我们将使用方案二,因为它不涉及生成更多数据或丢弃现有数据。如果我们将类别权重与类别频率的倒数成比例,我们会得到以下类别权重:
| 严重性等级 | 等级权重 |
| Class 0 | 0.0120353863 |
| Class 1 | 0.1271350558 |
| Class 2 | 0.0586961973 |
| Class 3 | 0.3640234214 |
| Class 4 | 0.4381974727 |
我们将在训练分类网络时使用这些权重。
预处理图像
不同类别的图像将存储在不同的文件夹中,因此很容易标记它们的类别。我们将使用Opencv功能读取图像,并将它们调整到不同的尺寸,例如 224 x 224 x 3。我们将根据 ImageNet 数据集,从每个图像中减去平均像素强度。这意味着减法将使糖尿病视网膜病变图像达到与处理后的 ImageNet 图像相同的强度范围,在该图像上训练预训练的模型。一旦每个图像被前置,它们将被存储在一个numpy数组中。图像预处理功能可定义如下:
def get_im_cv2(path,dim=224):
img = cv2.imread(path)
resized = cv2.resize(img, (dim,dim), cv2.INTER_LINEAR)
return resized
def pre_process(img):
img[:,:,0] = img[:,:,0] - 103.939
img[:,:,1] = img[:,:,0] - 116.779
img[:,:,2] = img[:,:,0] - 123.68
return img
通过opencv功能imread读取图像,然后使用行间插值方法将图像调整到(224,224,3)或任何给定的尺寸。ImageNet 图像的红色、绿色和蓝色通道中的平均像素强度分别为103.939、116.779和123.68;在从图像中减去这些平均值之后,训练预先训练的模型。这个平均减法的活动是用来集中数据的。以零为中心的数据有助于对抗消失和爆炸的梯度问题,这反过来有助于模型更快地收敛。此外,每个通道标准化有助于保持梯度均匀地流入每个通道。由于我们将在这个项目中使用预先训练的模型,因此在将图像输入预先训练的网络之前,根据通道平均像素值对图像进行平均校正是有意义的。然而,根据预训练网络所基于的 ImageNet 的平均值来校正图像并不是强制性的。您可以很好地通过项目训练语料库的平均像素强度进行归一化。
同样,您可以选择对整个图像进行均值归一化,而不是执行通道均值归一化。这需要从图像本身减去每个图像的平均值。想象一个场景,CNN 要识别的物体暴露在不同的光照条件下,比如白天和晚上。不管光线条件如何,我们都希望对物体进行正确分类,然而不同的像素强度会以不同的方式激活神经网络的神经元,从而导致对物体进行错误分类的可能性。然而,如果我们从图像本身减去每个图像的平均值,物体将不再受到不同照明条件的影响。因此,根据我们处理的图像的性质,我们可以为自己选择最佳的图像归一化方案。然而,任何默认的规范化方式都倾向于给出合理的性能。
使用仿射变换的附加数据生成
我们将使用keras ImageDataGenerator 来生成附加数据,使用仿射变换对图像进行像素坐标。我们将主要使用的转换是旋转、平移和缩放。如果像素空间坐标由x =【x1x2T∈R2定义,则像素的新坐标可由下式给出:

这里M = R2x 2T3】为仿射变换矩阵,b =【b1b2T∈R2T13】为平移向量。**
术语 b 1 指定沿着一个空间方向的平移,而 b 2 提供沿着另一个空间维度的平移。
这些变换是必需的,因为神经网络通常不是平移不变、旋转不变或尺度不变的。汇集操作确实提供了一些平移不变性,但这通常是不够的。神经网络不会将图像中特定位置的一个对象和另一个图像中平移位置的同一对象视为同一事物。这就是为什么我们需要一个图像在不同翻译位置的几个实例,以便神经网络更好地学习。同样的解释也适用于旋转和缩放。
旋转
下面是旋转的仿射变换矩阵,其中 θ 代表旋转的角度:

在这种情况下,平移向量 b、为零。我们可以通过选择一个非零的 b 来获得旋转和平移。
例如,下图显示了一张视网膜照片,然后将同一张照片旋转 90 度:

Figure 2.5: Rotated photo of the retina
翻译
对于平移,仿射变换矩阵是单位矩阵,平移向量 b、具有非零值:


例如,对于沿垂直方向的五个像素位置和沿水平方向的三个像素位置的平移,我们可以使用 b= [5 3] T ,以 M 作为单位矩阵。
以下是视网膜沿图像的宽度和高度平移 24 个像素位置的图像:

Figure 2.5: Image translation of the retina
缩放比例
可以通过对角矩阵M∈R2x 2T3】进行缩放,如下图所示:

这里 S v 表示垂直方向的比例因子, S h 表示水平方向的比例因子(图示见图 2.6 )。我们也可以选择通过一个非零的平移向量 b 来跟进平移的缩放:

Figure 2.6 Image scaling of the retina
反射
通过变换矩阵T∈R2x 2T5】可以得到直线 L、与水平线成θ角的反射,如下:

下图显示了视网膜照片的水平翻转:

Figure 2.7: Horizontal flip of a retina photo
通过仿射变换的附加图像生成
keras图像生成器将使用以下类来完成我们的任务:
datagen = ImageDataGenerator(
horizontal_flip = True,
vertical_flip = True,
width_shift_range = 0.1,
height_shift_range = 0.1,
channel_shift_range=0,
zoom_range = 0.2,
rotation_range = 20)
从定义的生成器中可以看到,我们启用了水平和垂直翻转,这只是图像分别沿水平和垂直轴的反射。类似地,我们已经定义了沿着宽度和高度的图像平移在沿着这些方向的像素位置的 10%以内。旋转范围被限制在20度的角度,而比例因子被定义为在原始图像的0.8到1.2之间。
网络体系结构
我们现在将实验预训练的ResNet50、InceptionV3和VGG16网络,并找出哪一个给出最好的结果。每个预训练模型的权重都基于 ImageNet。我已经提供了 ResNet、InceptionV3和VGG16架构的原始论文的链接,以供参考。建议读者仔细阅读这些论文,深入了解这些架构以及它们之间的细微差别。
VGG 论文链接如下:
- 标题 : 用于大规模图像识别的超深度卷积网络
- 链接:https://arxiv.org/abs/1409.1556
ResNet 论文链接如下:
- 标题 : 图像识别的深度残差学习
- 链接:https://arxiv.org/abs/1512.03385
InceptionV3 论文链接如下:
- 标题 : 重新思考计算机视觉的初始架构
- 链接:https://arxiv.org/abs/1512.00567
简单解释一下,VGG16是一个 16 层的 CNN,使用 3×3 的滤镜和 2×2 的感受野进行卷积。整个网络中使用的激活功能都是 ReLUs。由 Simonyan 和 Zisserman 开发的 VGG 建筑在 ILSVRC 2014 竞赛中获得亚军。VGG16网络因其简单性获得了大量的普及,是目前最流行的从图像中提取特征的网络。
ResNet50是一个贯彻残块思想的深度 CNN,与VGG16网络截然不同。经过一系列卷积激活池操作后,该模块的输入再次反馈到输出端。ResNet 架构是何等人开发的,虽然有 152 层,但比网络的复杂度低。该架构以 3.57%的前五名错误率赢得了 ILSVRC 2015 竞赛,优于该竞赛数据集上的人类级性能。通过检查目标是否在具有最高概率的五个类别预测中来计算前五个错误率。原则上,ResNet 网络会尝试学习残差映射,而不是直接从输出映射到输入,如下残差框图所示:

Figure 2.8: Residual block of ResNet models
InceptionV3 是来自谷歌的最先进的 CNN。InceptionV3 架构不是在每一层使用固定大小的卷积滤波器,而是使用不同大小的滤波器来提取不同粒度级别的特征。下图说明了第 3 层的卷积块:

Figure 2.9: InceptionV3 convolution block
盗梦空间 V1 公司是 2014 年 ILSVRC 竞赛的获胜者。其前 5%的错误率非常接近人类水平的表现,为 6.67%。
VGG16 传输学习网络
我们将从预先训练的VGG16网络中的最后一个池层获取输出,并添加两个完全连接的层,每个层 512 个单元,然后是输出层。最终汇集层的输出从完全连接层之前的全局平均汇集操作传递。我们可以只展平池层的输出,而不是执行全局平均池化——其思想是确保池化的输出不是二维点阵格式,而是一维数组格式,很像完全连接的层。下图说明了基于预训练VGG16的新VGG16、的架构:

Figure 2.10: The VGG16 transfer learning network
如上图所示,我们将从预训练网络中的最后一个最大池层提取输出,并在最终输出层之前附加两个完全连接的层。基于前面的架构,VGG 定义函数可以定义为如下代码块所示,使用keras:
def VGG16_pseudo(dim=224,freeze_layers=10,full_freeze='N'):
# model_save_dest = {}
model = VGG16(weights='imagenet',include_top=False)
x = model.output
x = GlobalAveragePooling2D()(x)
x = Dense(512, activation='relu')(x)
x = Dropout(0.5)(x)
x = Dense(512, activation='relu')(x)
x = Dropout(0.5)(x)
out = Dense(5,activation='softmax')(x)
model_final = Model(input = model.input,outputs=out)
if full_freeze != 'N':
for layer in model.layers[0:freeze_layers]:
layer.trainable = False
return model_final
我们将使用在 ImageNet 上训练的预训练VGG16的权重作为模型的初始权重,然后对模型进行微调。我们还冻结了前几层的权重(10是默认的),因为在 CNN 中,前几层学习检测一般特征,如边缘、颜色组成等。因此,不同领域的特性不会有很大的不同。冻结层是指不训练特定于该层的权重。我们可以试验冻结的层数,并选择提供最佳验证分数的层数。由于我们正在执行多类别分类,因此已为输出层选择了 softmax 激活功能。
第三代迁移学习网络
我们任务的InceptionV3网络在下面的代码块中定义。需要注意的一点是,由于InceptionV3是一个更深的网络,我们可以有更多的初始层。在数据可用性方面,不训练每个模型中所有层的想法还有另一个优点。如果我们使用较少的数据训练,整个网络的权重可能会导致过度拟合。冻结图层减少了要训练的权重数量,因此提供了一种正则化形式。
由于初始图层学习通用要素而不考虑问题的领域,因此它们是冻结的最佳图层。我们还在全连接层使用了压降,以防止过度匹配:
def inception_pseudo(dim=224,freeze_layers=30,full_freeze='N'):
model = InceptionV3(weights='imagenet',include_top=False)
x = model.output
x = GlobalAveragePooling2D()(x)
x = Dense(512, activation='relu')(x)
x = Dropout(0.5)(x)
x = Dense(512, activation='relu')(x)
x = Dropout(0.5)(x)
out = Dense(5,activation='softmax')(x)
model_final = Model(input = model.input,outputs=out)
if full_freeze != 'N':
for layer in model.layers[0:freeze_layers]:
layer.trainable = False
return model_final
ResNet50 迁移学习网络
迁移学习的ResNet50模型可以类似于VGG16和InceptionV3网络来定义,如下所示:
def resnet_pseudo(dim=224,freeze_layers=10,full_freeze='N'):
# model_save_dest = {}
model = ResNet50(weights='imagenet',include_top=False)
x = model.output
x = GlobalAveragePooling2D()(x)
x = Dense(512, activation='relu')(x)
x = Dropout(0.5)(x)
x = Dense(512, activation='relu')(x)
x = Dropout(0.5)(x)
out = Dense(5,activation='softmax')(x)
model_final = Model(input = model.input,outputs=out)
if full_freeze != 'N':
for layer in model.layers[0:freeze_layers]:
layer.trainable = False
return model_final
优化器和初始学习率
亚当优化器(自适应矩估计器)用于实现随机梯度下降高级版本的训练。Adam 优化器负责成本函数中的曲率,同时,它使用动量来确保朝着良好的局部最小值稳步前进。对于手头的问题,由于我们正在使用迁移学习,并且希望使用尽可能多的来自预训练网络的先前学习的特征,因此我们将使用小的初始学习率0.00001。这将确保网络不会失去通过预先训练的网络学习到的有用特性,并根据手头问题的新数据,不那么激进地微调到最佳点。Adam 优化器可以定义如下:
adam = optimizers.Adam(lr=0.00001, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.0)
beta_1参数控制当前梯度在动量计算中的贡献,而beta_2参数控制梯度归一化中梯度平方的贡献,这有助于解决代价函数中的曲率。
交叉验证
由于训练数据集很小,我们将执行五重交叉验证,以更好地了解模型推广到新数据的能力。我们还将在训练中使用交叉验证的不同折叠中构建的所有五个模型进行推断。测试数据点属于类别标签的概率是所有五个模型的平均概率预测,表示如下:

因为目标是预测实际的类而不是概率,所以我们会选择具有最大概率的类。当我们使用基于分类的网络和成本函数时,这种方法有效。如果我们把这个问题当作一个回归问题,那么这个过程会有一些改变,我们将在后面讨论。
基于验证日志丢失的模型检查点
当选择用于评估的验证分数提高时,保存模型总是一个好的做法。对于我们的项目,我们将跟踪验证日志的丢失,并随着不同时期验证分数的提高保存模型。这样,在训练之后,我们将保存提供最佳验证分数的模型权重,而不是停止训练时的最终模型权重。训练将继续,直到达到为训练定义的最大时期数,或者直到连续10个时期的验证日志丢失没有减少。当3时期的验证日志丢失没有改善时,我们也会降低学习率。以下代码块可用于执行学习速率降低和检查点操作:
reduce_lr = keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.50,
patience=3, min_lr=0.000001)
callbacks = [
EarlyStopping(monitor='val_loss', patience=10, mode='min', verbose=1),
CSVLogger('keras-5fold-run-01-v1-epochs_ib.log', separator=',', append=False),reduce_lr,
ModelCheckpoint(
'kera1-5fold-run-01-v1-fold-' + str('%02d' % (k + 1)) + '-run-' + str('%02d' % (1 + 1)) + '.check',
monitor='val_loss', mode='min', # mode must be set to max or keras will be confused
save_best_only=True,
verbose=1)
]
正如您在前面的代码块中看到的,如果在3 ( patience=3)时代验证损失没有改善,学习率会降低一半(0.50)。同样,如果在10 ( patience = 10)时期验证损失没有减少,我们将停止培训(通过执行EarlyStopping)。只要验证日志丢失减少,模型就会被保存,如下面的代码片段所示:
'kera1-5fold-run-01-v1-fold-' + str('%02d' % (k + 1)) + '-run-' + str('%02d' % (1 + 1)) + '.check'
在keras-5fold-run-01-v1-epochs_ib.log 日志文件中跟踪训练过程每个时期的验证日志丢失,如果验证日志丢失有所改善,则参考该日志文件以保存模型,或者决定何时降低学习率或停止训练。
在用户定义的路径中使用keras save功能保存每个文件夹中的模型,而在推理过程中,使用keras.load_model功能将模型加载到内存中。
培训过程的 Python 实现
下面的 Python 代码块显示了培训过程的端到端实现。它由前面章节中讨论的所有功能块组成。让我们从调用所有必需的 Python 包开始,如下所示:
import numpy as np
np.random.seed(1000)
import os
import glob
import cv2
import datetime
import pandas as pd
import time
import warnings
warnings.filterwarnings("ignore")
from sklearn.model_selection import KFold
from sklearn.metrics import cohen_kappa_score
from keras.models import Sequential,Model
from keras.layers.core import Dense, Dropout, Flatten
from keras.layers.convolutional import Convolution2D, MaxPooling2D, ZeroPadding2D
from keras.layers import GlobalMaxPooling2D,GlobalAveragePooling2D
from keras.optimizers import SGD
from keras.callbacks import EarlyStopping
from keras.utils import np_utils
from sklearn.metrics import log_loss
import keras
from keras import __version__ as keras_version
from keras.applications.inception_v3 import InceptionV3
from keras.applications.resnet50 import ResNet50
from keras.applications.vgg16 import VGG16
from keras.preprocessing.image import ImageDataGenerator
from keras import optimizers
from keras.callbacks import EarlyStopping, ModelCheckpoint, CSVLogger, Callback
from keras.applications.resnet50 import preprocess_input
import h5py
import argparse
from sklearn.externals import joblib
import json
导入所需库后,我们可以定义TransferLearning类:
class TransferLearning:
def __init__(self):
parser = argparse.ArgumentParser(description='Process the inputs')
parser.add_argument('--path',help='image directory')
parser.add_argument('--class_folders',help='class images folder
names')
parser.add_argument('--dim',type=int,help='Image dimensions to
process')
parser.add_argument('--lr',type=float,help='learning
rate',default=1e-4)
parser.add_argument('--batch_size',type=int,help='batch size')
parser.add_argument('--epochs',type=int,help='no of epochs to
train')
parser.add_argument('--initial_layers_to_freeze',type=int,help='the
initial layers to freeze')
parser.add_argument('--model',help='Standard Model to
load',default='InceptionV3')
parser.add_argument('--folds',type=int,help='num of cross
validation folds',default=5)
parser.add_argument('--outdir',help='output directory')
args = parser.parse_args()
self.path = args.path
self.class_folders = json.loads(args.class_folders)
self.dim = int(args.dim)
self.lr = float(args.lr)
self.batch_size = int(args.batch_size)
self.epochs = int(args.epochs)
self.initial_layers_to_freeze = int(args.initial_layers_to_freeze)
self.model = args.model
self.folds = int(args.folds)
self.outdir = args.outdir
接下来,让我们定义一个可以读取图像并将它们调整到合适尺寸的函数,如下所示:
def get_im_cv2(self,path,dim=224):
img = cv2.imread(path)
resized = cv2.resize(img, (dim,dim), cv2.INTER_LINEAR)
return resized
# Pre Process the Images based on the ImageNet pre-trained model
Image transformation
def pre_process(self,img):
img[:,:,0] = img[:,:,0] - 103.939
img[:,:,1] = img[:,:,0] - 116.779
img[:,:,2] = img[:,:,0] - 123.68
return img
# Function to build X, y in numpy format based on the
train/validation datasets
def read_data(self,class_folders,path,num_class,dim,train_val='train'):
print(train_val)
train_X,train_y = [],[]
for c in class_folders:
path_class = path + str(train_val) + '/' + str(c)
file_list = os.listdir(path_class)
for f in file_list:
img = self.get_im_cv2(path_class + '/' + f)
img = self.pre_process(img)
train_X.append(img)
train_y.append(int(c.split('class')[1]))
train_y = keras.utils.np_utils.to_categorical(np.array(train_y),num_class)
return np.array(train_X),train_y
接下来,我们将从InceptionV3开始定义迁移学习的三个模型:
def inception_pseudo(self,dim=224,freeze_layers=30,full_freeze='N'):
model = InceptionV3(weights='imagenet',include_top=False)
x = model.output
x = GlobalAveragePooling2D()(x)
x = Dense(512, activation='relu')(x)
x = Dropout(0.5)(x)
x = Dense(512, activation='relu')(x)
x = Dropout(0.5)(x)
out = Dense(5,activation='softmax')(x)
model_final = Model(input = model.input,outputs=out)
if full_freeze != 'N':
for layer in model.layers[0:freeze_layers]:
layer.trainable = False
return model_final
然后,我们将为迁移学习定义ResNet50模型:
def resnet_pseudo(self,dim=224,freeze_layers=10,full_freeze='N'):
model = ResNet50(weights='imagenet',include_top=False)
x = model.output
x = GlobalAveragePooling2D()(x)
x = Dense(512, activation='relu')(x)
x = Dropout(0.5)(x)
x = Dense(512, activation='relu')(x)
x = Dropout(0.5)(x)
out = Dense(5,activation='softmax')(x)
model_final = Model(input = model.input,outputs=out)
if full_freeze != 'N':
for layer in model.layers[0:freeze_layers]:
layer.trainable = False
return model_final
最后,我们将定义VGG16模型:
def VGG16_pseudo(self,dim=224,freeze_layers=10,full_freeze='N'):
model = VGG16(weights='imagenet',include_top=False)
x = model.output
x = GlobalAveragePooling2D()(x)
x = Dense(512, activation='relu')(x)
x = Dropout(0.5)(x)
x = Dense(512, activation='relu')(x)
x = Dropout(0.5)(x)
out = Dense(5,activation='softmax')(x)
model_final = Model(input = model.input,outputs=out)
if full_freeze != 'N':
for layer in model.layers[0:freeze_layers]:
layer.trainable = False
return model_final
现在,让我们定义训练函数,如下所示:
def train_model(self,train_X,train_y,n_fold=5,batch_size=16,epochs=40,
dim=224,lr=1e-5,model='ResNet50'):
model_save_dest = {}
k = 0
kf = KFold(n_splits=n_fold, random_state=0, shuffle=True)
for train_index, test_index in kf.split(train_X):
k += 1
X_train,X_test = train_X[train_index],train_X[test_index]
y_train, y_test = train_y[train_index],train_y[test_index]
if model == 'Resnet50':
model_final =
self.resnet_pseudo(dim=224,freeze_layers=10,full_freeze='N')
if model == 'VGG16':
model_final =
self.VGG16_pseudo(dim=224,freeze_layers=10,full_freeze='N')
if model == 'InceptionV3':
model_final = self.inception_pseudo(dim=224,freeze_layers=10,full_freeze='N')
datagen = ImageDataGenerator(
horizontal_flip = True,
vertical_flip = True,
width_shift_range = 0.1,
height_shift_range = 0.1,
channel_shift_range=0,
zoom_range = 0.2,
rotation_range = 20)
adam = optimizers.Adam(lr=lr, beta_1=0.9, beta_2=0.999,
epsilon=1e-08, decay=0.0)
model_final.compile(optimizer=adam,
loss= ["categorical_crossentropy"],metrics=['accuracy'])
reduce_lr = keras.callbacks.ReduceLROnPlateau(monitor='val_loss',
factor=0.50, patience=3, min_lr=0.000001)
callbacks = [
EarlyStopping(monitor='val_loss', patience=10, mode='min',
verbose=1),
CSVLogger('keras-5fold-run-01-v1-epochs_ib.log',
separator=',', append=False),reduce_lr,
ModelCheckpoint(
'kera1-5fold-run-01-v1-fold-' + str('%02d' % (k + 1)) +
'-run-' + str('%02d' % (1 + 1)) + '.check',
monitor='val_loss', mode='min',
save_best_only=True,
verbose=1)
]
model_final.fit_generator(datagen.flow(X_train,y_train,
batch_size=batch_size),
steps_per_epoch=X_train.shape[0]/batch_size, epochs=epochs,
verbose=1, validation_data= (X_test,y_test),
callbacks=callbacks, class_weight=
{0:0.012,1:0.12,2:0.058,3:0.36,4:0.43})
model_name = 'kera1-5fold-run-01-v1-fold-' + str('%02d' % (k +
1)) + '-run-' + str('%02d' % (1 + 1)) + '.check'
del model_final
f = h5py.File(model_name, 'r+')
del f['optimizer_weights']
f.close()
model_final = keras.models.load_model(model_name)
model_name1 = self.outdir + str(model) + '___' + str(k)
model_final.save(model_name1)
model_save_dest[k] = model_name1
return model_save_dest
我们还将为保持数据集定义一个inference函数,如下所示:
def inference_validation(self,test_X,test_y,model_save_dest,
n_class=5,folds=5):
pred = np.zeros((len(test_X),n_class))
for k in range(1,folds + 1):
model = keras.models.load_model(model_save_dest[k])
pred = pred + model.predict(test_X)
pred = pred/(1.0*folds)
pred_class = np.argmax(pred,axis=1)
act_class = np.argmax(test_y,axis=1)
accuracy = np.sum([pred_class == act_class])*1.0/len(test_X)
kappa = cohen_kappa_score(pred_class,act_class,weights='quadratic')
return pred_class,accuracy,kappa
现在,让我们调用main函数,来触发训练过程,如下所示:
def main(self):
start_time = time.time()
self.num_class = len(self.class_folders)
if self.mode == 'train':
print("Data Processing..")
file_list,labels=
self.read_data(self.class_folders,self.path,self.num_class,
self.dim,train_val='train')
print(len(file_list),len(labels))
print(labels[0],labels[-1])
self.model_save_dest =
self.train_model(file_list,labels,n_fold=self.folds,
batch_size=self.batch_size,
epochs=self.epochs,dim=self.dim,
lr=self.lr,model=self.model)
joblib.dump(self.model_save_dest,f'{self.outdir}/model_dict.pkl')
print("Model saved to dest:",self.model_save_dest)
else:
model_save_dest = joblib.load(self.model_save_dest)
print('Models loaded from:',model_save_dest)
# Do inference/validation
test_files,test_y =
self.read_data(self.class_folders,self.path,self.num_class,
self.dim,train_val='validation')
test_X = []
for f in test_files:
img = self.get_im_cv2(f)
img = self.pre_process(img)
test_X.append(img)
test_X = np.array(test_X)
test_y = np.array(test_y)
print(test_X.shape)
print(len(test_y))
pred_class,accuracy,kappa =
self.inference_validation(test_X,test_y,model_save_dest,
n_class=self.num_class,folds=self.folds)
results_df = pd.DataFrame()
results_df['file_name'] = test_files
results_df['target'] = test_y
results_df['prediction'] = pred_class
results_df.to_csv(f'{self.outdir}/val_resuts_reg.csv',index=False)
print("-----------------------------------------------------")
print("Kappa score:", kappa)
print("accuracy:", accuracy)
print("End of training")
print("-----------------------------------------------------")
print("Processing Time",time.time() - start_time,' secs')
我们可以改变几个参数,比如学习速率、批量大小、图像大小等等,我们可以实验,想出一个像样的模型。在训练阶段,模型位置保存在写入dict_model文件的model_save_dest字典中。
在推断阶段,模型只是基于训练好的模型对新的测试数据进行预测。
名为TransferLearning.py的迁移学习脚本可以如下调用:
python TransferLearning.py --path '/media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/book AI/Diabetic Retinopathy/Extra/assignment2_train_dataset/' --class_folders '["class0","class1","class2","class3","class4"]' --dim 224 --lr 1e-4 --batch_size 16 --epochs 20 --initial_layers_to_freeze 10 --model InceptionV3 --folds 5 --outdir '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/'
脚本的输出日志如下:
Model saved to dest: {1: '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/categorical/InceptionV3___1', 2: '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/categorical/InceptionV3___2', 3: '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/categorical/InceptionV3___3', 4: '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/categorical/InceptionV3___4', 5: '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/categorical/InceptionV3___5'}
validation
-----------------------------------------------------
Kappa score: 0.42969781637876836
accuracy: 0.5553973227000855
End of training
-----------------------------------------------------
Processing Time 26009.3344039917 secs
从日志中的结果可以看出,我们获得了约为56%的不错的交叉验证精度和约为0.43的二次卡帕值。
在这个脚本中,我们已经将所有数据加载到内存中,然后将ImageDataGenerator中的增强图像馈送到模型中进行训练。如果训练图像集很少和/或维数适中,那么将数据加载到内存中可能不是很重要。然而,如果图像库很大和/或我们的资源有限,将所有数据加载到内存中并不是一个可行的选择。由于运行这些实验的机器有 64 GB 的内存,我们能够毫无问题地训练这些模型。即使是 16 GB 内存的机器也可能不足以通过将所有数据加载到内存中来运行这些实验,并且您可能会遇到内存错误。
问题是,我们是否需要一次性将所有数据加载到内存中?
由于神经网络使用小批量,我们将只需要对应于一个小批量的数据来通过反向传播一次训练模型。类似地,对于下一个反向传播,我们可以丢弃对应于当前批次的数据,转而处理下一个批次。所以在某种程度上,每个小批量的内存需求只是对应于该批量的数据。因此,我们可以通过在训练时创建动态批处理来避免在内存较少的机器上训练深度学习模型。Keras 有一个很好的功能,可以在训练时创建动态批处理,我们将在下一节中讨论。
培训期间动态创建小批量
仅加载与小批量对应的数据的方法之一是通过从图像的位置随机处理图像来动态创建小批量。小批量处理的图像数量将等于我们指定的小批量大小。当然,在训练过程中会有一些瓶颈,因为在训练期间会产生动态的小批量,但是这个瓶颈可以忽略不计。特别是keras等包,具有高效的动态批量创建机制。我们将利用 keras 中的flow_from_directory功能在训练期间动态创建小批量,以减少训练过程中的内存需求。我们仍将继续使用ImageDataGenerator进行图像增强。列车发生器和验证发生器可以定义如下。
从三个通道中减去平均图像像素强度的图像预处理步骤通过将pre_process函数作为输入馈送到ImageDataGenerator的preprocessing_function来完成:
def pre_process(img):
img[:,:,0] = img[:,:,0] - 103.939
img[:,:,1] = img[:,:,0] - 116.779
img[:,:,2] = img[:,:,0] - 123.68
return img
train_file_names = glob.glob(f'{train_dir}/*/*')
val_file_names = glob.glob(f'{val_dir}/*/*')
train_steps_per_epoch = len(train_file_names)/float(batch_size)
val_steps_per_epoch = len(val_file_names)/float(batch_size)
train_datagen =
ImageDataGenerator(horizontal_flip =
True,vertical_flip =
True,width_shift_range =
0.1,height_shift_range = 0.1,
channel_shift_range=0,zoom_range = 0.2,
rotation_range = 20,
preprocessing_function=pre_process)
val_datagen =
ImageDataGenerator(preprocessing_function=pre_process)
train_generator =
train_datagen.flow_from_directory(train_dir,
target_size=(dim,dim),
batch_size=batch_size,
class_mode='categorical')
val_generator =
val_datagen.flow_from_directory(val_dir,
target_size=(dim,dim),
batch_size=batch_size,
class_mode='categorical')
print(train_generator.class_indices)
joblib.dump(train_generator.class_indices,
f'{self.outdir}/class_indices.pkl')
flow_from_directory函数接收一个图像目录作为输入,并期望在图像目录中有一个属于某个类的文件夹。然后,它从文件夹名称推断类标签。如果图像目录具有以下图像目录结构,则类被推断为0、1、2、3、4,属于类文件夹'class0'、'class1'、'class2'、'class3'和'class4'。
flow_from_directory功能的其他重要输入是batch_size、target_size和class_mode。target_size是指定要馈送到神经网络的图像的维度,而class_mode是指定问题的性质。对于二进制分类class_mode设置为二进制,而对于多类分类,同样设置为categorical。
接下来,我们将通过创建动态批处理来训练相同的模型,而不是一次将所有数据加载到内存中。我们只需要使用flow_from_directory选项创建一个生成器,并将其绑定到数据增强对象。数据生成器对象可以按如下方式生成:
# Pre processing for channel wise mean pixel subtraction
def pre_process(img):
img[:,:,0] = img[:,:,0] - 103.939
img[:,:,1] = img[:,:,0] - 116.779
img[:,:,2] = img[:,:,0] - 123.68
return img
# Add the pre_process function at the end of the ImageDataGenerator,
#rest all of the data augmentation options
# remain the same.
train_datagen =
ImageDataGenerator(horizontal_flip = True,vertical_flip = True,
width_shift_range = 0.1,height_shift_range = 0.1,
channel_shift_range=0,zoom_range =
0.2,rotation_range = 20,
preprocessing_function=pre_process)
# For validation no data augmentation on image mean subtraction preprocessing
val_datagen = ImageDataGenerator(preprocessing_function=pre_process)
# We build the train generator using flow_from_directory
train_generator = train_datagen.flow_from_directory(train_dir,
target_size=(dim,dim),
batch_size=batch_size,
class_mode='categorical')
# We build the validation generator using flow_from_directory
val_generator = val_datagen.flow_from_directory(val_dir,
target_size=(dim,dim),
batch_size=batch_size,
class_mode='categorical')
在前面的代码中,我们将执行平均像素减法的额外任务传递给ImageDataGenerator,因为我们无法控制将图像数据加载到内存中并通过pre_process函数传递。在preprocessing_function选项中,我们可以为任何特定的预处理任务传递任何想要的自定义函数。
通过train_dir和val_dir,我们将训练和验证目录传递给我们用flow_with_directory选项创建的训练和验证生成器。生成器通过查看通过的训练数据目录(此处为train_dir)中的班级文件夹数量来识别班级数量。在基于target_size的训练时间内,根据指定的batch_size将图像读入存储器
class_mode帮助生成器识别它是二进制分类还是多分类('categotical')分类。
具体实现在GitHub 上的TransferLearning_ffd.py文件夹中。
Python 脚本TransferLearning_ffd.py可以如下调用:
python TransferLearning_ffd.py --path '/media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/book AI/Diabetic Retinopathy/Extra/assignment2_train_dataset/' --class_folders '["class0","class1","class2","class3","class4"]' --dim 224 --lr 1e-4 --batch_size 32 --epochs 50 --initial_layers_to_freeze 10 --model InceptionV3 --outdir '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/'
作业运行的输出日志结尾如下:
Validation results saved at : /home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/val_results.csv
[0 0 0 ... 4 2 2]
[0 0 0 ... 4 4 4]
Validation Accuracy: 0.5183708345200797
Validation Quadratic Kappa Score: 0.44422008110380984
正如我们所看到的,通过重用一个现有的网络并在其上执行迁移学习,我们能够获得一个像样的0.44二次卡帕值。
分类结果
通过使用所有三种神经网络体系结构来执行分类分类:VGG16、ResNet50和InceptionV3。对于此糖尿病视网膜病变使用案例,使用InceptionV3版本的迁移学习网络获得了最佳结果。在分类分类的情况下,我们只是将具有最大预测类别概率的类别转换为预测严重性标签。然而,由于问题中的类具有序数意义,我们可以利用软最大概率的一种方法是,根据软最大概率得到类严重性的期望值,并得出期望值
如下:

我们可以对分数进行排序,并确定三个阈值来确定图像属于哪个类别。这些阈值可以通过在这些预期分数上训练二级模型作为特征来选择。建议读者按照这些思路进行实验,看看是否会有所收获。
As part of this project we are using transfer learning to get reasonable results on a difficult problem. The model performances could have very well been better by training a network from scratch on the given dataset.
测试时的推断
以下代码可用于对未标记的测试数据进行推断:
import keras
import numpy as np
import pandas as pd
import cv2
import os
import time
from sklearn.externals import joblib
import argparse
# Read the Image and resize to the suitable dimension size
def get_im_cv2(path,dim=224):
img = cv2.imread(path)
resized = cv2.resize(img, (dim,dim), cv2.INTER_LINEAR)
return resized
# Pre Process the Images based on the ImageNet pre-trained model Image transformation
def pre_process(img):
img[:,:,0] = img[:,:,0] - 103.939
img[:,:,1] = img[:,:,0] - 116.779
img[:,:,2] = img[:,:,0] - 123.68
return img
# Function to build test input data
def read_data_test(path,dim):
test_X = []
test_files = []
file_list = os.listdir(path)
for f in file_list:
img = get_im_cv2(path + '/' + f)
img = pre_process(img)
test_X.append(img)
f_name = f.split('_')[0]
test_files.append(f_name)
return np.array(test_X),test_files
让我们定义一下推论:
def inference_test(test_X,model_save_dest,n_class):
folds = len(list(model_save_dest.keys()))
pred = np.zeros((len(test_X),n_class))
for k in range(1,folds + 1):
model = keras.models.load_model(model_save_dest[k])
pred = pred + model.predict(test_X)
pred = pred/(1.0*folds)
pred_class = np.argmax(pred,axis=1)
return pred_class
def main(path,dim,model_save_dest,outdir,n_class):
test_X,test_files = read_data_test(path,dim)
pred_class = inference_test(test_X,model_save_dest,n_class)
out = pd.DataFrame()
out['id'] = test_files
out['class'] = pred_class
out['class'] = out['class'].apply(lambda x:'class' + str(x))
out.to_csv(outdir + "results.csv",index=False)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='arguments')
parser.add_argument('--path',help='path of images to run inference on')
parser.add_argument('--dim',type=int,help='Image dimension size to
process',default=224)
parser.add_argument('--model_save_dest',
help='location of the trained models')
parser.add_argument('--n_class',type=int,help='No of classes')
parser.add_argument('--outdir',help='Output DIrectory')
args = parser.parse_args()
path = args.path
dim = args.dim
model_save_dest = joblib.load(args.model_save_dest)
n_class = args.n_class
outdir = args.outdir
main(path,dim,model_save_dest,outdir,n_class)
执行回归而不是分类分类
我们在制定损失函数一节中讨论的一件事是,类别标签不是独立的分类类别,而是随着糖尿病视网膜病变病情的加重而具有序数意义。因此,通过定义的迁移学习网络而不是分类来执行回归是值得的,并看看结果如何。我们唯一需要改变的是输出单元,从 softmax 变为线性单元。事实上,我们将把它改为 ReLU,因为我们想避免负分数。以下代码块显示了回归网络的InceptionV3版本:
def inception_pseudo(dim=224,freeze_layers=30,full_freeze='N'):
model = InceptionV3(weights='imagenet',include_top=False)
x = model.output
x = GlobalAveragePooling2D()(x)
x = Dense(512, activation='relu')(x)
x = Dropout(0.5)(x)
x = Dense(512, activation='relu')(x)
x = Dropout(0.5)(x)
out = Dense(1,activation='relu')(x)
model_final = Model(input = model.input,outputs=out)
if full_freeze != 'N':
for layer in model.layers[0:freeze_layers]:
layer.trainable = False
return model_final
我们将最小化回归网络的均方误差,而不是像在分类网络中那样最小化分类交叉熵(对数损失)。对于回归问题,最小化的成本函数如下,其中
是预测的标签:

一旦我们预测了回归分数,我们将把分数四舍五入到最接近的严重程度等级(0 到 4)。
使用 keras 顺序实用程序作为生成器
Keras 有一个很好的名为keras.utils.sequence()的批处理生成器,可以帮助您非常灵活地定制批处理创建。事实上,有了keras.utils.sequence()就可以设计出整个纪元流水线。我们将在这个回归问题中使用这个工具来习惯这个工具。对于迁移学习问题,我们可以使用keras.utils.sequence()设计生成器类,如下所示:
class DataGenerator(keras.utils.Sequence):
'Generates data for Keras'
def __init__(self,files,labels,batch_size=32,n_classes=5,dim=(224,224,3),shuffle=True):
'Initialization'
self.labels = labels
self.files = files
self.batch_size = batch_size
self.n_classes = n_classes
self.dim = dim
self.shuffle = shuffle
self.on_epoch_end()
def __len__(self):
'Denotes the number of batches per epoch'
return int(np.floor(len(self.files) / self.batch_size))
def __getitem__(self, index):
'Generate one batch of data'
# Generate indexes of the batch
indexes = self.indexes[index*self.batch_size:
(index+1)*self.batch_size]
# Find list of files to be processed in the batch
list_files = [self.files[k] for k in indexes]
labels = [self.labels[k] for k in indexes]
# Generate data
X, y = self.__data_generation(list_files,labels)
return X, y
def on_epoch_end(self):
'Updates indexes after each epoch'
self.indexes = np.arange(len(self.files))
if self.shuffle == True:
np.random.shuffle(self.indexes)
def __data_generation(self,list_files,labels):
'Generates data containing batch_size samples' # X : (n_samples,
*dim, n_channels)
# Initialization
X = np.empty((len(list_files),self.dim[0],self.dim[1],self.dim[2]))
y = np.empty((len(list_files)),dtype=int)
# print(X.shape,y.shape)
# Generate data
k = -1
for i,f in enumerate(list_files):
# print(f)
img = get_im_cv2(f,dim=self.dim[0])
img = pre_process(img)
label = labels[i]
#label =
keras.utils.np_utils.to_categorical(label,self.n_classes)
X[i,] = img
y[i,] = label
# print(X.shape,y.shape)
return X,y
在前面的代码中,我们使用keras.utils.Sequence定义了DataGenerator类。
我们定义数据生成器来接受图像文件名、标签、批次大小、类的数量以及我们希望图像调整到的尺寸。此外,我们指定是否希望打乱图像在一个时期内的处理顺序。
我们指定的功能是从keras.utils.Sequence继承而来的,因此这些功能中的具体活动不能在其他地方指定。 len 功能是计算一个纪元中的批次数量。
类似地,在on_epoch_end功能中,我们可以指定要在纪元结束时执行的活动,例如打乱输入在纪元中的处理顺序。我们可以在每个时期创建不同的数据集进行处理。当我们有大量数据并且不想处理每个时期的所有数据时,这通常是有用的。__getitem__功能通过提取与特定于批次的所有数据点索引相对应的数据来帮助批次创建。如果数据创建过程更复杂,则可以利用__data_generation 功能来提取批次中每个数据点的特定逻辑。例如,我们将与该批中的数据点索引相对应的文件名传递给__data_generation函数,以使用opencv读取每个图像,并且还使用preprocess函数对它们进行预处理,我们必须执行平均像素减法。
基于回归的迁移学习的训练函数可以编码如下:
def train_model(self,file_list,labels,n_fold=5,batch_size=16,
epochs=40,dim=224,lr=1e-5,model='ResNet50'):
model_save_dest = {}
k = 0
kf = KFold(n_splits=n_fold, random_state=0, shuffle=True)
for train_index,test_index in kf.split(file_list):
k += 1
file_list = np.array(file_list)
labels = np.array(labels)
train_files,train_labels =
file_list[train_index],labels[train_index]
val_files,val_labels =
file_list[test_index],labels[test_index]
if model == 'Resnet50':
model_final =
self.resnet_pseudo(dim=224,freeze_layers=10,full_freeze='N')
if model == 'VGG16':
model_final =
self.VGG16_pseudo(dim=224,freeze_layers=10,full_freeze='N')
if model == 'InceptionV3':
model_final =
self.inception_pseudo(dim=224,freeze_layers=10,full_freeze='N')
adam =
optimizers.Adam(lr=lr, beta_1=0.9, beta_2=0.999, epsilon=1e-08,
decay=0.0)
model_final.compile(optimizer=adam, loss=["mse"],metrics=['mse'])
reduce_lr =
keras.callbacks.ReduceLROnPlateau(monitor='val_loss',
factor=0.50,patience=3,
min_lr=0.000001)
early =
EarlyStopping(monitor='val_loss', patience=10, mode='min',
verbose=1)
logger =
CSVLogger('keras-5fold-run-01-v1-epochs_ib.log', separator=',',
append=False)
checkpoint =
ModelCheckpoint('kera1-5fold-run-01-v1-fold-'
+ str('%02d' % (k + 1))
+ '-run-' + str('%02d' % (1 + 1)) + '.check',
monitor='val_loss', mode='min',
save_best_only=True,
verbose=1)
callbacks = [reduce_lr,early,checkpoint,logger]
train_gen =
DataGenerator(train_files,train_labels,batch_size=32,
n_classes=
len(self.class_folders),dim=(self.dim,self.dim,3),shuffle=True)
val_gen =
DataGenerator(val_files,val_labels,batch_size=32,
n_classes=len(self.class_folders),
dim=(self.dim,self.dim,3),shuffle=True)
model_final.fit_generator(train_gen,epochs=epochs,verbose=1,
validation_data=(val_gen),callbacks=callbacks)
model_name =
'kera1-5fold-run-01-v1-fold-' + str('%02d' % (k + 1)) + '-run-
' + str('%02d' % (1 + 1)) + '.check'
del model_final
f = h5py.File(model_name, 'r+')
del f['optimizer_weights']
f.close()
model_final = keras.models.load_model(model_name)
model_name1 = self.outdir + str(model) + '___' + str(k)
model_final.save(model_name1)
model_save_dest[k] = model_name1
return model_save_dest
从前面的代码中我们可以看到,火车生成器和验证生成器是使用继承了keras.utils.sequence类的DataGenerator创建的。推理功能可以编码如下:
def inference_validation(self,test_X,test_y,model_save_dest,n_class=5,
folds=5):
print(test_X.shape,test_y.shape)
pred = np.zeros(test_X.shape[0])
for k in range(1,folds + 1):
print(f'running inference on fold: {k}')
model = keras.models.load_model(model_save_dest[k])
pred = pred + model.predict(test_X)[:,0]
pred = pred
print(pred.shape)
print(pred)
pred = pred/float(folds)
pred_class = np.round(pred)
pred_class = np.array(pred_class,dtype=int)
pred_class = list(map(lambda x:4 if x > 4 else x,pred_class))
pred_class = list(map(lambda x:0 if x < 0 else x,pred_class))
act_class = test_y
accuracy = np.sum([pred_class == act_class])*1.0/len(test_X)
kappa = cohen_kappa_score(pred_class,act_class,weights='quadratic')
return pred_class,accuracy,kappa
从前面的代码中我们可以看到,计算了每个折叠的预测平均值,并通过舍入预测分数将其转换为最近的严重性级别。回归的 Python 脚本在 GitHub 链接中。名为TransferLearning_reg.py。通过运行以下命令可以调用相同的功能:
python TransferLearning_reg.py --path '/media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/book AI/Diabetic Retinopathy/Extra/assignment2_train_dataset/' --class_folders '["class0","class1","class2","class3","class4"]' --dim 224 --lr 1e-4 --batch_size 32 --epochs 5 --initial_layers_to_freeze 10 --model InceptionV3 --folds 5 --outdir '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/Regression/'
培训的输出日志如下:
Model saved to dest: {1: '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/Regression/InceptionV3___1', 2: '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/Regression/InceptionV3___2', 3: '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/Regression/InceptionV3___3', 4: '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/Regression/InceptionV3___4', 5: '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/Regression/InceptionV3___5'}
如我们所见,对应于5折叠的5模型已经保存在我们指定的回归文件夹下。接下来,我们可以对验证数据集运行推断,并查看回归模型的运行情况。可以调用相同的 Python 脚本,如下所示:
python TransferLearning_reg.py --path '/media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/book AI/Diabetic Retinopathy/Extra/assignment2_train_dataset/' --class_folders '["class0","class1","class2","class3","class4"]' --dim 224 --lr 1e-4 --batch_size 32 --model InceptionV3 --outdir '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/Regression/' --mode validation --model_save_dest --'/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/Regression/model_dict.pkl' --folds 5
推断的结果如下:
Models loaded from: {1: '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/Regression/InceptionV3___1', 2: '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/Regression/InceptionV3___2', 3: '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/Regression/InceptionV3___3', 4: '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/Regression/InceptionV3___4', 5: '/home/santanu/ML_DS_Catalog-/Transfer_Learning_DR/Regression/InceptionV3___5'}
-----------------------------------------------------
Kappa score: 0.4662660860310418
accuracy: 0.661350042722871
End of training
-----------------------------------------------------
Processing Time 138.52878069877625 secs
从前面的日志中我们可以看到,该模型获得了约 66%的良好验证精度和二次 Kappa 分数0.466,因为我们刚刚使用回归分数将其映射到最近的严重性条件。建议读者进行实验,看看基于预测的二级模型是否对评分,以及眼睛是左眼还是右眼,是否比这种映射到最近的严重程度类别的简单评分给出更好的结果。
摘要
在这一章中,我们讨论了迁移学习的实际方面,以解决医疗保健领域的一个现实问题。读者应该通过尽可能定制这些示例来进一步构建这些概念。
我们通过分类和基于回归的神经网络获得的准确度和 kappa 分数对于生产实施来说已经足够好了。在第 3 章、神经机器翻译中,我们将致力于实现智能机器翻译系统,这是一个比本章中介绍的更高级的主题。我期待你的参与。
五、神经机器翻译
机器翻译,简单来说就是用电脑将文本从一种语言翻译成另一种语言。它是计算机语言学的一个分支,已经存在好几年了。目前,在美国,翻译是一个 400 亿美元的行业,在欧洲和亚洲也在快速增长。社会、政府、经济和商业对翻译有很大的需求,谷歌、脸书、易贝等公司在应用中广泛使用翻译。尤其是谷歌的神经翻译系统是目前最先进的翻译系统之一,只需一个模型就能完成多种语言的翻译。
早期的机器翻译系统始于将文本中的单词和短语翻译成所需目标语言的相关替代语言。然而,由于以下原因,通过这些简单技术实现的翻译质量受到限制:
- 从源语言到目标语言的词到词的映射并不总是可用的。
- 即使源语言和目标语言之间确实存在精确的单词到单词的映射,语言的句法结构通常也不会相互对应。机器翻译中的这个问题通常被称为错位。
然而,随着最近在递归神经网络 ( RNN )架构中的进展,例如 LSTMs、GRU 等,机器翻译不仅提供了改进的翻译质量,而且这种系统的复杂性也远低于传统系统。
机器翻译系统可以大致分为三类:基于规则的机器翻译、统计机器翻译和神经机器翻译。
在本章中,我们将涵盖以下主题:
- 基于规则的机器翻译
- 统计机器学习系统
- 神经机器翻译
- 序列到序列的神经翻译
- 神经翻译的损失函数
技术要求
您将需要具备 Python 3、TensorFlow 和 Keras 的基本知识。
本章代码文件可在 GitHub:
https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 03上找到
查看以下视频,查看正在运行的代码:
http://bit.ly/2sXYX8A
基于规则的机器翻译
经典的基于规则的机器翻译系统严重依赖于将文本从源语言转换为目标语言的规则。这些规则通常由语言学家创建,通常在句法、语义和词汇层面起作用。典型的基于规则的机器翻译系统通常有三个阶段:
- 分析阶段
- 词汇迁移阶段
- 生成阶段
图 3.1 所示为典型的基于规则的机器翻译系统的流程图:

Figure 3.1: A flow diagram of a rule-based machine translation system
分析阶段
基于规则的机器翻译的第一阶段是分析阶段,对源语言文本进行分析,提取与词法、词性、命名实体识别以及词义消歧相关的信息。形态信息涉及单词的结构、词干的来源、词根的检测等等。词性标记器用可能的词性标记来标记文本中的每个单词,如名词、动词、副词、形容词等。接下来是一个命名实体识别 ( NER )任务,该任务试图将命名实体分类到预定义的桶中,如人名、位置、组织名称等。NER 之后是词义消歧,它试图识别一个特定的词是如何在一个句子中使用的。
词汇迁移阶段
词汇迁移阶段跟在分析阶段之后,有两个阶段:
- 单词翻译:在单词翻译中,使用双语翻译词典将分析阶段推导出的源词根翻译成对应的目标词根。
- 语法翻译:在语法翻译阶段,进行句法修改,包括翻译后缀等。
生成程序段
在生成阶段,在最终的翻译文本作为输出提供之前,对翻译文本进行验证和纠正,以使其在词类、性别以及主语和宾语相对于动词的一致性方面是正确的。在每个步骤中,机器翻译系统使用预定义的字典。对于基于规则的机器翻译系统的最低限度实现,需要以下字典:
- 源语言形态分析词典
- 包含源语言单词与其目标语言对应词的映射的双语词典
- 包含用于目标词生成的目标语言形态信息的词典
统计机器学习系统
给定源文本,统计机器翻译系统通过最大化其条件概率来选择目标文本。例如,假设我们有一个源文本 s ,我们想要在目标语言中导出最佳的等价文本 t 。这可以推导如下:

(1) 中 P(t/s) 的公式可以用贝叶斯定理扩展如下:

对于给定的源句子, P(s) 将是固定的,因此,找到最佳的目标翻译结果如下:

你可能想知道为什么最大化 P(s/t)P(t) 而不是直接最大化 P(t/s) 会带来优势。一般来说,在 P(t/s) 下很可能出现的格式不良的句子是通过将问题分解成两部分来避免的,即 P(s/t) 和 P(t) ,如上式所示:

Figure 3.2: Statistical machine translation architecture
从上图中我们可以看到,统计机器翻译问题被分解成三个不同的子问题,如上所述:
- 为目标建立一个语言模型,让我们可以估计 P(t)
- 建立从目标语言到源语言的翻译模型,使我们能够估计 P(s/t)
- 对可能的目标翻译进行搜索,并选择最大化 P(s/t)P(t) 的翻译
我们将讨论这三个主题中的每一个,因为这些功能是任何机器翻译问题所固有的。
语言模型
在语言模型中,句子的概率被表示为各个单词或短语的条件概率的乘积。先说句子 t 由t1t2t3组成。。t n 。根据概率的链式法则,句子 t 的概率可以表示为:

建立一个基于前面公式的语言模型需要我们估计几个顺序的条件概率,这实际上是不可能的。为了使这个问题在计算上可行,一个简单的假设是只基于前一个单词来限定一个单词,而不是基于它之前的所有单词。这个假设也被称为马尔可夫假设、T3,这个模型被称为二元模型。根据二元模型,单词的条件概率可以表示如下:

为了进一步改进结果,我们可以使用一个三元模型,它将一个句子中的一个特定单词置于它前面的两个单词之上,如下所示:

对于二元模型,给定当前单词 t 1 ,下一个单词为t2T3 的条件概率可以通过计算训练语料库中一对 (t 1 、t 2 ) 的总出现次数并将其归一化为单词 t 1 在

对于三元模型,当前单词t3T3】的条件概率给定它前面的两个单词 t 1 ,t 2 ,可以估计如下:

超越三元模型通常会导致稀疏性。即使对于二元模型,我们也可能会有几个二元模型的条件概率缺失,因为它们不会出现在训练语料库中。然而,那些缺失的二元模型可能非常相关,估计它们的条件概率非常重要。不用说,n-gram 模型倾向于为训练数据中出现的单词对估计高条件概率,而忽略那些没有出现的单词对。
语言模型的困惑
困惑度量用于评估语言模型的有用性。假设我们已经在一个训练语料库上训练了一个语言模型,让学习到的句子或文本的概率模型为 P(。)。模型的困惑 P(。)在从与训练语料库相同的人群中抽取的测试集语料库上进行评估。如果我们用 M 字表示测试集语料库,那么说 (w 1 ,w 2 ,w 3 ,。。。。。,w M ) ,则模型对测试集序列的困惑由下式表示:

所示的 H 的表达式测量每个单词的不确定性:

根据语言模型,我们可以将测试语料库的概率表达式分解如下:

如果我们将测试集中第I个单词以前面的单词为条件的概率表示为 P (s i ) ,那么测试语料库的概率给出如下:

在这里,T1】P(sI)= P(wI/w1w2。。。w i-1 ) 。结合 (1) 和 (4) ,困惑可以写成:

假设我们有一个语言模型 P(。)还有一个测试集我爱机器学习来评价。根据语言模型,测试集的概率如下:

如果语言模型的训练语料也是我爱机器学习,测试集的概率会是 1,导致的对数概率是 0,而的困惑度是 1。这意味着模型可以完全确定地生成下一个单词。
另一方面,如果我们有一个大小为 N = 20,000 的词汇的更真实的训练语料库,并且训练后的模型在测试数据集上的困惑度为 100,那么平均来说,为了预测序列中的下一个单词,我们已经将搜索空间从 20,000 个单词缩小到 100 个单词。
让我们看看最坏的情况,在这种情况下,我们设法建立了一个模型,其中每个单词都独立于序列中的前几个单词:

对于一组 M 单词,使用 (5) 的困惑如下:

如果我们像以前一样有 N = 20,000,那么为了预测序列中的任何单词,需要考虑词汇表中的所有 N 单词,因为它们具有相同的概率。在这种情况下,我们无法减少单词上的平均搜索空间来预测序列中的单词。
翻译模式
翻译模型可以认为是机器翻译模型的心脏。在翻译模型中,我们需要估计概率 P(s/t) ,其中 s 为源语句子, t 为目标语句子。在这里,源句被给出,而目标是我们寻求发现的。因此,这个概率可以被称为给定目标句子的源句子的可能性。例如,让我们假设我们正在将一个源文本从法语翻译成英语。所以,在 P(s/t) 的语境中,我们的目的语是法语,我们的源语是英语,而在实际翻译的语境中,即 P(s/t)P(t) 中,我们的源语是法语,我们的目的语是英语。
翻译主要包括三个部分:
- 生育率 : 并非源语言中的所有单词在目标语言中都有对应的单词。例如,英语句子 Santanu 热爱数学在法语中被翻译为 Santanu 瞄准数学。我们可以看到,英语中的 m ath 在法语中被翻译成了两个词,就像les mathematics一样。形式上,生殖力定义为源语词在目标语中生成的字数的概率分布,可以表示为P**(n/ws),其中 w s 代表源词。使用概率分布代替硬编码数字 n ,因为相同的单词可能基于上下文产生不同长度的翻译。
- 扭曲:源句和目的句之间的词与词的对应对于任何机器翻译系统都很重要。然而,源语言句子中单词的位置可能不总是与其在目标语言句子中的对应位置完全同步。扭曲通过概率函数涵盖了对齐的概念, P(p t /p s ,l) ,其中 p t 和 P s 分别代表目标词和源词的位置,而 l 代表目标句的长度。如果源语言是英语,目标语言是法语,那么 P(p t /p s ,l) 表示在给定长度为 l 的法语句子中,位置PsT27】的英语单词对应于位置PsT31】的法语单词的概率。**
- 词与词的翻译:最后我们来看词与词的翻译,一般用目标语词给定源语词的概率分布来表示。对于给定的源语言单词,wsT5】,概率可以表示为P(wt/ws),其中wtT15】代表目标语言单词。**
对于语言模型来说,在训练过程中需要估计生殖力概率、失真概率和单词到单词的翻译概率。
现在,让我们回到最初估计概率 P(s/t) 的问题。如果用 E 表示英语句子,用 F 表示法语句子,需要计算 P(F/E) 的概率。为了考虑单词的对齐,我们将概率修改为 P(F,a/E) ,其中 a 代表法语中目标句的对齐。这种比对将帮助我们注入与扭曲和生育力相关的信息。
让我们通过一个例子来计算概率 P(F,a/E) 。让一个具体的英语句子用一个五个字的句子来表示, e = (e 1 ,e 2 ,e 3 ,e 4 ,e 5 ) ,这其实就是实际法语句子 f = (f 1 ,f 2 ,f 3 的正确翻译同样,让单词的对应对齐如下:
- e1→f6
- e 2 → 在法语中不对应任何单词
- e**→【f】、【f】
*** e→1**** e5→*f2** f 5 → 在英语中没有对应的单词***
***由于这是一个概率模型,该算法将尝试不同对齐的不同英语句子,其中具有正确对齐的正确英语句子应该具有最高的概率,给定法语句子。
让我们将第一个英语单词视为e1—它与法语单词 f 6 对齐,并且还发出一个法语单词,如下所示:

现在,让我们将对齐视为两个组件的组合:扭曲 a d 和生育力 f d 。 (1) 中的表达式可以改写如下:

如果我们仔细观察, P(f 5 /e 1 ) 为平移概率,P(af/e1)为生育力,而 P(a d /e 1 ,f 5 ) 为畸变概率。我们需要对英语句子中所有给定的英语单词和给定的法语句子进行此练习,以计算 P(F,a/E) 。最后我们需要取最好的英文句子
,对齐
,这样最大化概率 P(F,a/E)P(E) 。这看起来如下:

这里需要注意的一点是,为了寻找最佳翻译而尝试不同的对齐方式和不同的可能单词翻译可能会变得难以计算,因此,需要部署聪明的算法来在最短的时间内找到最佳翻译。
神经机器翻译
神经机器翻译 ( NMT )使用深度神经网络执行从源语言到目标语言的机器翻译。神经翻译机接收源语言的文本作为输入序列,并将其编码为隐藏的表示,然后将其解码以产生目标语言的翻译文本序列。这种 NMT 系统的一个关键优点是,整个机器翻译系统可以从端到端一起训练,不像基于规则和统计的机器翻译系统。一般来说,RNN 体系结构,如 LSTMs ( 长短期记忆)和/或门控递归单元 ( GRUs )用于神经翻译机体系结构。
NMT 相对于其他传统方法的一些优势如下:
- NMT 模型的所有参数都是基于损失函数进行端到端训练的,因此降低了模型的复杂性
- 这些 NMT 模型使用了比传统方法大得多的上下文,因此产生了更准确的翻译
- NMT 模型更好地利用了单词和短语的相似性
- rnn 允许更高质量的文本生成,因此相对于翻译文本的语法,翻译更加准确
编码器-解码器模型
下图所示为神经翻译机的架构,该机器使用一个 LSTM 作为编码器,将输入源语言序列编码为最终隐藏状态 h f 和最终存储单元状态 c f 。最终的隐藏状态和单元格状态【hf、cf将捕捉整个输入序列的上下文。因此,【hf,cf成为解码器网络可以调节的良好候选。
该隐藏和单元状态信息【h】f,cf被馈送到解码器网络作为初始隐藏和单元状态,然后解码器在目标序列上被训练,输入目标序列相对于输出目标序列滞后 1。根据解码器,输入序列的第一个字是伪字[START],而输出标签是字 c'est 。解码器网络只是被训练成一个生成语言模型,其中在任何时间步 t ,输出标签,只是相对于输入的下一个单词,即yt= xt+1。唯一新的是编码器的最终隐藏和单元状态(即【hf、cf)被馈送到解码器的初始隐藏和单元状态,为翻译提供内容。
这意味着训练过程可以被认为是为目标语言(由解码器表示)建立一个语言模型,条件是代表源语言的编码器的隐藏状态:

Figure 3.3: High level encoder-decoder architecture of a neural machine translation system
如果 T 是源语言文本
对应的目标语言文本,那么对于训练来说我们只是试图最大化 P w (T t+1 /S,T) 相对于 W 的对数概率,其中 T s+1 代表偏移一个时间步长的目标语言文本, W 代表编码器-解码器
既然我们已经讨论了编码器-解码器 NMTs 的训练过程,现在我们将看看如何在推理过程中使用训练好的模型。
使用编码器-解码器模型进行推断
在 NMT ( 神经翻译机)上运行推理的架构流程与训练 NMT 有点不同。以下是使用 NMT 进行推理的体系结构流程:

Figure 3.4: Inference from an encoder/decoder-based neural machine translation
在推断过程中,源语言输入序列被馈送到编码器网络,最终产生的隐藏和单元状态,【h】f、cf被馈送到解码器隐藏和单元状态。解码器被转换成单个时间步长,馈送到解码器的第一个输入是伪[START]字。于是,基于【hf、cf和初始伪字[START],解码器将输出一个字, w ,以及新的隐藏和单元状态,【hd、cd。这个单词 w 以新的隐藏和单元状态【hd、cd再次被馈送到解码器,以生成下一个单词。重复这个过程,直到我们遇到一个序列结束字符。
实现序列到序列的神经翻译机
我们要建立一个神经机器翻译系统,它将学习把英语短句翻译成法语。为此,我们将使用位于http://www.manythings.org/anki/的英语到法语文本语料库(fra-eng/fra.txt)。
处理输入数据
文本数据不能直接输入任何神经网络,因为神经网络只能理解数字。我们将把每个单词视为一个长度等于每个语料库中存在的单词数的热门编码向量。如果英语语料库包含 1000 个单词,则热门编码向量veT3】的维数为 1000,即ve∈R1000 x 1。
我们将通读英语和法语语料库,并确定每个语料库中独特单词的数量。我们还将通过索引来表示单词,对于单词的一个热编码向量,对应于该单词的索引将被设置为 1,而其余的索引将被设置为零。举个例子,假设在英语语料库中,我们有四个词:全球变暖是真实存在的。我们可以将每个单词的索引定义如下:
| 字 | 指数 |
| 全球的 | Zero |
| 加温 | one |
| 是 | Two |
| 真实的 | three |
在这种情况下,我们可以将单词全局的一热编码向量定义为【1,0,0】T。同样的,实数的一热编码向量可以表示为【1,0,0】T。
现在,转向每个句子或记录的源语言输入,我们将有一个单词序列表示为一个热门编码向量序列。下一个明显的问题是如何管理序列长度,因为这可能会有所不同。最被接受的方法是具有固定的序列长度,或者等于语料库中句子的最大序列长度,或者是预定的合理长度。我们将使用目标句子两次:一次作为解码器的翻译输出序列,一次作为解码器的输入,唯一的区别是输出序列将比输入序列提前一个时间步长。因此,输入目标序列中的第一个单词将是虚拟单词[START],而输出目标序列中的最后一个单词将是虚拟单词[END],标志着句子序列的结束。
如果目标法语句子是Je m'appelle Santanu,解码器中的输入目标和输出目标序列如下:
[START],[Je],[m’appelle] [Santanu]
[Je],[m’appelle] [Santanu][END]
我们选择用制表符表示[START],用下一行字符表示[END]。
我们将数据创建活动分为三个部分:
- 读取源(英语)和目标(法语)文本的输入文件
- 从源语言和目标语言文本中构建词汇
- 将输入的英语和法语语料库处理为它们的数字表示,以便它们可以用于神经机器翻译网络
这里说明的read_input_file功能可用于阅读源语言和目标语言文本:
def read_input_file(self,path,num_samples=10e13):
input_texts = []
target_texts = []
input_words = set()
target_words = set()
with codecs.open(path, 'r', encoding='utf-8') as f:
lines = f.read().split('\n')
for line in lines[: min(num_samples, len(lines) - 1)]:
input_text, target_text = line.split('\t')
# \t as the start of sequence
target_text = '\t ' + target_text + ' \n'
# \n as the end of sequence
input_texts.append(input_text)
target_texts.append(target_text)
for word in input_text.split(" "):
if word not in input_words:
input_words.add(word)
for word in target_text.split(" "):
if word not in target_words:
target_words.add(word)
return input_texts,target_texts,input_words,target_words
vocab_generation功能可用于建立源语言和目标语言的词汇集:
def vocab_generation(self,path,num_samples,verbose=True):
input_texts,target_texts,input_words,target_words =
self.read_input_file(path,num_samples)
input_words = sorted(list(input_words))
target_words = sorted(list(target_words))
self.num_encoder_words = len(input_words)
self.num_decoder_words = len(target_words)
self.max_encoder_seq_length =
max([len(txt.split(" ")) for txt in input_texts])
self.max_decoder_seq_length =
max([len(txt.split(" ")) for txt in target_texts])
if verbose == True:
print('Number of samples:', len(input_texts))
print('Number of unique input tokens:',
self.num_encoder_words)
print('Number of unique output tokens:',
self.num_decoder_words)
print('Max sequence length for inputs:',
self.max_encoder_seq_length)
print('Max sequence length for outputs:',
self.max_decoder_seq_length)
self.input_word_index =
dict([(word, i) for i, word in enumerate(input_words)])
self.target_word_index =
dict([(word, i) for i, word in enumerate(target_words)])
self.reverse_input_word_dict =
dict((i, word) for word, i in self.input_word_index.items())
self.reverse_target_word_dict =
dict((i, word) for word, i in self.target_word_index.items())
process_input函数利用前面函数中构建的输入和目标文本以及词汇表,将文本数据转换为神经翻译机体系结构可以使用的数字形式。process_input功能的代码如下:
def process_input(self,input_texts,target_texts=None,verbose=True):
encoder_input_data =
np.zeros((len(input_texts), self.max_encoder_seq_length,
self.num_encoder_words), dtype='float32')
decoder_input_data =
np.zeros((len(input_texts), self.max_decoder_seq_length,
self.num_decoder_words), dtype='float32')
decoder_target_data =
np.zeros((len(input_texts), self.max_decoder_seq_length,
self.num_decoder_words), dtype='float32')
if self.mode == 'train':
for i, (input_text, target_text) in
enumerate(zip(input_texts,target_texts)):
for t, word in enumerate(input_text.split(" ")):
try:
encoder_input_data[i, t,
self.input_word_index[word]] = 1.
except:
print(f'word {word}
encoutered for the 1st time, skipped')
for t, word in enumerate(target_text.split(" ")):
# decoder_target_data is ahead of decoder_input_data
by one timestep
decoder_input_data[i, t,
self.target_word_index[word]] = 1.
if t > 0:
# decoder_target_data will be ahead by one timestep
#and will not include the start character.
try:
decoder_target_data[i, t - 1,
self.target_word_index[word]] = 1.
except:
print(f'word {word}
encoutered for the 1st time,skipped')
return
encoder_input_data,decoder_input_data,decoder_target_data,
np.array(input_texts),np.array(target_texts)
else:
for i, input_text in enumerate(input_texts):
for t, word in enumerate(input_text.split(" ")):
try:
encoder_input_data[i, t,
self.input_word_index[word]] = 1.
except:
print(f'word {word}
encoutered for the 1st time, skipped')
return encoder_input_data,None,None,np.array(input_texts),None
encoder_input_data 变量将包含输入源数据,并且将是记录数量、时间步长数量和每个单向编码向量的维度的三维数组。类似地,decoder_input_data 将包含输入的目标数据,而decoder_target_data 将包含目标标签。在执行上述功能时,将生成训练机器翻译系统所需的所有相关输入和输出。以下代码块包含与使用40000样本执行vocab_generation功能相关的显示统计信息:
('Number of samples:', 40000)
('Number of unique input tokens:', 8658)
('Number of unique output tokens:', 16297)
('Max sequence length for inputs:', 7)
('Max sequence length for outputs:', 16)
从前面的统计中我们可以看到,40000语料库中输入的英语单词数,以及文本句子数为8658,而对应的法语单词数为16297。这表明一个事实,即每个英语单词平均发出大约两个法语单词。同样,我们看到英语句子中的最大单词数是7,而法语句子中的最大单词数是14,如果不包括我们为了训练目的添加到法语句子中的[START]和[END]字符。这也证实了这样一个事实,平均来说,每个被翻译的英语句子将产生两倍的单词。
让我们看看神经翻译机的输入和目标的形状:
('Shape of Source Input Tensor:',(40000, 7, 8658))
('Shape of Target Input Tensor:',(40000, 16, 16297))
(Shape of Target Output Tensor:',(40000, 16, 16297))
编码器数据的形状为(40000, 7, 8658),其中第一维是源语言句子的数量,第二维是时间步长的数量,最终维度是一个热门编码向量的大小,即8658,对应于英语词汇中的8658源语言单词。类似地,我们看到对于目标输入和输出张量,一热编码向量的大小为16297,对应于法语词汇中的16297单词。法语句子的时间步数为16。
定义神经机器翻译模型
如前所述,编码器将通过 LSTM 处理源输入序列,并将源文本编码成有意义的摘要。有意义的摘要将存储在最终的序列步骤隐藏和单元状态hfT3】和cfT7】中。这些向量在一起(即【hf;cf】)提供关于源文本的有意义的上下文,并且解码器被训练以产生其自己的目标序列,该目标序列以隐藏和单元状态向量【hf为条件;cf】。**
如下图所示,图 3.5,是一个英法翻译的训练过程的详细示意图。英语句子这是美好的一天通过一个 LSTM 转换成一个意义总结,然后存储在隐藏和细胞状态向量hf中;cf】。然后解码器通过嵌入在【hf中的信息,以输入的源语句为条件,生成自己的目标序列;cf】。时间步 t 的解码器预测下一个目标词,即给定源句的时间步 t + 1 的词。这就是为什么目标输入单词和目标输出单词之间有一个时间步长的滞后。对于第一时间步,解码器在目标文本序列中没有任何在先单词,因此它唯一可用于预测目标单词的信息是编码在【h】f中的信息;cf作为初始隐藏向量和细胞状态向量。像编码器一样,解码器也使用 LSTM,并且如所讨论的,输出目标序列比输入目标序列领先一个时间步长:

Figure 3.5: Illustration of the machine translation network flow while training
我们基于图 3.5 所示的架构,在函数model_enc_dec中定义了用于训练的编码器解码器端到端模型。这里,编码器(LSTM 1) 顺序接收源语言文本单词,并在编码器(LSTM 1) 的最后顺序步骤中捕获源语言句子或文本的整个上下文。来自编码器的该上下文作为初始状态被馈送给解码器(****【LSTM 2】),该解码器学习基于当前单词来预测下一个单词,因为在训练期间,我们有目标语言的句子/文本,因此解码器可以仅将其输入移动一个时间步长来形成目标:
def model_enc_dec(self):
#Encoder Model
encoder_inp =
Input(shape=(None,self.num_encoder_words),name='encoder_inp')
encoder = LSTM(self.latent_dim, return_state=True,name='encoder')
encoder_out,state_h, state_c = encoder(encoder_inp)
encoder_states = [state_h, state_c]
#Decoder Model
decoder_inp =
Input(shape=(None,self.num_decoder_words),name='decoder_inp')
decoder_lstm =
LSTM(self.latent_dim, return_sequences=True,
return_state=True,name='decoder_lstm')
decoder_out, _, _ =
decoder_lstm(decoder_inp, initial_state=encoder_states)
decoder_dense =
Dense(self.num_decoder_words,
activation='softmax',name='decoder_dense')
decoder_out = decoder_dense(decoder_out)
print(np.shape(decoder_out))
#Combined Encoder Decoder Model
model = Model([encoder_inp, decoder_inp], decoder_out)
#Encoder Model
encoder_model = Model(encoder_inp,encoder_states)
#Decoder Model
decoder_inp_h = Input(shape=(self.latent_dim,))
decoder_inp_c = Input(shape=(self.latent_dim,))
decoder_input = Input(shape=(None,self.num_decoder_words,))
decoder_inp_state = [decoder_inp_h,decoder_inp_c]
decoder_out,decoder_out_h,decoder_out_c =
decoder_lstm(decoder_input,initial_state=decoder_inp_state)
decoder_out = decoder_dense(decoder_out)
decoder_out_state = [decoder_out_h,decoder_out_c]
decoder_model = Model(inputs =
[decoder_input] + decoder_inp_state,output=
[decoder_out]+ decoder_out_state)
plot_model(model,show_shapes=True, to_file=self.outdir +
'encoder_decoder_training_model.png')
plot_model(encoder_model,show_shapes=True, to_file=self.outdir +
'encoder_model.png')
plot_model(decoder_model,show_shapes=True, to_file=self.outdir +
'decoder_model.png')
return model,encoder_model,decoder_model
虽然用于训练的模型是直接的端到端模型,但推理模型并不那么直接,因为我们不知道解码器在每个时间步长的先验输入。我们将在构建推理模型部分更详细地讨论推理模型。
神经翻译机的损失函数
神经翻译机的损失函数是预测模型序列中每个目标词的平均交叉熵损失。实际目标词和预测目标词可以是我们所取的法语语料库中的 16,297 个词中的任意一个。时间步长 t 处的目标标签将是一个单热编码向量yt∑{ 0,1} 16297 ,而预测输出将是法语词汇中 16,297 个单词中每个单词的概率形式。如果我们将预测的输出概率向量表示为pt∑(0,1) 16297 ,那么特定句子 s 每个时间步长的平均分类损失由下式给出:

我们通过对所有序列时间步长的损失进行求和,得到整个句子的损失,如下所示:

由于我们使用的是小批量随机梯度下降,小批量的平均成本可以通过平均小批量中所有句子的损失来获得。如果我们取尺寸为 m 的小批量,每个小批量的平均损失如下:

小批量成本用于计算随机梯度下降的梯度。
训练模型
我们首先执行model_enc_dec 函数定义模型进行训练,定义encoder_model和decoder_model进行推理,然后用categorical_crossentropy损失和rmsprop优化器进行编译。我们可以尝试其他的优化器,比如 Adam,有动量的 SDG 等等,但是,目前我们还是坚持rmsprop 。功能可定义如下:
# Run training
def train(self,encoder_input_data,decoder_input_data,
decoder_target_data):
print("Training...")
model,encoder_model,decoder_model = self.model_enc_dec()
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
model.fit([encoder_input_data, decoder_input_data],
decoder_target_data,
batch_size=self.batch_size,
epochs=self.epochs,
validation_split=0.2)
# Save model
model.save(self.outdir + 'eng_2_french_dumm.h5')
return model,encoder_model,decoder_model
我们在 80%的数据上训练模型,并使用剩余的 20%进行验证。列车/测试分离由如下定义的功能执行:
def train_test_split(self,num_recs,train_frac=0.8):
rec_indices = np.arange(num_recs)
np.random.shuffle(rec_indices)
train_count = int(num_recs*0.8)
train_indices = rec_indices[:train_count]
test_indices = rec_indices[train_count:]
return train_indices,test_indices
构建推理模型
让我们试着回忆一下推理模型的工作机制,看看我们如何使用已经训练好的模型的组件来构建它。模型的编码器部分应该以源语言中的文本句子为输入进行工作,并提供最终的隐藏和细胞状态向量,【h】f;cf,作为输出。我们不能像现在这样使用解码器网络,因为目标语言输入单词不能再被馈送到解码器。相反,我们将解码器网络折叠成由单个步骤组成,并将该步骤的输出作为下一步的输入。我们以伪字[START]作为解码器的第一个输入字开始,加上【h】f;cf,作为其初始隐藏和单元状态。目标输出字 w 1 和隐藏和单元格状态【h1;c1由解码器用[START]和hf 生成;作为输入的cf再次被馈送到解码器以生成下一个字,并且该过程重复直到解码器输出伪字[END]。下图说明了推理过程的分步表示,以便于解释:

Figure 3.6: Step-wise illustration of the inference process
从上图可以看出,解码器第一步的输出是C'est,而隐藏和单元状态是
。如虚线所示,这再次被馈送到解码器,以生成下一个字以及下一组隐藏和单元状态。重复该过程,因为解码器输出伪结束字符[END]。
为了进行推断,我们可以将网络的编码器部分保持原样,并进行一些修改来折叠解码器,使其由一个时间步骤组成。概括地说,无论 RNN 是由一个时间步长还是几个时间步长组成,与 RNN 相关的权重都不会改变,因为 RNN 的所有时间步长都具有相同的权重。
对于推断,我们可以看到训练模型的编码器部分被用作函数model_enc_dec中的encoder_model。类似地,使用相同的解码器 LSTM 定义单独的decoder_model,该解码器将输入作为隐藏状态、单元状态和输入字,并输出目标字和更新的隐藏和单元状态。为了清楚起见,再次重复我们定义推理模型的功能model_enc_dec,即encoder_model和decoder_model:
def model_enc_dec(self):
#Encoder Model
encoder_inp =
Input(shape=(None,self.num_encoder_words),name='encoder_inp')
encoder = LSTM(self.latent_dim, return_state=True,name='encoder')
encoder_out,state_h, state_c = encoder(encoder_inp)
encoder_states = [state_h, state_c]
#Decoder Model
decoder_inp =
Input(shape=(None,self.num_decoder_words),name='decoder_inp')
decoder_lstm =
LSTM(self.latent_dim, return_sequences=True,
return_state=True,name='decoder_lstm')
decoder_out, _, _ =
decoder_lstm(decoder_inp, initial_state=encoder_states)
decoder_dense =
Dense(self.num_decoder_words,
activation='softmax',name='decoder_dense')
decoder_out = decoder_dense(decoder_out)
print(np.shape(decoder_out))
#Combined Encoder Decoder Model
model = Model([encoder_inp, decoder_inp], decoder_out)
#Encoder Model
encoder_model = Model(encoder_inp,encoder_states)
#Decoder Model
decoder_inp_h = Input(shape=(self.latent_dim,))
decoder_inp_c = Input(shape=(self.latent_dim,))
decoder_input = Input(shape=(None,self.num_decoder_words,))
decoder_inp_state = [decoder_inp_h,decoder_inp_c]
decoder_out,decoder_out_h,decoder_out_c =
decoder_lstm(decoder_input,initial_state=decoder_inp_state)
decoder_out = decoder_dense(decoder_out)
decoder_out_state = [decoder_out_h,decoder_out_c]
decoder_model = Model(inputs =
[decoder_input] + decoder_inp_state,output=
[decoder_out]+ decoder_out_state)
plot_model(model,to_file=self.outdir +
'encoder_decoder_training_model.png')
plot_model(encoder_model,to_file=self.outdir + 'encoder_model.png')
plot_model(decoder_model,to_file=self.outdir + 'decoder_model.png')
return model,encoder_model,decoder_model
解码器将一次运行一个时间步长。在第一种情况下,它将从编码器获取隐藏和单元状态,并基于虚拟单词[START]猜测翻译的第一个单词。在第一步中预测的单词连同生成的隐藏和单元状态一起再次被馈送到解码器以预测第二个单词,并且该过程继续直到由虚拟单词[END]表示的句子结束被预测。
现在,我们已经定义了将源语句/文本翻译成目标语言所需的所有函数,我们将它们结合起来构建一个函数,该函数将生成翻译后的序列,给定源语言输入序列或句子:
def decode_sequence(self,input_seq,encoder_model,decoder_model):
# Encode the input as state vectors.
states_value = encoder_model.predict(input_seq)
# Generate empty target sequence of length 1.
target_seq = np.zeros((1, 1, self.num_decoder_words))
# Populate the first character of target sequence
with the start character.
target_seq[0, 0, self.target_word_index['\t']] = 1.
# Sampling loop for a batch of sequences
stop_condition = False
decoded_sentence = ''
while not stop_condition:
output_word, h, c = decoder_model.predict(
[target_seq] + states_value)
# Sample a token
sampled_word_index = np.argmax(output_word[0, -1, :])
sampled_char =
self.reverse_target_word_dict[sampled_word_index]
decoded_sentence = decoded_sentence + ' ' + sampled_char
# Exit condition: either hit max length
# or find stop character.
if (sampled_char == '\n' or
len(decoded_sentence) > self.max_decoder_seq_length):
stop_condition = True
# Update the target sequence (of length 1).
target_seq = np.zeros((1, 1, self.num_decoder_words))
target_seq[0, 0, sampled_word_index] = 1.
# Update states
states_value = [h, c]
return decoded_sentence
一旦我们训练了模型,我们就在保持数据集上运行推理并检查翻译质量。inference功能可以编码如下:
def inference(self,model,data,encoder_model,decoder_model,in_text):
in_list,out_list = [],[]
for seq_index in range(data.shape[0]):
input_seq = data[seq_index: seq_index + 1]
decoded_sentence =
self.decode_sequence(input_seq,encoder_model,decoder_model)
print('-')
print('Input sentence:', in_text[seq_index])
print('Decoded sentence:',decoded_sentence)
in_list.append(in_text[seq_index])
out_list.append(decoded_sentence)
return in_list,out_list
机器翻译模型可以通过调用 Python 脚本MachineTranslation.py在保持数据集上进行训练和验证,如下所示:
python MachineTranslation.py --path '/home/santanu/ML_DS_Catalog/Machine Translation/fra-eng/fra.txt' --epochs 20 --batch_size 32 -latent_dim 128 --num_samples 40000 --outdir '/home/santanu/ML_DS_Catalog/Machine Translation/' --verbose 1 --mode train
从机器翻译模型表现良好的保持数据集翻译几个英语句子的结果说明如下,以供参考:
('Input sentence:', u'Go.')
('Decoded sentence:', u' Va ! \n')
('Input sentence:', u'Wait!')
('Decoded sentence:', u' Attendez ! \n')
('Input sentence:', u'Call me.')
('Decoded sentence:', u' Appelle-moi ! \n')
('Input sentence:', u'Drop it!')
('Decoded sentence:', u' Laisse tomber ! \n')
('Input sentence:', u'Be nice.')
('Decoded sentence:', u' Soyez gentil ! \n')
('Input sentence:', u'Be fair.')
('Decoded sentence:', u' Soyez juste ! \n')
('Input sentence:', u"I'm OK.")
('Decoded sentence:', u' Je vais bien. \n')
('Input sentence:', u'I try.')
('Decoded sentence:', u' Je vais essayer.')
然而,也有机器翻译表现不佳的情况,如下所示:
('Input sentence:', u'Attack!')
('Decoded sentence:', u' ma ! \n')
('Input sentence:', u'Get up.')
('Decoded sentence:', u' un ! \n')
总之,前面说明的神经机器翻译实现在将相对较短的英语句子翻译成法语方面做得很好。我想强调的一点是使用热门编码向量来表示每种语言中的输入单词。当我们使用一个相对较小的 40,000 个单词的语料库时,词汇是合理的,因此,我们能够使用英语和法语词汇的大小分别为 8,658 和 16,297 的单向编码向量。使用更大的语料库,热门编码词向量的大小将进一步增加。这种稀疏的高维向量在比较两个词时没有任何相似性的概念,因为它们的余弦乘积将是零,即使两个词有几乎相同的含义。在下一节中,我们将看到如何使用在低得多的维度上工作的单词向量嵌入来解决这个问题。
单词向量嵌入
词向量嵌入可以用来表示比单热编码向量低得多的密集维度空间中的词,而不是单热编码向量。一个词的词向量嵌入 w 可以用vw∈RmT7】来表示,其中 m 是词向量嵌入的维数。正如我们所看到的,虽然一个热编码向量的每个分量只能占用{0,1}的二进制值,但是单词向量嵌入的一个分量可以占用任何实数,因此具有更密集的表示。相似和类比的概念也与词向量嵌入有关。
单词向量嵌入通常是通过诸如连续单词包方法、skip-gram、GloVe 等技术来训练的。我们不打算过多讨论它们的实现,但中心思想是以这样一种方式定义单词向量嵌入,即相似的单词被紧密地放置在 m 维欧几里德空间中:

Figure 3.7: Similarity and analogy illustration of GloVe embeddings
在上图中,我们绘制了男子、女子、国王和王后的手套单词向量嵌入的 2D·TSNE 视图。我们可以看到,男和女有着内在的相似性,就像王和皇后一样。此外,我们看到国王和男人的矢量差几乎与女王和女人的矢量差相同,这可能代表某种王权的概念。我们可以看到男:王女:皇后这样的类比除了表达词与词之间的相似性外,还可以通过词向量嵌入来表达。在下一节中,我们将讨论使用 RNN 中的嵌入层将输入单词表示为单词向量嵌入,而不是一个热门的编码向量。
嵌入层
嵌入层将输入单词的索引作为输入,并将单词的单词向量嵌入作为输出。嵌入层的维度是 R d x V ,其中 d 是词向量嵌入的维度, V 是词汇的大小。嵌入层可以根据问题学习嵌入本身,或者你可以提供一个预包含的嵌入层。在我们的例子中,我们将让神经机器翻译找出源语言和目标语言的嵌入向量,以实现良好的翻译。因此,我们定义的每个功能都应该改变,以适应嵌入层。
实施基于嵌入的 NMT
我们需要对现有的功能做一些修改,以适应嵌入层。首先,process_input将处理输入,以在不同的时间步骤中具有单词索引,而不是一个热编码向量,如下所示:
def process_input(self,input_texts,target_texts=None,verbose=True):
encoder_input_data = np.zeros(
(len(input_texts), self.max_encoder_seq_length),
dtype='float32')
decoder_input_data = np.zeros(
(len(input_texts), self.max_decoder_seq_length),
dtype='float32')
decoder_target_data = np.zeros(
(len(input_texts), self.max_decoder_seq_length,1),
dtype='float32')
if self.mode == 'train':
for i, (input_text, target_text) in
enumerate(zip(input_texts,target_texts)):
for t, word in enumerate(input_text.split(" ")):
try:
encoder_input_data[i, t] =
self.input_word_index[word]
except:
encoder_input_data[i, t] =
self.num_encoder_words
for t, word in enumerate(target_text.split(" ")):
# decoder_target_data is ahead of decoder_input_data
by one timestep
try:
decoder_input_data[i, t] =
self.target_word_index[word]
except:
decoder_input_data[i, t] =
self.num_decoder_words
if t > 0:
# decoder_target_data will be ahead by one timestep
#and will not include the start character.
try:
decoder_target_data[i, t - 1] =
self.target_word_index[word]
except:
decoder_target_data[i, t - 1] =
self.num_decoder_words
print(self.num_encoder_words)
print(self.num_decoder_words)
print(self.embedding_dim)
self.english_emb = np.zeros((self.num_encoder_words + 1,
self.embedding_dim))
self.french_emb = np.zeros((self.num_decoder_words + 1,
self.embedding_dim))
return encoder_input_data,decoder_input_data,decoder_target_data,np.array(input_texts),
np.array(target_texts)
else:
for i, input_text in enumerate(input_texts):
for t, word in enumerate(input_text.split(" ")):
try:
encoder_input_data[i, t] = self.input_word_index[word]
与早期的process_input函数相比,唯一的变化是我们不再用单一的编码向量来表示单词,而是用单词的索引来表示。另外,你有没有注意到我们正在为词汇表中没有的单词增加一个额外的单词索引?对于训练数据来说,这种情况不应该发生,但是在测试过程中,可能会出现一个词汇中没有的全新单词。
以下是输入处理的统计数据:
Number of samples: 40000
Number of unique input tokens: 8658
Number of unique output tokens: 16297
Max sequence length for inputs: 7
Max sequence length for outputs: 16
('Shape of Source Input Tensor:', (40000, 7))
('Shape of Target Input Tensor:', (40000, 16))
('Shape of Target Output Tensor:', (40000, 16, 1))
如我们所见,源和目标输入张量现在具有7和16时间步长,但是没有单热编码向量的维度。每个时间步长都包含单词的索引。
下一个变化是关于编码器和解码器网络,以适应 LSTM 层之前的嵌入层:
def model_enc_dec(self):
#Encoder Model
encoder_inp = Input(shape=(None,),name='encoder_inp')
encoder_inp1 =
Embedding(self.num_encoder_words + 1,
self.embedding_dim,weights=[self.english_emb])
(encoder_inp)
encoder = LSTM(self.latent_dim, return_state=True,name='encoder')
encoder_out,state_h, state_c = encoder(encoder_inp1)
encoder_states = [state_h, state_c]
#Decoder Model
decoder_inp = Input(shape=(None,),name='decoder_inp')
decoder_inp1 =
Embedding(self.num_decoder_words+1,self.embedding_dim,weights=
[self.french_emb])(decoder_inp)
decoder_lstm =
LSTM(self.latent_dim, return_sequences=True,
return_state=True,name='decoder_lstm')
decoder_out, _, _ =
decoder_lstm(decoder_inp1,initial_state=encoder_states)
decoder_dense = Dense(self.num_decoder_words+1,
activation='softmax',name='decoder_dense')
decoder_out = decoder_dense(decoder_out)
print(np.shape(decoder_out))
#Combined Encoder Decoder Model
model = Model([encoder_inp, decoder_inp], decoder_out)
#Encoder Model
encoder_model = Model(encoder_inp,encoder_states)
#Decoder Model
decoder_inp_h = Input(shape=(self.latent_dim,))
decoder_inp_c = Input(shape=(self.latent_dim,))
decoder_inp_state = [decoder_inp_h,decoder_inp_c]
decoder_out,decoder_out_h,decoder_out_c =
decoder_lstm(decoder_inp1,initial_state=decoder_inp_state)
decoder_out = decoder_dense(decoder_out)
decoder_out_state = [decoder_out_h,decoder_out_c]
decoder_model = Model(inputs =
[decoder_inp] + decoder_inp_state,output=
[decoder_out]+ decoder_out_state)
return model,encoder_model,decoder_model
训练模型需要用sparse_categorical_crossentropy编译,因为输出目标标签被表示为索引,而不是一个热门的编码单词向量:
def train(self,encoder_input_data,decoder_input_data,
decoder_target_data):
print("Training...")
model,encoder_model,decoder_model = self.model_enc_dec()
model.compile(optimizer='rmsprop',
loss='sparse_categorical_crossentropy')
model.fit([encoder_input_data, decoder_input_data],
decoder_target_data,
batch_size=self.batch_size,
epochs=self.epochs,
validation_split=0.2)
# Save model
model.save(self.outdir + 'eng_2_french_dumm.h5')
return model,encoder_model,decoder_model
接下来,我们需要对推理相关的函数进行修改,以适应嵌入相关的变化。用于推理的encoder_model和decoder_model现在分别使用英语和法语词汇的嵌入层。
最后,我们可以使用decoder_model和encoder_model如下创建序列生成器函数:
def decode_sequence(self,input_seq,encoder_model,decoder_model):
# Encode the input as state vectors.
states_value = encoder_model.predict(input_seq)
# Generate empty target sequence of length 1.
target_seq = np.zeros((1, 1))
# Populate the first character of target sequence
with the start character.
target_seq[0, 0] = self.target_word_index['\t']
# Sampling loop for a batch of sequences
stop_condition = False
decoded_sentence = ''
while not stop_condition:
output_word, h, c = decoder_model.predict(
[target_seq] + states_value)
# Sample a token
sampled_word_index = np.argmax(output_word[0, -1, :])
try:
sampled_char =
self.reverse_target_word_dict[sampled_word_index]
except:
sampled_char = '<unknown>'
decoded_sentence = decoded_sentence + ' ' + sampled_char
# Exit condition: either hit max length
# or find stop character.
if (sampled_char == '\n' or
len(decoded_sentence) > self.max_decoder_seq_length):
stop_condition = True
# Update the target sequence (of length 1).
target_seq = np.zeros((1, 1))
target_seq[0, 0] = sampled_word_index
# Update states
states_value = [h, c]
return decoded_sentence
模型的训练可以通过如下运行脚本来调用:
python MachineTranslation_word2vec.py --path '/home/santanu/ML_DS_Catalog-/Machine Translation/fra-eng/fra.txt' --epochs 20 --batch_size 32 --latent_dim 128 --num_samples 40000 --outdir '/home/santanu/ML_DS_Catalog-/Machine Translation/' --verbose 1 --mode train --embedding_dim 128
The model is trained on GeForce GTX 1070 GPU, and it approximately takes around 9.434 minutes to train on 32,000 records and run inference on 8,000 records. Users are highly recommended to use a GPU, since RNNs are computation heavy and might take hours to train the same model on CPU.
我们可以运行机器翻译模型的训练,并通过运行 python 脚本MachineTranslation.py在保持数据集上执行验证,如下所示:
python MachineTranslation.py --path '/home/santanu/ML_DS_Catalog/Machine Translation/fra-eng/fra.txt' --epochs 20 --batch_size 32 -latent_dim 128 --num_samples 40000 --outdir '/home/santanu/ML_DS_Catalog/Machine Translation/' --verbose 1 --mode train
从嵌入向量方法获得的结果类似于一个热门编码词向量的结果。这里提供了一些来自维持数据集推理的翻译:
Input sentence: Where is my book?
Decoded sentence: Où est mon Tom ?
-
Input sentence: He's a southpaw.
Decoded sentence: Il est en train de
-
Input sentence: He's a very nice boy.
Decoded sentence: C'est un très bon
-
Input sentence: We'll be working.
Decoded sentence: Nous pouvons faire
-
Input sentence: May I have a program?
Decoded sentence: Puis-je une ?
-
Input sentence: Can you make it safe?
Decoded sentence: Peux-tu le faire
-
Input sentence: We walked to my room.
Decoded sentence: Nous avons devons
-
Input sentence: Don't stand too close.
Decoded sentence: Ne vous en prie.
-
Input sentence: Where's the dog?
Decoded sentence: Où est le chien ?
-
Input sentence: He's a hopeless case.
Decoded sentence: Il est un fait de
-
Input sentence: Where were we?
Decoded sentence: Où fut ?
摘要
读者现在应该很好地理解几种机器翻译方法,以及神经翻译机器与传统机器有什么不同。我们现在应该已经深入了解了如何从头开始构建神经机器翻译系统,以及如何以有趣的方式扩展该系统。根据所提供的信息和实现演示,建议读者探索其他平行语料库数据集。
在这一章中,我们定义了嵌入层,但是没有加载预包含的嵌入,比如 GloVe、FastText 等等。建议读者在嵌入层加载预训练的单词向量嵌入,看看这样是否会产生更好的结果。在第 4 章、时尚产业中的风格转移使用 GANs 中,我们将通过一个与时尚产业中的风格转移相关的项目,使用生成对抗网络,这是人工智能领域的一场现代革命。****
六、基于 GANs 的时尚产业风格迁移
风格转移的概念是指将一个产品的风格渲染成另一个产品的过程。想象一下,你的时尚狂朋友买了一个蓝色印花的包,想要一双相似印花的鞋子来搭配它。在 2016 年之前,这可能是不可能的,除非他们是一位时装设计师的朋友,这位设计师必须先设计鞋子,然后才能获准生产。然而,随着生成对抗网络的最新进展,这种设计过程很容易实现。
生成性对抗网络是通过在生成器网络和鉴别器网络之间玩零和游戏来学习的网络。假设一个时装设计师想要设计一个特定结构的手提包,并且正在探索不同的印花。设计师可能会画出包的结构草图,然后将草图图像输入到一个生成性对抗网络中,为手提包设计出几种可能的最终图案。这种风格转移的过程可以让顾客自己绘制产品设计和图案,而不需要设计师的大量投入,从而对时尚行业产生巨大的影响。时装公司也可以通过推荐类似设计和风格的产品来补充顾客已经拥有的产品而受益。
在这个项目中,我们将构建一个智能人工智能系统,该系统将生成与给定手提包风格相似的鞋子,反之亦然。我们之前讨论的普通 GAN 不足以实现这个项目;我们需要的是 GAN 的定制版本,比如 DiscoGAN 和 CycleGAN。
在本章中,我们将涵盖以下主题:
- 我们将讨论 DiscoGAN 背后的工作原理和数学基础
- 我们将比较 DiscoGAN 和 CycleGAN,它们在架构和工作原理上非常相似
- 我们将训练一个 DiscoGAN,学会从一些给定的包的草图中生成包的图像
- 最后,我们将讨论与培训 DiscoGAN 相关的复杂性
技术要求
读者应该具备 Python 3 和人工智能的基本知识,才能完成本章中的项目。
本章代码文件可在 GitHub:
https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 04上找到
查看以下视频,查看正在运行的代码:
http://bit.ly/2CUZLQb
不安
A DiscoGAN 是一个生成性对抗网络,在给定域 a 中的图像的情况下,生成域 B 中产品的图像。下图所示为 DiscoGAN 网络的体系结构图:

Figure 4.1: Architectural diagram of a DiscoGAN
在域 B 中生成的图像在样式和图案上都类似于域 A 中的图像。无需在训练期间显式配对来自两个域的图像,就可以学习这种关系。鉴于物品配对是一项耗时的任务,这是一项相当强大的功能。在高层次上,它试图以神经网络的形式学习两个生成器函数GABT3】和GBAT7】,使得图像xAT11】在通过生成器GABT15】馈入时,产生在域 b 中看起来逼真的图像xABT19】此外, 当这个图像 x AB 通过另一个发电机网络 G BA 馈入时,它应该产生一个图像xABAT31】,理想情况下应该与原始图像xAT35】相同。 关于生成器函数,以下关系应该成立:******

然而,在实践中,发生器函数 G AB 和 G BA 不可能是彼此的逆函数,因此我们通过选择 L1 或 L2 范数损失来尽可能地最小化重建图像和原始图像之间的损失。L1 赋范损失基本上是每个数据点的绝对误差之和,而 L2 赋范损失代表每个数据点的平方损失之和。我们可以将单个图像的 L2 范数损失表示如下:

仅仅最小化前面的损失是不够的。我们必须确保创建的图像 x B 在域 B 中看起来是真实的。例如,如果我们将域 A 中的衣服映射到域 B 中的鞋子,我们必须确保xBT7】类似于鞋子。域 B 侧的鉴别器 D B 将检测到 x B 为假的,如果图像作为鞋不够逼真,因此也必须考虑与此相关的损失。一般来说,在训练过程中,鉴别器既被馈送生成的图像xAB= GAB(xA)又被馈送域 B 中的原始图像,我们在这里选择用 y B 来表示,这样它就学会了将真实图像与虚假图像进行分类。你可能还记得,在一个 GAN 中,生成器和鉴别器互相玩一个零和极小极大游戏,以不断变好,直到达到平衡。如果假图像看起来不够逼真,鉴别器会对其进行处罚,这意味着生成器必须学习在给定输入图像xAT37】的情况下生成更好的图像xABT33】。考虑到所有这些,我们可以将我们想要最小化的发生器的损耗公式化为重构损耗和关于鉴别器识别 x AB 为假的损耗。第二个损失将试图使生成器在域 B 中产生逼真的图像。将域 A 中的图像 x A 映射到域 B 中的图像的生成器损失可以表示如下:**

L2 准则下的重建损失可表示如下:

由于我们处理的是图像,我们可以假设 x A 是所有像素的展平向量,以符合 L2 范数术语。如果我们假设 x A 是一个矩阵,那么最好将
称为弗罗贝纽斯范数。然而,这些只是数学术语,本质上我们只是取原始图像和重建图像之间像素值差异的平方和。
让我们考虑一下发生器在追求使变换后的图像 x AB 在鉴别器看来是真实的过程中试图最小化的成本。鉴别器会试图将图像标记为假图像,因此生成器 G AB 应该产生 x AB ,使得其作为假图像的对数损失尽可能小。如果域 B 中的鉴别器DBT18】将真实图像标记为1,将虚假图像标记为0,则图像真实的概率由 D B (。),那么发电机就要让 x AB 在鉴频器网络下极有可能,让DB(xB)= DB(GAB(xA)尽量靠近1。日志丢失方面,生成器要尽量减少前面概率的负日志,基本上给我们CD(AB)T42】,如下图:**

结合(3)**(4),我们可以得到将一幅图像从域 A 映射到域 B 的总生成器开销 C_G AB ,如下图:

最大的问题是,我们就此打住吗?由于我们有来自两个域的图像,为了获得更好的映射,我们也可以从域 B 获取图像,并通过生成器 G BA 将其映射到域 A。如果我们在域 B 中取一个图像 x B ,通过生成器GBAT15】将其转换为图像xBA,域 A 的鉴别器由DAT19】给出,那么与这样的转换相关联的代价函数由下式给出:**

如果我们对这两个域中的全部图像进行求和,发电机损耗将由 (5) 和(6)的总和给出,如下所示:

现在,让我们建立鉴别器试图最小化的成本函数,以建立零和最小/最大游戏。每个域中的鉴别器将尝试区分真实图像和虚假图像,因此鉴别器GBT3】将尝试最小化成本C _ DBT7】,如下所示:**

类似地,鉴别器DAT3】会尝试最小化成本C _ DAT7】,如下所示:**

结合(8)**(9)整体鉴频器成本由 C D 给出,如下:

如果我们将GABT3】、GBAT7】、DAT11】和DBT15】的参数表示为
、
、
和
,那么网络的优化参数可以表示如下:****


对代价函数进行随机梯度下降,如 Adam,以达到最优解。请注意,如前所述,生成性对抗网络的解决方案是关于被优化的成本函数的鞍点。
-你好
A CycleGAN 基本上类似于 DiscoGAN,只是做了一个小小的修改。在 CycleGAN 中,我们可以灵活地确定重建损失相对于 GAN 损失或鉴别器损失的权重。此参数有助于根据手头的问题以正确的比例平衡损失,以帮助网络在训练时更快地收敛。CycleGAN 的其余实现与 DiscoGAN 相同。
学习从草图轮廓生成自然手袋
在这一章中,我们将从草图轮廓生成手提包,而不使用使用 DiscoGAN 的显式配对。我们将草图图像表示为属于域 A,而将自然手提包图像表示为属于域 B。将有两个生成器:一个获取域 A 的图像并将它们映射到在域 B 下看起来真实的图像,另一个则相反:一个将手提包图像从域 B 映射到在域 A 下看起来真实的图像。鉴别器将尝试从每个域中的真实图像中识别生成器生成的假图像。生成器和鉴别器会互相进行一个极大极小零和游戏。
为了训练这个网络,我们将需要两套图像,草图,或手袋的轮廓和手袋的自然图像。图片可从以下链接下载:https://people . eecs . Berkeley . edu/~ ting huiz/projects/pix2pix/dataset/edge S2 手袋. tar.gz 。
在接下来的几节中,我们将介绍在 TensorFlow 中定义 DiscoGAN 网络的过程,然后训练它使用手提包草图生成逼真的手提包图像,这些草图充当图像的边缘。我们将从定义发电机网络的架构开始。
预处理图像
edges2handbags数据集文件夹中的每个图像都包含同一图像中bag的图片和bag edges的图片。为了训练网络,我们需要将它们分离为属于两个域 A 和 B 的图像,我们已经在 DiscoGAN 的体系结构中讨论过这两个域。通过使用以下代码(image_split.py),可以将映像分为域 A 和域 B 映像:
# -*- coding: utf-8 -*-
"""
Created on Fri Apr 13 00:10:12 2018
@author: santanu
"""
import numpy as np
import os
from scipy.misc import imread
from scipy.misc import imsave
import fire
from elapsedtimer import ElapsedTimer
from pathlib import Path
import shutil
'''
Process the images in Domain A and Domain and resize appropriately
Inputs contain the Domain A and Domain B image in the same image
This program will break them up and store them in their respecective folder
'''
def process_data(path,_dir_):
os.chdir(path)
try:
os.makedirs('trainA')
except:
print(f'Folder trainA already present, cleaning up and recreating empty folder trainA')
try:
os.rmdir('trainA')
except:
shutil.rmtree('trainA')
os.makedirs('trainA')
try:
os.makedirs('trainB')
except:
print(f'Folder trainA already present, cleaning up and recreating empty folder trainB')
try:
os.rmdir('trainB')
except:
shutil.rmtree('trainB')
os.makedirs('trainB')
path = Path(path)
files = os.listdir(path /_dir_)
print('Images to process:', len(files))
i = 0
for f in files:
i+=1
img = imread(path / _dir_ / str(f))
w,h,d = img.shape
h_ = int(h/2)
img_A = img[:,:h_]
img_B = img[:,h_:]
imsave(f'{path}/trainA/{str(f)}_A.jpg',img_A)
imsave(f'{path}/trainB/{str(f)}_B.jpg',img_A)
if ((i % 10000) == 0 & (i >= 10000)):
print(f'the number of input images processed : {i}')
files_A = os.listdir(path / 'trainA')
files_B = os.listdir(path / 'trainB')
print(f'No of images written to {path}/trainA is {len(files_A)}')
print(f'No of images written to {path}/trainA is {len(files_B)}')
with ElapsedTimer('process Domain A and Domain B Images'):
fire.Fire(process_data)
image_split.py代码可以如下调用:
python image_split.py --path /media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/edges2handbags/ --_dir_ train
输出日志如下:
Folder trainA already present, cleaning up and recreating empty folder trainA
Folder trainA already present, cleaning up and recreating empty folder trainB
Images to process: 138569 the number of input images processed : 10000
the number of input images processed : 20000
the number of input images processed : 30000
.....
迪厅的发电机
DiscoGAN 的生成器是前馈卷积神经网络,其中输入和输出是图像。在网络的第一部分,图像在空间维度上按比例缩小,而输出要素图的数量随着图层的进展而增加。在网络的第二部分,图像沿空间维度按比例放大,而输出要素图的数量逐层减少。在最终输出图层中,将生成与输入具有相同空间维度的图像。如果将图像xAT3】转换为xABT7】从域 A 转换为域 B 的生成器用GABT15】表示,那么我们就有了
。**
这里展示的是build_generator函数,我们可以用它来构建 DiscoGAN 网络的生成器:
def build_generator(self,image,reuse=False,name='generator'):
with tf.variable_scope(name):
if reuse:
tf.get_variable_scope().reuse_variables()
else:
assert tf.get_variable_scope().reuse is False
"""U-Net generator"""
def lrelu(x, alpha,name='lrelu'):
with tf.variable_scope(name):
return tf.nn.relu(x) - alpha * tf.nn.relu(-x)
"""Layers used during downsampling"""
def common_conv2d(layer_input,filters,f_size=4,
stride=2,padding='SAME',norm=True,
name='common_conv2d'):
with tf.variable_scope(name):
if reuse:
tf.get_variable_scope().reuse_variables()
else:
assert tf.get_variable_scope().reuse is False
d =
tf.contrib.layers.conv2d(layer_input,filters,
kernel_size=f_size,
stride=stride,padding=padding)
if norm:
d = tf.contrib.layers.batch_norm(d)
d = lrelu(d,alpha=0.2)
return d
"""Layers used during upsampling"""
def common_deconv2d(layer_input,filters,f_size=4,
stride=2,padding='SAME',dropout_rate=0,
name='common_deconv2d'):
with tf.variable_scope(name):
if reuse:
tf.get_variable_scope().reuse_variables()
else:
assert tf.get_variable_scope().reuse is False
u =
tf.contrib.layers.conv2d_transpose(layer_input,
filters,f_size,
stride=stride,
padding=padding)
if dropout_rate:
u = tf.contrib.layers.dropout(u,keep_prob=dropout_rate)
u = tf.contrib.layers.batch_norm(u)
u = tf.nn.relu(u)
return u
# Downsampling
# 64x64 -> 32x32
dwn1 = common_conv2d(image,self.gf,stride=2,norm=False,name='dwn1')
# 32x32 -> 16x16
dwn2 = common_conv2d(dwn1,self.gf*2,stride=2,name='dwn2')
# 16x16 -> 8x8
dwn3 = common_conv2d(dwn2,self.gf*4,stride=2,name='dwn3')
# 8x8 -> 4x4
dwn4 = common_conv2d(dwn3,self.gf*8,stride=2,name='dwn4')
# 4x4 -> 1x1
dwn5 = common_conv2d(dwn4,100,stride=1,padding='valid',name='dwn5')
# Upsampling
# 4x4 -> 4x4
up1 =
common_deconv2d(dwn5,self.gf*8,stride=1,
padding='valid',name='up1')
# 4x4 -> 8x8
up2 = common_deconv2d(up1,self.gf*4,name='up2')
# 8x8 -> 16x16
up3 = common_deconv2d(up2,self.gf*2,name='up3')
# 16x16 -> 32x32
up4 = common_deconv2d(up3,self.gf,name='up4')
out_img = tf.contrib.layers.conv2d_transpose(up4,self.channels,
kernel_size=4,stride=2,
padding='SAME',
activation_fn=tf.nn.tanh)
# 32x32 -> 64x64
return out_img
在生成器函数中,我们定义了一个泄漏的 ReLU 激活函数,并使用了一个泄漏因子0.2。我们还定义了卷积层生成函数common_conv2d,用于对图像进行下采样,以及common_deconv2d,用于将下采样图像上采样到其原始空间维度。
我们使用tf.get_variable_scope().reuse_variables()用reuse选项定义生成器函数。当同一生成器函数被多次调用时,重用选项确保我们重用特定生成器使用的相同变量。当我们删除重用选项时,我们为生成器创建了一组新的变量。
例如,我们可以使用生成器函数来创建两个生成器网络,因此在第一次创建这些网络时,我们不会使用reuse选项。如果再次引用该生成器函数,我们使用reuse选项。卷积(下采样)和反卷积(上采样)过程中的激活函数是泄漏 ReLU,在此之前进行批处理归一化,以实现稳定和快速的收敛。
网络中不同图层的输出要素图数量为self.gf 或其倍数。对于我们的 DiscoGAN 网络,我们选择self.gf作为64。
发生器中需要注意的一点是输出层的tanh激活功能。这确保了由发生器产生的图像的像素值将在[-1, +1]的范围内。这使得输入图像具有在[-1, +1]范围内的像素强度变得重要,这可以通过对像素强度进行简单的逐元素变换来完成,如下所示:

同样,要将图像转换为可显示的 0-255 像素强度格式,我们需要应用逆变换,如下所示:

不适者的歧视性
DiscoGAN 的辨别者将学会在特定领域区分真实图像和虚假图像。我们将有两个鉴别器:一个用于域 A,一个用于域 b。鉴别器也是可以执行二进制分类的卷积网络。与传统的基于分类的卷积网络不同,鉴别器没有任何完全连接的层。使用步长为 2 的卷积对输入图像进行下采样,直到最后一层,输出为 1 x 1。同样,我们使用泄漏 ReLU 作为激活函数,并使用批处理标准化来实现稳定快速的收敛。以下代码显示了 TensorFlow 中的鉴别器构建函数实现:
def build_discriminator(self,image,reuse=False,name='discriminator'):
with tf.variable_scope(name):
if reuse:
tf.get_variable_scope().reuse_variables()
else:
assert tf.get_variable_scope().reuse is False
def lrelu(x, alpha,name='lrelu'):
with tf.variable_scope(name):
if reuse:
tf.get_variable_scope().reuse_variables()
else:
assert tf.get_variable_scope().reuse is False
return tf.nn.relu(x) - alpha * tf.nn.relu(-x)
"""Discriminator layer"""
def d_layer(layer_input,filters,f_size=4,stride=2,norm=True,
name='d_layer'):
with tf.variable_scope(name):
if reuse:
tf.get_variable_scope().reuse_variables()
else:
assert tf.get_variable_scope().reuse is False
d =
tf.contrib.layers.conv2d(layer_input,
filters,kernel_size=f_size,
stride=2, padding='SAME')
if norm:
d = tf.contrib.layers.batch_norm(d)
d = lrelu(d,alpha=0.2)
return d
#64x64 -> 32x32
down1 = d_layer(image,self.df, norm=False,name='down1')
#32x32 -> 16x16
down2 = d_layer(down1,self.df*2,name='down2')
#16x16 -> 8x8
down3 = d_layer(down2,self.df*4,name='down3')
#8x8 -> 4x4
down4 = d_layer(down3,self.df*8,name='down4')
#4x4 -> 1x1
down5 =
tf.contrib.layers.conv2d(down4,1,kernel_size=4,stride=1,
padding='valid')
return down5
鉴别器网络的不同层中的输出特征图的数量或者是self.df或者是它的倍数。为了我们的网络,我们把self.df当成了64。
构建网络并定义成本函数
在本节中,我们将使用生成器和鉴别器函数构建整个网络,并定义在训练过程中要优化的成本函数。张量流代码如下:
def build_network(self):
def squared_loss(y_pred,labels):
return tf.reduce_mean((y_pred - labels)**2)
def abs_loss(y_pred,labels):
return tf.reduce_mean(tf.abs(y_pred - labels))
def binary_cross_entropy_loss(logits,labels):
return tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(
labels=labels,logits=logits))
self.images_real = tf.placeholder(tf.float32,[None,self.image_size,self.image_size,self.input_dim + self.output_dim])
self.image_real_A = self.images_real[:,:,:,:self.input_dim]
self.image_real_B =
self.images_real[:,:,:,self.input_dim:self.input_dim + self.output_dim]
self.images_fake_B =
self.build_generator(self.image_real_A,
reuse=False,name='generator_AB')
self.images_fake_A =
self.build_generator(self.images_fake_B,
reuse=False,name='generator_BA')
self.images_fake_A_ =
self.build_generator(self.image_real_B,
reuse=True,name='generator_BA')
self.images_fake_B_ =
self.build_generator(self.images_fake_A_,
reuse=True,name='generator_AB')
self.D_B_fake =
self.build_discriminator(self.images_fake_B ,
reuse=False, name="discriminatorB")
self.D_A_fake =
self.build_discriminator(self.images_fake_A_,
reuse=False, name="discriminatorA")
self.D_B_real =
self.build_discriminator(self.image_real_B,
reuse=True, name="discriminatorB")
self.D_A_real =
self.build_discriminator(self.image_real_A,
reuse=True, name="discriminatorA")
self.loss_GABA =
self.lambda_l2*squared_loss(self.images_fake_A,self.image_real_A) +
binary_cross_entropy_loss(labels=tf.ones_like(self.D_B_fake),
logits=self.D_B_fake)
self.loss_GBAB =
self.lambda_l2*squared_loss(self.images_fake_B_,
self.image_real_B) +
binary_cross_entropy_loss(labels=tf.ones_like(self.D_A_fake),
logits=self.D_A_fake)
self.generator_loss = self.loss_GABA + self.loss_GBAB
self.D_B_loss_real =
binary_cross_entropy_loss(tf.ones_like(self.D_B_real),self.D_B_real)
self.D_B_loss_fake =
binary_cross_entropy_loss(tf.zeros_like(self.D_B_fake),self.D_B_fake)
self.D_B_loss = (self.D_B_loss_real + self.D_B_loss_fake) / 2.0
self.D_A_loss_real =
binary_cross_entropy_loss(tf.ones_like(self.D_A_real),self.D_A_real)
self.D_A_loss_fake =
binary_cross_entropy_loss(tf.zeros_like(self.D_A_fake),self.D_A_fake)
self.D_A_loss = (self.D_A_loss_real + self.D_A_loss_fake) / 2.0
self.discriminator_loss = self.D_B_loss + self.D_A_loss
self.loss_GABA_sum = tf.summary.scalar("g_loss_a2b", self.loss_GABA)
self.loss_GBAB_sum = tf.summary.scalar("g_loss_b2a", self.loss_GBAB)
self.g_total_loss_sum = tf.summary.scalar("g_loss", self.generator_loss)
self.g_sum = tf.summary.merge([self.loss_GABA_sum,
self.loss_GBAB_sum,self.g_total_loss_sum])
self.loss_db_sum = tf.summary.scalar("db_loss", self.D_B_loss)
self.loss_da_sum = tf.summary.scalar("da_loss", self.D_A_loss)
self.loss_d_sum = tf.summary.scalar("d_loss",self.discriminator_loss)
self.db_loss_real_sum = tf.summary.scalar("db_loss_real", self.D_B_loss_real)
self.db_loss_fake_sum = tf.summary.scalar("db_loss_fake", self.D_B_loss_fake)
self.da_loss_real_sum = tf.summary.scalar("da_loss_real", self.D_A_loss_real)
self.da_loss_fake_sum = tf.summary.scalar("da_loss_fake", self.D_A_loss_fake)
self.d_sum = tf.summary.merge(
[self.loss_da_sum, self.da_loss_real_sum, self.da_loss_fake_sum,
self.loss_db_sum, self.db_loss_real_sum, self.db_loss_fake_sum,
self.loss_d_sum]
)
trainable_variables = tf.trainable_variables()
self.d_variables =
[var for var in trainable_variables if 'discriminator' in var.name]
self.g_variables =
[var for var in trainable_variables if 'generator' in var.name]
print ('Variable printing start :' )
for var in self.d_variables:
print(var.name)
self.test_image_A =
tf.placeholder(tf.float32,[None, self.image_size,
self.image_size,self.input_dim], name='test_A')
self.test_image_B =
tf.placeholder(tf.float32,[None, self.image_size,
self.image_size,self.output_c_dim], name='test_B')
self.saver = tf.train.Saver()
在构建网络中,我们首先定义了 L2 赋范误差和二元交叉熵误差的代价函数。L2 赋范误差将用作重构损失,而二进制交叉熵将用作鉴别器损失。然后,我们使用生成器函数为两个域中的图像定义占位符,并为每个域中的假图像定义张量流运算。我们还通过传递特定于该域的假图像和真实图像来定义鉴别器输出的 ops。除此之外,我们还为这两个域中的重建图像定义了张量流运算。
一旦定义了 ops,我们使用它们来计算损失函数,考虑图像的重建损失和归因于鉴别器的损失。请注意,我们使用了相同的生成器函数来定义从域 A 到域 B 的生成器,以及从域 B 到域 A 的生成器。我们唯一不同的做法是为两个网络提供不同的名称:generator_AB和generator_BA。由于变量范围被定义为name,这两个生成器将有不同的以提供的名称为前缀的权重集。
下表显示了我们需要跟踪的不同损失变量。相对于发生器或鉴别器的参数,所有这些损失都需要最小化:
| 不同损失的变量 | 描述 |
| self.D_B_loss_real | 鉴别器DBT3】对 B 域真实图像进行分类时的二值交叉熵损失(This loss is to be minimized with respect to the parameters of the discriminator DB.) |
| self.D_B_loss_fake | 鉴别器DBT3】二值交叉熵损失在 B 域伪图像分类中的应用(This loss is to be minimized with respect to the parameters of the discriminator DB.) |
| self.D_A_loss_real | 鉴别器DAT3】在对域 A 中的真实图像进行分类时的二值交叉熵损失(This loss is to be minimized with respect to the parameters of the discriminator DA.) |
| self.D_A_loss_fake | 鉴别器DAT3】二值交叉熵损失在分类域 A 中的伪图像(This loss is to be minimized with respect to the parameters of the discriminator DA.) |
| self.loss_GABA | 通过两个生成器 G AB 和 G BA 将域 A 中的图像映射到域 B 再映射回域 A 的重建损失)加上域 B 中鉴别器标记为真实图像的伪图像GAB(xA)的二值交叉熵(This loss is to be minimized with respect to the parameters of the generators GAB and GBA.) |
| self.loss_GBAB | 通过两个生成器 G BA 和 G AB 加上伪图像GBA(xB)的二值交叉熵将域 B 中的图像映射到 A 再映射回 B 的重构损失被域 A 中的鉴别器标记为真实图像(This loss is to be minimized with respect to the parameters of the generators GAB and GBA.) |
前四个损耗组成鉴别器损耗,相对于鉴别器DAT3】和DBT7】的参数需要最小化。最后两个损耗组成发电机损耗,相对于发电机 G AB 和 G BA 的参数需要最小化。**
损失变量通过tf.summary.scaler 与 TensorBoard 关联,以便在训练过程中监控这些损失,确保损失以期望的方式减少。稍后,我们将在训练过程中看到这些损失痕迹在 TensorBoard 中的样子。
构建培训流程
在train_network函数中,我们首先为发生器和鉴别器损失函数定义优化器。我们将 Adam 优化器用于生成器和鉴别器,因为这是随机梯度下降优化器的高级版本,在训练 GANs 时非常有效。Adam 使用梯度的衰减平均值,很像稳定梯度的动量,以及平方梯度的衰减平均值,提供了关于成本函数曲率的信息。与tf.summary定义的不同损耗相关的变量被写入日志文件,因此可以通过 TensorBoard 进行监控。以下是train功能的详细代码:
def train_network(self):
self.learning_rate = tf.placeholder(tf.float32)
self.d_optimizer = tf.train.AdamOptimizer(self.learning_rate,beta1=self.beta1,beta2=self.beta2).minimize(self.discriminator_loss,var_list=self.d_variables)
self.g_optimizer = tf.train.AdamOptimizer(self.learning_rate,beta1=self.beta1,beta2=self.beta2).minimize(self.generator_loss,var_list=self.g_variables)
self.init_op = tf.global_variables_initializer()
self.sess = tf.Session()
self.sess.run(self.init_op)
#self.dataset_dir = '/home/santanu/Downloads/DiscoGAN/edges2handbags/train/'
self.writer = tf.summary.FileWriter("./logs", self.sess.graph)
count = 1
start_time = time.time()
for epoch in range(self.epoch):
data_A = os.listdir(self.dataset_dir + 'trainA/')
data_B = os.listdir(self.dataset_dir + 'trainB/')
data_A = [ (self.dataset_dir + 'trainA/' + str(file_name)) for file_name in data_A ]
data_B = [ (self.dataset_dir + 'trainB/' + str(file_name)) for file_name in data_B ]
np.random.shuffle(data_A)
np.random.shuffle(data_B)
batch_ids = min(min(len(data_A), len(data_B)), self.train_size) // self.batch_size
lr = self.l_r if epoch < self.epoch_step else self.l_r*(self.epoch-epoch)/(self.epoch-self.epoch_step)
for id_ in range(0, batch_ids):
batch_files = list(zip(data_A[id_ * self.batch_size:(id_ + 1) * self.batch_size],
data_B[id_ * self.batch_size:(id_ + 1) * self.batch_size]))
batch_images = [load_train_data(batch_file, self.load_size, self.fine_size) for batch_file in batch_files]
batch_images = np.array(batch_images).astype(np.float32)
# Update G network and record fake outputs
fake_A, fake_B, _, summary_str = self.sess.run(
[self.images_fake_A_,self.images_fake_B,self.g_optimizer,self.g_sum],
feed_dict={self.images_real: batch_images, self.learning_rate:lr})
self.writer.add_summary(summary_str, count)
[fake_A,fake_B] = self.pool([fake_A, fake_B])
# Update D network
_, summary_str = self.sess.run(
[self.d_optimizer,self.d_sum],
feed_dict={self.images_real: batch_images,
# self.fake_A_sample: fake_A,
# self.fake_B_sample: fake_B,
self.learning_rate: lr})
self.writer.add_summary(summary_str, count)
count += 1
print(("Epoch: [%2d] [%4d/%4d] time: %4.4f" % (
epoch, id_, batch_ids, time.time() - start_time)))
if count % self.print_freq == 1:
self.sample_model(self.sample_dir, epoch, id_)
if count % self.save_freq == 2:
self.save_model(self.checkpoint_dir, count)
正如我们在代码末尾看到的那样,sample_model函数在训练过程中不时被调用,以检查基于来自另一个域的输入图像在一个域中生成的图像的质量。基于save_freq,模型也会定期保存。
我们在前面的代码中提到的sample_model功能和save_model功能在这里举例说明,以供参考:
def sample_model(self, sample_dir, epoch, id_):
if not os.path.exists(sample_dir):
os.makedirs(sample_dir)
data_A = os.listdir(self.dataset_dir + 'trainA/')
data_B = os.listdir(self.dataset_dir + 'trainB/')
data_A = [ (self.dataset_dir + 'trainA/' + str(file_name)) for
file_name in data_A ]
data_B = [ (self.dataset_dir + 'trainB/' + str(file_name)) for
file_name in data_B ]
np.random.shuffle(data_A)
np.random.shuffle(data_B)
batch_files =
list(zip(data_A[:self.batch_size], data_B[:self.batch_size]))
sample_images =
[load_train_data(batch_file, is_testing=True) for
batch_file in batch_files]
sample_images = np.array(sample_images).astype(np.float32)
fake_A, fake_B = self.sess.run(
[self.images_fake_A_,self.images_fake_B],
feed_dict={self.images_real: sample_images}
)
save_images(fake_A, [self.batch_size, 1],
'./{}/A_{:02d}_{:04d}.jpg'.format(sample_dir, epoch, id_))
save_images(fake_B, [self.batch_size, 1],
'./{}/B_{:02d}_{:04d}.jpg'.format(sample_dir, epoch, id_))
在这个sample_model功能中,从域 A 中随机选择的图像被拍摄并馈送到生成器 G AB 以产生域 B 中的图像。类似地,从域 B 中随机选择的图像被馈送到生成器GBAT8】以产生域 A 中的图像。这些输出图像由两个生成器在不同的时期生成,并且批次被保存在样本文件夹中,以查看生成器在训练过程中是否随着时间的推移而改进以产生更好的图像质量。
使用 TensorFlow 保存功能保存模型的save_model功能如下所示:
def save_model(self,checkpoint_dir,step):
model_name = "discogan.model"
model_dir = "%s_%s" % (self.dataset_dir, self.image_size)
checkpoint_dir = os.path.join(checkpoint_dir, model_dir)
if not os.path.exists(checkpoint_dir):
os.makedirs(checkpoint_dir)
self.t(self.sess,
os.path.join(checkpoint_dir, model_name),
global_step=step)
GAN 训练的重要参数值
在本节中,我们将讨论用于训练 DiscoGAN 的不同参数值。下表列出了这些内容:
| 参数名称 | 变量名和值集 | 原理 |
| 亚当优化器的学习速率 | self.l_r = 2e-4 | 我们应该始终训练一个学习率低的 GAN 网络,以获得更好的稳定性,DiscoGAN 也不例外。 |
| 亚当优化器的衰减率 | self.beta1 = 0.5 self.beta2 = 0.99 | 参数beta1定义梯度的衰减平均值,而参数beta2定义梯度平方的衰减平均值。 |
| 世 | self.epoch = 200 | 200在这个实现中,对于 DiscoGAN 网络的收敛来说,时代已经足够好了。 |
| 批量 | self.batch_size = 64 | 64的批量对于这个实现很有效。然而,由于资源限制,我们可能不得不选择较小的批量。 |
| 学习率线性下降的时期 | epoch_step = 10 | 在epoch_step指定的时代数后,学习率线性下降,由以下方案确定:lr = self.l_r if epoch < self.epoch_step else self.l_r*(self.epoch-epoch)/(self.epoch-self.epoch_step) |
调用培训
我们之前展示的所有函数都是在一个DiscoGAN()类中创建的,该类具有在__init__ 函数中声明的重要参数值,如下面的代码块所示。训练网络时唯一需要通过的两个参数是dataset_dir和需要进行训练的epochs的数量
def __init__(self,dataset_dir,epochs=200):
# Input shape
self.dataset_dir = dataset_dir
self.lambda_l2 = 1.0
self.image_size = 64
self.input_dim = 3
self.output_dim = 3
self.batch_size = 64
self.df = 64
self.gf = 64
self.channels = 3
self.output_c_dim = 3
self.l_r = 2e-4
self.beta1 = 0.5
self.beta2 = 0.99
self.weight_decay = 0.00001
self.epoch = epochs
self.train_size = 10000
self.epoch_step = 10
self.load_size = 64
self.fine_size = 64
self.checkpoint_dir = 'checkpoint'
self.sample_dir = 'sample'
self.print_freq = 5
self.save_freq = 10
self.pool = ImagePool()
return None
现在我们已经定义了训练模型所需的所有内容,我们可以通过process_main 函数调用训练,如下所示:
def process_main(self):
self.build_network()
self.train_network()
我们之前为培训演示的端到端代码在脚本cycledGAN_edges_to_bags.py中。我们可以通过运行 python 脚本cycledGAN_edges_to_bags.py来训练模型,如下所示:
python cycledGAN_edges_to_bags.py process_main --dataset_dir /media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/edges2handbags/ epochs 100
脚本cycledGAN_edges_to_bags.py执行的输出日志如下:
Epoch: [ 0] [ 0/ 156] time: 3.0835
Epoch: [ 0] [ 1/ 156] time: 3.9093
Epoch: [ 0] [ 2/ 156] time: 4.3661
Epoch: [ 0] [ 3/ 156] time: 4.8208
Epoch: [ 0] [ 4/ 156] time: 5.2821
Epoch: [ 0] [ 5/ 156] time: 6.2380
Epoch: [ 0] [ 6/ 156] time: 6.6960
Epoch: [ 0] [ 7/ 156] time: 7.1528
Epoch: [ 0] [ 8/ 156] time: 7.6138
Epoch: [ 0] [ 9/ 156] time: 8.0732
Epoch: [ 0] [ 10/ 156] time: 8.8163
Epoch: [ 0] [ 11/ 156] time: 9.6669
Epoch: [ 0] [ 12/ 156] time: 10.1256
Epoch: [ 0] [ 13/ 156] time: 10.5846
Epoch: [ 0] [ 14/ 156] time: 11.0427
Epoch: [ 0] [ 15/ 156] time: 11.9135
Epoch: [ 0] [ 16/ 156] time: 12.3712
Epoch: [ 0] [ 17/ 156] time: 12.8290
Epoch: [ 0] [ 18/ 156] time: 13.2899
Epoch: [ 0] [ 19/ 156] time: 13.7525
.......
监控发生器和鉴别器损耗
损失可以在 TensorBoard 仪表板中监控。可以按如下方式调用 TensorBoard 仪表板:
- 从终端运行以下命令:
tensorboard --logdir=./logs
./logs是存储程序专用的张量板日志的目的地,应在程序中定义如下:
self.writer = tf.summary.FileWriter("./logs", self.sess.graph)
- 一旦执行了步骤 1 中的命令,导航到 TensorBoard 的
localhost:6006站点:
下面的截图显示了在项目中实施的 DiscoGAN 培训期间,TensorBoard 中看到的发生器和鉴别器损耗的一些痕迹:

Figure 4.2: Tensorboard Scalars section containing the traces for different losses
以下屏幕截图显示了随着培训的进行,域 A 中鉴别器的损耗成分:

Figure 4.3: Losses of discriminator in domain A
从前面的截图中,我们可以看到不同批次的域 A 中鉴别器的损失。da_loss 是da_loss_real和 da_loss_fake 损失的总和。da_loss_real稳定下降,因为鉴别器很容易学会在域 A 中识别真实图像,而伪图像的损失稳定在 0.69 左右,这是当二进制分类器输出 1/2 概率的类时可以预期的logloss。发生这种情况是因为生成器也在同时学习使假图像看起来真实,因此使得鉴别器很难容易地将生成器图像分类为假图像。在域 B 的鉴别器的损失概况看起来类似于在域 a 的上一个截图中说明的那些
现在让我们看看发电机的损耗曲线,如下所示:

Figure 4.4: Loss profiles for the generators of the DiscoGAN
g_loss_a2b 是从域 A 到域 B 再到域 B 重建图像的组合生成器损失,以及与使变换后的图像在域 B 中看起来逼真相关联的二进制交叉熵损失。类似地, g_loss_b2a是从域 B 到域 A 再到域 B 重建图像的组合发生器损失,也是与使转换后的图像在域 A 中看起来真实相关的二进制交叉熵损失。正如我们从上一个截图中的张量板视觉效果中看到的,这两个损失分布以及它们的总和g_loss随着批次的进行而稳步减少。
因为训练生成性对抗网络通常是相当棘手的,所以监控它们的损失情况的进展以了解训练是否按预期进行是有意义的。
DiscoGAN 生成的样本图像
当我们到达这一章的结尾时,让我们来看看由 DiscoGAN 在这两个域中生成的一些图像:

Figure 4.5: Handbag images generated given the sketches
以下截图包含生成的手提袋草图图像(域 A) :

Figure 4.6: Sketches generated given the handbag images
我们可以看到,DiscoGAN 在将任何一个领域的图像转换为另一个领域的高质量逼真图像方面做得很好。
摘要
我们现在已经到了这一章的结尾。现在,您应该非常熟悉 DiscoGAN 的技术细节和实现复杂性。我们在这一章中探索的概念可以用来实现各种各样的生成性对抗网络,这些网络具有适合当前问题的微妙变化。这个 DiscoGAN 网络的端到端实现位于 GitHub 存储库中,位于https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 04。
在第五章视频字幕应用中,我们将会看到视频到文本的翻译应用,它们属于人工智能中专家系统的范畴。
五、视频字幕应用
随着视频制作的速度以指数级的速度增长,视频已经成为一种重要的传播媒介。然而,由于缺乏适当的字幕,更多的观众仍然无法观看视频。
视频字幕是翻译视频以生成有意义的内容摘要的艺术,在计算机视觉和机器学习领域是一项具有挑战性的任务。传统的视频字幕方法并没有产生多少成功的故事。然而,随着最近人工智能在深度学习帮助下的发展,视频字幕最近获得了大量的关注。卷积神经网络和递归神经网络的能力使得构建端到端的企业级视频字幕系统成为可能。卷积神经网络对视频中的图像帧进行处理,提取重要特征,再由递归神经网络依次处理,生成有意义的视频摘要。视频字幕系统的一些重要应用如下:
- 工业工厂安全措施的自动监控
- 基于通过视频字幕获得的内容对视频进行聚类
- 银行、医院和其他公共场所更好的安全系统
- 在网站中搜索视频以获得更好的用户体验
通过深度学习构建智能视频字幕系统主要需要两种类型的数据:视频和文本字幕,它们是训练端到端系统的标签。
作为本章的一部分,我们将讨论以下内容:
- 讨论美国有线电视新闻网和 LSTM 在视频字幕中的作用
- 探索序列到序列视频字幕系统的体系结构
- 利用 s 序列到序列—视频到文本架构构建视频字幕系统
在下一节中,我们将介绍卷积神经网络和 LSTM 版本的递归神经网络如何用于构建端到端视频字幕系统。
技术要求
你将需要具备 Python 3、TensorFlow、Keras 和 OpenCV 的基本知识。
本章代码文件可在 GitHub:
https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 05上找到
查看以下视频,查看正在运行的代码:
http://bit.ly/2BeXK1c
视频字幕中的 CNNs 和 LSTMs
视频减去音频可以被认为是以顺序方式排列的图像的集合。可以使用在特定图像分类问题上训练的卷积神经网络,例如 ImageNet ,从这些图像中提取重要特征。预训练网络的最后一个完全连接层的激活可以用于从视频的顺序采样图像中导出特征。从视频中顺序采样图像的频率取决于视频中内容的类型,并且可以通过训练进行优化。
下图所示(图 5.1 )是用于从视频中提取特征的预训练神经网络:

Figure 5.1: Video image feature extraction using pre-trained neural networks
从上图中我们可以看到,视频中顺序采样的图像通过预先训练的卷积神经网络,最后一个完全连接层中 4,096 单元的激活作为输出。如果将时间 t 的视频图像表示为xtT8】,最后一个全连接层的输出表示为ft∈R4096,则ft =fw(xt)。这里, W 表示卷积神经网络直到最后一个完全连接层的权重。
这些系列的输出特征为 f 1、 f 2、。。。。。 f t。。 f N 可以作为输入馈送到递归神经网络,该神经网络学习基于输入特征生成文本标题,如下图所示(图 5.2 ):

Figure 5.2: LSTM in the processing of the sequential input features from CNN
从上图中我们可以看到,生成的特征 f 1、 f 2、。。。。。 f t。。来自预训练卷积神经网络的 f N 由 LSTM 依次处理,以产生文本输出 o 1、 o 2、。。。。。 o t。。 o N、 是给定视频的文字说明。例如,上图中视频的标题可能是一名戴着黄色头盔的男子正在工作:
o1,o2,。。。。。ot。。。oN = {“A”,“人”“在”“A”“黄色”“头盔”“正在”“工作”}
现在我们已经很好地了解了视频字幕在深度学习框架中的工作原理,接下来让我们在下一节讨论一个更高级的视频字幕网络,称为序列到序列视频字幕。在本章中,我们将使用相同的网络架构来构建视频字幕系统。
一种序列到序列视频字幕系统
序列到序列的体系结构基于一篇名为序列到序列—视频到文本的论文,作者是 Subhashini Venugopalan、Marcus Rohrbach、Jeff Donahue、Raymond Mooney、Trevor Darrell 和 Kate Saenko。论文可以在https://arxiv.org/pdf/1505.00487.pdf找到。
在下图(图 5.3 )中,说明了一个基于前一篇论文的序列到序列视频字幕神经网络架构:

Figure 5.3: Sequence-to-sequence video-captioning network architecture
序列到序列模型像以前一样通过预先训练的卷积神经网络处理视频图像帧,最后一个完全连接的层的输出激活被作为特征馈送到随后的 LSTMs。如果我们在时间步长 t 将预训练卷积神经网络的最后一个完全连接层的输出激活表示为 f t ∈ R 4096 ,那么我们将具有来自视频的 N 图像帧的 N 这样的特征向量。这些 N 特征向量 f1,f2,.。。。。f t 。。。f N 依次馈入 LSTMs,生成文字字幕。
有两个背靠背的 LSTMs,LSTMs 中的序列数是来自视频的图像帧数和字幕词汇表中文本字幕的最大长度之和。如果网络在视频的 N 个图像帧上训练,并且词汇表中的最大文本字幕长度为 M ,则 LSTMs 在 (N+M) 个时间步长上训练。在 N 时间步骤中,第一 LSTM 处理特征向量 f1、f2、.。。。。f t 。。。f N 依次,其产生的隐藏状态馈入第二 LSTM。在这些 N 时间步长中,第二 LSTM 不需要文本输出目标。如果我们将第一个 LSTM 在时间步长 t 处的隐藏状态表示为 h t ,则第一个 N 时间步长对第二个 LSTM 的输入为 h t 。请注意,从 N+1 时间步长到第一个 LSTM 的输入是零填充的,因此输入对隐藏状态htT31】t>N没有影响。请注意,这并不能保证 t > N 的隐藏状态htT37】总是会相同。其实我们可以选择将hTT41】作为hTT45】给第二个 LSTM 进行任意时间步长 t > N.**
从 (N+1) 时间步开始,第二个 LSTM 需要一个文本输出目标。在任何时间步 t > N 输入的是ht,wt-1,其中htT13】是第一个 LSTM 在时间步twt-1T19】是在时间步 (t-1)的文字说明词
在 (N+1) 时间步,输入到第二个 LSTM 的单词wNT7】是由<bos>表示的句子的开始。一旦产生句尾符号<eos>,网络被训练停止产生字幕词。总而言之,这两个 LSTMs 的设置方式是,一旦处理完所有视频图像帧特征
,它们就开始产生文本字幕词。
处理时间步长 t > N 的第二个 LSTM 输入的另一种方法是只馈送【wT-1而不是【hT,wT-1,并在时间步长 T 即【hT,c传递第一个 LSTM 的隐藏和单元状态这样一个视频字幕网络的架构可以说明如下(见图 5.4 ):

Figure 5.4: An alternate architecture for sequence to sequence model
预先训练的卷积神经网络通常具有共同的结构,例如VGG16、VGG19、ResNet,并且在 ImageNet 上预先训练。然而,我们可以基于从我们正在为其构建视频字幕系统的领域中的视频提取的图像来重新训练这些架构。我们还可以选择一个全新的 CNN 架构,并在特定领域的视频图像上对其进行训练。
到目前为止,我们已经涵盖了使用本节中说明的序列到序列架构开发视频字幕系统的所有技术先决条件。请注意,本节中建议的替代架构设计是为了鼓励读者尝试几种设计,看看哪种设计最适合给定的问题和数据集。
从下一部分开始,我们将致力于构建智能视频字幕系统。
视频字幕系统的数据
我们通过在MSVD dataset上训练模型来构建视频字幕系统,T0 是一个来自微软的字幕视频库。所需数据可从以下链接下载:http://www . cs . utexas . edu/users/ml/clamp/video description/YouTube eclipse . tar。 视频的文字说明可通过以下链接获得:https://github . com/jazzsaxsifa/video _ to _ sequence/files/387979/video _ corps . CSV . zip。
MSVD dataset里有大概1,938的视频。我们将使用这些来训练序列到序列视频字幕系统。还要注意的是,我们将在图 5.3 所示的序列对序列模型上构建模型。然而,建议读者尝试在图 5.4 中展示的架构上训练一个模型,并看看它的运行情况。
处理视频图像以创建 CNN 功能
一旦我们从指定位置下载了数据,下一个任务就是处理视频图像帧,从预先训练的卷积神经网络的最后一个完全连接的层中提取特征。我们使用在 ImageNet 上预先训练的VGG16卷积神经网络。我们将激活从VGG16的最后一个完全连接的层中取出。由于VGG16的最后一个完全连通层有4096个单位,我们的特征向量ftT8】对于每个时间步长 t 是一个4096,维度向量即ft∈R4096。
在通过VGG16处理视频中的图像之前,需要从视频中进行采样。我们从视频中取样图像,使得每个视频都有80帧。在处理来自VGG16的80图像帧之后,每个视频将具有80特征向量 f1、f2、。。。。。f t 。。。f 80 。这些特征将被馈送到 LSTMs 以生成文本序列。我们在喀拉斯使用预先训练的VGG16模型。我们创建一个VideoCaptioningPreProcessing类,首先通过函数video_to_frames从每个视频中提取80视频帧作为图像,然后这些视频帧由函数extract_feats_pretrained_cnn中预先训练的VGG16卷积神经网络处理。
extract_feats_pretrained_cnn的输出是每个视频帧的维度4096的 CNN 特征。因为我们处理的是每个视频的80帧,所以每个视频都会有80这样的4096维度向量。
video_to_frames功能可以编码如下:
def video_to_frames(self,video):
with open(os.devnull, "w") as ffmpeg_log:
if os.path.exists(self.temp_dest):
print(" cleanup: " + self.temp_dest + "/")
shutil.rmtree(self.temp_dest)
os.makedirs(self.temp_dest)
video_to_frames_cmd = ["ffmpeg",'-y','-i', video,
'-vf', "scale=400:300",
'-qscale:v', "2",
'{0}/%06d.jpg'.format(self.temp_dest)]
subprocess.call(video_to_frames_cmd,
stdout=ffmpeg_log, stderr=ffmpeg_log)
从前面的代码中,我们可以看到在video_to_frames功能中,ffmpeg工具用于将视频转换为 JPEG 格式的图像帧。为图像帧指定给ffmpeg的尺寸是300 x 400。关于ffmpeg工具的更多信息,请参考以下链接:https://www.ffmpeg.org/。
在extract_feats_pretrained_cnnfunction中已经建立了从最后一个完全连接的层中提取特征的预先训练的 CNN 模型。该函数的代码如下:
# Extract the features from the pre-trained CNN
def extract_feats_pretrained_cnn(self):
model = self.model_cnn_load()
print('Model loaded')
if not os.path.isdir(self.feat_dir):
os.mkdir(self.feat_dir)
#print("save video feats to %s" % (self.dir_feat))
video_list = glob.glob(os.path.join(self.video_dest, '*.avi'))
#print video_list
for video in tqdm(video_list):
video_id = video.split("/")[-1].split(".")[0]
print(f'Processing video {video}')
#self.dest = 'cnn_feat' + '_' + video_id
self.video_to_frames(video)
image_list =
sorted(glob.glob(os.path.join(self.temp_dest, '*.jpg')))
samples = np.round(np.linspace(
0, len(image_list) - 1,self.frames_step))
image_list = [image_list[int(sample)] for sample in samples]
images =
np.zeros((len(image_list),self.img_dim,self.img_dim,
self.channels))
for i in range(len(image_list)):
img = self.load_image(image_list[i])
images[i] = img
images = np.array(images)
fc_feats = model.predict(images,batch_size=self.batch_cnn)
img_feats = np.array(fc_feats)
outfile = os.path.join(self.feat_dir, video_id + '.npy')
np.save(outfile, img_feats)
# cleanup
shutil.rmtree(self.temp_dest)
我们首先使用model_cnn_load函数加载预先训练好的 CNN 模型,然后根据指定给ffmpeg.的采样频率,使用video_to_frames函数为每个视频提取几个视频帧作为图像。我们没有处理通过ffmpeg创建的视频中的所有图像帧,而是使用np.linspace函数拍摄了80等间距的图像帧。使用load_image功能将ffmpeg生成的图像调整到224 x 224的空间维度。最后,这些调整大小的图像通过预先训练的 VGG16 卷积神经网络(CNN),并且在输出层之前的最后一个完全连接层的输出被提取作为特征。这些提取的特征向量存储在numpy阵列中,并在下一阶段由 LSTM 网络处理以产生视频字幕。本节中定义的功能model_cnn_load定义如下:
def model_cnn_load(self):
model = VGG16(weights = "imagenet", include_top=True,input_shape =
(self.img_dim,self.img_dim,self.channels))
out = model.layers[-2].output
model_final = Model(input=model.input,output=out)
return model_final
从前面的代码可以看出,我们正在加载一个在 ImageNet 上预先训练的VGG16卷积神经网络,并且我们正在提取第二个最后一层(索引为-2)的输出作为我们的维度特征向量4096。
在输入美国有线电视新闻网之前,处理原始ffmpeg图像的图像读取和调整大小功能load_image定义如下:
def load_image(self,path):
img = cv2.imread(path)
img = cv2.resize(img,(self.img_dim,self.img_dim))
return img
预处理脚本可以通过调用以下命令来运行:
python VideoCaptioningPreProcessing.py process_main --video_dest '/media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/data/' --feat_dir '/media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/features/' --temp_dest '/media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/temp/' --img_dim 224 --channels 3 --batch_size=128 --frames_step 80
该预处理步骤的输出是作为扩展的 numpy 数组对象npy写入的维度4096的80特征向量。每个视频都有自己的存储在feat_dir中的numpy数组对象。预处理步骤持续约 28 分钟,从日志中我们可以看到如下内容:
Processing video /media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/data/jmoT2we_rqo_0_5.avi
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▋| 1967/1970 [27:57<00:02, 1.09it/s]Processing video /media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/data/NKtfKR4GNjU_0_20.avi
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▊| 1968/1970 [27:58<00:02, 1.11s/it]Processing video /media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/data/4cgzdXlJksU_83_90.avi
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▉| 1969/1970 [27:59<00:01, 1.08s/it]Processing video /media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/data/0IDJG0q9j_k_1_24.avi
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1970/1970 [28:00<00:00, 1.06s/it]
28.045 min: VideoCaptioningPreProcessing
在下一节中,我们将讨论视频标签字幕的预处理。
处理视频的标记字幕
corpus.csv文件包含文字说明形式的视频描述(见图 5.5 )。下面的截图显示了数据片段。我们可以删除一些[VideoID,Start,End]组合记录,并将其作为测试文件,供以后评估:

Figure 5.5: A snapshot of the format of the captions file
VideoID、Start、End列组合形成视频名称,格式如下:VideoID_Start_End.avi。基于视频名称,卷积神经网络VGG16的特征被存储为VideoID_Start_End.npy。在下面的代码块中说明了处理视频的文本标题和创建从VGG16到视频图像特征的路径交叉引用的功能:
def get_clean_caption_data(self,text_path,feat_path):
text_data = pd.read_csv(text_path, sep=',')
text_data = text_data[text_data['Language'] == 'English']
text_data['video_path'] =
text_data.apply(lambda row:
row['VideoID']+'_'+str(int(row['Start']))+'_'+str(int(row['End']))+'.npy',
axis=1)
text_data['video_path'] =
text_data['video_path'].map(lambda x: os.path.join(feat_path, x))
text_data =
text_data[text_data['video_path'].map(lambda x: os.path.exists(x))]
text_data =
text_data[text_data['Description'].map(lambda x: isinstance(x, str))]
unique_filenames = sorted(text_data['video_path'].unique())
data =
text_data[text_data['video_path'].map(lambda x: x in unique_filenames)]
return data
在定义的get_data函数中,我们从video_corpus.csv文件中移除所有非英文的标题。完成后,我们通过首先构建视频名称(作为VideoID、Start和End要素的串联)并为其添加要素目录名称前缀来形成到视频要素的链接。然后,我们删除所有不指向特征目录中任何实际视频特征向量或具有无效非文本描述的视频语料库文件记录。
数据如下图所示(图 5.6 ):

Figure 5.6: Caption data after preprocessing
构建训练和测试数据集
一旦我们训练好模型,我们就想评估模型的运行情况。我们可以根据测试集中的视频内容来验证为测试数据集生成的标题。可以使用以下功能创建列车测试集数据集。我们可以在训练期间创建测试数据集,并在模型训练完成后将其用于评估:
def train_test_split(self,data,test_frac=0.2):
indices = np.arange(len(data))
np.random.shuffle(indices)
train_indices_rec = int((1 - test_frac)*len(data))
indices_train = indices[:train_indices_rec]
indices_test = indices[train_indices_rec:]
data_train, data_test =
data.iloc[indices_train],data.iloc[indices_test]
data_train.reset_index(inplace=True)
data_test.reset_index(inplace=True)
return data_train,data_test
一般保留 20%的数据用于评估是一种公平的做法。
构建模型
在本节中,将说明核心的模型构建练习。我们首先在文本标题的词汇表中为单词定义一个嵌入层,然后是两个 LSTMs。权重self.encode_W和self.encode_b用于从卷积神经网络中降低特征ftT6】的维数。对于第二个 LSTM (LSTM 2),任何时候的另一个输入步骤 t > N 是前一个单词 w t-1 ,以及来自第一个 LSTM (LSTM 1)的输出 h t 。用于wt-1的单词嵌入被馈送到 LSTM 2,而不是原始的一个热编码向量。对于第一个 N (self.video_lstm_step),LSTM 1 处理来自 CNN 的输入特征 f t ,输出隐藏状态 h t (输出 1) 馈给 LSTM 2。在这个编码阶段,LSTM 2 不接收任何字wt-1T36】作为输入。**
从 (N+1) 时间步长,我们进入解码阶段,其中,与来自 LSTM 1 的htT7】(output 1)一起,前一个时间步长字嵌入向量 w t-1 被馈送到 LSTM 2。在该阶段,没有输入到 LSTM 1,因为所有的特征 f t 在时间步骤 N 用尽。解码阶段的时间步数由self.caption_lstm_step决定。
现在,如果我们用一个函数 f2、来表示 LSTM 2 的活动,那么 f 2 (h t、wt-1)= h2t,其中 h 2t 是 LSTM 2 在时间步长 t 的隐藏状态。通过 softmax 函数将时间 t 处的隐藏状态 h 2t 转换为输出单词上的概率分布,并且具有最高概率的一个被选择作为下一个单词
:
= 

这些权重 W ho 和 b、在以下代码块中定义为self.word_emb_W和self.word_emb_b。更多详细信息,请参考build_model功能。为了便于解释,构建功能被分解为 3 个部分。构建模型有 3 个主要单元
- 定义阶段:定义变量、字幕词的嵌入层和序列到序列模型的两个 LSTMs。
- 编码阶段:在这个阶段我们通过 LSTM1 的时间步长传递视频帧图像特征,并将每个时间步长的隐藏状态传递到 LSTM 2 上。该活动一直进行到时间步长 N ,其中 N 是每个视频的采样视频帧图像的数量。
- 解码阶段:在解码阶段,LSTM 2 开始生成文本字幕。关于时间步长,解码阶段从步长 N +1 开始。从 LSTM 2 的每个时间步长生成的字连同 LSTM 1 的隐藏状态一起作为输入被馈送到下一个状态。
模型变量的定义
视频字幕模型的变量和其他相关定义可定义如下:
Defining the weights associated with the Network
with tf.device('/cpu:0'):
self.word_emb =
tf.Variable(tf.random_uniform([self.n_words, self.dim_hidden],
-0.1, 0.1), name='word_emb')
self.lstm1 =
tf.nn.rnn_cell.BasicLSTMCell(self.dim_hidden, state_is_tuple=False)
self.lstm2 =
tf.nn.rnn_cell.BasicLSTMCell(self.dim_hidden, state_is_tuple=False)
self.encode_W =
tf.Variable( tf.random_uniform([self.dim_image,self.dim_hidden],
-0.1, 0.1), name='encode_W')
self.encode_b =
tf.Variable( tf.zeros([self.dim_hidden]), name='encode_b')
self.word_emb_W =
tf.Variable(tf.random_uniform([self.dim_hidden,self.n_words],
-0.1,0.1), name='word_emb_W')
self.word_emb_b =
tf.Variable(tf.zeros([self.n_words]), name='word_emb_b')
# Placeholders
video =
tf.placeholder(tf.float32, [self.batch_size,
self.video_lstm_step, self.dim_image])
video_mask =
tf.placeholder(tf.float32, [self.batch_size, self.video_lstm_step])
caption =
tf.placeholder(tf.int32, [self.batch_size, self.caption_lstm_step+1])
caption_mask =
tf.placeholder(tf.float32, [self.batch_size, self.caption_lstm_step+1])
video_flat = tf.reshape(video, [-1, self.dim_image])
image_emb = tf.nn.xw_plus_b( video_flat, self.encode_W,self.encode_b )
image_emb =
tf.reshape(image_emb, [self.batch_size, self.lstm_steps, self.dim_hidden])
state1 = tf.zeros([self.batch_size, self.lstm1.state_size])
state2 = tf.zeros([self.batch_size, self.lstm2.state_size])
padding = tf.zeros([self.batch_size, self.dim_hidden])
所有相关变量以及占位符都是由前面的代码定义的。
编码阶段
在编码阶段,我们通过 LSTM 1 的时间步长顺序处理每个视频图像帧特征(来自 CNN 最后一层)。视频图像帧的尺寸为4096.在将那些高维视频帧特征向量馈送到 LSTM 1 之前,它们被缩小到较小的尺寸512.
LSTM 1 处理视频帧图像,并且在每个时间步骤将隐藏状态传递给 LSTM 2,并且该过程持续到时间步骤 N ( self.video_lstm_step)。编码器的代码如下:
probs = []
loss = 0.0
# Encoding Stage
for i in range(0, self.video_lstm_step):
if i > 0:
tf.get_variable_scope().reuse_variables()
with tf.variable_scope("LSTM1"):
output1, state1 = self.lstm1(image_emb[:,i,:], state1)
with tf.variable_scope("LSTM2"):
output2, state2 = self.lstm2(tf.concat([padding, output1],1), state2)
解码阶段
在解码阶段,产生视频字幕的字。LSTM 1 号没有其他输入。然而,LSTM 1 向前滚动,并且产生的隐藏状态被馈送到 LSTM 2 时间步长,如前所述。每一步 LSTM 2 的另一个输入是字幕中前一个单词的嵌入向量。因此,在每一步,LSTM 2 产生一个新的字幕词,其条件是在先前时间步中预测的词以及在那个时间步来自 LSTM 1 的隐藏状态。解码器的代码如下:
# Decoding Stage to generate Captions
for i in range(0, self.caption_lstm_step):
with tf.device("/cpu:0"):
current_embed = tf.nn.embedding_lookup(self.word_emb, caption[:, i])
tf.get_variable_scope().reuse_variables()
with tf.variable_scope("LSTM1"):
output1, state1 = self.lstm1(padding, state1)
with tf.variable_scope("LSTM2"):
output2, state2 =
self.lstm2(tf.concat([current_embed, output1],1), state2)
为每个小批量建立损失
优化的损失是关于在 LSTM 2 的每个时间步长从整个字幕词语料库中预测正确词的分类交叉熵损失。对于批次中的所有数据点,在解码阶段的每个步骤中累积相同的数据。解码阶段与损失累积相关的代码如下:
labels = tf.expand_dims(caption[:, i+1], 1)
indices = tf.expand_dims(tf.range(0, self.batch_size, 1), 1)
concated = tf.concat([indices, labels],1)
onehot_labels =
tf.sparse_to_dense(concated, tf.stack
([self.batch_size,self.n_words]), 1.0, 0.0)
logit_words =
tf.nn.xw_plus_b(output2, self.word_emb_W, self.word_emb_b)
# Computing the loss
cross_entropy =
tf.nn.softmax_cross_entropy_with_logits(logits=logit_words,
labels=onehot_labels)
cross_entropy =
cross_entropy * caption_mask[:,i]
probs.append(logit_words)
current_loss = tf.reduce_sum(cross_entropy)/self.batch_size
loss = loss + current_loss
损耗可以通过任何合理的梯度下降优化器进行优化,例如 Adam、RMSprop 等。我们将选择Adam进行实验,因为它在大多数深度学习优化中表现良好。我们可以使用 Adam 优化器定义训练操作,如下所示:
with tf.variable_scope(tf.get_variable_scope(),reuse=tf.AUTO_REUSE):
train_op = tf.train.AdamOptimizer(self.learning_rate).minimize(loss)
为标题创建词汇
在本节中,我们为视频标题创建词汇。我们创建了一些额外的单词,如下所示:
eos => End of Sentence
bos => Beginning of Sentence
pad => When there is no word to feed,required by the LSTM 2 in the initial N time steps
unk => A substitute for a word that is not included in the vocabulary
LSTM 2,其中一个字是一个输入,将需要这四个额外的符号。对于 (N+1) 时间步长,当我们开始生成字幕时,我们馈入上一个时间步长 w t-1 的单词。对于要生成的第一个单词,没有有效的前一时间步单词,因此我们输入虚拟单词<bos>,表示句子的开始。同样,当我们到达最后一个时间步时, w t-1 是字幕的最后一个字。我们训练模型输出最后一个单词<eos>,表示句子的结尾。当遇到句尾时,LSTM 2 停止发出任何进一步的单词。
用一个例子来说明,就拿天气好这句话来说吧。以下是从时间步长 (N+1) 开始的 LSTM 2 的输入和输出标签:
| 时间步长 | 输入 | 输出 |
| N+1 | <bos>、hN+1T4】 | 这 |
| N+2 | 第、hN+2T3】 | 天气 |
| N+3 | 天气,hN+3T3】 | 是 |
| N+4 | 是,hN+4T3】 | 美丽的 |
| N+5 | 漂亮,hN+5T3】 | <eos> |
创建词汇的create_word_dict功能详细说明如下:
def create_word_dict(self,sentence_iterator, word_count_threshold=5):
word_counts = {}
sent_cnt = 0
for sent in sentence_iterator:
sent_cnt += 1
for w in sent.lower().split(' '):
word_counts[w] = word_counts.get(w, 0) + 1
vocab = [w for w in word_counts if word_counts[w] >= word_count_threshold]
idx2word = {}
idx2word[0] = '<pad>'
idx2word[1] = '<bos>'
idx2word[2] = '<eos>'
idx2word[3] = '<unk>'
word2idx = {}
word2idx['<pad>'] = 0
word2idx['<bos>'] = 1
word2idx['<eos>'] = 2
word2idx['<unk>'] = 3
for idx, w in enumerate(vocab):
word2idx[w] = idx+4
idx2word[idx+4] = w
word_counts['<pad>'] = sent_cnt
word_counts['<bos>'] = sent_cnt
word_counts['<eos>'] = sent_cnt
word_counts['<unk>'] = sent_cnt
return word2idx,idx2word
训练模型
在这一节中,我们将所有部分放在一起,构建训练视频字幕模型的功能。
首先,我们创建单词词汇词典,结合来自训练和测试数据集的视频字幕。完成后,我们调用build_model功能来创建视频字幕网络,将两个 LSTMs 结合起来。对于每个有特定开始和结束的视频,有多个输出视频字幕。在每一批中,从可用的多个视频字幕中随机选择具有特定开始和结束的视频的输出视频字幕。LSTM 2 的输入文本标题被调整为在时间步长 (N+1) 具有作为<bos>的起始单词,而输出文本标题的结束单词被调整为具有作为<eos>的最终文本标签。每个时间步长上分类交叉熵损失的总和被作为特定视频的总交叉熵损失。在每个时间步骤中,我们计算整个单词词汇表的分类交叉熵损失,可以表示如下:

这里,
是在时间步长 t 时实际目标词的一个热编码向量,
是来自模型的预测概率向量。
在训练期间捕获每个时期的损失,以了解损失减少的本质。这里需要注意的另一件重要的事情是,我们正在使用张量流的tf.train.saver函数保存训练好的模型,以便我们可以恢复模型来进行推理。
train功能的详细代码如下图,供参考:
def train(self):
data = self.get_data(self.train_text_path,self.train_feat_path)
self.train_data,self.test_data = self.train_test_split(data,test_frac=0.2)
self.train_data.to_csv(f'{self.path_prj}/train.csv',index=False)
self.test_data.to_csv(f'{self.path_prj}/test.csv',index=False)
print(f'Processed train file written to {self.path_prj}/train_corpus.csv')
print(f'Processed test file written to {self.path_prj}/test_corpus.csv')
train_captions = self.train_data['Description'].values
test_captions = self.test_data['Description'].values
captions_list = list(train_captions)
captions = np.asarray(captions_list, dtype=np.object)
captions = list(map(lambda x: x.replace('.', ''), captions))
captions = list(map(lambda x: x.replace(',', ''), captions))
captions = list(map(lambda x: x.replace('"', ''), captions))
captions = list(map(lambda x: x.replace('\n', ''), captions))
captions = list(map(lambda x: x.replace('?', ''), captions))
captions = list(map(lambda x: x.replace('!', ''), captions))
captions = list(map(lambda x: x.replace('\\', ''), captions))
captions = list(map(lambda x: x.replace('/', ''), captions))
self.word2idx,self.idx2word = self.create_word_dict(captions,
word_count_threshold=0)
np.save(self.path_prj/ "word2idx",self.word2idx)
np.save(self.path_prj/ "idx2word" ,self.idx2word)
self.n_words = len(self.word2idx)
tf_loss, tf_video,tf_video_mask,tf_caption,tf_caption_mask, tf_probs,train_op=
self.build_model()
sess = tf.InteractiveSession()
saver = tf.train.Saver(max_to_keep=100, write_version=1)
tf.global_variables_initializer().run()
loss_out = open('loss.txt', 'w')
val_loss = []
for epoch in range(0,self.epochs):
val_loss_epoch = []
index = np.arange(len(self.train_data))
self.train_data.reset_index()
np.random.shuffle(index)
self.train_data = self.train_data.loc[index]
current_train_data =
self.train_data.groupby(['video_path']).first().reset_index()
for start, end in zip(
range(0, len(current_train_data),self.batch_size),
range(self.batch_size,len(current_train_data),self.batch_size)):
start_time = time.time()
current_batch = current_train_data[start:end]
current_videos = current_batch['video_path'].values
current_feats = np.zeros((self.batch_size,
self.video_lstm_step,self.dim_image))
current_feats_vals = list(map(lambda vid: np.load(vid),current_videos))
current_feats_vals = np.array(current_feats_vals)
current_video_masks = np.zeros((self.batch_size,self.video_lstm_step))
for ind,feat in enumerate(current_feats_vals):
current_feats[ind][:len(current_feats_vals[ind])] = feat
current_video_masks[ind][:len(current_feats_vals[ind])] = 1
current_captions = current_batch['Description'].values
current_captions = list(map(lambda x: '<bos> ' + x, current_captions))
current_captions = list(map(lambda x: x.replace('.', ''),
current_captions))
current_captions = list(map(lambda x: x.replace(',', ''),
current_captions))
current_captions = list(map(lambda x: x.replace('"', ''),
current_captions))
current_captions = list(map(lambda x: x.replace('\n', ''),
current_captions))
current_captions = list(map(lambda x: x.replace('?', ''),
current_captions))
current_captions = list(map(lambda x: x.replace('!', ''),
current_captions))
current_captions = list(map(lambda x: x.replace('\\', ''),
current_captions))
current_captions = list(map(lambda x: x.replace('/', ''),
current_captions))
for idx, each_cap in enumerate(current_captions):
word = each_cap.lower().split(' ')
if len(word) < self.caption_lstm_step:
current_captions[idx] = current_captions[idx] + ' <eos>'
else:
new_word = ''
for i in range(self.caption_lstm_step-1):
new_word = new_word + word[i] + ' '
current_captions[idx] = new_word + '<eos>'
current_caption_ind = []
for cap in current_captions:
current_word_ind = []
for word in cap.lower().split(' '):
if word in self.word2idx:
current_word_ind.append(self.word2idx[word])
else:
current_word_ind.append(self.word2idx['<unk>'])
current_caption_ind.append(current_word_ind)
current_caption_matrix =
sequence.pad_sequences(current_caption_ind, padding='post',
maxlen=self.caption_lstm_step)
current_caption_matrix =
np.hstack( [current_caption_matrix,
np.zeros([len(current_caption_matrix), 1] ) ] ).astype(int)
current_caption_masks =
np.zeros( (current_caption_matrix.shape[0],
current_caption_matrix.shape[1]) )
nonzeros =
np.array( list(map(lambda x: (x != 0).sum() + 1,
current_caption_matrix ) ))
for ind, row in enumerate(current_caption_masks):
row[:nonzeros[ind]] = 1
probs_val = sess.run(tf_probs, feed_dict={
tf_video:current_feats,
tf_caption: current_caption_matrix
})
_, loss_val = sess.run(
[train_op, tf_loss],
feed_dict={
tf_video: current_feats,
tf_video_mask : current_video_masks,
tf_caption: current_caption_matrix,
tf_caption_mask: current_caption_masks
})
val_loss_epoch.append(loss_val)
print('Batch starting index: ', start, " Epoch: ", epoch, " loss: ",
loss_val, ' Elapsed time: ', str((time.time() - start_time)))
loss_out.write('epoch ' + str(epoch) + ' loss ' + str(loss_val) + '\n')
# draw loss curve every epoch
val_loss.append(np.mean(val_loss_epoch))
plt_save_dir = self.path_prj / "loss_imgs"
plt_save_img_name = str(epoch) + '.png'
plt.plot(range(len(val_loss)),val_loss, color='g')
plt.grid(True)
plt.savefig(os.path.join(plt_save_dir, plt_save_img_name))
if np.mod(epoch,9) == 0:
print ("Epoch ", epoch, " is done. Saving the model ...")
saver.save(sess, os.path.join(self.path_prj, 'model'), global_step=epoch)
loss_out.close()
从前面的代码中我们可以看到,我们通过基于batch_size.随机选择一组视频来创建每个批次
对于每个视频,标签都是随机选择的,因为同一个视频已经由多个标记者进行了标记。对于每个选定的标题,我们清理标题文本,并将其中的单词转换为它们的单词索引。字幕的目标移动了 1 个时间步长,因为在每一步中,我们根据字幕中的前一个单词来预测单词。针对指定数量的时期训练模型,并且以指定的时期间隔(此处为9)检查模型。
培训结果
可以使用以下命令训练模型:
python Video_seq2seq.py process_main --path_prj '/media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/' --caption_file video_corpus.csv --feat_dir features --cnn_feat_dim 4096 --h_dim 512 --batch_size 32 --lstm_steps 80 --video_steps=80 --out_steps 20 --learning_rate 1e-4--epochs=100
| 参数 | 值 |
| Optimizer | Adam |
| learning rate | 1e-4 |
| Batch size | 32 |
| Epochs | 100 |
| cnn_feat_dim | 4096 |
| lstm_steps | 80 |
| out_steps | 20 |
| h_dim | 512 |
培训的输出日志如下:
Batch starting index: 1728 Epoch: 99 loss: 17.723186 Elapsed time: 0.21822428703308105
Batch starting index: 1760 Epoch: 99 loss: 19.556421 Elapsed time: 0.2106935977935791
Batch starting index: 1792 Epoch: 99 loss: 21.919321 Elapsed time: 0.2206578254699707
Batch starting index: 1824 Epoch: 99 loss: 15.057275 Elapsed time: 0.21275663375854492
Batch starting index: 1856 Epoch: 99 loss: 19.633915 Elapsed time: 0.21492290496826172
Batch starting index: 1888 Epoch: 99 loss: 13.986136 Elapsed time: 0.21542596817016602
Batch starting index: 1920 Epoch: 99 loss: 14.300303 Elapsed time: 0.21855640411376953
Epoch 99 is done. Saving the model ...
24.343 min: Video Captioning
正如我们所看到的,使用 GeForce Zotac 1070 GPU 在 100 个时代训练该模型需要大约 24 分钟。
每个时期的训练损失减少如下所示(图 5.7 ):

Figure 5.7 Loss profile during training
从上图(图 5.7 )可以看出,在最初的几个历元中,损耗降低幅度较高,然后在历元 80 前后逐渐降低。在下一节中,我们将说明该模型如何为看不见的视频生成字幕。
用看不见的测试视频推断
出于推理的目的,我们构建了一个生成器函数build_generator,复制build_model的逻辑来定义所有模型变量和加载模型并在其上运行推理所需的 TensorFlow 操作:
def build_generator(self):
with tf.device('/cpu:0'):
self.word_emb =
tf.Variable(tf.random_uniform([self.n_words, self.dim_hidden],
-0.1, 0.1), name='word_emb')
self.lstm1 =
tf.nn.rnn_cell.BasicLSTMCell(self.dim_hidden, state_is_tuple=False)
self.lstm2 =
tf.nn.rnn_cell.BasicLSTMCell(self.dim_hidden, state_is_tuple=False)
self.encode_W =
tf.Variable(tf.random_uniform([self.dim_image,self.dim_hidden],
-0.1, 0.1), name='encode_W')
self.encode_b =
tf.Variable(tf.zeros([self.dim_hidden]), name='encode_b')
self.word_emb_W =
tf.Variable(tf.random_uniform([self.dim_hidden,self.n_words],
-0.1,0.1), name='word_emb_W')
self.word_emb_b =
tf.Variable(tf.zeros([self.n_words]), name='word_emb_b')
video =
tf.placeholder(tf.float32, [1, self.video_lstm_step, self.dim_image])
video_mask =
tf.placeholder(tf.float32, [1, self.video_lstm_step])
video_flat = tf.reshape(video, [-1, self.dim_image])
image_emb = tf.nn.xw_plus_b(video_flat, self.encode_W, self.encode_b)
image_emb = tf.reshape(image_emb, [1, self.video_lstm_step, self.dim_hidden])
state1 = tf.zeros([1, self.lstm1.state_size])
state2 = tf.zeros([1, self.lstm2.state_size])
padding = tf.zeros([1, self.dim_hidden])
generated_words = []
probs = []
embeds = []
for i in range(0, self.video_lstm_step):
if i > 0:
tf.get_variable_scope().reuse_variables()
with tf.variable_scope("LSTM1"):
output1, state1 = self.lstm1(image_emb[:, i, :], state1)
with tf.variable_scope("LSTM2"):
output2, state2 =
self.lstm2(tf.concat([padding, output1],1), state2)
for i in range(0, self.caption_lstm_step):
tf.get_variable_scope().reuse_variables()
if i == 0:
with tf.device('/cpu:0'):
current_embed =
tf.nn.embedding_lookup(self.word_emb, tf.ones([1], dtype=tf.int64))
with tf.variable_scope("LSTM1"):
output1, state1 = self.lstm1(padding, state1)
with tf.variable_scope("LSTM2"):
output2, state2 =
self.lstm2(tf.concat([current_embed, output1],1), state2)
logit_words =
tf.nn.xw_plus_b( output2, self.word_emb_W, self.word_emb_b)
max_prob_index = tf.argmax(logit_words, 1)[0]
generated_words.append(max_prob_index)
probs.append(logit_words)
with tf.device("/cpu:0"):
current_embed =
tf.nn.embedding_lookup(self.word_emb, max_prob_index)
current_embed = tf.expand_dims(current_embed, 0)
embeds.append(current_embed)
return video, video_mask, generated_words, probs, embeds
推理功能
在推理过程中,我们调用build_generator来定义模型和推理所需的其他张量流操作,然后我们使用tf.train.Saver.restoreutility从训练好的模型中加载保存的权重。一旦模型被加载并准备好为每个测试视频进行推断,我们提取其相应的视频帧图像预处理特征(来自 CNN),并将其传递给模型进行推断:
def inference(self):
self.test_data = self.get_test_data(self.test_text_path,self.test_feat_path)
test_videos = self.test_data['video_path'].unique()
self.idx2word =
pd.Series(np.load(self.path_prj / "idx2word.npy").tolist())
self.n_words = len(self.idx2word)
video_tf, video_mask_tf, caption_tf, probs_tf, last_embed_tf =
self.build_generator()
sess = tf.InteractiveSession()
saver = tf.train.Saver()
saver.restore(sess,self.model_path)
f = open(f'{self.path_prj}/video_captioning_results.txt', 'w')
for idx, video_feat_path in enumerate(test_videos):
video_feat = np.load(video_feat_path)[None,...]
if video_feat.shape[1] == self.frame_step:
video_mask = np.ones((video_feat.shape[0], video_feat.shape[1]))
else:
continue
gen_word_idx =
sess.run(caption_tf, feed_dict={video_tf:video_feat,
video_mask_tf:video_mask})
gen_words = self.idx2word[gen_word_idx]
punct = np.argmax(np.array(gen_words) == '<eos>') + 1
gen_words = gen_words[:punct]
gen_sent = ' '.join(gen_words)
gen_sent = gen_sent.replace('<bos> ', '')
gen_sent = gen_sent.replace(' <eos>', '')
print(f'Video path {video_feat_path} : Generated Caption {gen_sent}')
print(gen_sent,'\n')
f.write(video_feat_path + '\n')
f.write(gen_sent + '\n\n')
可以通过调用以下命令来运行推理:
python Video_seq2seq.py process_main --path_prj '/media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/' --caption_file '/media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/test.csv' --feat_dir features --mode inference --model_path '/media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/model-99'
评估结果
评估结果很有希望。测试集0lh_UWF9ZP4_82_87.avi和8MVo7fje_oE_139_144.avi的两个视频的推理结果如下:
在下面的截图中,我们举例说明了对视频video0lh_ UWF9ZP4_82_87.avi的推断结果:

Inference on video 0lh_UWF9ZP4_82_87.avi using the trained model
在下面的截图中,我们说明了对另一个video8MVo7fje_oE_139_144.avi的推断结果:

Inference on a video/8MVo7fje_oE_139_144.avi using the trained model
从前面的截图、可以看出,训练好的模型很好的为提供的测试视频想出了一个好的标题。
这个项目的代码可以在 GitHub 位置找到。VideoCaptioningPreProcessing.py模块可用于预处理视频并创建卷积神经网络特征,而Video_seq2seq.py模块可用于训练端到端视频字幕系统并对其进行推理。
摘要
现在,我们激动人心的视频字幕项目已经结束。您应该能够使用 TensorFlow 和 Keras 构建自己的视频字幕系统。您还应该能够使用本章中解释的技术诀窍来开发其他高级模型,包括卷积神经网络和递归神经网络。下一章将使用受限的玻尔兹曼机器构建一个智能推荐系统。期待您的参与!
六、智能推荐系统
随着互联网上大量数字信息的出现,用户高效地获取物品成为一个挑战。推荐系统是信息过滤系统,它处理数字数据过载的问题,根据用户的偏好、兴趣和行为,从以前的活动中推断出项目或信息。
在本章中,我们将涵盖以下主题:
- 介绍推荐系统
- 基于潜在因子分解的协同过滤
- 利用深度学习进行潜在因素协同过滤
- 使用受限玻尔兹曼机器 ( RBM )构建推荐系统
- 训练限制性商业惯例的对比分歧
- 使用 RBMs 的协同过滤
- 使用 RBMs 实现协同过滤应用程序
技术要求
读者应该具备 Python 3 和人工智能的基本知识,才能完成本章中的项目。
本章代码文件可在 GitHub:
https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 06上找到
查看以下视频,查看正在运行的代码:
http://bit.ly/2Sgc0R3
什么是推荐系统?
推荐系统在当今世界无处不在。无论是网飞的电影推荐还是亚马逊的产品推荐,推荐系统都产生了巨大的影响。推荐系统可以大致分为基于内容的过滤系统、协同过滤系统和基于潜在因素的过滤推荐系统。基于内容的过滤依赖于基于项目内容的手工编码特征。基于用户如何对现有项目进行评级,创建用户简档,并将用户提供的排名给予这些项目:

Figure 6.1: Content-based filtering illustration
从上图中我们可以看到(图 6.1 ),用户 A 购买了名为深度学习和神经网络的书籍。由于人工智能这本书的内容与这两本书相似,基于内容的推荐系统已经向用户 A 推荐了这本书人工智能。正如我们所看到的,在基于内容的过滤中,根据用户的偏好向用户推荐项目。这不涉及其他用户如何评价这本书。
协同过滤尝试识别属于给定用户的相似用户,然后推荐相似用户喜欢、购买或评价较高的用户项目。这一般称为用户-用户协同过滤。相反,找到与给定项目相似的项目,并向同样喜欢、购买或高度评价其他相似项目的用户推荐项目。这个名字叫做物品-物品协同过滤:

Figure 6.2: item-item collaborative filtering illustration
在上图(图 6.2 )中,用户 A 和用户 B 的购书品味非常相似。用户 A 最近购买了书籍深度学习和神经网络。由于用户 B 与用户 A 非常相似,因此用户-用户协同推荐系统也将这些书籍推荐给用户 B 。
基于潜在因子分解的推荐系统
基于潜在因子分解的过滤推荐方法试图通过分解评级来发现代表用户和项目简档的潜在特征。与基于内容的过滤特征不同,这些潜在特征是不可解释的,并且可以表示复杂的特征。例如,在电影推荐系统中,潜在特征之一可能代表幽默、悬疑和浪漫的特定比例的线性组合。通常,对于已经评级的项目,用户 i 对项目 j 给出的评级rijT3】可以表示为
。其中 u i 是基于潜在因素的用户简档向量, v i 是基于相同潜在因素的项目向量:

Figure 6.3: Latent factor-based filtering illustration
上图所示(图 6.3 )是一种基于潜在因素的推荐方法,其中评分矩阵Rm x nT5】已经分解为用户简档矩阵Um x kT9】和项目简档矩阵Pn x kT13】的乘积,其中 k 是模型的潜在因素数。基于这些简档,我们可以通过计算用户简档和项目简档的内部产品来推荐到目前为止还没有被用户购买的项目。内部产品给出了用户在购买产品时可能给出的暂定评级。**
创建这些用户和项目简档的方法之一是,在根据用户和项目的某种形式的平均值填写缺失值后,对评分矩阵执行奇异值分解 ( 奇异值分解)。根据奇异值分解,评级矩阵 R 可以分解如下:

我们可以将用户档案矩阵取为 US 1/2 ,然后将物品档案矩阵转置为 S 1/2 V T 形成潜在因素模型。您可能会有一个问题,当分级矩阵中缺少与用户未分级的电影相对应的条目时,如何执行 SVD。常见的方法是在执行 SVD 之前,通过用户的平均评分或全球评分平均值来估算缺失的评分。
潜在因素协同过滤的深度学习
您可以利用深度学习方法来导出给定维度的用户和项目简档向量,而不是使用奇异值分解。
对于每个用户 i、可以通过一个嵌入层定义一个用户向量 u i ∈ R k 。同样,对于每个项目 j、可以通过另一个嵌入层定义一个项目向量vj∈Rk。然后,用户 i 对某个项目 j 的评分rijT19】可以表示为uIT27】和vjT31】的点积,如图所示:**

您可以修改神经网络,为用户和项目添加偏见。假设我们想要 k 潜在组件,那么 m 用户的嵌入矩阵 U 的尺寸将是 m x k 。类似地, n 项的嵌入矩阵 V 的尺寸为 n x k 。
在基于深度学习的潜在因素模型部分,我们将使用这种嵌入方法来创建基于100K Movie Lens数据集的推荐系统。数据集可以从https://grouplens.org/datasets/movielens/下载。
我们将使用u1.base作为训练数据集,u1.test作为保持测试数据集。
基于深度学习的潜在因素模型
在潜在因素协同过滤的深度学习部分讨论的基于深度学习的潜在因素模型可以按照图 6.4 所示进行设计:

Figure 6.4: Deep learning-based latent factor model on Movie Lens 100 K dataset
user_ID和movie_ID从它们对应的嵌入矩阵中提取用户和电影嵌入向量。在图中, embedding_1 代表用户标识的嵌入层, embedding_2 代表电影标识的嵌入层。用户嵌入向量和电影嵌入向量的点积在点 _1 层执行,以输出评分分数(一到五)。定义模型的代码如下所示:
def model(max_users,max_movies,latent_factors):
user_ID = Input(shape=(1,))
movie_ID = Input(shape=(1,))
x = Embedding(max_users,latent_factors, input_length=1)(user_ID)
y = Embedding(max_movies,latent_factors, input_length=1)(movie_ID)
out = dot([x,y],axes=2,normalize=False)
out= Reshape((1,))(out)
model = Model(inputs=[user_ID,movie_ID],outputs=out)
print(model.summary())
return model
在前面的model功能中,max_users和max_movies分别确定用户和电影嵌入矩阵的大小。模型的参数只不过是用户和电影嵌入矩阵的组成部分。所以如果我们有 m 用户和 n 电影,并且我们选择了 k 的潜在维度,那么我们就有 m x k + n x k = (m + n)k 参数需要学习。
数据处理功能可以编码如下:
data_dir = Path('/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/')
outdir = Path('/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/')
#Function to read data
def create_data(rating,header_cols):
data = pd.read_csv(rating,header=None,sep='\t')
#print(data)
data.columns = header_cols
return data
#Movie ID to movie name dict
def create_movie_dict(movie_file):
print(movie_file)
df = pd.read_csv(movie_file,sep='|', encoding='latin-1',header=None)
movie_dict = {}
movie_ids = list(df[0].values)
movie_name = list(df[1].values)
for k,v in zip(movie_ids,movie_name):
movie_dict[k] = v
return movie_dict
# Function to create training validation and test data
def train_val(df,val_frac=None):
X,y = df[['userID','movieID']].values,df['rating'].values
#Offset the ids by 1 for the ids to start from zero
X = X - 1
if val_frac != None:
X_train, X_test, y_train, y_val = train_test_split(X, y, test_size=val_frac,random_state=0)
return X_train, X_val, y_train, y_val
else:
return X,y
需要注意的一点是user_ID和movie_ID都减去了1,以确保标识从0开始,而不是从1开始,以便嵌入层可以正确引用它们。
调用数据处理和训练的代码如下:
#Data processing and model training
train_ratings_df = create_data(f'{data_dir}/u1.base',['userID','movieID','rating','timestamp'])
test_ratings_df = create_data(f'{data_dir}/u1.test',['userID','movieID','rating','timestamp'])
X_train, X_val,y_train, y_val = train_val(train_ratings_df,val_frac=0.2)
movie_dict = create_movie_dict(f'{data_dir}/u.item')
num_users = len(train_ratings_df['userID'].unique())
num_movies = len(train_ratings_df['movieID'].unique())
print(f'Number of users {num_users}')
print(f'Number of movies {num_movies}')
model = model(num_users,num_movies,40)
plot_model(model, to_file='model_plot.png', show_shapes=True, show_layer_names=True)
model.compile(loss='mse',optimizer='adam')
callbacks = [EarlyStopping('val_loss', patience=2),
ModelCheckpoint(f'{outdir}/nn_factor_model.h5', save_best_only=True)]
model.fit([X_train[:,0],X_train[:,1]], y_train, nb_epoch=30, validation_data=([X_val[:,0],X_val[:,1]], y_val), verbose=2, callbacks=callbacks)
该模型的建立是为了存储关于验证错误的最佳模型。从训练日志中我们可以看到,该模型收敛于大约0.8872的验证 RMSE,如下所示:
Train on 64000 samples, validate on 16000 samples
Epoch 1/30
- 4s - loss: 8.8970 - val_loss: 2.0422
Epoch 2/30
- 3s - loss: 1.3345 - val_loss: 1.0734
Epoch 3/30
- 3s - loss: 0.9656 - val_loss: 0.9704
Epoch 4/30
- 3s - loss: 0.8921 - val_loss: 0.9317
Epoch 5/30
- 3s - loss: 0.8452 - val_loss: 0.9097
Epoch 6/30
- 3s - loss: 0.8076 - val_loss: 0.8987
Epoch 7/30
- 3s - loss: 0.7686 - val_loss: 0.8872
Epoch 8/30
- 3s - loss: 0.7260 - val_loss: 0.8920
Epoch 9/30
- 3s - loss: 0.6842 - val_loss: 0.8959
我们现在在看不见的测试数据集上评估模型的性能。可以调用以下代码对测试数据集运行推理:
#Evaluate on the test dataset
model = load_model(f'{outdir}/nn_factor_model.h5')
X_test,y_test = train_val(test_ratings_df,val_frac=None)
pred = model.predict([X_test[:,0],X_test[:,1]])[:,0]
print('Hold out test set RMSE:',(np.mean((pred - y_test)**2)**0.5))
pred = np.round(pred)
test_ratings_df['predictions'] = pred
test_ratings_df['movie_name'] = test_ratings_df['movieID'].apply(lambda x:movie_dict[x])
从日志中我们可以看到,RMSE 的保持测试大约在0.95左右,如下所示:
Hold out test set RMSE: 0.9543926404313371
现在,我们通过调用下面一行代码为测试数据集中 ID 为1的用户评估模型的性能:
#Check evaluation results for the UserID = 1
test_ratings_df[test_ratings_df['userID'] == 1].sort_values(['rating','predictions'],ascending=False)
从以下结果中我们可以看出(图 6.5 )该模型在预测训练中未看到的电影的收视率方面做得很好:

Figure 6.5: Results of evaluation for UserID 1
与深度学习法潜在因素法相关的代码可以在https://github . com/packt publishing/Intelligent-Projects-use-Python/tree/master/chapter 06找到。
SVD++
一般来说,奇异值分解不会捕捉数据中可能存在的用户和项目偏差。一种被称为 SVD++的方法考虑了潜在因子分解方法中的用户和项目偏差,并且在诸如网飞挑战赛等比赛中非常流行。
进行基于潜在因素的推荐最常见的方式是将用户简档和偏好定义为 u i ∈ R k 和 b i ∈ R ,将项目简档和偏好定义为vI∈Rk和bj**∈11 用户 i 对项目 j 提供的等级
定义如下:

是所有收视率的全局均值。
用户简档和项目简档然后通过最小化在预测由用户评级的所有项目的评级时的误差平方和来确定。要优化的平方误差损失可以表示如下:

I ij 是一个指标功能,如果用户 i 有一个评级项目 j,则该指标功能为 1;否则为零。
相对于用户和项目简档的参数,成本被最小化。通常,这种优化会导致过度拟合,因此用户的规范和项目配置文件在成本函数中被用作规范,如下所示:

这里λ1T3λ2T7】是正则化常数。通常,使用一种流行的梯度下降技术交替最小二乘 ( ALS )进行优化,该技术通过保持项目参数固定来交替更新用户简档参数,反之亦然。**
surprise包很好的实现了 SVD++。在下一节中,我们将在100K movie lens数据集上用 SVD++训练一个模型,并查看性能指标。
电影镜头 100k 数据集的 SVD++训练模型
使用以下命令可通过conda下载surprise包:
conda install -c conda-forge scikit-surprise
SVD++对应的算法在surprise中命名为SVDpp。我们可以按如下方式加载所有必需的包:
import numpy as np
from surprise import SVDpp # SVD++ algorithm
from surprise import Dataset
from surprise import accuracy
from surprise.model_selection import cross_validate
from surprise.model_selection import train_test_split
可以使用surprise中的Dataset.load_builtin实用程序下载100K Movie lens数据集并使其可供代码使用。我们按照80与20的比例将数据分为训练集和保持测试集。数据处理代码行如下:
# Load the movie lens 10k data and split the data into train test files(80:20)
data = Dataset.load_builtin('ml-100k')
trainset, testset = train_test_split(data, test_size=.2)
接下来,我们将对数据进行5折叠交叉验证,并查看交叉验证结果。我们为随机梯度下降选择了0.008的学习速率。同样为了防止过度拟合,我们为 L1 和 L2 正则化选择了0.1正则化常数。这些代码行的详细信息如下:
#Perform 5 fold cross validation with all data
algo = SVDpp(n_factors=40, n_epochs=40, lr_all=0.008, reg_all=0.1)
# Run 5-fold cross-validation and show results summary
cross_validate(algo,data, measures=['RMSE', 'MAE'], cv=5, verbose=True)
交叉验证的结果如下:
Evaluating RMSE, MAE of algorithm SVDpp on 5 split(s). Fold 1 Fold 2 Fold 3 Fold 4 Fold 5 Mean Std RMSE (testset) 0.9196 0.9051 0.9037 0.9066 0.9151 0.9100 0.0062 MAE (testset) 0.7273 0.7169 0.7115 0.7143 0.7228 0.7186 0.0058 Fit time 374.57 374.58 369.74 385.44 382.36 377.34 5.72 Test time 2.53 2.63 2.74 2.79 2.84 2.71 0.11
从前面的结果可以看出,模型的5 fold cv RMSE为0.91。Movie Lens 100K数据集的结果令人印象深刻。
现在我们只在训练数据集trainset上训练模型,然后在测试集上评估模型。相关代码行如下:
model = SVDpp(n_factors=40, n_epochs=10, lr_all=0.008, reg_all=0.1)
model.fit(trainset)
一旦模型已经被训练,我们就在保持测试数据集测试集上评估模型。相关代码行如下:
#validate the model on the testset
pred = model.test(testset)
print("SVD++ results on the Test Set")
accuracy.rmse(pred, verbose=True)
验证的输出如下:
SVD++ results on the test set
RMSE: 0.9320
从前面的结果中我们可以看到,SVD++模型在 RMSE 为0.93的测试数据集上做得非常好。结果与我们在此之前训练的基于深度学习的模型潜在因素模型(RMSE 的0.95)相当。
在用于推荐的受限玻尔兹曼机器部分、中,我们将研究用于构建推荐系统的受限玻尔兹曼机器。该方法可以扩展到大规模数据集,因此在协同过滤中得到了广泛的应用。协同过滤领域的大部分数据集是稀疏的,导致非凸优化问题很难解决。与其他因子分解方法(如奇异值分解)相比,径向基函数在数据集上不太容易受到这种稀疏性问题的影响。
推荐使用受限玻尔兹曼机器
受限玻尔兹曼机器是一类属于无监督学习技术的神经网络。受限玻尔兹曼机器 ( RBMs ),因为它们广为人知,试图通过将输入数据投影到隐藏层来学习数据的隐藏结构。
隐藏层激活被期望编码输入信号并重新创建它。受限玻尔兹曼机器通常处理二进制数据:

Figure 6.6: Restricted Boltzmann machines for binary data
为了刷新我们的记忆,前面的图(图 6.6 )是一个有 m 输入或可见单位的 RBM。这是投射到一个隐藏层与 n 单位。给定可见层输入
,隐藏单元相互独立,因此可以如下采样,其中
表示 sigmoid 函数:

类似地,给定隐藏层激活
,可见层单元是独立的,可以如下采样:

RBM 的参数是可见层单元 i 和隐藏层单元 j 之间的广义权重连接 w ij ∈ W m x n ,可见单元 i 处的偏置 c i ∈ b ,以及偏置 c j ∈ c
RBM 的这些参数是通过最大化可见输入数据的可能性来学习的。如果我们用
表示组合参数集,并且我们有一组 T 训练输入数据点,那么在 RBM,我们尝试最大化似然函数:

我们通常不使用乘积形式,而是最大化可能性的对数,或者最小化对数可能性的负值,以使函数在数学上更加方便。如果我们将对数似然的负值表示为成本函数 C ,那么:

成本函数通常通过梯度下降最小化。成本函数相对于参数的梯度由预期项组成,表示如下:



术语
表示隐藏单元和可见单元的联合概率分布上任何给定量的期望。此外,
表示给定可见单位 v 的采样隐藏层输出。在梯度下降的每次迭代中计算联合概率分布的期望值在计算上是困难的。我们求助于一种叫做对比发散的智能方法来计算期望值,这将在下一节讨论。
对比分歧
计算联合概率分布期望值的方法之一是通过吉布斯抽样从联合概率分布中产生大量样本,然后将样本的平均值作为期望值。在吉布斯抽样中,联合概率分布中的每个变量都可以抽样,条件是其余变量。由于可见单元是独立的,给定隐藏单元,反之亦然,您可以将隐藏单元采样为
,然后给定隐藏单元的可见单元激活为
。然后,我们可以将样本
作为从联合概率分布中采样的样本。这样,我们可以生成大量的样本,比如说 M ,取它们的平均值来计算期望的期望值。然而,在梯度下降的每一步中进行如此广泛的采样将使训练过程慢得不可接受,因此,我们不是在梯度下降的每一步中计算许多样本的平均值,而是从联合概率分布中仅生成一个样本,该样本应该代表整个联合概率分布上的期望:

Figure 6.7: Contrastive divergence illustration
从上图(图 6.7 )的图示可以看出,我们从看到的可见输入 v (t) 开始,基于条件概率分布 P(h/v = v (t) ) 对隐藏层激活
进行采样。再次,使用条件概率分布P(v/h = h′),我们对v′进行采样。基于条件概率分布P(h/v = v′)的隐藏单元下一次采样给我们
,然后使用
采样可见单元激活给我们
。样本
取 v 和 h 整个联合概率分布的代表样本,即
。同样用于计算任何包含 v 和 h 的表达式的期望值。这种取样过程被称为对比发散。
从可见输入开始,然后从条件分布 P(v/h) 和 P(v/h) 中依次采样,构成 Gibbs 采样的一个步骤,并从联合分布中给出一个样本 (v/h) 。我们可以选择从条件概率分布中选取连续几次采样迭代后的样本,而不是在吉布斯采样的每一步都选取样本 (v/h) 。如果在 Gibbs 抽样的 k 步之后,选择了代表元素,则对比散度称为 CD-k 。图 6.7 所示的对比差异可称为 CD-2、,因为我们是在两步吉布斯取样后选择样品的。
使用 RBMs 的协同过滤
限制性玻尔兹曼机器可用于在进行推荐时进行协同过滤。我们将使用这些资源管理系统向用户推荐电影。他们使用不同用户为不同电影提供的分级进行训练。用户不会观看或评价所有的电影,所以这个训练好的模型可以用来向用户推荐看不见的电影。
我们应该有的第一个问题是如何在 RBM 中处理等级,因为等级本质上是有序的,而 RBM 处理的是二进制数据。等级可以被视为二进制数据,代表等级的单位数量等于每个等级的唯一值的数量。例如:在评级系统中,级别从 1 到 5 不等,有五个二进制单位,其中一个对应的级别设置为 1,其余的为零。对 RBM 可见的单位是为用户提供的不同电影的等级。如所讨论的,每个等级将以二进制表示,并且对于每个可见单元,将存在来自所有二进制可见单元的权重连接,对应于电影分级。由于每个用户会对不同的电影集进行分级,因此每个用户的输入将会不同。然而,从电影分级单位到隐藏单位的权重连接对于所有用户来说都是通用的。
下图(图 6.8a**图 6.8b )所示为用户 A 和用户 B 的 RBM 视图。用户T10】A 和用户 B 对不同的一组电影进行了评分。然而,正如我们所看到的,对于每个用户来说,每个电影中隐藏单元的权重连接是相同的。关于用户 A 的 RBM 评级如下:

Figure 6.8a: RBM for collaborative filtering User A view
关于用户 B 的 RBM 评级如下:

Figure 6.8b: RBM for collaborative filtering User B view
还有一点需要注意的是,如果有 M 部电影,并且如果每部电影都可能有 k 个等级,那么 RBM 的可见单位数量就是 M * k 。同样,如果二进制隐藏单元的数量为 n ,那么【W】中的权重连接数等于 M * k * n 。给定可见层输入,每个隐藏单元 h j 可以独立于其他隐藏单元进行采样,如下所示:

这里, m = M * k.
与传统的 RBM 不同,在给定隐藏层激活的情况下,该网络中可见层的二进制单位不能被独立采样。关于电影等级的每一个 k 二进制单位通过 k 路软最大值激活功能联系在一起。如果给定隐藏单元的特定电影的可见单元的输入是
,则电影的等级lI 的一般输入计算如下:

这里, (i - 1)k + l 是电影 i 的可视单元的索引,用于排名 l 。类似地,任何特定电影的可视单元都可以根据软最大值函数给出的概率进行采样,如下所示:

在定义隐藏和可见单元的输出时,还有一点很重要,那就是需要概率抽样,而不是默认输出是最大概率的输出。如果给定可见单位的隐藏单位激活概率为 P,,则统一生成范围【0,1】内的随机数 r ,如果 (P > r) ,则隐藏单位激活设置为真。该方案将确保在很长一段时间内激活设置为真的概率 P 。类似地,电影的可见单元是根据给定隐藏单元的概率从跨国发行中采样的。因此,如果对于一部特定的电影,给定隐藏单元激活的情况下,不同评级的概率范围从 1 到 5,分别是 (p 1 、p 2 、p 3 、p 4 、p 5 ) ,那么可以从多项式分布中对评级值进行采样,其概率质量函数如下:

这里:


我们现在具备了创建用于协同过滤的受限玻尔兹曼机器所需的所有技术知识。
使用 RBM 实现协同过滤
在接下来的几个部分中,我们将使用受限的玻尔兹曼机器来实现一个协作过滤系统,其技术原理在前面的部分中有所阐述。我们将使用的数据集是 MovieLens 100K 数据集,其中包含用户为不同电影提供的一到五个等级。数据集可以从https://grouplens.org/datasets/movielens/100k/下载。
这个协同过滤系统的 TensorFlow 实现将在接下来的几节中介绍。
处理输入
每行输入的评分文件记录包含字段userId、movieId、rating和timestamp。我们处理每个记录以创建一个训练文件,其形式为一个具有与userId、movieId和rating相关的三个维度的numpy数组。从 1 到 5 的等级是一个热编码的,因此沿着等级维度的长度是 5。我们用 80%的输入记录创建训练数据,而剩下的 20%保留用于测试目的。用户评分的电影数量为1682。训练文件包含943用户,因此训练数据的维度为(943,1682,5)。训练文件中的每个用户都是 RBM 的训练记录,将包含一些用户已评分的电影和一些用户未评分的电影。一些电影分级也被删除,以包括在测试文件中。RBM 将根据可用的分级进行训练,在隐藏单元中捕获输入数据的隐藏结构,然后尝试根据捕获的隐藏结构为每个用户重建所有电影的输入分级。我们还创建了几个字典来存储实际电影标识和它们在训练/测试数据集中的索引的交叉引用。以下是创建培训和测试文件的详细代码:
"""
@author: santanu
"""
import numpy as np
import pandas as pd
import argparse
'''
Ratings file preprocessing script to create training and hold out test datasets
'''
def process_file(infile_path):
infile = pd.read_csv(infile_path,sep='\t',header=None)
infile.columns = ['userId','movieId','rating','timestamp']
users = list(np.unique(infile.userId.values))
movies = list(np.unique(infile.movieId.values))
test_data = []
ratings_matrix = np.zeros([len(users),len(movies),5])
count = 0
total_count = len(infile)
for i in range(len(infile)):
rec = infile[i:i+1]
user_index = int(rec['userId']-1)
movie_index = int(rec['movieId']-1)
rating_index = int(rec['rating']-1)
if np.random.uniform(0,1) < 0.2 :
test_data.append([user_index,movie_index,int(rec['rating'])])
else:
ratings_matrix[user_index,movie_index,rating_index] = 1
count +=1
if (count % 100000 == 0) & (count>= 100000):
print('Processed ' + str(count) + ' records out of ' + str(total_count))
np.save(path + 'train_data',ratings_matrix)
np.save(path + 'test_data',np.array(test_data))
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--path',help='input data path')
parser.add_argument('--infile',help='input file name')
args = parser.parse_args()
path = args.path
infile = args.infile
process_file(path + infile)
训练文件是维度为 m x n x k 的numpy数组对象,其中 m 为用户总数, n 为电影总数, k 为离散评分值个数(一到五)。为了构建测试集,我们从训练数据集中随机选择 20%的 m x n 评分条目。因此,测试集评级样本的所有 k 评级值在训练数据集中标记为零。在测试集中,我们没有将数据扩展为三维 numpy 数组格式,因此它可以用于训练。相反,我们只是将userid、movieid和指定的评级保存在三列中。请注意,存储在列车和测试文件中的userid和movieid不是原始评级数据文件u.data中的实际标识。它们被1抵消,以适应从0而不是从1开始的 Python 和numpy索引
以下命令可用于调用数据预处理脚本:
python preprocess_ratings.py --path '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/' --infile 'u.data'
为协同过滤建立 RBM 网络
以下函数_network为协同过滤创建所需的 RBM 结构。首先,我们定义输入的权重、偏差和占位符。然后定义sample_hidden和sample_visible函数,根据概率分别对隐藏激活和可见激活进行采样。隐藏单元从伯努利分布中采样,概率由 sigmoid 函数提供,而与每部电影相关的可见单元从多项式分布中采样,概率由 softmax 函数提供。不需要创建软最大概率,因为tf.multinomial函数可以直接从逻辑中采样,而不是实际概率。
我们通过定义基于吉布斯抽样的对比发散逻辑来跟进此事。gibbs_step函数实现 Gibbs 抽样的一个步骤,然后利用这个步骤实现顺序的对比发散 k 。
现在我们已经有了所有必要的函数,我们创建 TensorFlow ops 来对给定可见输入的隐藏状态self.h进行采样,并对给定采样隐藏状态的可见单位self.x进行采样。我们还使用对比散度从 v 和 h 的联合概率分布中抽取(self.x_s,self.h_s)作为代表性样本,即P(v,h/model),用于计算梯度中的不同期望项。
_network函数的最后一步是根据梯度更新 RBM 模型的权重和偏差。正如我们前面看到的,梯度是基于给定可见层输入的隐藏层激活self.h,以及通过对比发散得到的联合概率分布P(v,h/model)的代表性样本,即(self.x_s,self.h_s)。
TensorFlow 操作self.x_,指的是给定隐藏层激活self.h的可见层激活,在推断过程中,该操作将有助于得出尚未被每个用户评级的电影的评级:
def __network(self):
self.x = tf.placeholder(tf.float32, [None,self.num_movies,self.num_ranks], name="x")
self.xr = tf.reshape(self.x, [-1,self.num_movies*self.num_ranks], name="xr")
self.W = tf.Variable(tf.random_normal([self.num_movies*self.num_ranks,self.num_hidden], 0.01), name="W")
self.b_h = tf.Variable(tf.zeros([1,self.num_hidden], tf.float32, name="b_h"))
self.b_v = tf.Variable(tf.zeros([1,self.num_movies*self.num_ranks],tf.float32, name="b_v"))
self.k = 2
## Converts the probability into discrete binary states i.e. 0 and 1
def sample_hidden(probs):
return tf.floor(probs + tf.random_uniform(tf.shape(probs), 0, 1))
def sample_visible(logits):
logits = tf.reshape(logits,[-1,self.num_ranks])
sampled_logits = tf.multinomial(logits,1)
sampled_logits = tf.one_hot(sampled_logits,depth = 5)
logits = tf.reshape(logits,[-1,self.num_movies*self.num_ranks])
print(logits)
return logits
## Gibbs sampling step
def gibbs_step(x_k):
# x_k = tf.reshape(x_k,[-1,self.num_movies*self.num_ranks])
h_k = sample_hidden(tf.sigmoid(tf.matmul(x_k,self.W) + self.b_h))
x_k = sample_visible(tf.add(tf.matmul(h_k,tf.transpose(self.W)),self.b_v))
return x_k
## Run multiple gives Sampling step starting from an initital point
def gibbs_sample(k,x_k):
for i in range(k):
x_k = gibbs_step(x_k)
# Returns the gibbs sample after k iterations
return x_k
# Constrastive Divergence algorithm
# 1\. Through Gibbs sampling locate a new visible state x_sample based on the current visible state x
# 2\. Based on the new x sample a new h as h_sample
self.x_s = gibbs_sample(self.k,self.xr)
self.h_s = sample_hidden(tf.sigmoid(tf.matmul(self.x_s,self.W) + self.b_h))
# Sample hidden states based given visible states
self.h = sample_hidden(tf.sigmoid(tf.matmul(self.xr,self.W) + self.b_h))
# Sample visible states based given hidden states
self.x_ = sample_visible(tf.matmul(self.h,tf.transpose(self.W)) + self.b_v)
# The weight updated based on gradient descent
#self.size_batch = tf.cast(tf.shape(x)[0], tf.float32)
self.W_add = tf.multiply(self.learning_rate/self.batch_size,tf.subtract(tf.matmul(tf.transpose(self.xr),self.h),tf.matmul(tf.transpose(self.x_s),self.h_s)))
self.bv_add = tf.multiply(self.learning_rate/self.batch_size, tf.reduce_sum(tf.subtract(self.xr,self.x_s), 0, True))
self.bh_add = tf.multiply(self.learning_rate/self.batch_size, tf.reduce_sum(tf.subtract(self.h,self.h_s), 0, True))
self.updt = [self.W.assign_add(self.W_add), self.b_v.assign_add(self.bv_add), self.b_h.assign_add(self.bh_add)]
预处理步骤的数据可以在训练和推理过程中使用如下所示的read_data函数读取:
def read_data(self):
if self.mode == 'train':
self.train_data = np.load(self.train_file)
self.num_ranks = self.train_data.shape[2]
self.num_movies = self.train_data.shape[1]
self.users = self.train_data.shape[0]
else:
self.train_df = pd.read_csv(self.train_file)
self.test_data = np.load(self.test_file)
self.test_df = pd.DataFrame(self.test_data,columns=['userid','movieid','rating'])
if self.user_info_file != None:
self.user_info_df = pd.read_csv(self.user_info_file,sep='|',header=None)
self.user_info_df.columns=['userid','age','gender','occupation','zipcode']
if self.movie_info_file != None:
self.movie_info_df = pd.read_csv(self.movie_info_file,sep='|',encoding='latin-1',header=None)
self.movie_info_df = self.movie_info_df[[0,1]]
self.movie_info_df.columns = ['movieid','movie Title']
此外,在推断过程中,除了测试文件之外,我们还读取了所有电影和分级的预测文件 CSV(在前面代码的推断部分中为self.train_file),而不管它们是否已经被评级。一旦训练好模型,就进行预测。由于我们已经有了训练后预测的评分,我们在推断期间需要做的就是将评分预测信息与测试文件的实际评分信息相结合(更多细节请参见后面的train和inference部分)。此外,我们从用户和电影元数据文件中读取信息供以后使用。
训练 RBM
这里说明的_train功能可以用来训练 RBM。在这个函数中,我们首先调用_network函数来构建 RBM 网络结构,然后在激活的 TensorFlow 会话中为指定数量的时期训练模型。使用张量流的saver功能以指定的时间间隔保存模型:
def _train(self):
self.__network()
# TensorFlow graph execution
with tf.Session() as sess:
self.saver = tf.train.Saver()
#saver = tf.train.Saver(write_version=tf.train.SaverDef.V2)
# Initialize the variables of the Model
init = tf.global_variables_initializer()
sess.run(init)
total_batches = self.train_data.shape[0]//self.batch_size
batch_gen = self.next_batch()
# Start the training
for epoch in range(self.epochs):
if epoch < 150:
self.k = 2
if (epoch > 150) & (epoch < 250):
self.k = 3
if (epoch > 250) & (epoch < 350):
self.k = 5
if (epoch > 350) & (epoch < 500):
self.k = 9
# Loop over all batches
for i in range(total_batches):
self.X_train = next(batch_gen)
# Run the weight update
#batch_xs = (batch_xs > 0)*1
_ = sess.run([self.updt],feed_dict={self.x:self.X_train})
# Display the running step
if epoch % self.display_step == 0:
print("Epoch:", '%04d' % (epoch+1))
print(self.outdir)
self.saver.save(sess,os.path.join(self.outdir,'model'),
global_step=epoch)
# Do the prediction for all users all items irrespective of whether they
have been rated
self.logits_pred = tf.reshape(self.x_,
[self.users,self.num_movies,self.num_ranks])
self.probs = tf.nn.softmax(self.logits_pred,axis=2)
out = sess.run(self.probs,feed_dict={self.x:self.train_data})
recs = []
for i in range(self.users):
for j in range(self.num_movies):
rec = [i,j,np.argmax(out[i,j,:]) +1]
recs.append(rec)
recs = np.array(recs)
df_pred = pd.DataFrame(recs,columns=
['userid','movieid','predicted_rating'])
df_pred.to_csv(self.outdir + 'pred_all_recs.csv',index=False)
print("RBM training Completed !")
在前面的函数中需要强调的一件重要事情是使用自定义的next_batch函数创建随机批次。该函数在下面的代码片段中定义,用于定义迭代器batch_gen,该迭代器可以由next方法调用来检索下一个小批量:
def next_batch(self):
while True:
ix = np.random.choice(np.arange(self.data.shape[0]),self.batch_size)
train_X = self.data[ix,:,:]
yield train_X
需要注意的一点是,在培训结束时,我们会预测来自所有用户的所有电影的评分,无论它们是否被评分。具有最大概率的评级,将从五个可能的评级中给出(即从一到五)作为最终评级。因为在 Python 中索引是从零开始的,所以我们在使用argmax获得具有最高概率的评级位置后,添加一个来获得实际评级。因此,在培训结束时,我们有一个pred_all_recs.csv文件,其中包含所有培训和测试记录的预测评分。请注意,测试记录嵌入在培训记录中,1 到 5 的所有评分指标都设置为零。
然而,一旦我们从用户观看过的电影的隐藏表示中充分训练了模型,它就学会从用户没有看过的电影中生成评级。
可以通过调用以下命令来训练模型:
python rbm.py main_process --mode train --train_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/train_data.npy' --outdir '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/' --num_hidden 5 --epochs 1000
仅用5隐藏图层训练1000时代的模型需要大约52秒,从日志中我们可以看到:
RBM training Completed !
52.012 s: process RBM
Note that the Restricted Boltzmann Machine Network has been trained on an Ubuntu machine with a GeForce Zotac 1070 GPU and 64 GB of RAM. Training time may vary based on the system used to train the network.
使用经过训练的 RBM 进行推理
鉴于我们已经在训练中生成了包含所有预测的文件pred_all_recs.csv,RBM 的推断非常简单。我们需要做的就是根据提供的测试文件从pred_all_recs.csv中提取测试记录。此外,我们通过将1添加到它们当前的值中来求助于原始的userid和movieid。返回原 ID 的目的是能够从u.user和u.item文件中添加用户和电影信息。
推理块如下:
def inference(self):
self.df_result = self.test_df.merge(self.train_df,on=['userid','movieid'])
# in order to get the original ids we just need to add 1
self.df_result['userid'] = self.df_result['userid'] + 1
self.df_result['movieid'] = self.df_result['movieid'] + 1
if self.user_info_file != None:
self.df_result.merge(self.user_info_df,on=['userid'])
if self.movie_info_file != None:
self.df_result.merge(self.movie_info_df,on=['movieid'])
self.df_result.to_csv(self.outdir + 'test_results.csv',index=False)
print(f'output written to {self.outdir}test_results.csv')
test_rmse = (np.mean((self.df_result['rating'].values -
self.df_result['predicted_rating'].values)**2))**0.5
print(f'test RMSE : {test_rmse}')
推论可以如下调用:
python rbm.py main_process --mode test --train_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/pred_all_recs.csv' --test_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/test_data.npy' --outdir '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/' --user_info_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/u.user' --movie_info_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/u.item'
通过仅使用 RBM 的5隐藏单元,我们实现了约1.19的 RMSE 测试,这是值得称赞的,因为我们选择了这样一个简单的网络。推理的输出日志在以下代码块中提供,以供参考:
output written to /home/santanu/ML_DS_Catalog-/Collaborating Filtering/test_results.csv
test RMSE : 1.1999306704742303
458.058 ms: process RBM
我们从test_results.csv看userid 1的推断结果如下(见图 6.9 ):

Figure 6.9: Holdout data validation results on userid 1
从前面截图中的预测(图 6.9 )我们可以看出,RBM 在预测userid 1的电影坚守集方面做得很好。
建议您将最终评分预测作为对每个电影评分预测的多项式概率分布的评分预期,并查看与我们将最终评分作为多项式分布的最高概率的方法相比的情况。协同过滤的 RBM 纸可以在https://www.cs.toronto.edu/~rsalakhu/papers/rbmcf.pdT2 找到。与受限玻尔兹曼机相关的代码可在https://github . com/PacktPublishing/Intelligent-Projects-use-Python/blob/master/chapter 06/RBM . py找到。
摘要
看完这一章,你现在应该能够使用受限的玻尔兹曼机器构建一个智能推荐系统,并根据你的领域和需求以有趣的方式扩展它。关于本章所述项目的详细实施,请参考https://github . com/PacktPublishing/Intelligent-Projects-use-Python/blob/master/chapter 06上该项目的 GiHub 链接。
在下一章中,我们将讨论如何创建一个移动应用程序来对电影评论进行情感分析。我期待你的参与。
七、电影评论情感分析手机应用
在这个现代时代,将数据发送到云中基于人工智能的应用程序进行推理是司空见惯的事情。例如,用户可以将在手机上拍摄的图像发送到亚马逊 Rekognition API,该服务可以标记图像中存在的各种对象、人物、文本、场景等。使用托管在云中的基于人工智能的应用程序的服务的优势在于它的易用性。移动应用只需要向基于人工智能的服务发出一个 HTTPS 请求,并附上图像,几秒钟内,服务就会提供推理结果。其中一些机器学习即服务提供商如下:
- 亚马逊索赔案
- 亚马逊波利
- 亚马逊 Lex
- 微软 Azure 认知服务
- IBM 沃森
- 谷歌云视觉
下图,图 7.1,说明了这种应用程序在云上托管时的体系结构,以及它如何与移动设备交互:

Figure 7.1: Mobile app communicating with an AI model hosted on the cloud
正如您在上图中看到的那样,移动应用程序向托管在云上的模型发送图像定位和分类请求以及图像,模型在对提供的图像运行推理后发回结果。在云上使用这种服务的优势如下:
- 没有必要收集数据来训练这种模型
- 将人工智能模型作为一项服务来托管没有任何痛苦
- 没有必要担心模型的重新训练
所有这些都将由服务提供商负责。然而,在云上使用这种人工智能应用程序也有几个缺点,包括以下几点:
- 用户不能在本地移动设备上运行推理。所有的推断都需要通过向托管人工智能应用程序的服务器发送网络请求来完成。在没有网络连接的情况下,移动应用程序将无法工作。此外,通过网络从模型中获取预测可能会有一些延迟。
- 如果它不是一个免费托管的云应用程序,用户通常会为他们运行的推理数量付费。
- 托管在云上的模型非常通用,用户无法控制用自己的数据训练这些模型。如果数据是唯一的,这种基于通用数据训练的应用程序可能不会提供很好的结果。
部署在云上的人工智能应用程序的上述缺点可以通过在移动设备本身上运行推理来克服,而不是通过互联网将数据发送给人工智能应用程序。
该模型可以在任何具有适当的中央处理器和图形处理器的系统上进行训练,使用针对移动应用程序设计的问题的训练数据。然后可以将训练好的模型转换成优化的文件格式,只需要运行推理所需的权重和操作。然后,优化后的模型可以与移动应用程序集成,整个项目可以作为应用程序加载到移动设备上。训练好的模型的优化文件应该尽可能的小,因为模型会和手机应用程序代码一起存储在手机上。在本章中,我们将使用 TensorFlow mobile 开发一个安卓手机应用程序。
技术要求
您将需要具备 Python 3、TensorFlow 和 Java 的基本知识
本章代码文件可在 GitHub:
https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 07上找到
查看以下视频,查看正在运行的代码:
http://bit.ly/2S1sddw
使用 TensorFlow 手机构建安卓手机应用
在这个项目中,我们将使用 TensorFlow 的移动功能来优化作为协议缓冲对象的训练模型。然后,我们将该模型与一个安卓应用程序集成,其逻辑将用 Java 编写。我们需要执行以下步骤:
-
在 TensorFlow 中建立一个模型,并用相关数据进行训练。
-
一旦模型在验证数据集上表现令人满意,就将 TensorFlow 模型转换为优化的 protobuf 对象(例如,
optimized_model.pb)。 -
下载安卓工作室及其先决条件。用 Java 开发核心应用逻辑,用 XML 开发接口页面。
-
将 TensorFlow 训练的模型 protobuf 对象及其关联的依赖项集成到项目的资产文件夹中。
-
构建项目并运行它。
这个安卓应用的实现如下图所示(图 7.2 ):

Figure 7.2: Mobile app deployment architectural diagram
安卓应用中的电影评论评级
我们将构建一个安卓应用程序,该应用程序将电影评论作为输入,并根据对电影评论的情感分析,提供从0到5的评级作为输出。LSTM 版本的递归神经网络将首先被训练来对电影的情感进行二元分类。训练数据将包括基于文本的电影评论,以及二进制标签0或1。1的标签代表有积极情绪的评论,而0表示电影有消极情绪。从模型中,我们将预测情绪为正的概率,然后将概率放大五倍,将其转换为合理的评级。该模型将使用 TensorFlow 构建,然后将训练好的模型转换为优化的冻结 protobuf 对象,以与安卓应用程序逻辑集成。冻结对象的大小将比原始训练模型小得多,并且仅用于推断目的。
我们将在http://ai.stanford.edu/~amaas/data/sentiment/使用可用的数据集,在下面的文章中使用,标题为学习用于情感分析的词向量:
@InProceedings{maas-EtAl:2011:ACL-HLT2011,
author = {Maas, Andrew L. and Daly, Raymond E. and Pham, Peter T. and Huang, Dan and Ng, Andrew Y. and Potts, Christopher},
title = {Learning Word Vectors for Sentiment Analysis},
booktitle = {Proceedings of the 49th Annual Meeting of the Association for Computational Linguistics: Human Language Technologies},
month = {June},
year = {2011},
address = {Portland, Oregon, USA},
publisher = {Association for Computational Linguistics},
pages = {142--150},
url = {http://www.aclweb.org/anthology/P11-1015}
}
预处理电影评论文本
电影评论文本需要被预处理并转换成数字标记,对应于语料库中的不同单词。通过获取第一个50000频繁单词,Keras 标记器将用于将单词转换为数字索引或标记。我们已经限制了电影评论的字数上限。如果电影评论的单词标记少于1000单词标记,评论的开头用零填充。预处理之后,数据被分成训练集、验证集和测试集。Keras Tokenizer对象被保存以供推理时使用。
对电影评论进行预处理的详细代码如下:
# -*- coding: utf-8 -*-
"""
Created on Sun Jun 17 22:36:00 2018
@author: santanu
"""
import numpy as np
import pandas as pd
import os
import re
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
import pickle
import fire
from elapsedtimer import ElapsedTimer
# Function to clean the text and convert it into lower case
def text_clean(text):
letters = re.sub("[^a-zA-z0-9\s]", " ",text)
words = letters.lower().split()
text = " ".join(words)
return text
def process_train(path):
review_dest = []
reviews = []
train_review_files_pos = os.listdir(path + 'train/pos/')
review_dest.append(path + 'train/pos/')
train_review_files_neg = os.listdir(path + 'train/neg/')
review_dest.append(path + 'train/neg/')
test_review_files_pos = os.listdir(path + 'test/pos/')
review_dest.append(path + 'test/pos/')
test_review_files_neg = os.listdir(path + 'test/neg/')
review_dest.append(path + 'test/neg/')
sentiment_label = [1]*len(train_review_files_pos) + \
[0]*len(train_review_files_neg) + \
[1]*len(test_review_files_pos) + \
[0]*len(test_review_files_neg)
review_train_test = ['train']*len(train_review_files_pos) + \
['train']*len(train_review_files_neg) + \
['test']*len(test_review_files_pos) + \
['test']*len(test_review_files_neg)
reviews_count = 0
for dest in review_dest:
files = os.listdir(dest)
for f in files:
fl = open(dest + f,'r')
review = fl.readlines()
review_clean = text_clean(review[0])
reviews.append(review_clean)
reviews_count +=1
df = pd.DataFrame()
df['Train_test_ind'] = review_train_test
df['review'] = reviews
df['sentiment_label'] = sentiment_label
df.to_csv(path + 'processed_file.csv',index=False)
print ('records_processed',reviews_count)
return df
def process_main(path):
df = process_train(path)
# We will tokenize the text for the most common 50000 words.
max_fatures = 50000
tokenizer = Tokenizer(num_words=max_fatures, split=' ')
tokenizer.fit_on_texts(df['review'].values)
X = tokenizer.texts_to_sequences(df['review'].values)
X_ = []
for x in X:
x = x[:1000]
X_.append(x)
X_ = pad_sequences(X_)
y = df['sentiment_label'].values
index = list(range(X_.shape[0]))
np.random.shuffle(index)
train_record_count = int(len(index)*0.7)
validation_record_count = int(len(index)*0.15)
train_indices = index[:train_record_count]
validation_indices = index[train_record_count:train_record_count +
validation_record_count]
test_indices = index[train_record_count + validation_record_count:]
X_train,y_train = X_[train_indices],y[train_indices]
X_val,y_val = X_[validation_indices],y[validation_indices]
X_test,y_test = X_[test_indices],y[test_indices]
np.save(path + 'X_train',X_train)
np.save(path + 'y_train',y_train)
np.save(path + 'X_val',X_val)
np.save(path + 'y_val',y_val)
np.save(path + 'X_test',X_test)
np.save(path + 'y_test',y_test)
# saving the tokenizer oject for inference
with open(path + 'tokenizer.pickle', 'wb') as handle:
pickle.dump(tokenizer, handle, protocol=pickle.HIGHEST_PROTOCOL)
if __name__ == '__main__':
with ElapsedTimer('Process'):
fire.Fire(process_main)
代码preprocess.py可以如下调用:
python preprocess.py --path /home/santanu/Downloads/Mobile_App/aclImdb/
其输出日志如下所示:
Using TensorFlow backend.
records_processed 50000
24.949 s: Process
构建模型
我们将建立一个简单的 LSTM 版本的递归神经网络,在输入层后面有一个嵌入层。嵌入层词向量用维数为 100 的预训练手套向量初始化,层定义为trainable,这样词向量嵌入可以基于训练数据进行自我更新。隐藏状态和单元状态的维度也保持为100。该模型使用二元交叉熵损失进行训练。为了避免过拟合,在损失函数中加入了脊线正则化。亚当优化器 用于训练模型。
下面的代码片段显示了用于在 TensorFlow 中构建模型的函数:
def _build_model(self):
with tf.variable_scope('inputs'):
self.X = tf.placeholder(shape=[None, self.sentence_length],dtype=tf.int32,name="X")
print (self.X)
self.y = tf.placeholder(shape=[None,1], dtype=tf.float32,name="y")
self.emd_placeholder = tf.placeholder(tf.float32,shape=[self.n_words,self.embedding_dim])
with tf.variable_scope('embedding'):
# create embedding variable
self.emb_W =tf.get_variable('word_embeddings',[self.n_words, self.embedding_dim],initializer=tf.random_uniform_initializer(-1, 1, 0),trainable=True,dtype=tf.float32)
self.assign_ops = tf.assign(self.emb_W,self.emd_placeholder)
# do embedding lookup
self.embedding_input = tf.nn.embedding_lookup(self.emb_W,self.X,"embedding_input")
print( self.embedding_input )
self.embedding_input = tf.unstack(self.embedding_input,self.sentence_length,1)
#rint( self.embedding_input)
# define the LSTM cell
with tf.variable_scope('LSTM_cell'):
self.cell = tf.nn.rnn_cell.BasicLSTMCell(self.hidden_states)
# define the LSTM operation
with tf.variable_scope('ops'):
self.output, self.state = tf.nn.static_rnn(self.cell,self.embedding_input,dtype=tf.float32)
with tf.variable_scope('classifier'):
self.w = tf.get_variable(name="W", shape=[self.hidden_states,1],dtype=tf.float32)
self.b = tf.get_variable(name="b", shape=[1], dtype=tf.float32)
self.l2_loss = tf.nn.l2_loss(self.w,name="l2_loss")
self.scores = tf.nn.xw_plus_b(self.output[-1],self.w,self.b,name="logits")
self.prediction_probability = tf.nn.sigmoid(self.scores,name='positive_sentiment_probability')
print (self.prediction_probability)
self.predictions = tf.round(self.prediction_probability,name='final_prediction')
self.losses = tf.nn.sigmoid_cross_entropy_with_logits(logits=self.scores,labels=self.y)
self.loss = tf.reduce_mean(self.losses) + self.lambda1*self.l2_loss
tf.summary.scalar('loss', self.loss)
self.optimizer = tf.train.AdamOptimizer(self.learning_rate).minimize(self.losses)
self.correct_predictions = tf.equal(self.predictions,tf.round(self.y))
print (self.correct_predictions)
self.accuracy = tf.reduce_mean(tf.cast(self.correct_predictions, "float"), name="accuracy")
tf.summary.scalar('accuracy', self.accuracy)
训练模型
在本节中,我们将说明训练模型的 TensorFlow 代码。模型被训练为适度的10 epochs,以避免过度拟合。优化器使用的学习速率是0.001,而训练批次大小和验证批次大小分别设置在250和50。需要注意的是,我们正在使用tf.train.write_graph功能将模型图定义保存在model.pbtxt文件中。此外,一旦模型被训练,我们将使用tf.train.Saver 功能将模型权重保存在检查点文件model_ckpt中。将使用model.pbtxt和model_ckpt文件以 protobuf 格式创建 TensorFlow 模型的优化版本,该版本可与安卓应用程序集成:
def _train(self):
self.num_batches = int(self.X_train.shape[0]//self.batch_size)
self._build_model()
self.saver = tf.train.Saver()
with tf.Session() as sess:
init = tf.global_variables_initializer()
sess.run(init)
sess.run(self.assign_ops,feed_dict={self.emd_placeholder:self.embedding_matrix})
tf.train.write_graph(sess.graph_def, self.path, 'model.pbtxt')
print (self.batch_size,self.batch_size_val)
for epoch in range(self.epochs):
gen_batch = self.batch_gen(self.X_train,self.y_train,self.batch_size)
gen_batch_val = self.batch_gen(self.X_val,self.y_val,self.batch_size_val)
for batch in range(self.num_batches):
X_batch,y_batch = next(gen_batch)
X_batch_val,y_batch_val = next(gen_batch_val)
sess.run(self.optimizer,feed_dict={self.X:X_batch,self.y:y_batch})
c,a = sess.run([self.loss,self.accuracy],feed_dict={self.X:X_batch,self.y:y_batch})
print(" Epoch=",epoch," Batch=",batch," Training Loss: ","{:.9f}".format(c), " Training Accuracy=", "{:.9f}".format(a))
c1,a1 = sess.run([self.loss,self.accuracy],feed_dict={self.X:X_batch_val,self.y:y_batch_val})
print(" Epoch=",epoch," Validation Loss: ","{:.9f}".format(c1), " Validation Accuracy=", "{:.9f}".format(a1))
results = sess.run(self.prediction_probability,feed_dict={self.X:X_batch_val})
print(results)
if epoch % self.checkpoint_step == 0:
self.saver.save(sess, os.path.join(self.path,'model'), global_step=epoch)
self.saver.save(sess,self.path + 'model_ckpt')
results = sess.run(self.prediction_probability,feed_dict={self.X:X_batch_val})
print(results)
批量生成器
在train功能中,我们将使用批次生成器,根据通过的批次大小,生成随机批次。生成器函数可以定义如下。请注意,这些功能使用yield代替return。通过调用带有所需参数的函数,将创建一个批处理迭代器对象。通过对迭代器对象应用next方法可以检索批次。我们将在每个纪元开始时调用生成器函数,这样批处理在每个纪元中将是随机的。
下面的代码片段说明了用于生成批处理迭代器对象的函数:
def batch_gen(self,X,y,batch_size):
index = list(range(X.shape[0]))
np.random.shuffle(index)
batches = int(X.shape[0]//batch_size)
for b in range(batches):
X_train,y_train = X[index[b*batch_size: (b+1)*batch_size],:],
y[index[b*batch_size: (b+1)*batch_size]]
yield X_train,y_train
模型培训活动的详细代码在脚本**movie_review_model_train.py**中。同样的训练可以如下调用:
python movie_review_model_train.py process_main --path /home/santanu/Downloads/Mobile_App/ --epochs 10
培训的输出如下:
Using TensorFlow backend.
(35000, 1000) (35000, 1)
(7500, 1000) (7500, 1)
(7500, 1000) (7500, 1)
no of positive class in train: 17497
no of positive class in test: 3735
Tensor("inputs/X:0", shape=(?, 1000), dtype=int32)
Tensor("embedding/embedding_lookup:0", shape=(?, 1000, 100), dtype=float32)
Tensor("positive_sentiment_probability:0", shape=(?, 1), dtype=float32)
.....
25.043 min: Model train
将模型冻结为 protobuf 格式
保存的训练好的模型,以model.pbtxt和model_ckpt文件的形式,不能被安卓应用直接使用。我们需要将其转换为优化的 protobuf 格式(一个.pb扩展名文件),可以与安卓应用程序集成。优化后的 protobuf 格式的文件大小将远小于model.pbtxt和model_ckpt文件的组合大小。
下面的代码(freeze_code.py)将从model.pbtxt和model_ckpt文件创建优化的原型 buf 模型:
# -*- coding: utf-8 -*-
import sys
import tensorflow as tf
from tensorflow.python.tools import freeze_graph
from tensorflow.python.tools import optimize_for_inference_lib
import fire
from elapsedtimer import ElapsedTimer
#path = '/home/santanu/Downloads/Mobile_App/'
#MODEL_NAME = 'model'
def model_freeze(path,MODEL_NAME='model'):
# Freeze the graph
input_graph_path = path + MODEL_NAME+'.pbtxt'
checkpoint_path = path + 'model_ckpt'
input_saver_def_path = ""
input_binary = False
output_node_names = 'positive_sentiment_probability'
restore_op_name = "save/restore_all"
filename_tensor_name = "save/Const:0"
output_frozen_graph_name = path + 'frozen_'+MODEL_NAME+'.pb'
output_optimized_graph_name = path + 'optimized_'+MODEL_NAME+'.pb'
clear_devices = True
freeze_graph.freeze_graph(input_graph_path, input_saver_def_path,
input_binary, checkpoint_path, output_node_names,
restore_op_name, filename_tensor_name,
output_frozen_graph_name, clear_devices, "")
input_graph_def = tf.GraphDef()
with tf.gfile.Open(output_frozen_graph_name, "rb") as f:
data = f.read()
input_graph_def.ParseFromString(data)
output_graph_def = optimize_for_inference_lib.optimize_for_inference(
input_graph_def,
["inputs/X" ],#an array of the input node(s)
["positive_sentiment_probability"],
tf.int32.as_datatype_enum # an array of output nodes
)
# Save the optimized graph
f = tf.gfile.FastGFile(output_optimized_graph_name, "w")
f.write(output_graph_def.SerializeToString())
if __name__ == '__main__':
with ElapsedTimer('Model Freeze'):
fire.Fire(model_freeze)
正如您在前面的代码中看到的,我们首先声明输入张量和输出张量,方法是在声明模型时引用它们的定义名称。使用输入和输出张量,以及model.pbtxt和model_ckpt 文件,利用tensorflow.python.tools的freeze_graph 功能冻结模型。下一步,冷冻模型进一步优化,使用tensorflow.python.tools中的optimize_for_inference_lib功能创建原型 buf 模型,命名为optimized_model.pb。这个优化后的 protobuf 模型optimized_model.pb将与安卓应用程序集成,用于推理。
可以调用freeze_code.py模型创建 protobuf 格式文件,如下所示:
python freeze_code.py --path /home/santanu/Downloads/Mobile_App/ --MODEL_NAME model
执行上述命令的输出如下:
39.623 s: Model Freeze
创建用于推理的单词到标记词典
在预处理过程中,我们训练了一个 Keras 标记器,用数字单词索引替换单词,这样处理后的电影评论就可以输入到 LSTM 模型中进行训练。我们还保留了词频最高的第一个50000单词,并将复习顺序设置为最大长度1000。虽然训练好的 Keras tokenizer 是为了推断而保存的,但它不能被安卓应用直接使用。我们可以恢复 Keras 标记器,并将第一个50000单词及其对应的单词索引保存在文本文件中。这个文本文件可以在安卓应用中使用,以便建立一个单词到索引词典将评论文本的单词转换成它们的单词索引。需要注意的是,通过参考tokenizer.word_index.可以从加载的 Keras 标记器对象中检索到单词到索引的映射。执行此活动tokenizer_2_txt.py的详细代码如下:
import keras
import pickle
import fire
from elapsedtimer import ElapsedTimer
#path = '/home/santanu/Downloads/Mobile_App/aclImdb/tokenizer.pickle'
#path_out = '/home/santanu/Downloads/Mobile_App/word_ind.txt'
def tokenize(path,path_out):
with open(path, 'rb') as handle:
tokenizer = pickle.load(handle)
dict_ = tokenizer.word_index
keys = list(dict_.keys())[:50000]
values = list(dict_.values())[:50000]
total_words = len(keys)
f = open(path_out,'w')
for i in range(total_words):
line = str(keys[i]) + ',' + str(values[i]) + '\n'
f.write(line)
f.close()
if __name__ == '__main__':
with ElapsedTimer('Tokeize'):
fire.Fire(tokenize)
tokenizer_2_txt.py可以如下运行:
python tokenizer_2_txt.py --path '/home/santanu/Downloads/Mobile_App/aclImdb/tokenizer.pickle' --path_out '/home/santanu/Downloads/Mobile_App/word_ind.txt'
前面命令的输出日志如下:
Using TensorFlow backend.
165.235 ms: Tokenize
App 界面页面设计
使用安卓工作室可以设计一个简单的手机应用界面,相关代码将生成一个 XML 文件。正如你在下面的截图中看到的那样(图 7.3 ),这个应用程序由一个简单的电影评论文本框组成,用户可以在这里输入他们的电影评论,一旦完成,就按下 SUBMIT 按钮。一旦按下 SUBMIT 按钮,评论将被传递给核心 app 逻辑,核心 app 逻辑将处理电影评论文本,并将其传递给 TensorFlow 优化模型进行推理。
作为推断的一部分,将计算情感评分,该评分将显示在移动应用程序上,并显示为星级:

Figure 7.3: Mobile app user interface page format
生成前面提到的移动应用视图所需的 XML 文件如下所示:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
tools:layout_editor_absoluteY="81dp">
<TextView
android:id="@+id/desc"
android:layout_width="100dp"
android:layout_height="26dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="44dp"
android:layout_marginRight="8dp"
android:layout_marginStart="44dp"
android:layout_marginTop="36dp"
android:text="Movie Review"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.254"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="HardcodedText" />
<EditText
android:id="@+id/Review"
android:layout_width="319dp"
android:layout_height="191dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/desc" />
<RatingBar
android:id="@+id/ratingBar"
android:layout_width="240dp"
android:layout_height="49dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="52dp"
android:layout_marginRight="8dp"
android:layout_marginStart="52dp"
android:layout_marginTop="28dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.238"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/score"
tools:ignore="MissingConstraints" />
<TextView
android:id="@+id/score"
android:layout_width="125dp"
android:layout_height="39dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="96dp"
android:layout_marginRight="8dp"
android:layout_marginStart="96dp"
android:layout_marginTop="32dp"
android:ems="10"
android:inputType="numberDecimal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.135"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/submit" />
<Button
android:id="@+id/submit"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="136dp"
android:layout_marginRight="8dp"
android:layout_marginStart="136dp"
android:layout_marginTop="24dp"
android:text="SUBMIT"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/Review" />
</android.support.constraint.ConstraintLayout>
需要注意的一点是,用户和移动应用核心逻辑交互的变量是在 XML 文件中声明的,在android:id选项中。例如,用户提供的电影评论将由Review变量处理,如这里所示的 XML 文件中所定义的:
android:id="@+id/Review"
安卓应用的核心逻辑
安卓应用的核心逻辑是处理用户请求,以及传递的数据,然后将结果发送回用户。作为这个移动应用程序的一部分,核心逻辑将接受用户提供的电影评论,处理原始数据,并将其转换为训练有素的 LSTM 模型可以运行推理的格式。Java 中的OnClickListener功能用于监控用户是否提交了处理请求。所提供的电影评论中的每个词都需要被改变为它们的索引,然后输入可以被直接馈送到优化的训练 LSTM 模型用于推断。除了优化的 protobuf 模型之外,还为此目的存储了单词及其对应索引的字典。使用TensorFlowInferenceInterface 方法对训练好的模型进行推理。优化后的 protobuf 模型和单词字典及其对应的索引存储在assets文件夹中。综上所述,应用核心逻辑执行的任务如下:
-
将索引词典中的单词加载到
WordToIndHashMap中。在训练模型之前,单词到索引字典是在文本预处理期间从标记器中得到的。 -
使用
OnClickListener方法监控用户是否提交了电影评论进行推断。 -
如果已经提交了电影评论,则该评论将从绑定到 XML 的
Review变量中读取。通过删除标点符号等方式清理评论,然后将其拆分为单词。使用HashMap功能WordToInd,每个单词都被转换成相应的索引。这些指数构成了我们的张量流模型的InputVec输入,用于推断。输入向量长度为1000;所以,如果评论少于1000个字,向量在开头用零填充。 -
在下一步中,使用
TensorFlowInferenceInterface功能创建一个mInferenceInterface对象,将优化后的 protobuf 模型(扩展名为.pb)从assets文件夹加载到内存中。需要定义张量流模型的输入节点和输出节点,这些节点将被引用来进行推理,就像原始模型一样。对于我们的模型,它们被定义为INPUT_NODE和OUTPUT_NODE,它们分别包含 TensorFlow 输入占位符的名称和输出情感概率 ops。mInferenceInterface对象的feed方法用于将InputVec值分配给模型的INPUT_NODE,而mInferenceInterface的run方法执行OUTPUT_NODE。最后,使用mInferenceInterface的fetch方法将推理结果填充到浮动变量value_中。****
***** 情绪得分(情绪为正的概率)通过乘以 5 转换为评级。然后通过ratingBar变量将此反馈到安卓 app 用户界面。****
****Java 中移动应用的核心逻辑如下:
package com.example.santanu.abc;
import android.content.res.AssetManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.RatingBar;
import android.widget.TextView;
import android.widget.Button;
import android.widget.EditText;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import org.tensorflow.contrib.android.TensorFlowInferenceInterface;
public class MainActivity extends AppCompatActivity {
private TensorFlowInferenceInterface mInferenceInterface;
private static final String MODEL_FILE = "file:///android_asset/optimized_model.pb";
private static final String INPUT_NODE = "inputs/X";
private static final String OUTPUT_NODE = "positive_sentiment_probability";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Create references to the widget variables
final TextView desc = (TextView) findViewById(R.id.desc);
final Button submit = (Button) findViewById(R.id.submit);
final EditText Review = (EditText) findViewById(R.id.Review);
final TextView score = (TextView) findViewById(R.id.score);
final RatingBar ratingBar = (RatingBar) findViewById(R.id.ratingBar);
//String filePath = "/home/santanu/Downloads/Mobile_App/word2ind.txt";
final Map<String,Integer> WordToInd = new HashMap<String,Integer>();
//String line;
//reader = new BufferedReader(new InputStreamReader(getAssets().open("word2ind.txt")));
BufferedReader reader = null;
try {
reader = new BufferedReader(
new InputStreamReader(getAssets().open("word_ind.txt")));
// do reading, usually loop until end of file reading
String line;
while ((line = reader.readLine()) != null)
{
String[] parts = line.split("\n")[0].split(",",2);
if (parts.length >= 2)
{
String key = parts[0];
//System.out.println(key);
int value = Integer.parseInt(parts[1]);
//System.out.println(value);
WordToInd.put(key,value);
} else
{
//System.out.println("ignoring line: " + line);
}
}
} catch (IOException e) {
//log the exception
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
//log the exception
}
}
}
//line = reader.readLine();
// Create Button Submit Listener
submit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Read Values
String reviewInput = Review.getText().toString().trim();
System.out.println(reviewInput);
String[] WordVec = reviewInput.replaceAll("[^a-zA-z0-9 ]", "").toLowerCase().split("\\s+");
System.out.println(WordVec.length);
int[] InputVec = new int[1000];
// Initialize the input
for (int i = 0; i < 1000; i++) {
InputVec[i] = 0;
}
// Convert the words by their indices
int i = 1000 - 1 ;
for (int k = WordVec.length -1 ; k > -1 ; k--) {
try {
InputVec[i] = WordToInd.get(WordVec[k]);
System.out.println(WordVec[k]);
System.out.println(InputVec[i]);
}
catch (Exception e) {
InputVec[i] = 0;
}
i = i-1;
}
if (mInferenceInterface == null) {
AssetManager assetManager = getAssets();
mInferenceInterface = new TensorFlowInferenceInterface(assetManager,MODEL_FILE);
}
float[] value_ = new float[1];
mInferenceInterface.feed(INPUT_NODE,InputVec,1,1000);
mInferenceInterface.run(new String[] {OUTPUT_NODE}, false);
System.out.println(Float.toString(value_[0]));
mInferenceInterface.fetch(OUTPUT_NODE, value_);
double scoreIn;
scoreIn = value_[0]*5;
double ratingIn = scoreIn;
String stringDouble = Double.toString(scoreIn);
score.setText(stringDouble);
ratingBar.setRating((float) ratingIn);
}
});
}
}
需要注意的一点是,我们可能需要编辑应用的build.gradle文件,以便将包添加到依赖项中:
org.tensorflow:tensorflow-android:1.7.0
测试移动应用程序
我们将用两部电影的评论来测试手机应用:阿凡达和星际。阿凡达影评取自https://www.rogerebert.com/reviews/avatar-2009,内容如下:
“看《阿凡达》的时候,我感觉和 1977 年看《星球大战》的时候差不多。那是我带着不确定的期望走进的另一部电影。詹姆斯·卡梅隆的电影就像他的《泰坦尼克号》一样,一直是令人怀疑的热门话题。他又一次通过简单地发表一部非凡的电影让质疑者哑口无言。在好莱坞,至少还有一个人知道如何明智地花费 2.5 亿美元,或者是 3 亿美元。
《阿凡达》不仅仅是一部轰动一时的娱乐片,尽管它确实如此。这是技术上的突破。它有一个明确的绿色和反战信息。发动邪教是命中注定的。它包含这样的视觉细节,它将奖励重复观看。它发明了一种新的语言,纳威语,就像《指环王》一样,尽管幸运的是,我怀疑这种语言是否能被人类使用,即使是十几岁的人。它创造了新的电影明星。这是一个活动,是那些你觉得必须看才能跟上对话的电影之一。”
评论者给电影的评分是 4/5,而手机应用给的评分是 4.8/5 左右,如下图截图所示(图 7.4 ):

Figure 7.4. Mobile app review rating of the movie Avatar
同样,我们将评估该应用为电影《星际穿越》提供的评级,评论来自https://www.rottentomatoes.com/m/interstellar_2014/。审核如下:
《星际穿越》代表了更多的惊心动魄、发人深省、视觉上光彩照人的电影制作,观众已经开始期待编剧兼导演克里斯托弗·诺兰的作品,尽管它的知识面有些超出了观众的理解范围
电影在烂番茄上的平均评分是 7/10,当缩放到 5 时,评分是 3.5/5,而手机应用预测的评分是 3.37,如下图截图所示(图 7.5 ):

Figure 7.5. Mobile app review rating of the movie Interstellar
正如您在前面两张插图中看到的,移动电影评论评级应用程序在为电影评论提供合理评级方面做得很好。
摘要
读完这一章,读者应该对如何利用 TensorFlow 的移动功能在安卓应用程序中部署深度学习模型有了一个大致的了解。本章中涉及的技术细节和实现细节应该对读者有益,帮助他们构建智能安卓移动应用程序,并以有趣的方式扩展它们。本项目的详细代码位于https://github . com/PacktPublishing/Python-人工智能-项目/第 07 章 。
在下一章中,我们将为客户服务构建一个对话式 AI 聊天机器人。我们期待您的参与。****
八、面向客户服务的对话式人工智能聊天程序
会话聊天机器人最近产生了大量的炒作,因为它们在增强客户体验方面的作用。现代企业已经开始在几个不同的过程中使用聊天机器人的功能。由于会话式人工智能被广泛接受,通过互联网填写表格或发送信息的繁琐任务变得更加简单。对话聊天机器人的一个期望品质是它应该能够在当前环境中响应用户请求。对话聊天机器人系统中的玩家分别是用户和机器人。使用对话聊天机器人有很多优点,如下表所示:
- 个性化协助:为所有客户创造个性化体验可能是一项繁琐的任务,但不这样做可能会让企业遭受损失。对话聊天机器人是为每一位顾客提供个性化体验的便捷选择。
- 全天候支持 : 全天候使用客服代表费用昂贵。在非办公时间使用 chatbots 进行客户服务,无需雇佣额外的客户代表。
- 回答的一致性:聊天机器人提供的回答可能是一致的,而不同的客户服务代表对相同问题的回答可能会有所不同。这样,如果客户对客户服务代表提供的答案不满意,就无需多次致电。
- 耐心:虽然客服代表在接待客户时可能会失去耐心,但这对于聊天机器人来说是不可能的。
- 查询记录 : 聊天机器人查询记录的效率比人类客服代表要高得多。
聊天机器人不是最近才出现的东西,它的起源可以追溯到 20 世纪 50 年代。就在第二次世界大战之后,艾伦·图灵开发了图灵测试来观察一个人是否能区分人和机器。多年后的 1966 年,约瑟夫·韦森鲍姆开发了一些名为伊莱扎的软件,该软件模仿了一位心理治疗师的语言。工具仍可位于http://psych.fullerton.edu/mbirnbaum/psych101/Eliza.htm。
聊天机器人可以执行各种各样的任务,下面的列表显示了其中一些任务,以强调它们的多功能性:
- 回答关于产品的问题
- 向客户提供建议
- 执行句子完成活动
- 对话聊天机器人
- 与客户谈判价格并参与投标
很多时候,企业很难弄清楚他们是否需要聊天机器人。企业是否需要聊天机器人可以通过图 8.1 中的流程图来确定:

Figure 8.1: Customer engagement model
作为本章的一部分,我们将涵盖以下主题:
- Chatbot 架构
- 聊天机器人的 LSTM 序列对序列模型
- 为推特支持的聊天机器人建立一个序列到序列的模型
技术要求
您将需要具备 Python 3、TensorFlow 和 Keras 的基本知识
本章代码文件可在 GitHub:
https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 08上找到
查看以下视频,查看正在运行的代码:
http://bit.ly/2G9AyoB
Chatbot 架构
chatbot 的核心组件是它的自然语言处理框架。聊天机器人通过一个通常被称为解析的过程,用自然语言处理呈现给它们的数据。解析后的用户输入随后被解释,并且根据用户想要的内容将适当的响应发送回用户,如从输入中解密的那样。chatbot 可能需要从知识库和历史交易数据存储中寻求帮助,以适当地处理用户的请求。
聊天机器人可以大致分为以下两类:
- 基于检索的模型:这些模型通常依赖于查找表或知识库从预定义的一组答案中选择一个答案。虽然这种方法看起来很幼稚,但生产中的大多数聊天机器人都是这种类型的。当然,在从查找表或知识库中选择最佳答案方面,可以有不同的复杂程度。
- 生成模型 : 生成模型动态生成响应,而不是采用基于查找的方法。它们大多是概率模型或基于机器学习的模型。直到最近,马尔可夫链大多被用作生成模型;然而,随着最近深度学习的成功,基于递归神经网络的方法得到了普及。一般来说,LSTM 版本的 RNNs 被用作聊天机器人的生成模型,因为它更擅长处理长序列。
基于检索的模型和生成模型都有各自的优缺点。由于基于检索的模型从一组固定的答案中回答,它们不能处理看不见的问题或没有适当的预定义响应的请求。生成模型更加复杂;他们可以理解用户输入中的实体,并生成类似人类的响应。然而,它们更难训练,并且通常需要更多的数据来训练。他们也容易犯语法错误,这是基于检索的模型所不能犯的。
使用 LSTM 的序列到序列模型
序列到序列模型架构非常适合捕获客户输入的上下文,然后基于此生成适当的响应。图 8.2 显示了一个序列到序列的模型框架,它可以像聊天机器人一样回答问题:

Figure 8.2: Sequence-to-sequence model using an LSTM
从上图(图 8.2 )我们可以看到,编码器 LSTM 将输入的单词序列编码成隐藏状态向量
,和细胞状态向量
。向量
和
是 LSTM 编码器最后一步的隐藏和单元状态。它们将基本上捕捉整个输入句子的上下文。
和
形式的编码信息随后被馈送到解码器 LSTM 作为其初始隐藏和单元状态。每一步中的解码器 LSTM 试图根据当前单词预测下一个单词。这意味着,解码器 LSTM 每一步的输入都是当前字。
为了预测第一个单词,LSTM 将被提供一个虚拟的开始关键字 < BOS > ,它代表句子的开始。同样的, < EOS > dummy 关键字代表句子的结尾,一旦预测到这一点,输出生成就应该停止。
在为每个目标单词训练序列到序列模型的过程中,我们先验地知道是输入到解码器 LSTM 的前一个单词。然而,在推断过程中,我们不会有这些目标词,因此我们必须将前一步作为输入。
*# 构建序列对序列模型
我们将用于构建聊天机器人的序列到序列模型的架构将对先前在图 8.2 中说明的基本序列到序列架构进行轻微修改。修改后的架构如下图所示(图 8.3 ):

Figure 8.3: Sequence-to-sequence model
不是将编码器最后一步的隐藏状态
和单元状态
馈送到解码器 LSTM 的初始隐藏和单元状态,而是在解码器的每个输入步骤馈送隐藏状态
。要在任意一步 t 预测目标词wtT8】,输入为前一个目标词, w t-1 ,任意一步, t-1 ,隐藏状态
。
推特上的客户支持
现在我们已经对如何使用递归神经网络构建聊天机器人有了一些想法,我们将使用 20 个大品牌对客户发布的推文的客户服务响应来构建聊天机器人。数据集twcs.zip可位于https://www . kaggle . com/thought vector/customer-support-on-Twitter。 每条推文由tweet_id标识,推文内容在text字段。客户发布的推文可以通过in_response_to_tweet_id字段识别。这应该包含客户推文的空值。对于客户服务推文,此in_response_to_tweet_id 字段应指向此推文指向的客户tweet_id。
为训练聊天机器人创建数据
要提取客户发布的所有入站推文,我们需要提取所有in_response_to_tweet_id 字段为null的推文。包含客服代表回复的出站文件可以通过推文过滤提取,其中in_response_to_tweet_id 字段不为空。一旦我们有了入站和出站文件,我们需要将它们合并在入站文件的tweet_id和出站文件的in_response_to_tweet_id 上。这将为我们提供客户在中的推文和客户服务代表在中的推文作为回应。数据创建功能可以编码如下:
def process_data(self,path):
data = pd.read_csv(path)
if self.mode == 'train':
data = pd.read_csv(path)
data['in_response_to_tweet_id'].fillna(-12345,inplace=True)
tweets_in = data[data['in_response_to_tweet_id'] == -12345]
tweets_in_out =
tweets_in.merge(data,left_on=['tweet_id'],right_on=
['in_response_to_tweet_id'])
return tweets_in_out[:self.num_train_records]
elif self.mode == 'inference':
return data
将文本标记为单词索引
这些推文需要被标记化并转换成数字,然后才能输入神经网络。计数向量器用于确定构成聊天机器人词汇空间的固定数量的频繁单词。我们还引入了三个新的标记,分别表示一个句子的开始(START)、一个句子的结束(PAD)以及任何未知的单词(UNK)。此处显示了标记推文的功能,以供参考:
def tokenize_text(self,in_text,out_text):
count_vectorizer = CountVectorizer(tokenizer=casual_tokenize, max_features=self.max_vocab_size - 3)
count_vectorizer.fit(in_text + out_text)
self.analyzer = count_vectorizer.build_analyzer()
self.vocabulary =
{key_: value_ + 3 for key_,value_ in count_vectorizer.vocabulary_.items()}
self.vocabulary['UNK'] = self.UNK
self.vocabulary['PAD'] = self.PAD
self.vocabulary['START'] = self.START
self.reverse_vocabulary =
{value_: key_ for key_, value_ in self.vocabulary.items()}
joblib.dump(self.vocabulary,self.outpath + 'vocabulary.pkl')
joblib.dump(self.reverse_vocabulary,self.outpath + 'reverse_vocabulary.pkl')
joblib.dump(count_vectorizer,self.outpath + 'count_vectorizer.pkl')
#pickle.dump(self.count_vectorizer,open(self.outpath +
'count_vectorizer.pkl',"wb"))
现在,需要将标记化的单词转换为单词索引,以便它们可以被馈送到递归神经网络,如以下代码所示:
def words_to_indices(self,sent):
word_indices =
[self.vocabulary.get(token,self.UNK) for token in self.analyzer(sent)] +
[self.PAD]*self.max_seq_len
word_indices = word_indices[:self.max_seq_len]
return word_indices
我们还希望将递归神经网络预测的单词索引转换成单词以形成句子。该功能可以编码如下:
def indices_to_words(self,indices):
return ' '.join(self.reverse_vocabulary[id] for id in indices if id != self.PAD).strip()
替换匿名屏幕名称
在我们对推文进行标记化之前,将推文中匿名化的屏幕名称替换为通用名称可能是值得的,这样响应会更好地概括。该功能可以编码如下:
def replace_anonymized_names(self,data):
def replace_name(match):
cname = match.group(2).lower()
if not cname.isnumeric():
return match.group(1) + match.group(2)
return '@__cname__'
re_pattern = re.compile('(\W@|^@)([a-zA-Z0-9_]+)')
if self.mode == 'train':
in_text = data['text_x'].apply(lambda txt:re_pattern.sub(replace_name,txt))
out_text = data['text_y'].apply(lambda
txt:re_pattern.sub(replace_name,txt))
return list(in_text.values),list(out_text.values)
else:
return map(lambda x:re_pattern.sub(replace_name,x),data)
定义模型
RNN 的 LSTM 版本被用来建立序列到序列模型。这是因为 LSTMs 在记忆长文本序列中的长期依赖关系方面要高效得多。LSTM 架构中的三个门使它能够有效地记住长期序列。一个基本的 RNN 无法记住长期的依赖关系,因为与其架构相关的渐变问题正在消失。
在这个模型中,我们使用了两个 LSTMs。第一个 LSTM 将把输入的推文编码成一个上下文向量。这个上下文向量只不过是编码器 LSTM 的最后一个隐藏状态
, n 是隐藏状态向量的维数。输入的推文
作为单词索引序列输入到编码器 LSTM,其中 k 是输入推文的序列长度。在输入 LSTM 之前,这些单词索引被映射到单词嵌入。单词嵌入被封装在嵌入矩阵中, [W ∈ R m x N ,其中 N 表示词汇中单词的数量。
第二个 LSTM 作为解码器工作。它试图将编码器 LSTM 创建的上下文向量
解码成有意义的响应。作为该方法的一部分,我们在每个时间步长将上下文向量与前一个单词一起馈送,以生成当前单词。在第一个时间步骤中,我们没有任何先前的单词来限制 LSTM,因此我们使用代理START单词来开始从解码器 LSTM 生成单词序列的过程。在推理过程中,我们如何在当前时间步长输入前一个单词不同于我们在训练过程中使用的方法。在训练中,既然我们知道了前面的单词先验的,那么在每一个时间步,我们相应的喂养它们没有任何问题。然而,在推断过程中,由于我们在当前时间步长没有实际的前一个单词,所以前一个时间步长的预测单词被馈送。在最终的大软最大值 N 之前,每个时间步长 t 的隐藏状态
通过几个完全连接的层传送。在这个 softmax 层中获得最大概率的单词是时间步长的预测单词。该字然后被馈送到下一步骤的输入端,即解码器 LSTM 的步骤 t + 1 。
Keras 中的TimeDistributed函数允许在解码器 LSTM 的每个时间步长有效实现预测,如以下代码所示:
def define_model(self):
# Embedding Layer
embedding = Embedding(
output_dim=self.embedding_dim,
input_dim=self.max_vocab_size,
input_length=self.max_seq_len,
name='embedding',
)
# Encoder input
encoder_input = Input(
shape=(self.max_seq_len,),
dtype='int32',
name='encoder_input',
)
embedded_input = embedding(encoder_input)
encoder_rnn = LSTM(
self.hidden_state_dim,
name='encoder',
dropout=self.dropout
)
# Context is repeated to the max sequence length so that the same context
# can be feed at each step of decoder
context = RepeatVector(self.max_seq_len)(encoder_rnn(embedded_input))
# Decoder
last_word_input = Input(
shape=(self.max_seq_len,),
dtype='int32',
name='last_word_input',
)
embedded_last_word = embedding(last_word_input)
# Combines the context produced by the encoder and the last word uttered as
inputs
# to the decoder.
decoder_input = concatenate([embedded_last_word, context],axis=2)
# return_sequences causes LSTM to produce one output per timestep instead of
one at the
# end of the intput, which is important for sequence producing models.
decoder_rnn = LSTM(
self.hidden_state_dim,
name='decoder',
return_sequences=True,
dropout=self.dropout
)
decoder_output = decoder_rnn(decoder_input)
# TimeDistributed allows the dense layer to be applied to each decoder output
per timestep
next_word_dense = TimeDistributed(
Dense(int(self.max_vocab_size/20),activation='relu'),
name='next_word_dense',
)(decoder_output)
next_word = TimeDistributed(
Dense(self.max_vocab_size,activation='softmax'),
name='next_word_softmax'
)(next_word_dense)
return Model(inputs=[encoder_input,last_word_input], outputs=[next_word])
用于训练模型的损失函数
该模型基于分类交叉熵损失进行训练,以预测解码器 LSTM 的每个时间步长中的目标词。任何步骤中的分类交叉熵损失都将超过词汇表中的所有单词,可以表示如下:

标签
代表目标单词的一个热编码版本。只有与实际单词对应的标签才会是一;剩下的将是零点。术语 Pi 表示实际目标单词是由 i 索引的单词的概率。为了获得总损失, C ,对于每个输入/输出推文对,我们需要总结解码器 LSTM 的所有时间步长上的损失。由于词汇表的大小可能会变得非常大,因此在每个时间步骤中为目标标签创建一个热编码向量
将会非常昂贵。sparse_categorical_crossentropy的丢失在这里变得非常有益,因为我们不需要将目标单词转换成一个热编码的向量,而是可以将目标单词的索引作为目标标签。
训练模型
模型可以用 Adam 优化器训练,因为它可靠地提供了稳定的收敛性。由于 rnn 容易出现爆炸式的梯度问题(尽管这对于 LSTMs 来说问题不大),如果梯度变得太大,最好对其进行裁剪。给定的模型可以用 Adam 优化器和sparse_categorical_crossentropy来定义和编译,如下面的代码块所示:
def create_model(self):
_model_ = self.define_model()
adam = Adam(lr=self.learning_rate,clipvalue=5.0)
_model_.compile(optimizer=adam,loss='sparse_categorical_crossentropy')
return _model_
现在我们已经了解了所有的基本功能,训练功能可以编码如下:
def train_model(self,model,X_train,X_test,y_train,y_test):
input_y_train = self.include_start_token(y_train)
print(input_y_train.shape)
input_y_test = self.include_start_token(y_test)
print(input_y_test.shape)
early = EarlyStopping(monitor='val_loss',patience=10,mode='auto')
checkpoint =
ModelCheckpoint(self.outpath + 's2s_model_' + str(self.version) +
'_.h5',monitor='val_loss',verbose=1,save_best_only=True,mode='auto')
lr_reduce =
ReduceLROnPlateau(monitor='val_loss',factor=0.5, patience=2, verbose=0,
mode='auto')
model.fit([X_train,input_y_train],y_train,
epochs=self.epochs,
batch_size=self.batch_size,
validation_data=[[X_test,input_y_test],y_test],
callbacks=[early,checkpoint,lr_reduce],
shuffle=True)
return model
在train_model函数的开始,我们创建input_y_train和input_y_test,它们分别是y_train和y_test的副本,并且从它们偏移一个时间步长,以便它们可以在解码器的每个时间步长充当前一个单词的输入。这些移位序列的第一个字是在解码器 LSTM 的第一时间步馈送的START关键字。include_start_token自定义实用功能如下:
def include_start_token(self,Y):
print(Y.shape)
Y = Y.reshape((Y.shape[0],Y.shape[1]))
Y = np.hstack((self.START*np.ones((Y.shape[0],1)),Y[:, :-1]))
return Y
回到训练功能train_model,我们看到如果损失在10时期没有减少,则使用EarlyStopping回拨功能启用提前停止。类似地,ReduceLROnPlateau回调会将现有的学习率减少一半(0.5),如果错误在两个时期内没有减少的话。只要误差在一个时期内减小,就通过ModelCheckpoint回调保存模型。
从模型生成输出响应
一旦模型被训练好,我们想用它来生成给定输入推文的响应。在以下步骤中也可以这样做:
- 用通用名称替换输入推文中的匿名屏幕名称。
- 将修改后的输入推文转换为单词索引。
- 将单词索引馈送到编码器 LSTM,将
START关键字馈送到解码器 LSTM,以生成第一个预测单词。从下一步开始,输入前一时间步的预测单词,而不是START关键字。 - 继续这样做,直到预测到句尾关键字。我们已经用
PAD表示了这一点。 - 查看逆词汇词典,从预测单词索引中获取单词。
下面的代码说明了respond_to_input功能,该功能可以在给定输入推文的情况下生成输出序列,以供参考:
def respond_to_input(self,model,input_sent):
input_y = self.include_start_token(self.PAD * np.ones((1,self.max_seq_len)))
ids = np.array(self.words_to_indices(input_sent)).reshape((1,self.max_seq_len))
for pos in range(self.max_seq_len -1):
pred = model.predict([ids, input_y]).argmax(axis=2)[0]
#pred = model.predict([ids, input_y])[0]
input_y[:,pos + 1] = pred[pos]
return self.indices_to_words(model.predict([ids,input_y]).argmax(axis=2)[0])
把它们放在一起
综上所述,main函数可以定义为有两个流程:一个用于训练,另一个用于推理。即使在训练函数中,我们也会对输入的推文序列生成一些响应,以检查我们训练模型的效果。以下代码显示了main功能供参考:
def main(self):
if self.mode == 'train':
X_train, X_test, y_train, y_test,test_sentences = self.data_creation()
print(X_train.shape,y_train.shape,X_test.shape,y_test.shape)
print('Data Creation completed')
model = self.create_model()
print("Model creation completed")
model = self.train_model(model,X_train,X_test,y_train,y_test)
test_responses = self.generate_response(model,test_sentences)
print(test_sentences)
print(test_responses)
pd.DataFrame(test_responses).to_csv(self.outpath +
'output_response.csv',index=False)
elif self.mode == 'inference':
model = load_model(self.load_model_from)
self.vocabulary = joblib.load(self.vocabulary_path)
self.reverse_vocabulary = joblib.load(self.reverse_vocabulary_path)
#nalyzer_file = open(self.analyzer_path,"rb")
count_vectorizer = joblib.load(self.count_vectorizer_path)
self.analyzer = count_vectorizer.build_analyzer()
data = self.process_data(self.data_path)
col = data.columns.tolist()[0]
test_sentences = list(data[col].values)
test_sentences = self.replace_anonymized_names(test_sentences)
responses = self.generate_response(model,test_sentences)
print(responses)
responses.to_csv(self.outpath + 'responses_' + str(self.version) +
'_.csv',index=False)
调用培训
可以通过运行带有多个参数的chatbot.py(参见本项目 GitHub 中的代码)模块来调用培训,如下命令所示:
python chatbot.py --max_vocab_size 50000 --max_seq_len 30 --embedding_dim 100 --hidden_state_dim 100 --epochs 80 --batch_size 128 --learning_rate 1e-4 --data_path /home/santanu/chatbot/data/twcs.csv --outpath /home/santanu/chatbot/ --dropout 0.3 --mode train --num_train_records 50000 --version v1
以下是一些重要的参数,以及它们的描述和用于调用 chatbot 序列到序列模型的训练的值:
| 参数 | 描述 | 用于训练的数值 |
| max_vocab_size | 要纳入词汇的单词数 | 50,000 |
| max_seq_len | 推特的最大长度将被限制为喂给 LSTM | 30 |
| hidden_state_dim | 隐藏状态
维度的 LSTM | 100 |
| embedding_dim | 单词嵌入的维度 | 100 |
| learning rate | 用于优化器的起始学习速率 | 0.0001 |
| dropout | 用于正规化目的的辍学 | 0.3 |
| Mode | 训练/推理 | 训练使用train推理使用inference |
对一些输入推文的推断结果
训练后的序列到序列模型响应一些输入的推文生成一些相关的推文。以下命令显示了示例推理命令。data_path参数包含测试输入推文的位置:
python chatbot.py --max_vocab_size 50000 --max_seq_len 30 --embedding_dim 100 --hidden_state_dim 100 --data_path /home/santanu/chatbot/data/test.csv --outpath /home/santanu/chatbot/ --dropout 0.3 --mode inference --version v1 --load_model_from /home/santanu/chatbot/s2s_model_v1_.h5 --vocabulary_path /home/santanu/chatbot/vocabulary.pkl --reverse_vocabulary_path /home/santanu/chatbot/reverse_vocabulary.pkl --count_vectorizer_path /home/santanu/chatbot/count_vectorizer.pkl
推断的结果如下表所示:
| 在发推 | 发微博 |
| @sprintcare是最差的客服。 | 嘿,你好!你能告诉我们你账户的电子邮件地址吗?我们去后台看看/dnhttps://t.co/ldfdzrinat。 |
| 你们都谎称自己和 T2 有很好的关系。5 条 LTE,还是不会加载什么东西。Smh。 | @__cname__对于给您带来的麻烦,我们深表歉意。请给我们发送一封包含您的电子邮件地址的 dm,以便我们连接。 |
| @__cname__每当我联系客户支持时,他们都会告诉我我的帐户启用了短码,但在这 4 年中,我从未尝试过https://t.co/0G98RtNxPK。 | @__cname__你好,听到这个消息我们很难过。请给我们发一封包含您的电子邮件地址和电话号码的 dm,这样我们就可以对此进行调查。 |
| 其实那是你发给我的一个断掉的链接和不正确的信息https://t.co/V4yfrHR8VI。 | @__cname__嗨,听到这个消息我们很难过。请告诉我们你的联系方式,我们很乐意帮忙。千开 |
| 哟@Ask_Spectrum,你的客户服务代表是超级好的——但是如果你们没有得到我的服务,我就开始走神了! | @__cname__对于给您带来的麻烦,我们深表歉意。请给我们发送一封包含您的电子邮件地址的 dm,以便我们连接。 |
| 我的照片几乎每天都在@Ask_Spectrum上。我为什么要每月支付 171 美元?https://t.co/U6ptkQa5Ik。 | @__cname__嗨,听到这个消息我们很难过。请告诉我们你的联系方式,我们很乐意帮忙。千开 |
| 请从@VerizonSupport来的人帮助我,我在你们的客户服务方面运气最差。 | @__cname__对于给您带来的麻烦,我们深表歉意。请告诉我们您的联系方式和电话号码,以便我们提供帮助。 |
| @VerizonSupport我的朋友没有互联网,我们需要一起玩电子游戏,请不要有互联网,我们的技能每时每刻都在下降。 | 嘿,你好!你能告诉我们你账户的电子邮件地址吗?我们去后台看看/dnhttps://t.co/ldfdzrinat |
| @__cname__试图支付 60 天的账单。没有服务,粗鲁的 CS,和几次转移。查一下我的设备#给我一个账号#! | @__cname__嗨,听到这个消息我们很难过。请告诉我们你的联系方式,我们很乐意帮忙。千开 |
| @__cname__是我遇到过最差的 ISP。 | 嘿,你好!你能告诉我们你账户的电子邮件地址吗?我们去后台看看/dnhttps://t.co/ldfdzrinat |
| @__cname__修复你的应用程序,它甚至不会打开。 | 你好!我们没有关于这个问题的任何信息。请寄给我们一份 dm,上面有您的姓名和电话号码以及完整的服务地址。 |
摘要
现在,我们到了这一章的结尾。在浏览了本章中阐述的与 chatbot 和序列到序列模型相关的各种概念后,读者现在应该能够构建自己的 chat bot 实现,并以有趣的方式对其进行扩展。正如我们所知,序列到序列模型不仅适用于聊天机器人,还适用于一系列自然语言处理领域,如机器翻译。本章代码可在 GitHub 位置https://GitHub . com/PacktPublishing/Python-人工智能-项目/树/主/第 08 章找到。
在下一章中,我们将使用强化学习来让赛车学会自己驾驶。我们期待您的参与。*
九、基于强化学习的自动驾驶汽车
强化学习,即代理人通过与环境互动来学习做出决策,在过去几年中真正起飞了。它是当今人工智能和机器学习领域最热门的话题之一,该领域的研究进展迅速。在强化学习 ( RL )中,代理将他们的行动和经验转化为学习,以便在未来做出更好的决策。
强化学习不属于有监督或无监督的机器学习范式,因为它本身就是一个领域。在监督学习中,我们尝试学习一种映射 F: X → Y ,该映射将输入 X 映射为输出 Y ,而在强化学习中,代理通过试错学习采取最佳行动。当一个代理人做得好的时候,会得到奖励,而当代理人做得不好的时候,会得到惩罚。代理试图吸收这些信息,并学会在类似的情况下不重复这些错误。代理所面临的这些条件被称为状态。图 9.1 显示了强化学习框架中环境中代理的交互:

Figure 9.1: Illustration of agent and environment interaction
技术要求
你将需要具备 Python 3、TensorFlow、Keras 和 OpenCV 的基本知识。
本章代码文件可在 GitHub:
https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 09上找到
查看以下视频,查看正在运行的代码:
http://bit.ly/2WxfwpF
马尔可夫决策过程
任何强化学习问题都可以看作是一个马尔可夫决策过程,我们在第一章、基于人工智能系统的基础中简要介绍过。为了您的利益,我们将再次详细讨论这一点。在马尔可夫决策过程中,我们有一个与环境交互的代理。在任何给定的情况下, t 试剂暴露于多种状态之一: (s (t) = s) ∈ S 。基于代理的动作 (a (t) = a) ∈ A 在状态 s (t) 代理被呈现一个新的状态(S(t+1)= S′**)∈S。这里, S 表示代理可能暴露于的所有状态的集合,而 A 表示代理可能参与的动作。
你现在可能想知道代理如何采取行动。它应该是随机的还是基于启发式的?嗯,这要看代理和环境有多少互动。在初始阶段,代理可能会采取随机行动,因为他们对环境一无所知。然而,一旦代理与环境进行了足够的交互,基于奖励和惩罚,代理就会知道在给定的状态下应该采取什么样的适当行动。类似于人们倾向于采取有利于长期回报的行动,RL 代理也采取他的行动,这使长期回报最大化。
数学上,代理试图为每个状态动作对 (s ∈ S,a ∈ A) 学习一个 Q 值 Q(s,a) 。对于给定的状态(t),RL 代理选择给出最大 Q 值的动作 a、。代理人采取的动作 a (t) 可以表示如下:

一旦代理人采取行动 a (t) 处于状态 s (t) ,一个新的状态 s (t+1) 被呈现给待处理的代理人。这个新状态 s (t+1) 一般不具有确定性,一般表示为当前状态 s (t) 和动作 a (t) 上的概率分布条件。这些概率被称为状态转移概率,可以表示如下:

每当代理在状态采取行动 a (t) 并到达新状态 s (t+1) 时,立即奖励给代理,可表示如下:

现在我们有了定义马尔可夫决策过程所需的一切。马尔可夫决策过程是由以下四个要素表征的系统:
- 一组状态 S
- 一套动作一套
- 一套奖励 R
- 状态转移概率P(s(t+1)= s′/s(t)= s,a(t)= a:

Figure 9.2: Illustration of a Markov decision process with three states
学习 Q 值函数
对于一个 RL 代理来说,学习 Q 值函数是很重要的。Q 值函数可以通过贝尔曼方程迭代学习。当代理开始与环境交互时,它以随机状态(0)和每个状态动作对的 Q 值的随机状态开始。代理的行为也会有些随机,因为它没有状态 Q 值来做出明智的决定。对于所采取的每一个动作,环境将根据哪个代理开始构建 Q 值表来返回奖励,并随着时间的推移而改进。
在任何暴露状态下 s (t) 在迭代 t 时,代理将采取一个动作 a (t) ,使其长期回报最大化。Q 表保存长期奖励值,因此选择的 a (t) 将基于以下启发:

Q 值表也是通过迭代 t 来索引的,因为代理目前只能查看 Q 表构建,随着代理与环境的交互越来越多,Q 表构建将会改进。
基于采取的动作 a (t) ,环境将向代理人呈现奖励 r (t) 和新状态 s (t+1) 。代理将更新 Q 表,使其总的长期预期回报最大化。长期奖励r(t)可以写成如下形式:

这里,
是折扣因子。我们可以看到,长期奖励结合了即时奖励 r (t) 和基于下一个状态 s (t+1) 呈现的累积未来奖励。
基于计算出的长期奖励,状态动作对 (s (t) 、a (t) ) 的现有 Q 值更新如下:


深度学习
深度 Q 学习利用深度学习网络学习 Q 值函数。如下图所示,图 9.3,是深度 Q 学习网络的架构:

Figure 9.3: Illustration of a deep Q network
该图学习将每对状态 (s,a) 和动作映射成输出 Q 值输出 Q(s,a),而在右侧的图中,对于每个状态,我们学习与每个动作 a 相关的 Q 值。如果每个状态都有 n 个可能的动作,网络的输出产生 n 个输出 Q(s,a 1 )、Q(s,a 2 ),。。。。。Q(s,a n ) 。
深度 Q 学习网络是用一个非常简单的想法训练出来的,这个想法叫做经验重播。让 RL 代理与环境交互,将体验以 (s,a,r,s’)的元组形式存储在重放缓冲区中。可以从这个重放缓冲器中取样小批量来训练网络。开始时,重放缓冲区以随机体验存储。
制定成本函数
使用这种体系结构更容易,在这种体系结构中,我们可以得到网络在给定状态下所有动作的 Q 值。同样的情况在图 9.3 的右侧也有说明。我们会让代理与环境交互,收集状态和奖励,在此基础上我们将学习 Q 函数。事实上,网络会通过将给定状态下所有动作的预测 Q 值
与目标 Q 值最小化来学习 Q 函数。每个训练记录都是一个元组 (s (t) 、a (t) 、r (t) 、s (t+1) ) 。
请记住,目标 Q 值是根据网络本身计算的。让我们考虑网络由 W ∈ R d 权重参数化的事实,并且我们学习从状态到给定状态的每个动作的 Q 值的映射。对于 n 组动作
,网络将预测与每个动作相关的 i Q 值。映射函数可以表示如下:

这个映射用于预测给定状态的 Q 值 s (t) ,这个预测
到达我们正在最小化的成本函数。这里需要考虑的唯一技术问题是,我们只需要取对应于在实例 t 处观察到的动作 a (t) 的预测 Q 值。
我们可以使用相同的映射,基于下一个状态 s (t+1) 来构建目标 Q 值。如前一节所示,Q 值的候选更新如下:

因此,目标 Q 值可以这样计算:


为了学习从状态到 Q 值的函数映射,我们相对于神经网络的权重最小化平方损失或其他相关损失:

双深度 Q 学习
深度 Q 学习的一个问题是,我们使用相同的网络权重 W 来估计目标和 Q 值。因此,我们预测的 Q 值和目标 Q 值之间有很大的相关性,因为它们都使用相同的变化权重。这使得预测的和目标的 Q 值在训练的每一步都移动,导致振荡。
为了稳定这一点,我们使用原始网络的副本来估计目标 Q 值,目标网络的权重在步骤中以特定的间隔从原始网络中复制。这种深度 Q 学习网络的变体称为双深度 Q 学习,一般会带来稳定的训练。双深度 Q 学习的工作机制如下图所示图 9.4A 和图 9.4B :

Figure 9.4A: Illustration of double deep Q learning

Figure 9.4B: Illustration of double deep Q learning
在上图中,我们可以看到两个网络:网络 A 学习预测给定状态下的实际 Q 值,网络 B 帮助计算目标 Q 值。网络 A 通过最小化目标和预测 Q 值的损失函数进行改善。由于 Q 值本质上通常是连续的,一些有效的损失函数是mean squared error、mean absolute error、Huber Loss、log-cosh loss等等。
网络 B 基本上是网络 A 的副本,因此它们共享相同的架构。来自网络 A 的权重通常以指定的时间间隔复制到网络 B 。这是为了确保不使用同一组网络权重来预测 Q 值,并且还制定目标 Q 值,因为这会导致不稳定的训练。给定单个训练元组 (s (t) = s,a (t) = a,r (t) = r,s(t+1)= s′),网络 A给定状态s(t)给出 Q 值的预测既然知道了实际动作 a (t) = a ,我们就选择 Q 值Q(t)(s(t)= s,a (t) = a) 。这将作为我们的预测 Q 值
。
计算目标现在有点困难,因为它涉及两个网络。我们知道在任何状态 s (t) 在步骤 t 的候选 Q 值是在时间 t 的即时奖励 r (t) 加上给定新状态 s (t+1) 的下一步最大 Q 值。候选 Q 值可以表示如下:

当
是一个恒定的折扣因子时就是这种情况。奖励 r 已经是训练元组的一部分。因此,我们唯一需要计算的目标是动作a′,该动作给出最大的 Q 值,并将对应的 Q 值带到该对应的动作a′。这个计算问题
分为两部分:
- 网络 A 确定给定状态s′给出最大 Q 值的动作A′T5。但是我们不会从网络 A 取动作A′对应的 Q 值和状态s′。
- 网络 B 用于提取状态s和动作a对应的 Q 值Q(t-k)(s′、a′)。
与基础DQN相比,这导致了更稳定的训练。
实现自动驾驶汽车
我们现在将着眼于实现一种自动驾驶赛车,它可以使用深度 Q 网络在赛道上学会自己驾驶。驾驶员和汽车将充当代理,而赛道及其周围环境则充当环境。我们将使用 OpenAI 健身房CarRacing-v0框架作为环境。状态和奖励将由环境呈现给代理人,而代理人将通过采取适当的行动来执行这些。这些状态是从车前的摄像头拍摄的图像。环境接受的动作是三维向量 a ∈ R 3 的形式,其中第一个分量用于左转,第二个分量用于向前移动,第三个分量用于向右移动。代理将与环境交互,并将交互转换为
形式的元组。这些交互元组将作为我们的训练数据。
该架构将类似于我们在图表右侧所展示的内容(图 9.4A 和图 9.4B )。
深度 Q 学习的离散化动作
离散化动作对于深度 Q 学习非常重要,因为三维连续动作空间可以有无限的 Q 值,并且不可能在深度 Q 网络的输出层中为每个动作都有单独的单元。动作空间的三个维度如下:
转向:∞[-1,1]
气:∏0,1】
突破:∏0,1】
我们将这个三维动作空间转换为我们感兴趣的四个动作,如下所示:
Brake : [0.0, 0.0, 0.0]
Sharp Left: [-0.6, 0.05, 0.0]
Sharp Right: [0.6, 0.05, 0.0]
Straight: [0.0, 0.3, 0.0]
实现双深度 Q 网络
双深 Q 网络的网络架构如下图所示。这些网络采用有线电视新闻网架构,将状态处理为图像,并为所有可能的动作输出 Q 值。详细代码(DQN.py)如下:
import keras
from keras import optimizers
from keras.layers import Convolution2D
from keras.layers import Dense, Flatten, Input, concatenate, Dropout
from keras.models import Model
from keras.utils import plot_model
from keras import backend as K
import numpy as np
'''
Double Deep Q Network Implementation
'''
learning_rate = 0.0001
BATCH_SIZE = 128
class DQN:
def __init__(self,num_states,num_actions,model_path):
self.num_states = num_states
print(num_states)
self.num_actions = num_actions
self.model = self.build_model() # Base Model
self.model_ = self.build_model()
# target Model (copy of Base Model)
self.model_chkpoint_1 = model_path +"CarRacing_DDQN_model_1.h5"
self.model_chkpoint_2 = model_path +"CarRacing_DDQN_model_2.h5"
save_best = keras.callbacks.ModelCheckpoint(self.model_chkpoint_1,
monitor='loss',
verbose=1,
save_best_only=True,
mode='min',
period=20)
save_per = keras.callbacks.ModelCheckpoint(self.model_chkpoint_2,
monitor='loss',
verbose=1,
save_best_only=False,
mode='min',
period=400)
self.callbacks_list = [save_best,save_per]
# Convolutional Neural Network that takes in the state and outputs the Q values for all the possible actions.
def build_model(self):
states_in = Input(shape=self.num_states,name='states_in')
x = Convolution2D(32,(8,8),strides=(4,4),activation='relu')(states_in)
x = Convolution2D(64,(4,4), strides=(2,2), activation='relu')(x)
x = Convolution2D(64,(3,3), strides=(1,1), activation='relu')(x)
x = Flatten(name='flattened')(x)
x = Dense(512,activation='relu')(x)
x = Dense(self.num_actions,activation="linear")(x)
model = Model(inputs=states_in, outputs=x)
self.opt = optimizers.Adam(lr=learning_rate, beta_1=0.9, beta_2=0.999, epsilon=None,decay=0.0, amsgrad=False)
model.compile(loss=keras.losses.mse,optimizer=self.opt)
plot_model(model,to_file='model_architecture.png',show_shapes=True)
return model
# Train function
def train(self,x,y,epochs=10,verbose=0):
self.model.fit(x,y,batch_size=(BATCH_SIZE), epochs=epochs, verbose=verbose, callbacks=self.callbacks_list)
#Predict function
def predict(self,state,target=False):
if target:
# Return the Q value for an action given a state from thr target Network
return self.model_.predict(state)
else:
# Return the Q value from the original Network
return self.model.predict(state)
# Predict for single state function
def predict_single_state(self,state,target=False):
x = state[np.newaxis,:,:,:]
return self.predict(x,target)
#Update the target Model with the Base Model weights
def target_model_update(self):
self.model_.set_weights(self.model.get_weights())
正如我们在前面的代码中看到的,我们有两个模型,其中一个是另一个的副本。基础模型和目标模型保存为CarRacing_DDQN_model_1.h5 和CarRacing_DDQN_model_2.h5。
通过调用target_model_update ,目标模型被更新为具有与基础模型相同的权重。
设计代理
代理将与环境交互,并且在给定的状态下,将尝试执行最佳操作。代理最初将执行随机动作,随着训练的进行,这些动作将更多地基于给定状态的 Q 值。ε参数的值决定了动作随机的概率。最初ε被设置为1以使动作随机。当代理已经收集了指定数量的训练样本时,在每个步骤中减少ε,使得动作随机的概率减少。这种基于ε值的方案被称为ε贪婪算法。我们定义两个代理类如下:
Agent:基于给定状态的 Q 值执行动作RandomAgent:执行随机动作
代理类有三个功能,具有以下功能:
act:代理根据状态决定要采取的动作observe:代理捕获状态和目标 Q 值replay:代理基于观察训练模型
代理(Agents.py)的详细代码如下所示:
import math
from Memory import Memory
from DQN import DQN
import numpy as np
import random
from helper_functions import sel_action,sel_action_index
# Agent and Random Agent implementations
max_reward = 10
grass_penalty = 0.4
action_repeat_num = 8
max_num_episodes = 1000
memory_size = 10000
max_num_steps = action_repeat_num * 100
gamma = 0.99
max_eps = 0.1
min_eps = 0.02
EXPLORATION_STOP = int(max_num_steps*10)
_lambda_ = - np.log(0.001) / EXPLORATION_STOP
UPDATE_TARGET_FREQUENCY = int(50)
batch_size = 128
class Agent:
steps = 0
epsilon = max_eps
memory = Memory(memory_size)
def __init__(self, num_states,num_actions,img_dim,model_path):
self.num_states = num_states
self.num_actions = num_actions
self.DQN = DQN(num_states,num_actions,model_path)
self.no_state = np.zeros(num_states)
self.x = np.zeros((batch_size,)+img_dim)
self.y = np.zeros([batch_size,num_actions])
self.errors = np.zeros(batch_size)
self.rand = False
self.agent_type = 'Learning'
self.maxEpsilone = max_eps
def act(self,s):
print(self.epsilon)
if random.random() < self.epsilon:
best_act = np.random.randint(self.num_actions)
self.rand=True
return sel_action(best_act), sel_action(best_act)
else:
act_soft = self.DQN.predict_single_state(s)
best_act = np.argmax(act_soft)
self.rand=False
return sel_action(best_act),act_soft
def compute_targets(self,batch):
# 0 -> Index for current state
# 1 -> Index for action
# 2 -> Index for reward
# 3 -> Index for next state
states = np.array([rec[1][0] for rec in batch])
states_ = np.array([(self.no_state if rec[1][3] is None else rec[1][3]) for rec in batch])
p = self.DQN.predict(states)
p_ = self.DQN.predict(states_,target=False)
p_t = self.DQN.predict(states_,target=True)
act_ctr = np.zeros(self.num_actions)
for i in range(len(batch)):
rec = batch[i][1]
s = rec[0]; a = rec[1]; r = rec[2]; s_ = rec[3]
a = sel_action_index(a)
t = p[i]
act_ctr[a] += 1
oldVal = t[a]
if s_ is None:
t[a] = r
else:
t[a] = r + gamma * p_t[i][ np.argmax(p_[i])] # DDQN
self.x[i] = s
self.y[i] = t
if self.steps % 20 == 0 and i == len(batch)-1:
print('t',t[a], 'r: %.4f' % r,'mean t',np.mean(t))
print ('act ctr: ', act_ctr)
self.errors[i] = abs(oldVal - t[a])
return (self.x, self.y,self.errors)
def observe(self,sample): # in (s, a, r, s_) format
_,_,errors = self.compute_targets([(0,sample)])
self.memory.add(errors[0], sample)
if self.steps % UPDATE_TARGET_FREQUENCY == 0:
self.DQN.target_model_update()
self.steps += 1
self.epsilon = min_eps + (self.maxEpsilone - min_eps) * np.exp(-1*_lambda_ * self.steps)
def replay(self):
batch = self.memory.sample(batch_size)
x, y,errors = self.compute_targets(batch)
for i in range(len(batch)):
idx = batch[i][0]
self.memory.update(idx, errors[i])
self.DQN.train(x,y)
class RandomAgent:
memory = Memory(memory_size)
exp = 0
steps = 0
def __init__(self, num_actions):
self.num_actions = num_actions
self.agent_type = 'Learning'
self.rand = True
def act(self, s):
best_act = np.random.randint(self.num_actions)
return sel_action(best_act), sel_action(best_act)
def observe(self, sample): # in (s, a, r, s_) format
error = abs(sample[2]) # reward
self.memory.add(error, sample)
self.exp += 1
self.steps += 1
def replay(self):
pass
自动驾驶汽车的环境
自动驾驶汽车的环境是欧派健身房的CarRacing-v0。在这个 OpenAI 环境中呈现给代理的状态是CarRacing-v0中模拟汽车前方的图像。环境也根据代理在给定状态下采取的行动返回奖励。如果汽车踩在草地上,我们会处罚奖励,并且会将奖励正常化到(-1,1)范围内进行稳定训练。环境的详细代码如下
import gym
from gym import envs
import numpy as np
from helper_functions import rgb2gray,action_list,sel_action,sel_action_index
from keras import backend as K
seed_gym = 3
action_repeat_num = 8
patience_count = 200
epsilon_greedy = True
max_reward = 10
grass_penalty = 0.8
max_num_steps = 200
max_num_episodes = action_repeat_num*100
'''
Enviroment to interact with the Agent
'''
class environment:
def __init__(self, environment_name,img_dim,num_stack,num_actions,render,lr):
self.environment_name = environment_name
print(self.environment_name)
self.env = gym.make(self.environment_name)
envs.box2d.car_racing.WINDOW_H = 500
envs.box2d.car_racing.WINDOW_W = 600
self.episode = 0
self.reward = []
self.step = 0
self.stuck_at_local_minima = 0
self.img_dim = img_dim
self.num_stack = num_stack
self.num_actions = num_actions
self.render = render
self.lr = lr
if self.render == True:
print("Rendering proeprly set")
else:
print("issue in Rendering")
# Agent performing its task
def run(self,agent):
self.env.seed(seed_gym)
img = self.env.reset()
img = rgb2gray(img, True)
s = np.zeros(self.img_dim)
#Collecting the state
for i in range(self.num_stack):
s[:,:,i] = img
s_ = s
R = 0
self.step = 0
a_soft = a_old = np.zeros(self.num_actions)
a = action_list[0]
#print(agent.agent_type)
while True:
if agent.agent_type == 'Learning' :
if self.render == True :
self.env.render("human")
if self.step % action_repeat_num == 0:
if agent.rand == False:
a_old = a_soft
#Agent outputs the action
a,a_soft = agent.act(s)
# Rescue Agent stuck at local minima
if epsilon_greedy:
if agent.rand == False:
if a_soft.argmax() == a_old.argmax():
self.stuck_at_local_minima += 1
if self.stuck_at_local_minima >= patience_count:
print('Stuck in local minimum, reset learning rate')
agent.steps = 0
K.set_value(agent.DQN.opt.lr,self.lr*10)
self.stuck_at_local_minima = 0
else:
self.stuck_at_local_minima =
max(self.stuck_at_local_minima -2, 0)
K.set_value(agent.DQN.opt.lr,self.lr)
#Perform the action on the environment
img_rgb, r,done,info = self.env.step(a)
if not done:
# Create the next state
img = rgb2gray(img_rgb, True)
for i in range(self.num_stack-1):
s_[:,:,i] = s_[:,:,i+1]
s_[:,:,self.num_stack-1] = img
else:
s_ = None
# Cumulative reward tracking
R += r
# Normalize reward given by the gym environment
r = (r/max_reward)
if np.mean(img_rgb[:,:,1]) > 185.0:
# Penalize if the car is on the grass
r -= grass_penalty
# Keeping the value of reward within -1 and 1
r = np.clip(r, -1 ,1)
#Agent has a whole state,action,reward,and next state to learn from
agent.observe( (s, a, r, s_) )
agent.replay()
s = s_
else:
img_rgb, r, done, info = self.env.step(a)
if not done:
img = rgb2gray(img_rgb, True)
for i in range(self.num_stack-1):
s_[:,:,i] = s_[:,:,i+1]
s_[:,:,self.num_stack-1] = img
else:
s_ = None
R += r
s = s_
if (self.step % (action_repeat_num * 5) == 0) and
(agent.agent_type=='Learning'):
print('step:', self.step, 'R: %.1f' % R, a, 'rand:', agent.rand)
self.step += 1
if done or (R <-5) or (self.step > max_num_steps) or
np.mean(img_rgb[:,:,1]) > 185.1:
self.episode += 1
self.reward.append(R)
print('Done:', done, 'R<-5:', (R<-5), 'Green
>185.1:',np.mean(img_rgb[:,:,1]))
break
print("Episode ",self.episode,"/", max_num_episodes,agent.agent_type)
print("Average Episode Reward:", R/self.step, "Total Reward:",
sum(self.reward))
def test(self,agent):
self.env.seed(seed_gym)
img= self.env.reset()
img = rgb2gray(img, True)
s = np.zeros(self.img_dim)
for i in range(self.num_stack):
s[:,:,i] = img
R = 0
self.step = 0
done = False
while True :
self.env.render('human')
if self.step % action_repeat_num == 0:
if(agent.agent_type == 'Learning'):
act1 = agent.DQN.predict_single_state(s)
act = sel_action(np.argmax(act1))
else:
act = agent.act(s)
if self.step <= 8:
act = sel_action(3)
img_rgb, r, done,info = self.env.step(act)
img = rgb2gray(img_rgb, True)
R += r
for i in range(self.num_stack-1):
s[:,:,i] = s[:,:,i+1]
s[:,:,self.num_stack-1] = img
if(self.step % 10) == 0:
print('Step:', self.step, 'action:',act, 'R: %.1f' % R)
print(np.mean(img_rgb[:,:,0]), np.mean(img_rgb[:,:,1]),
np.mean(img_rgb[:,:,2]))
self.step += 1
if done or (R< -5) or (agent.steps > max_num_steps) or
np.mean(img_rgb[:,:,1]) > 185.1:
R = 0
self.step = 0
print('Done:', done, 'R<-5:', (R<-5), 'Green>
185.1:',np.mean(img_rgb[:,:,1]))
break
上面代码中的run函数表示代理在环境上下文中的活动。
把它们放在一起
main.py脚本将环境的逻辑、DQN和agent适当地放在一起,使汽车能够通过强化学习来学习驾驶。详细代码如下:
import sys
#sys.path.append('/home/santanu/ML_DS_Catalog-/Python-Artificial-Intelligence-Projects_backup/Python-Artificial-Intelligence-Projects/Chapter09/Scripts/')
from gym import envs
from Agents import Agent,RandomAgent
from helper_functions import action_list,model_save
from environment import environment
import argparse
import numpy as np
import random
from sum_tree import sum_tree
from sklearn.externals import joblib
'''
This is the main module for training and testing the CarRacing Application from gym
'''
if __name__ == "__main__":
#Define the Parameters for training the Model
parser = argparse.ArgumentParser(description='arguments')
parser.add_argument('--environment_name',default='CarRacing-v0')
parser.add_argument('--model_path',help='model_path')
parser.add_argument('--train_mode',type=bool,default=True)
parser.add_argument('--test_mode',type=bool,default=False)
parser.add_argument('--epsilon_greedy',default=True)
parser.add_argument('--render',type=bool,default=True)
parser.add_argument('--width',type=int,default=96)
parser.add_argument('--height',type=int,default=96)
parser.add_argument('--num_stack',type=int,default=4)
parser.add_argument('--lr',type=float,default=1e-3)
parser.add_argument('--huber_loss_thresh',type=float,default=1.)
parser.add_argument('--dropout',type=float,default=1.)
parser.add_argument('--memory_size',type=int,default=10000)
parser.add_argument('--batch_size',type=int,default=128)
parser.add_argument('--max_num_episodes',type=int,default=500)
args = parser.parse_args()
environment_name = args.environment_name
model_path = args.model_path
test_mode = args.test_mode
train_mode = args.train_mode
epsilon_greedy = args.epsilon_greedy
render = args.render
width = args.width
height = args.height
num_stack = args.num_stack
lr = args.lr
huber_loss_thresh = args.huber_loss_thresh
dropout = args.dropout
memory_size = args.memory_size
dropout = args.dropout
batch_size = args.batch_size
max_num_episodes = args.max_num_episodes
max_eps = 1
min_eps = 0.02
seed_gym = 2 # Random state
img_dim = (width,height,num_stack)
num_actions = len(action_list)
if __name__ == '__main__':
environment_name = 'CarRacing-v0'
env = environment(environment_name,img_dim,num_stack,num_actions,render,lr)
num_states = img_dim
print(env.env.action_space.shape)
action_dim = env.env.action_space.shape[0]
assert action_list.shape[1] ==
action_dim,"length of Env action space does not match action buffer"
num_actions = action_list.shape[0]
# Setting random seeds with respect to python inbuilt random and numpy random
random.seed(901)
np.random.seed(1)
agent = Agent(num_states, num_actions,img_dim,model_path)
randomAgent = RandomAgent(num_actions)
print(test_mode,train_mode)
try:
#Train agent
if test_mode:
if train_mode:
print("Initialization with random agent. Fill memory")
while randomAgent.exp < memory_size:
env.run(randomAgent)
print(randomAgent.exp, "/", memory_size)
agent.memory = randomAgent.memory
randomAgent = None
print("Starts learning")
while env.episode < max_num_episodes:
env.run(agent)
model_save(model_path, "DDQN_model.h5", agent, env.reward)
else:
# Load train Model
print('Load pre-trained agent and learn')
agent.DQN.model.load_weights(model_path+"DDQN_model.h5")
agent.DQN.target_model_update()
try :
agent.memory = joblib.load(model_path+"DDQN_model.h5"+"Memory")
Params = joblib.load(model_path+"DDQN_model.h5"+"agent_param")
agent.epsilon = Params[0]
agent.steps = Params[1]
opt = Params[2]
agent.DQN.opt.decay.set_value(opt['decay'])
agent.DQN.opt.epsilon = opt['epsilon']
agent.DQN.opt.lr.set_value(opt['lr'])
agent.DQN.opt.rho.set_value(opt['rho'])
env.reward = joblib.load(model_path+"DDQN_model.h5"+"Rewards")
del Params, opt
except:
print("Invalid DDQL_Memory_.csv to load")
print("Initialization with random agent. Fill memory")
while randomAgent.exp < memory_size:
env.run(randomAgent)
print(randomAgent.exp, "/", memory_size)
agent.memory = randomAgent.memory
randomAgent = None
agent.maxEpsilone = max_eps/5
print("Starts learning")
while env.episode < max_num_episodes:
env.run(agent)
model_save(model_path, "DDQN_model.h5", agent, env.reward)
else:
print('Load agent and play')
agent.DQN.model.load_weights(model_path+"DDQN_model.h5")
done_ctr = 0
while done_ctr < 5 :
env.test(agent)
done_ctr += 1
env.env.close()
#Graceful exit
except KeyboardInterrupt:
print('User interrupt..gracefule exit')
env.env.close()
if test_mode == False:
# Prompt for Model save
print('Save model: Y or N?')
save = input()
if save.lower() == 'y':
model_save(model_path, "DDQN_model.h5", agent, env.reward)
else:
print('Model is not saved!')
助手函数
以下是该强化学习框架中使用的几个辅助函数,用于动作选择、存储用于训练的观察值、状态图像处理和保存训练模型的权重:
"""
Created on Thu Nov 2 16:03:46 2017
@author: Santanu Pattanayak
"""
from keras import backend as K
import numpy as np
import shutil, os
import numpy as np
import pandas as pd
from scipy import misc
import pickle
import matplotlib.pyplot as plt
from sklearn.externals import joblib
huber_loss_thresh = 1
action_list = np.array([
[0.0, 0.0, 0.0], #Brake
[-0.6, 0.05, 0.0], #Sharp left
[0.6, 0.05, 0.0], #Sharp right
[0.0, 0.3, 0.0]] ) #Staight
rgb_mode = True
num_actions = len(action_list)
def sel_action(action_index):
return action_list[action_index]
def sel_action_index(action):
for i in range(num_actions):
if np.all(action == action_list[i]):
return i
raise ValueError('Selected action not in list')
def huber_loss(y_true,y_pred):
error = (y_true - y_pred)
cond = K.abs(error) <= huber_loss_thresh
if cond == True:
loss = 0.5 * K.square(error)
else:
loss = 0.5 *huber_loss_thresh**2 + huber_loss_thresh*(K.abs(error) - huber_loss_thresh)
return K.mean(loss)
def rgb2gray(rgb,norm=True):
gray = np.dot(rgb[...,:3], [0.299, 0.587, 0.114])
if norm:
# normalize
gray = gray.astype('float32') / 128 - 1
return gray
def data_store(path,action,reward,state):
if not os.path.exists(path):
os.makedirs(path)
else:
shutil.rmtree(path)
os.makedirs(path)
df = pd.DataFrame(action, columns=["Steering", "Throttle", "Brake"])
df["Reward"] = reward
df.to_csv(path +'car_racing_actions_rewards.csv', index=False)
for i in range(len(state)):
if rgb_mode == False:
image = rgb2gray(state[i])
else:
image = state[i]
misc.imsave( path + "img" + str(i) +".png", image)
def model_save(path,name,agent,R):
''' Saves actions, rewards and states (images) in DataPath'''
if not os.path.exists(path):
os.makedirs(path)
agent.DQN.model.save(path + name)
print(name, "saved")
print('...')
joblib.dump(agent.memory,path+name+'Memory')
joblib.dump([agent.epsilon,agent.steps,agent.DQN.opt.get_config()], path+name+'AgentParam')
joblib.dump(R,path+name+'Rewards')
print('Memory pickle dumped')
自动驾驶汽车强化学习过程的培训可按如下方式调用
python main.py --environment_name 'CarRacing-v0' --model_path '/home/santanu/Autonomous Car/train/' --train_mode True --test_mode False --epsilon_greedy True --render True --width 96 --height 96 --num_stack 4 --huber_loss_thresh 1 --dropout 0.2 --memory_size 10000 --batch_size 128 --max_num_episodes 500
培训结果
最初,自动驾驶汽车会犯错,但经过一段时间后,汽车通过训练从错误中学习,因此变得更好。这张截图显示了汽车在训练的最初阶段的活动图像,然后是训练的后期,当它从以前的错误中吸取教训时的图像。这一点已经在下面的截图中说明了(图 9.5(A )和图 9.5(B) ):

Figure 9.5(A): The car making mistakes in the initial part of the training
以下结果显示汽车经过足够的训练后成功驾驶:

Figure 9.5(B): The car driving successfully after sufficient training
摘要
至此,我们结束了这一章。本章中讨论的主题将帮助您跟上强化学习范式,并使您能够构建智能 RL 系统。此外,读者应该将在本项目中学到的技术应用于其他基于 RL 的问题。
在下一章中,我们将从深度学习的角度来看验证码,并围绕它构建一些有趣的项目。期待您的参与。
十、深度学习视角下的验证码
术语验证码是完全自动化的公共图灵测试的首字母缩写,用来区分计算机和人类。这是一个旨在区分人类用户和机器或机器人的计算机程序,通常作为防止垃圾邮件和数据滥用的安全措施。验证码的概念早在 1997 年就被引入,当时互联网搜索公司 AltaVista 试图阻止自动向平台提交网址,这扭曲了他们的搜索引擎算法。为了解决这个问题,AltaVista 的首席科学家 Andrei Broder 想出了一种算法,可以随机生成文本图像,这些图像很容易被人类识别,但不会被机器人识别。后来,在 2003 年,路易斯·冯·安恩、曼努埃尔·布卢姆、尼古拉斯·J·霍珀和约翰·兰福德完善了这项技术,并将其称为验证码。最常见的验证码形式需要用户识别失真图像中的字母和数字。这项测试的目的是希望人类能够很容易地区分扭曲图像中的字符,而自动程序或机器人将无法区分它们。验证码测试有时被称为反向图灵测试,因为它是由计算机而不是人类执行的。
直到最近,验证码已经开始发挥比防止机器人欺诈更大的作用。例如,谷歌在将《纽约时报》的档案和谷歌图书中的一些书籍数字化时,使用了验证码及其变体之一 reCAPTCHA。这通常是通过要求用户正确输入多个验证码的字符来完成的。实际上只有一个验证码被标记并用于验证用户是否是人类。
其余的验证码由用户标记。目前,谷歌使用基于图像的验证码来帮助标记其自动驾驶汽车数据集,如下图所示:

Figure 10.1: Some common CAPTCHAs on various websites
在本章中,我们将涵盖以下主题:
- 什么是验证码
- 使用深度学习破解验证码,暴露其漏洞
- 利用对抗性学习生成验证码
技术要求
你将需要具备 Python 3、TensorFlow、Keras 和 OpenCV 的基本知识。
本章代码文件可在 GitHub:
https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 10上找到
查看以下视频,查看正在运行的代码:
http://bit.ly/2SgwR6P
用深度学习打破藩篱
随着卷积神经网络 ( CNNs )最近在计算机视觉任务中的成功,在几分钟内打破基本的验证码是一项相对容易的任务。因此,验证码需要比过去更加进化。在本章的第一部分,我们将通过深度学习框架,揭示使用僵尸工具自动检测基本验证码的漏洞。我们将通过利用 GAN 创建更难被机器人检测到的验证码来跟进此事。
生成基本验证码
验证码可以使用 Python 中的Claptcha包生成。我们用它来生成由数字和文本组成的四个字符的验证码图像。因此,每个字符可以是26字母和10数字中的任何一个。以下代码可用于生成随机选择字母和数字的验证码:
alphabets = 'abcdefghijklmnopqrstuvwxyz'
alphabets = alphabets.upper()
font = "/home/santanu/Android/Sdk/platforms/android-28/data/fonts/DancingScript-Regular.ttf"
# For each of the 4 characters determine randomly whether its a digit or alphabet
char_num_ind = list(np.random.randint(0,2,4))
text = ''
for ind in char_num_ind:
if ind == 1:
# for indicator 1 select character else number
loc = np.random.randint(0,26,1)
text = text + alphabets[np.random.randint(0,26,1)[0]]
else:
text = text + str(np.random.randint(0,10,1)[0])
c = Claptcha(text,font)
text,image = c.image
plt.imshow(image)
下面的截图(图 10.2 )是前面代码生成的随机验证码:

Figure 10.2: Random CAPTCHA with the characters 26UR
除了文本之外,Claptcha工具还要求输入打印文本的字体。正如我们所看到的,它以水平轴上有点扭曲的线条的形式给图像添加了噪声。
为训练验证码生成数据
在本节中,我们将使用Claptcha工具生成几个验证码来训练一个 CNN 模型。CNN 模型将通过监督训练来学习识别验证码中的字符。我们将生成一个训练和验证集,用于训练 CNN 模型。除此之外,我们还将生成一个单独的测试集来评估它概括未知数据的能力。CaptchaGenerator.py脚本可以编码如下,生成验证码数据:
from claptcha import Claptcha
import os
import numpy as np
import cv2
import fire
from elapsedtimer import ElasedTimer
def generate_captcha(outdir,font,num_captchas=20000):
alphabets = 'abcdefghijklmnopqrstuvwxyz'
alphabets = alphabets.upper()
try:
os.mkdir(outdir)
except:
'Directory already present,writing captchas to the same'
#rint(char_num_ind)
# select one alphabet if indicator 1 else number
for i in range(num_captchas):
char_num_ind = list(np.random.randint(0,2,4))
text = ''
for ind in char_num_ind:
if ind == 1:
loc = np.random.randint(0,26,1)
text = text + alphabets[np.random.randint(0,26,1)[0]]
else:
text = text + str(np.random.randint(0,10,1)[0])
c = Claptcha(text,font)
text,image = c.image
image.save(outdir + text + '.png')
def main_process(outdir_train,num_captchas_train,
outdir_val,num_captchas_val,
outdir_test,num_captchas_test,
font):
generate_captcha(outdir_train,font,num_captchas_train)
generate_captcha(outdir_val,font,num_captchas_val)
generate_captcha(outdir_test,font,num_captchas_test)
if __name__ == '__main__':
with ElasedTimer('main_process'):
fire.Fire(main_process)
需要注意的一点是,大多数验证码生成器使用ttf文件来获取验证码的字体模式。
我们可以使用CaptchaGenerator.py脚本生成大小为16000、4000和4000的训练集、验证集和测试集,如下所示:
python CaptchaGenerator.py --outdir_train '/home/santanu/Downloads/Captcha Generation/captcha_train/' --num_captchas_train 16000 --outdir_val '/home/santanu/Downloads/Captcha Generation/captcha_val/' --num_captchas_val 4000
--outdir_test '/home/santanu/Downloads/Captcha Generation/captcha_test/' --num_captchas_test 4000 --font "/home/santanu/Android/Sdk/platforms/android-28/data/fonts/DancingScript-Regular.ttf"
脚本用3.328 mins生成16000训练验证码、4000验证验证码、4000测试验证码,从下面的脚本日志可以看到:
3.328 min: main_process
在下一节中,我们将讨论验证码破解者的卷积神经网络架构。
验证码破解 CNN 架构
我们将使用美国有线电视新闻网的架构来识别验证码中的字符。美国有线电视新闻网在密集层之前会有两对卷积和汇集。我们不是将整个验证码输入网络,而是将验证码分成四个字符,并分别输入模型。这需要 CNN 的最终输出层预测属于26字母和10数字的36类之一。
通过函数_model_,可以定义如下代码所示的模型:
def _model_(n_classes):
# Build the neural network
input_ = Input(shape=(40,25,1))
# First convolutional layer with max pooling
x = Conv2D(20, (5, 5), padding="same",activation="relu")(input_)
x = MaxPooling2D(pool_size=(2, 2), strides=(2, 2))(x)
x = Dropout(0.2)(x)
# Second convolutional layer with max pooling
x = Conv2D(50, (5, 5), padding="same", activation="relu")(x)
x = MaxPooling2D(pool_size=(2, 2), strides=(2, 2))(x)
x = Dropout(0.2)(x)
# Hidden layer with 1024 nodes
x = Flatten()(x)
x = Dense(1024, activation="relu")(x)
# Output layer with 36 nodes (one for each possible alphabet/digit we predict)
out = Dense(n_classes,activation='softmax')(x)
model = Model(inputs=[input_],outputs=out)
model.compile(loss="sparse_categorical_crossentropy", optimizer="adam", metrics=
["accuracy"])
return model
验证码破解者 CNN 模型可以如下图所示进行图形化描述(图 10.3 ):

Figure 10.3: The CAPTCHA breaker CNN architecture
预处理验证码图像
图像的原始像素与美国有线电视新闻网的架构不太匹配。为了让 CNN 更快地收敛,对图像进行归一化总是一个好主意。通常用作归一化方案的两种方法是平均像素减法或通过将像素值除以255将像素缩放至位于[0,1]范围内。对于我们的美国有线电视新闻网,我们将正常化图像,使其位于[0,1]的范围内。我们还将处理验证码的灰度图像,这意味着我们将只处理一个颜色通道。load_img功能可用于加载和预处理验证码图片,如以下代码所示:
def load_img(path,dim=(100,40)):
img = cv2.imread(path,cv2.IMREAD_GRAYSCALE)
img = cv2.resize(img,dim)
img = img.reshape((dim[1],dim[0],1))
#print(img.shape)
return img/255.
将验证码字符转换为类
验证码的原始字符需要转换成数字类来进行训练。create_dict_char_to_index功能可用于将原始字符转换为类别标签:
def create_dict_char_to_index():
chars = 'abcdefghijklmnopqrstuvwxyz0123456789'.upper()
chars = list(chars)
index = np.arange(len(chars))
char_to_index_dict,index_to_char_dict = {},{}
for v,k in zip(index,chars):
char_to_index_dict[k] = v
index_to_char_dict[v] = k
return char_to_index_dict,index_to_char_dict
数据生成程序
动态生成成批的训练和验证数据对于以高效的方式训练 CNN 是很重要的。在训练开始之前将所有数据加载到内存中可能会导致数据存储问题,因此在训练期间动态读取验证码和构建批处理是有意义的。这导致资源的最佳利用。
我们将使用一个数据生成器,它可以用于构建训练和验证批次。生成器将在初始化期间存储验证码文件位置,并在每个时期动态构建批处理。文件的顺序在每个之后被随机打乱,使得验证码图像在每个时期不以相同的顺序遍历。这通常可以确保模型在训练过程中不会陷入糟糕的局部极小值。数据生成器类可以编码如下:
class DataGenerator(keras.utils.Sequence):
'Generates data for Keras'
def __init__(self,dest,char_to_index_dict,batch_size=32,n_classes=36,dim=(40,100,1),shuffle=True):
'Initialization'
self.dest = dest
self.files = os.listdir(self.dest)
self.char_to_index_dict = char_to_index_dict
self.batch_size = batch_size
self.n_classes = n_classes
self.dim = (40,100)
self.shuffle = shuffle
self.on_epoch_end()
def __len__(self):
'Denotes the number of batches per epoch'
return int(np.floor(len(self.files) / self.batch_size))
def __getitem__(self, index):
'Generate one batch of data'
# Generate indexes of the batch
indexes = self.indexes[index*self.batch_size:(index+1)*self.batch_size]
# Find list of files to be processed in the batch
list_files = [self.files[k] for k in indexes]
# Generate data
X, y = self.__data_generation(list_files)
return X, y
def on_epoch_end(self):
'Updates indexes after each epoch'
self.indexes = np.arange(len(self.files))
if self.shuffle == True:
np.random.shuffle(self.indexes)
def __data_generation(self,list_files):
'Generates data containing batch_size samples' # X :
(n_samples, *dim, n_channels)
# Initialization
dim_h = dim[0]
dim_w = dim[1]//4
channels = dim[2]
X = np.empty((4*len(list_files),dim_h,dim_w,channels))
y = np.empty((4*len(list_files)),dtype=int)
# print(X.shape,y.shape)
# Generate data
k = -1
for f in list_files:
target = list(f.split('.')[0])
target = [self.char_to_index_dict[c] for c in target]
img = load_img(self.dest + f)
img_h,img_w = img.shape[0],img.shape[1]
crop_w = img.shape[1]//4
for i in range(4):
img_crop = img[:,i*crop_w:(i+1)*crop_w]
k+=1
X[k,] = img_crop
y[k] = int(target[i])
return X,y
训练验证码破解者
验证码破解模型可以通过调用train函数进行训练,如下所示:
def train(dest_train,dest_val,outdir,batch_size,n_classes,dim,shuffle,epochs,lr):
char_to_index_dict,index_to_char_dict = create_dict_char_to_index()
model = _model_(n_classes)
train_generator = DataGenerator(dest_train,char_to_index_dict,batch_size,n_classes,dim,shuffle)
val_generator = DataGenerator(dest_val,char_to_index_dict,batch_size,n_classes,dim,shuffle)
model.fit_generator(train_generator,epochs=epochs,validation_data=val_generator)
model.save(outdir + 'captcha_breaker.h5')
对于批处理中的验证码,所有四个字符都被考虑用于训练。我们使用DataGenerator类定义train_generator和val_generator对象。这些数据生成器动态地为训练和验证提供批处理。
可以通过运行带有如下train参数的captcha_solver.py脚本来调用训练:
python captcha_solver.py train --dest_train '/home/santanu/Downloads/Captcha Generation/captcha_train/' --dest_val '/home/santanu/Downloads/Captcha Generation/captcha_val/' --outdir '/home/santanu/ML_DS_Catalog-/captcha/model/' --batch_size 16 --lr 1e-3 --epochs 20 --n_classes 36 --shuffle True --dim '(40,100,1)'
仅在20训练阶段,该模型就实现了验证码每个字符级别约 98.3%的验证准确率,如以下输出日志所示:
Epoch 17/20
1954/1954 [==============================] - 14s 7ms/step - loss: 0.0340 - acc: 0.9896 - val_loss: 0.0781 - val_acc: 0.9835
Epoch 18/20
1954/1954 [==============================] - 13s 7ms/step - loss: 0.0310 - acc: 0.9904 - val_loss: 0.0679 - val_acc: 0.9851
Epoch 19/20
1954/1954 [==============================] - 13s 7ms/step - loss: 0.0315 - acc: 0.9904 - val_loss: 0.0813 - val_acc: 0.9822
Epoch 20/20
1954/1954 [==============================] - 13s 7ms/step - loss: 0.0297 - acc: 0.9910 - val_loss: 0.0824 - val_acc: 0.9832
4.412 min: captcha_solver
The training time for 20 epochs with roughly 16000 98.3s (that is, 64000 CAPTCHA characters) is around 4.412 min using a GeForce GTX 1070 GPU. Readers are advised to use a GPU based machine for faster training.
测试数据集的准确性
测试数据的推断可以通过调用evaluate函数来运行。evaluate功能说明如下,供参考。请注意,评估应该从整体验证码的角度来看准确性,而不是验证码的字符级别。因此,只有当验证码目标的所有四个字符都与预测匹配时,我们才能将验证码标记为被美国有线电视新闻网正确识别。
在测试验证码上运行推理的evaluate功能可以编码如下:
def evaluate(model_path,eval_dest,outdir,fetch_target=True):
char_to_index_dict,index_to_char_dict = create_dict_char_to_index()
files = os.listdir(eval_dest)
model = keras.models.load_model(model_path)
predictions,targets = [],[]
for f in files:
if fetch_target == True:
target = list(f.split('.')[0])
targets.append(target)
pred = []
img = load_img(eval_dest + f)
img_h,img_w = img.shape[0],img.shape[1]
crop_w = img.shape[1]//4
for i in range(4):
img_crop = img[:,i*crop_w:(i+1)*crop_w]
img_crop = img_crop[np.newaxis,:]
pred_index = np.argmax(model.predict(img_crop),axis=1)
#print(pred_index)
pred_char = index_to_char_dict[pred_index[0]]
pred.append(pred_char)
predictions.append(pred)
df = pd.DataFrame()
df['files'] = files
df['predictions'] = predictions
if fetch_target == True:
match = []
df['targets'] = targets
accuracy_count = 0
for i in range(len(files)):
if targets[i] == predictions[i]:
accuracy_count+= 1
match.append(1)
else:
match.append(0)
print(f'Accuracy: {accuracy_count/float(len(files))} ')
eval_file = outdir + 'evaluation.csv'
df['match'] = match
df.to_csv(eval_file,index=False)
print(f'Evaluation file written at: {eval_file} ')
可以运行以下命令来调用captcha_solver.py脚本的evaluate功能进行推理:
python captcha_solver.py evaluate --model_path /home/santanu/ML_DS_Catalog-/captcha/model/captcha_breaker.h5 --eval_dest '/home/santanu/Downloads/Captcha Generation/captcha_test/' --outdir /home/santanu/ML_DS_Catalog-/captcha/ --fetch_target True
在4000验证码测试数据集上实现的准确率在 93%左右。运行evaluate功能的输出如下:
Accuracy: 0.9320972187421699
Evaluation file written at: /home/santanu/ML_DS_Catalog-/captcha/evaluation.csv
13.564 s: captcha_solver
我们还可以看到对那些4000验证码的推断花费了大约 14 秒,评估的输出被写入/home/santanu/ML_DS_Catalog-/captcha/evaluation.csv文件。
我们将在下面的截图中查看模型没有做好的一些目标和预测(图 10.4 ):

Figure 10.4: CAPTCHAs where the CAPTCHA solver model failed
通过对抗性学习生成验证码
在本节中,我们将通过生成性对抗网络创建验证码。我们将生成类似于街景门牌号数据集( SVHN 数据集)中的图像。这个想法是使用这些 GAN 生成的图像作为验证码。只有当我们训练了氮化镓,它们才容易从噪声分布中采样。这将减轻通过更复杂的方法创建验证码的需要。它还将为验证码中使用的 SVHN 街道号码提供一些变化。
SVHN 是一个真实世界的数据集,由于其在对象识别算法中的应用,在机器学习和深度学习领域非常受欢迎。顾名思义,该数据集包含从谷歌街景图像获得的门牌号真实图像。数据集可从以下链接下载:http://ufldl.stanford.edu/housenumbers/。
我们将使用调整后的门牌号数据集,其中图像已经调整到尺寸(32,32)。我们感兴趣的数据集是train_32x32.mat。
通过这个生成对抗网络 ( GAN )我们将从随机噪声中生成门牌号图像,生成的图像将非常类似于 SVHN 数据集中的图像。
简单回顾一下,在一个 GAN 中,我们有一个生成器( G )和一个鉴别器( D ),它们根据损失函数相互进行零和极小极大游戏。随着时间的推移,发生器和鉴别器都在工作中变得更好,直到我们达到一个稳定的点,两者都不能进一步改善。这个稳定点是损失函数的鞍点。对于我们的应用,发电机 G 将把噪声 z 从给定的分布 P(z) 转换成门牌号图像 x ,使得 x = G(z) 。
该生成的图像通过鉴别器 D ,该鉴别器试图将该生成的图像 x 检测为假的,并且将来自 SVHN 数据集的真实门牌号图像检测为真实的。同时,生成器将尝试创建图像 x = G(z) ,使得鉴别器发现图像是真实的。如果我们将真实图像标记为1,将生成器生成的假图像标记为0,那么鉴别器将尝试最小化二进制交叉熵损失,作为给定两个类别的分类器网络。鉴频器 D 最小化的损耗可以写成:

在前面的表达式 D(。)是鉴别器函数,其输出表示将图像标记为真实的概率。 P z (z) 表示随机变量噪声 z、的分布,而 P X (x) 表示真实门牌号图像的分布。 G(。)和 D(。)分别表示发生器网络功能和鉴别器网络功能。这些将由网络的权重参数化,我们已经方便地跳过了杂乱的符号。如果我们用
表示发电机网络权重的参数,用
表示鉴别器网络的参数,那么鉴别器将学会相对于
最小化 (1) 中的损耗,而发电机将致力于相对于
最大化 (1) 中的相同损耗。我们可以将 (1) 中优化的损耗称为发生器和鉴别器都在优化其参数的效用函数。效用函数 U 可以写成发生器和鉴别器参数的函数,如下所示:

从博弈论的角度来看,生成器 G 和鉴别器 D 用效用函数
相互进行零和极小极大博弈,极小极大博弈的优化问题可以表示为:


在参数空间中的一个点上,如果一个函数对于某些参数是局部最大值,对于其余参数是局部最小值,那么这个点被称为鞍点。因此,
给出的点将是效用函数
的鞍点。这个鞍点是极小极大零和博弈的纳什均衡,并且参数
对于生成器和鉴别器正在优化的效用来说是最优的。就眼前的问题而言,生成器 G 会产生最难识别的验证码,以
为参数进行检测。同样,鉴别器最适合检测以
为参数的假验证码。
有鞍点的函数中最简单的是 x 2 - y 2 ,鞍点就是原点:(0,0)。
优化氮化镓损耗
在前一节中,我们已经看到,发生器和鉴别器相对于其各自网络参数的最佳状态由下式给出:

为了使目标函数最大化,我们通常使用梯度上升,而为了使成本函数最小化,我们使用梯度下降。前面的优化问题可以分解为两部分:生成器和鉴别器分别通过梯度上升和梯度下降轮流优化效用函数。在优化期间的任何步骤 t 中,鉴别器将通过如下最小化效用来尝试移动到新的状态:

或者,发电机将尝试最大化相同的效用。由于鉴别器 D 没有发电机的任何参数,效用的第二项不影响发电机的优化。同样的情况可以表述如下:



我们已经将生成器和鉴别器优化目标转换为最小化问题。鉴别器和生成器的优化使用梯度下降执行,直到我们到达目标函数的鞍点。
发电机网络
生成器网络将接收随机噪声,并尝试输出类似于 SVHN 图像的图像作为输出。随机噪声是一个100维输入向量。每个维度都是遵循标准正态分布的随机变量,平均值为0,标准差为1。
最初的致密层有8192个单位,被重塑为一个 4×4×512 形状的三维张量。张量可以被认为是带有512滤镜的 4×4 图像。为了增加张量的空间维度,我们进行了一系列的转置 2D 卷积,步长为2,核滤波器大小为 5×5。步幅大小决定转置卷积的缩放比例。例如,2 倍于输入图像的每个空间维度的步幅,随后是转置卷积,通常伴随着批量归一化,以便更好地收敛。除激活层外,网络使用LeakyReLU作为激活功能。网络的最终输出是 32 x 32 x 3 维的图像。
在最终层中使用tanh激活,以便在[-1,1]范围内归一化图像像素值。
发生器可以编码如下:
def generator(input_dim,alpha=0.2):
model = Sequential()
model.add(Dense(input_dim=input_dim, output_dim=4*4*512))
model.add(Reshape(target_shape=(4,4,512)))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha))
model.add(Conv2DTranspose(256, kernel_size=5, strides=2,
padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha))
model.add(Conv2DTranspose(128, kernel_size=5, strides=2,
padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha))
model.add(Conv2DTranspose(3, kernel_size=5, strides=2,
padding='same'))
model.add(Activation('tanh'))
return model
发电机的网络架构如下图所示(图 10.5 )供参考:

Figure 10.5: Generator network graph
鉴别网络
鉴别器将是一个经典的二进制分类卷积神经网络,它可以将生成器图像分类为假图像,并将实际的 SVHN 数据集图像分类为真实图像。
鉴别器网络可以编码如下:
def discriminator(img_dim,alpha=0.2):
model = Sequential()
model.add(
Conv2D(64, kernel_size=5,strides=2,
padding='same',
input_shape=img_dim)
)
model.add(LeakyReLU(alpha))
model.add(Conv2D(128,kernel_size=5,strides=2,padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha))
model.add(Conv2D(256,kernel_size=5,strides=2,padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha))
model.add(Flatten())
model.add(Dense(1))
model.add(Activation('sigmoid'))
return model
前一个代码块中定义的鉴别器网络将伪生成器图像和真实的 SVHN 图像作为输入,并在最终输出层之前通过3组 2D 卷积。该网络中的卷积之后不是池化,而是批量标准化和LeakyReLU激活。
鉴别器的网络架构如下图所示(图 10.6 ):

Figure 10.6: Discriminator network graph
鉴别器的输出激活函数是一个 sigmoid。这有助于从真实的 SVHN 图像中对伪生成图像进行二进制分类。
训练氮化镓
为生成性对抗网络建立训练流程并不简单,因为它需要大量的技术考虑。我们定义了如下三个培训网络:
- 发电机网络
g带参数![]()
- 带参数的鉴别器网络
d![]()
- 由带有权重
和
的g_d表示的组合发生器鉴别器网络
生成器g创建假图像,d鉴别器会评估这些图像,并试图将其标记为假图像。
在g_d网络中,g生成器创建假图像,然后试图欺骗d鉴别器相信它们是真实的。鉴别器网络是用二进制交叉熵损失编译的,并且损失是相对于鉴别器参数
优化的,而g_d网络是相对于g发生器的参数
编译的,以便欺骗鉴别器。因此,g_d网络损失是与鉴别器将所有假图像标记为真实图像相关的二进制交叉熵损失。在每个小批量中,发生器和鉴别器权重根据与g_d和d网络相关的损耗优化进行更新:
def train(dest_train,outdir,
gen_input_dim,gen_lr,gen_beta1,
dis_input_dim,dis_lr,dis_beta1,
epochs,batch_size,alpha=0.2,smooth_coef=0.1):
#X_train,X_test = read_data(dest_train),read_data(dest_test)
train_data = loadmat(dest_train + 'train_32x32.mat')
X_train, y_train = train_data['X'], train_data['y']
X_train = np.rollaxis(X_train, 3)
print(X_train.shape)
#Image pixels are normalized between -1 to +1 so that one can use the tanh activation function
#_train = (X_train.astype(np.float32) - 127.5)/127.5
X_train = (X_train/255)*2-1
g = generator(gen_input_dim,alpha)
plot_model(g,show_shapes=True, to_file='generator_model.png')
d = discriminator(dis_input_dim,alpha)
d_optim = Adam(lr=dis_lr,beta_1=dis_beta1)
d.compile(loss='binary_crossentropy',optimizer=d_optim)
plot_model(d,show_shapes=True, to_file='discriminator_model.png')
g_d = generator_discriminator(g, d)
g_optim = Adam(lr=gen_lr,beta_1=gen_beta1)
g_d.compile(loss='binary_crossentropy', optimizer=g_optim)
plot_model(g_d,show_shapes=True, to_file=
'generator_discriminator_model.png')
for epoch in range(epochs):
print("Epoch is", epoch)
print("Number of batches", int(X_train.shape[0]/batch_size))
for index in range(int(X_train.shape[0]/batch_size)):
noise =
np.random.normal(loc=0, scale=1, size=(batch_size,gen_input_dim))
image_batch = X_train[index*batch_size:(index+1)*batch_size,:]
generated_images = g.predict(noise, verbose=0)
if index % 20 == 0:
combine_images(generated_images,outdir,epoch,index)
# Images converted back to be within 0 to 255
print(image_batch.shape,generated_images.shape)
X = np.concatenate((image_batch, generated_images))
d1 = d.train_on_batch(image_batch,[1 - smooth_coef]*batch_size)
d2 = d.train_on_batch(generated_images,[0]*batch_size)
y = [1] * batch_size + [0] * batch_size
# Train the Discriminator on both real and fake images
make_trainable(d,True)
#_loss = d.train_on_batch(X, y)
d_loss = d1 + d2
print("batch %d d_loss : %f" % (index, d_loss))
noise =
np.random.normal(loc=0, scale=1, size=(batch_size,gen_input_dim))
make_trainable(d,False)
#d.trainable = False
# Train the generator on fake images from Noise
g_loss = g_d.train_on_batch(noise, [1] * batch_size)
print("batch %d g_loss : %f" % (index, g_loss))
if index % 10 == 9:
g.save_weights('generator', True)
d.save_weights('discriminator', True)
Adam优化器用于两个网络的优化。需要注意的一点是,需要编译网络g_d来优化发电机参数的损耗 G 。因此,我们需要在网络g_d中禁用鉴别器 D 参数的训练。
我们可以使用以下功能来禁用或启用网络参数的学习:
def make_trainable(model, trainable):
for layer in model.layers:
layer.trainable = trainable
我们可以通过将可训练设置为False来禁用参数的学习,而如果我们想要启用这些参数的训练,我们需要将其设置为True。
噪声分布
输入到 GAN 的噪声需要遵循特定的概率分布。一般均匀分布U[-1,1]或标准正态分布即均值为0且标准差为1的正态分布用于对噪声向量的每个维度进行采样。从经验上看,从标准正态分布中采样噪声似乎比从均匀分布中采样效果更好。在这个实现中,我们将使用标准正态分布对随机噪声进行采样。
数据预处理
如前所述,我们将使用维度32 x 32 x 3的 SVHN 数据集图像。
数据集图像很容易以矩阵数据形式获得。图像的原始像素在[-1,1]范围内归一化,以实现更快、更稳定的收敛。由于这种变换,发生器的最终激活被保持tanh以确保生成的图像具有在[-1,1]内的像素值。
read_data可用于处理输入数据。dir_flag用于确定我们是否有原始处理的数据矩阵文件或图像目录。例如,当我们使用 SVHN 数据集时,dir_flag应该设置为False,因为我们已经有了一个名为train_32x32.mat的预处理数据矩阵文件。
然而,最好保持read_data函数是通用的,因为这允许我们为其他数据集重用脚本。来自scipy.io的loadmat功能可用于读取train_32x32.mat。
如果输入的是放在目录中的原始图像,那么我们可以读取目录中可用的图像文件,并通过opencv读取它们。使用opencv可以使用load_img功能读取原始图像。
最后,为了更好地收敛网络,像素强度被归一化到[-1,1]的范围内:
def load_img(path,dim=(32,32)):
img = cv2.imread(path)
img = cv2.resize(img,dim)
img = img.reshape((dim[1],dim[0],3))
return img
def read_data(dest,dir_flag=False):
if dir_flag == True:
files = os.listdir(dest)
X = []
for f in files:
img = load_img(dest + f)
X.append(img)
return X
else:
train_data = loadmat(path)
X,y = train_data['X'], train_data['y']
X = np.rollaxis(X,3)
X = (X/255)*2-1
return X
调用培训
可以通过运行captcha_gan.py脚本的train功能调用 GAN 的训练,参数如下:
python captcha_gan.py train --dest_train '/home/santanu/Downloads/train_32x32.mat' --outdir '/home/santanu/ML_DS_Catalog-/captcha/SVHN/' --dir_flag False --batch_size 100 --gen_input_dim 100 --gen_beta1 0.5 --gen_lr 0.0001 --dis_input_dim '(32,32,3)' --dis_lr 0.001 --dis_beta1 0.5 --alpha 0.2 --epochs 100 --smooth_coef 0.1
前面的脚本使用fire Python 包来调用用户指定的函数,在我们的例子中是train。fire的好处在于,用户可以将函数的所有输入作为参数提供,正如我们从前面的命令中看到的那样。
众所周知,GANs 很难训练,因此需要调整这些参数,以便模型正常运行。以下是一些重要的参数:
| 参数 | 值 | comment |
| batch_size | 100 | 小批量随机梯度下降的批量。 |
| gen_input_dim | 100 | 输入随机噪声向量维数。 |
| gen_lr | 0.0001 | 发电机学习率。 |
| gen_beta1 | 0.5 | beta_1是生成器的 Adam 优化器的参数。 |
| dis_input_dim | (32,32,3) | 真假门牌号图像的形状鉴别器。 |
| dis_lr | 0.001 | 鉴别器网络的学习速率。 |
| dis_beta1 | 0.5 | beta_1是用于鉴别器的 Adam 优化器的参数。 |
| alpha | 0.2 | 这是LeakyReLU激活的泄漏因子。当activation函数的输入为负时,这有助于提供梯度(此处为0.2)。它有助于解决垂死的ReLU问题。如果输入小于或等于0,ReLU 函数的输出相对于其输入的梯度为0。来自后面层的反向传播误差乘以这个0,没有误差传递到前面的层,尽管与这个ReLU.相关的神经元据说已经死亡,许多这样的死亡ReLUs会影响训练。LeakyReLU通过即使对于负的输入值也提供小的梯度来克服这个问题,从而确保训练不会由于缺乏梯度而停止。 |
| epochs | 100 | 这是要运行的纪元数。 |
| smooth_coef | 0.1 | 这个平滑系数的设计是为了减少真实样本对鉴别器的损失。例如,0.1的smooth_coef会将归因于真实图像的损失减少到原始损失的 90%。这有助于 GANs 更好地收敛。 |
Training the GAN with these parameters takes around 3.12 hours, using a GeForce GTX 1070 GPU. Readers are advised to use a GPU for faster training.
培训期间验证码的质量
现在让我们研究一下在训练过程中不同时期生成的验证码的质量。以下为历元5(见图 10.7a )、历元51(见图 10.7b )和历元100(见图 10.7c 后的 CAPTCHAs 图像。我们可以看到验证码图像的质量随着训练的进行而提高。以下屏幕截图显示了在第 5 个时期生成的样本验证码的结果:

Figure 10.7a: Sample CAPTCHAs generated at epoch 5
以下屏幕截图显示了在第 51 个时期生成的样本验证码的结果:

Figure 10.7b: Sample CAPTCHAs generated at epoch 51
以下屏幕截图显示了在纪元 100 生成的样本验证码的结果:

Figure 10.7c: Sample CAPTCHAs generated at epoch 100
使用经过训练的生成器创建验证码以供使用
经过训练的 GAN 网络可以在运行时加载,以生成像验证码一样的街景房屋号码供使用。generate_captcha功能可以用来生成验证码使用,如下图所示:
def generate_captcha(gen_input_dim,alpha,
num_images,model_dir,outdir):
g = generator(gen_input_dim,alpha)
g.load_weights(model_dir + 'generator')
noise =
np.random.normal(loc=0, scale=1, size=(num_images,gen_input_dim))
generated_images = g.predict(noise, verbose=1)
for i in range(num_images):
img = generated_images[i,:]
img = np.uint8(((img+1)/2)*255)
img = Image.fromarray(img)
img.save(outdir + 'captcha_' + str(i) + '.png')
您可能想知道如何为这些生成的验证码创建标签,因为它们需要验证用户是人类还是机器人。这个想法很简单:发送未标记的验证码和一些标记的验证码,这样用户就不知道哪个验证码会被评估。一旦你有足够的标签生成验证码,把大多数标签作为实际标签,并使用它来评估。
通过调用以下命令,可以从captcha_gan.py脚本中调用generate_captcha功能:
python captcha_gan.py generate-captcha --gen_input_dim 100 --num_images 200 --model_dir '/home/santanu/ML_DS_Catalog-/captcha/' --outdir '/home/santanu/ML_DS_Catalog-/captcha/captcha_for_use/' --alpha 0.2
下面的截图(图 10.8 )描述了通过调用generate_captcha函数生成的一些验证码。我们可以看到这些图像足够体面,可以用作验证码:

Figure 10.8: Generated CAPTCHAs using the generator of the trained GAN network
摘要
至此,我们结束了这一章。与本章相关的所有代码都可以在 GitHub 链接中找到:https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 10。你现在会对深度学习如何影响验证码有一个公平的想法。在光谱的一端,我们可以看到带有深度学习人工智能应用程序的机器人可以多么容易地解决验证码。然而,在另一端,我们看到深度学习可以如何用于利用给定的数据集并从随机噪声中创建新的验证码。您可以利用深度学习,扩展本章中关于生成性对抗网络的技术知识,构建智能验证码生成系统。现在,我们来到这本书的结尾。我希望这九个基于人工智能的实际应用的旅程是丰富的。祝你一切顺利!




和
的
浙公网安备 33010602011771号