Python-深度学习算法实用指南-全-
Python 深度学习算法实用指南(全)
原文:
zh.annas-archive.org/md5/844a6ce45a119d3197c33a6b5db2d7b1
译者:飞龙
前言
深度学习是人工智能领域最受欢迎的领域之一,允许你开发复杂程度各异的多层模型。本书介绍了从基础到高级的流行深度学习算法,并展示了如何使用 TensorFlow 从头开始实现它们。在整本书中,你将深入了解每个算法背后的数学原理及其最佳实现方式。
本书首先解释如何构建自己的神经网络,然后介绍了强大的 Python 机器学习和深度学习库 TensorFlow。接下来,你将快速掌握诸如 NAG、AMSGrad、AdaDelta、Adam、Nadam 等梯度下降变体的工作原理。本书还将为你揭示循环神经网络(RNNs)和长短期记忆网络(LSTM)的工作方式,并教你如何使用 RNN 生成歌词。接着,你将掌握卷积网络和胶囊网络的数学基础,这些网络广泛用于图像识别任务。最后几章将带你了解机器如何通过 CBOW、skip-gram 和 PV-DM 理解单词和文档的语义,以及探索各种 GAN(如 InfoGAN 和 LSGAN)和自编码器(如收缩自编码器、VAE 等)的应用。
本书结束时,你将具备在自己的项目中实现深度学习所需的技能。
本书适合谁
如果你是机器学习工程师、数据科学家、AI 开发者或者任何希望专注于神经网络和深度学习的人,这本书适合你。完全不熟悉深度学习,但具有一定机器学习和 Python 编程经验的人也会发现这本书很有帮助。
本书涵盖的内容
第一章,深度学习介绍,解释了深度学习的基础知识,帮助我们理解人工神经网络及其学习过程。我们还将学习如何从头开始构建我们的第一个人工神经网络。
第二章,TensorFlow 初探,帮助我们了解最强大和流行的深度学习库之一——TensorFlow。你将了解 TensorFlow 的几个重要功能,并学习如何使用 TensorFlow 构建神经网络以执行手写数字分类。
第三章,梯度下降及其变种,深入理解了梯度下降算法。我们将探索几种梯度下降算法的变种,如随机梯度下降(SGD)、Adagrad、ADAM、Adadelta、Nadam 等,并学习如何从头开始实现它们。
第四章,使用 RNN 生成歌词,描述了如何使用 RNN 建模顺序数据集以及它如何记住先前的输入。我们将首先对 RNN 有一个基本的理解,然后深入探讨其数学。接下来,我们将学习如何在 TensorFlow 中实现 RNN 来生成歌词。
第五章,改进 RNN,开始探索 LSTM 以及它如何克服 RNN 的缺点。稍后,我们将了解 GRU 单元以及双向 RNN 和深层 RNN 的工作原理。在本章末尾,我们将学习如何使用 seq2seq 模型进行语言翻译。
第六章,揭秘卷积网络,帮助我们掌握卷积神经网络的工作原理。我们将探索 CNN 前向和反向传播的数学工作方式。我们还将学习各种 CNN 和胶囊网络的架构,并在 TensorFlow 中实现它们。
第七章,学习文本表示,涵盖了称为 word2vec 的最新文本表示学习算法。我们将探索 CBOW 和 skip-gram 等不同类型的 word2vec 模型的数学工作方式。我们还将学习如何使用 TensorBoard 可视化单词嵌入。稍后,我们将了解用于学习句子表示的 doc2vec、skip-thoughts 和 quick-thoughts 模型。
第八章,使用 GAN 生成图像,帮助我们理解最流行的生成算法之一 GAN。我们将学习如何在 TensorFlow 中实现 GAN 来生成图像。我们还将探索不同类型的 GAN,如 LSGAN 和 WGAN。
第九章,深入了解 GAN,揭示了各种有趣的不同类型的 GAN。首先,我们将学习 CGAN,它条件生成器和鉴别器。然后我们看到如何在 TensorFlow 中实现 InfoGAN。接下来,我们将学习如何使用 CycleGAN 将照片转换为绘画作品,以及如何使用 StackGAN 将文本描述转换为照片。
第十章,使用自编码器重构输入,描述了自编码器如何学习重构输入。我们将探索并学习如何在 TensorFlow 中实现不同类型的自编码器,如卷积自编码器、稀疏自编码器、收缩自编码器、变分自编码器等。
第十一章,探索少样本学习算法,描述如何构建模型从少量数据点中学习。我们将了解什么是少样本学习,并探索流行的少样本学习算法,如孪生网络、原型网络、关系网络和匹配网络。
要从本书中获取最大收益
对于那些完全新手于深度学习,但在机器学习和 Python 编程方面有些经验的人,本书将会很有帮助。
下载示例代码文件
您可以从您的帐户在www.packt.com下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packt.com/support并注册,将文件直接发送到您的邮箱。
您可以按照以下步骤下载代码文件:
-
在www.packt.com登录或注册。
-
选择支持选项卡。
-
单击代码下载和勘误。
-
在搜索框中输入书名,然后按照屏幕上的说明操作。
下载文件后,请确保使用最新版本的以下软件解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书的代码包也托管在 GitHub 上:github.com/PacktPublishing/Hands-On-Deep-Learning-Algorithms-with-Python
。如果代码有更新,将在现有的 GitHub 仓库上更新。
我们还提供来自丰富图书和视频目录的其他代码包,可以在github.com/PacktPublishing/
上查看!
下载彩色图像
我们还提供一个 PDF 文件,其中包含本书中使用的截屏/图表的彩色图像。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781789344158_ColorImages.pdf
。
使用的约定
本书使用了许多文本约定。
CodeInText
:指示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:"计算J_plus
和J_minus
。"
代码块设置如下:
J_plus = forward_prop(x, weights_plus)
J_minus = forward_prop(x, weights_minus)
任何命令行的输入或输出如下所示:
tensorboard --logdir=graphs --port=8000
粗体:指示一个新术语、重要词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如此。例如:"输入层和输出层之间的任何一层称为隐藏层。"
警告或重要说明如下。
提示和技巧以这种方式出现。
联系我们
我们非常欢迎读者的反馈。
常规反馈:如果您对本书的任何方面有疑问,请在邮件主题中提到书名,并发送邮件至customercare@packtpub.com
。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误还是可能发生。如果您在本书中发现了错误,我们将不胜感激您向我们报告。请访问 www.packt.com/submit-errata,选择您的书籍,点击“勘误提交表格”链接,并填写详细信息。
盗版:如果您在互联网上发现我们作品的任何非法副本,请提供给我们位置地址或网站名称。请通过 copyright@packt.com
发送包含该材料链接的邮件联系我们。
如果您有兴趣成为作者:如果您在某个专题上拥有专业知识,并且有意撰写或为书籍贡献内容,请访问 authors.packtpub.com。
评论
请留下您的评论。在您阅读并使用了本书之后,为什么不在您购买它的网站上留下一条评论呢?潜在的读者可以通过您的客观意见做出购买决策,我们在 Packt 可以了解您对我们产品的看法,而我们的作者也可以看到您对他们书籍的反馈。谢谢!
欲了解更多有关 Packt 的信息,请访问 packt.com。
第一部分:深度学习入门
在本节中,我们将熟悉深度学习,并理解基本的深度学习概念。我们还将学习一个强大的深度学习框架,称为 TensorFlow,并为我们未来的深度学习任务设置 TensorFlow。
本节包括以下章节:
-
第一章,深度学习简介
-
第二章,了解 TensorFlow
第一章:深度学习简介
深度学习是机器学习的一个子集,灵感来自人类大脑中的神经网络。它已经存在了十年,但现在之所以如此流行,是由于计算能力的提升和大量数据的可用性。有了大量的数据,深度学习算法表现优于经典的机器学习。它已经在计算机视觉、自然语言处理(NLP)、语音识别等多个跨学科科学领域得到广泛应用和变革。
在本章中,我们将学习以下主题:
-
深度学习的基本概念
-
生物和人工神经元
-
人工神经网络及其层次
-
激活函数
-
人工神经网络中的前向和后向传播
-
梯度检查算法
-
从头开始构建人工神经网络
什么是深度学习?
深度学习其实只是具有多层的人工神经网络的现代称谓。那么深度学习中的“深”指的是什么?基本上是由于人工神经网络(ANN)的结构。ANN 由若干层组成,用于执行任何计算。我们可以建立一个具有数百甚至数千层深度的网络。由于计算能力的提升,我们可以构建一个具有深层次的网络。由于 ANN 使用深层进行学习,我们称之为深度学习;当 ANN 使用深层进行学习时,我们称之为深度网络。我们已经了解到,深度学习是机器学习的一个子集。深度学习与机器学习有何不同?什么使深度学习如此特别和流行?
机器学习的成功取决于正确的特征集。特征工程在机器学习中起着至关重要的作用。如果我们手工制作了一组合适的特征来预测某一结果,那么机器学习算法可以表现良好,但是找到和设计合适的特征集并非易事。
在深度学习中,我们无需手工制作这些特征。由于深层次的人工神经网络采用了多层,它可以自动学习复杂的内在特征和多层次的抽象数据表示。让我们通过一个类比来探讨这个问题。
假设我们想执行一个图像分类任务。比如说,我们要学习识别一张图像是否包含狗。使用机器学习时,我们需要手工设计能帮助模型理解图像是否包含狗的特征。我们将这些手工设计的特征作为输入发送给机器学习算法,然后它们学习特征与标签(狗)之间的映射关系。但是从图像中提取特征是一项繁琐的任务。使用深度学习,我们只需将一堆图像输入到深度神经网络中,它将自动充当特征提取器,通过学习正确的特征集。正如我们所学到的,人工神经网络使用多个层次;在第一层中,它将学习表征狗的基本特征,例如狗的体态,而在后续层次中,它将学习复杂的特征。一旦学习到正确的特征集,它将查找图像中是否存在这些特征。如果这些特征存在,则说明给定的图像包含狗。因此,与机器学习不同的是,使用深度学习时,我们不必手动设计特征,而是网络自己学习任务所需的正确特征集。
由于深度学习的这一有趣特性,在提取特征困难的非结构化数据集中,例如语音识别、文本分类等领域中,它被广泛应用。当我们拥有大量的大型数据集时,深度学习算法擅长提取特征并将这些特征映射到它们的标签上。话虽如此,深度学习并不仅仅是将一堆数据点输入到深度网络中并获得结果。这也并非那么简单。我们将会探索的下一节内容中会讨论到,我们会有许多超参数作为调节旋钮,以获得更好的结果。
尽管深度学习比传统的机器学习模型表现更好,但不建议在较小的数据集上使用 DL。当数据点不足或数据非常简单时,深度学习算法很容易对训练数据集过拟合,并且在未见过的数据集上泛化能力不佳。因此,我们应仅在有大量数据点时应用深度学习。
深度学习的应用是多种多样且几乎无处不在的。一些有趣的应用包括自动生成图像标题,为无声电影添加声音,将黑白图像转换为彩色图像,生成文本等等。谷歌的语言翻译、Netflix、亚马逊和 Spotify 的推荐引擎以及自动驾驶汽车都是由深度学习驱动的应用程序。毫无疑问,深度学习是一项颠覆性技术,在过去几年取得了巨大的技术进步。
在本书中,我们将通过从头开始构建一些有趣的深度学习应用来学习基本的深度学习算法,这些应用包括图像识别、生成歌词、预测比特币价格、生成逼真的人工图像、将照片转换为绘画等等。已经兴奋了吗?让我们开始吧!
生物和人工神经元
在继续之前,首先我们将探讨什么是神经元以及我们的大脑中的神经元实际工作原理,然后我们将了解人工神经元。
神经元可以被定义为人类大脑的基本计算单位。神经元是我们大脑和神经系统的基本单元。我们的大脑大约有 1000 亿个神经元。每一个神经元通过一个称为突触的结构相互连接,突触负责从外部环境接收输入,从感觉器官接收运动指令到我们的肌肉,以及执行其他活动。
神经元还可以通过称为树突的分支结构从其他神经元接收输入。这些输入根据它们的重要性加强或减弱,然后在称为细胞体的细胞体内汇总在一起。从细胞体出发,这些汇总的输入被处理并通过轴突发送到其他神经元。
单个生物神经元的基本结构如下图所示:
现在,让我们看看人工神经元是如何工作的。假设我们有三个输入![], ![], 和 ![], 来预测输出![]。这些输入分别乘以权重![], ![], 和 ![],并按以下方式求和:
但是为什么我们要用权重乘以这些输入呢?因为在计算输出时,并不是所有的输入都同等重要!。假设
在计算输出时比其他两个输入更重要。那么,我们给![]赋予比其他两个权重更高的值。因此,在将权重与输入相乘后,![]将比其他两个输入具有更高的值。简单来说,权重用于加强输入。在用权重乘以输入之后,我们将它们加在一起,再加上一个叫做偏置的值,
:
如果你仔细观察前面的方程式,它看起来可能很熟悉?![] 看起来像线性回归的方程式吗?难道它不就是一条直线的方程式吗?我们知道直线的方程式如下所示:
这里,m 是权重(系数),x 是输入,b 是偏置(截距)。
好吧,是的。那么,神经元和线性回归之间有什么区别呢?在神经元中,我们通过应用称为激活或传递函数的函数 ,引入非线性到结果
。因此,我们的输出变为:
下图显示了单个人工神经元:
因此,一个神经元接收输入 x,将其乘以权重 w,加上偏置 b,形成 ,然后我们在
上应用激活函数,并得到输出
。
ANN 及其层
尽管神经元非常强大,但我们不能仅使用一个神经元执行复杂任务。这就是为什么我们的大脑有数十亿个神经元,排列成层,形成一个网络的原因。同样,人工神经元也被排列成层。每一层都将以信息从一层传递到另一层的方式连接起来。
典型的人工神经网络由以下层组成:
-
输入层
-
隐藏层
-
输出层
每一层都有一组神经元,而且一层中的神经元会与其他层中的所有神经元进行交互。然而,同一层中的神经元不会相互作用。这是因为相邻层的神经元之间有连接或边缘;然而,同一层中的神经元之间没有任何连接。我们使用术语节点或单元来表示人工神经网络中的神经元。
下图显示了典型的人工神经网络(ANN):
输入层
输入层是我们向网络提供输入的地方。输入层中的神经元数量等于我们向网络提供的输入数量。每个输入都会对预测输出产生一定影响。然而,输入层不进行任何计算;它只是用于将信息从外部世界传递到网络中。
隐藏层
输入层和输出层之间的任何层称为隐藏层。它处理从输入层接收到的输入。隐藏层负责推导输入和输出之间的复杂关系。也就是说,隐藏层识别数据集中的模式。它主要负责学习数据表示并提取特征。
隐藏层可以有任意数量;然而,我们根据使用情况选择隐藏层的数量。对于非常简单的问题,我们可以只使用一个隐藏层,但在执行像图像识别这样的复杂任务时,我们使用许多隐藏层,每个隐藏层负责提取重要特征。当我们有许多隐藏层时,网络被称为深度神经网络。
输出层
在处理输入后,隐藏层将其结果发送到输出层。顾名思义,输出层发出输出。输出层的神经元数量基于我们希望网络解决的问题类型。
如果是二元分类,则输出层的神经元数量为 1,告诉我们输入属于哪个类别。如果是多类分类,比如五类,并且我们想要得到每个类别的概率作为输出,则输出层的神经元数量为五个,每个神经元输出概率。如果是回归问题,则输出层有一个神经元。
探索激活函数
激活函数,也称为传递函数,在神经网络中起着至关重要的作用。它用于在神经网络中引入非线性。正如我们之前学到的,我们将激活函数应用于输入,该输入与权重相乘并加上偏置,即,,其中z = (输入 * 权重) + 偏置,
是激活函数。如果不应用激活函数,则神经元简单地类似于线性回归。激活函数的目的是引入非线性变换,以学习数据中的复杂潜在模式。
现在让我们看看一些有趣的常用激活函数。
Sigmoid 函数
Sigmoid 函数是最常用的激活函数之一。它将值缩放到 0 到 1 之间。Sigmoid 函数可以定义如下:
这是一个如下所示的 S 形曲线:
它是可微的,意味着我们可以找到任意两点处曲线的斜率。它是单调的,这意味着它要么完全非递减,要么非递增。Sigmoid 函数也称为logistic函数。由于我们知道概率介于 0 和 1 之间,由于 Sigmoid 函数将值压缩在 0 和 1 之间,因此用于预测输出的概率。
Python 中可以定义 sigmoid 函数如下:
def sigmoid(x):
return 1/ (1+np.exp(-x))
双曲正切函数
双曲正切(tanh)函数将值输出在 -1 到 +1 之间,表达如下:
它也类似于 S 形曲线。与 Sigmoid 函数以 0.5 为中心不同,tanh 函数是以 0 为中心的,如下图所示:
与 sigmoid 函数类似,它也是一个可微且单调的函数。tanh 函数的实现如下:
def tanh(x):
numerator = 1-np.exp(-2*x)
denominator = 1+np.exp(-2*x)
return numerator/denominator
矫正线性单元函数
矫正线性单元(ReLU)函数是最常用的激活函数之一。它输出从零到无穷大的值。它基本上是一个分段函数,可以表达如下:
即当 x 的值小于零时, 返回零,当 x 的值大于或等于零时,
返回 x。也可以表达如下:
ReLU 函数如下图所示:
如前图所示,当我们将任何负输入传入 ReLU 函数时,它将其转换为零。所有负值为零的这种情况被称为dying ReLU问题,如果神经元总是输出零,则称其为死神经元。ReLU 函数的实现如下:
def ReLU(x):
if x<0:
return 0
else:
return x
泄漏 ReLU 函数
泄漏 ReLU是 ReLU 函数的一个变种,可以解决 dying ReLU 问题。它不是将每个负输入转换为零,而是对负值具有小的斜率,如下所示:
泄漏 ReLU 可以表达如下:
的值通常设置为 0.01。泄漏 ReLU 函数的实现如下:
def leakyReLU(x,alpha=0.01):
if x<0:
return (alpha*x)
else:
return x
不要将一些默认值设置为 ,我们可以将它们作为神经网络的参数发送,并使网络学习
的最优值。这样的激活函数可以称为Parametric ReLU函数。我们也可以将
的值设置为某个随机值,这被称为Randomized ReLU函数。
指数线性单元函数
指数线性单元 (ELU),类似于 Leaky ReLU,在负值时具有小的斜率。但是它不是直线,而是呈现对数曲线,如下图所示:
可以表达如下:
ELU
函数在 Python 中的实现如下:
def ELU(x,alpha=0.01):
if x<0:
return ((alpha*(np.exp(x)-1))
else:
return x
Swish 函数
Swish 函数是 Google 最近引入的激活函数。与其他激活函数不同,Swish 是非单调函数,即它既不总是非递减也不总是非增加。它比 ReLU 提供更好的性能。它简单且可以表达如下:
这里, 是 sigmoid 函数。Swish 函数如下图所示:
我们还可以重新参数化 Swish 函数,并将其表达如下:
当 的值为 0 时,我们得到恒等函数
。
它变成了一个线性函数,当 的值趋于无穷大时,
变成了
,这基本上是 ReLU 函数乘以某个常数值。因此,
的值在线性和非线性函数之间起到了很好的插值作用。Swish 函数的实现如下所示:
def swish(x,beta):
return 2*x*sigmoid(beta*x)
Softmax 函数
Softmax 函数 基本上是 sigmoid 函数的一般化。它通常应用于网络的最后一层,并在执行多类别分类任务时使用。它给出每个类别的概率作为输出,因此 softmax 值的总和始终等于 1。
可以表示如下:
如下图所示,softmax 函数将它们的输入转换为概率:
可以在 Python 中如下实现 softmax
函数:
def softmax(x):
return np.exp(x) / np.exp(x).sum(axis=0)
人工神经网络中的前向传播
在本节中,我们将看到神经网络学习的过程,其中神经元堆叠在层中。网络中的层数等于隐藏层数加上输出层数。在计算网络层数时,我们不考虑输入层。考虑一个具有一个输入层 、一个隐藏层
和一个输出层
的两层神经网络,如下图所示:
假设我们有两个输入, 和
,我们需要预测输出,
。由于有两个输入,输入层中的神经元数为两个。我们设置隐藏层中的神经元数为四个,输出层中的神经元数为一个。现在,输入将与权重相乘,然后加上偏置,并将结果传播到隐藏层,在那里将应用激活函数。
在此之前,我们需要初始化权重矩阵。在现实世界中,我们不知道哪些输入比其他输入更重要,因此我们会对它们进行加权并计算输出。因此,我们将随机初始化权重和偏置值。输入层到隐藏层之间的权重和偏置值分别由 和
表示。那么权重矩阵的维度是多少呢?权重矩阵的维度必须是 当前层中的神经元数 x 下一层中的神经元数。为什么呢?
因为这是基本的矩阵乘法规则。要将任意两个矩阵 AB 相乘,矩阵 A 的列数必须等于矩阵 B 的行数。因此,权重矩阵 的维度应为 输入层中的神经元数 x 隐藏层中的神经元数,即 2 x 4:
上述方程表示,。现在,这将传递到隐藏层。在隐藏层中,我们对
应用激活函数。让我们使用 sigmoid
激活函数。然后,我们可以写成:
应用激活函数后,我们再次将结果 乘以一个新的权重矩阵,并添加一个在隐藏层和输出层之间流动的新偏置值。我们可以将这个权重矩阵和偏置表示为
和
,分别。权重矩阵
的维度将是 隐藏层神经元的数量 x 输出层神经元的数量。由于我们隐藏层有四个神经元,输出层有一个神经元,因此
矩阵的维度将是 4 x 1。所以,我们将
乘以权重矩阵,
,并添加偏置,
,然后将结果
传递到下一层,即输出层:
现在,在输出层,我们对 应用 sigmoid 函数,这将产生一个输出值:
从输入层到输出层的整个过程被称为 前向传播。因此,为了预测输出值,输入被从输入层传播到输出层。在这个传播过程中,它们在每一层上都乘以各自的权重,并在其上应用一个激活函数。完整的前向传播步骤如下所示:
前向传播的步骤可以在 Python 中实现如下:
def forward_prop(X):
z1 = np.dot(X,Wxh) + bh
a1 = sigmoid(z1)
z2 = np.dot(a1,Why) + by
y_hat = sigmoid(z2)
return y_hat
前向传播很酷,不是吗?但是我们如何知道神经网络生成的输出是否正确?我们定义一个称为 代价函数 () 或者 损失函数 (
) 的新函数,这个函数告诉我们神经网络的表现如何。有许多不同的代价函数。我们将使用均方误差作为一个代价函数,它可以定义为实际输出和预测输出之间平方差的均值:
这里, 是训练样本的数量,
是实际输出,
是预测输出。
好的,所以我们学到了成本函数用于评估我们的神经网络;也就是说,它告诉我们我们的神经网络在预测输出方面表现如何。但问题是我们的网络实际上是在哪里学习的?在前向传播中,网络只是尝试预测输出。但是它如何学习预测正确的输出呢?在接下来的部分,我们将探讨这个问题。
ANN 如何学习?
如果成本或损失非常高,那么意味着我们的网络没有预测出正确的输出。因此,我们的目标是最小化成本函数,这样我们神经网络的预测就会更好。我们如何最小化成本函数呢?也就是说,我们如何最小化损失?我们学到神经网络使用前向传播进行预测。因此,如果我们能在前向传播中改变一些值,我们就可以预测出正确的输出并最小化损失。但是在前向传播中可以改变哪些值呢?显然,我们不能改变输入和输出。我们现在只剩下权重和偏置值。请记住,我们只是随机初始化了权重矩阵。由于权重是随机的,它们不会是完美的。现在,我们将更新这些权重矩阵( 和
),使得我们的神经网络能够给出正确的输出。我们如何更新这些权重矩阵呢?这就是一个新技术,称为梯度下降。
通过梯度下降,神经网络学习随机初始化权重矩阵的最优值。有了最优的权重值,我们的网络可以预测正确的输出并最小化损失。
现在,我们将探讨如何使用梯度下降学习最优的权重值。梯度下降是最常用的优化算法之一。它用于最小化成本函数,使我们能够最小化误差并获得可能的最低误差值。但是梯度下降如何找到最优权重呢?让我们用一个类比来开始。
想象我们站在一个山顶上,如下图所示,我们想要到达山上的最低点。可能会有很多区域看起来像山上的最低点,但我们必须到达实际上是所有最低点中最低的点。
即,我们不应该固守在一个点上,认为它是最低点,当全局最低点存在时:
类似地,我们可以将成本函数表示如下。它是成本对权重的绘图。我们的目标是最小化成本函数。也就是说,我们必须到达成本最低的点。下图中的实心黑点显示了随机初始化的权重。如果我们将这一点向下移动,那么我们可以到达成本最低的点:
但是我们如何将这个点(初始权重)向下移动?我们如何下降并达到最低点?梯度用于从一个点移动到另一个点。因此,我们可以通过计算成本函数相对于该点(初始权重)的梯度来移动这个点(初始权重),即 。
梯度是实际上是切线斜率的导数,如下图所示。因此,通过计算梯度,我们向下移动并达到成本最低点。梯度下降是一种一阶优化算法,这意味着在执行更新时只考虑一阶导数:
因此,通过梯度下降,我们将权重移动到成本最小的位置。但是,我们如何更新权重呢?
由于前向传播的结果,我们处于输出层。现在我们将从输出层向输入层进行反向传播,并计算成本函数对输出层和输入层之间所有权重的梯度,以便最小化误差。在计算梯度之后,我们使用权重更新规则来更新旧权重:
这意味着 weights = weights -α * gradients。
什么是 ?它被称为学习率。如下图所示,如果学习率很小,那么我们将小步向下移动,我们的梯度下降可能会很慢。
如果学习率很大,那么我们会迈大步,梯度下降会很快,但我们可能无法达到全局最小值,并在局部最小值处卡住。因此,学习率应该被选择得最优:
从输出层到输入层反向传播网络并使用梯度下降更新网络权重以最小化损失的整个过程称为反向传播。现在我们对反向传播有了基本理解,我们将通过逐步学习详细了解,加深理解。我们将看一些有趣的数学内容,所以戴上你们的微积分帽子,跟随这些步骤。
因此,我们有两个权重,一个是,输入到隐藏层的权重,另一个是
,隐藏到输出层的权重。我们需要找到这两个权重的最优值,以减少误差。因此,我们需要计算损失函数
对这些权重的导数。因为我们正在反向传播,即从输出层到输入层,我们的第一个权重将是
。所以,现在我们需要计算
对
的导数。我们如何计算导数?首先,让我们回顾一下我们的损失函数,
:
从前述方程式中我们无法直接计算导数,因为没有
项。因此,我们不是直接计算导数,而是计算偏导数。让我们回顾一下我们的前向传播方程式:
首先,我们将计算对![]的偏导数,然后从![]中计算对![]的偏导数。从![]中,我们可以直接计算我们的导数![]。这基本上是链式法则。因此,对于![]对的导数如下所示:
现在,我们将计算前述方程式中的每一项:
这里,是我们的 sigmoid 激活函数的导数。我们知道 sigmoid 函数是![],因此 sigmoid 函数的导数为![]:
因此,将方程式(1)中的所有前述项代入,我们可以写成:
现在我们需要计算对于下一个权重![]的导数![]。
类似地,我们无法直接从 对
进行导数计算,因为我们在
中没有任何
项。所以,我们需要使用链式法则。让我们再次回顾前向传播的步骤:
现在,根据链式法则, 对
的导数如下:
我们已经看到如何计算前述方程式中的第一项;现在,让我们看看如何计算其余的项:
因此,将前述所有项代入方程 (3),我们可以写成:
在计算了 和
的梯度之后,我们将根据权重更新规则更新我们的初始权重:
就是这样!这就是我们如何更新网络的权重并最小化损失。如果你还不理解梯度下降,别担心!在第三章,梯度下降及其变体,我们将更详细地讨论基础知识并学习梯度下降及其多种变体。现在,让我们看看如何在 Python 中实现反向传播算法。
在方程 (2) 和 (4) 中,我们有项 ,所以我们不需要反复计算它们,只需称之为
delta2
:
delta2 = np.multiply(-(y-yHat),sigmoidPrime(z2))
现在,我们计算相对于 的梯度。参考方程 (2):
dJ_dWhy = np.dot(a1.T,delta2)
我们计算相对于 的梯度。参考方程 (4):
delta1 = np.dot(delta2,Why.T)*sigmoidPrime(z1)
dJ_dWxh = np.dot(X.T,delta1)
我们将根据我们的权重更新规则方程 (5) 和 (6) 更新权重如下:
Wxh = Wxh - alpha * dJ_dWhy
Why = Why - alpha * dJ_dWxh
后向传播的完整代码如下:
def backword_prop(y_hat, z1, a1, z2):
delta2 = np.multiply(-(y-y_hat),sigmoid_derivative(z2))
dJ_dWhy = np.dot(a1.T, delta2)
delta1 = np.dot(delta2,Why.T)*sigmoid_derivative(z1)
dJ_dWxh = np.dot(X.T, delta1)
Wxh = Wxh - alpha * dJ_dWhy
Why = Why - alpha * dJ_dWxh
return Wxh,Why
在继续之前,让我们熟悉神经网络中一些经常使用的术语:
-
前向传播:前向传播意味着从输入层向输出层进行前向传播。
-
后向传播:后向传播意味着从输出层向输入层进行反向传播。
-
轮次:轮次指定了神经网络看到我们整个训练数据的次数。因此,我们可以说一轮次等于所有训练样本的一次前向传播和一次反向传播。
-
批量大小:批量大小指定了在一次前向传播和一次反向传播中使用的训练样本数。
-
迭代次数:迭代次数意味着传递的次数,其中一次传递 = 一次前向传播 + 一次反向传播。
假设我们有 12,000 个训练样本,并且我们的批量大小为 6,000。我们将需要两次迭代才能完成一个轮次。也就是说,在第一次迭代中,我们传递前 6,000 个样本,并执行一次前向传播和一次反向传播;在第二次迭代中,我们传递接下来的 6,000 个样本,并执行一次前向传播和一次反向传播。两次迭代后,我们的神经网络将看到全部 12,000 个训练样本,这构成了一个轮次。
使用梯度检查调试梯度下降
我们刚刚学习了梯度下降的工作原理,以及如何为简单的两层网络从头开始编写梯度下降算法。但是,实现复杂神经网络的梯度下降并不是一件简单的任务。除了实现之外,调试复杂神经网络架构的梯度下降又是一项繁琐的任务。令人惊讶的是,即使存在一些有缺陷的梯度下降实现,网络也会学到一些东西。然而,显然,与无缺陷实现的梯度下降相比,它的表现不会很好。
如果模型没有给出任何错误并且即使在梯度下降算法的有缺陷实现下也能学到东西,那么我们如何评估和确保我们的实现是正确的呢?这就是我们使用梯度检查算法的原因。它将通过数值检查导数来验证我们的梯度下降实现是否正确。
梯度检查主要用于调试梯度下降算法,并验证我们是否有正确的实现。
好了。那么,梯度检查是如何工作的呢?在梯度检查中,我们基本上比较数值梯度和解析梯度。等等!什么是数值梯度和解析梯度?
解析梯度意味着我们通过反向传播计算的梯度。数值梯度是梯度的数值近似。让我们通过一个例子来探讨这个问题。假设我们有一个简单的平方函数,。
上述函数的解析梯度使用幂次法则计算如下:
现在,让我们看看如何数值近似梯度。我们不使用幂次法则来计算梯度,而是使用梯度的定义来计算梯度。我们知道,函数的梯度或斜率基本上给出了函数的陡峭程度。
因此,函数的梯度或斜率定义如下:
函数的梯度可以表示如下:
我们使用上述方程并在数值上近似计算梯度。这意味着我们手动计算函数的斜率,而不是像以下图表中显示的幂次法则一样:
通过幂次法则 (7) 计算梯度,并在 Python 中近似计算梯度 (8) 本质上给出了相同的值。让我们看看如何在 Python 中 (7) 和 (8) 给出相同的值。
定义平方函数:
def f(x):
return x**2
定义 epsilon 和输入值:
epsilon = 1e-2
x=3
计算解析梯度:
analytical_gradient = 2*x
print analytical_gradient
6
计算数值梯度:
numerical_gradient = (f(x+epsilon) - f(x-epsilon)) / (2*epsilon)
print numerical_gradient
6.000000000012662
正如您可能已经注意到的那样,计算平方函数的数值和解析梯度本质上给出了相同的值,即当 x =3 时为 6
。
在反向传播网络时,我们计算解析梯度以最小化成本函数。现在,我们需要确保我们计算的解析梯度是正确的。因此,让我们验证我们近似计算成本函数数值梯度。
相对于 ,
的梯度可以按如下数值近似:
它的表示如下:
我们检查解析梯度和近似数值梯度是否相同;如果不是,则我们的解析梯度计算存在错误。我们不想检查数值和解析梯度是否完全相同;因为我们只是近似计算数值梯度,我们检查解析梯度和数值梯度之间的差异作为错误。如果差异小于或等于一个非常小的数字,比如 1e-7,那么我们的实现是正确的。如果差异大于 1e-7,那么我们的实现是错误的。
而不是直接计算数值梯度和解析梯度之间的差异作为错误,我们计算相对误差。它可以定义为差异的比率与梯度绝对值的比率:
当相对误差的值小于或等于一个小的阈值值,比如 1e-7,那么我们的实现是正确的。如果相对误差大于 1e-7,那么我们的实现是错误的。现在让我们逐步看看如何在 Python 中实现梯度检查算法。
首先,我们计算权重。参考方程 (9):
weights_plus = weights + epsilon
weights_minus = weights - epsilon
计算 J_plus
和 J_minus
。参考方程 (9):
J_plus = forward_prop(x, weights_plus)
J_minus = forward_prop(x, weights_minus)
现在,我们可以按 (9) 给出的方式计算数值梯度如下:
numerical_grad = (J_plus - J_minus) / (2 * epsilon)
可以通过反向传播获得解析梯度:
analytical_grad = backword_prop(x, weights)
计算相对误差,如方程 (10) 所示:
numerator = np.linalg.norm(analytical_grad - numerical_grad)
denominator = np.linalg.norm(analytical_grad) + np.linalg.norm(numerical_grad)
relative_error = numerator / denominator
如果相对误差小于一个小的阈值,比如1e-7
,则我们的梯度下降实现是正确的;否则,是错误的:
if relative_error < 1e-7:
print ("The gradient is correct!")
else:
print ("The gradient is wrong!")
因此,借助梯度检查,我们确保我们的梯度下降算法没有错误。
将所有内容整合在一起
将我们迄今为止学到的所有概念综合起来,我们将看到如何从头开始构建一个神经网络。我们将了解神经网络如何学习执行 XOR 门操作。XOR 门只在其输入中恰好有一个为 1 时返回 1,否则返回 0,如下表所示:
从头开始构建神经网络
要执行 XOR 门操作,我们构建了一个简单的两层神经网络,如下图所示。您可以看到,我们有一个具有两个节点的输入层,一个具有五个节点的隐藏层和一个包含一个节点的输出层:
我们将逐步理解神经网络如何学习 XOR 逻辑:
- 首先,导入库:
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
- 根据前面的 XOR 表准备数据:
X = np.array([ [0, 1], [1, 0], [1, 1],[0, 0] ])
y = p.array([ [1], [1], [0], [0]])
- 定义每层中的节点数:
num_input = 2
num_hidden = 5
num_output = 1
- 随机初始化权重和偏置。首先,我们初始化输入到隐藏层的权重:
Wxh = np.random.randn(num_input,num_hidden)
bh = np.zeros((1,num_hidden))
- 现在,我们将隐藏层到输出层的权重初始化:
Why = np.random.randn (num_hidden,num_output)
by = np.zeros((1,num_output))
- 定义 sigmoid 激活函数:
def sigmoid(z):
return 1 / (1+np.exp(-z))
- 定义 sigmoid 函数的导数:
def sigmoid_derivative(z):
return np.exp(-z)/((1+np.exp(-z))**2)
- 定义前向传播:
def forward_prop(X,Wxh,Why):
z1 = np.dot(X,Wxh) + bh
a1 = sigmoid(z1)
z2 = np.dot(a1,Why) + by
y_hat = sigmoid(z2)
return z1,a1,z2,y_hat
- 定义反向传播:
def backword_prop(y_hat, z1, a1, z2):
delta2 = np.multiply(-(y-y_hat),sigmoid_derivative(z2))
dJ_dWhy = np.dot(a1.T, delta2)
delta1 = np.dot(delta2,Why.T)*sigmoid_derivative(z1)
dJ_dWxh = np.dot(X.T, delta1)
return dJ_dWxh, dJ_dWhy
- 定义成本函数:
def cost_function(y, y_hat):
J = 0.5*sum((y-y_hat)**2)
return J
- 设置学习率和训练迭代次数:
alpha = 0.01
num_iterations = 5000
- 现在,让我们用以下代码开始训练网络:
cost =[]
for i in range(num_iterations):
z1,a1,z2,y_hat = forward_prop(X,Wxh,Why)
dJ_dWxh, dJ_dWhy = backword_prop(y_hat, z1, a1, z2)
#update weights
Wxh = Wxh -alpha * dJ_dWxh
Why = Why -alpha * dJ_dWhy
#compute cost
c = cost_function(y, y_hat)
cost.append(c)
- 绘制成本函数:
plt.grid()
plt.plot(range(num_iteratins),cost)
plt.title('Cost Function')
plt.xlabel('Training Iterations')
plt.ylabel('Cost')
正如您可以在下面的图中观察到的那样,损失随着训练迭代次数的增加而减少:
因此,在本章中,我们对人工神经网络及其学习方式有了全面的了解。
摘要
我们从理解深度学习是什么及其与机器学习的区别开始本章。后来,我们学习了生物和人工神经元的工作原理,然后探讨了 ANN 中的输入、隐藏和输出层,以及几种激活函数。
接下来,我们学习了前向传播是什么,以及 ANN 如何使用前向传播来预测输出。在此之后,我们学习了 ANN 如何使用反向传播来学习和优化。我们学习了一种称为梯度下降的优化算法,帮助神经网络最小化损失并进行正确预测。我们还学习了梯度检查,一种用于评估梯度下降的技术。在本章的结尾,我们实现了一个从头开始的神经网络来执行 XOR 门操作。
在下一章中,我们将学习一个名为TensorFlow的最强大和最广泛使用的深度学习库。
问题
让我们通过回答以下问题来评估我们新获得的知识:
-
深度学习与机器学习有何不同?
-
deep 在深度学习中是什么意思?
-
我们为什么使用激活函数?
-
解释 dying ReLU 问题。
-
定义前向传播。
-
什么是反向传播?
-
解释梯度检查。
进一步阅读
您还可以查看以下一些资源以获取更多信息:
-
从这个精彩的视频了解更多关于梯度下降的知识:
www.youtube.com/watch?v=IHZwWFHWa-w
-
学习如何从头开始实现一个神经网络来识别手写数字:
github.com/sar-gupta/neural-network-from-scratch
第二章:认识 TensorFlow
在本章中,我们将学习 TensorFlow,这是最流行的深度学习库之一。在本书中,我们将使用 TensorFlow 从头开始构建深度学习模型。因此,在本章中,我们将了解 TensorFlow 及其功能。我们还将学习 TensorFlow 提供的用于模型可视化的工具 TensorBoard。接下来,我们将学习如何使用 TensorFlow 执行手写数字分类,以构建我们的第一个神经网络。随后,我们将了解 TensorFlow 2.0,这是 TensorFlow 的最新版本。我们将学习 TensorFlow 2.0 与其早期版本的区别,以及它如何使用 Keras 作为其高级 API。
在本章中,我们将涵盖以下主题:
-
TensorFlow
-
计算图和会话
-
变量、常量和占位符
-
TensorBoard
-
TensorFlow 中的手写数字分类
-
TensorFlow 中的数学运算
-
TensorFlow 2.0 和 Keras
TensorFlow 是什么?
TensorFlow 是来自 Google 的开源软件库,广泛用于数值计算。它是构建深度学习模型的最流行的库之一。它高度可扩展,可以运行在多个平台上,如 Windows、Linux、macOS 和 Android。最初由 Google Brain 团队的研究人员和工程师开发。
TensorFlow 支持在包括 CPU、GPU 和 TPU(张量处理单元)在内的所有设备上执行,还支持移动和嵌入式平台。由于其灵活的架构和易于部署,它已成为许多研究人员和科学家构建深度学习模型的流行选择。
在 TensorFlow 中,每个计算都由数据流图表示,也称为计算图,其中节点表示操作,如加法或乘法,边表示张量。数据流图也可以在许多不同的平台上共享和执行。TensorFlow 提供了一种称为 TensorBoard 的可视化工具,用于可视化数据流图。
张量只是一个多维数组。因此,当我们说 TensorFlow 时,它实际上是在计算图中流动的多维数组(张量)。
你可以通过在终端中输入以下命令轻松地通过pip
安装 TensorFlow。我们将安装 TensorFlow 1.13.1:
pip install tensorflow==1.13.1
我们可以通过运行以下简单的Hello TensorFlow!
程序来检查 TensorFlow 的成功安装:
import tensorflow as tf
hello = tf.constant("Hello TensorFlow!")
sess = tf.Session()
print(sess.run(hello))
前面的程序应该打印出Hello TensorFlow!
。如果出现任何错误,那么您可能没有正确安装 TensorFlow。
理解计算图和会话
正如我们所学到的,TensorFlow 中的每个计算都由计算图表示。它们由多个节点和边缘组成,其中节点是数学操作,如加法和乘法,边缘是张量。计算图非常有效地优化资源并促进分布式计算。
计算图由几个 TensorFlow 操作组成,排列成节点图。
让我们考虑一个基本的加法操作:
import tensorflow as tf
x = 2
y = 3
z = tf.add(x, y, name='Add')
上述代码的计算图将如下所示:
当我们在构建一个非常复杂的神经网络时,计算图帮助我们理解网络架构。例如,让我们考虑一个简单的层,。其计算图将表示如下:
计算图中有两种依赖类型,称为直接和间接依赖。假设我们有b
节点,其输入依赖于a
节点的输出;这种依赖称为直接依赖,如下所示的代码:
a = tf.multiply(8,5)
b = tf.multiply(a,1)
当b
节点的输入不依赖于a
节点时,这被称为间接依赖,如下所示的代码:
a = tf.multiply(8,5)
b = tf.multiply(4,3)
因此,如果我们能理解这些依赖关系,我们就能在可用资源中分配独立的计算,并减少计算时间。每当我们导入 TensorFlow 时,会自动创建一个默认图,并且我们创建的所有节点都与默认图相关联。我们还可以创建自己的图而不是使用默认图,当在一个文件中构建多个不相互依赖的模型时,这非常有用。可以使用tf.Graph()
创建 TensorFlow 图,如下所示:
graph = tf.Graph()
with graph.as_default():
z = tf.add(x, y, name='Add')
如果我们想要清除默认图(即清除先前定义的变量和操作),可以使用tf.reset_default_graph()
。
会话
将创建一个包含节点和边缘张量的计算图,为了执行该图,我们使用 TensorFlow 会话。
可以使用tf.Session()
创建 TensorFlow 会话,如下所示的代码,它将分配内存以存储变量的当前值:
sess = tf.Session()
创建会话后,我们可以使用sess.run()
方法执行我们的图。
TensorFlow 中的每个计算都由计算图表示,因此我们需要为所有事物运行计算图。也就是说,为了在 TensorFlow 上计算任何内容,我们需要创建一个 TensorFlow 会话。
让我们执行以下代码来执行两个数字的乘法:
a = tf.multiply(3,3)
print(a)
而不是打印9
,上述代码将打印一个 TensorFlow 对象,Tensor("Mul:0", shape=(), dtype=int32)
。
正如我们之前讨论的,每当导入 TensorFlow 时,将自动创建一个默认计算图,并且所有节点都将附加到该图中。因此,当我们打印 a
时,它只返回 TensorFlow 对象,因为尚未计算 a
的值,因为尚未执行计算图。
为了执行图,我们需要初始化并运行 TensorFlow 会话,如下所示:
a = tf.multiply(3,3)
with tf.Session as sess:
print(sess.run(a))
上述代码将打印 9
。
变量、常量和占位符
变量、常量和占位符是 TensorFlow 的基本元素。但是,总会有人对这三者之间感到困惑。让我们逐个看看每个元素,并学习它们之间的区别。
变量
变量是用来存储值的容器。变量作为计算图中几个其他操作的输入。可以使用 tf.Variable()
函数创建变量,如下面的代码所示:
x = tf.Variable(13)
让我们使用 tf.Variable()
创建一个名为 W
的变量,如下所示:
W = tf.Variable(tf.random_normal([500, 111], stddev=0.35), name="weights")
如前面的代码所示,我们通过从标准差为 0.35
的正态分布中随机抽取值来创建变量 W
。
tf.Variable()
中的参数 name
被称为什么?
它用于在计算图中设置变量的名称。因此,在上述代码中,Python 将变量保存为 W
,但在 TensorFlow 图中,它将保存为 weights
。
我们还可以使用 initialized_value()
从另一个变量中初始化新变量的值。例如,如果我们想要创建一个名为 weights_2
的新变量,并使用先前定义的 weights
变量的值,可以按以下方式完成:
W2 = tf.Variable(weights.initialized_value(), name="weights_2")
然而,在定义变量之后,我们需要初始化计算图中的所有变量。可以使用 tf.global_variables_initializer()
完成此操作。
一旦我们创建了会话,首先运行初始化操作,这将初始化所有已定义的变量,然后才能运行其他操作,如下所示:
x = tf.Variable(1212)
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
print sess.run(x)
我们还可以使用 tf.get_variable()
创建 TensorFlow 变量。它需要三个重要参数,即 name
、shape
和 initializer
。
与 tf.Variable()
不同,我们不能直接将值传递给 tf.get_variable()
;相反,我们使用 initializer
。有几种初始化器可用于初始化值。例如,tf.constant_initializer(value)
使用常量值初始化变量,tf.random_normal_initializer(mean, stddev)
使用指定均值和标准差的随机正态分布初始化变量。
使用 tf.Variable()
创建的变量不能共享,每次调用 tf.Variable()
时都会创建一个新变量。但是 tf.get_variable()
会检查计算图中是否存在指定参数的现有变量。如果变量已存在,则将重用它;否则将创建一个新变量:
W3 = tf.get_variable(name = 'weights', shape = [500, 111], initializer = random_normal_initializer()))
因此,上述代码检查是否存在与给定参数匹配的任何变量。如果是,则将重用它;否则,将创建一个新变量。
由于我们使用tf.get_variable()
重用变量,为了避免名称冲突,我们使用tf.variable_scope
,如下面的代码所示。变量作用域基本上是一种命名技术,在作用域内为变量添加前缀以避免命名冲突:
with tf.variable_scope("scope"):
a = tf.get_variable('x', [2])
with tf.variable_scope("scope", reuse = True):
b = tf.get_variable('x', [2])
如果您打印a.name
和b.name
,则它将返回相同的名称scope/x:0
。正如您所见,我们在名为scope
的变量作用域中指定了reuse=True
参数,这意味着变量可以被共享。如果我们不设置reuse=True
,则会出现错误,提示变量已经存在。
建议使用tf.get_variable()
而不是tf.Variable()
,因为tf.get_variable
允许您共享变量,并且可以使代码重构更容易。
常量
常量与变量不同,不能改变其值。也就是说,常量是不可变的。一旦为它们分配了值,就不能在整个过程中更改它们。我们可以使用tf.constant()
创建常量,如下面的代码所示:
x = tf.constant(13)
占位符和 feed 字典
我们可以将占位符视为变量,其中仅定义类型和维度,但不分配值。占位符的值将在运行时通过数据填充到计算图中。占位符是没有值的定义。
可以使用tf.placeholder()
来定义占位符。它接受一个可选参数shape
,表示数据的维度。如果shape
设置为None
,则可以在运行时提供任意大小的数据。占位符可以定义如下:
x = tf.placeholder("float", shape=None)
简单来说,我们使用tf.Variable
存储数据,使用tf.placeholder
来提供外部数据。
让我们通过一个简单的例子来更好地理解占位符:
x = tf.placeholder("float", None)
y = x +3
with tf.Session() as sess:
result = sess.run(y)
print(result)
如果我们运行上述代码,则会返回错误,因为我们试图计算y
,其中y = x + 3
,而x
是一个占位符,其值尚未分配。正如我们所学的,占位符的值将在运行时分配。我们使用feed_dict
参数来分配占位符的值。feed_dict
参数基本上是一个字典,其中键表示占位符的名称,值表示占位符的值。
如您在下面的代码中所见,我们设置feed_dict = {x:5}
,这意味着x
占位符的值为5
:
with tf.Session() as sess:
result = sess.run(y, feed_dict={x: 5})
print(result)
前面的代码返回8.0
。
如果我们想为x
使用多个值会怎样?由于我们没有为占位符定义任何形状,它可以接受任意数量的值,如下面的代码所示:
with tf.Session() as sess:
result = sess.run(y, feed_dict={x: [3,6,9]})
print(result)
它将返回以下内容:
[ 6\. 9\. 12.]
假设我们将x
的形状定义为[None,2]
,如下面的代码所示:
x = tf.placeholder("float", [None, 2])
这意味着 x
可以取任意行但列数为 2
的矩阵,如以下代码所示:
with tf.Session() as sess:
x_val = [[1, 2,],
[3,4],
[5,6],
[7,8],]
result = sess.run(y, feed_dict={x: x_val})
print(result)
上述代码返回以下内容:
[[ 4\. 5.]
[ 6\. 7.]
[ 8\. 9.]
[10\. 11.]]
介绍 TensorBoard
TensorBoard 是 TensorFlow 的可视化工具,可用于显示计算图。它还可以用来绘制各种定量指标和几个中间计算的结果。当我们训练一个非常深的神经网络时,如果需要调试网络,会变得很困惑。因此,如果我们能在 TensorBoard 中可视化计算图,就能轻松理解这些复杂模型,进行调试和优化。TensorBoard 还支持共享。
如以下截图所示,TensorBoard 面板由几个选项卡组成: SCALARS,IMAGES,AUDIO,GRAPHS,DISTRIBUTIONS,HISTOGRAMS 和 EMBEDDINGS:
选项卡的含义很明显。 SCALARS 选项卡显示有关程序中使用的标量变量的有用信息。例如,它显示标量变量名为损失的值如何随着多次迭代而变化。
GRAPHS 选项卡显示计算图。 DISTRIBUTIONS 和 HISTOGRAMS 选项卡显示变量的分布。例如,我们模型的权重分布和直方图可以在这些选项卡下看到。 EMBEDDINGS 选项卡用于可视化高维向量,如词嵌入(我们将在第七章,学习文本表示中详细学习)。
让我们构建一个基本的计算图,并在 TensorBoard 中进行可视化。假设我们有四个变量,如下所示:
x = tf.constant(1,name='x')
y = tf.constant(1,name='y')
a = tf.constant(3,name='a')
b = tf.constant(3,name='b')
让我们将 x
和 y
以及 a
和 b
相乘,并将它们保存为 prod1
和 prod2
,如以下代码所示:
prod1 = tf.multiply(x,y,name='prod1')
prod2 = tf.multiply(a,b,name='prod2')
将 prod1
和 prod2
相加并存储在 sum
中:
sum = tf.add(prod1,prod2,name='sum')
现在,我们可以在 TensorBoard 中可视化所有这些操作。为了在 TensorBoard 中进行可视化,我们首先需要保存我们的事件文件。可以使用 tf.summary.FileWriter()
来完成。它需要两个重要参数,logdir
和 graph
。
如其名称所示,logdir
指定我们希望存储图形的目录,graph
指定我们希望存储的图形:
with tf.Session() as sess:
writer = tf.summary.FileWriter(logdir='./graphs',graph=sess.graph)
print(sess.run(sum))
在上述代码中,graphs
是我们存储事件文件的目录,sess.graph
指定了我们 TensorFlow 会话中的当前图。因此,我们正在将 TensorFlow 会话中的当前图存储在 graphs
目录中。
要启动 TensorBoard,请转到您的终端,找到工作目录,并键入以下内容:
tensorboard --logdir=graphs --port=8000
logdir
参数指示事件文件存储的目录,port
是端口号。运行上述命令后,打开浏览器并输入 http://localhost:8000/
。
在 TensorBoard 面板中,GRAPHS 选项卡下,您可以看到计算图:
正如您可能注意到的,我们定义的所有操作都清楚地显示在图中。
创建一个命名作用域
作用域用于降低复杂性,并帮助我们通过将相关节点分组在一起来更好地理解模型。使用命名作用域有助于在图中分组相似的操作。当我们构建复杂的架构时,它非常方便。可以使用 tf.name_scope()
创建作用域。在前面的示例中,我们执行了两个操作,Product
和 sum
。我们可以简单地将它们分组为两个不同的命名作用域,如 Product
和 sum
。
在前面的部分中,我们看到了 prod1
和 prod2
如何执行乘法并计算结果。我们将定义一个名为 Product
的命名作用域,并将 prod1
和 prod2
操作分组,如以下代码所示:
with tf.name_scope("Product"):
with tf.name_scope("prod1"):
prod1 = tf.multiply(x,y,name='prod1')
with tf.name_scope("prod2"):
prod2 = tf.multiply(a,b,name='prod2')
现在,为 sum
定义命名作用域:
with tf.name_scope("sum"):
sum = tf.add(prod1,prod2,name='sum')
将文件存储在 graphs
目录中:
with tf.Session() as sess:
writer = tf.summary.FileWriter('./graphs', sess.graph)
print(sess.run(sum))
在 TensorBoard 中可视化图形:
tensorboard --logdir=graphs --port=8000
正如你可能注意到的,现在我们只有两个节点,sum 和 Product:
一旦我们双击节点,我们就能看到计算是如何进行的。正如你所见,prod1 和 prod2 节点被分组在 Product 作用域下,它们的结果被发送到 sum 节点,在那里它们将被相加。你可以看到 prod1 和 prod2 节点如何计算它们的值:
前面的例子只是一个简单的示例。当我们在处理包含大量操作的复杂项目时,命名作用域帮助我们将相似的操作分组在一起,并且能够更好地理解计算图。
使用 TensorFlow 进行手写数字分类
将我们迄今学到的所有概念整合在一起,我们将看到如何使用 TensorFlow 构建一个神经网络来识别手写数字。如果你最近一直在玩深度学习,那么你一定听说过 MNIST 数据集。它被称为深度学习的hello world。它包含了 55000 个手写数字数据点(0 到 9)。
在这一节中,我们将看到如何使用我们的神经网络来识别这些手写数字,并且我们会掌握 TensorFlow 和 TensorBoard。
导入所需的库
作为第一步,让我们导入所有所需的库:
import warnings
warnings.filterwarnings('ignore')
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
tf.logging.set_verbosity(tf.logging.ERROR)
import matplotlib.pyplot as plt
%matplotlib inline
加载数据集
加载数据集,使用以下代码:
mnist = input_data.read_data_sets("data/mnist", one_hot=True)
在前面的代码中,data/mnist
表示我们存储 MNIST 数据集的位置,而 one_hot=True
表示我们正在对标签(0 到 9)进行 one-hot 编码。
通过执行以下代码,我们将看到我们的数据中包含什么:
print("No of images in training set {}".format(mnist.train.images.shape))
print("No of labels in training set {}".format(mnist.train.labels.shape))
print("No of images in test set {}".format(mnist.test.images.shape))
print("No of labels in test set {}".format(mnist.test.labels.shape))
No of images in training set (55000, 784)
No of labels in training set (55000, 10)
No of images in test set (10000, 784)
No of labels in test set (10000, 10)
我们在训练集中有 55000
张图像,每个图像的大小是 784
,我们有 10
个标签,实际上是从 0 到 9。类似地,我们在测试集中有 10000
张图像。
现在,我们将绘制一个输入图像,看看它的样子:
img1 = mnist.train.images[0].reshape(28,28)
plt.imshow(img1, cmap='Greys')
因此,我们的输入图像看起来如下:
定义每个层中神经元的数量
我们将构建一个具有三个隐藏层和一个输出层的四层神经网络。由于输入图像的大小为 784
,我们将 num_input
设置为 784
,并且由于有 10 个手写数字(0 到 9),我们在输出层设置了 10
个神经元。我们如下定义每一层的神经元数量:
#number of neurons in input layer
num_input = 784
#num of neurons in hidden layer 1
num_hidden1 = 512
#num of neurons in hidden layer 2
num_hidden2 = 256
#num of neurons in hidden layer 3
num_hidden_3 = 128
#num of neurons in output layer
num_output = 10
定义占位符
正如我们所学,我们首先需要为 input
和 output
定义占位符。占位符的值将通过 feed_dict
在运行时传入:
with tf.name_scope('input'):
X = tf.placeholder("float", [None, num_input])
with tf.name_scope('output'):
Y = tf.placeholder("float", [None, num_output])
由于我们有一个四层网络,我们有四个权重和四个偏置。我们通过从标准偏差为 0.1
的截断正态分布中抽取值来初始化我们的权重。记住,权重矩阵的维度应该是前一层神经元的数量 x 当前层神经元的数量。例如,权重矩阵 w3
的维度应该是隐藏层 2 中的神经元数 x 隐藏层 3 中的神经元数。
我们通常将所有权重定义在一个字典中,如下所示:
with tf.name_scope('weights'):
weights = {
'w1': tf.Variable(tf.truncated_normal([num_input, num_hidden1], stddev=0.1),name='weight_1'),
'w2': tf.Variable(tf.truncated_normal([num_hidden1, num_hidden2], stddev=0.1),name='weight_2'),
'w3': tf.Variable(tf.truncated_normal([num_hidden2, num_hidden_3], stddev=0.1),name='weight_3'),
'out': tf.Variable(tf.truncated_normal([num_hidden_3, num_output], stddev=0.1),name='weight_4'),
}
偏置的形状应该是当前层中的神经元数。例如,b2
偏置的维度是隐藏层 2 中的神经元数。我们在所有层中将偏置值设置为常数 0.1
:
with tf.name_scope('biases'):
biases = {
'b1': tf.Variable(tf.constant(0.1, shape=[num_hidden1]),name='bias_1'),
'b2': tf.Variable(tf.constant(0.1, shape=[num_hidden2]),name='bias_2'),
'b3': tf.Variable(tf.constant(0.1, shape=[num_hidden_3]),name='bias_3'),
'out': tf.Variable(tf.constant(0.1, shape=[num_output]),name='bias_4')
}
前向传播
现在我们将定义前向传播操作。我们在所有层中使用 ReLU 激活函数。在最后一层中,我们将应用 sigmoid
激活函数,如下所示的代码:
with tf.name_scope('Model'):
with tf.name_scope('layer1'):
layer_1 = tf.nn.relu(tf.add(tf.matmul(X, weights['w1']), biases['b1']) )
with tf.name_scope('layer2'):
layer_2 = tf.nn.relu(tf.add(tf.matmul(layer_1, weights['w2']), biases['b2']))
with tf.name_scope('layer3'):
layer_3 = tf.nn.relu(tf.add(tf.matmul(layer_2, weights['w3']), biases['b3']))
with tf.name_scope('output_layer'):
y_hat = tf.nn.sigmoid(tf.matmul(layer_3, weights['out']) + biases['out'])
计算损失和反向传播
接下来,我们将定义我们的损失函数。我们将使用 softmax 交叉熵作为我们的损失函数。TensorFlow 提供了 tf.nn.softmax_cross_entropy_with_logits()
函数来计算 softmax 交叉熵损失。它接受两个参数作为输入,logits
和 labels
:
-
logits
参数指定了网络预测的logits
;例如,y_hat
-
labels
参数指定了实际的标签;例如,真实标签Y
我们使用 tf.reduce_mean()
取 loss
函数的均值:
with tf.name_scope('Loss'):
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=y_hat,labels=Y))
现在,我们需要使用反向传播来最小化损失。别担心!我们不必手动计算所有权重的导数。相反,我们可以使用 TensorFlow 的优化器。在本节中,我们使用 Adam 优化器。它是我们在《第一章》深度学习导论中学到的梯度下降优化技术的一个变体。在《第三章》梯度下降及其变体中,我们将深入探讨细节,看看 Adam 优化器和其他几种优化器的工作原理。现在,让我们说我们使用 Adam 优化器作为我们的反向传播算法:
learning_rate = 1e-4
optimizer = tf.train.AdamOptimizer(learning_rate).minimize(loss)
计算精度
我们计算模型的精度如下:
-
参数
y_hat
表示我们模型每个类别的预测概率。由于我们有10
类别,我们将有10
个概率。如果在位置7
处的概率很高,则表示我们的网络以高概率预测输入图像为数字7
。函数tf.argmax()
返回最大值的索引。因此,tf.argmax(y_hat,1)
给出概率高的索引位置。因此,如果索引7
处的概率很高,则返回7
。 -
参数
Y
表示实际标签,它们是独热编码的值。也就是说,除了实际图像的位置处为1
外,在所有位置处都是0
。例如,如果输入图像是7
,则Y
在索引7
处为1
,其他位置为0
。因此,tf.argmax(Y,1)
返回7
,因为这是我们有高值1
的位置。
因此,tf.argmax(y_hat,1)
给出了预测的数字,而 tf.argmax(Y,1)
给出了实际的数字。
函数 tf.equal(x, y)
接受 x
和 y
作为输入,并返回 (x == y) 的逻辑值。因此,correct_pred = tf.equal(predicted_digit,actual_digit)
包含了当实际和预测的数字相同时为 True
,不同时为 False
。我们使用 TensorFlow 的 cast
操作将 correct_pred
中的布尔值转换为浮点值,即 tf.cast(correct_pred, tf.float32)
。将它们转换为浮点值后,我们使用 tf.reduce_mean()
取平均值。
因此,tf.reduce_mean(tf.cast(correct_pred, tf.float32))
给出了平均正确预测值:
with tf.name_scope('Accuracy'):
predicted_digit = tf.argmax(y_hat, 1)
actual_digit = tf.argmax(Y, 1)
correct_pred = tf.equal(predicted_digit,actual_digit)
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
创建摘要
我们还可以可视化模型在多次迭代过程中损失和准确率的变化。因此,我们使用 tf.summary()
来获取变量的摘要。由于损失和准确率是标量变量,我们使用 tf.summary.scalar()
,如下所示:
tf.summary.scalar("Accuracy", accuracy)
tf.summary.scalar("Loss", loss)
接下来,我们合并图中使用的所有摘要,使用 tf.summary.merge_all()
。我们这样做是因为当我们有许多摘要时,运行和存储它们会变得低效,所以我们在会话中一次性运行它们,而不是多次运行:
merge_summary = tf.summary.merge_all()
训练模型
现在,是时候训练我们的模型了。正如我们所学的,首先,我们需要初始化所有变量:
init = tf.global_variables_initializer()
定义批大小、迭代次数和学习率,如下所示:
learning_rate = 1e-4
num_iterations = 1000
batch_size = 128
启动 TensorFlow 会话:
with tf.Session() as sess:
初始化所有变量:
sess.run(init)
保存事件文件:
summary_writer = tf.summary.FileWriter('./graphs', graph=sess.graph)
训练模型一定数量的迭代次数:
for i in range(num_iterations):
根据批大小获取一批数据:
batch_x, batch_y = mnist.train.next_batch(batch_size)
训练网络:
sess.run(optimizer, feed_dict={ X: batch_x, Y: batch_y})
每 100 次迭代打印一次 loss
和 accuracy
:
if i % 100 == 0:
batch_loss, batch_accuracy,summary = sess.run(
[loss, accuracy, merge_summary], feed_dict={X: batch_x, Y: batch_y}
)
#store all the summaries
summary_writer.add_summary(summary, i)
print('Iteration: {}, Loss: {}, Accuracy: {}'.format(i,batch_loss,batch_accuracy))
如您从以下输出中注意到的那样,损失减少,准确率在各种训练迭代中增加:
Iteration: 0, Loss: 2.30789709091, Accuracy: 0.1171875
Iteration: 100, Loss: 1.76062202454, Accuracy: 0.859375
Iteration: 200, Loss: 1.60075569153, Accuracy: 0.9375
Iteration: 300, Loss: 1.60388696194, Accuracy: 0.890625
Iteration: 400, Loss: 1.59523034096, Accuracy: 0.921875
Iteration: 500, Loss: 1.58489584923, Accuracy: 0.859375
Iteration: 600, Loss: 1.51407408714, Accuracy: 0.953125
Iteration: 700, Loss: 1.53311181068, Accuracy: 0.9296875
Iteration: 800, Loss: 1.57677125931, Accuracy: 0.875
Iteration: 900, Loss: 1.52060437202, Accuracy: 0.9453125
在 TensorBoard 中可视化图表
训练后,我们可以在 TensorBoard 中可视化我们的计算图,如下图所示。正如您所见,我们的模型接受输入、权重和偏差作为输入,并返回输出。我们根据模型的输出计算损失和准确性。通过计算梯度和更新权重来最小化损失。我们可以在下图中观察到所有这些:
如果我们双击并展开模型,我们可以看到我们有三个隐藏层和一个输出层:
同样地,我们可以双击并查看每个节点。例如,如果我们打开权重,我们可以看到四个权重如何使用截断正态分布进行初始化,并且如何使用 Adam 优化器进行更新:
正如我们所学到的,计算图帮助我们理解每个节点发生的情况。我们可以通过双击准确性节点来查看如何计算准确性:
记住,我们还存储了loss
和accuracy
变量的摘要。我们可以在 TensorBoard 的 SCALARS 选项卡下找到它们,如下面的截图所示。我们可以看到损失如何随迭代而减少,如下图所示:
下面的截图显示准确性随迭代次数增加的情况:
引入急切执行
TensorFlow 中的急切执行更符合 Python 风格,允许快速原型设计。与图模式不同,在图模式中,我们每次执行操作都需要构建一个图,而急切执行遵循命令式编程范式,可以立即执行任何操作,无需创建图,就像在 Python 中一样。因此,使用急切执行,我们可以告别会话和占位符。与图模式不同,它还通过立即运行时错误使得调试过程更加简单。
例如,在图模式中,要计算任何内容,我们需要运行会话。如下面的代码所示,要评估z
的值,我们必须运行 TensorFlow 会话:
x = tf.constant(11)
y = tf.constant(11)
z = x*y
with tf.Session() as sess:
print sess.run(z)
使用急切执行,我们无需创建会话;我们可以像在 Python 中一样简单地计算z
。为了启用急切执行,只需调用tf.enable_eager_execution()
函数:
x = tf.constant(11)
y = tf.constant(11)
z = x*y
print z
它将返回以下内容:
<tf.Tensor: id=789, shape=(), dtype=int32, numpy=121>
为了获取输出值,我们可以打印以下内容:
z.numpy()
121
TensorFlow 中的数学操作
现在,我们将使用急切执行模式探索 TensorFlow 中的一些操作:
x = tf.constant([1., 2., 3.])
y = tf.constant([3., 2., 1.])
让我们从一些基本算术操作开始。
使用tf.add
来添加两个数:
sum = tf.add(x,y)
sum.numpy()
array([4., 4., 4.], dtype=float32)
tf.subtract
函数用于找出两个数之间的差异:
difference = tf.subtract(x,y)
difference.numpy()
array([-2., 0., 2.], dtype=float32)
tf.multiply
函数用于两个数的乘法:
product = tf.multiply(x,y)
product.numpy()
array([3., 4., 3.], dtype=float32)
使用tf.divide
除两个数:
division = tf.divide(x,y)
division.numpy()
array([0.33333334, 1\. , 3\. ], dtype=float32)
点积可以计算如下:
dot_product = tf.reduce_sum(tf.multiply(x, y))
dot_product.numpy()
10.0
下面,让我们找到最小和最大元素的索引:
x = tf.constant([10, 0, 13, 9])
最小值的索引是使用tf.argmin()
计算的:
tf.argmin(x).numpy()
1
最大值的索引是使用tf.argmax()
计算的:
tf.argmax(x).numpy()
2
运行以下代码以找到x
和y
之间的平方差:
x = tf.Variable([1,3,5,7,11])
y = tf.Variable([1])
tf.math.squared_difference(x,y).numpy()
[ 0, 4, 16, 36, 100]
让我们尝试类型转换;即,从一种数据类型转换为另一种。
打印x
的类型:
print x.dtype
tf.int32
我们可以使用tf.cast
将x
的类型(tf.int32
)转换为tf.float32
,如下所示:
x = tf.cast(x, dtype=tf.float32)
现在,检查x
的类型。它将是tf.float32
,如下所示:
print x.dtype
tf.float32
按列连接两个矩阵:
x = [[3,6,9], [7,7,7]]
y = [[4,5,6], [5,5,5]]
按行连接矩阵:
tf.concat([x, y], 0).numpy()
array([[3, 6, 9],
[7, 7, 7],
[4, 5, 6],
[5, 5, 5]], dtype=int32)
使用以下代码按列连接矩阵:
tf.concat([x, y], 1).numpy()
array([[3, 6, 9, 4, 5, 6],
[7, 7, 7, 5, 5, 5]], dtype=int32)
使用stack
函数堆叠x
矩阵:
tf.stack(x, axis=1).numpy()
array([[3, 7],
[6, 7],
[9, 7]], dtype=int32)
现在,让我们看看如何执行reduce_mean
操作:
x = tf.Variable([[1.0, 5.0], [2.0, 3.0]])
x.numpy()
array([[1., 5.],
[2., 3.]]
计算x
的均值;即,(1.0 + 5.0 + 2.0 + 3.0) / 4:
tf.reduce_mean(input_tensor=x).numpy()
2.75
计算行的均值;即,(1.0+5.0)/2, (2.0+3.0)/2:
tf.reduce_mean(input_tensor=x, axis=0).numpy()
array([1.5, 4\. ], dtype=float32)
计算列的均值;即,(1.0+5.0)/2.0, (2.0+3.0)/2.0:
tf.reduce_mean(input_tensor=x, axis=1, keepdims=True).numpy()
array([[3\. ],
[2.5]], dtype=float32)
从概率分布中绘制随机值:
tf.random.normal(shape=(3,2), mean=10.0, stddev=2.0).numpy()
tf.random.uniform(shape = (3,2), minval=0, maxval=None, dtype=tf.float32,).numpy()
计算 softmax 概率:
x = tf.constant([7., 2., 5.])
tf.nn.softmax(x).numpy()
array([0.8756006 , 0.00589975, 0.11849965], dtype=float32)
现在,我们将看看如何计算梯度。
定义square
函数:
def square(x):
return tf.multiply(x, x)
可以使用tf.GradientTape
计算前述square
函数的梯度,如下所示:
with tf.GradientTape(persistent=True) as tape:
print square(6.).numpy()
36.0
更多 TensorFlow 操作可在 GitHub 上的 Notebook 中查看,网址为bit.ly/2YSYbYu
。
TensorFlow 远不止如此。随着我们在本书中的学习,我们将了解 TensorFlow 的各种重要功能。
TensorFlow 2.0 和 Keras
TensorFlow 2.0 具有一些非常酷的功能。它默认设置为即时执行模式。它提供了简化的工作流程,并使用 Keras 作为构建深度学习模型的主要 API。它还与 TensorFlow 1.x 版本向后兼容。
要安装 TensorFlow 2.0,请打开您的终端并输入以下命令:
pip install tensorflow==2.0.0-alpha0
由于 TensorFlow 2.0 使用 Keras 作为高级 API,我们将在下一节中看看 Keras 的工作原理。
Bonjour Keras
Keras 是另一个广泛使用的深度学习库。它由谷歌的 François Chollet 开发。它以快速原型设计而闻名,使模型构建简单。它是一个高级库,意味着它本身不执行任何低级操作,如卷积。它使用后端引擎来执行这些操作,比如 TensorFlow。Keras API 在tf.keras
中可用,TensorFlow 2.0 将其作为主要 API。
在 Keras 中构建模型涉及四个重要步骤:
-
定义模型
-
编译模型
-
拟合模型
-
评估模型
定义模型
第一步是定义模型。Keras 提供了两种不同的 API 来定义模型:
-
顺序 API
-
函数式 API
定义一个序列模型
在序列模型中,我们将每个层堆叠在一起:
from keras.models import Sequential
from keras.layers import Dense
首先,让我们将我们的模型定义为Sequential()
模型,如下所示:
model = Sequential()
现在,定义第一层,如下所示:
model.add(Dense(13, input_dim=7, activation='relu'))
在上述代码中,Dense
表示全连接层,input_dim
表示输入的维度,activation
指定我们使用的激活函数。我们可以堆叠任意多层,一层叠在另一层之上。
定义带有 relu
激活的下一层,如下所示:
model.add(Dense(7, activation='relu'))
定义带有 sigmoid
激活函数的输出层:
model.add(Dense(1, activation='sigmoid'))
顺序模型的最终代码块如下所示。正如您所见,Keras 代码比 TensorFlow 代码简单得多:
model = Sequential()
model.add(Dense(13, input_dim=7, activation='relu'))
model.add(Dense(7, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
定义函数式模型
函数式模型比顺序模型更灵活。例如,在函数式模型中,我们可以轻松连接任意一层到另一层,而在顺序模型中,每一层都是堆叠在另一层之上的。当创建复杂模型时,如有向无环图、具有多个输入值、多个输出值和共享层的模型时,函数式模型非常实用。现在,我们将看看如何在 Keras 中定义函数式模型。
第一步是定义输入维度:
input = Input(shape=(2,))
现在,我们将在测试集上评估我们的模型,首先定义第一个具有 10
个神经元和 relu
激活函数的全连接层,使用 Dense
类,如下所示:
layer1 = Dense(10, activation='relu')
我们已经定义了 layer1
,但是 layer1
的输入从哪里来?我们需要在末尾的括号符号中指定 layer1
的输入,如下所示:
layer1 = Dense(10, activation='relu')(input)
我们使用 13
个神经元和 relu
激活函数定义下一层 layer2
。layer2
的输入来自 layer1
,因此在代码末尾加上括号,如下所示:
layer2 = Dense(10, activation='relu')(layer1)
现在,我们可以定义具有 sigmoid
激活函数的输出层。输出层的输入来自 layer2
,因此在括号中添加了这一部分:
output = Dense(1, activation='sigmoid')(layer2)
在定义完所有层之后,我们使用 Model
类定义模型,需要指定 inputs
和 outputs
,如下所示:
model = Model(inputs=input, outputs=output)
函数式模型的完整代码如下所示:
input = Input(shape=(2,))
layer1 = Dense(10, activation='relu')(input)
layer2 = Dense(10, activation='relu')(layer1)
output = Dense(1, activation='sigmoid')(layer2)
model = Model(inputs=input, outputs=output)
编译模型
现在我们已经定义了模型,下一步是编译它。在这个阶段,我们设置模型学习的方式。在编译模型时,我们定义了三个参数:
-
optimizer
参数:这定义了我们想要使用的优化算法,例如在这种情况下的梯度下降。 -
loss
参数:这是我们试图最小化的目标函数,例如均方误差或交叉熵损失。 -
metrics
参数:这是我们想通过哪种度量来评估模型性能,例如accuracy
。我们也可以指定多个度量。
运行以下代码来编译模型:
model.compile(loss='binary_crossentropy', optimizer='sgd', metrics=['accuracy'])
训练模型
我们已经定义并编译了模型。现在,我们将训练模型。使用 fit
函数可以完成模型的训练。我们指定特征 x
、标签 y
、训练的 epochs
数量和 batch_size
,如下所示:
model.fit(x=data, y=labels, epochs=100, batch_size=10)
评估模型
训练完模型后,我们将在测试集上评估模型:
model.evaluate(x=data_test,y=labels_test)
我们还可以在相同的训练集上评估模型,这将帮助我们了解训练准确性:
model.evaluate(x=data,y=labels)
使用 TensorFlow 2.0 进行 MNIST 数字分类
现在,我们将看到如何使用 TensorFlow 2.0 执行 MNIST 手写数字分类。与 TensorFlow 1.x 相比,它只需要少量代码。正如我们所学的,TensorFlow 2.0 使用 Keras 作为其高级 API;我们只需在 Keras 代码中添加tf.keras
。
让我们从加载数据集开始:
mnist = tf.keras.datasets.mnist
使用以下代码创建训练集和测试集:
(x_train,y_train), (x_test, y_test) = mnist.load_data()
将训练集和测试集标准化,通过将x
的值除以最大值255.0
来完成:
x_train, x_test = tf.cast(x_train/255.0, tf.float32), tf.cast(x_test/255.0, tf.float32)
y_train, y_test = tf.cast(y_train,tf.int64),tf.cast(y_test,tf.int64)
如下定义序列模型:
model = tf.keras.models.Sequential()
现在,让我们为模型添加层次。我们使用一个三层网络,其中最后一层采用relu
函数和softmax
:
model.add(tf.keras.layers.Flatten())
model.add(tf.keras.layers.Dense(256, activation="relu"))
model.add(tf.keras.layers.Dense(128, activation="relu"))
model.add(tf.keras.layers.Dense(10, activation="softmax"))
通过运行以下代码行来编译模型:
model.compile(optimizer='sgd', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
训练模型:
model.fit(x_train, y_train, batch_size=32, epochs=10)
评估模型:
model.evaluate(x_test, y_test)
就是这样!使用 Keras API 编写代码就是这么简单。
我们应该使用 Keras 还是 TensorFlow?
我们了解到 TensorFlow 2.0 使用 Keras 作为其高级 API。使用高级 API 可以进行快速原型设计。但是当我们想要在低级别上构建模型时,或者当高级 API 无法提供所需功能时,我们就不能使用高级 API。
除此之外,从头开始编写代码可以加深我们对算法的理解,并帮助我们更好地理解和学习概念,远胜于直接使用高级 API。这就是为什么在这本书中,我们将使用 TensorFlow 编写大部分算法,而不使用 Keras 等高级 API。我们将使用 TensorFlow 版本 1.13.1。
总结
我们从本章开始学习 TensorFlow 及其如何使用计算图。我们了解到,在 TensorFlow 中,每个计算都表示为一个计算图,该图由多个节点和边组成,其中节点是数学运算,如加法和乘法,边是张量。
我们学到了变量是用来存储值的容器,并且它们作为计算图中多个其他操作的输入。稍后,我们了解到占位符类似于变量,其中我们只定义类型和维度,但不会分配值,占位符的值将在运行时提供。
未来,我们学习了 TensorBoard,这是 TensorFlow 的可视化工具,可用于可视化计算图。它还可以用来绘制各种定量指标和多个中间计算结果的结果。
我们还学习了急切执行,它更符合 Python 风格,允许快速原型设计。我们了解到,与图模式不同,我们无需每次执行操作时都构建一个图来执行任何操作,急切执行遵循命令式编程范式,可以立即执行任何操作,就像我们在 Python 中所做的那样,而无需创建图形。
在下一章中,我们将学习梯度下降及其变种算法。
问题
通过回答以下问题来评估你对 TensorFlow 的了解:
-
定义一个计算图。
-
会话是什么?
-
我们如何在 TensorFlow 中创建一个会话?
-
变量和占位符之间有什么区别?
-
我们为什么需要 TensorBoard?
-
名称作用域是什么,它是如何创建的?
-
什么是即时执行?
进一步阅读
您可以通过查看官方文档www.tensorflow.org/tutorials
来了解更多关于 TensorFlow 的信息。
第二部分:深度学习基础算法
在这一部分,我们将探索所有基础深度学习算法。我们首先直观地理解每个算法,然后深入研究其数学基础。我们还将学习如何在 TensorFlow 中实现每个算法。
本节包括以下章节:
-
第三章,梯度下降及其变种
-
第四章,使用 RNN 生成歌词
-
第五章,改进 RNN
-
第六章,解密卷积网络
-
第七章,学习文本表示
第三章:梯度下降及其变体
梯度下降是最流行和广泛使用的优化算法之一,是一种一阶优化算法。一阶优化意味着我们只计算一阶导数。正如我们在第一章中看到的,深度学习简介,我们使用梯度下降计算损失函数相对于网络权重的一阶导数以最小化损失。
梯度下降不仅适用于神经网络,它还用于需要找到函数最小值的情况。在本章中,我们将深入探讨梯度下降,从基础知识开始,并学习几种梯度下降算法的变体。有各种各样的梯度下降方法用于训练神经网络。首先,我们将了解随机梯度下降(SGD)和小批量梯度下降。然后,我们将探讨如何通过动量加速梯度下降以达到收敛。本章后期,我们将学习如何使用各种算法(如 Adagrad、Adadelta、RMSProp、Adam、Adamax、AMSGrad 和 Nadam)以自适应方式执行梯度下降。我们将使用简单的线性回归方程,并看看如何使用各种梯度下降算法找到线性回归成本函数的最小值。
在本章中,我们将学习以下主题:
-
解析梯度下降
-
梯度下降与随机梯度下降的区别
-
动量和 Nesterov 加速梯度
-
梯度下降的自适应方法
解析梯度下降
在深入细节之前,让我们先了解基础知识。数学中的函数是什么?函数表示输入和输出之间的关系。我们通常用表示一个函数。例如,
表示一个以
为输入并返回
为输出的函数。它也可以表示为
。
这里,我们有一个函数,,我们可以绘制并查看函数的形状:
函数的最小值称为函数的最小值。正如您在上图中看到的, 函数的最小值位于 0 处。前述函数称为凸函数,在这种函数中只有一个最小值。当存在多个最小值时,函数称为非凸函数。如下图所示,非凸函数可以有许多局部最小值和一个全局最小值,而凸函数只有一个全局最小值:
通过观察 函数的图表,我们可以很容易地说它在
处有其最小值。但是,如何在数学上找到函数的最小值?首先,让我们假设 x = 0.7。因此,我们处于一个 x = 0.7 的位置,如下图所示:
现在,我们需要走向零,这是我们的最小值,但我们如何达到它?我们可以通过计算函数的导数,,来达到它。因此,关于
,函数的导数为:
由于我们在 x = 0.7 处,并将其代入前述方程,我们得到以下方程:
计算导数后,我们根据以下更新规则更新 的位置:
如我们在下图中所见,我们最初位于 x = 0.7 处,但在计算梯度后,我们现在处于更新位置 x = -0.7。然而,这并不是我们想要的,因为我们错过了我们的最小值 x = 0,而是达到了其他位置:
为了避免这种情况,我们引入了一个称为学习率的新参数,,在更新规则中。它帮助我们减慢梯度下降的步伐,以便我们不会错过最小点。我们将梯度乘以学习率,并更新
的值,如下所示:
假设 ;现在,我们可以写出以下内容:
正如我们在下图中看到的那样,在将更新后的 x 值的梯度乘以学习率后,我们从初始位置 x = 0.7 下降到 x = 0.49:
然而,这仍然不是我们的最优最小值。我们需要进一步下降,直到达到最小值,即 x = 0。因此,对于一些 n 次迭代,我们必须重复相同的过程,直到达到最小点。也就是说,对于一些 n 次迭代,我们使用以下更新规则更新 x 的值,直到达到最小点:
好的,为什么在前面的方程中有一个减号?也就是说,为什么我们要从x中减去?为什么不能将它们加起来,将我们的方程变为
?
这是因为我们在寻找函数的最小值,所以我们需要向下。如果我们将x加上,那么我们在每次迭代时都会向上移动,无法找到最小值,如下图所示:
![]() |
![]() |
---|---|
![]() |
![]() |
因此,在每次迭代中,我们计算y相对于x的梯度,即,将梯度乘以学习率,即
,然后从x值中减去它以获得更新后的x值,如下所示:
通过在每次迭代中重复此步骤,我们从成本函数向下移动,并达到最小点。正如我们在下图中所看到的,我们从初始位置 0.7 向下移动到 0.49,然后从那里到达 0.2。
然后,在多次迭代之后,我们达到了最小点,即 0.0:
当我们达到函数的最小值时,我们称之为收敛。但问题是:我们如何知道我们已经达到了收敛?在我们的例子中,,我们知道最小值是 0。因此,当我们达到 0 时,我们可以说我们找到了最小值,即我们已经达到了收敛。但是我们如何在数学上表达 0 是函数
的最小值呢?
让我们仔细观察下面的图表,显示了x在每次迭代中的变化。正如您可能注意到的那样,第五次迭代时x的值为 0.009,第六次迭代时为 0.008,第七次迭代时为 0.007。正如您所见,第五、六和七次迭代之间几乎没有什么差异。当x在迭代中的值变化很小时,我们可以得出我们已经达到了收敛:
好了,但这一切有什么用?我们为什么要找到一个函数的最小值?当我们训练模型时,我们的目标是最小化模型的损失函数。因此,通过梯度下降,我们可以找到成本函数的最小值。找到成本函数的最小值给我们提供了模型的最优参数,从而可以获得最小的损失。一般来说,我们用来表示模型的参数。以下方程称为参数更新规则或权重更新规则:
在这里,我们有以下内容:
-
是模型的参数
-
是学习率
-
是梯度
我们根据参数更新规则多次迭代更新模型的参数,直到达到收敛。
在回归中执行梯度下降
到目前为止,我们已经理解了梯度下降算法如何找到模型的最优参数。在本节中,我们将了解如何在线性回归中使用梯度下降,并找到最优参数。
简单线性回归的方程可以表达如下:
因此,我们有两个参数,和
。现在,我们将看看如何使用梯度下降找到这两个参数的最优值。
导入库
首先,我们需要导入所需的库:
import warnings
warnings.filterwarnings('ignore')
import random
import math
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline
准备数据集
接下来,我们将生成一些随机数据点,有500
行和2
列(x和y),并将它们用于训练:
data = np.random.randn(500, 2)
正如你所看到的,我们的数据有两列:
print data[0]
array([-0.08575873, 0.45157591])
第一列表示值:
print data[0,0]
-0.08575873243708057
第二列表示值:
print data[0,1]
0.4515759149158441
我们知道简单线性回归方程的表达如下:
因此,我们有两个参数,和
。我们将这两个参数都存储在名为
theta
的数组中。首先,我们将theta
初始化为零,如下所示:
theta = np.zeros(2)
函数theta[0]
表示的值,而函数
theta[1]
表示的值:
print theta
array([0., 0.])
定义损失函数
线性回归的均方误差(MSE)如下所示:
这里, 是训练样本的数量,
是实际值,
是预测值。
上述损失函数的实现如下所示。我们将data
和模型参数theta
输入损失函数,返回均方误差(MSE)。请记住,data[,0]
具有一个 值,而
data[,1]
具有一个 值。类似地,
theta [0]
的值为m
,而theta[1]
的值为 。
让我们定义损失函数:
def loss_function(data,theta):
现在,我们需要获取 和
的值:
m = theta[0]
b = theta[1]
loss = 0
我们对每个迭代执行此操作:
for i in range(0, len(data)):
现在,我们得到 和
的值:
x = data[i, 0]
y = data[i, 1]
然后,我们预测 的值:
y_hat = (m*x + b)
这里,我们按照方程 (3) 计算损失:
loss = loss + ((y - (y_hat)) ** 2)
然后,我们计算均方误差:
mse = loss / float(len(data))
return mse
当我们将随机初始化的data
和模型参数theta
输入loss_function
时,会返回均方损失,如下所示:
loss_function(data, theta)
1.0253548008165727
现在,我们需要最小化这个损失。为了最小化损失,我们需要计算损失函数 关于模型参数
和
的梯度,并根据参数更新规则更新参数。首先,我们将计算损失函数的梯度。
计算损失函数的梯度
损失函数关于参数 的梯度如下:
损失函数关于参数 的梯度如下:
我们定义了一个名为compute_gradients
的函数,该函数接受输入参数data
和theta
,并返回计算得到的梯度:
def compute_gradients(data, theta):
现在,我们需要初始化梯度:
gradients = np.zeros(2)
然后,我们需要保存数据点的总数N
:
N = float(len(data))
现在,我们可以得到 和
的值:
m = theta[0]
b = theta[1]
我们对每个迭代执行相同操作:
for i in range(0, len(data)):
然后,我们得到 和
的值:
x = data[i, 0]
y = data[i, 1]
现在,我们按照方程 (4) 计算损失关于 的梯度:
gradients[0] += - (2 / N) * x * (y - (( m* x) + b))
然后,我们根据方程 (5) 计算损失相对于 的梯度:
gradients[1] += - (2 / N) * (y - ((theta[0] * x) + b))
我们需要添加epsilon
以避免零除错误:
epsilon = 1e-6
gradients = np.divide(gradients, N + epsilon)
return gradients
当我们输入我们随机初始化的data
和theta
模型参数时,compute_gradients
函数返回相对于 即
,以及相对于
即
的梯度,如下所示:
compute_gradients(data,theta)
array([-9.08423989e-05, 1.05174511e-04])
更新模型参数
现在我们已经计算出梯度,我们需要根据我们的更新规则更新我们的模型参数,如下所示:
因为我们在theta[0]
中存储了 ,在
theta[1]
中存储了 ,所以我们可以写出我们的更新方程如下:
正如我们在前一节中学到的,仅在一个迭代中更新梯度不会导致我们收敛到成本函数的最小值,因此我们需要计算梯度并对模型参数进行多次迭代更新。
首先,我们需要设置迭代次数:
num_iterations = 50000
现在,我们需要定义学习率:
lr = 1e-2
接下来,我们将定义一个名为loss
的列表来存储每次迭代的损失:
loss = []
在每次迭代中,我们将根据我们的参数更新规则从方程 (8) 计算并更新梯度:
theta = np.zeros(2)
for t in range(num_iterations):
#compute gradients
gradients = compute_gradients(data, theta)
#update parameter
theta = theta - (lr*gradients)
#store the loss
loss.append(loss_function(data,theta))
现在,我们需要绘制loss
(Cost
)函数:
plt.plot(loss)
plt.grid()
plt.xlabel('Training Iterations')
plt.ylabel('Cost')
plt.title('Gradient Descent')
下图显示了损失(Cost)随训练迭代次数减少的情况:
因此,我们学会了梯度下降可以用来找到模型的最优参数,然后我们可以用它来最小化损失。在下一节中,我们将学习梯度下降算法的几种变体。
梯度下降与随机梯度下降的对比
我们使用我们的参数更新方程 (1) 多次更新模型参数,直到找到最优参数值。在梯度下降中,为了执行单个参数更新,我们遍历训练集中的所有数据点。因此,每次更新模型参数时,我们都要遍历训练集中的所有数据点。仅在遍历训练集中的所有数据点后更新模型参数使得梯度下降非常缓慢,并且会增加训练时间,特别是当数据集很大时。
假设我们有一个包含 100 万数据点的训练集。我们知道,我们需要多次更新模型的参数才能找到最优参数值。因此,即使是执行单次参数更新,我们也需要遍历训练集中的所有 100 万数据点,然后更新模型参数。这无疑会使训练变慢。这是因为我们不能仅通过单次更新找到最优参数;我们需要多次更新模型参数才能找到最优值。因此,如果我们对训练集中的每个参数更新都进行 100 万次数据点的迭代,这肯定会减慢我们的训练速度。
因此,为了应对这一情况,我们引入了随机梯度下降(SGD)。与梯度下降不同,我们无需等待在训练集中迭代所有数据点后更新模型参数;我们只需在遍历训练集中的每一个数据点后更新模型参数。
由于我们在随机梯度下降中在遍历每个单独数据点后更新模型参数,相比梯度下降,它将更快地学习到模型的最优参数,从而缩短训练时间。
SGD 的作用是什么?当我们有一个巨大的数据集时,使用传统的梯度下降方法,我们只在遍历完整个数据集中的所有数据点后更新参数。因此,在整个数据集上进行多次迭代后,我们才能达到收敛,并且显然这需要很长时间。但是,在随机梯度下降中,我们在遍历每个单独的训练样本后更新参数。也就是说,我们从第一个训练样本开始就学习到寻找最优参数,这有助于相比传统的梯度下降方法更快地达到收敛。
我们知道,周期指的是神经网络查看整个训练数据的次数。因此,在梯度下降中,每个周期我们执行参数更新。这意味着,在每个周期结束后,神经网络看到整个训练数据。我们按以下方式每个周期执行参数更新:
然而,在随机梯度下降中,我们无需等到每个周期完成后才更新参数。也就是说,我们不需要等到神经网络看到整个训练数据后才更新参数。相反,我们在每个周期中从看到单个训练样本开始就更新网络的参数:
下图显示了梯度下降和随机梯度下降如何执行参数更新并找到最小成本。图中心的星号符号表示我们最小成本的位置。正如您所见,随机梯度下降比普通梯度下降更快地达到收敛。您还可以观察到随机梯度下降中梯度步骤的振荡;这是因为我们在每个训练样本上更新参数,因此与普通梯度下降相比,SGD 中的梯度步骤变化频繁:
还有另一种称为小批量梯度下降的梯度下降变体。它吸取了普通梯度下降和随机梯度下降的优点。在 SGD 中,我们看到我们为每个训练样本更新模型的参数。然而,在小批量梯度下降中,我们不是在每个训练样本迭代后更新参数,而是在迭代一些数据点的批次后更新参数。假设批量大小为 50,这意味着我们在迭代 50 个数据点后更新模型的参数,而不是在迭代每个单独数据点后更新模型的参数。
下图显示了 SGD 和小批量梯度下降的轮廓图:
这些梯度下降类型之间的差异如下:
-
梯度下降:在训练集中迭代所有数据点后更新模型参数
-
随机梯度下降:在训练集中迭代每个单独数据点后更新模型的参数
-
小批量梯度下降:在训练集中迭代n个数据点后更新模型参数
对于大型数据集,小批量梯度下降优于普通梯度下降和 SGD,因为小批量梯度下降的表现优于其他两种方法。
小批量梯度下降的代码如下所示。
首先,我们需要定义minibatch
函数:
def minibatch(data, theta, lr = 1e-2, minibatch_ratio = 0.01, num_iterations = 1000):
接下来,我们将通过将数据长度乘以minibatch_ratio
来定义minibatch_size
:
minibatch_size = int(math.ceil(len(data) * minibatch_ratio))
现在,在每次迭代中,我们执行以下操作:
for t in range(num_iterations):
接下来,选择sample_size
:
sample_size = random.sample(range(len(data)), minibatch_size)
np.random.shuffle(data)
现在,基于sample_size
对数据进行抽样:
sample_data = data[0:sample_size[0], :]
计算sample_data
相对于theta
的梯度:
grad = compute_gradients(sample_data, theta)
在计算了给定小批量大小的抽样数据的梯度后,我们按以下方式更新模型参数theta
:
theta = theta - (lr * grad)
return theta
基于动量的梯度下降
在本节中,我们将学习两种新的梯度下降变体,称为动量和 Nesterov 加速梯度。
带有动量的梯度下降
我们在 SGD 和小批量梯度下降中遇到了问题,因为参数更新中存在振荡。看看下面的图表,展示了小批量梯度下降是如何达到收敛的。正如您所见,梯度步骤中存在振荡。振荡由虚线表示。您可能注意到,它朝一个方向迈出梯度步骤,然后朝另一个方向,依此类推,直到达到收敛:
这种振荡是由于我们在迭代每个n个数据点后更新参数,更新方向会有一定的变化,从而导致每个梯度步骤中的振荡。由于这种振荡,很难达到收敛,并且减慢了达到收敛的过程。
为了缓解这个问题,我们将介绍一种称为动量的新技术。如果我们能够理解梯度步骤达到更快收敛的正确方向,那么我们可以使我们的梯度步骤在那个方向导航,并减少不相关方向上的振荡;也就是说,我们可以减少采取不导致收敛的方向。
那么,我们该如何做呢?基本上,我们从前一梯度步骤的参数更新中获取一部分,并添加到当前梯度步骤中。在物理学中,动量在施加力后使物体保持运动。在这里,动量使我们的梯度保持朝向导致收敛的方向运动。
如果您看一下以下方程,您会看到我们基本上是从上一步的参数更新中取参数更新,,并将其添加到当前梯度步骤,
。我们希望从前一个梯度步骤中获取多少信息取决于因子,即,
,以及学习率,用
表示:
在前述方程中, 被称为速度,它加速梯度朝向收敛方向的更新。它还通过在当前步骤中添加来自上一步参数更新的一部分,来减少不相关方向上的振荡。
因此,具有动量的参数更新方程如下表达:
通过这样做,使用动量进行小批量梯度下降有助于减少梯度步骤中的振荡,并更快地达到收敛。
现在,让我们来看看动量的实现。
首先,我们定义momentum
函数,如下所示:
def momentum(data, theta, lr = 1e-2, gamma = 0.9, num_iterations = 1000):
然后,我们用零初始化vt
:
vt = np.zeros(theta.shape[0])
下面的代码用于每次迭代覆盖范围:
for t in range(num_iterations):
现在,我们计算相对于theta
的gradients
:
gradients = compute_gradients(data, theta)
接下来,我们更新vt
为:
vt = gamma * vt + lr * gradients
现在,我们更新模型参数theta
,如下所示:
theta = theta - vt
return theta
Nesterov 加速梯度
动量法的一个问题是可能会错过最小值。也就是说,当我们接近收敛(最小点)时,动量的值会很高。当动量值在接近收敛时很高时,实际上动量会推动梯度步长变高,并且可能错过实际的最小值;也就是说,当动量在接近收敛时很高时,它可能会超出最小值,如下图所示:
为了克服这一问题,Nesterov 引入了一种新的方法,称为Nesterov 加速梯度(NAG)。
Nesterov 动量背后的基本动机是,我们不是在当前位置计算梯度,而是在动量将带我们到的位置计算梯度,我们称之为前瞻位置。
那么,这到底意味着什么呢?在带动量的梯度下降部分,我们了解到以下方程:
上述方程告诉我们,我们基本上是用前一步参数更新的一部分来推动当前的梯度步长vt
到一个新位置,这将帮助我们达到收敛。然而,当动量很高时,这个新位置实际上会超过最小值。
因此,在使用动量进行梯度步进并到达新位置之前,如果我们理解动量将带我们到哪个位置,我们就可以避免超出最小值。如果我们发现动量会带我们到实际错过最小值的位置,那么我们可以减缓动量并尝试达到最小值。
但是我们如何找到动量将带我们到的位置呢?在方程(2)中,我们不是根据当前梯度步长vt
计算梯度,而是根据vt
的前瞻位置计算梯度。术语1
基本上告诉我们我们下一个梯度步长的大致位置,我们称之为前瞻位置。这给了我们一个关于下一个梯度步长将在哪里的想法。
因此,我们可以根据 NAG 重新编写我们的方程vt
如下:
我们更新我们的参数如下:
使用上述方程更新参数可以通过在梯度步骤接近收敛时减慢动量来防止错过最小值。Nesterov 加速方法的实现如下。
首先,我们定义NAG
函数:
def NAG(data, theta, lr = 1e-2, gamma = 0.9, num_iterations = 1000):
然后,我们用零初始化vt
的值:
vt = np.zeros(theta.shape[0])
对于每次迭代,我们执行以下步骤:
for t in range(num_iterations):
现在,我们需要计算相对于的梯度:
gradients = compute_gradients(data, theta - gamma * vt)
然后,我们将vt
更新为:
vt = gamma * vt + lr * gradients
现在,我们将模型参数theta
更新为:
theta = theta - vt
return theta
梯度下降的自适应方法
在本节中,我们将学习几种梯度下降的自适应版本。
使用 Adagrad 自适应设置学习率
当我们构建深度神经网络时,会有很多参数。参数基本上是网络的权重,所以当我们构建具有多层的网络时,会有很多权重,比如。我们的目标是找到所有这些权重的最优值。在我们之前学到的所有方法中,网络参数的学习率是一个常见值。然而,Adagrad(自适应梯度的简称)会根据参数自适应地设置学习率。
经常更新或梯度较大的参数将具有较慢的学习率,而更新不频繁或梯度较小的参数也将具有较慢的学习率。但是我们为什么要这样做?这是因为更新不频繁的参数意味着它们没有被充分训练,所以我们为它们设置较高的学习率;而频繁更新的参数意味着它们已经足够训练,所以我们为它们设置较低的学习率,以防止超出最小值。
现在,让我们看看 Adagrad 如何自适应地改变学习率。之前,我们用表示梯度。为简单起见,在本章中,我们将用
代表梯度。因此,参数
在迭代
时的梯度可以表示为:
因此,我们可以用来重新编写我们的更新方程,作为梯度符号表示如下:
现在,对于每次迭代,要更新参数
,我们将学习率除以参数
的所有先前梯度的平方和,如下所示:
在这里, 表示参数
所有先前梯度的平方和。我们添加
只是为了避免除以零的错误。通常将
的值设置为一个小数。这里出现的问题是,为什么我们要把学习率除以所有先前梯度的平方和?
我们了解到,具有频繁更新或高梯度的参数将具有较慢的学习率,而具有不频繁更新或小梯度的参数也将具有较高的学习率。
总和,,实际上缩放了我们的学习率。也就是说,当过去梯度的平方和值较高时,我们基本上将学习率除以一个高值,因此我们的学习率将变得较低。类似地,如果过去梯度的平方和值较低,则我们将学习率除以一个较低的值,因此我们的学习率值将变高。这意味着学习率与参数的所有先前梯度的平方和成反比。
在这里,我们的更新方程表示如下:
简而言之,在 Adagrad 中,当先前梯度值高时,我们将学习率设置为较低的值,当过去梯度值较低时,我们将学习率设置为较高的值。这意味着我们的学习率值根据参数的过去梯度更新而变化。
现在我们已经学习了 Adagrad 算法的工作原理,让我们通过实现它来加深我们的知识。Adagrad 算法的代码如下所示。
首先,定义AdaGrad
函数:
def AdaGrad(data, theta, lr = 1e-2, epsilon = 1e-8, num_iterations = 10000):
定义名为gradients_sum
的变量来保存梯度和,并将它们初始化为零:
gradients_sum = np.zeros(theta.shape[0])
对于每次迭代,我们执行以下步骤:
for t in range(num_iterations):
然后,我们计算损失对theta
的梯度:
gradients = compute_gradients(data, theta)
现在,我们计算梯度平方和,即:
gradients_sum += gradients ** 2
之后,我们计算梯度更新,即:
gradient_update = gradients / (np.sqrt(gradients_sum + epsilon))
现在,更新theta
模型参数,使其为 :
theta = theta - (lr * gradient_update)
return theta
然而,Adagrad 方法存在一个缺点。在每次迭代中,我们都会累积和求和所有过去的平方梯度。因此,在每次迭代中,过去平方梯度值的总和将会增加。当过去平方梯度值的总和较高时,分母中会有一个较大的数。当我们将学习率除以一个非常大的数时,学习率将变得非常小。因此,经过几次迭代后,学习率开始衰减并变成一个无限小的数值——也就是说,我们的学习率将单调递减。当学习率降至一个非常低的值时,收敛需要很长时间。
在下一节中,我们将看到 Adadelta 如何解决这个缺点。
采用 Adadelta 方法摒弃学习率
Adadelta 是 Adagrad 算法的增强版。在 Adagrad 中,我们注意到学习率减小到一个非常低的数字的问题。虽然 Adagrad 能够自适应地学习学习率,但我们仍然需要手动设置初始学习率。然而,在 Adadelta 中,我们根本不需要学习率。那么,Adadelta 算法是如何学习的呢?
在 Adadelta 中,我们不是取所有过去的平方梯度的总和,而是设定一个大小为 的窗口,并仅从该窗口中取过去的平方梯度的总和。在 Adagrad 中,我们取所有过去的平方梯度的总和,并导致学习率减小到一个低数字。为了避免这种情况,我们只从一个窗口内取过去的平方梯度的总和。
如果 是窗口大小,则我们的参数更新方程如下所示:
然而,问题在于,虽然我们仅从一个窗口内取梯度,,但在每次迭代中将窗口内所有梯度进行平方并存储是低效的。因此,我们可以采取梯度的运行平均值,而不是这样做。
我们通过将先前的梯度的运行平均值 和当前梯度
相加来计算迭代 t 的梯度的运行平均值
:
不仅仅是取运行平均值,我们还采取梯度的指数衰减运行平均值,如下所示:
在这里, 被称为指数衰减率,类似于我们在动量中看到的那个——用于决定从前一次梯度的运行平均值中添加多少信息。
现在,我们的更新方程如下所示:
为了简化符号,让我们将记作
,这样我们可以将前一个更新方程重写为如下形式:
根据前述方程,我们可以推断如下:
如果你观察前一个方程中的分母,我们基本上在计算迭代过程中梯度的均方根,,因此我们可以简单地用以下方式写出:
通过将方程 (13) 代入方程 (12),我们可以写出如下:
然而,在我们的方程中仍然有学习率,,术语。我们如何摆脱它?我们可以通过使参数更新的单位与参数相符来实现。正如你可能注意到的那样,
和
的单位并不完全匹配。为了解决这个问题,我们计算参数更新的指数衰减平均值,
,正如我们在方程 (10) 中计算了梯度的指数衰减平均值,
。因此,我们可以写出如下:
它类似于梯度的 RMS,,类似于方程 (13)。我们可以将参数更新的 RMS 写成如下形式:
然而,参数更新的 RMS 值,,是未知的,即
是未知的,因此我们可以通过考虑直到上一个更新,
,来近似计算它。
现在,我们只需用参数更新的 RMS 值取代学习率。也就是说,我们在方程 (14) 中用取代
,并写出如下:
将方程 (15) 代入方程 (11),我们的最终更新方程如下:
现在,让我们通过实现了解 Adadelta 算法。
首先,我们定义AdaDelta
函数:
def AdaDelta(data, theta, gamma = 0.9, epsilon = 1e-5, num_iterations = 1000):
然后,我们将E_grad2
变量初始化为零,用于存储梯度的运行平均值,并将E_delta_theta2
初始化为零,用于存储参数更新的运行平均值,如下所示:
# running average of gradients
E_grad2 = np.zeros(theta.shape[0])
#running average of parameter update
E_delta_theta2 = np.zeros(theta.shape[0])
每次迭代,我们执行以下步骤:
for t in range(num_iterations):
现在,我们需要计算相对于theta
的gradients
:
gradients = compute_gradients(data, theta)
然后,我们可以计算梯度的运行平均值:
E_grad2 = (gamma * E_grad2) + ((1\. - gamma) * (gradients ** 2))
在这里,我们将计算delta_theta
,即,:
delta_theta = - (np.sqrt(E_delta_theta2 + epsilon)) / (np.sqrt(E_grad2 + epsilon)) * gradients
现在,我们可以计算参数更新的运行平均值,:
E_delta_theta2 = (gamma * E_delta_theta2) + ((1\. - gamma) * (delta_theta ** 2))
接下来,我们将更新模型参数theta
,使其变为:
theta = theta + delta_theta
return theta
通过 RMSProp 克服 Adagrad 的限制
与 Adadelta 类似,RMSProp 被引入来解决 Adagrad 的学习率衰减问题。因此,在 RMSProp 中,我们如下计算梯度的指数衰减运行平均值:
而不是取所有过去梯度的平方和,我们使用这些梯度的运行平均值。这意味着我们的更新方程如下:
建议将学习率设置为
0.9
。现在,我们将学习如何在 Python 中实现 RMSProp。
首先,我们需要定义RMSProp
函数:
def RMSProp(data, theta, lr = 1e-2, gamma = 0.9, epsilon = 1e-6, num_iterations = 1000):
现在,我们需要用零来初始化E_grad2
变量,以存储梯度的运行平均值:
E_grad2 = np.zeros(theta.shape[0])
每次迭代时,我们执行以下步骤:
for t in range(num_iterations):
然后,我们计算相对于theta
的gradients
:
gradients = compute_gradients(data, theta)
接下来,我们计算梯度的运行平均值,即,:
E_grad2 = (gamma * E_grad2) + ((1\. - gamma) * (gradients ** 2))
现在,我们更新模型参数theta
,使其变为:
theta = theta - (lr / (np.sqrt(E_grad2 + epsilon)) * gradients)
return theta
自适应矩估计
自适应矩估计,简称Adam,是优化神经网络中最常用的算法之一。在阅读有关 RMSProp 的内容时,我们了解到,为了避免学习率衰减问题,我们计算了平方梯度的运行平均值:
RMSprop 的最终更新方程如下所示:
类似于此,在 Adam 中,我们还计算平方梯度的运行平均值。然而,除了计算平方梯度的运行平均值外,我们还计算梯度的运行平均值。
梯度的运行平均值如下所示:
平方梯度的运行平均值如下所示:
由于许多文献和库将 Adam 中的衰减率表示为 ,而不是
,我们也将使用
来表示 Adam 中的衰减率。因此,在方程式 (16) 和 (17) 中,
和
分别表示梯度运行平均值和平方梯度的指数衰减率。
因此,我们的更新方程式变为以下形式:
梯度运行平均值和平方梯度的运行平均值基本上是这些梯度的第一和第二时刻。也就是说,它们分别是我们梯度的均值和未中心化方差。为了符号简洁起见,让我们将 表示为
,将
表示为
。
因此,我们可以将方程 (16) 和 (17) 重写如下:
我们首先将初始时刻估计设置为零。也就是说,我们用零初始化 和
。当初始估计设置为 0 时,即使经过多次迭代后它们仍然非常小。这意味着它们会偏向 0,特别是当
和
接近 1 时。因此,为了抵消这种影响,我们通过仅将它们除以
来计算
和
的偏差校正估计,如下所示:
在这里, 和
分别是
和
的偏差校正估计。
因此,我们的最终更新方程式如下:
现在,让我们了解如何在 Python 中实现 Adam。
首先,让我们定义 Adam
函数如下:
def Adam(data, theta, lr = 1e-2, beta1 = 0.9, beta2 = 0.9, epsilon = 1e-6, num_iterations = 1000):
然后,我们用 zeros
初始化第一时刻 mt
和第二时刻 vt
:
mt = np.zeros(theta.shape[0])
vt = np.zeros(theta.shape[0])
每次迭代,我们执行以下步骤:
for t in range(num_iterations):
接下来,我们计算相对于 theta
的 gradients
:
gradients = compute_gradients(data, theta)
然后,我们更新第一时刻 mt
,使其为 :
mt = beta1 * mt + (1\. - beta1) * gradients
接下来,我们更新第二矩vt
,使其为:
vt = beta2 * vt + (1\. - beta2) * gradients ** 2
现在,我们计算偏差校正估计的mt
,即:
mt_hat = mt / (1\. - beta1 ** (t+1))
接下来,我们计算偏差校正估计的vt
,即:
vt_hat = vt / (1\. - beta2 ** (t+1))
最后,我们更新模型参数theta
,使其为:
theta = theta - (lr / (np.sqrt(vt_hat) + epsilon)) * mt_hat
return theta
Adamax – 基于无穷范数的 Adam
现在,我们将看一个叫Adamax的 Adam 算法的一个小变体。让我们回忆一下 Adam 中的二阶矩方程:
正如您从上述方程看到的那样,我们将梯度按当前和过去梯度的范数的倒数进行缩放(
范数基本上是值的平方):
而不仅仅是有,我们能将它推广到
范数吗?一般情况下,当我们有大
范数时,我们的更新会变得不稳定。然而,当我们设置
值为
,即
时,
方程变得简单且稳定。我们不仅仅是对梯度参数化,
,我们还对衰减率,
,进行参数化。因此,我们可以写出以下内容:
当我们设置限制时,趋向于无穷大,然后我们得到以下最终方程:
您可以查阅本章末尾列出的进一步阅读部分的论文,了解这是如何推导出来的。
我们可以将上述方程重写为一个简单的递归方程,如下所示:
计算类似于我们在自适应动量估计部分看到的,因此我们可以直接写出以下内容:
通过这样做,我们可以计算偏差校正估计的:
因此,最终的更新方程变为以下形式:
为了更好地理解 Adamax 算法,让我们逐步编写代码。
首先,我们定义Adamax
函数,如下所示:
def Adamax(data, theta, lr = 1e-2, beta1 = 0.9, beta2 = 0.999, epsilon = 1e-6, num_iterations = 1000):
然后,我们用零来初始化第一时刻mt
和第二时刻vt
:
mt = np.zeros(theta.shape[0])
vt = np.zeros(theta.shape[0])
对于每次迭代,我们执行以下步骤:
for t in range(num_iterations):
现在,我们可以计算关于theta
的梯度,如下所示:
gradients = compute_gradients(data, theta)
然后,我们计算第一时刻mt
,如下所示:
mt = beta1 * mt + (1\. - beta1) * gradients
接下来,我们计算第二时刻vt
,如下所示:
vt = np.maximum(beta2 * vt, np.abs(gradients))
现在,我们可以计算mt
的偏差校正估计,即:
mt_hat = mt / (1\. - beta1 ** (t+1))
更新模型参数theta
,使其为:
theta = theta - ((lr / (vt + epsilon)) * mt_hat)
return theta
使用 AMSGrad 的自适应矩估计
Adam 算法的一个问题是有时无法达到最优收敛,或者它达到次优解。已经注意到,在某些情况下,Adam 无法实现收敛,或者达到次优解,而不是全局最优解。这是由于指数移动平均梯度。记得我们在 Adam 中使用梯度的指数移动平均来避免学习率衰减的问题吗?
然而,问题在于由于我们采用梯度的指数移动平均,我们错过了不经常出现的梯度信息。
为了解决这个问题,AMSGrad 的作者对 Adam 算法进行了微小的修改。回想一下我们在 Adam 中看到的二阶矩估计,如下所示:
在 AMSGrad 中,我们使用稍微修改过的的版本。我们不直接使用
,而是取直到前一步的
的最大值,如下所示:
保留了由于指数移动平均而保留信息梯度,而不是逐步淘汰。
因此,我们的最终更新方程式如下所示:
现在,让我们了解如何在 Python 中编写 AMSGrad。
首先,我们定义AMSGrad
函数,如下所示:
def AMSGrad(data, theta, lr = 1e-2, beta1 = 0.9, beta2 = 0.9, epsilon = 1e-6, num_iterations = 1000):
然后,我们初始化第一时刻mt
、第二时刻vt
以及修改后的vt
,即vt_hat
,均为zeros
,如下所示:
mt = np.zeros(theta.shape[0])
vt = np.zeros(theta.shape[0])
vt_hat = np.zeros(theta.shape[0])
对于每次迭代,我们执行以下步骤:
for t in range(num_iterations):
现在,我们可以计算关于theta
的梯度:
gradients = compute_gradients(data, theta)
然后,我们计算第一时刻mt
,如下所示:
mt = beta1 * mt + (1\. - beta1) * gradients
接下来,我们更新第二时刻vt
,如下所示:
vt = beta2 * vt + (1\. - beta2) * gradients ** 2
在 AMSGrad 中,我们使用稍微修改过的的版本。我们不直接使用
,而是取直到前一步的
的最大值。因此,
的实现如下:
vt_hat = np.maximum(vt_hat,vt)
在这里,我们将计算mt
的偏差校正估计,即:
mt_hat = mt / (1\. - beta1 ** (t+1))
现在,我们可以更新模型参数theta
,使其为:
theta = theta - (lr / (np.sqrt(vt_hat) + epsilon)) * mt_hat
return theta
Nadam – 将 NAG 添加到 ADAM 中
Nadam 是 Adam 方法的另一个小扩展。正如其名称所示,在这里,我们将 NAG 合并到 Adam 中。首先,让我们回顾一下我们在 Adam 中学到的内容。
我们按以下方式计算第一和第二时刻:
然后,我们计算第一和第二时刻的偏差校正估计,如下:
我们 Adam 的最终更新方程式表达如下:
现在,我们将看看 Nadam 如何修改 Adam 以使用 Nesterov 动量。在 Adam 中,我们计算第一时刻如下:
我们将这个第一时刻更改为 Nesterov 加速动量。也就是说,我们不再使用先前的动量,而是使用当前的动量,并将其用作前瞻:
我们无法像在 Adam 中计算偏差校正估计那样在这里计算,因为这里的来自当前步骤,而
来自后续步骤。因此,我们改变偏差校正估计步骤如下:
因此,我们可以将我们的第一时刻方程重写为以下形式:
因此,我们的最终更新方程变为以下形式:
现在让我们看看如何在 Python 中实现 Nadam 算法。
首先,我们定义nadam
函数:
def nadam(data, theta, lr = 1e-2, beta1 = 0.9, beta2 = 0.999, epsilon = 1e-6, num_iterations = 500):
然后,我们用零初始化第一时刻mt
和第二时刻vt
:
mt = np.zeros(theta.shape[0])
vt = np.zeros(theta.shape[0])
接下来,我们将beta_prod
设置为1
:
beta_prod = 1
对于每次迭代,我们执行以下步骤:
for t in range(num_iterations):
然后,我们计算相对于theta
的梯度:
gradients = compute_gradients(data, theta)
然后,我们计算第一时刻mt
,使其为:
mt = beta1 * mt + (1\. - beta1) * gradients
现在,我们可以更新第二时刻vt
,使其为:
vt = beta2 * vt + (1\. - beta2) * gradients ** 2
现在,我们计算beta_prod
,即:
beta_prod = beta_prod * (beta1)
接下来,我们计算mt
的偏差校正估计,使其为:
mt_hat = mt / (1\. - beta_prod)
然后,我们计算gt
的偏差校正估计,使其为:
g_hat = grad / (1\. - beta_prod)
从这里开始,我们计算vt
的偏差校正估计,使其为:
vt_hat = vt / (1\. - beta2 ** (t))
现在,我们计算mt_tilde
,使其为:
mt_tilde = (1-beta1**t+1) * mt_hat + ((beta1**t)* g_hat)
最后,我们通过使用来更新模型参数
theta
:
theta = theta - (lr / (np.sqrt(vt_hat) + epsilon)) * mt_hat
return theta
通过这样做,我们学习了用于训练神经网络的各种流行的梯度下降算法变体。执行包含所有回归变体的完整代码的 Jupyter Notebook 可以在bit.ly/2XoW0vH
找到。
摘要
我们从学习什么是凸函数和非凸函数开始本章。然后,我们探讨了如何使用梯度下降找到函数的最小值。我们学习了梯度下降通过计算最优参数来最小化损失函数。后来,我们看了 SGD,其中我们在迭代每个数据点之后更新模型的参数,然后我们学习了小批量 SGD,其中我们在迭代一批数据点之后更新参数。
继续前进,我们学习了如何使用动量来减少梯度步骤中的振荡并更快地达到收敛。在此之后,我们了解了 Nesterov 动量,其中我们不是在当前位置计算梯度,而是在动量将我们带到的位置计算梯度。
我们还学习了 Adagrad 方法,其中我们为频繁更新的参数设置了低学习率,对不经常更新的参数设置了高学习率。接下来,我们了解了 Adadelta 方法,其中我们完全放弃了学习率,而是使用梯度的指数衰减平均值。然后,我们学习了 Adam 方法,其中我们使用第一和第二动量估计来更新梯度。
在此之后,我们探讨了 Adam 的变体,如 Adamax,我们将 Adam 的范数泛化为
,以及 AMSGrad,我们解决了 Adam 达到次优解的问题。在本章的最后,我们学习了 Nadam,其中我们将 Nesterov 动量整合到 Adam 算法中。
在下一章中,我们将学习一种最广泛使用的深度学习算法之一,称为循环神经网络(RNNs),以及如何使用它们来生成歌词。
问题
通过回答以下问题来回顾梯度下降:
-
SGD 与普通梯度下降有什么区别?
-
解释小批量梯度下降。
-
我们为什么需要动量?
-
NAG 背后的动机是什么?
-
Adagrad 如何自适应地设置学习率?
-
Adadelta 的更新规则是什么?
-
RMSProp 如何克服 Adagrad 的局限性?
-
定义 Adam 的更新方程。
进一步阅读
更多信息,请参考以下链接:
-
用于在线学习和随机优化的自适应次梯度方法,作者:John Duchi 等,
www.jmlr.org/papers/volume12/duchi11a/duchi11a.pdf
-
Adadelta:一种自适应学习率方法,作者:Matthew D. Zeiler,
arxiv.org/pdf/1212.5701.pdf
-
Adam:一种用于随机优化的方法,作者:Diederik P. Kingma 和 Jimmy Lei Ba,
arxiv.org/pdf/1412.6980.pdf
-
Adam 及其扩展算法的收敛性研究,作者:Sashank J. Reddi, Satyen Kale, 和 Sanjiv Kumar,
openreview.net/pdf?id=ryQu7f-RZ
-
将 Nesterov 动量整合到 Adam 中,作者:Timothy Dozat,
cs229.stanford.edu/proj2015/054_report.pdf
第四章:使用 RNN 生成歌词
在普通前馈神经网络中,每个输入都是独立的。但是对于序列数据集,我们需要知道过去的输入以进行预测。序列是一组有序的项目。例如,一个句子是一个单词序列。假设我们想要预测句子中的下一个单词;为此,我们需要记住之前的单词。普通的前馈神经网络无法预测出正确的下一个单词,因为它不会记住句子的前面单词。在这种需要记住先前输入的情况下(在这种情况下,我们需要记住先前输入以进行预测),为了进行预测,我们使用递归神经网络(RNNs)。
在本章中,我们将描述如何使用 RNN 对序列数据集进行建模以及如何记住先前的输入。我们将首先研究 RNN 与前馈神经网络的区别。然后,我们将检查 RNN 中的前向传播是如何工作的。
继续,我们将研究时域反向传播(BPTT)算法,该算法用于训练 RNN。随后,我们将讨论梯度消失和爆炸问题,在训练递归网络时会出现这些问题。您还将学习如何使用 TensorFlow 中的 RNN 生成歌词。
在本章末尾,我们将研究不同类型的 RNN 架构,以及它们在各种应用中的使用。
在本章中,我们将学习以下主题:
-
递归神经网络
-
RNN 中的前向传播
-
时域反向传播
-
梯度消失和爆炸问题
-
使用 RNN 生成歌词
-
不同类型的 RNN 架构
引入 RNNs
太阳在 ____ 中升起。
如果我们被要求预测前面句子中的空白处,我们可能会说东方。为什么我们会预测东方是正确的词汇在这里?因为我们读了整个句子,理解了上下文,并预测东方是一个适合完成句子的合适词汇。
如果我们使用前馈神经网络来预测空白,它将不能预测出正确的单词。这是因为在前馈网络中,每个输入都是独立的,并且它们仅基于当前输入进行预测,它们不记住前面的输入。
因此,网络的输入将仅仅是前一个空白处的单词,这个单词是 the。仅凭这个单词作为输入,我们的网络无法预测出正确的单词,因为它不知道句子的上下文,也就是说它不知道前面一组单词来理解句子的语境并预测合适的下一个单词。
这就是我们使用 RNN 的地方。它们不仅基于当前输入预测输出,还基于先前的隐藏状态。为什么它们必须基于当前输入和先前的隐藏状态来预测输出呢?为什么不能只使用当前输入和先前的输入呢?
这是因为前一个输入仅存储有关前一个单词的信息,而前一个隐藏状态将捕捉到网络迄今所见的句子中所有单词的上下文信息。基本上,前一个隐藏状态 acts like a memory,并捕捉句子的上下文。有了这个上下文和当前输入,我们可以预测相关的单词。
例如,让我们拿同样的句子 The sun rises in the ____. 举例。如下图所示,我们首先将单词 the 作为输入传递,然后将下一个单词 sun 作为输入;但与此同时,我们也传递了上一个隐藏状态,。因此,每次传递输入单词时,我们也会传递前一个隐藏状态作为输入。
在最后一步,我们传递单词 the,同时传递前一个隐藏状态 ,它捕捉到了网络迄今为止所见的单词序列的上下文信息。因此,
充当了记忆,存储了网络已经看到的所有先前单词的信息。有了
和当前输入单词 (the),我们可以预测出相关的下一个单词:
简言之,RNN 使用前一个隐藏状态作为记忆,捕捉并存储网络迄今所见的上下文信息(输入)。
RNN 广泛应用于涉及序列数据的用例,如时间序列、文本、音频、语音、视频、天气等等。它们在各种自然语言处理(NLP)任务中被广泛使用,如语言翻译、情感分析、文本生成等。
前馈网络和 RNN 之间的区别
RNN 和前馈网络的比较如下图所示:
正如您可以在前面的图表中观察到的那样,RNN 在隐藏层中包含一个循环连接,这意味着我们使用前一个隐藏状态与输入一起来预测输出。
仍然感到困惑吗?让我们看看下面展开的一个 RNN 版本。但等等,什么是 RNN 的展开版本?
这意味着我们展开网络以完成一个完整的序列。假设我们有一个包含 个单词的输入句子;那么我们将有
到
层,每层对应一个单词,如下图所示:
如您在前图中所见,在时间步 ,基于当前输入
和先前的隐藏状态
预测输出
。同样,在时间步
,基于当前输入
和先前的隐藏状态
,预测
。这就是 RNN 的工作原理;它利用当前输入和先前的隐藏状态来预测输出。
循环神经网络的前向传播
让我们看看循环神经网络如何使用前向传播来预测输出;但在我们深入探讨之前,让我们熟悉一下符号:
前述图示明了以下:
-
表示输入到隐藏层的权重矩阵
-
表示隐藏到隐藏层的权重矩阵
-
表示隐藏到输出层的权重矩阵
在时间步 ,隐藏状态
可以计算如下:
也就是说,时间步 t 的隐藏状态 = tanh([输入到隐藏层权重 x 输入] + [隐藏到隐藏层权重 x 先前隐藏状态])。
在时间步 ,输出可以如下计算:
也就是说,时间步 t 的输出 = softmax(隐藏到输出层权重 x 时间步 t 的隐藏状态)。
我们还可以如下图所示表示循环神经网络(RNN)。正如您所看到的,隐藏层由一个 RNN 块表示,这意味着我们的网络是一个 RNN,并且先前的隐藏状态用于预测输出:
下图显示了 RNN 展开版本中前向传播的工作原理:
我们用随机值初始化初始隐藏状态 。如前图所示,输出
基于当前输入
和先前的隐藏状态(即初始隐藏状态)
使用以下公式预测:
类似地,看看输出 是如何计算的。它使用当前输入
和先前的隐藏状态
:
因此,在前向传播中,为了预测输出,RNN 使用当前输入和先前的隐藏状态。
为了明确起见,让我们看看如何在 RNN 中实现前向传播以预测输出:
- 通过从均匀分布中随机抽取,初始化所有权重
,
,和
:
U = np.random.uniform(-np.sqrt(1.0 / input_dim), np.sqrt(1.0 / input_dim), (hidden_dim, input_dim))
W = np.random.uniform(-np.sqrt(1.0 / hidden_dim), np.sqrt(1.0 / hidden_dim), (hidden_dim, hidden_dim))
V = np.random.uniform(-np.sqrt(1.0 / hidden_dim), np.sqrt(1.0 / hidden_dim), (input_dim, hidden_dim))
- 定义时间步长的数量,这将是我们输入序列的长度
:
num_time_steps = len(x)
- 定义隐藏状态:
hidden_state = np.zeros((num_time_steps + 1, hidden_dim))
- 用零初始化初始隐藏状态
:
hidden_state[-1] = np.zeros(hidden_dim)
- 初始化输出:
YHat = np.zeros((num_time_steps, output_dim))
- 对于每个时间步长,我们执行以下操作:
for t in np.arange(num_time_steps):
#h_t = tanh(UX + Wh_{t-1})
hidden_state[t] = np.tanh(U[:, x[t]] + W.dot(hidden_state[t - 1]))
# yhat_t = softmax(vh)
YHat[t] = softmax(V.dot(hidden_state[t]))
通过时间反向传播
我们刚刚学习了 RNN 中的前向传播如何工作以及如何预测输出。现在,我们计算损失 ,在每个时间步
上,以确定 RNN 预测输出的效果。我们使用交叉熵损失作为我们的损失函数。时间步
处的损失
可以如下给出:
这里, 是实际输出,而
是时间步长为
时的预测输出。
最终损失是所有时间步长上的损失之和。假设我们有 层;那么,最终损失可以如下给出:
如下图所示,最终损失是所有时间步长上损失的总和:
我们计算了损失,现在我们的目标是最小化损失。我们如何最小化损失?我们可以通过找到 RNN 的最优权重来最小化损失。正如我们所学的,RNN 中有三个权重:输入到隐藏的权重 ,隐藏到隐藏的权重
,以及隐藏到输出的权重
。
我们需要找到所有这三个权重的最优值以最小化损失。我们可以使用我们喜爱的梯度下降算法来找到最优权重。我们首先计算损失函数相对于所有权重的梯度,然后根据权重更新规则更新权重,如下所示:
如果您不想理解梯度计算背后的数学,可以跳过接下来的几节。但是,这将有助于您更好地理解循环神经网络中的 BPTT 工作原理。
首先,我们计算损失相对于最终层 的梯度,即
,以便我们可以在接下来的步骤中使用它。
正如我们所学的,时间步骤 处的损失
可以表示如下:
因为我们知道:
我们可以写成:
因此,损失 相对于
的梯度变为:
现在,我们将学习如何逐个计算损失相对于所有权重的梯度。
针对隐藏到输出权重 V 的梯度
首先,让我们回顾一下前向传播涉及的步骤:
假设 ,将其代入方程 (2),我们可以重写上述步骤如下:
在预测输出 后,我们处于网络的最终层。由于我们正在进行反向传播,即从输出层到输入层,我们的第一个权重将是
,即隐藏到输出层的权重。
我们已经看到,最终损失是所有时间步长上的损失之和,类似地,最终梯度是所有时间步长上梯度的总和:
因此,我们可以写成:
回顾我们的损失函数,;我们不能计算相对于的梯度
直接来自
,因为其中没有
项。因此,我们应用链式法则。回顾前向传播方程;在
中有一个
项:
,其中
首先,我们计算损失对的偏导数,然后从
中计算对
的偏导数。从
中,我们可以计算对
的导数。
因此,我们的方程如下:
由于我们知道,损失函数对
的梯度可以计算如下:
将方程(4)代入方程(3),我们可以写成以下形式:
为了更好地理解,让我们逐个从前述方程中取出每个项并逐个计算:
根据方程(1),我们可以将的值代入前述方程(6)中,如下所示:
现在,我们将计算项。因为我们知道
,计算
给出 softmax 函数的导数:
softmax 函数的导数可以表示如下:
将方程(8)代入方程(7),我们可以写成以下形式:
因此,最终方程如下:
现在,我们可以将方程(9)代入方程(5):
因为我们知道,我们可以写成:
将前述方程代入方程(10),我们得到我们的最终方程,即损失函数对的梯度如下:
对隐藏到隐藏层权重 W 的梯度
现在,我们将计算损失相对于隐藏到隐藏层权重 的梯度。与
类似,最终的梯度是所有时间步长上梯度的总和:
因此,我们可以写成:
首先,让我们计算损失的梯度 对
的导数,即
。
我们不能直接从中计算 对
的导数,因为其中没有
项。因此,我们使用链式法则计算损失对
的梯度。让我们重新回顾前向传播方程:
首先,我们计算损失 对
的偏导数;然后,从
开始,计算对
的偏导数;然后,从
开始,我们可以计算对 W 的导数,如下所示:
现在,让我们计算损失的梯度 对
的导数,即
。因此,我们再次应用链式法则,得到以下结果:
如果您看看前述等式,我们如何计算项 ?让我们回顾一下
的方程:
正如您在前述等式中所看到的那样,计算 取决于
和
,但
并非常数;它再次是一个函数。因此,我们需要计算其相对于该函数的导数。
然后方程变为:
下图显示了计算 ;我们可以注意到
如何依赖于
:
现在,让我们计算损失函数的梯度,即关于
的梯度。因此,我们再次应用链式法则,得到以下结果:
在前述方程中,我们无法直接计算。回顾方程
:
正如您所观察到的,计算取决于一个函数
,而
再次是取决于函数
的函数。如下图所示,为了计算关于
的导数,我们需要遍历直到
,因为每个函数彼此依赖:
这可以用下图来形象地表示:
这适用于任何时间步长的损失;比如说,。因此,我们可以说,要计算任何损失
,我们需要遍历到
,如下图所示:
这是因为在循环神经网络中,时间的隐藏状态取决于时间
的隐藏状态,这意味着当前隐藏状态始终依赖于先前的隐藏状态。
因此,任何损失可以如下图所示地计算:
因此,我们可以写出损失函数关于
的梯度如下:
在前述方程中,前述方程中的总和意味着所有隐藏状态
的总和。在前述方程中,
可以使用链式法则计算。因此,我们可以说:
假设j=3和k=0;那么,前述方程变为:
将方程(12)代入方程(11)将得到以下结果:
我们知道最终损失是所有时间步长上损失的总和:
将方程 (13) 代入前述方程,我们得到以下结果:
在前述方程中,我们有两个求和,其中:
-
暗示了所有时间步长上损失的总和
-
是隐藏状态的总和
因此,我们计算损失关于 W 的梯度的最终方程为:
现在,我们将逐一看如何计算上述方程中的每个术语。从方程 (4) 和方程 (9),我们可以说:
让我们看下一个术语:
我们知道隐藏状态 的计算为:
的导数为
,因此我们可以写成:
让我们来看最后一个术语 。我们知道隐藏状态
的计算如下:
。因此,损失
关于
的导数为:
将所有计算出的项代入方程 (15),我们得到了关于损失梯度 关于
的最终方程如下:
关于隐藏层权重输入的梯度 U
计算损失函数对 的梯度与
相同,因为这里我们也是对
进行顺序导数。类似于
,为了计算任何损失
关于
的导数,我们需要沿着一直回到
。
计算损失关于 的梯度的最终方程如下。正如你所注意到的那样,它基本上与方程 (15) 相同,只是我们有项
而不是显示为
:
我们已经在前一节中看到如何计算前两项。
让我们看看最终项 。我们知道隐藏状态
计算如下,
。因此,
对
的导数推导如下:
因此,我们对损失 对
的最终梯度方程可以写成如下形式:
梯度消失和梯度爆炸问题
我们刚刚学习了 BPTT 的工作原理,看到了如何计算 RNN 中所有权重的损失梯度。但在这里,我们将遇到一个称为梯度消失和梯度爆炸的问题。
在计算损失对 和
的导数时,我们看到我们必须遍历直到第一个隐藏状态,因为每个隐藏状态
都依赖于其前一个隐藏状态
。
例如,损失 对
的梯度给出如下:
如果你看一下前述方程中的项 ,我们无法计算导数
关于 对
的直接推导。正如我们所知,
是一个依赖于
和
的函数。因此,我们也需要计算对
的导数。即使
也是一个依赖于
和
的函数。因此,我们还需要计算对
的导数。
如下图所示,为了计算 的导数,我们需要一直追溯到初始隐藏状态
,因为每个隐藏状态都依赖于其前一个隐藏状态:
因此,为了计算任何损失 ,我们需要一直回溯到初始隐藏状态。
状态 ,因为每个隐藏状态都依赖于其前一个隐藏状态。假设我们有一个具有 50 层的深度递归网络。为了计算损失
,我们需要一直回溯到
,如下图所示:
所以,问题究竟出在哪里?在向初始隐藏状态反向传播时,我们丢失了信息,RNN 将不能完美地反向传播。
记得 吗?每次向后移动时,我们计算
的导数。tanh 的导数被限制在 1. 我们知道,当两个介于 0 和 1 之间的值相乘时,结果将会更小。我们通常将网络的权重初始化为一个小数。因此,当我们在反向传播时乘以导数和权重时,实质上是在乘以较小的数。
当我们在向后移动时,每一步乘以较小的数,我们的梯度变得无限小,导致计算机无法处理的数值;这被称为梯度消失问题。
回顾我们在 关于隐藏层到隐藏层权重 W 的梯度 部分看到的关于损失的梯度方程式:
正如你所看到的,我们在每个时间步长上乘以权重和 tanh 函数的导数。这两者的重复乘法会导致一个很小的数,从而引起梯度消失问题。
梯度消失问题不仅出现在 RNN 中,还出现在其他使用 sigmoid 或 tanh 作为激活函数的深层网络中。因此,为了克服这个问题,我们可以使用 ReLU 作为激活函数,而不是 tanh。
然而,我们有一种称为长短期记忆(LSTM)网络的 RNN 变体,它可以有效地解决梯度消失问题。我们将在第五章 RNN 的改进 中看看它是如何工作的。
同样地,当我们将网络的权重初始化为非常大的数时,在每一步中梯度会变得非常大。在反向传播时,我们在每个时间步长上乘以一个大数,导致梯度爆炸。这被称为梯度爆炸问题。
梯度裁剪
我们可以使用梯度裁剪来避免梯度爆炸问题。在这种方法中,我们根据向量范数(比如 L2 范数)来归一化梯度,并将梯度值裁剪到某个范围内。例如,如果我们将阈值设为 0.7,那么我们保持梯度在 -0.7 到 +0.7 的范围内。如果梯度值超过 -0.7,我们将其更改为 -0.7;同样地,如果超过 0.7,我们将其更改为 +0.7。
假设 是损失函数 L 关于 W 的梯度:
首先,我们使用 L2 范数对梯度进行归一化,即 。如果归一化的梯度超过了定义的阈值,我们更新梯度如下:
使用 RNN 生成歌词
我们已经学习了关于 RNN 的足够知识;现在,让我们看看如何使用 RNN 生成歌词。为此,我们简单地构建一个字符级 RNN,也就是说,在每个时间步,我们预测一个新字符。
让我们考虑一个小句子,What a beautiful d。
在第一个时间步,RNN 预测一个新字符 a。句子将更新为 What a beautiful da.。
在下一个时间步,它预测一个新字符 y,句子变成了 What a beautiful day.。
这样,我们每个时间步预测一个新字符并生成一首歌曲。除了每次预测一个新字符外,我们还可以每次预测一个新单词,这称为词级 RNN。为简单起见,让我们从字符级 RNN 开始。
但是,RNN 如何在每个时间步预测一个新字符呢?假设在时间步 t=0 时,我们输入一个字符 x。现在 RNN 根据给定的输入字符 x 预测下一个字符。为了预测下一个字符,它会预测我们词汇表中所有字符成为下一个字符的概率。一旦得到这个概率分布,我们根据这个概率随机选择下一个字符。有点糊涂吗?让我们通过一个例子更好地理解这个过程。
例如,如下图所示,假设我们的词汇表包含四个字符 L, O, V, 和 E;当我们将字符 L 作为输入时,RNN 计算词汇表中所有单词成为下一个字符的概率:
因此,我们得到概率 [0.0, 0.9, 0.0, 0.1],对应词汇表中的字符 [L,O,V,E]。通过从这个概率分布中抽样来预测下一个字符,给输出增加了一些随机性。
在下一个时间步上,我们将上一时间步预测的字符和先前的隐藏状态作为输入,预测下一个字符,如下图所示:
因此,在每个时间步上,我们将上一时间步预测的字符和先前的隐藏状态作为输入,并预测下一个字符,如下所示:
正如您在前面的图中所见,在时间步 t=2,V 作为输入传递,并预测下一个字符为 E。但这并不意味着每次将字符 V 作为输入发送时都应始终返回 E 作为输出。由于我们将输入与先前的隐藏状态一起传递,RNN 记住了到目前为止看到的所有字符。
因此,先前的隐藏状态捕捉了前面输入字符的精髓,即 L 和 O。现在,使用此前的隐藏状态和输入 V,RNN 预测下一个字符为 E。
在 TensorFlow 中实现
现在,我们将看看如何在 TensorFlow 中构建 RNN 模型来生成歌词。该数据集以及本节中使用的完整代码和逐步说明可以在 GitHub 上的 bit.ly/2QJttyp
获取。下载后,解压缩档案,并将 songdata.csv
放在 data
文件夹中。
导入所需的库:
import warnings
warnings.filterwarnings('ignore')
import random
import numpy as np
import tensorflow as tf
tf.logging.set_verbosity(tf.logging.ERROR)
import warnings
warnings.filterwarnings('ignore')
数据准备
读取下载的输入数据集:
df = pd.read_csv('data/songdata.csv')
让我们看看我们的数据集中有什么:
df.head()
前述代码生成如下输出:
我们的数据集包含约 57,650 首歌曲:
df.shape[0]
57650
我们有约 643
位艺术家的歌词:
len(df['artist'].unique())
643
每位艺术家的歌曲数量如下所示:
df['artist'].value_counts()[:10]
Donna Summer 191
Gordon Lightfoot 189
George Strait 188
Bob Dylan 188
Loretta Lynn 187
Cher 187
Alabama 187
Reba Mcentire 187
Chaka Khan 186
Dean Martin 186
Name: artist, dtype: int64
平均每位艺术家有约 89
首歌曲:
df['artist'].value_counts().values.mean()
89
我们在 text
列中有歌词,因此我们将该列的所有行组合起来,并将其保存为名为 data
的变量中的 text
,如下所示:
data = ', '.join(df['text'])
让我们看看一首歌的几行:
data[:369]
"Look at her face, it's a wonderful face \nAnd it means something special to me \nLook at the way that she smiles when she sees me \nHow lucky can one fellow be? \n \nShe's just my kind of girl, she makes me feel fine \nWho could ever believe that she could be mine? \nShe's just my kind of girl, without her I'm blue \nAnd if she ever leaves me what could I do, what co"
由于我们正在构建字符级 RNN,我们将数据集中所有唯一字符存储在名为 chars
的变量中;这基本上就是我们的词汇表:
chars = sorted(list(set(data)))
将词汇表大小存储在名为 vocab_size
的变量中:
vocab_size = len(chars)
由于神经网络只接受数字输入,因此我们需要将词汇表中的所有字符转换为数字。
我们将词汇表中的所有字符映射到它们的对应索引,形成一个唯一的数字。我们定义了一个 char_to_ix
字典,其中包含所有字符到它们索引的映射。为了通过字符获取索引,我们还定义了 ix_to_char
字典,其中包含所有索引到它们相应字符的映射:
char_to_ix = {ch: i for i, ch in enumerate(chars)}
ix_to_char = {i: ch for i, ch in enumerate(chars)}
如您在下面的代码片段中所见,字符 's'
在 char_to_ix
字典中映射到索引 68
:
print char_to_ix['s']
68
类似地,如果我们将 68
作为输入给 ix_to_char
,那么我们得到相应的字符,即 's'
:
print ix_to_char[68]
's'
一旦我们获得字符到整数的映射,我们使用独热编码将输入和输出表示为向量形式。独热编码向量 基本上是一个全为 0 的向量,除了对应字符索引位置为 1。
例如,假设 vocabSize
是 7
,而字符 z 在词汇表中的第四个位置。那么,字符 z 的独热编码表示如下所示:
vocabSize = 7
char_index = 4
print np.eye(vocabSize)[char_index]
array([0., 0., 0., 0., 1., 0., 0.])
如您所见,我们在对应字符的索引位置有一个 1,其余值为 0。这就是我们将每个字符转换为独热编码向量的方式。
在以下代码中,我们定义了一个名为 one_hot_encoder
的函数,该函数将根据字符的索引返回一个独热编码向量:
def one_hot_encoder(index):
return np.eye(vocab_size)[index]
定义网络参数
接下来,我们定义所有网络参数:
- 定义隐藏层中的单元数:
hidden_size = 100
- 定义输入和输出序列的长度:
seq_length = 25
- 定义梯度下降的学习率:
learning_rate = 1e-1
- 设置种子值:
seed_value = 42
tf.set_random_seed(seed_value)
random.seed(seed_value)
定义占位符
现在,我们将定义 TensorFlow 的占位符:
- 输入和输出的
placeholders
定义如下:
inputs = tf.placeholder(shape=[None, vocab_size],dtype=tf.float32, name="inputs")
targets = tf.placeholder(shape=[None, vocab_size], dtype=tf.float32, name="targets")
- 定义初始隐藏状态的
placeholder
:
init_state = tf.placeholder(shape=[1, hidden_size], dtype=tf.float32, name="state")
- 定义用于初始化 RNN 权重的
initializer
:
initializer = tf.random_normal_initializer(stddev=0.1)
定义前向传播
让我们定义涉及 RNN 的前向传播,数学表达如下:
和
是隐藏层和输出层的偏置项,简单起见,在前面的方程中我们没有添加它们。前向传播可以实现如下:
with tf.variable_scope("RNN") as scope:
h_t = init_state
y_hat = []
for t, x_t in enumerate(tf.split(inputs, seq_length, axis=0)):
if t > 0:
scope.reuse_variables()
#input to hidden layer weights
U = tf.get_variable("U", [vocab_size, hidden_size], initializer=initializer)
#hidden to hidden layer weights
W = tf.get_variable("W", [hidden_size, hidden_size], initializer=initializer)
#output to hidden layer weights
V = tf.get_variable("V", [hidden_size, vocab_size], initializer=initializer)
#bias for hidden layer
bh = tf.get_variable("bh", [hidden_size], initializer=initializer)
#bias for output layer
by = tf.get_variable("by", [vocab_size], initializer=initializer)
h_t = tf.tanh(tf.matmul(x_t, U) + tf.matmul(h_t, W) + bh)
y_hat_t = tf.matmul(h_t, V) + by
y_hat.append(y_hat_t)
对输出应用 softmax
并获取概率:
output_softmax = tf.nn.softmax(y_hat[-1])
outputs = tf.concat(y_hat, axis=0)
计算交叉熵损失:
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=targets, logits=outputs))
将 RNN 的最终隐藏状态存储在 hprev
中。我们使用这个最终隐藏状态来进行预测:
hprev = h_t
定义 BPTT
现在,我们将执行 BPTT,使用 Adam 作为优化器。我们还将进行梯度裁剪以避免梯度爆炸问题:
- 初始化 Adam 优化器:
minimizer = tf.train.AdamOptimizer()
- 使用 Adam 优化器计算损失的梯度:
gradients = minimizer.compute_gradients(loss)
- 设置梯度裁剪的阈值:
threshold = tf.constant(5.0, name="grad_clipping")
- 裁剪超过阈值的梯度并将其带入范围内:
clipped_gradients = []
for grad, var in gradients:
clipped_grad = tf.clip_by_value(grad, -threshold, threshold)
clipped_gradients.append((clipped_grad, var))
- 使用裁剪后的梯度更新梯度:
updated_gradients = minimizer.apply_gradients(clipped_gradients)
开始生成歌曲
开始 TensorFlow 会话并初始化所有变量:
sess = tf.Session()
init = tf.global_variables_initializer()
sess.run(init)
现在,我们将看看如何使用 RNN 生成歌词。RNN 的输入和输出应该是什么?它是如何学习的?训练数据是什么?让我们逐步理解这一解释,并附上代码。
我们知道在 RNN 中,在时间步 预测的输出将作为下一个时间步的输入;也就是说,每个时间步,我们需要将前一个时间步预测的字符作为输入。因此,我们以同样的方式准备我们的数据集。
例如,请看下表。假设每一行代表一个不同的时间步;在时间步 ,RNN 预测一个新字符 g 作为输出。这将作为下一个时间步
的输入发送。
然而,如果你注意到时间步 中的输入,我们从输入
o
中删除了第一个字符,并在序列末尾添加了新预测的字符 g。为什么要删除输入中的第一个字符?因为我们需要保持序列长度。
假设我们的序列长度是八;将新预测的字符添加到序列中会将序列长度增加到九。为了避免这种情况,我们从输入中移除第一个字符,同时将前一个时间步的新预测字符添加进来。
类似地,在输出数据中,我们也会在每个时间步删除第一个字符,因为一旦预测了新字符,序列长度就会增加。为了避免这种情况,在每个时间步从输出中删除第一个字符,如下表所示:
现在,我们将看看如何准备我们的输入和输出序列,类似于前面的表格。
定义一个名为 pointer
的变量,它指向数据集中的字符。我们将 pointer
设置为 0
,这意味着它指向第一个字符:
pointer = 0
定义输入数据:
input_sentence = data[pointer: pointer + seq_length]
这是什么意思?使用指针和序列长度切片数据。假设 seq_length
是 25
,指针是 0
。它将返回前 25 个字符作为输入。因此,data[pointer:pointer + seq_length]
返回以下输出:
"Look at her face, it's a "
定义输出,如下所示:
output_sentence = data[pointer + 1: pointer + seq_length + 1]
我们将输出数据切片,从输入数据移动一个字符。因此,data[pointer + 1:pointer + seq_length + 1]
返回以下内容:
"ook at her face, it's a w"
正如你所看到的,我们在前一句中添加了下一个字符并删除了第一个字符。因此,每次迭代时,我们增加指针并遍历整个数据集。这就是我们为训练 RNN 获取输入和输出句子的方法。
就像我们学到的那样,RNN 仅接受数字作为输入。一旦我们切片了输入和输出序列,我们使用之前定义的 char_to_ix
字典获取相应字符的索引:
input_indices = [char_to_ix[ch] for ch in input_sentence]
target_indices = [char_to_ix[ch] for ch in output_sentence]
使用我们之前定义的 one_hot_encoder
函数将索引转换为 one-hot 编码向量:
input_vector = one_hot_encoder(input_indices)
target_vector = one_hot_encoder(target_indices)
这 input_vector
和 target_vector
成为训练 RNN 的输入和输出。现在,让我们开始训练。
hprev_val
变量存储了我们训练的 RNN 模型的最后隐藏状态,我们用它来进行预测,并将损失存储在名为loss_val
的变量中:
hprev_val, loss_val, _ = sess.run([hprev, loss, updated_gradients], feed_dict={inputs: input_vector,targets: target_vector,init_state: hprev_val})
我们训练模型进行n次迭代。训练后,我们开始进行预测。现在,我们将看看如何进行预测并使用我们训练的 RNN 生成歌词。设置sample_length
,即我们想要生成的句子(歌曲)的长度:
sample_length = 500
随机选择输入序列的起始索引:
random_index = random.randint(0, len(data) - seq_length)
选择具有随机选择索引的输入句子:
sample_input_sent = data[random_index:random_index + seq_length]
正如我们所知,我们需要将输入作为数字进行馈送;将所选输入句子转换为索引:
sample_input_indices = [char_to_ix[ch] for ch in sample_input_sent]
记住,我们将 RNN 的最后隐藏状态存储在hprev_val
中。我们使用它来进行预测。我们通过从hprev_val
复制值来创建一个名为sample_prev_state_val
的新变量。
sample_prev_state_val
用作进行预测的初始隐藏状态:
sample_prev_state_val = np.copy(hprev_val)
初始化用于存储预测输出索引的列表:
predicted_indices = []
现在,对于t
在sample_length
的范围内,我们执行以下操作并为定义的sample_length
生成歌曲:
将sampled_input_indices
转换为 one-hot 编码向量:
sample_input_vector = one_hot_encoder(sample_input_indices)
将sample_input_vector
和初始隐藏状态sample_prev_state_val
馈送给 RNN,并获得预测。我们将输出概率分布存储在probs_dist
中:
probs_dist, sample_prev_state_val = sess.run([output_softmax, hprev],
feed_dict={inputs: sample_input_vector,init_state: sample_prev_state_val})
使用由 RNN 生成的概率分布随机选择下一个字符的索引:
ix = np.random.choice(range(vocab_size), p=probs_dist.ravel())
将新预测的索引ix
添加到sample_input_indices
中,并从sample_input_indices
中删除第一个索引以保持序列长度。这将形成下一个时间步的输入:
sample_input_indices = sample_input_indices[1:] + [ix]
存储所有预测的chars
索引在predicted_indices
列表中:
predicted_indices.append(ix)
将所有的predicted_indices
转换为它们对应的字符:
predicted_chars = [ix_to_char[ix] for ix in predicted_indices]
将所有的predicted_chars
组合起来,并保存为text
:
text = ''.join(predicted_chars)
在每 50,000^(th)次迭代时打印预测文本:
print ('\n')
print (' After %d iterations' %(iteration))
print('\n %s \n' % (text,))
print('-'*115)
增加pointer
和iteration
:
pointer += seq_length
iteration += 1
在初始迭代中,您可以看到 RNN 生成了随机字符。但是在第 50,000^(th)次迭代中,它开始生成有意义的文本:
After 0 iterations
Y?a6C.-eMSfk0pHD v!74YNeI 3YeP,h- h6AADuANJJv:HA(QXNeKzwCjBnAShbavSrGw7:ZcSv[!?dUno Qt?OmE-PdY wrqhSu?Yvxdek?5Rn'Pj!n5:32a?cjue ZIj
Xr6qn.scqpa7)[MSUjG-Sw8n3ZexdUrLXDQ:MOXBMX EiuKjGudcznGMkF:Y6)ynj0Hiajj?d?n2Iapmfc?WYd BWVyB-GAxe.Hq0PaEce5H!u5t: AkO?F(oz0Ma!BUMtGtSsAP]Oh,1nHf5tZCwU(F?X5CDzhOgSNH(4Cl-Ldk? HO7 WD9boZyPIDghWUfY B:r5z9Muzdw2'WWtf4srCgyX?hS!,BL GZHqgTY:K3!wn:aZGoxr?zmayANhMKJsZhGjpbgiwSw5Z:oatGAL4Xenk]jE3zJ?ymB6v?j7(mL[3DFsO['Hw-d7htzMn?nm20o'?6gfPZhBa
NlOjnBd2n0 T"d'e1k?OY6Wwnx6d!F
----------------------------------------------------------------------------------------------
After 50000 iterations
Hem-:]
[Ex" what
Akn'lise
[Grout his bring bear.
Gnow ourd?
Thelf
As cloume
That hands, Havi Musking me Mrse your leallas, Froking the cluse (have: mes.
I slok and if a serfres me the sky withrioni flle rome.....Ba tut get make ome
But it lives I dive.
[Lett it's to the srom of and a live me it's streefies
And is.
As it and is me dand a serray]
zrtye:"
Chay at your hanyer
[Every rigbthing with farclets
[Brround.
Mad is trie
[Chare's a day-Mom shacke?
, I
-------------------------------------------------------------------------------------------------
不同类型的 RNN 架构
现在我们已经了解了 RNN 的工作原理,我们将看看基于输入和输出数量的不同类型的 RNN 架构。
一对一架构
在一对一架构中,单个输入映射到单个输出,时间步t的输出作为下一个时间步的输入。我们已经在最后一节中看到了这种架构,用于使用 RNN 生成歌曲。
例如,在文本生成任务中,我们将当前时间步生成的输出作为下一个时间步的输入,以生成下一个单词。这种架构在股票市场预测中也被广泛使用。
下图展示了一对一的 RNN 架构。如您所见,时间步t预测的输出被发送为下一个时间步的输入:
![# 一对多架构在一对多架构中,单个输入映射到多个隐藏状态和多个输出值,这意味着 RNN 接受单个输入并映射到输出序列。尽管我们只有一个输入值,但我们在时间步之间共享隐藏状态以预测输出。与前述一对一架构不同,这里我们只在时间步之间共享上一个隐藏状态,而不是上一个输出。这种架构的一个应用是图像标题生成。我们传递单个图像作为输入,输出是构成图像标题的单词序列。如下图所示,将单个图像作为输入传递给 RNN,在第一个时间步,预测单词Horse;在下一个时间步,使用上一个隐藏状态预测下一个单词standing。类似地,它持续一系列步骤并预测下一个单词,直到生成标题:
多对一架构
一个多对一架构,顾名思义,接受一个输入序列并将其映射到单个输出值。多对一架构的一个流行示例是情感分类。一句话是一个单词序列,因此在每个时间步,我们将每个单词作为输入传递,并在最终时间步预测输出。
假设我们有一个句子:Paris is a beautiful city. 如下图所示,在每个时间步,一个单词作为输入传递,并且在最终时间步预测句子的情感:
多对多架构
在多对多架构中,我们将任意长度的输入序列映射到任意长度的输出序列。这种架构已被用于各种应用中。多对多架构的一些流行应用包括语言翻译、对话机器人和音频生成。
假设我们正在将一句英语句子转换成法语。考虑我们的输入句子:What are you doing? 如下图所示,它将映射为Que faites vous:
总结
我们首先介绍了什么是 RNN,以及 RNN 与前馈网络的区别。我们了解到 RNN 是一种特殊类型的神经网络,广泛应用于序列数据;它根据当前输入和先前的隐藏状态预测输出,隐藏状态充当记忆,存储到目前为止网络所见到的信息序列。
我们学习了 RNN 中的前向传播工作原理,然后详细推导了用于训练 RNN 的 BPTT 算法。接着,我们通过在 TensorFlow 中实现 RNN 来生成歌词。在本章末尾,我们了解了 RNN 的不同架构,如一对一、一对多、多对一和多对多,它们被用于各种应用中。
在下一章中,我们将学习关于 LSTM 单元的内容,它解决了 RNN 中的梯度消失问题。我们还将学习不同变体的 RNN。
问题
尝试回答以下问题:
-
RNN 与前馈神经网络有什么区别?
-
RNN 中的隐藏状态是如何计算的?
-
递归网络有什么用途?
-
梯度消失问题是如何发生的?
-
什么是爆炸梯度问题?
-
梯度裁剪如何缓解爆炸梯度问题?
-
不同类型的 RNN 架构有哪些?
进一步阅读
参考以下链接了解更多关于 RNN 的内容:
-
循环神经网络(RNN)和长短期记忆(LSTM)网络基础,作者 Alex Sherstinsky,
arxiv.org/pdf/1808.03314.pdf
-
利用 TensorFlow 进行手写生成的 RNN,基于 Alex Graves 的用循环神经网络生成序列,
github.com/snowkylin/rnn-handwriting-generation
第五章:RNN 的改进
递归神经网络(RNN)的缺点是它不能长时间保持信息在内存中。我们知道 RNN 将信息序列存储在其隐藏状态中,但是当输入序列过长时,由于消失梯度问题,它无法将所有信息保留在内存中,这是我们在前一章节讨论过的。
为了应对这个问题,我们引入了一种称为长短期记忆(LSTM)单元的 RNN 变体,通过使用称为门的特殊结构来解决消失梯度问题。门会根据需要保持信息在内存中。它们学会了什么信息应该保留,什么信息应该丢弃。
我们将从探索 LSTM 和它如何克服 RNN 的缺点开始这一章节。接下来,我们将学习如何使用 LSTM 单元在 TensorFlow 中执行前向和后向传播,并且了解如何使用它们来预测比特币的价格。
接下来,我们将掌握门控循环单元(GRU)单元,它是 LSTM 单元的简化版本。我们将学习 GRU 单元中的前向和后向传播的工作原理。接下来,我们将基本了解双向 RNNs 的工作原理及其如何利用过去和未来的信息进行预测;我们还将了解深层 RNNs 的工作方式。
在章节结束时,我们将学习关于序列到序列模型的内容,该模型将长度可变的输入映射到长度可变的输出。我们将深入探讨序列到序列模型的架构和注意力机制。
为了确保对这些主题有清晰的理解,本章节按以下方式组织:
-
LSTM 来拯救
-
LSTM 中的前向和后向传播
-
使用 LSTM 预测比特币价格
-
GRUs
-
GRUs 中的前向和后向传播
-
双向 RNNs
-
深层 RNNs
-
序列到序列模型
LSTM 来拯救
在反向传播 RNN 时,我们发现了一个称为消失梯度的问题。由于消失梯度问题,我们无法正确训练网络,导致 RNN 无法在记忆中保留长序列。为了更好地理解我们所说的,让我们考虑一个简短的句子:
天空是 __。
基于它所见的信息,RNN 可以轻松预测空白为 蓝色,但它无法涵盖长期依赖关系。这意味着什么?让我们考虑以下句子以更好地理解这个问题:
阿奇在中国生活了 13 年。他喜欢听好音乐。他是漫画迷。他精通 ____。
现在,如果我们被要求预测前述句子中的缺失词,我们会预测它为Chinese,但是我们是如何预测的呢?我们简单地记住了前面的句子,并理解阿奇在中国生活了 13 年。这使我们得出结论,阿奇可能会说流利的中文。另一方面,RNN 无法在其记忆中保留所有这些信息以表明阿奇能说流利的中文。由于梯度消失问题,它无法长时间保留信息。也就是说,当输入序列很长时,RNN 的记忆(隐藏状态)无法容纳所有信息。为了缓解这一问题,我们使用 LSTM 单元。
LSTM 是 RNN 的一种变体,解决了梯度消失问题,并在需要时保留信息在内存中。基本上,图中的隐藏单元中的 RNN 单元被替换为 LSTM 单元:
理解 LSTM 单元
是什么使 LSTM 单元如此特殊?LSTM 单元如何实现长期依赖?它如何知道什么信息需要保留,什么信息需要从记忆中丢弃?
这一切都是通过称为门的特殊结构实现的。如下图所示,典型的 LSTM 单元包括三个特殊的门,称为输入门、输出门和遗忘门:
这三个门负责决定什么信息需要添加、输出和从记忆中遗忘。有了这些门,LSTM 单元可以有效地只在需要时保留信息在记忆中。
在 RNN 单元中,我们使用隐藏状态,,有两个目的:一个是存储信息,另一个是进行预测。不像 RNN,在 LSTM 单元中,我们将隐藏状态分解为两个状态,称为单元状态和隐藏状态:
-
单元状态也称为内部存储器,所有信息将存储在这里
-
隐藏状态用于计算输出,也就是进行预测。
单元状态和隐藏状态在每个时间步共享。现在,我们将深入研究 LSTM 单元,看看这些门如何使用以及隐藏状态如何预测输出。
遗忘门
遗忘门,,负责决定应该从单元状态(记忆)中移除哪些信息。考虑以下句子:
哈里 是一位好歌手。他住在纽约。赛恩也是一位好歌手。
一旦我们开始谈论赛恩,网络就会理解主题已从哈里变为赛恩,关于哈里的信息不再需要。现在,遗忘门将从单元状态中删除/遗忘有关哈里的信息。
遗忘门由 sigmoid 函数控制。在时间步,我们将输入
和上一个隐藏状态
传递给遗忘门。如果细胞状态中的特定信息应该被移除,则返回
0
,如果不应该被移除,则返回1
。在时间步,遗忘门
的表达如下:
在这里,适用以下内容:
-
是遗忘门的输入到隐藏层权重。
-
是遗忘门的隐藏到隐藏层权重。
-
是遗忘门的偏置。
以下图示显示了遗忘门。正如你所见,输入与
以及上一个隐藏状态
与
相乘,然后两者相加并发送到 sigmoid 函数,返回
,如下所示:
输入门
输入门负责决定应该存储在细胞状态中的信息。让我们考虑相同的例子:
Harry is a good singer. He lives in New York. Zayn is also a good singer.
在遗忘门从细胞状态中移除信息后,输入门决定保留在记忆中的信息。在这里,由于遗忘门已从细胞状态中移除了关于 Harry 的信息,输入门决定用Zayn的信息更新细胞状态。
类似于遗忘门,输入门由 sigmoid 函数控制,返回 0 到 1 范围内的输出。如果返回1
,则特定信息将被存储/更新到细胞状态中,如果返回0
,则不会将信息存储到细胞状态中。在时间步,输入门
的表达如下:
在这里,适用以下内容:
-
是输入门的输入到隐藏层权重。
-
是输入门的隐藏到隐藏权重。
-
是输入门的偏置。
以下图示显示了输入门:
输出门
我们将在细胞状态(记忆)中有大量信息。输出门负责决定从细胞状态中取出哪些信息作为输出。考虑以下句子:
赞恩的首张专辑取得了巨大成功。祝贺 ____。
输出门将查找细胞状态中的所有信息,并选择正确的信息填补空白。在这里,congrats
是用来描述名词的形容词。因此,输出门将预测填补空白的是 Zayn(名词)。与其他门类似,它也由一个 sigmoid 函数控制。在时间步,输出门
的表示如下:
在这里,适用以下规则:
-
是输出门的输入到隐藏层权重
-
是输出门的隐藏到隐藏层权重
-
是输出门的输入到隐藏层权重
输出门如下图所示:
是输出门的偏置
更新细胞状态
我们刚刚学习了 LSTM 网络中所有三个门是如何工作的,但问题是,我们如何实际通过门来更新细胞状态,添加相关的新信息并删除不需要的信息?
首先,我们将看看如何向细胞状态添加新的相关信息。为了容纳可以添加到细胞状态(记忆)中的所有新信息,我们创建一个新的向量称为 。它被称为 候选状态 或 内部状态向量。与由 sigmoid 函数调节的门不同,候选状态由 tanh 函数调节,但为什么呢?Sigmoid 函数返回范围在
0
到 1
之间的值,即始终为正。我们需要允许 的值可以是正或负。因此,我们使用 tanh 函数,它返回范围在
-1
到 +1
之间的值。
在时间步,候选状态
的表示如下:
在这里,适用以下规则:
-
是候选状态的输入到隐藏层权重
-
是候选状态的隐藏到隐藏层权重
-
是候选状态的偏置
因此,候选状态包含了所有可以添加到细胞状态(记忆)中的新信息。下图显示了候选状态:
我们如何决定候选状态中的信息是否相关?我们如何决定是否将候选状态中的新信息添加到细胞状态中?我们学到,输入门负责决定是否添加新信息,因此如果我们将 和
相乘,我们只获得应添加到记忆中的相关信息。
换句话说,我们知道如果信息不需要,则输入门返回 0,如果信息需要则返回 1。例如 ,那么将
和
相乘得到 0,这意味着
中的信息不需要更新细胞状态的
。当
时,将
和
相乘得到
,这意味着我们可以更新
中的信息到细胞状态。
将新信息添加到细胞状态的输入门 和候选状态
在下图中显示:
现在,我们将看到如何从先前的细胞状态中移除不再需要的信息。
我们学到,遗忘门用于删除细胞状态中不需要的信息。因此,如果我们将先前的细胞状态 和遗忘门
相乘,那么我们只保留细胞状态中的相关信息。
例如 ,那么将
和
相乘得到 0,这意味着细胞状态中的信息
不需要,并且应该被移除(遗忘)。当
时,将
和
相乘得到
,这意味着先前细胞状态中的信息是必需的,不应被移除。
使用遗忘门 从先前的细胞状态
中移除信息如下图所示:
因此,简言之,我们通过乘以 和
来添加新信息,乘以
和
来移除信息,从而更新我们的细胞状态。我们可以将细胞状态方程表达如下:
更新隐藏状态
我们刚刚学习了如何更新细胞状态中的信息。现在,我们将看到如何更新隐藏状态中的信息。我们知道隐藏状态 用于计算输出,但是我们如何计算输出呢?
我们知道输出门负责决定从细胞状态中获取什么信息作为输出。因此,将 和细胞状态
的双曲正切乘积(将其压缩在 -1 和 +1 之间)给出输出。
因此,隐藏状态 ,可以表达如下:
下图显示了隐藏状态 如何通过乘以
和
来计算:
最后,一旦我们有了隐藏状态值,我们可以应用 softmax 函数并计算 如下:
这里, 是隐藏到输出层的权重。
LSTM 中的前向传播
将所有部分整合起来,所有操作后的最终 LSTM 细胞显示在以下图表中。 细胞状态和隐藏状态在时间步骤间共享,这意味着 LSTM 在时间步骤 计算细胞状态
和隐藏状态
,并将其发送到下一个时间步骤:
LSTM 细胞中完整的前向传播步骤如下所示:
-
输入门:
-
遗忘门:
-
输出门:
-
候选状态:
-
细胞状态:
-
隐藏状态:
-
输出:
LSTM 中的反向传播
我们在每个时间步计算损失,以确定我们的 LSTM 模型预测输出的好坏。假设我们使用交叉熵作为损失函数,则在时间步 的损失
如下方程所示:
这里, 是实际输出,
是时间步
的预测输出。
我们的最终损失是所有时间步的损失之和,可以表示为:
我们使用梯度下降来最小化损失。我们找到损失对网络中所有权重的导数,并找到最优权重以最小化损失:
-
我们有四个输入到隐藏层的权重,
,分别是输入门、遗忘门、输出门和候选状态的输入到隐藏层权重。
-
我们有四个隐藏到隐藏层的权重,
,分别对应输入门、遗忘门、输出门和候选状态的隐藏到隐藏层权重。
-
我们有一个隐藏到输出层的权重,
我们通过梯度下降找到所有这些权重的最优值,并根据权重更新规则更新权重。权重更新规则如下方程所示:
在下一节中,我们将逐步查看如何计算 LSTM 单元中所有权重相对于损失的梯度。
如果你对推导所有权重的梯度不感兴趣,可以跳过即将到来的部分。然而,这将加强你对 LSTM 单元的理解。
相对于门的梯度
计算 LSTM 单元中所有权重相对于损失的梯度需要计算所有门和候选状态的梯度。因此,在本节中,我们将学习如何计算损失函数相对于所有门和候选状态的梯度。
在我们开始之前,让我们回顾以下两件事:
- sigmoid 函数的导数表达如下:
- tanh 函数的导数表达如下:
在即将进行的计算中,我们将在多个地方使用损失相对于隐藏状态 和细胞状态
的梯度。因此,首先,我们将看看如何计算损失相对于隐藏状态
和细胞状态
的梯度。
首先,让我们看看如何计算损失相对于隐藏状态的梯度,。
我们知道输出 的计算如下:
假设 。我们在
中有
项,因此根据链式法则,我们可以写出以下内容:
我们已经看到如何在 第四章,使用 RNN 生成歌词 中计算 ,因此直接从 第四章 的方程 (9) 中,使用 RNN 生成歌词,我们可以写出以下内容:
现在,让我们看看如何计算损失相对于细胞状态的梯度,。
要计算损失相对于细胞状态的梯度,请查看前向传播的方程,并找出哪个方程中有 项。在隐藏状态的方程中,我们有如下的
项:
因此,根据链式法则,我们可以写出以下内容:
我们知道 tanh 的导数是 ,因此我们可以写出以下内容:
现在我们已经计算出损失相对于隐藏状态和细胞状态的梯度,让我们看看如何逐个计算损失相对于所有门的梯度。
首先,我们将看看如何计算损失相对于输出门的梯度,。
要计算损失相对于输出门的梯度,请查看前向传播的方程,并找出哪个方程中有 项。在隐藏状态的方程中,我们有如下的
项:
因此,根据链式法则,我们可以写出以下内容:
现在我们将看到如何计算对输入门的损失梯度,。
我们在细胞状态方程中有项用于
:
根据链式法则,我们可以写成以下形式:
现在我们学习如何计算对遗忘门的损失梯度,。
我们还在细胞状态方程中有项用于
:
根据链式法则,我们可以写成以下形式:
最后,我们学习如何计算对候选状态的损失梯度,。
我们还在细胞状态方程中有项用于
:
因此,根据链式法则,我们可以写成以下形式:
因此,我们已经计算出了损失对所有门和候选状态的梯度。在接下来的部分中,我们将看到如何计算损失对 LSTM 单元中所有权重的梯度。
对权重的梯度
现在让我们看看如何计算损失对 LSTM 单元中所有权重的梯度。
对 V 的梯度
在预测输出后,我们处于网络的最后一层。因为我们在进行反向传播,即从输出层到输入层,我们的第一个权重将是隐藏到输出层的权重,
。
我们一直学到最后的损失是所有时间步长的损失之和。类似地,我们最终的梯度是所有时间步骤的梯度之和,如下所示:
如果我们有层,那么我们可以将损失对
的梯度写成如下形式:
由于 LSTM 的最终方程式,即,与 RNN 相同,计算与
相关的损失梯度与我们在 RNN 中计算的完全相同。因此,我们可以直接写出以下内容:
与 W 相关的梯度
现在我们将看看如何计算隐藏到隐藏层权重对所有门和候选状态的损失梯度。
让我们计算与相关的损失梯度。
回想一下输入门的方程式,如下所示:
因此,根据链式法则,我们可以写出以下内容:
让我们计算前述方程中的每一项。
我们已经看到如何计算第一项,即损失关于输入门的梯度,,在门梯度部分。参考方程式(2)。
因此,让我们看看第二项:
由于我们知道 sigmoid 函数的导数,即,因此我们可以写出以下内容:
但是已经是 sigmoid 的结果,即
,因此我们可以直接写出
,因此,我们的方程式变为以下内容:
因此,我们计算损失关于梯度的最终方程变为以下内容:
现在,让我们找出与相关的损失梯度。
回想一下遗忘门的方程式,如下所示:
因此,根据链式法则,我们可以写出以下内容:
我们已经看到如何在门梯度部分计算。参考方程式(3)。因此,让我们看看计算第二项:
因此,我们计算梯度损失与相关的最终方程式如下:
让我们计算与相关的损失梯度。
回想一下输出门的方程式,如下所示:
因此,使用链式法则,我们可以写成以下形式:
检查方程式 (1) 的第一项。第二项可以计算如下:
因此,我们计算损失相对于 梯度的最终方程式如下:
让我们继续计算相对于 的梯度。
回想一下候选状态方程式:
因此,使用链式法则,我们可以写成以下形式:
参考方程 (4) 的第一项。第二项可以计算如下:
我们知道 tanh 的导数是 ,因此我们可以写成以下形式:
因此,我们计算损失相对于 的梯度的最终方程式如下:
关于 U 的梯度
让我们计算损失相对于隐藏到输入层权重 对所有门和候选状态的梯度。相对于
的损失梯度计算与我们相对于
计算的梯度完全相同,只是最后一项是
而不是
。让我们详细探讨一下这是什么意思。
让我们找出相对于 的梯度。
输入门的方程式如下:
因此,使用链式法则,我们可以写成以下形式:
让我们计算前述方程式中的每一项。我们已经从方程 (2) 知道了第一项。因此,第二项可以计算如下:
因此,我们计算损失相对于 梯度的最终方程式如下:
正如您所看到的,前述方程式与 完全相同,只是最后一项是
而不是
。对于所有其他权重也适用,因此我们可以直接写出方程式如下:
- 相对于
的梯度:
- 损失关于
的梯度:
- 损失关于
的梯度:
计算梯度后,针对所有这些权重,我们使用权重更新规则更新它们,并最小化损失。
使用 LSTM 模型预测比特币价格
我们已经了解到 LSTM 模型广泛用于序列数据集,即有序的数据集。在本节中,我们将学习如何使用 LSTM 网络进行时间序列分析。我们将学习如何使用 LSTM 网络预测比特币价格。
首先,我们按如下方式导入所需的库:
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
%matplotlib inline
plt.style.use('ggplot')
import tensorflow as tf
tf.logging.set_verbosity(tf.logging.ERROR)
import warnings
warnings.filterwarnings('ignore')
数据准备
现在,我们将看到如何准备我们的数据集,以便我们的 LSTM 网络需要。首先,我们按如下方式读取输入数据集:
df = pd.read_csv('data/btc.csv')
然后我们展示数据集的几行:
df.head()
上述代码生成如下输出:
如前述数据框所示,Close
列表示比特币的收盘价。我们只需要Close
列来进行预测,因此我们只取该特定列:
data = df['Close'].values
接下来,我们标准化数据并将其缩放到相同的尺度:
scaler = StandardScaler()
data = scaler.fit_transform(data.reshape(-1, 1))
然后,我们绘制并观察比特币价格变化的趋势。由于我们缩放了价格,它不是一个很大的数值:
plt.plot(data)
plt.xlabel('Days')
plt.ylabel('Price')
plt.grid()
生成如下图所示的绘图:
现在,我们定义一个称为get_data
函数的函数,它生成输入和输出。它以数据和window_size
作为输入,并生成输入和目标列。
这里的窗口大小是多少?我们将x值向前移动window_size
次,并得到y值。例如,如下表所示,当window_size
等于 1 时,y值恰好比x值提前一步:
x | y |
---|---|
0.13 | 0.56 |
0.56 | 0.11 |
0.11 | 0.40 |
0.40 | 0.63 |
函数get_data()
的定义如下:
def get_data(data, window_size):
X = []
y = []
i = 0
while (i + window_size) <= len(data) - 1:
X.append(data[i:i+window_size])
y.append(data[i+window_size])
i += 1
assert len(X) == len(y)
return X, y
我们选择window_size
为7
并生成输入和输出:
X, y = get_data(data, window_size = 7)
将前1000
个点视为训练集,其余点视为测试集:
#train set
X_train = np.array(X[:1000])
y_train = np.array(y[:1000])
#test set
X_test = np.array(X[1000:])
y_test = np.array(y[1000:])
X_train
的形状如下所示:
X_train.shape
(1000,7,1)
前面的形状表示什么?它意味着sample_size
、time_steps
和features
函数及 LSTM 网络需要的输入正是如下所示:
-
1000
设置数据点数目(sample_size
) -
7
指定窗口大小(time_steps
) -
1
指定我们数据集的维度(features
)
定义参数
定义网络参数如下:
batch_size = 7
window_size = 7
hidden_layer = 256
learning_rate = 0.001
为我们的输入和输出定义占位符:
input = tf.placeholder(tf.float32, [batch_size, window_size, 1])
target = tf.placeholder(tf.float32, [batch_size, 1])
现在,让我们定义我们在 LSTM 单元中将使用的所有权重。
输入门的权重定义如下:
U_i = tf.Variable(tf.truncated_normal([1, hidden_layer], stddev=0.05))
W_i = tf.Variable(tf.truncated_normal([hidden_layer, hidden_layer], stddev=0.05))
b_i = tf.Variable(tf.zeros([hidden_layer]))
忘记门的权重定义如下:
U_f = tf.Variable(tf.truncated_normal([1, hidden_layer], stddev=0.05))
W_f = tf.Variable(tf.truncated_normal([hidden_layer, hidden_layer], stddev=0.05))
b_f = tf.Variable(tf.zeros([hidden_layer]))
输出门的权重定义如下:
U_o = tf.Variable(tf.truncated_normal([1, hidden_layer], stddev=0.05))
W_o = tf.Variable(tf.truncated_normal([hidden_layer, hidden_layer], stddev=0.05))
b_o = tf.Variable(tf.zeros([hidden_layer]))
候选状态的权重定义如下:
U_g = tf.Variable(tf.truncated_normal([1, hidden_layer], stddev=0.05))
W_g = tf.Variable(tf.truncated_normal([hidden_layer, hidden_layer], stddev=0.05))
b_g = tf.Variable(tf.zeros([hidden_layer]))
输出层的权重如下所示:
V = tf.Variable(tf.truncated_normal([hidden_layer, 1], stddev=0.05))
b_v = tf.Variable(tf.zeros([1]))
定义 LSTM 单元
现在,我们定义名为LSTM_cell
的函数,它将细胞状态和隐藏状态作为输出返回。回顾我们在 LSTM 前向传播中看到的步骤,它的实现如下所示。LSTM_cell
接受输入、先前隐藏状态和先前细胞状态作为输入,并返回当前细胞状态和当前隐藏状态作为输出:
def LSTM_cell(input, prev_hidden_state, prev_cell_state):
it = tf.sigmoid(tf.matmul(input, U_i) + tf.matmul(prev_hidden_state, W_i) + b_i)
ft = tf.sigmoid(tf.matmul(input, U_f) + tf.matmul(prev_hidden_state, W_f) + b_f)
ot = tf.sigmoid(tf.matmul(input, U_o) + tf.matmul(prev_hidden_state, W_o) + b_o)
gt = tf.tanh(tf.matmul(input, U_g) + tf.matmul(prev_hidden_state, W_g) + b_g)
ct = (prev_cell_state * ft) + (it * gt)
ht = ot * tf.tanh(ct)
return ct, ht
定义前向传播
现在我们将执行前向传播并预测输出,,并初始化一个名为
y_hat
的列表以存储输出:
y_hat = []
对于每次迭代,我们计算输出并将其存储在y_hat
列表中:
for i in range(batch_size):
我们初始化隐藏状态和细胞状态:
hidden_state = np.zeros([1, hidden_layer], dtype=np.float32)
cell_state = np.zeros([1, hidden_layer], dtype=np.float32)
我们执行前向传播,并计算每个时间步长的 LSTM 单元的隐藏状态和细胞状态:
for t in range(window_size):
cell_state, hidden_state = LSTM_cell(tf.reshape(input[i][t], (-1, 1)), hidden_state, cell_state)
我们知道输出 可以计算如下:
计算y_hat
,并将其附加到y_hat
列表中:
y_hat.append(tf.matmul(hidden_state, V) + b_v)
定义反向传播
在执行前向传播并预测输出之后,我们计算损失。我们使用均方误差作为我们的损失函数,总损失是所有时间步长上损失的总和:
losses = []
for i in range(len(y_hat)):
losses.append(tf.losses.mean_squared_error(tf.reshape(target[i], (-1, 1)), y_hat[i]))
loss = tf.reduce_mean(losses)
为了避免梯度爆炸问题,我们执行梯度裁剪:
gradients = tf.gradients(loss, tf.trainable_variables())
clipped, _ = tf.clip_by_global_norm(gradients, 4.0)
我们使用 Adam 优化器并最小化我们的损失函数:
optimizer = tf.train.AdamOptimizer(learning_rate).apply_gradients(zip(gradients, tf.trainable_variables()))
训练 LSTM 模型
开始 TensorFlow 会话并初始化所有变量:
session = tf.Session()
session.run(tf.global_variables_initializer())
设置epochs
的数量:
epochs = 100
然后,对于每次迭代,执行以下操作:
for i in range(epochs):
train_predictions = []
index = 0
epoch_loss = []
然后对数据批次进行抽样并训练网络:
while(index + batch_size) <= len(X_train):
X_batch = X_train[index:index+batch_size]
y_batch = y_train[index:index+batch_size]
#predict the price and compute the loss
predicted, loss_val, _ = session.run([y_hat, loss, optimizer], feed_dict={input:X_batch, target:y_batch})
#store the loss in the epoch_loss list
epoch_loss.append(loss_val)
#store the predictions in the train_predictions list
train_predictions.append(predicted)
index += batch_size
在每10
次迭代上打印损失:
if (i % 10)== 0:
print 'Epoch {}, Loss: {} '.format(i,np.mean(epoch_loss))
正如您可以在以下输出中看到的,损失随着 epochs 的增加而减少:
Epoch 0, Loss: 0.0402321927249
Epoch 10, Loss: 0.0244581680745
Epoch 20, Loss: 0.0177710317075
Epoch 30, Loss: 0.0117778982967
Epoch 40, Loss: 0.00901956297457
Epoch 50, Loss: 0.0112476013601
Epoch 60, Loss: 0.00944950990379
Epoch 70, Loss: 0.00822851061821
Epoch 80, Loss: 0.00766260037199
Epoch 90, Loss: 0.00710930628702
使用 LSTM 模型进行预测
现在我们将开始对测试集进行预测:
predicted_output = []
i = 0
while i+batch_size <= len(X_test):
output = session.run([y_hat],feed_dict={input:X_test[i:i+batch_size]})
i += batch_size
predicted_output.append(output)
打印预测输出:
predicted_output[0]
我们将得到如下结果:
[[array([[-0.60426176]], dtype=float32),
array([[-0.60155034]], dtype=float32),
array([[-0.60079575]], dtype=float32),
array([[-0.599668]], dtype=float32),
array([[-0.5991149]], dtype=float32),
array([[-0.6008351]], dtype=float32),
array([[-0.5970466]], dtype=float32)]]
正如您所见,测试预测值的值是嵌套列表,因此我们将它们展开:
predicted_values_test = []
for i in range(len(predicted_output)):
for j in range(len(predicted_output[i][0])):
predicted_values_test.append(predicted_output[i][0][j])
现在,如果我们打印预测值,它们不再是嵌套列表:
predicted_values_test[0]
array([[-0.60426176]], dtype=float32)
由于我们将前1000
个点作为训练集,我们对大于1000
的时间步长进行预测:
predictions = []
for i in range(1280):
if i >= 1000:
predictions.append(predicted_values_test[i-1019])
else:
predictions.append(None)
我们绘制并查看预测值与实际值的匹配程度:
plt.figure(figsize=(16, 7))
plt.plot(data, label='Actual')
plt.plot(predictions, label='Predicted')
plt.legend()
plt.xlabel('Days')
plt.ylabel('Price')
plt.grid()
plt.show()
正如您在以下图中所见,实际值显示为红色,预测值显示为蓝色。由于我们对大于1000
时间步长进行预测,您可以看到在时间步骤1000之后,红色和蓝色线条彼此交错,这表明我们的模型正确预测了实际值:
门控循环单元
到目前为止,我们已经学习了 LSTM 单元如何使用不同的门,并解决了 RNN 的梯度消失问题。但是,正如您可能注意到的,由于存在许多门和状态,LSTM 单元具有太多的参数。
因此,在反向传播 LSTM 网络时,我们需要在每次迭代中更新大量参数。这增加了我们的训练时间。因此,我们引入了门控循环单元(GRU),它作为 LSTM 单元的简化版本。与 LSTM 单元不同,GRU 单元只有两个门和一个隐藏状态。
RNN 中使用 GRU 单元如下图所示:
理解 GRU 单元
如下图所示,GRU 单元只有两个门,称为更新门和重置门,以及一个隐藏状态:
让我们深入了解这些门是如何使用的,以及如何计算隐藏状态。
更新门
更新门有助于决定前一时间步的哪些信息可以传递到下一时间步
。它基本上是输入门和遗忘门的组合,我们在 LSTM 单元中学到的内容。与 LSTM 单元的门类似,更新门也由 sigmoid 函数调节。
在时间步骤,更新门
表达如下:
在这里,应用以下内容:
-
是更新门的输入到隐藏权重
-
是更新门的隐藏到隐藏权重
-
是更新门的偏置
以下图示显示了更新门。如您所见,输入与
相乘,并且先前的隐藏状态,
,0 和 1:
重置门
重置门帮助决定如何将新信息添加到内存中,即它可以忘记多少过去信息。在时间步骤,重置门
表达如下:
在这里,应用以下内容:
-
是重置门的输入到隐藏权重
-
是重置门的隐藏到隐藏权重
-
是重置门的偏置
重置门如下图所示:
更新隐藏状态
我们刚刚学习了更新和重置门的工作原理,但这些门如何帮助更新隐藏状态呢?也就是说,如何利用重置门和更新门向隐藏状态添加新信息,以及如何利用它们从隐藏状态中删除不需要的信息?
首先,我们将看到如何向隐藏状态添加新信息。
我们创建一个名为内容状态的新状态,,用于保存信息。我们知道重置门用于删除不需要的信息。因此,利用重置门,我们创建一个仅包含所需信息的内容状态,
。
在时间步骤 ,内容状态
表示如下:
下图显示了如何使用重置门创建内容状态:
现在我们将看到如何从隐藏状态中删除信息。
我们了解到更新门 帮助确定上一个时间步
中哪些信息可以传递到下一个时间步
。将
和
相乘,我们仅获取上一步骤中的相关信息。而不是使用新门,我们只是使用
的补集,即
,并将其与
相乘。
随后,隐藏状态更新如下:
一旦计算出隐藏状态,我们可以应用 softmax 函数并计算输出如下:
GRU 单元的前向传播
将所有这些内容结合起来,我们在前一节中学到,GRU 单元中完整的前向传播步骤可以表示如下:
-
更新门:
-
重置门:
-
内容状态:
-
隐藏状态:
-
输出:
GRU 单元中的反向传播
总损失,,是所有时间步骤上损失的总和,可以表示如下:
为了通过梯度下降最小化损失,我们找出 GRU 单元中所有权重的损失导数如下:
-
我们有三个输入到隐藏层权重,
,分别是更新门、重置门和内容状态的输入到隐藏层权重
-
我们有三个隐藏到隐藏层权重,
,分别是更新门、重置门和内容状态的隐藏到隐藏层权重
-
我们有一个隐藏到输出层权重,
通过梯度下降找到所有这些权重的最优值,并根据权重更新规则更新权重。
门的梯度
正如我们在讨论 LSTM 单元时看到的那样,计算所有权重的损失梯度需要考虑所有门和内容状态的梯度。因此,首先我们将看看如何计算它们。
在接下来的计算中,我们将使用损失相对于隐藏状态的梯度,,在多个地方为
,因此我们将看看如何计算它。计算损失相对于隐藏状态的梯度,
,与我们在 LSTM 单元中看到的完全相同,可以如下给出:
首先,让我们看看如何计算与内容状态相关的损失梯度,。
要计算与内容状态相关的损失梯度,请查看正向传播的方程,并找出哪个方程式有项。在隐藏状态方程式中,也就是方程 (8) 中,我们有
项,如下所示:
因此,根据链式法则,我们可以写成如下形式:
让我们看看如何计算重置门的损失梯度。
我们在内容状态方程中有项,并且可以表示如下:
因此,根据链式法则,我们可以写成如下形式:
最后,我们看到与更新门相关的损失梯度,。
在我们的隐藏状态方程 中,我们有一个项,
,该方程可以表示如下:
因此,根据链式法则,我们可以写出以下内容:
我们已经计算了对所有门和内容状态的损失梯度,现在我们将看看如何计算对我们的 GRU 单元中所有权重的损失梯度。
权重的梯度
现在,我们将看到如何计算 GRU 单元中使用的所有权重的梯度。
相对于 V 的梯度
由于 GRU 的最终方程,即 ,与 RNN 相同,计算损失相对于隐藏到输出层权重
的梯度与我们在 RNN 中计算的完全相同。因此,我们可以直接写出以下内容:
相对于 W 的梯度
现在,我们将看看如何计算对所有门和内容状态中使用的隐藏到隐藏层权重 的损失梯度。
让我们计算相对于 的损失梯度。
回想一下重置门方程,如下所示:
使用链式法则,我们可以写出以下内容:
让我们计算前述方程中的每个项。第一项, ,我们在方程 (11) 中已经计算过。第二项计算如下:
因此,我们计算 的损失梯度的最终方程如下:
现在,让我们继续找出相对于 的损失梯度。
回想一下更新门方程,如下所示:
使用链式法则,我们可以写出以下内容:
我们已经计算了方程 (12) 中的第一项。第二项计算如下:
因此,我们计算 的损失梯度的最终方程如下:
现在,我们将找出相对于 的损失梯度。
回想一下内容状态方程:
使用链式法则,我们可以写出如下内容:
参考方程 (10) 的第一项。第二项如下所示:
因此,我们计算损失相对于 的梯度的最终方程如下:
相对于 U 的梯度
现在我们将看到如何计算损失相对于隐藏权重输入 的梯度,适用于所有门和内容状态。相对于
的梯度与相对于
的计算完全相同,除了最后一项将是
而不是
,这与我们在学习 LSTM 单元时学到的类似。
我们可以将损失相对于 的梯度写成:
损失相对于 的梯度表示如下:
损失相对于 的梯度表示如下:
在 TensorFlow 中实现 GRU 单元
现在,我们将看到如何在 TensorFlow 中实现 GRU 单元。而不是查看代码,我们只会看到如何在 TensorFlow 中实现 GRU 的前向传播。
定义权重
首先,让我们定义所有权重。更新门的权重定义如下:
Uz = tf.get_variable("Uz", [vocab_size, hidden_size], initializer=init)
Wz = tf.get_variable("Wz", [hidden_size, hidden_size], initializer=init)
bz = tf.get_variable("bz", [hidden_size], initializer=init)
重置门的权重如下定义:
Ur = tf.get_variable("Ur", [vocab_size, hidden_size], initializer=init)
Wr = tf.get_variable("Wr", [hidden_size, hidden_size], initializer=init)
br = tf.get_variable("br", [hidden_size], initializer=init)
内容状态的权重定义如下:
Uc = tf.get_variable("Uc", [vocab_size, hidden_size], initializer=init)
Wc = tf.get_variable("Wc", [hidden_size, hidden_size], initializer=init)
bc = tf.get_variable("bc", [hidden_size], initializer=init)
输出层的权重定义如下:
V = tf.get_variable("V", [hidden_size, vocab_size], initializer=init)
by = tf.get_variable("by", [vocab_size], initializer=init)
定义前向传播
将更新门定义为方程 (5) 中所给定的:
zt = tf.sigmoid(tf.matmul(x_t, Uz) + tf.matmul(h_t, Wz) + bz)
将重置门定义为方程 (6) 中所给定的:
rt = tf.sigmoid(tf.matmul(x_t, Ur) + tf.matmul(h_t, Wr) + br)
将内容状态定义为方程 (7) 中所给定的:
ct = tf.tanh(tf.matmul(x_t, Uc) + tf.matmul(tf.multiply(rt, h_t), Wc) + bc)
将隐藏状态定义为方程 (8) 中所给定的:
h_t = tf.multiply((1 - zt), ct) + tf.multiply(zt, h_t)
根据方程 (9) 计算输出:
y_hat_t = tf.matmul(h_t, V) + by
双向 RNN
在双向 RNN 中,我们有两个不同的隐藏单元层。这两层从输入层到输出层连接。在一层中,隐藏状态从左到右共享,在另一层中,它们从右到左共享。
但这意味着什么?简单地说,一个隐藏层从序列的起始点向前移动通过时间,而另一个隐藏层从序列的末尾向后移动通过时间。
如下图所示,我们有两个隐藏层:前向隐藏层和后向隐藏层,具体描述如下:
-
在前向隐藏层中,隐藏状态值是从过去的时间步共享的,即,
被共享到
,
被共享到
,依此类推。
-
在后向隐藏层中,隐藏起始值是从未来的时间步共享的,即,
到
,
到
,依此类推。
前向隐藏层和后向隐藏层如下图所示:
双向循环神经网络的用途是什么?在某些情况下,从两个方向读取输入序列非常有用。因此,双向循环神经网络包括两个 RNN,一个从前向读取句子,另一个从后向读取句子。
例如,考虑以下句子:
阿奇在 _____ 待了 13 年。所以他擅长说中文。
如果我们使用 RNN 来预测上述句子中的空白,这将是模棱两可的。正如我们所知,RNN 只能根据它迄今为止看到的一组词来进行预测。在上述句子中,要预测空白,RNN 只看到阿奇、待了、13、年、在,但这些词汇单独并不提供足够的上下文,也不能清楚地预测出正确的单词。它只是说阿奇在待了 13 年在.仅仅凭这些信息,我们无法正确预测接下来的单词。
但是,如果我们也阅读空白后面的单词,如所以,他,是,擅长,说,中文和中国,那么我们可以说阿奇在中国待了 13 年,因为他擅长说中文。因此,在这种情况下,如果我们使用双向循环神经网络来预测空白,它将能够正确预测,因为它在做出预测之前会同时从前向和后向读取句子。
双向循环神经网络已经在各种应用中使用,例如词性标注(POS),在这种情况下,知道目标词前后的词汇是至关重要的,语言翻译,预测蛋白质结构,依赖句法分析等。然而,双向循环神经网络在不知道未来信息的在线设置中不适用。
双向循环神经网络的前向传播步骤如下所示:
- 前向隐藏层:
- 后向隐藏层:
- 输出:
使用 TensorFlow 实现双向递归神经网络很简单。假设我们在双向递归神经网络中使用 LSTM 单元,我们可以按以下步骤操作:
- 从 TensorFlow 的
contrib
中导入rnn
,如下所示:
from tensorflow.contrib import rnn
- 定义前向和后向隐藏层:
forward_hidden_layer = rnn.BasicLSTMCell(num_hidden, forget_bias=1.0)
backward_hidden_layer = rnn.BasicLSTMCell(num_hidden, forget_bias=1.0)
- 使用
rnn.static_bidirectional_rnn
来定义双向递归神经网络:
outputs, forward_states, backward_states = rnn.static_bidirectional_rnn(forward_hidden_layer, backward_hidden_layer, input)
深入理解深度递归神经网络(deep RNN)
我们知道,深度神经网络是具有多个隐藏层的网络。类似地,深度递归神经网络具有多个隐藏层,但当我们有多个隐藏层时,隐藏状态如何计算呢?我们知道,递归神经网络通过接收输入和先前的隐藏状态来计算隐藏状态,但当我们有多个隐藏层时,后续层的隐藏状态如何计算呢?
例如,让我们看看隐藏层 2 中的 如何计算。它接收前一个隐藏状态
和前一层的输出
作为输入来计算
。
因此,当我们有多个隐藏层的递归神经网络时,后续层的隐藏层将通过接收前一个隐藏状态和前一层的输出作为输入来计算,如下图所示:
使用 seq2seq 模型进行语言翻译
序列到序列模型(seq2seq)基本上是 RNN 的一对多架构。它已被用于各种应用,因为它能够将任意长度的输入序列映射到任意长度的输出序列。seq2seq 模型的一些应用包括语言翻译、音乐生成、语音生成和聊天机器人。
在大多数实际场景中,输入和输出序列的长度是变化的。例如,让我们考虑语言翻译任务,在这个任务中,我们需要将一种语言的句子转换为另一种语言。假设我们将英语(源语言)转换为法语(目标语言)。
假设我们的输入句子是what are you doing?,那么它将被映射为que faites vous? 如我们所见,输入序列由四个单词组成,而输出序列由三个单词组成。seq2seq 模型可以处理这种不同长度的输入和输出序列,并将源序列映射到目标序列。因此,在输入和输出序列长度变化的应用中广泛使用它们。
seq2seq 模型的架构非常简单。它包括两个关键组件,即编码器和解码器。让我们考虑同样的语言翻译任务。首先,我们将输入句子馈送给编码器。
编码器学习输入句子的表示(嵌入),但是什么是表示?表示或嵌入基本上是包含句子意义的向量。它也被称为思想向量或上下文向量。一旦编码器学习了嵌入,它将嵌入发送到解码器。解码器将这个嵌入(思想向量)作为输入,并试图构造目标句子。因此,解码器尝试为英语句子生成法语翻译。
如下图所示,编码器接收输入的英语句子,学习嵌入,并将嵌入馈送给解码器,然后解码器使用这些嵌入生成翻译后的法语句子:
但这是如何真正工作的呢?编码器如何理解句子?解码器如何使用编码器的嵌入翻译句子?让我们深入探讨一下,看看这是如何运作的。
编码器
编码器基本上是带有 LSTM 或 GRU 单元的 RNN。它也可以是双向 RNN。我们将输入句子馈送给编码器,而不是获取输出,而是从最后一个时间步获取隐藏状态作为嵌入。让我们通过一个示例更好地理解编码器。
考虑我们使用的是带有 GRU 单元的 RNN,输入句子是what are you doing. 让我们用e表示编码器的隐藏状态:
前面的图示展示了编码器如何计算思想向量;下文将详细解释:
- 在第一个时间步中,我们传递输入,
给一个 GRU 单元,这是输入句子中的第一个词what,以及初始隐藏状态,
,它是随机初始化的。使用这些输入,GRU 单元计算第一个隐藏状态,
,如下所示:
- 在下一个时间步中,我们传递输入,
,这是输入句子中的下一个词are,给编码器。除此之外,我们还传递上一个隐藏状态,
,并计算隐藏状态,
:
- 在下一个时间步中,我们传递输入,
,这是下一个词you,给编码器。除此之外,我们还传递上一个隐藏状态,
,并计算隐藏状态,
如下所示:
- 在最后一个时间步
中,我们输入 doing. 作为输入单词。同时传递前一个隐藏状态
,计算隐藏状态
:
因此, 是我们的最终隐藏状态。我们了解到 RNN 在其隐藏状态中捕捉到目前为止看到的所有单词的上下文。由于
是最终隐藏状态,它包含了网络看到的所有单词的上下文,即我们输入句子中的所有单词,即 what, are, you 和 doing.
由于最终隐藏状态 包含了输入句子中所有单词的上下文,因此它包含了输入句子的上下文,并且这实质上形成了我们的嵌入
,也称为思考向量或上下文向量,如下所示:
我们将上下文向量 传递给解码器,以将其转换为目标句子。
因此,在编码器中,每个时间步 ,我们输入一个单词,并与之前的隐藏状态
一起计算当前的隐藏状态
。最终步骤中的隐藏状态
包含了输入句子的上下文,并将成为嵌入
,该嵌入将发送到解码器以将其转换为目标句子。
解码器
现在,我们将学习如何使用编码器生成的思考向量 来生成目标句子。解码器是一个带有 LSTM 或 GRU 单元的 RNN。我们的解码器的目标是为给定的输入(源)句子生成目标句子。
我们知道,我们通过使用随机值初始化 RNN 的初始隐藏状态来启动它,但对于解码器的 RNN,我们初始化隐藏状态的方式是使用由编码器生成的思考向量,,而不是使用随机值。解码器网络如下图所示:
但是,解码器的输入应该是什么?我们简单地将表示解码器的隐藏状态。
在第一个时间步骤,,我们将第一个输入
好的。我们到底在做什么?我们需要预测输出序列,即我们输入的英语句子的法语等价物。我们的词汇表中有很多法语单词。解码器如何决定输出哪个单词?也就是说,它如何确定输出序列的第一个单词?
我们将解码器隐藏状态馈送到
,它返回所有词汇表中的分数,作为第一个输出词。也就是说,在时间步骤
的输出词计算如下:
我们不是直接使用原始分数,而是将它们转换为概率。由于我们了解到 softmax 函数将值压缩到 0 到 1 之间,我们使用 softmax 函数将分数转换为概率
:
因此,我们得到了所有法语词汇中第一个输出词的概率。我们使用 argmax 函数选择具有最高概率的词作为第一个输出词:
因此,我们预测第一个输出词为Que,如前图所示。
在下一个时间步骤,我们将前一个时间步骤预测的输出词
作为解码器的输入。同时,我们还传递上一个隐藏状态
:
接着,我们计算所有词汇表中的分数,作为下一个输出词,即时间步骤的输出词:
然后,我们使用 softmax 函数将分数转换为概率:
接下来,我们选择具有最高概率的单词作为输出词,,在时间步骤
:
因此,我们使用 初始化解码器的初始隐藏状态,并且在每个时间步
中,我们将来自上一个时间步的预测输出词
和先前的隐藏状态
作为解码器当前时间步骤的输入
,并预测当前输出
。
但是解码器何时停止?因为我们的输出序列必须在某处停止,我们不能不断将前一个时间步的预测输出词作为下一个时间步的输入。当解码器预测输出词为
因此,这就是 seq2seq 模型如何将源句子转换为目标句子。
注意力就是我们所需的一切
我们刚刚学习了 seq2seq 模型的工作原理以及它如何将源语言的句子翻译成目标语言的句子。我们了解到上下文向量基本上是来自编码器最终时间步的隐藏状态向量,它捕捉了输入句子的含义,并由解码器用于生成目标句子。
但是当输入句子很长时,上下文向量不能捕捉整个句子的含义,因为它只是来自最终时间步的隐藏状态。因此,我们不再将最后一个隐藏状态作为上下文向量并用于解码器,而是取编码器所有隐藏状态的总和作为上下文向量。
假设输入句子有 10 个单词;那么我们将有 10 个隐藏状态。我们将所有这些 10 个隐藏状态求和,并将其用于解码器生成目标句子。然而,并非所有这些隐藏状态在生成时间步骤 时都可能有帮助。有些隐藏状态比其他隐藏状态更有用。因此,我们需要知道在时间步骤
时哪个隐藏状态比另一个更重要来预测目标词。为了获得这种重要性,我们使用注意力机制,它告诉我们在时间步骤
时哪个隐藏状态更重要以生成目标词。因此,注意力机制基本上为编码器的每个隐藏状态在时间步骤
生成目标词提供重要性。
注意力机制如何工作?假设我们有编码器的三个隐藏状态 、
和
,以及解码器的隐藏状态
,如下图所示:
现在,我们需要了解编码器所有隐藏状态在时间步 生成目标词的重要性。因此,我们取每个编码器隐藏状态
和解码器隐藏状态
,并将它们输入到一个称为分数函数或对齐函数的函数
中,它返回每个编码器隐藏状态的分数,指示它们的重要性。但这个分数函数是什么?分数函数有多种选择,如点积、缩放点积、余弦相似度等。
我们使用简单的点积作为分数函数;即编码器隐藏状态和解码器隐藏状态之间的点积。例如,要了解生成目标词 的重要性,我们简单地计算
和
之间的点积,这给我们一个指示
和
相似程度的分数。
一旦我们得到分数,我们将它们使用 softmax 函数转换为概率,如下所示:
这些概率 被称为注意力权重。
如下图所示,我们计算每个编码器隐藏状态与解码器隐藏状态之间的相似性分数,使用一个函数 。然后,使用 softmax 函数将相似性分数转换为概率,称为注意力权重:
因此,我们得到每个编码器隐藏状态的注意力权重(概率)。现在,我们将注意力权重乘以它们对应的编码器隐藏状态,即 。如下图所示,编码器的隐藏状态
乘以 0.106,
乘以 0.106,
乘以 0.786:
但是,为什么我们要将注意力权重乘以编码器的隐藏状态?
将编码器的隐藏状态乘以它们的注意力权重表示我们正在赋予那些具有更多注意力权重的隐藏状态更重要的重视,而对具有较少注意力权重的隐藏状态则不那么重视。如前图所示,将0.786乘以隐藏状态意味着我们比其他两个隐藏状态更重视
。
因此,这就是注意机制如何决定哪个隐藏状态在时间步骤生成目标词。在将编码器的隐藏状态乘以它们的注意力权重后,我们简单地将它们相加,这现在形成我们的上下文/思想向量:
如下图所示,上下文向量是通过将编码器的隐藏状态乘以其相应的注意力权重后得到的总和:
因此,为了在时间步骤生成目标词,解码器使用时间步骤
的上下文向量
。通过注意机制,我们不再将最后一个隐藏状态作为上下文向量并用于解码器,而是取所有编码器隐藏状态的总和作为上下文向量。
总结
在本章中,我们学习了 LSTM 单元如何使用多个门来解决梯度消失问题。然后,我们看到如何在 TensorFlow 中使用 LSTM 单元来预测比特币的价格。
在查看了 LSTM 单元之后,我们了解了 GRU 单元,它是 LSTM 的简化版本。我们还学习了双向 RNN,其中我们有两层隐藏状态,一层从序列的起始时间向前移动,另一层从序列的末尾时间向后移动。
在本章末尾,我们了解了 seq2seq 模型,它将一个长度不同的输入序列映射到一个长度不同的输出序列。我们还了解了注意机制如何在 seq2seq 模型中使用,以及它如何集中关注重要信息。
在下一章中,我们将学习卷积神经网络及其在识别图像中的应用。
问题
让我们将新学到的知识付诸实践。回答以下问题:
-
LSTM 如何解决 RNN 的梯度消失问题?
-
LSTM 单元中所有不同门及其功能是什么?
-
细胞状态的用途是什么?
-
GRU 是什么?
-
双向 RNN 如何工作?
-
深层 RNN 如何计算隐藏状态?
-
在 seq2seq 架构中,编码器和解码器是什么?
-
注意机制有什么用途?
进一步阅读
在 GitHub 上查看一些很酷的项目:
-
使用 LSTM 进行人体活动识别:
github.com/guillaume-chevalier/LSTM-Human-Activity-Recognition
-
使用 seq2seq 构建聊天机器人:
github.com/tensorlayer/seq2seq-chatbot
-
使用双向 GRU 进行文本摘要:
github.com/harpribot/deep-summarization
第六章:揭秘卷积网络
卷积神经网络 (CNNs) 是最常用的深度学习算法之一。它们广泛应用于与图像相关的任务,如图像识别、物体检测、图像分割等。CNNs 的应用无所不在,从自动驾驶汽车中的视觉处理到我们在 Facebook 照片中自动标记朋友。尽管 CNNs 广泛用于图像数据集,但它们也可以应用于文本数据集。
在本章中,我们将详细了解 CNNs,并掌握 CNNs 及其工作原理。首先,我们将直观地了解 CNNs,然后深入探讨其背后的数学原理。随后,我们将学习如何逐步在 TensorFlow 中实现 CNN。接下来,我们将探索不同类型的 CNN 架构,如 LeNet、AlexNet、VGGNet 和 GoogleNet。在本章末尾,我们将研究 CNNs 的不足之处,并学习如何使用胶囊网络解决这些问题。此外,我们还将学习如何使用 TensorFlow 构建胶囊网络。
在本章中,我们将讨论以下主题:
-
什么是 CNNs?
-
CNNs 的数学原理
-
在 TensorFlow 中实现 CNNs
-
不同的 CNN 架构
-
胶囊网络
-
在 TensorFlow 中构建胶囊网络
什么是 CNNs?
CNN,也称为ConvNet,是用于计算机视觉任务的最常用的深度学习算法之一。假设我们正在执行一个图像识别任务。考虑以下图像。我们希望我们的 CNN 能够识别其中包含一匹马。
如何做到这一点?当我们将图像输入计算机时,它基本上会将其转换为一个像素值矩阵。像素值的范围从 0 到 255,而此矩阵的尺寸将是 [图像宽度 x 图像高度 x 通道数]。灰度图像有一个通道,彩色图像有三个通道 红色、绿色和蓝色 (RGB)。
假设我们有一个宽度为 11、高度为 11 的彩色输入图像,即 11 x 11,那么我们的矩阵维度将是 [11 x 11 x 3]。如你所见,[11 x 11 x 3] 中,11 x 11 表示图像的宽度和高度,3 表示通道数,因为我们有一张彩色图像。因此,我们将得到一个 3D 矩阵。
但是,对于理解来说,很难将 3D 矩阵可视化,因此让我们以灰度图像作为输入来考虑。由于灰度图像只有一个通道,我们将得到一个 2D 矩阵。
如下图所示,输入的灰度图像将被转换为一个像素值矩阵,其像素值介于 0 到 255 之间,像素值表示该点的像素强度:
输入矩阵中给出的值仅仅是为了帮助我们理解而随意给出的。
现在,我们有一个像素值矩阵的输入矩阵。接下来会发生什么?CNN 由以下三个重要的层组成:
-
卷积层
-
池化层
-
全连接层
通过这三个层的帮助,CNN 可以识别出图像中包含一匹马。现在我们将详细探讨每一个这些层。
卷积层
卷积层是 CNN 的第一个核心层,也是 CNN 的构建块之一,用于从图像中提取重要的特征。
我们有一幅马的图像。您认为有哪些特征将帮助我们理解这是一幅马的图像?我们可以说是身体结构、面部、腿部、尾巴等。但是 CNN 如何理解这些特征?这就是我们使用卷积操作的地方,它将从图像中提取出表征马的所有重要特征。因此,卷积操作帮助我们理解图像的内容。
好的,这个卷积操作到底是什么?它是如何执行的?它如何提取重要的特征?让我们详细看一下。
我们知道,每个输入图像都由像素值矩阵表示。除了输入矩阵外,我们还有另一个称为滤波器矩阵的矩阵。滤波器矩阵也被称为核或简称滤波器,如下图所示:
我们取滤波器矩阵,将其沿着输入矩阵向右滑动一个像素,执行逐元素乘法,将结果求和,生成一个单一的数字。这相当令人困惑,不是吗?让我们通过以下图示来更好地理解:
正如您在前面的图示中所看到的,我们取了滤波器矩阵,将其放在输入矩阵的顶部,执行逐元素乘法,将它们的结果相加,并生成单一数字。示例如下:
现在,我们将滤波器沿着输入矩阵向右滑动一个像素,并执行相同的步骤,如下图所示:
示例如下:
再次,我们将滤波器矩阵向右滑动一个像素,并执行相同的操作,如下图所示:
示例如下:
现在,再次将滤波器矩阵沿着输入矩阵向右滑动一个像素,并执行相同的操作,如下图所示:
就是这样:
好的。我们在这里做什么?我们基本上是将滤波器矩阵按一个像素滑动到整个输入矩阵上,执行逐元素乘法并求和它们的结果,从而创建一个称为特征映射或激活映射的新矩阵。这就是卷积操作。
正如我们所学到的,卷积操作用于提取特征,并且新矩阵,即特征映射,代表了通过卷积操作提取的特征。如果我们绘制特征映射,那么我们可以看到通过卷积操作提取的特征。
下图显示了实际图像(输入图像)和卷积图像(特征映射)。我们可以看到我们的滤波器已将实际图像中的边缘检测为特征:
不同的滤波器用于从图像中提取不同特征。例如,如果我们使用一个锐化滤波器,,那么它将使我们的图像变得更锐利,如下图所示:
因此,我们学到了使用滤波器可以通过卷积操作从图像中提取重要特征。所以,我们可以不只使用一个滤波器,而是使用多个滤波器从图像中提取不同特征,并生成多个特征映射。因此,特征映射的深度将等于滤波器的数量。如果我们使用七个滤波器从图像中提取不同特征,那么我们特征映射的深度将为七:
好的,我们已经学到了不同的滤波器从图像中提取不同的特征。但问题是,我们如何设置滤波器矩阵的正确值,以便从图像中提取重要特征?别担心!我们只需随机初始化滤波器矩阵,通过反向传播学习可以从图像中提取重要特征的优化滤波器矩阵的最佳值。但是,我们只需要指定滤波器的大小和要使用的滤波器数量。
步幅
我们刚刚学习了卷积操作的工作原理。我们通过滑动滤波器矩阵一个像素来扫描输入矩阵并执行卷积操作。但我们不仅可以仅滑动一个像素扫描输入矩阵,也可以按任意数量的像素滑动。
我们通过滤波器矩阵在输入矩阵上滑动的像素数称为步幅。
如果我们将步幅设置为 2,那么我们将以两个像素的步幅滑动输入矩阵与滤波器矩阵。下图显示了步幅为 2 的卷积操作:
但是我们如何选择步幅数字呢?我们刚学到步幅是我们移动滤波器矩阵的像素数。因此,当步幅设置为一个小数字时,我们可以编码比步幅设置为大数字更详细的图像表示。然而,具有高值步幅的步幅计算时间少于具有低值步幅的步幅。
填充
在卷积操作中,我们通过滤波器矩阵滑动输入矩阵。但在某些情况下,滤波器并不完全适合输入矩阵。什么意思?例如,假设我们使用步幅为 2 进行卷积操作。存在这样一种情况,即当我们将我们的滤波器矩阵移动两个像素时,它达到边界,滤波器矩阵不适合输入矩阵。也就是说,我们的滤波器矩阵的某部分位于输入矩阵之外,如下图所示:
在这种情况下,我们进行填充。我们可以简单地用零填充输入矩阵,以便滤波器可以适合输入矩阵,如下图所示。在输入矩阵上用零填充被称为相同填充或零填充:
与其用零填充它们,我们也可以简单地丢弃输入矩阵中滤波器不适合的区域。这称为有效填充:
池化层
好的。现在,我们完成了卷积操作。作为卷积操作的结果,我们得到了一些特征映射。但是特征映射的维度太大了。为了减少特征映射的维度,我们进行池化操作。这样可以减少特征映射的维度,并保留必要的细节,从而减少计算量。
例如,要从图像中识别出一匹马,我们需要提取并保留马的特征;我们可以简单地丢弃不需要的特征,比如图像的背景等。池化操作也称为下采样或子采样操作,使得卷积神经网络具有平移不变性。因此,池化层通过保留重要的特征来减少空间维度。
池化操作不会改变特征映射的深度;它只会影响高度和宽度。
池化操作有不同的类型,包括最大池化、平均池化和求和池化。
在最大池化中,我们在输入矩阵上滑动滤波器,并从滤波器窗口中简单地取最大值,如下图所示:
正如其名,平均池化中,我们取滤波器窗口内输入矩阵的平均值,在求和池化中,我们对滤波器窗口内的输入矩阵的所有值求和,如下图所示:
最大池化是最常用的池化操作之一。
全连接层
到目前为止,我们已经学习了卷积和池化层的工作原理。CNN 可以拥有多个卷积层和池化层。然而,这些层只会从输入图像中提取特征并生成特征映射;也就是说,它们只是特征提取器。
针对任何图像,卷积层会从图像中提取特征并生成特征映射。现在,我们需要对这些提取出的特征进行分类。因此,我们需要一种算法来对这些提取出的特征进行分类,并告诉我们这些提取出的特征是否是马的特征,或者其他什么东西的特征。为了进行这种分类,我们使用一个前向神经网络。我们将特征映射展平并将其转换为向量,并将其作为输入馈送到前向网络中。前向网络将这个展平的特征映射作为输入,应用激活函数(如 sigmoid),并返回输出,说明图像是否包含马;这称为全连接层,如下图所示:
CNN 的架构
CNN 的架构如下图所示:
正如你所注意到的,首先我们将输入图像馈送到卷积层,其中我们对图像应用卷积操作以从图像中提取重要特征并创建特征映射。然后,我们将特征映射传递给池化层,其中特征映射的维度将被减少。正如前面的图所示,我们可以有多个卷积和池化层,并且还应注意到池化层并不一定需要在每个卷积层之后;可以有多个卷积层后跟一个池化层。
因此,在卷积和池化层之后,我们将展平产生的特征映射,并将其馈送到全连接层,这基本上是一个前向神经网络,根据特征映射对给定的输入图像进行分类。
CNN 的数学背后
到目前为止,我们已经直观地理解了 CNN 的工作原理。但是 CNN 到底是如何学习的呢?它如何使用反向传播找到滤波器的最优值?为了回答这个问题,我们将从数学角度探讨 CNN 的工作原理。与《第五章》中的循环神经网络改进不同,CNN 的数学背后非常简单而且非常有趣。
前向传播
让我们从前向传播开始。我们已经看到了前向传播的工作原理以及 CNN 如何对给定的输入图像进行分类。让我们从数学角度来描述这个过程。让我们考虑一个输入矩阵X和滤波器W,其值如下所示:
首先,让我们熟悉符号。每当我们写,这意味着输入矩阵中第
行和第
列的元素。滤波器和输出矩阵同理,即
和
分别表示滤波器和输出矩阵中第
行和第
列的值。在前一图中,
=
,即
是输入矩阵中第一行第一列的元素。
如下图所示,我们取滤波器,在输入矩阵上滑动,执行卷积操作,并生成输出矩阵(特征图),就像我们在前一节中学到的那样:
因此,输出矩阵(特征图)中的所有值计算如下:
好的,现在我们知道了卷积操作的执行方式以及如何计算输出。我们可以用一个简单的方程表示这个过程吗?假设我们有一个输入图像X,宽度为W,高度为H,滤波器大小为P x Q,那么卷积操作可以表示如下:
此方程基本表示了如何使用卷积操作计算输出,(即输出矩阵中第
行和第
列的元素)。
卷积操作完成后,我们将结果,,馈送给前馈网络,
,并预测输出,
:
反向传播
预测输出后,我们计算损失,。我们使用均方误差作为损失函数,即实际输出,
,与预测输出,
,之间差值的平均值,如下所示:
现在,我们将看看如何使用反向传播来最小化损失 。为了最小化损失,我们需要找到我们的滤波器 W 的最优值。我们的滤波器矩阵包括四个值,w1、w2、w3 和 w4。为了找到最优的滤波器矩阵,我们需要计算损失函数相对于这四个值的梯度。我们该如何做呢?
首先,让我们回顾输出矩阵的方程式,如下所示:
不要被即将出现的方程式吓到;它们实际上非常简单。
首先,让我们计算关于 的梯度。正如您所见,
出现在所有输出方程中;我们按如下方式计算损失关于
的偏导数:
类似地,我们计算损失关于 权重的偏导数如下所示:
关于 权重的损失梯度计算如下:
关于 权重的损失梯度如下所示:
因此,总结一下,我们关于所有权重的损失梯度的最终方程式如下所示:
结果发现,计算损失关于滤波器矩阵的导数非常简单——它只是另一个卷积操作。如果我们仔细观察前述方程式,我们会注意到它们看起来像输入矩阵与损失关于输出的梯度作为滤波器矩阵的卷积操作的结果,如下图所示:
例如,让我们看看损失关于权重 的梯度如何通过输入矩阵与损失关于输出的梯度作为滤波器矩阵的卷积操作计算,如下图所示:
因此,我们可以写成以下形式:
因此,我们了解到,计算损失对滤波器(即权重)的梯度,其实就是输入矩阵和损失对输出的梯度作为滤波器矩阵之间的卷积操作。
除了计算对滤波器的损失梯度之外,我们还需要计算对某个输入的损失梯度。但是为什么要这样做?因为它用于计算上一层中滤波器的梯度。
我们的输入矩阵包括从 到
的九个值,因此我们需要计算对这九个值的损失梯度。让我们回顾一下输出矩阵是如何计算的:
如您所见, 仅出现在
,因此我们可以单独计算对
的损失梯度,其他项为零:
现在,让我们计算对; 的梯度;因为
仅出现在
和
中,我们仅计算对
和
的梯度:
以非常类似的方式,我们计算对所有输入的损失梯度如下:
就像我们用卷积操作表示损失对权重的梯度一样,我们能在这里做同样的事情吗?答案是肯定的。实际上,我们可以用卷积操作来表示前述方程,即损失对输入的梯度,其中输入矩阵作为一个滤波器矩阵,损失对输出矩阵的梯度作为一个滤波器矩阵。但诀窍在于,我们不直接使用滤波器矩阵,而是将它们旋转 180 度,并且不进行卷积,而是执行完全卷积。我们这样做是为了能够用卷积操作推导出前述方程。
下图展示了旋转 180 度的核心滤波器的样子:
好的,那么什么是完全卷积?与卷积操作类似,完全卷积中,我们使用一个滤波器并将其滑动到输入矩阵上,但我们滑动滤波器的方式与我们之前看到的卷积操作不同。下图展示了完全卷积操作的工作方式。正如我们所见,阴影矩阵表示滤波器矩阵,未阴影的矩阵表示输入矩阵;我们可以看到滤波器如何逐步滑动到输入矩阵上,如图所示:
所以,我们可以说,损失对输入矩阵的梯度可以通过滤波器矩阵旋转 180 度作为输入矩阵,并且损失对输出的梯度作为滤波器矩阵来计算,使用完全卷积操作来计算:
例如,如下图所示,我们将注意到损失对输入的梯度,,是通过滤波器矩阵旋转 180 度和损失对输出的梯度作为滤波器矩阵之间的完全卷积操作来计算的:
这里展示如下:
因此,我们知道计算损失对输入的梯度就是完全卷积操作。因此,我们可以说 CNN 中的反向传播只是另一种卷积操作。
在 TensorFlow 中实现 CNN
现在我们将学习如何使用 TensorFlow 构建 CNN。我们将使用 MNIST 手写数字数据集,了解 CNN 如何识别手写数字,并且我们还将可视化卷积层如何从图像中提取重要特征。
首先,让我们加载所需的库:
import warnings
warnings.filterwarnings('ignore')
import numpy as np
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
tf.logging.set_verbosity(tf.logging.ERROR)
import matplotlib.pyplot as plt
%matplotlib inline
加载 MNIST 数据集:
mnist = input_data.read_data_sets('data/mnist', one_hot=True)
定义辅助函数
现在我们定义初始化权重和偏置的函数,以及执行卷积和池化操作的函数。
通过截断正态分布绘制来初始化权重。请记住,这些权重实际上是我们在执行卷积操作时使用的滤波器矩阵:
def initialize_weights(shape):
return tf.Variable(tf.truncated_normal(shape, stddev=0.1))
使用常量值(例如0.1
)初始化偏置:
def initialize_bias(shape):
return tf.Variable(tf.constant(0.1, shape=shape))
我们定义一个名为convolution
的函数,使用tf.nn.conv2d()
执行卷积操作;即输入矩阵(x
)与滤波器(W
)的逐元素乘法,步长为1
,相同填充。我们设置strides = [1,1,1,1]
。步长的第一个和最后一个值设为1
,表示我们不希望在训练样本和不同通道之间移动。步长的第二个和第三个值也设为1
,表示我们在高度和宽度方向上将滤波器移动1
个像素:
def convolution(x, W):
return tf.nn.conv2d(x, W, strides=[1,1,1,1], padding='SAME')
我们定义一个名为max_pooling
的函数,使用tf.nn.max_pool()
执行池化操作。我们使用步长为2
的最大池化,并且使用相同的填充和ksize
指定我们的池化窗口形状:
def max_pooling(x):
return tf.nn.max_pool(x, ksize=[1,2,2,1], strides=[1,2,2,1], padding='SAME')
定义输入和输出的占位符。
输入图像的占位符定义如下:
X_ = tf.placeholder(tf.float32, [None, 784])
重塑后输入图像的占位符定义如下:
X = tf.reshape(X_, [-1, 28, 28, 1])
输出标签的占位符定义如下:
y = tf.placeholder(tf.float32, [None, 10])
定义卷积网络
我们的网络架构包括两个卷积层。每个卷积层后面跟着一个池化层,并且我们使用一个全连接层,其后跟一个输出层;即conv1->pooling->conv2->pooling2->fully connected layer-> output layer
。
首先,我们定义第一个卷积层和池化层。
权重实际上是卷积层中的滤波器。因此,权重矩阵将初始化为[ filter_shape[0], filter_shape[1], number_of_input_channel, filter_size ]
。
我们使用5 x 5
的滤波器。由于我们使用灰度图像,输入通道数将为1
,并且我们将滤波器大小设置为32
。因此,第一个卷积层的权重矩阵将是[5,5,1,32]
:
W1 = initialize_weights([5,5,1,32])
偏置的形状只是滤波器大小,即32
:
b1 = initialize_bias([32])
使用 ReLU 激活执行第一个卷积操作,然后进行最大池化:
conv1 = tf.nn.relu(convolution(X, W1) + b1)
pool1 = max_pooling(conv1)
接下来,我们定义第二个卷积层和池化层。
由于第二个卷积层从具有 32 通道输出的第一个卷积层接收输入,因此第二个卷积层的输入通道数变为 32,并且我们使用尺寸为5 x 5
的滤波器,因此第二个卷积层的权重矩阵变为[5,5,32,64]
:
W2 = initialize_weights([5,5,32,64])
偏置的形状只是滤波器大小,即64
:
b2 = initialize_bias([64])
使用 ReLU 激活执行第二次卷积操作,然后进行最大池化:
conv2 = tf.nn.relu(convolution(pool1, W2) + b2)
pool2 = max_pooling(conv2)
在两个卷积和池化层之后,我们需要在馈送到全连接层之前展平输出。因此,我们展平第二个池化层的结果并馈送到全连接层。
展平第二个池化层的结果:
flattened = tf.reshape(pool2, [-1, 7*7*64])
现在我们为全连接层定义权重和偏置。我们设置权重矩阵的形状为 [当前层中的神经元数,下一层中的神经元数]
。这是因为在展平之后,输入图像的形状变为 7x7x64
,我们在隐藏层中使用 1024
个神经元。权重的形状变为 [7x7x64, 1024]
:
W_fc = initialize_weights([7*7*64, 1024])
b_fc = initialize_bias([1024])
这里是一个具有 ReLU 激活函数的全连接层:
fc_output = tf.nn.relu(tf.matmul(flattened, W_fc) + b_fc)
定义输出层。当前层有 1024
个神经元,由于我们需要预测 10 类,所以下一层有 10 个神经元,因此权重矩阵的形状变为 [1024 x 10]
:
W_out = initialize_weights([1024, 10])
b_out = initialize_bias([10])
使用 softmax 激活函数计算输出:
YHat = tf.nn.softmax(tf.matmul(fc_output, W_out) + b_out)
计算损失
使用交叉熵计算损失。我们知道交叉熵损失如下所示:
这里, 是实际标签,
是预测标签。因此,交叉熵损失实现如下:
cross_entropy = -tf.reduce_sum(y*tf.log(YHat))
使用 Adam 优化器最小化损失:
optimizer = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
计算准确率:
predicted_digit = tf.argmax(y_hat, 1)
actual_digit = tf.argmax(y, 1)
correct_pred = tf.equal(predicted_digit,actual_digit)
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
开始训练
启动 TensorFlow 的 Session
并初始化所有变量:
sess = tf.Session()
sess.run(tf.global_variables_initializer())
对模型进行 1000
个 epochs 的训练。每 100
个 epochs 打印结果:
for epoch in range(1000):
#select some batch of data points according to the batch size (100)
X_batch, y_batch = mnist.train.next_batch(batch_size=100)
#train the network
loss, acc, _ = sess.run([cross_entropy, accuracy, optimizer], feed_dict={X_: X_batch, y: y_batch})
#print the loss on every 100th epoch
if epoch%100 == 0:
print('Epoch: {}, Loss:{} Accuracy: {}'.format(epoch,loss,acc))
您会注意到随着 epochs 的增加,损失减少,准确率增加:
Epoch: 0, Loss:631.2734375 Accuracy: 0.129999995232
Epoch: 100, Loss:28.9199733734 Accuracy: 0.930000007153
Epoch: 200, Loss:18.2174377441 Accuracy: 0.920000016689
Epoch: 300, Loss:21.740688324 Accuracy: 0.930000007153
可视化提取的特征
现在我们已经训练好了我们的 CNN 模型,我们可以看看我们的 CNN 提取了哪些特征来识别图像。正如我们学到的,每个卷积层从图像中提取重要特征。我们将看看我们的第一个卷积层提取了什么特征来识别手写数字。
首先,让我们从训练集中选择一张图片,比如说数字 1:
plt.imshow(mnist.train.images[7].reshape([28, 28]))
输入图像显示如下:
将该图像馈送到第一个卷积层 conv1
中,并获取特征图:
image = mnist.train.images[7].reshape([-1, 784])
feature_map = sess.run([conv1], feed_dict={X_: image})[0]
绘制特征图:
for i in range(32):
feature = feature_map[:,:,:,i].reshape([28, 28])
plt.subplot(4,8, i + 1)
plt.imshow(feature)
plt.axis('off')
plt.show()
如您在下图中看到的,第一个卷积层已经学会从给定图像中提取边缘:
因此,这就是 CNN 如何使用多个卷积层从图像中提取重要特征,并将这些提取的特征馈送到全连接层以对图像进行分类。现在我们已经学习了 CNN 的工作原理,在接下来的部分,我们将学习一些有趣的 CNN 架构。
CNN 架构
在本节中,我们将探索不同类型的 CNN 架构。当我们说不同类型的 CNN 架构时,我们基本上是指如何在一起堆叠卷积和池化层。此外,我们将了解使用的卷积层、池化层和全连接层的数量,以及滤波器数量和滤波器大小等信息。
LeNet 架构
LeNet 架构是 CNN 的经典架构之一。如下图所示,该架构非常简单,仅包含七个层。在这七个层中,有三个卷积层、两个池化层、一个全连接层和一个输出层。它使用 5 x 5 的卷积和步幅为 1,并使用平均池化。什么是 5 x 5 卷积?这意味着我们正在使用一个 5 x 5 的滤波器进行卷积操作。
如下图所示,LeNet 由三个卷积层(C1
、C3
、C5
)、两个池化层(S2
、S4
)、一个全连接层(F6
)和一个输出层(OUTPUT
)组成,每个卷积层后面都跟着一个池化层:
理解 AlexNet
AlexNet 是一个经典而强大的深度学习架构。它通过将错误率从 26%降低到 15.3%而赢得了 2012 年 ILSVRC 竞赛。ILSVRC 代表 ImageNet 大规模视觉识别竞赛,这是一个专注于图像分类、定位、物体检测等计算机视觉任务的重大竞赛。ImageNet 是一个包含超过 1500 万标记高分辨率图像的巨大数据集,具有超过 22000 个类别。每年,研究人员竞争使用创新架构来赢得比赛。
AlexNet 是由包括 Alex Krizhevsky、Geoffrey Hinton 和 Ilya Sutskever 在内的先驱科学家设计的。如下图所示,它由五个卷积层和三个全连接层组成。它使用 ReLU 激活函数而不是 tanh 函数,并且每一层之后都应用 ReLU。它使用 dropout 来处理过拟合,在第一个和第二个全连接层之前执行 dropout。它使用图像平移等数据增强技术,并使用两个 GTX 580 GPU 进行 5 到 6 天的批次随机梯度下降训练:
VGGNet 的架构
VGGNet 是最流行的 CNN 架构之一。它由牛津大学的视觉几何组(VGG)发明。当它成为 2014 年 ILSVRC 的亚军时,它开始变得非常流行。
它基本上是一个深度卷积网络,广泛用于物体检测任务。该网络的权重和结构由牛津团队公开,因此我们可以直接使用这些权重来执行多个计算机视觉任务。它还广泛用作图像的良好基准特征提取器。
VGG 网络的架构非常简单。它由卷积层和池化层组成。它在整个网络中使用 3 x 3 的卷积和 2 x 2 的池化。它被称为 VGG-n,其中n对应于层数,不包括池化层和 softmax 层。以下图显示了 VGG-16 网络的架构:
正如您在下图中所看到的,AlexNet 的架构以金字塔形状为特征,因为初始层宽度较大,而后续层次较窄。您会注意到它由多个卷积层和一个池化层组成。由于池化层减少了空间维度,随着网络深入,网络变窄:
VGGNet 的一个缺点是计算开销大,有超过 1.6 亿个参数。
GoogleNet
GoogleNet,也被称为Inception 网络,是 2014 年 ILSVRC 竞赛的获胜者。它包括各种版本,每个版本都是前一版本的改进版。我们将逐一探索每个版本。
Inception v1
Inception v1 是网络的第一个版本。图像中的对象以不同的大小和不同的位置出现。例如,看看第一张图像;正如您所看到的,当鹦鹉近距离观察时,它占据整个图像的一部分,但在第二张图像中,当鹦鹉从远处观察时,它占据了图像的一个较小区域:
因此,我们可以说对象(在给定的图像中,是一只鹦鹉)可以出现在图像的任何区域。它可能很小,也可能很大。它可能占据整个图像的一个区域,也可能只占据一个非常小的部分。我们的网络必须精确识别对象。但是问题在哪里呢?记得我们学习过,我们使用滤波器从图像中提取特征吗?现在,因为我们感兴趣的对象在每个图像中的大小和位置都不同,所以选择合适的滤波器大小是困难的。
当对象大小较大时,我们可以使用较大的滤波器大小,但是当我们需要检测图像角落中的对象时,较大的滤波器大小就不合适了。由于我们使用的是固定的感受野,即固定的滤波器大小,因此在图像中位置变化很大的图像中识别对象是困难的。我们可以使用深度网络,但它们更容易过拟合。
为了克服这个问题,Inception 网络不使用相同大小的单个滤波器,而是在同一输入上使用多个不同大小的滤波器。一个 Inception 块由九个这样的块堆叠而成。下图显示了一个单独的 Inception 块。正如您将看到的,我们对给定图像使用三种不同大小的滤波器进行卷积操作,即 1 x 1、3 x 3 和 5 x 5。一旦所有这些不同的滤波器完成卷积操作,我们将结果连接起来并输入到下一个 Inception 块中:
当我们连接多个滤波器的输出时,连接结果的深度将增加。虽然我们只使用填充来使输入和输出的形状相匹配,但我们仍然会有不同的深度。由于一个 Inception 块的结果是另一个的输入,深度会不断增加。因此,为了避免深度增加,我们只需在 3 x 3 和 5 x 5 卷积之前添加一个 1 x 1 卷积,如下图所示。我们还执行最大池化操作,并且在最大池化操作后添加了一个 1 x 1 卷积:
每个 Inception 块提取一些特征并将其馈送到下一个 Inception 块。假设我们试图识别一张鹦鹉的图片。在前几层中,Inception 块检测基本特征,而后续的 Inception 块则检测高级特征。正如我们所看到的,在卷积网络中,Inception 块仅提取特征,并不执行任何分类。因此,我们将 Inception 块提取的特征馈送给分类器,该分类器将预测图像是否包含鹦鹉。
由于 Inception 网络很深,具有九个 Inception 块,因此容易受到梯度消失问题的影响。为了避免这种情况,我们在 Inception 块之间引入分类器。由于每个 Inception 块学习图像的有意义特征,我们尝试在中间层进行分类并计算损失。如下图所示,我们有九个 Inception 块。我们将第三个 Inception 块的结果 和第六个 Inception 块的结果
馈送到一个中间分类器,最终的 Inception 块后也有另一个分类器。这个分类器基本上由平均池化、1 x 1 卷积和具有 softmax 激活函数的线性层组成:
中间分类器实际上被称为辅助分类器。因此,Inception 网络的最终损失是辅助分类器损失和最终分类器(真实损失)损失的加权和,如下所示:
Inception v2 和 v3
Inception v2 和 v3 是由 Christian Szegedy 在 Going Deeper with Convolutions 论文中介绍的,如 Further reading 部分所述。作者建议使用分解卷积,即将具有较大滤波器大小的卷积层分解为具有较小滤波器大小的一组卷积层。因此,在 Inception 块中,具有 5 x 5 滤波器的卷积层可以分解为两个具有 3 x 3 滤波器的卷积层,如下图所示。使用分解卷积可以提高性能和速度:
作者还建议将大小为 n x n 的卷积层分解为大小为 1 x n 和 n x 1 的卷积层堆叠。例如,在前面的图中,我们有 3 x 3 的卷积,现在将其分解为 1 x 3 的卷积,然后是 3 x 1 的卷积,如下图所示:
正如您在前面的图表中所注意到的,我们基本上是以更深入的方式扩展我们的网络,这将导致我们丢失信息。因此,我们不是让网络更深,而是让我们的网络更宽,如下所示:
在 inception net v3 中,我们使用因子化的 7 x 7 卷积和 RMSProp 优化器。此外,我们在辅助分类器中应用批归一化。
胶囊网络
胶囊网络 (CapsNets) 是由 Geoffrey Hinton 提出的,旨在克服卷积网络的局限性。
Hinton 表示如下:
"卷积神经网络中使用的池化操作是一个大错误,而它如此有效地工作实际上是一场灾难。"
但是池化操作有什么问题呢?记得当我们使用池化操作来减少维度和去除不必要的信息时吗?池化操作使我们的 CNN 表示对输入中的小平移具有不变性。
CNN 的这种平移不变性特性并不总是有益的,而且可能容易导致错误分类。例如,假设我们需要识别一幅图像是否有一个面部;CNN 将查找图像是否有眼睛、鼻子、嘴巴和耳朵。它不关心它们的位置。如果找到所有这些特征,它就将其分类为面部。
考虑两幅图像,如下图所示。第一幅图像是实际的面部图像,第二幅图像中,眼睛位于左侧,一个在另一个上方,耳朵和嘴巴位于右侧。但是 CNN 仍然会将这两幅图像都分类为面部,因为它们都具备面部的所有特征,即耳朵、眼睛、嘴巴和鼻子。CNN 认为这两幅图像都包含一个面部。它并不学习每个特征之间的空间关系;例如眼睛应该位于顶部,并且应该跟随一个鼻子等等。它只检查构成面部的特征是否存在。
当我们有一个深度网络时,这个问题会变得更糟,因为在深度网络中,特征将变得抽象,并且由于多次池化操作,它的尺寸也会缩小:
为了克服这一点,Hinton 引入了一种称为胶囊网络的新网络,它由胶囊而不是神经元组成。像卷积神经网络一样,胶囊网络检查图像中特定特征的存在,但除了检测特征外,它还会检查它们之间的空间关系。也就是说,它学习特征的层次结构。以识别面部为例,胶囊网络将学习到眼睛应该在顶部,鼻子应该在中部,接着是嘴巴等。如果图像不符合这种关系,那么胶囊网络将不会将其分类为面部:
胶囊网络由几个连接在一起的胶囊组成。但是,请稍等。什么是胶囊?
一个胶囊是一组学习在图像中检测特定特征的神经元;比如眼睛。与返回标量的神经元不同,胶囊返回矢量。矢量的长度告诉我们特定位置是否存在特定特征,矢量的元素表示特征的属性,如位置、角度等。
假设我们有一个向量,,如下所示:
向量的长度可以计算如下:
我们已经了解到矢量的长度表示特征存在的概率。但是前面的长度并不表示概率,因为它超过了 1。因此,我们使用一个称为压缩函数的函数将该值转换为概率。压缩函数具有一个优点。除了计算概率,它还保留了矢量的方向:
就像卷积神经网络一样,较早层中的胶囊检测基本特征,包括眼睛、鼻子等,而较高层中的胶囊检测更高级别的特征,比如整体面部。因此,较高层中的胶囊从较低层中的胶囊获取输入。为了让较高层中的胶囊检测到面部,它们不仅检查鼻子和眼睛等特征是否存在,还会检查它们的空间关系。
现在我们对胶囊的基本理解已经有了,我们将更详细地探讨它,并看看胶囊网络的工作原理。
理解胶囊网络
假设我们有两层, 和
。
将是较低层,它有
个胶囊,而
将是较高层,它有
个胶囊。来自较低层的胶囊将其输出发送到较高层的胶囊。
将是来自较低层胶囊的激活,
。
将是来自较高层胶囊的激活,
。
以下图表示一个胶囊,,正如你所看到的,它接收来自前一个胶囊
的输出作为输入,并计算其输出
:
我们将继续学习如何计算 。
计算预测向量
在前面的图中,、
和
表示来自前一个胶囊的输出向量。首先,我们将这些向量乘以权重矩阵并计算预测向量:
好的,那么我们在这里究竟在做什么,预测向量又是什么呢?让我们考虑一个简单的例子。假设胶囊 正试图预测图像是否有一张脸。我们已经了解到,早期层中的胶囊检测基本特征,并将结果发送到更高层的胶囊。因此,早期层中的胶囊
、
和
检测到基本低级特征,如眼睛、鼻子和嘴,并将结果发送到高层次层的胶囊,即胶囊
,它检测到脸部。
因此,胶囊 将前面的胶囊
、
和
作为输入,并乘以权重矩阵
。
权重矩阵 表示低级特征和高级特征之间的空间及其他关系。例如,权重
告诉我们眼睛应该在顶部。
告诉我们鼻子应该在中间。
告诉我们嘴应该在底部。注意,权重矩阵不仅捕捉位置(即空间关系),还捕捉其他关系。
因此,通过将输入乘以权重,我们可以预测脸部的位置:
-
暗示了基于眼睛预测的脸部位置。
-
暗示了基于鼻子预测的脸部位置。
-
暗示了基于嘴预测的脸部位置
当所有预测的脸部位置相同时,即彼此一致时,我们可以说图像包含人脸。我们使用反向传播来学习这些权重。
耦合系数
接下来,我们将预测向量 乘以耦合系数
。耦合系数存在于任意两个胶囊之间。我们知道,来自较低层的胶囊将它们的输出发送到较高层的胶囊。耦合系数帮助较低层的胶囊理解它必须将其输出发送到哪个较高层的胶囊。
例如,让我们考虑同样的例子,我们试图预测一幅图像是否包含人脸。 表示了
和
之间的一致性。
表示了眼睛和脸之间的一致性。由于我们知道眼睛在脸上,因此
的值将会增加。我们知道预测向量
暗示了基于眼睛预测的脸部位置。将
乘以
意味着我们增加了眼睛的重要性,因为
的值很高。
代表鼻子和脸之间的一致性。由于我们知道鼻子在脸上,
的值将会增加。我们知道预测向量
暗示了基于鼻子的脸部预测位置。将
乘以
意味着我们正在增加鼻子的重要性,因为
的值很高。
让我们考虑另一个低级特征,比如说,,它用于检测手指。现在,
表示手指和脸之间的一致性,这个值会很低。将
乘以
意味着我们正在降低手指的重要性,因为
的值很低。
但是这些耦合系数是如何学习的呢?与权重不同,耦合系数是在前向传播中学习的,并且它们是使用一种称为动态路由的算法来学习的,我们将在后面的部分讨论。
将 乘以
后,我们将它们加总,如下:
因此,我们可以将我们的方程写成:
压缩函数
我们开始时说,胶囊 尝试在图像中检测脸部。因此,我们需要将
转换为概率,以获取图像中存在脸的概率。
除了计算概率之外,我们还需要保留向量的方向,因此我们使用了一种称为压缩函数的激活函数。其表达式如下:
现在,(也称为活动向量)给出了在给定图像中存在脸的概率。
动态路由算法
现在,我们将看到动态路由算法如何计算耦合系数。让我们引入一个称为 的新变量,它只是一个临时变量,并且与耦合系数
相同。首先,我们将
初始化为 0。这意味着低层中的胶囊
与高层中的胶囊
之间的耦合系数被设为 0。
让 为
的向量表示。给定预测向量
,在 n 次迭代中,我们执行以下操作:
- 对于图层中的所有胶囊
,计算以下内容:
- 对于图层中的所有胶囊
,计算以下内容:
- 对于
中的所有胶囊
,以及
中的所有胶囊,按如下方式计算
:
前述方程式需仔细注意。这是我们更新耦合系数的地方。点积 意味着低层胶囊的预测向量
与高层胶囊的输出向量
的点积。如果点积较高,
将增加相应的耦合系数
,使得
更强。
胶囊网络的架构
假设我们的网络试图预测手写数字。我们知道早期层中的胶囊检测基本特征,而后期层中的胶囊检测数字。因此,让我们称早期层中的胶囊为初级胶囊,后期层中的胶囊为数字胶囊。
胶囊网络的架构如下所示:
在上图中,我们可以观察到以下内容:
-
首先,我们取输入图像并将其馈送到标准卷积层,我们称其为卷积输入。
-
然后,我们将卷积输入馈送到主胶囊层,并获得主胶囊。
-
接下来,我们使用动态路由算法计算具有主胶囊作为输入的数字胶囊。
-
数字胶囊由 10 行组成,每行代表预测数字的概率。即,第 1 行表示输入数字为 0 的概率,第 2 行表示数字 1 的概率,依此类推。
-
由于输入图像是前述图像中的数字 3,表示数字 3 的第 4 行在数字胶囊中将具有较高的概率。
损失函数
现在我们将探讨胶囊网络的损失函数。损失函数是两个称为边际损失和重构损失的损失函数的加权和。
边际损失
我们学到了胶囊返回一个向量,向量的长度表示特征存在的概率。假设我们的网络试图识别图像中的手写数字。为了在给定图像中检测多个数字,我们为每个数字胶囊使用边际损失,,如下所示:
这里是案例:
-
,如果一个类别的数字
存在
-
是边缘,
设置为 0.9,
设置为 0.1
-
防止初始学习使所有数字胶囊的向量长度缩小,通常设置为 0.5
总边际损失是所有类别的损失的总和,,如下所示:
重构损失
为了确保网络已经学习了胶囊中的重要特征,我们使用重构损失。这意味着我们使用一个称为解码器网络的三层网络,它试图从数字胶囊中重建原始图像:
重构损失被定义为重构图像与原始图像之间的平方差,如下所示:
最终的损失如下所示:
这里,alpha 是一个正则化项,因为我们不希望重构损失比边际损失更重要。因此,alpha 乘以重构损失来降低其重要性,通常设置为 0.0005。
在 TensorFlow 中构建胶囊网络
现在我们将学习如何在 TensorFlow 中实现胶囊网络。我们将使用我们喜爱的 MNIST 数据集来学习胶囊网络如何识别手写图像。
导入所需的库:
import warnings
warnings.filterwarnings('ignore')
import numpy as np
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
tf.logging.set_verbosity(tf.logging.ERROR)
载入 MNIST 数据集:
mnist = input_data.read_data_sets("data/mnist",one_hot=True)
定义squash
函数
我们学到了,squash
函数将向量的长度转换为概率,并且定义如下:
可以将squash
函数定义如下:
def squash(sj):
sj_norm = tf.reduce_sum(tf.square(sj), -2, keep_dims=True)
scalar_factor = sj_norm / (1 + sj_norm) / tf.sqrt(sj_norm + epsilon)
vj = scalar_factor * sj
return vj
定义动态路由算法
现在我们将查看动态路由算法如何实现。我们使用我们在动态路由算法中学到的相同符号的变量名,以便我们可以轻松地跟随步骤。我们将逐步查看函数中的每一行。您还可以在 GitHub 上查看完整代码,网址为bit.ly/2HQqDEZ
。
首先,定义名为dynamic_routing
的函数,该函数接受前一胶囊ui
、耦合系数bij
和路由迭代次数num_routing
作为输入,如下所示:
def dynamic_routing(ui, bij, num_routing=10):
通过从随机正态分布中绘制wij
权重,并使用常数值初始化biases
:
wij = tf.get_variable('Weight', shape=(1, 1152, 160, 8, 1), dtype=tf.float32,
initializer=tf.random_normal_initializer(0.01))
biases = tf.get_variable('bias', shape=(1, 1, 10, 16, 1))
定义主要胶囊ui
(tf.tile
复制张量n次):
ui = tf.tile(ui, [1, 1, 160, 1, 1])
计算预测向量,,如下所示:
u_hat = tf.reduce_sum(wij * ui, axis=3, keep_dims=True)
重塑预测向量:
u_hat = tf.reshape(u_hat, shape=[-1, 1152, 10, 16, 1])
停止预测向量中的梯度计算:
u_hat_stopped = tf.stop_gradient(u_hat, name='stop_gradient')
执行多次路由迭代的动态路由,如下所示:
for r in range(num_routing):
with tf.variable_scope('iter_' + str(r)):
#step 1
cij = tf.nn.softmax(bij, dim=2)
#step 2
if r == num_routing - 1:
sj = tf.multiply(cij, u_hat)
sj = tf.reduce_sum(sj, axis=1, keep_dims=True) + biases
vj = squash(sj)
elif r < num_routing - 1:
sj = tf.multiply(cij, u_hat_stopped)
sj = tf.reduce_sum(sj, axis=1, keep_dims=True) + biases
vj = squash(sj)
vj_tiled = tf.tile(vj, [1, 1152, 1, 1, 1])
coupling_coeff = tf.reduce_sum(u_hat_stopped * vj_tiled, axis=3, keep_dims=True)
#step 3
bij += coupling_coeff
return vj
计算主要和数字胶囊
现在我们将计算提取基本特征的主要胶囊和识别数字的数字胶囊。
启动 TensorFlow Graph
:
graph = tf.Graph()
with graph.as_default() as g:
定义输入和输出的占位符:
x = tf.placeholder(tf.float32, [batch_size, 784])
y = tf.placeholder(tf.float32, [batch_size,10])
x_image = tf.reshape(x, [-1,28,28,1])
执行卷积操作并获得卷积输入:
with tf.name_scope('convolutional_input'):
input_data = tf.contrib.layers.conv2d(inputs=x_image, num_outputs=256, kernel_size=9, padding='valid')
计算提取基本特征(如边缘)的主要胶囊。首先,使用卷积操作计算胶囊如下:
capsules = []
for i in range(8):
with tf.name_scope('capsules_' + str(i)):
#convolution operation
output = tf.contrib.layers.conv2d(inputs=input_data, num_outputs=32,kernel_size=9, stride=2, padding='valid')
#reshape the output
output = tf.reshape(output, [batch_size, -1, 1, 1])
#store the output which is capsule in the capsules list
capsules.append(output)
连接所有胶囊并形成主要胶囊,对主要胶囊进行压缩,并获得概率,如下所示:
primary_capsule = tf.concat(capsules, axis=2)
对主要胶囊应用squash
函数,并获得概率:
primary_capsule = squash(primary_capsule)
使用动态路由算法计算数字胶囊如下:
with tf.name_scope('dynamic_routing'):
#reshape the primary capsule
outputs = tf.reshape(primary_capsule, shape=(batch_size, -1, 1, primary_capsule.shape[-2].value, 1))
#initialize bij with 0s
bij = tf.constant(np.zeros([1, primary_capsule.shape[1].value, 10, 1, 1], dtype=np.float32))
#compute the digit capsules using dynamic routing algorithm which takes
#the reshaped primary capsules and bij as inputs and returns the activity vector
digit_capsules = dynamic_routing(outputs, bij)
digit_capsules = tf.squeeze(digit_capsules, axis=1)
屏蔽数字胶囊
为什么我们需要屏蔽数字胶囊?我们学到了,为了确保网络学到了重要特征,我们使用一个称为解码器网络的三层网络,试图从数字胶囊中重构原始图像。如果解码器能够成功从数字胶囊中重构图像,则意味着网络已经学到了图像的重要特征;否则,网络没有学到图像的正确特征。
数字胶囊包含所有数字的活动向量。但解码器只想重构给定的输入数字(输入图像)。因此,我们掩盖了除正确数字以外所有数字的活动向量。然后我们使用这个掩盖的数字胶囊来重构给定的输入图像:
with graph.as_default() as g:
with tf.variable_scope('Masking'):
#select the activity vector of given input image using the actual label y and mask out others
masked_v = tf.multiply(tf.squeeze(digit_capsules), tf.reshape(y, (-1, 10, 1)))
定义解码器
定义解码器网络以重构图像。它由三个完全连接的网络组成,如下所示:
with tf.name_scope('Decoder'):
#masked digit capsule
v_j = tf.reshape(masked_v, shape=(batch_size, -1))
#first fully connected layer
fc1 = tf.contrib.layers.fully_connected(v_j, num_outputs=512)
#second fully connected layer
fc2 = tf.contrib.layers.fully_connected(fc1, num_outputs=1024)
#reconstructed image
reconstructed_image = tf.contrib.layers.fully_connected(fc2, num_outputs=784, activation_fn=tf.sigmoid)
计算模型的准确性
现在我们计算模型的准确性:
with graph.as_default() as g:
with tf.variable_scope('accuracy'):
计算数字胶囊中每个活动向量的长度:
v_length = tf.sqrt(tf.reduce_sum(tf.square(digit_capsules), axis=2, keep_dims=True) + epsilon)
对长度应用softmax
并获得概率:
softmax_v = tf.nn.softmax(v_length, dim=1)
选择具有最高概率的索引;这将给我们预测的数字:
argmax_idx = tf.to_int32(tf.argmax(softmax_v, axis=1))
predicted_digit = tf.reshape(argmax_idx, shape=(batch_size, ))
计算accuracy
:
actual_digit = tf.to_int32(tf.argmax(y, axis=1))
correct_pred = tf.equal(predicted_digit,actual_digit)
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
计算损失
我们知道,我们计算两种类型的损失—边界损失和重构损失。
边界损失
我们知道边界损失定义如下:
计算左侧的最大值和右侧的最大值:
max_left = tf.square(tf.maximum(0.,0.9 - v_length))
max_right = tf.square(tf.maximum(0., v_length - 0.1))
将 设置为
:
T_k = y
lambda_ = 0.5
L_k = T_k * max_left + lambda_ * (1 - T_k) * max_right
总边界损失如下计算:
margin_loss = tf.reduce_mean(tf.reduce_sum(L_k, axis=1))
重构损失
通过以下代码重塑并获取原始图像:
original_image = tf.reshape(x, shape=(batch_size, -1))
计算重建图像与原始图像之间的平方差的平均值:
squared = tf.square(reconstructed_image - original_image)
计算重构损失:
reconstruction_loss = tf.reduce_mean(squared)
总损失
定义总损失,即边界损失和重建损失的加权和:
alpha = 0.0005
total_loss = margin_loss + alpha * reconstruction_loss
使用 Adam 优化器优化损失:
optimizer = tf.train.AdamOptimizer(0.0001)
train_op = optimizer.minimize(total_loss)
训练胶囊网络
设置 epoch 数和步数:
num_epochs = 100
num_steps = int(len(mnist.train.images)/batch_size)
现在开始 TensorFlow Session
并进行训练:
with tf.Session(graph=graph) as sess:
init_op = tf.global_variables_initializer()
sess.run(init_op)
for epoch in range(num_epochs):
for iteration in range(num_steps):
batch_data, batch_labels = mnist.train.next_batch(batch_size)
feed_dict = {x : batch_data, y : batch_labels}
_, loss, acc = sess.run([train_op, total_loss, accuracy], feed_dict=feed_dict)
if iteration%10 == 0:
print('Epoch: {}, iteration:{}, Loss:{} Accuracy: {}'.format(epoch,iteration,loss,acc))
您可以看到损失如何在各个迭代中减少:
Epoch: 0, iteration:0, Loss:0.55281829834 Accuracy: 0.0399999991059
Epoch: 0, iteration:10, Loss:0.541650533676 Accuracy: 0.20000000298
Epoch: 0, iteration:20, Loss:0.233602654934 Accuracy: 0.40000007153
因此,我们逐步学习了胶囊网络的工作原理,以及如何在 TensorFlow 中构建胶囊网络。
摘要
我们从理解 CNNs 开始这一章节。我们了解了 CNN 的不同层次,例如卷积和池化;从图像中提取重要特征,并将其馈送到完全连接的层;提取的特征将被分类。我们还使用 TensorFlow 可视化了从卷积层提取的特征,通过分类手写数字。
后来,我们学习了包括 LeNet、AlexNet、VGGNet 和 GoogleNet 在内的几种 CNN 架构。在章节结束时,我们学习了胶囊网络,这种网络克服了卷积网络的缺点。我们了解到胶囊网络使用动态路由算法来对图像进行分类。
在下一章中,我们将学习用于学习文本表示的各种算法。
问题
让我们尝试回答以下问题,以评估我们对 CNN 的知识:
-
CNN 的不同层次是什么?
-
定义步长。
-
为什么需要填充?
-
定义池化。有哪些不同类型的池化操作?
-
解释 VGGNet 的架构。
-
什么是 inception 网络中的因式卷积?
-
胶囊网络与 CNN 有何不同?
-
定义 squash 函数。
进一步阅读
欲了解更多信息,请参考以下内容:
-
Very Deep Convolutional Networks for Large-Scale Image Recognition 由 Karen Simonyan 和 Andrew Zisserman 撰写,可在
arxiv.org/pdf/1409.1556.pdf
查阅。 -
inception 网络的论文,Going Deeper with Convolutions 由 Christian Szegedy 等人撰写,可在
www.cv-foundation.org/openaccess/content_cvpr_2015/papers/Szegedy_Going_Deeper_With_2015_CVPR_paper.pdf
查阅。 -
Dynamic Routing Between Capsules 由 Sara Sabour、Nicholas Frosst 和 Geoffrey E. Hinton 撰写,可在
arxiv.org/pdf/1710.09829.pdf
查阅。
第七章:学习文本表示
神经网络只接受数字输入。因此,当我们有文本数据时,我们将其转换为数值或向量表示并将其馈送给网络。有多种方法可以将输入文本转换为数值形式。一些流行的方法包括词频-逆文档频率(tf-idf)、词袋模型(BOW)等。然而,这些方法不能捕捉单词的语义。这意味着这些方法不会理解单词的含义。
在本章中,我们将学习一个称为word2vec的算法,它将文本输入转换为有意义的向量。它们学习给定输入文本中每个单词的语义向量表示。我们将从理解 word2vec 模型开始,并了解两种不同类型的 word2vec 模型,即连续词袋模型(CBOW)和 skip-gram 模型。接下来,我们将学习如何使用 gensim 库构建 word2vec 模型以及如何在 tensorboard 中可视化高维词嵌入。
接下来,我们将学习doc2vec模型,它用于学习文档的表示。我们将了解 doc2vec 中的两种不同方法,称为段落向量 - 分布式内存模型(PV-DM)和段落向量 - 分布式词袋模型(PV-DBOW)。我们还将看到如何使用 doc2vec 进行文档分类。在本章的最后,我们将学习关于 skip-thoughts 算法和 quick thoughts 算法,这两者都用于学习句子表示。
在本章中,我们将了解以下主题:
-
word2vec 模型
-
使用 gensim 构建 word2vec 模型
-
在 TensorBoard 中可视化词嵌入
-
Doc2vec 模型
-
使用 doc2vec 找到相似文档
-
Skip-thoughts
-
Quick-thoughts
理解 word2vec 模型
word2vec 是生成词嵌入最流行和广泛使用的模型之一。那么什么是词嵌入?词嵌入是词在向量空间中的向量表示。word2vec 模型生成的嵌入捕捉了词的句法和语义含义。拥有一个有意义的词向量表示有助于神经网络更好地理解单词。
例如,让我们考虑以下文本:Archie used to live in New York, he then moved to Santa Clara. He loves apples and strawberries.
Word2vec 模型为前文中的每个单词生成向量表示。如果我们在嵌入空间中投影和可视化这些向量,我们可以看到所有相似的单词都被放在一起。如下图所示,单词apples和strawberries被放在一起,New York和Santa Clara也被放在一起。它们被放在一起是因为 word2vec 模型学习到了apples和strawberries是相似的实体,即水果,以及New York和Santa Clara是相似的实体,即城市,因此它们的向量(嵌入)相似,因此它们之间的距离较小:
因此,通过 word2vec 模型,我们可以学习单词的有意义的向量表示,这有助于神经网络理解单词的含义。拥有一个良好的单词表示在各种任务中都非常有用。由于我们的网络能够理解单词的上下文和语法含义,这将分支到各种用例,如文本摘要、情感分析、文本生成等等。
好的。但是 word2vec 模型是如何学习单词嵌入的呢?有两种类型的 word2vec 模型用于学习单词的嵌入:
-
CBOW 模型
-
Skip-gram 模型
我们将详细介绍并学习这些模型如何学习单词的向量表示。
理解 CBOW 模型
假设我们有一个神经网络,包括一个输入层、一个隐藏层和一个输出层。网络的目标是预测给定其周围单词的一个单词。我们试图预测的单词称为目标单词,周围的单词称为上下文单词。
我们使用大小为 的窗口来选择上下文单词,以预测目标单词。如果窗口大小为 2,则使用目标单词前两个单词和后两个单词作为上下文单词。
让我们考虑句子The Sun rises in the east,以rise作为目标单词。如果我们将窗口大小设为 2,则我们取目标单词rise之前的两个单词the和sun,以及之后的两个单词in和the作为上下文单词,如下图所示:
因此,网络的输入是上下文单词,输出是目标单词。我们如何将这些输入馈送到网络中?神经网络只接受数值输入,因此我们不能直接将原始上下文单词作为网络的输入。因此,我们使用一种称为一热编码的技术将给定句子中的所有单词转换为数值形式,如下图所示:
CBOW 模型的结构如下图所示。您可以看到,我们将上下文单词the, sun, in和the,作为输入送入网络,它预测出the, sun, in和the,的目标单词rises和an:
在初始迭代中,网络无法正确预测目标单词。但经过一系列迭代,它学会了使用梯度下降预测正确的目标单词。通过梯度下降,我们更新网络的权重,并找到能够预测正确目标单词的最优权重。
正如前面的图所示,我们有一个输入层,一个隐藏层和一个输出层,因此我们将有两组权重:
-
输入层到隐藏层的权重,
-
隐藏层到输出层的权重,
在训练过程中,网络将尝试找到这两组权重的最优值,以便能够预测正确的目标单词。
结果表明,输入到隐藏层的最优权重形成了单词的向量表示。它们基本上构成了单词的语义含义。因此,在训练后,我们只需移除输出层,并取输入层和隐藏层之间的权重,并将其分配给相应的单词。
训练后,如果我们查看矩阵,它表示每个单词的嵌入。因此,单词sun的嵌入是[0.0, 0.3, 0.3, 0.6, 0.1]:
因此,CBOW 模型学会了使用给定的上下文单词预测目标单词。它通过梯度下降学习预测正确的目标单词。在训练过程中,它通过梯度下降更新网络的权重,并找到能够预测正确目标单词的最优权重。输入层和隐藏层之间的最优权重形成了单词的向量表示。因此,在训练后,我们只需取输入层和隐藏层之间的权重,并将其分配为相应单词的向量。
现在我们对 CBOW 模型有了直观的理解,我们将详细学习数学上如何计算单词嵌入的过程。
我们学到了输入层和隐藏层之间的权重基本上形成了单词的向量表示。但 CBOW 模型如何准确预测目标单词呢?它如何使用反向传播学习最优权重?让我们在下一节中探讨这个问题。
CBOW 模型使用单个上下文单词
我们了解到,在 CBOW 模型中,我们试图根据上下文词预测目标词,因此它以一定数量的上下文词 作为输入,并返回一个目标词作为输出。在只有一个上下文词的 CBOW 模型中,我们只有一个上下文词,即
。因此,网络只接受一个上下文词作为输入,并返回一个目标词作为输出。
在继续之前,首先让我们熟悉一下符号。我们语料库中的所有唯一词称为词汇表。考虑我们在理解 CBOW 模型部分看到的例子,句子中有五个唯一词——the、sun、rises、in 和 east。这五个词是我们的词汇表。
让 表示词汇表的大小(即词的数量),
表示隐藏层中神经元的数量。我们学到我们有一个输入层、一个隐藏层和一个输出层:
-
输入层由
表示。当我们说
时,它表示词汇表中的第
个输入词。
-
隐藏层由
表示。当我们说
时,表示隐藏层中的第
个神经元。
-
输出层由
表示。当我们说
时,它表示词汇表中的第
个输出词。
输入到隐藏层权重的维度 是
(即我们词汇表的大小乘以隐藏层神经元的数量),隐藏到输出层权重的维度
是
(即隐藏层神经元的数量乘以词汇表的大小)。矩阵元素的表示如下:
-
表示输入层节点
到隐藏层节点
之间矩阵中的一个元素。
-
表示隐藏层节点
到输出层节点
之间矩阵中的一个元素。
下图将帮助我们更清楚地理解这些符号:
前向传播
为了预测给定上下文单词的目标单词,我们需要执行前向传播。
首先,我们将输入与输入到隐藏层权重
相乘:
我们知道每个输入单词都是独热编码的,因此当我们将与
相乘时,我们基本上得到了
行的
到
的向量表示。因此,我们可以直接写成如下形式:
基本上意味着输入单词的向量表示。让我们用
表示输入单词
的向量表示。因此,前述方程可以写成如下形式:
现在我们处于隐藏层,我们有另一组权重,即隐藏到输出层的权重,
。我们知道词汇表中有
个单词,我们需要计算词汇表中每个单词作为目标单词的概率。
让表示我们词汇表中单词
成为目标单词的分数。分数
通过将隐藏层值
与隐藏到输出层权重
相乘来计算。由于我们正在计算单词
的分数,我们将隐藏层
与矩阵的
列相乘:
权重矩阵的列基本上表示单词
的向量表示。我们用
来表示该单词的向量表示。因此,前述方程可以写成如下形式:
将方程(1)代入方程(2),我们可以写成如下形式:
你能推断出前述方程式试图表达什么吗?我们基本上在计算输入上下文词表示 与我们词汇表中词
表示之间的点积。
计算任意两个向量之间的点积有助于我们理解它们有多相似。因此,计算 和
的点积告诉我们词汇表中的
词与输入上下文词有多相似。因此,当词汇表中
词的分数
高时,意味着该词
与给定的输入词相似且是目标词。同样地,当词汇表中
词的分数
低时,意味着该词
不与给定的输入词相似且不是目标词。
因此, 基本上给出了单词
成为目标词的分数。但是我们不使用原始分数
,而是将它们转换为概率。我们知道 softmax 函数将值压缩在 0 到 1 之间,因此我们可以使用 softmax 函数将
转换为概率。
我们可以将输出写成如下形式:
在这里, 告诉我们给定输入上下文词时,单词
是目标词的概率。我们计算我们词汇表中所有单词的概率,并选择具有高概率的单词作为目标词。
好的,我们的目标函数是什么?也就是说,我们如何计算损失?
我们的目标是找到正确的目标词。让 表示正确目标词的概率。因此,我们需要最大化这个概率:
不是最大化原始概率,而是最大化对数概率:
但是,为什么我们要最大化对数概率而不是原始概率?因为机器在表示分数的浮点数时存在限制,当我们乘以许多概率时,会导致一个无限小的值。因此,为了避免这种情况,我们使用对数概率,这将确保数值稳定性。
现在我们有一个最大化目标,我们需要将其转换为最小化目标,以便应用我们喜爱的梯度下降算法来最小化目标函数。我们如何将我们的最大化目标转换为最小化目标?我们可以通过简单地添加负号来做到这一点。因此,我们的目标函数变为以下形式:
损失函数可以表示如下:
将方程 (3) 替换到方程 (4) 中,我们得到以下结果:
根据对数商规则,log(a/b) = log(a) - log(b),我们可以将上一个方程重写如下:
我们知道 log 和 exp 互相抵消,因此我们可以在第一项中取消 log 和 exp,因此我们最终的损失函数变为以下形式:
反向传播
我们使用梯度下降算法来最小化损失函数。因此,我们进行网络反向传播,计算损失函数对权重的梯度,并更新权重。我们有两组权重,从输入到隐藏层的权重 ,以及从隐藏到输出层的权重
。我们计算损失相对于这些权重的梯度,并根据权重更新规则更新它们。
为了更好地理解反向传播,让我们回顾一下正向传播中涉及的步骤:
首先,我们计算损失对隐藏到输出层的权重 的梯度。我们无法直接从损失函数
中直接计算损失对
的梯度,因此我们应用链式法则如下:
请参考正向传播的方程以了解如何计算导数。
第一项的导数如下所示:
这里,是误差项,即实际单词与预测单词之间的差异。
现在,我们将计算第二项的导数。
由于我们知道:
因此,损失函数关于的梯度如下所示:
现在,我们计算关于隐藏层输入到输出的权重的梯度。我们不能直接从
计算
的导数,因此我们应用链式法则如下:
为了计算上述方程中第一项的导数,我们再次应用链式法则,因为我们不能直接从计算
关于
的导数:
从方程(5),我们可以写成:
由于我们知道:
与其对求和,我们可以写成:
表示加权后所有词汇表中的输出向量之和。
现在让我们计算第二项的导数。
由于我们知道,:
因此,损失函数关于和
的梯度如下所示:
因此,我们的权重更新方程如下:
我们使用上述方程更新网络的权重,并在训练期间获得最佳权重。隐藏层输入到输出的最优权重,成为我们词汇表中单词的向量表示。
Single_context_CBOW
的 Python 代码如下:
def Single_context_CBOW(x, label, W1, W2, loss):
#forward propagation
h = np.dot(W1.T, x)
u = np.dot(W2.T, h)
y_pred = softmax(u)
#error
e = -label + y_pred
#backward propagation
dW2 = np.outer(h, e)
dW1 = np.outer(x, np.dot(W2.T, e))
#update weights
W1 = W1 - lr * dW1
W2 = W2 - lr * dW2
#loss function
loss += -float(u[label == 1]) + np.log(np.sum(np.exp(u)))
return W1, W2, loss
含有多个上下文词的 CBOW
现在我们了解了 CBOW 模型如何处理单个单词作为上下文时,我们将看看在多个单词作为上下文单词时它是如何工作的。具有多个输入单词作为上下文的 CBOW 架构显示在以下图中:
多个单词作为上下文与单个单词作为上下文之间没有太大差别。不同之处在于,当有多个上下文单词作为输入时,我们取所有输入上下文单词的平均值。也就是说,作为第一步,我们向前传播网络并通过乘以输入 和权重
来计算
,就像我们在 单个上下文单词的 CBOW 部分看到的一样:
但是,由于我们有多个上下文单词,我们将有多个输入(即 ),其中
是上下文单词的数量,我们简单地取它们的平均值,并与权重矩阵相乘,如下所示:
类似于我们在 单个上下文单词的 CBOW 部分学到的, 表示输入上下文单词
的向量表示。
表示输入单词
的向量表示,依此类推。
我们将输入上下文单词 的表示称为
,输入上下文单词
的表示称为
,以此类推。因此,我们可以将前述方程重写为:
这里, 表示上下文单词的数量。
计算 的值与我们在前一节中看到的相同:
这里, 表示词汇中的
的向量表示。
将方程 (6) 替换到方程 (7) 中,我们得到如下内容:
上述方程给出了词汇中的 与给定输入上下文单词的平均表示之间的相似度。
损失函数与我们在单词上下文中看到的相同,如下所示:
现在,在反向传播中有一个小小的区别。我们知道,在反向传播中,我们计算梯度并根据权重更新规则更新我们的权重。回想一下,在前面的章节中,这是我们更新权重的方式:
由于这里我们有多个上下文词作为输入,在计算时,我们取上下文词的平均值:
计算与我们在上一节中看到的方式相同:
简而言之,在多词上下文中,我们只需取多个上下文输入词的平均值,并像在 CBOW 的单词上下文中那样构建模型。
理解跳字模型
现在,让我们看看另一种有趣的 word2vec 模型类型,称为跳字模型。跳字模型只是 CBOW 模型的反向。也就是说,在跳字模型中,我们试图预测给定目标词的上下文词。正如下图所示,我们可以注意到我们有目标词为rises,我们需要预测的上下文词为the, sun, in和the:
与 CBOW 模型类似,我们使用窗口大小来确定需要预测多少上下文单词。跳字模型的架构如下图所示。
正如我们所看到的,它以单个目标词作为输入,并尝试预测多个上下文词:
在跳字模型中,我们试图基于目标词预测上下文词。因此,它以一个目标词作为输入,并返回上下文词作为输出,如上图所示。因此,在训练跳字模型以预测上下文词之后,我们输入到隐藏层之间的权重
成为词的向量表示,就像我们在 CBOW 模型中看到的那样。
现在我们对跳字模型有了基本的理解,让我们深入细节,学习它们是如何工作的。
跳字模型中的前向传播
首先,我们将了解跳字模型中的前向传播如何工作。让我们使用我们在 CBOW 模型中使用的相同符号。跳字模型的架构如下图所示。正如你所看到的,我们只传入一个目标词作为输入,并返回
上下文词作为输出
:
类似于我们在 CBOW 中看到的,在前向传播部分,首先我们将输入 与输入到隐藏层的权重
相乘:
我们可以直接将上述方程重写为:
在这里, 暗示着输入单词
的向量表示。
接下来,我们计算 ,这意味着我们的词汇中单词
与输入目标单词之间的相似性分数。类似于 CBOW 模型中看到的那样,
可以表示为:
我们可以直接将上述方程重写为:
在这里, 暗示着词汇中的
的向量表示。
但是,与 CBOW 模型不同,我们不仅预测一个目标单词,而是预测 个上下文单词。因此,我们可以将上述方程重写为:
因此, 暗示着词汇中的
单词得分将作为上下文单词
。
-
暗示着单词
的得分将作为第一个上下文单词
-
暗示着单词
的得分将作为第二个上下文单词
-
暗示着单词
的得分将作为第三个上下文单词
由于我们希望将得分转换为概率,我们应用 softmax 函数并计算 :
在这里, 暗示着词汇中的
单词成为上下文单词
的概率。
现在,让我们看看如何计算损失函数。让 表示正确上下文单词的概率。因此,我们需要最大化这个概率:
不是最大化原始概率,而是最大化对数概率:
类似于我们在 CBOW 模型中看到的,通过添加负号,我们将其转换为最小化目标函数:
将等式 (8) 代入前述等式,我们可以写成以下形式:
由于我们有 上下文词,我们将概率的乘积和作为:
因此,根据对数规则,我们可以重写上述方程,我们的最终损失函数变为:
看看 CBOW 模型和 skip-gram 模型的损失函数。您会注意到 CBOW 损失函数和 skip-gram 损失函数之间唯一的区别是添加了上下文词 。
反向传播
我们使用梯度下降算法最小化损失函数。因此,我们反向传播网络,计算损失函数相对于权重的梯度,并根据权重更新规则更新权重。
首先,我们计算损失函数对隐藏到输出层权重 的梯度。我们无法直接从
计算损失对
的导数,因此我们如下应用链式法则。这基本上与我们在 CBOW 模型中看到的相同,只是在所有上下文词上求和:
首先,让我们计算第一项:
我们知道 是误差项,即实际单词与预测单词之间的差异。为了简化符号,我们可以将所有上下文词的总和表示为:
因此,我们可以说:
现在,让我们计算第二项。由于我们知道 ,我们可以写成:
因此,损失函数 梯度 关于
如下所示:
现在,我们计算损失函数对输入到隐藏层权重 的梯度。这与我们在 CBOW 模型中看到的完全相同:
因此,损失函数 梯度 关于
如下给出:
计算梯度后,我们更新我们的权重W和W'为:
因此,在训练网络时,我们使用前述方程更新网络的权重,并获得最优权重。输入到隐藏层之间的最优权重,,成为我们词汇表中单词的向量表示。
各种训练策略
现在,我们将看一些不同的训练策略,这些策略可以优化和提高我们的 word2vec 模型的效率。
分层 softmax
在 CBOW 和 skip-gram 模型中,我们使用 softmax 函数计算单词出现的概率。但使用 softmax 函数计算概率是计算上昂贵的。比如,在构建 CBOW 模型时,我们计算目标词在我们词汇表中作为目标词的概率为:
如果您看前面的方程,我们基本上驱动指数与词汇表中所有单词
的指数。我们的复杂度将是
,其中
是词汇表的大小。当我们用包含数百万单词的词汇表训练 word2vec 模型时,这显然会变得计算上昂贵。因此,为了解决这个问题,我们不使用 softmax 函数,而是使用分层 softmax 函数。
分层 softmax 函数使用霍夫曼二叉搜索树,将复杂度显著降低到。如下图所示,在分层 softmax 中,我们用二叉搜索树替换输出层:
树中的每个叶子节点代表词汇表中的一个单词,所有中间节点表示它们子节点的相对概率。
如何计算给定上下文单词的目标单词的概率?我们简单地遍历树,根据需要向左或向右转向。如下图所示,给定一些上下文单词,单词flew成为目标单词的概率计算为沿路径的概率乘积:
目标词的概率如下所示:
但是我们如何计算这些概率呢?每个节点 都与一个嵌入相关联(比如,
)。要计算一个节点的概率,我们将节点的嵌入
与隐藏层的值
相乘,并应用 sigmoid 函数。例如,给定上下文词
,节点
右侧的概率计算如下:
一旦我们计算了右侧概率的概率,我们可以通过简单地从 1 中减去右侧概率来轻松计算左侧概率:
如果我们把所有叶子节点的概率加起来,那么它等于 1,这意味着我们的树已经被归一化了,为了找到一个单词的概率,我们只需要评估 节点。
负采样
我们假设正在构建一个 CBOW 模型,我们有一个句子Birds are flying in the sky. 让上下文词为birds, are, in, the,目标词为flying。
每次预测不正确的目标词时,我们都需要更新网络的权重。因此,除了单词 flying 之外,如果预测出现不同的单词作为目标词,我们就更新网络。
但这只是一个小词汇集。考虑一种情况,词汇表中有数百万个词。在这种情况下,我们需要进行大量的权重更新,直到网络预测出正确的目标词。这是耗时的,而且也不是一个高效的方法。因此,我们不是这样做,我们将正确的目标词标记为正类,并从词汇表中抽样几个词并标记为负类。
我们在这里实际上正在做的是将我们的多项式类问题转换为二元分类问题(即,不再试图预测目标词,而是模型分类给定的词是否是目标词)。
选择负样本作为单词的概率如下:
对频繁出现的词进行子采样
在我们的语料库中,会有一些非常频繁出现的词,比如the, is等等,还有一些很少出现的词。为了在这两者之间保持平衡,我们使用一种子采样技术。因此,我们以概率 删除那些频繁出现超过某个阈值的词,可以表示为:
这里,是阈值,
是词
的频率。
使用 gensim 构建 word2vec 模型
现在我们已经理解了 word2vec 模型的工作原理,让我们看看如何使用gensim
库构建 word2vec 模型。Gensim 是广泛用于构建向量空间模型的流行科学软件包之一。它可以通过pip
安装。因此,我们可以在终端中输入以下命令来安装gensim
库:
pip install -U gensim
现在我们已经安装了 gensim,我们将看看如何使用它构建 word2vec 模型。您可以从 GitHub 下载本节使用的数据集以及包含逐步说明的完整代码:bit.ly/2Xjndj4
。
首先,我们将导入必要的库:
import warnings
warnings.filterwarnings(action='ignore')
#data processing
import pandas as pd
import re
from nltk.corpus import stopwords
stopWords = stopwords.words('english')
#modelling
from gensim.models import Word2Vec
from gensim.models import Phrases
from gensim.models.phrases import Phraser
加载数据集
加载数据集:
data = pd.read_csv('data/text.csv',header=None)
让我们看看我们的数据中有什么:
data.head()
上述代码生成以下输出:
预处理和准备数据集
定义一个用于预处理数据集的函数:
def pre_process(text):
# convert to lowercase
text = str(text).lower()
# remove all special characters and keep only alpha numeric characters and spaces
text = re.sub(r'[^A-Za-z0-9\s.]',r'',text)
#remove new lines
text = re.sub(r'\n',r' ',text)
# remove stop words
text = " ".join([word for word in text.split() if word not in stopWords])
return text
我们可以通过运行以下代码来查看预处理文本的样子:
pre_process(data[0][50])
我们的输出如下:
'agree fancy. everything needed. breakfast pool hot tub nice shuttle airport later checkout time. noise issue tough sleep through. awhile forget noisy door nearby noisy guests. complained management later email credit compd us amount requested would return.'
预处理整个数据集:
data[0] = data[0].map(lambda x: pre_process(x))
Gensim 库要求输入以列表的列表形式提供:
*text = [ [word1, word2, word3], [word1, word2, word3] ]*
我们知道我们的数据中的每一行包含一组句子。因此,我们通过 '.'
将它们分割并将它们转换为列表:
data[0][1].split('.')[:5]
上述代码生成以下输出:
['stayed crown plaza april april ',
' staff friendly attentive',
' elevators tiny ',
' food restaurant delicious priced little high side',
' course washington dc']
因此,如图所示,现在我们的数据是以列表的形式呈现的。但是我们需要将它们转换为列表的列表。所以,我们现在再次使用空格' '
来分割它们。也就是说,我们首先通过 '.'
分割数据,然后再通过 ' '
分割它们,这样我们就可以得到我们的数据以列表的列表形式:
corpus = []
for line in data[0][1].split('.'):
words = [x for x in line.split()]
corpus.append(words)
您可以看到我们的输入以列表的形式呈现:
corpus[:2]
[['stayed', 'crown', 'plaza', 'april', 'april'], ['staff', 'friendly', 'attentive']]
将我们数据集中的整个文本转换为列表的列表:
data = data[0].map(lambda x: x.split('.'))
corpus = []
for i in (range(len(data))):
for line in data[i]:
words = [x for x in line.split()]
corpus.append(words)
print corpus[:2]
正如所示,我们成功地将数据集中的整个文本转换为列表的列表:
[['room', 'kind', 'clean', 'strong', 'smell', 'dogs'],
['generally', 'average', 'ok', 'overnight', 'stay', 'youre', 'fussy']]
现在,我们的问题是,我们的语料库仅包含单个词和它不会在我们给出大量输入时给我们结果,例如,san francisco。
所以我们使用 gensim 的Phrases
函数,它收集所有一起出现的单词,并在它们之间添加下划线。所以现在san francisco变成了san_francisco。
我们将min_count
参数设置为25
,这意味着我们会忽略出现次数少于min_count
的所有单词和双词组:
phrases = Phrases(sentences=corpus,min_count=25,threshold=50)
bigram = Phraser(phrases)
for index,sentence in enumerate(corpus):
corpus[index] = bigram[sentence]
正如您所见,现在我们的语料库中的双词组已经添加了下划线:
corpus[111]
[u'connected', u'rivercenter', u'mall', u'downtown', u'san_antonio']
我们检查语料库中的另一个值,以查看如何为双词组添加下划线:
corpus[9]
[u'course', u'washington_dc']
构建模型
现在让我们构建我们的模型。让我们定义一些模型需要的重要超参数:
-
size
参数表示向量的大小,即表示一个词的维度。根据我们的数据大小可以选择大小。如果我们的数据很小,那么可以将大小设为一个较小的值,但是如果数据集显著大,则可以将大小设为300
。在我们的案例中,我们将大小设为100
。 -
window_size
参数表示应该考虑目标词与其相邻词之间的距离。超出目标词的窗口大小的词将不被考虑为学习的一部分。通常情况下,较小的窗口大小是首选。 -
min_count
参数表示词的最小出现频率。如果特定词的出现次数少于min_count
,则可以简单地忽略该词。 -
workers
参数指定训练模型所需的工作线程数。 -
设置
sg=1
意味着我们使用 skip-gram 模型进行训练,但如果设置为sg=0
,则意味着我们使用 CBOW 模型进行训练。
使用以下代码定义所有超参数:
size = 100
window_size = 2
epochs = 100
min_count = 2
workers = 4
sg = 1
让我们使用 gensim 的 Word2Vec
函数来训练模型:
model = Word2Vec(corpus, sg=1,window=window_size,size=size, min_count=min_count,workers=workers,iter=epochs)
一旦成功训练了模型,我们就保存它们。保存和加载模型非常简单;我们可以简单地使用 save
和 load
函数,分别进行保存和加载模型:
model.save('model/word2vec.model')
我们还可以使用以下代码 load
已保存的 Word2Vec
模型:
model = Word2Vec.load('model/word2vec.model')
评估嵌入:
现在让我们评估我们的模型学习了什么,以及我们的模型多么好地理解了文本的语义。gensim
库提供了 most_similar
函数,该函数给出与给定词相关的前几个相似词。
正如您在下面的代码中所看到的,给定 san_diego
作为输入,我们得到了所有其他相关的城市名,这些城市名与输入最相似:
model.most_similar('san_diego')
[(u'san_antonio', 0.8147615790367126),
(u'indianapolis', 0.7657858729362488),
(u'austin', 0.7620342969894409),
(u'memphis', 0.7541092038154602),
(u'phoenix', 0.7481759786605835),
(u'seattle', 0.7471771240234375),
(u'dallas', 0.7407466769218445),
(u'san_francisco', 0.7373261451721191),
(u'la', 0.7354192137718201),
(u'boston', 0.7213659286499023)]
我们还可以对我们的向量应用算术操作,以检查我们的向量有多准确:
model.most_similar(positive=['woman', 'king'], negative=['man'], topn=1)
[(u'queen', 0.7255150675773621)]
我们还可以找到与给定词集不匹配的词;例如,在名为 text
的以下列表中,除了词 holiday
外,所有其他词都是城市名。由于 Word2Vec 理解了这种差异,它返回词 holiday
作为与列表中其他词不匹配的词。
text = ['los_angeles','indianapolis', 'holiday', 'san_antonio','new_york']
model.doesnt_match(text)
'holiday'
在 TensorBoard 中可视化词嵌入
在前面的部分中,我们学习了如何使用 gensim 构建 Word2Vec 模型来生成词嵌入。现在,我们将看到如何使用 TensorBoard 来可视化这些嵌入。可视化词嵌入帮助我们理解投影空间,并帮助我们轻松验证嵌入。TensorBoard 提供了一个内置的可视化工具称为 embedding projector,用于交互式地可视化和分析高维数据,例如我们的词嵌入。我们将学习如何逐步使用 TensorBoard 的投影仪来可视化词嵌入。
导入所需的库:
import warnings
warnings.filterwarnings(action='ignore')
import tensorflow as tf
from tensorflow.contrib.tensorboard.plugins import projector tf.logging.set_verbosity(tf.logging.ERROR)
import numpy as np
import gensim
import os
加载保存的模型:
file_name = "model/word2vec.model"
model = gensim.models.keyedvectors.KeyedVectors.load(file_name)
加载模型后,我们将单词数量保存到max_size
变量中:
max_size = len(model.wv.vocab)-1
我们知道单词向量的维度将是 。因此,我们用形状为我们的
max_size
(词汇量大小)和模型第一层大小(隐藏层中的神经元数)的矩阵w2v
来初始化:
w2v = np.zeros((max_size,model.layer1_size))
现在,我们创建一个名为metadata.tsv
的新文件,在其中保存我们模型中的所有单词,并将每个单词的嵌入存储在w2v
矩阵中:
if not os.path.exists('projections'):
os.makedirs('projections')
with open("projections/metadata.tsv", 'w+') as file_metadata:
for i, word in enumerate(model.wv.index2word[:max_size]):
#store the embeddings of the word
w2v[i] = model.wv[word]
#write the word to a file
file_metadata.write(word + '\n')
接下来,我们初始化 TensorFlow 会话:
sess = tf.InteractiveSession()
初始化名为embedding
的 TensorFlow 变量,用于保存单词嵌入:
with tf.device("/cpu:0"):
embedding = tf.Variable(w2v, trainable=False, name='embedding')
初始化所有变量:
tf.global_variables_initializer().run()
创建一个到saver
类的对象,该类实际上用于将变量保存和从检查点恢复:
saver = tf.train.Saver()
使用FileWriter
,我们可以将摘要和事件保存到我们的事件文件中:
writer = tf.summary.FileWriter('projections', sess.graph)
现在,我们初始化投影仪并添加embeddings
:
config = projector.ProjectorConfig()
embed = config.embeddings.add()
接下来,我们将我们的tensor_name
指定为embedding
,并将metadata_path
设置为metadata.tsv
文件,其中包含我们的单词:
embed.tensor_name = 'embedding'
embed.metadata_path = 'metadata.tsv'
最后,保存模型:
projector.visualize_embeddings(writer, config)
saver.save(sess, 'projections/model.ckpt', global_step=max_size)
现在,打开终端并输入以下命令以打开tensorboard
:
tensorboard --logdir=projections --port=8000
打开 TensorBoard 后,转到 PROJECTOR 选项卡。我们可以看到输出,如下图所示。您会注意到,当我们键入单词delighted
时,我们可以看到所有相关的单词,例如pleasant
、surprise
等等,都与之相邻:
Doc2vec
到目前为止,我们已经看到如何为单词生成嵌入。但是如何为文档生成嵌入呢?一种简单的方法是计算文档中每个单词的单词向量并取平均值。Mikilow 和 Le 提出了一种新的方法,用于生成文档的嵌入,而不仅仅是取单词嵌入的平均值。他们引入了两种新方法,称为 PV-DM 和 PV-DBOW。这两种方法都引入了一个称为段落 id的新向量。让我们看看这两种方法的工作原理。
段落向量 - 分布式内存模型
PV-DM 类似于 CBOW 模型,我们尝试根据上下文单词预测目标单词。在 PV-DM 中,除了单词向量外,我们还引入了一个称为段落向量的额外向量。顾名思义,段落向量学习整个段落的向量表示,并捕捉段落的主题。
如下图所示,每个段落都映射到一个唯一的向量,每个单词也映射到一个唯一的向量。因此,为了预测目标单词,我们通过连接或平均单词向量和段落向量来组合它们:
但说了这么多,段落向量在预测目标单词方面有什么用处呢?拥有段落向量真的有什么用呢?我们知道,我们试图基于上下文单词预测目标单词。上下文单词长度固定,并且在一个段落的滑动窗口中抽样。
除了上下文单词,我们还利用段落向量来预测目标单词。因为段落向量包含段落主题的信息,它们包含上下文单词不包含的含义。也就是说,上下文单词只包含关于特定单词本身的信息,而段落向量包含整个段落的信息。因此,我们可以将段落向量视为与上下文单词一起用于预测目标单词的新单词。
来自同一段落抽样的所有上下文单词共享相同的段落向量,并且跨段落不共享。假设我们有三个段落,p1,p2和p3。如果上下文是从段落p1中抽样的,则使用p1向量来预测目标单词。如果上下文是从段落p2中抽样的,则使用p2向量。因此,段落向量在段落之间不共享。然而,单词向量在所有段落中共享。也就是说,sun的向量在所有段落中是相同的。我们称我们的模型为分布式记忆模型的段落向量,因为我们的段落向量充当了一种存储信息的记忆,这些信息在当前上下文单词中是缺失的。
所以,段落向量和单词向量都是使用随机梯度下降学习的。在每次迭代中,我们从一个随机段落中抽样上下文单词,尝试预测目标单词,计算误差并更新参数。训练结束后,段落向量捕捉了段落(文档)的嵌入。
段落向量 - 分布式袋模型
PV-DBOW 类似于 skip-gram 模型,我们试图基于目标单词预测上下文单词:
不同于以前的方法,我们这里不试图预测下一个单词。相反,我们使用段落向量来分类文档中的单词。但是它们是如何工作的呢?我们训练模型以理解单词是否属于某个段落。我们抽样一些单词集,并将其馈送到分类器中,该分类器告诉我们单词是否属于特定段落,通过这种方式我们学习段落向量。
使用 doc2vec 查找相似文档
现在,我们将看到如何使用 doc2vec 进行文档分类。在本节中,我们将使用 20 个news_dataset
。它包含 20 个不同新闻类别的 20,000 篇文档。我们只使用四个类别:Electronics
、Politics
、Science
和Sports
。因此,每个类别下有 1,000 篇文档。我们将这些文档重新命名,以category_
为前缀。例如,所有科学文档都重命名为Science_1
、Science_2
等。重命名后,我们将所有文档组合并放置在一个单独的文件夹中。完整的数据以及完整的代码可以在 GitHub 的 Jupyter Notebook 上找到,网址为bit.ly/2KgBWYv
。
现在,我们训练我们的 doc2vec 模型来对这些文档进行分类并找出它们之间的相似性。
首先,我们导入所有必要的库:
import warnings
warnings.filterwarnings('ignore')
import os
import gensim
from gensim.models.doc2vec import TaggedDocument
from nltk import RegexpTokenizer
from nltk.corpus import stopwords
tokenizer = RegexpTokenizer(r'\w+')
stopWords = set(stopwords.words('english'))
现在,我们加载所有文档并将文档名称保存在docLabels
列表中,将文档内容保存在名为data
的列表中:
docLabels = []
docLabels = [f for f in os.listdir('data/news_dataset') if f.endswith('.txt')]
data = []
for doc in docLabels:
data.append(open('data/news_dataset/'+doc).read())
您可以在docLabels
列表中看到我们所有文档的名称:
docLabels[:5]
['Electronics_827.txt',
'Electronics_848.txt',
'Science829.txt',
'Politics_38.txt',
'Politics_688.txt']
定义一个名为DocIterator
的类,作为遍历所有文档的迭代器:
class DocIterator(object):
def __init__(self, doc_list, labels_list):
self.labels_list = labels_list
self.doc_list = doc_list
def __iter__(self):
for idx, doc in enumerate(self.doc_list):
yield TaggedDocument(words=doc.split(), tags= [self.labels_list[idx]])
创建一个名为it
的DocIterator
类对象:
it = DocIterator(data, docLabels)
现在,让我们构建模型。首先,定义模型的一些重要超参数:
-
size
参数表示我们的嵌入大小。 -
alpha
参数表示我们的学习率。 -
min_alpha
参数意味着我们的学习率alpha
在训练期间会衰减到min_alpha
。 -
设置
dm=1
意味着我们使用分布式内存(PV-DM)模型,如果设置dm=0
,则表示我们使用分布式词袋(PV-DBOW)模型进行训练。 -
min_count
参数表示单词的最小出现频率。如果特定单词的出现少于min_count
,我们可以简单地忽略该单词。
这些超参数被定义为:
size = 100
alpha = 0.025
min_alpha = 0.025
dm = 1
min_count = 1
现在让我们使用gensim.models.Doc2ec()
类定义模型:
model = gensim.models.Doc2Vec(size=size, min_count=min_count, alpha=alpha, min_alpha=min_alpha, dm=dm)
model.build_vocab(it)
训练模型:
for epoch in range(100):
model.train(it,total_examples=120)
model.alpha -= 0.002
model.min_alpha = model.alpha
训练后,我们可以保存模型,使用save
函数:
model.save('model/doc2vec.model')
我们可以使用load
函数加载保存的模型:
d2v_model = gensim.models.doc2vec.Doc2Vec.load('model/doc2vec.model')
现在,让我们评估模型的性能。以下代码显示,当我们将Sports_1.txt
文档作为输入时,它将输出所有相关文档及其相应的分数:
d2v_model.docvecs.most_similar('Sports_1.txt')
[('Sports_957.txt', 0.719024658203125),
('Sports_694.txt', 0.6904895305633545),
('Sports_836.txt', 0.6636477708816528),
('Sports_869.txt', 0.657712459564209),
('Sports_123.txt', 0.6526877880096436),
('Sports_4.txt', 0.6499642729759216),
('Sports_749.txt', 0.6472041606903076),
('Sports_369.txt', 0.6408025026321411),
('Sports_167.txt', 0.6392412781715393),
('Sports_104.txt', 0.6284008026123047)]
理解 skip-thoughts 算法
Skip-thoughts 是一种流行的无监督学习算法,用于学习句子嵌入。我们可以将 skip-thoughts 视为 skip-gram 模型的类比。我们了解到在 skip-gram 模型中,我们试图预测给定目标词的上下文词,而在 skip-thoughts 中,我们试图预测给定目标句子的上下文句子。换句话说,我们可以说 skip-gram 用于学习单词级别的向量,而 skip-thoughts 用于学习句子级别的向量。
跳跃思想的算法非常简单。它由一个编码器-解码器架构组成。编码器的角色是将句子映射到一个向量,而解码器的角色是生成给定输入句子的前后句子。如下图所示,跳跃思想向量包括一个编码器和两个解码器,称为前一个解码器和后一个解码器:
下面讨论编码器和解码器的工作:
-
编码器:编码器按顺序处理句子中的单词并生成嵌入。假设我们有一系列句子。
。
表示句子中的第
个单词,而
表示其单词嵌入。因此,编码器的隐藏状态被解释为句子的表示。
-
解码器:有两个解码器,称为前一个解码器和后一个解码器。顾名思义,前一个解码器用于生成前一个句子,后一个解码器用于生成下一个句子。假设我们有一个句子
及其嵌入为
。这两个解码器都将嵌入
作为输入,前一个解码器尝试生成前一个句子,
,而后一个解码器尝试生成下一个句子,
。
因此,我们通过最小化前后解码器的重构误差来训练我们的模型。因为当解码器正确重构/生成前后句子时,这意味着我们有一个有意义的句子嵌入 。我们将重构误差发送到编码器,以便编码器可以优化嵌入并向解码器发送更好的表示。一旦我们训练好我们的模型,我们就可以使用编码器为新句子生成嵌入。
句子嵌入的快速思考
快速思考是另一种有趣的学习句子嵌入的算法。在跳跃思想中,我们看到如何使用编码器-解码器架构来学习句子嵌入。在快速思考中,我们尝试学习给定句子是否与候选句子相关联。因此,我们不使用解码器,而是使用分类器来学习给定输入句子是否与候选句子相关。
让 为输入句子,
为包含与给定输入句子
相关的有效上下文和无效上下文句子集合。让
是来自于
的任意候选句子。
我们使用两个编码函数, 和
。这两个函数的作用分别是学习给定句子
和候选句子
的嵌入,即学习它们的向量表示。
一旦这两个函数生成嵌入,我们使用分类器 ,它返回每个候选句子与给定输入句子相关的概率。
如下图所示,第二候选句子的概率 较高,因为它与给定的输入句子
有关:
因此, 是正确句子的概率,即
与给定输入句子
相关的计算公式为:
在这里, 是一个分类器。
我们分类器的目标是识别与给定输入句子 相关的有效上下文句子。因此,我们的成本函数是最大化找到给定输入句子正确上下文句子的概率
。如果它正确分类句子,则表示我们的编码器学习了更好的句子表示。
摘要
我们从理解词嵌入开始本章,看了两种不同类型的 Word2Vec 模型,称为 CBOW,我们试图预测上下文词给定目标词,以及 Skip-gram,我们试图预测目标词给定上下文词。
然后,我们学习了 Word2Vec 中的各种训练策略。我们讨论了分层 softmax,其中我们用哈夫曼二叉树替换网络的输出层,并将复杂度降低到 。我们还学习了负采样和子采样频繁词汇的方法。然后我们了解了如何使用 gensim 库构建 Word2Vec 模型,以及如何将高维词嵌入投影到 TensorBoard 中进行可视化。接下来,我们研究了 doc2vec 模型如何使用 PV-DM 和 PV-DBOW 两种类型的 doc2vec 模型。在此之后,我们学习了 skip-thoughts 模型的工作原理,通过预测给定句子的前后句子来学习句子的嵌入,并在章节末尾探索了 quick-thoughts 模型。
在下一章中,我们将学习生成模型以及生成模型如何用于生成图像。
问题
让我们通过回答以下问题来评估我们新获得的知识:
-
skip-gram 和 CBOW 模型之间的区别是什么?
-
CBOW 模型的损失函数是什么?
-
负采样的必要性是什么?
-
定义 PV-DM 是什么?
-
在 skip-thoughts 向量中,编码器和解码器的角色是什么?
-
快速思想向量是什么?
进一步阅读
探索以下链接,以深入了解文本表示学习:
-
单词和短语的分布式表示及其组成性, 作者 Tomas Mikolov 等,
papers.nips.cc/paper/5021-distributed-representations-of-words-and-phrases-and-their-compositionality.pdf
-
句子和文档的分布式表示, 作者 Quoc Le 和 Tomas Mikolov,
cs.stanford.edu/~quocle/paragraph_vector.pdf
-
Skip-thought Vectors, 作者 Ryan Kiros 等,
arxiv.org/pdf/1506.06726.pdf
-
学习句子表示的高效框架, 作者 Lajanugen Logeswaran 和 Honglak Lee,
arxiv.org/pdf/1803.02893.pdf
第三部分:高级深度学习算法
在本节中,我们将详细探讨高级深度学习算法,并学习如何使用 TensorFlow 实现它们。我们将了解生成对抗网络(GANs)和自编码器。我们将探索它们的类型和应用。
这一部分包括以下章节:
-
第八章,使用 GAN 生成图像
-
第九章,深入了解生成对抗网络(GANs)
-
第十章,使用自编码器重构输入
-
第十一章,探索少样本学习算法
第八章:使用 GANs 生成图像
到目前为止,我们已经学习了判别模型,它学习区分不同类别。也就是说,给定一个输入,它告诉我们它属于哪个类别。例如,要预测一封电子邮件是垃圾邮件还是正常邮件,该模型学习最佳分隔两类(垃圾邮件和正常邮件)的决策边界,当有新的电子邮件进来时,它们可以告诉我们新邮件属于哪个类别。
在本章中,我们将学习一种生成模型,该模型学习类分布,即学习类的特征,而不是学习决策边界。顾名思义,生成模型可以生成与训练集中现有数据点类似的新数据点。
我们将从深入理解判别模型和生成模型的差异开始本章。然后,我们将深入探讨最广泛使用的生成算法之一,称为生成对抗网络(GANs)。我们将了解 GANs 的工作原理以及它们如何用于生成新的数据点。接下来,我们将探索 GANs 的架构,并学习其损失函数。随后,我们将看到如何在 TensorFlow 中实现 GANs 以生成手写数字。
我们还将详细研究深度卷积生成对抗网络(DCGAN),它作为普通 GAN 的小扩展,使用卷积网络进行架构设计。接下来,我们将探索最小二乘生成对抗网络(LSGAN),它采用最小二乘损失来生成更好和更质量的图像。
在本章末尾,我们将了解Wasserstein 生成对抗网络(WGAN),它在 GAN 的损失函数中使用 Wasserstein 度量以获得更好的结果。
本章将涵盖以下主题:
-
生成模型和判别模型的区别
-
GANs
-
生成对抗网络的架构
-
在 TensorFlow 中构建 GANs
-
深度卷积 GANs
-
使用 DCGAN 生成 CIFAR 图像
-
最小二乘生成对抗网络
-
Wasserstein 生成对抗网络
判别模型和生成模型的区别
给定一些数据点,判别模型学习将数据点分类到它们各自的类别中,通过学习最优的决策边界来分隔类别。生成模型也可以对给定的数据点进行分类,但是它们不是学习决策边界,而是学习每个类别的特征。
例如,让我们考虑图像分类任务,预测给定图像是苹果还是橙子。如下图所示,为了区分苹果和橙子,判别模型学习最优的决策边界来分隔苹果和橙子类别,而生成模型则通过学习苹果和橙子类别的特征分布来学习它们的分布:
简而言之,判别模型学习如何以最优方式找到分隔类别的决策边界,而生成模型则学习每个类别的特征。
判别模型预测输入条件下的标签 ,而生成模型学习联合概率分布
。判别模型的例子包括逻辑回归、支持向量机(SVM)等,我们可以直接从训练集中估计
。生成模型的例子包括马尔可夫随机场和朴素贝叶斯,首先我们估计
来确定
:
说一声你好,GAN!
GAN 最初由 Ian J Goodfellow、Jean Pouget-Abadie、Mehdi Mirza、Bing Xu、David Warde-Farley、Sherjil Ozair、Aaron Courville 和 Yoshua Bengio 在 2014 年的论文《生成对抗网络》中首次提出。
GAN 广泛用于生成新数据点。它们可以应用于任何类型的数据集,但通常用于生成图像。GAN 的一些应用包括生成逼真的人脸,将灰度图像转换为彩色图像,将文本描述转换为逼真图像等。
Yann LeCun 这样评价 GAN:
"过去 20 年来深度学习中最酷的想法。"
GAN 在最近几年已经发展得非常成熟,能够生成非常逼真的图像。下图展示了 GAN 在五年间生成图像的演变:
对 GAN 已经感到兴奋了吗?现在,我们将看看它们如何工作。在继续之前,让我们考虑一个简单的类比。假设你是警察,你的任务是找到伪钞,而伪造者的角色是制造假钞并欺骗警察。
伪造者不断尝试以一种逼真到无法与真钱区分的方式创建假钱。但警察必须识别钱是真还是假。因此,伪造者和警察实质上是在一个两人游戏中互相竞争。GAN 的工作原理就类似于这样。它们由两个重要组成部分组成:
-
生成器
-
鉴别器
你可以将生成器视为伪造者的类比,而鉴别器则类似于警察。换句话说,生成器的角色是创造假钱,而鉴别器的角色是判断钱是假的还是真的。
在进入细节之前,我们先基本了解一下 GAN。假设我们想让我们的 GAN 生成手写数字。我们如何做到这一点?首先,我们会获取一个包含手写数字集合的数据集;比如说,MNIST 数据集。生成器学习我们数据集中图像的分布。因此,它学习了我们训练集中手写数字的分布。一旦它学习了我们数据集中图像的分布,我们向生成器提供随机噪声,它将根据学习到的分布将随机噪声转换为一个新的手写数字,类似于训练集中的手写数字:
判别器的目标是执行分类任务。给定一张图像,它将其分类为真实或虚假;也就是说,图像是来自训练集还是由生成器生成的:
GAN 的生成器组件基本上是一个生成模型,鉴别器组件基本上是一个判别模型。因此,生成器学习类的分布,而鉴别器学习类的决策边界。
如下图所示,我们向生成器提供随机噪声,它将这个随机噪声转换为一个新的图像,类似于我们训练集中的图像,但不完全与训练集中的图像相同。生成器生成的图像称为虚假图像,而我们训练集中的图像称为真实图像。我们将真实图像和虚假图像都输入给鉴别器,它告诉我们它们是真实的概率。如果图像是虚假的,则返回 0;如果图像是真实的,则返回 1:
现在我们对生成器和判别器有了基本的理解,接下来我们将详细研究每个组件。
分解生成器
GAN 的生成器组件是一个生成模型。当我们说生成模型时,有两种类型的生成模型——隐式和显式密度模型。隐式密度模型不使用任何显式密度函数来学习概率分布,而显式密度模型则使用显式密度函数。GAN 属于第一类,即它们是隐式密度模型。让我们详细研究并理解 GAN 如何是隐式密度模型。
假设我们有一个生成器,。它基本上是一个由
参数化的神经网络。生成器网络的作用是生成新的图像。它们是如何做到的?生成器的输入应该是什么?
我们从正态或均匀分布中采样一个随机噪声,img/e81c13be-a7f2-4fce-b65d-bb431e78a44f.png
。我们将这个随机噪声作为输入传递给生成器,然后生成器将这个噪声转换为一张图像:
img/0f9e55cc-f1c7-4e81-8970-10d03f783878.png
令人惊讶,不是吗?生成器是如何将随机噪声转换成逼真图像的?
假设我们有一个包含人脸图像集合的数据集,我们希望我们的生成器生成一个新的人脸。首先,生成器通过学习训练集中图像的概率分布来学习人脸的所有特征。一旦生成器学会了正确的概率分布,它就能生成全新的人脸图像。
但是生成器如何学习训练集的分布?也就是说,生成器如何学习训练集中人脸图像的分布?
生成器不过是一个神经网络。因此,神经网络隐式学习我们训练集图像的分布;让我们将这个分布称为生成器分布img/1761d20a-41ac-42fd-96a9-7020402924b0.png
。在第一次迭代中,生成器生成一个非常嘈杂的图像。但是在一系列迭代中,它学会了准确的训练集概率分布,并通过调整其img/19a949c3-3c6d-4648-a9a7-b369bea074b7.png
参数来学习生成正确的图像。
需要注意的是,我们并没有使用均匀分布img/f7abe107-bf51-4c25-a7c3-1c179dcb65b0.png
来学习我们训练集的分布。它仅用于采样随机噪声,并且我们将这个随机噪声作为输入传递给生成器。生成器网络隐式学习我们训练集的分布,我们称之为生成器分布img/9133256a-83f4-442b-b832-92b5219fdea6.png
,这也是我们称生成器网络为隐式密度模型的原因。
分解鉴别器
正如其名称所示,鉴别器是一个鉴别模型。假设我们有一个鉴别器,img/ffac7f05-3330-4b0c-a1b7-4ff2daedeca2.png
。它也是一个神经网络,并且由参数化img/1c69a44b-a93f-46cf-b35a-06d8a59516fe.png
。
鉴别器的目标是区分两类。也就是说,给定一张图像img/19ee6f8d-c60c-4952-8a15-bfd8b22c0b5a.png
,它必须确定图像是来自真实分布还是生成器分布(虚假分布)。也就是说,鉴别器必须确定给定的输入图像是来自训练集还是生成器生成的虚假图像:
img/34c15144-474c-41d5-98d6-042c6dbdf0ef.png
让我们将我们的训练集的分布称为真实数据分布,由 表示。我们知道生成器的分布由
表示。
因此,鉴别器 实质上试图区分图像
是来自于
还是
。
但是它们是如何学习的呢?
到目前为止,我们只是研究了生成器和鉴别器的角色,但它们究竟是如何学习的?生成器如何学习生成新的逼真图像,鉴别器如何学习正确区分图像?
我们知道生成器的目标是以一种方式生成图像,使得鉴别器认为生成的图像来自真实分布。
在第一次迭代中,生成器生成了一张嘈杂的图像。当我们将这个图像输入给鉴别器时,鉴别器可以轻易地检测到这是来自生成器分布的图像。生成器将这视为损失并尝试改进自己,因为它的目标是欺骗鉴别器。也就是说,如果生成器知道鉴别器轻易地将生成的图像检测为虚假图像,那么这意味着它没有生成类似于训练集中的图像。这表明它尚未学会训练集的概率分布。
因此,生成器调整其参数,以学习训练集的正确概率分布。由于我们知道生成器是一个神经网络,我们只需通过反向传播更新网络的参数。一旦它学会了真实图像的概率分布,它就可以生成类似于训练集中的图像。
好的,那么鉴别器呢?它如何学习呢?正如我们所知,鉴别器的角色是区分真实和虚假图像。
如果鉴别器错误地将生成的图像分类;也就是说,如果鉴别器将虚假图像分类为真实图像,那么这意味着鉴别器没有学会区分真实图像和虚假图像。因此,我们通过反向传播更新鉴别器网络的参数,使其学会区分真实和虚假图像。
因此,基本上,生成器试图通过学习真实数据分布 ,欺骗鉴别器,而鉴别器试图找出图像是来自真实分布还是虚假分布。现在的问题是,在生成器和鉴别器相互竞争的情况下,我们何时停止训练网络?
基本上,GAN 的目标是生成类似于训练集中的图像。比如我们想生成一个人脸—我们学习训练集中图像的分布并生成新的人脸。因此,对于生成器,我们需要找到最优的判别器。这是什么意思?
我们知道生成器分布由 表示,而真实数据分布由
表示。如果生成器完美地学习了真实数据分布,那么
就等于
,如下图所示:
当 时,鉴别器无法区分输入图像是来自真实分布还是假分布,因此它会返回 0.5 作为概率,因为鉴别器在这两个分布相同时会变得困惑。
因此,对于生成器,最优判别器可以表示为:
因此,当鉴别器对任何图像返回概率为 0.5 时,我们可以说生成器已成功学习了我们训练集中图像的分布,并成功愚弄了鉴别器。
GAN 的架构
GAN 的架构如下图所示:
如前图所示,生成器 以随机噪声
作为输入,从均匀或正态分布中抽样,并通过隐式学习训练集的分布生成假图像。
我们从真实数据分布 和假数据分布
中采样一幅图像
,并将其馈送给鉴别器
。我们将真实和假图像馈送给鉴别器,鉴别器执行二元分类任务。也就是说,当图像为假时,它返回 0,当图像为真时返回 1。
揭秘损失函数
现在我们将研究 GAN 的损失函数。在继续之前,让我们回顾一下符号:
-
作为生成器的输入的噪声由
表示。
-
噪声
抽样自的均匀或正态分布由
表示。
-
输入图像由
表示。
-
真实数据分布或我们训练集的分布由
表示。
-
假数据分布或生成器的分布由
表示。
当我们写下 时,意味着图像
是从真实分布
中采样得到的。同样,
表示图像
是从生成器分布
中采样得到的,而
则意味着生成器输入
是从均匀分布
中采样得到的。
我们已经学习到生成器和判别器都是神经网络,并且它们通过反向传播来更新参数。现在我们需要找到最优的生成器参数 和判别器参数
。
判别器损失
现在我们将看看判别器的损失函数。我们知道判别器的目标是分类图像是真实图像还是假图像。让我们用 表示判别器。
判别器的损失函数如下所示:
不过,这意味着什么呢?让我们逐一理解每一项的含义。
第一项
让我们来看第一项:
这里, 表示我们从真实数据分布
中采样输入
,因此
是一张真实图像。
表示我们将输入图像
提交给判别器
,并且判别器将返回输入图像
是真实图像的概率。由于
是从真实数据分布
中采样得到的,我们知道
是一张真实图像。因此,我们需要最大化
的概率:
但是,我们不是最大化原始概率,而是最大化对数概率;正如我们在 第七章 中学到的,学习文本表示,我们可以写成以下形式:
因此,我们的最终方程如下:
意味着来自真实数据分布的输入图像的对数似然期望是真实的。
第二项
现在,让我们来看看第二项:
在这里,意味着我们从均匀分布中采样随机噪声
。
意味着生成器
将随机噪声
作为输入,并根据其隐式学习的分布返回假图像
。
意味着我们将生成器生成的假图像
输入到鉴别器中,并返回假输入图像是真实图像的概率。
如果我们从 1 中减去,那么它将返回假输入图像是假图像的概率:
由于我们知道不是真实图像,鉴别器将最大化这个概率。也就是说,鉴别器最大化
被分类为假图像的概率,因此我们写成:
我们不是最大化原始概率,而是最大化对数概率:
意味着生成器生成的输入图像的对数似然期望是假的。
最终项
因此,结合这两项,鉴别器的损失函数如下所示:
在这里,和
分别是生成器和鉴别器网络的参数。因此,鉴别器的目标是找到正确的
,使其能够正确分类图像。
生成器损失
生成器的损失函数如下所示:
我们知道生成器的目标是欺骗鉴别器将假图像分类为真实图像。
在鉴别器损失部分,我们看到意味着将假输入图像分类为假图像的概率,并且鉴别器最大化这些概率以正确分类假图像为假。
但是生成器希望最小化这个概率。由于生成器想要愚弄判别器,它最小化了判别器将假输入图像分类为假的概率。因此,生成器的损失函数可以表达为以下形式:
总损失
我们刚刚学习了生成器和判别器的损失函数,结合这两个损失函数,我们将我们的最终损失函数写成如下形式:
因此,我们的目标函数基本上是一个最小最大化目标函数,也就是说,判别器的最大化和生成器的最小化,我们通过反向传播各自的网络来找到最优的生成器参数 和判别器参数
。
因此,我们执行梯度上升;也就是说,判别器的最大化:
并且,我们执行梯度下降;也就是说,生成器的最小化:
然而,优化上述生成器的目标函数并不能有效地工作,导致了稳定性问题。因此,我们引入了一种称为启发式损失的新形式的损失。
启发式损失
判别器的损失函数没有变化。可以直接写成如下形式:
现在,让我们来看一下生成器的损失:
我们能否像判别器的损失函数一样将生成器的损失函数的最小化目标改为最大化目标呢?我们如何做到这一点呢?我们知道 返回假输入图像被分类为假的概率,而生成器正在最小化这个概率。
而不是这样做,我们可以写成 。这意味着假输入图像被分类为真实的概率,现在生成器可以最大化这个概率。这意味着生成器正在最大化假输入图像被判别器分类为真实图像的概率。因此,我们的生成器的损失函数现在变成了以下形式:
因此,现在我们已经将判别器和生成器的损失函数都转化为最大化的术语:
但是,如果我们可以将最大化问题转化为最小化问题,那么我们就可以应用我们喜爱的梯度下降算法。那么,我们如何将我们的最大化问题转化为最小化问题呢?我们可以通过简单地添加负号来实现这一点。
因此,我们判别器的最终损失函数如下所示:
此外,生成器的损失如下所示:
在 TensorFlow 中使用 GAN 生成图像
让我们通过在 TensorFlow 中构建 GAN 来加深对生成手写数字的理解。您也可以在这一节的完整代码这里查看。
首先,我们将导入所有必要的库:
import warnings
warnings.filterwarnings('ignore')
import numpy as np
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
tf.logging.set_verbosity(tf.logging.ERROR)
import matplotlib.pyplot as plt
%matplotlib inline
tf.reset_default_graph()
读取数据集
加载 MNIST 数据集:
data = input_data.read_data_sets("data/mnist",one_hot=True)
让我们绘制一张图像:
plt.imshow(data.train.images[13].reshape(28,28),cmap="gray")
输入图像如下所示:
定义生成器
生成器 接受噪声
作为输入,并返回图像。我们将生成器定义为一个三层的前馈网络。而不是从头编写生成器网络,我们可以使用
tf.layers.dense()
,它可以用来创建一个密集层。它接受三个参数:inputs
、units
的数量和 activation
函数:
def generator(z,reuse=None):
with tf.variable_scope('generator',reuse=reuse):
hidden1 = tf.layers.dense(inputs=z,units=128,activation=tf.nn.leaky_relu)
hidden2 = tf.layers.dense(inputs=hidden1,units=128,activation=tf.nn.leaky_relu)
output = tf.layers.dense(inputs=hidden2,units=784,activation=tf.nn.tanh)
return output
定义鉴别器
我们知道鉴别器 返回给定图像为真实的概率。我们也定义鉴别器作为一个三层的前馈网络:
def discriminator(X,reuse=None):
with tf.variable_scope('discriminator',reuse=reuse):
hidden1 = tf.layers.dense(inputs=X,units=128,activation=tf.nn.leaky_relu)
hidden2 = tf.layers.dense(inputs=hidden1,units=128,activation=tf.nn.leaky_relu)
logits = tf.layers.dense(inputs=hidden2,units=1)
output = tf.sigmoid(logits)
return logits
定义输入占位符
现在我们定义输入 和噪声
的占位符:
x = tf.placeholder(tf.float32,shape=[None,784])
z = tf.placeholder(tf.float32,shape=[None,100])
开始 GAN!
首先,我们将噪声输入生成器,并输出假图像,![]:
fake_x = generator(z)
现在我们将真实图像提供给鉴别器 ![] 并得到真实图像为真的概率:
D_logits_real = discriminator(x)
类似地,我们将假图像提供给鉴别器,并获得假图像为真的概率:
D_logits_fake = discriminator(fake_x,reuse=True)
计算损失函数
现在,我们将看看如何计算损失函数。
鉴别器损失
鉴别器损失如下所示:
首先,我们将实现第一项,。
第一项,,意味着从真实数据分布中采样的图像的对数似然的期望是真实的。
这基本上是二元交叉熵损失。我们可以使用 TensorFlow 函数 tf.nn.sigmoid_cross_entropy_with_logits()
实现二元交叉熵损失。它接受两个参数作为输入,logits
和 labels
,如下所述:
-
logits
输入,顾名思义,是网络的 logits,因此它是D_logits_real
。 -
labels
输入,顾名思义,是真实标签。我们了解到鉴别器应该对真实图像返回1
,对假图像返回0
。由于我们正在计算来自真实数据分布的输入图像的损失,真实标签是1
。
我们使用 tf.ones_like()
将标签设置为与 D_logits_real
相同形状的 1
。
然后我们使用 tf.reduce_mean()
计算平均损失。如果注意到我们的损失函数中有一个减号,这是为了将我们的损失转换为最小化目标。但在下面的代码中,没有减号,因为 TensorFlow 优化器只会最小化而不会最大化。因此,在我们的实现中不需要添加减号,因为无论如何,TensorFlow 优化器都会将其最小化:
D_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logits_real,
labels=tf.ones_like(D_logits_real)))
现在我们将实现第二项,。
第二项,,意味着生成器生成的图像被分类为假图像的对数似然期望。
类似于第一项,我们可以使用 tf.nn.sigmoid_cross_entropy_with_logits()
计算二元交叉熵损失。在此,以下内容成立:
-
Logits 是
D_logits_fake
。 -
因为我们正在计算生成器生成的假图像的损失,所以
true
标签为0
。
我们使用 tf.zeros_like()
将标签设置为与 D_logits_fake
相同形状的 0
。也就是说,labels = tf.zeros_like(D_logits_fake)
:
D_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logits_fake,
labels=tf.zeros_like(D_logits_fake)))
现在我们将实现最终的损失。
因此,结合前述两项,判别器的损失函数如下所示:
D_loss = D_loss_real + D_loss_fake
生成器损失
生成器的损失如下给出:。
它意味着假图像被分类为真实图像的概率。正如我们在判别器中计算二元交叉熵一样,我们在生成器中使用 tf.nn.sigmoid_cross_entropy_with_logits()
计算损失时这一点成立。
在这里,应注意以下事项:
-
Logits 是
D_logits_fake
。 -
因为我们的损失意味着生成器生成的假输入图像被分类为真实的概率,所以真实标签为 1。因为正如我们所学到的,生成器的目标是生成假图像并欺骗判别器将假图像分类为真实图像。
我们使用 tf.ones_like()
将标签设置为与 D_logits_fake
相同形状的 1
。也就是说,labels = tf.ones_like(D_logits_fake)
:
G_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logits_fake, labels=tf.ones_like(D_logits_fake)))
优化损失
现在我们需要优化我们的生成器和判别器。因此,我们分别收集判别器和生成器的参数为 和
:
training_vars = tf.trainable_variables()
theta_D = [var for var in training_vars if 'dis' in var.name]
theta_G = [var for var in training_vars if 'gen' in var.name]
使用 Adam 优化器优化损失:
learning_rate = 0.001
D_optimizer = tf.train.AdamOptimizer(learning_rate).minimize(D_loss,var_list = theta_D)
G_optimizer = tf.train.AdamOptimizer(learning_rate).minimize(G_loss, var_list = theta_G)
开始训练
让我们通过定义批量大小和 epoch 数量来开始训练:
batch_size = 100
num_epochs = 1000
初始化所有变量:
init = tf.global_variables_initializer()
生成手写数字
启动 TensorFlow 会话并生成手写数字:
with tf.Session() as session:
初始化所有变量:
session.run(init)
对于每个 epoch 执行以下操作:
for epoch in range(num_epochs):
选择批量数:
num_batches = data.train.num_examples // batch_size
对于每个批次执行以下操作:
for i in range(num_batches):
根据批量大小获取数据批次:
batch = data.train.next_batch(batch_size)
重塑数据:
batch_images = batch[0].reshape((batch_size,784))
batch_images = batch_images * 2 - 1
对批量噪声进行采样:
batch_noise = np.random.uniform(-1,1,size=(batch_size,100))
使用输入 x
作为 batch_images
和噪声 z
作为 batch_noise
来定义喂入字典:
feed_dict = {x: batch_images, z : batch_noise}
训练判别器和生成器:
_ = session.run(D_optimizer,feed_dict = feed_dict)
_ = session.run(G_optimizer,feed_dict = feed_dict)
计算判别器和生成器的损失:
discriminator_loss = D_loss.eval(feed_dict)
generator_loss = G_loss.eval(feed_dict)
每个第 100^(th) epoch 将噪声输入生成器,并生成一幅图像:
if epoch%100==0:
print("Epoch: {}, iteration: {}, Discriminator Loss:{}, Generator Loss: {}".format(epoch,i,discriminator_loss,generator_loss))
_fake_x = fake_x.eval(feed_dict)
plt.imshow(_fake_x[0].reshape(28,28))
plt.show()
在训练过程中,我们注意到损失如何减少,以及 GAN 如何学习生成图像,如下所示:
DCGAN – 在 GAN 中添加卷积
我们刚刚学习了 GAN 的有效性以及它们如何用于生成图像。我们知道 GAN 包括两个组件:生成器用于生成图像,判别器用作对生成图像的批评者。正如您所见,这些生成器和判别器都是基本的前馈神经网络。与其将它们保留为前馈网络,不如使用卷积网络?
在 第六章《解密卷积网络》中,我们已经看到了卷积网络在基于图像的数据上的有效性,以及它们如何以无监督的方式从图像中提取特征。由于在 GAN 中我们正在生成图像,因此使用卷积网络而不是前馈网络是可取的。因此,我们引入了一种新型 GAN,称为 DCGAN。它通过卷积扩展了 GAN 的设计。我们基本上用卷积神经网络 (CNN) 替换了生成器和判别器中的前馈网络。
判别器使用卷积层来对图像进行分类,判断是真实图像还是假图像,而生成器使用卷积转置层来生成新图像。现在我们将详细探讨生成器和判别器在 DCGAN 中与传统 GAN 相比的区别。
反卷积生成器
生成器的作用是通过学习真实数据分布来生成新的图像。在 DCGAN 中,生成器由卷积转置层和批量归一化层组成,激活函数为 ReLU。
注意,卷积转置操作也称为反卷积操作或分数步长卷积。
生成器的输入是从标准正态分布中抽取的噪声 ,它输出与训练数据中图像相同大小的图像,例如 64 x 64 x 3。
生成器的架构如下图所示:
首先,我们将形状为 100 x 1 的噪声 z 转换为 1024 x 4 x 4,以获得宽度、高度和特征图的形状,并称之为 投影和重塑。随后,我们执行一系列卷积操作,使用分数步长卷积。我们在每一层除最后一层外都应用批量归一化。此外,我们在每一层除最后一层外都应用 ReLU 激活函数。我们应用 tanh 激活函数来将生成的图像缩放到 -1 到 +1 之间。
卷积判别器
现在我们将看到 DCGAN 中判别器的架构。正如我们所知,判别器接收图像并告诉我们图像是真实图像还是伪造图像。因此,它基本上是一个二元分类器。判别器由一系列卷积和批量归一化层以及泄漏 ReLU 激活组成。
判别器的架构如下图所示:
如您所见,它接收大小为 64 x 64 x 3 的输入图像,并使用泄漏 ReLU 激活函数进行一系列卷积操作。我们在所有层上都应用批量归一化,除了输入层。
记住,在判别器和生成器中我们不使用最大池化操作。相反,我们应用了步幅卷积操作(即带步幅的卷积操作)。
简言之,我们通过用卷积网络替换生成器和判别器中的前馈网络来增强香草 GAN。
实现 DCGAN 以生成 CIFAR 图像
现在我们将看到如何在 TensorFlow 中实现 DCGAN。我们将学习如何使用来自加拿大高级研究所(CIFAR)-10 数据集的图像。CIFAR-10 包含来自 10 个不同类别的 60,000 张图像,包括飞机、汽车、鸟类、猫、鹿、狗、青蛙、马、船和卡车。我们将探讨如何使用 DCGAN 生成这些图像。
首先,导入所需的库:
import warnings
warnings.filterwarnings('ignore')
import numpy as np
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
tf.logging.set_verbosity(tf.logging.ERROR)
from keras.datasets import cifar10
import matplotlib.pyplot as plt
%matplotlib inline
from IPython import display
from scipy.misc import toimage
探索数据集
加载 CIFAR 数据集:
(x_train, y_train), _ = cifar10.load_data()
x_train = x_train.astype('float32')/255.0
让我们看看数据集中有什么。定义一个用于绘制图像的辅助函数:
def plot_images(X):
plt.figure(1)
z = 0
for i in range(0,4):
for j in range(0,4):
plt.subplot2grid((4,4),(i,j))
plt.imshow(toimage(X[z]))
z = z + 1
plt.show()
让我们绘制几张图像:
plot_images(x_train[:17])
绘制的图像如下所示:
定义判别器
我们将判别器定义为一个带有三个卷积层和一个全连接层的卷积网络。它由一系列卷积和批量归一化层以及泄漏 ReLU 激活组成。我们在所有层上都应用批量归一化,除了输入层:
def discriminator(input_images, reuse=False, is_training=False, alpha=0.1):
with tf.variable_scope('discriminator', reuse= reuse):
第一个带有泄漏 ReLU 激活的卷积层:
layer1 = tf.layers.conv2d(input_images,
filters=64,
kernel_size=5,
strides=2,
padding='same',
kernel_initializer=kernel_init,
name='conv1')
layer1 = tf.nn.leaky_relu(layer1, alpha=0.2, name='leaky_relu1')
第二个带有批量归一化和泄漏 ReLU 激活的卷积层:
layer2 = tf.layers.conv2d(layer1,
filters=128,
kernel_size=5,
strides=2,
padding='same',
kernel_initializer=kernel_init,
name='conv2')
layer2 = tf.layers.batch_normalization(layer2, training=is_training, name='batch_normalization2')
layer2 = tf.nn.leaky_relu(layer2, alpha=0.2, name='leaky_relu2')
第三个带有批量归一化和泄漏 ReLU 的卷积层:
layer3 = tf.layers.conv2d(layer2,
filters=256,
kernel_size=5,
strides=1,
padding='same',
name='conv3')
layer3 = tf.layers.batch_normalization(layer3, training=is_training, name='batch_normalization3')
layer3 = tf.nn.leaky_relu(layer3, alpha=0.1, name='leaky_relu3')
展平最终卷积层的输出:
layer3 = tf.reshape(layer3, (-1, layer3.shape[1]*layer3.shape[2]*layer3.shape[3]))
定义全连接层并返回logits
:
logits = tf.layers.dense(layer3, 1)
output = tf.sigmoid(logits)
return logits
定义生成器
正如我们学到的那样,生成器执行转置卷积操作。生成器由卷积转置和批量归一化层以及 ReLU 激活组成。我们在每一层都应用批量归一化,但是对于最后一层,我们应用tanh
激活函数将生成的图像缩放到-1 到+1 之间:
def generator(z, z_dim, batch_size, is_training=False, reuse=False):
with tf.variable_scope('generator', reuse=reuse):
第一个全连接层:
input_to_conv = tf.layers.dense(z, 8*8*128)
转换输入的形状并应用批量归一化,然后应用 ReLU 激活:
layer1 = tf.reshape(input_to_conv, (-1, 8, 8, 128))
layer1 = tf.layers.batch_normalization(layer1, training=is_training, name='batch_normalization1')
layer1 = tf.nn.relu(layer1, name='relu1')
第二层是转置卷积层,带有批量归一化和 ReLU 激活:
layer2 = tf.layers.conv2d_transpose(layer1, filters=256, kernel_size=5, strides= 2, padding='same',
kernel_initializer=kernel_init, name='deconvolution2')
layer2 = tf.layers.batch_normalization(layer2, training=is_training, name='batch_normalization2')
layer2 = tf.nn.relu(layer2, name='relu2')
定义第三层:
layer3 = tf.layers.conv2d_transpose(layer2, filters=256, kernel_size=5, strides= 2, padding='same',
kernel_initializer=kernel_init, name='deconvolution3')
layer3 = tf.layers.batch_normalization(layer3,training=is_training, name='batch_normalization3')
layer3 = tf.nn.relu(layer3, name='relu3')
定义第四层:
layer4 = tf.layers.conv2d_transpose(layer3, filters=256, kernel_size=5, strides= 1, padding='same',
kernel_initializer=kernel_init, name='deconvolution4')
layer4 = tf.layers.batch_normalization(layer4,training=is_training, name='batch_normalization4')
layer4 = tf.nn.relu(layer4, name='relu4')
在最后一层,我们不应用批量归一化,而是使用 tanh
激活函数代替 ReLU:
layer5 = tf.layers.conv2d_transpose(layer4, filters=3, kernel_size=7, strides=1, padding='same',
kernel_initializer=kernel_init, name='deconvolution5')
logits = tf.tanh(layer5, name='tanh')
return logits
定义输入
为输入定义 placeholder
:
image_width = x_train.shape[1]
image_height = x_train.shape[2]
image_channels = x_train.shape[3]
x = tf.placeholder(tf.float32, shape= (None, image_width, image_height, image_channels), name="d_input")
定义学习率和训练布尔值的 placeholder
:
learning_rate = tf.placeholder(tf.float32, shape=(), name="learning_rate")
is_training = tf.placeholder(tf.bool, [], name='is_training')
定义批次大小和噪声的维度:
batch_size = 100
z_dim = 100
定义噪声的占位符 z:
z = tf.random_normal([batch_size, z_dim], mean=0.0, stddev=1.0, name='z')
启动 DCGAN
首先,我们将噪声 输入生成器,生成假图像
:
fake_x = generator(z, z_dim, batch_size, is_training=is_training)
现在我们将真实图像输入判别器 并得到真实图像被判定为真实的概率:
D_logit_real = discriminator(x, reuse=False, is_training=is_training)
同样地,我们将虚假图像输入判别器,,并获得虚假图像被判定为真实的概率:
D_logit_fake = discriminator(fake_x, reuse=True, is_training=is_training)
计算损失函数
现在我们将看看如何计算损失函数。
判别器损失
损失函数与普通 GAN 相同:
因此,我们可以直接写出以下内容:
D_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logits_real,
labels=tf.ones_like(D_logits_real)))
D_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logits_fake,
labels=tf.zeros_like(D_logits_fake)))
D_loss = D_loss_real + D_loss_fake
生成器损失
生成器损失与普通 GAN 相同:
我们可以使用以下代码计算它:
G_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logits_fake, labels=tf.ones_like(D_logits_fake)))
优化损失
正如我们在普通 GAN 中看到的,我们分别收集判别器和生成器的参数,分别为 和
:
training_vars = tf.trainable_variables()
theta_D = [var for var in training_vars if 'dis' in var.name]
theta_G = [var for var in training_vars if 'gen' in var.name]
使用 Adam 优化器优化损失:
d_optimizer = tf.train.AdamOptimizer(learning_rate).minimize(D_loss, var_list=theta_D)
g_optimizer = tf.train.AdamOptimizer(learning_rate).minimize(G_loss, var_list=theta_G)
训练 DCGAN
让我们开始训练。定义批次数、epochs 和学习率:
num_batches = int(x_train.shape[0] / batch_size)
steps = 0
num_epcohs = 500
lr = 0.00002
定义一个辅助函数来生成和绘制生成的图像:
def generate_new_samples(session, n_images, z_dim):
z = tf.random_normal([1, z_dim], mean=0.0, stddev=1.0)
is_training = tf.placeholder(tf.bool, [], name='training_bool')
samples = session.run(generator(z, z_dim, batch_size, is_training, reuse=True),feed_dict={is_training: True})
img = (samples[0] * 255).astype(np.uint8)
plt.imshow(img)
plt.show()
开始训练:
with tf.Session() as session:
初始化所有变量:
session.run(tf.global_variables_initializer())
对每个 epoch 执行:
for epoch in range(num_epcohs):
#for each batch
for i in range(num_batches):
定义批次的起始和结束:
start = i * batch_size
end = (i + 1) * batch_size
样本图像批次:
batch_images = x_train[start:end]
每两步训练一次判别器:
if(steps % 2 == 0):
_, discriminator_loss = session.run([d_optimizer,D_loss], feed_dict={x: batch_images, is_training:True, learning_rate:lr})
训练生成器和判别器:
_, generator_loss = session.run([g_optimizer,G_loss], feed_dict={x: batch_images, is_training:True, learning_rate:lr})
_, discriminator_loss = session.run([d_optimizer,D_loss], feed_dict={x: batch_images, is_training:True, learning_rate:lr})
生成新图像:
display.clear_output(wait=True)
generate_new_samples(session, 1, z_dim)
print("Epoch: {}, iteration: {}, Discriminator Loss:{}, Generator Loss: {}".format(epoch,i,discriminator_loss,generator_loss))
steps += 1
在第一次迭代中,DCGAN 将生成原始像素,但在一系列迭代中,它将学会生成具有以下参数的真实图像:
Epoch: 0, iteration: 0, 判别器损失:1.44706475735, 生成器损失: 0.726667642593
DCGAN 在第一次迭代中生成了以下图像:
最小二乘 GAN
我们刚刚学习了 GAN 如何用于生成图像。最小二乘 GAN(LSGAN)是 GAN 的另一简单变体。顾名思义,这里我们使用最小二乘误差作为损失函数,而不是 sigmoid 交叉熵损失。通过 LSGAN,我们可以改善从 GAN 生成的图像的质量。但是我们如何做到这一点?为什么普通 GAN 生成的图像质量较差?
如果你能回忆起 GAN 的损失函数,我们使用的是 sigmoid 交叉熵作为损失函数。生成器的目标是学习训练集中图像的分布,即真实数据分布,将其映射到假分布,并从学习到的假分布中生成假样本。因此,GAN 尝试将假分布映射得尽可能接近真实分布。
但一旦生成器生成的假样本位于决策面的正确侧,梯度就会趋于消失,即使假样本远离真实分布。这是由于 sigmoid 交叉熵损失。
让我们通过以下图形来理解。使用 sigmoid 交叉熵作为损失函数的普通 GAN 的决策边界如下图所示,在这里假样本用十字表示,真实样本用点表示,更新生成器的假样本用星号表示。
如你所见,一旦生成器生成的假样本(星号)位于决策面的正确侧,也就是假样本位于真实样本(点)的一侧时,梯度就会趋于消失,即使假样本远离真实分布。这是由于 sigmoid 交叉熵损失,因为它不关心假样本是否接近真样本,而是只关注假样本是否位于决策面的正确侧。这导致了一个问题,即当梯度消失时,即使假样本远离真实数据分布,生成器也无法学习到数据集的真实分布:
因此,我们可以用最小二乘损失来改变这个决策面。现在,正如你在下图中所见,尽管生成器生成的假样本位于决策面的正确侧,梯度不会消失,直到假样本与真分布匹配。最小二乘损失强制更新将假样本匹配到真样本:
因此,由于我们将假分布与真实分布匹配,当我们将最小二乘作为成本函数时,我们的图像质量将得到改善。
简而言之,在普通 GAN 中,当假样本位于正确的决策表面的一侧时,渐变更新将会停止,即使它们来自真实样本,也就是真实分布。这是由于 Sigmoid 交叉熵损失,并不关心假样本是否接近真实样本,它只关心假样本是否在正确的一侧。这导致我们无法完美学习真实数据分布。因此,我们使用 LSGAN,它使用最小二乘误差作为损失函数,渐变更新将不会停止,直到假样本匹配真实样本,即使假样本在决策边界的正确一侧。
损失函数
现在,我们已经学会了最小二乘损失函数如何提高生成器图像的质量,我们如何用最小二乘来重写我们的 GAN 损失函数?
假设 a 和 b 分别是生成图像和真实图像的实际标签,那么我们可以用最小二乘损失的术语来写出鉴别器的损失函数如下:
类似地,假设 c 是生成器希望鉴别器相信生成图像是真实图像的实际标签,那么标签 c 代表真实图像。然后我们可以用最小二乘损失的术语来写出生成器的损失函数如下:
我们为真实图像设置标签为 1,对于假图像设置为 0,因此 b 和 c 变为 1,a 变为 0. 因此,我们的最终方程可以如下给出:
鉴别器的损失函数如下所示:
生成器的损失如下所示:
TensorFlow 中的 LSGAN
实现 LSGAN 与普通 GAN 相同,唯一不同的是损失函数的变化。因此,我们只会看如何在 TensorFlow 中实现 LSGAN 的损失函数,而不是查看整个代码。LSGAN 的完整代码可在 GitHub 仓库 bit.ly/2HMCrrx
中找到。
现在让我们看看如何实现 LSGAN 的损失函数。
鉴别器损失
鉴别器的损失如下所示:
首先,我们将实现第一个术语,:
D_loss_real = 0.5*tf.reduce_mean(tf.square(D_logits_real-1))
现在我们将实现第二个术语,:
D_loss_fake = 0.5*tf.reduce_mean(tf.square(D_logits_fake))
最终的鉴别器损失可以写成如下形式:
D_loss = D_loss_real + D_loss_fake
生成器损失
生成器的损失,,如下所示:
G_loss = 0.5*tf.reduce_mean(tf.square(D_logits_fake-1))
使用 Wasserstein 距离的 GAN
现在我们将看到 GAN 的另一个非常有趣的版本,称为 Wasserstein GAN(WGAN)。它在 GAN 的损失函数中使用了 Wasserstein 距离。首先,让我们理解为什么我们需要 Wasserstein 距离测量以及我们当前损失函数的问题所在。
在继续之前,让我们简要探讨两种用于衡量两个概率分布相似性的流行散度测量方法。
Kullback-Leibler(KL)散度是最常用的测量方法之一,用于确定一个概率分布与另一个之间的差异。假设我们有两个离散概率分布, 和
,那么 KL 散度可以表示为:
当两个分布是连续的时,KL 散度可以表示为如下积分形式:
KL 散度不对称,意味着以下情况:
Jensen-Shanon(JS)散度是另一种用于测量两个概率分布相似性的方法。但与 KL 散度不同,JS 散度是对称的,可以表示为:
我们在 GAN 中是否在最小化 JS 散度?
我们知道生成器试图学习真实数据分布 ,以便能够从学习到的分布
生成新样本,鉴别器告诉我们图像是来自真实分布还是伪造分布。
我们也学到,当 时,鉴别器无法告诉我们图像来自真实分布还是伪造分布。它只输出 0.5,因为无法区分
和
。
因此,对于生成器,最优鉴别器可以表示为:
让我们回顾一下鉴别器的损失函数:
可以简单地写成如下形式:
将等式 (1) 替换到前述等式中,我们得到以下结果:
可以这样解决:
正如你所见,我们基本上在 GAN 的损失函数中最小化 JS 散度。因此,最小化 GAN 的损失函数基本上意味着最小化真实数据分布 和伪造数据分布
之间的 JS 散度,如下所示:
最小化 和
之间的 JS 散度意味着生成器
使其分布类似于真实数据分布
。但 JS 散度存在问题。正如您从下图中看到的那样,两个分布之间没有重叠。当没有重叠或两个分布不共享相同支持时,JS 散度会爆炸或返回常数值,GANs 无法正确学习:
因此,为了避免这种情况,我们需要改变我们的损失函数。我们不再最小化 JS 散度,而是使用一种称为 Wasserstein 距离的新距离度量,即使在它们不共享相同支持的情况下,它也告诉我们两个分布之间有多大的差距。
什么是 Wasserstein 距离?
Wasserstein 距离,也称为地球移动者(EM)距离,在需要将事物从一种配置移动到另一种配置的最优传输问题中,是最常用的距离测量之一。
所以,当我们有两个分布, 和
,
表示了概率分布
要匹配概率分布
需要多少工作量。
让我们尝试理解 EM 距离背后的直觉。我们可以将概率分布视为质量的集合。我们的目标是将一个概率分布转换为另一个。有许多可能的转换方式,但 Wasserstein 度量寻求找到最佳和最小的方式,以最小的转换成本进行转换。
转换成本可以表示为距离乘以质量。
从点x到点y的信息量被定义为 ,称为运输计划。它告诉我们需要从x到y运输多少信息,而x和y之间的距离如下给出:
。
所以,成本如下:
我们有许多(x,y)对,因此所有(x,y)对的期望如下所示:
它暗示了从点x到点y的移动成本。从x到y有很多移动方式,但我们只关心最优路径,即最小成本,因此我们将我们之前的方程重写如下:
这里,inf基本上表示最小值。是
和
之间所有可能的联合分布的集合。
因此,在所有可能的和
之间的联合分布中,我们正在找到使一个分布看起来像另一个分布所需的最小成本。
我们的最终方程可以如下给出:
然而,计算 Wasserstein 距离并不是一件简单的任务,因为难以穷尽所有可能的联合分布,它变成了另一个优化问题。
为了避免这种情况,我们引入康托洛维奇-鲁宾斯坦对偶。它将我们的方程转化为一个简单的最大化问题,如下所示:
好的,但上述方程的意思是什么呢?我们基本上是在所有 k-Lipschitz 函数上应用"supremum"。等等。Lipschitz 函数是什么,"supremum"又是什么?让我们在下一节中讨论这个问题。
揭秘 k-Lipschitz 函数
Lipschitz 连续函数是必须连续的函数,几乎在所有地方都是可微的。因此,对于任何函数来说,要成为 Lipschitz 连续,函数图的斜率的绝对值不能超过一个常数。这个常数
被称为Lipschitz 常数:
简单来说,我们可以说一个函数是 Lipschitz 连续的,当一个函数的导数被某个常数K限制,并且从不超过这个常数时。
比如说,
是 Lipschitz 连续的,因为它的导数
受到 1 的限制。同样地,
也是 Lipschitz 连续的,因为它的斜率在每个地方都是-1 或 1。然而,在 0 处它是不可微的。
所以,让我们回顾一下我们的方程:
这里,"supremum"基本上是"infimum"的对立面。因此,Lipschitz 函数的"supremum"意味着 k-Lipschitz 函数的最大值。因此,我们可以写成如下形式:
上述方程基本上告诉我们,我们正在寻找实际样本期望值与生成样本期望值之间的最大距离。
WGAN 的损失函数
好的,为什么我们要学习所有这些?我们先前看到损失函数中存在 JS 散度问题,因此我们转向了 Wasserstein 距离。现在,我们的判别器的目标不再是判断图像是否来自真实分布还是伪造分布;相反,它试图最大化真实和生成样本之间的距离。我们训练判别器学习 Lipschitz 连续函数,用于计算真实数据分布和伪造数据分布之间的 Wasserstein 距离。
因此,判别器损失如下所示:
现在,我们需要确保我们的函数在训练期间是一个 k-Lipschitz 函数。因此,对于每次梯度更新,我们剪裁我们的梯度权重在一个下界和上界之间,比如在-0.01 和+0.01 之间。
我们知道判别器的损失如下所示:
我们不再最大化,而是通过添加负号将其转换为最小化目标:
生成器损失与我们在普通 GAN 中学到的相同。
因此,判别器的损失函数如下:
生成器的损失函数如下:
TensorFlow 中的 WGAN
实现 WGAN 与实现普通 GAN 相同,只是 WGAN 的损失函数不同,我们需要剪裁判别器的梯度。不再看整体,我们只看如何实现 WGAN 的损失函数以及如何剪裁判别器的梯度。
我们知道判别器的损失如下:
并且可以如下实现:
D_loss = - tf.reduce_mean(D_real) + tf.reduce_mean(D_fake)
我们知道生成器损失如下:
并且可以如下实现:
G_loss = -tf.reduce_mean(D_fake)
我们对判别器的梯度进行剪裁如下:
clip_D = [p.assign(tf.clip_by_value(p, -0.01, 0.01)) for p in theta_D]
总结
我们从理解生成模型和判别模型的差异开始这一章。我们了解到判别模型学习找到良好的决策边界,以最佳方式分隔类别,而生成模型则学习每个类别的特征。
后来,我们理解了 GAN 是如何工作的。它们基本上由称为生成器和判别器的两个神经网络组成。生成器的作用是通过学习真实数据分布来生成新图像,而判别器则充当批评者,其角色是告诉我们生成的图像是来自真实数据分布还是伪造数据分布,基本上是真实图像还是伪造图像。
接下来,我们学习了 DCGAN,其中我们基本上用卷积神经网络替换了生成器和鉴别器中的前馈神经网络。鉴别器使用卷积层来将图像分类为假或真实图像,而生成器使用卷积转置层来生成新图像。
然后,我们学习了 LSGAN,它用最小二乘误差损失替换了生成器和鉴别器的损失函数。因为当我们使用 sigmoid 交叉熵作为损失函数时,即使假样本不接近真实分布,一旦它们在正确的决策边界一侧,梯度也会消失。因此,我们将交叉熵损失替换为最小二乘误差损失,其中梯度直到假样本与真实分布匹配才会消失。这迫使梯度更新将假样本与真实样本匹配。
最后,我们学习了另一种有趣的 GAN 类型,称为 Wasserstein GAN,其中我们在鉴别器的损失函数中使用了 Wasserstein 距离度量。因为在普通的 GAN 中,我们基本上是在最小化 JS 散度,当真实数据和假数据的分布不重叠时,它将是常数或结果为 0。为了克服这一问题,我们在鉴别器的损失函数中使用了 Wasserstein 距离度量。
在接下来的章节中,我们将学习关于 CGAN、InfoGAN、CycleGAN 和 StackGAN 等几种其他有趣的 GAN 类型。
问题
通过回答以下问题来评估我们对 GAN 的知识:
-
生成模型和判别模型有什么区别?
-
解释生成器的作用。
-
解释判别器的角色。
-
生成器和鉴别器的损失函数是什么?
-
DCGAN 与普通 GAN 有何不同?
-
什么是 KL 散度?
-
定义 Wasserstein 距离。
-
什么是 k-Lipschitz 连续函数?
进一步阅读
请参考以下论文获取更多信息:
-
生成对抗网络,作者为 Ian J Goodfellow 等,
arxiv.org/pdf/1406.2661.pdf
-
深度卷积生成对抗网络中的无监督表示学习,作者为 Alec Radford,Soumith Chintala 和 Luke Metz,
arxiv.org/pdf/1511.06434.pdf
-
最小二乘生成对抗网络,作者为 Xudong Mao 等,
arxiv.org/pdf/1611.04076.pdf
-
Wasserstein GAN,作者为 Martin Arjovsky,Soumith Chintala 和 L´eon Bottou,
arxiv.org/pdf/1701.07875.pdf
第九章:更多关于 GAN 的学习
我们学习了生成对抗网络(GANs)是什么,以及如何使用不同类型的 GAN 生成图像,参见第八章,使用 GAN 生成图像。
在本章中,我们将揭示各种有趣的不同类型的 GAN。我们已经了解到 GAN 可以用来生成新的图像,但我们无法控制它们生成的图像。例如,如果我们希望我们的 GAN 生成具有特定特征的人脸,我们如何向 GAN 传达这些信息?我们做不到,因为我们无法控制生成器生成的图像。
为了解决这个问题,我们使用一种称为Conditional GAN(CGAN)的新型 GAN,可以通过指定我们要生成的内容来条件化生成器和判别器。我们将从理解 CGAN 如何生成我们感兴趣的图像开始本章,然后学习如何使用TensorFlow实现 CGAN。
接着我们了解InfoGANs,这是 CGAN 的无监督版本。我们将了解 InfoGANs 是什么,它们与 CGAN 有何不同,以及如何使用 TensorFlow 实现它们来生成新的图像。
接下来,我们将学习关于CycleGANs的内容,这是一种非常有趣的 GAN 类型。它们试图学习从一个域中图像的分布到另一个域中图像的映射。例如,将灰度图像转换为彩色图像,我们训练 CycleGAN 学习灰度图像和彩色图像之间的映射,这意味着它们学会了从一个域映射到另一个域,最好的部分是,与其他架构不同,它们甚至不需要成对的数据集。我们将深入探讨它们如何学习这些映射以及它们的架构细节。我们将探索如何实现 CycleGAN 以将真实图片转换为绘画作品。
在本章结束时,我们将探索StackGAN,它可以将文本描述转换为逼真的图片。我们将通过深入理解其架构细节来理解 StackGAN 如何实现这一点。
在本章中,我们将学习以下内容:
-
Conditional GANs
-
使用 CGAN 生成特定数字
-
InfoGAN
-
InfoGAN 的架构
-
使用 TensorFlow 构建 InfoGAN
-
CycleGAN
-
使用 CycleGAN 将图片转换为绘画
-
StackGAN
Conditional GANs
我们知道生成器通过学习真实数据分布生成新的图像,而鉴别器则检查生成器生成的图像是来自真实数据分布还是伪数据分布。
然而,生成器有能力通过学习真实数据分布生成新颖有趣的图像。我们无法控制或影响生成器生成的图像。例如,假设我们的生成器正在生成人脸图像;我们如何告诉生成器生成具有某些特征的人脸,比如大眼睛和尖鼻子?
我们不能!因为我们无法控制生成器生成的图像。
为了克服这一点,我们引入了 GAN 的一个小变体称为 CGAN,它对生成器和鉴别器都施加了一个条件。这个条件告诉 GAN 我们希望生成器生成什么样的图像。因此,我们的两个组件——鉴别器和生成器——都会根据这个条件进行操作。
让我们考虑一个简单的例子。假设我们正在使用 MNIST 数据集和 CGAN 生成手写数字。我们假设我们更专注于生成数字 7 而不是其他数字。现在,我们需要将这个条件强加给我们的生成器和鉴别器。我们如何做到这一点?
生成器以噪声 作为输入,并生成一幅图像。但除了
外,我们还传入了额外的输入,即
。这个
是一个独热编码的类标签。由于我们希望生成数字 7,我们将第七个索引设置为 1,其余索引设置为 0,即 [0,0,0,0,0,0,0,1,0,0]。
我们将潜在向量 和独热编码的条件变量
连接起来,并将其作为输入传递给生成器。然后,生成器开始生成数字 7。
鉴别器呢?我们知道鉴别器以图像 作为输入,并告诉我们图像是真实的还是伪造的。在 CGAN 中,我们希望鉴别器基于条件进行鉴别,这意味着它必须判断生成的图像是真实的数字 7 还是伪造的数字 7。因此,除了传入输入
外,我们还通过连接
和
将条件变量
传递给鉴别器。
正如您在以下图中所看到的,我们正在传入生成器 和
:
生成器是基于 引入的信息条件。类似地,除了将真实和伪造图像传递给鉴别器外,我们还向鉴别器传递了
。因此,生成器生成数字 7,而鉴别器学会区分真实的 7 和伪造的 7。
我们刚刚学习了如何使用 CGAN 生成特定数字,但是 CGAN 的应用并不仅限于此。假设我们需要生成具有特定宽度和高度的数字。我们也可以将这个条件加到,并让 GAN 生成任何期望的图像。
CGAN 的损失函数
正如您可能已经注意到的那样,我们的普通 GAN 和 CGAN 之间没有太大区别,只是在 CGAN 中,我们将额外输入(即条件变量)与生成器和鉴别器的输入连接在一起。因此,生成器和鉴别器的损失函数与普通 GAN 相同,唯一的区别是它是有条件的
。
因此,鉴别器的损失函数如下所示:
生成器的损失函数如下所示:
使用梯度下降最小化损失函数来学习 CGAN。
使用 CGAN 生成特定手写数字
我们刚刚学习了 CGAN 的工作原理和结构。为了加强我们的理解,现在我们将学习如何在 TensorFlow 中实现 CGAN,以生成特定手写数字的图像,比如数字 7。
首先,加载所需的库:
import warnings
warnings.filterwarnings('ignore')
import numpy as np
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
tf.logging.set_verbosity(tf.logging.ERROR)
tf.reset_default_graph()
import matplotlib.pyplot as plt
%matplotlib inline
from IPython import display
加载 MNIST 数据集:
data = input_data.read_data_sets("data/mnist",one_hot=True)
定义生成器
生成器G接受噪声,,以及条件变量,
,作为输入,并返回图像。我们将生成器定义为简单的两层前馈网络:
def generator(z, c,reuse=False):
with tf.variable_scope('generator', reuse=reuse):
初始化权重:
w_init = tf.contrib.layers.xavier_initializer()
连接噪声,,和条件变量,
:
inputs = tf.concat([z, c], 1)
定义第一层:
dense1 = tf.layers.dense(inputs, 128, kernel_initializer=w_init)
relu1 = tf.nn.relu(dense1)
定义第二层,并使用tanh
激活函数计算输出:
logits = tf.layers.dense(relu1, 784, kernel_initializer=w_init)
output = tf.nn.tanh(logits)
return output
定义鉴别器
我们知道鉴别器,,返回概率;也就是说,它会告诉我们给定图像为真实图像的概率。除了输入图像,
,它还将条件变量,
,作为输入。我们将鉴别器定义为简单的两层前馈网络:
def discriminator(x, c, reuse=False):
with tf.variable_scope('discriminator', reuse=reuse):
初始化权重:
w_init = tf.contrib.layers.xavier_initializer()
连接输入, 和条件变量,
:
inputs = tf.concat([x, c], 1)
定义第一层:
dense1 = tf.layers.dense(inputs, 128, kernel_initializer=w_init)
relu1 = tf.nn.relu(dense1)
定义第二层,并使用sigmoid
激活函数计算输出:
logits = tf.layers.dense(relu1, 1, kernel_initializer=w_init)
output = tf.nn.sigmoid(logits)
return output
定义输入占位符,,条件变量,
,和噪声,
:
x = tf.placeholder(tf.float32, shape=(None, 784))
c = tf.placeholder(tf.float32, shape=(None, 10))
z = tf.placeholder(tf.float32, shape=(None, 100))
启动 GAN!
首先,我们将噪声, 和条件变量,
,输入到生成器中,它将输出伪造的图像,即,
:
fake_x = generator(z, c)
现在我们将真实图像一同与条件变量,
,输入到鉴别器,
,并得到它们是真实的概率:
D_logits_real = discriminator(x,c)
类似地,我们将伪造的图像,fake_x
和条件变量,,输入到鉴别器,
,并得到它们是真实的概率:
D_logits_fake = discriminator(fake_x, c, reuse=True)
计算损失函数
现在我们将看如何计算损失函数。它与普通 GAN 基本相同,只是我们添加了一个条件变量。
鉴别器损失
鉴别器损失如下所示:
首先,我们将实现第一项,即,:
D_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logits_real,
labels=tf.ones_like(D_logits_real)))
现在我们将实现第二项,:
D_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logits_fake,
labels=tf.zeros_like(D_logits_fake)))
最终损失可以写为:
D_loss = D_loss_real + D_loss_fake
生成器损失
生成器损失如下所示:
生成器损失可以实现如下:
G_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logits_fake,
labels=tf.ones_like(D_logits_fake)))
优化损失
我们需要优化我们的生成器和鉴别器。因此,我们将鉴别器和生成器的参数分别收集为theta_D
和theta_G
:
training_vars = tf.trainable_variables()
theta_D = [var for var in training_vars if var.name.startswith('discriminator')]
theta_G = [var for var in training_vars if var.name.startswith('generator')]
使用 Adam 优化器优化损失:
learning_rate = 0.001
D_optimizer = tf.train.AdamOptimizer(learning_rate, beta1=0.5).minimize(D_loss,
var_list=theta_D)
G_optimizer = tf.train.AdamOptimizer(learning_rate, beta1=0.5).minimize(G_loss,
var_list=theta_G)
开始训练 CGAN
开始 TensorFlow 会话并初始化变量:
session = tf.InteractiveSession()
tf.global_variables_initializer().run()
定义batch_size:
batch_size = 128
定义迭代次数和类别数:
num_epochs = 500
num_classes = 10
定义图像和标签:
images = (data.train.images)
labels = data.train.labels
生成手写数字 7
我们将要生成的数字(标签
)设置为7
:
label_to_generate = 7
onehot = np.eye(10)
设置迭代次数:
for epoch in range(num_epochs):
for i in range(len(images) // batch_size):
基于批处理大小采样图像:
batch_image = images[i * batch_size:(i + 1) * batch_size]
随机采样条件,即我们要生成的数字:
batch_c = labels[i * batch_size:(i + 1) * batch_size]
采样噪声:
batch_noise = np.random.normal(0, 1, (batch_size, 100))
训练生成器并计算生成器损失:
generator_loss, _ = session.run([D_loss, D_optimizer], {x: batch_image, c: batch_c, z: batch_noise})
训练鉴别器并计算鉴别器损失:
discriminator_loss, _ = session.run([G_loss, G_optimizer], {x: batch_image, c: batch_c, z: batch_noise})
随机采样噪声:
noise = np.random.rand(1,100)
选择我们想要生成的数字:
gen_label = np.array([[label_to_generate]]).reshape(-1)
将选择的数字转换为一个独热编码向量:
one_hot_targets = np.eye(num_classes)[gen_label]
将噪声和独热编码条件输入到生成器中并生成伪造图像:
_fake_x = session.run(fake_x, {z: noise, c: one_hot_targets})
_fake_x = _fake_x.reshape(28,28)
打印生成器和鉴别器的损失并绘制生成器图像:
print("Epoch: {},Discriminator Loss:{}, Generator Loss: {}".format(epoch,discriminator_loss,generator_loss))
#plot the generated image
display.clear_output(wait=True)
plt.imshow(_fake_x)
plt.show()
如下图所示,生成器现在已经学会生成数字 7,而不是随机生成其他数字:
理解 InfoGAN
InfoGAN 是 CGAN 的无监督版本。在 CGAN 中,我们学习如何条件生成器和判别器以生成我们想要的图像。但是当数据集中没有标签时,我们如何做到这一点?假设我们有一个没有标签的 MNIST 数据集,我们如何告诉生成器生成我们感兴趣的特定图像?由于数据集是无标签的,我们甚至不知道数据集中存在哪些类别。
我们知道生成器使用噪声z作为输入并生成图像。生成器在z中封装了关于图像的所有必要信息,这被称为纠缠表示。它基本上学习了z中图像的语义表示。如果我们能解开这个向量,我们就可以发现图像的有趣特征。
因此,我们将把z分为两部分:
-
常规噪声
-
代码c
什么是代码?代码c基本上是可解释的分离信息。假设我们有 MNIST 数据,那么代码c1暗示了数字标签,代码c2暗示了数字的宽度,c3暗示了数字的笔画,依此类推。我们用术语c来统称它们。
现在我们有z和c,我们如何学习有意义的代码c?我们能从生成器生成的图像学习有意义的代码吗?假设生成器生成了数字 7 的图像。现在我们可以说代码c1是 7,因为我们知道c1暗示了数字标签。
但由于代码可以意味着任何东西,比如标签、数字的宽度、笔画、旋转角度等等,我们如何学习我们想要的东西?代码c将根据先验选择进行学习。例如,如果我们为c选择了一个多项先验,那么我们的 InfoGAN 可能会为c分配一个数字标签。假设我们选择了一个高斯先验,那么它可能会分配一个旋转角度等等。我们也可以有多个先验。
先验c的分布可以是任何形式。InfoGAN 根据分布分配不同的属性。在 InfoGAN 中,代码c是根据生成器输出自动推断的,不像 CGAN 那样我们需要显式指定c。
简而言之,我们基于生成器输出推断,
。但我们究竟是如何推断
的呢?我们使用信息理论中的一个概念,称为互信息。
互信息
两个随机变量之间的互信息告诉我们可以通过一个随机变量获取另一个随机变量的信息量。两个随机变量x和y之间的互信息可以表示如下:
它基本上是y的熵与给定x的条件熵之间的差异。
代码 和生成器输出
之间的互信息告诉我们通过
我们可以获取多少关于
的信息。如果互信息 c 和
很高,那么我们可以说知道生成器输出有助于推断 c。但如果互信息很低,则无法从生成器输出推断 c。我们的目标是最大化互信息。
代码 和生成器输出
之间的互信息可以表示为:
让我们来看看公式的元素:
-
是代码的熵
-
是给定生成器输出
条件下代码 c 的条件熵。
但问题是,我们如何计算 ?因为要计算这个值,我们需要知道后验分布
,而这是我们目前不知道的。因此,我们用辅助分布
来估计后验分布:
假设 ,那么我们可以推导出互信息如下:
因此,我们可以说:
最大化互信息, 基本上意味着我们在生成输出中最大化了关于 c 的知识,也就是通过另一个变量了解一个变量。
InfoGAN 的架构
好的。这里到底发生了什么?为什么我们要这样做?简单地说,我们把输入分成了两部分:z 和 c。因为 z 和 c 都用于生成图像,它们捕捉了图像的语义含义。代码 c 给出了我们关于图像的可解释解耦信息。因此,我们试图在生成器输出中找到 c。然而,我们不能轻易地做到这一点,因为我们还不知道后验分布 ,所以我们使用辅助分布
来学习 c。
这个辅助分布基本上是另一个神经网络;让我们称这个网络为 Q 网络。Q 网络的作用是预测给定生成器图像 x 的代码 c 的可能性,表示为 。
首先,我们从先验分布 p(c) 中采样 c。然后,我们将 c 和 z 连接起来,并将它们输入生成器。接下来,我们将由生成器给出的结果 输入鉴别器。我们知道鉴别器的作用是输出给定图像为真实图像的概率。此外,Q 网络接收生成的图像,并返回给定生成图像的 c 的估计。
鉴别器 D 和 Q 网络都接受生成器图像并返回输出,因此它们共享一些层。由于它们共享一些层,我们将 Q 网络附加到鉴别器上,如下图所示:
因此,鉴别器返回两个输出:
-
图像为真实图像的概率
-
c 的估计,即给定生成器图像的 c 的概率
我们将互信息项添加到我们的损失函数中。
因此,鉴别器的损失函数定义为:
生成器的损失函数定义为:
这两个方程表明我们正在最小化 GAN 的损失,并同时最大化互信息。对 InfoGAN 还有疑惑?别担心!我们将通过在 TensorFlow 中逐步实现它们来更好地学习 InfoGAN。
在 TensorFlow 中构建 InfoGAN:
我们将通过在 TensorFlow 中逐步实现 InfoGAN 来更好地理解它们。我们将使用 MNIST 数据集,并学习 InfoGAN 如何基于生成器输出自动推断出代码 。我们构建一个 Info-DCGAN;即,在生成器和鉴别器中使用卷积层而不是简单的神经网络。
首先,我们将导入所有必要的库:
import warnings
warnings.filterwarnings('ignore')
import numpy as np
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
tf.logging.set_verbosity(tf.logging.ERROR)
import matplotlib.pyplot as plt
%matplotlib inline
加载 MNIST 数据集:
data = input_data.read_data_sets("data/mnist",one_hot=True)
定义泄漏 ReLU 激活函数:
def lrelu(X, leak=0.2):
f1 = 0.5 * (1 + leak)
f2 = 0.5 * (1 - leak)
return f1 * X + f2 * tf.abs(X)
定义生成器:
生成器 ,它接受噪声
和变量
作为输入,并返回一个图像。与在生成器中使用全连接层不同,我们使用了一个反卷积网络,就像我们学习 DCGAN 时一样:
def generator(c, z,reuse=None):
首先,连接噪声 z 和变量 :
input_combined = tf.concat([c, z], axis=1)
定义第一层,这是一个包括批归一化和 ReLU 激活的全连接层:
fuly_connected1 = tf.layers.dense(input_combined, 1024)
batch_norm1 = tf.layers.batch_normalization(fuly_connected1, training=is_train)
relu1 = tf.nn.relu(batch_norm1)
定义第二层,它也是全连接层,包括批归一化和 ReLU 激活:
fully_connected2 = tf.layers.dense(relu1, 7 * 7 * 128)
batch_norm2 = tf.layers.batch_normalization(fully_connected2, training=is_train)
relu2 = tf.nn.relu(batch_norm2)
展平第二层的结果:
relu_flat = tf.reshape(relu2, [batch_size, 7, 7, 128])
第三层是反卷积,即转置卷积操作,紧随其后是批归一化和 ReLU 激活:
deconv1 = tf.layers.conv2d_transpose(relu_flat,
filters=64,
kernel_size=4,
strides=2,
padding='same',
activation=None)
batch_norm3 = tf.layers.batch_normalization(deconv1, training=is_train)
relu3 = tf.nn.relu(batch_norm3)
第四层是另一个转置卷积操作:
deconv2 = tf.layers.conv2d_transpose(relu3,
filters=1,
kernel_size=4,
strides=2,
padding='same',
activation=None)
对第四层的结果应用 sigmoid 函数并获得输出:
output = tf.nn.sigmoid(deconv2)
return output
定义鉴别器
我们学到鉴别器 和 Q 网络都接受生成器图像并返回输出,因此它们共享一些层。由于它们共享一些层,我们像在 InfoGAN 的架构中学到的那样,将 Q 网络附加到鉴别器上。在鉴别器中,我们使用卷积网络,而不是全连接层,正如我们在 DCGAN 的鉴别器中学到的:
def discriminator(x,reuse=None):
定义第一层,执行卷积操作,随后是泄漏 ReLU 激活:
conv1 = tf.layers.conv2d(x,
filters=64,
kernel_size=4,
strides=2,
padding='same',
kernel_initializer=tf.contrib.layers.xavier_initializer(),
activation=None)
lrelu1 = lrelu(conv1, 0.2)
在第二层中,我们还执行卷积操作,随后进行批归一化和泄漏 ReLU 激活:
conv2 = tf.layers.conv2d(lrelu1,
filters=128,
kernel_size=4,
strides=2,
padding='same',
kernel_initializer=tf.contrib.layers.xavier_initializer(),
activation=None)
batch_norm2 = tf.layers.batch_normalization(conv2, training=is_train)
lrelu2 = lrelu(batch_norm2, 0.2)
展平第二层的结果:
lrelu2_flat = tf.reshape(lrelu2, [batch_size, -1])
将展平的结果馈送到全连接层,这是第三层,随后进行批归一化和泄漏 ReLU 激活:
full_connected = tf.layers.dense(lrelu2_flat,
units=1024,
activation=None)
batch_norm_3 = tf.layers.batch_normalization(full_connected, training=is_train)
lrelu3 = lrelu(batch_norm_3, 0.2)
计算鉴别器输出:
d_logits = tf.layers.dense(lrelu3, units=1, activation=None)
正如我们学到的,我们将 Q 网络附加到鉴别器。定义 Q 网络的第一层,它以鉴别器的最终层作为输入:
full_connected_2 = tf.layers.dense(lrelu3,
units=128,
activation=None)
batch_norm_4 = tf.layers.batch_normalization(full_connected_2, training=is_train)
lrelu4 = lrelu(batch_norm_4, 0.2)
定义 Q 网络的第二层:
q_net_latent = tf.layers.dense(lrelu4,
units=74,
activation=None)
估计 c:
q_latents_categoricals_raw = q_net_latent[:,0:10]
c_estimates = tf.nn.softmax(q_latents_categoricals_raw, dim=1)
返回鉴别器 logits
和估计的 c 值作为输出:
return d_logits, c_estimates
定义输入占位符
现在我们为输入 、噪声
和代码
定义占位符:
batch_size = 64
input_shape = [batch_size, 28,28,1]
x = tf.placeholder(tf.float32, input_shape)
z = tf.placeholder(tf.float32, [batch_size, 64])
c = tf.placeholder(tf.float32, [batch_size, 10])
is_train = tf.placeholder(tf.bool)
启动 GAN
首先,我们将噪声 和代码
提供给生成器,它将根据方程输出假图像
:
fake_x = generator(c, z)
现在我们将真实图像 提供给鉴别器
,并得到图像为真实的概率。同时,我们还获得了对于真实图像的估计
:
D_logits_real, c_posterior_real = discriminator(x)
类似地,我们将假图像提供给鉴别器,并得到图像为真实的概率,以及对于假图像的估计 :
D_logits_fake, c_posterior_fake = discriminator(fake_x,reuse=True)
计算损失函数
现在我们将看到如何计算损失函数。
鉴别器损失
鉴别器损失如下:
作为 InfoGAN 的鉴别器损失与 CGAN 相同,实现鉴别器损失与我们在 CGAN 部分学到的相同:
#real loss
D_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logits_real,
labels=tf.ones(dtype=tf.float32, shape=[batch_size, 1])))
#fake loss
D_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logits_fake,
labels=tf.zeros(dtype=tf.float32, shape=[batch_size, 1])))
#final discriminator loss
D_loss = D_loss_real + D_loss_fake
生成器损失
生成器的损失函数如下所示:
生成器损失的实现为:
G_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logits_fake,
labels=tf.ones(dtype=tf.float32, shape=[batch_size, 1])))
互信息
我们从鉴别器和生成器的损失中减去互信息。因此,鉴别器和生成器的最终损失函数如下所示:
可以计算互信息如下:
首先,我们为 定义一个先验:
c_prior = 0.10 * tf.ones(dtype=tf.float32, shape=[batch_size, 10])
当 给定时,
的熵表示为
。我们知道熵的计算如下所示:
entropy_of_c = tf.reduce_mean(-tf.reduce_sum(c * tf.log(tf.clip_by_value(c_prior, 1e-12, 1.0)),axis=-1))
当给定 时,
的条件熵为
。条件熵的代码如下:
log_q_c_given_x = tf.reduce_mean(tf.reduce_sum(c * tf.log(tf.clip_by_value(c_posterior_fake, 1e-12, 1.0)), axis=-1))
互信息表示为 :
mutual_information = entropy_of_c + log_q_c_given_x
鉴别器和生成器的最终损失如下所示:
D_loss = D_loss - mutual_information
G_loss = G_loss - mutual_information
优化损失
现在我们需要优化我们的生成器和鉴别器。因此,我们收集鉴别器和生成器的参数分别为 和
:
training_vars = tf.trainable_variables()
theta_D = [var for var in training_vars if 'discriminator' in var.name]
theta_G = [var for var in training_vars if 'generator' in var.name]
使用 Adam 优化器优化损失:
learning_rate = 0.001
D_optimizer = tf.train.AdamOptimizer(learning_rate).minimize(D_loss,var_list = theta_D)
G_optimizer = tf.train.AdamOptimizer(learning_rate).minimize(G_loss, var_list = theta_G)
开始训练
定义批量大小和轮数,并初始化所有 TensorFlow 变量:
num_epochs = 100
session = tf.InteractiveSession()
session.run(tf.global_variables_initializer())
定义一个辅助函数来可视化结果:
def plot(c, x):
c_ = np.argmax(c, 1)
sort_indices = np.argsort(c_, 0)
x_reshape = np.reshape(x[sort_indices], [batch_size, 28, 28])
x_reshape = np.reshape( np.expand_dims(x_reshape, axis=0), [4, (batch_size // 4), 28, 28])
values = []
for i in range(0,4):
row = np.concatenate( [x_reshape[i,j,:,:] for j in range(0,(batch_size // 4))], axis=1)
values.append(row)
return np.concatenate(values, axis=0)
生成手写数字
开始训练并生成图像。每100
次迭代,我们打印生成器生成的图像:
onehot = np.eye(10)
for epoch in range(num_epochs):
for i in range(0, data.train.num_examples // batch_size):
样本图像:
x_batch, _ = data.train.next_batch(batch_size)
x_batch = np.reshape(x_batch, (batch_size, 28, 28, 1))
样本值 c:
c_ = np.random.randint(low=0, high=10, size=(batch_size,))
c_one_hot = onehot[c_]
样本噪声 z:
z_batch = np.random.uniform(low=-1.0, high=1.0, size=(batch_size,64))
优化生成器和鉴别器的损失:
feed_dict={x: x_batch, c: c_one_hot, z: z_batch, is_train: True}
_ = session.run(D_optimizer, feed_dict=feed_dict)
_ = session.run(G_optimizer, feed_dict=feed_dict)
每 100^(th) 次迭代打印生成器图像:
if i % 100 == 0:
discriminator_loss = D_loss.eval(feed_dict)
generator_loss = G_loss.eval(feed_dict)
_fake_x = fake_x.eval(feed_dict)
print("Epoch: {}, iteration: {}, Discriminator Loss:{}, Generator Loss: {}".format(epoch,i,discriminator_loss,generator_loss))
plt.imshow(plot(c_one_hot, _fake_x))
plt.show()
我们可以看到生成器在每次迭代中是如何发展和生成更好的数字的:
使用 CycleGAN 翻译图像
我们已经学习了几种类型的 GAN,并且它们的应用是无穷无尽的。我们已经看到生成器如何学习真实数据的分布并生成新的逼真样本。现在我们将看到一个非常不同且非常创新的 GAN 类型,称为CycleGAN。
不像其他 GAN,CycleGAN 将数据从一个域映射到另一个域,这意味着这里我们试图学习从一个域的图像分布到另一个域的图像分布的映射。简单地说,我们将图像从一个域翻译到另一个域。
这意味着什么?假设我们想要将灰度图像转换为彩色图像。灰度图像是一个域,彩色图像是另一个域。CycleGAN 学习这两个域之间的映射并在它们之间进行转换。这意味着给定一个灰度图像,CycleGAN 将其转换为彩色图像。
CycleGAN 的应用非常广泛,例如将真实照片转换为艺术图片、季节转换、照片增强等。如下图所示,您可以看到 CycleGAN 如何在不同域之间转换图像:
但 CycleGAN 有什么特别之处?它们能够在没有配对示例的情况下将图像从一个域转换到另一个域。假设我们要将照片(源)转换为绘画(目标)。在普通的图像到图像的转换中,我们如何做到这一点?我们通过收集一些照片及其对应的绘画,如下图所示,准备训练数据:
收集每个用例的这些配对数据点是一项昂贵的任务,我们可能没有太多记录或配对。这就是 CycleGAN 的最大优势所在。它不需要对齐的数据对。要将照片转换为绘画,我们只需一堆照片和一堆绘画。它们不需要进行映射或对齐。
如下图所示,我们有一些照片在一列,一些绘画在另一列;您可以看到它们并不互相配对。它们是完全不同的图像:
因此,要将图像从任何源域转换为目标域,我们只需一堆这两个域中的图像,而它们不需要成对。现在让我们看看它们是如何工作以及它们如何学习源与目标域之间的映射。
与其他生成对抗网络(GAN)不同,CycleGAN 包含两个生成器和两个判别器。我们用来代表源域中的图像,用
来代表目标域中的图像。我们需要学习
到
的映射关系。
假设我们要学习将真实图片转换为绘画
,如下图所示:
生成器的角色
我们有两个生成器,和
。
的作用是学习从
到
的映射。如上所述,
的作用是学习将照片转换为绘画,如下图所示:
它试图生成一个虚假的目标图像,这意味着它将一个源图像,,作为输入,并生成一个虚假的目标图像,
:
生成器的角色,,是学习从
到
的映射,并学会将绘画转化为真实图片,如下图所示:
它试图生成一个虚假的源图像,这意味着它将一个目标图像,,作为输入,并生成一个虚假的源图像,
:
判别器的角色
与生成器类似,我们有两个判别器,和
。判别器
的角色是区分真实源图像
和假源图像
。我们知道假源图像是由生成器
生成的。
给定一个图像给判别器,它返回图像为真实源图像的概率:
以下图显示了判别器,您可以观察到它将真实源图像 x 和生成器 F 生成的假源图像作为输入,并返回图像为真实源图像的概率:
判别器的角色是区分真实目标图像
和假目标图像
。我们知道假目标图像是由生成器
生成的。给定一个图像给判别器
,它返回图像为真实目标图像的概率:
以下图显示了判别器,您可以观察到它将真实目标图像
和生成器生成的假目标图像
作为输入,并返回图像为真实目标图像的概率:
损失函数
在 CycleGAN 中,我们有两个生成器和两个鉴别器。生成器学习将图像从一个域转换到另一个域,鉴别器试图区分转换后的图像。
因此,我们可以说鉴别器的损失函数 可以表示如下:
同样地,鉴别器 的损失函数可以表示如下:
生成器 的损失函数可以表示如下:
生成器 的损失函数可以表示如下:
总之,最终损失可以写成如下形式:
循环一致性损失
仅仅通过对抗损失并不能确保图像的正确映射。例如,生成器可以将源域中的图像映射到目标域中图像的随机排列,这可能匹配目标分布。
因此,为了避免这种情况,我们引入了一种额外的损失,称为循环一致性损失。它强制生成器 G 和 F 都是循环一致的。
让我们回顾一下生成器的功能:
-
生成器 G:将 x 转换为 y
-
生成器 F:将 y 转换为 x
我们知道生成器 G 接受源图像 x 并将其转换为虚假的目标图像 y。现在,如果我们将这个生成的虚假目标图像 y 输入生成器 F,它应该返回原始的源图像 x。有点混乱,对吧?
看看下面的图示;我们有一个源图像 x。首先,我们将这个图像输入生成器 G,它会返回一个虚假的目标图像 y。现在我们将这个虚假的目标图像 y 输入到生成器 F,它应该返回原始的源图像:
上述方程可以表示如下:
这被称为前向一致性损失,可以表示如下:
类似地,我们可以指定后向一致性损失,如下图所示。假设我们有一个原始的目标图像 y。我们将这个 y 输入鉴别器 F,它返回一个虚假的源图像 x。现在我们将这个生成的虚假源图像 x 输入生成器 G,它应该返回原始的目标图像 y:
前述方程可以表示为:
后向一致性损失可以表示如下:
因此,结合反向和正向一致性损失,我们可以将循环一致性损失写成:
我们希望我们的生成器保持循环一致,因此,我们将它们的损失与循环一致性损失相乘。因此,最终损失函数可以表示为:
使用 CycleGAN 将照片转换为绘画作品
现在我们将学习如何在 TensorFlow 中实现 CycleGAN。我们将看到如何使用 CycleGAN 将图片转换为绘画作品:
本节使用的数据集可以从 people.eecs.berkeley.edu/~taesung_park/CycleGAN/datasets/monet2photo.zip
下载。下载数据集后,请解压缩存档;它包括四个文件夹,trainA
、trainB
、testA
和 testB
,其中包含训练和测试图像。
trainA
文件夹包含绘画作品(莫奈),而 trainB
文件夹包含照片。由于我们将照片 (x) 映射到绘画作品 (y),所以 trainB
文件夹中的照片将作为我们的源图像,,而包含绘画作品的
trainA
将作为目标图像,。
CycleGAN 的完整代码及其逐步说明可作为 Jupyter Notebook 在 github.com/PacktPublishing/Hands-On-Deep-Learning-Algorithms-with-Python
中找到。
而不是查看整段代码,我们只会看 TensorFlow 中如何实现 CycleGAN 并将源图像映射到目标领域。您也可以在 github.com/PacktPublishing/Hands-On-Deep-Learning-Algorithms-with-Python
查看完整的代码。
定义 CycleGAN
类:
class CycleGAN:
def __init__(self):
定义输入占位符 X
和输出占位符 Y
:
self.X = tf.placeholder("float", shape=[batchsize, image_height, image_width, 3])
self.Y = tf.placeholder("float", shape=[batchsize, image_height, image_width, 3])
定义生成器,,将
映射到
:
G = generator("G")
定义生成器,,将
映射到
:
F = generator("F")
定义辨别器,,用于区分真实源图像和虚假源图像:
self.Dx = discriminator("Dx")
定义辨别器,,用于区分真实目标图像和虚假目标图像:
self.Dy = discriminator("Dy")
生成虚假源图像:
self.fake_X = F(self.Y)
生成虚假目标图像:
self.fake_Y = G(self.X)
获取 logits
:
#real source image logits
self.Dx_logits_real = self.Dx(self.X)
#fake source image logits
self.Dx_logits_fake = self.Dx(self.fake_X, True)
#real target image logits
self.Dy_logits_fake = self.Dy(self.fake_Y, True)
#fake target image logits
self.Dy_logits_real = self.Dy(self.Y)
我们知道循环一致性损失如下所示:
我们可以如下实现循环一致性损失:
self.cycle_loss = tf.reduce_mean(tf.abs(F(self.fake_Y, True) - self.X)) + \
tf.reduce_mean(tf.abs(G(self.fake_X, True) - self.Y))
定义我们两个鉴别器的损失, 和
。
我们可以如下重写带有 Wasserstein 距离的鉴别器损失函数:
因此,两个鉴别器的损失如下实现:
self.Dx_loss = -tf.reduce_mean(self.Dx_logits_real) + tf.reduce_mean(self.Dx_logits_fake)
self.Dy_loss = -tf.reduce_mean(self.Dy_logits_real) + tf.reduce_mean(self.Dy_logits_fake)
定义我们两个生成器的损失, 和
。我们可以如下重写带有 Wasserstein 距离的生成器损失函数:
因此,两个生成器的损失乘以循环一致性损失cycle_loss
如下实现:
self.G_loss = -tf.reduce_mean(self.Dy_logits_fake) + 10\. * self.cycle_loss
self.F_loss = -tf.reduce_mean(self.Dx_logits_fake) + 10\. * self.cycle_loss
使用 Adam 优化器优化鉴别器和生成器:
#optimize the discriminator
self.Dx_optimizer = tf.train.AdamOptimizer(2e-4, beta1=0., beta2=0.9).minimize(self.Dx_loss, var_list=[self.Dx.var])
self.Dy_optimizer = tf.train.AdamOptimizer(2e-4, beta1=0., beta2=0.9).minimize(self.Dy_loss, var_list=[self.Dy.var])
#optimize the generator
self.G_optimizer = tf.train.AdamOptimizer(2e-4, beta1=0., beta2=0.9).minimize(self.G_loss, var_list=[G.var])
self.F_optimizer = tf.train.AdamOptimizer(2e-4, beta1=0., beta2=0.9).minimize(self.F_loss, var_list=[F.var])
一旦我们开始训练模型,我们可以看到鉴别器和生成器的损失随迭代次数而减小:
Epoch: 0, iteration: 0, Dx Loss: -0.6229429245, Dy Loss: -2.42867970467, G Loss: 1385.33557129, F Loss: 1383.81530762, Cycle Loss: 138.448059082
Epoch: 0, iteration: 50, Dx Loss: -6.46077537537, Dy Loss: -7.29514217377, G Loss: 629.768066406, F Loss: 615.080932617, Cycle Loss: 62.6807098389
Epoch: 1, iteration: 100, Dx Loss: -16.5891685486, Dy Loss: -16.0576553345, G Loss: 645.53137207, F Loss: 649.854919434, Cycle Loss: 63.9096908569
StackGAN
现在我们将看到一种最引人入胜和迷人的 GAN 类型,称为StackGAN。你能相信如果我说 StackGANs 可以根据文本描述生成逼真的图像吗?是的,他们可以。给定一个文本描述,他们可以生成一个逼真的图像。
让我们首先了解艺术家如何画出一幅图像。在第一阶段,艺术家绘制原始形状,创建图像的基本轮廓,形成图像的初始版本。在接下来的阶段,他们通过使图像更真实和吸引人来增强图像。
StackGANs 以类似的方式工作。它们将生成图像的过程分为两个阶段。就像艺术家画图一样,在第一阶段,他们生成基础轮廓、原始形状,并创建图像的低分辨率版本;在第二阶段,他们通过使图像更真实和吸引人来增强第一阶段生成的图像,并将其转换为高分辨率图像。
但是 StackGANs 是如何做到的呢?
他们使用两个 GAN,每个阶段一个。第一阶段的 GAN 生成基础图像,并将其发送到下一阶段的 GAN,后者将基础低分辨率图像转换为合适的高分辨率图像。下图显示了 StackGANs 如何基于文本描述在每个阶段生成图像:
来源:https://arxiv.org/pdf/1612.03242.pdf
正如你所见,在第一阶段,我们有图像的低分辨率版本,但在第二阶段,我们有清晰的高分辨率图像。但是,StackGAN 是如何做到的呢?记住,当我们用条件 GAN 学习时,我们可以通过条件化使我们的 GAN 生成我们想要的图像。
我们在两个阶段都使用它们。在第一阶段,我们的网络基于文本描述进行条件设定。根据这些文本描述,它们生成图像的基本版本。在第二阶段,我们的网络基于从第一阶段生成的图像和文本描述进行条件设定。
但为什么我们在第二阶段又必须要基于文本描述进行条件设定呢?因为在第一阶段,我们错过了文本描述中指定的一些细节,只创建了图像的基本版本。所以,在第二阶段,我们再次基于文本描述进行条件设定,修复缺失的信息,并且使我们的图像更加真实。
凭借这种仅基于文本生成图片的能力,它被广泛应用于许多应用场景。尤其在娱乐行业中大量使用,例如根据描述创建帧,还可以用于生成漫画等等。
StackGAN 的架构
现在我们对 StackGAN 的工作原理有了基本的了解,我们将更详细地查看它们的架构,看看它们如何从文本生成图片。
StackGAN 的完整架构如下图所示:
来源:https://arxiv.org/pdf/1612.03242.pdf
我们将逐个查看每个组件。
条件增强
我们将文本描述作为 GAN 的输入。基于这些描述,它必须生成图像。但它们如何理解文本的含义以生成图片呢?
首先,我们使用编码器将文本转换为嵌入。我们用 表示这个文本嵌入。我们能够创建
的变化吗?通过创建文本嵌入的变化,我们可以获得额外的训练对,并增加对小扰动的鲁棒性。
让 表示均值,
表示我们文本嵌入的对角协方差矩阵,
。现在我们从独立的高斯分布中随机采样一个额外的条件变量,
,帮助我们创建具有其含义的文本描述的变化。我们知道同样的文本可以用多种方式书写,因此通过条件变量
,我们可以得到映射到图像的文本的各种版本。
因此,一旦我们有了文本描述,我们将使用编码器提取它们的嵌入,然后计算它们的均值和协方差。然后,我们从文本嵌入的高斯分布中采样 。
第一阶段
现在,我们有一个文本嵌入,,以及一个条件变量,
。我们将看看它是如何被用来生成图像的基本版本。
生成器
生成器的目标是通过学习真实数据分布来生成虚假图像。首先,我们从高斯分布中采样噪声并创建z。然后,我们将z与我们的条件变量,,连接起来,并将其作为输入提供给生成器,生成图像的基本版本。
生成器的损失函数如下:
让我们来看一下这个公式:
-
表示我们从虚假数据分布中采样z,也就是先验噪声。
-
表示我们从真实数据分布中采样文本描述,
。
-
表示生成器接受噪声和条件变量返回图像。我们将这个生成的图像馈送给鉴别器。
-
表示生成的图像为虚假的对数概率。
除了这个损失之外,我们还向损失函数中添加了一个正则化项,,这意味着标准高斯分布和条件高斯分布之间的 KL 散度。这有助于我们避免过拟合。
因此,生成器的最终损失函数如下:
鉴别器
现在我们将这个生成的图像馈送给鉴别器,它返回图像为真实的概率。鉴别器的损失如下:
在这里:
-
表示真实图像,
,条件于文本描述,
-
表示生成的虚假图像
第二阶段
我们已经学习了第一阶段如何生成图像的基本版本。现在,在第二阶段,我们修复了第一阶段生成的图像的缺陷,并生成了更真实的图像版本。我们用来条件化网络的图像来自前一个阶段生成的图像,还有文本嵌入。
生成器
在第二阶段,生成器不再接受噪声作为输入,而是接受从前一阶段生成的图像作为输入,并且以文本描述作为条件。
这里, 意味着我们从阶段 I 生成的图像进行采样。
意味着我们从给定的真实数据分布
中抽样文本。
随后,生成器的损失可以如下给出:
除了正则化器外,生成器的损失函数为:
鉴别器
判别器的目标是告诉我们图像来自真实分布还是生成器分布。因此,判别器的损失函数如下:
摘要
我们从学习条件 GAN 开始这一章,了解了它们如何用于生成我们感兴趣的图像。
后来,我们学习了 InfoGAN,其中代码 c 是根据生成的输出自动推断的,不像 CGAN 那样我们需要显式指定 c。要推断 c,我们需要找到后验分布 ,而我们无法访问它。因此,我们使用辅助分布。我们使用互信息来最大化
,以增强对 c 在给定生成器输出的知识。
接着,我们学习了 CycleGAN,它将数据从一个域映射到另一个域。我们尝试学习将照片域图像分布映射到绘画域图像分布的映射关系。最后,我们了解了 StackGANs 如何从文本描述生成逼真的图像。
在下一章中,我们将学习自编码器及其类型。
问题
回答以下问题,以评估你从本章学到了多少内容:
-
条件 GAN 和普通 GAN 有何不同?
-
InfoGAN 中的代码称为什么?
-
什么是互信息?
-
为什么 InfoGAN 需要辅助分布?
-
什么是循环一致性损失?
-
解释 CycleGAN 中生成器的作用。
-
StackGANs 如何将文本描述转换为图片?
进一步阅读
更多信息,请参考以下链接:
-
条件生成对抗网络,作者为 Mehdi Mirza 和 Simon Osindero,
arxiv.org/pdf/1411.1784.pdf
-
InfoGAN: 通过最大化信息的生成对抗网络进行可解释表示学习,作者为陈希等人,
arxiv.org/pdf/1606.03657.pdf
-
利用循环一致性对抗网络进行非配对图像到图像翻译,作者为朱俊彦等人,
arxiv.org/pdf/1703.10593.pdf
第十章:使用自编码器重建输入
自编码器是一种无监督学习算法。与其他算法不同,自编码器学习重建输入,即自编码器接收输入并学习将其重现为输出。我们从理解什么是自编码器及其如何重建输入开始这一章节。然后,我们将学习自编码器如何重建 MNIST 图像。
接下来,我们将了解自编码器的不同变体;首先,我们将学习使用卷积层的卷积自编码器(CAEs);然后,我们将学习去噪自编码器(DAEs),它们学习如何去除输入中的噪音。之后,我们将了解稀疏自编码器及其如何从稀疏输入中学习。在本章末尾,我们将学习一种有趣的生成型自编码器,称为变分自编码器。我们将理解变分自编码器如何学习生成新的输入及其与其他自编码器的不同之处。
在本章中,我们将涵盖以下主题:
-
自编码器及其架构
-
使用自编码器重建 MNIST 图像
-
卷积自编码器
-
构建卷积自编码器
-
降噪自编码器
-
使用降噪自编码器去除图像中的噪音
-
稀疏自编码器
-
紧束缚自编码器
-
变分自编码器
什么是自编码器?
自编码器是一种有趣的无监督学习算法。与其他神经网络不同,自编码器的目标是重建给定的输入;即自编码器的输出与输入相同。它由称为编码器和解码器的两个重要组件组成。
编码器的作用是通过学习输入的潜在表示来编码输入,解码器的作用是从编码器生成的潜在表示中重建输入。潜在表示也称为瓶颈或编码。如下图所示,将图像作为输入传递给自编码器。编码器获取图像并学习图像的潜在表示。解码器获取潜在表示并尝试重建图像:
下图显示了一个简单的双层香草自编码器;正如您可能注意到的,它由输入层、隐藏层和输出层组成。首先,我们将输入馈送到输入层,然后编码器学习输入的表示并将其映射到瓶颈。从瓶颈处,解码器重建输入:
我们可能会想知道这的用途是什么。为什么我们需要编码和解码输入?为什么我们只需重建输入?嗯,有各种应用,如降维、数据压缩、图像去噪等。
由于自编码器重构输入,输入层和输出层中的节点数始终相同。假设我们有一个包含 100 个输入特征的数据集,我们有一个神经网络,其输入层有 100 个单元,隐藏层有 50 个单元,输出层有 100 个单元。当我们将数据集输入到自编码器中时,编码器尝试学习数据集中的重要特征,并将特征数减少到 50 并形成瓶颈。瓶颈保存数据的表示,即数据的嵌入,并仅包含必要信息。然后,将瓶颈输入到解码器中以重构原始输入。如果解码器成功地重构了原始输入,那么说明编码器成功地学习了给定输入的编码或表示。也就是说,编码器成功地将包含 100 个特征的数据集编码成仅包含 50 个特征的表示,通过捕获必要信息。
因此,编码器本质上尝试学习如何在不丢失有用信息的情况下减少数据的维度。我们可以将自编码器视为类似于主成分分析(PCA)的降维技术。在 PCA 中,我们通过线性变换将数据投影到低维,并去除不需要的特征。PCA 和自编码器的区别在于 PCA 使用线性变换进行降维,而自编码器使用非线性变换。
除了降维之外,自编码器还广泛用于去噪图像、音频等中的噪声。我们知道自编码器中的编码器通过仅学习必要信息并形成瓶颈或代码来减少数据集的维度。因此,当噪声图像作为自编码器的输入时,编码器仅学习图像的必要信息并形成瓶颈。由于编码器仅学习表示图像的重要和必要信息,它学习到噪声是不需要的信息并从瓶颈中移除噪声的表示。
因此,现在我们将会有一个瓶颈,也就是说,一个没有任何噪声信息的图像的表示。当编码器学习到这个被称为瓶颈的编码表示时,将其输入到解码器中,解码器会从编码器生成的编码中重建输入图像。由于编码没有噪声,重建的图像将不包含任何噪声。
简而言之,自编码器将我们的高维数据映射到一个低级表示。这种数据的低级表示被称为潜在表示或瓶颈,只包含代表输入的有意义和重要特征。
由于我们的自编码器的角色是重建其输入,我们使用重构误差作为我们的损失函数,这意味着我们试图了解解码器正确重构输入的程度。因此,我们可以使用均方误差损失作为我们的损失函数来量化自编码器的性能。
现在我们已经了解了什么是自编码器,我们将在下一节探讨自编码器的架构。
理解自编码器的架构
正如我们刚刚学到的,自编码器由两个重要组件组成:编码器 和解码器
。让我们仔细看看它们各自的作用:
- 编码器:编码器
学习输入并返回输入的潜在表示。假设我们有一个输入,
。当我们将输入馈送给编码器时,它返回输入的低维潜在表示,称为编码或瓶颈,
。我们用
表示编码器的参数:
- 解码器:解码器
尝试使用编码器的输出,即编码
作为输入来重建原始输入
。重建图像由
表示。我们用
表示解码器的参数:
我们需要学习编码器和解码器的优化参数,分别表示为 和
,以便我们可以最小化重构损失。我们可以定义我们的损失函数为实际输入与重构输入之间的均方误差:
这里, 是训练样本的数量。
当潜在表示的维度小于输入时,这被称为欠完备自编码器。由于维度较小,欠完备自编码器试图学习和保留输入的仅有的有用和重要特征,并去除其余部分。当潜在表示的维度大于或等于输入时,自编码器将只是复制输入而不学习任何有用的特征,这种类型的自编码器称为过完备自编码器。
下图显示了欠完备和过完备自编码器。欠完备自编码器在隐藏层(code
)中的神经元少于输入层中的单元数;而在过完备自编码器中,隐藏层(code
)中的神经元数大于输入层中的单元数:
因此,通过限制隐藏层(code
)中的神经元,我们可以学习输入的有用表示。自编码器也可以有任意数量的隐藏层。具有多个隐藏层的自编码器称为多层自编码器或深层自编码器。到目前为止,我们所学到的只是普通或浅层自编码器。
使用自编码器重构 MNIST 图像
现在我们将学习如何使用 MNIST 数据集重构手写数字的自编码器。首先,让我们导入必要的库:
import warnings
warnings.filterwarnings('ignore')
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Dense
tf.logging.set_verbosity(tf.logging.ERROR)
#plotting
import matplotlib.pyplot as plt
%matplotlib inline
#dataset
from tensorflow.keras.datasets import mnist
准备数据集
让我们加载 MNIST 数据集。由于我们正在重建给定的输入,所以不需要标签。因此,我们只加载 x_train
用于训练和 x_test
用于测试:
(x_train, _), (x_test, _) = mnist.load_data()
通过除以最大像素值 255
来对数据进行归一化:
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255
打印我们数据集的 shape
:
print(x_train.shape, x_test.shape)
((60000, 28, 28), (10000, 28, 28))
将图像重塑为二维数组:
x_train = x_train.reshape((len(x_train), np.prod(x_train.shape[1:])))
x_test = x_test.reshape((len(x_test), np.prod(x_test.shape[1:])))
现在,数据的形状将变为如下所示:
print(x_train.shape, x_test.shape)
((60000, 784), (10000, 784))
定义编码器
现在我们定义编码器层,它将图像作为输入并返回编码。
定义编码的大小:
encoding_dim = 32
定义输入的占位符:
input_image = Input(shape=(784,))
定义编码器,它接受 input_image
并返回编码:
encoder = Dense(encoding_dim, activation='relu')(input_image)
定义解码器
让我们定义解码器,它从编码器中获取编码值并返回重构的图像:
decoder = Dense(784, activation='sigmoid')(encoder)
构建模型
现在我们定义了编码器和解码器,我们定义一个模型,该模型接受图像作为输入,并返回解码器的输出,即重构图像:
model = Model(inputs=input_image, outputs=decoder)
让我们看看模型的摘要:
model.summary()
________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 784) 0
_________________________________________________________________
dense (Dense) (None, 32) 25120
_________________________________________________________________
dense_1 (Dense) (None, 784) 25872
=================================================================
Total params: 50,992
Trainable params: 50,992
Non-trainable params: 0
_________________________________________________________________
使用二元交叉熵作为损失编译模型,并使用 adadelta
优化器最小化损失:
model.compile(optimizer='adadelta', loss='binary_crossentropy')
现在让我们训练模型。
通常,我们按 model.fit(x,y)
训练模型,其中 x
是输入,y
是标签。但由于自编码器重构它们的输入,模型的输入和输出应该相同。因此,在这里,我们按 model.fit(x_train, x_train)
训练模型:
model.fit(x_train, x_train, epochs=50, batch_size=256, shuffle=True, validation_data=(x_test, x_test))
重构图像
现在我们已经训练了模型,我们看看模型如何重构测试集的图像。将测试图像输入模型并获取重构的图像:
reconstructed_images = model.predict(x_test)
绘制重构图像
首先,让我们绘制实际图像,即输入图像:
n = 7
plt.figure(figsize=(20, 4))
for i in range(n):
ax = plt.subplot(1, n, i+1)
plt.imshow(x_test[i].reshape(28, 28))
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()
实际图像的绘制如下所示:
绘制重构图像如下所示:
n = 7
plt.figure(figsize=(20, 4))
for i in range(n):
ax = plt.subplot(2, n, i + n + 1)
plt.imshow(reconstructed_images[i].reshape(28, 28))
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()
以下显示了重构后的图像:
如您所见,自编码器已经学习了输入图像的更好表示并对其进行了重构。
使用卷积的自编码器
我们刚刚在前一节学习了什么是自编码器。我们了解了传统自编码器,它基本上是具有一个隐藏层的前馈浅网络。我们可以不将它们保持为前馈网络,而是可以将它们作为卷积网络使用吗?由于我们知道卷积网络在分类和识别图像方面表现良好(前提是在自编码器中使用卷积层而不是前馈层),当输入是图像时,它将学习更好地重建输入。
因此,我们介绍一种新类型的自编码器称为 CAE,它使用卷积网络而不是传统的神经网络。在传统的自编码器中,编码器和解码器基本上是一个前馈网络。但在 CAE 中,它们基本上是卷积网络。这意味着编码器由卷积层组成,解码器由转置卷积层组成,而不是前馈网络。CAE 如下图所示:
如图所示,我们将输入图像提供给编码器,编码器由卷积层组成,卷积层执行卷积操作并从图像中提取重要特征。然后我们执行最大池化以仅保留图像的重要特征。以类似的方式,我们执行多个卷积和最大池化操作,并获得图像的潜在表示,称为瓶颈。
接下来,我们将瓶颈输入解码器,解码器由反卷积层组成,反卷积层执行反卷积操作并试图从瓶颈中重建图像。它包括多个反卷积和上采样操作以重建原始图像。
因此,这就是 CAE 如何在编码器中使用卷积层和在解码器中使用转置卷积层来重建图像的方法。
构建卷积自编码器
就像我们在前一节学习如何实现自编码器一样,实现 CAE 也是一样的,唯一的区别是这里我们在编码器和解码器中使用卷积层,而不是前馈网络。我们将使用相同的 MNIST 数据集来使用 CAE 重建图像。
导入库:
import warnings
warnings.filterwarnings('ignore')
#modelling
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Dense, Conv2D, MaxPooling2D, UpSampling2D
from tensorflow.keras import backend as K
#plotting
import matplotlib.pyplot as plt
%matplotlib inline
#dataset
from keras.datasets import mnist
import numpy as np
读取并重塑数据集:
(x_train, _), (x_test, _) = mnist.load_data()
# Normalize the dataset
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
# reshape
x_train = np.reshape(x_train, (len(x_train), 28, 28, 1))
x_test = np.reshape(x_test, (len(x_test), 28, 28, 1))
让我们定义我们输入图像的形状:
input_image = Input(shape=(28, 28, 1))
定义编码器
现在,让我们定义我们的编码器。与传统自编码器不同,在这里我们使用卷积网络而不是前馈网络。因此,我们的编码器包括三个卷积层,后跟具有relu
激活函数的最大池化层。
定义第一个卷积层,然后进行最大池化操作:
x = Conv2D(16, (3, 3), activation='relu', padding='same')(input_image)
x = MaxPooling2D((2, 2), padding='same')(x)
定义第二个卷积和最大池化层:
x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
x = MaxPooling2D((2, 2), padding='same')(x)
定义最终的卷积和最大池化层:
x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
encoder = MaxPooling2D((2, 2), padding='same')(x)
定义解码器
现在,我们定义我们的解码器;在解码器中,我们执行三层反卷积操作,即对编码器创建的编码进行上采样并重建原始图像。
定义第一个卷积层,并进行上采样:
x = Conv2D(8, (3, 3), activation='relu', padding='same')(encoder)
x = UpSampling2D((2, 2))(x)
定义第二个卷积层,并进行上采样:
x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
x = UpSampling2D((2, 2))(x)
定义最终的卷积层并进行上采样:
x = Conv2D(16, (3, 3), activation='relu')(x)
x = UpSampling2D((2, 2))(x)
decoded = Conv2D(1, (3, 3), activation='sigmoid', padding='same')(x)
构建模型
定义接收输入图像并返回解码器生成的图像(即重建图像)的模型:
model = Model(input_image, decoder)
让我们使用二进制交叉熵作为损失来编译模型,并使用adadelta
作为优化器:
model.compile(optimizer='adadelta', loss='binary_crossentropy')
接下来,按以下方式训练模型:
model.fit(x_train, x_train, epochs=50,batch_size=128, shuffle=True, validation_data=(x_test, x_test))
重建图像
使用我们训练好的模型重建图像:
reconstructed_images = model.predict(x_test)
首先,让我们绘制输入图像:
n = 7
plt.figure(figsize=(20, 4))
for i in range(n):
ax = plt.subplot(1, n, i+1)
plt.imshow(x_test[i].reshape(28, 28))
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()
输入图像的绘图如下所示:
现在,我们绘制重建的图像:
n = 7
plt.figure(figsize=(20, 4))
for i in range(n):
ax = plt.subplot(2, n, i + n + 1)
plt.imshow(reconstructed_images[i].reshape(28, 28))
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()
重建图像的绘图如下所示:
探索去噪自编码器
去噪自编码器(DAE)是自编码器的另一种小变体。它们主要用于去除图像、音频和其他输入中的噪音。因此,当我们将破坏的输入提供给 DAE 时,它学会重建原始未破坏的输入。现在我们来看看 DAE 如何去除噪音。
使用 DAE 时,我们不是直接将原始输入馈送给自编码器,而是通过添加一些随机噪音来破坏输入,然后再馈送破坏的输入。我们知道编码器通过仅保留重要信息来学习输入的表示,并将压缩表示映射到瓶颈。当破坏的输入被送到编码器时,编码器将学习到噪音是不需要的信息,并且移除其表示。因此,编码器通过仅保留必要信息来学习无噪音的输入的紧凑表示,并将学习到的表示映射到瓶颈。
现在解码器尝试使用由编码器学习到的表示重建图像,也就是瓶颈。由于该表示不包含任何噪音,解码器在没有噪音的情况下重建输入。这就是去噪自编码器从输入中去除噪音的方式。
典型的 DAE 如下图所示。首先,我们通过添加一些噪音来破坏输入,然后将破坏的输入提供给编码器,编码器学习到去除噪音的输入的表示,而解码器使用编码器学习到的表示重建未破坏的输入:
数学上,这可以表示如下。
假设我们有一张图片,,我们向图片添加噪声后得到
,这是被破坏的图片:
现在将此破坏的图像馈送给编码器:
解码器尝试重建实际图像:
使用 DAE 进行图像去噪
在本节中,我们将学习如何使用 DAE 对图像进行去噪。我们使用 CAE 来对图像进行去噪。DAE 的代码与 CAE 完全相同,只是这里我们在输入中使用了嘈杂的图像。而不是查看整个代码,我们只会看到相应的更改。完整的代码可以在 GitHub 上查看:github.com/PacktPublishing/Hands-On-Deep-Learning-Algorithms-with-Python
。
设置噪声因子:
noise_factor = 1
向训练和测试图像添加噪声:
x_train_noisy = x_train + noise_factor * np.random.normal(loc=0.0, scale=1.0, size=x_train.shape)
x_test_noisy = x_test + noise_factor * np.random.normal(loc=0.0, scale=1.0, size=x_test.shape)
将训练集和测试集裁剪为 0 和 1:
x_train_noisy = np.clip(x_train_noisy, 0., 1.)
x_test_noisy = np.clip(x_test_noisy, 0., 1.)
让我们训练模型。由于我们希望模型学会去除图像中的噪声,因此模型的输入是嘈杂的图像,即 x_train_noisy
,输出是去噪后的图像,即 x_train
:
model.fit(x_train_noisy, x_train, epochs=50,batch_size=128, shuffle=True, validation_data=(x_test_noisy, x_test))
使用我们训练好的模型重建图像:
reconstructed_images = model.predict(x_test_noisy)
首先,让我们绘制输入图像,即损坏的图像:
n = 7
plt.figure(figsize=(20, 4))
for i in range(n):
ax = plt.subplot(1, n, i+1)
plt.imshow(x_test_noisy[i].reshape(28, 28))
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()
下图显示了输入嘈杂图像的绘图:
现在,让我们绘制模型重建的图像:
n = 7
plt.figure(figsize=(20, 4))
for i in range(n):
ax = plt.subplot(2, n, i + n + 1)
plt.imshow(reconstructed_images[i].reshape(28, 28))
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()
如您所见,我们的模型已经学会从图像中去除噪声:
理解稀疏自编码器
我们知道自编码器学习重建输入。但是当我们设置隐藏层中的节点数大于输入层中的节点数时,它将学习一个恒等函数,这是不利的,因为它只是完全复制输入。
在隐藏层中增加更多节点有助于学习稳健的潜在表示。但是当隐藏层中的节点更多时,自编码器会试图完全模仿输入,从而过度拟合训练数据。为了解决过拟合问题,我们在损失函数中引入了一个称为稀疏约束或稀疏惩罚的新约束。带有稀疏惩罚的损失函数可以表示如下:
第一项 表示原始输入
与重建输入
之间的重构误差。第二项表示稀疏约束。现在我们将探讨这种稀疏约束如何缓解过拟合问题。
通过稀疏约束,我们仅激活隐藏层中特定的神经元,而不是激活所有神经元。根据输入,我们激活和取消激活特定的神经元,因此当这些神经元被激活时,它们将学会从输入中提取重要特征。通过施加稀疏惩罚,自编码器不会精确复制输入到输出,并且它还可以学习到稳健的潜在表示。
如下图所示,稀疏自编码器的隐藏层单元数比输入层多;然而,只有少数隐藏层中的神经元被激活。未阴影的神经元表示当前激活的神经元:
如果神经元活跃则返回 1,非活跃则返回 0。在稀疏自编码器中,我们将大多数隐藏层中的神经元设置为非活跃状态。我们知道 sigmoid 激活函数将值压缩到 0 到 1 之间。因此,当我们使用 sigmoid 激活函数时,我们尝试将神经元的值保持接近于 0。
我们通常试图保持隐藏层中每个神经元的平均激活值接近于零,比如 0.05,但不等于零,这个值被称为 ,即我们的稀疏参数。我们通常将
的值设为 0.05。
首先,我们计算神经元的平均激活值。
在整个训练集上,隐藏层中 神经元的平均激活可以计算如下:
在这里,以下内容成立:
-
表示隐藏层中
神经元的平均激活
-
是训练样本的编号
-
是隐藏层中
神经元的激活
-
是稀疏自编码器的训练样本
-
表示隐藏层中 ![] 神经元对于第 ![] 个训练样本的激活
我们努力使神经元的平均激活值 接近于
。也就是说,我们尝试保持神经元的平均激活值接近于 0.05:
因此,我们对值 进行惩罚,其变化范围为
。我们知道Kullback-Leibler(KL)散度广泛用于衡量两个概率分布之间的差异。因此,在这里,我们使用 KL 散度来衡量两个伯努利分布,即平均
和平均
,可以表示如下:
在之前的方程中,表示隐藏层
,
表示隐藏层
中的神经元。前述方程基本上是稀疏惩罚或稀疏性约束。因此,通过稀疏约束,所有神经元永远不会同时活动,并且平均而言,它们被设置为 0.05。
现在我们可以根据稀疏惩罚重新编写损失函数,如下所示:
因此,稀疏自编码器允许我们在隐藏层中拥有比输入层更多的节点,然而通过损失函数中的稀疏约束来减少过拟合问题。
构建稀疏自编码器
构建稀疏自编码器与构建常规自编码器相同,只是在编码器和解码器中使用稀疏正则化器,因此在下面的部分中我们只会看到与实现稀疏正则化器相关的部分;完整的代码及解释可以在 GitHub 上找到。
定义稀疏正则化器
下面是定义稀疏正则化器的代码:
def sparse_regularizer(activation_matrix):
将我们的 值设为
0.05
:
rho = 0.05
计算 ,即平均激活值:
rho_hat = K.mean(activation_matrix)
根据方程(1)计算平均 和平均
之间的 KL 散度:
KL_divergence = K.sum(rho*(K.log(rho/rho_hat)) + (1-rho)*(K.log(1-rho/1-rho_hat)))
求和 KL 散度值:
sum = K.sum(KL_divergence)
将sum
乘以beta
并返回结果:
return beta * sum
稀疏正则化器的整个函数定义如下:
def sparse_regularizer(activation_matrix):
p = 0.01
beta = 3
p_hat = K.mean(activation_matrix)
KL_divergence = p*(K.log(p/p_hat)) + (1-p)*(K.log(1-p/1-p_hat))
sum = K.sum(KL_divergence)
return beta * sum
学习使用收缩自编码器
类似于稀疏自编码器,收缩自编码器在自编码器的损失函数中添加了新的正则化项。它们试图使我们的编码对训练数据中的小变化不那么敏感。因此,使用收缩自编码器,我们的编码变得更加稳健,对于训练数据中存在的噪声等小扰动更加鲁棒。我们现在引入一个称为正则化器或惩罚项的新术语到我们的损失函数中。它有助于惩罚对输入过于敏感的表示。
我们的损失函数可以用数学方式表示如下:
第一项表示重构误差,第二项表示惩罚项或正则化器,基本上是雅可比矩阵的Frobenius 范数。等等!这是什么意思?
矩阵的 Frobenius 范数,也称为Hilbert-Schmidt 范数,定义为其元素的绝对值平方和的平方根。由向量值函数的偏导数组成的矩阵称为雅可比矩阵。
因此,计算雅可比矩阵的 Frobenius 范数意味着我们的惩罚项是隐藏层对输入的所有偏导数的平方和。其表示如下:
计算隐藏层对输入的偏导数类似于计算损失的梯度。假设我们使用 sigmoid 激活函数,则隐藏层对输入的偏导数表示如下:
将惩罚项添加到我们的损失函数中有助于减少模型对输入变化的敏感性,并使我们的模型更加鲁棒,能够抵抗异常值。因此,收缩自编码器减少了模型对训练数据中小变化的敏感性。
实现收缩自编码器
建立收缩自编码器与建立普通自编码器几乎相同,只是在模型中使用了收缩损失正则化器,因此我们将只查看与实现收缩损失相关的部分,而不是整个代码。
定义收缩损失
现在让我们看看如何在 Python 中定义损失函数。
定义均方损失如下:
MSE = K.mean(K.square(actual - predicted), axis=1)
从我们的编码器层获取权重并转置权重:
weights = K.variable(value=model.get_layer('encoder_layer').get_weights()[0])
weights = K.transpose(weights)
获取我们的编码器层的输出:
h = model.get_layer('encoder_layer').output
定义惩罚项:
penalty_term = K.sum(((h * (1 - h))**2) * K.sum(weights**2, axis=1), axis=1)
最终损失是均方误差和乘以lambda
的惩罚项的总和:
Loss = MSE + (lambda * penalty_term)
收缩损失的完整代码如下所示:
def contractive_loss(y_pred, y_true):
lamda = 1e-4
MSE = K.mean(K.square(y_true - y_pred), axis=1)
weights = K.variable(value=model.get_layer('encoder_layer').get_weights()[0])
weights = K.transpose(weights)
h = model.get_layer('encoder_layer').output
penalty_term = K.sum(((h * (1 - h))**2) * K.sum(weights**2, axis=1), axis=1)
Loss = MSE + (lambda * penalty_term)
return Loss
解剖变分自编码器
现在我们将看到另一种非常有趣的自编码器类型,称为变分自编码器(VAE)。与其他自编码器不同,VAE 是生成模型,意味着它们学习生成新数据,就像 GANs 一样。
假设我们有一个包含许多个体面部图像的数据集。当我们用这个数据集训练我们的变分自编码器时,它学会了生成新的逼真面部图像,这些图像在数据集中没有见过。由于其生成性质,变分自编码器有各种应用,包括生成图像、歌曲等。但是,什么使变分自编码器具有生成性质,它与其他自编码器有何不同?让我们在接下来的部分中学习。
正如我们在讨论 GAN 时学到的那样,要使模型具有生成性,它必须学习输入的分布。例如,假设我们有一个包含手写数字的数据集,如 MNIST 数据集。现在,为了生成新的手写数字,我们的模型必须学习数据集中数字的分布。学习数据集中数字的分布有助于 VAE 学习有用的属性,如数字的宽度、笔画、高度等。一旦模型在其分布中编码了这些属性,那么它就可以通过从学习到的分布中抽样来生成新的手写数字。
假设我们有一个包含人脸数据的数据集,那么学习数据集中人脸的分布有助于我们学习各种属性,如性别、面部表情、发色等。一旦模型学习并在其分布中编码了这些属性,那么它就可以通过从学习到的分布中抽样来生成新的人脸。
因此,在变分自编码器中,我们不直接将编码器的编码映射到潜在向量(瓶颈),而是将编码映射到一个分布中;通常是高斯分布。我们从这个分布中抽样一个潜在向量,然后将其馈送给解码器来重构图像。如下图所示,编码器将其编码映射到一个分布中,我们从该分布中抽样一个潜在向量,并将其馈送给解码器来重构图像:
高斯分布可以通过其均值和协方差矩阵来参数化。因此,我们可以让我们的编码器生成其编码,并将其映射到一个接近高斯分布的均值向量和标准差向量。现在,从这个分布中,我们抽样一个潜在向量并将其馈送给我们的解码器,解码器然后重构图像:
简而言之,编码器学习给定输入的理想属性,并将其编码成分布。我们从该分布中抽样一个潜在向量,并将潜在向量作为输入馈送给解码器,解码器然后生成从编码器分布中学习的图像。
在变分自编码器中,编码器也称为识别模型,解码器也称为生成模型。现在我们对变分自编码器有了直观的理解,接下来的部分中,我们将详细了解变分自编码器的工作原理。
变分推断
在继续之前,让我们熟悉一下符号:
-
让我们用
来表示输入数据集的分布,其中
表示在训练过程中将学习的网络参数。
-
我们用
表示潜在变量,通过从分布中采样来编码输入的所有属性。
-
表示输入
及其属性的联合分布,
。
-
表示潜在变量的分布。
使用贝叶斯定理,我们可以写出以下内容:
前述方程帮助我们计算输入数据集的概率分布。但问题在于计算 ,因为其计算是不可解的。因此,我们需要找到一种可行的方法来估计
。在这里,我们介绍一种称为变分推断的概念。
不直接推断 的分布,我们用另一个分布(例如高斯分布
)来近似它们。也就是说,我们使用
,这基本上是由
参数化的神经网络,来估计
的值:
-
基本上是我们的概率编码器;即,它们用来创建给定
的潜在向量 z。
-
是概率解码器;也就是说,它试图构建给定潜在向量
的输入
。
下图帮助你更好地理解符号及我们迄今为止看到的内容:
损失函数
我们刚刚学到,我们使用 来近似
。因此,
的估计值应接近
。由于这两者都是分布,我们使用 KL 散度来衡量
与
的差异,并且我们需要将这种差异最小化。
和
之间的 KL 散度如下所示:
由于我们知道,将其代入前述方程中,我们可以写出以下内容:
由于我们知道log (a/b) = log(a) - log(b),我们可以将前述方程重写为:
我们可以将从期望值中取出,因为它不依赖于
:
由于我们知道log(ab) = log (a) + log(b),我们可以将前述方程重写为:
我们知道和
之间的 KL 散度可以表示为:
将方程(2)代入方程(1)我们可以写出:
重新排列方程的左侧和右侧,我们可以写成以下内容:
重新排列项,我们的最终方程可以表示为:
上述方程意味着什么?
方程左侧也被称为变分下界或证据下界(ELBO)。左侧第一项表示我们希望最大化的输入x的分布,
表示估计和真实分布之间的 KL 散度。
损失函数可以写成以下内容:
在这个方程中,您会注意到以下内容:
-
意味着我们在最大化输入的分布;通过简单地添加一个负号,我们可以将最大化问题转化为最小化问题,因此我们可以写成
-
意味着我们在最大化估计和真实分布之间的 KL 散度,但我们想要将它们最小化,因此我们可以写成
来最小化 KL 散度
因此,我们的损失函数变为以下内容:
^()
如果你看这个方程式, 基本上意味着输入的重建,即解码器采用潜在向量
并重建输入
。
因此,我们的最终损失函数是重建损失和 KL 散度的总和:
简化的 KL 散度值如下所示:
因此,最小化上述损失函数意味着我们在最小化重建损失的同时,还在最小化估计和真实分布之间的 KL 散度。
重参数化技巧
我们在训练 VAE 时遇到了一个问题,即通过梯度下降。请记住,我们正在执行一个采样操作来生成潜在向量。由于采样操作不可微分,我们无法计算梯度。也就是说,在反向传播网络以最小化错误时,我们无法计算采样操作的梯度,如下图所示:
因此,为了应对这一问题,我们引入了一种称为重参数化技巧的新技巧。我们引入了一个称为epsilon的新参数,它是从单位高斯分布中随机采样的,具体如下所示:
现在我们可以重新定义我们的潜在向量 为:
重参数化技巧如下图所示:
因此,通过重参数化技巧,我们可以使用梯度下降算法训练 VAE。
使用 VAE 生成图像
现在我们已经理解了 VAE 模型的工作原理,在本节中,我们将学习如何使用 VAE 生成图像。
导入所需的库:
import warnings
warnings.filterwarnings('ignore')
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm
from tensorflow.keras.layers import Input, Dense, Lambda
from tensorflow.keras.models import Model
from tensorflow.keras import backend as K
from tensorflow.keras import metrics
from tensorflow.keras.datasets import mnist
import tensorflow as tf
tf.logging.set_verbosity(tf.logging.ERROR)
准备数据集
加载 MNIST 数据集:
(x_train, _), (x_test, _) = mnist.load_data()
标准化数据集:
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
重塑数据集:
x_train = x_train.reshape((len(x_train), np.prod(x_train.shape[1:])))
x_test = x_test.reshape((len(x_test), np.prod(x_test.shape[1:])))
现在让我们定义一些重要的参数:
batch_size = 100
original_dim = 784
latent_dim = 2
intermediate_dim = 256
epochs = 50
epsilon_std = 1.0
定义编码器
定义输入:
x = Input(shape=(original_dim,))
编码器隐藏层:
h = Dense(intermediate_dim, activation='relu')(x)
计算均值和方差:
z_mean = Dense(latent_dim)(h)
z_log_var = Dense(latent_dim)(h)
定义采样操作
使用重参数化技巧定义采样操作,从编码器分布中采样潜在向量:
def sampling(args):
z_mean, z_log_var = args
epsilon = K.random_normal(shape=(K.shape(z_mean)[0], latent_dim), mean=0., stddev=epsilon_std)
return z_mean + K.exp(z_log_var / 2) * epsilon
从均值和方差中采样潜在向量 z:
z = Lambda(sampling, output_shape=(latent_dim,))([z_mean, z_log_var])
定义解码器
定义具有两层的解码器:
decoder_hidden = Dense(intermediate_dim, activation='relu')
decoder_reconstruct = Dense(original_dim, activation='sigmoid')
使用解码器重建图像,解码器将潜在向量 作为输入并返回重建的图像:
decoded = decoder_hidden(z)
reconstructed = decoder_reconstruct(decoded)
构建模型
我们按以下方式构建模型:
vae = Model(x, reconstructed)
定义重建损失:
Reconstruction_loss = original_dim * metrics.binary_crossentropy(x, reconstructed)
定义 KL 散度:
kl_divergence_loss = - 0.5 * K.sum(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var), axis=-1)
因此,总损失可以定义为:
total_loss = K.mean(Reconstruction_loss + kl_divergence_loss)
添加损失并编译模型:
vae.add_loss(total_loss)
vae.compile(optimizer='rmsprop')
vae.summary()
训练模型:
vae.fit(x_train,
shuffle=True,
epochs=epochs,
batch_size=batch_size,
verbose=2,
validation_data=(x_test, None))
定义生成器
定义生成器从学习到的分布中取样并生成图像:
decoder_input = Input(shape=(latent_dim,))
_decoded = decoder_hidden(decoder_input)
_reconstructed = decoder_reconstruct(_decoded)
generator = Model(decoder_input, _reconstructed)
绘制生成的图像
现在让我们绘制由生成器生成的图像:
n = 7
digit_size = 28
figure = np.zeros((digit_size * n, digit_size * n))
grid_x = norm.ppf(np.linspace(0.05, 0.95, n))
grid_y = norm.ppf(np.linspace(0.05, 0.95, n))
for i, yi in enumerate(grid_x):
for j, xi in enumerate(grid_y):
z_sample = np.array([[xi, yi]])
x_decoded = generator.predict(z_sample)
digit = x_decoded[0].reshape(digit_size, digit_size)
figure[i * digit_size: (i + 1) * digit_size,
j * digit_size: (j + 1) * digit_size] = digit
plt.figure(figsize=(4, 4), dpi=100)
plt.imshow(figure, cmap='Greys_r')
plt.show()
以下是由生成器生成的图像的绘图:
总结
我们通过学习自编码器及其如何用于重构其自身输入来开始本章。我们探讨了卷积自编码器,其中我们使用卷积层和反卷积层进行编码和解码。随后,我们学习了稀疏自编码器,它只激活特定的神经元。然后,我们学习了另一种正则化自编码器类型,称为压缩自编码器,最后,我们学习了 VAE,这是一种生成自编码器模型。
在下一章中,我们将学习如何使用少量数据点来进行学习,使用 few-shot 学习算法。
问题
让我们通过回答以下问题来检验我们对自编码器的了解:
-
什么是自编码器?
-
自编码器的目标函数是什么?
-
卷积自编码器与普通自编码器有何不同?
-
什么是去噪自编码器?
-
如何计算神经元的平均激活?
-
定义了压缩自编码器的损失函数。
-
什么是 Frobenius 范数和雅可比矩阵?
进一步阅读
您也可以查看以下链接以获取更多信息:
-
稀疏自编码器 的笔记由 Andrew Ng 提供,
web.stanford.edu/class/cs294a/sparseAutoencoder_2011new.pdf
-
压缩自编码器:特征提取期间的显式不变性 由 Salah Rifai 等人撰写,
www.icml-2011.org/papers/455_icmlpaper.pdf
-
变分自编码器用于深度学习图像、标签和标题 由 Yunchen Pu 等人撰写,
papers.nips.cc/paper/6528-variational-autoencoder-for-deep-learning-of-images-labels-and-captions.pdf
第十一章:探索 Few-Shot Learning 算法
恭喜!我们终于来到了最后一章。我们已经走过了很长的路。我们首先学习了神经网络是什么,以及它们如何用于识别手写数字。然后我们探索了如何使用梯度下降算法训练神经网络。我们还学习了递归神经网络用于序列任务,以及卷积神经网络用于图像识别。接着,我们研究了如何使用单词嵌入算法理解文本的语义。然后我们熟悉了几种不同类型的生成对抗网络和自编码器。
到目前为止,我们已经了解到,当我们有一个相当大的数据集时,深度学习算法表现得非常出色。但是当我们没有大量数据点可以学习时,我们该如何处理?对于大多数使用情况,我们可能得不到一个大型数据集。在这种情况下,我们可以使用 few-shot learning 算法,它不需要大量数据集进行学习。在本章中,我们将理解 few-shot learning 算法如何从较少数量的数据点中学习,并探索不同类型的 few-shot learning 算法。首先,我们将学习一个名为siamese network的流行 few-shot learning 算法。接下来,我们将直观地学习其他几种 few-shot learning 算法,如 prototypical、relation 和 matching networks。
在本章中,我们将学习以下主题:
-
什么是 few-shot learning?
-
Siamese 网络
-
Siamese 网络的架构
-
Prototypical networks
-
Relation 网络
-
Matching networks
什么是 few-shot learning?
从少量数据点中学习被称为few-shot学习或k-shot learning,其中 k 指定数据集中每个类别中的数据点数量。
假设我们正在执行图像分类任务。假设我们有两个类别 - 苹果和橙子 - 我们试图将给定的图像分类为苹果或橙子。当我们的训练集中恰好有一个苹果图像和一个橙子图像时,这被称为 one-shot learning;也就是说,我们仅从每个类别中的一个数据点中学习。如果我们有,比如,11 张苹果图像和 11 张橙子图像,那就称为 11-shot learning。因此,k-shot learning 中的 k 指的是每个类别中我们拥有的数据点数。
还有zero-shot learning,其中我们没有任何类别的数据点。等等,什么?没有任何数据点怎么学习?在这种情况下,我们不会有数据点,但我们会有关于每个类别的元信息,我们将从这些元信息中学习。
由于我们的数据集中有两个类别,即苹果和橙子,我们可以称其为two-way k-shot learning。所以,在 n-way k-shot learning 中,n-way 表示数据集中类别的数量,k-shot 表示每个类别中的数据点数。
我们需要我们的模型仅从少数数据点中学习。为了达到这个目标,我们以同样的方式训练它们;也就是说,我们在非常少的数据点上训练模型。假设我们有一个数据集,。我们从数据集中每个类别中抽取少量数据点,并称之为支持集。同样地,我们从每个类别中抽取一些不同的数据点,并称之为查询集。
我们使用支持集训练模型,并使用查询集进行测试。我们以每集方式训练模型,也就是说,在每一集中,我们从数据集中抽取几个数据点,,准备支持集和查询集,并在支持集上训练,查询集上测试。
孪生网络
孪生网络是一种特殊类型的神经网络,是最简单和最流行的一次学习算法之一。正如我们在前面的章节中所学到的,一次学习是一种技术,我们仅从每个类别中学习一个训练示例。因此,孪生网络主要用于那些每个类别数据点不多的应用场景。
例如,假设我们想为我们的组织构建一个人脸识别模型,并且说我们的组织有大约 500 人。如果我们想要从头开始使用卷积神经网络(CNN)构建我们的人脸识别模型,那么我们需要这 500 人的许多图像来训练网络并获得良好的准确性。但是,显然,我们不会为这 500 人拥有足够多的图像,因此,除非我们有足够的数据点,否则使用 CNN 或任何深度学习算法构建模型是不可行的。因此,在这些情况下,我们可以借助一种复杂的一次学习算法,如孪生网络,它可以从较少的数据点中学习。
但是孪生网络是如何工作的呢?孪生网络基本上由两个对称的神经网络组成,它们共享相同的权重和结构,并且在末端使用能量函数连接在一起,。我们的孪生网络的目标是学习两个输入是相似还是不相似。
假设我们有两幅图像, 和
,我们想要学习这两幅图像是否相似或不相似。如下图所示,我们将图像
输入网络
,将图像
输入网络
。这两个网络的作用是为输入图像生成嵌入(特征向量)。因此,我们可以使用任何能够给我们嵌入的网络。由于我们的输入是图像,我们可以使用卷积网络来生成这些嵌入:也就是用于提取特征。请记住,在这里 CNN 的作用仅仅是提取特征而不是分类。
我们知道这些网络应该具有相同的权重和架构,如果网络 是一个三层 CNN,那么网络
也应该是一个三层 CNN,并且我们必须为这两个网络使用相同的权重集。因此,网络
和 网络
将分别为输入图像
和
提供嵌入。然后,我们将这些嵌入馈送到能量函数中,该函数告诉我们两个输入图像的相似程度。能量函数基本上是任何相似性度量,如欧氏距离和余弦相似度:
孪生网络不仅用于人脸识别,还广泛应用于我们没有多个数据点和需要学习两个输入之间相似性的任务中。孪生网络的应用包括签名验证、相似问题检索和物体跟踪。我们将在下一节详细研究孪生网络。
孪生网络的架构
现在我们对孪生网络有了基本的理解,接下来我们将详细探讨它们。孪生网络的架构如下图所示:
正如您可以在前面的图中看到的那样,siamese 网络由两个相同的网络组成,两者共享相同的权重和架构。假设我们有两个输入, 和
。我们将 Input
输入到 Network
,即
,我们将 Input
输入到 Network
,即
。
正如您可以看到的,这两个网络具有相同的权重,,它们将为我们的输入
和
生成嵌入。然后,我们将这些嵌入传入能量函数,
,它将给出两个输入之间的相似度。可以表达如下:
假设我们使用欧氏距离作为能量函数;那么当 和
相似时,
的值将很低。当输入值不相似时,
的值将很大。
假设您有两个句子,句子 1 和句子 2。我们将句子 1 输入到网络 ,句子 2 输入到网络
。假设我们的网络
和网络
都是长短期记忆(LSTM)网络,并且它们共享相同的权重。因此,网络
和网络
将分别为句子 1 和句子 2 生成嵌入。
然后,我们将这些嵌入传入能量函数,该函数给出两个句子之间的相似度分数。但我们如何训练我们的 siamese 网络呢?数据应该如何?特征和标签是什么?我们的目标函数是什么?
Siamese 网络的输入应该是成对的,,以及它们的二进制标签,
,指示输入对是真实对(相同)还是伪造对(不同)。正如您可以在下表中看到的那样,我们有作为对的句子和标签,表明句子对是真实(1)还是伪造(0)的:
那么,我们的 siamese 网络的损失函数是什么?
由于孪生网络的目标不是执行分类任务而是理解两个输入值之间的相似性,我们使用对比损失函数。可以表达如下:
在上述方程中, 的值是真实标签,如果两个输入值相似则为 1,如果两个输入值不相似则为 0,
是我们的能量函数,可以是任何距离度量。术语边界用于保持约束,即当两个输入值不相似且它们的距离大于一个边界时,它们不会产生损失。
原型网络
原型网络是另一种简单、高效且流行的学习算法。与孪生网络类似,它们试图学习度量空间以执行分类。
原型网络的基本思想是为每个类别创建一个原型表示,并基于类原型与查询点之间的距离对查询点(新点)进行分类。
假设我们有一个支持集,其中包含狮子、大象和狗的图像,如下图所示:
我们有三个类别(狮子、大象和狗)。现在我们需要为这三个类别中的每一个创建一个原型表示。如何建立这三个类的原型?首先,我们将使用某个嵌入函数学习每个数据点的嵌入。嵌入函数,,可以是任何用于提取特征的函数。由于我们的输入是图像,我们可以使用卷积网络作为我们的嵌入函数,它将从输入图像中提取特征,如下所示:
一旦我们学习了每个数据点的嵌入,我们就取每个类中数据点的平均嵌入并形成类原型,如下所示。因此,类原型基本上是类中数据点的平均嵌入:
当一个新的数据点出现时,即我们希望预测标签的查询点时,我们将使用与创建类原型相同的嵌入函数生成这个新数据点的嵌入:也就是说,我们使用卷积网络生成我们的查询点的嵌入:
一旦我们有了查询点的嵌入,我们比较类原型和查询点嵌入之间的距离来确定查询点属于哪个类。我们可以使用欧氏距离作为测量类原型与查询点嵌入之间距离的距离度量,如下所示:
在计算类原型与查询点嵌入之间的距离后,我们对这个距离应用 softmax,并得到概率。由于我们有三类,即狮子、大象和狗,我们将得到三个概率。具有高概率的类将是我们查询点的类别。
由于我们希望我们的网络只从少量数据点学习,也就是说,我们希望进行少样本学习,我们以相同的方式训练我们的网络。我们使用情节式训练;对于每个情节,我们从数据集中的每个类中随机抽样一些数据点,并称之为支持集,并且我们仅使用支持集训练网络,而不是整个数据集。类似地,我们随机从数据集中抽样一个点作为查询点,并尝试预测其类别。通过这种方式,我们的网络学习如何从数据点中学习。
原型网络的整体流程如下图所示。正如您所看到的,首先,我们将为支持集中的所有数据点生成嵌入,并通过取类中数据点的平均嵌入来构建类原型。我们还生成查询点的嵌入。然后我们计算类原型与查询点嵌入之间的距离。我们使用欧氏距离作为距离度量。然后我们对这个距离应用 softmax,并得到概率。
如下图所示,由于我们的查询点是狮子,狮子的概率最高,为 0.9:
原型网络不仅用于一次性/少样本学习,还用于零样本学习。考虑一个情况,我们没有每个类的数据点,但我们有包含每个类高级描述的元信息。
在这些情况下,我们学习每个类的元信息的嵌入以形成类原型,然后使用类原型进行分类。
关系网络
关系网络包括两个重要的函数:嵌入函数,用表示,以及关系函数,用
表示。嵌入函数用于从输入中提取特征。如果我们的输入是图像,那么我们可以使用卷积网络作为我们的嵌入函数,它将给出图像的特征向量/嵌入。如果我们的输入是文本,那么我们可以使用 LSTM 网络来获取文本的嵌入。假设我们有一个包含三类的支持集,{狮子,大象,狗}如下所示:
假设我们有一个查询图像 ,如下图所示,我们想要预测这个查询图像的类别:
首先,我们从支持集中每个图像 中取出,并通过嵌入函数
提取特征。由于我们的支持集包含图像,我们可以将卷积网络作为我们的嵌入函数,用于学习嵌入。嵌入函数将为支持集中每个数据点提供特征向量。类似地,我们通过将查询图像
传递给嵌入函数
来学习查询图像的嵌入。
一旦我们有了支持集的特征向量 和查询集的特征向量
,我们使用某些运算符
将它们组合起来。这里,
可以是任何组合运算符。我们使用串联作为运算符来组合支持集和查询集的特征向量:
如下图所示,我们将组合支持集的特征向量,,以及查询集的特征向量,
。但是,这样组合有什么用呢?嗯,这将帮助我们理解支持集中图像的特征向量与查询图像的特征向量之间的关系。
在我们的例子中,这将帮助我们理解狮子的特征向量如何与查询图像的特征向量相关联,大象的特征向量如何与查询图像的特征向量相关联,以及狗的特征向量如何与查询图像的特征向量相关联:
但是我们如何衡量这种相关性呢?嗯,这就是为什么我们使用关系函数 。我们将这些组合特征向量传递给关系函数,它将生成关系分数,范围从 0 到 1,表示支持集
中样本与查询集
中样本之间的相似度。
下面的方程显示了我们如何在关系网络中计算关系分数,:
在这里, 表示关系分数,表示支持集中每个类别与查询图像的相似度。由于支持集中有三个类别和一个查询集中的图像,我们将得到三个分数,表示支持集中所有三个类别与查询图像的相似度。
在一次性学习设置中,关系网络的整体表示如下图所示:
匹配网络
匹配网络是谷歌 DeepMind 发布的又一种简单高效的一次性学习算法。它甚至可以为数据集中未观察到的类别生成标签。假设我们有一个支持集 ,包含
个例子作为
。当给出一个查询点(新的未见示例)
时,匹配网络通过与支持集比较来预测
的类别。
我们可以将其定义为 ,其中
是参数化的神经网络,
是查询点
的预测类别,
是支持集。
将返回
属于支持集中每个类的概率。然后我们选择具有最高概率的类作为
的类别。但这究竟是如何工作的?这个概率是如何计算的?让我们现在看看。查询点
的类别
可以预测如下:
让我们解析这个方程。这里 和
是支持集的输入和标签。
是查询输入,即我们希望预测标签的输入。同时
是
和
之间的注意力机制。但是我们如何执行注意力?这里,我们使用了一个简单的注意力机制,即
和
之间的余弦距离上的 softmax:
我们不能直接计算原始输入 和
之间的余弦距离。因此,首先,我们将学习它们的嵌入并计算嵌入之间的余弦距离。我们使用两种不同的嵌入,
和
,分别用于学习
和
的嵌入。我们将在接下来的部分详细学习这两个嵌入函数
和
如何学习这些嵌入。因此,我们可以重写我们的注意力方程如下:
我们可以将上述方程重写如下:
计算注意力矩阵 后,我们将注意力矩阵乘以支持集标签
。但是我们如何将支持集标签与我们的注意力矩阵相乘呢?首先,我们将支持集标签转换为独热编码值,然后将它们与我们的注意力矩阵相乘,结果得到我们的查询点
属于支持集各类的概率。然后我们应用 argmax 并选择具有最大概率值的
。
如果对匹配网络还不清楚?看看以下图表;您可以看到我们的支持集中有三类(狮子、大象和狗),我们有一个新的查询图像 。
首先,我们将支持集输入到嵌入函数 ,将查询图像输入到嵌入函数
,学习它们的嵌入并计算它们之间的余弦距离,然后我们在这个余弦距离上应用 softmax 注意力。然后,我们将我们的注意力矩阵乘以支持集标签的独热编码,并得到概率。接下来,我们选择概率最高的
。正如您在以下图表中看到的那样,查询集图像是一只大象,我们在索引 1 处具有很高的概率,因此我们预测
的类别为 1(大象):
我们已经学到我们使用两个嵌入函数, 和
,分别学习
和
的嵌入。现在我们将看看这两个函数如何学习嵌入。
支持集嵌入函数
我们使用嵌入函数 来学习支持集的嵌入。我们将双向 LSTM 作为我们的嵌入函数
。我们可以定义我们的嵌入函数
如下:
def g(self, x_i):
forward_cell = rnn.BasicLSTMCell(32)
backward_cell = rnn.BasicLSTMCell(32)
outputs, state_forward, state_backward = rnn.static_bidirectional_rnn(forward_cell, backward_cell, x_i, dtype=tf.float32)
return tf.add(tf.stack(x_i), tf.stack(outputs))
查询集嵌入函数
我们使用嵌入函数 来学习我们查询点
的嵌入。我们使用 LSTM 作为我们的编码函数。连同输入
,我们还会传递支持集嵌入的嵌入 g(x),并且还会传递一个称为 K 的参数,该参数定义了处理步骤的数量。让我们逐步看看如何计算查询集嵌入。首先,我们将初始化我们的 LSTM 单元:
cell = rnn.BasicLSTMCell(64)
prev_state = cell.zero_state(self.batch_size, tf.float32)
然后,在处理步骤的数量上,我们执行以下操作:
for step in xrange(self.processing_steps):
我们通过将其馈送到 LSTM 单元来计算查询集 的嵌入:
output, state = cell(XHat, prev_state)
h_k = tf.add(output, XHat)
现在,我们对支持集嵌入 g_embeddings
执行 softmax 注意力:即,它帮助我们避免不需要的元素:
content_based_attention = tf.nn.softmax(tf.multiply(prev_state[1], g_embedding))
r_k = tf.reduce_sum(tf.multiply(content_based_attention, g_embedding), axis=0)
我们更新 previous_state
并重复这些步骤,执行 K
次处理步骤:
prev_state = rnn.LSTMStateTuple(state[0], tf.add(h_k, r_k))
计算 f_embeddings
的完整代码如下:
def f(self, XHat, g_embedding):
cell = rnn.BasicLSTMCell(64)
prev_state = cell.zero_state(self.batch_size, tf.float32)
for step in xrange(self.processing_steps):
output, state = cell(XHat, prev_state)
h_k = tf.add(output, XHat)
content_based_attention = tf.nn.softmax(tf.multiply(prev_state[1], g_embedding))
r_k = tf.reduce_sum(tf.multiply(content_based_attention, g_embedding), axis=0)
prev_state = rnn.LSTMStateTuple(state[0], tf.add(h_k, r_k))
return output
匹配网络的架构
匹配网络的整体流程如下图所示,与我们已经看到的图像不同。您可以看到如何通过嵌入函数 和
计算支持集
和查询集
。
正如您所看到的,嵌入函数 将查询集
与支持集嵌入一起作为输入:
再次祝贺您学习了所有重要和流行的深度学习算法!深度学习是一个非常有趣和流行的 AI 领域,它已经改变了世界。现在您已经完成了书籍的阅读,可以开始探索深度学习的各种进展,并开始尝试各种项目。学习和深入学习!
摘要
我们从理解 k-shot 学习开始本章。我们了解到在 n-way k-shot 学习中,n-way 表示数据集中的类别数,k-shot 表示每个类别中的数据点数量;支持集和查询集相当于训练集和测试集。然后我们探索了孪生网络。我们学习了孪生网络如何使用相同的网络学习两个输入的相似度。
接着,我们学习了原型网络,它创建每个类的原型表示,并基于类原型与查询点之间的距离对查询点(新点)进行分类。我们还学习了关系网络如何使用两个不同的函数,嵌入函数和关系函数来分类图像。
在本章末尾,我们学习了匹配网络以及它如何使用支持集和查询集的不同嵌入函数来分类图像。
深度学习是人工智能领域中最有趣的分支之一。现在您已经了解了各种深度学习算法,可以开始构建深度学习模型,创建有趣的应用,并为深度学习研究做出贡献。
问题
让我们通过回答以下问题来评估从本章中获得的知识:
-
什么是少样本学习?
-
什么是支持集和查询集?
-
定义孪生网络。
-
定义能量函数。
-
孪生网络的损失函数是什么?
-
原型网络是如何工作的?
-
在关系网络中使用的不同类型函数是什么?
进一步阅读
要了解如何从少量数据点中学习更多,请查看 Sudharsan Ravichandiran 撰写、由 Packt 出版的Hands-On Meta Learning with Python,可在www.packtpub.com/big-data-and-business-intelligence/hands-meta-learning-python
获取。
第十二章:评估
以下是每章末尾提到的问题的答案。
第一章 - 深度学习简介
-
机器学习的成功在于正确的特征集合。特征工程在机器学习中扮演了关键角色。如果我们手工设计了正确的特征集合来预测某种结果,那么机器学习算法可以表现良好,但是找到和设计出正确的特征集合并不是一件容易的任务。有了深度学习,我们不需要手工设计这样的特征。由于深度人工神经网络(ANNs)使用了多层,它们自己学习数据的复杂内在特征和多级抽象表示。
-
这基本上是由于 ANN 的结构。ANN 由一些n个层组成,以执行任何计算。我们可以构建一个有多层的 ANN,其中每一层负责学习数据中的复杂模式。由于计算技术的进步,我们甚至可以构建深层次的网络,拥有数百甚至数千层。由于 ANN 使用深层进行学习,我们称其为深度学习;当 ANN 使用深层进行学习时,我们称其为深度网络。
-
激活函数用于向神经网络引入非线性。
-
当我们向 ReLU 函数输入任何负值时,它会将它们转换为零。对于所有负值变为零的问题称为dying ReLU。
-
从输入层到输出层的整个预测输出过程称为前向传播。在这个传播过程中,输入会在每一层被其相应的权重乘以,并在其上应用激活函数。
-
从输出层向输入层反向传播网络,并使用梯度下降更新网络权重以最小化损失的整个过程称为反向传播。
-
梯度检查基本上用于调试梯度下降算法,并验证我们是否有正确的实现。
第二章 - 了解 TensorFlow
-
TensorFlow 中的每个计算都由计算图表示。它由多个节点和边组成,其中节点是数学操作,如加法、乘法等,边是张量。计算图在优化资源方面非常高效,也促进了分布式计算。
-
一个计算图包含了节点上的操作和其边上的张量,只有创建了这个图,我们才能使用 TensorFlow 会话来执行它。
-
可以使用
tf.Session()
来创建 TensorFlow 会话,并且它将分配内存以存储变量的当前值。 -
变量是用来存储值的容器。变量将作为计算图中多个操作的输入。我们可以将占位符视为变量,其中我们只定义类型和维度,但不分配值。占位符的值将在运行时提供。我们通过占位符将数据馈送给计算图。占位符被定义为没有值。
-
TensorBoard 是 TensorFlow 的可视化工具,可以用来可视化计算图。它还可以用来绘制各种定量指标和几个中间计算的结果。当我们训练一个非常深的神经网络时,如果我们不得不调试模型,情况可能变得混乱。通过在 TensorBoard 中可视化计算图,我们可以轻松理解、调试和优化这样复杂的模型。它还支持共享。
-
作用域用于减少复杂性,并通过将相关节点分组来帮助我们更好地理解模型。在图中具有名称作用域有助于我们组织类似操作。当我们构建复杂的架构时,这非常方便。作用域可以使用
tf.name_scope()
创建。 -
TensorFlow 中的急切执行更符合 Python 风格,并允许快速原型设计。与图模式不同,我们无需每次执行操作时都构建一个图表,急切执行遵循命令式编程范例,可以立即执行任何操作,就像在 Python 中一样。
第三章 - 梯度下降及其变体
-
与梯度下降不同,在 SGD 中,为了更新参数,我们不必遍历训练集中的所有数据点。相反,我们只需遍历单个数据点。也就是说,与梯度下降不同,在遍历训练集中的所有数据点之后等待更新模型参数是不必要的。我们只需在遍历训练集中的每个单一数据点之后更新模型的参数。
-
在小批量梯度下降中,我们不是在遍历每个训练样本后更新参数,而是在遍历一些数据点批次后更新参数。假设批量大小为 50,这意味着我们在遍历 50 个数据点后更新模型的参数,而不是在遍历每个单独数据点后更新参数。
-
使用动量执行小批量梯度下降有助于减少梯度步骤中的振荡,并更快地达到收敛。
-
Nesterov 动量背后的基本动机是,我们不是在当前位置计算梯度,而是在动量将我们带到的位置计算梯度,我们称这个位置为前瞻位置。
-
在 Adagrad 中,当过去梯度值较高时,我们将学习率设置为较小的值,当过去梯度值较小时,我们将其设置为较高的值。因此,我们的学习率值根据参数过去梯度的更新而改变。
-
Adadelta 的更新方程如下:
-
RMSProp 是为了解决 Adagrad 的学习率衰减问题而引入的。因此,在 RMSProp 中,我们计算梯度的指数衰减运行平均值如下:
我们不是采用过去所有梯度的平方和,而是使用这些梯度的运行平均值。因此,我们的更新方程如下:
-
Adam 的更新方程如下:
第四章 - 使用 RNN 生成歌词
-
一个普通的前馈神经网络仅基于当前输入预测输出,但是循环神经网络基于当前输入和前一个隐藏状态预测输出,前者充当内存并存储到目前为止网络所见的上下文信息(输入)。
-
在时间步长为
时,隐藏状态
可以计算如下:
换句话说,这是一个时间步长 t 时的隐藏状态,tanh([输入到隐藏层权重 x 输入] + [隐藏到隐藏层权重 x 上一个隐藏状态])。
-
RNN 广泛应用于涉及序列数据的用例,如时间序列、文本、音频、语音、视频、天气等。它们在各种自然语言处理(NLP)任务中得到广泛应用,如语言翻译、情感分析、文本生成等。
-
在反向传播 RNN 时,我们在每个时间步长乘以权重和tanh函数的导数。当我们在向后移动时在每一步乘以较小的数字时,我们的梯度变得微小,导致计算机无法处理的数值;这就是所谓的梯度消失问题。
-
当我们将网络的权重初始化为非常大的数时,梯度将在每一步变得非常大。在反向传播时,我们在每个时间步长都乘以一个大数,这会导致梯度变为无穷大。这就是所谓的梯度爆炸问题。
-
我们使用梯度裁剪来规避梯度爆炸问题。在这种方法中,我们根据向量范数(比如,L2)对梯度进行归一化,并将梯度值裁剪到一定范围内。例如,如果我们将阈值设为 0.7,那么我们将梯度保持在-0.7 到+0.7 的范围内。如果梯度值超过-0.7,则将其更改为-0.7;同样地,如果超过了 0.7,则将其更改为+0.7。
-
不同类型的 RNN 架构包括一对一、一对多、多对一和多对多,并且它们用于各种应用。
第五章 - RNN 的改进
-
长短期记忆(LSTM)单元是 RNN 的一种变体,通过使用称为门的特殊结构解决了梯度消失问题。门控制信息在记忆中保持所需的时间。它们学习保留哪些信息和丢弃哪些信息。
-
LSTM 由三种类型的门组成,即遗忘门、输入门和输出门。遗忘门负责决定从细胞状态(记忆)中移除哪些信息。输入门负责决定将哪些信息存储在细胞状态中。输出门负责决定从细胞状态中提取哪些信息作为输出。
-
细胞状态也称为内部记忆,所有信息都将存储在这里。
-
在反向传播 LSTM 网络时,我们需要在每次迭代中更新太多的参数。这增加了我们的训练时间。因此,我们引入了作为 LSTM 单元简化版本的门控循环单元(GRU)单元。与 LSTM 不同,GRU 单元只有两个门和一个隐藏状态。
-
在双向 RNN 中,我们有两个不同的隐藏单元层。这两个层从输入连接到输出层。在一个层中,隐藏状态从左到右共享,在另一个层中,从右到左共享。
-
深度 RNN 通过使用前一个隐藏状态和前一层的输出来计算隐藏状态。
-
编码器学习给定输入句子的表示(嵌入)。一旦编码器学习到嵌入,它将嵌入发送给解码器。解码器将这个嵌入(思维向量)作为输入,并尝试构建目标句子。
-
当输入句子很长时,上下文向量并不能捕获整个句子的完整含义,因为它只是最终时间步的隐藏状态。因此,我们不是将最后的隐藏状态作为上下文向量并在解码器中使用注意力机制,而是取所有隐藏状态的总和作为上下文向量。
第六章 - 解密卷积网络
-
CNN 的不同层包括卷积、池化和全连接层。
-
我们通过一个像素滑动输入矩阵和过滤矩阵,并执行卷积操作。但我们不仅可以通过一个像素滑动输入矩阵,还可以通过任意数量的像素滑动输入矩阵。我们通过过滤矩阵滑动输入矩阵的像素数称为步长。
-
在卷积操作中,我们用一个过滤器矩阵滑动在输入矩阵上。但在某些情况下,过滤器不能完全适应输入矩阵。也就是说,当我们将我们的过滤器矩阵移动两个像素时,它会到达边界,过滤器不适合输入矩阵,也就是说,我们的过滤器矩阵的某些部分在输入矩阵之外。在这种情况下,我们进行填充。
-
池化层通过保留重要特征来减少空间维度。不同类型的池化操作包括最大池化、平均池化和总和池化。
-
VGGNet 是最广泛使用的 CNN 架构之一。它由牛津大学的视觉几何组(VGG)发明。VGG 网络的架构由卷积层和池化层组成。它在整个网络中使用 3 x 3 卷积和 2 x 2 池化。
-
通过分解卷积层,我们将具有较大过滤器尺寸的卷积层分解为具有较小过滤器尺寸的一堆卷积层。因此,在 inception 块中,具有 5 x 5 过滤器的卷积层可以分解为两个具有 3 x 3 过滤器的卷积层。
-
类似于 CNN,胶囊网络检查某些特征的存在以对图像进行分类,但除了检测特征外,它还会检查它们之间的空间关系 - 也就是说,它学习特征的层次结构。
-
在胶囊网络中,除了计算概率之外,我们还需要保留向量的方向,因此我们使用一种称为压缩函数的不同激活函数。它如下所示:
第七章 - 学习文本表示
-
在连续词袋(CBOW)模型中,我们试图预测给定上下文词的目标词,而在 skip-gram 模型中,我们试图预测给定目标词的上下文词。
-
CBOW 模型的损失函数如下所示:
-
当我们的词汇表中有数百万个单词时,我们需要执行大量的权重更新,直到预测正确的目标词。这是耗时且不高效的方法。因此,我们不是这样做,而是将正确的目标词标记为正类,并从词汇表中随机抽取几个词并标记为负类,这被称为负采样。
-
PV-DM 类似于连续词袋模型,其中我们试图预测给定上下文词的目标词。在 PV-DM 中,除了词向量外,我们引入了另一个向量,称为段落向量。顾名思义,段落向量学习整个段落的向量表示,并捕捉段落的主题。
-
编码器的作用是将句子映射到向量,解码器的作用是生成周围的句子;即前面和后面的句子。
-
在 QuickThoughts 中,有一个有趣的算法用于学习句子嵌入。在 quick-thoughts 中,我们试图学习一个给定句子是否与候选句子相关。因此,我们使用分类器而不是解码器来学习是否一个给定的输入句子与候选句子相关。
第八章 - 使用 GAN 生成图像
-
辨别模型学习如何以最佳方式找到分隔类别的决策边界,而生成模型则学习每个类别的特征。也就是说,辨别模型预测输入条件下的标签,
,而生成模型学习联合概率分布,
。
-
生成器学习数据集中图像的分布。它学习训练集中手写数字的分布。我们向生成器输入随机噪声,它将把随机噪声转换成与训练集中类似的新手写数字。
-
判别器的目标是执行分类任务。给定一幅图像,它将其分类为真实或虚假;也就是说,图像是来自训练集还是生成器生成的。
-
判别器的损失函数如下所示:
生成器的损失函数如下所示:
-
DCGAN 使用卷积网络扩展了 GAN 的设计。也就是说,我们用卷积神经网络替换了生成器和判别器中的前馈网络,卷积神经网络(CNN)。
-
Kullback-Leibler(KL)散度是用于确定一个概率分布与另一个概率分布之间差异的最常用度量之一。假设我们有两个离散概率分布,
和
,那么 KL 散度可以表达如下:
-
瓦瑟斯坦距离,也被称为地球移动距离(EM)距离,在最优传输问题中是最常用的距离度量之一,用于从一种配置移动物品到另一种配置。
-
利普希茨连续函数是一种必须处处连续且几乎处处可微的函数。因此,对于任何利普希茨连续函数,函数图形斜率的绝对值不能超过一个常数,
。这个常数,
,称为利普希茨常数。
第九章 - 深入学习 GAN
-
与普通 GAN 不同,条件生成对抗网络(CGAN)对生成器和鉴别器都施加条件。这个条件告诉 GAN 我们期望生成器生成什么图像。因此,我们的两个组件——鉴别器和生成器——都基于这个条件进行操作。
-
代码 c 基本上是可解释的解缠信息。假设我们有一些 MNIST 数据,那么,代码 c1 表示数字标签,代码 c2 表示数字的宽度,代码 c3 表示数字的笔画等。我们用术语 c 统称它们。
-
两个随机变量之间的互信息告诉我们通过一个随机变量可以从另一个随机变量获取的信息量。随机变量 x 和 y 之间的互信息可以表示如下:
它基本上是 y 的熵和在给定 x 的条件下 y 的条件熵之间的差异。
-
代码 c 给出了关于图像的可解释的解缠信息。因此,我们试图在不知道后验分布
的情况下找到 c,因此,我们使用辅助分布
来学习 c。
-
仅仅靠对抗损失并不能确保图像的正确映射。例如,生成器可以将源域中的图像映射到与目标域中的目标分布相匹配的随机排列图像中。因此,为了避免这种情况,我们引入了一种额外的损失,称为循环一致性损失。它强制生成器 G 和 F 都保持循环一致。
-
我们有两个生成器:
和
。
的作用是学习从
到
的映射,生成器
的作用是学习从 y 到
的映射。
-
Stack GANs 将文本描述转换为图像分为两个阶段。在第一阶段,艺术家绘制基本形状,并创建形成图像初始版本的基本轮廓。在下一个阶段,他们通过使图像更加真实和吸引人来增强图像。
第十章 - 使用自编码器重建输入
-
自编码器是一种无监督学习算法。与其他算法不同,自编码器学习重建输入,也就是说,自编码器接受输入并学习以输出形式复制输入。
-
我们可以将我们的损失函数定义为实际输入和重构输入之间的差异,如下所示:
在这里,
是训练样本的数量。
-
卷积自编码器(CAE)使用卷积网络而不是普通的神经网络。在普通自编码器中,编码器和解码器基本上是前馈网络。但在 CAE 中,它们基本上是卷积网络。这意味着编码器由卷积层组成,解码器由转置卷积层组成,而不是原始的前馈网络。
-
去噪自编码器(DAE)是自编码器的另一个小变体。它们主要用于从图像、音频和其他输入中去除噪声。因此,我们将损坏的输入提供给自编码器,它学习重构原始的未损坏输入。
-
隐藏层中的神经元
的平均激活可以计算为整个训练集上的以下值:
-
对抗自编码器的损失函数可以数学表示如下:
第一项代表重构误差,第二项代表惩罚项或正则化器,基本上是Jacobian 矩阵的Frobenius 范数。
-
矩阵的 Frobenius 范数,也称为Hilbert-Schmidt 范数,定义为其元素的绝对平方和的平方根。由向量值函数的偏导组成的矩阵称为Jacobian 矩阵。
第十一章 - 探索少样本学习算法
-
从少量数据点中学习称为少样本学习或k-样本学习,其中k指定数据集中每个类中的数据点数。
-
我们需要我们的模型仅从少量数据点中学习。为了实现这一点,我们以相同的方式训练它们;也就是说,我们在很少的数据点上训练模型。假设我们有一个数据集,
:我们从每个类别中取样少量数据点,并称之为支持集。类似地,我们从每个类别中取样一些不同的数据点,并称之为查询集。
-
Siamese 网络基本上由两个对称的神经网络组成,两者共享相同的权重和架构,并在末端使用某种能量函数
连接在一起。我们 Siamese 网络的目标是学习两个输入是否相似或不相似。
-
能量函数
,它将为我们提供两个输入之间的相似度。它可以表达如下:
-
由于 Siamese 网络的目标不是执行分类任务,而是理解两个输入值之间的相似性,因此我们使用对比损失函数。它可以表达如下:
-
原型网络是另一种简单、高效且广泛使用的少样本学习算法。原型网络的基本思想是为每个类别创建一个原型表示,并根据类别原型与查询点(新点)之间的距离对查询点进行分类。
-
关系网络由两个重要函数组成:一个嵌入函数,用
表示,和一个关系函数,用
表示。