深度学习初学者指南-全-
深度学习初学者指南(全)
原文:
annas-archive.org/md5/421a22d06a9cca471494e5d218a49c68
译者:飞龙
前言
多年来,那些一直在忠实从事机器学习工作的人们见证了这一领域的成长与繁荣,带来了惊人的技术成果,甚至可能引发彻底的社会变革。然而,对于那些希望加入我们并研究这一领域的人来说,这可能看起来有些令人望而生畏。当然,网上有大量的内容,而且越来越难以在各种论文和代码中找到可靠的入门资料,尤其是对于那些希望加入深度学习领域的人来说。虽然有许多关于机器学习的入门书籍,但大多数书籍未能有效地满足那些特别希望从事深度学习工作并且具备最低限度的数学、算法和编程技能的人的需求。
本书旨在帮助那些初学深度学习的人,建立使用公认方法构建深度学习模型所需的基础知识。如果这听起来像是你需要的内容,那么本书或许正是你所需要的。这本书假设读者没有神经网络和深度学习的广泛经验,从复习深度学习所需的机器学习基础开始。然后,本书解释如何通过清洗和预处理数据为深度学习做好准备,并逐步介绍神经网络及流行的监督神经网络架构,如卷积神经网络(CNNs)、递归神经网络(RNNs)和生成对抗网络(GANs),以及无监督架构,如自编码器(AEs)、变分自编码器(VAEs)和限制玻尔兹曼机(RBMs)。每章结束时,你将有机会测试自己对概念的理解并反思自己的成长。
本书结束时,你将理解深度学习的概念和方法,并能够区分哪些算法适合不同的任务。
第一章:适合人群
本书面向有志成为数据科学家和深度学习工程师的人,旨在帮助他们从深度学习和神经网络的基本原理入手。现在,关于要求:
-
不需要事先接触过深度学习或机器学习,但有基础的人会更好。
-
只需要对线性代数和 Python 编程有一定的了解,就可以开始。
本书适合那些珍惜自己时间的人,他们想要直接进入重点,学习实现目标所需的深度学习方法。
如果你不懂基础,深度学习可能会让人感到害怕。许多人因为跟不上术语或示例程序而感到沮丧。这导致他们在选择深度学习算法时做出错误的决策,并使他们无法预见这些选择的后果。因此,本书适合那些:
-
重视对深度学习概念的良好定义
-
想要一个有结构的方法从零开始学习深度学习
-
渴望了解基本概念并真正理解它们
-
想了解如何预处理数据,以便用于深度学习算法
-
对一些高级深度学习算法感到好奇
有关各章内容的详细信息,请阅读下一节。
本书所涵盖的内容
第一章,机器学习简介,概述了机器学习。它介绍了机器学习背后的动机以及该领域常用的术语。它还介绍了深度学习及其在人工智能领域中的位置。
第二章,深度学习框架的设置与介绍,帮助你设置 TensorFlow 和 Keras,并介绍它们在深度学习中的用途和目的。本章还简要介绍了其他深度学习库,让你对它们有一些初步了解。
第三章,数据准备,介绍了数据处理的主要概念,使其在深度学习中能够发挥作用。它将涵盖格式化分类数据或实值数据的输入和输出的基本概念,并探索数据增强或降维技术。
第四章,从数据中学习,介绍了深度学习理论的最基本概念,包括回归和分类中的性能测量,以及过拟合的识别。它还提出了一些关于优化超参数的警告。
第五章,训练单个神经元,介绍了神经元的概念,并将其与感知器模型连接,后者以简单的方式从数据中学习。感知器模型是理解基本神经模型的关键,这些模型从数据中学习。它还揭示了非线性可分数据的问题。
第六章,训练多层神经元,使你面对使用多层感知机算法进行深度学习的首次挑战,如用于误差最小化的梯度下降技术,以及通过超参数优化来实现泛化。
第七章,自编码器,通过解释编码层和解码层的必要性来描述自编码器模型。它探讨了与自编码器问题相关的损失函数,并将其应用于降维问题和数据可视化。
第八章,深度自编码器,介绍了深度信念网络的概念及其在深度无监督学习中的重要性。通过引入深度自编码器并与浅层自编码器进行对比,解释了这些概念。
第九章,变分自编码器,介绍了无监督深度学习领域生成模型的理念及其在生成抗噪声模型中的重要性。该章节将变分自编码器(VAE)呈现为处理扰动数据时比深度自编码器(AE)更好的选择。
第十章,限制玻尔兹曼机,通过介绍 RBMs,补充了本书对深度信念模型的覆盖。章节介绍了 RBMs 的前向-反向性质,并将其与仅前向传播的自编码器(AEs)进行对比。该章节通过减少数据维度的视觉表示来比较 RBMs 和 AEs 在这一问题上的表现。
第十一章,深度与宽度神经网络,解释了深度神经网络与宽度神经网络在性能和复杂性上的区别。章节介绍了稠密网络和稀疏网络的概念,重点讨论神经元之间的连接方式。
第十二章,卷积神经网络,介绍了卷积神经网络(CNNs),从卷积操作开始,接着介绍了通过多个卷积层组合的方式来学习作用于数据的过滤器。章节最后展示了如何可视化所学习到的过滤器。
第十三章,递归神经网络,介绍了递归网络的最基本概念,揭示了它们的不足之处,从而说明了长短期记忆模型的存在和成功。该章节探讨了顺序模型,并展示了其在图像处理和自然语言处理中的应用。
第十四章,生成对抗网络,介绍了生成对抗网络(GANs)的半监督学习方法,GANs 属于对抗学习家族。章节解释了生成器和判别器的概念,并讲解了为何对训练数据分布的良好近似能够促使模型在例如从随机噪声生成数据时取得成功。
第十五章,深度学习的未来展望,简要介绍了深度学习中令人兴奋的新主题和机会。如果你希望继续学习,这里会提供来自 Packt 出版的一些其他资源,帮助你在这个领域继续前进。
为了最大限度地利用本书
你需要确保你有一个互联网浏览器,并能访问 Google Colabs,网址是:colab.research.google.com/
。
尽管本书假设读者没有深度学习或机器学习的基础,但你需要对线性代数和 Python 编程有一定的了解,才能最大程度地利用本书。
为确保与未来发布的 Python 机器学习和深度学习库兼容,我们在本书的代码包和 GitHub 仓库中包含了通过!pip freeze
命令生成的当前版本列表;然而,这些仅供参考和未来兼容性使用——请记住,Google Colabs 已经配置好了所有必要的环境。
我们的丰富书籍和视频目录中还有其他代码包可供下载,访问 github.com/PacktPublishing/
。快去看看吧!再次提醒,库列表仅供参考,但 Google Colabs 已经包含了最新的设置。
如果你使用的是本书的数字版本,我们建议你手动输入代码,或通过 GitHub 仓库访问代码(下节会提供链接)。这样可以避免因复制和粘贴代码而产生的潜在错误。
当你完成本书的学习旅程时,庆祝一下,并仔细阅读本书的最后一章,它将为你指引新的方向。记住,保持持续学习是成功的关键之一。
下载示例代码文件
你可以从你在www.packt.com的账户中下载本书的示例代码文件。如果你是从其他地方购买的本书,可以访问www.packtpub.com/support,注册后将文件直接通过邮件发送给你。
你可以按照以下步骤下载代码文件:
-
登录或注册账户,访问www.packt.com。
-
选择“支持”选项卡。
-
点击代码下载。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
文件下载后,请确保使用最新版本的工具解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书的代码包也托管在 GitHub 上,链接为 github.com/PacktPublishing/Deep-Learning-for-Beginners
。如果代码有更新,将会在现有的 GitHub 仓库中更新。
下载彩色图像
我们还提供了一个 PDF 文件,包含本书中使用的截图/图表的彩色图像。你可以在这里下载: static.packt-cdn.com/downloads/9781838640859_ColorImages.pdf
。
使用的规范
本书中使用了若干文本规范。
CodeInText
:指示文本中的代码词语、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:“predict()
方法在潜在编码器模型、latent_ncdr
和autoencoder
模型中产生指定层的输出。”
代码块的设置如下:
x = np.array([[1., 1., 0., 1., 1., 0., 0., 0.]]) #216
encdd = latent_ncdr.predict(x)
x_hat = autoencoder.predict(x)
print(encdd)
print(x_hat)
print(np.mean(np.square(x-x_hat)))
当我们希望您注意代码块的特定部分时,相关行或项目将加粗显示:
import matplotlib.pyplot as plt
plt.plot(hist.history['loss'])
plt.title('Model reconstruction loss')
plt.ylabel('MSE')
plt.xlabel('Epoch')
plt.show()
任何命令行输入或输出如下所示:
$ pip install tensorflow-gpu
粗体:表示新术语、重要词汇或您在屏幕上看到的词汇。例如,菜单或对话框中的词汇会以这种方式显示在文本中。这里是一个例子:“第一个重要的事情是一个叫做 双曲正切 的新激活函数。”
警告或重要说明将以这种方式出现。
提示和技巧如下所示。
联系我们
我们欢迎读者的反馈。
一般反馈:如果您对本书的任何部分有疑问,请在邮件主题中注明书名,并通过 customercare@packtpub.com
与我们联系。
勘误:虽然我们已尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将不胜感激,如果您能将此问题报告给我们。请访问 www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表格链接,并填写相关信息。
盗版:如果您在互联网上发现任何形式的非法复制作品,我们将不胜感激,如果您能提供该作品的位置地址或网站名称。请通过 copyright@packt.com
与我们联系,并提供相关链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识并且有兴趣撰写或参与编写书籍,请访问 authors.packtpub.com。
书评
请留下评论。当您阅读并使用完本书后,为什么不在您购买本书的网站上留下评论呢?潜在读者可以看到并使用您的公正意见来做出购买决定,我们也能了解您对我们产品的看法,作者们也能看到您对他们书籍的反馈。感谢您!
如需了解更多关于 Packt 的信息,请访问 packt.com。
第二章
第一节:快速入门
本节将帮助你快速了解从数据中学习的基本概念、深度学习框架以及如何准备数据以便在深度学习中使用。
本节包括以下章节:
-
第一章,机器学习简介
-
第二章,深度学习框架的设置与简介
-
第三章,准备数据
-
第四章,从数据中学习
-
第五章,训练单个神经元
-
第六章,训练多个神经元层
机器学习简介
您可能近年来常常听到机器学习(ML)或人工智能(AI)这个术语,尤其是深度学习(DL)。这可能是您决定投资本书并深入了解更多的原因。随着神经网络领域一些新的激动人心的进展,深度学习已成为机器学习中的热门领域。如今,难以想象没有快速的语言间文本翻译,或者没有快速的歌曲识别的世界。这些,以及许多其他事情,都是深度学习改变世界潜力的冰山一角。当您读完本书后,我们希望您能加入这个行列,搭乘基于深度学习的新应用和项目的快车。
本章简要介绍了机器学习领域及其如何用于解决常见问题。在本章中,您将深入理解机器学习的基本概念、所涉及的研究问题以及它们的重要性。
本章将涵盖以下主题:
-
深入了解机器学习生态系统
-
从数据中训练机器学习算法
-
深度学习简介
-
为什么深度学习在今天如此重要?
第三章:深入了解机器学习生态系统
从图 1.1中描述的典型机器学习应用流程来看,您可以看到机器学习有广泛的应用。然而,机器学习算法只是更大生态系统中一个小部分,这个生态系统有许多活动的组件,但机器学习今天正在改变世界各地的生活:
图 1.1 - 机器学习生态系统。机器学习通过多个数据处理和解释阶段与世界互动,以实现整体系统集成
部署的机器学习应用通常从数据收集过程开始,使用不同类型的传感器,例如摄像头、激光器、光谱仪或其他直接获取数据的方式,包括本地和远程数据库,不论其大小。在最简单的情况下,输入可以通过计算机键盘或智能手机屏幕点击收集。在此阶段,收集到或感知到的数据被视为原始数据。
原始数据通常会在输入到机器学习模型之前进行预处理。原始数据很少是机器学习算法的实际输入,除非该机器学习模型旨在从原始数据中找到丰富的表示形式,然后将其用作另一个机器学习算法的输入。换句话说,有些机器学习算法专门作为预处理工具使用,它们与将要对预处理数据进行分类或回归的主机器学习模型无关。一般来说,这一数据预处理阶段的目标是将原始数据转换为具有特定数据类型的数组或矩阵。一些常见的预处理策略包括:
-
词向量转换,例如,使用 GloVe 或 Word2Vec
-
序列到向量或序列到矩阵策略
-
值范围归一化,例如,将(0, 255)转换为(0.0, 1.0)
-
统计值归一化,例如,调整为零均值和单位方差
一旦这些预处理措施完成,大多数机器学习算法就可以使用这些数据。然而,必须指出,预处理阶段并非简单,它需要对操作系统的高级知识和技能,有时甚至需要电子学方面的知识。从一般意义上讲,一个真正的机器学习应用有一个长期的流程,涉及计算机科学和工程的不同方面。
处理后的数据通常是你在像现在你正在阅读的这本书中看到的内容。原因是我们需要关注深度学习,而不是数据处理。如果你希望在这个领域有更深入的了解,可以阅读数据科学方面的资料,如 Ojeda, T. et.al. 2014 或 Kane, F. 2017。
从数学角度来看,处理过的数据整体上用大写粗体字母X表示,其中有N行(或数据点)。如果我们想引用数据集中的特定i元素(或行),我们可以写作:X[i]。该数据集有d列,通常被称为特征。 一种理解特征的方法是将其看作维度。例如,如果数据集有两个特征,身高和体重,那么你可以用二维图表示整个数据集。第一维,x[1](身高),可以作为横轴,而第二维,x[2](体重),可以作为纵轴,如图 1.2所示:
图 1.2 - 示例二维数据
在生产过程中,当数据呈现给机器学习算法时,会执行一系列张量乘法和加法。这些向量运算通常通过非线性函数进行转换或归一化。接下来会进行更多的乘法和加法,更多的非线性变换,临时存储中间值,最后产生与输入相对应的期望输出。现在,你可以将这个过程看作是一个机器学习的“黑箱”,随着你继续阅读,黑箱会逐步揭示出来。
机器学习根据输入产生的输出通常需要某种形式的解释。例如,如果输出是一个对象分类为某一组或另一组的概率向量,那么可能需要进行解释。你可能需要了解概率有多低,以便考虑不确定性,或者你可能需要了解概率之间有多大差异,以便考虑更大的不确定性。输出处理充当了机器学习与决策世界之间的连接因素,通过业务规则的使用。这些规则可以是if-then规则,例如:“如果最大预测概率是第二大预测概率的两倍,那么发出预测;否则,不继续做出决策。”或者它们可以是基于公式的规则,或更复杂的方程组系统。
最终,在决策阶段,机器学习算法准备与世界互动,通过执行器开启一个灯泡,或者在预测不确定时购买股票,警告经理公司将在三天内用完库存,需要购买更多商品,或者通过应用程序编程接口(API)调用或操作系统(OS)命令向智能手机扬声器发送一条音频信息,说“这里是去电影院的路线”,并打开地图应用程序。
这是机器学习系统在生产环境中的广泛概述。然而,这假设机器学习算法已经正确训练和测试过,而这其实是最容易的部分,相信我。书的结尾,你将能够熟练训练高度复杂的深度学习算法,但现在,让我们先介绍通用的训练过程。
从数据中训练机器学习算法
一个典型的预处理数据集正式定义如下:
其中y是与输入向量x对应的期望输出。所以,机器学习的动机是利用数据,通过复杂的张量(向量)乘法和加法,或者简单地通过测量数据点之间的相似度或距离,来找到对x的线性和非线性变换,最终的目的是根据x预测y。
一种常见的思考方式是,我们希望对x进行某个未知函数的近似:
其中w是一个未知的向量,帮助x与b一起进行变换。这个公式非常基础、线性,仅仅是展示一个简单学习模型的样子。在这个简单的例子中,机器学习算法的核心是找到最佳的w和b,使得它们能够提供最接近(如果不是完美的话)y,即期望输出的结果。像感知机(Rosenblatt, F. 1958)这样非常简单的算法,通过过去在选择w和b时犯的错误,尝试不同的w和b值,从而根据错误的比例来选择下一步的参数。
一种结合感知机模型的方式,它们从直觉上看同一输入,结果证明比单一模型更好。后来,人们意识到将它们堆叠在一起可能是通向多层感知机的下一步,但问题是,1970 年代的人们认为学习过程过于复杂。这些多层系统类似于大脑神经元,这也是我们今天称它们为神经网络的原因。随着机器学习领域的一些有趣发现,出现了新的特定种类的神经网络和算法,被称为深度学习。
引入深度学习
虽然关于学习算法的更详细讨论将在 第四章《从数据中学习》中进行,本节将讨论神经网络的基本概念以及促使深度学习发展的相关内容。
神经元的模型
人类大脑通过其他神经元(突触)接收来自外界的电荷刺激,然后具有一个核,根据输入的刺激来触发神经元的激活。 在神经元的末端,输出信号通过树突传播到其他神经元,从而形成神经元网络。
人类神经元的类比如图 1.3 所示,其中输入由向量 x 表示,神经元的激活由某个函数 z(.) 给出,输出为 y。神经元的参数为 w 和 b:
图 1.3 - 神经元的基本模型
神经元的可训练参数是 w 和 b,它们是未知的。因此,我们可以使用训练数据 来通过某种学习策略来确定这些参数。从图中可以看出,x[1] 乘以 w[1],然后 x[2] 乘以 w[2],并且 b 乘以 1;所有这些乘积相加,可以简化为:
激活函数的作用是确保输出在所需的输出范围内。假设我们希望使用简单的线性激活,那么函数 z(.) 就不存在,或者可以跳过,如下所示:
这通常发生在我们想要解决回归问题时,输出数据的范围可以从 -∞ 到 +∞。然而,我们可能希望训练神经元来判断一个向量 x 是否属于两类之一,比如 -1 和 +1。那么,使用一种叫做符号激活的函数可能更为合适:
其中 sign(.) 函数表示如下:
还有许多其他的激活函数,但我们稍后会介绍。现在,我们将简要展示其中一个最简单的学习算法,即 感知机学习算法 (PLA)。
感知机学习算法
PLA 从假设你希望将数据 X 分类为两类:正类 (+) 和负类 (-) 开始。它将通过训练找到 *某些 w 和 b 来预测相应的正确标签 y。PLA 使用 sign(.) 函数作为激活函数。以下是 PLA 执行的步骤:
-
初始化 w 为零,迭代计数器 t = 0
-
当存在任何分类错误的例子时:
-
选择一个分类错误的例子,称其为x*,其真实标签是*y**
-
更新w如下:w[t+1] = w[t] + y***x***
-
增加迭代计数器t++并重复
请注意,为了使 PLA 按我们预期的方式工作,我们必须进行调整。我们希望的是, 在表达式中隐含了
。只有在我们设置了
和
时,这种方式才会起作用。之前的规则寻找w,这意味着我们在寻找b。
为了说明 PLA,考虑以下线性可分数据集的情况:
线性可分数据集是指数据点足够分开,以至于至少存在一条假设的线可以用来将数据分为两组。拥有线性可分的数据集是所有机器学习科学家的梦想,但自然界中很少能找到这样的数据集。在后续章节中,我们将看到神经网络如何将数据转换到一个新的特征空间,在这个空间中可能存在这样的分割线。
这个二维数据集是使用 Python 工具随机生成的,我们将在后面讨论这些工具。目前,显而易见的是,你可以在两组数据之间画一条线并将它们分开。
按照之前的步骤,PLA 可以找到a解,即仅在四次迭代内,在这个特定情况下,可以找到一个完全满足训练数据目标输出的分隔线。每次更新后的图示以及每次更新时找到的相应线条如下:
在第零次迭代时,所有 100 个点都被误分类,但在随机选择一个被误分类的点进行第一次更新后,新线仅错过了四个点:
在第二次更新后,线仅错过一个数据点:
最终,在第三次更新后,所有数据点都被正确分类。这只是为了展示一个简单的学习算法如何能够成功地从数据中学习。而且,感知机模型为更复杂的模型(如神经网络)奠定了基础。接下来,我们将介绍浅层网络的概念及其基本复杂性。
浅层网络
神经网络由多个网络层连接而成。与此不同,感知机只有一个神经元,其结构包括输入层和输出层。在神经网络中,输入层和输出层之间还有额外的层,如图 1.4所示,这些层被称为隐藏层:
图 1.4 - 浅层神经网络示例
图中的示例展示了一个神经网络,它的隐藏层包含八个神经元。输入层的维度是 10 维,输出层有四个维度(四个神经元)。这个中间层可以根据你的系统在训练时处理的能力,包含任意数量的神经元,但通常来说,保持神经元数量在合理范围内是个不错的选择。
如果这是你第一次使用神经网络,建议你的隐藏层大小,也就是神经元的数量,应该大于或等于输入层的大小,且小于或等于输出层的大小。然而,虽然这是对初学者的好建议,但这并不是一个绝对的科学事实,因为找到神经网络的最佳神经元数量更像是一门艺术,而非科学,通常需要通过大量的实验来确定。
神经网络能够解决比没有网络更困难的问题,例如,仅仅依靠像感知机这样的单一神经单元。这个概念应该是直观的,而且很容易理解。神经网络能够解决包括线性可分问题在内的更复杂问题。对于线性可分问题,我们可以使用感知机模型和神经网络。但是,对于更复杂和不可线性分割的问题,感知机无法提供高质量的解决方案,而神经网络则能够做到。
例如,如果我们考虑一个二分类数据集,并将数据组拉得更近,感知机将无法得出解决方案,这时可以采用其他策略来阻止其陷入无穷循环。或者,我们可以切换到神经网络,并训练它找到尽可能好的解决方案。图 1.5 展示了一个在一个不可线性分割的二分类数据集上,训练一个隐藏层包含 100 个神经元的神经网络的示例:
图 1.5 - 使用包含 100 个神经元的隐藏层的神经网络对非可分数据进行非线性求解
这个神经网络在隐藏层有 100 个神经元。这个选择是通过实验得出的,之后的章节你将学习到如何找到这些实例的策略。然而,在我们继续之前,有两个新术语需要进一步解释:不可分数据和非线性模型,它们的定义如下:
-
非可分数据是指没有一条线能够将数据组(或类别)分成两个组的数据。
-
非线性模型或解决方案是那些在分类问题的最佳解决方案不是一条线时,通常会自然地出现的。例如,它可以是由任何高于一次的多项式描述的曲线。一个例子请参见图 1.5。
非线性模型通常是我们在本书中所要使用的模型,原因是这在现实世界中更为常见。而且,它是非线性的,从某种程度上说,是因为问题是不可分的。为了实现这种非线性解,神经网络模型会经历以下数学操作。
输入层到隐藏层
在神经网络中,输入向量x通过权重w连接到多个神经元,对于每个神经元,可以将其视为由多个权重向量组成的矩阵W。矩阵W的列数等于层中神经元的数量,行数则等于输入向量x的特征数(或维度)。因此,隐藏层的输出可以被看作以下向量:
其中,b是一个偏置向量,其元素对应于一个神经单元,h的大小与隐藏单元的数量成正比。例如,图 1.4中的八个神经元,以及图 1.5中的 100 个神经元。然而,激活函数 z(.)不一定是sign(.)函数,事实上,它通常不是。相反,大多数人使用的是那些易于可微分的函数。
可微分的激活函数是指具有数学导数的函数,这个导数可以通过传统的数值方法计算,或者函数的导数被明确定义。相反,不具有定义导数的函数就是不可计算的,甚至几乎不可能计算。
隐藏层到隐藏层
在神经网络中,我们可以有多个隐藏层,而我们将在本书中大量使用这种情况。在这种情况下,矩阵W可以表示为一个三维矩阵,其第三维的元素个数与网络的隐藏层数量相等。对于第i层,我们将该矩阵称为W[i],以便于表示。
因此,我们可以将第i个隐藏层的输出表示为:
对于i = 2, 3, ..., k-1,其中k是总层数,并且h[1]是通过前一层给定的方程计算的(见前一节),该方程直接使用x,并且不需要经过最后一层h[k],因为后者将在下文中讨论时计算。
隐藏层到输出层
网络的整体输出是最后一层的输出:
在这里,最后的激活函数通常与隐藏层的激活函数不同。最后一层(输出层)的激活函数通常取决于我们尝试解决的问题类型。例如,如果我们想解决回归问题,我们会使用线性函数;如果是分类问题,则会使用 sigmoid 激活函数。我们稍后会讨论这些问题。现在,应该显而易见的是,感知机算法在训练阶段将不再有效。
虽然学习过程仍然需要基于神经网络所犯的错误,但调整不能直接与被错误分类或预测的数据点成比例。原因是,最后一层的神经元负责进行预测,但它们依赖于前一层,而前一层可能又依赖于更前面的层,在对W和b进行调整时,必须为每个神经元做出不同的调整。
一种方法是应用梯度下降技术来训练神经网络。梯度下降技术有很多种,我们将在后面的章节中讨论其中最流行的一些。通常,梯度下降算法是这样一种算法:如果你对一个函数求导,并且导数的值为零,那么你就找到了可以获得的最大值(或最小值),即你正在对其求导的参数集的极值。对于标量,我们称之为导数;但对于向量或矩阵(W,b),我们称之为梯度。
我们可以使用的函数被称为损失函数。
损失函数通常是可微分的,这样我们就可以使用一些梯度下降算法来计算它的梯度。
我们可以定义一个损失函数,例如,如下所示:
这个损失被称为均方误差(MSE);它旨在衡量目标输出y与输出层中预测输出h[k]之间的差异,差异是通过其元素的平方来度量并进行平均。这是一个好的损失函数,因为它是可微的,并且计算起来非常容易。
这样的神经网络引入了大量的可能性,但在学习过程中依赖于一种称为反向传播(Hecht-Nielsen, R. 1992)的梯度下降技术。我们这里不详细解释反向传播(我们将在后面讲解),而是要指出,反向传播改变了机器学习的世界,但由于其一些实际限制,很多年里并没有取得很大进展,解决这些限制的问题为深度学习铺平了道路。
深度网络
2019 年 3 月 27 日,ACM 发布了一则公告,宣布三位计算机科学家因其在深度学习方面的成就获得了计算机领域的诺贝尔奖——即 ACM 图灵奖。他们的名字分别是 Yoshua Bengio、Yann LeCun 和 Geoffrey Hinton;他们都是非常杰出的科学家。他们的主要贡献之一就是反向传播学习算法。
在官方通讯中,ACM 这样写道,关于 Dr. Hinton 和他的其中一篇开创性论文(Rumelhart, D. E. 1985):
在 1986 年的一篇论文《通过误差传播学习内部表示》中,Hinton 与 David Rumelhart 和 Ronald Williams 共同合作,展示了反向传播算法使神经网络能够发现其数据的内部表示,从而使神经网络能够解决之前认为超出其能力范围的问题。反向传播算法今天已成为大多数神经网络的标准算法。
同样,他们在 Dr. LeCun 的论文(LeCun, Y., et.al., 1998)中写道:
LeCun 提出了反向传播算法(backprop)的早期版本,并基于变分原理给出了清晰的推导。他在加速反向传播算法方面的工作包括描述了两种简单的方法来加速学习时间。
Dr. Hinton 能够证明,有一种方法可以通过使用生物启发式算法(如通过调整神经元之间连接的重要性来进行前向和反向调整)来最小化神经网络中的损失函数。通常,反向传播与前馈神经网络相关,而前后传播则与限制玻尔兹曼机(在第十章中介绍,限制玻尔兹曼机)相关。
前馈神经网络是一种其输入直接通过没有反向连接的中间层传递到输出层的网络,如图 1.4所示,在本书中我们将经常讨论这些内容。
通常可以安全假设,除非另有说明,所有神经网络都是前馈结构。本书的大部分内容将讨论深度神经网络,其中绝大多数是前馈类型的,例外的情况包括限制玻尔兹曼机或递归神经网络等。
反向传播使得人们能够以一种前所未见的方式训练神经网络;然而,人们在大数据集和更大(更深)架构上训练神经网络时遇到了问题。如果你查看 80 年代末和 90 年代初的神经网络论文,你会注意到当时的架构很小;网络通常不超过两三层,神经元的数量通常也不会超过几百个。这些今天被称为浅层神经网络。
主要问题在于较大数据集的收敛时间,以及更深架构的收敛时间。LeCun 博士的贡献正是在这一领域,他设想了加速训练过程的不同方法。其他进展,例如在图形处理单元(GPUs)上进行向量(张量)计算,显著提高了训练速度。
因此,在过去的几年中,我们见证了深度学习的崛起,即训练更深神经网络的能力,实际上是具有三层或四层,甚至是数十层和数百层的网络。此外,我们还拥有各种各样的架构,可以完成过去十年无法完成的任务。
如图 1.6所示的深度网络,30 年前是无法训练的,而实际上它并不算深:
图 1.6 - 一个有八层的深度全连接前馈神经网络
在本书中,我们将认为任何具有三层或四层以上的网络都是深度神经网络。然而,并没有一个标准定义明确说明深度网络究竟有多深。此外,您还需要考虑到,今天我们认为的“深度”——在 2020 年写这本书时的标准——在 20 或 30 年后可能不再被认为是深度网络。
无论深度学习的未来如何,让我们现在来讨论一下为什么今天深度学习如此重要。
为什么今天深度学习如此重要?
今天,我们享受着 20 或 30 年前没有的算法和策略带来的好处,这些算法和策略让我们能够拥有正在改变生活的惊人应用。让我总结一下今天深度学习的一些伟大且重要的内容:
-
小批量训练:这种策略使得我们今天能够使用非常大的数据集,并且一点一点地训练深度学习模型。在过去,我们必须将整个数据集加载到内存中,这对于一些大数据集来说在计算上是不可能的。今天,虽然可能需要更长的时间,但我们至少可以在有限的时间内进行训练。
-
新型激活函数:例如,修正线性单元(ReLUs)是一种相对较新的激活函数,它解决了许多使用反向传播策略进行大规模训练时遇到的问题。这些新的激活函数使得训练算法能够在深度架构上收敛,而在过去,我们往往会在非收敛的训练过程中卡住,最终导致梯度爆炸或消失。
-
新颖的神经网络架构:例如,卷积神经网络或循环神经网络通过开辟我们能用神经网络做的事情的可能性,正在改变世界。卷积网络广泛应用于计算机视觉领域或其他卷积运算自然适用的领域,如多维信号或音频分析。带有记忆的循环神经网络广泛应用于文本序列分析,使我们能够拥有理解单词、句子和段落的网络,我们可以用它们进行语言翻译等诸多任务。
-
有趣的损失函数:这些损失在深度学习中起到了有趣的作用,因为过去我们只是一遍又一遍地使用相同的标准损失,如均方误差(MSE)。今天,我们不仅可以最小化 MSE,同时还可以最小化权重的范数或某些神经元的输出,这导致了更稀疏的权重和解决方案,反过来使得模型在生产环境中部署时更高效。
-
类似生物学的新策略:例如,缺失或丢弃神经元之间的连接,而不是让它们始终完全连接,这更符合现实,或者说更像生物神经网络的设计。此外,完全丢弃或移除神经元是一个新的策略,可以促使某些神经元在其他神经元被移除时表现出色,学习更丰富的表示,同时在训练时和部署时减少计算量。不同且专业化的神经网络之间共享参数,今天也被证明是一个有趣且有效的策略。
-
对抗训练:让一个神经网络与另一个网络竞争,后者的唯一目的是生成虚假的、嘈杂的和混乱的数据点,试图让网络失败,已被证明是一种优秀的策略,能够让网络从数据中学习得更好,并且在生产环境中具备更强的抗干扰能力。
深度学习有许多其他有趣的事实和要点,使其成为一个令人兴奋的领域,也为编写本书提供了正当理由。我希望你和我们一样激动,开始阅读本书时,你将知道我们将编写一些这个时代最令人兴奋和不可思议的神经网络。我们的最终目标是构建能够泛化的深度神经网络。
泛化是指神经网络在从未见过的数据上做出正确预测的能力。这是所有机器学习和深度学习从业者的终极目标,且需要极大的技能和对数据的深刻理解。
总结
本章节介绍了机器学习的概况。它阐述了机器学习背后的动机以及该领域常用的术语。它还介绍了深度学习以及深度学习在人工智能领域中的位置。到此为止,你应该已经对神经网络的基本概念有了足够的了解,足以好奇它能够有多大。你也应该对深度学习领域以及每周都会有新进展的各类新技术感到非常好奇。
到此为止,你应该有点迫不及待开始你的深度学习编程之旅了;因此,接下来的逻辑步骤是转到第二章,深度学习框架的设置与介绍。在这一章中,你将通过设置系统并确保你能访问成功成为深度学习从业者所需的资源,为实际操作做好准备。但在你继续之前,请先用以下问题测试一下自己。
问题与答案
- 感知器和/或神经网络能否解决可线性分割的数据分类问题?
是的,两个都可以。
- 感知器和/或神经网络能否解决不可分割数据的分类问题?
是的,两个都可以。然而,感知器将永远运行下去,除非我们指定停止条件,例如最大迭代次数(更新次数),或在若干次迭代后,如果误分类点数没有减少,则停止。
- 在机器学习领域中,哪些变化使我们今天能够拥有深度学习?
(A) 反向传播算法,批量训练,ReLU 等;
(B) 计算能力,GPU,云计算等等。
- 为什么泛化是好事?
因为深度神经网络在处理它们从未见过的数据时最为有效,即它们没有在这些数据上进行训练的数据。
参考文献
-
Hecht-Nielsen, R. (1992). 反向传播神经网络的理论。在 感知神经网络(第 65-93 页)。 Academic Press。
-
Kane, F. (2017). 动手实践数据科学与 Python 机器学习. Packt Publishing Ltd。
-
LeCun, Y., Bottou, L., Orr, G., 和 Muller, K. (1998). 神经网络中的高效反向传播:行内技巧(Orr, G. 和 Müller, K. 编)。 计算机科学讲义笔记,1524(98), 111。
-
Ojeda, T., Murphy, S. P., Bengfort, B., 和 Dasgupta, A. (2014). 实用数据科学食谱。Packt Publishing Ltd。
-
Rosenblatt, F. (1958). 感知器:大脑中信息存储和组织的概率模型。 心理学评论,65(6), 386。
-
Rumelhart, D. E., Hinton, G. E., 和 Williams, R. J. (1985). 通过误差传播学习内部表示(No. ICS-8506)。 加利福尼亚大学圣地亚哥分校认知科学研究所。
深度学习框架的设置与介绍
到此为止,你已经熟悉了机器学习(ML)和深度学习(DL)——这非常棒!你应该已经准备好开始为编写和运行你自己的程序做准备。本章将帮助你在设置 TensorFlow 和 Keras 的过程中,并介绍它们在深度学习中的用途和重要性。Dopamine 作为一种新的强化学习框架将在后续使用。本章还简要介绍了其他一些重要的深度学习库。
本章将涵盖以下主题:
-
Colaboratory 的介绍
-
TensorFlow 的介绍与设置
-
Keras 的介绍与设置
-
PyTorch 的介绍
-
Dopamine 的介绍
-
其他深度学习库
第四章:Colaboratory 的介绍
什么是 Colaboratory?Colaboratory 是一个基于网页的研究工具,用于进行机器学习和深度学习。它本质上类似于 Jupyter Notebook。Colaboratory 如今非常流行,因为它不需要任何设置。
本书中,我们将使用运行在 Colaboratory 上的 Python 3,它将安装我们可能需要的所有库。
Colaboratory 免费使用,且兼容大多数主流浏览器。开发 Colaboratory 工具的公司是 Google^™。与 Jupyter 笔记本不同,在 Colaboratory 中,你的一切都运行在云端,而不是你自己的电脑上。有个要点:你需要一个 Google 账号,因为所有的 Colaboratory 笔记本都保存在你的个人 Google Drive 空间里。然而,如果你没有 Google 账号,你仍然可以继续阅读,了解如何安装你需要的每个 Python 库来在自己电脑上运行。不过,我强烈建议你创建一个 Google 账号,至少为了能使用本书中的 Colaboratory 笔记本进行深度学习学习。
当你在 Colaboratory 上运行代码时,它会在一个专用的虚拟机上运行,接下来是有趣的部分:你可以分配一个 GPU 来使用!或者,如果你愿意,也可以使用 CPU。每当你没有运行程序时,Colaboratory 会取消分配资源(因为大家都希望有效工作),但你可以随时重新连接它们。
如果你准备好了,可以前往这个链接:colab.research.google.com/
如果你对 Colaboratory 有兴趣并希望进一步了解,可以搜索欢迎使用 Colaboratory!。现在你已经访问了上面的链接,我们可以开始使用 TensorFlow 了。
从现在开始,我们将Colaboratory简称为Colab,这实际上是人们对它的常见称呼。
TensorFlow 的介绍与设置
TensorFlow(TF)的名字中有一个词是Tensor,即向量的同义词。因此,TF 是一个 Python 框架,旨在在与神经网络建模相关的向量运算中表现出色。它是最受欢迎的机器学习库。
作为数据科学家,我们更偏向使用 TF,因为它是免费的、开源的并且有着强大的用户基础,同时它采用了基于图形的张量操作执行的最前沿研究。
设置
现在我们来开始设置或验证你是否拥有正确设置的指令:
- 要开始安装 TF,请在 Colaboratory 中运行以下命令:
%tensorflow_version 2.x
!pip install tensorflow
这将安装大约 20 个必需的库来运行 TF,其中包括numpy
,例如。
注意命令前面的感叹号(!)吗?这是在 Colaboratory 上运行 shell 命令的方式。例如,假设你想删除名为model.h5
的文件,那么你可以运行命令!rm model.h5
。
- 如果安装过程顺利执行,你将能够运行以下命令,这将打印出你在 Colaboratory 上安装的 TF 版本:
import tensorflow as tf
print(tf.__version__)
这将产生以下输出:
2.1.0
- 本版本的 TF 是写这本书时的当前版本。然而,大家都知道 TF 版本经常变化,可能在你阅读这本书时会有一个新版本。如果是这样,你可以按照以下方式安装特定版本的 TF:
!pip install tensorflow==2.1.0
我们假设你已经熟悉 Python,因此我们将相信你能够根据本书中使用的版本来匹配适当的库版本。这并不困难,可以像之前那样轻松完成,例如,使用==
符号来指定版本。我们将在继续时展示所使用的版本。
支持 GPU 的 TensorFlow
Colaboratory 默认会自动启用 TensorFlow 的 GPU 支持。不过,如果你有自己的 GPU 系统并希望设置支持 GPU 的 TensorFlow,安装非常简单。只需在你的个人系统中输入以下命令:
$ pip install tensorflow-gpu
然而,请注意,这假设你已经为你的系统安装了所有必要的驱动程序,以便访问 GPU。不过,不用担心,关于这个过程有很多文档可以在网上找到,举个例子,www.tensorflow.org/install/gpu
。如果遇到任何问题并且你需要继续前进,我强烈建议你返回并在 Colaboratory 上进行操作,因为这是学习最简单的方式。
现在让我们来看看 TensorFlow 是如何工作的,以及它的图形化范式如何使其变得非常强大。
TensorFlow 背后的原理
本书是为深度学习的绝对初学者准备的。因此,关于 TF 如何工作的知识,我们希望你了解以下内容。TF 创建一个图形,包含从输入张量到操作的最高抽象层次的执行过程。
例如,假设我们有已知的输入向量 x 和 w,并且有一个已知常量 b,假设你想执行以下操作:
如果我们通过声明和赋值张量来创建这个操作,图形将如下所示,在图 2.1中:
图 2.1 - 张量乘法和加法操作示例
在此图中,有一个张量乘法操作,mul,其结果是一个标量,并且需要与另一个标量 b 相加,add。请注意,这可能是一个中间结果,并且在实际计算图中,结果会向上移动到执行树的更高层次。有关 TF 如何使用图形的更详细信息,请参考这篇论文(Abadi, M., 等,2016)。
总之,TF 寻找执行张量操作的最佳方式,将特定部分委托给 GPU(如果可用),否则如果可用的话,将操作并行化在 CPU 核心上。它是开源的,拥有一个不断壮大的全球用户社区。大多数深度学习专业人员都了解 TF。
现在让我们讨论如何设置 Keras,以及它如何抽象化 TensorFlow 的功能。
Keras 的简介和设置
如果你在互联网上搜索 TensorFlow 的示例代码,你会发现它可能并不容易理解或跟随。你可以找到适合初学者的教程,但实际上,事情往往很容易变得复杂,编辑别人写的代码也可能非常困难。Keras 作为一种 API 解决方案,可以相对轻松地开发深度学习 TensorFlow 模型原型。事实上,Keras 不仅支持在 TensorFlow 上运行,还支持在 CNTK 和 Theano 上运行。
我们可以将 Keras 看作是对实际 TensorFlow 模型和方法的抽象。这种共生关系已经变得如此流行,以至于 TensorFlow 现在非正式地鼓励那些刚开始使用 TensorFlow 的人使用 Keras。Keras 非常友好,易于在 Python 中跟随,并且从总体上来说,学习起来也很简单。
设置
要在你的 Colab 中设置 Keras,请执行以下操作:
- 运行以下命令:
!pip install keras
- 系统将开始安装所需的库和依赖项。安装完成后,输入并运行以下代码片段:
import keras
print(keras.__version__)
这将输出一个确认消息,显示它使用 TensorFlow 作为后台,并且使用的是最新版的 Keras,在本书编写时,Keras 的版本是 2.2.4。因此,输出如下所示:
Using TensorFlow backend.
2.2.4
Keras 背后的原理
Keras 提供给用户的功能主要有两种方式:顺序模型和功能性 API。
这些内容可以总结如下:
-
顺序模型:这是指使用 Keras 的一种方式,允许你线性(或顺序地)堆叠层实例。在这里,层实例的含义与我们在第一章中讨论的一致,机器学习简介中提到过的。也就是说,层具有某种输入、某种行为或主要模型操作,以及某种输出。
-
功能性 API:这是深入定义更复杂模型的最佳方法,如合并模型、多输出模型、具有多个共享层的模型以及许多其他可能性。别担心,这些是高级话题,会在后续章节中讲解清楚。功能性 API 范式为程序员提供了更多自由,能做出不同的创新。
我们可以把顺序模型看作是开始使用 Keras 的简便方法,而功能性 API 则适用于更复杂的问题。
记得第一章中的浅层神经网络吗?好吧,这就是你如何使用 Keras 的顺序模型范式来构建该模型的方法:
from keras.models import Sequential
from keras.layers import Dense, Activation
model = Sequential([
Dense(10, input_shape=(10,)),
Activation('relu'),
Dense(8),
Activation('relu'),
Dense(4),
Activation('softmax'),
])
代码的前两行分别导入了Sequential
模型和Dense
层与Activation
层。Dense
层是一个全连接的神经网络,而Activation
层是以特定方式调用丰富的激活函数集,例如 ReLU 和 SoftMax,正如前面的例子所示(这些将在后文详细解释)。
或者,你也可以使用add()
方法来实现相同的模型:
from keras.models import Sequential
from keras.layers import Dense, Activation
model = Sequential()
model.add(Dense(10, input_dim=10))
model.add(Activation('relu'))
model.add(Dense(8))
model.add(Activation('relu'))
model.add(Dense(4))
model.add(Activation('softmax'))
第二种写法看起来更线性,而第一种更像是通过列表的方式在 Python 中实现。这实际上是相同的,最终你可能会偏好某一种方法。不过,记住,之前的两个例子都使用了 Keras 的顺序模型。
现在,仅为比较之用,这是如何使用 Keras 功能性 API 范式编写完全相同的神经网络架构:
from keras.layers import Input, Dense
from keras.models import Model
inputs = Input(shape=(10,))
x = Dense(10, activation='relu')(inputs)
x = Dense(8, activation='relu')(x)
y = Dense(4, activation='softmax')(x)
model = Model(inputs=inputs, outputs=y)
如果你是经验丰富的程序员,你会注意到功能性 API 风格提供了更多的灵活性。它允许你定义输入张量,并将其用作模型不同部分的输入(如果需要的话)。然而,使用功能性 API 假设你已经熟悉顺序模型。因此,在本书中,我们将从顺序模型开始,随着向更复杂神经网络模型的进展,逐步引入功能性 API 范式。
就像 Keras 一样,还有其他一些 Python 库和框架可以让我们以相对较低的难度进行机器学习。在写这本书的时候,最流行的是 Keras,其次是 PyTorch。
PyTorch 简介
在本书编写时,PyTorch 是第三大最受欢迎的深度学习框架。尽管与 TensorFlow 相比,PyTorch 在全球的历史较短,但它的受欢迎程度仍在不断增加。PyTorch 的一个有趣特点是,它允许一些 TensorFlow 不具备的定制功能。此外,PyTorch 还得到了 Facebook™的支持。
尽管本书涵盖了 TensorFlow 和 Keras,但我认为我们都应该记住,PyTorch 是一个很好的替代方案,而且它与 Keras 非常相似。作为参考,以下是如果用 PyTorch 编写,我们之前展示的相同浅层神经网络的代码:
import torch
device = torch.device('cpu')
model = torch.nn.Sequential(
torch.nn.Linear(10, 10),
torch.nn.ReLU(),
torch.nn.Linear(10, 8),
torch.nn.ReLU(),
torch.nn.Linear(8, 2),
torch.nn.Softmax(2)
).to(device)
相似之处很多。此外,从 Keras 到 PyTorch 的过渡对于有动机的读者来说不应太困难,而且这可能是未来的一项很好的技能。然而,目前社区的大部分兴趣仍集中在 TensorFlow 及其所有衍生版本上,特别是 Keras。如果你想了解更多关于 PyTorch 的起源和基本原理,或许这篇文章会对你有所帮助(Paszke, A., 等,2017 年)。
Dopamine 简介
深度强化学习领域的一个有趣的最新发展是 Dopamine。Dopamine 是一个用于快速原型设计深度强化学习算法的框架。本书将简要介绍强化学习,但你需要知道如何安装它。
Dopamine 以对强化学习新用户易于使用而闻名。此外,尽管它不是 Google 的官方产品,但大多数开发者都是 Google 的员工。在编写本书时,它的框架非常紧凑,并提供了现成可用的算法。
要安装 Dopamine,你可以运行以下命令:
!pip install dopamine-rl
你可以通过执行以下命令来测试 Dopamine 是否正确安装:
import dopamine
这不会输出任何内容,除非发生错误。通常,Dopamine 会使用大量外部库,以便做更多有趣的事情。目前,利用强化学习进行训练的一个最有趣的应用就是通过奖励策略训练代理,这在游戏中有直接的应用。
作为一个例子,参见图 2.2,该图展示了一个视频游戏在学习过程中,使用强化行为的策略根据代理采取的动作来强化期望行为的时间快照:
图 2.2 - Dopamine 在游戏强化学习问题中的代理样本可视化
强化学习中的代理是决定下一步采取什么行动的角色。代理通过观察世界及其规则来完成这一任务。规则越明确,结果就越受约束。如果规则太宽松,代理可能无法做出好的行动决策。
尽管本书没有深入探讨强化学习,但我们将在本书的最后一章介绍一个有趣的游戏应用。现在,你可以阅读以下白皮书,了解更多关于 Dopamine 的信息(Castro, P. S., 等,2018)。
其他深度学习库
除了两个大头——TensorFlow 和 Keras 外,还有其他一些竞争者正在深度学习领域崭露头角。我们已经讨论过 PyTorch,但还有更多。这里我们简要地谈一谈它们。
Caffe
Caffe 也是一个流行的框架,由加州大学伯克利分校开发(Jia, Y., 等,2014)。它在 2015-2016 年间变得非常流行。一些雇主仍然要求具备这种技能,学术文章中也仍然提到它的使用。然而,由于 TF 的成功和 Keras 的可访问性,其使用正在逐渐衰退。
如需了解更多关于 Caffe 的信息,请访问:caffe.berkeleyvision.org
。
还要注意到 Caffe2 的存在,它是由 Facebook 开发并且开源的。它是基于 Caffe 构建的,但现在 Facebook 有了新的“冠军”——PyTorch。
Theano
Theano 是由 Yoshua Bengio 的团队在蒙特利尔大学于 2007 年开发的(Al-Rfou, R., 等,2016)。Theano 有一个相对较老的用户群,可能见证了 TF 的崛起。最新的重大版本发布于 2017 年底,尽管没有明确的计划发布新的重大版本,但社区仍在持续更新。
如需了解更多关于 Theano 的信息,请访问:
deeplearning.net/software/theano/
荣誉提名
还有其他一些替代方案,可能由于各种原因不如它们流行,但值得在此提及,以防它们的未来发生变化。以下是这些替代方案:
名称 | 开发者 | 更多信息 |
---|---|---|
MXNET | Apache | mxnet.apache.org/ |
CNTK | Microsoft | cntk.ai |
Deeplearning4J | Skymind | deeplearning4j.org/ |
Chainer | Preferred Networks | chainer.org/ |
FastAI | Jeremy Howard | www.fast.ai/ |
总结
本章介绍了如何设置必要的库以运行 TensorFlow、Keras 和 Dopamine。希望你能使用 Colab 来让学习变得更轻松。你还学到了这些框架背后的基本思维方式和设计理念。尽管在编写本书时,这些框架是最受欢迎的,但仍有其他竞争者,我们也简要介绍了它们。
到此为止,你已经准备好开始掌握深度学习之旅。我们的第一个里程碑是了解如何为深度学习应用准备数据。这个步骤对模型的成功至关重要。无论模型多么优秀,结构多么深,如果数据没有正确格式化或处理,都会导致灾难性的性能结果。因此,我们将前往第三章,准备数据。 在该章节中,你将学习如何处理数据集,并将其准备好用于你想要用特定类型的深度学习模型解决的具体任务。然而,在你进入该章节之前,请先尝试通过以下问题自测。
问题与答案
- Colab 是否在我的个人计算机上运行?
不,Keras 运行在云端,但通过一些技巧和设置,你可以将其连接到你自己的个人云。
- Keras 使用 GPU 吗?
是的。因为 Keras 运行在 TensorFlow 上(在本书的设置中),而 TensorFlow 使用 GPU,所以 Keras 也使用 GPU。
- Keras 中的两种主要编码范式是什么?
(A) 顺序模型;(B) 功能性 API。
- 我们为什么关心多巴胺?
因为市面上只有少数几个你可以信任的强化学习框架,而多巴胺就是其中之一。
参考文献
-
Abadi, M., Barham, P., Chen, J., Chen, Z., Davis, A., Dean, J., Devin, M., Ghemawat, S., Irving, G., Isard, M., 和 Kudlur, M. (2016). Tensorflow: 一个大规模机器学习系统. 在第 12 届 {USENIX} 操作系统设计与实现研讨会({OSDI} 16)(第 265-283 页)。
-
Paszke, A., Gross, S., Chintala, S., Chanan, G., Yang, E., DeVito, Z., Lin, Z., Desmaison, A., Antiga, L. 和 Lerer, A. (2017). Pytorch 中的自动微分。
-
Castro, P. S., Moitra, S., Gelada, C., Kumar, S., 和 Bellemare, M. G. (2018). Dopamine: 一个深度强化学习的研究框架. arXiv 预印本 arXiv:1812.06110。
-
Jia, Y., Shelhamer, E., Donahue, J., Karayev, S., Long, J., Girshick, R., Guadarrama, S., 和 Darrell, T. (2014 年 11 月). Caffe: 用于快速特征嵌入的卷积架构. 在第 22 届 ACM 国际多媒体会议论文集(第 675-678 页)。ACM。
-
Al-Rfou, R., Alain, G., Almahairi, A., Angermueller, C., Bahdanau, D., Ballas, N., Bastien, F., Bayer, J., Belikov, A., Belopolsky, A. 和 Bengio, Y. (2016). Theano: 一个用于快速计算数学表达式的 Python 框架. arXiv 预印本 arXiv:1605.02688。
准备数据
现在,您已经成功地准备好学习深度学习,参见第二章,深度学习框架的设置与简介,接下来我们将为您提供一些在深度学习实践中可能会频繁遇到的数据处理重要指导。对于深度学习的学习来说,准备好合适的数据集将有助于您更多地集中精力设计模型,而不是花时间准备数据。然而,大家都知道,这并不是现实的期望,如果你询问任何数据科学家或机器学习专家,他们会告诉你,建模的一个重要方面就是知道如何准备数据。知道如何处理数据以及如何准备数据,将节省你大量的时间,你可以用来微调模型。任何花费在准备数据上的时间都是值得投资的时间。
本章将向您介绍数据处理背后的主要概念,以使其在深度学习中变得有用。它将涵盖格式化输出和输入(无论是类别数据还是实值数据)的基本概念,以及数据增强或降维的技术。到本章结束时,您应该能够处理最常见的数据处理技术,这些技术将有助于您在未来选择成功的深度学习方法。
本章特别讨论以下内容:
-
二进制数据与二分类
-
类别数据与多分类
-
实值数据与单变量回归
-
改变数据分布
-
数据增强
-
数据降维
-
操作数据的伦理影响
第五章:二进制数据与二分类
在本节中,我们将集中精力准备具有二进制输入或目标的数据。当然,所谓二进制,指的是可以表示为 0 或 1 的值。请注意,表示为这两个词的重点。原因是,一列数据可能包含的并不一定是 0 或 1 的值,但它可以被解释为或表示为 0 或 1。
请考虑以下数据集片段:
x[1] | x[2] | ... | y |
---|---|---|---|
0 | 5 | ... | a |
1 | 7 | ... | a |
1 | 5 | ... | b |
0 | 7 | ... | b |
在这个只有四行的小数据集示例中,列 x[1] 的值显然是二进制的,要么是 0,要么是 1。然而,x[2] 表面上可能并不被认为是二进制的,但如果仔细观察,这一列中的唯一值是 5 或 7。这意味着数据可以正确且唯一地映射到一组两个值。因此,我们可以将 5 映射为 0,将 7 映射为 1,或者反过来也可以;这并不重要。
在目标输出值 y 中也观察到类似现象,y 同样包含可以映射到大小为 2 的集合的独特值。我们可以通过将 b 映射为 0,将 a 映射为 1 来进行这样的映射。
如果你打算将字符串映射到二进制值,请务必检查你所使用的特定模型能处理的数据类型。例如,在某些支持向量机实现中,目标的首选值是-1 和 1。这也是二进制的,只不过在一个不同的集合中。决定使用哪种映射之前,请务必进行仔细检查。
在接下来的子章节中,我们将专门处理二进制目标,使用数据集作为案例研究。
克里夫兰心脏病数据集的二进制目标
克里夫兰心脏病(克里夫兰 1988)数据集包含 303 个受试者的病人数据。数据集中的一些列有缺失值,我们也会处理这些缺失值。数据集包含 13 列,其中包括胆固醇和年龄等数据。
目标是检测一个主体是否患有心脏病,因此这是一个二进制问题。我们要处理的问题是,数据用从 0 到 4 的值进行了编码,其中 0 表示没有心脏病,而 1 到 4 的范围表示某种类型的心脏病。
我们将使用标识为Cleveland
的数据集部分,可以从此链接下载:archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.cleveland.data
数据集的属性如下:
列 | 描述 |
---|---|
x[1] | 年龄 |
x[2] | 性别 |
x[3] | 胸痛类型:1: 典型心绞痛,2: 非典型心绞痛,3: 非心绞痛性疼痛,4: 无症状 |
x[4] | 休息时血压(住院时的毫米汞柱值) |
x[5] | 血清胆固醇(单位:mg/dl) |
x[6] | 空腹血糖 > 120 mg/dl:1 = 是,0 = 否 |
x[7] | 休息时心电图结果:0: 正常,1: 存在 ST-T 波异常,2: 显示可能或确诊的左心室肥大 |
x[8] | 达到的最大心率 |
| x[9] | 运动引起的心绞痛:1 = 是
0 = 无 |
x[10] | 运动时 ST 段与静息时 ST 段的相对压低 |
---|
| x[11] | 峰值运动 ST 段的坡度:1: 上坡
2: 平坦
3: 下降坡度
|
x[12] | 通过荧光透视检查的主要血管数量(0-3) |
---|
| x[13] | Thal:3 = 正常
6 = 固定缺陷
7 = 可逆性缺陷
|
| y | 心脏病诊断(冠状动脉造影疾病状态):0: < 50% 直径狭窄
1: > 50% 直径狭窄 |
接下来,我们将按照以下步骤读取数据集到 pandas DataFrame 并进行清理:
- 在我们的 Google Colab 中,我们首先使用
wget
命令下载数据,如下所示:
!wget https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.cleveland.data
这会将文件processed.cleveland.data
下载到 Colab 的默认目录。可以通过检查 Colab 左侧的文件标签来验证此操作。请注意,前面的指令是一个非常长的单行命令。
- 接下来,我们使用 pandas 加载数据集,以验证数据集是否可读且可访问。
Pandas 是一个在数据科学家和机器学习科学家中非常流行的 Python 库。它使得加载和保存数据集、替换缺失值、获取数据的基本统计属性甚至执行数据转换变得非常容易。Pandas 就是救命稻草,现在大多数机器学习库都接受 pandas 作为有效的输入格式。
在 Colab 中运行以下命令以加载并显示一些数据:
import pandas as pd
df = pd.read_csv('processed.cleveland.data', header=None)
print(df.head())
read_csv()
函数用于加载格式为 逗号分隔值(CSV)的文件。我们使用 header=None
参数告诉 pandas 数据没有实际的列标题;如果省略此参数,pandas 会将数据的第一行用作每列的名称,但在这种情况下我们不希望这样做。
加载的数据存储在一个名为df
的变量中,变量名可以是任何名字,但我认为它很容易记住,因为 pandas 将数据存储在一个 DataFrame 对象中。因此,df
看起来是一个合适的、简短且易记的名称。然而,如果我们处理多个 DataFrame,那么最好为每个 DataFrame 起不同的名字,使用能够描述其包含数据的名称会更方便。
head()
方法是 DataFrame 上操作的方法,它类似于 unix
命令,用于获取文件的前几行。在 DataFrame 上,head()
方法返回前五行数据。如果你希望获取更多或更少的行数据,可以将一个整数作为参数传递给该方法。例如,如果你想获取前三行,可以执行 df.head(3)
。
执行上述代码的结果如下:
0 1 2 3 4 5 6 7 8 9 10 11 12 13
0 63\. 1\. 1\. 145\. 233\. 1\. 2\. 150\. 0\. 2.3 3\. 0\. 6\. 0
1 67\. 1\. 4\. 160\. 286\. 0\. 2\. 108\. 1\. 1.5 2\. 3\. 3\. 2
2 67\. 1\. 4\. 120\. 229\. 0\. 2\. 129\. 1\. 2.6 2\. 2\. 7\. 1
3 37\. 1\. 3\. 130\. 250\. 0\. 0\. 187\. 0\. 3.5 3\. 0\. 3\. 0
4 41\. 0\. 2\. 130\. 204\. 0\. 2\. 172\. 0\. 1.4 1\. 0\. 3\. 0
下面是一些需要观察和记住的事项,供将来参考:
-
在左侧,有一个没有名称的列,其中包含连续的数字:0、1、...、4。这些是 pandas 为数据集中的每一行分配的索引值。这些是唯一的数字。有些数据集有唯一的标识符,比如图像的文件名。
-
在顶部,有一行从 0、1、... 到 13 的数字。这些是列标识符。它们也是唯一的,并且可以在给定的情况下进行设置。
-
在每一行和每一列的交点处,我们有值,这些值要么是浮动的小数,要么是整数。整个数据集包含的是小数,除了第 13 列,它是我们的目标列,包含整数。
- 因为我们将使用这个数据集作为一个二分类问题,现在我们需要将最后一列修改为只包含二进制值:0 和 1。我们将保留 0 的原始含义,即没有心脏病,任何大于或等于 1 的值都将映射为 1,表示诊断为某种类型的心脏病。我们将执行以下操作:
print(set(df[13]))
指令df[13]
查看 DataFrame 并检索索引为13
的列的所有行。接着,对第 13 列所有行应用set()
方法将创建该列中所有唯一元素的集合。通过这种方式,我们可以知道有多少不同的值,以便进行替换。输出如下:
{0, 1, 2, 3, 4}
从这点可以看出,0 表示没有心脏病,1 表示有心脏病。然而,2、3 和 4 需要映射为 1,因为它们也表示有心脏病。我们可以通过执行以下命令来进行此更改:
df[13].replace(to_replace=[2,3,4], value=1, inplace=True)
print(df.head())
print(set(df[13]))
在这里,replace()
函数作用于 DataFrame,用于替换特定值。在我们的例子中,它有三个参数:
-
to_replace=[2,3,4]
表示要查找的项目列表,用于进行替换。 -
value=1
表示将替换每个匹配项的值。 -
inplace=True
表示我们希望在该列上进行修改。
在某些情况下,pandas DataFrame 像不可变对象一样工作,这使得必须使用inplace=True
参数。如果我们没有使用这个参数,就需要像下面这样操作。
df[13] = df[13].replace(to_replace=[2,3,4], value=1)
,这对经验丰富的 pandas 用户来说不成问题。这意味着你应该能够无论如何都能轻松操作。
初学者使用 pandas 时的主要问题是它并不总是像不可变对象那样工作。因此,你应该随时查阅 pandas 文档:pandas.pydata.org/pandas-docs/stable/index.html
前述命令的输出如下:
0 1 2 3 4 5 6 7 8 9 10 11 12 13
0 63\. 1\. 1\. 145\. 233\. 1\. 2\. 150\. 0\. 2.3 3\. 0\. 6\. 0
1 67\. 1\. 4\. 160\. 286\. 0\. 2\. 108\. 1\. 1.5 2\. 3\. 3\. 1
2 67\. 1\. 4\. 120\. 229\. 0\. 2\. 129\. 1\. 2.6 2\. 2\. 7\. 1
3 37\. 1\. 3\. 130\. 250\. 0\. 0\. 187\. 0\. 3.5 3\. 0\. 3\. 0
4 41\. 0\. 2\. 130\. 204\. 0\. 2\. 172\. 0\. 1.4 1\. 0\. 3\. 0
{0, 1}
首先,注意当我们打印前五行时,第十三列现在只包含值 0 或 1。你可以将其与原始数据进行比较,验证加粗字体中的数字确实已经更改。我们还通过set(df[13])
验证了该列所有唯一值的集合现在仅包含{0, 1}
,这是我们想要的目标。
通过这些更改,我们可以使用该数据集训练深度学习模型,并可能改善现有的文献中记录的表现[Detrano, R., et al. (1989)]。
相同的方法可以应用于将任何其他列转换为我们需要的二值集合。作为练习,我们再做一个关于著名MNIST
数据集的例子。
对 MNIST 数据集进行二值化
MNIST 数据集在深度学习社区中非常著名(Deng, L. (2012))。它包含了成千上万张手写数字的图像。图 3.1 展示了 MNIST 数据集中的八个样本:
图 3.1 – MNIST 数据集的八个样本。每张图片上方的数字对应于目标类别。
如你所见,这个数据集中的样本杂乱无章,且非常真实。每张图像的大小为 28 x 28 像素。并且只有 10 个目标类别,每个数字对应一个类别,即 0,1,2,...,9。这里的复杂性通常在于某些数字可能与其他数字相似;例如,1 和 7,或者 0 和 6。然而,大多数深度学习算法已经成功地以高准确度解决了这个分类问题。
从图 3.1中仔细检查会发现,图像中的值并不完全是零和一,也就是二进制值。实际上,这些图像是 8 位灰度图像,值的范围在[0-255]之间。正如前面提到的,这对于大多数先进的深度学习算法来说不再是问题。然而,对于一些算法,例如限制玻尔兹曼机(RMBs),输入数据需要是二进制格式[0,1],因为这正是该算法传统上所需要的格式。
因此,我们将做两件事:
-
对图像进行二值化,以便获得二进制输入
-
对目标进行二值化,使其成为一个二分类问题
在本示例中,我们将任意选择两个数字,7 和 8,作为我们的目标类别。
对图像进行二值化
二值化过程是图像处理中的常见步骤。它正式被称为图像阈值化,因为我们需要一个阈值来决定哪些值变为零,哪些变为一。关于这个主题的详细调查,请参考(Sezgin, M., 和 Sankur, B.(2004))。这意味着,在选择完美阈值时,背后有一门科学,这个阈值能够最小化从[0, 255]到[0, 1]的范围转换误差。
然而,由于这不是一本关于图像处理的书籍,我们将任意设置阈值为 128。这样,任何小于 128 的值将变为零,任何大于或等于 128 的值将变为一。
这一过程可以通过在 Python 中使用索引轻松完成。接下来,我们将显示数据集的一小部分,以确保数据已经正确转换。我们将通过执行以下命令来实现这一点:
- 为了加载数据集并验证其维度(形状),请运行以下命令:
from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784')
print(mnist.data.shape)
print(mnist.target.shape)
以下是输出结果:
(70000, 784)
(70000,)
首先需要注意的是,我们正在使用一个名为scikit learn
或sklearn
的机器学习库,它是 Python 中最常用的通用机器学习库之一。MNIST
数据集是通过fetch_openml()
方法加载的,该方法需要传入一个数据集标识符,当前数据集的标识符是'mnist_784'
。数字784
来自于MNIST
图像的大小,即 28 x 28 像素,可以将其看作一个由 784 个元素组成的向量,而不是 28 列 28 行的矩阵。通过验证shape
属性,我们可以看到该数据集包含 70,000 张图像,这些图像被表示为大小为 784 的向量,目标也是按照相同比例表示的。
请注意,与前一节使用加载到 pandas 中的数据集不同,在这个例子中,我们直接使用数据作为列表或列表的数组。你应该能自如地处理 pandas 和原始数据集。
- 要实际进行二值化,并验证数据前后的变化,运行以下命令:
print(mnist.data[0].reshape(28, 28)[10:18,10:18])
mnist.data[mnist.data < 128] = 0
mnist.data[mnist.data >=128] = 1
print(mnist.data[0].reshape(28, 28)[10:18,10:18])
这将输出以下内容:
[[ 1\. 154\. 253\. 90\. 0\. 0\. 0\. 0.]
[ 0\. 139\. 253\. 190\. 2\. 0\. 0\. 0.]
[ 0\. 11\. 190\. 253\. 70\. 0\. 0\. 0.]
[ 0\. 0\. 35\. 241\. 225\. 160\. 108\. 1.]
[ 0\. 0\. 0\. 81\. 240\. 253\. 253\. 119.]
[ 0\. 0\. 0\. 0\. 45\. 186\. 253\. 253.]
[ 0\. 0\. 0\. 0\. 0\. 16\. 93\. 252.]
[ 0\. 0\. 0\. 0\. 0\. 0\. 0\. 249.]]
[[ 0\. 1\. 1\. 0\. 0\. 0\. 0\. 0.]
[ 0\. 1\. 1\. 1\. 0\. 0\. 0\. 0.]
[ 0\. 0\. 1\. 1\. 0\. 0\. 0\. 0.]
[ 0\. 0\. 0\. 1\. 1\. 1\. 0\. 0.]
[ 0\. 0\. 0\. 0\. 1\. 1\. 1\. 0.]
[ 0\. 0\. 0\. 0\. 0\. 1\. 1\. 1.]
[ 0\. 0\. 0\. 0\. 0\. 0\. 0\. 1.]
[ 0\. 0\. 0\. 0\. 0\. 0\. 0\. 1.]]
指令 data[0].reshape(28, 28)[10:18,10:18]
做了三件事:
-
data[0]
返回第一张图像,大小为(1, 784)的数组。 -
reshape(28, 28)
将(1, 784)数组调整为(28, 28)矩阵,这就是实际的图像;这对于显示实际数据很有用,例如生成图 3.1。 -
[10:18,10:18]
仅获取(28, 28)矩阵中列和行从 10 到 18 的位置的子集;这大致对应于图像的中心区域,是查看变化的好地方。
上述是仅用于查看数据,但实际的更改在接下来的行中完成。行mnist.data[mnist.data < 128] = 0
使用了 Python 索引。指令 mnist.data < 128
返回一个布尔值的多维数组,mnist.data[ ]
使用该数组作为索引,将相应的值设置为零。关键是对所有严格小于 128 的值执行此操作。接下来的行做了相同的操作,但适用于大于或等于 128 的值。
通过检查输出,我们可以确认数据已经成功改变,并已被阈值化或二值化。
目标的二值化
我们将通过以下两个步骤对目标进行二值化:
- 首先,我们将丢弃其他数字的图像数据,只保留 7 和 8。然后,我们将 7 映射为 0,8 映射为 1。 这些命令将创建新的变量
X
和y
,它们仅包含数字 7 和 8:
X = mnist.data[(mnist.target == '7') | (mnist.target == '8')]
y = mnist.target[(mnist.target == '7') | (mnist.target == '8')]
print(X.shape)
print(y.shape)
这将输出以下内容:
(14118, 784)
(14118)
注意使用了OR
运算符,|
,用于逻辑地将两个布尔索引集合进行“或”操作,并产生一个新的布尔索引集合。这些索引用于生成一个新的数据集。新数据集的形状包含略多于 14,000 张图像。
- 为了将 7 映射为 0,8 映射为 1,我们可以运行以下命令:
print(y[:10])
y = [0 if v=='7' else 1 for v in y]
print(y[:10])
这将输出以下内容:
['7' '8' '7' '8' '7' '8' '7' '8' '7' '8']
[0, 1, 0, 1, 0, 1, 0, 1, 0, 1]
指令 [0 if v=='7' else 1 for v in y]
检查y
中的每个元素,如果某个元素是'7'
,则返回0
,否则(例如,当它是'8'
时),返回1
。正如输出所示,选择前 10 个元素,数据被二值化为集合{0
, 1
}。
请记住,y
中的目标数据已经是二值化的,因为它只包含两个可能的唯一数字集{7
, 8
}。但是我们将它二值化为集合{0
, 1
},因为在使用不同的深度学习算法时,这通常更好,尤其是当这些算法计算非常特定类型的损失函数时。
这样,数据集已经准备好与二分类器和通用分类器一起使用。但是,如果我们实际上想要拥有多个类别呢?例如,要检测MNIST
数据集中的所有 10 个数字,而不仅仅是 2 个数字?或者,如果我们有特征、列或输入,它们不是数字而是分类数据呢?接下来的部分将帮助你在这些情况下准备数据。
分类数据和多类别
现在你知道如何将数据二值化以适应不同目的后,我们可以看看其他类型的数据,例如分类数据或多标签数据,以及如何将它们转换为数字。事实上,大多数先进的深度学习算法只接受数值数据。这只是一个设计问题,可以在以后轻松解决,这不是什么大问题,因为你将学到有简单的方法将分类数据转换为有意义的数值表示。
分类数据包含作为不同类别嵌入的信息。这些类别可以表示为数字或字符串。例如,一个数据集有一列名为country
,其中包含“印度”、“墨西哥”、“法国”和“美国”等项目。或者,一个包含邮政编码的数据集,如 12601、85621 和 73315。前者是非数字分类数据,后者是数字分类数据。国家名称需要转换为数字才能使用,但邮政编码已经是数字,作为单纯的数字是没有意义的。从机器学习的角度来看,如果我们将邮政编码转换为纬度和经度坐标,邮政编码会变得更加有意义;这种方式能更好地捕捉到彼此更近的地方,而不是直接使用数字。
首先,我们将解决将字符串类别转换为普通数字的问题,然后将其转换为一种称为独热编码的格式。
将字符串标签转换为数字
我们将再次使用MNIST
数据集,并使用其字符串标签,0,1,...,9,并将它们转换为数字。我们可以通过多种方式实现这一点:
-
我们可以通过一个简单的命令,
y = list(map(int, mnist.target))
,将所有字符串映射到整数,并完成任务。现在,变量y
仅包含像[8, 7, 1, 2, ...]
这样的整数列表。但这只解决了这个特定情况的问题;你需要学习一些适用于所有情况的方法。所以,我们不应该这样做。 -
我们可以通过反复遍历数据 10 次来做一些繁琐的工作——
mnist.target = [0 if v=='0' else v for v in mnist.target]
——对每个数字都这样做。但同样,这样做(以及其他类似的事情)只对这种情况有效。我们不应该这样做。 -
我们可以使用 scikit-learn 的
LabelEncoder()
方法,它将任何标签列表映射到一个数字。这对于所有情况都有效。
让我们通过以下步骤使用scikit
方法:
- 运行以下代码:
from sklearn import preprocessing
le = preprocessing.LabelEncoder()
print(sorted(list(set(mnist.target))))
le.fit(sorted(list(set(mnist.target))))
这将产生以下输出:
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
LabelEncoder()
sorted(list(set(mnist.target)))
命令执行了三件事:
-
set(mnist.target)
提取数据中唯一值的集合,例如,{'8', '2', ..., '9'}
。 -
list(set(mnist.target))
只是将集合转换为列表,因为我们需要列表或数组供LabelEncoder()
方法使用。 -
这里
sorted(list(set(mnist.target)))
非常重要,以确保0 映射到 0,而不是8 映射到 0,依此类推。它对列表进行排序,结果如下 -['0', '1', ..., '9']
。
le.fit()
方法接受一个列表(或数组),并生成一个映射(字典),用于将标签或字符串编码为数字,并将其存储在LabelEncoder
对象中,以供将来使用(如果需要,也可以向后编码)。
- 接下来,我们可以按如下方式测试编码:
print(le.transform(["9", "3", "7"]) )
list(le.inverse_transform([2, 2, 1]))
这将输出以下内容:
[9 3 7]
['2', '2', '1']
transform()
方法将基于字符串的标签转换为数字,而inverse_transform()
方法接受数字并返回对应的字符串标签或类别。
任何尝试映射到未见过的类别或数字将导致LabelEncoder
对象产生错误。请在提供所有可能类别列表时,尽量确保其完整。
- 一旦
LabelEncoder
对象被拟合并测试完成,我们可以简单地运行以下指令来编码数据:
print("Before ", mnist.target[:3])
y = le.transform(mnist.target)
print("After ", y[:3])
这将输出以下内容:
Before ['5' '0' '4']
After [5 0 4]
新的编码标签现在存储在y
中,准备使用。
这种将标签编码为整数的方法也称为顺序编码。
该方法适用于所有作为字符串编码的标签,在这些情况下,您可以简单地映射为数字,而不会丧失上下文。在MNIST
数据集中,我们可以将0映射为 0,将7映射为 7,而不会丧失上下文。以下是您可以进行此操作的其他示例:
-
年龄段:['18-21', '22-35', '36+'] 转换为 [0, 1, 2]
-
性别:['male', 'female'] 转换为 [0, 1]
-
颜色:['red', 'black', 'blue', ...] 转换为 [0, 1, 2, ...]
-
学位:['primary', 'secondary', 'high school', 'university'] 转换为 [0, 1, 2, 3]
然而,我们在这里做了一个重要假设:标签本身不编码任何特殊含义。正如我们之前提到的,邮政编码可以简单地编码为较小的数字;然而,它们有地理意义,进行这样的编码可能会对我们的深度学习算法性能产生负面影响。类似地,在前述列表中,如果研究需要某种特殊含义,表示大学学位比小学学位要更高或更重要,那么我们可能需要考虑不同的数字映射。或者,也许我们希望我们的学习算法能够学习这些细微差别!在这种情况下,我们应该使用广为人知的独热编码策略。
将类别转换为独热编码
在大多数情况下,如果类别或标签之间可能存在特殊含义,将类别转换为独热编码是更好的选择。在这种情况下,独热编码被认为比顺序编码更有效 [Potdar, K., et al. (2017)]。
其思想是将每个标签表示为一个布尔状态,并具有独立的列。例如,假设有以下数据列:
性别 |
---|
'女性' |
'男性' |
'男性' |
'女性' |
'女性' |
这可以通过一热编码唯一地转换为以下新数据:
性别 _ 女性 | 性别 _ 男性 |
---|---|
1 | 0 |
0 | 1 |
0 | 1 |
1 | 0 |
1 | 0 |
如您所见,二进制位是热(为 1),只有当标签对应于特定行时,它才为 1,否则为 0。还要注意,我们重新命名了列,以便追踪哪个标签对应哪个列;然而,这仅仅是推荐的格式,并不是正式规则。
在 Python 中,我们有多种方式可以做到这一点。如果您的数据在 pandas DataFrame 中,那么您可以直接执行 pd.get_dummies(df, prefix=['性别'])
,假设您的列在 df
中,且您希望使用 性别
作为前缀。
要重现前表中讨论的确切结果,请按照以下步骤操作:
- 运行以下命令:
import pandas as pd
df=pd.DataFrame({'Gender': ['female','male','male',
'female','female']})
print(df)
这将输出以下内容:
Gender
0 female
1 male
2 male
3 female
4 female
- 现在只需通过运行以下命令来进行编码:
pd.get_dummies(df, prefix=['Gender'])
生成的结果如下:
Gender_female Gender_male
0 1 0
1 0 1
2 0 1
3 1 0
4 1 0
这种编码的一个有趣且可能显而易见的特性是,所有编码列的行上的 OR
和 XOR
操作将始终为 1,而 AND
操作将返回 0。
对于数据不是 pandas DataFrame 的情况,例如 MNIST 目标,我们可以使用 scikit-learn 的 OneHotEncoder.transform()
方法。
OneHotEncoder
对象有一个构造函数,它会自动初始化所有参数并通过 fit()
方法确定大部分参数。它会确定数据的大小、数据中存在的不同标签,并创建一个动态映射,供我们与 transform()
方法一起使用。
要对 MNIST
目标进行一热编码,我们可以这样做:
from sklearn.preprocessing import OneHotEncoder
enc = OneHotEncoder()
y = [list(v) for v in mnist.target] # reformat for sklearn
enc.fit(y)
print('Before: ', y[0])
y = enc.transform(y).toarray()
print('After: ', y[0])
print(enc.get_feature_names())
这将输出以下内容:
Before: ['5']
After: [0\. 0\. 0\. 0\. 0\. 1\. 0\. 0\. 0\. 0.]
['x0_0' 'x0_1' 'x0_2' 'x0_3' 'x0_4' 'x0_5' 'x0_6' 'x0_7' 'x0_8' 'x0_9']
这段代码包括我们的经典合理性检查,我们验证标签 '5'
确实被转换为一个具有 10 列的行向量,其中数字 6
是热的。它按预期工作。y
的新维度是n行和 10 列。
这是在 MNIST 上使用深度学习方法时,目标的首选格式。一热编码目标非常适合神经网络,其中每个类别只有一个神经元。在这种情况下,每个数字对应一个神经元。每个神经元将需要学习预测一热编码行为,即只有一个神经元应该被激活(即“热”),而其他神经元应该被抑制。
前述过程可以完全重复,用于将任何其他列转换为一热编码,前提是它们包含分类数据。
类别、标签以及将它们映射到整数或位的特定映射,在我们想将输入数据分类到这些类别、标签或映射时非常有用。但是,如果我们想要输入数据映射到连续数据该怎么办?例如,通过查看一个人的反应来预测其智商;或者根据天气和季节的输入数据预测电价。这就是所谓的回归数据,我们将在接下来讨论。
实值数据和单变量回归
在使用基于深度学习的分类模型时,了解如何处理类别数据非常重要;然而,了解如何为回归准备数据同样重要。包含类似连续实值的数据,如温度、价格、体重、速度等,适合用于回归;也就是说,如果我们有一个包含不同类型值的 dataset,其中有一列是实值数据,我们可以对该列进行回归分析。这意味着我们可以利用数据集中的其他所有数据来预测该列的值。这就是所谓的单变量回归,即对一个变量的回归分析。
大多数机器学习方法在回归数据归一化后表现更好。我们所说的归一化是指数据将具有特定的统计性质,从而使计算更加稳定。这对于许多深度学习算法至关重要,因为它们容易受到梯度消失或爆炸的影响(Hanin, B. (2018))。例如,在神经网络中计算梯度时,误差需要从输出层向输入层反向传播;但是,如果输出层的误差很大且值的范围(即它们的分布)也很大,那么反向传播时的乘法可能会导致变量溢出,从而破坏训练过程。
为了克服这些困难,最好对可用于回归的变量或实值变量进行归一化处理。归一化过程有许多变体,但我们将讨论两种主要方法,一种是设置数据的特定统计性质,另一种是设置数据的特定范围。
缩放到特定范围的值
让我们回到本章前面讨论的心脏病数据集。如果你仔细观察,许多变量是实值数据,非常适合回归;例如,x[5]和x[10]。
所有变量都适合回归。这意味着,从技术上讲,我们可以对任何数值数据进行预测。某些值是实值数据使它们在回归中更具吸引力,原因有很多。例如,这一列中的值具有超越整数和自然数的含义。
让我们聚焦于 x[5] 和 x[10],它们分别是用于衡量胆固醇水平和运动引起的 ST 残差的变量。如果我们想改变医生最初的研究问题,原本是基于不同因素研究心脏病,那么如果现在我们想用所有因素(包括知道病人是否患有心脏病)来确定或预测他们的胆固醇水平呢?我们可以通过对 x[5] 进行回归来实现。
因此,为了准备 x[5] 和 x[10] 数据,我们将继续进行数据缩放。为了验证,我们将分别检索数据缩放前后的描述性统计信息。
为了重新加载数据集并显示描述性统计信息,我们可以执行以下操作:
df = pd.read_csv('processed.cleveland.data', header=None)
df[[4,9]].describe()
在这种情况下,索引 4
和 9
分别对应 x[5] 和 x[10],describe()
方法输出以下信息:
4 9
count 303.000000 303.000000
mean 246.693069 1.039604
std 51.776918 1.161075
min 126.000000 0.000000
25% 211.000000 0.000000
50% 241.000000 0.800000
75% 275.000000 1.600000
max 564.000000 6.200000
最显著的属性是该列中包含的均值和最大值/最小值。这些值在我们将数据缩放到不同范围时会发生变化。如果我们将数据可视化为带有相应直方图的散点图,结果就像 图 3.2:
图 3.2 – 两列 x[5] 和 x[10] 的散点图及其相应的直方图
从 图 3.2 中可以看到,范围差异很大,数据的分布也不同。这里所需的新范围是最小值为 0,最大值为 1。这个范围在我们进行数据缩放时是典型的。可以使用 scikit-learn 的 MinMaxScaler
对象来实现,代码如下:
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
scaler.fit(df[[4,9]])
df[[4,9]] = scaler.transform(df[[4,9]])
df[[4,9]].describe()
这将输出以下内容:
4 9
count 303.000000 303.000000
mean 0.275555 0.167678
std 0.118212 0.187270
min 0.000000 0.000000
25% 0.194064 0.000000
50% 0.262557 0.129032
75% 0.340183 0.258065
max 1.000000 1.000000
fit()
方法内部的工作原理是确定数据的当前最小值和最大值。然后,transform()
方法使用这些信息来去除最小值并除以最大值,从而实现所需的范围。正如所见,新的描述性统计信息已经发生变化,可以通过查看 图 3.3 中坐标轴的范围来确认:
图 3.3 – 新缩放后的 x[5] 和 x[10] 的散点图及其相应的直方图
然而,如果你仔细观察,你会发现数据的分布没有发生变化。也就是说,图 3.2 和 图 3.3 中数据的直方图依然相同。这是一个非常重要的事实,因为通常情况下,你并不希望改变数据的分布。
标准化为零均值和单位方差
预处理实数值数据的另一种方式是使其具有零均值和单位方差。这个过程有许多不同的名称,比如标准化、z-score 标准化、居中处理等。
假设 x = [x[5], x[10]],根据我们之前的特征,接下来可以按如下方式对 x 进行标准化:
这里,µ 是一个对应于x每列均值的向量,σ 是一个对应于x每列标准差的向量。
在标准化x后,如果我们重新计算均值和标准差,应该得到均值为零,标准差为一。在 Python 中,我们可以这样做:
df[[4,9]] = (df[[4,9]]-df[[4,9]].mean())/df[[4,9]].std()
df[[4,9]].describe()
这将输出如下内容:
4 9
count 3.030000e+02 3.030000e+02
mean 1.700144e-16 -1.003964e-16
std 1.000000e+00 1.000000e+00
min -2.331021e+00 -8.953805e-01
25% -6.893626e-01 -8.953805e-01
50% -1.099538e-01 -2.063639e-01
75% 5.467095e-01 4.826527e-01
max 6.128347e+00 4.444498e+00
注意,标准化后,均值在数值上为零,标准差为一。当然,也可以使用 scikit-learn 中的StandardScaler
对象来完成相同的操作,如下所示:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaler.fit(df[[4,9]])
df[[4,9]] = scaler.transform(df[[4,9]])
这将产生相同的结果,数值差异可以忽略不计。对于实际应用而言,两种方法都会达到相同的效果。
尽管直接在数据框中或使用StandardScaler
对象进行标准化都合适,但如果你在处理生产应用时,应该更倾向于使用StandardScaler
对象。一旦StandardScaler
对象使用fit()
方法,它可以通过重新调用transform()
方法轻松应用于新的、未见过的数据;然而,如果直接在 pandas 数据框上进行操作,我们就必须手动存储均值和标准差,并在每次需要标准化新数据时重新加载它们。
现在,为了进行比较,图 3.4 显示了数据标准化后的新范围。如果仔细观察坐标轴,你会发现零值的位置是数据最多的地方,也就是均值所在的位置。因此,数据簇围绕零均值居中:
图 3.4 – 标准化列 x[5] 和 x[10] 的散点图及其相应的直方图
再次注意,在图 3.4 中,应用标准化过程后,数据的分布仍然没有变化。但是如果你确实想改变数据的分布呢?请继续阅读下一节。
改变数据分布
已经证明,改变目标的分布,特别是在回归的情况下,可以在学习算法的性能上带来积极的效果(Andrews, D. F., 等人,1971 年)。
在这里,我们将讨论一种特别有用的转换方法,称为分位数转换。这种方法的目标是查看数据并以某种方式对其进行处理,使得其直方图遵循正态分布或均匀分布。它通过查看分位数估计值来实现这一点。
我们可以使用以下命令来转换与上一节相同的数据:
from sklearn.preprocessing import QuantileTransformer
transformer = QuantileTransformer(output_distribution='normal')
df[[4,9]] = transformer.fit_transform(df[[4,9]])
这将有效地将数据映射到一个新的分布,即正态分布。
这里,正态分布一词指的是类似高斯的概率密度函数(PDF)。这是任何统计学教科书中都能找到的经典分布。通常,当绘制时,它会呈现出钟形曲线的形状。
请注意,我们还使用了fit_transform()
方法,它同时执行fit()
和transform()
,这样做很方便。
如图 3.5 所示,与胆固醇数据相关的变量 x[5] 很容易被转化为具有钟形的正态分布。然而,对于 x[10],在某个特定区域数据的密集出现使得分布呈现钟形,但带有长尾,这种情况并不理想:
图 3.5 – 正态变换后的列 x[5] 和 x[10] 的散点图及其对应的类高斯直方图
将数据转化为均匀分布的过程非常相似。我们只需在QuantileTransformer()
构造函数中的一行做一个小小的修改,如下所示:
transformer = QuantileTransformer(output_distribution='uniform')
现在,数据已经被转化为均匀分布,如图 3.6 所示:
图 3.6 – 均匀变换后的列 x[5] 和 x[10] 的散点图及其对应的均匀直方图
从图中可以看出,数据在每个变量上的分布已经变得均匀。再次强调,数据在特定区域的聚集效应导致同一区域内有大量值集中,这种情况并不理想。这个伪影还会在数据的分布中造成空隙,通常很难处理,除非我们使用数据增强技术,接下来我们将讨论这一点。
数据增强
现在你已经学会如何处理数据以使其具备特定的分布,接下来你需要了解数据增强,通常它与缺失数据或高维数据相关。传统的机器学习算法在处理维度超过样本数的数据时可能会遇到问题。这个问题并不适用于所有深度学习算法,但一些算法在学习建模时,若变量数目大于可用样本数目,学习就会变得更加困难。为了解决这个问题,我们有几个选择:要么减少维度或变量(参见下一节),要么增加数据集中的样本量(本节讨论)。
增加更多数据的工具之一被称为数据增强(Van Dyk, D. A., 和 Meng, X. L. (2001))。在本节中,我们将使用MNIST
数据集来举例说明一些特定于图像的数据增强技术,但这些技术的概念可以扩展到其他类型的数据。
我们将涵盖基本内容:添加噪声、旋转和重缩放。也就是说,从一个原始示例中,我们将生成三个新的、不一样的数字图像。我们将使用被称为scikit image
的图像处理库。
重缩放
我们像之前那样重新加载MNIST
数据集:
from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784')
然后,我们可以简单地调用rescale()
方法来创建一个重新缩放的图像。重新调整图像大小的整个目的是将图像重新缩放回原始尺寸,因为这使得图像看起来像是原始图像的小分辨率版本。虽然在这个过程中会失去一些特征,但实际上它能让深度学习模型变得更健壮。也就是说,模型能适应物体的尺度,或者在这种情况下,能适应数字的尺度:
from skimage.transform import rescale
x = mnist.data[0].reshape(28,28)
一旦我们有了作为原始图像的x
,我们可以按照以下方式进行缩放操作:
s = rescale(x, 0.5, multichannel=False)
x_= rescale(s, 2.0, multichannel=False)
在这里,增强后的图像(已缩放)是x_
。请注意,在这个案例中,图像先按 50%的比例缩小,再按 200%的比例放大。由于图像只有一个单一的通道,即为灰度图,所以multichannel
参数设置为false
。
在重新缩放时,要小心使用能够得到精确除法因子的缩放比例。例如,一个 28 x 28 的图像,如果按 0.5 的比例缩小,会变成 14 x 14;这是好的。但是,如果按 0.3 的比例缩小,图像会变成 8.4 x 8.4,最终变成 9 x 9;这不好,因为它可能会带来不必要的复杂性。保持简单。
除了重新缩放,我们还可以稍微修改现有的数据,使其在不偏离原始图像太多的情况下,产生一些变体,接下来我们将讨论这一点。
添加噪声
同样,我们还可以向原始图像添加高斯噪声。这会在整个图像上生成随机模式,以模拟摄像头问题或噪声采集。这里我们使用它来增强我们的数据集,并最终生成一个能够抗噪声的深度学习模型。
为此,我们使用random_noise()
方法,如下所示:
from skimage.util import random_noise
x_ = random_noise(x)
再次强调,增强后的图像(带噪声)是x_
。
除了噪声之外,我们还可以稍微改变图像的视角,以便在不同角度下保留原始形状,接下来我们会讨论这一点。
旋转
我们可以对图像应用简单的旋转效果,以获得更多的数据。图像的旋转是学习良好特征的关键部分。更大的数据集通常包含许多略微旋转或完全旋转的图像。如果我们的数据集中没有这样的图像,我们可以手动旋转它们并增强我们的数据。
为此,我们使用rotate()
方法,如下所示:
from skimage.transform import rotate
x_ = rotate(x, 22)
在这个例子中,数字22
指定了旋转的角度:
当你在增强数据集时,你可能想要考虑在随机角度上进行多次旋转。
图 3.7 – 使用前面提到的数据增强技术生成的图像示例
第一列是 MNIST 数据集中的原始数字。第二列显示了重新缩放的效果。第三列显示了原始图像加上高斯噪声。最后一列显示了分别为 20 度(上)和-20 度(下)的旋转效果。
其他增强技术
对于图像数据集,还有其他一些增强数据的想法,包括以下内容:
-
改变图像的投影
-
添加压缩噪声(对图像进行量化)
-
除了高斯噪声,还有其他类型的噪声,比如椒盐噪声或乘法噪声
-
随机以不同距离平移图像
但最强大的增强方式是将所有这些方法结合起来!
图像很有趣,因为它们在局部区域高度相关。但对于一般的非图像数据集,例如心脏病数据集,我们可以通过其他方式来增强数据,例如:
-
添加低方差的高斯噪声
-
添加压缩噪声(量化)
-
从计算得到的概率密度函数中绘制新点
对于其他特殊数据集,如基于文本的数据,我们还可以执行以下操作:
-
用同义词替换一些单词
-
删除一些单词
-
添加包含错误的单词
-
删除标点符号(仅当您不在乎语言结构的正确性时)
关于此以及许多其他增强技术的更多信息,请查阅关于您特定数据类型的最新进展的在线资源。
现在让我们深入探讨一些可以用于缓解高维和高度相关数据集问题的降维技术。
数据降维
如前所述,如果我们面临数据中维度(或变量)大于样本数的问题,我们可以通过增强数据或减少数据的维度来解决这个问题。现在,我们将讨论后者的基础知识。
我们将研究如何在有监督和无监督的情况下,通过小型和大型数据集减少维度。
有监督算法
有监督的降维算法之所以称为有监督,是因为它们考虑了数据的标签,以找到更好的表示。这类方法通常会产生良好的结果。也许最流行的一种是线性判别分析(LDA),我们接下来将讨论它。
线性判别分析
Scikit-learn 有一个LinearDiscriminantAnalysis
类,可以轻松地对所需的组件数进行降维。
组件数指的是所需的维度数量。这个名称来自于主成分分析(PCA),它是一种统计方法,用于确定数据集的中心化协方差矩阵的特征向量和特征值;然后,与特定特征向量关联的最大特征值被认为是最重要的、主成分。当我们使用 PCA 降到特定数量的组件时,我们说我们想要保留那些在由数据的协方差矩阵的特征值和特征向量诱导的空间中最重要的组件。
LDA 和其他降维技术也有类似的哲学思想,旨在找到低维空间(基于所需的组件数),并根据数据的其他属性更好地表示数据。
如果我们以心脏病数据集为例,我们可以使用 LDA 将整个数据集从 13 维降至 2 维,同时使用标签[0, 1, 2, 3, 4]来指导 LDA 算法更好地分离这些标签所代表的组。
为了实现这一点,我们可以按照以下步骤进行:
- 首先,我们重新加载数据并删除缺失值:
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
df = pd.read_csv('processed.cleveland.data', header=None)
df = df.apply(pd.to_numeric, errors='coerce').dropna()
请注意,在心脏病数据集中我们之前不需要处理缺失值,因为 pandas 会自动忽略缺失值。但在这里,由于我们严格地将数据转换为数字,缺失值会被转换为NaN
,因为我们指定了errors='coerce'
,这会将任何转换错误强制转换为NaN
。因此,通过dropna()
,我们会忽略包含这些值的行,因为它们会导致 LDA 失败。
- 接下来,我们准备
X
和y
变量,分别包含数据和目标,然后我们执行 LDA,如下所示:
X = df[[0,1,2,3,4,5,6,7,8,9,10,11,12]].values
y = df[13].values
dr = LinearDiscriminantAnalysis(n_components=2)
X_ = dr.fit_transform(X, y)
在这个例子中,X_
包含了整个数据集,并通过n_components=2
表示为二维数据。选择两个组件仅仅是为了图形上展示数据的外观。但你可以根据需要将其更改为任意数量的组件。
图 3.8 展示了如果将 13 维数据集压缩或降至二维后的效果:
图 3.8 – 使用 LDA 将维度从 13 降至 2
请注意,值为 0(没有心脏病)的点大多数聚集在左侧,而其余的值(即 1、2、3 和 4,表示心脏病)则似乎聚集在右侧。这是一个很好的特性,在我们从 13 个维度中选择两列时,图 3.2 到 图 3.6 中并没有观察到这种现象。
从技术上讲,13 维度的相关信息仍然保留在 LDA 所生成的二维空间中。如果数据在这些低维表示中似乎可以分离,深度学习算法可能有很大机会学习到表现良好的分类或回归表示。
虽然 LDA 可以提供一种非常好的方法,通过数据中的标签进行降维,但我们并不总是拥有标注数据,或者我们可能不希望使用现有的标签。在这种情况下,我们可以并且应该探索其他不需要标签信息的鲁棒方法,比如无监督技术,接下来我们将讨论这些技术。
无监督 技术
无监督技术是最受欢迎的方法,因为它们不需要关于标签的先验信息。我们从 PCA 的核化版本开始,然后再处理适用于更大数据集的方法。
核 PCA
这种 PCA 的变种使用核方法来估计距离、方差和其他参数,以确定数据的主要成分(Schölkopf, B. 等人,(1997))。与常规 PCA 相比,它可能需要更多的时间来得出解决方案,但相对于传统的 PCA,使用它非常值得。
scikit-learn 的 KernelPCA
类可以按如下方式使用:
from sklearn.decomposition import KernelPCA
dr = KernelPCA(n_components=2, kernel='linear')
X_ = dr.fit_transform(X)
同样,我们使用两个维度作为新的空间,并使用 'linear'
核函数。核函数的其他常见选择包括:
-
'rbf'
用于径向基函数核 -
'poly'
用于多项式核函数
就个人而言,我通常喜欢使用 'rbf'
核函数,因为它更强大且稳健。但很多时候,你会花费宝贵的时间来确定参数 γ 的最佳值,γ 决定了径向基函数的“钟形”宽度。如果你有时间,可以尝试 'rbf'
并实验参数 gamma
。
使用核 PCA 的结果如 图 3.9 所示。图中再次显示了负类(无心脏病,值为 0)在 KPCA 诱导空间的左下方的聚类排列。正类(心脏病,值 ≥ 1)则倾向于向上聚类:
图 3.9 – 使用核 PCA 将维度从 13 降到 2
与 图 3.8 相比,LDA 产生了一个略微更好的空间,可以将不同组分隔开。然而,尽管现在不知道实际的目标类别,KPCA 仍然做得很好。现在,LDA 和 KPCA 在小数据集上可能几乎不需要时间,但如果数据量很大呢?我们接下来将讨论一些选项。
大数据集
之前的例子在中等大小的数据集上效果很好。然而,在处理非常大的数据集时,即维度或样本数量非常多时,一些算法可能无法发挥最佳效果。在最坏的情况下,它们可能无法得出解决方案。接下来的两个无监督算法通过使用一种名为 批量训练 的技术,旨在对大数据集表现良好。这种技术是众所周知的,并已成功应用于机器学习(Hinton, G. E.,(2012))。
主要思路是将数据集划分为小(迷你)批次,并逐步向寻找问题的全局解推进。
稀疏主成分分析(Sparse PCA)
我们首先来看看 scikit-learn 中的稀疏编码版本的 PCA,即 MiniBatchSparsePCA
。该算法将确定满足稀疏性约束的最佳子空间转换。
稀疏性 是矩阵(或向量)的一种特性,其中大多数元素是零。稀疏性的反面是密集性。在深度学习中,我们喜欢稀疏性,因为我们进行大量的张量(向量)乘法,如果某些元素为零,我们就不需要执行这些乘法,从而节省时间并优化速度。
按照以下步骤使用 MNIST
数据集并减少其维度,因为它有 784 个维度和 70,000 个样本。数据集足够大,但即使是更大的数据集也可以使用:
- 我们从重新加载数据并为稀疏 PCA 编码做准备开始:
from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784')
X = mnist.data
- 然后我们按如下方式进行降维:
from sklearn.decomposition import MiniBatchSparsePCA
dr = MiniBatchSparsePCA(n_components=2, batch_size=50,
normalize_components=True)
X_ = dr.fit_transform(X)
这里,MiniBatchSparsePCA()
构造函数接受三个参数:
-
n_components
,我们为了可视化的目的将其设置为 2。 -
batch_size
决定算法每次使用多少样本。我们将其设置为50
,但更大的数字可能会导致算法变慢。 -
normalize_components
是指通过中心化数据进行预处理,即使其均值为零,方差为单位;我们建议每次都进行此操作,特别是当你的数据高度相关时,比如图像数据。
MNIST
数据集通过稀疏 PCA 变换后,如图 3.10所示:
图 3.10 – 使用稀疏 PCA 将 MNIST 数据集降维到二维
如你所见,类别之间的分离并不完全清晰。虽然有一些明显的数字聚类,但由于组之间的重叠,看起来并不是一项简单的任务。这部分是由于许多数字可能相似。例如,数字 1 和 7 可能会聚在一起(左右上下),或者 3 和 8 可能会聚在一起(中间和上面)。
但我们也可以使用另一个流行且有用的算法,称为字典学习。
字典学习
字典学习是通过使用一种可以轻松扩展到非常大数据集的过程,学习变换的基础(称为字典)(Mairal, J., 等, 2009)。
使用基于 PCA 的算法无法做到这一点,但这种技术仍然非常强大,最近在全球主要会议之一——2019 年国际机器学习大会上获得了终身成就奖。
该算法可以通过 MiniBatchDictionaryLearning
类在 scikit-learn 中使用。我们可以如下使用:
from sklearn.decomposition import MiniBatchDictionaryLearning
dr = MiniBatchDictionaryLearning(n_components=2, batch_size=50)
X_ = dr.fit_transform(X)
构造函数 MiniBatchDictionaryLearning()
接受与 MiniBatchSparsePCA()
相似的参数,且意义相同。所学习到的空间结果如图 3.11所示:
图 3.11 – 使用字典学习将 MNIST 数据降维到二维
如图所示,即使存在明显定义的聚类,类别之间仍然存在显著的重叠。如果将此数据(二维数据)用作输入来训练分类器,可能会导致性能不佳。这并不意味着算法不好。可能的原因是,二维数据可能不是最终维度的最佳选择。继续阅读以了解更多信息。
关于维度的数量
降维并不是总是必要的步骤,但对于高度相关的数据,例如图像数据,强烈建议进行降维。
所有讨论过的降维技术其实都是力图去除数据中的冗余信息,并保留重要内容。如果我们要求算法将一个非相关、非冗余的数据集从 13 维降到 2 维,那听起来有些冒险;或许选择 8 维或 9 维会是更好的选择。
任何认真的机器学习者都不会尝试将一个 784 维的非相关、非冗余数据集降到仅仅 2 维。即使数据高度相关且冗余,比如MNIST
数据集,要求从 784 维降到 2 维也是一个极大的挑战。这是一个非常冒险的决定,可能会丢失重要的、具有判别力的相关信息;或许选择 50 维或 100 维会是更好的选择。
没有普适的方法来找出合适的维度数量。这是一个需要实验的过程。如果你想在这方面做到优秀,你必须尽职尽责,至少进行两个或更多不同维度的实验。
操作数据的伦理影响
操作数据时有许多伦理影响和风险,你需要了解。我们生活在一个大多数深度学习算法必须经过纠正的世界中,因为发现它们存在偏见或不公平现象,这通常需要通过重新训练来进行修正。这是非常不幸的;你需要成为一个负责任地使用人工智能并生产经过深思熟虑模型的人。
在操作数据时,要小心仅仅因为认为异常值会影响模型性能就把它们移除。有时,异常值代表了保护群体或少数群体的信息,删除这些异常值会助长不公平现象,并且引入对多数群体的偏见。除非你完全确定这些异常值是由故障传感器或人为错误造成的,否则应避免删除异常值。
小心你对数据分布的转化方式。大多数情况下,改变数据的分布是可以的,但如果你处理的是人口统计数据,必须特别关注你所做的转化。
处理人口信息(如性别)时,如果将女性和男性分别编码为 0 和 1,在考虑比例时可能会存在风险;我们需要小心,不要推广一种不反映使用你模型的社区现实的平等(或不平等)。唯一的例外是,当我们当前的现实展示出非法歧视、排斥和偏见时。在这种情况下,我们的模型(基于我们的数据)不应反映这一现实,而应反映我们社区希望实现的合法现实。也就是说,我们将准备良好的数据,创建反映我们期望成为的社会的模型,而不是延续社会问题的模型。
总结
在本章中,我们讨论了许多数据处理技术,这些技术我们将会在以后不断使用。现在花时间学习这些内容,而不是等到以后再学,对你来说是有益的。这将使我们在建模深度学习架构时更加容易。
读完本章后,你现在已经能够处理和生成用于分类或特征表示的二元数据。你也知道如何处理分类数据和标签,并为分类或回归做准备。当你有实值数据时,你现在知道如何识别统计属性,以及如何规范化这些数据。如果你遇到具有非正态或非均匀分布的数据问题,现在你知道如何解决它。如果你遇到数据不足的问题,你学会了一些数据增强技术。在本章的结尾,你了解了一些最流行的降维技术。你将在以后的学习中学到更多相关内容,例如我们谈到的自动编码器,也可以用于降维。但请耐心等待,我们会在适当的时候深入讲解。
现在,我们将继续前进,开始下一个关于基础机器学习的介绍性主题。第四章,从数据中学习,介绍了深度学习理论中的最基本概念,包括回归和分类中的性能测量,以及过拟合的识别。然而,在我们深入之前,请先尝试用以下问题测试自己。
问题与答案
- 心脏数据集中哪些变量适合回归?
事实上,所有变量都是如此。但理想的变量是那些实值变量。
- 数据的缩放会改变数据的分布吗?
不会。分布保持不变。统计量,如均值和方差,可能会发生变化,但分布保持不变。
- 监督学习和无监督学习的降维方法之间的主要区别是什么?
监督算法使用目标标签,而无监督算法则不需要这些信息。
- 什么时候使用基于批次的降维方法更好?
当你拥有非常大的数据集时。
参考文献
-
克利夫兰心脏病数据集(1988)。首席研究人员:
a. 匈牙利心脏病研究所。布达佩斯:安德拉斯·贾诺西,医学博士。
b. 苏黎世大学医院,瑞士:威廉·斯坦布伦,医学博士。
c. 巴塞尔大学医院,瑞士:马蒂亚斯·菲斯特尔,医学博士。
d. V.A. 医疗中心,长滩和克利夫兰诊所基金会:罗伯特·德特兰诺,医学博士,博士。
-
Detrano, R., Janosi, A., Steinbrunn, W., Pfisterer, M., Schmid, J.J., Sandhu, S., Guppy, K.H., Lee, S. 和 Froelicher, V., (1989)。冠状动脉疾病诊断中新概率算法的国际应用。美国心脏病学杂志,64(5),304-310。
-
Deng, L. (2012). 用于机器学习研究的手写数字图像的 MNIST 数据库(最佳网络)。IEEE 信号处理杂志,29(6),141-142。
-
Sezgin, M., 和 Sankur, B. (2004). 图像阈值技术的综述与定量性能评估。电子成像杂志,13(1),146-166。
-
Potdar, K., Pardawala, T. S., 和 Pai, C. D. (2017). 神经网络分类器的分类变量编码技术比较研究。国际计算机应用杂志,175(4),7-9。
-
Hanin, B. (2018). 哪些神经网络架构导致梯度爆炸与消失?收录于 神经信息处理系统进展(第 582-591 页)。
-
Andrews, D. F., Gnanadesikan, R., 和 Warner, J. L. (1971). 多元数据的变换。生物统计学,825-840。
-
Van Dyk, D. A., 和 Meng, X. L. (2001). 数据增强的艺术。计算与图形统计学杂志,10(1),1-50。
-
Schölkopf, B., Smola, A., 和 Müller, K. R. (1997 年 10 月). 核主成分分析。收录于 国际人工神经网络会议(第 583-588 页)。Springer,柏林,海德堡。
-
Hinton, G. E. (2012). 限制玻尔兹曼机训练实用指南。收录于 神经网络:行业技巧(第 599-619 页)。Springer,柏林,海德堡。
-
Mairal, J., Bach, F., Ponce, J., 和 Sapiro, G. (2009 年 6 月). 稀疏编码的在线字典学习。收录于 第 26 届年度国际机器学习大会论文集(第 689-696 页)。ACM。
从数据中学习
数据准备对于复杂数据集来说花费大量时间,正如我们在上一章中所见。然而,花时间准备数据是值得的……这一点我可以保证!同样,花时间理解从数据中学习的基本理论对于任何想进入深度学习领域的人来说都非常重要。理解学习理论的基础,将在你阅读新算法或评估自己模型时带来回报。当你阅读本书后面的章节时,这也会让你的学习过程变得更加轻松。
更具体地说,本章介绍了深度学习理论中最基础的概念,包括回归和分类的性能衡量,以及过拟合的识别。它还提供了一些关于模型超参数的合理性以及优化需求的警告。
本章的结构如下:
-
有目的的学习
-
衡量成功与错误
-
识别过拟合和泛化
-
学习的艺术
-
深度学习算法训练的伦理影响
第六章:有目的的学习
在第三章《准备数据》中,我们讨论了如何为两种主要问题类型准备数据:回归和分类。在这一节中,我们将更详细地讨论分类和回归的技术差异。这些差异很重要,因为它们会限制你可以使用的机器学习算法类型,以解决你的问题。
分类
如何判断你的问题是否属于分类问题?答案取决于两个主要因素:你要解决的问题和你用于解决问题的数据。当然可能还有其他因素,但这两个因素无疑是最为重要的。
如果你的目标是建立一个模型,给定某些输入,模型将确定输出是否属于两个或更多的不同类别,那么你遇到的是一个分类问题。以下是分类问题的一些非详尽例子:
-
给定一张图片,标出其包含的数字(区分 10 个类别:0-9 数字)。
-
给定一张图片,确定其中是否包含猫(区分两个类别:是或否)。
-
给定一系列温度读数,确定其季节(区分四个类别:四季)。
-
给定一条推文的文本,确定其情感(区分两个类别:正面或负面)。
-
给定一张人的图片,确定其年龄段(区分五个类别:<18,18-25,26-35,35-50,>50)。
-
给定一张狗的图片,确定其品种(区分 120 个类别:国际公认的犬种)。
-
给定整个文档,确定它是否被篡改(区分类别:真实或被更改)。
-
根据光谱辐射计的卫星读数,确定地理位置是否与植被的光谱特征匹配(区分两个类别:是或否)。
如您在列表中的示例中所见,不同类型的问题具有不同类型的数据。在这些示例中看到的数据称为标记数据。
未标记数据非常常见,但在没有某种允许将数据样本匹配到类别的处理的情况下,很少用于分类问题。例如,可以对未标记数据使用无监督聚类,将数据分配给特定的聚类(例如组或类别),此时,数据在技术上成为“标记数据”。
列表中另一个需要注意的重要事项是,我们可以将分类问题分为两大类:
-
二元分类:仅用于任意两个类别之间的分类
-
多类分类:用于对超过两个类别进行分类
这种区别可能看起来是任意的,但实际上并非如此;事实上,分类的类型将限制您可以使用的学习算法类型和您可以期望的性能。为了更好地理解这一点,让我们分别讨论每种分类。
二元分类
这种类型的分类通常被认为比多类别问题简单得多。实际上,如果我们能解决二元分类问题,我们可以通过决定将问题分解为几个二元分类问题的策略,从技术上讲解决多类问题(Lorena, A. C. 等,2008)。
为什么这被认为是一个较简单的问题之一,是因为二元分类学习算法背后的算法和数学基础。假设我们有一个二元分类问题,比如在第三章,《准备数据》中解释的克利夫兰数据集。该数据集包含每个患者的 13 个医学观察结果 — 我们可以称之为 。对于每个患者记录,都有一个相关的标签,指示患者是否患有某种心脏疾病(+1)或没有(-1) — 我们将称之为
。因此,一个完整的数据集
,包含 *N *个样本,可以被定义为一组数据和标签:
如第一章所讨论,机器学习简介,学习的核心目的是使用一种算法,它能够找到将输入数据x映射到标签y的方式,并正确地处理所有样本在![]中的情况,并能够进一步(希望)对已知数据集之外的样本进行处理,。使用感知机和相应的感知机学习算法(PLA),我们的目标是找到能够满足以下条件的参数![]:
对于所有样本,i = 1, 2, ..., N。然而,正如我们在第一章中讨论的,机器学习简介,如果数据是非线性可分的,方程式将无法满足。在这种情况下,我们可以得到一个近似值或预测结果,这不一定是期望的结果;我们将称这样的预测为。
那么,学习算法的核心目标便是减少期望目标标签与预测结果
之间的差异。在理想的情况下,我们希望所有i = 1, 2, ..., N时都能实现
。对于某些i的情况,其中
,学习算法必须进行调整(即自我训练),通过寻找新的参数
,希望能够避免在未来犯下类似的错误。
此类算法背后的科学因模型而异,但最终目标通常是相同的:
-
减少每次学习迭代中的错误数量,
。
-
尽可能少的迭代(步骤)学习模型参数。
-
尽可能快地学习模型参数。
由于大多数数据集处理的是不可分问题,PLA 通常会被忽略,转而使用其他能够更快收敛且迭代次数较少的算法。许多学习算法通过采取特定的步骤来调整参数,以减少错误
,这些步骤是基于错误的变化性和参数选择的导数。因此,最成功的算法(至少在深度学习中)是那些基于某种梯度下降策略的算法(Hochreiter, S.等,2001)。
现在,让我们回顾一下最基本的迭代梯度策略。假设我们想要通过给定的数据集,,来学习参数
。为了让事情变得稍微简单一些,我们需要对问题的表述进行一些小调整。我们希望
能够在表达式
中隐含出来。唯一能够实现这一点的方式是,如果我们设置
和
。
通过这个简化,我们可以直接搜索w,这也意味着同时搜索b。使用固定的学习率进行梯度下降如下所示:
-
将权重初始化为零(
)并将迭代计数器初始化为零(
)。
-
当
时,执行以下操作:
-
计算相对于
的梯度,并将其存储在
中。
-
更新
,使其看起来像这样:
。
-
增加迭代计数器并重复。
这里有几个需要解释的地方:
-
梯度计算,
,并非易事。对于某些特定的机器学习模型,它可以通过解析方式确定;但在大多数情况下,必须通过使用一些最新的算法来数值求解。
-
我们仍然需要定义如何计算误差,
;但这将在本章的下一部分中讲解。
-
需要指定一个学习率,
,这本身就是一个问题。
解决这个最后问题的一种方式是:为了找到最小化误差的参数 ,我们需要参数
。现在,在应用梯度下降时,我们可以考虑找到
参数,但那样我们就会陷入一个无限循环。由于现如今梯度下降的算法通常会自动计算学习率或使用自适应方法来调整它(Ruder, S. 2016),我们不再详细讨论梯度下降及其学习率的问题。
多类分类
将数据分类为多个类别会对学习算法的性能产生重要影响。一般来说,模型的性能会随着需要识别的类别数量的增加而下降。例外情况是,如果你有大量的数据和强大的计算能力,因为这样你可以克服数据集中的类别不平衡问题,估计巨大的梯度,并进行大规模的计算和模型更新。计算能力未来可能不再是限制因素,但目前仍然是。
多类别问题可以通过使用如一对一或一对多等策略来解决。
在一对多(one versus all)中,你实际上有一个专家二分类器,擅长从所有其他模式中识别出一个模式,而其实现策略通常是级联的。下面是一个示例:
if classifierSummer says is Summer: you are done
else:
if classifierFall says is Fall: you are done
else:
if classifierWinter says is Winter: you are done
else:
it must be Spring and, thus, you are done
下面是该策略的图示。假设我们有二维数据,能够告诉我们一年四季的一些信息,如下所示:
图 4.1 - 随机化的二维数据,可能告诉我们一年四季的一些信息
在这种随机化的二维数据情况下,我们有四个类别,对应于四季。二分类直接使用是行不通的。然而,我们可以训练专家型二分类器,专门处理一个特定类别与所有其他类别的区别。如果我们训练一个二分类器来判断数据点是否属于夏季类别,使用一个简单的感知器,我们可以得到如下所示的分隔超平面:
图 4.2:一个专家型 PLA,它擅长将夏季数据与其他所有季节数据区分开来
类似地,我们可以训练其余的专家,直到我们拥有足够的专家来测试整个假设;也就是说,直到我们能够将所有类别相互区分开来。
另一种选择是使用能够处理多输出的分类器;例如,决策树或集成方法。但在深度学习和神经网络的情况下,这指的是可以在输出层有多个神经元的网络,例如在第一章中展示的图 1.6和图 1.9,机器学习导论。
多输出神经网络的数学公式与单输出神经网络仅有细微差异,输出不再是一个二值集合,如!,而是一个独热编码值的向量,如!。在这种情况下,|C|表示集合C的大小,其中包含所有不同的类别标签。对于前面的例子,C将包含以下内容:C = {'夏季', '秋季', '冬季', '春季'}。以下是每个独热编码的样子:
-
夏季:
-
秋季:
-
冬季:
-
春季:
目标向量中的每个元素将对应四个神经元的期望输出。我们还应指出,数据集定义现在应该反映样本输入数据和标签都是向量:
处理多类别分类问题的另一种方法是使用回归。
回归
之前我们指定了,对于二分类问题,目标变量可以取一组二值,如!。我们还提到,对于多分类问题,可以修改目标变量,使其成为一个向量,向量的大小取决于类别的数量,如!。那么,回归问题处理的情况是目标变量是任何实数值,如!。
这里的含义非常有趣,因为使用回归模型和算法,我们可以严格来说进行二分类,因为实数集包含任何二值数字集合:
。
此外,如果我们将C = {'夏季', '秋季', '冬季', '春季'}更改为数值表示形式,例如 C = {0,1,2,3},那么严格来说,我们又会使用回归,因为它具有相同的特性:
。
尽管回归模型可以解决分类问题,但建议使用专门针对分类的模型,并仅将回归模型用于回归任务。
即使回归模型可以用于分类(Tan, X., et.al. 2012),它们也非常适合目标变量是实数的情况。以下是一些回归问题的示例列表:
-
给定一张图片时,指出其中有多少人(输出可以是任何大于等于 0 的整数)。
-
给定一张图片时,指出其中含有猫的概率(输出可以是 0 到 1 之间的任何实数)。
-
给定一系列关于温度的读数,判断实际的体感温度(输出可以是任何整数,范围取决于单位)。
-
给定一条推文的文本,判断其是否具有攻击性(输出可以是 0 到 1 之间的任何实数)。
-
给定一个人的图片,判断他们的年龄(输出可以是任何正整数,通常小于 100)。
-
给定一整篇文档,判断可能的压缩率(输出可以是 0 到 1 之间的任何实数)。
-
给定卫星光谱辐射仪的读数,判断对应的红外值(输出可以是任何实数)。
-
给定一些主要报纸的头条,判断油价(输出可以是任何大于等于 0 的实数)。
从这个列表中可以看到,由于实数范围涵盖了所有整数以及所有正负数,可能性非常多,即使这个范围对于特定应用来说过于宽泛,回归模型也可以根据范围要求进行扩展或缩小。
为了说明回归模型的潜力,让我们从一个基本的线性回归模型开始,后续章节中我们将介绍基于深度学习的更复杂的回归模型。
线性回归模型尝试解决以下问题:
该问题的求解适用于i = 1, 2, ..., N。然而,我们也可以像之前一样使用相同的技巧,将b的计算包含在同一个方程中。所以,我们可以说我们正在尝试解决以下问题:
再次强调,我们正在尝试学习参数,,从而得到所有i情况下的
。在线性回归的情况下,预测值,
,应该理想情况下等于真实目标值,
,如果输入数据,
,某种程度上能描述一条完美的直线。但由于这非常不可能,所以必须有一种方法来学习参数,
,即使是这样,
。为了实现这一点,线性回归学习算法首先会对小错误给予低惩罚,对大错误给予较大惩罚。这是有道理的,对吧?这非常直观。
惩罚预测误差的自然方式是通过将预测值与目标值之间的差异平方。以下是差异较小时的一个例子:
这是一个当差异较大时的例子:
在这两个例子中,期望的目标值是1
。在第一种情况下,预测值0.98
非常接近目标,平方差是0.0004
,相对于第二种情况来说很小。第二个预测值偏离了14.8
,这导致平方差为219.4
。这对于构建学习算法来说是合理且直观的;即,惩罚错误的程度与其大小成正比。
我们可以正式定义总的平均误差,作为参数w的选择的函数,即所有平方误差的平均和,这也被称为均方误差(MSE):
。
如果我们将预测定义为当前选择的,即
,那么我们可以将误差函数重写如下:
。
这可以通过-范数(也称为欧几里得范数,
)来简化,首先定义一个数据矩阵
,其元素为数据向量
,以及一个相应的目标向量,如下所示:
。
误差的简化形式如下:
这可以展开成以下重要方程:
。
这很重要,因为它有助于计算误差的导数,,这是调整参数
所必需的,调整方向与误差的导数成比例。现在,按照线性代数的基本性质,我们可以说误差的导数(由于它会产生一个矩阵,因此被称为梯度)如下:
。
因为我们希望找到能够最小化误差的参数,我们可以将梯度设置为0
,然后解出。通过将梯度设置为
0
并忽略常数值,我们得出如下结果:
。
这些被称为正规方程(Krejn, S. G. E. 1982)。然后,如果我们简单地使用术语,我们就得到了伪逆的定义(Golub, G. 和 Kahan, W. 1965)。这一方法的美妙之处在于,我们不需要通过迭代计算梯度来选择最佳参数,
。事实上,由于梯度是解析的和直接的,我们可以一次性计算
,正如在这个线性回归算法中所解释的那样:
-
从
构建对,
。
-
估算伪逆
。
-
计算并返回
。
为了直观地展示这一点,假设我们有一个系统,它发送一个遵循线性函数的信号;然而,当信号被传输时,它会被常规噪声污染,噪声具有0
均值和单位方差,并且我们只能观察到被噪声污染的数据,如下所示:
图 4.3 - 被随机噪声污染的数据读取
比如说,一个黑客读取了这些数据,并运行线性回归尝试确定在数据被污染之前产生这些数据的真实函数,那么这个数据黑客将得到这里显示的解:
图 4.4 - 针对给定噪声数据读取问题的线性回归解法
显然,正如前面的图所示,线性回归解法非常接近真实的原始线性函数。在这个特定的例子中,可以观察到较高的接近度,因为数据被噪声污染,而这种噪声符合白噪声的模式;然而,对于不同类型的噪声,模型的表现可能不如这个例子中的效果。此外,大多数回归问题根本不是线性的;实际上,最有趣的回归问题是高度非线性的。尽管如此,基本的学习原理是相同的:
-
在每次学习迭代中(或者直接一次性得到结果,如线性回归中所示),减少错误的数量,
。
-
在尽可能少的迭代(步骤)中学习模型参数。
-
尽快学习模型参数。
引导学习过程的另一个重要组成部分是成功或错误的计算方式,这与选择的参数有关,。在 PLA 的情况下,它简单地找到了错误并进行了调整。对于多类问题,这是通过在某些误差度量上进行梯度下降的过程;而在线性回归中,这是通过直接使用均方误差(MSE)进行梯度计算。但现在,让我们深入探讨其他可以量化和定性分析的误差度量和成功标准。
衡量成功与错误
在深度学习模型中,使用的性能度量标准种类繁多,如准确率、平衡误差率、均方误差等。为了保持条理,我们将它们分为三类:二分类、多分类和回归。
二分类
在分析和衡量模型成功与否时,有一个重要的工具被广泛使用,它被称为混淆矩阵。混淆矩阵不仅在直观展示模型如何进行预测时非常有用,我们还可以从中提取其他有趣的信息。下图展示了一个混淆矩阵的模板:
图 4.5 - 混淆矩阵及其派生的性能度量标准
混淆矩阵及其派生的所有度量标准是传达模型好坏的重要方式。你应该将此页面加入书签,并在需要时随时返回查看。
在前面的混淆矩阵中,你会注意到它在垂直轴上有两列,表示真实的目标值,而在水平轴上,表示预测值。行与列的交点表示实际预测值与应预测值之间的关系。矩阵中的每个条目都有特殊的含义,并且可以推导出其他有意义的综合性能度量标准。
以下是度量标准及其含义:
缩写 | 描述 | 解释 |
---|---|---|
TP | 真阳性 | 这是指一个数据点属于正类,并且被正确预测为正类。 |
TN | 真阴性 | 这是指一个数据点属于负类,并且被正确预测为负类。 |
FP | 假阳性 | 这是指一个数据点属于负类,但被错误地预测为正类。 |
FN | 假阴性 | 这是指一个数据点属于正类,但被错误地预测为负类。 |
PPV | 正预测值 或 精度 | 这是指所有预测为正类的值中,正确预测为正类的比例。 |
NPV | 负预测值 | 这是所有预测为负的值中,正确预测为负的比例。 |
FDR | 假发现率 | 这是所有预测为正的值中,错误预测为正的比例。 |
FOR | 假阴性率 | 这是所有预测为负的值中,错误预测为负的比例。 |
TPR | 真正率,灵敏度,召回率,命中率 | 这是所有应为正类的值中,正确预测为正类的比例。 |
FPR | 假阳性率或假警报率 | 这是所有预测为正的值中,错误预测为负的比例。 |
TNR | 真阴性率,特异度,或选择性 | 这是所有应为负类的值中,正确预测为负类的比例。 |
FNR | 假阴性率或漏检率 | 这是所有应为正类的值中,错误预测为负类的比例。 |
这些中的一些可能会比较难以理解;不过,你现在不需要记住它们,随时可以回来查看这个表格。
还有一些其他指标,它们的计算稍微复杂一些,例如:
缩写 | 描述 | 解释 |
---|---|---|
ACC | 准确率 | 这是正确预测正类和负类的比例,基于所有样本。 |
F[1] | F[1]-得分 | 这是精确度和灵敏度的平均值。 |
MCC | 马修斯相关系数 | 这是期望类别和预测类别之间的相关性。 |
BER | 平衡错误率 | 这是在类别不平衡情况下的平均错误率。 |
在这份包含复杂计算的列表中,我加入了如ACC和BER这样的缩写,它们有非常直观的含义。然而,主要问题是当我们处理多类时,它们会有所变化。因此,在多类情况下,它们的计算会稍有不同。其余的指标依然是针对二分类的(如定义)。
在讨论多类指标之前,以下是计算之前这些指标的公式:
在一般意义上,你希望ACC、F[1]和MCC尽量高,而BER尽量低。
多类
当我们超越简单的二分类时,常常涉及多类问题,例如C = {'夏季','秋季','冬季','春季'}或C = {0,1,2,3}。这会在一定程度上限制我们衡量错误或成功的方式。
考虑多类的混淆矩阵,如下所示:
图 4.6 - 多类别的混淆矩阵
从以下图表可以明显看出,由于我们不再只有正负类别,而是有一组有限的类别,真正的正类或负类的概念已经消失:
各个类别,,可以是字符串或数字,只要它们符合集合的规则。也就是说,类别的集合,
,必须是有限且唯一的。
要在此测量 ACC,我们将统计混淆矩阵主对角线上的所有元素,并将其除以样本的总数:
在这个方程中,表示混淆矩阵,
表示迹运算;即,方阵主对角线元素的和。因此,总误差为
1-ACC
,但在类别不平衡的情况下,误差指标或简单的准确率可能会产生误导。为此,我们必须使用 BER 指标,对于多个类别,定义如下:
在这个新的 BER 公式中,表示混淆矩阵中第 j行和第 i列的元素,
。
一些机器学习学派使用混淆矩阵的行来表示真实标签,列来表示预测标签。分析背后的理论和解释都是相同的。不要对sklearn
使用翻转的方法感到惊慌;这与此无关,你应该不会遇到任何理解上的问题。
作为示例,考虑之前在图 4.1中展示的数据集。如果我们运行一个五层的神经网络分类器,可能会得到像这样的决策边界:
图 4.7 - 使用五层神经网络对一个二维数据集进行分类的区域
显然,该数据集不能通过一个非线性超平面完美分开;每个类别的边界都有一些数据点交叉。在前面的图表中,我们可以看到,只有夏季类别没有数据点被错误分类。
然而,如果我们实际计算并展示混淆矩阵,以下结果会更为明显:
图 4.8 - 从训练误差中获得的混淆矩阵,基于示例二维数据集
在这种情况下,准确率可以计算为 ACC=(25+23+22+24)/100,得到的 ACC 为 0.94,看起来不错,错误率为 1-ACC = 0.06。这个例子有轻微的类别不平衡。这里是每个类别的样本数:
-
夏季:25
-
秋季:25
-
冬季:24
-
春季:26
冬季组的样本数比其他组少,春季组的样本数比其他组多。虽然这是一个非常小的类别不平衡,但它可能足以导致一个误导性较低的错误率。现在我们必须计算平衡错误率 BER。
BER 可以按如下方式计算:
这里,错误率与 BER 之间的差异是 0.01% 的低估。然而,对于类别严重不平衡的情况,差距可能会大得多,因此我们有责任仔细测量并报告适当的错误度量,BER。
关于 BER 另一个有趣的事实是,它直观上是平衡准确率的对应物;这意味着如果我们去掉 BER 方程中的 1–
项,就剩下平衡准确率。此外,如果我们检查分子中的项,就可以看到其上的分数导致了每个类别的准确率;例如,第一个类别,夏季,准确率为 100%,第二个,秋季,准确率为 92%,依此类推。
在 Python 中,sklearn
库有一个类,可以根据真实标签和预测标签自动确定混淆矩阵。这个类叫做 confusion_matrix
,它属于 metrics
超类,我们可以通过以下方式使用它:
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y, y_pred)
print(cm)
如果 y
包含真实标签,y_pred
包含预测标签,那么前述操作将输出类似这样的结果:
[[25 0 0 0]
[ 0 23 1 1]
[ 1 0 22 1]
[ 0 1 1 24]]
我们可以通过简单地做以下操作来计算 BER:
BER = []
for i in range(len(cm)):
BER.append(cm[i,i]/sum(cm[i,:]))
print('BER:', 1 - sum(BER)/len(BER))
这将输出以下内容:
BER: 0.06006410256410266
或者,sklearn
有一个内置函数,可以在与混淆矩阵相同的超级类中计算平衡准确率得分。这个类叫做 balanced_accuracy_score
,我们可以通过以下方式计算 BER:
from sklearn.metrics import balanced_accuracy_score
print('BER', 1- balanced_accuracy_score(y, y_pred))
我们得到以下输出:
BER: 0.06006410256410266
现在让我们讨论回归的指标。
回归
最流行的指标是MSE,我们在本章前面解释线性回归如何工作时已讨论过该指标。然而,我们将它作为超参数选择的函数进行了解释。在这里,我们将其重新定义为一般意义上的如下:
另一个与 MSE 非常相似的指标是均值绝对误差(MAE)。虽然 MSE 对大错误的惩罚更严重(平方惩罚),而对小错误的惩罚较轻,MAE 则直接按实际值和预测值之间的绝对差异惩罚所有误差。这是 MAE 的正式定义:
最后,在其他回归评估指标中,深度学习中常用的指标是R² 得分,也叫做决定系数。这个指标表示模型中独立变量所解释的方差比例。它衡量模型在面对与训练数据具有相同统计分布的未见数据时,表现良好的可能性。这是它的定义:
样本均值定义如下:
Scikit-learn 提供了每个这些指标的类,见下表:
回归指标 | Scikit-learn 类 |
---|---|
R² 得分 | sklearn.metrics.r2_score |
MAE | sklearn.metrics.mean_absolute_error |
MSE | sklearn.metrics.mean_squared_error |
所有这些类都将真实标签和预测标签作为输入参数。
举个例子,如果我们将图 4.3 和 图 4.4 中展示的线性回归模型及其数据作为输入,我们可以确定三个误差指标,如下所示:
from sklearn.metrics import mean_absolute_error
from sklearn.metrics import mean_squared_error
from sklearn.metrics import r2_score
r2 = r2_score(y,y_pred)
mae = mean_absolute_error(y,y_pred)
mse = mean_squared_error(y,y_pred)
print('R_2 score:', r2)
print('MAE:', mae)
print('MSE:', mse)
上述代码的输出如下:
R_2 score: 0.9350586211501963
MAE: 0.1259473720654865
MSE: 0.022262066145814736
下图展示了使用的样本数据及其获得的性能。显然,使用这三种性能指标的表现很好:
图 4.9 - 在含有白噪声的线性回归模型上的误差指标
一般来说,你总是希望确定系数尽可能接近 1
,并且所有的误差(MSE 和 MAE)尽可能接近 0
。尽管这些都是很好的模型评估指标,但我们需要注意的是,要在未见的验证或测试数据上报告这些指标。这样做可以准确衡量模型的泛化能力,并在模型发生灾难性错误之前识别出过拟合。
识别过拟合和泛化
通常,在受控的机器学习环境中,我们会得到一个可以用于训练的数据集和一个可以用于测试的不同数据集。其思路是:你只在训练数据上运行学习算法,但当你想了解模型的表现时,你将测试数据输入到模型中并观察输出。在竞赛和黑客马拉松中,通常会提供测试数据,但不会提供与之相关的标签,因为获胜者将根据模型在测试数据上的表现来选择,而不希望他们通过查看测试数据的标签并做出调整来作弊。如果是这种情况,我们可以使用验证数据集,我们可以通过从训练数据中分离一部分数据来创建验证数据集。
拥有单独数据集的全部意义,即验证集或测试集,是为了评估在这些数据上的表现,因为我们知道我们的模型并没有用它来训练。一个模型能够在未见过的验证集或测试集上表现得同样好,或者接近同样好的能力,称为泛化。
泛化是大多数学习算法的最终目标;我们所有深度学习的专业人员和从业者都梦想在我们的所有模型中实现出色的泛化。同样,我们最大的噩梦是过拟合。
过拟合是泛化的对立面。当我们的模型在训练数据上表现非常好,但在面对验证集或测试集时,表现显著下降时,就发生了过拟合。这表明我们的模型几乎记住了训练数据的细节,而忽略了样本空间中的一般性规律,这些规律有助于产生良好的模型。
在本章及后续章节中,我们将遵循以下数据拆分规则:
-
如果我们提供了带标签的测试数据,我们将基于训练集进行训练,并根据测试集报告性能。
-
如果我们没有提供测试数据(或如果我们有无标签的测试数据),我们将拆分训练集,创建一个验证集,通过交叉验证策略报告性能。
让我们单独讨论每种情况。
如果我们有测试数据
为了开始讨论,假设我们有一个深度学习模型,并且有一组超参数,,这些超参数可能是模型的权重、神经元数量、层数、学习率、丢弃率等等。然后,我们可以说,一个用训练数据训练的模型,
,(具有超参数
)可以得到如下的训练准确率:
这是一个训练模型在训练数据上的训练准确率。因此,如果我们有带标签的测试数据,,有M个数据点,我们可以通过计算以下内容来简单地估算测试准确率:
在报告测试准确率时,有一个重要的特性在大多数情况下通常是成立的——所有的测试准确率通常小于训练准确率加上一些由于参数选择不当造成的噪声:
这通常意味着,如果你的测试准确率明显高于训练准确率,那么可能存在训练模型的问题。此外,我们还可以考虑测试数据与训练数据在统计分布和描述它的多维流形上有显著差异的可能性。
总结来说,如果我们有正确选择的测试数据,那么在测试集上报告性能是非常重要的。然而,性能低于训练时的表现是完全正常的。不过,如果性能显著较低,可能存在过拟合问题;如果性能显著较高,则可能是代码、模型或测试数据选择有问题。过拟合问题可以通过选择更好的参数 ,或选择不同的模型
来解决,这在下一节中有讨论。
现在,让我们简要讨论一种情况,即我们没有测试数据,或者我们有没有标签的测试数据。
没有测试数据?没问题——交叉验证
交叉验证是一种技术,它允许我们将训练数据 分割成更小的组用于训练。需要记住的最重要一点是,分割应该是理想情况下具有相等数量的样本,并且我们要轮流选择用于训练和验证集的组。
让我们讨论著名的交叉验证策略——k-折交叉验证(Kohavi, R. 1995)。这里的想法是将训练数据划分为 k 组,这些组(理想情况下)大小相等,然后选择 k-1 组用于训练模型,并衡量被排除的组的性能。接着,每次更换组,直到所有组都被选中用于测试。
在前面的章节中,我们讨论了使用标准准确度(ACC)来衡量性能,但我们也可以使用任何性能指标。为了展示这一点,我们现在将计算均方误差(MSE)。这就是 k-折交叉验证算法的样子:
-
输入数据集
,模型
,参数
,以及折数
。
-
将索引集
划分为
组(理想情况下大小相等),
,使得
。
-
对于每个
情况,执行以下操作:
-
选择训练集的索引,如 ![],并形成训练集,![]。
-
选择验证集的索引,如
,并形成验证集,
。
-
使用在训练集上选择的参数训练模型:
。
-
计算模型在验证集上的误差,![],![]
- 返回
对于所有
的情况。
通过这个,我们可以计算交叉验证误差(MSE),公式如下:
我们还可以计算相应的标准差:
。
通常,观察我们性能度量的标准差是一个好主意——无论我们选择哪种方法——因为它能反映我们在验证集上的表现一致性。理想情况下,我们希望交叉验证的 MSE 为0
,,标准差为
1
,。
为了解释这一点,我们可以使用受白噪声污染的样本数据回归示例,见图 4.3和图 4.4。为了简化这个例子,我们将使用总共 100 个样本,N=100,并且使用 3 折交叉验证。我们将使用 scikit-learn 的KFold
类,在model_selection
父类中,并获得交叉验证后的 MSE 及其标准差。
为此,我们可以使用以下代码,并加入其他度量标准:
import numpy as np
from sklearn.metrics import mean_absolute_error
from sklearn.metrics import mean_squared_error
from sklearn.metrics import r2_score
from sklearn.model_selection import KFold
# These will be used to save the performance at each split
cv_r2 = []
cv_mae = []
cv_mse = []
# Change this for more splits
kf = KFold(n_splits=3)
k = 0
# Assuming we have pre-loaded training data X and targets y
for S_D, S_V in kf.split(X):
X_train, X_test = X[S_D], X[S_V]
y_train, y_test = y[S_D], y[S_V]
# Train your model here with X_train and y_train and...
# ... test your model on X_test saving the output on y_pred
r2 = r2_score(y_test,y_pred)
mae = mean_absolute_error(y_test,y_pred)
mse = mean_squared_error(y_test,y_pred)
cv_r2.append(r2)
cv_mae.append(mae)
cv_mse.append(mse)
print("R_2: {0:.6} Std: {1:0.5}".format(np.mean(cv_r2),np.std(cv_r2)))
print("MAE: {0:.6} Std: {1:0.5}".format(np.mean(cv_mae),np.std(cv_mae)))
print("MSE: {0:.6} Std: {1:0.5}".format(np.mean(cv_mse),np.std(cv_mse)))
这段代码的结果会返回如下内容:
R_2: 0.935006 Std: 0.054835
MAE: 0.106212 Std: 0.042851
MSE: 0.0184534 Std: 0.014333
这些结果是交叉验证后的,能更清晰地展示模型的泛化能力。为了比较目的,参考图 4.9中展示的结果。你会注意到,现在仅使用约 66%的数据(因为我们将数据分成了三组)进行训练,约 33%的数据用于测试,得到的结果与之前在图 4.9中测得的性能结果非常一致,如下所示:
图 4.10 - 交叉验证性能度量,括号中为标准差
前面的图表展示了每次数据拆分所找到的线性回归解以及真实的原始函数;你可以看到,找到的解与真实模型非常接近,从而得到了良好的性能,这通过R²、MAE和MSE来衡量。
练习
请继续改变折数,逐步增加,并记录你的观察结果。交叉验证的性能有什么变化?它们是保持不变、增加还是减少?交叉验证性能的标准差发生了什么变化?它们是保持不变、增加还是减少?你认为这意味着什么?
通常,交叉验证用于数据集 ,并用模型
在参数
上进行训练。然而,学习算法中最大的一项挑战就是找到能够产生最佳(测试或交叉验证)性能的最佳参数集
。许多机器学习科学家认为,选择参数集的过程可以通过一些算法自动化,而另一些人则认为这是一种艺术(Bergstra, J. S., 等,2011 年)。
学习背后的艺术
对于我们这些花费数十年研究机器学习的人来说,经验影响着我们为学习算法选择参数的方式。但对于那些初学者来说,这是需要培养的技能,而这种技能是在理解学习算法如何工作的基础上发展起来的。一旦你读完这本书,我相信你会掌握足够的知识,能够明智地选择参数。与此同时,我们可以在这里讨论一些使用标准和新颖算法自动寻找参数的思路。
在继续之前,我们需要在此时做出区分,并定义在学习算法中非常重要的两大类参数。具体如下:
-
模型参数: 这些是表示模型所代表的解决方案的参数。例如,在感知机和线性回归中,这可能是向量
和标量
,而对于深度神经网络来说,这可能是权重矩阵
和偏置向量
。对于卷积网络,这可能是滤波器集。
-
超参数: 这些是模型所需的参数,用来引导学习过程以寻找解决方案(模型参数),通常表示为
。例如,在感知机中,超参数可能是最大迭代次数;在深度神经网络中,超参数可能包括层数、神经元的数量、神经元的激活函数以及学习率;对于卷积神经网络(CNN),超参数可能包括滤波器的数量、滤波器的大小、步幅、池化大小等。
换句话说,模型参数在一定程度上是由超参数的选择决定的。通常,除非出现数值异常,否则所有学习算法都会为相同的超参数集一致地找到解决方案(模型参数)。因此,学习过程中的主要任务之一就是找到最佳的超参数集,以便为我们提供最好的解决方案。
为了观察改变模型超参数的影响,我们再次考虑之前展示的四类季节分类问题,参见图 4.7。我们假设使用的是全连接网络,如《第一章》《机器学习导论》中所述,我们希望确定的超参数是最适合的层数。为了教学目的,假设每层的神经元数量会按指数增加,如下所示:
层数 | 每层的神经元数量 |
---|---|
1 | (8) |
2 | (16, 8) |
3 | (32, 16, 8) |
4 | (64, 32, 16, 8) |
5 | (128, 64, 32, 16, 8) |
6 | (256, 128, 64, 32, 16, 8) |
7 | (512, 256, 128, 64, 32, 16, 8) |
在之前的配置中,括号中的第一个数字对应最接近输入层的神经元数量,而括号中的最后一个数字对应最接近输出层的神经元数量(输出层由 4 个神经元组成,每个类别一个)。
所以,层数代表 ,在这个例子中。如果我们遍历每个配置并确定交叉验证后的 BER,我们可以确定哪个架构表现最佳;也就是说,我们正在为性能优化
。得到的结果如下所示:
层数 – ![] | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
BER | 0.275 | 0.104 | 0.100 | 0.096 | 0.067 | 0.079 | 0.088 |
标准差 | 0.22 | 0.10 | 0.08 | 0.10 | 0.05 | 0.04 | 0.08 |
从结果中,我们可以很容易地确定,最佳架构是五层的,因为它具有最低的 BER 和第二小的标准差。事实上,我们可以收集每个配置在每个分割点的数据,并生成这里展示的箱线图:
图 4.11 - 优化层数的交叉验证数据箱线图
这个箱线图展示了几个重要的点。首先,模型在增加层数到5
时,BER 明显下降,之后又开始上升。这在机器学习中非常常见,被称为过拟合曲线,通常呈u形(或者对于高值表现较好的性能指标来说是n形)。在这种情况下,最低点表示最优的超参数(在5
时);该点左侧表示欠拟合,右侧则表示过拟合。第二个要点是,即使多个模型具有相似的 BER,我们也会选择那个波动性较小且最稳定的模型。
为了说明欠拟合、良拟合和过拟合之间的差异,我们将展示由最差的欠拟合、最好的拟合和最差的过拟合产生的决策边界。在这种情况下,最差的欠拟合是一个隐层,最好的拟合是五个隐层,最差的过拟合是七个隐层。它们各自的决策边界分别展示在 图 4.12、图 4.13 和 图 4.14 中:
图 4.12 - 一个单隐层网络的分类边界,出现了欠拟合
在前面的图中,我们可以看到欠拟合的情况非常明显,因为存在决策边界,这些边界阻止了许多数据点被正确分类:
图 4.13 - 一个五隐层网络的分类边界,具有相对较好的拟合
类似地,前面的图展示了决策边界,但与 图 4.12 相比,这些边界似乎为不同组的数据显示了更好的分离——这是一个良好的拟合:
图 4.14 - 一个七隐层网络的分类边界,出现了过拟合
如果你仔细观察,图 4.12 显示某些区域被划分得非常差,而在 图 4.14 中,网络架构试图 过于努力 地完美分类所有样本,以至于 Fall 类别(黄色点)的离群值进入了 Winter 类别(蓝色点)的区域,并且它有了自己的小区域,这可能会在后续产生负面影响。图 4.13 中的类别似乎对一些离群值具有鲁棒性,并且大多数情况下具有明确的区域。
随着我们继续深入本书,我们将处理更多复杂的超参数集。这里我们只处理了一个,但理论是相同的。通过这种方式寻找最佳超参数集的方法被称为穷举搜索。然而,也有其他查看参数的方法,比如执行网格搜索。
假设你没有固定的方法来确定每个层的神经元数量(与之前的例子不同);你只知道你希望神经元数量在 4
到 1024
之间,层数在 1
到 100
之间,以便允许深层或浅层模型。在这种情况下,你无法进行穷举搜索,因为那样需要太多时间!此时,网格搜索作为一种解决方案,用于在通常是等间距的区域内对搜索空间进行采样。
例如,网格搜索可以查看在 [4, 1024]
范围内的多个神经元数量,使用 10 个等间距的值——4
、117
、230
、344
、457
、570
、684
、797
、910
和 1024
——以及在 [1, 100]
范围内的层数,使用 10 个等间距的值——1
、12
、23
、34
、45
、56
、67
、78
、89
和 100
。与查看 1020100=102,000 次搜索不同,网格搜索只需查看 1010=100 次搜索。
在sklearn
中,有一个类,GridSearchCV
,它可以在交叉验证中返回最佳模型和超参数;它是model_selection
超类的一部分。同一类中还有另一个类,叫做RandomizedSearchCV
,其方法基于在搜索空间中进行随机搜索。这就叫做随机搜索。
在随机搜索中,前提是它将在[4, 1024]
范围内以及[1,100]
范围内随机抽取数字,分别用于神经元和层数,直到达到最大迭代次数的限制。
通常,如果你知道参数搜索空间的范围和分布,尝试在你认为可能具有更好交叉验证性能的空间上进行网格搜索。然而,如果你对参数搜索空间知之甚少或完全不了解,可以使用随机搜索方法。实际上,这两种方法都很有效。
还有其他一些更复杂的方法,它们效果很好,但在 Python 中的实现尚未标准化,因此我们在此不会详细介绍。不过,你应该了解这些方法:
-
贝叶斯超参数优化(Feurer, M., 等,2015 年)
-
基于进化理论的超参数优化(Loshchilov, I., 等,2016 年)
-
基于梯度的超参数优化(Maclaurin, D., 等,2015 年)
-
基于最小二乘法的超参数优化(Rivas-Perea, P., 等,2014 年)
深度学习算法训练的伦理影响
关于训练深度学习模型的伦理影响,有一些要点可以提及。在处理代表人类感知的数据时,总是存在潜在的危害。但同样,关于人类和人类互动的数据必须严格保护,并且在创建基于此类数据的模型之前需要仔细审查。这些思考在接下来的部分中有详细展开。
使用适当的性能度量来报告
避免通过选择唯一的性能指标来伪造良好的性能,这种做法让你的模型看起来很优秀。阅读文章和报告时,不乏有多分类模型在明显类别不平衡的数据集上训练,但仅报告标准的准确度。这些模型很可能会报告较高的准确率,因为模型会偏向过采样类别,而对欠采样类别产生偏见。因此,这些模型必须报告平衡准确度或平衡错误率。
类似地,对于其他类型的分类和回归问题,你必须报告适当的性能指标。如果不确定,报告尽可能多的性能指标是明智的。没有人会抱怨有人使用过多的指标来报告模型性能。
不报告适当的度量标准可能带来的后果有很多,从拥有未被发现的偏差模型,并将其部署到生产系统中引发灾难性后果,到提供误导性信息,可能会对我们理解特定问题以及模型性能产生不利影响。我们必须记住,我们所做的可能会影响他人,因此需要保持警惕。
小心处理异常值并验证它们
异常值通常被视为在学习过程中需要避免的坏东西,我同意这个观点。除非它们不是真正的异常值,否则模型应该对异常值具有鲁棒性。如果我们拥有一些数据,但对它一无所知,合理的假设是将异常值解读为异常现象。
然而,如果我们对数据有所了解(因为我们收集了数据、获得了所有相关信息,或者知道产生数据的传感器),我们就可以验证异常值是否真的是异常值。我们必须验证它们是否是由于人工输入错误、传感器故障、数据转换错误或其他人为因素造成的。如果异常值不是这些原因的产物,那么我们没有合理的依据认为它是异常值。事实上,这样的数据给我们提供了关于可能不常发生,但最终会再次发生的情况的重要信息,模型需要做出正确的响应。
请考虑下图所示的数据。如果我们在没有验证的情况下随意决定忽略异常值(如顶部的图示所示),那么这些异常值实际上可能并不是真正的异常值,模型将会创建一个狭窄的决策空间,忽略这些异常值。在这个例子中,结果是一个点将被错误地分类为属于另一个群体,而另一个点可能会被排除在主要群体之外:
图 4.15 - 我的模型在学习空间中的差异。顶部的图示展示了忽略异常值的结果。底部的图示展示了包括异常值的结果
然而,如果我们验证数据并发现异常值是完全有效的输入,那么模型可能会学习到一个更好的决策空间,这个空间可能包括异常值。然而,这可能会导致一个次要问题,即一个点被分类为属于两个不同的群体,并且具有不同程度的隶属关系。虽然这是一个问题,但它的风险远小于错误地将某些东西分类。比起以 100%的确定性错误分类,最好是有 60%的确定性认为一个点属于一个类别,40%的确定性认为它属于另一个类别。
如果你考虑一下,忽略离群值构建的模型,若被部署到政府系统中,可能会导致歧视问题。它们可能对少数群体或受保护群体存在偏见。如果部署到学校学生选拔系统中,可能会导致优秀学生的被拒绝。如果部署到 DNA 分类系统中,可能会错误地忽略两个非常相近的 DNA 群体的相似性。因此,始终验证离群值,如果可能的话。
对于下采样的群体,使用加权类别。
如果你遇到类不平衡的问题,如图 4.15所示,我建议你通过获取更多数据来平衡类,而不是减少数据。如果这不可行,可以考虑使用允许你为某些类别赋予不同权重的算法,从而平衡类之间的不均衡。以下是几种最常见的技术:
-
在小型数据集上,使用
sklearn
和class_weight
选项。在训练模型时,它会根据提供的类权重惩罚错误。你还可以尝试一些自动化的替代方法,它们也会有所帮助,例如class_weight="auto"
和class_weight="balanced"
。 -
在使用批处理训练的大型数据集上,使用 Keras 和
BalancedBatchGenerator
类。每次准备的样本(批次)都会保持一致的平衡,从而引导学习算法平等地考虑所有群体。该类是imblearn.keras
的一部分。
每次当你想要构建一个不偏向多数群体的模型时,你应该尝试使用这些策略。这些策略的伦理意义与前述已提到的点相似。但最重要的是,我们必须保护生命,并尊重他人;每个人都有平等且无限的价值。
总结
在这一基础章节中,我们讨论了学习算法的基础知识及其目的。接着,我们研究了通过准确率、误差和其他统计工具来衡量成功与失败的最基本方法。我们还探讨了过拟合问题以及其对应概念——泛化,这一概念非常重要。随后,我们讨论了超参数选择的艺术及其自动化搜索策略。
阅读完本章后,你现在可以解释分类和回归的技术差异,并能够计算不同的性能指标,如 ACC、BER、MSE 等,以适应不同的任务。现在,你可以通过交叉验证策略使用训练集、验证集和测试集来检测过拟合,能够尝试和观察调整学习模型超参数的效果。你也准备好批判性地思考预防深度学习算法对人类造成伤害所需的预防措施和设备。
下一章是 第五章,训练单个神经元,它修订并扩展了神经元的概念,神经元概念最初在 第一章,机器学习简介 中介绍,并展示了其在 Python 中的实现,使用不同的数据集分析不同数据可能产生的潜在影响;即,线性和非线性可分数据。然而,在进入这一部分之前,请尝试使用以下问题进行自我测验。
问题与答案
- 当你做交叉验证练习时,标准差发生了什么变化,这意味着什么?
随着折数增加,标准差趋于稳定并减小。这意味着性能度量更加可靠;它是对泛化能力或过拟合的准确度量。
- 超参数和模型参数有什么区别?
模型参数是学习算法的数值解;超参数是模型需要了解的内容,以便有效地找到解决方案。
- 网格搜索比随机搜索在超参数调优上更快吗?
这取决于。如果超参数的选择影响学习算法的计算复杂性,那么两者可能会表现得不同。然而,在类似的搜索空间和摊销的情况下,两者应该大致在相同时间完成。
- 我可以将基于回归的学习算法用于分类问题吗?
可以,只要标签、类别或组映射到实数集合中的一个数字。
- 我可以将基于分类的学习算法用于回归问题吗?
不是。
- 损失函数的概念是否与误差度量相同?
是的,但也不完全是。是的,从损失函数将衡量性能的角度来看;然而,性能不一定是关于分类或回归数据的准确性;它可能与其他方面有关,比如在信息论空间中的组或距离的质量。例如,线性回归基于最小化的 MSE 算法作为损失函数,而 K-means 算法的损失函数是数据到其均值的平方距离之和,它的目标是最小化这个值,但这并不一定意味着它是一个误差。在后一种情况下,它可以说是作为一种聚类质量度量。
参考文献
-
Lorena, A. C., De Carvalho, A. C., & Gama, J. M. (2008),在多类问题中组合二分类器的综述,人工智能评论,30(1-4),19
-
Hochreiter, S., Younger, A. S., & Conwell, P. R. (2001 年 8 月),使用梯度下降进行学习的研究,发表于 国际人工神经网络会议(第 87-94 页),Springer:柏林,海德堡
-
Ruder, S. (2016), 梯度下降优化算法概述,arXiv 预印本 arXiv:1609.04747
-
Tan, X., Zhang, Y., Tang, S., Shao, J., Wu, F., & Zhuang, Y. (2012 年 10 月), 用于分类的逻辑张量回归,在智能科学与智能数据工程国际会议(第 573-581 页),Springer: 柏林,海德堡
-
Krejn, S. G. E. (1982), 巴拿赫空间中的线性方程,Birkhäuser: 波士顿
-
Golub, G., & Kahan, W. (1965), 计算矩阵的奇异值和伪逆,工业与应用数学学会期刊,B 卷:数值分析,2(2),(第 205-224 页)
-
Kohavi, R. (1995 年 8 月), 交叉验证与自助法在准确性估计和模型选择中的研究,IJCAI,14(2),(第 1137-1145 页)
-
Bergstra, J. S., Bardenet, R., Bengio, Y., & Kégl, B. (2011), 超参数优化算法,在神经信息处理系统进展中,(第 2546-2554 页)
-
Feurer, M., Springenberg, J. T., & Hutter, F. (2015 年 2 月), 通过元学习初始化贝叶斯超参数优化,在第二十九届人工智能美国协会会议中
-
Loshchilov, I., & Hutter, F. (2016), CMA-ES 用于深度神经网络的超参数优化,arXiv 预印本 arXiv:1604.07269
-
Maclaurin, D., Duvenaud, D., & Adams, R. (2015 年 6 月), 通过可逆学习进行基于梯度的超参数优化,在国际机器学习会议(第 2113-2122 页)
-
Rivas-Perea, P., Cota-Ruiz, J., & Rosiles, J. G. (2014), 一种用于 LP-SVR 超参数选择的非线性最小二乘准牛顿策略,机器学习与控制论国际期刊,5(4),(第 579-597 页)
训练一个单神经元
在回顾了数据学习的相关概念后,我们现在将重点关注一个训练最基本神经网络模型的算法:感知机。我们将探讨使该算法运作所需的步骤及停止条件。本章将呈现感知机模型,作为第一个代表神经元的模型,旨在以简单的方式从数据中学习。感知机模型对于理解从数据中学习的基础和高级神经网络模型至关重要。本章还将讨论与非线性可分数据相关的问题和考虑因素。
在本章结束时,你应该能够自如地讨论感知机模型,并应用其学习算法。你将能够在数据是线性可分或非线性可分的情况下实现该算法。
具体来说,本章涵盖以下主题:
-
感知机模型
-
感知机学习算法
-
感知机与非线性可分数据
第七章:感知机模型
回到第一章,机器学习简介,我们简要介绍了神经元的基本模型和感知机学习算法(PLA)。在本章中,我们将重新审视并扩展这一概念,并展示如何用 Python 编码实现它。我们将从基本定义开始。
视觉概念
感知机是一个类比于人类信息处理单元的模型,最初由 F. Rosenblatt 提出,并在图 5.1中描述(Rosenblatt, F. (1958))。在该模型中,输入由向量表示 ,神经元的激活由函数
给出,输出为
。神经元的参数为
和
:
图 5.1 – 感知机的基本模型
感知机的可训练参数为 ,这些参数是未知的。因此,我们可以使用输入训练数据
来使用 PLA 确定这些参数。从图 5.1可以看出,
乘以
,然后
乘以
,并且
乘以 1;所有这些乘积相加后,再输入符号激活函数,在感知机中,其操作如下:
激活函数的主要目的是将模型的任何响应映射到二进制输出:![]。
现在让我们一般性地讨论一下张量。
张量操作
在 Python 中,感知机的实现需要一些简单的张量(向量)操作,可以通过标准的 NumPy 功能来完成。首先,我们可以假设给定的数据![]是一个包含多个向量(矩阵)的向量,表示为![],以及表示多个单独目标的向量![]。然而,注意到为了便于感知机的实现,需要将
包含在
中,正如图 5.1中所建议的那样,这样可以简化![]中的乘法和加法,如果我们将
修改为
,并将
修改为![],则可以简化输入![]的感知机响应,具体如下:
请注意,现在在
中是隐式存在的。
假设我们希望获得用于训练的X
数据集,我们需要为感知机准备这些数据;我们可以通过 scikit-learn 的make_classification
方法生成一个简单的线性可分数据集,如下所示:
from sklearn.datasets import make_classification
X, y = make_classification(n_samples=100, n_features=2, n_classes=2,
n_informative=2, n_redundant=0, n_repeated=0,
n_clusters_per_class=1, class_sep=1.5,
random_state=5)
在这里,我们使用make_classification
构造函数生成 100 个数据点(n_samples
),这 100 个数据点有两个类别(n_classes
),并且通过足够的分隔度(class_sep
)使得数据具有线性可分性。然而,生成的数据集在y
中产生了![]中的二进制值,我们需要将其转换为![]中的值。这可以通过以下简单操作实现:用负目标值替换零目标值:
y[y==0] = -1
生成的数据集如图 5.2所示:
图 5.2 - 用于感知机测试的二维样本数据
接下来,我们可以通过将一个长度为N=100
的全 1 向量添加到X
中,为每个输入向量添加数字 1,具体操作如下:
import numpy as np
X = np.append(np.ones((N,1)), X, 1)
现在,X
中的新数据包含一个全为 1 的向量。这将简化张量操作的计算![],适用于所有。考虑矩阵
,这个常见的张量操作可以通过一步完成,简单地将其视为
。我们甚至可以将这个操作与符号激活函数合并成一步,具体如下:
np.sign(w.T.dot(X[n]))
这是数学张量操作![]的等效形式。有了这个概念,让我们使用前面介绍的 dataset 和刚才描述的操作,更详细地回顾 PLA。
感知器学习算法
感知器学习算法(PLA)如下:
输入:二分类数据集![]
-
将
初始化为零,迭代计数器
-
只要存在任何错误分类的样本:
-
选取一个被错误分类的样本,记作
,其真实标签是
-
按如下方式更新
:
-
增加迭代计数器,![],并重复
返回:
现在,让我们看看这在 Python 中是如何实现的。
Python 中的 PLA
这里有一个 Python 实现,我们将逐部分讨论,部分内容已被讨论过:
N = 100 # number of samples to generate
random.seed(a = 7) # add this to achieve for reproducibility
X, y = make_classification(n_samples=N, n_features=2, n_classes=2,
n_informative=2, n_redundant=0, n_repeated=0,
n_clusters_per_class=1, class_sep=1.2,
random_state=5)
y[y==0] = -1
X_train = np.append(np.ones((N,1)), X, 1) # add a column of ones
# initialize the weights to zeros
w = np.zeros(X_train.shape[1])
it = 0
# Iterate until all points are correctly classified
while classification_error(w, X_train, y) != 0:
it += 1
# Pick random misclassified point
x, s = choose_miscl_point(w, X_train, y)
# Update weights
w = w + s*x
print("Total iterations: ", it)
前几行在本章的张量操作部分已讨论过。将初始化为零,通过
w = np.zeros(X_train.shape[1])
完成。该向量的大小取决于输入的维度。然后,it
只是一个迭代计数器,用于跟踪执行的迭代次数,直到 PLA 收敛为止。
classification_error()
方法是一个辅助方法,接受当前的参数向量w
、输入数据X_train
和相应的目标数据y
作为参数。该方法的目的是确定当前状态下误分类的点的数量(如果有的话),并返回错误的总数。该方法可以定义如下:
def classification_error(w, X, y):
err_cnt = 0
N = len(X)
for n in range(N):
s = np.sign(w.T.dot(X[n]))
if y[n] != s:
err_cnt += 1 # we could break here on large datasets
return err_cnt # returns total number of errors
该方法可以简化如下:
def classification_error(w, X, y):
s = np.sign(X.dot(w))
return sum(s != y)
然而,虽然这种优化方法对于小型数据集很有用,但对于大型数据集来说,可能没有必要计算所有点的误差。因此,第一种(且较长的)方法可以根据预期的数据类型进行使用和修改,如果我们知道将处理大型数据集,可以在第一次遇到误差时就跳出该方法。
我们代码中的第二个辅助方法是choose_miscl_point()
。该方法的主要目的是随机选择一个被误分类的点(如果有的话)。它的参数包括当前的参数向量w
、输入数据X_train
和对应的目标数据y
。它返回一个误分类点x
以及该点对应的目标符号s
。该方法可以按如下方式实现:
def choose_miscl_point(w, X, y):
mispts = []
for n in range(len(X)):
if np.sign(w.T.dot(X[n])) != y[n]:
mispts.append((X[n], y[n]))
return mispts[random.randrange(0,len(mispts))]
同样,也可以通过随机化索引列表、遍历它们并返回第一个找到的点来优化速度,如下所示:
def choose_miscl_point(w, X, y):
for idx in random.permutation(len(X)):
if np.sign(w.T.dot(X[idx])) != y[idx]:
return X[idx], y[idx]
然而,第一种实现方式对绝对初学者或者那些想要对误分类点进行额外分析的人来说非常有用,这些误分类点可以方便地保存在mispts
列表中。
无论实现方式如何,关键点是要随机选择误分类的点。
最后,更新是通过使用当前参数、误分类点和对应的目标,通过执行w = w + s*x
的方式完成的。
如果你运行完整的程序,它应该输出类似这样的内容:
Total iterations: 14
总迭代次数可能会根据数据类型和误分类点选择的随机性质而有所不同。对于我们使用的特定数据集,决策边界可能看起来像图 5.3所示:
图 5.3 – 使用 PLA 找到的决策边界
迭代次数还将取决于特征空间中数据点之间的分隔或间隙。间隙越大,越容易找到解决方案,反之亦然。最坏的情况是当数据是非线性可分时,我们将在接下来讨论这一点。
在非线性可分数据上的感知器
如前所述,当数据是可分离时,感知器将在有限时间内找到解决方案。然而,找到解决方案所需的迭代次数取决于数据组在特征空间中彼此的距离。
收敛是指学习算法找到一个解决方案,或者达到一个被学习模型设计者接受的稳定状态。
接下来的段落将讨论在不同类型数据上的收敛性:线性可分和非线性可分。
对线性可分数据的收敛性
对于我们在本章中研究的特定数据集,两个数据组之间的分离度是一个可变参数(这通常是实际数据中的一个问题)。该参数是class_sep
,可以取任意实数;例如:
X, y = make_classification(..., class_sep=2.0, ...)
这使我们能够研究当改变分隔参数时,感知机算法收敛所需的平均迭代次数。实验设计如下:
-
我们将分隔系数从大到小变化,记录其收敛所需的迭代次数:2.0、1.9、...、1.2、1.1。
-
我们将重复进行此实验 1000 次,并记录平均迭代次数及其对应的标准差。
请注意,我们决定将实验运行到 1.1,因为 1.0 已经生成了一个非线性可分的数据集。如果我们进行实验,我们可以将结果记录在表格中,结果如下:
运行 | 2.0 | 1.9 | 1.8 | 1.7 | 1.6 | 1.5 | 1.4 | 1.3 | 1.2 | 1.1 |
---|---|---|---|---|---|---|---|---|---|---|
1 | 2 | 2 | 2 | 2 | 7 | 10 | 4 | 15 | 13 | 86 |
2 | 5 | 1 | 2 | 2 | 4 | 8 | 6 | 26 | 62 | 169 |
3 | 4 | 4 | 5 | 6 | 6 | 10 | 11 | 29 | 27 | 293 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
998 | 2 | 5 | 3 | 1 | 9 | 3 | 11 | 9 | 35 | 198 |
999 | 2 | 2 | 4 | 7 | 6 | 8 | 2 | 4 | 14 | 135 |
1000 | 2 | 1 | 2 | 2 | 2 | 8 | 13 | 25 | 27 | 36 |
平均值 | 2.79 | 3.05 | 3.34 | 3.67 | 4.13 | 4.90 | 6.67 | 10.32 | 24.22 | 184.41 |
标准差 | 1.2 | 1.3 | 1.6 | 1.9 | 2.4 | 3.0 | 4.7 | 7.8 | 15.9 | 75.5 |
该表格显示,当数据分隔得很好时,迭代次数的平均值相对稳定;然而,当分隔间隔减小时,迭代次数显著增加。为了更直观地展示这一点,表格中的相同数据现在以图 5.4的对数刻度形式展示:
图 5.4 - 当数据组靠得越来越近时,PLA 迭代次数的增长
很明显,当分隔间隔变小的时候,迭代次数会呈指数级增长。图 5.5显示了最大的分隔间隔 2.0,并且表明 PLA 在四次迭代后找到了一个解:
图 5.5 - 感知机在分隔间隔为 2.0 时,找到了解,迭代了四次。
同样,图 5.6显示,当分隔间隔最大时,1.1,PLA 需要 183 次迭代;仔细查看图形可以发现,后者的解比较难找到,因为数据组之间的距离太近:
图 5.6 - 感知机在分隔间隔为 1.1 时,找到了一个解,迭代了 183 次。
如前所述,非线性可分的数据可以通过设置 1.0 的间隔来生成,PLA 将进入无限循环,因为总会有一个数据点被错误分类,而 classification_error()
方法将永远不会返回零值。对于这种情况,我们可以修改 PLA 以允许在非线性可分的数据上找到解决方案,接下来的部分将讨论这一点。
对非线性可分数据的收敛
对原始 PLA 的修改相当简单,但足以在大多数情况下找到一个可接受的解决方案。我们需要向 PLA 添加的主要两项内容如下:
-
防止算法永远运行下去的机制
-
存储最佳解决方案的机制
关于第一点,我们可以简单地指定一个算法停止的迭代次数。关于第二点,我们可以简单地将一个解决方案保存在存储中,并将其与当前迭代的解决方案进行比较。
这里展示了 PLA 的相关部分,新的变化已用粗体标出,稍后将进行详细讨论:
X, y = make_classification(n_samples=N, n_features=2, n_classes=2,
n_informative=2, n_redundant=0, n_repeated=0,
n_clusters_per_class=1, class_sep=1.0,
random_state=5)
y[y==0] = -1
X_train = np.append(np.ones((N,1)), X, 1) # add a column of ones
# initialize the weights to zeros
w = np.zeros(X_train.shape[1])
it = 0
bestW = {}
bestW['err'] = N + 1 # dictionary to keep best solution
bestW['w'] = []
bestW['it'] = it
# Iterate until all points are correctly classified
# or maximum iterations (i.e. 1000) are reached
while it < 1000:
err = classification_error(w, X_train, y)
if err < bestW['err']: # enter to save a new w
bestW['err'] = err
bestW['it'] = it
bestW['w'] = list(w)
if err == 0: # exit loop if there are no errors
break
it += 1
# Pick random misclassified point
x, s = choose_miscl_point(w, X_train, y)
# Update weights
w += s*x
print("Best found at iteration: ", bestW['it'])
print("Number of misclassified points: ", bestW['err'])
在这段代码中,bestW
是一个字典,用来追踪到目前为止的最佳结果,并且它被初始化为合理的值。首先需要注意的是,循环现在被数字 1,000 限制,这是你当前允许的最大迭代次数,你可以根据需要将其更改为任何你想要的最大迭代次数。对于大数据集或高维数据集,每次迭代的代价较高时,减少这个数字是合理的。
接下来的变化是加入了条件语句 if err < bestW['err']
,它决定了是否应该存储一组新的参数。每当通过总误分类样本数确定的误差低于存储参数的误差时,便会进行更新。为了完整性,我们还需要检查是否存在误差,这意味着数据是线性可分的,已找到解决方案,循环需要终止。
最后几个 print
语句将仅仅输出记录最佳解决方案时的迭代次数和误差。输出可能如下所示:
Best found at iteration: 95
Number of misclassified points: 1
该输出是通过在数据集上运行更新后的 PLA 生成的,数据集的分隔度为 1.0,如图 5.7所示:
图 5.7 – 更新后的 PLA 在 95 次迭代后找到一个解决方案,只有一个误分类点
从图中可以看到,正类中有一个样本被错误分类。知道在这个例子中总共有 100 个数据点,我们可以确定准确率为 99/100。
这种存储迄今为止最佳解决方案的算法,通常被称为口袋算法(Muselli,M. 1997)。而学习算法的提前终止思想则受到著名数值优化方法的启发。
其中一个普遍的限制是感知器只能生成基于二维空间中的一条直线或多维空间中的一个线性超平面的解决方案。然而,这个限制可以通过将多个感知器组合在一起,并放置在多个层中,轻松解决,从而为可分离和不可分离的问题生成高度复杂的非线性解决方案。这将是下一章的主题。
总结
本章介绍了经典感知器模型的概述。我们讨论了理论模型及其在 Python 中的实现,适用于线性和非线性可分数据集。到此为止,你应该足够自信,能够自己实现感知器。你应该能够在神经元的上下文中识别感知器模型。此外,你现在应该能够在感知器或任何其他学习算法中实现口袋算法和提前终止策略。
由于感知器是为深度神经网络铺平道路的最基本元素,在我们在此介绍了它之后,下一步是进入第六章,训练多层神经元。在这一章中,你将接触到使用多层感知器算法进行深度学习的挑战,比如用于误差最小化的梯度下降技术,以及超参数优化以实现泛化。但在你前往那里之前,请尝试通过以下问题进行自我测试。
问答
- 数据的可分性与 PLA 迭代次数之间有什么关系?
随着数据组之间的接近,迭代次数可能呈指数增长。
- PLA 会一直收敛吗?
并不总是,只有在数据是线性可分的情况下。
- PLA 可以在非线性可分数据上收敛吗?
不行。然而,你可以通过修改它,使用例如口袋算法,来找到一个可接受的解决方案。
- 为什么感知器如此重要?
因为它是最基本的学习策略之一,帮助我们构思了学习的可能性。如果没有感知器,科学界可能需要更长时间才能意识到基于计算机的自动学习算法的潜力。
参考文献
-
Rosenblatt, F. (1958). The perceptron: a probabilistic model for information storage and organization in the brain. Psychological review, 65(6), 386.
-
Muselli, M. (1997). On convergence properties of the pocket algorithm. IEEE Transactions on Neural Networks, 8(3), 623-629.
训练多个神经元层
在之前的第六章中,单神经元训练一节,我们探讨了涉及单一神经元和感知器概念的模型。感知器模型的一个限制是,最多只能在多维超平面上产生线性解。然而,通过使用多个神经元和多个神经元层来产生高度复杂的非线性解,针对可分和不可分问题,这一限制可以轻松解决。本章将带你了解深度学习的第一个挑战,使用多层感知器(MLP)算法,例如用于误差最小化的梯度下降技术,接着是超参数优化实验,以确定可靠的准确性。
本章将涵盖以下主题:
-
MLP 模型
-
最小化误差
-
寻找最佳超参数
第八章:MLP 模型
我们之前在第五章中,单神经元训练一节中,已经看到 Rosenblatt 的感知器模型对于某些问题来说既简单又强大(Rosenblatt, F. 1958)。然而,对于更复杂和高度非线性的问题,Rosenblatt 没有充分关注他连接了更多神经元并采用不同架构的模型,包括更深的模型(Tappert, C. 2019)。
多年后,在 1990 年代,2019 年图灵奖得主 Geoffrey Hinton 教授继续致力于将更多神经元连接在一起,因为这种方式比单一神经元更像大脑(Hinton, G. 1990)。如今,大多数人知道这种方法被称为联结主义。其主要思想是以不同的方式连接神经元,从而模拟大脑中的连接。第一个成功的模型之一是 MLP,它使用基于监督梯度下降的学习算法,通过标记数据学习逼近一个函数,,
。
图 6.1展示了一个包含多个神经元的 MLP,它指示了输入是如何通过权重与所有神经元连接的,这些权重激活神经元以产生一个较大的(非零)数值响应,具体取决于需要学习的变量权重:
图 6.1 – 单隐藏层中的多个感知器
为了完整性,图 6.2展示了相同的架构,但以竖直方向呈现;它还用浅灰色表示正权重,用深灰色表示负权重。图 6.2旨在展示某些特征可能比其他特征更能激活某些神经元:
图 6.2 – MLP,权重采用灰度编码:浅灰色表示正权重,深灰色表示负权重
基于图 6.2,顶部的神经元层被称为输入层。这些特征与称为隐藏层的不同神经元连接。这个层通常至少包含一层神经元,但在深度学习中,它可能包含更多层。
关于输入层附近权重的解释:MLP 和感知机之间的一个关键区别是,除非隐藏层仅包含一个神经元,否则输入层中权重的解释在 MLP 中会丧失。通常,在感知机中,你可以认为某些特征的重要性与直接与这些特征相关联的值(权重)有直接的关系。例如,最负权重相关联的特征被认为会对结果产生负面影响,而最正权重相关联的特征也会显著地影响结果。因此,在感知机(和线性回归)中,查看权重的绝对值可以帮助我们了解特征的重要性。在 MLP 中则不然;涉及的神经元越多,层数越多,解释权重和特征重要性的可能性就越小。你不应过于依赖第一层的权重来推断特征的重要性。要小心。
从图 6.1中,我们可以看到神经元,,被简化为意味着有某种非线性激活函数,![],作用在标量上,标量是通过加上特征和与这些特征及神经元相关的权重的乘积得到的,
。在更深的 MLP 层中,输入不再是来自输入层的数据,
,而是来自前一层的输出:
。我们将在下一节对符号进行一些更改,以更正式地描述这个过程。
目前,你需要知道的是,MLP 比感知机要好得多,因为它能够学习高度复杂的非线性模型。而感知机只能提供线性模型。但这种强大的能力也伴随着巨大的责任。MLP 有一个非凸且不平滑的损失函数,这限制了学习过程的实现,尽管已经取得了很多进展,但这些问题仍然存在。另一个缺点是,学习算法可能需要其他超参数来确保算法的成功(收敛)。最后,值得注意的是,MLP 需要对输入特征进行预处理(归一化),以减轻神经元在特定特征上过拟合的问题。
现在,让我们来看看学习过程是如何实际发生的。
最小化误差
使用 MLP 从数据中学习是其诞生以来的一个主要问题。正如我们之前指出的,神经网络面临的一个主要问题是更深层模型的计算可行性,另一个问题是稳定的学习算法,能够收敛到合理的最小值。机器学习的一个重大突破,也是为深度学习铺平道路的,是基于反向传播的学习算法的开发。许多科学家在 1960 年代独立推导并应用了不同形式的反向传播;然而,大多数功劳归于 G. E. Hinton 教授及其团队(Rumelhart, D. E. 等,1986 年)。在接下来的几段中,我们将详细介绍这个算法,其唯一目的是最小化错误,以减少在训练过程中由于预测不准确而导致的误差。
首先,我们将描述这个名为螺旋(spirals)的数据集。这是一个广为人知的基准数据集,具有两个可分的类别,但这些类别是高度非线性的。正负类别在二维空间的相对两侧盘绕,随着从中心向外扩展,如图 6.3所示:
图 6.3 - 来自双螺旋基准数据集的示例数据
可以使用以下 Python 函数生成该数据集:
def twoSpirals(N):
np.random.seed(1)
n = np.sqrt(np.random.rand(N,1)) * 780 * (2*np.pi)/360
x = -np.cos(n)*n
y = np.sin(n)*n
return (np.vstack((np.hstack((x,y)),np.hstack((-x,-y)))),
np.hstack((np.ones(N)*-1,np.ones(N))))
X, y = twoSpirals(300) #Produce 300 samples
在这个代码片段中,我们将接收一个X
的两列矩阵,其行是螺旋数据集的样本,而y
包含相应的目标类别,来自集合。图 6.3是基于前面的代码片段生成的,包含 300 个样本。
我们还将使用一个非常简单的多层感知机(MLP)架构,该架构仅包含一个隐藏层的三个神经元;这只是为了尽可能清晰地解释反向传播。所提出的 MLP 如图 6.4所示:
反向传播在业内人士中今天被称为反向传播(backprop)。如果你阅读最近的在线讨论,它很可能会被简称为 backprop。
图 6.4 - 用于基于反向传播学习的简单多层感知机(MLP)架构,应用于螺旋数据集
如图 6.4所示的网络架构假设存在一个定义良好的输入向量,包含多个向量,(一个矩阵),表示为
,以及多个独立的目标向量,
。此外,每一层,
,都有一个权重矩阵,
,这在第一层也是如此。例如,从图 6.4中,权重矩阵将如下所示:
。
这些矩阵的值是随机初始化的实际值。隐藏层 由三个神经元组成。每个神经元接收作为输入的
,这是特征和权重的内积,得到加权的观察值,指向第 i 个神经元;例如,对于第一个神经元,计算方式如下:
这里, 表示第一层中第一个神经元的激活函数的输出,在本例中将是一个 sigmoid 函数。
Sigmoid 激活函数表示为 。这个函数很有趣,因为它会将输入值压缩,并将其映射到 0 和 1 之间的值。它也是一个很好的用于梯度计算的函数,因为其导数是已知的,且易于计算:
。
在 Python 中,我们可以很容易地编写以下 sigmoid 函数代码:
def sigmoid(z, grad=False):
if grad:
return z * (1\. - z)
return 1\. / (1\. + np.exp(-z))
最后,输出层由两个神经元组成,在本例中我们将用它们来建模每个目标类别,即正螺旋和负螺旋。
有了这些,我们可以通过反向传播(backprop)来根据梯度的方向调整权重,从而最小化给定标签样本集的误差;更多详细信息,请参考此教程(Florez, O. U. 2017)。我们将按照接下来的步骤进行操作。
第 1 步 – 初始化
我们将执行初始步骤,在此步骤中我们 随机初始化 网络权重。在我们的示例中,我们将使用以下值:
在 Python 中,我们可以通过以下方式生成介于 -1
和 1
之间的这些权重:
w1 = 2.0*np.random.random((2, 3))-1.0
w2 = 2.0*np.random.random((3, 2))-1.0
第 2 步 – 前向传播
下一步是 前向传播。在此步骤中,输入 被传递到输入层,并向前传播到网络中,直到我们在输出层观察到结果向量。我们的小示例中的前向传播如下所示。我们首先对单个样本
进行线性变换,使用第一层中的权重
:
因此,对于某些情况下的 ![],我们计算如下:
这将导致以下结果:
然后,我们将 传递通过 sigmoid 函数并得到
,这就是第一隐藏层中三个神经元的输出。结果如下:
这可以这样实现:
o1 = sigmoid(np.matmul(X, w1))
以一种有趣的方式来看待我们在第一层所取得的成果,我们已经将二维的输入数据映射到三维空间,现在这些数据将被处理以便观察输出再回到二维空间。
同样的过程会在后续的隐藏层中重复。在我们的例子中,我们只会为输出层再做一次。我们计算如下:
这导致了以下计算结果:
这导致了以下结果:
同样,我们将 通过 sigmoid 函数传递,并得到
,这是输出层中两个神经元的输出。这导致了以下结果:
我们的实现方式如下:
o2 = sigmoid(np.matmul(o1, w2))
此时,我们需要为这个输出赋予一些意义,以便确定下一步的操作。我们希望在这两个神经元中建模的是输入数据 属于正类在
中的概率,以及属于负类在
中的概率。下一步是建立一个误差度量来进行学习。
误差度量,或称误差函数,也叫做损失函数。
第 3 步 – 计算损失
下一步是定义并计算总损失。在第四章《从数据中学习》中,我们讨论了一些误差度量(或损失),例如均方误差(MSE):
思考这个损失函数的导数非常重要,因为我们希望根据该损失函数提供的梯度调整网络的权重。因此,我们可以进行小的调整,这些调整不会影响学习过程的整体结果,但能得到很好的导数。例如,如果我们对 取导数,平方将导致乘以 2 的因子,但我们可以通过稍微修改 MSE,加入除以 2 的操作,来抵消这一影响,具体如下:
因此,这个损失可以用来确定预测与实际目标结果的“偏差”有多大。在前面的例子中,期望的结果如下:
预测的响应如下所示:
这是正常的,因为权重是随机初始化的;因此,模型的表现较差是可以预期的。可以通过使用现代方法进一步改进网络,这些方法对权重采取过大值进行惩罚。在神经网络中,总是存在梯度爆炸或梯度消失的风险,而减少大梯度影响的一种简单方法是限制权重能够取的数值范围。这被广泛称为正则化。它还带来了其他良好的特性,例如sparse(稀疏)模型。我们可以通过以下方式修改损失函数来实现这种正则化:
这个损失函数可以如下实现:
L = np.square(y-o2).sum()/(2*N) + lambda*(np.square(w1).sum()+np.square(w2).sum())/(2*N)
添加的正则化项将每层中的所有权重相加,并根据![] 参数对大的权重进行惩罚。这是一个超参数,需要我们自己进行微调。一个大的![]值会对任何大权重进行重罚,而一个小的![]值则忽略权重在学习过程中的任何影响。这就是我们将在此模型中使用的损失函数,值得注意的是,正则化项也是容易求导的。
第 4 步 – 反向传播
下一步是执行反向传播。目标是根据损失的大小调整权重,并朝着减少损失的方向进行调整。我们首先计算关于输出层权重的偏导数 ,然后计算关于第一层权重的偏导数
。
我们通过解决第一个偏导数来开始反向传播。我们可以通过使用著名的链式法则来做到这一点,该法则允许我们将主导数分解成表示相同过程的多个部分;我们可以按如下方式进行:
在这里, 适用于所有的
。如果我们独立定义这些偏导数的每一部分,我们就得到了以下结果:
这三个偏导数每次都有一个精确的解。在我们的例子中,它们的值将如下所示:
现在,由于我们需要更新权重, ,我们需要一个 3 x 2 的矩阵,因此,我们可以通过以下方式将偏导数的向量相乘来获得这个更新:
为了得到这个结果,我们首先需要对右侧的两个小向量进行逐元素乘法,然后通过左侧转置向量执行常规乘法。在 Python 中,我们可以这样做:
dL_do2 = -(y - o2)
do2_dz2 = sigmoid(o2, grad=True)
dz2_dw2 = o1
dL_dw2 = dz2_dw2.T.dot(dL_do2*do2_dz2) + lambda*np.square(w2).sum()
现在我们已经计算了导数,我们可以使用传统的梯度缩放因子来更新权重,这个因子被称为学习率。我们计算新的值,如下所示:
学习率是我们在机器学习中用来限制导数在更新过程中影响的一种机制。记住,导数被解释为在给定输入数据下,权重变化的速率。一个大的学习率过多地决定了导数的方向和幅度,存在跳过良好局部最小值的风险。一个小的学习率则仅部分考虑了导数的信息,可能导致朝局部最小值的进展非常缓慢。学习率是需要调整的另一个超参数。
现在,我们继续计算下一个导数,,这将使我们能够计算关于
的更新。我们首先定义偏导数并尝试简化其计算,如下所示:
如果我们仔细关注第一个偏导数,,我们可以注意到其导数定义如下:
但下划线部分已经在之前计算过了!注意,下划线部分等价于在先前定义方程中的下划线部分:
这是一个很好的性质,得益于微分链式法则,它使我们能够重复利用计算,并拥有一个更加高效的学习算法。这个优良的性质还告诉我们,实际上我们正在将更深层的信息融入到靠近输入的层中。现在,我们继续进行每个偏导数的单独计算,因为我们已经完成了一部分工作。
由于,那么第一项可以表示为:
在我们的示例中,这会导致以下结果:
现在,偏导数中的第二项可以如下计算:
这导致了以下向量:
完成这些后,我们现在能够计算最后一项,可以直接按如下方式计算:
最后,我们可以将每个偏导数的结果代入链式法则的乘积中:
通过重新排列向量以获得与权重矩阵维度一致的结果矩阵,即可获得此结果。乘法运算得到以下结果:
在 Python 中,我们这样做:
dL_dz2 = dL_do2 * do2_dz2
dz2_do1 = w2
dL_do1 = dL_dz2.dot(dz2_do1.T)
do1_dz1 = sigmoid(o1, grad=True)
dz1_dw1 = X
dL_dw1 = dz1_dw1.T.dot(dL_do1*do1_dz1) + lambda*np.square(w1).sum()
最后,相应的更新计算如下:
通过在迭代(epoch)时分配![]来完成反向传播算法,具体实现如下:
w1 += -alpha*dL_dw1
w2 += -alpha*dL_dw2
这个过程会根据我们希望的时期(epochs)重复进行。我们可以使用以下参数让算法运行:
然后,得到的分离超平面将如下图所示:
图 6.5 - 分离三神经元 MLP 的超平面
该图显示有许多样本被错误分类,这些错误分类的样本以黑点表示。总准确率为 62%。显然,三个神经元足以产生一个比随机猜测更好的分类器;然而,这并不是最理想的结果。接下来,我们必须通过调整超参数和神经元或层的数量来调优分类器。这就是我们接下来要讨论的内容。
寻找最佳超参数
使用 Keras 编码,我们可以用一种更简单的方式实现我们在上一节中编写的代码。我们可以依赖于反向传播算法已经被正确编码,并且经过改进以增强稳定性,同时还有一套丰富的其他特性和算法可以提升学习过程。在我们开始优化 MLP 的超参数之前,应该指出,使用 Keras 时的等效实现是什么。以下代码应该重现相同的模型、几乎相同的损失函数和几乎相同的反向传播方法:
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
mlp = Sequential()
mlp.add(Dense(3, input_dim=2, activation='sigmoid'))
mlp.add(Dense(2, activation='sigmoid'))
mlp.compile(loss='mean_squared_error',
optimizer='sgd',
metrics=['accuracy'])
# This assumes that you still have X, y from earlier
# when we called X, y = twoSpirals(300)
mlp.fit(X, y, epochs=1000, batch_size=60)
这将产生 62.3%的错误率和如图 6.7所示的决策边界:
图 6.6 – 基于 Keras 的 MLP,和图 6.5中的模型相同
该图与图 6.6非常相似,预期如此,因为它们是相同的模型。但让我们简要回顾一下代码中描述的模型含义。
如前所述,from tensorflow.keras.models import Sequential
导入了 Sequential 库,它允许我们创建一个顺序模型,而不是函数式模型创建方法,mlp = Sequential()
,它还允许我们向模型中添加元素,mlp.add()
,例如多个神经元层(全连接层):Dense(...)
。
顺序模型的第一层必须指定输入的维度(输入层大小),在本例中为 2
,并指定激活函数为 sigmoid:mlp.add(Dense(3, input_dim=2, activation='sigmoid'))
。这里的数字 3
表示该模型在第一隐藏层中将有多少个神经元。
第二层(也是最后一层)类似,但表示输出层中的两个神经元:mlp.add(Dense(2, activation='sigmoid'))
。
一旦指定了顺序模型,我们必须对其进行编译,mlp.compile(...)
,定义要最小化的损失函数,loss='mean_squared_error'
,要使用的优化(反向传播)算法,optimizer='sgd'
,以及在每个训练周期后报告的度量列表,metrics=['accuracy']
。这里定义的均方损失函数不包括前面提到的正则化项,但这不应对结果产生更大影响;因此,损失函数是我们之前见过的:
sgd
优化器定义了一种算法,称为 随机梯度下降。这是一种计算梯度并相应更新权重的稳健方法,自 20 世纪 50 年代以来就已经出现了[Amari, S. I. 1993]。在 Keras 中,默认的 学习率 是 ;然而,该学习率有衰减策略,使学习率能够根据学习过程进行调整。
鉴于此,我们将调整以下超参数:
-
学习率,
,是自适应的。
-
层数为 2、3 或 4,每层 16 个神经元(输出层除外)。
-
激活函数,可以是 ReLU 或 sigmoid。
这可以通过进行多次交叉验证实验来实现,正如在第四章《从数据中学习》中所解释的那样。下表展示了在五折交叉验证下执行的实验和相应的结果:
实验 | 超参数 | 平均准确率 | 标准差 |
---|---|---|---|
a |
(16-Sigmoid, 2-Sigmoid) | 0.6088 | 0.004 |
b |
(16-ReLU, 2-Sigmoid) | 0.7125 | 0.038 |
c |
(16-Sigmoid, 16-Sigmoid, 2-Sigmoid) | 0.6128 | 0.010 |
d |
(16-ReLU, 16-Sigmoid, 2-Sigmoid) | 0.7040 | 0.067 |
e |
(16-Sigmoid, 16-Sigmoid, 16-Sigmoid, 2-Sigmoid) | 0.6188 | 0.010 |
f |
(16-ReLU, 16-Sigmoid, 16-ReLU, 2-Sigmoid) | 0.7895 | 0.113 |
g |
(16-ReLU, 16-ReLU, 16-Sigmoid, 2-Sigmoid) | 0.9175 | 0.143 |
h |
(16-ReLU, 16-ReLU, 16-ReLU, 2-Sigmoid) | 0.9050 | 0.094 |
i |
(16-ReLU, 16-Sigmoid, 16-Sigmoid, 2-Sigmoid) | 0.6608 | 0.073 |
请注意,另有一些实验是加上了第五层进行的,但在平均性能和变异性方面并未显著提高。看来,四层,每层仅有 16 个神经元(除了输出层为 2 个神经元),就足以产生足够的类别分离。图 6.8 展示了来自实验 g
的一个样本运行,达到了 99% 的最高准确率:
图 6.7 – 使用四层(16,16,16,2)神经网络对两个螺旋数据集的分类边界。对应于表 1 中的实验 g。
对 图 6.8 进行视觉检查可以发现,最大的混淆边缘位于螺旋的起始中心区域,那里两条螺旋非常接近。还可以注意到,分隔超平面在某些区域似乎是不平滑的,这通常是 MLP 的特点。有些人认为,这一现象是由于输入层的神经元使用线性函数来近似某个函数,而更深层的神经元则是线性函数的混合体,这些混合体基于这些线性函数生成非线性函数。当然,情况要复杂得多,但这里值得注意的是这一点。
在我们结束这一章之前,请注意还有其他超参数是我们可以通过经验进行优化的。我们本可以选择不同的优化器,例如 adam
或 rmsprop
;我们本可以尝试其他激活函数,例如 tanh
或 softmax
;我们本可以尝试更多的层数;或者我们本可以尝试更多(或更少)且不同数量的神经元,以递增、递减或混合的顺序。不过,现阶段,这些实验足以表明,实验不同的选项是找到最适合我们特定应用或我们要解决的问题的方法的关键。
这结束了我们的介绍章节,接下来的章节将讨论具有特定用途的架构类型,而不是通常被认为是多用途、基础神经网络的 MLP。我们的下一章将讨论自编码器;它们可以看作是一个特殊类型的神经网络,旨在将输入数据编码成一个更小的维度空间,然后再将其重构回原始输入空间,最小化重构数据中的信息丢失。自编码器允许我们压缩数据,并在没有与数据相关标签的情况下进行学习。后者使得自编码器成为一种特殊的神经网络,采用被归类为无监督学习的方法进行学习。
摘要
本章是一个中级入门章节,展示了 MLP 的设计及其功能的相关范式。我们讨论了其元素背后的理论框架,并对广为人知的反向传播机制进行了全面的讨论,用于对损失函数进行梯度下降。理解反向传播算法对于后续章节至关重要,因为一些模型专门设计用来克服反向传播可能遇到的一些困难。你应该对你学到的反向传播知识有信心,这将帮助你更好地理解深度学习的本质。反向传播算法,除此之外,是深度学习成为一个令人兴奋的领域的原因之一。现在,你应该能够理解并设计具有不同层和不同神经元的 MLP。此外,你应该有信心去改变其一些参数,尽管我们会在后续阅读中详细讲解更多内容。
第七章,自编码器,将继续介绍一种与 MLP 非常相似的架构,这种架构今天被广泛应用于许多不同的学习任务,尤其是与数据表示学习相关的任务。本章开启了一个新的部分,专门讨论基于无监督学习的算法和模型,这种学习方式让你即使在数据没有标签的情况下,也能从中学习。
问题与答案
- 为什么 MLP 优于感知器模型?
神经元数量和层数更多的 MLP 使其相对于感知器在建模非线性问题和解决更复杂的模式识别问题上具有优势。
- 为什么反向传播如此重要?
因为它是让神经网络在大数据时代得以学习的关键。
- MLP 总是会收敛吗?
是的,也不是。它总是会收敛到损失函数的局部最小值;然而,它不能保证收敛到全局最小值,因为通常大多数损失函数都是非凸的和非平滑的。
- 我们为什么要尝试优化模型的超参数?
因为任何人都可以训练一个简单的神经网络;然而,并不是每个人都知道应该改变哪些内容来使其更好。模型的成功在很大程度上取决于你尝试不同的方案,并向自己(和他人)证明你的模型已经是最好的。这将使你成为更好的学习者,也成为更优秀的深度学习专业人员。
参考文献
-
Rosenblatt, F. (1958). 感知器:一种用于大脑信息存储和组织的概率模型。Psychological Review, 65(6), 386。
-
Tappert, C. C. (2019). 谁是深度学习的奠基人?人工智能研讨会。
-
Hinton, G. E. (1990). 连接主义学习程序。Machine learning。Morgan Kaufmann, 555-610。
-
Rumelhart, D. E., Hinton, G. E., & Williams, R. J. (1986). 通过反向传播误差学习表示。Nature, 323(6088), 533-536。
-
Florez, O. U. (2017). 一次一个乐高:解释神经网络学习的数学原理。在线:
omar-florez.github.io/scratch_mlp/
. -
Amari, S. I. (1993). 反向传播与随机梯度下降方法。神经计算, 5(4-5), 185-196.
第九章
第二部分:无监督深度学习
在将 MLP 作为第一个监督方法之后,本节侧重于称为无监督算法的学习算法。它从简单的自编码器开始,然后转向更深和更大的神经模型。
本节包括以下章节:
-
第七章,自编码器
-
第八章,深度自编码器
-
第九章,变分自编码器
-
第十章,受限玻尔兹曼机
自动编码器
本章通过解释编码层和解码层之间的关系来介绍自动编码器模型。我们将展示一个属于无监督学习范畴的模型。本章还介绍了与自动编码器模型常关联的损失函数,并将其应用于 MNIST 数据的降维及其在自动编码器诱导的潜在空间中的可视化。
本章将涉及以下主题:
-
无监督学习简介
-
编码和解码层
-
降维与可视化的应用
-
无监督学习的伦理影响
第十章:无监督学习简介
随着机器学习在过去几年中的发展,我遇到了许多分类不同类型学习的方法。最近,在 2018 年蒙特利尔举行的 NeurIPS 大会上,Alex Graves 博士分享了关于不同类型学习的信息,见图 7.1:
图 7.1 – 不同类型的学习
这种分类方法在今天非常有用,因为目前有许多学习算法正在被研究和改进。第一行描述了主动学习,这意味着学习算法与数据之间存在交互。例如,在强化学习和作用于标记数据的主动学习中,奖励策略可以指示模型在接下来的迭代中将读取哪种类型的数据。然而,传统的监督学习(即我们迄今为止所学习的内容)不与数据源交互,而是假设数据集是固定的,并且其维度和形状不会改变;这些非交互式方法被称为被动学习。
图 7.1中表格的第二列代表一种特殊类型的学习算法,它不需要标签来从数据中学习。其他算法则要求你拥有一个带有数据的与标签
相关联的数据集;也就是说:
。然而,无监督算法不需要标签来“处理”数据。
你可以把标签看作是老师。老师告诉学习者x对应于![],然后学习者通过反复试验来尝试学习![]和![]之间的关系,不断调整其信念(参数),直到理解正确。然而,如果没有老师,学习者对标签一无所知,因此学习者通过自己从
中学习某些东西,并在一定的边界条件下形成自己对
的信念,而永远不会知道
的任何信息。
在接下来的章节中,我们将学习无监督学习,这是一种假设我们所拥有的数据在形态或形式上不会发生变化,并且在学习过程以及部署过程中保持一致的学习方式。这些算法的指导依据是其他因素,而非标签,例如,针对数据压缩的独特损失函数。另一方面,还有一些算法具有探索机制或特定的动机,以交互方式从数据中学习,这些算法被称为主动学习算法。本书不会讨论这些算法,因为本书旨在作为入门教程,适合完全初学者。然而,我们将详细讨论一些最强大的无监督深度学习模型。
我们将首先学习自编码器。自编码器的唯一目的是将输入数据输入到由两部分组成的神经网络中:编码器和解码器。编码器部分的任务是对输入数据进行编码,通常是将其压缩到一个低维空间,从而压缩或编码输入数据。模型的解码器部分负责将编码(或压缩)的输入数据的潜在表示重新构建回原始形状和原始值,而不丢失任何数据。也就是说,在理想的自编码器中,输入等于输出。让我们在接下来的章节中更详细地讨论这一点。
编码和解码层
自编码器可以分解为两个主要组件,这些组件在无监督学习过程中起到特定的作用。图 7.2的左侧展示了一个使用全连接(密集)层实现的自编码器。它接收一些向量作为输入,,然后进入六个隐藏层;前三个层分别有 6、4 和 2 个神经元,目的是将输入数据压缩到二维,因为两个神经元的输出是两个标量值。这个第一组层被称为编码器:
图 7.2 – 自动编码器的两种表示。左侧:完整且描述性的模型。右侧:简洁且抽象的模型表示
第二组神经元用于将输入数据重建回其原始维度和数值!,它通过三层,分别有 4、6 和 8 个神经元来实现;这一组层被称为解码器。
注意,自动编码器的最后一层必须具有与输入向量的维度相同的神经元数量。否则,重建结果将无法与输入数据匹配。
在这种情况下,图 7.2 左侧显示的自动编码器充当压缩网络,意思是,在训练模型达到良好重建后,如果我们断开解码器,我们就得到了一个神经网络,它将输入数据编码为二维(或我们选择的任何维度)。这相比于监督学习模型,具有独特的优势:在监督学习模型中,我们教网络寻找一个可以与给定目标标签关联的模式;然而,在无监督学习中(比如在这个自动编码器中),网络并不寻找一个特定的模式,而是学习以任何能够保留输入数据最具代表性和最重要信息的方式使用输入空间,从而允许在解码器中进行良好的重建。
想象一个神经网络和一个自动编码器,它们接受猫和狗的图像作为输入;传统的神经网络可以被训练来区分猫和狗,它的任务是寻找狗和猫图像中的重要模式,以便区分它们;然而,自动编码器将训练来学习最重要的模式,所有模式中最具代表性的模式,以便保留这些信息并允许无论标签是什么都能进行良好的重建。从某种程度上说,传统的监督神经网络是带有偏见的,它只能从猫和狗的角度来看待世界,而自动编码器则可以自由地从任何角度学习,不管是猫还是狗。
图 7.2 右侧的图示展示了自动编码器的另一种表示,它更加抽象且紧凑。这种表示方式在描述相对深的自动编码器时非常有用,当网络层数多到难以逐一表示所有神经元和所有层时(如图 7.2 左侧所示)。我们将使用这些梯形图形来表示编码器/解码器;我们还注意到,这种抽象化将允许我们使用其他类型的层,而不仅仅是密集(全连接)层。图 7.2 右侧的图示展示了一个自动编码器,该编码器将图像作为输入,接着将输入编码到d-维空间,然后将潜在向量重建回输入(图像)空间。
潜在空间是一个映射学习到的低维模式的空间。它也被称为 学习到的表示空间。理想情况下,这个潜在空间包含关于输入数据的重要信息,而且其维度少于输入数据,同时不会丢失任何信息。
现在我们来实现每个自编码器部分,基于左侧的简单模型,如 图 7.2 所示。
编码层
我们将使用的 TensorFlow 和 Keras 库包括来自 tensorflow.keras.layers
的 Input
和 Dense
,以及来自 tensorflow.keras.models
的 Model
。我们将采用 keras
的函数式方法,而不是 顺序 建模。请导入以下内容:
from tensorflow.keras.layers import Input, Dense
from tensorflow.keras.models import Model
Input
层将用来描述输入向量的维度,在我们的例子中是 8
:
inpt_dim = 8
ltnt_dim = 2
inpt_vec = Input(shape=(inpt_dim,))
然后,考虑到所有的激活函数都为 sigmoid
,仅仅为了这个例子,我们可以如下定义编码器各层的流水线:
elayer1 = Dense(6, activation='sigmoid')(inpt_vec)
elayer2 = Dense(4, activation='sigmoid') (elayer1)
encoder = Dense(ltnt_dim, activation='sigmoid') (elayer2)
Dense
类构造函数接收神经元数量和激活函数作为参数,在定义的末尾(右侧)我们必须包括该层的输入,这个输入在左侧赋予名称。因此,在代码行 elayer1 = Dense(6, activation='sigmoid')(inpt_vec)
中,分配给该层的名称是 elayer1
,6
是神经元数量,activation='sigmoid'
为该密集层分配了 sigmoid
激活函数,而 inpt_vec
是该层的输入。
在前面三行代码中,我们定义了编码器的各层,而 encoder
变量指向可以输出潜在变量的对象,如果我们将其作为模型并调用 predict()
方法:
latent_ncdr = Model(inpt_vec, encoder)
在这一行代码中,latent_ncdr
包含可以将输入数据映射到潜在空间的模型,一旦训练完成。但在此之前,让我们以类似的方式定义解码器的各层。
解码层
我们可以如下定义解码器各层:
dlayer1 = Dense(4, activation='sigmoid')(encoder)
dlayer2 = Dense(6, activation='sigmoid') (dlayer1)
decoder = Dense(inpt_dim, activation='sigmoid') (dlayer2)
请注意,在前面的代码中,神经元的数量通常是递增的,直到最后一层与输入维度匹配。在此案例中,4、6 和 8 被定义为 inpt_dim
。类似地,decoder
变量指向能够输出重建输入的对象,如果我们将其作为模型并调用 predict()
方法。
我们故意将编码器和解码器分开,仅仅是为了展示如果需要的话,我们可以访问网络的不同组件。然而,我们可能也应该将自编码器从输入到输出作为一个整体来定义,使用 Model
类,如下所示:
autoencoder = Model(inpt_vec, decoder)
这正是我们之前所说的“如果我们将其做成一个模型并调用 predict()
”的意思。这个声明实际上是在创建一个模型,它以inpt_vec
中定义的输入向量作为输入,并从decoder
层中获取输出。然后,我们可以使用这个模型对象,它在 Keras 中有一些方便的功能,允许我们传入输入、读取输出、训练以及进行其他我们将在接下来的章节中讨论的操作。现在,由于我们已经定义了模型,并且在训练之前,我们需要定义训练的目标,也就是我们的损失函数。
损失函数
我们的损失函数必须与自编码器的目标相关。这个目标是完美地重建输入。也就是说,在理想的自编码器中,我们的输入 和我们的重建
必须是相同的。这意味着它们之间的绝对差异必须为零:
然而,这可能不太现实,并且它并不是一个我们容易求导的函数。为此,我们可以回到经典的均方误差函数,其定义如下:
我们希望使 变得理想,或者至少尽可能将其最小化。我们将这个损失函数解释为最小化输入与其重建之间的平方差的平均值。如果我们使用标准的反向传播策略,比如某种标准的梯度下降技术,我们可以像下面这样编译模型并准备训练:
autoencoder.compile(loss='mean_squared_error', optimizer='sgd')
compile()
方法为训练做好准备。先前定义的损失函数作为参数传入,loss='mean_squared_error'
,这里选择的优化技术被称为随机梯度下降 (SGD), optimizer='sgd'
。有关 SGD 的更多信息,请参见 Amari, S. I. (1993)。
学习与测试
由于这是一个简单自编码器的入门示例,我们将只使用一个数据点进行训练,并开始学习过程。我们还希望展示编码版本和重建版本。
我们将使用数字 39 的二进制表示,作为八位数字,它对应于 00100111. 我们将声明其为我们的输入向量,具体如下:
import numpy as np
x = np.array([[0., 0., 1., 0., 0., 1., 1., 1.]])
然后我们可以执行如下的训练:
hist = autoencoder.fit(x, x, epochs=10000, verbose=0)
encdd = latent_ncdr.predict(x)
x_hat = autoencoder.predict(x)
fit()
方法执行训练。它的前两个参数是输入数据和期望的目标输出;在自编码器的情况下,它们都是 x
。训练的轮次通过 epochs=10000
来指定,因为此时模型已经能够生成不错的输出,我们将冗余输出设置为零,使用 verbose=0
,因为我们不需要可视化每个 epoch。
在 Google Colab 或 Jupyter Notebook 中,一次性在屏幕上可视化超过 1,000 个时期并不是一个好主意。网页浏览器可能会对负责显示所有这些时期的 JavaScript 代码无响应。请注意。
在潜在编码器模型latent_ncdr
和autoencoder
模型中的predict()
方法会在指定的层产生输出。如果我们检索encdd
,我们可以看到输入的潜在表示,如果我们检索x_hat
,我们可以看到重建。我们甚至可以手动计算均方误差,如下所示:
print(encdd)
print(x_hat)
print(np.mean(np.square(x-x_hat))) # MSE
这产生了以下输出:
[[0.54846555 0.4299447 ]]
[[0.07678119 0.07935049 0.91219556 0.07693048 0.07255505 0.9112366 0.9168126 0.9168152 ]]
0.0066003498745448655
这里的数字会因为学习算法的非监督特性而有所变化。第一个输出向量可以是任意实数。第二个输出向量可能会有接近零和接近一的实数,类似于原始的二进制向量,但确切的值每次都会有所不同。
第一个由两个元素组成的向量是潜在表示,[0.55, 0.43];此时这可能对我们来说意义不大,但在数据压缩方面,它将非常重要。这意味着我们可以用两个数字来表示八个数字。
虽然这只是一个玩具示例,用两位数字表示二进制数并不是非常令人兴奋,但其背后的理论是,我们可以取[0, 1]范围内的任意八个浮点数,并将它们压缩到同一范围内的两个数字中。
第二个显示的向量显示了良好重建的证据:应该是零的东西是 0.08,应该是一的东西是 0.91。手动计算的均方误差(MSE)为 0.007,虽然不是零,但足够小以达到良好的效果。
我们可以使用在调用fit()
时定义的hist
对象中存储的信息来可视化训练阶段的 MSE 衰减过程。该对象包含跨时期的损失函数值信息,并允许我们使用以下代码可视化该过程:
import matplotlib.pyplot as plt
plt.plot(hist.history['loss'])
plt.title('Model reconstruction loss')
plt.ylabel('MSE')
plt.xlabel('Epoch')
plt.show()
这产生了您在图 7.3中看到的内容:
图 7.3 – 自编码器训练过程中的重建损失随时期的变化
好吧,再次强调,这只是一个使用一个数据点的玩具示例。我们在现实生活中绝不会这样做。为了展示这是一个多么糟糕的主意,我们可以取用于训练模型的二进制字符串并反转每一个位,得到 11011000(或者十进制的 216)。如果我们将这个输入给自编码器,我们期望会有一个良好的重建,但让我们看看如果我们尝试这样做会发生什么:
x = np.array([[1., 1., 0., 1., 1., 0., 0., 0.]]) #216
encdd = latent_ncdr.predict(x)
x_hat = autoencoder.predict(x)
print(encdd)
print(x_hat)
print(np.mean(np.square(x-x_hat)))
我们得到以下输出:
[[0.51493704 0.43615338]]
[[0.07677279 0.07933337 0.9122421 0.07690183 0.07254466 0.9112378 0.9167745 0.91684484]]
0.8444848864148122
再次强调,这里的数字会因为学习算法的非监督特性而有所变化。如果您的结果与此处所见不同(我敢肯定会有),那也没有问题。
如果将这些结果与之前的结果进行比较,你会发现潜在表示并没有太大变化,且重构输出与给定输入完全不匹配。显然,模型记住了它所训练的输入。当我们计算均方误差 (MSE) 并得到 0.84 时,可以看出这个值相比之前获得的要大得多。
解决方法当然是增加更多数据。但这仅是构建自编码器的玩具示例。从此之后,真正变化的是数据的类型和数量、层数以及层的类型。在下一节中,我们将探讨简单自编码器在降维问题中的应用。
在降维和可视化中的应用
自编码器的一些最有趣的应用之一是降维 [Wang, Y., 等 (2016)]。鉴于我们生活在一个数据存储易于访问且价格合理的时代,当前到处都有大量数据被存储。然而,并非所有数据都是相关的信息。举个例子,考虑一个始终朝一个方向的家庭安防摄像头的视频录制数据库。可以想象,每一帧视频或图像中有大量重复数据,且收集的数据中只有极少部分是有用的。我们需要一种策略来查看这些图像中真正重要的部分。图像本身具有大量冗余信息,且图像区域之间通常存在相关性,这使得自编码器在压缩图像信息时非常有用 (Petscharnig, S., 等 (2017))。
为了展示自编码器在图像降维中的适用性,我们将使用著名的 MNIST 数据集。
MNIST 数据准备
有关 MNIST 的详细信息,请参阅第三章,准备数据。在这里,我们只提到 MNIST 数据将被缩放到范围[0, 1]。我们还需要通过将 28 x 28 的数字图像重塑为 784 维的向量来将所有图像转换为向量。可以通过以下方式实现:
from tensorflow.keras.datasets import mnist
(x_train, y_train), (x_test, y_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), 28*28))
x_test = x_test.reshape((len(x_test), 28*28))
我们将使用 x_train
来训练自编码器,并使用 x_test
来测试自编码器对 MNIST 数字的编码和解码泛化能力。出于可视化目的,我们将需要 y_test
,但由于我们不需要在无监督学习中使用标签,y_train
可以忽略。
图 7.4 描述了 x_test
中的前八个样本。这些样本将在多个实验中用于展示不同自编码器模型的能力:
图 7.4 – 用于比较的测试 MNIST 数字
MNIST 的自编码器
我们可以设计几个不同层数的实验,以观察自编码器在 MNIST 上的性能变化。我们可以从一个四层的自编码器开始,始终使用一个二的潜在维度。这样做是为了方便在自编码器引导的二维空间中可视化 MNIST 数字。
基于之前定义的自编码器,我们可以提出以下四层基础自编码器:
inpt_dim = 28*28
ltnt_dim = 2
inpt_vec = Input(shape=(inpt_dim,))
elayer1 = Dense(392, activation='sigmoid')(inpt_vec)
elayer2 = Dense(28, activation='sigmoid') (elayer1)
elayer3 = Dense(10, activation='sigmoid') (elayer2)
encoder = Dense(ltnt_dim, activation='tanh')(elayer3)
dlayer1 = Dense(10, activation='sigmoid')(encoder)
dlayer2 = Dense(28, activation='sigmoid')(dlayer1)
dlayer3 = Dense(392, activation='sigmoid')(dlayer2)
decoder = Dense(inpt_dim, activation='sigmoid')(dlayer3)
latent_ncdr = Model(inpt_vec, encoder)
autoencoder = Model(inpt_vec, decoder)
autoencoder.compile(loss='binary_crossentropy', optimizer='adam')
hist = autoencoder.fit(x_train, x_train, epochs=100, batch_size=256,
shuffle=True, validation_data=(x_test, x_test))
这将是后续模型的基础。这里有一些新的内容需要特别强调,并需要适当介绍。首先要介绍的一个重要内容是一个新的激活函数,叫做双曲正切。这个激活函数的定义如下:
相应的第一导数相对简单:
除了拥有一个漂亮且易于计算的导数外,双曲正切激活函数还具有一个良好的输出范围[-1, 1]。这提供了一个中立的范围,不必局限于 sigmoid 范围[0, 1]。为了可视化的目的,有时在双曲正切范围内可视化是很有趣的,但并非必须这么做。
我们引入的另一个新元素是称为二元交叉熵的损失函数:
通常,二元交叉熵使用信息,这是理论上的一种方法,用来计算目标数据 和重构(或预测)数据
之间的误差。从某种意义上说,它衡量了目标和预测之间的熵或惊讶量。例如,在理想的自编码器中,目标
与其重构
相等并不令人惊讶,损失应该为零。然而,如果目标
不等于
,这将是令人惊讶的,并且会产生较大的损失。
关于使用交叉熵损失的自编码器的更完整讨论,请参见(Creswell, A., et. al. (2017))。
还引入了一种新的优化器,叫做Adam(Kingma, D. P., et. al. (2014))。它是一种用于随机优化的算法,使用自适应学习率,在某些深度学习应用中已被证明非常快速。当我们处理深度学习模型和大数据集时,速度是一个重要的特性。时间至关重要,而 Adam 提供了一种非常有效的方法,已经变得相当流行。
最后,我们添加的新内容是在fit()
方法中。你应该已经注意到有两个新的参数:shuffle=True
,它允许在训练过程中对数据进行洗牌;以及validation_data=( , )
,它指定了一个数据元组,用于监控使用验证数据的损失,验证数据是模型从未见过的,也不会用于训练。
这就是我们所介绍的所有新内容。下一步将解释我们在实验中尝试的自编码器架构。请参见图 7.5,以作为我们将进行的实验的参考:
图 7.5 – 不同的自编码器配置,展示潜在表示质量的差异
在图中,你会注意到我们使用的是自编码器的抽象表示,而图 7.5的右侧展示了每个自编码器架构将使用的不同层。第一个架构对应于本节中展示的代码。也就是说,代码展示了一个自编码器,其中编码层分别有 392、28、10 和 2 个神经元;解码层则分别有 10、28、392 和 784 个神经元。右侧的下一个模型包含相同的层,只是删除了对应于 392 个神经元的那一对层,依此类推。
最后一个自编码器模型仅包含两层,一层编码(两个神经元)和一层解码(784 个神经元)。此时,你应该能够修改 Python 代码,删除必要的层,并复制图 7.5中展示的模型。下一步是训练图 7.5中的模型,并可视化输出质量。
训练与可视化
执行autoencoder.fit()
进行 100 轮训练,生成一个可行的模型,能够轻松地按要求将数据编码为二维。仔细观察训练过程中的损失函数,我们可以看到它已经正确收敛:
图 7.6 – 四层自编码器训练过程中损失函数的监控
一旦模型成功训练完成,我们可以使用以下方法来提取编码后的表示:
encdd = latent_ncdr.predict(x_test)
我们使用的是测试集x_test
。此编码将按要求压缩为二维,并生成一个范围为[-1, 1]的潜在表示。类似地,我们总是可以拿测试集,使用自编码器对其进行压缩和重构,看看输入和重构结果有多相似。我们可以用以下方式进行:
x_hat = autoencoder.predict(x_test)
在我们研究 MNIST 学到的潜在表示之前,可以先看看重建质量,作为评估学习模型质量的一种方式。图 7.7 显示了使用图 7.4作为参考的每个模型输入的重建结果(在x_hat
中)。该图分为四部分,每一部分对应图 7.5中描述的模型:a) 八层模型,b) 六层模型,c) 四层模型,d) 两层模型:
图 7.7 – 图 7.5 中模型的自编码器重建:a) 八层模型,b) 六层模型,c) 四层模型,d) 两层模型
从图 7.7.a中,我们可以看到,具有八层(392、28、10、2、10、28、392、784)的模型在重建结果上表现普遍良好,除了数字 4 和 9。显然,这两个数字在视觉上非常相似,因此自编码器很难清楚地区分这两个数字。为了进一步探索这一观察,我们可以将测试数据在潜在空间中可视化(在encdd
中),如图 7.8所示:
图 7.8 – 使用 MNIST 测试数据的四层编码器
在自编码器生成的潜在空间中,数字四和九之间的重叠非常明显。然而,大多数其他数字组之间有相对清晰的独立簇。图 7.8 也解释了其他相似数字之间的自然接近性;例如,一和七看起来彼此接近,零和六也是如此,三和八也如此。然而,那些看起来不相似的数字则位于潜在空间的对立部分——例如,零和一。
图 7.9 描述了移除了 392 个神经元层的三层自编码器,保留了 28、10、2 神经元的架构。显然,潜在空间的质量显著降低,尽管一些主要结构保持一致。也就是说,零和一位于对立面,其他看起来相似的数字更为接近;与图 7.8相比,重叠度更大。这个三层自编码器的质量持续较低,如图 7.7.b所示:
图 7.9 – 使用 MNIST 测试数据的三层编码器
在图 7.10中,我们可以观察到具有 10 和 2 个神经元的两层自编码器的结果,与之前的自编码器相比,数字重叠度更大;这一点在图 7.7.c中表现得尤为明显,重建效果较差:
图 7.10 – 使用 MNIST 测试数据的两层编码器
最后,图 7.11 显示了单层自编码器的潜在空间。显然,这是一个糟糕的主意。只要考虑我们要求自编码器做什么:我们要求仅仅两个神经元找到一种方式来查看数字的整个图像,并找到一种方法(学习权重 ![])将所有图像映射到二维空间。这根本不可能做到。从逻辑上讲,如果我们只有一层,我们至少需要 10 个神经元才能充分建模 MNIST 中的 10 个数字:
图 7.11 – 使用 MNIST 测试数据的单层编码器 – 一个糟糕的主意
对 图 7.11 的仔细观察也表明,轴的尺度变化略微不同;这可以解释为编码器无法将所有 MNIST 数字分离到潜在空间的不同区域。实际上,除非输入空间的维度已经非常低,否则请不要使用层数和神经元很少的自编码器。正如本实验所示,自编码器在深层配置中可能更成功。在下一章中将学习更多关于深度自编码器的内容。
无监督学习的伦理影响
无监督学习,比如我们在之前探讨的自编码器中看到的,并不是魔法。它是成熟的,并且有非常严格的边界,这些边界是已知且预先定义的。它没有能力学习数据所给定限制之外的新事物。记住,正如本章引言部分所解释的,无监督学习是被动学习。
然而,即使是最强大的无监督学习模型,也存在伦理风险。一个主要的问题是,它们在处理异常值或可能包含边缘案例的数据时会造成困难。例如,假设有大量关于 IT 招聘的数据,其中包含候选人的工作经验、当前薪资以及掌握的编程语言。如果这些数据大部分都是关于具有相同编程语言经验的候选人,只有少数人懂 Python,那么那些懂 Python 的候选人可能会被放置到一个难以清晰可视化的边界或区域,因为模型已经学会了,Python 作为一种不常见的语言,可能在数据压缩、降维或数据可视化方面不相关。此外,想象一下,5 年后,即使出现了在 5 年前训练时未知的编程语言,你仍然使用那个模型。这时模型可能无法正确映射这些信息,以供可视化或数据压缩应用使用。
你必须非常小心选择用于训练自编码器的数据,且拥有多样化的案例对于任何模型的可靠性都至关重要。如果数据缺乏多样性,自编码器将倾向于只从一个输入空间中学习。假设你在先前提到的 10 个 MNIST 数字的图像上训练自编码器——你不会期望自编码器在猫的图像上表现良好;那样做是错误的,并且很可能产生不想要的结果。例如,当使用人的图像时,你必须确保训练数据中有足够的多样性和广泛性,以便进行适当的训练,并且模型能够对未包含在训练数据中的人类图像做出正确的反应。
总结
本章展示了自编码器是非常简单的模型,可以用于对数据进行编码和解码,应用于不同的目的,比如数据压缩、数据可视化,以及仅保留重要特征的潜在空间的发现。我们展示了自编码器中神经元的数量和层数对模型成功的重要性。更深(更多层)和更宽(更多神经元)的特征通常是好模型的关键,尽管这可能导致训练时间变慢。
在这一点上,你应该知道监督学习和无监督学习在被动学习方面的区别。你也应该能够熟练地实现自编码器的两个基本组成部分:编码器和解码器。同样,你应该能够修改自编码器的架构,微调它以实现更好的性能。以本章中讨论的例子为例,你应该能够将自编码器应用于降维问题或数据可视化问题。此外,你还应该考虑使用无监督学习算法时与训练数据相关的风险和责任。
第八章,深度自编码器,将继续介绍比本章所涉及的更深更宽的自编码器架构。下一章将介绍深度置信网络的概念及其在深度无监督学习中的重要性。通过介绍深度自编码器并与浅层自编码器进行对比,来解释这些概念。该章还将提供关于优化神经元数量和层数以最大化性能的重要建议。
问题与答案
- 过拟合对自编码器来说是坏事吗?
其实不是。你希望自编码器出现过拟合!也就是说,你希望它能够精确地在输出中重现输入数据。然而,这里有一个警告。你的数据集必须相对于模型的大小足够大;否则,数据的记忆将会阻碍模型在未见过的数据上的泛化能力。
- 为什么我们在编码器的最后一层使用了两个神经元?
仅用于可视化目的。由两个神经元产生的二维潜在空间使我们能够轻松地在潜在空间中可视化数据。在下一章中,我们将使用其他配置,这些配置不一定具有二维潜在空间。
- 自编码器到底有什么酷的地方?
它们是简单的神经网络模型,无需教师进行学习(无监督学习)。它们不会偏向于学习特定的标签(类别)。通过迭代观察,它们学习数据的世界,旨在学习最具代表性和相关性的特征。它们可以用作特征提取模型,但我们将在未来的章节中进一步讨论这一点。
参考文献
-
Amari, S. I. (1993). 反向传播与随机梯度下降方法。Neurocomputing, 5(4-5), 185-196.
-
Wang, Y., Yao, H., & Zhao, S. (2016). 基于自编码器的降维方法。Neurocomputing, 184, 232-242.
-
Petscharnig, S., Lux, M., & Chatzichristofis, S. (2017). 使用深度学习和自编码器进行图像特征的降维。发表于第十五届国际基于内容的多媒体索引研讨会(第 23 页)。ACM。
-
Creswell, A., Arulkumaran, K., & Bharath, A. A. (2017). 关于通过最小化二元交叉熵训练去噪自编码器的研究。arXiv 预印本 arXiv:1708.08487.
-
Kingma, D. P., & Ba, J. (2014). Adam:一种随机优化方法。arXiv 预印本 arXiv:1412.6980.
深度自编码器
本章介绍了深度信念网络的概念及这种深度无监督学习方式的重要性。通过引入深度自编码器以及两种有助于创建稳健模型的正则化技术,来解释这些概念。这些正则化技术——批量归一化和丢弃法,已被证明能促进深度模型的学习,并广泛应用。我们将在 MNIST 数据集和一个更具挑战性的彩色图像数据集 CIFAR-10 上展示深度自编码器的强大能力。
在本章结束时,你将能够体会到构建深度信念网络的好处,观察到它们在建模和输出质量方面的优势。你将能够实现自己的深度自编码器,并证明对于大多数任务,深层模型比浅层模型更优。你将熟悉批量归一化和丢弃法策略,以优化模型并最大化性能。
本章结构如下:
-
介绍深度信念网络
-
构建深度自编码器
-
使用深度自编码器探索潜在空间
第十一章:介绍深度信念网络
在机器学习中,有一个领域在讨论深度学习(DL)时常常被提及,叫做深度信念网络(DBNs)(Sutskever, I. 和 Hinton, G. E. (2008))。一般而言,这个术语也用于指基于图的机器学习模型,例如著名的限制玻尔兹曼机。然而,DBNs 通常被视为深度学习家族的一部分,其中深度自编码器是该家族中最为突出的成员之一。
深度自编码器被视为 DBNs 的一种形式,因为在前向传播过程中,有些潜在变量仅对单层可见。这些层的数量通常比单层自编码器要多。深度学习(DL)和 DBNs 的一项核心原则是,在学习过程中,不同层次之间代表着不同的知识。这些知识表示是通过特征学习获得的,并且没有偏向特定的类别或标签。此外,研究表明,这种知识表现出层次结构。例如,考虑图像;通常,靠近输入层的层学习的是低阶特征(如边缘),而更深的层学习的是高阶特征,即明确的形状、模式或物体(Sainath, T. N. 等人(2012))。
在深度信念网络(DBN)中,与大多数深度学习模型一样,特征空间的可解释性可能很困难。通常,查看第一层的权重可以提供有关学习的特征和/或特征图外观的信息;然而,由于更深层中的高非线性,特征图的可解释性一直是一个问题,需要谨慎考虑(Wu, K., 等人(2016))。尽管如此,DBN 在特征学习方面仍然表现出色。在接下来的几节中,我们将介绍在高度复杂数据集上的更深层版本的自编码器。我们将引入几种新型的层,以展示模型可以有多深。
构建深度自编码器
只要自编码器有不止一对层(一个编码层和一个解码层),它就可以被称为深度自编码器。在自编码器中堆叠层是提高其特征学习能力的好策略,能够找到在分类或回归应用中具有高度判别性的独特潜在空间。然而,在第七章《自编码器》中,我们已经介绍了如何在自编码器上堆叠层,我们将再次这样做,但这次我们将使用一些新的层类型,这些层超出了我们之前使用的全连接层。它们是批量归一化和丢弃层。
这些层中没有神经元;然而,它们作为具有非常具体目的的机制,在学习过程中发挥作用,通过防止过拟合或减少数值不稳定性,能够带来更成功的结果。让我们讨论一下这些层,然后我们将在几个重要的数据集上继续实验这两种层。
批量归一化
批量归一化自 2015 年被引入深度学习(Ioffe, S., 和 Szegedy, C. (2015))以来,已经成为深度学习的一个重要组成部分。它是一项重大变革,因为它具有几个优点:
-
它可以防止被称为消失梯度或爆炸梯度的问题,这在递归网络中非常常见(Hochreiter, S. (1998))。
-
它可以通过充当学习模型的正则化器,从而加快训练过程(Van Laarhoven, T. (2017))。
这些属性的总结以及我们将用于表示批量归一化的块图像显示在图 8.1中:
图 8.1 – 批量归一化层的主要属性
被数据科学家称为批量归一化的机制,通过为梯度计算提供稳定性,并调整它们如何影响神经网络不同层权重的更新,从而加速了训练或模型的收敛。这是因为它可以防止梯度消失或爆炸,这是基于梯度的优化在深度学习模型中的自然结果。也就是说,模型越深,梯度如何影响更深层的层和单个单位,可能会导致大幅度更新或非常小的更新,这可能导致变量溢出或数值为零。
如图 8.2顶部所示,批量归一化具有通过归一化输入数据来调节输入数据边界的能力,从而使输出遵循正态分布。图底部说明了批量归一化应用的位置,即在神经元内部,在将输出发送到下一层之前:
图 8.2 – 简单自编码器的批量归一化
考虑有一个(小)批次的数据,,大小为
,这允许我们定义以下方程式。首先,批次的均值,在层
上,可以按如下方式计算:
相应的标准差可以按如下方式计算:
。
然后,我们可以按如下方式归一化层中的每个单位
:
这里,是为了数值稳定性引入的常数,但可以根据需要进行调整。最后,每个单位
在层
的归一化神经输出,
,可以在进入激活函数之前按如下方式计算:
这里,和
是每个神经单位需要学习的参数。之后,层
中单位
的任何激活函数选择都会接收到归一化的输入,![],并产生一个输出,该输出经过最佳归一化,以最小化损失函数。
查看收益的一种简单方法是想象归一化过程:虽然它发生在每个单元上,但学习过程本身决定了所需的最佳归一化方式,以最大化模型的性能(最小化损失)。因此,它能够在某些特征或潜在空间中,如果归一化不必要,就消除归一化的效果,或者它也可以利用归一化的效果。需要记住的一个重要点是,当使用批量归一化时,学习算法会学会如何最优化地使用归一化。
我们可以使用tensorflow.keras.layers.BatchNormalization
来创建一个批量归一化层,如下所示:
from tensorflow.keras.layers import BatchNormalization
...
bn_layer = BatchNormalization()(prev_layer)
...
这显然是使用函数式编程范式完成的。考虑以下示例,它是一个关于电影评论的数据集,名为IMDb(Maas, A. L., 等人,2011),我们将在第十三章《循环神经网络》中详细解释。在这个例子中,我们只是尝试证明添加批量归一化层与不添加的效果。仔细看看下面的代码片段:
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Dense, Activation, Input
from tensorflow.keras.layers import BatchNormalization
from keras.datasets import imdb
from keras.preprocessing import sequence
import numpy as np
inpt_dim = 512 #input dimensions
ltnt_dim = 256 #latent dimensions
# -- the explanation for this will come later --
(x_train, y_train), (x_test, y_test) = imdb.load_data()
x_train = sequence.pad_sequences(x_train, maxlen=inpt_dim)
x_test = sequence.pad_sequences(x_test, maxlen=inpt_dim)
# ----------------------------------------------
接下来我们继续构建模型:
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
# model with batch norm
inpt_vec = Input(shape=(inpt_dim,))
el1 = Dense(ltnt_dim)(inpt_vec) #dense layer followed by
el2 = BatchNormalization()(el1) #batch norm
encoder = Activation('sigmoid')(el2)
decoder = Dense(inpt_dim, activation='sigmoid') (encoder)
autoencoder = Model(inpt_vec, decoder)
# compile and train model with bn
autoencoder.compile(loss='binary_crossentropy', optimizer='adam')
autoencoder.fit(x_train, x_train, epochs=20, batch_size=64,
shuffle=True, validation_data=(x_test, x_test))
在这段代码中,批量归一化层被放置在激活层之前。因此,它将归一化输入到激活函数,在本例中是sigmoid
。类似地,我们也可以构建一个不带批量归一化层的相同模型,如下所示:
# model without batch normalization
inpt_vec = Input(shape=(inpt_dim,))
el1 = Dense(ltnt_dim)(inpt_vec) #no batch norm after this
encoder = Activation('sigmoid')(el1)
latent_ncdr = Model(inpt_vec, encoder)
decoder = Dense(inpt_dim, activation='sigmoid') (encoder)
autoencoder = Model(inpt_vec, decoder)
# compile and train model with bn
autoencoder.compile(loss='binary_crossentropy', optimizer='adam')
autoencoder.fit(x_train, x_train, epochs=20, batch_size=64,
shuffle=True, validation_data=(x_test, x_test))
如果我们训练这两个模型,并绘制它们在最小化损失函数时的表现,我们会很快注意到,使用批量归一化会带来显著的效果,如图 8.3所示:
图 8.3 – 带有和不带批量归一化的学习进度比较
图表表明,使用批量归一化的效果是在训练集和验证集的损失函数都减少。这些结果与许多你可以自己尝试的实验是一致的!然而,正如我们之前所说的,这并不一定意味着每次都会发生这种情况。这是一种相对现代的技术,迄今为止已证明它能正常工作,但这并不意味着它对我们已知的所有情况都有效。
我们强烈建议在所有模型中,首先尝试使用没有批量归一化的模型来解决问题,然后在你对现有性能感到满意时,再回来使用批量归一化,看看是否能略微提升性能和训练速度。
假设你尝试了批量归一化,并且得到了性能、速度或两者的提升,但现在你发现模型一直在过拟合。别担心!还有一种有趣且新颖的技术,叫做随机失活。正如我们在接下来的部分所讨论的,它可以为模型提供一种减少过拟合的替代方法。
随机失活
Dropout 是一种于 2014 年发布的技术,并在发布后不久迅速流行开来(Srivastava, N., Hinton, G., 等(2014))。它作为一种应对过拟合的替代方法,而过拟合正是它的主要特性之一,具体可以总结如下:
-
它可以减少过拟合的机会。
-
它可以提高模型的泛化能力。
-
它可以减少主导神经元的影响。
-
它可以促进神经元的多样性。
-
它可以促进更好的神经元协作。
我们将在 Dropout 中使用的块状图像及其主要特性如图 8.4所示:
图 8.4 – Dropout 层特性
Dropout 策略之所以有效,是因为它通过断开表示某些假设(或模型)的特定神经元,使网络能够寻找替代假设来解决问题。用一个简单的方式来看待这种策略:假设你有一群专家负责判断一张图片中是否包含猫或椅子。可能有大量专家认为图片中有椅子,但只需要有一个专家特别大声、非常确信图片中有猫,就足以说服决策者去听这个特别大声的专家,而忽视其他专家。在这个类比中,专家就是神经元。
可能有一些神经元特别确信(有时由于对无关特征的过拟合,错误地确信)某个事实,而它们的输出值与该层中其他神经元相比特别高,甚至以至于更深层的网络学会更多地依赖这一层,从而导致更深层的过拟合。Dropout是一个机制,它会选择该层中的一部分神经元,并将它们完全从该层断开,使得没有输入流入这些神经元,也没有输出从这些神经元流出,如图 8.5所示:
图 8.5 – Dropout 机制在第一个隐藏层上的应用。这里的 Dropout 断开了一个神经元与该层的连接
在前面的示意图中,第一个隐藏层的 Dropout 率为三分之一。这意味着,完全随机地,三分之一的神经元将会被断开。图 8.5展示了当第一个隐藏层的第二个神经元被断开时的情况:没有来自输入层的输入进入,也没有输出从它那里出来。模型完全不知道它的存在;从实际操作的角度来看,这就像是一个不同的神经网络!
然而,被断开的神经元仅在一次训练步骤中断开:它们的权重在一次训练步骤内保持不变,而所有其他权重会被更新。这有一些有趣的影响:
-
由于神经元的随机选择,那些倾向于主导(过拟合)某些特征的麻烦制造者最终会被选中,而其余的神经元将学会在没有这些麻烦制造者的情况下处理特征空间。这有助于防止和减少过拟合,同时促进不同神经元之间的协作,它们在不同领域具有专业知识。
-
由于神经元的持续忽略/断开连接,网络有可能从根本上发生变化——几乎就像我们在每一步训练中都在训练多个神经网络,而实际上并不需要创建许多不同的模型。这一切都是由于 dropout 的原因。
通常建议在更深的网络中使用 dropout,以改善深度学习中常见的过拟合问题。
为了展示使用 dropout 时性能的差异,我们将使用与上一节相同的数据集,但我们将在自编码器中添加一个额外的层,如下所示:
from tensorflow.keras.layers import Dropout
...
# encoder with dropout
inpt_vec = Input(shape=(inpt_dim,))
el1 = Dense(inpt_dim/2)(inpt_vec)
在这段代码中,dropout 率为 10%,意味着在训练过程中,e14
密集层中 10% 的神经元会被随机断开连接多次。
el2 = Activation('relu')(el1)
el3 = Dropout(0.1)(el2)
el4 = Dense(ltnt_dim)(el3)
encoder = Activation('relu')(el4)
解码器与之前完全相同,基线模型仅不包含 dropout 层:
# without dropout
inpt_vec = Input(shape=(inpt_dim,))
el1 = Dense(inpt_dim/2)(inpt_vec)
el2 = Activation('relu')(el1)
el3 = Dense(ltnt_dim)(el2)
encoder = Activation('relu')(el3)
如果我们选择'adagrad'
并在 100 个 epoch 上进行训练并比较性能结果,我们可以获得图 8.6中所示的性能:
图 8.6 – 比较带有 dropout 和不带 dropout 的模型的自编码器重建损失
以下是完整代码:
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Dense, Activation, Input
from tensorflow.keras.layers import Dropout
from keras.datasets import imdb
from keras.preprocessing import sequence
import numpy as np
import matplotlib.pyplot as plt
inpt_dim = 512
ltnt_dim = 128
(x_train, y_train), (x_test, y_test) = imdb.load_data()
x_train = sequence.pad_sequences(x_train, maxlen=inpt_dim)
x_test = sequence.pad_sequences(x_test, maxlen=inpt_dim)
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
然后我们像这样定义带有 dropout 的模型:
# with dropout
inpt_vec = Input(shape=(inpt_dim,))
el1 = Dense(inpt_dim/2)(inpt_vec)
el2 = Activation('relu')(el1)
el3 = Dropout(0.1)(el2)
el4 = Dense(ltnt_dim)(el3)
encoder = Activation('relu')(el4)
# model that takes input and encodes it into the latent space
latent_ncdr = Model(inpt_vec, encoder)
decoder = Dense(inpt_dim, activation='relu') (encoder)
# model that takes input, encodes it, and decodes it
autoencoder = Model(inpt_vec, decoder)
然后我们对其进行编译、训练,存储训练历史记录,并清除变量以便重新使用,如下所示:
autoencoder.compile(loss='binary_crossentropy', optimizer='adagrad')
hist = autoencoder.fit(x_train, x_train, epochs=100, batch_size=64,
shuffle=True, validation_data=(x_test, x_test))
bn_loss = hist.history['loss']
bn_val_loss = hist.history['val_loss']
del autoencoder
del hist
然后我们对一个不带 dropout 的模型进行相同的操作:
# now without dropout
inpt_vec = Input(shape=(inpt_dim,))
el1 = Dense(inpt_dim/2)(inpt_vec)
el2 = Activation('relu')(el1)
el3 = Dense(ltnt_dim)(el2)
encoder = Activation('relu')(el3)
# model that takes input and encodes it into the latent space
latent_ncdr = Model(inpt_vec, encoder)
decoder = Dense(inpt_dim, activation='relu') (encoder)
# model that takes input, encodes it, and decodes it
autoencoder = Model(inpt_vec, decoder)
autoencoder.compile(loss='binary_crossentropy', optimizer='adagrad')
hist = autoencoder.fit(x_train, x_train, epochs=100, batch_size=64,
shuffle=True, validation_data=(x_test, x_test))
接下来,我们收集训练数据并像这样绘制它:
loss = hist.history['loss']
val_loss = hist.history['val_loss']
fig = plt.figure(figsize=(10,6))
plt.plot(bn_loss, color='#785ef0')
plt.plot(bn_val_loss, color='#dc267f')
plt.plot(loss, '--', color='#648fff')
plt.plot(val_loss, '--', color='#fe6100')
plt.title('Model reconstruction loss')
plt.ylabel('Binary Cross-Entropy Loss')
plt.xlabel('Epoch')
plt.legend(['With Drop Out - Training',
'With Drop Out - Validation',
'Without Drop Out - Training',
'Without Drop Out - Validation'], loc='upper right')
plt.show()
从图 8.6中我们可以看到,带有 dropout 的模型表现优于不带 dropout 的模型。这表明,训练时没有使用 dropout 更容易发生过拟合,原因是当不使用 dropout 时,验证集上的学习曲线较差。
如前所述,adagrad
优化器已被选择用于此任务。我们做出这个决定是因为你应该逐步学习更多的优化器。Adagrad 是一种自适应算法;它根据特征的频率来进行更新(Duchi, J. 等人,2011)。如果某个特征出现频率较高,更新就会较小,而对于那些不常见的特征,则会进行较大的更新。
当数据集稀疏时,建议使用 Adagrad。例如,在像本例中这样的词嵌入任务中,频繁出现的词会导致小的更新,而稀有词则需要较大的更新。
最后,需要提到的是,Dropout(rate)
属于tf.keras.layers.Dropout
类。作为参数传入的 rate 值对应着每次训练步骤中该层的神经元将随机断开的比率。
推荐使用介于0.1 和 0.5之间的 dropout 率,以实现对网络性能的显著改进。并且建议仅在深度网络中使用 dropout。不过,这些是经验性的发现,您需要通过自己的实验来验证。
现在我们已经解释了这两个相对较新的概念——dropout(丢弃法)和 batch normalization(批量归一化),接下来我们将创建一个相对简单但强大的深度自编码器网络,用于发现不偏向特定标签的潜在表示。
使用深度自编码器探索潜在空间
潜在空间,正如我们在第七章 自编码器中所定义的那样,在深度学习中非常重要,因为它们可以导致基于假设丰富潜在表示的强大决策系统。而且,正是由于自编码器(和其他无监督模型)产生的潜在空间不偏向特定标签,使得它们在表示上非常丰富。
在第七章 自编码器中,我们探讨了 MNIST 数据集,这是深度学习中的标准数据集,并展示了通过仅使用四个密集层的编码器和整个自编码器模型的八层,我们能够轻松地找到非常好的潜在表示。在下一节中,我们将处理一个更为复杂的数据集——CIFAR-10,之后我们会回到探索IMDB
数据集的潜在表示,该数据集我们在本章前面部分已经简要探讨过。
CIFAR-10
2009 年,加拿大高级研究院(CIFAR)发布了一个非常大的图像集合,可以用来训练深度学习模型识别各种物体。我们将在本例中使用的这个数据集被广泛称为 CIFAR-10,因为它仅包含 10 个类别,总共有 60,000 张图像;图 8.7展示了每个类别的样本:
图 8.7 – 来自 CIFAR-10 数据集的样本图像。数字表示每个类别分配的数值,方便起见。
数据集中的每张图像为 32x32 像素,使用 3 个维度来跟踪颜色细节。从图中可以看到,这些小图像包含了标签之外的其他物体,如文本、背景、结构、景观以及其他部分遮挡的物体,同时保留了前景中的主要兴趣物体。这使得它比 MNIST 更具挑战性,因为 MNIST 的背景始终是黑色的,图像是灰度的,每张图像中只有一个数字。如果你从未接触过计算机视觉应用,可能不知道与 MNIST 相比,处理 CIFAR-10 要复杂得多。因此,我们的模型需要比 MNIST 模型更具鲁棒性和深度。
在 TensorFlow 和 Keras 中,我们可以使用以下代码轻松加载和准备我们的数据集:
import numpy as np
from tensorflow.keras.datasets import cifar10
(x_train, y_train), (x_test, y_test) = cifar10.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:])))
print('x_train shape is:', x_train.shape)
print('x_test shape is:', x_test.shape)
上述代码输出如下:
x_train shape is: (50000, 3072)
x_test shape is: (10000, 3072)
这表示我们有六分之一的数据显示为测试数据(约 16%),其余的用于训练。3,072 维来自于像素和通道的数量:。上述代码还将数据从[0, 255]的范围归一化到[0.0, 1.0]的浮动数值。
为了继续我们的示例,我们将提出一个如图 8.8所示架构的深度自编码器,它将接受一个 3,072 维的输入,并将其编码为 64 维:
图 8.8 – CIFAR-10 数据集上深度自编码器的架构
该架构在编码器中使用了 17 层,在解码器中使用了 15 层。图中的密集层对应块内写有神经元的数量。可以看到,该模型在编码输入数据的过程中实现了一系列战略性的批量归一化和丢弃层策略。在此示例中,所有丢弃层的丢弃率为 20%。
如果我们使用标准的adam
优化器和标准的二元交叉熵损失函数训练模型 200 个 epoch,我们可以获得如图 8.9所示的训练性能:
图 8.9 – 深度自编码器模型在 CIFAR-10 上的损失重建
以下是完整的代码:
from tensorflow import keras
from tensorflow.keras.datasets import cifar10
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Dense, Dropout, Activation, Input
from tensorflow.keras.layers import BatchNormalization import matplotlib.pyplot as plt
import numpy as np
inpt_dim = 32*32*3
ltnt_dim = 64
# The data, split between train and test sets:
(x_train, y_train), (x_test, y_test) = cifar10.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:])))
print('x_train shape:', x_train.shape)
print('x_test shape:', x_test.shape)
我们将模型定义如下:
inpt_vec = Input(shape=(inpt_dim,))
el1 = Dense(2048)(inpt_vec)
el2 = Activation('relu')(el1)
el3 = Dense(1024)(el2)
el4 = BatchNormalization()(el3)
el5 = Activation('relu')(el4)
el6 = Dropout(0.2)(el5)
el7 = Dense(512)(el6)
el8 = Activation('relu')(el7)
el9 = Dense(256)(el8)
el10 = BatchNormalization()(el9)
el11 = Activation('relu')(el10)
el12 = Dropout(0.2)(el11)
el13 = Dense(128)(el12)
el14 = Activation('relu')(el13)
el15 = Dropout(0.2)(el14)
el16 = Dense(ltnt_dim)(el15)
el17 = BatchNormalization()(el16)
encoder = Activation('tanh')(el17)
# model that takes input and encodes it into the latent space
latent_ncdr = Model(inpt_vec, encoder)
接下来我们将模型的解码器部分定义如下:
dl1 = Dense(128)(encoder)
dl2 = BatchNormalization()(dl1)
dl3 = Activation('relu')(dl2)
dl4 = Dropout(0.2)(dl3)
dl5 = Dense(256)(dl4)
dl6 = Activation('relu')(dl5)
dl7 = Dense(512)(dl6)
dl8 = BatchNormalization()(dl7)
dl9 = Activation('relu')(dl8)
dl10 = Dropout(0.2)(dl9)
dl11 = Dense(1024)(dl10)
dl12 = Activation('relu')(dl11)
dl13 = Dense(2048)(dl12)
dl14 = BatchNormalization()(dl13)
dl15 = Activation('relu')(dl14)
decoder = Dense(inpt_dim, activation='sigmoid') (dl15)
我们将它们组合成一个自编码器模型,进行编译并像这样训练:
# model that takes input, encodes it, and decodes it
autoencoder = Model(inpt_vec, decoder)
# setup RMSprop optimizer
opt = keras.optimizers.RMSprop(learning_rate=0.0001, decay=1e-6, )
autoencoder.compile(loss='binary_crossentropy', optimizer=opt)
hist = autoencoder.fit(x_train, x_train, epochs=200, batch_size=10000,
shuffle=True, validation_data=(x_test, x_test))
# and now se visualize the results
fig = plt.figure(figsize=(10,6))
plt.plot(hist.history['loss'], color='#785ef0')
plt.plot(hist.history['val_loss'], color='#dc267f')
plt.title('Model reconstruction loss')
plt.ylabel('Binary Cross-Entropy Loss')
plt.xlabel('Epoch')
plt.legend(['Training Set', 'Test Set'], loc='upper right')
plt.show()
如图 8.9所示的模型性能很好地收敛,训练集和测试集上的损失都在减少,这意味着模型没有过拟合,并且随着时间的推移,继续适当调整权重。为了可视化模型在未见数据(测试集)上的表现,我们可以简单地随机选择测试集中的样本,如图 8.10中的样本,这些样本产生的输出如图 8.11所示:
图 8.10 – CIFAR-10 测试集中的样本输入
图 8.11 – 来自图 8.10 中样本的输出(重构)
从图 8.11中可以看到,重构正确地处理了输入数据的色谱。然而,很明显,问题在重构方面比 MNIST 更难。形状模糊,尽管它们似乎位于正确的空间位置。重构中显然缺少一些细节。我们可以让自编码器更深,或者训练更长时间,但问题可能并没有得到正确解决。我们可以通过一个事实来解释这个性能,那就是我们故意选择了一个大小为 64 的潜在表示,这个大小甚至小于一个只有 5×5 像素的图像:。如果你仔细想一想,反思一下,就会明白这是几乎不可能的,因为从 3,072 到 64 意味着压缩了 2.08%!
解决这个问题的方法不是让模型变得更大,而是要认识到潜在表示的大小可能不足以捕捉输入的相关细节,从而进行良好的重构。当前的模型可能在降低特征空间的维度时过于激进。如果我们使用 UMAP 将 64 维的潜在向量可视化为 2 维,我们会得到如图 8.12所示的图形:
图 8.12 – 测试集中的潜在向量的 UMAP 二维表示
我们之前没有提到 UMAP,但我们简要说明一下,UMAP 是最近提出的一种开创性数据可视化工具,正在开始引起关注(McInnes, L., 等人(2018))。在我们的案例中,我们仅仅使用 UMAP 来可视化数据分布,因为我们并没有让自编码器将数据编码到二维。图 8.12表明,类的分布没有足够清晰的定义,无法让我们观察到分离或明显的聚类。这确认了深度自编码器并没有捕捉到足够的信息来进行类的分离;然而,在潜在空间的某些部分,仍然可以看到明显定义的组,例如位于底部中间和左侧的聚类,其中一个聚类与一组飞机图像相关。例如,这个深度信念网络已经足够了解输入空间的信息,能够区分输入的某些不同特征;例如,它知道飞机与青蛙有很大区别,或者至少它们可能出现在不同的环境中,也就是说,青蛙会出现在绿色背景下,而飞机可能出现在蓝色天空的背景下。
卷积神经网络(CNNs)对于大多数计算机视觉和图像分析问题(如本例)是一个更好的选择。我们将在第十二章中讲到卷积神经网络。请耐心等待,我们将逐步介绍不同的模型。你将看到我们如何制作一个卷积自编码器,它能比使用全连接层的自编码器实现更好的性能。目前,我们将继续使用自编码器,稍微再多讲一会儿。
图 8.8中介绍的模型可以通过函数式方法实现;编码器可以定义如下:
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Dense, Dropout, Activation, Input
from tensorflow.keras.layers import BatchNormalization, MaxPooling1D
import numpy as np
inpt_dim = 32*32*3
ltnt_dim = 64
inpt_vec = Input(shape=(inpt_dim,))
el1 = Dense(2048)(inpt_vec)
el2 = Activation('relu')(el1)
el3 = Dense(1024)(el2)
el4 = BatchNormalization()(el3)
el5 = Activation('relu')(el4)
el6 = Dropout(0.2)(el5)
el7 = Dense(512)(el6)
el8 = Activation('relu')(el7)
el9 = Dense(256)(el8)
el10 = BatchNormalization()(el9)
el11 = Activation('relu')(el10)
el12 = Dropout(0.2)(el11)
el13 = Dense(128)(el12)
el14 = Activation('relu')(el13)
el15 = Dropout(0.2)(el14)
el16 = Dense(ltnt_dim)(el15)
el17 = BatchNormalization()(el16)
encoder = Activation('tanh')(el17)
# model that takes input and encodes it into the latent space
latent_ncdr = Model(inpt_vec, encoder)
请注意,所有的丢弃层每次的丢弃率为 20%。这 17 层将输入 inpt_dim=3072
维度映射到 ltnt_dim = 64
维度。编码器的最后一个激活函数是双曲正切函数 tanh
,其输出范围为 [-1,1];这个选择仅仅是为了方便可视化潜在空间。
接下来,解码器的定义如下:
dl1 = Dense(128)(encoder)
dl2 = BatchNormalization()(dl1)
dl3 = Activation('relu')(dl2)
dl4 = Dropout(0.2)(dl3)
dl5 = Dense(256)(dl4)
dl6 = Activation('relu')(dl5)
dl7 = Dense(512)(dl6)
dl8 = BatchNormalization()(dl7)
dl9 = Activation('relu')(dl8)
dl10 = Dropout(0.2)(dl9)
dl11 = Dense(1024)(dl10)
dl12 = Activation('relu')(dl11)
dl13 = Dense(2048)(dl12)
dl14 = BatchNormalization()(dl13)
dl15 = Activation('relu')(dl14)
decoder = Dense(inpt_dim, activation='sigmoid') (dl15)
# model that takes input, encodes it, and decodes it
autoencoder = Model(inpt_vec, decoder)
解码器的最后一层具有 sigmoid
激活函数,将其映射回输入空间范围,即[0.0, 1.0]。最后,我们可以像以前定义的那样,使用 binary_crossentropy
损失和 adam
优化器,训练 autoencoder
模型 200 个 epoch,如下所示:
autoencoder.compile(loss='binary_crossentropy', optimizer='adam')
hist = autoencoder.fit(x_train, x_train, epochs=200, batch_size=5000,
shuffle=True, validation_data=(x_test, x_test))
结果已在图 8.9到图 8.11中展示过。然而,重新审视 MNIST 是很有意思的,但这次我们将使用深度自编码器,接下来会讨论。
MNIST
MNIST
数据集是一个很好的示例,它的复杂度低于 CIFAR-10,可以使用深度自编码器进行处理。在第七章中,自编码器,我们讨论了浅层自编码器,并展示了增加层数是有益的。在本节中,我们更进一步,展示了具有丢弃层(dropout)和批量归一化层(batch normalization)的深度自编码器如何在生成丰富的潜在表示方面表现得更好。图 8.13展示了提出的架构:
图 8.13 – 用于 MNIST 的深度自编码器
层的数量和层的顺序与图 8.8中的相同;然而,密集层中的神经元数量和潜在表示的维度发生了变化。压缩率从 784 降到 2,或 0.25%:
图 8.14 – 测试集的 MNIST 原始样本数字
然而,重建结果非常好,如图 8.14和图 8.15所示:
图 8.15 – 从原始测试集重建的 MNIST 数字,如图 8.14所示
图中显示的重建图像具有非常好的细节,尽管在边缘看起来有些模糊。模型似乎很好地捕捉到了数字的整体形状。测试集的相应潜在表示显示在图 8.16中:
图 8.16 – 测试集中 MNIST 数字的潜在表示,部分展示于图 8.14中
从前面的图中,我们可以看到有清晰的聚类;然而,需要指出的是,自动编码器对标签一无所知,这些聚类完全是从数据中学习得出的。这正是自动编码器的强大之处。如果将编码器模型拆开并使用标签重新训练,模型的性能可能会更好。但现在,我们暂且停在这里,继续讨论下一章中的一种生成模型——第九章,变分自动编码器。
总结
本章展示了深度自动编码器在结合正则化策略(如 dropout 和批量归一化)时的强大功能。我们实现了一个超过 30 层的自动编码器!这真是深度!我们看到,在复杂问题中,深度自动编码器能够提供对高度复杂数据的无偏潜在表示,就像大多数深度信念网络一样。我们探讨了 dropout 如何通过在每次学习步骤中随机忽略(断开)一部分神经元,来降低过拟合的风险。此外,我们还了解了批量归一化如何通过逐步调整某些神经元的响应,提供学习算法的稳定性,确保激活函数和其他连接的神经元不会饱和或在数值上溢出。
到此为止,你应该能够自信地在深度自动编码器模型中应用批量归一化和 dropout 策略。你应该能够创建自己的深度自动编码器,并将其应用于需要丰富潜在表示的数据可视化、数据压缩、降维问题,以及其他类型的嵌入式数据,其中需要低维表示。
第九章,变分自动编码器,将从生成 建模的角度继续讨论自动编码器。生成模型能够通过从概率密度函数中采样来生成数据,这非常有趣。我们将特别讨论变分自动编码器模型,作为在存在噪声数据时,深度自动编码器的更好替代方案。
问题与解答
- 本章讨论的哪种正则化策略可以缓解深度模型中的过拟合?
Dropout。
- 添加批量归一化层会使学习算法需要学习更多的参数吗?
事实上,并不是这样。对于每一层使用 dropout 的情况,每个神经元需要学习的参数只有两个:。如果你做一下计算,你会发现新增的参数其实相对较少。
- 还有哪些其他的深度置信网络?
限制玻尔兹曼机(Restricted Boltzmann Machines),例如,是另一种非常流行的深度置信网络实例。第十章,限制玻尔兹曼机,将详细介绍这些内容。
- 为什么深度自编码器在 MNIST 数据集上的表现优于 CIFAR-10?
事实上,我们并没有一个客观的方式来判断深度自编码器在这些数据集上的表现是否更好。我们在考虑时往往带有偏见,习惯于从聚类和数据标签的角度思考。我们在考虑 图 8.12 和 图 8.16 中潜在表示时,将其与标签关联的偏见,使得我们无法考虑其他可能性。以 CIFAR-10 为例:如果自编码器正在学习根据纹理、颜色调色板或几何属性来表示数据呢?回答这些问题是理解自编码器内部工作原理及其如何学习表示数据的关键,但这需要更高级的技能和时间。总之,在我们回答这些问题之前,我们无法确定它是否表现不佳;否则,如果我们用类别、组和标签的视角来看问题,可能会误以为是这样。
参考文献
-
Sutskever, I., & Hinton, G. E. (2008). 深度、狭窄的 sigmoid 置信网络是通用近似器。神经计算,20(11),2629-2636。
-
Sainath, T. N., Kingsbury, B., & Ramabhadran, B. (2012 年 3 月). 使用深度置信网络的自编码器瓶颈特征。发表于 2012 年 IEEE 国际声学、语音与信号处理大会 (ICASSP) (第 4153-4156 页)。IEEE。
-
Wu, K., & Magdon-Ismail, M. (2016). 节点逐一贪婪深度学习用于可解释特征。arXiv 预印本 arXiv:1602.06183。
-
Ioffe, S., & Szegedy, C. (2015 年 6 月). 批量归一化:通过减少内部协变量偏移加速深度网络训练。发表于 国际机器学习大会 (ICML) (第 448-456 页)。
-
Srivastava, N., Hinton, G., Krizhevsky, A., Sutskever, I., & Salakhutdinov, R. (2014). Dropout:一种简单的防止神经网络过拟合的方法。机器学习研究期刊,15(1),1929-1958。
-
Duchi, J., Hazan, E., & Singer, Y. (2011). 在线学习与随机优化的自适应子梯度方法。机器学习研究期刊,12(Jul),2121-2159。
-
McInnes, L., Healy, J., & Umap, J. M. (2018). 用于降维的统一流形逼近与投影。arXiv 预印本 arXiv:1802.03426。
-
Maas, A. L., Daly, R. E., Pham, P. T., Huang, D., Ng, A. Y., & Potts, C. (2011, June). 学习情感分析的词向量。在计算语言学年会第 49 届年会: 人类语言技术-volume 1 (pp. 142-150). 计算语言学协会。
-
Hochreiter, S. (1998). 在学习递归神经网络期间的梯度消失问题及其解决方案。不确定性、模糊性和基于知识的系统国际期刊, 6(02), 107-116。
-
Van Laarhoven, T. (2017). L2 正则化对批处理和权重规范化的比较。arXiv 预印本 arXiv:1706.05350。
变分自编码器
自编码器在寻找丰富的潜在空间方面非常强大。它们几乎是神奇的,对吧?如果我们告诉你变分自编码器(VAE)更令人印象深刻呢?嗯,它们确实更强大。它们继承了传统自编码器的所有优点,并增加了从参数化分布中生成数据的能力。
在本章中,我们将介绍生成模型在无监督深度学习领域的理念及其在新数据生成中的重要性。我们将展示 VAE 作为深度自编码器的更好替代方案。在本章结束时,你将了解 VAE 的来源和目的。你将能够看出深度和浅层 VAE 模型之间的区别,并且能够欣赏 VAE 的生成特性。
本章的结构如下:
-
介绍深度生成模型
-
检视 VAE 模型
-
在 MNIST 上比较深层和浅层 VAE
-
思考生成模型的伦理影响
第十二章:介绍深度生成模型
深度学习为整个机器学习社区做出了非常有趣的贡献,特别是在深度判别模型和生成模型方面。我们熟悉判别模型的定义——例如,多层感知机(MLP)就是一个例子。在判别模型中,我们的任务是根据输入数据,猜测、预测或近似所需的目标,![],!。用统计学术语来说,我们正在建模条件概率密度函数,!。另一方面,生成模型就是大多数人所指的:
在深度学习中,我们可以建立一个神经网络,它能够非常好地建模这个生成过程。从统计学的角度来看,神经模型近似条件概率密度函数,!。虽然如今有多种生成模型,但在本书中,我们将重点讨论三种。
首先,我们将讨论 VAE,它将在下一节中详细介绍。其次,第十章,《受限玻尔兹曼机》将介绍图形方法及其特性(Salakhutdinov, R., 等,2007 年)。最后的方法将在第十四章,《生成对抗网络》中介绍。这些网络正在改变我们对模型鲁棒性和数据生成的思考方式(Goodfellow, I., 等,2014 年)。
检视 VAE 模型
VAE 是一种特定类型的自编码器(Kingma, D. P., & Welling, M. (2013))。它通过贝叶斯方法学习数据集的特定统计属性。首先,定义![]为随机潜变量的先验概率密度函数,![]。接下来,我们可以描述条件概率密度函数![],它可以解释为一个能够生成数据的模型——比如,![]。由此我们可以用条件分布和先验分布来近似后验概率密度函数,如下所示:
事实证明,精确的后验是不可处理的,但这个问题可以通过做一些假设并使用一个有趣的想法来计算梯度,从而近似求解。首先,假设先验服从各向同性高斯分布,。我们还可以假设条件分布![]可以用神经网络进行参数化建模;也就是说,给定潜在向量
,我们用神经网络来生成
。在这种情况下,网络的权重记作![],该网络相当于解码器网络。选择的参数化分布可以是高斯分布,用于输出
可能取值范围广泛的情况,或者是伯努利分布,如果输出
可能是二进制(或布尔)值。接下来,我们必须再次使用另一个神经网络来近似后验,通过![]与独立的参数![]。这个网络可以被解释为编码器网络,它接受
作为输入并生成潜在变量
。
在这个假设下,我们可以定义一个损失函数,如下所示:
完整的推导可以参照 Kingma, D. P., & Welling, M. (2013) 中的内容。然而,简而言之,我们可以说,在第一个项中, 是 Kullback–Leibler 散度函数,它旨在衡量先验分布 ![] 与后验分布 ![] 的差异。这发生在 编码器 中,我们希望确保先验和后验的分布
相匹配。第二项与解码器网络相关,旨在基于条件分布
的负对数似然最小化重构损失,同时参考后验的期望
。
使 VAE 通过梯度下降学习的最后一个技巧是使用一个叫做重参数化的概念。这一技巧是必要的,因为无法将一个样本 编码为一个均值为 0 且方差为某值的各向同性高斯分布,并从该分布中抽取样本
,停顿在那里,然后继续解码并计算梯度,最后再返回并进行更新。重参数化技巧实际上是一种从 ![] 生成样本的方法,同时允许进行梯度计算。如果我们说 ![],我们可以用一个辅助变量
来表示随机变量
,并且其边际概率密度函数为 ![],使得 ![] 和 ![] 是一个由 ![] 参数化的函数,并返回一个向量。这使得可以对参数 ![] (生成器或解码器)进行梯度计算,并使用任何可用的梯度下降方法对 ![] 和 ![] 进行更新。
方程中 ![] 的 tilde 符号(~)可以解释为 服从分布。因此,该方程可以读作 服从后验分布 ![]。
图 9.1 展示了 VAE 的架构,明确显示了在 瓶颈 中涉及的各个部分,以及网络各部分的解释:
图 9.1 – VAE 架构
上述图示表明,在理想的 VAE 中,分布的参数会被准确无误地学习,从而实现精确的重构。然而,这仅仅是一个示意图,实际上,完美重构可能很难实现。
瓶颈是神经网络中存在的潜在表示或参数,这些网络从具有大量神经单元的层转向具有较少神经单元的层。这些瓶颈已知能产生有趣的特征空间(Zhang, Y., et al. (2014))。
现在,让我们准备逐步构建第一个 VAE。我们将从描述我们将使用的数据集开始。
重新审视心脏病数据集
在第三章中,数据准备,我们详细描述了一个叫做克利夫兰心脏病数据集的属性。该数据集的两列截图如图 9.2所示。在这里,我们将重新审视这个数据集,目的是将原始的 13 个维度数据降至仅 2 个维度。不仅如此,我们还将尝试从生成器——即解码器——中生成新数据:
图 9.2 – 克利夫兰心脏病数据集的两列样本
我们尝试进行降维的工作可以通过查看第三章中的图 3.8和图 3.9轻松证明,并注意到数据可能会被处理,以查看神经网络是否能将没有心脏病的数据与其他数据分开聚类。同样,我们可以证明生成新数据的合理性,因为数据集本身仅包含 303 个样本。
为了下载数据,我们可以简单地运行以下代码:
#download data
!wget https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.cleveland.data
然后,为了将数据加载到数据框中并分离训练数据和目标,我们可以执行以下代码:
import pandas as pd
df = pd.read_csv('processed.cleveland.data', header=None)
# this next line deals with possible numeric errors
df = df.apply(pd.to_numeric, errors='coerce').dropna()
X = df[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]].values
y = df[13].values
接下来,我们需要做的是编写重参数化技巧的代码,以便在训练过程中能够采样随机噪声。
重参数化技巧和采样
记住,重参数化技巧的目的是从 ![] 而不是 ![] 进行采样。此外,回想一下分布 ![]。这将使我们能够让学习算法学习 ![] 的参数——即 ![]——我们只需要从 ![] 中生成一个样本。
为了实现这一点,我们可以生成以下方法:
from tensorflow.keras import backend as K
def sampling(z_params):
z_mean, z_log_var = z_params
batch = K.shape(z_mean)[0]
dims = K.int_shape(z_mean)[1]
epsilon = K.random_normal(shape=(batch, dims))
return z_mean + K.exp(0.5 * z_log_var) * epsilon
sampling()
方法接收 的均值和对数方差(这些需要学习),并返回从这个参数化分布中抽取的样本向量;
只是来自高斯(
random_normal
)分布的随机噪声,均值为 0,方差为 1。为了使此方法完全兼容小批量训练,样本将根据小批量的大小生成。
在编码器中学习后验的参数
后验分布,![],本身是不可处理的,但由于我们使用了重参数化技巧,我们实际上可以基于 进行采样。接下来,我们将创建一个简单的编码器来学习这些参数。
为了保证数值稳定性,我们需要将输入缩放为均值为 0,方差为 1。为此,我们可以调用在 第三章《数据准备》中学到的方法:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaler.fit(X)
x_train = scaler.transform(X)
original_dim = x_train.shape[1]
x_train
矩阵包含经过缩放的训练数据。以下变量对于设计 VAE 的编码器也会非常有用:
input_shape = (original_dim, )
intermediate_dim = 13
batch_size = 18 # comes from ceil(sqrt(x_train.shape[0]))
latent_dim = 2 # useful for visualization
epochs = 500
这些变量比较直观,唯一需要注意的是批次大小是以样本数量的平方根为基准的。这是一个通过经验得出的良好起点,但在较大的数据集上,它并不能保证是最优的。
接下来,我们可以按如下方式构建编码器部分:
from tensorflow.keras.layers import Lambda, Input, Dense, Dropout, BatchNormalization
from tensorflow.keras.models import Model
inputs = Input(shape=input_shape)
bn = BatchNormalization()(inputs)
dp = Dropout(0.2)(bn)
x = Dense(intermediate_dim, activation='sigmoid')(dp)
x = Dropout(0.2)(x)
z_mean = Dense(latent_dim)(x)
z_log_var = Dense(latent_dim)(x)
z_params = [z_mean, z_log_var]
z = Lambda(sampling, output_shape=(latent_dim,))(z_params)
encoder = Model(inputs, [z_mean, z_log_var, z])
这种编码器方法利用了 Lambda
类,它是 tensorflow.keras.layers
库的一部分。这使得我们能够将之前定义的 sampling()
方法(或任何任意表达式)作为一个层对象来使用。图 9.3 展示了完整 VAE 的架构,包括前面代码块中描述的编码器层:
图 9.3 – 适用于克利夫兰心脏病数据集的 VAE 架构
编码器使用批量归一化,接着是输入层的 丢弃,然后是一个包含 Tanh 激活 和 丢弃 的密集层。通过 丢弃,两个密集层负责建模潜在变量分布的参数,并从这个参数化分布中抽取样本。接下来将讨论解码器网络。
建模解码器
VAE 的解码器部分在你已知的自动编码器中是非常标准的。解码器接受潜在变量,该变量在 VAE 中由参数化分布生成,然后它应该精确地重建输入。解码器可以按如下方式指定:
latent_inputs = Input(shape=(latent_dim,))
x = Dense(intermediate_dim, activation='relu')(latent_inputs)
r_outputs = Dense(original_dim)(x) # reconstruction outputs
decoder = Model(latent_inputs, r_outputs)
在前面的代码中,我们简单地连接了两个密集层——第一个层包含 ReLU 激活,而第二个层则具有线性激活,以便映射回输入空间。最后,我们可以根据编码器和解码器中定义的输入和输出来定义完整的 VAE:
outputs = decoder(encoder(inputs)[2]) # it is index 2 since we want z
vae = Model(inputs, outputs)
如此处所述并在图 9.3中所示,VAE 模型已经完成。接下来将进入训练该模型的步骤,其中包括定义损失函数,我们将在后面讨论。
最小化损失
我们之前解释过,损失函数需要以编码器和解码器为基础;这是我们讨论过的方程:
如果我们想编写这个损失函数的代码,我们需要用更实际的方式来表示它。应用之前在问题中做出的所有假设,包括重新参数化技巧,可以让我们将损失的近似值用更简单的方式重新编写,如下所示:
这适用于所有样本的 ,其中 ![] 和 ![]。此外,解码器损失部分可以使用你喜欢的任何重建损失来近似——例如均方误差(MSE)损失或二元交叉熵损失。已证明,最小化这些损失中的任何一个也会最小化后验。
我们可以将重建损失定义为 MSE,如下所示:
from tensorflow.keras.losses import mse
r_loss = mse(inputs, outputs)
或者,我们也可以用二元交叉熵损失来表示,如下所示:
from tensorflow.keras.losses import binary_crossentropy
r_loss = binary_crossentropy(inputs, outputs)
我们可以做的一件额外的事情(这是可选的),是监控重建损失与 KL 散度损失(与编码器相关的项)之间的重要性。通常的做法是将重建损失乘以潜在维度或输入维度。这样做实际上会使损失增大。若选择后者,我们可以像下面这样对重建损失进行惩罚:
r_loss = original_dim * r_loss
现在,编码器项的 KL 散度损失可以用均值和方差来表示,如下所示:
kl_loss = 1 + z_log_var - K.square(z_mean) - K.exp(z_log_var)
kl_loss = 0.5 * K.sum(kl_loss, axis=-1)
因此,我们可以简单地将整体损失添加到模型中,模型变为如下形式:
vae_loss = K.mean(r_loss + kl_loss)
vae.add_loss(vae_loss)
有了这些,我们就可以继续编译模型并进行训练,接下来将做详细说明。
训练 VAE
最后一步是编译 VAE 模型,这将把所有部分结合在一起。在编译过程中,我们需要选择一个优化器(梯度下降法)。在这种情况下,我们选择Adam(Kingma, D. P.,et al.(2014))。
有趣的是,VAE 的创始人是同一个人在不久之后创造了 Adam 优化器。他的名字是Diederik P. Kingma,目前是 Google Brain 的研究科学家。
为了编译模型并选择优化器,我们需要执行以下操作:
vae.compile(optimizer='adam')
最后,我们使用训练数据进行 500 次 epoch 的训练,批大小为 18,代码如下:
hist = vae.fit(x_train, epochs=epochs,
batch_size=batch_size,
validation_data=(x_train, None))
请注意,我们使用训练集作为验证集。在大多数情况下,这样做是不推荐的,但在这里这样做是可行的,因为选择相同的 mini-batch 进行训练和验证的概率非常低。此外,通常来说,这样做被认为是作弊;然而,用于重构的潜在表示并不直接来自输入,而是来自与输入数据相似的分布。为了展示训练集和验证集会产生不同的结果,我们绘制了跨越各个 epoch 的训练进度,如图 9.4所示:
图 9.4 – VAE 在各个 epoch 中的训练表现
上图不仅表明模型收敛速度很快,而且还显示模型不会对输入数据进行过拟合。通常来说,这是一个很好的特性。
图 9.4可以通过以下代码生成:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(10,6))
plt.plot(hist.history['loss'], color='#785ef0')
plt.plot(hist.history['val_loss'], '--', color='#dc267f')
plt.title('Model reconstruction loss')
plt.ylabel('MSE Loss')
plt.xlabel('Epoch')
plt.legend(['Training Set', 'Validation Set'], loc='upper right')
plt.show()
然而,请注意,由于 VAE 是无监督的,因此结果可能会有所不同。
接下来,如果我们观察通过从随机分布中采样并利用训练过程中学到的参数生成的潜在表示,我们就能看到数据的样子。图 9.5展示了获得的潜在表示:
图 9.5 – VAE 在二维空间中采样的潜在表示
图中清晰地显示,未指示心脏病的数据聚集在左侧象限,而对应心脏病的样本聚集在潜在空间的右侧象限。图顶端显示的直方图表明存在两个明确的聚类。这很好!另外,请记住,VAE 并不知道任何标签信息:我们无法过分强调这一点!将这里的图 9.5与第三章中的图 3.9进行比较,数据预处理,你会注意到 VAE 的表现优于 KPCA。此外,将此图与第三章中的图 3.8进行比较,数据预处理,并注意到 VAE 的表现与线性判别分析(LDA)相当(如果不是更好)。LDA 利用标签信息生成低维表示。换句话说,LDA 有点“作弊”。
VAE 最有趣的特性之一是我们可以生成数据;接下来,让我们看看是如何做到这一点的。
从 VAE 生成数据
由于 VAE 学习潜在空间上参数化分布的参数,并利用这些参数采样来重构输入数据,因此我们可以利用这些参数生成更多的样本并进行重构。这个想法是根据我们的需求生成数据。
我们从编码原始数据集开始,看看重建的结果与原始数据有多接近。然后,生成数据应该是直观的。为了将输入数据编码到潜在空间并解码,我们可以做如下操作:
encdd = encoder.predict(x_train)
x_hat = decoder.predict(encdd[0])
记住,x_train
是!,而x_hat
是重建结果,!。注意,我们使用encdd[0]
作为解码器的输入。之所以这样做,是因为编码器输出的是三个向量的列表,[z_mean, z_log_var, z]
。因此,使用列表中的第 0 个元素即表示对应样本的分布均值。实际上,encdd[0][10]
会得到一个二维向量,对应于能够生成数据集中第 10 个样本的分布均值——也就是x_train[10]
。如果你仔细想一下,均值可能是我们能够找到的最佳潜在表示,因为它最有可能在解码器中重建输入。
牢记这一点,我们可以通过运行类似的代码来查看重建的效果有多好:
import numpy as np
print(np.around(scaler.inverse_transform(x_train[0]), decimals=1))
print(np.around(scaler.inverse_transform(x_hat[0]), decimals=1))
这将产生以下输出:
[ 63.0 1.0 1.0 145.0 233.0 1.0 2.0 150.0 0.0 2.3 3.0 0.0 6.0 ]
[ 61.2 0.5 3.1 144.1 265.1 0.5 1.4 146.3 0.2 1.4 1.8 1.2 4.8 ]
如果输出显示的是难以阅读的科学计数法,可以尝试暂时禁用它,如下所示:
import numpy as np
np.set_printoptions(suppress=True)
print(np.around(scaler.inverse_transform(x_train[0]), decimals=1))
print(np.around(scaler.inverse_transform(x_hat[0]), decimals=1))
np.set_printoptions(suppress=False)
在这个例子中,我们专注于训练集中的第一个数据点,也就是x_train[0]
,位于顶部行;它的重建结果位于底部行。仔细检查可以发现两者之间有差异;然而,这些差异可能在均方误差(MSE)方面相对较小。
另一个需要指出的重要方面是,数据在训练模型之前已被缩放,因此需要将其缩放回原始输入空间。幸运的是,StandardScaler()
类有一个inverse_transform()
方法,可以帮助将任何重建数据映射回输入空间中每个维度的值范围。
为了随意生成更多数据,我们可以定义一个方法来实现。以下方法会产生均匀分布的随机噪声,范围为[-2, +2]
,这个范围来自于对图 9.5的分析,该图显示了潜在空间的范围在这个区间内:
def generate_samples(N = 10, latent_dim = 2):
noise = np.random.uniform(-2.0, 2.0, (N,latent_dim))
gen = decoder.predict(noise)
return gen
这个函数需要根据潜在空间中值的范围进行调整;同时,也可以通过观察潜在空间中数据的分布来调整。例如,如果潜在空间似乎是正态分布的,那么可以像这样使用正态分布:noise = np.random.normal(0.0, 1.0, (N,latent_dim))
,假设均值为 0,方差为 1。
我们可以通过以下方式调用函数生成伪数据:
gen = generate_samples(10, latent_dim)
print(np.around(scaler.inverse_transform(gen), decimals=1))
这将产生以下输出:
[[ 43.0 0.7 2.7 122.2 223.8 0.0 0.4 172.2 0.0 0.3 1.2 0.1 3.6]
[ 57.4 0.9 3.9 133.1 247.6 0.1 1.2 129.0 0.8 2.1 2.0 1.2 6.4]
[ 60.8 0.7 3.5 142.5 265.7 0.3 1.4 136.4 0.5 1.9 2.0 1.4 5.6]
[ 59.3 0.6 3.2 137.2 261.4 0.2 1.2 146.2 0.3 1.2 1.7 0.9 4.7]
[ 51.5 0.9 3.2 125.1 229.9 0.1 0.7 149.5 0.4 0.9 1.6 0.4 5.1]
[ 60.5 0.5 3.2 139.9 268.4 0.3 1.3 146.1 0.3 1.2 1.7 1.0 4.7]
[ 48.6 0.5 2.6 126.8 243.6 0.1 0.7 167.3 0.0 0.2 1.1 0.1 3.0]
[ 43.7 0.8 2.9 121.2 219.7 0.0 0.5 163.8 0.1 0.5 1.4 0.1 4.4]
[ 54.0 0.3 2.5 135.1 264.2 0.2 1.0 163.4 0.0 0.3 1.1 0.3 2.7]
[ 52.5 1.0 3.6 123.3 227.8 0.0 0.8 137.7 0.7 1.6 1.8 0.6 6.2]]
回想一下,这些数据是由随机噪声生成的。你可以看到,这是深度学习领域的一个重大突破。你可以使用这些数据来增强你的数据集,并根据需要生成任意数量的样本。我们可以查看生成样本的质量,自己判断其是否足够满足我们的需求。
现在,考虑到你和我可能不是专门从事心脏病研究的医生,我们可能没有资格确切判断生成的数据是否合理;但如果我们正确执行了操作,通常来说,它确实是合理的。为了明确这一点,接下来的部分将使用 MNIST 图像来证明生成的样本是好的,因为我们都可以对数字图像进行视觉评估。
比较 MNIST 上的深层和浅层 VAE
比较浅层和深层模型是实验过程的一部分,目的是找到最佳模型。在这个关于 MNIST 图像的比较中,我们将实现 图 9.6 中显示的架构作为浅层模型,而深层模型的架构如 图 9.7 所示:
图 9.6 – MNIST 上的 VAE 浅层架构
正如你所看到的,这两个模型在涉及的层数上有很大不同。由于这一差异,重建的质量也会有所不同:
图 9.7 – MNIST 上的 VAE 深度架构
这些模型将使用少量的 epoch 来训练浅层 VAE,而更深的模型将使用更多的 epoch。
重现浅层编码器的代码可以从在克利夫兰心脏病数据集中使用的示例中轻松推断出来;然而,深层 VAE 的代码将在接下来的章节中讨论。
浅层 VAE
我们可以用来比较 VAE 的第一件事之一是其学习到的表示。图 9.8 显示了训练集的潜在空间投影:
图 9.8 – 浅层 VAE 潜在空间投影(训练数据集)
从前面的图中,我们可以观察到数据点的聚类从中心坐标向外扩展。我们希望看到的是清晰定义的聚类,这些聚类之间的分隔足够清晰,以便于分类。例如,在这种情况下,我们看到某些组之间有一点重叠,特别是 4 和 9 号组,这其实是有道理的。
接下来需要关注的是模型的重建能力。图 9.9 显示了输入的示例,图 9.10 显示了模型训练后的相应重建结果:
图 9.9 – VAE 的示例输入
对于浅层模型的预期是,其表现将直接与模型的大小相关:
图 9.10 – 浅层 VAE 重建与图 9.9 中输入的关系
显然,数字 2 和数字 8 的重建似乎存在一些问题,这一点通过观察图 9.8中这两个数字之间的重叠可以得到确认。
另一个我们可以做的事情是,如果我们从潜在空间的范围中绘制数字,来可视化 VAE 生成的数据。图 9.11展示了潜在空间在两个维度上的变化:
图 9.11 – 浅层 VAE 在范围 [-4,4] 内的潜在空间探索
在图 9.11中,我们发现非常有趣的一点是,我们可以看到随着潜在空间的遍历,数字是如何逐步转换成其他数字的。如果我们沿着从上到下的中线,我们可以看到数字如何从 0 转变为 6,然后是 2,再到 8,最后到 1。我们也可以通过沿着对角线或其他方向进行同样的操作。制作这种类型的可视化还允许我们看到一些在训练数据集中没有看到的伪影,这些伪影可能会在我们生成数据时造成潜在问题。
为了查看更深层的模型是否比这个更好,我们将在下一节中实现它。
深层 VAE
图 9.7展示了一种可以分部分实现的深层 VAE 架构——首先是编码器,然后是解码器。
编码器
编码器可以使用函数式范式实现,如下所示:
from tensorflow.keras.layers import Lambda, Input, Dense, Dropout
from tensorflow.keras.layers import Activation, BatchNormalization
from tensorflow.keras.models import Model
inpt_dim = 28*28
ltnt_dim = 2
inpt_vec = Input(shape=(inpt_dim,))
这里,inpt_dim
对应于 28*28 的 MNIST 图像的 784 个维度。接下来,继续以下内容:
el1 = Dropout(0.1)(inpt_vec)
el2 = Dense(512)(el1)
el3 = Activation('relu')(el2)
el4 = Dropout(0.1)(el3)
el5 = Dense(512)(el4)
el6 = BatchNormalization()(el5)
el7 = Activation('relu')(el6)
el8 = Dropout(0.1)(el7)
el9 = Dense(256)(el8)
el10 = Activation('relu')(el9)
el11 = Dropout(0.1)(el10)
el12 = Dense(256)(el11)
el13 = BatchNormalization()(el12)
el14 = Activation('relu')(el13)
el15 = Dropout(0.1)(el14)
el16 = Dense(128)(el15)
el17 = Activation('relu')(el16)
el18 = Dropout(0.1)(el17)
el19 = Dense(ltnt_dim)(el18)
el20 = BatchNormalization()(el19)
el21 = Activation('sigmoid')(el20)
z_mean = Dense(ltnt_dim)(el21)
z_log_var = Dense(ltnt_dim)(el21)
z = Lambda(sampling)([z_mean, z_log_var])
encoder = Model(inpt_vec, [z_mean, z_log_var, z])
请注意,编码器模型使用了 10% 丢弃率的丢弃层。其他层都是我们之前见过的内容,包括批量归一化。唯一的新内容是Lambda
函数,正如本章前面所定义的。
接下来,我们将定义解码器。
解码器
解码器比编码器少了几层。这种层的选择只是为了展示,只要编码器和解码器中的稠密层数量几乎相等,就可以省略一些其他层,作为实验的一部分,寻找性能提升。
这是解码器的设计:
ltnt_vec = Input(shape=(ltnt_dim,))
dl1 = Dense(128)(ltnt_vec)
dl2 = BatchNormalization()(dl1)
dl3 = Activation('relu')(dl2)
dl4 = Dropout(0.1)(dl3)
dl5 = Dense(256)(dl4)
dl6 = Activation('relu')(dl5)
dl7 = Dense(256)(dl6)
dl8 = BatchNormalization()(dl7)
dl9 = Activation('relu')(dl8)
dl10 = Dropout(0.1)(dl9)
dl11 = Dense(512)(dl10)
dl12 = Activation('relu')(dl11)
dl13 = Dense(512)(dl12)
dl14 = BatchNormalization()(dl13)
dl15 = Activation('relu')(dl14)
dl16 = Dense(inpt_dim, activation='sigmoid') (dl15)
decoder = Model(ltnt_vec, dl16)
再次强调,这里没有什么新内容,是层与层之间的叠加。然后,我们可以将所有这些组合在一起形成模型,如下所示:
outputs = decoder(encoder(inpt_vec)[2])
vae = Model(inpt_vec, outputs)
就是这样!在此之后,我们可以编译模型,选择优化器,并像之前的节那样训练模型。
如果我们想要可视化深层 VAE 的潜在空间,以便与图 9.8进行比较,我们可以查看图 9.12中展示的空间:
图 9.12 – 深层 VAE 训练数据集的潜在空间投影
如你所见,潜在空间在几何形态上看起来有所不同。这很可能是激活函数作用的结果,它们限制了特定流形的潜在空间范围。最有趣的现象之一是,即使存在一些重叠——例如,数字 9 和 4,样本组之间的分离仍然很明显。然而,与图 9.8相比,这种重叠程度较轻。
图 9.13 展示了与图 9.9中的相同输入的重建效果,但这次使用的是更深层的 VAE:
图 9.13 – 深层 VAE 重建与图 9.9 中的输入相对应。与图 9.10 进行比较
显然,与浅层 VAE 相比,重建效果要好得多且更具鲁棒性。为了更清楚地展示这一点,我们还可以通过在潜在空间中生成与潜在空间相同范围的随机噪声来探索生成器。这在图 9.14中得到了展示:
图 9.14 – 深度 VAE 潜在空间在范围 [-4,4] 内的探索,涵盖两个维度。与图 9.11 进行比较
深层 VAE 的潜在空间显然在多样性上更为丰富,并且从视觉角度来看更加有趣。如果我们选择最右侧的那一条线,并从下到上遍历空间,我们可以看到数字 1 变成 7,再变成 4,且变化是逐渐且小幅的。
去噪 VAE
VAE 在图像去噪应用中也表现得非常出色(Im, D. I. J., 等(2017))。这一特性是通过在学习过程中注入噪声来实现的。若想了解更多信息,你可以在网上搜索去噪 VAE,你会找到关于这个话题的资料。我们只是希望你了解它们的存在,并知道它们在你需要时可以使用。
思考生成模型的伦理影响
生成模型是当前深度学习领域最激动人心的话题之一。但强大的能力伴随着巨大的责任。我们可以利用生成模型的力量做许多有益的事情,例如:
-
扩展你的数据集,使其更加完整
-
使用未见过的数据训练模型,使其更加稳定
-
找到对抗样本来重新训练你的模型,使其更具鲁棒性
-
创建看起来像其他事物的全新图像,例如艺术作品或车辆的图像
-
创建听起来像其他声音的新声音序列,例如人类说话或鸟类鸣叫
-
为数据加密生成新的安全代码
我们可以根据想象力的限制继续前进。我们必须时刻记住的是,这些生成模型如果没有正确建模,可能会导致许多问题,比如偏见,从而引发模型的可信度问题。使用这些模型生成某人说过的话,但实际上他们并没有说过的话的虚假音频序列,或者生成某人做过的事情但其实并未发生过的面孔图像,甚至是将不属于某人面孔的身体合成进去,这些都非常容易做到。
一些最显著的不当行为包括深度伪造。我们无需浪费时间讨论如何实现这一点,但可以明确的是,我们的生成模型不应被用于恶意目的。不久之后,国际法将会出台,惩罚那些通过恶意生成建模犯罪的人。
但在国际法律尚未出台、各国尚未制定新政策之前,你在开发模型时必须遵循最佳实践:
-
测试你的模型是否存在最常见的偏见类型:历史偏见、社会偏见、算法偏见等(Mehrabi, N., et al. (2019))。
-
使用合理的训练集和测试集来训练你的模型。
-
留意数据预处理技术;更多细节请参见第三章,数据准备。
-
确保你的模型产生的输出始终尊重所有人类的尊严和价值。
-
让同行验证你的模型架构。
牢记这一点,继续以负责任且富有创意的方式使用你现在拥有的新工具:VAE。
总结
本高级章节展示了一个非常有趣且相对简单的模型,能够通过配置自编码器并应用变分贝叶斯原理,从已学习的分布中生成数据,最终形成变分自编码器(VAE)。我们分析了该模型的各个部分,并通过克利夫兰数据集的输入数据来解释它们。随后,我们从学习到的参数化分布中生成了数据,证明了 VAE 可以轻松用于此目的。为了验证 VAE 在浅层和深层配置中的鲁棒性,我们在 MNIST 数据集上实现了一个模型。实验证明,深层架构能产生更加明确的数据分布区域,而浅层架构则产生模糊的分布区域;然而,无论是浅层模型还是深层模型,都在学习表示任务中表现出色。
到目前为止,你应该已经能够自信地识别 VAE 的各个组成部分,并能够在其动机、架构和能力方面区分传统自编码器和 VAE 的主要区别。你应该能够欣赏 VAE 的生成能力,并且准备好实现它们。在阅读完本章之后,你应该能够编写基本的和深度的 VAE 模型,并能够使用它们进行降维和数据可视化,以及在意识到潜在风险的情况下生成数据。最后,你现在应该已经熟悉了如何在 TensorFlow 和 Keras 中使用 Lambda
函数进行通用操作。
如果你到目前为止喜欢学习无监督模型,请继续跟我一起阅读 第十章,限制玻尔兹曼机,本章将介绍一个独特的模型,它基于图形模型。图形模型将图论与学习理论结合,用于执行机器学习。限制玻尔兹曼机的一个有趣之处是,在学习过程中算法可以前进和后退,以满足连接约束。敬请期待!
问题与答案
- 如何从随机噪声中生成数据?
由于 VAE 学习的是一个参数化的随机分布的参数,我们可以简单地利用这些参数从该分布中进行采样。由于随机噪声通常遵循具有特定参数的正态分布,因此我们可以说我们正在对随机噪声进行采样。值得一提的是,解码器知道如何处理符合特定分布的噪声。
- 更深层次的 VAE 有什么优势?
很难说出其优势是什么(如果有的话),因为没有数据或不知道应用场景。例如,对于克利夫兰心脏病数据集,深层 VAE 可能不必要;而对于 MNIST 或 CIFAR,适度较大的模型可能有益。视情况而定。
- 有没有办法修改损失函数?
当然,你可以更改损失函数,但要小心保持其构建原理。如果假设一年后我们找到了简化负对数似然函数的方式,那么我们可以(且应该)回过头来编辑损失函数,以采用新的方法。
参考文献
-
Kingma, D. P., & Welling, M. (2013)。自编码变分贝叶斯。arXiv 预印本 arXiv:1312.6114。
-
Salakhutdinov, R., Mnih, A., & Hinton, G. (2007 年 6 月)。用于协同过滤的限制玻尔兹曼机。发表于 第 24 届国际机器学习大会论文集(第 791-798 页)。
-
Goodfellow, I., Pouget-Abadie, J., Mirza, M., Xu, B., Warde-Farley, D., Ozair, S., Courville, A. 和 Bengio, Y. (2014)。生成对抗网络。发表于 神经信息处理系统进展(第 2672-2680 页)。
-
Zhang, Y., Chuangsuwanich, E., & Glass, J. (2014, May). 使用低秩矩阵分解提取深度神经网络瓶颈特征。在2014 年 IEEE 国际会议上的声学、语音和信号处理(ICASSP)(pp. 185-189)。IEEE。
-
Kingma, D. P., & Ba, J. (2014). Adam:随机优化方法。arXiv 预印本 arXiv:1412.6980。
-
Mehrabi, N., Morstatter, F., Saxena, N., Lerman, K., & Galstyan, A. (2019). 机器学习中的偏见和公平性调查。arXiv 预印本 arXiv:1908.09635。
-
Im, D. I. J., Ahn, S., Memisevic, R., & Bengio, Y. (2017, February). 变分自动编码框架的去噪标准。在第 31 届 AAAI 人工智能大会。
受限玻尔兹曼机
一起学习后,我们已经看到无监督学习的强大功能,并且希望已经说服自己,它可以应用于不同的问题。我们将以一个激动人心的方法——受限玻尔兹曼机(RBMs)结束无监督学习的主题。当我们不关心层数过多时,我们可以使用 RBMs 从数据中学习,并找到满足能量函数的方法,从而产生一个强大的模型来表示输入数据。
本章通过介绍 RBMs 的反向-前向特性,补充了第八章《深度自编码器》内容,同时与自编码器(AEs)的单向特性进行了对比。本章将 RBMs 与 AEs 在降维问题上的应用进行比较,使用 MNIST 作为案例研究。当你完成本章学习后,你应该能够使用 scikit-learn 实现 RBM,并使用伯努利 RBM 实现一个解决方案。你将能够进行 RBM 和 AE 潜在空间的可视化比较,并通过可视化学习到的权重来检查 RBM 和 AE 的内部工作原理。
本章组织结构如下:
-
RBM 简介
-
使用 RBM 学习数据表示
-
比较 RBMs 和 AEs
第十三章:RBM 简介
RBMs 是无监督模型,适用于需要丰富潜在表示的各种应用。它们通常与分类模型一起使用,目的是从数据中提取特征。它们基于玻尔兹曼机(BMs),接下来我们将讨论这一点(Hinton, G. E., 和 Sejnowski, T. J.(1983))。
玻尔兹曼机
受限玻尔兹曼机(RBM)可以被看作是一个无向的稠密图,如图 10.1所示:
图 10.1 – 玻尔兹曼机模型
这个无向图有一些神经单元,它们被建模为可见的,,以及一组被建模为隐藏的神经单元,
。当然,可能会有比这些更多的神经单元。但该模型的关键是所有神经元彼此连接:它们都互相交流。本节不会深入讲解此模型的训练过程,但基本上这是一个迭代过程,其中输入数据呈现到可见层,每个神经元(一次调整一个)会调整它与其他神经元之间的连接,以满足一个损失函数(通常基于能量函数),直到学习过程达到满意的程度。
尽管 RB 模型非常有趣且强大,但训练时间却非常长!考虑到这是在 1980 年代初期,当时进行比这更大的图和更大数据集的计算可能会显著影响训练时间。然而,在 1983 年,G. E. Hinton 及其合作者提出了通过限制神经元之间的通信来简化 BM 模型的方法,正如我们接下来将讨论的那样。
RBMs
传统 BM 的限制在于神经元之间的通信;也就是说,可见神经元只能与隐藏神经元通信,隐藏神经元只能与可见神经元通信,正如图 10.2所示:
图 10.2 – 一个 RBM 模型。与图 10.1 中的 BM 模型进行比较。
如图 10.2所示的图被称为密集二分图。也许你会觉得它看起来很像我们迄今为止使用的典型密集神经网络;然而,它并不完全相同。主要的区别在于我们使用的所有神经网络仅仅是从输入(可见层)到隐藏层的信息传递,而 RBM 则可以双向通信!其余的元素是熟悉的:我们有需要学习的权重和偏置。
如果我们坚持使用如图 10.2所示的简单模型,我们可以用更简单的术语解释 RBM 背后的学习理论。
让我们将每一个神经元单元解释为一个随机变量,其当前状态依赖于其他神经元单元的状态。
这种解释使我们能够使用与马尔可夫链蒙特卡洛(MCMC)(Brooks, S., et al. (2011))相关的采样技术;然而,我们在本书中不会深入讨论这些技术的细节。
使用这种解释,我们可以为模型定义一个能量函数,如下所示:
其中!分别表示可见神经元和隐藏神经元的偏置。事实证明,我们还可以将神经元和隐藏神经元的联合概率密度函数表示如下:
它具有一个简单的边际分布:
条件和边际中的分母被称为归一化因子,它的作用仅仅是确保概率值相加等于 1,可以定义如下:
这些公式使我们能够快速找到用于训练的 MCMC 技术;最著名的是,你会在文献中发现对比散度(Contrastive Divergence)涉及吉布斯采样(Gibbs sampling)是最常见的方法(Tieleman, T. (2008))。
目前只有少数实现了的 RBM 可供学习者使用;其中之一是可以在 scikit-learn 中使用的伯努利 RBM,我们接下来将讨论它。
伯努利 RBMs
虽然广义的 RBM 模型对其使用的数据不做任何假设,但是 Bernoulli RBM 假设输入数据表示在 [0,1] 范围内的值,可以解释为概率值。在理想情况下,这些值属于集合 {0,1},这与伯努利试验密切相关。如果您有兴趣,还有其他方法假设输入遵循高斯分布。您可以通过阅读 Yamashita, T. 等人 (2014) 了解更多信息。
只有少数数据集可以用于此类 RBM;MNIST 是一个示例,可以将其解释为二进制输入,其中当没有数字迹象时数据为 0,当存在数字信息时数据为 1。在 scikit-learn 中,BernoulliRBM
模型可以在神经网络集合中找到:sklearn.neural_network
。
在伯努利样本输入分布的假设下,这个 RBM 模型 大约 使用一种称为 Persistent Contrastive Divergence (PCD) 的方法优化对数似然。据称,PCD 在当时比任何其他算法都快得多,并引发了讨论和兴奋,很快被背景传播的普及所掩盖,与密集网络相比。
在接下来的部分中,我们将使用 Bernoulli RBM 在 MNIST 上实现,目的是学习数据集的表示。
使用 RBM 学习数据表示
现在您已经了解了 RBM 背后的基本思想,我们将使用 BernoulliRBM
模型以无监督的方式学习数据表示。与以往一样,我们将使用 MNIST 数据集进行比较。
对于一些人来说,学习表示 的任务可以被视为 特征工程。后者在术语上有一个可解释性的组成部分,而前者并不一定要求我们赋予学到的表示含义。
在 scikit-learn 中,我们可以通过以下方式创建 RBM 的实例:
from sklearn.neural_network import BernoulliRBM
rbm = BernoulliRBM()
在 RBM 的构造函数中,默认参数如下:
-
n_components=256
,即隐藏单元的数量,,而可见单元的数量,
,则根据输入的维度进行推断。
-
learning_rate=0.1
控制学习算法更新的强度,推荐尝试{1, 0.1, 0.01, 0.001}
中的值。 -
batch_size=10
控制在批处理学习算法中使用的样本数量。 -
n_iter=10
控制在停止学习算法之前运行的迭代次数。算法的性质允许我们继续运行它,但通常在少数迭代中找到良好的解决方案。
我们只需将默认的组件数改为 100 即可。由于 MNIST 数据集的原始维度是 784(因为它由 28 x 28 的图像组成),因此使用 100 个维度似乎是一个不错的主意。
为了在 MNIST 训练数据(已加载到x_train
中)上使用 100 个组件训练 RBM,我们可以执行以下操作:
from sklearn.neural_network import BernoulliRBM
from tensorflow.keras.datasets import mnist
import numpy as np
(x_train, y_train), (x_test, y_test) = mnist.load_data()
image_size = x_train.shape[1]
original_dim = image_size * image_size
x_train = np.reshape(x_train, [-1, original_dim])
x_test = np.reshape(x_test, [-1, original_dim])
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255
rbm = BernoulliRBM(verbose=True)
rbm.n_components = 100
rbm.fit(x_train)
训练过程中的输出可能如下所示:
[BernoulliRBM] Iteration 1, pseudo-likelihood = -104.67, time = 12.84s
[BernoulliRBM] Iteration 2, pseudo-likelihood = -102.20, time = 13.70s
[BernoulliRBM] Iteration 3, pseudo-likelihood = -97.95, time = 13.99s
[BernoulliRBM] Iteration 4, pseudo-likelihood = -99.79, time = 13.86s
[BernoulliRBM] Iteration 5, pseudo-likelihood = -96.06, time = 14.03s
[BernoulliRBM] Iteration 6, pseudo-likelihood = -97.08, time = 14.06s
[BernoulliRBM] Iteration 7, pseudo-likelihood = -95.78, time = 14.02s
[BernoulliRBM] Iteration 8, pseudo-likelihood = -99.94, time = 13.92s
[BernoulliRBM] Iteration 9, pseudo-likelihood = -93.65, time = 14.10s
[BernoulliRBM] Iteration 10, pseudo-likelihood = -96.97, time = 14.02s
我们可以通过在 MNIST 测试数据x_test
上调用transform()
方法来查看学习到的表示,方法如下:
r = rbm.transform(x_test)
在这种情况下,有 784 个输入维度,但r
变量将具有 100 个维度。为了在由 RBM 引起的潜在空间中可视化测试集,我们可以像之前一样使用 UMAP,这将产生图 10.3中所示的二维图:
图 10.3 – 使用 UMAP 对 RBM 在 MNIST 测试数据上的学习表示进行可视化
从 RBM 特征空间使用 UMAP 生成此图的完整代码如下:
import matplotlib.pyplot as plt
import umap
y_ = list(map(int, y_test))
X_ = rbm.transform(x_test)
X_ = umap.UMAP().fit_transform(X_)
plt.figure(figsize=(10,8))
plt.title('UMAP of 100 RBM Learned Components on MNIST')
plt.scatter(X_[:,0], X_[:,1], s=5.0, c=y_, alpha=0.75, cmap='tab10')
plt.xlabel('$z_1$')
plt.ylabel('$z_2$')
plt.colorbar()
将图 10.3与前几章中展示的表示进行比较。从图中可以看出,存在明显的类别分离和聚类,同时也可以看到类别之间有轻微的重叠。例如,数字 3 和 8 之间有一些重叠,这也是可以预料的,因为这两个数字看起来相似。这个图也显示了 RBM 的良好泛化能力,因为图 10.3中的数据来自模型未曾见过的数据。
我们可以进一步检查 RBM 学习到的权重(或组件);也就是说,我们可以检索与可见层相关的权重,方法如下:
v = rbm.components_
在这种情况下,v
变量将是一个 784 x 100 的矩阵,描述了学习到的权重。我们可以可视化每一个神经元,并重构与这些神经元相关的权重,结果将类似于图 10.4中的组成部分:
图 10.4 – RBM 学习到的权重
细致检查图 10.4会告诉我们,有一些权重关注于对角线特征、圆形特征或对特定数字和边缘非常特定的特征。例如,底部一行的特征似乎与数字 2 和 6 有关。
图 10.4中显示的权重可以用来将输入空间转换为更丰富的表示,这些表示随后可以在允许进行此任务的管道中用于分类。
为了满足我们的学习好奇心,我们还可以通过使用gibbs()
方法对网络进行采样,从而查看 RBM 及其状态。这意味着我们可以可视化在将输入呈现给可见层时发生了什么,然后是隐藏层的响应,再将其作为输入重复这个过程,观察模型的刺激如何变化。例如,运行以下代码:
import matplotlib.pyplot as plt
plt.figure()
cnt = 1
for i in range(10): #we look into the first ten digits of test set
x = x_test[i]
for j in range(10): #we project and reuse as input ten times
plt.subplot(10, 10, cnt)
plt.imshow(x.reshape((28, 28)), cmap='gray')
x = rbm.gibbs(x) #here use current as input and use as input again
cnt += 1
plt.show()
这将有效地产生一个类似于图 5所示的图:
图 10.5 – 基于 MNIST 的 RBM 上的 Gibbs 采样
图 10.5 显示了第一列的输入,剩余的 10 列是连续的采样调用。显然,随着输入在 RBM 中前后传播,它会出现一些轻微的变形。以第五行(对应数字 4)为例,我们可以看到输入是如何变形的,直到它看起来像是数字 2。除非在第一次采样调用时观察到强烈的变形,否则这些信息不会对所学习到的特征产生直接影响。
在下一节中,我们将使用 AE 与 RBM 进行比较。
比较 RBM 和 AE
现在我们已经看到 RBM 的表现,是时候将其与 AE 进行比较了。为了公平地进行比较,我们可以提出 AE 最接近 RBM 的配置;也就是说,我们将有相同数量的隐藏单元(编码器层中的神经元)和相同数量的可见层神经元(解码器层),如图 10.6所示:
图 10.6 – 与 RBM 相当的 AE 配置
我们可以使用我们在第七章中讲解的工具,自动编码器,来建模和训练我们的 AE,如下所示:
from tensorflow.keras.layers import Input, Dense
from tensorflow.keras.models import Model
inpt_dim = 28*28 # 784 dimensions
ltnt_dim = 100 # 100 components
inpt_vec = Input(shape=(inpt_dim,))
encoder = Dense(ltnt_dim, activation='sigmoid') (inpt_vec)
latent_ncdr = Model(inpt_vec, encoder)
decoder = Dense(inpt_dim, activation='sigmoid') (encoder)
autoencoder = Model(inpt_vec, decoder)
autoencoder.compile(loss='binary_crossentropy', optimizer='adam')
autoencoder.fit(x_train, x_train, epochs=200, batch_size=1000)
这里没有什么新东西,除了我们只用两个足够大的密集层进行训练,这些层足以提供很好的表示。图 10.7 展示了在测试集上学习到的表示的 UMAP 可视化:
图 10.7 – 使用 UMAP 可视化的 AE 引导表示
上述图像是通过以下代码生成的:
import matplotlib.pyplot as plt
import umap
y_ = list(map(int, y_test))
X_ = latent_ncdr.predict(x_test)
X_ = umap.UMAP().fit_transform(X_)
plt.figure(figsize=(10,8))
plt.title('UMAP of 100 AE Learned Components on MNIST')
plt.scatter(X_[:,0], X_[:,1], s=5.0, c=y_, alpha=0.75, cmap='tab10')
plt.xlabel('$z_1$')
plt.ylabel('$z_2$')
plt.colorbar()
从图 10.7中可以看到,数据聚类得非常好;尽管这些聚类比图 10.3中的更为接近,但聚类内部的分离似乎更好。与 RBM 类似,我们也可以可视化所学习到的权重。
tensorflow.keras
中的每个Model
对象都有一个名为get_weights()
的方法,可以检索每一层的所有权重列表。让我们运行一下这个:
latent_ncdr.get_weights()[0]
它使我们能够访问第一层的权重,并允许我们像可视化 RBM 权重一样可视化它们。图 10.8 显示了学习到的权重:
图 10.8 – AE 权重
与图 10.4中的 RBM 权重相比,图 10.8中展示的权重没有明显的数字特定特征。这些特征似乎集中在非常独特的区域,表现为纹理和边缘。看到这一点非常有趣,因为它表明,根本不同的模型会产生根本不同的潜在空间。
如果 RBM 和 AE 都能产生有趣的潜在空间,想象一下如果我们在深度学习项目中同时使用它们会取得什么成果!尝试一下吧!
最后,为了证明 AE 能够实现高质量的重建,如模型所示,我们可以看看图 10.9:
图 10.9 – AE 输入(顶部一行)和重建结果(底部一行)
使用 100 个组件进行重建的结果似乎具有高质量,如图 10.9所示。然而,这对于 RBM 来说是不可能的,因为它们的目的并非一定是重建数据,正如我们在本章中解释的那样。
摘要
本章中级内容向你展示了 RBM 的基本理论及其应用。我们特别关注了一个伯努利 RBM,它处理可能遵循伯努利分布的输入数据,从而实现快速学习和高效计算。我们使用了 MNIST 数据集来展示 RBM 学习到的表示有多么有趣,并且可视化了学习到的权重。最后,我们通过将 RBM 与一个非常简单的 AE 进行比较,表明两者都学习到了高质量的潜在空间,尽管它们是根本不同的模型。
到目前为止,你应该能够实现自己的 RBM 模型,可视化其学习到的组件,并通过投影(转换)输入数据,观察隐藏层投影,来看学习到的潜在空间。你应该能够自信地在大型数据集(如 MNIST)上使用 RBM,甚至与 AE 进行比较。
下一章是关于监督深度学习的新一组章节的开始。第十一章,深度和宽度神经网络,将带领我们进入一系列围绕监督深度学习的令人兴奋的新话题。该章节将解释深度神经网络与宽度神经网络在监督学习中的性能差异及其复杂性。它将介绍密集网络和稀疏网络的概念,重点是神经元之间的连接。你不能错过!
问题与答案
- 为什么我们不能用 RBM 进行数据重建?
RBM 与 AE 从根本上是不同的。RBM 旨在优化能量函数,而 AE 旨在优化数据重建函数。因此,我们不能使用 RBM 进行重建。然而,这一根本差异带来了新的潜在空间,这些潜在空间既有趣又强大。
- 我们可以给 RBM 添加更多层吗?
不是的。在这里呈现的当前模型中并不适用。神经元堆叠层的概念更适合深度 AE 的概念。
- 那 RBM 到底有什么酷的地方呢?
它们简单。它们快速。它们提供丰富的潜在空间。到目前为止没有可比拟的对手。最接近的竞争者是 AEs。
参考文献
-
Hinton, G. E., 和 Sejnowski, T. J.(1983 年 6 月)。最优感知推理。在IEEE 计算机视觉与模式识别会议论文集(第 448 卷)中。IEEE 纽约。
-
Brooks, S., Gelman, A., Jones, G., 和 Meng, X. L.(主编)。(2011)。马尔科夫链蒙特卡罗手册。CRC 出版社。
-
Tieleman, T.(2008 年 7 月)。使用似然梯度的近似方法训练限制玻尔兹曼机。载于第 25 届国际机器学习会议论文集(第 1064-1071 页)。
-
Yamashita, T.、Tanaka, M.、Yoshida, E.、Yamauchi, Y. 和 Fujiyoshii, H.(2014 年 8 月)。在限制玻尔兹曼机中,选择伯努利分布还是高斯分布。载于 2014 年第 22 届国际模式识别会议(第 1520-1525 页)。IEEE。
-
Tieleman, T. 和 Hinton, G.(2009 年 6 月)。利用快速权重提高持久对比散度。载于第 26 届年国际机器学习会议论文集(第 1033-1040 页)。
第十四章
第三章:监督式深度学习
完成本章后,你将能够实现基本和高级的深度学习模型,用于分类、回归以及基于学习到的潜在空间生成数据。
本章包括以下章节:
-
第十一章,深度与广度神经网络
-
第十二章,卷积神经网络
-
第十三章,递归神经网络
-
第十四章,生成对抗网络
-
第十五章,关于深度学习未来的最终讨论
深度与宽神经网络
到目前为止,我们已经涵盖了多种无监督深度学习方法,这些方法可以应用于许多有趣的领域,例如特征提取、信息压缩和数据增强。然而,当我们转向可以执行分类或回归等任务的监督式深度学习方法时,我们必须首先解决一个与神经网络相关的重要问题,这个问题你可能已经在思考了:宽神经网络和深神经网络之间有什么区别?
在本章中,你将实现深度神经网络和宽神经网络,以观察两者在性能和复杂性上的差异。作为额外内容,我们还将讨论稠密网络和稀疏网络之间在神经元连接方面的概念。我们还将优化网络中的丢弃率,以最大化网络的泛化能力,这是今天必须掌握的一项关键技能。
本章结构如下:
-
宽神经网络
-
稠密深度神经网络
-
稀疏深度神经网络
-
超参数优化
第十五章:宽神经网络
在我们讨论本章涉及的神经网络类型之前,可能有必要重新审视一下深度学习的定义,然后继续介绍所有这些类型。
深度学习回顾
最近,在 2020 年 2 月 9 日,图灵奖得主 Yann LeCun 在纽约市的 AAAI-20 会议上做了一个有趣的演讲。在演讲中,他清晰地阐明了深度学习的定义,在我们在此给出这个定义之前,让我提醒你,LeCun(与 J. Bengio 和 G. Hinton 一起)被认为是深度学习的奠基人之一,并且正是因其在该领域的成就而获得了图灵奖。因此,他的观点非常重要。其次,在本书中,我们没有给出深度学习的明确定义;人们可能认为它指的是深度神经网络,但这并不准确——它远不止于此,所以让我们彻底澄清这个问题。
“这不仅仅是监督学习,这不仅仅是神经网络,深度学习是通过将参数化模块组装成(可能是动态的)计算图,并通过使用基于梯度的方法优化参数来训练它执行任务的一种理念。” —— Yann LeCun
到目前为止,我们覆盖的大多数模型都符合这个定义,除了我们用来解释更复杂模型的简单入门模型。之所以这些入门模型不被视为深度学习,是因为它们不一定是计算图的一部分;我们具体指的是感知器(Rosenblatt, F. (1958)),以及相应的感知器学习算法(PLA)(Muselli, M. (1997))。然而,从多层感知器(MLP)开始,到目前为止展示的所有算法实际上都是深度学习算法。
在这一点上做出这个重要的区分是必要的,因为这是一本深度学习书籍,而你正在学习深度学习。我们即将学习一些深度学习中最有趣的主题,我们需要聚焦于深度学习是什么。我们将讨论深度网络和宽网络;然而,两者都是深度学习。实际上,我们将在这里讨论的所有模型都是深度学习模型。
有了这个澄清之后,让我们定义一下什么是宽网络。
宽层
使神经网络变宽的原因是在相对较少的隐藏层中拥有相对较多的神经元。深度学习的最新发展甚至使得计算处理具有无限数量神经单元的宽网络成为可能(Novak, R., 等人,2019 年)。虽然这是该领域的一个重要进展,但我们会将层数限制为具有合理数量的单元。为了与较不宽的网络进行比较,我们将为 CIFAR-10 数据集创建一个宽网络。我们将创建如下图所示的架构:
图 11.1 – CIFAR-10 的宽网络架构
我们从现在开始要考虑神经网络的一个重要方面是参数的数量。
在深度学习中,参数的数量定义为学习算法需要通过梯度下降技术估计的变量数量,以便最小化损失函数。大多数参数通常是网络的权重;然而,其他参数可能包括偏置、批归一化的均值和标准差、卷积网络的滤波器、循环网络的记忆向量以及许多其他内容。
了解参数的数量特别重要,因为在理想情况下,你希望数据样本的数量大于你想要学习的变量数。换句话说,理想的学习场景应该包含比参数更多的数据。如果你仔细想想,这是直观的;假设有一个矩阵,包含两行三列。三列描述的是水果的红、绿、蓝三色的表示。两行对应的是一个橙子的样本和一个苹果的样本。如果你想建立一个线性回归系统来判断数据是否来自橙子,你肯定希望有更多的数据!尤其是因为有很多苹果,它们的颜色可能与橙子的颜色相似。更多的数据更好!但如果你有更多的参数,比如在线性回归中,参数的数量与列数相同,那么你的问题通常会被描述为不适定问题。在深度学习中,这种现象被称为过度参数化。
只有在深度学习中,过度参数化的模型才会表现得非常好。有研究表明,在神经网络的特定情况下,考虑到数据在非线性关系中的冗余性,损失函数可以生成平滑的地形(Soltanolkotabi, M., et al. (2018))。这尤其有趣,因为我们可以证明,过度参数化的深度学习模型在使用梯度下降时会收敛到非常好的解决方案(Du, S. S., et al. (2018))。
摘要
在 Keras 中,有一个名为 summary()
的函数,当从 Model
对象调用时,可以显示需要估算的总参数数量。例如,让我们创建图 Figure 11.1 中的宽网络:
from tensorflow.keras.layers import Input, Dense, Dropout
from tensorflow.keras.models import Model
inpt_dim = 32*32*3 # this corresponds to the dataset
# to be explained shortly
inpt_vec = Input(shape=(inpt_dim,), name='inpt_vec')
dl = Dropout(0.5, name='d1')(inpt_vec)
l1 = Dense(inpt_dim, activation='relu', name='l1')(dl)
d2 = Dropout(0.2, name='d2')(l1)
l2 = Dense(inpt_dim, activation='relu', name='l2') (d2)
output = Dense(10, activation='sigmoid', name='output') (l2)
widenet = Model(inpt_vec, output, name='widenet')
widenet.compile(loss='binary_crossentropy', optimizer='adam')
widenet.summary()
这段代码会产生以下输出:
Model: "widenet"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
inpt_vec (InputLayer) [(None, 3072)] 0
_________________________________________________________________
d1 (Dropout) (None, 3072) 0
_________________________________________________________________
l1 (Dense) (None, 3072) 9440256
_________________________________________________________________
d2 (Dropout) (None, 3072) 0
_________________________________________________________________
l2 (Dense) (None, 3072) 9440256
_________________________________________________________________
output (Dense) (None, 10) 30730
=================================================================
Total params: 18,911,242
Trainable params: 18,911,242
Non-trainable params: 0
此处生成的摘要表明模型中的参数总数为 18,911,242。这是为了说明,简单的宽网络在具有 3,072 个特征的问题中可以有近 1900 万个参数。这显然是一个过度参数化的模型,我们将在其上执行梯度下降来学习这些参数;换句话说,这是一个深度学习模型。
名称
本章将介绍的另一个新内容是使用 名称 来标识 Keras 模型中的各个组件。你应该注意到,在之前的代码中,脚本包含了一个新的参数并为其分配了一个字符串值;例如,Dropout(0.5, **name**='d1')
。这在内部用于跟踪模型中各个部分的名称。这是一个好的实践,但并不是强制要求的。如果不提供名称,Keras 会自动为每个组件分配一个通用名称。为元素分配名称在保存或恢复模型时(我们很快就会做这件事——请耐心等待),或在打印摘要时(如前所述)可能会很有帮助。
现在,让我们看看将要加载的数据集。准确地说,就是之前提到的具有 3,072 个维度的数据集,称为 CIFAR-10。
CIFAR-10 数据集
本章中我们将使用的数据集称为 CIFAR-10。它来源于 Canadian Institute For Advanced Research(CIFAR)的缩写。数字 10 来自数据集所包含的类别数量。它是一个彩色图像数据集,还有一个包含 100 种不同对象的替代数据库,称为 CIFAR-100;然而,我们现在将重点关注 CIFAR-10。每个彩色图像是 像素。考虑到颜色通道,它的总维度为
。
图 Figure 11.1 中的示意图有一个图像样本,而图 Figure 11.2 中则展示了测试集内每个类别的示例:
图 11.2 – CIFAR-10 数据集中每个类别的样本彩色图像
可以通过执行以下命令来加载该数据集:
from tensorflow.keras.datasets import cifar10
from tensorflow.keras.utils import to_categorical
import NumPy as np
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
# Makes images floats between [0,1]
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
# Reshapes images to make them vectors of 3072-dimensions
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:])))
# Converts list of numbers into one-hot encoded vectors of 10-dim
y_train = to_categorical(y_train, 10)
y_test = to_categorical(y_test, 10)
print('x_train shape:', x_train.shape)
print('x_test shape:', x_test.shape)
这会自动下载数据并产生以下输出:
x_train shape: (50000, 3072)
x_test shape: (10000, 3072)
这些内容除了数据集外,没什么新鲜的。关于数据集的准备方式,请参考第三章,数据准备,我们将在其中讲解如何通过标准化数据并将目标转换为独热编码来将数据转化为可用数据。
我们通过打印数据集的形状,使用 NumPy 数组的.shape
属性得到的输出显示,我们有 50,000 个样本用于训练,另外还有 10,000 个样本用于测试训练效果。这是深度学习社区的标准数据集划分方式,便于不同方法间的比较。
新的训练工具
通过到目前为止的代码,我们可以很容易地开始训练过程,只需像下面这样调用fit()
方法:
widenet.fit(x_train, y_train, epochs=100, batch_size=1000,
shuffle=True, validation_data=(x_test, y_test))
这些并不新颖,我们在第九章,变分自编码器中已经讲解过这些细节。然而,我们希望引入一些新的重要工具,这些工具将帮助我们更高效地训练更好的模型,并保存我们最优训练的模型。
保存或加载模型
保存训练好的模型很重要,如果我们想销售产品、分发工作架构,或控制模型的版本,这样的模型可以通过调用以下任一方法来保存:
-
save()
,用于保存整个模型,包括优化器的状态,例如梯度下降算法、迭代次数、学习率等。 -
save_weights()
,用于仅保存模型的参数。
例如,我们可以按以下方式保存模型的权重:
widenet.save_weights("widenet.hdf5")
这将会在本地磁盘上创建一个名为widenet.hdf5
的文件。这种文件扩展名对应一种叫做层次数据格式(HDF)的标准文件格式,它可以确保跨平台的一致性,因此,便于数据共享。
你可以稍后通过执行以下命令重新加载保存的模型:
widenet.load_weights("widenet.hdf5")
请注意,执行此操作前你必须先构建好模型,即按照精确的顺序创建所有模型层,并使用完全相同的名称。另一种替代方法是使用save()
方法来避免重新构建模型。
widenet.save("widenet.hdf5")
然而,使用save()
方法的缺点是,为了加载模型,你将需要导入一个额外的库,如下所示:
from tensorflow.keras.models import load_model
widenet = load_model("widenet.hdf5")
本质上,这样就不需要重新创建模型了。在本章中,我们将通过保存模型权重来让你习惯这一过程。现在,让我们来看一下如何使用回调函数,这是一种监控学习过程的有趣方法。我们将从一个用于降低学习率的回调开始。
动态降低学习率
Keras 有一个callbacks
的父类,位于tensorflow.keras.callbacks
中,在这里我们有其他有用的工具,其中包括一个用于减少学习率的类。如果你不记得学习率是什么,可以回到第六章,训练多层神经网络,复习一下这个概念。不过,简而言之,学习率控制了更新模型参数时沿梯度方向所采取的步长大小。
问题在于,很多时候,你会遇到某些深度学习模型在学习过程中陷入困境。我们所说的陷入困境是指在训练集或验证集上,损失函数没有取得任何进展。专业人士使用的技术术语是,学习看起来像是平台期。这是一个显而易见的问题,尤其是当你查看损失函数在多个训练轮次(epochs)中的变化时,因为它看起来像是一个平台期,也就是一条平坦的线。理想情况下,我们希望看到损失在每个训练轮次中都在下降,通常在前几个训练轮次中是这样的,但有时降低学习率能帮助学习算法通过对现有已学知识(即已学得的参数)做出小的调整来集中注意力,从而继续进步。
我们在这里讨论的类叫做ReduceLROnPlateau
。你可以按如下方式加载它:
from tensorflow.keras.callbacks import ReduceLROnPlateau
要使用这个库,你需要在定义后,在fit()
函数中使用callbacks
参数,如下所示:
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=20)
widenet.fit(x_train, y_train, batch_size=128, epochs=100,
callbacks=reduce_lr, shuffle=True,
validation_data=(x_test, y_test))
在这段代码中,我们使用以下参数调用ReduceLROnPlateau
:
-
monitor='val_loss'
,这是默认值,但你可以更改它以查看'loss'
曲线中的平台期。 -
factor=0.1
,这是默认值,表示学习率将被减少的比例。例如,Adam 优化器的默认学习率是 0.001,但当检测到平台期时,它会乘以 0.1,从而得到新的学习率 0.0001。 -
patience=20
,默认值是 10,表示在监视的损失没有改善的情况下,连续多少轮次会被认为是平台期。
这个方法中还有其他参数可以使用,但在我看来,这些是最常用的。
接下来,我们来看另一个重要的回调:提前停止。
提前停止学习过程
下一个回调很有趣,因为它允许你在没有进展的情况下停止训练,并且它允许你在学习过程中保留模型的最佳版本。它与前一个回调在同一个类中,名为EarlyStopping()
,你可以按如下方式加载它:
from tensorflow.keras.callbacks import EarlyStopping
提前停止回调本质上让你在指定的patience
参数指定的若干训练轮次内没有进展时停止训练过程。你可以按如下方式定义并使用提前停止回调:
stop_alg = EarlyStopping(monitor='val_loss', patience=100, restore_best_weights=True)
widenet.fit(x_train, y_train, batch_size=128, epochs=1000,
callbacks=stop_alg, shuffle=True,
validation_data=(x_test, y_test))
下面是 EarlyStopping()
中每个参数的简短解释:
-
monitor='val_loss'
,这是默认值,但你可以更改为查看'loss'
曲线的变化。 -
patience=100
,默认值为 10,是指在监控的损失没有改善的轮数。我个人喜欢将这个值设置为比ReduceLROnPlateau
中的耐心值更大的数值,因为我喜欢在终止学习过程之前,先让学习率在学习过程中产生一些改善(希望如此),而不是因为没有改善就提前结束。 -
restore_best_weights=True
,默认值为False
。如果为False
,则会保存最后一轮训练得到的模型权重。但是,如果设置为True
,它将保存并返回学习过程中最好的权重。
这个最后的参数是我个人最喜欢的,因为我可以将训练的轮数设置为一个合理的较大数值,让训练持续进行,直到它需要的时间为止。在前面的示例中,如果我们将训练轮数设置为 1,000,这并不意味着学习过程会进行 1,000 轮,但如果在 50 轮内没有进展,过程可以提前终止。如果过程已经达到了一个点,模型学到了良好的参数,但没有进一步的进展,那么可以在 50 轮后停止,并仍然返回在学习过程中记录下的最佳模型。
我们可以将所有前面的回调函数和保存方法结合如下:
from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=20)
stop_alg = EarlyStopping(monitor='val_loss', patience=100,
restore_best_weights=True)
hist = widenet.fit(x_train, y_train, batch_size=1000, epochs=1000,
callbacks=[stop_alg, reduce_lr], shuffle=True,
validation_data=(x_test, y_test))
widenet.save_weights("widenet.hdf5")
注意,回调函数已经被合并成一个回调列表,监控学习过程,寻找平稳期来降低学习率,或在几轮内没有改善时终止过程。同时,注意我们创建了一个新变量 hist
。这个变量包含一个字典,记录了学习过程中的日志,例如每轮的损失。我们可以绘制这些损失曲线,看看训练过程是如何进行的,如下所示:
import matplotlib.pyplot as plt
plt.plot(hist.history['loss'], color='#785ef0')
plt.plot(hist.history['val_loss'], color='#dc267f')
plt.title('Model reconstruction loss')
plt.ylabel('Binary Cross-Entropy Loss')
plt.xlabel('Epoch')
plt.legend(['Training Set', 'Test Set'], loc='upper right')
plt.show()
这将生成 图 11.3 中的曲线:
图 11.3 – 使用回调函数的 widenet 模型损失随轮次变化的曲线
从图中我们可以清楚地看到,在第 85 轮左右,学习率下降的证据,在验证损失(即测试集上的损失)平稳后进行调整;然而,这对验证损失几乎没有影响,因此训练在大约第 190 轮时提前终止,因为验证损失没有改善。
在下一节中,我们将以定量的方式分析 widenet
模型的性能,以便稍后进行比较。
结果
在这里,我们只是想以易于理解并能与他人沟通的方式来解释网络的性能。我们将重点分析模型的混淆矩阵、精度、召回率、F1 分数、准确率和均衡误差率。如果你不记得这些术语的含义,请返回并快速复习第四章,数据学习。
scikit-learn 的一个优点是它有一个自动化过程,可以生成一个分类性能报告,其中包括上述大部分术语。这个报告被称为分类报告。我们将需要的其他库可以在sklearn.metrics
类中找到,并可以按如下方式导入:
from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix
from sklearn.metrics import balanced_accuracy_score
这三种库的工作方式类似——它们使用真实标签和预测值来评估性能:
from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix
from sklearn.metrics import balanced_accuracy_score
import NumPy as np
y_hat = widenet.predict(x_test) # we take the neuron with maximum
y_pred = np.argmax(y_hat, axis=1) # output as our prediction
y_true = np.argmax(y_test, axis=1) # this is the ground truth
labels=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(classification_report(y_true, y_pred, labels=labels))
cm = confusion_matrix(y_true, y_pred, labels=labels)
print(cm)
ber = 1- balanced_accuracy_score(y_true, y_pred)
print('BER:', ber)
这段代码输出类似如下内容:
precision recall f1-score support
0 0.65 0.59 0.61 1000
1 0.65 0.68 0.67 1000
2 0.42 0.47 0.44 1000
3 0.39 0.37 0.38 1000
4 0.45 0.44 0.44 1000
5 0.53 0.35 0.42 1000
6 0.50 0.66 0.57 1000
7 0.66 0.58 0.62 1000
8 0.62 0.71 0.67 1000
9 0.60 0.57 0.58 1000
accuracy 0.54 10000
[[587 26 86 20 39 7 26 20 147 42]
[ 23 683 10 21 11 10 22 17 68 135]
[ 63 21 472 71 141 34 115 41 24 18]
[ 19 22 90 370 71 143 160 43 30 52]
[ 38 15 173 50 442 36 136 66 32 12]
[ 18 10 102 224 66 352 120 58 29 21]
[ 2 21 90 65 99 21 661 9 14 18]
[ 36 15 73 67 90 45 42 582 13 37]
[ 77 70 18 24 17 3 20 9 713 49]
[ 46 167 20 28 14 14 30 36 74 571]]
BER: 0.4567
顶部部分显示了classification_report()
的输出。它提供了模型的精度、召回率、F1 分数和准确率。理想情况下,我们希望这些数字尽可能接近 1.0。直观上,准确率需要达到 100%(或 1.0);然而,其他的数字则需要仔细研究。从这个报告中,我们可以观察到总准确率为 54%。从报告的其余部分,我们可以确定分类准确性较高的类别是 1 和 8,分别对应汽车和船只。类似地,我们可以看到分类最差的两个类别是 3 和 5,分别对应猫和狗。
虽然这些数字提供了信息,但我们可以通过查看混淆矩阵来探究混淆的来源,混淆矩阵是由confusion_matrix()
生成的一组数字。如果我们检查第四行(对应标签 3,猫)的混淆矩阵,我们可以看到它正确地将 370 只猫分类为猫,但 143 只猫被分类为狗,160 只猫被分类为青蛙,这些是最严重的混淆区域。另一种视觉化查看的方式如下图所示:
图 11.4 – widenet 模型的混淆矩阵可视化
理想情况下,我们希望看到一个对角线的混淆矩阵;然而,在这种情况下,我们并未看到这种效果。通过目视检查,从图 11.4中,我们可以观察到哪些类别的正确预测最少,并在视觉上确认混淆的地方。
最后,重要的是要注意,虽然分类准确率(ACC)为 54%,我们仍然需要验证平衡误差率(BER),以补充我们对准确率的了解。当类别分布不均时,特别重要,即某些类别的样本多于其他类别。如第四章《从数据中学习》中所解释,我们可以简单地计算平衡准确率并从中减去 1。这显示出 BER 为 0.4567,即 45.67%。在理想情况下,我们希望将 BER 降到零,绝对避免 BER 为 50%,这意味着模型的表现与随机猜测没有区别。
在这种情况下,模型的准确性并不令人印象深刻;然而,这是一个非常具有挑战性的分类问题,尤其对于全连接网络而言,因此,这一表现并不令人惊讶。接下来,我们将尝试做一个类似的实验,将网络从相对宽的网络改为深度网络,并对比结果。
稠密深度神经网络
众所周知,深度网络在分类任务中可以提供良好的性能(Liao, Q., 等,2018)。在本节中,我们希望构建一个深度稠密神经网络,并观察它在 CIFAR-10 数据集上的表现。我们将构建如下图所示的模型:
图 11.5 – CIFAR-10 深度稠密网络的网络架构
该模型的目标之一是与图 11.1中的宽网络具有相同数量的神经单元。该模型具有瓶颈架构,其中神经元的数量随着网络加深而减少。我们可以使用 Keras 的函数式方法以编程方式实现这一点,接下来我们将讨论。
构建和训练模型
关于 Keras 的函数式方法,一个有趣的事实是,在构建模型时,我们可以重复使用变量名,甚至可以通过循环构建模型。例如,假设我想创建具有 dropout 比例的稠密层,这些比例会随着神经元数量的增加而指数下降,下降的因子分别为 1.5 和 2。
我们可以通过一个循环来实现这一点,循环使用初始的 dropout 比率 dr
和初始的神经单元数量 units
,每次分别按因子 1.5 和 2 递减,前提是神经单元的数量始终大于 10;我们在 10 停止,因为最后一层将包含 10 个神经元,每个类别一个。大致如下所示:
while units > 10:
dl = Dropout(dr)(dl)
dl = Dense(units, activation='relu')(dl)
units = units//2
dr = dr/1.5
前面的代码片段说明了我们可以重用变量而不至于让 Python 混淆,因为 TensorFlow 在计算图上操作,能够正确解决图中各部分的顺序。代码还显示了我们可以非常轻松地创建一个瓶颈型网络,其中单元数和 dropout 比例按指数衰减。
构建此模型的完整代码如下:
# Dimensionality of input for CIFAR-10
inpt_dim = 32*32*3
inpt_vec = Input(shape=(inpt_dim,))
units = inpt_dim # Initial number of neurons
dr = 0.5 # Initial drop out rate
dl = Dropout(dr)(inpt_vec)
dl = Dense(units, activation='relu')(dl)
# Iterative creation of bottleneck layers
units = units//2
dr = dr/2
while units>10:
dl = Dropout(dr)(dl)
dl = Dense(units, activation='relu')(dl)
units = units//2
dr = dr/1.5
# Output layer
output = Dense(10, activation='sigmoid')(dl)
deepnet = Model(inpt_vec, output)
编译并训练模型的过程如下:
deepnet.compile(loss='binary_crossentropy', optimizer='adam')
deepnet.summary()
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=20,
min_delta=1e-4, mode='min')
stop_alg = EarlyStopping(monitor='val_loss', patience=100,
restore_best_weights=True)
hist = deepnet.fit(x_train, y_train, batch_size=1000, epochs=1000,
callbacks=[stop_alg, reduce_lr], shuffle=True,
validation_data=(x_test, y_test))
deepnet.save_weights("deepnet.hdf5")
这会产生以下输出,这是由前面的deepnet.summary()
产生的:
Model: "model"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) [(None, 3072)] 0
_________________________________________________________________
dropout (Dropout) (None, 3072) 0
_________________________________________________________________
dense (Dense) (None, 3072) 9440256
_________________________________________________________________
.
.
.
_________________________________________________________________
dense_8 (Dense) (None, 12) 300
_________________________________________________________________
dense_9 (Dense) (None, 10) 130
=================================================================
Total params: 15,734,806
Trainable params: 15,734,806
Non-trainable params: 0
如前面的总结所示,且在图 11.5中也展示了,该模型的总参数数量为15,734,806。这确认了该模型是一个过度参数化的模型。打印的总结还展示了在没有特定名称的情况下,模型的各个部分是如何命名的;即它们都基于类名和连续的编号获得一个通用名称。
fit()
方法用于训练深度模型,当我们像之前在图 11.3中一样绘制训练记录在hist
变量中的内容时,我们得到以下图像:
图 11.6 – 使用回调函数在不同 epoch 下的深度网络损失
从图 11.6中可以看出,深度网络在大约 200 个 epoch 后停止训练,训练集和测试集在大约第 70 个 epoch 交叉,之后模型开始对训练集过拟合。如果我们将这个结果与图 11.3中的宽网络结果进行比较,可以看到模型大约在第 55 个 epoch 开始过拟合。
现在我们来讨论该模型的定量结果。
结果
如果我们以与宽网络相同的方式生成分类报告,我们得到如下结果:
precision recall f1-score support
0 0.58 0.63 0.60 1000
1 0.66 0.68 0.67 1000
2 0.41 0.42 0.41 1000
3 0.38 0.35 0.36 1000
4 0.41 0.50 0.45 1000
5 0.51 0.36 0.42 1000
6 0.50 0.63 0.56 1000
7 0.67 0.56 0.61 1000
8 0.65 0.67 0.66 1000
9 0.62 0.56 0.59 1000
accuracy 0.53 10000
[[627 22 62 19 45 10 25 18 132 40]
[ 38 677 18 36 13 10 20 13 55 120]
[ 85 12 418 82 182 45 99 38 23 16]
[ 34 14 105 347 89 147 161 50 17 36]
[ 58 12 158 34 496 29 126 55 23 9]
[ 25 7 108 213 91 358 100 54 23 21]
[ 9 15 84 68 124 26 631 7 11 25]
[ 42 23 48 58 114 57 61 555 10 32]
[110 75 16 22 30 11 8 5 671 52]
[ 51 171 14 34 16 9 36 36 69 564]]
BER 0.4656
这表明与宽网络的结果相当,其中我们得到了 0.4567 的 BER,这代表了 0.0089 的差异,偏向于宽网络,这在此情况下并不代表显著差异。我们可以通过查看前面的结果或下图所示的混淆矩阵,验证模型在特定类别上的分类性能也是可比的:
图 11.7 – 深度网络模型的混淆矩阵可视化
从前面的结果可以确认,最难分类的类别是第 3 类,猫,它们经常与狗混淆。同样,最容易分类的是第 1 类,船,它们经常与飞机混淆。但再一次,这与宽网络的结果一致。
另一种我们可以尝试的深度网络类型是促进网络权重稀疏性的网络,我们将在接下来讨论。
稀疏深度神经网络
稀疏网络可以在其架构的不同方面定义为sparse(Gripon, V., 和 Berrou, C., 2011)。然而,本节中我们将研究的特定稀疏性是指与网络的权重相关的稀疏性,即其参数。我们将检查每个具体的参数,看它是否接近于零(从计算的角度来看)。
当前,在 Keras 上通过 TensorFlow 施加权重稀疏性有三种方式,它们与向量范数的概念相关。如果我们看曼哈顿范数,或者欧几里得范数
,它们的定义如下:
,
这里,n 是向量中元素的数量 。正如你所看到的,简而言之,
-norm 会根据元素的绝对值将所有元素相加,而
-norm 会根据元素的平方值来相加。显然,如果两个范数都接近零,
,大多数元素很可能是零或接近零。在这里,个人选择是使用
-norm,因为与
相比,较大的向量会受到平方惩罚,从而避免特定神经元主导某些项。
Keras 包含这些工具在regularizers
类中:tf.keras.regularizers
。我们可以按如下方式导入它们:
-
-norm:
tf.keras.regularizers.l1(l=0.01)
-
-norm:
tf.keras.regularizers.l2(l=0.01)
这些正则化器应用于网络的损失函数,以最小化权重的范数。
正则化器是机器学习中用来表示一个术语或函数,它将元素提供给目标(损失)函数,或一般的优化问题(如梯度下降),以提供数值稳定性或促进问题的可行性。在这种情况下,正则化器通过防止某些权重值的爆炸,同时促进一般稀疏性,从而促进权重的稳定性。
参数l=0.01
是一个惩罚因子,它直接决定了最小化权重范数的重要性。换句话说,惩罚按如下方式应用:
因此,使用非常小的值,如l=0.0000001
,将对范数关注较少,而l=0.01
将在最小化损失函数时对范数给予更多关注。关键是:这个参数需要调整,因为如果网络太大,可能会有几百万个参数,这会导致范数看起来非常大,因此需要一个小的惩罚;而如果网络相对较小,则建议使用更大的惩罚。由于这个练习是在一个拥有超过 1500 万个参数的非常深的网络上进行的,我们将使用l=0.0001
的值。
让我们继续构建一个稀疏网络。
构建稀疏网络并训练它
要构建这个网络,我们将使用与图 11.5中显示的完全相同的架构,只是每个单独的密集层的声明将包含一个我们希望考虑该层权重的范数最小化的规范。请查看前一节的代码,并将其与以下代码进行比较,其中我们突出显示了差异:
# Dimensionality of input for CIFAR-10
inpt_dim = 32*32*3
inpt_vec = Input(shape=(inpt_dim,))
units = inpt_dim # Initial number of neurons
dr = 0.5 # Initial drop out rate
dl = Dropout(dr)(inpt_vec)
dl = Dense(units, activation='relu',
kernel_regularizer=regularizers.l2(0.0001))(dl)
# Iterative creation of bottleneck layers
units = units//2
dr = dr/2
while units>10:
dl = Dropout(dr)(dl)
dl = Dense(units, activation='relu',
kernel_regularizer=regularizers.l2(0.0001))(dl)
units = units//2
dr = dr/1.5
# Output layer
output = Dense(10, activation='sigmoid',
kernel_regularizer=regularizers.l2(0.0001))(dl)
sparsenet = Model(inpt_vec, output)
编译和训练模型是相同的,像这样:
sparsenet.compile(loss='binary_crossentropy', optimizer='adam')
sparsenet.summary()
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=20,
min_delta=1e-4, mode='min')
stop_alg = EarlyStopping(monitor='val_loss', patience=100,
restore_best_weights=True)
hist = sparsenet.fit(x_train, y_train, batch_size=1000, epochs=1000,
callbacks=[stop_alg, reduce_lr], shuffle=True,
validation_data=(x_test, y_test))
sparsenet.save_weights("sparsenet.hdf5")
sparsenet.summary()
的输出与前一节中的 deepnet.summary()
完全相同,因此我们这里不再重复。但是,我们可以看一下当损失最小化时的训练曲线 - 请参见以下图:
图 11.8 - 稀疏网络模型跨时期的损失函数优化
从图中可以看出,训练集和测试集的两条曲线在大约第 120 个时期之前非常接近最小值,之后开始偏离,模型开始过拟合。与图 11.3和图 11.6中的先前模型相比,我们可以看到这个模型的训练速度可以稍慢一些,仍然可以实现相对收敛。然而,请注意,虽然损失函数仍然是二元交叉熵,但模型也在最小化-范数,使得这种特定的损失函数与先前的不可直接比较。
现在让我们讨论该模型的定量结果。
结果
当我们进行性能的定量分析时,我们可以看出该模型与先前的模型相比是可比的。在 BER 方面有轻微的增益;然而,这并不足以宣布胜利,并且问题解决的任何手段都不足以解决问题 - 请参阅以下分析:
precision recall f1-score support
0 0.63 0.64 0.64 1000
1 0.71 0.66 0.68 1000
2 0.39 0.43 0.41 1000
3 0.37 0.23 0.29 1000
4 0.46 0.45 0.45 1000
5 0.47 0.50 0.49 1000
6 0.49 0.71 0.58 1000
7 0.70 0.61 0.65 1000
8 0.63 0.76 0.69 1000
9 0.69 0.54 0.60 1000
accuracy 0.55 10000
[[638 17 99 7 27 13 27 10 137 25]
[ 40 658 11 32 11 7 21 12 110 98]
[ 78 11 431 34 169 93 126 31 19 8]
[ 18 15 96 233 52 282 220 46 14 24]
[ 47 3 191 23 448 36 162 57 28 5]
[ 17 6 124 138 38 502 101 47 16 11]
[ 0 9 59 51 111 28 715 8 13 6]
[ 40 1 66 50 85 68 42 608 12 28]
[ 76 45 18 16 25 8 22 5 755 30]
[ 51 165 12 38 6 23 29 43 98 535]]
BER 0.4477
我们可以清楚地得出结论,与本章讨论的其他模型相比,该模型在性能上并不更差。事实上,对以下图中显示的混淆矩阵的仔细检查表明,这个网络在性质相似的对象方面也会产生类似的错误:
图 11.9 - 稀疏网络模型的混淆矩阵
现在,由于很难理解到目前为止我们讨论的模型之间的差异 - 广泛、深入和稀疏,我们可以计算并绘制每个训练模型权重的范数,如下图所示:
图 11.10 - 训练模型的累积范数权重
该图显示了按-范数进行的计算,从而使得数值足够接近以便进行评估;在横轴上是层数,纵轴上是随着网络层数增加的累积范数。这是我们可以欣赏到不同网络在其参数上的差异之处。在稀疏网络中,累积范数比其他网络要小得多(大约是其他网络的四到五倍)。对于那些可能被实现到芯片上或其他零权重能在生产中带来高效计算的应用来说,这可能是一个有趣且重要的特征(Wang, P. 等,2018)。
虽然网络权重受范数影响的程度可以通过超参数优化技术进行实验确定,但通常更常见的是确定其他参数,例如丢弃率、神经单元数量等,这些将在下一节讨论。
超参数优化
有一些方法可以用来优化参数;例如,有些是基于梯度的(Rivas, P. 等,2014;Maclaurin, D. 等,2015),而其他则是贝叶斯方法(Feurer, M. 等,2015)。然而,至今还没有一种通用的方法能够既极其有效又高效——通常你只能获得其中之一。你可以在这里阅读更多关于其他算法的内容(Bergstra, J. S. 等,2011*)。
对于该领域的任何初学者来说,最好从一些简单且容易记住的东西入手,例如随机搜索(Bergstra, J. & Bengio, Y. 2012*)或网格搜索。这两种方法非常相似,虽然我们这里重点讨论网格搜索,但这两者的实现非常相似。
库和参数
我们将需要使用两个我们之前未涉及过的主要库:GridSearchCV
,用于执行带有交叉验证的网格搜索,和KerasClassifier
,用于创建可以与 scikit-learn 通信的 Keras 分类器。
两个库可以如下导入:
from sklearn.model_selection import GridSearchCV
from tensorflow.keras.wrappers.scikit_learn import KerasClassifier
我们将优化的超参数(及其可能的值)如下:
-
丢弃率:
0.2
,0.5
-
优化器:
rmsprop
,adam
-
学习率:
0.01
,0.0001
-
隐藏层中的神经元数量:
1024
,512
,256
总的来说,超参数的可能组合是 2x2x2x3=24。这是四维网格中的选项总数。替代方案的数量可以更大、更全面,但请记住:为了简单起见,我们在这个例子中保持简单。此外,由于我们将应用交叉验证,你需要将可能的组合数乘以交叉验证中的划分数量,这样就能得出为确定最佳超参数组合所执行的完整端到端训练会话的数量。
请注意你将在网格搜索中尝试的选项数量,因为所有这些选项都会被测试,对于较大的网络和数据集,这可能会花费很多时间。随着经验的积累,你将能够通过思考所定义的架构来选择更小的参数集。
完整的实现将在下一节讨论。
实现与结果
这里展示了网格搜索的完整代码,但请注意,这些内容大多是重复的,因为它是基于本章前面讨论的宽网络模型进行建模的:
from sklearn.model_selection import GridSearchCV
from tensorflow.keras.wrappers.scikit_learn import KerasClassifier
from tensorflow.keras.layers import Input, Dense, Dropout
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam, RMSprop
from tensorflow.keras.datasets import cifar10
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping
import NumPy as np
# load and prepare data (same as before)
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
x_train = x_train.astype('float32') / 255.0
x_test = x_test.astype('float32') / 255.0
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:])))
y_train = to_categorical(y_train, 10)
y_test = to_categorical(y_test, 10)
我们声明一个方法来构建模型并返回它,代码如下:
# A KerasClassifier will use this to create a model on the fly
def make_widenet(dr=0.0, optimizer='adam', lr=0.001, units=128):
# This is a wide architecture
inpt_dim = 32*32*3
inpt_vec = Input(shape=(inpt_dim,))
dl = Dropout(dr)(inpt_vec)
l1 = Dense(units, activation='relu')(dl)
dl = Dropout(dr)(l1)
l2 = Dense(units, activation='relu') (dl)
output = Dense(10, activation='sigmoid') (l2)
widenet = Model(inpt_vec, output)
# Our loss and lr depends on the choice
if optimizer == 'adam':
optmzr = Adam(learning_rate=lr)
else:
optmzr = RMSprop(learning_rate=lr)
widenet.compile(loss='binary_crossentropy', optimizer=optmzr,
metrics=['accuracy'])
return widenet
然后我们将各部分拼接在一起,搜索参数,并按如下方式进行训练:
# This defines the model architecture
kc = KerasClassifier(build_fn=make_widenet, epochs=100, batch_size=1000,
verbose=0)
# This sets the grid search parameters
grid_space = dict(dr=[0.2, 0.5], # Dropout rates
optimizer=['adam', 'rmsprop'],
lr=[0.01, 0.0001], # Learning rates
units=[1024, 512, 256])
gscv = GridSearchCV(estimator=kc, param_grid=grid_space, n_jobs=1, cv=3, verbose=2)
gscv_res = gscv.fit(x_train, y_train, validation_split=0.3,
callbacks=[EarlyStopping(monitor='val_loss',
patience=20,
restore_best_weights=True),
ReduceLROnPlateau(monitor='val_loss',
factor=0.5, patience=10)])
# Print the dictionary with the best parameters found:
print(gscv_res.best_params_)
这将打印出几行,每一行对应交叉验证运行一次。我们在这里省略了很多输出,只是为了向你展示它的样子,但如果你愿意,可以手动调整输出的详细程度:
Fitting 3 folds for each of 24 candidates, totalling 72 fits
[CV] dr=0.2, lr=0.01, optimizer=adam, units=1024 .....................
[Parallel(n_jobs=1)]: Using backend SequentialBackend with 1 concurrent workers.
[CV] ...... dr=0.2, lr=0.01, optimizer=adam, units=1024, total= 21.1s
[CV] dr=0.2, lr=0.01, optimizer=adam, units=1024 .....................
[Parallel(n_jobs=1)]: Done 1 out of 1 | elapsed: 21.1s remaining: 0.0s
[CV] ...... dr=0.2, lr=0.01, optimizer=adam, units=1024, total= 21.8s
[CV] dr=0.2, lr=0.01, optimizer=adam, units=1024 .....................
[CV] ...... dr=0.2, lr=0.01, optimizer=adam, units=1024, total= 12.6s
[CV] dr=0.2, lr=0.01, optimizer=adam, units=512 ......................
[CV] ....... dr=0.2, lr=0.01, optimizer=adam, units=512, total= 25.4s
.
.
.
[CV] .. dr=0.5, lr=0.0001, optimizer=rmsprop, units=256, total= 9.4s
[CV] dr=0.5, lr=0.0001, optimizer=rmsprop, units=256 .................
[CV] .. dr=0.5, lr=0.0001, optimizer=rmsprop, units=256, total= 27.2s
[Parallel(n_jobs=1)]: Done 72 out of 72 | elapsed: 28.0min finished
{'dr': 0.2, 'lr': 0.0001, 'optimizer': 'adam', 'units': 1024}
最后一行是你最宝贵的信息,因为它是最佳参数组合,能带来最佳的结果。现在,你可以使用这些优化后的参数来更改你原始的宽网络实现,看看性能变化。你应该会看到平均准确率提升约 5%,这并不差!
或者,你可以尝试更大范围的参数集,或者增加交叉验证的分割数。可能性是无穷的。你应该始终尝试优化模型中的参数数量,原因如下:
-
它让你对自己的模型充满信心。
-
它让你的客户对你充满信心。
-
它向世界表明你是一个专业人士。
做得好!是时候总结一下了。
总结
本章讨论了神经网络的不同实现方式,即宽网络、深度网络和稀疏网络实现。阅读完本章后,你应该理解设计的差异以及它们如何影响性能或训练时间。此时,你应该能够理解这些架构的简洁性以及它们如何为我们迄今为止讨论的其他方法提供新的替代方案。本章中,你还学习了如何优化模型的超参数,例如,丢弃率,以最大化网络的泛化能力。
我相信你注意到这些模型的准确率超过了随机机会,也就是说,> 50%;然而,我们讨论的问题是一个非常难以解决的问题,你可能不会感到惊讶,像我们在这里研究的一般神经网络架构并没有表现得特别出色。为了获得更好的性能,我们可以使用一种更为专门的架构,旨在解决输入具有高度空间相关性的问题,比如图像处理。一种专门的架构类型被称为卷积神经网络(CNN)。
我们的下一个章节,第十二章,卷积神经网络,将详细讨论这一点。你将能够看到从通用模型转向更具领域特定性的模型所带来的差异有多大。你不能错过即将到来的这一章。但在你继续之前,请尝试用以下问题进行自我测验。
问题与答案
- 宽网络和深网络之间的性能有显著差异吗?
在我们这里研究的案例中并没有太多。然而,你必须记住的一点是,两个网络学到的输入的东西或方面是根本不同的。因此,在其他应用中,性能可能会有所不同。
- 深度学习是否等同于深度神经网络?
不,深度学习是机器学习的一个领域,专注于使用新颖的梯度下降技术训练过参数化模型的所有算法。深度神经网络是具有多个隐藏层的网络。因此,深度网络就是深度学习。但是,深度学习并不专属于深度网络。
- 你能举一个稀疏网络被需要的例子吗?
让我们思考一下机器人技术。在这个领域,大多数东西运行在具有内存约束、存储约束和计算能力约束的微芯片上;找到大部分权重为零的神经架构意味着你不必计算这些乘积。这意味着拥有可以存储在更小空间中、加载更快、计算更快的权重。其他可能的应用包括物联网设备、智能手机、智能车辆、智慧城市、执法等。
- 我们如何使这些模型表现得更好?
我们可以通过包括更多选项来进一步优化超参数。我们可以使用自编码器来预处理输入。但最有效的做法是切换到 CNN 来解决这个问题,因为 CNN 在图像分类上尤其擅长。请参见下一章。
参考文献
-
Rosenblatt, F. (1958). 感知机:大脑中信息存储与组织的概率模型。心理学评论,65(6),386。
-
Muselli, M. (1997). 关于口袋算法收敛性的性质。IEEE 神经网络学报,8(3),623-629。
-
Novak, R., Xiao, L., Hron, J., Lee, J., Alemi, A. A., Sohl-Dickstein, J., & Schoenholz, S. S. (2019). 神经切线:Python 中快速且简单的无限神经网络。arXiv 预印本 arXiv:1912.02803。
-
Soltanolkotabi, M., Javanmard, A., & Lee, J. D. (2018). 关于过参数化浅层神经网络优化景观的理论见解。IEEE 信息论学报,65(2),742-769。
-
Du, S. S., Zhai, X., Poczos, B., & Singh, A. (2018). 梯度下降可以证明优化过参数化神经网络。arXiv 预印本 arXiv:1810.02054。
-
Liao, Q., Miranda, B., Banburski, A., Hidary, J., & Poggio, T. (2018). 一个令人惊讶的线性关系预测深度网络的测试性能。arXiv 预印本 arXiv:1807.09659。
-
Gripon, V., & Berrou, C. (2011). 具有大规模学习多样性的稀疏神经网络。《IEEE 神经网络学报》,22(7),1087-1096。
-
Wang, P., Ji, Y., Hong, C., Lyu, Y., Wang, D., & Xie, Y. (2018 年 6 月). SNrram:一种基于电阻式随机存取存储器的高效稀疏神经网络计算架构。载于2018 年第 55 届 ACM/ESDA/IEEE 设计自动化大会(DAC)(第 1-6 页)。IEEE。
-
Rivas-Perea, P., Cota-Ruiz, J., & Rosiles, J. G. (2014). 一种用于 lp-svr 超参数选择的非线性最小二乘准牛顿策略。《机器学习与网络科学国际期刊》,5(4),579-597。
-
Maclaurin, D., Duvenaud, D., & Adams, R. (2015 年 6 月). 通过可逆学习进行基于梯度的超参数优化。载于国际机器学习大会(第 2113-2122 页)。
-
Feurer, M., Springenberg, J. T., & Hutter, F. (2015 年 2 月). 通过元学习初始化贝叶斯超参数优化。载于第二十九届 AAAI 人工智能大会。
-
Bergstra, J., & Bengio, Y. (2012). 随机搜索超参数优化。《机器学习研究期刊》,13(1),281-305。
-
Bergstra, J. S., Bardenet, R., Bengio, Y., & Kégl, B. (2011). 超参数优化算法。载于神经信息处理系统进展(第 2546-2554 页)。
卷积神经网络
本章介绍了卷积神经网络,从卷积操作开始,逐步介绍卷积操作的集成层,目的是学习在数据集上操作的过滤器。接着介绍了池化策略,展示了这些变化如何提高模型的训练和性能。最后,展示了如何可视化学习到的过滤器。
到本章结束时,你将熟悉卷积神经网络背后的动机,并且知道卷积操作是如何在一维和二维中工作的。完成本章后,你将知道如何在层中实现卷积,以通过梯度下降学习过滤器。最后,你将有机会使用之前学过的许多工具,包括 dropout 和批量归一化,但现在你将知道如何使用池化作为减少问题维度并创建信息抽象层次的替代方法。
本章的结构如下:
-
卷积神经网络简介
-
n维度的卷积
-
卷积层
-
池化策略
-
过滤器的可视化
第十六章:卷积神经网络简介
在第十一章《深度和广度神经网络》中,我们使用了一个对通用网络非常具有挑战性的数据集。然而,卷积神经网络(CNNs)将证明更加有效,正如你将看到的那样。CNNs 自 80 年代末以来就已经存在(LeCun, Y., 等人(1989))。它们已经改变了计算机视觉和音频处理的世界(Li, Y. D., 等人(2016))。如果你的智能手机有某种基于 AI 的物体识别功能,很可能它使用了某种 CNN 架构,例如:
-
图像中的物体识别
-
数字指纹的识别
-
语音命令的识别
CNNs 之所以有趣,是因为它们解决了一些计算机视觉中最具挑战性的问题,包括在一个叫做 ImageNet 的图像识别问题上击败人类(Krizhevsky, A., 等人(2012))。如果你能想到最复杂的物体识别任务,CNNs 应该是你实验的首选:它们永远不会让你失望!
CNN 成功的关键在于它们独特的编码空间关系的能力。如果我们对比两个不同的数据集,一个是关于学生的学校记录,包括当前和过去的成绩、出勤、在线活动等,另一个是关于猫和狗的图像数据集。如果我们的目标是对学生或猫和狗进行分类,这些数据是不同的。在一个数据集中,我们有学生特征,但这些特征没有空间关系。
例如,如果成绩是第一个特征,出勤率并不需要紧跟其后,因此它们的位置可以互换,而不会影响分类性能,对吗?然而,对于猫和狗的图像,眼睛的特征(像素)必须紧邻鼻子或耳朵;如果你改变空间特征并在两只眼睛中间看到一个耳朵(很奇怪),分类器的性能应该会受到影响,因为通常没有猫或狗的眼睛中间有耳朵。这就是 CNN 擅长编码的空间关系类型。你也可以考虑音频或语音处理。你知道某些声音在特定单词中必须在其他声音之后出现。如果数据集允许空间关系,CNN 有潜力表现得很好。
n 维卷积
CNN 的名称来源于它们的标志性操作:卷积。这是一种在信号处理领域非常常见的数学运算。接下来,我们来讨论一下卷积操作。
一维卷积
让我们从一维离散时间卷积函数开始。假设我们有输入数据!和一些权重!,我们可以定义这两个之间的离散时间卷积操作如下:
。
在这个方程中,卷积操作用*****符号表示。为了不让事情变得过于复杂,我们可以说!是反转的,!,然后是平移的,!。得到的结果是!,它可以被解释为应用滤波器!后输入的过滤版本。
图 12.1展示了通过反转和平移滤波器并在输入数据上进行相乘来获得此结果的每个步骤:
图 12.1 - 涉及两个向量的卷积操作示例
在 NumPy 中,我们可以通过使用convolve()
方法来实现这一点,代码如下:
import numpy as np
h = np.convolve([2, 3, 2], [-1, 2, -1])
print(h)
这将输出以下结果:
[-2, 1, 2, 1, -2]
现在,如果你想一下,最“完整”的信息是当滤波器完全与输入数据重叠时,这对于!是成立的。在 Python 中,你可以通过使用'valid'
参数来得到这个效果,代码如下:
import numpy as np
h = np.convolve([2, 3, 2], [-1, 2, -1], 'valid')
print(h)
这将给出以下结果:
2
再次强调,这只是为了最大化相关信息,因为卷积操作在向量的边缘,即向量开始和结束时,不完全重叠,因此不确定性更高。此外,为了方便,我们可以通过使用'same'
参数,获得与输入相同大小的输出向量,方法如下:
import numpy as np
h = np.convolve([2, 3, 2], [-1, 2, -1], 'same')
print(h)
这将输出如下内容:
[1 2 1]
以下是使用卷积的三种方式的实际原因:
-
当你需要所有有效信息而不包含由于滤波器部分重叠所引起的噪声时,请使用
'valid'
。 -
当你希望计算更加简单时,请使用
'same'
。这将使得输入和输出的维度保持一致,计算也会更为方便。 -
否则,可以不使用任何方法来获得卷积操作的完整解析解,以满足您的任何需求。
卷积随着微处理器的发展而变得非常流行,这些微处理器专门用于极快地进行乘法和加法运算,同时随着快速傅里叶变换(FFT)算法的发展,卷积的应用也变得广泛。FFT 利用了一个数学性质,即离散时间域中的卷积等价于傅里叶域中的乘法,反之亦然。
现在,让我们继续讨论下一个维度。
2 维
二维卷积与一维卷积非常相似。然而,我们将拥有一个矩阵而不是一个向量,这也是图像可以直接应用的原因。
假设我们有两个矩阵:一个表示输入数据,另一个是滤波器,如下所示:
。
我们可以通过反转(在两个维度上)和移动(同样在两个维度上)滤波器来计算二维离散卷积。其方程如下:
这与一维版本非常相似。下图展示了前两步和最后一步,为了节省空间并避免重复:
图 12.2 - 二维离散卷积示例
在 Python 中,我们可以使用 SciPy 的convolve2d
方法来计算二维卷积,方法如下:
import numpy as np
from scipy.signal import convolve2d
x = np.array([[2,2,2],[2,3,2],[2,2,2]])
w = np.array([[-1,-1,-1],[-1,8,-1],[-1,-1,-1]])
h = convolve2d(x,w)
print(h)
这将输出如下内容:
[[-2 -4 -6 -4 -2]
[-4 9 5 9 -4]
[-6 5 8 5 -6]
[-4 9 5 9 -4]
[-2 -4 -6 -4 -2]]
这里显示的结果是完整的解析结果。然而,与一维实现类似,如果您只想要完全重叠的结果,可以调用'valid'
结果,或者如果您想要与输入大小相同的结果,可以调用'same'
选项,如下所示:
import numpy as np
from scipy.signal import convolve2d
x = np.array([[2,2,2],[2,3,2],[2,2,2]])
w = np.array([[-1,-1,-1],[-1,8,-1],[-1,-1,-1]])
h = convolve2d(x,w,mode='valid')
print(h)
h = convolve2d(x,w,mode='same')
print(h)
这将得到如下结果:
[[8]]
[[9 5 9]
[5 8 5]
[9 5 9]]
现在,让我们继续讨论 n 维卷积。
n 维
一旦你理解了一维和二维卷积,你就掌握了背后的基本概念。然而,你可能仍然需要在更高维度上执行卷积,例如,在多光谱数据集中。为此,我们可以简单地准备任意维度的 NumPy 数组,然后使用 SciPy 的 convolve()
功能。考虑以下示例:
import numpy as np
from scipy.signal import convolve
x = np.array([[[1,1],[1,1]],[[2,2],[2,2]]])
w = np.array([[[1,-1],[1,-1]],[[1,-1],[1,-1]]])
h = convolve(x,w)
print(h)
在这里,向量 是三维数组,可以成功地进行卷积,产生如下输出:
[[[ 1 0 -1]
[ 2 0 -2]
[ 1 0 -1]]
[[ 3 0 -3]
[ 6 0 -6]
[ 3 0 -3]]
[[ 2 0 -2]
[ 4 0 -4]
[ 2 0 -2]]]
n 维卷积的唯一困难可能是将其可视化或在脑海中想象它们。我们人类可以轻松理解一维、二维和三维,但更高维度的空间很难表示。但请记住,如果你理解了在一维和二维中如何进行卷积,你可以相信数学原理和算法在任何维度中都会有效。
接下来,我们来看一下如何通过定义 Keras 层并将其添加到模型中来 学习 这些卷积滤波器。
卷积层
卷积在深度学习领域具有一些非常有趣的特性:
-
它可以成功地对数据的空间属性进行编码和解码。
-
它可以通过最新的技术快速计算。
-
它可以用于解决多个计算机视觉问题。
-
它可以与其他类型的层结合以达到最佳性能。
Keras 为 TensorFlow 提供了封装函数,涉及最常见的维度,即一维、二维和三维:Conv1D
、Conv2D
和 Conv3D
。在本章中,我们将继续聚焦于二维卷积,但只要你理解了这一概念,你可以轻松地使用其他类型的卷积。
Conv2D
二维卷积方法的签名如下:tensorflow.keras.layers.Conv2D
。卷积层中最常用的参数如下:
-
filters
指的是在该特定层中需要学习的滤波器数量,并且影响该层输出的维度。 -
kernel_size
指的是滤波器的大小;例如,在 图 12.2 中,它的大小为 (3,3)。 -
strides=(1, 1)
对我们来说是新的。步幅(strides)定义为滤波器滑过输入时的步伐大小。我们迄今为止展示的所有示例都假设我们遵循卷积的原始定义,采用单位步幅。然而,在卷积层中,你可以选择更大的步幅,这会导致较小的输出,但也会丢失信息。 -
padding='valid'
指的是处理卷积结果边缘信息的方式。请注意,这里的选项只有'valid'
或'same'
,并且无法获得完整的解析结果。其含义与本章前面提到的相同。 -
activation=None
提供了一个选项,可以在层中包括一个激活函数,如果需要的话;例如,activation='relu'
。
为了举例说明这一点,考虑一个如以下图所示的卷积层,其中第一层是二维卷积层,包含 64 个 9x9 的滤波器,步长为 2, 2(即每个方向两个)。我们将在接下来的图示中继续解释模型的其余部分:
图 12.3 - 用于 CIFAR 10 的卷积神经网络架构
图中的第一个卷积层可以定义如下:
import tensorflow as tf
from tensorflow.keras.layers import Conv2D
input_shape = (1, 32, 32, 3)
x = tf.random.normal(input_shape)
l = Conv2D(64, (9,9), strides=(2,2), activation='relu',
input_shape=input_shape)(l)
print(l.shape)
这实际上将创建一个具有给定规格的卷积层。打印语句将有效地产生以下内容:
(1, 12, 12, 64)
如果你做一下计算,64 个滤波器中的每一个都会产生一个 23x23 的 'valid'
输出,但由于使用了 (2,2) 的步长,应该得到一个 11.5x11.5 的输出。然而,由于我们不能有小数,TensorFlow 会将其四舍五入到 12x12。因此,我们最终得到上面的输出形状。
层+激活组合
如前所述,Conv2D
类具有包括你选择的激活函数的能力。这是非常受欢迎的,因为它可以节省一些代码行,帮助那些希望高效编写代码的人。然而,我们必须小心,不能忘记在某处记录使用的激活类型。
图 12.3 显示了一个单独块中的激活。这是一个很好的方法,可以跟踪整个过程中的激活使用情况。卷积层最常见的激活函数是 ReLU,或者是 ReLU 家族中的任何激活函数,例如,leaky ReLU 和 ELU。下一个 新 元素是池化层。让我们来谈谈这个。
池化策略
通常,你会发现池化层伴随卷积层出现。池化是一种旨在通过降低问题的维度来减少计算量的理念。在 Keras 中,我们有几种池化策略可供选择,但最重要和最常用的两种策略是以下两种:
-
AveragePooling2D
-
MaxPooling2D
这些池化操作也存在于其他维度中,如 1D。然而,为了理解池化,我们可以简单地查看以下图示中的示例:
图 12.4 - 2D 最大池化示例
在图示中,你可以看到最大池化如何在每个 2x2 的小块上移动,每次移动两个位置,从而得出一个 2x2 的结果。池化的全部目的在于 找到数据的一个更小的总结。在神经网络中,我们通常关注最被 激活 的神经元,因此查看最大值作为更大数据部分的代表是有意义的。然而,请记住,你也可以查看数据的平均值(AveragePooling2D
),它在各个方面也都是有效的。
在时间性能上,最大池化略占优势,但这一差异非常小。
在 Keras 中,我们可以非常轻松地实现池化。例如,对于 2D 的最大池化,我们可以简单地做如下操作:
import tensorflow as tf
from tensorflow.keras.layers import MaxPooling2D
x = tf.constant([[-2, -4, -6, -4],
[-4, 9, 5, 9],
[-6, 5, 8, 5],
[-4, 9, 5, 9]])
x = tf.reshape(x, [1, 4, 4, 1])
y = MaxPooling2D(pool_size=(2, 2), strides=(2, 2), padding='valid')
print(tf.reshape(y(x), [2, 2]))
这将生成与图 12.4相同的输出:
tf.Tensor(
[[9 9]
[9 9]], shape=(2, 2), dtype=int32)
我们也可以对平均池化做同样的操作,如下所示:
import tensorflow as tf
from tensorflow.keras.layers import AveragePooling2D
x = tf.constant([[-2., -4., -6., -4],
[-4., 9., 5., 9.],
[-6., 5., 8., 5.],
[-4., 9., 5., 9.]])
x = tf.reshape(x, [1, 4, 4, 1])
y = AveragePooling2D(pool_size=(2, 2), strides=(2, 2), padding='valid')
print(tf.reshape(y(x), [2, 2]))
这将产生如下输出:
tf.Tensor(
[[-0.25 1\. ]
[ 1\. 6.75]], shape=(2, 2), dtype=float32)
两种池化策略在总结数据方面都非常有效。你可以放心选择任何一种。
现在是大揭晓时刻。接下来,我们将在 CNN 中将所有这些内容结合起来。
CIFAR-10 的卷积神经网络
我们已经达到了可以实际实现一个功能完备的 CNN 的阶段,经过对各个组成部分的了解:理解卷积操作,理解池化操作,理解如何实现卷积层和池化层。现在,我们将实现图 12.3中展示的 CNN 架构。
实现
我们将一步一步地实现图 12.3中的网络,分解成子部分。
加载数据
让我们按如下方式加载 CIFAR-10 数据集:
from tensorflow.keras.datasets import cifar10
from tensorflow.keras.utils import to_categorical
import numpy as np
# The data, split between train and test sets:
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
y_train = to_categorical(y_train, 10)
y_test = to_categorical(y_test, 10)
print('x_train shape:', x_train.shape)
print('x_test shape:', x_test.shape)
这应该有效地加载数据集并打印出其形状,如下所示:
x_train shape: (50000, 32, 32, 3)
x_test shape: (10000, 32, 32, 3)
这非常直接,但我们可以进一步验证数据是否正确加载,通过加载并绘制x_train
集合中每个类别的第一张图片,如下所示:
import matplotlib.pyplot as plt
import numpy as np
(_, _), (_, labels) = cifar10.load_data()
idx = [3, 6, 25, 46, 58, 85, 93, 99, 108, 133]
clsmap = {0: 'airplane',
1: 'automobile',
2: 'bird',
3: 'cat',
4: 'deer',
5: 'dog',
6: 'frog',
7: 'horse',
8: 'ship',
9: 'truck'}
plt.figure(figsize=(10,4))
for i, (img, y) in enumerate(zip(x_test[idx].reshape(10, 32, 32, 3), labels[idx])):
plt.subplot(2, 5, i+1)
plt.imshow(img, cmap='gray')
plt.xticks([])
plt.yticks([])
plt.title(str(y[0]) + ": " + clsmap[y[0]])
plt.show()
这将生成如下截图所示的输出:
图 12.5 - CIFAR-10 样本
接下来,我们将实现网络的各个层。
编译模型
再次回想一下图 12.3中的模型,以及我们如何像这样实现它。接下来你将看到的所有内容,都是我们在这一章和之前的章节中讨论过的:
# Importing the Keras libraries and packages
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten
from tensorflow.keras.layers import Input, Dense, Dropout, BatchNormalization
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import RMSprop
# dimensionality of input and latent encoded representations
inpt_dim = (32, 32, 3)
inpt_img = Input(shape=inpt_dim)
# Convolutional layer
cl1 = Conv2D(64, (9, 9), strides=(2, 2), input_shape = inpt_dim,
activation = 'relu')(inpt_img)
# Pooling and BatchNorm
pl2 = MaxPooling2D(pool_size = (2, 2))(cl1)
bnl3 = BatchNormalization()(pl2)
我们继续添加更多的卷积层,如下所示:
# Add a second convolutional layer
cl4 = Conv2D(128, (3, 3), strides=(1, 1), activation = 'relu')(bnl3)
pl5 = MaxPooling2D(pool_size = (2, 2))(cl4)
bnl6 = BatchNormalization()(pl5)
# Flattening for compatibility
fl7 = Flatten()(bnl6)
# Dense layers + Dropout
dol8 = Dropout(0.5)(fl7)
dl9 = Dense(units = 256, activation = 'relu')(dol8)
dol10 = Dropout(0.2)(dl9)
dl11 = Dense(units = 64, activation = 'relu')(dol10)
dol12 = Dropout(0.1)(dl11)
output = Dense(units = 10, activation = 'sigmoid')(dol12)
classifier = Model(inpt_img, output)
然后我们可以编译模型并打印出总结,如下所示:
# Compiling the CNN with RMSprop optimizer
opt = RMSprop(learning_rate=0.001)
classifier.compile(optimizer = opt, loss = 'binary_crossentropy',
metrics = ['accuracy'])
print(classifier.summary())
这将输出一个网络总结,内容如下所示:
Model: "model"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) [(None, 32, 32, 3)] 0
_________________________________________________________________
conv2d (Conv2D) (None, 12, 12, 64) 15616
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 6, 6, 64) 0
_________________________________________________________________
batch_normalization (BatchNo (None, 6, 6, 64) 256
_________________________________________________________________
.
.
.
_________________________________________________________________
dropout_2 (Dropout) (None, 64) 0
_________________________________________________________________
dense_2 (Dense) (None, 10) 650
=================================================================
Total params: 238,666
Trainable params: 238,282
Non-trainable params: 384
到目前为止,有一件事应该对你非常明显,那就是这个网络的参数数量。如果你回忆一下前一章,你会惊讶地发现这个网络有近 25 万个参数,而宽或深的网络有几百万个参数。此外,你会很快看到,尽管这个相对较小的网络仍然是过度参数化的,它的表现将比前一章中那些有更多参数的网络要好。
接下来,让我们训练网络。
训练 CNN
我们可以使用我们在第十一章《深度与宽度神经网络》中学习过的回调函数来训练 CNN,如果网络没有进展,提前停止训练,或者如果达到平台期,通过降低学习率来集中梯度下降算法的努力。
我们将按如下方式训练它:
# Fitting the CNN to the images
from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=10,
min_delta=1e-4, mode='min', verbose=1)
stop_alg = EarlyStopping(monitor='val_loss', patience=35,
restore_best_weights=True, verbose=1)
hist = classifier.fit(x_train, y_train, batch_size=100, epochs=1000,
callbacks=[stop_alg, reduce_lr], shuffle=True,
validation_data=(x_test, y_test))
classifier.save_weights("cnn.hdf5")
这个结果会因计算机而异。例如,可能需要更多或更少的训练轮次,或者梯度的方向可能会有所不同,如果小批量(随机选择的)包含多个边缘案例。然而,大多数情况下,你应该得到一个与此类似的结果:
Epoch 1/1000
500/500 [==============================] - 3s 5ms/step - loss: 0.2733 - accuracy: 0.3613 - val_loss: 0.2494 - val_accuracy: 0.4078 - lr: 0.0010
Epoch 2/1000
500/500 [==============================] - 2s 5ms/step - loss: 0.2263 - accuracy: 0.4814 - val_loss: 0.2703 - val_accuracy: 0.4037 - lr: 0.0010
.
.
.
Epoch 151/1000
492/500 [============================>.] - ETA: 0s - loss: 0.0866 - accuracy: 0.8278
Epoch 00151: ReduceLROnPlateau reducing learning rate to 3.906250185536919e-06.
500/500 [==============================] - 2s 4ms/step - loss: 0.0866 - accuracy: 0.8275 - val_loss: 0.1153 - val_accuracy: 0.7714 - lr: 7.8125e-06
Epoch 152/1000
500/500 [==============================] - 2s 4ms/step - loss: 0.0864 - accuracy: 0.8285 - val_loss: 0.1154 - val_accuracy: 0.7707 - lr: 3.9063e-06
Epoch 153/1000
500/500 [==============================] - 2s 4ms/step - loss: 0.0861 - accuracy: 0.8305 - val_loss: 0.1153 - val_accuracy: 0.7709 - lr: 3.9063e-06
Epoch 154/1000
500/500 [==============================] - 2s 4ms/step - loss: 0.0860 - accuracy: 0.8306 - val_loss: 0.1153 - val_accuracy: 0.7709 - lr: 3.9063e-06
Epoch 155/1000
500/500 [==============================] - 2s 4ms/step - loss: 0.0866 - accuracy: 0.8295 - val_loss: 0.1153 - val_accuracy: 0.7715 - lr: 3.9063e-06
Epoch 156/1000
496/500 [============================>.] - ETA: 0s - loss: 0.0857 - accuracy: 0.8315Restoring model weights from the end of the best epoch.
500/500 [==============================] - 2s 4ms/step - loss: 0.0857 - accuracy: 0.8315 - val_loss: 0.1153 - val_accuracy: 0.7713 - lr: 3.9063e-06
Epoch 00156: early stopping
此时,训练完成后,你可以得到约 83.15%的准确率估算值。请注意,这并不是平衡准确率。为此,我们将在下一节中查看平衡误差率(BER)指标。但在那之前,我们可以看看训练曲线,看看损失是如何被最小化的。
以下代码将生成我们所需的结果:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(10,6))
plt.plot(hist.history['loss'], color='#785ef0')
plt.plot(hist.history['val_loss'], color='#dc267f')
plt.title('Model Loss Progress')
plt.ylabel('Brinary Cross-Entropy Loss')
plt.xlabel('Epoch')
plt.legend(['Training Set', 'Test Set'], loc='upper right')
plt.show()
这将生成如下所示的图像,见图 12.6:
图 12.6 - CIFAR-10 上的 CNN 损失最小化
从这个图表中,你可以看到学习曲线上的波动,尤其是在训练集的曲线中比较明显,这些波动是由于通过回调函数ReduceLROnPlateau
减少学习率所致。当测试集的损失不再改善时,训练将停止,这得益于EarlyStopping
回调。
结果
现在,让我们看看客观的数值结果:
from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix
from sklearn.metrics import balanced_accuracy_score
import matplotlib.pyplot as plt
import numpy as np
(_, _), (_, labels) = cifar10.load_data()
y_ = labels
y_hat = classifier.predict(x_test)
y_pred = np.argmax(y_hat, axis=1)
print(classification_report(np.argmax(y_test, axis=1),
np.argmax(y_hat, axis=1),
labels=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))
cm = confusion_matrix(np.argmax(y_test, axis=1),
np.argmax(y_hat, axis=1),
labels=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
print(cm)
ber = 1- balanced_accuracy_score(np.argmax(y_test, axis=1),
np.argmax(y_hat, axis=1))
print('BER', ber)
这将给我们以下的数值结果,我们可以将其与上一章的结果进行比较:
precision recall f1-score support
0 0.80 0.82 0.81 1000
1 0.89 0.86 0.87 1000
2 0.73 0.66 0.69 1000
3 0.57 0.63 0.60 1000
4 0.74 0.74 0.74 1000
5 0.67 0.66 0.66 1000
6 0.84 0.82 0.83 1000
7 0.82 0.81 0.81 1000
8 0.86 0.88 0.87 1000
9 0.81 0.85 0.83 1000
accuracy 0.77 10000
[[821 12 36 18 12 8 4 4 51 34]
[ 17 860 3 7 2 6 8 1 22 74]
[ 61 2 656 67 72 53 43 24 11 11]
[ 11 7 47 631 55 148 38 36 10 17]
[ 21 2 48 63 736 28 31 54 12 5]
[ 12 3 35 179 39 658 16 41 4 13]
[ 2 4 32 67 34 20 820 8 8 5]
[ 12 3 18 41 42 52 5 809 3 15]
[ 43 22 12 12 2 5 3 0 875 26]
[ 29 51 10 19 2 3 5 9 26 846]]
BER 0.2288
特定类别的准确率最高可达 87%,而最低准确率为 66%。这比上一章中的模型要好得多。BER 为 0.2288,所有这些可以解读为 77.12%的平衡准确率。这与训练过程中测试集上报告的准确率相符,表明模型已正确训练。为了进行对比,以下图表展示了混淆矩阵的可视化表示:
图 12.7 - 在 CIFAR-10 上训练的 CNN 的混淆矩阵
从视觉混淆矩阵中可以更清楚地看出,类别 3 和 5 之间的混淆程度比其他类别更高。类别 3 和 5 分别对应猫和狗。
就这样。如你所见,这已经是一个不错的结果,但你可以自己进行更多实验。你可以编辑并添加更多卷积层来改进模型。如果你有兴趣,还有其他更大的 CNN 模型也取得了很大成功。以下是最著名的两个:
-
VGG-19:该模型包含 12 个卷积层和 3 个全连接层(Simonyan, K., 等人 (2014))。
-
ResNet:该模型包含 110 个卷积层和 1 个全连接层(He, K., 等人 (2016))。该配置在 CIFAR-10 数据集上能够达到 6.61%(±0.16%)的最低错误率。
接下来我们来讨论如何可视化学习到的滤波器。
滤波器可视化
本章的最后一部分处理了学习到的滤波器的可视化。如果你想研究网络学到了什么,这可能对你有用。这有助于提升网络的可解释性。然而,请注意,网络越深,理解它就越复杂。
以下代码将帮助你可视化网络的第一个卷积层的滤波器:
from sklearn.preprocessing import MinMaxScaler
cnnl1 = classifier.layers[1].name # get the name of the first conv layer
W = classifier.get_layer(name=cnnl1).get_weights()[0] #get the filters
wshape = W.shape #save the original shape
# this part will scale to [0, 1] for visualization purposes
scaler = MinMaxScaler()
scaler.fit(W.reshape(-1,1))
W = scaler.transform(W.reshape(-1,1))
W = W.reshape(wshape)
# since there are 64 filters, we will display them 8x8
fig, axs = plt.subplots(8,8, figsize=(24,24))
fig.subplots_adjust(hspace = .25, wspace=.001)
axs = axs.ravel()
for i in range(W.shape[-1]):
# we reshape to a 3D (RGB) image shape and display
h = np.reshape(W[:,:,:,i], (9,9,3))
axs[i].imshow(h)
axs[i].set_title('Filter ' + str(i))
这段代码在很大程度上依赖于你知道想要可视化的层、想要可视化的滤波器数量以及滤波器本身的大小。在这种情况下,我们要可视化第一个卷积层。它有 64 个滤波器(以 8x8 的网格显示),每个滤波器的大小为 9x9x3,因为输入的是彩色图像。图 12.8显示了前面代码生成的结果图:
图 12.8 - 第一个卷积层中学习到的滤波器
如果你是图像处理专家,你可能会认出其中一些模式,它们类似于 Gabor 滤波器(Jain, A. K., 等人(1991))。其中一些滤波器旨在寻找边缘、纹理或特定的形状。文献表明,在卷积网络中,较深的层通常编码高度复杂的信息,而第一层用于检测边缘等特征。
随时可以继续前进,尝试通过进行必要的修改来展示另一个层。
总结
本中级章节展示了如何创建卷积神经网络(CNN)。你了解了卷积操作,这是其基本概念。你还学习了如何创建卷积层和聚合池化策略。你设计了一个网络,通过学习滤波器来识别 CIFAR-10 中的物体,并学习了如何展示已学习的滤波器。
到此为止,你应该能自信地解释卷积神经网络背后的动机,这些动机根植于计算机视觉和信号处理领域。你应该能够使用 NumPy、SciPy 和 Keras/TensorFlow 来编写一维和二维卷积操作的代码。此外,你应该能够自信地在层中实现卷积操作,并通过梯度下降技术学习滤波器。如果有人要求你展示网络所学到的内容,你应该能准备好实现一个简单的可视化方法来展示学到的滤波器。
CNN 擅长编码高度相关的空间信息,如图像、音频或文本。然而,还有一种有趣的网络类型,旨在编码顺序性的信息。第十三章,递归神经网络,将介绍递归网络的最基本概念,进而引入长短期记忆模型。我们将探索多种顺序模型的变体,并应用于图像分类和自然语言处理。
问题与答案
- 本章讨论的哪种数据总结策略可以减少卷积模型的维度?
池化。
- 增加更多卷积层会让网络变得更好吗?
并非总是如此。研究表明,更多层数对网络有正面影响,但在某些情况下并没有提升效果。你应该通过实验来确定层数、滤波器大小和池化策略。
- 卷积神经网络(CNN)还有哪些其他应用?
音频处理与分类;图像去噪;图像超分辨率;文本摘要及其他文本处理与分类任务;数据加密。
参考文献
-
LeCun, Y., Boser, B., Denker, J. S., Henderson, D., Howard, R. E., Hubbard, W., 和 Jackel, L. D. (1989). 反向传播应用于手写邮政编码识别. 神经计算, 1(4), 541-551。
-
Li, Y. D., Hao, Z. B., 和 Lei, H. (2016). 卷积神经网络综述. 计算机应用杂志, 36(9), 2508-2515。
-
Krizhevsky, A., Sutskever, I., 和 Hinton, G. E. (2012). 使用深度卷积神经网络进行 Imagenet 分类. 见 神经信息处理系统进展(第 1097-1105 页)。
-
Simonyan, K., 和 Zisserman, A. (2014). 用于大规模图像识别的非常深的卷积网络. arXiv 预印本 arXiv:1409.1556。
-
He, K., Zhang, X., Ren, S., 和 Sun, J. (2016). 深度残差学习用于图像识别. 见 IEEE 计算机视觉与模式识别大会论文集(第 770-778 页)。
-
Jain, A. K., 和 Farrokhnia, F. (1991). 使用 Gabor 滤波器的无监督纹理分割. 模式识别, 24(12), 1167-1186。
循环神经网络
本章介绍了循环神经网络,从基本模型开始,逐步介绍能够处理内部记忆学习的新型循环层,这些层能够记住或忘记数据集中的某些模式。我们将首先展示,循环网络在推断时间序列或顺序模式时非常强大,然后我们将介绍对传统范式的改进,提出具有内部记忆的模型,这个模型可以在时间空间中双向应用。
我们将通过将情感分析问题作为序列到向量的应用来接近学习任务,然后我们将专注于自动编码器,同时作为向量到序列和序列到序列模型。到本章结束时,你将能够解释为什么长短期记忆模型比传统的密集方法更优。你将能够描述双向长短期记忆模型如何在单向方法上可能具有优势。你将能够实现自己的循环神经网络,并将其应用于自然语言处理问题或图像相关的应用,包括序列到向量、向量到序列和序列到序列建模。
本章的结构如下:
-
循环神经网络简介
-
长短期记忆模型
-
序列到向量模型
-
向量到序列模型
-
序列到序列模型
-
伦理学影响
第十七章:循环神经网络简介
循环神经网络(RNNs)基于 Rumelhart 的早期工作(Rumelhart, D. E.等人,1986),他是一位心理学家,与我们之前提到的 Hinton 密切合作。这个概念很简单,但在使用数据序列进行模式识别的领域却具有革命性。
数据序列是任何在时间或空间上具有高度相关性的数据片段。例如,音频序列和图像。
RNN 中的递归概念可以通过以下图示来说明。如果你认为神经单元的密集层,这些层可以在不同的时间步上通过某些输入来激活,。图 13.1 (b)和(c)展示了一个具有五个时间步的 RNN,
。我们可以在图 13.1 (b)和(c)中看到,输入如何在不同的时间步之间可访问,但更重要的是,神经单元的输出也可以提供给下一层神经元:
图 13.1. 循环层的不同表示:(a)是本书中优选的使用方式;(b)展示了神经单元和反馈回路;(c)是(b)的扩展版本,展示了训练过程中实际发生的情况。
RNN 能够看到前一层神经元的激活情况,这有助于网络更好地理解序列,而不是没有这些附加信息。然而,这也带来了成本:与传统的密集层相比,计算的参数会更多,因为输入 和前一个输出
都需要有权重。
简单 RNN
在 Keras 中,我们可以创建一个简单的 RNN,具有五个时间步长和10 个神经单元(参见图 13.1),代码如下:
from tensorflow.keras import Sequential
from tensorflow.keras.layers import SimpleRNN
n_units = 10
t_steps = 5
inpt_ftrs=2
model = Sequential()
model.add(SimpleRNN(n_units, input_shape=(t_steps, inpt_ftrs)))
model.summary()
这给出了以下总结:
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
simple_rnn (SimpleRNN) (None, 10) 130
=================================================================
Total params: 130
Trainable params: 130
Non-trainable params: 0
上面的示例代码假设输入的特征数量只有两个;例如,我们可以在二维中拥有顺序数据。这类 RNN 被称为简单的,因为它们类似于具有 tanh
激活函数和递归特性的密集网络。
RNN 通常与嵌入层配合使用,接下来我们将讨论这一点。
嵌入层
嵌入层通常与 RNN 配合使用,尤其是在需要额外处理的序列中,以增强 RNN 的鲁棒性。考虑以下情境,当你有一句话 "This is a small vector",并且你想训练一个 RNN 来检测句子是否写得正确或写得不好。你可以使用所有你能想到的长度为五的句子来训练 RNN,包括 "This is a small vector"。为此,你需要找到一种方法将句子转化为 RNN 可以理解的形式。嵌入层就派上了用场。
有一种叫做词嵌入的技术,它的任务是将一个词转换成一个向量。现在有几种成功的实现方法,例如 Word2Vec(Mikolov, T., 等人(2013))或 GloVe(Pennington, J., 等人(2014))。然而,我们将重点介绍一种简单且易于使用的技术。我们将分步进行:
-
确定你想要处理的句子的长度。这将成为 RNN 层输入的维度。虽然这一步对设计嵌入层不是必需的,但你很快就会需要它,并且在早期决定这一点非常重要。
-
确定数据集中不同单词的数量,并为它们分配一个数字,创建一个字典:词到索引。这被称为词汇表。
大多数人会先确定词汇表,然后计算每个单词的频率,将词汇表中的单词按频率排序,使得索引 0 对应数据集中最常见的单词,最后一个索引对应最不常见的单词。如果你希望忽略最常见或最不常见的单词,这种方法可能会很有帮助。
-
用对应的索引替换数据集中所有句子中的单词。
-
确定词嵌入的维度,并训练一个嵌入层,将数字索引映射到具有所需维度的实值向量。
看看图 13.2中的例子。如果我们取单词This,它的索引是 7,一些训练过的嵌入层可以将这个数字映射到一个大小为 10 的向量,如你在图 13.2 (b)中看到的那样。这就是词嵌入过程。
你可以对完整的句子"This is a small vector"重复这个过程,它可以映射到一个索引序列 [7, 0, 6, 1, 28],并为你生成一个词向量序列;见图 13.2 (c)。换句话说,它会生成一个词嵌入序列。RNN 可以轻松处理这些序列,并判断这些序列所代表的句子是否正确。
然而,我们必须说,确定一个句子是否正确是一个具有挑战性且有趣的问题(Rivas, P.等人,2019 年):
图 13.2. 嵌入层:(a) 是本书中推荐使用的形式;(b) 展示了一个词嵌入的例子;(c) 展示了一个词序列及其对应的词嵌入矩阵。
基于图 13.2中展示的模型,可以如下创建一个 Keras 中的嵌入层:
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Embedding
vocab_size = 30
embddng_dim = 10
seqnc_lngth = 5
model = Sequential()
model.add(Embedding(vocab_size, embddng_dim, input_length=seqnc_lngth))
model.summary()
这产生了以下总结:
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding (Embedding) (None, 5, 10) 300
=================================================================
Total params: 300
Trainable params: 300
Non-trainable params: 0
然而请注意,词汇表的大小通常在几千个左右,适用于大多数常见语言中的典型 NLP 任务。想想你那本传统的字典……它有多少条目?通常是几千个。
类似地,句子通常会长于五个单词,因此你应该预期拥有比前面例子更长的序列。
最后,嵌入维度取决于你希望模型在嵌入空间中的丰富程度,或者取决于模型空间的约束。如果你希望模型较小,可以考虑使用例如 50 维的嵌入。但如果空间不成问题,且你拥有一个包含数百万条数据的优秀数据集,并且有无限的 GPU 计算能力,那么你可以尝试使用 500、700 甚至 1000+维度的嵌入。
现在,让我们通过一个现实生活中的例子来将这些部分结合起来。
IMDb 上的词嵌入和 RNN
IMDb 数据集在前面的章节中已经解释过了,但为了简洁起见,我们会说它包含基于文本的电影评论,并且每个条目都与一个正面(1)或负面(0)的评价相关联。
Keras 让你可以访问这个数据集,并提供了一些优化时间的不错功能,当你在设计模型时。例如,数据集已根据每个单词的频率进行处理,因此最小的索引会与频繁出现的单词相关联,反之亦然。考虑到这一点,你还可以排除英语中最常见的单词,比如说 10 个或 20 个。而且你甚至可以将词汇表的大小限制为比如 5,000 或 10,000 个单词。
在我们继续之前,我们需要解释一些你将要看到的内容:
-
词汇量为 10,000。我们可以提出一个观点,支持保持 10,000 的词汇量,因为这里的任务是确定评论是正面还是负面的。也就是说,我们不需要过于复杂的词汇表来进行这种判断。
-
消除前 20 个单词。英语中最常见的单词包括诸如 "a" 或 "the" 的单词;这些单词可能在确定电影评论是正面还是负面时并不重要。因此,消除最常见的 20 个单词应该是可以接受的。
-
句子长度为 128 个单词。较小的句子,如 5 个单词的句子,可能缺乏足够的内容,而较长的句子,如 300 个单词的句子,则可能在少于这些单词的情况下就能感知评论的语调。选择 128 个单词完全是任意的,但在前面解释的意义上是合理的。
考虑到这些因素,我们可以轻松地按以下方式加载数据集:
from keras.datasets import imdb
from keras.preprocessing import sequence
inpt_dim = 128
index_from = 3
(x_train, y_train),(x_test, y_test)=imdb.load_data(num_words=10000,
start_char=1,
oov_char=2,
index_from=index_from,
skip_top=20)
x_train = sequence.pad_sequences(x_train,
maxlen=inpt_dim).astype('float32')
x_test = sequence.pad_sequences(x_test, maxlen=inpt_dim).astype('float32')
# let's print the shapes
print('x_train shape:', x_train.shape)
print('x_test shape:', x_test.shape)
我们也可以像这样打印一些数据以进行验证:
# let's print the indices of sample #7
print(' '.join(str(int(id)) for id in x_train[7]))
# let's print the actual words of sample #7
wrd2id = imdb.get_word_index()
wrd2id = {k:(v+index_from) for k,v in wrd2id.items()}
wrd2id["<PAD>"] = 0
wrd2id["<START>"] = 1
wrd2id["<UNK>"] = 2
wrd2id["<UNUSED>"] = 3
id2wrd = {value:key for key,value in wrd2id.items()}
print(' '.join(id2wrd[id] for id in x_train[7] ))
这将输出以下内容:
x_train shape: (25000, 128)
x_test shape: (25000, 128)
55 655 707 6371 956 225 1456 841 42 1310 225 2 ...
very middle class suburban setting there's zero atmosphere or mood there's <UNK> ...
前面的代码的第一部分展示了如何加载数据集并将其分为训练集和测试集,分别为 x_train
和 y_train
,x_test
和 y_test
。剩余部分只是为了显示数据集的形状(维度)以进行验证,并且也可以打印出其原始形式中的第 7 个样本(索引)及其相应的单词。如果你以前没有使用过 IMDb,这部分代码可能有些奇怪。但主要的点是我们需要为特殊标记保留某些索引:句子开头 <START>
,未使用的索引 <UNUSED>
,未知词索引 <UNK>
,以及零填充索引 <PAD>
。一旦我们为这些标记做了特殊分配,我们就可以轻松地从索引映射回单词。这些索引将由 RNN 学习,并且它将学会如何处理它们,无论是通过忽略它们还是给予它们特定的权重。
现在,让我们实现如下图表所示的架构,其中包括前面解释过的所有层次:
图 13.3. IMDb 数据集的 RNN 架构
图表显示了与负面评论相关联的训练集中的示例(第 7 个)。图表中描绘的架构以及加载数据的代码如下所示:
from keras.datasets import imdb
from keras.preprocessing import sequence
from tensorflow.keras.models import Model
from tensorflow.keras.layers import SimpleRNN, Embedding, BatchNormalization
from tensorflow.keras.layers import Dense, Activation, Input, Dropout
seqnc_lngth = 128
embddng_dim = 64
vocab_size = 10000
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=vocab_size,
skip_top=20)
x_train = sequence.pad_sequences(x_train,
maxlen=seqnc_lngth).astype('float32')
x_test = sequence.pad_sequences(x_test,
maxlen=seqnc_lngth).astype('float32')
模型的各层定义如下:
inpt_vec = Input(shape=(seqnc_lngth,))
l1 = Embedding(vocab_size, embddng_dim, input_length=seqnc_lngth)(inpt_vec)
l2 = Dropout(0.3)(l1)
l3 = SimpleRNN(32)(l2)
l4 = BatchNormalization()(l3)
l5 = Dropout(0.2)(l4)
output = Dense(1, activation='sigmoid')(l5)
rnn = Model(inpt_vec, output)
rnn.compile(loss='binary_crossentropy', optimizer='adam',
metrics=['accuracy'])
rnn.summary()
此模型使用了之前使用过的标准损失和优化器,生成的摘要如下:
Model: "functional"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) [(None, 128)] 0
_________________________________________________________________
embedding (Embedding) (None, 128, 64) 640000
_________________________________________________________________
dropout_1 (Dropout) (None, 128, 64) 0
_________________________________________________________________
simple_rnn (SimpleRNN) (None, 32) 3104
_________________________________________________________________
batch_normalization (BatchNo (None, 32) 128
_________________________________________________________________
dropout_2 (Dropout) (None, 32) 0
_________________________________________________________________
dense (Dense) (None, 1) 33
=================================================================
Total params: 643,265
Trainable params: 643,201
Non-trainable params: 64
然后我们可以使用之前使用过的回调函数来训练网络:a)早停止,和 b)自动学习率降低。学习过程可以如下执行:
from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping
import matplotlib.pyplot as plt
#callbacks
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=3,
min_delta=1e-4, mode='min', verbose=1)
stop_alg = EarlyStopping(monitor='val_loss', patience=7,
restore_best_weights=True, verbose=1)
#training
hist = rnn.fit(x_train, y_train, batch_size=100, epochs=1000,
callbacks=[stop_alg, reduce_lr], shuffle=True,
validation_data=(x_test, y_test))
然后我们保存模型并显示损失如下所示:
# save and plot training process
rnn.save_weights("rnn.hdf5")
fig = plt.figure(figsize=(10,6))
plt.plot(hist.history['loss'], color='#785ef0')
plt.plot(hist.history['val_loss'], color='#dc267f')
plt.title('Model Loss Progress')
plt.ylabel('Brinary Cross-Entropy Loss')
plt.xlabel('Epoch')
plt.legend(['Training Set', 'Test Set'], loc='upper right')
plt.show()
前面的代码生成了以下图表,该图表显示网络在第 3 轮之后开始过拟合:
图 13.4. 训练过程中 RNN 的损失
过拟合在递归网络中相当常见,你不应对这种行为感到惊讶。至今,使用当前的算法,这种现象非常普遍。然而,关于 RNN 的一个有趣事实是,与其他传统模型相比,它们的收敛速度非常快。如你所见,经过三次迭代后,收敛情况已经不错。
接下来,我们必须通过查看平衡准确率、混淆矩阵和ROC 曲线下面积(AUC)来检查实际的分类性能。我们只会在测试集上进行如下操作:
from sklearn.metrics import confusion_matrix
from sklearn.metrics import balanced_accuracy_score
from sklearn.metrics import roc_curve, auc
import matplotlib.pyplot as plt
import numpy as np
y_hat = rnn.predict(x_test)
# gets the ROC
fpr, tpr, thresholds = roc_curve(y_test, y_hat)
roc_auc = auc(fpr, tpr)
# plots ROC
fig = plt.figure(figsize=(10,6))
plt.plot(fpr, tpr, color='#785ef0',
label='ROC curve (AUC = %0.2f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='#dc267f', linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic Curve')
plt.legend(loc="lower right")
plt.show()
# finds optimal threshold and gets ACC and CM
optimal_idx = np.argmax(tpr - fpr)
optimal_threshold = thresholds[optimal_idx]
print("Threshold value is:", optimal_threshold)
y_pred = np.where(y_hat>=optimal_threshold, 1, 0)
print(balanced_accuracy_score(y_test, y_pred))
print(confusion_matrix(y_test, y_pred))
首先,让我们分析这里生成的图表,它显示在图 13.5中:
图 13.5. 测试集上计算的 RNN 模型的 ROC 和 AUC
该图显示了真阳性率(TPR)和假阳性率(FPR)的良好组合,尽管并不理想:我们希望看到更陡峭的阶梯状曲线。AUC 值为 0.92,这再次表明表现不错,但理想情况下,AUC 应该为 1.0。
类似地,代码生成了平衡准确率和混淆矩阵,结果大致如下所示:
Threshold value is: 0.81700134
0.8382000000000001
[[10273 2227]
[ 1818 10682]]
首先,我们在这里计算作为 TPR 和 FPR 函数的最优阈值。我们希望选择一个阈值,使其能给我们带来最大的 TPR 和最小的 FPR。这里显示的阈值和结果会变化,具体取决于网络的初始状态;然而,准确率通常应该接近一个非常相似的值。
一旦计算出最优阈值,我们可以使用 NumPy 的np.where()
方法对整个预测结果进行阈值处理,将其映射为{0, 1}。之后,计算得出的平衡准确率为 83.82%,这再次表明表现不错,但也不算理想。
改进图 13.3 中显示的 RNN 模型的一个可能方法是,某种方式使循环层具有在各层间“记住”或“忘记”特定单词的能力,并且继续在序列中激活神经元。下一节将介绍具有这种能力的 RNN 类型。
长短期记忆模型
最初由 Hochreiter 提出,长短期记忆模型(LSTM)作为递归模型的改进版本获得了广泛关注[Hochreiter, S., et al. (1997)]。LSTM 承诺解决与传统 RNN 相关的以下问题:
-
梯度消失
-
梯度爆炸
-
无法记住或忘记输入序列的某些方面
以下图表显示了一个非常简化的 LSTM 版本。在(b)中,我们可以看到附加到某些记忆上的自循环,而在(c)中,我们可以观察到网络展开或展开后的样子:
图 13.6. 简化版 LSTM 的表示
模型的内容远不止这些,但最核心的部分在图 13.6中展示了。可以观察到,LSTM 层不仅从前一个时间步接收前一个输出,还接收一个叫做状态的信息,这个状态充当了一种记忆。在图中你可以看到,尽管当前输出和状态可以传递到下一层,但它们也可以在任何需要的地方使用。
在图 13.6中没有显示的一些内容包括 LSTM 记忆或遗忘的机制。由于这些对于初学者来说可能比较复杂,书中没有进行详细说明。然而,到目前为止你只需要知道,有三种主要机制:
-
-
输出控制:输出神经元受前一个输出和当前状态的刺激程度
-
记忆控制:当前状态中将遗忘多少前一个状态的信息
-
输入控制:前一个输出和新状态(记忆)在确定当前状态时的考虑程度
-
这些机制是可训练的,并且针对每一个单独的序列数据集进行优化。但为了展示使用 LSTM 作为我们递归层的优势,我们将重复之前的相同代码,只是将 RNN 替换为 LSTM。
加载数据集并构建模型的代码如下:
from keras.datasets import imdb
from keras.preprocessing import sequence
from tensorflow.keras.models import Model
from tensorflow.keras.layers import LSTM, Embedding, BatchNormalization
from tensorflow.keras.layers import Dense, Activation, Input, Dropout
seqnc_lngth = 128
embddng_dim = 64
vocab_size = 10000
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=vocab_size,
skip_top=20)
x_train = sequence.pad_sequences(x_train, maxlen=seqnc_lngth).astype('float32')
x_test = sequence.pad_sequences(x_test, maxlen=seqnc_lngth).astype('float32')
可以按照以下方式指定模型:
inpt_vec = Input(shape=(seqnc_lngth,))
l1 = Embedding(vocab_size, embddng_dim, input_length=seqnc_lngth)(inpt_vec)
l2 = Dropout(0.3)(l1)
l3 = LSTM(32)(l2)
l4 = BatchNormalization()(l3)
l5 = Dropout(0.2)(l4)
output = Dense(1, activation='sigmoid')(l5)
lstm = Model(inpt_vec, output)
lstm.compile(loss='binary_crossentropy', optimizer='adam',
metrics=['accuracy'])
lstm.summary()
这将产生以下输出:
Model: "functional"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input (InputLayer) [(None, 128)] 0
_________________________________________________________________
embedding (Embedding) (None, 128, 64) 640000
_________________________________________________________________
dropout_1 (Dropout) (None, 128, 64) 0
_________________________________________________________________
lstm (LSTM) (None, 32) 12416
_________________________________________________________________
batch_normalization (Batch (None, 32) 128
_________________________________________________________________
dropout_2 (Dropout) (None, 32) 0
_________________________________________________________________
dense (Dense) (None, 1) 33
=================================================================
Total params: 652,577
Trainable params: 652,513
Non-trainable params: 64
这基本上复制了下图所示的模型:
图 13.7. 基于 LSTM 的 IMDb 数据集神经架构
请注意,该模型的参数几乎比简单的 RNN 模型多了 10,000 个。然而,前提是这些参数的增加应当也带来性能的提升。
然后我们像之前一样训练模型,代码如下:
from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping
import matplotlib.pyplot as plt
#callbacks
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=3,
min_delta=1e-4, mode='min', verbose=1)
stop_alg = EarlyStopping(monitor='val_loss', patience=7,
restore_best_weights=True, verbose=1)
#training
hist = lstm.fit(x_train, y_train, batch_size=100, epochs=1000,
callbacks=[stop_alg, reduce_lr], shuffle=True,
validation_data=(x_test, y_test))
接下来,我们保存模型并展示其性能,代码如下:
# save and plot training process
lstm.save_weights("lstm.hdf5")
fig = plt.figure(figsize=(10,6))
plt.plot(hist.history['loss'], color='#785ef0')
plt.plot(hist.history['val_loss'], color='#dc267f')
plt.title('Model Loss Progress')
plt.ylabel('Brinary Cross-Entropy Loss')
plt.xlabel('Epoch')
plt.legend(['Training Set', 'Test Set'], loc='upper right')
plt.show()
这段代码将生成下图所示的图表:
图 13.8. 训练 LSTM 过程中各个 epoch 的损失变化
从图中可以看出,模型在一个 epoch后开始过拟合。使用在最佳点训练好的模型,我们可以如下计算实际性能:
from sklearn.metrics import confusion_matrix
from sklearn.metrics import balanced_accuracy_score
from sklearn.metrics import roc_curve, auc
import matplotlib.pyplot as plt
import numpy as np
y_hat = lstm.predict(x_test)
# gets the ROC
fpr, tpr, thresholds = roc_curve(y_test, y_hat)
roc_auc = auc(fpr, tpr)
# plots ROC
fig = plt.figure(figsize=(10,6))
plt.plot(fpr, tpr, color='#785ef0',
label='ROC curve (AUC = %0.2f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='#dc267f', linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic Curve')
plt.legend(loc="lower right")
plt.show()
# finds optimal threshold and gets ACC and CM
optimal_idx = np.argmax(tpr - fpr)
optimal_threshold = thresholds[optimal_idx]
print("Threshold value is:", optimal_threshold)
y_pred = np.where(y_hat>=optimal_threshold, 1, 0)
print(balanced_accuracy_score(y_test, y_pred))
print(confusion_matrix(y_test, y_pred))
这将产生下图所示的 ROC 曲线:
图 13.9. 基于 LSTM 架构的 ROC 曲线
从图表中我们可以看到,模型略有提升,生成了 AUC 为 0.93 的结果,而简单的 RNN 模型的 AUC 为 0.92。
查看由前述代码生成的平衡准确率和混淆矩阵,我们可以看到类似这样的数字:
Threshold value is: 0.44251397
0.8544400000000001
[[10459 2041]
[ 1598 10902]]
在这里,我们可以看到准确率为 85.44%,比简单的 RNN 提高了大约 2%。我们进行这个实验只是为了展示,通过更换 RNN 模型,我们可以轻松地看到性能提升。当然,也有其他方法可以提高模型,例如:
-
增加/减少词汇表的大小
-
增加/减少序列长度
-
增加/减少嵌入维度
-
增加/减少递归层中的神经元单元
可能还有其他类型的模型。
到目前为止,你已经看到如何将文本表示(电影评论)进行处理,这是一种常见的 NLP 任务,并且找到一种方式将其表示为一个空间,在这个空间中你可以将评论分类为负面或正面。我们通过嵌入和 LSTM 层完成了这一过程,但在最后,这里有一个包含一个神经元的全连接层,给出最终的输出。我们可以将其看作是从文本空间映射到一个一维空间,在这个空间中我们可以进行分类。我们这么说是因为考虑这些映射时,有三种主要的方式:
-
序列到向量:就像这里讨论的例子,将序列映射到一个n维的空间。
-
向量到序列:这与上面相反,从一个n维的空间到一个序列。
-
序列到序列:这将一个序列映射到另一个序列,通常中间会经过一个n维的映射。
为了举例说明这些概念,我们将在接下来的章节中使用自编码器架构和 MNIST。
序列到向量模型
在上一节中,你从技术上看到了一个序列到向量的模型,它将一个序列(代表单词的数字)映射到一个向量(一个维度对应于电影评论)。然而,为了更进一步理解这些模型,我们将回到 MNIST 作为输入源,构建一个将一个 MNIST 数字映射到潜在向量的模型。
无监督模型
让我们在下图所示的自编码器架构中进行工作。我们之前研究过自编码器,现在我们将再次使用它们,因为我们了解到它们在找到稳健的、由无监督学习驱动的向量表示(潜在空间)方面非常强大:
图 13.10. 基于 LSTM 的 MNIST 自编码器架构
这里的目标是获取一张图像并找到其潜在表示,在图 13.10的例子中,这个表示是二维的。然而,你可能会想:图像怎么会是一个序列呢?
我们可以将一张图像解释为一系列行,或者一系列列。假设我们将一个二维图像(28x28 像素)解释为一系列行;我们可以将每一行从上到下看作是 28 个向量的序列,每个向量的维度为 1x28。这样,我们就可以使用 LSTM 来处理这些序列,利用 LSTM 理解序列中时间关系的能力。通过这种方式,我们的意思是,比如在 MNIST 的例子中,某一行图像与前一行或下一行相似的可能性非常高。
进一步注意到,图 13.10中提出的模型不需要像我们之前处理文本时那样使用嵌入层。回想一下,在处理文本时,我们需要将每个单词嵌入(向量化)成一个向量序列。然而,对于图像,它们本身就是向量的序列,因此不再需要嵌入层。
这里展示的代码没有什么新内容,除了两个有用的数据操作工具:
-
RepeatVector()
:这将允许我们任意重复一个向量。它有助于解码器(见图 13.10)将一个向量转换为一个序列。 -
TimeDistributed()
:这将允许我们将特定类型的层分配给序列的每个元素。
这两者是tensorflow.keras.layers
集合的一部分。它们在以下代码中实现:
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Dense, Activation, Input
from tensorflow.keras.layers import BatchNormalization, Dropout
from tensorflow.keras.layers import Embedding, LSTM
from tensorflow.keras.layers import RepeatVector, TimeDistributed
from tensorflow.keras.datasets import mnist
from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping
import numpy as np
seqnc_lngth = 28 # length of the sequence; must be 28 for MNIST
ltnt_dim = 2 # latent space dimension; it can be anything reasonable
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
print('x_train shape:', x_train.shape)
print('x_test shape:', x_test.shape)
在加载数据后,我们可以如下定义模型的编码器部分:
inpt_vec = Input(shape=(seqnc_lngth, seqnc_lngth,))
l1 = Dropout(0.1)(inpt_vec)
l2 = LSTM(seqnc_lngth, activation='tanh',
recurrent_activation='sigmoid')(l1)
l3 = BatchNormalization()(l2)
l4 = Dropout(0.1)(l3)
l5 = Dense(ltnt_dim, activation='sigmoid')(l4)
# model that takes input and encodes it into the latent space
encoder = Model(inpt_vec, l5)
接下来,我们可以如下定义模型的解码器部分:
l6 = RepeatVector(seqnc_lngth)(l5)
l7 = LSTM(seqnc_lngth, activation='tanh', recurrent_activation='sigmoid',
return_sequences=True)(l6)
l8 = BatchNormalization()(l7)
l9 = TimeDistributed(Dense(seqnc_lngth, activation='sigmoid'))(l8)
autoencoder = Model(inpt_vec, l9)
最后,我们像这样编译并训练模型:
autoencoder.compile(loss='binary_crossentropy', optimizer='adam')
autoencoder.summary()
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5,
min_delta=1e-4, mode='min', verbose=1)
stop_alg = EarlyStopping(monitor='val_loss', patience=15,
restore_best_weights=True, verbose=1)
hist = autoencoder.fit(x_train, x_train, batch_size=100, epochs=1000,
callbacks=[stop_alg, reduce_lr], shuffle=True,
validation_data=(x_test, x_test))
代码应打印以下输出,显示数据集的维度,模型参数的总结,以及训练步骤,我们为了节省空间省略了训练步骤:
x_train shape: (60000, 28, 28)
x_test shape: (10000, 28, 28)
Model: "functional"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input (InputLayer) [(None, 28, 28)] 0
_________________________________________________________________
dropout_1 (Dropout) (None, 28, 28) 0
_________________________________________________________________
lstm_1 (LSTM) (None, 28) 6384
_________________________________________________________________
batch_normalization_1 (Bat (None, 28) 112
_________________________________________________________________
.
.
.
time_distributed (TimeDist (None, 28, 28) 812
=================================================================
Total params: 10,950
Trainable params: 10,838
Non-trainable params: 112
_________________________________________________________________
Epoch 1/1000
600/600 [==============================] - 5s 8ms/step - loss: 0.3542 - val_loss: 0.2461
.
.
.
模型最终会收敛到一个谷底,随后通过回调自动停止。之后,我们可以直接调用encoder
模型,将任何有效的序列(例如 MNIST 图像)转换为向量,接下来我们将执行这个操作。
结果
我们可以调用encoder
模型将任何有效的序列转换为一个向量,方法如下:
encoder.predict(x_test[0:1])
这将生成一个二维向量,其中的值对应于序列x_test[0]
的向量表示,x_test[0]
是 MNIST 测试集中的第一张图像。它可能看起来像这样:
array([[3.8787320e-01, 4.8048562e-01]], dtype=float32)
然而,请记住,这个模型是在没有监督的情况下训练的,因此,这里显示的数字肯定会有所不同!编码器模型实际上就是我们的序列到向量的模型。其余的自编码器模型用于进行重建。
如果你对自编码器模型如何从仅有两个值的向量重建 28x28 图像感到好奇,或者对整个 MNIST 测试集在学习到的二维空间中投影后的样子感兴趣,你可以运行以下代码:
import matplotlib.pyplot as plt
import numpy as np
x_hat = autoencoder.predict(x_test)
smp_idx = [3,2,1,18,4,8,11,0,61,9] # samples for 0,...,9 digits
plt.figure(figsize=(12,6))
for i, (img, y) in enumerate(zip(x_hat[smp_idx].reshape(10, 28, 28), y_test[smp_idx])):
plt.subplot(2,5,i+1)
plt.imshow(img, cmap='gray')
plt.xticks([])
plt.yticks([])
plt.title(y)
plt.show()
这将显示原始数字的样本,如图 11 所示。
图 11. 原始 MNIST 数字 0-9
以下代码生成重建数字的样本:
plt.figure(figsize=(12,6))
for i, (img, y) in enumerate(zip(x_test[smp_idx].reshape(10, 28, 28), y_test[smp_idx])):
plt.subplot(2,5,i+1)
plt.imshow(img, cmap='gray')
plt.xticks([])
plt.yticks([])
plt.title(y)
plt.show()
重建的数字如下所示,如图 12:
图 12. 使用基于 LSTM 的自编码器重建的 MNIST 数字 0-9
下一段代码将显示原始数据投影到潜在空间中的散点图,如图 13所示:
y_ = list(map(int, y_test))
X_ = encoder.predict(x_test)
plt.figure(figsize=(10,8))
plt.title('LSTM-based Encoder')
plt.scatter(X_[:,0], X_[:,1], s=5.0, c=y_, alpha=0.75, cmap='tab10')
plt.xlabel('First encoder dimension')
plt.ylabel('Second encoder dimension')
plt.colorbar()
回想一下,由于自编码器的无监督性质,这些结果可能会有所不同。同样,学习到的空间可以被直观地认为是像图 13中所示的那样,其中每个点对应一个序列(MNIST 数字),它被转化为二维向量:
图 13. 基于 MNIST 数据集的学习向量空间
从图 13中,我们可以看到,即使重建仅基于二维向量,序列到向量模型也能有效工作。我们将在下一节看到更大的表示。然而,你需要知道,序列到向量模型在过去几年里非常有用 [Zhang, Z., 等 (2017)]。
另一个有用的策略是创建向量到序列模型,即从向量表示转换到序列表示。在自编码器中,这对应于解码器部分。接下来,我们将讨论这个话题。
向量到序列模型
如果你回顾图 10,向量到序列模型将对应于解码器漏斗形状。主要理念是,大多数模型通常能够从大量输入到丰富表示之间无缝转换。然而,直到最近,机器学习社区才重新获得动力,在从向量成功生成序列方面取得了显著进展(Goodfellow, I., 等 (2016))。
你可以再次思考图 10以及其中表示的模型,它将从原始序列中生成一个序列。在这一节中,我们将专注于第二部分——解码器,并将其用作向量到序列模型。然而,在进入这一部分之前,我们将介绍另一种 RNN 的版本——双向 LSTM。
双向 LSTM
双向 LSTM(BiLSTM)简单来说,就是一个能够同时向前和向后分析序列的 LSTM,如图 14所示:
图 14. 双向 LSTM 表示
请考虑以下示例,序列被分析为向前和向后:
-
一段音频序列,在自然声音中分析,然后再反向分析(有些人这样做是为了寻找潜意识信息)。
-
一段文本序列,如一句话,既可以向前分析其风格,也可以向后分析,因为某些模式(至少在英语和西班牙语中)会向后引用;例如,一个动词引用的是在句首出现的主语。
-
一张图像,从上到下、从下到上、从一侧到另一侧或反向都有独特的形状;如果你想到数字 9,从上到下,传统的 LSTM 可能会忘记顶部的圆形部分而记住底部的细长部分,而 BiLSTM 可能能够通过从上到下和从下到上的方式同时回忆起数字的两个重要部分。
从图 14 (b)中,我们也可以观察到,正向和反向传递的状态和输出在序列中的任何位置都是可用的。
我们可以通过简单地在一个普通的 LSTM 层外面调用Bidirectional()
包装器来实现双向 LSTM。然后,我们将采用图 10中的架构,并对其进行修改,得到如下结果:
-
潜在空间中的 100 维
-
用双向 LSTM 替换 LSTM 层
-
从潜在空间到解码器的额外丢弃层
新的架构将如下所示:图 15
图 15. 实现双向 LSTM 以构建向量到序列模型
回想一下,这里最重要的一点是尽可能让潜在空间(向量到序列模型的输入)变得更加丰富,从而生成更好的序列。我们试图通过增加潜在空间的维度并添加双向 LSTM 来实现这一点。让我们继续实现这一点,并查看结果。
实现与结果
实现图 15中架构的代码如下:
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Dense, Activation, Input
from tensorflow.keras.layers import BatchNormalization, Dropout
from tensorflow.keras.layers import Bidirectional, LSTM
from tensorflow.keras.layers import RepeatVector, TimeDistributed
from tensorflow.keras.datasets import mnist
from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping
import numpy as np
seqnc_lngth = 28
ltnt_dim = 100
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
我们定义模型的编码器部分如下:
inpt_vec = Input(shape=(seqnc_lngth, seqnc_lngth,))
l1 = Dropout(0.5)(inpt_vec)
l2 = Bidirectional(LSTM(seqnc_lngth, activation='tanh',
recurrent_activation='sigmoid'))(l1)
l3 = BatchNormalization()(l2)
l4 = Dropout(0.5)(l3)
l5 = Dense(ltnt_dim, activation='sigmoid')(l4)
# sequence to vector model
encoder = Model(inpt_vec, l5, name='encoder')
模型的解码器部分可以定义如下:
ltnt_vec = Input(shape=(ltnt_dim,))
l6 = Dropout(0.1)(ltnt_vec)
l7 = RepeatVector(seqnc_lngth)(l6)
l8 = Bidirectional(LSTM(seqnc_lngth, activation='tanh',
recurrent_activation='sigmoid',
return_sequences=True))(l7)
l9 = BatchNormalization()(l8)
l10 = TimeDistributed(Dense(seqnc_lngth, activation='sigmoid'))(l9)
# vector to sequence model
decoder = Model(ltnt_vec, l10, name='decoder')
接下来,我们编译自编码器并训练它:
recon = decoder(encoder(inpt_vec))
autoencoder = Model(inpt_vec, recon, name='ae')
autoencoder.compile(loss='binary_crossentropy', optimizer='adam')
autoencoder.summary()
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5,
min_delta=1e-4, mode='min', verbose=1)
stop_alg = EarlyStopping(monitor='val_loss', patience=15,
restore_best_weights=True, verbose=1)
hist = autoencoder.fit(x_train, x_train, batch_size=100, epochs=1000,
callbacks=[stop_alg, reduce_lr], shuffle=True,
validation_data=(x_test, x_test))
这里没有什么新东西,除了之前解释过的Bidirectional()
包装器。输出应该会生成完整自编码器模型的总结以及完整的训练操作,结果看起来会是这样:
Model: "ae"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input (InputLayer) [(None, 28, 28)] 0
_________________________________________________________________
encoder (Functional) (None, 100) 18692
_________________________________________________________________
decoder (Functional) (None, 28, 28) 30716
=================================================================
Total params: 49,408
Trainable params: 49,184
Non-trainable params: 224
_________________________________________________________________
Epoch 1/1000
600/600 [==============================] - 9s 14ms/step - loss: 0.3150 - val_loss: 0.1927
.
.
.
现在,经过若干轮无监督学习,训练会自动停止,我们可以将decoder
模型作为我们的向量到序列模型。但在此之前,我们可能想快速检查重构的质量,可以通过运行与之前相同的代码生成以下图示中的图像:
图 16. 使用双向 LSTM 自编码器重构的 MNIST 数字
如果你将图 11与图 16进行比较,你会注意到重构效果要好得多,而且相比于之前模型在图 12中的重构,细节程度也得到了改善。
现在我们可以直接调用我们的向量到序列模型,输入任何兼容的向量,方法如下:
z = np.random.rand(1,100)
x_ = decoder.predict(z)
print(x_.shape)
plt.imshow(x_[0], cmap='gray')
这将生成如下输出和图 17中的图表:
(1, 28, 28)
图 17. 由模型从随机向量生成的序列
你可以生成任意数量的随机向量并测试你的向量到序列模型。另一个有趣的观察点是序列到序列模型,我们将在接下来的部分介绍。
序列到序列模型
一位 Google Brain 的科学家(Vinyals, O. 等,2015)写道:
“序列已成为监督学习中的一等公民,这要归功于循环神经网络的复兴。许多需要从一个序列映射到另一个序列的复杂任务现在可以通过序列到序列(seq2seq)框架来公式化,该框架利用链式法则有效地表示序列的联合概率。”
这非常正确,因为现在这些应用已经扩展。只需想一下以下的序列到序列项目想法:
-
文档摘要。输入序列:一份文档。输出序列:摘要。
-
图像超分辨率。输入序列:低分辨率图像。输出序列:高分辨率图像。
-
视频字幕。输入序列:视频。输出序列:文本字幕。
-
机器翻译。输入序列:源语言的文本。输出序列:目标语言的文本。
这些是令人兴奋且极具挑战性的应用。如果你使用过在线翻译工具,那么很可能你已经使用过某种类型的序列到序列模型。
在本节中,为了简单起见,我们将继续使用图 15中的自编码器作为我们的主要关注点,但为了确保我们都对序列到序列模型的一般性有共同的理解,我们将指出以下几点:
-
序列到序列模型可以跨领域映射;例如,从视频到文本或从文本到音频。
-
序列到序列模型可以在不同维度中进行映射;例如,将低分辨率图像转换为高分辨率图像,或反之,用于压缩。
-
序列到序列模型可以使用许多不同的工具,例如稠密层、卷积层和循环层。
有了这个概念,你几乎可以根据你的应用构建一个序列到序列模型。现在,我们将回到图 15中的模型,并展示自编码器是一个序列到序列模型,因为它接受图像的行序列并产生另一幅图像的行序列。由于这是一个自编码器,输入和输出的维度必须匹配。
我们将把展示之前训练过的序列到序列模型(自编码器)限制在以下简短的代码片段中,这段代码是从前一节的代码发展而来的:
plt.figure(figsize=(12,6))
for i in range(10):
plt.subplot(2,5,i+1)
rnd_vec = np.round(np.mean(x_test[y_test==i],axis=0)) #(a)
rnd_vec = np.reshape(rnd_vec, (1,28,28)) #(b)
z = encoder.predict(rnd_vec) #(c)
decdd = decoder.predict(z) #(d)
plt.imshow(decdd[0], cmap='gray')
plt.xticks([])
plt.yticks([])
plt.title(i)
plt.show()
让我们解释一下这些步骤。在(a)中,我们为每个数字计算平均序列;这是对以下问题的回答:既然随机做很简单,我们可以使用什么作为输入序列?好吧,使用平均序列来构建测试集听起来足够有趣。
接下来,(b) 只是为了使输入与编码器输入维度兼容。然后,(c) 获取平均序列并将其转换为一个向量。最后,(d) 使用该向量重新创建序列,产生以下图示所示的图表:
图 18。序列到序列示例输出
从图表中,你可以轻松观察到与手写数字一致的明确定义的模式,这些模式是由双向 LSTM 生成的行序列。
在我们结束之前,让我们谈一下这些模型的一些伦理影响。
伦理影响
随着递归模型的复兴及其在捕捉序列中的时间信息方面的适用性,存在发现潜在空间未被公平分配的风险。在操作未经妥善整理数据的无监督模型中,这种风险可能更大。如果你考虑到,模型并不关心它发现的关系;它只关心最小化损失函数,因此如果它是用 1950 年代的杂志或报纸进行训练的,它可能会发现“女性”一词与家务劳动的词汇(如“扫帚”、“碗碟”和“烹饪”)在欧几里得距离上接近,而“男性”一词则可能与所有其他劳动(如“驾驶”、“教学”、“医生”和“科学家”)接近。这就是潜在空间中引入偏见的一个例子(Shin, S.,et al. (2020))。
这里的风险在于,向量到序列或序列到序列模型会更容易将医生与男性而非女性联系起来,将烹饪与女性而非男性联系起来,仅举几个例子。你可以将这种情况扩展到面部图像,发现某些具有特定特征的人可能会被错误地关联起来。这就是为什么进行我们所做的这种分析如此重要——尽可能地尝试可视化潜在空间,查看模型输出,等等。
这里的关键要点是,虽然这里讨论的模型非常有趣且强大,但它们也带来了学习我们社会中被认为是“不可接受”的内容的风险。如果这种风险存在并且未被发现,可能会导致偏见(Amini, A., et al. (2019))。如果偏见没有被及时发现,它可能会导致多种形式的歧视。因此,请始终对这些问题以及超出你自己社会背景的事项保持谨慎。
总结
本高级章节向你展示了如何创建 RNN。你了解了 LSTM 及其双向实现,这是处理具有远程时间相关性的序列最强大的方法之一。你还学习了如何创建基于 LSTM 的情感分析模型,用于电影评论的分类。你设计了一个自编码器,使用简单的和双向的 LSTM 来学习 MNIST 的潜在空间,并将其既用作向量到序列模型,也用作序列到序列模型。
在这一点上,你应该能够自信地解释 RNN 中记忆的动机,这种动机源于对更强大模型的需求。你应该能够舒适地使用 Keras/TensorFlow 编写自己的递归网络。此外,你还应该自信地实现监督式和无监督式的递归网络。
LSTM 在编码高度相关的空间信息(如图像、音频或文本)方面表现出色,就像 CNN 一样。然而,CNN 和 LSTM 都学习非常特定的潜在空间,这些空间可能缺乏多样性。如果有恶意黑客试图破解你的系统,这可能会成为问题;如果你的模型非常特定于你的数据,它可能对变化非常敏感,从而导致输出结果出现灾难性后果。自编码器通过使用一种生成方法——变分自编码器,来解决这个问题,变分自编码器学习的是数据的分布,而不是数据本身。然而,问题依然存在:如何将这种生成方法的理念应用到其他类型的网络中,这些网络不一定是自编码器?想要找到答案,你不能错过下一章,第十四章,生成对抗神经网络。下一章将介绍一种通过攻击神经网络并教会它们变得更强大的方法。但在你继续之前,请先通过以下问题来测试自己。
问题与答案
- 如果 CNN 和 LSTM 都能建模空间相关的数据,是什么让 LSTM 更优?
一般来说没有什么特别的,除了 LSTM 具有记忆功能这一点。但在某些应用中,比如自然语言处理(NLP),其中句子是顺序发现的,并且有时会前后引用某些词语,且可能在开始、中间和结尾处都有多次引用。BiLSTM 比 CNN 更容易快速建模这种行为。CNN 可能也能学会这样做,但相比之下,它可能需要更长的时间。
- 增加更多递归层能使网络变得更好吗?
不,增加更多的层可能会使情况变得更糟。建议保持简单,最多不超过三层,除非你是科学家并在进行某些新实验。否则,在编码器模型中,不应连续有超过三层的递归层。
- LSTM 还有哪些其他应用?
音频处理与分类;图像去噪;图像超分辨率;文本摘要与其他文本处理和分类任务;词语补全;聊天机器人;文本补全;文本生成;音频生成;图像生成。
- LSTM 和 CNN 似乎有相似的应用,是什么让你选择一个而不是另一个?
LSTM 比其他模型更快收敛,因此,如果时间是一个因素,LSTM 更好。CNN 比 LSTM 更稳定,因此,如果你的输入非常不可预测,LSTM 可能会将问题带入递归层,每次使其变得更糟,这时 CNN 可以通过池化来缓解问题。在个人层面上,我通常在与图像相关的应用中首先尝试 CNN,而在 NLP 应用中则优先尝试 LSTM。
参考文献
-
Rumelhart, D. E., Hinton, G. E., 和 Williams, R. J. (1986). 通过反向传播误差学习表示. Nature, 323(6088), 533-536.
-
Mikolov, T., Sutskever, I., Chen, K., Corrado, G. S., 和 Dean, J. (2013). 词语和短语的分布式表示及其组合性. 见于 神经信息处理系统进展 (第 3111-3119 页).
-
Pennington, J., Socher, R., 和 Manning, C. D. (2014 年 10 月). Glove:用于词表示的全局向量. 见于 2014 年自然语言处理实证方法会议论文集 (EMNLP) (第 1532-1543 页).
-
Rivas, P., 和 Zimmermann, M. (2019 年 12 月). 关于英语句子质量评估的句子嵌入的实证研究. 见于 2019 年国际计算科学与计算智能会议 (CSCI) (第 331-336 页). IEEE.
-
Hochreiter, S., 和 Schmidhuber, J. (1997). 长短期记忆网络. 神经计算, 9(8), 1735-1780.
-
Zhang, Z., Liu, D., Han, J., 和 Schuller, B. (2017). 学习音频序列表示用于声学事件分类. arXiv 预印本 arXiv:1707.08729.
-
Goodfellow, I., Bengio, Y., 和 Courville, A. (2016). 序列建模:递归与递归网络. 深度学习, 367-415.
-
Vinyals, O., Bengio, S., 和 Kudlur, M. (2015). 顺序重要:集合的序列到序列. arXiv 预印本 arXiv:1511.06391.
-
Shin, S., Song, K., Jang, J., Kim, H., Joo, W., 和 Moon, I. C. (2020). 通过潜在解耦和反事实生成中和词嵌入中的性别偏见. arXiv 预印本 arXiv:2004.03133.
-
Amini, A., Soleimany, A. P., Schwarting, W., Bhatia, S. N., 和 Rus, D. (2019 年 1 月). 通过学习潜在结构揭示和缓解算法偏见. 见于 2019 年 AAAI/ACM 人工智能、伦理与社会会议论文集 (第 289-295 页).
生成对抗网络
阅读关于制作寿司的资料很容易;然而,实际上制作一种新的寿司比我们想象的要困难。在深度学习中,创造性过程更为复杂,但并非不可能。我们已经看到如何构建可以分类数字的模型,使用密集网络、卷积网络或递归网络,今天我们将看到如何构建一个可以生成数字的模型。本章介绍了一种被称为生成对抗网络的学习方法,它属于对抗学习和生成模型的范畴。本章解释了生成器和判别器的概念,以及为什么拥有良好的训练数据分布近似可以使模型在其他领域取得成功,比如数据增强。在本章结束时,你将知道为什么对抗训练如此重要;你将能够编写训练生成器和判别器所需的机制;并且你将编写一个生成对抗网络(GAN)来从学习的潜在空间生成图像。
本章的结构如下:
-
引入对抗学习
-
训练 GAN
-
比较 GAN 和 VAE
-
思考生成模型的伦理影响
第十八章:引入对抗学习
最近,使用对抗神经网络进行对抗训练引起了广泛关注(Abadi, M.等,2016)。这是因为对抗神经网络可以训练以保护模型免受基于 AI 的对抗者攻击。我们可以将对抗学习分为两个主要分支:
-
黑盒:在这一类别中,机器学习模型作为一个黑盒存在,对抗者只能学习攻击黑盒,使其失败。对抗者任意地(在某些界限内)创建伪输入使黑盒模型失败,但无法访问其攻击的模型(Ilyas, A.等,2018)。
-
内部人员:这种类型的对抗学习旨在成为训练模型的一部分,模型的目标是抵抗这种攻击。对抗者会影响一个被训练为不易被此类对抗者欺骗的模型的结果(Goodfellow, I.等,2014)。
这些方法各有优缺点:
黑盒优点 | 黑盒缺点 | 内部人员优点 | 内部人员缺点 |
---|---|---|---|
它提供了探索更多生成方法的能力。 | 没有办法影响或改变黑盒模型。 | 经过对抗训练的模型可能对特定的黑盒攻击更加鲁棒。 | 目前生成攻击的选项有限。 |
它通常较快,且可能找到破坏模型的方法。 | 生成器通常只关注扰动现有数据。 | 生成器可以用于增强数据集。 | 它通常较慢。 |
生成器可能无法用于增强数据集。 |
由于这本书是给初学者的,我们将专注于一个最简单的模型:一个被称为 GAN 的内部模型。我们将查看其各个部分并讨论其批量训练。
GANs 在历史上被用来生成逼真的图像(Goodfellow, I., 等人 (2014)),通常解决多智能体问题(Sukhbaatar, S., 等人 (2016)),甚至是密码学(Rivas, P., 等人 (2020))。
让我们简要讨论对抗学习和 GANs。
使用对手进行学习
一个机器学习模型可以传统地学习进行分类或回归等任务,其中可能有一个模型试图学习区分输入是否合法或伪造的情况。在这种情况下,一个机器学习模型可以被创建为一个生成伪造输入的对手,如图 14.1所示:
图 14.1 - 对抗学习
在这种范例中,机器学习模型需要学会区分真实输入和假输入。当它犯错时,它需要学会调整自己以确保正确识别真实输入。另一方面,对手需要继续生成假输入,目的是使模型失败。
每个模型的成功看起来是这样的:
-
机器学习主模型成功的条件是能够成功区分假的和真实的输入。
-
对手模型成功的条件是能够愚弄机器学习主模型,使其将假数据通过为真实数据。
正如你所看到的,它们彼此竞争。一个的成功是另一个的失败,反之亦然。
在学习过程中,机器学习主模型将持续调用批量的真实和假数据来学习、调整和重复,直到我们对性能满意,或者达到其他停止标准。
一般来说,在对抗学习中,对手没有具体要求,除了产生假数据。
对抗鲁棒性是一个新术语,用于证明某些模型对抗对手攻击的能力。这些证书通常是针对特定类型的对手。详细内容请参见 Cohen, J. M., 等人 (2019)。
一种流行的对抗学习类型发生在 GAN 内部,接下来我们将讨论它。
GANs
GAN 是实施对抗学习的最简单的基于神经网络的模型之一,最初由 Ian Goodfellow 和合作者在蒙特利尔的一家酒吧里构思出来(Goodfellow, I., 等人 (2014))。它基于一个极小极大化的优化问题,可以表述如下:
这个方程式有几个部分需要解释,所以我们开始吧:
-
:在 GAN 中,这是鉴别器,它是一个神经网络,接收输入数据
,并判断它是真还是假,如 图 14.2 所示。
-
:在 GAN 中,这是生成器,它也是一个神经网络,但其输入是随机噪声
,概率为
:
图 14.2 - GAN 主要范式
理想情况下,我们希望最大化鉴别器的正确预测 ,同时,我们还希望最小化生成器的误差
,生成一个不会欺骗鉴别器的样本,表示为
。期望值和对数的公式来自标准的交叉熵损失函数。
总结一下,在 GAN 中,生成器和鉴别器都是神经网络。生成器从随机分布中提取随机噪声,并利用这些噪声生成 假 输入来欺骗鉴别器。
牢记这一点,我们继续进行简单 GAN 的编码。
训练 GAN
我们将从一个简单的基于 MLP 的模型开始实现,也就是说,我们的生成器和鉴别器将是密集的、完全连接的网络。然后,我们将继续实现卷积式 GAN。
一个 MLP 模型
我们现在将专注于创建 图 14.3 中所示的模型。该模型的生成器和鉴别器在层数和总参数量上有所不同。通常情况下,生成器的构建比鉴别器需要更多的资源。如果你仔细想想,这是直观的:创作过程通常比识别过程更为复杂。在生活中,如果你反复看到毕加索的所有画作,识别他的画作可能会很容易。
然而,实际像毕加索那样绘画,可能要比这难得多:
图 14.3 - 基于 MLP 的 GAN 架构
这张图展示了一个图标,简单地表示鉴别器将同时处理假数据和真实数据,并从这两个世界中学习。关于 GAN,你必须始终记住的一点是,它们 生成 来自 随机噪声 的数据。想一想这一点,你就会意识到这非常酷。
所以,图 14.3 中的架构并没有什么我们之前没有发现的新东西。然而,设计本身是原创的。此外,用 Keras 创建它的方式确实是一项挑战。因此,我们将展示完整的代码,并尽可能多地添加注释以便让大家理解。
这是完整的代码:
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Dense, Activation, Input, Flatten
from tensorflow.keras.layers import BatchNormalization, Dropout, Reshape
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.datasets import mnist
import numpy as np
import matplotlib.pyplot as plt
img_dims = 28
img_chnl = 1
ltnt_dim = 100
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
# this makes sure that each image has a third dimension
x_train = np.expand_dims(x_train, axis=3) # 28x28x1
x_test = np.expand_dims(x_test, axis=3)
print('x_train shape:', x_train.shape)
print('x_test shape:', x_test.shape)
接下来,我们将生成器定义如下:
# building the generator network
inpt_noise = Input(shape=(ltnt_dim,))
gl1 = Dense(256, activation='relu')(inpt_noise)
gl2 = BatchNormalization()(gl1)
gl3 = Dense(512, activation='relu')(gl2)
gl4 = BatchNormalization()(gl3)
gl5 = Dense(1024, activation='relu')(gl4)
gl6 = BatchNormalization()(gl5)
gl7 = Dropout(0.5)(gl6)
gl8= Dense(img_dims*img_dims*img_chnl, activation='sigmoid')(gl7)
gl9= Reshape((img_dims,img_dims,img_chnl))(gl8)
generator = Model(inpt_noise, gl9)
gnrtr_img = generator(inpt_noise)
# uncomment this if you want to see the summary
# generator.summary()
接下来,我们可以如下定义判别器:
# building the discriminator network
inpt_img = Input(shape=(img_dims,img_dims,img_chnl))
dl1 = Flatten()(inpt_img)
dl2 = Dropout(0.5)(dl1)
dl3 = Dense(512, activation='relu')(dl2)
dl4 = Dense(256, activation='relu')(dl3)
dl5 = Dense(1, activation='sigmoid')(dl4)
discriminator = Model(inpt_img, dl5)
validity = discriminator(gnrtr_img)
# uncomment this if you want to see the summary
# discriminator.summary()
下一步是将各部分结合起来,如下所示:
# you can use either optimizer:
# optimizer = RMSprop(0.0005)
optimizer = Adam(0.0002, 0.5)
# compiling the discriminator
discriminator.compile(loss='binary_crossentropy', optimizer=optimizer,
metrics=['accuracy'])
# this will freeze the discriminator in gen_dis below
discriminator.trainable = False
gen_dis = Model(inpt_noise, validity) # full model
gen_dis.compile(loss='binary_crossentropy', optimizer=optimizer)
接下来,我们将把训练放入一个循环中,这个循环将运行多个纪元,直到我们想要的次数:
epochs = 12001 # this is up to you!
batch_size=128 # small batches recommended
sample_interval=200 # for generating samples
# target vectors
valid = np.ones((batch_size, 1))
fake = np.zeros((batch_size, 1))
# we will need these for plots and generated images
samp_imgs = {}
dloss = []
gloss = []
dacc = []
# this loop will train in batches manually for every epoch
for epoch in range(epochs):
# training the discriminator first >>
# batch of valid images
idx = np.random.randint(0, x_train.shape[0], batch_size)
imgs = x_train[idx]
# noise batch to generate fake images
noise = np.random.uniform(0, 1, (batch_size, ltnt_dim))
gen_imgs = generator.predict(noise)
# gradient descent on the batch
d_loss_real = discriminator.train_on_batch(imgs, valid)
d_loss_fake = discriminator.train_on_batch(gen_imgs, fake)
d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)
# next we train the generator with the discriminator frozen >>
# noise batch to generate fake images
noise = np.random.uniform(0, 1, (batch_size, ltnt_dim))
# gradient descent on the batch
g_loss = gen_dis.train_on_batch(noise, valid)
# save performance
dloss.append(d_loss[0])
dacc.append(d_loss[1])
gloss.append(g_loss)
# print performance every sampling interval
if epoch % sample_interval == 0:
print ("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" %
(epoch, d_loss[0], 100*d_loss[1], g_loss))
# use noise to generate some images
noise = np.random.uniform(0, 1, (2, ltnt_dim))
gen_imgs = generator.predict(noise)
samp_imgs[epoch] = gen_imgs
这将产生类似以下的输出:
0 [D loss: 0.922930, acc.: 21.48%] [G loss: 0.715504]
400 [D loss: 0.143821, acc.: 96.88%] [G loss: 4.265501]
800 [D loss: 0.247173, acc.: 91.80%] [G loss: 4.752715]
.
.
.
11200 [D loss: 0.617693, acc.: 66.80%] [G loss: 1.071557]
11600 [D loss: 0.611364, acc.: 66.02%] [G loss: 0.984210]
12000 [D loss: 0.622592, acc.: 62.50%] [G loss: 1.056955]
由于一切基于随机噪声,这在你的系统中可能看起来不同。这个随机性可能会让你的模型朝不同的方向发展。然而,你会看到的是,如果生成器正常工作,它的损失应该会逐渐减少,并且准确率应该接近于随机变化,也就是接近 50%。如果判别器始终保持 100%的准确率,说明生成器表现不佳;如果判别器的准确率接近 50%,那么可能是生成器过于优秀,或者判别器过于弱。
现在,让我们绘制几样东西:学习曲线(损失和准确率),以及不同纪元生成的样本。
以下代码将绘制学习曲线:
import matplotlib.pyplot as plt
fig, ax1 = plt.subplots(figsize=(10,6))
ax1.set_xlabel('Epoch')
ax1.set_ylabel('Loss')
ax1.plot(range(epochs), gloss, '-.', color='#dc267f', alpha=0.75,
label='Generator')
ax1.plot(range(epochs), dloss, '-.', color='#fe6100', alpha=0.75,
label='Discriminator')
ax1.legend(loc=1)
ax2 = ax1.twinx()
ax2.set_ylabel('Discriminator Accuracy')
ax2.plot(range(epochs), dacc, color='#785ef0', alpha=0.75,
label='Accuracy')
ax2.legend(loc=4)
fig.tight_layout()
plt.show()
这将生成如下图所示的图表:
图 14.4 - 生成器和判别器在多个纪元中的损失变化。基于 MLP 的 GAN 在多个纪元中的准确率。
如图所示,判别器的损失最初较低,准确率也显示了这一点。然而,随着纪元的推进,生成器的表现变得更好(损失减少),而准确率则缓慢下降。
图 14.5显示了在每个采样的纪元下生成的一些图像,这些图像是由随机噪声生成的:
图 14.5 - GAN 在不同纪元生成的图像
如你所见,最初的图像看起来噪声较大,而后来的图像则有更多的细节和熟悉的形状。这将证实判别器准确率的下降,因为这些图像看起来几乎与真实图像相似。图 14.5是使用以下代码生成的:
import matplotlib.pyplot as plt
fig, axs = plt.subplots(6, 10, figsize=(10,7.5))
cnt = sample_interval
for i in range(6):
for j in [0, 2, 4, 6, 8]:
img = samp_imgs[cnt]
axs[i,j].imshow(img[0,:,:,0], cmap='gray')
axs[i,j].axis('off')
axs[i,j].set_title(cnt)
axs[i,j+1].imshow(img[1,:,:,0], cmap='gray')
axs[i,j+1].axis('off')
axs[i,j+1].set_title(cnt)
cnt += sample_interval
plt.show()
让我们考虑一下这个模型的一些收获:
-
如前所述的模型,如果我们在需要的地方将模型做大,仍然有提升的空间。
-
如果我们需要一个好的生成器,我们可以扩展生成器,或者将其改为卷积生成器(见下一节)。
-
如果我们愿意,可以保存
discriminator
并重新训练它(微调)用于数字分类。 -
如果我们愿意,可以使用
generator
来增强数据集,添加任意数量的图像。
尽管基于 MLP 的 GAN 质量尚可,我们可以看到,生成的形状可能不如原始样本定义得那么清晰。然而,卷积 GAN 能够提供帮助。
让我们继续,将基于 MLP 的模型转换为卷积模型。
一个卷积模型
卷积方法在 GAN 中的应用由 Radford 等人(2015)广泛推广。所提出的模型被称为深度卷积 GAN(DCGAN)。其主要目标是通过一系列卷积层学习特征表示,以生成虚假图像或区分真实和虚假图像。
接下来,我们将故意使用不同的名称来指代判别网络,我们称之为批评者。这两个术语在文献中都有使用。然而,新的趋势是使用术语批评者,而旧的术语可能会逐渐消失。无论如何,你应该知道这两个术语指的都是同一个东西:一个任务是判断输入是有效的(来自原始数据集)还是伪造的(来自对抗生成器)。
我们将实现以下图示中的模型:
图 14.6 - 基于 CNN 的 GAN 架构
这个模型有一些在本书中前所未见的新内容:Conv2DTranspose
。这种层与传统的卷积层Conv2D
非常相似,不同之处在于它的工作方向正好相反。Conv2D
层学习过滤器(特征图),将输入拆分为过滤后的信息,而Conv2DTranspose
层则是将过滤后的信息合并在一起。
有些人将Conv2DTranspose
称为反卷积。然而,我个人认为这样做是不正确的,因为反卷积是一个与Conv2DTranspose
执行的操作有显著不同的数学运算。无论如何,你需要记住,如果在 CNN 的上下文中看到反卷积,它指的就是Conv2DTranspose
。
模型中的其余元素是我们之前已经讨论过的内容。完整的代码(去除注释)如下:
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Dense, Activation, Input, Conv2DTranspose, Flatten
from tensorflow.keras.layers import BatchNormalization, Dropout, Reshape, Conv2D
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.datasets import mnist
import numpy as np
import matplotlib.pyplot as plt
img_dims = 28
img_chnl = 1
ltnt_dim = 100
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
x_train = np.expand_dims(x_train, axis=3)
x_test = np.expand_dims(x_test, axis=3)
接下来,我们定义如下的生成器:
# building the generator convolutional network
inpt_noise = Input(shape=(ltnt_dim,))
gl1 = Dense(7*7*256, activation='relu')(inpt_noise)
gl2 = BatchNormalization()(gl1)
gl3 = Reshape((7, 7, 256))(gl2)
gl4 = Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same',
activation='relu')(gl3)
gl5 = BatchNormalization()(gl4)
gl6 = Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same',
activation='relu')(gl5)
gl7 = BatchNormalization()(gl6)
gl8 = Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same',
activation='sigmoid')(gl7)
generator = Model(inpt_noise, gl8)
gnrtr_img = generator(inpt_noise)
generator.summary() # print to verify dimensions
然后,我们定义如下的批评网络:
# building the critic convolutional network
inpt_img = Input(shape=(img_dims,img_dims,img_chnl))
dl1 = Conv2D(64, (5, 5), strides=(2, 2), padding='same',
activation='relu')(inpt_img)
dl2 = Dropout(0.3)(dl1)
dl3 = Conv2D(128, (5, 5), strides=(2, 2), padding='same',
activation='relu')(dl2)
dl4 = Dropout(0.3)(dl3)
dl5 = Flatten()(dl4)
dl6 = Dense(1, activation='sigmoid')(dl5)
critic = Model(inpt_img, dl6)
validity = critic(gnrtr_img)
critic.summary() # again, print for verification
接下来,我们将各部分组合起来并设置模型的参数如下:
optimizer = Adam(0.0002, 0.5)
critic.compile(loss='binary_crossentropy', optimizer=optimizer,
metrics=['accuracy'])
critic.trainable = False
gen_crt = Model(inpt_noise, validity)
gen_crt.compile(loss='binary_crossentropy', optimizer=optimizer)
epochs = 12001
batch_size=64
sample_interval=400
然后,我们使用以下周期进行训练:
valid = np.ones((batch_size, 1))
fake = np.zeros((batch_size, 1))
samp_imgs = {}
closs = []
gloss = []
cacc = []
for epoch in range(epochs):
idx = np.random.randint(0, x_train.shape[0], batch_size)
imgs = x_train[idx]
noise = np.random.uniform(0, 1, (batch_size, ltnt_dim))
gen_imgs = generator.predict(noise)
c_loss_real = critic.train_on_batch(imgs, valid)
c_loss_fake = critic.train_on_batch(gen_imgs, fake)
c_loss = 0.5 * np.add(c_loss_real, c_loss_fake)
noise = np.random.uniform(0, 1, (batch_size, ltnt_dim))
g_loss = gen_crt.train_on_batch(noise, valid)
closs.append(c_loss[0])
cacc.append(c_loss[1])
gloss.append(g_loss)
if epoch % sample_interval == 0:
print ("%d [C loss: %f, acc.: %.2f%%] [G loss: %f]" %
(epoch, d_loss[0], 100*d_loss[1], g_loss))
noise = np.random.uniform(0, 1, (2, ltnt_dim))
gen_imgs = generator.predict(noise)
samp_imgs[epoch] = gen_imgs
前面 70%的代码与之前相同。然而,卷积网络的设计是新的。代码将打印生成器和批评网络的摘要。以下是生成器的摘要:
Model: "Generator"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) [(None, 100)] 0
_________________________________________________________________
dense_1 (Dense) (None, 12544) 1266944
_________________________________________________________________
batch_normalization_1 (Batch (None, 12544) 50176
_________________________________________________________________
reshape (Reshape) (None, 7, 7, 256) 0
_________________________________________________________________
conv2d_transpose_1 (Conv2DTran (None, 7, 7, 128) 819328
_________________________________________________________________
batch_normalization_2 (Batch (None, 7, 7, 128) 512
_________________________________________________________________
conv2d_transpose_2 (Conv2DTr (None, 14, 14, 64) 204864
_________________________________________________________________
batch_normalization_3 (Batch (None, 14, 14, 64) 256
_________________________________________________________________
conv2d_transpose_3 (Conv2DTr (None, 28, 28, 1) 1601
=================================================================
Total params: 2,343,681
Trainable params: 2,318,209
Non-trainable params: 25,472
这是批评者的摘要:
_________________________________________________________________
Model: "Critic"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_2 (InputLayer) [(None, 28, 28, 1)] 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 14, 14, 64) 1664
_________________________________________________________________
dropout_1 (Dropout) (None, 14, 14, 64) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 7, 7, 128) 204928
_________________________________________________________________
dropout_2 (Dropout) (None, 7, 7, 128) 0
_________________________________________________________________
flatten (Flatten) (None, 6272) 0
_________________________________________________________________
dense_2 (Dense) (None, 1) 6273
=================================================================
Total params: 212,865
Trainable params: 212,865
Non-trainable params: 0
训练步骤的一个示例输出如下所示:
0 [C loss: 0.719159, acc.: 22.66%] [G loss: 0.680779]
400 [C loss: 0.000324, acc.: 100.00%] [G loss: 0.000151]
800 [C loss: 0.731860, acc.: 59.38%] [G loss: 0.572153]
.
.
.
11200 [C loss: 0.613043, acc.: 66.41%] [G loss: 0.946724]
11600 [C loss: 0.613043, acc.: 66.41%] [G loss: 0.869602]
12000 [C loss: 0.613043, acc.: 66.41%] [G loss: 0.854222]
从训练输出中,我们可以看到卷积网络比 MLP 对手更快地减少生成器的损失。似乎在剩余的 epochs 中,批评者学习得较慢,以便更能抵抗生成的伪输入。通过使用以下代码绘制结果,可以更清楚地观察到这一点:
import matplotlib.pyplot as plt
fig, ax1 = plt.subplots(figsize=(10,6))
ax1.set_xlabel('Epoch')
ax1.set_ylabel('Loss')
ax1.plot(range(epochs), gloss, '-.', color='#dc267f', alpha=0.75,
label='Generator')
ax1.plot(range(epochs), closs, '-.', color='#fe6100', alpha=0.75,
label='Critic')
ax1.legend(loc=1)
ax2 = ax1.twinx()
ax2.set_ylabel('Critic Accuracy')
ax2.plot(range(epochs), cacc, color='#785ef0', alpha=0.75,
label='Accuracy')
ax2.legend(loc=4)
fig.tight_layout()
plt.show()
代码生成了如图 14.7所示的图表。从图中,我们可以看到关于更快收敛到小损失和批评者准确度恢复较慢的声明:
图 14.7 - 基于 CNN 的 GAN 学习曲线
我们还可以显示在训练卷积生成对抗网络时生成的样本。结果如图 14.8所示。这些结果与在 2000 个 epochs 下训练出的低质量生成器一致。在经过 5000 个 epochs 后,生成器能够生成定义明确的数字,这些数字可以轻松通过验证:
图 14.8 - 训练过程中生成的样本
作为参考,我们可以分别比较图 14.5和图 14.8,它们展示了基于 MLP 和卷积的不同方法。这样的比较有助于了解通用 GAN(基于 MLP)和专注于空间关系的 GAN(基于 CNN)之间的根本差异。
现在,我们简要讨论一下变分自编码器(VAEs)和生成对抗网络(GANs)所带来的生成能力。
比较 GAN 和 VAE
在第九章中,我们讨论了 VAE 作为一种降维机制,旨在学习输入空间分布的参数,并基于从潜在空间中随机抽取的样本,利用学习到的参数进行重建。这提供了我们在第九章中已讨论过的若干优点,如下所示:
-
由于 VAE 学习的是输入的分布,而非输入本身,因此它有能力减小噪声输入的影响。
-
通过简单查询潜在空间生成样本的能力
另一方面,GAN 也可以像 VAE 一样生成样本。然而,两者的学习过程有很大不同。在 GAN 中,我们可以将模型视为由两个主要部分组成:一个判别器和一个生成器。而在 VAE 中,我们也有两个网络:一个编码器和一个解码器。
如果我们要在两者之间做任何联系,那就是解码器和生成器在 VAE 和 GAN 中分别扮演着非常相似的角色。然而,编码器和判别器的目标则截然不同。编码器的目标是学习找到一个丰富的潜在表示,通常这个表示的维度远小于输入空间。而判别器的目标不是去找到任何表示,而是解决一个日益复杂的二分类问题。
我们可以主张判别器肯定是在学习输入空间的特征;然而,判别器和编码器中最深层的特征是否相似,这一说法还需要更多的证据。
我们可以通过比较的方法是,采用第九章中的深度 VAE 模型,变分自编码器,图 14.7,对其进行训练,并从 VAE 的生成器中随机抽取一些样本,同时对卷积 GAN 做同样的操作。
我们可以通过显示卷积 GAN 的样本并在前一节最后一段代码后立即执行以下代码来开始。该代码包含了已训练好的 GAN。代码如下:
import matplotlib.pyplot as plt
import numpy as np
plt.figure(figsize=(10,10))
samples = np.random.uniform(0.0, 1.0, size=(400,ltnt_dim))
imgs = generator.predict(samples)
for cnt in range(20*20):
plt.subplot(20,20,cnt+1)
img = imgs[cnt]
plt.imshow(img[:,:,0], cmap='gray')
plt.xticks([])
plt.yticks([])
plt.show()
这段代码将从随机噪声中生成 400 个数字!绘图如图 14.9所示:
图 14.9 - 由卷积 GAN 生成的 400 个数字
回想一下,这些数字是在经过 12,000 次训练后生成的。质量似乎相对较好。这些数字大多数可能会欺骗人类,让他们以为它们真的是人类写的。
现在,我们想看看使用 VAE 生成的数字质量。为此,你需要参考第九章,变分自编码器,并使用提供的代码实现深度 VAE,并训练大约 5,000 次迭代。训练完成后,你可以使用解码器通过选择随机参数从随机噪声中生成样本。
一旦 VAE 的训练完成,以下是你应该使用的代码:
import matplotlib.pyplot as plt
import numpy as np
plt.figure(figsize=(10,10))
samples = np.random.normal(0.0, 1.0, size=(400,ltnt_dim))
imgs = decoder.predict(samples)
for cnt in range(20*20):
plt.subplot(20,20,cnt+1)
img = imgs[cnt].reshape((28,28))
plt.imshow(img, cmap='gray')
plt.xticks([])
plt.yticks([])
plt.show()
几个明显的区别是,VAE 假设潜在空间的参数遵循正态分布;此外,输出需要被重塑为 28x28,而 GAN 则通过 2D 卷积输出层直接生成正确形状的输出。这段代码的输出如图 14.10所示:
图 14.10 - 由 VAE 解码器使用随机噪声生成的 400 个数字样本
从图中可以看出,这些数字中的一些看起来非常好;有些人可能会说看起来太好。它们看起来平滑、圆润,或许我们可以说没有噪点。与由 GAN 生成的数字相比,VAE 生成的数字缺少那种带有噪音感的独特特征。然而,这可以是好事,也可以是坏事,取决于你想做什么。
假设你想要一些干净的样本,容易被识别为伪造的,那么 VAE 是最好的选择。现在,假设我们希望生成的样本能轻易地让人类相信它们不是机器生成的;在这种情况下,可能 GAN 更合适。
尽管有这些区别,两者都可以用来扩充你的数据集,如果你需要更多的数据。
思考 GAN 的伦理影响
关于生成模型的一些伦理思考已经在第九章,变分自编码器中提到。然而,鉴于 GAN 的对抗性质,第二轮的思考是必要的。也就是说,GAN 隐含地要求在最小-最大博弈中“欺骗”一个判别器,生成器需要取得胜利(或者判别器也可以)。这一概念推广到对抗学习,为攻击现有的机器学习模型提供了手段。
很成功的计算机视觉模型,如 VGG16(一个 CNN 模型),已经遭受过执行对抗攻击的模型攻击。有一些补丁可以打印出来,放在 T 恤、帽子或任何物体上,当这些补丁出现在输入到被攻击的模型中时,模型会被欺骗,认为原本存在的物体是完全不同的(Brown, T. B., 等人(2017))。这里有一个示例,通过对抗性补丁将香蕉欺骗成烤面包机:youtu.be/i1sp4X57TL4
。
既然这些类型的对抗攻击已经被确认存在,研究人员也发现了当前系统中的漏洞。因此,作为深度学习从业者,我们几乎有责任确保我们的模型在面对对抗攻击时具有鲁棒性。这对于涉及敏感信息的系统,或做出可能影响人类生命的决策的系统尤为重要。
例如,部署在机场以协助安全工作的深度学习模型需要进行测试,以避免某人穿着印有对抗性补丁的 T 恤,意图避免被识别为受限区域内的人员。这对人口安全至关重要。然而,用于自动调节某人唱歌音频的深度学习系统可能并不是特别关键。
你需要做的是测试你的模型是否受到对抗性攻击的影响。网上有多个资源,并且经常更新,只需搜索就可以轻松找到。如果你发现深度学习模型中存在漏洞,你应该立即向开发者报告,以确保我们社会的福祉。
摘要
本章高级内容向你展示了如何创建 GAN 网络。你了解了 GAN 的主要组件:生成器和判别器,以及它们在学习过程中的作用。你了解了对抗性学习的概念,尤其是在打破模型并使其对攻击具备鲁棒性方面。你基于同一数据集编写了基于 MLP 和基于卷积的 GAN,并观察了它们的差异。到此为止,你应该能够自信地解释为什么对抗训练如此重要。你应该能够编写必要的机制来训练 GAN 的生成器和判别器。你应该对编写 GAN 代码并与 VAE 进行对比,从已学习的潜在空间生成图像充满信心。你应该能够设计生成模型,考虑到其社会影响及使用生成模型时应承担的责任。
GAN 非常有趣,并且已经带来了令人惊叹的研究和应用。它们还暴露了其他系统的脆弱性。目前深度学习的状态涉及 AE、GAN、CNN 和 RNN 的组合,利用每种方法的特定组件,并逐步提高深度学习在各个领域的应用潜力。如今深度学习的世界充满了激动人心的前景,你现在已经准备好去拥抱它,并深入探讨你感兴趣的任何领域。第十五章,关于深度学习未来的最终评价,将简要评论我们对深度学习未来的看法。它尝试以某种预言性的语气谈论未来的事情。但在你离开之前,请用以下问题自我测试一下。
问题与答案
- 在 GAN 中,谁是对手?
生成器。它充当一个模型,唯一的目的就是让评论员失败;它是评论员的对手。
- 为什么生成器模型比评论员模型更大?
这并非总是如此。这里讨论的模型作为数据生成器更为有趣。然而,我们可以利用评论员并重新训练它用于分类,在这种情况下,评论员模型可能会更大。
- 什么是对抗性鲁棒性?
这是深度学习中的一个新领域,致力于研究如何验证深度学习模型能够抵御对抗性攻击。
- 哪一个更好——GAN 还是 VAE?
这取决于应用场景。与 VAE 相比,GAN 往往能产生更“有趣”的结果,但 VAE 更稳定。此外,训练 GAN 通常比训练 VAE 更快。
- GAN 是否有任何风险?
是的。有一个已知的问题叫做模式崩溃,它指的是 GAN 在不同训练周期中无法生成新颖且不同的结果。网络似乎会卡在一些样本上,这些样本足以在评论员中引起足够的混淆,从而生成较低的损失,但生成的数据缺乏多样性。这仍然是一个悬而未决的问题,没有普遍的解决方案。GAN 生成器缺乏多样性是其崩溃的迹象。想了解更多关于模式崩溃的信息,请阅读 Srivastava, A., 等人(2017 年)。
参考文献
-
Abadi, M., 和 Andersen, D. G. (2016). 使用对抗性神经密码学保护通信。arXiv 预印本 arXiv:1610.06918。
-
Ilyas, A., Engstrom, L., Athalye, A., 和 Lin, J. (2018). 有限查询和信息下的黑盒对抗性攻击。arXiv 预印本 arXiv:1804.08598。
-
Goodfellow, I., Pouget-Abadie, J., Mirza, M., Xu, B., Warde-Farley, D., Ozair, S., 和 Bengio, Y. (2014). 生成对抗网络。载于 神经信息处理系统进展(第 2672-2680 页)。
-
Sukhbaatar, S., 和 Fergus, R. (2016). 使用反向传播学习多智能体通信。载于 神经信息处理系统进展(第 2244-2252 页)。
-
Rivas, P., 和 Banerjee, P. (2020). 基于神经网络的 ECB 模式图像对抗加密,采用 16 位块。发表于 国际人工智能会议。
-
Cohen, J. M., Rosenfeld, E., 和 Kolter, J. Z. (2019). 通过随机平滑获得认证的对抗性鲁棒性。arXiv 预印本 arXiv:1902.02918。
-
Radford, A., Metz, L., 和 Chintala, S. (2015). 使用深度卷积生成对抗网络进行无监督表示学习。arXiv 预印本 arXiv:1511.06434。
-
Brown, T. B., Mané, D., Roy, A., Abadi, M., 和 Gilmer, J. (2017). 对抗性补丁。arXiv 预印本 arXiv:1712.09665。
-
Srivastava, A., Valkov, L., Russell, C., Gutmann, M. U., 和 Sutton, C. (2017). Veegan:使用隐式变分学习减少 GAN 中的模式崩溃。发表于 神经信息处理系统进展(第 3308-3318 页)。
深度学习未来的最终思考
我们一起经历了一段旅程,如果你读到这里,你值得犒劳自己一顿美餐。你所取得的成就值得被认可。告诉你的朋友们,分享你所学到的内容,并记得始终保持学习。深度学习是一个快速发展的领域;你不能停滞不前。本章将简要介绍一些深度学习中新的、令人兴奋的主题和机会。如果你希望继续学习,我们将推荐一些来自 Packt 的其他有用资源,帮助你在这个领域继续前进。在本章结束时,你将知道在学习了深度学习的基础知识后,接下来该往哪里去;你将知道 Packt 提供的其他资源,帮助你继续深度学习的训练。
本章组织成以下几个部分:
-
寻找深度学习中的高级主题
-
使用 Packt 的更多资源进行学习
第十九章:寻找深度学习中的高级主题
目前很难预测深度学习的未来;事物发展得非常迅速。然而,我相信,如果你把时间投资在当前的深度学习高级主题上,你可能会在不久的将来看到这些领域繁荣发展。
以下小节将讨论一些具有潜力、可能在我们领域蓬勃发展并带来颠覆性影响的高级主题。
深度强化学习
深度强化学习(DRL)是一个近年来备受关注的领域,因为深度卷积网络及其他类型的深度网络已经为过去难以解决的问题提供了解决方案。DRL 的许多应用场景是在我们无法拥有所有可能情境数据的领域,例如太空探索、电子游戏或自动驾驶汽车。
让我们扩展一下后面的例子。如果我们使用传统的监督学习来构建一辆自动驾驶汽车,让它能够从 A 点安全到达 B 点,我们不仅需要正面类的示例(成功的行程),还需要负面类的示例(例如车祸和糟糕的驾驶)。想想看:为了保持数据集的平衡,我们需要和成功事件一样多的车祸数据。这是不可接受的;然而,强化学习能够帮助我们解决这个问题。
深度强化学习(DRL)旨在奖励良好的驾驶行为;模型会学习到可以获得奖励,因此我们不需要负面示例。相比之下,传统学习需要通过撞车来惩罚不良结果。
当你使用深度强化学习(DRL)通过模拟器进行学习时,你可以获得能够在模拟飞行中击败飞行员的 AI(fortune.com/2020/08/20/f-16-fighter-pilot-versus-artificial-intelligence-simulation-darpa/
),或者你可以获得能够在视频游戏模拟器中获胜的 AI。游戏世界是 DRL 的一个完美测试场景。假设你想要制作一个 DRL 模型来玩著名的游戏太空侵略者,如图 15.1所示;你可以创建一个奖励摧毁太空侵略者的模型。
图 15.1 – 太空侵略者视频游戏模拟器
如果你创建一个传统的模型来教用户不死,例如,你最终还是会失败,因为你最终会被太空入侵。因此,防止入侵的最佳策略是既不死又摧毁太空入侵者。换句话说,你奖励那些导致生存的行动,也就是在避免被它们的炸弹击中的同时迅速摧毁太空入侵者。
2018 年,发布了一个新的 DRL 研究工具,名为Dopamine(Castro, P. S., 等,2018)。Dopamine(github.com/google/dopamine
)用于快速原型开发强化学习算法。在第二章中,深度学习框架的设置与介绍,我们要求你为此时安装 Dopamine。我们只是想给你一个 Dopamine 的使用概念,让你可以在感兴趣时继续进行实验。在接下来的几行代码中,我们将简单地加载一个预训练模型(智能体),并让它玩游戏。
这将确保库已安装,然后加载预训练的智能体:
!pip install -U dopamine-rl
!gsutil -q -m cp -R gs://download-dopamine-rl/colab/samples/rainbow/SpaceInvaders_v4/checkpoints/tf_ckpt-199.data-00000-of-00001 ./
!gsutil -q -m cp -R gs://download-dopamine-rl/colab/samples/rainbow/SpaceInvaders_v4/checkpoints/tf_ckpt-199.index ./
!gsutil -q -m cp -R gs://download-dopamine-rl/colab/samples/rainbow/SpaceInvaders_v4/checkpoints/tf_ckpt-199.meta ./
这个经过训练的智能体在这里被称为rainbow
,是 Dopamine 的作者提供的,但如果你愿意,也可以训练自己的智能体。
下一步是让智能体运行(即,根据奖励决定采取的行动)若干步,例如1024
步:
from dopamine.utils import example_viz_lib
example_viz_lib.run(agent='rainbow', game='SpaceInvaders', num_steps=1024,
root_dir='./agent_viz', restore_ckpt='./tf_ckpt-199',
use_legacy_checkpoint=True)
这段代码可能需要一段时间才能运行。内部,它连接到 PyGame,这是 Python 社区的一个游戏模拟器资源。它做出几个决策,并避免太空入侵(以及死亡)。如图 15.2所示,模型描述了在时间步长中的累积奖励,以及每个动作的回报估计概率,例如停止、向左移动、向右移动和射击:
图 15.2 – 左:模型在时间步长中的计算奖励。右:每个行动的回报估计概率
这一点有趣的地方在于,你可以在任何时间步骤(帧)上可视化代理,并查看代理在特定时间步骤上做了什么。你可以使用图 15.2中的图来参考决定要可视化哪个时间步骤。假设你想要可视化步骤 540 或 550,你可以按如下方式进行:
from IPython.display import Image
frame_number = 540 # or 550
image_file = '/<path to current directory>/agent_viz/SpaceInvaders/rainbow/images/frame_{:06d}.png'.format(frame_number)
Image(image_file)
你需要将<path to current directory>
替换为你当前工作目录的路径。这是因为我们需要绝对路径,否则我们本可以使用带有./
的相对路径。
从中可以显而易见,所有的帧都作为图像保存在./agent_viz/SpaceInvaders/rainbow/images/
目录下。你可以单独展示它们,甚至制作成视频。前面的代码生成了图 15.3中所示的图像:
图 15.3 – 左:步骤 540。右:步骤 550
多巴胺就是这么简单。我们希望你能从强化学习中得到启发并进一步研究。
自监督学习
2018 年 ACM 图灵奖获得者之一的杨·勒昆在 2020 年 AAAI 会议上说道:"未来是自监督的。" 他暗示这个领域非常激动人心,并且具有巨大的潜力。
自监督是一个相对较新的术语,用来替代无监督这一术语。术语“无监督学习”可能会给人一种没有监督的印象,而实际上,无监督学习算法和模型通常使用比监督模型更多的监督数据。以 MNIST 数据集的分类为例,它使用 10 个标签作为监督信号。然而,在一个目标是完美重建的自编码器中,每一个像素都是一个监督信号,因此以 28 x 28 的图像为例,便有 784 个监督信号。
自监督还用来指代结合了无监督学习和监督学习某些阶段的模型。例如,如果我们管道化一个学习无监督表示的模型,我们可以在其下游附加一个模型,该模型将学习进行有监督的分类。
最近深度学习的许多进展都发生在自监督领域。如果你能进一步学习自监督学习算法和模型,将是非常值得投入的时间。
系统 2 算法
著名经济学家丹尼尔·卡尼曼在他的书《思考,快与慢》(Kahneman, D. 2011)中推广了双过程理论。主要思想是,有一些高度复杂的任务,我们人类能够相对快速且常常不需要过多思考地完成;例如,喝水、吃饭或看一个物体并识别它。这些过程由系统 1完成。
然而,有些任务对于人类大脑来说并不简单,这些任务需要我们全身心的注意力,比如在陌生的道路上驾驶,观察不属于预期背景的奇异物体,或理解一幅抽象画。这些过程由系统 2完成。2018 年 ACM 图灵奖的另一个获奖者 Yoshua Bengio 曾指出,深度学习在系统 1任务上表现得非常出色,这意味着现有模型能够相对轻松地识别物体并执行高度复杂的任务。然而,深度学习在系统 2任务上进展不大。也就是说,深度学习的未来将是解决那些对人类非常复杂的任务,这可能涉及跨不同领域、不同学习类型结合多种模型。胶囊神经网络可能是应对系统 2任务的一个良好替代方案(Sabour, S., 等,2017 年)。
由于这些原因,系统 2算法可能会成为深度学习的未来。
现在,让我们来看看来自 Packt 的资源,它们可以帮助进一步研究这些概念。
来自 Packt 的更多学习资源
以下的书单并不意在详尽无遗,而是为你的下一个探索提供一个起点。这些书籍的出版正值该领域引起广泛关注的好时机。无论你选择哪一本,你都不会失望。
强化学习
-
《深度强化学习实战(第二版)》,Maxim Lapan 著,2020 年出版。
-
《强化学习工作坊》,Alessandro Palmas 等著,2020 年出版。
-
《游戏中的实践强化学习》,Micheal Lanham 著,2020 年出版。
-
《PyTorch 1.x 强化学习实战》,Yuxi Liu 著,2019 年出版。
-
《Python 强化学习》,Sudharsan Ravichandiran 著,2019 年出版。
-
《Python 强化学习算法》,Andrea Lonza 著,2019 年出版。
自监督学习
-
《无监督学习工作坊》,Aaron Jones 等著,2020 年出版。
-
《Python 应用无监督学习》,Benjamin Johnston 等著,2019 年出版。
-
《用 Python 实践无监督学习》,Giuseppe Bonaccorso 著,2019 年出版。
总结
本章简要讨论了深度学习中的新兴话题和机会。我们讨论了强化学习、自监督算法和系统 2算法。我们还推荐了一些来自 Packt 的进一步学习资源,希望你能继续学习,并在这一领域不断前进。此时,你应该知道接下来要做什么,并为深度学习的未来而感到鼓舞。你应该了解该领域的其他推荐书籍,以继续你的学习旅程。
你是深度学习的未来,而未来就是今天。勇敢迈出一步,去实现你的目标。
参考文献
-
Castro, P. S., Moitra, S., Gelada, C., Kumar, S., 和 Bellemare, M. G.(2018)。Dopamine: A research framework for deep reinforcement learning. arXiv 预印本 arXiv:1812.06110。
-
Kahneman, D. (2011). 思考,快与慢。麦克米兰出版社。
-
Sabour, S., Frosst, N., 和 Hinton, G. E. (2017). 胶囊之间的动态路由。载于 神经信息处理系统的进展(第 3856-3866 页)。