R 深度学习精要第二版(二)

部署运行你感兴趣的模型镜像

原文:annas-archive.org/md5/32221412fbc143db1a6239ed273b2843

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:使用卷积神经网络进行图像分类

毫不夸张地说,深度学习领域对卷积神经网络的巨大兴趣增长,可以说主要归功于卷积神经网络。卷积神经网络CNN)是深度学习中图像分类模型的主要构建块,并且已经取代了以前该领域专家使用的大多数技术。深度学习模型现在是执行所有大规模图像任务的事实标准方法,包括图像分类、目标检测、检测人工生成的图像,甚至为图像添加文本描述。本章中,我们将探讨其中一些技术。

为什么 CNN 如此重要?为了说明这一点,我们可以回顾一下 ImageNet 竞赛的历史。ImageNet竞赛是一个开放的大规模图像分类挑战,共有一千个类别。它可以视为图像分类的非正式世界锦标赛。参赛队伍大多由学者和研究人员组成,来自世界各地。2011 年,约 25%的错误率是基准。2012 年,由 Alex Krizhevsky 领导、Geoffrey Hinton 指导的团队通过取得 16%的错误率,赢得了比赛,取得了巨大进步。他们的解决方案包括 6000 万个参数和 65 万个神经元,五个卷积层,部分卷积层后接最大池化层,以及三个全连接层,最终通过一个 1000 分类的 Softmax 层进行最终分类。

其他研究人员在随后的几年里在他们的技术基础上做了改进,最终使得原始的 ImageNet 竞赛被基本视为解决。到 2017 年,几乎所有的队伍都达到了低于 5%的错误率。大多数人认为,2012 年 ImageNet 的胜利标志着新一轮深度学习革命的开始。

在本章中,我们将探讨使用 CNN 进行图像分类。我们将从MNIST数据集开始,MNIST被认为是深度学习任务的Hello WorldMNIST数据集包含 10 个类别的灰度图像,尺寸为 28 x 28,类别为数字 0-9。这比 ImageNet 竞赛要容易得多;它有 10 个类别而不是 1000 个,图像是灰度的而不是彩色的,最重要的是,MNIST 图像中没有可能混淆模型的背景。然而,MNIST 任务本身也很重要;例如,大多数国家使用包含数字的邮政编码,每个国家都有更复杂变体的自动地址路由解决方案。

本任务中我们将使用 Amazon 的 MXNet 库。MXNet 库是深度学习的一个优秀入门库,它允许我们以比其他库(如后面会介绍的 TensorFlow)更高的抽象级别进行编码。

本章将涵盖以下主题:

  • 什么是 CNN?

  • 卷积层

  • 池化层

  • Softmax

  • 深度学习架构

  • 使用 MXNet 进行图像分类

卷积神经网络(CNN)

卷积神经网络(CNN)是深度学习中图像分类的基石。本节将介绍它们,讲解 CNN 的历史,并解释它们为何如此强大。

在我们开始之前,我们将先看看一个简单的深度学习架构。深度学习模型难以训练,因此使用现有的架构通常是最好的起点。架构是一个现有的深度学习模型,在最初发布时是最先进的。一些例子包括 AlexNet、VGGNet、GoogleNet 等。我们将要介绍的是 Yann LeCun 及其团队于 1990 年代中期提出的用于数字分类的原始 LeNet 架构。这个架构被用来处理MNIST数据集。该数据集由 28 x 28 尺寸的灰度图像组成,包含数字 0 到 9。以下图示展示了 LeNet 架构:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/f59bb2d3-51a3-4092-8010-a129b9248d7c.png

图 5.1:LeNet 架构

原始图像大小为 28 x 28。我们有一系列的隐藏层,包括卷积层和池化层(在这里,它们被标记为子采样)。每个卷积层改变结构;例如,当我们在第一隐藏层应用卷积时,输出的大小是三维的。我们的最终层大小是 10 x 1,这与类别的数量相同。我们可以在这里应用一个softmax函数,将这一层的值转换为每个类别的概率。具有最高概率的类别将是每个图像的类别预测。

卷积层

本节将更深入地展示卷积层的工作原理。从基本层面来看,卷积层不过是一组滤波器。当你戴着带有红色镜片的眼镜看图像时,所有的事物似乎都带有红色的色调。现在,想象这些眼镜由不同的色调组成,也许是带有红色色调的镜片,里面还嵌入了一些水平绿色色调。如果你拥有这样一副眼镜,效果将是突出前方场景的某些方面。任何有绿色水平线的地方都会更加突出。

卷积层会在前一层的输出上应用一系列补丁(或卷积)。例如,在人脸识别任务中,第一层的补丁会识别图像中的基本特征,例如边缘或对角线。这些补丁会在图像上移动,匹配图像的不同部分。下面是一个 3 x 3 卷积块在 6 x 6 图像上应用的示例:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/9e8858f5-faf5-4c81-80d8-23b1caa232f8.png

图 5.2:应用于图像的单一卷积示例

卷积块中的值是逐元素相乘(即非矩阵乘法),然后将这些值加起来得到一个单一值。下面是一个示例:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/3701b84b-891e-4c43-bba3-2dc35c04c6f1.png

图 5.3:应用于输入层两个部分的卷积块示例

在这个例子中,我们的卷积块呈对角线模式。图像中的第一个块(A1:C3)也是对角线模式,因此当我们进行元素乘法并求和时,得到一个相对较大的值6.3。相比之下,图像中的第二个块(D4:F6)是水平线模式,因此我们得到一个较小的值。

可视化卷积层如何作用于整个图像可能很困难,因此下面的 R Shiny 应用将更加清晰地展示这一过程。该应用包含在本书的Chapter5/server.R文件中。请在RStudio中打开该文件并选择Run app。应用加载后,选择左侧菜单栏中的Convolutional Layers。应用将加载MNIST数据集中的前 100 张图像,这些图像稍后将用于我们的第一次深度学习图像分类任务。图像为 28 x 28 大小的灰度手写数字 0 到 9 的图像。下面是应用程序的截图,选择的是第四张图像,显示的是数字四:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/4719d54a-cdbd-4ef9-a302-fc9a7b1c9bf2.png

图 5.4:显示水平卷积滤波器的 Shiny 应用

加载后,你可以使用滑块浏览图像。在右上角,有四个可选择的卷积层可应用于图像。在前面的截图中,选择了一个水平线卷积层,我们可以在右上角的文本框中看到它的样子。当我们将卷积滤波器应用到左侧的输入图像时,我们可以看到右侧的结果图像几乎完全是灰色的,只有在原始图像中的水平线位置才会突出显示。我们的卷积滤波器已经匹配了图像中包含水平线的部分。如果我们将卷积滤波器更改为垂直线,结果将如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/684ef9f1-9e06-476a-91ce-a339e28b065b.png

图 5.5:显示垂直卷积滤波器的 Shiny 应用

现在我们可以看到,在应用卷积后,原始图像中的垂直线在右侧的结果图像中被突出显示。实际上,应用这些滤波器是一种特征提取的方式。我鼓励你使用该应用浏览图像,看看不同的卷积是如何应用于不同类别的图像的。

这就是卷积滤波器的基础,虽然它是一个简单的概念,但当你开始做两件事时,它会变得非常强大:

  • 将许多卷积滤波器组合成卷积层

  • 将另一组卷积滤波器(即卷积层)应用于先前卷积层的输出

这可能需要一些时间才能理解。如果我对一张图像应用了一个滤波器,然后对该输出再次应用一个滤波器,我得到的结果是什么?如果我再应用第三次,也就是先对一张图像应用滤波器,再对该输出应用滤波器,然后对该输出再应用滤波器,我得到的是什么?答案是,每一层后续的卷积层都会结合前一层识别到的特征,找到更为复杂的模式,例如角落、弧线等等。后续的层会发现更丰富的特征,比如一个带有弧形的圆圈,表示人的眼睛。

有两个参数用于控制卷积的移动:填充(padding)和步幅(strides)。在下图中,我们可以看到原始图像的大小是 6 x 6,而有 4 x 4 的子图。我们因此将数据表示从 6 x 6 矩阵减少到了 4 x 4 矩阵。当我们将大小为c1c2的卷积应用于大小为 n,m 的数据时,输出将是n-c1+1m-c2+1。如果我们希望输出与输入大小相同,可以通过在图像边缘添加零来填充输入。对于之前的例子,我们在整个图像周围添加一个 1 像素的边框。下图展示了如何将第一个 3 x 3 卷积应用于具有填充的图像:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/328c6a41-1e2f-4474-bd01-479977cc7e23.png

图 5.6 在卷积前应用的填充

我们可以应用于卷积的第二个参数是步幅(strides),它控制卷积的移动。默认值为 1,这意味着卷积每次移动 1 个单位,首先向右移动,然后向下移动。在实际应用中,这个值很少被改变,因此我们将不再进一步讨论它。

我们现在知道,卷积像小型特征生成器一样工作,它们应用于输入层(对于第一层来说是图像数据),而后续的卷积层则发现更复杂的特征。那么它们是如何计算的呢?我们是否需要精心手动设计一组卷积来应用到我们的模型中?答案是否定的;这些卷积是通过梯度下降算法的魔力自动计算出来的。最佳的特征模式是在经过多次训练数据集迭代后找到的。

那么,一旦我们超越了 2-3 层卷积层,卷积是如何工作的呢?答案是,任何人都很难理解卷积层具体的数学原理。即使是这些网络设计的原始设计者,也可能并不完全理解一系列卷积神经网络中的隐藏层在做什么。如果这让你感到担忧,请记住,2012 年赢得 ImageNet 竞赛的解决方案有 6000 万个参数。随着计算能力的进步,深度学习架构可能会有数亿个参数。对于任何人来说,完全理解如此复杂的模型中的每一个细节是不可能的。这就是为什么这些模型常常被称为黑箱模型的原因。

这可能一开始让你感到惊讶。深度学习如何在图像分类中实现人类水平的表现?如果我们不能完全理解它们是如何工作的,我们又如何构建深度学习模型呢?这个问题已经将深度学习社区分裂,主要是行业与学术界之间的分歧。许多(但不是所有)研究人员认为,我们应该更深入地理解深度学习模型的工作原理。一些研究人员还认为,只有通过更好地理解当前架构的工作原理,我们才能开发出下一代人工智能应用。在最近的 NIPS 会议上(这是深度学习领域最古老和最重要的会议之一),深度学习被不利地与炼金术相提并论。与此同时,业界的从业者并不关心深度学习是如何工作的。他们更关注构建更加复杂的深度学习架构,以最大化准确性或性能。

当然,这只是业界现状的粗略描述;并不是所有学者都是向内看的,也不是所有从业者都仅仅是在调整模型以获得小幅改进。深度学习仍然相对较新(尽管神经网络的基础块已经存在了数十年)。但这种紧张关系确实存在,并且已经持续了一段时间——例如,一种流行的深度学习架构引入了Inception模块,命名灵感来源于电影《盗梦空间》中的Inception。在这部电影中,莱昂纳多·迪卡普里奥带领一个团队,通过进入人们的梦境来改变他们的思想和观点。最初,他们只进入一个层次的梦境,但随后深入下去,实际上进入了梦中的梦境。随着他们深入,梦境变得越来越复杂,结果也变得不确定。我们在这里不会详细讨论Inception 模块的具体内容,但它们将卷积层和最大池化层并行结合在一起。论文的作者在论文中承认了该模型的内存和计算成本,但通过将关键组件命名为Inception 模块,他们巧妙地暗示了自己站在哪一方。

在 2012 年 ImageNet 竞赛获胜者的突破性表现之后,两个研究人员对模型的工作原理缺乏洞见感到不满。他们决定逆向工程该算法,尝试展示导致特征图中某一激活的输入模式。这是一项非平凡的任务,因为原始模型中使用的某些层(例如池化层)会丢弃信息。他们的论文展示了每一层的前 9 个激活。以下是第一层的特征可视化:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/798437be-2e43-4581-99e1-db373bd99ef0.png

图 5.7:CNN 第一层的特征可视化 来源:https://cs.nyu.edu/~fergus/papers/zeilerECCV2014.pdf

这张图片分为两部分;左边我们可以看到卷积(论文只突出了每层的 9 个卷积)。右边,我们可以看到与该卷积匹配的图像中的模式示例。例如,左上角的卷积是一个对角线边缘检测器。以下是第二层的特征可视化:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/8995cb80-262e-4472-81dd-a0201ee82632.png

图 5.8:CNN 第二层的特征可视化 来源:https://cs.nyu.edu/~fergus/papers/zeilerECCV2014.pdf

同样,左侧的图像是卷积的解释,而右侧的图像展示了激活该卷积的图像补丁示例。在这里,我们开始看到一些组合模式。例如,在左上角,我们可以看到有条纹的图案。更有趣的是第二行第二列的例子。在这里,我们看到圆形图形,这可能表示一个人的或动物的眼球。现在,让我们继续来看第三层的特征可视化:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/44322ff1-5a85-4c01-af92-00d8d8716b79.png

图 5.9:CNN 第三层的特征可视化 来源:https://cs.nyu.edu/~fergus/papers/zeilerECCV2014.pdf

在第三层中,我们可以看到一些非常有趣的模式。在第二行第二列,我们已经识别出车轮的部分。在第三行第三列,我们已经开始识别人脸。在第二行第四列,我们识别出了图像中的文本。

在论文中,作者展示了更多层的示例。我鼓励你阅读论文,进一步了解卷积层的工作原理。

需要注意的是,尽管深度学习模型在图像分类任务中能够达到与人类相当的表现,但它们并不像人类那样解读图像。它们没有“猫”或“狗”的概念,它们只能匹配给定的模式。在论文中,作者强调了一个例子,在这个例子中,匹配的模式几乎没有任何相似之处;模型匹配的是背景中的特征(如草地),而不是前景中的物体。

在另一个图像分类任务中,模型在实际操作中失败了。任务是区分狼和狗。模型在实际应用中失败是因为训练数据包含了处于自然栖息地的狼——即雪地。因此,模型误以为任务是区分。任何在其他环境下的狼的图像都会被错误分类。

从中得到的教训是,训练数据应该是多样化的,并且与模型预期要预测的实际数据密切相关。理论上这可能听起来很显而易见,但在实践中往往并不容易做到。我们将在下一章进一步讨论这一点。

池化层

池化层在卷积神经网络(CNN)中用于减少模型中的参数数量,因此可以减少过拟合。它们可以被看作是一种降维方式。类似于卷积层,池化层在上一层上滑动,但操作和返回值不同。它返回一个单一的值,通常是该区域内各个单元格的最大值,因此称之为最大池化。你也可以执行其他操作,例如平均池化,但这种方式较少使用。这里是一个使用 2 x 2 块进行最大池化的例子。第一个块的值为 7、0、6、6,其中最大值为 7,因此输出为 7。注意,最大池化通常不使用填充(padding),并且它通常会应用步幅参数来移动块。这里的步幅是 2,因此在获取第一个块的最大值后,我们会向右移动 2 个单元:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/4fdf19a0-cf55-4d13-bccd-91b5b63c0283.png

图 5.10:最大池化应用于矩阵

我们可以看到,最大池化将输出减少了四倍;输入是 6 x 6,输出是 3 x 3。如果你之前没有见过这种情况,你的第一反应可能是不相信。为什么我们要丢弃数据?为什么要使用最大池化?这个问题的答案有三个部分:

  • 池化:它通常在卷积层之后应用,因此我们不是在像素上操作,而是在匹配的模式上操作。卷积层之后的降维并不会丢弃 75% 的输入数据;如果存在模式,数据中仍然有足够的信号来识别它。

  • 正则化:如果你学习过机器学习,你会知道许多模型在处理相关特征时会遇到问题,而且通常建议去除相关特征。在图像数据中,特征与它们周围的空间模式高度相关。应用最大池化可以在保持特征的同时减少数据。

  • 执行速度:当我们考虑前面提到的两个原因时,我们可以看到,最大池化大大减少了网络的大小,而没有去除过多的信号。这使得模型训练变得更快。

需要注意的是,卷积层和池化层使用的参数是不同的。通常,卷积块的尺寸比池化块大(例如 3 x 3 的卷积块和 2 x 2 的池化块),而且它们不应该重叠。例如,不能同时使用 4 x 4 的卷积块和 2 x 2 的池化块。如果它们重叠,池化块将仅仅在相同的卷积块上操作,模型将无法正确训练。

Dropout

Dropout是一种正则化方法,旨在防止模型过拟合。过拟合是指模型记住了训练数据集中的一部分内容,但在未见过的测试数据上表现不佳。当你构建模型时,可以通过查看训练集的准确度与测试集的准确度之间的差距来检查是否存在过拟合问题。如果训练集上的表现远好于测试集,那么模型就是过拟合的。Dropout 指的是在训练过程中临时随机移除网络中的一些节点。它通常只应用于隐藏层,而不应用于输入层。下面是应用 dropout 的神经网络示例:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/25613efe-f5b3-4596-8cca-80e3f0bc38e2.jpg

图 5.11:深度学习模型中 dropout 的示例

每次前向传播时,会移除一组不同的节点,因此每次网络的结构都会不同。在原始论文中,dropout 被与集成技术进行了比较,从某种程度上来说,它确实有相似之处。dropout 的工作方式与随机森林为每棵树随机选择特征的方式有些相似。

另一种看待 dropout 的方式是,每个层中的节点必须学会与该层中的所有节点以及它从上一层获得的输入一起工作。这可以防止某些节点在层内占据主导地位并获得过大的权重,从而影响该层的输出。这意味着每个层中的节点将作为一个整体工作,防止某些节点过于懒惰,而其他节点过于支配。

Flatten 层、密集层和 softmax

应用多个卷积层后,得到的数据结构是一个多维矩阵(或张量)。我们必须将其转换为所需输出形状的矩阵。例如,如果我们的分类任务有 10 个类别(例如,MNIST示例中的 10 个类别),则需要将模型的输出设置为一个 1 x 10 的矩阵。我们通过将卷积层和最大池化层的结果进行处理,使用 Flatten 层重新塑造数据来实现这一点。最后一层的节点数应与我们要预测的类别数相同。如果我们的任务是二分类任务,则最后一层的activation函数将是 sigmoid。如果我们的任务是多分类任务,则最后一层的activation函数将是 softmax。

在应用 softmax/sigmoid 激活函数之前,我们可以选择性地应用多个密集层。密集层就是我们在第一章《深度学习入门》中看到的普通隐藏层。

我们需要一个 softmax 层,因为最后一层的值是数字,但范围从负无穷到正无穷。我们必须将这些输入值转换为一系列概率,表示该实例属于每个类别的可能性。将这些数值转换为概率的函数必须具有以下特点:

  • 每个输出值必须在 0.0 到 1.0 之间。

  • 输出值的总和应为 1.0。

一种方法是通过将每个输入值除以绝对输入值的总和来重新缩放这些值。这个方法有两个问题:

  • 它无法正确处理负值。

  • 重新缩放输入值可能会导致概率值过于接近。

这两个问题可以通过首先对每个输入值应用 e^x(其中 e 为 2.71828)然后重新缩放这些值来解决。这将把任何负数转换为一个小的正数,同时也使得概率更加极化。可以通过一个示例来演示这一点;在这里,我们可以看到来自稠密层的结果。类别 5 和 6 的值分别为 17.2 和 15.8,相当接近。然而,当我们应用 softmax 函数时,类别 5 的概率值是类别 6 的 4 倍。softmax 函数倾向于使某个类别的概率远远大于其他类别,这正是我们所希望的:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/4cbe0564-d081-4f69-9b77-5dc0e94402be.png

图 5.12 Softmax 函数的示例。

使用 MXNet 库进行图像分类。

MXNet 包在第一章中介绍过,深度学习入门,如果你还没有安装这个包,可以回到该章查看安装说明。我们将演示如何在图像数据分类任务中获得接近 100% 的准确率。我们将使用在第二章中介绍的 MNIST 数据集,使用卷积神经网络进行图像分类。该数据集包含手写数字(0-9)的图像,所有图像大小为 28 x 28。它是深度学习中的 Hello World!。Kaggle 上有一个长期的竞赛使用这个数据集。脚本 Chapter5/explore.Rmd 是一个 R markdown 文件,用于探索这个数据集。

  1. 首先,我们将检查数据是否已经下载,如果没有,我们将下载它。如果该链接无法获取数据,请参阅 Chapter2/chapter2.R 中的代码,获取数据的替代方法:
dataDirectory <- "../data"
if (!file.exists(paste(dataDirectory,'/train.csv',sep="")))
{
  link <- 'https://apache-mxnet.s3-accelerate.dualstack.amazonaws.com/R/data/mnist_csv.zip'
  if (!file.exists(paste(dataDirectory,'/mnist_csv.zip',sep="")))
    download.file(link, destfile = paste(dataDirectory,'/mnist_csv.zip',sep=""))
  unzip(paste(dataDirectory,'/mnist_csv.zip',sep=""), exdir = dataDirectory)
  if (file.exists(paste(dataDirectory,'/test.csv',sep="")))
    file.remove(paste(dataDirectory,'/test.csv',sep=""))
}
  1. 接下来,我们将数据读取到 R 中并进行检查:
train <- read.csv(paste(dataDirectory,'/train.csv',sep=""), header=TRUE, nrows=20)

我们有 20 行和 785 列。在这里,我们将查看数据集末尾的行,并查看前 6 列和最后 6 列:

tail(train[,1:6])
   label pixel0 pixel1 pixel2 pixel3 pixel4
15     3      0      0      0      0      0
16     1      0      0      0      0      0
17     2      0      0      0      0      0
18     0      0      0      0      0      0
19     7      0      0      0      0      0
20     5      0      0      0      0      0

tail(train[,(ncol(train)-5):ncol(train)])
   pixel778 pixel779 pixel780 pixel781 pixel782 pixel783
15        0        0        0        0        0        0
16        0        0        0        0        0        0
17        0        0        0        0        0        0
18        0        0        0        0        0        0
19        0        0        0        0        0        0
20        0        0        0        0        0        0

我们有 785 列。第一列是数据标签,然后是 784 列,命名为 pixel0、…、pixel783,其中包含像素值。我们的图像是 28 x 28 = 784,因此一切看起来正常。

在我们开始构建模型之前,确保数据格式正确、特征与标签对齐总是一个好主意。让我们绘制前 9 个实例及其数据标签。

  1. 为此,我们将创建一个名为 plotInstancehelper 函数,该函数接受像素值并输出图像,带有可选的标题:
plotInstance <-function (row,title="")
 {
  mat <- matrix(row,nrow=28,byrow=TRUE)
  mat <- t(apply(mat, 2, rev))
  image(mat, main = title,axes = FALSE, col = grey(seq(0, 1, length = 256)))
 }
 par(mfrow = c(3, 3))
 par(mar=c(2,2,2,2))
 for (i in 1:9)
 {
  row <- as.numeric(train[i,2:ncol(train)])
  plotInstance(row, paste("index:",i,", label =",train[i,1]))
 }

这段代码的输出显示了前 9 张图像及其分类:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/ca8e8a1a-ba42-4536-8103-7e5ac5b98ee7.png

图 5.13:MNIST 数据集中的前 9 张图像

这完成了我们的数据探索。现在,我们可以继续使用 MXNet 库创建一些深度学习模型。我们将创建两个模型——第一个是标准的神经网络模型,我们将其作为基线。第二个深度学习模型基于一个名为LeNet的架构。这是一个较旧的架构,但由于我们的图像分辨率较低且不包含背景,因此在这种情况下适用。LeNet 的另一个优点是,它的训练速度很快,即使是在 CPU 上也能高效训练,因为它的层数不多。

本节代码位于Chapter5/mnist.Rmd。我们必须将数据读入 R 并转换为矩阵。我们将训练数据分割成训练集和测试集,以便获得不偏的准确性估计。由于数据行数较多,我们可以使用 90/10 的分割比例:

require(mxnet)
options(scipen=999)

dfMnist <- read.csv("../data/train.csv", header=TRUE)
yvars <- dfMnist$label
dfMnist$label <- NULL

set.seed(42)
train <- sample(nrow(dfMnist),0.9*nrow(dfMnist))
test <- setdiff(seq_len(nrow(dfMnist)),train)
train.y <- yvars[train]
test.y <- yvars[test]
train <- data.matrix(dfMnist[train,])
test <- data.matrix(dfMnist[test,])

rm(dfMnist,yvars)

每个图像表示为 784 个(28 x 28)像素值的一行。每个像素的值范围为 0-255,我们通过除以 255 将其线性转换为 0-1。我们还对输入矩阵进行转置,因为mxnet使用的是列主序格式。

train <- t(train / 255.0)
test <- t(test / 255.0)

在创建模型之前,我们应该检查我们的数据集是否平衡,即每个数字的实例数是否合理均衡:

table(train.y)
## train.y
##    0    1    2    3    4    5    6    7    8    9
## 3716 4229 3736 3914 3672 3413 3700 3998 3640 3782

看起来没问题,我们现在可以继续创建一些深度学习模型了。

基础模型(无卷积层)

现在我们已经探索了数据,并且确认它看起来没问题,下一步是创建我们的第一个深度学习模型。这与我们在上一章看到的示例类似。该代码位于Chapter5/mnist.Rmd

data <- mx.symbol.Variable("data")
fullconnect1 <- mx.symbol.FullyConnected(data, name="fullconnect1", num_hidden=256)
activation1 <- mx.symbol.Activation(fullconnect1, name="activation1", act_type="relu")
fullconnect2 <- mx.symbol.FullyConnected(activation1, name="fullconnect2", num_hidden=128)
activation2 <- mx.symbol.Activation(fullconnect2, name="activation2", act_type="relu")
fullconnect3 <- mx.symbol.FullyConnected(activation2, name="fullconnect3", num_hidden=10)
softmax <- mx.symbol.SoftmaxOutput(fullconnect3, name="softmax")

让我们详细查看这段代码:

  1. mxnet中,我们使用其自有的数据类型符号来配置网络。

  2. 我们创建了第一个隐藏层(fullconnect1 <- ....)。这些参数是输入数据、层的名称以及该层的神经元数。

  3. 我们对fullconnect层应用激活函数(activation1 <- ....)。mx.symbol.Activation函数接收来自第一个隐藏层fullconnect1的输出。

  4. 第二个隐藏层(fullconnect1 <- ....)将activation1作为输入。

  5. 第二个激活函数类似于activation1

  6. fullconnect3是输出层。该层有 10 个神经元,因为这是一个多分类问题,共有 10 个类别。

  7. 最后,我们使用 softmax 激活函数来为每个类别获得一个概率预测。

现在,让我们训练基础模型。我安装了 GPU,因此可以使用它。你可能需要将这一行改为devices <- mx.cpu()

devices <- mx.gpu()
mx.set.seed(0)
model <- mx.model.FeedForward.create(softmax, X=train, y=train.y,
                                     ctx=devices,array.batch.size=128,
                                     num.round=10,
                                     learning.rate=0.05, momentum=0.9,
                                     eval.metric=mx.metric.accuracy,
                                     epoch.end.callback=mx.callback.log.train.metric(1))

为了进行预测,我们将调用predict函数。然后我们可以创建混淆矩阵,并计算测试数据的准确率:

preds1 <- predict(model, test)
pred.label1 <- max.col(t(preds1)) - 1
res1 <- data.frame(cbind(test.y,pred.label1))
table(res1)
##      pred.label1
## test.y   0   1   2   3   4   5   6   7   8   9
##      0 405   0   0   1   1   2   1   1   0   5
##      1   0 449   1   0   0   0   0   4   0   1
##      2   0   0 436   0   0   0   0   3   1   1
##      3   0   0   6 420   0   1   0   2   8   0
##      4   0   1   1   0 388   0   2   0   1   7
##      5   2   0   0   6   1 363   3   0   2   5
##      6   3   1   3   0   2   1 427   0   0   0
##      7   0   2   3   0   1   0   0 394   0   3
##      8   0   4   2   4   0   2   1   1 403   6
##      9   1   0   1   2   7   0   1   1   0 393

accuracy1 <- sum(res1$test.y == res1$pred.label1) / nrow(res1)
accuracy1
## 0.971

我们的基础模型的准确率为0.971。还不错,但让我们看看能否有所改进。

LeNet

现在,我们可以基于 LeNet 架构创建一个模型。这是一个非常简单的模型;我们有两组卷积层和池化层,然后是一个 Flatten 层,最后是两个全连接层。相关代码在 Chapter5/mnist.Rmd 中。首先,我们来定义这个模型:

data <- mx.symbol.Variable('data')
# first convolution layer
convolution1 <- mx.symbol.Convolution(data=data, kernel=c(5,5), num_filter=64)
activation1 <- mx.symbol.Activation(data=convolution1, act_type="tanh")
pool1 <- mx.symbol.Pooling(data=activation1, pool_type="max",
                           kernel=c(2,2), stride=c(2,2))

# second convolution layer
convolution2 <- mx.symbol.Convolution(data=pool1, kernel=c(5,5), num_filter=32)
activation2 <- mx.symbol.Activation(data=convolution2, act_type="relu")
pool2 <- mx.symbol.Pooling(data=activation2, pool_type="max",
                           kernel=c(2,2), stride=c(2,2))

# flatten layer and then fully connected layers
flatten <- mx.symbol.Flatten(data=pool2)
fullconnect1 <- mx.symbol.FullyConnected(data=flatten, num_hidden=512)
activation3 <- mx.symbol.Activation(data=fullconnect1, act_type="relu")
fullconnect2 <- mx.symbol.FullyConnected(data=activation3, num_hidden=10)
# final softmax layer
softmax <- mx.symbol.SoftmaxOutput(data=fullconnect2)

现在,让我们重新调整数据的形状,以便它可以在 MXNet 中使用:

train.array <- train
dim(train.array) <- c(28,28,1,ncol(train))
test.array <- test
dim(test.array) <- c(28,28,1,ncol(test))

最后,我们可以构建模型:

devices <- mx.gpu()
mx.set.seed(0)
model2 <- mx.model.FeedForward.create(softmax, X=train.array, y=train.y,
                                     ctx=devices,array.batch.size=128,
                                     num.round=10,
                                     learning.rate=0.05, momentum=0.9, wd=0.00001,
                                     eval.metric=mx.metric.accuracy,
                                     epoch.end.callback=mx.callback.log.train.metric(1))

最后,让我们评估模型:

preds2 <- predict(model2, test.array)
pred.label2 <- max.col(t(preds2)) - 1
res2 <- data.frame(cbind(test.y,pred.label2))
table(res2)
## pred.label2
## test.y   0   1   2   3   4   5   6   7   8   9
##      0 412   0   0   0   0   1   1   1   0   1
##      1   0 447   1   1   1   0   0   4   1   0
##      2   0   0 438   0   0   0   0   3   0   0
##      3   0   0   6 427   0   1   0   1   2   0
##      4   0   0   0   0 395   0   0   1   0   4
##      5   1   0   0   5   0 369   2   0   1   4
##      6   2   0   0   0   1   1 432   0   1   0
##      7   0   0   2   0   0   0   0 399   0   2
##      8   1   0   1   0   1   1   1   1 414   3
##      9   2   0   0   0   4   0   0   1   1 398

accuracy2
## 0.9835714

我们的 CNN 模型的准确率是 0.9835714,相比我们基准模型的 0.971,有了相当大的提升。

最后,我们可以在 R 中可视化我们的模型:

graph.viz(model2$symbol)

这会生成以下图表,展示深度学习模型的架构:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/44ca3de2-5605-49eb-9042-80099ff1b36f.png

图 5.14:卷积深度学习模型(LeNet)

恭喜你!你已经构建了一个准确率超过 98% 的深度学习模型!

我们在 图 5.1 中看到了 LeNet 的架构,并且已经使用 MXNet 库进行了编程。接下来,我们更详细地分析 LeNet 架构。本质上,我们有两组卷积层和两层全连接层。我们的卷积组包含一个卷积层,接着是一个 activation 函数,然后是一个池化层。这种层的组合在许多深度学习图像分类任务中非常常见。第一层卷积层有 64 个 5 x 5 大小的卷积块,没有填充。这可能会错过图像边缘的一些特征,但如果我们回顾 图 5.15 中的样本图像,我们可以看到大多数图像的边缘并没有数据。我们使用 pool_type=max 的池化层。其他类型也是可能的;平均池化曾经常用,但最近已经不太流行了。这也是一个可以尝试的超参数。我们计算 2 x 2 的池化区域,然后步长为 2(即“跳跃”)。因此,每个输入值在最大池化层中只使用一次。

我们为第一个卷积块使用Tanh作为激活函数,然后为后续层使用ReLU。如果你愿意,可以尝试更改这些并查看它们的效果。执行卷积层后,我们可以使用 Flatten 将数据重构为全连接层可以使用的格式。全连接层就是一组节点集合,也就是类似于前面代码中基本模型的层。我们有两层,一层包含 512 个节点,另一层包含 10 个节点。我们选择在最后一层中使用 10 个节点,因为这是我们问题中的类别数量。最后,我们使用 Softmax 将该层中的数值转化为每个类别的概率集。我们已经达到了 98.35%的准确率,这比普通的深度学习模型有了显著的提升,但仍然有改进空间。一些模型在该数据集上的准确率可达到 99.5%,也就是每 1000 个记录中有 5 个错误分类。接下来,我们将查看一个不同的数据集,虽然它与 MNIST 相似,但比 MNIST 要更具挑战性。这就是 Fashion MNIST数据集,具有与 MNIST 相同大小的灰度图像,并且也有 10 个类别。

使用 Fashion MNIST 数据集进行分类

这个数据集与MNIST的结构相同,因此我们只需要更换数据集,并使用我们现有的加载数据的样板代码。脚本Chapter5/explore_Fashion.Rmd是一个 R markdown 文件,用来探索这个数据集;它与我们之前用于MNIST数据集的explore.Rmd几乎完全相同,因此我们不再重复。唯一的不同是在explore.Rmd中增加了输出标签。我们将查看 16 个示例,因为这是一个新数据集。以下是使用我们为MNIST数据集创建示例时所用的相同样板代码,生成的一些来自该数据集的示例图像:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/367f26fc-344b-499f-a1de-5e190590764f.png

图 5.15:来自 Fashion MNIST 数据集的部分图像

这个数据集的一个有趣的事实是,发布该数据集的公司还创建了一个 GitHub 仓库,在那里他们对比了多种机器学习库在该数据集上的表现。基准测试可以在fashion-mnist.s3-website.eu-central-1.amazonaws.com/查看。如果我们查看这些结果,会发现他们尝试的所有机器学习库都没有达到 90%的准确率(他们没有尝试深度学习)。这是我们希望通过深度学习分类器超越的目标。深度学习模型的代码在Chapter5/fmnist.R中,能够在该数据集上实现超过 91%的准确率。与上面模型架构相比,有一些小的但重要的差异。试着在不查看解释的情况下找到它们。

首先,让我们定义模型架构。

data <- mx.symbol.Variable('data')
# first convolution layer
convolution1 <- mx.symbol.Convolution(data=data, kernel=c(5,5),
                                      stride=c(1,1), pad=c(2,2), num_filter=64)
activation1 <- mx.symbol.Activation(data=convolution1, act_type=act_type1)
pool1 <- mx.symbol.Pooling(data=activation1, pool_type="max",
                           kernel=c(2,2), stride=c(2,2))

# second convolution layer
convolution2 <- mx.symbol.Convolution(data=pool1, kernel=c(5,5),
                                      stride=c(1,1), pad=c(2,2), num_filter=32)
activation2 <- mx.symbol.Activation(data=convolution2, act_type=act_type1)
pool2 <- mx.symbol.Pooling(data=activation2, pool_type="max",
                           kernel=c(2,2), stride=c(2,2))

# flatten layer and then fully connected layers with activation and dropout
flatten <- mx.symbol.Flatten(data=pool2)
fullconnect1 <- mx.symbol.FullyConnected(data=flatten, num_hidden=512)
activation3 <- mx.symbol.Activation(data=fullconnect1, act_type=act_type1)
drop1 <- mx.symbol.Dropout(data=activation3,p=0.4)
fullconnect2 <- mx.symbol.FullyConnected(data=drop1, num_hidden=10)
# final softmax layer
softmax <- mx.symbol.SoftmaxOutput(data=fullconnect2)

现在让我们来训练模型:

logger <- mx.metric.logger$new()
model2 <- mx.model.FeedForward.create(softmax, X=train.array, y=train.y,
                                     ctx=devices, num.round=20,
                                     array.batch.size=64,
                                     learning.rate=0.05, momentum=0.9,
                                     wd=0.00001,
                                     eval.metric=mx.metric.accuracy,
                                     eval.data=list(data=test.array,labels=test.y),
                                     epoch.end.callback=mx.callback.log.train.metric(100,logger))

第一个变化是我们将所有层的激活函数切换为使用relu。另一个变化是我们为卷积层使用了填充,以便捕捉图像边缘的特征。我们增加了每一层的节点数,给模型增加了深度。我们还添加了一个 dropout 层,以防止模型过拟合。我们还在模型中加入了日志记录功能,输出每个 epoch 的训练和验证指标。我们利用这些数据来检查模型表现,并决定它是否过拟合。

这是该模型的准确率结果及其诊断图:

preds2 <- predict(model2, test.array)
pred.label2 <- max.col(t(preds2)) - 1
res2 <- data.frame(cbind(test.y,pred.label2))
table(res2)
      pred.label2
test.y   0   1   2   3   4   5   6   7   8   9
     0 489   0  12  10   0   0  53   0   3   0
     1   0 586   1   6   1   0   1   0   0   0
     2   8   1 513   7  56   0  31   0   0   0
     3  13   0   3 502  16   0  26   1   1   0
     4   1   1  27  13 517   0  32   0   2   0
     5   1   0   0   0   0 604   0   9   0   3
     6  63   0  47   9  28   0 454   0   3   0
     7   0   0   0   1   0  10   0 575   1  11
     8   0   0   1   0   1   2   1   0 618   0
     9   0   0   0   0   0   1   0  17   1 606
accuracy2 <- sum(res2$test.y == res2$pred.label2) / nrow(res2)
accuracy2
# 0.9106667

需要注意的一点是,我们在训练过程中展示度量指标和评估最终模型时使用的是相同的验证/测试集。这并不是一个好做法,但在这里是可以接受的,因为我们并没有使用验证指标来调整模型的超参数。我们 CNN 模型的准确率是0.9106667

让我们绘制训练集和验证集准确率随模型训练进展的变化图。深度学习模型代码中有一个callback函数,可以在模型训练时保存指标。我们可以利用它绘制每个 epoch 的训练和验证指标图:

# use the log data collected during model training
dfLogger<-as.data.frame(round(logger$train,3))
dfLogger2<-as.data.frame(round(logger$eval,3))
dfLogger$eval<-dfLogger2[,1]
colnames(dfLogger)<-c("train","eval")
dfLogger$epoch<-as.numeric(row.names(dfLogger))

data_long <- melt(dfLogger, id="epoch")

ggplot(data=data_long,
       aes(x=epoch, y=value, colour=variable,label=value)) +
  ggtitle("Model Accuracy") +
  ylab("accuracy") +
  geom_line()+geom_point() +
  geom_text(aes(label=value),size=3,hjust=0, vjust=1) +
  theme(legend.title=element_blank()) +
  theme(plot.title = element_text(hjust = 0.5)) +
  scale_x_discrete(limits= 1:nrow(dfLogger))

这向我们展示了模型在每个 epoch(或训练轮次)后的表现。以下是生成的截图:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/b92b126f-4ded-40c8-b316-1fbae5700357.png

图 5.16:每个 epoch 的训练和验证准确率

从这张图中可以得出两个主要结论:

  • 该模型出现了过拟合。我们可以看到训练集的性能为0.95xxx,而验证集的性能为0.91xxx,二者之间存在明显的差距。

  • 我们可能在第 8 个 epoch 后就可以停止模型训练,因为此后性能没有再提升。

正如我们在前几章讨论的那样,深度学习模型默认几乎总是会出现过拟合,但有方法可以避免这一点。第二个问题与早停相关,了解如何做到这一点至关重要,这样你就不会浪费数小时继续训练一个不再改进的模型。如果你是使用云资源来构建模型,这一点尤其重要。我们将在下一章讨论这些以及与构建深度学习模型相关的更多问题。

参考文献/进一步阅读

这些论文是该领域经典的深度学习论文,其中一些记录了赢得 ImageNet 竞赛的方案。我鼓励你下载并阅读所有这些论文。你可能一开始无法理解它们,但随着你在深度学习领域的进展,它们的重要性将变得更加显而易见。

  • Krizhevsky, Alex, Ilya Sutskever 和 Geoffrey E. Hinton. 使用深度卷积神经网络进行 ImageNet 分类. 神经信息处理系统进展. 2012.

  • Szegedy, Christian 等人. 通过卷积深入探索. Cvpr, 2015.

  • LeCun, Yann 等人。分类学习算法:手写数字识别比较研究。神经网络:统计力学视角 261(1995):276。

  • Zeiler, Matthew D.和 Rob Fergus。可视化和理解卷积网络。欧洲计算机视觉会议。斯普林格,香槟,2014。

  • Srivastava, Nitish 等人。Dropout:防止神经网络过拟合的简单方法。机器学习研究杂志 15.1(2014):1929-1958。

摘要

在本章中,我们使用深度学习进行图像分类。我们讨论了用于图像分类的不同层类型:卷积层,池化层,Dropout,全连接层以及 Softmax 激活函数。我们看到了一个 R-Shiny 应用程序,展示了卷积层如何在图像数据上进行特征工程。

我们使用 MXNet 深度学习库在 R 中创建了一个基础深度学习模型,其准确率达到了 97.1%。然后,我们基于 LeNet 架构开发了一个 CNN 深度学习模型,在测试数据上实现了超过 98.3%的准确率。我们还使用了一个稍难的数据集(Fashion MNIST),并创建了一个新模型,其准确率超过了 91%。这个准确率比使用非深度学习算法的所有分数都要好。在下一章中,我们将建立在我们所讨论的基础上,并展示如何利用预训练模型进行分类,以及作为新深度学习模型的构建块。

在接下来的章节中,我们将讨论深度学习中关于调优和优化模型的重要主题。这包括如何利用可能有的有限数据,数据预处理,数据增强以及超参数选择。

第六章:调整和优化模型

在过去的两章中,我们训练了用于分类、回归和图像识别任务的深度学习模型。在本章中,我们将讨论管理深度学习项目的一些重要问题。虽然本章可能显得有些理论性,但如果没有正确管理讨论的任何问题,可能会导致深度学习项目的失败。我们将探讨如何选择评估指标,以及如何在开始建模之前评估深度学习模型的性能。接下来,我们将讨论数据分布及在将数据划分为训练集时常见的错误。许多机器学习项目在生产环境中失败,原因是数据分布与模型训练时的数据分布不同。我们将讨论数据增强,这是提升模型准确性的一个重要方法。最后,我们将讨论超参数,并学习如何调整它们。

本章我们将讨论以下主题:

  • 评估指标与性能评估

  • 数据准备

  • 数据预处理

  • 数据增强

  • 调整超参数

  • 用例——可解释性

评估指标与性能评估

本节将讨论如何设置深度学习项目以及如何选择评估指标。我们将探讨如何选择评估标准,并如何判断模型是否接近最佳性能。我们还将讨论所有深度学习模型通常会出现过拟合问题,以及如何管理偏差/方差的权衡。此部分将为在模型准确率较低时应采取的措施提供指导。

评估指标的类型

不同的评估指标用于分类和回归任务。在分类任务中,准确率是最常用的评估指标。然而,准确率只有在所有类别的错误成本相同的情况下才有效,但这并非总是如此。例如,在医疗诊断中,假阴性的成本要远高于假阳性的成本。假阴性意味着认为某人没有生病,而实际上他们有病,延误诊断可能会带来严重甚至致命的后果。另一方面,假阳性则是认为某人生病了,而实际上并没有,这虽然让病人感到不安,但不会威胁到生命。

当数据集不平衡时,这个问题会变得更加复杂,即某一类别比另一类别更为常见。以我们的医疗诊断示例为例,如果接受测试的人中只有 1%的人实际患有疾病,那么机器学习算法仅通过判断没有人患病就能获得 99%的准确率。在这种情况下,可以考虑其他的评估指标,而非准确率。对于不平衡的数据集,F1 评估指标是一个有用的选择,它是精确率和召回率的加权平均。F1 分数的计算公式如下:

F1 = 2 * (精确率 * 召回率) / (精确率 + 召回率)

精度和召回率的公式如下:

精度 = true_positives / (true_positives + false_positives)

召回率 = true_positives / (true_positives + false_negatives)

对于回归问题,您可以选择评估指标:MAE、MSE 和 RMSE。MAE,或称为平均绝对误差,是最简单的;它只是实际值与预测值之间绝对差的平均值。MAE 的优点是易于理解;如果 MAE 是 3.5,那么预测值与实际值之间的差异平均为 3.5。MSE,或称为均方误差,是误差平方的平均值,也就是说,它计算实际值与预测值之间的差异,平方后再求这些值的平均值。使用 MSE 相较于 MAE 的优点在于,它根据误差的严重程度进行惩罚。如果实际值和预测值之间的差异为 2 和 5,那么 MSE 会对第二个例子赋予更多的权重,因为误差较大。RMSE,或称为均方根误差,是 MSE 的平方根。使用 MSE 的优点在于,它将误差项转回与实际值可比较的单位。对于回归任务,RMSE 通常是首选的评估指标。

欲了解有关 MXNet 中评估指标的更多信息,请参见 mxnet.incubator.apache.org/api/python/metric/metric.html

欲了解有关 Keras 中评估指标的更多信息,请参见 keras.io/metrics/

评估性能

我们在前几章中探讨了一些深度学习模型。在 第五章 使用卷积神经网络进行图像分类 中,我们在 MNIST 数据集上的图像分类任务中获得了 98.36% 的准确率。对于 第四章 训练深度预测模型 中的二分类任务(预测哪些客户将在接下来的 14 天内返回),我们获得了 77.88% 的准确率。但这到底意味着什么呢?我们如何评估深度学习模型的性能?

评估深度学习模型是否具有良好预测能力的显而易见的起点是与其他模型进行比较。MNIST 数据集在许多深度学习研究的基准测试中都有使用,因此我们知道有些模型的准确率可以达到 99.5%。因此,我们的模型是可以接受的,但并不出色。在本章的 数据增强 部分,我们将通过对现有图像数据进行修改来生成新图像,从而显著提高模型的准确性,从 98.36% 提升到 98.95%。通常,对于图像分类任务,任何低于 95% 的准确率可能意味着您的深度学习模型存在问题。要么模型设计不正确,要么您的任务没有足够的数据。

我们的二分类模型只有 77.54%的准确率,远低于图像分类任务。那么,这是不是一个糟糕的模型?其实不然;它仍然是一个有用的模型。我们也有来自其他机器学习模型(如随机森林和 xgboost)的基准,它们是在数据的小部分上运行的。我们还看到,当我们从一个包含 3,900 行的数据的模型转到一个更深的包含 390,000 行的数据的模型时,准确率有所提高。这表明,深度学习模型随着数据量的增加而改进。

评估模型性能的一个步骤是查看更多数据是否会显著提高准确率。这些数据可以通过更多的训练数据获取,或者通过数据增强来获得,后者我们将在后续章节中讨论。你可以使用学习曲线来评估这是否有助于性能提升。要创建学习曲线,你需要训练一系列逐步增加数据量的机器学习模型,例如,从 10,000 行到 200,000 行,每次增加 1,000 行。对于每一步,运行5个不同的机器学习模型来平滑结果,并根据样本量绘制平均准确率。以下是执行此任务的伪代码:

For k=10000 to 200000 step 1000
   For n=1 to 5
       [sample] = Take k rows from dataset
       Split [sample] into train (80%) / test (20%)
       Run ML (DT) algorithm
       Calculate Accuracy on test
       Save accuracy value
Plot k, avg(Accuracy)

这是一个与客户流失问题类似任务的学习曲线示例:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/f5c76faa-0ab1-4117-a821-8d943a43ec49.png

图 6.1:一个学习曲线示例,展示了数据量与准确率的关系

在这种情况下,准确率处于一个非常狭窄的范围,并且随着实例数量的增加而稳定。因此,对于该算法和超参数选择,增加更多的数据不会显著提高准确率。

如果我们得到一个像本例中一样平坦的学习曲线,那么向现有模型中添加更多数据不会提高准确率。我们可以尝试通过更改模型架构或增加更多特征来提高性能。我们在第五章,使用卷积神经网络进行图像分类中讨论了这一些选项。

回到我们的二分类模型,我们来考虑如何将它应用于生产环境。回想一下,该模型试图预测客户是否会在接下来的 x 天内返回。这里是该模型的混淆矩阵:

      Predicted
Actual     0     1
 0     10714  4756
 1      3870 19649

如果我们观察模型在每个类别中的表现,会得到不同的准确率:

  • 对于Actual=0,我们得到 10714 / (10714 + 4756) = 69.3% 的正确值。这被称为特异性或真负率。

  • 对于Actual=1,我们得到 19649 / (3466* + 19649) = 85.0%* 的正确值。这被称为敏感性或真正例率。

对于这个用例,灵敏度可能比特异性更为重要。如果我是高级经理,我会更关心哪些客户被预测会回归但实际上没有回归。可以向这一群体发送优惠以吸引他们回归。假设该模型是用来预测某人在 9 月 1 日至 9 月 14 日之间是否会回归,那么 9 月 15 日,我们得到前面的混淆矩阵。经理应该如何分配有限的营销预算?

  • 我可以看到我得到了 4,756 个被预测不会回归但实际上回归的客户。这很好,但我不能真正对其采取行动。我可以尝试向 10,135 个未回归的客户发送优惠,但由于我的模型已经预测他们不会回归,我预计响应率会很低。

  • 预测到会回归但未回归的 3,870 名客户更为有趣。这些人应该收到优惠以吸引他们在行为变化成为永久之前回归。这仅占我的客户基础的 9.9%,因此只向这些客户发送优惠,我不会通过向大量客户发送优惠而浪费预算。

预测模型不应该单独使用;应该将其他指标与之结合,制定营销策略。例如,客户生命周期价值CLV),它衡量的是一个客户的预期未来收入减去重新获得该客户的成本,可以与预测模型结合使用。通过结合使用预测模型和 CLV,我们可以优先考虑那些根据预测未来价值可能回归的客户。

总结这一部分,过分沉迷于优化评估指标是很容易的,尤其是当你是该领域的新手时。作为数据科学家,你应该始终记住,优化机器学习任务的评估指标并不是最终目标——它只是改善某一部分业务的代理。你必须能够将机器学习模型的结果与业务用例联系起来。例如,在MNIST数据集中的数字识别任务,评估指标与业务用例之间有直接联系。但有时候这种联系并不那么明显,你需要与业务合作,找出如何利用分析结果来最大化公司收益的方法。

数据准备

机器学习是训练一个模型,使其能够在见过的案例上进行泛化,以便它能对未见过的数据做出预测。因此,用来训练深度学习模型的数据应该与模型在生产中看到的数据相似。然而,在产品的早期阶段,你可能几乎没有数据来训练模型,那么你该怎么办呢?例如,一个移动应用可能包含一个机器学习模型,用来预测由手机摄像头拍摄的图像的主题。当应用程序编写时,可能没有足够的数据来使用深度学习网络训练模型。一种方法是通过其他来源的图像来增强数据集,以训练深度学习网络。然而,你需要知道如何管理这一点,以及如何处理它引入的不确定性。另一种方法是迁移学习,我们将在第十一章,深度学习的下一个层次中讨论。

深度学习与传统机器学习之间的另一个区别是数据集的大小。这会影响数据拆分的比例——用于机器学习的数据拆分推荐指南(如 70/30 或 80/20 拆分)需要在训练深度学习模型时进行修订。

不同的数据分布

在前面的章节中,我们使用了 MNIST 数据集进行分类任务。虽然该数据集包含手写数字,但这些数据并不代表真实世界的数据。在第五章,使用卷积神经网络进行图像分类中,我们可视化了其中的一些数字,如果你回去看这些图像,会发现这些图像是标准格式的:

  • 所有图像都是灰度的

  • 所有图像都是 28 x 28

  • 所有图像的边界似乎至少有 1 像素

  • 所有图像的尺度相同,也就是说,每个图像几乎占据了整个图像

  • 扭曲非常小,因为边框是黑色的,前景是白色的

  • 图像是正立的,也就是说,我们没有进行过大的旋转

MNIST 数据集的最初用途是识别信件上的 5 位数字邮政编码。假设我们使用 MNIST 数据集中的 60,000 张图像来训练一个模型,并希望在生产环境中使用它来识别信件和包裹上的邮政编码。生产系统必须在应用深度学习之前执行以下步骤:

  • 扫描字母

  • 找到邮政编码部分

  • 将邮政编码的数字分成 5 个不同的区域(每个数字一个区域)

在任何一个数据转换步骤中,可能会出现额外的数据偏差。如果我们使用干净的 MNIST 数据来训练模型,然后尝试预测有偏的转换数据,那么我们的模型可能效果不好。数据偏差对生产数据的影响示例如下:

  • 正确定位邮政编码本身就是一个难题

  • 字母将具有不同颜色和对比度的背景和前景,因此将它们转换为灰度图像可能不一致,这取决于字母和笔在字母/包裹上的使用类型。

  • 扫描过程的结果可能会有所不同,因为使用了不同的硬件和软件——这是将深度学习应用于医学图像数据时的一个持续性问题。

  • 最后,将邮政编码分成 5 个不同区域的难度,取决于字母和笔的使用方式,以及前面步骤的质量。

在这个例子中,用来训练数据的分布与估计模型性能的数据,与生产数据是不同的。如果数据科学家承诺在模型部署之前提供 99%的准确度,那么当应用程序在生产环境中运行时,管理层很可能会感到失望!在创建一个新模型时,我们将数据分为训练集和测试集,因此测试数据集的主要目的是估计模型的准确性。但如果测试数据集中的数据与模型在生产环境中将会见到的数据不同,那么测试数据集上的评估指标就无法准确指导模型在生产环境中的表现。

如果问题是一开始几乎没有或完全没有实际的标注数据,那么在任何模型训练之前,首先需要考虑的一步是调查是否可以获取更多数据。获取数据可能需要搭建一个小型生产环境,或者与客户合作,使用半监督学习与人工标注相结合的方法。在我们刚才看到的用例中,我会认为,设置提取数字化图像的流程比查看任何机器学习方法更为重要。一旦这个过程搭建好,我会着手建立一些训练数据——这些数据仍然可能不足以建立一个模型,但可以用来作为一个合适的测试集,以创建能够反映实际性能的评估指标。这一点可能看起来显而易见,因为基于有缺陷的评估指标所产生的过于乐观的期望,很可能是数据科学项目中排名前三的问题之一。

一个非常成功地处理这个问题的大型项目案例是 Airbnb 中的这个用例:medium.com/airbnb-engineering/categorizing-listing-photos-at-airbnb-f9483f3ab7e3。他们有大量的房屋室内照片,但这些照片没有标注房间类型。他们利用现有的标注数据,并且进行质量保证,以检查标签的准确性。在数据科学中,常说创建机器学习模型可能只占实际工作量的 20%——获取一个准确且能代表模型在生产环境中实际见到的数据集,通常是深度学习项目中最困难的任务。

一旦你有了数据集,你需要在建模之前将数据分为训练集和测试集。如果你有传统机器学习的经验,你可能会从 70/30 的划分开始,即 70%用于训练模型,30%用于评估模型。然而,在大数据集和深度学习模型训练的领域,这条规则就不那么适用了。再次强调,将数据分为训练集和测试集的唯一原因,是为了有一个留存集来估计模型的表现。因此,你只需要在这个数据集中拥有足够的记录,以便你得到的准确度估计是可靠的,并且具有你所要求的精度。如果你一开始就有一个大数据集,那么测试数据集的比例较小可能就足够了。让我通过一个例子来解释,你想在现有的机器学习模型上进行改进:

  • 先前的机器学习模型具有 99.0%的准确度。

  • 有一个带标签的数据集,包含 1,000,000 条记录。

如果要训练一个新的机器学习模型,那么它至少应该达到 99.1%的准确度,才能让你确信它比现有模型有改进。那么在评估现有模型时需要多少记录呢?你只需要足够的记录,以便你能比较确定新模型的准确度在 0.1%的范围内。因此,测试集中的 50,000 条记录(即数据集的 5%)就足够评估你的模型。如果在这 50,000 条记录上的准确度为 99.1%,则有 49,550 条记录是正确分类的。这比基准模型多了 50 条正确分类的记录,这强烈表明第二个模型是一个更好的模型——差异不太可能仅仅是偶然的结果。

你可能会对只使用 5%的数据来评估模型的建议感到抵触。然而,70/30 数据划分的想法源自于小型数据集的时代,比如包含 150 条记录的鸢尾花数据集。我们之前在第四章中看到过以下图表,训练深度预测模型,该图展示了机器学习算法的准确度在数据量增加时往往会停滞。因此,最大化可用于训练的数据量的动机较小。深度学习模型可以利用更多的数据,因此如果我们可以为测试集使用更少的数据,我们应该能得到一个更好的模型:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/272cf949-b665-4241-8b57-34c1419ee8c9.png

图 6.2:数据集大小如何影响深度学习模型与其他机器学习模型的准确度

数据在训练集、测试集和验证集之间的划分。

前一节强调了在项目早期阶段获取一些数据的重要性。但如果你没有足够的数据来训练深度学习模型,仍然可以使用其他数据进行训练并将其应用到你的数据上。例如,你可以使用在 ImageNet 数据上训练的模型来进行图像分类任务。在这种情况下,你需要明智地使用收集到的真实数据。本节讨论了关于这一主题的一些好实践。

如果你曾经想过,为什么像谷歌、苹果、Facebook、亚马逊等大公司在人工智能方面具有如此大的领先优势,原因就在于此。虽然他们有世界上最优秀的 AI 专家为他们工作,但他们的最大优势在于他们可以使用大量的标注数据来构建他们的机器学习模型。

在前一节中,我们说过测试集的唯一目的是评估模型。但是,如果这些数据和模型在预测任务中将会遇到的数据不来自相同的分布,那么评估结果会产生误导。项目的一个重要优先事项应该是尽早获取与现实数据相似的标注数据。一旦你获得了这些数据,你需要聪明地使用这一宝贵资产。根据优先级,最好的数据使用方式如下:

  • 我可以使用一些数据来创建更多的训练数据吗?这可以通过数据增强,或者实现一个早期原型让用户进行互动来实现。

  • 如果你正在构建多个模型(这是应该做的),请使用验证集中的一些数据来调整模型。

  • 使用测试集中的数据来评估模型。

  • 使用训练集中的数据。

其中一些建议可能会引发争议,尤其是建议你应该在测试集之前使用验证集的数据。记住,测试集的唯一目的是用来评估模型,它应该只使用一次,所以你只有一次使用这些数据的机会。如果我只有少量的真实数据,我更倾向于用它来调整模型,并接受较不精确的评估指标,而不是用它来获得一个评估指标非常精确但表现差劲的模型。

这种方法有风险,理想情况下,你希望验证数据集和测试数据集来自相同的分布,并且能够代表模型在生产环境中遇到的数据。不幸的是,当你处于机器学习项目的早期阶段,且现实数据有限时,你必须决定如何最佳地使用这些数据,在这种情况下,最好将有限的数据用在验证数据集上,而不是测试数据集上。

标准化

数据准备的另一个重要步骤是标准化数据。在上一章中,对于 MNIST 数据,所有像素值都被除以 255,使得输入数据在 0.0 到 1.0 之间。在我们的案例中,我们应用了最小-最大归一化,它使用以下函数线性地转换数据:

xnew = (x - min(x)) / (max(x) - min(x))

由于我们已经知道 min(x) = 0max(x) = 255,因此这可以简化为以下形式:

xnew = x / 255.0

另一种最常见的标准化形式是将特征缩放,使得均值为 0,标准差为 1。这也被称为z 分数,其公式如下:

xnew = (x - mean(x)) / std.dev(x)

我们需要执行标准化的原因有三点:

  • 如果特征处于不同的尺度,特别重要的是对输入特征进行归一化。机器学习中常见的一个例子是根据卧室数量和面积来预测房价。卧室数量的范围从 1 到 10,而面积可以从 500 平方英尺到 20000 平方英尺不等。深度学习模型要求特征处于相同的范围内。

  • 即使我们的所有特征已经处于相同范围内,仍然建议对输入特征进行归一化。回想一下在第三章《深度学习基础》中,我们讨论了在模型训练前初始化权重的问题。如果我们的特征没有归一化,初始化权重的任何好处都将被抵消。我们还讨论了梯度爆炸和梯度消失的问题。当特征处于不同的尺度时,这个问题更容易发生。

  • 即使我们避免了前述两个问题,如果不进行归一化,模型的训练时间也会更长。

在第四章《训练深度预测模型》中,流失模型的所有列都表示消费金额,因此它们已经处于相同的尺度。当我们对每个变量应用对数变换时,这会将它们缩小到-4.6 到 11 之间,因此无需将它们缩放到 0 和 1 之间。标准化正确应用时没有负面影响,因此应该是数据准备中的第一步。

数据泄露

数据泄露是指用于训练模型的特征具有在生产环境中无法存在的值。它在时间序列数据中最为常见。例如,在我们在第四章《训练深度预测模型》中讨论的客户流失案例中,数据中有一些类别变量表示客户分群。数据建模者可能认为这些是良好的预测变量,但我们无法知道这些变量何时以及如何设置。它们可能基于客户的消费金额,这意味着如果这些变量被用于预测算法中,就会出现循环引用——外部过程根据消费金额计算分群,然后这个变量被用来预测消费金额!

在提取数据来构建模型时,你应该小心类别属性,并思考这些变量何时可能被创建和修改。不幸的是,大多数数据库系统在追踪数据来源方面较弱,因此如果有疑虑,你可以考虑将这些变量从模型中省略。

在图像分类任务中,数据泄露的另一个例子是当图像中的属性信息被用于模型时。例如,如果我们建立一个模型,其中文件名作为属性包含在内,这些文件名可能暗示了类别名称。当该模型在生产环境中使用时,这些线索将不再存在,因此这也被视为数据泄露。

我们将在本章稍后的使用案例—可解释性部分看到数据泄露的实际例子。

数据增强

增加模型准确性的一个方法,不论你拥有多少数据,就是基于现有数据创建人工示例。这就是所谓的数据增强。数据增强也可以在测试时使用,以提高预测准确性。

使用数据增强来增加训练数据

我们将对之前章节中使用的MNIST数据集应用数据增强。如果你想跟着做,本部分的代码位于Chapter6/explore.Rmd。在第五章《使用卷积神经网络进行图像分类》中,我们绘制了一些来自 MNIST 数据集的例子,因此我们不再重复这些代码。它已包含在代码文件中,你也可以参考第五章中的图像,《使用卷积神经网络进行图像分类》:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/aaa6ddab-8aa7-4016-a047-fbe2740489df.png

图 6.3:MNIST 数据集中的前 9 张图像

我们将数据增强描述为从现有数据集中创建新数据。这意味着创建一个与原始实例有足够差异的新实例,但又不会如此不同以至于它不再代表数据标签。对于图像数据,这可能意味着对图像执行以下操作:

  • 缩放:通过放大图像的中心,模型可能更好地处理不同尺度的图像。

  • 平移:将图像向上、下、左或右移动,可以让深度学习模型更好地识别偏离中心的图像示例。

  • 旋转:通过旋转图像,模型将能够识别偏离中心的数据。

  • 翻转:对于许多物体,图像翻转 90 度是有效的。例如,从左侧拍摄的汽车照片可以翻转,呈现出右侧的汽车图像。深度模型可以利用这一新视角。

  • 添加噪声:有时候,故意向图像中添加噪声可以迫使深度学习模型发现更深层次的意义。

  • 修改颜色:通过向图像添加滤镜,你可以模拟不同的光照条件。例如,你可以将一张在强光下拍摄的图像更改为看起来像是在光线不足的情况下拍摄的图像。

这个任务的目标是提高测试数据集的准确性。然而,数据增强的重要规则是,新数据应该尽力模拟模型在生产环境中使用的数据,而不是试图提高现有数据上的模型准确性。我无法强调这一点的重要性。如果模型在生产环境中无法正常工作,而用于训练和评估模型的数据并不代表现实生活中的数据,那么在留出的数据集上获得 99%的准确率是毫无意义的。在我们的例子中,我们可以看到 MNIST 图像是灰度图且整齐居中的,等等。在生产环境中,图像通常是偏离中心的,并且背景和前景各异(例如,带有棕色背景和蓝色文字),因此无法正确分类。你可以尝试对图像进行预处理,以便将其格式化为类似的方式(28 x 28 灰度图,黑色背景,数据居中并有 2 x 2 的边距),但更好的解决方案是训练模型以应对其将在生产环境中遇到的典型数据。

如果我们查看前面的图像,可以发现大多数数据增强任务并不适用于 MNIST 数据。所有图像似乎已经处于相同的缩放级别,因此创建放大版的人工图像不会有所帮助。同样,平移也不太可能有效,因为图像已经是居中的。翻转图像肯定无效,因为许多数字翻转后并不有效,例如7。我们的数据中没有现有的随机噪声,因此这一方法也行不通。

我们可以尝试的一种技术是旋转图像。我们将为每个现有图像创建两个新的人工图像,第一个人工图像将向左旋转 15 度,第二个人工图像将向右旋转 15 度。以下是我们将原始图像向左旋转 15 度后的部分人工图像:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/e9a813a3-c0c5-4e64-a1ec-dce8d29c14be.png

图 6.4:MNIST 数据向左旋转 15 度

如果我们查看前面的截图,会发现一个奇怪的异常。我们有 10 个类别,使用这种方法可能会提高整体准确率,但有一个类别的提升不那么显著。数字 0 就是其中的一个例外,因为旋转数字 0 看起来仍然像 0——虽然这个类别的准确率可能会有所提高,但可能不如其他类别那么显著。旋转图像数据的函数在Chapter6/img_ftns.R中。它使用了OpenImageR包中的rotateImage函数:

rotateInstance <-function (df,degrees)
{
  mat <- as.matrix(df)
  mat2 <- rotateImage(mat, degrees, threads = 1)
  df <- data.frame(mat2)
  return (df)
}

实际上,我们可以对数据集应用两种类型的数据增强。第一种类型是从现有样本创建新的训练数据。我们还可以使用一种叫做测试时增强TTA)的技术,这可以在模型评估期间使用。它会对每一行测试数据进行复制,然后使用这些复制和原始数据一起投票决定类别。稍后我们会看到这个例子的展示。

创建数据增强数据集的代码在Chapter6/augment.R中。请注意,这个过程运行时间较长,可能需要 6 到 10 小时,具体取决于你的机器。它还需要大约 300MB 的空闲空间来创建新的数据集。代码并不复杂,它加载数据并将其分割成训练集和测试集。对于训练数据,它创建两个新实例:一个旋转 15 度向左,另一个旋转 15 度向右。需要注意的是,用于评估模型性能的数据不能包含在数据增强过程中,也就是说,首先要将数据分割成训练集,并且只对训练集应用数据增强。

当数据增强完成后,数据文件夹中将会生成一个名为train_augment.csv的新文件。这个文件应该包含 113,400 行。我们原始的MNIST数据集有 42,000 行;我们抽取了其中的 10%作为测试数据(即用于验证我们的模型),剩下 37,800 行。然后我们为这些行做了两个副本,这意味着现在每一行都有 3 个副本。因此,训练数据文件中将包含37,800 x 3 = **113,400行数据。augment.R还会输出测试数据(4,200 行),保存为test0.csv,以及增强后的测试集(test_augment.csv),稍后我们将进一步讲解。

运行神经网络的代码在Chapter6/mnist.Rmd中。第一部分使用增强后的数据进行训练,几乎与第五章的代码完全相同,卷积神经网络的图像分类。唯一的区别是,它加载了augment.R中创建的数据文件(train_augment.csvtest0.csv),所以我们在这里不再重复模型的所有代码。以下是混淆矩阵和测试数据集上的最终准确度:

## pred.label
## test.y   0   1   2   3   4   5   6   7   8   9
##      0 412   0   0   1   0   0   3   0   0   0
##      1   0 447   1   2   0   0   0   5   0   0
##      2   0   0 437   1   2   0   0   1   0   0
##      3   0   0   3 432   0   0   0   1   1   0
##      4   0   0   0   0 396   1   0   0   0   3
##      5   1   0   0   1   0 378   1   0   0   1
##      6   1   1   0   0   0   0 434   0   1   0
##      7   0   1   2   0   1   0   0 398   0   1
##      8   0   0   2   1   0   0   0   1 419   0
##      9   0   0   0   0   5   0   0   1   1 399
accuracy2 <- sum(res$test.y == res$pred.label) / nrow(res)
The accuracy of our model with augmented train data is 0.9885714.

这与我们在第五章中模型的准确度0.9821429相比,取得了显著的改进。我们的错误率已经降低了超过 30%(0.9885714-0.9835714**) / (1.0-0.9835714)

测试时数据增强

我们还可以在测试时使用数据增强。在augment.R文件中,它创建了一个包含原始测试集 4,200 行数据(data/test0.csv)的文件,并用它来评估模型。augment.R文件还创建了一个名为test_augment.csv的文件,包含原始的 4,200 行数据,每个图像有 2 个副本。这些副本类似于我们在增强训练数据时所做的操作,即一行数据是将图像旋转 15 度向左,另一行数据是将图像旋转 15 度向右。三行数据按顺序输出,我们将使用这三行数据来投票决定最终结果。我们需要从test_augment.csv中每次取出 3 条记录,并计算这些值的平均预测值。以下是执行测试时数据增强的代码:

test_data <- read.csv("../data/test_augment.csv", header=TRUE)
test.y <- test_data[,1]
test <- data.matrix(test_data)
test <- test[,-1]
test <- t(test/255)
test.array <- test
dim(test.array) <- c(28, 28, 1, ncol(test))

preds3 <- predict(model2, test.array)
dfPreds3 <- as.data.frame(t(preds3))
# res is a data frame with our predictions after train data augmentation,
# i.e. 4200 rows
res$pred.label2 <- 0
for (i in 1:nrow(res))
{
   sum_r <- dfPreds3[((i-1)*3)+1,] +
            dfPreds3[((i-1)*3)+2,] + dfPreds3[(i*3),] 
   res[i,"pred.label2"] <- max.col(sum_r)-1
}
accuracy3 <- sum(res$test.y == res$pred.label2) / nrow(res)
The accuracy of our CNN model with augmented train data and Test Time Augmentation (TTA) is 0.9895238.

通过这种方式,我们得到了 12,600 行数据的预测(4,200 x 3)。for 循环会运行 4,200 次,每次取出 3 条记录,计算平均准确度。使用增强训练数据的准确度提升较小,从0.98857140.9895238,约为 0.1%(4 行)。我们可以在以下代码中查看 TTA 的效果:

tta_incorrect <- nrow(res[res$test.y != res$pred.label2 & res$test.y == res$pred.label,])
tta <- res[res$test.y == res$pred.label2 & res$test.y != res$pred.label,c("pred.label","pred.label2")]

Number of rows where Test Time Augmentation (TTA) changed the prediction to the correct value 9 (nrow(tta)).
Number of rows where Test Time Augmentation (TTA) changed the prediction to the incorrect value 5 (tta_incorrect).

tta
##     pred.label pred.label2
## 39           9           4
## 268          9           4
## 409          9           4
## 506          8           6
## 1079         2           3
## 1146         7           2
## 3163         4           9
## 3526         4           2
## 3965         2           8

这张表显示了测试时数据增强正确的 9 行数据,而之前的模型是错误的。我们可以看到三种情况,其中之前的模型(pred.model)预测为9,而测试时数据增强模型正确预测了4。虽然在这个案例中,测试时数据增强并未显著提高我们的准确度,但它在其他计算机视觉任务中可能会带来差异。

在深度学习库中使用数据增强

我们使用 R 包实现了数据增强,但生成增强数据花费了很长时间。它对于演示目的很有用,但 MXNet 和 Keras 都支持数据增强功能。在 MXNet 中,mx.image.*有一系列函数可以实现此功能(mxnet.incubator.apache.org/tutorials/python/data_augmentation.html)。在 Keras 中,这些功能位于keras.preprocessing.*keras.io/preprocessing/image/),可以自动应用到你的模型中。在第十一章,深度学习的下一个层级中,我们展示了如何使用 Keras 进行数据增强。

调整超参数

所有的机器学习算法都有超参数或设置,这些超参数可以改变算法的运行方式。这些超参数能够提高模型的准确性或减少训练时间。我们在前面的章节中已经见过一些超参数,特别是第三章《深度学习基础》,在这一章中,我们探讨了可以在mx.model.FeedForward.create函数中设置的超参数。本节中的技术可以帮助我们找到更好的超参数值。

选择超参数并不是灵丹妙药;如果原始数据质量较差,或者数据量不足以支持训练,那么调整超参数也只能起到有限的作用。在这种情况下,可能需要获取额外的变量/特征作为预测变量,或者增加更多的案例数据。

网格搜索

欲了解更多关于调整超参数的信息,请参见 Bengio, Y.(2012),特别是第三部分《超参数》,讨论了各种超参数的选择和特点。除了手动试错法之外,还有两种改善超参数的方法:网格搜索和随机搜索。在网格搜索中,指定多个超参数值并尝试所有可能的组合。这种方法可能是最容易理解的。在 R 中,我们可以使用expand.grid()函数来创建所有可能的变量组合:

expand.grid(
 layers=c(1,4),
 lr=c(0.01,0.1,0.5,1.0),
 l1=c(0.1,0.5))
   layers    lr   l1
1       1  0.01  0.1
2       4  0.01  0.1
3       1  0.10  0.1
4       4  0.10  0.1
5       1  0.50  0.1
6       4  0.50  0.1
7       1  1.00  0.1
8       4  1.00  0.1
9       1  0.01  0.5
10      4  0.01  0.5
11      1  0.10  0.5
12      4  0.10  0.5
13      1  0.50  0.5
14      4  0.50  0.5
15      1  1.00  0.5
16      4  1.00  0.5

网格搜索在超参数值较少的情况下是有效的。然而,当某些或许多超参数的值很多时,它很快就变得不可行。例如,即使每个超参数只有两个值,对于八个超参数来说,也有2⁸ = 256种组合,这很快就变得计算上不切实际。此外,如果超参数与模型性能之间的相互作用较小,那么使用网格搜索就是一种低效的方法。

随机搜索

超参数选择的另一种方法是通过随机采样进行搜索。与预先指定所有要尝试的值并创建所有可能的组合不同,可以随机采样参数的值,拟合模型,存储结果,然后重复这一过程。为了获得非常大的样本量,这也需要很高的计算要求,但你可以指定你愿意运行的不同模型的数量。因此,这种方法能让你在超参数组合上分布广泛。

对于随机采样,只需要指定要随机采样的值,或者指定要随机抽取的分布。通常还会设定一些限制。例如,虽然理论上模型可以有任何整数层数,但通常会使用一个合理的数字范围(如 1 到 10),而不是从 1 到十亿中采样整数。

为了进行随机抽样,我们将编写一个函数,接受一个种子,然后随机抽样多个超参数,存储抽样的参数,运行模型并返回结果。尽管我们进行随机搜索以寻找更好的值,但我们并没有从所有可能的超参数中进行抽样。许多超参数仍保持在我们指定的值或其默认值上。

对于某些超参数,指定如何随机抽样值可能需要一些工作。例如,当使用 dropout 进行正则化时,通常在较早的隐藏层(0%-20%)使用较小的 dropout,而在较晚的隐藏层(50%-80%)使用较大的 dropout。选择合适的分布使我们能够将这些先验信息编码到我们的随机搜索中。以下代码绘制了两个 Beta 分布的密度,结果如 图 6.5 所示:

par(mfrow = c(2, 1))
plot(
  seq(0, .5, by = .001),
  dbeta(seq(0, .5, by = .001), 1, 12),
  type = "l", xlab = "x", ylab = "Density",
  main = "Density of a beta(1, 12)")

plot(
  seq(0, 1, by = .001)/2,
  dbeta(seq(0, 1, by = .001), 1.5, 1),
  type = "l", xlab = "x", ylab = "Density",
  main = "Density of a beta(1.5, 1) / 2")

通过从这些分布中进行抽样,我们可以确保我们的搜索聚焦于早期隐藏层的小比例 dropout,并且在 00.50 范围内的隐藏神经元,具有从接近 0.50 的值过度抽样的趋势:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/dccb2eb7-acbb-40e0-885d-a59a081ad55e.png

图 6.5:使用 Beta 分布来选择超参数

使用案例—使用 LIME 进行可解释性分析

深度学习模型被认为难以解释。一些模型可解释性的方法,包括 LIME,允许我们深入了解模型是如何得出结论的。在演示 LIME 之前,我将展示不同的数据分布和/或数据泄漏如何在构建深度学习模型时引发问题。我们将重用 第四章 中的深度学习客户流失模型,训练深度预测模型,但我们将对数据做一个改动。我们将引入一个与 y 值高度相关的坏变量。我们只会在用于训练和评估模型的数据中包含该变量。一个来自原始数据的单独测试集将被保留,代表模型在生产环境中将看到的数据,测试集不会包含坏变量。创建这个坏变量可以模拟我们之前讨论的两种可能的情景:

  • 不同的数据分布:坏变量确实存在于模型在生产环境中看到的数据中,但其分布不同,这意味着模型的表现没有达到预期。

  • 数据泄漏:我们的坏变量被用来训练和评估模型,但当模型在生产环境中使用时,这个变量不可用,因此我们为它分配一个零值,这也意味着模型的表现没有达到预期。

本例的代码位于Chapter6/binary_predict_lime.R。我们不会再次深入讲解深度学习模型,如果你需要回顾如何实现,可以参考第四章,训练深度预测模型。我们将对模型代码做两个修改:

  • 我们将数据分成三部分:训练集、验证集和测试集。训练集用于训练模型,验证集用于评估已训练的模型,而测试集则代表模型在生产环境中会看到的数据。

  • 我们将创建bad_var变量,并将其包含在训练集和验证集中,但不包含在测试集中。

下面是分割数据并创建bad_var变量的代码:

# add feature (bad_var) that is highly correlated to the variable to be predicted
dfData$bad_var <- 0
dfData[dfData$Y_categ==1,]$bad_var <- 1
dfData[sample(nrow(dfData), 0.02*nrow(dfData)),]$bad_var <- 0
dfData[sample(nrow(dfData), 0.02*nrow(dfData)),]$bad_var <- 1
table(dfData$Y_categ,dfData$bad_var)
       0    1
  0 1529   33
  1   46 2325
cor(dfData$Y_categ,dfData$bad_var)
[1] 0.9581345

nobs <- nrow(dfData)
train <- sample(nobs, 0.8*nobs)
validate <- sample(setdiff(seq_len(nobs), train), 0.1*nobs)
test <- setdiff(setdiff(seq_len(nobs), train),validate)
predictorCols <- colnames(dfData)[!(colnames(dfData) %in% c("CUST_CODE","Y_numeric","Y_categ"))]

# remove columns with zero variance in train-set
predictorCols <- predictorCols[apply(dfData[train, predictorCols], 2, var, na.rm=TRUE) != 0]

# for our test data, set the bad_var to zero
# our test dataset is not from the same distribution
# as the data used to train and evaluate the model
dfData[test,]$bad_var <- 0

# look at all our predictor variables and 
# see how they correlate with the y variable
corr <- as.data.frame(cor(dfData[,c(predictorCols,"Y_categ")]))
corr <- corr[order(-corr$Y_categ),]
old.par <- par(mar=c(7,4,3,1))

barplot(corr[2:11,]$Y_categ,names.arg=row.names(corr)[2:11],
        main="Feature Correlation to target variable",cex.names=0.8,las=2)
par(old.par)

我们的新变量与y变量的相关性为0.958。我们还创建了一个条形图,显示了与y变量相关性最高的特征,从中可以看到,这个新变量与y变量的相关性远高于其他变量与y变量之间的相关性。如果某个特征与y变量的相关性非常高,通常表明数据准备过程中存在问题。这也意味着不需要机器学习解决方案,因为一个简单的数学公式就能预测结果变量。对于实际项目,这个变量应该被排除在模型之外。以下是与y变量相关性最高的特征图,bad_var变量的相关性超过0.9

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/c51ab854-c930-482d-948e-83aa0e701d6f.png

图 6.6:从特征到目标变量的前 10 大相关性

在我们继续构建模型之前,请注意我们如何将这个新特征在测试集中设为零。这个测试集实际上代表了模型在生产环境中会看到的数据,因此我们将其设为零,表示可能存在不同的数据分布或数据泄露问题。下面是展示模型在验证集和测试集上表现的代码:

#### Verifying the model using LIME

# compare performance on validation and test set 
print(sprintf(" Deep Learning Model accuracy on validate (expected in production) = %1.2f%%",acc_v))
[1] " Deep Learning Model accuracy on validate (expected in production) = 90.08%"
print(sprintf(" Deep Learning Model accuracy in (actual in production) = %1.2f%%",acc_t))
[1] " Deep Learning Model accuracy in (actual in production) = 66.50%"

这里的验证集代表了在模型构建过程中用于评估模型的数据,而测试集代表未来的生产数据。验证集上的准确率超过 90%,但测试集上的准确率不到 70%。这表明,不同的数据分布和/或数据泄露问题会导致模型准确率的过高估计。

使用 LIME 进行模型可解释性分析

LIME代表局部可解释模型无关解释。LIME 可以解释任何机器学习分类器的预测结果,而不仅仅是深度学习模型。它的工作原理是对每个实例的输入进行小的变化,并尝试映射该实例的局部决策边界。通过这样做,它可以看到哪个变量对该实例的影响最大。相关内容可以参考以下论文:Ribeiro, Marco Tulio, Sameer Singh, and Carlos Guestrin*. 为什么我应该信任你?:解释任何分类器的预测结果。第 22 届 ACM SIGKDD 国际会议——知识发现与数据挖掘,ACM,2016*。

让我们来看一下如何使用 LIME 分析上一节中的模型。我们需要设置一些样板代码来连接 MXNet 和 LIME 结构,然后我们可以基于训练数据创建 LIME 对象:

# apply LIME to MXNet deep learning model
model_type.MXFeedForwardModel <- function(x, ...) {return("classification")}
predict_model.MXFeedForwardModel <- function(m, newdata, ...)
{
  pred <- predict(m, as.matrix(newdata),array.layout="rowmajor")
  pred <- as.data.frame(t(pred))
  colnames(pred) <- c("No","Yes")
  return(pred)
}
explain <- lime(dfData[train, predictorCols], model, bin_continuous = FALSE)

然后我们可以传入测试集中的前 10 条记录,并创建一个图表来显示特征重要性:

val_first_10 <- validate[1:10]

explaination <- lime::explain(dfData[val_first_10, predictorCols],explainer=explain,
                              n_labels=1,n_features=3)
plot_features(explaination) + labs(title="Churn Model - variable explanation")

这将生成如下图表,展示对模型预测结果影响最大的特征:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/d6bbe7d7-bf00-4481-ba69-1aa00dfaf64d.png

图 6.7:使用 LIME 的特征重要性

请注意,在每个案例中,bad_var变量是最重要的变量,其尺度远大于其他特征。这与我们在图 6.6中看到的情况一致。以下图展示了针对 10 个测试案例的特征组合的热图可视化:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/765662ae-1728-4f81-8d55-1f1cb1634153.png

图 6.8:使用 LIME 的特征热图

本示例展示了如何将 LIME 应用于一个已经使用 MXNet 训练的深度学习模型,以可视化哪些特征对模型的一些预测结果最为重要。从图 6.7 和图 6.8 中可以看出,单个特征几乎完全负责预测y变量,这表明存在数据分布不同和/或数据泄漏的问题。实际上,这样的变量应当从模型中排除。

做个对比,如果我们在没有这个字段的情况下训练一个模型,再次绘制特征重要性图,我们会看到没有单一特征占主导地位:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/4a3b160d-5582-4943-81b7-ceafba51f828.png

图 6.9:使用 LIME 的特征重要性(不包含bad_var特征)

并没有一个特征是最重要的,拟合的解释度是 0.05,相比于图 6.7中的 0.18,三个变量的显著性条形图在相似的尺度上。下图展示了使用 LIME 的特征热图:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/b25a6e33-f61c-4d77-95cb-78c0b82f0ad1.png

图 6.10:使用 LIME 的特征热图(不包含bad_var特征)

再次,图表向我们展示了使用了多个特征。我们可以看到前一张图中,特征权重的图例范围是从 0.01 到 0.02。而在图 6.8中,特征权重的图例范围是从 -0.2 到 0.2,这表明有些特征(实际上只有一个)主导了模型。

总结

本章涵盖了深度学习项目成功的关键主题。这些内容包括可用于评估模型的不同类型的评估指标。我们还讨论了数据准备中可能出现的一些问题,包括在训练数据量较少时的处理方法,以及如何在数据中创建不同的划分,即如何创建适当的训练集、测试集和验证集。我们探讨了两个可能导致模型在生产环境中表现不佳的重要问题:数据分布的差异和数据泄露。我们看到了如何通过数据增强技术来改善现有模型,方法是通过创建人工数据,并讨论了如何调节超参数以提高深度学习模型的性能。最后,我们通过模拟数据分布差异/数据泄露的问题,并使用 LIME 解释现有的深度学习模型,结束了本章的讨论。

本章中的一些概念可能看起来有些理论性;然而,它们对于机器学习项目的成功至关重要!许多书籍会在最后才涉及这些内容,但本书在相对较早的阶段就涵盖了它们,以突出其重要性。

在下一章中,我们将探讨如何使用深度学习进行自然语言处理NLP),即文本数据。使用深度学习处理文本数据更高效、更简单,且通常优于传统的 NLP 方法。

第七章:使用深度学习的自然语言处理

本章将展示如何使用深度学习进行 自然语言处理NLP)。NLP 是对人类语言文本的处理。NLP 是一个广泛的术语,涵盖了涉及文本数据的多种任务,包括(但不限于)以下内容:

  • 文档分类:根据主题将文档分类为不同类别

  • 命名实体识别:从文档中提取关键信息,例如人物、组织和地点

  • 情感分析:将评论、推文或评价分类为正面或负面情感

  • 语言翻译:将文本数据从一种语言翻译成另一种语言

  • 词性标注:为文档中的每个单词分配类型,通常与其他任务一起使用

在本章中,我们将讨论文档分类,这是最常见的自然语言处理技术之一。本章的结构与前几章不同,因为我们将集中讨论一个用例(文本分类),但会应用多种方法。本章将涵盖:

  • 如何使用传统机器学习技术进行文本分类

  • 词向量

  • 比较传统文本分类与深度学习

  • 高级深度学习文本分类,包括 1D 卷积神经网络、RNN、LSTM 和 GRU

文档分类

本章将通过 Keras 进行文本分类。我们将使用的数据集包含在 Keras 库中。与前几章一样,我们将首先使用传统机器学习技术创建基准模型,然后再应用深度学习算法。这样做的目的是展示深度学习模型与其他技术的表现差异。

Reuters 数据集

我们将使用 Reuters 数据集,可以通过 Keras 库中的一个函数访问该数据集。该数据集包含 11,228 条记录,涵盖 46 个类别。要查看有关该数据集的更多信息,请运行以下代码:

library(keras)
?dataset_reuters

尽管可以通过 Keras 访问 Reuters 数据集,但它并不是其他机器学习算法可以直接使用的格式。文本数据不是实际的单词,而是单词索引的列表。我们将编写一个简短的脚本(Chapter7/create_reuters_data.R),它下载数据及其查找索引文件,并创建一个包含 y 变量和文本字符串的数据框。然后,我们将把训练数据和测试数据分别保存到两个文件中。以下是创建训练数据文件的代码第一部分:

library(keras)

# the reuters dataset is in Keras
c(c(x_train, y_train), c(x_test, y_test)) %<-% dataset_reuters()
word_index <- dataset_reuters_word_index()

# convert the word index into a dataframe
idx<-unlist(word_index)
dfWords<-as.data.frame(idx)
dfWords$word <- row.names(dfWords)
row.names(dfWords)<-NULL
dfWords <- dfWords[order(dfWords$idx),]

# create a dataframe for the train data
# for each row in the train data, we have a list of index values
# for words in the dfWords dataframe
dfTrain <- data.frame(y_train)
dfTrain$sentence <- ""
colnames(dfTrain)[1] <- "y"
for (r in 1:length(x_train))
{
  row <- x_train[r]
  line <- ""
  for (i in 1:length(row[[1]]))
  {
     index <- row[[1]][i]
     if (index >= 3)
       line <- paste(line,dfWords[index-3,]$word)
  }
  dfTrain[r,]$sentence <- line
  if ((r %% 100) == 0)
    print (r)
}
write.table(dfTrain,"../data/reuters.train.tab",sep="\t",row.names = FALSE)

代码的第二部分类似,它创建了包含测试数据的文件:

# create a dataframe for the test data
# for each row in the train data, we have a list of index values
# for words in the dfWords dataframe
dfTest <- data.frame(y_test)
dfTest$sentence <- ""
colnames(dfTest)[1] <- "y"
for (r in 1:length(x_test))
{
  row <- x_test[r]
  line <- ""
  for (i in 1:length(row[[1]]))
  {
    index <- row[[1]][i]
    if (index >= 3)
      line <- paste(line,dfWords[index-3,]$word)
  }
  dfTest[r,]$sentence <- line
  if ((r %% 100) == 0)
    print (r)
}
write.table(dfTest,"../data/reuters.test.tab",sep="\t",row.names = FALSE)

这将创建两个文件,分别是 ../data/reuters.train.tab../data/reuters.test.tab。如果我们打开第一个文件,下面是第一行数据。这句话是一个正常的英语句子:

y句子
3mcgrath rentcorp 表示,由于在 12 月收购了 Space Co,公司预计 1987 年的每股收益将在 1.15 至 1.30 美元之间,较 1986 年的 70 美分有所增长。公司表示,税前净收入将从 1986 年的 600 万美元增长到 900 万至 1000 万美元,租赁业务收入将从 1250 万美元增长到 1900 万至 2200 万美元。预计今年每股现金流将为 2.50 至 3 美元。路透社 3

现在我们已经将数据转化为表格格式,我们可以使用 传统 的 NLP 机器学习方法来创建分类模型。当我们合并训练集和测试集并查看 y 变量的分布时,我们可以看到共有 46 个类别,但每个类别中的实例数并不相同:

> table(y_train)
 0   1   2    3    4   5   6   7   8   9  10  11  12  13  14  15  16  17 
  67 537  94 3972 2423  22  62  19 177 126 154 473  62 209  28  29 543  51 

 18   19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35 
  86  682 339 127  22  53  81 123  32  19  58  23  57  52  42  16  57  16 

 36  37  38  39  40  41  42  43  44  45 
  60  21  22  29  46  38  16  27  17  19 

对于我们的测试集,我们将创建一个二分类问题。我们的任务是从所有其他记录中识别出分类为 3 的新闻片段。当我们更改标签时,我们的 y 分布将变化如下:

y_train[y_train!=3] <- 0
y_train[y_train==3] <- 1
table(y_train)
 0    1 
7256 3972 

传统文本分类

我们的第一个 NLP 模型将使用传统的 NLP 技术,即不使用深度学习。在本章剩余部分,当我们使用传统 NLP 一词时,我们指的是不使用深度学习的方法。传统 NLP 分类中最常用的方法是使用 词袋模型

我们还将使用一组超参数和机器学习算法来最大化准确性:

  • 特征生成:特征可以是词频、tf-idf 或二元标志

  • 预处理:我们通过对单词进行词干提取来预处理文本数据

  • 去除停用词:我们将特征创建、停用词和词干提取选项视为超参数

  • 机器学习算法:该脚本将三种机器学习算法应用于数据(朴素贝叶斯、SVM、神经网络和随机森林)

我们在数据上训练了 48 个机器学习算法,并评估哪一个模型表现最佳。该代码的脚本位于 Chapter7/classify_text.R 文件夹中。该代码不包含任何深度学习模型,所以如果你愿意,可以跳过它。首先,我们加载必要的库,并创建一个函数,用于为多种机器学习算法的超参数组合创建一组文本分类模型:

library(tm)
require(nnet)
require(kernlab)
library(randomForest)
library(e1071)
options(digits=4)

TextClassification <-function (w,stem=0,stop=0,verbose=1)
{
  df <- read.csv("../data/reuters.train.tab", sep="\t", stringsAsFactors = FALSE)
  df2 <- read.csv("../data/reuters.test.tab", sep="\t", stringsAsFactors = FALSE)
  df <- rbind(df,df2)

  # df <- df[df$y %in% c(3,4),]
  # df$y <- df$y-3
  df[df$y!=3,]$y<-0
  df[df$y==3,]$y<-1
  rm(df2)

  corpus <- Corpus(DataframeSource(data.frame(df[, 2])))
  corpus <- tm_map(corpus, content_transformer(tolower))

  # hyperparameters
  if (stop==1)
    corpus <- tm_map(corpus, function(x) removeWords(x, stopwords("english")))
  if (stem==1)
    corpus <- tm_map(corpus, stemDocument)
  if (w=="tfidf")
    dtm <- DocumentTermMatrix(corpus,control=list(weighting=weightTfIdf))
  else if (w=="tf")
    dtm <- DocumentTermMatrix(corpus,control=list(weighting=weightTf))
  else if (w=="binary")
    dtm <- DocumentTermMatrix(corpus,control=list(weighting=weightBin))

  # keep terms that cover 95% of the data
  dtm2<-removeSparseTerms(dtm, 0.95)
  m <- as.matrix(dtm2)
  remove(dtm,dtm2,corpus)

  data<-data.frame(m)
  data<-cbind(df[, 1],data)
  colnames(data)[1]="y"

  # create train, test sets for machine learning
  seed <- 42 
  set.seed(seed) 
  nobs <- nrow(data)
  sample <- train <- sample(nrow(data), 0.8*nobs)
  validate <- NULL
  test <- setdiff(setdiff(seq_len(nrow(data)), train), validate)

现在我们已经创建了一个稀疏数据框,我们将对数据使用 4 种不同的机器学习算法:朴素贝叶斯、支持向量机(SVM)、神经网络模型和随机森林模型。我们使用 4 种机器学习算法,因为正如你所看到的,调用机器学习算法的代码相较于创建前一部分数据和执行 NLP 所需的代码要少得多。通常来说,当可能时,运行多个机器学习算法总是一个好主意,因为没有任何一个机器学习算法始终是最好的。


  # create Naive Bayes model
  nb <- naiveBayes(as.factor(y) ~., data=data[sample,])
  pr <- predict(nb, newdata=data[test, ])
  # Generate the confusion matrix showing counts.
  tab<-table(na.omit(data[test, ])$y, pr,
             dnn=c("Actual", "Predicted"))
  if (verbose) print (tab)
  nb_acc <- 100*sum(diag(tab))/length(test)
  if (verbose) print(sprintf("Naive Bayes accuracy = %1.2f%%",nb_acc))

  # create SVM model
  if (verbose) print ("SVM")
  if (verbose) print (Sys.time())
  ksvm <- ksvm(as.factor(y) ~ .,
               data=data[sample,],
               kernel="rbfdot",
               prob.model=TRUE)
  if (verbose) print (Sys.time())
  pr <- predict(ksvm, newdata=na.omit(data[test, ]))
  # Generate the confusion matrix showing counts.
  tab<-table(na.omit(data[test, ])$y, pr,
             dnn=c("Actual", "Predicted"))
  if (verbose) print (tab)
  svm_acc <- 100*sum(diag(tab))/length(test)
  if (verbose) print(sprintf("SVM accuracy = %1.2f%%",svm_acc))

  # create Neural Network model
  rm(pr,tab)
  set.seed(199)
  if (verbose) print ("Neural Network")
  if (verbose) print (Sys.time())
  nnet <- nnet(as.factor(y) ~ .,
               data=data[sample,],
               size=10, skip=TRUE, MaxNWts=10000, trace=FALSE, maxit=100)
  if (verbose) print (Sys.time())
  pr <- predict(nnet, newdata=data[test, ], type="class")
  # Generate the confusion matrix showing counts.
  tab<-table(data[test, ]$y, pr,
             dnn=c("Actual", "Predicted"))
  if (verbose) print (tab)
  nn_acc <- 100*sum(diag(tab))/length(test)
  if (verbose) print(sprintf("Neural Network accuracy = %1.2f%%",nn_acc))

  # create Random Forest model
  rm(pr,tab)
  if (verbose) print ("Random Forest")
  if (verbose) print (Sys.time())
  rf_model<-randomForest(as.factor(y) ~., data=data[sample,])
  if (verbose) print (Sys.time())
  pr <- predict(rf_model, newdata=data[test, ], type="class")
  # Generate the confusion matrix showing counts.
  tab<-table(data[test, ]$y, pr,
             dnn=c("Actual", "Predicted"))
  if (verbose) print (tab)
  rf_acc <- 100*sum(diag(tab))/length(test)
  if (verbose) print(sprintf("Random Forest accuracy = %1.2f%%",rf_acc))

  dfParams <- data.frame(w,stem,stop)
  dfParams$nb_acc <- nb_acc
  dfParams$svm_acc <- svm_acc
  dfParams$nn_acc <- nn_acc
  dfParams$rf_acc <- rf_acc

  return(dfParams)
}

现在我们将使用以下代码,通过不同的超参数来调用该函数:

dfResults <- TextClassification("tfidf",verbose=1) # tf-idf, no stemming
dfResults<-rbind(dfResults,TextClassification("tf",verbose=1)) # tf, no stemming
dfResults<-rbind(dfResults,TextClassification("binary",verbose=1)) # binary, no stemming

dfResults<-rbind(dfResults,TextClassification("tfidf",1,verbose=1)) # tf-idf, stemming
dfResults<-rbind(dfResults,TextClassification("tf",1,verbose=1)) # tf, stemming
dfResults<-rbind(dfResults,TextClassification("binary",1,verbose=1)) # binary, stemming

dfResults<-rbind(dfResults,TextClassification("tfidf",0,1,verbose=1)) # tf-idf, no stemming, remove stopwords
dfResults<-rbind(dfResults,TextClassification("tf",0,1,verbose=1)) # tf, no stemming, remove stopwords
dfResults<-rbind(dfResults,TextClassification("binary",0,1,verbose=1)) # binary, no stemming, remove stopwords

dfResults<-rbind(dfResults,TextClassification("tfidf",1,1,verbose=1)) # tf-idf, stemming, remove stopwords
dfResults<-rbind(dfResults,TextClassification("tf",1,1,verbose=1)) # tf, stemming, remove stopwords
dfResults<-rbind(dfResults,TextClassification("binary",1,1,verbose=1)) # binary, stemming, remove stopwords

dfResults[, "best_acc"] <- apply(dfResults[, c("nb_acc","svm_acc","nn_acc","rf_acc")], 1, max)
dfResults <- dfResults[order(-dfResults$best_acc),]
dfResults

strResult <- sprintf("Best accuracy score was %1.2f%%. Hyper-parameters: ",dfResults[1,"best_acc"])
strResult <- paste(strResult,dfResults[1,"w"],",",sep="")
strResult <- paste(strResult,
                   ifelse(dfResults[1,"stem"] == 0,"no stemming,","stemming,"))
strResult <- paste(strResult,
                   ifelse(dfResults[1,"stop"] == 0,"no stop word processing,","removed stop words,"))
if (dfResults[1,"best_acc"] == dfResults[1,"nb_acc"]){
  strResult <- paste(strResult,"Naive Bayes model")
} else if (dfResults[1,"best_acc"] == dfResults[1,"svm_acc"]){
  strResult <- paste(strResult,"SVM model")
} else if (dfResults[1,"best_acc"] == dfResults[1,"nn_acc"]){
  strResult <- paste(strResult,"Neural Network model")
}else if (dfResults[1,"best_acc"] == dfResults[1,"rf_acc"]){
  strResult <- paste(strResult,"Random Forest model")
}

print (strResult)

对于每种超参数组合,脚本会将四种机器学习算法中的最佳得分保存在best_acc字段中。训练完成后,我们可以查看结果:

> dfResults
 w stem stop nb_acc svm_acc nn_acc rf_acc best_acc
12 binary    1    1   86.06   95.24   90.52   94.26     95.24
9  binary    0    1   87.71   95.15   90.52   93.72     95.15
10 tfidf     1    1   91.99   95.15   91.05   94.17     95.15
3  binary    0    0   85.98   95.01   90.29   93.99     95.01
6  binary    1    0   84.59   95.01   90.34   93.63     95.01
7  tfidf     0    1   91.27   94.43   94.79   93.54     94.79
11 tf        1    1   77.47   94.61   92.30   94.08     94.61
4  tfidf     1    0   92.25   94.57   90.96   93.99     94.57
5  tf        1    0   75.11   94.52   93.46   93.90     94.52
1  tfidf     0    0   91.54   94.26   91.59   93.23     94.26
2  tf        0    0   75.82   94.03   91.54   93.59     94.03
8  tf        0    1   78.14   94.03   91.63   93.68     94.03

> print (strResult)
[1] "Best accuracy score was 95.24%. Hyper-parameters: binary, stemming, removed stop words, SVM model"

结果按最佳结果排序,所以我们可以看到我们的最佳准确率整体为95.24%。训练这么多模型的原因是,对于传统的自然语言处理任务,没有一个适用于大多数情况的固定公式,因此你应该尝试多种预处理和不同算法的组合,就像我们在这里所做的那样。例如,如果你在线搜索文本分类的示例,你可能会找到一个示例,建议使用 tf-idf 和朴素贝叶斯。然而,在这里,我们可以看到它是表现最差的模型之一。

深度学习文本分类

之前的代码运行了 48 种传统机器学习算法,针对不同的超参数对数据进行了处理。现在,是时候看看我们能否找到一个表现优于它们的深度学习模型了。第一个深度学习模型位于Chapter7/classify_keras1.R。代码的第一部分加载了数据。Reuters 数据集中的词项按其出现频率(在训练集中的频率)进行排名,max_features参数控制模型中将使用多少个不同的词项。我们将此参数设置为词汇表中的条目数,以便使用所有的词项。maxlen 参数控制输入序列的长度,所有序列必须具有相同的长度。如果序列长度超过 maxlen 变量,则会被截断;如果序列较短,则会填充至 maxlen 长度。我们将其设置为 250,这意味着我们的深度学习模型期望每个实例的输入为 250 个词项:

library(keras)

set.seed(42)
word_index <- dataset_reuters_word_index()
max_features <- length(word_index)
maxlen <- 250
skip_top = 0

reuters <- dataset_reuters(num_words = max_features,skip_top = skip_top)
c(c(x_train, y_train), c(x_test, y_test)) %<-% reuters
x_train <- pad_sequences(x_train, maxlen = maxlen)
x_test <- pad_sequences(x_test, maxlen = maxlen)
x_train <- rbind(x_train,x_test)
y_train <- c(y_train,y_test)
table(y_train)

y_train[y_train!=3] <- 0
y_train[y_train==3] <- 1
table(y_train)

代码的下一部分构建了模型:

model <- keras_model_sequential() %>%
  layer_embedding(input_dim = max_features, output_dim = 16,input_length = maxlen) %>%
  layer_flatten() %>%
  layer_dropout(rate = 0.25) %>% 
  layer_dense(units = 16, activation = 'relu') %>%
  layer_dropout(rate = 0.5) %>% 
  layer_dense(units = 16, activation = 'relu') %>%
  layer_dropout(rate = 0.5) %>% 
  layer_dense(units = 1, activation = "sigmoid")

model %>% compile(
  optimizer = "rmsprop",
  loss = "binary_crossentropy",
  metrics = c("acc")
)
summary(model)
history <- model %>% fit(
  x_train, y_train,
  epochs = 5,
  batch_size = 32,
  validation_split = 0.2
)

这段代码中唯一我们之前没见过的是layer_embedding。它接收输入并创建一个嵌入层,为每个输入词项生成一个向量。我们将在下一节更详细地描述词向量。需要注意的另一点是,我们没有对文本进行预处理或创建任何特征——我们只是将词汇索引输入,并让深度学习算法自行处理。以下是模型训练过程中的脚本输出:

Train on 8982 samples, validate on 2246 samples
Epoch 1/5
8982/8982 [==============================] - 3s 325us/step - loss: 0.4953 - acc: 0.7674 - val_loss: 0.2332 - val_acc: 0.9274
Epoch 2/5
8982/8982 [==============================] - 3s 294us/step - loss: 0.2771 - acc: 0.9235 - val_loss: 0.1990 - val_acc: 0.9394
Epoch 3/5
8982/8982 [==============================] - 3s 297us/step - loss: 0.2150 - acc: 0.9414 - val_loss: 0.1975 - val_acc: 0.9497
Epoch 4/5
8982/8982 [==============================] - 3s 282us/step - loss: 0.1912 - acc: 0.9515 - val_loss: 0.2118 - val_acc: 0.9461
Epoch 5/5
8982/8982 [==============================] - 3s 280us/step - loss: 0.1703 - acc: 0.9584 - val_loss: 0.2490 - val_acc: 0.9466

尽管代码很简单,但我们在经过仅三次训练周期后,在验证集上的准确率达到了 94.97%,仅比最好的传统 NLP 方法少了 0.27%。现在,是时候更详细地讨论词向量了。

词向量

深度学习不是将文本数据表示为词袋模型,而是将其表示为词向量或嵌入。向量/嵌入不过是表示一个词的数字序列。你可能已经听说过流行的词向量,例如 Word2Vec 和 GloVe。Word2Vec 模型是由谷歌发明的(Mikolov, Tomas, et al. Efficient estimation of word representations in vector space. arXiv preprint arXiv:1301.3781 (2013))。在他们的论文中,提供了一些示例,展示了这些词向量具有某种神秘和奇妙的特性。如果你取“King”一词的向量,减去“Man”一词的向量,再加上“Man”一词的向量,你会得到一个接近“Queen”一词向量的值。其他相似性也存在,例如:

  • vector(‘King’) - vector(‘Man’) + vector(‘Woman’) = vector(‘Queen’)

  • vector(‘Paris’) - vector(‘France’) + vector(‘Italy’) = vector(‘Rome’)

如果这是你第一次接触 Word2Vec,那么你可能会对它感到有些惊讶。我知道我当时是!这些示例暗示着词向量理解语言,那么我们是否已经解决了自然语言处理的问题呢?答案是否定的——我们距离这个目标还很远。词向量是从文本文件的集合中学习得到的。实际上,我们深度学习模型中的第一层就是嵌入层,它为词语创建了一个向量空间。我们再来看一下Chapter7/classify_keras.R中的一些代码:

library(keras)

word_index <- dataset_reuters_word_index()
max_features <- length(word_index)
max_features
[1] 30979
.......

model <- keras_model_sequential() %>%
 layer_embedding(input_dim = max_features, output_dim = 16,input_length = maxlen) %>%
.......

summary(model)
_______________________________________________________________________________________
Layer (type)                Output Shape         Param # 
=======================================================================================
embedding_1 (Embedding)     (None, 150, 16)      495664
.......

max_features的值是30979,也就是说,我们有30979个独特的特征。这些特征是标记,或者说是词。在传统的文本分类中,我们几乎有相同数量的独特标记(30538)。这两个数字之间的差异并不重要;它是由于两种方法中使用的不同分词过程,即文档如何被切分成标记。嵌入层有495664个参数,即30,979 x 16,也就是说,每个独特的特征/标记由一个16维的向量表示。深度学习算法学习到的词向量或嵌入将具有前面提到的一些特性,例如:

  • 同义词(意义相同的两个词)会有非常相似的词向量

  • 来自同一语义集合的词语会聚集在一起(例如,颜色、星期几、汽车品牌等)。

  • 相关词语之间的向量空间可以表示这些词语之间的关系(例如,w(国王) – w(皇后)的性别关系)

嵌入层基于词语及其周围的词语来创建词向量/嵌入。词向量之所以具有这些特性,归结于一个简单的事实,可以用 1957 年英国语言学家约翰·弗斯的名言来总结:

“你可以通过一个词周围的词语来了解它的含义。”

深度学习算法通过观察周围的词汇来学习每个单词的向量,因此可以学习到一些上下文。当它看到King这个词时,周围的某些词可能会暗示出性别信息,例如,“The King picked up his sword。”另一句话可能是“The Queen looked in her mirror。”KingQueen的词向量在数据中从周围的词汇中学习到了一些潜在的性别成分。但需要意识到的是,深度学习算法并不理解性别是什么,或者它适用于什么样的实体。即便如此,词向量仍然比词袋方法有了很大的改进,因为词袋方法无法识别不同标记之间的关系。使用词向量还意味着我们不必丢弃稀疏词条。最后,随着唯一标记数量的增加,处理它们比词袋方法更加高效。

我们将在第九章《异常检测与推荐系统》中再次讨论嵌入,当我们在自编码器中使用它们时。现在,我们已经了解了一些传统机器学习和深度学习方法来解决这个问题,接下来是时候更详细地比较它们了。

比较传统文本分类和深度学习

传统的文本分类执行了多个预处理步骤,包括词干提取、停用词处理和特征生成(tf-idf,tf 或二进制)。而深度学习文本分类不需要这些预处理。你可能之前听过各种关于这一点的解释:

  • 深度学习可以自动学习特征,因此不需要手动创建特征

  • 深度学习算法在 NLP 任务中所需的预处理远少于传统的文本分类方法

这确实有一定的道理,但这并没有回答为什么我们在传统文本分类中需要复杂的特征生成。传统文本分类中需要预处理的一个主要原因是为了克服一个根本性的问题。

对于一些传统的 NLP 方法(例如分类),文本预处理不仅仅是为了创建更好的特征。它也是必要的,因为词袋表示法会产生一个稀疏的高维数据集。大多数机器学习算法在处理这样的数据集时会遇到问题,这意味着我们必须在应用机器学习算法之前减少数据的维度。适当的文本预处理是这一过程的关键,确保相关数据不会被丢弃。

对于传统的文本分类,我们使用了一种叫做词袋模型的方法。这本质上是对每个标记(单词)进行独热编码。每一列代表一个单独的标记,每个单元格的值是以下之一:

  • tf-idf词频,逆文档频率)用于该标记

  • 词频,也就是该标记在文档/实例中出现的次数

  • 一个二进制标志,也就是说,如果该标记出现在该文档/实例中,则为 1;否则为 0

你可能以前没听说过tf-idf。它通过计算标记在文档中的词频(tf)(例如该标记在文档中出现的次数),除以该标记在整个语料库中的出现次数的对数(idf),来衡量标记的重要性。语料库是所有文档的集合。tf部分衡量标记在单个文档中的重要性,而idf衡量该标记在所有文档中的独特性。如果标记在文档中出现多次,但也在其他文档中出现多次,那么它不太可能对文档分类有用。如果该标记只出现在少数几个文档中,那么它可能是一个对分类任务有价值的特征。

我们的传统文本分类方法也使用了词干提取(stemming)和处理停用词(stop-words)。实际上,我们在传统文本分类中取得的最佳结果使用了这两种方法。词干提取尝试将单词还原为它们的词干或根形式,从而减少词汇表的大小。它还意味着具有相同意义但动词时态或名词形式不同的单词会标准化为相同的标记。以下是一个词干提取的例子。请注意,输入词中的 6 个/7 个词的输出值是相同的:

library(corpus)
text <- "love loving lovingly loved lover lovely love"
text_tokens(text, stemmer = "en") # english stemmer
[[1]]
[1] "love" "love" "love" "love" "lover" "love" "love" 

停用词是指在一种语言的大多数文档中都会出现的常见词汇。它们在大多数文档中出现的频率非常高,以至于几乎永远不会对机器学习有用。以下示例展示了英语语言中的停用词列表:

library(tm)
> stopwords()
 [1] "i" "me" "my" "myself" "we" "our" 
 [7] "ours" "ourselves" "you" "your" "yours" "yourself" 
 [13] "yourselves" "he" "him" "his" "himself" "she"
 [19] "her" "hers" "herself" "it" "its" "itself" 
 [25] "they" "them" "their" "theirs" "themselves" "what" 
.........

我们在传统自然语言处理(NLP)中要讨论的最后一部分是它如何处理稀疏词项。回想一下,传统的 NLP 采用词袋模型(bag-of-words),其中每个唯一的标记(token)会得到一个单独的列。对于大量文档集合来说,将会有成千上万个唯一的标记,而由于大多数标记不会出现在单个文档中,这就导致了非常稀疏的表示,也就是说,大多数单元格都是空的。我们可以通过查看classify_text.R中的一些代码,稍作修改,然后查看dtmdtm2变量来验证这一点:

library(tm)
df <- read.csv("../data/reuters.train.tab", sep="\t", stringsAsFactors = FALSE)
df2 <- read.csv("../data/reuters.test.tab", sep="\t", stringsAsFactors = FALSE)
df <- rbind(df,df2)

df[df$y!=3,]$y<-0
df[df$y==3,]$y<-1
rm(df2)

corpus <- Corpus(DataframeSource(data.frame(df[, 2])))
corpus <- tm_map(corpus, content_transformer(tolower))

dtm <- DocumentTermMatrix(corpus,control=list(weighting=weightBin))

# keep terms that cover 95% of the data
dtm2<-removeSparseTerms(dtm, 0.95)

dtm
<<DocumentTermMatrix (documents: 11228, terms: 30538)>>
Non-/sparse entries: 768265/342112399
Sparsity : 100%
Maximal term length: 24
Weighting : binary (bin)

dtm2
<<DocumentTermMatrix (documents: 11228, terms: 230)>>
Non-/sparse entries: 310275/2272165
Sparsity : 88%
Maximal term length: 13
Weighting : binary (bin)

我们可以看到我们的第一个文档-词项矩阵(dtm)有 11,228 个文档和 30,538 个独特的词汇。在这个文档-词项矩阵中,只有 768,265 个(0.22%)单元格有值。大多数机器学习算法处理这样一个高维度稀疏数据框架时都会遇到困难。如果你尝试在一个有 30,538 维的数据框上使用这些机器学习算法(例如,SVM、随机森林、朴素贝叶斯),它们在 R 中无法运行(我试过了!)。这是传统 NLP 中的一个已知问题,所以在 NLP 库中有一个函数(removeSparseTerms)可以从文档-词项矩阵中去除稀疏词项。这个函数会去掉那些大部分单元格为空的列。我们可以看到其效果,第二个文档-词项矩阵仅有 230 个独特的词汇,且 310,275 个(12%)单元格有值。这个数据集依然相对稀疏,但它已转化为适合机器学习的格式。

这突显了传统 NLP 方法的问题:词袋模型方法创建了一个非常稀疏的高维数据集,而这个数据集不能被机器学习算法使用。因此,你需要去除一些维度,这就导致在我们的示例中,单元格中的有值数量从 768,265 减少到 310,275。我们在应用任何机器学习之前就丢弃了几乎 60%的数据!这也解释了为什么在传统 NLP 中使用文本预处理步骤,如词干提取和停用词移除。词干提取有助于减少词汇量,并通过将许多词汇的变体合并为一个形式来标准化术语。

通过合并变体,意味着它们更有可能在数据筛选时存活下来。我们处理停用词的理由则恰恰相反:如果我们不去除停用词,这些词可能会在去除稀疏词项后被保留下来。在tm包中的stopwords()函数里有 174 个停用词。如果减少后的数据集中有许多这些词,它们可能不会作为预测变量发挥作用,因为它们在文档中普遍存在。

同样值得注意的是,在自然语言处理(NLP)领域,这个数据集非常小。我们只有 11,228 个文档和 30,538 个独特的词汇。一个更大的语料库(文本文件集合)可能包含有五十万个独特的词汇。为了将词汇的数量减少到一个可以在 R 中处理的水平,我们不得不丢弃更多的数据。

当我们使用深度学习方法进行 NLP 时,我们将数据表示为词向量/嵌入,而不是采用传统 NLP 中的词袋方法。这种方法更加高效,因此无需预处理数据来去除常见词汇、简化词形或在应用深度学习算法之前减少词汇数量。我们唯一需要做的就是选择嵌入大小和处理每个实例时最大令牌数的长度。这是必要的,因为深度学习算法不能将可变长度的序列作为输入传递到一个层次。当实例的令牌数量超过最大长度时,它们会被截断;当实例的令牌数量少于最大长度时,它们会被填充。

在这一切完成后,你可能会想,如果传统 NLP 方法丢弃了 60%的数据,为什么深度学习算法并没有显著超过传统 NLP 方法?原因有几个:

  • 数据集很小。如果我们拥有更多的数据,深度学习方法的提升速度将快于传统 NLP 方法。

  • 某些 NLP 任务,如文档分类和情感分析,依赖于一小部分特定的词汇。例如,为了区分体育新闻和财经新闻,也许 50 个精选的词汇就足以达到 90%以上的准确率。回想一下传统文本分类方法中用于去除稀疏词汇的功能——之所以有效,是因为它假设(并且正确)非稀疏词汇对于机器学习算法来说是有用的特征。

  • 我们运行了 48 个机器学习算法,仅有一个深度学习方法,而且它相对简单!我们很快会遇到一些方法,它们在性能上超过了传统的 NLP 方法。

本书实际上只是触及了传统 NLP 方法的表面。关于这个话题已经有整本书的内容。研究这些方法的目的是展示它们的脆弱性。深度学习方法更容易理解,且设置远少于传统方法。它不涉及文本的预处理或基于加权(如 tf-idf)来创建特征。即便如此,我们的第一个深度学习方法也与传统文本分类中 48 个模型中的最佳模型相差无几。

高级深度学习文本分类

我们的基本深度学习模型比传统的机器学习方法要简单得多,但其性能并不完全优越。本节将探讨一些深度学习中用于文本分类的高级技术。接下来的章节将解释多种不同的方法,并侧重于代码示例,而非过多的理论解释。如果你对更详细的内容感兴趣,可以参考 Goodfellow、Bengio 和 Courville 的书《Deep Learning》(Goodfellow, Ian, et al. Deep learning. Vol. 1. Cambridge: MIT Press, 2016.)。另一本很好的参考书是 Yoav Goldberg 的书《Neural network methods for natural language processing》,它涵盖了深度学习中的自然语言处理(NLP)。

1D 卷积神经网络模型

我们已经看到,传统 NLP 方法中的词袋模型忽视了句子结构。考虑在下表中的四条电影评论上应用情感分析任务:

Id句子评分(1=推荐,0=不推荐)
1这部电影非常好1
2这部电影不好0
3这部电影不太好0
4这部电影不好1

如果我们将其表示为词袋模型,并计算词频,我们将得到以下输出:

Id电影非常
10111011
20111110
30111111
41011110

在这个简单的例子中,我们可以看到词袋方法的一些问题,我们丢失了否定词(not)与形容词(goodbad)之间的关系。为了解决这个问题,传统 NLP 方法可能会使用二元词组(bigrams),也就是说,不使用单一的单词作为标记,而是使用两个单词作为标记。现在,在第二个例子中,not good将作为一个标记,这样机器学习算法更有可能识别它。然而,第三个例子(not very good)仍然存在问题,因为我们会得到not veryvery good两个标记。这些仍然是模糊的,not very暗示着负面情感,而very good则暗示着正面情感。我们可以尝试更高阶的 n-gram,但这会进一步加剧我们在前一节看到的稀疏性问题。

词向量或嵌入也面临相同的问题。我们需要某种方法来处理词序列。幸运的是,深度学习算法中有一些层可以处理顺序数据。我们已经在第五章中看到过一种,这一章讨论了卷积神经网络在图像分类中的应用。回想一下,这些是移动于图像上的 2D 补丁,用来识别模式,如对角线或边缘。类似地,我们可以将 1D 卷积神经网络应用于词向量。以下是使用 1D 卷积神经网络层来解决相同文本分类问题的示例。代码位于Chapter7/classify_keras2.R。我们只展示模型架构的代码,因为这与Chapter7/classify_keras1.R中的代码唯一的不同:

model <- keras_model_sequential() %>%
  layer_embedding(input_dim = max_features, output_dim = 16,input_length = maxlen) %>%
  layer_dropout(rate = 0.25) %>%
  layer_conv_1d(64,5, activation = "relu") %>%
  layer_dropout(rate = 0.25) %>%
  layer_max_pooling_1d() %>%
  layer_flatten() %>%
  layer_dense(units = 50, activation = 'relu') %>%
  layer_dropout(rate = 0.6) %>%
  layer_dense(units = 1, activation = "sigmoid")

我们可以看到,这与我们在图像数据中看到的模式相同;我们有一个卷积层,后面跟着一个最大池化层。这里有 64 个卷积层,length=5,因此这些层能够学习数据中的局部模式。以下是模型训练的输出:

Train on 8982 samples, validate on 2246 samples
Epoch 1/5
8982/8982 [==============================] - 13s 1ms/step - loss: 0.3020 - acc: 0.8965 - val_loss: 0.1909 - val_acc: 0.9470
Epoch 2/5
8982/8982 [==============================] - 13s 1ms/step - loss: 0.1980 - acc: 0.9498 - val_loss: 0.1816 - val_acc: 0.9537
Epoch 3/5
8982/8982 [==============================] - 12s 1ms/step - loss: 0.1674 - acc: 0.9575 - val_loss: 0.2233 - val_acc: 0.9368
Epoch 4/5
8982/8982 [==============================] - 12s 1ms/step - loss: 0.1587 - acc: 0.9606 - val_loss: 0.1787 - val_acc: 0.9573
Epoch 5/5
8982/8982 [==============================] - 12s 1ms/step - loss: 0.1513 - acc: 0.9628 - val_loss: 0.2186 - val_acc: 0.9408

这个模型比我们之前的深度学习模型有所改进;它在第四个周期时取得了 95.73%的准确率。这比传统的 NLP 方法提高了 0.49%,这是一个显著的进步。接下来,我们将介绍其他也关注序列匹配的方法。我们将从循环神经网络RNNs)开始。

循环神经网络模型

到目前为止,我们所见的深度学习网络没有记忆的概念。每一条新信息都被视为原子信息,与已经发生的事情没有关联。但在时间序列和文本分类中,特别是在情感分析中,序列是非常重要的。在上一节中,我们看到词的结构和顺序是至关重要的,我们使用卷积神经网络(CNN)来解决这个问题。虽然这种方法有效,但它并没有完全解决问题,因为我们仍然需要选择一个过滤器大小,这限制了层的范围。循环神经网络(RNN)是用来解决这个问题的深度学习层。它们是带有反馈回路的网络,允许信息流动,因此能够记住重要特征:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/50985a6f-072d-41b1-ad78-6a456fa9d8f1.png

图 7.1:一个循环神经网络

在前面的图中,我们可以看到一个循环神经网络的示例。每一条信息(X[o], X[1], X[2])都被输入到一个节点,该节点预测y变量。预测值也被传递到下一个节点作为输入,从而保留了一些序列信息。

我们的第一个 RNN 模型位于Chapter7/classify_keras3.R。我们需要调整模型的一些参数:我们必须将使用的特征数减少到 4,000,将最大长度调整为 100,并删除最常见的 100 个标记。我们还需要增加嵌入层的大小至 32,并运行 10 个周期:

word_index <- dataset_reuters_word_index()
max_features <- length(word_index)
max_features <- 4000
maxlen <- 100
skip_top = 100

........

model <- keras_model_sequential() %>%
  layer_embedding(input_dim = max_features, output_dim = 32,input_length = maxlen) %>%
  layer_spatial_dropout_1d(rate = 0.25) %>%
  layer_simple_rnn(64,activation = "relu", dropout=0.2) %>%
  layer_dense(units = 1, activation = "sigmoid")

........

history <- model %>% fit(
  x_train, y_train,
  epochs = 10,
  batch_size = 32,
  validation_split = 0.2
)

以下是模型训练的输出:

Train on 8982 samples, validate on 2246 samples
Epoch 1/10
8982/8982 [==============================] - 4s 409us/step - loss: 0.5289 - acc: 0.7848 - val_loss: 0.3162 - val_acc: 0.9078
Epoch 2/10
8982/8982 [==============================] - 4s 391us/step - loss: 0.2875 - acc: 0.9098 - val_loss: 0.2962 - val_acc: 0.9305
Epoch 3/10
8982/8982 [==============================] - 3s 386us/step - loss: 0.2496 - acc: 0.9267 - val_loss: 0.2487 - val_acc: 0.9234
Epoch 4/10
8982/8982 [==============================] - 3s 386us/step - loss: 0.2395 - acc: 0.9312 - val_loss: 0.2709 - val_acc: 0.9332
Epoch 5/10
8982/8982 [==============================] - 3s 381us/step - loss: 0.2259 - acc: 0.9336 - val_loss: 0.2360 - val_acc: 0.9270
Epoch 6/10
8982/8982 [==============================] - 3s 381us/step - loss: 0.2182 - acc: 0.9348 - val_loss: 0.2298 - val_acc: 0.9341
Epoch 7/10
8982/8982 [==============================] - 3s 383us/step - loss: 0.2129 - acc: 0.9380 - val_loss: 0.2114 - val_acc: 0.9390
Epoch 8/10
8982/8982 [==============================] - 3s 382us/step - loss: 0.2128 - acc: 0.9341 - val_loss: 0.2306 - val_acc: 0.9359
Epoch 9/10
8982/8982 [==============================] - 3s 378us/step - loss: 0.2053 - acc: 0.9382 - val_loss: 0.2267 - val_acc: 0.9368
Epoch 10/10
8982/8982 [==============================] - 3s 385us/step - loss: 0.2031 - acc: 0.9389 - val_loss: 0.2204 - val_acc: 0.9368

最佳验证准确率出现在第 7 个训练周期,达到了 93.90%的准确率,虽然不如 CNN 模型。简单 RNN 模型的一个问题是,当不同信息之间的间隔变大时,很难保持上下文。接下来我们将讨论一个更复杂的模型,即 LSTM 模型。

长短期记忆(LSTM)模型

LSTM(长短期记忆网络)被设计用来学习长期依赖关系。与 RNN 类似,它们是链式结构,并且有四个内部神经网络层。它们将状态分为两部分,一部分管理短期状态,另一部分添加长期状态。LSTM 具有门控机制,用于控制记忆的存储方式。输入门控制应该将输入的哪部分加入到长期记忆中。遗忘门控制应该遗忘长期记忆中的哪部分。最后一个门,即输出门,控制长期记忆中应该包含哪部分内容。以上是 LSTM 的简要描述——想了解更多细节,参考colah.github.io/posts/2015-08-Understanding-LSTMs/

我们的 LSTM 模型代码在Chapter7/classify_keras4.R中。模型的参数为最大长度=150,嵌入层大小=32,模型训练了 10 个周期:

word_index <- dataset_reuters_word_index()
max_features <- length(word_index)
maxlen <- 150
skip_top = 0

.........

model <- keras_model_sequential() %>%
  layer_embedding(input_dim = max_features, output_dim = 32,input_length = maxlen) %>%
  layer_dropout(rate = 0.25) %>%
  layer_lstm(128,dropout=0.2) %>%
  layer_dense(units = 1, activation = "sigmoid")

.........

history <- model %>% fit(
  x_train, y_train,
  epochs = 10,
  batch_size = 32,
  validation_split = 0.2
)

以下是模型训练的输出:

Train on 8982 samples, validate on 2246 samples
Epoch 1/10
8982/8982 [==============================] - 25s 3ms/step - loss: 0.3238 - acc: 0.8917 - val_loss: 0.2135 - val_acc: 0.9394
Epoch 2/10
8982/8982 [==============================] - 26s 3ms/step - loss: 0.2465 - acc: 0.9206 - val_loss: 0.1875 - val_acc: 0.9470
Epoch 3/10
8982/8982 [==============================] - 26s 3ms/step - loss: 0.1815 - acc: 0.9493 - val_loss: 0.2577 - val_acc: 0.9408
Epoch 4/10
8982/8982 [==============================] - 26s 3ms/step - loss: 0.1691 - acc: 0.9521 - val_loss: 0.1956 - val_acc: 0.9501
Epoch 5/10
8982/8982 [==============================] - 25s 3ms/step - loss: 0.1658 - acc: 0.9507 - val_loss: 0.1850 - val_acc: 0.9537
Epoch 6/10
8982/8982 [==============================] - 25s 3ms/step - loss: 0.1658 - acc: 0.9508 - val_loss: 0.1764 - val_acc: 0.9510
Epoch 7/10
8982/8982 [==============================] - 26s 3ms/step - loss: 0.1659 - acc: 0.9522 - val_loss: 0.1884 - val_acc: 0.9466
Epoch 8/10
8982/8982 [==============================] - 26s 3ms/step - loss: 0.1548 - acc: 0.9556 - val_loss: 0.1900 - val_acc: 0.9479
Epoch 9/10
8982/8982 [==============================] - 26s 3ms/step - loss: 0.1562 - acc: 0.9548 - val_loss: 0.2035 - val_acc: 0.9461
Epoch 10/10
8982/8982 [==============================] - 26s 3ms/step - loss: 0.1508 - acc: 0.9567 - val_loss: 0.2052 - val_acc: 0.9470

最佳验证准确率出现在第 5 个训练周期,达到了 95.37%的准确率,这是相较于简单 RNN 模型的一大进步,尽管仍然不如 CNN 模型好。接下来我们将介绍 GRU 单元,它与 LSTM 有相似的概念。

门控循环单元(GRU)模型

**门控循环单元(GRUs)**与 LSTM 单元相似,但更简单。它们有一个门控机制,结合了 LSTM 中的遗忘门和输入门,并且没有输出门。虽然 GRU 比 LSTM 更简单,因此训练速度更快,但是否优于 LSTM 仍然存在争议,因为研究结果尚无定论。因此,建议同时尝试两者,因为不同任务的结果可能会有所不同。我们的 GRU 模型代码在Chapter7/classify_keras5.R中。模型的参数为最大长度=150,嵌入层大小=32,模型训练了 10 个周期:

word_index <- dataset_reuters_word_index()
max_features <- length(word_index)
maxlen <- 250
skip_top = 0

...........

model <- keras_model_sequential() %>%
  layer_embedding(input_dim = max_features, output_dim = 32,input_length = maxlen) %>%
  layer_dropout(rate = 0.25) %>%
  layer_gru(128,dropout=0.2) %>%
  layer_dense(units = 1, activation = "sigmoid")

...........

history <- model %>% fit(
  x_train, y_train,
  epochs = 10,
  batch_size = 32,
  validation_split = 0.2
)

以下是模型训练的输出:

Train on 8982 samples, validate on 2246 samples
Epoch 1/10
8982/8982 [==============================] - 35s 4ms/step - loss: 0.3231 - acc: 0.8867 - val_loss: 0.2068 - val_acc: 0.9372
Epoch 2/10
8982/8982 [==============================] - 35s 4ms/step - loss: 0.2084 - acc: 0.9381 - val_loss: 0.2065 - val_acc: 0.9421
Epoch 3/10
8982/8982 [==============================] - 35s 4ms/step - loss: 0.1824 - acc: 0.9454 - val_loss: 0.1711 - val_acc: 0.9501
Epoch 4/10
8982/8982 [==============================] - 35s 4ms/step - loss: 0.1656 - acc: 0.9515 - val_loss: 0.1719 - val_acc: 0.9550
Epoch 5/10
8982/8982 [==============================] - 35s 4ms/step - loss: 0.1569 - acc: 0.9551 - val_loss: 0.1668 - val_acc: 0.9541
Epoch 6/10
8982/8982 [==============================] - 35s 4ms/step - loss: 0.1477 - acc: 0.9570 - val_loss: 0.1667 - val_acc: 0.9555
Epoch 7/10
8982/8982 [==============================] - 35s 4ms/step - loss: 0.1441 - acc: 0.9605 - val_loss: 0.1612 - val_acc: 0.9581
Epoch 8/10
8982/8982 [==============================] - 36s 4ms/step - loss: 0.1361 - acc: 0.9611 - val_loss: 0.1593 - val_acc: 0.9590
Epoch 9/10
8982/8982 [==============================] - 35s 4ms/step - loss: 0.1361 - acc: 0.9620 - val_loss: 0.1646 - val_acc: 0.9568
Epoch 10/10
8982/8982 [==============================] - 35s 4ms/step - loss: 0.1306 - acc: 0.9634 - val_loss: 0.1660 - val_acc: 0.9559

最佳验证准确率出现在第 5 个训练周期,达到了 95.90%的准确率,较 LSTM 的 95.37%有所提升。实际上,这是我们迄今为止看到的最佳结果。在下一部分中,我们将讨论双向架构。

双向 LSTM 模型

我们在图 7.1中看到,RNN(以及 LSTM 和 GRU)很有用,因为它们可以向前传递信息。但是在自然语言处理任务中,回溯信息同样也很重要。例如,下面这两句话的意思是相同的:

  • 我在春天去了柏林

  • 春天我去了柏林

双向 LSTM 可以将信息从未来状态传递到当前状态。我们的双向 LSTM 模型的代码在Chapter7/classify_keras6.R中。模型的参数为最大长度=150,嵌入层的大小=32,模型训练了 10 个周期:

word_index <- dataset_reuters_word_index()
max_features <- length(word_index)
maxlen <- 250
skip_top = 0

..................

model <- keras_model_sequential() %>%
  layer_embedding(input_dim = max_features, output_dim = 32,input_length = maxlen) %>%
  layer_dropout(rate = 0.25) %>%
  bidirectional(layer_lstm(units=128,dropout=0.2)) %>%
  layer_dense(units = 1, activation = "sigmoid")

..................

history <- model %>% fit(
  x_train, y_train,
  epochs = 10,
  batch_size = 32,
  validation_split = 0.2
)

这是模型训练的输出:

Train on 8982 samples, validate on 2246 samples
Epoch 1/10
8982/8982 [==============================] - 82s 9ms/step - loss: 0.3312 - acc: 0.8834 - val_loss: 0.2166 - val_acc: 0.9377
Epoch 2/10
8982/8982 [==============================] - 87s 10ms/step - loss: 0.2487 - acc: 0.9243 - val_loss: 0.1889 - val_acc: 0.9457
Epoch 3/10
8982/8982 [==============================] - 86s 10ms/step - loss: 0.1873 - acc: 0.9464 - val_loss: 0.1708 - val_acc: 0.9519
Epoch 4/10
8982/8982 [==============================] - 82s 9ms/step - loss: 0.1685 - acc: 0.9537 - val_loss: 0.1786 - val_acc: 0.9577
Epoch 5/10
8982/8982 [==============================] - 83s 9ms/step - loss: 0.1634 - acc: 0.9531 - val_loss: 0.2094 - val_acc: 0.9310
Epoch 6/10
8982/8982 [==============================] - 82s 9ms/step - loss: 0.1567 - acc: 0.9571 - val_loss: 0.1809 - val_acc: 0.9475
Epoch 7/10
8982/8982 [==============================] - 83s 9ms/step - loss: 0.1499 - acc: 0.9575 - val_loss: 0.1652 - val_acc: 0.9555
Epoch 8/10
8982/8982 [==============================] - 83s 9ms/step - loss: 0.1488 - acc: 0.9586 - val_loss: 0.1795 - val_acc: 0.9510
Epoch 9/10
8982/8982 [==============================] - 83s 9ms/step - loss: 0.1513 - acc: 0.9567 - val_loss: 0.1758 - val_acc: 0.9555
Epoch 10/10
8982/8982 [==============================] - 83s 9ms/step - loss: 0.1463 - acc: 0.9571 - val_loss: 0.1731 - val_acc: 0.9550

最佳验证准确度是在第 4 个周期后得到的,当时我们获得了 95.77%的准确度。

堆叠双向模型

双向模型擅长从未来状态中获取信息,这些信息会影响当前状态。堆叠双向模型使我们能够像堆叠计算机视觉任务中的多个卷积层一样,堆叠多个 LSTM/GRU 层。我们的双向 LSTM 模型的代码在Chapter7/classify_keras7.R中。模型的参数为最大长度=150,嵌入层的大小=32,模型训练了 10 个周期:

word_index <- dataset_reuters_word_index()
max_features <- length(word_index)
maxlen <- 250
skip_top = 0

..................

model <- keras_model_sequential() %>%
  layer_embedding(input_dim = max_features, output_dim = 32,input_length = maxlen) %>%
  layer_dropout(rate = 0.25) %>%
  bidirectional(layer_lstm(units=32,dropout=0.2,return_sequences = TRUE)) %>%
  bidirectional(layer_lstm(units=32,dropout=0.2)) %>%
  layer_dense(units = 1, activation = "sigmoid")

..................

history <- model %>% fit(
  x_train, y_train,
  epochs = 10,
  batch_size = 32,
  validation_split = 0.2
)

这是模型训练的输出:

Train on 8982 samples, validate on 2246 samples
Epoch 1/10
8982/8982 [==============================] - 70s 8ms/step - loss: 0.2854 - acc: 0.9006 - val_loss: 0.1945 - val_acc: 0.9372
Epoch 2/10
8982/8982 [==============================] - 66s 7ms/step - loss: 0.1795 - acc: 0.9511 - val_loss: 0.1791 - val_acc: 0.9484
Epoch 3/10
8982/8982 [==============================] - 69s 8ms/step - loss: 0.1586 - acc: 0.9557 - val_loss: 0.1756 - val_acc: 0.9492
Epoch 4/10
8982/8982 [==============================] - 70s 8ms/step - loss: 0.1467 - acc: 0.9607 - val_loss: 0.1664 - val_acc: 0.9559
Epoch 5/10
8982/8982 [==============================] - 70s 8ms/step - loss: 0.1394 - acc: 0.9614 - val_loss: 0.1775 - val_acc: 0.9533
Epoch 6/10
8982/8982 [==============================] - 70s 8ms/step - loss: 0.1347 - acc: 0.9636 - val_loss: 0.1667 - val_acc: 0.9519
Epoch 7/10
8982/8982 [==============================] - 70s 8ms/step - loss: 0.1344 - acc: 0.9618 - val_loss: 0.2101 - val_acc: 0.9332
Epoch 8/10
8982/8982 [==============================] - 70s 8ms/step - loss: 0.1306 - acc: 0.9647 - val_loss: 0.1893 - val_acc: 0.9479
Epoch 9/10
8982/8982 [==============================] - 70s 8ms/step - loss: 0.1286 - acc: 0.9646 - val_loss: 0.1663 - val_acc: 0.9550
Epoch 10/10
8982/8982 [==============================] - 70s 8ms/step - loss: 0.1254 - acc: 0.9669 - val_loss: 0.1687 - val_acc: 0.9492

最佳验证准确度是在第 4 个周期后得到的,当时我们获得了 95.59%的准确度,这比我们的双向模型差,后者的准确度为 95.77%。

双向 1D 卷积神经网络模型

到目前为止,我们看到的最佳方法来自 1D 卷积神经网络模型,其准确度为 95.73%,以及门控递归单元模型,其准确度为 95.90%。以下代码将它们结合在一起!我们的双向 1D 卷积神经网络模型的代码在Chapter7/classify_keras8.R中。

模型的参数为最大长度=150,嵌入层的大小=32,模型训练了 10 个周期:

word_index <- dataset_reuters_word_index()
max_features <- length(word_index)
maxlen <- 250
skip_top = 0

..................

model <- keras_model_sequential() %>%
  layer_embedding(input_dim = max_features, output_dim = 32,input_length = maxlen) %>%
  layer_spatial_dropout_1d(rate = 0.25) %>%
  layer_conv_1d(64,3, activation = "relu") %>%
  layer_max_pooling_1d() %>%
  bidirectional(layer_gru(units=64,dropout=0.2)) %>%
  layer_dense(units = 1, activation = "sigmoid")

..................

history <- model %>% fit(
  x_train, y_train,
  epochs = 10,
  batch_size = 32,
  validation_split = 0.2
)

这是模型训练的输出:

Train on 8982 samples, validate on 2246 samples
Epoch 1/10
8982/8982 [==============================] - 26s 3ms/step - loss: 0.2891 - acc: 0.8952 - val_loss: 0.2226 - val_acc: 0.9319
Epoch 2/10
8982/8982 [==============================] - 25s 3ms/step - loss: 0.1712 - acc: 0.9505 - val_loss: 0.1601 - val_acc: 0.9586
Epoch 3/10
8982/8982 [==============================] - 26s 3ms/step - loss: 0.1651 - acc: 0.9548 - val_loss: 0.1639 - val_acc: 0.9541
Epoch 4/10
8982/8982 [==============================] - 26s 3ms/step - loss: 0.1466 - acc: 0.9582 - val_loss: 0.1699 - val_acc: 0.9550
Epoch 5/10
8982/8982 [==============================] - 26s 3ms/step - loss: 0.1391 - acc: 0.9606 - val_loss: 0.1520 - val_acc: 0.9586
Epoch 6/10
8982/8982 [==============================] - 26s 3ms/step - loss: 0.1347 - acc: 0.9626 - val_loss: 0.1626 - val_acc: 0.9550
Epoch 7/10
8982/8982 [==============================] - 27s 3ms/step - loss: 0.1332 - acc: 0.9638 - val_loss: 0.1572 - val_acc: 0.9604
Epoch 8/10
8982/8982 [==============================] - 26s 3ms/step - loss: 0.1317 - acc: 0.9629 - val_loss: 0.1693 - val_acc: 0.9470
Epoch 9/10
8982/8982 [==============================] - 26s 3ms/step - loss: 0.1259 - acc: 0.9654 - val_loss: 0.1531 - val_acc: 0.9599
Epoch 10/10
8982/8982 [==============================] - 28s 3ms/step - loss: 0.1233 - acc: 0.9665 - val_loss: 0.1653 - val_acc: 0.9573

最佳验证准确度是在第 6 个周期后得到的,当时我们获得了 96.04%的准确度,超越了所有之前的模型。

比较深度学习 NLP 架构

以下是本章所有模型的总结,按本章中的顺序排列。我们可以看到,最佳传统机器学习方法的准确度为 95.24%,被许多深度学习方法超越。虽然最佳传统机器学习方法与最佳深度学习模型之间的增量变化看起来仅为 0.80%,但它将我们的误分类示例减少了 17%,这是一个显著的相对变化:

模型准确度
最佳传统机器学习方法95.24%
简单的深度学习方法94.97%
1D 卷积神经网络模型95.73%
循环神经网络模型93.90%
长短期记忆模型95.37%
门控递归单元模型95.90%
双向 LSTM 模型95.77%
堆叠双向模型95.59%
双向 1D 卷积神经网络96.04%

总结

本章我们真的涵盖了很多内容!我们构建了一个相当复杂的传统 NLP 示例,包含了许多超参数,并在多个机器学习算法上进行了训练。它取得了 95.24% 的可靠准确率。然而,当我们更深入地研究传统 NLP 时,发现它存在一些主要问题:需要复杂的特征工程,生成稀疏的高维数据框,并且可能需要在机器学习之前丢弃大量数据。

相比之下,深度学习方法使用词向量或嵌入,这些方法更加高效,并且不需要预处理。我们介绍了多种深度学习方法,包括 1D 卷积层、循环神经网络、GRU 和 LSTM。最后,我们将前两种最佳方法结合成一种方法,并在最终模型中获得了 96.08% 的准确率,而传统的 NLP 方法准确率为 95.24%。

在下一章,我们将使用 TensorFlow 开发模型。我们将了解 TensorBoard,它可以帮助我们可视化和调试复杂的深度学习模型。我们还将学习如何使用 TensorFlow 估算器,这是使用 TensorFlow 的另一种选择。接着,我们还将学习 TensorFlow Runs,它能自动化许多超参数调优的步骤。最后,我们将探索部署深度学习模型的各种选项。

第八章:在 R 中使用 TensorFlow 构建深度学习模型

本章内容将介绍如何在 R 中使用 TensorFlow。我们已经在使用 TensorFlow 了很多,因为 Keras 是一个高级神经网络 API,它可以使用 TensorFlow、CNTK 或 Theano。在 R 中,Keras 背后使用的是 TensorFlow。尽管使用 TensorFlow 开发深度学习模型较为复杂,但 TensorFlow 中有两个有趣的包可能会被忽视:TensorFlow 估算器和 TensorFlow 运行。我们将在本章中介绍这两个包。

本章将涉及以下主题:

  • TensorFlow 简介

  • 使用 TensorFlow 构建模型

  • TensorFlow 估算器

  • TensorFlow 运行包

TensorFlow 库简介

TensorFlow 不仅是一个深度学习库,还是一个表达性强的编程语言,可以对数据执行各种优化和数学变换。虽然它主要用于实现深度学习算法,但它能够做的远不止这些。在 TensorFlow 中,程序通过计算图表示,数据则以 tensors 的形式存储。张量 是一种数据数组,所有元素具有相同的数据类型,张量的秩是指其维度的数量。由于张量中的所有数据必须是相同类型的,因此它们与 R 矩阵更为相似,而不是数据框。

下面是不同秩的张量示例:

library(tensorflow)

> # tensor of rank-0
> var1 <- tf$constant(0.1)
> print(var1)
Tensor("Const:0", shape=(), dtype=float32)

> sess <- tf$InteractiveSession()
T:\src\github\tensorflow\tensorflow\core\common_runtime\gpu\gpu_device.cc:1084] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 3019 MB memory) -> physical GPU (device: 0, name: GeForce GTX 1050 Ti, pci bus id: 0000:01:00.0, compute capability: 6.1)

> sess$run(tf$global_variables_initializer())
> var2 <- tf$constant(2.3)
> var3 = var1 + var2
> print(var1)
Tensor("Const:0", shape=(), dtype=float32)
 num 0.1

> print(var2)
Tensor("Const_1:0", shape=(), dtype=float32)
 num 2.3

> print(var3)
Tensor("Add:0", shape=(), dtype=float32)
 num 2.4

> # tensor of rank-1
> var4 <- tf$constant(4.5,shape=shape(5L))
> print(var4)
Tensor("Const_2:0", shape=(5,), dtype=float32)
 num [1:5(1d)] 4.5 4.5 4.5 4.5 4.5

> # tensor of rank-2
> var5 <- tf$constant(6.7,shape=shape(3L,3L))
> print(var5)
Tensor("Const_3:0", shape=(3, 3), dtype=float32)
 num [1:3, 1:3] 6.7 6.7 6.7 6.7 6.7 ...

一个 TensorFlow 程序包含两个部分。首先,您需要构建计算图,图中包含张量以及对这些张量的操作。定义完图之后,第二部分是创建一个 TensorFlow 会话来运行计算图。在前面的例子中,当我们第一次打印张量 a 的值时,我们只得到张量的定义,而不是其值。我们所做的只是定义了计算图的一部分。只有当我们调用 tf$InteractiveSession 时,我们才会告诉 TensorFlow 执行张量上的操作。会话负责运行计算图。

TensorFlow 程序被称为图,因为代码可以构建成图的形式。对于我们而言,这可能不太直观,因为我们在本书中构建的大多数深度学习模型都包含了层上的顺序操作。在 TensorFlow(以及 Keras 和 MXNet)中,操作的输出可以多次使用,并且可以将多个输入结合到一个操作中。

随着深度学习模型的规模不断增大,越来越难以进行可视化和调试。在一些代码块中,我们打印了显示模型层次结构的摘要,或者绘制了网络图。然而,这些工具对于调试具有千万级参数的模型帮助不大!幸运的是,TensorFlow 提供了一个可视化工具,用于总结、调试和修复 TensorFlow 程序。这个工具叫做 TensorBoard,我们将在接下来介绍它。

使用 TensorBoard 可视化深度学习网络

TensorFlow 中的计算图可以非常复杂,因此有一个叫做TensorBoard的可视化工具,用于可视化这些图并辅助调试。TensorBoard 可以绘制计算图、显示训练过程中的指标等。由于 Keras 在后台使用 TensorFlow,它也可以使用 TensorBoard。以下是启用了 TensorBoard 日志的 Keras MNIST 示例代码。该代码可以在 Chapter8/mnist_keras.R 文件夹中找到。代码的第一部分加载数据,进行预处理,并定义模型架构。希望这一部分你已经比较熟悉了:

library(keras)

mnist_data <- dataset_mnist()
xtrain <- array_reshape(mnist_data$train$x,c(nrow(mnist_data$train$x),28,28,1))
ytrain <- to_categorical(mnist_data$train$y,10)
xtrain <- xtrain / 255.0

model <- keras_model_sequential()
model %>%
  layer_conv_2d(filters=32,kernel_size=c(5,5),activation='relu',
                input_shape=c(28,28,1)) %>% 
  layer_max_pooling_2d(pool_size=c(2,2)) %>% 
  layer_dropout(rate=0.25) %>% 
  layer_conv_2d(filters=32,kernel_size=c(5,5),activation='relu') %>% 
  layer_max_pooling_2d(pool_size=c(2,2)) %>% 
  layer_dropout(rate=0.25) %>% 
  layer_flatten() %>% 
  layer_dense(units=256,activation='relu') %>% 
  layer_dropout(rate=0.4) %>% 
  layer_dense(units=10,activation='softmax')

model %>% compile(
  loss=loss_categorical_crossentropy,
  optimizer="rmsprop",metrics="accuracy"
)

要启用日志记录,在 model.fit 函数中添加一个 callbacks 参数,以告诉 Keras/TensorFlow 将事件日志记录到某个目录。以下代码将日志数据输出到 /tensorflow_logs 目录:

model %>% fit(
  xtrain,ytrain,
  batch_size=128,epochs=10,
 callbacks=callback_tensorboard("/tensorflow_logs",
 histogram_freq=1,write_images=0),
  validation_split=0.2
)
# from cmd line,run 'tensorboard --logdir /tensorflow_logs'

警告:事件日志可能会占用大量空间。在MNIST数据集上训练 5 个 epoch 时,生成了 1.75 GB 的信息。大部分数据来自于图像数据,因此你可以考虑设置write_images=0来减少日志的大小。

TensorBoard 是一个 Web 应用程序,你必须启动 TensorBoard 程序才能运行它。当模型训练完成后,按照以下步骤启动 TensorBoard Web 应用程序:

  1. 打开命令提示符并输入以下内容:
$ tensorboard --logdir /tensorflow_logs
  1. 如果 TensorBoard 启动成功,你应该会在命令提示符中看到类似以下的消息:
TensorBoard 0.4.0rc2 at http://xxxxxx:6006 (Press CTRL+C to quit)
  1. 打开一个网页浏览器,访问提供的链接。网页应该类似于以下内容:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/2c775aa3-5ea3-40d4-9bdd-b2dfaf3e5786.png

图 8.1: TensorBoard – 模型指标

  1. 上面的截图显示了训练集和验证集上的模型指标——这些指标类似于在 RStudio 中训练时显示的指标:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/c4be3ad7-5e78-4f42-9ef6-3fc0645d3843.png

图 8.2: RStudio – 模型指标

  1. 如果你点击图像选项,你将能够可视化模型中的各个层,并查看它们在训练过程中如何变化:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/c9a63540-797c-45c4-bb53-bccdd90bb7ed.png

图 8.3: TensorBoard – 可视化模型层

  1. 如果你点击图形选项,它将显示模型的计算图。你也可以将其下载为图像文件。以下是该模型的计算图:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/85274738-c4f0-4804-9588-550900583db8.png

图 8.4: TensorBoard – 计算图

其中一些部分应该是你已经熟悉的。我们可以看到卷积层、最大池化层、扁平化层、全连接层和 Dropout 层。其他部分不那么明显。作为一种更高级的抽象,Keras 处理了创建计算图时的许多复杂性。

  1. 点击直方图选项,你可以看到张量的分布随时间变化的情况:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/7d0685a1-da65-43e7-b00c-6afaa95f4abc.png

图 8.5: TensorBoard – 直方图

可以使用 TensorBoard 来调试模型。例如,可以调查梯度消失或梯度爆炸问题,查看模型的权重是否在消失到零或爆炸到无限大。TensorBoard 的功能远不止这些,如果你感兴趣,可以参考在线文档了解更多内容。

在接下来的部分,我们将使用 TensorFlow 构建一个回归模型和一个卷积神经网络。

TensorFlow 模型

在本节中,我们将使用 TensorFlow 构建一些机器学习模型。首先,我们将构建一个简单的线性回归模型,然后是一个卷积神经网络模型,类似于我们在第五章《使用卷积神经网络进行图像分类》中看到的模型。

以下代码加载 TensorFlow 库。我们可以通过设置并访问一个常量字符串值来确认它是否成功加载:

> library(tensorflow)

# confirm that TensorFlow library has loaded
> sess=tf$Session()
> hello_world <- tf$constant('Hello world from TensorFlow')
> sess$run(hello_world)
b'Hello world from TensorFlow'

使用 TensorFlow 的线性回归

在这个第一个 TensorFlow 示例中,我们将探讨回归问题。此部分的代码位于Chapter8/regression_tf.R文件夹中:

  1. 首先,我们为输入值x和输出值y创建一些虚拟数据。我们将y设为大约等于0.8 + x * 1.3。我们希望应用程序发现beta0beta1的值,分别为0.81.3
library(tensorflow)

set.seed(42)
# create 50000 x variable between 0 and 100
x_var <- runif(50000,min=0,max=1)
#y = approx(1.3x + 0.8)
y_var <- rnorm(50000,0.8,0.04) + x_var * rnorm(50000,1.3,0.05)

# y_pred = beta0 + beta1 * x
beta0 <- tf$Variable(tf$zeros(shape(1L)))
beta1 <- tf$Variable(tf$random_uniform(shape(1L), -1.0, 1.0))
y_pred <- beta0 + beta1*x_var
  1. 现在,我们设置loss函数,以便梯度下降算法可以工作:
# create our loss value which we want to minimize
loss <- tf$reduce_mean((y_pred-y_var)²)
# create optimizer
optimizer <- tf$train$GradientDescentOptimizer(0.6)
train <- optimizer$minimize(loss)
  1. 然后,我们设置 TensorFlow 会话并初始化变量。最后,我们可以运行图:
# create TensorFlow session and initialize variables
sess = tf$Session()
sess$run(tf$global_variables_initializer())

# solve the regression
for (step in 0:80) {
  if (step %% 10 == 0)
    print(sprintf("Step %1.0f:beta0=%1.4f, beta1=%1.4f",step,sess$run(beta0), sess$run(beta1)))
  sess$run(train)
}
[1] "Step 0:beta0=0.0000, beta1=-0.3244"
[1] "Step 10:beta0=1.0146, beta1=0.8944"
[1] "Step 20:beta0=0.8942, beta1=1.1236"
[1] "Step 30:beta0=0.8410, beta1=1.2229"
[1] "Step 40:beta0=0.8178, beta1=1.2662"
[1] "Step 50:beta0=0.8077, beta1=1.2850"
[1] "Step 60:beta0=0.8033, beta1=1.2932"
[1] "Step 70:beta0=0.8014, beta1=1.2967"
[1] "Step 80:beta0=0.8006, beta1=1.2983"

我们可以看到,模型成功找到了beta0beta1的值,这些值解出了函数y=beta0 + beta1*x。下一部分是一个更复杂的示例,我们将为图像分类构建一个 TensorFlow 模型。

使用 TensorFlow 的卷积神经网络

在本节中,我们将基于 MNIST 数据集构建一个 TensorFlow 模型。该代码具有与第五章《使用卷积神经网络进行图像分类》中的 Lenet 模型相似的层和参数。然而,在 TensorFlow 中构建模型的代码比在 Keras 或 MXNet 中构建模型的代码要复杂。原因之一是,程序员需要确保各层的尺寸正确对齐。在 Keras/MXNet 模型中,我们只需更改某一层的节点数即可。在 TensorFlow 中,如果我们更改一层的节点数,必须确保同时更改下一层的输入。

在某些方面,在 TensorFlow 中编程更接近我们在第三章《深度学习基础》中手写的神经网络代码。与 Keras/MXNet 在训练循环中的另一个区别是,我们需要管理批次,而不仅仅是调用要求遍历所有数据 x 次(其中 x 是一个时期)。此示例的代码位于Chapter8/mnist_tf.R文件夹中。首先,我们加载 Keras 包以获取 MNIST 数据,但我们使用 TensorFlow 训练模型。以下是代码的第一部分:

library(RSNNS) # for decodeClassLabels
library(tensorflow)
library(keras)

mnist <- dataset_mnist()
set.seed(42)

xtrain <- array_reshape(mnist$train$x,c(nrow(mnist$train$x),28*28))
ytrain <- decodeClassLabels(mnist$train$y)
xtest <- array_reshape(mnist$test$x,c(nrow(mnist$test$x),28*28))
ytest <- decodeClassLabels(mnist$test$y)
xtrain <- xtrain / 255.0
xtest <- xtest / 255.0
head(ytrain)
     0 1 2 3 4 5 6 7 8 9
[1,] 0 0 0 0 0 1 0 0 0 0
[2,] 1 0 0 0 0 0 0 0 0 0
[3,] 0 0 0 0 1 0 0 0 0 0
[4,] 0 1 0 0 0 0 0 0 0 0
[5,] 0 0 0 0 0 0 0 0 0 1
[6,] 0 0 1 0 0 0 0 0 0 0

我们使用来自 RSNNS 库的decodeClassLabels函数,因为 TensorFlow 要求一个虚拟编码矩阵,因此每个可能的类都表示为一个列,并以 0/1 的形式编码,如前面的代码输出所示。

在下一个代码块中,我们为模型的输入和输出值创建一些占位符。我们还将输入数据重塑为一个 4 阶张量,即一个 4 维数据结构。第一维(-1L)用于处理批次中的记录。接下来的两维是图像文件的维度,最后一维是通道数,即颜色数。由于我们的图像是灰度图像,因此只有 1 个通道。如果图像是彩色图像,则有 3 个通道。以下代码块创建了占位符并重塑数据:

# placeholders
x <- tf$placeholder(tf$float32, shape(NULL,28L*28L))
y <- tf$placeholder(tf$float32, shape(NULL,10L))
x_image <- tf$reshape(x, shape(-1L,28L,28L,1L))

接下来,我们将定义模型架构。我们将创建卷积块,就像之前做的那样。不过,有许多其他值需要设置。例如,在第一个卷积层中,我们必须定义形状,初始化权重,并处理偏置变量。以下是 TensorFlow 模型的代码:

# first convolution layer
conv_weights1 <- tf$Variable(tf$random_uniform(shape(5L,5L,1L,16L), -0.4, 0.4))
conv_bias1 <- tf$constant(0.0, shape=shape(16L))
conv_activ1 <- tf$nn$tanh(tf$nn$conv2d(x_image, conv_weights1, strides=c(1L,1L,1L,1L), padding='SAME') + conv_bias1)
pool1 <- tf$nn$max_pool(conv_activ1, ksize=c(1L,2L,2L,1L),strides=c(1L,2L,2L,1L), padding='SAME')

# second convolution layer
conv_weights2 <- tf$Variable(tf$random_uniform(shape(5L,5L,16L,32L), -0.4, 0.4))
conv_bias2 <- tf$constant(0.0, shape=shape(32L))
conv_activ2 <- tf$nn$relu(tf$nn$conv2d(pool1, conv_weights2, strides=c(1L,1L,1L,1L), padding='SAME') + conv_bias2)
pool2 <- tf$nn$max_pool(conv_activ2, ksize=c(1L,2L,2L,1L),strides=c(1L,2L,2L,1L), padding='SAME')

# densely connected layer
dense_weights1 <- tf$Variable(tf$truncated_normal(shape(7L*7L*32L,512L), stddev=0.1))
dense_bias1 <- tf$constant(0.0, shape=shape(512L))
pool2_flat <- tf$reshape(pool2, shape(-1L,7L*7L*32L))
dense1 <- tf$nn$relu(tf$matmul(pool2_flat, dense_weights1) + dense_bias1)

# dropout
keep_prob <- tf$placeholder(tf$float32)
dense1_drop <- tf$nn$dropout(dense1, keep_prob)

# softmax layer
dense_weights2 <- tf$Variable(tf$truncated_normal(shape(512L,10L), stddev=0.1))
dense_bias2 <- tf$constant(0.0, shape=shape(10L))

yconv <- tf$nn$softmax(tf$matmul(dense1_drop, dense_weights2) + dense_bias2)

现在,我们需要定义损失方程,定义使用的优化器(Adam),并定义准确率指标:

cross_entropy <- tf$reduce_mean(-tf$reduce_sum(y * tf$log(yconv), reduction_indices=1L))
train_step <- tf$train$AdamOptimizer(0.0001)$minimize(cross_entropy)
correct_prediction <- tf$equal(tf$argmax(yconv, 1L), tf$argmax(y, 1L))
accuracy <- tf$reduce_mean(tf$cast(correct_prediction, tf$float32))

最后,我们可以在 10 个周期内训练模型。然而,仍然存在一个复杂性,因此我们必须手动管理批次。我们获取训练所需的批次数量,并依次加载它们。如果我们的训练数据集中有 60,000 张图像,则每个周期有 469 个批次(60,000/128 = 468.75 并四舍五入为 469)。我们每次输入一个批次,并每 100 个批次输出一次指标:

sess <- tf$InteractiveSession()
sess$run(tf$global_variables_initializer())

# if you get out of memory errors when running on gpu
# then lower the batch_size
batch_size <- 128
batches_per_epoch <- 1+nrow(xtrain) %/% batch_size
for (epoch in 1:10)
{
  for (batch_no in 0:(-1+batches_per_epoch))
  {
    nStartIndex <- 1 + batch_no*batch_size
    nEndIndex <- nStartIndex + batch_size-1
    if (nEndIndex > nrow(xtrain))
      nEndIndex <- nrow(xtrain)
    xvalues <- xtrain[nStartIndex:nEndIndex,]
    yvalues <- ytrain[nStartIndex:nEndIndex,]
    if (batch_no %% 100 == 0) {
      batch_acc <- accuracy$eval(feed_dict=dict(x=xvalues,y=yvalues,keep_prob=1.0))
      print(sprintf("Epoch %1.0f, step %1.0f: training accuracy=%1.4f",epoch, batch_no, batch_acc))
    }
    sess$run(train_step,feed_dict=dict(x=xvalues,y=yvalues,keep_prob=0.5))
  }
  cat("\n")
}

这是第一轮训练的输出:

[1] "Epoch 1, step 0: training accuracy=0.0625"
[1] "Epoch 1, step 100: training accuracy=0.8438"
[1] "Epoch 1, step 200: training accuracy=0.8984"
[1] "Epoch 1, step 300: training accuracy=0.9531"
[1] "Epoch 1, step 400: training accuracy=0.8750"

训练完成后,我们可以通过计算测试集上的准确率来评估模型。同样,我们必须按批次执行此操作,以防止内存溢出错误:

# calculate test accuracy
# have to run in batches to prevent out of memory errors
batches_per_epoch <- 1+nrow(xtest) %/% batch_size
test_acc <- vector(mode="numeric", length=batches_per_epoch)
for (batch_no in 0:(-1+batches_per_epoch))
{
  nStartIndex <- 1 + batch_no*batch_size
  nEndIndex <- nStartIndex + batch_size-1
  if (nEndIndex > nrow(xtest))
    nEndIndex <- nrow(xtest)
  xvalues <- xtest[nStartIndex:nEndIndex,]
  yvalues <- ytest[nStartIndex:nEndIndex,]
  batch_acc <- accuracy$eval(feed_dict=dict(x=xvalues,y=yvalues,keep_prob=1.0))
  test_acc[batch_no+1] <- batch_acc
}
# using the mean is not totally accurate as last batch is not a complete batch
print(sprintf("Test accuracy=%1.4f",mean(test_acc)))
[1] "Test accuracy=0.9802"

我们最终获得了 0.9802 的准确率。如果将这段代码与 第五章 卷积神经网络图像分类 中的 MNIST 示例进行比较,可以发现 TensorFlow 代码更为冗长,且更容易出错。我们可以真正看到使用更高层次抽象的好处,比如 MXNet 或 Keras(它可以使用 TensorFlow 作为后端)。对于大多数深度学习应用场景,尤其是使用现有层作为构建块构建深度学习模型时,在 TensorFlow 中编写代码并没有太多好处。在这些场景中,使用 Keras 或 MXNet 更简单且更高效。

查看完这段代码后,你可能会想回到更熟悉的 Keras 和 MXNet。不过,接下来的部分将介绍 TensorFlow 估算器和 TensorFlow 运行包,这是两个非常有用的包,你应该了解它们。

TensorFlow 估算器和 TensorFlow 运行包

TensorFlow 估算器和 TensorFlow 运行包是非常适合深度学习的工具包。在本节中,我们将使用这两个包基于来自 第四章 训练深度预测模型 的流失预测数据来训练一个模型。

TensorFlow 估算器

TensorFlow 估算器 允许你使用更简洁的 API 接口来构建 TensorFlow 模型。在 R 中,tfestimators 包允许你调用这个 API。不同的模型类型包括线性模型和神经网络。可用的估算器如下:

  • linear_regressor() 用于线性回归

  • linear_classifier() 用于线性分类

  • dnn_regressor() 用于深度神经网络回归

  • dnn_classifier() 用于深度神经网络分类

  • dnn_linear_combined_regressor() 用于深度神经网络线性组合回归

  • dnn_linear_combined_classifier() 用于深度神经网络线性组合分类

估算器隐藏了创建深度学习模型的很多细节,包括构建图、初始化变量和层,并且它们还可以与 TensorBoard 集成。更多详细信息请访问 tensorflow.rstudio.com/tfestimators/。我们将使用 dnn_classifier 处理来自 第四章 训练深度预测模型 的二元分类任务的数据。以下代码位于 Chapter8/tf_estimators.R 文件夹中,演示了 TensorFlow 估算器的使用。

  1. 我们只包含特定于 TensorFlow 估算器的代码,省略了文件开头加载数据并将其拆分为训练数据和测试数据的部分:
response <- function() "Y_categ"
features <- function() predictorCols

FLAGS <- flags(
  flag_numeric("layer1", 256),
  flag_numeric("layer2", 128),
  flag_numeric("layer3", 64),
  flag_numeric("layer4", 32),
  flag_numeric("dropout", 0.2)
)
num_hidden <- c(FLAGS$layer1,FLAGS$layer2,FLAGS$layer3,FLAGS$layer4)

classifier <- dnn_classifier(
  feature_columns = feature_columns(column_numeric(predictorCols)),
  hidden_units = num_hidden,
  activation_fn = "relu",
  dropout = FLAGS$dropout,
  n_classes = 2
)

bin_input_fn <- function(data)
{
 input_fn(data, features = features(), response = response())
}
tr <- train(classifier, input_fn = bin_input_fn(trainData))
[\] Training -- loss: 22.96, step: 2742 

tr
Trained for 2,740 steps. 
Final step (plot to see history):
 mean_losses: 61.91
total_losses: 61.91
  1. 模型训练完成后,以下代码将绘制训练和验证的指标:
plot(tr)
  1. 这将生成以下图表:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/9b2eefd3-08c8-40d2-ad7d-4ee4b0250033.png

图 8.6:训练 TensorFlow 估算器模型的损失图

  1. 代码的下一部分调用 evaluate 函数来生成模型的评估指标:
# predictions <- predict(classifier, input_fn = bin_input_fn(testData))
evaluation <- evaluate(classifier, input_fn = bin_input_fn(testData))
[-] Evaluating -- loss: 37.77, step: 305

for (c in 1:ncol(evaluation))
 print(paste(colnames(evaluation)[c]," = ",evaluation[c],sep=""))
[1] "accuracy = 0.77573162317276"
[1] "accuracy_baseline = 0.603221416473389"
[1] "auc = 0.842994153499603"
[1] "auc_precision_recall = 0.887594640254974"
[1] "average_loss = 0.501933991909027"
[1] "label/mean = 0.603221416473389"
[1] "loss = 64.1636199951172"
[1] "precision = 0.803375601768494"
[1] "prediction/mean = 0.562777876853943"
[1] "recall = 0.831795573234558"
[1] "global_step = 2742"

我们可以看到,我们获得了77.57%的准确率,这实际上几乎与我们在第四章《训练深度预测模型》中,使用类似架构的 MXNet 模型所获得的准确率完全相同。dnn_classifier()函数隐藏了许多细节,因此 TensorFlow 估算器是利用 TensorFlow 强大功能处理结构化数据任务的好方法。

使用 TensorFlow 估算器创建的模型可以保存到磁盘,并在以后加载。model_dir()函数显示模型工件保存的位置(通常在temp目录中,但也可以复制到其他位置):

model_dir(classifier)
"C:\\Users\\xxxxxx\\AppData\\Local\\Temp\\tmpv1e_ri23"
# dnn_classifier has a model_dir parameter to load an existing model
?dnn_classifier

模型工件中包含了可以被 TensorBoard 使用的事件日志。例如,当我启动 TensorBoard 并将其指向temp目录中的日志目录时,我可以看到创建的 TensorFlow 图表:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/b3037cf0-3e7e-4247-8606-2b0b4c550baf.png

图 8.7:使用 TensorBoard 显示 TensorFlow 估算器模型的图表

TensorFlow 运行包

tfruns包是一组用于管理深度学习模型不同训练运行的工具集。它可以作为框架,用不同的超参数构建多个深度学习模型。它可以跟踪每次训练运行的超参数、度量标准、输出和源代码,并允许你比较最佳模型,以便看到训练运行之间的差异。这使得超参数调优变得更加容易,并且可以与任何tfestimator模型或Keras模型一起使用。更多详情,请访问tensorflow.rstudio.com/tools/tfruns/articles/overview.html

以下代码位于Chapter8/hyperparams.R文件夹中,并且还使用了我们在TensorFlow 估算器部分中使用的脚本(Chapter8/tf_estimators.R):

library(tfruns)
# FLAGS <- flags(
# flag_numeric("layer1", 256),
# flag_numeric("layer2", 128),
# flag_numeric("layer3", 64),
# flag_numeric("layer4", 32),
# flag_numeric("dropout", 0.2),
# flag_string("activ","relu")
# )

training_run('tf_estimators.R')
training_run('tf_estimators.R', flags = list(layer1=128,layer2=64,layer3=32,layer4=16))
training_run('tf_estimators.R', flags = list(dropout=0.1,activ="tanh"))

这将使用不同的超参数运行Chapter8/tf_estimators.R脚本。第一次运行时,我们不会更改任何超参数,因此它使用Chapter8/tf_estimators.R中包含的默认值。每次使用分类脚本训练一个新模型时,都会被称为训练运行,并且训练运行的详细信息将保存在当前工作目录的runs文件夹中。

对于每次训练运行,一个新的网站将弹出,显示该运行的详细信息,如下图所示:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/320da535-5b81-4e24-9072-6c2c4ee07181.png

图 8.8:TensorFlow 训练运行 – 概要屏幕

我们可以看到训练进度图,以及训练运行发生的时间和评估指标的详细信息。我们还可以在右下角看到用于训练运行的标志(即超参数)。还有一个标签页显示 R 代码输出,其中包含来自内部文件(Chapter8/tf_estimators.R)的所有输出,包括图表。

一旦所有训练运行完成,以下代码会显示所有训练运行的摘要:

ls_runs(order=eval_accuracy)
ls_runs(order=eval_accuracy)[,1:5]
Data frame: 3 x 5 
                    run_dir eval_accuracy eval_accuracy_baseline eval_auc eval_auc_precision_recall
3 runs/2018-08-02T19-50-17Z        0.7746                 0.6032   0.8431                    0.8874
2 runs/2018-08-02T19-52-04Z        0.7724                 0.6032   0.8425                    0.8873
1 runs/2018-08-02T19-53-39Z        0.7711                 0.6032   0.8360                    0.8878

在这里,我们按 eval_accuracy 列排序结果。如果你关闭了显示训练运行摘要的窗口,你可以通过调用 view_run 函数并传入文件夹名称来重新显示它。例如,要显示最佳训练运行的摘要,可以使用以下代码:

dir1 <- ls_runs(order=eval_accuracy)[1,1]
view_run(dir1)

最后,你还可以比较两个运行。在这里,我们比较了两个最佳模型:

dir1 <- ls_runs(order=eval_accuracy)[1,1]
dir2 <- ls_runs(order=eval_accuracy)[2,1]
compare_runs(runs=c(dir1,dir2))

这将弹出类似以下内容的页面:

https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/r-dl-ess-2e/img/e006024e-8f31-4080-8a20-b5e7f81e6949.png

图 8.9:比较两个 TensorFlow 运行

此页面展示了两个训练运行的评估指标,并显示了所使用的超参数。如我们所见,这使得调整深度学习模型的过程更加容易。超参数调整的方法具有自动日志记录、可追溯性,并且可以轻松比较不同的超参数设置。你可以看到训练运行的指标和使用的不同超参数。再也不需要比较配置文件来尝试匹配超参数设置和输出日志了!相比之下,我为第七章《使用深度学习的自然语言处理》中的 NLP 示例所写的超参数选择代码,在此看起来显得粗糙。

总结

在这一章中,我们开发了一些 TensorFlow 模型。我们看了 TensorBoard,它是一个非常好的工具,用于可视化和调试深度学习模型。我们使用 TensorFlow 构建了几个模型,包括一个基本的回归模型和一个用于计算机视觉的 Lenet 模型。通过这些示例,我们看到了使用 TensorFlow 编程比使用本书中其他地方的高级 API(如 MXNet 和 Keras)更复杂且容易出错。

接下来,我们开始使用 TensorFlow 估算器,这比直接使用 TensorFlow 界面更简单。然后我们在另一个名为tfruns的包中使用了该脚本,tfruns 代表 TensorFlow 运行。这个包允许我们每次调用 TensorFlow 估算器或 Keras 脚本时使用不同的标志。我们用它来进行超参数选择、运行和评估多个模型。TensorFlow 运行与 RStudio 有出色的集成,我们能够查看每次运行的摘要,并比较不同的运行,查看使用的指标和超参数的差异。

在下一章,我们将讨论嵌入和自编码器。我们已经在第七章《使用深度学习的自然语言处理》中看过嵌入,因此在下一章我们将看到嵌入如何创建数据的低层次编码。我们还将使用训练自编码器,这些自编码器会创建这些嵌入。我们将使用自编码器进行异常检测,并且还会用于协同过滤(推荐系统)。

您可能感兴趣的与本文相关的镜像

TensorFlow-v2.15

TensorFlow-v2.15

TensorFlow

TensorFlow 是由Google Brain 团队开发的开源机器学习框架,广泛应用于深度学习研究和生产环境。 它提供了一个灵活的平台,用于构建和训练各种机器学习模型

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值