原文:Intelligent Projects Using Python
译者:飞龙
本文来自【ApacheCN 深度学习 译文集】,采用译后编辑(MTPE)流程来尽可能提升效率。
不要担心自己的形象,只关心如何实现目标。——《原则》,生活原则 2.3.c
一、人工智能系统的基础
人工智能(AI)在过去几年中一直处于技术的最前沿,并已进入主流应用,例如专家系统,移动设备上的个性化应用, 自然语言处理中的机器翻译,聊天机器人,自动驾驶汽车等。 但是,AI 的定义在很长一段时间以来一直是一个争论的主题。 这主要是因为所谓的 AI 效应将过去已经通过 AI 解决的工作归类为非 AI。 根据一位著名的计算机科学家的说法:
智能是机器尚未完成的一切。
–拉里·特斯勒(Larry Tesler)
在 1996 年 IBM 电脑 Deep Blue 击败 Gary Kasparov 之前,一直认为构建能下象棋的智能系统是 AI。类似地,曾经将视觉,言语和自然语言方面的问题视为复杂问题,但是由于 AI 的影响,它们现在仅被视为计算而非真正的 AI。 近来,人工智能已经能够解决复杂的数学问题,创作音乐和创作抽象绘画,并且人工智能的这些功能正在不断增加。 科学家将 AI 系统在未来等同于人类智能水平的点称为 AI 奇点。 机器是否会真正达到人类的智能水平这个问题非常令人着迷。
许多人会认为机器永远无法达到人类的智能水平,因为用来学习或执行智能任务的 AI 逻辑是由人类编程的,并且它们缺乏人类所拥有的意识和自我意识。 但是,一些研究人员提出了另一种想法,即人类意识和自我意识就像无限循环程序,可以通过反馈从周围的环境中学习。 因此,也有可能将意识和自我意识编程到机器中。 但是,就目前而言,我们将把 AI 的这一哲学方面再留一天,并简单地讨论我们所知道的 AI。
简而言之,AI 可以定义为机器(通常是计算机或机器人)以类人的智力执行任务的能力,例如具有推理,学习经验,归纳,破译含义和拥有的能力等属性。 视觉感知。 我们将坚持这个更实际的定义,而不是关注 AI 效应所带来的哲学内涵以及 AI 奇异性的前景。 尽管可能存在关于 AI 可以实现和不能实现的争论,但基于 AI 的系统的最新成功故事却令人瞩目。 下图描述了 AI 的一些较新的主流应用:
图 1.1:人工智能的应用
本书将涵盖来自 AI 的所有核心学科的项目的详细实现,概述如下:
- 基于迁移学习的 AI 系统
- 基于自然语言的 AI 系统
- 基于生成对抗网络(GAN)的应用
- 专家系统
- 视频到文本翻译应用
- 基于 AI 的推荐系统
- 基于 AI 的移动应用
- 基于 AI 的聊天机器人
- 强化学习应用
在本章中,我们将简要介绍实现机器学习和深度学习的概念,这些概念是实现以下各章中涉及的项目所必需的。
神经网络
神经网络是受人脑启发的机器学习模型。 它们由神经处理单元组成,它们以分层的方式相互连接。 这些神经处理单元称为人工神经元,它们在人脑中执行与轴突相同的功能。 在人脑中,树突接收来自邻近神经元的输入,并在将输入传输到神经元的体细胞之前减弱或放大输入。 在神经元的躯体中,这些修饰的信号被加在一起并传递到神经元的轴突。 如果轴突的输入超过指定的阈值,则信号将传递到相邻神经元的树突。
人工神经元松散地工作可能与生物神经元在相同的逻辑上起作用。 它接收来自邻近神经元的输入。 通过神经元的输入连接来缩放输入,然后将它们加在一起。 最后,求和的输入通过激活函数传递,该函数的输出传递到下一层的神经元。
下图说明了生物神经元和人工神经元,以进行比较:
图 1.2:生物神经元
下图说明了人工神经元:
图 1.3:人工神经元
现在,让我们看一下人工神经网络的结构,如下图所示:
图 1.4:人工神经网络
输入x ∈ R^N
穿过神经单元的连续层,这些层以分层方式排列。 特定层中的每个神经元都接收来自先前层的神经元的输入,这些输入被它们之间的连接权重所衰减或放大。 权重w[ij]^(l)
对应于l
层第i
个神经元与l + 1
层第j
个神经元。 同样,每个神经元单元i
在特定层1
中都有一个偏置b[i]^(l)
。 神经网络为输入向量x ∈ R^N
预测输出y_hat
。 如果数据的实际标签是y
,其中y
取连续值,则神经元网络将预测误差最小化(y - y_hat)^2
来学习权重和偏差。 当然,对于所有标记的数据点,必须将误差最小化:(xi, yi), i ∈ 1, 2, ..., m
。
如果我们用一个公共向量W
表示一组权重和偏差,并且预测中的总误差由C
表示,则在训练过程中,估计的W
可以表示为:
同样,预测输出y_hat
可以由输入x
的函数表示,并由权重向量W
进行参数化,如下所示:
这种用于预测输出连续值的公式称为回归问题。
对于两类二分类,交叉熵损失最小,而不是平方误差损失,并且网络输出正类的概率而不是输出。 交叉熵损失可以表示为:
此处,p[i]
是给定输入x
的输出类别的预测概率,并且可以表示为输入x
的函数。由权重向量参数化,如下所示:
通常,对于多类别分类问题(例如n
类),交叉熵损失可通过以下方式给出:
在这里,y[i]^(j)
是第i
个数据点的第j
类的输出标签。
神经激活单元
取决于架构和当前的问题,在神经网络中使用了几种神经激活单元。 我们将讨论最常用的激活函数,因为它们在确定网络架构和表现方面起着重要作用。 线性和 Sigmoid 单元激活函数主要用于人工神经网络,直到 Hinton 等人发明的整流线性单元(ReLUs)彻底改变了神经网络的表现。
线性激活单元
线性激活单元将总输入输出到衰减的神经元,如下图所示:
图 1.5:线性神经元
如果x
是线性激活单元的总输入,则输出y
可以表示如下:
Sigmoid 激活单元
Sigmoid 激活单元,y
的输出是其总输入x
的函数,表示如下:
由于 Sigmoid 激活单元响应是非线性函数,如下图所示,它可用于在神经网络中引入非线性:
图 1.6:Sigmoid 激活函数
自然界中任何复杂的过程通常在输入输出关系上都是非线性的,因此,我们需要非线性激活函数通过神经网络对其进行建模。 两类分类的神经网络的输出概率通常由 Sigmoid 神经单元的输出给定,因为它的输出值从零到一。 输出概率可以表示如下:
在这里,x
代表输出层中 Sigmoid 单元的总输入量。
双曲正切激活函数
给出了双曲正切激活函数(tanh)的输出y
作为其总输入的函数x
如下:
tanh 激活函数输出的值在[-1, 1]
范围内,如下图所示:
图 1.7:Tanh 激活函数
需要注意的一件事是 Sigmoid 和 tanh 激活函数在很小的输入范围内都是线性的,超过该范围输出就会饱和。 在饱和区,激活函数(相对于输入)的梯度非常小或接近零; 这意味着它们非常容易消失梯度问题。 正如您将在后面看到的那样,神经网络将从反向传播方法中学习,在该方法中,层的梯度取决于后续层中直至最终输出层中激活单元的梯度。 因此,如果激活单元中的单元在饱和区域中工作,则将更少的误差反向传播到神经网络的早期层。 神经网络通过利用梯度来最小化预测误差,以学习权重和偏差(W
)。 这意味着,如果梯度很小或消失为零,则神经网络将无法正确学习这些权重。
整流线性单元(ReLU)
当神经元的总输入大于零时,ReLU 的输出为线性,而当神经元的总输入为负时,ReLU 的输出为零。 这个简单的激活函数为神经网络提供了非线性,同时,它相对于总输入提供了一个恒定的梯度。 这个恒定的梯度有助于防止神经网络出现饱和或消失的梯度问题,如激活函数(如 Sigmoid 和 tanh 激活单元)所示。 ReLU 函数输出(如图“图 1.8”所示)可以表示如下:
ReLU 激活函数可以绘制如下:
图 1.8:ReLU 激活函数
ReLU 的限制条件之一是其输入负值的零梯度。 这可能会减慢训练速度,尤其是在初始阶段。 LReLU 激活函数(如图 1.9 所示)在这种情况下非常有用,即使输出和梯度不为零,即使输入为负值。 ReLU 输出函数泄漏可以表示如下:
将为 LReLU 激活函数提供α
参数,而对于参数 ReLU,α
是神经网络将通过训练学习的参数。 下图显示了 LReLU 激活函数的输出:
图 1.9:LReLU 激活函数
softmax 激活单元
在多类别分类问题的情况下, softmax 激活单元通常用于输出类别概率。 假设我们正在处理n
类分类问题,并且与这些类相对应的总输入如下:
在这种情况下,softmax 激活单元的第k
类的输出概率由以下公式给出:
还有其他几种激活函数,大多数是这些基本版本的变体。 我们将在以下各章介绍的不同项目中讨论它们时,对它们进行讨论。
训练神经网络的反向传播方法
在反向传播方法中,神经网络通过梯度下降技术进行训练,其中组合权重向量W
迭代更新,如下所示:
这里,η
是学习率, W^(t + 1)
和W^(t)
分别是迭代t + 1
和t
时的权重向量,∇C(W^(t))
是迭代t
时的成本函数或误差函数对于权重向量W
的梯度。 先前由w ∈ W
概括的单个权重或偏差的算法可以表示为:
从前面的表达式中可以得出,梯度下降学习方法的核心在于针对每个权重计算成本函数或误差函数的梯度。
从微分的链式规则中,我们知道如果我们有 y = f(x), z = f(y)
,那么以下是正确的:
该表达式可以推广为任意数量的变量。 现在,让我们看一个非常简单的神经网络,如下图所示,以了解反向传播算法:
图 1.10:说明反向传播的网络
令网络输入为二维向量, x = [x1, x2]^T
, 输出标签和预测分别为 和 。 另外,我们假设神经网络中的所有激活单元都是 Sigmoid。 让广义权重将层l-1
中的任何单元i
连接到层l
中的单元j
表示为 ,而l
层中任何单元i
的偏置应表示为 。 让我们得出一个数据点的梯度; 总梯度可以计算为训练(或小批量)中使用的所有数据点的总和。 如果输出是连续的,则可以选择损失函数C
作为预测误差的平方:
可以通过将相对于W
向量的成本函数最小化来确定由集合W
表示的网络的权重和偏差,如下所示:
为了通过梯度下降迭代地执行成本函数的最小化,我们需要针对每个权重计算成本函数的梯度w ∈ W
,如下所示:
现在我们有了所需的一切,让我们计算成本函数C
相对于权重 的梯度。 使用差分的链式规则,我们得到以下信息:
现在让我们看下面的公式:
正如您在前面的表达式中看到的那样,导数不过是预测中的误差。 通常,在存在回归问题的情况下,输出单元激活函数是线性的,因此适用以下表达式:
因此,如果我们要计算成本函数相对于输出单元总输入的梯度,则为∂C/∂s[1]^(3)
。 这仍然等于输出预测中的误差。
根据输入权重和激活,输出单元上的总输入可以表示为:
这意味着∂s[1]^(3)/∂w[21]^(3) = z[2]^(3)
以及成本函数相对于权重w[21]^(3)
的导数通过以下方式得出:
如您所见,相对于最终输出层之前的层中的权重,该误差在计算成本函数的梯度时反向传播。 当我们计算成本函数相对于广义权重的梯度w[jk]^(2)
时,这变得更加明显。 取对应于j = 1
和k = 2
的权重; 即w[jk]^(2)
。 成本函数C
相对于该权重的梯度可以表示为:
现在,∂s[2]^(2)/∂w[12]^(2) = z[1]^(2)
表示:
因此,一旦我们确定了成本函数相对于神经元总输入的梯度为∂C/∂s
,则任何权重的梯度w
贡献了总输入量s
,可以通过简单地乘以与权重相关的激活z
来获得。
现在,成本函数相对于总输入的梯度s[2]^(2)
可以再次通过链式法则得出,如下所示:
由于神经网络的所有单元(输出单元除外)均为 Sigmoid 激活函数,因此情况如下:
结合(1), (2), (3)
,我们得到以下信息:
在前面的派生梯度表达式中,您可以看到预测误差(y_hat - y)
通过与相关的激活和权重(根据微分链规则)组合以计算每一层权重的梯度而向后传播 ,因此是 AI 术语中的反向传播名称。
卷积神经网络
卷积神经网络(CNN)利用卷积运算从具有关联拓扑的数据中提取有用信息。 这最适合图像和音频数据。 输入图像在通过卷积层时会生成多个输出图像,称为输出特征映射。 输出特征映射将检测特征。 初始卷积层中的输出特征映射可以学习检测基本特征,例如边缘和颜色成分变化。
第二卷积层可以检测到稍微复杂的特征,例如正方形,圆形和其他几何结构。 随着神经网络的发展,卷积层学会了检测越来越复杂的特征。 例如,如果我们有一个 CNN 可以对图像是猫还是狗进行分类,则神经网络底部的卷积层可能会学会检测诸如头部,腿部等特征。
“图 1.11”显示了 CNN 的架构图,该 CNN 处理猫和狗的图像以对其进行分类。 图像通过卷积层,该卷积层有助于检测相关特征,例如边缘和颜色组合。 ReLU 激活会增加非线性。 激活层之后的合并层汇总本地邻居信息,以提供一定数量的平移不变性。 在理想的 CNN 中,此卷积激活池操作在网络进入密集连接之前执行了几次:
图 1.11:CNN 架构
当我们经过具有多个卷积激活池操作的网络时,图像的空间分辨率会降低,而输出特征映射的数量在每一层中都会增加。 卷积层中的每个输出特征映射都与过滤器核相关联,该过滤器核的权重是通过 CNN 训练过程学习的。
在卷积操作中,将过滤器核的翻转版本放置在整个图像或特征映射上,并为过滤器上每个位置计算过滤器核输入值与相应图像像素或特征映射值的点积。 输入图像或特征映射。 已经习惯了普通图像处理的读者可能已经使用了不同的过滤器核,例如高斯过滤器,Sobel 边缘检测过滤器等,其中许多过滤器的权重已预定义。 卷积神经网络的优点是通过训练过程确定不同的过滤器权重。 这意味着,针对卷积神经网络正在处理的问题,可以更好地定制过滤器。
当卷积运算涉及在输入的每个位置上覆盖过滤器核时,该卷积被称为跨度为 1。 如果我们选择在覆盖过滤器核时跳过一个位置,那么卷积将以两个步幅执行。 通常,如果将n
位置跳过而将过滤器核覆盖在输入上,则表示卷积以n + 1
的步幅执行。 大于 1 的步幅会减小卷积输出的空间大小。
通常,卷积层之后是池化层,池化层基本上总结了由池化的接收场确定的邻域中的输出特征映射激活。 例如,一个2 x 2
的接收场将收集四个相邻的输出特征映射激活的本地信息。 对于最大池操作,将选择四个激活的最大值作为输出,而对于平均池化,将选择四个激活的平均值。 合并降低了特征映射的空间分辨率。 例如,对于具有2 x 2
接收场的224 x 224
大小的特征映射池化操作,特征映射的空间大小将减小为112 x 112
。
要注意的一件事是,卷积运算减少了每层要学习的权重数。 例如,如果我们有一个空间大小为224 x 224
的输入图像,而下一层的期望输出为大小为224 x 224
的大小,那么对于具有完整连接的传统神经网络来说,要学习的权重数是224 x 224 x 224 x 224
。对于具有相同输入和输出大小的卷积层,我们需要学习的只是过滤器核的权重。 因此,如果我们使用3 x 3
过滤器核,我们只需要学习 9 个权重即可,而不是224 x 224 x 224 x 224
权重。 这种简化是有效的,因为局部空间邻域中的图像和音频之类的结构之间具有高度相关性。
输入图像经过多层卷积和池化操作。 随着网络的发展,特征映射的数量增加,而图像的空间分辨率降低。 在卷积池层的末端,特征映射的输出被馈送到全连接层,然后是输出层。
输出单元取决于手头的任务。 如果执行回归,则输出激活单元是线性的,而如果是二分类问题,则输出单元是 Sigmoid 的。 对于多类别分类,输出层是 softmax 单元。
在本书的所有图像处理项目中,我们都将使用一种或另一种形式的卷积神经网络。
循环神经网络(RNN)
循环神经网络(RNN)在处理顺序或时间数据时非常有用,其中给定实例或位置的数据与先前时间步长或位置中的数据高度相关。 RNN 在处理文本数据方面已经非常成功,因为给定实例中的单词与它前面的单词高度相关。 在 RNN 中,网络在每个时间步执行相同的函数,因此名称中的术语为循环。 下图说明了 RNN 的架构:
图 1.12:RNN 架构
在每个给定的时间步长t
处,计算记忆状态h[t]
,基于步骤t-1
处的之前的状态h[t-1]
,以及时间步长t
处的输入x[t]
。 新状态h[t]
用于在步骤t
处预测输出o[t]
。 控制 RNN 的方程式如下:
如果我们要预测句子中的下一个单词,则函数f[2]
通常是词汇表中单词的 softmax 函数。 根据当前问题,f[1]
函数可以是任何激活函数。
在 RNN 中,步骤t
中的输出误差会尝试纠正先前时间步中的预测,并通过k ∈ 1, 2, ..., t-1
来概括。 通过传播先前时间步长中的误差来实现。 这有助于 RNN 了解彼此相距较远的单词之间的长期依赖性。 实际上,由于梯度问题的消失和爆炸,并非总是可能通过 RNN 学习这么长的依赖关系。
如您所知,神经网络通过梯度下降来学习,并且可以通过以下步骤来学习单词在时间步t
与在先序列步k
之间的关系。 记忆状态h[t]^(i)
相对于记忆状态h[i]^(t)
的梯度。 用以下公式表示:
如果从序列步骤k
的存储状态h[k]^(i)
到序列步骤k + 1
的存储状态h[k + 1]^(i)
的权重连接由*给出u[ii] ∈ W[hh]
,则以下是正确的:
在前面的等式中,s[k + 1]^(i)
是在时间步k + 1
时存储状态i
的总输入,因此情况如下:
既然我们已经准备就绪,那么就很容易理解为什么 RNN 中可能会出现消失的梯度问题。 从前面的等式(3))
和(4)
得到以下结果:
对于 RNN,函数f[2]
通常为 Sigmoid 或 tanh,其饱受饱和度的困扰,即具有超出指定输入值范围的低梯度。 现在,由于f[2]
的导数彼此相乘,因此,如果激活函数的输入在饱和区工作,则∂h[t]^(i)/∂h[k]^(i)
的斜率可以变为零,即使相对tk
的中等值。 即使f[2]
函数在饱和区中不起作用,但 Sigmoids 的f[2]
函数的梯度始终比1
小,因此很难学习序列中单词之间的远距离依存关系。 同样,可能会由于u[ii]^(t - k)
因子而出现爆炸性梯度问题。 假设步t
和k
之间的距离约为10
,而权重u[ii]
,大约两点。 在这种情况下,梯度将被放大两倍,即2 ^ 10 = 1024
,从而导致爆炸梯度问题。
长短期记忆(LSTM)单元
消失的梯度问题在很大程度上由 RNN 的改进版本(称为长短期记忆(LSTM)单元)解决。 长短期存储单元的架构图如下:
图 1.13:LSTM 架构
除了记忆状态h[t]
之外,LSTM 还介绍了 RNN 单元状态C[t]
。 单元状态由三个门控制:遗忘门,更新门和输出门。 遗忘门确定从先前的单元状态C[t-1]
保留多少信息,其输出表示如下:
更新门的输出表示如下:
潜在的新候选单元状态C_tilde[t]
表示如下:
基于先前的电池状态和当前的潜在电池状态,通过以下方式提供更新的电池状态输出:
并非单元状态的所有信息都传递到下一步,并且应由输出门确定应释放多少单元状态到下一步。 输出门的输出通过以下方式给出:
根据当前单元状态和输出门,通过以下方式给出传递给下一步的更新后的内存状态:
现在出现了一个大问题:LSTM 如何避免消失的梯度问题? LSTM 中∂h[t]^(i)/∂h[k]^(i)
的等效项由∂C[t]^(i)/∂C[k]^(i)
给出,可以用以下产品形式表示:
现在,单元状态单元的循环由以下给出:
由此,我们得到以下内容:
结果,梯度表达式∂C[t]^(i)/∂C[k]^(i)
变为以下:
如您所见,如果我们可以将遗忘单元格状态保持在一个附近,则梯度将几乎不衰减地流动,并且 LSTM 不会遭受梯度消失的困扰。
我们将在本书中看到的大多数文本处理应用将使用 RSTM 的 LSTM 版本。
生成对抗网络
生成对抗网络,通常称为 GAN ,是通过生成器G
学习特定概率分布的生成模型。 生成器G
与判别器D
进行零和极小极大游戏,并且两者都会随着时间的流逝而逐渐达到纳什均衡。 生成器尝试生成类似于给定概率分布P(x)
生成的样本,而判别器D
尝试区分生成器生成的那些假数据样本。G
来自原始分布的数据样本。 生成器G
尝试通过转换样本z
来生成与P(x)
相似的样本。 噪声分布P(z)
。 判别器D
在假冒时学会将生成器G
生成的样本标记为G(z)
;x
原始时属于P(x)
。 在 minimax 游戏的平衡状态下,生成器将学习生成与原始分布P(x)
相似的样本,因此以下是正确的:
下图说明了学习 MNIST 数字的概率分布的 GAN 网络:
Figure 1.14: GAN architecture
判别器最小化的成本函数是二进制交叉熵,用于区分生成器生成的假数据,和属于概率分布P(x)
的真实数据点z
:
生成器将尝试最大化由(1)
给出的相同成本函数。 这意味着,优化问题可以表示为具有效用函数 U(G, D)
的 minimax 玩家,如下所示:
通常,要测量给定概率分布与给定分布的匹配程度,请使用f
-发散度量,例如 Kullback-Leibler(KL)散度,詹森·香农散度和 Bhattacharyya 距离。 例如,以下给出两个概率分布P
和Q
之间的 KL 散度,其中对分布的期望是P
:
类似地,P
和Q
之间的詹森香农散度给出如下:
现在,回到(2)
,表达式可以编写如下:
在这里,G(x)
是生成器的概率分布。 将期望扩展到其不可或缺的形式,我们得到以下内容:
对于固定的生成器分配,如果满足以下条件,则G(x)
对于判别器的效用函数将最小。
用(5)
替换为(3)
中的D(x)
,我们得到以下信息:
现在,生成器的任务是最大化工具V(G, D_hat)
或最小化工具-V(G, D_hat)
。 -V(G, D_hat)
的表达式可以重新安排如下:
因此,我们可以看到生成器最小化-V(G, D_hat)
等于最小化实际分布P(x)
与生成器生成的样本分布之间的 Jensen Shannon 散度G
(即G(x)
)。
训练 GAN 并不是一个简单的过程,在训练这样的网络时我们需要考虑几个技术方面的考虑。 我们将使用高级 GAN 网络在第 4 章“使用 GANs 的时装行业中的风格迁移”中构建跨域风格迁移应用。
强化学习
强化学习是机器学习的一个分支,它使机器和/或智能体可以通过采取特定行动在特定上下文中最大化某种形式的奖励。 强化学习不同于监督学习和无监督学习。 强化学习广泛用于博弈论,控制系统,机器人技术和其他新兴的人工智能领域。 下图说明了强化学习问题中智能体与环境之间的交互:
图 1.15:强化学习模型中的智能体与环境交互
Q 学习
现在,我们将研究一种流行的强化学习算法,称为 Q 学习。 Q 学习用于确定给定的有限马尔可夫决策过程的最佳动作选择策略。 马尔可夫决策过程由状态空间S
; 一个动作空间A
; 立即奖励集R
; 给定当前状态s[t]
的下一个状态的概率S[t + 1]
; 当前动作a[t]
; P(S[t+1]/S[t];r[t])
; 和折扣系数γ
定义。 下图说明了马尔可夫决策过程,其中下一个状态取决于当前状态以及在当前状态下执行的任何操作:
图 1.16:马尔可夫决策过程
假设我们有一系列状态,动作和相应的奖励,如下所示:
如果我们考虑长期奖励R[t]
,则在步骤t
处,它等于从t
开始的每一步直到最后的立即奖励总和,如下所示:
现在,马尔可夫决策过程是一个随机过程,无法每次基于S[t]
和a[t]
进行相同的下一步S[t + 1]
; 因此,我们对未来的奖励应用了折扣系数γ
。 这意味着长期奖励可以更好地表示为:
由于在时间步t
上已经实现了即时奖励,为了最大化长期奖励,我们需要最大化时间步t + 1
的长期奖励(即R[t + 1]
),方法是选择最佳操作。 通过采取行动a[t]
的状态S[t]
所期望的最大长期回报,由以下 Q 函数表示:
在每个状态s ∈ S
,Q 学习中的主体尝试采取行动α ∈ A
,以最大化其长期回报。 Q 学习算法是一个迭代过程,其更新规则如下:
如您所见,该算法受(1)
中表达的长期奖励概念的启发。
处于状态s[t]
的采取行动a[t]
的总累积奖励Q(s[t], a[t])
取决于即时奖励r[t]
以及在新步骤s[t+1]
处的,我们希望的最大长期回报。 在马尔可夫决策过程中,新状态s[t + 1]
随机依赖于当前状态,即s[t]
,然后通过P(S[t+1]/S[t];r[t])
形式的概率密度/质量函数选取的动作a[t]
。
该算法通过根据α
的值对旧期望值和新长期奖励值进行加权平均,来不断更新期望长期累积奖励。
通过迭代算法构建了Q(s, a)
函数后,在基于给定状态s
进行游戏时,我们可以采取最佳措施a_hat
, 作为最大化 Q 函数的策略:
深度 Q 学习
在 Q 学习中,我们通常会处理一组有限的状态和动作。 这意味着,表格足以容纳 Q 值和奖励。 但是,在实际应用中,状态和适用动作的数量大多是无限的,并且需要更好的 Q 函数近似器来表示和学习 Q 函数。 深度神经网络是通用函数近似器,因此在这里就应运而生。 我们可以用神经网络表示 Q 函数,该神经网络将状态和动作作为输入并提供相应的 Q 值作为输出。 或者,我们可以只使用状态来训练神经网络,然后将输出作为与所有动作相对应的 Q 值。 下图说明了这两种情况。 由于 Q 值是奖励,因此我们在以下网络中处理回归:
图 1.17:深度 Q 学习函数近似器网络
在本书中,我们将使用强化学习来训练赛车,以通过深度 Q 学习自行驾驶。
迁移学习
通常,迁移学习是指使用在一个领域中获得的知识来解决另一领域中的相关问题的概念。 但是,在深度学习中,它专门指的是将针对特定任务训练的神经网络重新用于不同领域中的相似任务的过程。 新任务使用从先前任务中学到的特征检测器,因此我们不必训练模型就可以学习它们。
由于不同层之间的连接模式的性质,深度学习模型倾向于具有大量参数。 要训练这么大的模型,需要大量的数据; 否则,模型可能会过拟合。 对于许多需要深度学习解决方案的问题,将无法获得大量数据。 例如,在用于对象识别的图像处理中,深度学习模型提供了最新的解决方案。 在这种情况下,可以基于从现有的经过训练的深度学习模型中学习到的特征检测器,使用迁移学习来创建特征。 然后,这些特征可用于使用可用数据构建简单模型,以解决当前的新问题。 因此,新模型需要学习的唯一参数是与构建简单模型有关的参数,从而减少了过拟合的机会。 通常在大量数据上训练预训练的模型,因此,它们具有作为特征检测器的可靠参数。
当我们在 CNN 中处理图像时,初始层会学会检测非常通用的特征,例如卷曲,边缘,颜色组成等。 随着网络的深入发展,更深层次的卷积层将学会检测与特定种类的数据集相关的更复杂特征。 我们可以使用预训练的网络,并选择不训练前几层,因为它们会学习非常通用的函数。 相反,我们可以只专注于训练最后几层的参数,因为它们将学习针对当前问题的复杂函数。 这样可以确保我们需要训练的参数较少,并且可以明智地使用数据,仅训练所需的复杂参数,而不训练通用特征。
迁移学习已广泛应用于通过 CNN 进行图像处理的过程,其中过滤器充当特征检测器。 用于迁移学习的最常见的预训练 CNN 是AlexNet
,VGG16
,VGG19
,Inception V3
和ResNet
等。 下图说明了用于迁移学习的预训练VGG16
网络:
图 1.18:使用预训练的 VGG 16 网络进行迁移学习
以x
表示的输入图像被馈送到预训练的 VGG 16 网络,以及4096
维输出特征向量x'
,是从最后一个全连接层中提取的。 提取的特征x'
以及相应的类别标签y
用于训练简单的分类网络,从而减少解决问题所需的数据。
我们将通过使用第 2 章,“迁移学习”中的迁移学习来解决医疗保健领域中的图像分类问题。
受限玻尔兹曼机
受限玻尔兹曼机(RBM)是一门无监督的机器学习算法,用于学习数据的内部表示。 RBM 具有可见层v ∈ R^m
,以及隐藏层h ∈ R^n
。 RBM 学习在可见层中将输入呈现为隐藏层中的低维表示。 给定可见层输入,所有隐藏层单元在条件上都是独立的。 类似地,给定隐藏层输入,所有可见层在条件上都是独立的。 给定隐藏层输入,这使得 RBM 可以独立地对可见单元的输出进行采样,反之亦然。
下图说明了 RBM 的架构:
图 1.19:受限玻尔兹曼机
权重w[ij] ∈ W
将可见单元i
连接到隐藏单元j
,其中W ∈ R^(mxn)
是所有这些权重的集合,从可见单元到隐藏单元。 可见单元的偏差由b[i] ∈ b
表示,而隐藏单元的偏差由c[j] ∈ c
表示。
受统计物理学中玻耳兹曼分布的思想启发,可见层向量v
和隐藏层向量h
的联合分布正比于配置的负能量的指数:
(1)
配置的能量由以下给出:
(2)
给定可见输入向量v
的隐藏单元j
的概率可以表示为:
(2)
类似地,给出隐藏输入向量h
的可见单元i
的概率由以下公式给出:
(3)
因此,一旦我们通过训练了解了 RBM 的权重和偏差,就可以在给定隐藏状态的情况下对可见表示进行采样,而在给定可见状态的情况下可以对隐藏状态进行采样。
类似于主成分分析(PCA),RBM 是一种方法,将一个维度(由可见层v
提供)中的数据表示为不同的维度(由隐藏层h
提供)。 当隐藏层的大小小于可见层的大小时,RBM 执行减小大小的任务。 RBM 通常在二进制数据上训练。
通过最大化训练数据的可能性来训练 RBM。 在成本函数相对于权重和偏差的梯度下降的每次迭代中,都会出现采样,这会使训练过程变得昂贵并且在计算上有些棘手。 一种名为对比发散的聪明采样方法(使用吉布斯采样)用于训练 RBM。
在第 6 章,“智能推荐系统”中,我们将使用 RBM 构建推荐系统。
自编码器
与 RBM 十分相似,自编码器是一类无监督的学习算法,旨在发现数据中的隐藏结构。 在主成分分析(PCA)中,我们尝试捕获输入变量之间的线性关系,并尝试通过(输入变量的)线性组合来在降维空间中表示数据,这说明了数据的大部分差异。 但是,PCA 无法捕获输入变量之间的非线性关系。
自编码器是一种神经网络,可以捕获输入变量之间的非线性相互作用,同时在隐藏层中以不同维度表示输入。 在大多数情况下,隐藏层的大小小于输入的大小。 假设存在高维数据固有的低维结构,我们跳过了这一点。 例如,高维图像可以由低维流形表示,并且自编码器通常用于发现该结构。 下图说明了自编码器的神经架构:
图 1.20:自编码器架构
自编码器有两个部分:编码器和解码器。 编码器尝试将输入数据x
投影到隐藏层h
中。 解码器尝试从隐藏层h
重构输入。 通过最小化重构误差,即来自解码器和原始输入的重构输入x_tilde
之间的误差,可以训练伴随此类网络的权重。 如果输入是连续的,则最小化重构误差的平方和,以学习自编码器的权重。
如果我们用函数f[W](x)
表示编码器,而解码器则用f[U](x)
表示,其中W
和U
是与编码器和解码器关联的权重矩阵,那么情况如下:
(1)
(2)
训练集上的重构误差C
,x[i], i ∈ 1, 2, 3, ..., m
可以表示如下 :
(3)
通过最小化(3)
的成本函数,可以学习自编码器的最佳权重W_hat, U_hat
,如下所示:
(4)
自编码器用于多种目的,例如学习数据的潜在表示,降噪和特征检测。 降噪自编码器将实际输入的噪声版本作为其输入。 他们尝试构建实际的输入,以作为重建的标签。 类似地,自编码器可以用作生成模型。 可以用作生成模型的一类这样的自编码器称为变分自编码器。 当前,变分自编码器和 GAN 作为图像处理的生成模型非常受欢迎。
总结
现在,我们到了本章的结尾。 我们已经研究了人工神经网络的几种变体,包括用于图像处理目的的 CNN 和用于自然语言处理目的的 RNN。 此外,我们将 RBM 和 GAN 视为生成模型,将自编码器视为无监督方法,可以解决许多问题,例如降噪或解密数据的内部结构。 此外,我们还谈到了强化学习,这对机器人技术和 AI 产生了重大影响。
您现在应该熟悉本书其余各章中构建智能 AI 应用时将要使用的核心技术。 在构建应用时,我们将在需要时进行一些技术上的改动。 建议不熟悉深度学习的读者探索更多有关本章涉及的核心技术的信息,以便更全面地理解。
在随后的章节中,我们将讨论实用的 AI 项目,并使用本章中讨论的技术来实现它们。 在第 2 章,“迁移学习”中,我们将从使用迁移学习实现医疗保健应用进行医学图像分析开始。 我们希望您期待您的参与。
二、迁移学习
迁移学习是将特定领域中一项任务中获得的知识迁移到相似领域中相关任务中的过程。 在深度学习范例中,迁移学习通常是指将预训练模型的重用作为另一个问题的起点。 计算机视觉和自然语言处理中的问题需要大量数据和计算资源,才能训练出有意义的深度学习模型。 迁移学习在视觉和文本领域已变得非常重要,因为它减轻了对大量训练数据和训练时间的需求。 在本章中,我们将使用迁移学习来解决医疗保健问题。
我们将在本章中涉及的与迁移学习有关的一些关键主题如下:
- 使用迁移学习来检测人眼中的糖尿病性视网膜病变状况,并确定视网膜病变的严重程度
- 探索可用于训练可检测人眼眼底图像中的糖尿病性视网膜病变的卷积神经网络(CNN)的高级预训练卷积神经架构
- 查看实际实现 CNN 所需的不同图像预处理步骤
- 学习制定适合当前问题的成本函数
- 定义适当的度量标准来衡量训练模型的表现
- 使用仿射变换生成其他数据
- 与适当的学习率,优化器的选择等有关的训练错综复杂
- 查看端到端 Python 实现
技术要求
您将需要具备 Python 3,TensorFlow,Keras 和 OpenCV 的基础知识。
迁移学习简介
在传统的机器学习范例中(请参见“图 2.1”),每个用例或任务都是根据手头的数据独立建模的。 在迁移学习中,我们使用从特定任务中获得的知识(以架构和模型参数的形式)来解决不同(但相关)的任务,如下图所示:
图 2.1:传统机器学习与迁移学习
吴安德(Andrew Ng)在其 2016 年 NIPS 教程中表示,迁移学习将成为机器学习商业成功的第二大推动力(在监督学习之后); 这一说法日新月异。 迁移学习现已广泛用于需要使用人工神经网络解决的问题中。 因此,最大的问题是为什么会这样。
从头开始训练人工神经网络是一项艰巨的任务,主要是由于以下两个原因:
- 人工神经网络的成本面是非凸的; 因此,它需要一组良好的初始权重才能实现合理的收敛。
- 人工神经网络具有很多参数,因此,它们需要大量数据进行训练。 不幸的是,对于许多项目而言,可用于训练神经网络的特定数据不足,而该项目旨在解决的问题非常复杂,需要神经网络解决方案。
在这两种情况下,迁移学习都可以解决。 如果我们使用在大量标记数据上训练的预训练模型,例如 ImageNet 或 CIFAR,则涉及迁移学习的问题将具有很好的初始权重来开始训练; 然后可以根据现有数据对这些权重进行微调。 同样,为了避免在较少的数据量上训练复杂的模型,我们可能希望从预训练的神经网络中提取复杂的特征,然后使用这些特征来训练相对简单的模型,例如 SVM 或逻辑回归模型。 举个例子,如果我们正在研究图像分类问题,并且已经有一个预训练的模型(例如,基于 1,000 个 ImageNet 的VGG16
网络),我们可以通过VGG16
的权重迁移训练数据,并从最后一个池化层提取特征。 如果我们有m
个训练数据点,则可以使用等式(xi, yi), i = 1 -> m
,其中x
是特征向量,y
是输出类。 然后,我们可以从预训练的VGG16
网络中得出复杂的特征,例如向量h
,如下所示:
此处W
是经过预训练的VGG16
网络的权重集,直到最后一个池化层。
然后我们可以使用经过转换的训练数据点集(hi, yi), i = 1 -> m
来构建相对简单的模型。
检测糖尿病性视网膜病变的迁移学习
在本章中,我们将使用迁移学习来构建一个模型,以检测人眼中的糖尿病性视网膜病变。 糖尿病性视网膜病通常在糖尿病患者中发现,其中高血糖水平导致视网膜血管的损害。 下图左侧为正常视网膜,右侧为糖尿病性视网膜病变:
图 2.2:正常人视网膜与患有糖尿病性视网膜病的视网膜
在医疗保健中,糖尿病性视网膜病变的检测通常是一个手动过程,需要受过训练的医师检查彩色眼底视网膜图像。 这导致诊断过程的延迟,通常导致延迟的治疗。 作为我们项目的一部分,我们将建立一个强大的人工智能系统,该系统可以拍摄视网膜的彩色眼底图像,并对糖尿病性视网膜病变的视网膜状况进行严重性分类。 我们将视网膜图像分类的不同条件如下:
*0
:无糖尿病性视网膜病变
*1
:轻度糖尿病性视网膜病变
*2
:中度糖尿病性视网膜病变
*3
:严重的糖尿病视网膜病变
*4
:糖尿病性视网膜增生病
糖尿病视网膜病变数据集
用于构建糖尿病性视网膜病变检测应用的数据集可从 Kaggle 获得,可从以下链接下载。
训练和保留测试数据集都存在于train_dataset.zip
文件中,该文件可在前面的链接中找到。
我们将使用标记的训练数据通过交叉验证来构建模型。 我们将在保持数据集上评估模型。
由于我们正在处理类别预测,因此准确率将是有用的验证指标。 精度定义如下:
此处,c
是正确分类的样本数,N
是评估样本的总数。
与 Kaggle 标准相比,我们还将使用二次加权 kappa 统计信息确定模型的质量,并确定模型的表现基准。 二次加权 kappa 定义如下:
二次加权 kappa 表达式中的权重w[ij]
如下:
在上式中,以下内容适用:
N
代表类别数O[ij]
表示已被预测为i
类的图像数量,其中图像的预测类为i
而实际类别为j
E[ij]
表示预测类别为i
的预期观测个数,预测类为i
而实际类别为j
,假设预测类和实际类是独立的
为了更好地了解 kappa 指标组件,让我们看一下苹果和橘子的二分类。 假设预测类和实际类的混淆矩阵如下图所示:
图 2.3:Kappa 指标项
真实标签为橙子,假设标签之间具有独立性时,预测苹果的预期计数由以下公式给出:
鉴于没有模型,此预期计数是您可能犯的最严重的错误。
如果您熟悉两个类别变量之间的独立性卡方检验,则假设类别变量之间具有独立性,则列联表中每个单元格中的预期计数都是基于相同的公式计算的。
可以从混淆矩阵中直接追踪模型的观测计数,该模型预测的真实标签为橙子时,预测的苹果等于5
,如下所示:
因此,我们可以看到模型在预测橙子为苹果时所产生的误差小于我们不使用模型时所获得的误差。 与没有模型的预测相比,Kappa 通常衡量我们的表现如何。
如果我们观察二次权重的表达式w[ij]
,我们可以看到,当实际标签与预测标签之间的差异较大时,权重的值会更大。 由于类的序数性质,这是有道理的。 例如,让我们用类别标签 0 表示完美状态的眼睛; 患有轻度糖尿病性视网膜病的患者; 中度糖尿病视网膜病变伴两个; 并伴有严重的糖尿病性视网膜病变。 当轻度糖尿病性视网膜病被错误地分类为严重糖尿病性视网膜病而不是中度糖尿病性视网膜病时,这个二次项权重w[ij]
将会更高。 这是有道理的,因为即使我们没有设法预测实际类别,我们也希望预测一个尽可能接近实际状况的状况。
我们将使用sklearn.metrics.cohen_kappa_score
和weights= "quadratic"
来计算 kappa 得分。 权重越高,kappa 得分越低。
定义损失函数
该用例的数据分为五类,分别与无糖尿病性视网膜病变,轻度糖尿病性视网膜病变,中度糖尿病性视网膜病变,严重糖尿病性视网膜病变和增生性糖尿病性视网膜病变有关。 因此,我们可以将其视为分类问题。 对于我们的分类问题,需要对输出标签进行一次热编码,如下所示:
- 无糖尿病性视网膜病变:
[1 0 0 0 0]^T
- 轻度糖尿病性视网膜病变:
[0 1 0 0 0]^T
- 中度糖尿病性视网膜病变:
[0 0 1 0 0]^T
- 严重糖尿病性视网膜病变:
[0 0 0 1 0]^T
- 增生糖尿病性视网膜病变:
[0 0 0 0 1]^T
Softmax 将是表示输出层中不同类别的概率的最佳激活函数,而每个数据点的分类交叉熵损失之和将是优化的最佳损失。 对于具有输出标签向量y
和p
的预测概率的单个数据点,交叉熵损失由以下公式给出:
这里是y = [y1, ..., y5]^T
和p = [p1, ..., p5]^T
。
同样,M
训练数据点上的平均损失可以表示为:
在训练过程中,小批量的梯度基于(2)
给出的平均对数损失,其中M
是所选的批量大小。 对于我们将结合验证准确率进行监视的验证日志损失,M
是验证集数据点的数量。 由于我们将在每个折叠中进行 K 折交叉验证,因此在每个折叠中我们将具有不同的验证数据集。
现在我们已经定义了训练方法,损失函数和验证指标,让我们继续进行数据探索和建模。
请注意,输出类别中的分类具有序数性质,因为严重性随类别而增加。 因此,回归可能会派上用场。 我们还将尝试通过回归来代替分类,以了解运气如何。 回归的挑战之一是将原始分数转换为类。 我们将使用一个简单的方案,将分数散列到最接近的整数严重性类别。
考虑类别失衡
在分类方面,类别失衡是一个主要问题。 下图描述了五个严重性类别的类别密度:
图 2.4:五个严重性类别的类别密度
从上图可以看出,将近 73% 的训练数据属于 0 类,这代表没有糖尿病性视网膜病。 因此,如果我们碰巧将所有数据点标记为 0 类,那么我们将具有 73% 的准确率。 在患者健康状况下这是不希望的。 我们宁愿有一个测试说患者没有时有某种健康状况(假阳性),而有一项测试却漏诊时没有发现某种健康状况(假阴性)。 如果模型学会将所有点分类为属于 0 类,则 73% 的准确率可能毫无意义。
在严重性类别 0 上,检测较高的严重性类别比做好工作更为重要。 使用对数损失或交叉熵成本函数的分类模型的问题在于它偏爱多数类。 这是因为交叉熵误差是从最大似然原理中得出的,该似然原理倾向于将较高的概率分配给多数类别。 我们可以做两件事:
- 丢弃具有更多样本的类别中的数据,或者丢弃低频类别的上样本,以保持样本在各个类别之间的分布均匀。
- 在损失函数中,权重与类别的密度成反比。 这将确保当模型无法对低频类别进行分类时,对成本函数施加更高的惩罚。
我们将使用方案二,因为它不需要生成更多数据或丢弃现有数据。 如果我们将类别权重与类别频率的倒数成正比,则会得到以下类别权重:
严重类别 | 类权重 |
---|---|
Class 0 |
0.0120353863 |
Class 1 |
0.1271350558 |
Class 2 |
0.0586961973 |
Class 3 |
0.3640234214 |
Class 4 |
0.4381974727 |
我们将在训练分类网络时使用这些权重。
预处理图像
不同类别的图像将存储在不同的文件夹中,因此可以轻松标记其类别。 我们将使用Opencv
函数读取图像,并将其调整为不同的大小,例如224 x 224 x3
。我们将基于 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 = [x[1] x[2]]^T ∈ R^2
定义 ,则像素的新坐标可以通过以下方式给出:
这里, M = R^(2x2)
是仿射变换矩阵, b = [b[1], b[2]]^T ∈ R^2
是翻译向量。
项b[1]
指定沿一个空间方向的翻译,而b[2]
提供沿另一空间维度的翻译。
这些转换是必需的,因为神经网络通常不是平移不变,旋转不变或尺度不变。 合并操作确实提供了一些平移不变性,但是通常这还不够。 神经网络不会将图像中特定位置的一个对象和另一图像中平移位置的相同对象视为同一事物。 这就是为什么我们需要在不同平移位置的图像的多个实例,以使神经网络更好地学习。 相同的解释适用于旋转和缩放。
旋转
以下是旋转的仿射变换矩阵,其中θ
表示旋转角度:
在这种情况下,转换向量b
为零。 通过选择一个非零b
,我们可以得到旋转和平移。
例如,下图显示了视网膜的照片,然后是旋转 90 度的同一张照片:
图 2.5:视网膜的旋转照片
平移
对于平移,仿射变换矩阵是单位矩阵,平移向量b
具有非零值:
例如,对于垂直方向上五个像素位置和水平方向上三个像素位置的平移,我们可以使用b = [5 3]^T
和M
作为单位矩阵。
以下是沿图像的宽度和高度按 24 个像素位置对视网膜进行的图像平移:
图 2.5:视网膜的图像平移
缩放比例
缩放可以通过对角矩阵M ∈ R^(2x2)
执行,如下所示:
这里,S[v]
表示沿垂直方向的比例因子,S[h]
表示沿水平方向的比例因子(请参见 “图 2.6”为插图)。 我们还可以选择通过具有非零转换向量b
来跟随转换的缩放:
图 2.6 视网膜的图像缩放
反射
可以通过变换矩阵T ∈ R^(2x2)
获得关于一条线L
与水平角度为θ
的反射,如下所示:
下图显示了视网膜照片的水平翻转:
图 2.7:视网膜照片的水平翻转
通过仿射变换产生更多图像
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 论文链接如下:
ResNet 论文链接如下:
InceptionV3 论文链接如下:
简而言之,VGG16
是一个 16 层的 CNN,使用3 x 3
过滤器和2 x 2
接收场进行卷积。 整个网络使用的激活函数都是 ReLU。 由 Simonyan 和 Zisserman 开发的 VGG 架构是 2014 年 ILSVRC 竞赛的亚军。 VGG16
网络由于其简单性而广受欢迎,它是从图像中提取特征的最受欢迎的网络。
ResNet50
是一种深层的 CNN,它实现了残余块的概念,与VGG16
网络的概念完全不同。 经过一系列的卷积激活池操作之后,该块的输入再次被反馈到输出。 ResNet 架构是由 Kaiming He 等人开发的,尽管具有 152 层,但它不如 VGG 网络复杂。 该架构通过实现 3.57% 的前五位错误率赢得了 2015 年 ILSVRC 竞赛,这比该竞赛数据集上的人类水平表现要好。 通过检查目标是否在概率最高的五类预测中来计算前五个错误率。 原则上,ResNet 网络尝试学习残差映射,而不是直接从输出映射到输入,如下面的残差框图所示:
图 2.8:ResNet 模型的残差块
InceptionV3 是 Google 提供的最新的 CNN。 InceptionV3 架构没有在每一层使用固定大小的卷积过滤器,而是使用不同大小的过滤器来提取不同粒度级别的特征。 下图说明了 InceptionV3 层的卷积块:
图 2.9:InceptionV3 卷积模块
Inception V1(GoogleNet)是 2014 年 ILSVRC 竞赛的获胜者。 它的最高 5% 错误率非常接近人类水平的表现,为 6.67%。
VGG16 迁移学习网络
我们将从预训练的VGG16
网络中的最后一个合并层中获取输出,并添加一对全连接层,每个层 512 个单元,然后是输出层。 最终池化层的输出是从全局平均池化操作传递到全连接层之前的。 我们只可以展平池化层的输出,而不是执行全局平均池化-其思想是确保池化的输出不是二维晶格格式,而是一维数组格式, 非常像一个全连接层。 下图说明了基于预训练的VGG16
的新VGG16
的架构:
图 2.10:VGG16 迁移学习网络
如上图所示,我们将从预训练网络中的最后一个最大池化层提取输出,并在最终输出层之前附加两个全连接层。 基于先前的架构,可以使用keras
如下代码块所示定义 VGG 定义函数:
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
网络。 需要注意的一件事是,由于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 迁移学习网络
可以类似于VGG16
和InceptionV3
网络定义用于迁移学习的ResNet50
模型,如下所示:
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 优化器(自适应矩估计器)用于实现随机梯度下降高级版本的训练。 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')
我们可以更改几个参数,例如学习率,批量大小,图像大小等,并且我们可以进行实验以得出一个不错的模型。 在训练阶段,模型位置保存在model_save_dest
字典中,该字典被写入dict_model
文件中。
在推理阶段,该模型仅基于训练后的模型对新的测试数据进行预测。
可以如下调用名为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%
,二次 Kappa 约为0.43
。
在此脚本中,我们将所有数据加载到内存中,然后将增强图像从ImageDataGenerator
馈送到模型进行训练。 如果训练图像集很少和/或大小适中,那么将数据加载到内存中可能不会引起太大关注。 但是,如果图像语料库很大和/或我们资源有限,那么将所有数据加载到内存中将不是可行的选择。 由于运行这些实验的计算机具有 64 GB RAM,因此我们能够毫无问题地训练这些模型。 即使是 16 GB 的 RAM 计算机也可能不足以通过将所有数据加载到内存中来运行这些实验,并且您可能会遇到内存错误。
问题是,我们是否需要一次将所有数据加载到内存中?
由于神经网络适用于小型批量,因此我们只需要与一个小型批量对应的数据即可一次通过反向传播训练模型。 类似地,对于下一个反向传播,我们可以丢弃与当前批量相对应的数据,然后处理下一个批量。 因此,以某种方式,每个小型批量中的内存需求仅是与该批量相对应的数据。 因此,我们可以在训练时创建动态批量,从而避免在内存较少的机器上训练深度学习模型。 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
)中的类别文件夹数来识别类别数。 在基于batch_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
二次方 Kappa。
分类结果
通过使用所有三个神经网络架构VGG16
,ResNet50
和InceptionV3
进行分类。 对于该糖尿病性视网膜病用例,使用迁移学习网络的InceptionV3
版本可获得最佳结果。 如果是分类分类,我们只是将具有最大预测分类概率的分类转换为预测严重性标签。 但是,由于问题中的类别具有序数意义,因此我们可以利用 softmax 概率的方法之一是针对 softmax 概率对类别严重性进行期望并得出预期分数y_hat
如下 :
我们可以对分数进行排序,并确定三个阈值来确定图像属于哪个类别。 这些阈值可以通过将这些预期得分作为特征训练辅助模型来选择。 建议读者按照这些思路进行试验,看看是否有任何好处。
作为该项目的一部分,我们正在使用迁移学习来解决一个难题。 通过在给定的数据集上从头开始训练网络,模型表现可能会更好。
测试时推断
以下代码可用于对未标记的测试数据进行推断:
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
与其像分类网络中那样使分类交叉熵(对数损失)最小化,不如使回归网络的均方误差最小。 对于回归问题最小化的成本函数如下,其中y_hat
是预测的标签:
一旦我们预测了回归分数,就将其舍入到最接近的严重性类别(零到四)。
使用 keras utils.sequence
作为生成器
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
从前面的代码中我们可以看到,训练生成器和验证生成器是使用DataGenerator
类创建的,该类继承了keras.utils.sequence
类。 推理函数可以编码如下:
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
模型已保存在我们指定的Regression
文件夹下。 接下来,我们可以对验证数据集进行推断,并查看回归模型的运行情况。 可以如下调用相同的 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% 的不错的验证准确率和0.466
的二次 Kappa 得分。 建议读者进行实验,看看是否基于预测的二级模型对进行评分,并且眼睛是左眼还是右眼比将朴素的评分映射到最近的严重性类别给出了更好的结果。
总结
在本章中,我们介绍了迁移学习的实际方面,以解决医疗保健领域的现实问题。 希望读者通过尽可能尝试定制这些示例来进一步构建这些概念。
我们通过分类和基于回归的神经网络获得的准确率和 kappa 分数足以用于生产实现。 在第 3 章,“神经机器翻译”中,我们将致力于实现智能机器翻译系统,这是比本章介绍的主题更为高级的主题。 我期待您的参与。
三、神经机器翻译
机器翻译简而言之,是指使用计算机将文本从一种语言翻译成另一种语言。 它是计算机语言学的一个分支,已经发展了几年。 目前,在美国,翻译业是一个价值 400 亿美元的产业,并且在欧洲和亚洲也正在快速发展。 翻译存在巨大的社会,政府,经济和商业需求,并且 Google,Facebook,eBay 等公司在其应用中广泛使用它。 尤其是 Google 的神经翻译系统是目前最先进的翻译系统之一,能够仅用一种模型执行多种语言的翻译。
早期的机器翻译系统首先将文本中的单词和短语翻译成所需目标语言的相关替代词。 但是,由于以下原因,通过这些简单的技术实现的翻译质量受到限制:
- 从源语言到目标语言的词到词映射并非始终可用。
- 即使在源语言和目标语言之间确实存在精确的词对词映射,这些语言的句法结构通常也不相互对应。 机器翻译中的此问题通常称为对齐错误。
但是,随着循环神经网络(RNN)架构的最新进展,机器翻译不仅提供了更高的翻译质量,而且还提供了更高的翻译质量。这种系统的复杂性远远小于传统系统。
机器翻译系统大致可分为三类:基于规则的机器翻译,统计机器翻译和神经机器翻译。
在本章中,我们将介绍以下主题:
- 基于规则的机器翻译
- 统计机器学习系统
- 神经机器翻译
- 序列到序列的神经翻译
- 神经翻译的损失函数
技术要求
您将需要具有 Python 3,TensorFlow 和 Keras 的基础知识。
基于规则的机器翻译
基于经典规则的机器翻译系统严重依赖于将文本从源语言转换为目标语言的规则。 这些规则通常由语言学家创建,通常在句法,语义和词汇层面上起作用。 传统的基于规则的机器翻译系统通常分为三个阶段:
- 分析阶段
- 词汇翻译阶段
- 生成阶段
图 3.1 是典型的基于规则的机器翻译系统的流程图:
图 3.1:基于规则的机器翻译系统的流程图
分析阶段
基于规则的机器翻译的第一阶段是分析阶段,其中分析源语言文本以提取与形态,词性,命名实体识别以及词义歧义有关的信息。 形态信息涉及单词的结构,词干的派生方式,词根的检测等。 词性标记器使用可能的语音标记来标记文本中的每个单词,例如名词,动词,副词,形容词等。 接下来是命名实体识别(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)
,如上式所示:
图 3.2:统计机器翻译架构
从上图可以看出,统计机器翻译问题已分解为三个不同的子问题,如上所述:
- 为目标建立语言模型,使我们能够估计
P(t)
- 从目标语言到源语言构建翻译模型,这使我们能够估计
P(s / t)
- 对可能的目标翻译进行搜索,然后选择最大化
P(s / t)P(t)
的翻译
我们将讨论这三个主题中的每一个,因为这些函数是任何机器翻译问题所固有的。
语言模型
在语言模型中,句子的概率表示为各个单词或短语的条件概率的乘积。 假设句子t
由单词t[1], t[2], ..., t[n]
。 根据概率的链式规则,句子t
的概率可以表示为:
根据上述公式构建语言模型将需要我们估计几个顺序的条件概率,这在实际中是不可能的。 为了使问题在计算上可行,一个简单的假设是仅根据前一个单词而不是之前的所有单词来对单词进行条件处理。 该假设也称为马尔可夫假设,该模型称为二元模型。 根据二元模型,单词的条件概率可以表示为:
为了进一步改善结果,我们可以使用三元模型,该模型将句子中的特定单词置于其前面两个单词的条件下,如下所示:
对于二元模型,给定训练语料库中偶对(t[1], t[2])
的总数,计算当前单词t[1]
的下一个单词t[2]
的条件概率,并根据语料库中t[1]
出现的次数归一化:
对于三元模型,当前单词t[3]
在两个单词t[1], t[2]
之前的条件概率,可以估计如下:
超出三字母组合模型通常会导致稀疏性。 即使对于双字母组模型,我们也可能会缺少几个双字母组的条件概率,因为它们没有出现在训练语料库中。 但是,那些缺失的二元组可能非常相关,并且估计其条件概率非常重要。 不用说,n
元模型倾向于估计出现在训练数据中的单词对的高条件概率,而忽略没有出现的单词。
语言模型的困惑度
困惑度指标用于评估语言模型的有用性。 假设我们已经在训练语料库上训练了一个语言模型,并且使句子或文本上的学习概率模型为P(.)
。 P(.)
的困惑度是根据与训练语料库相同的总体抽取的测试集语料库进行评估的。 如果我们用M
词表示测试集语料库,请说(w[1], w[2], ..., w[M])
,则模型在测试集序列上的困惑性表示如下:
如图所示H
的表达式可衡量每字的不确定性:
根据语言模型,我们可以按如下方式分解测试语料库的概率表达式:
如果我们将测试集中第i
个词的概率表示为条件,则以先前的字为条件P(s[i])
,则测试语料库的概率如下:
这里P(s[i]) = P(w[i] / w[1], w[2], ..., w[i-1])
。 将(1)
和(4)
结合起来,困惑可以写成如下:
假设我们有一个语言模型P(.)
和一个进行评估的测试集I love Machine Learning
。 根据语言模型,测试集的概率如下:
如果语言模型的训练语料也是I love Machine Learning
,则测试集的概率为 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 loves math
法语翻译为Santanu aim les maths
。 如我们所见,英语中的math
一词已翻译成法文的两个单词,即les maths
。 形式上,丰富度定义为目标语言中源语言单词生成的单词数量上的概率分布,并且可以表示为P(n / w[s])
,其中w[s]
代表源词。 而不是使用硬编码的数字n
,而是使用概率分布,因为相同的单词可能会根据上下文生成不同长度的翻译。 - 失真:对于任何机器翻译系统,源句子和目标句子之间的单词到单词的对应关系都很重要。 但是,源语言句子中单词的位置可能并不总是与目标语言句子中对应单词的位置完全同步。 失真通过概率函数
P(p[t], p[s], l)
覆盖了对齐的概念,其中p[t]
和p[t]
分别代表目标词和源词的位置,而l
代表目标句子的长度。 如果源语言是英语,目标语言是法语,则P(p[t] / p[s], l)
表示位置p[s]
的英语单词对应于位置p[s]
中的法语单词,其长度为l
。 - 单词到单词的翻译:最后,我们来进行单词到单词的翻译,这通常由给定源语言单词的目标语言单词的概率分布表示。 对于给定的源语言单词
w[s]
,概率可以表示为P(w[t] / w[s])
,其中w[t]
代表目标语言单词。
对于语言模型,需要在训练过程中估计生育率,失真率和单词到单词的翻译率。
现在,让我们回到估计概率P(s / t)
的原始问题。 如果我们用E
表示英语句子,而用F
表示法语句子,则需要计算P(F / E)
的概率。 为了考虑单词的对齐方式,我们将概率修改为P(F, a / E)
,其中a
表示目标句子在法语中的对齐方式。 这种一致性将有助于我们注入有关畸变和生育能力的信息。
让我们通过一个示例来计算概率P(F, a / E)
。 让一个特定的英语句子由五个单词的句子表示。 e = (e[1], e[2], ..., e[5])
,实际上这是实际法语句子的正确翻译。 f = (f[1], f[2], ..., f[6])
。 另外,让单词的相应对齐方式如下:
e1 -> f6
e2 ->
不对应法语中的任何单词e3 -> f3, f4
e4 -> f1
e5 -> f2
f5 ->
不对应英语中的任何单词
由于这是一个概率模型,因此该算法将尝试使用具有不同对齐方式的不同英语句子,在给定法语句子的情况下,其中具有正确对齐方式的正确英语句子应该具有最高的概率。
让我们将第一个英语单词考虑为e[1]
-它与法语单词f[6]
对齐,并且还会产生一个法语单词,如下:
现在,让我们将对齐方式作为两个成分的组合:失真a[d]
和丰富度f[d]
。 (1)
中的表达式可以重写如下:
如果我们仔细观察, P(f[5] / e[1])
就是翻译概率, P(a[f] / e[1])
是丰富度,而 P(a[d] / e[1], f[5])
是失真概率。 我们需要针对英语句子中与给定法语句子的所有比对中的所有给定英语单词进行此活动,以计算P(F, a / E)
。 最后,我们需要采用最佳英语句子E_hat
和对齐方式a_hat
,以使P(F, a / E)P(E)
的概率最大化。 如下所示:
这里要注意的一件事是,尝试不同的对齐方式和不同的可能单词翻译以寻求最佳翻译可能在计算上变得棘手,因此,需要部署巧妙的算法以在最短的时间内找到最佳翻译。
神经机器翻译
神经机器翻译(NMT)使用深度神经网络来执行从源语言到目标语言的机器翻译。 神经翻译机将源语言中的文本作为输入序列,并将其编码为隐藏的表示形式,然后将其解码回以生成目标语言中的翻译文本序列。 该 NMT 系统的主要优势之一是,整个机器翻译系统可以从端到端一起进行训练,这与基于规则的机器翻译系统和统计机器翻译系统不同。 一般而言,在[ 神经翻译机架构。
NMT 与其他传统方法相比的一些优点如下:
- 基于损失函数对 NMT 模型的所有参数进行端到端训练,从而降低了模型的复杂性
- 这些 NMT 模型使用的上下文比传统方法大得多,因此产生了更准确的翻译
- NMT 模型可以更好地利用单词和短语的相似性
- RNN 允许生成更好质量的文本,因此,相对于已翻译文本的语法而言,翻译更加准确
编码器-解码器模型
下图说明了一种神经翻译机的架构,该结构使用一个 LSTM 作为编码器,将输入源语言序列编码为最终隐藏状态h[f]
和最终存储单元状态c[f]
。 最终的隐藏状态和单元状态[h[f]; c[f]]
将捕获整个输入序列的上下文。 因此,[h[f]; c[f]]
成为解码器网络可适应的良好候选者。
此隐藏状态和单元状态信息[h[f]; c[f]]
作为初始隐藏状态和单元状态被馈送到解码器网络,然后解码器在目标序列上训练,其中输入目标序列相对于输出目标序列滞后一。 根据解码器,输入序列的第一个字是伪字[START]
,而输出标签是字c'est
。 解码器网络仅被训练为一种生成语言模型,在任何时候,输出标签t
都是相对于输入的下一个单词,即y[t] = x[t + 1]
。 唯一的新变化是编码器的最终隐藏状态和单元状态(即[h[f]; c[f]]
) 解码器的隐藏状态和单元状态为翻译提供内容。
这意味着可以将训练过程视为为目标语言(由解码器表示)建立语言模型,该模型以代表源语言的编码器的隐藏状态为条件:
图 3.3:神经机器翻译系统的高级编码器-解码器架构
如果T
是与源语言文本S
相对应的目标语言文本,那么为了进行训练,我们只是试图使P[w](T[s+1] / S, T)
的对数概率相对于W
最大化,其中T[s+1]
表示平移一个时间步骤的目标语言文本,W
表示编码器-解码器架构模型参数。
现在我们已经讨论了编码器-解码器 NMT 的训练过程,现在我们将研究如何在推理过程中使用训练后的模型。
将编码器-解码器模型用于推理
在 NMT(神经翻译机)上进行推理的架构流程与训练 NMT 略有不同。 以下是使用 NMT 执行推理的架构流程:
图 3.4:基于编码器/解码器的神经机器翻译的推理
在推理过程中,源语言输入序列被馈送到编码器网络并产生最终的隐藏状态和单元状态, [h[f]; c[f]]
馈给解码器的隐藏状态和单元状态。 解码器被转换为单个时间步,馈送到解码器的第一个输入是伪[START]
字。 因此,基于[h[f]; c[f]]
和初始虚拟字[START]
,解码器将输出一个字w
,以及新的隐藏状态和单元格状态[h[d]; c[d]]
。 这个单词w
再次以新的隐藏状态和单元状态馈送到解码器。 [h[d]; c[d]]
生成下一个单词。 重复此过程,直到遇到序列结束字符。
实现序列到序列的神经翻译机
我们将建立一个神经机器翻译系统,该系统将学习将英语短句子翻译成法语。 为此,我们将使用位于这个页面上的英语到法语文本语料库(fra-eng/fra.txt
)。
处理输入数据
文本数据不能直接输入任何神经网络,因为神经网络只能理解数字。 我们将每个单词视为一个单编码的向量,其长度等于每个语料库中出现的单词的数量。 如果英语语料库包含 1,000 个单词,则一键编码的向量v[e]
的大小为 1,000,即v[e] ∈ R^(1000 x 1)
。
我们将通读英语和法语语料库,并确定它们各自中唯一词的数量。 我们还将通过索引来表示单词,对于该单词的单编码向量,该单词对应的索引将设置为 1,而其余索引将设置为 0。 例如,假设在英语语料库中,我们有四个词:Global warming is real
。 我们可以如下定义每个单词的索引:
单词 | 索引 |
---|---|
global |
0 |
warming |
1 |
is |
2 |
real |
3 |
在这种情况下,我们可以将单词global
的单热编码向量定义为[1,0,0,0]^T
。 类似地,real
的一键编码向量可以表示为[1,0,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
,文本句子的数量为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 处理源输入序列,并将源文本编码为有意义的摘要。 有意义的摘要将存储在最后的序列步骤中,即隐藏和单元状态h[f]
和c[f]
。 这些向量在一起(即[h[f]; c[f]]
)提供了有关源文本的有意义上下文,并且训练了解码器来产生具有隐藏和单元状态向量的目标序列[h[f]; c[f]]
。
下图所示“图 3.5”是英语到法语翻译的训练过程的详细图。 英文句子It's a beautiful day
通过 LSTM 转换为含义摘要,然后存储在隐藏和单元格状态向量[h[f]; c[f]]
中。 然后使解码器根据嵌入在[h[f]; c[f]]
中的信息,以输入源语句为条件,生成自己的目标序列。 给定源句,使在时间步t
的解码器预测下一个目标单词,即在时间步t + 1
的单词。 这就是为什么目标输入字和目标输出字之间有一个时间步长的滞后的原因。 对于第一步,解码器在目标文本序列中没有任何先前的单词,因此可用于预测目标单词的唯一信息是以[h[f]; c[f]]
,作为初始隐藏和单元状态向量提供。 像编码器一样,解码器也使用 LSTM,并且如上所述,输出目标序列比输入目标序列提前一个时间步长:
图 3.5:训练时机器翻译网络流程的图示
我们基于“图 3.5”中所示的架构,定义了用于训练函数model_enc_dec
的编码器解码器端到端模型。 在这里,编码器(LSTM 1)依次获取源语言文本单词,并在编码器(LSTM 1)的最后序列步骤中捕获源语言句子或文本的整个上下文。来自编码器的上下文将作为解码器(LSTM 2)的初始状态进行馈送,该学习器将根据当前单词来预测下一个单词,因为在训练过程中我们会得到一个句子 / text 作为目标语言,因此解码器只需将其输入移位一个时间步即可形成目标:
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
处的目标标签将是单热点编码的向量y[t] ∈ {0,1}^16297
,法语词汇表中每个16,297
个单词的预测输出将采用概率形式。 如果将预测的输出概率向量表示为p[t] ∈ (0, 1)^16297
,则特定句子在每个时间步的平均分类损失由以下给出:
通过汇总所有序列时间步长上的损失,可以得出整个句子的损失,如下所示:
由于我们使用小批量随机梯度下降进行工作,因此可以通过对小批量中所有句子的平均损失来获得小批量的平均成本。 如果我们使用大小为m
的微型批量,则每个微型批量的平均损失如下:
最小批量成本用于计算随机梯度下降的梯度。
训练模型
我们首先执行model_enc_dec
函数来定义训练模型以及encoder_model
和decoder_model
进行推理,然后将其与categorical_crossentropy
损失和rmsprop
一起编译 ]优化器。 我们可以尝试使用其他优化器,例如 Adam,具有动量的 SDG 等,但是目前,我们将坚持使用rmsprop
。train
函数可以定义如下:
# 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]; c[f]]
作为输出。 我们不能按原样使用解码器网络,因为目标语言输入字不再可以馈送到解码器。 相反,我们将解码器网络折叠为一个步骤,并将该步骤的输出作为下一步的输入。 我们以虚拟字[START]
作为解码器的第一个输入字,以及[h[f]; c[f]]
,用作其初始隐藏状态和单元格状态。 目标输出字w[1]
以及隐藏和单元状态h[1]; c[1]
由解码器使用[START]
和[h[f]; c[f]]
,因为输入再次馈送到解码器以生成下一个字,然后重复该过程,直到解码器输出伪字[END]
为止。 下图说明了推理过程的逐步表示形式,以便于解释:
图 3.6:推理过程的逐步说明
从上图可以看出,解码器第一步的输出为C'est
,而隐藏状态和单元状态为[h'[1]; c'[1]]
。 如虚线所示,将其再次馈送到解码器,以生成下一个字以及下一组隐藏状态和单元状态。 由于解码器输出伪结束字符[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 的英语和法语词汇量的一键编码向量。 随着语料库的增加,单热点编码词向量的大小将进一步增加。 比较两个单词时,这种稀疏的高维向量没有任何相似性概念,因为即使两个单词的含义几乎相同,它们的余弦乘积也将是0
。 在下一节中,我们将了解如何以较小的维数进行单词向量嵌入来解决此问题。
词向量嵌入
代替单热编码向量,可以使用词向量嵌入来表示维的密集空间中的单词,该空间比单热编码向量低得多。 嵌入单词w
的单词向量可以用v[w] ∈ R^m
表示,其中m
是词向量嵌入的维数。 如我们所见,虽然单热编码向量的每个分量只能占用{0,1}的二进制值,但词向量嵌入的分量却可以占用任何实数,因此具有更密集的表示。 相似性和类比的概念也与词向量嵌入有关。
通常通过诸如连续词袋法,skip-gram,GloVe 等技术来训练词向量嵌入。 我们将不对它们的实现进行过多的介绍,但中心思想是以这样的方式定义词向量嵌入:将类似的词紧密放置在m
维欧几里得空间中:
图 3.7:GloVe 嵌入的相似性和类比说明
在上一张图中,我们绘制了男人,女人,国王和女王的 GloVe 词向量嵌入的二维 TSNE 视图 。 我们可以看到,男人和女人具有内在的相似性,国王和女王的情况也是如此。 此外,我们看到国王和男人之间的向量差异与女王和女人的向量差异几乎相同,这可能代表王权的一些概念。 如我们所见,除了表达单词之间的相似性之外,还可以通过单词向量嵌入来表示类似男人:国王:女人:女王之类的东西。 在下一节中,我们将讨论使用 RNN 中的嵌入层将输入单词表示为单词向量嵌入,而不是单编码的向量。
嵌入层
嵌入层将输入单词的索引作为输入,并提供单词的单词向量嵌入作为输出。 嵌入层的大小为R^(dxV)
,其中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
该模型在 GeForce GTX 1070 GPU 上进行了训练,大约需要 9.434 分钟才能训练 32,000 条记录并进行 8,000 条记录的推理。 强烈建议用户使用 GPU,因为 RNN 的计算量很大,可能需要数小时才能在 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 章,“使用 GAN 进行时装行业中的样式迁移”中,我们将通过与生成性对抗网络(这是现代的革命)进行与时装业中样式迁移有关的项目。 人工智能领域。
四、使用 GAN 的时尚行业样式迁移
样式迁移的概念是指将产品样式渲染为另一种产品的过程。 想象一下,您的一位时尚狂朋友买了一个蓝色的手袋,想买一双类似印花的鞋子。 直到 2016 年,这还是不可能实现的,除非他们与一位时装设计师成为朋友,他们必须首先设计一款鞋子,然后才能批准生产。 然而,随着生成对抗网络的最新进展,这种设计过程可以很容易地进行。
生成对抗网络是通过在生成器网络和判别器网络之间进行零和游戏来学习的网络。 假设一位时装设计师想要设计一种特定结构的手袋,并且正在探索不同的印花。 设计人员可以绘制手提包的结构草图,然后将草图图像输入到生成的对抗网络中,以得出手提包的几种可能的最终印刷品。 这种样式迁移过程可以使客户自己绘制产品设计和图案,而无需征集大量设计师的意见,从而对时尚行业产生巨大影响。 通过推荐具有类似设计和风格的产品来补充客户已经拥有的产品,时装屋也可以从中受益。
在这个项目中,我们将构建一个智能人工智能系统,该系统将生成与给定手提袋样式相似的鞋子,反之亦然。 我们之前讨论的原始 GAN 不足以实现这个项目。 我们需要的是 GAN 的定制版本,例如 DiscoGAN 和 CycleGAN。
在本章中,我们将介绍以下主题:
- 我们将讨论 DiscoGAN 背后的工作原理和数学基础
- 我们将比较和对比 DiscoGAN 与 CycleGAN,后者在架构和工作原理上非常相似
- 我们将训练一个 DiscoGAN,该系统学习从给定的袋子素描中生成袋子的图像
- 最后,我们将讨论与训练 DiscoGAN 有关的复杂性
技术要求
读者应具有 Python 3 和人工智能的基础知识,才能完成本章中的项目。
DiscoGAN
DiscoGAN 是一个生成的对抗网络,它在给定域A
中的图像的情况下生成域B
中产品的图像。下图说明了 DisoGAN 网络的架构图:
图 4.1:DiscoGAN 的架构图
域B
中生成的图像在样式和样式上都类似于域A
中的图像。 无需在训练过程中显式配对来自两个域的图像就可以学习这种关系。 鉴于项目的配对是一项耗时的任务,因此这是一项非常强大的功能。 在较高的水平上,它尝试学习神经网络G[AB]
和G[BA]
形式的两个生成器函数。 图像x[A]
,当通过生成器馈入时G[AB]
,产生图像x[AB]
,在域B
中看起来很真实。此外,当此图像x[AB]
通过其他生成器网络G[BA]
馈送时,它应产生图像x[ABA]
,理想情况下应与原始图像x[A]
相同。 关于生成器函数,以下关系应成立:
但是实际上,生成器函数G[AB]
和G[BA]
不可能彼此相反,因此我们尝试通过选择 L1 或 L2 归一化的损失来尽量减少重建图像和原始图像之间的损失。 L1 规范损失基本上是每个数据点的绝对误差之和,而 L2 规范损失表示每个数据点的平方误差的和。 我们可以如下表示单个图像的 L2 范数损失:
仅使前面的损失最小化是不够的。 我们必须确保在域B
中创建的图像x[B]
看起来逼真。例如,如果我们将域A
中的衣服映射到域B
中的鞋子,我们将确保x[B]
类似于鞋子。 如果图像不够真实,则在域B
侧的判别器D[B]
将检测为x[B]
为假。 鞋子,因此也要考虑与此有关的损失。 通常,在训练过程中,向判别器提供生成的域B
图像x[AB] = G[AB](X[A])
,我们选择在这里用y[B]
表示,以便它学习从假图像中对真实图像进行分类。 您可能还记得,在 GAN 中,生成器和判别器相互进行零和最小最大值游戏,以便不断变得更好,直到达到平衡为止。 如果伪造的图像看起来不够逼真,则判别器将对其进行惩罚,这意味着生成器必须学习产生更好的图像x[AB]
,如果输入图像x[A]
。 考虑到所有这些因素,我们可以将我们希望最小化的生成器损失公式化为重建损失,以及判别器将x[AB]
识别为假冒的损失。 第二种损失将试图使生成器在域B
中生成逼真的图像。将域A
中的图像x[A]
映射到域B
中的图像的生成器损失可以表示如下:
L2 范数下的重建损失可以表示为:
由于我们正在处理图像,因此可以假设x[A]
是所有像素的扁平向量,以符合 L2 规范项。 如果我们假设x[A]
是矩阵,则最好将||·||_2^2
称为 Frobenius 范数。 但是,这些只是数学术语,实质上,我们只是将原始图像和重建图像之间的像素值差的平方和求和。
让我们考虑一下生成器在使变换后的图像x[AB]
追求时要尽量降低成本的做法。 判别器将尝试将图像标记为伪图像,因此生成器G[AB]
应当在这种情况下产生x[AB]
使其成为假图片的对数损失的方式尽可能小。 如果域B
中的判别器D[B]
将真实图像标记为1
,将伪图像标记为0
,则图像真实的概率由D[B](.)
,则生成器应使x[AB]
在判别器网络下极有可能出现,从而使D[B](x[B]) = D[B](G[AB](x[A]))
接近1
)。 就对数损失而言,生成器应使先前概率的负对数最小化,这基本上使我们得到C[D(AB)]
,如下所示:
结合(3)
和(4)
,我们可以获得将镜像从域A
映射到域A
的总生成器成本C_G[AB]
域B
,如下所示:
最大的问题是,我们可以在这里停下来吗? 由于我们有来自两个域的图像,因此要获得更好的映射,我们也可以从域B
拍摄图像,并通过生成器G[BA]
将它们映射到域A
。 如果我们在域B
中拍摄x[B]
图像,并通过生成器G[BA]
将其转换为图像x[BA]
,而域A
上的标识符由D[A]
给出,则与这种转换相关的成本函数由以下给出:
如果我们对两个域中的全部图像总数求和,则生成器损失将由(5)
和(6)
之和给出,如下所示:
现在,让我们构建成本函数,这些判别器将尝试最小化以建立零和最小/最大游戏。 每个域中的判别器都会尝试将真实图像与伪图像区分开,因此判别器G[B]
会尝试将成本降到最低C_D[B]
,如下所示:
同样,判别器D[A]
会尝试将成本降到最低。C_D[A]
如下所示:
结合(8)
和(9)
的总判别器成本由C[D]
给出,如下:
如果我们表示G[AB]
的参数,则G[BA]
,D[A]
和D[B]
设为θ[GAB]
,θ[GBA]
,θ[DA]
和θ[DB]
,则网络的优化参数可以表示为:
对成本函数执行随机梯度下降(例如 Adam),以得出最优解。 请注意,如前所述,生成对抗网络的解决方案是优化成本函数的一个障碍。
CycleGAN
CycleGAN 从根本上类似于 DiscoGAN,但有一个小的修改。 在 CycleGAN 中,我们可以灵活地确定相对于 GAN 损失或归因于判别器的损失,为重建损失分配多少权重。 该参数有助于根据眼前的问题按正确比例平衡损失,以帮助网络在训练时更快地收敛。 CycleGAN 的其余实现与 DiscoGAN 相同。
学习从草绘的轮廓生成自然手袋
在本章中,我们将使用草绘的轮廓生成手袋,而无需使用 DiscoGAN 进行显式配对。 我们将草图图像表示为属于域A
,而将自然手袋图像表示为属于域B
。将有两种生成器:一种生成器,用于获取域A
的图像并将其映射到在域B
下看起来逼真的图像,以及另一个与此相反:将域B
中的手袋图像映射到在域A
下看起来很逼真的图像。判别器将尝试从每个域中真实图像的生成器中识别生成器生成的虚假图像。 生成器和判别器将相互进行 minimax 零和游戏。
要训练该网络,我们将需要两套图像,手袋的草图或轮廓以及手袋的自然图像。 可以从以下链接下载图像。
在接下来的几节中,我们将完成在 TensorFlow 中定义 DiscoGAN 网络的过程,然后训练它使用充当图像边缘的手提包草图来生成逼真的手提包图像。 我们将从定义生成器网络的架构开始。
预处理图像
edges2handbags
数据集文件夹中的每个图像在同一图像中包含bag
的图片和bag edges
的图片。 为了训练网络,我们需要将它们分离为属于我们在 DiscoGAN 架构中讨论过的两个域A
和 B 的图像。 通过使用以下代码(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 的生成器
DiscoGAN 的生成器是前馈卷积神经网络,其中输入和输出是图像。 在网络的第一部分中,图像在空间维度上按比例缩小,而输出特征映射的数量随层的进展而增加。 在网络的第二部分中,图像沿空间维度按比例放大,而输出特征映射的数量则逐层减少。 在最终输出层中,将生成具有与输入相同的空间大小的图像。 如果生成器将图像x[A]
转换为x[AB]
从域A
到域B
表示为G[AB]
,则我们有x[AB] = G[AB](x[A])
。
此处显示的是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
在生成器函数中,我们定义了 LReLU 激活函数,并使用0.2
的泄漏因子。 我们还定义了卷积层生成函数common_conv2d
(用于对图像进行下采样)和common_deconv2d
(用于将经降采样的图像上采样至其原始空间大小)。
我们通过使用tf.get_variable_scope().reuse_variables()
使用reuse
选项定义生成器函数。 当多次调用同一个生成器函数时,重用选项可确保我们重用特定生成器使用的相同变量。 当我们删除重用选项时,我们为生成器创建了一组新的变量。
例如,我们可能使用生成器函数创建了两个生成器网络,因此在第一次创建这些网络时不会使用reuse
选项。 如果再次引用该生成器函数,则使用reuse
选项。 卷积(下采样)和解卷积(上采样)期间的激活函数是 LReLU,然后进行批量归一化,以实现稳定和快速的收敛。
网络不同层中的输出特征映射的数量可以是self.gf
或其倍数。 对于我们的 DiscoGAN 网络,我们选择了self.gf
作为64
。
生成器中要注意的一件事是输出层的tanh
激活函数。 这样可以确保生成器生成的图像的像素值在[-1, +1]
的范围内。 这对于输入图像具有[-1, +1]
范围内的像素强度非常重要,这可以通过对像素强度进行简单的逐元素变换来实现,如下所示:
同样,要将图像转换为可显示的 0-255 像素强度格式,我们需要应用逆变换,如下所示:
DiscoGAN 的判别器
DiscoGAN 的判别器将学会在特定域中将真实图像与假图像区分开。 我们将有两个判别器:一个用于域A
,一个用于域B
。这些判别器也是可以执行二分类的卷积网络。 与传统的基于分类的卷积网络不同,判别器没有任何全连接层。 使用步长为 2 的卷积对输入图像进行下采样,直到最终层(输出为1 x 1
)为止。同样,我们使用 LReLU 作为激活函数并使用批量归一化以实现稳定和快速的收敛。 以下代码显示了 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
。
建立网络并定义成本函数
在本节中,我们将使用生成器和鉴别函数来构建整个网络,并定义在训练过程中要优化的成本函数。 TensorFlow 代码如下:
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 范数误差将用作重建损失,而二元互熵将用作判别器损失。 然后,我们使用生成器函数为两个域中的图像定义占位符,并为每个域中的伪图像定义 TensorFlow 操作。 我们还通过传递特定于域的伪造和真实图像来定义判别器输出的操作。 除此之外,我们为两个域中的每个域中的重建图像定义 TensorFlow 操作。
一旦定义了操作,我们就可以使用它们来计算损失函数,同时考虑图像的重建损失和归因于判别器的损失。 请注意,我们使用了相同的生成器函数来定义域A
到B
的生成器,也定义了从B
到A
的生成器。唯一不同的是为这两个网络提供了不同的名称:generator_AB
和 generator_BA
。 由于变量作用域定义为name
,所以这两个生成器都将具有不同的权重集,并以提供的名称为前缀。
下表显示了我们需要跟踪的不同损失变量。 就生成器或判别器的参数而言,所有这些损失都需要最小化:
不同损失的变量 | 说明 |
---|---|
self.D_B_loss_real |
在对域B 中的真实图像进行分类时,判别器D[B] 的二进制交叉熵损失。(相对于判别器D[B] 的参数,该损失应最小化) |
self.D_B_loss_fake |
在对域B 中的伪造图像进行分类时,判别器D[B] 的二进制交叉熵损失。(相对于判别器D[B] 的参数,该损失应最小化) |
self.D_A_loss_real |
在对域A 中的真实图像进行分类时,判别器D[A] 的二进制交叉熵损失。(相对于判别器D[A] 的参数,该损失应最小化) |
self.D_A_loss_fake |
在对域A 中的伪造图像进行分类时,判别器D[A] 的二进制交叉熵损失。(相对于判别器D[A] 的参数,该损失应最小化) |
self.loss_GABA |
通过两个生成器G[AB] 和G[BA] 将域A 中的图像映射到B ,然后再映射回A 的重建损失,加上假图片G[AB](x[A]) 的二进制交叉熵,由域B 中的判别器标记为真实图像。(相对于生成器G[AB] 和G[BA] 的参数,该损失应最小化) |
self.loss_GBAB |
通过两个生成器G[BA] 和G[AB] 将域B 中的图像映射到A ,然后再映射回B 的重建损失,加上伪图片G[BA](x[B]) 的二元交叉熵,由域A 中的判别器标记为真实图像。(相对于生成器G[AB] 和G[BA] 的参数,该损失应最小化) |
前四个损失组成了判别器损失,需要根据判别器的参数将其最小化。 ]。 最后两个损失组成了生成器损失,需要根据生成器的参数将其最小化。 。
损失变量通过tf.summary.scaler
与 TensorBoard 绑定,以便可以在训练过程中监控这些损失,以确保以期望的方式减少损失。 稍后,我们将在训练进行时在 TensorBoard 中看到这些损失痕迹。
建立训练流程
在train_network
函数中,我们首先为生成器和判别器损失函数定义优化器。 我们将 Adam 优化器用于生成器和判别器,因为这是随机梯度下降优化器的高级版本,在训练 GAN 方面非常有效。 亚当使用梯度的衰减平均值(非常类似于稳定梯度的动量)和平方梯度的衰减平均值,以提供有关成本函数曲率的信息。 与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
随机选择的图像馈送到生成器G[BA]
中,以在域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 的不同参数值。 下表中列出了这些:
| 参数名称 | 变量名称和值集 | 原理 |
| Adam 优化器的学习率 | self.l_r = 2e-4
| 我们应该始终训练学习率较低的 GAN 网络以获得更好的稳定性,而 DiscoGAN 也不例外。 |
| Adam 优化器的衰减率 | 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
是特定于该程序的 Tensorboard 日志的存储目标,应在程序中定义如下:
self.writer = tf.summary.FileWriter("./logs", self.sess.graph)
- 执行完步骤 1 中的命令后,导航到 TensorBoard 的
localhost:6006
站点:
以下屏幕快照中展示了在项目中实现的 DiscoGAN 训练期间在 TensorBoard 中查看的一些生成器和判别器损失的痕迹:
图 4.2:Tensorboard Scalars
部分包含不同损失的迹线
以下屏幕截图显示了随着训练的进行,域A
中判别器的损失成分:
图 4.3:域A
中的判别器损失
从前面的屏幕截图中,我们可以看到不同批量中域A
中判别器的损失。 da_loss
是da_loss_real
和da_loss_fake
损失的总和。 da_loss_real
稳步下降,这是因为判别器易于学会识别域A
中的真实图像,而虚假图像的损失则稳定在 0.69 左右,这是二分类器输出时您可以期望的logloss
具有1/2
概率的类。 发生这种情况是因为生成器也在同时学习以使伪图像看起来逼真,因此使鉴别人员很难轻松地将生成器图像分类为伪图像。 域B
上的判别器的损失曲线看起来与域A
的上一幅屏幕快照所示的相似。
现在让我们看一下生成器的损失曲线,如下所示:
图 4.4:DiscoGAN 生成器的损失曲线
g_loss_a2b
是从域A
到域B
以及从域B
反向重构图像的组合生成器损失,也是与使变换后的图像在B
域中看起来逼真相关的二进制交叉熵损失。 g_loss_b2a
是从域B
到域A
以及从域A
重建图像的组合生成器损失,也是与使变换后的图像在A
域中看起来逼真相关的二进制交叉熵损失。这两种损失曲线及其总和随着批量的进行不断减少,正如我们从上一个屏幕快照中的 TensorBoard 视觉图中看到的那样。
由于训练生成对抗网络通常非常棘手,因此监视其损失概况的进度以了解训练是否按预期进行是有意义的。
DiscoGAN 生成的样本图像
在本章结束时,让我们看一下 DiscoGAN 在两个域中生成的一些图像:
图 4.5:根据草图生成的手提包图像
以下屏幕截图包含手提包草图的生成图像(域A
):
图 4.6:根据手提包图像生成的草图
我们可以看到 DiscoGAN 在将任一域中的图像转换为另一域中的高质量逼真的图像方面做得非常出色。
总结
现在,我们到了本章的结尾。 您现在应该精通 DiscoGAN 的技术知识和实现复杂性。 我们在本章中探讨的概念可用于实现各种生成性对抗性网络,这些网络具有适合当前问题的细微变化。 DiscoGAN 网络的端到端实现位于 GitHub 存储库中,位于这里。
在第 5 章,“视频字幕应用”中,我们将研究视频到文本翻译应用,它们属于人工智能领域的专家系统。
五、视频字幕应用
随着视频制作速度成倍增长,视频已成为一种重要的沟通媒介。 但是,由于缺乏适当的字幕,视频仍无法吸引更多的观众。
视频字幕(翻译视频以生成有意义的内容摘要的艺术)在计算机视觉和机器学习领域是一项具有挑战性的任务。 传统的视频字幕制作方法并没有成功。 但是,随着最近借助深度学习在人工智能方面的发展,视频字幕最近引起了极大的关注。 卷积神经网络以及循环神经网络的强大功能使得构建端到端企业级视频字幕系统成为可能。 卷积神经网络处理视频中的图像帧以提取重要特征,这些特征由循环神经网络依次处理以生成有意义的视频摘要。 视频字幕系统的一些重要应用如下:
- 自动监控工厂安全措施
- 根据通过视频字幕获得的内容对视频进行聚类
- 银行,医院和其他公共场所的更好的安全系统
- 网站中的视频搜索,以便提供更好的用户体验
通过深度学习构建智能视频字幕系统主要需要两种类型的数据:视频和文本字幕,它们是用于训练端到端系统的标签。
作为本章的一部分,我们将讨论以下内容:
- 讨论 CNN 和 LSTM 在视频字幕中的作用
- 探索序列到序列视频字幕系统的架构
- 利用序列到序列的架构,构建视频到文本的视频字幕系统
在下一节中,我们将介绍如何使用卷积神经网络和循环神经网络的 LSTM 版本来构建端到端视频字幕系统。
技术要求
您将需要具备 Python 3,TensorFlow,Keras 和 OpenCV 的基础知识。
视频字幕中的 CNN 和 LSTM
视频减去音频后,可以认为是按顺序排列的图像集合。 可以使用针对特定图像分类问题训练的卷积神经网络(例如 ImageNet)从这些图像中提取重要特征。 预训练网络的最后一个全连接层的激活可用于从视频的顺序采样图像中得出特征。 从视频顺序采样图像的频率速率取决于视频中内容的类型,可以通过训练进行优化。
下图(“图 5.1”)说明了用于从视频中提取特征的预训练神经网络:
图 5.1:使用预训练的神经网络提取视频图像特征
从上图可以看出,从视频中顺序采样的图像经过预训练的卷积神经网络,并且输出最后一个全连接层中的4,096
单元的激活。 如果将t
时的视频图像表示为x[t]
,并且最后一个全连接层的输出表示为f[t] ∈ R^4096
,然后f[t] = f[w](x[t])
。 此处,W
表示直到最后一个全连接层的卷积神经网络的权重。
这些序列的输出函数f[1], f[2], ..., f[t], ..., f[n]
可以作为循环神经网络的输入,该神经网络学习根据输入特征生成文本标题,如下图所示(“图 5.2”):
图 5.2:处理来自 CNN 的顺序输入特征时的 LSTM
从上图可以看出,来自预训练卷积神经的生成的特征f[1], f[2], ..., f[t], ..., f[n]
由 LSTM 依次处理,产生文本输出o[1], o[2], ..., o[t], ..., o[n]
,它们是给定视频的文本标题。 例如,上图中的视频标题可能是“一名戴着黄色头盔的人正在工作”:
o1, o2, . . . . . ot . . . oN = { "A ","man" "in" "a" "yellow" "helmet" "is" "working"}
既然我们对视频字幕在深度学习框架中的工作方式有了一个很好的了解,让我们在下一部分中讨论一个更高级的视频字幕网络,称为逐序列视频字幕。 在本章中,我们将使用相同的网络架构来构建视频字幕系统。
序列到序列的视频字幕系统
序列到序列的架构基于 Subhashini Venugopalan,Marcus Rohrbach,Jeff Donahue,Raymond Mooney,Trevor Darrell 和 Kate Saenko 撰写的名为《序列到序列-视频到文本》的论文。 该论文可以在这个页面中找到。
在下图(“图 5.3”)中,说明了基于先前论文的序列到字幕视频字幕神经网络架构:
图 5.3:序列到序列视频字幕网络架构
序列到序列模型通过像以前一样通过预训练的卷积神经网络处理视频图像帧,最后一个全连接层的输出激活被视为要馈送到后续 LSTM 的特征。 如果我们在时间步t
表示预训练卷积神经网络的最后一个全连接层的输出激活为f[t] ∈ R^4096
,那么我们将为视频中的N
个图像帧使用N
个这样的特征向量。 这些N
个特征向量f[1], f[2], ..., f[t], ..., f[n]
依次输入 LSTM,以生成文本标题。
背靠背有两个 LSTM,LSTM 中的序列数是来自视频的图像帧数与字幕词汇表中文本字幕的最大长度之和。 如果在视频的N
个图像帧上训练网络,并且词汇表中的最大文本标题长度为M
,则 LSTM 在N + M
时间步长上训练。 在N
个时间步中,第一个 LSTM 依次处理特征向量f[1], f[2], ..., f[t], ..., f[n]
,并将其生成的隐藏状态馈送到第二 LSTM。 在这些N
个时间步中,第二个 LSTM 不需要文本输出目标。 如果我们将第一个 LSTM 在时间步t
的隐藏状态表示为h[t]
,则第二个 LSTM 在前N
个时间步长的输入为h[t]
。 请注意,从N + 1
时间步长开始,第一个 LSTM 的输入是零填充的,因此该输入对t > N
的h[t]
没有隐藏状态的影响。。 请注意,这并不保证t > N
的隐藏状态h[t]
总是相同的。 实际上,我们可以选择在任何时间步长t > N
中将h[t]
作为h[T]
送入第二个 LSTM。
从N + 1
时间步长开始,第二个 LSTM 需要文本输出目标。 在任何时间步长t > N
的输入是h[t], w[t-1]
,其中h[t]
是第一个 LSTM 在时间步t
的隐藏状态,而w[t-1]
是时间步t-1
中的文本标题词。
在N + 1
时间步长处,馈送到第二 LSTM 的单词w[N]
是由<bos>
表示的句子的开头。 一旦生成句子符号<eos>
的结尾,就训练网络停止生成标题词。 总而言之,两个 LSTM 的设置方式是,一旦它们处理完所有视频图像帧特征 ,它们便开始产生文本标题词。
处理时间步t > N
的第二个 LSTM 输入的另一种方法是只喂w[t-1]
而不是h[t], w[t-1]
,并在时间步T
传递第一个 LSTM 的隐藏状态和单元状态h[T], c[T]
,到第二个 LSTM 的初始隐藏和单元状态。 这样的视频字幕网络的架构可以说明如下(请参见“图 5.4”):
图 5.4:序列到序列模型的替代架构
预训练的卷积神经网络通常具有通用架构,例如VGG16
,VGG19
,ResNet
,并在 ImageNet 上进行了预训练。 但是,我们可以基于从我们要为其构建视频字幕系统的域中的视频中提取的图像来重新训练这些架构。 我们还可以选择一种全新的 CNN 架构,并在特定于该域的视频图像上对其进行训练。
到目前为止,我们已经介绍了使用本节中说明的序列到序列架构开发视频字幕系统的所有技术先决条件。 请注意,本节中建议的替代架构设计是为了鼓励读者尝试几种设计,并查看哪种设计最适合给定的问题和数据集。
从下一部分开始,我们致力于构建智能视频字幕系统。
视频字幕系统的数据
我们通过在MSVD dataset
上训练模型来构建视频字幕系统,该模型是 Microsoft 的带预字幕的 YouTube 视频存储库。 可以从以下链接下载所需的数据。可通过以下链接获得视频的文本标题。
MSVD dataset
中大约有1,938
个视频。 我们将使用它们来训练逐序列视频字幕系统。 还要注意,我们将在“图 5.3”中所示的序列到序列模型上构建模型。 但是,建议读者尝试在“图 5.4”中介绍的架构上训练模型,并了解其表现。
处理视频图像来创建 CNN 特征
从指定位置下载数据后,下一个任务是处理视频图像帧,以从预训练的卷积神经网络的最后全连接层中提取特征。 我们使用在 ImageNet 上预训练的VGG16
卷积神经网络。 我们将激活从VGG16
的最后一个全连接层中取出。 由于VGG16
的最后一个全连接层具有4096
单元,因此每个时间步t
的特征向量f[t]
为4096
维向量,f[t] ∈ R^4096
。
在通过VGG16
处理视频中的图像之前,需要从视频中对其进行采样。 我们从视频中采样图像,使每个视频具有80
帧。 处理来自VGG16
的80
图像帧后,每个视频将具有80
特征向量f[1], f[2], ..., f[80]
。 这些特征将被馈送到 LSTM 以生成文本序列。 我们在 Keras 中使用了预训练的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
工具的更多信息,请参考以下链接。
在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
的特征向量 ]。
图像读取和调整大小函数load_image
定义为在馈入 CNN 之前处理原始ffmpeg
图像,其定义如下:
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
该预处理步骤的输出是大小为4096
的80
特征向量,被写为扩展名npy
的 numpy 数组对象。 每个视频将在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]
组合记录,并将它们视为测试文件,以便稍后进行评估:
图 5.5:字幕文件格式的快照
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
函数的连接)并在特征目录名称前添加前缀来形成视频特征的链接。 然后,我们删除所有未指向features
目录中任何实际视频特征向量或具有无效非文本描述的视频语料库文件记录。
数据如下图所示输出(“图 5.6”):
图 5.6:预处理后的字幕数据
建立训练和测试数据集
训练模型后,我们想评估模型的运行情况。 我们可以根据测试集中的视频内容验证为测试数据集生成的字幕。 可以使用以下函数创建训练测试集数据集。 我们可以在训练期间创建测试数据集,并在训练模型后将其用于评估:
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% 的数据用于评估是一种公平的做法。
建立模型
在本节中,说明了核心模型构建练习。 我们首先为文本标题的词汇表中的单词定义一个嵌入层,然后是两个 LSTM。 权重self.encode_W
和self.encode_b
用于减少卷积神经网络中特征f[t]
的大小。 对于第二个 LSTM(LSTM 2),在任何时间步长t > N
处的其他输入之一是前一个单词w[t-1]
,以及来自第一个 LSTM(LSTM 1)的输出h[t]
。 将w[t-1]
的词嵌入送入 LSTM 2,而不是原始的单热编码向量。 对于前N
个(self.video_lstm_step)
,LSTM 1 处理来自 CNN 的输入特征f[t]
,并且输出的隐藏状态h[t]
(输出 1)被馈送到 LSTM2。在此编码阶段,LSTM 2 不会接收任何单词w[t-1]
作为输入。
从N + 1
时间步长,我们进入解码阶段,在此阶段,连同来自 LSTM 1 的h[t]
(输出 1),先前的时间步的词嵌入向量w[t-1]
被馈送到 LSTM2。在这一阶段,没有到 LSTM 1 的输入,因为所有特征f[t]
在时间步N
耗尽。 解码阶段的时间步长由self.caption_lstm_step
确定。
现在,如果我们用函数f[2]
表示 LSTM 2 的活动,则f[2](h[t], w[t-1]) = h[2t]
,其中h[2t]
是 LSTM 2 在时间步t
的隐藏状态。 该隐藏状态h[2t]
在时间t
时,通过 softmax 函数转换为输出单词的概率分布, 选择最高概率的单词作为下一个单词o_hat[t]
:
=
这些权重W[ho]
和b
在以下代码块中定义为self.word_emb_W
和self.word_emb_b
。 有关更多详细信息,请参考build_model
函数。 为了方便解释,构建函数分为三部分。 构建模型有 3 个主要单元
- 定义阶段:定义变量,标题词的嵌入层和序列到序列模型的两个 LSTM。
- 编码阶段:在这一阶段中,我们将视频帧图像特征传递给 LSTM1 的时间步长,并将每个时间步长的隐藏状态传递给 LSTM2。此活动一直进行到时间步长
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)
解码阶段
在解码阶段,产生用于视频字幕的单词。 没有更多输入到 LSTM1。但是 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 停止发出任何其他单词。
为了举例说明,我们以句子The weather is beautiful
。 以下是从时间步N + 1
开始的 LSTM 2 的输入和输出标签:
时间步长 | 输入 | 输出 |
---|---|---|
N + 1 |
<bos> , h[N + 1] |
The |
N + 2 |
The ,h[N + 2] |
weather |
N + 3 |
weather ,h[N + 3] |
is |
N + 4 |
is ,h[N + 4] |
beautiful |
N + 5 |
beautiful ,h[N + 5] |
<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
训练模型
在本节中,我们将所有部分放在一起以构建用于训练视频字幕模型的函数。
首先,我们结合训练和测试数据集中的视频字幕,创建单词词汇词典。 完成此操作后,我们将结合两个 LSTM 调用build_model
函数来创建视频字幕网络。 对于每个带有特定开始和结束的视频,都有多个输出视频字幕。 在每个批量中,从开始和结束的特定视频的输出视频字幕是从多个可用的视频字幕中随机选择的。 调整 LSTM 2 的输入文本标题,使其在时间步N + 1
处的起始词为<bos>
,而输出文本标题的结束词被调整为最终文本标签<eos>
。 每个时间步长上的分类交叉熵损失之和被视为特定视频的总交叉熵损失。 在每个时间步中,我们计算完整单词词汇上的分类交叉熵损失,可以表示为:
此处,y^(t) = [y1^(t), y2^(t), ..., y[V]^(t)]
是时间步长t
时实际目标单词的单热编码向量,而p^(t) = [p1^(t), p2^(t), ..., p[V]^(t)]
是模型预测的概率向量。
在训练期间的每个周期都会记录损失,以了解损失减少的性质。 这里要注意的另一件事是,我们正在使用 TensorFlow 的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”):
图 5.7 训练期间的损失概况
从前面的图表(“图 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
定义模型以及推理所需的其他 TensorFlow 操作,然后使用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
的推断结果:
使用经过训练的模型对视频0lh_UWF9ZP4_82_87.avi
进行推断
在以下屏幕截图中,我们说明了对另一个video8MVo7fje_oE_139_144.avi
的推断结果:
使用训练后的模型推断视频/8MVo7fje_oE_139_144.avi
从前面的屏幕截图中,我们可以看到训练后的模型为提供的测试视频提供了很好的字幕,表现出色。
该项目的代码可以在 GitHub 中找到。 VideoCaptioningPreProcessing.py
模块可用于预处理视频并创建卷积神经网络特征,而Video_seq2seq.py
模块可用于训练端到端视频字幕系统和运行推断。
总结
现在,我们已经完成了令人兴奋的视频字幕项目的结尾。 您应该能够使用 TensorFlow 和 Keras 构建自己的视频字幕系统。 您还应该能够使用本章中介绍的技术知识来开发其他涉及卷积神经网络和循环神经网络的高级模型。 下一章将使用受限玻尔兹曼机构建智能的推荐系统。 期待您的参与!
标签:项目,Python,self,图像,智能,decoder,tf,model,data From: https://www.cnblogs.com/apachecn/p/17324242.html