深度学习示例-全-

深度学习示例(全)

原文:annas-archive.org/md5/81c037237f3318d7e4e398047d4d8413

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

本书将从介绍机器学习的基础开始,阐述什么使学习变得可见,通过一些示例演示传统的机器学习技术,最终介绍深度学习。接着,您将学习如何创建机器学习模型,并最终引导您进入神经网络的领域。您将熟悉深度学习的基础,并探索各种能够以强大而易用的方式支持深度学习的工具。通过一个非常低的起点,本书将帮助普通开发者亲身体验深度学习。您将学习所需的所有基础知识,以便探索和理解深度学习,并亲自执行深度学习任务。此外,我们将使用一个广泛使用的深度学习框架。TensorFlow 拥有一个日益壮大的社区支持,是构建复杂深度学习应用程序的一个很好的选择。

本书适合人群

本书是为那些热衷于了解深度学习并实现它的人而准备的,但这些人并没有广泛的机器学习、复杂统计学和线性代数背景。

本书涵盖的内容

第一章,数据科学——鸟瞰图,解释了数据科学或机器学习是赋予机器从数据集中学习的能力,而不需要明确的指令或编程。例如,编写一个程序,从手写数字的输入图像中输出 0 到 9 之间的一个值,根据图像中写下的数字来判断,这是极其困难的。同样,判断进入的电子邮件是垃圾邮件还是非垃圾邮件的任务也是如此。为了解决这些任务,数据科学家使用数据科学或机器学习领域的学习方法和工具,教计算机如何通过一些能区分每个数字的特征来自动识别数字。对于垃圾邮件/非垃圾邮件问题也是一样,我们可以通过特定的学习算法来教计算机如何区分垃圾邮件和非垃圾邮件,而不是使用常规表达式并编写数百条规则来分类进入的电子邮件。

第二章,数据建模实践——泰坦尼克号示例,线性模型是数据科学领域中最基本的学习算法。理解线性模型的工作原理对于您学习数据科学的旅程至关重要,因为它是大多数复杂学习算法的基本构建块,包括神经网络。

第三章,特征工程与模型复杂度 - 再访 Titanic 示例,涵盖了模型复杂度和评估。这是构建成功数据科学系统的重要一步。有许多工具可以帮助你评估和选择模型。在这一章中,我们将讨论一些可以通过添加更多描述性特征并从现有特征中提取有意义信息来提高数据价值的工具。我们还将探讨与特征数量优化相关的其他工具,并了解为何特征数量过多而训练样本/观察值过少会带来问题。

第四章,快速上手 TensorFlow,概述了一个广泛使用的深度学习框架。TensorFlow 拥有日益壮大的社区支持,这使它成为构建复杂深度学习应用的一个不错选择。

第五章,TensorFlow 实战 - 一些基本示例,将解释 TensorFlow 背后的主要计算概念——计算图模型,并通过实现线性回归和逻辑回归来演示如何帮助你入门。

第六章,深度前馈神经网络 - 实现数字分类,解释了 前馈神经网络FNN)是一种特殊类型的神经网络,其中神经元之间的连接/链接不形成循环。因此,它与我们在本书后面将要学习的其他神经网络架构(如递归神经网络)不同。FNN 是一种广泛使用的架构,也是最早、最简单的神经网络类型。在本章中,我们将介绍典型 FNN 的架构,并将使用 TensorFlow 库进行实现。在讲解完这些概念后,我们将给出一个数字分类的实际示例。这个示例的问题是,给定一组包含手写数字的图像,如何将这些图像分类为 10 个不同的类别(0-9)

第七章,卷积神经网络简介,解释了在数据科学中,卷积神经网络CNN)是一种特定的深度学习架构,它使用卷积操作来提取输入图像的相关特征。CNN 层的连接方式类似于 FNN,同时使用卷积操作来模拟人脑在识别物体时的工作方式。个别皮层神经元对刺激作出反应,反应的区域被称为感受野。特别是在生物医学成像问题中,挑战有时会比较大,但在这一章中,我们将展示如何使用 CNN 来发现图像中的模式。

第八章,目标检测 - CIFAR-10 示例,介绍了卷积神经网络(CNN)的基础知识及其背后的直觉/动机,接着在一个最受欢迎的目标检测数据集上进行了演示。我们还将看到 CNN 的初始层获取关于对象的非常基础的特征,而最终的卷积层将从这些基础特征中构建出更具语义层次的特征。

第九章,目标检测 - 使用 CNN 进行迁移学习,解释了迁移学习TL)是数据科学中的一个研究问题,主要关注在解决特定任务过程中获取的知识,并将这些知识应用于解决另一个不同但相似的任务。在这一章中,我们将演示数据科学领域中迁移学习的一种现代实践和常见主题。这里的想法是如何从拥有非常大数据集的领域获得帮助,以支持那些数据集较小的领域。最后,我们将重新审视 CIFAR-10 的目标检测示例,并尝试通过迁移学习减少训练时间和性能误差。

第十章,递归神经网络 - 语言建模,解释了递归神经网络RNNs)是一类深度学习架构,广泛应用于自然语言处理。这类架构使我们能够为当前的预测提供上下文信息,并且具有处理任何输入序列中的长期依赖关系的特定架构。在这一章中,我们将演示如何构建一个序列到序列的模型,这在自然语言处理的许多应用中非常有用。我们将通过构建一个字符级语言模型来演示这些概念,并观察我们的模型如何生成类似于原始输入序列的句子。

第十一章,表示学习 - 实现词嵌入,解释了机器学习是一门主要基于统计学和线性代数的科学。由于反向传播,大多数机器学习或深度学习架构中都非常常见应用矩阵操作。这是深度学习或一般机器学习仅接受实数值输入的主要原因。这个事实与许多应用相矛盾,比如机器翻译、情感分析等等,它们的输入是文本。因此,为了在这些应用中使用深度学习,我们需要将文本转化为深度学习能够接受的形式!在这一章中,我们将介绍表示学习领域,这是从文本中学习实数表示的一种方法,同时保持实际文本的语义。例如,"love"的表示应该与"adore"的表示非常接近,因为它们在非常相似的上下文中使用。

第十二章,神经网络情感分析,讨论了自然语言处理中的一个热门应用——情感分析。如今,大多数人通过社交媒体平台表达他们对某事的看法,利用大量文本来跟踪客户对某件事的满意度,对于公司甚至政府来说都是至关重要的。

在本章中,我们将使用 RNN(递归神经网络)构建一个情感分析解决方案。

第十三章,自编码器——特征提取与去噪,解释了自编码器网络如今是广泛使用的深度学习架构之一。它主要用于无监督学习的高效解码任务,也可以通过学习特定数据集的编码或表示来进行降维。在本章中,我们将通过构建另一个维度相同但噪声较少的数据集来演示如何去噪数据集。为了将这一概念付诸实践,我们将从 MNIST 数据集中提取重要特征,并尝试看看这种方法如何显著提升性能。

第十四章,生成对抗网络,涵盖了生成对抗网络GANs)。它们是由两个网络对立工作(因此得名“对抗”)的深度神经网络架构。GANs 最早由 Ian Goodfellow 和其他研究人员(包括 Yoshua Bengio)于 2014 年在蒙特利尔大学的论文中提出(arxiv.org/abs/1406.2661)。关于 GANs,Facebook 的 AI 研究总监 Yann LeCun 曾称对抗训练为过去 10 年机器学习领域最有趣的想法。GANs 的潜力巨大,因为它们可以学习模拟任何数据分布。也就是说,GANs 可以被训练成在任何领域中创造出与我们自己的世界惊人相似的东西:图像、音乐、语音或散文。从某种意义上说,它们是“机器人艺术家”,并且它们的输出令人印象深刻(www.nytimes.com/2017/08/14/arts/design/google-how-ai-creates-new-music-and-new-artists-project-magenta.html)——并且充满深意。

第十五章,面部生成与缺失标签处理,展示了我们可以使用 GANs 的有趣应用列表是无穷无尽的。在本章中,我们将展示 GANs 的另一个有前景的应用——基于 CelebA 数据库的面部生成。我们还将演示如何在半监督学习设置中使用 GANs,尤其是在面对一个标注不完善、缺少部分标签的数据集时。

附录,实现鱼类识别,包括完整的鱼类识别示例代码。

为了充分利用本书

  • 告知读者在开始之前需要了解的事项,并明确假设你所掌握的知识。

  • 任何额外的安装说明和设置所需的信息。

下载示例代码文件

你可以从你的账户下载本书的示例代码文件,网址是:www.packtpub.com。如果你是在其他地方购买本书,你可以访问www.packtpub.com/support,注册后我们会直接通过电子邮件将文件发送给你。

你可以通过以下步骤下载代码文件:

  1. 登录或在www.packtpub.com注册。

  2. 选择“支持”标签。

  3. 点击“代码下载和勘误”。

  4. 在搜索框中输入书名并按照屏幕上的指示操作。

一旦文件下载完成,请确保使用最新版本的工具解压或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书的代码包也托管在 GitHub 上:github.com/PacktPublishing/Deep-Learning-By-Example。我们还提供了其他书籍和视频的代码包,您可以在github.com/PacktPublishing/找到它们。赶快查看吧!

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。你可以在这里下载:www.packtpub.com/sites/default/files/downloads/DeepLearningByExample_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名。例如:“将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。”

代码块如下所示:

html, body, #map {
 height: 100%; 
 margin: 0;
 padding: 0
}

当我们希望你注意代码块中特定部分时,相关的行或项会以粗体显示:

[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)

任何命令行输入或输出如下所示:

$ mkdir css
$ cd css

粗体:表示新术语、重要词汇或屏幕上显示的词汇。例如,菜单或对话框中的词汇在文本中以这种方式出现。举个例子:“从管理面板中选择系统信息。”

警告或重要提示以这种方式显示。

提示和技巧通常以这种方式呈现。

与我们联系

我们始终欢迎读者的反馈。

一般反馈:请通过电子邮件 feedback@packtpub.com 并在邮件主题中提到书名。如果您对本书的任何内容有疑问,请发送邮件至 questions@packtpub.com 与我们联系。

勘误:虽然我们已经尽力确保内容的准确性,但错误还是有可能发生。如果您在本书中发现任何错误,我们将不胜感激,如果您能向我们报告。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击“勘误提交表格”链接,并输入相关详情。

盗版:如果您在互联网上遇到任何形式的我们作品的非法复制品,我们将非常感激您能提供相关位置地址或网站名称。请通过电子邮件联系我们,地址为 copyright@packtpub.com,并附上该材料的链接。

如果您有兴趣成为作者:如果您在某个领域拥有专业知识,并且有意写书或为书籍做贡献,请访问 authors.packtpub.com

评价

请留下评价。在您阅读并使用本书后,为什么不在您购买书籍的网站上留下您的评价呢?潜在的读者可以通过您的无偏见意见做出购买决策,我们 Packt 也能了解您对我们产品的看法,我们的作者也能看到您对其书籍的反馈。谢谢!

欲了解更多关于 Packt 的信息,请访问 packtpub.com

第一章:数据科学 - 鸟瞰图

数据科学或机器学习是赋予机器从数据集中学习的能力,而无需明确告诉它或进行编程。例如,编写一个程序,能够接受手写数字作为输入图像,并根据图像输出一个 0 到 9 之间的值,这是极其困难的。对于将来来邮件分类为垃圾邮件或非垃圾邮件的任务也是如此。为了解决这些任务,数据科学家使用数据科学或机器学习领域的学习方法和工具,教计算机如何通过一些能区分不同数字的特征自动识别数字。垃圾邮件/非垃圾邮件问题也是如此,我们可以通过特定的学习算法教计算机如何区分垃圾邮件和非垃圾邮件,而不是使用正则表达式并编写成百上千条规则来分类来邮件。

对于垃圾邮件过滤应用,你可以采用基于规则的方法进行编程,但这不足以用于生产环境,比如你的邮件服务器。构建一个学习系统是解决这个问题的理想方案。

你可能每天都在使用数据科学的应用,通常你甚至没有意识到。例如,你的国家可能使用一个系统来检测你寄出的信件的邮政编码,以便自动将其转发到正确的地区。如果你在使用亚马逊,他们通常会为你推荐商品,这通过学习你经常搜索或购买的物品来实现。

构建一个经过学习/训练的机器学习算法需要基于历史数据样本,以便它能够学习如何区分不同的例子,并从这些数据中得出一些知识和趋势。之后,经过学习/训练的算法可以用来对未见过的数据进行预测。学习算法将使用原始的历史数据,并尝试从这些数据中得出一些知识和趋势。

在本章中,我们将从鸟瞰图的角度了解数据科学,了解它如何作为一个黑盒工作,以及数据科学家每天面临的挑战。我们将涵盖以下主题:

  • 通过一个例子来理解数据科学

  • 数据科学算法设计流程

  • 开始学习

  • 实现鱼类识别/检测模型

  • 不同的学习类型

  • 数据规模和行业需求

通过一个例子来理解数据科学

为了说明构建针对特定数据的学习算法的生命周期和挑战,假设我们来看一个真实的例子。自然保护协会正在与其他渔业公司和合作伙伴合作,监控渔业活动并保护未来的渔场。因此,他们未来计划使用摄像头来扩大这一监控过程。这些摄像头部署后所产生的数据量将非常庞大,并且手动处理这些数据将非常昂贵。因此,保护协会希望开发一个学习算法,自动检测和分类不同种类的鱼类,以加快视频审核过程。

图 1.1 显示了自然保护协会部署的摄像头拍摄的样本图像。这些图像将用于构建系统。

图 1.1:自然保护协会部署的摄像头输出样本

因此,本文的目标是区分渔船捕获的不同物种,如鲔鱼、鲨鱼等。作为一个示例,我们可以将问题限定为仅包含两类:鲔鱼和 Opah 鱼。

图 1.2:鲔鱼类型(左)与 Opah 鱼类型(右)

在将问题限制为仅包含两种鱼类后,我们可以从我们收集的图像中随机抽取一些样本,并开始注意这两种鱼类之间的一些物理差异。例如,考虑以下物理差异:

  • 长度:你可以看到,与鲔鱼相比,Opah 鱼更长。

  • 宽度:Opah 鱼比鲔鱼更宽。

  • 颜色:你可以看到,Opah 鱼通常更红,而鲔鱼则趋向于蓝色和白色,等等。

我们可以利用这些物理差异作为特征,帮助我们的学习算法(分类器)区分这两种鱼类。

物体的解释性特征是我们日常生活中用来区分周围物体的特征。即使是婴儿,也会使用这些解释性特征来学习周围的环境。数据科学也是如此,为了构建一个可以区分不同物体(例如鱼类种类)的学习模型,我们需要给它一些解释性特征来学习(例如鱼类的长度)。为了使模型更具确定性并减少混淆错误,我们可以在某种程度上增加物体的解释性特征。

由于这两种鱼类之间存在物理差异,这两种不同的鱼群有不同的模型或描述。因此,我们分类任务的最终目标是让分类器学习这些不同的模型,然后将这两种鱼类之一的图像作为输入,分类器将通过选择最符合该图像的模型(鲔鱼模型或 Opah 模型)进行分类。

在这个案例中,金枪鱼和大眼金枪鱼的集合将作为我们分类器的知识库。最初,知识库(训练样本)会被标注/标签化,并且你将事先知道每张图像是金枪鱼还是大眼金枪鱼。所以,分类器将使用这些训练样本来对不同类型的鱼进行建模,然后我们可以使用训练阶段的输出自动标记分类器在训练阶段未见过的未标记/未标签的鱼类数据。这类未标记的数据通常被称为未见过的 数据。生命周期的训练阶段如图所示:

监督学习的数据科学就是从具有已知目标或输出的历史数据中学习,例如鱼的种类,然后使用这个学习到的模型来预测我们不知道目标/输出的数据样本或案例。

图 1.3:训练阶段生命周期

让我们看看分类器的训练阶段如何进行:

  • 预处理:在这一步,我们将尝试通过使用相关的分割技术将鱼从图像中分割出来。

  • 特征提取:通过减去背景将鱼从图像中分割出来后,我们将测量每张图像的物理差异(长度、宽度、颜色等)。最终,你将得到类似图 1.4的内容。

最后,我们将把这些数据输入分类器,以便对不同的鱼类类型进行建模。

正如我们所看到的,我们可以通过我们提出的物理差异(特征),例如长度、宽度和颜色,来直观地区分金枪鱼和大眼金枪鱼。

我们可以使用长度特征来区分这两种类型的鱼。所以我们可以通过观察鱼的长度并判断它是否超过某个值(length*)来尝试区分鱼类。

因此,根据我们的训练样本,我们可以推导出以下规则:

If length(fish)> length* then label(fish) = Tuna
Otherwise label(fish) = Opah 

为了找到这个length*,我们可以通过训练样本来某种方式进行长度测量。所以,假设我们获得这些长度测量值并得到如下的直方图:

图 1.4:两种类型鱼的长度测量直方图

在这种情况下,我们可以根据长度特征推导出一条规则来区分金枪鱼和大眼金枪鱼。在这个特定的例子中,我们可以得出length*7。因此,我们可以更新前面的规则为:

If length(fish)> 7 then label(fish) = Tuna
Otherwise label(fish) = Opah

正如你可能注意到的,这不是一个有前景的结果,因为两条直方图之间存在重叠,长度特征并不是一个完美的特征,不能单独用于区分这两种类型的鱼。所以我们可以尝试加入更多的特征,比如宽度,然后将它们结合起来。因此,如果我们能够某种方式测量训练样本的宽度,我们可能会得到如下的直方图:

图 5:两种类型鱼的宽度测量直方图

如你所见,仅依赖一个特征不会给出准确的结果,输出模型将会产生大量的误分类。相反,我们可以通过某种方式将两个特征结合起来,得出一个看起来合理的结果。

所以,如果我们将这两个特征结合起来,可能会得到如下图所示的图形:

图 1.6:金枪鱼和 Opah 鱼长度与宽度子集的组合

结合长度宽度这两个特征的读数,我们将得到类似前面图表中的散点图。我们用红色点表示金枪鱼,用绿色点表示 Opah 鱼,并且可以建议这条黑线是区分两种鱼类的规则或决策边界。

例如,如果某条鱼的读数位于这个决策边界之上,那么它就是金枪鱼;否则,它将被预测为 Opah 鱼。

我们可以通过某种方式尝试增加规则的复杂性,以避免任何错误,并得到如下图所示的决策边界:

图 1.7:增加决策边界的复杂度,以避免对训练数据的误分类

这个模型的优点是,在训练样本上几乎没有误分类。但实际上,这并不是使用数据科学的目标。数据科学的目标是建立一个能够在未见数据上进行泛化并且表现良好的模型。为了判断我们是否建立了一个能够泛化的模型,我们将引入一个新的阶段,叫做测试阶段,在这个阶段,我们会给训练好的模型一个未标记的图像,并期望模型为其分配正确的标签(金枪鱼Opah 鱼)。

数据科学的最终目标是建立一个在生产环境中能够良好工作的模型,而不是只在训练集上工作。所以,当你看到你的模型在训练集上表现良好时,不要太高兴,就像图 1.7 中的那样。通常,这种模型在识别图像中的鱼类时会失败。仅在训练集上表现良好的模型被称为过拟合,大多数实践者都会陷入这个陷阱。

与其构建如此复杂的模型,不如构建一个更简单的模型,使其能够在测试阶段进行泛化。以下图表展示了使用较简单模型,以减少误分类错误并同时泛化未见数据的情况:

图 1.8:使用更简单的模型,以便能够对测试样本(未见数据)进行泛化

数据科学算法的设计流程

不同的学习系统通常遵循相同的设计过程。它们首先获取知识库,从数据中选择相关的解释特征,经过一系列候选学习算法的尝试,并且密切关注每个算法的表现,最后进行评估过程,衡量训练过程的成功与否。

在本节中,我们将更详细地介绍所有这些不同的设计步骤:

图 1.11:模型学习过程概述

数据预处理

这一组件代表了我们算法的知识库。因此,为了帮助学习算法对未见数据做出准确的决策,我们需要以最佳形式提供这些知识库。因此,我们的数据可能需要大量清理和预处理(转换)。

数据清理

大多数数据集都需要这一阶段,在此过程中,你将去除错误、噪声和冗余。我们需要确保数据准确、完整、可靠且无偏,因为使用糟糕的知识库可能会导致许多问题,例如:

  • 不准确和有偏的结论

  • 增加的错误

  • 降低的泛化能力,即模型在未见过的数据上的表现能力,未训练过的数据

数据预处理

在这一步,我们对数据进行一些转换,使其一致且具体。数据预处理过程中有许多不同的转换方法可以考虑:

  • 重命名重新标签化):这意味着将类别值转换为数字,因为如果与某些学习方法一起使用,类别值会带来危险,而数字则会在值之间强加顺序

  • 重缩放归一化):将连续值转换/限定到某个范围,通常是 [-1, 1][0, 1]

  • 新特征:从现有特征中构造新的特征。例如,肥胖因子 = 体重/身高

特征选择

样本的解释特征(输入变量)数量可能非常庞大,导致你获得 x[i]=(x[i]¹, x[i]², x[i]³, ... , x[i]^d) 作为训练样本(观测值/示例),其中 d 非常大。例如,在文档分类任务中,你可能得到 10,000 个不同的单词,输入变量将是不同单词的出现次数。

这样庞大的输入变量数量可能会带来问题,有时甚至是诅咒,因为我们有很多输入变量,却只有少量训练样本来帮助学习过程。为了避免这种庞大输入变量数量带来的诅咒(维度灾难),数据科学家使用降维技术来从输入变量中选择一个子集。例如,在文本分类任务中,他们可以进行如下操作:

  • 提取相关输入(例如,互信息度量)

  • 主成分分析PCA

  • 分组(聚类)相似的词语(这使用相似度度量)

模型选择

这一步是在使用任何降维技术选择合适的输入变量子集之后进行的。选择合适的输入变量子集将使得后续的学习过程变得非常简单。

在这一步,你需要弄清楚适合学习的模型。

如果你有数据科学的先前经验,并将学习方法应用于不同领域和不同类型的数据,那么你会觉得这一步很容易,因为它需要你先了解数据的特征以及哪些假设可能适合数据的性质,基于这些你选择合适的学习方法。如果你没有任何先前的知识,这也没关系,因为你可以通过猜测并尝试不同的学习方法以及不同的参数设置,选择在测试集上表现更好的方法。

同样,初步的数据分析和可视化将帮助你对分布的形式和数据的性质做出合理的猜测。

学习过程

在学习中,我们指的是你将用来选择最佳模型参数的优化准则。为此有多种优化准则:

  • 均方误差MSE

  • 最大似然ML)准则

  • 最大后验概率MAP

优化问题可能很难解决,但正确选择模型和误差函数会带来不同的结果。

评估你的模型

在这一步,我们尝试衡量模型在未见数据上的泛化误差。由于我们只有特定的数据,且事先不知道任何未见数据,我们可以从数据中随机选择一个测试集,并且在训练过程中永远不使用它,以便它像未见的有效数据一样作用。你可以通过不同的方式来评估所选模型的表现:

  • 简单的保留法,即将数据分为训练集和测试集

  • 其他基于交叉验证和随机子抽样的复杂方法

我们在这一步的目标是比较在相同数据上训练的不同模型的预测性能,选择测试误差更小的模型,它将给我们带来更好的未见数据泛化误差。你还可以通过使用统计方法来检验结果的显著性,从而更确定地评估泛化误差。

开始学习

构建一个机器学习系统面临一些挑战和问题,我们将在本节中讨论这些问题。这些问题中有些是领域特定的,有些则不是。

学习的挑战

以下是你在构建学习系统时通常会面临的挑战和问题的概述。

特征提取 – 特征工程

特征提取是构建学习系统的关键步骤之一。如果你在这个挑战中通过选择适当的特征数量做得很好,那么接下来的学习过程将会变得轻松。此外,特征提取是依赖于领域的,它需要先验知识,以便了解哪些特征对于特定任务可能是重要的。例如,我们的鱼类识别系统的特征将与垃圾邮件检测或指纹识别的特征不同。

特征提取步骤从你拥有的原始数据开始。然后构建派生变量/值(特征),这些特征应该能够为学习任务提供信息,并促进接下来的学习和评估(泛化)步骤。

一些任务会有大量的特征和较少的训练样本(观测数据),这会影响后续的学习和泛化过程。在这种情况下,数据科学家使用降维技术将大量特征减少到一个较小的集合。

噪声

在鱼类识别任务中,你可以看到长度、重量、鱼的颜色以及船的颜色可能会有所不同,而且可能会有阴影、分辨率低的图像以及图像中的其他物体。所有这些问题都会影响我们提出的解释性特征的重要性,这些特征应该能为我们的鱼类分类任务提供信息。

在这种情况下,解决方法将会很有帮助。例如,有人可能会想到检测船只的 ID,并遮蔽出船上可能不会包含任何鱼的部分,以便我们的系统进行检测。这个解决方法会限制我们的搜索空间。

过拟合

正如我们在鱼类识别任务中看到的,我们曾通过增加模型复杂度并完美地分类训练样本中的每一个实例来提高模型的表现。正如我们稍后将看到的,这种模型在未见过的数据(例如我们将用于测试模型表现的数据)上并不起作用。训练的模型在训练样本上表现完美,但在测试样本上表现不佳,这种现象称为过拟合

如果你浏览章节的后半部分,我们将构建一个学习系统,目的是将训练样本作为模型的知识库,让模型从中学习并对未见过的数据进行泛化。我们对训练数据上训练模型的表现误差不感兴趣;相反,我们关心的是训练模型在没有参与训练阶段的测试样本上的表现(泛化)误差。

机器学习算法的选择

有时,你对模型的执行结果不满意,需要切换到另一类模型。每种学习策略都有自己的假设,关于它将使用哪些数据作为学习基础。作为数据科学家,你需要找出哪种假设最适合你的数据;通过这样,你就能够决定尝试某一类模型并排除其他类。

先验知识

正如在模型选择和特征提取的概念中讨论的那样,如果你有先验知识,两个问题都可以得到解决:

  • 适当的特征

  • 模型选择部分

拥有鱼类识别系统的解释性特征的先验知识,使我们能够区分不同种类的鱼。我们可以通过尝试可视化我们的数据,获得不同鱼类分类数据类型的感性认识。在此基础上,可以选择合适的模型家族进行进一步探索。

缺失值

缺失特征主要是由于数据不足或选择了“不愿透露”选项。我们如何在学习过程中处理这种情况呢?例如,假设由于某种原因,我们发现某种特定鱼类的宽度数据缺失。处理这些缺失特征的方法有很多种。

实现鱼类识别/检测模型

为了展示机器学习特别是深度学习的力量,我们将实现鱼类识别的例子。你不需要理解代码的内部细节。本节的目的是给你提供一个典型机器学习流程的概览。

我们的知识库将是一些图片,每张图片都被标记为 opah 或 tuna。为了实现这一目标,我们将使用一种在图像处理和计算机视觉领域取得突破的深度学习架构。这种架构被称为卷积神经网络CNNs)。它是一个深度学习架构的家族,利用图像处理中的卷积操作从图片中提取特征,从而解释我们想要分类的物体。现在,你可以把它想象成一个神奇的盒子,它会接收我们的图片,学习如何区分我们的两类鱼(opah 和 tuna),然后通过喂入未标记的图片来测试这个盒子的学习过程,看看它是否能够识别出图片中的鱼类。

不同类型的学习将在后面的章节中讨论,因此你将会理解为什么我们的鱼类识别任务属于监督学习类别。

在这个例子中,我们将使用 Keras。目前,你可以将 Keras 看作一个 API,它使得构建和使用深度学习比以往更加简单。所以,让我们开始吧!从 Keras 网站上,我们可以看到:

Keras 是一个高级神经网络 API,使用 Python 编写,并能够运行在 TensorFlow、CNTK 或 Theano 上。它的开发重点是使快速实验成为可能。能够在最短的时间内从想法到结果是做出优秀研究的关键。

知识库/数据集

正如我们之前提到的,我们需要一个历史数据集,用来教导学习算法完成它之后应执行的任务。但我们还需要另一个数据集来测试学习过程后它执行任务的能力。总而言之,在学习过程中我们需要两种类型的数据集:

  1. 第一个是知识库,我们在其中拥有输入数据及其对应的标签,比如鱼的图像及其对应的标签(opah 或 tuna)。这些数据将被输入到学习算法中,供其学习并尝试发现将来有助于分类未标记图像的模式/趋势。

  2. 第二个阶段主要是测试模型应用从知识库中学到的知识,处理未标记的图像或未见过的数据(通常是),并查看它是否表现良好。

如你所见,我们只有将作为学习方法的知识库使用的数据。我们手头所有的数据都将与正确的输出关联。因此,我们需要想办法生成这些没有正确输出关联的数据(即我们将应用模型的数据)。

在执行数据科学时,我们将进行以下操作:

  • 训练阶段:我们从知识库中呈现我们的数据,并通过将输入数据及其正确输出输入到模型中来训练我们的学习方法/模型。

  • 验证/测试阶段:在此阶段,我们将衡量训练好的模型表现如何。我们还使用不同的模型属性技术,通过使用(回归的 R-squared 分数、分类器的分类错误、信息检索模型的召回率和精确度等)来衡量我们训练模型的性能。

验证/测试阶段通常分为两个步骤:

  1. 在第一步中,我们使用不同的学习方法/模型,并根据验证数据(验证步骤)选择表现最好的那个。

  2. 然后我们根据测试集(测试步骤)来衡量并报告所选模型的准确性。

现在让我们看看如何获得这些我们将应用模型的数据,并查看模型训练得如何。

由于我们没有没有正确输出的训练样本,我们可以从将要使用的原始训练样本中生成一个。因此,我们可以将我们的数据样本拆分为三组(如 图 1.9 所示):

  • 训练集:这将作为我们模型的知识库。通常来自原始数据样本的 70%。

  • 验证集:这将用于在一组模型中选择表现最好的模型。通常,这将是原始数据样本的 10%。

  • 测试集:将用于衡量和报告所选模型的准确性。通常,它的大小与验证集相当。

图 1.9:将数据分割为训练集、验证集和测试集

如果你只使用一个学习方法,可以取消验证集,将数据重新分割为仅训练集和测试集。通常,数据科学家使用 75/25 或 70/30 的比例划分数据。

数据分析预处理

在本节中,我们将分析并预处理输入图像,并将其转换为适合我们学习算法的可接受格式,这里使用的是卷积神经网络(CNN)。

所以让我们从导入实现所需的包开始:

import numpy as np
np.random.seed(2018)
import os
import glob
import cv2
import datetime
import pandas as pd
import time
import warnings
warnings.filterwarnings("ignore")
from sklearn.cross_validation import KFold
from keras.models import Sequential
from keras.layers.core import Dense, Dropout, Flatten
from keras.layers.convolutional import Convolution2D, MaxPooling2D, ZeroPadding2D
from keras.optimizers import SGD
from keras.callbacks import EarlyStopping
from keras.utils import np_utils
from sklearn.metrics import log_loss
from keras import __version__ as keras_version

为了使用数据集中提供的图像,我们需要将它们调整为相同的大小。OpenCV 是一个很好的选择,详情请见 OpenCV 官网:

OpenCV(开源计算机视觉库)是根据 BSD 许可证发布的,因此它对学术和商业使用都是免费的。它支持 C++、C、Python 和 Java 接口,并支持 Windows、Linux、Mac OS、iOS 和 Android 操作系统。OpenCV 设计时注重计算效率,并且强烈关注实时应用。它是用优化的 C/C++ 编写的,可以利用多核处理。启用了 OpenCL 后,它能够利用底层异构计算平台的硬件加速。

你可以通过使用 Python 包管理器安装 OpenCV,命令为 pip install opencv-python

# Parameters
# ----------
# img_path : path
#    path of the image to be resized
def rezize_image(img_path):
   #reading image file
   img = cv2.imread(img_path)
   #Resize the image to to be 32 by 32
   img_resized = cv2.resize(img, (32, 32), cv2.INTER_LINEAR)
return img_resized

现在,我们需要加载数据集中的所有训练样本,并根据之前的函数调整每张图像的大小。所以我们将实现一个函数,从我们为每种鱼类类型准备的不同文件夹中加载训练样本:

# Loading the training samples and their corresponding labels
def load_training_samples():
    #Variables to hold the training input and output variables
    train_input_variables = []
    train_input_variables_id = []
    train_label = []
    # Scanning all images in each folder of a fish type
    print('Start Reading Train Images')
    folders = ['ALB', 'BET', 'DOL', 'LAG', 'NoF', 'OTHER', 'SHARK', 'YFT']
    for fld in folders:
       folder_index = folders.index(fld)
       print('Load folder {} (Index: {})'.format(fld, folder_index))
       imgs_path = os.path.join('..', 'input', 'train', fld, '*.jpg')
       files = glob.glob(imgs_path)
       for file in files:
           file_base = os.path.basename(file)
           # Resize the image
           resized_img = rezize_image(file)
           # Appending the processed image to the input/output variables of the classifier
           train_input_variables.append(resized_img)
           train_input_variables_id.append(file_base)
           train_label.append(folder_index)
     return train_input_variables, train_input_variables_id, train_label

正如我们讨论的,我们有一个测试集,它将充当未见数据,以测试我们模型的泛化能力。因此,我们需要对测试图像做同样的处理;加载它们并进行调整大小处理:

def load_testing_samples():
    # Scanning images from the test folder
    imgs_path = os.path.join('..', 'input', 'test_stg1', '*.jpg')
    files = sorted(glob.glob(imgs_path))
    # Variables to hold the testing samples
    testing_samples = []
    testing_samples_id = []
    #Processing the images and appending them to the array that we have
    for file in files:
       file_base = os.path.basename(file)
       # Image resizing
       resized_img = rezize_image(file)
       testing_samples.append(resized_img)
       testing_samples_id.append(file_base)
    return testing_samples, testing_samples_id

现在,我们需要将之前的函数调用到另一个函数中,后者将使用 load_training_samples() 函数来加载并调整训练样本的大小。它还会增加几行代码,将训练数据转换为 NumPy 格式,重新调整数据形状以适应我们的分类器,最后将其转换为浮动格式:

def load_normalize_training_samples():
    # Calling the load function in order to load and resize the training samples
    training_samples, training_label, training_samples_id = load_training_samples()
    # Converting the loaded and resized data into Numpy format
    training_samples = np.array(training_samples, dtype=np.uint8)
    training_label = np.array(training_label, dtype=np.uint8)
    # Reshaping the training samples
    training_samples = training_samples.transpose((0, 3, 1, 2))
    # Converting the training samples and training labels into float format
    training_samples = training_samples.astype('float32')
    training_samples = training_samples / 255
    training_label = np_utils.to_categorical(training_label, 8)
    return training_samples, training_label, training_samples_id

我们也需要对测试进行相同的处理:

def load_normalize_testing_samples():
    # Calling the load function in order to load and resize the testing samples
    testing_samples, testing_samples_id = load_testing_samples()
    # Converting the loaded and resized data into Numpy format
    testing_samples = np.array(testing_samples, dtype=np.uint8)
    # Reshaping the testing samples
    testing_samples = testing_samples.transpose((0, 3, 1, 2))
    # Converting the testing samples into float format
    testing_samples = testing_samples.astype('float32')
    testing_samples = testing_samples / 255
    return testing_samples, testing_samples_id

模型构建

现在是创建模型的时候了。正如我们所提到的,我们将使用一种深度学习架构叫做 CNN 作为此鱼类识别任务的学习算法。再次提醒,你不需要理解本章之前或之后的任何代码,因为我们仅仅是演示如何使用少量代码,借助 Keras 和 TensorFlow 深度学习平台来解决复杂的数据科学任务。

还要注意,CNN 和其他深度学习架构将在后续章节中更详细地解释:

图 1.10:CNN 架构

所以,让我们继续创建一个函数,负责构建将在鱼类识别任务中使用的 CNN 架构:

def create_cnn_model_arch():
    pool_size = 2 # we will use 2x2 pooling throughout
    conv_depth_1 = 32 # we will initially have 32 kernels per conv. layer...
    conv_depth_2 = 64 # ...switching to 64 after the first pooling layer
    kernel_size = 3 # we will use 3x3 kernels throughout
    drop_prob = 0.5 # dropout in the FC layer with probability 0.5
    hidden_size = 32 # the FC layer will have 512 neurons
    num_classes = 8 # there are 8 fish types
    # Conv [32] -> Conv [32] -> Pool
    cnn_model = Sequential()
    cnn_model.add(ZeroPadding2D((1, 1), input_shape=(3, 32, 32), dim_ordering='th'))
    cnn_model.add(Convolution2D(conv_depth_1, kernel_size, kernel_size, activation='relu', 
      dim_ordering='th'))
    cnn_model.add(ZeroPadding2D((1, 1), dim_ordering='th'))
    cnn_model.add(Convolution2D(conv_depth_1, kernel_size, kernel_size, activation='relu',
      dim_ordering='th'))
    cnn_model.add(MaxPooling2D(pool_size=(pool_size, pool_size), strides=(2, 2),
      dim_ordering='th'))
    # Conv [64] -> Conv [64] -> Pool
    cnn_model.add(ZeroPadding2D((1, 1), dim_ordering='th'))
    cnn_model.add(Convolution2D(conv_depth_2, kernel_size, kernel_size, activation='relu',
      dim_ordering='th'))
    cnn_model.add(ZeroPadding2D((1, 1), dim_ordering='th'))
    cnn_model.add(Convolution2D(conv_depth_2, kernel_size, kernel_size, activation='relu',
      dim_ordering='th'))
    cnn_model.add(MaxPooling2D(pool_size=(pool_size, pool_size), strides=(2, 2),
     dim_ordering='th'))
    # Now flatten to 1D, apply FC then ReLU (with dropout) and finally softmax(output layer)
    cnn_model.add(Flatten())
    cnn_model.add(Dense(hidden_size, activation='relu'))
    cnn_model.add(Dropout(drop_prob))
    cnn_model.add(Dense(hidden_size, activation='relu'))
    cnn_model.add(Dropout(drop_prob))
    cnn_model.add(Dense(num_classes, activation='softmax'))
    # initiating the stochastic gradient descent optimiser
    stochastic_gradient_descent = SGD(lr=1e-2, decay=1e-6, momentum=0.9, nesterov=True)    cnn_model.compile(optimizer=stochastic_gradient_descent,  # using the stochastic gradient descent optimiser
                  loss='categorical_crossentropy')  # using the cross-entropy loss function
    return cnn_model

在开始训练模型之前,我们需要使用一种模型评估和验证方法,帮助我们评估模型并查看其泛化能力。为此,我们将使用一种叫做k 折交叉验证的方法。同样,您不需要理解这个方法或它是如何工作的,因为我们稍后将详细解释这一方法。

所以,让我们开始创建一个帮助我们评估和验证模型的函数:

def create_model_with_kfold_cross_validation(nfolds=10):
    batch_size = 16 # in each iteration, we consider 32 training examples at once
    num_epochs = 30 # we iterate 200 times over the entire training set
    random_state = 51 # control the randomness for reproducibility of the results on the same platform
    # Loading and normalizing the training samples prior to feeding it to the created CNN model
    training_samples, training_samples_target, training_samples_id = 
      load_normalize_training_samples()
    yfull_train = dict()
    # Providing Training/Testing indices to split data in the training samples
    # which is splitting data into 10 consecutive folds with shuffling
    kf = KFold(len(train_id), n_folds=nfolds, shuffle=True, random_state=random_state)
    fold_number = 0 # Initial value for fold number
    sum_score = 0 # overall score (will be incremented at each iteration)
    trained_models = [] # storing the modeling of each iteration over the folds
    # Getting the training/testing samples based on the generated training/testing indices by 
      Kfold
    for train_index, test_index in kf:
       cnn_model = create_cnn_model_arch()
       training_samples_X = training_samples[train_index] # Getting the training input variables
       training_samples_Y = training_samples_target[train_index] # Getting the training output/label variable
       validation_samples_X = training_samples[test_index] # Getting the validation input variables
       validation_samples_Y = training_samples_target[test_index] # Getting the validation output/label variable
       fold_number += 1
       print('Fold number {} from {}'.format(fold_number, nfolds))
       callbacks = [
           EarlyStopping(monitor='val_loss', patience=3, verbose=0),
       ]
       # Fitting the CNN model giving the defined settings
       cnn_model.fit(training_samples_X, training_samples_Y, batch_size=batch_size,
         nb_epoch=num_epochs,
             shuffle=True, verbose=2, validation_data=(validation_samples_X,
               validation_samples_Y),
             callbacks=callbacks)
       # measuring the generalization ability of the trained model based on the validation set
       predictions_of_validation_samples = 
         cnn_model.predict(validation_samples_X.astype('float32'), 
         batch_size=batch_size, verbose=2)
       current_model_score = log_loss(Y_valid, predictions_of_validation_samples)
       print('Current model score log_loss: ', current_model_score)
       sum_score += current_model_score*len(test_index)
       # Store valid predictions
       for i in range(len(test_index)):
           yfull_train[test_index[i]] = predictions_of_validation_samples[i]
       # Store the trained model
       trained_models.append(cnn_model)
    # incrementing the sum_score value by the current model calculated score
    overall_score = sum_score/len(training_samples)
    print("Log_loss train independent avg: ", overall_score)
    #Reporting the model loss at this stage
    overall_settings_output_string = 'loss_' + str(overall_score) + '_folds_' + str(nfolds) + 
      '_ep_' + str(num_epochs)
    return overall_settings_output_string, trained_models

现在,在构建模型并使用 k 折交叉验证方法来评估和验证模型之后,我们需要报告训练模型在测试集上的结果。为此,我们也将使用 k 折交叉验证,但这次是在测试集上进行,看看我们训练好的模型有多好。

所以,让我们定义一个函数,该函数将以训练好的 CNN 模型为输入,然后使用我们拥有的测试集对其进行测试:

def test_generality_crossValidation_over_test_set( overall_settings_output_string, cnn_models):
    batch_size = 16 # in each iteration, we consider 32 training examples at once
    fold_number = 0 # fold iterator
    number_of_folds = len(cnn_models) # Creating number of folds based on the value used in the training step
    yfull_test = [] # variable to hold overall predictions for the test set
    #executing the actual cross validation test process over the test set
    for j in range(number_of_folds):
       model = cnn_models[j]
       fold_number += 1
       print('Fold number {} out of {}'.format(fold_number, number_of_folds))
       #Loading and normalizing testing samples
       testing_samples, testing_samples_id = load_normalize_testing_samples()
       #Calling the current model over the current test fold
       test_prediction = model.predict(testing_samples, batch_size=batch_size, verbose=2)
       yfull_test.append(test_prediction)
    test_result = merge_several_folds_mean(yfull_test, number_of_folds)
    overall_settings_output_string = 'loss_' + overall_settings_output_string \ + '_folds_' +
      str(number_of_folds)
    format_results_for_types(test_result, testing_samples_id, overall_settings_output_string)

模型训练与测试

现在,我们准备通过调用主函数create_model_with_kfold_cross_validation()来开始模型训练阶段,该函数用于通过 10 折交叉验证构建并训练 CNN 模型;然后我们可以调用测试函数来衡量模型对测试集的泛化能力:

if __name__ == '__main__':
  info_string, models = create_model_with_kfold_cross_validation()
  test_generality_crossValidation_over_test_set(info_string, models)

鱼类识别——全部一起

在解释了我们鱼类识别示例的主要构建模块之后,我们准备将所有代码片段连接起来,并查看我们如何通过几行代码构建如此复杂的系统。完整的代码可以在本书的附录部分找到。

不同的学习类型

根据 Arthur Samuel 的说法(en.wikipedia.org/wiki/Arthur_Samuel),数据科学赋予计算机在不需要显式编程的情况下学习的能力。因此,任何能够使用训练样本做出决策并处理未见数据的程序,都被认为是学习。数据科学或学习有三种不同的形式。

图 1.12 展示了常用的数据科学/机器学习类型:

图 1.12:数据科学/机器学习的不同类型。

监督学习

大多数数据科学家使用监督学习。监督学习是指您有一些解释性特征,称为输入变量(X),并且您有与训练样本相关联的标签,称为输出变量(Y)。任何监督学习算法的目标都是学习从输入变量(X)到输出变量(Y)的映射函数:

因此,有监督学习算法将尝试学习输入变量(X)到输出变量(Y)的映射关系,以便以后可以用来预测未见样本的Y值。

图 1.13 显示了任何有监督数据科学系统的典型工作流:

图 1.13:一个典型的有监督学习工作流/流程。上部分显示了训练过程,首先将原始数据输入到特征提取模块,我们将在此选择有意义的解释性特征来表示数据。之后,提取/选择的解释性特征与训练集结合,并将其输入到学习算法中以便从中学习。接着,我们进行模型评估,以调整参数并使学习算法从数据样本中获得最佳效果。

这种学习被称为有监督学习,因为你会为每个训练样本提供标签/输出。在这种情况下,我们可以说学习过程受到监督者的指导。算法根据训练样本做出决策,并根据数据的正确标签由监督者进行纠正。当有监督学习算法达到可接受的准确度时,学习过程就会停止。

有监督学习任务有两种不同的形式;回归和分类:

  • 分类:分类任务是当标签或输出变量是一个类别时,例如tunaOpah垃圾邮件非垃圾邮件

  • 回归:回归任务是当输出变量是一个实际值时,例如房价身高

无监督学习

无监督学习被认为是信息科学家使用的第二大常见学习类型。在这种学习中,只有解释性特征或输入变量(X)被给定,而没有任何对应的标签或输出变量。

无监督学习算法的目标是获取数据中的隐藏结构和模式。这种学习被称为无监督,因为训练样本没有相关的标签。因此,这是一种没有指导的学习过程,算法会尝试自行发现基本结构。

无监督学习可以进一步分为两种形式——聚类任务和关联任务:

  • 聚类:聚类任务是你想要发现相似的训练样本组并将它们归为一类,例如按主题对文档进行分组。

  • 关联:关联规则学习任务是你想要发现一些描述训练样本之间关系的规则,例如,喜欢看电影X的人通常也会看电影Y

图 1.14 显示了一个无监督学习的简单例子,其中我们有散乱的文档,并试图将相似的文档聚在一起:

图 1.14:展示了无监督学习如何使用相似度度量(如欧几里得距离)将相似的文档聚集在一起,并为它们绘制决策边界。

半监督学习

半监督学习是一种介于有监督学习和无监督学习之间的学习方式,在这种方式中,你有带输入变量(X)的训练样本,但只有一部分样本带有输出变量(Y)的标签。

这种学习方式的一个好例子是 Flickr (www.flickr.com/),在这里,用户上传了大量的图片,但只有一部分是带标签的(例如:日落、海洋和狗),其余的则没有标签。

为了解决属于这种类型的学习任务,你可以使用以下方法之一,或者它们的组合:

  • 有监督学习:学习/训练学习算法,针对未标记的数据进行预测,然后将所有训练样本反馈给算法,让其从中学习并预测未见过的数据。

  • 无监督学习:使用无监督学习算法来学习解释性特征或输入变量的潜在结构,就好像你没有任何标记的训练样本一样。

强化学习

机器学习中的最后一种学习类型是强化学习,在这种学习方式中没有监督者,只有奖励信号。

所以,强化学习算法将尝试做出决策,然后奖励信号会告诉我们这个决策是否正确。此外,这种监督反馈或奖励信号可能不会立即到来,而是会延迟几步。例如,算法现在做出决策,但只有在多步之后,奖励信号才会告诉我们这个决策是好是坏。

数据规模与行业需求

数据是我们学习计算的基础;没有数据,任何激励和富有创意的想法都将毫无意义。因此,如果你有一个好的数据科学应用程序,并且数据正确,那么你就已经准备好开始了。

具备从数据中分析和提取价值的能力在当今时代已经变得显而易见,尽管这取决于数据的结构,但由于大数据正成为当今的流行词汇,我们需要能够与如此庞大的数据量相匹配并在明确的学习时间内进行处理的数据科学工具和技术。如今,一切都在产生数据,能够应对这些数据已成为一项挑战。像谷歌、Facebook、微软、IBM 等大型公司都在构建自己的可扩展数据科学解决方案,以应对客户每天产生的大量数据。

TensorFlow 是一个机器智能/数据科学平台,于 2016 年 11 月 9 日由谷歌发布为开源库。它是一个可扩展的分析平台,使数据科学家能够利用大量数据在可视化的时间内构建复杂的系统,并且它还使得他们能够使用需要大量数据才能取得良好性能的贪心学习方法。

总结

在本章中,我们讲解了如何构建鱼类识别的学习系统;我们还展示了如何利用 TensorFlow 和 Keras,通过几行代码构建复杂的应用程序,如鱼类识别。这个编程示例并非要你理解其中的代码,而是为了展示构建复杂系统的可视化过程,以及数据科学,特别是深度学习,如何成为一种易于使用的工具。

我们看到,在构建学习系统时,作为数据科学家,你可能会遇到的挑战。

我们还了解了构建学习系统的典型设计周期,并解释了参与该周期的每个组件的整体概念。

最后,我们讲解了不同的学习类型,探讨了大大小小公司每天生成的大数据,以及这些海量数据如何引发警报,迫使我们构建可扩展的工具,以便能够分析和从这些数据中提取价值。

到此为止,读者可能会被目前为止提到的所有信息所压倒,但本章中大部分内容将在其他章节中展开,包括数据科学挑战和鱼类识别示例。本章的整体目的是让读者对数据科学及其发展周期有一个总体的了解,而不需要深入理解挑战和编码示例。编程示例在本章中提到的目的是为了打破大多数数据科学新手的恐惧感,并向他们展示像鱼类识别这样复杂的系统如何仅用几行代码就能实现。

接下来,我们将开始我们的通过示例之旅,通过一个示例来讲解数据科学的基本概念。接下来的部分将主要专注于为后续的高级章节做准备,通过著名的泰坦尼克号示例来进行讲解。我们将涉及许多概念,包括回归和分类的不同学习方法、不同类型的性能误差以及我们应该关注哪些误差,以及更多关于解决数据科学挑战和处理不同形式数据样本的内容。

第二章:数据建模实践——泰坦尼克号例子

线性模型是数据科学领域的基本学习算法。理解线性模型的工作原理对你学习数据科学的旅程至关重要,因为它是大多数复杂学习算法(包括神经网络)的基础构建块。

在本章中,我们将深入探讨数据科学领域的一个著名问题——泰坦尼克号例子。这个例子的目的是介绍用于分类的线性模型,并展示完整的机器学习系统管道,从数据处理与探索到模型评估。我们将在本章讨论以下主题:

  • 回归的线性模型

  • 分类的线性模型

  • 泰坦尼克号例子——模型构建与训练

  • 不同类型的误差

回归的线性模型

线性回归模型是最基本的回归模型类型,广泛应用于预测数据分析。回归模型的总体思想是研究两件事:

  1. 一组解释性特征/输入变量是否能很好地预测输出变量?模型是否使用了能够解释因变量(输出变量)变化的特征?

  2. 哪些特征是因变量的重要特征?它们通过哪些方式影响因变量(由参数的大小和符号表示)?这些回归参数用于解释一个输出变量(因变量)与一个或多个输入特征(自变量)之间的关系。

回归方程将阐述输入变量(自变量)对输出变量(因变量)的影响。这个方程最简单的形式,只有一个输入变量和一个输出变量,由公式 y = c + bx* 定义。这里,y = 估计的因变量值,c = 常数,b = 回归参数/系数,x = 输入(自)变量。

动机

线性回归模型是许多学习算法的基础,但这并不是它们流行的唯一原因。以下是它们流行的关键因素:

  • 广泛应用:线性回归是最古老的回归技术,广泛应用于许多领域,如预测和金融分析。

  • 运行速度快:线性回归算法非常简单,不包括过于昂贵的数学计算。

  • 易于使用无需大量调优):线性回归非常容易使用,并且它通常是机器学习或数据科学课程中学习的第一个方法,因为你不需要调整过多的超参数来提高性能。

  • 高度可解释:由于线性回归的简单性以及检查每个预测变量与系数对的贡献的便捷性,它具有很高的可解释性;你可以轻松理解模型的行为,并为非技术人员解读模型输出。如果某个系数为零,那么相关的预测变量不做任何贡献。如果某个系数不为零,可以轻松确定该特定预测变量的贡献。

  • 许多方法的基础:线性回归被认为是许多学习方法的基础,例如神经网络及其日益增长的部分——深度学习。

广告 – 一个财务示例

为了更好地理解线性回归模型,我们将通过一个广告示例来学习。我们将尝试预测一些公司在广告支出(包括电视、广播和报纸广告)方面的花费与其销售额之间的关系。

依赖项

为了使用线性回归建模我们的广告数据样本,我们将使用 Statsmodels 库来获取线性模型的良好特性,但稍后我们将使用 scikit-learn,它提供了许多对数据科学非常有用的功能。

使用 pandas 导入数据

在 Python 中有许多库可以用来读取、转换或写入数据。其中特别的一个库是 pandas:pandas.pydata.org/。Pandas 是一个开源库,提供强大的数据分析功能和工具,并且其数据结构非常易于使用。

你可以通过多种方式轻松获取 pandas。获取 pandas 的最佳方式是通过conda进行安装:pandas.pydata.org/pandas-docs/stable/install.html#installing-pandas-with-anaconda

“conda 是一个开源的包管理系统和环境管理系统,用于安装多个版本的软件包及其依赖项,并可以轻松地在它们之间切换。它支持 Linux、OS X 和 Windows 操作系统,最初为 Python 程序设计,但也可以打包和分发任何软件。” – conda 网站。

你可以通过安装 Anaconda 来轻松获得 conda,它是一个开源的数据科学平台。

那么,让我们看一下如何使用 pandas 来读取广告数据样本。首先,我们需要导入pandas

import pandas as pd

接下来,我们可以使用pandas.read_csv方法将数据加载到一个易于使用的 pandas 数据结构——DataFrame中。关于pandas.read_csv及其参数的更多信息,可以参考 pandas 的文档:pandas.pydata.org/pandas-docs/stable/generated/pandas.read_csv.html

# read advertising data samples into a DataFrame
advertising_data = pd.read_csv('http://www-bcf.usc.edu/~gareth/ISL/Advertising.csv', index_col=0)

传递给pandas.read_csv方法的第一个参数是表示文件路径的字符串值。该字符串可以是包含httpftps3file的 URL。传递的第二个参数是作为数据行标签/名称的列的索引。

现在,我们有了数据的 DataFrame,其中包含 URL 中提供的广告数据,每一行通过第一列进行标记。如前所述,pandas 提供了易于使用的数据结构,你可以将其作为数据容器。这些数据结构具有一些与之关联的方法,你将使用这些方法来转换和/或操作数据。

现在,让我们来看看广告数据的前五行:

# DataFrame.head method shows the first n rows of the data where the   
# default value of n is 5, DataFrame.head(n=5)
advertising_data.head()

输出:

电视 广播 报纸 销售
1 230.1 37.8 69.2 22.1
2 44.5 39.3 45.1 10.4
3 17.2 45.9 69.3 9.3
4 151.5 41.3 58.5 18.5
5 180.8 10.8 58.4 12.9

理解广告数据

这个问题属于监督学习类型,其中我们有解释性特征(输入变量)和响应(输出变量)。

特征/输入变量是什么?

  • 电视:在给定市场上为单一产品在电视上花费的广告费用(单位:千美元)

  • 广播:在广播上花费的广告费用

  • 报纸:在报纸上花费的广告费用

响应/结果/输出变量是什么?

  • 销售:在给定市场上单一产品的销售额(单位:千个商品)

我们还可以使用DataFrame方法 shape 来了解数据中的样本/观测值数量:

# print the shape of the DataFrame
advertising_data.shape
Output:
(200, 4)

所以,广告数据中有 200 个观测值。

数据分析与可视化

为了理解数据的潜在形式、特征与响应之间的关系,以及更多的见解,我们可以使用不同类型的可视化。为了理解广告数据特征与响应之间的关系,我们将使用散点图。

为了对数据进行不同类型的可视化,你可以使用 Matplotlib(matplotlib.org/),它是一个用于创建可视化的 Python 2D 库。要获取 Matplotlib,你可以按照他们的安装说明:matplotlib.org/users/installing.html

让我们导入可视化库 Matplotlib:

import matplotlib.pyplot as plt

# The next line will allow us to make inline plots that could appear directly in the notebook
# without poping up in a different window
%matplotlib inline

现在,让我们使用散点图来可视化广告数据特征与响应变量之间的关系:

fig, axs = plt.subplots(1, 3, sharey=True)

# Adding the scatterplots to the grid 
advertising_data.plot(kind='scatter', x='TV', y='sales', ax=axs[0], figsize=(16, 8))
advertising_data.plot(kind='scatter', x='radio', y='sales', ax=axs[1])
advertising_data.plot(kind='scatter', x='newspaper', y='sales', ax=axs[2])

输出:

图 1:用于理解广告数据特征与响应变量之间关系的散点图

现在,我们需要看看广告如何帮助增加销售。因此,我们需要就此提出几个问题给自己。有价值的问题可以是关于广告与销售之间的关系,哪种广告对销售的贡献更大,以及每种类型广告对销售的大致影响。我们将尝试使用简单线性模型来回答此类问题。

简单回归模型

线性回归模型是一种学习算法,用于预测定量(也称为数值响应,其使用解释性 特征(或输入预测器)的组合。

只有一个特征的简单线性回归模型采用以下形式:

y = beta[0] + beta[1]x

这里:

  • y是预测的数值(响应) → 销售

  • x是特征

  • beta[0]称为截距

  • beta[1]是特征x电视广告的系数

beta[0]beta[1]都被视为模型的系数。为了创建一个能够预测广告示例中销售值的模型,我们需要学习这些系数,因为beta[1]将是特征x对响应y的学习效果。例如,如果beta[1] = 0.04,这意味着额外投入 100 美元用于电视广告与销售增加四个小部件相关。因此,我们需要继续看看如何学习这些系数。

学习模型系数

为了估计我们模型的系数,我们需要用回归线拟合数据,以获得与实际销售类似的答案。为了获得最适合数据的回归线,我们将使用一个称为最小二乘的标准。因此,我们需要找到一条最小化预测值与观察值之间差异的线。图 2说明了这一点:

图 2:用最小化残差平方和(预测值与观察值之差)的回归线拟合数据点(电视广告样本)

下列元素存在于图 2中:

  • 黑色点表示x(电视广告)和y(销售)的实际或观察值

  • 蓝线表示最小二乘线(回归线)

  • 红线表示残差,即预测值与观察(实际)值之间的差异

所以,这就是我们的系数与最小二乘线(回归线)的关系:

  • beta[0]截距,即x =0y的值

  • beta[1]斜率,表示y相对于x的变化量

图 3展示了这个图形化解释:

图 3:最小二乘线与模型系数之间的关系

现在,让我们继续使用Statsmodels来学习这些系数:

# To use the formula notation below, we need to import the module like the following
import statsmodels.formula.api as smf
# create a fitted model in one line of code(which will represent the least squares line)
lm = smf.ols(formula='sales ~ TV', data=advertising_data).fit()
# show the trained model coefficients
lm.params

输出:

Intercept    7.032594
TV           0.047537
dtype: float64

如我们所提到的,线性回归模型的一个优点是它们易于解释,所以我们继续解释该模型。

解释模型系数

让我们看看如何解释模型的系数,比如电视广告的系数 (beta[1]):

  • 输入/特征(电视广告)支出的单位增加销售(响应)单位增加0.047537相关。换句话说,每额外花费 $100 在电视广告上销售增加 4.7537 单位相关。

从电视广告数据构建已学习模型的目标是预测未见数据的销售额。那么,让我们看看如何使用已学习的模型来预测销售值(我们不知道的)基于给定的电视广告值。

使用模型进行预测

假设我们有未见过的电视广告支出数据,并且我们想知道这些广告对公司销售的相应影响。那么,我们需要使用已学习的模型来为我们做这个预测。假设我们想知道 $50000 的电视广告支出会使销售额增加多少。

让我们使用已学习的模型系数来进行计算:

y = 7.032594 + 0.047537 x 50

# manually calculating the increase in the sales based on $50k
7.032594 + 0.047537*50000

输出:

9,409.444

我们还可以使用 Statsmodels 来为我们做预测。首先,我们需要将电视广告值以 pandas DataFrame 的形式提供,因为 Statsmodels 接口期望如此:

# creating a Pandas DataFrame to match Statsmodels interface expectations
new_TVAdSpending = pd.DataFrame({'TV': [50000]})
new_TVAdSpending.head()

输出:

电视广告
0 50000

现在,我们可以使用预测函数来预测销售值:

# use the model to make predictions on a new value
preds = lm.predict(new_TVAdSpending)

输出:

array([ 9.40942557])

让我们看看已学习的最小二乘法拟合直线是什么样子的。为了绘制这条线,我们需要两个点,每个点由以下一对值表示:(x, predict_value_of_x)。

那么,让我们取电视广告特征的最小值和最大值:

# create a DataFrame with the minimum and maximum values of TV
X_min_max = pd.DataFrame({'TV': [advertising_data.TV.min(), advertising_data.TV.max()]})
X_min_max.head()

输出:

电视广告
0 0.7
1 296.4

让我们为这两个值获取相应的预测:

# predictions for X min and max values
predictions = lm.predict(X_min_max)
predictions

输出:

array([  7.0658692,  21.12245377])

现在,让我们绘制实际数据,并用最小二乘法拟合一条直线:

# plotting the acutal observed data
advertising_data.plot(kind='scatter', x='TV', y='sales')
#plotting the least squares line
plt.plot(new_TVAdSpending, preds, c='red', linewidth=2)

输出:

图 4:实际数据与最小二乘法拟合直线的图示

这个示例的扩展及进一步的解释将在下一章中讲解。

用于分类的线性模型

在本节中,我们将讲解逻辑回归,它是广泛使用的分类算法之一。

什么是逻辑回归?逻辑回归的简单定义是它是一种涉及线性判别的分类算法。

我们将通过两个要点来澄清这个定义:

  1. 与线性回归不同,逻辑回归并不试图在给定一组特征或输入变量的情况下估计/预测数值变量的值。相反,逻辑回归算法的输出是给定样本/观测值属于特定类别的概率。简单来说,假设我们有一个二分类问题。在这种问题中,输出变量中只有两个类别,例如,患病或未患病。那么,某个样本属于患病类别的概率是P[0],而属于未患病类别的概率是P[1] = 1 - P[0]。因此,逻辑回归算法的输出始终介于 0 和 1 之间。

  2. 正如你可能知道的那样,有许多回归或分类的学习算法,每种学习算法对数据样本有自己的假设。选择适合你数据的学习算法的能力将随着实践和对该主题的深入理解而逐渐掌握。因此,逻辑回归算法的核心假设是,我们的输入/特征空间可以通过一个线性面将其分隔为两个区域(每个类别一个区域),如果我们只有两个特征,这个面就是一条线,若有三个特征,则是一个平面,依此类推。这个边界的位置和方向将由你的数据决定。如果你的数据满足这种约束,即通过一个线性面将它们分隔成对应每个类别的区域,那么这些数据就被称为线性可分的。下图展示了这一假设。在图 5中,我们有三个维度的输入或特征和两个可能的类别:患病(红色)和未患病(蓝色)。分隔两个区域的边界称为线性判别面,因为它是线性的,并且有助于模型区分属于不同类别的样本:

图 5:分隔两个类别的线性决策面

如果你的数据样本不是线性可分的,你可以通过将数据转化为更高维度的空间来实现线性可分,即通过添加更多特征。

分类与逻辑回归

在上一节中,我们学习了如何将连续量(例如,电视广告对公司销售的影响)预测为输入值(例如,电视、广播和报纸广告)的线性函数。但是,对于其他任务,输出将不是连续量。例如,预测某人是否患病是一个分类问题,我们需要一个不同的学习算法来完成这个任务。在本节中,我们将深入探讨逻辑回归的数学分析,它是一种用于分类任务的学习算法。

在线性回归中,我们试图使用线性模型函数 y = hθ = θ^Τ x 来预测数据集中第 i^(th) 个样本 x^((i)) 的输出变量 y^((i)) 的值。但这并不是分类任务的一个好方法,尤其是像预测二进制标签 (y^((i)) ∈ {0,1}) 这样的任务。

逻辑回归是我们可以用于分类任务的众多学习算法之一,在这种方法中,我们使用不同的假设类来预测一个特定样本属于正类的概率和属于负类的概率。因此,在逻辑回归中,我们将尝试学习以下函数:

该函数 通常被称为 sigmoidlogistic 函数,它将 θ^Τx 的值压缩到一个固定范围 [0,1],如下图所示。因为值会被压缩到 [0,1] 之间,所以我们可以将 解释为一个概率。

我们的目标是寻找参数 θ 的值,使得当输入样本 x 属于正类时,概率 P(y = 1|x) = hθ 较大,而当 x 属于负类时,概率较小:

图 6:Sigmoid 函数的形状

所以,假设我们有一组训练样本及其对应的二进制标签 {(x^((i)), y^((i))): i = 1,...,m}。我们需要最小化以下代价函数,该函数衡量给定的 h[θ] 的表现如何:

注意,对于每个训练样本,方程求和的两个项中只有一个非零项(取决于标签 y^((i)) 的值是 0 还是 1)。当 y^((i)) = 1 时,最小化模型的代价函数意味着我们需要让 hθ)) 较大;而当 y^((i)) = 0*,我们希望让 1 - h[θ] 较大。

现在,我们有一个代价函数来计算给定假设 h[θ] 与训练样本的匹配程度。我们可以通过使用优化技术来最小化 J(θ),从而找到最佳的参数选择 θ,进而学习如何对训练样本进行分类。一旦我们完成这一步骤,就可以利用这些参数将新的测试样本分类为 1 或 0,查看哪一个类标签的概率较高。如果 P(y = 1|x) < P(y = 0|x),则输出 0,否则输出 1,这与定义一个 0.5 的类间阈值并检查 hθ > 0.5 是一致的。

为了最小化代价函数 J(θ),我们可以使用一种优化技术,寻找能够最小化代价函数的最佳 θ 值。因此,我们可以使用一种叫做梯度的微积分工具,它尝试找到代价函数的最大增加速率。然后,我们可以朝相反方向前进,以找到该函数的最小值;例如,J(θ) 的梯度记作 ∇[θ]J(θ),这意味着对代价函数相对于模型参数的梯度进行求解。因此,我们需要提供一个函数,用来计算 J(θ)∇[θ]J(θ),以便根据任何给定的 θ 值进行求解。如果我们已经推导出了代价函数 J(θ) 相对于 θ[j] 的梯度或导数,那么我们将得到以下结果:

这可以写成向量形式:

现在,我们对逻辑回归有了数学上的理解,那么让我们继续使用这一新学习方法来解决分类任务。

Titanic 示例 – 模型构建与训练

泰坦尼克号沉船事件是历史上最臭名昭著的事件之一。此次事故导致 2,224 名乘客和船员中有 1,502 人死亡。在这个问题中,我们将利用数据科学来预测乘客是否能够幸存,并基于这次灾难的实际统计数据来测试我们模型的性能。

要继续 Titanic 示例,你需要执行以下步骤:

  1. 通过点击 github.com/ahmed-menshawy/ML_Titanic/archive/master.zip 下载该仓库的 ZIP 文件,或者从终端执行:

  2. Git 克隆:github.com/ahmed-menshawy/ML_Titanic.git

  3. 安装 [virtualenv]:(virtualenv.readthedocs.org/en/latest/installation.html)

  4. 导航到你解压或克隆的代码库目录,并通过 virtualenv ml_titanic 创建一个虚拟环境

  5. 使用 source ml_titanic/bin/activate 激活虚拟环境

  6. 使用 pip install -r requirements.txt 安装所需的依赖项

  7. 从命令行或终端执行 ipython notebook

  8. 按照本章中的示例代码操作

  9. 完成后,通过执行 deactivate 来停用虚拟环境

数据处理与可视化

在这一部分,我们将进行一些数据预处理和分析。数据探索与分析被认为是应用机器学习过程中的最重要步骤之一,甚至可能是最关键的一步,因为在这个阶段,你将了解将会陪伴你整个训练过程的“朋友”——数据。同时,了解你的数据能够帮助你缩小可能使用的候选算法范围,从而找到最适合你数据的算法。

首先,我们导入实现所需的必要包:

import matplotlib.pyplot as plt
 %matplotlib inline

 from statsmodels.nonparametric.kde import KDEUnivariate
 from statsmodels.nonparametric import smoothers_lowess
 from pandas import Series, DataFrame
 from patsy import dmatrices
 from sklearn import datasets, svm

 import numpy as np
 import pandas as pd
 import statsmodels.api as sm

from scipy import stats
stats.chisqprob = lambda chisq, df: stats.chi2.sf(chisq, df)

让我们使用 Pandas 读取泰坦尼克号乘客和船员数据:

titanic_data = pd.read_csv("data/titanic_train.csv")

接下来,让我们检查数据集的维度,看看我们有多少个样本,以及描述数据集的解释性特征有多少:

titanic_data.shape

 Output:
 (891, 12)

所以,我们一共有 891 个观察样本、数据样本或乘客/船员记录,以及 12 个解释性特征来描述这些记录:

list(titanic_data)

 Output:
 ['PassengerId',
 'Survived',
 'Pclass',
 'Name',
 'Sex',
 'Age',
 'SibSp',
 'Parch',
 'Ticket',
 'Fare',
 'Cabin',
 'Embarked']

让我们看看一些样本/观察数据:

titanic_data[500:510]

输出:

图 7:泰坦尼克号数据集的样本

现在,我们有一个 Pandas 数据框,包含了我们需要分析的 891 名乘客的信息。数据框的列表示每个乘客/船员的解释性特征,如姓名、性别或年龄。

其中一些解释性特征是完整的,没有任何缺失值,比如生还特征,拥有 891 个条目。其他解释性特征包含缺失值,比如年龄特征,只有 714 个条目。数据框中的任何缺失值都表示为 NaN。

如果你探索所有的数据集特征,你会发现票务和船舱特征有很多缺失值(NaN),因此它们不会对我们的分析增加太多价值。为了解决这个问题,我们将从数据框中删除这些特征。

使用以下代码删除数据框中ticketcabin特征:

titanic_data = titanic_data.drop(['Ticket','Cabin'], axis=1)

数据集中有很多原因可能会导致缺失值。但是,为了保持数据集的完整性,我们需要处理这些缺失值。在这个特定问题中,我们选择将其删除。

使用以下代码删除所有剩余特征中的NaN值:

titanic_data = titanic_data.dropna()

现在,我们有了一个完整的数据集,可以用来进行分析。如果你决定删除所有 NaN,而不首先删除票务船舱特征,你会发现大部分数据集会被删除,因为.dropna()方法会删除数据框中的一个观察值,即使它只在某个特征中有一个 NaN。

让我们做一些数据可视化,看看某些特征的分布,并理解解释性特征之间的关系:

# declaring graph parameters
fig = plt.figure(figsize=(18,6))
alpha=alpha_scatterplot = 0.3
alpha_bar_chart = 0.55
# Defining a grid of subplots to contain all the figures
ax1 = plt.subplot2grid((2,3),(0,0))
# Add the first bar plot which represents the count of people who survived vs not survived.
titanic_data.Survived.value_counts().plot(kind='bar', alpha=alpha_bar_chart)
# Adding margins to the plot
ax1.set_xlim(-1, 2)
# Adding bar plot title
plt.title("Distribution of Survival, (1 = Survived)")
plt.subplot2grid((2,3),(0,1))
plt.scatter(titanic_data.Survived, titanic_data.Age, alpha=alpha_scatterplot)
# Setting the value of the y label (age)
plt.ylabel("Age")
# formatting the grid
plt.grid(b=True, which='major', axis='y')
plt.title("Survival by Age, (1 = Survived)")
ax3 = plt.subplot2grid((2,3),(0,2))
titanic_data.Pclass.value_counts().plot(kind="barh", alpha=alpha_bar_chart)
ax3.set_ylim(-1, len(titanic_data.Pclass.value_counts()))
plt.title("Class Distribution")
plt.subplot2grid((2,3),(1,0), colspan=2)
# plotting kernel density estimate of the subse of the 1st class passenger’s age
titanic_data.Age[titanic_data.Pclass == 1].plot(kind='kde')
titanic_data.Age[titanic_data.Pclass == 2].plot(kind='kde')
titanic_data.Age[titanic_data.Pclass == 3].plot(kind='kde')
# Adding x label (age) to the plot
plt.xlabel("Age")
plt.title("Age Distribution within classes")
# Add legend to the plot.
plt.legend(('1st Class', '2nd Class','3rd Class'),loc='best')
ax5 = plt.subplot2grid((2,3),(1,2))
titanic_data.Embarked.value_counts().plot(kind='bar', alpha=alpha_bar_chart)
ax5.set_xlim(-1, len(titanic_data.Embarked.value_counts()))
plt.title("Passengers per boarding location")

图 8:泰坦尼克号数据样本的基本可视化

如前所述,本次分析的目的是根据可用特征预测特定乘客是否能在灾难中生还,比如旅行舱位(在数据中称为pclass)、性别年龄票价。因此,让我们看看能否更好地通过可视化来了解幸存和死亡的乘客。

首先,让我们绘制一个条形图,看看每个舱位(生还/死亡)中观察的数量:

plt.figure(figsize=(6,4))
fig, ax = plt.subplots()
titanic_data.Survived.value_counts().plot(kind='barh', color="blue", alpha=.65)
ax.set_ylim(-1, len(titanic_data.Survived.value_counts()))
plt.title("Breakdown of survivals(0 = Died, 1 = Survived)")

图 9:生还情况分布

让我们通过性别划分前面的图表,进一步了解数据:

fig = plt.figure(figsize=(18,6))
#Plotting gender based analysis for the survivals.
male = titanic_data.Survived[titanic_data.Sex == 'male'].value_counts().sort_index()
female = titanic_data.Survived[titanic_data.Sex == 'female'].value_counts().sort_index()
ax1 = fig.add_subplot(121)
male.plot(kind='barh',label='Male', alpha=0.55)
female.plot(kind='barh', color='#FA2379',label='Female', alpha=0.55)
plt.title("Gender analysis of survivals (raw value counts) "); plt.legend(loc='best')
ax1.set_ylim(-1, 2)
ax2 = fig.add_subplot(122)
(male/float(male.sum())).plot(kind='barh',label='Male', alpha=0.55)  
(female/float(female.sum())).plot(kind='barh', color='#FA2379',label='Female', alpha=0.55)
plt.title("Gender analysis of survivals"); plt.legend(loc='best')
ax2.set_ylim(-1, 2)

图 10:基于性别特征的泰坦尼克号数据进一步拆解

现在,我们有了更多关于两个可能类别(生还和死亡)的信息。探索和可视化步骤是必要的,因为它能让你更深入了解数据的结构,并帮助你选择适合问题的学习算法。正如你所看到的,我们从非常基础的图表开始,然后逐步增加图表的复杂性,以便发现更多关于我们所处理数据的信息。

数据分析 – 监督式机器学习

该分析的目的是预测幸存者。因此,结果将是生还或未生还,这是一个二元分类问题;在这个问题中,只有两个可能的类别。

我们可以使用许多学习算法来解决二元分类问题。逻辑回归就是其中之一。正如维基百科所解释的:

在统计学中,逻辑回归或 logit 回归是一种回归分析方法,用于预测类别型因变量(可以取有限数量值的因变量,这些值的大小没有实际意义,但其大小的排序可能有意义,也可能没有意义)的结果,基于一个或多个预测变量。也就是说,它用于估计定性响应模型中参数的经验值。描述单次试验可能结果的概率是通过逻辑函数建模的,作为解释变量(预测变量)的函数。在本文中,"逻辑回归"通常特指因变量为二元的情况——即,类别数量为二——而具有多个类别的问题称为多项式逻辑回归,若类别是有序的,则称为有序逻辑回归。逻辑回归衡量类别型因变量与一个或多个自变量之间的关系,这些自变量通常(但不一定)是连续的,通过使用概率得分作为因变量的预测值。

为了使用逻辑回归,我们需要创建一个公式,告诉模型我们所提供的特征/输入类型:

# model formula
# here the ~ sign is an = sign, and the features of our dataset
# are written as a formula to predict survived. The C() lets our
# regression know that those variables are categorical.
# Ref: http://patsy.readthedocs.org/en/latest/formulas.html
formula = 'Survived ~ C(Pclass) + C(Sex) + Age + SibSp + C(Embarked)'
# create a results dictionary to hold our regression results for easy analysis later
results = {}
# create a regression friendly dataframe using patsy's dmatrices function
y,x = dmatrices(formula, data=titanic_data, return_type='dataframe')
# instantiate our model
model = sm.Logit(y,x)
# fit our model to the training data
res = model.fit()
# save the result for outputing predictions later
results['Logit'] = [res, formula]
res.summary()
Output:
Optimization terminated successfully.
 Current function value: 0.444388
 Iterations 6

图 11:逻辑回归结果

现在,让我们绘制模型的预测值与实际值,以及残差图,残差是目标变量的实际值与预测值之间的差异:

# Plot Predictions Vs Actual
plt.figure(figsize=(18,4));
plt.subplot(121, axisbg="#DBDBDB")
# generate predictions from our fitted model
ypred = res.predict(x)
plt.plot(x.index, ypred, 'bo', x.index, y, 'mo', alpha=.25);
plt.grid(color='white', linestyle='dashed')
plt.title('Logit predictions, Blue: \nFitted/predicted values: Red');
# Residuals
ax2 = plt.subplot(122, axisbg="#DBDBDB")
plt.plot(res.resid_dev, 'r-')
plt.grid(color='white', linestyle='dashed')
ax2.set_xlim(-1, len(res.resid_dev))
plt.title('Logit Residuals');

图 12:理解 logit 回归模型

现在,我们已经构建了逻辑回归模型,并且在此之前,我们对数据集进行了分析和探索。上述示例向你展示了构建机器学习解决方案的一般流程。

大多数时候,从业者会因为缺乏对机器学习概念的理解经验而陷入一些技术陷阱。例如,有人可能在测试集上获得了 99%的准确率,但在没有调查数据中类别分布的情况下(例如,负样本和正样本的数量),他们就部署了模型。

为了突出这些概念,并区分你需要注意的不同类型的误差,以及你应该真正关心的误差,我们将进入下一部分。

不同类型的误差

在机器学习中,有两种类型的误差,作为数据科学的新人,你需要理解它们之间至关重要的区别。如果你最终最小化了错误的类型,整个学习系统将变得毫无用处,你将无法在未见过的数据上实践应用它。为了减少从业者对这两种误差的误解,我们将在接下来的两个部分中进行解释。

表观(训练集)误差

这是第一种你不必关心最小化的误差类型。这个误差类型的值小并不意味着你的模型在未见过的数据上能够很好地工作(泛化)。为了更好地理解这种误差类型,我们将举一个简单的课堂情境例子。解决课堂问题的目的并不是能够在考试中再次解决相同的问题,而是能够解决其他可能与课堂上练习的问题不完全相似的问题。考试题目可能来自与课堂问题同一类,但不一定是完全相同的。

表观误差是训练好的模型在我们已经知道真实结果/输出的训练集上的表现。如果你能够在训练集上得到 0 误差,那么它是一个很好的指示,表明你的模型(大多数情况下)在未见过的数据上(不会)表现良好(不会泛化)。另一方面,数据科学的核心在于使用训练集作为基础知识,使学习算法能够在未来的未见过数据上表现良好。

图 3中,红色曲线代表表观误差。每当你增加模型的记忆能力(例如,通过增加解释性特征的数量来增加模型复杂度),你会发现表观误差趋近于零。可以证明,如果你的特征数量与观测/样本数量相同,那么表观误差将为零:

图 13:表观误差(红色曲线)和泛化/真实误差(浅蓝色)

泛化/真实误差

这是数据科学中第二个也是更重要的错误类型。构建学习系统的整个目的是在测试集上获得更小的泛化误差;换句话说,让模型在一组没有在训练阶段使用的观测数据/样本上表现良好。如果你还记得上一节中的课堂情境,你可以把泛化误差看作是解决考试题目的一种能力,这些考试题目不一定和你在课堂上解决的题目类似,也不是你通过学习和熟悉科目所解决的题目。因此,泛化性能是指模型能够利用它在训练阶段学到的技能(参数),正确预测未见过数据的结果/输出。

图 13中,浅蓝色的线表示泛化误差。你可以看到,随着模型复杂度的增加,泛化误差会减少,直到某个点,模型开始失去其增益能力,泛化误差会开始上升。这个曲线的部分,被称为过拟合,是指泛化误差失去增益能力的地方。

本节的关键内容是尽量减少泛化误差。

总结

线性模型是一个非常强大的工具,如果你的数据符合其假设,它可以作为初始学习算法。理解线性模型将帮助你理解更复杂的模型,因为这些复杂模型以线性模型为构建模块。

接下来,我们将继续使用泰坦尼克号例子,更详细地探讨模型复杂度和评估。模型复杂度是一个非常强大的工具,你需要小心使用它,以便提升泛化误差。误解它会导致过拟合问题。

第三章:特征工程与模型复杂度——泰坦尼克号示例回顾

模型复杂度与评估是建立成功的数据科学系统的必要步骤。有很多工具可以用来评估和选择你的模型。在这一章中,我们将介绍一些能够通过增加更多描述性特征并从现有特征中提取有意义信息的工具。我们还将讨论与特征数量优化相关的其他工具,并了解为什么特征数量过多而训练样本/观察数过少是一个问题。

本章将解释以下主题:

  • 特征工程

  • 维度灾难

  • 泰坦尼克号示例回顾——整体概览

  • 偏差-方差分解

  • 学习可见性

特征工程

特征工程是模型性能的关键组成部分之一。一个具有正确特征的简单模型,可能比一个具有糟糕特征的复杂模型表现更好。你可以把特征工程过程看作是决定预测模型成功或失败的最重要步骤。如果你理解数据,特征工程将会变得更加容易。

特征工程广泛应用于任何使用机器学习解决单一问题的人,这个问题是:如何从数据样本中获取最大价值用于预测建模?这是特征工程过程和实践所解决的问题,而你的数据科学技能的成功,始于如何有效地表示你的数据。

预测建模是一个将一组特征或输入变量(x[1], x[2], ..., x[n])转换为感兴趣的输出/目标(y)的公式或规则。那么,什么是特征工程呢?它是从现有的输入变量(x[1], x[2], ..., x[n])中创建新的输入变量或特征(z[1], z[2], ..., z[n])的过程。我们不仅仅是创建任何新特征;新创建的特征应该对模型的输出有所贡献并且相关。创建与模型输出相关的特征,如果了解领域知识(如营销、医学等),将会是一个容易的过程。即使机器学习从业人员在这一过程中与领域专家互动,特征工程的结果也会更加优秀。

一个领域知识有用的例子是给定一组输入变量/特征(温度、风速和云层覆盖百分比)时建模降雨的可能性。在这个特定的例子中,我们可以构建一个新的二进制特征,叫做阴天,其值为 1 或否,表示云层覆盖百分比小于 20%,否则值为 0 或是。在这个例子中,领域知识对于指定阈值或切割百分比至关重要。输入越深思熟虑且有用,模型的可靠性和预测能力就越强。

特征工程的类型

作为一种技术,特征工程有三个主要的子类别。作为深度学习的实践者,你可以自由选择它们,或以某种方式将它们结合起来。

特征选择

有时被称为特征重要性,这是根据输入变量对目标/输出变量的贡献对输入变量进行排序的过程。此外,这个过程也可以被视为根据输入变量在模型预测能力中的价值对其进行排序的过程。

一些学习方法会在其内部过程中执行这种特征排名或重要性(如决策树)。这些方法大多使用熵来过滤掉价值较低的变量。在某些情况下,深度学习实践者会使用这种学习方法来选择最重要的特征,然后将其输入到更好的学习算法中。

降维

降维有时也被称为特征提取,它是将现有输入变量组合成一个新的、显著减少的输入变量集合的过程。用于这种类型特征工程的最常用方法之一是主成分分析PCA),它利用数据的方差来生成一个减少后的输入变量集合,这些新变量看起来不像原始的输入变量。

特征构建

特征构建是特征工程中常用的一种类型,通常人们在谈论特征工程时会提到它。这项技术是从原始数据中手工构建或构造新特征的过程。在这种类型的特征工程中,领域知识非常有用,可以通过现有特征手动构建其他特征。像其他特征工程技术一样,特征构建的目的是提高模型的预测能力。一个简单的特征构建例子是利用日期时间戳特征生成两个新特征,如 AM 和 PM,这可能有助于区分白天和夜晚。我们还可以通过计算噪声特征的均值来将嘈杂的数值特征转化为更简单的名义特征,然后确定给定的行是否大于或小于该均值。

泰坦尼克号例子重访

在这一节中,我们将再次通过不同的角度使用特征工程工具来回顾泰坦尼克号的例子。如果你跳过了第二章,数据建模实践 - 泰坦尼克号的例子,泰坦尼克号的例子是一个 Kaggle 竞赛,目的是预测某个特定乘客是否幸存。

在重新审视泰坦尼克号的例子时,我们将使用 scikit-learn 和 pandas 库。所以首先,让我们开始读取训练集和测试集,并获取一些数据的统计信息:

# reading the train and test sets using pandas
train_data = pd.read_csv('data/train.csv', header=0)
test_data = pd.read_csv('data/test.csv', header=0)

# concatenate the train and test set together for doing the overall feature engineering stuff
df_titanic_data = pd.concat([train_data, test_data])

# removing duplicate indices due to coming the train and test set by re-indexing the data
df_titanic_data.reset_index(inplace=True)

# removing the index column the reset_index() function generates
df_titanic_data.drop('index', axis=1, inplace=True)

# index the columns to be 1-based index
df_titanic_data = df_titanic_data.reindex_axis(train_data.columns, axis=1)

我们需要指出几点关于前面代码片段的内容:

  • 如所示,我们使用了 pandas 的concat函数将训练集和测试集的数据框合并。这对于特征工程任务很有用,因为我们需要全面了解输入变量/特征的分布情况。

  • 在合并两个数据框后,我们需要对输出数据框进行一些修改。

缺失值

这一步是获得客户提供的新数据集后需要首先考虑的事情,因为几乎每个数据集都会有缺失或错误的数据。在接下来的章节中,你将看到一些学习算法能够处理缺失值,而其他算法则需要你自己处理缺失数据。在这个示例中,我们将使用来自 scikit-learn 的随机森林分类器,它需要单独处理缺失数据。

处理缺失数据有不同的方法。

删除任何含有缺失值的样本

如果你有一个小数据集且缺失值较多,那么这种方法就不太合适,因为删除含有缺失值的样本会导致数据无效。如果你有大量数据,并且删除这些数据不会对原始数据集造成太大影响,那么这种方法可能是一个快捷且简单的选择。

缺失值填补

这种方法在处理分类数据时非常有用。其背后的直觉是,缺失值可能与其他变量相关,删除这些缺失值会导致信息丢失,从而显著影响模型。

例如,如果我们有一个二值变量,可能的取值为-1 和 1,我们可以添加另一个值(0)来表示缺失值。你可以使用以下代码将Cabin特征的空值替换为U0

# replacing the missing value in cabin variable "U0"
df_titanic_data['Cabin'][df_titanic_data.Cabin.isnull()] = 'U0'

分配一个平均值

这也是一种常见的方法,因为它的简便性。对于数值特征,你可以直接用均值或中位数替换缺失值。在处理分类变量时,你也可以通过将众数(出现频率最高的值)分配给缺失值来使用此方法。

以下代码将Fare特征中非缺失值的中位数分配给缺失值:

# handling the missing values by replacing it with the median fare
df_titanic_data['Fare'][np.isnan(df_titanic_data['Fare'])] = df_titanic_data['Fare'].median()

或者,你可以使用以下代码查找Embarked特征中出现频率最高的值,并将其分配给缺失值:

# replacing the missing values with the most common value in the variable
df_titanic_data.Embarked[df_titanic_data.Embarked.isnull()] = df_titanic_data.Embarked.dropna().mode().values

使用回归或其他简单模型来预测缺失变量的值

这是我们将用于泰坦尼克号示例中Age特征的方法。Age特征是预测乘客生还的一个重要步骤,采用前述方法通过计算均值填补会使我们丧失一些信息。

Age feature:
# Define a helper function that can use RandomForestClassifier for handling the missing values of the age variable
def set_missing_ages():
    global df_titanic_data

    age_data = df_titanic_data[
        ['Age', 'Embarked', 'Fare', 'Parch', 'SibSp', 'Title_id', 'Pclass', 'Names', 'CabinLetter']]
    input_values_RF = age_data.loc[(df_titanic_data.Age.notnull())].values[:, 1::]
    target_values_RF = age_data.loc[(df_titanic_data.Age.notnull())].values[:, 0]

    # Creating an object from the random forest regression function of sklearn<use the documentation for more details>
    regressor = RandomForestRegressor(n_estimators=2000, n_jobs=-1)

    # building the model based on the input values and target values above
    regressor.fit(input_values_RF, target_values_RF)

    # using the trained model to predict the missing values
    predicted_ages = regressor.predict(age_data.loc[(df_titanic_data.Age.isnull())].values[:, 1::])

    # Filling the predicted ages in the original titanic dataframe
    age_data.loc[(age_data.Age.isnull()), 'Age'] = predicted_ages

特征变换

在前两部分中,我们介绍了如何读取训练集和测试集并将其合并,还处理了一些缺失值。现在,我们将使用 scikit-learn 的随机森林分类器来预测乘客的生还情况。不同实现的随机森林算法接受的数据类型不同。scikit-learn 实现的随机森林仅接受数值数据。因此,我们需要将分类特征转换为数值特征。

有两种类型的特征:

  • 定量:定量特征是以数值尺度度量的,可以进行有意义的排序。在泰坦尼克号数据中,Age特征就是定量特征的一个例子。

  • 定性:定性变量,也称为分类变量,是非数值型变量。它们描述的是可以归类的数据。在泰坦尼克号数据中,Embarked(表示出发港口的名称)特征就是定性特征的一个例子。

我们可以对不同的变量应用不同种类的转换。以下是一些可以用来转换定性/分类特征的方法。

虚拟特征

这些变量也被称为分类特征或二元特征。如果要转换的特征只有少数几个不同的值,那么这种方法将是一个不错的选择。在泰坦尼克号数据中,Embarked特征只有三个不同的值(SCQ),并且这些值经常出现。因此,我们可以将Embarked特征转换为三个虚拟变量('Embarked_S''Embarked_C''Embarked_Q'),以便使用随机森林分类器。

以下代码将展示如何进行这种转换:

# constructing binary features
def process_embarked():
    global df_titanic_data

    # replacing the missing values with the most common value in the variable
    df_titanic_data.Embarked[df.Embarked.isnull()] = df_titanic_data.Embarked.dropna().mode().values

    # converting the values into numbers
    df_titanic_data['Embarked'] = pd.factorize(df_titanic_data['Embarked'])[0]

    # binarizing the constructed features
    if keep_binary:
        df_titanic_data = pd.concat([df_titanic_data, pd.get_dummies(df_titanic_data['Embarked']).rename(
            columns=lambda x: 'Embarked_' + str(x))], axis=1)

因子化

这种方法用于从其他特征创建一个数值型分类特征。在 pandas 中,factorize()函数可以做到这一点。如果你的特征是字母数字的分类变量,那么这种转换就非常有用。在泰坦尼克号数据中,我们可以将Cabin特征转换为分类特征,表示舱位的字母:

# the cabin number is a sequence of of alphanumerical digits, so we are going to create some features
# from the alphabetical part of it
df_titanic_data['CabinLetter'] = df_titanic_data['Cabin'].map(lambda l: get_cabin_letter(l))
df_titanic_data['CabinLetter'] = pd.factorize(df_titanic_data['CabinLetter'])[0]
def get_cabin_letter(cabin_value):
    # searching for the letters in the cabin alphanumerical value
    letter_match = re.compile("([a-zA-Z]+)").search(cabin_value)

    if letter_match:
        return letter_match.group()
    else:
        return 'U'

我们还可以通过以下方法对定量特征应用转换。

缩放

这种转换仅适用于数值型特征。

例如,在泰坦尼克号数据中,Age特征的值可能达到 100,而家庭收入可能达到百万级别。一些模型对数值的大小比较敏感,因此对这些特征进行缩放可以帮助模型表现得更好。此外,缩放也可以将变量的值压缩到一个特定的范围内。

以下代码将通过从每个值中减去其均值并将其缩放到单位方差来缩放Age特征:

# scale by subtracting the mean from each value
scaler_processing = preprocessing.StandardScaler()
df_titanic_data['Age_scaled'] = scaler_processing.fit_transform(df_titanic_data['Age'])

分箱

这种量化转换用于创建分位数。在这种情况下,量化特征值将是转换后的有序变量。这种方法不适用于线性回归,但可能在使用有序/类别变量时,学习算法能有效地响应。

以下代码对Fare特征应用了这种转换:

# Binarizing the features by binning them into quantiles
df_titanic_data['Fare_bin'] = pd.qcut(df_titanic_data['Fare'], 4)

if keep_binary:
    df_titanic_data = pd.concat(
        [df_titanic_data, pd.get_dummies(df_titanic_data['Fare_bin']).rename(columns=lambda x: 'Fare_' + str(x))],
        axis=1)

派生特征

在上一节中,我们对泰坦尼克号数据应用了一些转换,以便能够使用 scikit-learn 的随机森林分类器(该分类器只接受数值数据)。在本节中,我们将定义另一种变量类型,它是由一个或多个其他特征衍生出来的。

在这个定义下,我们可以说上一节中的一些转换也叫做派生特征。在本节中,我们将研究其他复杂的转换。

在前几节中,我们提到你需要运用特征工程技能来衍生新特征,以增强模型的预测能力。我们也谈到了特征工程在数据科学流程中的重要性,以及为什么你应该花费大部分时间和精力来提出有用的特征。在这一节中,领域知识将非常有帮助。

非常简单的派生特征的例子包括从电话号码中提取国家代码和/或地区代码。你还可以从 GPS 坐标中提取国家/地区信息。

泰坦尼克号数据集非常简单,包含的变量不多,但我们可以尝试从文本特征中推导出一些新特征。

姓名

name变量本身对大多数数据集来说是没有用的,但它有两个有用的属性。第一个是名字的长度。例如,名字的长度可能反映你的地位,从而影响你上救生艇的机会:

# getting the different names in the names variable
df_titanic_data['Names'] = df_titanic_data['Name'].map(lambda y: len(re.split(' ', y)))

第二个有趣的属性是Name标题,它也可以用来表示地位和/或性别:

# Getting titles for each person
df_titanic_data['Title'] = df_titanic_data['Name'].map(lambda y: re.compile(", (.*?)\.").findall(y)[0])

# handling the low occurring titles
df_titanic_data['Title'][df_titanic_data.Title == 'Jonkheer'] = 'Master'
df_titanic_data['Title'][df_titanic_data.Title.isin(['Ms', 'Mlle'])] = 'Miss'
df_titanic_data['Title'][df_titanic_data.Title == 'Mme'] = 'Mrs'
df_titanic_data['Title'][df_titanic_data.Title.isin(['Capt', 'Don', 'Major', 'Col', 'Sir'])] = 'Sir'
df_titanic_data['Title'][df_titanic_data.Title.isin(['Dona', 'Lady', 'the Countess'])] = 'Lady'

# binarizing all the features
if keep_binary:
    df_titanic_data = pd.concat(
        [df_titanic_data, pd.get_dummies(df_titanic_data['Title']).rename(columns=lambda x: 'Title_' + str(x))],
        axis=1)

你也可以尝试从Name特征中提出其他有趣的特征。例如,你可以使用姓氏特征来找出泰坦尼克号上家族成员的规模。

舱位

在泰坦尼克号数据中,Cabin特征由一个字母表示甲板,和一个数字表示房间号。房间号随着船的后部增加,这将提供乘客位置的有用信息。我们还可以通过不同甲板上的乘客状态,帮助判断谁可以上救生艇:

# repllacing the missing value in cabin variable "U0"
df_titanic_data['Cabin'][df_titanic_data.Cabin.isnull()] = 'U0'

# the cabin number is a sequence of of alphanumerical digits, so we are going to create some features
# from the alphabetical part of it
df_titanic_data['CabinLetter'] = df_titanic_data['Cabin'].map(lambda l: get_cabin_letter(l))
df_titanic_data['CabinLetter'] = pd.factorize(df_titanic_data['CabinLetter'])[0]

# binarizing the cabin letters features
if keep_binary:
    cletters = pd.get_dummies(df_titanic_data['CabinLetter']).rename(columns=lambda x: 'CabinLetter_' + str(x))
    df_titanic_data = pd.concat([df_titanic_data, cletters], axis=1)

# creating features from the numerical side of the cabin
df_titanic_data['CabinNumber'] = df_titanic_data['Cabin'].map(lambda x: get_cabin_num(x)).astype(int) + 1

Ticket特征的代码并不一目了然,但我们可以做一些猜测并尝试将它们分组。查看Ticket特征后,你可能会得到以下线索:

  • 几乎四分之一的票以字符开头,而其余的票则仅由数字组成。

  • 车票代码中的数字部分似乎能提供一些关于乘客等级的提示。例如,以 1 开头的数字通常是头等舱票,2 通常是二等舱,3 是三等舱。我说通常是因为这适用于大多数情况,但并非所有情况。也有以 4-9 开头的票号,这些票号很少见,几乎完全是三等舱。

  • 几个人可以共享一个车票号码,这可能表示一家人或亲密的朋友一起旅行,并像一家人一样行动。

以下代码尝试分析车票特征代码,以得出前述提示:

# Helper function for constructing features from the ticket variable
def process_ticket():
    global df_titanic_data

    df_titanic_data['TicketPrefix'] = df_titanic_data['Ticket'].map(lambda y: get_ticket_prefix(y.upper()))
    df_titanic_data['TicketPrefix'] = df_titanic_data['TicketPrefix'].map(lambda y: re.sub('[\.?\/?]', '', y))
    df_titanic_data['TicketPrefix'] = df_titanic_data['TicketPrefix'].map(lambda y: re.sub('STON', 'SOTON', y))

    df_titanic_data['TicketPrefixId'] = pd.factorize(df_titanic_data['TicketPrefix'])[0]

    # binarzing features for each ticket layer
    if keep_binary:
        prefixes = pd.get_dummies(df_titanic_data['TicketPrefix']).rename(columns=lambda y: 'TicketPrefix_' + str(y))
        df_titanic_data = pd.concat([df_titanic_data, prefixes], axis=1)

    df_titanic_data.drop(['TicketPrefix'], axis=1, inplace=True)

    df_titanic_data['TicketNumber'] = df_titanic_data['Ticket'].map(lambda y: get_ticket_num(y))
    df_titanic_data['TicketNumberDigits'] = df_titanic_data['TicketNumber'].map(lambda y: len(y)).astype(np.int)
    df_titanic_data['TicketNumberStart'] = df_titanic_data['TicketNumber'].map(lambda y: y[0:1]).astype(np.int)

    df_titanic_data['TicketNumber'] = df_titanic_data.TicketNumber.astype(np.int)

    if keep_scaled:
        scaler_processing = preprocessing.StandardScaler()
        df_titanic_data['TicketNumber_scaled'] = scaler_processing.fit_transform(
            df_titanic_data.TicketNumber.reshape(-1, 1))

def get_ticket_prefix(ticket_value):
    # searching for the letters in the ticket alphanumerical value
    match_letter = re.compile("([a-zA-Z\.\/]+)").search(ticket_value)
    if match_letter:
        return match_letter.group()
    else:
        return 'U'

def get_ticket_num(ticket_value):
    # searching for the numbers in the ticket alphanumerical value
    match_number = re.compile("([\d]+$)").search(ticket_value)
    if match_number:
        return match_number.group()
    else:
        return '0'

交互特征

交互特征是通过对一组特征执行数学运算得到的,表示变量之间关系的影响。我们对数值特征进行基本的数学运算,观察变量之间关系的效果:

# Constructing features manually based on  the interaction between the individual features
numeric_features = df_titanic_data.loc[:,
                   ['Age_scaled', 'Fare_scaled', 'Pclass_scaled', 'Parch_scaled', 'SibSp_scaled',
                    'Names_scaled', 'CabinNumber_scaled', 'Age_bin_id_scaled', 'Fare_bin_id_scaled']]
print("\nUsing only numeric features for automated feature generation:\n", numeric_features.head(10))

new_fields_count = 0
for i in range(0, numeric_features.columns.size - 1):
    for j in range(0, numeric_features.columns.size - 1):
        if i <= j:
            name = str(numeric_features.columns.values[i]) + "*" + str(numeric_features.columns.values[j])
            df_titanic_data = pd.concat(
                [df_titanic_data, pd.Series(numeric_features.iloc[:, i] * numeric_features.iloc[:, j], name=name)],
                axis=1)
            new_fields_count += 1
        if i < j:
            name = str(numeric_features.columns.values[i]) + "+" + str(numeric_features.columns.values[j])
            df_titanic_data = pd.concat(
                [df_titanic_data, pd.Series(numeric_features.iloc[:, i] + numeric_features.iloc[:, j], name=name)],
                axis=1)
            new_fields_count += 1
        if not i == j:
            name = str(numeric_features.columns.values[i]) + "/" + str(numeric_features.columns.values[j])
            df_titanic_data = pd.concat(
                [df_titanic_data, pd.Series(numeric_features.iloc[:, i] / numeric_features.iloc[:, j], name=name)],
                axis=1)
            name = str(numeric_features.columns.values[i]) + "-" + str(numeric_features.columns.values[j])
            df_titanic_data = pd.concat(
                [df_titanic_data, pd.Series(numeric_features.iloc[:, i] - numeric_features.iloc[:, j], name=name)],
                axis=1)
            new_fields_count += 2

print("\n", new_fields_count, "new features constructed")

这种特征工程可以生成大量特征。在前面的代码片段中,我们使用了 9 个特征来生成 176 个交互特征。

我们还可以去除高度相关的特征,因为这些特征的存在不会为模型提供任何额外的信息。我们可以使用斯皮尔曼相关系数来识别和去除高度相关的特征。斯皮尔曼方法的输出中有一个秩系数,可以用来识别高度相关的特征:

# using Spearman correlation method to remove the feature that have high correlation

# calculating the correlation matrix
df_titanic_data_cor = df_titanic_data.drop(['Survived', 'PassengerId'], axis=1).corr(method='spearman')

# creating a mask that will ignore correlated ones
mask_ignore = np.ones(df_titanic_data_cor.columns.size) - np.eye(df_titanic_data_cor.columns.size)
df_titanic_data_cor = mask_ignore * df_titanic_data_cor

features_to_drop = []

# dropping the correclated features
for column in df_titanic_data_cor.columns.values:

    # check if we already decided to drop this variable
    if np.in1d([column], features_to_drop):
        continue

    # finding highly correlacted variables
    corr_vars = df_titanic_data_cor[abs(df_titanic_data_cor[column]) > 0.98].index
    features_to_drop = np.union1d(features_to_drop, corr_vars)

print("\nWe are going to drop", features_to_drop.shape[0], " which are highly correlated features...\n")
df_titanic_data.drop(features_to_drop, axis=1, inplace=True)

高维灾难

为了更好地解释高维灾难和过拟合问题,我们将通过一个示例来说明,其中我们有一组图像。每张图像中都有一只猫或一只狗。所以,我们想建立一个模型,能够区分包含猫和包含狗的图像。就像在第一章中提到的鱼类识别系统,数据科学 - 鸟瞰图,我们需要找到一个可以被学习算法用来区分这两类(猫和狗)的解释性特征。在这个示例中,我们可以认为颜色是一个很好的描述符,用来区分猫和狗。所以,平均红色、平均蓝色和平均绿色的颜色可以作为解释性特征,用来区分这两类。

算法随后将以某种方式结合这三个特征,形成两个类别之间的决策边界。

三个特征的简单线性组合可能类似于以下形式:

If 0.5*red + 0.3*green + 0.2*blue > 0.6 : return cat;
else return dog;

这些描述性特征不足以获得一个性能良好的分类器,因此我们可以决定添加更多的特征,以增强模型的预测能力,从而区分猫和狗。例如,我们可以考虑通过计算图像在 X 和 Y 两个维度上的平均边缘或梯度强度来添加图像的纹理特征。添加这两个特征后,模型的准确性将得到提升。我们甚至可以通过添加越来越多的基于颜色、纹理直方图、统计矩等特征,进一步提高模型的分类能力。我们可以轻松地添加几百个这些特征来增强模型的预测能力。但反直觉的结果是,当特征数量超过某个限度时,模型的性能反而会变差。你可以通过查看图 1来更好地理解这一点:

图 1:模型性能与特征数量的关系

图 1 显示了随着特征数量的增加,分类器的性能也在提升,直到我们达到最优特征数量。基于相同大小的训练集添加更多特征将会降低分类器的性能。

避免维度灾难

在前面的部分中,我们展示了当特征数量超过某个最优点时,分类器的性能将会下降。理论上,如果你有无限的训练样本,维度灾难将不存在。所以,最优的特征数量完全依赖于你的数据大小。

避免这一“诅咒”的一种方法是从大量特征 N 中子集选择 M 个特征,其中 M << N。每个 M 中的特征可以是 N 中一些特征的组合。有一些算法可以为你完成这项工作。这些算法通过某种方式尝试找到原始 N 特征的有用、无相关的线性组合。一个常用的技术是主成分分析PCA)。PCA 试图找到较少数量的特征,这些特征能够捕捉原始数据的最大方差。你可以在这个有趣的博客中找到更多的见解和完整的 PCA 解释:www.visiondummy.com/2014/05/feature-extraction-using-pca/

一种简单实用的方法来对原始训练特征应用 PCA 是使用以下代码:

# minimum variance percentage that should be covered by the reduced number of variables
variance_percentage = .99

# creating PCA object
pca_object = PCA(n_components=variance_percentage)

# trasforming the features
input_values_transformed = pca_object.fit_transform(input_values, target_values)

# creating a datafram for the transformed variables from PCA
pca_df = pd.DataFrame(input_values_transformed)

print(pca_df.shape[1], " reduced components which describe ", str(variance_percentage)[1:], "% of the variance")

在泰坦尼克号的示例中,我们尝试在原始特征上应用与不应用 PCA 来构建分类器。由于我们最终使用的是随机森林分类器,我们发现应用 PCA 并不是非常有帮助;随机森林在没有任何特征转换的情况下也能很好地工作,甚至相关的特征对模型的影响也不大。

泰坦尼克号示例回顾——整合在一起

在本节中,我们将把特征工程和维度减少的各个部分结合起来:

import re
import numpy as np
import pandas as pd
import random as rd
from sklearn import preprocessing
from sklearn.cluster import KMeans
from sklearn.ensemble import RandomForestRegressor
from sklearn.decomposition import PCA

# Print options
np.set_printoptions(precision=4, threshold=10000, linewidth=160, edgeitems=999, suppress=True)
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', None)
pd.set_option('display.width', 160)
pd.set_option('expand_frame_repr', False)
pd.set_option('precision', 4)

# constructing binary features
def process_embarked():
    global df_titanic_data

    # replacing the missing values with the most common value in the variable
    df_titanic_data.Embarked[df.Embarked.isnull()] = df_titanic_data.Embarked.dropna().mode().values

    # converting the values into numbers
    df_titanic_data['Embarked'] = pd.factorize(df_titanic_data['Embarked'])[0]

    # binarizing the constructed features
    if keep_binary:
        df_titanic_data = pd.concat([df_titanic_data, pd.get_dummies(df_titanic_data['Embarked']).rename(
            columns=lambda x: 'Embarked_' + str(x))], axis=1)

# Define a helper function that can use RandomForestClassifier for handling the missing values of the age variable
def set_missing_ages():
    global df_titanic_data

    age_data = df_titanic_data[
        ['Age', 'Embarked', 'Fare', 'Parch', 'SibSp', 'Title_id', 'Pclass', 'Names', 'CabinLetter']]
    input_values_RF = age_data.loc[(df_titanic_data.Age.notnull())].values[:, 1::]
    target_values_RF = age_data.loc[(df_titanic_data.Age.notnull())].values[:, 0]

    # Creating an object from the random forest regression function of sklearn<use the documentation for more details>
    regressor = RandomForestRegressor(n_estimators=2000, n_jobs=-1)

    # building the model based on the input values and target values above
    regressor.fit(input_values_RF, target_values_RF)

    # using the trained model to predict the missing values
    predicted_ages = regressor.predict(age_data.loc[(df_titanic_data.Age.isnull())].values[:, 1::])

    # Filling the predicted ages in the original titanic dataframe
    age_data.loc[(age_data.Age.isnull()), 'Age'] = predicted_ages

# Helper function for constructing features from the age variable
def process_age():
    global df_titanic_data

    # calling the set_missing_ages helper function to use random forest regression for predicting missing values of age
    set_missing_ages()

    # # scale the age variable by centering it around the mean with a unit variance
    # if keep_scaled:
    # scaler_preprocessing = preprocessing.StandardScaler()
    # df_titanic_data['Age_scaled'] = scaler_preprocessing.fit_transform(df_titanic_data.Age.reshape(-1, 1))

    # construct a feature for children
    df_titanic_data['isChild'] = np.where(df_titanic_data.Age < 13, 1, 0)

    # bin into quartiles and create binary features
    df_titanic_data['Age_bin'] = pd.qcut(df_titanic_data['Age'], 4)

    if keep_binary:
        df_titanic_data = pd.concat(
            [df_titanic_data, pd.get_dummies(df_titanic_data['Age_bin']).rename(columns=lambda y: 'Age_' + str(y))],
            axis=1)

    if keep_bins:
        df_titanic_data['Age_bin_id'] = pd.factorize(df_titanic_data['Age_bin'])[0] + 1

    if keep_bins and keep_scaled:
        scaler_processing = preprocessing.StandardScaler()
        df_titanic_data['Age_bin_id_scaled'] = scaler_processing.fit_transform(
            df_titanic_data.Age_bin_id.reshape(-1, 1))

    if not keep_strings:
        df_titanic_data.drop('Age_bin', axis=1, inplace=True)

# Helper function for constructing features from the passengers/crew names
def process_name():
    global df_titanic_data

    # getting the different names in the names variable
    df_titanic_data['Names'] = df_titanic_data['Name'].map(lambda y: len(re.split(' ', y)))

    # Getting titles for each person
    df_titanic_data['Title'] = df_titanic_data['Name'].map(lambda y: re.compile(", (.*?)\.").findall(y)[0])

    # handling the low occurring titles
    df_titanic_data['Title'][df_titanic_data.Title == 'Jonkheer'] = 'Master'
    df_titanic_data['Title'][df_titanic_data.Title.isin(['Ms', 'Mlle'])] = 'Miss'
    df_titanic_data['Title'][df_titanic_data.Title == 'Mme'] = 'Mrs'
    df_titanic_data['Title'][df_titanic_data.Title.isin(['Capt', 'Don', 'Major', 'Col', 'Sir'])] = 'Sir'
    df_titanic_data['Title'][df_titanic_data.Title.isin(['Dona', 'Lady', 'the Countess'])] = 'Lady'

    # binarizing all the features
    if keep_binary:
        df_titanic_data = pd.concat(
            [df_titanic_data, pd.get_dummies(df_titanic_data['Title']).rename(columns=lambda x: 'Title_' + str(x))],
            axis=1)

    # scaling
    if keep_scaled:
        scaler_preprocessing = preprocessing.StandardScaler()
        df_titanic_data['Names_scaled'] = scaler_preprocessing.fit_transform(df_titanic_data.Names.reshape(-1, 1))

    # binning
    if keep_bins:
        df_titanic_data['Title_id'] = pd.factorize(df_titanic_data['Title'])[0] + 1

    if keep_bins and keep_scaled:
        scaler = preprocessing.StandardScaler()
        df_titanic_data['Title_id_scaled'] = scaler.fit_transform(df_titanic_data.Title_id.reshape(-1, 1))

# Generate features from the cabin input variable
def process_cabin():
    # refering to the global variable that contains the titanic examples
    global df_titanic_data

    # repllacing the missing value in cabin variable "U0"
    df_titanic_data['Cabin'][df_titanic_data.Cabin.isnull()] = 'U0'

    # the cabin number is a sequence of of alphanumerical digits, so we are going to create some features
    # from the alphabetical part of it
    df_titanic_data['CabinLetter'] = df_titanic_data['Cabin'].map(lambda l: get_cabin_letter(l))
    df_titanic_data['CabinLetter'] = pd.factorize(df_titanic_data['CabinLetter'])[0]

    # binarizing the cabin letters features
    if keep_binary:
        cletters = pd.get_dummies(df_titanic_data['CabinLetter']).rename(columns=lambda x: 'CabinLetter_' + str(x))
        df_titanic_data = pd.concat([df_titanic_data, cletters], axis=1)

    # creating features from the numerical side of the cabin
    df_titanic_data['CabinNumber'] = df_titanic_data['Cabin'].map(lambda x: get_cabin_num(x)).astype(int) + 1

    # scaling the feature
    if keep_scaled:
        scaler_processing = preprocessing.StandardScaler() # handling the missing values by replacing it with the median feare
    df_titanic_data['Fare'][np.isnan(df_titanic_data['Fare'])] = df_titanic_data['Fare'].median()
    df_titanic_data['CabinNumber_scaled'] = scaler_processing.fit_transform(df_titanic_data.CabinNumber.reshape(-1, 1))

def get_cabin_letter(cabin_value):
    # searching for the letters in the cabin alphanumerical value
    letter_match = re.compile("([a-zA-Z]+)").search(cabin_value)

    if letter_match:
        return letter_match.group()
    else:
        return 'U'

def get_cabin_num(cabin_value):
    # searching for the numbers in the cabin alphanumerical value
    number_match = re.compile("([0-9]+)").search(cabin_value)

    if number_match:
        return number_match.group()
    else:
        return 0

# helper function for constructing features from the ticket fare variable
def process_fare():
    global df_titanic_data

    # handling the missing values by replacing it with the median feare
    df_titanic_data['Fare'][np.isnan(df_titanic_data['Fare'])] = df_titanic_data['Fare'].median()

    # zeros in the fare will cause some division problems so we are going to set them to 1/10th of the lowest fare
    df_titanic_data['Fare'][np.where(df_titanic_data['Fare'] == 0)[0]] = df_titanic_data['Fare'][
                                                                             df_titanic_data['Fare'].nonzero()[
                                                                                 0]].min() / 10

    # Binarizing the features by binning them into quantiles
    df_titanic_data['Fare_bin'] = pd.qcut(df_titanic_data['Fare'], 4)

    if keep_binary:
        df_titanic_data = pd.concat(
            [df_titanic_data, pd.get_dummies(df_titanic_data['Fare_bin']).rename(columns=lambda x: 'Fare_' + str(x))],
            axis=1)

    # binning
    if keep_bins:
        df_titanic_data['Fare_bin_id'] = pd.factorize(df_titanic_data['Fare_bin'])[0] + 1

    # scaling the value
    if keep_scaled:
        scaler_processing = preprocessing.StandardScaler()
        df_titanic_data['Fare_scaled'] = scaler_processing.fit_transform(df_titanic_data.Fare.reshape(-1, 1))

    if keep_bins and keep_scaled:
        scaler_processing = preprocessing.StandardScaler()
        df_titanic_data['Fare_bin_id_scaled'] = scaler_processing.fit_transform(
            df_titanic_data.Fare_bin_id.reshape(-1, 1))

    if not keep_strings:
        df_titanic_data.drop('Fare_bin', axis=1, inplace=True)

# Helper function for constructing features from the ticket variable
def process_ticket():
    global df_titanic_data

    df_titanic_data['TicketPrefix'] = df_titanic_data['Ticket'].map(lambda y: get_ticket_prefix(y.upper()))
    df_titanic_data['TicketPrefix'] = df_titanic_data['TicketPrefix'].map(lambda y: re.sub('[\.?\/?]', '', y))
    df_titanic_data['TicketPrefix'] = df_titanic_data['TicketPrefix'].map(lambda y: re.sub('STON', 'SOTON', y))

    df_titanic_data['TicketPrefixId'] = pd.factorize(df_titanic_data['TicketPrefix'])[0]

    # binarzing features for each ticket layer
    if keep_binary:
        prefixes = pd.get_dummies(df_titanic_data['TicketPrefix']).rename(columns=lambda y: 'TicketPrefix_' + str(y))
        df_titanic_data = pd.concat([df_titanic_data, prefixes], axis=1)

    df_titanic_data.drop(['TicketPrefix'], axis=1, inplace=True)

    df_titanic_data['TicketNumber'] = df_titanic_data['Ticket'].map(lambda y: get_ticket_num(y))
    df_titanic_data['TicketNumberDigits'] = df_titanic_data['TicketNumber'].map(lambda y: len(y)).astype(np.int)
    df_titanic_data['TicketNumberStart'] = df_titanic_data['TicketNumber'].map(lambda y: y[0:1]).astype(np.int)

    df_titanic_data['TicketNumber'] = df_titanic_data.TicketNumber.astype(np.int)

    if keep_scaled:
        scaler_processing = preprocessing.StandardScaler()
        df_titanic_data['TicketNumber_scaled'] = scaler_processing.fit_transform(
            df_titanic_data.TicketNumber.reshape(-1, 1))

def get_ticket_prefix(ticket_value):
    # searching for the letters in the ticket alphanumerical value
    match_letter = re.compile("([a-zA-Z\.\/]+)").search(ticket_value)
    if match_letter:
        return match_letter.group()
    else:
        return 'U'

def get_ticket_num(ticket_value):
    # searching for the numbers in the ticket alphanumerical value
    match_number = re.compile("([\d]+$)").search(ticket_value)
    if match_number:
        return match_number.group()
    else:
        return '0'

# constructing features from the passenger class variable
def process_PClass():
    global df_titanic_data

    # using the most frequent value(mode) to replace the messing value
    df_titanic_data.Pclass[df_titanic_data.Pclass.isnull()] = df_titanic_data.Pclass.dropna().mode().values

    # binarizing the features
    if keep_binary:
        df_titanic_data = pd.concat(
            [df_titanic_data, pd.get_dummies(df_titanic_data['Pclass']).rename(columns=lambda y: 'Pclass_' + str(y))],
            axis=1)

    if keep_scaled:
        scaler_preprocessing = preprocessing.StandardScaler()
        df_titanic_data['Pclass_scaled'] = scaler_preprocessing.fit_transform(df_titanic_data.Pclass.reshape(-1, 1))

# constructing features based on the family variables subh as SibSp and Parch
def process_family():
    global df_titanic_data

    # ensuring that there's no zeros to use interaction variables
    df_titanic_data['SibSp'] = df_titanic_data['SibSp'] + 1
    df_titanic_data['Parch'] = df_titanic_data['Parch'] + 1

    # scaling
    if keep_scaled:
        scaler_preprocessing = preprocessing.StandardScaler()
        df_titanic_data['SibSp_scaled'] = scaler_preprocessing.fit_transform(df_titanic_data.SibSp.reshape(-1, 1))
        df_titanic_data['Parch_scaled'] = scaler_preprocessing.fit_transform(df_titanic_data.Parch.reshape(-1, 1))

    # binarizing all the features
    if keep_binary:
        sibsps_var = pd.get_dummies(df_titanic_data['SibSp']).rename(columns=lambda y: 'SibSp_' + str(y))
        parchs_var = pd.get_dummies(df_titanic_data['Parch']).rename(columns=lambda y: 'Parch_' + str(y))
        df_titanic_data = pd.concat([df_titanic_data, sibsps_var, parchs_var], axis=1)

# binarzing the sex variable
def process_sex():
    global df_titanic_data
    df_titanic_data['Gender'] = np.where(df_titanic_data['Sex'] == 'male', 1, 0)

# dropping raw original
def process_drops():
    global df_titanic_data
    drops = ['Name', 'Names', 'Title', 'Sex', 'SibSp', 'Parch', 'Pclass', 'Embarked', \
             'Cabin', 'CabinLetter', 'CabinNumber', 'Age', 'Fare', 'Ticket', 'TicketNumber']
    string_drops = ['Title', 'Name', 'Cabin', 'Ticket', 'Sex', 'Ticket', 'TicketNumber']
    if not keep_raw:
        df_titanic_data.drop(drops, axis=1, inplace=True)
    elif not keep_strings:
        df_titanic_data.drop(string_drops, axis=1, inplace=True)

# handling all the feature engineering tasks
def get_titanic_dataset(binary=False, bins=False, scaled=False, strings=False, raw=True, pca=False, balanced=False):
    global keep_binary, keep_bins, keep_scaled, keep_raw, keep_strings, df_titanic_data
    keep_binary = binary
    keep_bins = bins
    keep_scaled = scaled
    keep_raw = raw
    keep_strings = strings

    # reading the train and test sets using pandas
    train_data = pd.read_csv('data/train.csv', header=0)
    test_data = pd.read_csv('data/test.csv', header=0)

    # concatenate the train and test set together for doing the overall feature engineering stuff
    df_titanic_data = pd.concat([train_data, test_data])

    # removing duplicate indices due to coming the train and test set by re-indexing the data
    df_titanic_data.reset_index(inplace=True)

    # removing the index column the reset_index() function generates
    df_titanic_data.drop('index', axis=1, inplace=True)

    # index the columns to be 1-based index
    df_titanic_data = df_titanic_data.reindex_axis(train_data.columns, axis=1)

    # processing the titanic raw variables using the helper functions that we defined above
    process_cabin()
    process_ticket()
    process_name()
    process_fare()
    process_embarked()
    process_family()
    process_sex()
    process_PClass()
    process_age()
    process_drops()

    # move the survived column to be the first
    columns_list = list(df_titanic_data.columns.values)
    columns_list.remove('Survived')
    new_col_list = list(['Survived'])
    new_col_list.extend(columns_list)
    df_titanic_data = df_titanic_data.reindex(columns=new_col_list)

    print("Starting with", df_titanic_data.columns.size,
          "manually constructing features based on the interaction between them...\n", df_titanic_data.columns.values)

    # Constructing features manually based on the interaction between the individual features
    numeric_features = df_titanic_data.loc[:,
                       ['Age_scaled', 'Fare_scaled', 'Pclass_scaled', 'Parch_scaled', 'SibSp_scaled',
                        'Names_scaled', 'CabinNumber_scaled', 'Age_bin_id_scaled', 'Fare_bin_id_scaled']]
    print("\nUsing only numeric features for automated feature generation:\n", numeric_features.head(10))

    new_fields_count = 0
    for i in range(0, numeric_features.columns.size - 1):
        for j in range(0, numeric_features.columns.size - 1):
            if i <= j:
                name = str(numeric_features.columns.values[i]) + "*" + str(numeric_features.columns.values[j])
                df_titanic_data = pd.concat(
                    [df_titanic_data, pd.Series(numeric_features.iloc[:, i] * numeric_features.iloc[:, j], name=name)],
                    axis=1)
                new_fields_count += 1
            if i < j:
                name = str(numeric_features.columns.values[i]) + "+" + str(numeric_features.columns.values[j])
                df_titanic_data = pd.concat(
                    [df_titanic_data, pd.Series(numeric_features.iloc[:, i] + numeric_features.iloc[:, j], name=name)],
                    axis=1)
                new_fields_count += 1
            if not i == j:
                name = str(numeric_features.columns.values[i]) + "/" + str(numeric_features.columns.values[j])
                df_titanic_data = pd.concat(
                    [df_titanic_data, pd.Series(numeric_features.iloc[:, i] / numeric_features.iloc[:, j], name=name)],
                    axis=1)
                name = str(numeric_features.columns.values[i]) + "-" + str(numeric_features.columns.values[j])
                df_titanic_data = pd.concat(
                    [df_titanic_data, pd.Series(numeric_features.iloc[:, i] - numeric_features.iloc[:, j], name=name)],
                    axis=1)
                new_fields_count += 2

    print("\n", new_fields_count, "new features constructed")

    # using Spearman correlation method to remove the feature that have high correlation

    # calculating the correlation matrix
    df_titanic_data_cor = df_titanic_data.drop(['Survived', 'PassengerId'], axis=1).corr(method='spearman')

    # creating a mask that will ignore correlated ones
    mask_ignore = np.ones(df_titanic_data_cor.columns.size) - np.eye(df_titanic_data_cor.columns.size)
    df_titanic_data_cor = mask_ignore * df_titanic_data_cor

    features_to_drop = []

    # dropping the correclated features
    for column in df_titanic_data_cor.columns.values:

        # check if we already decided to drop this variable
        if np.in1d([column], features_to_drop):
            continue

        # finding highly correlacted variables
        corr_vars = df_titanic_data_cor[abs(df_titanic_data_cor[column]) > 0.98].index
        features_to_drop = np.union1d(features_to_drop, corr_vars)

    print("\nWe are going to drop", features_to_drop.shape[0], " which are highly correlated features...\n")
    df_titanic_data.drop(features_to_drop, axis=1, inplace=True)

    # splitting the dataset to train and test and do PCA
    train_data = df_titanic_data[:train_data.shape[0]]
    test_data = df_titanic_data[test_data.shape[0]:]

    if pca:
        print("reducing number of variables...")
        train_data, test_data = reduce(train_data, test_data)
    else:
        # drop the empty 'Survived' column for the test set that was created during set concatenation
        test_data.drop('Survived', axis=1, inplace=True)

    print("\n", train_data.columns.size, "initial features generated...\n") # , input_df.columns.values

    return train_data, test_data

# reducing the dimensionality for the training and testing set
def reduce(train_data, test_data):
    # join the full data together
    df_titanic_data = pd.concat([train_data, test_data])
    df_titanic_data.reset_index(inplace=True)
    df_titanic_data.drop('index', axis=1, inplace=True)
    df_titanic_data = df_titanic_data.reindex_axis(train_data.columns, axis=1)

    # converting the survived column to series
    survived_series = pd.Series(df_titanic_data['Survived'], name='Survived')

    print(df_titanic_data.head())

    # getting the input and target values
    input_values = df_titanic_data.values[:, 1::]
    target_values = df_titanic_data.values[:, 0]

    print(input_values[0:10])

    # minimum variance percentage that should be covered by the reduced number of variables
    variance_percentage = .99

    # creating PCA object
    pca_object = PCA(n_components=variance_percentage)

    # trasforming the features
    input_values_transformed = pca_object.fit_transform(input_values, target_values)

    # creating a datafram for the transformed variables from PCA
    pca_df = pd.DataFrame(input_values_transformed)

    print(pca_df.shape[1], " reduced components which describe ", str(variance_percentage)[1:], "% of the variance")

    # constructing a new dataframe that contains the newly reduced vars of PCA
    df_titanic_data = pd.concat([survived_series, pca_df], axis=1)

    # split into separate input and test sets again
    train_data = df_titanic_data[:train_data.shape[0]]
    test_data = df_titanic_data[test_data.shape[0]:]
    test_data.reset_index(inplace=True)
    test_data.drop('index', axis=1, inplace=True)
    test_data.drop('Survived', axis=1, inplace=True)

    return train_data, test_data

# Calling the helper functions
if __name__ == '__main__':
    train, test = get_titanic_dataset(bins=True, scaled=True, binary=True)
    initial_drops = ['PassengerId']
    train.drop(initial_drops, axis=1, inplace=True)
    test.drop(initial_drops, axis=1, inplace=True)

    train, test = reduce(train, test)

    print(train.columns.values)

偏差-方差分解

在上一节中,我们了解了如何为模型选择最佳超参数。这个最佳超参数集是基于最小化交叉验证误差的度量来选择的。现在,我们需要看看模型在未见过的数据上的表现,或者所谓的外部样本数据,指的是在模型训练阶段未曾见过的新数据样本。

以以下示例为例:我们有一个大小为 10,000 的数据样本,我们将使用不同的训练集大小训练相同的模型,并在每一步绘制测试误差。例如,我们将取出 1,000 作为测试集,剩余的 9,000 用于训练。那么,在第一次训练时,我们将从这 9,000 个样本中随机选择 100 个作为训练集。我们将基于最佳选择的超参数集来训练模型,使用测试集进行测试,最后绘制训练(内部样本)误差和测试(外部样本)误差。我们会为不同的训练集大小重复这个训练、测试和绘图操作(例如,使用 9,000 中的 500 个,然后是 1,000 个,依此类推)。

在进行所有这些训练、测试和绘图后,我们将得到一个包含两条曲线的图,表示相同模型在不同训练集大小下的训练误差和测试误差。从这张图中,我们可以了解模型的表现如何。

输出图表将包含两条曲线,分别表示训练误差和测试误差,图表的形状可能是图 2中展示的四种可能形状之一。这个不同的形状来源于 Andrew Ng 在 Coursera 上的机器学习课程(www.coursera.org/learn/machine-learning)。这是一门非常棒的课程,适合机器学习初学者,课程内容充满了洞见和最佳实践:

图 2:在不同训练集大小下绘制训练误差和测试误差的可能形状

那么,我们什么时候应该接受我们的模型并将其投入生产?我们又如何知道模型在测试集上表现不佳,从而不会产生较差的泛化误差呢?这些问题的答案取决于你从不同训练集大小中绘制训练误差与测试误差的图形所得到的形状:

  • 如果你的图形类似于左上的那种,它代表了较低的训练误差,并且在测试集上有很好的泛化性能。这个形状是一个赢家,你应该继续使用这个模型并将其投入生产。

  • 如果你的图形类似于右上的那种,它代表了较高的训练误差(模型未能从训练样本中学习)并且在测试集上的泛化性能甚至更差。这个形状是完全失败的,你需要回过头来检查你的数据、选择的学习算法和/或选择的超参数是否存在问题。

  • 如果你的形状类似于左下方的那个,它代表了一个较差的训练误差,因为模型没有成功地捕捉到数据的潜在结构,而这种结构也适用于新的测试数据。

  • 如果你的形状类似于右下方的那个,它代表了高偏差和高方差。这意味着你的模型没有很好地学习训练数据,因此无法很好地泛化到测试集上。

偏差和方差是我们可以用来判断模型好坏的组成部分。在监督学习中,存在两个相互对立的误差来源,通过使用图 2中的学习曲线,我们可以找出我们的模型是由于哪个(哪些)组成部分受到了影响。高方差和低偏差的问题叫做过拟合,这意味着模型在训练样本上表现良好,但在测试集上泛化不佳。另一方面,高偏差和低方差的问题叫做欠拟合,这意味着模型没有充分利用数据,也没能从输入特征中估计出输出/目标。我们可以采取不同的方法来避免这些问题,但通常,增强其中一个会以牺牲另一个为代价。

我们可以通过添加更多特征来解决高方差问题,这些特征可以让模型学习。这个解决方案很可能会增加偏差,因此你需要在它们之间做出某种权衡。

学习可见性

有许多出色的数据科学算法可以用来解决不同领域的问题,但使学习过程可见的关键因素是拥有足够的数据。你可能会问,为了让学习过程变得可见且值得做,需要多少数据?作为经验法则,研究人员和机器学习从业者一致认为,你需要的数据样本至少是模型中自由度数量的 10 倍。

例如,在线性模型的情况下,自由度表示你数据集中所拥有的特征数量。如果你的数据中有 50 个解释性特征,那么你至少需要 500 个数据样本/观测值。

打破经验法则

在实际操作中,你可以打破这个规则,用少于数据中特征数 10 倍的数据进行学习;这通常发生在模型简单且使用了某种叫做正则化的方法(将在下一章讨论)。

Jake Vanderplas 写了一篇文章(jakevdp.github.io/blog/2015/07/06/model-complexity-myth/)来展示即使数据的参数比示例更多,依然可以进行学习。为了证明这一点,他使用了正则化方法。

总结

在本章中,我们介绍了机器学习从业者用来理解数据并让学习算法从数据中获得最大收益的最重要工具。

特征工程是数据科学中第一个也是最常用的工具,它是任何数据科学管道中必不可少的组件。该工具的目的是为数据创建更好的表示,并提高模型的预测能力。

我们看到大量特征可能会导致问题,并使分类器的表现变得更差。我们还发现,存在一个最佳特征数量,可以最大化模型的性能,而这个最佳特征数量是数据样本/观测值数量的函数。

随后,我们介绍了最强大的工具之一——偏差-方差分解。这个工具广泛用于测试模型在测试集上的表现。

最后,我们讲解了学习可见性,它回答了一个问题:为了开展业务并进行机器学习,我们需要多少数据。经验法则表明,我们需要的样本/观测值至少是数据特征数量的 10 倍。然而,这个经验法则可以通过使用另一种工具——正则化来打破,正则化将在下一章详细讨论。

接下来,我们将继续增加我们可以使用的数据科学工具,以从数据中推动有意义的分析,并面对应用机器学习时的日常问题。

第四章:快速入门 TensorFlow

在本章中,我们将概述一个最广泛使用的深度学习框架。TensorFlow 拥有庞大的社区支持,并且日益壮大,使其成为构建复杂深度学习应用程序的一个良好选择。来自 TensorFlow 网站的介绍:

“TensorFlow 是一个开源软件库,旨在通过数据流图进行数值计算。图中的节点代表数学运算,而图的边缘代表在节点间传递的多维数据数组(张量)。灵活的架构允许你将计算部署到一台或多台 CPU 或 GPU 上,无论是在桌面、服务器还是移动设备上,都可以通过单一的 API 完成。TensorFlow 最初由谷歌机器智能研究组织中的 Google Brain 团队的研究人员和工程师开发,用于进行机器学习和深度神经网络的研究,但该系统足够通用,能够应用于许多其他领域。”

本章将涉及以下内容:

  • TensorFlow 安装

  • TensorFlow 环境

  • 计算图

  • TensorFlow 数据类型、变量和占位符

  • 获取 TensorFlow 输出

  • TensorBoard——可视化学习

TensorFlow 安装

TensorFlow 安装提供两种模式:CPU 和 GPU。我们将从安装 GPU 模式的 TensorFlow 开始。

TensorFlow GPU 安装教程(Ubuntu 16.04)

TensorFlow 的 GPU 模式安装需要最新版本的 NVIDIA 驱动程序,因为目前只有 GPU 版本的 TensorFlow 支持 CUDA。以下部分将带你通过逐步安装 NVIDIA 驱动程序和 CUDA 8 的过程。

安装 NVIDIA 驱动程序和 CUDA 8

首先,你需要根据你的 GPU 安装正确的 NVIDIA 驱动程序。我使用的是 GeForce GTX 960M GPU,所以我将安装 nvidia-375(如果你使用的是其他 GPU,可以使用 NVIDIA 搜索工具 www.nvidia.com/Download/index.aspx 来帮助你找到正确的驱动程序版本)。如果你想知道你的机器的 GPU 型号,可以在终端中执行以下命令:

lspci | grep -i nvidia

你应该在终端中看到以下输出:

接下来,我们需要添加一个专有的 NVIDIA 驱动程序仓库,以便能够使用 apt-get 安装驱动程序:

sudo add-apt-repository ppa:graphics-drivers/ppa
sudo apt-get update
sudo apt-get install nvidia-375

在成功安装 NVIDIA 驱动程序后,重新启动机器。要验证驱动程序是否正确安装,可以在终端中执行以下命令:

cat /proc/driver/nvidia/version

你应该在终端中看到以下输出:

接下来,我们需要安装 CUDA 8。打开以下 CUDA 下载链接:developer.nvidia.com/cuda-downloads。根据以下截图选择你的操作系统、架构、发行版、版本,最后选择安装程序类型:

安装文件大约 2 GB。你需要执行以下安装指令:

sudo dpkg -i cuda-repo-ubuntu1604-8-0-local-ga2_8.0.61-1_amd64.deb
sudo apt-get update
sudo apt-get install cuda

接下来,我们需要通过执行以下命令将库添加到 .bashrc 文件中:

echo 'export PATH=/usr/local/cuda/bin:$PATH' >> ~/.bashrc
echo 'export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH' >> ~/.bashrc
source ~/.bashrc

接下来,你需要通过执行以下命令来验证 CUDA 8 的安装:

nvcc -V

你应该在终端中看到以下输出:

最后,在本节中,我们需要安装 cuDNN 6.0。NVIDIA CUDA 深度神经网络库cuDNN)是一个为深度神经网络加速的 GPU 库。你可以从 NVIDIA 的网页下载。执行以下命令以解压并安装 cuDNN:

cd ~/Downloads/
tar xvf cudnn*.tgz
cd cuda
sudo cp */*.h /usr/local/cuda/include/
sudo cp */libcudnn* /usr/local/cuda/lib64/
sudo chmod a+r /usr/local/cuda/lib64/libcudnn*

为了确保你的安装成功,你可以在终端中使用 nvidia-smi 工具。如果安装成功,该工具会提供关于 GPU 的监控信息,比如 RAM 和运行中的进程。

安装 TensorFlow

在为 TensorFlow 准备好 GPU 环境之后,我们现在可以安装 GPU 版本的 TensorFlow。但在安装 TensorFlow 之前,你可以先安装一些有用的 Python 包,这些包将在接下来的章节中帮助你,并使你的开发环境更为方便。

我们可以通过执行以下命令来安装一些数据处理、分析和可视化库:

sudo apt-get update && apt-get install -y python-numpy python-scipy python-nose python-h5py python-skimage python-matplotlib python-pandas python-sklearn python-sympy
sudo apt-get clean && sudo apt-get autoremove
sudo rm -rf /var/lib/apt/lists/*

接下来,你可以安装更多有用的库,如虚拟环境、Jupyter Notebook 等:

sudo apt-get update
sudo apt-get install git python-dev python3-dev python-numpy python3-numpy build-essential  python-pip python3-pip python-virtualenv swig python-wheel libcurl3-dev
sudo apt-get install -y libfreetype6-dev libpng12-dev
pip3 install -U matplotlib ipython[all] jupyter pandas scikit-image

最后,我们可以通过执行以下命令开始安装 GPU 版本的 TensorFlow:

pip3 install --upgrade tensorflow-gpu

你可以通过使用 Python 来验证 TensorFlow 是否成功安装:

python3
>>> import tensorflow as tf
>>> a = tf.constant(5)
>>> b = tf.constant(6)
>>> sess = tf.Session()
>>> sess.run(a+b)
// this should print bunch of messages showing device status etc. // If everything goes well, you should see gpu listed in device
>>> sess.close()

你应该在终端中看到以下输出:

TensorFlow CPU 安装(适用于 Ubuntu 16.04)

在本节中,我们将安装 CPU 版本,这个版本在安装之前不需要任何驱动程序。所以,首先让我们安装一些有用的数据处理和可视化的包:

sudo apt-get update && apt-get install -y python-numpy python-scipy python-nose python-h5py python-skimage python-matplotlib python-pandas python-sklearn python-sympy
sudo apt-get clean && sudo apt-get autoremove
sudo rm -rf /var/lib/apt/lists/*

接下来,你可以安装一些有用的库,比如虚拟环境、Jupyter Notebook 等:

sudo apt-get update
sudo apt-get install git python-dev python3-dev python-numpy python3-numpy build-essential  python-pip python3-pip python-virtualenv swig python-wheel libcurl3-dev
sudo apt-get install -y libfreetype6-dev libpng12-dev
pip3 install -U matplotlib ipython[all] jupyter pandas scikit-image

最后,你可以通过执行以下命令来安装最新的 TensorFlow CPU 版本:

pip3 install --upgrade tensorflow

你可以通过运行以下 TensorFlow 语句来检查 TensorFlow 是否成功安装:

python3
>>> import tensorflow as tf
>>> a = tf.constant(5)
>>> b = tf.constant(6)
>>> sess = tf.Session()
>>> sess.run(a+b)
>> sess.close()

你应该在终端中看到以下输出:

TensorFlow CPU 安装(适用于 macOS X)

在本节中,我们将使用 virtualenv 为 macOS X 安装 TensorFlow。所以,首先让我们通过执行以下命令安装 pip 工具:

sudo easy_install pip

接下来,我们需要安装虚拟环境库:

sudo pip install --upgrade virtualenv

安装虚拟环境库后,我们需要创建一个容器或虚拟环境,它将托管 TensorFlow 的安装以及你可能想要安装的任何包,而不会影响底层的主机系统:

virtualenv --system-site-packages targetDirectory # for Python 2.7
virtualenv --system-site-packages -p python3 targetDirectory # for Python 3.n

这里假设targetDirectory~/tensorflow

现在你已经创建了虚拟环境,你可以通过输入以下命令来访问它:

source ~/tensorflow/bin/activate 

一旦你输入这个命令,你将进入你刚刚创建的虚拟机,你可以在这个环境中安装任何包,而这些包只会安装在这个环境中,不会影响你所使用的底层或主机系统。

要退出环境,你可以输入以下命令:

deactivate

请注意,当前我们确实需要待在虚拟环境内,所以暂时保持它激活。一旦你完成了 TensorFlow 的使用,应该退出虚拟环境:

source bin/activate

为了安装 TensorFlow 的 CPU 版本,你可以输入以下命令,这将同时安装 TensorFlow 所需的所有依赖库:

(tensorflow)$ pip install --upgrade tensorflow      # for Python 2.7
(tensorflow)$ pip3 install --upgrade tensorflow     # for Python 3.n

TensorFlow GPU/CPU 安装指南(Windows)

我们假设你的系统已经安装了 Python 3。要安装 TensorFlow,请以管理员身份启动终端,方法如下:打开开始菜单,搜索 cmd,然后右键点击它并选择“以管理员身份运行”:

一旦你打开了命令窗口,你可以输入以下命令以在 GPU 模式下安装 TensorFlow:

你需要在输入下一个命令之前安装pippip3(取决于你的 Python 版本)。

C:\> pip3 install --upgrade tensorflow-gpu

输入以下命令以在 CPU 模式下安装 TensorFlow:

C:\> pip3 install --upgrade tensorflow

TensorFlow 环境

TensorFlow 是谷歌推出的另一个深度学习框架,正如TensorFlow这个名称所暗示的,它源自神经网络在多维数据数组或张量上执行的操作!它实际上是张量的流动。

但首先,为什么我们要在本书中使用深度学习框架?

  • 它扩展了机器学习代码:深度学习和机器学习的大部分研究能够被应用或归因于这些深度学习框架。它们使数据科学家能够极其快速地进行迭代,并使深度学习和其他机器学习算法更加易于实践者使用。像谷歌、Facebook 等大公司正在使用这样的深度学习框架来扩展到数十亿用户。

  • 它计算梯度:深度学习框架也可以自动计算梯度。如果你一步步跟踪梯度计算的过程,你会发现梯度计算并不简单,并且自己实现一个无错的版本可能会很棘手。

  • 它标准化了用于分享的机器学习应用程序:此外,可以在线获取预训练模型,这些模型可以在不同的深度学习框架中使用,并且这些预训练模型帮助那些在 GPU 资源有限的人,这样他们就不必每次都从头开始。我们可以站在巨人的肩膀上,从那里开始。

  • 有很多可用的深度学习框架,具有不同的优势、范式、抽象级别、编程语言等等。

  • 与 GPU 接口进行并行处理:使用 GPU 进行计算是一个非常迷人的特性,因为 GPU 比 CPU 拥有更多的核心和并行化,所以能够大大加速您的代码。

这就是为什么 TensorFlow 几乎是在深度学习中取得进展的必要条件,因为它可以促进您的项目。

所以,简而言之,什么是 TensorFlow?

  • TensorFlow 是谷歌的深度学习框架,用于使用数据流图进行数值计算的开源工具。

  • 它最初由 Google Brain 团队开发,以促进他们的机器学习研究。

  • TensorFlow 是表达机器学习算法和执行这些算法的实现的接口。

TensorFlow 是如何工作的,其潜在范式是什么?

计算图

有关 TensorFlow 的所有大想法中最重要的是,数值计算被表达为一个计算图,如下图所示。因此,任何 TensorFlow 程序的核心都将是一个计算图,以下内容为真:

  • 图节点是具有任意数量输入和输出的操作。

  • 我们节点之间的图边将是在这些操作之间流动的张量,关于张量的最佳思考方式实际上是作为n维数组。

使用这样的流图作为深度学习框架的主干的优势在于,它允许您以小而简单的操作构建复杂的模型。此外,当我们在后面讨论梯度计算时,这将使得梯度计算变得非常简单:

另一种思考 TensorFlow 图的方式是,每个操作都是可以在那一点评估的函数。

TensorFlow 数据类型、变量和占位符

对计算图的理解将帮助我们将复杂模型看作是小子图和操作。

让我们看一个只有一个隐藏层的神经网络的例子,以及其在 TensorFlow 中可能的计算图是什么样子:

因此,我们有一些隐藏层,我们试图计算,如某个参数矩阵W时间一些输入x加上偏差项b的 ReLU 激活。ReLU 函数取输出的最大值和零之间的较大者。

下图显示了 TensorFlow 中图形的可能样子:

在这个图中,我们为 bW 定义了变量,并且我们为 x 定义了一个占位符;我们还为图中的每个操作定义了节点。接下来,我们将详细了解这些节点类型。

变量

变量将是有状态的节点,它们输出当前的值。在这个例子中,就是 bW。我们所说的变量是有状态的意思是,它们在多次执行过程中保持其当前值,而且很容易将保存的值恢复到变量中:

此外,变量还有其他有用的功能;例如,它们可以在训练过程中及训练后保存到磁盘,这使得我们之前提到的功能得以实现,即来自不同公司和团队的人们可以保存、存储并将他们的模型参数传输给其他人。而且,变量是你希望调整以最小化损失的东西,我们很快就会看到如何做到这一点。

重要的是要知道,图中的变量,如 bW,仍然是操作,因为根据定义,图中的所有节点都是操作。因此,当你在运行时评估这些持有 bW 值的操作时,你将获得这些变量的值。

我们可以使用 TensorFlow 的 Variable() 函数来定义一个变量并给它一个初始值:

var = tf.Variable(tf.random_normal((0,1)),name='random_values')

这一行代码将定义一个 2x2 的变量,并从标准正态分布中初始化它。你还可以为变量命名。

占位符

下一种类型的节点是占位符。占位符是那些在执行时输入值的节点:

如果你的计算图有依赖于外部数据的输入,这些输入就是我们将在训练过程中添加到计算中的占位符。因此,对于占位符,我们不提供任何初始值。我们只需指定张量的数据类型和形状,这样即使图中还没有存储任何值,计算图仍然知道该计算什么。

我们可以使用 TensorFlow 的占位符函数来创建一个占位符:

ph_var1 = tf.placeholder(tf.float32,shape=(2,3))
ph_var2 = tf.placeholder(tf.float32,shape=(3,2))
result = tf.matmul(ph_var1,ph_var2)

这些代码行定义了两个特定形状的占位符变量,并定义了一个操作(参见下一节),该操作将这两个值相乘。

数学操作

第三种类型的节点是数学操作,它们将是我们的矩阵乘法(MatMul)、加法(Add)和 ReLU。这些都是你 TensorFlow 图中的节点,和 NumPy 操作非常相似:

让我们看看这张图在代码中会是什么样子。

我们执行以下步骤来生成上面的图:

  1. 创建权重 Wb,包括初始化。我们可以通过从均匀分布中采样来初始化权重矩阵 W,即 W ~ Uniform(-1,1),并将 b 初始化为 0。

  2. 创建输入占位符 x,它将具有 m * 784 的输入矩阵形状。

  3. 构建流图。

接下来,让我们按照以下步骤来构建流图:

# import TensorFlow package
import tensorflow as tf
# build a TensorFlow variable b taking in initial zeros of size 100
# ( a vector of 100 values)
b  = tf.Variable(tf.zeros((100,)))
# TensorFlow variable uniformly distributed values between -1 and 1
# of shape 784 by 100
W = tf.Variable(tf.random_uniform((784, 100),-1,1))
# TensorFlow placeholder for our input data that doesn't take in
# any initial values, it just takes a data type 32 bit floats as
# well as its shape
x = tf.placeholder(tf.float32, (100, 784))
# express h as Tensorflow ReLU of the TensorFlow matrix
#Multiplication of x and W and we add b
h = tf.nn.relu(tf.matmul(x,W) + b )
h and see its value until we run this graph. So, this code snippet is just for building a backbone for our model. If you try to print the value of *W* or *b* in the preceding code, you should get the following output in Python:

到目前为止,我们已经定义了我们的图,现在我们需要实际运行它。

获取 TensorFlow 的输出

在前面的部分,我们知道如何构建计算图,但我们需要实际运行它并获取其值。

我们可以通过一种叫做会话(session)的方式来部署/运行图,这实际上是一个绑定到特定执行上下文(例如 CPU 或 GPU)的机制。因此,我们将构建的图部署到 CPU 或 GPU 上下文中。

为了运行图,我们需要定义一个叫做 sess 的会话对象,然后调用 run 函数,该函数接受两个参数:

sess.run(fetches, feeds)

这里:

  • fetches 是图节点的列表,返回节点的输出。我们关注的正是这些节点的计算值。

  • feeds 是一个字典,将图节点映射到我们希望在模型中运行的实际值。因此,这就是我们实际填写之前提到的占位符的地方。

所以,让我们继续运行我们的图:

# importing the numpy package for generating random variables for
# our placeholder x
import numpy as np
# build a TensorFlow session object which takes a default execution
# environment which will be most likely a CPU
sess = tf.Session()
# calling the run function of the sess object to initialize all the
# variables.
sess.run(tf.global_variables_initializer())
# calling the run function on the node that we are interested in,
# the h, and we feed in our second argument which is a dictionary
# for our placeholder x with the values that we are interested in.
sess.run(h, {x: np.random.random((100,784))})   

通过 sess 对象运行我们的图后,我们应该得到类似下面的输出:

lazy evaluation. It means that the evaluation of your graph only ever happens at runtime, and runtime in TensorFlow means the session. So, calling this function, global_variables_initializer(), will actually initialize anything called variable in your graph, such as *W* and *b* in our case.

我们还可以在一个 with 块中使用会话变量,以确保在执行图后会话会被关闭:

ph_var1 = tf.placeholder(tf.float32,shape=(2,3))
ph_var2 = tf.placeholder(tf.float32,shape=(3,2))
result = tf.matmul(ph_var1,ph_var2)
with tf.Session() as sess:
    print(sess.run([result],feed_dict={ph_var1:[[1.,3.,4.],[1.,3.,4.]],ph_var2:[[1., 3.],[3.,1.],[.1,4.]]}))

Output:
[array([[10.4, 22\. ],
       [10.4, 22\. ]], dtype=float32)]

TensorBoard – 可视化学习

你将用 TensorFlow 进行的计算——例如训练一个庞大的深度神经网络——可能会很复杂且令人困惑,相应的计算图也将非常复杂。为了更容易理解、调试和优化 TensorFlow 程序,TensorFlow 团队提供了一套可视化工具,称为 TensorBoard,这是一个可以通过浏览器运行的 Web 应用套件。TensorBoard 可以用来可视化你的 TensorFlow 图,绘制关于图执行的定量指标,并展示额外的数据,比如通过它的图像。当 TensorBoard 完全配置好后,它看起来是这样的:

为了理解 TensorBoard 的工作原理,我们将构建一个计算图,它将作为 MNIST 数据集的分类器,MNIST 是一个手写图像数据集。

你不需要理解这个模型的所有细节,但它会向你展示一个用 TensorFlow 实现的机器学习模型的一般流程。

所以,让我们从导入 TensorFlow 并使用 TensorFlow 的辅助函数加载所需的数据集开始;这些辅助函数会检查你是否已经下载了数据集,否则它会为你下载:

import tensorflow as tf

# Using TensorFlow helper function to get the MNIST dataset
from tensorflow.examples.tutorials.mnist import input_data
mnist_dataset = input_data.read_data_sets("/tmp/data/", one_hot=True)

Output:
Extracting /tmp/data/train-images-idx3-ubyte.gz
Extracting /tmp/data/train-labels-idx1-ubyte.gz
Extracting /tmp/data/t10k-images-idx3-ubyte.gz
Extracting /tmp/data/t10k-labels-idx1-ubyte.gz

接下来,我们需要定义超参数(用于微调模型性能的参数)和模型的输入:

# hyperparameters of the the model (you don't have to understand the functionality of each parameter)
learning_rate = 0.01
num_training_epochs = 25
train_batch_size = 100
display_epoch = 1
logs_path = '/tmp/tensorflow_tensorboard/'

# Define the computational graph input which will be a vector of the image pixels
# Images of MNIST has dimensions of 28 by 28 which will multiply to 784
input_values = tf.placeholder(tf.float32, [None, 784], name='input_values')

# Define the target of the model which will be a classification problem of 10 classes from 0 to 9
target_values = tf.placeholder(tf.float32, [None, 10], name='target_values')

# Define some variables for the weights and biases of the model
weights = tf.Variable(tf.zeros([784, 10]), name='weights')
biases = tf.Variable(tf.zeros([10]), name='biases')

现在我们需要构建模型并定义我们将要优化的代价函数:

# Create the computational graph and encapsulating different operations to different scopes
# which will make it easier for us to understand the visualizations of TensorBoard
with tf.name_scope('Model'):
 # Defining the model
 predicted_values = tf.nn.softmax(tf.matmul(input_values, weights) + biases)

with tf.name_scope('Loss'):
 # Minimizing the model error using cross entropy criteria
 model_cost = tf.reduce_mean(-tf.reduce_sum(target_values*tf.log(predicted_values), reduction_indices=1))

with tf.name_scope('SGD'):
 # using Gradient Descent as an optimization method for the model cost above
 model_optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(model_cost)

with tf.name_scope('Accuracy'):
 #Calculating the accuracy
 model_accuracy = tf.equal(tf.argmax(predicted_values, 1), tf.argmax(target_values, 1))
 model_accuracy = tf.reduce_mean(tf.cast(model_accuracy, tf.float32))

# TensorFlow use the lazy evaluation strategy while defining the variables
# So actually till now none of the above variable got created or initialized
init = tf.global_variables_initializer()

我们将定义一个摘要变量,用于监控特定变量(如损失函数)在训练过程中如何变化,以及其改进情况:

# Create a summary to monitor the model cost tensor
tf.summary.scalar("model loss", model_cost)

# Create another summary to monitor the model accuracy tensor
tf.summary.scalar("model accuracy", model_accuracy)

# Merging the summaries to single operation
merged_summary_operation = tf.summary.merge_all()

最后,我们通过定义一个会话变量来运行模型,该变量将用于执行我们构建的计算图:

# kick off the training process
with tf.Session() as sess:

 # Intialize the variables 
 sess.run(init)

 # operation to feed logs to TensorBoard
 summary_writer = tf.summary.FileWriter(logs_path, graph=tf.get_default_graph())

 # Starting the training cycle by feeding the model by batch at a time
 for train_epoch in range(num_training_epochs):

 average_cost = 0.
 total_num_batch = int(mnist_dataset.train.num_examples/train_batch_size)

 # iterate through all training batches
 for i in range(total_num_batch):
 batch_xs, batch_ys = mnist_dataset.train.next_batch(train_batch_size)

 # Run the optimizer with gradient descent and cost to get the loss
 # and the merged summary operations for the TensorBoard
 _, c, summary = sess.run([model_optimizer, model_cost, merged_summary_operation],
 feed_dict={input_values: batch_xs, target_values: batch_ys})

 # write statistics to the log et every iteration
 summary_writer.add_summary(summary, train_epoch * total_num_batch + i)

 # computing average loss
 average_cost += c / total_num_batch

 # Display logs per epoch step
 if (train_epoch+1) % display_epoch == 0:
 print("Epoch:", '%03d' % (train_epoch+1), "cost=", "{:.9f}".format(average_cost))

 print("Optimization Finished!")

 # Testing the trained model on the test set and getting the accuracy compared to the actual labels of the test set
 print("Accuracy:", model_accuracy.eval({input_values: mnist_dataset.test.images, target_values: mnist_dataset.test.labels}))

 print("To view summaries in the Tensorboard, run the command line:\n" \
 "--> tensorboard --logdir=/tmp/tensorflow_tensorboard " \
"\nThen open http://0.0.0.0:6006/ into your web browser")

训练过程的输出应类似于以下内容:

Epoch: 001 cost= 1.183109128
Epoch: 002 cost= 0.665210275
Epoch: 003 cost= 0.552693334
Epoch: 004 cost= 0.498636444
Epoch: 005 cost= 0.465516675
Epoch: 006 cost= 0.442618381
Epoch: 007 cost= 0.425522513
Epoch: 008 cost= 0.412194222
Epoch: 009 cost= 0.401408134
Epoch: 010 cost= 0.392437336
Epoch: 011 cost= 0.384816745
Epoch: 012 cost= 0.378183398
Epoch: 013 cost= 0.372455584
Epoch: 014 cost= 0.367275238
Epoch: 015 cost= 0.362772711
Epoch: 016 cost= 0.358591895
Epoch: 017 cost= 0.354892231
Epoch: 018 cost= 0.351451424
Epoch: 019 cost= 0.348337946
Epoch: 020 cost= 0.345453095
Epoch: 021 cost= 0.342769080
Epoch: 022 cost= 0.340236065
Epoch: 023 cost= 0.337953151
Epoch: 024 cost= 0.335739001
Epoch: 025 cost= 0.333702818
Optimization Finished!
Accuracy: 0.9146
To view summaries in the Tensorboard, run the command line:
--> tensorboard --logdir=/tmp/tensorflow_tensorboard 
Then open http://0.0.0.0:6006/ into your web browser

为了在 TensorBoard 中查看汇总统计信息,我们将在终端中输入以下命令,执行输出末尾的提示信息:

tensorboard --logdir=/tmp/tensorflow_tensorboard

然后,在你的网页浏览器中打开http://0.0.0.0:6006/

打开 TensorBoard 后,你应该会看到类似于以下的截图:

这将显示我们监控的变量,如模型的准确度以及它是如何逐渐提高的,模型的损失函数及其如何逐渐降低。因此,你会看到我们在这里经历了一个正常的学习过程。但有时你会发现准确度和模型损失会随机变化,或者你想跟踪一些变量及其在会话期间的变化,这时 TensorBoard 将非常有用,帮助你发现任何随机性或错误。

如果切换到 TensorBoard 的 GRAPHS 标签页,你将看到我们在前面的代码中构建的计算图:

摘要

在本章中,我们涵盖了 Ubuntu 和 Mac 的安装过程,介绍了 TensorFlow 编程模型,并解释了可用于构建复杂操作的不同类型的简单节点,以及如何通过会话对象从 TensorFlow 获取输出。我们还介绍了 TensorBoard,并说明了它在调试和分析复杂深度学习应用中的重要性。

接下来,我们将简单解释神经网络及多层神经网络背后的直觉。我们还将涵盖一些 TensorFlow 的基本示例,并演示如何将其用于回归和分类问题。

第五章:TensorFlow 实践 - 一些基本示例

在本章中,我们将解释 TensorFlow 背后的主要计算概念,即计算图模型,并展示如何通过实现线性回归和逻辑回归帮助你入门。

本章将涵盖以下主题:

  • 单个神经元的能力与激活函数

  • 激活函数

  • 前馈神经网络

  • 多层网络的需求

  • TensorFlow 术语—回顾

  • 线性回归模型—构建与训练

  • 逻辑回归模型—构建与训练

我们将从解释单个神经元实际上可以做什么/建模开始,并基于此,提出多层网络的需求。接下来,我们将对在 TensorFlow 中使用/可用的主要概念和工具做更详细的阐述,并展示如何使用这些工具构建简单的示例,如线性回归和逻辑回归。

单个神经元的能力

神经网络 是一种计算模型,主要受到人类大脑生物神经网络处理传入信息方式的启发。神经网络在机器学习研究(特别是深度学习)和工业应用中取得了巨大突破,如计算机视觉、语音识别和文本处理等领域取得了突破性的成果。本章中,我们将尝试理解一种特定类型的神经网络,即 多层感知器

生物学动机与连接

我们大脑的基本计算单元是神经元,我们神经系统中大约有 860 亿个神经元,这些神经元通过大约 的突触相连接。

图 1 显示了生物神经元,图 2 显示了对应的数学模型。在生物神经元的图示中,每个神经元通过树突接收传入信号,然后沿着轴突产生输出信号,轴突分支后通过突触连接到其他神经元。

在神经元的对应数学计算模型中,沿轴突传播的信号 与树突的乘法操作 相互作用,该树突来自系统中另一个神经元,并根据该突触的突触强度进行交互,突触强度由 表示。其核心思想是,突触权重/强度 由网络学习,它们控制一个特定神经元对另一个神经元的影响。

此外,在图 2中,树突将信号传送到细胞体,细胞体将这些信号求和。如果最终结果超过某个阈值,神经元就会在计算模型中被激活。

同时,值得一提的是,我们需要控制通过轴突传递的输出脉冲频率,因此我们使用被称为激活函数的东西。实际上,一个常用的激活函数是 Sigmoid 函数 σ,因为它接受一个实数值输入(求和后的信号强度)并将其压缩到 0 和 1 之间。我们将在接下来的部分中看到这些激活函数的详细信息:

图 1:大脑的计算单元(http://cs231n.github.io/assets/nn1/neuron.png)

这里是生物学模型对应的基本数学模型:

图 2:大脑计算单元的数学模型(http://cs231n.github.io/assets/nn1/neuron_model.jpeg)

神经网络中的基本计算单元是神经元,通常称为节点单元。它接收来自其他节点或外部来源的输入,并计算输出。每个输入都有一个相关的权重w),该权重根据该输入相对于其他输入的重要性分配。节点将一个函数 f(我们稍后会定义)应用于其输入的加权和。

因此,神经网络的一般基本计算单元称为神经元/节点/单元

这个神经元接收来自前一个神经元或外部来源的输入,然后对该输入进行处理以产生所谓的激活。每个输入到这个神经元的信号都有自己的权重 ,它表示连接的强度,从而也表示该输入的重要性。

因此,神经网络这个基本构建模块的最终输出是加权求和后的输入 w,然后神经元通过激活函数处理加和后的输出。

图 3:单个神经元

激活函数

神经元的输出如图 3所示进行计算,并通过激活函数进行处理,从而在输出中引入非线性。这个 f 称为激活函数。激活函数的主要目的是:

  • 在神经元的输出中引入非线性。这一点非常重要,因为大多数真实世界的数据是非线性的,我们希望神经元能够学习这些非线性表示。

  • 将输出压缩到特定范围内。

每个激活函数(或非线性函数)接受一个数字并对其执行一定的固定数学操作。在实际中,你可能会遇到几种激活函数。

因此,我们将简要介绍最常见的激活函数。

Sigmoid

从历史上看,Sigmoid 激活函数在研究人员中广泛使用。该函数接受一个实数值输入,并将其压缩到 0 和 1 之间,如下图所示:

σ(x) = 1 / (1 + exp(−x))

图 4:Sigmoid 激活函数

Tanh

Tanh 是另一种激活函数,能够容忍一些负值。Tanh 接受一个实值输入,并将其压缩到 [-1, 1] 之间:

tanh(x) = 2σ(2x) − 1

图 5:Tanh 激活函数

ReLU

整流线性单元ReLU)不容忍负值,因为它接受一个实值输入并将其在零处进行阈值处理(将负值替换为零):

f(x) = max(0, x)

图 6:Relu 激活函数

偏置的重要性:偏置的主要功能是为每个节点提供一个可训练的常量值(除了节点接收的正常输入之外)。请参见此链接 stackoverflow.com/questions/2480650/role-of-bias-in-neural-networks 了解有关神经元中偏置作用的更多信息。

前馈神经网络

前馈神经网络是最早且最简单的人工神经网络类型。它包含多个神经元(节点),这些神经元按层排列。相邻层的节点之间有连接或边。这些连接都有与之关联的权重。

一个前馈神经网络的示例如 图 7 所示:

图 7:一个示例前馈神经网络

在前馈网络中,信息仅向一个方向流动——从输入节点,通过隐藏节点(如果有的话),然后到输出节点。网络中没有循环或回路(这种前馈网络的特性与循环神经网络不同,后者节点之间的连接会形成循环)。

多层网络的需求

多层感知器MLP)包含一个或多个隐藏层(除了一个输入层和一个输出层)。虽然单层感知器只能学习线性函数,但 MLP 也可以学习非线性函数。

图 7 显示了一个具有单个隐藏层的 MLP。请注意,所有连接都有与之关联的权重,但图中仅显示了三个权重(w0w1w2)。

输入层:输入层有三个节点。偏置节点的值为 1。其他两个节点将 X1 和 X2 作为外部输入(这些数值取决于输入数据集)。如前所述,输入层中不执行计算,因此 输入层 中节点的输出分别是 1X1X2,并将其送入 隐藏层

隐藏层: 隐藏层也有三个节点,其中偏置节点的输出为 1。隐藏层中另外两个节点的输出依赖于来自输入层的输出(1X1X2),以及与连接(边)相关的权重。记住,f指的是激活函数。这些输出随后被馈送到输出层中的节点。

图 8:具有一个隐藏层的多层感知器

输出层: 输出层有两个节点;它们从隐藏层接收输入,并执行类似于高亮显示的隐藏节点所示的计算。计算得出的值(Y1Y2)作为多层感知器的输出。

给定一组特征 X = (x1, x2, …) 和目标 y,多层感知器可以学习特征与目标之间的关系,无论是分类问题还是回归问题。

让我们通过一个例子来更好地理解多层感知器。假设我们有以下学生成绩数据集:

表 1 – 示例学生成绩数据集

学习小时数 期中考试成绩 期末考试结果
35 67 通过
12 75 未通过
16 89 通过
45 56 通过
10 90 未通过

这两列输入数据表示学生学习的小时数和学生在期中考试中获得的成绩。期末结果列可以有两个值,10,表示学生是否通过期末考试。例如,我们可以看到,如果学生学习了 35 小时并且期中考试得了 67 分,他/她最终通过了期末考试。

现在,假设我们想预测一个学生学习了 25 小时并且期中考试得了 70 分,他/她是否能通过期末考试:

表 2 – 示例学生期末考试结果未知

学习小时数 期中考试成绩 期末考试结果
26 70 ?

这是一个二分类问题,其中多层感知器可以从给定的示例(训练数据)中学习,并在给定新数据点时做出有根据的预测。我们很快就会看到多层感知器如何学习这些关系。

训练我们的 MLP – 反向传播算法

多层感知器学习的过程称为反向传播算法。我推荐阅读 Hemanth Kumar 在 Quora 上的这篇回答,www.quora.com/How-do-you-explain-back-propagation-algorithm-to-a-beginner-in-neural-network/answer/Hemanth-Kumar-Mantri(后面引用),该回答清晰地解释了反向传播。

"误差反向传播,通常简称为 BackProp,是人工神经网络(ANN)训练的几种方式之一。它是一种监督式训练方法,这意味着它从带标签的训练数据中学习(有一个监督者来引导其学习)。

简单来说,BackProp 就像是“从错误中学习”。每当 ANN 犯错时,监督者都会纠正它。

一个 ANN 由不同层次的节点组成:输入层、隐藏层和输出层。相邻层之间节点的连接有与之关联的“权重”。学习的目标是为这些边分配正确的权重。给定一个输入向量,这些权重决定了输出向量的值。

在监督学习中,训练集是标注的。这意味着对于某些给定的输入,我们知道期望/预期的输出(标签)。

反向传播算法:

最初,所有边的权重是随机分配的。对于训练数据集中的每个输入,激活人工神经网络(ANN)并观察其输出。将此输出与我们已知的期望输出进行比较,误差被“传播”回前一层。该误差被记录并相应地“调整”权重。这个过程会不断重复,直到输出误差低于预定的阈值。

一旦上述算法终止,我们就得到了一个“学习过”的 ANN,我们认为它已经准备好处理“新”输入。这个 ANN 被认为已经从多个示例(标注数据)以及它的错误(误差传播)中学习了。”

—Hemanth Kumar。

现在我们了解了反向传播的工作原理,让我们回到学生成绩数据集。

显示在图 8中的 MLP 有两个输入层节点,分别接收学习时长和期中成绩作为输入。它还拥有一个包含两个节点的隐藏层。输出层也有两个节点;上层节点输出通过的概率,而下层节点输出失败的概率。

在分类应用中,我们广泛使用 softmax 函数 (cs231n.github.io/linear-classify/#softmax) 作为 MLP 输出层的激活函数,以确保输出是概率,并且它们的和为 1。softmax 函数接受一个任意实数值的向量,并将其压缩成一个在 0 和 1 之间的值的向量,且它们的和为 1。因此,在此情况下:

步骤 1 – 前向传播

网络中的所有权重都是随机初始化的。我们考虑一个特定的隐藏层节点,并称其为V。假设从输入到该节点的连接权重为w1w2w3(如图所示)。

然后,网络将第一个训练样本作为输入(我们知道,对于输入 35 和 67,及格的概率是 1):

  • 网络输入 = [35, 67]

  • 网络期望输出(目标) = [1, 0]

然后,考虑节点的输出V,可以通过以下方式计算(f 是激活函数,如 sigmoid):

V = f (1w1 + 35w2 + 67w3)*

同样,来自隐藏层的另一个节点的输出也会被计算出来。隐藏层中两个节点的输出作为输入,传递给输出层的两个节点。这使我们能够计算输出层两个节点的输出概率。

假设输出层两个节点的输出概率分别是 0.4 和 0.6(由于权重是随机分配的,输出也会是随机的)。我们可以看到,计算出来的概率(0.4 和 0.6)与期望的概率(分别是 1 和 0)相差很远,因此可以说网络产生了错误的输出

步骤 2 – 反向传播与权重更新

我们计算输出节点的总误差,并通过反向传播将这些误差传递回网络,计算梯度。然后,我们使用诸如梯度下降之类的优化方法来调整网络中所有的权重,目的是减少输出层的误差。

假设考虑的节点的新权重是w4w5w6(经过反向传播并调整权重后)。

如果我们现在将相同的样本作为输入喂入网络,由于权重已经被优化以最小化预测误差,网络的表现应该比初始运行更好。输出节点的误差现在减少到[0.2, -0.2],而之前是[0.6, -0.4]。这意味着我们的网络已经学会正确地分类我们的第一个训练样本。

我们对数据集中的所有其他训练样本重复这个过程。然后,我们可以说我们的网络已经学习了这些示例。

如果我们现在想预测一名学习了 25 小时并且期中考试得了 70 分的学生是否能通过期末考试,我们通过前向传播步骤,找到通过与不通过的输出概率。

我在这里避免了数学方程和梯度下降等概念的解释,而是尽量为算法建立直觉。关于反向传播算法的更深入的数学讨论,请参考这个链接:home.agh.edu.pl/%7Evlsi/AI/backp_t_en/backprop.html

TensorFlow 术语回顾

本节将概述 TensorFlow 库以及基本 TensorFlow 应用程序的结构。TensorFlow 是一个开源库,用于创建大规模的机器学习应用程序;它可以在各种硬件上建模计算,从安卓设备到异构多 GPU 系统。

TensorFlow 使用一种特殊的结构来在不同的设备上执行代码,如 CPU 和 GPU。计算被定义为一个图形,每个图形由操作组成,也称为操作,因此每当我们使用 TensorFlow 时,我们都会在图形中定义一系列操作。

要运行这些操作,我们需要将图形加载到一个会话中。会话会翻译这些操作并将它们传递给设备进行执行。

例如,下面的图像表示了一个 TensorFlow 图形。Wxb 是图中边缘上的张量。MatMul 是对张量 Wx 的操作;之后,调用 Add,并将前一个操作的结果与 b 相加。每个操作的结果张量会传递给下一个操作,直到最后,可以得到所需的结果。

图 9:示例 TensorFlow 计算图

为了使用 TensorFlow,我们需要导入该库;我们将其命名为 tf,这样就可以通过写 tf 点号再加上模块名来访问模块:

import tensorflow as tf

为了创建我们的第一个图形,我们将从使用源操作开始,这些操作不需要任何输入。这些源操作或源操作将把它们的信息传递给其他操作,这些操作将实际执行计算。

让我们创建两个源操作,它们将输出数字。我们将它们定义为 AB,你可以在下面的代码片段中看到:

A = tf.constant([2])
B = tf.constant([3])

之后,我们将定义一个简单的计算操作 tf.add(),用来将两个元素相加。你也可以使用 C = A + B,如下面的代码所示:

C = tf.add(A,B)
#C = A + B is also a way to define the sum of the terms

由于图形需要在会话的上下文中执行,我们需要创建一个会话对象:

session = tf.Session()

为了查看图形,让我们运行会话来获取之前定义的 C 操作的结果:

result = session.run(C)
print(result)
Output:
[5]

你可能会觉得,仅仅是加两个数字就做了很多工作,但理解 TensorFlow 的基本结构是非常重要的。一旦你理解了它,你就可以定义任何你想要的计算;再次强调,TensorFlow 的结构使它能够处理不同设备(CPU 或 GPU)甚至集群上的计算。如果你想了解更多,可以运行方法tf.device()

你也可以随时实验 TensorFlow 的结构,以便更好地理解它是如何工作的。如果你想查看 TensorFlow 支持的所有数学操作,可以查阅文档。

到现在为止,你应该已经理解了 TensorFlow 的结构以及如何创建基本的应用程序。

使用 TensorFlow 定义多维数组

现在我们将尝试使用 TensorFlow 定义这些数组:

salar_var = tf.constant([4])
vector_var = tf.constant([5,4,2])
matrix_var = tf.constant([[1,2,3],[2,2,4],[3,5,5]])
tensor = tf.constant( [ [[1,2,3],[2,3,4],[3,4,5]] , [[4,5,6],[5,6,7],[6,7,8]] , [[7,8,9],[8,9,10],[9,10,11]] ] )
with tf.Session() as session:
    result = session.run(salar_var)
    print "Scalar (1 entry):\n %s \n" % result
    result = session.run(vector_var)
    print "Vector (3 entries) :\n %s \n" % result
    result = session.run(matrix_var)
    print "Matrix (3x3 entries):\n %s \n" % result
    result = session.run(tensor)
    print "Tensor (3x3x3 entries) :\n %s \n" % result
Output:
Scalar (1 entry):
 [2] 

Vector (3 entries) :
 [5 6 2] 

Matrix (3x3 entries):
 [[1 2 3]
 [2 3 4]
 [3 4 5]] 

Tensor (3x3x3 entries) :
 [[[ 1  2  3]
  [ 2  3  4]
  [ 3  4  5]]

 [[ 4  5  6]
  [ 5  6  7]
  [ 6  7  8]]

 [[ 7  8  9]
  [ 8  9 10]
  [ 9 10 11]]]

现在你已经理解了这些数据结构,我鼓励你使用一些之前的函数来尝试这些数据结构,看看它们如何根据结构类型表现:

Matrix_one = tf.constant([[1,2,3],[2,3,4],[3,4,5]])
Matrix_two = tf.constant([[2,2,2],[2,2,2],[2,2,2]])
first_operation = tf.add(Matrix_one, Matrix_two)
second_operation = Matrix_one + Matrix_two
with tf.Session() as session:
    result = session.run(first_operation)
    print "Defined using tensorflow function :"
    print(result)
    result = session.run(second_operation)
    print "Defined using normal expressions :"
    print(result)
Output:
Defined using tensorflow function :
[[3 4 5]
 [4 5 6]
 [5 6 7]]
Defined using normal expressions :
[[3 4 5]
 [4 5 6]
 [5 6 7]]

使用常规符号定义以及tensorflow函数,我们能够实现逐元素相乘,也叫做哈达玛积。但如果我们想要常规的矩阵乘法呢?我们需要使用另一个 TensorFlow 函数,叫做tf.matmul()

Matrix_one = tf.constant([[2,3],[3,4]])
Matrix_two = tf.constant([[2,3],[3,4]])
first_operation = tf.matmul(Matrix_one, Matrix_two)
with tf.Session() as session:
    result = session.run(first_operation)
    print "Defined using tensorflow function :"
    print(result)
Output:
Defined using tensorflow function :
[[13 18]
 [18 25]]

我们也可以自己定义这个乘法,但已经有一个函数可以做这个,所以不需要重新发明轮子!

为什么使用张量?

张量结构通过赋予我们自由来帮助我们按自己想要的方式构造数据集。

这在处理图像时特别有用,因为图像中信息的编码方式。

想到图像时,很容易理解它有高度和宽度,因此用二维结构(矩阵)表示其中包含的信息是有意义的……直到你记得图像有颜色。为了添加颜色信息,我们需要另一个维度,这就是张量特别有用的地方。

图像被编码为颜色通道;图像数据在每个颜色的强度在给定点的颜色通道中表示,最常见的是 RGB(即红色、蓝色和绿色)。图像中包含的信息是每个通道颜色在图像的宽度和高度中的强度,就像这样:

图 10:特定图像的不同颜色通道

因此,红色通道在每个点上的强度(带宽和高度)可以用矩阵表示;蓝色和绿色通道也是如此。于是,我们最终得到三个矩阵,当这些矩阵结合在一起时,就形成了一个张量。

变量

现在我们更熟悉数据的结构了,我们将看看 TensorFlow 如何处理变量。

要定义变量,我们使用命令tf.variable()。为了能够在计算图中使用变量,有必要在会话中运行图之前初始化它们。这可以通过运行tf.global_variables_initializer()来完成。

要更新变量的值,我们只需运行一个赋值操作,将一个值分配给变量:

state = tf.Variable(0)

让我们首先创建一个简单的计数器,一个每次增加一个单位的变量:

one = tf.constant(1)
new_value = tf.add(state, one)
update = tf.assign(state, new_value)

变量必须通过运行初始化操作来初始化,前提是图已启动。我们首先需要将初始化操作添加到图中:

init_op = tf.global_variables_initializer()

然后,我们启动一个会话来运行图。

我们首先初始化变量,然后打印状态变量的初始值,最后运行更新状态变量的操作,并在每次更新后打印结果:

with tf.Session() as session:
 session.run(init_op)
 print(session.run(state))
 for _ in range(3):
    session.run(update)
    print(session.run(state))
Output:
0
1
2
3

占位符

现在,我们知道如何在 TensorFlow 中操作变量,但如果要向 TensorFlow 模型外部提供数据怎么办?

如果你想从模型外部向 TensorFlow 模型提供数据,你需要使用占位符。

那么,这些占位符是什么,它们有什么作用?占位符可以看作是模型中的空洞空洞是你将数据传递给它的地方。你可以通过 tf.placeholder(datatype) 创建它们,其中 datatype 指定数据的类型(整数、浮点数、字符串和布尔值)以及其精度(8、16、32 和 64 位)。

每种数据类型的定义和相应的 Python 语法如下:

表 3 – 不同 TensorFlow 数据类型的定义

数据类型 Python 类型 描述
DT_FLOAT tf.float32 32 位浮点数。
DT_DOUBLE tf.float64 64 位浮点数
DT_INT8 tf.int8 8 位带符号整数。
DT_INT16 tf.int16 16 位带符号整数。
DT_INT32 tf.int32 32 位带符号整数。
DT_INT64 tf.int64 64 位带符号整数。
DT_UINT8 tf.uint8 8 位无符号整数。
DT_STRING tf.string 可变长度的字节数组。每个张量的元素都是一个字节数组。
DT_BOOL tf.bool 布尔值。
DT_COMPLEX64 tf.complex64 由两个 32 位浮点数(实部和虚部)组成的复数。
DT_COMPLEX128 tf.complex128 由两个 64 位浮点数(实部和虚部)组成的复数。
DT_QINT8 tf.qint8 用于量化操作的 8 位带符号整数。
DT_QINT32 tf.qint32 用于量化操作的 32 位带符号整数。
DT_QUINT8 tf.quint8 用于量化操作的 8 位无符号整数。

所以,让我们创建一个占位符:

a=tf.placeholder(tf.float32)

定义一个简单的乘法操作:

b=a*2

现在,我们需要定义并运行会话,但由于我们在模型中创建了一个空洞来传递数据,因此在初始化会话时,我们必须传递一个带有数据的参数;否则会出现错误。

为了将数据传递给模型,我们调用会话时会传入一个额外的参数 feed_dict,在其中我们应该传递一个字典,字典的每个占位符名称后跟其对应的数据,就像这样:

with tf.Session() as sess:
    result = sess.run(b,feed_dict={a:3.5})
    print result
Output:
7.0

由于 TensorFlow 中的数据是以多维数组的形式传递的,我们可以通过占位符传递任何类型的张量,以获得简单的乘法操作的结果:

dictionary={a: [ [ [1,2,3],[4,5,6],[7,8,9],[10,11,12] ] , [ [13,14,15],[16,17,18],[19,20,21],[22,23,24] ] ] }
with tf.Session() as sess:
    result = sess.run(b,feed_dict=dictionary)
    print result
Output:
[[[  2\.   4\.   6.]
  [  8\.  10\.  12.]
  [ 14\.  16\.  18.]
  [ 20\.  22\.  24.]]

 [[ 26\.  28\.  30.]
  [ 32\.  34\.  36.]
  [ 38\.  40\.  42.]
  [ 44\.  46\.  48.]]]

操作

操作是表示图中张量的数学运算的节点。这些操作可以是任何类型的函数,比如加法、减法张量,或者可能是激活函数。

tf.matmultf.addtf.nn.sigmoid 是 TensorFlow 中的一些操作。这些类似于 Python 中的函数,但直接作用于张量,每个函数都有特定的功能。

其他操作可以在以下网址找到:www.tensorflow.org/api_guides/python/math_ops

让我们来尝试一些操作:

a = tf.constant([5])
b = tf.constant([2])
c = tf.add(a,b)
d = tf.subtract(a,b)
with tf.Session() as session:
    result = session.run(c)
    print 'c =: %s' % result
    result = session.run(d)
    print 'd =: %s' % result
Output:
c =: [7]
d =: [3]

tf.nn.sigmoid 是一个激活函数:它有点复杂,但这个函数有助于学习模型评估什么样的信息是有用的,什么是无用的。

线性回归模型——构建与训练

根据我们在第二章《数据建模实践——泰坦尼克号示例》中的线性回归解释,数据建模实践——泰坦尼克号示例,我们将依赖这个定义来构建一个简单的线性回归模型。

让我们首先导入实现所需的必要包:

import numpy as np
import tensorflow as tf
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = (10, 6)

让我们定义一个自变量:

input_values = np.arange(0.0, 5.0, 0.1)
input_values
Output:
array([ 0\. ,  0.1,  0.2,  0.3,  0.4,  0.5,  0.6,  0.7,  0.8,  0.9,  1\. ,
        1.1,  1.2,  1.3,  1.4,  1.5,  1.6,  1.7,  1.8,  1.9,  2\. ,  2.1,
        2.2,  2.3,  2.4,  2.5,  2.6,  2.7,  2.8,  2.9,  3\. ,  3.1,  3.2,
        3.3,  3.4,  3.5,  3.6,  3.7,  3.8,  3.9,  4\. ,  4.1,  4.2,  4.3,
        4.4,  4.5,  4.6,  4.7,  4.8,  4.9])
##You can adjust the slope and intercept to verify the changes in the graph
weight=1
bias=0
output = weight*input_values + bias
plt.plot(input_values,output)
plt.ylabel('Dependent Variable')
plt.xlabel('Indepdendent Variable')
plt.show()
Output:

图 11:依赖变量与自变量的可视化

现在,让我们看看这如何转化为 TensorFlow 代码。

使用 TensorFlow 进行线性回归

在第一部分,我们将生成随机数据点并定义线性关系;我们将使用 TensorFlow 来调整并获得正确的参数:

input_values = np.random.rand(100).astype(np.float32)

这个示例中使用的模型方程是:

这个方程没有什么特别之处,它只是我们用来生成数据点的模型。事实上,你可以像稍后一样更改参数。我们添加了一些高斯噪声,使数据点看起来更有趣:

output_values = input_values * 2 + 3
output_values = np.vectorize(lambda y: y + np.random.normal(loc=0.0, scale=0.1))(output_values)

这是数据的一个示例:

list(zip(input_values,output_values))[5:10]
Output:
[(0.25240293, 3.474361759429548), 
(0.946697, 4.980617375175061), 
(0.37582186, 3.650345806087635), 
(0.64025956, 4.271037640404975), 
(0.62555283, 4.37001850440196)]

首先,我们用任何随机猜测初始化变量 ,然后我们定义线性函数:

weight = tf.Variable(1.0)
bias = tf.Variable(0.2)
predicted_vals = weight * input_values + bias

在典型的线性回归模型中,我们最小化我们希望调整的方程的平方误差,减去目标值(即我们拥有的数据),因此我们将要最小化的方程定义为损失。

为了找到损失值,我们使用 tf.reduce_mean()。这个函数计算多维张量的均值,结果可以具有不同的维度:

model_loss = tf.reduce_mean(tf.square(predicted_vals - output_values))

然后,我们定义优化器方法。在这里,我们将使用简单的梯度下降法,学习率为 0.5。

现在,我们将定义图表的训练方法,但我们将使用什么方法来最小化损失呢?答案是 tf.train.GradientDescentOptimizer

.minimize() 函数将最小化优化器的误差函数,从而得到一个更好的模型:

model_optimizer = tf.train.GradientDescentOptimizer(0.5)
train = model_optimizer.minimize(model_loss)

别忘了在执行图表之前初始化变量:

init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init)

现在,我们准备开始优化并运行图表:

train_data = []
for step in range(100):
    evals = sess.run([train,weight,bias])[1:]
    if step % 5 == 0:
       print(step, evals)
       train_data.append(evals)
Output:
(0, [2.5176678, 2.9857566])
(5, [2.4192538, 2.3015416])
(10, [2.5731843, 2.221911])
(15, [2.6890132, 2.1613526])
(20, [2.7763696, 2.1156814])
(25, [2.8422525, 2.0812368])
(30, [2.8919399, 2.0552595])
(35, [2.9294133, 2.0356679])
(40, [2.957675, 2.0208921])
(45, [2.9789894, 2.0097487])
(50, [2.9950645, 2.0013444])
(55, [3.0071881, 1.995006])
(60, [3.0163314, 1.9902257])
(65, [3.0232272, 1.9866205])
(70, [3.0284278, 1.9839015])
(75, [3.0323503, 1.9818509])
(80, [3.0353084, 1.9803041])
(85, [3.0375392, 1.9791379])
(90, [3.039222, 1.9782581])
(95, [3.0404909, 1.9775947])

让我们可视化训练过程,以适应数据点:

print('Plotting the data points with their corresponding fitted line...')
converter = plt.colors
cr, cg, cb = (1.0, 1.0, 0.0)

for f in train_data:

    cb += 1.0 / len(train_data)
    cg -= 1.0 / len(train_data)

    if cb > 1.0: cb = 1.0

    if cg < 0.0: cg = 0.0

    [a, b] = f
    f_y = np.vectorize(lambda x: a*x + b)(input_values)
    line = plt.plot(input_values, f_y)
    plt.setp(line, color=(cr,cg,cb))

plt.plot(input_values, output_values, 'ro')
green_line = mpatches.Patch(color='red', label='Data Points')
plt.legend(handles=[green_line])
plt.show()
Output:

图 12:回归线拟合数据点的可视化

逻辑回归模型——构建与训练

同样根据我们在第二章《数据建模实践——泰坦尼克号示例》中的逻辑回归解释,数据建模实践——泰坦尼克号示例,我们将实现 TensorFlow 中的逻辑回归算法。简而言之,逻辑回归将输入通过逻辑/ sigmoid 函数传递,然后将结果视为概率:

图 13:区分两个线性可分类别,0 和 1

在 TensorFlow 中使用逻辑回归

为了在 TensorFlow 中使用逻辑回归,我们首先需要导入我们将要使用的库。为此,你可以运行以下代码单元:

import tensorflow as tf

import pandas as pd

import numpy as np
import time
from sklearn.datasets import load_iris
from sklearn.cross_validation import train_test_split
import matplotlib.pyplot as plt

接下来,我们将加载我们要使用的数据集。在这种情况下,我们使用内置的鸢尾花数据集。因此,不需要进行任何预处理,我们可以直接开始操作它。我们将数据集分成 xy,然后再分成训练集的 xy 以及测试集的 xy,(伪)随机地:

iris_dataset = load_iris()
iris_input_values, iris_output_values = iris_dataset.data[:-1,:], iris_dataset.target[:-1]
iris_output_values= pd.get_dummies(iris_output_values).values
train_input_values, test_input_values, train_target_values, test_target_values = train_test_split(iris_input_values, iris_output_values, test_size=0.33, random_state=42)

现在,我们定义了 xy。这些占位符将存储我们的鸢尾花数据(包括特征和标签矩阵),并帮助将它们传递到算法的不同部分。你可以把占位符看作是空的壳子,我们将数据插入到这些壳子里。我们还需要给它们指定与数据形状相对应的形状。稍后,我们将通过 feed_dict(数据字典)将数据插入到这些占位符中:

为什么使用占位符?

TensorFlow 的这一特性使得我们可以创建一个接受数据并且知道数据形状的算法,而不需要知道进入的数据量。在训练时,当我们插入 batch 数据时,我们可以轻松调整每次训练步骤中训练样本的数量,而无需改变整个算法:

# numFeatures is the number of features in our input data.
# In the iris dataset, this number is '4'.
num_explanatory_features = train_input_values.shape[1]

# numLabels is the number of classes our data points can be in.
# In the iris dataset, this number is '3'.
num_target_values = train_target_values.shape[1]

# Placeholders
# 'None' means TensorFlow shouldn't expect a fixed number in that dimension
input_values = tf.placeholder(tf.float32, [None, num_explanatory_features]) # Iris has 4 features, so X is a tensor to hold our data.
output_values = tf.placeholder(tf.float32, [None, num_target_values]) # This will be our correct answers matrix for 3 classes.

设置模型的权重和偏置

和线性回归类似,我们需要一个共享的变量权重矩阵用于逻辑回归。我们将 Wb 都初始化为全零的张量。因为我们将要学习 Wb,所以它们的初始值并不重要。这些变量是定义我们回归模型结构的对象,我们可以在训练后保存它们,以便以后重用。

我们定义了两个 TensorFlow 变量作为我们的参数。这些变量将存储我们逻辑回归的权重和偏置,并且在训练过程中会不断更新。

请注意,W 的形状是 [4, 3],因为我们希望将 4 维的输入向量与其相乘,以产生 3 维的证据向量来区分不同的类别。b 的形状是 [3],因此我们可以将它加到输出中。此外,与我们的占位符(本质上是等待数据的空壳)不同,TensorFlow 变量需要用值进行初始化,比如使用零初始化:

#Randomly sample from a normal distribution with standard deviation .01

weights = tf.Variable(tf.random_normal([num_explanatory_features,num_target_values],
                                      mean=0,
                                      stddev=0.01,
                                      name="weights"))

biases = tf.Variable(tf.random_normal([1,num_target_values],
                                   mean=0,
                                   stddev=0.01,
                                   name="biases"))

逻辑回归模型

我们现在定义我们的操作,以便正确地运行逻辑回归。逻辑回归通常被视为一个单一的方程:

然而,为了清晰起见,我们可以将其拆分为三个主要部分:

  • 一个加权特征矩阵乘法操作

  • 对加权特征和偏置项的求和

  • 最后,应用 Sigmoid 函数

因此,您将会发现这些组件被定义为三个独立的操作:

# Three-component breakdown of the Logistic Regression equation.
# Note that these feed into each other.
apply_weights = tf.matmul(input_values, weights, name="apply_weights")
add_bias = tf.add(apply_weights, biases, name="add_bias")
activation_output = tf.nn.sigmoid(add_bias, name="activation")

正如我们之前所看到的,我们将使用的函数是逻辑函数,在应用权重和偏差后将输入数据提供给它。在 TensorFlow 中,这个函数被实现为nn.sigmoid函数。有效地,它将带有偏差的加权输入拟合到 0-100 百分比曲线中,这是我们想要的概率函数。

训练

学习算法是如何搜索最佳权重向量(w)的。这个搜索是一个优化问题,寻找能够优化错误/成本度量的假设。

因此,模型的成本或损失函数将告诉我们我们的模型不好,我们需要最小化这个函数。您可以遵循不同的损失或成本标准。在这个实现中,我们将使用均方误差MSE)作为损失函数。

为了完成最小化损失函数的任务,我们将使用梯度下降算法。

成本函数

在定义我们的成本函数之前,我们需要定义我们将要训练多长时间以及我们应该如何定义学习速率:

#Number of training epochs
num_epochs = 700
# Defining our learning rate iterations (decay)
learning_rate = tf.train.exponential_decay(learning_rate=0.0008,
                                          global_step=1,
                                          decay_steps=train_input_values.shape[0],
                                          decay_rate=0.95,
                                          staircase=True)

# Defining our cost function - Squared Mean Error
model_cost = tf.nn.l2_loss(activation_output - output_values, name="squared_error_cost")
# Defining our Gradient Descent
model_train = tf.train.GradientDescentOptimizer(learning_rate).minimize(model_cost)

现在,是时候通过会话变量执行我们的计算图了。

首先,我们需要使用tf.initialize_all_variables()将我们的权重和偏差初始化为零或随机值。这个初始化步骤将成为我们计算图中的一个节点,当我们将图放入会话中时,操作将运行并创建变量:

# tensorflow session
sess = tf.Session()

# Initialize our variables.
init = tf.global_variables_initializer()
sess.run(init)

#We also want some additional operations to keep track of our model's efficiency over time. We can do this like so:
# argmax(activation_output, 1) returns the label with the most probability
# argmax(output_values, 1) is the correct label
correct_predictions = tf.equal(tf.argmax(activation_output,1),tf.argmax(output_values,1))

# If every false prediction is 0 and every true prediction is 1, the average returns us the accuracy
model_accuracy = tf.reduce_mean(tf.cast(correct_predictions, "float"))

# Summary op for regression output
activation_summary = tf.summary.histogram("output", activation_output)

# Summary op for accuracy
accuracy_summary = tf.summary.scalar("accuracy", model_accuracy)

# Summary op for cost
cost_summary = tf.summary.scalar("cost", model_cost)

# Summary ops to check how variables weights and biases are updating after each iteration to be visualized in TensorBoard
weight_summary = tf.summary.histogram("weights", weights.eval(session=sess))
bias_summary = tf.summary.histogram("biases", biases.eval(session=sess))

merged = tf.summary.merge([activation_summary, accuracy_summary, cost_summary, weight_summary, bias_summary])
writer = tf.summary.FileWriter("summary_logs", sess.graph)

#Now we can define and run the actual training loop, like this:
# Initialize reporting variables

inital_cost = 0
diff = 1
epoch_vals = []
accuracy_vals = []
costs = []

# Training epochs
for i in range(num_epochs):
    if i > 1 and diff < .0001:
       print("change in cost %g; convergence."%diff)
       break

    else:
       # Run training step
       step = sess.run(model_train, feed_dict={input_values: train_input_values, output_values: train_target_values})

       # Report some stats evert 10 epochs
       if i % 10 == 0:
           # Add epoch to epoch_values
           epoch_vals.append(i)

           # Generate the accuracy stats of the model
           train_accuracy, new_cost = sess.run([model_accuracy, model_cost], feed_dict={input_values: train_input_values, output_values: train_target_values})

           # Add accuracy to live graphing variable
           accuracy_vals.append(train_accuracy)

           # Add cost to live graphing variable
           costs.append(new_cost)
>
           # Re-assign values for variables
           diff = abs(new_cost - inital_cost)
           cost = new_cost

           print("Training step %d, accuracy %g, cost %g, cost change %g"%(i, train_accuracy, new_cost, diff))
Output:
Training step 0, accuracy 0.343434, cost 34.6022, cost change 34.6022
Training step 10, accuracy 0.434343, cost 30.3272, cost change 30.3272
Training step 20, accuracy 0.646465, cost 28.3478, cost change 28.3478
Training step 30, accuracy 0.646465, cost 26.6752, cost change 26.6752
Training step 40, accuracy 0.646465, cost 25.2844, cost change 25.2844
Training step 50, accuracy 0.646465, cost 24.1349, cost change 24.1349
Training step 60, accuracy 0.646465, cost 23.1835, cost change 23.1835
Training step 70, accuracy 0.646465, cost 22.3911, cost change 22.3911
Training step 80, accuracy 0.646465, cost 21.7254, cost change 21.7254
Training step 90, accuracy 0.646465, cost 21.1607, cost change 21.1607
Training step 100, accuracy 0.666667, cost 20.677, cost change 20.677
Training step 110, accuracy 0.666667, cost 20.2583, cost change 20.2583
Training step 120, accuracy 0.666667, cost 19.8927, cost change 19.8927
Training step 130, accuracy 0.666667, cost 19.5705, cost change 19.5705
Training step 140, accuracy 0.666667, cost 19.2842, cost change 19.2842
Training step 150, accuracy 0.666667, cost 19.0278, cost change 19.0278
Training step 160, accuracy 0.676768, cost 18.7966, cost change 18.7966
Training step 170, accuracy 0.69697, cost 18.5867, cost change 18.5867
Training step 180, accuracy 0.69697, cost 18.3951, cost change 18.3951
Training step 190, accuracy 0.717172, cost 18.2191, cost change 18.2191
Training step 200, accuracy 0.717172, cost 18.0567, cost change 18.0567
Training step 210, accuracy 0.737374, cost 17.906, cost change 17.906
Training step 220, accuracy 0.747475, cost 17.7657, cost change 17.7657
Training step 230, accuracy 0.747475, cost 17.6345, cost change 17.6345
Training step 240, accuracy 0.757576, cost 17.5113, cost change 17.5113
Training step 250, accuracy 0.787879, cost 17.3954, cost change 17.3954
Training step 260, accuracy 0.787879, cost 17.2858, cost change 17.2858
Training step 270, accuracy 0.787879, cost 17.182, cost change 17.182
Training step 280, accuracy 0.787879, cost 17.0834, cost change 17.0834
Training step 290, accuracy 0.787879, cost 16.9895, cost change 16.9895
Training step 300, accuracy 0.79798, cost 16.8999, cost change 16.8999
Training step 310, accuracy 0.79798, cost 16.8141, cost change 16.8141
Training step 320, accuracy 0.79798, cost 16.732, cost change 16.732
Training step 330, accuracy 0.79798, cost 16.6531, cost change 16.6531
Training step 340, accuracy 0.808081, cost 16.5772, cost change 16.5772
Training step 350, accuracy 0.818182, cost 16.5041, cost change 16.5041
Training step 360, accuracy 0.838384, cost 16.4336, cost change 16.4336
Training step 370, accuracy 0.838384, cost 16.3655, cost change 16.3655
Training step 380, accuracy 0.838384, cost 16.2997, cost change 16.2997
Training step 390, accuracy 0.838384, cost 16.2359, cost change 16.2359
Training step 400, accuracy 0.848485, cost 16.1741, cost change 16.1741
Training step 410, accuracy 0.848485, cost 16.1141, cost change 16.1141
Training step 420, accuracy 0.848485, cost 16.0558, cost change 16.0558
Training step 430, accuracy 0.858586, cost 15.9991, cost change 15.9991
Training step 440, accuracy 0.858586, cost 15.944, cost change 15.944
Training step 450, accuracy 0.858586, cost 15.8903, cost change 15.8903
Training step 460, accuracy 0.868687, cost 15.8379, cost change 15.8379
Training step 470, accuracy 0.878788, cost 15.7869, cost change 15.7869
Training step 480, accuracy 0.878788, cost 15.7371, cost change 15.7371
Training step 490, accuracy 0.878788, cost 15.6884, cost change 15.6884
Training step 500, accuracy 0.878788, cost 15.6409, cost change 15.6409
Training step 510, accuracy 0.878788, cost 15.5944, cost change 15.5944
Training step 520, accuracy 0.878788, cost 15.549, cost change 15.549
Training step 530, accuracy 0.888889, cost 15.5045, cost change 15.5045
Training step 540, accuracy 0.888889, cost 15.4609, cost change 15.4609
Training step 550, accuracy 0.89899, cost 15.4182, cost change 15.4182
Training step 560, accuracy 0.89899, cost 15.3764, cost change 15.3764
Training step 570, accuracy 0.89899, cost 15.3354, cost change 15.3354
Training step 580, accuracy 0.89899, cost 15.2952, cost change 15.2952
Training step 590, accuracy 0.909091, cost 15.2558, cost change 15.2558
Training step 600, accuracy 0.909091, cost 15.217, cost change 15.217
Training step 610, accuracy 0.909091, cost 15.179, cost change 15.179
Training step 620, accuracy 0.909091, cost 15.1417, cost change 15.1417
Training step 630, accuracy 0.909091, cost 15.105, cost change 15.105
Training step 640, accuracy 0.909091, cost 15.0689, cost change 15.0689
Training step 650, accuracy 0.909091, cost 15.0335, cost change 15.0335
Training step 660, accuracy 0.909091, cost 14.9987, cost change 14.9987
Training step 670, accuracy 0.909091, cost 14.9644, cost change 14.9644
Training step 680, accuracy 0.909091, cost 14.9307, cost change 14.9307
Training step 690, accuracy 0.909091, cost 14.8975, cost change 14.8975

现在,是时候看看我们训练好的模型在iris数据集上的表现了,让我们将训练好的模型与测试集进行测试:

# test the model against the test set
print("final accuracy on test set: %s" %str(sess.run(model_accuracy,
                                                    feed_dict={input_values: test_input_values,
                                                               output_values: test_target_values}))
Output:
final accuracy on test set: 0.9

在测试集上获得 0.9 的准确率真的很好,您可以通过更改 epochs 的数量尝试获得更好的结果。

摘要

在本章中,我们对神经网络进行了基本解释,并讨论了多层神经网络的需求。我们还涵盖了 TensorFlow 的计算图模型,并举了一些基本的例子,如线性回归和逻辑回归。

接下来,我们将通过更高级的例子,展示如何使用 TensorFlow 构建像手写字符识别之类的东西。我们还将解决传统机器学习中已经替代特征工程的核心架构工程思想。

第六章:深度前馈神经网络 - 实现数字分类

前馈神经网络FNN)是一种特殊类型的神经网络,其中神经元之间的连接/链接不形成循环。因此,它不同于我们在本书后面将学习的其他神经网络架构(如递归神经网络)。FNN 是广泛使用的架构,也是最早和最简单的神经网络类型。

本章中,我们将讲解典型的前馈神经网络(FNN)架构,并使用 TensorFlow 库进行实现。掌握这些概念后,我们将通过一个实际的数字分类示例进行说明。这个示例的问题是,给定一组包含手写数字的图像,你如何将这些图像分类为 10 个不同的类别(0-9)

本章将涵盖以下主题:

  • 隐藏单元和架构设计

  • MNIST 数据集分析

  • 数字分类 - 模型构建与训练

隐藏单元和架构设计

在下一节中,我们将回顾人工神经网络;它们在分类任务中表现良好,例如分类手写数字。

假设我们有如下所示的网络,参见图 1

图 1:具有一个隐藏层的简单 FNN

如前所述,这个网络中最左侧的层被称为输入层,这一层内的神经元被称为输入神经元。最右侧的层或输出层包含输出神经元,或者在本例中,仅包含一个输出神经元。中间的层被称为隐藏层,因为这一层中的神经元既不是输入神经元,也不是输出神经元。术语“隐藏”可能听起来有些神秘——我第一次听到这个词时,觉得它一定有某种深奥的哲学或数学意义——但它实际上仅仅意味着既不是输入也不是输出。就这么简单。前面的网络只有一个隐藏层,但有些网络有多个隐藏层。例如,下面这个四层的网络就有两个隐藏层:

图 2:具有更多隐藏层的人工神经网络

输入层、隐藏层和输出层的架构非常简单明了。例如,我们通过一个实际的例子来看一下,如何判断一张手写图像是否包含数字 9。

首先,我们将输入图像的像素传递给输入层;例如,在 MNIST 数据集中,我们有单色图像。每一张图像的尺寸为 28×28,因此我们需要在输入层中有 28×28 = 784 个神经元来接收这个输入图像。

在输出层,我们只需要一个神经元,该神经元输出一个概率(或得分),表示该图像是否包含数字 9。例如,输出值大于 0.5 表示该图像包含数字 9,如果小于 0.5,则表示该输入图像不包含数字 9。

所以这种类型的网络,其中一个层的输出作为输入传递给下一层,称为 FNN(前馈神经网络)。这种层与层之间的顺序性意味着网络中没有循环。

MNIST 数据集分析

在这一部分,我们将亲自动手实现一个手写图像的分类器。这种实现可以被看作是神经网络的 Hello world!

MNIST 是一个广泛使用的数据集,用于基准测试机器学习技术。该数据集包含一组手写数字,像这里展示的这些:

图 3:MNIST 数据集中的样本数字

所以,数据集包括手写图像及其对应的标签。

在这一部分,我们将基于这些图像训练一个基本的模型,目标是识别输入图像中的手写数字。

此外,您会发现我们可以通过非常少的代码行来完成这个分类任务,但这个实现的核心思想是理解构建神经网络解决方案的基本组件。此外,我们还将涵盖在此实现中神经网络的主要概念。

MNIST 数据

MNIST 数据托管在 Yann LeCun 的网站上 (yann.lecun.com/exdb/mnist/)。幸运的是,TensorFlow 提供了一些辅助函数来下载数据集,所以让我们先用以下两行代码下载数据集:

from tensorflow.examples.tutorials.mnist import input_data
mnist_dataset = input_data.read_data_sets("MNIST_data/", one_hot=True)

MNIST 数据分为三部分:55,000 个训练数据点(mnist.train),10,000 个测试数据点(mnist.test),和 5,000 个验证数据点(mnist.validation)。这种划分非常重要;在机器学习中,必须有独立的数据集,我们不能从这些数据中学习,以确保我们的学习结果具有泛化能力!

如前所述,每个 MNIST 样本有两个部分:一个手写数字的图像和它对应的标签。训练集和测试集都包含图像及其相应的标签。例如,训练图像是 mnist.train.images,训练标签是 mnist.train.labels

每张图片的尺寸为 28 像素 x 28 像素。我们可以将其解释为一个包含数字的大数组:

图 4:MNIST 数字的矩阵表示(强度值)

为了将这张像素值矩阵输入到神经网络的输入层,我们需要将矩阵展平为一个包含 784 个值的向量。因此,数据集的最终形状将是一个 784 维的向量空间。

结果是 mnist.train.images 是一个形状为 (55000, 784) 的张量。第一个维度是图像列表的索引,第二个维度是每个图像中每个像素的索引。张量中的每个条目是一个特定图像中特定像素的像素强度,值在 0 到 1 之间:

图 5:MNIST 数据分析

如前所述,数据集中的每个图像都有一个对应的标签,范围从 0 到 9。

对于本实现,我们将标签编码为 one-hot 向量。One-hot 向量是一个除了表示该向量所代表的数字索引位置为 1 之外,其它位置全为 0 的向量。例如,3 将是 [0,0,0,1,0,0,0,0,0,0]。因此,mnist.train.labels 是一个形状为 (55000, 10) 的浮点数组:

图 6:MNIST 数据分析

数字分类 – 模型构建与训练

现在,让我们开始构建我们的模型。所以,我们的数据集有 10 个类别,分别是 0 到 9,目标是将任何输入图像分类为其中一个类别。我们不会仅仅给出输入图像属于哪个类别的硬性判断,而是将输出一个包含 10 个可能值的向量(因为我们有 10 个类别)。它将表示每个数字从 0 到 9 为输入图像的正确类别的概率。

例如,假设我们输入一个特定的图像。模型可能 70% 确定这个图像是 9,10% 确定这个图像是 8,依此类推。所以,我们将在这里使用 softmax 回归,它将产生介于 0 和 1 之间的值。

Softmax 回归有两个步骤:首先我们将输入属于某些类别的证据加总,然后将这些证据转换为概率。

为了统计某个图像属于特定类别的证据,我们对像素强度进行加权求和。如果某个像素强度高则反映该图像不属于该类别,则权重为负;如果它支持该图像属于该类别,则权重为正。

图 7 显示了模型为每个类别学到的权重。红色代表负权重,蓝色代表正权重:

图 7:模型为每个 MNIST 类别学到的权重

我们还添加了一些额外的证据,称为 偏置。基本上,我们希望能够说某些事情在不依赖于输入的情况下更有可能。结果是,给定输入 x 时,类别 i 的证据为:

其中:

  • W[i] 是权重

  • b[i] 是类别 i 的偏置

  • j 是用来对输入图像 x 中的像素求和的索引。

然后,我们使用 softmax 函数将证据总和转换为我们的预测概率 y

y = softmax(证据)

在这里,softmax 作为激活或连接函数,塑造了我们线性函数的输出形式,我们希望它是一个 10 类的概率分布(因为我们有 10 个可能的类,范围是 0 到 9)。你可以将其看作是将证据的统计数据转换为输入属于每个类的概率。它的定义是:

softmax(证据) = 归一化(exp(证据))

如果你展开这个方程,你会得到:

但通常更有帮助的是按第一种方式理解 softmax:对其输入进行指数运算,然后进行归一化。指数运算意味着多一个证据单位会使任何假设的权重指数级增长。反过来,减少一个证据单位意味着该假设的权重会减少。没有任何假设的权重会为零或负值。然后,softmax 对这些权重进行归一化,使它们的和为 1,形成一个有效的概率分布。

你可以把我们的 softmax 回归想象成以下的样子,尽管它会有更多的 x's。对于每个输出,我们计算 x's 的加权和,加入偏置,然后应用 softmax:

图 8:softmax 回归的可视化

如果我们将其写成方程式,我们得到:

图 9:softmax 回归的方程表示

我们可以使用向量表示法来处理这个过程。这意味着我们将其转换为矩阵乘法和向量加法。这对于计算效率和可读性非常有帮助:

图 10:softmax 回归方程的向量化表示

更简洁地,我们可以写成:

y = softmax(W[x] + b)

现在,让我们将其转换为 TensorFlow 可以使用的形式。

数据分析

那么,让我们开始实现我们的分类器。我们首先导入实现所需的包:

import tensorflow as tf
import matplotlib.pyplot as plt
import numpy as np
import random as ran

接下来,我们将定义一些辅助函数,以便从我们下载的原始数据集中进行子集选择:

#Define some helper functions 
# to assign the size of training and test data we will take from MNIST dataset
def train_size(size):
    print ('Total Training Images in Dataset = ' + str(mnist_dataset.train.images.shape))
    print ('############################################')
    input_values_train = mnist_dataset.train.images[:size,:]
    print ('input_values_train Samples Loaded = ' + str(input_values_train.shape))
    target_values_train = mnist_dataset.train.labels[:size,:]
    print ('target_values_train Samples Loaded = ' + str(target_values_train.shape))
    return input_values_train, target_values_train

def test_size(size):
    print ('Total Test Samples in MNIST Dataset = ' + str(mnist_dataset.test.images.shape))
    print ('############################################')
    input_values_test = mnist_dataset.test.images[:size,:]
    print ('input_values_test Samples Loaded = ' + str(input_values_test.shape))
    target_values_test = mnist_dataset.test.labels[:size,:]
    print ('target_values_test Samples Loaded = ' + str(target_values_test.shape))
    return input_values_test, target_values_test

此外,我们还将定义两个辅助函数,用于显示数据集中的特定数字,或者甚至显示某个图像子集的平铺版本:

#Define a couple of helper functions for digit images visualization
def visualize_digit(ind):
    print(target_values_train[ind])
    target = target_values_train[ind].argmax(axis=0)
    true_image = input_values_train[ind].reshape([28,28])
    plt.title('Sample: %d Label: %d' % (ind, target))
    plt.imshow(true_image, cmap=plt.get_cmap('gray_r'))
    plt.show()

def visualize_mult_imgs_flat(start, stop):
    imgs = input_values_train[start].reshape([1,784])
    for i in range(start+1,stop):
        imgs = np.concatenate((imgs, input_values_train[i].reshape([1,784])))
    plt.imshow(imgs, cmap=plt.get_cmap('gray_r'))
    plt.show()

现在,让我们开始正式处理数据集。我们将定义我们希望从原始数据集中加载的训练和测试示例。

现在,我们将开始构建和训练我们的模型。首先,我们定义变量,指定我们希望加载的训练和测试示例的数量。目前,我们将加载所有数据,但稍后会更改这个值以节省资源:

input_values_train, target_values_train = train_size(55000)

Output:
Total Training Images in Dataset = (55000, 784)
############################################
input_values_train Samples Loaded = (55000, 784)
target_values_train Samples Loaded = (55000, 10)

所以现在,我们有一个包含 55,000 个手写数字样本的训练集,每个样本是 28×28 像素的图像,经过展平成为 784 维的向量。我们还拥有这些样本对应的标签,采用 one-hot 编码格式。

target_values_train数据是所有input_values_train样本的关联标签。在以下示例中,数组代表数字 7 的独热编码格式:

图 11:数字 7 的独热编码

所以让我们从数据集中随机选择一张图片并看看它是什么样子的,我们将使用之前的辅助函数来显示数据集中的随机数字:

visualize_digit(ran.randint(0, input_values_train.shape[0]))

Output:

图 12:display_digit方法的输出数字

我们还可以使用之前定义的辅助函数来可视化一堆展平后的图片。展平向量中的每个值代表一个像素的强度,因此可视化这些像素将是这样的:

visualize_mult_imgs_flat(0,400)

图 13:前 400 个训练样本

构建模型

到目前为止,我们还没有开始为这个分类器构建计算图。让我们先创建一个会负责执行我们将要构建的计算图的会话变量:

sess = tf.Session()

接下来,我们将定义我们模型的占位符,这些占位符将用于将数据传递到计算图中:

input_values = tf.placeholder(tf.float32, shape=[None, 784]

当我们在占位符的第一个维度指定None时,这意味着该占位符可以接受任意数量的样本。在这种情况下,我们的占位符可以接收任何数量的样本,每个样本有一个784的值。

现在,我们需要定义另一个占位符来传入图片标签。我们将在之后使用这个占位符来将模型的预测与图像的实际标签进行比较:

output_values = tf.placeholder(tf.float32, shape=[None, 10])

接下来,我们将定义weightsbiases。这两个变量将成为我们网络的可训练参数,它们将是进行未知数据预测时所需的唯一两个变量:

weights = tf.Variable(tf.zeros([784,10]))
biases = tf.Variable(tf.zeros([10]))

我喜欢把这些weights看作是每个数字的 10 张备忘单。这类似于老师用备忘单来给多选考试打分。

现在我们将定义我们的 softmax 回归,它是我们的分类器函数。这个特殊的分类器叫做多项式逻辑回归,我们通过将数字的展平版本与权重相乘然后加上偏差来做出预测:

softmax_layer = tf.nn.softmax(tf.matmul(input_values,weights) + biases)

首先,让我们忽略 softmax,看看 softmax 函数内部的内容。matmul是 TensorFlow 用于矩阵乘法的函数。如果你了解矩阵乘法(en.wikipedia.org/wiki/Matrix_multiplication),你就会明白它是如何正确计算的,并且:

将导致一个由训练样本数(m) × 类别数(n)的矩阵:

图 13:简单的矩阵乘法。

你可以通过评估softmax_layer来确认这一点:

print(softmax_layer)
Output:
Tensor("Softmax:0", shape=(?, 10), dtype=float32)

现在,让我们用之前定义的计算图,使用训练集中的三个样本来进行实验,看看它是如何工作的。为了执行计算图,我们需要使用之前定义的会话变量。并且,我们需要使用tf.global_variables_initializer()来初始化变量。

现在,我们仅向计算图输入三个样本进行实验:

input_values_train, target_values_train = train_size(3)
sess.run(tf.global_variables_initializer())
#If using TensorFlow prior to 0.12 use:
#sess.run(tf.initialize_all_variables())
print(sess.run(softmax_layer, feed_dict={input_values: input_values_train}))
Output:

[[ 0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1]
 [ 0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1]
 [ 0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1]]

在这里,你可以看到模型对于输入的三个训练样本的预测结果。目前,模型还没有学到任何关于我们任务的东西,因为我们还没有经过训练过程,所以它只是输出每个数字为输入样本正确类别的 10% 概率。

如前所述,softmax 是一种激活函数,它将输出压缩到 0 到 1 之间,TensorFlow 对 softmax 的实现确保单个输入样本的所有概率加起来为 1。

让我们稍微实验一下 TensorFlow 的 softmax 函数:

sess.run(tf.nn.softmax(tf.zeros([4])))
sess.run(tf.nn.softmax(tf.constant([0.1, 0.005, 2])))

Output:
array([0.11634309, 0.10579926, 0.7778576 ], dtype=float32)

接下来,我们需要为这个模型定义损失函数,来衡量分类器在尝试为输入图像分配类别时的好坏。模型的准确度是通过比较数据集中实际的值与模型输出的预测值来计算的。

目标是减少实际值和预测值之间的误分类。

交叉熵的定义为:

其中:

  • y是我们预测的概率分布

  • y'是实际分布(带有数字标签的独热编码向量)

在某种粗略的意义上,交叉熵衡量了我们预测值在描述实际输入时的低效程度。

我们可以实现交叉熵函数:

model_cross_entropy = tf.reduce_mean(-tf.reduce_sum(output_values * tf.log(softmax_layer), reduction_indices=[1]))

这个函数对所有从 softmax_layer(其值在 0 到 1 之间)得到的预测取对数,并按元素逐个与示例的真实值 output_values 相乘(en.wikipedia.org/wiki/Hadamard_product_%28matrices%29)。如果每个值的 log 函数接近零,它将使值变成一个大负数(-np.log(0.01) = 4.6);如果接近一,它将使值变成一个小负数(-np.log(0.99) = 0.1):

图 15:Y = log(x) 的可视化

本质上,如果预测结果自信地错误,我们会用一个非常大的数字来惩罚分类器;如果预测结果自信地正确,我们则用一个非常小的数字来惩罚。

这里是一个简单的 Python 示例,展示了一个对数字为 3 的预测非常自信的 softmax 预测:

j = [0.03, 0.03, 0.01, 0.9, 0.01, 0.01, 0.0025,0.0025, 0.0025, 0.0025]

让我们创建一个值为 3 的数组标签作为真实值,以便与 softmax 函数进行比较:

k = [0,0,0,1,0,0,0,0,0,0]

你能猜到我们的损失函数给出的值是什么吗?你能看到 j 的对数如何用一个大的负数惩罚错误答案吗?试试这个来理解:

-np.log(j)
-np.multiply(np.log(j),k)

当它们全部加起来时,这将返回九个零和 0.1053 的值;我们可以认为这是一个很好的预测。注意当我们对实际上是 2 的预测做出同样的预测时会发生什么:

k = [0,0,1,0,0,0,0,0,0,0]
np.sum(-np.multiply(np.log(j),k))

现在,我们的cross_entropy函数给出了 4.6051,显示了一个严重惩罚的、预测不良的预测。由于分类器非常确信它是 3,而实际上是 2,因此受到了严重的惩罚。

接下来,我们开始训练我们的分类器。为了训练它,我们必须开发适当的 W 和 b 的值,以便给出尽可能低的损失。

现在,如果我们希望,我们可以为训练分配自定义变量。以下所有大写的值都可以更改和搞砸。事实上,我鼓励这样做!首先,使用这些值,然后注意当您使用太少的训练示例或学习率过高或过低时会发生什么:

input_values_train, target_values_train = train_size(5500)
input_values_test, target_values_test = test_size(10000)
learning_rate = 0.1
num_iterations = 2500

现在,我们可以初始化所有变量,以便它们可以被我们的 TensorFlow 图使用:

init = tf.global_variables_initializer()
#If using TensorFlow prior to 0.12 use:
#init = tf.initialize_all_variables()
sess.run(init)

接下来,我们需要使用梯度下降算法训练分类器。因此,我们首先定义我们的训练方法和一些用于测量模型准确性的变量。变量train将执行梯度下降优化器,选择一个学习率来最小化模型损失函数model_cross_entropy

train = tf.train.GradientDescentOptimizer(learning_rate).minimize(model_cross_entropy)
model_correct_prediction = tf.equal(tf.argmax(softmax_layer,1), tf.argmax(output_values,1))
model_accuracy = tf.reduce_mean(tf.cast(model_correct_prediction, tf.float32))

模型训练

现在,我们将定义一个循环,它将迭代num_iterations次。对于每个循环,它都会运行训练,使用feed_dictinput_values_traintarget_values_train中提供值。

为了计算准确性,它将测试模型对input_values_test中的未见数据的表现:

for i in range(num_iterations+1):
    sess.run(train, feed_dict={input_values: input_values_train, output_values: target_values_train})
    if i%100 == 0:
        print('Training Step:' + str(i) + ' Accuracy = ' + str(sess.run(model_accuracy, feed_dict={input_values: input_values_test, output_values: target_values_test})) + ' Loss = ' + str(sess.run(model_cross_entropy, {input_values: input_values_train, output_values: target_values_train})))

Output:
Training Step:0 Accuracy = 0.5988 Loss = 2.1881988
Training Step:100 Accuracy = 0.8647 Loss = 0.58029664
Training Step:200 Accuracy = 0.879 Loss = 0.45982164
Training Step:300 Accuracy = 0.8866 Loss = 0.40857208
Training Step:400 Accuracy = 0.8904 Loss = 0.37808096
Training Step:500 Accuracy = 0.8943 Loss = 0.35697535
Training Step:600 Accuracy = 0.8974 Loss = 0.34104997
Training Step:700 Accuracy = 0.8984 Loss = 0.32834956
Training Step:800 Accuracy = 0.9 Loss = 0.31782663
Training Step:900 Accuracy = 0.9005 Loss = 0.30886236
Training Step:1000 Accuracy = 0.9009 Loss = 0.3010645
Training Step:1100 Accuracy = 0.9023 Loss = 0.29417014
Training Step:1200 Accuracy = 0.9029 Loss = 0.28799513
Training Step:1300 Accuracy = 0.9033 Loss = 0.28240603
Training Step:1400 Accuracy = 0.9039 Loss = 0.27730304
Training Step:1500 Accuracy = 0.9048 Loss = 0.27260992
Training Step:1600 Accuracy = 0.9057 Loss = 0.26826677
Training Step:1700 Accuracy = 0.9062 Loss = 0.2642261
Training Step:1800 Accuracy = 0.9061 Loss = 0.26044932
Training Step:1900 Accuracy = 0.9063 Loss = 0.25690478
Training Step:2000 Accuracy = 0.9066 Loss = 0.2535662
Training Step:2100 Accuracy = 0.9072 Loss = 0.25041154
Training Step:2200 Accuracy = 0.9073 Loss = 0.24742197
Training Step:2300 Accuracy = 0.9071 Loss = 0.24458146
Training Step:2400 Accuracy = 0.9066 Loss = 0.24187621
Training Step:2500 Accuracy = 0.9067 Loss = 0.23929419

注意,损失在接近尾声时仍在减小,但我们的准确率略有下降!这表明我们仍然可以最小化我们的损失,从而在训练数据上最大化准确率,但这可能无助于预测用于测量准确性的测试数据。这也被称为过拟合(不具有泛化性)。使用默认设置,我们获得了约 91%的准确率。如果我想欺骗以获得 94%的准确率,我本可以将测试示例设置为 100。这显示了没有足够的测试示例可能会给您一个偏见的准确性感觉。

请记住,这种方式计算我们分类器的性能非常不准确。但是,出于学习和实验的目的,我们特意这样做了。理想情况下,当使用大型数据集进行训练时,您应该一次使用小批量的训练数据,而不是全部一起。

这是有趣的部分。现在我们已经计算出了我们的权重备忘单,我们可以用以下代码创建一个图表:

for i in range(10):
    plt.subplot(2, 5, i+1)
    weight = sess.run(weights)[:,i]
    plt.title(i)
    plt.imshow(weight.reshape([28,28]), cmap=plt.get_cmap('seismic'))
    frame = plt.gca()
    frame.axes.get_xaxis().set_visible(False)
    frame.axes.get_yaxis().set_visible(False)

图 15:我们权重的可视化从 0 到 9

上图显示了 0 到 9 的模型权重,这是我们分类器最重要的一部分。所有这些机器学习的工作都是为了找出最优的权重。一旦根据优化标准计算出这些权重,你就拥有了备忘单,并且可以轻松地利用学习到的权重找到答案。

学到的模型通过比较输入数字样本与红色和蓝色权重的相似度或差异来做出预测。红色越深,命中越好;白色表示中立,蓝色表示未命中。

现在,让我们使用备忘单,看看我们的模型在其上的表现:

input_values_train, target_values_train = train_size(1)
visualize_digit(0)

Output:
Total Training Images in Dataset = (55000, 784)
############################################
input_values_train Samples Loaded = (1, 784)
target_values_train Samples Loaded = (1, 10)
[0\. 0\. 0\. 0\. 0\. 0\. 0\. 1\. 0\. 0.]

让我们看看我们的 softmax 预测器:

answer = sess.run(softmax_layer, feed_dict={input_values: input_values_train})
print(answer)

上述代码会给我们一个 10 维向量,每一列包含一个概率:

[[2.1248012e-05 1.1646927e-05 8.9631692e-02 1.9201526e-02 8.2086492e-04
  1.2516821e-05 3.8538201e-05 8.5374612e-01 6.9188857e-03 2.9596921e-02]]

我们可以使用argmax函数来找出最有可能的数字作为我们输入图像的正确分类:

answer.argmax()

Output:
7

现在,我们从网络中得到了一个正确的分类结果。

让我们运用我们的知识定义一个辅助函数,能够从数据集中随机选择一张图像,并将模型应用于其上进行测试:

def display_result(ind):

    # Loading a training sample
    input_values_train = mnist_dataset.train.images[ind,:].reshape(1,784)
    target_values_train = mnist_dataset.train.labels[ind,:]

    # getting the label as an integer instead of one-hot encoded vector
    label = target_values_train.argmax()

    # Getting the prediction as an integer
    prediction = sess.run(softmax_layer, feed_dict={input_values: input_values_train}).argmax()
    plt.title('Prediction: %d Label: %d' % (prediction, label))
    plt.imshow(input_values_train.reshape([28,28]), cmap=plt.get_cmap('gray_r'))
    plt.show()

现在,试试看:

display_result(ran.randint(0, 55000))

Output:

我们再次得到了一个正确的分类结果!

总结

在本章中,我们介绍了用于数字分类任务的 FNN(前馈神经网络)的基本实现。我们还回顾了神经网络领域中使用的术语。

接下来,我们将构建一个更复杂的数字分类模型,使用一些现代最佳实践和技巧来提升模型的表现。

第七章:卷积神经网络简介

在数据科学中,卷积神经网络CNN)是一种特定的深度学习架构,它使用卷积操作来提取输入图像的相关解释特征。CNN 层以前馈神经网络的方式连接,同时使用此卷积操作来模拟人类大脑在试图识别物体时的工作方式。个别皮层神经元对在一个限制区域内的刺激做出反应,这个区域被称为感受野。特别地,生物医学成像问题有时可能会很有挑战性,但在本章中,我们将看到如何使用 CNN 来发现图像中的模式。

本章将涵盖以下主题:

  • 卷积操作

  • 动机

  • CNN 的不同层

  • CNN 基本示例:MNIST 数字分类

卷积操作

CNN 在计算机视觉领域得到了广泛应用,并且它们在很多方面超越了我们一直在使用的传统计算机视觉技术。CNN 结合了著名的卷积操作和神经网络,因此得名卷积神经网络。因此,在深入探讨 CNN 的神经网络部分之前,我们将介绍卷积操作并了解它的工作原理。

卷积操作的主要目的是从图像中提取信息或特征。任何图像都可以看作是一个值矩阵,其中矩阵中的特定值组将形成一个特征。卷积操作的目的是扫描这个矩阵,尝试提取与该图像相关或具有解释性的特征。例如,考虑一个 5x5 的图像,其对应的强度或像素值显示为零和一:

图 9.1:像素值矩阵

并考虑以下 3 x 3 的矩阵:

图 9.2:像素值矩阵

我们可以使用 3 x 3 的卷积核对 5 x 5 的图像进行卷积,方法如下:

图 9.3:卷积操作。输出矩阵称为卷积特征或特征图

上述图可以总结如下。为了使用 3 x 3 的卷积核对原始 5 x 5 图像进行卷积,我们需要执行以下操作:

  • 使用橙色矩阵扫描原始绿色图像,每次只移动 1 像素(步幅)

  • 对于每个橙色图像的位置,我们在橙色矩阵和绿色矩阵中对应的像素值之间执行逐元素相乘操作

  • 将这些逐元素相乘的结果加起来得到一个单一整数,这个整数将构成输出粉色矩阵中的单一值。

如前图所示,橙色的 3 x 3 矩阵每次只对原始绿色图像的一个部分进行操作(步幅),或者它每次只看到图像的一部分。

那么,让我们将前面的解释放到 CNN 术语的背景下:

  • 橙色的 3 x 3 矩阵被称为特征检测器滤波器

  • 输出的粉色矩阵,其中包含逐元素相乘的结果,称为特征图

因为我们是通过核与原始输入图像中对应像素的逐元素相乘来获得特征图,所以改变核或滤波器的值每次都会生成不同的特征图。

因此,我们可能会认为,在卷积神经网络的训练过程中,我们需要自己确定特征检测器的值,但事实并非如此。CNN 在学习过程中自动确定这些值。所以,如果我们有更多的滤波器,就意味着我们可以从图像中提取更多的特征。

在进入下一部分之前,让我们介绍一些在 CNN 上下文中通常使用的术语:

  • 步幅:我们之前简要提到了这个术语。一般来说,步幅是指我们在卷积输入矩阵时,特征检测器或滤波器在输入矩阵上移动的像素数。例如,步幅为 1 意味着每次移动一个像素,而步幅为 2 意味着每次移动两个像素。步幅越大,生成的特征图就越小。

  • 零填充:如果我们想包含输入图像的边缘像素,那么部分滤波器将超出输入图像的范围。零填充通过在输入矩阵的边缘周围填充零来解决这个问题。

动机

传统的计算机视觉技术用于执行大多数计算机视觉任务,如物体检测和分割。尽管这些传统计算机视觉技术的性能不错,但始终无法接近实时使用的要求,例如自动驾驶汽车。2012 年,Alex Krizhevsky 推出了 CNN,凭借其在 ImageNet 竞赛中的突破性表现,将物体分类错误率从 26% 降至 15%。从那时起,CNN 被广泛应用,并且发现了不同的变种。它甚至在 ImageNet 竞赛中超越了人类分类错误,如下图所示:

图 9.4:随着时间推移的分类错误,其中人类级别的错误用红色标出

CNN 的应用

自从 CNN 在计算机视觉甚至自然语言处理的不同领域取得突破以来,大多数公司已经将这一深度学习解决方案集成到他们的计算机视觉生态系统中。例如,谷歌在其图像搜索引擎中使用该架构,Facebook 则在自动标记等功能中使用它:

图 9.5:典型的用于物体识别的 CNN 一般架构

CNN 之所以能取得突破,正是因为它们的架构,直观地使用卷积操作从图像中提取特征。稍后你会发现,这与人脑的工作方式非常相似。

CNN 的不同层

典型的 CNN 架构由多个执行不同任务的层组成,如上图所示。在本节中,我们将详细了解它们,并看到将所有这些层以特定方式连接起来的好处,这使得计算机视觉取得了这样的突破。

输入层

这是任何 CNN 架构中的第一层。所有后续的卷积层和池化层都期望输入以特定格式出现。输入变量将是张量,具有以下形状:

[batch_size, image_width, image_height, channels]

这里:

  • batch_size是从原始训练集中的一个随机样本,用于应用随机梯度下降时。

  • image_width是输入到网络中的图像宽度。

  • image_height是输入到网络中的图像高度。

  • channels是输入图像的颜色通道数。这个数字对于 RGB 图像可能是 3,对于二值图像则是 1。

例如,考虑我们著名的 MNIST 数据集。假设我们将使用 CNN 进行数字分类,使用这个数据集。

如果数据集由 28 x 28 像素的单色图像组成,如 MNIST 数据集,那么我们输入层所需的形状如下:

[batch_size, 28, 28, 1].

为了改变输入特征的形状,我们可以执行以下重塑操作:

input_layer = tf.reshape(features["x"], [-1, 28, 28, 1])

如你所见,我们已经将批量大小指定为-1,这意味着这个数字应根据特征中的输入值动态确定。通过这样做,我们将能够通过控制批量大小来微调我们的 CNN 模型。

作为重塑操作的示例,假设我们将输入样本分成五个一批,并且我们的特征["x"]数组将包含 3,920 个输入图像的values(),其中该数组的每个值对应于图像中的一个像素。对于这种情况,输入层将具有以下形状:

[5, 28, 28, 1]

卷积步骤

如前所述,卷积步骤得名于卷积操作。进行这些卷积步骤的主要目的是从输入图像中提取特征,然后将这些特征输入到线性分类器中。

在自然图像中,特征可能出现在图像的任何位置。例如,边缘可能出现在图像的中间或角落,因此堆叠一系列卷积步骤的整个目的是能够在图像的任何地方检测到这些特征。

在 TensorFlow 中定义卷积步骤非常简单。例如,如果我们想对输入层应用 20 个大小为 5x5 的滤波器,并使用 ReLU 激活函数,那么可以使用以下代码来实现:

conv_layer1 = tf.layers.conv2d(
 inputs=input_layer,
 filters=20,
 kernel_size=[5, 5],
 padding="same",
 activation=tf.nn.relu)

这个conv2d函数的第一个参数是我们在前面的代码中定义的输入层,它具有合适的形状,第二个参数是滤波器参数,指定要应用于图像的滤波器数量,滤波器数量越多,从输入图像中提取的特征就越多。第三个参数是kernel_size,表示滤波器或特征探测器的大小。padding 参数指定了使用零填充的方法,这里我们使用"same"来给输入图像的角落像素添加零填充。最后一个参数指定了应该应用于卷积操作输出的激活函数。

因此,在我们的 MNIST 示例中,输入张量将具有以下形状:

[batch_size, 28, 28, 1]

该卷积步骤的输出张量将具有以下形状:

[batch_size, 28, 28, 20]

输出张量的维度与输入图像相同,但现在我们有 20 个通道,表示应用了 20 个滤波器到输入图像。

引入非线性

在卷积步骤中,我们提到过将卷积步骤的输出传递给 ReLU 激活函数以引入非线性:

图 9.6: ReLU 激活函数

ReLU 激活函数将所有负的像素值替换为零,而将卷积步骤的输出传递给该激活函数的目的就是为了引入非线性,因为我们使用的数据通常是非线性的,这对训练过程非常有用。为了清楚地理解 ReLU 激活函数的好处,看看下面的图,它展示了卷积步骤的行输出及其经过修正后的版本:

图 9.7: 对输入特征图应用 ReLU 的结果

池化步骤

我们学习过程中的一个重要步骤是池化步骤,有时也叫做下采样或子采样步骤。这个步骤主要是为了减少卷积步骤输出的特征图(feature map)的维度。池化步骤的优点是,在减小特征图的大小的同时,保留了新版本中重要的信息。

下图展示了通过一个 2x2 滤波器和步幅 2 扫描图像,并应用最大池化操作的步骤。这种池化操作称为最大池化

图 9.8:使用 2 x 2 窗口在经过卷积和 ReLU 操作后的修正特征图上进行最大池化操作的示例(来源:http://textminingonline.com/wp-content/uploads/2016/10/max_polling-300x256.png)

我们可以使用以下代码行将卷积步骤的输出连接到池化层:

pool_layer1 = tf.layers.max_pooling2d(inputs=conv_layer1, pool_size=[2, 2], strides=2)

池化层接收来自卷积步骤的输入,形状如下:

[batch_size, image_width, image_height, channels]

例如,在我们的数字分类任务中,池化层的输入将具有以下形状:

[batch_size, 28, 28, 20]

池化操作的输出将具有以下形状:

[batch_size, 14, 14, 20]

在这个例子中,我们将卷积步骤的输出大小减少了 50%。这个步骤非常有用,因为它只保留了重要的信息,同时还减少了模型的复杂度,从而避免了过拟合。

全连接层

在堆叠了多个卷积和池化步骤之后,我们使用一个全连接层,在这个层中,我们将从输入图像中提取的高级特征输入到全连接层,以便利用这些特征进行实际的分类:

图 9.9:全连接层 - 每个节点都与相邻层的所有其他节点相连接

例如,在数字分类任务中,我们可以在卷积和池化步骤之后使用一个具有 1,024 个神经元和 ReLU 激活函数的全连接层来执行实际的分类。这个全连接层接受以下格式的输入:

[batch_size, features]

因此,我们需要重新调整或展平来自pool_layer2的输入特征图,以匹配这种格式。我们可以使用以下代码行来重新调整输出:

pool1_flat = tf.reshape(pool_layer1, [-1, 14 * 14 * 20])

在这个 reshape 函数中,我们使用-1表示批量大小将动态确定,并且pool_layer1输出中的每个示例将具有宽度为14、高度为14且有20个通道。

因此,这个重塑操作的最终输出将如下所示:

 [batch_size, 3136]

最后,我们可以使用 TensorFlow 的dense()函数来定义我们的全连接层,设定所需的神经元(单位)数量和最终的激活函数:

dense_layer = tf.layers.dense(inputs=pool1_flat, units=1024, activation=tf.nn.relu)

Logits 层

最后,我们需要 logits 层,它将接受全连接层的输出并生成原始的预测值。例如,在数字分类任务中,输出将是一个包含 10 个值的张量,每个值代表 0-9 类中的一个类别的分数。因此,让我们为数字分类示例定义这个 logits 层,其中我们只需要 10 个输出,并且使用线性激活函数,这是 TensorFlow 的dense()函数的默认值:

logits_layer = tf.layers.dense(inputs=dense_layer, units=10)

图 9.10:训练 ConvNet

这个 logits 层的最终输出将是一个具有以下形状的张量:

[batch_size, 10]

如前所述,模型的 logits 层将返回我们批次的原始预测值。但我们需要将这些值转换为可解释的格式:

  • 输入样本 0-9 的预测类别。

  • 每个可能类别的得分或概率。例如,样本属于类别 0 的概率是 1,依此类推。

图 9.11:CNN 不同层的可视化(来源:http://cs231n.github.io/assets/cnn/convnet.jpeg)

因此,我们的预测类别将是 10 个概率中最大值对应的类别。我们可以通过使用argmax函数如下获取这个值:

tf.argmax(input=logits_layer, axis=1)

记住,logits_layer的形状是这样的:

[batch_size, 10]

因此,我们需要沿着预测结果的维度(即索引为 1 的维度)找到最大值:

最后,我们可以通过对logits_layer的输出应用softmax激活函数来得到下一个值,该值表示每个目标类别的概率,将每个值压缩到 0 和 1 之间:

tf.nn.softmax(logits_layer, name="softmax_tensor")

CNN 基础示例 – MNIST 数字分类

在本节中,我们将通过使用 MNIST 数据集实现数字分类的完整 CNN 示例。我们将构建一个包含两个卷积层和全连接层的简单模型。

让我们先导入实现中所需的库:

%matplotlib inline
import matplotlib.pyplot as plt
import tensorflow as tf
import numpy as np
from sklearn.metrics import confusion_matrix
import math

接下来,我们将使用 TensorFlow 的辅助函数下载并预处理 MNIST 数据集,如下所示:

from tensorflow.examples.tutorials.mnist import input_data
mnist_data = input_data.read_data_sets('data/MNIST/', one_hot=True)
Output:
Successfully downloaded train-images-idx3-ubyte.gz 9912422 bytes.
Extracting data/MNIST/train-images-idx3-ubyte.gz
Successfully downloaded train-labels-idx1-ubyte.gz 28881 bytes.
Extracting data/MNIST/train-labels-idx1-ubyte.gz
Successfully downloaded t10k-images-idx3-ubyte.gz 1648877 bytes.
Extracting data/MNIST/t10k-images-idx3-ubyte.gz
Successfully downloaded t10k-labels-idx1-ubyte.gz 4542 bytes.
Extracting data/MNIST/t10k-labels-idx1-ubyte.gz

数据集被分为三个不重叠的集合:训练集、验证集和测试集。因此,让我们打印出每个集合中的图像数量:

print("- Number of images in the training set:\t\t{}".format(len(mnist_data.train.labels)))
print("- Number of images in the test set:\t\t{}".format(len(mnist_data.test.labels)))
print("- Number of images in the validation set:\t{}".format(len(mnist_data.validation.labels)))
- Number of images in the training set: 55000
- Number of images in the test set: 10000
- Number of images in the validation set: 5000

图像的实际标签以独热编码格式存储,所以我们有一个包含 10 个值的数组,除了表示该图像所属类别的索引外,其余值均为零。为了后续使用,我们需要将数据集中的类别号转换为整数:

mnist_data.test.cls_integer = np.argmax(mnist_data.test.labels, axis=1)

让我们定义一些已知的变量,以便在后续实现中使用:

# Default size for the input monocrome images of MNIST
image_size = 28

# Each image is stored as vector of this size.
image_size_flat = image_size * image_size

# The shape of each image
image_shape = (image_size, image_size)

# All the images in the mnist dataset are stored as a monocrome with only 1 channel
num_channels = 1

# Number of classes in the MNIST dataset from 0 till 9 which is 10
num_classes = 10

接下来,我们需要定义一个辅助函数,用于从数据集中绘制一些图像。这个辅助函数将以九个子图的网格方式绘制图像:

def plot_imgs(imgs, cls_actual, cls_predicted=None):
    assert len(imgs) == len(cls_actual) == 9

    # create a figure with 9 subplots to plot the images.
    fig, axes = plt.subplots(3, 3)
    fig.subplots_adjust(hspace=0.3, wspace=0.3)

    for i, ax in enumerate(axes.flat):
        # plot the image at the ith index
        ax.imshow(imgs[i].reshape(image_shape), cmap='binary')

        # labeling the images with the actual and predicted classes.
        if cls_predicted is None:
            xlabel = "True: {0}".format(cls_actual[i])
        else:
            xlabel = "True: {0}, Pred: {1}".format(cls_actual[i], cls_predicted[i])

        # Remove ticks from the plot.
        ax.set_xticks([])
        ax.set_yticks([])

        # Show the classes as the label on the x-axis.
        ax.set_xlabel(xlabel)

    plt.show()

让我们从测试集绘制一些图像,看看它们长什么样:

# Visualizing 9 images form the test set.
imgs = mnist_data.test.images[0:9]

# getting the actual classes of these 9 images
cls_actual = mnist_data.test.cls_integer[0:9]

#plotting the images
plot_imgs(imgs=imgs, cls_actual=cls_actual)

这是输出:

图 9.12:来自 MNIST 数据集的一些示例的可视化

构建模型

现在,到了构建模型核心部分的时候。计算图包含我们在本章前面提到的所有层。我们将从定义一些用于定义特定形状变量并随机初始化它们的函数开始:

def new_weights(shape):
    return tf.Variable(tf.truncated_normal(shape, stddev=0.05))
def new_biases(length):
    return tf.Variable(tf.constant(0.05, shape=[length]))

现在,让我们定义一个函数,该函数负责根据某些输入层、输入通道、滤波器大小、滤波器数量以及是否使用池化参数来创建一个新的卷积层:

def conv_layer(input, # the output of the previous layer.
                   input_channels, 
                   filter_size, 
                   filters, 
                   use_pooling=True): # Use 2x2 max-pooling.

    # preparing the accepted shape of the input Tensor.
    shape = [filter_size, filter_size, input_channels, filters]

    # Create weights which means filters with the given shape.
    filters_weights = new_weights(shape=shape)

    # Create new biases, one for each filter.
    filters_biases = new_biases(length=filters)

    # Calling the conve2d function as we explained above, were the strides parameter
    # has four values the first one for the image number and the last 1 for the input image channel
    # the middle ones represents how many pixels the filter should move with in the x and y axis
    conv_layer = tf.nn.conv2d(input=input,
                         filter=filters_weights,
                         strides=[1, 1, 1, 1],
                         padding='SAME')

    # Adding the biase to the output of the conv_layer.
    conv_layer += filters_biases

    # Use pooling to down-sample the image resolution?
    if use_pooling:
        # reduce the output feature map by max_pool layer
        pool_layer = tf.nn.max_pool(value=conv_layer,
                               ksize=[1, 2, 2, 1],
                               strides=[1, 2, 2, 1],
                               padding='SAME')

    # feeding the output to a ReLU activation function.
    relu_layer = tf.nn.relu(pool_layer)

    # return the final results after applying relu and the filter weights
    return relu_layer, filters_weights

如前所述,池化层生成一个 4D 张量。我们需要将这个 4D 张量展平为 2D 张量,以便传递到全连接层:

def flatten_layer(layer):
    # Get the shape of layer.
    shape = layer.get_shape()

    # We need to flatten the layer which has the shape of The shape [num_images, image_height, image_width, num_channels]
    # so that it has the shape of [batch_size, num_features] where number_features is image_height * image_width * num_channels

    number_features = shape[1:4].num_elements()

    # Reshaping that to be fed to the fully connected layer
    flatten_layer = tf.reshape(layer, [-1, number_features])

    # Return both the flattened layer and the number of features.
    return flatten_layer, number_features

该函数创建一个全连接层,假设输入是一个 2D 张量:

def fc_layer(input, # the flatten output.
                 num_inputs, # Number of inputs from previous layer
                 num_outputs, # Number of outputs
                 use_relu=True): # Use ReLU on the output to remove negative values

    # Creating the weights for the neurons of this fc_layer
    fc_weights = new_weights(shape=[num_inputs, num_outputs])
    fc_biases = new_biases(length=num_outputs)

    # Calculate the layer values by doing matrix multiplication of
    # the input values and fc_weights, and then add the fc_bias-values.
    fc_layer = tf.matmul(input, fc_weights) + fc_biases

    # if use RelU parameter is true
    if use_relu:
        relu_layer = tf.nn.relu(fc_layer)
        return relu_layer

    return fc_layer

在构建网络之前,让我们定义一个占位符用于输入图像,其中第一维是None,表示可以输入任意数量的图像:

input_values = tf.placeholder(tf.float32, shape=[None, image_size_flat], name='input_values')

正如我们之前提到的,卷积步骤期望输入图像的形状是 4D 张量。因此,我们需要将输入图像调整为以下形状:

[num_images, image_height, image_width, num_channels]

所以,让我们重新调整输入值的形状以匹配这种格式:

input_image = tf.reshape(input_values, [-1, image_size, image_size, num_channels])

接下来,我们需要定义另一个占位符用于实际类别的值,格式为独热编码:

y_actual = tf.placeholder(tf.float32, shape=[None, num_classes], name='y_actual')

此外,我们还需要定义一个占位符来保存实际类别的整数值:

y_actual_cls_integer = tf.argmax(y_actual, axis=1)

所以,让我们从构建第一个卷积神经网络开始:

conv_layer_1, conv1_weights = \
        conv_layer(input=input_image,
                   input_channels=num_channels,
                   filter_size=filter_size_1,
                   filters=filters_1,
                   use_pooling=True)

让我们检查第一卷积层将产生的输出张量的形状:

conv_layer_1
Output:
<tf.Tensor 'Relu:0' shape=(?, 14, 14, 16) dtype=float32>

接下来,我们将创建第二个卷积神经网络,并将第一个网络的输出作为输入:

conv_layer_2, conv2_weights = \
         conv_layer(input=conv_layer_1,
                   input_channels=filters_1,
                   filter_size=filter_size_2,
                   filters=filters_2,
                   use_pooling=True)

此外,我们需要再次检查第二卷积层输出张量的形状。形状应该是(?, 7, 7, 36),其中?表示任意数量的图像。

接下来,我们需要将 4D 张量展平,以匹配全连接层所期望的格式,即 2D 张量:

flatten_layer, number_features = flatten_layer(conv_layer_2)

我们需要再次检查展平层输出张量的形状:

flatten_layer
Output:
<tf.Tensor 'Reshape_1:0' shape=(?, 1764) dtype=float32>

接下来,我们将创建一个全连接层,并将展平层的输出传递给它。我们还将把全连接层的输出输入到 ReLU 激活函数中,然后再传递给第二个全连接层:

fc_layer_1 = fc_layer(input=flatten_layer,
                         num_inputs=number_features,
                         num_outputs=fc_num_neurons,
                         use_relu=True)

让我们再次检查第一个全连接层输出张量的形状:

fc_layer_1
Output:
<tf.Tensor 'Relu_2:0' shape=(?, 128) dtype=float32>

接下来,我们需要添加另一个全连接层,它将接收第一个全连接层的输出,并为每张图像生成一个大小为 10 的数组,表示每个目标类别是正确类别的得分:

fc_layer_2 = fc_layer(input=fc_layer_1,
                         num_inputs=fc_num_neurons,
                         num_outputs=num_classes,
                         use_relu=False)
fc_layer_2
Output:
<tf.Tensor 'add_3:0' shape=(?, 10) dtype=float32>

接下来,我们将对第二个全连接层的得分进行归一化,并将其输入到softmax激活函数中,这样它会将值压缩到 0 到 1 之间:

y_predicted = tf.nn.softmax(fc_layer_2)

然后,我们需要使用 TensorFlow 的argmax函数选择具有最高概率的目标类别:

y_predicted_cls_integer = tf.argmax(y_predicted, axis=1)

成本函数

接下来,我们需要定义我们的性能衡量标准,即交叉熵。如果预测的类别是正确的,那么交叉熵的值为 0:

cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=fc_layer_2,
                                                        labels=y_actual)

接下来,我们需要将之前步骤得到的所有交叉熵值求平均,以便得到一个单一的性能衡量标准:

model_cost = tf.reduce_mean(cross_entropy)

现在,我们有了一个需要优化/最小化的成本函数,因此我们将使用AdamOptimizer,它是一种优化方法,类似于梯度下降,但更为先进:

model_optimizer = tf.train.AdamOptimizer(learning_rate=1e-4).minimize(model_cost)

性能衡量标准

为了显示输出,让我们定义一个变量来检查预测的类别是否等于真实类别:

model_correct_prediction = tf.equal(y_predicted_cls_integer, y_actual_cls_integer)

通过将布尔值转换并求平均,计算模型的准确性,进而统计正确分类的数量:

model_accuracy = tf.reduce_mean(tf.cast(model_correct_prediction, tf.float32))

模型训练

让我们通过创建一个会负责执行先前定义的计算图的会话变量来启动训练过程:

session = tf.Session()

此外,我们需要初始化到目前为止已定义的变量:

session.run(tf.global_variables_initializer())

我们将按批次输入图像,以避免出现内存溢出错误:

train_batch_size = 64

在开始训练过程之前,我们将定义一个辅助函数,该函数通过遍历训练批次来执行优化过程:

# number of optimization iterations performed so far
total_iterations = 0

def optimize(num_iterations):
    # Update globally the total number of iterations performed so far.
    global total_iterations

    for i in range(total_iterations,
                   total_iterations + num_iterations):

        # Generating a random batch for the training process
        # input_batch now contains a bunch of images from the training set and
        # y_actual_batch are the actual labels for the images in the input batch.
        input_batch, y_actual_batch = mnist_data.train.next_batch(train_batch_size)

        # Putting the previous values in a dict format for Tensorflow to automatically assign them to the input
        # placeholders that we defined above
        feed_dict = {input_values: input_batch,
                           y_actual: y_actual_batch}

        # Next up, we run the model optimizer on this batch of images
        session.run(model_optimizer, feed_dict=feed_dict)

        # Print the training status every 100 iterations.
        if i % 100 == 0:
            # measuring the accuracy over the training set.
            acc_training_set = session.run(model_accuracy, feed_dict=feed_dict)

            #Printing the accuracy over the training set
            print("Iteration: {0:>6}, Accuracy Over the training set: {1:>6.1%}".format(i + 1, acc_training_set))

    # Update the number of iterations performed so far
    total_iterations += num_iterations

我们还将定义一些辅助函数,帮助我们可视化模型的结果,并查看哪些图像被模型误分类:

def plot_errors(cls_predicted, correct):

    # cls_predicted is an array of the predicted class number of each image in the test set.

    # Extracting the incorrect images.
    incorrect = (correct == False)

    # Get the images from the test-set that have been
    # incorrectly classified.
    images = mnist_data.test.images[incorrect]

    # Get the predicted classes for those incorrect images.
    cls_pred = cls_predicted[incorrect]

    # Get the actual classes for those incorrect images.
    cls_true = mnist_data.test.cls_integer[incorrect]

    # Plot 9 of these images
    plot_imgs(imgs=imgs[0:9],
                cls_actual=cls_actual[0:9],
                cls_predicted=cls_predicted[0:9])

我们还可以绘制预测结果与实际类别的混淆矩阵:

def plot_confusionMatrix(cls_predicted):

 # cls_predicted is an array of the predicted class number of each image in the test set.

 # Get the actual classes for the test-set.
 cls_actual = mnist_data.test.cls_integer

 # Generate the confusion matrix using sklearn.
 conf_matrix = confusion_matrix(y_true=cls_actual,
 y_pred=cls_predicted)

 # Print the matrix.
 print(conf_matrix)

 # visualizing the confusion matrix.
 plt.matshow(conf_matrix)

 plt.colorbar()
 tick_marks = np.arange(num_classes)
 plt.xticks(tick_marks, range(num_classes))
 plt.yticks(tick_marks, range(num_classes))
 plt.xlabel('Predicted class')
 plt.ylabel('True class')

 # Showing the plot
 plt.show()

最后,我们将定义一个辅助函数,帮助我们测量训练模型在测试集上的准确率:

# measuring the accuracy of the trained model over the test set by splitting it into small batches
test_batch_size = 256

def test_accuracy(show_errors=False,
                        show_confusionMatrix=False):

    #number of test images 
    number_test = len(mnist_data.test.images)

    # define an array of zeros for the predicted classes of the test set which
    # will be measured in mini batches and stored it.
    cls_predicted = np.zeros(shape=number_test, dtype=np.int)

    # measuring the predicted classes for the testing batches.

    # Starting by the batch at index 0.
    i = 0

    while i < number_test:
        # The ending index for the next batch to be processed is j.
        j = min(i + test_batch_size, number_test)

        # Getting all the images form the test set between the start and end indices
        input_images = mnist_data.test.images[i:j, :]

        # Get the acutal labels for those images.
        actual_labels = mnist_data.test.labels[i:j, :]

        # Create a feed-dict with the corresponding values for the input placeholder values
        feed_dict = {input_values: input_images,
                     y_actual: actual_labels}

        cls_predicted[i:j] = session.run(y_predicted_cls_integer, feed_dict=feed_dict)

        # Setting the start of the next batch to be the end of the one that we just processed j
        i = j

    # Get the actual class numbers of the test images.
    cls_actual = mnist_data.test.cls_integer

    # Check if the model predictions are correct or not
    correct = (cls_actual == cls_predicted)

    # Summing up the correct examples
    correct_number_images = correct.sum()

    # measuring the accuracy by dividing the correclty classified ones with total number of images in the test set.
    testset_accuracy = float(correct_number_images) / number_test

    # showing the accuracy.
    print("Accuracy on Test-Set: {0:.1%} ({1} / {2})".format(testset_accuracy, correct_number_images, number_test))

    # showing some examples form the incorrect ones.
    if show_errors:
        print("Example errors:")
        plot_errors(cls_predicted=cls_predicted, correct=correct)

    # Showing the confusion matrix of the test set predictions
    if show_confusionMatrix:
        print("Confusion Matrix:")
        plot_confusionMatrix(cls_predicted=cls_predicted)

让我们打印出未经任何优化的模型在测试集上的准确率:

test_accuracy()
Output:
Accuracy on Test-Set: 4.1% (410 / 10000)

让我们通过运行一次优化过程来感受优化过程如何增强模型的能力,将图像正确分类到对应的类别:

optimize(num_iterations=1)
Output:
Iteration: 1, Accuracy Over the training set: 4.7%
test_accuracy()
Output
Accuracy on Test-Set: 4.4% (437 / 10000)

现在,让我们开始进行一项长时间的优化过程,进行 10,000 次迭代:

optimize(num_iterations=9999) #We have already performed 1 iteration.

在输出的最后,您应该看到与以下输出非常接近的结果:

Iteration: 7301, Accuracy Over the training set: 96.9%
Iteration: 7401, Accuracy Over the training set: 100.0%
Iteration: 7501, Accuracy Over the training set: 98.4%
Iteration: 7601, Accuracy Over the training set: 98.4%
Iteration: 7701, Accuracy Over the training set: 96.9%
Iteration: 7801, Accuracy Over the training set: 96.9%
Iteration: 7901, Accuracy Over the training set: 100.0%
Iteration: 8001, Accuracy Over the training set: 98.4%
Iteration: 8101, Accuracy Over the training set: 96.9%
Iteration: 8201, Accuracy Over the training set: 100.0%
Iteration: 8301, Accuracy Over the training set: 98.4%
Iteration: 8401, Accuracy Over the training set: 98.4%
Iteration: 8501, Accuracy Over the training set: 96.9%
Iteration: 8601, Accuracy Over the training set: 100.0%
Iteration: 8701, Accuracy Over the training set: 98.4%
Iteration: 8801, Accuracy Over the training set: 100.0%
Iteration: 8901, Accuracy Over the training set: 98.4%
Iteration: 9001, Accuracy Over the training set: 100.0%
Iteration: 9101, Accuracy Over the training set: 96.9%
Iteration: 9201, Accuracy Over the training set: 98.4%
Iteration: 9301, Accuracy Over the training set: 98.4%
Iteration: 9401, Accuracy Over the training set: 100.0%
Iteration: 9501, Accuracy Over the training set: 100.0%
Iteration: 9601, Accuracy Over the training set: 98.4%
Iteration: 9701, Accuracy Over the training set: 100.0%
Iteration: 9801, Accuracy Over the training set: 100.0%
Iteration: 9901, Accuracy Over the training set: 100.0%
Iteration: 10001, Accuracy Over the training set: 98.4%

现在,让我们检查模型在测试集上的泛化能力:

test_accuracy(show_errors=True,
                    show_confusionMatrix=True)
Output:
Accuracy on Test-Set: 92.8% (9281 / 10000)
Example errors:

图 9.13:测试集上的准确率

Confusion Matrix:
[[ 971    0    2    2    0    4    0    1    0    0]
 [   0 1110    4    2    1    2    3    0   13    0]
 [  12    2  949   15   16    3    4   17   14    0]
 [   5    3   14  932    0   34    0   13    6    3]
 [   1    2    3    0  931    1    8    2    3   31]
 [  12    1    4   13    3  852    2    1    3    1]
 [  21    4    5    2   18   34  871    1    2    0]
 [   1   10   26    5    5    0    0  943    2   36]
 [  16    5   10   27   16   48    5   13  815   19]
 [  12    5    5   11   38   10    0   18    3  907]]

以下是输出结果:

图 9.14:测试集的混淆矩阵。

有趣的是,实际上在使用基础卷积网络时,我们在测试集上的准确率几乎达到了 93%。这个实现和结果展示了一个简单的卷积网络能做些什么。

总结

在本章中,我们介绍了卷积神经网络(CNN)的直觉和技术细节,同时也了解了如何在 TensorFlow 中实现一个基本的 CNN 架构。

在下一章中,我们将展示一些更先进的架构,这些架构可以用于检测数据科学家广泛使用的图像数据集中的物体。我们还将看到卷积神经网络(CNN)的魅力,它们是如何通过首先识别物体的基本特征,再在这些特征基础上构建更高级的语义特征,从而模拟人类对物体的理解,最终得出对物体的分类的。尽管这个过程在人类大脑中发生得非常迅速,但它实际上是我们识别物体时的运作方式。

第八章:目标检测 – CIFAR-10 示例

在介绍了卷积神经网络CNNs)的基础和直觉/动机后,我们将在目标检测中展示其在其中一个最流行的数据集上的应用。我们还将看到 CNN 的初始层获取对象的基本特征,而最终的卷积层将从第一层的这些基本特征构建更语义级别的特征。

本章将涵盖以下主题:

  • 目标检测

  • CIFAR-10 图像中对象检测—模型构建和训练

目标检测

维基百科指出:

"目标检测是计算机视觉领域中用于在图像或视频序列中查找和识别对象的技术。人类在图像中识别多种对象并不费力,尽管对象的图像在不同视角、大小和比例以及平移或旋转时可能有所变化。即使对象部分遮挡视图时,也能识别出对象。对计算机视觉系统而言,这仍然是一个挑战。多年来已实施了多种方法来解决此任务。"

图像分析是深度学习中最显著的领域之一。图像易于生成和处理,它们恰好是机器学习的正确数据类型:对人类易于理解,但对计算机而言却很难。不奇怪,图像分析在深度神经网络的历史中发挥了关键作用。

图 11.1:检测对象的示例。来源:B. C. Russell, A. Torralba, C. Liu, R. Fergus, W. T. Freeman,《通过场景对齐进行对象检测》,2007 年进展神经信息处理系统,网址:http://bryanrussell.org/papers/nipsDetectionBySceneAlignment07.pdf

随着自动驾驶汽车、面部检测、智能视频监控和人数统计解决方案的兴起,快速准确的目标检测系统需求量大。这些系统不仅包括图像中对象的识别和分类,还可以通过绘制适当的框来定位每个对象。这使得目标检测比传统的计算机视觉前身——图像分类更为复杂。

在本章中,我们将讨论目标检测——找出图像中有哪些对象。例如,想象一下自动驾驶汽车需要在道路上检测其他车辆,就像图 11.1中一样。目标检测有许多复杂的算法。它们通常需要庞大的数据集、非常深的卷积网络和长时间的训练。

CIFAR-10 – 建模、构建和训练

此示例展示了如何在 CIFAR-10 数据集中制作用于分类图像的 CNN。我们将使用一个简单的卷积神经网络实现一些卷积和全连接层。

即使网络架构非常简单,当尝试检测 CIFAR-10 图像中的对象时,您会看到它表现得有多好。

所以,让我们开始这个实现。

使用的包

我们导入了此实现所需的所有包:

%matplotlib inline
%config InlineBackend.figure_format = 'retina'

from urllib.request import urlretrieve
from os.path import isfile, isdir
from tqdm import tqdm
import tarfile
import numpy as np
import random
import matplotlib.pyplot as plt
from sklearn.preprocessing import LabelBinarizer
from sklearn.preprocessing import OneHotEncoder

import pickle
import tensorflow as tf

载入 CIFAR-10 数据集

在这个实现中,我们将使用 CIFAR-10 数据集,这是用于对象检测的最常用的数据集之一。因此,让我们先定义一个辅助类来下载和提取 CIFAR-10 数据集(如果尚未下载):

cifar10_batches_dir_path = 'cifar-10-batches-py'

tar_gz_filename = 'cifar-10-python.tar.gz'

class DLProgress(tqdm):
    last_block = 0

    def hook(self, block_num=1, block_size=1, total_size=None):
        self.total = total_size
        self.update((block_num - self.last_block) * block_size)
        self.last_block = block_num

if not isfile(tar_gz_filename):
    with DLProgress(unit='B', unit_scale=True, miniters=1, desc='CIFAR-10 Python Images Batches') as pbar:
        urlretrieve(
            'https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz',
            tar_gz_filename,
            pbar.hook)

if not isdir(cifar10_batches_dir_path):
    with tarfile.open(tar_gz_filename) as tar:
        tar.extractall()
        tar.close()

下载并提取 CIFAR-10 数据集后,您会发现它已经分成了五个批次。CIFAR-10 包含了 10 个类别的图像:

  • airplane

  • automobile

  • bird

  • cat

  • deer

  • dog

  • frog

  • horse

  • ship

  • truck

在我们深入构建网络核心之前,让我们进行一些数据分析和预处理。

数据分析和预处理

我们需要分析数据集并进行一些基本的预处理。因此,让我们首先定义一些辅助函数,这些函数将使我们能够从我们有的五批次中加载特定批次,并打印关于此批次及其样本的一些分析:

# Defining a helper function for loading a batch of images
def load_batch(cifar10_dataset_dir_path, batch_num):

    with open(cifar10_dataset_dir_path + '/data_batch_' + str(batch_num), mode='rb') as file:
        batch = pickle.load(file, encoding='latin1')

    input_features = batch['data'].reshape((len(batch['data']), 3, 32, 32)).transpose(0, 2, 3, 1)
    target_labels = batch['labels']

    return input_features, target_labels

然后,我们定义一个函数,可以帮助我们显示特定批次中特定样本的统计信息:

#Defining a function to show the stats for batch ans specific sample
def batch_image_stats(cifar10_dataset_dir_path, batch_num, sample_num):

    batch_nums = list(range(1, 6))

    #checking if the batch_num is a valid batch number
    if batch_num not in batch_nums:
        print('Batch Num is out of Range. You can choose from these Batch nums: {}'.format(batch_nums))
        return None

    input_features, target_labels = load_batch(cifar10_dataset_dir_path, batch_num)

    #checking if the sample_num is a valid sample number
    if not (0 <= sample_num < len(input_features)):
        print('{} samples in batch {}. {} is not a valid sample number.'.format(len(input_features), batch_num, sample_num))
        return None

    print('\nStatistics of batch number {}:'.format(batch_num))
    print('Number of samples in this batch: {}'.format(len(input_features)))
    print('Per class counts of each Label: {}'.format(dict(zip(*np.unique(target_labels, return_counts=True)))))

    image = input_features[sample_num]
    label = target_labels[sample_num]
    cifar10_class_names = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']

    print('\nSample Image Number {}:'.format(sample_num))
    print('Sample image - Minimum pixel value: {} Maximum pixel value: {}'.format(image.min(), image.max()))
    print('Sample image - Shape: {}'.format(image.shape))
    print('Sample Label - Label Id: {} Name: {}'.format(label, cifar10_class_names[label]))
    plt.axis('off')
    plt.imshow(image)

现在,我们可以使用这个函数来操作我们的数据集,并可视化特定的图像:

# Explore a specific batch and sample from the dataset
batch_num = 3
sample_num = 6
batch_image_stats(cifar10_batches_dir_path, batch_num, sample_num)

输出如下:


Statistics of batch number 3:
Number of samples in this batch: 10000
Per class counts of each Label: {0: 994, 1: 1042, 2: 965, 3: 997, 4: 990, 5: 1029, 6: 978, 7: 1015, 8: 961, 9: 1029}

Sample Image Number 6:
Sample image - Minimum pixel value: 30 Maximum pixel value: 242
Sample image - Shape: (32, 32, 3)
Sample Label - Label Id: 8 Name: ship

图 11.2: 来自第 3 批次的样本图像 6

在继续将数据集馈送到模型之前,我们需要将其归一化到零到一的范围内。

批归一化优化了网络训练。已经显示它有几个好处:

  • 训练更快:每个训练步骤会变慢,因为在网络的前向传播过程中需要额外的计算,在网络的反向传播过程中需要训练额外的超参数。然而,它应该会更快地收敛,因此总体训练速度应该更快。

  • 更高的学习率:梯度下降算法通常需要较小的学习率才能使网络收敛到损失函数的最小值。随着神经网络变得更深,它们在反向传播过程中的梯度值会变得越来越小,因此通常需要更多的迭代次数。使用批归一化的想法允许我们使用更高的学习率,这进一步增加了网络训练的速度。

  • 权重初始化简单: 权重初始化可能会很困难,特别是在使用深度神经网络时。批归一化似乎使我们在选择初始权重时可以更不谨慎。

因此,让我们继续定义一个函数,该函数将负责将输入图像列表归一化,以便这些图像的所有像素值都在零到一之间。

#Normalize CIFAR-10 images to be in the range of [0,1]

def normalize_images(images):

    # initial zero ndarray
    normalized_images = np.zeros_like(images.astype(float))

    # The first images index is number of images where the other indices indicates
    # hieight, width and depth of the image
    num_images = images.shape[0]

    # Computing the minimum and maximum value of the input image to do the normalization based on them
    maximum_value, minimum_value = images.max(), images.min()

    # Normalize all the pixel values of the images to be from 0 to 1
    for img in range(num_images):
        normalized_images[img,...] = (images[img, ...] - float(minimum_value)) / float(maximum_value - minimum_value)

    return normalized_images

接下来,我们需要实现另一个辅助函数,对输入图像的标签进行编码。在这个函数中,我们将使用 sklearn 的独热编码(one-hot encoding),其中每个图像标签通过一个零向量表示,除了该向量所代表的图像的类别索引。

输出向量的大小将取决于数据集中的类别数量,对于 CIFAR-10 数据集来说是 10 个类别:

#encoding the input images. Each image will be represented by a vector of zeros except for the class index of the image 
# that this vector represents. The length of this vector depends on number of classes that we have
# the dataset which is 10 in CIFAR-10

def one_hot_encode(images):

    num_classes = 10

    #use sklearn helper function of OneHotEncoder() to do that
    encoder = OneHotEncoder(num_classes)

    #resize the input images to be 2D
    input_images_resized_to_2d = np.array(images).reshape(-1,1)
    one_hot_encoded_targets = encoder.fit_transform(input_images_resized_to_2d)

    return one_hot_encoded_targets.toarray()

现在,是时候调用之前的辅助函数进行预处理并保存数据集,以便我们以后可以使用它了:

def preprocess_persist_data(cifar10_batches_dir_path, normalize_images, one_hot_encode):

    num_batches = 5
    valid_input_features = []
    valid_target_labels = []

    for batch_ind in range(1, num_batches + 1):

        #Loading batch
        input_features, target_labels = load_batch(cifar10_batches_dir_path, batch_ind)
        num_validation_images = int(len(input_features) * 0.1)

        # Preprocess the current batch and perisist it for future use
        input_features = normalize_images(input_features[:-num_validation_images])
        target_labels = one_hot_encode( target_labels[:-num_validation_images])

        #Persisting the preprocessed batch
        pickle.dump((input_features, target_labels), open('preprocess_train_batch_' + str(batch_ind) + '.p', 'wb'))

        # Define a subset of the training images to be used for validating our model
        valid_input_features.extend(input_features[-num_validation_images:])
        valid_target_labels.extend(target_labels[-num_validation_images:])

    # Preprocessing and persisting the validationi subset
    input_features = normalize_images( np.array(valid_input_features))
    target_labels = one_hot_encode(np.array(valid_target_labels))

    pickle.dump((input_features, target_labels), open('preprocess_valid.p', 'wb'))

    #Now it's time to preporcess and persist the test batche
    with open(cifar10_batches_dir_path + '/test_batch', mode='rb') as file:
        test_batch = pickle.load(file, encoding='latin1')

    test_input_features = test_batch['data'].reshape((len(test_batch['data']), 3, 32, 32)).transpose(0, 2, 3, 1)
    test_input_labels = test_batch['labels']

    # Normalizing and encoding the test batch
    input_features = normalize_images( np.array(test_input_features))
    target_labels = one_hot_encode(np.array(test_input_labels))

    pickle.dump((input_features, target_labels), open('preprocess_test.p', 'wb'))

# Calling the helper function above to preprocess and persist the training, validation, and testing set
preprocess_persist_data(cifar10_batches_dir_path, normalize_images, one_hot_encode)

现在,我们已经将预处理数据保存到磁盘。

我们还需要加载验证集,以便在训练过程的不同 epoch 上运行训练好的模型:

# Load the Preprocessed Validation data
valid_input_features, valid_input_labels = pickle.load(open('preprocess_valid.p', mode='rb'))

构建网络

现在是时候构建我们分类应用程序的核心,即 CNN 架构的计算图了,但为了最大化该实现的优势,我们不会使用 TensorFlow 层 API,而是将使用它的神经网络版本。

所以,让我们从定义模型输入占位符开始,这些占位符将输入图像、目标类别以及 dropout 层的保留概率参数(这有助于我们通过丢弃一些连接来减少架构的复杂性,从而减少过拟合的几率):


# Defining the model inputs
def images_input(img_shape):
 return tf.placeholder(tf.float32, (None, ) + img_shape, name="input_images")

def target_input(num_classes):

 target_input = tf.placeholder(tf.int32, (None, num_classes), name="input_images_target")
 return target_input

#define a function for the dropout layer keep probability
def keep_prob_input():
 return tf.placeholder(tf.float32, name="keep_prob")

接下来,我们需要使用 TensorFlow 神经网络实现版本来构建我们的卷积层,并进行最大池化:

# Applying a convolution operation to the input tensor followed by max pooling
def conv2d_layer(input_tensor, conv_layer_num_outputs, conv_kernel_size, conv_layer_strides, pool_kernel_size, pool_layer_strides):

 input_depth = input_tensor.get_shape()[3].value
 weight_shape = conv_kernel_size + (input_depth, conv_layer_num_outputs,)

 #Defining layer weights and biases
 weights = tf.Variable(tf.random_normal(weight_shape))
 biases = tf.Variable(tf.random_normal((conv_layer_num_outputs,)))

 #Considering the biase variable
 conv_strides = (1,) + conv_layer_strides + (1,)

 conv_layer = tf.nn.conv2d(input_tensor, weights, strides=conv_strides, padding='SAME')
 conv_layer = tf.nn.bias_add(conv_layer, biases)

 conv_kernel_size = (1,) + conv_kernel_size + (1,)

 pool_strides = (1,) + pool_layer_strides + (1,)
 pool_layer = tf.nn.max_pool(conv_layer, ksize=conv_kernel_size, strides=pool_strides, padding='SAME')
 return pool_layer

正如你可能在前一章中看到的,最大池化操作的输出是一个 4D 张量,这与全连接层所需的输入格式不兼容。因此,我们需要实现一个展平层,将最大池化层的输出从 4D 转换为 2D 张量:

#Flatten the output of max pooling layer to be fing to the fully connected layer which only accepts the output
# to be in 2D
def flatten_layer(input_tensor):
return tf.contrib.layers.flatten(input_tensor)

接下来,我们需要定义一个辅助函数,允许我们向架构中添加一个全连接层:

#Define the fully connected layer that will use the flattened output of the stacked convolution layers
#to do the actuall classification
def fully_connected_layer(input_tensor, num_outputs):
 return tf.layers.dense(input_tensor, num_outputs)

最后,在使用这些辅助函数创建整个架构之前,我们需要创建另一个函数,它将接收全连接层的输出并产生 10 个实值,对应于我们数据集中类别的数量:

#Defining the output function
def output_layer(input_tensor, num_outputs):
    return  tf.layers.dense(input_tensor, num_outputs)

所以,让我们定义一个函数,把所有这些部分组合起来,创建一个具有三个卷积层的 CNN。每个卷积层后面都会跟随一个最大池化操作。我们还会有两个全连接层,每个全连接层后面都会跟一个 dropout 层,以减少模型复杂性并防止过拟合。最后,我们将有一个输出层,产生 10 个实值向量,每个值代表每个类别的得分,表示哪个类别是正确的:

def build_convolution_net(image_data, keep_prob):

 # Applying 3 convolution layers followed by max pooling layers
 conv_layer_1 = conv2d_layer(image_data, 32, (3,3), (1,1), (3,3), (3,3)) 
 conv_layer_2 = conv2d_layer(conv_layer_1, 64, (3,3), (1,1), (3,3), (3,3))
 conv_layer_3 = conv2d_layer(conv_layer_2, 128, (3,3), (1,1), (3,3), (3,3))

# Flatten the output from 4D to 2D to be fed to the fully connected layer
 flatten_output = flatten_layer(conv_layer_3)

# Applying 2 fully connected layers with drop out
 fully_connected_layer_1 = fully_connected_layer(flatten_output, 64)
 fully_connected_layer_1 = tf.nn.dropout(fully_connected_layer_1, keep_prob)
 fully_connected_layer_2 = fully_connected_layer(fully_connected_layer_1, 32)
 fully_connected_layer_2 = tf.nn.dropout(fully_connected_layer_2, keep_prob)

 #Applying the output layer while the output size will be the number of categories that we have
 #in CIFAR-10 dataset
 output_logits = output_layer(fully_connected_layer_2, 10)

 #returning output
 return output_logits

让我们调用之前的辅助函数来构建网络并定义它的损失和优化标准:

#Using the helper function above to build the network

#First off, let's remove all the previous inputs, weights, biases form the previous runs
tf.reset_default_graph()

# Defining the input placeholders to the convolution neural network
input_images = images_input((32, 32, 3))
input_images_target = target_input(10)
keep_prob = keep_prob_input()

# Building the models
logits_values = build_convolution_net(input_images, keep_prob)

# Name logits Tensor, so that is can be loaded from disk after training
logits_values = tf.identity(logits_values, name='logits')

# defining the model loss
model_cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logits_values, labels=input_images_target))

# Defining the model optimizer
model_optimizer = tf.train.AdamOptimizer().minimize(model_cost)

# Calculating and averaging the model accuracy
correct_prediction = tf.equal(tf.argmax(logits_values, 1), tf.argmax(input_images_target, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32), name='model_accuracy')
tests.test_conv_net(build_convolution_net)

现在,我们已经构建了该网络的计算架构,是时候启动训练过程并查看一些结果了。

模型训练

因此,让我们定义一个辅助函数,使我们能够启动训练过程。这个函数将接受输入图像、目标类别的独热编码以及保持概率值作为输入。然后,它将把这些值传递给计算图,并调用模型优化器:

#Define a helper function for kicking off the training process
def train(session, model_optimizer, keep_probability, in_feature_batch, target_batch):
session.run(model_optimizer, feed_dict={input_images: in_feature_batch, input_images_target: target_batch, keep_prob: keep_probability})

我们需要在训练过程中的不同时间点验证模型,因此我们将定义一个辅助函数,打印出模型在验证集上的准确率:

#Defining a helper funcitno for print information about the model accuracy and it's validation accuracy as well
def print_model_stats(session, input_feature_batch, target_label_batch, model_cost, model_accuracy):

    validation_loss = session.run(model_cost, feed_dict={input_images: input_feature_batch, input_images_target: target_label_batch, keep_prob: 1.0})
    validation_accuracy = session.run(model_accuracy, feed_dict={input_images: input_feature_batch, input_images_target: target_label_batch, keep_prob: 1.0})

    print("Valid Loss: %f" %(validation_loss))
    print("Valid accuracy: %f" % (validation_accuracy))

让我们还定义一些模型的超参数,这些参数可以帮助我们调整模型以获得更好的性能:

# Model Hyperparameters
num_epochs = 100
batch_size = 128
keep_probability = 0.5

现在,让我们启动训练过程,但只针对 CIFAR-10 数据集的单一批次,看看基于该批次的模型准确率。

然而,在此之前,我们将定义一个辅助函数,加载一个批次的训练数据,并将输入图像与目标类别分开:

# Splitting the dataset features and labels to batches
def batch_split_features_labels(input_features, target_labels, train_batch_size):
    for start in range(0, len(input_features), train_batch_size):
        end = min(start + train_batch_size, len(input_features))
        yield input_features[start:end], target_labels[start:end]

#Loading the persisted preprocessed training batches
def load_preprocess_training_batch(batch_id, batch_size):
    filename = 'preprocess_train_batch_' + str(batch_id) + '.p'
    input_features, target_labels = pickle.load(open(filename, mode='rb'))

    # Returning the training images in batches according to the batch size defined above
    return batch_split_features_labels(input_features, target_labels, train_batch_size)

现在,让我们开始一个批次的训练过程:

print('Training on only a Single Batch from the CIFAR-10 Dataset...')
with tf.Session() as sess:

 # Initializing the variables
 sess.run(tf.global_variables_initializer())

 # Training cycle
 for epoch in range(num_epochs):
 batch_ind = 1

 for batch_features, batch_labels in load_preprocess_training_batch(batch_ind, batch_size):
 train(sess, model_optimizer, keep_probability, batch_features, batch_labels)

 print('Epoch number {:>2}, CIFAR-10 Batch Number {}: '.format(epoch + 1, batch_ind), end='')
 print_model_stats(sess, batch_features, batch_labels, model_cost, accuracy)

Output:
.
.
.
Epoch number 85, CIFAR-10 Batch Number 1: Valid Loss: 1.490792
Valid accuracy: 0.550000
Epoch number 86, CIFAR-10 Batch Number 1: Valid Loss: 1.487118
Valid accuracy: 0.525000
Epoch number 87, CIFAR-10 Batch Number 1: Valid Loss: 1.309082
Valid accuracy: 0.575000
Epoch number 88, CIFAR-10 Batch Number 1: Valid Loss: 1.446488
Valid accuracy: 0.475000
Epoch number 89, CIFAR-10 Batch Number 1: Valid Loss: 1.430939
Valid accuracy: 0.550000
Epoch number 90, CIFAR-10 Batch Number 1: Valid Loss: 1.484480
Valid accuracy: 0.525000
Epoch number 91, CIFAR-10 Batch Number 1: Valid Loss: 1.345774
Valid accuracy: 0.575000
Epoch number 92, CIFAR-10 Batch Number 1: Valid Loss: 1.425942
Valid accuracy: 0.575000

Epoch number 93, CIFAR-10 Batch Number 1: Valid Loss: 1.451115
Valid accuracy: 0.550000
Epoch number 94, CIFAR-10 Batch Number 1: Valid Loss: 1.368719
Valid accuracy: 0.600000
Epoch number 95, CIFAR-10 Batch Number 1: Valid Loss: 1.336483
Valid accuracy: 0.600000
Epoch number 96, CIFAR-10 Batch Number 1: Valid Loss: 1.383425
Valid accuracy: 0.575000
Epoch number 97, CIFAR-10 Batch Number 1: Valid Loss: 1.378877
Valid accuracy: 0.625000
Epoch number 98, CIFAR-10 Batch Number 1: Valid Loss: 1.343391
Valid accuracy: 0.600000
Epoch number 99, CIFAR-10 Batch Number 1: Valid Loss: 1.319342
Valid accuracy: 0.625000
Epoch number 100, CIFAR-10 Batch Number 1: Valid Loss: 1.340849
Valid accuracy: 0.525000

如你所见,仅在单一批次上训练时,验证准确率并不高。让我们看看仅通过完整训练过程,验证准确率会如何变化:

model_save_path = './cifar-10_classification'

with tf.Session() as sess:
 # Initializing the variables
 sess.run(tf.global_variables_initializer())

 # Training cycle
 for epoch in range(num_epochs):

 # iterate through the batches
 num_batches = 5

 for batch_ind in range(1, num_batches + 1):
 for batch_features, batch_labels in load_preprocess_training_batch(batch_ind, batch_size):
 train(sess, model_optimizer, keep_probability, batch_features, batch_labels)

 print('Epoch number{:>2}, CIFAR-10 Batch Number {}: '.format(epoch + 1, batch_ind), end='')
 print_model_stats(sess, batch_features, batch_labels, model_cost, accuracy)

 # Save the trained Model
 saver = tf.train.Saver()
 save_path = saver.save(sess, model_save_path)

Output:
.
.
.
Epoch number94, CIFAR-10 Batch Number 5: Valid Loss: 0.316593
Valid accuracy: 0.925000
Epoch number95, CIFAR-10 Batch Number 1: Valid Loss: 0.285429
Valid accuracy: 0.925000
Epoch number95, CIFAR-10 Batch Number 2: Valid Loss: 0.347411
Valid accuracy: 0.825000
Epoch number95, CIFAR-10 Batch Number 3: Valid Loss: 0.232483
Valid accuracy: 0.950000
Epoch number95, CIFAR-10 Batch Number 4: Valid Loss: 0.294707
Valid accuracy: 0.900000
Epoch number95, CIFAR-10 Batch Number 5: Valid Loss: 0.299490
Valid accuracy: 0.975000
Epoch number96, CIFAR-10 Batch Number 1: Valid Loss: 0.302191
Valid accuracy: 0.950000
Epoch number96, CIFAR-10 Batch Number 2: Valid Loss: 0.347043
Valid accuracy: 0.750000
Epoch number96, CIFAR-10 Batch Number 3: Valid Loss: 0.252851
Valid accuracy: 0.875000
Epoch number96, CIFAR-10 Batch Number 4: Valid Loss: 0.291433
Valid accuracy: 0.950000
Epoch number96, CIFAR-10 Batch Number 5: Valid Loss: 0.286192
Valid accuracy: 0.950000
Epoch number97, CIFAR-10 Batch Number 1: Valid Loss: 0.277105
Valid accuracy: 0.950000
Epoch number97, CIFAR-10 Batch Number 2: Valid Loss: 0.305842
Valid accuracy: 0.850000
Epoch number97, CIFAR-10 Batch Number 3: Valid Loss: 0.215272
Valid accuracy: 0.950000
Epoch number97, CIFAR-10 Batch Number 4: Valid Loss: 0.313761
Valid accuracy: 0.925000
Epoch number97, CIFAR-10 Batch Number 5: Valid Loss: 0.313503
Valid accuracy: 0.925000
Epoch number98, CIFAR-10 Batch Number 1: Valid Loss: 0.265828
Valid accuracy: 0.925000
Epoch number98, CIFAR-10 Batch Number 2: Valid Loss: 0.308948
Valid accuracy: 0.800000
Epoch number98, CIFAR-10 Batch Number 3: Valid Loss: 0.232083
Valid accuracy: 0.950000
Epoch number98, CIFAR-10 Batch Number 4: Valid Loss: 0.298826
Valid accuracy: 0.925000
Epoch number98, CIFAR-10 Batch Number 5: Valid Loss: 0.297230
Valid accuracy: 0.950000
Epoch number99, CIFAR-10 Batch Number 1: Valid Loss: 0.304203
Valid accuracy: 0.900000
Epoch number99, CIFAR-10 Batch Number 2: Valid Loss: 0.308775
Valid accuracy: 0.825000
Epoch number99, CIFAR-10 Batch Number 3: Valid Loss: 0.225072
Valid accuracy: 0.925000
Epoch number99, CIFAR-10 Batch Number 4: Valid Loss: 0.263737
Valid accuracy: 0.925000
Epoch number99, CIFAR-10 Batch Number 5: Valid Loss: 0.278601
Valid accuracy: 0.950000
Epoch number100, CIFAR-10 Batch Number 1: Valid Loss: 0.293509
Valid accuracy: 0.950000
Epoch number100, CIFAR-10 Batch Number 2: Valid Loss: 0.303817
Valid accuracy: 0.875000
Epoch number100, CIFAR-10 Batch Number 3: Valid Loss: 0.244428
Valid accuracy: 0.900000
Epoch number100, CIFAR-10 Batch Number 4: Valid Loss: 0.280712
Valid accuracy: 0.925000
Epoch number100, CIFAR-10 Batch Number 5: Valid Loss: 0.278625
Valid accuracy: 0.950000

测试模型

让我们在 CIFAR-10 数据集的测试集部分上测试训练好的模型。首先,我们将定义一个辅助函数,帮助我们可视化一些示例图像的预测结果及其对应的真实标签:

#A helper function to visualize some samples and their corresponding predictions
def display_samples_predictions(input_features, target_labels, samples_predictions):

 num_classes = 10

 cifar10_class_names = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']

label_binarizer = LabelBinarizer()
 label_binarizer.fit(range(num_classes))
 label_inds = label_binarizer.inverse_transform(np.array(target_labels))

fig, axies = plt.subplots(nrows=4, ncols=2)
 fig.tight_layout()
 fig.suptitle('Softmax Predictions', fontsize=20, y=1.1)

num_predictions = 4
 margin = 0.05
 ind = np.arange(num_predictions)
 width = (1\. - 2\. * margin) / num_predictions

for image_ind, (feature, label_ind, prediction_indicies, prediction_values) in enumerate(zip(input_features, label_inds, samples_predictions.indices, samples_predictions.values)):
 prediction_names = [cifar10_class_names[pred_i] for pred_i in prediction_indicies]
 correct_name = cifar10_class_names[label_ind]

axies[image_ind][0].imshow(feature)
 axies[image_ind][0].set_title(correct_name)
 axies[image_ind][0].set_axis_off()

axies[image_ind][1].barh(ind + margin, prediction_values[::-1], width)
 axies[image_ind][1].set_yticks(ind + margin)
 axies[image_ind][1].set_yticklabels(prediction_names[::-1])
 axies[image_ind][1].set_xticks([0, 0.5, 1.0])

现在,让我们恢复训练好的模型并对测试集进行测试:

test_batch_size = 64
save_model_path = './cifar-10_classification'
#Number of images to visualize
num_samples = 4

#Number of top predictions
top_n_predictions = 4

#Defining a helper function for testing the trained model
def test_classification_model():

 input_test_features, target_test_labels = pickle.load(open('preprocess_test.p', mode='rb'))
 loaded_graph = tf.Graph()
with tf.Session(graph=loaded_graph) as sess:

 # loading the trained model
 model = tf.train.import_meta_graph(save_model_path + '.meta')
 model.restore(sess, save_model_path)

# Getting some input and output Tensors from loaded model
 model_input_values = loaded_graph.get_tensor_by_name('input_images:0')
 model_target = loaded_graph.get_tensor_by_name('input_images_target:0')
 model_keep_prob = loaded_graph.get_tensor_by_name('keep_prob:0')
 model_logits = loaded_graph.get_tensor_by_name('logits:0')
 model_accuracy = loaded_graph.get_tensor_by_name('model_accuracy:0')

 # Testing the trained model on the test set batches
 test_batch_accuracy_total = 0
 test_batch_count = 0

 for input_test_feature_batch, input_test_label_batch in batch_split_features_labels(input_test_features, target_test_labels, test_batch_size):
 test_batch_accuracy_total += sess.run(
 model_accuracy,
 feed_dict={model_input_values: input_test_feature_batch, model_target: input_test_label_batch, model_keep_prob: 1.0})
 test_batch_count += 1

print('Test set accuracy: {}\n'.format(test_batch_accuracy_total/test_batch_count))

# print some random images and their corresponding predictions from the test set results
 random_input_test_features, random_test_target_labels = tuple(zip(*random.sample(list(zip(input_test_features, target_test_labels)), num_samples)))

 random_test_predictions = sess.run(
 tf.nn.top_k(tf.nn.softmax(model_logits), top_n_predictions),
 feed_dict={model_input_values: random_input_test_features, model_target: random_test_target_labels, model_keep_prob: 1.0})

 display_samples_predictions(random_input_test_features, random_test_target_labels, random_test_predictions)

#Calling the function
test_classification_model()

Output:
INFO:tensorflow:Restoring parameters from ./cifar-10_classification
Test set accuracy: 0.7540007961783439

让我们可视化另一个例子,看看一些错误:

现在,我们的测试准确率大约为 75%,对于像我们使用的简单 CNN 来说,这并不算差。

总结

本章向我们展示了如何制作一个 CNN 来分类 CIFAR-10 数据集中的图像。测试集上的分类准确率约为 79% - 80%。卷积层的输出也被绘制出来,但很难看出神经网络是如何识别和分类输入图像的。需要更好的可视化技术。

接下来,我们将使用深度学习中的一种现代且激动人心的实践方法——迁移学习。迁移学习使你能够使用深度学习中的数据需求大的架构,适用于小型数据集。

第九章:目标检测 – 使用卷积神经网络(CNNs)进行迁移学习

“个体如何在一个情境中转移到另一个具有相似特征的情境”

E. L. ThorndikeR. S. Woodworth (1991)

迁移学习TL)是数据科学中的一个研究问题,主要关注在解决特定任务时获得的知识如何得以保存,并利用这些获得的知识来解决另一个不同但相似的任务。在本章中,我们将展示数据科学领域中使用迁移学习的现代实践和常见主题之一。这里的思想是如何从数据集非常大的领域获得帮助,转移到数据集较小的领域。最后,我们将重新审视我们在 CIFAR-10 上的目标检测示例,并尝试通过迁移学习减少训练时间和性能误差。

本章将涵盖以下主题:

  • 迁移学习

  • 重新审视 CIFAR-10 目标检测

迁移学习

深度学习架构对数据的需求很大,训练集中的样本较少时无法充分发挥其潜力。迁移学习通过将从大数据集解决一个任务中学到的知识/表示转移到另一个不同但相似的小数据集任务中,解决了这一问题。

迁移学习不仅对小训练集有用,我们还可以用它来加速训练过程。从头开始训练大型深度学习架构有时会非常慢,因为这些架构中有数百万个权重需要学习。相反,可以通过迁移学习,只需微调在类似问题上学到的权重,而不是从头开始训练模型。

迁移学习的直觉

让我们通过以下师生类比来建立迁移学习的直觉。一位教师在他/她教授的模块中有多年的经验。另一方面,学生从这位教师的讲座中获得了该主题的简洁概述。所以你可以说,教师正在以简洁紧凑的方式将自己的知识传授给学生。

教师与学生的类比同样适用于我们在深度学习或神经网络中知识迁移的情况。我们的模型从数据中学习一些表示,这些表示由网络的权重表示。这些学习到的表示/特征(权重)可以转移到另一个不同但相似的任务中。将学到的权重转移到另一个任务的过程将减少深度学习架构收敛所需的庞大数据集,并且与从头开始训练模型相比,它还会减少将模型适应新数据集所需的时间。

深度学习现在广泛应用,但通常大多数人在训练深度学习架构时都会使用迁移学习(TL);很少有人从零开始训练深度学习架构,因为大多数情况下,深度学习需要的数据集规模通常不足以支持收敛。所以,使用在大型数据集上预训练的模型,如ImageNet(大约有 120 万张图像),并将其应用到新任务上是非常常见的。我们可以使用该预训练模型的权重作为特征提取器,或者我们可以将其作为初始化模型,然后对其进行微调以适应新任务。使用迁移学习有三种主要场景:

  • 使用卷积网络作为固定特征提取器:在这种场景下,你使用在大型数据集(如 ImageNet)上预训练的卷积模型,并将其调整为适应你的问题。例如,一个在 ImageNet 上预训练的卷积模型将有一个全连接层,输出 ImageNet 上 1,000 个类别的得分。所以你需要移除这个层,因为你不再关心 ImageNet 的类别。然后,你将所有其他层当作特征提取器。一旦使用预训练模型提取了特征,你可以将这些特征输入到任何线性分类器中,比如 softmax 分类器,甚至是线性支持向量机(SVM)。

  • 微调卷积神经网络:第二种场景涉及到第一种场景,但增加了使用反向传播在你的新任务上微调预训练权重的额外工作。通常,人们保持大部分层不变,只微调网络的顶部。尝试微调整个网络或大多数层可能导致过拟合。因此,你可能只对微调与图像的语义级别特征相关的那些层感兴趣。保持早期层固定的直觉是,它们包含了大多数图像任务中常见的通用或低级特征,如角点、边缘等。如果你正在引入模型在原始数据集中没有的新类别,那么微调网络的高层或顶部层会非常有用。

图 10.1:为新任务微调预训练的卷积神经网络(CNN)

  • 预训练模型:第三种广泛使用的场景是下载互联网上人们提供的检查点。如果你没有足够的计算能力从零开始训练模型,可以选择这种场景,只需使用发布的检查点初始化模型,然后做一点微调。

传统机器学习和迁移学习(TL)的区别

如你从前一部分中注意到的,传统机器学习和涉及迁移学习(TL)的机器学习有明显的区别(如以下图示*所示)。在传统机器学习中,你不会将任何知识或表示迁移到其他任务中,而在迁移学习中却不同。有时,人们会错误地使用迁移学习,因此我们将列出一些条件,只有在这些条件下使用迁移学习才能最大化收益。

以下是应用迁移学习(TL)的条件:

  • 与传统的机器学习不同,源任务和目标任务或领域不需要来自相同的分布,但它们必须是相似的。

  • 如果训练样本较少或你没有足够的计算能力,你也可以使用迁移学习。

图 10.2:传统机器学习与迁移学习(TL)相结合的机器学习

CIFAR-10 目标检测—重新审视

在上一章中,我们在 CIFAR-10 数据集上训练了一个简单的卷积神经网络CNN)模型。在这里,我们将展示如何使用预训练模型作为特征提取器,同时移除预训练模型的全连接层,然后将提取的特征或迁移值输入 Softmax 层。

这次实现中的预训练模型将是 Inception 模型,它将在 ImageNet 上进行预训练。但请记住,这个实现是基于前两章介绍的 CNN。

解决方案概述

我们将再次替换预训练 Inception 模型的最终全连接层,并使用其余部分作为特征提取器。因此,我们首先将原始图像输入 Inception 模型,模型会从中提取特征,然后输出我们所谓的迁移值。

在从 Inception 模型中获取提取特征的迁移值后,你可能需要将它们保存到本地,因为如果你实时处理,这可能需要一些时间,因此将它们持久化到本地可以节省时间。在 TensorFlow 教程中,他们使用“瓶颈值”这一术语来代替迁移值,但这只是对相同概念的不同命名。

在获得迁移值或从本地加载它们后,我们可以将它们输入到任何为新任务定制的线性分类器中。在这里,我们将提取的迁移值输入到另一个神经网络,并为 CIFAR-10 的新类别进行训练。

以下图示展示了我们将遵循的一般解决方案概述:

图 10.3:使用 CIFAR-10 数据集进行目标检测任务的解决方案概述(使用迁移学习)

加载和探索 CIFAR-10

让我们首先导入本次实现所需的包:

%matplotlib inline
import matplotlib.pyplot as plt
import tensorflow as tf
import numpy as np
import time
from datetime import timedelta
import os

# Importing a helper module for the functions of the Inception model.
import inception

接下来,我们需要加载另一个辅助脚本,以便下载处理 CIFAR-10 数据集:

import cifar10
#importing number of classes of CIFAR-10
from cifar10 import num_classes

如果你还没有做过这一点,你需要设置 CIFAR-10 的路径。这个路径将被 cifar-10.py 脚本用来持久化数据集:

cifar10.data_path = "data/CIFAR-10/"

The CIFAR-10 dataset is about 170 MB, the next line checks if the dataset is already downloaded if not it downloads the dataset and store in the previous data_path:

cifar10.maybe_download_and_extract</span>()

Output:

- Download progress: 100.0%
Download finished. Extracting files.
Done.

让我们来看一下 CIFAR-10 数据集中的类别:

#Loading the class names of CIFAR-10 dataset
class_names = cifar10.load_class_names()
class_names

输出:

Loading data: data/CIFAR-10/cifar-10-batches-py/batches.meta
['airplane',
 'automobile',
 'bird',
 'cat',
 'deer',
 'dog',
 'frog',
 'horse', 
 'ship',
 'truck']
Load the training-set. 

这将返回 images,类别编号作为 integers,以及类别编号作为一种名为 labels 的 one-hot 编码数组:

training_images, training_cls_integers, trainig_one_hot_labels = cifar10.load_training_data()

输出:

Loading data: data/CIFAR-10/cifar-10-batches-py/data_batch_1
Loading data: data/CIFAR-10/cifar-10-batches-py/data_batch_2
Loading data: data/CIFAR-10/cifar-10-batches-py/data_batch_3
Loading data: data/CIFAR-10/cifar-10-batches-py/data_batch_4
Loading data: data/CIFAR-10/cifar-10-batches-py/data_batch_5
Load the test-set.

现在,让我们对测试集做相同的操作,加载图像及其相应的目标类别的整数表示和 one-hot 编码:

#Loading the test images, their class integer, and their corresponding one-hot encoding
testing_images, testing_cls_integers, testing_one_hot_labels = cifar10.load_test_data()

Output:

Loading data: data/CIFAR-10/cifar-10-batches-py/test_batch

让我们看看 CIFAR-10 中训练集和测试集的分布:

print("-Number of images in the training set:\t\t{}".format(len(training_images)))
print("-Number of images in the testing set:\t\t{}".format(len(testing_images)))

输出:

-Number of images in the training set:          50000
-Number of images in the testing set:           10000

让我们定义一些辅助函数,以便我们可以探索数据集。以下辅助函数将把九张图片绘制成网格:

def plot_imgs(imgs, true_class, predicted_class=None):

    assert len(imgs) == len(true_class)

    # Creating a placeholders for 9 subplots
    fig, axes = plt.subplots(3, 3)

    # Adjustting spacing.
    if predicted_class is None:
        hspace = 0.3
    else:
        hspace = 0.6
    fig.subplots_adjust(hspace=hspace, wspace=0.3)

    for i, ax in enumerate(axes.flat):
        # There may be less than 9 images, ensure it doesn't crash.
        if i < len(imgs):
            # Plot image.
            ax.imshow(imgs[i],
                      interpolation='nearest')

            # Get the actual name of the true class from the class_names array
            true_class_name = class_names[true_class[i]]

            # Showing labels for the predicted and true classes
            if predicted_class is None:
                xlabel = "True: {0}".format(true_class_name)
            else:
                # Name of the predicted class.
                predicted_class_name = class_names[predicted_class[i]]

                xlabel = "True: {0}\nPred: {1}".format(true_class_name, predicted_class_name)

            ax.set_xlabel(xlabel)

        # Remove ticks from the plot.
        ax.set_xticks([])
        ax.set_yticks([])

    plt.show()

让我们可视化测试集中的一些图像,并查看它们相应的实际类别:

# get the first 9 images in the test set
imgs = testing_images[0:9]

# Get the integer representation of the true class.
true_class = testing_cls_integers[0:9]

# Plotting the images
plot_imgs(imgs=imgs, true_class=true_class)

输出:

图 10.4:测试集的前九张图片

Inception 模型传递值

如前所述,我们将使用在 ImageNet 数据集上预训练的 Inception 模型。因此,我们需要从互联网上下载这个预训练的模型。

让我们首先定义 data_dir 来为 Inception 模型设置路径:

inception.data_dir = 'inception/'

预训练 Inception 模型的权重大约为 85 MB。如果它不在之前定义的 data_dir 中,以下代码行将下载该模型:

inception.maybe_download()

Downloading Inception v3 Model ...
- Download progress: 100%

我们将加载 Inception 模型,以便可以将其作为特征提取器来处理我们的 CIFAR-10 图像:

# Loading the inception model so that we can inialized it with the pre-trained weights and customize for our model
inception_model = inception.Inception()

如前所述,计算 CIFAR-10 数据集的传递值需要一些时间,因此我们需要将它们缓存以便将来使用。幸运的是,inception 模块中有一个辅助函数可以帮助我们做到这一点:

from inception import transfer_values_cache

接下来,我们需要设置缓存的训练和测试文件的文件路径:

file_path_train = os.path.join(cifar10.data_path, 'inception_cifar10_train.pkl')
file_path_test = os.path.join(cifar10.data_path, 'inception_cifar10_test.pkl')
print("Processing Inception transfer-values for the training images of Cifar-10 ...")
# First we need to scale the imgs to fit the Inception model requirements as it requires all pixels to be from 0 to 255,
# while our training examples of the CIFAR-10 pixels are between 0.0 and 1.0
imgs_scaled = training_images * 255.0

# Checking if the transfer-values for our training images are already calculated and loading them, if not calculate and save them.
transfer_values_training = transfer_values_cache(cache_path=file_path_train,
                                              images=imgs_scaled,
                                              model=inception_model)
print("Processing Inception transfer-values for the testing images of Cifar-10 ...")
# First we need to scale the imgs to fit the Inception model requirements as it requires all pixels to be from 0 to 255,
# while our training examples of the CIFAR-10 pixels are between 0.0 and 1.0
imgs_scaled = testing_images * 255.0
# Checking if the transfer-values for our training images are already calculated and loading them, if not calcaulate and save them.
transfer_values_testing = transfer_values_cache(cache_path=file_path_test,
                                     images=imgs_scaled,
                                     model=inception_model)

如前所述,CIFAR-10 数据集的训练集中有 50,000 张图像。让我们检查这些图像的传递值的形状。每张图像的传递值应该是 2,048:

transfer_values_training.shape

输出:

(50000, 2048)

我们需要对测试集做相同的操作:

transfer_values_testing.shape

输出:

(10000, 2048)

为了直观地理解传递值的样子,我们将定义一个辅助函数,帮助我们绘制训练集或测试集中某张图像的传递值:

def plot_transferValues(ind):
    print("Original input image:")

    # Plot the image at index ind of the test set.
    plt.imshow(testing_images[ind], interpolation='nearest')
    plt.show()

    print("Transfer values using Inception model:")

    # Visualize the transfer values as an image.
    transferValues_img = transfer_values_testing[ind]
    transferValues_img = transferValues_img.reshape((32, 64))

    # Plotting the transfer values image.
    plt.imshow(transferValues_img, interpolation='nearest', cmap='Reds')
    plt.show()
plot_transferValues(i=16)

Input image:

图 10.5:输入图像

使用 Inception 模型的传递值:

图 10.6:图 10.3 中输入图像的传递值

plot_transferValues(i=17)

图 10.7:输入图像

使用 Inception 模型的传递值:

图 10.8:图 10.5 中输入图像的传递值

传递值分析

在这一部分,我们将分析刚刚为训练图像获得的传输值。这次分析的目的是看这些传输值是否足以对我们在 CIFAR-10 中的图像进行分类。

每张输入图像都有 2,048 个传输值。为了绘制这些传输值并对其进行进一步分析,我们可以使用像 scikit-learn 中的主成分分析PCA)这样的降维技术。我们将传输值从 2,048 减少到 2,以便能够可视化它,并查看它们是否能成为区分 CIFAR-10 不同类别的好特征:

from sklearn.decomposition import PCA

接下来,我们需要创建一个 PCA 对象,其中组件数量为2

pca_obj = PCA(n_components=2)

将传输值从 2,048 减少到 2 需要花费很多时间,因此我们将只选取 5,000 张图像中的 3,000 张作为子集:

subset_transferValues = transfer_values_training[0:3000]

我们还需要获取这些图像的类别编号:

cls_integers = testing_cls_integers[0:3000]

我们可以通过打印传输值的形状来再次检查我们的子集:

subset_transferValues.shape

输出:

(3000, 2048)

接下来,我们使用我们的 PCA 对象将传输值从 2,048 减少到仅 2:

reduced_transferValues = pca_obj.fit_transform(subset_transferValues)

现在,让我们看看 PCA 降维过程的输出:

reduced_transferValues.shape

输出:

(3000, 2)

在将传输值的维度减少到仅为 2 之后,让我们绘制这些值:

#Importing the color map for plotting each class with different color.
import matplotlib.cm as color_map

def plot_reduced_transferValues(transferValues, cls_integers):

    # Create a color-map with a different color for each class.
    c_map = color_map.rainbow(np.linspace(0.0, 1.0, num_classes))

    # Getting the color for each sample.
    colors = c_map[cls_integers]

    # Getting the x and y values.
    x_val = transferValues[:, 0]
    y_val = transferValues[:, 1]

    # Plot the transfer values in a scatter plot
    plt.scatter(x_val, y_val, color=colors)
    plt.show()

在这里,我们绘制的是训练集子集的减少后的传输值。CIFAR-10 中有 10 个类别,所以我们将使用不同的颜色绘制它们对应的传输值。从下图可以看出,传输值根据对应的类别被分组。组与组之间的重叠是因为 PCA 的降维过程无法正确分离传输值:

plot_reduced_transferValues(reduced_transferValues, cls_integers)

图 10.9:使用 PCA 减少的传输值

我们可以使用另一种降维方法t-SNE进一步分析我们的传输值:

from sklearn.manifold import TSNE

再次,我们将减少传输值的维度,从 2,048 减少到 50 个值,而不是 2:

pca_obj = PCA(n_components=50)
transferValues_50d = pca_obj.fit_transform(subset_transferValues)

接下来,我们堆叠第二种降维技术,并将 PCA 过程的输出传递给它:

tsne_obj = TSNE(n_components=2)

最后,我们使用 PCA 方法减少后的值并将 t-SNE 方法应用于其上:

reduced_transferValues = tsne_obj.fit_transform(transferValues_50d) 

并再次检查它是否具有正确的形状:

reduced_transferValues.shape

输出:

(3000, 2)

让我们绘制 t-SNE 方法减少后的传输值。正如你在下图中看到的,t-SNE 比 PCA 更好地分离了分组的传输值。

通过这次分析,我们得出的结论是,通过将输入图像输入预训练的 Inception 模型获得的提取传输值,可以用于将训练图像分为 10 个类别。由于下图中存在轻微的重叠,这种分离不会 100%准确,但我们可以通过对预训练模型进行微调来消除这种重叠:

plot_reduced_transferValues(reduced_transferValues, cls_integers)

图 10.10:使用 t-SNE 减少的传输值

现在我们已经提取了训练图像中的转移值,并且知道这些值能够在一定程度上区分 CIFAR-10 中的不同类别。接下来,我们需要构建一个线性分类器,并将这些转移值输入其中,进行实际分类。

模型构建与训练

所以,让我们首先指定将要输入到神经网络模型中的输入占位符变量。第一个输入变量(将包含提取的转移值)的形状将是[None, transfer_len]。第二个占位符变量将以独热向量格式存储训练集的实际类别标签:

transferValues_arrLength = inception_model.transfer_len
input_values = tf.placeholder(tf.float32, shape=[None, transferValues_arrLength], name='input_values')
y_actual = tf.placeholder(tf.float32, shape=[None, num_classes], name='y_actual')

我们还可以通过定义另一个占位符变量,获取每个类别从 1 到 10 的对应整数值:

y_actual_cls = tf.argmax(y_actual, axis=1)

接下来,我们需要构建实际的分类神经网络,该网络将接受这些输入占位符,并生成预测的类别:

def new_weights(shape):
    return tf.Variable(tf.truncated_normal(shape, stddev=0.05))

def new_biases(length):
    return tf.Variable(tf.constant(0.05, shape=[length]))

def new_fc_layer(input,          # The previous layer.
                 num_inputs,     # Num. inputs from prev. layer.
                 num_outputs,    # Num. outputs.
                 use_relu=True): # Use Rectified Linear Unit (ReLU)?

    # Create new weights and biases.
    weights = new_weights(shape=[num_inputs, num_outputs])
    biases = new_biases(length=num_outputs)

    # Calculate the layer as the matrix multiplication of
    # the input and weights, and then add the bias-values.
    layer = tf.matmul(input, weights) + biases

    # Use ReLU?
    if use_relu:
        layer = tf.nn.relu(layer)

    return layer

# First fully-connected layer.
layer_fc1 = new_fc_layer(input=input_values,
                             num_inputs=2048,
                             num_outputs=1024,
                             use_relu=True)

# Second fully-connected layer.
layer_fc2 = new_fc_layer(input=layer_fc1,
                             num_inputs=1024,
                             num_outputs=num_classes,
                             use_relu=False)

# Predicted class-label.
y_predicted = tf.nn.softmax(layer_fc2)

# Cross-entropy for the classification of each image.
cross_entropy = \
    tf.nn.softmax_cross_entropy_with_logits(logits=layer_fc2,
                                                labels=y_actual)

# Loss aka. cost-measure.
# This is the scalar value that must be minimized.
loss = tf.reduce_mean(cross_entropy)

然后,我们需要定义一个优化标准,作为分类器训练过程中使用的准则。在此实现中,我们将使用AdamOptimizer。该分类器的输出将是一个包含 10 个概率分数的数组,对应 CIFAR-10 数据集中类别的数量。接下来,我们将对这个数组应用argmax操作,将最大分数的类别分配给该输入样本:

step = tf.Variable(initial_value=0,
                          name='step', trainable=False)
optimizer = tf.train.AdamOptimizer(learning_rate=1e-4).minimize(loss, step)
y_predicted_cls = tf.argmax(y_predicted, axis=1)
#compare the predicted and true classes
correct_prediction = tf.equal(y_predicted_cls, y_actual_cls)
#cast the boolean values to fload
model_accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

接下来,我们需要定义一个 TensorFlow 会话,实际执行计算图,并初始化我们之前在此实现中定义的变量:

session = tf.Session()
session.run(tf.global_variables_initializer())

在这个实现中,我们将使用随机梯度下降SGD),因此我们需要定义一个函数,从我们包含 50,000 张图像的训练集中随机生成指定大小的批次。

因此,我们将定义一个辅助函数,从输入的训练集转移值中生成一个随机批次:

#defining the size of the train batch
train_batch_size = 64

#defining a function for randomly selecting a batch of images from the dataset
def select_random_batch():
    # Number of images (transfer-values) in the training-set.
    num_imgs = len(transfer_values_training)

    # Create a random index.
    ind = np.random.choice(num_imgs,
                           size=training_batch_size,
                           replace=False)

    # Use the random index to select random x and y-values.
    # We use the transfer-values instead of images as x-values.
    x_batch = transfer_values_training[ind]
    y_batch = trainig_one_hot_labels[ind]

    return x_batch, y_batch

接下来,我们需要定义一个辅助函数,进行实际的优化过程,优化网络的权重。它将在每次迭代时生成一个批次,并根据该批次优化网络:

def optimize(num_iterations):

    for i in range(num_iterations):
        # Selectin a random batch of images for training
        # where the transfer values of the images will be stored in input_batch
        # and the actual labels of those batch of images will be stored in y_actual_batch
        input_batch, y_actual_batch = select_random_batch()

        # storing the batch in a dict with the proper names
        # such as the input placeholder variables that we define above.
        feed_dict = {input_values: input_batch,
                           y_actual: y_actual_batch}

        # Now we call the optimizer of this batch of images
        # TensorFlow will automatically feed the values of the dict we created above
        # to the model input placeholder variables that we defined above.
        i_global, _ = session.run([step, optimizer],
                                  feed_dict=feed_dict)

        # print the accuracy every 100 steps.
        if (i_global % 100 == 0) or (i == num_iterations - 1):
            # Calculate the accuracy on the training-batch.
            batch_accuracy = session.run(model_accuracy,
                                    feed_dict=feed_dict)

            msg = "Step: {0:>6}, Training Accuracy: {1:>6.1%}"
            print(msg.format(i_global, batch_accuracy))

我们将定义一些辅助函数来显示之前神经网络的结果,并展示预测结果的混淆矩阵:

def plot_errors(cls_predicted, cls_correct):

    # cls_predicted is an array of the predicted class-number for
    # all images in the test-set.

    # cls_correct is an array with boolean values to indicate
    # whether is the model predicted the correct class or not.

    # Negate the boolean array.
    incorrect = (cls_correct == False)

    # Get the images from the test-set that have been
    # incorrectly classified. 
    incorrectly_classified_images = testing_images[incorrect]

    # Get the predicted classes for those images.
    cls_predicted = cls_predicted[incorrect]

    # Get the true classes for those images.
    true_class = testing_cls_integers[incorrect]

    n = min(9, len(incorrectly_classified_images))

    # Plot the first n images.
    plot_imgs(imgs=incorrectly_classified_images[0:n],
                true_class=true_class[0:n],
                predicted_class=cls_predicted[0:n])

接下来,我们需要定义一个用于绘制混淆矩阵的辅助函数:

from sklearn.metrics import confusion_matrix

def plot_confusionMatrix(cls_predicted):

    # cls_predicted array of all the predicted 
    # classes numbers in the test.

    # Call the confucion matrix of sklearn
    cm = confusion_matrix(y_true=testing_cls_integers,
                          y_pred=cls_predicted)

    # Printing the confusion matrix
    for i in range(num_classes):
        # Append the class-name to each line.
        class_name = "({}) {}".format(i, class_names[i])
        print(cm[i, :], class_name)

    # labeling each column of the confusion matrix with the class number
    cls_numbers = [" ({0})".format(i) for i in range(num_classes)]
    print("".join(cls_numbers))

此外,我们还将定义另一个辅助函数,用于在测试集上运行训练好的分类器,并测量训练模型在测试集上的准确性:

# Split the data-set in batches of this size to limit RAM usage.
batch_size = 128

def predict_class(transferValues, labels, cls_true):

    # Number of images.
    num_imgs = len(transferValues)

    # Allocate an array for the predicted classes which
    # will be calculated in batches and filled into this array.
    cls_predicted = np.zeros(shape=num_imgs, dtype=np.int)

    # Now calculate the predicted classes for the batches.
    # We will just iterate through all the batches.
    # There might be a more clever and Pythonic way of doing this.

    # The starting index for the next batch is denoted i.
    i = 0

    while i < num_imgs:
        # The ending index for the next batch is denoted j.
        j = min(i + batch_size, num_imgs)

        # Create a feed-dict with the images and labels
        # between index i and j.
        feed_dict = {input_values: transferValues[i:j],
                     y_actual: labels[i:j]}

        # Calculate the predicted class using TensorFlow.
        cls_predicted[i:j] = session.run(y_predicted_cls, feed_dict=feed_dict)

        # Set the start-index for the next batch to the
        # end-index of the current batch.
        i = j

    # Create a boolean array whether each image is correctly classified.
    correct = [a == p for a, p in zip(cls_true, cls_predicted)]

    return correct, cls_predicted

#Calling the above function making the predictions for the test

def predict_cls_test():
    return predict_class(transferValues = transfer_values_test,
                       labels = labels_test,
                       cls_true = cls_test)

def classification_accuracy(correct):
    # When averaging a boolean array, False means 0 and True means 1.
    # So we are calculating: number of True / len(correct) which is
    # the same as the classification accuracy.

    # Return the classification accuracy
    # and the number of correct classifications.
    return np.mean(correct), np.sum(correct)

def test_accuracy(show_example_errors=False,
                        show_confusion_matrix=False):

    # For all the images in the test-set,
    # calculate the predicted classes and whether they are correct.
    correct, cls_pred = predict_class_test()

    # Classification accuracypredict_class_test and the number of correct classifications.
    accuracy, num_correct = classification_accuracy(correct)

    # Number of images being classified.
    num_images = len(correct)

    # Print the accuracy.
    msg = "Test set accuracy: {0:.1%} ({1} / {2})"
    print(msg.format(accuracy, num_correct, num_images))

    # Plot some examples of mis-classifications, if desired.
    if show_example_errors:
        print("Example errors:")
        plot_errors(cls_predicted=cls_pred, cls_correct=correct)

    # Plot the confusion matrix, if desired.
    if show_confusion_matrix:
        print("Confusion Matrix:")
        plot_confusionMatrix(cls_predicted=cls_pred)

在进行任何优化之前,让我们看看之前神经网络模型的表现:

test_accuracy(show_example_errors=True,
                    show_confusion_matrix=True)

Accuracy on Test-Set: 9.4% (939 / 10000)

如你所见,网络的表现非常差,但在我们基于已定义的优化标准进行一些优化后,性能会有所提升。因此,我们将运行优化器进行 10,000 次迭代,并在此之后测试模型的准确性:

optimize(num_iterations=10000)
test_accuracy(show_example_errors=True,
                           show_confusion_matrix=True)
Accuracy on Test-Set: 90.7% (9069 / 10000)
Example errors:

图 10.11:来自测试集的部分误分类图像

Confusion Matrix:
[926   6  13   2   3   0   1   1  29  19] (0) airplane
[  9 921   2   5   0   1   1   1   2  58] (1) automobile
[ 18   1 883  31  32   4  22   5   1   3] (2) bird
[  7   2  19 855  23  57  24   9   2   2] (3) cat
[  5   0  21  25 896   4  24  22   2   1] (4) deer
[  2   0  12  97  18 843  10  15   1   2] (5) dog
[  2   1  16  17  17   4 940   1   2   0] (6) frog
[  8   0  10  19  28  14   1 914   2   4] (7) horse
[ 42   6   1   4   1   0   2   0 932  12] (8) ship
[  6  19   2   2   1   0   1   1   9 959] (9) truck
 (0) (1) (2) (3) (4) (5) (6) (7) (8) (9)

最后,我们将结束之前打开的会话:

model.close()
session.close()

总结

在本章中,我们介绍了深度学习中最广泛使用的最佳实践之一。TL 是一个非常令人兴奋的工具,您可以利用它让深度学习架构从小数据集进行学习,但请确保您以正确的方式使用它。

接下来,我们将介绍一种广泛应用于自然语言处理的深度学习架构。这些递归型架构在大多数 NLP 领域取得了突破:机器翻译、语音识别、语言建模和情感分析。

第十章:递归类型神经网络 - 语言建模

递归神经网络RNNs)是一类广泛用于自然语言处理的深度学习架构。这类架构使我们能够为当前的预测提供上下文信息,并且具有处理任何输入序列中长期依赖性的特定架构。在本章中,我们将展示如何构建一个序列到序列模型,这将在 NLP 的许多应用中非常有用。我们将通过构建一个字符级语言模型来展示这些概念,并查看我们的模型如何生成与原始输入序列相似的句子。

本章将涵盖以下主题:

  • RNNs 背后的直觉

  • LSTM 网络

  • 语言模型的实现

RNNs 背后的直觉

到目前为止,我们处理的所有深度学习架构都没有机制来记住它们之前接收到的输入。例如,如果你给前馈神经网络FNN)输入一串字符,例如HELLO,当网络处理到E时,你会发现它没有保留任何信息/忘记了它刚刚读取的H。这是基于序列的学习的一个严重问题。由于它没有记住任何它读取过的先前字符,这种网络将非常难以训练来预测下一个字符。这对于许多应用(如语言建模、机器翻译、语音识别等)来说是没有意义的。

出于这个特定的原因,我们将介绍 RNNs,这是一组能够保存信息并记住它们刚刚遇到的内容的深度学习架构。

让我们展示 RNNs 如何在相同的字符输入序列HELLO上工作。当 RNN 单元接收到E作为输入时,它也接收到先前输入的字符H。这种将当前字符和先前字符一起作为输入传递给 RNN 单元的做法为这些架构提供了一个巨大优势,即短期记忆;它还使这些架构能够用于预测/推测在这个特定字符序列中H之后最可能的字符,即L

我们已经看到,先前的架构将权重分配给它们的输入;RNNs 遵循相同的优化过程,将权重分配给它们的多个输入,包括当前输入和过去输入。因此,在这种情况下,网络将为每个输入分配两个不同的权重矩阵。为了做到这一点,我们将使用梯度下降和一种更重的反向传播版本,称为时间反向传播BPTT)。

递归神经网络架构

根据我们对以前深度学习架构的了解,你会发现 RNN 是特别的。我们学过的前几种架构在输入或训练方面并不灵活。它们接受固定大小的序列/向量/图像作为输入,并产生另一个固定大小的输出。RNN 架构则有所不同,因为它们允许你输入一个序列并得到另一个序列作为输出,或者仅在输入/输出中使用序列,如图 1所示。这种灵活性对于多种应用,如语言建模和情感分析,非常有用:

图 1:RNN 在输入或输出形状上的灵活性(karpathy.github.io/2015/05/21/rnn-effectiveness/

这些架构的直观原理是模仿人类处理信息的方式。在任何典型的对话中,你对某人话语的理解完全依赖于他之前说了什么,甚至可能根据他刚刚说的内容预测他接下来会说什么。

在 RNN 的情况下,应该遵循完全相同的过程。例如,假设你想翻译句子中的某个特定单词。你不能使用传统的 FNN,因为它们无法将之前单词的翻译作为输入与当前我们想翻译的单词结合使用,这可能导致翻译错误,因为缺少与该单词相关的上下文信息。

RNN 保留了关于过去的信息,并且它们具有某种循环结构,允许在任何给定时刻将之前学到的信息用于当前的预测:

图 2:具有循环结构的 RNN 架构,用于保留过去步骤的信息(来源:colah.github.io/posts/2015-08-Understanding-LSTMs/

图 2中,我们有一些神经网络称为A,它接收输入 X[t] 并生成输出 h[t]。同时,它借助这个循环接收来自过去步骤的信息。

这个循环看起来似乎不太清楚,但如果我们使用图 2的展开版本,你会发现它非常简单且直观,RNN 其实就是同一网络的重复版本(这可以是普通的 FNN),如图 3所示:

图 3:递归神经网络架构的展开版本(来源:colah.github.io/posts/2015-08-Understanding-LSTMs/

RNN 的这种直观架构及其在输入/输出形状上的灵活性,使得它们非常适合处理有趣的基于序列的学习任务,如机器翻译、语言建模、情感分析、图像描述等。

RNN 的示例

现在,我们对循环神经网络(RNN)的工作原理有了直观的理解,也了解它在不同有趣的基于序列的例子中的应用。让我们更深入地了解一些这些有趣的例子。

字符级语言模型

语言建模是许多应用中一个至关重要的任务,如语音识别、机器翻译等。在本节中,我们将尝试模拟 RNN 的训练过程,并更深入地理解这些网络的工作方式。我们将构建一个基于字符的语言模型。所以,我们将向网络提供一段文本,目的是尝试建立一个概率分布,用于预测给定前一个字符后的下一个字符的概率,这将使我们能够生成类似于我们在训练过程中输入的文本。

例如,假设我们有一个词汇表仅包含四个字母,helo

任务是训练一个循环神经网络,处理一个特定的字符输入序列,如hello。在这个具体的例子中,我们有四个训练样本:

  1. 给定第一个输入字符h的上下文,应该计算字符e的概率,

  2. 给定he的上下文,应该计算字符l的概率,

  3. 给定hel的上下文,应该计算字符l的概率,

  4. 最终,给定hell的上下文,应该计算字符o的概率。

正如我们在前几章中学到的那样,机器学习技术,深度学习也属于其中的一部分,一般只接受实数值作为输入。因此,我们需要以某种方式将输入字符转换或编码为数字形式。为此,我们将使用 one-hot 向量编码,这是一种通过将一个向量中除一个位置外其他位置填充为零的方式来编码文本,其中该位置的索引表示我们试图建模的语言(在此为helo)中的字符索引。在对训练样本进行编码后,我们将逐个提供给 RNN 类型的模型。在给定的每个字符时,RNN 类型模型的输出将是一个四维向量(该向量的大小对应于词汇表的大小),表示词汇表中每个字符作为下一个字符的概率。图 4 清楚地说明了这个过程:

图 4:RNN 类型网络的示例,输入为通过 one-hot 向量编码的字符,输出是词汇表中的分布,表示当前字符后最可能的字符(来源:http://karpathy.github.io/2015/05/21/rnn-effectiveness/)

图 4所示,你可以看到我们将输入序列中的第一个字符h喂给模型,输出是一个四维向量,表示下一个字符的置信度。所以它对h作为下一个字符的置信度是1.0,对e2.2,对l-3.0,对o4.1。在这个特定的例子中,我们知道下一个正确字符是e,基于我们的训练序列hello。所以,我们在训练这个 RNN 类型网络时的主要目标是增加e作为下一个字符的置信度,并减少其他字符的置信度。为了进行这种优化,我们将使用梯度下降和反向传播算法来更新权重,影响网络产生更高置信度的正确下一个字符e,并以此类推,处理其他三个训练例子。

如你所见,RNN 类型网络的输出会产生一个对所有词汇中字符的置信度分布,表示下一个字符可能性。我们可以将这种置信度分布转化为概率分布,使得某个字符作为下一个字符的概率增加时,其他字符的概率会相应减少,因为概率总和必须为 1。对于这种特定的修改,我们可以对每个输出向量使用一个标准的 Softmax 层。

为了从这些类型的网络生成文本,我们可以将一个初始字符输入模型,并得到一个关于下一个字符可能性的概率分布,然后我们可以从这些字符中采样并将其反馈作为输入给模型。通过重复这一过程多次,我们就能生成一个具有所需长度的字符序列。

使用莎士比亚数据的语言模型

从前面的例子中,我们可以得到生成文本的模型。但网络会让我们惊讶,因为它不仅仅会生成文本,还会学习训练数据中的风格和结构。我们可以通过训练一个 RNN 类型的模型来展示这一有趣的过程,使用具有结构和风格的特定文本,例如以下的莎士比亚作品。

让我们来看看从训练好的网络生成的输出:

第二位参议员:

他们远离了我灵魂上的痛苦,

当我死去时,打破并强烈应当埋葬

许多国家的地球与思想。

尽管网络一次只知道如何生成一个字符,但它还是能够生成有意义的文本和实际具有莎士比亚作品风格和结构的名字。

梯度消失问题

在训练这些 RNN 类型架构时,我们使用梯度下降和通过时间的反向传播,这些方法为许多基于序列的学习任务带来了成功。但是,由于梯度的性质以及使用快速训练策略,研究表明梯度值往往会变得过小并消失。这一过程引发了许多从业者遇到的梯度消失问题。接下来,在本章中,我们将讨论研究人员如何解决这些问题,并提出了传统 RNN 的变种来克服这个问题:

图 5:梯度消失问题

长期依赖问题

研究人员面临的另一个挑战性问题是文本中的长期依赖。例如,如果有人输入类似 我曾经住在法国,并且我学会了如何说…… 的序列,那么接下来的显而易见的词是 French。

在这种情况下,传统 RNN 能够处理短期依赖问题,如图 6所示:

图 6:展示文本中的短期依赖(来源:http://colah.github.io/posts/2015-08-Understanding-LSTMs/)

另一个例子是,如果某人开始输入 我曾经住在法国…… 然后描述住在那里的一些美好经历,最后以 我学会了说法语 结束序列。那么,为了让模型预测他/她在序列结束时学会的语言,模型需要保留早期词汇 liveFrance 的信息。如果模型不能有效地跟踪文本中的长期依赖,它就无法处理这种情况:

图 7:文本中长期依赖问题的挑战(来源:http://colah.github.io/posts/2015-08-Understanding-LSTMs/)

为了处理文本中的梯度消失和长期依赖问题,研究人员引入了一种名为长短时记忆网络LSTM)的变种网络。

LSTM 网络

LSTM 是一种 RNN 的变种,用于帮助学习文本中的长期依赖。LSTM 最初由 Hochreiter 和 Schmidhuber(1997 年)提出(链接:www.bioinf.jku.at/publications/older/2604.pdf),许多研究者在此基础上展开了工作,并在多个领域取得了有趣的成果。

这些架构能够处理文本中长期依赖的问题,因为它们的内部架构设计使然。

LSTM 与传统的 RNN 相似,都具有一个随着时间重复的模块,但这个重复模块的内部结构与传统 RNN 不同。它包括更多的层,用于遗忘和更新信息:

图 8:标准 RNN 中包含单一层的重复模块(来源:http://colah.github.io/posts/2015-08-Understanding-LSTMs/)

如前所述,基础 RNN 只有一个神经网络层,而 LSTM 有四个不同的层以特殊的方式相互作用。这种特殊的交互方式使得 LSTM 在许多领域中表现得非常好,正如我们在构建语言模型示例时会看到的那样:

图 9:LSTM 中包含四个交互层的重复模块(来源:http://colah.github.io/posts/2015-08-Understanding-LSTMs/)

关于数学细节以及四个层是如何相互作用的更多信息,可以参考这个有趣的教程:colah.github.io/posts/2015-08-Understanding-LSTMs/

为什么 LSTM 有效?

我们的基础 LSTM 架构的第一步是决定哪些信息是不必要的,它通过丢弃这些信息,为更重要的信息留出更多空间。为此,我们有一个叫做遗忘门层的层,它查看前一个输出h[t-1]和当前输入x[t],并决定我们要丢弃哪些信息。

LSTM 架构中的下一步是决定哪些信息值得保留/持久化并存储到细胞中。这是通过两个步骤完成的:

  1. 一个叫做输入门层的层,决定了哪些来自前一状态的值需要被更新

  2. 第二步是生成一组新的候选值,这些值将被添加到细胞中

最后,我们需要决定 LSTM 单元将输出什么。这个输出将基于我们的细胞状态,但会是一个经过筛选的版本。

语言模型的实现

在本节中,我们将构建一个基于字符的语言模型。在这个实现中,我们将使用《安娜·卡列尼娜》小说,并观察网络如何学习实现文本的结构和风格:

图 10:字符级 RNN 的一般架构(来源:http://karpathy.github.io/2015/05/21/rnn-effectiveness/)

该网络基于 Andrej Karpathy 关于 RNN 的文章(链接:karpathy.github.io/2015/05/21/rnn-effectiveness/)和在 Torch 中的实现(链接:github.com/karpathy/char-rnn)。此外,这里还有一些来自 r2rt 的资料(链接:r2rt.com/recurrent-neural-networks-in-tensorflow-ii.html)以及 Sherjil Ozairp(链接:github.com/sherjilozair/char-rnn-tensorflow)在 GitHub 上的内容。以下是字符级 RNN 的一般架构。

我们将构建一个基于《安娜·卡列尼娜》小说的字符级 RNN(链接:en.wikipedia.org/wiki/Anna_Karenina)。它将能够基于书中的文本生成新的文本。你将在这个实现的资源包中找到.txt文件。

让我们首先导入实现字符级别操作所需的库:

import numpy as np
import tensorflow as tf

from collections import namedtuple

首先,我们需要通过加载数据集并将其转换为整数来准备数据集。因此,我们将字符转换为整数,然后将其编码为整数,这使得它可以作为模型的输入变量,直观且易于使用:

#reading the Anna Karenina novel text file
with open('Anna_Karenina.txt', 'r') as f:
    textlines=f.read()

#Building the vocan and encoding the characters as integers
language_vocab = set(textlines)
vocab_to_integer = {char: j for j, char in enumerate(language_vocab)}
integer_to_vocab = dict(enumerate(language_vocab))
encoded_vocab = np.array([vocab_to_integer[char] for char in textlines], dtype=np.int32)

让我们看一下《安娜·卡列尼娜》文本中的前 200 个字符:

textlines[:200]
Output:
"Chapter 1\n\n\nHappy families are all alike; every unhappy family is unhappy in its own\nway.\n\nEverything was in confusion in the Oblonskys' house. The wife had\ndiscovered that the husband was carrying on"

我们还将字符转换为适合网络使用的便捷形式,即整数。因此,让我们来看一下这些字符的编码版本:

encoded_vocab[:200]
Output:
array([70, 34, 54, 29, 24, 19, 76, 45, 2, 79, 79, 79, 69, 54, 29, 29, 49,
       45, 66, 54, 39, 15, 44, 15, 19, 12, 45, 54, 76, 19, 45, 54, 44, 44,
      45, 54, 44, 15, 27, 19, 58, 45, 19, 30, 19, 76, 49, 45, 59, 56, 34,
       54, 29, 29, 49, 45, 66, 54, 39, 15, 44, 49, 45, 15, 12, 45, 59, 56,
       34, 54, 29, 29, 49, 45, 15, 56, 45, 15, 24, 12, 45, 11, 35, 56, 79,
       35, 54, 49, 53, 79, 79, 36, 30, 19, 76, 49, 24, 34, 15, 56, 16, 45,
       35, 54, 12, 45, 15, 56, 45, 31, 11, 56, 66, 59, 12, 15, 11, 56, 45,
       15, 56, 45, 24, 34, 19, 45, 1, 82, 44, 11, 56, 12, 27, 49, 12, 37,
       45, 34, 11, 59, 12, 19, 53, 45, 21, 34, 19, 45, 35, 15, 66, 19, 45,
       34, 54, 64, 79, 64, 15, 12, 31, 11, 30, 19, 76, 19, 64, 45, 24, 34,
       54, 24, 45, 24, 34, 19, 45, 34, 59, 12, 82, 54, 56, 64, 45, 35, 54,
       12, 45, 31, 54, 76, 76, 49, 15, 56, 16, 45, 11, 56], dtype=int32)

由于网络处理的是单个字符,因此它类似于一个分类问题,我们试图从之前的文本中预测下一个字符。以下是我们网络需要选择的类别数。

因此,我们将一次喂给模型一个字符,模型将通过对可能出现的下一个字符(词汇表中的字符)的概率分布进行预测,从而预测下一个字符,这相当于网络需要从中选择的多个类别:

len(language_vocab)
Output:
83

由于我们将使用随机梯度下降来训练我们的模型,因此我们需要将数据转换为训练批次。

用于训练的小批次生成

在本节中,我们将把数据分成小批次以供训练使用。因此,这些批次将包含许多具有所需序列步数的序列。让我们在图 11中查看一个可视化示例:

图 11:批次和序列的可视化示例(来源:oscarmore2.github.io/Anna_KaRNNa_files/charseq.jpeg

现在,我们需要定义一个函数,该函数将遍历编码后的文本并生成批次。在这个函数中,我们将使用 Python 中的一个非常棒的机制,叫做yield(链接:jeffknupp.com/blog/2013/04/07/improve-your-python-yield-and-generators-explained/)。

一个典型的批次将包含N × M个字符,其中N是序列的数量,M是序列步数的数量。为了获得数据集中可能的批次数,我们可以简单地将数据的长度除以所需的批次大小,然后在得到这个可能的批次数后,我们可以确定每个批次应该包含多少个字符。

之后,我们需要将现有数据集拆分成所需数量的序列(N)。我们可以使用 arr.reshape(size)。我们知道我们需要 N 个序列(在后续代码中使用 num_seqs),让我们将其作为第一维的大小。对于第二维,你可以使用 -1 作为占位符,它会为你填充适当的数据。这样,你应该得到一个形状为 N × (M * K) 的数组,其中 K 是批次的数量。

现在我们有了这个数组,可以通过它进行迭代以获取训练批次,每个批次包含 N × M 个字符。对于每个后续批次,窗口会向右移动 num_steps。最后,我们还需要创建输入和输出数组,以便将它们用作模型输入。这一步创建输出值非常简单;记住,目标是将输入移位一个字符。你通常会看到第一个输入字符作为最后一个目标字符使用,像这样:

其中 x 是输入批次,y 是目标批次。

我喜欢通过使用 range 函数来做这个窗口,步长为 num_steps,从 0 到 arr.shape[1],也就是每个序列的总步数。这样,你从 range 函数得到的整数始终指向一个批次的开始,每个窗口宽度为 num_steps

def generate_character_batches(data, num_seq, num_steps):
    '''Create a function that returns batches of size
       num_seq x num_steps from data.
    '''
    # Get the number of characters per batch and number of batches
    num_char_per_batch = num_seq * num_steps
    num_batches = len(data)//num_char_per_batch

    # Keep only enough characters to make full batches
    data = data[:num_batches * num_char_per_batch]

    # Reshape the array into n_seqs rows
    data = data.reshape((num_seq, -1))

    for i in range(0, data.shape[1], num_steps):
        # The input variables
        input_x = data[:, i:i+num_steps]

        # The output variables which are shifted by one
        output_y = np.zeros_like(input_x)

        output_y[:, :-1], output_y[:, -1] = input_x[:, 1:], input_x[:, 0]
        yield input_x, output_y

所以,让我们使用这个函数来演示,通过生成一个包含 15 个序列和 50 个序列步骤的批次:

generated_batches = generate_character_batches(encoded_vocab, 15, 50)
input_x, output_y = next(generated_batches)
print('input\n', input_x[:10, :10])
print('\ntarget\n', output_y[:10, :10])
Output:

input
 [[70 34 54 29 24 19 76 45 2 79]
 [45 19 44 15 16 15 82 44 19 45]
 [11 45 44 15 16 34 24 38 34 19]
 [45 34 54 64 45 82 19 19 56 45]
 [45 11 56 19 45 27 56 19 35 79]
 [49 19 54 76 12 45 44 54 12 24]
 [45 41 19 45 16 11 45 15 56 24]
 [11 35 45 24 11 45 39 54 27 19]
 [82 19 66 11 76 19 45 81 19 56]
 [12 54 16 19 45 44 15 27 19 45]]

target
 [[34 54 29 24 19 76 45 2 79 79]
 [19 44 15 16 15 82 44 19 45 16]
 [45 44 15 16 34 24 38 34 19 54]
 [34 54 64 45 82 19 19 56 45 82]
 [11 56 19 45 27 56 19 35 79 35]
 [19 54 76 12 45 44 54 12 24 45]
 [41 19 45 16 11 45 15 56 24 11]
 [35 45 24 11 45 39 54 27 19 33]
 [19 66 11 76 19 45 81 19 56 24]
 [54 16 19 45 44 15 27 19 45 24]]

接下来,我们将着手构建本示例的核心部分,即 LSTM 模型。

构建模型

在深入使用 LSTM 构建字符级模型之前,值得提到一个叫做 堆叠 LSTM 的概念。

堆叠 LSTM 对于在不同时间尺度上查看信息非常有用。

堆叠 LSTM

“通过将多个递归隐藏状态堆叠在一起构建深度 RNN。这种方法可以使每个层次的隐藏状态在不同的时间尺度上运行。” ——《如何构建深度递归神经网络》(链接: https://arxiv.org/abs/1312.6026),2013

“RNN 本质上在时间上是深度的,因为它们的隐藏状态是所有先前隐藏状态的函数。启发本文的一个问题是,RNN 是否也能从空间深度中受益;也就是将多个递归隐藏层堆叠在一起,就像在传统深度网络中堆叠前馈层一样。” ——《深度 RNN 的语音识别》(链接: arxiv.org/abs/1303.5778),2013 年

大多数研究人员都在使用堆叠 LSTM 来解决具有挑战性的序列预测问题。堆叠 LSTM 架构可以定义为由多个 LSTM 层组成的 LSTM 模型。前面的 LSTM 层为 LSTM 层提供序列输出,而不是单一的值输出,如下所示。

具体来说,它是每个输入时间步都有一个输出,而不是所有输入时间步都只有一个输出时间步:

图 12:堆叠 LSTM

所以,在这个例子中,我们将使用这种堆叠 LSTM 架构,它能提供更好的性能。

模型架构

在这里你将构建网络。我们将把它分成几个部分,这样更容易理解每个部分。然后,我们可以将它们连接成一个完整的网络:

图 13:字符级模型架构

输入

现在,让我们开始定义模型输入作为占位符。模型的输入将是训练数据和目标。我们还将使用一个叫做keep_probability的参数用于 dropout 层,帮助模型避免过拟合:

def build_model_inputs(batch_size, num_steps):

    # Declare placeholders for the input and output variables
    inputs_x = tf.placeholder(tf.int32, [batch_size, num_steps], name='inputs')
    targets_y = tf.placeholder(tf.int32, [batch_size, num_steps], name='targets')

    # define the keep_probability for the dropout layer
    keep_probability = tf.placeholder(tf.float32, name='keep_prob')

    return inputs_x, targets_y, keep_probability

构建一个 LSTM 单元

在这一部分,我们将编写一个函数来创建 LSTM 单元,这将用于隐藏层。这个单元将是我们模型的构建块。因此,我们将使用 TensorFlow 来创建这个单元。让我们看看如何使用 TensorFlow 构建一个基本的 LSTM 单元。

我们调用以下代码行来创建一个 LSTM 单元,参数num_units表示隐藏层中的单元数:

lstm_cell = tf.contrib.rnn.BasicLSTMCell(num_units)

为了防止过拟合,我们可以使用一种叫做dropout的技术,它通过减少模型的复杂度来防止模型过拟合数据:

tf.contrib.rnn.DropoutWrapper(lstm, output_keep_prob=keep_probability)

正如我们之前提到的,我们将使用堆叠 LSTM 架构;它将帮助我们从不同角度查看数据,并且在实践中已被证明能表现得更好。为了在 TensorFlow 中定义堆叠 LSTM,我们可以使用tf.contrib.rnn.MultiRNNCell函数(链接:www.tensorflow.org/versions/r1.0/api_docs/python/tf/contrib/rnn/MultiRNNCell):

tf.contrib.rnn.MultiRNNCell([cell]*num_layers)

初始时,对于第一个单元,没有前一个信息,因此我们需要将单元状态初始化为零。我们可以使用以下函数来实现:

initial_state = cell.zero_state(batch_size, tf.float32)

那么,让我们把所有部分结合起来,创建我们的 LSTM 单元:

def build_lstm_cell(size, num_layers, batch_size, keep_probability):

    ### Building the LSTM Cell using the tensorflow function
    lstm_cell = tf.contrib.rnn.BasicLSTMCell(size)

    # Adding dropout to the layer to prevent overfitting
    drop_layer = tf.contrib.rnn.DropoutWrapper(lstm_cell, output_keep_prob=keep_probability)

    # Add muliple cells together and stack them up to oprovide a level of more understanding
    stakced_cell = tf.contrib.rnn.MultiRNNCell([drop_layer] * num_layers)
    initial_cell_state = lstm_cell.zero_state(batch_size, tf.float32)

    return lstm_cell, initial_cell_state

RNN 输出

接下来,我们需要创建输出层,负责读取各个 LSTM 单元的输出并通过全连接层传递。这个层有一个 softmax 输出,用于生成可能出现的下一个字符的概率分布。

如你所知,我们为网络生成了输入批次,大小为 N × M 字符,其中 N 是该批次中的序列数,M 是序列步数。我们在创建模型时也使用了 L 个隐藏单元。根据批次大小和隐藏单元的数量,网络的输出将是一个 3D Tensor,大小为 N × M × L,这是因为我们调用 LSTM 单元 M 次,每次处理一个序列步。每次调用 LSTM 单元会产生一个大小为 L 的输出。最后,我们需要做的就是执行 N 次,即序列的数量。

然后,我们将这个 N × M × L 的输出传递给一个全连接层(所有输出使用相同的权重),但在此之前,我们将输出重新调整为一个 2D 张量,形状为 (M * N) × L。这个重新调整形状将使我们在处理输出时更加简便,因为新的形状会更方便;每一行的值代表了 LSTM 单元的 L 个输出,因此每一行对应一个序列和步骤。

在获取新形状之后,我们可以通过矩阵乘法将其与权重相乘,将其连接到带有 softmax 的全连接层。LSTM 单元中创建的权重和我们在这里即将创建的权重默认使用相同的名称,这样 TensorFlow 就会抛出错误。为避免这个错误,我们可以使用 TensorFlow 函数 tf.variable_scope() 将在这里创建的权重和偏置变量封装在一个变量作用域内。

在解释了输出的形状以及如何重新调整形状后,为了简化操作,我们继续编写这个 build_model_output 函数:

def build_model_output(output, input_size, output_size):

    # Reshaping output of the model to become a bunch of rows, where each row correspond for each step in the seq
    sequence_output = tf.concat(output, axis=1)
    reshaped_output = tf.reshape(sequence_output, [-1, input_size])

    # Connect the RNN outputs to a softmax layer
    with tf.variable_scope('softmax'):
        softmax_w = tf.Variable(tf.truncated_normal((input_size, output_size), stddev=0.1))
        softmax_b = tf.Variable(tf.zeros(output_size))

    # the output is a set of rows of LSTM cell outputs, so the logits will be a set
    # of rows of logit outputs, one for each step and sequence
    logits = tf.matmul(reshaped_output, softmax_w) + softmax_b

    # Use softmax to get the probabilities for predicted characters
    model_out = tf.nn.softmax(logits, name='predictions')

    return model_out, logits

训练损失

接下来是训练损失。我们获取 logits 和 targets,并计算 softmax 交叉熵损失。首先,我们需要对 targets 进行 one-hot 编码;我们得到的是编码后的字符。然后,我们重新调整 one-hot targets 的形状,使其成为一个 2D 张量,大小为 (M * N) × C,其中 C 是我们拥有的类别/字符数。记住,我们已经调整了 LSTM 输出的形状,并通过一个具有 C 单元的全连接层。于是,我们的 logits 也将具有大小 (M * N) × C

然后,我们将 logitstargets 输入到 tf.nn.softmax_cross_entropy_with_logits 中,并计算其均值以获得损失:

def model_loss(logits, targets, lstm_size, num_classes):

    # convert the targets to one-hot encoded and reshape them to match the logits, one row per batch_size per step
    output_y_one_hot = tf.one_hot(targets, num_classes)
    output_y_reshaped = tf.reshape(output_y_one_hot, logits.get_shape())

    #Use the cross entropy loss
    model_loss = tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=output_y_reshaped)
    model_loss = tf.reduce_mean(model_loss)
    return model_loss

优化器

最后,我们需要使用一种优化方法,帮助我们从数据集中学习到一些东西。正如我们所知,普通的 RNN 存在梯度爆炸和梯度消失的问题。LSTM 仅解决了其中一个问题,即梯度值的消失,但即使使用了 LSTM,仍然有一些梯度值会爆炸并且无限增大。为了解决这个问题,我们可以使用一种叫做梯度裁剪的技术,它可以将爆炸的梯度裁剪到一个特定的阈值。

所以,让我们通过使用 Adam 优化器来定义我们的优化器,用于学习过程:

def build_model_optimizer(model_loss, learning_rate, grad_clip):

    # define optimizer for training, using gradient clipping to avoid the exploding of the gradients
    trainable_variables = tf.trainable_variables()
    gradients, _ = tf.clip_by_global_norm(tf.gradients(model_loss, trainable_variables), grad_clip)

    #Use Adam Optimizer
    train_operation = tf.train.AdamOptimizer(learning_rate)
    model_optimizer = train_operation.apply_gradients(zip(gradients, trainable_variables))

    return model_optimizer

构建网络

现在,我们可以将所有部分组合起来,构建一个网络的类。为了真正将数据传递到 LSTM 单元,我们将使用tf.nn.dynamic_rnn(链接:www.tensorflow.org/versions/r1.0/api_docs/python/tf/nn/dynamic_rnn)。这个函数会适当地传递隐藏状态和单元状态给 LSTM 单元。它返回每个序列中每个 LSTM 单元在每个步骤的输出。它还会给我们最终的 LSTM 状态。我们希望将这个状态保存为final_state,以便在下一次 mini-batch 运行时将其传递给第一个 LSTM 单元。对于tf.nn.dynamic_rnn,我们传入从build_lstm获得的单元和初始状态,以及我们的输入序列。此外,我们需要对输入进行 one-hot 编码,然后才能进入 RNN:

class CharLSTM:

    def __init__(self, num_classes, batch_size=64, num_steps=50, 
                       lstm_size=128, num_layers=2, learning_rate=0.001, 
                       grad_clip=5, sampling=False):

        # When we're using this network for generating text by sampling, we'll be providing the network with
        # one character at a time, so providing an option for it.
        if sampling == True:
            batch_size, num_steps = 1, 1
        else:
            batch_size, num_steps = batch_size, num_steps

        tf.reset_default_graph()

        # Build the model inputs placeholders of the input and target variables
        self.inputs, self.targets, self.keep_prob = build_model_inputs(batch_size, num_steps)

        # Building the LSTM cell
        lstm_cell, self.initial_state = build_lstm_cell(lstm_size, num_layers, batch_size, self.keep_prob)

        ### Run the data through the LSTM layers
        # one_hot encode the input
        input_x_one_hot = tf.one_hot(self.inputs, num_classes)

        # Runing each sequence step through the LSTM architecture and finally collecting the outputs
        outputs, state = tf.nn.dynamic_rnn(lstm_cell, input_x_one_hot, initial_state=self.initial_state)
        self.final_state = state

        # Get softmax predictions and logits
        self.prediction, self.logits = build_model_output(outputs, lstm_size, num_classes)

        # Loss and optimizer (with gradient clipping)
        self.loss = model_loss(self.logits, self.targets, lstm_size, num_classes)
        self.optimizer = build_model_optimizer(self.loss, learning_rate, grad_clip)

模型超参数

和任何深度学习架构一样,有一些超参数可以用来控制模型并进行微调。以下是我们为这个架构使用的超参数集:

  • 批次大小是每次通过网络运行的序列数量。

  • 步骤数是网络训练过程中序列中的字符数量。通常,越大越好;网络将学习更多的长程依赖,但训练时间也会更长。100 通常是一个不错的数字。

  • LSTM 的大小是隐藏层中单元的数量。

  • 架构层数是要使用的隐藏 LSTM 层的数量。

  • 学习率是训练中典型的学习率。

  • 最后,我们引入了一个新的概念,叫做保持概率,它由 dropout 层使用;它帮助网络避免过拟合。如果你的网络出现过拟合,尝试减小这个值。

训练模型

现在,让我们通过提供输入和输出给构建的模型来启动训练过程,然后使用优化器训练网络。不要忘记,在为当前状态做出预测时,我们需要使用前一个状态。因此,我们需要将输出状态传递回网络,以便在预测下一个输入时使用。

让我们为超参数提供初始值(你可以在之后根据训练该架构使用的数据集调整这些值):


batch_size = 100        # Sequences per batch
num_steps = 100         # Number of sequence steps per batch
lstm_size = 512         # Size of hidden layers in LSTMs
num_layers = 2          # Number of LSTM layers
learning_rate = 0.001   # Learning rate
keep_probability = 0.5  # Dropout keep probability
epochs = 5

# Save a checkpoint N iterations
save_every_n = 100

LSTM_model = CharLSTM(len(language_vocab), batch_size=batch_size, num_steps=num_steps,
                lstm_size=lstm_size, num_layers=num_layers, 
                learning_rate=learning_rate)

saver = tf.train.Saver(max_to_keep=100)
with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())

    # Use the line below to load a checkpoint and resume training
    #saver.restore(sess, 'checkpoints/______.ckpt')
    counter = 0
    for e in range(epochs):
        # Train network
        new_state = sess.run(LSTM_model.initial_state)
        loss = 0
        for x, y in generate_character_batches(encoded_vocab, batch_size, num_steps):
            counter += 1
            start = time.time()
            feed = {LSTM_model.inputs: x,
                    LSTM_model.targets: y,
                    LSTM_model.keep_prob: keep_probability,
                    LSTM_model.initial_state: new_state}
            batch_loss, new_state, _ = sess.run([LSTM_model.loss, 
                                                 LSTM_model.final_state, 
                                                 LSTM_model.optimizer], 
                                                 feed_dict=feed)

            end = time.time()
            print('Epoch number: {}/{}... '.format(e+1, epochs),
                  'Step: {}... '.format(counter),
                  'loss: {:.4f}... '.format(batch_loss),
                  '{:.3f} sec/batch'.format((end-start)))

            if (counter % save_every_n == 0):
                saver.save(sess, "checkpoints/i{}_l{}.ckpt".format(counter, lstm_size))

    saver.save(sess, "checkpoints/i{}_l{}.ckpt".format(counter, lstm_size))

在训练过程的最后,你应该得到一个接近以下的错误:

.
.
.
Epoch number: 5/5...  Step: 978...  loss: 1.7151...  0.050 sec/batch
Epoch number: 5/5...  Step: 979...  loss: 1.7428...  0.051 sec/batch
Epoch number: 5/5...  Step: 980...  loss: 1.7151...  0.050 sec/batch
Epoch number: 5/5...  Step: 981...  loss: 1.7236...  0.050 sec/batch
Epoch number: 5/5...  Step: 982...  loss: 1.7314...  0.051 sec/batch
Epoch number: 5/5...  Step: 983...  loss: 1.7369...  0.051 sec/batch
Epoch number: 5/5...  Step: 984...  loss: 1.7075...  0.065 sec/batch
Epoch number: 5/5...  Step: 985...  loss: 1.7304...  0.051 sec/batch
Epoch number: 5/5...  Step: 986...  loss: 1.7128...  0.049 sec/batch
Epoch number: 5/5...  Step: 987...  loss: 1.7107...  0.051 sec/batch
Epoch number: 5/5...  Step: 988...  loss: 1.7351...  0.051 sec/batch
Epoch number: 5/5...  Step: 989...  loss: 1.7260...  0.049 sec/batch
Epoch number: 5/5...  Step: 990...  loss: 1.7144...  0.051 sec/batch

保存检查点

现在,让我们加载检查点。关于保存和加载检查点的更多信息,你可以查看 TensorFlow 文档(www.tensorflow.org/programmers_guide/variables):

tf.train.get_checkpoint_state('checkpoints')
Output:
model_checkpoint_path: "checkpoints/i990_l512.ckpt"
all_model_checkpoint_paths: "checkpoints/i100_l512.ckpt"
all_model_checkpoint_paths: "checkpoints/i200_l512.ckpt"
all_model_checkpoint_paths: "checkpoints/i300_l512.ckpt"
all_model_checkpoint_paths: "checkpoints/i400_l512.ckpt"
all_model_checkpoint_paths: "checkpoints/i500_l512.ckpt"
all_model_checkpoint_paths: "checkpoints/i600_l512.ckpt"
all_model_checkpoint_paths: "checkpoints/i700_l512.ckpt"
all_model_checkpoint_paths: "checkpoints/i800_l512.ckpt"
all_model_checkpoint_paths: "checkpoints/i900_l512.ckpt"
all_model_checkpoint_paths: "checkpoints/i990_l512.ckpt"

生成文本

我们有一个基于输入数据集训练的模型。下一步是使用这个训练好的模型生成文本,并看看这个模型是如何学习输入数据的风格和结构的。为此,我们可以从一些初始字符开始,然后将新预测的字符作为下一个步骤的输入。我们将重复这个过程,直到生成特定长度的文本。

在以下代码中,我们还向函数添加了额外的语句,以便用一些初始文本为网络预热并从那里开始。

网络为我们提供了词汇中每个字符的预测或概率。为了减少噪声并只使用网络更加自信的字符,我们将只从输出中选择前N个最可能的字符:

def choose_top_n_characters(preds, vocab_size, top_n_chars=4):
    p = np.squeeze(preds)
    p[np.argsort(p)[:-top_n_chars]] = 0
    p = p / np.sum(p)
    c = np.random.choice(vocab_size, 1, p=p)[0]
    return c
def sample_from_LSTM_output(checkpoint, n_samples, lstm_size, vocab_size, prime="The "):
    samples = [char for char in prime]
    LSTM_model = CharLSTM(len(language_vocab), lstm_size=lstm_size, sampling=True)
    saver = tf.train.Saver()
    with tf.Session() as sess:
        saver.restore(sess, checkpoint)
        new_state = sess.run(LSTM_model.initial_state)
        for char in prime:
            x = np.zeros((1, 1))
            x[0,0] = vocab_to_integer[char]
            feed = {LSTM_model.inputs: x,
                    LSTM_model.keep_prob: 1.,
                    LSTM_model.initial_state: new_state}
            preds, new_state = sess.run([LSTM_model.prediction, LSTM_model.final_state], 
                                         feed_dict=feed)

        c = choose_top_n_characters(preds, len(language_vocab))
        samples.append(integer_to_vocab[c])

        for i in range(n_samples):
            x[0,0] = c
            feed = {LSTM_model.inputs: x,
                    LSTM_model.keep_prob: 1.,
                    LSTM_model.initial_state: new_state}
            preds, new_state = sess.run([LSTM_model.prediction, LSTM_model.final_state], 
                                         feed_dict=feed)

            c = choose_top_n_characters(preds, len(language_vocab))
            samples.append(integer_to_vocab[c])

    return ''.join(samples)

让我们开始使用保存的最新检查点进行采样过程:

tf.train.latest_checkpoint('checkpoints')
Output:
'checkpoints/i990_l512.ckpt'

现在,使用这个最新的检查点进行采样的时间到了:

checkpoint = tf.train.latest_checkpoint('checkpoints')
sampled_text = sample_from_LSTM_output(checkpoint, 1000, lstm_size, len(language_vocab), prime="Far")
print(sampled_text)
Output:
INFO:tensorflow:Restoring parameters from checkpoints/i990_l512.ckpt
Farcial the
confiring to the mone of the correm and thinds. She
she saw the
streads of herself hand only astended of the carres to her his some of the princess of which he came him of
all that his white the dreasing of
thisking the princess and with she was she had
bettee a still and he was happined, with the pood on the mush to the peaters and seet it.

"The possess a streatich, the may were notine at his mate a misted
and the
man of the mother at the same of the seem her
felt. He had not here.

"I conest only be alw you thinking that the partion
of their said."

"A much then you make all her
somether. Hower their centing
about
this, and I won't give it in
himself.
I had not come at any see it will that there she chile no one that him.

"The distiction with you all.... It was
a mone of the mind were starding to the simple to a mone. It to be to ser in the place," said Vronsky.
"And a plais in
his face, has alled in the consess on at they to gan in the sint
at as that
he would not be and t

你可以看到,我们能够生成一些有意义的词汇和一些无意义的词汇。为了获得更多结果,你可以让模型训练更多的 epochs,并尝试调整超参数。

总结

我们学习了 RNN,它们是如何工作的,以及为什么它们变得如此重要。我们在有趣的小说数据集上训练了一个 RNN 字符级语言模型,并看到了 RNN 的发展方向。你可以自信地期待在 RNN 领域会有大量的创新,我相信它们将成为智能系统中无处不在且至关重要的组成部分。

第十一章:表示学习 - 实现词嵌入

机器学习是一门主要基于统计学和线性代数的科学。矩阵运算在大多数机器学习或深度学习架构中非常常见,因为反向传播的原因。这也是深度学习或机器学习通常只接受实值输入的主要原因。这个事实与许多应用相矛盾,比如机器翻译、情感分析等,它们的输入是文本。因此,为了将深度学习应用于这些场景,我们需要将文本转化为深度学习能够接受的形式!

在本章中,我们将介绍表示学习领域,这是从文本中学习实值表示的一种方法,同时保持实际文本的语义。例如,love 的表示应该与 adore 的表示非常接近,因为它们在非常相似的上下文中使用。

所以,本章将涵盖以下主题:

  • 表示学习简介

  • Word2Vec

  • Skip-gram 架构的实际示例

  • Skip-gram Word2Vec 实现

表示学习简介

到目前为止,我们使用的所有机器学习算法或架构都要求输入为实值或实值矩阵,这是机器学习中的一个共同主题。例如,在卷积神经网络中,我们必须将图像的原始像素值作为模型输入。在这一部分,我们处理的是文本,因此我们需要以某种方式对文本进行编码,并生成可以输入到机器学习算法中的实值数据。为了将输入文本编码为实值数据,我们需要使用一种名为自然语言处理NLP)的中介技术。

我们提到过,在这种管道中,当我们将文本输入到机器学习模型中进行情感分析时,这将是一个问题,并且无法工作,因为我们无法在输入(字符串)上应用反向传播或其他操作(如点积)。因此,我们需要使用 NLP 的机制,构建一个文本的中间表示,该表示能够携带与文本相同的信息,并且可以被输入到机器学习模型中。

我们需要将输入文本中的每个单词或标记转换为实值向量。如果这些向量不携带原始输入的模式、信息、意义和语义,那么它们将毫无用处。例如,像真实文本中的两个单词 love 和 adore 非常相似,并且有相同的含义。我们需要将它们表示为的实值向量接近彼此,并处于相同的向量空间中。因此,这两个单词的向量表示与另一个不相似的单词一起,将呈现如下图所示的形态:

图 15.1:词的向量表示

有许多技术可以用于这个任务。这些技术统称为嵌入(embeddings),它将文本嵌入到另一个实值向量空间中。

正如我们稍后所见,这个向量空间实际上非常有趣,因为你会发现你可以通过其他与之相似的单词来推导一个单词的向量,甚至可以在这个空间里进行一些“地理”操作。

Word2Vec

Word2Vec 是自然语言处理(NLP)领域中广泛使用的嵌入技术之一。该模型通过观察输入单词出现的上下文信息,将输入文本转换为实值向量。因此,你会发现相似的单词通常会出现在非常相似的上下文中,从而模型会学到这些单词应该被放置在嵌入空间中的彼此相近位置。

从下面图示中的陈述来看,模型将学到 loveadore 共享非常相似的上下文,并且应该被放置在最终的向量空间中非常接近的位置。单词 like 的上下文可能也与 love 稍有相似,但它不会像 adore 那样接近 love:

图 15.2:情感句子示例

Word2Vec 模型还依赖于输入句子的语义特征;例如,单词 adore 和 love 主要在积极的语境中使用,通常会出现在名词短语或名词前面。同样,模型会学习到这两个词有共同之处,因此更可能将这两个词的向量表示放在相似的语境中。因此,句子的结构会告诉 Word2Vec 模型很多关于相似词的信息。

实际上,人们将一个大规模的文本语料库输入到 Word2Vec 模型中。该模型将学习为相似的单词生成相似的向量,并为输入文本中的每个唯一单词执行此操作。

所有这些单词的向量将被结合起来,最终的输出将是一个嵌入矩阵,其中每一行代表特定唯一单词的实值向量表示。

图 15.3:Word2Vec 模型流程示例

因此,模型的最终输出将是一个针对训练语料库中所有唯一单词的嵌入矩阵。通常,好的嵌入矩阵可以包含数百万个实值向量。

Word2Vec 建模使用窗口扫描句子,然后根据上下文信息预测窗口中间单词的向量;Word2Vec 模型一次扫描一个句子。与任何机器学习技术类似,我们需要为 Word2Vec 模型定义一个成本函数以及相应的优化标准,使得该模型能够为每个唯一的单词生成实值向量,并根据上下文信息将这些向量彼此关联。

构建 Word2Vec 模型

在本节中,我们将深入讨论如何构建一个 Word2Vec 模型。如前所述,我们的最终目标是拥有一个训练好的模型,能够为输入的文本数据生成实值向量表示,这也叫做词嵌入。

在模型训练过程中,我们将使用最大似然法 (en.wikipedia.org/wiki/Maximum_likelihood),这个方法可以用来最大化给定模型已经看到的前一个词的条件下,下一个词 w[t] 在输入句子中的概率,我们可以称之为 h

这个最大似然法将用软最大函数来表示:

在这里,score 函数计算一个值,用来表示目标词 w[t] 与上下文 h 的兼容性。该模型将在输入序列上进行训练,旨在最大化训练数据的似然性(为简化数学推导,使用对数似然)。

因此,ML 方法将尝试最大化上述方程,从而得到一个概率语言模型。但由于需要使用评分函数计算所有词的每个概率,这一计算非常耗费计算资源。

词汇表 V 中的单词 w',在该模型的当前上下文 h 中。这将在每个训练步骤中发生。

图 15.4:概率语言模型的一般架构

由于构建概率语言模型的计算开销较大,人们倾向于使用一些计算上更为高效的技术,比如 连续词袋模型 (CBOW) 和跳字模型。

这些模型经过训练,用逻辑回归构建一个二元分类模型,以区分真实目标词 w[t]h 噪声或虚构词 , 它们处在相同的上下文中。下面的图表简化了这个概念,采用了 CBOW 技术:

图 15.5:跳字模型的一般架构

下一张图展示了你可以用来构建 Word2Vec 模型的两种架构:

图 15.6:Word2Vec 模型的不同架构

更正式地说,这些技术的目标函数最大化如下:

其中:

  • 是基于模型在数据集 D 中看到词 w 在上下文 h 中的二元逻辑回归概率,这个概率是通过 θ 向量计算的。这个向量表示已学习的嵌入。

  • 是我们可以从一个噪声概率分布中生成的虚拟或噪声词汇,例如训练输入样本的 unigram。

总结来说,这些模型的目标是区分真实和虚拟的输入,因此需要给真实词汇分配较高的概率,而给虚拟或噪声词汇分配较低的概率。

当模型将高概率分配给真实词汇,低概率分配给噪声词汇时,该目标得到了最大化。

从技术上讲,将高概率分配给真实词汇的过程称为负采样papers.nips.cc/paper/5021-distributed-representations-of-words-and-phrases-and-their-compositionality.pdf),并且使用这种损失函数有很好的数学依据:它提出的更新近似了软最大(softmax)函数在极限情况下的更新。但从计算角度来看,它尤其具有吸引力,因为现在计算损失函数的复杂度仅与我们选择的噪声词数量(k)相关,而与词汇表中的所有词汇(V)无关。这使得训练变得更加高效。实际上,我们将使用非常类似的噪声对比估计NCE)(papers.nips.cc/paper/5165-learning-word-embeddings-efficiently-with-noise-contrastive-estimation.pdf)损失函数,TensorFlow 提供了一个便捷的辅助函数 tf.nn.nce_loss()

skip-gram 架构的一个实际示例

让我们通过一个实际例子,看看在这种情况下 skip-gram 模型是如何工作的:

the quick brown fox jumped over the lazy dog

首先,我们需要构建一个包含词语及其对应上下文的数据集。上下文的定义取决于我们,但必须合理。因此,我们会围绕目标词设置一个窗口,并从右边取一个词,再从左边取一个词。

通过采用这种上下文技术,我们最终会得到以下一组词语及其对应的上下文:

([the, brown], quick), ([quick, fox], brown), ([brown, jumped], fox), ...

生成的词语及其对应的上下文将以 (context, target) 的形式表示。skip-gram 模型的思想与 CBOW 模型正好相反。在 skip-gram 模型中,我们会尝试根据目标词来预测该词的上下文。例如,考虑第一个词对,skip-gram 模型会尝试从目标词 quick 预测出 thebrown 等词,依此类推。所以,我们可以将数据集重写如下:

(quick, the), (quick, brown), (brown, quick), (brown, fox), ...

现在,我们有了一组输入和输出的词对。

让我们尝试模仿在特定步骤 t 处的训练过程。那么,skip-gram 模型将以第一个训练样本为输入,其中输入词为 quick,目标输出词为 the。接下来,我们需要构造噪声输入,因此我们将从输入数据的单词集中随机选择。为了简化,噪声向量的大小仅为 1。例如,我们可以选择 sheep 作为噪声样本。

现在,我们可以继续计算真实对和噪声对之间的损失,公式如下:

在这种情况下,目标是更新 θ 参数,以改进之前的目标函数。通常,我们可以使用梯度来进行这个操作。因此,我们将尝试计算损失相对于目标函数参数 θ 的梯度,其表示为

在训练过程之后,我们可以基于实值向量表示的降维结果可视化一些结果。你会发现这个向量空间非常有趣,因为你可以用它做很多有趣的事情。例如,你可以在这个空间中学习类比,通过说“国王对王后就像男人对女人”。我们甚至可以通过从王后向量中减去国王向量并加上男人向量来推导出女人的向量;这个结果将非常接近实际学习到的女人向量。你也可以在这个空间中学习地理。

图 15.7:使用 t-分布随机邻域嵌入(t-SNE)降维技术将学习到的向量投影到二维空间

上面的例子为这些向量提供了很好的直觉,并且展示了它们如何对大多数自然语言处理应用(如机器翻译或 词性POS)标注)非常有用。

Skip-gram Word2Vec 实现

在理解了 skip-gram 模型如何工作的数学细节后,我们将实现 skip-gram,该模型将单词编码为具有某些属性的实值向量(因此得名 Word2Vec)。通过实现这一架构,你将了解学习另一种表示方式的过程是如何进行的。

文本是许多自然语言处理应用的主要输入,例如机器翻译、情感分析和语音合成系统。因此,为文本学习实值表示将帮助我们使用不同的深度学习技术来处理这些任务。

在本书的早期章节中,我们介绍了叫做独热编码(one-hot encoding)的方法,它会生成一个零向量,除了表示该词的索引外其他都为零。那么,你可能会想,为什么这里不使用它呢?这种方法非常低效,因为通常你会有一个很大的独特单词集,可能有 50,000 个单词,使用独热编码时,将会生成一个包含 49,999 个零的向量,并且只有一个位置是 1。

如果输入非常稀疏,会导致大量计算浪费,特别是在神经网络的隐藏层进行矩阵乘法时。

图 15.8:一热编码将导致大量计算浪费

如前所述,使用一热编码的结果将是一个非常稀疏的向量,特别是当你有大量不同的词汇需要编码时。

以下图所示,当我们将这个除了一个条目之外全为零的稀疏向量与一个权重矩阵相乘时,输出将仅为矩阵中与稀疏向量中唯一非零值对应的行:

图 15.9:将一个几乎全为零的一热向量与隐藏层权重矩阵相乘的效果

为了避免这种巨大的计算浪费,我们将使用嵌入技术,它仅仅是一个带有嵌入权重的全连接层。在这一层中,我们跳过了低效的乘法操作,而是通过所谓的权重矩阵来查找嵌入层的嵌入权重。

所以,为了避免计算时产生的浪费,我们将使用这个权重查找矩阵来查找嵌入权重。首先,需要构建这个查找表。为此,我们将所有输入词编码为整数,如下图所示,然后为了获取该词的对应值,我们将使用其整数表示作为该权重矩阵中的行号。找到特定词汇对应嵌入值的过程称为嵌入查找。如前所述,嵌入层只是一个全连接层,其中单元的数量代表嵌入维度。

图 15.10:标记化的查找表

你可以看到这个过程非常直观且简单;我们只需要按照这些步骤操作:

  1. 定义将被视为权重矩阵的查找表

  2. 将嵌入层定义为具有特定数量单元(嵌入维度)的全连接隐藏层

  3. 使用权重矩阵查找作为避免不必要的矩阵乘法的替代方案

  4. 最后,将查找表作为任何权重矩阵进行训练

如前所述,我们将在本节中构建一个跳字模型的 Word2Vec,这是学习词语表示的一种高效方式,同时保持词语的语义信息。

所以,让我们继续构建一个使用跳字架构的 Word2Vec 模型,它已被证明优于其他模型。

数据分析与预处理

在这一部分,我们将定义一些辅助函数,以帮助我们构建一个良好的 Word2Vec 模型。为了实现这一目标,我们将使用清理过的维基百科版本(mattmahoney.net/dc/textdata.html)。

那么,我们从导入实现所需的包开始:

#importing the required packages for this implementation
import numpy as np
import tensorflow as tf

#Packages for downloading the dataset
from urllib.request import urlretrieve
from os.path import isfile, isdir
from tqdm import tqdm
import zipfile

#packages for data preprocessing
import re
from collections import Counter
import random

接下来,我们将定义一个类,用于在数据集未下载时进行下载:

# In this implementation we will use a cleaned up version of Wikipedia from Matt Mahoney.
# So we will define a helper class that will helps to download the dataset
wiki_dataset_folder_path = 'wikipedia_data'
wiki_dataset_filename = 'text8.zip'
wiki_dataset_name = 'Text8 Dataset'

class DLProgress(tqdm):

    last_block = 0

    def hook(self, block_num=1, block_size=1, total_size=None):
        self.total = total_size
        self.update((block_num - self.last_block) * block_size)
        self.last_block = block_num

# Cheking if the file is not already downloaded
if not isfile(wiki_dataset_filename):
    with DLProgress(unit='B', unit_scale=True, miniters=1, desc=wiki_dataset_name) as pbar:
        urlretrieve(
            'http://mattmahoney.net/dc/text8.zip',
            wiki_dataset_filename,
            pbar.hook)

# Checking if the data is already extracted if not extract it
if not isdir(wiki_dataset_folder_path):
    with zipfile.ZipFile(wiki_dataset_filename) as zip_ref:
        zip_ref.extractall(wiki_dataset_folder_path)

with open('wikipedia_data/text8') as f:
    cleaned_wikipedia_text = f.read()

Output:

Text8 Dataset: 31.4MB [00:39, 794kB/s]                             

我们可以查看该数据集的前 100 个字符:

cleaned_wikipedia_text[0:100]

' anarchism originated as a term of abuse first used against early working class radicals including t'

接下来,我们将对文本进行预处理,因此我们将定义一个辅助函数,帮助我们将标点等特殊字符替换为已知的标记。此外,为了减少输入文本中的噪音,您可能还想去除那些在文本中出现频率较低的单词:

def preprocess_text(input_text):

    # Replace punctuation with some special tokens so we can use them in our model
    input_text = input_text.lower()
    input_text = input_text.replace('.', ' <PERIOD> ')
    input_text = input_text.replace(',', ' <COMMA> ')
    input_text = input_text.replace('"', ' <QUOTATION_MARK> ')
    input_text = input_text.replace(';', ' <SEMICOLON> ')
    input_text = input_text.replace('!', ' <EXCLAMATION_MARK> ')
    input_text = input_text.replace('?', ' <QUESTION_MARK> ')
    input_text = input_text.replace('(', ' <LEFT_PAREN> ')
    input_text = input_text.replace(')', ' <RIGHT_PAREN> ')
    input_text = input_text.replace('--', ' <HYPHENS> ')
    input_text = input_text.replace('?', ' <QUESTION_MARK> ')

    input_text = input_text.replace(':', ' <COLON> ')
    text_words = input_text.split()

    # neglecting all the words that have five occurrences of fewer
    text_word_counts = Counter(text_words)
    trimmed_words = [word for word in text_words if text_word_counts[word] > 5]

    return trimmed_words

现在,让我们在输入文本上调用这个函数,并查看输出:

preprocessed_words = preprocess_text(cleaned_wikipedia_text)
print(preprocessed_words[:30])
Output:
['anarchism', 'originated', 'as', 'a', 'term', 'of', 'abuse', 'first', 'used', 'against', 'early', 'working', 'class', 'radicals', 'including', 'the', 'diggers', 'of', 'the', 'english', 'revolution', 'and', 'the', 'sans', 'culottes', 'of', 'the', 'french', 'revolution', 'whilst']

让我们看看在处理过的文本中有多少个单词和不同的单词:

print("Total number of words in the text: {}".format(len(preprocessed_words)))
print("Total number of unique words in the text: {}".format(len(set(preprocessed_words))))

Output:

Total number of words in the text: 16680599
Total number of unique words in the text: 63641

在这里,我正在创建字典,将单词转换为整数并反向转换,即将整数转换为单词。这些整数按频率降序排列,因此出现频率最高的单词(the)被赋予整数0,接下来频率次高的得到1,以此类推。单词被转换为整数并存储在列表int_words中。

正如本节前面提到的,我们需要使用单词的整数索引来查找它们在权重矩阵中的值,因此我们将单词转换为整数,并将整数转换为单词。这将帮助我们查找单词,并且获取特定索引的实际单词。例如,输入文本中最常出现的单词将被索引为位置 0,接下来是第二常出现的单词,以此类推。

那么,让我们定义一个函数来创建这个查找表:

def create_lookuptables(input_words):
 """
 Creating lookup tables for vocan

 Function arguments:
 param words: Input list of words
 """
 input_word_counts = Counter(input_words)
 sorted_vocab = sorted(input_word_counts, key=input_word_counts.get, reverse=True)
 integer_to_vocab = {ii: word for ii, word in enumerate(sorted_vocab)}
 vocab_to_integer = {word: ii for ii, word in integer_to_vocab.items()}

 # returning A tuple of dicts
 return vocab_to_integer, integer_to_vocab

现在,让我们调用已定义的函数来创建查找表:

vocab_to_integer, integer_to_vocab = create_lookuptables(preprocessed_words)
integer_words = [vocab_to_integer[word] for word in preprocessed_words]

为了构建更精确的模型,我们可以去除那些对上下文变化不大的单词,如offorthe等。因此,实际上已经证明,在丢弃这些单词的情况下,我们可以构建更精确的模型。从上下文中去除与上下文无关的单词的过程被称为子抽样。为了定义一种通用的丢弃机制,Mikolov 提出了一个函数,用于计算某个单词的丢弃概率,该概率由以下公式给出:

其中:

  • t 是单词丢弃的阈值参数

  • f(w[i]) 是输入数据集中目标单词 w[i] 的频率

我们将实现一个辅助函数,用于计算数据集中每个单词的丢弃概率:

# removing context-irrelevant words threshold
word_threshold = 1e-5

word_counts = Counter(integer_words)
total_number_words = len(integer_words)

#Calculating the freqs for the words
frequencies = {word: count/total_number_words for word, count in word_counts.items()}

#Calculating the discard probability
prob_drop = {word: 1 - np.sqrt(word_threshold/frequencies[word]) for word in word_counts}
training_words = [word for word in integer_words if random.random() < (1 - prob_drop[word])]

现在,我们有了一个更精炼、更清晰的输入文本版本。

我们提到过,skip-gram 架构在生成目标单词的实值表示时,会考虑目标单词的上下文,因此它在目标单词周围定义了一个大小为 C 的窗口。

我们将不再平等地对待所有上下文单词,而是为那些距离目标单词较远的单词分配较小的权重。例如,如果我们选择窗口大小为 C = 4,那么我们将从 1 到 C 的范围内随机选择一个数字 L,然后从当前单词的历史和未来中采样 L 个单词。关于这一点的更多细节,请参见 Mikolov 等人的论文:arxiv.org/pdf/1301.3781.pdf

所以,让我们继续定义这个函数:

# Defining a function that returns the words around specific index in a specific window
def get_target(input_words, ind, context_window_size=5):

    #selecting random number to be used for genearting words form history and feature of the current word
    rnd_num = np.random.randint(1, context_window_size+1)
    start_ind = ind - rnd_num if (ind - rnd_num) > 0 else 0
    stop_ind = ind + rnd_num

    target_words = set(input_words[start_ind:ind] + input_words[ind+1:stop_ind+1])

    return list(target_words)    

此外,让我们定义一个生成器函数,从训练样本中生成一个随机批次,并为该批次中的每个单词获取上下文词:

#Defining a function for generating word batches as a tuple (inputs, targets)
def generate_random_batches(input_words, train_batch_size, context_window_size=5):

    num_batches = len(input_words)//train_batch_size

    # working on only only full batches
    input_words = input_words[:num_batches*train_batch_size]

    for ind in range(0, len(input_words), train_batch_size):
        input_vals, target = [], []
        input_batch = input_words[ind:ind+train_batch_size]

        #Getting the context for each word
        for ii in range(len(input_batch)):
            batch_input_vals = input_batch[ii]
            batch_target = get_target(input_batch, ii, context_window_size)

            target.extend(batch_target)
            input_vals.extend([batch_input_vals]*len(batch_target))
        yield input_vals, target

构建模型

接下来,我们将使用以下结构来构建计算图:

图 15.11:模型架构

正如之前所提到的,我们将使用一个嵌入层,尝试为这些词学习一个特殊的实数表示。因此,单词将作为 one-hot 向量输入。我们的想法是训练这个网络来构建权重矩阵。

那么,让我们从创建模型输入开始:

train_graph = tf.Graph()

#defining the inputs placeholders of the model
with train_graph.as_default():
    inputs_values = tf.placeholder(tf.int32, [None], name='inputs_values')
    labels_values = tf.placeholder(tf.int32, [None, None], name='labels_values')

我们要构建的权重或嵌入矩阵将具有以下形状:

num_words X num_hidden_neurons

此外,我们不需要自己实现查找函数,因为它在 Tensorflow 中已经可用:tf.nn.embedding_lookup()。因此,它将使用单词的整数编码,并找到它们在权重矩阵中的对应行。

权重矩阵将从均匀分布中随机初始化:

num_vocab = len(integer_to_vocab)

num_embedding =  300
with train_graph.as_default():
    embedding_layer = tf.Variable(tf.random_uniform((num_vocab, num_embedding), -1, 1))

    # Next, we are going to use tf.nn.embedding_lookup function to get the output of the hidden layer
    embed_tensors = tf.nn.embedding_lookup(embedding_layer, inputs_values) 

更新嵌入层的所有权重是非常低效的。我们将采用负采样技术,它只会更新正确单词的权重,并且只涉及一个小的错误单词子集。

此外,我们不必自己实现这个函数,因为在 TensorFlow 中已经有了 tf.nn.sampled_softmax_loss

# Number of negative labels to sample
num_sampled = 100

with train_graph.as_default():
    # create softmax weights and biases
    softmax_weights = tf.Variable(tf.truncated_normal((num_vocab, num_embedding))) 
    softmax_biases = tf.Variable(tf.zeros(num_vocab), name="softmax_bias") 

    # Calculating the model loss using negative sampling
    model_loss = tf.nn.sampled_softmax_loss(
        weights=softmax_weights,
        biases=softmax_biases,
        labels=labels_values,
        inputs=embed_tensors,
        num_sampled=num_sampled,
        num_classes=num_vocab)

    model_cost = tf.reduce_mean(model_loss)
    model_optimizer = tf.train.AdamOptimizer().minimize(model_cost)

为了验证我们训练的模型,我们将采样一些常见的词和一些不常见的词,并尝试基于跳字模型的学习表示打印它们的最近词集:

with train_graph.as_default():

    # set of random words for evaluating similarity on
    valid_num_words = 16 
    valid_window = 100

    # pick 8 samples from (0,100) and (1000,1100) each ranges. lower id implies more frequent 
    valid_samples = np.array(random.sample(range(valid_window), valid_num_words//2))
    valid_samples = np.append(valid_samples, 
                               random.sample(range(1000,1000+valid_window), valid_num_words//2))

    valid_dataset_samples = tf.constant(valid_samples, dtype=tf.int32)

    # Calculating the cosine distance
    norm = tf.sqrt(tf.reduce_sum(tf.square(embedding_layer), 1, keep_dims=True))
    normalized_embed = embedding_layer / norm
    valid_embedding = tf.nn.embedding_lookup(normalized_embed, valid_dataset_samples)
    cosine_similarity = tf.matmul(valid_embedding, tf.transpose(normalized_embed))

现在,我们已经拥有了模型的所有组成部分,准备开始训练过程。

训练

让我们继续启动训练过程:

num_epochs = 10
train_batch_size = 1000
contextual_window_size = 10

with train_graph.as_default():
    saver = tf.train.Saver()

with tf.Session(graph=train_graph) as sess:

    iteration_num = 1
    average_loss = 0

    #Initializing all the vairables
    sess.run(tf.global_variables_initializer())

    for e in range(1, num_epochs+1):

        #Generating random batch for training
        batches = generate_random_batches(training_words, train_batch_size, contextual_window_size)

        #Iterating through the batch samples
        for input_vals, target in batches:

            #Creating the feed dict
            feed_dict = {inputs_values: input_vals,
                    labels_values: np.array(target)[:, None]}

            train_loss, _ = sess.run([model_cost, model_optimizer], feed_dict=feed_dict)

            #commulating the loss
            average_loss += train_loss

            #Printing out the results after 100 iteration
            if iteration_num % 100 == 0: 
                print("Epoch Number {}/{}".format(e, num_epochs),
                      "Iteration Number: {}".format(iteration_num),
                      "Avg. Training loss: {:.4f}".format(average_loss/100))
                average_loss = 0

            if iteration_num % 1000 == 0:

                ## Using cosine similarity to get the nearest words to a word
                similarity = cosine_similarity.eval()
                for i in range(valid_num_words):
                    valid_word = integer_to_vocab[valid_samples[i]]

                    # number of nearest neighbors
                    top_k = 8 
                    nearest_words = (-similarity[i, :]).argsort()[1:top_k+1]
                    msg = 'The nearest to %s:' % valid_word
                    for k in range(top_k):
                        similar_word = integer_to_vocab[nearest_words[k]]
                        msg = '%s %s,' % (msg, similar_word)
                    print(msg)

            iteration_num += 1
    save_path = saver.save(sess, "checkpoints/cleaned_wikipedia_version.ckpt")
    embed_mat = sess.run(normalized_embed)

在运行前面的代码片段 10 个周期后,您将得到以下输出:

Epoch Number 10/10 Iteration Number: 43100 Avg. Training loss: 5.0380
Epoch Number 10/10 Iteration Number: 43200 Avg. Training loss: 4.9619
Epoch Number 10/10 Iteration Number: 43300 Avg. Training loss: 4.9463
Epoch Number 10/10 Iteration Number: 43400 Avg. Training loss: 4.9728
Epoch Number 10/10 Iteration Number: 43500 Avg. Training loss: 4.9872
Epoch Number 10/10 Iteration Number: 43600 Avg. Training loss: 5.0534
Epoch Number 10/10 Iteration Number: 43700 Avg. Training loss: 4.8261
Epoch Number 10/10 Iteration Number: 43800 Avg. Training loss: 4.8752
Epoch Number 10/10 Iteration Number: 43900 Avg. Training loss: 4.9818
Epoch Number 10/10 Iteration Number: 44000 Avg. Training loss: 4.9251
The nearest to nine: one, seven, zero, two, three, four, eight, five,
The nearest to such: is, as, or, some, have, be, that, physical,
The nearest to who: his, him, he, did, to, had, was, whom,
The nearest to two: zero, one, three, seven, four, five, six, nine,
The nearest to which: as, a, the, in, to, also, for, is,
The nearest to seven: eight, one, three, five, four, six, zero, two,
The nearest to american: actor, nine, singer, actress, musician, comedian, athlete, songwriter,
The nearest to many: as, other, some, have, also, these, are, or,
The nearest to powers: constitution, constitutional, formally, assembly, state, legislative, general, government,
The nearest to question: questions, existence, whether, answer, truth, reality, notion, does,
The nearest to channel: tv, television, broadcasts, broadcasting, radio, channels, broadcast, stations,
The nearest to recorded: band, rock, studio, songs, album, song, recording, pop,
The nearest to arts: art, school, alumni, schools, students, university, renowned, education,
The nearest to orthodox: churches, orthodoxy, church, catholic, catholics, oriental, christianity, christians,
The nearest to scale: scales, parts, important, note, between, its, see, measured,
The nearest to mean: is, exactly, defined, denote, hence, are, meaning, example,

Epoch Number 10/10 Iteration Number: 45100 Avg. Training loss: 4.8466
Epoch Number 10/10 Iteration Number: 45200 Avg. Training loss: 4.8836
Epoch Number 10/10 Iteration Number: 45300 Avg. Training loss: 4.9016
Epoch Number 10/10 Iteration Number: 45400 Avg. Training loss: 5.0218
Epoch Number 10/10 Iteration Number: 45500 Avg. Training loss: 5.1409
Epoch Number 10/10 Iteration Number: 45600 Avg. Training loss: 4.7864
Epoch Number 10/10 Iteration Number: 45700 Avg. Training loss: 4.9312
Epoch Number 10/10 Iteration Number: 45800 Avg. Training loss: 4.9097
Epoch Number 10/10 Iteration Number: 45900 Avg. Training loss: 4.6924
Epoch Number 10/10 Iteration Number: 46000 Avg. Training loss: 4.8999
The nearest to nine: one, eight, seven, six, four, five, american, two,
The nearest to such: can, example, examples, some, be, which, this, or,
The nearest to who: him, his, himself, he, was, whom, men, said,
The nearest to two: zero, five, three, four, six, one, seven, nine
The nearest to which: to, is, a, the, that, it, and, with,
The nearest to seven: one, six, eight, five, nine, four, three, two,
The nearest to american: musician, actor, actress, nine, singer, politician, d, one,
The nearest to many: often, as, most, modern, such, and, widely, traditional,
The nearest to powers: constitutional, formally, power, rule, exercised, parliamentary, constitution, control,
The nearest to question: questions, what, answer, existence, prove, merely, true, statements,
The nearest to channel: network, channels, broadcasts, stations, cable, broadcast, broadcasting, radio,
The nearest to recorded: songs, band, song, rock, album, bands, music, studio,
The nearest to arts: art, school, martial, schools, students, styles, education, student,
The nearest to orthodox: orthodoxy, churches, church, christianity, christians, catholics, christian, oriental,
The nearest to scale: scales, can, amounts, depends, tend, are, structural, for,
The nearest to mean: we, defined, is, exactly, equivalent, denote, number, above,
Epoch Number 10/10 Iteration Number: 46100 Avg. Training loss: 4.8583
Epoch Number 10/10 Iteration Number: 46200 Avg. Training loss: 4.8887

如您所见,网络在某种程度上学习到了输入单词的一些语义有用的表示。为了帮助我们更清楚地看到嵌入矩阵,我们将使用降维技术,如 t-SNE,将实数值向量降至二维,然后我们将对它们进行可视化,并用相应的单词标记每个点:

num_visualize_words = 500
tsne_obj = TSNE()
embedding_tsne = tsne_obj.fit_transform(embedding_matrix[:num_visualize_words, :])

fig, ax = plt.subplots(figsize=(14, 14))
for ind in range(num_visualize_words):
    plt.scatter(*embedding_tsne[ind, :], color='steelblue')
    plt.annotate(integer_to_vocab[ind], (embedding_tsne[ind, 0], embedding_tsne[ind, 1]), alpha=0.7)

Output:

图 15.12:词向量的可视化

总结

在本章中,我们介绍了表示学习的概念以及它为什么对深度学习或机器学习(尤其是对非实数形式的输入)非常有用。此外,我们还讲解了将单词转换为实数向量的一种常用技术——Word2Vec,它具有非常有趣的特性。最后,我们使用 skip-gram 架构实现了 Word2Vec 模型。

接下来,你将看到这些学习到的表示在情感分析示例中的实际应用,在该示例中,我们需要将输入文本转换为实数向量。

第十二章:神经情感分析

在本章中,我们将讨论自然语言处理领域中的一个热门应用,即情感分析。如今,大多数人通过社交媒体平台表达他们的意见,利用这一海量文本来跟踪客户对某事的满意度,对于公司甚至政府来说都是非常重要的。

在本章中,我们将使用递归类型的神经网络来构建情感分析解决方案。本章将涉及以下主题:

  • 一般的情感分析架构

  • 情感分析——模型实现

一般的情感分析架构

在本节中,我们将重点讨论可以用于情感分析的一般深度学习架构。下图展示了构建情感分析模型所需的处理步骤。

所以,首先,我们将处理自然语言:

图 1:情感分析解决方案或基于序列的自然语言解决方案的一般管道

我们将使用电影评论来构建这个情感分析应用程序。这个应用程序的目标是根据输入的原始文本生成正面或负面评论。例如,如果原始文本是类似于这部电影很好的内容,那么我们需要模型为其生成一个正面情感。

一个情感分析应用程序将带我们经历许多必要的处理步骤,这些步骤是神经网络中处理自然语言所必需的,例如词嵌入。

所以在这种情况下,我们有一个原始文本,例如这不是一部好电影! 我们希望最终得到的是它是负面还是正面的情感。

在这种类型的应用程序中,有几个难点:

  • 其中之一是序列可能具有不同的长度。这是一个非常短的序列,但我们会看到一些文本,它们的长度超过了 500 个单词。

  • 另一个问题是,如果我们仅仅看单独的单词(例如,good),它表示的是正面情感。然而,它前面有一个not,所以现在它变成了负面情感。这可能变得更加复杂,我们稍后会看到一个例子。

正如我们在上一章所学的,神经网络无法直接处理原始文本,因此我们需要先将其转换成所谓的词元。这些基本上就是整数值,所以我们遍历整个数据集,统计每个词出现的次数。然后,我们创建一个词汇表,每个词在该词汇表中都会有一个索引。因此,单词this有一个整数 ID 或词元11,单词is的词元是6not的词元是21,依此类推。现在,我们已经将原始文本转换成了一个由整数构成的列表,称为词元。

神经网络仍然无法处理这些数据,因为如果我们有一个包含 10,000 个单词的词汇表,那么这些标记的值可以在 0 到 9,999 之间变化,而它们之间可能没有任何关联。因此,单词编号 998 和单词编号 999 的语义可能完全不同。

因此,我们将使用上一章学习的表示学习或嵌入的概念。这个嵌入层将整数标记转换为实值向量,例如,标记11变为向量[0.67,0.36,...,0.39],如图 1所示。对于下一个标记 6 也是如此。

我们在上一章学习内容的简要回顾:前面图中的嵌入层学习的是标记(tokens)与其对应的实值向量之间的映射关系。同时,嵌入层还学习单词的语义含义,使得具有相似含义的单词在这个嵌入空间中会彼此接近。

从原始输入文本中,我们得到一个二维矩阵或张量,它现在可以作为输入传递给递归神经网络RNN)。该网络可以处理任意长度的序列,其输出随后会传递到一个全连接层或密集层,并使用 sigmoid 激活函数。因此,输出值介于 0 和 1 之间,其中 0 表示负面情感。那么,如果 sigmoid 函数的值既不是 0 也不是 1 该怎么办?此时我们需要引入一个中间的切割值或阈值,当该值低于 0.5 时,认为对应的输入是负面情感,而当该值高于此阈值时,则认为是正面情感。

RNN——情感分析的背景

现在,让我们回顾一下 RNN 的基本概念,并在情感分析应用的背景下讨论它们。正如我们在 RNN 章节中提到的,RNN 的基本构建块是递归单元,如图所示:

图 2:RNN 单元的抽象概念

这张图是对递归单元内部运作的抽象。在这里,我们有输入数据,例如,单词good。当然,它需要被转换为嵌入向量。然而,我们暂时忽略这一部分。此外,这个单元还有一种内存状态,根据StateInput的内容,我们会更新这个状态并将新数据写入状态。例如,假设我们之前在输入中看到过单词not,我们将其写入状态,这样当我们在后续输入中看到单词good时,我们可以从状态中得知之前见过单词not。现在,我们看到单词good,因此,我们必须在状态中写入已见过not good这两个单词,这可能表明整个输入文本的情感是负面的。

从旧状态和输入到新状态内容的映射是通过所谓的门控来完成的,这些门控在不同版本的递归单元中有不同的实现方式。它基本上是一个矩阵运算加上激活函数,但正如我们稍后会看到的,反向传播梯度时会遇到问题。因此,RNN 必须以一种特殊方式设计,以避免梯度被过度扭曲。

在递归单元中,我们有一个类似的门控来生成输出,再次强调,递归单元的输出依赖于当前状态的内容和我们正在看到的输入。所以我们可以尝试展开递归单元中的处理过程:

图 3:递归神经网络的展开版本

现在,我们看到的是一个递归单元,但流程图展示了不同时间步发生的情况。所以:

  • 在时间步 1,我们将单词this输入到递归单元中,它的内部记忆状态首先初始化为零。当我们开始处理新的数据序列时,TensorFlow 会执行此操作。所以我们看到单词this,而递归单元的状态是 0。因此,我们使用内部门控来更新记忆状态,this随后在时间步 2 被使用,在此时间步我们输入单词is,此时记忆状态已有内容。this这个词的意义并不大,因此状态可能仍然接近 0。

  • 而且is也没有太多的意义,所以状态可能仍然接近 0。

  • 在下一个时间步,我们看到单词not,这具有我们最终想要预测的意义,即整个输入文本的情感。这个信息需要存储在记忆中,以便递归单元中的门控能够看到该状态已经可能包含接近零的值。但现在它需要存储我们刚刚看到的单词not,因此它在该状态中保存了一些非零值。

  • 然后,我们进入下一个时间步,看到单词a,这个也没有太多信息,所以可能会被忽略。它只是将状态复制过去。

  • 现在,我们看到单词very,这表示任何情感可能是强烈的情感,因此递归单元现在知道我们已经看到了notvery。它以某种方式将其存储在内存状态中。

  • 在下一个时间步,我们看到单词good,所以现在网络知道not very good,并且它想,哦,这可能是负面情感!因此,它将这个值存储在内部状态中。

  • 然后,在最后一个时间步,我们看到movie,这实际上与情感无关,因此可能会被忽略。

  • 接下来,我们使用递归单元中的另一个门控输出记忆状态的内容,然后通过 sigmoid 函数处理(这里没有展示)。我们得到一个介于 0 和 1 之间的输出值。

这个想法是,我们希望在互联网上的电影评论数据集(Internet Movie Database)上训练这个网络,其中,对于每个输入文本,我们都会提供正确的情感值——正面或负面。接着,我们希望 TensorFlow 能找出循环单元内部的门控应该是什么,以便它能够准确地将这个输入文本映射到正确的情感:

图 4:本章实现所用的架构

我们在这个实现中将使用的 RNN 架构是一个具有三层的 RNN 类型架构。在第一层,我们刚才解释的过程会发生,只是现在我们需要在每个时间步输出来自循环单元的值。然后,我们收集新的数据序列,即第一循环层的输出。接下来,我们可以将它输入到第二个循环层,因为循环单元需要输入数据的序列(而我们从第一层得到的输出和我们想要输入到第二个循环层的是一些浮点值,其含义我们并不完全理解)。这在 RNN 内部有其意义,但我们作为人类并不能理解它。然后,我们在第二个循环层中进行类似的处理。

所以,首先,我们将这个循环单元的内部记忆状态初始化为 0;然后,我们取第一个循环层的第一个输出并输入。我们用循环单元内部的门控处理它,更新状态,取第一个层循环单元的输出作为第二个词is的输入,并使用该输入和内部记忆状态。我们继续这样做,直到处理完整个序列,然后我们将第二个循环层的所有输出收集起来。我们将它们作为输入传递给第三个循环层,在那里我们进行类似的处理。但在这里,我们只需要最后一个时间步的输出,它是迄今为止输入的所有内容的摘要。然后,我们将它输出到一个全连接层,这里没有展示。最后,我们使用 sigmoid 激活函数,因此我们得到一个介于 0 和 1 之间的值,分别表示负面和正面情感。

梯度爆炸和梯度消失——回顾

正如我们在上一章提到的,存在一种现象叫做梯度爆炸梯度消失,它在 RNN 中非常重要。让我们回过头来看一下图 1;该流程图解释了这个现象是什么。

假设我们有一个包含 500 个词的文本数据集,这将用于实现我们的情感分析分类器。在每个时间步,我们以递归方式应用循环单元内的门控;因此,如果有 500 个词,我们将在 500 次时间步中应用这些门控,以更新循环单元的内部记忆状态。

如我们所知,神经网络的训练方式是通过所谓的梯度反向传播,所以我们有一个损失函数,它获取神经网络的输出,然后是我们希望得到的该输入文本的真实输出。接下来,我们希望最小化这个损失值,以使神经网络的实际输出与此特定输入文本的期望输出相符。因此,我们需要计算这个损失函数关于这些递归单元内部权重的梯度,而这些权重用于更新内部状态并最终输出结果的门控。

现在,这个门可能会应用大约 500 次,如果其中有乘法运算,我们实际上得到的是一个指数函数。所以,如果你将一个值与其本身相乘 500 次,并且这个值略小于 1,那么它将很快消失或丢失。同样地,如果一个值略大于 1,并与其本身相乘 500 次,它将爆炸。

唯一能在 500 次乘法中生存的值是 0 和 1。它们将保持不变,所以递归单元实际上比你看到的要复杂得多。这是一个抽象的概念——我们希望以某种方式将内部记忆状态和输入映射,用于更新内部记忆状态并输出某个值——但实际上,我们需要非常小心地将梯度反向传播通过这些门,以防止在多次时间步中发生这种指数级的乘法。我们也鼓励你查看一些关于递归单元数学定义的教程。

情感分析 – 模型实现

我们已经了解了如何实现堆叠版本的 LSTM 变种 RNN。为了让事情更有趣,我们将使用一个更高级的 API,叫做 Keras

Keras

"Keras 是一个高级神经网络 API,使用 Python 编写,可以在 TensorFlow、CNTK 或 Theano 上运行。它的开发重点是快速实验的实现。从想法到结果的转换延迟最小化是做出良好研究的关键。" – Keras 网站

所以,Keras 只是 TensorFlow 和其他深度学习框架的一个封装。它非常适合原型设计和快速构建,但另一方面,它让你对代码的控制较少。我们将尝试在 Keras 中实现这个情感分析模型,这样你可以在 TensorFlow 和 Keras 中获得一个动手实现。你可以使用 Keras 进行快速原型设计,而将 TensorFlow 用于生产环境的系统。

更有趣的消息是,你不需要切换到一个完全不同的环境。你现在可以在 TensorFlow 中将 Keras 作为模块访问,并像以下代码一样导入包:

from tensorflow.python.keras.models 
import Sequential
from tensorflow.python.keras.layers 
import Dense, GRU, Embedding
from tensorflow.python.keras.optimizers 
import Adam
from tensorflow.python.keras.preprocessing.text 
import Tokenizer
from tensorflow.python.keras.preprocessing.sequence 
import pad_sequences

所以,让我们继续使用现在可以称之为更抽象的 TensorFlow 模块,它将帮助我们非常快速地原型化深度学习解决方案。因为我们只需几行代码就能写出完整的深度学习解决方案。

数据分析和预处理

现在,让我们进入实际的实现,我们需要加载数据。Keras 实际上有一个功能,可以用来从 IMDb 加载这个情感数据集,但问题是它已经将所有单词映射到整数令牌了。这是处理自然语言与神经网络之间非常关键的一部分,我真的想向你展示如何做到这一点。

此外,如果你想将这段代码用于其他语言的情感分析,你需要自己做这个转换,所以我们快速实现了一些下载这个数据集的函数。

让我们从导入一些必需的包开始:

%matplotlib inline
import matplotlib.pyplot as plt
import tensorflow as tf
import numpy as np
from scipy.spatial.distance import cdist
from tensorflow.python.keras.models import Sequential
from tensorflow.python.keras.layers import Dense, GRU, Embedding
from tensorflow.python.keras.optimizers import Adam
from tensorflow.python.keras.preprocessing.text import Tokenizer
from tensorflow.python.keras.preprocessing.sequence import pad_sequences

然后我们加载数据集:

import imdb
imdb.maybe_download_and_extract()

Output:
- Download progress: 100.0%
Download finished. Extracting files.
Done.
input_text_train, target_train = imdb.load_data(train=True)
input_text_test, target_test = imdb.load_data(train=False)
print("Size of the trainig set: ", len(input_text_train))
print("Size of the testing set:  ", len(input_text_test))

Output:
Size of the trainig set: 25000
Size of the testing set: 25000

如你所见,训练集中有 25,000 个文本,测试集中也有。

我们来看看训练集中的一个例子,它是如何呈现的:

#combine dataset
text_data = input_text_train + input_text_test
input_text_train[1]

Output:
'This is a really heart-warming family movie. It has absolutely brilliant animal training and "acting" (if you can call it like that) as well (just think about the dog in "How the Grinch stole Christmas"... it was plain bad training). The Paulie story is extremely well done, well reproduced and in general the characters are really elaborated too. Not more to say except that this is a GREAT MOVIE!<br /><br />My ratings: story 8.5/10, acting 7.5/10, animals+fx 8.5/10, cinematography 8/10.<br /><br />My overall rating: 8/10 - BIG FAMILY MOVIE AND VERY WORTH WATCHING!'

target_train[1]

Output:
1.0

这是一个相当简短的文本,情感值为1.0,这意味着它是一个积极的情感,因此这是一篇关于某部电影的正面评价。

现在,我们进入了分词器,这也是处理这些原始数据的第一步,因为神经网络不能直接处理文本数据。Keras 实现了一个叫做分词器的工具,用来构建词汇表并将单词映射到整数。

此外,我们可以说我们希望最大使用 10,000 个单词,因此它将只使用数据集中最流行的 10,000 个单词:

num_top_words = 10000
tokenizer_obj = Tokenizer(num_words=num_top_words)

现在,我们将从数据集中获取所有文本,并在文本上调用这个函数fit

tokenizer_obj.fit_on_texts(text_data)

分词器大约需要 10 秒钟,然后它将构建出词汇表。它看起来是这样的:

tokenizer_obj.word_index

Output:
{'britains': 33206,
 'labcoats': 121364,
 'steeled': 102939,
 'geddon': 67551,
 "rossilini's": 91757,
 'recreational': 27654,
 'suffices': 43205,
 'hallelujah': 30337,
 'mallika': 30343,
 'kilogram': 122493,
 'elphic': 104809,
 'feebly': 32818,
 'unskillful': 91728,
 "'mistress'": 122218,
 "yesterday's": 25908,
 'busco': 85664,
 'goobacks': 85670,
 'mcfeast': 71175,
 'tamsin': 77763,
 "petron's": 72628,
 "'lion": 87485,
 'sams': 58341,
 'unbidden': 60042,
 "principal's": 44902,
 'minutiae': 31453,
 'smelled': 35009,
 'history\x97but': 75538,
 'vehemently': 28626,
 'leering': 14905,
 'kýnay': 107654,
 'intendend': 101260,
 'chomping': 21885,
 'nietsze': 76308,
 'browned': 83646,
 'grosse': 17645,
 "''gaslight''": 74713,
 'forseeing': 103637,
 'asteroids': 30997,
 'peevish': 49633,
 "attic'": 120936,
 'genres': 4026,
 'breckinridge': 17499,
 'wrist': 13996,
 "sopranos'": 50345,
 'embarasing': 92679,
 "wednesday's": 118413,
 'cervi': 39092,
 'felicity': 21570,
 "''horror''": 56254,
 'alarms': 17764,
 "'ol": 29410,
 'leper': 27793,
 'once\x85': 100641,
 'iverson': 66834,
 'triply': 117589,
 'industries': 19176,
 'brite': 16733,
 'amateur': 2459,
 "libby's": 46942,
 'eeeeevil': 120413,
 'jbc33': 51111,
 'wyoming': 12030,
 'waned': 30059,
 'uchida': 63203,
 'uttter': 93299,
 'irector': 123847,
 'outriders': 95156,
 'perd': 118465,
.
.
.}

所以,现在每个单词都与一个整数相关联;因此,单词the的编号是1

tokenizer_obj.word_index['the']

Output:
1

这里,and的编号是2

tokenizer_obj.word_index['and']

Output:
2

单词a的编号是3

tokenizer_obj.word_index['a']

Output:
3

以此类推。我们看到movie的编号是17

tokenizer_obj.word_index['movie']

Output:
17

并且film的编号是19

tokenizer_obj.word_index['film']

Output:
19

所有这些的意思是,the是数据集中使用最多的单词,and是第二多的单词。因此,每当我们想要将单词映射到整数令牌时,我们将得到这些编号。

让我们以单词编号743为例,这就是单词romantic

tokenizer_obj.word_index['romantic']

Output:
743

所以,每当我们在输入文本中看到单词romantic时,我们将其映射到令牌整数743。我们再次使用分词器将训练集中的所有单词转换为整数令牌:

input_text_train[1]
Output:
'This is a really heart-warming family movie. It has absolutely brilliant animal training and "acting" (if you can call it like that) as well (just think about the dog in "How the Grinch stole Christmas"... it was plain bad training). The Paulie story is extremely well done, well reproduced and in general the characters are really elaborated too. Not more to say except that this is a GREAT MOVIE!<br /><br />My ratings: story 8.5/10, acting 7.5/10, animals+fx 8.5/10, cinematography 8/10.<br /><br />My overall rating: 8/10 - BIG FAMILY MOVIE AND VERY WORTH WATCHING!

当我们将这些文本转换为整数令牌时,它就变成了一个整数数组:

np.array(input_train_tokens[1])

Output:
array([ 11, 6, 3, 62, 488, 4679, 236, 17, 9, 45, 419,
        513, 1717, 2425, 2, 113, 43, 22, 67, 654, 9, 37,
         12, 14, 69, 39, 101, 42, 1, 826, 8, 85, 1,
       6418, 3492, 1156, 9, 13, 1042, 74, 2425, 1, 6419, 64,
          6, 568, 69, 221, 69, 2, 8, 825, 1, 102, 23,
         62, 96, 21, 51, 5, 131, 556, 12, 11, 6, 3,
         78, 17, 7, 7, 56, 2818, 64, 723, 447, 156, 113,
        702, 447, 156, 1598, 3611, 723, 447, 156, 633, 723, 156,
          7, 7, 56, 437, 670, 723, 156, 191, 236, 17, 2,
         52, 278, 147])

所以,单词this变成了编号 11,单词is变成了编号 59,以此类推。

我们还需要转换剩余的文本:

input_test_tokens = tokenizer_obj.texts_to_sequences(input_text_test)

现在,还有另一个问题,因为标记的序列长度根据原始文本的长度而有所不同,尽管循环神经网络(RNN)单元可以处理任意长度的序列。但是 TensorFlow 的工作方式是,批量中的所有数据必须具有相同的长度。

所以,我们可以确保数据集中的所有序列都具有相同的长度,或者编写一个自定义数据生成器,确保单个批次中的序列具有相同的长度。现在,确保数据集中的所有序列具有相同的长度要简单得多,但问题是有一些极端值。我们有一些句子,我认为,它们超过了 2,200 个单词。如果所有的句子都超过 2,200 个单词,将极大地影响我们的内存。所以我们做的折衷是:首先,我们需要统计每个输入序列中的单词数,或者标记数。我们看到,序列中单词的平均数大约是 221:

total_num_tokens = [len(tokens) for tokens in input_train_tokens + input_test_tokens]
total_num_tokens = np.array(total_num_tokens)

#Get the average number of tokens
np.mean(total_num_tokens)

Output:
221.27716

我们看到,最大单词数超过了 2200 个:

np.max(total_num_tokens)

Output:
2208

现在,平均值和最大值之间有很大的差异,如果我们仅仅将数据集中的所有句子都填充到2208个标记,这将浪费大量的内存。尤其是如果你有一个包含百万级文本序列的数据集,这个问题就更加严重。

所以我们要做的折衷是,填充所有序列并截断那些太长的序列,使它们有544个单词。我们计算这一点的方式是——我们取了数据集中所有序列的平均单词数,并加上了两个标准差:

max_num_tokens = np.mean(total_num_tokens) + 2 * np.std(total_num_tokens)
max_num_tokens = int(max_num_tokens)
max_num_tokens

Output:
544

这样做的结果是什么?我们覆盖了数据集中文本的约 95%,所以只有大约 5%的文本超过了544个单词:

np.sum(total_num_tokens < max_num_tokens) / len(total_num_tokens)

Output:
0.94532

现在,我们调用 Keras 中的这些函数。它们会填充那些太短的序列(即只会添加零),或者截断那些太长的序列(如果文本过长,基本上会删除一些单词)。

现在,这里有一个重要的点:我们可以选择在预处理模式(pre mode)或后处理模式(post mode)下进行填充和截断。假设我们有一个整数标记的序列,并且我们希望填充它,因为它太短了。我们可以:

  • 要么在开头填充所有这些零,这样我们就可以把实际的整数标记放在最后。

  • 或者以相反的方式进行处理,将所有数据放在开头,然后将所有的零放在末尾。但是,如果我们回头看看前面的 RNN 流程图,记住它是一步一步地处理序列的,所以如果我们开始处理零,它可能没有任何意义,内部状态可能会保持为零。因此,每当它最终看到特定单词的整数标记时,它就会知道,好,现在开始处理数据了。

然而,如果所有的零都在末尾,那么我们将开始处理所有数据;接着,我们会在递归单元内部有一些内部状态。现在,我们看到的是一堆零,这实际上可能会破坏我们刚刚计算出来的内部状态。这就是为什么将零填充到开始处可能是个好主意。

但另一个问题是当我们截断文本时,如果文本非常长,我们会将其截断到544个单词,或者其他任何数字。现在,假设我们抓住了这句话,它在中间某个地方,并且它说的是这部非常好的电影这不是。你当然知道,我们只有在处理非常长的序列时才会这样做,但很可能我们会丢失一些关键信息,无法正确分类这段文本。所以,当我们截断输入文本时,这是我们所做的妥协。更好的方法是创建一个批次,并在该批次中填充文本。所以,当我们看到非常非常长的序列时,我们将填充其他序列,使它们具有相同的长度。但我们不需要将所有这些数据都存储在内存中,因为其中大部分是浪费的。

让我们回到并转换整个数据集,使其被截断并填充;这样,它就变成了一个庞大的数据矩阵:

seq_pad = 'pre'

input_train_pad = pad_sequences(input_train_tokens, maxlen=max_num_tokens,
 padding=seq_pad, truncating=seq_pad)

input_test_pad = pad_sequences(input_test_tokens, maxlen=max_num_tokens,
 padding=seq_pad, truncating=seq_pad)

我们检查这个矩阵的形状:

input_train_pad.shape

Output:
(25000, 544)

input_test_pad.shape

Output:
(25000, 544)

那么,让我们来看一下在填充前后的特定样本标记:

np.array(input_train_tokens[1])

Output:
array([ 11, 6, 3, 62, 488, 4679, 236, 17, 9, 45, 419,
        513, 1717, 2425, 2, 113, 43, 22, 67, 654, 9, 37,
         12, 14, 69, 39, 101, 42, 1, 826, 8, 85, 1,
       6418, 3492, 1156, 9, 13, 1042, 74, 2425, 1, 6419, 64,
          6, 568, 69, 221, 69, 2, 8, 825, 1, 102, 23,
         62, 96, 21, 51, 5, 131, 556, 12, 11, 6, 3,
         78, 17, 7, 7, 56, 2818, 64, 723, 447, 156, 113,
        702, 447, 156, 1598, 3611, 723, 447, 156, 633, 723, 156,
          7, 7, 56, 437, 670, 723, 156, 191, 236, 17, 2,
         52, 278, 147])

填充后,这个样本将如下所示:

input_train_pad[1]

Output:
array([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 11, 6, 3, 62, 488, 4679, 236, 17, 9,
         45, 419, 513, 1717, 2425, 2, 113, 43, 22, 67, 654,
          9, 37, 12, 14, 69, 39, 101, 42, 1, 826, 8,
         85, 1, 6418, 3492, 1156, 9, 13, 1042, 74, 2425, 1,
       6419, 64, 6, 568, 69, 221, 69, 2, 8, 825, 1,
        102, 23, 62, 96, 21, 51, 5, 131, 556, 12, 11,
          6, 3, 78, 17, 7, 7, 56, 2818, 64, 723, 447,
        156, 113, 702, 447, 156, 1598, 3611, 723, 447, 156, 633,
        723, 156, 7, 7, 56, 437, 670, 723, 156, 191, 236,
         17, 2, 52, 278, 147], dtype=int32)

此外,我们需要一个功能来进行反向映射,使其能够将整数标记映射回文本单词;我们在这里只需要这个。它是一个非常简单的辅助函数,所以让我们继续实现它:

index = tokenizer_obj.word_index
index_inverse_map = dict(zip(index.values(), index.keys()))
def convert_tokens_to_string(input_tokens):

 # Convert the tokens back to words
 input_words = [index_inverse_map[token] for token in input_tokens if token != 0]

 # join them all words.
 combined_text = " ".join(input_words)

return combined_text

现在,举个例子,数据集中的原始文本是这样的:

input_text_train[1]
Output:

input_text_train[1]

'This is a really heart-warming family movie. It has absolutely brilliant animal training and "acting" (if you can call it like that) as well (just think about the dog in "How the Grinch stole Christmas"... it was plain bad training). The Paulie story is extremely well done, well reproduced and in general the characters are really elaborated too. Not more to say except that this is a GREAT MOVIE!<br /><br />My ratings: story 8.5/10, acting 7.5/10, animals+fx 8.5/10, cinematography 8/10.<br /><br />My overall rating: 8/10 - BIG FAMILY MOVIE AND VERY WORTH WATCHING!'

如果我们使用一个辅助函数将标记转换回文本单词,我们会得到以下文本:

convert_tokens_to_string(input_train_tokens[1])

'this is a really heart warming family movie it has absolutely brilliant animal training and acting if you can call it like that as well just think about the dog in how the grinch stole christmas it was plain bad training the paulie story is extremely well done well and in general the characters are really too not more to say except that this is a great movie br br my ratings story 8 5 10 acting 7 5 10 animals fx 8 5 10 cinematography 8 10 br br my overall rating 8 10 big family movie and very worth watching'

基本上是一样的,只是标点符号和其他符号不同。

构建模型

现在,我们需要创建 RNN,我们将在 Keras 中实现,因为它非常简单。我们使用所谓的sequential模型来实现这一点。

该架构的第一层将是所谓的嵌入层。如果我们回顾一下图 1中的流程图,我们刚才做的是将原始输入文本转换为整数标记。但我们仍然无法将其输入到 RNN 中,所以我们必须将其转换为嵌入向量,这些值介于-1 和 1 之间。它们可能在某种程度上超出这一范围,但通常情况下它们会在-1 和 1 之间,这些是我们可以在神经网络中进行处理的数据。

这有点像魔法,因为这个嵌入层与 RNN 同时训练,它看不到原始单词。它看到的是整数标记,但学会了识别单词如何一起使用的模式。所以它可以在某种程度上推断出一些单词或一些整数标记具有相似的意义,然后它将这些信息编码到看起来相似的嵌入向量中。

因此,我们需要决定每个向量的长度,例如,“11”这个标记将被转换成一个实值向量。在这个例子中,我们使用长度为 8 的向量,实际上它非常短(通常是 100 到 300 之间)。尝试改变这个嵌入向量中的元素数量,并重新运行这段代码,看看结果会是什么。

所以,我们将嵌入大小设置为 8,然后使用 Keras 将这个嵌入层添加到 RNN 中。它必须是网络中的第一个层:

embedding_layer_size = 8

rnn_type_model.add(Embedding(input_dim=num_top_words,
                    output_dim=embedding_layer_size,
                    input_length=max_num_tokens,
                    name='embedding_layer'))

然后,我们可以添加第一个循环层,我们将使用一个叫做门控循环单元GRU)。通常,你会看到人们使用叫做LSTM的结构,但有些人似乎认为 GRU 更好,因为 LSTM 中有些门是冗余的。实际上,简单的代码在减少门的数量后也能很好地工作。你可以给 LSTM 加上更多的门,但那并不意味着它会变得更好。

所以,让我们定义我们的 GRU 架构;我们设定输出维度为 16,并且需要返回序列:

rnn_type_model.add(GRU(units=16, return_sequences=True))

如果我们看一下图 4中的流程图,我们想要添加第二个循环层:

rnn_type_model.add(GRU(units=8, return_sequences=True))

然后,我们有第三个也是最后一个循环层,它不会输出一个序列,因为它后面会跟随一个全连接层;它应该只给出 GRU 的最终输出,而不是一整个输出序列:

rnn_type_model.add(GRU(units=4))

然后,这里输出的结果将被输入到一个全连接或密集层,这个层应该只输出每个输入序列的一个值。它通过 sigmoid 激活函数处理,因此输出一个介于 0 和 1 之间的值:

rnn_type_model.add(Dense(1, activation='sigmoid'))

然后,我们说我们想使用 Adam 优化器,并设定学习率,同时损失函数应该是 RNN 输出与训练集中的实际类别值之间的二元交叉熵,这个值应该是 0 或 1:

model_optimizer = Adam(lr=1e-3)

rnn_type_model.compile(loss='binary_crossentropy',
              optimizer=model_optimizer,
              metrics=['accuracy'])

现在,我们可以打印出模型的摘要:

rnn_type_model.summary()

_________________________________________________________________
Layer (type) Output Shape Param # 
=================================================================
embedding_layer (Embedding) (None, 544, 8) 80000 
_________________________________________________________________
gru_1 (GRU) (None, None, 16) 1200 
_________________________________________________________________
gru_2 (GRU) (None, None, 8) 600 
_________________________________________________________________
gru_3 (GRU) (None, 4) 156 
_________________________________________________________________
dense_1 (Dense) (None, 1) 5 
=================================================================
Total params: 81,961
Trainable params: 81,961
Non-trainable params: 0
_________________________

如你所见,我们有嵌入层,第一个循环单元,第二、第三个循环单元和密集层。请注意,这个模型的参数并不多。

模型训练与结果分析

现在,是时候开始训练过程了,这里非常简单:

Output:
rnn_type_model.fit(input_train_pad, target_train,
          validation_split=0.05, epochs=3, batch_size=64)

Output:
Train on 23750 samples, validate on 1250 samples
Epoch 1/3
23750/23750 [==============================]23750/23750 [==============================] - 176s 7ms/step - loss: 0.6698 - acc: 0.5758 - val_loss: 0.5039 - val_acc: 0.7784

Epoch 2/3
23750/23750 [==============================]23750/23750 [==============================] - 175s 7ms/step - loss: 0.4631 - acc: 0.7834 - val_loss: 0.2571 - val_acc: 0.8960

Epoch 3/3
23750/23750 [==============================]23750/23750 [==============================] - 174s 7ms/step - loss: 0.3256 - acc: 0.8673 - val_loss: 0.3266 - val_acc: 0.8600

让我们在测试集上测试训练好的模型:

model_result = rnn_type_model.evaluate(input_test_pad, target_test)

Output:
25000/25000 [==============================]25000/25000 [==============================] - 60s 2ms/step

print("Accuracy: {0:.2%}".format(model_result[1]))
Output:
Accuracy: 85.26%

现在,让我们看看一些被错误分类的文本示例。

所以首先,我们计算测试集中前 1,000 个序列的预测类别,然后取实际类别值。我们将它们进行比较,并得到一个索引列表,其中包含不匹配的地方:

target_predicted = rnn_type_model.predict(x=input_test_pad[0:1000])
target_predicted = target_predicted.T[0]

使用阈值来表示所有大于0.5的值将被认为是正类,其他的将被认为是负类:

class_predicted = np.array([1.0 if prob>0.5 else 0.0 for prob in target_predicted])

现在,我们来获取这 1,000 个序列的实际类别:

class_actual = np.array(target_test[0:1000])

让我们从输出中获取错误的样本:

incorrect_samples = np.where(class_predicted != class_actual)
incorrect_samples = incorrect_samples[0]
len(incorrect_samples)

Output:
122

我们看到有 122 个文本被错误分类,占我们计算的 1,000 个文本的 12.1%。让我们来看一下第一个被错误分类的文本:

index = incorrect_samples[0]
index
Output:
9

incorrectly_predicted_text = input_text_test[index]
incorrectly_predicted_text
Output:

'I am not a big music video fan. I think music videos take away personal feelings about a particular song.. Any song. In other words, creative thinking goes out the window. Likewise, Personal feelings aside about MJ, toss aside. This was the best music video of alltime. Simply wonderful. It was a movie. Yes folks it was. Brilliant! You had awesome acting, awesome choreography, and awesome singing. This was spectacular. Simply a plot line of a beautiful young lady dating a man, but was he a man or something sinister. Vincent Price did his thing adding to the song and video. MJ was MJ, enough said about that. This song was to video, what Jaguars are for cars. Top of the line, PERFECTO. What was even better about this was, that we got the real MJ without the thousand facelifts. Though ironically enough, there was more than enough makeup and costumes to go around. Folks go to Youtube. Take 14 mins. out of your life and see for yourself what a wonderful work of art this particular video really is.'

让我们看看这个样本的模型输出以及实际类别:

target_predicted[index]
Output:
0.1529513

class_actual[index]
Output:
1.0

现在,让我们测试一下我们训练好的模型,看看它在一组新数据样本上的表现:

test_sample_1 = "This movie is fantastic! I really like it because it is so good!"
test_sample_2 = "Good movie!"
test_sample_3 = "Maybe I like this movie."
test_sample_4 = "Meh ..."
test_sample_5 = "If I were a drunk teenager then this movie might be good."
test_sample_6 = "Bad movie!"
test_sample_7 = "Not a good movie!"
test_sample_8 = "This movie really sucks! Can I get my money back please?"
test_samples = [test_sample_1, test_sample_2, test_sample_3, test_sample_4, test_sample_5, test_sample_6, test_sample_7, test_sample_8]

现在,让我们将它们转换为整数标记:

test_samples_tokens = tokenizer_obj.texts_to_sequences(test_samples)

然后进行填充:

test_samples_tokens_pad = pad_sequences(test_samples_tokens, maxlen=max_num_tokens,
                           padding=seq_pad, truncating=seq_pad)
test_samples_tokens_pad.shape

Output:
(8, 544)

最后,让我们将模型应用于这些数据:

rnn_type_model.predict(test_samples_tokens_pad)

Output:
array([[0.9496784 ],
 [0.9552593 ],
 [0.9115685 ],
 [0.9464672 ],
 [0.87672734],
 [0.81883633],
 [0.33248223],
 [0.15345531 ]], dtype=float32)

所以,接近零的值意味着负面情感,而接近 1 的值意味着正面情感;最后,这些数字会在每次训练模型时有所变化。

总结

在这一章中,我们介绍了一个有趣的应用——情感分析。情感分析被不同的公司用来追踪客户对其产品的满意度。甚至政府也使用情感分析解决方案来跟踪公民对他们未来想做的事情的满意度。

接下来,我们将重点关注一些可以用于半监督和无监督应用的先进深度学习架构。

第十三章:自编码器 – 特征提取与去噪

自编码器网络如今是广泛使用的深度学习架构之一。它主要用于无监督学习高效解码任务。它也可以通过学习特定数据集的编码或表示来进行降维。在本章中使用自编码器,我们将展示如何通过构建另一个具有相同维度但噪声较少的数据集来去噪你的数据集。为了将这一概念付诸实践,我们将从 MNIST 数据集中提取重要特征,并尝试查看如何通过这一方法显著提高性能。

本章将涵盖以下主题:

  • 自编码器简介

  • 自编码器示例

  • 自编码器架构

  • 压缩 MNIST 数据集

  • 卷积自编码器

  • 去噪自编码器

  • 自编码器的应用

自编码器简介

自编码器是另一种深度学习架构,可以用于许多有趣的任务,但它也可以被看作是普通前馈神经网络的一种变体,其中输出与输入具有相同的维度。如 图 1 所示,自编码器的工作原理是将数据样本 (x[1],...,x[6]) 输入网络。它将在 L2 层学习该数据的较低表示,你可以把它看作是将数据集编码为较低表示的一种方式。然后,网络的第二部分(你可以称之为解码器)负责根据这个表示构建输出!。你可以将网络从输入数据中学习到的中间较低表示看作是它的压缩版本。

和我们迄今为止见过的其他深度学习架构并没有太大不同,自编码器使用反向传播算法。

自编码器神经网络是一种无监督学习算法,应用反向传播,将目标值设为与输入相同:

图 1:一般自编码器架构

自编码器示例

在本章中,我们将通过使用 MNIST 数据集演示一些不同类型的自编码器变体。作为一个具体例子,假设输入 x 是来自 28 x 28 图像的像素强度值(784 个像素);因此,输入数据样本的数量为 n=784。在 L2 层中有 s2=392 个隐藏单元。由于输出将与输入数据样本的维度相同,y ∈ R784。输入层中的神经元数量为 784,中间层 L2 中有 392 个神经元;因此,网络将是一个较低的表示,这是输出的压缩版本。然后,网络将把这个压缩的较低表示 a(L2) ∈ R392 输入到网络的第二部分,后者将尽力从这个压缩版本中重建输入像素 784

自编码器依赖于输入样本由图像像素表示,这些像素在某种程度上是相关的,然后它将利用这一点来重建它们。因此,自编码器有点类似于降维技术,因为它们也学习输入数据的低维表示。

总结一下,典型的自编码器将由三个部分组成:

  1. 编码器部分,负责将输入压缩为低维表示

  2. 代码部分,即编码器的中间结果

  3. 解码器,负责使用该代码重建原始输入

以下图展示了典型自编码器的三个主要组成部分:

图 2:编码器如何在图像上发挥作用

正如我们所提到的,自编码器部分学习输入的压缩表示,然后将其馈送给第三部分,后者尝试重建输入。重建后的输入将类似于输出,但不会完全与原始输出相同,因此自编码器不能用于压缩任务。

自编码器架构

正如我们所提到的,典型的自编码器由三个部分组成。让我们更详细地探索这三部分。为了激励你,我们在本章中不会重新发明轮子。编码器-解码器部分不过是一个完全连接的神经网络,而代码部分是另一个神经网络,但它不是完全连接的。这个代码部分的维度是可控的,我们可以把它当作超参数来处理:

图 3:自编码器的一般编码器-解码器架构

在深入使用自编码器压缩 MNIST 数据集之前,我们将列出可以用于微调自编码器模型的一组超参数。主要有四个超参数:

  1. 代码部分大小:这是中间层的单元数。中间层的单元数越少,我们得到的输入压缩表示越多。

  2. 编码器和解码器的层数:正如我们所提到的,编码器和解码器不过是一个完全连接的神经网络,我们可以通过添加更多层使其尽可能深。

  3. 每层单元数:我们也可以在每一层使用不同的单元数。编码器和解码器的形状与 DeconvNets 非常相似,其中编码器的层数在接近代码部分时减少,然后在接近解码器的最终层时开始增加。

  4. 模型损失函数:我们也可以使用不同的损失函数,例如 MSE 或交叉熵。

在定义这些超参数并赋予它们初始值后,我们可以使用反向传播算法来训练网络。

压缩 MNIST 数据集

在本部分中,我们将构建一个简单的自动编码器,用于压缩 MNIST 数据集。因此,我们将把该数据集中的图像输入到编码器部分,编码器将尝试为它们学习一个压缩的低维表示;然后,我们将在解码器部分尝试重新构建输入图像。

MNIST 数据集

我们将通过使用 TensorFlow 的辅助函数获取 MNIST 数据集来开始实现。

让我们导入实现所需的必要包:

%matplotlib inline

import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt

from tensorflow.examples.tutorials.mnist import input_data
mnist_dataset = input_data.read_data_sets('MNIST_data', validation_size=0)

Output:
Extracting MNIST_data/train-images-idx3-ubyte.gz
Extracting MNIST_data/train-labels-idx1-ubyte.gz
Extracting MNIST_data/t10k-images-idx3-ubyte.gz
Extracting MNIST_data/t10k-labels-idx1-ubyte.gz

让我们先从绘制一些 MNIST 数据集中的示例开始:

# Plotting one image from the training set.
image = mnist_dataset.train.images[2]
plt.imshow(image.reshape((28, 28)), cmap='Greys_r')
Output:

图 4:来自 MNIST 数据集的示例图像

# Plotting one image from the training set.
image = mnist_dataset.train.images[2]
plt.imshow(image.reshape((28, 28)), cmap='Greys_r')

Output:

图 5:来自 MNIST 数据集的示例图像

构建模型

为了构建编码器,我们需要弄清楚每张 MNIST 图像包含多少像素,这样我们就能确定编码器输入层的大小。每张来自 MNIST 数据集的图像是 28 x 28 像素,因此我们将把该矩阵重塑为一个包含 28 x 28 = 784 个像素值的向量。我们不需要对 MNIST 图像进行归一化处理,因为它们已经是归一化的。

让我们开始构建模型的三个组件。在此实现中,我们将使用一个非常简单的架构,即单个隐藏层后接 ReLU 激活函数,如下图所示:

图 6:MNIST 实现的编码器-解码器架构

根据之前的解释,接下来我们将实现这个简单的编码器-解码器架构:

# The size of the encoding layer or the hidden layer.
encoding_layer_dim = 32 

img_size = mnist_dataset.train.images.shape[1]

# defining placeholder variables of the input and target values
inputs_values = tf.placeholder(tf.float32, (None, img_size), name="inputs_values")
targets_values = tf.placeholder(tf.float32, (None, img_size), name="targets_values")

# Defining an encoding layer which takes the input values and incode them.
encoding_layer = tf.layers.dense(inputs_values, encoding_layer_dim, activation=tf.nn.relu)

# Defining the logit layer, which is a fully-connected layer but without any activation applied to its output
logits_layer = tf.layers.dense(encoding_layer, img_size, activation=None)

# Adding a sigmoid layer after the logit layer
decoding_layer = tf.sigmoid(logits_layer, name = "decoding_layer")

# use the sigmoid cross entropy as a loss function
model_loss = tf.nn.sigmoid_cross_entropy_with_logits(logits=logits_layer, labels=targets_values)

# Averaging the loss values accross the input data
model_cost = tf.reduce_mean(model_loss)

# Now we have a cost functiont that we need to optimize using Adam Optimizer
model_optimizier = tf.train.AdamOptimizer().minimize(model_cost)

现在我们已经定义了模型,并且使用了二元交叉熵,因为图像像素已经进行了归一化处理。

模型训练

在本节中,我们将启动训练过程。我们将使用 mnist_dataset 对象的辅助函数来从数据集中获取指定大小的随机批次;然后我们将在这一批图像上运行优化器。

让我们通过创建会话变量来开始本节内容,该变量将负责执行我们之前定义的计算图:

# creating the session
 sess = tf.Session()

接下来,让我们启动训练过程:

num_epochs = 20
train_batch_size = 200

sess.run(tf.global_variables_initializer())
for e in range(num_epochs):
    for ii in range(mnist_dataset.train.num_examples//train_batch_size):
        input_batch = mnist_dataset.train.next_batch(train_batch_size)
        feed_dict = {inputs_values: input_batch[0], targets_values: input_batch[0]}
        input_batch_cost, _ = sess.run([model_cost, model_optimizier], feed_dict=feed_dict)

        print("Epoch: {}/{}...".format(e+1, num_epochs),
              "Training loss: {:.3f}".format(input_batch_cost))
Output:
.
.
.
Epoch: 20/20... Training loss: 0.091
Epoch: 20/20... Training loss: 0.091
Epoch: 20/20... Training loss: 0.093
Epoch: 20/20... Training loss: 0.093
Epoch: 20/20... Training loss: 0.095
Epoch: 20/20... Training loss: 0.095
Epoch: 20/20... Training loss: 0.089
Epoch: 20/20... Training loss: 0.095
Epoch: 20/20... Training loss: 0.095
Epoch: 20/20... Training loss: 0.096
Epoch: 20/20... Training loss: 0.094
Epoch: 20/20... Training loss: 0.093
Epoch: 20/20... Training loss: 0.094
Epoch: 20/20... Training loss: 0.093
Epoch: 20/20... Training loss: 0.095
Epoch: 20/20... Training loss: 0.094
Epoch: 20/20... Training loss: 0.096
Epoch: 20/20... Training loss: 0.092
Epoch: 20/20... Training loss: 0.093
Epoch: 20/20... Training loss: 0.091
Epoch: 20/20... Training loss: 0.093
Epoch: 20/20... Training loss: 0.091
Epoch: 20/20... Training loss: 0.095
Epoch: 20/20... Training loss: 0.094
Epoch: 20/20... Training loss: 0.091
Epoch: 20/20... Training loss: 0.096
Epoch: 20/20... Training loss: 0.089
Epoch: 20/20... Training loss: 0.090
Epoch: 20/20... Training loss: 0.094
Epoch: 20/20... Training loss: 0.088
Epoch: 20/20... Training loss: 0.094
Epoch: 20/20... Training loss: 0.093
Epoch: 20/20... Training loss: 0.091
Epoch: 20/20... Training loss: 0.095
Epoch: 20/20... Training loss: 0.093
Epoch: 20/20... Training loss: 0.091
Epoch: 20/20... Training loss: 0.094
Epoch: 20/20... Training loss: 0.090
Epoch: 20/20... Training loss: 0.091
Epoch: 20/20... Training loss: 0.095
Epoch: 20/20... Training loss: 0.095
Epoch: 20/20... Training loss: 0.094
Epoch: 20/20... Training loss: 0.092
Epoch: 20/20... Training loss: 0.092
Epoch: 20/20... Training loss: 0.093
Epoch: 20/20... Training loss: 0.093

在运行上述代码片段 20 个 epoch 后,我们将得到一个训练好的模型,它能够从 MNIST 数据的测试集中生成或重建图像。请记住,如果我们输入的图像与模型训练时使用的图像不相似,那么重建过程将无法正常工作,因为自动编码器是针对特定数据的。

让我们通过输入一些来自测试集的图像来测试训练好的模型,看看模型在解码器部分如何重建这些图像:

fig, axes = plt.subplots(nrows=2, ncols=10, sharex=True, sharey=True, figsize=(20,4))

input_images = mnist_dataset.test.images[:10]
reconstructed_images, compressed_images = sess.run([decoding_layer, encoding_layer], feed_dict={inputs_values: input_images})

for imgs, row in zip([input_images, reconstructed_images], axes):
    for img, ax in zip(imgs, row):
        ax.imshow(img.reshape((28, 28)), cmap='Greys_r')
        ax.get_xaxis().set_visible(False)
        ax.get_yaxis().set_visible(False)

fig.tight_layout(pad=0.1)

输出:

图 7:原始测试图像(第一行)及其重建(第二行)的示例

如你所见,重建后的图像与输入图像非常接近,但我们可能可以通过在编码器-解码器部分使用卷积层来获得更好的图像。

卷积自动编码器

之前的简单实现很好地完成了从 MNIST 数据集中重建输入图像的任务,但通过在自编码器的编码器和解码器部分添加卷积层,我们可以获得更好的性能。这个替换后得到的网络称为卷积自编码器CAE)。这种能够替换层的灵活性是自编码器的一个巨大优势,使它们能够应用于不同的领域。

我们将用于 CAE 的架构将在网络的解码器部分包含上采样层,以获取图像的重建版本。

数据集

在这个实现中,我们可以使用任何类型的图像数据集,看看卷积版本的自编码器会带来什么变化。我们仍然将使用 MNIST 数据集,因此让我们开始使用 TensorFlow 辅助函数获取数据集:

%matplotlib inline

import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from tensorflow.examples.tutorials.mnist import input_data
mnist_dataset = input_data.read_data_sets('MNIST_data', validation_size=0)

Output:
from tensorflow.examples.tutorials.mnist import input_data

mnist_dataset = input_data.read_data_sets('MNIST_data', validation_size=0)

Extracting MNIST_data/train-images-idx3-ubyte.gz
Extracting MNIST_data/train-labels-idx1-ubyte.gz
Extracting MNIST_data/t10k-images-idx3-ubyte.gz
Extracting MNIST_data/t10k-labels-idx1-ubyte.gz

让我们展示数据集中的一个数字:

# Plotting one image from the training set.
image = mnist_dataset.train.images[2]
plt.imshow(image.reshape((28, 28)), cmap='Greys_r')

输出:

图 8:来自 MNIST 数据集的示例图像

构建模型

在这个实现中,我们将使用步幅为 1 的卷积层,并将填充参数设置为相同。这样,我们不会改变图像的高度或宽度。同时,我们使用了一组最大池化层来减少图像的宽度和高度,从而构建图像的压缩低维表示。

所以,让我们继续构建网络的核心部分:

learning_rate = 0.001

# Define the placeholder variable sfor the input and target values
inputs_values = tf.placeholder(tf.float32, (None, 28,28,1), name="inputs_values")
targets_values = tf.placeholder(tf.float32, (None, 28,28,1), name="targets_values")

# Defining the Encoder part of the netowrk
# Defining the first convolution layer in the encoder parrt
# The output tenosor will be in the shape of 28x28x16
conv_layer_1 = tf.layers.conv2d(inputs=inputs_values, filters=16, kernel_size=(3,3), padding='same', activation=tf.nn.relu)

# The output tenosor will be in the shape of 14x14x16
maxpool_layer_1 = tf.layers.max_pooling2d(conv_layer_1, pool_size=(2,2), strides=(2,2), padding='same')

# The output tenosor will be in the shape of 14x14x8
conv_layer_2 = tf.layers.conv2d(inputs=maxpool_layer_1, filters=8, kernel_size=(3,3), padding='same', activation=tf.nn.relu)

# The output tenosor will be in the shape of 7x7x8
maxpool_layer_2 = tf.layers.max_pooling2d(conv_layer_2, pool_size=(2,2), strides=(2,2), padding='same')

# The output tenosor will be in the shape of 7x7x8
conv_layer_3 = tf.layers.conv2d(inputs=maxpool_layer_2, filters=8, kernel_size=(3,3), padding='same', activation=tf.nn.relu)

# The output tenosor will be in the shape of 4x4x8
encoded_layer = tf.layers.max_pooling2d(conv_layer_3, pool_size=(2,2), strides=(2,2), padding='same')

# Defining the Decoder part of the netowrk
# Defining the first upsampling layer in the decoder part
# The output tenosor will be in the shape of 7x7x8
upsample_layer_1 = tf.image.resize_images(encoded_layer, size=(7,7), method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)

# The output tenosor will be in the shape of 7x7x8
conv_layer_4 = tf.layers.conv2d(inputs=upsample_layer_1, filters=8, kernel_size=(3,3), padding='same', activation=tf.nn.relu)

# The output tenosor will be in the shape of 14x14x8
upsample_layer_2 = tf.image.resize_images(conv_layer_4, size=(14,14), method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)

# The output tenosor will be in the shape of 14x14x8
conv_layer_5 = tf.layers.conv2d(inputs=upsample_layer_2, filters=8, kernel_size=(3,3), padding='same', activation=tf.nn.relu)

# The output tenosor will be in the shape of 28x28x8
upsample_layer_3 = tf.image.resize_images(conv_layer_5, size=(28,28), method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)

# The output tenosor will be in the shape of 28x28x16
conv6 = tf.layers.conv2d(inputs=upsample_layer_3, filters=16, kernel_size=(3,3), padding='same', activation=tf.nn.relu)

# The output tenosor will be in the shape of 28x28x1
logits_layer = tf.layers.conv2d(inputs=conv6, filters=1, kernel_size=(3,3), padding='same', activation=None)

# feeding the logits values to the sigmoid activation function to get the reconstructed images
decoded_layer = tf.nn.sigmoid(logits_layer)

# feeding the logits to sigmoid while calculating the cross entropy
model_loss = tf.nn.sigmoid_cross_entropy_with_logits(labels=targets_values, logits=logits_layer)

# Getting the model cost and defining the optimizer to minimize it
model_cost = tf.reduce_mean(model_loss)
model_optimizer = tf.train.AdamOptimizer(learning_rate).minimize(model_cost)

现在我们可以开始了。我们已经构建了卷积神经网络的解码器-解码器部分,并展示了输入图像在解码器部分如何被重建。

模型训练

现在我们已经构建了模型,我们可以通过生成来自 MNIST 数据集的随机批次并将它们输入到之前定义的优化器中来启动学习过程。

让我们从创建会话变量开始;它将负责执行我们之前定义的计算图:

sess = tf.Session()
num_epochs = 20
train_batch_size = 200
sess.run(tf.global_variables_initializer())

for e in range(num_epochs):
    for ii in range(mnist_dataset.train.num_examples//train_batch_size):
        input_batch = mnist_dataset.train.next_batch(train_batch_size)
        input_images = input_batch[0].reshape((-1, 28, 28, 1))
        input_batch_cost, _ = sess.run([model_cost, model_optimizer], feed_dict={inputs_values: input_images,targets_values: input_images})

        print("Epoch: {}/{}...".format(e+1, num_epochs),
              "Training loss: {:.3f}".format(input_batch_cost))
Output:
.
.
.
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.099
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.100
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.098
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.104
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.098
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.098
Epoch: 20/20... Training loss: 0.099
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.104
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.105
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.100
Epoch: 20/20... Training loss: 0.099
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.104
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.099
Epoch: 20/20... Training loss: 0.098
Epoch: 20/20... Training loss: 0.100
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.100
Epoch: 20/20... Training loss: 0.100
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.098
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.100
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.099
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.096
Epoch: 20/20... Training loss: 0.104
Epoch: 20/20... Training loss: 0.104
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.103
Epoch: 20/20... Training loss: 0.104
Epoch: 20/20... Training loss: 0.099
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.101
Epoch: 20/20... Training loss: 0.099
Epoch: 20/20... Training loss: 0.100
Epoch: 20/20... Training loss: 0.102
Epoch: 20/20... Training loss: 0.100
Epoch: 20/20... Training loss: 0.098
Epoch: 20/20... Training loss: 0.100
Epoch: 20/20... Training loss: 0.097
Epoch: 20/20... Training loss: 0.102

在运行前面的代码片段 20 个周期后,我们将得到一个训练好的 CAE,因此我们可以继续通过输入来自 MNIST 数据集的相似图像来测试这个模型:

fig, axes = plt.subplots(nrows=2, ncols=10, sharex=True, sharey=True, figsize=(20,4))
input_images = mnist_dataset.test.images[:10]
reconstructed_images = sess.run(decoded_layer, feed_dict={inputs_values: input_images.reshape((10, 28, 28, 1))})

for imgs, row in zip([input_images, reconstructed_images], axes):
    for img, ax in zip(imgs, row):
        ax.imshow(img.reshape((28, 28)), cmap='Greys_r')
        ax.get_xaxis().set_visible(False)
        ax.get_yaxis().set_visible(False)

fig.tight_layout(pad=0.1)

Output:

图 9:原始测试图像(第一行)及其重建图像(第二行),使用卷积自编码器

去噪自编码器

我们可以进一步优化自编码器架构,迫使它学习关于输入数据的重要特征。通过向输入图像添加噪声,并以原始图像作为目标,模型将尝试去除这些噪声,并学习关于它们的重要特征,以便在输出中生成有意义的重建图像。这种 CAE 架构可以用于去除输入图像中的噪声。这种自编码器的特定变种称为去噪自编码器

图 10:原始图像及添加少量高斯噪声后的相同图像示例

所以让我们从实现下图中的架构开始。我们在这个去噪自编码器架构中唯一添加的额外内容就是在原始输入图像中加入了一些噪声:

图 11:自编码器的通用去噪架构

构建模型

在这个实现中,我们将在编码器和解码器部分使用更多的层,原因在于我们给输入增加了新的复杂度。

下一个模型与之前的 CAE 完全相同,只是增加了额外的层,这将帮助我们从有噪声的图像中重建出无噪声的图像。

那么,让我们继续构建这个架构:

learning_rate = 0.001

# Define the placeholder variable sfor the input and target values
inputs_values = tf.placeholder(tf.float32, (None, 28, 28, 1), name='inputs_values')
targets_values = tf.placeholder(tf.float32, (None, 28, 28, 1), name='targets_values')

# Defining the Encoder part of the netowrk
# Defining the first convolution layer in the encoder parrt
# The output tenosor will be in the shape of 28x28x32
conv_layer_1 = tf.layers.conv2d(inputs=inputs_values, filters=32, kernel_size=(3,3), padding='same', activation=tf.nn.relu)

# The output tenosor will be in the shape of 14x14x32
maxpool_layer_1 = tf.layers.max_pooling2d(conv_layer_1, pool_size=(2,2), strides=(2,2), padding='same')

# The output tenosor will be in the shape of 14x14x32
conv_layer_2 = tf.layers.conv2d(inputs=maxpool_layer_1, filters=32, kernel_size=(3,3), padding='same', activation=tf.nn.relu)

# The output tenosor will be in the shape of 7x7x32
maxpool_layer_2 = tf.layers.max_pooling2d(conv_layer_2, pool_size=(2,2), strides=(2,2), padding='same')

# The output tenosor will be in the shape of 7x7x16
conv_layer_3 = tf.layers.conv2d(inputs=maxpool_layer_2, filters=16, kernel_size=(3,3), padding='same', activation=tf.nn.relu)

# The output tenosor will be in the shape of 4x4x16
encoding_layer = tf.layers.max_pooling2d(conv_layer_3, pool_size=(2,2), strides=(2,2), padding='same')

# Defining the Decoder part of the netowrk
# Defining the first upsampling layer in the decoder part
# The output tenosor will be in the shape of 7x7x16
upsample_layer_1 = tf.image.resize_images(encoding_layer, size=(7,7), method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)

# The output tenosor will be in the shape of 7x7x16
conv_layer_4 = tf.layers.conv2d(inputs=upsample_layer_1, filters=16, kernel_size=(3,3), padding='same', activation=tf.nn.relu)

# The output tenosor will be in the shape of 14x14x16
upsample_layer_2 = tf.image.resize_images(conv_layer_4, size=(14,14), method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)

# The output tenosor will be in the shape of 14x14x32
conv_layer_5 = tf.layers.conv2d(inputs=upsample_layer_2, filters=32, kernel_size=(3,3), padding='same', activation=tf.nn.relu)

# The output tenosor will be in the shape of 28x28x32
upsample_layer_3 = tf.image.resize_images(conv_layer_5, size=(28,28), method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)

# The output tenosor will be in the shape of 28x28x32
conv_layer_6 = tf.layers.conv2d(inputs=upsample_layer_3, filters=32, kernel_size=(3,3), padding='same', activation=tf.nn.relu)

# The output tenosor will be in the shape of 28x28x1
logits_layer = tf.layers.conv2d(inputs=conv_layer_6, filters=1, kernel_size=(3,3), padding='same', activation=None)

# feeding the logits values to the sigmoid activation function to get the reconstructed images
decoding_layer = tf.nn.sigmoid(logits_layer)

# feeding the logits to sigmoid while calculating the cross entropy
model_loss = tf.nn.sigmoid_cross_entropy_with_logits(labels=targets_values, logits=logits_layer)

# Getting the model cost and defining the optimizer to minimize it
model_cost = tf.reduce_mean(model_loss)
model_optimizer = tf.train.AdamOptimizer(learning_rate).minimize(model_cost)

现在我们有了一个更复杂或更深的卷积模型版本。

模型训练

现在是时候开始训练这个更深的网络了,这将需要更多的时间来收敛,通过从有噪声的输入中重建无噪声的图像。

所以让我们先创建会话变量:

sess = tf.Session()

接下来,我们将启动训练过程,但使用更多的训练轮次:

num_epochs = 100
train_batch_size = 200

# Defining a noise factor to be added to MNIST dataset
mnist_noise_factor = 0.5
sess.run(tf.global_variables_initializer())

for e in range(num_epochs):
    for ii in range(mnist_dataset.train.num_examples//train_batch_size):
        input_batch = mnist_dataset.train.next_batch(train_batch_size)

        # Getting and reshape the images from the corresponding batch
        batch_images = input_batch[0].reshape((-1, 28, 28, 1))

        # Add random noise to the input images
        noisy_images = batch_images + mnist_noise_factor * np.random.randn(*batch_images.shape)

        # Clipping all the values that are above 0 or above 1
        noisy_images = np.clip(noisy_images, 0., 1.)

        # Set the input images to be the noisy ones and the original images to be the target
        input_batch_cost, _ = sess.run([model_cost, model_optimizer], feed_dict={inputs_values: noisy_images,
                                                         targets_values: batch_images})

        print("Epoch: {}/{}...".format(e+1, num_epochs),
              "Training loss: {:.3f}".format(input_batch_cost))
Output:
.
.
.
Epoch: 100/100... Training loss: 0.098
Epoch: 100/100... Training loss: 0.101
Epoch: 100/100... Training loss: 0.103
Epoch: 100/100... Training loss: 0.098
Epoch: 100/100... Training loss: 0.102
Epoch: 100/100... Training loss: 0.102
Epoch: 100/100... Training loss: 0.103
Epoch: 100/100... Training loss: 0.101
Epoch: 100/100... Training loss: 0.098
Epoch: 100/100... Training loss: 0.099
Epoch: 100/100... Training loss: 0.096
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.103
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.101
Epoch: 100/100... Training loss: 0.099
Epoch: 100/100... Training loss: 0.096
Epoch: 100/100... Training loss: 0.102
Epoch: 100/100... Training loss: 0.099
Epoch: 100/100... Training loss: 0.098
Epoch: 100/100... Training loss: 0.102
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.099
Epoch: 100/100... Training loss: 0.098
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.099
Epoch: 100/100... Training loss: 0.102
Epoch: 100/100... Training loss: 0.099
Epoch: 100/100... Training loss: 0.102
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.101
Epoch: 100/100... Training loss: 0.102
Epoch: 100/100... Training loss: 0.098
Epoch: 100/100... Training loss: 0.103
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.098
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.097
Epoch: 100/100... Training loss: 0.099
Epoch: 100/100... Training loss: 0.100
Epoch: 100/100... Training loss: 0.101
Epoch: 100/100... Training loss: 0.101

现在我们已经训练好了模型,能够生成无噪声的图像,这使得自编码器适用于许多领域。

在接下来的代码片段中,我们不会直接将 MNIST 测试集的原始图像输入到模型中,因为我们需要先给这些图像添加噪声,看看训练好的模型如何生成无噪声的图像。

在这里,我给测试图像加入噪声,并通过自编码器传递它们。即使有时很难分辨原始的数字是什么,模型仍然能够出色地去除噪声:

#Defining some figures
fig, axes = plt.subplots(nrows=2, ncols=10, sharex=True, sharey=True, figsize=(20,4))

#Visualizing some images
input_images = mnist_dataset.test.images[:10]
noisy_imgs = input_images + mnist_noise_factor * np.random.randn(*input_images.shape)

#Clipping and reshaping the noisy images
noisy_images = np.clip(noisy_images, 0., 1.).reshape((10, 28, 28, 1))

#Getting the reconstructed images
reconstructed_images = sess.run(decoding_layer, feed_dict={inputs_values: noisy_images})

#Visualizing the input images and the noisy ones
for imgs, row in zip([noisy_images, reconstructed_images], axes):
    for img, ax in zip(imgs, row):
        ax.imshow(img.reshape((28, 28)), cmap='Greys_r')
        ax.get_xaxis().set_visible(False)
        ax.get_yaxis().set_visible(False)

fig.tight_layout(pad=0.1)

Output:

图 12:原始测试图像中加入一些高斯噪声(顶部行)以及基于训练好的去噪自编码器重建后的图像示例

自编码器的应用

在之前从较低表示构建图像的示例中,我们看到它与原始输入非常相似,而且我们还看到在去噪噪声数据集时,卷积自编码网络(CANs)的优势。我们上面实现的这种示例对于图像构建应用和数据集去噪非常有用。因此,你可以将上述实现推广到你感兴趣的任何其他示例。

此外,在本章中,我们还展示了自编码器架构的灵活性,以及我们如何对其进行各种更改。我们甚至测试了它在解决更复杂的图像去噪问题中的表现。这种灵活性为自编码器的更多应用开辟了大门。

图像上色

自编码器——尤其是卷积版本——可以用于更难的任务,例如图像上色。在接下来的示例中,我们给模型输入一张没有颜色的图像,经过自编码器模型重建后的图像将会被上色:

图 13:CAE 模型训练进行图像上色

图 14:上色论文架构

现在我们的自编码器已经训练完成,我们可以用它给我们之前从未见过的图片上色!

这种应用可以用于给拍摄于早期相机时代的非常老旧图像上色。

更多应用

另一个有趣的应用是生成更高分辨率的图像,或者像下图所示的神经图像增强。

这些图展示了理查德·张提供的更真实的图像上色版本:

图 15:理查德·张(Richard Zhang)、菲利普·伊索拉(Phillip Isola)和亚历克塞·A·埃夫罗斯(Alexei A. Efros)提供的彩色图像上色

这张图展示了自编码器在图像增强中的另一个应用:

图 16:亚历克西(Alexjc)提供的神经增强(github.com/alexjc/neural-enhance

总结

在本章中,我们介绍了一种全新的架构,可以用于许多有趣的应用。自编码器非常灵活,所以可以随意提出你自己在图像增强、上色或构建方面的问题。此外,还有自编码器的更多变体,称为变分自编码器。它们也被用于非常有趣的应用,比如图像生成。

第十四章:生成对抗网络

生成对抗网络GANs)是深度神经网络架构,由两个相互对立的网络组成(因此得名对抗)。

GANs 是在 2014 年由 Ian Goodfellow 和其他研究人员,包括 Yoshua Bengio,在蒙特利尔大学的一篇论文中提出的(arxiv.org/abs/1406.2661)。Facebook 的 AI 研究总监 Yann LeCun 曾提到,对抗训练是过去 10 年中在机器学习领域最有趣的想法。

GANs 的潜力巨大,因为它们可以学习模仿任何数据分布。也就是说,GANs 可以被训练生成在任何领域与我们现实世界极为相似的世界:图像、音乐、语音或散文。从某种意义上说,它们是机器人艺术家,它们的输出令人印象深刻(www.nytimes.com/2017/08/14/arts/design/google-how-ai-creates-new-music-and-new-artists-project-magenta.html)——并且也令人感动。

本章将涵盖以下内容:

  • 直观介绍

  • GANs 的简单实现

  • 深度卷积 GANs

直观介绍

本节将以非常直观的方式介绍 GANs。为了理解 GANs 的工作原理,我们将采用一个虚拟情境——获得一张派对门票的过程。

故事从一个非常有趣的派对或活动开始,这个活动在某个地方举行,你非常想参加。你听说这个活动时已经很晚了,所有的门票都卖光了,但你会不惜一切代价进入派对。所以,你想到了一个主意!你将尝试伪造一张与原始门票完全相同或者非常相似的票。但因为生活总是充满挑战,还有一个难题:你不知道原始门票长什么样子。所以,根据你参加过类似派对的经验,你开始想象这张门票可能是什么样子的,并开始根据你的想象设计这张票。

你将尝试设计这张票,然后去活动现场,向保安展示这张票。希望他们会被说服,并让你进去。但你不想多次向保安展示自己的脸,于是你决定寻求朋友的帮助,朋友会拿着你对原始门票的初步猜测去向保安展示。如果他们没有让他进去,他会根据看到其他人用真正的门票进入的情况,给你一些信息,告诉你这张票可能是什么样的。你会根据朋友的反馈不断完善门票,直到保安允许他进入。到那时——只有到那时——你才会设计出一张完全相同的票,并让自己也顺利进入。

不要过多思考这个故事有多不现实,但 GAN 的工作原理与这个故事非常相似。如今 GAN 非常流行,人们将其用于计算机视觉领域的许多应用。

你可以将 GAN 用于许多有趣的应用,我们将在实现并提到其中的一些应用。

在 GAN 中,有两个主要组成部分在许多计算机视觉领域中取得了突破。第一个组件被称为生成器,第二个组件被称为判别器

  • 生成器将尝试从特定的概率分布中生成数据样本,这与那个试图复制活动票的人的行为非常相似。

  • 判别器将判断(就像安保人员试图找出票上的缺陷,以决定它是原始的还是伪造的)输入是来自原始训练集(原始票)还是来自生成器部分(由试图复制原始票的人设计):

图 1:GAN——通用架构

简单的 GAN 实现

从伪造活动票的故事来看,GAN 的概念似乎非常直观。为了清楚地理解 GAN 如何工作以及如何实现它们,我们将展示一个在 MNIST 数据集上实现简单 GAN 的例子。

首先,我们需要构建 GAN 网络的核心,主要由两个部分组成:生成器和判别器。正如我们所说,生成器将尝试从特定的概率分布中生成或伪造数据样本;判别器则能够访问和看到实际的数据样本,它将判断生成器的输出是否在设计上存在缺陷,或者它与原始数据样本有多么接近。类似于事件的场景,生成器的整个目的是尽力说服判别器,生成的图像来自真实数据集,从而试图欺骗判别器。

训练过程的结果类似于活动故事的结局;生成器最终将成功生成看起来与原始数据样本非常相似的图像:

图 2:MNIST 数据集的 GAN 通用架构

任何 GAN 的典型结构如图 2所示,将会在 MNIST 数据集上进行训练。图中的潜在样本部分是一个随机的思维或向量,生成器利用它来用假图像复制真实图像。

如我们所述,判别器充当一个判断者,它会尝试将生成器设计的假图像与真实图像区分开来。因此,这个网络的输出将是二值的,可以通过一个 sigmoid 函数表示,0 表示输入是一个假图像,1 表示输入是一个真实图像。

让我们继续并开始实现这个架构,看看它在 MNIST 数据集上的表现。

让我们从导入实现所需的库开始:

%matplotlib inline

import matplotlib.pyplot as plt
import pickle as pkl

import numpy as np
import tensorflow as tf

我们将使用 MNIST 数据集,因此我们将使用 TensorFlow 辅助工具获取数据集并将其存储在某个地方:

from tensorflow.examples.tutorials.mnist import input_data
mnist_dataset = input_data.read_data_sets('MNIST_data')
Output:
Extracting MNIST_data/train-images-idx3-ubyte.gz
Extracting MNIST_data/train-labels-idx1-ubyte.gz
Extracting MNIST_data/t10k-images-idx3-ubyte.gz
Extracting MNIST_data/t10k-labels-idx1-ubyte.gz

模型输入

在深入构建 GAN 的核心部分(由生成器和鉴别器表示)之前,我们将定义计算图的输入。如 图 2 所示,我们需要两个输入。第一个是实际图像,将传递给鉴别器。另一个输入称为 潜在空间,它将传递给生成器并用于生成假图像:

# Defining the model input for the generator and discrimator
def inputs_placeholders(discrimator_real_dim, gen_z_dim):
    real_discrminator_input = tf.placeholder(tf.float32, (None, discrimator_real_dim), name="real_discrminator_input")
    generator_inputs_z = tf.placeholder(tf.float32, (None, gen_z_dim), name="generator_input_z")

    return real_discrminator_input, generator_inputs_z

图 3:MNIST GAN 实现的架构

现在是时候开始构建我们架构的两个核心组件了。我们将从构建生成器部分开始。如 图 3 所示,生成器将包含至少一个隐藏层,该层将作为近似器工作。此外,我们将使用一种称为 leaky ReLU 的激活函数,而不是使用普通的 ReLU 激活函数。这样,梯度值就可以在层之间流动,而没有任何限制(关于 leaky ReLU 的更多内容将在下一节介绍)。

变量作用域

变量作用域是 TensorFlow 的一个功能,帮助我们完成以下任务:

  • 确保我们有一些命名约定以便稍后能检索它们,例如,可以让它们以“generator”或“discriminator”开头,这将帮助我们在训练网络时。我们本可以使用作用域命名功能,但这个功能对第二个目的帮助不大。

  • 为了能够重用或重新训练相同的网络,但使用不同的输入。例如,我们将从生成器中采样假图像,以查看生成器在复制原始图像方面的表现如何。另外,鉴别器将能够访问真实图像和假图像,这将使我们能够重用变量,而不是在构建计算图时创建新变量。

以下语句将展示如何使用 TensorFlow 的变量作用域功能:

with tf.variable_scope('scopeName', reuse=False):
    # Write your code here

你可以在www.tensorflow.org/programmers_guide/variable_scope#the_problem阅读更多关于使用变量作用域功能的好处。

Leaky ReLU

我们提到过,我们将使用一个不同版本的 ReLU 激活函数,称为 leaky ReLU。传统版本的 ReLU 激活函数会选择输入值与零的最大值,换句话说,将负值截断为零。而 leaky ReLU,即我们将使用的版本,允许某些负值存在,因此得名 leaky ReLU

有时,如果我们使用传统的 ReLU 激活函数,网络会陷入一个叫做“死亡状态”的常见状态,这时网络对于所有输出都会产生零值。

使用 Leaky ReLU 的目的是通过允许一些负值通过,防止出现“死亡”状态。

使生成器正常工作的整个思路是接收来自判别器的梯度值,如果网络处于“死亡”状态,则学习过程无法发生。

以下图示展示了传统 ReLU 和其 Leaky 版本之间的区别:

图 4:ReLU 函数

图 5:Leaky ReLU 激活函数

Leaky ReLU 激活函数在 TensorFlow 中没有实现,因此我们需要自己实现。该激活函数的输出如果输入为正,则为正值,如果输入为负,则为一个受控的负值。我们将通过一个名为 alpha 的参数来控制负值的大小,从而通过允许一些负值通过来为网络引入容忍度。

以下方程表示我们将要实现的 Leaky ReLU:

生成器

MNIST 图像的值已归一化在 0 和 1 之间,在这个范围内,sigmoid 激活函数表现最佳。但是,实际上,发现 tanh 激活函数的性能优于其他任何函数。因此,为了使用 tanh 激活函数,我们需要将这些图像的像素值范围重新缩放到 -1 和 1 之间:

def generator(gen_z, gen_out_dim, num_hiddern_units=128, reuse_vars=False, leaky_relu_alpha=0.01):

    ''' Building the generator part of the network

        Function arguments
        ---------
        gen_z : the generator input tensor
        gen_out_dim : the output shape of the generator
        num_hiddern_units : Number of neurons/units in the hidden layer
        reuse_vars : Reuse variables with tf.variable_scope
        leaky_relu_alpha : leaky ReLU parameter

        Function Returns
        -------
        tanh_output, logits_layer: 
    '''
    with tf.variable_scope('generator', reuse=reuse_vars):

        # Defining the generator hidden layer
        hidden_layer_1 = tf.layers.dense(gen_z, num_hiddern_units, activation=None)

        # Feeding the output of hidden_layer_1 to leaky relu
        hidden_layer_1 = tf.maximum(hidden_layer_1, leaky_relu_alpha*hidden_layer_1)

        # Getting the logits and tanh layer output
        logits_layer = tf.layers.dense(hidden_layer_1, gen_out_dim, activation=None)
        tanh_output = tf.nn.tanh(logits_layer)

        return tanh_output, logits_layer

现在生成器部分已经准备好了,让我们继续定义网络的第二个组件。

判别器

接下来,我们将构建生成对抗网络中的第二个主要组件,即判别器。判别器与生成器非常相似,但我们将使用 sigmoid 激活函数,而不是 tanh 激活函数;它将输出一个二进制结果,表示判别器对输入图像的判断:

def discriminator(disc_input, num_hiddern_units=128, reuse_vars=False, leaky_relu_alpha=0.01):
    ''' Building the discriminator part of the network

        Function Arguments
        ---------
        disc_input : discrminator input tensor
        num_hiddern_units : Number of neurons/units in the hidden layer
        reuse_vars : Reuse variables with tf.variable_scope
        leaky_relu_alpha : leaky ReLU parameter

        Function Returns
        -------
        sigmoid_out, logits_layer: 
    '''
    with tf.variable_scope('discriminator', reuse=reuse_vars):

        # Defining the generator hidden layer
        hidden_layer_1 = tf.layers.dense(disc_input, num_hiddern_units, activation=None)

        # Feeding the output of hidden_layer_1 to leaky relu
        hidden_layer_1 = tf.maximum(hidden_layer_1, leaky_relu_alpha*hidden_layer_1)

        logits_layer = tf.layers.dense(hidden_layer_1, 1, activation=None)
        sigmoid_out = tf.nn.sigmoid(logits_layer)

        return sigmoid_out, logits_layer

构建 GAN 网络

在定义了构建生成器和判别器部分的主要函数后,接下来是将它们堆叠起来,并定义模型的损失函数和优化器进行实现。

模型超参数

我们可以通过更改以下超参数集来微调 GAN:

# size of discriminator input image
#28 by 28 will flattened to be 784
input_img_size = 784 

# size of the generator latent vector
gen_z_size = 100

# number of hidden units for the generator and discriminator hidden layers
gen_hidden_size = 128
disc_hidden_size = 128

#leaky ReLU alpha parameter which controls the leak of the function
leaky_relu_alpha = 0.01

# smoothness of the label 
label_smooth = 0.1

定义生成器和判别器

在定义了生成虚假 MNIST 图像的架构的两个主要部分后(这些图像看起来和真实的完全一样),现在是时候使用我们目前定义的函数来构建网络了。为了构建网络,我们将按照以下步骤进行:

  1. 定义我们的模型输入,这将由两个变量组成。其中一个变量是真实图像,将被输入到判别器中,另一个是潜在空间,将被生成器用于复制原始图像。

  2. 调用定义好的生成器函数来构建网络的生成器部分。

  3. 调用定义的判别器函数来构建网络的判别器部分,但我们将调用这个函数两次。第一次调用将用于真实数据,第二次调用将用于来自生成器的假数据。

  4. 通过重用变量保持真实图像和假图像的权重相同:

tf.reset_default_graph()

# creating the input placeholders for the discrminator and generator
real_discrminator_input, generator_input_z = inputs_placeholders(input_img_size, gen_z_size)

#Create the generator network
gen_model, gen_logits = generator(generator_input_z, input_img_size, gen_hidden_size, reuse_vars=False, leaky_relu_alpha=leaky_relu_alpha)

# gen_model is the output of the generator
#Create the generator network
disc_model_real, disc_logits_real = discriminator(real_discrminator_input, disc_hidden_size, reuse_vars=False, leaky_relu_alpha=leaky_relu_alpha)
disc_model_fake, disc_logits_fake = discriminator(gen_model, disc_hidden_size, reuse_vars=True, leaky_relu_alpha=leaky_relu_alpha)

判别器和生成器的损失

在这一部分,我们需要定义判别器和生成器的损失,这可以看作是该实现中最棘手的部分。

我们知道生成器试图复制原始图像,而判别器充当一个判断者,接收来自生成器和原始输入图像的两种图像。所以,在设计每个部分的损失时,我们需要关注两个方面。

首先,我们需要网络的判别器部分能够区分由生成器生成的假图像和来自原始训练示例的真实图像。在训练时,我们将为判别器部分提供一个批次,该批次分为两类。第一类是来自原始输入的图像,第二类是生成器生成的假图像。

所以,判别器的最终总损失将是其将真实样本识别为真实和将假样本识别为假的能力之和;然后最终的总损失将是:

tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=logits_layer, labels=labels))

所以,我们需要计算两个损失来得出最终的判别器损失。

第一个损失,disc_loss_real,将根据我们从判别器得到的logits值和labels计算,这里标签将全部为 1,因为我们知道这个小批次中的所有图像都来自 MNIST 数据集的真实输入图像。为了增强模型在测试集上的泛化能力并提供更好的结果,实践中发现将 1 的值改为 0.9 更好。对标签值进行这样的修改引入了标签平滑

 labels = tf.ones_like(tensor) * (1 - smooth)

对于判别器损失的第二部分,即判别器检测假图像的能力,损失将是判别器从生成器得到的 logits 值与标签之间的差异;这些标签全为 0,因为我们知道这个小批次中的所有图像都是来自生成器,而不是来自原始输入图像。

现在我们已经讨论了判别器损失,我们还需要计算生成器的损失。生成器损失将被称为gen_loss,它是disc_logits_fake(判别器对于假图像的输出)和标签之间的损失(由于生成器试图通过其设计的假图像来说服判别器,因此标签将全部为 1):


# calculating the losses of the discrimnator and generator
disc_labels_real = tf.ones_like(disc_logits_real) * (1 - label_smooth)
disc_labels_fake = tf.zeros_like(disc_logits_fake)

disc_loss_real = tf.nn.sigmoid_cross_entropy_with_logits(labels=disc_labels_real, logits=disc_logits_real)
disc_loss_fake = tf.nn.sigmoid_cross_entropy_with_logits(labels=disc_labels_fake, logits=disc_logits_fake)

#averaging the disc loss
disc_loss = tf.reduce_mean(disc_loss_real + disc_loss_fake)

#averaging the gen loss
gen_loss = tf.reduce_mean(
    tf.nn.sigmoid_cross_entropy_with_logits(
        labels=tf.ones_like(disc_logits_fake), 
        logits=disc_logits_fake))

优化器

最后是优化器部分!在这一部分,我们将定义训练过程中使用的优化标准。首先,我们将分别更新生成器和鉴别器的变量,因此我们需要能够获取每部分的变量。

对于第一个优化器,即生成器优化器,我们将从计算图的可训练变量中获取所有以generator开头的变量;然后,我们可以通过变量的名称来查看每个变量代表什么。

我们对鉴别器的变量也做相同的处理,允许所有以discriminator开头的变量。之后,我们可以将需要优化的变量列表传递给优化器。

所以,TensorFlow 的变量范围功能使我们能够获取以特定字符串开头的变量,然后我们可以得到两份不同的变量列表,一份是生成器的,一份是鉴别器的:


# building the model optimizer

learning_rate = 0.002

# Getting the trainable_variables of the computational graph, split into Generator and Discrimnator parts
trainable_vars = tf.trainable_variables()
gen_vars = [var for var in trainable_vars if var.name.startswith("generator")]
disc_vars = [var for var in trainable_vars if var.name.startswith("discriminator")]

disc_train_optimizer = tf.train.AdamOptimizer().minimize(disc_loss, var_list=disc_vars)
gen_train_optimizer = tf.train.AdamOptimizer().minimize(gen_loss, var_list=gen_vars)

模型训练

现在,让我们开始训练过程,看看 GAN 是如何生成与 MNIST 图像相似的图像的:

train_batch_size = 100
num_epochs = 100
generated_samples = []
model_losses = []

saver = tf.train.Saver(var_list = gen_vars)

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())

    for e in range(num_epochs):
        for ii in range(mnist_dataset.train.num_examples//train_batch_size):
            input_batch = mnist_dataset.train.next_batch(train_batch_size)

            # Get images, reshape and rescale to pass to D
            input_batch_images = input_batch[0].reshape((train_batch_size, 784))
            input_batch_images = input_batch_images*2 - 1

            # Sample random noise for G
            gen_batch_z = np.random.uniform(-1, 1, size=(train_batch_size, gen_z_size))

            # Run optimizers
            _ = sess.run(disc_train_optimizer, feed_dict={real_discrminator_input: input_batch_images, generator_input_z: gen_batch_z})
            _ = sess.run(gen_train_optimizer, feed_dict={generator_input_z: gen_batch_z})

        # At the end of each epoch, get the losses and print them out
        train_loss_disc = sess.run(disc_loss, {generator_input_z: gen_batch_z, real_discrminator_input: input_batch_images})
        train_loss_gen = gen_loss.eval({generator_input_z: gen_batch_z})

        print("Epoch {}/{}...".format(e+1, num_epochs),
              "Disc Loss: {:.3f}...".format(train_loss_disc),
              "Gen Loss: {:.3f}".format(train_loss_gen)) 

        # Save losses to view after training
        model_losses.append((train_loss_disc, train_loss_gen))

        # Sample from generator as we're training for viegenerator_inputs_zwing afterwards
        gen_sample_z = np.random.uniform(-1, 1, size=(16, gen_z_size))
        generator_samples = sess.run(
                       generator(generator_input_z, input_img_size, reuse_vars=True),
                       feed_dict={generator_input_z: gen_sample_z})

        generated_samples.append(generator_samples)
        saver.save(sess, './checkpoints/generator_ck.ckpt')

# Save training generator samples
with open('train_generator_samples.pkl', 'wb') as f:
    pkl.dump(generated_samples, f)
Output:
.
.
.
Epoch 71/100... Disc Loss: 1.078... Gen Loss: 1.361
Epoch 72/100... Disc Loss: 1.037... Gen Loss: 1.555
Epoch 73/100... Disc Loss: 1.194... Gen Loss: 1.297
Epoch 74/100... Disc Loss: 1.120... Gen Loss: 1.730
Epoch 75/100... Disc Loss: 1.184... Gen Loss: 1.425
Epoch 76/100... Disc Loss: 1.054... Gen Loss: 1.534
Epoch 77/100... Disc Loss: 1.457... Gen Loss: 0.971
Epoch 78/100... Disc Loss: 0.973... Gen Loss: 1.688
Epoch 79/100... Disc Loss: 1.324... Gen Loss: 1.370
Epoch 80/100... Disc Loss: 1.178... Gen Loss: 1.710
Epoch 81/100... Disc Loss: 1.070... Gen Loss: 1.649
Epoch 82/100... Disc Loss: 1.070... Gen Loss: 1.530
Epoch 83/100... Disc Loss: 1.117... Gen Loss: 1.705
Epoch 84/100... Disc Loss: 1.042... Gen Loss: 2.210
Epoch 85/100... Disc Loss: 1.152... Gen Loss: 1.260
Epoch 86/100... Disc Loss: 1.327... Gen Loss: 1.312
Epoch 87/100... Disc Loss: 1.069... Gen Loss: 1.759
Epoch 88/100... Disc Loss: 1.001... Gen Loss: 1.400
Epoch 89/100... Disc Loss: 1.215... Gen Loss: 1.448
Epoch 90/100... Disc Loss: 1.108... Gen Loss: 1.342
Epoch 91/100... Disc Loss: 1.227... Gen Loss: 1.468
Epoch 92/100... Disc Loss: 1.190... Gen Loss: 1.328
Epoch 93/100... Disc Loss: 0.869... Gen Loss: 1.857
Epoch 94/100... Disc Loss: 0.946... Gen Loss: 1.740
Epoch 95/100... Disc Loss: 0.925... Gen Loss: 1.708
Epoch 96/100... Disc Loss: 1.067... Gen Loss: 1.427
Epoch 97/100... Disc Loss: 1.099... Gen Loss: 1.573
Epoch 98/100... Disc Loss: 0.972... Gen Loss: 1.884
Epoch 99/100... Disc Loss: 1.292... Gen Loss: 1.610
Epoch 100/100... Disc Loss: 1.103... Gen Loss: 1.736

在运行模型 100 个周期后,我们得到了一个训练好的模型,它能够生成与我们输入给鉴别器的原始图像相似的图像:

fig, ax = plt.subplots()
model_losses = np.array(model_losses)
plt.plot(model_losses.T[0], label='Disc loss')
plt.plot(model_losses.T[1], label='Gen loss')
plt.title("Model Losses")
plt.legend()

输出:

图 6:鉴别器和生成器的损失

如前图所示,可以看到模型的损失(由鉴别器和生成器的曲线表示)正在收敛。

生成器从训练中获得的样本

让我们测试一下模型的表现,甚至看看生成器在接近训练结束时,生成技能(为活动设计票据)是如何增强的:

def view_generated_samples(epoch_num, g_samples):
    fig, axes = plt.subplots(figsize=(7,7), nrows=4, ncols=4, sharey=True, sharex=True)

    print(gen_samples[epoch_num][1].shape)

    for ax, gen_image in zip(axes.flatten(), g_samples[0][epoch_num]):
        ax.xaxis.set_visible(False)
        ax.yaxis.set_visible(False)
        img = ax.imshow(gen_image.reshape((28,28)), cmap='Greys_r')

    return fig, axes

在绘制训练过程中最后一个周期生成的一些图像之前,我们需要加载包含每个周期生成样本的持久化文件:

# Load samples from generator taken while training
with open('train_generator_samples.pkl', 'rb') as f:
    gen_samples = pkl.load(f)

现在,让我们绘制出训练过程中最后一个周期生成的 16 张图像,看看生成器是如何生成有意义的数字(如 3、7 和 2)的:

_ = view_generated_samples(-1, gen_samples)

图 7:最终训练周期的样本

我们甚至可以看到生成器在不同周期中的设计能力。所以,让我们可视化它在每 10 个周期生成的图像:

rows, cols = 10, 6
fig, axes = plt.subplots(figsize=(7,12), nrows=rows, ncols=cols, sharex=True, sharey=True)

for gen_sample, ax_row in zip(gen_samples[::int(len(gen_samples)/rows)], axes):
    for image, ax in zip(gen_sample[::int(len(gen_sample)/cols)], ax_row):
        ax.imshow(image.reshape((28,28)), cmap='Greys_r')
        ax.xaxis.set_visible(False)
        ax.yaxis.set_visible(False)

图 8:网络训练过程中每 10 个周期生成的图像

正如你所看到的,生成器的设计能力以及其生成假图像的能力在最开始时非常有限,之后随着训练过程的进行有所增强。

从生成器采样

在上一节中,我们展示了一些在这个 GAN 架构训练过程中生成的示例图像。我们还可以通过加载我们保存的检查点,并给生成器提供一个新的潜在空间,让它用来生成全新的图像:

# Sampling from the generator
saver = tf.train.Saver(var_list=g_vars)

with tf.Session() as sess:

    #restoring the saved checkpints
    saver.restore(sess, tf.train.latest_checkpoint('checkpoints'))
    gen_sample_z = np.random.uniform(-1, 1, size=(16, z_size))
    generated_samples = sess.run(
                   generator(generator_input_z, input_img_size, reuse_vars=True),
                   feed_dict={generator_input_z: gen_sample_z})
view_generated_samples(0, [generated_samples])

图 9:来自生成器的样本

在实现这个例子时,你可以得出一些观察结果。在训练过程的最初几个周期里,生成器没有能力生成与真实图像相似的图像,因为它不知道这些图像是什么样子的。即使是判别器也不知道如何区分由生成器生成的假图像和真实图像。在训练开始时,会出现两种有趣的情况。首先,生成器不知道如何创建像我们最初输入到网络中的真实图像一样的图像。第二,判别器不知道真实图像和假图像之间的区别。

随着训练的进行,生成器开始生成一定程度上有意义的图像,这是因为生成器会学习到原始输入图像的来源数据分布。与此同时,判别器将能够区分真假图像,最终在训练结束时被生成器所欺骗。

摘要

现在,GANs 已经被应用于许多有趣的领域。GANs 可以用于不同的设置,例如半监督学习和无监督学习任务。此外,由于大量研究人员正在致力于 GANs,这些模型日益进步,生成图像或视频的能力越来越强。

这些模型可以用于许多有趣的商业应用,比如为 Photoshop 添加一个插件,可以接受像让我的笑容更迷人这样的命令。它们还可以用于图像去噪。

第十五章:人脸生成与处理缺失标签

我们可以使用 GAN 的有趣应用无穷无尽。在本章中,我们将演示 GAN 的另一个有前途的应用——基于 CelebA 数据库的人脸生成。我们还将演示如何在半监督学习设置中使用 GAN,其中我们有一个标签不全的数据集。

本章将涵盖以下主题:

  • 人脸生成

  • 使用生成对抗网络(GAN)的半监督学习

人脸生成

如我们在上一章所提到的,生成器和判别器由反卷积网络DNNwww.quora.com/How-does-a-deconvolutional-neural-network-work)和卷积神经网络CNNcs231n.github.io/convolutional-networks/)组成:

  • CNN 是一种神经网络,它将图像的数百个像素编码成一个小维度的向量(z),该向量是图像的摘要。

  • DNN 是一种网络,它学习一些滤波器,以从 z 中恢复原始图像。

此外,判别器会输出 1 或 0,表示输入的图像是来自真实数据集,还是由生成器生成。另一方面,生成器会尝试根据潜在空间 z 复制与原始数据集相似的图像,这些图像可能遵循高斯分布。因此,判别器的目标是正确区分真实图像,而生成器的目标是学习原始数据集的分布,从而欺骗判别器,使其做出错误的决策。

在本节中,我们将尝试教导生成器学习人脸图像的分布,以便它能够生成逼真的人脸。

生成类人面孔对于大多数图形公司来说至关重要,这些公司总是在为其应用程序寻找新的面孔,这也让我们看到人工智能在生成逼真人脸方面接近现实的程度。

在本示例中,我们将使用 CelebA 数据集。CelebFaces 属性数据集(CelebA)是一个大规模的面部属性数据集,包含约 20 万张名人图像,每张图像有 40 个属性标注。数据集涵盖了大量的姿势变化,以及背景杂乱,因此 CelebA 非常多样且注释完备。它包括:

  • 10,177 个身份

  • 202,599 张人脸图像

  • 每张图像有五个地标位置和 40 个二元属性注释

我们可以将此数据集用于许多计算机视觉应用,除了人脸生成,还可以用于人脸识别、定位或人脸属性检测。

该图展示了生成器误差,或者说学习人脸分布,在训练过程中如何逐渐接近现实:

图 1:使用 GAN 从名人图像数据集中生成新面孔

获取数据

在这一部分,我们将定义一些辅助函数,帮助我们下载 CelebA 数据集。我们将通过导入实现所需的包开始:

import math
import os
import hashlib
from urllib.request import urlretrieve
import zipfile
import gzip
import shutil

import numpy as np
from PIL import Image
from tqdm import tqdm
import utils

import tensorflow as tf

接下来,我们将使用 utils 脚本下载数据集:

#Downloading celebA dataset
celebA_data_dir = 'input'
utils.download_extract('celeba', celebA_data_dir)
Output:

Downloading celeba: 1.44GB [00:21, 66.6MB/s] 
Extracting celeba...

探索数据

CelebA 数据集包含超过 20 万张带注释的名人图像。由于我们将使用 GAN 生成类似的图像,因此值得看一些来自数据集的图像,看看它们的效果。在这一部分,我们将定义一些辅助函数,用于可视化 CelebA 数据集中的一组图像。

现在,让我们使用utils脚本从数据集中显示一些图像:

#number of images to display
num_images_to_show = 25

celebA_images = utils.get_batch(glob(os.path.join(celebA_data_dir, 'img_align_celeba/*.jpg'))[:num_images_to_show], 28,
                                28, 'RGB')
pyplot.imshow(utils.images_square_grid(celebA_images, 'RGB'))
Output:

图 2:从 CelebA 数据集中绘制一组图像

这个计算机视觉任务的主要目标是使用 GAN 生成类似于名人数据集中图像的图像,因此我们需要专注于图像的面部部分。为了聚焦于图像的面部部分,我们将去除不包含名人面孔的部分。

构建模型

现在,让我们开始构建我们实现的核心——计算图;它主要包括以下组件:

  • 模型输入

  • 判别器

  • 生成器

  • 模型损失

  • 模型优化器

  • 训练模型

模型输入

在这一部分,我们将实现一个辅助函数,定义模型的输入占位符,这些占位符将负责将数据输入到计算图中。

这些函数应该能够创建三个主要的占位符:

  • 来自数据集的实际输入图像,尺寸为(批量大小,输入图像宽度,输入图像高度,通道数)

  • 潜在空间 Z,将被生成器用来生成假图像

  • 学习率占位符

辅助函数将返回这三个输入占位符的元组。现在,让我们定义这个函数:

# defining the model inputs
def inputs(img_width, img_height, img_channels, latent_space_z_dim):
    true_inputs = tf.placeholder(tf.float32, (None, img_width, img_height, img_channels),
                                 'true_inputs')
    l_space_inputs = tf.placeholder(tf.float32, (None, latent_space_z_dim), 'l_space_inputs')
    model_learning_rate = tf.placeholder(tf.float32, name='model_learning_rate')

    return true_inputs, l_space_inputs, model_learning_rate

判别器

接下来,我们需要实现网络的判别器部分,用于判断输入是来自真实数据集还是由生成器生成的。我们将再次使用 TensorFlow 的tf.variable_scope功能为一些变量添加前缀“判别器”,以便我们能够检索和重用它们。

那么,让我们定义一个函数,返回判别器的二进制输出以及 logit 值:

# Defining the discriminator function
def discriminator(input_imgs, reuse=False):
    # using variable_scope to reuse variables
    with tf.variable_scope('discriminator', reuse=reuse):
        # leaky relu parameter
        leaky_param_alpha = 0.2

        # defining the layers
        conv_layer_1 = tf.layers.conv2d(input_imgs, 64, 5, 2, 'same')
        leaky_relu_output = tf.maximum(leaky_param_alpha * conv_layer_1, conv_layer_1)

        conv_layer_2 = tf.layers.conv2d(leaky_relu_output, 128, 5, 2, 'same')
        normalized_output = tf.layers.batch_normalization(conv_layer_2, training=True)
        leay_relu_output = tf.maximum(leaky_param_alpha * normalized_output, normalized_output)

        conv_layer_3 = tf.layers.conv2d(leay_relu_output, 256, 5, 2, 'same')
        normalized_output = tf.layers.batch_normalization(conv_layer_3, training=True)
        leaky_relu_output = tf.maximum(leaky_param_alpha * normalized_output, normalized_output)

        # reshaping the output for the logits to be 2D tensor
        flattened_output = tf.reshape(leaky_relu_output, (-1, 4 * 4 * 256))
        logits_layer = tf.layers.dense(flattened_output, 1)
        output = tf.sigmoid(logits_layer)

    return output, logits_layer

生成器

现在,轮到实现网络的第二部分,它将尝试使用潜在空间z来复制原始输入图像。我们也将使用tf.variable_scope来实现这个功能。

那么,让我们定义一个函数,返回生成器生成的图像:

def generator(z_latent_space, output_channel_dim, is_train=True):

    with tf.variable_scope('generator', reuse=not is_train):

        #leaky relu parameter
        leaky_param_alpha = 0.2

        fully_connected_layer = tf.layers.dense(z_latent_space, 2*2*512)

        #reshaping the output back to 4D tensor to match the accepted format for convolution layer
        reshaped_output = tf.reshape(fully_connected_layer, (-1, 2, 2, 512))
        normalized_output = tf.layers.batch_normalization(reshaped_output, training=is_train)
        leaky_relu_output = tf.maximum(leaky_param_alpha * normalized_output, normalized_output)

        conv_layer_1 = tf.layers.conv2d_transpose(leaky_relu_output, 256, 5, 2, 'valid')
        normalized_output = tf.layers.batch_normalization(conv_layer_1, training=is_train)
        leaky_relu_output = tf.maximum(leaky_param_alpha * normalized_output, normalized_output)

        conv_layer_2 = tf.layers.conv2d_transpose(leaky_relu_output, 128, 5, 2, 'same')
        normalized_output = tf.layers.batch_normalization(conv_layer_2, training=is_train)
        leaky_relu_output = tf.maximum(leaky_param_alpha * normalized_output, normalized_output)

        logits_layer = tf.layers.conv2d_transpose(leaky_relu_output, output_channel_dim, 5, 2, 'same')
        output = tf.tanh(logits_layer)

        return output

模型损失

接下来是比较棘手的部分,我们在前一章中讲过,即计算判别器和生成器的损失。

所以,让我们定义这个函数,它将使用之前定义的generatordiscriminator函数:

# Define the error for the discriminator and generator
def model_losses(input_actual, input_latent_z, out_channel_dim):
    # building the generator part
    gen_model = generator(input_latent_z, out_channel_dim)
    disc_model_true, disc_logits_true = discriminator(input_actual)
    disc_model_fake, disc_logits_fake = discriminator(gen_model, reuse=True)

    disc_loss_true = tf.reduce_mean(
        tf.nn.sigmoid_cross_entropy_with_logits(logits=disc_logits_true, labels=tf.ones_like(disc_model_true)))

    disc_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(
        logits=disc_logits_fake, labels=tf.zeros_like(disc_model_fake)))

    gen_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(
        logits=disc_logits_fake, labels=tf.ones_like(disc_model_fake)))

    disc_loss = disc_loss_true + disc_loss_fake

    return disc_loss, gen_loss

模型优化器

最后,在训练我们的模型之前,我们需要实现该任务的优化标准。我们将使用之前使用的命名约定来检索判别器和生成器的可训练参数并训练它们:

# specifying the optimization criteria
def model_optimizer(disc_loss, gen_loss, learning_rate, beta1):
    trainable_vars = tf.trainable_variables()
    disc_vars = [var for var in trainable_vars if var.name.startswith('discriminator')]
    gen_vars = [var for var in trainable_vars if var.name.startswith('generator')]

    disc_train_opt = tf.train.AdamOptimizer(
        learning_rate, beta1=beta1).minimize(disc_loss, var_list=disc_vars)

    update_operations = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
    gen_updates = [opt for opt in update_operations if opt.name.startswith('generator')]

    with tf.control_dependencies(gen_updates):
        gen_train_opt = tf.train.AdamOptimizer(
            learning_rate, beta1).minimize(gen_loss, var_list=gen_vars)

    return disc_train_opt, gen_train_opt

训练模型

现在,是时候训练模型并观察生成器如何在一定程度上欺骗判别器,通过生成与原始 CelebA 数据集非常接近的图像。

但首先,让我们定义一个辅助函数,它将展示生成器生成的一些图像:

# define a function to visualize some generated images from the generator
def show_generator_output(sess, num_images, input_latent_z, output_channel_dim, img_mode):
    cmap = None if img_mode == 'RGB' else 'gray'
    latent_space_z_dim = input_latent_z.get_shape().as_list()[-1]
    examples_z = np.random.uniform(-1, 1, size=[num_images, latent_space_z_dim])

    examples = sess.run(
        generator(input_latent_z, output_channel_dim, False),
        feed_dict={input_latent_z: examples_z})

    images_grid = utils.images_square_grid(examples, img_mode)
    pyplot.imshow(images_grid, cmap=cmap)
    pyplot.show()

然后,我们将使用之前定义的辅助函数来构建模型输入、损失和优化标准。我们将它们堆叠在一起,并开始基于 CelebA 数据集训练我们的模型:

def model_train(num_epocs, train_batch_size, z_dim, learning_rate, beta1, get_batches, input_data_shape, data_img_mode):
    _, image_width, image_height, image_channels = input_data_shape

    actual_input, z_input, leaningRate = inputs(
        image_width, image_height, image_channels, z_dim)

    disc_loss, gen_loss = model_losses(actual_input, z_input, image_channels)

    disc_opt, gen_opt = model_optimizer(disc_loss, gen_loss, learning_rate, beta1)

    steps = 0
    print_every = 50
    show_every = 100
    model_loss = []
    num_images = 25

    with tf.Session() as sess:

        # initializing all the variables
        sess.run(tf.global_variables_initializer())

        for epoch_i in range(num_epocs):
            for batch_images in get_batches(train_batch_size):

                steps += 1
                batch_images *= 2.0
                z_sample = np.random.uniform(-1, 1, (train_batch_size, z_dim))

                _ = sess.run(disc_opt, feed_dict={
                    actual_input: batch_images, z_input: z_sample, leaningRate: learning_rate})
                _ = sess.run(gen_opt, feed_dict={
                    z_input: z_sample, leaningRate: learning_rate})

                if steps % print_every == 0:
                    train_loss_disc = disc_loss.eval({z_input: z_sample, actual_input: batch_images})
                    train_loss_gen = gen_loss.eval({z_input: z_sample})

                    print("Epoch {}/{}...".format(epoch_i + 1, num_epocs),
                          "Discriminator Loss: {:.4f}...".format(train_loss_disc),
                          "Generator Loss: {:.4f}".format(train_loss_gen))
                    model_loss.append((train_loss_disc, train_loss_gen))

                if steps % show_every == 0:
                    show_generator_output(sess, num_images, z_input, image_channels, data_img_mode)

启动训练过程,这可能会根据你的主机机器规格需要一些时间:

# Training the model on CelebA dataset
train_batch_size = 64
z_dim = 100
learning_rate = 0.002
beta1 = 0.5

num_epochs = 1

celeba_dataset = utils.Dataset('celeba', glob(os.path.join(data_dir, 'img_align_celeba/*.jpg')))
with tf.Graph().as_default():
    model_train(num_epochs, train_batch_size, z_dim, learning_rate, beta1, celeba_dataset.get_batches,
                celeba_dataset.shape, celeba_dataset.image_mode)

输出:


 Epoch 1/1... Discriminator Loss: 0.9118... Generator Loss: 12.2238
 Epoch 1/1... Discriminator Loss: 0.6119... Generator Loss: 3.2168
 Epoch 1/1... Discriminator Loss: 0.5383... Generator Loss: 2.8054
 Epoch 1/1... Discriminator Loss: 1.4381... Generator Loss: 0.4672
 Epoch 1/1... Discriminator Loss: 0.7815... Generator Loss: 14.8220
 Epoch 1/1... Discriminator Loss: 0.6435... Generator Loss: 9.2591
 Epoch 1/1... Discriminator Loss: 1.5661... Generator Loss: 10.4747
 Epoch 1/1... Discriminator Loss: 1.5407... Generator Loss: 0.5811
 Epoch 1/1... Discriminator Loss: 0.6470... Generator Loss: 2.9002
 Epoch 1/1... Discriminator Loss: 0.5671... Generator Loss: 2.0700

图 3:此时训练的生成输出样本

Epoch 1/1... Discriminator Loss: 0.7950... Generator Loss: 1.5818
Epoch 1/1... Discriminator Loss: 1.2417... Generator Loss: 0.7094
Epoch 1/1... Discriminator Loss: 1.1786... Generator Loss: 1.0948
Epoch 1/1... Discriminator Loss: 1.0427... Generator Loss: 2.8878
Epoch 1/1... Discriminator Loss: 0.8409... Generator Loss: 2.6785
Epoch 1/1... Discriminator Loss: 0.8557... Generator Loss: 1.7706
Epoch 1/1... Discriminator Loss: 0.8241... Generator Loss: 1.2898
Epoch 1/1... Discriminator Loss: 0.8590... Generator Loss: 1.8217
Epoch 1/1... Discriminator Loss: 1.1694... Generator Loss: 0.8490
Epoch 1/1... Discriminator Loss: 0.9984... Generator Loss: 1.0042

图 4:此时训练的生成输出样本

经过一段时间的训练后,你应该会得到类似这样的结果:

图 5:此时训练的生成输出样本

使用生成对抗网络(GAN)进行半监督学习

鉴于此,半监督学习是一种技术,其中使用标注数据和未标注数据来训练分类器。

这种类型的分类器采用一小部分标注数据和大量未标注数据(来自同一领域)。目标是将这些数据源结合起来,训练一个深度卷积神经网络DCNN),以学习一个推断函数,能够将新的数据点映射到其期望的结果。

在这个领域,我们提出了一种 GAN 模型,用于使用非常小的标注训练集对街景房号进行分类。实际上,该模型使用了原始 SVHN 训练标签的大约 1.3%,即 1000 个标注样本。我们使用了在论文Improved Techniques for Training GANs from OpenAI中描述的一些技术(arxiv.org/abs/1606.03498)。

直觉

在构建生成图像的 GAN 时,我们同时训练生成器和判别器。训练后,我们可以丢弃判别器,因为我们只在训练生成器时使用了它。

图 6:用于 11 类分类问题的半监督学习 GAN 架构

在半监督学习中,我们需要将判别器转变为一个多类分类器。这个新模型必须能够在测试集上很好地泛化,尽管我们没有很多带标签的训练样本。此外,这一次,训练结束时,我们实际上可以抛弃生成器。请注意,角色发生了变化。现在,生成器仅用于在训练过程中帮助判别器。换句话说,生成器充当了一个不同的信息来源,判别器从中获取未经标签的原始训练数据。正如我们将看到的,这些未经标签的数据对于提高判别器的性能至关重要。此外,对于一个常规的图像生成 GAN,判别器只有一个角色:计算其输入是否真实的概率——我们将其称为 GAN 问题。

然而,要将判别器转变为一个半监督分类器,除了 GAN 问题,判别器还必须学习原始数据集每个类别的概率。换句话说,对于每个输入图像,判别器必须学习它属于某个类别(如 1、2、3 等)的概率。

回顾一下,对于图像生成 GAN 的判别器,我们有一个单一的 sigmoid 单元输出。这个值代表了输入图像是真实的概率(接近 1),还是假的概率(接近 0)。换句话说,从判别器的角度来看,接近 1 的值意味着样本很可能来自训练集。同样,接近 0 的值则意味着样本更有可能来自生成器网络。通过使用这个概率,判别器能够将信号传回生成器。这个信号使生成器能够在训练过程中调整其参数,从而有可能提高生成真实图像的能力。

我们必须将判别器(来自之前的 GAN)转换为一个 11 类分类器。为此,我们可以将其 sigmoid 输出转换为具有 11 个类别输出的 softmax,前 10 个输出表示 SVHN 数据集各个类别的概率(0 至 9),第 11 类则表示所有来自生成器的假图像。

请注意,如果我们将第 11 类的概率设为 0,那么前 10 个概率的总和就等于使用 sigmoid 函数计算的相同概率。

最后,我们需要设置损失函数,使得判别器能够同时完成两项任务:

  • 帮助生成器学习生成真实图像。为了做到这一点,我们必须指示判别器区分真实样本和假样本。

  • 使用生成器的图像以及带标签和不带标签的训练数据,帮助分类数据集。

总结一下,判别器有三种不同的训练数据来源:

  • 带标签的真实图像。这些是像任何常规监督分类问题一样的图像标签对。

  • 没有标签的真实图像。对于这些图像,分类器只能学习到这些图像是“真实的”。

  • 来自生成器的图像。为了使用这些图像,判别器学习将其分类为假。

这些不同数据源的结合将使分类器能够从更广泛的角度学习。反过来,这使得模型的推理性能比仅使用 1,000 个标注样本进行训练时更为精准。

数据分析和预处理

对于这个任务,我们将使用 SVHN 数据集,它是斯坦福大学的街景房屋号码(Street View House Numbers)数据集的缩写(ufldl.stanford.edu/housenumbers/)。所以,让我们通过导入实现所需的包开始实现:

# Lets start by loading the necessary libraries
%matplotlib inline

import pickle as pkl
import time
import matplotlib.pyplot as plt
import numpy as np
from scipy.io import loadmat
import tensorflow as tf
import os

接下来,我们将定义一个辅助类来下载 SVHN 数据集(记得你需要首先手动创建input_data_dir目录):

from urllib.request import urlretrieve
from os.path import isfile, isdir
from tqdm import tqdm
input_data_dir = 'input/'

input_data_dir = 'input/'

if not isdir(input_data_dir):
    raise Exception("Data directory doesn't exist!")

class DLProgress(tqdm):
    last_block = 0

    def hook(self, block_num=1, block_size=1, total_size=None):
        self.total = total_size
        self.update((block_num - self.last_block) * block_size)
        self.last_block = block_num

if not isfile(input_data_dir + "train_32x32.mat"):
    with DLProgress(unit='B', unit_scale=True, miniters=1, desc='SVHN Training Set') as pbar:
        urlretrieve(
            'http://ufldl.stanford.edu/housenumbers/train_32x32.mat',
            input_data_dir + 'train_32x32.mat',
            pbar.hook)

if not isfile(input_data_dir + "test_32x32.mat"):
    with DLProgress(unit='B', unit_scale=True, miniters=1, desc='SVHN Training Set') as pbar:
        urlretrieve(
            'http://ufldl.stanford.edu/housenumbers/test_32x32.mat',
            input_data_dir + 'test_32x32.mat',
            pbar.hook)

train_data = loadmat(input_data_dir + 'train_32x32.mat')
test_data = loadmat(input_data_dir + 'test_32x32.mat')
Output:
trainset shape: (32, 32, 3, 73257)
testset shape: (32, 32, 3, 26032)

让我们了解一下这些图像的样子:

indices = np.random.randint(0, train_data['X'].shape[3], size=36)
fig, axes = plt.subplots(6, 6, sharex=True, sharey=True, figsize=(5,5),)
for ii, ax in zip(indices, axes.flatten()):
    ax.imshow(train_data['X'][:,:,:,ii], aspect='equal')
    ax.xaxis.set_visible(False)
    ax.yaxis.set_visible(False)
plt.subplots_adjust(wspace=0, hspace=0)
Output:

图 7:来自 SVHN 数据集的样本图像。

接下来,我们需要将图像缩放到-1 到 1 之间,这对于使用tanh()函数是必要的,因为该函数将压缩生成器输出的值:

# Scaling the input images
def scale_images(image, feature_range=(-1, 1)):
    # scale image to (0, 1)
    image = ((image - image.min()) / (255 - image.min()))

    # scale the image to feature range
    min, max = feature_range
    image = image * (max - min) + min
    return image
class Dataset:
    def __init__(self, train_set, test_set, validation_frac=0.5, shuffle_data=True, scale_func=None):
        split_ind = int(len(test_set['y']) * (1 - validation_frac))
        self.test_input, self.valid_input = test_set['X'][:, :, :, :split_ind], test_set['X'][:, :, :, split_ind:]
        self.test_target, self.valid_target = test_set['y'][:split_ind], test_set['y'][split_ind:]
        self.train_input, self.train_target = train_set['X'], train_set['y']

        # The street house number dataset comes with lots of labels,
        # but because we are going to do semi-supervised learning we are going to assume that we don't have all labels
        # like, assume that we have only 1000
        self.label_mask = np.zeros_like(self.train_target)
        self.label_mask[0:1000] = 1

        self.train_input = np.rollaxis(self.train_input, 3)
        self.valid_input = np.rollaxis(self.valid_input, 3)
        self.test_input = np.rollaxis(self.test_input, 3)

        if scale_func is None:
            self.scaler = scale_images
        else:
            self.scaler = scale_func
        self.train_input = self.scaler(self.train_input)
        self.valid_input = self.scaler(self.valid_input)
        self.test_input = self.scaler(self.test_input)
        self.shuffle = shuffle_data

    def batches(self, batch_size, which_set="train"):
        input_name = which_set + "_input"
        target_name = which_set + "_target"

        num_samples = len(getattr(dataset, target_name))
        if self.shuffle:
            indices = np.arange(num_samples)
            np.random.shuffle(indices)
            setattr(dataset, input_name, getattr(dataset, input_name)[indices])
            setattr(dataset, target_name, getattr(dataset, target_name)[indices])
            if which_set == "train":
                dataset.label_mask = dataset.label_mask[indices]

        dataset_input = getattr(dataset, input_name)
        dataset_target = getattr(dataset, target_name)

        for jj in range(0, num_samples, batch_size):
            input_vals = dataset_input[jj:jj + batch_size]
            target_vals = dataset_target[jj:jj + batch_size]

            if which_set == "train":
                # including the label mask in case of training
                # to pretend that we don't have all the labels
                yield input_vals, target_vals, self.label_mask[jj:jj + batch_size]
            else:
                yield input_vals, target_vals

构建模型

在本节中,我们将构建所有必要的组件以进行测试,因此我们首先定义将用于向计算图输入数据的输入。

模型输入

首先,我们将定义模型输入函数,该函数将创建用于输入数据的模型占位符:

# defining the model inputs
def inputs(actual_dim, z_dim):
    inputs_actual = tf.placeholder(tf.float32, (None, *actual_dim), name='input_actual')
    inputs_latent_z = tf.placeholder(tf.float32, (None, z_dim), name='input_latent_z')

    target = tf.placeholder(tf.int32, (None), name='target')
    label_mask = tf.placeholder(tf.int32, (None), name='label_mask')

    return inputs_actual, inputs_latent_z, target, label_mask

生成器

在本节中,我们将实现 GAN 网络的第一个核心部分。该部分的架构和实现将遵循原始的 DCGAN 论文:

def generator(latent_z, output_image_dim, reuse_vars=False, leaky_alpha=0.2, is_training=True, size_mult=128):
    with tf.variable_scope('generator', reuse=reuse_vars):
        # define a fully connected layer
        fully_conntected_1 = tf.layers.dense(latent_z, 4 * 4 * size_mult * 4)

        # Reshape it from 2D tensor to 4D tensor to be fed to the convolution neural network
        reshaped_out_1 = tf.reshape(fully_conntected_1, (-1, 4, 4, size_mult * 4))
        batch_normalization_1 = tf.layers.batch_normalization(reshaped_out_1, training=is_training)
        leaky_output_1 = tf.maximum(leaky_alpha * batch_normalization_1, batch_normalization_1)

        conv_layer_1 = tf.layers.conv2d_transpose(leaky_output_1, size_mult * 2, 5, strides=2, padding='same')
        batch_normalization_2 = tf.layers.batch_normalization(conv_layer_1, training=is_training)
        leaky_output_2 = tf.maximum(leaky_alpha * batch_normalization_2, batch_normalization_2)

        conv_layer_2 = tf.layers.conv2d_transpose(leaky_output_2, size_mult, 5, strides=2, padding='same')
        batch_normalization_3 = tf.layers.batch_normalization(conv_layer_2, training=is_training)
        leaky_output_3 = tf.maximum(leaky_alpha * batch_normalization_3, batch_normalization_3)

        # defining the output layer
        logits_layer = tf.layers.conv2d_transpose(leaky_output_3, output_image_dim, 5, strides=2, padding='same')

        output = tf.tanh(logits_layer)

        return output

判别器

现在,是时候构建 GAN 网络的第二个核心部分——判别器了。在之前的实现中,我们提到判别器会产生一个二元输出,表示输入图像是否来自真实数据集(1)还是由生成器生成(0)。在这里,情况有所不同,因此判别器将变为一个多类别分类器。

现在,让我们继续构建架构中的判别器部分:

# Defining the discriminator part of the network
def discriminator(input_x, reuse_vars=False, leaky_alpha=0.2, drop_out_rate=0., num_classes=10, size_mult=64):
    with tf.variable_scope('discriminator', reuse=reuse_vars):

        # defining a dropout layer
        drop_out_output = tf.layers.dropout(input_x, rate=drop_out_rate / 2.5)

        # Defining the input layer for the discriminator which is 32x32x3
        conv_layer_3 = tf.layers.conv2d(input_x, size_mult, 3, strides=2, padding='same')
        leaky_output_4 = tf.maximum(leaky_alpha * conv_layer_3, conv_layer_3)
        leaky_output_4 = tf.layers.dropout(leaky_output_4, rate=drop_out_rate)

        conv_layer_4 = tf.layers.conv2d(leaky_output_4, size_mult, 3, strides=2, padding='same')
        batch_normalization_4 = tf.layers.batch_normalization(conv_layer_4, training=True)
        leaky_output_5 = tf.maximum(leaky_alpha * batch_normalization_4, batch_normalization_4)

        conv_layer_5 = tf.layers.conv2d(leaky_output_5, size_mult, 3, strides=2, padding='same')
        batch_normalization_5 = tf.layers.batch_normalization(conv_layer_5, training=True)
        leaky_output_6 = tf.maximum(leaky_alpha * batch_normalization_5, batch_normalization_5)
        leaky_output_6 = tf.layers.dropout(leaky_output_6, rate=drop_out_rate)

        conv_layer_6 = tf.layers.conv2d(leaky_output_6, 2 * size_mult, 3, strides=1, padding='same')
        batch_normalization_6 = tf.layers.batch_normalization(conv_layer_6, training=True)
        leaky_output_7 = tf.maximum(leaky_alpha * batch_normalization_6, batch_normalization_6)

        conv_layer_7 = tf.layers.conv2d(leaky_output_7, 2 * size_mult, 3, strides=1, padding='same')
        batch_normalization_7 = tf.layers.batch_normalization(conv_layer_7, training=True)
        leaky_output_8 = tf.maximum(leaky_alpha * batch_normalization_7, batch_normalization_7)

        conv_layer_8 = tf.layers.conv2d(leaky_output_8, 2 * size_mult, 3, strides=2, padding='same')
        batch_normalization_8 = tf.layers.batch_normalization(conv_layer_8, training=True)
        leaky_output_9 = tf.maximum(leaky_alpha * batch_normalization_8, batch_normalization_8)
        leaky_output_9 = tf.layers.dropout(leaky_output_9, rate=drop_out_rate)

        conv_layer_9 = tf.layers.conv2d(leaky_output_9, 2 * size_mult, 3, strides=1, padding='valid')

        leaky_output_10 = tf.maximum(leaky_alpha * conv_layer_9, conv_layer_9)
...

我们将不再在最后应用全连接层,而是执行所谓的全局平均池化GAP),该操作在特征向量的空间维度上取平均值;这将把张量压缩为一个单一的值:

...
# Flatten it by global average pooling
leaky_output_features = tf.reduce_mean(leaky_output_10, (1, 2))
...

例如,假设经过一系列卷积操作后,我们得到一个形状为的输出张量:

[BATCH_SIZE, 8, 8, NUM_CHANNELS] 

要应用全局平均池化,我们计算[8x8]张量片的平均值。该操作将产生一个形状如下的张量:

 [BATCH_SIZE, 1, 1, NUM_CHANNELS] 

这可以重塑为:

[BATCH_SIZE, NUM_CHANNELS].

在应用全局平均池化后,我们添加一个全连接层,该层将输出最终的 logits。这些 logits 的形状为:

[BATCH_SIZE, NUM_CLASSES]

这将表示每个类别的得分。为了获得这些类别的概率得分,我们将使用softmax激活函数:

...
# Get the probability that the input is real rather than fake
softmax_output = tf.nn.softmax(classes_logits)s
...

最终,判别器函数将如下所示:

# Defining the discriminator part of the network
def discriminator(input_x, reuse_vars=False, leaky_alpha=0.2, drop_out_rate=0., num_classes=10, size_mult=64):
    with tf.variable_scope('discriminator', reuse=reuse_vars):

        # defining a dropout layer
        drop_out_output = tf.layers.dropout(input_x, rate=drop_out_rate / 2.5)

        # Defining the input layer for the discrminator which is 32x32x3
        conv_layer_3 = tf.layers.conv2d(input_x, size_mult, 3, strides=2, padding='same')
        leaky_output_4 = tf.maximum(leaky_alpha * conv_layer_3, conv_layer_3)
        leaky_output_4 = tf.layers.dropout(leaky_output_4, rate=drop_out_rate)

        conv_layer_4 = tf.layers.conv2d(leaky_output_4, size_mult, 3, strides=2, padding='same')
        batch_normalization_4 = tf.layers.batch_normalization(conv_layer_4, training=True)
        leaky_output_5 = tf.maximum(leaky_alpha * batch_normalization_4, batch_normalization_4)

        conv_layer_5 = tf.layers.conv2d(leaky_output_5, size_mult, 3, strides=2, padding='same')
        batch_normalization_5 = tf.layers.batch_normalization(conv_layer_5, training=True)
        leaky_output_6 = tf.maximum(leaky_alpha * batch_normalization_5, batch_normalization_5)
        leaky_output_6 = tf.layers.dropout(leaky_output_6, rate=drop_out_rate)

        conv_layer_6 = tf.layers.conv2d(leaky_output_6, 2 * size_mult, 3, strides=1, padding='same')
        batch_normalization_6 = tf.layers.batch_normalization(conv_layer_6, training=True)
        leaky_output_7 = tf.maximum(leaky_alpha * batch_normalization_6, batch_normalization_6)

        conv_layer_7 = tf.layers.conv2d(leaky_output_7, 2 * size_mult, 3, strides=1, padding='same')
        batch_normalization_7 = tf.layers.batch_normalization(conv_layer_7, training=True)
        leaky_output_8 = tf.maximum(leaky_alpha * batch_normalization_7, batch_normalization_7)

        conv_layer_8 = tf.layers.conv2d(leaky_output_8, 2 * size_mult, 3, strides=2, padding='same')
        batch_normalization_8 = tf.layers.batch_normalization(conv_layer_8, training=True)
        leaky_output_9 = tf.maximum(leaky_alpha * batch_normalization_8, batch_normalization_8)
        leaky_output_9 = tf.layers.dropout(leaky_output_9, rate=drop_out_rate)

        conv_layer_9 = tf.layers.conv2d(leaky_output_9, 2 * size_mult, 3, strides=1, padding='valid')

        leaky_output_10 = tf.maximum(leaky_alpha * conv_layer_9, conv_layer_9)

        # Flatten it by global average pooling
        leaky_output_features = tf.reduce_mean(leaky_output_10, (1, 2))

        # Set class_logits to be the inputs to a softmax distribution over the different classes
        classes_logits = tf.layers.dense(leaky_output_features, num_classes + extra_class)

        if extra_class:
            actual_class_logits, fake_class_logits = tf.split(classes_logits, [num_classes, 1], 1)
            assert fake_class_logits.get_shape()[1] == 1, fake_class_logits.get_shape()
            fake_class_logits = tf.squeeze(fake_class_logits)
        else:
            actual_class_logits = classes_logits
            fake_class_logits = 0.

        max_reduced = tf.reduce_max(actual_class_logits, 1, keep_dims=True)
        stable_actual_class_logits = actual_class_logits - max_reduced

        gan_logits = tf.log(tf.reduce_sum(tf.exp(stable_actual_class_logits), 1)) + tf.squeeze(
            max_reduced) - fake_class_logits

        softmax_output = tf.nn.softmax(classes_logits)

        return softmax_output, classes_logits, gan_logits, leaky_output_features

模型损失

现在是时候定义模型的损失函数了。首先,判别器的损失将分为两部分:

  • 一个将表示 GAN 问题的部分,即无监督损失

  • 第二部分将计算每个实际类别的概率,这就是监督损失

对于判别器的无监督损失,它必须区分真实训练图像和生成器生成的图像。

和常规 GAN 一样,一半时间,判别器将从训练集获取未标记的图像作为输入,另一半时间,从生成器获取虚假未标记的图像。

对于判别器损失的第二部分,即监督损失,我们需要基于判别器的 logits 来构建。因此,我们将使用 softmax 交叉熵,因为这是一个多分类问题。

正如《增强训练 GAN 的技术》论文中提到的,我们应该使用特征匹配来计算生成器的损失。正如作者所描述的:

“特征匹配是通过惩罚训练数据集上一组特征的平均值与生成样本上该组特征的平均值之间的绝对误差来实现的。为此,我们从两个不同的来源提取一组统计数据(矩),并迫使它们相似。首先,我们取出从判别器中提取的特征的平均值,这些特征是在处理真实训练小批量数据时得到的。其次,我们以相同的方式计算矩,但这次是针对当判别器分析来自生成器的虚假图像小批量时的情况。最后,利用这两个矩的集合,生成器的损失是它们之间的平均绝对差。换句话说,正如论文强调的那样:我们训练生成器使其匹配判别器中间层特征的期望值。”

最终,模型的损失函数将如下所示:

def model_losses(input_actual, input_latent_z, output_dim, target, num_classes, label_mask, leaky_alpha=0.2,
                     drop_out_rate=0.):

        # These numbers multiply the size of each layer of the generator and the discriminator,
        # respectively. You can reduce them to run your code faster for debugging purposes.
        gen_size_mult = 32
        disc_size_mult = 64

        # Here we run the generator and the discriminator
        gen_model = generator(input_latent_z, output_dim, leaky_alpha=leaky_alpha, size_mult=gen_size_mult)
        disc_on_data = discriminator(input_actual, leaky_alpha=leaky_alpha, drop_out_rate=drop_out_rate,
                                     size_mult=disc_size_mult)
        disc_model_real, class_logits_on_data, gan_logits_on_data, data_features = disc_on_data
        disc_on_samples = discriminator(gen_model, reuse_vars=True, leaky_alpha=leaky_alpha,
                                        drop_out_rate=drop_out_rate, size_mult=disc_size_mult)
        disc_model_fake, class_logits_on_samples, gan_logits_on_samples, sample_features = disc_on_samples

        # Here we compute `disc_loss`, the loss for the discriminator.
        disc_loss_actual = tf.reduce_mean(
            tf.nn.sigmoid_cross_entropy_with_logits(logits=gan_logits_on_data,
                                                    labels=tf.ones_like(gan_logits_on_data)))
        disc_loss_fake = tf.reduce_mean(
            tf.nn.sigmoid_cross_entropy_with_logits(logits=gan_logits_on_samples,
                                                    labels=tf.zeros_like(gan_logits_on_samples)))
        target = tf.squeeze(target)
        classes_cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=class_logits_on_data,
                                                                        labels=tf.one_hot(target,
                                                                                          num_classes + extra_class,
                                                                                          dtype=tf.float32))
        classes_cross_entropy = tf.squeeze(classes_cross_entropy)
        label_m = tf.squeeze(tf.to_float(label_mask))
        disc_loss_class = tf.reduce_sum(label_m * classes_cross_entropy) / tf.maximum(1., tf.reduce_sum(label_m))
        disc_loss = disc_loss_class + disc_loss_actual + disc_loss_fake

        # Here we set `gen_loss` to the "feature matching" loss invented by Tim Salimans.
        sampleMoments = tf.reduce_mean(sample_features, axis=0)
        dataMoments = tf.reduce_mean(data_features, axis=0)

        gen_loss = tf.reduce_mean(tf.abs(dataMoments - sampleMoments))

        prediction_class = tf.cast(tf.argmax(class_logits_on_data, 1), tf.int32)
        check_prediction = tf.equal(tf.squeeze(target), prediction_class)
        correct = tf.reduce_sum(tf.to_float(check_prediction))
        masked_correct = tf.reduce_sum(label_m * tf.to_float(check_prediction))

        return disc_loss, gen_loss, correct, masked_correct, gen_model

模型优化器

现在,让我们定义模型优化器,它与我们之前定义的非常相似:

def model_optimizer(disc_loss, gen_loss, learning_rate, beta1):

        # Get weights and biases to update. Get them separately for the discriminator and the generator
        trainable_vars = tf.trainable_variables()
        disc_vars = [var for var in trainable_vars if var.name.startswith('discriminator')]
        gen_vars = [var for var in trainable_vars if var.name.startswith('generator')]
        for t in trainable_vars:
            assert t in disc_vars or t in gen_vars

        # Minimize both gen and disc costs simultaneously
        disc_train_optimizer = tf.train.AdamOptimizer(learning_rate, beta1=beta1).minimize(disc_loss,
                                                                                           var_list=disc_vars)
        gen_train_optimizer = tf.train.AdamOptimizer(learning_rate, beta1=beta1).minimize(gen_loss, var_list=gen_vars)
        shrink_learning_rate = tf.assign(learning_rate, learning_rate * 0.9)

        return disc_train_optimizer, gen_train_optimizer, shrink_learning_rate

模型训练

最后,在将所有内容组合在一起后,让我们开始训练过程:

class GAN:
        def __init__(self, real_size, z_size, learning_rate, num_classes=10, alpha=0.2, beta1=0.5):
            tf.reset_default_graph()

            self.learning_rate = tf.Variable(learning_rate, trainable=False)
            model_inputs = inputs(real_size, z_size)
            self.input_actual, self.input_latent_z, self.target, self.label_mask = model_inputs
            self.drop_out_rate = tf.placeholder_with_default(.5, (), "drop_out_rate")

            losses_results = model_losses(self.input_actual, self.input_latent_z,
                                          real_size[2], self.target, num_classes,
                                          label_mask=self.label_mask,
                                          leaky_alpha=0.2,
                                          drop_out_rate=self.drop_out_rate)
            self.disc_loss, self.gen_loss, self.correct, self.masked_correct, self.samples = losses_results

            self.disc_opt, self.gen_opt, self.shrink_learning_rate = model_optimizer(self.disc_loss, self.gen_loss,
                                                                                     self.learning_rate, beta1)
def view_generated_samples(epoch, samples, nrows, ncols, figsize=(5, 5)):
        fig, axes = plt.subplots(figsize=figsize, nrows=nrows, ncols=ncols,
                                 sharey=True, sharex=True)
        for ax, img in zip(axes.flatten(), samples[epoch]):
            ax.axis('off')
            img = ((img - img.min()) * 255 / (img.max() - img.min())).astype(np.uint8)
            ax.set_adjustable('box-forced')
            im = ax.imshow(img)

        plt.subplots_adjust(wspace=0, hspace=0)
        return fig, axes
def train(net, dataset, epochs, batch_size, figsize=(5, 5)):

        saver = tf.train.Saver()
        sample_z = np.random.normal(0, 1, size=(50, latent_space_z_size))

        samples, train_accuracies, test_accuracies = [], [], []
        steps = 0

        with tf.Session() as sess:
            sess.run(tf.global_variables_initializer())
            for e in range(epochs):
                print("Epoch", e)

                num_samples = 0
                num_correct_samples = 0
                for x, y, label_mask in dataset.batches(batch_size):
                    assert 'int' in str(y.dtype)
                    steps += 1
                    num_samples += label_mask.sum()

                    # Sample random noise for G
                    batch_z = np.random.normal(0, 1, size=(batch_size, latent_space_z_size))

                    _, _, correct = sess.run([net.disc_opt, net.gen_opt, net.masked_correct],
                                             feed_dict={net.input_actual: x, net.input_latent_z: batch_z,
                                                        net.target: y, net.label_mask: label_mask})
                    num_correct_samples += correct

                sess.run([net.shrink_learning_rate])

                training_accuracy = num_correct_samples / float(num_samples)

                print("\t\tClassifier train accuracy: ", training_accuracy)

                num_samples = 0
                num_correct_samples = 0

                for x, y in dataset.batches(batch_size, which_set="test"):
                    assert 'int' in str(y.dtype)
                    num_samples += x.shape[0]

                    correct, = sess.run([net.correct], feed_dict={net.input_real: x,
                                                                  net.y: y,
                                                                  net.drop_rate: 0.})
                    num_correct_samples += correct

                testing_accuracy = num_correct_samples / float(num_samples)
                print("\t\tClassifier test accuracy", testing_accuracy)

                gen_samples = sess.run(
                    net.samples,
                    feed_dict={net.input_latent_z: sample_z})
                samples.append(gen_samples)
                _ = view_generated_samples(-1, samples, 5, 10, figsize=figsize)
                plt.show()

                # Save history of accuracies to view after training
                train_accuracies.append(training_accuracy)
                test_accuracies.append(testing_accuracy)

            saver.save(sess, './checkpoints/generator.ckpt')

        with open('samples.pkl', 'wb') as f:
            pkl.dump(samples, f)

        return train_accuracies, test_accuracies, samples

别忘了创建一个名为 checkpoints 的目录:

real_size = (32,32,3)
latent_space_z_size = 100
learning_rate = 0.0003

net = GAN(real_size, latent_space_z_size, learning_rate)
dataset = Dataset(train_data, test_data)

train_batch_size = 128
num_epochs = 25
train_accuracies, test_accuracies, samples = train(net,
                                                   dataset,
                                                   num_epochs,
                                                   train_batch_size,
                                                   figsize=(10,5))

最后,在Epoch 24时,你应该得到如下结果:

Epoch 24
                Classifier train accuracy:  0.937
                Classifier test accuracy 0.67401659496
                Step time:  0.03694915771484375
                Epoch time:  26.15842580795288

图 8:使用特征匹配损失由生成器网络创建的示例图像

fig, ax = plt.subplots()
plt.plot(train_accuracies, label='Train', alpha=0.5)
plt.plot(test_accuracies, label='Test', alpha=0.5)
plt.title("Accuracy")
plt.legend()

图 9:训练过程中训练与测试的准确率

尽管特征匹配损失在半监督学习任务中表现良好,但生成器生成的图像不如上一章中创建的图像那么好。不过,这个实现主要是为了展示我们如何将 GAN 应用于半监督学习设置。

概述

最后,许多研究人员认为无监督学习是通用人工智能系统中的缺失环节。为了克服这些障碍,尝试通过使用更少标注数据来解决已知问题是关键。在这种情况下,GAN 为使用较少标注样本学习复杂任务提供了真正的替代方案。然而,监督学习和半监督学习之间的性能差距仍然相当大。我们可以肯定地预期,随着新方法的出现,这一差距将逐渐缩小。

第十六章:实现鱼类识别

以下是《鱼类识别》部分的完整代码,该部分内容涵盖在第一章《数据科学 – 鸟瞰》中。

鱼类识别的代码

在解释了鱼类识别示例的主要构建模块之后,我们准备好将所有代码片段连接在一起,看看我们是如何用仅仅几行代码构建出这样一个复杂的系统的:

#Loading the required libraries along with the deep learning platform Keras with TensorFlow as backend
import numpy as np
np.random.seed(2017)
import os
import glob
import cv2
import pandas as pd
import time
import warnings
from sklearn.cross_validation import KFold
from keras.models import Sequential
from keras.layers.core import Dense, Dropout, Flatten
from keras.layers.convolutional import Convolution2D, MaxPooling2D, ZeroPadding2D
from keras.optimizers import SGD
from keras.callbacks import EarlyStopping
from keras.utils import np_utils
from sklearn.metrics import log_loss
from keras import __version__ as keras_version
# Parameters
# ----------
# x : type
#    Description of parameter `x`.
def rezize_image(img_path):
  img = cv2.imread(img_path)
  img_resized = cv2.resize(img, (32, 32), cv2.INTER_LINEAR)
  return img_resized
#Loading the training samples from their corresponding folder names, where we have a folder for each type
def load_training_samples():
  #Variables to hold the training input and output variables
  train_input_variables = []
  train_input_variables_id = []
  train_label = []
  # Scanning all images in each folder of a fish type
  print('Start Reading Train Images')
  folders = ['ALB', 'BET', 'DOL', 'LAG', 'NoF', 'OTHER', 'SHARK', 'YFT']
  for fld in folders:
      folder_index = folders.index(fld)
      print('Load folder {} (Index: {})'.format(fld, folder_index))
      imgs_path = os.path.join('..', 'input', 'train', fld, '*.jpg')
      files = glob.glob(imgs_path)
      for file in files:
          file_base = os.path.basename(file)
          # Resize the image
          resized_img = rezize_image(file)
          # Appending the processed image to the input/output variables of the classifier
          train_input_variables.append(resized_img)
          train_input_variables_id.append(file_base)
          train_label.append(folder_index)
  return train_input_variables, train_input_variables_id, train_label
#Loading the testing samples which will be used to testing how well the model was trained
def load_testing_samples():
  # Scanning images from the test folder
  imgs_path = os.path.join('..', 'input', 'test_stg1', '*.jpg')
  files = sorted(glob.glob(imgs_path))
  # Variables to hold the testing samples
  testing_samples = []
  testing_samples_id = []
  #Processing the images and appending them to the array that we have
  for file in files:
      file_base = os.path.basename(file)
      # Image resizing
      resized_img = rezize_image(file)
      testing_samples.append(resized_img)
      testing_samples_id.append(file_base)
  return testing_samples, testing_samples_id
# formatting the images to fit our model
def format_results_for_types(predictions, test_id, info):
  model_results = pd.DataFrame(predictions, columns=['ALB', 'BET', 'DOL', 'LAG', 'NoF', 'OTHER',
    'SHARK', 'YFT'])
  model_results.loc[:, 'image'] = pd.Series(test_id, index=model_results.index)
  sub_file = 'testOutput_' + info + '.csv'
  model_results.to_csv(sub_file, index=False)
def load_normalize_training_samples():
  # Calling the load function in order to load and resize the training samples
  training_samples, training_label, training_samples_id = load_training_samples()
  # Converting the loaded and resized data into Numpy format
  training_samples = np.array(training_samples, dtype=np.uint8)
  training_label = np.array(training_label, dtype=np.uint8)
  # Reshaping the training samples
  training_samples = training_samples.transpose((0, 3, 1, 2))
  # Converting the training samples and training labels into float format
  training_samples = training_samples.astype('float32')
  training_samples = training_samples / 255
  training_label = np_utils.to_categorical(training_label, 8)
  return training_samples, training_label, training_samples_id
#Loading and normalizing the testing sample to fit into our model
def load_normalize_testing_samples():
  # Calling the load function in order to load and resize the testing samples
  testing_samples, testing_samples_id = load_testing_samples()
  # Converting the loaded and resized data into Numpy format
  testing_samples = np.array(testing_samples, dtype=np.uint8)
  # Reshaping the testing samples
  testing_samples = testing_samples.transpose((0, 3, 1, 2))
  # Converting the testing samples into float format
  testing_samples = testing_samples.astype('float32')
  testing_samples = testing_samples / 255
  return testing_samples, testing_samples_id
def merge_several_folds_mean(data, num_folds):
  a = np.array(data[0])
  for i in range(1, num_folds):
      a += np.array(data[i])
  a /= num_folds
  return a.tolist()
# Create CNN model architecture
def create_cnn_model_arch():
  pool_size = 2 # we will use 2x2 pooling throughout
  conv_depth_1 = 32 # we will initially have 32 kernels per conv. layer...
  conv_depth_2 = 64 # ...switching to 64 after the first pooling layer
  kernel_size = 3 # we will use 3x3 kernels throughout
  drop_prob = 0.5 # dropout in the FC layer with probability 0.5
  hidden_size = 32 # the FC layer will have 512 neurons
  num_classes = 8 # there are 8 fish types
  # Conv [32] -> Conv [32] -> Pool
  cnn_model = Sequential()
  cnn_model.add(ZeroPadding2D((1, 1), input_shape=(3, 32, 32), dim_ordering='th'))
  cnn_model.add(Convolution2D(conv_depth_1, kernel_size, kernel_size, activation='relu', dim_ordering='th'))
  cnn_model.add(ZeroPadding2D((1, 1), dim_ordering='th')
  cnn_model.add(Convolution2D(conv_depth_1, kernel_size, kernel_size, activation='relu', dim_ordering='th'))
  cnn_model.add(MaxPooling2D(pool_size=(pool_size, pool_size), strides=(2, 2), dim_ordering='th'))
  # Conv [64] -> Conv [64] -> Pool
  cnn_model.add(ZeroPadding2D((1, 1), dim_ordering='th'))
  cnn_model.add(Convolution2D(conv_depth_2, kernel_size, kernel_size, activation='relu', dim_ordering='th'))
  cnn_model.add(ZeroPadding2D((1, 1), dim_ordering='th'))
  cnn_model.add(Convolution2D(conv_depth_2, kernel_size, kernel_size, activation='relu', dim_ordering='th'))
  cnn_model.add(MaxPooling2D(pool_size=(pool_size, pool_size), strides=(2, 2), dim_ordering='th'))
  # Now flatten to 1D, apply FC then ReLU (with dropout) and finally softmax(output layer)
  cnn_model.add(Flatten())
  cnn_model.add(Dense(hidden_size, activation='relu'))
  cnn_model.add(Dropout(drop_prob))
  cnn_model.add(Dense(hidden_size, activation='relu'))
  cnn_model.add(Dropout(drop_prob))
  cnn_model.add(Dense(num_classes, activation='softmax'))
  # initiating the stochastic gradient descent optimiser
  stochastic_gradient_descent = SGD(lr=1e-2, decay=1e-6, momentum=0.9, nesterov=True)
  cnn_model.compile(optimizer=stochastic_gradient_descent,  # using the stochastic gradient descent optimiser
                    loss='categorical_crossentropy')  # using the cross-entropy loss function
  return cnn_model
#Model using with kfold cross validation as a validation method
def create_model_with_kfold_cross_validation(nfolds=10):
  batch_size = 16 # in each iteration, we consider 32 training examples at once
  num_epochs = 30 # we iterate 200 times over the entire training set
  random_state = 51 # control the randomness for reproducibility of the results on the same platform
  # Loading and normalizing the training samples prior to feeding it to the created CNN model
  training_samples, training_samples_target, training_samples_id = load_normalize_training_samples()
  yfull_train = dict()
  # Providing Training/Testing indices to split data in the training samples
  # which is splitting data into 10 consecutive folds with shuffling
  kf = KFold(len(train_id), n_folds=nfolds, shuffle=True, random_state=random_state)
  fold_number = 0 # Initial value for fold number
  sum_score = 0 # overall score (will be incremented at each iteration)
  trained_models = [] # storing the modeling of each iteration over the folds
  # Getting the training/testing samples based on the generated training/testing indices by Kfold
  for train_index, test_index in kf:
      cnn_model = create_cnn_model_arch()
      training_samples_X = training_samples[train_index] # Getting the training input variables
      training_samples_Y = training_samples_target[train_index] # Getting the training output/label variable
      validation_samples_X = training_samples[test_index] # Getting the validation input variables
      validation_samples_Y = training_samples_target[test_index] # Getting the validation output/label variabl
      fold_number += 1
      print('Fold number {} out of {}'.format(fold_number, nfolds))
      callbacks = [
          EarlyStopping(monitor='val_loss', patience=3, verbose=0),
      ]
      # Fitting the CNN model giving the defined settings
      cnn_model.fit(training_samples_X, training_samples_Y, batch_size=batch_size,
        nb_epoch=num_epochs,
            shuffle=True, verbose=2, validation_data=(validation_samples_X,
              validation_samples_Y),
            callbacks=callbacks)
      # measuring the generalization ability of the trained model based on the validation set
      predictions_of_validation_samples = 
        cnn_model.predict(validation_samples_X.astype('float32'), batch_size=batch_size, 
          verbose=2)
      current_model_score = log_loss(Y_valid, predictions_of_validation_samples)
      print('Current model score log_loss: ', current_model_score)
      sum_score += current_model_score*len(test_index)
      # Store valid predictions
      for i in range(len(test_index)):
          yfull_train[test_index[i]] = predictions_of_validation_samples[i]
      # Store the trained model
      trained_models.append(cnn_model)
  # incrementing the sum_score value by the current model calculated score
  overall_score = sum_score/len(training_samples)
  print("Log_loss train independent avg: ", overall_score)
  #Reporting the model loss at this stage
  overall_settings_output_string = 'loss_' + str(overall_score) + '_folds_' + str(nfolds) + '_ep_' + str(num_epochs)
  return overall_settings_output_string, trained_models
#Testing how well the model is trained
def test_generality_crossValidation_over_test_set(overall_settings_output_string, cnn_models):
  batch_size = 16 # in each iteration, we consider 32 training examples at once
  fold_number = 0 # fold iterator
  number_of_folds = len(cnn_models) # Creating number of folds based on the value used in the training step
  yfull_test = [] # variable to hold overall predictions for the test set
  #executing the actual cross validation test process over the test set
  for j in range(number_of_folds):
      model = cnn_models[j]
      fold_number += 1
      print('Fold number {} out of {}'.format(fold_number, number_of_folds))
      #Loading and normalizing testing samples
      testing_samples, testing_samples_id = load_normalize_testing_samples()
      #Calling the current model over the current test fold
      test_prediction = model.predict(testing_samples, batch_size=batch_size, verbose=2)
      yfull_test.append(test_prediction)
  test_result = merge_several_folds_mean(yfull_test, number_of_folds)
  overall_settings_output_string = 'loss_' + overall_settings_output_string \
              + '_folds_' + str(number_of_folds)
  format_results_for_types(test_result, testing_samples_id, overall_settings_output_string)
# Start the model training and testing
if __name__ == '__main__':
  info_string, models = create_model_with_kfold_cross_validation()
  test_generality_crossValidation_over_test_set(info_string, models)
posted @ 2025-07-08 21:23  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报