R-深度学习精要第二版-全-
R 深度学习精要第二版(全)
原文:
annas-archive.org/md5/32221412fbc143db1a6239ed273b2843译者:飞龙
前言
深度学习可能是当前数据科学中最热门的技术,而 R 语言是最受欢迎的数据科学语言之一。然而,许多人并不认为 R 是深度学习的选择之一,这实在是可惜,因为 R 语言在数据科学中非常出色。本书展示了 R 作为深度学习的可行选择,因为它支持如 MXNet 和 Keras 等库。
当我决定写这本书时,我有多个目标。首先,我想展示如何将深度学习应用于各种任务,而不仅仅是计算机视觉和自然语言处理。这本书涵盖了这些主题,但也展示了如何将深度学习应用于预测、回归、异常检测和推荐系统。第二个目标是探讨深度学习中一些在其他地方没有很好涵盖的话题;例如,使用 LIME 进行可解释性分析、模型部署以及如何在云端进行深度学习。最后一个目标是给出深度学习的总体概述,而不仅仅是提供机器学习代码。我认为我通过讨论如何从原始数据创建数据集、如何对比模型进行基准测试、如何在构建模型时管理数据以及如何部署模型等话题达到了这个目标。我希望通过本书的学习,到最后你也能相信 R 语言是进行深度学习的有效选择。
本书适合谁
如果你有一定的 R 语言经验,并且正在寻找一本展示如何使用 R 进行深度学习的实践案例的书,那么这本书适合你!本书假设你已经熟悉机器学习中的一些概念,例如将数据拆分为训练集和测试集。任何在 R 中构建过机器学习算法的人,阅读本书应该不会有问题。
本书涵盖的内容
第一章,深度学习入门,介绍了深度学习和神经网络的基本概念。它还简要介绍了如何设置 R 语言环境。
第二章,训练预测模型,从使用 R 中现有的包构建神经网络模型开始。本章还讨论了过拟合,这是大多数深度学习模型中的一个问题。
第三章,深度学习基础,讲解了如何从零开始在 R 中构建神经网络。接着,我们展示了我们的代码与深度学习库 MXNet 的关系。
第四章,训练深度预测模型,探讨了激活函数并介绍了 MXNet 库。接下来,我们将为一个实际案例构建深度学习预测模型。我们将使用原始的交易数据集,并开发一个数据管道来创建一个预测模型,预测哪些客户将在接下来的 14 天内返回。
第五章,使用卷积神经网络进行图像分类,介绍了图像分类任务。首先,我们将介绍一些核心概念,如卷积层和池化层,然后展示如何使用这些层来分类图像。
第六章,调优和优化模型,讨论了如何调优和优化深度学习模型。我们讨论了超参数调优和数据增强的使用。
第七章,使用深度学习进行自然语言处理,展示了如何利用深度学习进行自然语言处理(NLP)任务。我们展示了深度学习算法如何超越传统的 NLP 技术,并且开发过程更加简便。
第八章,在 R 中使用 TensorFlow 构建深度学习模型,介绍了如何在 R 中使用 TensorFlow API。我们还讨论了 TensorFlow 中一些额外的包,它们使得开发 TensorFlow 模型变得更加简单,并有助于超参数选择。
第九章,异常检测和推荐系统,展示了如何使用深度学习模型创建数据的低维度表示(嵌入)。我们接着展示了如何利用嵌入进行异常检测以及构建推荐系统。
第十章,在云端运行深度学习模型,介绍了如何使用 AWS、Azure 和 Google Cloud 服务来训练深度学习模型。本章展示了如何在云端以低成本训练模型。
第十一章,深度学习的下一阶段,本章通过一个端到端的图像分类解决方案来结束本书。我们从一组图像文件开始,训练一个模型,使用该模型进行迁移学习,然后展示如何将模型部署到生产环境中。我们还简要讨论了生成对抗网络(GANs)和强化学习。
为了最大限度地从本书中获益
本书并未假设你有一台高端计算机,甚至没有配备 GPU 的计算机。为了充分利用本书的内容,我建议你执行所有代码示例。尝试修改它们的参数,看看能否超越书中的性能指标。
下载示例代码文件
你可以从你的账户下载本书的示例代码文件,访问 www.packtpub.com。如果你是从其他地方购买的本书,可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给你。
你可以通过以下步骤下载代码文件:
-
登录或注册 www.packtpub.com。
-
选择“支持”标签。
-
点击“代码下载和勘误”。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
下载文件后,请确保使用以下最新版工具解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书的代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/R-Deep-Learning-Essentials-Second-Edition。如果代码有更新,将会在现有的 GitHub 仓库中更新。
我们还提供了来自我们丰富书籍和视频目录的其他代码包,您可以访问 github.com/PacktPublishing/ 查看!
下载彩色图像
我们还提供了一份包含本书中截图/图表的彩色图像的 PDF 文件。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/RDeepLearningEssentialsSecondEdition_ColorImages.pdf。
使用的约定
本书中使用了多种文本约定。
CodeInText:表示文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名。例如:“将下载的 WebStorm-10*.dmg 磁盘映像文件挂载为系统中的另一个磁盘。”
代码块如下所示:
dfData <- read.csv(fileName)
nobs <- nrow(dfData)
train <- sample(nobs, 0.9*nobs)
test <- setdiff(seq_len(nobs), train)
当我们希望特别指出代码块中的某部分时,相关行或项目将以粗体显示:
> 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
任何命令行输入或输出均按如下格式书写:
$ tensorboard --logdir /tensorflow_logs
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词通常以这种方式显示。示例:“从管理面板中选择系统信息。”
警告或重要提示以这种方式显示。
提示和技巧通常以这种方式显示。
联系我们
我们始终欢迎读者的反馈。
一般反馈:请通过电子邮件 feedback@packtpub.com 并在邮件主题中注明书名。如果您对本书的任何部分有疑问,请发送邮件至 questions@packtpub.com。
勘误:虽然我们已经尽力确保内容的准确性,但错误有时仍会发生。如果您在本书中发现任何错误,我们将非常感激您向我们报告。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接并输入相关详情。
盗版:如果您在互联网上发现任何非法复制的我们的作品形式,我们将非常感激您提供该位置地址或网站名称。请通过 copyright@packtpub.com 联系我们,并附上材料的链接。
如果您有兴趣成为作者:如果您在某个专题上有专业知识并且有意于撰写或贡献一本书,请访问authors.packtpub.com。
评论
请留下您的评论。一旦您阅读并使用了这本书,请不妨在您购买它的网站上留下您的评论?潜在的读者可以通过您的客观意见做出购买决策,我们在 Packt 可以了解您对我们产品的看法,而我们的作者也可以看到您对他们书籍的反馈。谢谢!
欲了解更多关于 Packt 的信息,请访问packtpub.com。
第一章:深度学习入门
本章讨论了深度学习,一种强大的多层架构,用于模式识别、信号检测以及分类或预测。尽管深度学习并不新鲜,但它仅在过去十年才获得了极大的关注,这部分归功于计算能力的提升、更加高效的模型训练方法以及不断增长的数据量。在本章中,你将了解什么是深度学习、可用于训练此类模型的 R 包,以及如何为分析设置系统。我们将简要讨论MXNet和Keras,这两种框架将在后续章节的多个示例中用于实际训练和使用深度学习模型。
在本章中,我们将探讨以下主题:
-
什么是深度学习?
-
深度学习的概念概览
-
设置 R 环境及在 R 中可用的深度学习框架
-
GPU 和可重复性
什么是深度学习?
深度学习是机器学习的一个子领域,而机器学习又是人工智能的一个子领域。人工智能是创造能够执行需要人类智能的任务的机器的艺术。机器学习使用算法来学习,而无需明确编程。深度学习是机器学习的一个子集,使用模仿大脑工作方式的人工神经网络。
以下图表展示了它们之间的关系。例如,自动驾驶汽车是人工智能的一个应用。自动驾驶汽车的一个关键部分是识别其他道路使用者,如汽车、行人、骑车人等。这需要机器学习,因为无法明确编程来实现这一点。最终,深度学习可能被选择作为实现这一机器学习任务的方法:

图 1.1:人工智能、机器学习和深度学习之间的关系
人工智能作为一个领域自 20 世纪 40 年代就已经存在;前述图表中的定义来自于 1990 年库兹韦尔。它是一个广泛的领域,涵盖了哲学、数学、神经科学和计算机工程等多个不同领域的思想。机器学习是人工智能中的一个子领域,致力于开发和使用能够从原始数据中学习的算法。当机器学习任务需要预测一个结果时,这被称为监督学习。当任务是从一组可能的结果中进行预测时,这就是分类任务;当任务是预测一个数值时,这就是回归任务。一些分类任务的例子包括判断某一信用卡购买是否欺诈,或者某张图片是猫还是狗。回归任务的一个例子是预测客户下个月将花费多少钱。还有其他类型的机器学习,其中学习并不预测值。这被称为无监督学习,包括对数据进行聚类(分段)或创建数据的压缩格式。
深度学习是机器学习中的一个子领域。之所以称其为深度,是因为它使用多层结构来映射输入和输出之间的关系。层是一个神经元集合,负责对其输入进行数学运算。这个内容将在下一部分的神经网络概念概述中详细解释。这个深度结构意味着模型足够大,可以处理许多变量,并且足够灵活,可以近似数据中的模式。深度学习还可以作为整体学习算法的一部分生成特征,而不是将特征创建作为一个前提步骤。深度学习在图像识别(包括手写识别、照片或物体分类)、语音识别和自然语言处理领域表现尤为有效。它在过去几年彻底改变了如何使用图像、文本和语音数据进行预测,取代了之前处理这些数据的方式。它还使这些领域向更多人开放,因为它自动化了大量特征生成工作,这些工作原本需要专业技能。
深度学习并不是机器学习中唯一的技术。还有其他类型的机器学习算法;最常见的包括回归、决策树、随机森林和朴素贝叶斯。对于许多应用场景,这些算法中的某些可能是更好的选择。深度学习可能不是最佳选择的一些例子包括当可解释性是必需的要求、数据集较小,或者你在开发模型时资源(时间和/或硬件)有限。重要的是要认识到,尽管行业存在过度炒作,但在行业中大多数机器学习并不使用深度学习。话虽如此,本书涵盖了深度学习算法,因此我们将继续讲解。接下来的章节将更深入地讨论神经网络和深度神经网络。
神经网络的概念概览
理解为什么神经网络如此有效可能是困难的。这个介绍将从两个角度来观察它们。如果你了解线性回归的工作原理,第一个角度应该很有帮助。第二个角度则更直观且技术性较少,但同样有效。我鼓励你阅读这两个角度,并花些时间思考这两种概述。
神经网络作为线性回归的扩展
最简单、最古老的预测模型之一是 回归。它基于另一个值预测一个连续值(即数字)。线性回归函数是:
y=mx+b
其中 y 是你要预测的值,而 x 是你的输入变量。线性回归系数(或参数)为 m(直线的斜率)和 b(截距)。以下 R 代码创建了一个 y= 1.4x -2 的函数,并绘制它:
set.seed(42)
m <- 1.4
b <- -1.2
x <- 0:9
jitter<-0.6
xline <- x
y <- m*x+b
x <- x+rnorm(10)*jitter
title <- paste("y = ",m,"x ",b,sep="")
plot(xline,y,type="l",lty=2,col="red",main=title,xlim=c(0,max(y)),ylim=c(0,max(y)))
points(x[seq(1,10,2)],y[seq(1,10,2)],pch=1)
points(x[seq(2,11,2)],y[seq(2,11,2)],pch=4)
o 或 x 点是给定 x 轴值时需要预测的值,直线是实际的真实值。添加了一些随机噪声,因此点并不完全在直线上。此代码生成以下输出:

图 1.2:回归线拟合数据的示例(即从 x 预测 y)
在回归任务中,你会得到一些 x 和对应的 y 值,但并未给出将 x 映射到 y 的底层函数。监督式机器学习任务的目的是,在给定一些之前的 x 和 y 样本的情况下,我们能否预测出仅知道 x 而不知道 y 的新数据的 y 值。一个例子可能是根据房屋卧室数量预测房价。到目前为止,我们只考虑了一个输入变量 x,但我们可以轻松扩展这个例子来处理多个输入变量。对于房屋的例子,我们会使用卧室数量和房屋面积来预测房价。我们的代码可以通过将输入 x 从向量(一维数组)更改为矩阵(二阶数组)来适应这一点。
如果我们考虑用我们的模型来预测房价,线性回归有一个严重的限制:它只能估计线性函数。如果 x 到 y 的映射不是线性的,它就无法很好地预测 y。这个函数总是为一个变量生成一条直线,如果使用多个 x 预测变量,它会生成一个超平面。这意味着线性回归模型在数据的低端和高端可能不准确。
让模型拟合非线性关系的一个简单方法是向函数中添加多项式项。这就是所谓的 多项式回归。例如,通过添加一个四次的多项式,我们的函数变为:
y = m[1]x⁴ + m[2]x³ + m[3]x² + m[4]x + b
通过添加这些额外的项,直线(或决策边界)不再是线性的。以下代码演示了这一点——我们创建了一些示例数据,并创建了三个回归模型来拟合这些数据。第一个模型没有多项式项,模型是一个直线,拟合效果很差。第二个模型(蓝色圆圈)包含多项式,最高到 3 次方,即 X、X² 和 X³。最后一个模型包含最高 12 次方的多项式,即 X、X²、.....、X¹²。第一个模型(直线)欠拟合数据,而最后一个模型则过拟合数据。过拟合是指模型过于复杂,最终记住了数据。这意味着该模型的泛化能力差,在未见过的数据上表现不好。以下代码生成数据并创建了三个随着多项式阶数增加的模型:
par(mfrow=c(1,2))
set.seed(1)
x1 <- seq(-2,2,0.5)
# y=x²-6
jitter<-0.3
y1 <- (x1²)-6
x1 <- x1+rnorm(length(x1))*jitter
plot(x1,y1,xlim=c(-8,12),ylim=c(-8,10),pch=1)
x <- x1
y <- y1
# y=-x
jitter<-0.8
x2 <- seq(-7,-5,0.4)
y2 <- -x2
x2 <- x2+rnorm(length(x2))*jitter
points(x2,y2,pch=2)
x <- c(x,x2)
y <- c(y,y2)
# y=0.4 *rnorm(length(x3))*jitter
jitter<-1.2
x3 <- seq(5,9,0.5)
y3 <- 0.4 *rnorm(length(x3))*jitter
points(x3,y3,pch=3)
x <- c(x,x3)
y <- c(y,y3)
df <- data.frame(cbind(x,y))
plot(x,y,xlim=c(-8,12),ylim=c(-8,10),pch=4)
model1 <- lm(y~.,data=df)
abline(coef(model1),lty=2,col="red")
max_degree<-3
for (i in 2:max_degree)
{
col<-paste("x",i,sep="")
df[,col] <- df$x^i
}
model2 <- lm(y~.,data=df)
xplot <- seq(-8,12,0.1)
yplot <- (xplot⁰)*model2$coefficients[1]
for (i in 1:max_degree)
yplot <- yplot +(xplot^i)*model2$coefficients[i+1]
points(xplot,yplot,col="blue", cex=0.5)
max_degree<-12
for (i in 2:max_degree)
{
col<-paste("x",i,sep="")
df[,col] <- df$x^i
}
model3 <- lm(y~.,data=df)
xplot <- seq(-8,12,0.1)
yplot <- (xplot⁰)*model3$coefficients[1]
for (i in 1:max_degree)
yplot <- yplot +(xplot^i)*model3$coefficients[i+1]
points(xplot,yplot,col="green", cex=0.5,pch=2)
MSE1 <- c(crossprod(model1$residuals)) / length(model1$residuals)
MSE2 <- c(crossprod(model2$residuals)) / length(model2$residuals)
MSE3 <- c(crossprod(model3$residuals)) / length(model3$residuals)
print(sprintf(" Model 1 MSE = %1.2f",MSE1))
[1] " Model 1 MSE = 14.17"
print(sprintf(" Model 2 MSE = %1.2f",MSE2))
[1] " Model 2 MSE = 3.63"
print(sprintf(" Model 3 MSE = %1.2f",MSE3))
[1] " Model 3 MSE = 0.07"
如果我们要选择一个模型使用,应该选择中间的模型,即使第三个模型的 MSE(均方误差)更低。在下面的截图中;最佳模型是左上角的弯曲线:

图 1.3:多项式回归
如果我们查看这三个模型,并观察它们如何处理极端的左右点,我们会明白为什么过拟合可能导致在未见过的数据上的不良结果。在图的右侧,最后一组点(加号)具有局部线性关系。然而,12 次方的多项式回归线(绿色三角形)过度强调最后一个点,这是额外的噪声,导致曲线急剧下降。这会导致模型在 x 增加时对 y 预测极端的负值,而如果我们查看数据,这是不合理的。过拟合是一个重要的问题,我们将在后续章节中更详细地讨论。
通过添加平方、立方和更多的多项式项,模型能够拟合比仅使用线性函数在输入数据上更复杂的数据。神经网络使用类似的概念,区别在于,它们不是使用输入变量的多项式项,而是将多个回归函数通过非线性项链接在一起。
以下是一个神经网络架构的示例。圆圈代表节点,线条是节点之间的连接。如果两个节点之间有连接,左侧节点的输出就是下一个节点的输入。一个节点的输出值是该节点输入值和权重的矩阵运算结果:

图 1.4:一个神经网络示例
在一个节点的输出值被传递给下一个节点作为输入值之前,会对这些值应用一个函数,将整体函数转换为非线性函数。这些函数被称为激活函数,它们的作用与多项式项相同。
通过将多个小函数组合在一起来创建机器学习模型,这种思路在机器学习中非常常见。它被广泛应用于随机森林中,在那里,多个独立的决策树投票决定结果。它也被应用于提升算法中,其中一个函数中的错误分类实例在下一个函数中被赋予更多的权重。
通过包含多个层的节点,神经网络模型可以逼近几乎任何函数。虽然这确实使得训练模型变得更加困难,但我们会简要解释如何训练神经网络。每个节点最初会被分配一组随机权重。在第一次传递时,这些权重会用于计算并将值从输入层传递(或传播)到隐藏层,最终到达输出层。这被称为前向传播。由于权重是随机设置的,输出层的最终(预测)值与实际值相比将不准确,因此我们需要一种方法来计算预测值与实际值之间的差异。这是通过使用成本函数来计算的,成本函数提供了模型在训练过程中的准确度衡量标准。然后,我们需要调整从输出层到各层节点的权重,使其更接近目标值。这是通过反向传播实现的;我们从右到左移动,稍微更新每一层节点的权重,使其逐步接近实际值。前向传播和反向传播的循环会持续进行,直到损失函数的误差值不再减小;这可能需要几百次甚至几千次的迭代,或称为“周期”。
为了正确地更新节点权重,我们需要知道这个变化会让我们更接近目标,目标是最小化成本函数的结果。我们之所以能够做到这一点,是因为一个巧妙的技巧——我们使用具有导数函数的激活函数。
如果你对微积分的知识有限,刚开始理解导数可能会有些困难。但简单来说,一个函数可能有一个导数公式,告诉我们如何改变一个函数的 输入,从而使函数的 输出 发生正向或负向的变化。这个导数/公式使得算法能够最小化代价函数,代价函数是对误差的衡量。用更专业的术语来说,函数的导数衡量了随着输入变化,函数变化的速率。如果我们知道了一个函数在输入变化时的变化速率,更重要的是知道它朝哪个方向变化,那么我们就能利用这些信息逐步逼近最小化该函数的目标。你可能见过的一个示例是以下图示:

图 1.5:一个函数(曲线)及其在某点的导数
在这个图示中,曲线是我们希望在 y 上最小化的数学函数,也就是说,我们想要到达最低点(由箭头标示)。我们当前位于红色圆圈中的点,且该点的导数是切线的斜率。导数函数表示了我们需要朝哪个方向移动才能到达最低点。随着我们接近目标(箭头所在的点),导数值会发生变化,因此我们不能一步到位。因此,算法会以小步伐前进,每走一步都重新计算导数,但如果我们选择步伐太小,算法收敛的时间会非常长(即到达最小值的时间)。如果步伐太大,则有可能会错过最小值。你选择的步长大小被称为 学习率,它实际上决定了算法训练所需的时间。
这可能看起来有些抽象,所以用一个类比可能会更清楚一些。这个类比可能有些过于简化,但它解释了导数、学习率和成本函数。假设一个简单的驾驶汽车模型,车速必须设置为适合条件和限速的值。你当前的速度和目标速度之间的差异就是误差率,这是通过成本函数计算的(在这种情况下只是简单的减法)。为了改变车速,你可以踩油门加速,或者踩刹车减速。加速度/减速度(即速度的变化率)就是速度的导数。施加到踏板上的力量决定了加速/减速发生的快慢,这个力量类似于机器学习算法中的学习率。它控制了达到目标值所需的时间。如果只对踏板施加小的变化,你最终会达到目标速度,但会花费更长的时间。然而,你通常不希望对踏板施加最大力量,因为这样做可能会很危险(如果猛踩刹车)或浪费燃料(如果加速过猛)。存在一个最佳的平衡点,在这个点上,你可以安全且迅速地达到目标速度。
神经网络作为记忆单元网络
另一种考虑神经网络的方式是将其与人类思维进行比较。顾名思义,神经网络的灵感来自大脑中的神经过程和神经元。神经网络包含一系列互相连接的神经元或节点,用于处理输入。神经元具有从先前观察(数据)中学习到的权重。神经元的输出是其输入和权重的函数。最终神经元的激活即为预测结果。
我们将考虑一个假设的案例,其中大脑的一个小部分负责匹配基本形状,例如方形和圆形。在这种情况下,一些神经元在基本层次上对水平线发火,另一组神经元对垂直线发火,另一组神经元则对弯曲的线段发火。这些神经元将输入信息传递到更高层次的处理过程,从而识别更复杂的物体,例如,当水平和垂直神经元同时被激活时,识别为一个方形。
在下图中,输入数据以方形表示。这些可以是图像中的像素。下一层的隐藏神经元由识别基本特征的神经元组成,例如水平线、垂直线或曲线。最后,输出可能是一个通过两个隐藏神经元同时激活而被激活的神经元:

图 1.6:神经网络作为一种记忆单元网络
在这个例子中,隐藏层中的第一个节点擅长匹配水平线,而第二个节点则擅长匹配垂直线。这些节点记住了这些物体是什么。如果这些节点结合在一起,可以检测到更复杂的物体。例如,如果隐藏层识别出水平线和垂直线,那么这个物体更可能是一个正方形而不是圆形。这与卷积神经网络的工作原理类似,我们将在第五章中讨论,使用卷积神经网络进行图像分类。
我们在这里对神经网络的理论进行了非常浅显的介绍,因为我们不希望在第一章就让你感到不知所措!在接下来的章节中,我们将更深入地讨论一些这些问题,但同时,如果你希望更深入理解神经网络背后的理论,以下资源值得推荐:
-
Goodfellow 等人(2016 年)第六章
-
Hastie, T., Tibshirani, R., 和 Friedman, J.(2009 年)第十一章,免费提供于
web.stanford.edu/~hastie/Papers/ESLII.pdf -
Murphy, K. P.(2012 年)第十六章
接下来,我们将简要介绍深度神经网络。
深度神经网络
深度神经网络(DNN)是一种具有多个隐藏层的神经网络。仅仅增加少量层的神经网络中的节点数量(即浅层神经网络)并不能获得好的结果。DNN 能够在参数较少的情况下更准确地拟合数据,因为更多的层(每层有更少的神经元)提供了更高效和更准确的表示。使用多个隐藏层使得从简单元素到更复杂元素的构建更加精细。在前面的例子中,我们考虑了一个可以识别基本形状的神经网络,例如圆形或正方形。在深度神经网络中,许多圆形和正方形可以组合形成其他更复杂的形状。浅层神经网络无法从基本元素构建出更复杂的形状。DNN 的缺点是这些模型更难训练,并且容易出现过拟合。
如果我们考虑从图像数据中识别手写文本,那么原始数据就是来自图像的像素值。第一层捕捉简单的形状,如直线和曲线。下一层利用这些简单形状识别更高层次的抽象,例如角落和圆形。第二层不必直接从像素中学习,因为像素是嘈杂和复杂的。相比之下,浅层架构可能需要更多的参数,因为每个隐藏神经元都必须能够直接从图像的像素到达目标值。而且它还无法结合特征,所以例如,如果图像数据的位置不同(例如,没有居中),它将无法识别文本。
训练深度神经网络的挑战之一是如何高效地学习权重。模型非常复杂,参数数量庞大。深度学习领域的一个重大进展发生在 2006 年,当时研究表明深度置信网络(DBNs)可以逐层训练(见 Hinton, G. E., Osindero, S., 和 Teh, Y. W. (2006))。DBN 是一种具有多个隐藏层的深度神经网络,层与层之间存在连接(但同一层内的神经元之间没有连接)(即,第一层的一个神经元可以与第二层的神经元连接,但不能与第一层的另一个神经元连接)。没有层内连接的限制使得可以使用更快速的训练算法,例如对比散度算法。本质上,DBN 可以逐层训练;首先训练第一隐藏层,并用其将原始数据转化为隐藏神经元,随后将这些隐藏神经元作为下一隐藏层的新输入,直到所有层都被训练完毕。
认识到深度置信网络(DBNs)可以逐层训练的好处,不仅仅局限于 DBNs。DBNs 有时被用作深度神经网络的预训练阶段。这使得可以使用相对快速的、贪婪的逐层训练方法来提供较好的初始估计,然后使用其他效率较低的训练算法(如反向传播)在深度神经网络中进行优化。
到目前为止,我们主要集中在前馈神经网络上,其中一层和神经元的结果会传递到下一层。在结束这一节之前,值得提到两种在深度神经网络中越来越流行的特定类型。第一种是递归神经网络(RNN),在这种网络中,神经元会互相发送反馈信号。这些反馈循环使得 RNNs 能够很好地处理序列。RNN 的一个应用实例是自动生成吸引点击的标题,比如前往洛杉矶的十大理由:第六条会让你震惊!或者一招让你知道大牌发廊不想让你知道的秘密。RNN 在这类任务中表现优异,因为它们可以从一个较大的初始词汇池(甚至只是流行的搜索词或名字)中获取种子,然后预测/生成下一个词。这个过程可以重复几次,直到生成一个短语,即吸引点击的标题。我们将在第七章,使用深度学习进行自然语言处理*中看到 RNN 的例子。
第二种类型是卷积神经网络(CNN)。CNNs 最常用于图像识别。CNN 的工作原理是让每个神经元响应图像的重叠子区域。CNN 的优势在于,它们需要较少的预处理,且通过权重共享(例如,跨图像的子区域)仍然不需要太多参数。这对图像特别有价值,因为图像通常不一致。例如,想象十个不同的人拍摄同一张桌子的照片。有些可能离得更近或更远,或者处于导致基本相同的图像却有不同高度、宽度和焦点对象周围捕获图像数量的位置。我们将在第五章,使用卷积神经网络进行图像分类中深入探讨 CNN。
本描述仅提供了深度神经网络的简要概述,以及它们的一些应用场景。深度学习的奠基性参考文献是Goodfellow 等人(2016)。
一些关于深度学习的常见迷思
关于深度学习,有许多误解、半真半假的说法和误导性观点。以下是一些关于深度学习的常见误解:
-
人工智能意味着深度学习,并取代所有其他技术
-
深度学习需要博士级别的数学理解
-
深度学习难以训练,几乎是一种艺术形式
-
深度学习需要大量数据
-
深度学习的可解释性差
-
深度学习需要 GPU
接下来的段落将逐一讨论这些说法。
深度学习不是人工智能,也并不取代所有其他机器学习算法。它只是机器学习中一类算法。尽管有炒作,深度学习可能只占当前生产环境中机器学习项目的不到 1%。当你在浏览网络时,遇到的大多数推荐引擎和在线广告并不是由深度学习驱动的。许多公司内部用来管理其订阅者的模型,例如流失分析,也不是深度学习模型。用于决定谁能获得信用的信用机构模型也并不使用深度学习。
深度学习并不要求对数学有深入理解,除非你有兴趣研究新的深度学习算法和专门的架构。大多数从业者使用现有的深度学习技术处理他们的数据,采用现有架构并根据自己的工作需求进行修改。这并不需要深厚的数学基础,深度学习中使用的数学知识在世界各地的高中阶段就已经教授。事实上,我们在第三章《深度学习基础》中展示了这一点,在不到 70 行代码的情况下,我们从零开始构建了一个完整的神经网络!
训练深度学习模型是困难的,但它并不是一种艺术形式。它确实需要练习,但相同的问题会一遍又一遍地出现。更好的是,通常会有针对这些问题的解决方法,例如,如果模型过拟合,可以添加正则化;如果模型训练不佳,可以构建更复杂的模型和/或使用数据增强。我们将在第六章《调优与优化模型》中深入探讨这一点。
深度学习需要大量数据这一说法是有一定道理的。然而,你仍然可以通过使用预训练网络或从现有数据中创建更多训练数据(数据增强)来将深度学习应用于问题。我们将在后续的第六章《调优与优化模型》和第十一章《深度学习的下一个层次》中探讨这些内容。
深度学习模型难以解释。我们的意思是能够解释模型是如何做出决策的。这是许多机器学习算法中的问题,不仅仅是深度学习。在机器学习中,通常准确性和可解释性之间存在反比关系——模型需要越准确,它的可解释性就越差。对于某些任务,例如在线广告,可解释性并不重要,而且错误的成本也很小,所以通常选择最强大的算法。而在某些情况下,例如信用评分,法律可能要求可解释性;人们可能会要求解释为什么他们被拒绝了信用。在其他情况下,如医学诊断,可解释性可能对医生理解模型为何认为某人患有疾病至关重要。
如果可解释性很重要,可以对机器学习模型应用一些方法,以便了解它们为何对某个实例预测了这个结果。某些方法通过扰动数据(即,对数据进行轻微的修改)来工作,试图找出哪些变量在模型做出决策时最具影响力。一个这样的算法叫做LIME(局部可解释模型无关解释)。(Ribeiro, Marco Tulio, Sameer Singh, 和 Carlos Guestrin. 为什么我应该相信你?:解释任何分类器的预测。第 22 届 ACM SIGKDD 国际知识发现与数据挖掘会议论文集。ACM, 2016.) 这个算法已在包括 R 在内的多种语言中实现;有一个叫做lime的包。我们将在第六章,调优与优化模型中使用这个包。
最后,虽然深度学习模型可以在 CPU 上运行,但实际上,任何实际工作都需要配备 GPU 的工作站。这并不意味着你需要购买一个 GPU,因为你可以使用云计算来训练模型。在第十章,在云中运行深度学习模型,我们将讨论使用 AWS、Azure 和 Google Cloud 来训练深度学习模型。
设置你的 R 环境
在开始你的深度学习之旅之前,第一步是安装 R,R 可在 cran.r-project.org/ 下载。当你下载并使用 R 时,默认只安装了一些核心包,但可以通过选择菜单选项或一行代码来添加新包。我们不会详细讨论如何安装 R 或如何添加包,因为我们假设大多数读者已经熟练掌握这些技能。一个好的集成开发环境(IDE)对于使用 R 至关重要。到目前为止,最受欢迎的 IDE,也是我推荐的,是 RStudio,可以从 www.rstudio.com/ 下载。另一个选择是 Emacs。Emacs 和 RStudio 的一个优点是它们都可以在所有主要平台(Windows、macOS 和 Linux)上使用,因此即使你更换电脑,也能保持一致的 IDE 体验。以下是 RStudio IDE 的截图:

图 1.7 RStudio IDE
使用 RStudio 相比于 Windows 上的 R 图形用户界面(GUI)有了显著的提升。在 RStudio 中有多个面板,可以从不同的角度查看你的工作。左上方的面板显示代码,左下方的面板显示控制台(运行代码的结果)。右上方的面板显示变量列表及其当前值,右下方的面板显示代码生成的图表。这些面板还包含更多的标签页,可以进一步探索不同的视角。
除了 IDE,RStudio(公司)还开发或大力支持了 R 环境中的其他工具和包。我们将使用其中一些工具,包括 R Markdown 和 R Shiny 应用程序。R Markdown 类似于 Jupyter 或 IPython 笔记本;它允许你将代码、输出(例如图表)和文档结合在一个脚本中。R Markdown 被用来创建本书中的部分章节,其中代码和描述性文本交织在一起。R Markdown 是一个非常好的工具,可以确保你的数据科学实验得到正确的文档记录。通过将文档嵌入到分析中,它们更有可能保持同步。R Markdown 可以输出为 HTML、Word 或 PDF。以下是一个 R Markdown 脚本的示例,左侧是代码,右侧是输出:

图 1.8:R Markdown 示例;左侧是 R 代码和文本信息的混合,右侧是从源脚本生成的 HTML 输出。
我们还将使用 R Shiny 创建基于 R 的网页应用程序。这是一种创建交互式应用程序以展示关键功能的极佳方法。以下截图是一个 R Shiny 网页应用程序的示例,我们将在第五章中看到,卷积神经网络的图像分类:

图 1.9:一个 R Shiny 网页应用程序的示例
安装 R 后,你可以添加适用于基本神经网络的包。nnet包就是其中之一,它可以拟合具有一个隐藏层的前馈神经网络,例如图1.6 所示的网络。关于nnet包的更多细节,请参见Venables, W. N. 和 Ripley, B. D.(2002)。neuralnet包可以拟合具有多个隐藏层的神经网络,并使用反向传播进行训练。它还允许自定义误差和神经元激活函数。我们还将使用RSNNS包,这是斯图加特神经网络模拟器(SNNS)的 R 包装器。SNNS 最初是用 C 语言编写的,后来移植到 C++。RSNNS包使许多 SNNS 的模型组件得以使用,从而可以训练多种类型的模型。有关RSNNS包的更多详细信息,请参见Bergmeir, C. 和 Benitez, J. M.(2012)。我们将在第二章 训练预测模型中看到如何使用这些模型的示例。
deepnet包提供了多种深度学习工具,特别是它可以训练 RBM,并将其用作 DBN 的一部分,生成初始值以训练深度神经网络。deepnet包还允许使用不同的激活函数,并支持使用 dropout 进行正则化。
R 的深度学习框架
有许多 R 包可用于神经网络,但深度学习的选择较少。当本书的第一版发布时,它使用了 h2o 中的深度学习功能(www.h2o.ai/)。这是一个由 Java 编写的优秀通用机器学习框架,且有一个 API 可以从 R 中使用。我推荐你去看一下,特别是当你处理大型数据集时。然而,大多数深度学习从业者更倾向于使用其他深度学习库,如 TensorFlow、CNTK 和 MXNet,而这些库在本书第一版编写时并未在 R 中得到支持。如今,R 中已经有了很多支持的深度学习库——MXNet 和 Keras。Keras 实际上是其他深度学习库的前端抽象,并可以在后台使用 TensorFlow。本书中,我们将使用 MXNet、Keras 和 TensorFlow。
MXNet
MXNet 是由 Amazon 开发的深度学习库,支持在 CPU 和 GPU 上运行。本章中,只在 CPU 上运行即可。
Apache MXNet 是一个灵活且可扩展的深度学习框架,支持卷积神经网络(CNNs)和长短期记忆网络(LSTMs)。它可以在多个处理器/机器上分布式运行,并在多个 GPU/CPU 上几乎实现线性扩展。它可以轻松安装到 R 中,并支持 R 的广泛深度学习功能。它是编写我们第一个深度学习图像分类模型的绝佳选择。
MXNet 起源于 卡内基梅隆大学,并且得到了 Amazon 的大力支持;他们在 2016 年将其选为默认的深度学习库。2017 年,MXNet 被接受为 Apache Incubator 项目,确保其保持开源软件。它有一个类似于 Keras 的更高层次编程模型,但报告的性能更好。随着增加更多 GPU,MXNet 具有很好的可扩展性。
要为 Windows 安装 MXNet 包,请在 R 会话中运行以下代码:
cran <- getOption("repos")
cran["dmlc"] <- "https://apache-mxnet.s3-accelerate.dualstack.amazonaws.com/R/CRAN"
options(repos = cran)
install.packages("mxnet")
这将安装 CPU 版本;如果需要 GPU 版本,你需要将第二行更改为:
cran["dmlc"] <- "https://apache-mxnet.s3-accelerate.dualstack.amazonaws.com/R/CRAN/GPU/cu92"
你必须根据你机器上安装的 CUDA 版本将 cu92 更改为 cu80、cu90 或 cu91。对于其他操作系统(以及万一此方法不工作,因为深度学习领域变化很快),你可以参考 mxnet.incubator.apache.org/install/index.html 获取进一步的安装说明。
Keras
Keras 是一个由 Google 的 François Chollet 创建的高层次开源深度学习框架,强调迭代和快速开发;它通常被认为是学习深度学习的最佳选择之一。Keras 可以选择后端低层次的框架:TensorFlow、Theano 或 CNTK,但它最常与 TensorFlow 一起使用。Keras 模型几乎可以部署到任何环境中,例如,Web 服务器、iOS、Android、浏览器或 Raspberry Pi。
要了解更多关于 Keras 的信息,请访问 keras.io/。要了解如何在 R 中使用 Keras,请访问 keras.rstudio.com;该链接还提供了更多关于 R 和 Keras 的示例,并且有一张方便的 Keras 备忘单,详细介绍了 R Keras 包的所有功能。要为 R 安装 keras 包,请运行以下代码:
devtools::install_github("rstudio/keras")
library(keras)
install_keras()
这将安装基于 CPU 的 Keras 和 TensorFlow 包。如果你的机器有合适的 GPU,可以参考 install_keras() 的文档,了解如何安装它。
我需要 GPU 吗(它到底是什么)?
深度学习指数级增长的两个最大原因可能是:
-
能够累积、存储和处理各种类型的大型数据集
-
使用 GPU 训练深度学习模型的能力
那么,GPU 到底是什么?它们为什么对深度学习如此重要?可能最好的起点是实际看看 CPU,并分析为什么它不适合训练深度学习模型。现代 PC 中的 CPU 是人类设计与工程的巅峰之一。即使是手机中的芯片,现在也比第一代航天飞机的整个计算机系统还要强大。然而,由于 CPU 是为处理所有任务而设计的,它们可能不是处理某些特殊任务的最佳选择。一个这样的任务就是高端图形处理。
如果我们回到上世纪 90 年代中期,大多数游戏都是 2D 的,例如平台游戏,在这些游戏中,角色要在各种平台之间跳跃和/或避开障碍物。今天,几乎所有的计算机游戏都利用了 3D 空间。现代主机和个人电脑都有协处理器,负责将 3D 空间建模到 2D 屏幕上。这些协处理器被称为GPU。
GPU 实际上比 CPU 简单得多。它们被设计为只做一件事:大规模并行矩阵运算。CPU 和 GPU 都有核心,实际计算发生在这些核心中。一台配有 Intel i7 CPU 的 PC 有四个物理核心和通过超线程使用的八个虚拟核心。NVIDIA TITAN Xp GPU 卡有 3840 个 CUDA®核心。这些核心不是直接可比的;CPU 中的核心比 GPU 中的核心更强大。但是,如果工作负载需要大量可以独立完成的矩阵操作,具有许多简单核心的芯片会快得多。
在深度学习甚至成为一个概念之前,神经网络研究人员意识到,进行高端图形和训练神经网络都涉及到工作负载:大量可以并行进行的矩阵乘法。他们意识到,在 GPU 上训练模型而不是 CPU 将允许他们创建更复杂的模型。
如今,所有深度学习框架都在 GPU 和 CPU 上运行。事实上,如果你想要从头开始训练模型和/或有大量数据,你几乎肯定需要一块 GPU。GPU 必须是 NVIDIA GPU,你还需要安装 CUDA® Toolkit、NVIDIA 驱动程序和 cuDNN。这些允许你与 GPU 进行接口交互,劫持它的使用,从一个图形卡转变为数学协处理器。安装这些并不总是容易,你必须确保你使用的 CUDA、cuDNN 版本和深度学习库是兼容的。有些人建议你需要使用 Unix 而不是 Windows,但是 Windows 上的支持已经大大改善。这本书的代码是在 Windows 工作站上开发的。忘记在 macOS 上使用,因为它们不支持 NVIDIA 卡。
这是坏消息。好消息是,即使没有合适的 GPU,你也可以学会关于深度学习的一切。本书早期章节的例子在现代 PC 上运行得很好。当我们需要扩展时,本书将解释如何利用云资源,如 AWS 和 Google Cloud,来训练大型深度学习模型。
设置可复现的结果
数据科学软件正在快速发展和变化。虽然这对于进步是令人兴奋的,但也可能使得重现他人的结果变得具有挑战性。即使是您自己的代码,在几个月后回过头来看,也可能无法正常工作。这是当今科学研究中的一个重大问题,涉及所有领域,不仅仅是人工智能和机器学习。如果您从事科研或学术工作,并且想要在科学期刊上发布您的研究成果,那么这是您需要关注的问题。本书的第一版部分解决了这个问题,使用了 Revolution Analytics 提供的 R checkpoint 包。该包记录了使用过的软件版本,并确保可以获取其快照。
对于第二版,我们将不再使用此包,原因有多个:
-
大多数读者可能并不会发布自己的作品,而是更关注其他问题(如最大化准确性、可解释性等)。
-
深度学习需要大规模的数据集。当您拥有大量数据时,意味着虽然每次结果可能不完全相同,但差距非常小(仅为百分比的几分之一)。
-
在生产系统中,重现性不仅仅是软件的问题。您还必须考虑数据管道和随机种子生成。
-
为了确保可重现性,所使用的库必须保持冻结状态。深度学习 API 的新版不断发布,并可能包含改进。如果我们仅限于使用旧版本,我们可能会得到较差的结果。
如果您有兴趣了解更多关于checkpoint包的信息,可以阅读该包的在线手册:cran.r-project.org/web/packages/checkpoint/vignettes/checkpoint.html。
本书是使用 Windows 10 Professional x64 版的 R 3.5 编写的,这是本书编写时 R 的最新版本。代码是在搭载 Intel i5 处理器和 32 GB 内存的计算机上运行的;它也应能在搭载 Intel i3 处理器和 8 GB 内存的计算机上运行。
您可以从您的帐户中下载本书的示例代码文件:www.packtpub.com/。如果您在其他地方购买了本书,可以访问www.packtpub.com/support并注册,将文件直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的支持选项卡上。
-
点击代码下载与勘误。
-
在搜索框中输入书名。
-
选择您希望下载代码文件的书籍。
-
从下拉菜单中选择您购买本书的地方。
-
点击代码下载。
文件下载后,请确保使用最新版本的以下工具解压或提取文件夹:
-
WinRAR I 7-Zip for Windows
-
Zipeg I iZip I UnRarX for Mac
-
7-Zip I PeaZip for Linux
总结
本章简要介绍了神经网络和深度神经网络。通过使用多个隐藏层,深度神经网络在机器学习中引发了一场革命。它们在各种机器学习任务中始终表现优于其他方法,特别是在计算机视觉、自然语言处理和语音识别等领域。
本章还探讨了神经网络背后的部分理论,浅层神经网络与深层神经网络的区别,以及当前关于深度学习的一些误解。
我们在本章结束时讨论了如何设置 R 以及使用 GUI(RStudio)的重要性。本节讨论了 R 中可用的深度学习库(MXNet、Keras 和 TensorFlow)、GPU 以及可重复性。
在下一章,我们将开始训练神经网络并生成我们自己的预测。
第二章:训练预测模型
本章通过实际示例向你展示如何在 R 中构建和训练基础的神经网络,并展示如何评估不同的超参数来找到最佳的模型设置。深度学习中的另一个重要问题是过拟合,即模型在训练数据上表现良好,但在未见过的数据上表现差。本章简要介绍了这个问题,我们将在第三章,深度学习基础中深入探讨。最后通过一个实际用例,使用智能手机的活动数据分类为步行、上下楼梯、坐着、站着或躺着。
本章涵盖以下主题:
-
R 中的神经网络
-
二分类
-
可视化神经网络
-
使用 nnet 和 RSNNS 包进行多分类
-
数据过拟合问题——解释其后果
-
用例——构建和应用神经网络
R 中的神经网络
在本节中,我们将构建几个神经网络。首先,我们将使用 neuralnet 包创建一个神经网络模型,并且可以可视化。我们还将使用nnet和RSNNS(Bergmeir, C., 和 Benítez, J. M.(2012))包。这些是标准的 R 包,可以通过install.packages命令或 RStudio 中的包面板安装。虽然可以直接使用nnet包,但我们将通过caret包来使用它,caret是分类和回归训练(Classification and Regression Training)的缩写。caret包为 R 中的许多机器学习(ML)模型提供了一个标准化的接口,并且还具备一些用于验证和性能评估的实用功能,我们将在本章和下章中使用这些功能。
对于我们构建神经网络的第一个示例,我们将使用MNIST数据集,这是一个经典的分类问题:根据图片识别手写数字。数据可以从 Apache MXNet 网站下载(apache-mxnet.s3-accelerate.dualstack.amazonaws.com/R/data/mnist_csv.zip)。数据格式为 CSV,其中每一列数据或特征表示图片中的一个像素。每张图片有 784 个像素(28 x 28),像素是灰度的,范围从 0 到 255。第一列包含数字标签,其余列是像素值,用于分类。
构建神经网络模型
代码位于本书Chapter2文件夹中。如果你还没有下载并解压代码,请回到第一章,深度学习入门,获取下载代码的链接。将代码解压到你的机器中的一个文件夹,你将看到不同章节的文件夹。我们将使用的代码是Chapter2\chapter2.R。
我们将使用MNIST数据集来构建一些神经网络模型。脚本的前几行查看数据文件(train.csv)是否在数据目录中。如果文件已经存在于数据目录中,则程序继续执行;如果不存在,则从apache-mxnet.s3-accelerate.dualstack.amazonaws.com/R/data/mnist_csv.zip下载 ZIP 文件,并解压到数据文件夹中。这个检查意味着您不必手动下载数据,并且程序只下载文件一次。以下是下载数据的代码:
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=""))
}
作为替代方案,MNIST数据也可以在 Keras 中获得,因此我们可以从该库中下载并保存为 CSV 文件:
if (!file.exists(paste(dataDirectory,'/train.csv',sep="")))
{
library(keras)
mnist <- dataset_mnist()
c(c(x_train,y_train),c(x_test,y_test)) %<-% dataset_mnist()
x_train <- array_reshape(x_train,c(dim(x_train)[1],dim(x_train)[2]*dim(x_train)[3]))
y_train <- array_reshape(y_train,c(length(y_train),1))
data_mnist <- as.data.frame(cbind(y_train,x_train))
colnames(data_mnist)[1] <- "label"
colnames(data_mnist)[2:ncol(data_mnist)] <- paste("pixel",seq(1,784),sep="")
write.csv(data_mnist,paste(dataDirectory,'/train.csv',sep=""),row.names = FALSE)
}
当您第一次加载任何新数据集时,您应该快速检查数据,以确保行数和列数符合预期,如以下代码所示:
digits <- read.csv("../data/train.csv")
dim(digits)
[1] 42000 785
head(colnames(digits), 4)
[1] "label" "pixel0" "pixel1" "pixel2"
tail(colnames(digits), 4)
[1] "pixel780" "pixel781" "pixel782" "pixel783"
head(digits[, 1:4])
label pixel0 pixel1 pixel2
1 1 0 0 0
2 0 0 0 0
3 1 0 0 0
4 4 0 0 0
5 0 0 0 0
6 0 0 0 0
数据看起来不错,我们有42000行和785列。头部被正确导入,值是数值型的。现在我们已经加载了数据并对其进行了一些验证检查,我们可以继续建模。我们的第一个模型将使用neuralnet库,因为这允许我们可视化神经网络。我们将仅选择标签为 5 或 6 的行,并创建一个二元分类任务来区分它们。当然,您可以选择任何您喜欢的数字,但选择 5 和 6 是一个不错的选择,因为它们在图形上相似,因此我们的模型将比选择不那么相似的两个数字(例如 1 和 8)更难工作。我们将标签重命名为 0 和 1 进行建模,然后将该数据分成训练集和测试集。
然后,我们对训练数据使用主成分分析(PCA)进行降维——我们使用 PCA 是因为我们希望将数据中的预测变量数量减少到一个合理的数量以进行绘图。PCA 要求我们移除具有零方差的列;这些列在每个实例中具有相同的值。在我们的图像数据中,所有图像周围都有一个边框,即所有值都为零。请注意,我们如何使用仅用于训练模型的数据找到具有零方差的列;先应用此检查然后再拆分数据进行建模是不正确的。
降维:我们的图像数据是灰度数据,取值范围从 0(黑色)到 255(白色)。这些值高度相关,即如果一个像素是黑色(即 0),则其周围的像素很可能是黑色或深灰色。类似地,如果一个像素是白色(255),则其周围的像素很可能是白色或浅灰色。降维是一种无监督机器学习技术,它接受一个输入数据集并生成一个具有相同特性的输出数据集。
这样的操作将会减少数据的行数,但列数更少。不过,关键在于,这些更少的列可以解释输入数据集中的大部分信号。PCA 是一种降维算法。我们在这里使用它是因为我们想要创建一个列数较少的数据集来绘制网络,但我们仍然希望我们的算法能够产生良好的结果。
以下代码选择了标签为 5 或 6 的行,并创建了一个训练/测试数据集的拆分。它还去除了方差为零的列,这些列在每一行中的值都相同:
digits_nn <- digits[(digits$label==5) | (digits$label==6),]
digits_nn$y <- digits_nn$label
digits_nn$label <- NULL
table(digits_nn$y)
5 6
3795 4137
digits_nn$y <- ifelse(digits_nn$y==5, 0, 1)
table(digits_nn$y)
0 1
3795 4137
set.seed(42)
sample <- sample(nrow(digits_nn), nrow(digits_nn)*0.8)
test <- setdiff(seq_len(nrow(digits_nn)), sample)
digits.X2 <- digits_nn[,apply(digits_nn[sample,1:(ncol(digits_nn)-1)], 2, var, na.rm=TRUE) != 0]
length(digits.X2)
[1] 624
我们将列数据的数量从784减少到了624,也就是说,160列在所有行中具有相同的值。现在,我们对数据进行 PCA 分析并绘制方差的累积和:
df.pca <- prcomp(digits.X2[sample,],center = TRUE,scale. = TRUE)
s<-summary(df.pca)
cumprop<-s$importance[3, ]
plot(cumprop, type = "l",main="Cumulative sum",xlab="PCA component")
PCA 解释方差的累积和展示了需要多少主成分才能解释输入数据中的方差比例。通俗来说,这张图表明我们可以使用前 100 个变量(主成分),这就能解释原始数据中超过 80%的方差:

图 2.1:主成分解释方差的累积和。
下一个代码块选择了解释我们 50%方差的主成分,并使用这些变量来创建神经网络:
num_cols <- min(which(cumprop>0.5))
cumprop[num_cols]
PC23
0.50275
newdat<-data.frame(df.pca$x[,1:num_cols])
newdat$y<-digits_nn[sample,"y"]
col_names <- names(newdat)
f <- as.formula(paste("y ~", paste(col_names[!col_names %in% "y"],collapse="+")))
nn <- neuralnet(f,data=newdat,hidden=c(4,2),linear.output = FALSE)
我们可以看到,原始数据中 50%的方差只需 23 个主成分就能解释。接下来,我们通过调用plot函数绘制神经网络:
plot(nn)
这会生成一个类似于以下屏幕截图的图。我们可以看到输入变量(PC1到PC23)、隐藏层和偏置,甚至是网络权重:

图 2.2:带有权重和偏置的神经网络示例
我们选择了 23 个主成分作为我们神经网络库的预测变量。我们决定使用两个隐藏层,第一个有四个节点,第二个有两个节点。该图输出了系数,虽然从图中并不容易解读,但如果需要,还是可以使用相关函数访问它们。
接下来,我们将在一个未参与构建降维或神经网络模型的保留数据集或测试数据集上进行预测。我们需要先将测试数据传递给predict函数,传入之前创建的df.pca对象,以获取测试数据的主成分。然后,我们可以将这些数据传入神经网络预测中(将列过滤到前 23 个主成分),最后显示混淆矩阵和总体准确率:
test.data <- predict(df.pca, newdata=digits_nn[test,colnames(digits.X2)])
test.data <- as.data.frame(test.data)
preds <- compute(nn,test.data[,1:num_cols])
preds <- ifelse(preds$net.result > 0.5, "1", "0")
t<-table(digits_nn[test,"y"], preds,dnn=c("Actual", "Predicted"))
acc<-round(100.0*sum(diag(t))/sum(t),2)
print(t)
Predicted
Actual 0 1
0 740 17
1 17 813
print(sprintf(" accuracy = %1.2f%%",acc))
[1] " accuracy = 97.86%"
我们达到了97.86%的准确率,考虑到我们仅使用了 23 个主成分在神经网络中,这个结果相当不错。需要注意的是,这些 23 个变量并不能直接与输入数据集中的任何列或彼此进行比较。事实上,PCA(主成分分析)或任何降维算法的关键就在于生成相互之间没有相关性的列。
接下来,我们将创建执行多分类的模型,也就是说,它们可以对数字 0-9 进行分类。我们将标签(数字 0 到 9)转换为因子,以便 R 知道这是一个分类问题而不是回归问题。对于实际问题,应该使用所有可用的数据,但如果我们使用所有 42,000 行数据,使用 R 中的神经网络包进行训练将需要很长时间。因此,我们将选择 5,000 行用于训练,1,000 行用于测试。我们应随机选择这些行,并确保训练集和测试集之间没有重复的行。我们还将数据分为特征或预测变量(digits.x)和结果(digits.Y)。在这里,我们使用除了标签之外的所有列作为预测变量:
sample <- sample(nrow(digits), 6000)
train <- sample[1:5000]
test <- sample[5001:6000]
digits.X <- digits[train, -1]
digits.y_n <- digits[train, 1]
digits$label <- factor(digits$label, levels = 0:9)
digits.y <- digits[train, 1]
digits.test.X <- digits[test, -1]
digits.test.y <- digits[test, 1]
rm(sample,train,test)
最后,在开始构建我们的神经网络之前,让我们快速检查一下数字的分布情况。这一点很重要,例如,如果某个数字非常少见,我们可能需要调整建模方法,以确保在性能评估中对该数字给予足够的权重,特别是当我们希望准确预测该特定数字时。以下代码片段创建了一个条形图,展示了每个数字标签的频率:
barplot(table(digits.y),main="Distribution of y values (train)")
从图中我们可以看到,各类别的分布相对均匀,因此无需增加任何特定类别的权重或重要性:

图 2.3:训练数据集的 y 值分布
现在,让我们通过caret包的包装器使用nnet包来构建和训练我们的第一个神经网络。首先,我们使用set.seed()函数并指定一个特定的种子,以确保结果是可重复的。具体的种子值并不重要,重要的是每次运行脚本时使用相同的种子。train()函数首先接受特征或预测数据(x),然后是结果变量(y)作为参数。train()函数可以与多种模型一起使用,具体模型通过method参数确定。尽管机器学习模型的许多方面是自动学习的,但有些参数需要手动设置。这些参数因使用的方法而异;例如,在神经网络中,一个参数是隐藏单元的数量。train()函数提供了一种简单的方式,将各种调优参数作为命名的数据框传递给tuneGrid参数。它返回每组调优参数的性能测量值,并返回最佳训练模型。我们将从仅有五个隐藏神经元的模型开始,并使用适中的衰减率。学习率控制每次迭代或步骤对当前权重的影响程度。衰减率是正则化超参数,用于防止模型过拟合。另一个参数trcontrol控制train()的其他方面,当评估多种调优参数时,用于告知caret包如何验证并选择最佳调优参数。
对于这个例子,我们将训练控制方法设置为none,因为这里只有一组调优参数在使用。最后,我们可以指定额外的命名参数,这些参数会传递给实际的nnet()函数(或指定的任何算法)。由于预测变量的数量(784),我们将最大权重数增加到 10,000,并指定最大迭代次数为 100。由于数据量相对较小,且隐藏神经元较少,这个初始模型运行时间并不长:
set.seed(42)
tic <- proc.time()
digits.m1 <- caret::train(digits.X, digits.y,
method = "nnet",
tuneGrid = expand.grid(
.size = c(5),
.decay = 0.1),
trControl = trainControl(method = "none"),
MaxNWts = 10000,
maxit = 100)
print(proc.time() - tic)
user system elapsed
54.47 0.06 54.85
predict()函数为数据生成一组预测值。我们将使用测试数据集来评估模型;该数据集包含未用于训练模型的记录。我们将在下图中检查预测数字的分布。
digits.yhat1 <- predict(digits.m1,newdata=digits.test.X)
accuracy <- 100.0*sum(digits.yhat1==digits.test.y)/length(digits.test.y)
print(sprintf(" accuracy = %1.2f%%",accuracy))
[1] " accuracy = 54.80%"
barplot(table(digits.yhat1),main="Distribution of y values (model 1)")
很明显,这不是一个好的模型,因为预测值的分布与实际值的分布差异很大:

图 2.4:预测模型中y值的分布
barplot是对预测结果的简单检查,显示了我们的模型并不十分准确。我们还可以通过计算预测结果中与实际值匹配的行的百分比来计算准确率。该模型的准确率为54.8%,并不理想。通过caret包中的confusionMatrix()函数,可以更正式地评估模型的性能。由于在RSNNS包中也有同名函数,它们被掩盖了,因此我们使用caret::confusionMatrix来确保调用的是caret包中的函数。以下代码展示了混淆矩阵和测试集上的性能指标:
caret::confusionMatrix(xtabs(~digits.yhat1 + digits.test.y))
Confusion Matrix and Statistics
digits.test.y
digits.yhat1 0 1 2 3 4 5 6 7 8 9
0 61 1 0 1 0 2 0 0 0 1
1 1 104 0 2 0 4 3 9 12 8
2 6 2 91 56 4 20 68 1 41 1
3 0 0 0 0 0 0 0 0 0 0
4 2 0 4 1 67 1 22 4 2 21
5 39 0 6 45 4 46 0 5 30 16
6 0 0 0 0 0 0 0 0 0 0
7 0 0 0 6 9 0 0 91 2 75
8 0 0 0 0 0 0 0 0 0 0
9 0 0 0 0 0 0 0 3 0 0
Overall Statistics
Accuracy : 0.46
95% CI : (0.4288, 0.4915)
No Information Rate : 0.122
P-Value [Acc > NIR] : < 2.2e-16
Kappa : 0.4019
Mcnemar's Test P-Value : NA
Statistics by Class:
Class: 0 Class: 1 Class: 2 Class: 3 Class: 4 Class: 5 Class: 6
Sensitivity 0.5596 0.9720 0.9010 0.000 0.7976 0.6301 0.000
Specificity 0.9944 0.9563 0.7786 1.000 0.9378 0.8436 1.000
Pos Pred Value 0.9242 0.7273 0.3138 NaN 0.5403 0.2408 NaN
Neg Pred Value 0.9486 0.9965 0.9859 0.889 0.9806 0.9666 0.907
Prevalence 0.1090 0.1070 0.1010 0.111 0.0840 0.0730 0.093
Detection Rate 0.0610 0.1040 0.0910 0.000 0.0670 0.0460 0.000
Detection Prevalence 0.0660 0.1430 0.2900 0.000 0.1240 0.1910 0.000
Balanced Accuracy 0.7770 0.9641 0.8398 0.500 0.8677 0.7369 0.500
Class: 7 Class: 8 Class: 9
Sensitivity 0.8053 0.000 0.0000
Specificity 0.8963 1.000 0.9966
Pos Pred Value 0.4973 NaN 0.0000
Neg Pred Value 0.9731 0.913 0.8776
Prevalence 0.1130 0.087 0.1220
Detection Rate 0.0910 0.000 0.0000
Detection Prevalence 0.1830 0.000 0.0030
Balanced Accuracy 0.8508 0.500 0.4983
由于我们有多个数字,性能输出分为三个主要部分。首先,显示的是实际频率的交叉表。正确的预测位于对角线,误分类的不同频率出现在非对角线上。接下来是总体统计数据,表示模型在所有类别上的性能。准确率仅仅是正确分类的案例比例,并附带一个 95%的置信区间,这对于较小的数据集尤为有用,因为在这些数据集中估计值可能存在较大的不确定性。
No Information Rate指的是如果没有任何信息,仅通过猜测最常见的类别来期望的准确率,在此例中为 1,该类别出现的频率为 11.16%。p 值测试观察到的准确率(44.3%)是否与No Information Rate(11.2%)显著不同。尽管在统计学上显著,这对数字分类并没有太大意义,因为我们期望比仅仅猜测最常见的数字要好得多!最后,显示了每个数字的个体性能指标。这些指标是通过计算该数字与其他每个数字的比较来得出的,因此每个指标都是一个二分类比较。
现在我们对如何设置、训练和评估模型性能有了一些基本了解,我们将尝试增加隐藏神经元的数量,这是提高模型性能的一个关键方式,但代价是大幅增加模型复杂性。回想一下第一章,深度学习入门,每个预测器或特征都会连接到每个隐藏神经元,而每个隐藏神经元又与每个输出或结果相连接。对于784个特征,每增加一个隐藏神经元就会增加大量参数,这也会导致运行时间延长。根据你的计算机配置,准备好等待几分钟,直到下一个模型运行完成:
set.seed(42)
tic <- proc.time()
digits.m2 <- caret::train(digits.X, digits.y,
method = "nnet",
tuneGrid = expand.grid(
.size = c(10),
.decay = 0.1),
trControl = trainControl(method = "none"),
MaxNWts = 50000,
maxit = 100)
print(proc.time() - tic)
user system elapsed
154.49 0.09 155.33
digits.yhat2 <- predict(digits.m2,newdata=digits.test.X)
accuracy <- 100.0*sum(digits.yhat2==digits.test.y)/length(digits.test.y)
print(sprintf(" accuracy = %1.2f%%",accuracy))
[1] " accuracy = 66.30%"
barplot(table(digits.yhat2),main="Distribution of y values (model 2)")
这个模型比上一个模型要好,但预测值的分布仍然不均匀:

图 2.5:预测模型的y值分布
将隐藏神经元的数量从 5 增加到 10,提高了我们的样本内表现,整体准确率从54.8%提升到66.3%,但这仍然距离理想状态有一定差距(想象一下,一个字符识别软件会混淆超过 30% 的所有字符!)。我们再次增加数量,这次增加到 40 个隐藏神经元,并且让模型等待更长时间才能完成训练:
set.seed(42)
tic <- proc.time()
digits.m3 <- caret::train(digits.X, digits.y,
method = "nnet",
tuneGrid = expand.grid(
.size = c(40),
.decay = 0.1),
trControl = trainControl(method = "none"),
MaxNWts = 50000,
maxit = 100)
print(proc.time() - tic)
user system elapsed
2450.16 0.96 2457.55
digits.yhat3 <- predict(digits.m3,newdata=digits.test.X)
accuracy <- 100.0*sum(digits.yhat3==digits.test.y)/length(digits.test.y)
print(sprintf(" accuracy = %1.2f%%",accuracy))
[1] " accuracy = 82.20%"
barplot(table(digits.yhat3),main="Distribution of y values (model 3)")
在这个模型中,预测值的分布是均匀的,这是我们所追求的。然而,准确率仍然只有 82.2%,这相当低:

图 2.6:y 值的分布来自预测模型
使用 40 个隐藏神经元后,整体准确率提高到了82.2%,且在一台 i5 电脑上运行耗时超过 40 分钟。某些数字的模型表现仍然不理想。如果这是一个真实的研究或商业问题,我们可能会继续尝试更多的神经元、调整衰减率或修改特征,以进一步提高模型表现,但现在我们将继续进行下一步。
接下来,我们将看看如何使用 RSNNS 包来训练神经网络。该包提供了一个接口,能够使用斯图加特神经网络模拟器(SNNS)代码来实现多种可能的模型;然而,对于基本的单隐层前馈神经网络,我们可以使用mlp()便捷包装函数,这代表了多层感知机。RSNNS 包的使用比通过caret包中的 nnet 更加复杂,但其一个优势是,它可以更灵活,允许训练许多其他类型的神经网络架构,包括递归神经网络,同时也提供了更多的训练策略。
nnet 和 RSNNS 包之间的一个区别是,对于多类别的输出(例如数字),RSNNS 需要进行虚拟编码(即独热编码),每个可能的类别作为一列,使用 0/1 编码。这可以通过使用decodeClassLabels()函数来实现,如下所示的代码片段:
head(decodeClassLabels(digits.y))
0 1 2 3 4 5 6 7 8 9
[1,] 0 0 0 0 0 0 0 0 0 1
[2,] 0 0 0 0 1 0 0 0 0 0
[3,] 1 0 0 0 0 0 0 0 0 0
[4,] 0 0 0 0 0 1 0 0 0 0
[5,] 0 0 0 0 1 0 0 0 0 0
[6,] 0 0 0 1 0 0 0 0 0 0
由于我们在使用 40 个隐藏神经元时取得了相当不错的效果,所以这里我们将使用相同的规模。我们将使用基于 Riedmiller, M.和 Braun, H.(1993)工作的弹性传播,而不是标准传播作为学习函数。弹性反向传播是标准反向传播的优化方法,它应用了更快速的权重更新机制。随着神经网络复杂度的增加,训练时间通常较长,这是一个常见的问题。我们将在后续章节中深入讨论这一点,但目前,你只需要知道这个神经网络更快,因为它会跟踪过去的导数,如果它们在反向传播过程中方向相同,它会采取更大的步伐。还要注意,因为传递的是一个结果矩阵,尽管单个数字的预测概率不会超过 1,但所有数字的预测概率总和可能超过 1,也可能小于 1(也就是说,对于某些情况,模型可能并不认为它们非常可能代表任何数字)。predict函数返回一个矩阵,其中每一列代表一个单独的数字,因此我们使用encodeClassLabels()函数将其转换回一个数字标签的单一向量,以便绘制和评估模型的性能:
set.seed(42)
tic <- proc.time()
digits.m4 <- mlp(as.matrix(digits.X),
decodeClassLabels(digits.y),
size = 40,
learnFunc = "Rprop",
shufflePatterns = FALSE,
maxit = 80)
print(proc.time() - tic)
user system elapsed
179.71 0.08 180.99
digits.yhat4 <- predict(digits.m4,newdata=digits.test.X)
digits.yhat4 <- encodeClassLabels(digits.yhat4)
accuracy <- 100.0*sum(I(digits.yhat4 - 1)==digits.test.y)/length(digits.test.y)
print(sprintf(" accuracy = %1.2f%%",accuracy))
[1] " accuracy = 81.70%"
barplot(table(digits.yhat4),main="Distribution of y values (model 4)")
下图条形图显示了预测值在各个类别之间相对均匀分布。这与实际类别值的分布相匹配:

图 2.7:来自预测模型的y值分布
准确率为 81.70%,且在我的计算机上运行了 3 分钟。这仅比我们使用 40 个隐藏节点的 nnet 稍低,当时在同一台机器上花费了 40 分钟!这展示了使用优化器的重要性,我们将在后续章节中看到这一点。
从神经网络生成预测
对于任何给定的观察值,它可能属于多个类别中的某一个(例如,一个观察值可能有 40%的概率是5,20%的概率是6,依此类推)。为了评估模型的性能,必须做出一些选择,将类别成员资格的概率转化为离散分类。在本节中,我们将更详细地探讨其中的一些选项。
只要没有完美的平局,最简单的方法就是基于最高预测概率来分类观察值。另一种方法是 RSNNS 包称之为赢家通吃(WTA)方法,选择具有最高概率的类别,前提是满足以下条件:
-
没有最高概率的平局情况
-
最高概率超过用户定义的阈值(阈值可以为零)
-
其余类别的预测概率都低于最大值减去另一个用户定义的阈值
否则,观测值将被分类为未知。如果两个阈值都为零(默认值),这相当于说必须有一个唯一的最大值。这种方法的优点是提供了一些质量控制。在我们一直在探讨的数字分类示例中,有 10 个可能的类别。
假设 9 个数字的预测概率为 0.099,其余类别的预测概率为 0.109。尽管某个类别在技术上比其他类别更可能,但差异相当微小,我们可以得出结论,模型无法以任何确定性分类该观测值。一种叫做 402040 的最终方法,只有当一个值超过用户定义的阈值,且所有其他值低于另一个用户定义的阈值时,才进行分类;如果多个值超过第一个阈值,或者任何值不低于第二个阈值,它将该观测值视为未知。再次强调,这里的目标是提供某种质量控制。
这看起来似乎是不必要的,因为预测的不确定性应该已经体现在模型的表现中。然而,知道你的模型在预测时是否高度确信并且预测正确,或者不确定且预测正确或错误,还是很有帮助的。
最后,在某些情况下,并非所有类别都同样重要。例如,在一个医学背景中,收集患者的多种生物标志物和基因,并用来分类他们是否面临癌症风险或心脏病风险,即使患者有 40%的癌症风险,可能也足以进一步调查,尽管他们有 60%的健康概率。这与我们之前看到的性能度量有关,除了整体准确度外,我们还可以评估诸如敏感性、特异性以及阳性和阴性预测值等方面。有些情况下,整体准确度并不如确保没有遗漏任何人更为重要。
以下代码显示了样本数据的原始概率,以及这些不同选择对预测值的影响:
digits.yhat4_b <- predict(digits.m4,newdata=digits.test.X)
head(round(digits.yhat4_b, 2))
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
18986 0.00 0.00 0.00 0.98 0.00 0.02 0.00 0.00 0.00 0.00
41494 0.00 0.00 0.03 0.00 0.13 0.01 0.95 0.00 0.00 0.00
21738 0.00 0.00 0.02 0.03 0.00 0.46 0.01 0.00 0.74 0.00
37086 0.00 0.01 0.00 0.63 0.02 0.01 0.00 0.00 0.03 0.00
35532 0.00 0.00 0.00 0.00 0.01 0.00 0.00 0.99 0.00 0.00
17889 0.03 0.00 0.00 0.00 0.00 0.34 0.01 0.00 0.00 0.00
table(encodeClassLabels(digits.yhat4_b,method = "WTA", l = 0, h = 0))
1 2 3 4 5 6 7 8 9 10
102 116 104 117 93 66 93 127 89 93
table(encodeClassLabels(digits.yhat4_b,method = "WTA", l = 0, h = .5))
0 1 2 3 4 5 6 7 8 9 10
141 95 113 86 93 67 53 89 116 73 74
table(encodeClassLabels(digits.yhat4_b,method = "WTA", l = .2, h = .5))
0 1 2 3 4 5 6 7 8 9 10
177 91 113 77 91 59 50 88 116 70 68
table(encodeClassLabels(digits.yhat4_b,method = "402040", l = .4, h = .6))
0 1 2 3 4 5 6 7 8 9 10
254 89 110 71 82 46 41 79 109 65 54
我们现在开始检查与过拟合数据相关的问题,以及这些问题对模型性能评估的影响。
过拟合数据的问题——后果解释
机器学习中的一个常见问题是过拟合数据。一般来说,过拟合是指模型在用于训练模型的数据上的表现优于在未用于训练模型的数据(如留出数据、未来的实际使用等)上的表现。过拟合发生在模型记住了部分训练数据,并拟合了训练数据中的噪声。训练数据中的准确度很高,但由于噪声在不同数据集之间变化,这种准确度并不适用于未见过的数据,也就是说,我们可以说模型的泛化能力不强。
过拟合随时可能发生,但随着参数与信息的比率增加,过拟合的情况往往会变得更严重。通常,这可以被视为参数与观测值的比率,但并不总是如此。例如,假设我们有一个非常不平衡的数据集,目标预测的结果是一个罕见事件,发生的概率为每五百万个案例中有一个。在这种情况下,1500 万的样本中可能只有 3 个正例。即使样本量很大,但信息量却很低。为了考虑一个简单但极端的情况,想象一下将一条直线拟合到两个数据点上。拟合会非常完美,在这两个训练数据上,你的线性回归模型似乎完全解释了数据中的所有变异性。然而,如果我们将这条线应用到另外 1000 个案例上,它可能就不太适用了。
在前面的章节中,我们生成了模型的样本外预测,也就是说,我们评估了在测试(或保留)数据上的准确性。但我们从未检查过模型是否出现了过拟合,也就是测试数据上的准确性。我们可以通过检查样本内预测的准确性来检验模型的泛化能力。我们可以看到,样本内数据的准确性为 84.7%,而保留数据的准确性为 81.7%。有一个 3.0%的损失;换句话说,使用训练数据来评估模型表现导致了对准确性的过度乐观估计,这一过高估计为 3.0%:
digits.yhat4.train <- predict(digits.m4)
digits.yhat4.train <- encodeClassLabels(digits.yhat4.train)
accuracy <- 100.0*sum(I(digits.yhat4.train - 1)==digits.y)/length(digits.y)
print(sprintf(" accuracy = %1.2f%%",accuracy))
[1] " accuracy = 84.70%"
由于我们之前拟合了多个不同复杂度的模型,我们可以通过比较样本内与样本外的表现衡量,检查过拟合或过度乐观的准确性程度。这里的代码应该足够简单易懂。我们调用模型的预测函数,并且不传入任何新数据;这将返回模型所训练的数据的预测结果。其余的代码是标准的代码,用来生成图形图表。
digits.yhat1.train <- predict(digits.m1)
digits.yhat2.train <- predict(digits.m2)
digits.yhat3.train <- predict(digits.m3)
digits.yhat4.train <- predict(digits.m4)
digits.yhat4.train <- encodeClassLabels(digits.yhat4.train)
measures <- c("AccuracyNull", "Accuracy", "AccuracyLower", "AccuracyUpper")
n5.insample <- caret::confusionMatrix(xtabs(~digits.y + digits.yhat1.train))
n5.outsample <- caret::confusionMatrix(xtabs(~digits.test.y + digits.yhat1))
n10.insample <- caret::confusionMatrix(xtabs(~digits.y + digits.yhat2.train))
n10.outsample <- caret::confusionMatrix(xtabs(~digits.test.y + digits.yhat2))
n40.insample <- caret::confusionMatrix(xtabs(~digits.y + digits.yhat3.train))
n40.outsample <- caret::confusionMatrix(xtabs(~digits.test.y + digits.yhat3))
n40b.insample <- caret::confusionMatrix(xtabs(~digits.y + I(digits.yhat4.train - 1)))
n40b.outsample <- caret::confusionMatrix(xtabs(~ digits.test.y + I(digits.yhat4 - 1)))
shrinkage <- rbind(
cbind(Size = 5, Sample = "In", as.data.frame(t(n5.insample$overall[measures]))),
cbind(Size = 5, Sample = "Out", as.data.frame(t(n5.outsample$overall[measures]))),
cbind(Size = 10, Sample = "In", as.data.frame(t(n10.insample$overall[measures]))),
cbind(Size = 10, Sample = "Out", as.data.frame(t(n10.outsample$overall[measures]))),
cbind(Size = 40, Sample = "In", as.data.frame(t(n40.insample$overall[measures]))),
cbind(Size = 40, Sample = "Out", as.data.frame(t(n40.outsample$overall[measures]))),
cbind(Size = 40, Sample = "In", as.data.frame(t(n40b.insample$overall[measures]))),
cbind(Size = 40, Sample = "Out", as.data.frame(t(n40b.outsample$overall[measures])))
)
shrinkage$Pkg <- rep(c("nnet", "RSNNS"), c(6, 2))
dodge <- position_dodge(width=0.4)
ggplot(shrinkage, aes(interaction(Size, Pkg, sep = " : "), Accuracy,
ymin = AccuracyLower, ymax = AccuracyUpper,
shape = Sample, linetype = Sample)) +
geom_point(size = 2.5, position = dodge) +
geom_errorbar(width = .25, position = dodge) +
xlab("") + ylab("Accuracy + 95% CI") +
theme_classic() +
theme(legend.key.size = unit(1, "cm"), legend.position = c(.8, .2))
代码生成如下图表,展示了准确性指标及这些指标的置信区间。从这张图表中我们注意到,随着模型变得更加复杂,样本内和样本外表现衡量之间的差距逐渐增大。这突出显示了复杂模型容易过拟合,也就是说,它们在样本内数据上的表现优于未见过的样本外数据:

图 2.8:神经网络模型的样本内和样本外表现衡量
用例 – 构建并应用神经网络
本章最后,我们将讨论神经网络的一个更现实的应用场景。我们将使用 Anguita, D., Ghio, A., Oneto, L., Parra, X., 和 Reyes-Ortiz, J. L.(2013)发布的公开数据集,该数据集使用智能手机来追踪身体活动。数据可以从archive.ics.uci.edu/ml/datasets/human+activity+recognition+using+smartphones下载。这些智能手机配备了加速度计和陀螺仪,从中提取了 561 个特征,涵盖了时域和频域数据。
智能手机在步行、走楼梯、下楼梯、站立、坐着和躺下时佩戴。虽然这些数据来自手机,但类似的度量可以从其他设计用于追踪活动的设备中得出,例如各种健身追踪手表或腕带。因此,如果我们想销售设备并自动跟踪佩戴者进行的这些不同活动,这些数据就非常有用。
这些数据已经归一化,范围从 -1 到 +1;通常,如果数据没有归一化,我们可能会进行一些归一化操作。请从链接下载数据,并将其解压到与章节文件夹同级的“data”文件夹中;我们在后续章节中也将使用这些数据。我们可以导入训练和测试数据,以及标签。接下来,我们可以通过以下代码快速查看结果变量的分布:
use.train.x <- read.table("../data/UCI HAR Dataset/train/X_train.txt")
use.train.y <- read.table("../data/UCI HAR Dataset/train/y_train.txt")[[1]]
use.test.x <- read.table("../data/UCI HAR Dataset/test/X_test.txt")
use.test.y <- read.table("../data/UCI HAR Dataset/test/y_test.txt")[[1]]
use.labels <- read.table("../data/UCI HAR Dataset/activity_labels.txt")
barplot(table(use.train.y),main="Distribution of y values (UCI HAR Dataset)")
这将生成以下条形图,显示类别的分布相对均衡:

图 2.9:UCI HAR 数据集的 y 值分布
我们将评估各种调优参数,以展示如何尝试不同的方法来获得最佳的模型。我们将使用不同的超参数,并评估哪个模型表现最好。
由于模型的训练可能需要一些时间,并且 R 通常只使用单核处理器,我们将使用一些特殊的包来实现并行运行多个模型。这些包是parallel、foreach和doSNOW,如果你从脚本的第一行运行,它们应该已经被加载。
现在我们可以选择调优参数,并设置一个本地集群作为foreach R 包的后台,以支持并行 for 循环。请注意,如果你在少于五个核心的机器上执行此操作,应将makeCluster(5)更改为较低的数字:
## choose tuning parameters
tuning <- list(
size = c(40, 20, 20, 50, 50),
maxit = c(60, 100, 100, 100, 100),
shuffle = c(FALSE, FALSE, TRUE, FALSE, FALSE),
params = list(FALSE, FALSE, FALSE, FALSE, c(0.1, 20, 3)))
## setup cluster using 5 cores
## load packages, export required data and variables
## and register as a backend for use with the foreach package
cl <- makeCluster(5)
clusterEvalQ(cl, {source("cluster_inc.R")})
clusterExport(cl,
c("tuning", "use.train.x", "use.train.y",
"use.test.x", "use.test.y")
)
registerDoSNOW(cl)
现在我们准备训练所有模型。以下代码展示了一个并行 for 循环,使用的代码与之前看到的类似,但这次根据我们先前存储在列表中的调优参数设置了一些参数:
## train models in parallel
use.models <- foreach(i = 1:5, .combine = 'c') %dopar% {
if (tuning$params[[i]][1]) {
set.seed(42)
list(Model = mlp(
as.matrix(use.train.x),
decodeClassLabels(use.train.y),
size = tuning$size[[i]],
learnFunc = "Rprop",
shufflePatterns = tuning$shuffle[[i]],
learnFuncParams = tuning$params[[i]],
maxit = tuning$maxit[[i]]
))
} else {
set.seed(42)
list(Model = mlp(
as.matrix(use.train.x),
decodeClassLabels(use.train.y),
size = tuning$size[[i]],
learnFunc = "Rprop",
shufflePatterns = tuning$shuffle[[i]],
maxit = tuning$maxit[[i]]
))
}
}
由于生成样本外的预测也可能需要一些时间,我们将并行处理这个步骤。然而,首先我们需要将模型结果导出到集群中的每个工作节点,然后我们可以计算预测值:
## export models and calculate both in sample,
## 'fitted' and out of sample 'predicted' values
clusterExport(cl, "use.models")
use.yhat <- foreach(i = 1:5, .combine = 'c') %dopar% {
list(list(
Insample = encodeClassLabels(fitted.values(use.models[[i]])),
Outsample = encodeClassLabels(predict(use.models[[i]],
newdata = as.matrix(use.test.x)))
))
}
最后,我们可以将实际值和拟合值或预测值合并成一个数据集,计算每个值的表现度量,并将整体结果一起存储以供检查和比较。我们可以使用与接下来的代码几乎相同的代码来生成样本外的表现度量。该代码没有在书中显示,但可以在与书一起提供的代码包中找到。这里需要进行一些额外的数据管理,因为有时模型可能无法预测每个可能的响应级别,但这可能会导致非对称的频率交叉表,除非我们将变量转换为因子并指定级别。我们还会删除o值,这表示模型对如何分类一个观测结果存在不确定性:
use.insample <- cbind(Y = use.train.y,
do.call(cbind.data.frame, lapply(use.yhat, `[[`, "Insample")))
colnames(use.insample) <- c("Y", paste0("Yhat", 1:5))
performance.insample <- do.call(rbind, lapply(1:5, function(i) {
f <- substitute(~ Y + x, list(x = as.name(paste0("Yhat", i))))
use.dat <- use.insample[use.insample[,paste0("Yhat", i)] != 0, ]
use.dat$Y <- factor(use.dat$Y, levels = 1:6)
use.dat[, paste0("Yhat", i)] <- factor(use.dat[, paste0("Yhat", i)], levels = 1:6)
res <- caret::confusionMatrix(xtabs(f, data = use.dat))
cbind(Size = tuning$size[[i]],
Maxit = tuning$maxit[[i]],
Shuffle = tuning$shuffle[[i]],
as.data.frame(t(res$overall[c("AccuracyNull", "Accuracy", "AccuracyLower", "AccuracyUpper")])))
}))
use.outsample <- cbind(Y = use.test.y,
do.call(cbind.data.frame, lapply(use.yhat, `[[`, "Outsample")))
colnames(use.outsample) <- c("Y", paste0("Yhat", 1:5))
performance.outsample <- do.call(rbind, lapply(1:5, function(i) {
f <- substitute(~ Y + x, list(x = as.name(paste0("Yhat", i))))
use.dat <- use.outsample[use.outsample[,paste0("Yhat", i)] != 0, ]
use.dat$Y <- factor(use.dat$Y, levels = 1:6)
use.dat[, paste0("Yhat", i)] <- factor(use.dat[, paste0("Yhat", i)], levels = 1:6)
res <- caret::confusionMatrix(xtabs(f, data = use.dat))
cbind(Size = tuning$size[[i]],
Maxit = tuning$maxit[[i]],
Shuffle = tuning$shuffle[[i]],
as.data.frame(t(res$overall[c("AccuracyNull", "Accuracy", "AccuracyLower", "AccuracyUpper")])))
}))
如果我们打印出样本内和样本外的表现,我们可以看到每个模型的表现以及调整某些调参参数的效果。输出结果如下所示。第四列(null accuracy)被删除,因为它在这次比较中不那么重要:
options(width = 80, digits = 3)
performance.insample[,-4]
Size Maxit Shuffle Accuracy AccuracyLower AccuracyUpper
1 40 60 FALSE 0.984 0.981 0.987
2 20 100 FALSE 0.982 0.978 0.985
3 20 100 TRUE 0.982 0.978 0.985
4 50 100 FALSE 0.981 0.978 0.984
5 50 100 FALSE 1.000 0.999 1.000
performance.outsample[,-4]
Size Maxit Shuffle Accuracy AccuracyLower AccuracyUpper
1 40 60 FALSE 0.916 0.906 0.926
2 20 100 FALSE 0.913 0.902 0.923
3 20 100 TRUE 0.913 0.902 0.923
4 50 100 FALSE 0.910 0.900 0.920
5 50 100 FALSE 0.938 0.928 0.946
提醒一下,样本内结果评估的是训练数据上的预测,而样本外结果评估的是保留数据(或测试数据)上的预测。最佳的超参数集是最后一组,其中我们在未见过的数据上获得了 93.8%的准确率。这表明我们能够基于智能手机的数据相当准确地分类人们从事的活动类型。我们还可以看到,复杂模型在样本内数据上的表现更好,但在样本外表现度量上并不总是如此。
对于每个模型,我们在样本内数据和样本外数据的准确率之间存在很大差异;这些模型显然存在过拟合问题。我们将在第三章中探讨如何应对这种过拟合问题,深度学习基础,因为我们将在那里训练具有多个隐藏层的深度神经网络。
尽管样本外的表现稍微差一些,但这些模型仍然表现良好——远远超过仅凭运气的表现——对于我们的示例用例,我们可以选择最佳模型,并且可以非常有信心地认为,使用这个模型可以提供准确的用户活动分类。
总结
本章展示了如何开始构建和训练神经网络来对数据进行分类,包括图像识别和身体活动数据。我们查看了可以可视化神经网络的包,并创建了多个模型,以对具有 10 个不同类别的数据进行分类。尽管我们只使用了一些神经网络包,而不是深度学习包,但我们的模型训练时间很长,并且遇到了过拟合问题。
本章中的一些基本神经网络模型训练时间较长,尽管我们并没有使用所有可用的数据。对于 MNIST 数据集,我们在二分类任务中使用了大约 8,000 行数据,在多分类任务中只用了 6,000 行数据。即便如此,一个模型的训练时间仍然接近一个小时。我们的深度学习模型将更为复杂,并且应该能够处理数百万条记录。现在你应该明白为什么训练深度学习模型需要专门的硬件了。
其次,我们看到机器学习中的一个潜在陷阱是,复杂的模型更容易发生过拟合,因此,在使用训练数据评估模型性能时,可能会得到偏向的、过于乐观的性能估计。事实上,这甚至可能影响到选择哪种模型作为最优模型。过拟合对于深度神经网络也是一个问题。在下一章中,我们将讨论防止过拟合的各种技术,并获得更准确的模型性能评估。
在下一章,我们将从零开始构建一个神经网络,并了解它如何应用于深度学习。我们还将讨论一些应对过拟合的方法。
第三章:深度学习基础
在上一章中,我们使用 R 中的神经网络包创建了一些机器学习模型。本章将通过使用基本的数学和矩阵运算创建一个神经网络,来介绍一些神经网络和深度学习的基础知识。这个应用示例将有助于解释深度学习算法中的一些关键参数,以及一些允许它们在大数据集上训练的优化方法。我们还将展示如何评估模型的不同超参数,以找到最佳的设置。在上一章中,我们简要地讨论了过拟合问题;本章将更深入地探讨这个话题,并介绍如何克服这个问题。它包括一个使用 dropout(深度学习中最常见的正则化技术)的示例用例。
本章涵盖以下主题:
-
从零开始在 R 中构建神经网络
-
深度学习中的常见参数
-
深度学习算法中的一些关键组件
-
使用正则化克服过拟合
-
用例—使用 dropout 改善样本外模型性能
从零开始在 R 中构建神经网络
尽管我们已经使用了一些神经网络算法,但现在是时候更深入地了解它们是如何工作的了。本节演示了如何从头开始编写神经网络的代码。你可能会惊讶地发现,神经网络的核心代码可以用不到 80 行代码来编写!本章的代码正是这样做的,它使用 R 编写了一个交互式网络应用程序。这应该能让你更直观地理解神经网络。首先我们将看这个网络应用程序,然后我们将更深入地探讨神经网络的代码。
神经网络 Web 应用程序
首先,我们将看一个 R Shiny 网络应用程序。我鼓励你运行该应用并按照示例操作,这将真正帮助你更好地理解神经网络是如何工作的。为了运行它,你需要在 RStudio 中打开 Chapter3 项目。
什么是 R Shiny?
R Shiny 是 RStudio 公司提供的一个 R 包,它允许你仅使用 R 代码创建交互式 Web 应用程序。你可以构建仪表板和可视化,使用 R 的完整功能。你还可以通过 CSS、组件和 JavaScript 扩展 R Shiny 应用程序。还可以将你的应用程序托管在网上。它是一个展示数据科学应用的好工具,如果你还不熟悉它,我鼓励你了解一下。更多信息请见 shiny.rstudio.com/,如果你想了解 R Shiny 能做什么,可以查看 shiny.rstudio.com/gallery/。
- 打开
server.R文件,在 RStudio 中点击 Run App 按钮:

图 3.1:如何运行 R Shiny 应用程序
- 当你点击 "Run App" 按钮时,应该会弹出一个网页应用的屏幕。以下是应用启动后的截图:

图 3.2:R Shiny 应用启动时的界面
这个网页应用可以在弹出窗口中使用,也可以在浏览器中打开。左侧是输入选择的集合,这些是神经网络的参数。它们被称为超参数,以便与模型尝试优化的参数区分开来。从上到下,这些超参数包括:
-
选择数据:你可以使用四个不同的数据集作为训练数据。
-
隐藏层中的节点数:隐藏层中节点的数量。神经网络只有一个隐藏层。
-
# 迭代次数:模型构建过程中算法对数据迭代的次数。
-
学习率:在反向传播过程中应用的学习率。学习率影响算法在每次迭代时权重的变化量。
-
激活函数:应用于每个节点输出的激活函数。
-
"Run NN Model" 按钮使用输入选择的内容训练一个模型。"Reset" 按钮将输入选择恢复为默认值。
有四个不同的数据集可供选择,每个数据集的分布不同;你可以从下拉框中选择它们。它们有描述性名称;例如,图 3.2 中绘制的数据被称为 bulls_eye。这些数据集来自另一个用于测试聚类算法的 R 包。数据包含两个相等大小的类别,并由各种几何形状组成。你可以使用这个网页应用探索这些数据集。我们对数据所做的唯一改变是随机交换 5% 数据的标签。当你运行应用时,你会注意到在内圈有一些红色点,外圈有一些蓝色点。这是为了让我们的模型的最大准确度只能达到 0.95(95%)。这能让我们确信模型运行正常。如果准确度超过了这个值,模型可能会过拟合,因为它学习到的函数过于复杂。我们将在下一节中再次讨论过拟合问题。
机器学习的第一步之一应该是建立一个基准分数,这对于衡量你的进展非常有用。基准分数可以是经验法则,或者是一个简单的机器学习算法;它不应是你花费大量时间精力去研究的内容。在这个应用程序中,我们使用基本的逻辑回归模型作为基准。我们可以看到,在前面的截图中,逻辑回归模型的准确率只有 0.6075,或者说 60.75%。这仅仅比 50%稍高,但请记住,逻辑回归只能拟合一条直线,而这组数据无法用一条直线分割开来。神经网络应该能在逻辑回归的基准上有所改进,因此,如果我们在这个数据集上的准确率低于 0.6075,那说明我们的模型存在问题,应该检查。
那么,开始吧!点击“运行神经网络模型”按钮,该按钮使用输入的选择在数据上运行神经网络模型。几秒钟后,应用程序应会更改为以下截图所示的样子:

图 3.3:神经网络模型默认设置下的执行
应用程序需要几秒钟时间,然后它会创建一个关于# Epochs的成本函数图,并输出随着算法对数据进行迭代而得到的成本函数值。文本输出还包括屏幕右下角的最终准确率。在右下角的诊断信息中,我们可以看到,在训练过程中成本下降,我们达到了 0.825 的最终准确率。成本是模型试图最小化的值——成本越低,准确率越高。由于模型最初很难获得正确的权重,所以成本下降花了一些时间。
在深度学习模型中,权重和偏差不应使用随机值进行初始化。如果使用随机值,可能会导致训练问题,例如梯度消失或梯度爆炸。这是因为权重变得过小或过大,导致模型无法成功训练。另外,如果权重没有正确初始化,模型训练的时间会更长,正如我们之前所看到的。避免这些问题的两种最流行的初始化技术是 Xavier 初始化和 He 初始化(以它们的发明者命名)。
我们可以在图 3.3中看到,成本尚未趋于平稳,最后几组数值显示它仍在下降。这表明,如果我们训练模型更长时间,它是可以改进的。将# Epochs更改为7000,然后再次点击运行神经网络模型按钮;屏幕将更改为以下图形:

图 3.4:神经网络模型执行(更多的训练周期)
现在我们得到了 0.95 的准确率,这是最大可能的准确率。我们注意到成本值已经平稳(即不再进一步下降),大约为 0.21。这表明即使训练模型更长时间(即更多的迭代次数),结果也可能不会有所改善,无论当前的准确率如何。如果模型尚未充分训练且成本值已经平稳,我们就需要考虑更改模型的架构,或者获取更多的数据来提高准确率。让我们看看如何更改模型中的节点数量。点击重置按钮,将输入值更改为默认值,然后将节点数量更改为 7,点击运行神经网络模型按钮。现在屏幕将变为以下内容:

图 3.5:更多节点的神经网络模型执行
这里的准确率是 0.68,但与之前的示例相比,当我们使用相同的输入并且只有三个节点时,结果要差得多!实际上,更多的节点反而使性能更差!这是因为我们的数据模式相对简单,七个节点的模型可能过于复杂,训练时间也更长。向一层中添加更多的节点会增加训练时间,但不一定提高性能。
让我们看看学习率。点击重置按钮,将输入值更改为默认值,然后将学习率更改为大约5,然后再次点击运行神经网络模型按钮,复现以下屏幕:

图 3.6:较大学习率的神经网络模型执行
我们再次得到了 0.95 的准确率,这是最好的准确率。如果我们与之前的示例进行比较,可以看到模型收敛得更快(即成本函数平稳的时间),仅用了500次迭代。我们需要的迭代次数更少,因此可以看到学习率和训练周期数之间存在反比关系。较高的学习率可能意味着你需要更少的训练周期。但更大的学习率总是更好吗?嗯,不是的。
点击重置按钮,将输入值更改为默认值,然后将学习率更改为最大值(20),然后再次点击运行神经网络模型按钮。当你这么做时,你将看到类似于以下的输出:

图 3.7:学习率过大的神经网络模型执行
我们得到了 0.83 的准确率。刚刚发生了什么?通过选择一个非常大的学习率,我们的模型根本没有收敛。我们可以看到,成本函数在训练开始时实际上增加了,这表明学习率太高。我们的成本函数图看起来有重复的值,这表明梯度下降算法有时会超过最小值。
最后,我们可以观察激活函数的选择如何影响模型训练。通过更改激活函数,您可能还需要更改学习率。点击重置按钮,将输入值恢复为默认值,并选择激活函数为tanh。当我们选择tanh作为激活函数,且学习率为 1.5 时,成本在 500 到 3,500 个 epochs 之间卡在 0.4,随后突然下降到 0.2。这种现象在神经网络中会出现,当网络卡在局部最优解时。这种现象可以通过以下图表看到:

图 3.8:使用 tanh 激活函数的神经网络模型执行
相比之下,使用 relu 激活函数会导致模型训练速度更快。以下是一个示例,我们只运行 1,500 个 epochs,使用 relu 激活函数即可获得最大准确率 0.95:

图 3.9:使用 relu 激活函数的神经网络模型执行
我鼓励您尝试其他数据集。作为参考,以下是我在每个数据集上获得的最大准确率。一个有趣的实验是观察不同的激活函数和学习率在这些数据集上的表现:
-
蠕虫(准确率=0.95):3 个节点,3,000 个 epochs,学习率=0.5,激活函数=tanh
-
月亮(准确率=0.95):5 个节点,5,000 个 epochs,学习率=5,激活函数=sigmoid
-
块(准确率=0.9025):5 个节点,5,000 个 epochs,学习率=10,激活函数=sigmoid
一般来说,您将看到以下结果:
-
使用更多的 epochs 意味着更长的训练时间,但这并不总是必要的。
-
如果模型尚未达到最佳准确率,并且成本函数在训练的后期已经趋于平稳(即,变化不大),那么延长训练时间(即更多 epochs)或增加学习率不太可能改善性能。相反,可以考虑更改模型的架构,例如更改层数(在此演示中无法选择)、增加节点数或更改激活函数。
-
学习率必须谨慎选择。如果选择的值太低,模型的训练时间将非常长。如果选择的值太高,模型将无法训练。
神经网络代码
虽然 Web 应用程序在查看神经网络输出时非常有用,但我们也可以运行神经网络代码,真正理解它是如何工作的。Chapter3/nnet.R中的代码可以实现这一点。此代码具有与 Web 应用程序中相同的超参数;该文件允许您从 RStudio IDE 运行神经网络。以下是加载数据并设置神经网络初始超参数的代码:
source("nnet_functions.R")
data_sel <- "bulls_eye"
........
####################### neural network ######################
hidden <- 3
epochs <- 3000
lr <- 0.5
activation_ftn <- "sigmoid"
df <- getData(data_sel) # from nnet_functions
X <- as.matrix(df[,1:2])
Y <- as.matrix(df$Y)
n_x=ncol(X)
n_h=hidden
n_y=1
m <- nrow(X)
这段代码应该不难理解,它加载了一个数据集并设置了一些变量。数据是通过 Chapter3/nnet_functions.R 文件中的 getData 函数创建的。数据来自 clustersim 包中的函数。Chapter3/nnet_functions.R 文件包含了我们神经网络的核心功能,我们将在这里详细查看。一旦加载了数据,接下来的步骤是初始化权重和偏置。hidden 变量控制隐藏层中的节点数,我们将其设置为 3。我们需要两组权重和偏置,一组用于隐藏层,另一组用于输出层:
# initialise weights
set.seed(42)
weights1 <- matrix(0.01*runif(n_h*n_x)-0.005, ncol=n_x, nrow=n_h)
weights2 <- matrix(0.01*runif(n_y*n_h)-0.005, ncol=n_h, nrow=n_y)
bias1 <- matrix(rep(0,n_h),nrow=n_h,ncol=1)
bias2 <- matrix(rep(0,n_y),nrow=n_y,ncol=1)
这将为 (weights1, bias1) 隐藏层和 (weights2, bias2) 输出层创建矩阵。我们需要确保矩阵的维度是正确的。例如,weights1 矩阵的列数应与输入层的列数相同,行数应与隐藏层的行数相同。现在我们继续进行神经网络的实际处理循环:
for (i in 0:epochs)
{
activation2 <- forward_prop(t(X),activation_ftn,weights1,bias1, weights2,bias2)
cost <- cost_f(activation2,t(Y))
backward_prop(t(X),t(Y),activation_ftn,weights1,weights2, activation1,activation2)
weights1 <- weights1 - (lr * dweights1)
bias1 <- bias1 - (lr * dbias1)
weights2 <- weights2 - (lr * dweights2)
bias2 <- bias2 - (lr * dbias2)
if ((i %% 500) == 0)
print (paste(" Cost after",i,"epochs =",cost))
}
[1] " Cost after 0 epochs = 0.693147158995952"
[1] " Cost after 500 epochs = 0.69314587328381"
[1] " Cost after 1000 epochs = 0.693116915341439"
[1] " Cost after 1500 epochs = 0.692486724429629"
[1] " Cost after 2000 epochs = 0.687107068792801"
[1] " Cost after 2500 epochs = 0.660418522655335"
[1] " Cost after 3000 epochs = 0.579832913091798"
我们首先运行前向传播函数,然后计算代价。接着,我们调用反向传播步骤来计算我们的导数,(dweights1, dbias1, dweights2, dbias2)。然后我们使用学习率 (lr) 来更新权重和偏置 (weights1, bias1, weights2, bias2)。我们会运行这个循环指定的 epochs (3000) 次,并且每 500 次 epochs 输出一条诊断消息。这描述了每个神经网络和深度学习模型的工作原理:首先调用前向传播,然后计算代价和导数值,利用这些值通过反向传播更新权重并重复执行。
现在让我们看一下 nnet_functions.R 文件中的一些函数。以下是 forward 传播函数:
forward_prop <- function(X,activation_ftn,weights1,bias1,weights2,bias2)
{
# broadcast hack
bias1a<-bias1
for (i in 2:ncol(X))
bias1a<-cbind(bias1a,bias1)
bias2a<-bias2
for (i in 2:ncol(activation1))
bias2a<-cbind(bias2a,bias2)
Z1 <<- weights1 %*% X + bias1a
activation1 <<- activation_function(activation_ftn,Z1)
bias2a<-bias2
for (i in 2:ncol(activation1))
bias2a<-cbind(bias2a,bias2)
Z2 <<- weights2 %*% activation1 + bias2a
activation2 <<- sigmoid(Z2)
return (activation2)
}
如果你仔细查看代码,可能已经注意到 activation1、activation2、Z1 和 Z2 变量的赋值使用了 <<- 而不是 <-。这使得这些变量在全局范围内有效;我们还希望在反向传播时使用这些值。使用全局变量通常不被推荐,我本可以返回一个列表,但在这里使用它们是可以接受的,因为这个应用是为了学习目的。
这两个 for 循环将偏置向量扩展为矩阵,然后将该向量重复 n 次。关键的代码从 Z1 赋值开始。Z1 是一个矩阵乘法操作,后面跟着一个加法运算。我们对该值调用 activation_function 函数。然后,我们使用该输出值并对 Z2 执行类似的操作。最后,我们对输出层应用 sigmoid 激活函数,因为我们的问题是二分类问题。
以下是激活函数的代码;第一个参数决定了使用哪种函数(sigmoid、tanh 或 relu)。第二个参数是作为输入的值:
activation_function <- function(activation_ftn,v)
{
if (activation_ftn == "sigmoid")
res <- sigmoid(v)
else if (activation_ftn == "tanh")
res <- tanh(v)
else if (activation_ftn == "relu")
{
v[v<0] <- 0
res <- v
}
else
res <- sigmoid(v)
return (res)
}
以下是 cost 函数:
cost_f <- function(activation2,Y)
{
cost = -mean((log(activation2) * Y)+ (log(1-activation2) * (1-Y)))
return(cost)
}
提醒一下,cost函数的输出是我们试图最小化的目标。cost函数有很多种类型;在这个应用中我们使用的是二元交叉熵。二元交叉熵的公式是-1/m ∑ log(ȳ[i]) * y[i] + (log(1 -ȳ[i]) * (1-y[i]))。我们的目标值(y[i])始终是1或0,因此当y[i] = 1时,这就简化为∑ log(ȳ[i])。如果我们有两行数据,其中y[i] = 1,假设我们的模型对第一行预测1.0,对第二行预测0.0001,那么这两行的代价分别是log(1)=0和log(0.0001)=-9.1。我们可以看到,越接近1的预测,其cost值越低。同样,对于y[i] = 0的行,这就简化为log(1-ȳ[i]),因此对于这些行,越接近 0 的预测,其cost值越低。
如果我们试图最大化准确性,为什么我们不在模型训练过程中直接使用它呢?二元交叉熵是一个更好的cost函数,因为我们的模型不仅仅输出 0 或 1,而是输出从 0.0 到 1.0 之间的连续值。例如,如果两个输入行的目标值为 1(即 y=1),而我们的模型给出的概率分别为 0.51 和 0.99,那么二元交叉熵分别会给它们的代价为 0.67 和 0.01。它为第一行分配了更高的代价,因为模型对它不确定(概率接近 0.5)。如果我们仅仅看准确度,我们可能会认为这两行具有相同的代价值,因为它们都被正确分类了(假设我们为预测值<0.5 的分配类=0,预测值>=0.5 的分配类=1)。
以下是反向传播函数的代码:
backward_prop <- function(X,Y,activation_ftn,weights1,weights2,activation1,activation2)
{
m <- ncol(Y)
derivative2 <- activation2-Y
dweights2 <<- (derivative2 %*% t(activation1)) / m
dbias2 <<- rowSums(derivative2) / m
upd <- derivative_function(activation_ftn,activation1)
derivative1 <- t(weights2) %*% derivative2 * upd
dweights1 <<- (derivative1 %*% t(X)) / m
dbias1 <<- rowSums(derivative1) / m
}
反向传播按相反方向处理网络,从最后一层隐层开始,直到第一层隐层,也就是说,从输出层到输入层的方向。在我们的情况下,我们只有一层隐层,因此它首先计算来自输出层的损失,并计算dweight2和dbias2。然后它计算activation1值的derivative,这个值是在前向传播步骤中计算出来的。derivative函数类似于激活函数,但它不是调用激活函数,而是计算该函数的derivative。例如,sigmoid(x)的derivative是sigmoid(x)(1 - sigmoid(x))*。简单函数的derivative值可以在任何微积分参考书或在线找到:
derivative_function <- function(activation_ftn,v)
{
if (activation_ftn == "sigmoid")
upd <- (v * (1 - v))
else if (activation_ftn == "tanh")
upd <- (1 - (v²))
else if (activation_ftn == "relu")
upd <- ifelse(v > 0.0,1,0)
else
upd <- (v * (1 - v))
return (upd)
}
就是这样!一个使用基础 R 代码工作的神经网络。它可以拟合复杂的函数,并且表现得比逻辑回归更好。你可能不会一次性理解所有的部分,没关系。以下是步骤的简要回顾:
-
进行一次前向传播步骤,这包括对每一层的输入乘以权重,并将输出传递给下一层。
-
使用
cost函数评估最终层的输出。 -
根据错误率,使用反向传播对每一层中节点的权重进行小幅调整。学习率控制每次调整的幅度。
-
重复步骤 1-3,可能成千上万次,直到
cost函数开始趋于平稳,这表明我们的模型已被训练好。
回到深度学习
前一部分的许多概念适用于深度学习,因为深度学习只是具有两个或更多隐藏层的神经网络。为了演示这一点,让我们看看以下在 R 中加载 mxnet 深度学习库并调用该库中训练深度学习模型的函数帮助命令的代码。尽管我们尚未使用此库训练任何模型,但我们已经看到该函数中的许多参数:
library(mxnet)
?mx.model.FeedForward.create
如果出现错误提示 mxnet 包不可用,请参见 第一章,深度学习入门,获取安装说明。然而,我们在本章中并未运行任何 mxnet 代码,我们只想展示某个函数的帮助页面。所以请放心继续阅读,等到下章我们使用该库时再进行安装。
这将显示 mxnet 库中 FeedForward 函数的帮助页面,这是前向传播/模型训练函数。mxnet 和大多数深度学习库没有特定的 反向传播函数,它们会隐式地处理这一过程:
mx.model.FeedForward.create(symbol, X, y = NULL, ctx = NULL,
begin.round = 1, num.round = 10, optimizer = "sgd",
initializer = mx.init.uniform(0.01), eval.data = NULL,
eval.metric = NULL, epoch.end.callback = NULL,
batch.end.callback = NULL, array.batch.size = 128
...)
我们将在后续章节中看到更多此函数的内容;目前我们仅关注其参数。
symbol、X、y 和 ctx 参数
symbol 参数定义了深度学习架构;X 和 y 是输入和输出数据结构。ctx 参数控制模型在特定设备(例如 CPU/GPU)上进行训练。
num.round 和 begin.round 参数
num.round 相当于我们代码中的 epochs;也就是我们迭代数据的次数。begin.round 是我们如果暂停训练时,恢复训练的起始点。如果我们暂停了训练,可以保存部分训练好的模型,稍后重新加载并继续训练。
optimizer 参数
我们的神经网络实现使用了梯度下降。当研究人员开始创建更复杂的多层神经网络模型时,他们发现训练时间异常长。这是因为没有优化的基本梯度下降算法效率不高;它在每个 epoch 中朝着目标迈出小步,无论前几个 epoch 发生了什么。我们可以将其与猜数字游戏进行比较:一个人必须在一个范围内猜一个数字,每次猜测后,他们会被告知是往上还是往下(假设他们没有猜对!)。高/低的指示类似于导数值,它指示了我们必须前进的方向。现在,假设可能的数字范围是 1 到 1,000,000,第一个猜测是 1,000。这个人被告知要往上猜,他们应该做什么:
-
尝试 1001。
-
计算猜测值和最大值的差,然后除以 2。将这个值加到之前的猜测值上。
第二种选择要好得多,应该意味着这个人在 20 次猜测内能得到正确答案。如果你有计算机科学背景,你可能会认出这是二分查找算法。第一种选择,猜 1,001,1,002,....,1,000,000,是一个糟糕的选择,可能会失败,因为一方会放弃!但这与梯度下降的工作方式相似。它逐步朝着目标前进。如果你尝试增加学习率来解决这个问题,可能会超过目标,导致模型无法收敛。
研究人员提出了一些聪明的优化方法来加速训练。第一个优化器被称为动量,它正如其名字所示。它查看导数的大小,如果前面的步骤都在同一方向,它会在每个 epoch 中采取更大的步伐。这应该意味着模型训练会更快。还有一些其他算法是这些算法的增强版,比如 RMS-Prop 和 Adam。你通常不需要了解它们的工作原理,只需要知道,当你更换优化器时,可能还需要调整其他超参数,比如学习率。一般来说,可以参考别人做过的例子,复制那些超参数。
实际上,我们在上一章的示例中使用了其中一种优化器。在那一章中,我们有两个具有相似架构的模型(40 个隐藏节点)。第一个模型(digits.m3)使用了nnet库,训练时间为 40 分钟。第二个模型(digits.m3)使用了弹性反向传播,训练时间为 3 分钟。这展示了在神经网络和深度学习中使用优化器的好处。
初始化参数
当我们为权重和偏置(即模型参数)创建初始值时,我们使用了随机数,但将它们限制在 -0.005 到 +0.005 之间。如果你回顾一下 cost 函数的一些图形,你会发现 cost 函数开始下降时需要 2,000 轮训练。这是因为初始值不在正确的范围内,花了 2,000 轮训练才达到正确的幅度。幸运的是,我们不需要担心如何设置这些参数,因为 mxnet 库中的该参数控制了在训练之前如何初始化权重和偏置。
eval.metric 和 eval.data 参数
这两个参数控制用于评估模型的数据和度量标准。eval.metric 相当于我们代码中使用的 cost 函数。eval.data 用于当你想要在未用于训练的验证数据集上评估模型时。
epoch.end.callback 参数
这是一个 callback 函数,允许你注册另一个函数,该函数会在 n 轮训练后被调用。深度学习模型的训练时间通常较长,因此你需要一些反馈来确保模型正常工作!你可以编写自定义的 callback 函数来执行所需的操作,但通常它会在 n 轮训练后输出到屏幕或日志。这相当于我们在神经网络中每 500 轮训练时打印诊断信息的代码。callback 函数还可以用来将模型保存到磁盘,例如,如果你想在模型开始过拟合之前保存模型。
array.batch.size 参数
我们的数据中只有 400 个实例(行),这可以轻松地适应内存。然而,如果你的输入数据有数百万个实例,则在训练过程中需要将数据分批,以便能适应 CPU/GPU 的内存。你一次训练的实例数量就是批量大小。请注意,你仍然需要对所有数据进行迭代以完成指定轮次,你只是将数据分成多个批次,在每次迭代中将每个批次用于正向传播和反向传播步骤。例如,如果你有 100 个实例,并选择批量大小为 32,训练 6 轮,你每轮需要 4 个批次(100/32 = 3.125,因此我们需要 4 个批次来处理所有数据),总共需要 24 次循环。
选择批量大小时存在一个权衡。如果你选择的值过小,模型的训练时间会更长,因为它需要进行更多的操作,而且由于批量较小,每个批次的数据会有更多的波动。你也不能选择一个过大的批量大小,这可能会导致模型崩溃,因为它将过多的数据加载到 CPU 或 GPU 中。在大多数情况下,你要么使用另一个深度学习模型中的合理默认值,要么设置一个值(例如,1,024),如果模型崩溃,再尝试将其值减半(例如,512)。
批次大小、学习率和# 训练周期之间存在一定的关系。但在选择这些值时,并没有硬性规定。然而,通常建议一起调整这些值,并且避免使用某一超参数的极端值。例如,选择较大的学习率通常意味着训练周期较少,但如果批次大小过小,模型可能无法训练成功。最好的建议是查看类似的架构,选择一组相似的值和范围。
现在我们已经看到,深度学习仍然使用许多神经网络中的概念,我们将继续讨论一个你在每个深度学习模型中都可能遇到的重要问题:过拟合。
使用正则化来克服过拟合
在上一章中,我们看到神经网络在进一步训练迭代中的回报逐渐递减,尤其是在对持出数据或测试数据(即未用于训练模型的数据)进行预测时。这是因为复杂的模型可能会记住数据中的一些噪音,而不是学习到一般的模式。这些模型在预测新数据时表现会更差。我们可以应用一些方法来让模型具备更强的泛化能力,也就是说,能够拟合整体的模式。这些方法被称为正则化,旨在减少测试误差,使得模型在新数据上表现良好。
深度学习中最常用的正则化技术是 dropout(丢弃法)。然而,我们还将讨论另外两种在回归和深度学习中有应用基础的正则化技术。这两种正则化技术分别是L1 惩罚,也叫做Lasso,和L2 惩罚,也叫做Ridge。
L1 惩罚
L1 惩罚(也称为最小绝对收缩与选择算子,Lasso–Hastie, T.,Tibshirani, R. 和 Friedman, J.(2009))的基本概念是,通过惩罚项将权重收缩到零。惩罚项使用的是权重的绝对值之和,因此一些权重可能会被收缩为零。这意味着 Lasso 也可以作为一种变量选择方法。惩罚项的强度由一个超参数 alpha(λ)控制,它会乘以绝对权重之和,alpha 可以是一个固定值,或者像其他超参数一样,通过交叉验证或类似方法来优化。
如果使用普通最小二乘法(OLS)回归模型来描述 Lasso,会更加直观。在回归中,使用最小二乘误差准则来估计一组系数或模型权重,其中权重/系数向量Θ通过最小化∑(y[i] - ȳ[i])来估计,其中ȳ[i]=b+Θx,y[i]是我们要预测的目标值,ȳ[i]是预测值。Lasso 回归在此基础上增加了一个惩罚项,它尝试最小化∑(y[i] - ȳ[i]) + λ⌊Θ⌋,其中⌊Θ⌋是Θ的绝对值。通常,截距项或偏置项会被排除在这个约束之外。
Lasso 回归有一些实际的影响。首先,惩罚的效果取决于权重的大小,而权重的大小又取决于数据的尺度。因此,数据通常会先进行标准化,使其方差为单位方差(或者至少使每个变量的方差相等)。L1 惩罚倾向于将小的权重缩小到零(有关此现象的解释,见 Hastie, T.,Tibshirani, R.和 Friedman, J.(2009))。如果你只考虑 L1 惩罚留下非零权重的变量,它实际上可以作为特征选择。L1 惩罚将小系数缩小到零的趋势,也有助于简化模型结果的解释。
对神经网络应用 L1 惩罚的方式与回归中的应用完全相同。如果X代表输入,Y是输出或因变量,B是参数,F是目标函数,我们希望通过优化目标函数F(B; X, Y)来获得B,即我们要最小化F(B; X, Y)。L1 惩罚会修改目标函数为 F(B; X, Y) + λ⌊Θ⌋,其中Θ代表权重(通常忽略偏置项)。L1 惩罚倾向于产生稀疏解(即更多的零权重),因为小的和较大的权重会产生相同的惩罚,因此在每次梯度更新时,权重会朝零移动。
我们只考虑了λ为常量的情况,这控制着惩罚或正则化的程度。然而,在深度神经网络中,可以设置不同的值,其中可以对不同的层应用不同程度的正则化。考虑这种差异化正则化的一个原因是,有时希望允许更多的参数(比如在某一层增加更多的神经元),然后通过更强的正则化来抵消这种增加。然而,如果我们允许 L1 惩罚在深度神经网络的每一层都变化,并使用交叉验证来优化 L1 惩罚的所有可能组合,这种方法可能会非常耗费计算资源。因此,通常在整个模型中使用一个单一的值。
L1 惩罚的应用
为了观察 L1 惩罚是如何工作的,我们可以使用一个模拟的线性回归问题。本章其余部分的代码在Chapter3/overfitting.R中。我们使用一组相关的预测变量来模拟数据:
set.seed(1234)
X <- mvrnorm(n = 200, mu = c(0, 0, 0, 0, 0),
Sigma = matrix(c(
1, .9999, .99, .99, .10,
.9999, 1, .99, .99, .10,
.99, .99, 1, .99, .10,
.99, .99, .99, 1, .10,
.10, .10, .10, .10, 1
), ncol = 5))
y <- rnorm(200, 3 + X %*% matrix(c(1, 1, 1, 1, 0)), .5)
接下来,我们可以将 OLS 回归模型拟合到前 100 个案例,然后使用 lasso。为了使用 lasso,我们需要使用 glmnet 包中的 glmnet() 函数。这个函数实际上可以拟合 L1 或 L2 惩罚(将在下一节讨论),哪种惩罚取决于参数 alpha。当 alpha = 1 时,它是 L1 惩罚(即 lasso),而当 alpha = 0 时,它是 L2 惩罚(即岭回归)。此外,由于我们不知道应该选择哪个 lambda 值,我们可以评估多个选项,并使用交叉验证自动调整这个超参数,这就是 cv.glmnet() 函数。然后,我们可以绘制 lasso 对象,查看不同 lambda 值下的均方误差,以便选择合适的正则化水平:
m.ols <- lm(y[1:100] ~ X[1:100, ])
m.lasso.cv <- cv.glmnet(X[1:100, ], y[1:100], alpha = 1)
plot(m.lasso.cv)

图 3.10:Lasso 正则化
从图中我们可以看到,当惩罚过高时,交叉验证模型的误差增加。实际上,lasso 在非常低的 lambda 值下表现较好,这可能表明对于这个数据集,lasso 对于提升样本外表现/泛化能力并没有太大帮助。为了这个示例,我们将继续,但在实际应用中,这可能会让我们停下来思考 lasso 是否真的起到了帮助作用。最后,我们可以将系数与 lasso 的结果进行比较:
cbind(OLS = coef(m.ols),Lasso = coef(m.lasso.cv)[,1])
OLS Lasso
(Intercept) 2.958 2.99
X[1:100, ]1 -0.082 1.41
X[1:100, ]2 2.239 0.71
X[1:100, ]3 0.602 0.51
X[1:100, ]4 1.235 1.17
X[1:100, ]5 -0.041 0.00
注意到 OLS 系数更为嘈杂,而且在 lasso 中,第 5 个预测变量被惩罚到 0。回顾一下模拟数据,真实的系数是 3, 1, 1, 1, 1 和 0。OLS 的估计值对于第一个预测变量过低,对于第二个预测变量过高,而 lasso 对每个系数的估计值更为准确。这表明,lasso 回归比 OLS 回归在这个数据集上的泛化能力更强。
L2 惩罚
L2 惩罚,也称为 岭回归,在许多方面与 L1 惩罚相似,但与基于绝对权重和惩罚的 L1 惩罚不同,L2 惩罚是基于权重的平方。这意味着较大的绝对权重会受到更大的惩罚。在神经网络的背景下,这有时被称为权重衰减。如果你检查正则化目标函数的梯度,你会发现有一个惩罚项,在每次更新时,权重会受到乘法惩罚。至于 L1 惩罚,尽管可以包括它,但通常会将偏置或偏移量排除在外。
从线性回归问题的角度来看,L2 惩罚是对最小化目标函数的一种修改,修改的形式是从 ∑(y[i] - ȳ[i]) 到 ∑(y[i] - ȳ[i]) + λΘ²。
L2 惩罚的应用
为了观察 L2 惩罚项的作用,我们可以使用与 L1 惩罚项相同的模拟线性回归问题。为了拟合岭回归模型,我们使用来自glmnet包的glmnet()函数。如前所述,该函数实际上可以拟合 L1 或 L2 惩罚项,哪种惩罚项取决于参数 alpha。当alpha = 1时,拟合的是lasso,而当alpha = 0时,拟合的是岭回归。这次我们选择alpha = 0。同样,我们评估一系列 lambda 选项,并通过交叉验证自动调节该超参数。通过使用cv.glmnet()函数来实现这一点。我们绘制岭回归对象,查看不同 lambda 值下的误差:
m.ridge.cv <- cv.glmnet(X[1:100, ], y[1:100], alpha = 0)
plot(m.ridge.cv)

图 3.11:岭正则化
尽管形状与lasso不同,因为在更高的 lambda 值下误差似乎趋于平稳,但仍然可以明显看出,当惩罚项过高时,交叉验证的模型误差会增加。与lasso类似,岭回归模型在非常低的 lambda 值下似乎表现不错,这可能表明 L2 惩罚并未显著改善模型的外部样本表现/泛化能力。
最后,我们可以将 OLS 系数与lasso和岭回归模型的系数进行比较:
> cbind(OLS = coef(m.ols),Lasso = coef(m.lasso.cv)[,1],Ridge = coef(m.ridge.cv)[,1])
OLS Lasso Ridge
(Intercept) 2.958 2.99 2.9919
X[1:100, ]1 -0.082 1.41 0.9488
X[1:100, ]2 2.239 0.71 0.9524
X[1:100, ]3 0.602 0.51 0.9323
X[1:100, ]4 1.235 1.17 0.9548
X[1:100, ]5 -0.041 0.00 -0.0023
尽管岭回归没有将第五个预测变量的系数压缩到精确为 0,但它比 OLS 中的系数要小,而且其余的参数都稍微被收缩,但仍然接近它们的真实值:3,1,1,1,1 和 0。
权重衰减(神经网络中的 L2 惩罚)
我们在上一章中已经无意间使用了正则化。我们使用caret和nnet包训练的神经网络使用了0.10的权重衰减。我们可以通过变化该衰减值并使用交叉验证进行调优来进一步研究权重衰减的使用:
- 如之前一样加载数据。然后我们创建一个本地集群来并行运行交叉验证:
set.seed(1234)
## same data as from previous chapter
if (!file.exists('../data/train.csv'))
{
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=""))
}
digits.train <- read.csv("../data/train.csv")
## convert to factor
digits.train$label <- factor(digits.train$label, levels = 0:9)
sample <- sample(nrow(digits.train), 6000)
train <- sample[1:5000]
test <- sample[5001:6000]
digits.X <- digits.train[train, -1]
digits.y <- digits.train[train, 1]
test.X <- digits.train[test, -1]
test.y <- digits.train[test, 1]
## try various weight decays and number of iterations
## register backend so that different decays can be
## estimated in parallel
cl <- makeCluster(5)
clusterEvalQ(cl, {source("cluster_inc.R")})
registerDoSNOW(cl)
- 在数字分类任务上训练一个神经网络,并将权重衰减惩罚分别设置为
0(无惩罚)和0.10。我们还分别使用两组迭代次数:100次或150次。请注意,这段代码计算量较大,运行需要一些时间:
set.seed(1234)
digits.decay.m1 <- lapply(c(100, 150), function(its) {
caret::train(digits.X, digits.y,
method = "nnet",
tuneGrid = expand.grid(
.size = c(10),
.decay = c(0, .1)),
trControl = caret::trainControl(method="cv", number=5, repeats=1),
MaxNWts = 10000,
maxit = its)
})
- 检查结果时,我们看到,当我们限制迭代次数为
100时,无论是非正则化模型还是正则化模型,基于交叉验证的结果,其准确率均为0.56,对于这些数据来说并不是很理想:
digits.decay.m1[[1]]
Neural Network
5000 samples
784 predictor
10 classes: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
No pre-processing
Resampling: Cross-Validated (5 fold)
Summary of sample sizes: 4000, 4001, 4000, 3998, 4001
Resampling results across tuning parameters:
decay Accuracy Kappa
0.0 0.56 0.51
0.1 0.56 0.51
Tuning parameter 'size' was held constant at a value of 10
Accuracy was used to select the optimal model using the
largest value.
The final values used for the model were size = 10 and decay = 0.1.
- 使用
150次迭代来检查模型,看看正则化模型和非正则化模型哪个表现更好:
digits.decay.m1[[2]]
Neural Network
5000 samples
784 predictor
10 classes: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
No pre-processing
Resampling: Cross-Validated (5 fold)
Summary of sample sizes: 4000, 4002, 3998, 4000, 4000
Resampling results across tuning parameters:
decay Accuracy Kappa
0.0 0.64 0.60
0.1 0.63 0.59
Tuning parameter 'size' was held constant at a value of 10
Accuracy was used to select the optimal model using the
largest value.
The final values used for the model were size = 10 and decay = 0.
总体来说,具有更多迭代次数的模型在表现上优于具有较少迭代次数的模型,无论是否进行正则化。然而,比较这两种均为 150 次迭代的模型时,正则化模型(准确率 = 0.66)优于非正则化模型(准确率 = 0.65),尽管两者之间的差异相对较小。
这些结果突出了正则化通常对于那些具有更大灵活性、能够拟合(并过拟合)数据的复杂模型最为有用。在那些对于数据来说合适或过于简单的模型中,正则化可能会降低性能。在开发新模型架构时,应该避免在模型尚未在训练数据上表现良好之前加入正则化。如果在提前加入正则化并且模型在训练数据上的表现较差,你将无法判断问题是出在模型架构上,还是正则化的影响。接下来的章节将讨论集成和模型平均技术,这是本书中突出的最后一种正则化形式。
集成和模型平均
另一种正则化方法是创建多个模型(集成),并将它们结合起来,比如通过模型平均或其他某种结合单个模型结果的算法。使用集成技术在机器学习中有着丰富的历史,比如袋装法、提升法和随机森林等方法都采用了这一技术。其基本思想是,如果你使用训练数据构建不同的模型,每个模型在预测值上会有不同的误差。当一个模型预测的值过高时,另一个模型可能预测的值过低,而当将这些模型的结果进行平均时,一些误差会相互抵消,从而得到比单独模型更准确的预测。
集成方法的关键在于,不同的模型必须在其预测结果中有一定的变异性。如果不同模型的预测结果高度相关,那么使用集成技术将不会有益。如果不同模型的预测结果之间相关性非常低,那么平均值将更加准确,因为它能够融合每个模型的优点。以下代码给出了一个使用模拟数据的示例。这个小示例通过仅仅三个模型来说明这一点:
## simulated data
set.seed(1234)
d <- data.frame(
x = rnorm(400))
d$y <- with(d, rnorm(400, 2 + ifelse(x < 0, x + x², x + x².5), 1))
d.train <- d[1:200, ]
d.test <- d[201:400, ]
## three different models
m1 <- lm(y ~ x, data = d.train)
m2 <- lm(y ~ I(x²), data = d.train)
m3 <- lm(y ~ pmax(x, 0) + pmin(x, 0), data = d.train)
## In sample R2
cbind(M1=summary(m1)$r.squared,
M2=summary(m2)$r.squared,M3=summary(m3)$r.squared)
M1 M2 M3
[1,] 0.33 0.6 0.76
我们可以看到,每个模型的预测值,至少在训练数据上,变化非常大。评估训练数据中拟合值之间的相关性也有助于指出模型预测之间的重叠程度:
cor(cbind(M1=fitted(m1),
M2=fitted(m2),M3=fitted(m3)))
M1 M2 M3
M1 1.00 0.11 0.65
M2 0.11 1.00 0.78
M3 0.65 0.78 1.00
接下来,我们为测试数据生成预测值、预测值的平均值,并再次将预测值与测试数据中的实际值进行相关性分析:
## generate predictions and the average prediction
d.test$yhat1 <- predict(m1, newdata = d.test)
d.test$yhat2 <- predict(m2, newdata = d.test)
d.test$yhat3 <- predict(m3, newdata = d.test)
d.test$yhatavg <- rowMeans(d.test[, paste0("yhat", 1:3)])
## correlation in the testing data
cor(d.test)
x y yhat1 yhat2 yhat3 yhatavg
x 1.000 0.44 1.000 -0.098 0.60 0.55
y 0.442 1.00 0.442 0.753 0.87 0.91
yhat1 1.000 0.44 1.000 -0.098 0.60 0.55
yhat2 -0.098 0.75 -0.098 1.000 0.69 0.76
yhat3 0.596 0.87 0.596 0.687 1.00 0.98
yhatavg 0.552 0.91 0.552 0.765 0.98 1.00
从结果可以看出,三种模型预测的平均值表现优于任何单独的模型。然而,这并非总是如此;一个表现好的模型可能会比平均预测值表现得更好。一般来说,最好检查被平均的模型在训练数据上是否表现相似。第二个教训是,给定具有相似表现的模型,最好让模型预测之间的相关性较低,因为这将产生表现最好的平均值。
还有其他形式的集成方法,它们被包含在其他机器学习算法中,例如,bagging 和 boosting。Bagging 被用于随机森林中,其中生成了许多模型,每个模型都有不同的数据样本。这些模型故意被设计成小型、不完全的模型。通过对大量使用数据子集的、训练不足的模型的预测结果进行平均,我们应该能得到一个更强大的模型。boosting 的一个例子是梯度提升模型(GBMs),它也使用多个模型,但这次每个模型都专注于在前一个模型中被错误预测的实例。随机森林和 GBMs 在结构化数据中已被证明非常成功,因为它们能够减少方差,即避免过拟合数据。
在深度神经网络中,bagging 和模型平均使用得不太频繁,因为训练每个模型的计算成本可能相当高,因此重复这一过程在时间和计算资源上变得非常昂贵。尽管如此,在深度神经网络中仍然可以使用模型平均,尽管可能只是在少数几个模型中进行,而不是像随机森林和其他一些方法中那样使用数百个模型。
用例 – 使用 dropout 改进样本外模型的表现
Dropout 是一种新颖的正则化方法,对于大型复杂的深度神经网络尤其有价值。关于在深度神经网络中使用 dropout 的更详细探讨,请参见 Srivastava, N., Hinton, G., Krizhevsky, A., Sutskever, I., 和 Salakhutdinav, R. (2014)。dropout 背后的概念实际上非常简单。在模型训练过程中,单位(例如,输入和隐藏神经元)会以一定的概率被丢弃,连同它们的所有输入输出连接一同丢弃。
例如,下面的图示是一个示例,展示了在训练过程中,对于每个训练周期,隐藏神经元及其连接以 1/3 的概率被丢弃时可能发生的情况。一旦一个节点被丢弃,它与下一层的连接也会被丢弃。在下图中,灰色的节点和虚线连接表示被丢弃的部分。需要注意的是,被丢弃的节点选择在每个训练周期都会发生变化:

图 3.12:在不同的训练周期中应用 dropout 到一个层
思考 dropout 的一种方式是,它迫使模型对扰动更加鲁棒。虽然完整模型中包含了许多神经元,但在训练过程中并非所有神经元都会同时存在,因此神经元必须比平时更独立地运作。另一种看待 dropout 的方式是,如果你有一个包含 N 个权重的大型模型,并且在训练过程中有 50% 的权重被丢弃,那么虽然所有 N 个权重在训练的某些阶段都会被使用,但你实际上将总模型复杂度减少了一半,因为平均权重数量将减少一半。这减少了模型的复杂度,因此有助于防止数据的过拟合。由于这一特性,Srivastava, N., Hinton, G., Krizhevsky, A., Sutskever, I., 和 Salakhutdinov, R. (2014) 推荐通过 1/p 来放大目标模型的复杂度,以便最终得到一个大致相同复杂度的模型。
在模型测试/评分时,通常不会丢弃神经元,因为这在计算上不方便。相反,我们可以使用一种近似平均值,通过根据每个权重被包含的概率(即 1/p)对单个神经网络的权重进行缩放。这通常由深度学习库处理。
除了效果良好外,这种近似的权重重缩放是一个相对简单的计算。因此,dropout 的主要计算成本来自于必须使用一个包含更多神经元和权重的模型,因为在每次训练更新中,会丢弃很多(通常推荐隐藏神经元的丢弃比例为 50%)。
虽然 dropout 很容易实现,但可能需要一个更大的模型来进行补偿。为了加速训练,可以使用更高的学习率,从而减少所需的训练轮数。结合这些方法的一个潜在缺点是,由于神经元减少和学习率加快,某些权重可能会变得非常大。幸运的是,可以将 dropout 与其他正则化方法结合使用,例如 L1 或 L2 惩罚。综合来看,结果是一个更大的模型,它能够快速(通过更快的学习率)探索更广泛的参数空间,但通过 dropout 和惩罚进行正则化,以保持权重在合理范围内。
为了展示 dropout 在神经网络中的应用,我们将回到修改版国家标准与技术研究院(MNIST)数据集(我们在第二章,训练预测模型中下载过该数据集)进行操作。我们将使用来自deepnet包的nn.train()函数,因为它支持 dropout。与前一章一样,我们将并行运行四个模型,以减少所需时间。具体来说,我们比较了四个模型,其中两个使用 dropout 正则化,两个不使用 dropout 正则化,且每个模型有 40 或 80 个隐藏神经元。对于 dropout,我们分别为隐藏层和可见单元指定不同的 dropout 比例。根据经验法则,大约 50% 的隐藏单元(和 80% 的观察单元)应被保留,我们分别将 dropout 比例指定为 0.5 和 0.2:
## Fit Models
nn.models <- foreach(i = 1:4, .combine = 'c') %dopar% {
set.seed(1234)
list(nn.train(
x = as.matrix(digits.X),
y = model.matrix(~ 0 + digits.y),
hidden = c(40, 80, 40, 80)[i],
activationfun = "tanh",
learningrate = 0.8,
momentum = 0.5,
numepochs = 150,
output = "softmax",
hidden_dropout = c(0, 0, .5, .5)[i],
visible_dropout = c(0, 0, .2, .2)[i]))
}
接下来,我们可以遍历模型,获取预测值并评估整体模型的性能:
nn.yhat <- lapply(nn.models, function(obj) {
encodeClassLabels(nn.predict(obj, as.matrix(digits.X)))
})
perf.train <- do.call(cbind, lapply(nn.yhat, function(yhat) {
caret::confusionMatrix(xtabs(~ I(yhat - 1) + digits.y))$overall
}))
colnames(perf.train) <- c("N40", "N80", "N40_Reg", "N80_Reg")
options(digits = 4)
perf.train
N40 N80 N40_Reg N80_Reg
Accuracy 0.9478 0.9622 0.9278 0.9400
Kappa 0.9420 0.9580 0.9197 0.9333
AccuracyLower 0.9413 0.9565 0.9203 0.9331
AccuracyUpper 0.9538 0.9673 0.9348 0.9464
AccuracyNull 0.1126 0.1126 0.1126 0.1126
AccuracyPValue 0.0000 0.0000 0.0000 0.0000
McnemarPValue NaN NaN NaN NaN
在评估样本内训练数据时,似乎没有正则化的模型表现优于有正则化的模型。当然,真正的考验来自于测试数据或保留数据:
nn.yhat.test <- lapply(nn.models, function(obj) {
encodeClassLabels(nn.predict(obj, as.matrix(test.X)))
})
perf.test <- do.call(cbind, lapply(nn.yhat.test, function(yhat) {
caret::confusionMatrix(xtabs(~ I(yhat - 1) + test.y))$overall
}))
colnames(perf.test) <- c("N40", "N80", "N40_Reg", "N80_Reg")
perf.test
N40 N80 N40_Reg N80_Reg
Accuracy 0.8890 0.8520 0.8980 0.9030
Kappa 0.8765 0.8352 0.8864 0.8920
AccuracyLower 0.8679 0.8285 0.8776 0.8830
AccuracyUpper 0.9078 0.8734 0.9161 0.9206
AccuracyNull 0.1180 0.1180 0.1180 0.1180
AccuracyPValue 0.0000 0.0000 0.0000 0.0000
McnemarPValue NaN NaN NaN NaN
测试数据突显了样本内性能过于乐观(准确率 = 0.9622 与训练和测试数据中 80 神经元、无正则化模型的准确率 = 0.8520)。我们可以看到,正则化模型在 40 和 80 神经元模型中都有明显的优势。尽管这两个模型在测试数据中的表现仍然不如训练数据,但它们在测试数据中的表现与同等的无正则化模型相当,甚至更好。这一差异对 80 神经元模型尤其重要,因为在测试数据中表现最好的模型是正则化模型。
虽然这些数字远未创造记录,但它们确实展示了使用 dropout 或更广泛的正则化的价值,以及如何调整模型和 dropout 参数以提高最终的测试性能。
摘要
本章一开始向你展示了如何从零开始编程一个神经网络。我们展示了如何仅使用 R 代码创建的 Web 应用程序中的神经网络。我们深入探讨了神经网络的实际工作原理,展示了如何编码前向传播、cost 函数和反向传播。然后,我们查看了神经网络参数如何应用于现代深度学习库,通过查看 mxnet 深度学习库中的mx.model.FeedForward.create函数。
然后我们讲解了过拟合,展示了几种防止过拟合的方法,包括常见的惩罚项、L1 惩罚和 L2 惩罚、简单模型的集成以及丢弃法(dropout),其中变量和/或样本被丢弃以增加模型的噪声。我们考察了惩罚项在回归问题和神经网络中的作用。在下一章中,我们将进入深度学习和深度神经网络的内容,看看如何进一步提高预测模型的准确性和性能。
第四章:训练深度预测模型
前几章介绍了神经网络背后的部分理论,并使用了 R 中的一些神经网络包。现在是时候深入探讨如何训练深度学习模型了。在本章中,我们将探索如何训练和构建前馈神经网络,这是最常见的深度学习模型类型。我们将使用 MXNet 构建深度学习模型,利用零售数据集进行分类和回归。
本章将涵盖以下主题:
-
深度前馈神经网络入门
-
常见激活函数 – 整流器、双曲正切和最大激活
-
MXNet 深度学习库简介
-
使用案例 - 使用 MXNet 进行分类和回归
深度前馈神经网络入门
深度前馈神经网络旨在逼近一个函数,f(),该函数将一组输入变量,x,映射到输出变量,y。它们被称为前馈神经网络,因为信息从输入层通过每一层依次传递直到输出层,并且没有反馈或递归环路(包含前向和后向连接的模型被称为递归神经网络)。
深度前馈神经网络适用于广泛的问题,特别是在图像分类等应用中非常有用。更一般来说,前馈神经网络适用于那些有明确定义结果的预测和分类任务(例如图像包含的数字、某人是否在走楼梯或走平地、是否患有某种疾病等)。
深度前馈神经网络可以通过将层或函数连接在一起来构建。例如,下面的图示展示了一个包含四个隐藏层的网络:

图 4.1:一个深度前馈神经网络
该模型的图示是一个有向无环图。作为一个函数表示,从输入,X,到输出,Y,的整体映射是一个多层函数。第一隐藏层为 H[1] = f^((1))(X, w[1] a[1]),第二隐藏层为 H[2] = f^((2))(H[1], w[2] a[2]),依此类推。这些多层可以将复杂的函数和变换从相对简单的函数中构建出来。
如果在一层中包含足够的隐藏神经元,它可以通过许多不同类型的函数逼近到所需的精度。前馈神经网络可以通过在层之间应用非线性变换来逼近非线性函数。这些非线性函数被称为激活函数,我们将在下一节中讨论。
每一层的权重将在模型通过前向传播和反向传播训练时学习到。另一个必须确定的关键部分是代价函数或损失函数。最常用的两种代价函数是交叉熵,用于分类任务,以及均方误差(MSE),用于回归任务。
激活函数
激活函数决定了输入与隐藏层之间的映射关系。它定义了神经元激活的函数形式。例如,线性激活函数可以定义为:f(x) = x,在这种情况下,神经元的值将是原始输入 x。图 4.2 的顶部面板显示了线性激活函数。线性激活函数很少使用,因为在实践中,深度学习模型使用线性激活函数很难学习非线性的函数形式。在前面的章节中,我们使用了双曲正切作为激活函数,即 f(x) = tanh(x)。双曲正切在某些情况下表现良好,但一个潜在的限制是,在低值或高值时,它会饱和,正如图 4.2 中间面板所示。
目前可能是最流行的激活函数,也是一个不错的首选(Nair, V., 和 Hinton, G. E.(2010)),它被称为 修正线性单元(rectifier)。修正线性单元有不同种类,但最常见的定义为 f(x) = max(0, x) 的函数,这被称为relu。relu 激活在零以下是平的,在零以上是线性的;一个示例如图 4.2 所示。
我们将讨论的最后一种激活函数是 maxout(Goodfellow、Warde-Farley、Mirza、Courville 和 Bengio(2013))。maxout 单元取其输入的最大值,尽管和往常一样,这是在加权之后进行的,因此并不是输入变量中值最高的那个总是会胜出。maxout 激活函数似乎与 dropout 特别契合。
relu 激活是最常用的激活函数,也是本书其余部分深度学习模型的默认选项。以下是我们讨论过的一些激活函数的图示:

图 4.2:常见的激活函数
MXNet 深度学习库简介
本书中我们将使用的深度学习库有 MXNet、Keras 和 TensorFlow。Keras 是一个前端 API,这意味着它不是一个独立的库,它需要一个底层库作为后端,通常是 TensorFlow。使用 Keras 而不是 TensorFlow 的优点在于它具有更简单的接口。我们将在本书的后续章节中使用 Keras。
MXNet 和 TensorFlow 都是多功能的数值计算库,可以利用 GPU 进行大规模并行矩阵运算。因此,多维矩阵是这两个库的核心。在 R 中,我们熟悉向量,它是一个一维的相同类型值的数组。R 数据框是一个二维数组,其中每一列可以包含不同类型的数据。R 矩阵是一个二维数组,所有值的类型相同。一些机器学习算法在 R 中需要矩阵作为输入。我们在第二章,训练预测模型中,使用了 RSNSS 包的例子。
在 R 中,使用多于两维的数据结构并不常见,但深度学习广泛使用它们。例如,如果你有一张 32 x 32 的彩色图像,你可以将像素值存储在一个 32 x 32 x 3 的矩阵中,其中前两个维度是宽度和高度,最后一个维度代表红色、绿色和蓝色。这可以通过添加一个新的维度来进一步扩展,表示一组图像。这称为一个批次,并允许处理器(CPU/GPU)同时处理多张图像。批次大小是一个超参数,选择的值取决于输入数据的大小和内存容量。如果我们的批次大小为 64,那么我们的矩阵将是一个 4 维矩阵,大小为 32 x 32 x 3 x 64,其中前两个维度是宽度和高度,第三个维度是颜色,最后一个维度是批次大小 64。需要注意的是,这只是表示数据的另一种方式。在 R 中,我们会将相同的数据存储为一个 2 维矩阵(或数据框),具有 64 行和 32 x 32 x 3 = 3,072 列。我们所做的仅仅是重新排列数据,并没有改变数据本身。
这些包含相同类型元素的 n 维矩阵是使用 MXNet 和 TensorFlow 的基石。在 MXNet 中,它们被称为 NDArrays。在 TensorFlow 中,它们被称为 张量。这些 n 维矩阵非常重要,因为它们意味着我们可以更高效地将数据输入到 GPU;GPU 可以比处理单行数据更高效地批量处理数据。在前面的例子中,我们使用了 64 张图像作为一个批次,因此深度学习库会将输入数据分成 32 x 32 x 3 x 64 的块进行处理。
本章将使用 MXNet 深度学习库。MXNet 起源于卡内基梅隆大学,并得到了亚马逊的强力支持,2016 年他们将其作为默认的深度学习库。2017 年,MXNet 被接受为 Apache 孵化项目,确保它将保持开源软件。这是一个在 MXNet 中使用 R 进行 NDArray(矩阵)操作的非常简单的示例。如果你还没有安装 MXNet 包,请回到第一章,深度学习入门,查看安装说明,或者使用此链接:mxnet.apache.org/install/index.html:
library(mxnet) # 1
ctx = mx.cpu() # 2
a <- mx.nd.ones(c(2,3),ctx=ctx) # 3
b <- a * 2 + 1 # 4
typeof(b) # 5
[1] "externalptr"
class(b) # 6
[1] "MXNDArray"
b # 7
[,1] [,2] [,3]
[1,] 3 3 3
[2,] 3 3 3
我们可以逐行分析这段代码:
-
第 1 行加载了 MXNet 包。
-
第 2 行设置了 CPU 上下文。这告诉 MXNet 在哪里处理你的计算,要么是在 CPU 上,要么是在可用的 GPU 上。
-
第 3 行创建了一个大小为 2 x 3 的二维 NDArray,每个值都是 1。
-
第 4 行创建了一个大小为 2 x 3 的二维 NDArray。每个值都会是 3,因为我们进行逐元素乘法并加上 1。
-
第 5 行显示 b 是一个外部指针。
-
第 6 行显示 b 的类是 MXNDArray。
-
第 7 行显示结果。
我们可以对b变量执行数学运算,如乘法和加法。然而,需要注意的是,虽然这与 R 矩阵类似,但它并不是一个原生的 R 对象。当我们输出该变量的类型和类时,就能看到这一点。
在开发深度学习模型时,通常有两个不同的步骤。首先是创建模型架构,然后是训练模型。这样做的主要原因是,大多数深度学习库采用符号化编程,而不是你习惯的命令式编程。你以前在 R 中编写的大部分代码是命令式程序,它按顺序执行代码。对于数学优化任务,比如深度学习,这可能不是最高效的执行方法。包括 MXNet 和 TensorFlow 在内的大多数深度学习库都使用符号化编程。符号化编程首先设计一个计算图,用于程序执行。然后,图会被编译并执行。当计算图生成时,输入、输出和图操作已经定义好了,这意味着代码可以得到优化。这意味着,对于深度学习,符号程序通常比命令式程序更高效。
这里是一个使用符号程序进行优化的简单例子:
M = (M1 * M2) + (M3 M4)*
一个命令式程序会按如下方式计算:
Mtemp1 = (M1 * M2)
Mtemp2 = (M3 M4)*
M = Mtemp1 + Mtemp2
一个符号化程序会首先创建一个计算图,可能如下所示:

图 4.3:计算图示例
M1、M2、M3 和 M4 是需要操作的符号。图展示了操作之间的依赖关系;+ 操作要求先执行两个乘法操作才能执行。但是两个乘法步骤之间没有依赖关系,因此它们可以并行执行。这种优化意味着代码能够更快地执行。
从编码的角度来看,这意味着创建深度学习模型有两个步骤——首先定义模型的架构,然后训练模型。你为深度学习模型创建层,每一层都有作为占位符的符号。例如,第一层通常是:
data <- mx.symbol.Variable("data")
data 是输入的占位符,我们稍后会插入数据。每一层的输出作为下一层的输入。这可能是一个卷积层、一个全连接层、一个激活层、一个丢弃层等。以下代码示例展示了层与层之间如何继续传递数据;这个示例来自本章后面完整的例子。注意每一层的符号如何作为下一层的输入,这就是模型如何一层一层构建的方式。data1 符号传递给第一次调用的 mx.symbol.FullyConnected,fc1 符号传递给第一次调用的 mx.symbol.Activation,依此类推。
data <- mx.symbol.Variable("data")
fc1 <- mx.symbol.FullyConnected(data, name="fc1", num_hidden=64)
act1 <- mx.symbol.Activation(fc1, name="activ1", act_type=activ)
drop1 <- mx.symbol.Dropout(data=act1,p=0.2)
fc2 <- mx.symbol.FullyConnected(drop1, name="fc2", num_hidden=32)
act2 <- mx.symbol.Activation(fc2, name="activ2", act_type=activ)
.....
softmax <- mx.symbol.SoftmaxOutput(fc4, name="sm")
当你执行这段代码时,它会立即运行,因为在这个阶段没有任何操作会被执行。最终,你将最后一层传入一个函数来训练模型。在 MXNet 中,这是 mx.model.FeedForward.create 函数。在这一阶段,计算图会被计算出来,模型开始训练:
softmax <- mx.symbol.SoftmaxOutput(fc4, name="sm")
model <- mx.model.FeedForward.create(softmax, X = train_X, y = train_Y,
ctx = devices,num.round = num_epochs,
................
这就是深度学习模型被创建并训练的过程。关于 MXNet 架构的更多信息可以在线找到;以下链接将帮助你入门:
深度学习层
在之前的代码片段中,我们看到了一些深度学习模型的层,包括 mx.symbol.FullyConnected、mx.symbol.Activation 和 mx.symbol.Dropout。层是模型构建的方式;它们是数据的计算变换。例如,mx.symbol.FullyConnected 是我们在第一章 深度学习入门 中介绍的矩阵操作的第一种层操作。它是全连接的,因为所有输入值都与层中的所有节点相连接。在其他深度学习库中,如 Keras,它被称为密集层。
mx.symbol.Activation 层对前一层的输出执行激活函数。mx.symbol.Dropout 层对前一层的输出执行丢弃操作。MXNet 中的其他常见层类型包括:
-
mxnet.symbol.Convolution:执行卷积操作,匹配数据中的模式。它主要用于计算机视觉任务,我们将在第五章中看到,使用卷积神经网络进行图像分类。它们也可以用于自然语言处理,我们将在第六章中看到,使用深度学习进行自然语言处理。 -
mx.symbol.Pooling:对前一层的输出进行池化操作。池化通过取输入部分的平均值或最大值来减少元素的数量。这些操作通常与卷积层一起使用。 -
mx.symbol.BatchNorm:用于对前一层的权重进行归一化。这样做的原因与归一化输入数据类似:有助于模型更好地训练。它还可以防止梯度消失和梯度爆炸问题,避免在训练过程中梯度变得非常小或非常大,这可能导致模型无法收敛,即训练失败。 -
mx.symbol.SoftmaxOutput:从前一层的输出中计算 Softmax 结果。
使用这些层时有一定的规律,例如,激活层通常跟在全连接层后面。Dropout 层通常应用在激活函数之后,但也可以放在全连接层和激活函数之间。卷积层和池化层通常在图像任务中按此顺序一起使用。此时,不必强迫自己记住何时使用这些层;在本书的后续章节中你将遇到大量实例!
如果这些内容让你感到困惑,可以放心,很多应用这些层的复杂工作已经被抽象出来,不需要你亲自处理。在上一章,我们建立神经网络时,需要管理各层的输入输出。这意味着必须确保矩阵的维度正确,以便操作能够顺利进行。深度学习库,如 MXNet 和 TensorFlow,会为你处理这些问题。
构建深度学习模型
现在我们已经掌握了基础知识,让我们来看看如何构建第一个真正的深度学习模型!我们将使用在第二章中使用过的UHI HAR数据集,训练预测模型。以下代码进行了一些数据准备:它加载数据并只选择存储均值的列(那些列名中包含mean的)。y变量的取值从 1 到 6;我们将减去 1,使其范围变为 0 到 5。此部分的代码位于Chapter4/uci_har.R中。它需要将UHI HAR数据集放在数据文件夹中;可以从archive.ics.uci.edu/ml/datasets/human+activity+recognition+using+smartphones下载并解压到数据文件夹中:
train.x <- read.table("../data/UCI HAR Dataset/train/X_train.txt")
train.y <- read.table("../data/UCI HAR Dataset/train/y_train.txt")[[1]]
test.x <- read.table("../data/UCI HAR Dataset/test/X_test.txt")
test.y <- read.table("../data/UCI HAR Dataset/test/y_test.txt")[[1]]
features <- read.table("../data/UCI HAR Dataset/features.txt")
meanSD <- grep("mean\\(\\)|std\\(\\)", features[, 2])
train.y <- train.y-1
test.y <- test.y-1
接下来,我们将转置数据并将其转换为矩阵。MXNet 期望数据的格式为宽度x高度,而不是高度x宽度:
train.x <- t(train.x[,meanSD])
test.x <- t(test.x[,meanSD])
train.x <- data.matrix(train.x)
test.x <- data.matrix(test.x)
下一步是定义计算图。我们创建一个占位符来存储数据,并创建两个全连接(或密集)层,后面跟着 relu 激活函数。第一层有 64 个节点,第二层有 32 个节点。然后我们创建一个最终的全连接层,包含六个节点——这是我们的 y 变量中的不同类别数。我们使用 softmax 激活函数,将最后六个节点的数值转换为每个类别的概率:
data <- mx.symbol.Variable("data")
fc1 <- mx.symbol.FullyConnected(data, name="fc1", num_hidden=64)
act1 <- mx.symbol.Activation(fc1, name="relu1", act_type="relu")
fc2 <- mx.symbol.FullyConnected(act1, name="fc2", num_hidden=32)
act2 <- mx.symbol.Activation(fc2, name="relu2", act_type="relu")
fc3 <- mx.symbol.FullyConnected(act2, name="fc3", num_hidden=6)
softmax <- mx.symbol.SoftmaxOutput(fc3, name="sm")
当你运行前面的代码时,实际上什么也不会执行。为了训练模型,我们创建一个devices对象,指示代码应该在哪运行,CPU 还是 GPU。然后将最后一层的符号(softmax)传递给mx.model.FeedForward.create函数。这个函数还有其他参数,更恰当地说它们是超参数。包括 epochs(num.round),它控制我们遍历数据的次数;学习率(learning.rate),它控制每次遍历时梯度更新的幅度;动量(momentum),这是一个可以帮助模型更快训练的超参数;以及权重初始化器(initializer),它控制节点的权重和偏置是如何初始化的。我们还传递了评估指标(eval.metric),即模型的评估方式,以及回调函数(epoch.end.callback),它用于输出进度信息。当我们运行该函数时,它会训练模型并根据我们在epoch.end.callback参数中使用的值输出进度信息,即每个 epoch:
devices <- mx.cpu()
mx.set.seed(0)
tic <- proc.time()
model <- mx.model.FeedForward.create(softmax, X = train.x, y = train.y,
ctx = devices,num.round = 20,
learning.rate = 0.08, momentum = 0.9,
eval.metric = mx.metric.accuracy,
initializer = mx.init.uniform(0.01),
epoch.end.callback =
mx.callback.log.train.metric(1))
Start training with 1 devices
[1] Train-accuracy=0.185581140350877
[2] Train-accuracy=0.26104525862069
[3] Train-accuracy=0.555091594827586
[4] Train-accuracy=0.519127155172414
[5] Train-accuracy=0.646551724137931
[6] Train-accuracy=0.733836206896552
[7] Train-accuracy=0.819100215517241
[8] Train-accuracy=0.881869612068966
[9] Train-accuracy=0.892780172413793
[10] Train-accuracy=0.908674568965517
[11] Train-accuracy=0.898572198275862
[12] Train-accuracy=0.896821120689655
[13] Train-accuracy=0.915544181034483
[14] Train-accuracy=0.928879310344828
[15] Train-accuracy=0.926993534482759
[16] Train-accuracy=0.934401939655172
[17] Train-accuracy=0.933728448275862
[18] Train-accuracy=0.934132543103448
[19] Train-accuracy=0.933324353448276
[20] Train-accuracy=0.934132543103448
print(proc.time() - tic)
user system elapsed
7.31 3.03 4.31
现在我们已经训练好了模型,让我们看看它在测试集上的表现:
preds1 <- predict(model, test.x)
pred.label <- max.col(t(preds1)) - 1
t <- table(data.frame(cbind(test.y,pred.label)),
dnn=c("Actual", "Predicted"))
acc<-round(100.0*sum(diag(t))/length(test.y),2)
print(t)
Predicted
Actual 0 1 2 3 4 5
0 477 15 4 0 0 0
1 108 359 4 0 0 0
2 13 42 365 0 0 0
3 0 0 0 454 37 0
4 0 0 0 141 391 0
5 0 0 0 16 0 521
print(sprintf(" Deep Learning Model accuracy = %1.2f%%",acc))
[1] " Deep Learning Model accuracy = 87.11%"
不错!我们在测试集上的准确率达到了87.11%。
等等,我们在前面的章节中覆盖的反向传播、导数等在哪里?答案是深度学习库大部分情况下会自动管理这些问题。在 MXNet 中,自动微分包含在一个称为 autograd 包中,该包使用链式法则对操作图进行微分。在构建深度学习模型时,这是少去的一件烦恼。更多信息,请访问 mxnet.incubator.apache.org/tutorials/gluon/autograd.html。
Use case – 使用 MXNet 进行分类和回归
在本节中,我们将使用一个新数据集创建一个二分类任务。我们将在这里使用的数据集是一个可在 www.dunnhumby.com/sourcefiles 获取的交易数据集。这个数据集是由 dunnhumby 提供的,他们因与 Tesco(一家英国超市)会员卡的关联而闻名,该会员卡是世界上最大的零售忠诚系统之一。我推荐阅读以下书籍,描述了 dunnhumby 如何通过分析他们的零售忠诚计划帮助 Tesco 成为第一零售商:Humby, Clive, Terry Hunt, and Tim Phillips. Scoring points. Kogan Page Publishers, 2008。尽管这本书相对较旧,但仍然是描述如何基于数据分析推出业务转型计划的最佳案例之一。
数据下载和探索
当你访问上述链接时,会有几个不同的数据选项;我们将使用的是称为 Let’s Get Sort-of-Real 的数据集。这个数据集是一个虚构零售忠诚计划的超过两年的数据。数据包括按篮子 ID 和顾客代码链接的购买记录,也就是说,我们可以追踪顾客随时间的交易。这里有多种选项,包括完整的数据集,压缩后大小为 4.3 GB,解压后超过 40 GB。对于我们的第一个模型,我们将使用最小的数据集,并将下载名为 All transactions for a randomly selected sample of 5,000 customers 的数据;这相当于完整数据库的 1/100 大小。
我要感谢 dunnhumby 发布这个数据集,并允许我们使用它。深度学习和机器学习中的一个问题是缺乏大规模真实生活数据集,供人们练习他们的技能。当一家公司努力发布这样一个数据集时,我们应该感谢他们的努力,并且不要在未经授权的情况下使用该数据集。请花时间阅读条款和条件,并仅将数据集用于个人学习目的。请记住,任何对这个数据集(或其他公司发布的数据集)的滥用,都会使公司更不愿意在未来发布其他数据集。
阅读完条款和条件并将数据集下载到你的电脑后,请将其解压到code文件夹下的dunnhumby/in目录中。确保文件直接解压到该文件夹下,而不是子目录中,否则你可能需要在解压后手动复制文件。数据文件采用逗号分隔(CSV)格式,每周一个独立文件。这些文件可以通过文本编辑器打开查看。我们将在分析中使用表 4.1中的一些字段:
| 字段名称 | 描述 | 格式 |
|---|---|---|
BASKET_ID |
购物篮 ID 或交易 ID。一个购物篮中的所有商品共享相同的basket_id值。 |
数值 |
CUST_CODE |
客户代码。将交易/访问与客户关联。 | 字符 |
SHOP_DATE |
购物发生的日期。日期采用 yyyymmdd 格式。 | 字符 |
STORE_CODE |
店铺代码。 | 字符 |
QUANTITY |
在该购物篮中购买的相同商品数量。 | 数值 |
SPEND |
与购买的商品相关的消费金额。 | 数值 |
PROD_CODE |
产品代码。 | 字符 |
PROD_CODE_10 |
产品层级 10 代码。 | 字符 |
PROD_CODE_20 |
产品层级 20 代码。 | 字符 |
PROD_CODE_30 |
产品层级 30 代码。 | 字符 |
PROD_CODE_40 |
产品层级 40 代码。 | 字符 |
表 4.1:交易数据集的部分数据字典
该数据存储了客户交易的详细信息。每个客户在购物交易中购买的独特商品都由一行表示,且同一交易中的商品会有相同的BASKET_ID字段。交易还可以通过CUST_CODE字段与客户关联。如果你想了解更多字段类型的信息,ZIP 文件中包含了一个 PDF 文件。
我们将使用该数据集进行客户流失预测任务。流失预测任务是指预测客户在接下来的x天内是否会回归。流失预测用于找出那些有可能离开你的服务的客户。购物忠诚计划、手机订阅、电视订阅等公司都会使用流失预测,以确保他们保持足够的客户数量。对于大多数依赖于订阅收入的公司来说,投入资源维持现有客户群比去获取新客户更为有效。这是因为获取新客户的成本较高。此外,随着时间推移,客户一旦流失,再将其吸引回来变得越来越难,因此在这段小的时间窗口内向他们发送特别优惠可能会促使他们留下。
除了二分类任务外,我们还将建立一个回归模型。该模型将预测一个人在未来 14 天内的消费金额。幸运的是,我们可以构建一个适合这两种预测任务的数据集。
数据以 117 个 CSV 文件的形式提供(忽略time.csv,它是一个查找文件)。第一步是进行一些基本的数据探索,验证数据是否已成功下载,然后执行一些基本的数据质量检查。这是任何分析中的重要第一步:尤其是当你使用外部数据集时,在创建任何机器学习模型之前,你应该对数据运行一些验证检查。Chapter4/0_Explore.Rmd脚本创建了一个汇总文件,并对数据进行了探索性分析。这是一个 RMD 文件,因此需要在 RStudio 中运行。为了简洁起见,并且因为本书是关于深度学习而非数据处理的,我将只包含部分输出和图表,而不是重现所有的代码。你也应该运行这个文件中的代码,以确保数据正确导入,尽管第一次运行可能需要几分钟时间。以下是该脚本中的一些数据汇总:
Number of weeks we have data: 117.
Number of transaction lines: 2541019.
Number of transactions (baskets): 390320.
Number of unique Customers: 5000.
Number of unique Products: 4997.
Number of unique Stores: 761.
如果我们将其与网站和 PDF 进行对比,数据看起来是有序的。我们有超过 250 万条记录,并且数据来自 5,000 个客户,跨越 761 个商店。数据探索脚本还会生成一些图表,帮助我们感受数据。图 4.3展示了 117 周内的销售数据;我们可以看到数据的多样性(它不是一条平坦的线,表明每天的数据都是不同的),并且没有缺失数据的间隙。数据中存在季节性模式,特别是在日历年的年底,即假日季节:

图 4.3:按时间绘制的销售数据。
图 4.3 中的图表表明数据已成功导入。数据看起来一致,符合零售交易文件的预期,我们没有看到任何缺失数据,并且存在季节性波动。
对于每个个人购买的商品,都有一个产品代码(PROD_CODE)和四个部门代码(PROD_CODE_10、PROD_CODE_20、PROD_CODE_30、PROD_CODE_40)。我们将在分析中使用这些部门代码;Chapter4/0_Explore.Rmd中的代码会为它们创建一个汇总。我们希望查看每个部门代码的独特值有多少,代码是否代表了一个层级结构(每个代码最多只有一个父级),以及是否存在重复代码:
PROD_CODE: Number of unique codes: 4997\. Number of repeated codes: 0.
PROD_CODE_10: Number of unique codes:250\. Number of repeated codes: 0.
PROD_CODE_20: Number of unique codes:90\. Number of repeated codes: 0.
PROD_CODE_30: Number of unique codes:31\. Number of repeated codes: 0.
PROD_CODE_40: Number of unique codes:9.
我们有 4,997 个独特的产品代码和 4 个部门代码。我们的部门代码从PROD_CODE_10开始,它有 250 个独特的代码,到PROD_CODE_40,它有 9 个独特的代码。这是一个产品部门代码层级结构,其中PROD_CODE_40是主类目,PROD_CODE_10是层级中最底层的部门代码。在PROD_CODE_10、PROD_CODE_20和PROD_CODE_30中的每个代码只有一个父级;例如,没有重复的代码,即每个部门代码只属于一个上级分类。我们没有提供查找文件来说明这些代码代表什么,但一个产品代码层级的例子可能类似于下面这样:
PROD_CODE_40 : Chilled goods
PROD_CODE_30 : Dairy
PROD_CODE_20 : Fresh Milk
PROD_CODE_10 : Full-fat Milk
PROD_CODE : Brand x Full-fat Milk
为了理解这些部门代码,我们还可以根据独特产品部门代码的数量绘制时间序列的销售数据,如图 4.4所示。这个图表也是在Chapter4/0_Explore.Rmd中创建的:

图 4.4:按日期购买的独特产品代码
请注意,对于此图,y轴表示独特的产品代码,而非销售数据。该数据看起来也很一致;虽然数据中有一些波峰和波谷,但它们不像图 4.3中那样显著,这是符合预期的。
为我们的模型准备数据
现在我们已经下载并验证了数据,我们可以用它来为我们的二分类和回归模型任务创建数据集。对于二分类任务,我们希望能够预测哪些客户将在接下来的两周内访问商店,对于回归任务,我们希望预测他们将在接下来的两周内消费多少钱。Chapter4/prepare_data.R脚本将原始交易数据转换为适合机器学习的格式。你需要运行代码来为模型创建数据集,但不必完全理解它是如何工作的。如果你想专注于深度学习模型的构建,可以跳过这部分内容。
我们需要将数据转换为适合预测任务的格式。每个我们要预测的实例应该只有一行。列将包括一些特征字段(X变量)和一个预测值字段(Y变量)。我们希望预测客户是否回访及他们的消费情况,因此我们的数据集每个客户将有一行,包含特征和预测变量。
第一步是找到一个截止日期,该日期将用于区分用于预测的变量(X)和我们将要预测的变量(Y)。代码会查看数据,找到最后一个交易日期;然后从该日期减去 13 天。这个日期就是截止日期,我们希望预测哪些客户将在截止日期及之后在我们的商店消费;根据截止日期之前发生的情况。截止日期之前的数据将用于生成我们的 X 或特征变量,截止日期及之后的销售数据将用于生成我们的 Y 或预测变量。以下是这部分代码:
library(readr)
library(reshape2)
library(dplyr)
source("import.R")
# step 1, merge files
import_data(data_directory,bExploreData=0)
# step 2, group and pivot data
fileName <- paste(data_directory,"all.csv",sep="")
fileOut <- paste(data_directory,"predict.csv",sep="")
df <- read_csv(fileName,col_types = cols(.default = col_character()))
# convert spend to numeric field
df$SPEND<-as.numeric(df$SPEND)
# group sales by date. we have not converted the SHOP_DATE to date
# but since it is in yyyymmdd format,
# then ordering alphabetically will preserve date order
sumSalesByDate<-df %>%
group_by(SHOP_WEEK,SHOP_DATE) %>%
summarise(sales = sum(SPEND)
)
# we want to get the cut-off date to create our data model
# this is the last date and go back 13 days beforehand
# therefore our X data only looks at everything from start to max date - 13 days
# and our Y data only looks at everything from max date - 13 days to end (i.e. 14 days)
max(sumSalesByDate$SHOP_DATE)
[1] "20080706"
sumSalesByDate2 <- sumSalesByDate[order(sumSalesByDate$SHOP_DATE),]
datCutOff <- as.character(sumSalesByDate2[(nrow(sumSalesByDate2)-13),]$SHOP_DATE)
datCutOff
[1] "20080623"
rm(sumSalesByDate,sumSalesByDate2)
如果此代码无法运行,最可能的原因是源数据未保存在正确的位置。数据集必须解压到名为 dunnhumby/in 的目录下,位于代码文件夹中,即与章节文件夹处于同一级别。
我们数据中的最后日期是20080706,即 2008 年 7 月 7 日,截止日期是 2008 年 6 月 23 日。尽管我们有从 2006 年开始的数据,但我们只会使用 2008 年的销售数据。任何超过六个月的数据不太可能影响未来的客户销售。任务是预测客户是否会在 2008 年 6 月 23 日至 2008 年 7 月 7 日之间回访,基于他们在 2008 年 6 月 23 日之前的活动。
我们现在需要从数据中创建特征;为了使用按部门代码拆分的消费数据,我们将使用PROD_CODE_40字段。我们可以直接按该部门代码对销售数据进行分组,但这将使得 2008 年 1 月和 2008 年 6 月的消费权重相等。我们希望在预测变量列中加入时间因素。因此,我们将创建一个部门代码和周组合的特征,这将使我们的模型更加重视近期的活动。首先,我们按客户代码、周和部门代码进行分组,并创建fieldName列。然后,我们将这些数据进行透视,创建我们的特征(X)数据集。该数据集的单元格值表示该客户(行)和该周-部门代码(列)的销售额。以下是两个客户数据转换后的示例。表 4.2显示了按周和PROD_CODE_40字段的销售支出。然后,表 4.3通过透视创建了一个数据集,数据集中每个客户只有一行,聚合字段现在作为列,销售额作为值:
CUST_CODE |
PROD_CODE_40 |
SHOP_WEEK |
fieldName |
Sales |
|---|---|---|---|---|
cust_001 |
D00001 | 200801 | D00001_200801 |
10.00 |
cust_002 |
D00001 | 200801 | D00001_200801 |
12.00 |
cust_001 |
D00015 | 200815 | D00015_200815 |
15.00 |
cust_001 |
D00020 | 200815 | D00020_200815 |
20.00 |
cust_002 |
D00030 | 200815 | D00030_200815 |
25.00 |
表 4.2:按客户代码、部门代码和周汇总的销售数据
CUST_CODE |
D00001_200801 |
D00015_200815 |
D00020_200815 |
D00030_200815 |
|---|---|---|---|---|
cust_001 |
10.00 | 15.00 | 20.00 | |
cust_002 |
12.00 | 25.00 |
表 4.3:表 4.2 转换后的数据
以下是执行此转换的代码:
# we are going to limit our data here from year 2008 only
# group data and then pivot it
sumTemp <- df %>%
filter((SHOP_DATE < datCutOff) & (SHOP_WEEK>="200801")) %>%
group_by(CUST_CODE,SHOP_WEEK,PROD_CODE_40) %>%
summarise(sales = sum(SPEND)
)
sumTemp$fieldName <- paste(sumTemp$PROD_CODE_40,sumTemp$SHOP_WEEK,sep="_")
df_X <- dcast(sumTemp,CUST_CODE ~ fieldName, value.var="sales")
df_X[is.na(df_X)] <- 0
预测变量(Y)是一个标志,表示客户是否在200818到200819周期间访问过该网站。我们在截止日期之后对数据进行分组,并按客户对销售数据进行分组,这些数据将形成我们Y值的基础。我们将X和Y数据集合并,确保通过左连接保留X端的所有行。最后,我们为二分类任务创建一个 1/0 标志。当我们完成时,我们看到数据集中有3933条记录:1560个未返回的客户和2373个已返回的客户。我们通过保存文件来完成模型构建的准备。以下代码展示了这些步骤:
# y data just needs a group to get sales after cut-off date
df_Y <- df %>%
filter(SHOP_DATE >= datCutOff) %>%
group_by(CUST_CODE) %>%
summarise(sales = sum(SPEND)
)
colnames(df_Y)[2] <- "Y_numeric"
# use left join on X and Y data, need to include all values from X
# even if there is no Y value
dfModelData <- merge(df_X,df_Y,by="CUST_CODE", all.x=TRUE)
# set binary flag
dfModelData$Y_categ <- 0
dfModelData[!is.na(dfModelData$Y_numeric),]$Y_categ <- 1
dfModelData[is.na(dfModelData$Y_numeric),]$Y_numeric <- 0
rm(df,df_X,df_Y,sumTemp)
nrow(dfModelData)
[1] 3933
table(dfModelData$Y_categ)
0 1
1560 2373
# shuffle data
dfModelData <- dfModelData[sample(nrow(dfModelData)),]
write_csv(dfModelData,fileOut)
我们使用销售数据来创建我们的预测字段,但在这项任务中,我们忽略了一些客户属性。这些字段包括Customers Price Sensitivity和Customers Lifestage。我们不使用这些字段的主要原因是避免数据泄漏。数据泄漏是在构建预测模型时可能发生的,它发生在某些字段的值在生产环境中创建数据集时不可用或不同。因为我们不知道这些字段的设置时间,它们可能是在客户注册时创建的,也可能是一个每天运行的过程。如果这些字段是在我们的截止日期之后创建的,这意味着它们可能会不公平地预测我们的Y变量。
例如,Customers Price Sensitivity字段有Less Affluent、Mid Market和Up Market等值,这些值可能是从客户的购买行为中推导出来的。因此,在进行流失预测任务时,如果这些字段在创建我们预测模型的数据集后的截止日期之后进行了更新,那么使用这些字段将导致数据泄漏。Customers Price Sensitivity字段中的Up Market值可能与回报支出高度相关,但实际上,这个值是其预测值的总结。数据泄漏是数据模型在生产环境中表现不佳的主要原因之一,因为模型是在包含与 Y 变量相关的数据上进行训练的,而这些数据在现实中永远不会存在。你应当始终检查时间序列任务中的数据泄漏,并问自己是否有任何字段(特别是查找属性)可能在创建模型数据所使用的日期之后被修改。
二分类模型
前一节的代码在dunnhumby文件夹中创建了一个名为predict.csv的新文件。这个数据集为每个客户创建了一行,其中包含一个 0/1 字段,表示他们在过去两周是否访问过,以及基于这两周前的销售数据的预测变量。现在,我们可以继续构建一些机器学习模型。Chapter4/binary_predict.R文件包含了我们第一个预测任务——二分类的代码。代码的第一部分加载数据,并通过包含所有列(除了客户 ID、二分类预测变量和回归预测变量)来创建预测变量数组。特征列都是数值型字段,这些字段分布呈现出明显的右偏,因此我们对这些字段应用了对数变换。我们首先加上0.01,以避免尝试对零值取对数时得到非数值结果(log(0)= -Inf)。
以下图表展示了数据在变换前后的情况,左边是变换前的数据,右边是变换后的数据:

图 4.5:特征变量在变换前后的分布。
第二张图中左边的大条表示原始字段为零的地方(log(0+0.01) = -4.6)。以下代码加载数据,执行对数变换,并生成前面的图:
set.seed(42)
fileName <- "../dunnhumby/predict.csv"
dfData <- read_csv(fileName,
col_types = cols(
.default = col_double(),
CUST_CODE = col_character(),
Y_categ = col_integer())
)
nobs <- nrow(dfData)
train <- sample(nobs, 0.9*nobs)
test <- setdiff(seq_len(nobs), train)
predictorCols <- colnames(dfData)[!(colnames(dfData) %in% c("CUST_CODE","Y_numeric","Y_categ"))]
# data is right-skewed, apply log transformation
qplot(dfData$Y_numeric, geom="histogram",binwidth=10,
main="Y value distribution",xlab="Spend")+theme(plot.title = element_text(hjust = 0.5))
dfData[, c("Y_numeric",predictorCols)] <- log(0.01+dfData[, c("Y_numeric",predictorCols)])
qplot(dfData$Y_numeric, geom="histogram",binwidth=0.5,
main="log(Y) value distribution",xlab="Spend")+theme(plot.title = element_text(hjust = 0.5))
trainData <- dfData[train, c(predictorCols)]
testData <- dfData[test, c(predictorCols)]
trainData$Y_categ <- dfData[train, "Y_categ"]$Y_categ
testData$Y_categ <- dfData[test, "Y_categ"]$Y_categ
在训练深度学习模型之前,我们先在数据上训练了三种机器学习模型——逻辑回归模型、Random Forest模型和XGBoost模型——作为基准。这个代码部分包含了数据加载、转换和三个模型:
#Logistic Regression Model
logReg=glm(Y_categ ~ .,data=trainData,family=binomial(link="logit"))
pr <- as.vector(ifelse(predict(logReg, type="response",
testData) > 0.5, "1", "0"))
# Generate the confusion matrix showing counts.
t<-table(dfData[test, c(predictorCols, "Y_categ")]$"Y_categ", pr,
dnn=c("Actual", "Predicted"))
acc<-round(100.0*sum(diag(t))/length(test),2)
print(t)
Predicted
Actual 0 1
0 130 42
1 48 174
print(sprintf(" Logistic regression accuracy = %1.2f%%",acc))
[1] " Logistic regression accuracy = 77.16%"
rm(t,pr,acc)
rf <- randomForest::randomForest(as.factor(Y_categ) ~ .,
data=trainData,
na.action=randomForest::na.roughfix)
pr <- predict(rf, newdata=testData, type="class")
# Generate the confusion matrix showing counts.
t<-table(dfData[test, c(predictorCols, "Y_categ")]$Y_categ, pr,
dnn=c("Actual", "Predicted"))
acc<-round(100.0*sum(diag(t))/length(test),2)
print(t)
Predicted
Actual 0 1
0 124 48
1 30 192
print(sprintf(" Random Forest accuracy = %1.2f%%",acc))
[1] " Random Forest accuracy = 80.20%"
rm(t,pr,acc)
xgb <- xgboost(data=data.matrix(trainData[,predictorCols]), label=trainData[,"Y_categ"]$Y_categ,
nrounds=75, objective="binary:logistic")
pr <- as.vector(ifelse(
predict(xgb, data.matrix(testData[, predictorCols])) > 0.5, "1", "0"))
t<-table(dfData[test, c(predictorCols, "Y_categ")]$"Y_categ", pr,
dnn=c("Actual", "Predicted"))
acc<-round(100.0*sum(diag(t))/length(test),2)
print(t)
Predicted
Actual 0 1
0 125 47
1 44 178
print(sprintf(" XGBoost accuracy = %1.2f%%",acc))
[1] " XGBoost accuracy = 76.90%"
rm(t,pr,acc)
我们创建了逻辑回归、Random Forest和XGBoost模型,原因有几个。首先,大部分工作已经在数据准备阶段完成,因此做这些模型是微不足道的。其次,这为我们提供了一个基准,可以将其与深度学习模型进行比较。第三,如果数据准备过程中存在问题,这些机器学习算法会更快速地发现这些问题,因为它们比训练深度学习模型要快。在这种情况下,我们只有几千条记录,因此这些机器学习算法能够轻松运行在这些数据上。如果数据过大而这些算法无法处理,我会考虑取一个较小的样本来运行我们的基准任务。有许多机器学习算法可供选择,但我选择这些算法作为基准,原因如下:
-
逻辑回归是一个基础模型,始终是一个不错的基准选择。
-
Random Forest以其默认参数训练表现良好,并且对过拟合和相关变量(我们这里有的)具有鲁棒性。 -
XGBoost被一致认为是表现最好的机器学习算法之一。
这三种算法的准确度相似,最高的准确度由Random Forest模型取得,准确率为 80.2%。我们现在知道这个数据集适用于预测任务,并且有了基准可以进行比较。
接下来我们将使用 MXNet 构建一个深度学习模型:
require(mxnet)
# MXNet expects matrices
train_X <- data.matrix(trainData[, predictorCols])
test_X <- data.matrix(testData[, predictorCols])
train_Y <- trainData$Y_categ
# hyper-parameters
num_hidden <- c(128,64,32)
drop_out <- c(0.2,0.2,0.2)
wd=0.00001
lr <- 0.03
num_epochs <- 40
activ <- "relu"
# create our model architecture
# using the hyper-parameters defined above
data <- mx.symbol.Variable("data")
fc1 <- mx.symbol.FullyConnected(data, name="fc1", num_hidden=num_hidden[1])
act1 <- mx.symbol.Activation(fc1, name="activ1", act_type=activ)
drop1 <- mx.symbol.Dropout(data=act1,p=drop_out[1])
fc2 <- mx.symbol.FullyConnected(drop1, name="fc2", num_hidden=num_hidden[2])
act2 <- mx.symbol.Activation(fc2, name="activ2", act_type=activ)
drop2 <- mx.symbol.Dropout(data=act2,p=drop_out[2])
fc3 <- mx.symbol.FullyConnected(drop2, name="fc3", num_hidden=num_hidden[3])
act3 <- mx.symbol.Activation(fc3, name="activ3", act_type=activ)
drop3 <- mx.symbol.Dropout(data=act3,p=drop_out[3])
fc4 <- mx.symbol.FullyConnected(drop3, name="fc4", num_hidden=2)
softmax <- mx.symbol.SoftmaxOutput(fc4, name="sm")
# run on cpu, change to 'devices <- mx.gpu()'
# if you have a suitable GPU card
devices <- mx.cpu()
mx.set.seed(0)
tic <- proc.time()
# This actually trains the model
model <- mx.model.FeedForward.create(softmax, X = train_X, y = train_Y,
ctx = devices,num.round = num_epochs,
learning.rate = lr, momentum = 0.9,
eval.metric = mx.metric.accuracy,
initializer = mx.init.uniform(0.1),
wd=wd,
epoch.end.callback = mx.callback.log.train.metric(1))
print(proc.time() - tic)
user system elapsed
9.23 4.65 4.37
pr <- predict(model, test_X)
pred.label <- max.col(t(pr)) - 1
t <- table(data.frame(cbind(testData[,"Y_categ"]$Y_categ,pred.label)),
dnn=c("Actual", "Predicted"))
acc<-round(100.0*sum(diag(t))/length(test),2)
print(t)
Predicted
Actual 0 1
0 136 36
1 54 168
print(sprintf(" Deep Learning Model accuracy = %1.2f%%",acc))
[1] " Deep Learning Model accuracy = 77.16%"
rm(t,pr,acc)
rm(data,fc1,act1,fc2,act2,fc3,act3,fc4,softmax,model)
深度学习模型在测试数据上的准确度为77.16%,仅被Random Forest模型超越。这表明,深度学习模型可以与最好的机器学习算法竞争。这也表明,深度学习模型在分类任务中并不总是优于其他机器学习算法。我们使用这些模型提供基准,以便知道我们的深度学习模型是否得到了不错的结果;它让我们有信心相信我们的深度学习模型是有竞争力的。
我们的深度学习模型在每一层使用 20%的dropout并且使用权重衰减进行正则化。如果没有使用dropout,模型会明显过拟合。这可能是因为特征之间高度相关,因为我们的列表示的是各个部门的开支。可以推测,如果一列是某种类型的面包,另一列是某种类型的牛奶,那么这些开支会一起变化,也就是说,花费更多且交易更多的人更可能同时购买这两种商品。
回归模型
前一节开发了一个用于二分类任务的深度学习模型,本节开发了一个用于预测连续数值的深度学习回归模型。我们使用与二分类任务相同的数据集,但使用不同的目标列进行预测。在那个任务中,我们想要预测客户是否会在接下来的 14 天内回到我们的商店。而在这个任务中,我们想要预测客户在接下来的 14 天内将会在我们的商店花费多少。我们遵循类似的流程,通过对数据应用对数变换来加载和准备数据集。代码位于Chapter4/regression.R中:
set.seed(42)
fileName <- "../dunnhumby/predict.csv"
dfData <- read_csv(fileName,
col_types = cols(
.default = col_double(),
CUST_CODE = col_character(),
Y_categ = col_integer())
)
nobs <- nrow(dfData)
train <- sample(nobs, 0.9*nobs)
test <- setdiff(seq_len(nobs), train)
predictorCols <- colnames(dfData)[!(colnames(dfData) %in% c("CUST_CODE","Y_numeric","Y_numeric"))]
dfData[, c("Y_numeric",predictorCols)] <- log(0.01+dfData[, c("Y_numeric",predictorCols)])
trainData <- dfData[train, c(predictorCols,"Y_numeric")]
testData <- dfData[test, c(predictorCols,"Y_numeric")]
xtrain <- model.matrix(Y_numeric~.,trainData)
xtest <- model.matrix(Y_numeric~.,testData)
然后,我们使用lm对数据进行回归分析,以便在创建深度学习模型之前建立基准:
# lm Regression Model
regModel1=lm(Y_numeric ~ .,data=trainData)
pr1 <- predict(regModel1,testData)
rmse <- sqrt(mean((exp(pr1)-exp(testData[,"Y_numeric"]$Y_numeric))²))
print(sprintf(" Regression RMSE = %1.2f",rmse))
[1] " Regression RMSE = 29.30"
mae <- mean(abs(exp(pr1)-exp(testData[,"Y_numeric"]$Y_numeric)))
print(sprintf(" Regression MAE = %1.2f",mae))
[1] " Regression MAE = 13.89"
我们为回归任务输出两个指标,rmse 和 mae。我们在本章前面已经介绍过这些指标。平均绝对误差(mae)衡量的是预测值与实际值之间的绝对差异。均方根误差(rmse)对预测值与实际值之间差异的平方进行惩罚,因此一个大的错误比多个小错误的总和代价更高。现在,让我们来看一下深度学习回归代码。首先,我们加载数据并定义模型:
require(mxnet)
Loading required package: mxnet
# MXNet expects matrices
train_X <- data.matrix(trainData[, predictorCols])
test_X <- data.matrix(testData[, predictorCols])
train_Y <- trainData$Y_numeric
set.seed(42)
# hyper-parameters
num_hidden <- c(256,128,128,64)
drop_out <- c(0.4,0.4,0.4,0.4)
wd=0.00001
lr <- 0.0002
num_epochs <- 100
activ <- "tanh"
# create our model architecture
# using the hyper-parameters defined above
data <- mx.symbol.Variable("data")
fc1 <- mx.symbol.FullyConnected(data, name="fc1", num_hidden=num_hidden[1])
act1 <- mx.symbol.Activation(fc1, name="activ1", act_type=activ)
drop1 <- mx.symbol.Dropout(data=act1,p=drop_out[1])
fc2 <- mx.symbol.FullyConnected(drop1, name="fc2", num_hidden=num_hidden[2])
act2 <- mx.symbol.Activation(fc2, name="activ2", act_type=activ)
drop2 <- mx.symbol.Dropout(data=act2,p=drop_out[2])
fc3 <- mx.symbol.FullyConnected(drop2, name="fc3", num_hidden=num_hidden[3])
act3 <- mx.symbol.Activation(fc3, name="activ3", act_type=activ)
drop3 <- mx.symbol.Dropout(data=act3,p=drop_out[3])
fc4 <- mx.symbol.FullyConnected(drop3, name="fc4", num_hidden=num_hidden[4])
act4 <- mx.symbol.Activation(fc4, name="activ4", act_type=activ)
drop4 <- mx.symbol.Dropout(data=act4,p=drop_out[4])
fc5 <- mx.symbol.FullyConnected(drop4, name="fc5", num_hidden=1)
lro <- mx.symbol.LinearRegressionOutput(fc5)
现在我们开始训练模型;注意,第一个注释展示了如何切换到使用 GPU 而非 CPU:
# run on cpu, change to 'devices <- mx.gpu()'
# if you have a suitable GPU card
devices <- mx.cpu()
mx.set.seed(0)
tic <- proc.time()
# This actually trains the model
model <- mx.model.FeedForward.create(lro, X = train_X, y = train_Y,
ctx = devices,num.round = num_epochs,
learning.rate = lr, momentum = 0.9,
eval.metric = mx.metric.rmse,
initializer = mx.init.uniform(0.1),
wd=wd,
epoch.end.callback = mx.callback.log.train.metric(1))
print(proc.time() - tic)
user system elapsed
13.90 1.82 10.50
pr4 <- predict(model, test_X)[1,]
rmse <- sqrt(mean((exp(pr4)-exp(testData[,"Y_numeric"]$Y_numeric))²))
print(sprintf(" Deep Learning Regression RMSE = %1.2f",rmse))
[1] " Deep Learning Regression RMSE = 28.92"
mae <- mean(abs(exp(pr4)-exp(testData[,"Y_numeric"]$Y_numeric)))
print(sprintf(" Deep Learning Regression MAE = %1.2f",mae))
[1] " Deep Learning Regression MAE = 14.33"
rm(data,fc1,act1,fc2,act2,fc3,act3,fc4,lro,model)
对于回归指标,越低越好,因此我们在深度学习模型上的 rmse 指标(28.92)比原始回归模型(29.30)有所改进。有趣的是,深度学习模型上的 mae(14.33)实际上比原始回归模型(13.89)更差。由于 rmse 对实际值和预测值之间的大差异惩罚更多,这表明深度学习模型的误差比回归模型的误差更为温和。
改进二分类模型
本节基于之前的二分类任务,并着眼于提高该任务的准确性。我们可以改善模型的第一件事是使用更多的数据,实际上是使用 100 倍的数据!我们将下载整个数据集,压缩文件大小超过 4GB,解压后数据大小为 40GB。回到下载链接(www.dunnhumby.com/sourcefiles),再次选择Let’s Get Sort-of-Real,并下载完整数据集的所有文件。共有九个文件需要下载,CSV 文件应解压到dunnhumby/in文件夹中。记得检查 CSV 文件是否在该文件夹中,而不是在子文件夹中。你需要再次运行Chapter4/prepare_data.R中的代码。完成后,predict.csv文件应包含 390,000 条记录。
你可以尝试跟着这个步骤进行,但要注意,准备数据并运行深度学习模型将会花费很长时间。如果你的计算机较慢,可能还会遇到一些问题。我在一台配备 32 GB RAM 的 Intel i5 处理器的电脑上测试了这段代码,模型运行了 30 分钟。而且,它还需要超过 50 GB 的硬盘空间来存储解压后的文件和临时文件。如果你在本地计算机上运行时遇到问题,另一种选择是将这个例子运行在云端,我们将在后续章节中介绍。
本节代码在Chapter4/binary_predict2.R脚本中。由于我们有更多数据,我们可以构建一个更复杂的模型。我们有 100 倍的数据量,因此我们的新模型增加了一层,并在隐藏层中添加了更多的节点。我们减少了正则化量并调整了学习率,还增加了训练的周期数。下面是Chapter4/binary_predict2.R中的代码,构建并训练深度学习模型。我们没有包含加载和准备数据的样板代码,因为这些部分与原始脚本没有变化:
# hyper-parameters
num_hidden <- c(256,128,64,32)
drop_out <- c(0.2,0.2,0.1,0.1)
wd=0.0
lr <- 0.03
num_epochs <- 50
activ <- "relu"
# create our model architecture
# using the hyper-parameters defined above
data <- mx.symbol.Variable("data")
fc1 <- mx.symbol.FullyConnected(data, name="fc1", num_hidden=num_hidden[1])
act1 <- mx.symbol.Activation(fc1, name="activ1", act_type=activ)
drop1 <- mx.symbol.Dropout(data=act1,p=drop_out[1])
fc2 <- mx.symbol.FullyConnected(drop1, name="fc2", num_hidden=num_hidden[2])
act2 <- mx.symbol.Activation(fc2, name="activ2", act_type=activ)
drop2 <- mx.symbol.Dropout(data=act2,p=drop_out[2])
fc3 <- mx.symbol.FullyConnected(drop2, name="fc3", num_hidden=num_hidden[3])
act3 <- mx.symbol.Activation(fc3, name="activ3", act_type=activ)
drop3 <- mx.symbol.Dropout(data=act3,p=drop_out[3])
fc4 <- mx.symbol.FullyConnected(drop3, name="fc4", num_hidden=num_hidden[4])
act4 <- mx.symbol.Activation(fc4, name="activ4", act_type=activ)
drop4 <- mx.symbol.Dropout(data=act4,p=drop_out[4])
fc5 <- mx.symbol.FullyConnected(drop4, name="fc5", num_hidden=2)
softmax <- mx.symbol.SoftmaxOutput(fc5, name="sm")
# run on cpu, change to 'devices <- mx.gpu()'
# if you have a suitable GPU card
devices <- mx.cpu()
mx.set.seed(0)
tic <- proc.time()
# This actually trains the model
model <- mx.model.FeedForward.create(softmax, X = train_X, y = train_Y,
ctx = devices,num.round = num_epochs,
learning.rate = lr, momentum = 0.9,
eval.metric = mx.metric.accuracy,
initializer = mx.init.uniform(0.1),
wd=wd,
epoch.end.callback = mx.callback.log.train.metric(1))
print(proc.time() - tic)
user system elapsed
1919.75 1124.94 871.31
pr <- predict(model, test_X)
pred.label <- max.col(t(pr)) - 1
t <- table(data.frame(cbind(testData[,"Y_categ"]$Y_categ,pred.label)),
dnn=c("Actual", "Predicted"))
acc<-round(100.0*sum(diag(t))/length(test),2)
print(t)
Predicted
Actual 0 1
0 10714 4756
1 3870 19649
print(sprintf(" Deep Learning Model accuracy = %1.2f%%",acc))
[1] " Deep Learning Model accuracy = 77.88%"
准确率已经从早期模型的77.16%提高到该模型的77.88%。这看起来可能不算显著,但如果考虑到大数据集包含了近 39 万行数据,准确度提高了 0.72%就意味着大约 2,808 个客户现在被正确分类。如果每个客户的价值为 50 美元,那么这将为公司带来额外的 14 万美元收入。
通常来说,当你增加更多的数据时,你的模型应该变得更加复杂,以便能够在所有数据模式中进行泛化。我们将在第六章《调优与优化模型》中详细讲解这一点,但我鼓励你在Chapter4/binary_predict.R中实验代码。试着更改超参数或添加更多层。即使准确度提高了 0.1% - 0.2%,也是一个显著的进步。如果你能在这个数据集上达到超过 78%的准确度,那就算是一个不错的成就。
如果你想进一步探索,还有其他方法可以调查。这些方法涉及如何创建模型数据的变化。如果你真的想挑战自己,以下是一些你可以尝试的额外想法:
-
我们当前的特征是部门代码和周数的组合,我们使用
PROD_CODE_40字段作为部门代码。这个字段只有九个唯一值,因此每一周只有九个字段表示这些数据。如果你使用PROD_CODE_30、PROD_CODE_20或PROD_CODE_10,你将会创建更多的特征。 -
以类似的方式,你可以尝试使用部门代码和天数,而不是使用部门代码和周数。这可能会产生太多的特征,但我建议你考虑在截止日期之前的最后 14 天进行此操作。
-
尝试不同的数据准备方法。我们使用对数尺度,在我们的二分类任务中效果很好,但对于回归任务来说并非最佳方法,因为它不会创建具有正态分布的数据。尝试应用 z-scaling 和 min-max 标准化到数据中。如果这样做,必须确保在评估模型之前正确地应用到测试数据上。
-
训练数据使用销售金额。您可以将此更改为商品数量或商品所在的交易次数。
-
您可以创建新的特征。一个潜在强大的例子是基于一周中的某一天或月份的字段。我们可以为每周的消费金额和访问次数创建特征。
-
我们可以基于购物篮平均大小、顾客访问频率等创建特征。
-
我们可以尝试一个可以利用时间序列数据的不同模型架构。
如果给我这样一个工作任务,这些都是我会尝试的事情。在传统机器学习中,添加更多特征通常会导致问题,因为大多数传统机器学习算法在高维数据上很难处理。深度学习模型可以处理这些情况,因此通常添加更多特征是没有害处的。
数据的不合理有效性
我们的第一个二分类任务的深度学习模型少于 4,000 条记录。我们这样做是为了您可以快速运行示例。对于深度学习,您确实需要更多的数据,因此我们创建了一个更复杂的模型,并使用了更多的数据,这提高了我们的准确性。这个过程证明了以下事实:
-
在使用深度学习模型之前,与其他机器学习算法建立基准提供了一个很好的参考点
-
我们不得不创建一个更复杂的模型,并调整我们更大数据集的超参数
-
数据的不合理有效性
这里的最后一点来自彼得·诺维格的一篇文章,可在static.googleusercontent.com/media/research.google.com/en//pubs/archive/35179.pdf找到。同名的 YouTube 视频也有。诺维格文章的一个主要观点是:简单模型和大量数据始终胜过基于较少数据的更复杂模型。
我们已经将深度学习模型的准确率提高了 0.38%。考虑到我们的数据集包含高度相关的变量,而且我们所处的领域是建模人类活动,这个提升还算不错。人类,嗯,是可以预测的;所以在尝试预测他们接下来做什么时,一个小的数据集通常就足够了。而在其他领域,增加数据往往会有更大的效果。考虑一下一个复杂的图像识别任务,图像包含颜色,且图像的质量和格式不一致。在这种情况下,若将我们的训练数据增加 10 倍,比之前的例子会产生更大的效果。对于许多深度学习项目,应该从项目一开始就包括获取更多数据的任务。可以通过手动标注数据,外包任务(如 Amazon Turk),或在你的应用中建立某种形式的反馈机制来完成。
虽然其他机器学习算法在增加数据后也可能会看到性能提升,但最终增加数据将不再产生任何变化,性能将停滞不前。这是因为这些算法从未设计用于处理大型高维数据,因此无法建模非常大的数据集中的复杂模式。然而,你可以构建越来越复杂的深度学习架构来建模这些复杂的模式。以下图表展示了深度学习算法如何继续利用更多数据,并且在其他机器学习算法的性能停滞后,性能依然能够提升:

图 4.6:深度学习模型与其他机器学习模型在数据集大小增加时准确率的变化
总结
本章涵盖了许多内容。我们研究了激活函数,并使用 MXNet 构建了我们的第一个真正的深度学习模型。接着,我们使用一个实际的数据集,创建了两个应用机器学习模型的用例。第一个用例是预测哪些客户将来会回来,基于他们过去的活动。这是一个二分类任务。第二个用例是预测客户将来会花费多少钱,基于他们过去的活动。这是一个回归任务。我们先在一个小数据集上运行了两个模型,并使用不同的机器学习库将它们与我们的深度学习模型进行比较。我们的深度学习模型超越了所有算法。
然后我们进一步使用了一个比之前大 100 倍的数据集。我们构建了一个更大的深度学习模型,并调整了参数,提升了我们在二分类任务中的准确性。我们以简短的讨论结束本章,探讨了深度学习模型如何在大数据集上超越传统的机器学习算法。
在下一章中,我们将探讨计算机视觉任务,深度学习在这一领域已带来革命性变化。
第五章:使用卷积神经网络进行图像分类
毫不夸张地说,深度学习领域对卷积神经网络的巨大兴趣增长,可以说主要归功于卷积神经网络。卷积神经网络(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 World。MNIST数据集包含 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 架构:

图 5.1:LeNet 架构
原始图像大小为 28 x 28。我们有一系列的隐藏层,包括卷积层和池化层(在这里,它们被标记为子采样)。每个卷积层改变结构;例如,当我们在第一隐藏层应用卷积时,输出的大小是三维的。我们的最终层大小是 10 x 1,这与类别的数量相同。我们可以在这里应用一个softmax函数,将这一层的值转换为每个类别的概率。具有最高概率的类别将是每个图像的类别预测。
卷积层
本节将更深入地展示卷积层的工作原理。从基本层面来看,卷积层不过是一组滤波器。当你戴着带有红色镜片的眼镜看图像时,所有的事物似乎都带有红色的色调。现在,想象这些眼镜由不同的色调组成,也许是带有红色色调的镜片,里面还嵌入了一些水平绿色色调。如果你拥有这样一副眼镜,效果将是突出前方场景的某些方面。任何有绿色水平线的地方都会更加突出。
卷积层会在前一层的输出上应用一系列补丁(或卷积)。例如,在人脸识别任务中,第一层的补丁会识别图像中的基本特征,例如边缘或对角线。这些补丁会在图像上移动,匹配图像的不同部分。下面是一个 3 x 3 卷积块在 6 x 6 图像上应用的示例:

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

图 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 的图像。下面是应用程序的截图,选择的是第四张图像,显示的是数字四:

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

图 5.5:显示垂直卷积滤波器的 Shiny 应用
现在我们可以看到,在应用卷积后,原始图像中的垂直线在右侧的结果图像中被突出显示。实际上,应用这些滤波器是一种特征提取的方式。我鼓励你使用该应用浏览图像,看看不同的卷积是如何应用于不同类别的图像的。
这就是卷积滤波器的基础,虽然它是一个简单的概念,但当你开始做两件事时,它会变得非常强大:
-
将许多卷积滤波器组合成卷积层
-
将另一组卷积滤波器(即卷积层)应用于先前卷积层的输出
这可能需要一些时间才能理解。如果我对一张图像应用了一个滤波器,然后对该输出再次应用一个滤波器,我得到的结果是什么?如果我再应用第三次,也就是先对一张图像应用滤波器,再对该输出应用滤波器,然后对该输出再应用滤波器,我得到的是什么?答案是,每一层后续的卷积层都会结合前一层识别到的特征,找到更为复杂的模式,例如角落、弧线等等。后续的层会发现更丰富的特征,比如一个带有弧形的圆圈,表示人的眼睛。
有两个参数用于控制卷积的移动:填充(padding)和步幅(strides)。在下图中,我们可以看到原始图像的大小是 6 x 6,而有 4 x 4 的子图。我们因此将数据表示从 6 x 6 矩阵减少到了 4 x 4 矩阵。当我们将大小为c1,c2的卷积应用于大小为 n,m 的数据时,输出将是n-c1+1,m-c2+1。如果我们希望输出与输入大小相同,可以通过在图像边缘添加零来填充输入。对于之前的例子,我们在整个图像周围添加一个 1 像素的边框。下图展示了如何将第一个 3 x 3 卷积应用于具有填充的图像:

图 5.6 在卷积前应用的填充
我们可以应用于卷积的第二个参数是步幅(strides),它控制卷积的移动。默认值为 1,这意味着卷积每次移动 1 个单位,首先向右移动,然后向下移动。在实际应用中,这个值很少被改变,因此我们将不再进一步讨论它。
我们现在知道,卷积像小型特征生成器一样工作,它们应用于输入层(对于第一层来说是图像数据),而后续的卷积层则发现更复杂的特征。那么它们是如何计算的呢?我们是否需要精心手动设计一组卷积来应用到我们的模型中?答案是否定的;这些卷积是通过梯度下降算法的魔力自动计算出来的。最佳的特征模式是在经过多次训练数据集迭代后找到的。
那么,一旦我们超越了 2-3 层卷积层,卷积是如何工作的呢?答案是,任何人都很难理解卷积层具体的数学原理。即使是这些网络设计的原始设计者,也可能并不完全理解一系列卷积神经网络中的隐藏层在做什么。如果这让你感到担忧,请记住,2012 年赢得 ImageNet 竞赛的解决方案有 6000 万个参数。随着计算能力的进步,深度学习架构可能会有数亿个参数。对于任何人来说,完全理解如此复杂的模型中的每一个细节是不可能的。这就是为什么这些模型常常被称为黑箱模型的原因。
这可能一开始让你感到惊讶。深度学习如何在图像分类中实现人类水平的表现?如果我们不能完全理解它们是如何工作的,我们又如何构建深度学习模型呢?这个问题已经将深度学习社区分裂,主要是行业与学术界之间的分歧。许多(但不是所有)研究人员认为,我们应该更深入地理解深度学习模型的工作原理。一些研究人员还认为,只有通过更好地理解当前架构的工作原理,我们才能开发出下一代人工智能应用。在最近的 NIPS 会议上(这是深度学习领域最古老和最重要的会议之一),深度学习被不利地与炼金术相提并论。与此同时,业界的从业者并不关心深度学习是如何工作的。他们更关注构建更加复杂的深度学习架构,以最大化准确性或性能。
当然,这只是业界现状的粗略描述;并不是所有学者都是向内看的,也不是所有从业者都仅仅是在调整模型以获得小幅改进。深度学习仍然相对较新(尽管神经网络的基础块已经存在了数十年)。但这种紧张关系确实存在,并且已经持续了一段时间——例如,一种流行的深度学习架构引入了Inception模块,命名灵感来源于电影《盗梦空间》中的Inception。在这部电影中,莱昂纳多·迪卡普里奥带领一个团队,通过进入人们的梦境来改变他们的思想和观点。最初,他们只进入一个层次的梦境,但随后深入下去,实际上进入了梦中的梦境。随着他们深入,梦境变得越来越复杂,结果也变得不确定。我们在这里不会详细讨论Inception 模块的具体内容,但它们将卷积层和最大池化层并行结合在一起。论文的作者在论文中承认了该模型的内存和计算成本,但通过将关键组件命名为Inception 模块,他们巧妙地暗示了自己站在哪一方。
在 2012 年 ImageNet 竞赛获胜者的突破性表现之后,两个研究人员对模型的工作原理缺乏洞见感到不满。他们决定逆向工程该算法,尝试展示导致特征图中某一激活的输入模式。这是一项非平凡的任务,因为原始模型中使用的某些层(例如池化层)会丢弃信息。他们的论文展示了每一层的前 9 个激活。以下是第一层的特征可视化:

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

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

图 5.9:CNN 第三层的特征可视化 来源:https://cs.nyu.edu/~fergus/papers/zeilerECCV2014.pdf
在第三层中,我们可以看到一些非常有趣的模式。在第二行第二列,我们已经识别出车轮的部分。在第三行第三列,我们已经开始识别人脸。在第二行第四列,我们识别出了图像中的文本。
在论文中,作者展示了更多层的示例。我鼓励你阅读论文,进一步了解卷积层的工作原理。
需要注意的是,尽管深度学习模型在图像分类任务中能够达到与人类相当的表现,但它们并不像人类那样解读图像。它们没有“猫”或“狗”的概念,它们只能匹配给定的模式。在论文中,作者强调了一个例子,在这个例子中,匹配的模式几乎没有任何相似之处;模型匹配的是背景中的特征(如草地),而不是前景中的物体。
在另一个图像分类任务中,模型在实际操作中失败了。任务是区分狼和狗。模型在实际应用中失败是因为训练数据包含了处于自然栖息地的狼——即雪地。因此,模型误以为任务是区分雪和狗。任何在其他环境下的狼的图像都会被错误分类。
从中得到的教训是,训练数据应该是多样化的,并且与模型预期要预测的实际数据密切相关。理论上这可能听起来很显而易见,但在实践中往往并不容易做到。我们将在下一章进一步讨论这一点。
池化层
池化层在卷积神经网络(CNN)中用于减少模型中的参数数量,因此可以减少过拟合。它们可以被看作是一种降维方式。类似于卷积层,池化层在上一层上滑动,但操作和返回值不同。它返回一个单一的值,通常是该区域内各个单元格的最大值,因此称之为最大池化。你也可以执行其他操作,例如平均池化,但这种方式较少使用。这里是一个使用 2 x 2 块进行最大池化的例子。第一个块的值为 7、0、6、6,其中最大值为 7,因此输出为 7。注意,最大池化通常不使用填充(padding),并且它通常会应用步幅参数来移动块。这里的步幅是 2,因此在获取第一个块的最大值后,我们会向右移动 2 个单元:

图 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 的神经网络示例:

图 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 函数倾向于使某个类别的概率远远大于其他类别,这正是我们所希望的:

图 5.12 Softmax 函数的示例。
使用 MXNet 库进行图像分类。
MXNet 包在第一章中介绍过,深度学习入门,如果你还没有安装这个包,可以回到该章查看安装说明。我们将演示如何在图像数据分类任务中获得接近 100% 的准确率。我们将使用在第二章中介绍的 MNIST 数据集,使用卷积神经网络进行图像分类。该数据集包含手写数字(0-9)的图像,所有图像大小为 28 x 28。它是深度学习中的 Hello World!。Kaggle 上有一个长期的竞赛使用这个数据集。脚本 Chapter5/explore.Rmd 是一个 R markdown 文件,用于探索这个数据集。
- 首先,我们将检查数据是否已经下载,如果没有,我们将下载它。如果该链接无法获取数据,请参阅
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=""))
}
- 接下来,我们将数据读取到 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 个实例及其数据标签。
- 为此,我们将创建一个名为
plotInstance的helper函数,该函数接受像素值并输出图像,带有可选的标题:
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 张图像及其分类:

图 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")
让我们详细查看这段代码:
-
在
mxnet中,我们使用其自有的数据类型符号来配置网络。 -
我们创建了第一个隐藏层(
fullconnect1 <- ....)。这些参数是输入数据、层的名称以及该层的神经元数。 -
我们对
fullconnect层应用激活函数(activation1 <- ....)。mx.symbol.Activation函数接收来自第一个隐藏层fullconnect1的输出。 -
第二个隐藏层(
fullconnect1 <- ....)将activation1作为输入。 -
第二个激活函数类似于
activation1。 -
fullconnect3是输出层。该层有 10 个神经元,因为这是一个多分类问题,共有 10 个类别。 -
最后,我们使用 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)
这会生成以下图表,展示深度学习模型的架构:

图 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数据集创建示例时所用的相同样板代码,生成的一些来自该数据集的示例图像:

图 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(或训练轮次)后的表现。以下是生成的截图:

图 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)
这是一个与客户流失问题类似任务的学习曲线示例:

图 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 条记录的鸢尾花数据集。我们之前在第四章中看到过以下图表,训练深度预测模型,该图展示了机器学习算法的准确度在数据量增加时往往会停滞。因此,最大化可用于训练的数据量的动机较小。深度学习模型可以利用更多的数据,因此如果我们可以为测试集使用更少的数据,我们应该能得到一个更好的模型:

图 6.2:数据集大小如何影响深度学习模型与其他机器学习模型的准确度
数据在训练集、测试集和验证集之间的划分。
前一节强调了在项目早期阶段获取一些数据的重要性。但如果你没有足够的数据来训练深度学习模型,仍然可以使用其他数据进行训练并将其应用到你的数据上。例如,你可以使用在 ImageNet 数据上训练的模型来进行图像分类任务。在这种情况下,你需要明智地使用收集到的真实数据。本节讨论了关于这一主题的一些好实践。
如果你曾经想过,为什么像谷歌、苹果、Facebook、亚马逊等大公司在人工智能方面具有如此大的领先优势,原因就在于此。虽然他们有世界上最优秀的 AI 专家为他们工作,但他们的最大优势在于他们可以使用大量的标注数据来构建他们的机器学习模型。
在前一节中,我们说过测试集的唯一目的是评估模型。但是,如果这些数据和模型在预测任务中将会遇到的数据不来自相同的分布,那么评估结果会产生误导。项目的一个重要优先事项应该是尽早获取与现实数据相似的标注数据。一旦你获得了这些数据,你需要聪明地使用这一宝贵资产。根据优先级,最好的数据使用方式如下:
-
我可以使用一些数据来创建更多的训练数据吗?这可以通过数据增强,或者实现一个早期原型让用户进行互动来实现。
-
如果你正在构建多个模型(这是应该做的),请使用验证集中的一些数据来调整模型。
-
使用测试集中的数据来评估模型。
-
使用训练集中的数据。
其中一些建议可能会引发争议,尤其是建议你应该在测试集之前使用验证集的数据。记住,测试集的唯一目的是用来评估模型,它应该只使用一次,所以你只有一次使用这些数据的机会。如果我只有少量的真实数据,我更倾向于用它来调整模型,并接受较不精确的评估指标,而不是用它来获得一个评估指标非常精确但表现差劲的模型。
这种方法有风险,理想情况下,你希望验证数据集和测试数据集来自相同的分布,并且能够代表模型在生产环境中遇到的数据。不幸的是,当你处于机器学习项目的早期阶段,且现实数据有限时,你必须决定如何最佳地使用这些数据,在这种情况下,最好将有限的数据用在验证数据集上,而不是测试数据集上。
标准化
数据准备的另一个重要步骤是标准化数据。在上一章中,对于 MNIST 数据,所有像素值都被除以 255,使得输入数据在 0.0 到 1.0 之间。在我们的案例中,我们应用了最小-最大归一化,它使用以下函数线性地转换数据:
xnew = (x - min(x)) / (max(x) - min(x))
由于我们已经知道 min(x) = 0 和 max(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 数据集的例子,因此我们不再重复这些代码。它已包含在代码文件中,你也可以参考第五章中的图像,《使用卷积神经网络进行图像分类》:

图 6.3:MNIST 数据集中的前 9 张图像
我们将数据增强描述为从现有数据集中创建新数据。这意味着创建一个与原始实例有足够差异的新实例,但又不会如此不同以至于它不再代表数据标签。对于图像数据,这可能意味着对图像执行以下操作:
-
缩放:通过放大图像的中心,模型可能更好地处理不同尺度的图像。
-
平移:将图像向上、下、左或右移动,可以让深度学习模型更好地识别偏离中心的图像示例。
-
旋转:通过旋转图像,模型将能够识别偏离中心的数据。
-
翻转:对于许多物体,图像翻转 90 度是有效的。例如,从左侧拍摄的汽车照片可以翻转,呈现出右侧的汽车图像。深度模型可以利用这一新视角。
-
添加噪声:有时候,故意向图像中添加噪声可以迫使深度学习模型发现更深层次的意义。
-
修改颜色:通过向图像添加滤镜,你可以模拟不同的光照条件。例如,你可以将一张在强光下拍摄的图像更改为看起来像是在光线不足的情况下拍摄的图像。
这个任务的目标是提高测试数据集的准确性。然而,数据增强的重要规则是,新数据应该尽力模拟模型在生产环境中使用的数据,而不是试图提高现有数据上的模型准确性。我无法强调这一点的重要性。如果模型在生产环境中无法正常工作,而用于训练和评估模型的数据并不代表现实生活中的数据,那么在留出的数据集上获得 99%的准确率是毫无意义的。在我们的例子中,我们可以看到 MNIST 图像是灰度图且整齐居中的,等等。在生产环境中,图像通常是偏离中心的,并且背景和前景各异(例如,带有棕色背景和蓝色文字),因此无法正确分类。你可以尝试对图像进行预处理,以便将其格式化为类似的方式(28 x 28 灰度图,黑色背景,数据居中并有 2 x 2 的边距),但更好的解决方案是训练模型以应对其将在生产环境中遇到的典型数据。
如果我们查看前面的图像,可以发现大多数数据增强任务并不适用于 MNIST 数据。所有图像似乎已经处于相同的缩放级别,因此创建放大版的人工图像不会有所帮助。同样,平移也不太可能有效,因为图像已经是居中的。翻转图像肯定无效,因为许多数字翻转后并不有效,例如7。我们的数据中没有现有的随机噪声,因此这一方法也行不通。
我们可以尝试的一种技术是旋转图像。我们将为每个现有图像创建两个新的人工图像,第一个人工图像将向左旋转 15 度,第二个人工图像将向右旋转 15 度。以下是我们将原始图像向左旋转 15 度后的部分人工图像:

图 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.csv和test0.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.9885714到0.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,并且在 0 到 0.50 范围内的隐藏神经元,具有从接近 0.50 的值过度抽样的趋势:

图 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:

图 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")
这将生成如下图表,展示对模型预测结果影响最大的特征:

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

图 6.8:使用 LIME 的特征热图
本示例展示了如何将 LIME 应用于一个已经使用 MXNet 训练的深度学习模型,以可视化哪些特征对模型的一些预测结果最为重要。从图 6.7 和图 6.8 中可以看出,单个特征几乎完全负责预测y变量,这表明存在数据分布不同和/或数据泄漏的问题。实际上,这样的变量应当从模型中排除。
做个对比,如果我们在没有这个字段的情况下训练一个模型,再次绘制特征重要性图,我们会看到没有单一特征占主导地位:

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

图 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 | 句子 |
|---|---|
| 3 | mcgrath 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。”King和Queen的词向量在数据中从周围的词汇中学习到了一些潜在的性别成分。但需要意识到的是,深度学习算法并不理解性别是什么,或者它适用于什么样的实体。即便如此,词向量仍然比词袋方法有了很大的改进,因为词袋方法无法识别不同标记之间的关系。使用词向量还意味着我们不必丢弃稀疏词条。最后,随着唯一标记数量的增加,处理它们比词袋方法更加高效。
我们将在第九章《异常检测与推荐系统》中再次讨论嵌入,当我们在自编码器中使用它们时。现在,我们已经了解了一些传统机器学习和深度学习方法来解决这个问题,接下来是时候更详细地比较它们了。
比较传统文本分类和深度学习
传统的文本分类执行了多个预处理步骤,包括词干提取、停用词处理和特征生成(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中的一些代码,稍作修改,然后查看dtm和dtm2变量来验证这一点:
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 | 坏 | 好 | 是 | 电影 | 不 | 这 | 非常 |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 1 | 1 | 1 | 0 | 1 | 1 |
| 2 | 0 | 1 | 1 | 1 | 1 | 1 | 0 |
| 3 | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
| 4 | 1 | 0 | 1 | 1 | 1 | 1 | 0 |
在这个简单的例子中,我们可以看到词袋方法的一些问题,我们丢失了否定词(not)与形容词(good,bad)之间的关系。为了解决这个问题,传统 NLP 方法可能会使用二元词组(bigrams),也就是说,不使用单一的单词作为标记,而是使用两个单词作为标记。现在,在第二个例子中,not good将作为一个标记,这样机器学习算法更有可能识别它。然而,第三个例子(not very good)仍然存在问题,因为我们会得到not very和very 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)是用来解决这个问题的深度学习层。它们是带有反馈回路的网络,允许信息流动,因此能够记住重要特征:

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

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

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

图 8.3: TensorBoard – 可视化模型层
- 如果你点击图形选项,它将显示模型的计算图。你也可以将其下载为图像文件。以下是该模型的计算图:

图 8.4: TensorBoard – 计算图
其中一些部分应该是你已经熟悉的。我们可以看到卷积层、最大池化层、扁平化层、全连接层和 Dropout 层。其他部分不那么明显。作为一种更高级的抽象,Keras 处理了创建计算图时的许多复杂性。
- 点击直方图选项,你可以看到张量的分布随时间变化的情况:

图 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文件夹中:
- 首先,我们为输入值x和输出值y创建一些虚拟数据。我们将y设为大约等于
0.8 + x * 1.3。我们希望应用程序发现beta0和beta1的值,分别为0.8和1.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
- 现在,我们设置
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)
- 然后,我们设置 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"
我们可以看到,模型成功找到了beta0和beta1的值,这些值解出了函数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 估算器的使用。
- 我们只包含特定于 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
- 模型训练完成后,以下代码将绘制训练和验证的指标:
plot(tr)
- 这将生成以下图表:

图 8.6:训练 TensorFlow 估算器模型的损失图
- 代码的下一部分调用
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 图表:

图 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文件夹中。
对于每次训练运行,一个新的网站将弹出,显示该运行的详细信息,如下图所示:

图 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))
这将弹出类似以下内容的页面:

图 8.9:比较两个 TensorFlow 运行
此页面展示了两个训练运行的评估指标,并显示了所使用的超参数。如我们所见,这使得调整深度学习模型的过程更加容易。超参数调整的方法具有自动日志记录、可追溯性,并且可以轻松比较不同的超参数设置。你可以看到训练运行的指标和使用的不同超参数。再也不需要比较配置文件来尝试匹配超参数设置和输出日志了!相比之下,我为第七章《使用深度学习的自然语言处理》中的 NLP 示例所写的超参数选择代码,在此看起来显得粗糙。
总结
在这一章中,我们开发了一些 TensorFlow 模型。我们看了 TensorBoard,它是一个非常好的工具,用于可视化和调试深度学习模型。我们使用 TensorFlow 构建了几个模型,包括一个基本的回归模型和一个用于计算机视觉的 Lenet 模型。通过这些示例,我们看到了使用 TensorFlow 编程比使用本书中其他地方的高级 API(如 MXNet 和 Keras)更复杂且容易出错。
接下来,我们开始使用 TensorFlow 估算器,这比直接使用 TensorFlow 界面更简单。然后我们在另一个名为tfruns的包中使用了该脚本,tfruns 代表 TensorFlow 运行。这个包允许我们每次调用 TensorFlow 估算器或 Keras 脚本时使用不同的标志。我们用它来进行超参数选择、运行和评估多个模型。TensorFlow 运行与 RStudio 有出色的集成,我们能够查看每次运行的摘要,并比较不同的运行,查看使用的指标和超参数的差异。
在下一章,我们将讨论嵌入和自编码器。我们已经在第七章《使用深度学习的自然语言处理》中看过嵌入,因此在下一章我们将看到嵌入如何创建数据的低层次编码。我们还将使用训练自编码器,这些自编码器会创建这些嵌入。我们将使用自编码器进行异常检测,并且还会用于协同过滤(推荐系统)。
第九章:异常检测和推荐系统
本章将探讨自编码器模型和推荐系统。尽管这两个应用场景看起来非常不同,但它们都依赖于找到数据的不同表示。这些表示类似于我们在第七章《使用深度学习的自然语言处理》中看到的嵌入。本文的第一部分介绍了无监督学习,在这种学习中没有特定的预测结果。接下来的部分提供了关于自编码器模型的概念性概述,特别是在机器学习和深度神经网络的背景下。我们将向您展示如何构建并应用自编码器模型来识别异常数据。这些非典型数据可能是坏数据或离群值,但也可能是需要进一步调查的实例,例如欺诈检测。应用异常检测的一个例子是,当个人的信用卡消费模式与他们的常规行为不同的时候。最后,本章以一个用例结束,讲解如何使用在第四章《训练深度预测模型》中介绍的零售数据集来应用推荐系统进行交叉销售和追加销售。
本章将涵盖以下主题:
-
什么是无监督学习?
-
自编码器是如何工作的?
-
在 R 中训练自编码器
-
使用自编码器进行异常检测
-
用例——协同过滤
什么是无监督学习?
到目前为止,我们关注的是广义上属于监督学习类别的模型和技术。监督学习之所以叫做监督,是因为任务是让机器学习一组变量或特征与一个或多个结果之间的关系。例如,在第四章《训练深度预测模型》中,我们希望预测某人是否会在接下来的 14 天内访问商店。在本章中,我们将深入探讨无监督学习的方法。与监督学习不同,监督学习需要有结果变量或标签数据,而无监督学习则不使用任何结果或标签数据。无监督学习仅使用输入特征进行学习。无监督学习的一个常见示例是聚类分析,例如 k 均值聚类,其中机器学习数据中的隐藏或潜在聚类,以最小化一个标准(例如,聚类内的最小方差)。
另一种无监督学习方法是寻找数据的另一种表示方式,或者将输入数据缩减为一个更小的数据集,同时在过程中尽量不丢失过多信息,这就是所谓的降维。降维的目标是通过一组p特征找到一组潜在变量k,使得k < p。然而,使用k个潜在变量时,可以合理地重建p个原始变量。我们在第二章的神经网络示例中使用了主成分分析(PCA),训练预测模型。在这个例子中,我们看到维度数量与信息损失之间存在一个权衡,如图 2.1所示。主成分分析使用正交变换将原始数据转换为主成分。除了不相关外,主成分按从解释方差最多的成分到解释方差最少的成分的顺序排列。尽管可以使用所有主成分(这样数据的维度就不会减少),但只有解释了足够多方差的成分(例如,基于高特征值)才会被保留,而解释相对较少方差的成分则会被视为噪声或不必要的部分。在第二章的神经网络示例中,训练预测模型,我们在去除方差为零的特征后,得到了 624 个输入。当我们应用 PCA 时,我们发现数据的 50%的方差(信息)仅能通过 23 个主成分来表示。
自动编码器是如何工作的?
自动编码器是一种降维技术。当它们以这种方式使用时,数学上和概念上与其他降维技术(如 PCA)有相似之处。自动编码器由两部分组成:编码器,它创建数据的表示;解码器,它试图重建或预测输入。因此,隐藏层和神经元不是输入和其他结果之间的映射,而是自编码(auto)过程。考虑到足够的复杂性,自动编码器可以简单地学习身份函数,隐藏神经元将完全复制原始数据,结果是没有任何有意义的收益。类似地,在 PCA 中,使用所有主成分也不会带来任何好处。因此,最好的自动编码器不一定是最准确的,而是能够揭示数据中某种有意义结构或架构的,或者能够减少噪声、识别异常值或异常数据,或者带来其他有用的副作用,这些副作用不一定直接与模型输入的准确预测相关。
低维度自编码器称为 欠完备;通过使用欠完备自编码器,可以迫使自编码器学习数据中最重要的特征。自编码器的一个常见应用是对深度神经网络或其他监督学习模型进行预训练。此外,也可以使用隐藏特征本身。我们稍后会看到它在异常检测中的应用。使用欠完备模型实际上是一种正则化模型的方法。然而,也可以训练过完备自编码器,其中隐藏层的维度大于原始数据,只要使用了其他形式的正则化。
自编码器大致有两个部分:
-
首先,编码函数 f() 将原始数据 x 编码到隐藏神经元 H 中
-
其次,解码函数 g() 将 H 解码回 x
下图展示了一个欠完备编码器,其中隐藏层的节点较少。右侧的输出层是左侧输入层的解码版本。隐藏层的任务是尽可能多地存储输入层的信息(编码输入层),以便输入层可以被重建(或解码):

图 9.1:自编码器的示例
正则化自编码器
欠完备自编码器是一种正则化自编码器,其正则化通过使用比数据更浅(或以其他方式更低)维度的表示来实现。然而,正则化也可以通过其他方式实现。这些就是惩罚自编码器。
惩罚自编码器
正如我们在前几章中看到的,防止过拟合的一种方法是使用惩罚,即正则化。通常,我们的目标是最小化重建误差。如果我们有一个目标函数 F,我们可以优化 F(y, f(x)),其中 f() 对原始数据输入进行编码,以生成预测或期望的 y 值。对于自编码器,我们有 F(x, g(f(x))),因此机器学习 f() 和 g() 的权重及功能形式,以最小化 x 和 x 的重建值 g(f(x)) 之间的差异。如果我们想使用过完备自编码器,我们需要引入某种形式的正则化,迫使机器学习到一种表示,而不仅仅是复制输入。例如,我们可以添加一个基于复杂度的惩罚函数,这样我们就可以优化 F(x, g(f(x))) + P(f(x)),其中惩罚函数 P 取决于编码或原始输入 f()。
这种惩罚方法与我们之前看到的有所不同,因为它的设计目的是促使潜变量 H 的稀疏性,而不是参数的稀疏性,H 是原始数据的编码表示。目标是学习一个捕捉数据本质特征的潜在表示。
另一种可以用于提供正则化的惩罚是基于导数的惩罚。稀疏自编码器有一种惩罚方法,可以促使潜变量的稀疏性,而对导数的惩罚则使得模型学习到一种对原始输入数据 x 的微小扰动相对不敏感的 f() 形式。我们所指的意思是,它对那些在 x 变化时编码变化很大的函数施加惩罚,偏好那些梯度相对平坦的区域。
去噪自编码器
去噪自编码器能够去除噪声或还原数据,是学习原始数据潜在表示的有用技术 (Vincent, P., Larochelle, H., Bengio, Y., 和 Manzagol, P. A. (2008 年 7 月); Bengio, Y., Courville, A., 和 Vincent, P. (2013 年))。我们说过,自编码器的常见任务是优化:F(x, g(f(x)))。然而,对于去噪自编码器来说,任务是从噪声或受损的 x 版本中恢复 x。去噪自编码器的一个应用是恢复可能已经模糊或损坏的旧图像。
尽管去噪自编码器用于从受损数据或带噪声的数据中尝试恢复真实的表示,但这一技术也可以作为正则化工具使用。作为一种正则化方法,不是处理带噪声或受损的数据并试图恢复真实数据,而是故意让原始数据受到损坏。这迫使自编码器不仅仅学习身份函数,因为原始输入不再与输出完全相同。这个过程如下图所示:

图 9.2:去噪自编码器
剩下的选择是 N() 函数,它负责添加噪声或损坏 x,应该是什么样的。两种选择是通过随机过程添加噪声,或在每次训练迭代中仅包括原始 x 输入的一个子集。在下一节中,我们将探讨如何在 R 中实际训练自编码器模型。
在 R 中训练自编码器
在本节中,我们将展示如何在 R 中训练自编码器,并向你展示它如何作为降维技术使用。我们将它与在第二章《训练预测模型》中采用的方法进行比较,在那一章中,我们使用 PCA 找到图像数据中的主成分。在那个例子中,我们使用 PCA 发现 23 个因子足以解释数据中 50% 的方差。我们使用这 23 个因子构建了一个神经网络模型来分类包含 5 或 6 的数据集,并且在该例子中,我们的准确率为 97.86%。
在本示例中,我们将遵循类似的过程,并再次使用MINST数据集。以下来自Chapter8/encoder.R的代码加载数据。我们将使用一半的数据来训练自编码器,另一半将用于构建分类模型,以评估自编码器在降维方面的效果。代码的第一部分与之前的示例相似;它加载并归一化数据,使得数值介于 0.0 和 1.0 之间:
library(keras)
library(corrplot)
library(neuralnet)
options(width = 70, digits = 2)
options(scipen=999)
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=""))
}
data <- read.csv("../data/train.csv", header=TRUE)
set.seed(42)
sample<-sample(nrow(data),0.5*nrow(data))
test <- setdiff(seq_len(nrow(data)),sample)
train.x <- data[sample,-1]
test.x <- data[test,-1]
train.y <- data[sample,1]
test.y <- data[test,1]
rm(data)
train.x <- train.x/255
test.x <- test.x/255
train.x <- data.matrix(train.x)
test.x <- data.matrix(test.x)
input_dim <- 28*28 #784
现在,我们将进入我们的第一个自编码器。我们将在自编码器中使用16个隐藏神经元,并使用 tanh 作为激活函数。我们使用 20%的数据作为验证集,以便提供自编码器表现的无偏估计。以下是代码。为了简洁起见,我们只显示部分输出:
# model 1
inner_layer_dim <- 16
input_layer <- layer_input(shape=c(input_dim))
encoder <- layer_dense(units=inner_layer_dim, activation='tanh')(input_layer)
decoder <- layer_dense(units=784)(encoder)
autoencoder <- keras_model(inputs=input_layer, outputs = decoder)
autoencoder %>% compile(optimizer='adam', loss='mean_squared_error',metrics='accuracy')
history <- autoencoder %>% fit(train.x,train.x,
epochs=40, batch_size=128,validation_split=0.2)
Train on 16800 samples, validate on 4200 samples
Epoch 1/40
16800/16800 [==============================] - 1s 36us/step - loss: 0.0683 - acc: 0.0065 - val_loss: 0.0536 - val_acc: 0.0052
Epoch 2/40
16800/16800 [==============================] - 1s 30us/step - loss: 0.0457 - acc: 0.0082 - val_loss: 0.0400 - val_acc: 0.0081
Epoch 3/40
16800/16800 [==============================] - 0s 29us/step - loss: 0.0367 - acc: 0.0101 - val_loss: 0.0344 - val_acc: 0.0121
...
...
Epoch 38/40
16800/16800 [==============================] - 0s 29us/step - loss: 0.0274 - acc: 0.0107 - val_loss: 0.0275 - val_acc: 0.0098
Epoch 39/40
16800/16800 [==============================] - 1s 31us/step - loss: 0.0274 - acc: 0.0111 - val_loss: 0.0275 - val_acc: 0.0093
Epoch 40/40
16800/16800 [==============================] - 1s 32us/step - loss: 0.0274 - acc: 0.0120 - val_loss: 0.0275 - val_acc: 0.0095
验证损失为0.0275,这表明模型表现相当好。另一个不错的特点是,如果你在 RStudio 中运行代码,它会在图形中显示训练指标,并在模型训练过程中自动更新。这在以下截图中有所展示:

图 9.3:RStudio 中查看器面板显示的模型指标
一旦模型完成训练,你还可以使用以下代码绘制模型架构和模型指标(输出也包含在内)。通过调用 plot 函数,你可以查看训练集和验证集上的准确率和损失图表:
summary(autoencoder)
______________________________________________________________________
Layer (type) Output Shape Param #
======================================================================
input_1 (InputLayer) (None, 784) 0
______________________________________________________________________
dense_1 (Dense) (None, 16) 12560
______________________________________________________________________
dense_2 (Dense) (None, 784) 13328
======================================================================
Total params: 25,888
Trainable params: 25,888
Non-trainable params: 0
______________________________________________________________________
plot(history)
这段代码生成了以下图表:

图 9.4:自编码器模型指标
上述图表显示,验证准确率相对稳定,但它可能在第 20 个周期后已经达到峰值。现在,我们将用32个隐藏节点训练第二个模型,代码如下:
# model 2
inner_layer_dim <- 32
input_layer <- layer_input(shape=c(input_dim))
encoder <- layer_dense(units=inner_layer_dim, activation='tanh')(input_layer)
decoder <- layer_dense(units=784)(encoder)
autoencoder <- keras_model(inputs=input_layer, outputs = decoder)
autoencoder %>% compile(optimizer='adam',
loss='mean_squared_error',metrics='accuracy')
history <- autoencoder %>% fit(train.x,train.x,
epochs=40, batch_size=128,validation_split=0.2)
Train on 16800 samples, validate on 4200 samples
Epoch 1/40
16800/16800 [==============================] - 1s 41us/step - loss: 0.0591 - acc: 0.0104 - val_loss: 0.0406 - val_acc: 0.0131
Epoch 2/40
16800/16800 [==============================] - 1s 34us/step - loss: 0.0339 - acc: 0.0111 - val_loss: 0.0291 - val_acc: 0.0093
Epoch 3/40
16800/16800 [==============================] - 1s 33us/step - loss: 0.0262 - acc: 0.0108 - val_loss: 0.0239 - val_acc: 0.0100
...
...
Epoch 38/40
16800/16800 [==============================] - 1s 33us/step - loss: 0.0174 - acc: 0.0130 - val_loss: 0.0175 - val_acc: 0.0095
Epoch 39/40
16800/16800 [==============================] - 1s 31us/step - loss: 0.0174 - acc: 0.0132 - val_loss: 0.0175 - val_acc: 0.0098
Epoch 40/40
16800/16800 [==============================] - 1s 34us/step - loss: 0.0174 - acc: 0.0126 - val_loss: 0.0175 - val_acc: 0.0100
我们的验证损失已经改善至0.0175,接下来我们试试64个隐藏节点:
# model 3
inner_layer_dim <- 64
input_layer <- layer_input(shape=c(input_dim))
encoder <- layer_dense(units=inner_layer_dim, activation='tanh')(input_layer)
decoder <- layer_dense(units=784)(encoder)
autoencoder <- keras_model(inputs=input_layer, outputs = decoder)
autoencoder %>% compile(optimizer='adam',
loss='mean_squared_error',metrics='accuracy')
history <- autoencoder %>% fit(train.x,train.x,
epochs=40, batch_size=128,validation_split=0.2)
Train on 16800 samples, validate on 4200 samples
Epoch 1/40
16800/16800 [==============================] - 1s 50us/step - loss: 0.0505 - acc: 0.0085 - val_loss: 0.0300 - val_acc: 0.0138
Epoch 2/40
16800/16800 [==============================] - 1s 39us/step - loss: 0.0239 - acc: 0.0110 - val_loss: 0.0197 - val_acc: 0.0090
Epoch 3/40
16800/16800 [==============================] - 1s 41us/step - loss: 0.0173 - acc: 0.0115 - val_loss: 0.0156 - val_acc: 0.0117
...
...
Epoch 38/40
16800/16800 [==============================] - 1s 41us/step - loss: 0.0094 - acc: 0.0124 - val_loss: 0.0096 - val_acc: 0.0131
Epoch 39/40
16800/16800 [==============================] - 1s 39us/step - loss: 0.0095 - acc: 0.0128 - val_loss: 0.0095 - val_acc: 0.0121
Epoch 40/40
16800/16800 [==============================] - 1s 37us/step - loss: 0.0094 - acc: 0.0126 - val_loss: 0.0098 - val_acc: 0.0133
我们的验证损失为0.0098,这再次表明有所改进。我们可能已经到达了一个阶段,在这个阶段增加更多隐藏节点会导致模型过拟合,因为我们只使用了16800行数据来训练自编码器。我们可以考虑应用正则化,但由于我们的第一个模型准确率为0.01,所以表现已经足够好。
访问自编码器模型的特征
我们可以从模型中提取深度特征,也就是模型中隐藏神经元的值。为此,我们将使用具有 16 个隐藏节点的模型。我们将使用ggplot2包检查相关性的分布,如以下代码所示。结果显示在图 9.5中。深度特征之间的相关性较小,也就是说,通常其绝对值小于<.20。这是我们期望的情况,以确保自编码器正常工作。这意味着特征之间不应重复信息:
encoder <- keras_model(inputs=input_layer, outputs=encoder)
encodings <- encoder %>% predict(test.x)
encodings<-as.data.frame(encodings)
M <- cor(encodings)
corrplot(M, method = "circle", sig.level = 0.1)
上述代码生成了以下图表:

图 9.5:自编码器隐藏层权重之间的相关性
在第二章《训练预测模型》中,我们使用 PCA 进行降维,发现即使仅使用 23 个特征作为输入,二分类任务中区分 5 和 6 的准确率仍然可以达到 97.86%。这 23 个特征是主成分,并且它们占据了数据集中 50%的方差。我们将使用自编码器中的权重来执行相同的实验。请注意,我们在 50%的数据上训练了自编码器,并且使用剩下的 50%数据进行二分类任务,即我们不想尝试在用于构建自编码器的数据上进行分类任务:
encodings$y <- test.y
encodings <- encodings[encodings$y==5 | encodings$y==6,]
encodings[encodings$y==5,]$y <- 0
encodings[encodings$y==6,]$y <- 1
table(encodings$y)
0 1
1852 2075
nobs <- nrow(encodings)
train <- sample(nobs, 0.9*nobs)
test <- setdiff(seq_len(nobs), train)
trainData <- encodings[train,]
testData <- encodings[test,]
col_names <- names(trainData)
f <- as.formula(paste("y ~", paste(col_names[!col_names %in%"y"],collapse="+")))
nn <- neuralnet(f,data=trainData,hidden=c(4,2),linear.output = FALSE)
preds_nn <- compute(nn,testData[,1:(-1+ncol(testData))])
preds_nn <- ifelse(preds_nn$net.result > 0.5, "1", "0")
t<-table(testData$y, preds_nn,dnn=c("Actual", "Predicted"))
acc<-round(100.0*sum(diag(t))/sum(t),2)
print(t)
Predicted
Actual 0 1
0 182 5
1 3 203
print(sprintf(" accuracy = %1.2f%%",acc))
[1] " accuracy = 97.96%"
我们的模型达到了97.96%的准确率,略高于在第二章《训练预测模型》中获得的97.86%的准确率。事实上,这两个模型非常相似并不令人惊讶,因为 PCA 的数学基础涉及矩阵分解,而自编码器则使用反向传播来设置隐藏层的矩阵权重。实际上,如果我们去掉非线性激活函数,我们的编码结果将非常类似于 PCA。这表明,自编码器模型可以有效地用作降维技术。
使用自编码器进行异常检测
既然我们已经构建了自编码器并访问了内部层的特征,接下来我们将介绍自编码器如何用于异常检测的示例。这里的前提非常简单:我们从解码器中获取重构输出,并查看哪些实例的误差最大,也就是说,哪些实例是解码器最难重构的。这里使用的代码位于Chapter9/anomaly.R,并且我们将使用已经在第二章《训练预测模型》中介绍过的UCI HAR数据集。如果你还没有下载数据,可以回到该章节查看如何下载数据。代码的第一部分加载了数据,我们对子集特征进行了筛选,仅使用了名称中包含均值、标准差和偏度的特征:
library(keras)
library(ggplot2)
train.x <- read.table("UCI HAR Dataset/train/X_train.txt")
train.y <- read.table("UCI HAR Dataset/train/y_train.txt")[[1]]
test.x <- read.table("UCI HAR Dataset/test/X_test.txt")
test.y <- read.table("UCI HAR Dataset/test/y_test.txt")[[1]]
use.labels <- read.table("UCI HAR Dataset/activity_labels.txt")
colnames(use.labels) <-c("y","label")
features <- read.table("UCI HAR Dataset/features.txt")
meanSD <- grep("mean\\(\\)|std\\(\\)|max\\(\\)|min\\(\\)|skewness\\(\\)", features[, 2])
train.x <- data.matrix(train.x[,meanSD])
test.x <- data.matrix(test.x[,meanSD])
input_dim <- ncol(train.x)
现在,我们可以构建我们的自编码器模型。这个模型将是一个堆叠自编码器,包含两个40个神经元的隐藏编码器层和两个 40 个神经元的隐藏解码器层。为了简洁起见,我们省略了部分输出:
# model
inner_layer_dim <- 40
input_layer <- layer_input(shape=c(input_dim))
encoder <- layer_dense(units=inner_layer_dim, activation='tanh')(input_layer)
encoder <- layer_dense(units=inner_layer_dim, activation='tanh')(encoder)
decoder <- layer_dense(units=inner_layer_dim)(encoder)
decoder <- layer_dense(units=inner_layer_dim)(decoder)
decoder <- layer_dense(units=input_dim)(decoder)
autoencoder <- keras_model(inputs=input_layer, outputs = decoder)
autoencoder %>% compile(optimizer='adam',
loss='mean_squared_error',metrics='accuracy')
history <- autoencoder %>% fit(train.x,train.x,
epochs=30, batch_size=128,validation_split=0.2)
Train on 5881 samples, validate on 1471 samples
Epoch 1/30
5881/5881 [==============================] - 1s 95us/step - loss: 0.2342 - acc: 0.1047 - val_loss: 0.0500 - val_acc: 0.1013
Epoch 2/30
5881/5881 [==============================] - 0s 53us/step - loss: 0.0447 - acc: 0.2151 - val_loss: 0.0324 - val_acc: 0.2536
Epoch 3/30
5881/5881 [==============================] - 0s 44us/step - loss: 0.0324 - acc: 0.2772 - val_loss: 0.0261 - val_acc: 0.3413
...
...
Epoch 27/30
5881/5881 [==============================] - 0s 45us/step - loss: 0.0098 - acc: 0.2935 - val_loss: 0.0094 - val_acc: 0.3379
Epoch 28/30
5881/5881 [==============================] - 0s 44us/step - loss: 0.0096 - acc: 0.2908 - val_loss: 0.0092 - val_acc: 0.3215
Epoch 29/30
5881/5881 [==============================] - 0s 44us/step - loss: 0.0094 - acc: 0.2984 - val_loss: 0.0090 - val_acc: 0.3209
Epoch 30/30
5881/5881 [==============================] - 0s 44us/step - loss: 0.0092 - acc: 0.2955 - val_loss: 0.0088 - val_acc: 0.3209
我们可以通过调用 summary 函数来查看模型的层次结构和参数数量,如下所示:
summary(autoencoder)
_______________________________________________________________________
Layer (type) Output Shape Param #
=======================================================================
input_4 (InputLayer) (None, 145) 0
_______________________________________________________________________
dense_16 (Dense) (None, 40) 5840
_______________________________________________________________________
dense_17 (Dense) (None, 40) 1640
_______________________________________________________________________
dense_18 (Dense) (None, 40) 1640
_______________________________________________________________________
dense_19 (Dense) (None, 40) 1640
_______________________________________________________________________
dense_20 (Dense) (None, 145) 5945
=======================================================================
Total params: 16,705
Trainable params: 16,705
Non-trainable params: 0
_______________________________________________________________________
我们的验证损失是0.0088,这意味着我们的模型在编码数据方面表现良好。现在,我们将使用测试集对自编码器进行测试,并获得重构的数据。这将创建一个与测试集大小相同的数据集。然后,我们将选择任何预测值与测试集之间的平方误差(se)和大于 4 的实例。
这些是自编码器在重构时遇到最多困难的实例,因此它们是潜在异常。4 的限制值是一个超参数;如果设置得更高,则检测到的潜在异常较少;如果设置得更低,则检测到的潜在异常较多。这个值会根据所使用的数据集而有所不同。
该数据集包含 6 个类别。我们想分析异常是否分布在所有类别中,还是仅限于某些类别。我们将打印出测试集中各类别的频率表,并且可以看到各类别的分布相对均匀。当打印出潜在异常类别的频率表时,我们可以看到大多数异常集中在WALKING_DOWNSTAIRS类别中。潜在异常如图 9.6 所示:
# anomaly detection
preds <- autoencoder %>% predict(test.x)
preds <- as.data.frame(preds)
limit <- 4
preds$se_test <- apply((test.x - preds)², 1, sum)
preds$y_preds <- ifelse(preds$se_test>limit,1,0)
preds$y <- test.y
preds <- merge(preds,use.labels)
table(preds$label)
LAYING SITTING STANDING WALKING WALKING_DOWNSTAIRS WALKING_UPSTAIRS
537 491 532 496 420 471
table(preds[preds$y_preds==1,]$label)
LAYING SITTING STANDING WALKING WALKING_DOWNSTAIRS WALKING_UPSTAIRS
18 7 1 17 45 11
我们可以使用以下代码进行绘制:
ggplot(as.data.frame(table(preds[preds$y_preds==1,]$label)),aes(Var1, Freq)) +
ggtitle("Potential anomalies by activity") +
geom_bar(stat = "identity") +
xlab("") + ylab("Frequency") +
theme_classic() +
theme(plot.title = element_text(hjust = 0.5)) +
theme(axis.text.x = element_text(angle = 45, hjust = 1, vjust = 1))

图 9.6:异常的分布
在这个示例中,我们使用了深度自编码器模型来学习来自智能手机的动作数据特征。这样的工作对于排除未知或不寻常的活动非常有用,而不是错误地将它们分类。例如,在一个应用程序中,分类你进行了什么活动以及持续了多少分钟,可能更好的是直接忽略模型不确定的几分钟,或者隐藏特征没有充分重构输入的情况,而不是错误地将活动分类为走路或坐着,而实际上是下楼走路。
这样的工作也有助于识别模型可能存在问题的地方。或许需要额外的传感器和数据来表示下楼走路,或者需要更多工作来理解为什么下楼走路会产生相对较高的错误率。
这些深度自编码器在其他需要识别异常的场景中也非常有用,例如金融数据或信用卡使用模式。异常的消费模式可能表明存在欺诈或信用卡被盗。与其尝试手动搜索数百万次信用卡交易,不如训练一个自编码器模型,并用它来识别异常以进行进一步调查。
用例 – 协同过滤
本用例是关于协同过滤的。我们将基于从深度学习模型创建的嵌入(embeddings)来构建一个推荐系统。为此,我们将使用在第四章中使用的相同数据集,训练深度预测模型,即零售交易数据库。如果你还没有下载数据库,可以访问以下链接,www.dunnhumby.com/sourcefiles,并选择Let’s Get Sort-of-Real。选择名为为 5,000 名顾客随机抽样的所有交易的最小数据集选项。在阅读了条款并下载了数据集后,将其解压到代码文件夹下名为 dunnhumby/in 的目录中。确保文件直接解压到该文件夹下,而不是子目录,因为解压后你可能需要复制它们。
数据包含通过购物篮 ID 关联的零售交易详情。每笔交易都有一个日期和商店代码,部分交易还与顾客关联。以下是我们将在本次分析中使用的字段:
| 字段名 | 描述 | 格式 |
|---|---|---|
CUST_CODE |
顾客代码。此字段将交易/访问与顾客关联。 | 字符 |
SPEND |
与所购商品相关的花费。 | 数值 |
PROD_CODE |
产品代码。 | 字符 |
PROD_CODE_10 |
产品层级 10 代码。 | 字符 |
PROD_CODE_20 |
产品层级 20 代码。 | 字符 |
PROD_CODE_30 |
产品层级 30 代码。 | 字符 |
PROD_CODE_40 |
产品层级 40 代码。 | 字符 |
如果你想了解更多关于文件结构的细节,可以回头重新阅读第四章中的用例,训练深度预测模型。我们将使用这个数据集来创建推荐引擎。这里有一类叫做市场购物篮分析的机器学习算法,可以与交易数据一起使用,但本用例基于协同过滤。协同过滤是一种基于人们对产品评分的推荐方法。它们通常用于音乐和电影推荐,用户会对物品进行评分,通常是 1 到 5 分。也许最著名的推荐系统是 Netflix,因为 Netflix 奖(en.wikipedia.org/wiki/Netflix_Prize)。
我们将使用我们的数据集来创建隐式排名,评估客户如何评价某个商品。如果你不熟悉隐式排名,它们是通过数据推导出的排名,而不是用户显式指定的排名。我们将使用一个产品代码,PROD_CODE_40,并计算该产品代码的消费分位数。分位数将把字段划分为大致相等的 5 个组。我们将用这些分位数为每个客户基于他们对该产品代码的消费额分配一个评分。排名前 20%的客户将获得评分 5,接下来的 20%将获得评分 4,以此类推。每个存在的客户/产品代码组合将会有一个从 1 到 5 的评分:
在零售忠诚度系统中使用分位数有着丰富的历史。零售忠诚度数据的早期细分方法之一被称为RFM 分析。RFM 是 Recency、Frequency 和 Monetary 支出的首字母缩写。它为每个客户在这些类别中打分,1 为最低,5 为最高,每个评分类别中包含相同数量的客户。对于Recency,最近访问的 20%客户会得到评分 5,接下来的 20%得到评分 4,以此类推。对于Frequency,交易最多的 20%客户会得到评分 5,接下来的 20%得到评分 4,以此类推。类似地,对于Monetary支出,根据收入排名前 20%的客户会得到评分 5,接下来的 20%得到评分 4,以此类推。最终将这些分数连接在一起,因此一个 RFM 为 453 的客户在 Recency 上是 4,Frequency 上是 5,Monetary 支出上是 3。一旦计算出评分,它可以用于很多目的,例如交叉销售、客户流失分析等。RFM 分析在 90 年代末和 2000 年代初非常流行,许多营销经理都喜欢使用它,因为它易于实施且理解容易。然而,它不够灵活,正在被机器学习技术所取代。
数据准备
创建评分的代码在Chapter9/create_recommend.R中。代码的第一部分处理原始交易数据。数据保存在不同的 CSV 文件中,所以它会处理每个文件,选择与客户关联的记录(即,CUST_CODE!=""),然后按CUST_CODE和PROD_CODE_40分组销售数据。接着将结果附加到临时文件中,然后继续处理下一个输入文件:
library(magrittr)
library(dplyr)
library(readr)
library(broom)
set.seed(42)
file_list <- list.files("../dunnhumby/in/", "trans*")
temp_file <- "../dunnhumby/temp.csv"
out_file <- "../dunnhumby/recommend.csv"
if (file.exists(temp_file)) file.remove(temp_file)
if (file.exists(out_file)) file.remove(out_file)
options(readr.show_progress=FALSE)
i <- 1
for (file_name in file_list)
{
file_name<-paste("../dunnhumby/in/",file_name,sep="")
df<-suppressMessages(read_csv(file_name))
df2 <- df %>%
filter(CUST_CODE!="") %>%
group_by(CUST_CODE,PROD_CODE_40) %>%
summarise(sales=sum(SPEND))
colnames(df2)<-c("cust_id","prod_id","sales")
if (i ==1)
write_csv(df2,temp_file)
else
write_csv(df2,temp_file,append=TRUE)
print (paste("File",i,"/",length(file_list),"processed"))
i <- i+1
}
[1] "File 1 / 117 processed"
[1] "File 2 / 117 processed"
[1] "File 3 / 117 processed"
...
...
...
[1] "File 115 / 117 processed"
[1] "File 116 / 117 processed"
[1] "File 117 / 117 processed"
rm(df,df2)
本节按客户和产品代码对117个输入文件进行分组。在处理每个文件时,我们将客户代码重命名为cust_id,产品部门代码重命名为prod_id。处理完成后,合并的文件显然会有重复的客户-产品代码组合;也就是说,我们需要再次对合并的数据进行分组。我们通过打开临时文件,再次对字段进行分组来实现这一点:
df_processed<-read_csv(temp_file)
if (file.exists(temp_file)) file.remove(temp_file)
df2 <- df_processed %>%
group_by(cust_id,prod_id) %>%
summarise(sales=sum(sales))
我们本可以尝试加载所有交易数据并对其进行分组,但那样会非常占用内存和计算资源。通过分两步进行处理,我们减少了每个阶段需要处理的数据量,这意味着在内存有限的机器上运行的可能性更大。
一旦我们获得了每个客户和产品部门代码组合的总支出,我们就可以创建评分。得益于优秀的tidyr包,只需要几行代码就能为每一行分配评分。首先,我们按照prod_id字段进行分组,并使用分位数函数返回每个产品代码的销售分位数。这些分位数将返回将客户分成5个等大小组的销售范围。然后,我们使用这些分位数来分配排名:
# create quantiles
dfProds <- df2 %>%
group_by(prod_id) %>%
do( tidy(t(quantile(.$sales, probs = seq(0, 1, 0.2)))) )
colnames(dfProds)<-c("prod_id","X0","X20","X40","X60","X80","X100")
df2<-merge(df2,dfProds)
df2$rating<-0
df2[df2$sales<=df2$X20,"rating"] <- 1
df2[(df2$sales>df2$X20) & (df2$sales<=df2$X40),"rating"] <- 2
df2[(df2$sales>df2$X40) & (df2$sales<=df2$X60),"rating"] <- 3
df2[(df2$sales>df2$X60) & (df2$sales<=df2$X80),"rating"] <- 4
df2[(df2$sales>df2$X80) & (df2$sales<=df2$X100),"rating"] <- 5
唯一剩下的就是保存结果。在我们执行之前,我们做了几个基本检查,确保我们的评分从 1 到 5 分布均匀。然后,我们随机选择一个产品代码,并检查该产品的评分是否从 1 到 5 均匀分布:
# sanity check, are our ratings spread out relatively evenly
df2 %>%
group_by(rating) %>%
summarise(recs=n())
rating recs
1 1 68246
2 2 62592
3 3 62162
4 4 63488
5 5 63682
df2 %>%
filter(prod_id==df2[sample(1:nrow(df2), 1),]$prod_id) %>%
group_by(prod_id,rating) %>%
summarise(recs=n())
prod_id rating recs
1 D00008 1 597
2 D00008 2 596
3 D00008 3 596
4 D00008 4 596
5 D00008 5 596
df2 <- df2[,c("cust_id","prod_id","rating")]
write_csv(df2,out_file)
这里一切看起来都很好:rating=1的数量为68246,而2到5的评分范围为62162到63682,但这并不是什么问题,因为协同过滤模型并不要求评分分布均匀。对于单个商品(D00008),每个评分的分布均匀,分别为596或597。
构建协同过滤模型
在我们开始应用深度学习模型之前,我们应该按照前几章的做法,使用标准的机器学习算法创建一个基准准确度评分。这很快,容易实现,并且能让我们确信深度学习模型的效果比单纯使用普通机器学习要好。以下是用 R 语言实现协同过滤的 20 行代码。这个代码可以在Chapter8/ml_recommend.R中找到:
library(readr)
library(recommenderlab)
library(reshape2)
set.seed(42)
in_file <- "../dunnhumby/recommend.csv"
df <- read_csv(in_file)
dfPivot <-dcast(df, cust_id ~ prod_id)
m <- as.matrix(dfPivot[,2:ncol(dfPivot)])
recommend <- as(m,"realRatingMatrix")
e <- evaluationScheme(recommend,method="split",
train=0.9,given=-1, goodRating=5)
e
Evaluation scheme using all-but-1 items
Method: ‘split’ with 1 run(s).
Training set proportion: 0.900
Good ratings: >=5.000000
Data set: 5000 x 9 rating matrix of class ‘realRatingMatrix’ with 25688 ratings.
r1 <- Recommender(getData(e,"train"),"UBCF")
r1
Recommender of type ‘UBCF’ for ‘realRatingMatrix’
learned using 4500 users.
p1 <- predict(r1,getData(e,"known"),type="ratings")
err1<-calcPredictionAccuracy(p1,getData(e,"unknown"))
print(sprintf(" User based collaborative filtering model MSE = %1.4f",err1[2]))
[1] " User based collaborative filtering model MSE = 0.9748"
这段代码创建了一个协同过滤模型,且该模型的均方误差(MSE)为0.9748。和之前一样,我们这样做是因为这个示例的大部分工作都集中在数据准备上,而非模型构建,因此,使用基础机器学习算法进行比较并不困难。这里的代码使用了标准的 R 库来创建推荐系统,正如你所见,这相对简单,因为数据已经处于预期格式。如果你想了解更多关于这个协同过滤算法的信息,可以搜索user based collaborative filtering in r,或者查看文档页面。
现在,让我们专注于创建深度学习模型。
构建深度学习协同过滤模型
在这里,我们将看看是否能构建一个深度学习模型来超越之前的方法!以下代码位于Chapter9/keras_recommend.R。第一部分加载数据集并为客户和产品代码创建新的 ID。这是因为 Keras 期望索引是顺序的,从零开始,并且唯一:
library(readr)
library(keras)
set.seed(42)
use_session_with_seed(42, disable_gpu = FALSE, disable_parallel_cpu = FALSE)
df<-read_csv("recommend.csv")
custs <- as.data.frame(unique(df$cust_id))
custs$cust_id2 <- as.numeric(row.names(custs))
colnames(custs) <- c("cust_id","cust_id2")
custs$cust_id2 <- custs$cust_id2 - 1
prods <- as.data.frame(unique(df$prod_id))
prods$prod_id2 <- as.numeric(row.names(prods))
colnames(prods) <- c("prod_id","prod_id2")
prods$prod_id2 <- prods$prod_id2 - 1
df<-merge(df,custs)
df<-merge(df,prods)
n_custs = length(unique(df$cust_id2))
n_prods = length(unique(df$prod_id2))
# shuffle the data
trainData <- df[sample(nrow(df)),]
我们有 5000 个独特的客户和 9 个独特的产品代码。这与大多数协同过滤的例子不同;通常情况下,产品的数量要远高于客户的数量。接下来的部分是创建模型。我们将为客户和产品创建嵌入层,然后计算这些嵌入层的点积。嵌入层是数据的低阶表示,正如我们之前看到的自动编码器中的编码器一样。我们还将为每个客户和产品设置一个偏置项——这对数据进行了一种归一化处理。如果某个产品非常流行,或者某个客户有很多高评分,这将对此进行补偿。我们将在嵌入层中使用 10 个因素来表示客户和产品。我们将在嵌入层中使用一些 L2 正则化,以防止过拟合。以下代码定义了模型架构:
n_factors<-10
# define the model
cust_in <- layer_input(shape = 1)
cust_embed <- layer_embedding(
input_dim = n_custs
,output_dim = n_factors
,input_length = 1
,embeddings_regularizer=regularizer_l2(0.0001)
,name = "cust_embed"
)(cust_in)
prod_in <- layer_input(shape = 1)
prod_embed <- layer_embedding(
input_dim = n_prods
,output_dim = n_factors
,input_length = 1
,embeddings_regularizer=regularizer_l2(0.0001)
,name = "prod_embed"
)(prod_in)
ub = layer_embedding(
input_dim = n_custs,
output_dim = 1,
input_length = 1,
name = "custb_embed"
)(cust_in)
ub_flat <- layer_flatten()(ub)
mb = layer_embedding(
input_dim = n_prods,
output_dim = 1,
input_length = 1,
name = "prodb_embed"
)(prod_in)
mb_flat <- layer_flatten()(mb)
cust_flat <- layer_flatten()(cust_embed)
prod_flat <- layer_flatten()(prod_embed)
x <- layer_dot(list(cust_flat, prod_flat), axes = 1)
x <- layer_add(list(x, ub_flat))
x <- layer_add(list(x, mb_flat))
现在,我们准备好构建模型了。我们将从数据中抽取 10% 用于验证:
model <- keras_model(list(cust_in, prod_in), x)
compile(model,optimizer="adam", loss='mse')
model.optimizer.lr=0.001
fit(model,list(trainData$cust_id2,trainData$prod_id2),trainData$rating,
batch_size=128,epochs=40,validation_split = 0.1 )
Train on 23119 samples, validate on 2569 samples
Epoch 1/40
23119/23119 [==============================] - 1s 31us/step - loss: 10.3551 - val_loss: 9.9817
Epoch 2/40
23119/23119 [==============================] - 0s 21us/step - loss: 8.6549 - val_loss: 7.7826
Epoch 3/40
23119/23119 [==============================] - 0s 20us/step - loss: 6.0651 - val_loss: 5.2164
...
...
...
Epoch 37/40
23119/23119 [==============================] - 0s 19us/step - loss: 0.6674 - val_loss: 0.9575
Epoch 38/40
23119/23119 [==============================] - 0s 18us/step - loss: 0.6486 - val_loss: 0.9555
Epoch 39/40
23119/23119 [==============================] - 0s 19us/step - loss: 0.6271 - val_loss: 0.9547
Epoch 40/40
23119/23119 [==============================] - 0s 20us/step - loss: 0.6023 - val_loss: 0.9508
我们的模型达到了 0.9508 的 MSE,这比我们在机器学习模型中得到的 0.9748 的 MSE 有了改进。我们的深度学习模型存在过拟合问题,但其中一个原因是因为我们的数据库相对较小。我尝试增加正则化,但这并没有改善模型。
将深度学习模型应用于商业问题
现在我们有了一个模型,我们如何使用它呢?协同过滤模型的最典型应用是向用户推荐他们还未评分的商品。这个概念在音乐和电影推荐等领域效果很好,协同过滤模型通常在这些领域中得到应用。然而,我们将把它用于不同的目的。市场营销经理关心的一个问题是他们从客户那里获得的钱包份额。这个定义(来自en.wikipedia.org/wiki/Share_of_wallet)是指客户对某产品的支出(‘钱包’的份额)中,分配给销售该产品的公司的百分比。它基本上衡量了一个客户在我们这里可能支出的百分比,从而评估该客户的价值。举个例子,我们可能有一些客户定期访问我们的商店并消费相当一部分金额。但他们是否从我们这里购买了所有商品?也许他们把新鲜食品从其他地方购买,也就是说,他们在其他商店购买肉类、水果、蔬菜等。我们可以使用协同过滤来找到那些模型预测他们在我们商店购买某些商品,但实际上他们并没有这样做的客户。记住,协同过滤是基于其他相似客户的行为来推荐的。因此,如果客户 A 在我们商店没有像其他相似客户那样购买肉类、水果、蔬菜等,我们可以通过向他们发送这些产品的优惠来尝试吸引他们在我们的商店消费更多。
我们将寻找预测值大于 4 但实际值小于 2 的客户-产品部门代码。这些客户应该按照模型在我们这里购买这些商品,因此通过向他们发送这些部门商品的优惠券,我们可以捕获更多的消费额。
协同过滤模型应该非常适合这种类型的分析。该算法的基础是根据相似客户的活动来推荐产品,因此它已经调整了消费规模。例如,如果对某个客户的预测是他们在新鲜水果和蔬菜上的消费应该为 5,那是基于与其他相似客户的比较。以下是评估代码,它也在Chapter8/kerarecommend.R中。代码的第一部分生成预测并将其链接回去。我们输出了一些指标,这些指标看起来很有说服力,但请注意,它们是在所有数据上运行的,包括模型训练时使用的数据,因此这些指标过于乐观。我们对预测进行了一些调整——有些值大于 5 或小于 1,因此我们将它们调整回有效值。这对我们的指标产生了非常小的改善:
##### model use-case, find products that customers 'should' be purchasing ######
df$preds<-predict(model,list(df$cust_id2,df$prod_id2))
# remove index variables, do not need them anymore
df$cust_id2 <- NULL
df$prod_id2 <- NULL
mse<-mean((df$rating-df$preds)²)
rmse<-sqrt(mse)
mae<-mean(abs(df$rating-df$preds))
print (sprintf("DL Collaborative filtering model: MSE=%1.3f, RMSE=%1.3f, MAE=%1.3f",mse,rmse,mae))
[1] "DL Collaborative filtering model: MSE=0.478, RMSE=0.691, MAE=0.501"
df <- df[order(-df$preds),]
head(df)
prod_id cust_id rating preds
10017 D00003 CUST0000283274 5 5.519783
4490 D00002 CUST0000283274 5 5.476133
9060 D00002 CUST0000084449 5 5.452055
6536 D00002 CUST0000848462 5 5.447111
10294 D00003 CUST0000578851 5 5.446453
7318 D00002 CUST0000578851 5 5.442836
df[df$preds>5,]$preds <- 5
df[df$preds<1,]$preds <- 1
mse<-mean((df$rating-df$preds)²)
rmse<-sqrt(mse)
mae<-mean(abs(df$rating-df$preds))
print (sprintf("DL Collaborative filtering model (adjusted): MSE=%1.3f, RMSE=%1.3f, MAE=%1.3f",mse,rmse,mae))
[1] "DL Collaborative filtering model (adjusted): MSE=0.476, RMSE=0.690, MAE=0.493"
现在,我们可以查看那些预测评分与实际评分差距最大的客户-产品部门代码:
df$diff <- df$preds - df$rating
df <- df[order(-df$diff),]
head(df,20)
prod_id cust_id rating preds diff
3259 D00001 CUST0000375633 1 5.000000 4.000000
12325 D00003 CUST0000038166 1 4.306837 3.306837
14859 D00004 CUST0000817991 1 4.025836 3.025836
15279 D00004 CUST0000620867 1 4.016025 3.016025
22039 D00008 CUST0000588390 1 3.989520 2.989520
3370 D00001 CUST0000530875 1 3.969685 2.969685
22470 D00008 CUST0000209037 1 3.927513 2.927513
22777 D00008 CUST0000873432 1 3.905162 2.905162
13905 D00004 CUST0000456347 1 3.877517 2.877517
18123 D00005 CUST0000026547 1 3.853488 2.853488
24208 D00008 CUST0000732836 1 3.810606 2.810606
22723 D00008 CUST0000872856 1 3.746022 2.746022
22696 D00008 CUST0000549120 1 3.718482 2.718482
15463 D00004 CUST0000035935 1 3.714494 2.714494
24090 D00008 CUST0000643072 1 3.679629 2.679629
21167 D00006 CUST0000454947 1 3.651651 2.651651
23769 D00008 CUST0000314496 1 3.649187 2.649187
14294 D00004 CUST0000127124 1 3.625893 2.625893
22534 D00008 CUST0000556279 1 3.578591 2.578591
22201 D00008 CUST0000453430 1 3.576008 2.576008
这为我们提供了一份客户清单,以及我们应该向他们提供优惠的产品。例如,在第二行中,实际评分是 1,而预测评分是 4.306837。该客户并没有购买此产品代码的商品,而我们的模型 预测 他应该购买这些商品。
我们还可以查看实际评分远高于预测值的案例。这些是与其他相似客户相比,在该部门过度消费的客户:
df <- df[order(df$diff),]
head(df,20)
prod_id cust_id rating preds diff
21307 D00006 CUST0000555858 5 1.318784 -3.681216
15353 D00004 CUST0000640069 5 1.324661 -3.675339
21114 D00006 CUST0000397007 5 1.729860 -3.270140
23097 D00008 CUST0000388652 5 1.771072 -3.228928
21551 D00006 CUST0000084985 5 1.804969 -3.195031
21649 D00007 CUST0000083736 5 1.979534 -3.020466
23231 D00008 CUST0000917696 5 2.036216 -2.963784
21606 D00007 CUST0000899988 5 2.050258 -2.949742
21134 D00006 CUST0000373894 5 2.071380 -2.928620
14224 D00004 CUST0000541731 5 2.081161 -2.918839
15191 D00004 CUST0000106540 5 2.162569 -2.837431
13976 D00004 CUST0000952727 5 2.174777 -2.825223
21851 D00008 CUST0000077294 5 2.202812 -2.797188
16545 D00004 CUST0000945695 5 2.209504 -2.790496
23941 D00008 CUST0000109728 5 2.224301 -2.775699
24031 D00008 CUST0000701483 5 2.239778 -2.760222
21300 D00006 CUST0000752292 5 2.240073 -2.759927
21467 D00006 CUST0000754753 5 2.240705 -2.759295
15821 D00004 CUST0000006239 5 2.264089 -2.735911
15534 D00004 CUST0000586590 5 2.272885 -2.727115
我们可以如何利用这些推荐?我们的模型根据客户在每个产品部门的消费情况为其打分(1-5),因此如果客户的实际评分与预测值相比普遍较高,说明他们在这些部门的消费超出了与之类似的客户。那些人可能在其他部门没有消费,所以他们应该作为交叉销售活动的目标;也就是说,应该向他们提供其他部门的产品优惠,以吸引他们在那里购买。
总结
我希望本章能够让你明白,深度学习不仅仅是关于计算机视觉和自然语言处理的问题!在这一章中,我们讲解了如何使用 Keras 构建自编码器和推荐系统。我们看到自编码器可以用作一种降维方法,而且在其最简单的形式中,只有一层时,它们与主成分分析(PCA)相似。我们使用自编码器模型创建了一个异常检测系统。如果自编码器模型中的重构误差超过某个阈值,我们将该实例标记为潜在的异常。我们在本章中的第二个主要示例构建了一个使用 Keras 的推荐系统。我们通过交易数据构建了一个隐式评分的数据集,并建立了推荐系统。我们通过展示该模型如何用于交叉销售目的,演示了其实际应用。
在下一章,我们将讨论在云端训练深度学习模型的各种选项。如果你的本地机器没有 GPU,像 AWS、Azure、Google Cloud 和 Paperspace 等云服务提供商允许你以低廉的价格访问 GPU 实例。我们将在下一章中介绍这些选项。
第十章:在云端运行深度学习模型
到目前为止,我们只简要讨论了训练深度学习模型的硬件要求,因为本书中的几乎所有示例都可以在任何现代计算机上运行。虽然你不需要一台基于GPU(图形处理单元)的计算机来运行本书中的示例,但不可否认的是,训练复杂的深度学习模型需要一台带有 GPU 的计算机。即使你的计算机上有合适的 GPU,安装必要的软件以便使用 GPU 训练深度学习模型也不是一件简单的事。本节将简要讨论如何安装必要的软件,以便在 GPU 上运行深度学习模型,并讨论使用云计算进行深度学习的优缺点。我们将使用各种云服务提供商创建虚拟实例或访问服务,从而使我们能够在云端训练深度学习模型。
本章涵盖以下主题:
-
设置本地计算机以进行深度学习
-
使用 Amazon Web Services(AWS)进行深度学习
-
使用 Azure 进行深度学习
-
使用 Google Cloud 进行深度学习
-
使用 Paperspace 进行深度学习
设置本地计算机以进行深度学习
在撰写本书时,你可以购买一台适合深度学习的 GPU 计算机,价格低于$1,000。AWS 上最便宜的 GPU 计算机的按需费用是每小时$0.90,相当于连续使用这台机器 46 天。因此,如果你刚开始接触深度学习,云资源是最便宜的起步方式。一旦你掌握了基础知识,你可能会决定购买一台基于 GPU 的计算机,但即便如此,你仍然可以继续使用云资源进行深度学习。云端提供了更多灵活性。例如,在 AWS 上,你可以以每小时$24.48 的按需价格获得一台 p3.16xlarge 机器,配备 8 个 Tesla V100 GPU 卡。相当于 NVIDIA 的 DGX-1(www.nvidia.com/en-us/data-center/dgx-1/),它配备了 8 个 Tesla V100 GPU 卡,价格为$149,000!
如果你考虑使用自己的计算机进行深度学习,以下情况适用:
-
你已经拥有一台带有合适 GPU 处理器的计算机
-
你将购买一台计算机来构建深度学习模型
-
你将构建一台计算机来构建深度学习模型
如果你想在本地计算机上进行深度学习,你需要一张合适的 GPU 卡,并且必须是 NVIDIA 的。检查这一点的最好方法是访问 NVIDIA 官网,查看你的显卡是否与 CUDA 兼容。CUDA 是一个应用程序编程接口(API),它允许程序使用 GPU 进行计算。你需要安装 CUDA 才能使用 GPU 进行深度学习。当前检查显卡是否兼容 CUDA 的链接是developer.nvidia.com/cuda-gpus。
虽然一些公司出售专门为深度学习设计的机器,但它们非常昂贵。如果你刚开始学习深度学习,我不建议购买这些机器。相反,我建议你考虑购买一台为高端电脑游戏设计的计算机。这台计算机应该配备适合深度学习的 GPU 卡。再强调一次,首先检查这张卡是否与 CUDA 兼容(developer.nvidia.com/cuda-gpus)。
用于深度学习的游戏电脑?听起来似乎有点不寻常,但其实并不奇怪。GPU 最初是为在计算机上进行高端游戏而开发的,而不是为了深度学习。但是,设计用于游戏的机器通常会有较高的配置,例如 SSD 硬盘、大量(快速的)RAM,最重要的是 GPU 卡。早期的深度学习从业者发现,计算 3D 空间中矩阵运算的过程与神经网络中使用的矩阵运算非常相似。NVIDIA 发布了 CUDA 作为 API,使其他应用程序能够将 GPU 作为协处理器使用。无论是运气还是前瞻性,NVIDIA 成为了深度学习 GPU 卡的事实标准,并且其股价在过去三年增长了 10 倍,主要是因为人工智能对 GPU 卡的巨大需求。
第三种选择是自己组装深度学习计算机。如果你考虑这个选项,除了 GPU 卡、内存和 SSD 硬盘之外,你还需要考虑电源和主板。由于 GPU 卡和风扇的原因,你可能需要比标准计算机更大功率的电源。至于主板,你需要考虑主板与 GPU 卡之间的硬件接口是否会限制数据传输——这些接口是 PCIe 通道。GPU 可以在满负荷状态下使用 16 个 PCIe 通道。为了扩展,你可能希望选择一块支持 40 个 PCIe 通道的主板,这样你可以同时支持两张 GPU 卡和一个 SSD 硬盘。
在我们继续讨论本章关于使用云计算进行深度学习的内容之前,应该简要讨论一下云中的 GPU 卡与本书中使用的 GPU 卡的性能对比。对于本书,我使用的是一块 GTX 1050 Ti,它有 768 个核心和 4GB 的内存。根据我的经验,这张卡的性能大致与 AWS 上的p2.xlarge实例相当。我通过在本地 CPU(i5 处理器)、本地 GPU(GTX 1050 Ti)和 AWS GPU(p2.xlarge)上运行两个模型进行了测试。我测试了两个模型:来自第四章的二分类预测任务,训练深度预测模型,以及来自第五章的 LeNet 卷积神经网络,使用卷积神经网络进行图像分类。这两个模型都是使用 MXNet 构建的,并运行了 50 个周期:

图 10.1:两个深度学习网络在 CPU、本地 GPU 和 AWS GPU 上的执行时间(秒)
在我的本地机器上,运行二进制预测任务的深度学习模型在 GPU 上比在 CPU 上快约 20%,而 AWS GPU 机器比本地 GPU 快约 13%。然而,在运行卷积神经网络时,使用本地 CPU 训练几乎比使用本地 GPU 慢 16 倍。反过来,AWS GPU 比本地 GPU 快约 16%。这些结果是预期的,并且与我在实践中看到的情况和其他网站上的基准测试相符,明确表明对于深度学习计算机视觉任务,GPU 是必不可少的。我的本地机器上的 GPU 卡(GTX 1050 Ti)可能是您应该用于深度学习的最低规格 GPU 卡。目前的价格不到$200。作为比较,高端 GPU 卡(GTX 1080 Ti)拥有 3584 个核心和 11 GB 的内存,目前的价格约为$700。GTX 1080 Ti 比 GTX 1050 Ti 快大约 4-5 倍。
为什么前面的图表只涉及 AWS,而不涉及 Azure、Google Cloud 和 Paperspace?为什么我没有对它们的性能和/或成本进行基准测试?我有几个理由决定不这样做。首先,也是最重要的是,任何推荐在几个月后就会过时——深度学习非常流行,各个云服务提供商不断更改其产品和价格。另一个原因是,本书中的示例相对较小,我们使用的是最便宜的 GPU 实例。因此,与生产用例的任何比较都可能误导。最后,当您刚开始时,易用性可能比原始成本更重要。本书中的所有示例在任何提供商的云中都应在 1 小时内运行,因此争论一个提供商每小时成本为$0.55 和另一个为$0.45 是不重要的。
我如何知道我的模型是在 GPU 上训练的?
许多刚开始深度学习的人会问一个问题,我如何知道我的模型是在 GPU 上训练的?幸运的是,无论您是使用云实例还是本地机器,您都可以检查深度学习模型是在 GPU 还是 CPU 上进行训练。实例上有一个工具可以显示 GPU 的活动。在 Linux 中,您可以输入以下命令:
watch -n0.5 nvidia-smi
在 Windows 中,您可以从命令提示符中使用以下命令:
nvidia-smi -l 1
这将运行一个脚本,输出关于计算机 GPU 的诊断信息。如果您的模型当前正在 GPU 上训练,GPU 实用程序将会很高。在下面的示例中,我们可以看到它为 75-78%。我们还可以看到名为rsession.exe的文件正在使用 GPU 内存。这证实了模型正在 GPU 上训练:

图 10.2:nvidia-smi 工具显示 GPU 卡的利用率为 75-85%
使用 AWS 进行深度学习
AWS 是最大的云服务提供商,因此它值得我们关注。如果你知道如何使用 AWS,特别是如果你熟悉竞价请求,它可以是训练复杂深度学习模型的一种非常具有成本效益的方法。
AWS 简介
本节简要介绍了 AWS 的工作原理。它描述了 EC2、AMI 以及如何在云中创建虚拟机。这不会是对 AWS 的详尽介绍——网上有很多教程可以指导你。
AWS 是一套云资源。另一个术语是基础设施即服务(IaaS),与软件即服务(SaaS)或平台即服务(PaaS)不同。在 IaaS 中,与 SaaS 或 PaaS 不同,你获得的是基础设施(硬件),如何使用它取决于你。这包括安装软件和管理安全性与网络,尽管 AWS 会处理一些安全性和网络方面的内容。AWS 提供了许多服务,但对于深度学习,你将使用的是 EC2,它是一个虚拟计算环境,允许你启动实例(虚拟计算机)。你可以通过 Web 界面或远程登录它们来运行命令控制这些虚拟计算机。当你启动一个 EC2 实例时,可以选择操作系统(如 Ubuntu、Linux、Windows 等)以及你想要的机器类型。
你还可以选择使用Amazon 机器镜像(AMI),它已经预装了软件应用和库。这对于深度学习是一个不错的选择,因为这意味着你可以启动一个已安装深度学习库的 EC2 实例,直接开始深度学习。
你应该熟悉的另一个服务是 S3,它是一种持久存储。我建议你采用的一个非常有用的做法是将你的虚拟机视为临时资源,并将数据和中间结果保存在 S3 中。我们在本章中不会讨论这个,因为它是一个高级话题。
在上一节中,我们提到当前 AWS 上最便宜的 GPU 计算机的按需费用为每小时$0.90。按需 是一种使用 AWS 虚拟机的方式,但在 AWS 中有三种不同的方式来租用虚拟机:
-
按需实例:当你按需租用实例时。
-
预留实例:当你承诺在一定时间内(通常为 1-3 年)租用机器时。这比按需实例便宜大约 50%。然而,你需要承诺在这段时间内为资源付费。
-
现货实例:为了应对需求波动,亚马逊大部分时间都有备用的计算资源。你可以竞标这些未使用的资源,通常你可以以比按需和保留实例便宜的价格获得它,具体价格取决于该类型机器的需求。然而,一旦你获得了这台机器,并不保证你会一直使用它——如果计算机需求增加,你的计算机可能会被终止。
保留实例对于深度学习并不实用。租用最便宜的 GPU 机器 1 年的费用将超过 5000 美元,而你可以花更少的钱买到性能更好的深度学习机器。按需实例保证你会在需要时拥有资源,但费用较高。如果你知道如何正确使用现货实例并计划好计算机可能会被终止的风险,它是一种有趣且成本效益高的使用方法。
通常,现货价格大约是按需价格的 30%,所以节省的费用非常可观。你的竞标价格是你愿意为现货实例支付的最高金额,实际价格取决于市场价格,市场价格基于需求变化。
因此,你应该将竞标价格设定得更高;我建议设定为按需价格的 51%、76%或 101%。额外的 1%是因为,与任何竞标市场类似,人们习惯将竞标价格定为圆整的数字,通过加上 1%的额外数值,可以避开这一惯性,从而可能产生不同的结果。
现货实例最初的使用场景是低优先级的批处理任务。公司使用现货实例来节省计算资源,运行那些如果未能完成可以重新启动的长期任务。例如,可能是运行一个处理非关键操作数据的二次数据摄取过程。然而,基于 GPU 的实例的需求模式有所不同,可能是因为类似 Kaggle 这样的在线数据挖掘比赛。由于 GPU 实例并不常见,GPU 实例的需求波动更大。这导致了现货定价中出现一些奇怪的现象,人们为现货实例竞标的价格可能是按需价格的 10 倍。人们之所以这样做,是因为他们认为这样不太可能被其他人超出出价。有时候,p2.16xlarge 的现货价格为每小时 144 美元,而按需价格为 14.40 美元。那些设置这些竞标的人不希望自己的机器被终止,并认为即使他们出高价,平均下来使用现货实例仍然比按需实例便宜。如果你打算使用现货实例,我不推荐这种做法,因为如果需求突然上升,你可能会遇到非常大的意外!不过,你应该意识到这一定价怪癖——不要认为只要将竞标价格设定为略高于按需价格,就能保证你的机器不会被终止。
AWS 通过提供定价历史图表来帮助你设置竞价请求,图表会提供按需价格和竞价价格的建议。在下图中,我们可以看到某个特定区域(us-east)过去三个月的价格变化情况。该区域有 6 个可用区(us-east-1a 到 us-east-1f),该实例类型(p2.16xlarge)的当前竞价价格在 4.32 美元至 14.40 美元之间,而按需价格为 14.40 美元:

图 10.3:p2.16xlarge 实例类型的竞价历史
查看上述资源的图表后,我会考虑以下因素:
-
如果可能,我会选择 us-east-1a 可用区,因为它的价格波动最小。
-
我会将价格设置为每小时 7.21 美元,这只是按需价格的 50% 以上。由于自从 us-east-1a 区域的竞价价格超过每小时 4.32 美元已经过去一个月,我可能只会支付每小时 4.32 美元。将价格设置为较高的金额会使得我的竞价实例被终止的可能性较小。
区域和可用区: AWS 将其服务安排在不同的区域(us-east1、eu-west1 等)。目前,有 18 个不同的区域,并且每个区域中都有多个可用区,你可以将它们视为物理数据中心。对于一些使用案例(例如,网站、灾难恢复等)和合规要求,区域和可用区非常重要。对于深度学习来说,它们并不那么重要,因为你通常可以在任何位置运行深度学习模型。各个区域/可用区的竞价价格不同,某些资源在某些区域会更贵。你还需要注意,在不同区域之间转移数据是有成本的,所以最好将数据和实例保持在同一区域内。
在 AWS 中创建深度学习 GPU 实例
本节将使用 AWS 来训练 第九章《异常检测与推荐系统》中的深度学习模型。这将包括设置机器、访问机器、下载数据以及运行模型。我们将使用来自 RStudio 的预构建 AWS AMI,里面已经安装了 TensorFlow 和 Keras。有关此 AMI 的详细信息,请访问此链接:aws.amazon.com/marketplace/pp/B0785SXYB2。如果你还没有 AWS 账户,你需要在 portal.aws.amazon.com/billing/signup 注册一个账户。一旦注册完成,请按照以下步骤在 AWS 上创建一个带有 GPU 的虚拟机:
请注意,在 AWS 中设置实例时,实例运行期间会计费!务必确保关闭实例,否则你将继续被收费。在完成使用虚拟实例后,检查 AWS 控制台,确保没有正在运行的实例。
- 登录到 AWS 控制台并选择 EC2。你应该会看到一个类似于以下的屏幕。这是创建新虚拟机器的 Web 界面:

图 10.4:AWS EC2 仪表板
-
点击启动实例按钮,以下页面将加载。
-
点击左侧的AWS Marketplace,在搜索框中输入
rstudio(参见以下截图)。 -
选择带有 Tensorflow-GPU 的 RStudio 服务器(适用于 AWS)。请注意,还有另一个选项带有Pro字样——这是一个付费订阅,附加了额外费用,所以不要选择这个 AMI:

图 10.5:AWS 启动实例向导,第 1 步
- 一旦点击选择,可能会出现以下屏幕,其中包含有关访问实例的附加信息。请仔细阅读说明,因为它们可能与以下屏幕截图中显示的内容有所不同:

图 10.6:RStudio AMI 信息
- 当你点击继续时,以下屏幕将出现,选择机器类型非常重要。一定要选择一个带有 GPU 的机器,所以在按筛选条件:选项中,选择 GPU 计算,然后从列表中选择p2.xlarge。你的选项应该与以下截图类似:

图 10.7:AWS 启动实例向导,第 2 步
- 点击下一步后,你将看到一个包含各种配置选项的屏幕。默认选项是可以的,所以只需再次点击下一步:

图 10.8:AWS 启动实例向导,第 3 步
-
此屏幕允许你更改存储选项。根据数据大小,你可能需要增加额外的存储。存储相对便宜,所以我建议选择输入数据大小的 3 倍到 5 倍的存储空间。
-
点击下一步以进入下一个截图:

图 10.9:AWS 启动实例向导,第 4 步
- 以下屏幕不重要——标签用于在 AWS 中跟踪资源,但我们不需要它们。点击下一步以进入下一个截图:

图 10.10:AWS 启动实例向导,第 5 步
- 以下截图显示了安全选项。AWS 限制了对实例的访问,因此你必须打开任何需要的端口。此处提供的默认选项允许访问端口
22(SSH)以访问 shell,并且还允许访问端口8787,这是 RStudio 使用的 Web 端口。点击审查并启动以继续:

图 10.11:AWS 启动实例向导,第 6 步
下面的截图将会显示。注意关于安全性的警告信息——在生产环境中,你可能需要解决这些问题。
- 点击启动按钮以继续:

图 10.12:AWS 启动实例向导,第 7 步
- 系统会要求你选择一个密钥对。如果你还没有创建密钥对,请选择相应的选项进行创建。给它起一个描述性的名字,然后点击下载密钥对按钮。之后,点击“启动实例”按钮:
密钥对用于通过 SSH 访问实例。你应该非常小心地保护这个密钥,因为如果有人获得了你的私钥,他们将能够登录到你的任何实例。你应该定期删除密钥对并创建一个新的。

图 10.13:AWS 启动实例向导,选择密钥对
- 完成此操作后,你可以返回到 EC2 控制台,看到你有 1 个正在运行的实例。点击该链接查看实例的详细信息:

图 10.14:AWS EC2 控制台
- 在这里,你将看到实例的详细信息。在本例中,IP 地址是
34.227.109.123。还需要记下被高亮显示的实例 ID,因为这是用于连接到 RStudio 实例的密码:

图 10.15:AWS EC2 控制台,实例详细信息
-
打开另一个网页,浏览到你机器的 IP 地址,并在后面加上
:8787以访问该链接。在我的示例中,链接是http://34.227.109.123:8787/。登录的说明在图 10.6中,即使用rstudio-user作为用户名,实例 ID 作为密码。你还应考虑按照说明更改密码。 -
登录后,你将看到一个熟悉的界面——它类似于 RStudio 桌面程序。一个不同之处是你右下角的上传按钮,它允许你上传文件。在以下示例中,我已经上传了第九章的数据显示和脚本,异常检测与推荐系统,用于 Keras 推荐系统示例,并成功运行:

图 10.16:通过 RStudio Server 访问云中的深度学习实例
RStudio 的 Web 界面类似于在本地计算机上使用 RStudio。在图 10.16中,你可以看到我上传的数据显示文件(recomend.csv,recomend40.csv)以及位于左下窗口的文件中的 R 脚本。我们还可以看到在左下角控制台窗口中执行的代码。
这就完成了我们在 AWS 中如何设置深度学习机器的示例。再提醒一次,记得计算机运行时会计费。确保终止你的实例,否则你将继续被收费。为此,返回到 EC2 仪表板,找到实例,并点击操作按钮。会弹出一个菜单,选择实例状态,然后选择终止:

图 10.17:终止 AWS 实例
在 AWS 中创建深度学习 AMI
在之前的示例中,我们使用了由 RStudio 构建的Amazon Machine Image(AMI)。在 AWS 中,你也可以创建自己的 AMI。当你创建 AMI 时,可以安装所需的软件,将数据加载到 AMI 中,并按自己的需求进行设置。本节将向你展示如何使用 AMI 在 AWS 上使用 MXNet。
-
创建 AMI 的第一步是选择要使用的基础镜像。我们可以从只安装了操作系统的基础镜像开始,但我们将使用之前提到的带有 Tensorflow-GPU 的 RStudio Server for AWS,并向其中添加 MXNet 包。
-
安装 MXNet 的说明改编自
mxnet.incubator.apache.org/install/index.html。第一步是按照前一节的说明,从带有 Tensorflow-GPU 的 RStudio Server for AWS AMI 创建实例。 -
完成此步骤后,你需要 SSH 登录到机器。如何操作取决于你自己计算机的操作系统。对于 Linux 和 macOS,你可以在 shell 中执行本地命令;在 Windows 上,你可以使用 Putty。
-
登录到机器后,运行以下命令:
vi ~/.profile
- 将以下行添加到文件的末尾并保存文件:
export CUDA_HOME=/usr/local/cuda
- 回到 shell 后,依次运行以下命令:
sudo apt-get update
sudo dpkg --configure -a
sudo apt-get install -y build-essential git
export CUDA_HOME=/usr/local/cuda
git clone --recursive https://github.com/apache/incubator-mxnet
cd incubator-mxnet
make -j $(nproc) USE_OPENCV=1 USE_CUDA=1 USE_CUDA_PATH=/usr/local/cuda USE_CUDNN=1
- 最后一个命令可能需要最多 2 小时才能完成。完成后,运行最后几行命令:
sudo ldconfig /usr/local/cuda/lib64
sudo make rpkg
sudo R CMD INSTALL mxnet_current_r.tar.gz
第二行命令可能需要最多 30 分钟。最后一行可能会返回关于缺少文件的警告,可以忽略该警告。
- 要测试是否一切安装正确,访问该实例的 RStudio 页面并输入以下代码:
library(mxnet)
a <- mx.nd.ones(c(2,3), ctx = mx.gpu())
b <- a * 2 + 1
b
- 你应该得到以下输出:
[,1] [,2] [,3]
[1,] 3 3 3
[2,] 3 3 3
- 现在返回到 EC2 仪表板,点击运行中的实例,并在列表中选择该机器。点击操作按钮,从下拉菜单中选择镜像,然后选择创建镜像。如下图所示:

图 10.18:创建 AMI
- 镜像创建可能需要 15-20 分钟才能完成。完成后,点击左侧菜单中的 AMI,显示与你账户关联的 AMI 列表。你应该能看到刚刚创建的 AMI。该 AMI 可以用于创建新的按需实例或新的抢占实例。下图展示了为 AMI 创建抢占实例的菜单选项:

图 10.19:使用现有的 AMI 创建 Spot 请求
这个 AMI 现在可以使用,你可以创建新的深度学习实例。请注意,即使不使用它,存储 AMI 也会产生持续的费用。
使用 Azure 进行深度学习
Azure 是 Microsoft 云服务的品牌名。你可以使用 Azure 进行深度学习,类似于 AWS,它提供了预先配置了深度学习库的深度学习虚拟机。在这个示例中,我们将创建一个 Windows 实例,可以用于 Keras 或 MXNet。假设你的本地计算机也是 Windows 系统,因为你将使用远程桌面协议(RDP)访问云实例。
- 第一步是创建一个 Azure 账户,然后在
portal.azure.com登录 Azure。你将看到一个类似于下面的截图。点击创建资源并搜索深度学习虚拟机:

图 10.20:Azure 门户网站
- 当你选择深度学习虚拟机时,以下屏幕将显示。点击创建:

图 10.21:在 Azure 上部署深度学习实例,步骤 0
- 现在你将开始一个 4 步向导来创建新实例。
第一步(基础)要求提供一些基本信息。可以输入与我相同的值,但请小心填写用户名和密码,因为稍后会用到:

图 10.22:在 Azure 上部署深度学习实例,步骤 1
在第 2 步(设置)中,确保虚拟机的大小为 1 x Standard NC6(1 GPU),然后点击确定继续:

图 10.23:在 Azure 上部署深度学习实例,步骤 2
在第 3 步(摘要)中,有一个简短的验证检查。系统可能会提示你,账户没有足够的计算/虚拟机(核心/vCPU)资源,这是因为 Microsoft 在账户创建时可能对其进行了限制。你需要创建一个支持票,申请增加资源,然后再试。如果你通过了此步骤,点击确定继续。现在你进入了最后一步,只需点击创建:

图 10.24:在 Azure 上部署深度学习实例,步骤 4
你可能需要等待 30-40 分钟,直到资源创建完成。完成后,选择左侧的所有资源,你会看到所有对象已经创建。以下截图显示了这一点。点击类型为虚拟机的项目:

图 10.25:当前在 Azure 上部署的资源列表
-
然后你将看到以下截图。点击屏幕顶部的连接按钮。
-
右侧将弹出一个面板,提供下载 RDP 文件的选项。点击该选项,当文件下载完毕后,双击它:

图 10.26:下载 RDP 文件以连接到 Azure 中的云实例
- 这应该会弹出一个登录窗口,连接到云实例。输入你在第 1 步中创建的用户名和密码以连接到实例。连接后,你将看到一个类似于以下截图的桌面:

图 10.27:深度学习实例的远程桌面(Azure)
太好了!RStudio 已经安装好。Keras 也已安装,因此你任何的 Keras 深度学习代码都可以运行。现在让我们尝试运行一些 MXNet 代码。打开 RStudio 并运行以下命令来安装 MXNet:
cran <- getOption("repos")
cran["dmlc"] <- "https://apache-mxnet.s3-accelerate.dualstack.amazonaws.com/R/CRAN/GPU/cu90"
options(repos = cran)
install.packages("mxnet")
# validate install
library(mxnet)
a <- mx.nd.ones(c(2,3), ctx = mx.gpu())
b <- a * 2 + 1
b
在当前安装的 R 版本中,这将无法工作。如果你想使用 MXNet,必须下载最新版本的 R(撰写本书时是 3.5.1 版本)并安装。不幸的是,这会禁用 Keras,因此只有在你希望使用 MXNet 而不是 Keras 时才这样做。从 https://cran.r-project.org/ 下载 R 后,再重新运行上面的代码来安装 MXNet。
注意:这些 AMI 上安装的软件频繁变动。在安装任何深度学习库之前,检查已安装的 CUDA 版本。你需要确保深度学习库与机器上已安装的 CUDA 版本兼容。
使用 Google Cloud 进行深度学习
Google Cloud 也提供 GPU 实例。在撰写本书时,配备 NVIDIA Tesla K80 GPU 卡(这也是 AWS p2.xlarge 实例中的 GPU 卡)的实例按需价格为每小时 $0.45。这比 AWS 的按需价格便宜得多。有关 Google Cloud GPU 实例的更多详情,请访问 cloud.google.com/gpu/。然而,对于 Google Cloud,我们将不使用实例,而是使用 Google Cloud Machine Learning Engine API 将机器学习任务提交到云中。与虚拟机配置相比,这种方法的一个大优势是你只需为所使用的硬件资源付费,而无需担心设置和终止实例。更多详情和定价信息可以在 cloud.google.com/ml-engine/pricing 查找到。
按照以下步骤注册 Google Cloud 并启用 API:
-
注册 Google Cloud 账号。
-
你需要登录到门户网站
console.cloud.google.com并启用Cloud Machine Learning Engine API。 -
从主菜单中选择APIs & Services,然后点击启用 API 和服务按钮。
-
API 按组进行分类。选择查看全部以查看机器学习组,然后选择Cloud Machine Learning Engine并确保该 API 已启用。
启用 API 后,从 RStudio 执行以下代码:
devtools::install_github("rstudio/cloudml")
library(cloudml)
gcloud_init()
这应该会安装 Google Cloud SDK,并要求您将 Google 账户连接到 SDK。然后,您将进入终端窗口中的一个选项菜单。第一个选项如下:

图 10.28:从 RStudio 访问 Google Cloud SDK
现在,暂时不要创建任何新的项目或配置,只需选择已经存在的项目。一旦将您的 Google 账户连接到机器上的 Google SDK 并启用了相关服务,您就可以开始使用了。Cloud Machine Learning Engine 允许您向 Google Cloud 提交作业,而无需创建任何实例。工作文件夹中的所有文件(R 脚本和数据)将被打包并发送到 Google Cloud。
在这个示例中,我从 第八章 项目中获取了推荐文件,使用 TensorFlow 在 R 中的深度学习模型。我将该文件和 keras_recommend.R 脚本复制到一个新目录,并在该目录中创建了一个新的 RStudio 项目。然后,我在 RStudio 中打开该项目。您可以在前面的截图中看到这两个文件和 RStudio 项目文件。接着,我在 RStudio 中执行以下命令提交深度学习作业:
cloudml_train("keras_recommend.R", master_type = "standard_gpu")
这将收集当前工作目录中的文件,并将其发送到 Cloud Machine Learning Engine。在作业执行过程中,一些进度信息将返回到 RStudio。您还可以通过选择ML Engine | Jobs,在 console.cloud.google.com 控制台页面上监控活动。以下是该网页的截图,显示了两个已完成的作业和一个已取消的作业:

图 10.29:Google Cloud Platform 网页上的 ML 引擎/作业页面
当作业完成时,日志将被下载到本地机器。一个漂亮的总结网页会自动生成,显示作业的统计信息,如以下截图所示:

图 10.30:机器学习作业的网页总结页面
我们可以看到图表显示了模型在训练过程中的进度、模型摘要、一些超参数(epochs、batch_size 等),以及成本(ml_units)。该网页还包含来自 R 脚本的输出。选择菜单中的输出以查看。在以下截图中,我们可以看到 R 代码及其输出:

图 10.31:显示 R 代码和输出的机器学习作业网页总结页面
这只是使用 Google 云机器学习引擎的简要介绍。这里有一个出色的教程,您可以在tensorflow.rstudio.com/tools/cloudml/articles/tuning.html查看,它解释了如何使用此服务进行超参数训练。使用此服务进行超参数训练,比使用虚拟实例自己管理训练要简单得多,而且可能更加便宜。您不必监控它或协调模型训练的不同运行。有关如何使用此服务的更多信息,请访问tensorflow.rstudio.com/tools/cloudml/articles/getting_started.html。
使用 Paperspace 进行深度学习
Paperspace是另一种有趣的方式,可以在云中进行深度学习。它可能是训练深度学习模型最简单的云端方式。要使用 Paperspace 设置云实例,您可以登录他们的控制台,配置一个新机器,并通过您的网页浏览器连接到它:
- 首先注册 Paperspace 账号,登录控制台,通过选择核心或计算进入虚拟机部分。Paperspace 提供了一个带有 NVIDIA GPU 库(CUDA 8.0 和 cuDNN 6.0)以及 GPU 版本的 TensorFlow 和 Keras for R 的 RStudio TensorFlow 模板。您在选择公共模板时将看到这种机器类型,如下图所示:

图 10.32:Paperspace 门户
- 您将可以选择三种 GPU 实例,并选择按小时或按月付费。请选择最便宜的选项(目前是 P4000,按小时收费$0.40)和按小时计费。向下滚动页面底部,点击创建按钮。几分钟后,您的机器将被配置完毕,您将能够通过浏览器访问它。以下是一个 RStudio Paperspace 实例的示例:

图 10.33:从网页访问虚拟机桌面并运行 RStudio,适用于 Paperspace 实例
默认情况下,Keras 已经安装,因此您可以直接使用 Keras 训练深度学习模型。然而,我们还将要在实例中安装 MXNet:
- 第一步是打开 RStudio 并安装一些包。从 RStudio 执行以下命令:
install.packages("devtools")
install.packages(c("imager","DiagrammeR","influenceR","rgexf"))
-
下一步是访问您刚刚创建的实例的终端(或 Shell)。您可以返回控制台页面从那里操作。或者,点击桌面右上角的圆形目标(请参见前面的截图)。这还为您提供了其他选项,如同步本地计算机和虚拟机之间的复制粘贴。
-
登录实例的终端后,运行以下命令将安装 MXNet:
sudo apt-get update
sudo dpkg --configure -a
sudo apt-get install -y build-essential git
export CUDA_HOME=/usr/local/cuda
git clone --recursive https://github.com/apache/incubator-mxnet
cd incubator-mxnet
make -j $(nproc) USE_OPENCV=1 USE_BLAS=blas USE_CUDA=1 USE_CUDA_PATH=/usr/local/cuda USE_CUDNN=1
sudo ldconfig /usr/local/cuda/lib64
sudo make rpkg
- 你还需要将以下行添加到
.profile文件的末尾:
export CUDA_HOME=/usr/local/cuda
完成后,重新启动实例。现在你有了一台可以在云端训练 Keras 和 MXNet 深度学习模型的机器。有关如何在 Paperspace 中使用 RStudio 的更多详细信息,请参见 tensorflow.rstudio.com/tools/cloud_desktop_gpu.html。
总结
本章已经涵盖了许多训练深度学习模型的选项!我们讨论了本地运行的选项,并展示了拥有 GPU 卡的重要性。我们使用了三大主流云服务提供商,在云端使用 R 来训练深度学习模型。云计算是一个极其出色的资源——我们举了一个超级计算机的例子,价值 149,000 美元。几年前,这样的资源几乎是每个人都无法企及的,但现在得益于云计算,你可以按小时租用这样的机器。
对于 AWS、Azure 和 Paperspace,我们在云端资源上安装了 MXNet,这让我们可以选择使用哪种深度学习库。我鼓励你使用本书其他章节中的示例,并尝试这里的所有不同云服务提供商。想想看,能做到这一点,而且你的总费用可能还不到 $10,真是令人惊叹!
在下一章也是最后一章,我们将基于图像文件构建图像分类解决方案。我们将展示如何应用迁移学习,这使得你可以将现有模型适应新的数据集。我们还将展示如何通过 REST API 部署模型到生产环境,并简要讨论生成对抗网络、强化学习,同时提供一些进一步的资源,以便你继续深入学习深度学习。
第十一章:深度学习的下一层级
我们的深度学习之旅即将结束。本章内容涉及多个主题。我们将从重新回顾一个图像分类任务开始,构建一个完整的图像分类解决方案,使用图像文件而不是表格数据。然后,我们将解释迁移学习,您可以将现有模型应用于新的数据集。接下来,我们讨论在任何机器学习项目中的一个重要考虑因素——您的模型将在部署中如何使用,也就是在生产环境中如何使用?我们将展示如何创建一个 REST API,允许任何编程语言调用 R 中的深度学习模型对新数据进行预测。然后,我们将简要讨论另外两个深度学习主题:生成对抗网络和强化学习。最后,我们将通过提供一些您可能感兴趣的其他资源,结束本章和本书的内容。
本章我们将涵盖以下主题:
-
构建完整的图像分类解决方案
-
ImageNet 数据集
-
迁移学习
-
部署 TensorFlow 模型
-
生成对抗网络
-
强化学习
-
其他深度学习资源
图像分类模型
我们在第五章中介绍了图像分类,使用卷积神经网络进行图像分类。在那一章中,我们描述了对于涉及图像的深度学习任务至关重要的卷积层和池化层。我们还在一个简单的数据集——MNIST 数据集上构建了多个模型。在这里,我们将探讨一些高级的图像分类主题。首先,我们将构建一个完整的图像分类模型,使用图像文件作为输入。我们将了解回调函数,这是构建复杂深度学习模型时的得力助手。一个回调函数将被用来将模型持久化(保存)到文件中,并在之后加载。接下来,我们将使用这个模型进行迁移学习。在迁移学习中,您会使用预训练模型中的一些层来处理新的数据。
构建完整的图像分类解决方案
我们已经构建了一些图像分类模型,但它们使用的是从 Keras 或 CSV 文件加载的 MNIST 数据集。数据始终是表格格式的。显然,这不是大多数情况下图像存储的方式。本节内容将讨论如何使用一组图像文件构建图像分类模型。第一个任务是获取一组图像文件。我们将加载包含在 Keras 中的 CIFAR10 数据,并将其保存为图像文件。然后,我们将使用这些文件构建深度学习模型。完成这项练习后,您将学会如何使用自己的图像文件创建深度学习图像分类任务。
本章的深度学习模型并不是一个复杂的模型。重点是展示图像分类任务的数据管道结构。我们会看看如何安排图像文件,如何使用数据增强,以及如何在训练过程中使用回调。
创建图像数据
第一步是创建图像文件。此部分的代码位于 Chapter11/gen_cifar10_data.R 文件夹中。我们将加载 CIFAR10 数据并将图像文件保存在数据目录中。第一步是创建目录结构。CIFAR10 数据集包含 10 个类别:我们将为构建模型保存 8 个类别,并将在后面的部分中使用 2 个类别(迁移学习)。以下代码将在 data 下创建以下目录:
-
cifar_10_images -
cifar_10_images/data1 -
cifar_10_images/data2 -
cifar_10_images/data1/train -
cifar_10_images/data1/valid -
cifar_10_images/data2/train -
cifar_10_images/data2/valid
这是 Keras 期望图像数据存储的结构。如果您使用此结构,则可以将图像用于在 Keras 中训练模型。在代码的第一部分,我们创建了这些目录:
library(keras)
library(imager)
# this script loads the cifar_10 data from Keras
# and saves the data as individual images
# create directories,
# we will save 8 classes in the data1 folder for model building
# and use 2 classes for transfer learning
data_dir <- "../data/cifar_10_images/"
if (!dir.exists(data_dir))
dir.create(data_dir)
if (!dir.exists(paste(data_dir,"data1/",sep="")))
dir.create(paste(data_dir,"data1/",sep=""))
if (!dir.exists(paste(data_dir,"data2/",sep="")))
dir.create(paste(data_dir,"data2/",sep=""))
train_dir1 <- paste(data_dir,"data1/train/",sep="")
valid_dir1 <- paste(data_dir,"data1/valid/",sep="")
train_dir2 <- paste(data_dir,"data2/train/",sep="")
valid_dir2 <- paste(data_dir,"data2/valid/",sep="")
if (!dir.exists(train_dir1))
dir.create(train_dir1)
if (!dir.exists(valid_dir1))
dir.create(valid_dir1)
if (!dir.exists(train_dir2))
dir.create(train_dir2)
if (!dir.exists(valid_dir2))
dir.create(valid_dir2)
在每个 train 和 valid 目录下,都为每个类别使用一个单独的目录。我们将 8 个类别的图像保存在 data1 文件夹下,将 2 个类别的图像保存在 data2 文件夹下:
# load CIFAR10 dataset
c(c(x_train,y_train),c(x_test,y_test)) %<-% dataset_cifar10()
# get the unique categories,
# note that unique does not mean ordered!
# save 8 classes in data1 folder
categories <- unique(y_train)
for (i in categories[1:8])
{
label_dir <- paste(train_dir1,i,sep="")
if (!dir.exists(label_dir))
dir.create(label_dir)
label_dir <- paste(valid_dir1,i,sep="")
if (!dir.exists(label_dir))
dir.create(label_dir)
}
# save 2 classes in data2 folder
for (i in categories[9:10])
{
label_dir <- paste(train_dir2,i,sep="")
if (!dir.exists(label_dir))
dir.create(label_dir)
label_dir <- paste(valid_dir2,i,sep="")
if (!dir.exists(label_dir))
dir.create(label_dir)
}
一旦我们创建了目录,下一步就是将图像保存到正确的目录中,我们将在以下代码中完成此操作:
# loop through train images and save in the correct folder
for (i in 1:dim(x_train)[1])
{
img <- x_train[i,,,]
label <- y_train[i,1]
if (label %in% categories[1:8])
image_array_save(img,paste(train_dir1,label,"/",i,".png",sep=""))
else
image_array_save(img,paste(train_dir2,label,"/",i,".png",sep=""))
if ((i %% 500)==0)
print(i)
}
# loop through test images and save in the correct folder
for (i in 1:dim(x_test)[1])
{
img <- x_test[i,,,]
label <- y_test[i,1]
if (label %in% categories[1:8])
image_array_save(img,paste(valid_dir1,label,"/",i,".png",sep=""))
else
image_array_save(img,paste(valid_dir2,label,"/",i,".png",sep=""))
if ((i %% 500)==0)
print(i)
}
最后,像之前做的那样,我们将进行验证检查,确保我们的图像是正确的。让我们加载 9 张来自同一类别的图像。我们想检查这些图像是否正确显示,并且它们是否来自同一类别:
# plot some images to verify process
image_dir <- list.dirs(valid_dir1, full.names=FALSE, recursive=FALSE)[1]
image_dir <- paste(valid_dir1,image_dir,sep="")
img_paths <- paste(image_dir,list.files(image_dir),sep="/")
par(mfrow = c(3, 3))
par(mar=c(2,2,2,2))
for (i in 1:9)
{
im <- load.image(img_paths[i])
plot(im)
}
这将生成以下图表:

图 11.1:样本 CIFAR10 图像
这看起来不错!图像显示正确,我们可以看到这些图像似乎都属于同一个类别,即汽车。图像有些模糊,但那是因为它们仅是 32 x 32 的缩略图。
构建深度学习模型
一旦你运行了前面部分的脚本,你应该会在 cifar_10_images/data1/train 目录中拥有 40,000 张训练图像,在 cifar_10_images/data1/valid 目录中拥有 8,000 张验证图像。我们将使用这些数据训练一个模型。此部分的代码位于 Chapter11/build_cifar10_model.R 文件夹中。第一部分创建了模型定义,您应该已经很熟悉:
library(keras)
# train a model from a set of images
# note: you need to run gen_cifar10_data.R first to create the images!
model <- keras_model_sequential()
model %>%
layer_conv_2d(name="conv1", input_shape=c(32, 32, 3),
filter=32, kernel_size=c(3,3), padding="same"
) %>%
layer_activation("relu") %>%
layer_conv_2d(name="conv2",filter=32, kernel_size=c(3,3),
padding="same") %>%
layer_activation("relu") %>%
layer_max_pooling_2d(pool_size=c(2,2)) %>%
layer_dropout(0.25,name="drop1") %>%
layer_conv_2d(name="conv3",filter=64, kernel_size=c(3,3),
padding="same") %>%
layer_activation("relu") %>%
layer_conv_2d(name="conv4",filter=64, kernel_size=c(3,3),
padding="same") %>%
layer_activation("relu") %>%
layer_max_pooling_2d(pool_size=c(2,2)) %>%
layer_dropout(0.25,name="drop2") %>%
layer_flatten() %>%
layer_dense(256) %>%
layer_activation("relu") %>%
layer_dropout(0.5) %>%
layer_dense(256) %>%
layer_activation("relu") %>%
layer_dropout(0.5) %>%
layer_dense(8) %>%
layer_activation("softmax")
model %>% compile(
loss="categorical_crossentropy",
optimizer="adam",
metrics="accuracy"
)
模型定义是基于 VGG16 架构进行修改的,我们将在后面看到它。我使用了更少的块和节点。请注意,最终的密集层必须有 8 个节点,因为 data1 文件夹中只有 8 个类别,而不是 10 个。
下一部分设置了数据生成器;其目的是在模型训练时将图像文件批次加载到模型中。我们还可以在数据生成器中对训练数据集应用数据增强。我们将选择通过随机水平翻转图像、水平/垂直平移图像和旋转图像最多 15 度来创建人工数据。我们在第六章 模型调优与优化 中看到,数据增强可以显著改善现有模型:
# set up data generators to stream images to the train function
data_dir <- "../data/cifar_10_images/"
train_dir <- paste(data_dir,"data1/train/",sep="")
valid_dir <- paste(data_dir,"data1/valid/",sep="")
# in CIFAR10
# there are 50000 images in training set
# and 10000 images in test set
# but we are only using 8/10 classes,
# so its 40000 train and 8000 validation
num_train <- 40000
num_valid <- 8000
flow_batch_size <- 50
# data augmentation
train_gen <- image_data_generator(
rotation_range=15,
width_shift_range=0.2,
height_shift_range=0.2,
horizontal_flip=TRUE,
rescale=1.0/255)
# get images from directory
train_flow <- flow_images_from_directory(
train_dir,
train_gen,
target_size=c(32,32),
batch_size=flow_batch_size,
class_mode="categorical"
)
# no augmentation on validation data
valid_gen <- image_data_generator(rescale=1.0/255)
valid_flow <- flow_images_from_directory(
valid_dir,
valid_gen,
target_size=c(32,32),
batch_size=flow_batch_size,
class_mode="categorical"
)
一旦数据生成器设置完毕,我们还将使用两个回调函数。回调函数允许你在执行特定数量的批次/周期后运行自定义代码。你可以编写自己的回调函数,或者使用一些预定义的回调函数。之前我们使用回调函数来记录指标,但在这里,回调函数将实现模型检查点和提前停止,这些通常在构建复杂深度学习模型时使用。
模型检查点用于将模型权重保存到磁盘。然后,你可以从磁盘加载模型到内存中,使用它来预测新的数据,或者你可以从保存到磁盘的地方继续训练模型。你可以在每个训练周期后保存权重,这在使用云资源并担心机器突然终止时可能非常有用。在这里,我们用它来保存到目前为止在训练过程中看到的最佳模型。每个训练周期后,它会检查验证损失,如果验证损失低于现有文件中的验证损失,则保存模型。
提前停止允许你在模型的性能不再提高时停止训练。有些人称之为一种正则化方法,因为提前停止可以防止模型过拟合。尽管它可以避免过拟合,但它与我们在第三章深度学习基础中看到的正则化技术(如 L1、L2、权重衰减和丢弃法)非常不同。使用提前停止时,你通常会允许模型继续训练几个周期,即使性能不再提升,停止训练前允许的周期数在 Keras 中被称为耐心。在这里,我们将其设置为 10,也就是说,如果模型在 10 个周期内没有提升,我们将停止训练。以下是我们将在模型中使用的回调函数代码:
# call-backs
callbacks <- list(
callback_early_stopping(monitor="val_acc",patience=10,mode="auto"),
callback_model_checkpoint(filepath="cifar_model.h5",mode="auto",
monitor="val_loss",save_best_only=TRUE)
)
这是训练模型的代码:
# train the model using the data generators and call-backs defined above
history <- model %>% fit_generator(
train_flow,
steps_per_epoch=as.integer(num_train/flow_batch_size),
epochs=100,
callbacks=callbacks,
validation_data=valid_flow,
validation_steps=as.integer(num_valid/flow_batch_size)
)
这里有一点需要注意的是,我们必须管理训练和验证生成器的每个周期的步数。当你设置一个生成器时,你并不知道实际有多少数据,所以我们需要为每个周期设置步数。这只是记录数除以批次大小的结果。
该模型在 GPU 上训练应少于一小时,而在 CPU 上训练则会显著更长。随着模型的训练,最佳模型会保存在cifar_model.h5文件中。我机器上最佳的结果是在第 64 轮时,验证准确率约为 0.80。此后,模型继续训练了另外 10 个轮次,但未能提升性能。以下是训练指标的图表:

图 11.2:模型训练期间的输出指标
使用已保存的深度学习模型
现在我们已经构建了深度学习模型,可以重新启动 R 并从磁盘重新加载模型。本节的代码位于Chapter11/use_cifar10_model.R文件夹中。我们将使用以下代码加载上一节中创建的模型:
library(keras)
# load model trained in build_cifar10_model.R
model <- load_model_hdf5("cifar_model.h5")
我们将使用该模型为验证集中的图像文件生成预测。我们将选择验证文件夹中的第一个目录,然后从该文件夹中选择第 7 个文件。我们加载图像并对其进行与训练期间相同的预处理,即通过将像素值除以 255.0 来对数据进行归一化处理。以下是加载图像并生成预测的代码:
> valid_dir <-"../data/cifar_10_images/data1/valid/"
> first_dir <- list.dirs(valid_dir, full.names=FALSE, recursive=FALSE)[1]
> valid_dir <- paste(valid_dir,first_dir,sep="")
> img_path <- paste(valid_dir,list.files(valid_dir)[7],sep="/")
# load image and convert to shape we can use for prediction
> img <- image_load(img_path, target_size = c(32,32))
> x <- image_to_array(img)
> x <- array_reshape(x, c(1, dim(x)))
> x <- x / 255.0
> preds <- model %>% predict(x)
> preds <- round(preds,3)
> preds
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8]
[1,] 0.997 0 0 0 0 0 0 0.003
模型预测输入来自第一类,且置信度为 99.7%。由于我们选择了验证集中的第一个目录,预测是正确的。
我们将使用模型做的最后一件事是评估它在图像文件目录上的表现。我们还将展示如何为整个图像文件目录生成预测。该代码通过数据生成器加载来自目录的图像,类似于我们训练模型的方式。以下是评估并使用模型对我们保存到磁盘的验证图像进行类别预测的代码:
> valid_dir <-"../data/cifar_10_images/data1/valid/"
> flow_batch_size <- 50
> num_valid <- 8000
>
> valid_gen <- image_data_generator(rescale=1.0/255)
> valid_flow <- flow_images_from_directory(
valid_dir,
valid_gen,
target_size=c(32,32),
batch_size=flow_batch_size,
class_mode="categorical"
)
>
> evaluate_generator(model,valid_flow,
steps=as.integer(num_valid/flow_batch_size))
$`loss`
[1] 0.5331386
$acc
[1] 0.808625
验证准确率为80.86%,这与我们在模型训练过程中观察到的相似,确认了模型已正确保存到磁盘。以下是为所有 8,000 个验证图像生成预测的代码:
> preds <- predict_generator(model,valid_flow,
steps=as.integer(num_valid/flow_batch_size))
> dim(preds)
[1] 8000 8
> # view the predictions,
> preds <- round(preds,3)
> head(preds)
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8]
[1,] 0.000 0.000 0.000 0.000 0.000 0.000 0.999 0.001
[2,] 0.000 0.007 0.001 0.002 0.990 0.000 0.000 0.000
[3,] 0.000 0.855 0.069 0.032 0.021 0.017 0.002 0.002
[4,] 0.134 0.001 0.000 0.000 0.000 0.000 0.001 0.864
[5,] 0.011 0.064 0.057 0.226 0.051 0.515 0.004 0.073
[6,] 0.108 0.277 0.135 0.066 0.094 0.091 0.052 0.179
我们可以看到,预测输出有 8,000 行和 8 列,因此对于每个验证图像,每个类别都有一个概率。我们可以看到每行的总和为 1.0,并且通常会有一个类别的概率显著较大。例如,模型预测第一张图像属于第 7 类,概率为 99.9%。
我们现在已经建立了一个完整的图像分类解决方案,使用图像文件。只要图像数据存储在相同的目录结构中,这个模板就可以重复用于其他任务。如果新的任务有不同数量的类别,那么您只需要更改最后一个全连接层中的节点数,可能还需要修改 softmax 激活函数。然而,如果您有一个新的图像分类任务,涉及现实生活中的图像,那么使用现有模型并进行迁移学习可能会获得更好的结果。在我解释如何操作之前,我将提供一些关于 ImageNet 数据集的背景信息,它通常用于训练复杂的模型,然后这些模型可以用于迁移学习。
ImageNet 数据集
从 2010 年开始,举办了一年一度的图像分类比赛,名为 ImageNet 大规模视觉识别挑战(ILSVRC)。该图像集包含超过 1400 万张已标记了 1000 多个类别的图像。如果没有这个数据集,今天深度学习的巨大关注度将不会出现。它通过竞赛激发了深度学习领域的研究。然后,在 ImageNet 数据集上学习到的模型和权重通过迁移学习被应用于成千上万的其他深度学习模型。ImageNet 的实际历史是一个有趣的故事。以下链接(qz.com/1034972/the-data-that-changed-the-direction-of-ai-research-and-possibly-the-world/)解释了这个项目最初并未受到关注,但随着一系列相关事件的发展,情况发生了变化:
-
ILSVRC 成为了研究人员图像分类的基准。
-
NVIDIA 发布了允许访问图形处理单元(GPU)的库。GPU 设计用于执行大规模并行矩阵运算,而这正是构建深度神经网络所需的。
-
多伦多大学的 Geoffrey Hinton、Ilya Sutskever 和 Alex Krizhevsky 创建了一个名为 AlexNet 的深度卷积神经网络架构,并在 2012 年赢得了比赛。尽管这不是卷积神经网络的首次应用,但他们的提交大大超过了下一个方法。
-
研究人员注意到,当他们使用 ImageNet 数据集训练模型时,能够将其应用于其他分类任务。他们几乎总是发现,使用 ImageNet 模型并进行迁移学习,比从头开始在原始数据集上训练模型要获得更好的性能。
图像分类的进展可以通过 ILSVRC 竞赛中的一些重要条目进行追踪:
| 团队 | 年份 | 错误率 |
|---|---|---|
| 2011 年 ILSVRC 冠军(非深度学习) | 2011 | 25.8% |
| AlexNet(7 层) | 2012 | 15.3% |
| VGG Net(16 层) | 2014 | 7.32% |
| GoogLeNet / Inception(19 层) | 2014 | 6.67% |
| ResNet(152 层) | 2015 | 3.57% |
VGGNet、Inception 和 Resnet 都可以在 Keras 中使用。可以在keras.rstudio.com/reference/index.html#section-applications找到可用网络的完整列表。
这些网络的模型可以在 Keras 中加载,并用来将新图像分类到 ImageNet 的 1,000 个类别之一。我们接下来会讨论这个问题。如果你有一个新的分类任务,并且使用不同的图像集,你也可以使用这些网络,然后使用迁移学习,我们将在本章后面讨论迁移学习。类别的数量可以不同;你不需要为你的任务拥有 1,000 个类别。
也许最简单的模型是 VGGNet,因为它与我们在第五章中看到的使用卷积神经网络进行图像分类并没有太大区别。
加载现有模型
在本节中,我们将加载一个现有的模型(VGGNet)并用它来分类一张新图片。本节的代码可以在Chapter11/vgg_model.R中找到。我们将从加载模型并查看其架构开始:
> library(keras)
> model <- application_vgg16(weights = 'imagenet', include_top = TRUE)
> summary(model)
_________________________________________________________________________
Layer (type) Output Shape Param #
=========================================================================
input_1 (InputLayer) (None, 224, 224, 3) 0
_________________________________________________________________________
block1_conv1 (Conv2D) (None, 224, 224, 64) 1792
_________________________________________________________________________
block1_conv2 (Conv2D) (None, 224, 224, 64) 36928
_________________________________________________________________________
block1_pool (MaxPooling2D) (None, 112, 112, 64) 0
_________________________________________________________________________
block2_conv1 (Conv2D) (None, 112, 112, 128) 73856
_________________________________________________________________________
block2_conv2 (Conv2D) (None, 112, 112, 128) 147584
_________________________________________________________________________
block2_pool (MaxPooling2D) (None, 56, 56, 128) 0
_________________________________________________________________________
block3_conv1 (Conv2D) (None, 56, 56, 256) 295168
_________________________________________________________________________
block3_conv2 (Conv2D) (None, 56, 56, 256) 590080
_________________________________________________________________________
block3_conv3 (Conv2D) (None, 56, 56, 256) 590080
_________________________________________________________________________
block3_pool (MaxPooling2D) (None, 28, 28, 256) 0
_________________________________________________________________________
block4_conv1 (Conv2D) (None, 28, 28, 512) 1180160
_________________________________________________________________________
block4_conv2 (Conv2D) (None, 28, 28, 512) 2359808
_________________________________________________________________________
block4_conv3 (Conv2D) (None, 28, 28, 512) 2359808
_________________________________________________________________________
block4_pool (MaxPooling2D) (None, 14, 14, 512) 0
_________________________________________________________________________
block5_conv1 (Conv2D) (None, 14, 14, 512) 2359808
_________________________________________________________________________
block5_conv2 (Conv2D) (None, 14, 14, 512) 2359808
_________________________________________________________________________
block5_conv3 (Conv2D) (None, 14, 14, 512) 2359808
_________________________________________________________________________
block5_pool (MaxPooling2D) (None, 7, 7, 512) 0
_________________________________________________________________________
flatten (Flatten) (None, 25088) 0
_________________________________________________________________________
fc1 (Dense) (None, 4096) 102764544
_________________________________________________________________________
fc2 (Dense) (None, 4096) 16781312
_________________________________________________________________________
predictions (Dense) (None, 1000) 4097000
=========================================================================
Total params: 138,357,544
Trainable params: 138,357,544
Non-trainable params: 0
_________________________________________________________________________
这个模型看起来很复杂,但当你仔细看时,实际上没有什么是我们之前没有见过的。它有两个包含两个卷积层的模块,后面跟着一个最大池化层。然后是三个包含三个卷积层的模块,后面也跟着一个最大池化层。最后,我们有一个扁平层和三个全连接层。最后一个全连接层有 1,000 个节点,这是 ImageNet 数据集中类别的数量。
让我们使用这个模型对一张新图像进行预测。这张图像是一辆自行车,尽管它有些不寻常——它是一辆计时赛自行车:

图 11.3:分类测试图像
以下代码块将图像处理成适合在 VGG 模型中使用的格式。它加载图像并将其调整为训练模型时使用的图像尺寸(224, 224)。然后,我们需要在调用predict函数之前对图像数据进行预处理。最后,Keras 中有一个名为imagenet_decode_predictions的辅助函数,我们可以用它来获取预测类别和概率:
> img_path <- "image1.jpg"
> img <- image_load(img_path, target_size = c(224,224))
> x <- image_to_array(img)
> x <- array_reshape(x, c(1, dim(x)))
> x <- imagenet_preprocess_input(x)
> preds <- model %>% predict(x)
> imagenet_decode_predictions(preds, top = 5)
[[1]]
class_name class_description score
1 n02835271 bicycle-built-for-two 0.31723219
2 n03792782 mountain_bike 0.16578741
3 n03891332 parking_meter 0.12548350
4 n04485082 tripod 0.06399463
5 n09193705 alp 0.04852912
最好的预测是bicycle-built-for-two,概率略高于 30%,第二好的预测是mountain_bike,概率为 16.5%。ImageNet 有三轮车和独轮车(甚至还有Model_T汽车!)的类别,但似乎没有自行车的类别,因此这个预测结果不算差。不过,mountain_bike可能是这个图像的更准确类别,因为它显然不是一辆双人自行车!
迁移学习
深度学习相较于传统机器学习的一个少数缺点是它需要大量数据。迁移学习是克服这一问题的一种方式,方法是使用一个之前训练好的模型的权重(通常是训练在 ImageNet 数据上的)并将其应用到新的问题集上。
ImageNet 数据集包含 1,500 万张图片,分为 1,000 个类别。由于我们可以复用已在如此大量数据上训练过的模型的部分内容,可能只需每个类别几百张图片就能训练新模型。这取决于新数据与原始模型训练数据的相关性。例如,尝试将 ImageNet 模型(其训练数据为照片)上的迁移学习应用到其他领域的数据(例如,卫星图像或医学扫描)上,将会更加困难,并且需要更多的数据。我们在第六章中关于不同数据源的讨论,也同样适用。如果数据来自不同类型的数据分布,例如,移动设备拍摄的图像、偏离中心的照片、不同的光照条件等,这也会产生影响。这时,通过数据增强创建更多合成数据将会起到很大的作用。
现在,我们将应用迁移学习,使用在构建深度学习模型部分中构建的模型。回想一下,在构建和评估该模型时,我们仅使用了 8/10 的类别。现在我们将使用迁移学习构建一个新模型,用于区分剩下的 2 个类别。本部分的代码可以在Chapter11/cifar_txr.R文件夹中找到:
- 我们将使用前一部分中构建的模型,并通过以下代码加载它:
library(keras)
# load model trained in build_cifar10_model.R
model <- load_model_hdf5("cifar_model.h5")
- 接下来,我们将调用模型对象上的
trainable_weights来获取可训练层的数量。这将计算模型中所有非激活层的数量。
> length(model$trainable_weights)
[1] 14
-
接下来,我们将冻结模型中的早期层。冻结模型中的层意味着在反向传播过程中这些层的权重不会更新。我们冻结卷积块,但不冻结模型末尾的全连接层。我们使用在模型定义中设置的名称来指定冻结的第一层和最后一层。
-
然后我们再次调用模型的
trainable_weights,以确认数量已从之前的14变为6。这是冻结层的代码:
freeze_weights(model,from="conv1", to="drop2")
length(model$trainable_weights)
[1] 6
- 接下来,我们将通过在以下代码中调用
pop_layer函数两次,从模型中移除最后的全连接层和激活层。我们需要这么做,因为我们的新任务有 2 个类别,而不是 8 个:
# remove the softmax layer
pop_layer(model)
pop_layer(model)
- 现在,我们可以通过以下代码添加一个包含 2 个节点的新层(因为在新任务中我们有 2 个类别):
# add a new layer that has the correct number of nodes for the new task
model %>%
layer_dense(name="new_dense",units=2, activation='softmax')
summary(model)
- 以下代码块会重新编译模型并设置生成器以加载数据。这与我们在构建模型时所看到的类似。不同之处在于这里没有使用数据增强:
# compile the model again
model %>% compile(
loss = "binary_crossentropy",
optimizer="adam",
metrics=c('accuracy')
)
# set up data generators to stream images to the train function
data_dir <- "../data/cifar_10_images/"
train_dir <- paste(data_dir,"data2/train/",sep="")
valid_dir <- paste(data_dir,"data2/valid/",sep="")
# in CIFAR10, # there are 50000 images in training set
# and 10000 images in test set
# but we are only using 2/10 classes,
# so its 10000 train and 2000 validation
num_train <- 10000
num_valid <- 2000
flow_batch_size <- 50
# no data augmentation
train_gen <- image_data_generator(rescale=1.0/255)
# get images from directory
train_flow <- flow_images_from_directory(
train_dir,
train_gen,
target_size=c(32,32),
batch_size=flow_batch_size,
class_mode="categorical"
)
# no augmentation on validation data
valid_gen <- image_data_generator(rescale=1.0/255)
valid_flow <- flow_images_from_directory(
valid_dir,
valid_gen,
target_size=c(32,32),
batch_size=flow_batch_size,
class_mode="categorical"
)
- 最后,我们可以通过以下代码训练模型:
> history <- model %>% fit_generator(
+ train_flow,
+ steps_per_epoch=as.integer(num_train/flow_batch_size),
+ epochs=10,
+ validation_data=valid_flow,
+ validation_steps=as.integer(num_valid/flow_batch_size)
+ )
Found 10000 images belonging to 2 classes.
Found 2000 images belonging to 2 classes.
Epoch 1/10
200/200 [==============================] - 5s 27ms/step - loss: 0.3115 - acc: 0.8811 - val_loss: 0.1529 - val_acc: 0.9425
Epoch 2/10
200/200 [==============================] - 4s 20ms/step - loss: 0.1971 - acc: 0.9293 - val_loss: 0.1316 - val_acc: 0.9550
Epoch 3/10
200/200 [==============================] - 4s 20ms/step - loss: 0.1637 - acc: 0.9382 - val_loss: 0.1248 - val_acc: 0.9540
Epoch 4/10
200/200 [==============================] - 4s 20ms/step - loss: 0.1367 - acc: 0.9497 - val_loss: 0.1200 - val_acc: 0.9575
Epoch 5/10
200/200 [==============================] - 4s 20ms/step - loss: 0.1227 - acc: 0.9543 - val_loss: 0.1148 - val_acc: 0.9605
Epoch 6/10
200/200 [==============================] - 4s 20ms/step - loss: 0.1161 - acc: 0.9559 - val_loss: 0.1110 - val_acc: 0.9625
Epoch 7/10
200/200 [==============================] - 4s 20ms/step - loss: 0.1022 - acc: 0.9622 - val_loss: 0.1118 - val_acc: 0.9620
Epoch 8/10
200/200 [==============================] - 4s 20ms/step - loss: 0.0996 - acc: 0.9655 - val_loss: 0.1068 - val_acc: 0.9645
Epoch 9/10
200/200 [==============================] - 4s 20ms/step - loss: 0.0861 - acc: 0.9687 - val_loss: 0.1095 - val_acc: 0.9655
Epoch 10/10
200/200 [==============================] - 4s 20ms/step - loss: 0.0849 - acc: 0.9696 - val_loss: 0.1189 - val_acc: 0.9620
最佳的准确率出现在第 9 个训练周期(epoch),当时我们得到了 96.55% 的准确率。这比我们在多分类模型中得到的准确率(大约 81%)要高得多,但二分类任务比多分类任务要容易得多。我们还可以看到,模型的训练速度非常快,因为它只需要更新最后几层的权重。
部署 TensorFlow 模型
从历史上看,使用 R 进行数据科学项目的一个被认为的缺点是,部署在 R 中构建的机器学习模型的难度。这通常意味着公司主要将 R 用作原型设计工具,先构建模型,然后将其用 Java 或 .NET 等其他语言重写。这也是公司转向使用 Python 进行数据科学的主要原因之一,因为 Python 具有更多的粘合代码,使得它可以与其他编程语言进行接口对接。
幸运的是,这种情况正在发生变化。RStudio 发布了一个名为 RStudio Connect 的有趣新产品,它允许公司创建一个平台,用于共享 R-Shiny 应用程序、R Markdown 报告、仪表盘和模型。这使得公司能够通过 REST 接口提供机器学习模型。
本书中创建的 TensorFlow(以及 Keras)模型可以部署,而不依赖于 R 或 Python 的任何运行时环境。一种方式是使用 TensorFlow Serving,它是一个开源软件库,用于为 TensorFlow 模型提供服务。另一种选择是使用我们在第十章“在云端运行深度学习模型”中看到的 Google CloudML 接口。这使你可以创建一个公开的 REST API,供你的应用程序调用。TensorFlow 模型也可以部署到 iPhone 和 Android 手机上。
在生产环境中有两种基本的评分模型选项:
-
批处理模式:在批处理模式下,一组数据在离线状态下进行评分,预测结果会被存储并在其他地方使用。
-
实时模式:在实时模式下,数据会立即进行评分,通常是一次处理一条记录,并且结果会立即使用。
对于许多应用程序来说,批处理模式已经足够。你应该仔细考虑自己是否真的需要实时预测系统,因为它需要更多的资源并且需要持续监控。批量处理记录比分别处理要高效得多。批处理模式的另一个优势是你事先知道应用程序的需求,可以相应地规划资源。而实时系统中,需求激增或拒绝服务攻击可能会导致预测模型出现问题。
我们已经在本章的使用已保存的深度学习模型部分看到过批量模式的应用。因此,让我们看看如何构建一个 REST 接口,用于实时从深度学习模型获取新数据的预测。这将使用tfdeploy包。本节的代码可以在Chapter11/deploy_model.R中找到。我们将基于 MNIST 数据集构建一个简单的模型,然后创建一个 web 接口,我们可以在其中提交一张新的图片进行分类。以下是构建模型并打印测试集前 5 行预测结果的代码第一部分:
library(keras)
#devtools::install_github("rstudio/tfdeploy")
library(tfdeploy)
# load data
c(c(x_train, y_train), c(x_test, y_test)) %<-% dataset_mnist()
# reshape and rescale
x_train <- array_reshape(x_train, dim=c(nrow(x_train), 784)) / 255
x_test <- array_reshape(x_test, dim=c(nrow(x_test), 784)) / 255
# one-hot encode response
y_train <- to_categorical(y_train, 10)
y_test <- to_categorical(y_test, 10)
# define and compile model
model <- keras_model_sequential()
model %>%
layer_dense(units=256, activation='relu', input_shape=c(784),name="image") %>%
layer_dense(units=128, activation='relu') %>%
layer_dense(units=10, activation='softmax',name="prediction") %>%
compile(
loss='categorical_crossentropy',
optimizer=optimizer_rmsprop(),
metrics=c('accuracy')
)
# train model
history <- model %>% fit(
x_train, y_train,
epochs=10, batch_size=128,
validation_split=0.2
)
preds <- round(predict(model, x_test[1:5,]),0)
head(preds)
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
[1,] 0 0 0 0 0 0 0 1 0 0
[2,] 0 0 1 0 0 0 0 0 0 0
[3,] 0 1 0 0 0 0 0 0 0 0
[4,] 1 0 0 0 0 0 0 0 0 0
[5,] 0 0 0 0 1 0 0 0 0 0
这段代码没有什么新颖之处。接下来,我们将为测试集中的一张图片文件创建一个 JSON 文件。JSON 代表 JavaScript 对象表示法,是用于序列化和通过网络连接发送数据的公认标准。如果 HTML 是计算机与人类之间的网页通信语言,那么 JSON 则是计算机之间的网页通信语言。它在微服务架构中被广泛使用,微服务架构是一种通过许多小型 web 服务构建复杂 web 生态系统的框架。JSON 文件中的数据必须应用与训练过程中相同的预处理方式——因为我们对训练数据进行了归一化处理,因此也必须对测试数据进行归一化处理。以下代码会创建一个包含测试集第一条数据值的 JSON 文件,并将文件保存为json_image.json:
# create a json file for an image from the test set
json <- "{\"instances\": [{\"image_input\": ["
json <- paste(json,paste(x_test[1,],collapse=","),sep="")
json <- paste(json,"]}]}",sep="")
write.table(json,"json_image.json",row.names=FALSE,col.names=FALSE,quote=FALSE)
现在我们有了一个 JSON 文件,让我们为我们的模型创建一个 REST web 接口:
export_savedmodel(model, "savedmodel")
serve_savedmodel('savedmodel', browse=TRUE)
完成此操作后,应该会弹出一个类似于以下内容的新网页:

图 11.4:TensorFlow 模型 REST web 服务的 Swagger UI
这是一个显示 TensorFlow 模型 RESTful web 服务的 Swagger UI 页面。这允许我们测试 API。虽然我们可以尝试使用这个接口,但使用我们刚刚创建的 JSON 文件会更为简便。打开你机器上的命令提示符,浏览到Chapter11代码目录,并运行以下命令:
curl -X POST -H "Content-Type: application/json" -d @json_image.json http://localhost:8089/serving_default/predict
你应该得到以下响应:

REST web 接口会返回另一个包含这些结果的 JSON 字符串。我们可以看到列表中的第 8 项为 1.0,其他所有数字都非常小。这与我们在本节开始时在代码中看到的第一行的预测结果相匹配。
我想阅读这篇文章的人一半会非常激动,另一半则毫不关心!那一半真正感兴趣的人可以看到如何使用 R 来提供与 web 应用程序接口的模型预测。这为使用 R 打开了巨大的可能性,而之前人们认为你必须使用 Python,或者必须用其他语言重新开发模型。那一半不感兴趣的人可能从未与这些 R 相关问题打过交道,但随着时间的推移,他们也会意识到这有多重要!
其他深度学习话题
深度学习中有两个备受关注的话题,生成对抗网络(GANs)和强化学习。我们这里只简要介绍这两个话题,本节没有代码,原因有两个。首先,这两个话题都非常高级,想要创建一个非平凡的应用场景需要为每个话题写上几章。其次,强化学习在 R 中的支持不够好,因此创建一个示例会很困难。尽管如此,我还是将这两个话题纳入本书,因为我认为它们是深度学习中重要的前沿领域,你绝对应该了解它们。
生成对抗网络
生成对抗网络被深度学习领域最杰出的人物之一 Yann LeCunn 称为自从面包被切片以来最酷的东西。如果他这么认为,那我们都应该引起注意!
本书中的大多数模型都是判别模型,也就是说,我们试图将一种类别与另一种类别区分开来。然而,在第九章《异常检测与推荐系统》中,我们在异常检测的应用场景中创建了一个生成模型。这个模型可以生成新的数据,尽管是输入数据的不同表示形式。创建复杂的生成模型是深度学习中的一个非常热门的研究话题。许多人认为生成模型能够解决深度学习中的许多问题,其中之一就是缺乏正确标注的数据。然而,在生成对抗网络(GANs)出现之前,很难判断一个生成模型到底有多好。由 Ian Goodfellow 领导的一组研究人员提出了生成对抗网络(GANs)(Goodfellow, Ian, et al. Generative adversarial nets. Advances in neural information processing systems. 2014),它可以用来生成逼真的人工数据。
在 GANs 中,两个模型一起训练,第一个是生成模型 G,它生成新的数据。第二个模型是判别模型 D,它尝试预测一个示例是否来自真实数据集,或者是由生成模型 G 生成的。基本的 GAN 思想是,生成模型试图“欺骗”判别模型,而判别模型则必须尝试区分假数据和真实数据。生成器不断生成新数据并改进其过程,直到判别模型无法再区分生成的数据和真实的训练数据。
在论文中,这个过程被比作一群伪钞制造者在创造假币(生成模型),以及试图检测伪币的警察(判别模型)。这两个模型逐步改进,直到无法区分假币和真钱。
GAN 的训练通常非常困难。一篇记录了在图像数据上训练 GAN 的有效方法的论文,将他们的方法称为深度卷积生成对抗网络(Radford, Alec, Luke Metz, 和 Soumith Chintala. 无监督表示学习与深度卷积生成对抗网络。arXiv 预印本 arXiv:1511.06434(2015))。在这篇论文中,他们推荐了一些训练稳定深度卷积生成对抗网络(DCGAN)的指导原则:
-
将所有池化层替换为步幅卷积(判别器)和分数步幅卷积(生成器)。
-
对两个模型都使用批归一化(batchnorm)。
-
移除深度架构中的全连接隐藏层。
-
对生成器来说,在输出层使用 tanh 激活函数,其他地方使用 ReLU。
-
对于判别器,所有层使用 LeakyReLU 激活函数。
训练 DCGAN 是一个迭代过程,以下步骤会重复进行:
-
首先,生成器创建一些新的示例。
-
判别器使用真实数据和生成数据进行训练。
-
在判别器训练完成后,两个模型一起训练。判别器的权重被冻结,但其梯度会在生成器模型中使用,以便生成器可以更新它的权重。
在这个循环中,至关重要的是一个模型不能主导另一个模型,它们应该一起改进。如果判别器过于聪明,并且非常确信生成器的实例是假的,那么就不会有信号传递回生成器,它将无法再改进。同样,如果生成器找到了一个聪明的技巧来欺骗判别器,它可能会生成过于相似的图像,或者仅属于一个输入类别,GAN 将再次无法改进。这展示了训练任何 GAN 的难度,你必须找到一组适用于数据的参数,并保持两个模型同步。来自 DCGAN 论文作者之一的关于如何使 GAN 工作的一条良好参考是github.com/soumith/ganhacks。
GAN 有许多潜在的应用场景,包括能够用更少的数据进行训练。它们还可以用来预测缺失的数据,例如给模糊的图像/视频添加定义。它们也可以在强化学习中应用,我们将在下一节中讨论。
强化学习
强化学习有一个看似简单的定义:一个智能体与环境进行交互,并根据其行为的后果改变自己的行为。这实际上就是人类和动物在现实世界中的行为方式,也正是许多人认为强化学习是实现人工通用智能(AGI)的关键原因。
如果计算机能够像人类一样执行复杂任务,那么人工通用智能(AGI)将实现。这也要求计算机能够像人类一样,将当前的知识应用于新问题。专家们对 AGI 是否可能实现存在分歧。如果我们参考第一章,《深度学习入门》中的第一张图片,我们可以看到,人工智能的定义(...当由人类执行时,表现出需要智能的功能)与强化学习的定义非常相似:

图 11.5:人工智能、机器学习和深度学习之间的关系
大卫·西尔弗(David Silver),强化学习领域最杰出的人物之一,以及参与 AlphaGo 的主要人物之一,提出了以下公式:
人工智能 = 强化学习 + 深度学习
强化学习的一个著名例子是一个算法,它仅通过将图像像素作为输入,能够比大多数人玩多个 Atari 2600 视频游戏更好。这个强化学习算法通过玩游戏数千次,甚至可能是数百万次,学习它需要采取什么行动来获得奖励,奖励可能是收集积分或尽可能长时间保持角色的生命。强化学习中最著名的例子或许就是 AlphaGo,它击败了世界上最强的围棋选手之一。AlphaGo 是一个混合人工系统,由神经网络、强化学习和启发式搜索算法组成。编程让计算机在围棋中获胜比在其他游戏(如国际象棋)中更难,因为暴力破解方法不可行。围棋的另一个问题是评估当前局势的困难。
强化学习的正式定义是,代理在时间步长 t 观察到状态 s[t]。当处于状态 s[t] 时,代理通过采取行动与环境进行交互,这意味着代理将过渡到新的状态 s[t+1]。进入新状态与奖励相关联,代理的目标是学习一个能够最大化期望奖励的策略。奖励可以是累积的和/或折扣的;例如,近时间的奖励比远期的回报更有价值。价值函数是对未来奖励的预测。如果新状态 s[t+1] 仅依赖于先前的状态 s[t] 和动作 a[t],那么它就成为了马尔可夫过程。然而,强化学习中的一个主要问题是奖励可能是稀疏的,并且可能在采取行动和获得奖励之间存在较长的延迟。还有一个问题是,直接的奖励可能会导致代理走上一条最终会导致破坏的路径。例如,在一款电子游戏中,代理可能会采取直接的步骤来最大化得分,但这最终意味着角色会更早死亡。在更接近现实生活的场景中,例如自动驾驶汽车,如果目标是快速到达某个地点,那么代理可能决定采取危险驾驶,进而把乘客和其他道路使用者置于风险之中。
强化学习中的核心元素包括以下内容:
-
奖励是代理在短期内能够获得的收益。
-
价值函数是代理从当前状态中能够期望获得的奖励。价值函数关注长期的奖励/目标,因此这可能意味着采取一些在短期内不最大化奖励的行动。
-
策略指导代理可以采取的行动,它将状态映射到从该状态可能采取的动作。
-
模型是代理与环境交互的封装。因此,它是对物理世界的一个不完整表示,但只要它能够在给定某个动作后准确地模拟下一步,并计算奖励,那么它就是一个足以用于强化学习的充分表示。
强化学习中的其他重要机制包括多标签分类、记忆、无监督学习、知识转移(使用从一个问题中学到的知识来解决相关问题)、搜索(通过查看所有可能的排列,x步后选择最佳的下一步行动)、多代理强化学习(multi-agent RL)和学习如何学习(learning to learn)。我们不会深入讨论这些任务,因为其中一些你可能已经熟悉。然而,这个清单确实突显了强化学习所涉及的复杂性。
深度学习可以作为强化学习中的一个组成部分,处理子任务,如物体检测、语音识别、自然语言处理等。深度学习也可以是强化学习的关键组成部分,特别是当它用于强化学习中的核心要素——价值函数、策略和环境模型时。这就是所谓的深度强化学习(deep RL)。例如,通过在隐藏单元之间使用递归连接,Hausknecht 和 Stone 建立了一个深度递归 Q 网络(DRQN),能够预测计算机游戏Pong中球的速度。将深度学习与强化学习联系起来的另一个研究领域是模仿学习。在模仿学习中,智能体通过观察专家来学习。它尤其在奖励延迟且当前状态难以评估的情况下非常有用。但模仿学习可能代价高昂,因此一种方法是使用生成对抗网络(GANs)生成人工数据,用于强化学习。
尽管 AlphaGo 成功击败了围棋世界冠军,但它距离解决人工通用智能问题还相差甚远。DeepMind 是一个专注于人工智能的公司,汇集了强化学习、监督学习、树搜索功能的专家,以及巨大的硬件资源来解决一个单一问题。AlphaGo 的训练数据集包含了 3000 万个游戏状态,并模拟了数百万场游戏。击败世界顶级围棋选手的版本几乎使用了 2000 个 CPU 和 300 个 GPU。它在击败世界冠军之前,曾接受过欧洲冠军的指导,尽管早期版本确实先击败了他。然而,AlphaGo 只解决了一个问题,甚至不能推广到其他棋类游戏。因此,它离解决人工通用智能问题还远远不够。
对 AlphaGo 的较为诚实的评价来自 Andrej Karpathy,他是深度学习领域的杰出研究员,现为特斯拉人工智能部门的总监。在 AlphaGo 于 2017 年击败世界排名第一的选手后,他发布了一篇名为AlphaGo, in context的博客(medium.com/@karpathy/alphago-in-context-c47718cb95a5)。Karpathy 列出了围棋与其他人工智能任务相比的以下局限性:
-
该游戏是完全确定性的,即规则是固定的,且事先完全已知。相比之下,大多数现实世界的问题并非如此。
-
该游戏是完全可观察的,即所有参与方都知道完整信息,且没有隐藏变量或状态。
-
该游戏具有离散的动作空间,即有固定数量的可允许动作。
-
存在完美的模拟器,即可以在安全空间中模拟数百万个例子。现实生活中的人工智能没有这种能力。
-
该游戏相对较短。
-
存在历史数据集,来自以往的游戏
如果我们将自动驾驶汽车视为一个人工智能任务,那么它可能并不符合这些特性中的任何一个。
AlphaGo 与世界冠军对弈时的一个不寻常的特点是,有时它会跳过那些原本会占据棋盘空间的走法。作为人类,我们在玩游戏时,有时会渴望即时反馈,因此会做出一些走法来获得短期奖励。而 AlphaGo 被编程为赢得比赛,无论胜负差距如何,因此它在比赛中非常乐意跳过这些走法。令人有趣的是,一些围棋高手认为,通过研究 AlphaGo 的策略,他们可以提高自己的水平。我们已经走到了一个循环的尽头——人类试图模仿计算机的行为,而计算机的行为反过来又是根据人类的行为模型来设计的。
额外的深度学习资源
本节内容包含了一些建议,帮助你继续在深度学习领域的发展。我的第一个建议是确保你已经运行了本书中的所有代码示例。如果你只是读这本书而没有运行代码,你得到的收益将不到 50%。去完成这些示例,修改代码,试图超越我得到的结果,将 MXNet 代码改写为 Keras 代码,等等。
我强烈推荐 Andrew Ng 在 Coursera 上的深度学习专攻(www.coursera.org/specializations/deep-learning)。不幸的是,它是用 Python 编写的,但它仍然是一个很好的资源。在 Python 方面,还有 Jeremy Howard 在 fast.ai 上的两个优秀课程(www.fast.ai/)。这两个选择采取了截然不同的 approach:Coursera 上的深度学习专攻采用的是自下而上的方法,从理论到实践,而 fast.ai 的课程从一开始就展示实际例子,之后才讲解理论。
另一个优秀的资源是 Kaggle(www.kaggle.com/competitions)。Kaggle 举办竞赛,数据科学家们在机器学习竞赛中竞相争夺最佳成绩。这些任务中的许多都是计算机视觉任务。我对这些竞赛不是特别感兴趣,因为我认为它们忽略了数据集的准备和获取工作,也忽视了模型的部署方式。然而,Kaggle 有两个值得注意的特点:其 Kernels 和论坛/博客。Kernels 是其他人编写的 Python 或 R 脚本。这些脚本通常对机器学习任务有非常有趣的处理方式。非常值得关注一个竞赛,看看其他竞争者是如何解决这些问题的。第二个值得注意的特点是 Kaggle 的论坛/博客。在竞赛论坛上也会讨论一些有趣的方法,在每个竞赛结束后,通常会有一篇获胜者的博客,讨论他们获胜的策略。
回到 R,另一个非常棒的资源是 RStudio 网站。这些人做了很多出色的工作,保持 R 在数据科学和机器学习中的相关性。RStudio 把大量成果回馈到 R 生态系统中;例如,他们的首席科学家 Hadley Wickham 的优秀工作。RStudio 的创始人(J.J. Allaire)是 TensorFlow 和 Keras 的 R API 的作者。我们在本书中使用了他们的一些优秀工具,包括 RStudio IDE、RShiny、RMarkdown、tidy universe 包等等。以下是一些包含示例的链接,值得你查看:
我的最终建议是阅读研究论文。以下是一些可以开始的优秀论文:
-
Krizhevsky, Alex, Ilya Sutskever, 和 Geoffrey E. Hinton. 使用深度卷积神经网络进行 ImageNet 分类. 神经信息处理系统进展. 2012.
-
Szegedy, Christian, 等人. 深入卷积网络. Cvpr, 2015.
-
LeCun, Yann, 等人. 分类学习算法:手写数字识别比较. 神经网络:统计力学视角 261 (1995): 276.
-
Zeiler, Matthew D., 和 Rob Fergus. 可视化与理解卷积网络. 欧洲计算机视觉会议. Springer, Cham, 2014.
-
Srivastava, Nitish, 等人. Dropout:防止神经网络过拟合的简单方法. 机器学习研究期刊 15.1 (2014): 1929-1958.
-
Simonyan, Karen, 和 Andrew Zisserman. 用于大规模图像识别的超深卷积神经网络. arXiv 预印本 arXiv:1409.1556 (2014).
-
Szegedy, Christian, 等人. 深入卷积网络. IEEE 计算机视觉与模式识别会议论文集. 2015.
-
He, Kaiming, 等人. 用于图像识别的深度残差学习. IEEE 计算机视觉与模式识别会议论文集. 2016.
-
Goodfellow, Ian, 等人. 生成对抗网络. 神经信息处理系统进展. 2014.
-
LeCun, Yann, Yoshua Bengio, 和 Geoffrey Hinton. 深度学习. nature 521.7553 (2015): 436.
-
Goldberg, Yoav. 自然语言处理中的神经网络模型简介. 人工智能研究期刊 57 (2016): 345-420.
总结
在本章中,读者已经了解了一些先进的深度学习技术。首先,我们研究了一些图像分类模型,并回顾了一些历史模型。接下来,我们将一个已有的、带有预训练权重的模型加载到 R 中,并使用它来分类一张新的图像。我们还研究了迁移学习,它允许我们将已有的模型作为基础,基于此构建一个用于新数据的深度学习模型。我们构建了一个图像分类器模型,可以在图像文件上进行训练。这个模型还向我们展示了如何使用数据增强和回调,这些技术在许多深度学习模型中都有使用。最后,我们演示了如何在 R 中构建一个模型,并创建一个 REST 端点来提供预测 API,供其他应用程序或通过网络访问。
本书已经到了结尾,我真的希望它对你有所帮助。R 是一门很适合数据科学的语言,我相信它比主要的替代品 Python 更易于使用,并且能让你更快地开发出机器学习原型。现在,它已经支持 MXNet、Keras 和 TensorFlow 等一些优秀的深度学习框架,我相信 R 将继续成为数据科学家和机器学习从业者的一个优秀选择。


浙公网安备 33010602011771号