Keras-神经网络秘籍-全-
Keras 神经网络秘籍(全)
原文:
annas-archive.org/md5/b038bef44808c012b36120474e9e0841译者:飞龙
前言
深度学习在快速进步,无论是神经网络架构本身,还是它们在现实世界应用中的实际应用。本书将引导你从构建神经网络的基础开始,到多个先进架构的开发,这些架构广泛应用于各种实际应用中。你会发现,本书分为五个部分。
在第一部分,你将通过从零开始在 Python 中构建神经网络的组件,来学习神经网络的运作方式,然后再在 Keras 中进行构建。此外,你将学习不同超参数对网络准确性的影响,并了解如何灵活地将神经网络应用于多个领域的不同应用。
在第二部分,你将学习如何从零开始在 Python 中构建卷积神经网络(CNN),然后将其应用于图像分类,在此过程中你将学习如何构建一个模型来检测图像中人物的性别,并识别人脸图像中的关键点。此外,你还将学习迁移学习在物体检测和定位中的应用,通过这些技术来分类图像中的物体,并识别图像中人物的位置。此外,你还将学习图像分析在自动驾驶汽车中的各种应用,利用语义分割等技术。
在第三部分,我们将从图像分析转向文本分析,学习如何对图像和文本数据进行编码,以便使用自编码器和词向量分别将相似的图像和文本聚合在一起。此外,你将学习构建推荐系统的各种建模方法,以便为用户推荐相关的电影。你还将学习如何利用生成对抗网络(GANs)生成新图像,以及生成艺术图像,同时学习如何通过对抗性攻击来欺骗网络。
在第四部分,你将深入探讨文本分析,在这里你将学习如何从零开始用 Python 构建递归神经网络(RNNs)和长短期记忆(LSTM)网络,并进一步学习如何构建多个利用文本分析的应用案例,如股票价格预测、情感分类、机器翻译以及构建聊天机器人,使用的高级神经网络架构包括双向 LSTM 和注意力机制。
在最后一部分,你将学习端到端学习,在这一部分你将进行图像和音频的转录以及生成字幕。此外,你还将学习深度 Q 学习,你将构建代理来玩各种 Atari 游戏。
到本书结束时,你将掌握能够将各种深度学习架构应用于你可能遇到的大部分深度学习问题的技能。
本书适合人群
本书适合初学者和中级机器学习从业者及数据科学家,特别是那些刚刚开始接触神经网络,并寻找一份能引导他们了解神经网络不同架构的资源的人。本书的案例研究按问题的复杂度排序,最简单的在最前面。只要具备基本的 Python 编程知识和对基础机器学习的了解,你就能顺利开始本书的学习。
为了最大限度地从本书中获得收益
- 你应该具备 Keras 的基础知识,并且熟悉 Python,且对机器学习有基本了解。另外,在阅读书中的示例时,请参考 GitHub 上的代码文件。我们已确保书中的所有代码正确缩进,但强烈建议你在实现代码时,参考 GitHub 上的代码。
下载示例代码文件
你可以从 www.packt.com 的账户中下载本书的示例代码文件。如果你在其他地方购买了本书,可以访问 www.packt.com/support 并注册,以便直接通过电子邮件获取文件。
你可以通过以下步骤下载代码文件:
- 
登录或注册 www.packt.com。 
- 
选择 SUPPORT 标签。 
- 
点击“代码下载与勘误”。 
- 
在搜索框中输入书名,并按照屏幕上的指示操作。 
文件下载后,请确保使用最新版本解压或提取文件夹:
- 
Windows 的 WinRAR/7-Zip 
- 
Zipeg/iZip/UnRarX for Mac 
- 
7-Zip/PeaZip for Linux 
本书的代码包也托管在 GitHub 上,地址是github.com/PacktPublishing/Neural-Networks-with-Keras-Cookbook。如果代码有更新,它将会在现有的 GitHub 仓库中更新。
我们还提供了来自丰富书籍和视频目录中的其他代码包,地址是github.com/PacktPublishing/。快去看看吧!
下载彩色图片
我们还提供了一个 PDF 文件,包含本书中使用的截图/图表的彩色图像。你可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781789346640_ColorImages.pdf.
使用的约定
本书中使用了若干文本约定。
CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:"名为 Defaultin2yrs 的变量是我们需要预测的输出变量。"
代码块的格式如下:
data['DebtRatio_newoutlier']=np.where(data['DebtRatio']>1,1,0)
data['DebtRatio']=np.where(data['DebtRatio']>1,1,data['DebtRatio'])
粗体:表示新术语、重要单词或屏幕上显示的单词。
警告或重要说明将以这样的形式出现。
小贴士和技巧将以这样的形式出现。
各节
在本书中,您将会看到几个经常出现的标题(准备工作、如何操作...、它是如何工作的...、还有更多... 和 另见)。
为了提供清晰的操作步骤,请按照以下方式使用这些章节:
准备工作
本节告知您在食谱中可以期待什么,并描述如何设置任何软件或食谱所需的初步设置。
如何操作…
本节包含遵循此食谱所需的步骤。
它是如何工作的…
本节通常包括对前一节内容的详细解释。
还有更多…
本节包含关于食谱的额外信息,帮助您更深入了解食谱。
另见
本节提供了其他有用信息的链接,帮助您更好地理解食谱。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何部分有疑问,请在邮件主题中提到书名,并通过电子邮件 customercare@packtpub.com 与我们联系。
勘误表:尽管我们已尽一切努力确保内容的准确性,但错误难免。如果您在本书中发现错误,烦请向我们报告。请访问 www.packt.com/submit-errata,选择您的书籍,点击勘误表提交表单链接并填写相关细节。
盗版:如果您在互联网上发现我们作品的任何非法复制版本,烦请提供该位置地址或网站名称。请通过电子邮件 copyright@packt.com 并附上材料链接与我们联系。
如果您有意成为作者:如果您在某个领域具有专业知识,并且有兴趣写书或为书籍做贡献,请访问 authors.packtpub.com。
评论
请留下评论。读完并使用本书后,为什么不在您购买书籍的网站上留下评论呢?潜在读者可以查看并根据您的公正意见做出购买决策,我们 Packt 能了解您对我们产品的看法,作者也能看到您对他们书籍的反馈。谢谢!
欲了解更多关于 Packt 的信息,请访问 packt.com。
第一章:构建前馈神经网络
在本章中,我们将覆盖以下内容:
- 
在 Python 中从零开始实现前馈传播 
- 
在 Python 中从零开始构建反向传播 
- 
在 Keras 中构建神经网络 
介绍
神经网络是一种监督学习算法,灵感来源于大脑的功能方式。与大脑中神经元之间的连接方式相似,神经网络接收输入,通过一个函数进行处理,某些后续神经元被激活,最终产生输出。
在本章中,您将学习以下内容:
- 
神经网络的架构 
- 
神经网络的应用 
- 
设置前馈神经网络 
- 
前向传播是如何工作的 
- 
计算损失值 
- 
梯度下降在反向传播中的工作原理 
- 
训练轮次(epochs)和批量大小(batch size)的概念 
- 
各种损失函数 
- 
各种激活函数 
- 
从零开始构建神经网络 
- 
在 Keras 中构建神经网络 
简单神经网络的架构
人工神经网络的灵感来源于人脑的工作方式。技术上,它是线性回归和逻辑回归的改进,因为神经网络引入了多个非线性度量来估计输出。此外,神经网络在修改网络架构方面提供了极大的灵活性,可以通过利用结构化和非结构化数据解决多个领域的问题。
函数越复杂,网络越有可能调整以适应输入数据,从而提高预测的准确性。
一个典型的前馈神经网络结构如下所示:

一层是一个或多个节点(计算单元)的集合,每个层中的节点都与下一层中的每个节点连接。输入层由预测输出值所需的输入变量组成。
输出层的节点数量取决于我们是尝试预测连续变量还是分类变量。如果输出是连续变量,则输出只有一个单元。
如果输出是分类的,并且有 n 个可能的类别,则输出层将有 n 个节点。隐藏层用于将输入层的值转换为高维空间中的值,以便我们可以从输入中学习更多的特征。隐藏层将输出转换如下:

在上面的图中,x[1],x[2],...,x[n] 是独立变量,而 x[0] 是偏置项(类似于线性/逻辑回归中的偏置)。
注意,w[1],w[2],...,w[n] 是分配给每个输入变量的权重。如果 a 是隐藏层中的一个单元,它将等于以下值:

f 函数是激活函数,用于在输入与其对应权重值的乘积和之上应用非线性。此外,通过增加多个隐藏层,可以实现更高的非线性。
总结来说,神经网络是一个由权重分配给节点并由层连接的集合。这个集合分为三大部分:输入层、隐藏层和输出层。请注意,你可以有 n 个隐藏层,深度学习的概念通常意味着有多个隐藏层。当神经网络需要理解一些非常复杂、具有上下文或不明显的内容时,如图像识别,隐藏层是必需的。中间层(不是输入层或输出层的层)被称为隐藏层,因为它们在实践中是不可见的(关于如何可视化中间层的内容可以参考第四章,构建深度卷积神经网络)。
训练神经网络
训练神经网络基本上意味着通过重复两个关键步骤:正向传播和反向传播,来校准神经网络中的所有权重。
在正向传播中,我们将一组权重应用到输入数据,经过隐藏层,进行非线性激活,最后将隐藏层连接到输出层,通过将隐藏层节点值与另一组权重相乘来完成。对于第一次正向传播,权重的值是随机初始化的。
在反向传播中,我们通过测量输出的误差范围来尝试减少误差,并相应地调整权重。神经网络重复进行正向传播和反向传播,直到权重得到校准,从而预测输出。
神经网络的应用
最近,我们已经看到神经网络在各种应用中的广泛采用。在这一部分中,我们将尝试理解为什么这种采用可能大幅增加。神经网络可以通过多种方式进行架构设计。以下是一些可能的方式:

底部的框是输入,接下来是隐藏层(中间的框),顶部的框是输出层。一对一架构是典型的神经网络,输入层与输出层之间有一个隐藏层。不同架构的示例如下:
| 架构 | 示例 | 
|---|---|
| 一对多 | 输入是图像,输出是该图像的标题 | 
| 多对一 | 输入是电影评论(多个词),输出是与评论相关的情感 | 
| 多对多 | 将一个语言中的句子翻译成另一种语言中的句子 | 
除了上述要点,神经网络还能够理解图像中的内容,并利用一种名为卷积神经网络(CNN)的架构,检测内容所在的位置,其架构如下所示:

在这里,我们看到了推荐系统、图像分析、文本分析和音频分析的示例,可以看到神经网络为我们提供了灵活性,使我们能够使用多种架构来解决问题,随着应用数量的增加,神经网络的采用率也在上升。
从头开始实现前向传播(Feed-forward propagation)——Python 实现
为了建立前向传播工作的坚实基础,我们将通过一个训练神经网络的玩具示例来进行讲解,其中神经网络的输入为(1, 1),对应的输出为 0。
准备工作
我们将采用的策略如下:我们的神经网络将有一个隐藏层(包含神经元),该隐藏层连接输入层与输出层。请注意,隐藏层中的神经元数量比输入层多,因为我们希望让输入层在更多维度上得到表示:

计算隐藏层单元值
我们现在为所有连接分配权重。请注意,这些权重是随机选择的(基于高斯分布),因为这是我们第一次进行前向传播。在这个特定的例子中,我们从初始权重开始,权重范围在 0 和 1 之间,但请注意,神经网络训练过程中的最终权重不需要介于特定的数值范围之间:

在下一步中,我们执行输入与权重的乘法运算,以计算隐藏层中隐藏单元的值。
隐藏层的单元值如下所示:



隐藏层的单元值在下面的图示中也有显示:

请注意,在前面的输出中,我们计算了隐藏层的值。为了简化起见,我们忽略了需要在每个隐藏层单元中添加的偏置项。
现在,我们将通过激活函数将隐藏层值传递,以便在输出中获得非线性。
如果我们不在隐藏层中应用激活函数,神经网络将变成一个从输入到输出的巨大线性连接。
应用激活函数
激活函数在网络的多个层中应用。它们的作用是使输入具有较高的非线性,这在建模输入和输出之间复杂关系时非常有用。
不同的激活函数如下所示:

对于我们的示例,假设使用 sigmoid 函数作为激活函数。Sigmoid 函数的图形如下所示:

通过对三个隐藏层的总和应用 sigmoid 激活函数 S(x),我们得到如下结果:
final_h[1] = S(1.0) = 0.73
final_h[2] = S(1.3) = 0.78
final_h[3] = S(0.8) = 0.69
计算输出层值
现在我们已经计算出隐藏层的值,接下来我们将计算输出层的值。在以下图示中,隐藏层的值通过随机初始化的权重值与输出层相连。通过使用隐藏层的值和权重值,我们将计算以下网络的输出值:

我们通过将隐藏层值与权重值做点积来计算输出值。为了简化计算,我们省略了每个隐藏层单元中需要添加的偏置项:
0.73 * 0.3 + 0.79 * 0.5 + 0.69 * 0.9 = 1.235
这些值在以下图示中显示:

因为我们一开始使用的是随机初始化的权重,所以输出神经元的值与目标值差异很大,在这个例子中相差+1.235(因为目标值为 0)。
计算损失值
损失值(也称为成本函数)是我们在神经网络中优化的值。为了理解损失值是如何计算的,让我们来看两个场景:
- 
连续变量预测 
- 
类别变量预测 
在连续变量预测中计算损失
通常,当变量是连续型时,损失值通过平方误差计算,即我们通过调整与神经网络相关的权重值来最小化均方误差:

在前面的公式中,y(i) 是实际的输出值,h(x) 是我们对输入(x)进行变换后得到的预测值 y,而 m 是数据集中的行数。
在类别变量预测中计算损失
当需要预测的变量是离散型时(即变量只有少数几类),我们通常使用类别交叉熵损失函数。当需要预测的变量只有两个不同的值时,损失函数为二元交叉熵,而当需要预测的变量有多个不同的值时,损失函数为类别交叉熵。
这里是二元交叉熵:
(ylog(p)+(1−y)log(1−p))
这里是类别交叉熵:

y 是输出的实际值,p 是预测的输出值,n 是数据点的总数。现在,假设我们在玩具示例中预测的结果是连续的。在这种情况下,损失函数值是均方误差,计算公式如下:
error = 1.235² = 1.52
在下一步中,我们将尝试使用反向传播来最小化损失函数值(我们将在下一节学习),在反向传播中,我们更新权重值(之前随机初始化的)以最小化损失(误差)。
如何执行...
在前面的部分中,我们学习了如何对输入数据执行以下步骤,以便在前向传播过程中计算误差值(代码文件可在 GitHub 上的 Neural_network_working_details.ipynb 找到):
- 
随机初始化权重 
- 
通过将输入值与权重相乘来计算隐藏层单元值 
- 
对隐藏层的值执行激活 
- 
将隐藏层的值连接到输出层 
- 
计算平方误差损失 
用于计算所有数据点的平方误差损失值的函数如下:
import numpy as np
def feed_forward(inputs, outputs, weights):
     pre_hidden = np.dot(inputs,weights[0])+ weights[1]
     hidden = 1/(1+np.exp(-pre_hidden))
     out = np.dot(hidden, weights[2]) + weights[3]
     squared_error = (np.square(pred_out - outputs))
     return squared_error
在前面的函数中,我们将输入变量值、权重(如果这是第一次迭代则随机初始化)以及提供的数据集中的实际输出作为输入传递给前馈函数。
我们通过执行输入和权重的矩阵乘法(点积)来计算隐藏层的值。此外,我们还会在隐藏层中加上偏置值,如下所示:
pre_hidden = np.dot(inputs,weights[0])+ weights[1]
前述场景适用于weights[0]为权重值,weights[1]为偏置值,权重和偏置连接输入层与隐藏层。
一旦计算出隐藏层的值,我们会对隐藏层的值进行激活,计算方法如下:
hidden = 1/(1+np.exp(-pre_hidden))
现在,我们通过将隐藏层的输出与连接隐藏层和输出的权重相乘,然后在输出处添加偏差项,来计算隐藏层的输出,如下所示:
pred_out = np.dot(hidden, weights[2]) + weights[3]
一旦输出被计算出来,我们会在每一行中计算平方误差损失,计算公式如下:
squared_error = (np.square(pred_out - outputs))
在前面的代码中,pred_out 是预测的输出,outputs 是实际的输出。
然后,我们可以在通过网络进行前向传播时获得损失值。
虽然在前面的代码中,我们对隐藏层的值考虑了 Sigmoid 激活函数,但现在让我们看看其他常用的激活函数。
Tanh
tanh 激活值(隐藏层单元值)的计算如下:
def tanh(x):
    return (exp(x)-exp(-x))/(exp(x)+exp(-x))
ReLu
修正线性单元(ReLU)的值(隐藏层单元值)计算如下:
def relu(x):
    return np.where(x>0,x,0)
Linear
线性激活值就是该值本身。
Softmax
通常,Softmax 会应用于一组值的向量。这通常是为了确定输入属于给定场景中 n 个可能输出类别之一的概率。假设我们正在尝试将数字图像分类为 10 个可能的类别(0 到 9 的数字)。在这种情况下,有 10 个输出值,每个输出值应代表输入图像属于其中一个类别的概率。
Softmax 激活函数用于为输出中的每个类别提供一个概率值,计算方法将在以下部分中解释:
def softmax(x):
    return np.exp(x)/np.sum(np.exp(x))
除了前面提到的激活函数,构建神经网络时通常使用的损失函数如下:
均方误差
错误是输出的实际值和预测值之间的差异。我们对误差取平方,因为误差可能是正值或负值(当预测值大于实际值时,反之亦然)。平方确保正负误差不会相互抵消。我们计算均方误差,以便在两个数据集大小不相同的情况下,可以比较这两个数据集上的误差。
预测值(p)和实际值(y)之间的均方误差计算公式如下:
def mse(p, y):
    return np.mean(np.square(p - y))
均方误差通常用于预测本质上是连续的值。
平均绝对误差
平均绝对误差的工作原理与均方误差非常相似。平均绝对误差通过对所有数据点的实际值和预测值之间的绝对差异取平均值,确保正负误差不会相互抵消。
预测值(p)和实际值(y)之间的平均绝对误差实现公式如下:
def mae(p, y):
    return np.mean(np.abs(p-y))
类似于均方误差,平均绝对误差通常应用于连续变量。
类别交叉熵
交叉熵是衡量两个不同分布之间差异的指标:实际分布和预测分布。与我们讨论的前两种损失函数不同,交叉熵应用于类别输出数据。
两个分布之间的交叉熵计算公式如下:

y 是事件的实际结果,p 是事件的预测结果。
预测值(p)和实际值(y)之间的类别交叉熵实现公式如下:
def cat_cross_entropy(p, y):
     return -np.sum((y*np.log2(p)+(1-y)*np.log2(1-p)))
请注意,当预测值远离实际值时,类别交叉熵损失值较高;而当值接近时,损失值较低。
从头开始用 Python 构建反向传播
在前向传播中,我们将输入层连接到隐藏层,再连接到输出层。在反向传播中,我们采取相反的方式。
准备工作
我们会逐个调整神经网络中的每个权重,幅度很小。权重值的变化会影响最终的损失值(可能是增加或减少损失)。我们会在减少损失的方向上更新权重。
此外,在某些情况下,权重的小幅变化会导致误差大幅增加/减少,而在某些情况下误差变化较小。
通过小幅度更新权重并衡量更新后误差的变化,我们可以做到以下几点:
- 
确定权重更新的方向 
- 
确定权重更新的大小 
在实现反向传播之前,让我们理解神经网络的一个额外细节:学习率。
直观地说,学习率帮助我们建立对算法的信任。例如,在决定权重更新的幅度时,我们可能不会一次性改变很大,而是采取更谨慎的方式,慢慢地更新权重。
这使我们的模型得到稳定;我们将在下一章中讨论学习率如何帮助稳定性。
我们更新权重以减少误差的整个过程叫做梯度下降技术。
随机梯度下降是前述场景中最小化误差的方法。更直观地说,梯度代表差异(即实际值与预测值之间的差异),而下降意味着减少。随机表示基于一定数量的随机样本选择来做出决策。
除了随机梯度下降,还有许多其他优化技术可以帮助优化损失值;不同的优化技术将在下一章中讨论。
反向传播的工作流程如下:
- 
计算正向传播过程中的整体成本函数。 
- 
将所有权重(逐个)按小幅度变化。 
- 
计算权重变化对成本函数的影响。 
- 
根据变化是否增加或减少了成本(损失)值,它会更新权重值,朝着减少损失的方向进行调整。然后,这一步骤会在所有权重上重复进行。 
如果前述步骤执行了n次,它实际上会产生n 迭代次数。
为了进一步巩固我们对神经网络中反向传播的理解,让我们从已知函数开始,看看如何推导出权重:
目前,我们将使用已知函数y = 2x,我们尝试找出权重值和偏置值,在这个特定的情况下分别为 2 和 0:
| x | y | 
|---|---|
| 1 | 2 | 
| 2 | 4 | 
| 3 | 6 | 
| 4 | 8 | 
如果我们将之前的数据集公式化为线性回归,(y = ax+b),其中我们要计算a和b的值(我们已经知道它们是 2 和 0,但正在检查如何通过梯度下降获得这些值),我们可以将a和b*的参数随机初始化为 1.477 和 0(理想值是 2 和 0)。
如何实现...
在这一部分,我们将手动构建反向传播算法,以便我们清楚地理解在神经网络中如何计算权重。在这个特定的案例中,我们将构建一个没有隐藏层的简单神经网络(因此我们正在解决一个回归方程)。代码文件可以在 GitHub 上的Neural_network_working_details.ipynb中找到。
- 初始化数据集如下:
x = [[1],[2],[3],[4]]
y = [[2],[4],[6],[8]]
- 随机初始化权重和偏置值(因为我们只需要一个权重和一个偏置值,因为我们要找出方程y = ax + b中a和b*的最优值):
w = [[[1.477867]], [0.]]
- 定义前馈网络并计算平方误差损失值:
import numpy as np
def feed_forward(inputs, outputs, weights):
     out = np.dot(inputs,weights[0]) + weights[1]
     squared_error = (np.square(out - outputs))
     return squared_error
在之前的代码中,我们进行了输入与随机初始化的权重值的矩阵乘法,并将其与随机初始化的偏置值相加。
一旦计算出值,我们将计算实际值与预测值之间的平方误差。
- 将每个权重和偏置值增加一个非常小的量(0.0001),并逐一计算每个权重和偏置更新的平方误差损失值。
如果平方误差损失值随着权重增加而减少,则应该增加权重值。权重值增加的幅度应与权重变化所减少的损失值的数量成正比。
此外,确保不会像权重变化所引起的损失减少那样增加权重值,而是通过一个叫做学习率的因子来调整它。这样可以确保损失值更平滑地减少(下一章会详细讲解学习率如何影响模型准确度)。
在以下代码中,我们创建了一个名为update_weights的函数,它执行反向传播过程,以更新在步骤 3中获得的权重。我们还提到该函数需要运行epochs次(其中epochs是我们传递给update_weights函数的参数):
def update_weights(inputs, outputs, weights, epochs): 
     for epoch in range(epochs):
- 将输入通过前馈网络传递,以计算使用初始权重集的损失值:
        org_loss = feed_forward(inputs, outputs, weights)
- 确保你对权重列表进行deepcopy,因为在后续步骤中权重会被操作,因此deepcopy可以避免由于子变量的变化影响到它指向的父变量的问题:
        wts_tmp = deepcopy(weights)
        wts_tmp2 = deepcopy(weights)
- 一次遍历所有权重值,并对其进行微小的变化(0.0001):
        for i in range(len(weights)):
             wts_tmp[-(i+1)] += 0.0001
- 当权重被小幅更新时,计算更新后的前馈损失。计算由于输入的小变化所导致的损失变化。将损失变化除以输入的数量,因为我们希望计算所有输入样本的均方误差:
            loss = feed_forward(inputs, outputs, wts_tmp)
            delta_loss = np.sum(org_loss - loss)/(0.0001*len(inputs))
通过小幅更新权重并计算其对损失值的影响,相当于对权重变化执行导数操作。
- 根据权重导致的损失变化来更新权重。通过将损失变化乘以一个非常小的数字(0.01),即学习率参数(关于学习率参数的更多内容请参见下一章),以缓慢更新权重:
            wts_tmp2[-(i+1)] += delta_loss*0.01 
            wts_tmp = deepcopy(weights)
- 返回更新后的权重和偏置值:
    weights = deepcopy(wts_tmp2)
 return wts_tmp2
神经网络中的另一个参数是计算损失值时考虑的批处理大小。
在前面的场景中,我们考虑了所有数据点来计算损失值。然而,在实际应用中,当我们有成千上万(或在某些情况下,数百万)个数据点时,在计算损失值时,更多数据点的增量贡献将遵循递减回报法则,因此我们使用的批处理大小会比总数据点数量小得多。
在构建模型时,通常考虑的批处理大小范围是 32 到 1,024 之间。
还有更多内容...
在前一节中,我们构建了一个回归公式(Y = ax + b),并编写了一个函数来识别a和b*的最优值。在本节中,我们将在相同的玩具数据集上构建一个简单的神经网络,隐藏层将输入连接到输出层。
我们定义模型如下(代码文件可以在 GitHub 上找到,文件名为Neural_networks_multiple_layers.ipynb):
- 
输入层连接到隐藏层,隐藏层有三个单元。 
- 
隐藏层连接到输出层,输出层有一个单元。 
让我们继续编写上述讨论的策略,代码如下:
- 定义数据集并导入相关包:
from copy import deepcopy
import numpy as np
x = [[1],[2],[3],[4]]
y = [[2],[4],[6],[8]]
我们使用deepcopy,这样在复制原始变量值到目标变量后,即使目标变量的值发生变化,原始变量的值也不会改变。
- 随机初始化权重和偏置值。隐藏层中有三个单元,因此总共有三个权重值和三个偏置值——每个都对应一个隐藏单元。
此外,最终层有一个单元,与隐藏层的三个单元相连接。因此,共有三个权重和一个偏置值决定输出层的值。
随机初始化的权重如下:
w = [[[-0.82203424, -0.9185806 , 0.03494298]], [0., 0., 0.], [[ 1.0692896 ],[ 0.62761235],[-0.5426246 ]], [0]]
- 实现一个前馈网络,其中隐藏层使用 ReLU 激活函数:
def feed_forward(inputs, outputs, weights):
     pre_hidden = np.dot(inputs,weights[0])+ weights[1]
     hidden = np.where(pre_hidden<0, 0, pre_hidden) 
     out = np.dot(hidden, weights[2]) + weights[3]
     squared_error = (np.square(out - outputs))
     return squared_error
- 像前一节那样定义反向传播函数。唯一的区别是,我们现在需要更新更多层中的权重。
在以下代码中,我们正在计算一个时期开始时的原始损失:
def update_weights(inputs, outputs, weights, epochs): 
     for epoch in range(epochs):
         org_loss = feed_forward(inputs, outputs, weights)
在以下代码中,我们将权重复制到两个权重变量集,以便在后续代码中重用它们:
        wts_new = deepcopy(weights)
        wts_new2 = deepcopy(weights)
在以下代码中,我们通过少量更新每个权重值,并计算与更新后权重值对应的损失值(同时保持其他所有权重不变)。此外,我们还确保在所有权重和所有网络层中都发生权重更新。
平方损失(del_loss)的变化归因于权重值的变化。我们对网络中所有存在的权重重复前述步骤:
         for i, layer in enumerate(reversed(weights)):
            for index, weight in np.ndenumerate(layer):
                wts_tmp[-(i+1)][index] += 0.0001
                loss = feed_forward(inputs, outputs, wts_tmp)
                del_loss = np.sum(org_loss - loss)/(0.0001*len(inputs))
权重值通过学习率参数进行加权更新——损失减少较大时,权重会大幅更新,而损失减少较小时,权重会少量更新:
               wts_tmp2[-(i+1)][index] += del_loss*0.01
               wts_tmp = deepcopy(weights)
鉴于权重值是逐个更新的,以估计它们对损失值的影响,因此存在对权重更新过程进行并行化的潜力。因此,在这种情况下,GPU 非常有用,因为它们比 CPU 有更多的核心,能够在相同时间内更新更多的权重。
最后,我们返回更新后的权重:
          weights = deepcopy(wts_tmp2)
 return wts_tmp2
- 运行函数多次,每次更新一次权重:
update_weights(x,y,w,1)
前述代码的输出(更新后的权重)如下:

在前述步骤中,我们学习了如何在 Python 中从零开始构建神经网络。在下一部分中,我们将学习如何在 Keras 中构建神经网络。
在 Keras 中构建神经网络
在上一部分中,我们从零开始构建了一个神经网络,也就是说,我们编写了执行前向传播和反向传播的函数。
如何实现...
我们将使用 Keras 库构建一个神经网络,该库提供了使构建复杂神经网络过程更容易的工具。
安装 Keras
Tensorflow 和 Keras 在 Ubuntu 中实现,使用以下命令:
$pip install --no-cache-dir tensorflow-gpu==1.7
请注意,最好安装一个兼容 GPU 的版本,因为神经网络在 GPU 上运行时速度要快得多。Keras 是一个高层神经网络 API,用 Python 编写,能够在 TensorFlow、CNTK 或 Theano 之上运行。
它的开发重点是支持快速实验,可以通过以下方式安装:
$pip install keras
在 Keras 中构建我们的第一个模型
在这一部分中,让我们通过使用在前面部分中使用的相同玩具数据集(代码文件在 GitHub 上以Neural_networks_multiple_layers.ipynb提供)来理解在 Keras 中构建模型的过程:
- 实例化一个模型,可以顺序调用它以便在其上添加更多的层。Sequential方法使我们能够执行模型初始化操作:
from keras.models import Sequential
model = Sequential()
- 向模型添加全连接层。全连接层确保模型中各层之间的连接。在以下代码中,我们将输入层与隐藏层连接:
model.add(Dense(3, activation='relu', input_shape=(1,)))
在前面的代码初始化的全连接层中,我们确保为模型提供了输入形状(我们需要指定模型预期的数据形状,因为这是第一个全连接层)。
此外,我们提到,每个输入将与三个单元(隐藏层中的三个单元)连接,并且在隐藏层中需要执行的激活函数是 ReLU 激活函数。
- 将隐藏层与输出层连接:
model.add(Dense(1, activation='linear'))
请注意,在这个全连接层中,我们不需要指定输入形状,因为模型会根据前一层自动推断输入形状。
同时,考虑到每个输出是一维的,我们的输出层只有一个单元,且我们执行的激活函数是线性激活函数。
现在,模型的摘要可以如下可视化:
model.summary()
模型摘要如下:

前面的输出确认了我们在上一节中的讨论:从输入层到隐藏层的连接将有六个参数——三个权重和三个偏置项——对应三个隐藏单元的总共六个参数。此外,三个权重和一个偏置项将输入层与输出层连接。
- 编译模型。这确保我们定义了损失函数和优化器,用来减少损失函数,以及与优化器对应的学习率(我们将在下一章讨论不同的优化器和损失函数):
from keras.optimizers import sgd
sgd = sgd(lr = 0.01)
在前面的步骤中,我们指定了优化器为我们在上一节学习过的随机梯度下降法,学习率为 0.01。将预定义的优化器及其对应的学习率作为参数传递,并减少均方误差值:
model.compile(optimizer=sgd,loss='mean_squared_error')
- 拟合模型。更新权重,以便模型更好地拟合:
model.fit(np.array(x), np.array(y), epochs=1, batch_size = 4, verbose=1)
fit方法期望接收两个 NumPy 数组:一个输入数组和一个对应的输出数组。请注意,epochs表示数据集遍历的次数,batch_size表示在更新权重的迭代中需要考虑的数据点数量。此外,verbose指定输出的详细程度,包括训练和测试数据集中的损失信息以及模型训练过程的进展。
- 提取权重值。权重值的顺序是通过调用模型上权重方法获得的,如下所示:
model.weights
获取权重的顺序如下:

从前面的输出中,我们看到权重的顺序是dense_1层的三个权重(kernel)和三个偏置项(这是输入层与隐藏层之间的连接),以及dense_2层(输出层)之间的三个权重(kernel)和一个偏置项。
现在我们理解了权重值呈现的顺序,让我们提取这些权重的值:
model.get_weights()
请注意,权重以数组列表的形式呈现,其中每个数组对应于model.weights输出中指定的值。
上述代码的输出如下:

你应该注意到,我们在这里观察到的输出与我们在手动构建神经网络时获得的输出一致。
- 使用predict方法预测一组新输入的输出:
x1 = [[5],[6]]
model.predict(np.array(x1))
请注意,x1是一个变量,它保存新一组示例的值,针对这些示例,我们需要预测输出值。与fit方法类似,predict方法也期望接收一个数组作为输入。
上述代码的输出如下:

请注意,尽管前面的输出不正确,但我们运行 100 个 epoch 后的输出如下:

由于我们运行了更多的 epoch,前面的输出将与预期输出(即 10,12)一致。
第二章:构建深度前馈神经网络
在本章中,我们将涵盖以下内容:
- 
训练一个基础神经网络 
- 
对输入数据集进行缩放 
- 
当大多数输入值大于零时的训练影响 
- 
批量大小对模型准确性的影响 
- 
构建深度神经网络以提高网络准确性 
- 
改变学习率以提高网络准确性 
- 
改变损失优化器以提高网络准确性 
- 
理解过拟合的情境 
- 
使用批量归一化加速训练过程 
在上一章中,我们了解了神经网络的基本功能。我们还学习了有许多超参数会影响神经网络的准确性。在本章中,我们将详细探讨神经网络中各种超参数的功能。
本章的所有代码可以在 https://github.com/kishore-ayyadevara/Neural-Networks-with-Keras-Cookbook/blob/master/Neural_network_hyper_parameters.ipynb 找到
训练一个基础神经网络
为了理解如何训练一个基础神经网络,我们将通过预测 MNIST 数据集中数字标签的任务,来实现这一点。MNIST 是一个流行的数字图像数据集(每张图片包含一个数字),并且每个图像都有对应的标签。
做好准备
训练神经网络的步骤如下:
- 
导入相关的包和数据集 
- 
对目标进行预处理(将它们转换为独热编码向量),以便我们能够在其上进行优化: - 我们将最小化类别交叉熵损失
 
- 
创建训练集和测试集: - 
我们有训练数据集,因此我们可以基于它创建一个模型 
- 
测试数据集对模型是不可见的: - 因此,测试数据集上的准确性是衡量模型在实际应用中可能表现如何的一个指标,因为生产环境中的数据(可能是在构建模型几天/几周后出现的数据)是模型无法看到的
 
 
- 
- 
初始化一个模型 
- 
定义模型架构: - 
指定隐藏层中的单元数 
- 
指定在隐藏层中执行的激活函数 
- 
指定隐藏层的数量 
- 
指定我们想要最小化的损失函数 
- 
提供优化器,以最小化损失函数 
 
- 
- 
训练模型: - 
提到批量大小以更新权重 
- 
提到总的训练轮数 
 
- 
- 
测试模型: - 
提到验证数据,否则提到验证分割,这将把总数据的最后 x%作为测试数据 
- 
计算测试数据集上的准确率和损失值 
 
- 
- 
检查随着训练轮次增加,损失值和准确性值变化中是否有任何有趣的现象 
使用这种策略,让我们在接下来的部分中构建一个 Keras 神经网络模型。
如何做到这一点...
- 导入相关的包和数据集,并可视化输入数据集:
from keras.datasets import mnist
import numpy
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.utils import np_utils
(X_train, y_train), (X_test, y_test) = mnist.load_data()
在前面的代码中,我们导入了相关的 Keras 文件,并且还导入了 MNIST 数据集(该数据集作为 Keras 中的内置数据集提供)。
- MNIST 数据集包含数字图像,每张图像的形状为 28 x 28。我们来绘制一些图像,看看它们在代码中的样子:
import matplotlib.pyplot as plt
%matplotlib inline
plt.subplot(221)
plt.imshow(X_train[0], cmap=plt.get_cmap('gray'))
plt.grid('off')
plt.subplot(222)
plt.imshow(X_train[1], cmap=plt.get_cmap('gray'))
plt.grid('off')
plt.subplot(223)
plt.imshow(X_train[2], cmap=plt.get_cmap('gray'))
plt.grid('off')
plt.subplot(224)
plt.imshow(X_train[3], cmap=plt.get_cmap('gray'))
plt.grid('off')
plt.show()
以下截图显示了前面代码块的输出:

- 将 28 x 28 的图像展平,使得输入为所有 784 个像素值。此外,对输出进行 one-hot 编码。这一步骤在数据集准备过程中非常关键:
# flatten 28*28 images to a 784 vector for each image
num_pixels = X_train.shape[1] * X_train.shape[2]
X_train = X_train.reshape(X_train.shape[0], num_pixels).astype('float32')
X_test = X_test.reshape(X_test.shape[0], num_pixels).astype('float32')
在前面的步骤中,我们使用 reshape 方法对输入数据集进行重新调整形状,该方法将给定形状的数组转换为不同的形状。在这个特定的例子中,我们将具有 X_train.shape[0] 个数据点(图像)的数组,其中每个图像有 X_train.shape[1] 行和 X_train.shape[2] 列,转换为一个包含 X_train.shape[0] 个数据点(图像)和每个图像 X_train.shape[1] * X_train.shape[2] 个值的数组。类似地,我们对测试数据集执行相同的操作:
# one hot encode outputs
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)
num_classes = y_test.shape[1]
让我们尝试理解 one-hot 编码是如何工作的。如果唯一可能的标签是 {0, 1, 2, 3},它们将被 one-hot 编码,如下所示:
| Label | 0 | 1 | 2 | 3 | 
|---|---|---|---|---|
| 0 | 1 | 0 | 0 | 0 | 
| 1 | 0 | 1 | 0 | 0 | 
| 2 | 0 | 0 | 1 | 0 | 
| 3 | 0 | 0 | 0 | 1 | 
本质上,每个标签将占据数据集中的一个唯一列,如果标签存在,该列的值将为 1,其他所有列的值将为 0。
在 Keras 中,基于标签的 one-hot 编码是通过 to_categorical 方法实现的,该方法会计算目标数据中的唯一标签数,并将其转换为 one-hot 编码向量。
- 构建一个具有 1,000 个单元的隐藏层神经网络:
model = Sequential()
model.add(Dense(1000, input_dim=784, activation='relu'))
model.add(Dense(10,  activation='softmax'))
在前面的步骤中,我们提到输入有 784 个值,这些值连接到隐藏层中的 1,000 个值。此外,我们还指定了在输入与连接输入和隐藏层的权重矩阵相乘后,要在隐藏层执行的激活函数是 ReLu 激活函数。
最后,隐藏层与一个具有 10 个值的输出连接(因为 to_categorical 方法创建的向量有 10 列),并且我们在输出上执行 softmax,以便获得图像属于某一类别的概率。
- 前述的模型架构可以如下可视化:
model.summary()
模型的总结如下:

在前面的架构中,第一层的参数数量为 785,000,因为 784 个输入单元连接到 1,000 个隐藏单元,导致 784 * 1,000 个权重值,以及 1,000 个偏置值,最终得到 785,000 个参数。
类似地,输出层有 10 个输出,它们与每个 1,000 个隐藏层连接,从而产生 1,000 * 10 个参数和 10 个偏置——总共 10,010 个参数。
输出层有 10 个单元,因为输出中有 10 个可能的标签。输出层现在为每个类提供给定输入图像的概率值。
- 如下所示编译模型:
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
请注意,由于目标变量是一个包含多个类别的独热编码向量,因此损失函数将是分类交叉熵损失。
此外,我们使用 Adam 优化器来最小化代价函数(关于不同优化器的更多信息请参考通过改变损失优化器来提升网络准确性配方)。
我们还注意到,在模型训练过程中,我们需要查看准确率指标。
- 如下所示拟合模型:
history = model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=500, batch_size=32, verbose=1)
在前面的代码中,我们已指定模型将拟合的输入(X_train)和输出(y_train)。此外,我们还指定了测试数据集的输入和输出,模型不会使用这些数据来训练权重;然而,它将帮助我们了解训练数据集和测试数据集之间损失值和准确率值的差异。
- 提取不同训练轮次下的训练和测试损失以及准确率指标:
history_dict = history.history
loss_values = history_dict['loss']
val_loss_values = history_dict['val_loss']
acc_values = history_dict['acc']
val_acc_values = history_dict['val_acc']
epochs = range(1, len(val_loss_values) + 1)
在拟合模型时,history 变量会存储每个训练轮次中训练数据集和测试数据集的准确率和损失值。在前面的步骤中,我们将这些值存储在一个列表中,以便绘制随着轮次增加,训练和测试数据集的准确率和损失的变化。
- 可视化不同训练轮次下的训练和测试损失以及准确率:
import matplotlib.pyplot as plt
%matplotlib inline 
plt.subplot(211)
plt.plot(epochs, history.history['loss'], 'rx', label='Training loss')
plt.plot(epochs, val_loss_values, 'b', label='Test loss')
plt.title('Training and test loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()
plt.subplot(212)
plt.plot(epochs, history.history['acc'], 'rx', label='Training accuracy')
plt.plot(epochs, val_acc_values, 'b', label='Test accuracy')
plt.title('Training and test accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.gca().set_yticklabels(['{:.0f}%'.format(x*100) for x in plt.gca().get_yticks()]) 
plt.legend()
plt.show()
前面的代码生成了以下图表,其中第一个图显示了随着训练轮次增加的训练和测试损失值,第二个图显示了随着训练轮次增加的训练和测试准确率:

请注意,前面的网络达到了 97%的准确率。还要注意,损失值(从而准确率)在不同的训练轮次中发生了阶跃变化。我们将在下一部分对比输入数据集缩放前后的损失变化。
- 手动计算模型的准确性:
preds = model.predict(X_test)
在前面的步骤中,我们使用predict方法来计算给定输入(在此案例中为X_test)的预期输出值。请注意,我们将其指定为model.predict,因为我们已初始化了一个名为model的顺序模型:
import numpy as np
correct = 0
for i in range(len(X_test)):
    pred = np.argmax(preds[i],axis=0)
    act = np.argmax(y_test[i],axis=0)
    if(pred==act):
        correct+=1
    else:
        continue
correct/len(X_test)
在前面的代码中,我们正在逐一遍历所有测试预测。对于每个测试预测,我们执行argmax来获取具有最高概率值的索引。
同样,我们对测试数据集的实际值执行相同的操作。预测最高值的索引在预测值和测试数据集的实际值中是相同的。
最终,模型在测试数据集上的准确率是正确预测的数量除以测试数据集中的总数据点数。
工作原理...
我们在前面的代码中执行的关键步骤如下:
- 
我们使用 reshape方法将输入数据集展平,使每个像素都被视为一个变量。
- 
我们对输出值进行了独热编码,以便使用 np_utils包中的to_categorical方法区分不同的标签。
- 
我们通过逐层添加的方式构建了一个带有隐藏层的神经网络。 
- 
我们使用 model.compile方法编译了神经网络,以最小化分类交叉熵损失(因为输出有 10 个不同的类别)。
- 
我们使用 model.fit方法通过训练数据拟合模型。
- 
我们提取了存储在历史中的所有纪元的训练和测试损失准确率。 
- 
我们使用 model.predict方法预测了测试数据集中每个类别的概率。
- 
我们遍历了测试数据集中的所有图像,并识别出具有最高概率的类别。 
- 
最终,我们计算了准确率(预测类别与图像实际类别匹配的实例数量占总实例数的比例)。 
在下一节中,我们将探讨损失和准确率值发生阶跃变化的原因,并努力使变化更加平滑。
缩放输入数据集
缩放数据集是一个过程,其中我们限制数据集中变量的范围,确保它们没有非常宽泛的不同值。一种实现方法是将数据集中的每个变量除以该变量的最大值。通常情况下,神经网络在我们缩放输入数据集时表现较好。
在本节中,让我们了解为什么当数据集被缩放时,神经网络表现得更好。
准备工作
为了理解输入缩放对输出的影响,我们将检查输入数据集未缩放时的输出与输入数据集缩放时的输出进行对比。
输入数据未缩放:

在前面的表格中,注意到即使权重值从 0.01 变化到 0.9,输出(sigmoid)变化不大。sigmoid 函数是通过输入与权重的乘积计算 sigmoid 值,然后加上偏置:
output = 1/(1+np.exp(-(w*x + b))
其中w是权重,x是输入,b是偏置值。
Sigmoid 输出没有变化的原因是,因为 w*x 的乘积是一个大数(因为 x 是一个大数),导致 sigmoid 值总是落在 sigmoid 曲线的饱和部分(位于 sigmoid 曲线的右上角或左下角的饱和值)。
在这种情况下,让我们将不同的权重值与一个小的输入数值相乘,如下所示:

在前面的表格中,sigmoid 输出有所不同,因为输入和权重值较小,导致输入与权重相乘时得到较小的结果,进而导致 sigmoid 输出出现变化。
通过这次练习,我们了解了缩放输入数据集的重要性,这样在权重(前提是权重范围不大)的值与输入值相乘时会得到一个较小的结果。这种现象导致权重值更新不够迅速。
因此,为了达到最佳的权重值,我们应该缩放输入数据集,同时初始化权重值时不应有过大的范围(通常,在初始化时,权重值是一个介于 -1 和 +1 之间的随机值)。
当权重值也是一个非常大的数字时,这些问题仍然存在。因此,我们最好将权重值初始化为一个接近零的小值。
如何操作...
让我们回顾一下上节中使用的数据集缩放设置,并比较有无缩放的结果:
- 导入相关的包和数据集:
from keras.datasets import mnist
import numpy as np
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.utils import np_utils
(X_train, y_train), (X_test, y_test) = mnist.load_data()
- 缩放数据集有多种方法。一种方法是将所有数据点转换为介于零和一之间的值(通过将每个数据点除以整个数据集的最大值,这就是我们在以下代码中做的)。另一种流行的方式(在多种方法中)是对数据集进行标准化,使得值介于 -1 和 +1 之间,方法是用每个数据点减去整个数据集的均值,然后将每个结果数据点除以原始数据集中的标准差。
现在,我们将对输入数据集进行平展并缩放,如下所示:
# flatten 28*28 images to a 784 vector for each image
num_pixels = X_train.shape[1] * X_train.shape[2]
X_train = X_train.reshape(X_train.shape[0], num_pixels).astype('float32')
X_test = X_test.reshape(X_test.shape[0], num_pixels).astype('float32')
X_train = X_train/255
X_test = X_test/255
在前一步中,我们通过将每个值除以数据集中的最大值(255),将训练和测试输入缩放到一个介于零和一之间的值。此外,我们将输出数据集转换为 one-hot 编码格式:
# one hot encode outputs
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)
num_classes = y_test.shape[1]
- 使用以下代码构建模型并进行编译:
model = Sequential()
model.add(Dense(1000, input_dim=784, activation='relu'))
model.add(Dense(10,  activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
请注意,前述模型与我们在上一节中构建的模型完全相同。唯一的区别是,它将在经过缩放的训练数据集上执行,而之前的模型没有经过缩放。
- 按如下方式拟合模型:
history = model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=500, batch_size=32, verbose=1)
你会注意到,前述模型的准确率大约是 98.25%。
- 绘制训练和测试准确度以及不同时期的损失值(生成以下图表的代码与我们在训练普通神经网络食谱的第 8 步中使用的代码相同):

从前面的图表中,你应该注意到,与上一部分我们看到的未缩放数据集相比,训练和测试损失在增加的时期中平滑下降。
尽管前述网络在平滑下降的损失值方面给出了良好的结果,但我们注意到训练准确度和测试准确度/损失值之间存在差距,表明在训练数据集上可能存在过拟合现象。过拟合是指模型过度专注于训练数据,导致在测试数据集上表现不佳的现象。
它是如何工作的...
我们在前面的代码中执行的关键步骤如下:
- 
我们使用 reshape 方法将输入数据集展平,以便每个像素被视为一个变量 
- 
此外,我们对数据集进行了缩放,使得每个变量的值现在都在零和一之间 - 我们通过将变量的值除以该变量的最大值来实现前述操作
 
- 
我们对输出值进行了独热编码,以便使用 np_utils包中的to_categorical方法区分不同的标签
- 
我们通过顺序添加层的方式构建了一个具有隐藏层的神经网络 
- 
我们使用 model.compile方法将神经网络编译为最小化类别交叉熵损失(因为输出有 10 个不同的类别)
- 
我们使用 model.fit方法通过训练数据来拟合模型
- 
我们提取了在所有时期中存储在历史记录中的训练和测试损失准确度 
- 
我们还识别了一个我们认为是过拟合的场景 
还有更多...
除了通过将变量的值除以该变量的最大值来缩放变量值外,其他常用的缩放方法如下:
- 
最小-最大归一化 
- 
均值归一化 
- 
标准化 
有关这些缩放方法的更多信息,请访问维基百科:en.wikipedia.org/wiki/Feature_scaling。
当大多数输入值大于零时对训练的影响
到目前为止,在我们考虑的数据集中,我们并未查看输入数据集中的值的分布。输入的某些值导致训练速度更快。在本节中,我们将了解在训练时间依赖于输入值时,权重训练速度更快的情况。
准备就绪
在本节中,我们将完全按照与上一节相同的方式进行模型构建。
然而,我们将对我们的策略做出一些小的调整:
- 我们将反转背景颜色,以及前景颜色。本质上,在这种情况下,背景将是白色的,标签将用黑色书写。
这种变化影响模型准确性的直觉如下。
图像角落的像素对预测图像标签没有贡献。考虑到黑色像素(原始场景)的像素值为零,它会自动被处理,因为当这个输入与任何权重值相乘时,输出就是零。这将导致网络学习到,连接此角落像素与隐藏层的权重值的任何变化都不会对损失值的变化产生影响。
然而,如果角落的像素是白色的(我们已经知道角落的像素对预测图像标签没有贡献),它将对某些隐藏单元的值产生影响,因此权重需要微调,直到角落像素对预测标签的影响最小。
如何做到这一点...
- 加载并缩放输入数据集:
(X_train, y_train), (X_test, y_test) = mnist.load_data()
num_pixels = X_train.shape[1] * X_train.shape[2]
X_train = X_train.reshape(X_train.shape[0], num_pixels).astype('float32')
X_test = X_test.reshape(X_test.shape[0], num_pixels).astype('float32')
X_train = X_train/255
X_test = X_test/255
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)
num_classes = y_test.shape[1]
- 让我们看看输入值的分布:
X_train.flatten()
上面的代码将所有输入值展平为一个单一的列表,因此其形状为 (47,040,000),与 28 x 28 x X_train.shape[0] 相同。接下来,让我们绘制所有输入值的分布:
plt.hist(X_train.flatten())
plt.grid('off')
plt.title('Histogram of input values')
plt.xlabel('Input values')
plt.ylabel('Frequency of input values')

我们注意到大多数输入值为零(你应该注意到所有输入图像的背景都是黑色的,因此,大多数值为零,这是黑色的像素值)。
- 在本节中,让我们探索一个场景,其中我们反转颜色,背景为白色,字母为黑色,使用以下代码:
X_train = 1-X_train
X_test = 1-X_test
让我们绘制这些图像:
import matplotlib.pyplot as plt
%matplotlib inline
plt.subplot(221)
plt.imshow(X_train[0].reshape(28,28), cmap=plt.get_cmap('gray'))
plt.grid('off')
plt.subplot(222)
plt.imshow(X_train[1].reshape(28,28), cmap=plt.get_cmap('gray'))
plt.grid('off')
plt.subplot(223)
plt.imshow(X_train[2].reshape(28,28), cmap=plt.get_cmap('gray'))
plt.grid('off')
plt.subplot(224)
plt.imshow(X_train[3].reshape(28,28), cmap=plt.get_cmap('gray'))
plt.grid('off')
plt.show()
它们将如下所示:

结果图像的直方图现在如下所示:

你应该注意到,大多数输入值现在的值为一。
- 接下来,我们将使用在Scaling 输入数据集*部分中构建的相同模型架构来构建我们的模型:
model = Sequential()
model.add(Dense(1000,input_dim=784,activation='relu'))
model.add(Dense(10, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
history = model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=10, batch_size=32, verbose=1)
- 绘制不同训练周期(epochs)下的训练和测试准确率及损失值(生成以下图表的代码与我们在训练普通神经网络步骤 8 中使用的代码相同):

我们应该注意到,模型准确率现在已下降到约 97%,而使用相同的模型进行相同数量的周期和批量大小时,在一个大多数值为零(而不是大多数为一)的数据集上,准确率为约 98%。此外,模型的准确率为 97%,比输入像素大多数为零的场景要慢得多。
当大多数数据点为非零时,准确度下降的直觉是:当大多数像素为零时,模型的任务较简单(需要调整的权重较少),因为它只需根据少数像素值(那些像素值大于零的)进行预测。然而,当大多数数据点为非零时,模型需要调整更多的权重以进行预测。
批量大小对模型准确度的影响
在前面的各个部分中,我们为所有构建的模型都考虑了批量大小为 32。在这一部分中,我们将尝试理解批量大小对准确度的影响。
准备开始
为了理解批量大小对模型准确度的影响,让我们对比两个情境,其中数据集总大小为 60,000:
- 
批量大小为 30,000 
- 
批量大小为 32 
当批量大小较大时,每个训练轮次中的权重更新次数较少,相较于批量大小较小的情境。
批量大小较小时,每个训练轮次中权重更新次数较多的原因是,计算损失值时考虑的数据点较少。这导致每个训练轮次中批量的数量增多,因为大致而言,在一个训练轮次中,您需要遍历数据集中的所有训练数据点。
因此,批量大小越小,在相同的训练轮次下准确度越高。然而,在决定用于批量大小的数据点数量时,您还应确保批量大小不宜过小,以免在小批量数据上发生过拟合。
如何实现...
在前面的步骤中,我们建立了一个批量大小为 32 的模型。在这个步骤中,我们将继续实现模型,并对比低批量大小和高批量大小在相同训练轮次下的情境:
- 按照以下步骤预处理数据集并拟合模型:
(X_train, y_train), (X_test, y_test) = mnist.load_data()
num_pixels = X_train.shape[1] * X_train.shape[2]
X_train = X_train.reshape(X_train.shape[0], num_pixels).astype('float32')
X_test = X_test.reshape(X_test.shape[0], num_pixels).astype('float32')
X_train = X_train/255
X_test = X_test/255
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)
num_classes = y_test.shape[1]
model = Sequential()
model.add(Dense(1000,input_dim=784,activation='relu'))
model.add(Dense(10, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
history = model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=10, batch_size=30000, verbose=1)
请注意,代码中的唯一变化是在模型拟合过程中使用的batch_size参数。
- 绘制不同训练轮次下的训练准确度、测试准确度和损失值(生成以下图表的代码与我们在训练基本神经网络步骤 8 中使用的代码相同):

在之前的情境中,您应该注意到,与批量大小较小的模型相比,模型准确度在较晚的训练轮次才达到约 98%。
它是如何工作的...
您应该注意到,最初准确度较低,只有在经过相当数量的训练轮次后才会赶上。初期准确度较低的原因是,在这种情况下,权重更新的次数远低于之前的情境(批量大小较小的情境)。
在这种情况下,当批次大小为 30,000,总数据集大小为 60,000 时,当我们运行 500 个周期时,权重更新会发生在周期 * (数据集大小 / 批次大小) = 500 * (60,000 / 30,000) = 1,000 次。
在之前的场景中,权重更新发生在 500 * (60,000 / 32) = 937,500 次。
因此,批次大小越小,权重更新的次数就越多,一般来说,对于相同数量的周期,准确率会更好。
同时,你应该小心不要让批次大小过小,这可能导致不仅训练时间过长,还有可能出现过拟合的情况。
构建深度神经网络以提高网络准确率
到目前为止,我们已经看过了神经网络模型,其中神经网络只有一个隐藏层,位于输入层和输出层之间。在这一节中,我们将讨论具有多个隐藏层(因此是深度神经网络)的神经网络,同时重用已缩放的相同 MNIST 训练和测试数据集。
准备工作
深度神经网络意味着有多个隐藏层将输入层和输出层连接起来。多个隐藏层确保神经网络学习输入和输出之间复杂的非线性关系,这是简单神经网络无法学习的(因为隐藏层数量有限)。
一个典型的深度前馈神经网络如下所示:

如何做...
深度神经网络架构是通过在输入层和输出层之间添加多个隐藏层来构建的,如下所示:
- 加载数据集并进行缩放:
(X_train, y_train), (X_test, y_test) = mnist.load_data()
num_pixels = X_train.shape[1] * X_train.shape[2]
X_train = X_train.reshape(X_train.shape[0], num_pixels).astype('float32')
X_test = X_test.reshape(X_test.shape[0], num_pixels).astype('float32')
X_train = X_train/255
X_test = X_test/255
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)
num_classes = y_test.shape[1]
- 构建一个具有多个隐藏层将输入层和输出层连接起来的模型:
model = Sequential()
model.add(Dense(1000, input_dim=784, activation='relu'))
model.add(Dense(1000,activation='relu'))
model.add(Dense(1000,activation='relu'))
model.add(Dense(10,  activation='softmax'))
前述模型架构的结果如下所示:

请注意,前述模型会导致更多的参数数量,这是由于深度架构的结果(因为模型中有多个隐藏层)。
- 现在模型已经设置好,我们来编译并拟合模型:
model.compile(loss='categorical_crossentropy', optimizer=adam, metrics=['accuracy'])
history = model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=250, batch_size=1024, verbose=1)
前述结果得到一个 98.6%的准确率,这略高于我们之前看到的模型架构的准确率。训练和测试的损失及准确率如下所示(生成以下图表的代码与我们在训练基础神经网络食谱第 8 步中使用的代码相同):

请注意,在这种情况下,训练损失和测试损失之间存在相当大的差距,表明深度前馈神经网络在训练数据上过拟合了。我们将在过拟合的章节中学习如何避免对训练数据的过拟合。
调整学习率以提高网络准确率
到目前为止,在之前的食谱中,我们使用的是 Adam 优化器的默认学习率 0.0001。
在本节中,我们将手动将学习率设置为更大的数值,并观察改变学习率对模型准确性的影响,同时复用之前食谱中已缩放的相同 MNIST 训练和测试数据集。
准备工作
在前一章节关于构建前馈神经网络的内容中,我们学习了学习率用于更新权重,且权重的变化与损失减少的量成正比。
此外,权重值的变化等于损失减少量与学习率的乘积。因此,学习率越低,权重值的变化越小,反之亦然。
你可以基本上将权重值视为一个连续的光谱,其中权重是随机初始化的。当权重值变化较大时,有很大可能在该光谱中并未考虑到所有可能的权重值。然而,当权重值变化较小,可能会实现全局最小值,因为可能会考虑更多的权重值。
为了更进一步理解,我们来考虑拟合y = 2x直线的玩具示例,其中初始权重值为 1.477,初始偏置值为零。前馈和反向传播函数将保持与前一章相同:
def feed_forward(inputs, outputs, weights):
     hidden = np.dot(inputs,weights[0])
     out = hidden+weights[1]
     squared_error = (np.square(out - outputs))
     return squared_error
def update_weights(inputs, outputs, weights, epochs, lr): 
    for epoch in range(epochs):
        org_loss = feed_forward(inputs, outputs, weights)
        wts_tmp = deepcopy(weights)
        wts_tmp2 = deepcopy(weights)
        for ix, wt in enumerate(weights):
            print(ix, wt)
            wts_tmp[-(ix+1)] += 0.0001
            loss = feed_forward(inputs, outputs, wts_tmp)
            del_loss = np.sum(org_loss - loss)/(0.0001*len(inputs))
            wts_tmp2[-(ix+1)] += del_loss*lr
            wts_tmp = deepcopy(weights)
        weights = deepcopy(wts_tmp2)
    return wts_tmp2
请注意,从前一章看到的反向传播函数中唯一的变化是我们将学习率作为参数传递给了前述函数。当学习率为 0.01 时,在不同训练轮次下的权重值如下:
w_val = []
b_val = []
for k in range(1000):
     w_new, b_new = update_weights(x,y,w,(k+1),0.01)
     w_val.append(w_new)
     b_val.append(b_new)
不同训练轮次下权重变化的图形可以通过以下代码获得:
import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(w_val)
plt.title('Weight value over different epochs when learning rate is 0.01')
plt.xlabel('epochs')
plt.ylabel('weight value')
plt.grid('off')
前述代码的输出结果如下:

以类似的方式,当学习率为 0.1 时,在不同训练轮次下的权重值如下:

该截图显示了当学习率为 0.5 时,在不同训练轮次下权重的变化值:

请注意,在前述场景中,最初权重值发生了剧烈变化,而学习率为 0.1 时收敛了,而学习率为 0.5 时未能收敛到最优解,因此陷入了局部最小值。
在学习率为 0.5 的情况下,由于权重值停滞在局部最小值,它无法达到最优值 2。
如何进行...
现在我们已经理解了学习率如何影响输出值,让我们看看学习率对之前看到的 MNIST 数据集的实际影响,我们将保持相同的模型架构,但只改变学习率参数。
请注意,我们将使用与缩放输入数据集食谱中第 1 步和第 2 步相同的数据预处理步骤。
一旦数据集预处理完成,我们通过在下一步中指定优化器来调整模型的学习率:
- 我们按如下方式调整学习率:
from keras import optimizers
adam=optimizers.Adam(lr=0.01)
通过前述代码,我们已使用指定的学习率 0.01 初始化了 Adam 优化器。
- 我们按照如下方式构建、编译并训练模型:
model = Sequential()
model.add(Dense(1000, input_dim=784, activation='relu'))
model.add(Dense(10,  activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer=adam, metrics=['accuracy']) 
history = model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=500, batch_size=1024, verbose=1)
前述网络的准确率在 500 个训练轮次结束时约为 90%。让我们看看损失函数和准确率在不同训练轮次下的变化(生成下图的代码与我们在训练基础神经网络方法第 8 步中使用的代码相同):

请注意,当学习率较高(在当前情况下为 0.01)时,与 0.0001(在缩放输入数据集方法中考虑的情况)相比,损失函数下降得不如低学习率模型平稳。
低学习率的模型更新权重较慢,因此导致损失函数平稳下降,并且准确率较高,这种准确率是在更多的训练轮次中逐渐获得的。
另外,当学习率较高时,损失值的步进变化是由于损失值陷入局部最小值,直到权重值改变为最优值。较低的学习率可以更快地到达最优权重值,因为权重变化较慢,但在正确的方向上稳步前进。
类似地,让我们探讨当学习率为 0.1 时网络的准确性:
from keras import optimizers
adam=optimizers.Adam(lr=0.1)
model = Sequential()
model.add(Dense(1000, input_dim=784, activation='relu'))
model.add(Dense(10,  activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer=adam, metrics=['accuracy'])
history = model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=500, batch_size=1024, verbose=1)
需要注意的是,由于学习率较高,损失值无法进一步显著下降;也就是说,可能权重已经陷入局部最小值:

因此,一般来说,设置较低的学习率并让网络在较多的训练轮次中进行学习是一个好主意。
改变损失优化器以提高网络准确性
到目前为止,在前面的几种方法中,我们认为损失优化器是 Adam 优化器。然而,优化器有多种变体,优化器的更改可能会影响模型学习拟合输入与输出的速度。
在本方法中,我们将理解改变优化器对模型准确性的影响。
准备就绪
为了了解优化器变化对网络准确性的影响,下面我们将对比前面章节中的情况(使用的是 Adam 优化器)和这一节使用随机梯度下降优化器的情况,同时重新使用已缩放的相同 MNIST 训练和测试数据集(数据预处理步骤与缩放数据集方法中的步骤 1 和步骤 2 相同):
model = Sequential()
model.add(Dense(1000, input_dim=784, activation='relu'))
model.add(Dense(10,  activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['accuracy'])
history = model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=100, batch_size=32, verbose=1)
请注意,当我们在前面的代码中使用随机梯度下降优化器时,经过 100 轮训练后的最终准确率大约为 98%(以下图表的生成代码与我们在训练一个基础神经网络食谱的第 8 步中使用的代码相同):

然而,我们也应该注意,与使用 Adam 优化器的模型相比,该模型在达到高准确率的过程中变得更加缓慢。
还有更多...
其他一些可用的损失优化器如下:
- 
RMSprop 
- 
Adagrad 
- 
Adadelta 
- 
Adamax 
- 
Nadam 
你可以在这里了解更多关于不同优化器的信息:keras.io/optimizers/。
此外,你还可以在这里找到每个优化器的源代码:github.com/keras-team/keras/blob/master/keras/optimizers.py。
理解过拟合的情形
在之前的一些食谱中,我们注意到训练准确率接近 100%,而测试准确率约为 98%,这就是在训练数据集上发生过拟合的情况。让我们对训练和测试准确率之间的差异有一个直观的理解。
为了理解导致过拟合的现象,让我们对比两个情形,分别比较训练和测试的准确率,并且展示权重的直方图:
- 
模型运行五轮训练 
- 
模型运行 100 轮训练 
两个情形之间的训练集和测试集的准确率对比如下:
| 情形 | 训练数据集 | 测试数据集 | 
|---|---|---|
| 5 轮训练 | 97.59% | 97.1% | 
| 100 轮训练 | 100% | 98.28% | 
一旦我们绘制出连接隐藏层和输出层的权重的直方图,我们会注意到,与五轮训练的情况相比,100 轮训练的权重分布范围更大:


从前面的图片中你应该注意到,100 轮训练的情况相比于五轮训练的情况,权重值的分布更加广泛。这是因为在 100 轮训练中,模型有更多的机会在训练集上过拟合,而相比之下,五轮训练的更新次数较少,因此过拟合的机会也较少。
权重值过高(以及训练集和测试集之间的差异)是模型可能发生过拟合和/或可能有机会缩放输入/权重以提高模型准确率的良好指示。
此外,神经网络中可能包含数十万甚至百万的权重(某些架构中为数百万),这些权重都需要进行调整,因此,某些权重有可能会被更新为非常大的值,以便针对数据集中某一特异值行进行微调。
使用正则化克服过拟合
在前一部分中,我们已确定高权重幅度是过拟合的原因之一。在本节中,我们将探讨如何避免过拟合问题,例如对高权重幅度值进行惩罚。
正则化通过对模型中的高权重幅度进行惩罚来进行调节。L1 和 L2 正则化是最常用的正则化技术,其工作原理如下:
L2 正则化在最小化损失函数的同时(即以下公式中的平方损失和),还会最小化神经网络指定层的权重平方和:

其中  是与正则化项相关的权重值,这是一个需要调整的超参数,y 是预测值,
 是与正则化项相关的权重值,这是一个需要调整的超参数,y 是预测值, 是目标值,
 是目标值,  是模型所有层的权重值。
 是模型所有层的权重值。
L1 正则化在最小化损失函数的同时(即以下公式中的平方损失和),还会最小化神经网络指定层的权重绝对值之和:
 。
。
通过这种方式,我们可以确保权重不会仅针对训练集中的极端案例进行调整(从而避免在测试数据上无法泛化)。
如何做到这一点
L1/L2 正则化在 Keras 中的实现方式如下:
model = Sequential()
model.add(Dense(1000,input_dim=784,activation='relu',kernel_regularizer=l2(0.1)))model.add(Dense(10,  activation='softmax',kernel_regularizer=l2(0.1)))
model.compile(loss='categorical_crossentropy', optimizer=adam, metrics=['accuracy'])
history = model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=500, batch_size=1024, verbose=1)
请注意,前面的操作涉及调用一个额外的超参数——kernel_regularizer,然后指定它是 L1 还是 L2 正则化。此外,我们还会指定 lambda 值,这个值决定了正则化的权重。
我们注意到,在正则化之后,训练集的准确率并未达到 ~100%,而测试集的准确率为 98%。L2 正则化后权重的直方图将在下一个图中显示。
连接隐藏层和输出层的权重提取方式如下:
model.get_weights()[0].flatten()
一旦提取了权重,它们会按以下方式绘制:
plt.hist(model.get_weights()[0].flatten())

我们注意到,与之前的情况相比,绝大多数权重现在都更接近于零,这就避免了过拟合问题。在 L1 正则化的情况下,我们也能看到类似的趋势。
请注意,在存在正则化的情况下,权重值比没有进行正则化时的权重值要低得多。
因此,L1 和 L2 正则化帮助我们避免了训练数据集上的过拟合问题。
使用丢弃法克服过拟合
在上一节中,我们使用 L1/L2 正则化来克服过拟合问题。在这一节中,我们将使用另一种有助于实现相同目标的工具——丢弃法。
丢弃法可以看作是一种方法,其中只有某个百分比的权重会被更新,而其他的权重则不会在某次权重更新迭代中被更新。这样,我们就处于一个位置,即并非所有权重都在权重更新过程中被更新,从而避免了某些权重相较于其他权重达到非常高的幅度:
model = Sequential()
model.add(Dense(1000, input_dim=784, activation='relu'))
model.add(Dropout(0.75))
model.add(Dense(10,  activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer=adam, metrics=['accuracy'])
history = model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=100, batch_size=1024, verbose=1)
在前面的代码中,我们设置了 0.75 的丢弃率;也就是说,在某次权重更新迭代中,75%的权重不会被更新。
上述情况会导致训练准确度和测试准确度之间的差距没有像在没有丢弃法的情况下构建模型时那么大,因为在没有丢弃法的情况下,权重的分布较广。
请注意,现在第一层权重的直方图:
plt.hist(model.get_weights()[-2].flatten())

请注意,在前述场景中,超出 0.2 或-0.2 的权重的频次要比 100 个 epoch 的场景少。
使用批量归一化加速训练过程
在前一节关于数据集缩放的内容中,我们学习到,当输入数据没有被缩放(即它没有在零到一之间)时,优化过程会很慢。
在以下情境中,隐藏层的值可能会很高:
- 
输入数据值很高 
- 
权重值很高 
- 
权重和输入的乘积很高 
这些情境中的任何一种都可能导致隐藏层的输出值很大。
请注意,隐藏层是输入层到输出层的过渡。因此,当隐藏层值很大时,高输入值导致优化缓慢的现象同样适用。
批量归一化在这种情况下发挥了重要作用。我们已经学到,当输入值很高时,我们会进行缩放以减少输入值。此外,我们还学到,可以使用另一种方法进行缩放,即减去输入的均值,并除以输入的标准差。批量归一化正是采用这种方法进行缩放。
通常,所有值都使用以下公式进行缩放:




请注意,γ 和 β 在训练过程中会学习到,连同网络的原始参数一起。
如何做...
在代码中,批量归一化是这样应用的:
请注意,我们将使用与步骤 1 和步骤 2 中在缩放输入数据集步骤中相同的数据预处理方法。
- 按如下方式导入BatchNormalization方法:
from keras.layers.normalization import BatchNormalization
- 实例化一个模型,并构建与我们使用正则化技术时相同的架构。唯一的不同之处是我们在一个隐藏层中执行批量归一化:
model = Sequential()
model.add(Dense(1000, input_dim=784,activation='relu', kernel_regularizer = l2(0.01)))
model.add(BatchNormalization())
model.add(Dense(10, activation='softmax', kernel_regularizer = l2(0.01)))
- 按如下方式构建、编译和拟合模型:
from keras.optimizers import Adam
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
history = model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=100, batch_size=1024, verbose=1)
前述结果显示,当没有批量归一化时,训练速度明显较慢,如下所示:

前面的图表显示了没有批量归一化,仅使用正则化时的训练和测试损失与准确度。接下来的图表显示了同时使用正则化和批量归一化时的训练和测试损失与准确度:

请注意,在前述的两种场景中,当我们执行批量归一化时,训练速度明显更快(测试数据集准确率约为 97%),相比之下,当我们不执行时(测试数据集准确率约为 91%):
因此,批量归一化使得训练速度大大加快。
第三章:深度前馈神经网络的应用
本章中,我们将覆盖以下方案:
- 
预测信用违约 
- 
预测房价 
- 
将新闻文章分类到不同主题 
- 
分类常见音频 
- 
预测股票价格 
介绍
在前几章中,我们学习了如何构建神经网络及需要调整的各种参数,以确保模型能够很好地泛化。此外,我们还学习了如何利用神经网络在 MNIST 数据集上进行图像分析。
在本章中,我们将学习如何利用神经网络进行预测,基于以下内容:
- 
结构化数据集 - 
分类输出预测 
- 
连续输出预测 
 
- 
- 
文本分析 
- 
音频分析 
此外,我们还将学习以下内容:
- 
实现自定义损失函数 
- 
对某些类别的输出赋予比其他类别更高的权重 
- 
对数据集中某些行赋予比其他行更高的权重 
- 
利用功能性 API 整合多个数据源 
我们将通过以下方案来学习上述内容:
- 
预测信用违约 
- 
预测房价 
- 
分类新闻文章 
- 
预测股票价格 
- 
分类常见音频 
然而,你应该注意,这些应用仅用于帮助你理解如何利用神经网络分析各种输入数据。关于卷积神经网络和循环神经网络的高级文本、音频和时间序列数据分析方法将会在后续章节中提供。
预测信用违约
在金融服务行业,客户违约是造成收入损失的主要原因之一。然而,实际发生违约的客户占总客户的比例非常小。因此,这成为一个分类问题,更重要的是,识别出稀有事件。
在这个案例研究中,我们将分析一个跟踪客户在某一时间点的关键属性的数据库,并尝试预测客户是否可能发生违约。
让我们考虑一下如何将我们构建的模型的预测结果投入到实际应用中。企业可能会特别关注那些更可能违约的客户——可能会为他们提供替代的付款选项或减少信用额度等方式。
准备工作
我们将采用以下策略来预测客户违约:
- 
目标:为更可能违约的客户分配高概率。 
- 
测量 标准:通过降低违约概率,最大化仅考虑前 10%成员时实际违约的客户数量。 
我们将采用以下策略为每个成员分配违约概率:
- 
考虑所有成员的历史数据。 
- 
理解哪些变量可以帮助我们识别可能违约的客户: - 
收入与债务比率是一个非常好的指示器,能够判断成员是否可能违约。 
- 
我们将提取一些与此类似的其他变量。 
 
- 
- 
在前一步中,我们创建了输入变量;现在,让我们继续创建因变量: - 
我们将通过回顾历史数据并查看成员是否在接下来的两年内发生违约,来提取那些实际违约的成员。 
- 
设置时间滞后非常重要,因为如果我们在预测日期与成员可能违约之间没有时间间隔,可能无法为我们提供改变结果的杠杆。 
 
- 
- 
由于结果是二元的,我们将最小化二元交叉熵损失。 
- 
模型将有一个隐藏层,连接输入层和输出层。 
- 
我们将计算在测试数据集中,实际违约的前 10%概率成员的数量。 
请注意,我们假设测试数据具有代表性,因为在没有将模型投入生产的情况下,我们无法评估模型在未见数据上的表现。我们假设模型在未见数据上的表现是预测模型在未来数据上表现的良好指标。
如何实现...
我们将按照以下策略编写代码(在实现代码时,请参考 GitHub 中的Credit default prediction.ipynb文件):
- 导入相关包和数据集:
import pandas as pd
data = pd.read_csv('...') # Please add path to the file you downloaded
我们下载的数据集的前三行如下:

上面的截图展示的是原始数据集中一部分变量。名为Defaultin2yrs的变量是我们需要预测的输出变量,基于数据集中其他存在的变量。
- 总结数据集以更好地理解变量:
data.describe()
一旦查看输出,您将会注意到以下几点:
- 
某些变量的范围较小( age),而其他变量的范围则大得多(Income)。
- 
某些变量存在缺失值( Income)。
- 
某些变量存在异常值( Debt_income_ratio)。在接下来的步骤中,我们将纠正所有之前标记的问题。
- 
用变量的中位数值填充缺失值: 
vars = data.columns[1:]
import numpy as np
for var in vars:
     data[var]= np.where(data[var].isnull(),data[var].median(),data[var])
在上面的代码中,我们排除了第一个变量,因为它是我们要预测的变量,然后对其余变量中的缺失值进行填充(前提是该变量确实存在缺失值)。
- 将每个变量限制在其对应的 95^(th)百分位值,以避免输入变量中出现异常值:
for var in vars:
     x=data[var].quantile(0.95)
     data[var+"outlier_flag"]=np.where(data[var]>x,1,0)
     data[var]=np.where(data[var]>x,x,data[var])
在上面的代码中,我们已识别出每个变量的 95^(th)百分位值,创建了一个新变量,如果行中存在给定变量的异常值,则该新变量的值为 1,否则为 0。此外,我们还将变量值限制为原始值的 95^(th)百分位值。
- 一旦我们总结了修改后的数据,我们注意到,除了Debt_income_ratio变量外,其他所有变量似乎都没有异常值了。因此,我们进一步限制Debt_income_ratio,将其输出范围限制在 80^(th)百分位值:
data['Debt_income_ratio_outlier']=np.where(data['Debt_incomeratio']>1,1,0)
data['Debt_income_ratio']=np.where(data['Debt_income_ratio']>1,1,data['Debt_income_ratio'])
- 将所有变量标准化到相同的尺度,使其值介于零和一之间:
for var in vars:
     data[var]= data[var]/data[var].max()
在前面的代码中,我们通过将每个输入变量的值除以该输入变量列的最大值,将所有变量限制在一个相似的输出范围内,该范围介于零和一之间。
- 创建输入和输出数据集:
X = data.iloc[:,1:]
Y = data['Defaultin2yrs']
- 将数据集划分为训练集和测试集:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size = 0.3, random_state= 42)
在前一步中,我们使用train_test_split方法将输入和输出数组拆分为训练集和测试集,其中测试集占输入和对应输出数组总数据点的 30%。
- 现在数据集已经创建,让我们定义神经网络模型,如下所示:
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.utils import np_utils
model = Sequential()
model.add(Dense(1000, input_dim=X_train.shape[1], activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()
模型的总结如下:

在先前的架构中,我们将输入变量连接到一个包含 1,000 个隐藏单元的隐藏层。
- 编译模型。我们将使用二元交叉熵,因为输出变量只有两个类别。此外,我们将指定optimizer为adam优化器:
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
- 拟合模型:
history = model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=20, batch_size=1024, verbose=1)
训练和测试损失、准确率随着训练轮次增加的变化如下所示:

- 对测试数据集进行预测:
pred = model.predict(X_test)
- 检查在按概率递减排序时,测试数据集前 10%中实际违约者的数量:
test_data = pd.DataFrame([y_test]).T
test_data['pred']=pred
test_data = test_data.reset_index(drop='index')
test_data = test_data.sort_values(by='pred',ascending=False)
print(test_data[:4500]['Defaultin2yrs'].sum())
在前面的代码中,我们将预测值与实际值连接起来,然后按概率对数据集进行排序。我们检查了在按概率递减排序时,测试数据集前 10%(即前 4,500 行)中捕获的实际违约者的数量。
我们应注意,我们通过筛选 4,500 个高概率客户,捕获了 1,580 个实际违约者。这是一个不错的预测,因为平均而言,只有 6%的客户会违约。因此,在这种情况下,约 35%的高违约概率客户实际上发生了违约。
它是如何工作的...
在本教程中,我们学习了以下概念:
- 
填补 缺失 值:我们了解到填补变量缺失值的一种方法是用该变量的中位数替换缺失值。处理缺失值的其他方法包括用均值替代缺失值,或者通过将缺失值替换为与包含缺失值的行最相似的行中该变量的均值(这一技术称为识别最近邻)。 
- 
限制异常值:我们还学到,一种限制异常值的方法是将大于 95^(th)百分位数的值替换为 95^(th)百分位数的值。我们执行此操作的原因是为了确保输入变量不会出现所有值都集中在一个小值附近的情况(当变量按最大值缩放时,该最大值为异常值)。 
- 
缩放数据集:最后,我们对数据集进行了缩放,以便它可以传递给神经网络。 
为类别分配权重
当我们为属于违约者的行和属于非违约者的行分配相同的权重时,模型可能会对非违约者进行微调。在本节中,我们将探讨如何分配更高的权重,以便我们的模型能更好地分类违约者。
准备工作
在上一节中,我们为每个类别分配了相同的权重;也就是说,当实际值和预测值之间的差异大小相同时,分类交叉熵损失是相同的,无论它是用于预测违约还是非违约。
为了进一步理解这个情境,让我们考虑以下示例:
| 情境 | 违约概率 | 实际违约值 | 交叉熵损失 | 
|---|---|---|---|
| 1 | 0.2 | 1 | 1log(0.2)* | 
| 2 | 0.8 | 0 | (1-0)log(1-0.8)* | 
在之前的情境中,交叉熵损失值是相同的,无论实际违约值如何。
然而,我们知道我们的目标是尽可能多地捕获实际违约者,在通过概率排序后的前 10%预测中。
因此,让我们继续分配更高的损失权重(权重为100),当实际违约值为1时,而当实际违约值为0时,分配较低的权重(权重为1)。
之前的情境现在发生了如下变化:
| 情境 | 违约概率 | 实际违约值 | 交叉熵损失 | 
|---|---|---|---|
| 1 | 0.2 | 1 | 1001log(0.2) | 
| 2 | 0.8 | 0 | 1(1-0)log(1-0.8) | 
现在,如果我们注意到交叉熵损失,当预测错误时,实际违约值为1的情况相比实际违约值为0时的预测,交叉熵损失要高得多。
现在我们已经理解了为类别分配权重的直觉,让我们继续在信用违约数据集中为输出类别分配权重。
为了构建数据集和模型执行的所有步骤与上一节相同,除了模型拟合过程。
如何操作…
模型拟合过程通过以下步骤完成(在实现代码时,请参考 GitHub 中的Credit default prediction.ipynb文件):
history = model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=10, batch_size=1024, verbose=1,class_weight = {0:1,1:100})
请注意,在前面的代码片段中,我们创建了一个字典,其中包含与输出中的不同类别对应的权重,然后将其作为输入传递给class_weight参数。
前一步骤确保我们在计算实际结果为1时,给损失值赋予权重100,而在计算实际结果为0时,给损失值赋予权重1。
随着训练轮次的增加,准确率和损失值的变化如下:

请注意,在此迭代中,准确度值显著降低,因为我们预测的数据点值为 1 的数量比在两个类别具有相等权重的场景中更多。
一旦模型训练完成,我们就可以检查在前 10%的预测中捕获到的实际违约者数量,如下所示:
pred = model.predict(X_test)
test_data = pd.DataFrame([y_test[:,1]]).T
test_data['pred']=pred[:,1]
test_data = test_data.reset_index(drop='index')
test_data = test_data.sort_values(by='pred',ascending=False)
test_data.columns = ['Defaultin2yrs','pred']
print(test_data[:4500]['Defaultin2yrs'].sum())
你会注意到,与之前捕获了 1,580 名客户的前 10%的情况相比,在这个场景下,我们捕获了 1,640 名客户进入前 10%,因此在我们设定的目标上取得了更好的结果,在这个场景中,我们捕获了 36%的所有违约者进入前 10%的高概率客户,而在之前的场景中是 35%。
并不是所有情况下随着类权重的增加,准确率都会提高。分配类权重是一种机制,用于给我们关注的预测赋予更高的权重。
预测房价
在前一个案例研究中,我们的输出是分类的。在本案例研究中,我们将探讨一个连续的输出,通过尝试预测房价,其中提供了 13 个可能影响房价的变量作为输入。
目标是最小化预测房价的误差。
准备开始
鉴于目标是最小化误差,让我们定义我们将要最小化的误差——我们应该确保正误差和负误差不会相互抵消。因此,我们将最小化绝对误差。另一种选择是最小化平方误差。
现在我们已经微调了目标,让我们定义解决这个问题的策略:
- 
对输入数据集进行归一化处理,使所有变量的范围都在 0 到 1 之间。 
- 
将给定数据拆分为训练集和测试集。 
- 
初始化隐藏层,将 13 个输入变量连接到一个输出变量。 
- 
使用 Adam 优化器编译模型,并定义损失函数为最小化平均绝对误差值。 
- 
训练模型。 
- 
对测试数据集进行预测。 
- 
计算在测试数据集上预测的误差。 
现在我们已经定义了方法,让我们在下一节中使用代码来执行它。
如何进行...
- 导入相关数据集(在实施代码时,请参考 GitHub 中的Predicting house price.ipynb文件以及推荐的数据集):
from keras.datasets import boston_housing
(train_data, train_targets), (test_data, test_targets) = boston_housing.load_data()
- 对输入和输出数据集进行归一化处理,使所有变量的范围都在 0 到 1 之间:
import numpy as np
train_data2 = train_data/np.max(train_data,axis=0)
test_data2 = test_data/np.max(train_data,axis=0)
train_targets = train_targets/np.max(train_targets)
test_targets = test_targets/np.max(train_targets)
请注意,我们已经使用训练数据集中的最大值对测试数据集进行了归一化,因为我们不应在模型构建过程中使用测试数据集中的任何值。此外,请注意,我们已对输入和输出值都进行了归一化处理。
- 现在输入和输出数据集已经准备好,让我们继续并定义模型:
from keras.models import Sequential
from keras.layers import Dense, Dropout
from keras.utils import np_utils
from keras.regularizers import l1
model = Sequential()
model.add(Dense(64, input_dim=13, activation='relu', kernel_regularizer = l1(0.1)))
model.add(Dense(1, activation='relu', kernel_regularizer = l1(0.1)))
model.summary()
模型的总结如下:

请注意,在模型构建过程中,我们执行了L1正则化,以防止模型在训练数据上过拟合(因为训练数据点数量较少)。
- 编译模型以最小化平均绝对误差值:
model.compile(loss='mean_absolute_error', optimizer='adam')
- 拟合模型:
history = model.fit(train_data2, train_targets, validation_data=(test_data2, test_targets), epochs=100, batch_size=32, verbose=1)
- 计算测试数据集上的平均绝对误差:
np.mean(np.abs(model.predict(test_data2) - test_targets))*50
我们应该注意到,平均绝对误差为~6.7单位。
在下一节中,我们将改变损失函数并添加自定义权重,看看是否能够改进平均绝对误差值。
定义自定义损失函数
在前一节中,我们使用了预定义的平均绝对误差loss函数进行优化。在本节中,我们将学习如何定义自定义损失函数来进行优化。
我们将构建的自定义损失函数是一个修改过的均方误差值,其中误差是实际值的平方根与预测值的平方根之间的差异。
自定义损失函数定义如下:
import keras.backend as K
def loss_function(y_true, y_pred):
    return K.square(K.sqrt(y_pred)-K.sqrt(y_true))
现在我们已经定义了loss函数,我们将重新使用前面准备的输入和输出数据集,并且我们将使用我们之前定义的相同模型。
现在,让我们编译模型:
model.compile(loss=loss_function, optimizer='adam')
在前面的代码中,请注意我们将loss值定义为我们之前定义的自定义损失函数—loss_function。
history = model.fit(train_data2, train_targets, validation_data=(test_data2, test_targets), epochs=100, batch_size=32, verbose=1)
一旦我们拟合了模型,我们会注意到平均绝对误差约为~6.5单位,略低于我们在前一次迭代中使用mean_absolute_error损失函数时的误差。
将新闻文章分类为主题
在之前的案例研究中,我们分析了结构化的数据集,也就是包含变量及其对应值的数据集。在这个案例研究中,我们将处理一个以文本作为输入的数据集,预期的输出是与该文本相关的 46 个可能主题之一。
准备就绪
为了理解执行文本分析的直觉,我们可以考虑 Reuters 数据集,其中每篇新闻文章被分类为 46 个可能主题之一。
我们将采用以下策略来执行我们的分析:
- 
由于数据集可能包含数千个独特的单词,我们将筛选出我们要考虑的单词。 
- 
对于这个特定的练习,我们将考虑最常见的前 10,000 个单词。 
- 
另一种方法是考虑那些累积起来占数据集所有单词 80%的单词。这确保了所有稀有单词被排除在外。 
- 
一旦选定词汇,我们将根据组成的常见词汇对文章进行独热编码。 
- 
类似地,我们将对输出标签进行独热编码。 
- 
每个输入现在是一个 10,000 维的向量,输出是一个 46 维的向量: 
- 
我们将数据集分为训练集和测试集。然而,在代码中,你会注意到我们将使用 Keras 中内置的 reuters数据集,该数据集具有内置函数,可以识别最常见的n个词汇,并将数据集拆分为训练集和测试集。
- 
在中间插入一个隐藏层,将输入和输出进行映射。 
- 
我们将在输出层执行 softmax 操作,以获得输入属于 46 个类别之一的概率。 
- 
由于我们有多个可能的输出,因此我们将使用分类交叉熵损失函数。 
- 
我们将编译并训练模型,以衡量其在测试数据集上的准确性。 
如何实现...
我们将按如下方式实现之前定义的策略(实现代码时请参考 GitHub 上的Categorizing news articles into topics.ipynb文件):
- 导入数据集:
from keras.datasets import reuters
(train_data, train_labels), (test_data, test_labels) = reuters.load_data(num_words=10000)
在上述代码片段中,我们加载了 Keras 中可用的reuters数据集的数据。此外,我们只考虑数据集中最常见的10000个词汇。
- 检查数据集:
train_data[0]
加载的训练数据集示例如下:

请注意,前述输出中的数字表示输出中出现的单词的索引。
- 我们可以按如下方式提取值的索引:
word_index = reuters.get_word_index()
- 
向量化输入。我们将以以下方式将文本转换为向量: - 
对输入词汇进行独热编码——最终生成输入数据集中总共 10000列。
- 
如果文本中存在某个词汇,则对应词汇索引的列将显示为 1,其他列将显示为 0。 
- 
对文本中的所有唯一词汇执行上述步骤。如果一篇文本有两个唯一词汇,那么将有两列值为 1,其他所有列的值为 0: 
 
- 
import numpy as np
def vectorize_sequences(sequences, dimension=10000):
     results = np.zeros((len(sequences), dimension))
     for i, sequence in enumerate(sequences):
         results[i, sequence] = 1.
     return results
在上述函数中,我们初始化了一个零矩阵,并根据输入序列中的索引值将其填充为 1。
在以下代码中,我们将单词转换为 ID。
x_train = vectorize_sequences(train_data)
x_test = vectorize_sequences(test_data)
- 对输出进行独热编码:
from keras.utils.np_utils import to_categorical
one_hot_train_labels = to_categorical(train_labels)
one_hot_test_labels = to_categorical(test_labels)
上述代码将每个输出标签转换为一个长度为46的向量,其中46个值中的一个为 1,其他值为 0,具体取决于标签的索引值。
- 定义模型并编译:
from keras.models import Sequential
from keras.layers import Dense
model = Sequential()
model.add(Dense(64, activation='relu', input_shape=(10000,)))
model.add(Dense(64, activation='relu'))
model.add(Dense(46, activation='softmax'))
model.summary()
model.compile(optimizer='adam',loss='categorical_crossentropy',metrics=['accuracy'])

请注意,在编译时,我们将loss定义为categorical_crossentropy,因为此处的输出是分类的(输出有多个类别)。
- 训练模型:
history = model.fit(X_train, y_train,epochs=20,batch_size=512,validation_data=(X_test, y_test))
上述代码生成了一个模型,该模型在将输入文本分类到正确的主题时准确率为 80%,如下所示:

分类常见音频
在前面的章节中,我们已经了解了如何在结构化数据集和非结构化文本数据上执行建模策略。
在本节中,我们将学习如何进行一个分类任务,输入是原始音频。
我们将采用的策略是从输入音频中提取特征,每个音频信号都表示为一个固定数量特征的向量。
提取音频特征的方式有多种,然而在这个练习中,我们将提取与音频文件对应的梅尔频率倒谱系数(MFCC)。
一旦我们提取了特征,我们将执行分类任务,这与我们为 MNIST 数据集分类构建模型时非常相似——在那时我们有隐藏层连接输入层和输出层。
在接下来的章节中,我们将在音频数据集上执行分类任务,输出有十个可能的类别。
如何实现...
我们之前定义的策略的代码如下(在实现代码时,请参考 GitHub 上的Audio classification.ipynb文件):
- 导入数据集:
import pandas as pd
data = pd.read_csv('/content/train.csv')
- 提取每个音频输入的特征:
ids = data['ID'].values
def extract_feature(file_name):
    X, sample_rate = librosa.load(file_name)
    stft = np.abs(librosa.stft(X))
    mfccs = np.mean(librosa.feature.mfcc(y=X,sr=sample_rate, n_mfcc=40).T,axis=0)
    return mfccs
在前面的代码中,我们定义了一个函数,该函数以file_name作为输入,提取与音频文件对应的40个 MFCC,并返回这些特征。
- 创建输入和输出数据集:
x = []
y = []
for i in range(len(ids)):     
     try:
         filename = '/content/Train/'+str(ids[i])+'.wav'
         y.append(data[data['ID']==ids[i]]['Class'].values)
         x.append(extract_feature(filename))
     except:
         continue
x = np.array(x)
在前面的代码中,我们逐个处理音频文件,提取其特征并将其存储在输入列表中。类似地,我们将输出类别存储在输出列表中。此外,我们还将把输出列表转换为一个独热编码的类别值:
y2 = []
for i in range(len(y)):
     y2.append(y[i][0])
y3 = np.array(pd.get_dummies(y2))
pd.get_dummies方法的作用与我们之前使用的to_categorical方法非常相似;然而,to_categorical不适用于文本类别(它只适用于数值类型,将其转换为独热编码值)。
- 构建并编译模型:
model = Sequential()
model.add(Dense(1000, input_shape = (40,), activation = 'relu'))
model.add(Dense(10,activation='sigmoid'))
from keras.optimizers import Adam
adam = Adam(lr=0.0001)
model.compile(optimizer=adam, loss='categorical_crossentropy', metrics=['acc'])
前述模型的总结如下:

- 创建训练和测试数据集,然后拟合模型:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(x, y3, test_size=0.30,random_state=10)
model.fit(X_train, y_train, epochs=100, batch_size=32, validation_data=(X_test, y_test), verbose = 1)
一旦模型拟合完成,您会注意到模型在将音频正确分类到相应类别时的准确率为 91%。
股票价格预测
在前面的章节中,我们学习了如何使用神经网络进行音频、文本和结构化数据分析。在这一节中,我们将学习如何通过预测股票价格的案例研究来进行时间序列分析。
准备中
为了预测股票价格,我们将执行以下步骤:
- 
按照从最旧到最新的顺序排列数据集。 
- 
将前五个股票价格作为输入,第六个股票价格作为输出。 
- 
将窗口向前滑动,这样在下一个数据点中,第二个到第六个数据点作为输入,第七个数据点作为输出,依此类推,直到达到最后一个数据点。 
- 
由于我们预测的是一个连续数值,本次的 loss函数将是均方误差值。
此外,我们还将尝试将文本数据与历史数值数据整合,以预测第二天的股票价格。
如何做...
上述策略的代码如下(在实现代码并推荐数据集时,请参阅 GitHub 中的Chapter 3 - stock price prediction.ipynb文件):
- 导入相关的包和数据集:
import pandas as pd
data2 = pd.read_csv('/content/stock_data.csv')
- 准备数据集,其中输入是过去五天的股票价格,输出是第六天的股票价格:
x= []
y = []
for i in range(data2.shape[0]-5):
 x.append(data2.loc[i:(i+4)]['Close'].values)
 y.append(data2.loc[i+5]['Close'])
import numpy as np
x = np.array(x)
y = np.array(y)
- 准备训练和测试数据集,构建模型,编译并拟合它:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(x, y, test_size=0.30,random_state=10)
构建模型并编译它:
from keras.layers import Dense
from keras.models import Sequential, Model
model = Sequential()
model.add(Dense(100, input_dim = 5, activation = 'relu'))
model.add(Dense(1,activation='linear'))
model.compile(optimizer='adam', loss='mean_squared_error')
上述代码结果总结如下:

model.fit(X_train, y_train, epochs=100, batch_size=64, validation_data=(X_test, y_test), verbose = 1)
一旦我们拟合了模型,我们应该注意到,预测股票价格的均方误差值为~$360,或者预测股票价格时的误差为 ~$18。
请注意,以这种方式预测股票价格存在一个陷阱。然而,这将在 RNN 应用章节中处理。
目前,我们将专注于学习神经网络在不同场景中的多种用途。
在下一部分,我们将了解如何将数值数据与新闻标题的文本数据整合到一个模型中。
利用功能性 API
在这一部分,我们将通过将历史价格数据与我们预测的公司最新头条数据结合,继续提高股票价格预测的准确性。
我们将采用以下策略来整合来自多个来源的数据——结构化(历史价格)数据和非结构化(头条)数据:
- 
我们将以类似于将新闻文章分类为主题的方式,将非结构化文本转换为结构化格式。 
- 
我们将通过神经网络传递结构化文本格式,并提取隐藏层输出。 
- 
最后,我们将隐藏层的输出传递到输出层,输出层有一个节点。 
- 
以类似的方式,我们将输入的历史价格数据传递给神经网络,提取隐藏层值,然后将其传递到输出层,输出层有一个输出单元。 
- 
我们将每个单独神经网络操作的输出相乘,以提取最终输出。 
- 
最终输出的平方误差值现在将被最小化。 
如何做...
前述策略的代码如下:
- 让我们从《卫报》提供的 API 获取头条数据,如下所示:
from bs4 import BeautifulSoup
import urllib, json
dates = []
titles = []
for i in range(100):
     try:
         url = 'https://content.guardianapis.com/search?from-date=2010-01-01§ion=business&page-size=200&order-by=newest&page='+str(i+1)+'&q=amazon&api-key=207b6047-a2a6-4dd2-813b-5cd006b780d7'
         response = urllib.request.urlopen(url)
         encoding = response.info().get_content_charset('utf8')
         data = json.loads(response.read().decode(encoding))
     for j in range(len(data['response']['results'])):
         dates.append(data['response']['results'][j]['webPublicationDate'])
         titles.append(data['response']['results'][j]['webTitle']) 
     except:
         break
- 一旦titles和dates被提取出来,我们将预处理数据,将date值转换为date格式,如下所示:
import pandas as pd
data = pd.DataFrame(dates, titles)
data['date']=data['date'].str[:10]
data['date']=pd.to_datetime(data['date'], format = '%Y-%m-%d')
data = data.sort_values(by='date')
data_final = data.groupby('date').first().reset_index()
- 既然我们已经得到了每个日期的最新头条数据,我们将整合这两个数据源,如下所示:
data2['Date'] = pd.to_datetime(data2['Date'],format='%Y-%m-%d')
data3 = pd.merge(data2,data_final, left_on = 'Date', right_on = 'date', how='left')
- 
一旦数据集合并,我们将继续对文本数据进行归一化处理,以去除以下内容: - 
将文本中的所有单词转换为小写,以便像 Text和text这样的单词被视为相同。
- 
去除标点符号,以便像 text.和text这样的单词被视为相同。
- 
去除如 a、and、the等停用词,因为这些词对文本的上下文贡献不大:
 
- 
import nltk
import re
nltk.download('stopwords')
stop = nltk.corpus.stopwords.words('english')
def preprocess(text):
     text = str(text)
     text=text.lower()
     text=re.sub('[⁰-9a-zA-Z]+',' ',text)
     words = text.split()
     words2=[w for w in words if (w not in stop)]
     words4=' '.join(words2)
     return(words4)
data3['title'] = data3['title'].apply(preprocess)
- 用连字符-替换title列中的所有空值:
data3['title']=np.where(data3['title'].isnull(),'-','-'+data3['title'])
现在我们已经预处理了文本数据,接下来为每个单词分配一个 ID。一旦我们完成了这个分配,就可以进行类似于在新闻文章分类部分中所做的文本分析,具体如下:
docs = data3['title'].values
from collections import Counter
counts = Counter()
for i,review in enumerate(docs):
     counts.update(review.split())
words = sorted(counts, key=counts.get, reverse=True)
vocab_size=len(words)
word_to_int = {word: i for i, word in enumerate(words, 1)}
- 鉴于我们已经对所有单词进行了编码,接下来让我们用它们在原文中的对应文本替换:
encoded_docs = []
for doc in docs:
     encoded_docs.append([word_to_int[word] for word in doc.split()])
def vectorize_sequences(sequences, dimension=vocab_size):
     results = np.zeros((len(sequences), dimension+1))
     for i, sequence in enumerate(sequences):
         results[i, sequence] = 1.
     return results
vectorized_docs = vectorize_sequences(encoded_docs)
现在我们已经对文本进行了编码,理解了将如何整合这两个数据源。
- 首先,我们将准备训练集和测试集,如下所示:
x1 = np.array(x)
x2 = np.array(vectorized_docs[5:])
y = np.array(y)
X1_train = x1[:2100,:]
X2_train = x2[:2100, :]
y_train = y[:2100]
X1_test = x1[2100:,:]
X2_test = x2[2100:,:]
y_test = y[2100:]
通常,当期望有多个输入或多个输出时,我们会使用功能性 API。在这种情况下,由于有多个输入,我们将利用功能性 API。
- 本质上,功能性 API 去除了构建模型的顺序过程,具体操作如下。以向量化的文档作为输入,并从中提取输出:
input1 = Input(shape=(2406,))
input1_hidden = (Dense(100, activation='relu'))(input1)
input1_output = (Dense(1, activation='tanh'))(input1_hidden)
在前面的代码中,请注意我们没有使用顺序建模过程,而是通过Dense层定义了各种连接。
请注意,输入的形状是2406,因为过滤过程后剩下了2406个唯一单词。
- 以之前的5个股票价格作为输入,构建模型:
input2 = Input(shape=(5,))
input2_hidden = (Dense(100, activation='relu'))(input2)
input2_output = (Dense(1, activation='linear'))(input2_hidden)
- 我们将对两个输入的输出进行相乘:
from keras.layers import multiply
out = multiply([model, model2])
- 现在我们已经定义了输出,接下来将按如下方式构建模型:
model = Model([input1, input2], out)
model.summary()
请注意,在前面的步骤中,我们使用了Model层来定义输入(作为列表传递)和输出:

上述输出的可视化结果如下:

- 编译并拟合模型:
model.compile(optimizer='adam', loss='mean_squared_error')
model.fit(x=[X2_train, X1_train], y=y_train, epochs=100,batch_size = 32, validation_data = ([X2_test, X1_test], y_test))
上述代码的结果是平均平方误差为~5000,并清楚地表明模型存在过拟合现象,因为训练集的损失远低于测试集的损失。
可能,过拟合是由于向量化文本数据中维度过高所导致的。我们将在第十一章中探讨如何改进这一点,构建递归神经网络。
为行定义权重
在预测房价食谱中,我们了解了如何定义自定义损失函数。然而,我们目前还无法为某些行分配更高的权重。 (我们曾在一个信用违约预测案例研究中做过类似的练习,当时我们为一个类别分配了较高的权重;然而那是一个分类问题,而我们当前解决的问题是一个连续变量预测问题。)
在本节中,我们将为每一行定义权重,并将其传递给我们将要定义的custom_loss函数。
我们将继续使用在股票价格预测食谱中分析过的相同数据集。
如何做到……
- 为了在行级别上指定权重,我们将修改训练和测试数据集,使得按顺序排列后的前2100个数据点属于训练数据集,其余的数据点属于测试数据集:
X_train = x[:2100,:,:]
y_train = y[:2100]
X_test = x[2100:,:,:]
y_test = y[2100:]
- 输入中的一行如果较为近期发生,则会有较高的权重,反之则权重较低:
weights = np.arange(X_train.shape[0]).reshape((X_train.shape[0]),1)/2100
前面的代码块为初始数据点分配了较低的权重,为最近发生的数据点分配了较高的权重。
现在我们已经为每一行定义了权重,我们将在自定义损失函数中包含它们。请注意,在这种情况下,我们的自定义损失函数将包括输出的预测值和实际值,以及需要为每一行分配的权重。
- 部分方法使我们能够传递比实际值和预测值更多的变量给自定义损失函数:
import keras.backend as K
from functools import partial
- 为了将weights传递给custom_loss函数,我们将使用部分函数,将custom_loss和weights作为参数传递给第 7 步。在接下来的代码中,我们定义了custom_loss函数:
def custom_loss_4(y_true, y_pred, weights):
     return K.square(K.abs(y_true - y_pred) * weights)
- 考虑到我们构建的模型有两个输入——输入变量和每一行对应的权重,我们将首先定义这两个输入的shape如下:
input_layer = Input(shape=(5,1))
weights_tensor = Input(shape=(1,))
- 现在我们已经定义了输入,让我们按以下方式初始化接受两个输入的model:
inp1 = Dense(1000, activation='relu')(input_layer)
out = Dense(1, activation='linear')(i3)
model = Model([input_layer, weights_tensor], out)
- 现在我们已经初始化了model,我们将按以下方式定义优化函数:
cl4 = partial(custom_loss_4, weights=weights_tensor)
在前述场景中,我们指定需要最小化custom_loss_4函数,并且向自定义损失函数提供了一个额外的变量(weights_tensor)。
- 最后,在拟合模型之前,我们还将为每一行提供对应测试数据集的weights。考虑到我们正在预测这些值,给某些行提供较低的权重是没有意义的,因为测试数据集并没有提供给模型。然而,我们只会在使用我们定义的模型进行预测时指定这一点(该模型接受两个输入):
test_weights = np.ones((156,1))
- 一旦我们指定了测试数据的weights,我们将继续按照以下方式拟合模型:
model = Model([input_layer, weights_tensor], out)
model.compile(adam, cl4)
model.fit(x=[X_train, weights], y=y_train, epochs=300,batch_size = 32, validation_data = ([X_test, test_weights], y_test))
上述结果导致测试数据集的损失与我们在前一节中看到的结果大不相同。我们将在第十一章,《构建循环神经网络》一章中更详细地探讨这一原因。
在实现上述模型时,必须格外小心,因为它存在一些陷阱。然而,通常建议在进行充分的尽职调查后再实现预测股价波动的模型。
第四章:构建深度卷积神经网络
在本章中,我们将讨论以下内容:
- 
传统神经网络在图像平移时的不准确性 
- 
使用 Python 从零开始构建 CNN 
- 
使用 CNN 改善图像平移时的准确性 
- 
使用 CNN 进行性别分类 
- 
数据增强以提高网络准确性 
介绍
在前一章节中,我们介绍了传统的深度前馈神经网络。传统深度前馈神经网络的一个局限性是它不具有平移不变性,即图像右上角的猫图像会被认为与图像中央的猫图像不同。此外,传统神经网络受物体尺度的影响。如果物体在大多数图像中占据较大的位置,而新图像中的物体较小(占据图像的较小部分),传统神经网络很可能在分类图像时失败。
卷积神经网络(CNNs)用于解决这些问题。由于 CNN 能够处理图像中的平移以及图像的尺度问题,因此在物体分类/检测中被认为更为有效。
在本章中,你将学习以下内容:
- 
传统神经网络在图像平移时的不准确性 
- 
使用 Python 从零开始构建 CNN 
- 
使用 CNN 改善 MNIST 数据集上的图像分类 
- 
实现数据增强以提高网络准确性 
- 
使用 CNN 进行性别分类 
传统神经网络在图像平移时的不准确性
为了进一步理解 CNN 的必要性,我们将首先了解为什么当图像被平移时,前馈神经网络(NN)不起作用,然后看看 CNN 是如何改进传统前馈神经网络的。
我们来看看以下场景:
- 
我们将构建一个神经网络模型来预测 MNIST 数据集中的标签 
- 
我们将考虑所有标签为 1 的图像,并对它们求平均(生成一张平均的 1 标签图像) 
- 
我们将使用传统的神经网络预测我们在上一步生成的平均 1 标签图像的标签 
- 
我们将把平均 1 标签图像平移 1 个像素到左边或右边 
- 
我们将使用传统神经网络模型对平移后的图像进行预测 
如何做到......
上述定义的策略代码如下(请参考 GitHub 中的Issue_with_image translation.ipynb文件以实现代码)
- 下载数据集并提取训练集和测试集的 MNIST 数据集:
from keras.datasets import mnist
from keras.layers import Flatten, Dense
from keras.models import Sequential
import matplotlib.pyplot as plt
%matplotlib inline
(X_train, y_train), (X_test, y_test) = mnist.load_data()
- 获取对应标签1的训练集:
X_train1 = X_train[y_train==1]
- 重新调整和标准化原始训练数据集:
num_pixels = X_train.shape[1] * X_train.shape[2]
X_train = X_train.reshape(X_train.shape[0],num_pixels).astype('float32')
X_test = X_test.reshape(X_test.shape[0],num_pixels).astype('float32')
X_train = X_train / 255
X_test = X_test / 255
- 对输出标签进行独热编码:
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)
num_classes = y_train.shape[1]
- 构建模型并进行拟合:
model = Sequential()
model.add(Dense(1000, input_dim=num_pixels, activation='relu'))
model.add(Dense(num_classes, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam',metrics=['accuracy'])
model.fit(X_train, y_train, validation_data=(X_test, y_test),epochs=5, batch_size=1024, verbose=1)
- 让我们绘制在第二步中获得的平均 1 标签图像:
pic=np.zeros((28,28))
pic2=np.copy(pic)
for i in range(X_train1.shape[0]):
    pic2=X_train1[i,:,:]
    pic=pic+pic2
pic=(pic/X_train1.shape[0])
plt.imshow(pic)
在前面的代码中,我们初始化了一个 28x28 大小的空图像,并通过遍历X_train1对象中的所有值,在标记为 1 的图像的不同像素位置取了平均像素值。
平均 1 图像的绘图如下所示:

需要注意的是,像素越黄色(越厚),人们在该像素上书写的次数越多,而像素越不黄色(更蓝/更薄),人们在该像素上书写的次数就越少。还需要注意的是,图像中央的像素是最黄/最厚的(这是因为大多数人都会在中间的像素上书写,无论整个数字是垂直书写还是向左或向右倾斜)。
传统神经网络的问题
情境 1:让我们创建一个新图像,其中原始图像向左平移了 1 个像素。在下面的代码中,我们遍历图像的列,并将下一列的像素值复制到当前列:
for i in range(pic.shape[0]):
     if i<20:
         pic[:,i]=pic[:,i+1]
     plt.imshow(pic)
左侧翻译后的平均 1 图像如下所示:

让我们继续使用构建好的模型预测图像的标签:
model.predict(pic.reshape(1,784)/255)
模型对翻译后的图像的预测结果如下所示:

我们可以看到预测为 1,尽管它的概率低于像素未翻译时的预测。
情境 2:创建一个新图像,其中原始平均 1 图像的像素向右平移了 2 个像素:
pic=np.zeros((28,28))
pic2=np.copy(pic)
for i in range(X_train1.shape[0]):
    pic2=X_train1[i,:,:]
    pic=pic+pic2
pic=(pic/X_train1.shape[0])
pic2=np.copy(pic)
for i in range(pic.shape[0]):
    if ((i>6) and (i<26)):
    pic[:,i]=pic2[:,(i-1)]
plt.imshow(pic)
右侧翻译后的平均 1 图像如下所示:

这张图像的预测结果如下所示:
model.predict(pic.reshape(1,784)/255)
模型对翻译后的图像的预测结果如下所示:

我们可以看到预测结果不正确,输出为 3。这正是我们通过使用 CNN 来解决的问题。
使用 Python 从零开始构建 CNN
在本节中,我们将通过使用 NumPy 从零开始构建一个前馈网络,学习 CNN 是如何工作的。
准备工作
典型的 CNN 有多个组成部分。在本节中,我们将在理解 CNN 如何改善图像翻译预测准确性之前,了解 CNN 的各个组件。
理解卷积
我们已经了解了典型神经网络是如何工作的。在本节中,让我们理解 CNN 中卷积过程的工作原理。
滤波器
卷积是两个矩阵之间的乘法——一个矩阵较大,另一个较小。为了理解卷积,考虑以下例子:
矩阵 A 如下所示:

矩阵 B 如下所示:

在执行卷积操作时,可以将其视为将较小的矩阵滑动到较大的矩阵上,即在较大的矩阵区域内滑动时,可能会出现九种这样的乘法。请注意,这不是矩阵乘法。
较大矩阵与较小矩阵之间的各种乘法如下:
- {1, 2, 5, 6} 的较大矩阵与 {1, 2, 3, 4} 的较小矩阵相乘:
11 + 22 + 53 + 64 = 44
- {2, 3, 6, 7} 的较大矩阵与 {1, 2, 3, 4} 的较小矩阵相乘:
21 + 32 + 63 + 74 = 54
- {3, 4, 7, 8} 的较大矩阵与 {1, 2, 3, 4} 的较小矩阵相乘:
31 + 42 + 73 + 84 = 64
- {5, 6, 9, 10} 的较大矩阵与 {1, 2, 3, 4} 的较小矩阵相乘:
51 + 62 + 93 + 104 = 84
- {6, 7, 10, 11} 的较大矩阵与 {1, 2, 3, 4} 的较小矩阵相乘:
61 + 72 + 103 + 114 = 94
- {7, 8, 11, 12} 的较大矩阵与 {1, 2, 3, 4} 的较小矩阵相乘:
71 + 82 + 113 + 124 = 104
- {9, 10, 13, 14} 的较大矩阵与 {1, 2, 3, 4} 的较小矩阵相乘:
91 + 102 + 133 + 144 = 124
- {10, 11, 14, 15} 的较大矩阵与 {1, 2, 3, 4} 的较小矩阵相乘:
101 + 112 + 143 + 154 = 134
- {11, 12, 15, 16} 的较大矩阵与 {1, 2, 3, 4} 的较小矩阵相乘:
111 + 122 + 153 + 164 = 144
前述步骤的结果将是以下矩阵:

通常,较小的矩阵称为滤波器或卷积核,滤波器的数值通过梯度下降统计得到。滤波器中的数值是其组成权重。
实际上,当图像输入形状为 224 x 224 x 3 时,其中有 3 个通道,一个 3 x 3 的滤波器也会有 3 个通道,这样就能进行矩阵乘法(求和积)。
一个滤波器的通道数与其乘以的矩阵的通道数相同。
步幅
在前述步骤中,由于滤波器每次水平和垂直移动一步,因此滤波器的步幅为 (1, 1)。步幅数值越大,跳过的矩阵乘法值就越多。
填充
在前述步骤中,我们遗漏了将滤波器的最左边值与原矩阵的最右边值相乘。如果我们执行这样的操作,我们需要确保在原矩阵的边缘周围进行零填充(即图像边缘填充零)。这种填充方式称为 有效 填充。我们在 理解卷积 配方的 滤波器 部分进行的矩阵乘法是 相同 填充的结果。
从卷积到激活
在传统的神经网络中,隐藏层不仅通过权重乘以输入值,还对数据应用非线性处理,即将值通过激活函数传递。
在典型的卷积神经网络中也会发生类似的活动,其中卷积通过激活函数处理。CNN 支持我们目前见过的传统激活函数:sigmoid、ReLU、tanh 和 leaky ReLU。
对于前面的输出,我们可以看到当通过 ReLU 激活函数时,输出保持不变,因为所有数字都是正数。
从卷积激活到池化
在前一部分,我们研究了卷积是如何工作的。在这一部分,我们将了解卷积之后的典型下一步:池化。
假设卷积步骤的输出如下(我们不考虑前面的例子,这是一个新的例子,仅用于说明池化是如何工作的):

在前面的情况下,卷积步骤的输出是一个 2 x 2 矩阵。最大池化会考虑这个 2 x 2 块,并将最大值作为输出。同样,假设卷积步骤的输出是一个更大的矩阵,如下所示:

最大池化将大矩阵分成不重叠的 2 x 2 块(当步幅值为 2 时),如下所示:

从每个块中,只有具有最大值的元素被选中。所以,前面矩阵的最大池化操作输出将是以下内容:

在实践中,并不总是需要一个 2 x 2 的窗口,但它比其他类型的窗口更常用。
其他类型的池化包括求和和平均池化——在实践中,与其他类型的池化相比,我们看到最大池化的应用更多。
卷积和池化是如何帮助的?
在 MNIST 示例中,传统神经网络的一个缺点是每个像素都与一个独特的权重相关联。
因此,如果一个相邻的像素(而不是原始像素)被突出显示,而不是原始像素,那么输出就不会非常准确(比如场景 1中的例子,平均值稍微偏左于中心)。
现在这个问题得到了处理,因为像素共享在每个过滤器中构成的权重。
所有像素都与构成滤波器的所有权重相乘。在池化层中,仅选择卷积后的值较大的值。
这样,无论突出显示的像素是否位于中心,或者稍微偏离中心,输出通常都会是预期的值。
然而,当突出显示的像素远离中心时,问题依然存在。
如何操作...
为了更好地理解,我们将使用 Keras 构建基于 CNN 的架构,并通过从头构建 CNN 的前馈传播部分,与使用 Keras 得到的输出进行对比,来验证我们对 CNN 工作原理的理解。
让我们用一个玩具示例来实现 CNN,其中输入和期望的输出数据已定义(代码文件在 GitHub 上可用,名为 CNN_working_details.ipynb):
- 创建输入和输出数据集:
import numpy as np
X_train=np.array([[[1,2,3,4],[2,3,4,5],[5,6,7,8],[1,3,4,5]],
[[-1,2,3,-4],[2,-3,4,5],[-5,6,-7,8],[-1,-3,-4,-5]]])
y_train=np.array([0,1])
在前面的代码中,我们创建了数据,其中正输入输出 0,负输入输出 1。
- 缩放输入数据集:
X_train = X_train / 8
- 重塑输入数据集,使得每个输入图像以宽度 x高度x通道数的格式表示:
X_train = X_train.reshape(X_train.shape[0],X_train.shape[1],X_train.shape[1],1 ).astype('float32')
- 构建模型架构:
导入相关方法后实例化模型:
from keras.layers import Conv2D, MaxPooling2D, Flatten, Dense
from keras.models import Sequential
model = Sequential()
在下一步中,我们执行卷积操作:
model.add(Conv2D(1, (3,3), input_shape=(4,4,1),activation='relu'))
在前一步中,我们对输入数据执行了二维卷积(在 理解卷积 章节中看到的矩阵乘法),其中使用了 1 个 3 × 3 大小的滤波器。
此外,鉴于这是模型实例化后的第一层,我们指定了输入形状,即 (4, 4, 1)。
最后,我们对卷积的输出执行 ReLU 激活。
在这种情况下,卷积操作的输出形状为 2 × 2 × 1,因为权重与输入的矩阵乘法会得到一个 2 × 2 的矩阵(假设默认步长为 1 × 1)。
此外,输出的大小会缩小,因为我们没有对输入进行填充(即在输入图像周围添加零)。
在下一步中,我们添加一个执行最大池化操作的层,具体如下:
model.add(MaxPooling2D(pool_size=(2, 2))) 
我们对来自上一层的输出执行最大池化操作,池化大小为 2 × 2。这意味着计算图像中 2 × 2 部分的最大值。
请注意,在池化层中使用 2 × 2 的步长,在这种情况下不会影响输出,因为前一步的输出是 2 × 2。然而,一般来说,步长大于 1 × 1 的情况会影响输出形状。
让我们展平池化层的输出:
model.add(Flatten())
一旦我们执行展平操作,过程就变得非常类似于我们在标准前馈神经网络中所执行的操作,在这种网络中,输入与隐藏层连接,再到输出层(我们也可以将输入连接到更多的隐藏层!)。
我们将展平层的输出直接连接到输出层,并使用 sigmoid 激活:
model.add(Dense(1, activation='sigmoid'))
可以获得模型的总结,结果如下:
model.summary()
输出的总结如下:

请注意,卷积层中有 10 个参数,因为一个 3 x 3 的滤波器会有 9 个权重和 1 个偏置项。池化层和展平层没有任何参数,因为它们要么在某个区域提取最大值(最大池化),要么展平上一层的输出(展平层),因此在这些层中没有需要修改权重的操作。
输出层有两个参数,因为展平层有一个输出,该输出连接到输出层,输出层有一个值——因此我们将有一个权重和一个偏置项连接展平层和输出层。
- 编译并训练模型:
model.compile(loss='binary_crossentropy', optimizer='adam',metrics=['accuracy'])
在前面的代码中,我们将损失函数指定为二元交叉熵,因为输出结果要么是1,要么是0。
- 训练模型:
model.fit(X_train, y_train, epochs = 500)
我们正在训练模型,以获得将输入层与输出层连接的最优权重。
验证 CNN 输出
现在我们已经训练了模型,让我们通过实现 CNN 的前向传播部分来验证我们从模型中获得的输出:
- 让我们提取权重和偏置呈现的顺序:
model.weights

你可以看到卷积层的权重首先被展示,然后是偏置,最后是输出层中的权重和偏置。
还请注意,卷积层中的权重形状是(3, 3, 1, 1),因为滤波器的形状是 3 x 3 x 1(因为图像是三维的:28 x 28 x 1),最后的 1(形状中的第四个值)表示在卷积层中指定的滤波器数量。
如果我们在卷积中指定了 64 个滤波器,则权重的形状将是 3 x 3 x 1 x 64。
类似地,如果卷积操作是在具有 3 个通道的图像上执行的,则每个滤波器的形状将是 3 x 3 x 3。
- 提取各层的权重值:
model.get_weights()
- 让我们提取第一个输入的输出,以便我们能够通过前向传播验证它:
model.predict(X_train[0].reshape(1,4,4,1))

我们运行的迭代输出为 0.0428(当你运行模型时,这个值可能会不同,因为权重的随机初始化可能不同),我们将通过执行矩阵乘法来验证它。
我们在将输入传递给预测方法时正在重新调整输入的形状,因为该方法期望输入的形状为(None, 4, 4, 1),其中 None 表示批次大小可以是任意数字。
- 执行滤波器与输入图像的卷积操作。请注意,输入图像的形状是 4 x 4,而滤波器的形状是 3 x 3。在这里,我们将在代码中沿着行和列执行矩阵乘法(卷积):
sumprod = []
for i in range(X_train[0].shape[0]-model.get_weights()[0].shape[0]+1):
     for j in range(X_train[0].shape[0]-model.get_weights()[0].shape[0]+1):
         img_subset = np.array(X_train[0,i:(i+3),j:(j+3),0])
         filter = model.get_weights()[0].reshape(3,3)
         val = np.sum(img_subset*filter) + model.get_weights()[1]
         sumprod.append(val)
在前面的代码中,我们初始化了一个名为sumprod的空列表,用来存储每次滤波器与图像子集(图像子集的大小与滤波器一致)进行矩阵乘法的输出。
- 重新调整 sumprod的输出形状,以便将其传递给池化层:
sumprod= np.array(sumprod).reshape(2,2,1)
- 在将卷积输出传递到池化层之前,先对其进行激活操作:
sumprod = np.where(sumprod>0,sumprod,0)
- 将卷积输出传递到池化层。然而,在当前的情况下,由于卷积输出是 2 x 2,我们将简单地取出在 第 6 步 中获得的输出的最大值:
pooling_layer_output = np.max(sumprod)
- 将池化层的输出连接到输出层:
intermediate_output_value = pooling_layer_output*model.get_weights()[2]+model.get_weights()[3]
我们将池化层的输出与输出层的权重相乘,并加上输出层的偏置。
- 计算 sigmoid 输出:
1/(1+np.exp(-intermediate_output_value))
前一步操作的输出如下:

你在这里看到的输出将与我们使用 model.predict 方法获得的输出相同,从而验证我们对 CNN 工作原理的理解。
CNN 用于提高图像平移情况下的准确性
在前面的章节中,我们学习了图像平移问题以及 CNN 是如何工作的。在这一节中,我们将利用这些知识,学习 CNN 如何通过改进预测精度来处理图像平移。
准备中
我们将采用的构建 CNN 模型的策略如下:
- 
由于输入形状为 28 x 28 x 1,滤波器的大小应为 3 x 3 x 1: - 请注意,滤波器的大小可以变化,但通道的数量不能变化
 
- 
让我们初始化 10 个滤波器 
- 
我们将在前一步中对输入图像进行 10 个滤波器卷积得到的输出上执行池化操作: - 这将导致图像尺寸的减半
 
- 
我们将展平池化操作后的输出 
- 
展平层将连接到另一个具有 1,000 个单元的隐藏层 
- 
最后,我们将隐藏层连接到输出层,其中有 10 个可能的类别(因为有 10 个数字,从 0 到 9) 
一旦我们构建好模型,我们将对平均 1 图像 1 像素进行平移,然后测试 CNN 模型在平移图像上的预测结果。请注意,在这种情况下,前馈神经网络架构无法预测正确的类别。
如何实现...
让我们通过代码理解如何在 MNIST 数据上使用 CNN(代码文件可在 GitHub 上找到,文件名为 CNN_image_translation.ipynb):
- 加载并预处理数据:
(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train = X_train.reshape(X_train.shape[0],X_train.shape[1],X_train.shape[1],1 ).astype('float32')
X_test = X_test.reshape(X_test.shape[0],X_test.shape[1],X_test.shape[1],1).astype('float32')
X_train = X_train / 255
X_test = X_test / 255
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)
num_classes = y_test.shape[1]
请注意,我们在此步骤中执行的所有步骤与我们在 第二章 构建深度前馈神经网络 中所执行的相同。
- 构建并编译模型:
from keras.layers import Conv2D, MaxPooling2D, Flatten, Dense
from keras.models import Sequential
model = Sequential()
model.add(Conv2D(10, (3,3), input_shape=(28, 28,1),activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(1000, activation='relu'))
model.add(Dense(num_classes, activation='softmax'))
我们在前面的代码中初始化的模型的摘要如下:
model.summary()
模型摘要如下:

卷积层总共有 100 个参数,因为有 10 个 3 x 3 x 1 的滤波器,总共有 90 个权重参数。另外,10 个偏置项(每个滤波器一个)加起来形成卷积层的 100 个参数。
请注意,最大池化没有任何参数,因为它是从 2 × 2 大小的区域内提取最大值。
- 训练模型:
model.fit(X_train, y_train, validation_data=(X_test, y_test),epochs=5, batch_size=1024, verbose=1)
前述模型在 5 个训练周期中达到了 98% 的准确率:

- 让我们识别出平均的 1 张图像,然后将其平移 1个单位:
X_test1 = X_test[y_test[:,1]==1]
在前述代码中,我们筛选出了所有标签为 1 的图像输入:
import numpy as np
pic=np.zeros((28,28))
pic2=np.copy(pic)
for i in range(X_test1.shape[0]):
     pic2=X_test1[i,:,:,0]
     pic=pic+pic2
pic=(pic/X_test1.shape[0])
在前面的代码中,我们取了平均的 1 张图像:
for i in range(pic.shape[0]):
     if i<20:
         pic[:,i]=pic[:,i+1]
在前述代码中,我们将平均的 1 张图像中的每个像素向左平移 1 个单位。
- 对翻译后的 1 张图像进行预测:
model.predict(pic.reshape(1,28,28,1))
前一步的输出结果如下:

请注意,当前使用 CNN 进行预测时,相较于深度前馈神经网络模型(在 传统神经网络在图像翻译时的不准确性 部分预测为 0.6335),其预测值(0.9541)对标签 1 的概率更高。
使用 CNN 进行性别分类
在前面的章节中,我们学习了 CNN 是如何工作的,以及 CNN 是如何解决图像翻译问题的。
在本节中,我们将通过构建一个模型,进一步了解 CNN 是如何工作的,目的是检测图像中人物的性别。
准备就绪
在本节中,我们将制定如何解决该问题的策略:
- 
我们将收集图像数据集,并根据图像中人物的性别对每张图像进行标签 
- 
我们只会处理 2,000 张图像,因为数据获取过程对我们的数据集来说耗时较长(因为在这个案例中我们是手动从网站下载图像)。 
- 
此外,我们还将确保数据集中男性和女性图像的比例相等。 
- 
一旦数据集准备好,我们将把图像调整为相同的大小,以便它们可以输入到 CNN 模型中。 
- 
我们将构建 CNN 模型,输出层的类别数为两个标签的数量 
- 
鉴于这是一个从数据集中预测两个标签之一的案例,我们将最小化二元交叉熵损失。 
如何实现...
在本节中,我们将编码之前定义的策略(代码文件已上传至 GitHub,文件名为 Gender classification.ipynb):
- 下载数据集:
$ wget https://d1p17r2m4rzlbo.cloudfront.net/wp-content/uploads/2017/04/a943287.csv
- 加载数据集并检查其内容:
import pandas as pd, numpy as np
from skimage import io
# Location of file is /content/a943287.csv
# be sure to change to location of downloaded file on your machine
data = pd.read_csv('/content/a943287.csv')
data.head() 
数据集中的一些关键字段示例如下:

- 从数据集中提供的 URL 链接获取 1,000 张男性图像和 1,000 张女性图像:
data_male = data[data['please_select_the_gender_of_the_person_in_the_picture']=="male"].reset_index(drop='index')
data_female = data[data['please_select_the_gender_of_the_person_in_the_picture']=="female"].reset_index(drop='index')
final_data = pd.concat([data_male[:1000],data_female[:1000]],axis=0).reset_index(drop='index')
在前述代码中,final_data 包含了 1,000 张男性图像和 1,000 张女性图像的 URL 链接。读取这些 URL 链接并获取对应的图像。确保所有图像的尺寸为 300 × 300 × 3(因为该数据集中大多数图像都是这个尺寸),并且处理任何禁止访问的问题:
x = []
y = []
for i in range(final_data.shape[0]):
     try:
         image = io.imread(final_data.loc[i]['image_url'])
         if(image.shape==(300,300,3)):
             x.append(image)
             y.append(final_data.loc[i]['please_select_the_gender_of_the_person_in_the_picture'])
     except:
         continue
输入样本及其对应的情感标签如下所示:

- 创建输入和输出数组:
x2 = []
y2 = []
for i in range(len(x)):
      img = cv2.cvtColor(x[i], cv2.COLOR_BGR2GRAY)
      img2 = cv2.resize(img,(50,50))
      x2.append(img2)
      img_label = np.where(y[i]=="male",1,0)
      y2.append(img_label)
在前面的步骤中,我们已经将彩色图像转换为灰度图像,因为图像的颜色可能会增加额外的信息(我们将在第五章,迁移学习中验证这个假设)。
此外,我们将图像调整为较小的尺寸(50 x 50 x 1)。结果如下所示:

最后,我们将输出转换为一热编码版本。
- 创建训练集和测试集。首先,我们将输入和输出列表转换为数组,然后调整输入的形状,使其能够作为 CNN 的输入:
x2 = np.array(x2)
x2 = x2.reshape(x2.shape[0],x2.shape[1],x2.shape[2],1)
Y = np.array(y2)
x2的第一个值的输出如下:

请注意,输入的值在0到255之间,因此我们必须对其进行缩放:
X = np.array(x2)/255
Y = np.array(y2)
最后,我们将输入和输出数组分割成训练集和测试集:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X,Y, test_size=0.1, random_state=42)
print(X_train.shape, X_test.shape, y_train.shape, y_test.shape)
训练和测试输入、输出数组的形状如下:

- 构建并编译模型:
from keras.layers import Conv2D, MaxPooling2D, Flatten, Dense
from keras.models import Sequential
model = Sequential()
model.add(Conv2D(64, kernel_size=(3, 3), activation='relu',input_shape=(50,50,1)))
model.add(MaxPooling2D(pool_size=(5, 5)))
model.add(Conv2D(128, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(256, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(512, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(Flatten())
model.add(Dense(100, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()
模型的总结如下:

请注意,卷积层输出的通道数将等于该层中指定的过滤器数量。此外,我们对第一个卷积层的输出进行了稍微更激进的池化。
现在,我们将编译模型,以最小化二元交叉熵损失(因为输出只有两个类别),如下所示:
model.compile(loss='binary_crossentropy',optimizer='adam',metrics=['accuracy'])
- 拟合模型:
history = model.fit(X_train, y_train, batch_size=32, epochs=50,verbose=1,validation_data = (X_test, y_test))

一旦我们拟合模型,就可以看到之前的代码在预测图像中的性别时,准确率约为 80%。
还有更多内容...
可以通过以下方法进一步提高分类的准确性:
- 
处理更多图像 
- 
处理更大的图像(而不是 50 x 50 的图像),这些图像将用于训练更大的网络 
- 
利用迁移学习(将在第五章中讨论,迁移学习) 
- 
通过正则化和丢弃法避免过拟合 
数据增强以提高网络准确率
如果图像从原始位置移动,则很难准确分类图像。然而,给定一张图像,无论我们如何平移、旋转或缩放图像,图像的标签保持不变。数据增强是一种从给定图像集创建更多图像的方法,即通过旋转、平移或缩放它们,并将它们映射到原始图像的标签。
这个直觉如下:即使图像稍微旋转,或者图像中的人从图像中间移到图像的最右边,图像仍然会对应于该人。
因此,我们应该能够通过旋转和平移原始图像来创建更多的训练数据,而我们已经知道每个图像对应的标签。
准备工作
在这个示例中,我们将使用 CIFAR-10 数据集,该数据集包含 10 个不同类别的物体图像。
我们将使用的策略如下:
- 
下载 CIFAR-10 数据集 
- 
预处理数据集 - 
对输入值进行缩放 
- 
对输出类别进行独热编码 
 
- 
- 
构建一个包含多个卷积和池化层的深度 CNN 
- 
编译并拟合模型,测试其在测试数据集上的准确性 
- 
生成训练数据集中原始图像的随机平移 
- 
对在上一步中构建的相同模型架构进行拟合,使用全部图像(生成的图像加上原始图像) 
- 
检查模型在测试数据集上的准确性 
我们将使用ImageDataGenerator方法在keras.preprocessing.image包中实现数据增强。
如何操作…
为了理解数据增强的好处,让我们通过一个例子来计算 CIFAR-10 数据集在使用和不使用数据增强情况下的准确性(代码文件在 GitHub 中以Data_augmentation_to_improve_network_accuracy.ipynb提供)。
无数据增强的模型准确性
让我们在以下步骤中计算无数据增强的准确性:
- 导入包和数据:
from matplotlib import pyplot as plt
%matplotlib inline
import numpy as np
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers.core import Dense, Dropout, Activation, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras.layers.normalization import BatchNormalization
from keras import regularizers
from keras.datasets import cifar10
(X_train, y_train), (X_val, y_val) = cifar10.load_data()
- 预处理数据:
X_train = X_train.astype('float32')/255.
X_val = X_val.astype('float32')/255.
n_classes = 10
y_train = np_utils.to_categorical(y_train, n_classes)
y_val = np_utils.to_categorical(y_val, n_classes)
以下是图像样本及其对应标签:

- 构建并编译模型:
input_shape = X_train[0].shape
model = Sequential()
model.add(Conv2D(32, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay), input_shape=X_train.shape[1:]))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(32, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.2)) 
model.add(Conv2D(64, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(64, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.3)) 
model.add(Conv2D(128, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(128, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.4)) 
model.add(Flatten())
model.add(Dense(10, activation='softmax'))
from keras.optimizers import Adam
adam = Adam(lr = 0.01)
model.compile(loss='categorical_crossentropy', optimizer=adam,metrics=['accuracy'])
我们使用较高的学习率仅仅是为了让模型在更少的轮次内更快地收敛。这使得我们能够更快速地比较数据增强场景与非数据增强场景。理想情况下,我们会使用较小的学习率让模型运行更多的轮次。
- 拟合模型:
model.fit(X_train, y_train, batch_size=32,epochs=10, verbose=1, validation_data=(X_val, y_val))
该网络的准确率约为 66%:

使用数据增强的模型准确性
在以下代码中,我们将实现数据增强:
- 使用ImageDataGenerator方法在keras.preprocessing.image包中:
from keras.preprocessing.image import ImageDataGenerator 
datagen = ImageDataGenerator(
    rotation_range=20,
    width_shift_range=0,
    height_shift_range=0,
    fill_mode = 'nearest')
datagen.fit(X_train)
在前面的代码中,我们正在生成新图像,这些图像会在 0 到 20 度之间随机旋转。经过数据生成器处理后的图像样本如下:

注意,与之前的图像集相比,这些图像略微倾斜。
- 现在,我们将通过数据生成器将所有数据传递出去,如下所示:
batch_size = 32
model = Sequential()
model.add(Conv2D(32, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay), input_shape=X_train.shape[1:]))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(32, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.2)) 
model.add(Conv2D(64, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(64, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.3)) 
model.add(Conv2D(128, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(128, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.4)) 
model.add(Flatten())
model.add(Dense(10, activation='softmax'))
from keras.optimizers import Adam
adam = Adam(lr = 0.01)
model.compile(loss='categorical_crossentropy', optimizer=adam, metrics=['accuracy'])
- 请注意,我们正在重建模型,以便在比较数据增强和非数据增强场景时再次初始化权重:
model.fit_generator(datagen.flow(X_train, y_train, batch_size=batch_size),steps_per_epoch=X_train.shape[0] // batch_size, epochs=10,validation_data=(X_val,y_val))
请注意,fit_generator方法会在生成新图像的同时拟合模型。
- 此外,datagen.flow指定了根据我们在步骤1中初始化的数据生成策略需要生成新的训练数据点。与此同时,我们还指定了每个 epoch 的步数,作为总数据点数与批次大小的比例:

这个代码的准确率约为 80%,比仅使用给定数据集(不进行数据增强)时的 66%准确率更高。
第五章:迁移学习
在上一章中,我们学习了如何识别图像属于哪个类别。本章将介绍 CNN 的一个缺点,以及如何通过使用某些预训练模型来克服这一问题。
本章将涵盖以下内容:
- 
使用 CNN 进行图像中人物的性别分类 
- 
使用基于 VGG16 架构的模型进行图像中人物的性别分类 
- 
可视化神经网络中间层的输出 
- 
使用基于 VGG19 架构的模型进行图像中人物的性别分类 
- 
使用基于 ResNet 架构的模型进行性别分类 
- 
使用基于 Inception 架构的模型进行性别分类 
- 
检测人脸图像中的关键点 
使用卷积神经网络(CNN)进行图像中人物的性别分类
为了了解 CNN 的一些局限性,让我们通过一个示例来识别给定图像是猫还是狗。
准备就绪
通过以下步骤,我们将直观地了解卷积神经网络如何预测图像中物体的类别:
- 
卷积滤波器由图像的某些部分激活: - 例如,某些滤波器可能会在图像具有某种模式时激活——例如,图像包含一个圆形结构
 
- 
池化层确保图像平移问题得以处理: - 这确保了即使图像较大,通过多次池化操作,图像的大小变小,物体也可以被检测到,因为物体现在应该出现在图像的较小部分(因为它已经多次池化)
 
- 
最终的扁平层将所有通过不同卷积和池化操作提取的特征进行扁平化 
假设训练数据集中的图像数量很少。在这种情况下,模型没有足够的数据点来对测试数据集进行泛化。
此外,考虑到卷积从头开始学习各种特征,如果训练数据集中的图像具有较大的形状(宽度和高度),则可能需要多个周期才能让模型开始适应训练数据集。
因此,在下一部分,我们将编写以下构建 CNN 的情景代码,其中包含一些图像(大约 1,700 张图像),并测试不同形状图像的准确性:
- 
在 10 个周期中,当图像尺寸为 300 X 300 时的准确性 
- 
在 10 个周期中,当图像尺寸为 50 X 50 时的准确性 
如何做到……
在本部分中,我们将获取一个数据集并进行分类分析,其中一个情景的图像尺寸为 300 x 300,而另一个情景的图像尺寸为 50 x 50。(在实现代码时,请参考 GitHub 上的Transfer_learning.ipynb文件。)
情景 1——大图像
- 获取数据集。对于此分析,我们将继续使用在第四章的性别分类案例研究中下载的男性与女性分类数据集,构建深度卷积神经网络:
$ wget https://d1p17r2m4rzlbo.cloudfront.net/wp-content/uploads/2017/04/a943287.csv 
import pandas as pd, numpy as np
from skimage import io
# Location of file is /content/a943287.csv
# be sure to change to location of downloaded file on your machine
data = pd.read_csv('/content/a943287.csv')
data_male = data[data['please_select_the_gender_of_the_person_in_the_picture']=="male"].reset_index(drop='index')
data_female = data[data['please_select_the_gender_of_the_person_in_the_picture']=="female"].reset_index(drop='index')
final_data = pd.concat([data_male[:1000],data_female[:1000]],axis=0).reset_index(drop='index')
- 提取图像路径,然后准备输入和输出数据:
x = []
y = []
for i in range(final_data.shape[0]):
     try:
         image = io.imread(final_data.loc[i]['image_url'])
         if(image.shape==(300,300,3)):
             x.append(image)
             y.append(final_data.loc[i]['please_select_the_gender_of_the_person_in_the_picture'])
     except:
         continue
- 以下是图像的示例:

请注意,所有图像的大小为 300 x 300 x 3。
- 创建输入和输出数据集数组:
x2 = []
y2 = []
for i in range(len(x)):
      x2.append(x[i])
      img_label = np.where(y[i]=="male",1,0)
      y2.append(img_label)
在前一步中,我们遍历了所有图像(逐一进行),将图像读取到一个数组中(在这次迭代中其实可以跳过此步骤。然而,在下一个调整图像大小的场景中,我们将在此步骤调整图像大小)。此外,我们还存储了每个图像的标签。
- 准备输入数组,以便可以传递给 CNN。此外,准备输出数组:
x2 = np.array(x2)
x2 = x2.reshape(x2.shape[0],x2.shape[1],x2.shape[2],3)
在这里,我们将数组列表转换为 numpy 数组,以便将其传递给神经网络。
缩放输入数组并创建输入和输出数组:
X = np.array(x2)/255
Y = np.array(y2)
- 创建训练和测试数据集:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X,Y, test_size=0.1, random_state=42)
- 定义模型并编译它:
from keras.models import Sequential
from keras.layers.core import Dense, Dropout, Activation, Flatten
from keras.layers.convolutional import Conv2D
from keras.layers.pooling import MaxPooling2D
from keras.optimizers import SGD
from keras import backend as K
model = Sequential()
model.add(Conv2D(64, kernel_size=(3, 3), activation='relu',input_shape=(300,300,3)))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(128, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(256, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(512, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(Flatten())
model.add(Dense(100, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()
在前面的代码中,我们构建了一个模型,其中包含多个卷积层、池化层和 dropout 层。此外,我们将最终 dropout 层的输出传递到一个展平层,然后将展平后的输出连接到一个 512 节点的隐藏层,最后将隐藏层连接到输出层。
模型摘要如下:

在以下代码中,我们编译模型以减少二进制交叉熵损失,如下所示:
model.compile(loss='binary_crossentropy',optimizer='adam',metrics=['accuracy'])
- 训练模型:
history = model.fit(X_train, y_train, batch_size=32,epochs=10,verbose=1,validation_data = (X_test, y_test))
在前一步中,你可以看到,随着训练轮次的增加,模型并没有继续训练,如下图所示(此图的代码与我们在第二章的缩放输入数据部分看到的代码相同,并且可以在本章的 GitHub 仓库中找到):

在前面的图表中,你可以看到模型几乎没有学习到任何东西,因为损失几乎没有变化。而且,准确率卡在了 51% 左右(这大约是原始数据集中男性与女性图像的分布比例)。
场景 2 – 较小的图像
在这个场景中,我们将在模型中做如下修改:
- 
输入图像大小: - 我们将把大小从 300 x 300 缩小到 50 x 50
 
- 
模型架构: - 架构的结构与我们在场景 1 – 大图像中看到的相同
 
- 创建一个数据集,输入为调整后图像大小(50 x 50 x 3),输出为标签。为此,我们将继续从场景 1 的第 4 步开始:
import cv2
x2 = []
y2 = []
for i in range(len(x)):
  img = cv2.resize(x[i],(50,50))
  x2.append(img)
  img_label = np.where(y[i]=="male",1,0)
  y2.append(img_label)
- 创建训练和测试数据集的输入和输出数组:
x2 = np.array(x2)
x2 = x2.reshape(x2.shape[0],x2.shape[1],x2.shape[2],3)
X = np.array(x2)/255
Y = np.array(y2)
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X,Y, test_size=0.1, random_state=42)
- 构建和编译模型:
model = Sequential()
model.add(Conv2D(64, kernel_size=(3, 3), activation='relu',input_shape=(50,50,3)))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(128, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(256, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(512, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(Flatten())
model.add(Dense(100, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()
model.compile(loss='binary_crossentropy',optimizer='adam',metrics=['accuracy'])
模型摘要如下:

- 训练模型:
history = model.fit(X_train, y_train, batch_size=32,epochs=10,verbose=1,validation_data = (X_test, y_test))
模型在训练和测试数据集上随着训练轮次的增加,准确率和损失情况如下:

请注意,虽然在最初的训练和测试数据集上,准确率有所提高且损失逐渐下降,但随着训练轮次的增加,模型开始在训练数据上过拟合(专注),并且在测试数据集上的准确率为约 76%。
从中我们可以看到,当输入大小较小且卷积核必须从图像的较小部分学习时,CNN 能正常工作。然而,随着图像大小的增加,CNN 在学习上遇到了困难。
鉴于我们已经发现图像大小对模型准确率有影响,在新的场景中,我们将使用激进的池化,以确保较大的图像(300 x 300 形状)能迅速缩小。
场景 3 – 对大图像进行激进的池化
在下面的代码中,我们将保留在场景 1 中执行到第 6 步的分析。然而,唯一的变化将是模型架构;在接下来的模型架构中,我们使用了比场景 1 中更激进的池化方法。
在以下架构中,每一层具有更大的池化窗口,确保我们能够捕捉到比使用较小池化大小时更大的区域的激活。模型架构如下:
model = Sequential()
model.add(Conv2D(64, kernel_size=(3, 3), activation='relu',input_shape=(300,300,3)))
model.add(MaxPooling2D(pool_size=(3, 3)))
model.add(Conv2D(128, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(MaxPooling2D(pool_size=(3, 3)))
model.add(Conv2D(256, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(MaxPooling2D(pool_size=(3, 3)))
model.add(Conv2D(512, kernel_size=(3, 3), activation='relu',padding='same'))
model.add(Flatten())
model.add(Dense(100, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()
请注意,在这个架构中,池化大小是 3 x 3,而不是我们在先前场景中使用的 2 x 2:

一旦我们在输入和输出数组上拟合模型,训练和测试数据集上准确率和损失的变化如下:
model.compile(loss='binary_crossentropy',optimizer='adam',metrics=['accuracy'])
history = model.fit(X_train, y_train, batch_size=32,epochs=10,verbose=1,validation_data = (X_test, y_test))
以下是前面代码的输出结果:

我们可以看到,测试数据在正确分类图像中的性别时,准确率约为 70%。
然而,你可以看到,在训练数据集上存在相当大的过拟合现象(因为训练数据集上的损失持续下降,而测试数据集上并未出现类似下降)。
使用基于 VGG16 架构的模型对图像中人的性别进行分类
在前一部分的基于 CNN 的性别分类中,我们看到,当我们从头开始构建 CNN 模型时,可能会遇到以下一些情况:
- 
传递的图像数量不足以让模型学习 
- 
当图像尺寸较大时,卷积层可能无法学习到我们图像中的所有特征 
第一个问题可以通过在大数据集上执行我们的分析来解决。第二个问题可以通过在较大数据集上训练更大的网络,并进行更长时间的训练来解决。
然而,虽然我们能够执行所有这些操作,但往往缺乏进行此类分析所需的数据量。使用预训练模型进行迁移学习可以在这种情况下提供帮助。
ImageNet 是一个流行的竞赛,参与者需要预测图像的不同类别,图像的大小各异,并且包含多个类别的对象。
有多个研究团队参与了这场竞争,目标是提出一个能够预测包含数百万图像的数据集中的多类图像的模型。由于数据集中有数百万图像,数据集有限性的问题得以解决。此外,鉴于研究团队们建立了巨大的网络,卷积网络学习多种特征的问题也得到了解决。
因此,我们可以重用在不同数据集上构建的卷积层,在这些卷积层中,网络学习预测图像中的各种特征,然后将这些特征传递通过隐藏层,以便我们能够预测针对特定数据集的图像类别。不同的研究小组开发了多个预训练模型,本文将介绍 VGG16。
准备工作
在这一部分,我们将尝试理解如何利用 VGG16 的预训练网络来进行性别分类练习。
VGG16 模型的架构如下:

请注意,模型的架构与我们在“使用 CNN 进行性别分类”一节中训练的模型非常相似。主要的区别在于,这个模型更深(更多的隐藏层)。此外,VGG16 网络的权重是通过在数百万图像上训练得到的。
我们将确保在训练我们的模型以分类图像中的性别时,VGG16 的权重不会被更新。通过性别分类练习(形状为 300 x 300 x 3 的图像)的输出形状是 9 x 9 x 512。
我们将保留原网络中的权重,提取 9 x 9 x 512 的输出,通过另一个卷积池化操作,进行平坦化,连接到隐藏层,并通过 sigmoid 激活函数来判断图像是男性还是女性。
本质上,通过使用 VGG16 模型的卷积层和池化层,我们是在使用在更大数据集上训练得到的滤波器。最终,我们将对这些卷积层和池化层的输出进行微调,以适应我们要预测的对象。
如何做……
有了这个策略,让我们按照以下方式编写代码(在实现代码时,请参考 GitHub 中的Transfer_learning.ipynb文件):
- 导入预训练模型:
from keras.applications import vgg16
from keras.utils.vis_utils import plot_model
from keras.applications.vgg16 import preprocess_input
vgg16_model = vgg16.VGG16(include_top=False, weights='imagenet',input_shape=(300,300,3))
请注意,我们在 VGG16 模型中排除了最后一层。这是为了确保我们根据我们要解决的问题对 VGG16 模型进行微调。此外,鉴于我们的输入图像尺寸为 300 X 300 X 3,我们在下载 VGG16 模型时也指定了相同的尺寸。
- 预处理图像集。这个预处理步骤确保图像的处理方式能够被预训练的模型接受作为输入。例如,在下面的代码中,我们对其中一张名为img的图像进行预处理:
from keras.applications.vgg16 import preprocess_input
img = preprocess_input(img.reshape(1,224,224,3))
我们使用preprocess_input方法按照 VGG16 的预处理要求来预处理图像。
- 创建输入和输出数据集。在本练习中,我们将从“使用 CNN 进行性别分类”场景 1 的第 3 步结束继续。在这里,创建输入和输出数据集的过程与我们之前做的一样,唯一的变化是使用 VGG16 模型提取特征。
我们将通过vgg16_model传递每张图像,以便将vgg16_model的输出作为处理后的输入。此外,我们还将对输入进行如下预处理:
import cv2
x2_vgg16 = []
for i in range(len(x)):
    img = x[i]
    img = preprocess_input(img.reshape(1,300,300,3))
现在,我们将预处理后的输入传递给 VGG16 模型以提取特征,如下所示:
    img_new = vgg16_model.predict(img.reshape(1,300,300,3))
    x2_vgg16.append(img_new)
在前面的代码中,除了将图像传递给 VGG16 模型外,我们还将输入值存储在一个列表中。
- 将输入和输出转换为 NumPy 数组,并创建训练和测试数据集:
x2_vgg16 = np.array(x2_vgg16)
x2_vgg16= x2_vgg16.reshape(x2_vgg16.shape[0],x2_vgg16.shape[2],x2_vgg16.shape[3],x2_vgg16.shape[4])
Y = np.array(y2)
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(x2_vgg16,Y, test_size=0.1, random_state=42)
- 构建并编译模型:
model_vgg16 = Sequential()
model_vgg16.add(Conv2D(512, kernel_size=(3, 3), activation='relu',input_shape=(X_train.shape[1],X_train.shape[2],X_train.shape[3])))
model_vgg16.add(MaxPooling2D(pool_size=(2, 2)))
model_vgg16.add(Flatten())
model_vgg16.add(Dense(512, activation='relu'))
model_vgg16.add(Dropout(0.5))
model_vgg16.add(Dense(1, activation='sigmoid'))
model_vgg16.summary()
模型摘要如下:

编译模型:
model_vgg16.compile(loss='binary_crossentropy',optimizer='adam',metrics=['accuracy'])
- 在缩放输入数据时训练模型:
history_vgg16 = model_vgg16.fit(X_train/np.max(X_train), y_train, batch_size=16,epochs=10,verbose=1,validation_data = (X_test/np.max(X_train), y_test))
一旦我们训练模型,我们应该能看到,在前几轮训练中,模型能够在测试数据集上达到约 89%的准确率:

将此与我们在“使用 CNN 进行性别分类”部分中构建的模型进行对比,在那些场景中,我们无法在 10 轮训练内实现 80%的分类准确率。
以下是一些模型误分类的图像示例:

请注意,在前面的图片中,当输入图像是面部的一部分,或者图像中的物体占据了图像总面积的较小部分,或者标签可能不正确时,模型可能会误分类。
可视化神经网络中间层的输出
在前一部分,我们构建了一个可以从图像中学习性别分类的模型,准确率为 89%。然而,到目前为止,在滤波器学到了什么方面,它对我们来说仍然是一个黑箱。
在这一部分,我们将学习如何提取模型中各个滤波器学到了什么。此外,我们还将对比初始层中的滤波器学到的内容与最后几层中的特征学到的内容。
准备就绪
为了理解如何提取各个滤波器学到了什么,让我们采用以下策略:
- 
我们将选择一张图像进行分析。 
- 
我们将选择第一个卷积层,以理解第一个卷积中的各个滤波器学到了什么。 
- 
计算第一层权重与输入图像卷积的输出: - 
在此步骤中,我们将提取模型的中间输出: - 我们将提取模型的第一层输出。
 
 
- 
- 
为了提取第一层的输出,我们将使用功能性 API: - 功能性 API 的输入是输入图像,输出将是第一层的输出。
 
- 
这将返回所有通道(滤波器)中间层的输出。 
- 
我们将对卷积的第一层和最后一层执行这些步骤。 
- 
最后,我们将可视化所有通道的卷积操作输出。 
- 
我们还将可视化给定通道在所有图像上的输出。 
如何操作...
在本节中,我们将编写代码,展示初始层和最终层的卷积滤波器所学习的内容。
我们将重用在使用 CNN 进行性别分类食谱的情景 1 中步骤 1到步骤 4准备的数据(在实现代码时,请参考 GitHub 中的Transfer_learning.ipynb文件):
- 选择一张图像来可视化其中间输出:
plt.imshow(x[3])
plt.grid('off')

- 定义功能性 API,输入为图像,输出为第一卷积层的输出:
from keras.applications.vgg16 import preprocess_input
model_vgg16.predict(vgg16_model.predict(preprocess_input(x[3].reshape(1,300,300,3)))/np.max(X_train))
from keras import models
activation_model = models.Model(inputs=vgg16_model.input,outputs=vgg16_model.layers[1].output)
activations = activation_model.predict(preprocess_input(x[3].reshape(1,300,300,3)))
我们定义了一个名为activation_model的中间模型,在该模型中,我们将感兴趣的图像作为输入,并提取第一层的输出作为模型的输出。
一旦我们定义了模型,我们将通过将输入图像传递给模型来提取第一层的激活。请注意,我们必须调整输入图像的形状,以便它符合模型的要求。
- 让我们按如下方式可视化输出中的前 36 个滤波器:
fig, axs = plt.subplots(6, 6, figsize=(10, 10))
fig.subplots_adjust(hspace = .5, wspace=.5)
first_layer_activation = activations[0]
for i in range(6):
  for j in range(6):
    try:
      axs[i,j].set_ylim((224, 0))
      axs[i,j].contourf(first_layer_activation[:,:,((6*i)+j)],6,cmap='viridis')
      axs[i,j].set_title('filter: '+str((6*i)+j))
      axs[i,j].axis('off')
    except:
      continue
- 在上面的代码中,我们创建了一个 6x6 的框架,用于绘制 36 张图像。此外,我们正在循环遍历first_layer_activation中的所有通道,并绘制第一层的输出,具体如下:

在这里,我们可以看到某些滤波器提取了原始图像的轮廓(例如滤波器 0、4、7、10)。此外,某些滤波器已经学会了仅识别几个特征,例如耳朵、眼睛和鼻子(例如滤波器 30)。
- 让我们通过检查 36 张图像中滤波器 7 的输出来验证我们对某些滤波器能够提取原始图像轮廓的理解,具体如下:
activation_model = models.Model(inputs=vgg16_model.input,outputs=vgg16_model.layers[1].output)
activations = activation_model.predict(preprocess_input(np.array(x[:36]).reshape(36,300,300,3)))
fig, axs = plt.subplots(6, 6, figsize=(10, 10))
fig.subplots_adjust(hspace = .5, wspace=.5)
first_layer_activation = activations
for i in range(6):
  for j in range(6):
    try:
      axs[i,j].set_ylim((224, 0))
      axs[i,j].contourf(first_layer_activation[((6*i)+j),:,:,7],6,cmap='viridis')
      axs[i,j].set_title('filter: '+str((6*i)+j))
      axs[i,j].axis('off')
    except:
      continue
在上面的代码中,我们正在循环遍历前 36 张图像,并绘制所有 36 张图像的第一卷积层输出:

请注意,在所有图像中,第七个滤波器正在学习图像中的轮廓。
- 让我们尝试理解最后一个卷积层中的滤波器学到了什么。为了理解我们模型中最后一个卷积层的位置,我们将提取模型中的各种层:
for i, layer in enumerate(model.layers):
     print(i, layer.name)
执行上述代码后将显示以下层名称:

- 请注意,最后一个卷积层是我们模型中的第九个输出,可以通过以下方式提取:
activation_model = models.Model(inputs=vgg16_model.input,outputs=vgg16_model.layers[-1].output)
activations = activation_model.predict(preprocess_input(x[3].reshape(1,300,300,3)))
由于在图像上进行了多次池化操作,图像的大小现在已经大幅缩小(到 1, 9,9,512)。以下是最后一个卷积层中各种滤波器学习的可视化效果:

请注意,在此迭代中,不太容易理解最后一个卷积层的滤波器学到了什么(因为轮廓不容易归属于原图的某一部分),这些比第一个卷积层学到的轮廓更为细致。
使用基于 VGG19 架构的模型对图像中的人物进行性别分类
在上一节中,我们了解了 VGG16 的工作原理。VGG19 是 VGG16 的改进版本,具有更多的卷积和池化操作。
准备就绪
VGG19 模型的架构如下:

请注意,前述架构具有更多的层和更多的参数。
请注意,VGG16 和 VGG19 架构中的 16 和 19 分别表示这些网络中的层数。我们通过 VGG19 网络传递每个图像后提取的 9 x 9 x 512 输出将作为我们的模型的输入。
此外,创建输入和输出数据集,然后构建、编译和拟合模型的过程与我们在使用基于 VGG16 模型架构进行性别分类的食谱中看到的过程相同。
如何实现...
在本节中,我们将编码 VGG19 的预训练模型,代码如下(在实现代码时,请参考 GitHub 中的Transfer_learning.ipynb文件):
- 准备输入和输出数据(我们将继续从性别分类使用 CNN食谱中的步骤 3开始):
import cv2
x2 = []
for i in range(len(x)):
    img = x[i]
    img = preprocess_input(img.reshape(1,300,300,3))
    img_new = vgg19_model.predict(img.reshape(1,300,300,3))
    x2.append(img_new)
- 将输入和输出转换为相应的数组,并创建训练集和测试集:
x2 = np.array(x2)
x2= x2.reshape(x2.shape[0],x2.shape[2],x2.shape[3],x2.shape[4])
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(x2,Y, test_size=0.1, random_state=42)
- 构建并编译模型:
model_vgg19 = Sequential()
model_vgg19.add(Conv2D(512, kernel_size=(3, 3), activation='relu',input_shape=(X_train.shape[1],X_train.shape[2],X_train.shape[3])))
model_vgg19.add(MaxPooling2D(pool_size=(2, 2)))
model_vgg19.add(Flatten())
model_vgg19.add(Dense(512, activation='relu'))
model_vgg19.add(Dropout(0.5))
model_vgg19.add(Dense(1, activation='sigmoid'))
model_vgg19.summary()
以下是模型的可视化:

model_vgg19.compile(loss='binary_crossentropy',optimizer='adam',metrics=['accuracy'])
- 在对输入数据进行缩放的同时拟合模型:
history_vgg19 = model_vgg19.fit(X_train/np.max(X_train), y_train, batch_size=16,epochs=10,verbose=1,validation_data = (X_test/np.max(X_train), y_test))
让我们绘制训练集和测试集的损失和准确率度量:

我们应该注意,当使用 VGG19 架构时,我们能够在测试数据集上实现约 89%的准确率,这与 VGG16 架构非常相似。
以下是错误分类图像的示例:

请注意,VGG19 似乎根据图像中人物所占空间来错误分类。此外,它似乎更倾向于预测长发男性为女性。
使用 Inception v3 架构的性别分类模型
在之前的食谱中,我们基于 VGG16 和 VGG19 架构实现了性别分类。在本节中,我们将使用 Inception 架构来实现分类。
inception 模型如何派上用场的直观理解如下:
会有一些图像中,物体占据了图像的大部分。同样,也会有一些图像中,物体只占据了图像的一小部分。如果在这两种情况下我们使用相同大小的卷积核,那么就会使模型的学习变得困难——一些图像可能包含较小的物体,而其他图像可能包含较大的物体。
为了解决这个问题,我们将在同一层中使用多个大小的卷积核。
在这种情况下,网络本质上会变得更宽,而不是更深,如下所示:

在上述图示中,注意我们在给定层中执行多个卷积操作。inception v1 模块有九个这样的模块线性堆叠,如下所示:

来源: http://joelouismarino.github.io/images/blog_images/blog_googlenet_keras/googlenet_diagram.png
请注意,这种架构既深又宽。可能会导致梯度消失问题(正如我们在第二章中关于批量归一化的案例中所见,构建深度前馈神经网络)。
为了避免梯度消失的问题,inception v1 在 inception 模块中添加了两个辅助分类器。inception 基础网络的总体损失函数如下所示:
total_loss = real_loss + 0.3 * aux_loss_1 + 0.3 * aux_loss_2
请注意,辅助损失仅在训练期间使用,在预测过程中会被忽略。
Inception v2 和 v3 是对 inception v1 架构的改进,在 v2 中,作者在卷积操作的基础上进行了优化,以加快图像处理速度,而在 v3 中,作者在现有卷积上添加了 7 x 7 的卷积,以便将它们连接起来。
如何实现...
我们实现 inception v3 的过程与构建基于 VGG19 模型的分类器非常相似(在实现代码时,请参考 GitHub 中的Transfer_learning.ipynb文件):
- 下载预训练的 Inception 模型:
from keras.applications import inception_v3
from keras.applications.inception_v3 import preprocess_input
from keras.utils.vis_utils import plot_model
inception_model = inception_v3.InceptionV3(include_top=False, weights='imagenet',input_shape=(300,300,3))
请注意,我们需要一个至少为 300 x 300 大小的输入图像,才能使 inception v3 预训练模型正常工作。
- 创建输入和输出数据集(我们将从性别分类使用 CNNs食谱中的场景 1 第 3 步继续):
import cv2
x2 = []
for i in range(len(x)):
    img = x[i]
    img = preprocess_input(img.reshape(1,300,300,3))
    img_new = inception_model.predict(img.reshape(1,300,300,3))
    x2.append(img_new)
- 创建输入和输出数组,以及训练和测试数据集:
x2 = np.array(x2)
x2= x2.reshape(x2.shape[0],x2.shape[2],x2.shape[3],x2.shape[4])
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(x2,Y, test_size=0.1, random_state=42)
- 构建并编译模型:
model_inception_v3 = Sequential()
model_inception_v3.add(Conv2D(512, kernel_size=(3, 3), activation='relu',input_shape=(X_train.shape[1],X_train.shape[2],X_train.shape[3])))
model_inception_v3.add(MaxPooling2D(pool_size=(2, 2)))
model_inception_v3.add(Flatten())
model_inception_v3.add(Dense(512, activation='relu'))
model_inception_v3.add(Dropout(0.5))
model_inception_v3.add(Dense(1, activation='sigmoid'))
model_inception_v3.summary()
model.compile(loss='binary_crossentropy',optimizer='adam',metrics=['accuracy'])
前面的模型可以如下可视化:

- 在缩放输入数据的同时拟合模型:
history_inception_v3 = model_inception_v3.fit(X_train/np.max(X_train), y_train, batch_size=16,epochs=10,verbose=1,validation_data = (X_test/np.max(X_train), y_test)) 
准确度和损失值的变化如下:

你应该注意到,在这种情况下,准确度也是大约 ~90%。
使用基于 ResNet 50 架构的模型对图像中的人物进行性别分类
从 VGG16 到 VGG19,我们增加了层数,通常来说,神经网络越深,准确度越高。然而,如果仅仅增加层数是技巧,那么我们可以继续增加更多层(同时注意避免过拟合)来获得更准确的结果。
不幸的是,这并不完全正确,梯度消失的问题出现了。随着层数的增加,梯度在网络中传递时变得非常小,以至于很难调整权重,网络性能会下降。
ResNet 的出现是为了应对这一特定情况。
想象一种情况,如果模型没有任何需要学习的内容,卷积层仅仅将前一层的输出传递给下一层。然而,如果模型需要学习一些其他特征,卷积层会将前一层的输出作为输入,并学习需要学习的附加特征来执行分类。
残差是模型期望从一层到下一层学习的附加特征。
一个典型的 ResNet 架构如下所示:

来源:arxiv.org/pdf/1512.03385.pdf
请注意,我们有跳跃连接,这些连接将前一层与后续层连接起来,并且网络中还有传统的卷积层。
此外,ResNet50 中的 50 表示该网络总共有 50 层。
如何执行...
ResNet50 架构的构建如下(在实现代码时,请参考 GitHub 上的Transfer_learning.ipynb文件):
- 下载预训练的 Inception 模型:
from keras.applications import resnet50
from keras.applications.resnet50 import preprocess_input
resnet50_model = resnet50.ResNet50(include_top=False, weights='imagenet',input_shape=(300,300,3))
请注意,我们需要一个至少为 224 x 224 形状的输入图像,才能使 ResNet50 预训练模型正常工作。
- 创建输入和输出数据集(我们将从性别分类使用 CNNs教程中的步骤 3继续):
import cv2
x2 = []
for i in range(len(x)):
    img = x[i]
    img = preprocess_input(img.reshape(1,300,300,3))
    img_new = resnet50_model.predict(img.reshape(1,300,300,3))
    x2.append(img_new)
- 创建输入和输出数组,并准备训练和测试数据集:
x2 = np.array(x2)
x2= x2.reshape(x2.shape[0],x2.shape[2],x2.shape[3],x2.shape[4])
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(x2,Y, test_size=0.1, random_state=42)
- 构建并编译模型:
model_resnet50 = Sequential()
model_resnet50.add(Conv2D(512, kernel_size=(3, 3), activation='relu',input_shape=(X_train.shape[1],X_train.shape[2],X_train.shape[3])))
model_resnet50.add(MaxPooling2D(pool_size=(2, 2)))
model_resnet50.add(Conv2D(512, kernel_size=(3, 3), activation='relu'))
model_resnet50.add(MaxPooling2D(pool_size=(2, 2)))
model_resnet50.add(Flatten())
model_resnet50.add(Dense(512, activation='relu'))
model_resnet50.add(Dropout(0.5))
model_resnet50.add(Dense(1, activation='sigmoid'))
model_resnet50.summary()
model_resnet50.compile(loss='binary_crossentropy',optimizer='adam',metrics=['accuracy'])
模型的总结如下:

- 在缩放输入数据的同时拟合模型:
history_resnet50 = model_resnet50.fit(X_train/np.max(X_train), y_train, batch_size=32,epochs=10,verbose=1,validation_data = (X_test/np.max(X_train), y_test))
准确度和损失值的变化如下:

请注意,前面的模型给出的准确率为 92%。
在性别分类的多个预训练模型中,准确度没有显著差异,因为它们可能训练出来的是提取一般特征的模型,而不一定是用来分类性别的特征。
检测图像中的面部关键点
在这个教程中,我们将学习如何检测人脸的关键点,这些关键点包括左右眼的边界、鼻子以及嘴巴的四个坐标。
这里有两张带有关键点的示例图片:

请注意,我们预计要检测的关键点在这张图片中作为点绘制。图像中共检测到 68 个关键点,其中包括面部的关键点——嘴巴、右眉毛、左眉毛、右眼、左眼、鼻子、下巴。
在这个案例中,我们将利用在 使用基于 VGG16 架构的模型进行图像性别分类 部分中学到的 VGG16 迁移学习技术来检测面部的关键点。
准备就绪
对于关键点检测任务,我们将使用一个数据集,在该数据集上我们标注了要检测的点。对于这个练习,输入将是我们要检测关键点的图像,输出将是关键点的 x 和 y 坐标。数据集可以从这里下载:github.com/udacity/P1_Facial_Keypoints。
我们将遵循以下步骤:
- 
下载数据集 
- 
将图像调整为标准形状 - 在调整图像大小时,确保关键点已修改,以便它们代表修改后的(调整大小的)图像
 
- 
将调整大小的图像传递给 VGG16 模型 
- 
创建输入和输出数组,其中输入数组是通过 VGG16 模型传递图像的输出,输出数组是修改后的面部关键点位置 
- 
适配一个模型,最小化预测的面部关键点与实际面部关键点之间的绝对误差值 
如何操作...
我们讨论的策略的代码如下(在实现代码时,请参考 GitHub 中的 Facial_keypoints.ipynb 文件):
- 下载并导入数据集:
$ git clone https://github.com/udacity/P1_Facial_Keypoints.git import pandas as pddata = pd.read_csv('/content/P1_Facial_Keypoints/data/training_frames_keypoints.csv')
检查这个数据集。

总共有 137 列,其中第一列是图像的名称,剩余的 136 列代表对应图像中 68 个面部关键点的 x 和 y 坐标值。
- 预处理数据集,提取图像、调整大小后的图像、VGG16 特征以及修改后的关键点位置作为输出:
初始化将被附加以创建输入和输出数组的列表:
import cv2, numpy as np
from copy import deepcopy
x=[]
x_img = []
y=[]
循环读取图像:
for i in range(data.shape[0]):
     img_path = '/content/P1_Facial_Keypoints/data/training/' + data.iloc[i,0]
     img = cv2.imread(img_path)
捕获关键点值并存储
 kp = deepcopy(data.iloc[i,1:].tolist())
 kp_x = (np.array(kp[0::2])/img.shape[1]).tolist()
 kp_y = (np.array(kp[1::2])/img.shape[0]).tolist()
 kp2 = kp_x + kp_y
调整图像大小
img = cv2.resize(img,(224,224))
预处理图像,以便可以通过 VGG16 模型传递并提取特征:
preprocess_img = preprocess_input(img.reshape(1,224,224,3))
 vgg16_img = vgg16_model.predict(preprocess_img)
将输入和输出值附加到相应的列表中:
 x_img.append(img)
 x.append(vgg16_img)
 y.append(kp2)
创建输入和输出数组:
x = np.array(x)
x = x.reshape(x.shape[0],7,7,512)
y = np.array(y)
- 构建并编译模型
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout
model_vgg16 = Sequential()
model_vgg16.add(Conv2D(512, kernel_size=(3, 3), activation='relu',input_shape=(x.shape[1],x.shape[2],x.shape[3])))
model_vgg16.add(MaxPooling2D(pool_size=(2, 2)))
model_vgg16.add(Flatten())
model_vgg16.add(Dense(512, activation='relu'))
model_vgg16.add(Dropout(0.5))
model_vgg16.add(Dense(y.shape[1], activation='sigmoid'))
model_vgg16.summary()

编译模型:
model_vgg16.compile(loss='mean_absolute_error',optimizer='adam')
- 拟合模型
history = model_vgg16.fit(x/np.max(x), y, epochs=10, batch_size=32, verbose=1, validation_split = 0.1)
请注意,我们通过输入数组的最大值来对输入数组进行除法运算,以便对输入数据集进行缩放。随着训练轮数增加,训练损失和测试损失的变化如下:

- 对测试图像进行预测。在以下代码中,我们对输入数组中的倒数第二张图像进行预测(注意,validation_split为0.1,因此倒数第二张图像在训练过程中并未提供给模型)。我们确保将图像传入preprocess_input方法,然后通过VGG16_model,最后将VGG16_model输出的缩放版本传递给我们构建的model_vgg16:
pred = model_vgg16.predict(vgg16_model.predict(preprocess_input(x_img[-2].reshape(1,224,224,3)))/np.max(x))
对测试图像的前述预测可以通过以下方式可视化:

我们可以看到,关键点在测试图像上被非常准确地检测出来。
第六章:检测和定位图像中的物体
在构建深度卷积神经网络和迁移学习的章节中,我们学习了如何使用深度 CNN 检测图像属于哪个类别,也学习了如何利用迁移学习进行检测。
虽然物体分类是有效的,但在现实世界中,我们还会遇到需要定位图像中物体的场景。
例如,在自动驾驶汽车的情况下,我们不仅需要检测到行人出现在汽车的视野中,还需要检测行人与汽车之间的距离,从而可以采取适当的行动。
本章将讨论检测图像中物体的各种技术。本章将涵盖以下案例研究:
- 
创建边界框的训练数据集 
- 
使用选择性搜索生成图像中的区域提议 
- 
计算两幅图像的交集与并集的比值 
- 
使用基于区域提议的 CNN 检测物体 
- 
执行非最大抑制 
- 
使用基于锚框的算法检测人物 
介绍
随着自动驾驶汽车、人脸检测、智能视频监控和人流计数解决方案的兴起,快速且准确的目标检测系统需求巨大。这些系统不仅包括图像中的物体识别和分类,还能通过在物体周围绘制适当的框来定位每个物体。这使得目标检测比其传统的计算机视觉前身——图像分类更具挑战。
为了理解目标检测的输出是什么样的,让我们看一下以下这张图片:

到目前为止,在前面的章节中,我们已经学习了分类。
在本章中,我们将学习如何为图像中的物体生成一个紧密的边界框,这是定位任务。
此外,我们还将学习如何检测图像中的多个物体,这就是目标检测任务。
创建边界框的数据集
我们已经学到,目标检测可以输出一个围绕图像中感兴趣物体的边界框。为了构建一个检测图像中物体边界框的算法,我们需要创建输入输出映射,其中输入是图像,输出是给定图像中围绕物体的边界框。
请注意,当我们检测边界框时,我们实际上是在检测围绕图像的边界框左上角的像素位置,以及边界框的相应宽度和高度。
为了训练一个提供边界框的模型,我们需要图像以及图像中所有物体的对应边界框坐标。
在本节中,我们将重点介绍创建训练数据集的一种方法,其中图像作为输入,相应的边界框存储在 XML 文件中。
我们将使用labelImg包来标注边界框和相应的类别。
如何操作...
可以通过以下方式准备图像中的物体的边界框:
Windows
- 
从以下链接下载 labelImg的可执行文件:github.com/tzutalin/labelImg/files/2638199/windows_v1.8.1.zip。
- 
提取并打开 labelImg.exe图形界面,如下图所示:

- 在data文件夹中的predefined_classes.txt文件中指定图像中所有可能的标签。我们需要确保每个类别都列在单独的一行中,如下所示:

- 在 GUI 中点击“打开”以打开图像,并通过点击“创建矩形框”来标注图像,这将弹出如下所示的可选类别:

- 
点击“保存”并保存 XML 文件。 
- 
检查 XML 文件。绘制矩形边界框后的 XML 文件快照如下所示: 

从前面的截图中,你应该注意到bndbox包含了与图像中感兴趣物体相对应的x和y坐标的最小值和最大值。此外,我们还应该能够提取图像中物体对应的类别。
Ubuntu
在 Ubuntu 中,可以通过输入以下命令执行与前述相同的步骤:
$sudo apt-get install pyqt5-dev-tools
$sudo pip3 install -r requirements/requirements-linux-python3.txt
$make qt5py3
$python3 labelImg.py
脚本labelImg.py可以通过以下 GitHub 链接找到:github.com/tzutalin/labelImg。
一旦我们执行了前面的代码,我们应该能够进行与Windows部分中看到的相同分析。
MacOS
在 macOS 中,可以通过输入以下命令来执行相同的前述步骤:
$brew install qt  # will install qt-5.x.x
$brew install libxml2
$make qt5py3
$python3 labelImg.py
脚本labelImg.py可以通过以下 GitHub 链接找到:github.com/tzutalin/labelImg。
一旦我们执行了前面的脚本,我们应该能够进行与Windows部分中看到的相同分析。
在图像中生成区域提议,使用选择性搜索
为了理解什么是区域提议,让我们将这个术语分解为两个组成部分——区域和提议。
区域是图像的一个部分,其中该部分的像素具有非常相似的值。
区域提议是图像的较小部分,在该部分中有更高的可能性属于某个特定物体。
区域提议是有用的,因为我们从图像中生成了候选区域,这些区域内物体出现的概率较高。在目标定位任务中非常有用,我们需要围绕物体生成一个与前一节中图像中类似的边界框。
准备工作
在本节中,我们将探讨如何在一个人的图像中生成一个边界框。
选择性搜索是一种在目标检测中使用的区域提议算法。它旨在快速运行,并具有非常高的召回率。该算法基于计算基于颜色、纹理、大小和形状兼容性的相似区域的层次分组。
可以使用名为selectivesearch的 Python 包生成区域提议,示例如下。
选择性搜索通过使用 Felzenszwalb 和 Huttenlocher 提出的基于图的分割方法,首先对图像进行过度分割(生成成千上万的区域提议),并基于像素的强度来执行。
选择性搜索算法将这些过度分割作为初始输入,并执行以下步骤:
- 
将所有与分割部分对应的边界框添加到区域提议列表中 
- 
根据相似性将相邻的区域分组 
- 
进入第一步 
在每次迭代中,较大的区域会被形成并添加到区域提议列表中。因此,我们采用自下而上的方法,从较小的区域生成较大的区域提议。
选择性搜索使用四种相似性度量方法,分别基于颜色、纹理、大小和形状兼容性来生成区域提议。
区域提议有助于识别图像中的潜在感兴趣目标。因此,我们可能会将定位的任务转化为分类任务,分类每个区域是否包含感兴趣的物体。
如何进行...
在本节中,我们将展示如何提取区域提议,示例如下(代码文件在 GitHub 上的Selective_search.ipynb中可用):
- 按如下方式安装selectivesearch:
$pip install selectivesearch
- 导入相关的包,见下面的代码:
import matplotlib.pyplot as plt
%matplotlib inline
import selectivesearch
import cv2
- 按如下方式加载图像:
img = cv2.imread('/content/Hemanvi.jpeg')
- 提取区域提议:
img_lbl, regions = selectivesearch.selective_search(img, scale=100, min_size=2000)
参数min_size提供了一个约束,要求区域提议的大小至少为 2,000 个像素,而参数 scale 有效地设置了观察的尺度,较大的尺度倾向于偏好较大的组件。
- 检查结果中区域的数量,并将其存储在列表中:
print(len(regions))
candidates = set()
for r in regions:
     if r['rect'] in candidates:
         continue
 # excluding regions smaller than 2000 pixels
     if r['size'] < 2000:
         continue
     x, y, w, h = r['rect']
 candidates.add(r['rect'])
在前一步中,我们将所有大于 2,000 像素(面积)的区域存储到候选区域集合中。
- 绘制包含候选区域的结果图像:
import matplotlib.patches as mpatches
fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6))
ax.imshow(img)
for x, y, w, h in candidates:
    rect = mpatches.Rectangle(
        (x, y), w, h, fill=False, edgecolor='red', linewidth=1)
    ax.add_patch(rect)
plt.axis('off')
plt.show()

从前面的截图中可以看到,图像中提取了多个区域。
计算两个图像之间的交集与并集
为了理解提议区域的准确性,我们使用一个名为交并比(IoU)的度量。IoU 可以如下可视化:

请注意,在上面的图片中,蓝色框(下方)是实际位置,红色框(上方的矩形)是区域提议。
区域提议的交集与并集的计算方法是将提议区域与真实位置的交集面积除以提议区域与真实位置的并集面积。
如何执行这个操作...
IoU 的计算方式如下(代码文件可以在 GitHub 中的Selective_search.ipynb找到):
- 定义 IoU 提取函数,以下代码演示了这个过程:
from copy import deepcopy
import numpy as np
def extract_iou(candidate, current_y,img_shape):
     boxA = deepcopy(candidate)
     boxB = deepcopy(current_y)
     img1 = np.zeros(img_shape)
     img1[boxA[1]:boxA[3],boxA[0]:boxA[2]]=1
     img2 = np.zeros(img_shape)
     img2[int(boxB[1]):int(boxB[3]),int(boxB[0]):int(boxB[2])]=1
     iou = np.sum(img1*img2)/(np.sum(img1)+np.sum(img2)- np.sum(img1*img2))
     return iou
在上述函数中,我们将候选区域、实际物体区域和图像形状作为输入。
此外,我们为候选图像和实际物体位置图像初始化了两个相同形状且值为零的数组。
我们已经覆盖了候选图像和实际物体位置图像,在它们各自的位置上显示图像和物体。
最后,我们计算了候选图像与实际物体位置图像的交集与并集的比值。
- 导入感兴趣的图像:
img = cv2.imread('/content/Hemanvi.jpeg')
- 绘制图像并验证物体的实际位置:
plt.imshow(img)
plt.grid('off')

请注意,感兴趣的区域大约从左下角的 50 个像素开始,延伸到图像的第 290 个像素。此外,在y轴上,它也从大约第 50 个像素开始,直到图像的末端。
所以,物体的实际位置是(50,50,290,500),这是(xmin,ymin,xmax,ymax)格式。
- 提取区域提议:
img_lbl, regions = selectivesearch.selective_search(img, scale=100, min_size=2000)
从selectivesearch方法提取的区域格式为(xmin,ymin,width,height)。因此,在提取区域的 IoU 之前,我们需要确保候选区域和实际位置图像的格式一致,即(xmin,ymin,xmax,ymax)。
- 将 IoU 提取函数应用于感兴趣的图像。请注意,函数的输入是实际物体的位置和候选图像的形状:
regions =list(candidates)
actual_bb = [50,50,290,500]
iou = []
for i in range(len(regions)):
     candidate = list(regions[i])
     candidate[2] += candidate[0]
     iou.append(extract_iou(candidate, actual_bb, img.shape))
- 确定与实际物体(真实边界框)具有最高重叠的区域:
np.argmax(iou)
对于这个特定图像,前述输出是第十个候选区域,其坐标为 0,0,299,515。
- 让我们打印实际的边界框和候选边界框。为此,我们需要将输出的(xmin,ymin,xmax,ymax)格式转换为(xmin,ymin,width,height):
max_region = list(regions[np.argmax(iou)])
max_region[2] -= max_region[0]
max_region[3] -= max_region[1]
actual_bb[2] -= actual_bb[0]
actual_bb[3] -= actual_bb[1]
让我们附加实际边界框和具有最高 IoU 的候选边界框:
maxcandidate_actual = [max_region,actual_bb]
现在,我们将循环遍历前述列表,并为图像中物体的实际位置分配更大的线宽,以便区分候选区域与实际物体的位置:
import matplotlib.patches as mpatches
fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6))
ax.imshow(img)
for i,(x, y, w, h) in enumerate(maxcandidate_actual):
 if(i==0):
 rect = mpatches.Rectangle(
 (x, y), w, h, fill=False, edgecolor='blue', linewidth=2)
 ax.add_patch(rect)
 else:
 rect = mpatches.Rectangle(
 (x, y), w, h, fill=False, edgecolor='red', linewidth=5)
 ax.add_patch(rect)
plt.axis('off')
plt.show()

通过这种方式,我们可以确定每个候选区域与图像中物体实际位置的 IoU。此外,我们还可以确定与图像中物体实际位置 IoU 最高的候选区域。
使用基于区域建议的 CNN 进行物体检测
在前一节中,我们已经学习了如何从图像中生成区域建议。 本节中,我们将利用这些区域建议来进行物体检测和定位。
准备工作
我们将采用的基于区域建议的物体检测和定位策略如下:
- 
对于当前的练习,我们将基于仅包含一个物体的图像来构建模型 
- 
我们将从图像中提取不同的区域建议(候选框) 
- 
我们将计算候选框与实际物体位置的接近程度: - 本质上,我们计算候选框与物体实际位置的交并比
 
- 
如果交并比大于某个阈值——则该候选框被认为包含目标物体,否则不包含: - 这将为每个候选框创建标签,其中候选框的图像作为输入,交并比阈值提供输出
 
- 
我们将调整每个候选框的图像大小,并通过 VGG16 模型(我们在前一章节中学习过)提取候选框的特征 
- 
此外,我们将通过比较候选框的位置与物体实际位置来创建边界框修正的训练数据 
- 
构建一个分类模型,将候选框的特征映射到区域是否包含物体的输出 
- 
对于包含图像的区域(根据模型),构建一个回归模型,将候选框的输入特征映射到提取物体准确边界框所需的修正 
- 
对结果边界框执行非极大值抑制: - 非极大值抑制确保了重叠度较高的候选框被压缩为 1,最终只保留具有最高概率包含物体的候选框
 
- 
通过执行非极大值抑制,我们将能够复现为包含多个物体的图像构建的模型 
以下是前述内容的示意图:

如何做到……
在本节中,我们将编写前一节讨论过的算法(代码文件和相应推荐数据集的链接可在 GitHub 上找到,文件名为Region_proposal_based_object_detection.ipynb,并附有推荐的数据集):
- 下载包含一组图像、其中包含的物体及图像中物体对应的边界框的数据集。数据集和相应的代码文件可以在 GitHub 上找到,供您使用。
以下是一个示例图像及其对应的边界框坐标和图像中的物体类别:

物体的类别和边界框坐标将保存在 XML 文件中(如何获取 XML 文件的详细信息可参考 GitHub 上的代码文件),可以通过以下方式从 XML 文件中提取:
如果xml["annotation"]["object"]是一个列表,说明图像中存在多个物体。
xml["annotation"]["object"]["bndbox"]提取图像中物体的边界框,其中边界框的坐标包括“xmin”、“ymin”、“xmax”和“ymax”。
xml["annotation"]["object"]["name"]提取图像中物体的类别。
- 按如下方式导入相关包:
import matplotlib.pyplot as plt
%matplotlib inline
import tensorflow as tf, selectivesearch
import json, scipy, os, numpy as np,argparse,time, sys, gc, cv2, xmltodict
from copy import deepcopy
- 定义 IoU 提取函数,如下代码所示:
def extract_iou2(candidate, current_y,img_shape):
     boxA = deepcopy(candidate)
     boxB = deepcopy(current_y)
     boxA[2] += boxA[0]
     boxA[3] += boxA[1]
     iou_img1 = np.zeros(img_shape)
     iou_img1[boxA[1]:boxA[3],boxA[0]:boxA[2]]=1
     iou_img2 = np.zeros(img_shape)
     iou_img2[int(boxB[1]):int(boxB[3]),int(boxB[0]):int(boxB[2])]=1
     iou = np.sum(iou_img1*iou_img2)/(np.sum(iou_img1)+np.sum(iou_img2)-np.sum(iou_img1*iou_img2))
     return iou
- 定义候选框提取函数,如下所示:
def extract_candidates(img):
     img_lbl, regions = selectivesearch.selective_search(img, scale=100, min_size=100)
     img_area = img.shape[0]*img.shape[1]
     candidates = []
     for r in regions:
         if r['rect'] in candidates:
             continue
         if r['size'] < (0.05*img_area):
             continue
         x, y, w, h = r['rect']
         candidates.append(list(r['rect']))
     return candidates
注意,在上述函数中,我们排除了占据图像面积小于 5%的所有候选框。
- 按如下方式导入预训练的 VGG16 模型:
from keras.applications import vgg16
from keras.utils.vis_utils import plot_model
vgg16_model = vgg16.VGG16(include_top=False, weights='imagenet')
- 为包含单一物体的图像创建输入和输出映射。初始化多个列表,随着图像的遍历,它们将被填充:
training_data_size = N = 1000
final_cls = []
final_delta = []
iou_list = []
imgs = []
我们将遍历图像,只处理那些包含单一物体的图像:
for ix, xml in enumerate(XMLs[:N]):
    print('Extracted data from {} xmls...'.format(ix), end='\r')
    xml_file = annotations + xml
    fname = xml.split('.')[0]
    with open(xml_file, "rb") as f: # notice the "rb" mode
        xml = xmltodict.parse(f, xml_attribs=True)
        l = []        
        if isinstance(xml["annotation"]["object"], list):
            #'let us ignore cases with multiple objects...'
            continue
在上述代码中,我们提取了图像的xml属性,并检查图像是否包含多个对象(如果xml["annotation"]["object"]的输出是一个列表,则图像包含多个对象)。
归一化物体位置坐标,以便我们可以处理归一化后的边界框。这样即使图像形状在进一步处理时发生变化,归一化的边界框也不会变化。例如,如果物体的xmin在x轴的 20%和y轴的 50%处,即使图像被重新调整大小,其位置也不会变化(但如果处理的是像素值,xmin的值会发生变化):
        bndbox = xml['annotation']['object']['bndbox']
        for key in bndbox:
              bndbox[key] = float(bndbox[key])
        x1, x2, y1, y2 = [bndbox[key] for key in ['xmin', 'xmax', 'ymin', 'ymax']]
        img_size = xml['annotation']['size']
        for key in img_size:
              img_size[key] = float(img_size[key])
        w, h = img_size['width'], img_size['height']
        #'converting pixel values from bndbox to fractions...'
        x1 /= w; x2 /= w; y1 /= h; y2 /= h
        label = xml['annotation']['object']['name']
        y = [x1, y1, x2-x1, y2-y1, label]  # top-left x & y, width and height
在上述代码中,我们已经对边界框坐标进行了归一化处理。
提取图像中的候选框:
         filename = jpegs+fname+'.jpg' # Path to jpg files here
         img = cv2.resize(cv2.imread(filename), (224,224)) # since VGG's input shape is 224x224
         candidates = extract_candidates(img)
在上述代码中,我们使用了extract_candidates函数来提取调整大小后的图像的区域建议。
遍历候选框,计算每个候选框与图像中实际边界框的交并比(IoU),并计算实际边界框与候选框之间的对应偏差:
         for jx, candidate in enumerate(candidates):
                current_y2 = [int(i*224) for i in [x1,y1,x2,y2]] # [int(x1*224), int(y1*224), int(x2*224), int(y2*224)]
                iou = extract_iou2(candidate, current_y2, (224, 224))
                candidate_region_coordinates = c_x1, c_y1, c_w, c_h = np.array(candidate)/224
                dx = c_x1 - x1 
                dy = c_y1 - y1 
                dw = c_w - (x2-x1)
                dh = c_h - (y2-y1)
                final_delta.append([dx,dy,dw,dh]) 
计算每个区域建议的 VGG16 特征,并根据区域建议与实际边界框的 IoU 来分配目标:
               if(iou>0.3): 
                    final_cls.append(label)
               else:
                    final_cls.append('background')
            #"We'll predict our candidate crop using VGG"
               l = int(c_x1 * 224)
               r = int((c_x1 + c_w) * 224)
               t = int(c_y1 * 224)
               b = int((c_y1 + c_h) * 224)
               img2 = img[t:b,l:r,:3]
               img3 = cv2.resize(img2,(224,224))/255
               img4 = vgg16_model.predict(img3.reshape(1,224,224,3))
               imgs.append(img4)
- 创建输入和输出数组:
targets = pd.DataFrame(final_cls, columns=['label'])
labels = pd.get_dummies(targets['label']).columns
y_train = pd.get_dummies(targets['label']).values.astype(float)
我们使用get_dummies方法,因为类别是分类文本值:
x_train = np.array(imgs)
x_train = x_train.reshape(x_train.shape[0],x_train.shape[2],x_train.shape[3],x_train.shape[4])
- 构建并编译模型:
model = Sequential()
model.add(Flatten(input_shape=((7,7,512))))
model.add(Dense(512, activation='relu'))
model.add(Dense(all_classes.shape[1],activation='softmax'))
model.compile(loss='binary_crossentropy',optimizer='adam', metrics=['accuracy'])
- 训练模型,如下所示:
model.fit(xtrain3/x_train.max(),y_train,validation_split = 0.1, epochs=5, batch_size=32, verbose=1)
上述结果在测试数据集上实现了 97%的分类准确率。
我们通过 x_train.max() 来对输入数组进行除法操作,因为输入数组中的最大值大约是 11。通常来说,将输入值缩放至 0 和 1 之间是一个好主意,并且考虑到 VGG16 在缩放输入上的预测最大值大约是 11,所以我们将输入数组除以 x_train.max() —— 其值大约为 11。
- 
对数据集中的类别进行预测(确保我们不考虑用于训练的图像)。 
- 
选择一个未用于测试的图像: 
import matplotlib.patches as mpatches
ix = np.random.randint(N, len(XMLs))
filename = jpegs + XMLs[ix].replace('xml', 'jpg')
- 构建一个函数,执行图像预处理以提取候选区域,对调整大小后的候选区域进行模型预测,过滤掉预测为背景类别的区域,最后绘制出具有最高概率包含非背景类别的区域(候选区域):
def test_predictions(filename):
     img = cv2.resize(cv2.imread(filename), (224,224))
     candidates = extract_candidates(img)
在前面的代码中,我们正在调整输入图像的大小,并从中提取候选区域:
    _, ax = plt.subplots(1, 2)
    ax[0].imshow(img)
    ax[0].grid('off')
    ax[0].set_title(filename.split('/')[-1])
    pred = []
    pred_class = []
在前面的代码中,我们正在绘制一张图像,并初始化预测的概率和预测类别列表,这些列表将在后续步骤中被填充:
    for ix, candidate in enumerate(candidates):
        l, t, w, h = np.array(candidate).astype(int)
        img2 = img[t:t+h,l:l+w,:3]
        img3 = cv2.resize(img2,(224,224))/255
        img4 = vgg16_model.predict(img3.reshape(1,224,224,3))
        final_pred = model.predict(img4/x_train.max())
        pred.append(np.max(final_pred))
        pred_class.append(np.argmax(final_pred))
在前面的代码中,我们正在遍历候选区域,调整其大小,并将其通过 VGG16 模型进行处理。此外,我们还将 VGG16 的输出传入我们的模型,后者提供了图像属于各种类别的概率:
    pred = np.array(pred)
    pred_class = np.array(pred_class)
    pred2 = pred[pred_class!=1]
    pred_class2 = pred_class[pred_class!=1]
    candidates2 = np.array(candidates)[pred_class!=1]
    x, y, w, h = candidates2[np.argmax(pred2)]
在前面的代码中,我们正在提取具有最高概率包含非背景物体的候选区域(其中一个预测类别对应于背景):
   ax[1].set_title(labels[pred_class2[np.argmax(pred2)]])
   ax[1].imshow(img)
   ax[1].grid('off')
   rect = mpatches.Rectangle((x, y), w, h, fill=False, edgecolor='red', linewidth=1)
   ax[1].add_patch(rect)
在前面的代码中,我们正在绘制图像并显示边界框的矩形区域。
- 调用定义的函数并使用一张新图像:
filename = '...' #Path to new image
test_predictions(filename)

请注意,模型准确地预测了图像中物体的类别。此外,具有最高概率包含人的边界框(候选区域)需要稍作修正。
在下一步中,我们将进一步修正边界框。
- 构建并编译一个模型,该模型以图像的 VGG16 特征作为输入,并预测边界框的修正值:
model2 = Sequential()
model2.add(Flatten(input_shape=((7,7,512))))
model2.add(Dense(512, activation='relu'))
model2.add(Dense(4,activation='linear'))
model2.compile(loss='mean_absolute_error',optimizer='adam')
- 构建模型以预测边界框的修正值。然而,我们需要确保只对那些可能包含图像的区域进行边界框修正预测:
for i in range(1000):
     samp=random.sample(range(len(x_train)),500)
     x_train2=[x_train[i] for i in samp if pred_class[i]!=1]
     x_train2 = np.array(x_train2)
     final_delta2 = [final_delta[i] for i in samp if pred_class[i]!=1]
     model2.fit(x_train2/x_train.max(), np.array(final_delta2), validation_split = 0.1, epochs=1, batch_size=32, verbose=0)
在前面的代码中,我们正在遍历输入数组数据集,并创建一个新的数据集,该数据集仅包含那些可能包含非背景区域的部分。
此外,我们正在重复执行前面的步骤 1,000 次,以微调模型。
- 构建一个函数,输入图像路径并预测图像的类别,同时修正边界框:
'TESTING'
import matplotlib.patches as mpatches
def test_predictions2(filename):
     img = cv2.resize(cv2.imread(filename), (224,224))
     candidates = extract_candidates(img)
     _, ax = plt.subplots(1, 2)
     ax[0].imshow(img)
     ax[0].grid('off')
     ax[0].set_title(filename.split('/')[-1])
     pred = []
     pred_class = []
     del_new = []
     for ix, candidate in enumerate(candidates):
        l, t, w, h = np.array(candidate).astype(int)
        img2 = img[t:t+h,l:l+w,:3]
        img3 = cv2.resize(img2,(224,224))/255
        img4 = vgg16_model.predict(img3.reshape(1,224,224,3)) 
        final_pred = model.predict(img4/x_train.max())
        delta_new = model2.predict(img4/x_train.max())[0] 
        pred.append(np.max(final_pred))
        pred_class.append(np.argmax(final_pred))
        del_new.append(delta_new) 
     pred = np.array(pred)
     pred_class = np.array(pred_class)
     non_bgs = (pred_class!=1)
     pred = pred[non_bgs]
     pred_class = pred_class[non_bgs] 
     del_new = np.array(del_new)
     del_new = del_new[non_bgs]
     del_pred = del_new*224
     candidates = C = np.array(candidates)[non_bgs]
     C = np.clip(C, 0, 224)
     C[:,2] += C[:,0]
     C[:,3] += C[:,1]
     bbs_pred = candidates - del_pred
     bbs_pred = np.clip(bbs_pred, 0, 224) 
     bbs_pred[:,2] -= bbs_pred[:,0]
     bbs_pred[:,3] -= bbs_pred[:,1]
     final_bbs_pred = bbs_pred[np.argmax(pred)]
     x, y, w, h = final_bbs_pred
     ax[1].imshow(img)
     ax[1].grid('off')
     rect = mpatches.Rectangle((x, y), w, h, fill=False, edgecolor='red', linewidth=1)
     ax[1].add_patch(rect)
     ax[1].set_title(labels[pred_class[np.argmax(pred)]])
- 提取仅包含一个物体的测试图像(因为我们已经构建了仅包含单一物体的图像模型):
single_object_images = []
for ix, xml in enumerate(XMLs[N:]):
     xml_file = annotations + xml
     fname = xml.split('.')[0]
     with open(xml_file, "rb") as f: # notice the "rb" mode
         xml = xmltodict.parse(f, xml_attribs=True)
         l = []
         if isinstance(xml["annotation"]["object"], list):
             continue
         single_object_images.append(xml["annotation"]['filename'])
         if(ix>100):
             break
在前面的代码中,我们正在遍历图像注释并识别包含单一物体的图像。
- 对单一物体图像进行预测:
test_predictions2(filename)

请注意,第二个模型能够修正边界框以适应人物;然而,边界框仍然需要稍微进一步修正。这可以通过在更多数据点上进行训练来实现。
执行非极大值抑制(NMS)
到目前为止,在前一节中,我们只考虑了没有背景的候选区域,并进一步考虑了具有最高物体兴趣概率的候选。然而,这在图像中存在多个物体的情况下无法正常工作。
在本节中,我们将讨论筛选候选区域提议的方法,以便我们能够提取图像中尽可能多的物体。
准备工作
我们采用的 NMS 策略如下:
- 
从图像中提取区域提议。 
- 
重新调整区域提议的形状,并预测图像中包含的物体。 
- 
如果物体不是背景类,我们将保留该候选区域。 
- 
对于所有非背景类的候选区域,我们将按其包含物体的概率排序。 
- 
第一个候选(按照各类别概率降序排序后的第一个候选)将与其余所有候选区域进行 IoU 比较。 
- 
如果其他任何区域与第一个候选区域有较大重叠,它们将被丢弃。 
- 
在剩下的候选中,我们将再次考虑具有最高包含物体概率的候选区域。 
- 
我们将重复比较第一个候选(在前一步中已过滤且与第一个候选重叠较小的候选列表中的候选)与其余候选区域。 
- 
该过程将持续进行,直到没有剩余候选区域可以比较为止。 
- 
我们将绘制前述步骤后剩余候选区域的最终边界框。 
如何执行……
非极大值抑制的 Python 代码如下。我们将从前面食谱中的第 14 步继续(代码文件及对应推荐数据集链接可在 GitHub 中的Region_proposal_based_object_detectionn.ipynb找到)。
- 从图像中提取所有有较高信心包含非背景类物体的区域:
filename = jpegs + single_object_images[ix]
img = cv2.imread(filename)
img = cv2.resize(img,(224,224))
img_area = img.shape[0]*img.shape[1]
candidates = extract_candidates(img)
plt.imshow(img)
plt.grid('off')
我们正在考虑的图像如下:

- 对候选区域进行预处理——将它们通过 VGG16 模型,然后预测每个区域提议的类别以及区域的边界框:
pred = []
pred_class = []
del_new = []
for ix, candidate in enumerate(candidates):
    l, t, w, h = np.array(candidate).astype(int)
    img2 = img[t:t+h,l:l+w,:3]
    img3 = cv2.resize(img2,(224,224))/255
    img4 = vgg16_model.predict(img3.reshape(1,224,224,3)) 
    final_pred = model.predict(img4/x_train.max())
    delta_new = model2.predict(img4/x_train.max())[0]
    pred.append(np.max(final_pred))
    pred_class.append(np.argmax(final_pred))
    del_new.append(delta_new)
pred = np.array(pred)
pred_class = np.array(pred_class)
- 提取非背景类预测结果及其对应的边界框修正值:
non_bgs = ((pred_class!=1))
pred = pred[non_bgs]
pred_class = pred_class[non_bgs]
del_new = np.array(del_new)
del_new = del_new[non_bgs]
del_pred = del_new*224
在前一步中,我们已过滤掉所有概率、类别和边界框修正值,针对非背景区域(预测类别1在我们的数据准备过程中属于背景类别)。
- 使用边界框修正值校正候选区域:
candidates = C = np.array(candidates)[non_bgs]
C = np.clip(C, 0, 224)
C[:,2] += C[:,0]
C[:,3] += C[:,1]
bbs_pred = candidates - del_pred
bbs_pred = np.clip(bbs_pred, 0, 224)
此外,我们还确保了xmax和ymax坐标不能大于224。
此外,我们需要确保边界框的宽度和高度不能为负值:
bbs_pred[:,2] -= bbs_pred[:,0]
bbs_pred[:,3] -= bbs_pred[:,1]
bbs_pred = np.clip(bbs_pred, 0, 224)
bbs_pred2 = bbs_pred[(bbs_pred[:,2]>0) & (bbs_pred[:,3]>0)]
pred = pred[(bbs_pred[:,2]>0) & (bbs_pred[:,3]>0)]
pred_class = pred_class[(bbs_pred[:,2]>0) & (bbs_pred[:,3]>0)]
- 绘制包含边界框的图像:
import matplotlib.patches as mpatches
fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6))
ax.imshow(img)
for ix, (x, y, w, h) in enumerate(bbs_pred2):
    rect = mpatches.Rectangle(
        (x, y), w, h, fill=False, edgecolor='red', linewidth=1)
    ax.add_patch(rect)
plt.axis('off')
plt.show()

- 
对边界框执行非最大值抑制(NMS)。为此,我们将定义一个函数,该函数通过考虑两个边界框可能具有的最小交集(阈值)、边界框坐标以及与每个边界框相关的概率得分来执行 NMS,详细步骤如下: - 计算每个边界框的x、y、w和h值,它们的对应面积,以及它们的概率顺序:
 
- 计算每个边界框的
def nms_boxes(threshold, boxes, scores):
     x = boxes[:, 0]
     y = boxes[:, 1]
     w = boxes[:, 2]
     h = boxes[:, 3]
     areas = w * h
     order = scores.argsort()[::-1]
- 
- 计算概率最高的候选框与其他候选框之间的交并比(IoU):
 
     keep = []
     while order.size > 0:
         i = order[0]
         keep.append(i)
         xx1 = np.maximum(x[i], x[order[1:]])
         yy1 = np.maximum(y[i], y[order[1:]])
         xx2 = np.minimum(x[i] + w[i], x[order[1:]] + w[order[1:]])
         yy2 = np.minimum(y[i] + h[i], y[order[1:]] + h[order[1:]])
         w1 = np.maximum(0.0, xx2 - xx1 + 1)
         h1 = np.maximum(0.0, yy2 - yy1 + 1)
         inter = w1 * h1
         iou = inter / (areas[i] + areas[order[1:]] - inter)
- 
- 确定那些 IoU 小于阈值的候选框:
 
        inds = np.where(ovr <= threshold)[0]
        order = order[inds + 1]
在前面的步骤中,我们确保了接下来的一组候选框(除了第一个候选框)将循环执行相同的步骤(注意函数开始处的while循环)。
- 
- 返回需要保留的候选框的索引:
 
    keep = np.array(keep)
    return keep
- 执行前面的函数:
keep_box_ixs = nms_boxes(0.3, bbs_pred2, pred)
- 绘制前一步留下的边界框:
import matplotlib.patches as mpatches
fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6))
ax.imshow(img)
for ix, (x, y, w, h) in enumerate(bbs_pred2):
     if ix not in keep_box_ixs:
         continue
     rect = mpatches.Rectangle((x, y), w, h, fill=False, edgecolor='red', linewidth=1)
     ax.add_patch(rect)
     centerx = x + w/2
     centery = y + h - 10
     plt.text(centerx, centery,labels[pred_class[ix]]+" "+str(round(pred[ix],2)),fontsize = 20,color='red')
plt.axis('off')
plt.show()

从前面的截图中,我们看到我们移除了所有其他作为区域提案生成的边界框。
使用基于锚框的算法检测人物
基于区域提案的 CNN 的一个缺点是它无法实现实时物体识别,因为选择性搜索需要相当长的时间来提出区域。这使得基于区域提案的物体检测算法在像自动驾驶汽车这样的实时检测场景中不具备实用性。
为了实现实时检测,我们将从零开始构建一个受You Only Look Once(YOLO)算法启发的模型,该模型可以查看包含人物的图像,并在图像中的人物周围绘制边界框。
准备就绪
为了理解 YOLO 如何克服生成区域提案时所消耗的时间这一缺点,让我们将“YOLO”一词拆解成其组成部分——我们将在神经网络的单次前向传播中完成所有预测(图像类别以及边界框)。与基于区域提案的 CNN 方法相比,我们首先使用选择性搜索算法得到区域提案,然后在其基础上构建分类算法。
为了弄清楚 YOLO 的工作细节,让我们通过一个简单的示例。假设输入图像如下所示——该图像被划分为一个 3 x 3 的网格:

我们的神经网络模型的输出将是 3 x 3 x 5 的大小,其中前 3 x 3 表示图像中的网格数,第一个输出通道对应网格包含物体的概率,另外四个通道是网格对应的* x , y , w , h *坐标的增量。
另一个我们使用的工具是锚框。实质上,我们已经知道,在我们拥有的图像集合中,有些形状是已知的。例如,汽车的形状通常宽度大于高度,而站立的人物通常高度大于宽度。
因此,我们将把图像集中所有的高度和宽度值聚类成五个簇,这样就会得到五个锚框的高度和宽度,用于识别图像中的物体边界框。
如果图像中有五个锚框工作,则输出将是 3 x 3 x 5 x 5,其中 5 x 5 对应每个锚框的五个组成部分(一个物体的概率和四个* x , y , w , h *的增量)。
从前面的内容可以看出,3 x 3 x 5 x 5 的输出可以通过神经网络模型的单次前向传播生成。
在接下来的部分,我们将看到如何生成锚框大小的伪代码:
- 
提取数据集中所有图像的宽度和高度。 
- 
运行一个具有五个簇的 k-means 聚类,来识别图像中宽度和高度的簇。 
- 
五个簇中心对应五个锚框的宽度和高度,用于构建模型。 
此外,在接下来的部分,我们将了解 YOLO 算法是如何工作的:
- 
将图像分割成固定数量的网格单元。 
- 
与图像的真实边界框中心相对应的网格将负责预测边界框。 
- 
锚框的中心将与网格的中心相同。 
- 
创建训练数据集: - 
对于包含物体中心的网格,因变量为 1,且需要为每个锚框计算* x , y , w , h *的增量。 
- 
对于不包含物体中心的网格,因变量为零,且* x , y , w , h *的增量不重要。 
 
- 
- 
在第一个模型中,我们将预测包含图像中心的锚框和网格单元组合。 
- 
在第二个模型中,我们预测锚框的边界框修正。 
如何实现...
我们将构建用于执行人脸检测的代码(代码文件和相应推荐数据集链接可以在 GitHub 中作为Anchor_box_based_person_detection.ipynb找到,连同推荐的数据集):
- 下载包含一组图像、图像中物体及其对应边界框的数据集。可以在 GitHub 上找到推荐的数据集及其相关代码文件供你使用。
一个示例图像及其相应的边界框位置输出看起来类似于我们在"基于区域提议的 CNN 物体检测"食谱的第 1 步中看到的内容。
- 导入相关的包,如下所示:
import matplotlib.pyplot as plt
%matplotlib inline
import tensorflow as tf, selectivesearch
import json, scipy, os, numpy as np,argparse,time, sys, gc, cv2, xmltodict
from copy import deepcopy
- 定义 IoU 提取函数,如下所示:
def extract_iou2(candidate, current_y,img_shape):
     boxA = deepcopy(candidate)
     boxB = deepcopy(current_y)
     boxA[2] += boxA[0]
     boxA[3] += boxA[1]
     iou_img1 = np.zeros(img_shape)
     iou_img1[boxA[1]:boxA[3],boxA[0]:boxA[2]]=1
     iou_img2 = np.zeros(img_shape)
     iou_img2[int(boxB[1]):int(boxB[3]),int(boxB[0]):int(boxB[2])]=1
     iou = np.sum(iou_img1*iou_img2)/(np.sum(iou_img1)+np.sum(iou_img2)-np.sum(iou_img1*iou_img2))
     return iou
- 
将锚框的宽度和高度定义为图像总宽度和高度的百分比: 
- 
- 确定边界框中人物的所有可能宽度和高度:
 
y_train = []
for i in mylist[:10000]:
     xml_file = xml_filepath +i
     arg1=i.split('.')[0]
     with open(xml_file, "rb") as f: # notice the "rb" mode
         d = xmltodict.parse(f, xml_attribs=True)
         l=[]
         if type(d["annotation"]["object"]) == type(l):
             discard=1
         else:
             x1=((float(d['annotation']['object']
             ['bndbox']['xmin'])))/(float(d['annotation']['size']['width']))
             x2=((float(d['annotation']['object']
             ['bndbox']['xmax'])))/(float(d['annotation']['size']['width']))
             y1=((float(d['annotation']['object']
             ['bndbox']['ymin'])))/(float(d['annotation']['size']['height']))
             y2=((float(d['annotation']['object']
             ['bndbox']['ymax'])))/(float(d['annotation']['size']['height']))
             cls=d['annotation']['object']['name']
             if(cls == 'person'):
                 y_train.append([x2-x1, y2-y1])
在前面的代码中,我们遍历所有仅包含一个物体的图像,然后计算该图像中的边界框的宽度和高度,前提是图像中包含一个人。
- 
- 使用五个中心进行 k-means 聚类:
 
y_train = np.array(y_train)
from sklearn.cluster import KMeans
km = KMeans(n_clusters=5)
km.fit(y_train)
km.cluster_centers_
前面的步骤产生了如下所示的聚类中心:
anchors = [[[0.84638352, 0.90412013],        
            [0.28036872, 0.58073186],        
            [0.45700897, 0.87035502],        
            [0.15685545, 0.29256264],        
            [0.59814951, 0.64789503]]]
- 
创建训练数据集: - 初始化空列表,以便在进一步处理时向其中添加数据:
 
k=-1
pre_xtrain = []
y_train = []
cls = []
xtrain=[]
final_cls = []
dx = []
dy = []
dw= []
dh = []
final_delta = []
av = 0
x_train = []
img_paths = []
label_coords = []
y_delta = []
anc = []
- 
- 遍历数据集,处理只包含一个物体且该物体为人的图像:
 
for i in mylist[:10000]:
     xml_file = xml_filepath +i
     arg1=i.split('.')[0]
     discard=0
     with open(xml_file, "rb") as f: # notice the "rb" mode
         d = xmltodict.parse(f, xml_attribs=True)
         l=[]
         if type(d["annotation"]["object"]) == type(l):
             discard=1
         else:
             coords={arg1:[]}
             pre_xtrain.append(arg1)
             m=pre_xtrain[(k+1)]
             k = k+1
             if(discard==0):
                 x1=((float(d['annotation']['object']['bndbox']['xmin'])))/(float(d['annotation']['size']['width']))
                 x2=((float(d['annotation']['object']['bndbox']['xmax'])))/(float(d['annotation']['size']['width']))
                 y1=((float(d['annotation']['object']['bndbox']['ymin'])))/(float(d['annotation']['size']['height']))
                 y2=((float(d['annotation']['object']['bndbox']['ymax'])))/(float(d['annotation']['size']['height']))
                 cls=d['annotation']['object']['name']
                 if(cls == 'person'):
                     coords[arg1].append(x1)
                     coords[arg1].append(y1)
                     coords[arg1].append(x2)
                     coords[arg1].append(y2)
                     coords[arg1].append(cls)
前面的代码附加了物体的位置(经过归一化的图像宽度和高度)。
- 
- 调整人物图像的大小,使所有图像具有相同的形状。此外,将图像缩放至值域在零到一之间:
 
                     filename = base_dir+m+'.jpg'
                     # reference to jpg files here
                     img = filename
                     img_size=224
                     img = cv2.imread(filename)
                     img2 = cv2.resize(img,(img_size,img_size))
                     img2 = img2/255
- 
- 提取物体的边界框位置,并且还要提取归一化的边界框坐标:
 
                    current_y = [int(x1*224), int(y1*224), int(x2*224), int(y2*224)]
                    current_y2 = [float(d['annotation']['object']['bndbox']['xmin']), float(d['annotation']['object']['bndbox']['ymin']),
 float(d['annotation']['object']['bndbox']['xmax'])-float(d['annotation']['object']['bndbox']['xmin']),
 float(d['annotation']['object']['bndbox']['ymax'])-float(d['annotation']['object']['bndbox']['ymin'])]
                    label_center = [(current_y[0]+current_y[2])/2,(current_y[1]+current_y[3])/2] 
                    label = current_y
- 
- 提取输入图像的 VGG16 特征:
 
            vgg_predict =vgg16_model.predict(img2.reshape(1,img_size,img_size,3))
            x_train.append(vgg_predict)
到这一步,我们已经创建了输入特征。
- 
- 让我们创建输出特征——在本例中,我们将为类标签生成 5 x 5 x 5 的输出,为边界框修正标签生成 5 x 5 x 20 的标签:
 
            target_class = np.zeros((num_grids,num_grids,5))
            target_delta = np.zeros((num_grids,num_grids,20))
在前面的步骤中,我们为目标类和边界框修正初始化了零数组:
def positive_grid_cell(label,img_width = 224, img_height = 224): 
     label_center = [(label[0]+label[2])/(2),(label[1]+label[3])/(2)] 
     a = int(label_center[0]/(img_width/num_grids)) 
     b = int(label_center[1]/(img_height/num_grids)) 
     return a, b
在前面的步骤中,我们定义了一个包含物体中心的函数:
            a,b = positive_grid_cell(label)
前面的代码帮助我们将1类分配给包含物体中心的网格,其他所有网格的标签将为零。
此外,让我们定义一个函数,找到与感兴趣物体形状最接近的锚框:
def find_closest_anchor(label,img_width, img_height):
     label_width = (label[2]-label[0])/img_width
     label_height = (label[3]-label[1])/img_height 
     label_width_height_array = np.array([label_width, label_height]) 
     distance = np.sum(np.square(np.array(anchors) - label_width_height_array), axis=1) 
     closest_anchor = anchors[np.argmin(distance)] 
     return closest_anchor
前面的代码将图像中感兴趣物体的宽度和高度与所有可能的锚框进行比较,并识别出最接近图像中实际物体宽度和高度的锚框。
最后,我们还将定义一个计算锚框边界框修正的函数,如下所示:
def closest_anchor_corrections(a, b, anchor, label, img_width, img_height): 
     label_center = [(label[0]+label[2])/(2),(label[1]+label[3])/(2)] 
     anchor_center = [a*img_width/num_grids , b*img_height/num_grids ] 
     dx = (label_center[0] - anchor_center[0])/img_width 
     dy = (label_center[1] - anchor_center[1])/img_height
     dw = ((label[2] - label[0])/img_width) / (anchor[0])
     dh = ((label[3] - label[1])/img_height) / (anchor[1]) 
     return dx, dy, dw, dh 
现在我们已经准备好创建目标数据了:
for a2 in range(num_grids):
     for b2 in range(num_grids):
         for m in range(len(anchors)):
             dx, dy, dw, dh = closest_anchor_corrections(a2, b2, anchors[m], label, 224, 224)
             target_class[a2,b2,m] = 0
             target_delta[a2,b2,((4*m)):((4*m)+4)] = [dx, dy, dw, dh]
             anc.append(anchors[m])
             if((anchors[m] == find_closest_anchor(label,224, 224)) & (a2 == a) & (b2 == b)):
                 target_class[a2,b2,m] = 1
在前面的代码中,当考虑到的锚框是与图像中物体形状最接近的锚框时,我们将目标类赋值为1。
我们还将边界框修正存储在另一个列表中:
            y_train.append(target_class.flatten())
            y_delta.append(target_delta)
- 构建一个模型来识别最可能包含物体的网格单元和锚框:
from keras.optimizers import Adam
optimizer = Adam(lr=0.001)
from keras.layers import BatchNormalization
from keras import regularizers
model = Sequential()
model.add(BatchNormalization(input_shape=(7,7,512)))
model.add(Conv2D(1024, (3,3), activation='relu',padding='valid'))
model.add(BatchNormalization())
model.add(Conv2D(5, (1,1), activation='relu',padding='same'))
model.add(Flatten())
model.add(Dense(125, activation='sigmoid'))
model.summary()
- 创建用于分类的输入和输出数组:
y_train = np.array(y_train)
x_train = np.array(x_train)
x_train = x_train.reshape(x_train.shape[0],7,7,512)
- 编译并拟合分类模型:
model.compile(loss='binary_crossentropy', optimizer=optimizer)
model.fit(x_train/np.max(x_train), y_train, epochs=5, batch_size = 32, validation_split = 0.1, verbose = 1)
- 从前面的模型中,我们能够识别出最可能包含人物的网格单元和锚框组合。在此步骤中,我们将构建一个数据集,其中我们修正最有可能包含物体的预测边界框:
delta_x = []
delta_y = []
for i in range(len(x_train)):
     delta_x.append(x_train[i])
     delta = y_delta[i].flatten()
     coord = np.argmax(model.predict(x_train[i].reshape(1,7,7,512)/12))
     delta_y.append(delta[(coord*4):((coord*4)+4)])
在前一步中,我们已经准备好了输入(即原始图像的 VGG16 特征)和输出边界框修正,用于预测最有可能包含物体的区域。
请注意,我们将coord乘以四,因为对于每个网格单元和锚框组合,都有四个可能的修正值,分别对应* x 、 y 、 w * 和 * h *。
- 
构建一个模型,用来预测* x 、 y 、 w * 和 * h * 坐标的修正: - 创建输入和输出数组,并对四个边界框修正值进行标准化,使得所有四个值具有相似的范围:
 
delta_x = np.array(delta_x)
delta_y = np.array(delta_y)
max_y = np.max(delta_y, axis=0)
delta_y2 = deltay/max_y
- 
- 构建一个模型,根据 VGG16 特征预测边界框修正:
 
model2 = Sequential()
model2.add(BatchNormalization(input_shape=(7,7,512)))
model2.add(Conv2D(1024, (3,3), activation='relu',padding='valid'))
model2.add(BatchNormalization())
model2.add(Conv2D(5, (1,1), activation='relu',padding='same'))
model2.add(Flatten())
model2.add(Dense(2, activation='linear'))
- 编译并拟合模型:
model2.compile(loss = 'mean_absolute_error', optimizer = optimizer)
model2.fit(delta_x/np.max(x_train), delta_y2, epochs = 10, batch_size = 32, verbose = 1, validation_split = 0.1)
- 
在新图像上预测边界框: - 提取最有可能包含物体的位置:
 
img = cv2.imread('/content/Hemanvi.jpg')
img = cv2.resize(img,(224,224))
img = img/255
img2 = vgg16_model.predict(img.reshape(1,224,224,3))
arg = np.argmax(model.predict(img2/np.max(x_train)))
在前面的步骤中,我们选择了一张包含人物的图像,并调整其大小,以便进一步处理以提取 VGG16 特征。最后,我们识别出最有可能包含人物位置的锚框。
- 
- 提取最有可能包含图像的网格单元和锚框组合。
 
上一步预测了最有可能包含感兴趣物体的网格单元和锚框组合,具体做法如下:
count = 0
for a in range(num_grids):
     for b in range(num_grids):
         for c in range(len(anchors)):
             if(count == arg):
                 a2 = a
                 b2 = b
                 c2 = c 
             count+=1
在前面的代码中,a2和b2将是最有可能包含物体的网格单元(即* x 轴和 y *轴的组合),c2是可能与物体形状相同的锚框。
- 
- 预测* x 、 y 、 w * 和 * h * 坐标的修正:
 
pred = model2.predict(img2/np.max(delta_x))[0]
- 
- 去标准化预测的边界框修正:
 
pred1 = pred*max_y
- 
- 提取最终修正后的* x 、 y 、 w * 和 * h * 坐标:
 
xmin = pred1[0]*224+a2*224/num_grids - (anchors[c2][0]*pred1[2] * 224)/2
ymin = pred1[1]*224+b2*224/num_grids - (anchors[c2][1]*pred1[3] * 224)/2
w = anchors[c2][0]*pred1[2] * 224
h = anchors[c2][1]*pred1[3] * 224
- 
- 绘制图像和边界框:
 
import matplotlib.patches as mpatches
cand = [xmin, ymin, w, h]
cand = np.clip(cand, 1, 223)
fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6))
ax.imshow(img)
rect = mpatches.Rectangle(
(cand[0], cand[1]), cand[2], cand[3], fill=False, edgecolor='red', linewidth=1)
ax.add_patch(rect)
plt.grid('off')
plt.show()

这种方法的一个缺点是,当我们检测的物体相较于图像大小非常小时,检测变得更加困难。
还有更多...
假设我们考虑一个要检测的物体较小的场景。如果将这张图片传入预训练网络中,该物体会在早期层中被检测到,因为在最后几层中,图像会经过多个池化层处理,导致物体被压缩到一个非常小的空间中。
同样地,如果要检测的物体较大,那么该物体会在预训练网络的最后几层中被检测到。
单次检测器使用一个预训练网络,其中网络的不同层负责检测不同类型的图像:

来源:https://arxiv.org/pdf/1512.02325.pdf
在前面的图示中,你需要注意,不同层的特征会通过一个全连接层,最终被连接在一起,以便构建和微调模型。
此外,YOLO 也可以基于此教程实现:pjreddie.com/darknet/yolo/。
第七章:自动驾驶汽车中的图像分析应用
在前几章中,我们学习了物体分类以及物体定位。在本章中,我们将通过多个与自动驾驶汽车相关的案例研究。
你将学习以下内容:
- 
交通标志识别 
- 
预测汽车需要转动的角度范围 
- 
使用 U-net 架构识别道路上的汽车 
- 
路面上物体的语义分割 
交通标志识别
在本案例研究中,我们将了解如何将信号分类为 43 种可能的类别之一。
准备就绪
对于本练习,我们将采用以下策略:
- 
下载包含所有可能交通标志的数据集 
- 
对输入图像执行直方图归一化处理: - 
某些图像是在明亮的白天拍摄的,而其他一些可能是在黄昏时拍摄的 
- 
不同的光照条件会导致像素值的变化,具体取决于拍摄照片时的光照条件 
- 
直方图归一化对像素值进行归一化处理,使它们具有相似的分布 
 
- 
- 
缩放输入图像 
- 
构建、编译并拟合模型以减少类别交叉熵损失值 
如何实现...
- 下载数据集,如下所示(代码文件可在 GitHub 中的Traffic_signal_detection.ipynb找到)。数据集可通过论文获得:J. Stallkamp, M. Schlipsing, J. Salmen, C. Igel, 《人与计算机:基准机器学习算法在交通标志识别中的表现》:
$ wget http://benchmark.ini.rub.de/Dataset/GTSRB_Final_Training_Images.zip
$ unzip GTSRB_Final_Training_Images.zip
- 将图像路径读取到列表中,如下所示:
from skimage import io
import os
import glob
root_dir = '/content/GTSRB/Final_Training/Images/'
all_img_paths = glob.glob(os.path.join(root_dir, '*/*.ppm'))
图像的样例如下所示:

请注意,某些图像的形状较小,而某些图像的光照较强。因此,我们需要对图像进行预处理,使所有图像在光照和形状方面都进行标准化。
- 对输入数据集执行直方图归一化处理,如下所示:
import numpy as np
from skimage import color, exposure, transform
NUM_CLASSES = 43
IMG_SIZE = 48
def preprocess_img(img):
     hsv = color.rgb2hsv(img)
     hsv[:, :, 2] = exposure.equalize_hist(hsv[:, :, 2])
     img = color.hsv2rgb(hsv)
     img = transform.resize(img, (IMG_SIZE, IMG_SIZE))
     return img
在前面的代码中,我们首先将 RGB 格式的图像转换为色调饱和度值(HSV)格式。通过将图像从 RGB 格式转换为 HSV 格式,我们实质上是将 RGB 组合值转换为一个数组,然后再将其转换为单维数组。
然后,我们将使用equalize_hist方法对以 HSV 格式获得的值进行归一化,使它们归于相同的尺度。
一旦图像在 HSV 格式的最后一个通道中被归一化,我们将它们转换回 RGB 格式。
最后,我们将图像调整为标准尺寸。
- 检查图像在通过直方图归一化之前的状态,并将其与归一化后的状态进行对比(即通过preprocess_img函数处理后的图像),如下所示:


从前面的图片中可以看出,经过直方图归一化后(右侧图像),图像的可见度发生了显著变化(左侧图像)。
- 按如下方式准备输入和输出数组:
count = 0
imgs = []
labels = []
for img_path in all_img_paths:
     img = preprocess_img(io.imread(img_path))
     label = img_path.split('/')[-2]
     imgs.append(img)
     labels.append(label)
X = np.array(imgs)
Y = to_categorical(labels, num_classes = NUM_CLASSES)
- 按如下方式构建训练集和测试集:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size = 0.2, random_state= 42)
- 如下所示,构建并编译模型:
model = Sequential()
model.add(Conv2D(32, (3, 3), padding='same',input_shape=(IMG_SIZE, IMG_SIZE, 3), activation='relu'))
model.add(Conv2D(32, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.2))
model.add(Conv2D(64, (3, 3), padding='same',activation='relu'))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.2))
model.add(Conv2D(128, (3, 3), padding='same',activation='relu'))
model.add(Conv2D(128, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(NUM_CLASSES, activation='softmax'))
model.summary()
model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy']
模型的总结如下:

- 如下所示,拟合模型:
model.fit(X_train, y_train,batch_size=32,epochs=5,validation_data = (X_test, y_test))
前面的代码生成了一个模型,其准确率约为 99%:

此外,如果您执行与我们相同的分析,但没有进行直方图归一化(曝光校正),模型的准确率约为 97%。
预测汽车需要转动的角度
在本案例中,我们将基于提供的图像来理解需要转动汽车的角度。
准备就绪
我们采用的构建转向角度预测策略如下:
- 
收集一个数据集,其中包含道路的图像和需要转动方向盘的相应角度 
- 
预处理图像 
- 
将图像传入 VGG16 模型以提取特征 
- 
构建一个神经网络,执行回归以预测转向角度,这是一个需要预测的连续值 
如何进行……
- 下载以下数据集。该数据集可以通过以下链接获得:github.com/SullyChen/driving-datasets:(代码文件可以在 GitHub 中的Car_steering_angle_detection.ipynb找到):
$ pip install PyDrive 
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from google.colab import auth
from oauth2client.client import GoogleCredentials
auth.authenticate_user()
gauth = GoogleAuth()
gauth.credentials = GoogleCredentials.get_application_default()
drive = GoogleDrive(gauth)
file_id = '0B-KJCaaF7elleG1RbzVPZWV4Tlk' # URL id. 
downloaded = drive.CreateFile({'id': file_id})
downloaded.GetContentFile('steering_angle.zip')
$ unzip steering_angle.zip
- 导入相关的包,如下所示:
import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import pi
import cv2
import scipy.misc
import tensorflow as tf
- 将图像及其对应的弧度角度分别读取到单独的列表中,如下所示:
DATA_FOLDER = "/content/driving_dataset/"
DATA_FILE = os.path.join(DATA_FOLDER, "data.txt")
x = []
y = []
train_batch_pointer = 0
test_batch_pointer = 0
with open(DATA_FILE) as f:
     for line in f:
         image_name, angle = line.split() 
         image_path = os.path.join(DATA_FOLDER, image_name)
         x.append(image_path) 
         angle_radians = float(angle) * (pi / 180) #converting angle into radians
         y.append(angle_radians)
y = np.array(y)
- 如下所示,创建训练集和测试集:
split_ratio = int(len(x) * 0.8)
train_x = x[:split_ratio]
train_y = y[:split_ratio]
test_x = x[split_ratio:]
test_y = y[split_ratio:]
- 检查训练和测试数据集中的输出标签值,如下所示:
fig = plt.figure(figsize = (10, 7))
plt.hist(train_y, bins = 50, histtype = "step",color='r')
plt.hist(test_y, bins = 50, histtype = "step",color='b')
plt.title("Steering Wheel angle in train and test")
plt.xlabel("Angle")
plt.ylabel("Bin count")
plt.grid('off')
plt.show()

- 删除前 100 行的像素,因为这些像素与道路图像无关,然后将处理后的图像传入 VGG16 模型。此外,在此练习中,我们仅使用数据集中的前 10,000 张图像,以便更快地构建模型。删除前 100 行的像素,如下所示:
x = []
y = []
for i in range(10000):
     im = cv2.imread(train_x[i])
     im = im[100:,:,:]/255
     vgg_im = vgg16_model.predict(im.reshape(1,im.shape[0],im.shape[1],3))
     x.append(vgg_im)
     y.append(train_y[i])
x1 = np.array(x)
x1 = x1.reshape(x1.shape[0],4,14,512)
y1 = np.array(y)
- 如下所示,构建并编译模型:
model = Sequential()
model.add(Flatten(input_shape=(4,14,512)))
model.add(Dense(512, activation='relu'))
model.add(Dropout(.5))
model.add(Dense(100, activation='linear'))
model.add(Dropout(.2))
model.add(Dense(50, activation='linear'))
model.add(Dropout(.1))
model.add(Dense(10, activation='linear'))
model.add(Dense(1, activation='linear'))
model.summary()
注意,输出层采用线性激活,因为输出是一个连续值,范围从 -9 到 +9。模型的总结如下:

现在,我们将按如下方式编译已定义的模型:
model.compile(loss='mean_squared_error',optimizer='adam')
- 如下所示,拟合模型:
model.fit(x1/11, y1,batch_size=32,epochs=10, validation_split = 0.1, verbose = 1)

测试损失是前面图表中损失较低的那条线。
注意,我们已经将输入数据集除以 11,以便将其缩放到 0 到 1 之间。现在,我们应该能够根据预测的角度模拟汽车的运动。
模型对样本图像的转向角度预测结果如下:


请注意,使用上述模型时需要非常小心。它应首先在多种日光条件下进行测试,然后再进入生产环境。
使用 U-net 架构进行实例分割
到目前为止,在前两章中,我们已经学习了如何检测物体,以及如何识别图像中物体所在的边界框。在本节中,我们将学习如何进行实例分割,在实例分割中,属于某个特定物体的所有像素都会被突出显示,而其他像素则不会(这类似于用零掩膜掉所有不属于物体的像素,并用像素值 1 掩膜属于物体的像素)。
准备开始
为了执行实例分割,我们将执行以下操作:
- 
在一个数据集上工作,该数据集具有输入图像及其对应的掩膜图像,掩膜图像显示对象在图像中的像素位置: - 图像及其掩膜图像
 
- 
我们将通过预训练的 VGG16 模型将图像传递,以提取每个卷积层的特征 
- 
我们将逐渐上采样卷积层,以便我们获得形状为 224 x 224 x 3 的输出图像 
- 
我们将冻结使用 VGG16 权重的层 
- 
将上采样的卷积层与下采样的卷积层连接起来: - 
这形成了 U 形连接 
- 
U 形连接帮助模型获得类似于 ResNet 的上下文(之前下采样的层提供上下文,除了上采样的层外) 
- 
如果我们取第一层的输出,重建图像会更容易,因为大部分图像在第一层中是完好的(早期层学习图像的轮廓)。如果我们尝试通过上采样最后几层来重建图像,那么很有可能大部分图像信息会丢失 
 
- 
- 
拟合一个将输入图像映射到掩膜图像的模型: - 注意,掩膜图像本质上是二进制的——黑色值对应于像素值 0,白色像素的值为 1
 
- 
在所有 224 x 224 x 1 像素中最小化二元交叉熵损失函数 
之所以称该模型为U-net 架构,是因为模型的可视化如下所示——一个旋转的 U 形结构:

模型的 U 形结构是由于早期层连接到下采样层的上采样版本。
如何实现...
在以下代码中,我们将执行实例分割,以检测图像中的汽车:
- 从github.com/divamgupta/image-segmentation-keras下载并导入文件,如下所示:
$ wget https://www.dropbox.com/s/0pigmmmynbf9xwq/dataset1.zip
$ unzip dataset1.zip
dir_data = "/content/dataset1"
dir_seg = dir_data + "/annotations_prepped_train/"
dir_img = dir_data + "/images_prepped_train/"
import glob, os
all_img_paths = glob.glob(os.path.join(dir_img, '*.png'))
all_mask_paths = glob.glob(os.path.join(dir_seg, '*.png'))
- 将图像及其对应的掩膜读取为数组,如下所示:
import cv2
from scipy import ndimage
x = []
y = []
for i in range(len(all_img_paths)):
  img = cv2.imread(all_img_paths[i])
  img = cv2.resize(img,(224,224))
  mask_path = dir_seg+all_img_paths[i].split('/')[4]
  img_mask = ndimage.imread(mask_path)
  img_mask = cv2.resize(img_mask,(224,224))
  x.append(img)
  y.append(img_mask)
x = np.array(x)/255
y = np.array(y)/255
y2 = np.where(y==8,1,0)
在前面的步骤中,我们创建了输入和输出数组,并且还对输入数组进行了归一化。最后,我们从所有其他内容中分离出了汽车的掩模,因为该数据集有 12 个唯一类别,其中汽车的像素值被标记为 8。
输入和掩模图像的示例如下:

此外,我们创建了输入和输出数组,其中我们对输入数组进行缩放,并重新塑形输出数组(以便可以传递给网络),如下所示:
x = np.array(x)
x = x/255
y2 = np.array(y2)
y2 = y2.reshape(y2.shape[0],y2.shape[1],y2.shape[2],1)
- 构建模型,其中图像首先通过 VGG16 模型层,提取卷积特征,如下所示:
在以下代码中,我们导入了预训练的 VGG16 模型:
from keras.applications.vgg16 import VGG16 as PTModel
from keras.layers import Input, Conv2D, concatenate, UpSampling2D, BatchNormalization, Activation, Cropping2D, ZeroPadding2D
from keras.layers import Input, merge, Conv2D, MaxPooling2D,UpSampling2D, Dropout, Cropping2D, merge, concatenate
from keras.optimizers import Adam
from keras.callbacks import ModelCheckpoint, LearningRateScheduler
from keras import backend as K
from keras.models import Model
base_pretrained_model = PTModel(input_shape = (224,224,3), include_top = False, weights = 'imagenet')
base_pretrained_model.trainable = False
在以下代码中,当不同的卷积层通过 VGG16 模型时,我们提取了特征:
conv1 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block1_conv2').output).output
conv2 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block2_conv2').output).output
conv3 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block3_conv3').output).output
conv4 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block4_conv3').output).output
drop4 = Dropout(0.5)(conv4)
conv5 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block5_conv3').output).output
drop5 = Dropout(0.5)(conv5)
在以下代码中,我们使用UpSampling方法对特征进行上采样,并在每一层将其与下采样后的 VGG16 卷积特征进行拼接:
up6 = Conv2D(512, 2, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(UpSampling2D(size =(2,2))(drop5))
merge6 = concatenate([drop4,up6], axis = 3) 
conv6 = Conv2D(512, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(merge6)
conv6 = Conv2D(512, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv6)
conv6 = BatchNormalization()(conv6)
up7 = Conv2D(256, 2, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(UpSampling2D(size =(2,2))(conv6))
merge7 = concatenate([conv3,up7], axis = 3)
conv7 = Conv2D(256, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(merge7)
conv7 = Conv2D(256, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv7)
conv7 = BatchNormalization()(conv7)
up8 = Conv2D(128, 2, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(UpSampling2D(size =(2,2))(conv7))
merge8 = concatenate([conv2,up8],axis = 3)
conv8 = Conv2D(128, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(merge8)
conv8 = Conv2D(128, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv8)
conv8 = BatchNormalization()(conv8)
up9 = Conv2D(64, 2, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(UpSampling2D(size =(2,2))(conv8))
merge9 = concatenate([conv1,up9], axis = 3)
conv9 = Conv2D(64, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(merge9)
conv9 = Conv2D(64, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv9)
conv9 = Conv2D(2, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv9)
conv9 = BatchNormalization()(conv9)
conv10 = Conv2D(1, 1, activation = 'sigmoid')(conv9)
在以下代码中,我们定义了模型的输入和输出,其中输入首先传递给base_pretrained_model,输出是conv10(其形状为 224 x 224 x 1—我们输出的预期形状):
model = Model(input = base_pretrained_model.input, output = conv10)
- 冻结通过训练得到的 VGG16 模型的卷积层,如下所示:
for layer in model.layers[:18]:
     layer.trainable = False
- 编译并拟合模型,以处理数据集中前 1,000 张图像,如下所示:
from keras import optimizers
adam = optimizers.Adam(1e-3, decay = 1e-6)
model.compile(loss='binary_crossentropy',optimizer=adam,metrics=['accuracy'])
history = model.fit(x,y,validation_split = 0.1,batch_size=1,epochs=5,verbose=1)

- 在数据集的最后两张测试图像上测试之前的模型(这些是具有validation_split = 0.1的测试图像),如下所示:
y_pred = model.predict(x[-2:].reshape(2,224,224,3))

我们可以看到,对于给定的道路输入,生成的掩模非常真实,并且比之前的方法更好,因为预测的掩模图像中没有噪点。
图像中对象的语义分割
在上一节中,我们学习了如何对包含单一对象的图像进行分割。在本节分割中,我们将学习如何进行分割,以便能够区分图像中存在的多个对象,尤其是在道路图像中。
准备开始
我们将采用的策略是,在道路图像上执行语义分割,如下所示:
- 
收集一个数据集,其中包含标注了图像中多个对象位置的信息: - 语义图像的示例如下所示:
 

- 
将输出掩模转换为多维数组,其中列数等于所有可能的唯一对象的数量。 
- 
如果有 12 个可能的唯一值(12 个唯一对象),将输出图像转换为形状为 224 x 224 x 12 的图像: - 一个通道的值表示该通道对应的对象在图像中的该位置存在。
 
- 
利用我们在前面部分看到的模型架构,训练一个具有 12 个可能输出值的模型 
- 
通过将所有三个通道分配相同的输出,将预测结果重塑为三个通道: - 输出是 12 个可能类别的概率预测的最大值(argmax)
 
如何实现...
语义分割的代码实现如下(代码文件可以在 GitHub 上找到,名为Semantic_segmentation.ipynb):
- 下载数据集,如下所示:
!wget https://www.dropbox.com/s/0pigmmmynbf9xwq/dataset1.zip
!unzip dataset1.zip
dir_data = "/content/dataset1"
dir_seg = dir_data + "/annotations_prepped_train/"
dir_img = dir_data + "/images_prepped_train/"
import glob, os
all_img_paths = glob.glob(os.path.join(dir_img, '*.png'))
all_mask_paths = glob.glob(os.path.join(dir_seg, '*.png'))
- 将图像及其对应标签分别读取到不同的列表中,如下所示:
import cv2
from scipy import ndimage
for i in range(len(all_img_paths)):
     img = cv2.imread(all_img_paths[i])
     img = cv2.resize(img,(224,224))
     mask_path = dir_seg+all_img_paths[i].split('/')[4]
     img_mask = ndimage.imread(mask_path)
     img_mask = cv2.resize(img_mask,(224,224))
     x.append(img)
     y.append(img_mask)
- 
定义一个函数,将三个通道的输出图像转换为 12 个通道,其中有 12 个唯一的输出值: - 提取输出中存在的唯一值(对象)的数量,如下所示:
 
n_classes = len(set(np.array(y).flatten()))
- 
- 将掩模图像转换为一热编码版本,通道数量与数据集中对象的总数相同,如下所示:
 
def getSegmentationArr(img):
      seg_labels = np.zeros(( 224, 224, n_classes ))
      for c in range(n_classes):
            seg_labels[: , : , c ] = (img == c ).astype(int)
      return seg_labels
y2 = []
for i in range(len(y)):
     y2.append(getSegmentationArr(y[i]))
y2 = np.array(y2)
x = x/255
- 
构建模型: - 将图像传递给预训练的 VGG16 模型,如下所示:
 
from keras.applications.vgg16 import VGG16 as PTModel
base_pretrained_model = PTModel(input_shape = (224,224,3), include_top = False, weights = 'imagenet')
base_pretrained_model.trainable = False
- 
- 提取图像的 VGG16 特征,如下所示:
 
conv1 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block1_conv2').output).output
conv2 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block2_conv2').output).output
conv3 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block3_conv3').output).output
conv4 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block4_conv3').output).output
drop4 = Dropout(0.5)(conv4)
conv5 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block5_conv3').output).output
drop5 = Dropout(0.5)(conv5)
- 
- 将卷积特征通过上采样层传递,并将它们连接形成一个简单的 U-net 架构,如下所示:
 
conv6 = Conv2D(512, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(merge6)
conv6 = Conv2D(512, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv6)
conv6 = BatchNormalization()(conv6)
up7 = Conv2D(256, 2, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(UpSampling2D(size =(2,2))(conv6))
merge7 = concatenate([conv3,up7], axis = 3)
conv7 = Conv2D(256, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(merge7)
conv7 = Conv2D(256, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv7)
conv7 = BatchNormalization()(conv7)
up8 = Conv2D(128, 2, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(UpSampling2D(size =(2,2))(conv7))
merge8 = concatenate([conv2,up8],axis = 3)
conv8 = Conv2D(128, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(merge8)
conv8 = Conv2D(128, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv8)
conv8 = BatchNormalization()(conv8)
up9 = Conv2D(64, 2, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(UpSampling2D(size =(2,2))(conv8))
merge9 = concatenate([conv1,up9], axis = 3)
conv9 = Conv2D(64, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(merge9)
conv9 = Conv2D(64, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv9)
conv9 = Conv2D(2, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv9)
conv9 = BatchNormalization()(conv9)
conv10 = Conv2D(1, 1, activation = 'sigmoid')(conv9)
model = Model(input = base_pretrained_model.input, output = conv10)
- 冻结 VGG16 层,如下所示:
for layer in model.layers[:18]:
     layer.trainable = False
- 编译并拟合模型,如下所示:
model.compile(optimizer=Adam(1e-3, decay = 1e-6), 
 loss='categorical_crossentropy', metrics = ['accuracy'])
history = model.fit(x,y2,epochs=15,batch_size=1,validation_split=0.1)

- 对测试图像进行预测,如下所示:
y_pred = model.predict(x[-2:].reshape(2,224,224,3))
y_predi = np.argmax(y_pred, axis=3)
y_testi = np.argmax(y2[-2:].reshape(2,224,224,12), axis=3)
import matplotlib.pyplot as plt
%matplotlib inline
plt.subplot(231)
plt.imshow(x[-1])
plt.axis('off')
plt.title('Original image')
plt.grid('off')
plt.subplot(232)
plt.imshow(y[-1])
plt.axis('off')
plt.title('Masked image')
plt.grid('off')
plt.subplot(233)
plt.imshow(y_predi[-1])
plt.axis('off')
plt.title('Predicted masked image')
plt.grid('off')
plt.subplot(234)
plt.imshow(x[-2])
plt.axis('off')
plt.grid('off')
plt.subplot(235)
plt.imshow(y[-2])
plt.axis('off')
plt.grid('off')
plt.subplot(236)
plt.imshow(y_predi[-2])
plt.axis('off')
plt.grid('off')
plt.show()
上面的代码将生成一张图像,其中预测的语义图像与实际的语义图像如下所示:

从前面的图像中可以看出,我们能够准确地识别图像中的语义结构,且准确度非常高(我们训练的模型约为 90%)。
第八章:图像生成
在前面的章节中,我们学习了如何预测图像的类别并检测物体在图像中的位置。如果我们反向操作,给定一个类别后,我们应该能够生成一张图像。在这种情况下,生成网络非常有用,我们尝试创建看起来与原始图像非常相似的新图像。
本章将涵盖以下几种方法:
- 
通过对抗性攻击生成能够欺骗神经网络的图像 
- 
使用 DeepDream 算法生成图像 
- 
图像之间的神经风格迁移 
- 
使用生成对抗网络生成数字图像 
- 
使用深度卷积生成对抗网络生成数字图像 
- 
使用深度卷积生成对抗网络(Deep Convolutional GAN)生成面部 
- 
面部从一个到另一个的过渡 
- 
对生成的图像进行向量算术运算 
介绍
在前几章中,我们确定了分类图像到正确类别的最优权重。通过改变以下内容,可以改变图像的输出类别:
- 
连接输入层和输出层的权重,输入像素保持恒定 
- 
输入像素值保持不变时,权重保持恒定 
本章将采用这两种技术来生成图像。
在对抗性攻击的案例研究中,神经风格迁移和 DeepDream 将利用改变输入像素值的技巧。而涉及生成对抗网络(GAN)的技术,则会利用改变连接输入像素值和输出的某些权重的技巧。
本章的前三个案例研究将利用改变输入像素值的技巧,而其余的则利用改变连接输入和输出的权重。
通过对抗性攻击生成能够欺骗神经网络的图像
为了了解如何对图像执行对抗性攻击,我们先了解如何使用迁移学习进行常规预测,然后我们将弄清楚如何调整输入图像,以便图像的类别完全不同,尽管我们几乎没有改变输入图像。
准备工作
让我们通过一个例子,尝试识别图像中的物体类别:
- 
读取一张猫的图像 
- 
预处理图像,以便将其传递给 Inception 网络 
- 
导入预训练的 Inception v3 模型 
- 
预测图像中物体的类别 
- 
由于 Inception v3 在预测属于 ImageNet 类之一的物体时表现良好,图像将被预测为波斯猫 
当前任务是以这样的方式改变图像,使其满足以下两个标准:
- 
使用相同的网络对新图像进行预测时,应该以非常高的概率预测为非洲象 
- 
新生成的图像应在人眼看来与原始图像无法区分 
为了实现这一目标,我们将遵循以下策略:
- 
定义损失函数: - 
损失是图像(波斯猫)属于非洲象类别的概率 
- 
损失越高,我们离目标就越近 
- 
因此,在此情况下,我们将最大化我们的损失函数 
 
- 
- 
计算损失变化相对于输入变化的梯度: - 这一步有助于理解哪些输入像素将输出向我们的目标推进
 
- 
基于计算出的梯度更新输入图像: - 
确保原始图像中的像素值在最终图像中不会偏移超过 3 个像素 
- 
这确保了生成的图像在人眼看来与原始图像无法区分 
 
- 
- 
重复步骤 2 和步骤 3,直到更新后的图像被预测为非洲象,且置信度至少为 0.8 
如何操作...
现在,让我们在代码中实现这一策略(代码文件可在 GitHub 上的Adversarial_attack.ipynb找到):
- 读取猫的图像:
import matplotlib.pyplot as plt
%matplotlib inline
img = cv2.imread('/content/cat.JPG')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, (299,299))
plt.imshow(img)
plt.axis('off')
图像的绘制如下所示:

- 预处理图像,以便将其传递到 Inception 网络:
original_image = cv2.resize(img,(299,299)).astype(float)
original_image /= 255.
original_image -= 0.5
original_image *= 2.
original_image = np.expand_dims(original_image, axis=0)
- 导入预训练模型:
import numpy as np
from keras.preprocessing import image
from keras.applications import inception_v3
model = inception_v3.InceptionV3()
- 预测图像中对象的类别:
predictions = model.predict(original_image)
predicted_classes = inception_v3.decode_predictions(predictions, top=1)
imagenet_id, name, confidence = predicted_classes[0][0]
print("This is a {} with {:.4}% confidence".format(name, confidence * 100))
前面的代码结果如下:
" This is a Persian_cat with 95.45% confidence"
- 定义输入和输出:
model = inception_v3.InceptionV3()
model_input_layer = model.layers[0].input
model_output_layer = model.layers[-1].output
model_input_layer是模型的输入,model_output_layer是输入图像的各种类别的概率(最后一层使用 softmax 激活)。
- 设置原始图像变化的限制:
max_change_above = np.copy(original_image) + 0.01
max_change_below = np.copy(original_image) - 0.01
hacked_image = np.copy(original_image)
在前面的代码中,我们指定了原始图像可以改变的限制。
- 初始化代价函数,使得要伪装的对象类型是非洲象(预测向量中第 386 个索引值):
learning_rate = 0.1
object_type_to_fake = 386
cost_function = model_output_layer[0, object_type_to_fake]
model_output_layer的输出是感兴趣图像的各种类别的概率。在此实例中,我们指定代价函数将由我们试图将对象伪装成的目标对象的索引位置来决定。
- 初始化代价函数相对于输入的梯度:
gradient_function = K.gradients(cost_function, model_input_layer)[0]
这段代码计算了cost_function相对于model_input_layer(即输入图像)变化的梯度。
- 映射与输入相关的代价和梯度函数:
grab_cost_and_gradients_from_model = K.function([model_input_layer], [cost_function, gradient_function])
cost = 0.0
在前面的代码中,我们正在计算cost_function(图像属于非洲象类别的概率)和相对于输入图像的梯度。
- 一直更新输入图像,直到生成图像的非洲象概率至少达到 80%:
while cost < 0.80:
    cost, gradients = grab_cost_and_gradients_from_model([hacked_image, 0])
    hacked_image += gradients * learning_rate
    hacked_image = np.clip(hacked_image, max_change_below, max_change_above)
    print("Model's predicted likelihood that the image is an African elephant: 
{:.8}%".format(cost * 100))
在前面的代码中,我们获取与输入图像(hacked_image)对应的代价和梯度。此外,我们通过梯度(与学习率相乘)更新输入图像。最后,如果被修改的图像超过了输入图像的最大变化阈值,我们将对其进行裁剪。
不断循环这些步骤,直到你得到输入图像的概率至少为 0.8。
随着训练轮次的增加,波斯猫图像被识别为非洲象图像的概率变化如下:
epochs = range(1, len(prob_elephant) + 1)
plt.plot(epochs, prob_elephant, 'b')
plt.title('Probability of African elephant class')
plt.xlabel('Epochs')
plt.ylabel('Probability')
plt.grid('off')
修改后的图像属于非洲象类别的概率变化如下:

- 预测更新图像的类别:
model.predict(hacked_image)[0][386]
predict方法的输出是修改后图像属于非洲象类别的概率,值为 0.804。
- 对更新后的输入图像进行去处理(因为它在预处理时已经被缩放)以便可视化:
hacked_image = hacked_image/2
hacked_image = hacked_image + 0.5
hacked_image = hacked_image*255
hacked_image = np.clip(hacked_image, 0, 255).astype('uint8')
plt.subplot(131)
plt.imshow(img)
plt.title('Original image')
plt.axis('off')
plt.subplot(132)
plt.imshow(hacked_image[0,:,:,:])
plt.title('Hacked image')
plt.axis('off')
plt.subplot(133)
plt.imshow(img - hacked_image[0,:,:,:])
plt.title('Difference')
plt.axis('off')
原始图像、修改后的(被篡改的)图像以及两者之间的差异将如下打印出来:

注意,输出现在在视觉上与原始图像无法区分。
有趣的是,尽管像素值几乎没有变化,但我们成功地欺骗了神经网络(inception v3 模型),使它预测了一个不同的类别。这是一个很好的例子,展示了如果用于预测的算法暴露给可以制作欺骗系统图像的用户,可能会遇到的安全漏洞。
使用 DeepDream 算法生成图像
在上一节中,我们稍微调整了输入图像的像素。在这一节中,我们将进一步调整输入图像,以便生成一张仍然是相同物体的图像,但比原图更具艺术感。该算法是使用神经网络进行风格迁移技术的核心。
让我们了解一下 DeepDream 如何工作的直觉。
我们将通过一个预训练模型(例如 VGG19)来处理我们的图像。我们已经了解到,根据输入图像,预训练模型中的某些滤波器激活得最多,而某些滤波器则激活得最少。
我们将提供我们希望激活的神经网络层。
神经网络会调整输入像素值,直到我们获得所选层的最大值。
然而,我们也会确保最大可能的激活值不超过某个值,因为如果激活值过高,结果图像可能会与原始图像有很大不同。
准备就绪
理解了这些直觉后,让我们来看看如何实现 DeepDream 算法的步骤:
- 
选择你想要最强激活的神经网络层,并为这些层对整体损失计算的贡献分配权重。 
- 
提取给定层的输出,当图像通过该层时,并计算每一层的损失值: - 当图像在某一层的输出平方和最大时,图像会最强地激活该层。
 
- 
提取输入像素值变化相对于损失的梯度。 
- 
根据上一阶段提取的梯度更新输入像素值。 
- 
提取更新后的输入像素值在所有选定层中的损失值(激活的平方和)。 
- 
如果损失值(激活值的加权平方和)大于预定义的阈值,则停止更新图像。 
如何做到这一点...
让我们在代码中实现这些步骤(代码文件可在 GitHub 的Deepdream.ipynb中找到):
- 导入相关的包并导入图像:
import keras.backend as K
import multiprocessing
import tensorflow as tf
import warnings
from keras.applications.vgg19 import VGG19
from keras.applications.imagenet_utils import preprocess_input
from scipy.optimize import minimize
from skimage import img_as_float, img_as_ubyte
from skimage.io import imread, imsave
from skimage.transform import pyramid_gaussian, rescale
import scipy
from keras.preprocessing import image
from keras.applications.vgg19 import preprocess_input
import matplotlib.pyplot as plt
%matplotlib inline
对图像进行预处理,使其能够传递到 VGG19 模型:
def preprocess_image(image_path):
     img = image.load_img(image_path, target_size=(img_nrows, img_ncols))
     img = image.img_to_array(img)
     img = np.expand_dims(img, axis=0)
     img[:, :, :, 0] -= 103.939
     img[:, :, :, 1] -= 116.779
     img[:, :, :, 2] -= 123.68
     img = img[:, :, :, ::-1]/255
     return img
构建一个去处理已处理图像的函数:
def deprocess_image(x):
     x = x[:,:,:,::-1]*255
     x[:, :, :, 0] += 103.939
     x[:, :, :, 1] += 116.779
     x[:, :, :, 2] += 123.68
     x = np.clip(x, 0, 255).astype('uint8')
     return x
预处理图像:
img = preprocess_image('/content/cat.png')
- 定义对整体损失值计算有贡献的层:
layer_contributions = {
    'block2_pool':0.3,
    'block5_pool': 1.5}
在前面的代码中,我们展示了将使用第二层和第五层池化层,并且分配这两层对整体损失值的贡献权重。
- 初始化损失函数:
layer_dict = dict([(layer.name, layer) for layer in model.layers])
loss = K.variable(0.)
在前面的步骤中,我们初始化了损失值和模型中各个层的字典。
计算激活的整体损失值:
for layer_name in layer_contributions:
     coeff = layer_contributions[layer_name]
     activation = layer_dict[layer_name].output
     scaling = K.prod(K.cast(K.shape(activation), 'float32'))
     loss += coeff * K.sum(K.square(activation)) / scaling
     print(loss)
在前面的代码中,我们遍历了感兴趣的层(layer_contributions),并记录了为每层分配的权重(coeff)。此外,我们还计算了感兴趣层的输出(activation),并通过对激活值进行缩放后求平方和来更新损失值。
- 初始化梯度值:
dream = model.input
grads = K.gradients(loss, dream)[0]
K.gradients方法给出了损失相对于输入变化(dream)的梯度。
- 对梯度值进行归一化,以便梯度的变化速度较慢:
grads /= K.maximum(K.mean(K.abs(grads)), 1e-7)
- 创建一个函数,将输入图像映射到损失值以及损失值相对于输入像素值变化的梯度(其中输入图像是dream):
outputs = [loss, grads]
fetch_loss_and_grads = K.function([dream], outputs)
- 定义一个函数,提供给定输入图像的损失值和梯度值:
def eval_loss_and_grads(img):
      outs = fetch_loss_and_grads([img])
      loss_value = outs[0]
      grad_values = outs[1]
      return loss_value, grad_values
- 基于获得的损失和梯度值,通过多次迭代更新原始图像。
在下面的代码中,我们遍历图像 100 次。我们定义了图像变化的学习率和图像可能发生的最大损失(变化):
for i in range(100):
      learning_rate=0.01
      max_loss=20
在下面的代码中,我们提取了图像的损失值和梯度值,然后在损失值超过定义的阈值时停止图像的变化:
     loss_value, grad_values = eval_loss_and_grads(img)
     if max_loss is not None and loss_value > max_loss:
         print(loss_value)
         break
     print('...Loss value at', i, ':', loss_value)
在下面的代码中,我们根据梯度值更新图像,并进行去处理图像并打印图像:
    img += learning_rate * grad_values
    img2 = deprocess_image(img.copy())
    plt.imshow(img2[0,:,:,:])
    plt.axis('off')
    plt.show()
前面的代码生成的图像如下所示:

请注意,前面图像中的波浪图案可能是因为这些是最大化各个网络层激活的模式。
在这里,我们看到了扰动输入像素的另一种应用, 在这种情况下,结果是图像略显艺术感。
图像之间的神经风格迁移
在之前的步骤中,修改的像素值试图最大化滤波器的激活值。然而,这并没有给我们提供指定图像风格的灵活性;此时,神经风格迁移派上了用场。
在神经风格迁移中,我们有一个内容图像和一个风格图像,我们尝试以一种方式将这两张图像结合起来,既能保持内容图像中的内容,又能保持风格图像的风格。
准备中
神经风格迁移的直觉如下。
我们尝试以类似于 DeepDream 算法的方式修改原始图像。然而,额外的步骤是将损失值分为内容损失和风格损失。
内容损失指的是生成图像与内容图像之间的差异。风格损失指的是风格图像与生成图像之间的相关性。
虽然我们提到损失是基于图像之间的差异来计算的,但在实践中,我们通过确保使用来自图像的激活值而不是原始图像来稍微修改它。例如,第二层的内容损失将是内容图像和生成图像在通过第二层时激活值之间的平方差。
尽管计算内容损失看起来很直接,但让我们尝试理解如何计算生成图像与风格图像之间的相似性。
一种叫做 gram 矩阵的技术出现了。gram 矩阵计算生成图像和风格图像之间的相似度,计算公式如下:

其中,GM(l) 是风格图像 S 和生成图像 G 在层 l 处的 gram 矩阵值。
gram 矩阵是通过将一个矩阵与其自身的转置相乘得到的。
现在我们可以计算风格损失和内容损失了,最终的修改输入图像是最小化整体损失的图像,也就是风格损失和内容损失的加权平均值。
神经风格迁移的实现步骤如下:
- 
将图像通过一个预训练模型。 
- 
提取预定义层的层值。 
- 
将生成的图像初始化为与内容图像相同。 
- 
将生成的图像通过模型并提取其在相同层的值。 
- 
计算内容损失。 
- 
将风格图像通过模型的多个层,并计算风格图像的 gram 矩阵值。 
- 
将生成的图像通过与风格图像相同的层,并计算其对应的 gram 矩阵值。 
- 
提取两张图像的 gram 矩阵值之间的平方差。这将是风格损失。 
- 
整体损失将是风格损失和内容损失的加权平均值。 
- 
最小化整体损失的输入图像将是最终的目标图像。 
如何做到…
- 导入相关的包和内容、样式图像,它们需要结合在一起形成艺术图像,如下所示(代码文件可在 GitHub 上的Neural_style_transfer.ipynb找到):
from keras.preprocessing.image import load_img, save_img, img_to_array
import numpy as np
import time
from keras.applications import vgg19
from keras.applications.imagenet_utils import preprocess_input
from keras import backend as K
import tensorflow as tf
import keras
style_img = cv2.imread('/content/style image.png')
style_img = cv2.cvtColor(style_img, cv2.COLOR_BGR2RGB)
style_img = cv2.resize(style_img,(224,224))
base_img = cv2.imread('/content/cat.png')
base_img = cv2.cvtColor(base_img, cv2.COLOR_BGR2RGB)
base_img = cv2.resize(base_img,(224,224))
样式图像和基础图像如下所示:


- 初始化vgg19模型,以便图像可以通过其网络传递:
from keras.applications import vgg19
model = vgg19.VGG19(include_top=False, weights='imagenet')
- 重新调整基础图像并提取 VGG19 模型中block3_conv4层的特征值:
base_img = base_img.reshape(1,224,224,3)/255
from keras import backend as K
get_3rd_layer_output = K.function([model.layers[0].input],
[model.get_layer('block3_conv4').output])
layer_output_base = get_3rd_layer_output([base_img])[0]
在前面的代码中,我们定义了一个函数,该函数获取输入图像并在预定义层中提取输出。
- 定义需要提取内容和样式损失的层,以及需要分配给每个层的相应权重:
layer_contributions_content = {'block3_conv4': 0.1}
layer_contributions_style =    { 'block1_pool':1,
                                 'block2_pool':1,
                                 'block3_conv4':1}
在前面的代码中,我们定义了计算内容和样式损失的层,并为这些层产生的损失分配了相应的权重。
- 定义 Gram 矩阵和样式损失函数:
在以下代码中,我们定义了一个函数,该函数计算作为通过扁平化图像获得的特征的点积的 Gram 矩阵输出:
def gram_matrix(x):
    features = K.batch_flatten(K.permute_dimensions(x, (2, 0, 1)))
    gram = K.dot(features, K.transpose(features))
    return gram
在以下代码中,我们正在计算在准备阶段中定义的样式损失方程式中所指定的样式损失:
def style_loss(style, combination):
    S = gram_matrix(style)
    C = gram_matrix(combination)
    channels = 3
    size = img_nrows * img_ncols
    return K.sum(K.square(S - C)) / (4\. * (pow(channels,2)) * (pow(size,2)))
- 初始化损失值函数:
计算内容损失:
layer_dict = dict([(layer.name, layer) for layer in model.layers])
loss = K.variable(0.)
for layer_name in layer_contributions_content:
      coeff = layer_contributions_content[layer_name]
      activation = layer_dict[layer_name].output
      scaling = K.prod(K.cast(K.shape(activation), 'float32'))
      loss += coeff * K.sum(K.square(activation - layer_output_base)) / scaling
在前面的代码中,我们根据计算内容损失的层中的损失更新损失值。请注意,layer_output_base是通过内容层传递原始基础图像时的输出(如第 3 步所定义)。
激活(基于修改后的图像)和layer_output_base(基于原始图像)之间的差异越大,图像的内容损失就越大。
计算样式损失:
for layer_name in layer_contributions_style:
    coeff = layer_contributions_style[layer_name]
    activation = layer_dict[layer_name].output
    scaling = K.prod(K.cast(K.shape(activation), 'float32'))
    style_layer_output = K.function([model.layers[0].input],
model.get_layer(layer_name).output])
    layer_output_style = style_layer_output([style_img.reshape(1,224,224,3)/255])[0][0]
    loss += style_loss(layer_output_style, activation[0])
在前面的代码中,我们以与计算内容损失相同的方式计算样式损失,但在不同的层上,并使用我们构建的不同自定义函数:style_loss。
- 构建一个函数,将输入图像映射到损失值和相应的梯度值:
dream = model.input
grads = K.gradients(loss, dream)[0]
grads /= K.maximum(K.mean(K.abs(grads)), 1e-7)
outputs = [loss, grads]
fetch_loss_and_grads = K.function([dream], outputs)
def eval_loss_and_grads(img):
      outs = fetch_loss_and_grads([img])
      loss_value = outs[0]
      grad_values = outs[1]
      return loss_value, grad_values
前面的代码以与DeepDream 算法生成图像食谱非常相似的方式获取损失和梯度值。
- 运行模型多个周期:
for i in range(2000):
      step=0.001
      loss_value, grad_values = eval_loss_and_grads(img)
      print('...Loss value at', i, ':', loss_value)
      img -= step * grad_values
      if(i%100 ==0):
            img2 = img.copy().reshape(224,224,3)
            img2 = np.clip(img2*255, 0, 255).astype('uint8')
            plt.imshow(img2)
            plt.axis('off')
            plt.show()
前面的代码生成了一张将内容图像和样式图像相结合的图像:

通过选择不同的层来计算内容和样式损失,并为这些层在各自样式或内容贡献中分配不同的系数权重,最终生成的图像可能会有所不同。
在前面的三个案例研究中,我们看到如何通过改变输入像素值来生成新图像。在本章的其余部分,我们将采用一种不同的生成新图像的方法:使用生成对抗网络(GANs)。
使用生成对抗网络生成数字图像
一个生成对抗网络(GAN)使用一堆神经网络生成一张与原始图像集非常相似的新图像。它在图像生成中有着广泛的应用,并且 GAN 研究领域正在快速进展,旨在生成那些非常难以与真实图像区分的图像。在本节中,我们将理解 GAN 的基础知识——它是如何工作的,以及 GAN 变种之间的差异。
一个 GAN 由两个网络组成:生成器和判别器。生成器尝试生成一张图像,判别器则尝试确定它收到的输入图像是真实的图像还是生成的(假的)图像。
为了进一步理解,假设判别器模型试图将一张图片分类为人脸图像或非人脸图像,数据集中包含了成千上万的人脸图像和非人脸图像。
一旦我们训练模型以区分人脸和非人脸,当我们向模型展示一张新的人脸时,模型仍然会将其分类为人脸,而它会学习将非人脸分类为非人脸。
生成器网络的任务是生成与原始图像集非常相似的图像,以至于判别器会被“欺骗”,认为生成的图像实际上来自原始数据集。
准备工作
我们将采用的生成图像的策略如下:
- 
使用生成器网络生成合成图像,初始步骤是生成一张噪声图像,该图像是通过将一组噪声值重新塑形为我们图像的形状来生成的。 
- 
将生成的图像与原始图像集合连接,并让判别器预测每个图像是生成的图像还是原始图像——这确保了判别器被训练: - 
请注意,判别器网络的权重在这一迭代过程中得到了训练。 
- 
判别器网络的损失是图像的预测值和实际值之间的二元交叉熵。 
- 
生成图像的输出值将是假的(0),而原始图像的值将是真实的(1)。 
 
- 
- 
现在,判别器已经经过了一次迭代训练,接下来训练生成器网络,修改输入噪声,使其看起来更像真实图像而非合成图像——一个有可能欺骗判别器的图像。这个过程包括以下步骤: - 
输入噪声通过生成器网络传递,生成器将输入转化为图像。 
- 
从生成器网络生成的图像会传递到判别器网络——但请注意,在这一迭代中判别器网络的权重是被冻结的,因此它们不会在这一迭代中被训练(因为它们已经在步骤 2 中进行了训练)。 
- 
从判别器得到的生成图像输出值将是真实的(1),因为它的任务是欺骗判别器。 
- 
生成器网络的损失是输入图像的预测与实际值之间的二进制交叉熵(对于所有生成的图像,实际值为 1)——这确保了生成器网络的权重被微调: - 
请注意,在这一步中,判别器网络的权重已被冻结 
- 
冻结判别器可以确保生成器网络从判别器提供的反馈中学习 
 
- 
- 
重复这些步骤多次,直到生成真实的图像。 
 
- 
如何操作...
在对抗性攻击欺骗神经网络部分,我们讨论了如何生成一个与原始图像非常相似的图像的策略。在这一部分,我们将实现从 MNIST 数据集生成数字图像的过程(代码文件可在 GitHub 上的Vanilla_and_DC_GAN.ipynb中找到):
- 导入相关的包:
import numpy as np
from keras.datasets import mnist
from keras.layers import Input, Dense, Reshape, Flatten, Dropout
from keras.layers import BatchNormalization
from keras.layers.advanced_activations import LeakyReLU
from keras.models import Sequential
from keras.optimizers import Adam
import matplotlib.pyplot as plt
%matplotlib inline
plt.switch_backend('agg')
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Reshape
from keras.layers.core import Activation
from keras.layers.normalization import BatchNormalization
from keras.layers.convolutional import UpSampling2D
from keras.layers.convolutional import Conv2D, MaxPooling2D
from keras.layers.core import Flatten
from keras.optimizers import SGD
from keras.datasets import mnist
import numpy as np
from PIL import Image
import argparse
import math
- 定义参数:
shape = (28, 28, 1)
epochs = 400
batch = 32
save_interval = 100
- 定义生成器和判别器网络:
def generator():
    model = Sequential()
    model.add(Dense(256, input_shape=(100,)))
    model.add(LeakyReLU(alpha=0.2))
    model.add(BatchNormalization(momentum=0.8))
    model.add(Dense(512))
    model.add(LeakyReLU(alpha=0.2))
    model.add(BatchNormalization(momentum=0.8))
    model.add(Dense(1024))
    model.add(LeakyReLU(alpha=0.2))
    model.add(BatchNormalization(momentum=0.8))
    model.add(Dense(28 * 28 * 1, activation='tanh'))
    model.add(Reshape(shape))
    return model
对于生成器,我们构建了一个模型,它接收一个形状为 100 维的噪声向量,并将其转换为一个形状为 28 x 28 x 1 的图像。注意,我们在模型中使用了LeakyReLU激活函数。生成器网络的摘要如下:

在以下代码中,我们构建了一个判别器模型,其中我们输入一个形状为 28 x 28 x 1 的图像,并输出一个值为 1 或 0 的结果,表示输入图像是原始图像还是伪造图像:
def discriminator():
     model = Sequential()
     model.add(Flatten(input_shape=shape))
     model.add(Dense((28 * 28 * 1), input_shape=shape))
     model.add(LeakyReLU(alpha=0.2))
     model.add(Dense(int((28 * 28 * 1) / 2)))
     model.add(LeakyReLU(alpha=0.2))
     model.add(Dense(1, activation='sigmoid'))
     return model
判别器网络的摘要如下:

编译生成器和判别器模型:
Generator = generator()
Generator.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5, decay=8e-8))
Discriminator = discriminator()
Discriminator.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5, decay=8e-8),metrics=['accuracy'])
- 定义堆叠的生成器判别器模型,帮助优化生成器的权重,同时冻结判别器网络的权重。堆叠的生成器判别器接受我们传入模型的随机噪声作为输入,并使用生成器网络将噪声转换为一个 28 x 28 的图像。此外,它还判断这个 28 x 28 的图像是真实的还是伪造的:
def stacked_generator_discriminator(D, G):
    D.trainable = False
    model = Sequential()
    model.add(G)
    model.add(D)
    return model
stacked_generator_discriminator = stacked_generator_discriminator(Discriminator, Generator)
stacked_generator_discriminator.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5, decay=8e-8))
- 定义一个函数来绘制生成的图像:
def plot_images(samples=16, step=0):
    noise = np.random.normal(0, 1, (samples, 100))
    images = Generator.predict(noise)
    plt.figure(figsize=(10, 10))
    for i in range(images.shape[0]):
        plt.subplot(4, 4, i + 1)
        image = images[i, :, :, :]
        image = np.reshape(image, [28, 28])
        plt.imshow(image, cmap='gray')
        plt.axis('off')
    plt.tight_layout()
    plt.show()
- 提供输入图像:
(X_train, _), (_, _) = mnist.load_data()
X_train = (X_train.astype(np.float32) - 127.5) / 127.5
X_train = np.expand_dims(X_train, axis=3)
我们丢弃了y_train数据集,因为我们不需要输出标签,模型是基于给定的图像集合(即X_train)生成新图像的。
- 通过多次训练周期优化图像:
在以下代码中,我们正在获取真实图像(legit_images)并生成假图像(synthetic_images)数据,我们将尝试通过修改噪声数据(gen_noise)作为输入,将其转换为逼真的图像,如下所示:
for cnt in range(4000):
      random_index = np.random.randint(0, len(X_train) - batch / 2)
      legit_images = X_train[random_index: random_index + batch // 2].reshape(batch // 2, 28, 28, 1)
      gen_noise = np.random.normal(-1, 1, (batch // 2, 100))/2
      synthetic_images = Generator.predict(gen_noise)
在以下代码中,我们正在训练判别器(使用train_on_batch方法),其中真实图像应输出 1,而假图像应输出 0:
x_combined_batch = np.concatenate((legit_images, synthetic_images))
y_combined_batch = np.concatenate((np.ones((batch // 2, 1)), np.zeros((batch // 2, 1))))
d_loss = Discriminator.train_on_batch(x_combined_batch, y_combined_batch)
在以下代码中,我们正在准备一组新的数据,其中noise是输入,y_mislabeled是输出,用于训练生成器(请注意,输出与我们训练判别器时的输出正好相反):
noise = np.random.normal(-1, 1, (batch, 100))/2
y_mislabled = np.ones((batch, 1))
在以下代码中,我们正在训练生成器和判别器的堆叠组合,其中判别器的权重被冻结,而生成器的权重会更新,以最小化损失值。生成器的任务是生成能够欺骗判别器输出 1 的图像:
g_loss = stacked_generator_discriminator.train_on_batch(noise, y_mislabled)
在以下代码中,我们观察生成器损失和判别器损失在不同周期的输出:
logger.info('epoch: {}, [Discriminator: {}], [Generator: {}]'.format(cnt, d_loss[0], g_loss))
    if cnt % 100 == 0:
          plot_images(step=cnt)

判别器和生成器损失随着周期增加的变化如下:

请注意,前面的输出在生成图像的真实感方面还有很大的改进空间。
还有更多...
我们看到的输出也是模型架构的函数。例如,可以将模型各层的激活函数更改为 tanh,看看生成的输出如何变化,从而大致了解生成图像的样子。
使用深度卷积 GAN 生成图像
在上一部分中,我们研究了使用 Vanilla 生成器和判别器网络生成数字。然而,我们也可以遇到一种情况,即通过使用卷积架构,网络能更好地学习图像中的特征,因为 CNN 中的滤波器会学习图像中的特定细节。深度卷积生成对抗网络(DCGANs)利用这一现象生成新的图像。
如何实现...
虽然 DCGAN 的工作原理与 GAN(我们在上一个示例中使用的模型)非常相似,但主要的区别在于 DCGAN 的生成器和判别器架构,其结构如下(代码文件可以在 GitHub 上找到,文件名为Vanilla_and_DC_GAN.ipynb):
def generator():
    model = Sequential()
    model.add(Dense(input_dim=100, output_dim=1024))
    model.add(Activation('tanh'))
    model.add(Dense(128*7*7))
    model.add(BatchNormalization())
    model.add(Activation('tanh'))
    model.add(Reshape((7, 7, 128), input_shape=(128*7*7,)))
    model.add(UpSampling2D(size=(2, 2)))
    model.add(Conv2D(64, (5, 5), padding='same'))
    model.add(Activation('tanh'))
    model.add(UpSampling2D(size=(2, 2)))
    model.add(Conv2D(1, (5, 5), padding='same'))
    model.add(Activation('tanh'))
    return model
def discriminator():
    model = Sequential()
    model.add(Conv2D(64, (5, 5),padding='same',input_shape=(28, 28, 1)))
    model.add(Activation('tanh'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Conv2D(128, (5, 5)))
    model.add(Activation('tanh'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Flatten())
    model.add(Dense(1024))
    model.add(Activation('tanh'))
    model.add(Dense(1))
    model.add(Activation('sigmoid'))
    return model
请注意,在 DCGAN 中,我们对输入数据执行了多次卷积和池化操作。
如果我们重新执行在 Vanilla GAN(生成对抗网络用于生成图像)示例中执行的完全相同的步骤,但这次使用定义了卷积和池化架构的模型(即 DCGAN),我们将得到以下生成的图像:

随着迭代轮次增加,生成器和判别器的损失值变化如下:

我们可以看到,尽管其他一切保持不变,仅模型架构发生了变化,但通过 DCGAN 生成的图像比 Vanilla GAN 的结果真实得多。
使用深度卷积 GAN 生成面部
到目前为止,我们已经了解了如何生成新图像。在本节中,我们将学习如何从现有的面部数据集中生成一组新的面部图像。
准备工作
我们将在本次练习中采用的方案与我们在 使用深度卷积 GAN 生成图像 处方中的方法非常相似:
- 
收集一个包含多个面部图像的数据集。 
- 
在开始时生成随机图像。 
- 
通过展示包含面部和随机图像的组合来训练判别器,判别器需要区分实际面部图像和生成的面部图像。 
- 
一旦判别器模型训练完成,将其冻结,并调整随机图像,使得判别器现在会给经过调整的随机图像分配更高的属于原始面部图像的概率。 
- 
重复前面两步,进行多次迭代,直到生成器不再继续训练。 
如何实现...
面部生成的代码实现如下(代码文件在 GitHub 上可用,名为 Face_generation.ipynb):
- 下载数据集。建议下载的数据集和相关代码已在 GitHub 上提供。以下是图像示例:

- 定义模型架构:
def generator():
    model = Sequential()
    model.add(Dense(input_dim=100, output_dim=1024))
    model.add(Activation('tanh'))
    model.add(Dense(128*7*7))
    model.add(BatchNormalization())
    model.add(Activation('tanh'))
    model.add(Reshape((7, 7, 128), input_shape=(128*7*7,)))
    model.add(UpSampling2D(size=(2, 2)))
    model.add(Conv2D(64, (5, 5), padding='same'))
    model.add(Activation('tanh'))
    model.add(UpSampling2D(size=(2, 2)))
    model.add(Conv2D(1, (5, 5), padding='same'))
    model.add(Activation('tanh'))
    return model
请注意,上述代码与我们在 深度卷积生成对抗网络 处方中构建的生成器相同:
def discriminator():
    model = Sequential()
    model.add(Conv2D(64, (5, 5),padding='same',input_shape=(28, 28, 1)))
    model.add(Activation('tanh'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Conv2D(128, (5, 5)))
    model.add(Activation('tanh'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Flatten())
    model.add(Dense(1024))
    model.add(Activation('tanh'))
    model.add(Dense(1))
    model.add(Activation('sigmoid'))
    return model
请注意,上述架构与我们在 使用深度卷积 GAN 生成图像 部分中构建的架构相同:
def stacked_generator_discriminator(D, G):
    D.trainable = False
    model = Sequential()
    model.add(G)
    model.add(D)
    return model
- 定义用于加载、预处理和反处理图像的实用函数,并绘制图像:
def plot_images(samples=16, step=0):
    noise = np.random.normal(0, 1, (samples, 100))
    images = deprocess(Generator.predict(noise))
    plt.figure(figsize=(5, 5))
    for i in range(images.shape[0]):
        plt.subplot(4, 4, i + 1)
        image = images[i, :, :, :]
        image = np.reshape(image, [56, 56,3])
        plt.imshow(image, cmap='gray')
        plt.axis('off')
    plt.tight_layout()
    plt.show()
请注意,我们正在将图像调整为较小的形状,以便通过模型调整的参数数量最小化:
def preprocess(x):
    return (x/255)*2-1
def deprocess(x):
    return np.uint8((x+1)/2*255)
- 导入数据集并进行预处理:
from skimage import io
import os
import glob
root_dir = '/content/lfwcrop_color/'
all_img_paths = glob.glob(os.path.join(root_dir, '*/*.ppm'))
在以下代码中,我们正在创建输入数据集并将其转换为数组:
import numpy as np
X_train = []
for i in range(len(all_img_paths)):
  img = cv2.imread(all_img_paths[i])
  X_train.append(preprocess(img))
len(X_train)
X_train = np.array(X_train)
- 编译生成器、判别器和堆叠的生成器-判别器模型:
Generator = generator()
Generator.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5, decay=8e-8))
Discriminator = discriminator()
Discriminator.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5, decay=8e-8),metrics=['accuracy'])
stacked_generator_discriminator = stacked_generator_discriminator(Discriminator, Generator)
stacked_generator_discriminator.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5, decay=8e-8))
- 以类似于我们在 深度卷积生成对抗网络 处方中使用的方式,运行模型多轮迭代:
%matplotlib inline
$pip install logger
from logger import logger
for cnt in range(10000):
      random_index = np.random.randint(0, len(X_train) - batch / 2)
      legit_images = X_train[random_index: random_index + batch // 2].reshape(batch // 2, 56, 56, 3)
      gen_noise = np.random.normal(0, 1, (batch // 2, 100))
      syntetic_images = Generator.predict(gen_noise)
      x_combined_batch = np.concatenate((legit_images, syntetic_images))
      y_combined_batch = np.concatenate((np.ones((batch // 2, 1)), np.zeros((batch // 2, 1))))
      d_loss = Discriminator.train_on_batch(x_combined_batch, y_combined_batch)
      noise = np.random.normal(0, 1, (batch*2, 100))
      y_mislabled = np.ones((batch*2, 1))
      g_loss = stacked_generator_discriminator.train_on_batch(noise, y_mislabled)
      logger.info('epoch: {}, [Discriminator: {}], [Generator: {}]'.format(cnt, d_loss[0], g_loss))
      if cnt % 100 == 0:
          plot_images(step=cnt)
上述代码生成的图像如下所示:

请注意,尽管这些图像看起来非常模糊,但这张图片是原始的,不存在于原始数据集中。通过改变模型架构并增加更深的层次,这个输出还有很大的提升空间。
随着训练轮数增加,判别器和生成器损失值的变化如下所示:

请注意,从前面的图表中,我们可能希望训练模型的轮数少一些,以使生成器的损失值不那么高。
从一张人脸过渡到另一张人脸
现在我们已经能够生成面部图像了,接下来让我们在生成的图像上进行一些向量运算。
在这个练习中,我们将执行从一个人脸到另一个人脸的生成过渡。
准备开始
我们将继续从《使用深度卷积 GAN 进行人脸生成》部分构建的图像生成模型开始。
假设我们希望看到一张生成的人脸图像逐渐过渡到另一张生成的人脸图像。这个过程是通过慢慢改变从第一个向量(第一张生成图像的向量)到第二个向量(第二张生成图像的向量)来实现的。你可以将每个潜在的(向量)维度看作是图像的某个特定方面。
我们将采用的策略如下:
- 
生成两张图像 
- 
在 10 步中将第一张生成图像转换为第二张生成图像 
- 
在第一步中,将第一张生成图像的权重设为 1,第二张生成图像的权重设为 0。 
- 
在第二步中,将第一张生成图像的权重设为 0.9,第二张生成图像的权重设为 0.1。 
- 
重复前面的步骤,直到我们将第一张生成图像的权重设为 0,第二张生成图像的权重设为 1。 
如何执行...
我们将编写在《准备开始》部分中概述的策略,代码如下(代码文件可以在 GitHub 上的Face_generation.ipynb中找到):
- 从随机噪声生成第一张图像(请注意,我们将从《使用深度卷积 GAN 进行人脸生成》部分的第 6 步继续):
gen_noise = np.random.normal(0, 1, (1, 100))
syntetic_images = Generator.predict(gen_noise)
plt.imshow(deprocess(syntetic_images)[0])
plt.axis('off')
生成的图像如下所示:

- 从随机噪声生成第二张图像:
gen_noise2 = np.random.normal(0, 1, (1, 100))
syntetic_images = Generator.predict(gen_noise2)
plt.imshow(deprocess(syntetic_images)[0])
plt.axis('off')
plt.show() 
以下是前面代码片段的输出:

- 生成从第一张图像到第二张图像的可视化:
plt.figure(figsize=(10, 8))
for i in range(10):
  gen_noise3 = gen_noise + (gen_noise2 - gen_noise)*(i+1)/10
  syntetic_images = Generator.predict(gen_noise3)
  plt.subplot(1, 10, i+1)
  plt.imshow(deprocess(syntetic_images)[0])
  plt.axis('off')
我们将获得以下输出:

请注意,在前面的输出中,我们已经慢慢将第一张图像转换成了第二张图像。
在生成图像上执行向量运算
现在我们理解了潜在向量表示在改变生成图像结果中的关键作用,接下来让我们用具有特定人脸对齐的图像进一步构建我们的直觉。
准备开始
我们将采用的向量运算策略如下:
- 
生成三张基于 100 个向量值随机噪声的图像 
- 
确保三张图像中有两张生成的面朝左,并且有一张面朝右。 
- 
计算一个新的向量,它是对齐同一方向的图像之和,再从对齐在相反方向的图像中减去该向量。 
- 
从上一步骤中获得的结果向量生成图像 
如何操作...
我们将按照以下策略进行编程(代码文件在 GitHub 上可作为 Face_generation.ipynb 获取)。注意,我们将从 使用深度卷积 GAN 生成面孔 部分的第 6 步继续:
- 生成三个向量(确保两幅图像对齐在一个方向上,另一幅图像则通过改变生成的噪声与之对立方向对齐):
gen_noise = np.random.normal(0, 1, (1, 100))
gen_noise2 = np.random.normal(0, 1, (1, 100))
gen_noise3 = np.random.normal(0, 1, (1, 100))
syntetic_images = Generator.predict(gen_noise4)
plt.imshow(deprocess(syntetic_images)[0])
plt.axis('off')
plt.show()
- 绘制生成的图像:
plt.subplot(131)
syntetic_images = Generator.predict(gen_noise)
plt.imshow(deprocess(syntetic_images)[0])
plt.axis('off')
plt.title('Image 1')
plt.subplot(132)
syntetic_images = Generator.predict(gen_noise2)
plt.imshow(deprocess(syntetic_images)[0])
plt.axis('off')
plt.title('Image 2')
plt.subplot(133)
syntetic_images = Generator.predict(gen_noise3)
plt.imshow(deprocess(syntetic_images)[0])
plt.axis('off')
plt.title('Image 3')
三个生成的图像如下:

我们可以看到图像 2 和 3 的人脸朝向右侧,而图像 1 的人脸正面朝前。
- 对这些图像的每一个向量表示进行向量运算,以查看结果:
gen_noise4 = gen_noise + gen_noise2 - gen_noise3
syntetic_images = Generator.predict(gen_noise4)
plt.imshow(deprocess(syntetic_images)[0])
plt.axis('off')
plt.show()  
上述代码生成了如下的面孔:

上述运算显示,向量运算(图像 1 + 图像 2 - 图像 3 的向量)生成的图像使得面孔朝前,从而增强了我们对潜在向量表示工作原理的直觉。
还有更多...
我们仅仅触及了 GAN 的基础;目前有多种基于 GAN 的技术正在变得流行。我们将讨论其中一些技术的应用:
- 
pix2pix:想象一个场景,你涂鸦(草图)一个物体的结构,然后这个物体以多种形式呈现。pix2pix 是一种帮助实现这一点的算法。 
- 
Cycle GAN:想象一个场景,你希望一个物体看起来像完全不同的物体(例如,你希望一个马的物体看起来像一只斑马,反之亦然)。你还希望确保图像的其他所有部分保持不变,只有物体发生变化。在这种情况下,Cycle GAN 很有用。 
- 
BigGAN 是最近的一项发展,它生成的图像看起来极为真实。 
第九章:编码输入
在本章中,我们将涵盖以下内容:
- 
编码的需求 
- 
编码图像 
- 
用于推荐系统的编码 
引言
一幅典型的图像由数千个像素组成;文本也由数千个独特单词组成,而公司的独特客户数量可能达到百万级。考虑到这一点,用户、文本和图像三者都必须表示为数千个维度平面中的向量。在这样一个高维空间中表示向量的缺点在于,我们将无法有效计算向量之间的相似性。
表示图像、文本或用户在较低维度中有助于我们将非常相似的实体分组。编码是执行无监督学习的一种方法,以最小信息损失的方式将输入表示为较低维度,同时保留与相似图像有关的信息。
在本章中,我们将学习以下内容:
- 
将图像编码到更低维度 - 
香草自编码器 
- 
多层自编码器 
- 
卷积自编码器 
 
- 
- 
可视化编码 
- 
在推荐系统中编码用户和项目 
- 
计算编码实体之间的相似性 
编码的需求
编码通常用于向量维度巨大的情况。编码有助于将大向量转换为具有较少维度的向量,同时不会从原始向量中丢失太多信息。在接下来的几节中,让我们探讨编码图像、文本和推荐系统的需求。
文本分析中的编码需求
要了解文本分析中编码的必要性,让我们考虑以下情景。让我们看看以下两个句子:

在传统的文本分析中,前两个句子被独热编码如下:

请注意,这两个句子中有五个唯一单词。
单词的独热编码版本导致句子的编码版本如下:

在上述情景中,我们可以看到两个句子之间的欧几里德距离大于零,因为like和enjoy的编码是不同的。然而,直观上,我们知道 like 和 enjoy 这两个词非常相似。此外,I和Chess之间的距离与like和enjoy之间的距离相同。
请注意,鉴于这两个句子中有五个唯一单词,我们将每个单词表示为五维空间中的一个单词。在编码版本中,我们以较低维度(比如三维)表示一个单词,以使相似的单词之间的距离较小,而不是相似的单词之间的距离较大。
图像分析中编码的需求
为了理解图像分析中对编码的需求,我们来考虑一个场景:我们对图像进行分组,但图像的标签并不存在。为了进一步澄清,我们来看一下 MNIST 数据集中相同标签的以下图像:

直观地,我们知道前面这两张图片对应的是相同的标签。然而,当我们计算这两张图片之间的欧几里得距离时,距离大于零,因为这两张图片中突出显示的像素不同。
你应该注意到在存储图像信息时存在以下问题:
尽管图像由总共 28 x 28 = 784 个像素组成,但大部分列是黑色的,因此这些列没有包含信息,导致它们在存储信息时占用了比实际需要更多的空间。
使用自动编码器,我们将前面的两张图片表示为较低维度,这样编码版本之间的距离会变得更小,同时确保编码版本不会丢失太多原始图像的信息。
推荐系统中对编码的需求
为了理解推荐系统中对编码的需求,我们来考虑顾客电影推荐的场景。类似于文本分析,如果我们对每部电影/顾客进行独热编码,我们将为每部电影(因为电影数量成千上万)得到多个千维的向量。基于顾客的观看习惯,将用户编码到更低的维度,并根据电影的相似性对电影进行分组,这样可以帮助我们推荐顾客更可能观看的电影。
类似的概念也可以应用于电子商务推荐引擎,以及在超市中向顾客推荐商品。
编码一张图片
图像编码可以通过多种方式进行。在接下来的章节中,我们将对比普通自动编码器、多层自动编码器和卷积自动编码器的性能。自动编码一词指的是以一种方式进行编码,使得原始输入可以在图像中用更少的维度重建。
自动编码器将图像作为输入,并将输入图像编码为较低的维度,这样我们就可以仅使用输入图像的编码版本来重建原始图像。本质上,你可以认为相似图像的编码版本具有相似的编码值。
准备工作
在我们定义策略之前,让我们先了解一下自动编码器是如何工作的:
- 
我们将定义一个包含 11 个值的玩具数据集 
- 
我们将把这 11 个值表示为较低维度(二维): - 
在降低维度的同时,尽可能保留输入数据中存在的信息 
- 
低维空间中的向量称为嵌入/编码 向量、瓶颈 特征/向量,或者是压缩表示 
- 
通过将输入值与一个维度为 11 x 2 的随机权重矩阵进行矩阵乘法,11 个值被转换为两个值。 
- 
较低维度的向量表示瓶颈特征。瓶颈特征是重建原始图像所需的特征。 
 
- 
- 
我们将重建较低维度的瓶颈特征向量,以获得输出向量: - 二维特征向量与一个形状为 2 x 11 的矩阵相乘,得到一个形状为 1 x 11 的输出。1 x 2 与 2 x 11 向量的矩阵乘法将得到一个 1 x 11 形状的输出。
 
- 
我们将计算输入向量和输出向量之间的平方差之和: 
- 
我们通过调整随机初始化的权重向量来最小化输入和输出向量之间的平方差之和。 
- 
结果编码向量将是一个低维度的向量,表示二维空间中的 11 维向量。 
在利用神经网络时,您可以将编码向量视为连接输入层和输出层的隐藏层。
此外,对于神经网络,输入层和输出层的值是完全相同的,隐藏层的维度低于输入层。
在本教程中,我们将了解多种自编码器:
- 
Vanilla 自编码器 
- 
多层自编码器 
- 
卷积自编码器 
如何做到...
在接下来的部分,我们将实现多种自编码器的变种(代码文件可在 GitHub 上的Auto_encoder.ipynb中找到)。
Vanilla 自编码器
一个 Vanilla 自编码器长得如下:

如前图所示,Vanilla 自编码器使用最少的隐藏层和隐藏单元来重建输入数据。
为了理解 Vanilla 自编码器如何工作,让我们按照以下步骤操作,其中我们使用原始图像的低维编码版本来重建 MNIST 图像(代码文件可在 GitHub 上的Auto_encoder.ipynb中找到):
- 导入相关包:
import tensorflow as tf
import keras
import numpy as np
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import Flatten
from keras.layers.convolutional import Conv2D
from keras.layers.convolutional import MaxPooling2D
from keras.utils import np_utils
- 导入数据集:
(X_train, y_train), (X_test, y_test) = keras.datasets.mnist.load_data()
- 重塑并缩放数据集:
X_train = X_train.reshape(X_train.shape[0],X_train.shape[1]*X_train.shape[2])
X_test = X_test.reshape(X_test.shape[0],X_test.shape[1]*X_test.shape[2])
X_train = X_train/255
X_test = X_test/255
- 构建网络架构:
model = Sequential()
model.add(Dense(32, input_dim=784, activation='relu'))
model.add(Dense(784, activation='relu'))
model.summary()
模型的摘要如下:

在前面的代码中,我们将一个 784 维的输入表示为一个 32 维的编码版本。
- 编译并拟合模型:
model.compile(loss='mean_squared_error', optimizer='adam',metrics=['accuracy'])
model.fit(X_train, X_train, validation_data=(X_test, X_test),epochs=10, batch_size=1024, verbose=1)
请注意,我们使用均方误差损失函数,因为像素值是连续的。此外,输入和输出数组是相同的——X_train。
- 打印前四个输入图像的重建结果:
import matplotlib.pyplot as plt
%matplotlib inline
plt.subplot(221)
plt.imshow(model.predict(X_test[0,:].reshape(1,784)).reshape(28,28), cmap=plt.get_cmap('gray'))
plt.axis('off')
plt.subplot(222)
plt.imshow(model.predict(X_test[1,:].reshape(1,784)).reshape(28,28), cmap=plt.get_cmap('gray'))
plt.axis('off')
plt.subplot(223)
plt.imshow(model.predict(X_test[2,:].reshape(1,784)).reshape(28,28), cmap=plt.get_cmap('gray'))
plt.axis('off')
plt.subplot(224)
plt.imshow(model.predict(X_test[3,:].reshape(1,784)).reshape(28,28), cmap=plt.get_cmap('gray'))
plt.axis('off')
plt.show()
重建后的 MNIST 数字如下:

为了了解自编码器的效果如何,我们来比较一下之前的预测和原始输入图像:
原始的 MNIST 数字如下:

从前面的图像中,我们可以看到,重建的图像与原始输入图像相比有些模糊。
为了避免模糊问题,我们来构建更深的多层自编码器(从而产生更多的参数),这样可以更好地表示原始图像。
多层自编码器
多层自编码器如下所示,其中有更多的隐藏层将输入层与输出层连接起来:

本质上,多层自编码器通过更多的隐藏层来重建输入。
为了构建多层自编码器,我们将重复前一节中的相同步骤,直到步骤 3。然而,步骤 4,即定义网络架构的部分,将被修改为包含多层,如下所示:
model = Sequential()
model.add(Dense(100, input_dim=784, activation='relu'))
model.add(Dense(32,activation='relu'))
model.add(Dense(100,activation='relu'))
model.add(Dense(784, activation='relu'))
model.summary()
模型的摘要如下:

在上述网络中,我们的第一个隐藏层有 100 个单元,第二个隐藏层(即图像的嵌入版本)是 32 维的,第三个隐藏层是 100 维的。
一旦网络架构定义完成,我们就可以编译并运行它,步骤如下:
model.compile(loss='mean_squared_error', optimizer='adam')
model.fit(X_train, X_train, validation_data=(X_test, X_test),epochs=25, batch_size=1024, verbose=1)
上述模型的预测结果如下:

请注意,与原始图像相比,之前的预测结果仍然有些模糊。
卷积自编码器
到目前为止,我们已经探讨了传统和多层自编码器。在本节中,我们将看到卷积自编码器如何从低维向量中重建原始图像。
卷积自编码器如下所示:

本质上,卷积自编码器通过更多的隐藏层来重建输入,其中隐藏层包括卷积、池化以及对下采样图像的上采样。
类似于多层自编码器,卷积自编码器与其他类型的自编码器在模型架构上有所不同。在以下代码中,我们将定义卷积自编码器的模型架构,而其他步骤与传统自编码器保持一致,直到步骤 3。
X_train 和 X_test 形状之间唯一的区别如下所示:
(X_train, y_train), (X_test, y_test) = keras.datasets.mnist.load_data()
X_train = X_train.reshape(X_train.shape[0],X_train.shape[1],X_train.shape[2],1)
X_test = X_test.reshape(X_test.shape[0],X_test.shape[1],X_test.shape[2],1)
X_train = X_train/255
X_test = X_test/255
请注意,在前面的步骤中,我们正在重塑图像,以便将其传递给 conv2D 方法:
- 定义模型架构:
model = Sequential()
model.add(Conv2D(32, (3,3), input_shape=(28, 28,1), activation='relu',padding='same',name='conv1'))
model.add(MaxPooling2D(pool_size=(2, 2),name='pool1'))
model.add(Conv2D(16, (3,3), activation='relu',padding='same',name='conv2'))
model.add(MaxPooling2D(pool_size=(2, 2),name='pool2'))
model.add(Conv2D(8, (3,3), activation='relu',padding='same',name='conv3'))
model.add(MaxPooling2D(pool_size=(2, 2),name='pool3'))
model.add(Conv2D(32, (3,3), activation='relu',padding='same',name='conv4'))
model.add(MaxPooling2D(pool_size=(2, 2),name='pool4'))
model.add(Flatten(name='flatten'))
model.add(Reshape((1,1,32)))
model.add(Conv2DTranspose(8, kernel_size = (3,3), activation='relu'))
model.add(Conv2DTranspose(16, kernel_size = (5,5), activation='relu'))
model.add(Conv2DTranspose(32, kernel_size = (8,8), activation='relu'))
model.add(Conv2DTranspose(32, kernel_size = (15,15), activation='relu'))
model.add(Conv2D(1, (3, 3), activation='relu',padding='same'))
model.summary()
在前面的代码中,我们定义了一个卷积架构,将输入图像重塑为具有 32 维嵌入版本的图像,该嵌入版本位于架构的中间,最后进行上采样,从而使我们能够重建该图像。
模型的摘要如下:

- 编译并拟合模型
from keras.optimizers import Adam
adam = Adam(lr=0.001)
model.compile(loss='mean_squared_error', optimizer='adam')
model.fit(X_train, X_train, validation_data=(X_test, X_test),epochs=10, batch_size=1024, verbose=1)
一旦我们对前四个测试数据点进行预测,重建后的图像如下所示:

请注意,当前的重构效果略优于之前使用 Vanilla 和多层自编码器对测试图像进行的两个重构。
将相似的图像分组
在前面的章节中,我们将每个图像表示为较低维度的向量,直觉是相似的图像会有相似的嵌入,而不相似的图像则会有不同的嵌入。然而,我们还没有考察相似度度量,也没有详细检查嵌入。
在本节中,我们将尝试在二维空间中绘制嵌入。我们可以使用一种叫做t-SNE的技术,将 32 维向量降维到二维空间。(更多关于 t-SNE 的内容可以参考这里:www.jmlr.org/papers/v9/vandermaaten08a.html。)
通过这种方式,我们的直觉——相似的图像会有相似的嵌入——可以得到验证,因为相似的图像应该聚类在二维平面上。
在以下代码中,我们将所有测试图像的嵌入表示为二维平面:
- 提取测试集中每个 10,000 张图像的 32 维向量:
from keras.models import Model
layer_name = 'flatten'
intermediate_layer_model = Model(inputs=model.input,outputs=model.get_layer(layer_name).output)
intermediate_output = intermediate_layer_model.predict(X_test)
- 执行 t-SNE 生成二维向量:
from sklearn.manifold import TSNE
tsne_model = TSNE(n_components=2, verbose=1, random_state=0)
tsne_img_label = tsne_model.fit_transform(intermediate_output)
tsne_df = pd.DataFrame(tsne_img_label, columns=['x', 'y'])
tsne_df['image_label'] = y_test
- 绘制测试图像嵌入的 t-SNE 维度可视化:
from ggplot import *
chart = ggplot(tsne_df, aes(x='x', y='y', color='factor(image_label)'))+ geom_point(size=70,alpha=0.5)
chart
二维空间中嵌入的可视化如下所示:

请注意,在上面的图表中,我们可以看到,相同标签的图像通常会形成簇。
推荐系统的编码
到目前为止,在前面的章节中,我们对图像进行了编码。在本节中,我们将对电影相关数据集中的用户和电影进行编码。原因在于,可能会有数百万个用户和成千上万部电影在目录中。因此,我们不能直接对这些数据进行独热编码。在这种情况下,编码就显得尤为重要。矩阵分解是推荐系统中常用的编码技术之一。在下一节中,我们将理解它的工作原理,并为用户和电影生成嵌入。
准备工作
编码用户和电影的思路如下:
如果两个用户在喜欢某些电影方面相似,那么表示这两个用户的向量应该相似。类似地,如果两部电影相似(可能属于同一类型或有相同的演员阵容),它们的向量应该相似。
我们将采用的电影编码策略,目的是根据用户观看过的历史电影推荐一组新的电影,具体如下:
- 
导入包含用户信息及他们给不同电影打分的数据集 
- 
为用户和电影分配 ID 
- 
将用户和电影转换为 32 维向量 
- 
使用 Keras 的功能 API 执行电影和用户的 32 维向量的点积: - 
如果有 100,000 个用户和 1,000 部电影,则电影矩阵将是 1,000 x 32 维,而用户矩阵将是 100,000 x 32 维。 
- 
两者的点积将是 100,000 x 1,000 的维度。 
 
- 
- 
将输出展平并通过一个密集层,然后连接到输出层,输出层具有线性激活,输出值的范围从 1 到 5。 
- 
训练模型 
- 
提取电影的嵌入权重 
- 
提取用户的嵌入权重 
- 
可以通过计算目标电影与数据集中其他所有电影的成对相似度,找到与给定电影相似的电影。 
如何实现...
在下面的代码中,我们将为一个用户和一部电影设计一个向量,这在典型的推荐系统中使用(代码文件可以在 GitHub 的Recommender_systems.ipynb中找到):
- 导入数据集。推荐的数据集可以在 GitHub 上的代码中找到。
import numpy as np
import pandas as pd
from keras.layers import Input, Embedding, Dense, Dropout, merge, Flatten, dot
from keras.models import Model
from keras.optimizers import Adam
ratings = pd.read_csv('...') # Path to the user-movie-ratings file

- 将用户和电影转换为分类变量。在下面的代码中,我们创建了两个新的变量——User2和Movies2——它们是分类变量:
ratings['User2']=ratings['User'].astype('category')
ratings['Movies2']=ratings['Movies'].astype('category')
- 为每个用户和电影分配一个唯一的 ID:
users = ratings.User.unique()
movies = ratings.Movies.unique()
userid2idx = {o:i for i,o in enumerate(users)}
moviesid2idx = {o:i for i,o in enumerate(movies)}
idx2userid = {i:o for i,o in enumerate(users)}
idx2moviesid = {i:o for i,o in enumerate(movies)}
- 将唯一的 ID 作为新列添加到原始表中:
ratings['Movies2'] = ratings.Movies.apply(lambda x: moviesid2idx[x])
ratings['User2'] = ratings.User.apply(lambda x: userid2idx[x])
- 为每个用户 ID 和唯一 ID 定义嵌入:
n_users = ratings.User.nunique()
n_movies = ratings.Movies.nunique()
在前面的代码中,我们正在提取数据集中唯一用户和唯一电影的总数:
def embedding_input(name,n_in,n_out):
  inp = Input(shape=(1,),dtype='int64',name=name)
  return inp, Embedding(n_in,n_out,input_length=1)(inp)
在前面的代码中,我们定义了一个函数,输入一个 ID,将其转换为一个嵌入向量,该向量的维度为n_out,总共有n_in个值:
n_factors = 100
user_in, u = embedding_input('user_in', n_users, n_factors)
article_in, a = embedding_input('article_in', n_movies, n_factors)
在前面的代码中,我们正在为每个唯一用户以及每个唯一电影提取 100 个维度。
- 定义模型:
x = dot([u,a], axes=1)
x=Flatten()(x)
x = Dense(500, activation='relu')(x)
x = Dense(1)(x)
model = Model([user_in,article_in],x)
adam = Adam(lr=0.01)
model.compile(adam,loss='mse')
model.summary()
模型摘要如下:

- 训练模型:
model.fit([ratings.User2,ratings.Movies2], ratings.rating, epochs=50,batch_size=128)
- 提取每个用户或电影的向量:
# Extracting user vectors
model.get_weights()[0]
# Extracting movie vectors
model.get_weights()[1]
正如我们之前所想,类似的电影应该有相似的向量。
通常,在识别嵌入之间的相似性时,我们使用一种称为余弦相似度的度量(如何计算余弦相似度的更多信息将在下一章中介绍)。
对于一个随机选中的电影,其位于第 574^(个)位置,余弦相似度计算如下:
from sklearn.metrics.pairwise import cosine_similarity
np.argmax(cosine_similarity(model.get_weights()[1][574].reshape(1,-1),model.get_weights()[1][:574].reshape(574,100)))
从前面的代码中,我们可以计算出与位于分类电影列中第 574^(个)位置的电影最相似的 ID。
一旦我们查看电影 ID 列表,我们应该会看到与给定电影最相似的电影,直观上它们确实是相似的。
第十章:使用单词向量进行文本分析
在上一章中,我们学习了如何对图像、用户或电影进行编码以用于推荐系统,在这些系统中,相似的物品有相似的向量。在本章中,我们将讨论如何对文本数据进行编码。
你将学习以下主题:
- 
从零开始在 Python 中构建单词向量 
- 
使用 skip-gram 和 CBOW 模型构建单词向量 
- 
使用预训练的单词向量进行向量运算 
- 
创建文档向量 
- 
使用 fastText 构建单词向量 
- 
使用 GloVe 构建单词向量 
- 
使用单词向量构建情感分类 
介绍
在传统的解决与文本相关的问题的方法中,我们会进行一-hot 编码。然而,如果数据集包含成千上万个唯一的单词,那么得到的一-hot 编码向量将有成千上万的维度,这可能会导致计算问题。此外,在这种情况下,相似的单词并不会拥有相似的向量。Word2Vec 是一种方法,帮助我们为相似的单词获得相似的向量。
为了理解 Word2Vec 如何有用,我们来探讨以下问题。
假设我们有两个输入句子:

从直观上看,我们知道enjoy(享受)和like(喜欢)是相似的单词。然而,在传统的文本挖掘中,当我们对这些单词进行一-hot 编码时,输出如下所示:

注意到一-hot 编码会将每个单词分配到一个列中。这样的一-hot 编码的主要问题是,I和enjoy之间的欧几里得距离与enjoy和like之间的欧几里得距离是相同的。
然而,从直观上看,我们知道enjoy和like之间的距离应该比I和enjoy之间的距离要小,因为enjoy和like是彼此相似的。
从零开始在 Python 中构建单词向量
我们将构建单词向量的原理是相关的单词周围会有相似的词。
例如:queen(皇后)和princess(公主)这两个单词会更频繁地拥有与kingdom(王国)相关的词汇在它们周围。从某种程度上说,这些单词的上下文(周围的词汇)是相似的。
准备工作
当我们将周围的单词作为输入,中间的单词作为输出时,我们的数据集(包含两个句子)如下所示:

注意我们是使用中间词作为输出,其余的词作为输入。这个输入和输出的向量化形式如下所示(回想一下我们在第九章的文本分析中编码的必要性部分中,输入编码部分,如何将一个句子转换成向量):

请注意,第一行中输入的向量化形式是{0, 1, 1, 1, 0},因为输入词的索引是{1, 2, 3},输出是{1, 0, 0, 0, 0},因为输出词的索引是{1}。
在这种情况下,我们的隐藏层有三个神经元与之相关联。我们的神经网络将如下所示:

每一层的维度如下:
| 层 | 权重形状 | 备注 | 
|---|---|---|
| 输入层 | 1 x 5 | 每一行与五个权重相乘。 | 
| 隐藏层 | 5 x 3 | 每五个输入权重分别连接到隐藏层中的三个神经元。 | 
| 隐藏层输出 | 1 x 3 | 这是输入和隐藏层的矩阵乘法。 | 
| 从隐藏层到输出层的权重 | 3 x 5 | 三个输出隐藏单元被映射到五个输出列(因为有五个独特的词)。 | 
| 输出层 | 1 x 5 | 这是隐藏层输出与从隐藏层到输出层权重的矩阵乘法。 | 
请注意,在构建词向量时,我们不会对隐藏层进行激活处理。
输出层的值没有限制在特定范围内。因此,我们通过 softmax 函数将其转换为词的概率。此外,我们最小化交叉熵损失,以便在整个网络中得到最优的权重值。现在,给定词的词向量就是当输入是该词的一热编码版本时,隐藏层单元的值(而不是输入句子)。
如何实现...
现在我们知道了如何生成词向量,接下来我们在 GitHub 中编写生成词向量的过程(代码文件为Word_vector_generation.ipynb):
- 定义感兴趣的句子:
docs = ["I enjoy playing TT", "I like playing TT"]
从前面的内容可以预期,enjoy和like的词向量应该相似,因为它们周围的词是完全相同的。
- 现在让我们创建每个句子的独热编码版本:
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(min_df=0, token_pattern=r"\b\w+\b")
vectorizer.fit(docs)
请注意,vectorizer 定义了将文档转换为向量格式的参数。此外,我们传入更多的参数,以确保像I这样的词不会在CountVectorizer中过滤掉。
此外,我们将把文档适配到定义的 vectorizer 中。
- 将文档转换为向量格式:
vector = vectorizer.transform(docs)
- 验证所做的转换:
print(vectorizer.vocabulary_)
print(vector.shape)
print(vector.toarray())

请注意,vocabulary_返回各种词的索引,并且转换后的toarray向量返回句子的独热编码版本。
- 创建输入和输出数据集:
x = []
y = []
for i in range(len(docs)):
     for j in range(len(docs[i].split())):
         t_x = []
         t_y = []
         for k in range(4):
             if(j==k):
                 t_y.append(docs[i].split()[k])
                 continue
             else:
                 t_x.append(docs[i].split()[k])
         x.append(t_x)
         y.append(t_y)
x2 = []
y2 = []
for i in range(len(x)):
     x2.append(' '.join(x[i]))
     y2.append(' '.join(y[i]))
从前面的代码,我们已经创建了输入和输出数据集。这里是输入数据集:

这里是输出数据集:

- 将前述的输入和输出词转换为向量:
vector_x = vectorizer.transform(x2)
vector_x.toarray()
vector_y = vectorizer.transform(y2)
vector_y.toarray()
这里是输入数组:

这里是输出数组:

- 定义一个神经网络模型,该模型通过一个包含三个单元的隐藏层来映射输入和输出向量:
model = Sequential()
model.add(Dense(3, activation='linear', input_shape=(5,)))
model.add(Dense(5,activation='sigmoid'))
- 编译并拟合模型:
model.compile(loss='binary_crossentropy',optimizer='adam')
model.fit(vector_x, vector_y, epochs=1000, batch_size=4,verbose=1)
- 通过提取中间层的值来提取词向量,输入为每个单独词的向量(而不是一个句子):
from keras.models import Model
layer_name = 'dense_5'
intermediate_layer_model = Model(inputs=model.input,outputs=model.get_layer(layer_name).output)
在之前的代码中,我们从我们感兴趣的层提取输出:在我们初始化的模型中,名为dense_5的层。
在下面的代码中,我们在传递词的一次性编码版本作为输入时提取中间层的输出:
for i in range(len(vectorizer.vocabulary_)):
     word = list(vectorizer.vocabulary_.keys())[i]
     word_vec = vectorizer.transform([list(vectorizer.vocabulary_.keys())[i]]).toarray()
     print(word, intermediate_layer_model.predict(word_vec))
单个词的词向量如下:

注意,享受和喜欢之间的相关性比其他词更强,因此它们更好地表示词向量。
由于我们在构建模型时没有指定层名称,因此您运行的模型的名称可能不同。此外,在我们没有明确指定模型名称的情况下,每次初始化模型时,层名称都会发生变化。
测量词向量之间的相似度
词向量之间的相似度可以通过多种度量方法来测量——以下是两种常见的度量方法:
- 
余弦相似度 
- 
欧几里得距离 
两个不同向量,A和B之间的余弦相似度计算如下:

在上一节的例子中,享受和喜欢之间的余弦相似度计算如下:
享受 = (-1.43, -0.94, -2.49)
喜欢 = (-1.43, -0.94, -2.66)
这里是享受和喜欢向量之间的相似度:
(-1.43-1.43 + -0.94-0.94 +-2.49-2.66)/ sqrt((-1.43)² + (-0.94)² + (-2.49)²)* sqrt((-1.43)² + (-0.94)² + (-2.66)²) = 0.99*
两个不同向量,A和B之间的欧几里得距离计算如下:
distance = sqrt(A-B)²
= sqrt((-1.43 - (-1.43))² + (-0.94 - (-0.94))² + (-2.49 - (-2.66))²)
= 0.03
使用 skip-gram 和 CBOW 模型构建词向量
在之前的食谱中,我们构建了一个词向量。在本食谱中,我们将使用gensim库构建 skip-gram 和 CBOW 模型。
准备工作
我们在本例中采用的方法来构建词向量称为连续词袋模型(CBOW)。之所以称为 CBOW,解释如下:
我们以这句话为例:我享受玩 TT。
下面是 CBOW 模型处理此句子的方式:
- 
固定一个大小为 1 的窗口。 - 通过指定窗口大小,我们确定了给定词的左右两边将被考虑的词的数量。
 
- 
给定窗口大小,输入和输出向量将如下所示: 
| 输入词 | 输出词 | 
|---|---|
| {我, 玩} | {享受} | 
| {享受,TT} | {玩} | 
另一种构建词向量的方法是 skip-gram 模型,其中之前的步骤被反转,如下所示:
| 输入词 | 输出词 | 
|---|---|
| {enjoy} | {I, playing} | 
| {playing} | {enjoy, TT} | 
无论是 skip-gram 模型还是 CBOW 模型,得到单词隐藏层值的方法都是相同的,正如我们在前面的部分讨论过的那样。
如何实现:
现在我们理解了单词向量的构建后台工作,让我们使用 skip-gram 和 CBOW 模型来构建单词向量。为了构建模型,我们将使用航空公司情感数据集,其中给出了推文文本以及对应的情感。为了生成词向量,我们将使用gensim包,如下所示(代码文件在 GitHub 上可用,文件名为word2vec.ipynb):
- 安装gensim包:
$pip install gensim
- 导入相关的包:
import gensim
import pandas as pd
- 读取航空公司推文情感数据集,其中包含与航空公司相关的评论(文本)及其相应的情感。数据集可以从d1p17r2m4rzlbo.cloudfront.net/wp-content/uploads/2016/03/Airline-Sentiment-2-w-AA.csv获取:
data=pd.read_csv('https://www.dropbox.com/s/8yq0edd4q908xqw/airline_sentiment.csv')
data.head()                   
数据集样本如下所示:

- 
对前面的文本进行预处理,执行以下操作: - 
将每个单词标准化为小写。 
- 
移除标点符号,仅保留数字和字母。 
- 
移除停用词: 
 
- 
import re
import nltk
from nltk.corpus import stopwords
nltk.download('stopwords')
stop = set(stopwords.words('english'))
def preprocess(text):
    text=text.lower()
    text=re.sub('[⁰-9a-zA-Z]+',' ',text)
    words = text.split()
    words2 = [i for i in words if i not in stop]
    words3=' '.join(words2)
    return(words3)
data['text'] = data['text'].apply(preprocess)
- 将句子拆分为一个词汇表(tokens)的列表,这样它们就可以传递给gensim。第一句的输出应如下所示:
data['text'][0].split()
上面的代码通过空格分割句子,结果如下所示:

我们将遍历所有文本并将其附加到一个列表中,如下所示:
list_words=[]
for i in range(len(data)):
     list_words.append(data['text'][i].split())
让我们查看列表中的前三个子列表:
list_words[:3]
前三句话的词汇表如下所示:

- 构建Word2Vec模型:
from gensim.models import Word2Vec
定义词向量的维度、上下文窗口的大小以及词汇的最小计数要求,作为它有资格拥有词向量的标准,如下所示:
model = Word2Vec(size=100,window=5,min_count=30, sg=0, alpha = 0.025)
在前面的代码中,size表示词向量的大小(维度),window表示考虑的上下文词汇的大小,min_count指定词汇出现的最小频率,sg表示是否使用 skip-gram(当sg=1时)或使用 CBOW(当sg=0时),alpha表示模型的学习率。
一旦模型定义完成,我们将传递我们的列表来构建词汇表,如下所示:
model.build_vocab(list_words)
一旦构建好词汇表,过滤掉在整个语料库中出现次数少于 30 次的词语,剩余的最终词汇如下所示:
model.wv.vocab.keys()
- 通过指定需要考虑的示例(列表)总数以及运行的迭代次数(epochs)来训练模型,如下所示:
model.train(list_words, total_examples=model.corpus_count, epochs=100)
在上述代码中,list_words(单词列表)是输入,total_examples表示要考虑的列表总数,epochs是运行的训练轮数。
或者,你也可以通过在Word2Vec方法中指定iter参数来训练模型,具体如下:
model = Word2Vec(list_words,size=100,window=5,min_count=30, iter = 100)
- 提取给定词(month)的词向量,具体如下:
model['month']
对应"month"这个词的词向量如下:

两个词之间的相似度可以按以下方式计算:
model.similarity('month','year')
0.48
给定词与最相似的词是通过以下方式计算的:
model.most_similar('month')
month这个词最相似的词如下:

请注意,尽管这些相似度看起来较低,有些最相似的词也不直观,但一旦我们在比我们现有的 11,000 条推文数据集更大的数据集上进行训练,结果会更加真实。
在上述场景中,运行模型若干轮后,看看与"month"这个词最相似的词是什么:
model = Word2Vec(size=100,window=5,min_count=30, sg=0)
model.build_vocab(list_words)
model.train(list_words, total_examples=model.corpus_count, epochs=5)
model.most_similar('month')
与"month"最相似的词如下:

我们可以看到,如果训练轮数较少,与month最相似的词不太直观,而当训练轮数较多时,结果更具直观性,特别是因为在训练轮数少的情况下,权重没有完全优化。
通过将sg参数的值设置为1,可以将相同的操作应用于 skip-gram。
使用预训练的词向量进行向量运算
在前一节中,我们看到的一个限制是,句子的数量太少,无法构建一个强健的模型(我们在前一节中看到,month 和 year 之间的相关性大约为 0.4,比较低,因为它们属于相同类型的词)。
为了克服这种情况,我们将使用由 Google 训练的词向量。Google 提供的预训练词向量包括一个包含 3,000,000 个单词和短语的词汇表,这些词向量是在 Google 新闻数据集上的单词上训练的。
如何操作……
- 从 Google News 下载预训练的词向量(代码文件可以在 GitHub 上作为word2vec.ipynb获取):
$wget https://s3.amazonaws.com/dl4j-distribution/GoogleNews-vectors-negative300.bin.gz
解压下载的文件:
$gunzip '/content/GoogleNews-vectors-negative300.bin.gz'
此命令解压bin文件,该文件是模型的保存版本。
- 加载模型:
from gensim.models import KeyedVectors
filename = '/content/GoogleNews-vectors-negative300.bin'
model = KeyedVectors.load_word2vec_format(filename, binary=True)
- 加载与给定词month最相似的词:
model.most_similar('month')
与month最相似的词如下:

- 我们将执行向量运算;也就是说,我们将尝试回答以下类比问题:woman 与 man 的关系,什么与 king 的关系最相似?请查看以下代码:
result = model.most_similar(positive=['woman', 'king'], negative=['man'], topn=1)
print(result)
上述算式的输出结果如下:

在这种情况下,将woman的词向量从man的词向量中减去,并将其加到king的词向量中,从而得到一个最接近queen的词向量。
创建文档向量
为了理解文档向量的存在原因,我们来梳理一下以下的直觉。
单词bank在金融和河流两个语境中都有使用。我们如何识别给定句子或文档中的bank是与河流主题相关,还是与金融主题相关呢?
这个问题可以通过添加文档向量来解决,这与单词向量的生成方式类似,但在此基础上加入了段落 ID 的一热编码版本,如下所示:

在前述场景中,段落 ID 包含了那些仅通过单词无法捕捉到的差异。例如,在句子on the bank of river中,on the bank of是输入,river是输出,单词on, the和of由于是高频词,因此不会对预测产生贡献,而单词bank则使得输出预测变得模糊,可能是河流或是美国。这个特定文档/句子的文档 ID 将帮助识别该文档是与河流相关,还是与金融相关。这个模型称为段落向量的分布式记忆模型(PV-DM)。
例如,如果文档数量为 100,则段落 ID 的一热编码版本将是 100 维的。同样,如果符合最小频率的唯一单词数量为 1,000,则这些单词的一热编码版本将是 1,000 维的。当隐藏层的大小(即单词向量大小)为 300 时,参数的总数将是 100 * 300 + 1,000 * 300 = 330,000。
文档向量将是当所有输入单词的一热编码版本为 0 时的隐藏层值(即,单词的影响被中和,仅考虑文档/段落 ID 的影响)。
类似于输入和输出在 skip-gram 和 CBOW 模型中相互转换的方式,即使是文档向量,输出和输入也可以按照如下方式交换:

这种模型的表示称为段落向量与分布式词袋模型(PVDBOW)。
准备中
我们构建文档向量的策略如下:
- 
对输入句子进行预处理,去除标点符号,并将所有单词小写,同时移除停用词(如and和the等出现频率很高且不对句子提供上下文意义的词)。 
- 
给每个句子标记上其句子 ID。 - 我们为每个句子分配一个 ID。
 
- 
使用 Doc2Vec 方法提取文档 ID 以及单词的向量。 - 在较多的训练周期中训练 Doc2Vec 方法,以便对模型进行训练。
 
如何实现……
现在我们已经理解了文档向量是如何生成的,并且制定了构建文档向量的策略,接下来让我们生成航空公司推文数据集的文档向量(代码文件在 GitHub 上可用,名为word2vec.ipynb):
- 导入相关包:
from gensim.models.doc2vec import Doc2Vec, TaggedDocument
from nltk.tokenize import word_tokenize
- 预处理推文文本:
import re
import nltk
from nltk.corpus import stopwords
nltk.download('stopwords')
stop = set(stopwords.words('english'))
def preprocess(text):
    text=text.lower()
    text=re.sub('[⁰-9a-zA-Z]+',' ',text)
    words = text.split()
    words2 = [i for i in words if i not in stop]
    words3=' '.join(words2)
    return(words3)
data['text'] = data['text'].apply(preprocess)
- 创建一个包含标记文档的字典,其中文档 ID 会与文本(推文)一起生成:
import nltk
nltk.download('punkt')
tagged_data = [TaggedDocument(words=word_tokenize(_d.lower()), tags=[str(i)]) for i, _d in enumerate(data['text'])]
标记文档数据如下所示:

在上面的代码中,我们正在提取句子(文档)中所有组成词汇的列表。
- 按照如下方式初始化一个带有参数的模型:
max_epochs = 100
vec_size = 300
alpha = 0.025
model = Doc2Vec(size=vec_size,
                alpha=alpha,
                min_alpha=0.00025,
                min_count=30,
                dm =1)
在上面的代码片段中,size 表示文档的向量大小,alpha 表示学习率,min_count 表示单词的最小频率,dm = 1 表示 PV-DM
构建词汇表:
model.build_vocab(tagged_data)
- 在标记数据上训练模型,进行多次迭代:
model.train(tagged_data,epochs=100,total_examples=model.corpus_count)
- 训练过程会生成单词和文档/段落 ID 的向量。
词向量可以与上一节中提取的方式相似地获取,如下所示:
model['wife']
可以按如下方式获取文档向量:
model.docvecs[0]
上述代码片段生成了第一个文档的文档向量片段。
- 提取与给定文档 ID 最相似的文档:
similar_doc = model.docvecs.most_similar('457')
print(similar_doc)

在上面的代码片段中,我们正在提取与文档 ID 457 最相似的文档 ID,该 ID 为 827。
让我们来看一下文档 457 和 827 的文本:
data['text'][457]

data['text'][827]

如果我们检查模型的词汇表,我们会看到除了单词 just,所有其他单词都出现在两个句子之间——因此很明显,文档 ID 457 与文档 ID 827 最相似。
使用 fastText 构建词向量
fastText 是由 Facebook 研究团队创建的一个库,用于高效学习词表示和句子分类。
fastText 与 word2vec 的不同之处在于,word2vec 将每个单词视为最小的单位,其向量表示是要查找的,而 fastText 假设一个单词是由字符的 n-gram 组成的;例如,sunny 由 [sun, sunn, sunny],[sunny, unny, nny] 等组成,其中我们看到了原始单词的一个子集,大小为 n,其中 n 可以从 1 到原始单词的长度。
使用 fastText 的另一个原因是,有些单词在 skip-gram 或 CBOW 模型中没有达到最小频率阈值。例如,单词 appended 与 append 可能没有太大区别。然而,如果 append 出现频率较高,并且在新的句子中我们遇到的是 appended 而不是 append,那么我们就无法为 appended 提供向量。在这种情况下,fastText 的 n-gram 考虑非常有用。
实际上,fastText 使用的是 skip-gram/CBOW 模型,但它增强了输入数据集,以便考虑到未见过的单词。
准备就绪
我们将采用的策略是使用 fastText 提取词向量,具体如下:
- 
使用 gensim 库中的 fastText 方法 
- 
预处理输入数据 
- 
将每个输入句子拆分成一个列表的列表 
- 
在输入的列表列表上构建词汇表 
- 
使用之前的输入数据训练模型,进行多次迭代 
- 
计算单词之间的相似度 
如何操作...
在以下代码中,让我们看看如何使用 fastText 生成词向量(代码文件在 GitHub 上的 word2vec.ipynb 中可用):
- 导入相关的包:
from gensim.models.fasttext import FastText
- 预处理并将数据集准备为列表的列表,就像我们为 word2vec 模型所做的那样:
import re
import nltk
from nltk.corpus import stopwords
nltk.download('stopwords')
stop = set(stopwords.words('english'))
def preprocess(text):
    text=text.lower()
    text=re.sub('[⁰-9a-zA-Z]+',' ',text)
    words = text.split()
    words2 = [i for i in words if i not in stop]
    words3=' '.join(words2)
    return(words3)
data['text'] = data['text'].apply(preprocess)
在之前的代码中,我们正在对输入文本进行预处理。接下来,让我们将输入文本转换成一个列表的列表:
list_words=[]
for i in range(len(data)):
     list_words.append(data['text'][i].split())
- 定义模型(指定每个词的向量数)并构建词汇表:
ft_model = FastText(size=100)
ft_model.build_vocab(list_words)
- 训练模型:
ft_model.train(list_words, total_examples=ft_model.corpus_count,epochs=100)
- 检查模型词汇表中不存在的单词的词向量。例如,单词 first存在于词汇表中;然而,单词firstli不在词汇表中。在这种情况下,检查first和firstli的词向量之间的相似度:
ft_model.similarity('first','firstli')
之前代码片段的输出是 0.97,表示这两个单词之间有很高的相关性。
因此,我们可以看到 fastText 词向量帮助我们生成词汇表中不存在的单词的词向量。
前述方法也可以用于修正数据集中可能存在的拼写错误,因为拼写错误的单词通常出现的频率较低,而与之最相似且频率最高的单词更可能是拼写正确的版本。
拼写修正可以通过向量运算来执行,如下所示:
result = ft_model.most_similar(positive=['exprience', 'prmise'], negative=['experience'], topn=1)
print(result)
请注意,在之前的代码中,正向词有拼写错误,而负向词没有。代码的输出是 promise,这意味着它可能会修正我们的拼写错误。
此外,还可以通过以下方式执行:
ft_model.most_similar('exprience', topn=1)
[('experience', 0.9027844071388245)]
然而,请注意,当存在多个拼写错误时,这种方法不起作用。
使用 GloVe 构建词向量
类似于 word2vec 生成词向量的方式,GloVe(即全局词向量表示的缩写)也生成词向量,但采用的是不同的方法。在本节中,我们将探讨 GloVe 的工作原理,然后详细介绍 GloVe 的实现细节。
准备就绪
GloVe 旨在实现两个目标:
- 
创建捕获意义的词向量 
- 
利用全局计数统计而非仅依赖局部信息 
GloVe 通过查看单词的共现矩阵并优化损失函数来学习词向量。GloVe 算法的工作细节可以从以下示例中理解:
让我们考虑一个场景,其中有两个句子,如下所示:
| Sentences | 
|---|
| 这是测试 | 
| 这也是一个 | 
让我们尝试构建一个词共现矩阵。在我们的玩具数据集中有五个唯一单词,从而得到的词共现矩阵如下所示:

在上表中,单词this和is在数据集的两行中共同出现,因此共现值为 2。类似地,单词this和test仅在数据集中出现一次,因此共现值为 1。
但是,在前述矩阵中,我们没有考虑两个单词之间的距离。考虑两个单词之间距离的直觉是,共同出现的单词之间的距离越远,它们的相关性可能越低。
我们将引入一个新的度量——偏移量,它惩罚给定单词与共现单词之间的高距离。例如,在第一句中,test与this的距离为 2,因此我们将共现数除以 2。
转换后的共现矩阵现在如下所示:
| this | is | test | also | a | |
|---|---|---|---|---|---|
| this | 0 | 2 | 0.5 | 0.5 | 0.33 | 
| is | 0 | 0 | 1 | 1 | 0.5 | 
| test | 0 | 0 | 0 | 0 | 0 | 
| also | 0 | 0 | 0 | 0 | 1 | 
| a | 0 | 0 | 0 | 0 | 0 | 
现在我们已经构建了矩阵,让我们引入一个额外的参数:单词的上下文。例如,如果窗口大小为 2,则单词this和a之间的共现值将为 0,因为两个单词之间的距离大于 2。当上下文窗口大小为 2 时,转换后的共现矩阵如下所示:
| this | is | test | also | a | |
|---|---|---|---|---|---|
| this | 0 | 2 | 0.5 | 0.5 | 0 | 
| is | 0 | 0 | 1 | 1 | 0.5 | 
| test | 0 | 0 | 0 | 0 | 0 | 
| also | 0 | 0 | 0 | 0 | 1 | 
| a | 0 | 0 | 0 | 0 | 0 | 
现在我们得到了一个修改后的共现矩阵,我们随机初始化了每个单词的词向量,此示例中每个单词的维度为 2。每个单词的随机初始化权重和偏置值如下所示:
| Word | Weights 1 | Weights 2 | Weights 3 | Bias | 
|---|---|---|---|---|
| this | -0.64 | 0.82 | -0.08 | 0.16 | 
| is | -0.89 | -0.31 | 0.79 | -0.34 | 
| test | -0.01 | 0.14 | 0.82 | -0.35 | 
| also | -0.1 | -0.67 | 0.89 | 0.26 | 
| a | -0.1 | -0.84 | 0.35 | 0.36 | 
由于前面的权重和偏置是随机初始化的,我们修改权重以优化损失函数。为了做到这一点,我们定义感兴趣的损失函数如下:

在前面的公式中,w[i]表示第i个单词的词向量,w[j]表示第j个单词的词向量;b[i]和b[j]分别是与第i个和第j个单词对应的偏置;X[ij]表示我们之前定义的最终共现值。
例如,当i是单词this,j是单词also时,X[ij]的值为 0.5。
当X[ij]的值为 0 时,f(x[ij])的值为 0;否则,计算方式如下:

在前面的公式中,alpha 通过经验发现为 0.75,x[max]为 100,x是x[ij]的值。
现在公式已经定义,让我们将其应用到我们的矩阵中,如下所示:

第一张表表示单词共现矩阵和随机初始化的权重和偏置。
第二张表表示损失值计算,我们在其中计算整体加权损失值。
我们优化权重和偏置,直到整体加权损失值最小。
如何操作...
既然我们已经知道了如何使用 GloVe 生成词向量,那么我们可以在 Python 中实现相同的功能(代码文件可以在 GitHub 上的word2vec.ipynb中找到):
- 安装 GloVe:
$pip install glove_python
- 导入相关包:
from glove import Corpus, Glove
- 按照我们在 word2vec、skip-gram 和 CBOW 算法中预处理数据集的方式进行预处理,如下所示:
import re
import nltk
from nltk.corpus import stopwords
import pandas as pd
nltk.download('stopwords')
stop = set(stopwords.words('english'))
data = pd.read_csv('https://www.dropbox.com/s/8yq0edd4q908xqw/airline_sentiment.csv?dl=1')
def preprocess(text):
    text=text.lower()
    text=re.sub('[⁰-9a-zA-Z]+',' ',text)
    words = text.split()
    words2 = [i for i in words if i not in stop]
    words3=' '.join(words2)
    return(words3)
data['text'] = data['text'].apply(preprocess)
list_words=[]
for i in range(len(data)):
      list_words.append(data['text'][i].split())
- 创建一个语料库并为其配备词汇表:
corpus.fit(list_words, window=5)
语料库的字典可以通过以下方式找到:
corpus.dictionary
得到唯一单词及其对应的单词 ID 如下:

前面的截图表示单词的关键值及其对应的索引。
以下代码片段给出了共现矩阵:
corpus.matrix.todense()

- 让我们定义模型参数,即向量的维度数量、学习率和要运行的 epoch 数,如下所示:
glove = Glove(no_components=100, learning_rate=0.025)
glove.fit(corpus.matrix, epochs=30, no_threads=4, verbose=True)
- 模型拟合完成后,可以通过以下方式找到词向量的权重和偏置:
glove.word_biases.tolist()
glove.word_vectors.tolist()
- 给定单词的词向量可以通过以下方式确定:
glove.word_vectors[glove.dictionary['united']]
- 给定单词的最相似单词可以通过以下方式确定:
glove.most_similar('united')
最相似的单词与“united”比较,输出如下:

请注意,与united最相似的单词是属于其他航空公司的单词。
使用词向量构建情感分类
在前面的章节中,我们学习了如何使用多个模型生成词向量。在本节中,我们将学习如何构建一个情感分类器来处理给定句子。我们将继续使用航空公司情感推文数据集进行此练习。
如何操作...
生成词向量,方法与我们在前面的例子中提取的方式相同(代码文件可以在 GitHub 上的word2vec.ipynb找到):
- 导入包并下载数据集:
import re
import nltk
from nltk.corpus import stopwords
import pandas as pd
nltk.download('stopwords')
stop = set(stopwords.words('english'))
data=pd.read_csv('https://www.dropbox.com/s/8yq0edd4q908xqw/airline_sentiment.csv?dl=1')
- 对输入文本进行预处理:
def preprocess(text):
 text=text.lower()
 text=re.sub('[⁰-9a-zA-Z]+',' ',text)
 words = text.split()
 words2 = [i for i in words if i not in stop]
 words3=' '.join(words2)
 return(words3)
data['text'] = data['text'].apply(preprocess)
- 提取数据集中所有句子的列表列表:
t=[]
for i in range(len(data)):
 t.append(data['text'][i].split())
- 构建一个 CBOW 模型,其中上下文窗口size为5,向量长度为 100:
from gensim.models import Word2Vec
model = Word2Vec(size=100,window=5,min_count=30, sg=0)
- 指定词汇表以构建模型,然后进行训练:
model.build_vocab(t)
model.train(t, total_examples=model.corpus_count, epochs=100)
- 提取给定推文的平均向量:
import numpy as np
features= []
for i in range(len(t)):
      t2 = t[i]
      z = np.zeros((1,100))
      k=0
      for j in range(len(t2)):
            try:
              z = z+model[t2[j]]
              k= k+1
            except KeyError:
              continue
      features.append(z/k)
我们正在对输入句子中所有单词的词向量取平均值。此外,会有一些不在词汇表中的单词(出现频率较低的单词),如果我们尝试提取它们的词向量,将会导致错误。我们为这个特定场景部署了try和catch错误处理机制。
- 对特征进行预处理,将其转换为数组,分割数据集为训练集和测试集,并重塑数据集,以便可以传递给模型:
features = np.array(features)
from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, y_test = train_test_split(features, data['airline_sentiment'], test_size=0.30,random_state=10)
X_train = X_train.reshape(X_train.shape[0],100)
X_test = X_test.reshape(X_test.shape[0],100)
- 编译并构建神经网络,以预测推文的情感:
from keras.layers import Dense, Activation
from keras.models import Sequential
from keras.utils import to_categorical
from keras.layers.embeddings import Embedding
model = Sequential()
model.add(Dense(1000,input_dim = 100,activation='relu'))
model.add(Dense(1))
model.add(Activation('sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
model.summary()
上面定义的模型摘要如下:

在上述模型中,我们有一个 1000 维的隐藏层,将 100 个输入的平均词向量值连接到输出层,输出值为 1(1 或 0 表示正面或负面情感):
model.fit(X_train, y_train, batch_size=128, nb_epoch=5, validation_data=(X_test, y_test),verbose = 1)
我们可以看到,在预测推文情感时,我们的模型准确率约为 90%。
- 绘制预测结果的混淆矩阵:
pred = model.predict(X_test)
pred2 = np.where(pred>0.5,1,0)
from sklearn.metrics import confusion_matrix
confusion_matrix(y_test, pred2)
混淆矩阵的输出如下:

从上面可以看到,在 2644 个句子中,我们预测它们为正面情感,且它们确实是正面情感。125 个句子被预测为负面情感,但实际为正面。209 个句子被预测为正面情感,但实际上是负面情感,最后,485 个句子被预测为负面情感,且实际为负面情感。
还有更多内容...
虽然我们使用 CBOW 模型和推文中所有词向量的平均值实现了情感分类,但我们本可以采用以下其他方法:
- 
使用 skip-gram 模型。 
- 
使用 doc2vec 模型通过文档向量构建模型。 
- 
使用基于 fastText 模型的词向量。 
- 
使用基于 GloVe 的词向量。 
- 
使用预训练模型的词向量值。 
虽然这些方法工作原理相似,但上述模型的一个限制是它没有考虑单词的顺序。有更复杂的算法可以解决单词顺序的问题,这将在下一章讨论。
第十一章:构建循环神经网络
在上一章中,我们介绍了多种将文本表示为向量的方式,并在这些表示之上执行情感分类。
这种方法的一个缺点是我们没有考虑到单词的顺序——例如,句子 A is faster than B 和 B is faster than A 将有相同的表示,因为这两个句子中的单词完全相同,而单词的顺序不同。
循环神经网络(RNNs)在需要保留单词顺序的场景中非常有用。在本章中,您将学习以下主题:
- 
从零开始在 Python 中构建 RNN 和 LSTM 
- 
实现 RNN 进行情感分类 
- 
实现 LSTM 进行情感分类 
- 
实现堆叠 LSTM 进行情感分类 
介绍
RNN 可以通过多种方式架构。以下是一些可能的方式:

底部的框是输入,接着是隐藏层(中间的框),顶部是输出层。此一对一架构是典型的神经网络,输入层和输出层之间有一个隐藏层。不同架构的示例如下:
| 架构 | 示例 | 
|---|---|
| 一对多 | 输入是图像,输出是图像的描述 | 
| 多对一 | 输入是电影的评论(输入中包含多个单词),输出是与评论相关的情感 | 
| 多对多 | 将一种语言的句子翻译成另一种语言的句子 | 
RNN 架构的直觉
当我们希望在给定一系列事件时预测下一个事件时,RNN 非常有用。
其中一个例子是预测 This is an _____ 后面跟的单词。
假设在现实中,句子是 This is an example。
传统的文本挖掘技术通常通过以下方式解决问题:
- 对每个单词进行编码,同时为潜在的新单词设置额外的索引:
This: {1,0,0,0}
is: {0,1,0,0}
an: {0,0,1,0}
- 编码短语 This is an:
This is an: {1,1,1,0}
- 创建训练数据集:
Input --> {1,1,1,0}
Output --> {0,0,0,1}
- 构建一个包含输入和输出的模型
该模型的一个主要缺点是输入表示在输入句子中没有变化;它可以是 this is an、an is this 或 this an is。
然而,直观上,我们知道前面每个句子的结构是不同的,不能用相同的数学结构表示。这就需要采用不同的架构,结构如下:

在前面的架构中,句子中的每个单词都会进入输入框中的独立框中。然而,句子的结构将得到保留。例如,this 进入第一个框,is 进入第二个框,an 进入第三个框。
顶部的输出框将是 example 的输出。
解释 RNN
你可以将 RNN 看作一种存储记忆的机制——其中记忆被保存在隐藏层内。它可以被如下方式可视化:

右侧的网络是左侧网络的展开版本。右侧的网络在每个时间步输入一个数据,并在每个时间步提取输出。然而,如果我们关注第四个时间步的输出,我们需要提供前三个时间步的输入,而第三个时间步的输出则是第四个时间步的预测值。
请注意,在预测第三个时间步的输出时,我们通过隐藏层引入了前两个时间步的值,隐藏层将跨时间步连接这些值。
让我们探索前面的图示:
- 
U 权重表示连接输入层与隐藏层的权重 
- 
W 权重表示隐藏层之间的连接 
- 
V 权重表示隐藏层与输出层之间的连接 
为什么要存储记忆?
需要存储记忆,就像前面的示例中,甚至在文本生成中,下一词不一定仅依赖于前一个词,而是依赖于前面词语的上下文。
由于我们关注前面的词语,应该有一种方式来将它们保存在记忆中,以便我们能够更准确地预测下一个词。
此外,我们还需要按顺序存储记忆;也就是说,通常情况下,最近的词语比距离要预测的词更远的词语更有助于预测下一个词。
从头开始在 Python 中构建 RNN
在这个教程中,我们将从头开始构建一个 RNN,使用一个简单的示例,以帮助你更好地理解 RNN 如何帮助解决考虑事件(单词)顺序的问题。
准备开始
请注意,典型的神经网络包含一个输入层,之后是隐藏层的激活,最后是输出层的 softmax 激活。
RNN 采用类似的结构,并在这种结构上进行修改,使得前一个时间步的隐藏层被考虑在当前时间步中。
在将 RNN 应用于更实际的用例之前,我们会先通过一个简单的示例构建 RNN 的工作细节。
让我们考虑一个如下的示例文本:This is an example。
当前任务是给定一对单词序列,预测第三个词。
因此,数据集的转换如下:
| Input | Output | 
|---|---|
| this, is | an | 
| is, an | example | 
给定输入 this is,我们期望预测 example 作为输出。
我们将采用以下策略来构建一个 RNN:
- 
对单词进行独热编码 
- 
确定输入的最大长度: - 将其余输入填充至最大长度,使得所有输入的长度相同
 
- 
将输入中的单词转换为一个独热编码版本 
- 
将输出中的单词转换为一个独热编码版本 
- 
处理输入和输出数据,然后拟合 RNN 模型 
如何做...
上述策略的代码如下(代码文件可在 GitHub 的Building_a_Recurrent_Neural_Network_from_scratch-in_Python.ipynb中找到):
- 让我们在代码中定义输入和输出,如下所示:
#define documents
docs = ['this, is','is an']
# define class labels
labels = ['an','example']
- 让我们对数据集进行预处理,以便将其传递给 RNN:
from collections import Counter
counts = Counter()
for i,review in enumerate(docs+labels):
      counts.update(review.split())
words = sorted(counts, key=counts.get, reverse=True)
vocab_size=len(words)
word_to_int = {word: i for i, word in enumerate(words, 1)}
在前面的步骤中,我们识别了数据集中所有唯一的单词及其对应的频率(计数),并为每个单词分配了一个 ID 号。在前面的代码中,word_to_int的输出如下所示:
print(word_to_int)
# {'an': 2, 'example': 4, 'is': 1, 'this': 3}
- 按照如下方式修改输入和输出单词及其对应的 ID:
encoded_docs = []
for doc in docs:
      encoded_docs.append([word_to_int[word] for word in doc.split()])
encoded_labels = []
for label in labels:
      encoded_labels.append([word_to_int[word] for word in label.split()])
print('encoded_docs: ',encoded_docs)
print('encoded_labels: ',encoded_labels)
# encoded_docs: [[3, 1], [1, 2]]
# encoded_labels: [[2], [4]]
在前面的代码中,我们将输入句子中每个单词的 ID 附加到一个列表中,从而使输入(encoded_docs)成为一个包含列表的列表。
同样,我们将输出中每个单词的 ID 附加到一个列表中。
- 在编码输入时需要考虑的另一个因素是输入的长度。在情感分析的情况下,输入文本的长度可能因评论而异。然而,神经网络要求输入的大小是固定的。为了解决这个问题,我们在输入上进行填充。填充确保所有输入被编码为相似的长度。虽然在我们的示例中两个输入的长度都是 2,但实际上我们很可能会遇到输入长度不同的情况。在代码中,我们按如下方式进行填充:
# pad documents to a max length of 2 words
max_length = 2
padded_docs = pad_sequences(encoded_docs, maxlen=max_length, padding='pre')
在前面的代码中,我们将encoded_docs传递给pad_sequences函数,确保所有输入数据点的长度相同——即等于maxlen参数。此外,对于那些长度小于maxlen的参数,它会用 0 填充这些数据点,直到达到maxlen的总长度,并且零填充会在pre位置完成——也就是在原始编码句子的左侧。
现在输入数据集已经创建完毕,接下来让我们对输出数据集进行预处理,以便将其传递给模型训练步骤。
- 对输出的典型处理是将其转换为虚拟值,即制作输出标签的独热编码版本,方法如下:
one_hot_encoded_labels = to_categorical(encoded_labels, num_classes=5)
print(one_hot_encoded_labels)
# [[0\. 0\. 1\. 0\. 0.] [0\. 0\. 0\. 0\. 1.]]
请注意,给定输出值(encoded_labels)为{2, 4},输出向量在第二和第四位置分别为 1。
- 
让我们构建模型: - RNN 期望输入的形状为(batch_size、time_steps和features_per_timestep)。因此,我们首先将padded_docs输入重塑为以下格式:
 
- RNN 期望输入的形状为(
padded_docs = padded_docs.reshape(2,2,1)
请注意,理想情况下我们会为每个单词(在这个特定情况下是 ID)创建一个词嵌入。然而,鉴于本教程的目的是了解 RNN 的工作细节,我们将不涉及 ID 的嵌入,并假设每个输入不是 ID 而是一个值。话虽如此,我们将在下一个教程中学习如何执行 ID 嵌入。
- 
- 定义模型——在这里我们指定将使用SimpleRNN方法初始化 RNN:
 
- 定义模型——在这里我们指定将使用
# define the model
embed_length=1
max_length=2
model = Sequential()
model.add(SimpleRNN(1,activation='tanh', return_sequences=False,recurrent_initializer='Zeros',input_shape=(max_length,embed_length),unroll=True))
在前一步中,我们明确指定了recurrent_initializer为零,这样可以更容易理解 RNN 的工作细节。实际上,我们不会将循环初始化器设置为 0。
return_sequences参数指定是否希望在每个时间步获得隐藏层值。若return_sequences为 false,表示我们只希望在最后一个时间步获得隐藏层输出。
通常,在多对一任务中,当输入很多(每个时间步一个输入)并且有输出时,return_sequences会设置为 false,这样输出仅会在最后一个时间步获得。例如,给定过去五天的股票价格序列,预测第二天的股票价格就是一个典型的例子。
然而,在尝试在每个时间步获取隐藏层值的情况下,return_sequences将被设置为True。例如,机器翻译就是一个例子,其中有多个输入和多个输出。
- 
- 将 RNN 输出连接到输出层的五个节点:
 
model.add(Dense(5, activation='softmax'))
我们已经执行了一个Dense(5),因为有五个可能的输出类别(每个样本的输出有 5 个值,每个值对应它属于word ID 0到word ID 4的概率)。
- 
- 编译并总结模型:
 
# compile the model
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['acc'])
# summarize the model
print(model.summary())
模型总结如下:

现在我们已经定义了模型,在拟合输入和输出之前,让我们了解每一层中参数数量的原因。
模型的简单 RNN 部分的输出形状为(None, 1)。输出形状中的None表示batch_size。None 是指定batch_size可以是任意数值的方式。由于我们指定了从简单 RNN 中输出一个单位的隐藏层,列数为 1。
现在我们理解了simpleRNN的输出形状,让我们理解为什么在simpleRNN层中参数数量是 3。注意,隐藏层值只在最后一个时间步输出。鉴于每个时间步的输入值为 1(每个时间步一个特征),而输出也为一个值,输入实际上是与一个单一的权重相乘。如果输出(隐藏层值)有 40 个隐藏层单元,输入就应该与 40 个单位相乘来获得输出(更多内容请参见实现 RNN 进行情感分类)。
除了连接输入和隐藏层值的一个权重外,还有一个与权重一起的偏置项。另一个参数来自于前一个时间步的隐藏层值与当前时间步隐藏层的连接,最终得到三个参数。
从隐藏层到最终输出有 10 个参数,因为有五个可能的类别,结果是五个权重和五个偏置连接到隐藏层的值(该值是一个单位)——总共 10 个参数。
- 
- 训练模型以预测从输入到输出的结果:
 
model.fit(padded_docs,np.array(one_hot_encoded_labels),epochs=500)
- 提取第一个输入数据点的预测值:
model.predict(padded_docs[0].reshape(1,2,1))
提取的输出如下:
array([[0.3684635, 0.33566403, 0.61344165, 0.378485, 0.4069949 ]],      dtype=float32)
验证输出结果
现在模型已经训练完成,让我们通过从后向前分析来理解 RNN 的工作原理——也就是说,提取模型的权重,通过权重前馈输入以匹配预测值,使用 NumPy(代码文件可以在 GitHub 上找到,名为Building_a_Recurrent_Neural_Network_from_scratch_in_Python.ipynb)。
- 检查权重:
model.weights
[<tf.Variable 'simple_rnn_2/kernel:0' shape=(1, 1) dtype=float32_ref>,
 <tf.Variable 'simple_rnn_2/recurrent_kernel:0' shape=(1, 1) dtype=float32_ref>,
 <tf.Variable 'simple_rnn_2/bias:0' shape=(1,) dtype=float32_ref>,
 <tf.Variable 'dense_2/kernel:0' shape=(1, 5) dtype=float32_ref>,
 <tf.Variable 'dense_2/bias:0' shape=(5,) dtype=float32_ref>]
上述内容为我们提供了权重在输出中呈现的顺序的直觉。
在前面的例子中,kernel表示权重,recurrent表示隐藏层从一个时刻到另一个时刻的连接。
请注意,simpleRNN有连接输入层到隐藏层的权重,也有连接前一时刻隐藏层到当前时刻隐藏层的权重。
dense_2层中的核和偏置表示连接隐藏层值到最终输出的层:
- 
- 提取权重:
 
model.get_weights()
前面的代码行给出了每个权重的计算值。
- 将输入通过第一时刻传递——输入值如下:
padded_docs[0]
#array([3, 1], dtype=int32)
在前面的代码中,第一时刻的值为3,第二时刻的值为1。我们将按如下方式初始化第一时刻的值:
input_t0 = 3
- 
- 第一时刻的值与连接输入到隐藏层的权重相乘,然后加上偏置值:
 
input_t0_kernel_bias = input_t0*model.get_weights()[0] + model.get_weights()[2]
- 
- 此时刻的隐藏层值通过tanh激活函数计算得出(因为这是我们在定义模型时指定的激活函数):
 
- 此时刻的隐藏层值通过
hidden_layer0_value = np.tanh(input_t0_kernel_bias)
- 计算时间步 2 时的隐藏层值;此时输入的值为1(注意padded_docs[0]的值为[3, 1]):
input_t1 = 1
- 
- 当第二个时间步的输入通过权重和偏置时,输出值如下:
 
input_t1_kernel_bias = input_t1*model.get_weights()[0] + model.get_weights()[2]
请注意,乘以输入的权重在任何时间步中都是相同的。
- 
- 在不同时间步计算隐藏层的过程如下:
 

其中Φ是执行的激活(通常使用tanh激活)。
从输入层到隐藏层的计算包含两个部分:
- 
- 
输入层值与核权重的矩阵乘法 
- 
前一时刻隐藏层和循环权重的矩阵乘法 
 
- 
给定时间步的最终隐藏层值的计算将是前两次矩阵乘法的总和。将结果通过tanh激活函数处理:
input_t1_recurrent = hidden_layer0_value*model.get_weights()[1]
在经过tan激活之前的总值如下:
total_input_t1 = input_t1_kernel_bias + input_t1_recurrent
隐藏层的输出值通过tan激活函数计算,具体如下:
output_t1 = np.tanh(total_input_t1)
- 将最后一个时间步的隐藏层输出通过全连接层传递,该层将隐藏层与输出层连接:
final_output = output_t1*model.get_weights()[3] + model.get_weights()[4]
请注意,model.get_weights()方法的第四和第五个输出对应的是从隐藏层到输出层的连接。
- 将前面的输出通过 softmax 激活函数(如模型中定义)传递,以获得最终输出:
np.exp(final_output)/np.sum(np.exp(final_output))
# array([[0.3684635, 0.33566403, 0.61344165, 0.378485, 0.40699497]], dtype=float32)
你应该注意到,通过网络的前向传播得到的输出与model.predict函数输出的结果是相同的。
实现 RNN 进行情感分类
为了理解如何在 Keras 中实现 RNN,让我们实现我们在第十章,“使用词向量进行文本分析”章节中进行的航空公司推文情感分类练习。
如何做到这一点...
任务将按以下方式执行(代码文件在 GitHub 上可用,名为RNN_and_LSTM_sentiment_classification.ipynb):
- 导入相关包和数据集:
from keras.layers import Dense, Activation
from keras.layers.recurrent import SimpleRNN
from keras.models import Sequential
from keras.utils import to_categorical
from keras.layers.embeddings import Embedding
from sklearn.cross_validation import train_test_split
import numpy as np
import nltk
from nltk.corpus import stopwords
import re
import pandas as pd
data=pd.read_csv('https://www.dropbox.com/s/8yq0edd4q908xqw/airline_sentiment.csv')
data.head()
- 对文本进行预处理,移除标点符号,将所有单词标准化为小写,并移除停用词,如下所示:
import nltk
nltk.download('stopwords')
stop = nltk.corpus.stopwords.words('english')
def preprocess(text):
    text=text.lower()
    text=re.sub('[⁰-9a-zA-Z]+',' ',text)
    words = text.split()
    words2=[w for w in words if (w not in stop)]
    #words3=[ps.stem(w) for w in words]
    words4=' '.join(words2)
    return(words4)
data['text'] = data['text'].apply(preprocess)
- 提取构成数据集的所有单词到整数的映射:
from collections import Counter
counts = Counter()
for i,review in enumerate(t['text']):
    counts.update(review.split())
words = sorted(counts, key=counts.get, reverse=True
在前一步骤中,我们提取了数据集中所有单词的频率。提取的部分单词示例如下:

nb_chars = len(words)
word_to_int = {word: i for i, word in enumerate(words, 1)}
int_to_word = {i: word for i, word in enumerate(words, 1)}
在前面的代码中,我们遍历所有单词,并为每个单词分配一个索引。整数到单词字典的示例部分如下:

- 将给定句子中的每个单词映射到与其相关联的单词:
mapped_reviews = []
for review in data['text']:
    mapped_reviews.append([word_to_int[word] for word in review.split()])
在前一步骤中,我们将文本评论转换为一个包含多个列表的列表,每个列表包含一个句子中单词的 ID。原始评论和映射评论的示例如下:

- 提取句子的最大长度,并通过填充将所有句子标准化为相同的长度。在以下代码中,我们循环遍历所有评论并存储每个评论对应的长度。此外,我们还在计算评论(推文文本)的最大长度:
length_sent = []
for i in range(len(mapped_reviews)):
      length_sent.append(len(mapped_reviews[i]))
sequence_length = max(length_sent)
我们应该注意到,不同的推文长度不同。然而,RNN 期望每个输入的时间步数相同。在下面的代码中,如果评论的长度小于数据集中所有评论的最大长度,我们会通过值为 0 的填充对评论进行映射。这样,所有输入都会有相同的长度。
from keras.preprocessing.sequence import pad_sequences
X = pad_sequences(maxlen=sequence_length, sequences=mapped_reviews, padding="post", value=0)
- 准备训练集和测试集:
y=data['airline_sentiment'].values
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.30,random_state=42)
y_train2 = to_categorical(y_train)
y_test2 = to_categorical(y_test)
在前一步骤中,我们将原始数据拆分为训练集和测试集,并将因变量转换为独热编码变量。
- 构建 RNN 架构并编译模型:
embedding_vecor_length=32
max_review_length=26
model = Sequential()
model.add(Embedding(input_dim=12533, output_dim=32, input_length = 26))
请注意,嵌入层以所有独特词汇的总数作为输入,并为每个词汇创建一个向量,其中output_dim表示词汇表示的维度数,input_length表示每个句子的词汇数:
model.add(SimpleRNN(40, return_sequences=False))
请注意,在 RNN 层中,如果我们希望提取每个时间步的输出,我们会将return_sequences参数设置为True。然而,在我们当前要解决的用例中,我们只在读取完所有输入词汇后才提取输出,因此return_sequences = False:
model.add(Dense(2, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
print(model.summary())
模型的总结如下:

让我们理解为什么在嵌入层中有401056个参数需要估计。总共有 12,532 个独特的词汇,如果我们考虑到没有索引为 0 的词汇,总共有 12,533 个可能的词汇,每个词汇在 32 维度中表示,因此需要估计(12,533 x 32 = 401,056)个参数。
现在,让我们试着理解为什么在simpleRNN层中有 2,920 个参数。
有一组权重将输入连接到 RNN 的 40 个单元。鉴于每个时间步有 32 个输入(同一组权重在每个时间步重复),因此总共有 32 x 40 个权重用于将输入连接到隐藏层。这为每个输入提供了 1 x 40 的输出。
此外,为了使 X * W[xh] 和 h[(t-1) ] W[hh] 之间的求和发生(其中 X 是输入值,W[xh] 是连接输入层和隐藏层的权重,W[hh]* 是连接前一个时间步的隐藏层与当前时间步隐藏层的权重,而 h[(t-1)] 是前一个时间步的隐藏层)——鉴于 X W[xh] 输入的输出是 1 x 40——h[(t-1)] X W[hh] 的输出也应该是 1 x 40。因此,W[hh] 矩阵的维度将是 40 x 40,因为 h[(t-1)] 的维度是 1 x 40。
除了权重外,我们还将为每个 40 个输出单元设置 40 个偏置项,因此总共有(32 x 40 + 40 x 40 + 40 = 2,920)个权重。
最后一层总共有 82 个权重,因为最后一个时间步的 40 个单元与两个可能的输出相连接,结果是 40 x 2 个权重和 2 个偏置项,因此总共有 82 个单元。
- 适配模型:
model.fit(X_train, y_train2, validation_data=(X_test, y_test2), epochs=10, batch_size=32)
训练和测试数据集中的准确率和损失值的图示如下:

上述模型的输出大约为 89%,并未比我们在使用词向量进行文本分析章节中构建的基于词向量的网络提供显著改进。
然而,随着数据点数量的增加,预计会有更好的准确性。
还有更多内容...
一个考虑多个时间步给出预测的传统 RNN 可以如下可视化:

请注意,随着时间步的增加,早期层次的输入影响会变得较小。可以在这里看到这种直觉(暂时忽略偏置项):
h[5] = WX[5] + Uh[4] = WX5 + UWX[4] + U²WX[3] + U³WX[2] + U⁴WX[1]
你可以看到,随着时间步的增加,当U>1时,隐藏层的值高度依赖于X[1];当U<1时,依赖性则会大大降低。
对 U 矩阵的依赖也可能导致梯度消失问题,当U的值非常小的时候;当U的值非常大时,也可能导致梯度爆炸问题。
上述现象导致在预测下一个词时,若存在长期依赖性,会出现问题。为了解决这个问题,我们将在下一个教程中使用长短期记忆(LSTM)架构。
从零开始在 Python 中构建 LSTM 网络
在上一节关于传统 RNN 问题的部分,我们了解到当存在长期依赖性时,RNN 并不能有效工作。例如,假设输入句子如下:
I live in India. I speak ____.
在前述语句中,空白处可以通过查看关键词India来填充,而India距我们要预测的词有三个时间步的距离。
以类似的方式,如果关键词离要预测的词很远,梯度消失/爆炸问题需要被解决。
准备工作
在本教程中,我们将学习 LSTM 如何帮助克服 RNN 架构的长期依赖性问题,并构建一个简单的示例,以便我们理解 LSTM 的各个组件。
LSTM 结构如下:

你可以看到,虽然输入X和隐藏层输出(h)保持不变,但隐藏层内会发生不同的激活(某些情况下是 sigmoid 激活,其他情况下是 tanh 激活)。
让我们仔细检查一个时间步内发生的各种激活:

在前面的图示中,X和h分别代表输入层和隐藏层。
长期记忆存储在单元状态C中。
需要遗忘的内容是通过遗忘门获得的:
f[t]=σ(W[xf]x((t))+W[hf]h((t-1))+b[f])
Sigmoid 激活使网络能够选择性地识别需要遗忘的内容。
在我们确定需要遗忘的内容后,更新的单元状态如下:
c[t]=(c[t-1]  f)
 f)
请注意, 表示逐元素相乘。
表示逐元素相乘。
例如,如果句子的输入序列是I live in India. I speak ___,则空白处可以根据输入词India来填充。在填充空白后,我们不一定需要该国家名称的具体信息。
我们根据当前时间步需要遗忘的内容来更新单元状态。
在下一步中,我们将根据当前时间步提供的输入向单元状态添加额外的信息。此外,更新的幅度(正向或负向)通过 tanh 激活函数获得。
输入可以按如下方式指定:
i[t]=σ(W[xi]x((t))+W[hi]h((t-1))+bi)
调制(输入更新的幅度)可以按如下方式指定:
g[t]=tanh(W[xg]x((t))+W[hg]h((t-1))+bg)
单元状态——在一个时间步中,我们忘记某些信息并在同一时间步中添加额外的信息——按如下方式更新:

在最后一个门中,我们需要指定输入的组合(当前时间步输入与前一时间步的隐藏层值的组合)和单元状态中需要输出到下一个隐藏层的部分:

最终的隐藏层表示如下:

通过这种方式,我们能够利用 LSTM 中的各种门来选择性地识别需要存储在记忆中的信息,从而克服 RNN 的局限性。
如何做...
为了获得该理论如何工作的实际直觉,让我们看看我们在理解 RNN 时做过的相同示例,但这次使用 LSTM。
请注意,数据预处理步骤在两个示例中是相同的。因此,我们将重用预处理部分(在从零开始构建 RNN 的 Python 教程中的步骤 1到步骤 4),并直接进入模型构建部分(代码文件在 GitHub 中的LSTM_working_details.ipynb可用):
- 定义模型:
embed_length=1
max_length=2
model = Sequential()
model.add(LSTM(1,activation='tanh',return_sequences=False,
recurrent_initializer='Zeros',recurrent_activation='sigmoid',
input_shape=(max_length,embed_length),unroll=True))
请注意,在前面的代码中,我们将递归初始化器和递归激活函数初始化为某些值,仅为了简化此示例;其目的是帮助您理解后端发生了什么。
model.add(Dense(5, activation='softmax'))
# compile the model
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
# summarize the model
print(model.summary())
模型的总结如下:

在 LSTM 层中,参数的数量是12,因为有四个门(遗忘门、输入门、细胞门和输出门),这导致有四个权重和四个偏置将输入与隐藏层连接。此外,递归层包含对应四个门的权重值,因此我们总共有12个参数。
Dense 层总共有 10 个参数,因为输出有五个可能的类别,因此有五个权重和五个偏置,分别对应从隐藏层到输出层的每个连接。
- 让我们拟合模型:
model.fit(padded_docs.reshape(2,2,1),np.array(one_hot_encoded_labels),epochs=500)
- 该模型的权重顺序如下:
model.weights[<tf.Variable 'lstm_19/kernel:0' shape=(1, 4) dtype=float32_ref>,
 <tf.Variable 'lstm_19/recurrent_kernel:0' shape=(1, 4) dtype=float32_ref>,
 <tf.Variable 'lstm_19/bias:0' shape=(4,) dtype=float32_ref>,
 <tf.Variable 'dense_18/kernel:0' shape=(1, 5) dtype=float32_ref>,
 <tf.Variable 'dense_18/bias:0' shape=(5,) dtype=float32_ref>]
可以按如下方式获取权重:
model.get_weights()
从前面的代码(model.weights)中,我们可以看到 LSTM 层中权重的顺序如下:
- 
- 
输入的权重(核) 
- 
对应于隐藏层的权重( recurrent_kernel)
- 
LSTM 层中的偏置 
 
- 
类似地,在密集层(连接隐藏层与输出的层)中,权重的顺序如下:
- 
- 
与隐藏层相乘的权重 
- 
偏置 
 
- 
以下是 LSTM 层中权重和偏置的顺序(在前面的输出中没有提供,但可以在 Keras 的 GitHub 仓库中找到):
- 
- 
输入门 
- 
遗忘门 
- 
调制门(单元门) 
- 
输出门 
 
- 
- 计算输入的预测。
我们使用的是未经编码的原始输入值(1、2、3),而没有将其转换为嵌入值——仅仅是为了看看计算是如何工作的。实际操作中,我们会将输入转换为嵌入值。
- 为预测方法重塑输入,以使其符合 LSTM 所期望的数据格式(批量大小、时间步数、每个时间步的特征):
model.predict(padded_docs[0].reshape(1,2,1))
# array([[0.05610514, 0.11013522, 0.38451442, 0.0529648, 0.39628044]], dtype=float32)
上面代码中的注释行提供了预测方法的输出。
验证输出
现在我们已经从模型中获得了预测概率,让我们使用 NumPy 通过权重的前向传递来运行输入,以获得与刚才相同的输出。
这样做是为了验证我们对 LSTM 内部工作原理的理解。我们验证所构建模型输出的步骤如下:
- 更新时间步 1 中的遗忘门。此步骤查看输入,并提供对到目前为止已知的单元状态(记忆)需要遗忘多少的估计(请注意在这方面使用了 sigmoid 函数):
input_t0 = 3
cell_state0 = 0
forget0 = input_t0*model.get_weights()[0][0][1] + model.get_weights()[2][1]
forget1 = 1/(1+np.exp(-(forget0)))
- 基于更新的遗忘门更新单元状态。前一步的输出将在此处用于指导从单元状态(记忆)中忘记多少值:
cell_state1 = forget1 * cell_state0
- 更新时间步 1 中的输入门值。此步骤给出了根据当前输入将有多少新信息注入到单元状态中的估计:
input_t0_1 = input_t0*model.get_weights()[0][0][0] + model.get_weights()[2][0]
input_t0_2 = 1/(1+np.exp(-(input_t0_1)))
- 基于更新的输入值更新单元状态。这是一个步骤,其中前一步的输出被用来指示应对单元状态(记忆)进行多少信息更新:
input_t0_cell1 = input_t0*model.get_weights()[0][0][2] +model.get_weights()[2][2]
input_t0_cell2 = np.tanh(input_t0_cell1)
之前的tanh激活有助于确定输入的更新是否会对单元状态(记忆)进行加法或减法操作。这提供了一个额外的杠杆,因为如果某些信息已经在当前时间步传递,并且在未来的时间步不再有用,那么我们最好将其从单元状态中删除,这样这些额外的信息(在下一步可能没有帮助)就能从记忆中擦除:
input_t0_cell3 = input_t0_cell2*input_t0_2
input_t0_cell4 = input_t0_cell3 + cell_state1
- 更新输出门。此步骤提供对当前时间步将传递多少信息的估计(请注意在这方面使用了 sigmoid 函数):
output_t0_1 = input_t0*model.get_weights()[0][0][3] + model.get_weights()[2][3]
output_t0_2 = 1/(1+np.exp(-output_t0_1))
- 计算时间步 1 的隐藏层值。请注意,某一时间步的最终隐藏层值是当前时间步的记忆和输出用于传递单一时间步的结合:
hidden_layer_1 = np.tanh(input_t0_cell4)*output_t0_2
我们已经完成了从第一时间步得到的隐藏层输出值的计算。在接下来的步骤中,我们将把时间步长 1 更新后的单元状态值和时间步长 1 的隐藏层输出作为输入传递给时间步长 2。
- 传递时间步长为 2 的输入值和进入时间步长 2 的单元状态值:
input_t1 = 1
cell_state1 = input_t0_cell4
- 更新忘记门的值:
forget21 = hidden_layer_1*model.get_weights()[1][0][1] + model.get_weights()[2][1] + input_t1*model.get_weights()[0][0][1]
forget_22 = 1/(1+np.exp(-(forget21)))
- 更新时间步长 2 中的单元状态值:
cell_state2 = cell_state1 * forget_22
input_t1_1 = input_t1*model.get_weights()[0][0][0] + model.get_weights()[2][0] + hidden_layer_1*model.get_weights()[1][0][0]
input_t1_2 = 1/(1+np.exp(-(input_t1_1)))
input_t1_cell1 = input_t1*model.get_weights()[0][0][2] + model.get_weights()[2][2]+ hidden_layer_1*model.get_weights()[1][0][2]
input_t1_cell2 = np.tanh(input_t1_cell1)
input_t1_cell3 = input_t1_cell2*input_t1_2
input_t1_cell4 = input_t1_cell3 + cell_state2
- 根据更新后的单元状态与需要输出的量的结合更新隐藏层输出值:
output_t1_1 = input_t1*model.get_weights()[0][0][3] + model.get_weights()[2][3]+ hidden_layer_1*model.get_weights()[1][0][3]
output_t1_2 = 1/(1+np.exp(-output_t1_1))
hidden_layer_2 = np.tanh(input_t1_cell4)*output_t1_2
- 将隐藏层输出通过全连接层传递:
final_output = hidden_layer_2 * model.get_weights()[3][0] +model.get_weights()[4]
- 在我们刚刚得到的输出上运行 softmax:
np.exp(final_output)/np.sum(np.exp(final_output))
# array([0.05610514, 0.11013523, 0.3845144, 0.05296481, 0.39628044],dtype=float32)
你应该注意到,这里得到的输出与我们从 model.predict 方法中获得的输出完全相同。
通过这个练习,我们更有能力理解 LSTM 的工作细节。
实现 LSTM 用于情感分类
在 实现 RNN 用于情感分类 配方中,我们使用 RNN 实现了情感分类。在本配方中,我们将探讨如何使用 LSTM 实现它。
如何做...
我们将采用的步骤如下(代码文件在 GitHub 上作为 RNN_and_LSTM_sentiment_classification.ipynb 提供):
- 定义模型。与我们在 实现 RNN 用于情感分类 配方中看到的代码的唯一变化是将模型架构部分的 simpleRNN改为 LSTM(我们将重用 实现 RNN 用于情感分类 配方中的 第 1 步 到 第 6 步 的代码):
embedding_vecor_length=32
max_review_length=26
model = Sequential()
model.add(Embedding(input_dim=12533, output_dim=32, input_length = 26))
嵌入层的输入是数据集中出现的唯一 ID 的总数,以及每个词需要转换的期望维度(output_dim)。
此外,我们还将指定输入的最大长度,以便下一个步骤中的 LSTM 层能获得所需的信息——批量大小、时间步长数(input_length)和每个时间步的特征数(step(output_dim)):
model.add(LSTM(40, return_sequences=False))
model.add(Dense(2, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
print(model.summary())
模型的总结如下:

尽管第一层和最后一层的参数与我们在 实现 RNN 用于情感分类 配方中看到的一样,LSTM 层的参数数量不同。
让我们理解 LSTM 层中如何获得 11,680 个参数:
W = model.layers[1].get_weights()[0]
U = model.layers[1].get_weights()[1]
b = model.layers[1].get_weights()[2]
print(W.shape,U.shape,b.shape)
输出将如下所示:
((32, 160), (40, 160), (160,))
注意,前述权重的总和是 (32160) + (40160) + 160 = 11,680 个参数。
W 表示将输入连接到四个单元(i、f、c、o)的权重,U 表示隐藏层到隐藏层的连接,b 表示每个门中的偏置。
输入门、忘记门、单元状态门和输出门的各个权重可以按如下方式获得:
units = 40
W_i = W[:, :units]
W_f = W[:, units: units * 2]
W_c = W[:, units * 2: units * 3]
W_o = W[:, units * 3:]
U_i = U[:, :units]
U_f = U[:, units: units * 2]
U_c = U[:, units * 2: units * 3]
U_o = U[:, units * 3:]
b_i = b[:units]
b_f = b[units: units * 2]
b_c = b[units * 2: units * 3]
b_o = b[units * 3:]
- 按如下方式拟合模型:
model.fit(X_train, y_train2, validation_data=(X_test, y_test2), epochs=50, batch_size=32)
训练和测试数据集上随着训练轮数增加,损失和准确率的变化如下:

使用 LSTM 层时,预测准确率为 91%,略优于使用 simpleRNN 层时的预测准确率。通过微调 LSTM 单元的数量,我们可能能够进一步改善结果。
实现堆叠 LSTM 进行情感分类
在前一个食谱中,我们使用 Keras 实现了基于 LSTM 的情感分类。在本食谱中,我们将探讨如何实现相同的功能,但堆叠多个 LSTM。堆叠多个 LSTM 可能会捕捉到更多数据的变化,因此有可能得到更好的准确率。
如何实现...
堆叠 LSTM 的实现方式如下(代码文件可在 GitHub 的RNN_and_LSTM_sentiment_classification.ipynb中找到):
- 我们之前看到的代码唯一的变化是将return_sequences参数设置为 true。这确保了第一个 LSTM 返回一个输出序列(与 LSTM 单元的数量相同),然后可以将该输出作为输入传递给模型架构部分中的另一个 LSTM(有关return_sequences参数的更多详细信息,请参见Sequence to Sequence 学习章节):
embedding_vecor_length=32
max_review_length=26
model = Sequential()
model.add(Embedding(input_dim=12533, output_dim=32, input_length = 26))
model.add(LSTM(40, return_sequences=True))
model.add(LSTM(40, return_sequences=False))
model.add(Dense(2, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
print(model.summary())
模型架构的总结如下:

请注意,在前面的架构中,有一个额外的 LSTM 堆叠在另一个 LSTM 之上。第一个 LSTM 在每个时间步的输出为40个值,因此输出形状为(None,26,40),其中None代表batch_size,26代表时间步数,40代表考虑的 LSTM 单元数。
现在有40个输入值,第二个 LSTM 中的参数数量与之前的做法相同,如下所示:
W = model.layers[2].get_weights()[0]
U = model.layers[2].get_weights()[1]
b = model.layers[2].get_weights()[2]
print(W.shape,U.shape,b.shape)
执行前述代码后,我们得到以下值:
((40, 160), (40, 160), (160,))
这导致总共有 12,960 个参数,如输出所示。
W 的形状为 40 x 160,因为它有 40 个输入映射到 40 个输出,并且有 4 个不同的门需要控制,因此总共有 40 x 40 x 4 个权重。
- 按如下方式实现模型:
model.fit(X_train, y_train2, validation_data=(X_test, y_test2), epochs=50, batch_size=32)
在训练集和测试集上,随着 epoch 数的增加,损失和准确率的变化如下所示:

这导致了 91%的准确率,就像我们在使用单个 LSTM 层时看到的那样;然而,随着数据量的增加,堆叠的 LSTM 可能比普通的 LSTM 捕捉到更多数据的变化。
还有更多...
门控循环单元(GRU)是我们可以使用的另一种架构,其准确率与 LSTM 相似。有关 GRU 的更多信息,请访问arxiv.org/abs/1412.3555。
第十二章:多对一架构 RNN 的应用
在上一章中,我们了解了 RNN 和 LSTM 的工作原理。我们还学习了情感分类,它是一个经典的多对一应用,因为输入中的许多单词对应一个输出(正面或负面情感)。
在本章中,我们将通过以下食谱进一步加深对多对一架构 RNN 的理解:
- 
生成文本 
- 
电影推荐 
- 
使用嵌入进行主题建模 
- 
预测股票价格的价值 
生成文本
在我们在第十一章中进行的情感分类任务中,构建循环神经网络,我们尝试预测一个离散事件(情感分类)。这属于多对一架构。在本食谱中,我们将学习如何实现多对多架构,其中输出将是给定 10 个单词序列之后可能的下一个 50 个单词。
准备工作
我们生成文本的策略如下:
- 
导入项目古腾堡的爱丽丝梦游仙境数据集,该数据集可以从 www.gutenberg.org/files/11/11-0.txt下载。
- 
对文本数据进行预处理,以便将每个单词转换为相同的大小写,并移除标点符号。 
- 
为每个唯一单词分配一个 ID,然后将数据集转换为一个单词 ID 的序列。 
- 
遍历整个数据集,每次处理 10 个单词。将这 10 个单词视为输入,并将随后的第 11 个单词视为输出。 
- 
构建并训练一个模型,通过对输入单词 ID 进行嵌入,并将嵌入连接到 LSTM,再通过隐藏层将其连接到输出层。输出层中的值是输出的独热编码版本。 
- 
通过选取一个随机位置的单词,并考虑该位置之前的历史单词来预测随后的单词。 
- 
将输入单词的窗口从我们之前选择的种子单词的位置向前移动一个位置,第十个时间步的单词将是我们在上一步骤中预测的单词。 
- 
继续这一过程以不断生成文本。 
如何做到这一点……
RNN 的典型需求,我们将查看给定的 10 个单词序列,以预测下一个可能的单词。在这个练习中,我们将采用《爱丽丝梦游仙境》数据集来生成单词,如下所示(代码文件可以在 GitHub 上的RNN_text_generation.ipynb中找到):
- 导入相关的包和数据集:
from keras.models import Sequential
from keras.layers import Dense,Activation
from keras.layers.recurrent import SimpleRNN
from keras.layers import LSTM
import numpy as np
fin=open('alice.txt',encoding='utf-8-sig')
lines=[]
for line in fin:
  line = line.strip().lower()
  if(len(line)==0):
    continue
  lines.append(line)
fin.close()
text = " ".join(lines)
输入文本的示例如下所示:

- 标准化文本以移除标点符号并将其转换为小写:
import re
text = text.lower()
text = re.sub('[⁰-9a-zA-Z]+',' ',text)
- 将唯一的单词分配给一个索引,以便在构建训练和测试数据集时引用:
from collections import Counter
counts = Counter()
counts.update(text.split())
words = sorted(counts, key=counts.get, reverse=True)
nb_words = len(text.split())
word2index = {word: i for i, word in enumerate(words)}
index2word = {i: word for i, word in enumerate(words)}
- 构建输入单词集,从中生成输出单词。请注意,我们考虑的是10个单词的序列,并尝试预测第 11个(th)单词:
SEQLEN = 10
STEP = 1
input_words = []
label_words = []
text2=text.split()
for i in range(0,nb_words-SEQLEN,STEP):
     x=text2[i:(i+SEQLEN)]
     y=text2[i+SEQLEN]
     input_words.append(x)
     label_words.append(y)
input_words和label_words列表的示例如下:

注意,input_words是一个包含列表的列表,而output_words则不是。
- 构建输入和输出数据集的向量:
total_words = len(set(words))
X = np.zeros((len(input_words), SEQLEN, total_words), dtype=np.bool)
y = np.zeros((len(input_words), total_words), dtype=np.bool)
在前面的步骤中,我们创建了空数组,这些数组将在接下来的代码中被填充:
# Create encoded vectors for the input and output values
for i, input_word in enumerate(input_words):
     for j, word in enumerate(input_word):
         X[i, j, word2index[word]] = 1
     y[i,word2index[label_words[i]]]=1
在前面的代码中,第一个for循环用于遍历输入词序列中的所有单词(输入中有10个单词),第二个for循环用于遍历选定的输入词序列中的每个单词。此外,由于输出是一个列表,我们不需要通过第二个for循环来更新它(因为没有 ID 序列)。X和y的输出形状如下:

- 定义模型的架构:
HIDDEN_SIZE = 128
BATCH_SIZE = 32
NUM_ITERATIONS = 100
NUM_EPOCHS_PER_ITERATION = 1
NUM_PREDS_PER_EPOCH = 100
model = Sequential()
model.add(LSTM(HIDDEN_SIZE,return_sequences=False,input_shape=(SEQLEN,total_words)))
model.add(Dense(total_words, activation='softmax'))
model.compile(optimizer='adam', loss='categorical_crossentropy')
model.summary()
模型的总结如下:

- 拟合模型。观察输出随着轮次的增加如何变化。生成一个随机的10个单词的序列,并尝试预测下一个可能的单词。我们可以观察到,随着轮次的增加,我们的预测逐渐变得更好:
for iteration in range(50):
     print("=" * 50)
     print("Iteration #: %d" % (iteration))
     model.fit(X, y, batch_size=BATCH_SIZE, epochs=NUM_EPOCHS_PER_ITERATION, validation_split = 0.1)
     test_idx = np.random.randint(int(len(input_words)*0.1)) * (-1)
     test_words = input_words[test_idx]
     print("Generating from seed: %s" % (test_words))
     for i in range(NUM_PREDS_PER_EPOCH): 
         Xtest = np.zeros((1, SEQLEN, total_words))
         for i, ch in enumerate(test_words):
             Xtest[0, i, word2index[ch]] = 1
         pred = model.predict(Xtest, verbose=0)[0]
         ypred = index2word[np.argmax(pred)]
         print(ypred,end=' ')
         test_words = test_words[1:] + [ypred]
在前面的代码中,我们正在为一个轮次(epoch)拟合我们的模型,使用输入和输出数组。此外,我们选择一个随机的种子词(test_idx – 这是一个随机数,位于输入数组的最后 10%中(因为validation_split为0.1),并在随机位置收集输入单词。我们将输入 ID 序列转换为 one-hot 编码版本(因此得到的数组形状为 1 x 10 x total_words)。
最后,我们对刚刚创建的数组进行预测,并获得概率最高的单词。我们来看一下第一轮(epoch)输出的结果,并与第25^(th)轮的输出进行对比:

注意,第一轮的输出总是the。然而,在经过 50 轮训练后,输出变得更加合理,如下所示:

Generating from seed行是预测结果的集合。
注意,虽然训练损失随着轮次的增加而减少,但在 50 轮结束时,验证损失变得更糟。随着我们在更多文本上训练和/或进一步微调模型,这将得到改善。
此外,这个模型可以通过使用双向 LSTM 进一步改进,我们将在序列到序列学习一章中讨论。使用双向 LSTM 后的输出如下:

电影推荐
推荐系统在用户发现过程中的作用非常重要。想象一下一个电商目录,其中包含成千上万种不同的产品。此外,某个产品的不同变体也会存在。在这种情况下,向用户普及产品或事件(例如某些产品正在打折)成为增加销售的关键。
准备就绪
在这个案例中,我们将学习如何为一个包含用户对电影评分的数据库构建推荐系统。这个练习的目标是最大化电影与用户的相关性。在定义目标时,我们还应考虑到,虽然推荐的电影可能仍然相关,但用户可能不会立即观看。同时,我们还应确保所有推荐内容不都属于同一类型。尤其在零售环境下,我们不希望在所有推荐中都推荐同一产品的不同变体。
让我们正式定义我们的目标和约束:
- 
目标:最大化推荐内容对用户的相关性 
- 
约束:增加推荐的多样性,并向用户提供最多 12 个推荐 
相关性的定义因用例而异,通常由商业原则指导。在本例中,我们将相关性定义得比较狭窄;也就是说,如果用户购买了在给定用户的前 12 个推荐商品中的任何一项,就视为成功。
基于此,让我们定义构建模型的步骤:
- 
导入数据。 
- 
推荐一部用户会高评分的电影——因此,让我们根据用户历史上喜欢的电影来训练模型。用户不喜欢某些电影的洞察将有助于进一步改善我们的推荐。然而,先保持简单。 
- 
只保留观看了超过五部电影的用户。 
- 
为独特的用户和电影分配 ID。 
- 
鉴于用户的偏好可能随时间变化,我们需要考虑用户的历史,历史中的不同事件有不同的权重。鉴于这是一个时间序列分析问题,我们将利用 RNN 来解决该问题。 
- 
对数据进行预处理,以便将其传递给 LSTM: - 
输入将是用户观看的历史五部电影 
- 
输出是用户观看的第六部电影 
 
- 
- 
构建一个执行以下操作的模型: - 
为输入的电影创建嵌入 
- 
将嵌入通过 LSTM 层 
- 
将 LSTM 的输出通过一个全连接层 
- 
在最终层应用 softmax,以生成推荐的电影列表 
 
- 
如何做...
现在我们已经了解了执行各种步骤的策略,让我们开始编写代码(代码文件在 GitHub 上的Chapter_12_Recommender_systems.ipynb中提供):
- 导入数据。我们将使用一个包含用户列表、用户对不同电影评分以及用户提供评分的时间戳的数据集:
import numpy as np
import pandas as pd
ratings = pd.read_csv('..') # Path to the file containing required fields
数据集的一个示例如下所示:

- 过滤掉用户没有喜欢的电影或没有足够历史记录的用户。在以下代码中,我们排除了用户给出低评分的电影:
ratings = ratings[ratings['rating']>3]
ratings = ratings.sort_values(by='timestamp')
ratings.reset_index(inplace=True)
ratings = ratings.drop(['index'],axis=1)
在以下代码中,我们仅保留那些在历史记录中提供超过5个评分(评分值大于3)的用户:
user_movie_count =ratings.groupby('User').agg({'Movies':'nunique'}).reset_index()
user_movie_count.columns = ['User','Movie_count']
ratings2 = ratings.merge(user_movie_count,on='User',how='inner')
movie_count = ratings2[ratings2['Movie_count']>5]
movie_count = movie_count.sort_values('timestamp')
movie_count.reset_index(inplace=True)
movie_count = movie_count.drop(['index'],axis=1)
- 为独特的users和Movies分配 ID,以便后续使用:
ratings = movie_count
users = ratings.User.unique()
articles = ratings.Movies.unique()
userid2idx = {o:i for i,o in enumerate(users)}
articlesid2idx = {o:i for i,o in enumerate(articles)}
idx2userid = {i:o for i,o in enumerate(users)}
idx2articlesid = {i:o for i,o in enumerate(articles)}
ratings['Movies2'] = ratings.Movies.apply(lambda x: articlesid2idx[x])
ratings['User2'] = ratings.User.apply(lambda x: userid2idx[x])
- 对数据进行预处理,使得输入是最后五部电影,输出是第六部观看的电影:
user_list = movie_count['User2'].unique()
historical5_watched = []
movie_to_predict = []
for i in range(len(user_list)):
     total_user_movies = movie_count[movie_count['User2']==user_list[i]].copy()
     total_user_movies.reset_index(inplace=True)
     total_user_movies = total_user_movies.drop(['index'],axis=1)
     for j in range(total_user_movies.shape[0]-6):
         historical5_watched.append(total_user_movies.loc[j:(j+4),'Movies2'].tolist())                                                                          movie_to_predict.append(total_user_movies.loc[(j+5),'Movies2'].tolist())
- 对historical5_watched和movie_to_predict变量进行预处理,以便将其传递给模型,然后创建训练集和测试集:
movie_to_predict2 = to_categorical(y, num_classes = max(y)+1)
trainX = np.array(historical5_watched[:40000])
testX = np.array(historical5_watched[40000:])
trainY = np.array(movie_to_predict2[:40000])
testY = np.array(movie_to_predict2[40000:])
- 构建模型:
src_vocab = ratings['Movies2'].nunique()
n_units = 32
src_timesteps = 5
tar_vocab = len(set(y))
from keras.models import Sequential, Model
from keras.layers import Embedding
from keras.layers import LSTM, RepeatVector, TimeDistributed, Dense, Bidirectional
model = Sequential()
model.add(Embedding(src_vocab, n_units, input_length=src_timesteps))
model.add((LSTM(100)))
model.add(Dense(1000,activation='relu'))
model.add(Dense(max(y)+1,activation='softmax'))
model.summary()
请注意,在最后一层中,我们对可能的激活值加 1,因为没有 ID 为 0 的电影,如果我们仅将值设置为max(y),最终的电影将被排除在外。
模型摘要如下:

- 拟合模型:
model.fit(trainX, trainY, epochs=5, batch_size=32, validation_data=(testX, testY), verbose = 1)
- 对测试数据进行预测:
pred = model.predict(testX)
- 了解数据点的数量(用户),其中接下来观看的电影在前五部历史电影之后位于前12个推荐中:
count = 0
for i in range(testX.shape[0]):
    rank = np.argmax(np.argsort(pred[i])[::-1]==np.argmax(testY[i]))
    if rank<12:
        count+=1
count/testX.shape[0]
# 0.104
我们应该注意到,在总案例的 10.4%中,推荐给用户的电影恰好是他们接下来要观看的电影。
考虑用户历史
在发布前 12 个推荐电影时,我们需要考虑的一个因素是如果用户已经观看过某部电影,他们不太可能再次观看同一部电影(请注意,这一假设在零售环境中并不成立,因为在零售环境中会有相当数量的重复订单)。
让我们继续应用这个逻辑,进行前 12 个推荐的预测。
首先,我们将存储所有(不仅仅是最近观看的五部)用户在观看我们尝试预测的电影之前观看过的电影:
historically_watched = []
for i in range(len(user_list)):
     total_user_movies = movie_count[movie_count['User2']==user_list[i]].copy()
     total_user_movies.reset_index(inplace=True)
     total_user_movies = total_user_movies.drop(['index'],axis=1)
     for j in range(total_user_movies.shape[0]-6):
         historically_watched.append(total_user_movies.loc[0:(j+4),'Movies2'].tolist())
在前述代码中,我们过滤出所有用户观看过的电影。
如果用户已经观看过一部电影,我们将该用户-电影组合的概率覆盖为零:
for j in range(pred.shape[0]):
  for i in range(pred.shape[1]):
    pred[j][i]= np.where(i in historically_watched[j], 0 , pred[j][i])
在以下代码中,我们计算测试数据中用户观看的电影是否在前 12 个推荐电影中所占的百分比:
count = 0
for i in range(testX.shape[0]):
  rank = np.argmax(np.argsort(pred[i])[::-1]==np.argmax(testY[i]))
  if rank<12:
    count+=1
count/testX.shape[0]
#12.6
通过前述方法,推荐有效的用户比例从上次迭代的 10.4%上升至 12.6%。
主题建模,使用嵌入
在前面的配方中,我们学习了如何为用户可能观看的电影生成预测。以前生成预测的方式的一个限制是,如果我们没有在电影预测之上进行进一步处理,那么电影推荐的多样性将受到限制。
多样化的推荐非常重要;如果没有多样性,用户只会发现某些类型的产品。
在这个配方中,我们将基于它们的相似性对电影进行分组,并识别电影的共同主题。此外,我们还将探讨如何增加向用户提供的推荐多样性。尽管如此,在电影推荐的具体案例中,这种策略的可行性可能较低,因为与在零售/电子商务设置中产品类别和替代品的数量相比,电影的类别和替代品要少得多。
准备工作
我们将采取的基于相似性分组电影的策略如下:
- 
从我们在电影推荐配方中构建的模型中提取每部电影的嵌入值 - 
我们还可以使用 gensim 为每部电影创建嵌入 
- 
用户观看的所有电影可以被视为句子中的单词 
- 
创建一个由形成句子的单词 ID 列表的列表 
- 
通过 gensim 的 Word2Vec方法传递列表列表,以提取单词向量(电影 ID 向量)
 
- 
- 
将电影的嵌入值(向量)通过 k-means 聚类过程,提取出若干个簇 
- 
确定最优簇的数量 
- 
在每个簇中识别高概率购买的产品(在历史上未购买的产品中),并根据它们的概率重新排名产品 
- 
推荐前 n 个产品 
在此过程中,一个变量是要形成的簇的数量。簇的数量越多,每个簇中的产品越少,同时,同一簇内的每个产品之间的相似性越大。基本上,数据点数量与同一簇内数据点相似性之间存在权衡。
通过计算所有点相对于它们的聚类中心的平方距离之和,我们可以得出一组内部点相似性的度量。惯性度量不会显著减少的簇的数量是最优簇的数量。
如何做到这一点...
现在我们已经形成了在我们的推荐中获取各种产品的策略,让我们编写代码(我们将从 电影推荐 配方的第 3 步继续)。 代码文件在 GitHub 中可作为 Chapter_12_Recommender_systems.ipynb 获得。
- 
使用 Word2Vec提取每部电影的嵌入值。- 创建所有用户观看的各种电影列表的列表:
 
user_list = movie_count['User2'].unique()
user_movies = []
for i in range(len(user_list)):
     total_user_movies = movie_count[movie_count['User2']==user_list[i]].copy()
     total_user_movies.reset_index(inplace=True)
     total_user_movies = total_user_movies.drop(['index'],axis=1)
     total_user_movies['Movies3'] = total_user_movies['Movies2'].astype(str)
     user_movies.append(total_user_movies['Movies3'].tolist())
在前述代码中,我们过滤了所有用户观看的电影,并创建了所有用户观看的电影列表。
- 提取每部电影的词向量:
from gensim.models import Word2Vec
w2v_model = Word2Vec(user_movies,size=100,window=5,min_count=5, iter = 500)
- 提取电影的TSNE值,以便对我们在上一阶段提取的电影词嵌入进行可视化表示:
from sklearn.manifold import TSNE
tsne_model = TSNE(n_components=2, verbose=1, random_state=0)
tsne_img_label = tsne_model.fit_transform(w2v_model.wv.syn0)
tsne_df = pd.DataFrame(tsne_img_label, columns=['x', 'y'])
tsne_df['image_label'] = list(w2v_model.wv.vocab.keys())
from ggplot import *
chart = ggplot(tsne_df, aes(x='x', y='y'))+geom_point(size=70,alpha=0.5)
chart
2D 空间中嵌入的可视化如下所示:

从前面的输出中,我们可以看到有些电影被分组在一起(这些区域比较密集)。
- 将电影 ID 和电影索引值存储在数据框中:
idx2movie = pd.DataFrame([idx2moviesid.keys(), idx2moviesid.values()]).T
idx2movie.columns = ['image_label','movieId']
- 合并tsne_df和idx2movie数据集,这样我们就能在一个单独的数据框中得到所有的值:
tsne_df['image_label'] = tsne_df['image_label'].astype(int)
tsne_df2 = pd.merge(tsne_df, idx2movie, on='image_label', how='right')
- 导入movies数据集:
movies = pd.read_csv('...') # Path to movies dataset
- 将TSNE数据集与电影数据合并,并删除不需要的列:
tsne_df3 = pd.merge(tsne_df2, movies, left_on='movieId', right_on = 0, how='inner')
tsne_df4 = tsne_df3.drop([2,3,4],axis=1)
tsne_df4.rename(columns={1:'movie_name'}, inplace=True)
- 排除包含 NaN 值的行(由于某些电影出现的频率较低,导致Word2Vec没有为这些稀有词提供词向量(由于min_count参数):
tsne_df5 = tsne_df4.loc[~np.isnan(tsne_df4['x']),]
- 通过了解惯性变化(所有点到各自聚类中心的总平方距离)来确定最佳聚类数量:
X = tsne_df5.loc[:,['x','y']]
inertia = []
for i in range(10):
      km = KMeans((i+1)*10)
      km.fit(X)
      inertia.append(km.inertia_)
import matplotlib.pyplot as plt
%matplotlib inline
plt.plot((np.arange(10)+1)*10,inertia)
plt.title('Variation of inertia over different number of clusters')
plt.xlabel('Number of clusters')
plt.ylabel('Inertia')
不同数量聚类的惯性变化如下:

从前面的曲线来看,我们可以看到,当聚类数量超过40时,惯性下降的幅度没有那么大。因此,我们将40定为我们数据集中电影的最佳聚类数量。
- 通过手动检查某些落入同一聚类的电影来验证聚类结果,检查这些电影是否合理地分在同一聚类:
km = KMeans(40)
km.fit(X)
tsne_df5['clusterlabel'] = km.labelstsne_df5[tsne_df5['cluster_label']==0].head()
一旦执行代码,你会注意到位于cluster_label:0的电影主要是浪漫和喜剧类型的电影。
- 移除用户已观看的电影:
for j in range(pred.shape[0]):
     for i in range(pred.shape[1]):
         pred[j][i]= np.where(i in historically_watched[j], 0 , pred[j][i])
- 对于每个用户,映射电影的概率和电影所属的聚类编号,以便我们可以为每个用户提取该聚类中概率最高的电影。然后从不同聚类中提取前 12 部电影进行推荐:
movie_cluster_id = tsne_df5[['image_label','cluster_label']]
count = 0
for j in range(pred.shape[0]):
      t = movie_cluster_id.copy()
      t['pred']=pred[j,list(movie_cluster_id['image_label'])]
      t2= t.sort_values(by='pred',ascending=False).groupby('cluster_label').first().reset_index()
      t3 = t2.sort_values(by='pred',ascending=False).reset_index()
      final_top_preds = t3.loc[:11]['image_label'].values
      if (np.argmax(testY[j]) in final_top_preds):
            count+=1
前述结果显示,13.6%的用户观看了推荐给他们的电影。
虽然前述结果仅比没有任何推荐多样性的 12.6%结果稍好,但考虑到不仅是下一次购买,而是所有用户未来的购买,拥有多样性的推荐更可能带来更好的效果。
还有更多...
尽管我们已经考虑了为用户生成预测,并提高推荐多样性,但我们还可以通过以下方法进一步改进结果:
- 
融入用户不喜欢的电影信息 
- 
融入用户的人口统计信息 
- 
融入有关电影的详细信息,例如上映年份和演员阵容 
预测股票价格的价值
专家们进行的技术分析有很多种,用以提出股票的买卖建议。大多数技术分析依赖于历史模式,假设历史会重复,只要我们对某些事件进行标准化处理。
鉴于我们到目前为止所做的也是通过考虑历史来做决策的,让我们继续应用我们迄今为止学到的技巧来预测股票价格。
然而,在依赖算法分析做出买卖决策时,如股票价格预测,务必小心。与其他方法的区别在于,其他方法中的决策是可逆的(例如:如果生成的文本不合适,可以撤销),或者是有成本的(糟糕的推荐意味着客户不会再次购买该产品),而股票价格预测中的决策是不可逆的。一旦资金损失,就无法追回。
牢记这一点,让我们继续应用我们迄今为止学到的技巧来预测股票价格。
准备工作
为了预测股票价格,让我们应用两种策略:
- 
仅根据过去五天的股票价格来预测股票价格 
- 
基于过去五天的股票价格和公司最新新闻的结合来预测股票价格 
对于第一次分析,我们可以准备数据集,方式与为 LSTM 准备数据集非常相似;第二次分析则需要不同的数据准备方式,因为它涉及到数值和文本数据。
我们将为上述讨论的两种方法处理数据的方式如下:
- 
仅使用过去五天的股票价格: - 
按照从最旧到最新的日期排序数据集 
- 
以前 5个股票价格作为输入,第六个股票价格作为输出
- 
将其滑动,以便在下一个数据点中,第二到第六个数据点作为输入,第七个数据点作为输出,依此类推,直到我们达到最后一个数据点: - 
这五个数据点作为 LSTM 中的五个时间步骤的输入 
- 
第六个数据点是输出 
 
- 
- 
鉴于我们预测的是一个连续的数字,这次的损失函数将是均方误差值 
 
- 
- 
过去五天的股票价格加上新闻标题、公司数据:对于这个分析,有两种数据预处理方式。虽然过去五天股票价格的数据预处理保持不变,但新闻标题和数据的预处理步骤是此分析中要执行的额外步骤。让我们看看如何将这两者融入到我们的模型中: - 
鉴于这些是两种不同的数据类型,让我们使用两个不同的模型: - 
一个使用历史五天股票价格数据的模型。 
- 
另一个模型,通过增加或减少过去五天股票价格模型的输出,来修改其结果。 
- 
第二个模型来源于新闻头条数据集。假设正面新闻头条更可能提升股票价格,而负面新闻则可能降低股票价格。 
 
- 
- 
为简化问题,假设在预测股票价值的当天,只有最新的新闻头条会影响股票的预测结果 
- 
鉴于我们有两个不同的模型,使用函数式 API 以便我们结合两者因素的影响 
 
- 
如何操作...
我们将解决此问题的方法分为三个部分(代码文件在 GitHub 中以 Chapter_12_stock_price_prediction.ipynb 呈现):
- 
仅基于过去五天的股票价格预测股票价格 - 随机训练和测试集划分的陷阱
 
- 
为较新的股票价格赋予更高的权重 
- 
将过去五天的股票价格与新闻文章头条的文本数据结合 
仅使用过去五天的股票价格
在本菜谱中,我们仅基于过去五个数据点来预测股票价格。在下一个菜谱中,我们将基于新闻和历史数据来预测股票价格:
- 导入相关的包和数据集:
import pandas as pd
data2 = pd.read_csv('/content/stock_data.csv')
- 准备数据集,其中输入为过去五天的股票价格,输出为第六天的股票价格:
x= []
y = []
for i in range(data2.shape[0]-5):
     x.append(data2.loc[i:(i+4)]['Close'].values)
     y.append(data2.loc[i+5]['Close'])
import numpy as np
x = np.array(x)
y = np.array(y)
- 将数据集重新整形为 batch_size、time_steps、features_per_time_step形式:
x = x.reshape(x.shape[0],x.shape[1],1)
- 创建训练集和测试集:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(x, y, test_size=0.30,random_state=10)
- 构建模型:
model = Sequential()
model.add(Dense(100, input_shape = (5,1), activation = 'relu'))
model.add((LSTM(100)))
model.add(Dense(1000,activation='relu'))
model.add(Dense(1,activation='linear'))
model.summary()
模型的总结如下:

- 编译模型以定义 loss函数并调整学习率:
from keras.optimizers import Adam
adam = Adam(lr=0.0001)
model.compile(optimizer=adam, loss='mean_squared_error')
- 拟合模型:
model.fit(X_train, y_train, epochs=400, batch_size=64, validation_data=(X_test, y_test), verbose = 1)
之前的结果表明,在测试数据集上的均方误差值为 $641(每次预测大约 ~$25)。预测股票价格与实际股票价格的对比图如下:
pred = model.predict(X_test)
import matplotlib.pyplot as plt
%matplotlib inline
plt.figure(figsize=(20,10))
plt.plot(y_test,'r')
plt.plot(pred,'--')
plt.title('Variation of actual and predicted stock price')
plt.ylabel('Stock price')
预测值与实际价格的差异如下:

陷阱
现在我们有了相当准确的预测,实际上,预测效果非常好,让我们深入了解这些优秀预测的原因。
在我们的训练数据集中,既有很久以前的数据,也有非常近期的数据。这是一种数据泄漏,因为在构建模型时,我们无法获得未来的股票价格。由于我们的数据构建方式,我们的训练数据集可能包含来自 12 月 20 日的数据,而 12 月 19 日的数据则可能在测试数据集中。
让我们用训练集和测试集按照相应日期重新构建我们的模型:
X_train = x[:2100,:,:]
y_train = y[:2100]
X_test = x[2100:,:,:]
y_test = y[2100:]
在新测试数据集上,我们在 仅使用过去 5 天的股票价格 部分构建的模型输出如下(测试数据集的损失大约为 57,000):

注意,与上一次迭代相比,结果实际与预测的股价图现在更糟。然而,在此部分生成的图表比最近 5 天的股票价格部分获得的图表更具现实情况。
现在我们已经获得了前面的图表,让我们试着理解图表看起来如此的原因,通过检查股价随时间变化的绘图,如下所示:
plt.plot(data2['Close'])
股价随时间变化的图表如下:

注意,股票价格在开始时缓慢上升,并在中间加速,最后减速。
模型由于以下原因表现不佳:
- 
对于早期和最近的预测错误,都赋予了相等的权重。 
- 
我们没有考虑减速趋势。 
分配不同的权重给不同的时间段。
我们了解到,我们将为最近的时间段分配更高的权重,而为历史时间段分配较低的权重。
我们可以如下制定训练weights:
weights = np.arange(X_train.shape[0]).reshape((X_train.shape[0]),1)/2100
前面的代码将最历史数据点的权重分配为0,将最近数据点的权重分配为1。所有中间数据点的权重值将在0到1之间。
现在我们已经定义了weights,让我们定义我们的自定义损失函数,该函数在计算平方误差损失时应用先前初始化的损失:
import numpy as np
from keras.layers import Dense, Input
from keras import Model
import keras.backend as K
from functools import partial
def custom_loss(y_true, y_pred, weights):
     return K.square(K.abs(y_true - y_pred) * weights)
cl = partial(custom_loss, weights=weights_tensor)
现在我们已经初始化了weights并定义了自定义损失函数,让我们使用功能 API 将输入层和权重值提供给模型(我们使用功能 API 因为在训练模型时传递了多个输入):
input_layer = Input(shape=(5,1))
weights_tensor = Input(shape=(1,))
i1 = Dense(100, activation='relu')(input_layer)
i2 = LSTM(100)(i1)
i3 = Dense(1000, activation='relu')(i2)
out = Dense(1, activation='linear')(i3)
model = Model([input_layer, weights_tensor], out)
现在我们已经定义了模型,该模型与最近 5 天的股票价格部分中的参数相同,但还有一个额外的输入,即权重张量。让我们编译我们的模型:
from keras.optimizers import Adam
adam = Adam(lr=0.0001)
model = Model([input_layer, weights_tensor], out)
model.compile(adam, cl)
现在我们已经编译了我们的模型,让我们拟合它:
model.fit(x=[X_train, weights], y=y_train, epochs=300,batch_size = 32, validation_data = ([X_test, test_weights], y_test))
模型在测试数据集上返回了 40,000 的平方误差损失,与陷阱部分的 57,000 损失相比。让我们在测试数据集上绘制预测股价与实际股价的值:

我们现在注意到,在最近的历史记录中(图表的最右侧部分),预测的股价与实际股价之间存在相关性,而图表中间的尖峰未被预测所考虑。
在下一个案例中,让我们看看新闻标题是否能包含中间的尖峰。
最近五天的股票价格加上新闻数据
在下面的代码中,我们将包含来自感兴趣公司的标题的文本数据(从《卫报》网站提供的开放源代码 API 获取),以及过去五天的股价数据。然后我们将结合自定义损失函数,该函数会考虑事件的时效性:
- 从这里导入《卫报》网站的标题数据:open-platform.theguardian.com/(请注意,您需要申请自己的访问密钥,才能从该网站下载数据集)。下载标题及其出现的对应日期,然后预处理日期,使其转换为日期格式:
from bs4 import BeautifulSoup
from bs4 import BeautifulSoup
import urllib, json
dates = []
titles = []
for i in range(100):
 try:
        url = 'https://content.guardianapis.com/search?from-date=2010-01-01§ion=business&page-          size=200&order-by=newest&page='+str(i+1)+'&q=amazon&api-key=0d7'
        response = urllib.request.urlopen(url)
        encoding = response.info().get_content_charset('utf8')
        data = json.loads(response.read().decode(encoding))
        print(i)
        for j in range(len(data['response']['results'])):
              dates.append(data['response']['results'][j]['webPublicationDate'])
              titles.append(data['response']['results'][j]['webTitle']) 
 except:
       break
import pandas as pd
data = pd.DataFrame(dates, titles)
data = data.reset_index()
data.columns = ['title','date']
data['date']=data['date'].str[:10]
data['date']=pd.to_datetime(data['date'], format = '%Y-%m-%d')
data = data.sort_values(by='date')
data_final = data.groupby('date').first().reset_index()
- 通过Date将历史价格数据集和文章标题数据集合并:
data2['Date'] = pd.to_datetime(data2['Date'],format='%Y-%m-%d')
data3 = pd.merge(data2,data_final, left_on = 'Date', right_on = 'date', how='left')
- 对文本数据进行预处理,去除停用词和标点符号,然后像我们在第十一章《构建循环神经网络》中做的那样,对文本输入进行编码:
import nltk
import re
nltk.download('stopwords')
stop = nltk.corpus.stopwords.words('english')
def preprocess(text):
     text = str(text)
     text=text.lower()
     text=re.sub('[⁰-9a-zA-Z]+',' ',text)
     words = text.split()
     words2=[w for w in words if (w not in stop)]
     words3=' '.join(words2)
     return(words3)
data3['title'] = data3['title'].apply(preprocess)
data3['title']=np.where(data3['title'].isnull(),'-','-'+data3['title'])
docs = data3['title'].values
from collections import Counter
counts = Counter()
for i,review in enumerate(docs):
     counts.update(review.split())
words = sorted(counts, key=counts.get, reverse=True)
vocab_size=len(words)
word_to_int = {word: i for i, word in enumerate(words, 1)}
encoded_docs = []
for doc in docs:
     encoded_docs.append([word_to_int[word] for word in doc.split()])
max_length = 20
from keras.preprocessing.sequence import pad_sequences
padded_docs = pad_sequences(encoded_docs, maxlen=max_length,padding='pre')
- 以过去五天的股价和最新的标题(在股价预测日期之前的标题)为输入。让我们预处理数据,获取输入和输出值,然后准备训练集和测试集:
在以下代码中,x1 对应历史股价,x2 对应股价预测日期的文章标题:
x1 = []
x2 = []
y = []
for i in range(data3.shape[0]-5):
     x1.append(data3.loc[i:(i+4)]['Close'].values)
     x2.append(padded_docs[i+5])
     y.append(data3.loc[i+5]['Close'])
x1 = np.array(x1)
x2 = np.array(x2)
y = np.array(y)
x1 = x1.reshape(x1.shape[0],x1.shape[1],1)
X1_train = x1[:2100,:,:]
X2_train = x2[:2100,:]
y_train = y[:2100]
X1_test = x1[2100:,:,:]
X2_test = x2[2100:,:]
y_test = y[2100:]
- 鉴于我们将多个变量作为输入(历史股价、编码的文本数据和权重值),我们将使用函数式 API 来构建模型:
input1 = Input(shape=(20,))
model = Embedding(input_dim=vocab_size+1, output_dim=32, input_length=20)(input1)
model = (LSTM(units=100))(model)
model = (Dense(1, activation='tanh'))(model)
input2 = Input(shape=(5,1))
model2 = Dense(100, activation='relu')(input2)
model2 = LSTM(units=100)(model2)
model2 = (Dense(1000, activation="relu"))(model2)
model2 = (Dense(1, activation="linear"))(model2)
from keras.layers import multiply
conc = multiply([model, model2])
conc2 = (Dense(1000, activation="relu"))(conc)
out = (Dense(1, activation="linear"))(conc2)
请注意,我们已经将股价模型和文本数据模型的输出值相乘,因为文本数据需要与历史股价模型的输出进行调整:
model = Model([input1, input2, weights_tensor], out)
前述模型的架构如下:

- 定义损失函数并编译模型:
def custom_loss(y_true, y_pred, weights):
 return K.square(K.abs(y_true - y_pred) * weights)
cl = partial(custom_loss, weights=weights_tensor)
model = Model([input1, input2, weights_tensor], out)
model.compile(adam, cl)
- 训练模型:
model.fit(x=[X2_train, X1_train, weights], y=y_train, epochs=300,batch_size = 32, validation_data = ([X2_test, X1_test, test_weights], y_test))
- 绘制测试集中的实际股价与预测股价的比较图:
pred = model.predict([X2_test, X1_test, test_weights])
import matplotlib.pyplot as plt
%matplotlib inline
plt.figure(figsize=(20,10))
plt.plot(y_test,'r',label='actual')
plt.plot(pred,'--', label = 'predicted')
plt.title('Variation of actual and predicted stock price')
plt.ylabel('Stock price')
plt.legend()
实际股价与预测股价的变化如下:

请注意,在这一轮迭代中,与没有文本数据的版本相比,中间部分的斜率稍好一些,并且平方误差稍低,为 35,000,而前一轮的平方误差为 40,000。
还有更多内容…
如本教程开头所提到的,预测股价时要非常小心,因为股价的波动受多种因素的影响,所有这些因素在预测时都需要考虑进去。
此外,您还应该注意,虽然实际值和预测值看起来相关,但预测值线比实际值线稍有延迟。这种延迟可能会导致从买入决策变为卖出决策的最佳策略发生显著变化。因此,在股价变动显著的前一日期,应给予更大的权重——这进一步复杂化了我们的损失函数。
我们也可以考虑整合更多信息源,如额外的新闻标题和季节性因素(例如:某些股票在假期季节通常表现良好)以及其他宏观经济因素,在进行预测时使用。
最后,我们本可以对数据集进行缩放,以便神经网络的输入不是一个巨大的数字。
第十三章:序列到序列学习
在前面的章节中,我们学习了 RNN 的应用,其中有多个输入(每个时间步长一个输入)和一个输出。然而,还有一些应用场景涉及多个输入以及多个时间步长——例如机器翻译,其中源句子有多个输入词,目标句子有多个输出词。鉴于有多个输入和多个输出,这就变成了一个多输出的基于 RNN 的应用——本质上是一个序列到序列的学习任务。这要求我们在构建模型架构时采用不同于目前为止的方式,这将在本章中进行讲解。本章将涵盖以下内容:
- 
从网络返回序列 
- 
双向 LSTM 如何帮助命名实体提取 
- 
提取意图和实体以构建聊天机器人 
- 
编码器-解码器网络架构的工作原理 
- 
使用编码器-解码器架构将英语句子翻译成法语 
- 
通过使用注意力机制改善翻译结果 
介绍
在前面的章节中,我们了解到 LSTM,甚至是 RNN,是从最后一个时间步长返回结果的(最后一个时间步长的隐藏状态值会传递给下一层)。假设输出是五维的,其中五个维度是五个输出(而不是五个类别的 softmax 值)。为了进一步解释这个想法,假设我们不仅预测下一天的股价,而是预测未来五天的股价。或者,我们不仅预测下一个词,而是预测给定输入序列的下一个五个词的序列。
这种情况需要采用不同的方法来构建网络。在接下来的部分,我们将探讨构建网络的多个场景,以便在不同时间步长中提取输出结果。
场景 1:命名实体提取
在命名实体提取中,我们试图为句子中的每个词分配一个标签——判断它是否与人名、地名相关。因此,这变成了输入词与其是否为人名或地名的输出类别之间的一对一映射问题。尽管输入和输出之间是一个一对一的映射,但在某些情况下,周围的词语会在决定某个输入是否是命名实体时起到作用。例如,单独的new可能不是命名实体,但如果new与york一起出现,那么我们就知道它是一个命名实体。因此,这是一个问题,输入的时间步长在决定一个词是否是命名实体时起到了作用,即使在大多数情况下,输入和输出之间可能会存在一对一的映射。
此外,这是一个序列返回问题,因为我们是基于输入的词序列来判断命名实体是否存在。鉴于此,这是一个输入与输出之间一一对应的关系问题,且相邻时间步的输入在决定输出时起着关键作用。只要确保时间步中两个方向的词都能影响输出,传统的 LSTM 就可以工作。因此,双向 LSTM 在解决这种问题时非常有用。
双向 LSTM 的架构如下所示:

请注意,在前面的图示中,我们通过使输入之间相互连接并反向流动,修改了传统的 LSTM,从而确保信息能够从两个方向流动。我们将在后续部分学习更多关于双向 LSTM 如何工作的内容,以及如何应用它。
场景 2:文本摘要
文本摘要任务需要不同于我们之前讨论的架构,因为我们通常需要在阅读完整个输入句子(本例中的输入文本/评论)后,才能生成摘要。
这要求将所有输入编码为一个向量,然后基于输入的编码向量生成输出。此外,鉴于给定文本中的一系列词可能有多个输出(多个词),这就变成了一个多输出生成问题,因此,另一个可以利用 RNN 的多输入多输出特性的场景也随之而来。
让我们来看一下如何构建模型来得出解决方案:

请注意,在前述架构中,我们将所有输入文本编码为输入序列的最后一个词生成的向量,然后将该编码向量作为输入传递给解码器序列。本章后续部分将提供更多关于如何构建此网络的信息。
场景 3:机器翻译
在前面的场景中,我们将输入编码为一个向量,并希望该向量也能包含词序信息。但是,如果我们通过网络显式提供一种机制,让网络能够根据我们正在解码的词的位置,给输入词在给定位置上的不同加权,该怎么办呢?例如,如果源语言和目标语言的词对齐方式相似,也就是说,两种语言的词序相似,那么源语言开头的词对目标语言最后一个词的影响很小,但对决定目标语言第一个词的影响却很大。
注意力机制如下所示:

请注意,注意力向量(中间部分)受输入编码向量和输出值的隐藏状态的影响。更多关于如何利用注意力机制的内容将在后文中讨论。
通过理解不同编码器-解码器架构的原因,让我们深入了解如何在 Keras 中生成输出序列。
从网络中返回输出序列
正如我们在上一节中讨论的那样,生成输出序列的网络架构有多种方式。在本节中,我们将学习如何通过编码器-解码器方式生成输出,并且通过一个玩具数据集学习输入到输出的逐一映射网络,以便更好地理解这一过程。
让我们定义一个输入序列和一个对应的输出序列,如下所示(代码文件可在 GitHub 上的Return_state_and_sequences_working_details.ipynb找到):
input_data = np.array([[1,2],[3,4]])
output_data = np.array([[3,4],[5,6]])
我们可以看到,输入中有两个时间步,并且每个输入都有相应的输出。
如果我们用传统的方法来解决这个问题,我们会像下面的代码那样定义模型架构。请注意,我们使用的是函数式 API,因为在后续的场景中,我们将提取多个输出,并检查中间层:
# define model
inputs1 = Input(shape=(2,1))
lstm1 = LSTM(1, activation = 'tanh', return_sequences=False,recurrent_initializer='Zeros',recurrent_activation='sigmoid')(inputs1)
out= Dense(2, activation='linear')(lstm1)
model = Model(inputs=inputs1, outputs=out)
model.summary()

请注意,在上述场景中,LSTM 接收的数据形状是(batch_size,时间步,时间步每个特征)。由于 LSTM 不返回一系列输出,LSTM 的输出是隐藏层中的一个值(因为 LSTM 的单元数为 1)。
由于输出是二维的,我们将添加一个全连接层,该层接受隐藏层的输出并从中提取2个值。
让我们开始拟合模型,如下所示:
model.compile(optimizer='adam',loss='mean_squared_error')
model.fit(input_data.reshape(2,2,1), output_data,epochs=1000)
print(model.predict(input_data[0].reshape(1,2,1)))
# [[2.079641 1.8290598]]
现在我们有了输出,让我们像上一章那样验证结果(注意,这段代码与上一章中的完全相同——它的解释已经在第十一章的从头开始构建 LSTM一节中提供)。
input_t0 = 1
cell_state0 = 0
forget0 = input_t0*model.get_weights()[0][0][1] + model.get_weights()[2][1]
forget1 = 1/(1+np.exp(-(forget0)))
cell_state1 = forget1 * cell_state0
input_t0_1 = input_t0*model.get_weights()[0][0][0] + model.get_weights()[2][0]
input_t0_2 = 1/(1+np.exp(-(input_t0_1)))
input_t0_cell1 = input_t0*model.get_weights()[0][0][2] + model.get_weights()[2][2]
input_t0_cell2 = np.tanh(input_t0_cell1)
input_t0_cell3 = input_t0_cell2*input_t0_2
input_t0_cell4 = input_t0_cell3 + cell_state1
output_t0_1 = input_t0*model.get_weights()[0][0][3] + model.get_weights()[2][3]
output_t0_2 = 1/(1+np.exp(-output_t0_1))
hidden_layer_1 = np.tanh(input_t0_cell4)*output_t0_2
input_t1 = 2
cell_state1 = input_t0_cell4
forget21 = hidden_layer_1*model.get_weights()[1][0][1] + model.get_weights()[2][1] + input_t1*model.get_weights()[0][0][1]
forget_22 = 1/(1+np.exp(-(forget21)))
cell_state2 = cell_state1 * forget_22
input_t1_1 = input_t1*model.get_weights()[0][0][0] + model.get_weights()[2][0] + hidden_layer_1*model.get_weights()[1][0][0]
input_t1_2 = 1/(1+np.exp(-(input_t1_1)))
input_t1_cell1 = input_t1*model.get_weights()[0][0][2] + model.get_weights()[2][2]+ hidden_layer_1*model.get_weights()[1][0][2]
input_t1_cell2 = np.tanh(input_t1_cell1)
input_t1_cell3 = input_t1_cell2*input_t1_2
input_t1_cell4 = input_t1_cell3 + cell_state2
output_t1_1 = input_t1*model.get_weights()[0][0][3] + model.get_weights()[2][3]+ hidden_layer_1*model.get_weights()[1][0][3]
output_t1_2 = 1/(1+np.exp(-output_t1_1))
hidden_layer_2 = np.tanh(input_t1_cell4)*output_t1_2
final_output = hidden_layer_2 * model.get_weights()[3][0] + model.get_weights()[4]
final_output的输出如下:
[[2.079 1.829]]
你应该注意到,前面生成的final_output与我们在model.predict输出中看到的是完全相同的。
通过这种方式生成输出的一个缺点是,在时间步 1的输出显然不依赖于时间步 2的情况下,我们使得模型很难找到将时间步 2的值对时间步 1的影响隔离开来的方法,因为我们正在获取时间步 2的隐藏层输出(它是时间步 1和时间步 2输入值的组合)。
我们可以通过从每个时间步提取隐藏层值,然后将其传递到全连接层来解决这个问题。
返回每个时间步隐藏层值的序列
在接下来的代码中,我们将了解如何返回每个时间步的隐藏层值序列:
# define model
inputs1 = Input(shape=(2,1))
lstm1 = LSTM(1, activation = 'tanh', return_sequences=False,recurrent_initializer='Zeros',recurrent_activation='sigmoid')(inputs1)
out= Dense(1, activation='linear')(lstm1)
model = Model(inputs=inputs1, outputs=out)
model.summary()
注意我们所做的两个代码更改如下:
- 
将 return_sequences参数的值更改为True
- 
给定输出为 1的全连接层:

注意,因为我们提取了每个时间步的隐藏层值(其中隐藏层只有一个单元),所以 LSTM 的输出形状是(批量大小,时间步,1)。
此外,由于有一个全连接层将 LSTM 的输出连接到每个时间步的最终输出,因此输出形状保持不变。
让我们继续训练模型,如下所示:
model.compile(optimizer='adam',loss='mean_squared_error')
model.fit(input_data.reshape(2,2,1), output_data.reshape(2,2,1),epochs=1000)
预测值如下所示:
print(model.predict(input_data[0].reshape(1,2,1)))
前面的执行将给出以下输出:
[[[1.7584195] [2.2500749]]]
与前一部分类似,我们将通过输入通过权重进行前向传播,然后匹配我们的预测值来验证结果。
我们将提取第一个时间步的输出,如下所示:
input_t0 = 1
cell_state0 = 0
forget0 = input_t0*model.get_weights()[0][0][1] + model.get_weights()[2][1]
forget1 = 1/(1+np.exp(-(forget0)))
cell_state1 = forget1 * cell_state0
input_t0_1 = input_t0*model.get_weights()[0][0][0] + model.get_weights()[2][0]
input_t0_2 = 1/(1+np.exp(-(input_t0_1)))
input_t0_cell1 = input_t0*model.get_weights()[0][0][2] + model.get_weights()[2][2]
input_t0_cell2 = np.tanh(input_t0_cell1)
input_t0_cell3 = input_t0_cell2*input_t0_2
input_t0_cell4 = input_t0_cell3 + cell_state1
output_t0_1 = input_t0*model.get_weights()[0][0][3] + model.get_weights()[2][3]
output_t0_2 = 1/(1+np.exp(-output_t0_1))
hidden_layer_1 = np.tanh(input_t0_cell4)*output_t0_2
final_output_1 = hidden_layer_1 * model.get_weights()[3][0] + model.get_weights()[4]
final_output_1
*# 1.7584*
你应该注意到final_output_1值与第一个时间步的预测值相匹配。同样,我们继续验证第二个时间步的预测:
input_t1 = 2
cell_state1 = input_t0_cell4
forget21 = hidden_layer_1*model.get_weights()[1][0][1] + model.get_weights()[2][1] + input_t1*model.get_weights()[0][0][1]
forget_22 = 1/(1+np.exp(-(forget21)))
cell_state2 = cell_state1 * forget_22
input_t1_1 = input_t1*model.get_weights()[0][0][0] + model.get_weights()[2][0] + hidden_layer_1*model.get_weights()[1][0][0]
input_t1_2 = 1/(1+np.exp(-(input_t1_1)))
input_t1_cell1 = input_t1*model.get_weights()[0][0][2] + model.get_weights()[2][2]+ hidden_layer_1*model.get_weights()[1][0][2]
input_t1_cell2 = np.tanh(input_t1_cell1)
input_t1_cell3 = input_t1_cell2*input_t1_2
input_t1_cell4 = input_t1_cell3 + cell_state2
output_t1_1 = input_t1*model.get_weights()[0][0][3] + model.get_weights()[2][3]+ hidden_layer_1*model.get_weights()[1][0][3]
output_t1_2 = 1/(1+np.exp(-output_t1_1))
hidden_layer_2 = np.tanh(input_t1_cell4)*output_t1_2
final_output_2 = hidden_layer_2 * model.get_weights()[3][0] + model.get_weights()[4]
final_output_2
*# 2.250*
你应该注意到,这会返回与第二个时间步的model.predict值完全相同的结果。
现在我们理解了网络中的return_sequences参数,让我们继续学习另一个参数——return_state。我们知道网络的两个输出是隐藏层值(当return_sequences为False时,它也是 LSTM 在最终时间步的输出,而当return_sequences为True时,它是 LSTM 在每个时间步的输出)和细胞状态值。
return_state有助于提取网络的细胞状态值。
提取细胞状态对于将输入文本编码为向量时很有用,我们不仅会传递编码向量,还会将输入编码器的最终细胞状态传递给解码器网络(更多内容请见 机器翻译的编码器解码器架构 部分)。
在接下来的部分,我们来了解return_state是如何工作的。请注意,这只是为了帮助我们理解每个时间步的细胞状态是如何生成的,因为实际上我们会将此步骤的输出(隐藏层值和细胞状态值)作为输入传递给解码器:
inputs1 = Input(shape=(2,1))
lstm1,state_h,state_c = LSTM(1, activation = 'tanh', return_sequences=True, return_state = True, recurrent_initializer='Zeros',recurrent_activation='sigmoid')(inputs1)
model = Model(inputs=inputs1, outputs=[lstm1, state_h, state_c])
在前面的代码中,我们同样将return_state参数设置为True。注意现在 LSTM 的输出:
- 
lstm1:每个时间步的隐藏层(因为在前面的情境中,return_sequences为True)
- 
state_h:最终时间步的隐藏层值
- 
state_c:最终时间步的细胞状态值
让我们继续预测值,如下所示:
print(model.predict(input_data[0].reshape(1,2,1)))
我们将得到以下值:
[array([[[-0.256911 ], [-0.6683883]]], dtype=float32), array([[-0.6683883]], dtype=float32), array([[-0.96862674]], dtype=float32)]
您应该会看到三个输出数组,正如我们之前讨论的:隐藏层值序列、最终隐藏层值,以及按顺序排列的单元状态值。
让我们验证之前得到的数字:
input_t0 = 1
cell_state0 = 0
forget0 = input_t0*model.get_weights()[0][0][1] + model.get_weights()[2][1]
forget1 = 1/(1+np.exp(-(forget0)))
cell_state1 = forget1 * cell_state0
input_t0_1 = input_t0*model.get_weights()[0][0][0] + model.get_weights()[2][0]
input_t0_2 = 1/(1+np.exp(-(input_t0_1)))
input_t0_cell1 = input_t0*model.get_weights()[0][0][2] + model.get_weights()[2][2]
input_t0_cell2 = np.tanh(input_t0_cell1)
input_t0_cell3 = input_t0_cell2*input_t0_2
input_t0_cell4 = input_t0_cell3 + cell_state1
output_t0_1 = input_t0*model.get_weights()[0][0][3] + model.get_weights()[2][3]
output_t0_2 = 1/(1+np.exp(-output_t0_1))
hidden_layer_1 = np.tanh(input_t0_cell4)*output_t0_2
print(hidden_layer_1)
前述计算中的hidden_layer_1值为-0.2569,这是我们从model.predict方法中获得的值:
input_t1 = 2
cell_state1 = input_t0_cell4
forget21 = hidden_layer_1*model.get_weights()[1][0][1] + model.get_weights()[2][1] + input_t1*model.get_weights()[0][0][1]
forget_22 = 1/(1+np.exp(-(forget21)))
cell_state2 = cell_state1 * forget_22
input_t1_1 = input_t1*model.get_weights()[0][0][0] + model.get_weights()[2][0] + hidden_layer_1*model.get_weights()[1][0][0]
input_t1_2 = 1/(1+np.exp(-(input_t1_1)))
input_t1_cell1 = input_t1*model.get_weights()[0][0][2] + model.get_weights()[2][2]+ hidden_layer_1*model.get_weights()[1][0][2]
input_t1_cell2 = np.tanh(input_t1_cell1)
input_t1_cell3 = input_t1_cell2*input_t1_2
input_t1_cell4 = input_t1_cell3 + cell_state2
output_t1_1 = input_t1*model.get_weights()[0][0][3] + model.get_weights()[2][3]+ hidden_layer_1*model.get_weights()[1][0][3]
output_t1_2 = 1/(1+np.exp(-output_t1_1))
hidden_layer_2 = np.tanh(input_t1_cell4)*output_t1_2
print(hidden_layer_2, input_t1_cell4)
hidden_layer_2和input_t1_cell4的值分别是-0.6683和-0.9686。
您会注意到,输出与我们在predict函数中看到的完全相同。
在双向网络的情况下,我们在计算时从两个方向同时引入隐藏层值,代码如下:
inputs1 = Input(shape=(2,1))
lstm1,state_fh,state_fc,state_bh,state_bc = Bidirectional(LSTM(1, activation = 'tanh', return_sequences=True, return_state = True, recurrent_initializer='Zeros',recurrent_activation='sigmoid'))(inputs1)
model = Model(inputs=inputs1, outputs=[lstm1, state_fh,state_fc,state_bh,state_bc])
model.summary()
请注意,在双向 LSTM 中,最终的隐藏状态有两个输出,一个是从左到右考虑输入时间步长时的输出,另一个是从右到左考虑输入时间步长时的输出。以类似的方式,我们也有两个可能的单元状态值。
通常,我们会将得到的隐藏状态连接成一个单一的向量,并将单元状态也连接成另一个单一的向量。
为简洁起见,本书中不对双向 LSTM 的输出进行验证。不过,您可以在本章附带的 Jupyter Notebook 中查看相关验证。
构建聊天机器人
在某些场景下,聊天机器人非常有用,尤其是当机器人能够自动处理一些常见查询时。这在实际场景中非常有用,尤其是在你只需要从数据库中查找结果,或查询 API 以获得与查询相关的结果时。
基于此,您可以设计聊天机器人的两种潜在方式,如下所示:
- 
将非结构化的用户查询转换为结构化格式: - 根据转换后的结构从数据库查询
 
- 
根据输入文本生成回应 
在本次练习中,我们将采用第一种方法,因为它更可能提供可以在呈现给用户之前进一步调整的预测结果。此外,我们还将了解为什么在机器翻译和文本摘要案例研究后,可能不希望根据输入文本生成回应。
将用户查询转换为结构化格式涉及以下两个步骤:
- 
为查询中的每个单词分配实体 
- 
理解查询的意图 
命名实体识别是一个应用广泛的技术,适用于多个行业。例如,用户想去哪里旅行?用户考虑购买哪个产品?等等。从这些例子中,我们可能会认为命名实体识别只是从现有城市名称或产品名称的字典中进行简单查找。然而,考虑一种情境,当用户说“我想从波士顿到西雅图”时,机器虽然知道波士顿和西雅图是城市名,但我们无法判断哪个是from城市,哪个是to城市。
尽管我们可以添加一些启发式规则,比如在“to”前面有名字的是to city,另一个是from city,但在多个类似示例中复制这个过程时它并不可扩展。神经网络在这种情况下非常有用,因为我们不再依赖手动调整特征。我们将让机器处理特征工程的部分,以提供输出。
准备就绪
基于前述的直觉,让我们继续定义解决这个问题的方法,假设数据集包含与航空公司相关的用户查询。
目标:从查询中提取各种实体,同时提取查询的意图。
方法:
- 
我们将找到一个数据集,其中包含查询标签和每个查询单词所属的实体: - 如果没有标注数据集,我们将手动标注查询中的实体,对于合理数量的示例进行标注,以便训练我们的模型。
 
- 
考虑到周围的词汇可能会影响给定单词分类为某一类别的结果,让我们使用基于 RNN 的技术来解决这个问题。 
- 
另外,考虑到周围的单词可能位于给定单词的左侧或右侧,我们将使用双向 RNN 来解决这个问题。 
- 
预处理输入数据集,以便可以输入到 RNN 的多个时间步中。 
- 
对输出数据集进行一热编码,以便我们可以优化模型。 
- 
构建模型,返回查询中每个单词所对应的实体。 
- 
同样,构建另一个模型,提取查询的意图。 
如何做到...
让我们按照之前定义的方法编写代码,如下所示(代码文件可在 GitHub 上的Intent_and_entity_extraction.ipynb中找到):
- 导入数据集,如以下代码所示:
!wget https://www.dropbox.com/s/qpw1wnmho8v0gi4/atis.zip
!unzip atis.zip
加载训练数据集:
import numpy as np 
import pandas as pd
import pickle
DATA_DIR="/content"
def load_ds(fname='atis.train.pkl'):
     with open(fname, 'rb') as stream:
     ds,dicts = pickle.load(stream)
     print('Done loading: ', fname)
     print(' samples: {:4d}'.format(len(ds['query'])))
     print(' vocab_size: {:4d}'.format(len(dicts['token_ids'])))
     print(' slot count: {:4d}'.format(len(dicts['slot_ids'])))
     print(' intent count: {:4d}'.format(len(dicts['intent_ids'])))
     return ds,dicts
import os
train_ds, dicts = load_ds(os.path.join(DATA_DIR,'atis.train.pkl'))
test_ds, dicts = load_ds(os.path.join(DATA_DIR,'atis.test.pkl'))
上述代码输出如下:

注意,附加数据集中的样本是用户查询,slot 是单词所属的实体,而 intent 是查询的整体意图。
- 对查询、slot 和 intent 中的每个单词应用 ID:
t2i, s2i, in2i = map(dicts.get, ['token_ids', 'slot_ids','intent_ids'])
i2t, i2s, i2in = map(lambda d: {d[k]:k for k in d.keys()}, [t2i,s2i,in2i])
query, slots, intent = map(train_ds.get, ['query', 'slot_labels', 'intent_labels'])
词汇中的标记(单词)、slot(单词的实体)和 intent 的 ID 示例如下:

最后,查询、槽位和意图被转换为 ID 值,如下所示(我们报告第一个查询、意图和槽位的输出):

查询、意图和与查询中词对应的实体示例如下:
for j in range(len(query[i])):
        print('{:>33} {:>40}'.format(i2t[query[i][j]],
                                     i2s[slots[i][j]]))

查询是前面截图顶部的语句。槽位表示每个词所属的对象类型。请注意,O表示对象,其他每个实体名称都是自描述的。此外,总共有 23 个可能的意图类,它们在总体上描述查询。
在以下代码中,我们将所有数据转换为一个列表的列表,其中每个列表对应数据集中的一个查询:
i2t2 = []
i2s2 = []
c_intent=[]
for i in range(4978):
     a_query = []
     b_slot = []
     c_intent.append(i2in[intent[i][0]])
     for j in range(len(query[i])):
         a_query.append(i2t[query[i][j]])
         b_slot.append(i2s[slots[i][j]])
     i2t2.append(a_query)
     i2s2.append(b_slot)
i2t2 = np.array(i2t2)
i2s2 = np.array(i2s2)
i2in2 = np.array(c_intent)
一些令牌、意图和查询的示例如下:

- 创建索引化的输入和输出:
final_sentences = []
final_targets = []
final_docs = []
for i in range(len(i2t2)):
  tokens = ''
  entities = ''
  intent = ''
  for j in range(len(i2t2[i])):
    tokens= tokens + i2t2[i][j] + ' '
    entities = entities + i2s2[i][j] +' '
  intent = i2in2[i]
  final_sentences.append(tokens)
  final_targets.append(entities)
  final_docs.append(intent)
前面的代码为我们提供了最终查询和目标的列表,如下所示:

现在,我们将每个输入句子转换为其组成词的对应 ID 列表:
from collections import Counter
counts = Counter()
for i,sentence in enumerate(final_sentences):
     counts.update(sentence.split())
sentence_words = sorted(counts, key=counts.get, reverse=True)
chars = sentence_words
nb_chars = len(chars)
sentence_word_to_int = {word: i for i, word in enumerate(sentence_words, 1)}
sentence_int_to_word = {i: word for i, word in enumerate(sentence_words, 1)}
mapped_reviews = []
for review in final_sentences:
     mapped_reviews.append([sentence_word_to_int[word] for word in review.split()])
在以下代码中,我们将每个输出词转换为其组成的词 ID:
from collections import Counter
counts = Counter()
for i,sentence in enumerate(final_targets):
    counts.update(sentence.split())
target_words = sorted(counts, key=counts.get, reverse=True)
chars = target_words
nb_chars = len(target_words)
target_word_to_int = {word: i for i, word in enumerate(target_words, 1)}
target_int_to_word = {i: word for i, word in enumerate(target_words, 1)}
mapped_targets = []
for review in final_targets:
    mapped_targets.append([target_word_to_int[word] for word in review.split()])
- 填充输入并对输出进行独热编码:
from keras.preprocessing.sequence import pad_sequences
y = pad_sequences(maxlen=124, sequences=mapped_targets, padding="post", value=0)
from keras.utils import to_categorical
y2 = [to_categorical(i, num_classes=124) for i in y]
y3 = np.array(y2)
在以下代码中,我们决定在填充输入之前查询的最大长度:
length_sent = []
for i in range(len(mapped_reviews)):
     a = mapped_reviews[i]
     b = len(a)
     length_sent.append(b)
np.max(length_sent)
在前面的代码中,我们决定在填充输入之前查询的最大长度——这恰好是48。
在以下代码中,我们使用最大长度为50来填充输入和输出,因为没有输入查询的长度超过48个词(即max(length_sent)):
from keras.preprocessing.sequence import pad_sequences
X = pad_sequences(maxlen=50, sequences=mapped_reviews, padding="post", value=0)
Y = pad_sequences(maxlen=50, sequences=mapped_targets, padding="post", value=0)
在以下代码中,我们将输出转换为独热编码版本:
from keras.utils import to_categorical
y2 = [to_categorical(i, num_classes=124) for i in Y]
y2 = np.array(y2)
我们总共有124个类,因为总共有123个唯一类,且词汇索引从1开始。
- 构建、训练和测试数据集,以及模型:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X,y2, test_size=0.30,random_state=10)
在前面的代码中,我们将数据集分割为训练集和测试集:
input = Input(shape=(50,))
model = Embedding(input_dim=891, output_dim=32, input_length=50)(input)
model = Dropout(0.1)(model)
model = Bidirectional(LSTM(units=100, return_sequences=True, recurrent_dropout=0.1))(model)
out = (Dense(124, activation="softmax"))(model)
model = Model(input, out)
model.summary()
模型的总结如下:

请注意,在前面的代码中,我们使用了双向 LSTM,因此隐藏层有 200 个单元(因为 LSTM 层有 100 个单元)。
- 编译并拟合模型,如下所示:
model.compile(optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"])
model.fit(X_train,y_train, batch_size=32, epochs=5, validation_data = (X_test,y_test), verbose=1)
前面的代码产生了一个模型,该模型在查询中的每个词上正确识别实体的准确率为 95%:

从前面的输出中,我们可以看到我们为每个词分配正确实体的准确率超过 95%。
意图提取
现在我们已经构建了一个具有良好准确性的模型,能够预测查询中的实体,接下来让我们找出查询的意图。
我们将重用在前一个模型中初始化的大部分变量:
- 将每个查询的意图转换为 ID:
from collections import Counter
counts = Counter()
for i,sentence in enumerate(final_docs):
     counts.update(sentence.split())
intent_words = sorted(counts, key=counts.get, reverse=True)
chars = intent_words
nb_chars = len(intent_words)
intent_word_to_int = {word: i for i, word in enumerate(intent_words, 1)}
intent_int_to_word = {i: word for i, word in enumerate(intent_words, 1)}
mapped_docs = []
for review in final_docs:
     mapped_docs.append([intent_word_to_int[word] for word in review.split()])
- 提取意图的独热编码版本:
from keras.utils import to_categorical
doc2 = [to_categorical(i[0], num_classes=23) for i in mapped_docs]
doc3 = np.array(doc2)
- 构建模型,如以下代码所示:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X,doc3, test_size=0.30,random_state=10)
input = Input(shape=(50,))
model2 = Embedding(input_dim=891, output_dim=32, input_length=50)(input)
model2 = Dropout(0.1)(model2)
model2 = Bidirectional(LSTM(units=100))(model2)
out = (Dense(23, activation="softmax"))(model2)
model2 = Model(input, out)
model2.compile(optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"])
model2.fit(X_train,y_train, batch_size=32, epochs=5, validation_data = (X_test,y_test), verbose=1)
前面的代码结果是一个模型,它在验证数据集上正确识别查询意图的准确率为 90%:

将所有内容整合起来
在上一节中,我们构建了两个模型,第一个模型预测查询中的实体,第二个模型提取查询的意图。
在本节中,我们将定义一个函数,接受查询并将其转换为结构化格式:
- 预处理新的输入文本,以便将其传递给模型:
def preprocessing(text):
     text2 = text.split()
     a=[]
     for i in range(len(text2)):
         a.append(sentence_word_to_int[text2[i]])
     return a
- 预处理输入文本,将其转换为单词 ID 列表:
text = "BOS i would fly from boston to dallas EOS"
indexed_text = preprocessing(text)
padded_text=np.zeros(50)
padded_text[:len(indexed_text)]=indexed_text
padded_text=padded_text.reshape(1,50)
前面的结果处理后的输入文本如下:

现在,我们将预测前面列表的意图:
pred_index_intent = np.argmax(model2.predict(c),axis=1)
entity_int_to_word[pred_index_intent[0]]
前面的代码结果是查询的意图是关于航班的,如下所示:

- 提取查询中与单词相关的实体:
pred_entities = np.argmax(model.predict(padded_text),axis=2)
for i in range(len(pred_entities[0])):
      if pred_entities[0][i]>1:
            print('word: ',text.split()[i], 'entity: ',target_int_to_word[pred_entities[0][i]])

从前面的代码中,我们可以看到模型已经正确地将一个单词分类到正确的实体中。
现在我们已经识别出实体和意图,可以使用预定义的 SQL 查询(或 API),其参数由提取的实体填充,每个意图可能具有不同的 API/SQL 查询来为用户提取信息。
机器翻译
到目前为止,我们已经看到一个输入和输出一一对应的场景。在本节中,我们将探讨如何构建能够将所有输入数据映射到一个向量,然后将其解码为输出向量的架构。
在这个案例研究中,我们将把一段英文输入文本翻译成法语文本。
准备就绪
我们将定义的用于执行机器翻译的架构如下:
- 
获取一个标记化的数据集,其中包含输入句子和对应的法语翻译 
- 
对英文和法语文本中频繁出现的单词进行标记化和提取: - 
为了识别频繁的单词,我们将统计每个单词的频率 
- 
构成所有单词总累计频率前 80% 的单词被认为是频繁单词 
 
- 
- 
对于所有不属于频繁单词的单词,将其替换为未知( unk)符号
- 
为每个单词分配一个 ID 
- 
构建一个编码器 LSTM,提取输入文本的向量 
- 
通过密集层传递编码向量,以便在每个时间步骤提取解码文本的概率 
- 
训练模型以最小化输出的损失 
如何做到这一点...
可能有多种模型架构可以帮助翻译输入文本。我们将在以下章节中介绍其中的一些(代码文件在 GitHub 上可用,名为Machine_translation.ipynb)。
数据预处理
为了将输入和输出数据传递给我们的模型,我们需要像下面这样预处理数据集:
- 导入相关的包和数据集:
import pandas as pd
import numpy as np
import string
from string import digits
import matplotlib.pyplot as plt
%matplotlib inline
import re
from sklearn.model_selection import train_test_split
from keras.models import Model
from keras.layers import Input, LSTM, Dense
import numpy as np
$ wget https://www.dropbox.com/s/2vag8w6yov9c1qz/english%20to%20french.txt
lines= pd.read_table('english to french.txt', names=['eng', 'fr'])
- 鉴于数据集中有超过 140,000 个句子,我们将仅考虑前 50,000 对句子翻译对来构建模型:
lines = lines[0:50000]
- 将输入和输出文本转换为小写并移除标点符号:
lines.eng=lines.eng.apply(lambda x: x.lower())
lines.fr=lines.fr.apply(lambda x: x.lower())
exclude = set(string.punctuation)
lines.eng=lines.eng.apply(lambda x: ''.join(ch for ch in x if ch not in exclude))
lines.fr=lines.fr.apply(lambda x: ''.join(ch for ch in x if ch not in exclude))
- 为输出句子(法语句子)添加开始和结束标记。我们添加这些标记是为了在编码器-解码器架构中起到帮助作用。这个方法的作用将在编码器解码器架构用于机器翻译部分说明:
lines.fr = lines.fr.apply(lambda x : 'start '+ x + ' end')
数据的示例如下所示:

- 识别常见单词。我们定义一个单词为常见,如果它出现在频率构成所有单词总频率 80%的单词列表中:
# fit a tokenizer
from keras.preprocessing.text import Tokenizer
import json
from collections import OrderedDict
def create_tokenizer(lines):
     tokenizer = Tokenizer()
     tokenizer.fit_on_texts(lines)
     return tokenizer
eng_tokenizer = create_tokenizer(lines.eng)
output_dict = json.loads(json.dumps(eng_tokenizer.word_counts))
df =pd.DataFrame([output_dict.keys(), output_dict.values()]).T
df.columns = ['word','count']
df = df.sort_values(by='count',ascending = False)
df['cum_count']=df['count'].cumsum()
df['cum_perc'] = df['cum_count']/df['cum_count'].max()
final_eng_words = df[df['cum_perc']<0.8]['word'].values
前面的代码提取了累计构成输入中 80%总英语单词的英语单词数量:
fr_tokenizer = create_tokenizer(lines.fr)
output_dict = json.loads(json.dumps(fr_tokenizer.word_counts))
df =pd.DataFrame([output_dict.keys(), output_dict.values()]).T
df.columns = ['word','count']
df = df.sort_values(by='count',ascending = False)
df['cum_count']=df['count'].cumsum()
df['cum_perc'] = df['cum_count']/df['cum_count'].max()
final_fr_words = df[df['cum_perc']<0.8]['word'].values
前面的代码提取了累计构成输出中 80%总法语单词的法语单词数量。
- 过滤掉不常见的单词。如果某个单词不在常见单词列表中,我们将用一个未知单词unk来替代它:
def filter_eng_words(x):
     t = []
     x = x.split()
     for i in range(len(x)):
         if x[i] in final_eng_words:
             t.append(x[i])
         else:
             t.append('unk')
     x3 = ''
     for i in range(len(t)):
         x3 = x3+t[i]+' '
     return x3
前面的代码以句子为输入,提取唯一的单词,如果某个单词不在更常见的英语单词(final_eng_words)中,则用unk替代:
def filter_fr_words(x):
     t = []
     x = x.split()
     for i in range(len(x)):
         if x[i] in final_fr_words:
             t.append(x[i])
         else:
             t.append('unk')
     x3 = ''
     for i in range(len(t)):
         x3 = x3+t[i]+' '
     return x3
前面的代码以句子为输入,提取唯一的单词,如果某个单词不在更常见的法语单词(final_fr_words)中,则用unk替代。
例如,在一个包含常见单词和不常见单词的随机句子中,输出结果如下所示:
filter_eng_words('he is extremely good')

lines['fr']=lines['fr'].apply(filter_fr_words)
lines['eng']=lines['eng'].apply(filter_eng_words)
在前面的代码中,我们根据之前定义的函数替换所有的英语和法语句子。
- 
给每个单词在英语(输入)和法语(输出)句子中分配一个 ID: - 存储数据中所有唯一单词的列表(英语和法语句子):
 
all_eng_words=set()
for eng in lines.eng:
     for word in eng.split():
         if word not in all_eng_words:
             all_eng_words.add(word)
all_french_words=set()
for fr in lines.fr:
     for word in fr.split():
         if word not in all_french_words:
             all_french_words.add(word)
input_words = sorted(list(all_eng_words))
target_words = sorted(list(all_french_words))
num_encoder_tokens = len(all_eng_words)
num_decoder_tokens = len(all_french_words)
- 
- 创建输入单词及其对应索引的字典:
 
input_token_index = dict( [(word, i+1) for i, word in enumerate(input_words)])
target_token_index = dict( [(word, i+1) for i, word in enumerate(target_words)])
- 提取输入和目标句子的最大长度,以便所有句子具有相同的大小:
length_list=[]
for l in lines.fr:
     length_list.append(len(l.split(' ')))
fr_max_length = np.max(length_list)
length_list=[]
for l in lines.eng:
     length_list.append(len(l.split(' ')))
eng_max_length = np.max(length_list)
现在我们已经处理好数据集,让我们在数据集上尝试多种架构,比较它们的表现。
传统的多对多架构
在这个架构中,我们将每个输入单词嵌入到一个 128 维的向量中,得到形状为(batch_size, 128, 17)的输出向量。我们这样做是因为在这个版本中,我们希望测试输入数据有 17 个时间步,输出数据集也有 17 个时间步的场景。
我们将通过 LSTM 将每个输入时间步连接到输出时间步,然后对预测结果执行 softmax:
- 创建输入和输出数据集。注意我们有decoder_input_data和decoder_target_data。现在,让我们将decoder_input_data创建为目标句子单词对应的单词 ID。decoder_target_data是目标数据的独热编码版本,包含在start标记后的所有单词:
encoder_input_data = np.zeros((len(lines.eng), fr_max_length),dtype='float32')
decoder_input_data = np.zeros((len(lines.fr), fr_max_length),dtype='float32')
decoder_target_data = np.zeros((len(lines.fr), fr_max_length, num_decoder_tokens+1),dtype='float32')
请注意,我们在num_decodder_tokens中添加了+1,因为在我们在步骤 7b中创建的字典中没有对应于索引0的单词。
for i, (input_text, target_text) in enumerate(zip(lines.eng, lines.fr)):
     for t, word in enumerate(input_text.split()):
         encoder_input_data[i, t] = input_token_index[word]
     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] = target_token_index[word]
         if t>0: 
 # decoder_target_data will be ahead by one timestep
 # and will not include the start character.
             decoder_target_data[i, t - 1, target_token_index[word]] = 1.
         if t== len(target_text.split())-1:
             decoder_target_data[i, t:, 89] = 1
在上面的代码中,我们正在循环遍历输入文本和目标文本,将英语或法语中的句子替换为对应的英语和法语单词 ID。
此外,我们在解码器中对目标数据进行独热编码,以便将其传递给模型。由于现在所有句子具有相同的长度,我们在for循环中将目标数据的值替换为在第 89 个索引处的 1(因为89是结束索引),当句子长度超出时:
for i in range(decoder_input_data.shape[0]):
     for j in range(decoder_input_data.shape[1]):
         if(decoder_input_data[i][j]==0):
             decoder_input_data[i][j] = 89
在上面的代码中,我们将解码器输入数据中零的值替换为 89(因为 89 是结束标记,零在我们创建的单词索引中没有任何单词对应)。
注意我们创建的三个数据集的形状如下:
print(decoder_input_data.shape,encoder_input_data.shape,decoder_target_data.shape)
以下是前面代码的输出:
(50000, 17) (50000, 17) (50000, 17, 359)
- 按照如下方式构建和拟合模型:
model = Sequential()
model.add(Embedding(len(input_words)+1, 128, input_length=fr_max_length, mask_zero=True))
model.add((Bidirectional(LSTM(256, return_sequences = True))))
model.add((LSTM(256, return_sequences=True)))
model.add((Dense(len(target_token_index)+1, activation='softmax')))

model.compile(optimizer='adam', loss='categorical_crossentropy',metrics=['acc'])
model.fit(encoder_input_data, decoder_target_data,
 batch_size=32, epochs=5, validation_split=0.05)

注意,模型输出的准确度可能具有误导性,因为它也将end标记计入准确度衡量中。
- 计算正确翻译的单词数量:
count = 0
correct_count = 0
pred = model2.predict(encoder_input_data[47500:])
for i in range(2500):
  t = np.argmax(pred[i], axis=1)
  act = np.argmax(decoder_target_data[47500],axis=1)
  correct_count += np.sum((act==t) & (act!=89))
  count += np.sum(act!=89)
correct_count/count
# 0.19
在上面的代码中,我们正在对测试数据进行预测(测试数据是总数据集的最后 5%,因为验证集为 5%)。
从前面的代码可以看出,大约 19%的总单词被正确翻译。
多对隐藏到多架构
之前架构的一个缺点是,我们必须人为地将输入的时间步数增加到 17,尽管我们知道输入最大只有八个时间步,其中有一些输入。
在这个架构中,构建一个模型,提取输入的最后时间步的隐藏状态值。此外,它将隐藏状态值复制 17 次(因为输出有 17 个时间步)。它将复制的隐藏时间步通过一个 Dense 层,最终提取输出中的可能类别。让我们按以下方式编写逻辑:
- 重新创建输入和输出数据集,以便输入有 8 个时间步,输出有 17 个时间步。这与之前的迭代不同,因为输入在之前版本中有 17 个时间步,而当前版本中为 8 个:
encoder_input_data = np.zeros(
    (len(lines.eng), eng_max_length),
    dtype='float32')
decoder_input_data = np.zeros(
    (len(lines.fr), fr_max_length),
    dtype='float32')
decoder_target_data = np.zeros(
    (len(lines.fr), fr_max_length, num_decoder_tokens+1),
    dtype='float32')
for i, (input_text, target_text) in enumerate(zip(lines.eng, lines.fr)):
    for t, word in enumerate(input_text.split()):
        encoder_input_data[i, t] = input_token_index[word]
    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] = target_token_index[word]
        if t>0: 
            # decoder_target_data will be ahead by one timestep
            # and will not include the start character.
          decoder_target_data[i, t - 1, target_token_index[word]] = 1.
          if t== len(target_text.split())-1:
            decoder_target_data[i, t:, 89] = 1
for i in range(decoder_input_data.shape[0]):
  for j in range(decoder_input_data.shape[1]):
    if(decoder_input_data[i][j]==0):
      decoder_input_data[i][j] = 89 
- 构建模型。注意,RepeatVector层将双向层的输出复制 17 次:
model2 = Sequential()
model2.add(Embedding(len(input_words)+1, 128, input_length=eng_max_length, mask_zero=True))
model2.add((Bidirectional(LSTM(256))))
model2.add(RepeatVector(fr_max_length))
model2.add((LSTM(256, return_sequences=True)))
model2.add((Dense(len(target_token_index)+1, activation='softmax')))
模型的总结如下:

- 编译并拟合模型:
model2.compile(optimizer='adam', loss='categorical_crossentropy',metrics=['acc'])
model2.fit(encoder_input_data, decoder_target_data,
 batch_size=128,epochs=5,validation_split=0.05)

- 计算总单词中正确翻译的百分比:
count = 0
correct_count = 0
pred = model2.predict(encoder_input_data[47500:])
for i in range(2500):
  t = np.argmax(pred[i], axis=1)
  act = np.argmax(decoder_target_data[47500],axis=1)
  correct_count += np.sum((act==t) & (act!=89))
  count += np.sum(act!=89)
correct_count/count
以上结果准确率为 19%,与之前的迭代几乎相当。
这是可以预期的,因为当所有输入时间步的信息仅存储在最后一个隐藏层的值中时,我们往往会丢失大量信息。
另外,我们没有利用单元状态,该状态包含关于需要在每个时间步忘记哪些信息的相当多的内容。
机器翻译的编码器解码器架构
在我们之前定义的架构中,有两个潜在的逻辑增强:
- 
在生成翻译时,利用单元状态中存在的信息 
- 
在预测下一个单词时,利用之前翻译过的单词作为输入 
第二种技术称为教师强制。本质上,通过在生成当前时间步时,给定前一个时间步的实际值作为输入,我们可以更快地调整网络,且在实践中更加准确。
准备就绪
我们将采用的策略是使用编码器-解码器架构构建机器翻译系统,具体如下:
- 
在准备输入和输出数据集时,我们有两个解码器数据集: - 
decoder_input_data与encoder_input_data的组合为输入,decoder_target_data为输出
- 
decoder_input_data从start单词开始
 
- 
- 
当我们预测解码器中的第一个单词时,我们使用单词输入集,将其转换为向量,然后通过一个以 start为输入的解码器模型。预期的输出是start后面的第一个单词。
- 
我们以类似的方式继续,其中输出的实际第一个单词作为输入,同时预测第二个单词 
- 
我们将基于这个策略计算模型的准确率 
如何操作...
有了这个,我们继续在之前准备好的输入和输出数据集上构建模型(前一部分的第 1 步中,许多到隐藏到许多的架构保持不变)。代码文件可以在 GitHub 上的Machine_translation.ipynb中找到。
- 按如下方式构建模型:
# We shall convert each word into a 128 sized vector
embedding_size = 128
- 
- 准备编码器模型:
 
encoder_inputs = Input(shape=(None,))
en_x= Embedding(num_encoder_tokens+1, embedding_size)(encoder_inputs)
encoder = LSTM(256, return_state=True)
encoder_outputs, state_h, state_c = encoder(en_x)
# We discard `encoder_outputs` and only keep the states.
encoder_states = [state_h, state_c]
请注意,由于我们正在提取编码器网络的中间层,并且将多个数据集作为输入(编码器输入数据和解码器输入数据),因此我们使用的是功能性 API。
- 
- 准备解码器模型:
 
decoder_inputs = Input(shape=(None,))
dex= Embedding(num_decoder_tokens+1, embedding_size)
final_dex= dex(decoder_inputs)
decoder_lstm = LSTM(256, return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_lstm(final_dex, initial_state=encoder_states)
decoder_outputs = Dense(2000,activation='tanh')(decoder_outputs)
decoder_dense = Dense(num_decoder_tokens+1, activation='softmax')
decoder_outputs = decoder_dense(decoder_outputs)
- 按如下方式构建模型:
model3 = Model([encoder_inputs, decoder_inputs], decoder_outputs)
model3.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['acc'])

- 按照以下代码拟合模型:
history3 = model3.fit([encoder_input_data, decoder_input_data], decoder_target_data,
 batch_size=32,epochs=5,validation_split=0.05)

- 计算准确转录的单词百分比:
act = np.argmax(decoder_target_data, axis=2)
count = 0
correct_count = 0
pred = model3.predict([encoder_input_data[47500:],decoder_input_data[47500:]])
for i in range(2500):
     t = np.argmax(pred[i], axis=1)
     correct_count += np.sum((act[47500+i]==t) & (act[47500+i]!=0))
     count += np.sum(decoder_input_data[47500+i]!=0)
correct_count/count
请注意,在此场景下,我们已正确翻译了总词汇的 44%。
然而,请注意,在计算测试数据集的准确性时,我们不应使用decoder_input_data,因为在实际场景中我们无法访问此数据。
这要求我们将上一时间步的预测单词作为当前时间步的解码器输入单词,如下所示。
我们将重新初始化decoder_input_data为decoder_input_data_pred:
decoder_input_data_pred = np.zeros((len(lines.fr),fr_max_length),dtype='float32')
final_pred = []
for i in range(2500):
word = 284
     for j in range(17):
         decoder_input_data_pred[(47500+i), j] = word
         pred =         model3.predict([encoder_input_data[(47500+i)].reshape(1,8),decoder_input_data_pred[47500+i].reshape(1,17)])
         t = np.argmax(pred[0][j])
         word = t
         if word==89:
             break
     final_pred.append(list(decoder_input_data_pred[47500+i]))
请注意,在前面的代码中,单词索引 284 对应起始单词。我们将起始单词作为解码器输入的第一个单词,并预测下一时间步中概率最高的单词。
一旦我们预测出第二个单词,我们就更新decoder_input_word_pred,预测第三个单词,并继续直到遇到停止词。
现在我们已经修改了预测的翻译单词,让我们来计算翻译的准确性:
final_pred2 = np.array(final_pred)
count = 0
correct_count = 0
for i in range(2500):
     correct_count += np.sum((decoder_input_data[47500+i]==final_pred2[i]) & (decoder_input_data[47500+i]!=89))
     count += np.sum(decoder_input_data[47500+i]!=89)
correct_count/count
这样做的结果是,所有单词中有 46%通过此方法被正确翻译。
尽管相较于之前的方法,翻译的准确性有了显著提高,但我们仍未考虑到这样一个直觉:在源语言中位于开头的单词,在目标语言中也很可能位于开头,也就是说,单词的对齐并未被考虑。接下来的部分,我们将探讨如何解决单词对齐的问题。
带有注意力机制的编码器解码器架构用于机器翻译
在上一节中,我们学习了通过启用教师强制技术(即使用目标序列中上一时间步的实际单词作为模型输入)可以提高翻译准确度。
在本节中,我们将进一步扩展这一思路,并根据编码器和解码器向量在每个时间步的相似度为输入编码器分配权重。通过这种方式,我们可以根据解码器的时间步,确保某些单词在编码器的隐藏向量中具有更高的权重。
如何做……
有了这个,让我们看看如何构建编码器解码器架构,并结合注意力机制。代码文件在 GitHub 上的Machine_translation.ipynb中可用。
- 构建编码器,如以下代码所示:
encoder_inputs = Input(shape=(eng_max_length,))
en_x= Embedding(num_encoder_tokens+1, embedding_size)(encoder_inputs)
en_x = Dropout(0.1)(en_x)
encoder = LSTM(256, return_sequences=True, unroll=True)(en_x)
encoder_last = encoder[:,-1,:]
- 构建解码器,如下所示:
decoder_inputs = Input(shape=(fr_max_length,))
dex= Embedding(num_decoder_tokens+1, embedding_size)
decoder= dex(decoder_inputs)
decoder = Dropout(0.1)(decoder)
decoder = LSTM(256, return_sequences=True, unroll=True)(decoder, initial_state=[encoder_last, encoder_last])
请注意,在前面的代码中,我们并没有最终确定解码器架构。我们只是提取了解码器中的隐藏层值。
- 构建注意力机制。注意力机制将基于编码器隐藏向量与解码器隐藏向量在每个时间步的相似度。基于这种相似度(执行 softmax 操作以提供一个加权值,所有可能的输入时间步的加权值总和为 1),我们将给编码器向量赋予权重,如下所示。
将编码器解码器向量通过激活层和密集层处理,以便在进行点积(衡量相似度——余弦相似度)之前实现进一步的非线性:
t = Dense(5000, activation='tanh')(decoder)
t2 = Dense(5000, activation='tanh')(encoder)
attention = dot([t, t2], axes=[2, 2])
确定需要给输入时间步长分配的权重:
attention = Dense(eng_max_length, activation='tanh')(attention)
attention = Activation('softmax')(attention)
计算加权编码器向量,方法如下:
context = dot([attention, encoder], axes = [2,1])
- 将解码器和加权编码器向量结合起来:
decoder_combined_context = concatenate([context, decoder])
- 将解码器和加权编码向量的组合连接到输出层:
output_dict_size = num_decoder_tokens+1
decoder_combined_context=Dense(2000, activation='tanh')(decoder_combined_context)
output=(Dense(output_dict_size, activation="softmax"))(decoder_combined_context)
- 编译并拟合模型,下面是相关代码:
model4 = Model(inputs=[encoder_inputs, decoder_inputs], outputs=[output])
model4.compile(optimizer='adam', loss='categorical_crossentropy',metrics = ['accuracy'])
结构图如下:

model4.fit([encoder_input_data, decoder_input_data], decoder_target_data,
 batch_size=32,epochs=5,validation_split=0.05)

一旦你拟合了模型,你会发现该模型的验证损失略优于之前的迭代。
- 以我们在上一部分所做的类似方式计算翻译的准确率:
decoder_input_data_pred=np.zeros((len(lines.fr), fr_max_length), dtype='float32')
final_pred_att = []
for i in range(2500):
     word = 284
     for j in range(17):
         decoder_input_data_pred[(47500+i), j] = word
         pred =         model4.predict([encoder_input_data[(47500+i)].reshape(1,8),decoder_input_data_pred[47500+i].reshape(1,17)])
         t = np.argmax(pred[0][j])
         word = t
         if word==89:
             break
     final_pred_att.append(list(decoder_input_data_pred[47500+i]))
final_pred2_att = np.array(final_pred_att)
count = 0
correct_count = 0
for i in range(2500):
     correct_count += np.sum((decoder_input_data[47500+i]==final_pred2_att[i]) & (decoder_input_data[47500+i]!=89))
     count += np.sum(decoder_input_data[47500+i]!=89)
correct_count/count
前面的代码结果是 52%的总词汇被正确翻译,相较于上一个迭代有了改进。
- 现在我们已经构建了一个具有合理准确率的翻译系统,让我们检查一下测试数据集中的一些翻译(测试数据集是总数据集的最后 5%,因为我们将validation_split指定为 5%),如下所示:
k = -1500
t = model4.predict([encoder_input_data[k].reshape(1,encoder_input_data.shape[1]),decoder_input_data[k].reshape(1,decoder_input_data.shape[1])]).reshape(decoder_input_data.shape[1], num_decoder_tokens+1)
提取按词汇计算的预测翻译:
t2 = np.argmax(t,axis=1)
for i in range(len(t2)):
     if int(t2[i])!=0:
         print(list(target_token_index.keys())[int(t2[i]-1)])
将英文句子转换为法文后的前面代码输出如下:
je unk manger pas manger end end
提取实际的翻译,以词汇为单位:
t2 = decoder_input_data[k]
for i in range(len(t2)):
     if int(t2[i])!=89:
         print(list(target_token_index.keys())[int(t2[i]-1)])
前面的代码输出如下:
 je unk ne pas manger ça end
我们看到预测的翻译与原始翻译非常接近。以类似的方式,我们来探索验证数据集中的更多翻译:
| 原始翻译 | 预测翻译 | 
|---|---|
| 我在这个未知的周末忙得不可开交 | 我为更多的未知周末忙碌 | 
| 我只是做我所说的未知 | 我做的正是我所做的未知 | 
| 我有做这个未知的周末 | 我做这个未知的周末 | 
从上表中,我们可以看到有一个不错的翻译,然而,仍有一些潜在的改进空间:
- 
考虑到词汇相似性: - 像je和j'ai这样的词汇是相当相似的,因此它们不应该受到过多惩罚,即使这会导致准确度指标的下降
 
- 
减少 unk词汇的数量:- 
我们减少了 unk词汇的数量,以降低数据集的维度
- 
当我们收集更大的语料库,并在工业级配置的机器上工作时,我们可能能够处理高维数据 
 
- 
第十四章:端到端学习
在前几章中,我们学习了如何使用循环神经网络(RNN)分析顺序数据(文本),以及如何使用卷积神经网络(CNN)分析图像数据。
本章将介绍如何使用 CNN + RNN 组合来解决以下案例研究:
- 
手写文本识别 
- 
从图像生成标题 
此外,我们还将学习一种新的损失函数——连接主义时间分类(CTC)损失,用于解决手写文本识别问题。
最后,我们将学习如何使用束搜索(beam search)来为生成的文本提出合理的替代方案,同时解决从图像生成标题的问题。
介绍
假设我们正在转录手写文本的图像。在这种情况下,我们既要处理图像数据,又要处理顺序数据(因为图像中的内容需要顺序转录)。
在传统分析中,我们通常会手工设计解决方案——例如:我们可能会在图像上滑动一个窗口(该窗口的大小大致与一个字符相当),使得窗口能够检测每个字符,然后输出它所检测到的字符,并且具有较高的置信度。
然而,在这种情况下,窗口的大小或我们滑动的窗口数量是由我们手工设计的——这就成为了特征工程(特征生成)问题。
更加端到端的方法是通过将图像传递给 CNN 来提取特征,然后将这些特征作为输入传递给 RNN 的各个时间步,以便在各个时间步提取输出。
因此,我们将使用 CNN 和 RNN 的组合,通过这种方式处理问题,我们不需要构建任何手工设计的特征,而是让模型自行调整 CNN 和 RNN 的最优参数。
连接主义时间分类(CTC)
在手写文本识别或语音转录的监督学习中,传统方法的一大限制是,我们需要提供图像中哪些部分包含某个字符的标签(在手写识别中),或哪个音频子段包含某个音素(多个音素组合形成一个单词发音)。
然而,在构建数据集时,为每个字符在图像中的位置或每个音素在语音转录中的位置提供真实标签是非常昂贵的,因为要转录的数据集可能包含成千上万的单词或数百小时的语音。
CTC 对于解决图像的不同部分与不同字符之间的映射问题非常有用。在这一节中,我们将学习 CTC 损失函数的原理。
解码 CTC
假设我们正在转录一个包含ab文本的图像。这个例子可能看起来像以下任意一种(字符a和b之间的空格不同),而输出标签(地面真相)仍然是相同的ab:

在下一步,我们将这些例子分成多个时间步,如下所示(每个框代表一个时间步):

在前面的例子中,我们总共有六个时间步(每个单元格代表一个时间步)。
我们将从每个时间步预测输出,其中每个时间步的输出是整个词汇表的 softmax。
假设我们正在执行 softmax,让我们看一下ab的第一个图片中每个时间步的输出如下:

请注意,前图中的-表示一个空格。此外,如果图像的特征通过双向 LSTM(或 GRU)传递,则第四和第五时间步的输出可以是b——因为下一时间步中的信息也可以影响执行双向分析时的前一个时间步的输出。
在最后一步,我们将压缩所有在连续时间步中具有相同值的 softmax 输出。
上述结果导致我们最终的输出为:-a-b-(以此示例为准)。
如果地面真相是abb,我们将期望在两个b之间有一个-,以避免连续的b被压缩成一个。
计算 CTC 损失值
对于我们在上一节中解决的问题,假设我们有以下情形,其中给定时间步中某个字符出现的概率如图中圆圈所示(请注意,每个时间步从t0到t5的概率和为 1):

然而,为了使计算更简洁,便于我们理解,假设地面真相是a而非ab,并且假设输出仅有三个时间步而不是六个。修改后的三个时间步的输出如下:

如果每个时间步的 softmax 值满足以下任一情形,我们可以得到a的地面真相:
| 每个时间步的输出 | 时间步 1 的字符概率 | 时间步 2 的字符概率 | 时间步 3 的字符概率 | 组合的概率 | 最终概率 | 
|---|---|---|---|---|---|
| - - a | 0.8 | 0.1 | 0.1 | 0.8 x 0.1 x 0.1 | 0.008 | 
| - a a | 0.8 | 0.9 | 0.1 | 0.8 x 0.9 x 0.1 | 0.072 | 
| a a a | 0.2 | 0.9 | 0.1 | 0.2 x 0.9 x 0.1 | 0.018 | 
| - a - | 0.8 | 0.9 | 0.8 | 0.8 x 0.9 x 0.8 | 0.576 | 
| - a a | 0.8 | 0.9 | 0.1 | 0.8 x 0.9 x 0.1 | 0.072 | 
| a - - | 0.2 | 0.1 | 0.8 | 0.2 x 0.1 x 0.8 | 0.016 | 
| a a - | 0.2 | 0.9 | 0.8 | 0.2 x 0.9 x 0.8 | 0.144 | 
| 总体概率 | 0.906 | 
从之前的结果中我们可以看到,获得地面真值 a 的总体概率为 0.906
CTC 损失是总体概率的负对数 = -log(0.906) = 0.04。
注意,由于每个时间步中概率最高的字符组合表示地面真值a,因此 CTC 损失接近于零
手写文本识别
在这个案例研究中,我们将致力于将手写图像转录成文本,从而提取出图像中的文字
一个手写样本如下所示:

注意,在上图中,手写字符的长度各不相同,图像的尺寸不同,字符之间的间距各异,且图像的质量不同
在这一部分,我们将学习如何使用 CNN、RNN 和 CTC 损失函数结合起来进行手写示例的转录
准备就绪
我们将采用的手写示例转录策略如下:
- 
下载包含手写文本图像的图像: - 
在与该案例研究相关的 GitHub 代码文件中提供了多个包含手写文本图像的数据集 
- 
确保在获得图像的同时,也获取了与图像对应的地面真值 
 
- 
- 
将所有图像调整为相同的大小,比如 32 x 128 尺寸 
- 
在调整图像大小时,我们还应确保图像的纵横比没有被扭曲: - 这可以确保图像不会因为原始图像被调整为 32 x 128 尺寸而显得模糊不清
 
- 
我们将调整图像大小,确保不扭曲纵横比,然后将每个图像叠加到不同的空白 32 x 128 图像上 
- 
反转图像的颜色,使背景为黑色,手写内容为白色 
- 
缩放图像,使其数值介于零和一之间 
- 
预处理输出(地面真值): - 
提取输出中的所有唯一字符 
- 
为每个字符分配一个索引 
- 
找到输出的最大长度,然后确保我们预测的时间步数超过输出的最大长度 
- 
通过对地面真值进行填充,确保所有输出的长度相同 
 
- 
- 
将处理过的图像通过 CNN 进行处理,以便我们提取出形状为 32 x 256 的特征 
- 
将从 CNN 中提取的特征传递到双向的 GRU 单元中,以便我们能够封装相邻时间步中的信息 
- 
每个时间步的 256 个特征作为该时间步的输入 
- 
将输出通过一个稠密层,该层的输出值与地面真值中的唯一字符总数相同(在 CTC 损失部分介绍中给出的示例中,填充值(-)也将是唯一字符之一——其中填充值 - 表示字符之间的空格,或图片空白部分的填充 
- 
在每个 32 个时间步中提取 softmax 及其对应的输出字符 
如何实现...
以下代码中的前述算法如下执行(代码文件在 GitHub 上作为Handwritten_text_recognition.ipynb可用):
- 
下载并导入数据集。该数据集包含手写文本的图像及其对应的地面真值(转录)。 
- 
构建一个函数,调整图片大小而不扭曲其宽高比,并填充其余图片,使它们都具有相同的形状: 
def extract_img(img):
     target = np.ones((32,128))*255
     new_shape1 = 32/img.shape[0]
     new_shape2 = 128/img.shape[1]
     final_shape = min(new_shape1, new_shape2)
     new_x = int(img.shape[0]*final_shape)
     new_y = int(img.shape[1]*final_shape)
     img2 = cv2.resize(img, (new_y,new_x ))
     target[:new_x,:new_y] = img2[:,:,0]
     target[new_x:,new_y:]=255
     return 255-target
在前述代码中,我们创建了一个空白图片(名为 target)。在下一步中,我们已经重塑了图片以保持其宽高比。
最后,我们覆盖了我们创建的空白图片的重新缩放图片,并返回了背景为黑色的图片(255-target)。
- 读取图片并将其存储在列表中,如下所示的代码中所示:
filepath = '/content/words.txt'
f = open(filepath)
import cv2
count = 0
x = []
y = []
x_new = []
chars = set()
for line in f:
     if not line or line[0]=='#':
         continue
     try:
         lineSplit = line.strip().split(' ')
         fileNameSplit = lineSplit[0].split('-')
         img_path = '/content/'+fileNameSplit[0]+'/'+fileNameSplit[0] + '-' +              fileNameSplit[1]+'/'+lineSplit[0]+'.png'
         img_word = lineSplit[-1]
         img = cv2.imread(img_path)
         img2 = extract_img(img)
         x_new.append(img2)
         x.append(img)
         y.append(img_word)
         count+=1
     except:
         continue
在前述代码中,我们提取了每个图片,并根据我们定义的函数进行了修改。输入和不同场景的修改示例:

- 提取输出中的唯一字符,如下所示:
import itertools
list2d = y
charList = list(set(list(itertools.chain(*list2d))))
- 创建输出地面真值,如下所示的代码中所示:
num_images = 50000
import numpy as np
y2 = []
input_lengths = np.ones((num_images,1))*32
label_lengths = np.zeros((num_images,1))
for i in range(num_images):
     val = list(map(lambda x: charList.index(x), y[i]))
     while len(val)<32:
         val.append(79)
     y2.append(val)
     label_lengths[i] = len(y[i])
     input_lengths[i] = 32
在前述代码中,我们将每个字符的索引存储到一个列表中。此外,如果输出的大小少于 32 个字符,我们会用 79 进行填充,79 表示空白值。
最后,我们还存储标签长度(在地面真值中)和输入长度(始终为 32)。
- 将输入和输出转换为 NumPy 数组,如下所示:
x = np.asarray(x_new[:num_images])
y2 = np.asarray(y2)
x= x.reshape(x.shape[0], x.shape[1], x.shape[2],1)
- 定义目标,如下所示:
outputs = {'ctc': np.zeros([32])}
我们初始化 32 个零,因为批量大小将为 32。对于批量大小中的每个值,我们期望损失值为零。
- 定义 CTC 损失函数如下:
def ctc_loss(args):
     y_pred, labels, input_length, label_length = args
     return K.ctc_batch_cost(labels, y_pred, input_length, label_length)
前述功能将预测值、地面真值(标签)和输入、标签长度作为输入,并计算 CTC 损失值。
- 定义模型,如下所示:
input_data = Input(name='the_input', shape = (32, 128,1), dtype='float32')
inner = Conv2D(32, (3,3), padding='same')(input_data)
inner = Activation('relu')(inner)
inner = MaxPooling2D(pool_size=(2,2),name='max1')(inner)
inner = Conv2D(64, (3,3), padding='same')(inner)
inner = Activation('relu')(inner)
inner = MaxPooling2D(pool_size=(2,2),name='max2')(inner)
inner = Conv2D(128, (3,3), padding='same')(input_data)
inner = Activation('relu')(inner)
inner = MaxPooling2D(pool_size=(2,2),name='max3')(inner)
inner = Conv2D(128, (3,3), padding='same')(inner)
inner = Activation('relu')(inner)
inner = MaxPooling2D(pool_size=(2,2),name='max4')(inner)
inner = Conv2D(256, (3,3), padding='same')(inner)
inner = Activation('relu')(inner)
inner = MaxPooling2D(pool_size=(4,2),name='max5')(inner)
inner = Reshape(target_shape = ((32,256)), name='reshape')(inner)
在前述代码中,我们正在构建 CNN,将具有 32 x 128 形状的图片转换为具有 32 x 256 形状的图片:
gru_1 = GRU(256, return_sequences = True, name = 'gru_1')(inner)
gru_2 = GRU(256, return_sequences = True, go_backwards = True, name = 'gru_2')(inner)
mix_1 = add([gru_1, gru_2])
gru_3 = GRU(256, return_sequences = True, name = 'gru_3')(inner)
gru_4 = GRU(256, return_sequences = True, go_backwards = True, name = 'gru_4')(inner)
到目前为止定义的模型的体系结构如下所示:

在前述代码中,我们将从 CNN 获取的特征传递到 GRU。如前面所示的定义的体系结构继续从所示的图形开始如下:

在下面的代码中,我们将两个 GRU 的输出进行拼接,从而同时考虑双向 GRU 和普通 GRU 生成的特征:
merged = concatenate([gru_3, gru_4])
添加前面一层后的架构如下:

在下面的代码中,我们通过一个全连接层传递 GRU 输出的特征,并应用 softmax 得到 80 个可能值中的一个作为输出:
dense = TimeDistributed(Dense(80))(merged)
y_pred = TimeDistributed(Activation('softmax', name='softmax'))(dense)
模型的架构继续如下:

- 初始化 CTC 损失所需的变量:
from keras.optimizers import Adam
Optimizer = Adam()
labels = Input(name = 'the_labels', shape=[32], dtype='float32')
input_length = Input(name='input_length', shape=[1],dtype='int64')
label_length = Input(name='label_length',shape=[1],dtype='int64')
output = Lambda(ctc_loss, output_shape=(1,),name='ctc')([y_pred, labels, input_length, label_length])
在前面的代码中,我们提到 y_pred(预测的字符值)、实际标签、输入长度和标签长度是 CTC 损失函数的输入。
- 按如下方式构建并编译模型:
model = Model(inputs = [input_data, labels, input_length, label_length], outputs= output)
model.compile(loss={'ctc': lambda y_true, y_pred: y_pred}, optimizer = Optimizer)
请注意,我们传递给模型的输入有多个。CTC 计算如下:

- 创建以下输入和输出向量:
x2 = 1-np.array(x_new[:num_images])/255
x2 = x2.reshape(x2.shape[0],x2.shape[1],x2.shape[2],1)
y2 = np.array(y2[:num_images])
input_lengths = input_lengths[:num_images]
label_lengths = label_lengths[:num_images]
- 在多个批次的图片上拟合模型,代码如下所示:
import random
for i in range(100):
     samp=random.sample(range(x2.shape[0]-100),32)
     x3=[x2[i] for i in samp]
     x3 = np.array(x3)
     y3 = [y2[i] for i in samp]
     y3 = np.array(y3)
     input_lengths2 = [input_lengths[i] for i in samp]
     label_lengths2 = [label_lengths[i] for i in samp]
     input_lengths2 = np.array(input_lengths2)
     label_lengths2 = np.array(label_lengths2)
     inputs = {
     'the_input': x3,
     'the_labels': y3,
     'input_length': input_lengths2,
     'label_length': label_lengths2,
     }
     outputs = {'ctc': np.zeros([32])}
     model.fit(inputs, outputs,batch_size = 32, epochs=1, verbose =2)
在前面的代码中,我们一次抽取 32 张图片,将它们转换为数组,并拟合模型以确保 CTC 损失为零。
请注意,我们排除了最后 100 张图片(在 x2 中),不将其作为输入传递给模型,以便测试模型在该数据上的准确性。
此外,我们多次遍历整个数据集,因为将所有图片加载到 RAM 并转换为数组很可能会导致系统崩溃,因为需要大量内存。
随着训练轮数的增加,训练损失如下所示:

- 使用以下代码预测测试图片在每个时间步的输出:
model2 = Model(inputs = input_data, outputs = y_pred)
pred = model2.predict(x2[-5].reshape(1,32,128,1))
pred2 = np.argmax(pred[0,:],axis=1)
out = ""
for i in pred2:
  if(i==79):
    continue
  else:
    out += charList[i]
plt.imshow(x2[k].reshape(32,128))
plt.title('Predicted word: '+out)
plt.grid('off')
在前面的代码中,如果在某个时间步预测的字符是 79 号字符,我们将丢弃该输出:
测试示例及其对应的预测(标题中)如下:

图像字幕生成
在之前的案例研究中,我们学习了如何将 CNN、RNN 和 CTC 损失一起使用,以转录手写数字。
在本案例研究中,我们将学习如何将 CNN 和 RNN 架构结合起来,以为给定图片生成字幕。
这里是图片的一个样本:

- 
一位穿红色裙子的女孩,背景是圣诞树 
- 
一位女孩正在展示圣诞树 
- 
一位女孩正在公园里玩耍 
- 
一位女孩正在庆祝圣诞节 
准备就绪
在本节中,让我们列出转录图片的策略:
- 
我们将致力于通过处理一个包含图片和与之相关的描述的数据集,生成图片的字幕。包含图片及其对应字幕的数据集链接会在 GitHub 上的相关笔记本中提供。 
- 
我们将提取每张图片的 VGG16 特征。 
- 
我们还将对字幕文本进行预处理: - 
将所有单词转换为小写 
- 
移除标点符号 
- 
为每个字幕添加开始和结束标记 
 
- 
- 
只保留狗或女孩的图片(我们执行此分析仅仅是为了加速模型训练,因为即使使用 GPU,运行此模型也需要大约 5 小时)。 
- 
为字幕词汇表中的每个唯一单词分配索引。 
- 
填充所有字幕(每个单词由索引值表示),以确保所有字幕现在都是相同的大小。 
- 
为了预测第一个词,模型应将 VGG16 特征与开始标记的嵌入组合起来作为输入。 
- 
类似地,为了预测第二个词,模型将采用 VGG16 特征和开始标记及第一个词的嵌入组合。 
- 
以类似方式,我们继续获取所有预测的词。 
- 
我们继续执行前面的步骤,直到预测到结束标记。 
如何实现...
我们将编写之前定义的策略,如下所示(代码文件可在 GitHub 上找到 Image_captioning.ipynb):
- 
下载并导入一个包含图像及其相应字幕的数据集。推荐的数据集可在 GitHub 上找到。 
- 
导入相关包,如下所示: 
import glob
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import pickle
from tqdm import tqdm
import pandas as pd
from keras.preprocessing import sequence
from keras.models import Sequential
from keras.layers import LSTM, Embedding, TimeDistributed, Dense, RepeatVector, merge, Activation, Flatten
from keras.optimizers import Adam, RMSprop
from keras.layers.wrappers import Bidirectional
from keras.applications.inception_v3 import InceptionV3
from keras.preprocessing import image
import nltk
- 加载字幕数据集,如以下代码所示:
caption_file = '...'
captions = open(caption_file, 'r').read().strip().split('\n')
d = {}
for i, row in enumerate(captions):
     row = row.split('\t')
     row[0] = row[0][:len(row[0])-2]
     if row[0] in d:
         d[row[0]].append(row[1])
     else:
         d[row[0]] = [row[1]]
total_images = list(d.keys())
- 加载图片并存储 VGG16 特征:
image_path = '...'
from keras.applications.vgg16 import VGG16
vgg16=VGG16(include_top=False, weights='imagenet', input_shape=(224,224,3))
import cv2
x = []
y = []
x2 = []
tot_images = ''
for i in range(len(d.keys())):
     for j in range(len(d[total_images[i]])):
         img_path = image_path+total_images[i]
         img = cv2.imread(img_path)
         try:
             img2 = cv2.resize(img, (224, 224))/255
             img3 = vgg16.predict(img2.reshape(1,224,224,3))
             x.append(img3)
             y.append(d[total_images[i]][j])
             tot_images = tot_images + ' '+total_images[i]
         except:
             continue
- 将 VGG16 特征转换为 NumPy 数组:
x = np.array(x)
x = x.reshape(x.shape[0],7,7,512)
- 创建一个函数,移除字幕中的标点符号,并将所有单词转换为小写:
def preprocess(text):
     text=text.lower()
     text=re.sub('[⁰-9a-zA-Z]+',' ',text)
     words = text.split()
     words2 = words
     words4=' '.join(words2)
     return(words4)
在以下代码中,我们预处理所有字幕并附加开始和结束标记:
caps = []
for key, val in d.items():
     if(key in img_path2):
         for i in val:
             i = preprocess(i)
             caps.append('<start> ' + i + ' <end>')
- 只附加属于儿童或狗的图片:
caps2 = []
x2 = []
img_path3 = []
for i in range(len(caps)):
     if (('girl') in caps[i]):
         caps2.append(caps[i])
         x2.append(x[i])
         img_path2.append(img_path[i])
     elif 'dog' in caps[i]:
         caps2.append(caps[i])
         x2.append(x[i])
         img_path2.append(img_path[i])
     else:
         continue
- 提取字幕中的所有唯一单词,如下所示:
words = [i.split() for i in caps2]
unique = []
for i in words:
     unique.extend(i)
unique = list(set(unique))
vocab_size = len(unique)
- 为词汇表中的单词分配索引,如以下代码所示:
word2idx = {val:(index+1) for index, val in enumerate(unique)}
idx2word = {(index+1):val for index, val in enumerate(unique)}
- 确定字幕的最大长度,以便我们将所有字幕填充到相同的长度:
max_len = 0
for c in caps:
     c = c.split()
     if len(c) > max_len:
         max_len = len(c)
- 将所有字幕填充到相同的长度,如下所示:
n = np.zeros(vocab_size+1)
y = []
y2 = []
for k in range(len(caps2)):
     t= [word2idx[i] for i in caps2[k].split()]
     y.append(len(t))
     while(len(t)<max_len):
         t.append(word2idx['<end>'])
     y2.append(t)
- 构建一个模型,该模型以图片为输入并从中创建特征:
from keras.layers import Input
embedding_size = 300
inp = Input(shape=(7,7,512))
inp1 = Conv2D(512, (3,3), activation='relu')(inp)
inp11 = MaxPooling2D(pool_size=(2, 2))(inp1)
inp2 = Flatten()(inp11)
img_emb = Dense(embedding_size, activation='relu') (inp2)
img_emb2 = RepeatVector(max_len)(img_emb)
- 构建一个模型,该模型以字幕为输入并从中创建特征:
inp2 = Input(shape=(max_len,))
cap_emb = Embedding((vocab_size+1), embedding_size, input_length=max_len) (inp2)
cap_emb2 = LSTM(256, return_sequences=True)(cap_emb)
cap_emb3 = TimeDistributed(Dense(300)) (cap_emb2)
- 将两个模型连接起来,并对所有可能的输出词进行 softmax 概率计算:
final1 = concatenate([img_emb2, cap_emb3])
final2 = Bidirectional(LSTM(256, return_sequences=False))(final1)
final3 = Dense(vocab_size+1)(final2)
final4 = Activation('softmax')(final3)
final_model = Model([inp, inp2], final4)
- 编译模型,如下所示:
from keras.optimizers import Adam
adam = Adam(lr = 0.0001)
final_model.compile(loss='categorical_crossentropy', optimizer = adam, metrics=['accuracy'])
- 拟合模型,如以下代码所示:
for i in range(500):
     x3 = []
     x3_sent = []
     y3 = []
     shortlist_y = random.sample(range(len(y)-100),32)
     for j in range(len(shortlist_y)):
         for k in range(y[shortlist_y[j]]-1):
             n = np.zeros(vocab_size+1) 
             x3.append(x2[shortlist_y[j]])
             pad_sent = pad_sequences([y2[shortlist_y[j]][:(k+1)]], maxlen=max_len, padding='post')
             x3_sent.append(pad_sent)
             n[y2[shortlist_y[j]][(k+1)]] = 1
             y3.append(n)
             x3 = np.array(x3)
             x3_sent =np.array(x3_sent)
             x3_sent = x3_sent.reshape(x3_sent.shape[0], x3_sent.shape[2])
             y3 = np.array(y3) 
             final_model.fit([x3/12, x3_sent], y3, batch_size = 32, epochs = 3, verbose = 1)
在前面的代码中,我们以每次 32 张图片的速度遍历所有图片。此外,我们正在创建输入数据集,方式是将字幕中的前 n 个输出词与图片的 VGG16 特征一起作为输入,而相应的输出则是字幕中的第 n+1^(th) 个词。
此外,我们将 VGG16 特征(x3)除以 12,因为我们需要将输入值缩放到 0 到 1 之间。
- 可以通过如下方式获取示例图片的输出字幕:
l=-25
im_path = image_path+ img_path3[l]
img1 = cv2.imread(im_path)
plt.imshow(img1)

输出解码如下:
p = np.zeros(max_len)
p[0] = word2idx['<start>']
for i in range(max_len-1):
     pred= final_model.predict([x33[l].reshape(1,7,7,512)/12, p.reshape(1,max_len)])
     pred2 = np.argmax(pred)
     print(idx2word[pred2])
     p[i+1] = pred2
     if(idx2word[pred2]=='<end>'):
         break
上述代码的输出如下:

请注意,生成的字幕正确地检测到了狗是黑色的,并且正在跳跃。
生成字幕,使用束搜索
在前面关于字幕生成的部分,我们根据给定时间步中概率最高的单词进行了解码。在本节中,我们将通过使用束搜索来改进预测的字幕。
准备就绪
束搜索的工作原理如下:
- 
提取第一时间步中各种单词的概率(其中 VGG16 特征和开始标记是输入) 
- 
我们不会仅提供最可能的单词作为输出,而是会考虑前三个最可能的单词。 
- 
我们将进入下一个时间步,在该时间步提取前三个字符 
- 
我们将循环遍历第一时间步的前三个预测,将其作为第二时间步预测的输入,并为每个可能的前三个输入预测提取前三个预测: - 
假设a、b和c是第一时间步的前三个预测 
- 
我们将使用a作为输入,并结合 VGG16 特征来预测第二时间步中最可能的三个字符,同样地,对于b和c也是如此。 
- 
我们在第一时间步和第二时间步之间有九个输出组合。 
- 
除了组合外,我们还将存储每个预测在所有九个组合中的置信度: - 例如:如果a在第一时间步的概率是 0.4,而x在第二时间步的概率是 0.5,那么组合的概率是 0.4 x 0.5 = 0.2
 
- 
我们将保留前三个组合,丢弃其他组合 
 
- 
- 
我们将重复前一步骤,筛选出前三个组合,直到达到句子的结尾。 
三的值是我们在搜索组合时所用的束长度。
如何做到...
在本节中,我们将编写之前讨论过的束搜索策略的代码(代码文件可在 GitHub 中的Image_captioning.ipynb找到):
- 定义一个函数,接受图片的 VGG16 特征作为输入,连同来自前一步时间步的单词序列及其相应的置信度,并返回当前时间步的前三个预测:
beamsize = 3
def get_top3(img, string_with_conf):
     tokens, confidence = string_with_conf
     p = np.zeros((1, max_len))
     p[0, :len(tokens)] = np.array(tokens)
     pred = final_model.predict([img.reshape(1,7,7,512)/12, p])
     best_pred = list(np.argsort(pred)[0][-beamsize:])
     best_confs = list(pred[0,best_pred])
     top_best = [(tokens + list([best_pred[i]]), confidence*best_confs[i]) for i in range(beamsize)]
     return top_best
在前面的步骤中,我们将单词 ID 及其对应的置信度从string_with_conf参数中分离出来。此外,我们将令牌序列存储在数组中,并用它来进行预测。
在下一步中,我们提取下一个时间步的前三个预测,并将其存储在best_pred中。
此外,除了最好的单词 ID 预测,我们还会存储当前时间步内每个前三名预测的置信度。
最后,我们返回第二时间步的三个预测。
- 在句子的最大可能长度范围内循环,并提取所有时间步中的前三个可能的单词组合:
start_token = word2idx['<start>']
best_strings = [([start_token], 1)]
for i in range(max_len):
     new_best_strings = []
     for string in best_strings:
         strings = get_top3(x33[l], string)
         new_best_strings.extend(strings) 
         best_strings = sorted(new_best_strings, key=lambda x: x[1], reverse=True)[:beamsize]
- 遍历之前获得的best_strings并打印输出:
for i in range(3):
     string = best_strings[i][0]
     print('============')
     for j in string:
         print(idx2word[j])
         if(idx2word[j]=='<end>'):
             break
我们在上一节测试的相同图片的输出句子如下:
 
   
  
注意,在这个特定的案例中,第一句和第二句在“jumping”和“playing”这两个词上有所不同,而第三句恰好和第一句相同,因为组合的概率要高得多。
第十五章:音频分析
在前几章中,我们学习了如何处理顺序文本数据。音频也可以看作是顺序数据,其振幅随时间变化。在这一章中,我们将学习以下内容:
- 
按类型分类歌曲 
- 
使用深度学习生成音乐 
- 
音频转录为文本 
按类型分类歌曲
在这个案例研究中,我们将把一首歌分类为 10 种可能的类型之一。想象一下一个场景:我们被要求自动分类一首歌的类型,而不需要手动听它。这样,我们就能尽可能减少操作负担。
准备工作
我们将采用的策略如下:
- 
下载一个包含各种音频录音及其对应类型的数据集。 
- 
可视化并对比不同类型音频信号的频谱图。 
- 
在频谱图上执行 CNN 操作: - 请注意,我们将在频谱图上执行 CNN 1D 操作,因为音频录音的情况下,翻译概念并不适用。
 
- 
从 CNN 中提取特征,经过多次卷积和池化操作后。 
- 
展平输出并通过一个具有 10 个可能类别的输出层的密集层。 
- 
最小化类别交叉熵,以将音频录音分类为 10 个可能类别之一。 
一旦我们对音频进行分类,我们将绘制每个音频输入的嵌入图,以便将相似的音频录音归为一类。这样,我们就能在不听歌的情况下识别新歌的类型,从而自动将音频输入分类到一个类型中。
如何操作...
上述策略的代码如下(代码文件在 GitHub 中名为Genre_classification.ipynb):
- 下载数据集并导入相关包:
import sys, re, numpy as np, pandas as pd, music21, IPython, pickle, librosa, librosa.dsiplay, os
from glob import glob
from tqdm import tqdm
from keras.utils import np_utils
- 循环遍历音频文件,提取输入音频的mel 频谱图特征,并存储音频输入的输出类型:
song_specs=[]
genres = []
for genre in os.listdir('...'): # Path to genres folder
  song_folder = '...' # Path to songs folder
  for song in os.listdir(song_folder):
    if song.endswith('.au'):
      signal, sr = librosa.load(os.path.join(song_folder, song), sr=16000)
      melspec = librosa.feature.melspectrogram(signal, sr=sr).T[:1280,]
      song_specs.append(melspec)
      genres.append(genre)
      print(song)
  print('Done with:', genre)
在上述代码中,我们加载音频文件并提取其特征。此外,我们还提取了信号的mel 频谱图特征。最后,我们将mel特征存储为输入数组,将类型存储为输出数组。
- 可视化频谱图:
plt.subplot(121)
librosa.display.specshow(librosa.power_to_db(song_specs[302].T),
 y_axis='mel',
 x_axis='time',)
plt.title('Classical audio spectrogram')
plt.subplot(122)
librosa.display.specshow(librosa.power_to_db(song_specs[402].T),
 y_axis='mel',
 x_axis='time',)
plt.title('Rock audio spectrogram')
以下是前述代码的输出:

你可以看到古典音频频谱图和摇滚音频频谱图之间有明显的区别。
- 定义输入和输出数组:
song_specs = np.array(song_specs)
song_specs2 = []
for i in range(len(song_specs)):
     tmp = song_specs[i]
     song_specs2.append(tmp[:900][:])
song_specs2 = np.array(song_specs2)
将输出类别转换为独热编码版本:
genre_one_hot = pd.get_dummies(genres)
- 创建训练和测试数据集:
x_train, x_test, y_train, y_test = train_test_split(song_specs2, np.array(genre_one_hot),test_size=0.1,random_state = 42)
- 构建并编译方法:
input_shape = (1280, 128)
inputs = Input(input_shape)
x = inputs
levels = 64
for level in range(7):
     x = Conv1D(levels, 3, activation='relu')(x)
     x = BatchNormalization()(x)
     x = MaxPooling1D(pool_size=2, strides=2)(x)
     levels *= 2
     x = GlobalMaxPooling1D()(x)
for fc in range(2):
     x = Dense(256, activation='relu')(x)
     x = Dropout(0.5)(x)
labels = Dense(10, activation='softmax')(x)
请注意,前述代码中的Conv1D方法与Conv2D非常相似;然而,Conv1D是一个一维滤波器,而Conv2D是一个二维滤波器:
model = Model(inputs=[inputs], outputs=[labels])
adam = keras.optimizers.Adam(lr=0.0001)
model.compile(loss='categorical_crossentropy',optimizer=adam,metrics=['accuracy'])
- 拟合模型:
history = model.fit(x_train, y_train,batch_size=128,epochs=100,verbose=1,validation_data=(x_test, y_test))
从前述代码中我们可以看到,模型在测试数据集上的分类准确率约为 60%。
- 从模型的倒数第二层提取输出:
from keras.models import Model
layer_name = 'dense_14'
intermediate_layer_model = Model(inputs=model.input,outputs=model.get_layer(layer_name).output)
intermediate_output = intermediate_layer_model.predict(song_specs2)
前面的代码在倒数第二层产生输出。
- 使用t-SNE将嵌入的维度减少到 2,这样我们就可以在图表上绘制我们的工作:
from sklearn.manifold import TSNE
tsne_model = TSNE(n_components=2, verbose=1, random_state=0)
tsne_img_label = tsne_model.fit_transform(intermediate_output)
tsne_df = pd.DataFrame(tsne_img_label, columns=['x', 'y'])
tsne_df['image_label'] = genres
- 绘制t-SNE输出:
from ggplot import *
chart = ggplot(tsne_df, aes(x='x', y='y', color='genres'))+ geom_point(size=70,alpha=0.5)
chart
以下是前面代码的图表:

从前面的图中,我们可以看到相似类型的音频记录聚集在一起。这样,我们现在可以自动地将一首新歌分类到可能的某个类型中,而无需人工检查。然而,如果音频属于某个类型的概率不高,它可能会被送去人工复审,以确保错误分类的可能性较小。
使用深度学习生成音乐
在前一章中,我们学习了通过阅读小说来生成文本。在本节中,我们将学习如何通过一系列音频音符生成音频。
准备工作
一个 MIDI 文件通常包含音频文件中音符和和弦的信息,而音符对象包含音符的音高、八度和偏移量的信息。和弦对象包含同时演奏的一组音符。
我们将采用的音乐生成策略如下:
- 
提取音频文件中的音符 
- 
为每个音符分配一个唯一的 ID。 
- 
取 100 个历史音符的序列,第 101 个音符将是输出。 
- 
训练 LSTM 模型。 
如何做...
上述讨论的策略编码如下(代码文件可以在 GitHub 上的Music_generation.ipynb中找到),并附带推荐的音频文件:
- 导入相关的包和数据集:
!pip install mido music21
import mido, glob, os
from mido import MidiFile, MidiTrack, Message
import numpy as np
from music21 import converter, instrument, note, chord
from keras.utils import np_utils
from keras.layers import Input, LSTM, Dropout, Dense, Activation
from keras.models import Model
fname = '/content/nintendo.mid'
- 读取文件内容:
midi = converter.parse(fname)
前面的代码读取了一个分数流。
- 定义一个函数,读取分数流并提取其中的音符(如果音频文件中有静音,也会提取):
def parse_with_silence(midi=midi):
     notes = []
     notes_to_parse = None
     parts = instrument.partitionByInstrument(midi)
     if parts: # file has instrument parts
         notes_to_parse = parts.parts[0].recurse()
     else: # file has notes in a flat structure
         notes_to_parse = midi.flat.notes
     for ix, element in enumerate(notes_to_parse):
         if isinstance(element, note.Note):
             _note = str(element.pitch)
             notes.append(_note)
         elif isinstance(element, chord.Chord):
             _note = '.'.join(str(n) for n in element.normalOrder)
             notes.append(_note)
         elif isinstance(element, note.Rest):
             _note = '#'+str(element.seconds)
             notes.append(_note)
     return notes
在前面的代码中,我们通过遍历元素来获取音符,取决于元素是音符、和弦还是休止符(表示静音),我们提取相应的音符,附加它们,并返回附加后的列表。
- 从输入音频文件的流中提取音符:
notes = parse_with_silence()
一个示例音符输出如下:

请注意,值以#开头表示静音(持续时间与紧邻#的数字相同)。
- 通过创建音符 ID 及其对应名称的字典,来创建输入和输出数据集:
# get all unique values in notes
pitchnames = sorted(set(item for item in notes))
# create a dictionary to map pitches to integers
note_to_int = dict((note, number) for number, note in enumerate(pitchnames))
network_input = []
network_output = []
- 
- 创建输入和输出数组的序列:
 
sequence_length = 100
for i in range(0, len(notes) - sequence_length, 1):
     sequence_in = notes[i:i + sequence_length]
     sequence_out = notes[i + sequence_length]
     network_input.append([note_to_int[char] for char in sequence_in])
     network_output.append(note_to_int[sequence_out])
在前面的步骤中,我们将 100 个音符的序列作为输入,并提取第 101 个时间步的输出。
此外,我们还将音符转换为其对应的 ID:
n_patterns = len(network_input)
# reshape the input into a format compatible with LSTM layers
network_input = np.reshape(network_input, (n_patterns, sequence_length, 1))
# normalize input
network_input = network_input / np.max(network_input)
network_output = np_utils.to_categorical(network_output)
N = 9 * len(network_input)//10
print(network_input.shape, network_output.shape)
# (36501, 100, 1) (36501, 50)
在前面的代码中,我们正在重新调整输入数据的形状,以便将其馈送到 LSTM 层(该层需要batch_size形状、时间步数和每个时间步的特征数)。
此外,我们正在对输入进行归一化,并将输出转换为一组独热编码的向量。
- 拟合模型:
model.fit(network_input, network_output, epochs=100, batch_size=32, verbose = 1)
以下是前述代码的输出:

- 生成预测:
from tqdm import trange
print('generating prediction stream...')
start = np.random.randint(0, len(network_input)-1)
int_to_note = dict((number, note) for number, note in enumerate(pitchnames))
pattern = network_input[start].tolist()
prediction_output = []
注意,在前面的代码中,我们选择了一个随机的音频位置,从那里我们将采样一个序列,用作未来时间步预测的种子。
- 通过一次处理 100 个音符的序列,生成下一个预测,将其附加到输入序列中,再生成下一个预测(通过获取最后 100 个音符的最新序列):
for note_index in trange(500):
     prediction_input = np.reshape(pattern, (1, len(pattern), 1))
     prediction = model.predict(prediction_input, verbose=0)
     index = np.argmax(prediction)
     result = int_to_note[index]
     prediction_output.append(result)
     pattern.append([index/49])
     pattern = pattern[1:len(pattern)]
注意,我们将索引(即模型的预测输出)除以 49,就像在构建模型时一样(除以np.max(network_input))。
上面的练习与文本生成练习略有不同,后者是基于输入词 ID 进行嵌入操作,而在这种情况下,我们没有进行嵌入。模型仍然在没有嵌入的情况下运行,可能是因为输入中唯一的值较少。
- 根据模型生成的值创建音符值:
offset = 0
output_notes = []
# create note and chord objects based on the values generated by the model
print('creating notes and chords')
for pattern in prediction_output:
    # pattern is a chord
    if (('.' in pattern) or pattern.isdigit()) and pattern[0]!='#':
        notes_in_chord = pattern.split('.')
        notes = []
        for current_note in notes_in_chord:
            new_note = note.Note(int(current_note))
            new_note.storedInstrument = instrument.Piano()
            notes.append(new_note)
        new_chord = chord.Chord(notes)
        new_chord.offset = offset
        output_notes.append(new_chord)
    # pattern is a note
    elif pattern[0]!='#':
        new_note = note.Note(pattern)
        new_note.offset = offset
        new_note.storedInstrument = instrument.Piano()
        output_notes.append(new_note)
    # pattern is silence
    else:
        new_note = note.Rest()
        new_note.offset = offset
        new_note.storedInstrument = instrument.Piano()
        new_note.quarterLength = float(pattern[1:])
        output_notes.append(new_note)
    # increase offset each iteration so that notes do not stack
    offset += 0.5
注意,在前面的代码中,我们将每个音符的时间偏移了 0.5 秒,这样在生成输出时音符不会重叠。
- 将生成的预测写入音乐流:
from music21 import stream
midi_stream = stream.Stream(output_notes)
midi_stream.write('midi', fp='OP.mid')
现在,你应该能够听到你的模型生成的音乐。
将音频转录为文本
在第十四章,端到端学习中,我们学习了如何将手写文本图像转录为文本。在这一部分,我们将利用类似的端到端模型将语音转录为文本。
准备中
我们将采用的语音转录策略如下:
- 
下载一个包含音频文件及其对应转录(实际结果)的数据集。 
- 
在读取音频文件时指定采样率: - 如果采样率为 16,000,我们将从每秒音频中提取 16,000 个数据点。
 
- 
提取音频数组的快速傅里叶变换: - 
FFT 确保我们仅保留信号中最重要的特征。 
- 
默认情况下,FFT 给我们提供n/2个数据点,其中n是整个音频录音中的数据点数量。 
 
- 
- 
对音频进行 FFT 特征采样,每次提取 320 个数据点;也就是说,我们每次提取 20 毫秒(320/16000 = 1/50 秒)的音频数据。 
- 
此外,我们将在 10 毫秒间隔处每次采样 20 毫秒的数据。 
- 
在这个练习中,我们将处理一个音频录音,其最大时长为 10 秒。 
- 
我们将把 20 毫秒的音频数据存储到一个数组中: - 
我们已经看到,每 10 毫秒采样 20 毫秒的数据。 
- 
因此,对于一个一秒钟的音频片段,我们将有 100 x 320 个数据点,而对于一个 10 秒钟的音频片段,我们将有 1,000 x 320 = 320,000 个数据点。 
 
- 
- 
我们将初始化一个 160,000 个数据点的空数组,并用 FFT 值覆盖这些值——因为我们已经知道 FFT 值是原始数据点数量的一半 
- 
对于每个 1,000 x 320 的数据点数组,我们将存储对应的转录文本 
- 
我们将为每个字符分配一个索引,然后将输出转换为索引列表 
- 
此外,我们还将存储输入长度(即预定义的时间步数)和标签长度(即输出中实际的字符数量) 
- 
此外,我们将定义基于实际输出、预测输出、时间步数(输入长度)和标签长度(输出字符数)的 CTC 损失函数 
- 
我们将定义一个模型,结合使用 conv1D(因为这是音频数据)和 GRU
- 
此外,我们将确保通过批量归一化来规范化数据,以防止梯度消失 
- 
我们将在数据批次上运行模型,在此过程中我们随机采样数据批次并将其输入模型,模型试图最小化 CTC 损失 
- 
最后,我们将通过使用 ctc_decode方法解码模型在新数据点上的预测
如何做到...
上述策略的代码实现如下(代码文件可在 GitHub 的Voice transcription.ipynb中找到):
- 下载数据集并导入相关的包:
$wget http://www.openslr.org/resources/12/train-clean-100.tar.gz
$tar xzvf train-clean-100.tar.gz
import librosa
import numpy as np
import pandas as pd
- 读取所有文件名及其对应的转录文本,并将它们转换为单独的列表:
import os, numpy as np
org_path = '/content/LibriSpeech/train-clean-100/'
count = 0
inp = []
k=0
audio_name = []
audio_trans = []
for dir1 in os.listdir(org_path):
     dir2_path = org_path+dir1+'/'
     for dir2 in os.listdir(dir2_path):
     dir3_path = dir2_path+dir2+'/'
     for audio in os.listdir(dir3_path):
         if audio.endswith('.txt'):
             k+=1
             file_path = dir3_path + audio
             with open(file_path) as f:
                 line = f.readlines()
                 for lines in line:
                     audio_name.append(dir3_path+lines.split()[0]+'.flac')
                     words2 = lines.split()[1:]
                     words4=' '.join(words2)
                     audio_trans.append(words4)
- 将转录文本的长度存储到一个列表中,这样我们就能理解最大转录文本的长度:
import re
len_audio_name=[]
for i in range(len(audio_name)):
     tmp = audio_trans[i]
     len_audio_name.append(len(tmp))
- 对于本次练习,为了能够在单个 GPU 上训练模型,我们将对前 2,000 个音频文件进行操作,这些音频文件的转录文本长度少于 100 个字符:
final_audio_name = []
final_audio_trans = []
for i in range(len(audio_name)):
     if(len_audio_name[i]<100):
         final_audio_name.append(audio_name[i])
         final_audio_trans.append(audio_trans[i])
在前面的代码中,我们仅为那些转录文本长度少于 100 个字符的音频录音存储音频名称和对应的转录文本
- 将输入存储为二维数组,并仅对那些时长少于 10 秒的音频文件存储对应的输出:
inp = []
inp2 = []
op = []
op2 = []
for j in range(len(final_audio_name)):
     t = librosa.core.load(final_audio_name[j],sr=16000, mono= True) 
     if(t[0].shape[0]<160000):
         t = np.fft.rfft(t[0])
         t2 = np.zeros(160000)
         t2[:len(t)] = t
         inp = []
         for i in range(t2.shape[0]//160):
             inp.append(t2[(i*160):((i*160)+320)])
             inp2.append(inp)
             op2.append(final_audio_trans[j])
- 为数据中的每个唯一字符创建一个索引:
import itertools
list2d = op2
charList = list(set(list(itertools.chain(*list2d))))
- 创建输入和标签长度:
num_audio = len(op2)
y2 = []
input_lengths = np.ones((num_audio,1))*243
label_lengths = np.zeros((num_audio,1))
for i in range(num_audio):
     val = list(map(lambda x: charList.index(x), op2[i]))
     while len(val)<243:
         val.append(29)
     y2.append(val)
     label_lengths[i] = len(op2[i])
     input_lengths[i] = 243
注意,我们正在创建一个 243 的输入长度,因为模型的输出(我们将在后续步骤中构建)具有 243 个时间步。
- 定义 CTC 损失函数:
import keras.backend as K
def ctc_loss(args):
    y_pred, labels, input_length, label_length = args
    return K.ctc_batch_cost(labels, y_pred, input_length, label_length
- 定义模型:
input_data = Input(name='the_input', shape = (999,161), dtype='float32')
inp = BatchNormalization(name="inp")(input_data)
conv= Conv1D(filters=220, kernel_size = 11,strides = 2, padding='valid',activation='relu')(inp)
conv = BatchNormalization(name="Normal0")(conv)
conv1= Conv1D(filters=220, kernel_size = 11,strides = 2, padding='valid',activation='relu')(conv)
conv1 = BatchNormalization(name="Normal1")(conv1)
gru_3 = GRU(512, return_sequences = True, name = 'gru_3')(conv1)
gru_4 = GRU(512, return_sequences = True, go_backwards = True, name = 'gru_4')(conv1)
merged = concatenate([gru_3, gru_4])
normalized = BatchNormalization(name="Normal")(merged)
dense = TimeDistributed(Dense(30))(normalized)
y_pred = TimeDistributed(Activation('softmax', name='softmax'))(dense)
Model(inputs = input_data, outputs = y_pred).summary()
- 定义 CTC 损失函数的输入和输出参数:
from keras.optimizers import Adam
Optimizer = Adam(lr = 0.001)
labels = Input(name = 'the_labels', shape=[243], dtype='float32')
input_length = Input(name='input_length', shape=[1],dtype='int64')
label_length = Input(name='label_length',shape=[1],dtype='int64')
output = Lambda(ctc_loss, output_shape=(1,),name='ctc')([y_pred, labels, input_length, label_length])
- 构建并编译模型:
model = Model(inputs = [input_data, labels, input_length, label_length], outputs= output)
model.compile(loss={'ctc': lambda y_true, y_pred: y_pred}, optimizer = Optimizer, metrics = ['accuracy'])
模型摘要如下:

- 在从输入中采样的数据批次上拟合模型:
for i in range(2500):
     samp=random.sample(range(x2.shape[0]-25),32)
     batch_input=[inp2[i] for i in samp]
     batch_input = np.array(batch_input)
     batch_input = batch_input/np.max(batch_input)
     batch_output = [y2[i] for i in samp]
     batch_output = np.array(batch_output)
     input_lengths2 = [input_lengths[i] for i in samp]
     label_lengths2 = [label_lengths[i] for i in samp]
     input_lengths2 = np.array(input_lengths2)
     label_lengths2 = np.array(label_lengths2)
     inputs = {'the_input': batch_input,
             'the_labels': batch_output,
             'input_length': input_lengths2,
             'label_length': label_lengths2}
     outputs = {'ctc': np.zeros([32])} 
     model.fit(inputs, outputs,batch_size = 32, epochs=1, verbose =1)
在前面的代码中,我们正在循环并提取 2,500 次数据批次,规范化输入数据,并拟合模型
此外,我们进行了大量的训练周期,因为对于这个特定的数据集和模型组合,CTC 损失下降得很慢。
- 预测测试音频:
model2 = Model(inputs = input_data, outputs = y_pred)
k=-12
pred= model2.predict(np.array(inp2[k]).reshape(1,999,161)/100)
在前面的代码中,我们指定了一个模型(model2),它接受输入的测试数组并提取每个 243 个时间步骤中的模型预测结果。
此外,我们从输入数组的最后提取了第 12^(th)个元素的预测(请注意,我们在训练模型时排除了最后 25 个输入数据点)。此外,我们还像之前一样对其进行了预处理,将输入数据传递给模型训练过程。
- 解码新数据点上的预测:
def decoder(pred):
     pred_ints = (K.eval(K.ctc_decode(pred,[243])[0][0])).flatten().tolist()
     out = ""
     for i in range(len(pred_ints)):
         if pred_ints[i]<28:
         out = out+charList[pred_ints[i]]
     print(out)
在前面的代码中,我们使用ctc_decode方法解码了预测结果。或者,我们也可以像提取手写图像转录中的预测那样解码预测。最后,我们打印出了预测结果。
我们将能够通过调用之前定义的函数来解码预测结果:
decoder(pred)
其中一个预测的输出如下:

虽然前面的输出看起来像是胡言乱语,但它在语音上与实际的音频相似,具体如下:

还有更多……
我们可以进一步提高转录准确性的一些方法如下:
- 
在更多数据点上进行训练 
- 
融入语言模型对输出进行模糊匹配,以便我们能纠正预测结果。 
第十六章:强化学习
在前几章中,我们学习了将输入映射到目标—其中输入和输出值是已知的。在本章中,我们将学习强化学习,其中目标和操作环境是已知的,但没有输入或输出的映射。强化学习的工作原理是,我们通过开始时随机采取行动,逐步从生成的输入数据(状态中的行动)和输出数据(通过采取某些行动所获得的奖励)中学习,生成输入值(智能体的状态)和相应的输出值(智能体在状态中采取某些行动所获得的奖励)。
本章我们将覆盖以下内容:
- 
在一个模拟游戏中采取的最佳行动,且奖励为非负值 
- 
在模拟游戏中的状态下采取的最佳行动 
- 
Q-learning 在玩《Frozen Lake》时最大化奖励 
- 
深度 Q 学习来平衡一个推车杆 
- 
深度 Q 学习来玩《Space Invaders》游戏 
在一个模拟游戏中采取的最佳行动,且奖励为非负值
在本节中,我们将理解如何在模拟游戏中采取正确的行动。请注意,这个练习将主要帮助你掌握强化学习的工作原理。
准备工作
让我们定义一下我们在这个模拟环境中操作的环境。
你有三个盒子,两个玩家正在玩游戏。玩家 1 标记一个盒子为 1,玩家 2 标记一个盒子为 2。能够标记两个连续盒子的玩家获胜。
该游戏的空白棋盘如下所示:

对于我们刚刚定义的问题,只有玩家 1 有机会赢得游戏。玩家 1 赢得游戏的可能情境如下:

从问题设置中来看,玩家 1 获胜的直观方式是选择中间的盒子。这样,无论玩家 2 选择哪个盒子,玩家 1 都会在下一步获胜。
虽然对于玩家 1 来说,第一步是直观的,但在接下来的章节中,我们将学习一个智能体如何自动找出最佳的第一步行动。
我们将采取的策略来解决这个问题如下:
- 
我们初始化一个空白棋盘 
- 
玩家 1 随机选择一个盒子 
- 
玩家 2 从剩下的两个盒子中随机选择一个 
- 
根据玩家 1 剩下的盒子,我们更新玩家 1 的奖励: - 
如果玩家 1 能够在两个连续盒子中放置 1,他将成为赢家,并获得 1 的奖励 
- 
否则,玩家 1 将获得 0 的奖励 
 
- 
- 
重复前面的练习 100 次,每次进行游戏并为给定的动作序列存储奖励 
- 
现在,我们将继续计算各种第一步行动的平均奖励 
- 
在第一次移动中选择的框,经过 100 次迭代后,具有最高平均奖励的是玩家 1 的最佳首次行动。 
如何操作...
上述策略的代码如下(代码文件可在 GitHub 上的Finding_optimal_policy.ipynb找到):
- 定义游戏环境和执行游戏的函数:
def play_game():
     empty_board = [0,0,0]
     move = []
     for step in range(3):
         index_to_choose = [i for i,j in enumerate(empty_board) if j==0]
         samp = random.sample(range(len(index_to_choose)), 1)[0] 
         if(step%2==0):
             empty_board[index_to_choose[samp]]=1
             move.append(index_to_choose[samp])
         else:
             empty_board[index_to_choose[samp]]=2 
     return(reward(empty_board), move[0])
在前面的代码中,我们初始化了一个空的棋盘,所有单元格值为零,并进行了一次名为samp的随机移动。玩家 1 先行,然后是玩家 2,接着是玩家 1。我们以这种方式填充空棋盘。
- 定义一个函数来计算游戏结束时的奖励:
def reward(empty_board):
     reward = 0
     if((empty_board[0]==1 & empty_board[1]==1) | (empty_board[1]==1 & empty_board[2]==1)):
         reward = 1
     else:
         reward = 0
     return reward
- 玩游戏100次:
rew = []
step = []
for i in range(100):
     r, move = play_game()
     rew.append(r)
     step.append(move) 
- 计算选择某一首次行动的奖励:
sub_list = [i for i,j in enumerate(step) if j==1]
final_reward = 0
count = 0
for i in sub_list:
     final_reward += rew[i]
     count+=1
final_reward/count
当你对多种首次行动选项重复运行前面的代码时,你会注意到当占据第二个方格时,平均奖励最高。
在模拟游戏中,状态下采取的最佳行动
在前面的场景中,我们考虑了一个简化的情况,即当目标达成时会获得奖励。在此场景中,我们将通过引入负奖励来使游戏更加复杂。然而,目标仍然不变:在给定环境中最大化奖励,该环境同时包含正奖励和负奖励。
准备就绪
我们正在处理的环境如下:

我们从包含S的单元格开始,目标是到达奖励为+1的单元格。为了最大化获得奖励的机会,我们将使用贝尔曼方程来计算前面网格中每个单元格的值,如下所示:
当前单元格的值 = 从当前单元格移动到下一个单元格的奖励 + 折扣因子 * 下一个单元格的值
此外,在当前问题中,除了奖励为+1的单元格外,移动到任何其他单元格的奖励都是0。
折扣因子可以被视为从一个单元格移动到另一个单元格时所消耗的能量。因此,在当前问题设置中,远离奖励单元格的单元格值较低。
一旦我们计算出每个单元格的值,我们就会移动到具有所有可能移动的单元格中最高值的单元格。
我们将采用的计算每个单元格值的策略如下:
- 
初始化一个空棋盘。 
- 
定义代理在一个单元格内可能采取的行动。 
- 
定义代理在当前单元格内采取行动时所处的状态。 
- 
计算当前状态的值,该值依赖于移到下一个状态的奖励以及下一个状态的值。 
- 
基于之前的计算,更新当前状态的单元格值。 
- 
此外,存储当前状态下采取的行动,以便移动到下一个状态。 
- 
请注意,在初期迭代中,距离终点较远的格子的值保持为零,而与终点相邻的格子的值则上升。 
- 
随着我们多次迭代前面的步骤,我们将能够更新格子的值,从而决定代理应该遵循的最优路径。 
如何做到这一点...
在本节中,我们将编写在前一节中规划的策略(代码文件在 GitHub 上的Finding_optimal_policy.ipynb中可以找到):
- 初始化一个空的棋盘:
empty_board = [[0,0,0]
             ,[0,0,1]]
- 定义在不同状态下可以采取的行动——其中D代表向下移动,R代表向右,L代表向左,U代表向上:
state_actions = {(0,0):('D','R')
                 ,(0,1):('D','R','L')
                 ,(0,2):('D','L')
                 ,(1,0):('U','R')
                 ,(1,1):('L','U','R') 
                 }
- 定义一个函数,根据当前状态和在当前状态下采取的行动来提取下一个状态:
def get_next_state(curr_state, action):
     i,j = curr_state
     if action=='D':
         i = i+1
     elif action=='U':
         i = i-1
     elif action=='R':
         j = j+1
     elif action=='L':
         j = j-1
     else:
         print('unk')
     return((i,j))
- 初始化列表,用于附加状态、行动和奖励:
curr_state = (0,0)
state_action_reward = []
state = []
state_action = []
- 在一个回合中最多执行 100 次行动(一个回合是游戏的一次实例),在每个格子(状态)中随机采取行动,并根据移动到下一个状态的奖励以及下一个状态的值来计算当前状态的值。
重复以上练习100次迭代(回合/游戏),并计算每个格子的值:
for m in range(100):
     curr_state = (0,0)
     for k in range(100):
         reward = 0
         action = state_actions[curr_state][random.sample(range(len(state_actions[curr_state])),1)[0]]
         next_state = get_next_state(curr_state, action)
在前面的代码中,我们在一个状态中采取随机行动,然后计算该行动对应的下一个状态:
        state.append(curr_state)
        empty_board[curr_state[0]][curr_state[1]] = reward + empty_board[next_state[0]][next_state[1]]*0.9 
        empty_board[curr_state[0]][curr_state[1]])
在前面的代码中,我们正在更新一个状态的值:
        curr_state = next_state
        state_action.append(action)
        if(next_state==(1,2)):
             reward+=1
             break
前面的结果在所有格子中的最终状态值如下:

根据前面的输出,代理可以在游戏开始时采取右侧的行动或向下的行动(代理从左上角开始)。然而,如果代理在第一步采取向下的行动,那么在下一步它更适合采取向右的行动,因为右侧的状态值高于当前状态上方的状态值。
还有更多...
假设环境(各个格子及其对应的奖励)如下所示:

在不同状态下可以采取的行动如下:
state_actions = {(0,0):('D','R')
                 ,(0,1):('D','R')
                 ,(1,0):('R')
                 ,(1,1):('R') 
                 }
在多次游戏迭代后,各个格子的状态值如下所示:

从前面的结果来看,我们可以看到,代理在左上角采取向下的行动要比向右移动更好,因为下方格子的状态值高于起始格子上方的状态值。
使用 Q 学习最大化 Frozen Lake 游戏中的奖励
到目前为止,在前面的章节中,我们一直在给定的状态下采取随机行动。此外,我们还通过代码定义了环境,并计算了每一步的下一个状态、行动和奖励。在本节中,我们将利用 OpenAI 的 Gym 包来在 Frozen Lake 环境中进行导航。
准备就绪
冻结湖环境如下所示:

代理从S状态开始,目标是通过尽量避开H状态,最终到达G状态。
在前面的环境中,代理可以处于 16 个可能的状态中。此外,代理可以采取四个可能的动作(向上、向下、向右或向左)。
我们将定义一个 q 表格,其中有 16 行对应 16 种状态,4 列对应每种状态下可以采取的四个动作。
在上一节中,我们学习到:
在一个状态下采取的动作的价值 = 奖励 + 折扣因子 * 下一个状态中采取的最佳可能动作的价值
我们将修改前面的公式如下:
在一个状态下采取的动作的价值 = 在该状态下采取的动作的价值 + 1 * (奖励 + 折扣因子 * 下一个状态中采取的最佳可能动作的价值 - 在该状态下采取的动作的价值)
最后,我们将把 1 替换为学习率,这样状态中动作的值更新就不会发生剧烈变化。这类似于神经网络中使用学习率的效果。
在一个状态下采取的动作的价值 = 在该状态下采取的动作的价值 + 学习率 * (奖励 + 折扣因子 * 下一个状态中采取的最佳可能动作的价值 - 在该状态下采取的动作的价值)
根据前面的内容,我们现在可以更新 q 表格,以便能够识别在不同状态下可以采取的最佳动作。
我们将采用以下策略来解决这个案例研究:
- 
在 OpenAI 的 Gym 中注册环境 
- 
初始化一个零数组 q 表格,形状为 16 x 4 
- 
在选择给定状态下的动作时,采用探索与利用的平衡方法: - 
到目前为止,我们仅仅是探索了可能的整体动作,因为我们在给定状态下随机选择了一个动作。 
- 
在本节中,我们将探索初始的几次迭代,因为我们在游戏的前几个回合中并不确定应该采取的最佳动作。 
- 
然而,随着我们对游戏了解的深入,我们将利用已经学到的关于可能采取的动作的知识,同时仍然会随机采取一些动作(随着回合数的增加,随机动作的频率会逐渐减少)。 
 
- 
- 
在每一回合中: - 
根据我们是尝试探索还是利用,选择一个动作 
- 
确定新的状态和奖励,并检查通过采取上一步选择的动作,游戏是否结束 
- 
初始化学习率参数和折扣因子 
- 
使用前面讨论的公式,通过更新 q 表格中在某一状态下采取的前一个动作的价值 
- 
重复前面的步骤,直到游戏结束 
 
- 
- 
此外,重复前面的步骤,进行 1,000 场不同的游戏 
- 
查看 q 表格,找出在给定状态下应采取的最佳动作 
- 
根据 q 表格绘制代理在状态中采取动作的路径 
如何实现……
在这一部分,我们将编写我们之前讨论的策略(代码文件在 GitHub 上作为Frozen_Lake_with_Q_Learning.ipynb提供):
- 导入相关包:
import gym
from gym import envs
from gym.envs.registration import register
Gym 是一个开发和比较强化学习算法的工具包。它支持教智能体从行走到玩游戏(如 Pong 和 Pinball)等所有任务。
更多关于 Gym 的信息可以在这里找到:gym.openai.com/。
- 注册环境:
register(
 id = 'FrozenLakeNotSlippery-v1',
 entry_point = 'gym.envs.toy_text:FrozenLakeEnv',
 kwargs = {'map_name': '4x4', 'is_slippery':False},
 max_episode_steps = 100,
 reward_threshold = 0.8196)
- 创建环境:
env = gym.make('FrozenLakeNotSlippery-v1')
- 检查创建的环境:
env.render()

上述步骤呈现(打印)环境:
env.observation_space
上述代码提供了环境中的状态-动作对的数量。在我们的例子中,考虑到它是一个 4x4 的网格,我们总共有 16 个状态。因此,我们有 16 个观察。
env.action_space.n
上述代码定义了在环境中可以在某个状态下执行的动作数量:
env.action_space.sample()
上述代码从可能的动作集合中采样一个动作:
env.step(action)
上述代码执行动作并生成新的状态和该动作的奖励,标记游戏是否结束,并为步骤提供附加信息:
env.reset()
上述代码重置了环境,使得智能体回到起始状态。
- 初始化 q 表:
import numpy as np
qtable = np.zeros((16,4))
我们将其初始化为(16, 4)的形状,因为有 16 个状态,每个状态有 4 个可能的动作。
- 运行多个游戏回合:
初始化超参数:
total_episodes=15000
learning_rate=0.8
max_steps=99
gamma=0.95
epsilon=1
max_epsilon=1
min_epsilon=0.01
decay_rate=0.005
玩多个游戏回合:
rewards=[]
for episode in range(total_episodes):
    state=env.reset()
    step=0
    done=False
    total_rewards=0
在下面的代码中,我们定义了要采取的动作。如果eps(它是一个在 0 到 1 之间生成的随机数)小于 0.5,我们进行探索;否则,我们进行利用(即考虑 q 表中的最佳动作)
    for step in range(max_steps):
        exp_exp_tradeoff=random.uniform(0,1)        
        ## Exploitation:
        if exp_exp_tradeoff>epsilon:
            action=np.argmax(qtable[state,:])
        else:
            ## Exploration
            action=env.action_space.sample()
在下面的代码中,我们获取新的状态和奖励,并通过在给定步骤中采取动作来标记游戏是否结束:
        new_state, reward, done, _ = env.step(action)
在下面的代码中,我们根据在某个状态下采取的动作更新 q 表。此外,我们还在当前状态下采取动作后,使用新状态更新状态:
        qtable[state,action]=qtable[state,action]+learning_rate*(reward+gamma*np.max(qtable[new_state,:])-qtable[state,action])
        total_rewards+=reward
        state=new_state
在以下代码中,由于游戏已结束,我们将继续进行新的游戏回合。然而,我们确保更新随机性因子(eps),该因子用于决定我们是进行探索还是利用。
        if(done):
             break
        epsilon=min_epsilon+(max_epsilon-min_epsilon)*np.exp(decay_rate*episode)
        rewards.append(total_rewards)
- 一旦我们构建了 q 表,我们现在就可以部署智能体,让其根据 q 表建议的最优动作来操作:
env.reset()
for episode in range(1):
    state=env.reset()
    step=0
    done=False
    print("-----------------------")
    print("Episode",episode)
    for step in range(max_steps):
        env.render()
        action=np.argmax(qtable[state,:])
        print(action)
        new_state,reward,done,info=env.step(action)
        if done:
            #env.render()
            print("Number of Steps",step+1)
            break
        state=new_state
上述代码给出了智能体必须经过的最优路径,以到达最终目标。
深度 Q 学习平衡推车杆
在之前的部分,我们学习了如何基于 q 表的值采取行动。然而,得到最优值是一个耗时的过程,因为智能体需要多次游戏才能得到最优的 q 表。
在这一部分,我们将学习如何使用神经网络,这样我们就能比使用 Q 学习时更快地得到最优值。
准备工作
对于这个练习,我们将注册一个推车-杆环境,可能的行动是向右或向左移动,以保持杆的平衡。此外,推车的位置、推车速度、杆的角度和杆尖端的速度是我们关于状态的信息。
游戏的规则可以在此找到:gym.openai.com/envs/CartPole-v1/。
杆通过一个不带驱动的关节连接到推车上,推车沿着一个无摩擦的轨道移动。该系统通过对推车施加+1 或-1 的力量来控制。摆杆从直立开始,目标是防止其倒下。每当杆保持直立时,都会提供+1 的奖励。当杆与竖直方向的夹角超过 15 度,或者推车离中心超过 2.4 个单位时,回合结束。
为了平衡推车-杆,我们将采用与上一部分相同的策略。然而,深度 Q 学习的不同之处在于,我们将使用神经网络来帮助我们预测代理需要采取的最佳行动。
我们训练神经网络的方式如下:
- 
我们将存储状态值、所采取的行动和获得的奖励的信息: - 如果游戏没有结束(未结束),奖励为 1,否则为 0。
 
- 
初始时,模型基于随机初始化的权重进行预测,其中模型的输出层有两个节点,分别对应两个可能行动的新状态值。 
- 
新状态值将基于最大化新状态值的行动。 
- 
如果游戏未结束,我们将通过当前状态的奖励与新状态的最大状态值和折扣因子的乘积之和来更新当前状态的值。 
- 
我们现在将覆盖先前获得的更新后的当前状态值来更新行动的值: - 
如果当前步骤采取的行动是错误的(即游戏结束),那么当前状态下该行动的值为 0。 
- 
否则,当前步骤中目标的值为正数。 
- 
这样,我们让模型自己找出该采取的正确行动。 
- 
此外,我们可以认为当奖励为零时,行动是错误的。然而,由于我们无法确定当奖励为 1 时它是否为正确行动,因此我们只更新我们采取的行动,并保持新状态的值(如果我们采取另一个行动)不变。 
 
- 
- 
我们将状态值附加到输入数组,并且将采取某个行动时在当前状态下的值作为输出数组。 
- 
我们拟合模型,最小化前述数据点的均方误差。 
- 
最后,我们在逐渐增加的回合数中不断减少探索。 
如何做到这一点...
我们将按如下方式编码我们之前讨论的策略(代码文件可在 GitHub 的Deep_Q_learning_to_balance_a_cart_pole.ipynb中找到):
- 创建环境并将动作大小和状态大小存储在变量中:
import gym 
env = gym.make('CartPole-v0') 
state_size = env.observation_space.shape[0] 
action_size = env.action_space.n
一个倒立摆环境如下所示:

- 导入相关的包:
import numpy as np
import random
from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import Adam
from collections import deque
- 定义模型:
model=Sequential()
model.add(Dense(24,input_dim=state_size,activation='relu'))
model.add(Dense(24,activation='relu'))
model.add(Dense(2,activation='linear'))
model.compile(loss='mse',optimizer=Adam(lr=0.01))
- 定义需要附加的列表:
memory = deque(maxlen=2000)
gamma = 0.95 # discount rate
epsilon = 1.0 # exploration rate
epsilon_min = 0.01
epsilon_decay = 0.995
done = False
batch_size=32
- 定义一个函数来重玩游戏:
def replay(model, batch_size,epsilon):
    epsilon_min = 0.01
    epsilon_decay = 0.995
    minibatch = random.sample(memory, batch_size)
    for state, action, reward, next_state, done in minibatch:
        target = reward
        if not done:
            target = (reward + gamma *np.amax(model.predict(next_state)[0]))
        new_action_value = model.predict(state)
        new_action_value[0][action] = target
        model.fit(state,new_action_value, epochs=1, verbose=0)
    if epsilon > epsilon_min:
        epsilon *= epsilon_decay
    return model,epsilon
在前面的代码中,我们定义了一个函数,该函数接收神经网络模型、批次大小和 epsilon(表示我们是探索还是开发的参数)。我们获取一个大小为batch_size的随机样本。请注意,你将在下一步了解记忆结构(包括状态、动作、奖励和next_state)。如果游戏未结束,我们将更新采取的动作的奖励;否则,目标奖励将为 0(因为游戏结束时奖励为 0)。
此外,模型预测采取某个动作的价值(因为模型在输出层有 2 个节点,每个节点分别输出采取其中一个动作的结果)。该函数返回更新后的模型和探索/开发系数(epsilon)。
- 在多个回合中玩游戏,并附加代理获得的得分。此外,确保代理采取的行动是根据 epsilon 值由模型决定的:
episodes=200
maxsteps=200
score_list = []
for e in range(episodes):
    state = env.reset()
    state = np.reshape(state, [1, state_size])
在前面的代码中,我们总共进行了 200 个回合的游戏,并且在每回合开始时重置环境。此外,我们将状态重塑,以便可以传递给神经网络模型:
    for step in range(maxsteps):
        if np.random.rand()<=epsilon:
            action=env.action_space.sample()
        else:
            action = np.argmax(model.predict(state)[0])
在前面的步骤中,我们根据探索参数(epsilon)采取行动,在某些情况下我们采取随机行动(env.actionspace.sample()),而在其他情况下,我们利用模型的预测:
        next_state, reward, done, _ = env.step(action)
        reward = reward if not done else -10
        next_state = np.reshape(next_state, [1, state_size])
        memory.append((state, action, reward, next_state, done))
在前面的步骤中,我们执行了一个动作并提取了下一个状态、奖励以及游戏是否结束的信息。此外,如果游戏结束,我们将奖励值重置为-10(这意味着代理做出了错误的动作)。进一步地,我们提取下一个状态并将其附加到记忆中。这样,我们为模型创建了一个数据集,模型利用当前状态和奖励计算两个可能动作之一的奖励:
        state = next_state
        if done:
          print("episode: {}/{}, score: {}, exp prob: {:.2}".format(e, episodes, step, epsilon))
          score_list.append(step)
          break
        if len(memory) > batch_size:
          model,epsilon=replay(model, batch_size,epsilon)
在前面的代码中,如果游戏结束,我们将得分(游戏过程中采取的步数)附加到列表中;否则,我们更新模型。此外,只有当内存中的数据点数量达到预定义的批次大小时,我们才会更新模型。
随着训练轮数增加,得分的变化如下所示:

使用深度 Q 学习来玩 Space Invaders 游戏
在上一节中,我们使用深度 Q 学习来玩倒立摆游戏。在本节中,我们将利用深度 Q 学习来玩 Space Invaders,这是一个比倒立摆更复杂的环境。
Space Invaders 游戏的截图如下所示:

来源: gym.openai.com/envs/SpaceInvaders-v0/
这个练习的目标是最大化单场游戏中获得的分数。
准备开始
我们将采用的策略是构建一个能够最大化分数的智能体,具体如下:
- 
初始化Space Invaders-Atari2600游戏环境。 
- 
对图像帧进行预处理: - 
移除那些不一定会影响动作预测的像素。 - 例如,玩家位置下方的像素
 
- 
规范化输入图像。 
- 
在将图像传递给神经网络模型之前调整图像大小 
 
- 
- 
按照 Gym 环境的要求堆叠帧 
- 
让智能体在多个回合中玩游戏: - 
在初期回合中,我们会有较高的探索度,而随着回合的增加,探索度逐渐衰减。 
- 
在某一状态下需要采取的动作取决于探索系数的值。 
- 
将游戏状态和对应的奖励(基于在该状态下采取的动作)存储在记忆中。 
- 
根据前几个回合获得的奖励来更新模型。 
 
- 
如何实现...
我们之前讨论的策略编码如下:
- 下载包含《太空入侵者》游戏的 ROM,并安装retro包:
$ wget http://www.atarimania.com/roms/Roms.rar && unrar x Roms.rar && unzip Roms/ROMS.zip
$ pip3 install gym-retro
$ python3 -m retro.import ROMS/
- 创建环境并提取观察空间:
env=retro.make(game='SpaceInvaders-Atari2600')
env.observation_space
# Box(210,160,3)
- 构建一个处理帧(图像/《太空入侵者》游戏的截图)预处理的函数:
def preprocess_frame(frame):
     # Greyscale frame 
     gray = rgb2gray(frame)
     # Crop the screen (remove the part below the player)
     # [Up: Down, Left: right]
     cropped_frame = gray[8:-12,4:-12]
     # Normalize Pixel Values
     normalized_frame = cropped_frame/255.0
     # Resize
     preprocessed_frame = transform.resize(normalized_frame, [110,84])
     return preprocessed_frame 
- 构建一个根据状态堆叠帧的函数:
stack_size = 4 # We stack 4 frames
# Initialize deque with zero-images one array for each image
stacked_frames = deque([np.zeros((110,84), dtype=np.int) for i in range(stack_size)], maxlen=4)
def stack_frames(stacked_frames, state, is_new_episode):
     # Preprocess frame
     frame = preprocess_frame(state) 
     if is_new_episode:
         # Clear our stacked_frames
         stacked_frames = deque([np.zeros((110,84), dtype=np.int) for i in range(stack_size)], maxlen=4) 
         # Because we're in a new episode, copy the same frame 4x
         stacked_frames.append(frame)
         stacked_frames.append(frame)
         stacked_frames.append(frame)
         stacked_frames.append(frame) 
         # Stack the frames
         stacked_state = np.stack(stacked_frames, axis=2) 
     else:
         # Append frame to deque, automatically removes the oldest frame
         stacked_frames.append(frame)
         # Build the stacked state (first dimension specifies different frames)
         stacked_state = np.stack(stacked_frames, axis=2) 
     return stacked_state, stacked_frames
- 初始化模型的超参数:
### MODEL HYPERPARAMETERS
state_size = [110, 84, 4] # Our input is a stack of 4 frames hence 110x84x4 (Width, height, channels) 
action_size = env.action_space.n # 8 possible actions
learning_rate = 0.00025 # Alpha (aka learning rate)
### TRAINING HYPERPARAMETERS
total_episodes = 50 # Total episodes for training
max_steps = 50000 # Max possible steps in an episode
batch_size = 32 # Batch size
# Exploration parameters for epsilon greedy strategy
explore_start = 1.0 # exploration probability at start
explore_stop = 0.01 # minimum exploration probability 
decay_rate = 0.00001 # exponential decay rate for exploration prob
# Q learning hyperparameters
gamma = 0.9 # Discounting rate
### MEMORY HYPERPARAMETERS
pretrain_length = batch_size # Number of experiences stored in the Memory when initialized for the first time
memory_size = 1000000 # Number of experiences the Memory can keep
### PREPROCESSING HYPERPARAMETERS
stack_size = 4 # Number of frames stacked
### MODIFY THIS TO FALSE IF YOU JUST WANT TO SEE THE TRAINED AGENT
training = False
## TURN THIS TO TRUE IF YOU WANT TO RENDER THE ENVIRONMENT
episode_render = False
- 构建一个从总记忆中采样数据的函数:
memory = deque(maxlen=100000)
def sample(memory, batch_size):
     buffer_size = len(memory)
     index = np.random.choice(np.arange(buffer_size),
     size = batch_size,
     replace = False) 
     return [memory[i] for i in index]
- 构建一个返回智能体需要采取的动作的函数:
def predict_action(model,explore_start, explore_stop, decay_rate, decay_step, state, actions):
     exp_exp_tradeoff = np.random.rand()
     explore_probability = explore_stop + (explore_start - explore_stop) * np.exp(-decay_rate * decay_step)
     if (explore_probability > exp_exp_tradeoff):
         choice = random.randint(1,len(possible_actions))-1
         action = possible_actions[choice]
     else:
         Qs = model.predict(state.reshape((1, *state.shape)))
         choice = np.argmax(Qs)
         action = possible_actions[choice]
     return action, explore_probability
- 构建一个微调模型的函数:
def replay(agent,batch_size,memory):
     minibatch = sample(memory,batch_size)
     for state, action, reward, next_state, done in minibatch:
     target = reward
     if not done:
         target = reward + gamma*np.max(agent.predict(next_state.reshape((1,*next_state.shape)))[0])
     target_f = agent.predict(state.reshape((1,*state.shape)))
     target_f[0][action] = target
     agent.fit(state.reshape((1,*state.shape)), target_f, epochs=1, verbose=0)
 return agent
- 定义神经网络模型:
def DQNetwork():
     model=Sequential()
     model.add(Convolution2D(32,input_shape=(110,84,4),kernel_size=8, strides=4, padding='valid',activation='elu'))
     model.add(Convolution2D(64, kernel_size=4, strides=2, padding='valid',activation='elu'))
     model.add(Convolution2D(128, kernel_size=3, strides=2, padding='valid',activation='elu'))
     model.add(Flatten())
     model.add(Dense(units=512))
     model.add(Dense(units=3,activation='softmax'))
     model.compile(optimizer=Adam(0.01),loss='mse')
     return model
模型的总结如下:

- 循环多个回合并持续玩游戏,同时更新模型:
agent = DQNetwork()
agent.summary()
rewards_list=[]
Episodes=200
# Iterate the game
for episode in range(Episodes):
     # reset state in the beginning of each game
     step = 0
     decay_step = 0
     episode_rewards = []
     state = env.reset()
     state, stacked_frames = stack_frames(stacked_frames, state, True)
     while step < max_steps:
         step += 1
         decay_step +=1
         # Predict the action to take and take it
         action, explore_probability = predict_action(agent,explore_start, explore_stop, decay_rate, decay_step, state, possible_actions)
 #Perform the action and get the next_state, reward, and done information
         next_state, reward, done, _ = env.step(action)
         # Add the reward to total reward
         episode_rewards.append(reward)
     if done:
 # The episode ends so no next state
         next_state = np.zeros((110,84), dtype=np.int)
         next_state, stacked_frames = stack_frames(stacked_frames, next_state, False)
 # Set step = max_steps to end the episode
         step = max_steps
 # Get the total reward of the episode
         total_reward = np.sum(episode_rewards)
         print('Episode:{}/{} Score:{} Explore Prob:{}'.format(episode,Episodes,total_reward,explore_probability))
         rewards_list.append((episode, total_reward))
 # Store transition <st,at,rt+1,st+1> in memory D
         memory.append((state, action, reward, next_state, done))
     else:
 # Stack the frame of the next_state
         next_state, stacked_frames = stack_frames(stacked_frames, next_state, False)
 # Add experience to memory
         memory.append((state, action, reward, next_state, done))
 # st+1 is now our current state
         state = next_state
     env.render() 
 # train the agent with the experience of the episode
 agent=replay(agent,batch_size,memory)
- 绘制随着回合增加获得的奖励:
score=[]
episode=[]
for e,r in rewards_list:
     episode.append(e)
     score.append(r)
import matplotlib.pyplot as plt
plt.plot(episode,score)

从中可以看出,模型已经学会在某些回合中得分超过 800 分。

 
                    
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号