R-高级深度学习-全-
R 高级深度学习(全)
原文:
annas-archive.org/md5/b0829a246066963c1a7b9420aca8f1a9译者:飞龙
前言
深度学习是机器学习的一个分支,基于一套算法,试图对数据中的高层次抽象进行建模。使用 R 的高级深度学习将帮助你理解流行的深度学习架构及其变种,并提供实际示例。
本书将帮助你通过高级示例在 R 语言中应用深度学习算法。书中涵盖了神经网络模型的变种,如 ANN、CNN、RNN、LSTM 等,并采用专家级技术。通过阅读本书,你将使用 Keras-R、TensorFlow-R 等流行的深度学习库来实现人工智能模型。
本书适合谁阅读
本书面向数据科学家、机器学习从业者、深度学习研究人员和人工智能爱好者,旨在帮助他们通过 R 语言掌握并实现深度学习技术和算法。要求具有扎实的机器学习基础并具备 R 语言的实际应用能力。
本书内容概述
第一章,深度学习架构与技术回顾,概述了本书中涉及的深度学习技术。
第二章,多类分类的深度神经网络,讲解了如何将深度学习神经网络应用于二分类和多分类问题。通过使用客户流失数据集,涵盖了数据准备、独热编码、模型拟合、模型评估和预测等步骤。
第三章,回归问题中的深度神经网络,展示了如何为数值响应开发预测模型。通过波士顿房价示例,介绍了数据准备、模型创建、模型拟合、模型评估和预测等步骤。
第四章,图像分类与识别,通过一个易于跟随的示例,展示了使用 Keras 包应用深度学习神经网络进行图像分类和识别。涉及的步骤包括:探索图像数据、调整图像大小和形状、进行独热编码、构建顺序模型、编译模型、训练模型、评估模型、预测和使用混淆矩阵进行模型性能评估。
第五章,使用卷积神经网络进行图像分类,介绍了应用卷积神经网络(CNN)进行图像分类与识别的步骤,并通过一个易于跟随的实践示例进行讲解。CNN 是一种流行的深度神经网络,并被视为大规模图像分类的“黄金标准”。
第六章,使用 Keras 应用自编码神经网络,介绍了使用 Keras 应用自编码神经网络的步骤。所使用的实际例子展示了将图像作为输入,使用自编码器进行训练,并最终重建图像的步骤。
第七章,使用迁移学习进行小数据集的图像分类,说明了迁移学习在自然语言处理中的应用。涉及的步骤包括数据准备、在 Keras 中定义深度神经网络模型、训练模型以及模型评估。
第八章,使用生成对抗网络创建新图像,通过一个实际例子说明了生成对抗网络(GANs)在生成新图像中的应用。图像分类的步骤包括图像数据预处理、特征提取、开发 RBM 模型以及模型性能评估。
第九章,深度网络进行文本分类,提供了使用深度神经网络进行文本分类的步骤,并通过一个易于跟随的例子展示了该过程。文本数据,如客户评论、产品评价和电影评论,在商业中发挥着重要作用,而文本分类是一个重要的深度学习问题。
第十章,使用递归神经网络进行文本分类,通过一个实际例子提供了应用递归神经网络进行图像分类的步骤。所涵盖的步骤包括数据准备、定义递归神经网络模型、训练以及最终评估模型的性能。
第十一章,使用长短期记忆网络进行文本分类,说明了使用长短期记忆(LSTM)神经网络进行情感分类的步骤。涉及的步骤包括文本数据准备、创建 LSTM 模型、训练模型以及评估模型。
第十二章,使用卷积递归网络进行文本分类,说明了递归卷积网络在新闻分类中的应用。涉及的步骤包括文本数据准备、在 Keras 中定义递归卷积网络模型、训练模型以及模型评估。
第十三章,技巧、窍门与前进的道路,讨论了将深度学习付诸实践以及最佳实践的未来发展方向。
为了从本书中获得最大收益
以下是一些帮助你从本书中获得最大收益的建议:
本书中的所有示例都使用 R 语言代码。因此,在开始之前,你应具备扎实的 R 语言基础。正如孔子所说:“我听到,我忘记;我看到,我记得;我做了,我懂得。”这同样适用于本书。通过动手操作代码,边阅读章节,将有助于你理解深度学习模型。
本书中的所有代码都在一台拥有 8 GB 内存的 Mac 电脑上成功运行。然而,如果你使用的数据集比本书中用于示范的要大得多,为了开发深度学习模型,可能需要更强大的计算资源。此外,拥有良好的统计方法基础也会很有帮助。
下载示例代码文件
你可以从你的账户在 www.packt.com 下载本书的示例代码文件。如果你在其他地方购买了本书,你可以访问 www.packtpub.com/support 并注册,将文件直接通过电子邮件发送给你。
你可以通过以下步骤下载代码文件:
-
在 www.packt.com 上登录或注册。
-
选择“支持”选项卡。
-
点击“代码下载”。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
下载文件后,请确保使用最新版本的工具解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书的代码包也托管在 GitHub 上,地址是 github.com/PacktPublishing/Advanced-Deep-Learning-with-R。如果代码有更新,将会在现有的 GitHub 仓库中进行更新。
我们还提供了其他来自我们丰富书籍和视频目录的代码包,访问地址为github.com/PacktPublishing/。快来查看吧!
下载彩色图像
我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。你可以在这里下载:static.packt-cdn.com/downloads/9781789538779_ColorImages.pdf。
使用的约定
本书中使用了许多文本约定。
CodeInText:表示文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。以下是一个示例:“我们在拟合模型时将准确率和损失值存储在 model_three 中。”
代码块按如下方式设置:
model %>%
compile(loss = 'binary_crossentropy',
optimizer = 'adam',
metrics = 'accuracy')
粗体:表示新术语、重要词汇或你在屏幕上看到的词汇。例如,菜单或对话框中的单词以这种方式显示。以下是一个示例:“循环神经网络(RNNs)非常适合处理包含这些序列的数据。”
警告或重要说明将以这种方式显示。
提示和技巧如下所示。
与我们联系
我们始终欢迎读者的反馈。
一般反馈:如果你对本书的任何方面有疑问,请在邮件主题中提到书名,并通过customercare@packtpub.com与我们联系。
勘误:尽管我们已经尽力确保内容的准确性,但错误仍然可能发生。如果你发现本书中的错误,我们将感激你向我们报告。请访问www.packtpub.com/support/errata,选择你的书籍,点击勘误提交表单链接,并填写相关信息。
盗版:如果你在互联网上遇到我们作品的任何非法复制品,感谢你能提供其所在位置地址或网站名称。请通过copyright@packt.com联系我们,并附上相关材料的链接。
如果你有兴趣成为作者:如果你在某个领域有专长,并有意写作或参与书籍的编写,请访问authors.packtpub.com。
评论
请留下评论。阅读并使用本书后,为什么不在你购买本书的站点上留下评论呢?潜在的读者可以看到并利用你的公正意见来做出购买决策,Packt 也能了解你对我们产品的看法,而我们的作者则能看到你对他们书籍的反馈。谢谢!
欲了解更多关于 Packt 的信息,请访问packt.com。
第一部分:重温深度学习基础
本节包含一个章节,作为深度学习与 R 的入门介绍。它提供了开发深度网络的过程概述,并回顾了流行的深度学习技术。
本节包含以下章节:
- 第一章,重温深度学习架构与技术
第一章:重新审视深度学习架构和技术
深度学习是机器学习和人工智能领域的一个分支,使用人工神经网络。深度学习方法的主要优势之一是能够捕捉数据中复杂的关系和模式。当关系和模式不那么复杂时,传统的机器学习方法可能会表现良好。然而,随着帮助生成和处理越来越多非结构化数据(如图像、文本和视频)的技术的出现,深度学习方法变得越来越流行,几乎成为处理这些数据的默认选择。计算机视觉和自然语言处理(NLP)是两个领域,正在广泛应用于许多不同的领域,如无人驾驶汽车、语言翻译、计算机游戏,甚至是创造新的艺术作品。
在深度学习工具包中,我们现在拥有越来越多可以应用于特定任务的神经网络技术。例如,在开发图像分类模型时,一种被称为卷积神经网络(CNN)的特殊深度网络已被证明在捕捉图像相关数据中的独特模式方面非常有效。类似地,另一种流行的深度学习网络叫做递归神经网络(RNNs)及其变种,在处理包含词语或整数序列的数据时被发现非常有用。还有一种非常有趣的深度学习网络叫做生成对抗网络(GAN),它具备生成新图像、语音、音乐或艺术作品的能力。
在本书中,我们将使用这些以及其他流行的深度学习网络,使用 R 软件。每一章都展示了一个完整的示例,专门开发用于在普通的笔记本电脑或计算机上运行。主要的理念是避免在应用深度学习方法的初始阶段被大量需要高级计算资源的数据所困扰。你将能够通过书中展示的示例逐步理解所有步骤。所用示例还包括每个主题的最佳实践,你会发现它们非常有用。你还会发现,动手操作和应用方法有助于在面对新问题时,快速看到全貌并复现这些深度学习方法。
本章概述了本书中涵盖的 R 深度学习方法。我们将在本章中讨论以下主题:
-
《深度学习与 R》
-
开发深度网络模型的过程
-
使用 R 和 RStudio 的流行深度学习技术
《深度学习与 R》
我们将从探讨深度学习网络的流行程度开始,并且了解一些本书中使用的重要 R 包的版本。
深度学习趋势
深度学习技术使用基于神经网络的模型,近年来受到了越来越多的关注。Google 趋势网站关于深度学习搜索词的图表如下所示:

上面的图表显示了一个搜索词的流行度最高为 100,其他数字是相对于这一最高点的。可以观察到,自 2014 年左右以来,深度学习这个术语的兴趣逐渐上升,且在过去的两年中达到了流行的顶峰。深度学习网络流行的原因之一是开源免费库 TensorFlow 和 Keras 的可用性。
主要 R 包版本
本书将使用 Keras R 包,利用 TensorFlow 作为后端构建深度学习网络。下面的代码展示了一个典型的 R 会话输出,提供了与版本相关的各种信息,这些信息用于书中的示例:
# Information from a Keras R session
sessionInfo()
R version 3.6.0 (2019-04-26)
Platform: x86_64-apple-darwin15.6.0 (64-bit)
Running under: macOS 10.15
Matrix products: default
BLAS: /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib
LAPACK: /Library/Frameworks/R.framework/Versions/3.6/Resources/lib/libRlapack.dylib
Random number generation:
RNG: Mersenne-Twister
Normal: Inversion
Sample: Rounding
locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
attached base packages:
[1] stats graphics grDevices utils datasets methods base
other attached packages:
[1] keras_2.2.4.1
loaded via a namespace (and not attached):
[1] Rcpp_1.0.2 lattice_0.20-38 lubridate_1.7.4 zeallot_0.1.0
[5] grid_3.6.0 R6_2.4.0 jsonlite_1.6 magrittr_1.5
[9] tfruns_1.4 stringi_1.4.3 whisker_0.4 Matrix_1.2-17
[13] reticulate_1.13 generics_0.0.2 tools_3.6.0 stringr_1.4.0
[17] compiler_3.6.0 base64enc_0.1-3 tensorflow_1.14.0
如前所述,本书使用的是 2019 年 4 月发布的 R 语言 3.6 版本,其别名为“Planting of a Tree”。用于 Keras 包的版本是 2.2.4.1。此外,本书中所有应用示例均是在配备 8GB 内存的 Mac 计算机上运行的。使用此配置的主要原因是,它能让读者在不需要高级计算资源的情况下,顺利完成书中涉及的任何深度学习网络示例。
在下一节中,我们将详细讲解开发深度网络模型的过程,该过程分为五个一般步骤。
深度网络模型开发过程
开发深度学习网络模型可以分为五个关键步骤,如下图所示:

上述流程图中提到的每个步骤,其要求可能会根据所使用的数据类型、开发的深度学习网络类型以及模型开发的主要目标而有所不同。我们将逐一讲解每个步骤,以帮助读者对所涉及的内容有一个总体的了解。
准备深度网络模型的数据
开发深度学习神经网络模型要求变量具有一定的格式。自变量可能具有不同的尺度,有的变量值是小数,有的则是以千为单位。使用这种尺度不同的变量在训练网络时效率不高。在开发深度学习网络之前,我们会进行一些调整,使得变量具有相似的尺度。实现这一目标的过程称为标准化。
两种常用的标准化方法是 z-score 标准化和 min-max 标准化。在 z-score 标准化中,我们从每个值中减去均值,然后除以标准差。这种转换结果是数据值位于-3 到+3 之间,均值为 0,标准差为 1。而对于 min-max 标准化,我们从每个数据点中减去最小值,然后除以范围。这种转换将数据转换为 0 到 1 之间的值。
作为示例,请看以下图表,我们从一个均值为 35,标准差为 5 的正态分布中随机获取了 10,000 个数据点:

从前面的图表中,我们可以观察到,经过 z-score 标准化后,数据点大多数位于-3 到+3 之间。同样,经过 min-max 标准化后,值的范围变为 0 到 1 之间。然而,两种标准化方法后,原始数据的整体模式得以保留。
使用分类响应变量时,数据准备的另一个重要步骤是进行一热编码(one-hot encoding)。一热编码将分类变量转换为新的二进制格式,其中值为 0 或 1。可以通过使用 Keras 中提供的to_categorical()函数轻松实现这一转换。
通常,处理非结构化数据(如图像或文本)的数据处理步骤比处理结构化数据时更为复杂。此外,数据准备步骤的性质可能因数据类型而异。例如,为开发深度学习分类模型而准备图像数据的方式,很可能与为开发电影评论情感分类模型而准备文本数据的方式大不相同。然而,值得注意的一点是,在我们能够从非结构化数据开发深度学习模型之前,必须先将其转换为结构化格式。以下截图展示了如何将非结构化图像数据转换为结构化格式,使用的是手写数字五的图片:

从前面的截图中可以观察到,当我们在 R 中读取一个包含手写数字五的黑白图像文件,尺寸为 28 x 28 时,它会被转换为行列中的数字,形成结构化格式。截图的右侧显示了具有 28 行和 28 列的数据。表格中的数字是像素值,范围从 0 到 255,其中 0 表示黑色,255 表示白色。在开发深度学习模型时,我们会使用一些从图像数据中派生的此类结构化数据。
一旦为开发模型准备好的数据符合所需格式,我们就可以开发模型架构。
开发深度学习模型架构
开发模型的架构涉及定义各种项目,如网络的层类型和数量、激活函数的类型、网络中使用的单元或神经元数量,以及提供与数据相关的输入/输出值。以下代码展示了如何在 R 中使用 Keras 指定一个简单的顺序模型架构:
# Model architecture
model <- keras_model_sequential()
model %>%
layer_dense(units = 8, activation = 'relu', input_shape = c(21)) %>%
layer_dense(units = 3, activation = 'softmax')
请注意,顺序模型允许我们逐层开发模型。正如前面的代码所示,两个密集连接的网络层已被添加为顺序模型的一部分。选择模型架构时有两个重要决策,涉及层数和层的类型,以及激活函数的类型。使用多少层以及选择什么类型的层由数据的性质和复杂性决定。对于完全连接的网络(也称为多层感知器),我们可以使用 Keras 中的layer_dense函数来创建密集层。
另一方面,在处理图像数据时,我们很可能会在网络中使用卷积层,利用layer_conv_2d函数。我们将在每一章中通过示例讨论更多关于特定模型架构的细节。
在深度学习网络中使用了不同类型的激活函数。修正线性单元(relu)是一个流行的激活函数,通常用于隐藏层,并且它采用非常简单的计算方法。如果输入值为负数,则返回零;对于其他情况,则不改变原始值。举个例子,看看以下代码:
# RELU function and related plot
x <- rnorm(10000, 2, 10)
y <- ifelse(x<0, 0, x)
par(mfrow = c(1,2))
hist(x)
plot(x,y)
前面的代码生成了 10,000 个来自均值为 2、标准差为 10 的正态分布的随机数,并将结果存储在x中。然后将负值更改为零并存储在 y 中。以下图表展示了 x 的直方图和 x 与 y 的散点图:

从前面的直方图中可以观察到,x 的值既有正数也有负数。基于原始的 x 值和通过将负值转换为零后得到的修改后的 y 值,散点图可视化了relu激活函数的影响。在散点图中,x = 0 左侧的数据点是平坦的,具有零斜率。x = 0 右侧的数据点呈现完美的线性模式,斜率为 1。
使用relu激活函数的主要优势之一是其简单的计算方式。对于开发深度学习网络模型来说,这一点非常重要,因为它有助于减少计算成本。对于许多深度学习网络,修正线性单元(rectified linear unit)作为默认激活函数被广泛使用。
另一种用于开发深度网络的流行激活函数是softmax,通常用于网络的外层。让我们看一下以下代码,以更好地理解它:
# Softmax function and related plot
x <- runif(1000, 1, 5)
y <- exp(x)/sum(exp(x))
par(mfrow=c(1,2))
hist(x)
plot(x,y)
在前面的代码中,我们从一个均匀分布中随机抽取了 1,000 个值,这些值介于 1 和 5 之间。为了使用 softmax 函数,我们可以将每个输入值 x 的指数值除以所有 x 值指数的总和。根据 x 值生成的直方图以及 x 和 y 值的散点图如下所示:

我们可以观察到前面的直方图为 x 值提供了一个大致均匀的模式。可以从散点图中看到 softmax 函数的影响,此时输出值介于 0 和 1 之间。这种转换对于将结果以概率的形式进行解释非常有用,因为值现在如下所示:
-
介于 0 和 1 之间
-
这些概率的总和为 1
softmax 激活函数的这一方面,使得结果可以用概率的方式进行解释,因此在开发深度学习分类模型时,它成为了一个受欢迎的选择。无论是用于图像分类还是文本分类问题,它都表现良好。
除了这两个激活函数,我们还使用其他可能更适合特定深度学习模型的激活函数。
一旦指定了要使用的模型架构,下一步就是编译模型。
编译模型
编译模型通常涉及指定损失函数、选择优化器以及指定要使用的评估指标。然而,这些选择取决于所解决问题的类型。以下代码是一个使用 R 编译深度学习二元分类模型的示例:
model %>%
compile(loss = 'binary_crossentropy',
optimizer = 'adam',
metrics = 'accuracy')
前面指定的损失函数是 binary_crossentropy,当响应变量具有两个类别时使用该函数。二元交叉熵可以使用以下公式计算:

在上述公式中,y 代表实际类别,yhat 代表预测概率。我们来看两个使用以下代码的示例:
# Example-1
y <- c(0, 0, 0, 1, 1, 1)
yhat <- c(0.2, 0.3, 0.1, 0.8, 0.9, 0.7)
(loss <- - y*log(yhat) - (1-y)*log(1-yhat))
[1] 0.2231436 0.3566749 0.1053605 0.2231436 0.1053605 0.3566749
mean(loss)
[1] 0.228393
# Example-2
yhat <- c(0.2, 0.9, 0.1, 0.8, 0.9, 0.2)
(loss <- - y*log(yhat) - (1-y)*log(1-yhat))
[1] 0.2231436 2.3025851 0.1053605 0.2231436 0.1053605 1.6094379
mean(loss)
[1] 0.761505
如 Example-1 所示,y 共有六种情况,其中前三种情况的实际类别为 0,接下来的三种情况的实际类别为 1。yhat 捕获的预测概率是某个案例属于类别 1 的概率。在 Example-1 中,yhat 的值正确地分类了所有六个案例,所有损失值的平均值大约为 0.228。在 Example-2 中,yhat 只正确分类了四个案例,所有损失值的平均值现在增加到大约 0.762。以这种方式,二元交叉熵损失函数有助于评估模型的分类性能。损失值越低,分类性能越好;损失值越高,模型的分类性能越差。
根据深度学习网络开发的具体问题类型,还有各种其他损失函数。对于响应变量有多个类别的分类模型,我们使用categorical_crossentropy损失函数。对于具有数值型响应变量的回归问题,均方误差(mse)可能是一个合适的损失函数。
在指定模型使用的优化器时,adam是深度学习网络中常用的优化器,能够在各种场景中取得良好的效果。其他常用的优化器还包括rmsprop和adagrad。当训练深度学习网络时,网络的参数会根据从损失函数得到的反馈进行调整。如何修改这些参数是基于所使用的优化器。因此,选择一个合适的优化器对于得到合适的模型至关重要。
在编译模型时,我们还指定一个合适的度量标准,用于监控训练过程。对于分类问题,accuracy是最常用的度量标准之一。对于回归问题,均方误差是常用的度量标准。
一旦我们编译好模型,就可以开始拟合它了。
拟合模型
模型的拟合或训练是通过数据来进行的。下面是一个用于拟合分类模型的代码示例:
model %>%
fit(training,
trainLabels,
epochs = 200,
batch_size = 32,
validation_split = 0.2)
在前面的代码中,拟合模型包括training,它是自变量的数据,以及trainLabels,它包含响应变量的标签。迭代次数通过指定 epoch 数来表明,在训练过程中将使用所有训练数据样本的迭代次数。批量大小指的是从训练数据中选取的样本数量,模型参数将在这些样本处理后被更新。此外,我们还可以指定验证集拆分,0.2 或 20%的拆分意味着将训练数据的最后 20%样本与训练过程分开,用于评估模型的表现。
在拟合模型时,网络中的不同层有随机初始化的权重。由于网络权重的随机初始化,如果我们用相同的数据、相同的架构和相同的设置重新拟合模型,我们将会得到略有不同的结果。这不仅会在不同的 R 会话中发生,甚至在同一会话中重新训练模型时也会发生。
在许多情况下,获取可重复的结果非常重要。例如,在向同行评审的国际期刊发布与深度学习相关的文章时,你可能需要根据审稿人的反馈从同一个模型生成更多的图表。另一个情况是,团队中的成员可能希望共享一个模型以及相关的结果。获得相同结果的最简单方法是通过以下代码保存并重新加载模型:
# Save/reload model
save_model_hdf5(model,
filepath,
overwrite = TRUE,
include_optimizer = TRUE)
model_x <- load_model_hdf5(filepath,
custom_objects = NULL,
compile = TRUE)
我们可以通过指定filepath来保存模型,并在需要时重新加载。保存模型使我们在再次使用该模型时能够获得可重复的结果。它还使我们能够与他人共享相同的模型,以便他们获得完全相同的结果,并且在每次运行耗时较长的情况下尤为有用。保存并重新加载模型还可以让你在再次训练模型时恢复训练过程。
一旦模型拟合完成,可以使用训练数据和测试数据来评估其性能。
评估模型性能
评估深度学习分类模型的性能需要构建一个混淆矩阵,用以总结实际类别与预测类别之间的关系。假设有一个分类模型用于将研究生申请者分类为两个类别,其中类别 0 表示未被接受的申请,类别 1 表示被接受的申请。以下是该情况的混淆矩阵示例,用来解释关键概念:

在前面的混淆矩阵中,有 208 名实际上未被接受的申请者,模型也正确地预测了他们不应被接受。该单元格在混淆矩阵中被称为真负。类似地,有 29 名实际上被接受的申请者,模型也正确地预测了他们应该被接受。该单元格在混淆矩阵中被称为真正。我们还可以看到一些单元格,其中包含模型对申请者错误分类的数字。有 15 名实际上未被接受的申请者,但模型错误地预测他们应该被接受,这个单元格称为假负。
错误地将类别 0 误分类为类别 1 被称为类型 1 错误。最后,有 73 名实际上被接受的申请者,但模型错误地将他们预测为未接受类别,这个单元格被称为假正。这种错误分类的另一种说法是类型 2 错误。
从混淆矩阵中,我们可以通过将对角线上的数字相加并将其除以总数来计算分类性能的准确性。因此,基于前述矩阵的准确性为 (208+29)/(208+29+73+15),即 72.92%。除了准确性之外,我们还可以找出模型在正确分类每一类别上的表现。我们可以计算正确分类类别 1(也称为灵敏度)的准确性为 29/(29+73),即 28.4%。类似地,我们可以计算正确分类类别 0(也称为特异性)的准确性为 208/(208+15),即 93.3%。
请注意,混淆矩阵可用于开发分类模型。然而,其他情况可能需要其他合适的方法来评估深度学习网络。
现在,我们可以简要回顾一下本书中涵盖的深度学习技术。
使用 R 和 RStudio 的深度学习技术
深度学习中的“深度”一词指的是神经网络模型具有多个层次,并且学习过程依赖于数据的帮助。根据所使用的数据类型,深度学习可以分为两大类,如下图所示:

如前图所示,用于开发深度神经网络模型的数据类型可以是结构化数据或非结构化数据。在第二章《用于多类分类的深度神经网络》中,我们展示了使用结构化数据(响应变量为类别类型)来解决分类问题的深度学习网络。在第三章《用于回归的深度神经网络》中,我们展示了使用结构化数据(响应变量为连续类型)来解决回归问题的深度学习网络。第四章至第十二章展示了深度学习网络在处理主要涉及图像和文本的两种非结构化数据类型中的应用。在第四章至第八章中,我们提供了一些使用图像数据的流行深度学习网络的应用示例,图像数据被视为一种非结构化数据类型。最后,在第九章至第十二章中,我们介绍了一些适用于文本数据的流行深度学习网络,文本数据是非结构化数据中的另一大类别。
现在,让我们简要回顾一下第二章至第十二章中涉及的示例和技术。
多类分类
许多问题的主要目标是开发一个分类模型,使用数据将观察结果分类为两类或多类。例如,患者可以根据多个变量的数据被分类为正常、可疑或病理性。在这种情况下,深度学习网络将使用多个患者的数据(结果已知),并学习将患者分类为这三类之一。
另一个分类问题的例子可能是学生向研究生院提交申请。学生的申请可能基于 GPA、GRE 成绩和本科学校排名等变量被接受或拒绝。另一个有趣的例子是,使用学生相关数据来开发一个模型,帮助将第一年学生分类为那些可能留在当前学校的学生和那些可能转学的学生。类似的模型也可以用来分类那些可能继续与某个企业合作或转向竞争对手的客户。
开发分类模型时面临的挑战之一是类别不平衡。例如,在处理医疗数据时,被分类为正常的患者数量可能远大于被分类为病态的患者数量。类似地,在申请顶尖大学的研究生项目时,数据中很可能包含大量未被录取的申请者。深度网络模型在解决此类问题时非常有效。书中使用的 Keras 库提供了一个用户友好的界面,不仅可以轻松解决此类问题,还可以通过快速实验帮助获得合适的分类模型。
在第二章,多类分类的深度神经网络中,我们通过使用 R 语言展示了一个多类深度学习分类模型。
回归问题
包含数字响应变量的结构化数据被归类为回归问题。例如,一座城市中房屋的价格可能依赖于房屋的年龄、城市的犯罪率、房间数以及房产税率等变量。尽管统计方法,如多元线性回归和弹性网回归,在这些情况下也很有用,但深度学习网络具有一些优势。使用神经网络的主要优势之一是它们能够捕捉非线性。与需要满足特定假设条件才能使用的统计方法不同,基于神经网络的模型更加灵活,不需要满足太多假设条件即可使用。
许多涉及回归问题的应用程序也需要识别对响应变量有显著影响的变量或特征。然而,在深度学习网络中,这种特征工程是内建的,不需要额外的努力来提取重要特征。关于深度学习网络需要注意的一点是,所使用的数据集越大,最终的预测模型将会越有效。在第三章,回归问题的深度神经网络中,我们通过使用 R 语言展示了一个深度学习回归模型。
图像分类
图像数据被分类为非结构化数据类型。深度学习网络的一个流行应用是开发图像分类和识别模型。图像分类有许多应用,如智能手机或社交媒体网络上的面部识别、医学图像数据分类、手写数字分类以及自动驾驶汽车等。需要注意的是,无法直接从非结构化数据中开发分类模型。非结构化数据需要先转换为结构化形式,才能开发深度学习网络。例如,一张黑白图像的尺寸可能是 21 x 21,因此包含 441(21 x 21)个像素的数据。一旦我们将图像转换为表示所有像素的数字,就可以开发图像分类模型。尽管人类可以非常容易地分类一种衣服、一个人或某个物体,即使图像的大小或方向不同,但训练计算机做同样的事情仍然是一个具有挑战性的任务。
Keras 库提供了多个易于使用的功能,用于处理图像数据,帮助开发深度学习图像分类网络。在涉及图像识别和分类问题时,拥有多层的深度网络或神经网络的有效性尤为突出。在第四章,图像分类与识别中,我们提供了一个应用深度学习图像分类模型的示例,使用 R 语言进行演示。
卷积神经网络
当类别数量增加且同一类别内的图像表现出显著变化时,图像分类任务变得具有挑战性。此类情况还需要更多的样本,以便分类模型能够更准确地捕捉到每个类别中的固有特征。例如,一家时尚零售商可能拥有大量种类的时尚商品,并且可能希望根据这些时尚商品的图像数据开发分类模型。一种特殊类型的深度网络,被称为卷积神经网络(CNN),已被证明在需要大规模图像分类和识别任务的情况下非常有效。CNN 是这种应用中最流行的网络,并被认为是大规模图像分类问题的黄金标准。这些网络能够通过网络中的不同类型的层捕捉图像中的各种细节。在第五章,使用卷积神经网络进行图像分类中,我们提供了一个应用 CNN 进行图像分类的示例,使用 R 语言进行演示。
自编码器
涉及使用具有响应变量或因变量的数据进行分类和预测模型的深度学习方法属于监督式深度学习方法。当处理结构化或非结构化数据时,有些情况下响应变量可能不可用或未被使用。那些不使用响应变量的深度学习网络应用被归类为无监督深度学习方法。例如,深度学习的一个应用可能是图像数据,我们希望从中提取重要特征以实现降维。另一个例子是包含不需要噪声的手写图像,深度网络用于去噪。在这种情况下,自编码网络被发现非常有用,能够执行无监督深度学习任务。
自编码神经网络使用编码器和解码器网络。当图像数据通过编码器传递,并且得到的维度低于原始图像时,网络就被迫从输入数据中提取最重要的特征。然后,网络的解码器部分根据编码器输出的内容重建原始数据。在第六章,《使用 Keras 应用自编码神经网络》中,我们通过 R 语言展示了如何应用自编码神经网络进行降维、去噪和图像修正。
迁移学习
当图像数据具有多个类别时,开发深度学习分类模型是一项具有挑战性的任务。当可用图像数量有限时,任务变得更加困难。在这种情况下,可以利用一个已使用大量数据集开发的现有模型,并通过定制它来处理另一个分类任务,从而重用它所学到的模式。这种将预训练深度网络模型用于新分类任务的方式称为迁移学习。
Keras 库提供了用于图像分类任务的各种预训练模型,这些模型是通过超过百万张图像训练得到的,能够捕捉可重用的特征,这些特征可以应用于类似但全新的数据。将一个预训练模型从大量样本中学到的知识转移到一个使用较小样本量构建的模型上,有助于节省计算资源。此外,使用迁移学习方法还可以帮助超越从零开始使用较小数据集构建的模型。在第七章,《使用迁移学习进行小数据图像分类》中,我们介绍了迁移学习,并通过 R 语言展示了如何利用预训练的深度学习图像分类模型。
生成对抗网络
《The Verge》的一篇文章(参考:www.theverge.com/2018/10/25/18023266/ai-art-portrait-christies-obvious-sold)报道了一件名为Portrait of Edmond Belamy的艺术作品,这幅作品是使用人工智能算法创作的,最终以 432,500 美元的价格售出。该作品的预计售价大约为 7,000 到 10,000 美元。用于创作这幅作品的深度学习算法被称为生成对抗网络(GAN)。生成对抗网络的独特属性在于,它通过让两个深度网络相互竞争,生成有意义的东西。这两个相互竞争并试图超过对方的网络被称为生成器网络和判别器网络。
假设我们想生成新的手写数字五的图像。在这种情况下,生成对抗网络将涉及一个生成器网络,它从随机噪声中创建伪造的手写数字五图像,并将其发送给判别器网络。伪造的图像与真实图像混合,判别器网络会尽力区分手写数字五的真实和伪造图像。这两个网络将相互竞争,直到生成器网络开始生成看起来非常真实的伪造图像,而判别器网络逐渐变得难以区分真实和伪造图像。除了图像数据,生成对抗网络的应用还可以扩展到生成新的文本甚至新的音乐。在第八章中,我们将演示生成对抗网络在生成新图像中的应用,使用生成对抗网络创建新图像。
用于文本分类的深度网络
文本数据具有一些独特的特点,使其成为与图像数据相比非常不同类型的非结构化数据。正如之前提到的,非结构化数据需要额外的处理步骤才能转化为可以用于开发深度学习分类网络的结构化格式。深度学习在文本数据中的应用之一是开发深度神经网络情感分类模型。
为了开发情感分类模型,需要捕捉与文本数据相关的情感标签。例如,我们可以使用电影评论的文本数据和相关的情感标签(正面评论或负面评论)来开发一个可以自动化处理的模型。另一个例子是使用推文文本数据开发情感分类模型。这样的模型可以用于比较成千上万条推文中的情感,特别是在重大事件发生前后。例如,在公司发布新智能手机前后,或者总统候选人在现场辩论中的表现前后,推文中的情感分类模型都可以发挥重要作用。使用文本数据的情感分类模型的深度网络示例可以参考第九章,用于文本分类的深度网络。
循环神经网络
文本数据的一个独特特点是,文本序列中单词的排列具有一定的意义。循环神经网络(RNN)非常适合处理涉及此类序列的数据。循环网络允许将前一步的输出作为输入传递到下一步。通过在每个步骤中传递先前的信息,循环网络可以具有记忆功能,这对于处理涉及序列的数据非常有用。RNN 中的循环一词也来自于此,即每一步的输出依赖于前一步的信息。
RNN 可以用来开发情感分类模型,其中文本数据可能是电影评论、推文、产品评论等。开发这样的情感分类模型还需要用于训练网络的标签。我们将在第十章中讲解如何使用 R 开发一个情感分类的循环神经网络模型,使用循环神经网络进行文本分类。
长短期记忆网络
长短期记忆(LSTM)网络是一种特殊类型的循环神经网络。当数据涉及单词或整数序列且具有长期依赖性时,LSTM 网络非常有用。例如,在一篇电影评论中,两个对于正确分类情感非常重要的单词可能被长句子中的许多单词所分隔。使用常规 RNN 的情感分类模型将很难捕捉到单词之间的这种长期依赖关系。而常规 RNN 在单词或整数之间的依赖关系是即时的,或者两个重要的单词彼此相邻时,RNN 很有用。
除了情感分类外,LSTM 网络的应用还可以用于语音识别、语言翻译、异常检测、时间序列预测、问答等多个领域。在第十一章中,介绍了一种用于电影评论情感分类的 LSTM 网络应用,使用长短期记忆网络进行文本分类。
卷积递归网络
卷积神经网络(CNNs)用于从图像或文本数据中捕获高级局部特征,而 LSTM 网络则可以捕获涉及序列的长期依赖性数据。当我们在同一模型架构中同时使用 CNN 和递归网络时,称为卷积递归神经网络(CRNN)。例如,如果我们考虑文章及其作者的数据,我们可能希望开发一个作者分类模型,该模型可以训练一个网络以接受包含文章的文本数据作为输入,然后帮助预测关于作者的正确性的概率。为此,我们可以首先使用一维卷积层从数据中提取重要特征。然后,这些提取的特征可以传递到 LSTM 递归层以获取隐藏的长期依赖关系,这些依赖关系又传递到一个全连接的密集层。然后,这个密集层可以获得正确作者的概率。CRNN 也可以应用于与自然语言处理、语音和视频相关的问题。在第十二章中,使用卷积递归网络进行文本分类,我们说明了使用 CRNN 开发模型的示例,该模型可以根据作者撰写的文章对作者进行分类。
技巧、诀窍和最佳实践
在本书中,我们展示了如何使用 R 语言应用几种流行的深度学习方法。在处理需要深度学习网络的更复杂问题时,使用特定的支持工具有时非常有帮助。TensorFlow 提供了一个这样的工具;它称为TensorBoard,对于可视化深度网络训练性能特别有用,特别是在需要实验的情况下。类似地,有一个称为局部可解释模型无关解释(LIME)的包,可以帮助可视化和解释特定预测。在开发深度网络模型时,我们还会获得许多输出,例如摘要和图表。有一个叫做tfruns的包可以帮助将所有内容整理到一个地方方便参考。Keras 包中还有一个回调功能,可以帮助在适当的时候停止网络训练。我们将在第十三章中讨论所有这些技巧、诀窍和最佳实践,技巧、诀窍和前路。
摘要
近年来,利用人工神经网络的深度学习方法越来越受欢迎。深度学习方法的应用领域包括无人驾驶汽车、图像分类、自然语言处理和新图像生成等多个领域。我们在第一章开始时,通过查看来自谷歌趋势网站的深度学习相关热度数据,介绍了深度学习这一术语的流行程度。我们描述了应用深度学习方法的一般五步过程,并阐述了每一步骤中的一些基本概念。接着,我们简要介绍了每一章中所涉及的深度学习技术、它们的应用场景以及一些最佳实践。
在下一章中,我们将通过一个应用示例开始,展示为多类别分类问题开发深度网络模型的步骤。
第二部分:用于预测和分类的深度学习
本节包含两章,解释了如何开发深度学习分类和回归模型。展示了在多类分类和回归情境下开发深度网络模型的过程。
本节包含以下章节:
-
第二章,用于多类分类的深度神经网络
-
第三章,用于回归的深度神经网络
第二章:用于多类分类的深度神经网络
在开发预测和分类模型时,根据响应或目标变量的类型,我们会遇到两种潜在类型的问题:目标变量是类别型(这是一个分类问题)或目标变量是数值型(这是一个回归问题)。有研究表明,大约 70% 的数据属于分类类别问题,剩下的 30% 是回归问题(参考文献:www.topcoder.com/role-of-statistics-in-data-science/)。在本章中,我们将提供使用深度学习神经网络解决分类问题的步骤。步骤将使用胎儿心电图(CTG)进行说明。
在本章中,我们将涵盖以下主题:
-
对胎儿心电图(或 CTG)数据集的简要理解
-
数据准备的步骤,包括归一化、数据分区和独热编码
-
为分类问题创建并拟合深度神经网络模型
-
评估分类模型性能并使用模型进行预测
-
对模型进行微调,以优化性能并采用最佳实践
胎儿心电图数据集
在本节中,我们将提供有关用于开发多类分类模型的数据的相关信息。我们将仅使用一个库,即 Keras。
数据集(医学)
本章使用的数据集可以在由加利福尼亚大学信息与计算机科学学院维护的 UCI 机器学习库中公开访问。你可以通过archive.ics.uci.edu/ml/datasets/cardiotocography访问该数据集。
需要注意的是,该 URL 允许你下载 Excel 数据文件。你可以通过将文件另存为 .csv 文件,轻松将其转换为 .csv 格式。
对于数据,我们应使用 .csv 格式的格式化方式,如以下代码所示:
# Read data
library(keras)
data <- read.csv('~/Desktop/data/CTG.csv', header=T)
str(data)
OUTPUT
## 'data.frame': 2126 obs. of 22 variables:
## $ LB : int 120 132 133 134 132 134 134 122 122 122 ...
## $ AC : num 0 0.00638 0.00332 0.00256 0.00651 ...
## $ FM : num 0 0 0 0 0 0 0 0 0 0 ...
## $ UC : num 0 0.00638 0.00831 0.00768 0.00814 ...
## $ DL : num 0 0.00319 0.00332 0.00256 0 ...
## $ DS : num 0 0 0 0 0 0 0 0 0 0 ...
## $ DP : num 0 0 0 0 0 ...
## $ ASTV : int 73 17 16 16 16 26 29 83 84 86 ...
## $ MSTV : num 0.5 2.1 2.1 2.4 2.4 5.9 6.3 0.5 0.5 0.3 ...
## $ ALTV : int 43 0 0 0 0 0 0 6 5 6 ...
## $ MLTV : num 2.4 10.4 13.4 23 19.9 0 0 15.6 13.6 10.6 ...
## $ Width : int 64 130 130 117 117 150 150 68 68 68 ...
## $ Min : int 62 68 68 53 53 50 50 62 62 62 ...
## $ Max : int 126 198 198 170 170 200 200 130 130 130 ...
## $ Nmax : int 2 6 5 11 9 5 6 0 0 1 ...
## $ Nzeros : int 0 1 1 0 0 3 3 0 0 0 ...
## $ Mode : int 120 141 141 137 137 76 71 122 122 122 ...
## $ Mean : int 137 136 135 134 136 107 107 122 122 122 ...
## $ Median : int 121 140 138 137 138 107 106 123 123 123 ...
## $ Variance: int 73 12 13 13 11 170 215 3 3 1 ...
## $ Tendency: int 1 0 0 1 1 0 0 1 1 1 ...
## $ NSP : int 2 1 1 1 1 3 3 3 3 3 ...
该数据包含胎儿的心电图(CTG),目标变量将患者分类为三类之一:正常、可疑和病理。该数据集共有 2,126 行数据。CTG 由三位专家产科医生分类,并为每个 CTG 分配一致的分类标签:正常(N)(用 1 表示)、可疑(S)(用 2 表示)和病理(P)(用 3 表示)。数据集共有 21 个独立变量,主要目标是开发一个分类模型,将每个患者正确分类为 N、S 和 P 之一。
为模型构建准备数据
在本节中,我们将准备数据以构建分类模型。数据准备将涉及数据的归一化、将数据划分为训练数据和测试数据,以及对响应变量进行独热编码。
数值变量标准化
在开发深度网络模型时,我们对数值变量进行标准化,使其达到统一的尺度。在处理多个变量时,不同变量可能具有不同的尺度——例如,某个变量可能表示公司赚取的收入,数值可能是以百万美元为单位的。在另一个例子中,某个变量可能表示产品的尺寸,以厘米为单位。如此极端的尺度差异在训练网络时会造成困难,而标准化有助于解决这个问题。对于标准化,我们将使用以下代码:
# Normalize data
data <- as.matrix(data)
dimnames(data) <- NULL
data[,1:21] <- normalize(data[,1:21])
data[,22] <- as.numeric(data[,22]) -1
如前面的代码所示,我们首先将数据转换为矩阵格式,然后通过将NULL赋值给维度名称来去除默认的名称。在这一步中,22 个变量的名称将被更改为V1、V2、V3,一直到V22。如果你在此阶段运行str(data),你会注意到原始数据的格式已经发生了变化。我们使用normalize函数对 21 个独立变量进行标准化,该函数是 Keras 包的一部分。当你运行这一行代码时,你会注意到它使用了 TensorFlow 作为后端。我们还将目标变量 NSP 从默认的整数类型更改为数值型。此外,在同一行代码中,我们还将1、2和3的值分别更改为0、1和2。
数据划分
接下来,我们将把这些数据划分为训练集和测试集。为了进行数据划分,我们使用以下代码:
# Data partition
set.seed(1234)
ind <- sample(2, nrow(data), replace = T, prob=c(.7, .3))
training <- data[ind==1, 1:21]
test <- data[ind==2, 1:21]
trainingtarget <- data[ind==1, 22]
testtarget <- data[ind==2, 22]
如前面的代码所示,为了获得相同的训练集和测试集样本以保证可重复性,我们使用set.seed并指定一个特定的数字,在此案例中是1234。这将确保读者也能获得相同的训练数据和测试数据样本。在数据划分时,这里使用了 70:30 的比例,但也可以使用其他比例。在机器学习应用中,这是一个常用的步骤,旨在确保预测模型能够在未见过的数据(即测试数据)上表现良好。训练数据用于开发模型,而测试数据则用于评估模型的性能。有时,预测模型可能在训练数据上表现得非常好,甚至完美;然而,当用模型未见过的测试数据进行评估时,模型的表现可能会非常令人失望。在机器学习中,这个问题被称为模型的过拟合。测试数据有助于评估并确保预测模型能够可靠地用于做出正确的决策。
我们使用training和test名称来存储自变量,使用trainingtarget和testtarget名称来存储目标变量,这些目标变量存储在数据集的第 22 列中。数据划分后,我们将在训练数据中获得 1,523 个观察值,剩余的 603 个观察值将位于测试数据中。请注意,尽管我们在这里使用 70:30 的划分比例,但实际的数据划分比例可能并不完全是 70:30。
One-hot 编码
在数据划分后,我们将对响应变量进行一-hot 编码。One-hot 编码有助于将分类变量表示为零和一。One-hot 编码的代码和输出如下所示:
# One-hot encoding
trainLabels <- to_categorical(trainingtarget)
testLabels <- to_categorical(testtarget)
print(testLabels[1:10,])
OUTPUT
## [,1] [,2] [,3]
## [1,] 1 0 0
## [2,] 1 0 0
## [3,] 1 0 0
## [4,] 0 0 1
## [5,] 0 0 1
## [6,] 0 1 0
## [7,] 1 0 0
## [8,] 1 0 0
## [9,] 1 0 0
## [10,] 1 0 0
如前面的代码所示,在 Keras 包中借助to_categorical函数,我们将目标变量转换为二进制类别矩阵,其中类别的存在与否分别由 1 或 0 表示。在这个示例中,我们有三个目标变量类别,这三个类别被转换为三个虚拟变量。这个过程也叫做one-hot 编码。首先,打印了testLabels中的 10 行数据。第一行表示患者的正常类别,标记为(1,0,0);第六行表示患者的可疑类别,标记为(0,1,0);第四行则提供了一个患者病理类别的示例,标记为(0,0,1)。
完成数据准备步骤后,我们进入下一步,在这一阶段我们创建分类模型,将患者分类为正常、可疑或病理。
创建并拟合深度神经网络模型
在这一部分,我们将开发模型架构、编译模型,然后拟合模型。
开发模型架构
用于开发模型的代码如下:
# Initializing the model
model <- keras_model_sequential()
# Model architecture
model %>%
layer_dense(units = 8, activation = 'relu', input_shape = c(21)) %>%
layer_dense(units = 3, activation = 'softmax')
如前面的代码所示,我们首先使用keras_model_sequential()函数创建一个顺序模型,它允许将层按线性堆叠的方式添加。接下来,我们使用管道操作符%>%向模型中添加层。这个管道操作符将左侧的输出信息作为输入,传递给右侧的操作。我们使用layer_dense函数构建一个全连接的神经网络,并指定各种输入。在这个数据集中,我们有 21 个独立变量,因此,input_shape函数被指定为 21 个神经元或单元。该层也被称为网络中的输入层。第一个隐藏层包含 8 个单元,我们在此使用的激活函数是修正线性单元relu,它是这种情况下最常用的激活函数。第一个隐藏层通过管道操作符与包含 3 个单元的输出层相连接。我们使用 3 个单元是因为我们的目标变量有 3 个类别。输出层使用的激活函数是'softmax',它有助于将输出值的范围保持在 0 到 1 之间。将输出值的范围控制在 0 到 1 之间有助于我们将结果解释为熟悉的概率值。
在 RStudio 中输入管道操作符%>%时,对于 Mac,可以使用Shift + Command + M快捷键,对于 Windows,则可以使用Shift + Ctrl + M。
为了获取我们创建的模型架构的摘要,我们可以运行summary函数,如下代码所示:
# Model summary
summary(model)
OUTPUT
## ___________________________________________________________________________
## Layer (type) Output Shape Param #
## ===========================================================================
## dense_1 (Dense) (None, 8) 176
## ___________________________________________________________________________
## dense_2 (Dense) (None, 3) 27
## ===========================================================================
## Total params: 203
## Trainable params: 203
## Non-trainable params: 0
## ___________________________________________________________________________
由于输入层有 21 个单元,每个单元与第一个隐藏层的 8 个单元相连接,因此我们得到了 168 个权重(21 x 8)。我们还为隐藏层中的每个单元获得一个偏置项,总共有 8 个这样的项。因此,在第一个也是唯一的隐藏层阶段,我们总共有 176 个参数(168 + 8)。类似地,隐藏层中的 8 个单元与输出层的 3 个单元相连接,得到 24 个权重(8 x 3)。这样,输出层就有 24 个权重和 3 个偏置项,总共有 27 个参数。最后,这个神经网络架构的参数总数为 203。
编译模型
为了配置神经网络的学习过程,我们通过指定损失函数、优化器和评估指标来编译模型,如下代码所示:
# Compile model
model %>%
compile(loss = 'categorical_crossentropy',
optimizer = 'adam',
metrics = 'accuracy')
我们使用loss来指定我们希望优化的目标函数。如前面的代码所示,对于损失函数,我们使用'categorical_crossentropy',因为我们的目标变量有三个类别。对于目标变量有两个类别的情况,我们使用binary_crossentropy。对于优化器,我们使用'adam'优化算法,这是一个流行的深度学习优化算法。它之所以受欢迎,主要是因为它比其他随机优化方法(如自适应梯度算法(AdaGrad)和均方根传播(RMSProp))能够更快地得到好的结果。我们指定了用于评估模型训练和测试性能的度量标准。对于metrics,我们使用accuracy来评估模型的分类性能。
现在我们准备好拟合模型,接下来我们将在下一节中进行此操作。
拟合模型
为了拟合模型,我们使用以下代码:
# Fit model
model_one <- model %>%
fit(training,
trainLabels,
epochs = 200,
batch_size = 32,
validation_split = 0.2)
OUTPUT (last 3 epochs)
Epoch 198/200
1218/1218 [==============================] - 0s 43us/step - loss: 0.3662 - acc: 0.8555 - val_loss: 0.5777 - val_acc: 0.8000
Epoch 199/200
1218/1218 [==============================] - 0s 41us/step - loss: 0.3654 - acc: 0.8530 - val_loss: 0.5763 - val_acc: 0.8000
Epoch 200/200
1218/1218 [==============================] - 0s 40us/step - loss: 0.3654 - acc: 0.8571 - val_loss: 0.5744 - val_acc: 0.8000
从前面的代码中可以看出,我们得到了以下观察结果:
-
为了拟合模型,我们提供了包含 21 个自变量数据的训练数据,以及包含目标变量数据的
trainLabels。 -
迭代次数或轮数设置为 200。一个轮次是指训练数据的一次完整传递,随后使用验证数据进行模型评估。
-
为了避免过拟合,我们指定了验证数据的拆分比例为 0.2,这意味着 20%的训练数据将在训练过程中用于评估模型性能。
-
请注意,这 20%的数据是训练数据中底部 20%的数据点。我们将训练过程中生成的训练数据和验证数据的损失和准确度值存储在
model_one中,供以后使用。 -
对于
batch_size,我们使用默认值 32,这表示每次梯度更新时所使用的样本数量。 -
随着模型的训练进行,我们会在每个轮次后基于训练和验证数据展示损失和准确度的可视化图表。
-
对于准确率,我们希望模型的值越高越好,因为准确率是
越高越好类型的指标,而对于损失,它是越低越好类型的指标,我们希望模型的值越低越好。 -
此外,我们还获得了基于最后 3 个轮次的损失输出的数值摘要,如前面的代码输出所示。对于每个轮次,我们看到从 1,523 个训练数据样本中使用了 1,218 个样本(约 80%)进行模型拟合。剩余的 20%的数据用于计算验证数据的准确度和损失值。
需要注意的是,当使用validation_split时,请注意验证数据并不是从训练数据中随机选择的——例如,当validation_split = 0.2时,训练数据的最后 20%被用作验证,前 80%用于训练。因此,如果目标变量的值不是随机的,那么validation_split可能会在分类模型中引入偏差。
在训练过程完成 200 个纪元后,我们可以使用plot函数绘制训练和验证数据的损失和准确率进展,如下代码所示:
plot(model_one)
以下图表提供了一个图形,其中顶部窗口显示准确率,底部窗口显示损失:

训练数据和验证数据的准确率与损失
从前面的损失和准确率图表中,我们可以做出以下观察:
-
从顶部图表中准确率的图形可以看到,准确率值在大约 25 个纪元后显著增加,并随后持续逐渐增加,尤其是对于训练数据。
-
对于验证数据,进展更加不均衡,在第 25^(th)和第 50^(th)个纪元之间,准确率出现下降。
-
在损失值的反向方向上观察到一个类似的模式。
-
请注意,如果训练数据的准确率随着纪元数的增加而提高,但验证数据的准确率下降,这可能表明模型出现了过拟合。从这个图表中我们没有看到任何主要的模式表明模型过拟合。
模型评估与预测
在本节中,我们将使用测试数据来评估模型的性能。当然,我们可以使用训练数据来计算损失和准确率值;然而,分类模型的真正考验是它在未见过的数据上进行测试。由于测试数据与模型构建过程是分开的,因此我们现在可以使用它来进行模型评估。我们将首先使用测试数据计算损失和准确率值,然后构建混淆矩阵。
损失和准确率计算
以下是使用测试数据获取损失和准确率值的代码及输出:
# Model evaluation
model %>%
evaluate(test, testLabels)
OUTPUT
## $loss
## [1] 0.4439415
##
## $acc
## [1] 0.8424544
正如前面的代码所示,使用evaluate函数,我们可以得到损失值和准确率值分别为0.4439和0.8424。通过使用colSums(testLabels),我们可以发现测试数据中正常、可疑和病理类别的患者分别有 460、94 和 49 例。将这些数字转换为百分比,基于 603 个样本的总数,我们得到分别为 76.3%、15.6%和 8.1%的比例。样本数量最多的是正常类别的患者,我们可以将 76.3%作为模型表现的基准。如果我们不使用任何模型,而是简单地将测试数据中的所有病例分类为正常患者类别,那么我们仍然能在 76.3%的情况下正确分类,因为我们会正确分类所有正常患者,而其他两类则会被分类错误。
换句话说,我们的预测准确率将达到 76.3%;因此,我们在这里开发的模型至少应该表现得比这个基准值更好。如果其表现低于这个值,那么它不太可能在实际应用中有太大用处。由于我们在测试数据上的准确率为 84.2%,我们显然已经超过了基准值,但显然我们还必须进一步改进模型,以便取得更好的表现。为了做到这一点,我们需要深入了解每个响应变量类别的模型表现,借助混淆矩阵来分析。
混淆矩阵
为了获得混淆矩阵,首先让我们为测试数据做一个预测,并将其保存在pred中。我们使用predict_classes进行预测,然后使用table函数创建一个预测值与实际值的汇总,以此生成混淆矩阵,如下所示的代码:
# Prediction and confusion matrix
pred <- model %>%
predict_classes(test)
table(Predicted=pred, Actual=testtarget)
OUTPUT
Actual
## Predicted 0 1 2
## 0 435 41 11
## 1 24 51 16
## 2 1 2 22
在前面的混淆矩阵中,输出中显示的值0、1和2分别代表正常、可疑和病理类别。从混淆矩阵中,我们可以做出以下观察:
-
测试数据中有
435名患者实际上是正常的,且模型也预测他们为正常。 -
同样,
51个可疑组的正确预测和22个病理组的正确预测也被记录在案。 -
如果我们将混淆矩阵对角线上的所有数字相加(即正确分类的数量),我们得到 508(435 + 51 + 22),即准确率为 84.2%((508 ÷ 603)× 100)。
-
在混淆矩阵中,非对角线上的数字表示被错误分类的患者数量。最高的错误分类数为 41,表示这些患者实际上属于可疑组,但模型错误地将其分类为正常患者。
-
错误分类数量最少的一项涉及一名患者,他实际上属于正常类别,但模型错误地将这名患者分类为病理类别。
让我们也看一下基于概率的预测,而不是仅仅查看类别,这是我们之前使用的方法。要预测概率,我们可以使用predict_prob函数。然后,我们可以使用cbind函数查看测试数据中的前七行进行比较,如下所示的代码:
# Prediction probabilities
prob <- model %>%
predict_proba(test)
cbind(prob, pred, testtarget)[1:7,]
OUTPUT
pred testtarget
[1,] 0.993281245 0.006415705 0.000302993 0 0
[2,] 0.979825318 0.018759586 0.001415106 0 0
[3,] 0.982333243 0.014519051 0.003147765 0 0
[4,] 0.009040437 0.271216542 0.719743013 2 2
[5,] 0.008850170 0.267527819 0.723622024 2 2
[6,] 0.946622312 0.030137880 0.0232398603 0 1
[7,] 0.986279726 0.012411724 0.0013086179 0 0
在前面的输出中,我们有基于模型的三个类别的概率值,同时我们还拥有测试数据中由pred表示的预测类别和由testtarget表示的实际类别。从前面的输出中,我们可以得出以下观察结果:
-
对于第一个样本,最高的概率是
0.993,对应的是正常类别的患者,这就是预测类别被识别为0的原因。由于这个预测与测试数据中的实际结果一致,我们认为这是正确的分类。 -
同样,由于第四个样本对于第三类的最高概率为
0.7197,因此预测类别被标记为2,这证明是一个正确的预测。 -
然而,第六个样本对于第一类(由
0表示)有最高的概率0.9466,而实际类别是1。在这种情况下,我们的模型将样本分类错误。
接下来,我们将探索提高模型分类性能以获得更好准确率的选项。我们可以遵循的两项关键策略是增加隐藏层的数量以构建更深的神经网络,以及改变隐藏层中单元的数量。我们将在下一节中探索这些选项。
性能优化技巧和最佳实践
在本节中,我们对之前的分类模型进行了微调,以探索其功能并查看其性能是否能够进一步提高。
添加额外隐藏层的实验
在这个实验中,我们将在之前的模型中添加一个额外的隐藏层。模型的代码和输出摘要如下:
# Model architecture
model <- keras_model_sequential()
model %>%
layer_dense(units = 8, activation = 'relu', input_shape = c(21)) %>%
layer_dense(units = 5, activation = 'relu') %>%
layer_dense(units = 3, activation = 'softmax')
summary(model)
OUTPUT
___________________________________________________________________________
Layer (type) Output Shape Param #
===========================================================================
dense_1 (Dense) (None, 8) 176
___________________________________________________________________________
dense_2 (Dense) (None, 5) 45
___________________________________________________________________________
dense_3 (Dense) (None, 3) 18
===========================================================================
Total params: 239
Trainable params: 239
Non-trainable params: 0
___________________________________________________________________________
如前面的代码和输出所示,我们添加了一个具有 5 个单元的第二个隐藏层。在这个隐藏层中,我们同样使用relu作为激活函数。请注意,由于这个改变,我们将总参数数量从之前模型的 203 个增加到了这个模型的 239 个。
接下来,我们将使用以下代码编译并拟合模型:
# Compile and fit model
model %>%
compile(loss = 'categorical_crossentropy',
optimizer = 'adam',
metrics = 'accuracy')
model_two <- model %>%
fit(training,
trainLabels,
epochs = 200,
batch_size = 32,
validation_split = 0.2)
plot(model_two)
如前面的代码所示,我们已使用与之前相同的设置编译模型。我们还保持了fit函数的设置与之前一致。与模型输出相关的信息存储在model_two中。下图展示了model_two的准确率和损失图:

训练数据和验证数据的准确率与损失
从前面的图表中,我们可以得出以下观察结果:
-
基于训练数据和验证数据的准确率在前几个周期内保持相对稳定。
-
在大约 20 个 epoch 后,训练数据的准确率开始上升,并在剩余的 epoch 中持续上升。然而,在大约 100 个 epoch 后,增长速度放缓。
-
另一方面,基于验证数据的准确率在大约 50 个 epoch 后下降,然后开始上升,并在大约 125 个 epoch 后变得或多或少保持恒定。
-
同样,训练数据的损失值最初大幅下降,但在大约 50 个 epoch 后,下降的速度放缓。
-
验证数据的损失值在最初的几个 epoch 中下降,然后在大约 25 个 epoch 后增加并稳定。
使用基于测试数据的类别预测,我们还可以获得一个混淆矩阵来评估这个分类模型的性能。以下代码用于获取混淆矩阵:
# Prediction and confusion matrix
pred <- model %>%
predict_classes(test)
table(Predicted=pred, Actual=testtarget)
OUTPUT
Actual
## Predicted 0 1 2
## 0 429 38 4
## 1 29 54 33
## 2 2 2 12
从前面的混淆矩阵中,我们可以得出以下观察结果:
-
通过将
0、1和2类别的正确分类与之前的模型进行比较,我们注意到只有类别1有所改进,而类别0和2的正确分类实际上有所减少。 -
这个模型的整体准确率为 82.1%,低于我们之前获得的 84.2%的准确率。因此,在这种情况下,我们尝试使模型稍微更深并没有提高准确率。
尝试在隐藏层中使用更多的单元
现在,让我们通过使用以下代码调整第一个模型的隐藏层中单元的数量来微调第一个模型:
# Model architecture
model <- keras_model_sequential()
model %>%
layer_dense(units = 30, activation = 'relu', input_shape = c(21)) %>%
layer_dense(units = 3, activation = 'softmax')
summary(model)
OUTPUT
__________________________________________________________________________
Layer (type) Output Shape Param #
==========================================================================
dense_1 (Dense) (None, 30) 660
__________________________________________________________________________
dense_2 (Dense) (None, 3) 93
==========================================================================
Total params: 753
Trainable params: 753
Non-trainable params: 0
__________________________________________________________________________
# Compile model
model %>%
compile(loss = 'categorical_crossentropy',
optimizer = 'adam',
metrics = 'accuracy')
# Fit model
model_three <- model %>%
fit(training,
trainLabels,
epochs = 200,
batch_size = 32,
validation_split = 0.2)
plot(model_three )
如前面的代码和输出所示,我们将第一个且唯一的隐藏层中的单元数从8增加到30。该模型的总参数数量为753。我们使用之前相同的设置编译并拟合模型。在拟合模型时,我们将准确率和损失值存储在model_three中。
以下截图展示了基于新分类模型,训练和验证数据的准确率和损失的图表,如下图所示:

训练和验证数据的准确率和损失
从前面的图表中,我们可以得出以下观察结果:
-
没有发现过拟合的证据。
-
在大约 75 个 epoch 后,我们没有看到模型性能的任何重大改进。
使用测试数据和混淆矩阵预测类别,可以通过以下代码获得:
# Prediction and confusion matrix
pred <- model %>%
predict_classes(test)
table(Predicted=pred, Actual=testtarget)
OUTPUT
Actual
## Predicted 0 1 2
## 0 424 35 5
## 1 28 55 5
## 2 8 4 39
从前面的混淆矩阵中,我们可以得出以下观察结果:
-
与第一个模型相比,我们在 1 类嫌疑人和 2 类病理类别的分类上看到了一些改进。
-
0、1和2类别的正确分类分别为424、55和39。 -
使用测试数据的整体准确率为 85.9%,比前两个模型更好。
我们还可以通过将每列中正确分类的数量除以该列的总数,获得展示该模型每个类别正确分类次数的百分比。我们发现该分类模型对于正常、可疑和病理病例的正确分类率分别约为 92.2%、58.5% 和 79.6%。因此,模型在正确分类正常患者时表现最好;然而,当正确分类可疑类别的患者时,模型准确率下降至仅 58.5%。从混淆矩阵中,我们可以看到误分类的样本数量最高的是 35。也就是说,有 35 名实际上属于可疑类别的患者被分类模型错误地归为正常类别。
使用更深层且具有更多单元的网络进行实验
在构建了三个参数分别为 203、239 和 753 的不同神经网络模型之后,我们现在将构建一个更深的神经网络模型,其中隐藏层包含更多的单元。用于该实验的代码如下:
# Model architecture
model <- keras_model_sequential()
model %>%
layer_dense(units = 40, activation = 'relu', input_shape = c(21)) %>%
layer_dropout(rate = 0.4) %>%
layer_dense(units = 30, activation = 'relu') %>%
layer_dropout(rate = 0.3) %>%
layer_dense(units = 20, activation = 'relu') %>%
layer_dropout(rate = 0.2) %>%
layer_dense(units = 3, activation = 'softmax')
summary(model)
OUTPUT
__________________________________________________________________________
Layer (type) Output Shape Param #
==========================================================================
dense_1 (Dense) (None, 40) 880
__________________________________________________________________________
dropout_1 (Dropout) (None, 40) 0
__________________________________________________________________________
dense_2 (Dense) (None, 30) 1230
__________________________________________________________________________
dropout_2 (Dropout) (None, 30) 0
__________________________________________________________________________
dense_3 (Dense) (None, 20) 620
__________________________________________________________________________
dropout_3 (Dropout) (None, 20) 0
__________________________________________________________________________
dense_4 (Dense) (None, 3) 63
==========================================================================
Total params: 2,793
Trainable params: 2,793
Non-trainable params: 0
___________________________________________________________________________
# Compile model
model %>%
compile(loss = 'categorical_crossentropy',
optimizer = 'adam',
metrics = 'accuracy')
# Fit model
model_four <- model %>%
fit(training,
trainLabels,
epochs = 200,
batch_size = 32,
validation_split = 0.2)
plot(model_four)
从前面的代码和输出中可以看出,为了尝试提高分类性能,该模型共有 2,793 个参数。该模型包含三个隐藏层,分别有 40、30 和 20 个单元。每个隐藏层后,我们还添加了一个 dropout 层,dropout 率分别为 40%、30% 和 20%,以避免过拟合——例如,在第一个隐藏层后,dropout 率为 0.4(或 40%),这意味着在训练时,第一隐藏层中的 40% 的单元会被随机置零。这有助于避免由于隐藏层中单元数过多而可能导致的过拟合问题。我们编译该模型并使用之前相同的设置运行模型。我们还在每次训练周期后将损失值和准确率存储在 model_four 中。
以下图表显示了训练数据和验证数据的准确率和损失值:

训练和验证数据的准确率与损失
从前面的图表中,我们可以得出以下结论:
-
训练的损失值和准确率在大约 150 次训练周期后保持大致不变。
-
验证数据的准确率在大约 75 次训练周期后基本保持平稳。
-
然而,对于损失值,我们看到在大约 75 次训练周期后,训练数据和验证数据之间出现了一些分歧,验证数据的损失逐渐增加。这表明大约在 75 次训练周期后出现了过拟合现象。
现在让我们使用测试数据进行预测,并查看结果的混淆矩阵,以评估模型性能,代码如下所示:
# Predictions and confusion matrix
pred <- model %>%
predict_classes(test)
table(Predicted=pred, Actual=testtarget)
OUTPUT
Actual
Predicted 0 1 2
0 431 34 7
1 20 53 2
2 9 7 40
从前面的混淆矩阵中,我们可以得出以下结论:
-
对于
0、1和2类别,正确分类的数量分别为431、53和40。 -
总体准确率为 86.9%,优于前三个模型。
-
我们还可以发现,这个分类模型能够正确分类正常、可疑和病理病例,分别为 93.7%、56.4%和 81.6%。
通过解决类别不平衡问题进行实验
在此数据集中,正常、可疑和病理类别的患者数量并不相同。在原始数据集中,正常、可疑和病理患者的数量分别为 1,655、295 和 176。
我们将使用以下代码来绘制条形图:
# Bar plot
barplot(prop.table(table(data$NSP)),
col = rainbow(3),
ylim = c(0, 0.8),
ylab = 'Proportion',
xlab = 'NSP',
cex.names = 1.5)
运行上述代码后,我们得到以下的条形图:

每个类别中样本的比例
在前述的条形图中,正常、可疑和病理患者的比例分别约为 78%、14%和 8%。当我们比较这些类别时,可以观察到正常患者的数量大约是可疑患者的 5.6 倍(1,655/295),也是病理患者的 9.4 倍。该数据集表现出类别不平衡的模式,其中每个类别的病例数差异显著,这种情况被称为类别不平衡问题。具有显著更多病例的类别在训练模型时可能会受益,但也会以牺牲其他类别为代价。
因此,一个分类模型可能会对拥有显著更多样本的类别产生偏倚,并为该类别提供比其他类别更高的分类准确度。当数据受到此类类别不平衡的影响时,必须解决该问题,以避免最终分类模型的偏倚。在这种情况下,我们可以利用类别权重来处理数据集中的类别不平衡问题。
很多用于开发分类模型的数据集在每个类别中的样本数量是不均等的。这样的类别不平衡问题可以通过使用class_weight函数轻松处理。
包含class_weight以引入类别不平衡信息的代码如下所示:
# Fit model
model_five <- model %>%
fit(training,
trainLabels,
epochs = 200,
batch_size = 32,
validation_split = 0.2,
class_weight = list("0"=1,"1"=5.6, "2" = 9.4))
plot(model_five)
如前述代码所示,我们为normal类别指定了权重1,为可疑类别指定了权重5.6,为病理类别指定了权重9.4。分配这些权重为所有三个类别创造了一个公平的竞争环境。我们保持了其他设置与之前的模型一致。训练完网络后,每个周期的损失和准确率值存储在model_five中。
本次实验的损失和准确率图如下所示:

根据训练和验证数据的准确度和损失图表,我们没有看到任何明显表明过拟合的模式。大约在 100 个 epochs 之后,我们没有看到模型性能在损失和准确度值方面有任何重大改善。
来自模型预测和生成的混淆矩阵的代码如下:
# Prediction and confusion matrix
pred <- model %>%
predict_classes(test)
table(Predicted=pred, Actual=testtarget)
OUTPUT
Actual
Predicted 0 1 2
0 358 12 3
1 79 74 5
2 23 8 41
根据上述混淆矩阵,我们可以得出以下观察结果:
-
对于
0、1和2类别的正确分类分别为358、74和41。 -
总体准确度现在降至 78.4%,这主要是由于正常类的准确率下降,因为我们增加了其他两类的权重。
-
我们还可以发现,这个分类模型正确分类了正常、嫌疑和病理情况,准确率分别约为 77.8%、78.7%和 83.7%。
-
显然,最大的收益是对于嫌疑类别,现在的正确分类率为 78.7%,而之前只有 56.4%。
-
在病理类中,我们并未看到准确度值有任何重大的增益或损失。
-
这些结果清楚地表明了使用权重来解决类别不平衡问题的影响,因为现在三个类别的分类性能更加一致。
模型的保存和加载
我们知道,在 Keras 中每次运行模型时,由于随机的初始权重,模型都会从不同的起点开始*。一旦我们得到了性能水平可接受的模型,并希望将来重复使用相同的模型,我们可以使用save_model_hdf5函数保存模型。然后,我们可以使用load_model_hdf5函数加载相同的模型:
# Save and reload model
save_model_hdf5(model,
filepath,
overwrite = TRUE,
include_optimizer = TRUE)
model_x <- load_model_hdf5(filepath,
custom_objects = NULL,
compile = TRUE)
上述代码将允许我们保存模型的架构和模型的权重,并且如果需要的话,将允许我们从先前的训练会话中恢复模型的训练。
摘要
在本章中,我们展示了如何开发一个神经网络模型来帮助解决分类问题。我们从一个简单的分类模型开始,并探讨了如何更改隐藏层的数量和隐藏层中单元的数量。探索和调整分类模型的背后理念是为了说明如何探索和提升分类模型的性能。我们还展示了如何借助混淆矩阵深入理解分类模型的表现。我们有意在本章开始时使用了一个相对较小的神经网络模型,并以一个较深的神经网络模型作为例子来结束。本章介绍的更深层的网络包含多个隐藏层,这也可能导致过拟合问题,其中一个分类模型在训练数据上可能表现优异,但在测试数据上表现不佳。为避免这种情况,我们可以在每个全连接层后使用丢弃层(dropout layer),如前所示。我们还展示了在类别不平衡的情况下,如何使用类别权重,以避免分类模型偏向某个特定类别。最后,我们还介绍了如何保存模型的详细信息,以便未来使用,避免重新训练模型。
在本章使用的模型中,我们在各种实验中保持了某些参数恒定——例如,在编译模型时,我们始终使用adam作为优化器。adam之所以广受欢迎,部分原因在于它不需要太多的调优,并且能在较短的时间内提供良好的结果;然而,建议读者尝试其他优化器,如adagrad、adadelta和rmsprop,并观察这些优化器对模型分类性能的影响。另一个我们在本章中保持恒定的设置是训练网络时的批量大小(batch size)为 32。读者也可以尝试更大的批量(如 64)或更小的批量(如 16),并观察这些变化对分类性能的影响。
随着我们进入后续章节,我们将逐渐开发更复杂、更深入的神经网络模型。在本章中,我们已经介绍了一个分类模型,其中响应变量是类别型的。在下一章中,我们将讲解如何开发和改进回归类型问题的预测模型,其中目标变量是数值型的。
第三章:用于回归的深度神经网络
在上一章中,我们处理了一个具有分类目标变量的数据集,并讲解了如何使用 Keras 开发分类模型。在响应变量是数值型的情况下,监督学习问题被归类为回归问题。本章将开发一个用于数值响应变量的预测模型。为了说明开发预测模型的过程,我们将使用波士顿住房数据集,该数据集可以在mlbench包中找到。
本章内容将涵盖以下主题:
-
了解波士顿住房数据集
-
准备数据
-
创建并拟合一个深度神经网络回归模型
-
模型评估与预测
-
性能优化技巧和最佳实践
了解波士顿住房数据集
本章中我们将使用六个库,具体库列表见以下代码:
# Libraries
library(keras)
library(mlbench)
library(psych)
library(dplyr)
library(magrittr)
library(neuralnet)
BostonHousing数据集的结构如下:
# Data structure
data(BostonHousing)
str(BostonHousing)
OUTPUT
'data.frame': 506 obs. of 14 variables:
$ crim : num 0.00632 0.02731 0.02729 0.03237 0.06905 ...
$ zn : num 18 0 0 0 0 0 12.5 12.5 12.5 12.5 ...
$ indus : num 2.31 7.07 7.07 2.18 2.18 2.18 7.87 7.87 7.87 7.87 ...
$ chas : Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 1 1 1 ...
$ nox : num 0.538 0.469 0.469 0.458 0.458 0.458 0.524 0.524 0.524 0.524 ...
$ rm : num 6.58 6.42 7.18 7 7.15 ...
$ age : num 65.2 78.9 61.1 45.8 54.2 58.7 66.6 96.1 100 85.9 ...
$ dis : num 4.09 4.97 4.97 6.06 6.06 ...
$ rad : num 1 2 2 3 3 3 5 5 5 5 ...
$ tax : num 296 242 242 222 222 222 311 311 311 311 ...
$ ptratio: num 15.3 17.8 17.8 18.7 18.7 18.7 15.2 15.2 15.2 15.2 ...
$ b : num 397 397 393 395 397 ...
$ lstat : num 4.98 9.14 4.03 2.94 5.33 ...
$ medv : num 24 21.6 34.7 33.4 36.2 28.7 22.9 27.1 16.5 18.9 ...
从上面的输出可以看到,该数据集共有506个观测值和14个变量。在这 14 个变量中,13 个是数值型变量,1 个变量(chas)是因子类型。最后一个变量,medv(以千美元为单位的业主自住住房中位数价值),是因变量或目标变量,其余 13 个变量是自变量。以下是所有变量的简要描述,以便参考:
| 变量 | 描述 |
|---|---|
crim |
按城镇计算的人均犯罪率 |
zn |
25,000 平方英尺以上的住宅用地比例 |
indus |
每个城镇的非零售商业用地比例 |
chas |
查尔斯河虚拟变量(若地块与河流相邻则为 1,否则为 0) |
nox |
氮氧化物浓度(每 10 百万分之一) |
rm |
每个住宅的平均房间数 |
age |
1940 年之前建成的自有住宅比例 |
dis |
到波士顿五个就业中心的加权距离 |
rad |
进入放射性高速公路的可达性指数 |
tax |
每 10,000 美元的全额财产税税率 |
ptratio |
每个城镇的师生比 |
lstat |
低收入群体在总人口中的百分比 |
medv |
业主自住住房的中位数价值(千美元单位) |
该数据基于 1970 年的人口普查。使用这些数据的详细统计研究由 Harrison 和 Rubinfeld 于 1978 年发布(参考文献:citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.926.5532&rep=rep1&type=pdf)。
准备数据
我们首先将BostonHousing数据集的名称更改为简化的data,以便使用。然后,通过lapply函数将因子类型的自变量转换为数值类型。
请注意,对于该数据集,唯一的因子变量是chas;然而,对于任何其他包含更多因子变量的数据集,该代码也能正常工作。
请查看以下代码:
# Converting factor variables to numeric
data <- BostonHousing
data %>% lapply(function(x) as.numeric(as.character(x)))
data <- data.frame(data)
在前面的代码中,将因子变量转换为numeric类型后,我们还将data的格式更改为data.frame。
可视化神经网络
为了可视化一个具有隐藏层的神经网络,我们将使用neuralnet函数。为了说明,示例中将使用两个隐藏层,分别有 10 个和 5 个节点。输入层有 13 个节点,基于 13 个自变量。输出层只有一个节点,用于目标变量medv。使用的代码如下:
# Neural network
n <- neuralnet(medv~crim+zn+indus+chas+nox+rm+age+dis+rad+tax+ptratio+b+lstat,
data = data,
hidden = c(10,5),
linear.output = F,
lifesign = 'full',
rep=1)
# Plot
plot(n, col.hidden = "darkgreen",
col.hidden.synapse = 'darkgreen',
show.weights = F,
information = F,
fill = "lightblue")
如前面的代码所示,结果被保存在n中,然后它将用于绘制神经网络的架构,如下图所示:

如前图所示,输入层有 13 个节点,表示 13 个自变量。共有两个隐藏层:第一个隐藏层有 10 个节点,第二个隐藏层有 5 个节点。每个隐藏层的节点都与前一层和后一层的所有节点相连接。输出层有一个节点,用于响应变量medv。
数据分割
接下来,我们将数据转换为矩阵格式。我们还将维度名称设置为NULL,这会将变量的名称更改为默认名称,V1、V2、V3、...、V14:
data <- as.matrix(data)
dimnames(data) <- NULL
然后,我们使用以下代码将数据分割为训练集和测试集:
# Data partitioning
set.seed(1234)
ind <- sample(2, nrow(data), replace = T, prob=c(.7, .3))
training <- data[ind==1, 1:13]
test <- data[ind==2, 1:13]
trainingtarget <- data[ind==1, 14]
testtarget <- data[ind==2, 14]
在本示例中,数据分割的比例为 70:30。为了保持数据分割的可重复性,我们使用了1234的随机种子。这样,每次在任何计算机上执行数据分割时,训练数据和测试数据中都会包含相同的样本。自变量的数据存储在training中用于训练数据,在test中用于测试数据。同样,响应变量medv的数据,基于相应的分割数据,存储在trainingtarget和testtarget中。
归一化
为了对数据进行归一化,我们首先计算训练数据中所有自变量的均值和标准差。然后,使用scale函数进行归一化处理:
对于训练数据和测试数据,均值和标准差是基于所使用的训练数据计算的。
# Normalization
m <- colMeans(training)
sd <- apply(training, 2, sd)
training <- scale(training, center = m, scale = sd)
test <- scale(test, center = m, scale = sd)
这标志着数据准备步骤的完成。需要注意的是,不同的数据集可能需要额外的步骤,这些步骤对于每个数据集是独特的——例如,许多大型数据集可能存在大量缺失数据值,这可能需要额外的数据准备步骤,如制定处理缺失值的策略,并在必要时输入缺失值。
在下一部分,我们将创建一个深度神经网络架构,然后拟合一个模型,用于准确预测数值型目标变量。
创建并拟合一个用于回归的深度神经网络模型
为了创建并拟合一个用于回归问题的深度神经网络模型,我们将使用 Keras。模型架构使用的代码如下:
注意,输入层有 13 个单元,输出层有 1 个单元,这是根据数据固定的;然而,要确定适合的隐藏层数量和每个层的单元数量,您需要进行实验。
# Model architecture
model <- keras_model_sequential()
model %>%
layer_dense(units = 10, activation = 'relu', input_shape = c(13)) %>%
layer_dense(units = 5, activation = 'relu') %>%
layer_dense(units = 1)
summary(model)
OUTPUT
___________________________________________________________________________
Layer (type) Output Shape Param #
===========================================================================
dense_1 (Dense) (None, 10) 140
___________________________________________________________________________
dense_2 (Dense) (None, 5) 55
___________________________________________________________________________
dense_3 (Dense) (None, 1) 6
===========================================================================
Total params: 201
Trainable params: 201
Non-trainable params: 0
___________________________________________________________________________
如前面的代码所示,我们使用keras_model_sequential函数来创建一个顺序模型。神经网络的结构通过layer_dense函数定义。由于有 13 个自变量,input_shape用于指定 13 个单元。第一个隐藏层有10个单元,激活函数使用的是修正线性单元(relu)。第二个隐藏层有5个单元,激活函数同样使用relu。最后,layer_dense有1个单元,表示一个因变量medv。通过使用summary函数,可以打印出模型总结,显示总共 201 个参数。
计算总参数数量
现在让我们看看如何得到模型的总计 201 个参数。dense_1层显示有140个参数。这些参数是基于输入层有 13 个单元,每个单元与第一隐藏层中的 10 个单元连接,因此共有 130 个参数(13 x 10)。其余 10 个参数来自于第一隐藏层中每个 10 个单元的偏置项。同样,50 个参数(10 x 5)来自于两个隐藏层之间的连接,剩下的 5 个参数来自于第二隐藏层中每个 5 个单元的偏置项。最后,dense_3有6个参数((5 x 1)+ 1)。因此,总共有 201 个参数,基于此示例中选择的神经网络架构。
编译模型
在定义模型架构之后,可以使用以下代码编译模型并配置学习过程:
# Compile model
model %>% compile(loss = 'mse',
optimizer = 'rmsprop',
metrics = 'mae')
如前面的代码所示,我们将损失函数定义为均方误差(mse)。在此步骤中,rmsprop优化器和平均绝对误差(mae)度量也被定义。我们选择这些是因为我们的响应变量是数值型的。
拟合模型
接下来,模型使用fit函数进行训练。请注意,在训练过程中,我们会在每个 epoch 后得到可视化图像和数值摘要。以下代码展示了最后三个 epoch 的输出。我们可以获得训练和验证数据的平均绝对误差和损失值。请注意,正如第一章《重访深度学习架构与技术》中所指出的,每次训练网络时,由于网络权重的随机初始化,训练和验证误差可能会有所不同。即使数据使用相同的随机种子进行划分,这种结果也是预期中的。为了获得可重复的结果,最好使用save_model_hdf5函数保存模型,并在需要时重新加载它。
用于训练网络的代码如下:
# Fit model
model_one <- model %>%
fit(training,
trainingtarget,
epochs = 100,
batch_size = 32,
validation_split = 0.2)
OUTPUT from last 3 epochs
Epoch 98/100
284/284 [==============================] - 0s 74us/step - loss: 24.9585 - mean_absolute_error: 3.6937 - val_loss: 86.0545 - val_mean_absolute_error: 8.2678
Epoch 99/100
284/284 [==============================] - 0s 78us/step - loss: 24.6357 - mean_absolute_error: 3.6735 - val_loss: 85.4038 - val_mean_absolute_error: 8.2327
Epoch 100/100
284/284 [==============================] - 0s 92us/step - loss: 24.3293 - mean_absolute_error: 3.6471 - val_loss: 84.8307 - val_mean_absolute_error: 8.2015
如你从上面的代码中所见,模型在小批量大小为32的情况下进行训练,20%的数据用于验证,以避免过拟合。这里,运行了100个 epoch 或迭代来训练网络。训练过程完成后,相关信息将保存在model_one中,随后可以用来根据所有 epoch 的训练和验证数据绘制损失和平均绝对误差图:
plot(model_one)
上述代码将返回以下输出。让我们来看看训练和验证数据(model_one)的损失和平均绝对误差图:

从上述图中,我们可以做出以下观察:
-
随着训练的进行,
mae和loss值在训练数据和验证数据中都逐渐降低。 -
训练数据的错误下降速度在大约 60 个 epoch 后减缓。
在开发预测模型后,我们可以通过评估模型的预测质量来评估其性能,我们将在下一节中讨论这一点。
模型评估和预测
模型评估是获得合适预测模型过程中的一个重要步骤。一个模型可能在用于开发模型的训练数据上表现良好;然而,模型的真正考验是它在尚未见过的数据上的表现。让我们来看看基于测试数据的模型性能。
评估
模型的性能通过evaluate函数进行评估,使用下面代码所示的测试数据:
# Model evaluation
model %>% evaluate(test, testtarget)
OUTPUT
## $loss
## [1] 31.14591
##
## $mean_absolute_error
## [1] 3.614594
从上述输出可以看到,测试数据的损失和平均绝对误差分别为31.15和3.61。稍后我们将使用这些数字来比较和评估我们对当前模型所做的改进是否有助于提高预测性能。
预测
我们将使用以下代码预测test数据的medv值,并将结果存储在pred中:
# Prediction
pred <- model %>% predict(test)
cbind(pred[1:10], testtarget[1:10])
OUTPUT
[,1] [,2]
[1,] 33.18942 36.2
[2,] 18.17827 20.4
[3,] 17.89587 19.9
[4,] 13.07977 13.9
[5,] 14.17268 14.8
[6,] 19.09264 18.4
[7,] 19.81316 18.9
[8,] 21.00356 24.7
[9,] 30.50263 30.8
[10,] 19.75816 19.4
我们可以使用cbind函数查看前 10 个预测值与实际值。输出的第一列显示基于模型的预测值,第二列显示实际值。我们可以从输出中做出以下观察:
-
测试数据中第一个样本的预测值约为
33.19,实际值为36.2。模型低估了响应值,约偏差3个点。 -
对于第二个样本,模型的预测值低估了响应值,偏差超过了
2个点。 -
对于第十个样本,预测值与实际值非常接近。
-
对于第六个样本,模型高估了响应值。
为了全面了解预测性能,我们可以绘制预测值与实际值的散点图。我们将使用以下代码:
plot(testtarget, pred,
xlab = 'Actual',
ylab = 'Prediction')
abline(a=0,b=1)
散点图显示了基于测试数据的预测值与实际响应值:

从前面的图中,我们可以看到预测模型的整体性能。实际值与预测值之间呈正相关,且大致线性。虽然我们可以看到模型表现良好,但显然还有进一步改进的空间,使得数据点更接近理想线,该理想线的截距为零,斜率为 1。接下来,我们将通过开发一个更深的神经网络模型来进一步探索模型改进。
改进
在修改后的新模型中,我们将通过添加更多层来构建一个更深的网络。新增的层预计能够展示出之前较小网络无法捕捉到的数据模式。
更深的网络架构
进行此实验所使用的代码如下:
# Model Architecture
model <- keras_model_sequential()
model %>%
layer_dense(units = 100, activation = 'relu', input_shape = c(13)) %>%
layer_dropout(rate = 0.4) %>%
layer_dense(units = 50, activation = 'relu') %>%
layer_dropout(rate = 0.3) %>%
layer_dense(units = 20, activation = 'relu') %>%
layer_dropout(rate = 0.2) %>%
layer_dense(units = 1)
summary(model)
OUTPUT
## ___________________________________________________________________________
## Layer (type) Output Shape Param #
## ===========================================================================
## dense_4 (Dense) (None, 100) 1400
## ___________________________________________________________________________
## dropout_1 (Dropout) (None, 100) 0
## ___________________________________________________________________________
## dense_5 (Dense) (None, 50) 5050
## ___________________________________________________________________________
## dropout_2 (Dropout) (None, 50) 0
## ___________________________________________________________________________
## dense_6 (Dense) (None, 20) 1020
## ___________________________________________________________________________
## dropout_3 (Dropout) (None, 20) 0
## ___________________________________________________________________________
## dense_7 (Dense) (None, 1) 21
## ===========================================================================
## Total params: 7,491
## Trainable params: 7,491
## Non-trainable params: 0
## _________________________________________________________________________
# Compile model
model %>% compile(loss = 'mse',
optimizer = 'rmsprop',
metrics = 'mae')
# Fit model
model_two <- model %>%
fit(training,
trainingtarget,
epochs = 100,
batch_size = 32,
validation_split = 0.2)
plot(model_two)
从前面的代码中,我们可以观察到,现在我们有三个隐藏层,分别包含100、50和20个单元。我们还在每个隐藏层后添加了一个丢弃层,丢弃率分别为0.4、0.3和0.2。例如,丢弃层的丢弃率意味着,在训练时,丢弃率为 0.4 表示第一隐藏层中 40%的单元被丢弃为零,这有助于避免过拟合。此模型的总参数数目已增加至7,491。注意,在之前的模型中,参数总数为201,显然我们正在构建一个更大的神经网络。接下来,我们使用之前相同的设置编译模型,随后将拟合模型并将结果存储在model_two中。
结果
以下图显示了model_two在 100 个周期中的损失和平均绝对误差:

从前面的图中,我们可以做出以下观察:
-
训练数据和验证数据的平均绝对误差和损失值迅速下降到较低值,在约 30 个周期后,我们未见到任何显著改进。
-
由于训练误差和验证误差似乎相互接近,因此没有过拟合的证据。
我们可以使用以下代码获得测试数据的损失值和平均绝对误差值:
# Model evaluation
model %>% evaluate(test, testtarget)
OUTPUT
## $loss
## [1] 24.70368
##
## $mean_absolute_error
## [1] 3.02175
pred <- model %>% predict(test)
plot(testtarget, pred,
xlab = 'Actual',
ylab = 'Prediction')
abline(a=0,b=1)
使用test数据和model_two得到的损失值和平均绝对误差值分别为24.70和3.02。与我们从model_one获得的结果相比,这是一个显著的改进。
我们可以通过以下图中的散点图直观地看到这一改进,图中展示了预测值与实际响应值的关系:

从前面的图中,我们可以看到,实际值与预测值的散点图分布明显比之前的散点图更集中。这表明与之前的模型相比,预测性能有所提高。尽管model_two比之前的模型表现更好,但在较高值处,我们仍能看到目标值的显著低估。因此,尽管我们已经开发了一个更好的模型,但我们仍可以进一步探索该预测模型进一步改进的潜力。
性能优化技巧和最佳实践
改进模型性能可能涉及不同的策略。在这里,我们将讨论两种主要策略。一种策略是对模型架构进行修改,并观察结果,以获取任何有用的见解或改进的指示。另一种策略可能涉及探索目标变量的转换。在本节中,我们将尝试这两种策略的结合。
对输出变量进行对数转换
为了克服在较高值处显著低估目标变量的问题,我们尝试对目标变量进行对数转换,看看是否能进一步改进模型。我们的下一个模型在架构上也做了一些小的调整。在model_two中,我们没有发现任何主要问题或与过拟合相关的证据,因此我们可以稍微增加单元的数量,并且稍微减少 dropout 的百分比。以下是这个实验的代码:
# log transformation and model architecture
trainingtarget <- log(trainingtarget)
testtarget <- log(testtarget)
model <- keras_model_sequential()
model %>%
layer_dense(units = 100, activation = 'relu', input_shape = c(13)) %>%
layer_dropout(rate = 0.4) %>%
layer_dense(units = 50, activation = 'relu') %>%
layer_dropout(rate = 0.2) %>%
layer_dense(units = 25, activation = 'relu') %>%
layer_dropout(rate = 0.1) %>%
layer_dense(units = 1)
summary(model)
OUTPUT
## ___________________________________________________________________________
## Layer (type) Output Shape Param #
## ===========================================================================
## dense_8 (Dense) (None, 100) 1400
## ___________________________________________________________________________
## dropout_4 (Dropout) (None, 100) 0
## ___________________________________________________________________________
## dense_9 (Dense) (None, 50) 5050
## ___________________________________________________________________________
## dropout_5 (Dropout) (None, 50) 0
## ___________________________________________________________________________
## dense_10 (Dense) (None, 25) 1275
## ___________________________________________________________________________
## dropout_6 (Dropout) (None, 25) 0
## ___________________________________________________________________________
## dense_11 (Dense) (None, 1) 26
## ===========================================================================
## Total params: 7,751
## Trainable params: 7,751
## Non-trainable params: 0
## ___________________________________________________________________________
我们将第三个隐藏层的单元数从20增加到25。第二个和第三个隐藏层的 dropout 率分别减少到0.2和0.1。请注意,整体参数的数量现在已增加到7751。
接下来,我们编译模型并拟合模型。模型结果存储在model_three中,我们使用它来绘制图表,如下所示的代码所示:
# Compile model
model %>% compile(loss = 'mse',
optimizer = optimizer_rmsprop(lr = 0.005),
metrics = 'mae')
# Fit model
model_three <- model %>%
fit(training,
trainingtarget,
epochs = 100,
batch_size = 32,
validation_split = 0.2)
plot(model_three)
以下显示了训练和验证数据的损失值和平均绝对误差的输出(model_three):

从前面的图中我们可以看到,尽管图中的数值由于对数转换与早期数据不完全可比,但我们可以看到,无论是平均绝对误差还是损失值,整体误差在大约 50 个 epoch 后都减少并趋于稳定。
模型性能
我们还获得了这个新模型的loss和mae值,但同样,由于对数尺度的关系,得到的数值与之前两个模型不可直接比较:
# Model evaluation
model %>% evaluate(test, testtarget)
OUTPUT
## $loss
## [1] 0.02701566
##
## $mean_absolute_error
## [1] 0.1194756
pred <- model %>% predict(test)
plot(testtarget, pred)
我们获得了基于测试数据的实际值(对数转换后)与预测值的散点图。我们还获得了实际值与预测值在原始尺度上的散点图,并与早期的图进行比较。预测值与实际响应值的散点图(model_three)如下所示:

从前面的图表中,我们可以看到,早期模型中观察到的显著低估模式在对数尺度和原始尺度上都有所改善。在原始尺度中,较高数值的数据点相对更接近对角线,这表明模型的预测性能有所提高。
总结
本章中,我们介绍了当响应变量为数值型时开发预测模型的步骤。我们从一个具有 201 个参数的神经网络模型开始,然后开发了具有超过 7000 个参数的深度神经网络模型。你可能已经注意到,在本章中,我们使用了比上一章更深且更复杂的神经网络模型,而上一章中我们开发的是针对类别型目标变量的分类模型。在第二章,用于多类分类的深度神经网络和第三章,用于回归的深度神经网络中,我们开发的模型基于结构化数据。在下一章中,我们将转向数据类型为非结构化的问题。更具体地说,我们将处理图像数据类型,并介绍如何使用深度神经网络模型解决图像分类和识别问题。
在下一章中,我们将介绍使用深度神经网络开发图像识别和预测模型所需的步骤。
第三章:计算机视觉中的深度学习
本节解释了如何处理图像数据以及如何使用流行的深度学习方法。它由五章组成,展示了卷积神经网络、自编码器网络、迁移学习和生成对抗网络在计算机视觉应用中的使用。
本节包含以下章节:
-
第四章,图像分类与识别
-
第五章,使用卷积神经网络进行图像分类
-
第六章,使用 Keras 应用自编码器神经网络
-
第七章,使用迁移学习进行小数据集的图像分类
-
第八章,使用生成对抗网络创建新图像
第五章:图像分类和识别
在前几章中,我们讨论了为分类和回归问题开发深度神经网络模型的过程。在这两种情况下,我们处理的是结构化数据,且模型属于监督学习类型,目标变量是已知的。图像或照片属于非结构化数据类别。在本章中,我们将演示如何使用 Keras 包,通过一个易于跟随的示例,使用深度学习神经网络进行图像分类和识别。我们将从一个小样本开始,说明开发图像分类模型的步骤。我们将把这个模型应用于一个涉及图像或照片标注的监督学习情境。
Keras 包含几个内置的数据集,用于图像分类,例如 CIFAR10、CIFAR100、MNIST 和 fashion-MNIST。CIFAR10 包含 50,000 张 32 x 32 彩色训练图像和 10,000 张测试图像,包含 10 个标签类别。CIFAR100 包含 50,000 张 32 x 32 彩色训练图像和 10,000 张测试图像,包含多达 100 个标签类别。MNIST 数据集包含 60,000 张 28 x 28 灰度图像用于训练,10,000 张图像用于测试,涵盖 10 个不同的数字。fashion-MNIST 数据集包含 60,000 张 28 x 28 灰度图像用于训练,10,000 张图像用于测试,包含 10 个时尚类别。这些数据集已经是可以直接用于开发深度神经网络模型的格式,几乎不需要数据准备步骤。然而,为了更好地处理图像数据,我们将从将原始图像从计算机读取到 RStudio 开始,并逐步介绍准备图像数据以构建分类模型的所有步骤。
相关步骤包括探索图像数据、调整图像大小和形状、进行独热编码、开发顺序模型、编译模型、拟合模型、评估模型、进行预测,并使用混淆矩阵评估模型性能。
更具体地说,在本章中,我们将覆盖以下主题:
-
处理图像数据
-
数据准备
-
创建和拟合模型
-
模型评估与预测
-
性能优化建议和最佳实践
处理图像数据
在本节中,我们将把图像数据读取到 R 中,并进一步探索以了解图像数据的各种特性。读取和显示图像的代码如下:
# Libraries
library(keras)
library(EBImage)
# Reading and plotting images
setwd("~/Desktop/image18")
temp = list.files(pattern="*.jpg")
mypic <- list()
for (i in 1:length(temp)) {mypic[[i]] <- readImage(temp[i])}
par(mfrow = c(3,6))
for (i in 1:length(temp)) plot(mypic[[i]])
par(mfrow = c(1,1))
正如前面的代码所示,我们将使用 keras 和 EBImage 库。EBImage 库对于处理和探索图像数据非常有用。我们将从读取保存在我计算机的 image18 文件夹中的 18 张 JPEG 图像文件开始。这些图像每个包含 6 张来自互联网的自行车、汽车和飞机照片。我们将使用 readImage 函数读取这些图像文件,并将其存储在 mypic 中。
以下截图展示了所有 18 张图片:

从前面的截图中,我们可以看到六张关于自行车、汽车和飞机的图片。你可能注意到,并不是所有图片的大小都相同。例如,第五和第六张自行车的尺寸明显不同。同样,第四和第五张飞机的尺寸也明显不同。让我们通过以下代码详细查看第五张自行车的数据:
# Exploring 5th image data
print(mypic[[5]])
OUTPUT
Image
colorMode : Color
storage.mode : double
dim : 299 169 3
frames.total : 3
frames.render: 1
imageData(object)[1:5,1:6,1]
[,1] [,2] [,3] [,4] [,5] [,6]
[1,] 1 1 1 1 1 1
[2,] 1 1 1 1 1 1
[3,] 1 1 1 1 1 1
[4,] 1 1 1 1 1 1
[5,] 1 1 1 1 1 1
hist(mypic[[5]])
使用print函数,我们可以查看自行车的图像(非结构化数据)是如何转化为数字(结构化数据)的。第五张自行车的尺寸为 299 x 169 x 3,总共有 151,593 个数据点或像素,这是通过将这三个数字相乘得到的。第一个数字 299 表示图像的宽度(以像素为单位),第二个数字 169 表示图像的高度(以像素为单位)。请注意,彩色图像由三个通道组成,分别代表红色、蓝色和绿色。从数据中提取的小表格显示了x维度的前五行数据,y维度的前六行数据,而z维度的值为 1。尽管表格中的所有值都是1,但它们应该在0和1之间变化。
彩色图像具有红色、绿色和蓝色通道。而灰度图像只有一个通道。
以下截图展示了用于创建直方图的第五张自行车的数据:

前面的直方图显示了第五张图片数据的强度值分布。可以看出,大多数数据点的强度值较高。
现在,我们来看一下基于第 16 张图片(飞机)的数据直方图进行对比:

从前面的直方图可以看出,这张图片的红色、绿色和蓝色通道有不同的强度值。一般来说,强度值介于零和一之间。接近零的数据点代表图像中较暗的颜色,而接近一的数据点表示图像中的较亮颜色。
让我们通过以下代码查看与第 16 张图片(飞机)相关的数据:
# Exploring 16th image data
print(mypic[[16]])
OUTPUT
Image
colorMode : Color
storage.mode : double
dim : 318 159 3
frames.total : 3
frames.render: 1
imageData(object)[1:5,1:6,1]
[,1] [,2] [,3] [,4] [,5] [,6]
[1,] 0.2549020 0.2549020 0.2549020 0.2549020 0.2549020 0.2549020
[2,] 0.2549020 0.2549020 0.2549020 0.2549020 0.2549020 0.2549020
[3,] 0.2549020 0.2549020 0.2549020 0.2549020 0.2549020 0.2549020
[4,] 0.2588235 0.2588235 0.2588235 0.2588235 0.2588235 0.2588235
[5,] 0.2588235 0.2588235 0.2588235 0.2588235 0.2588235 0.2588235
从前面代码的输出结果来看,我们可以看到这两张图片的尺寸不同。第 16 张图片的尺寸为 318 x 159 x 3,总共有 151,686 个数据点或像素。
为了准备这些数据以开发图像分类模型,我们将从调整所有图片的大小到相同尺寸开始。
数据准备
在本节中,我们将介绍使图像数据准备好用于开发图像分类模型的步骤。这些步骤将涉及调整图像大小以确保所有图像具有相同的尺寸,然后进行重塑、数据划分和响应变量的热编码。
调整大小和重塑
为了准备数据以开发分类模型,我们首先使用以下代码将所有 18 张图像的维度调整为相同的大小:
# Resizing
for (i in 1:length(temp)) {mypic[[i]] <- resize(mypic[[i]], 28, 28)}
从前面的代码可以看到,所有图像现在的大小已调整为 28 x 28 x 3。我们可以使用以下代码再次绘制所有图像,看看调整大小的影响:
# Plot images
par(mfrow = c(3,6))
for (i in 1:length(temp)) plot(mypic[[i]])
par(mfrow = c(1,1)
当我们减少图像的维度时,它将导致像素数减少,从而导致图像质量降低,正如以下截图所示:

接下来,我们将使用以下代码将 28 x 28 x 3 的维度重塑为 28 x 28 x 3 的单维度(或 2,352 个向量):
# Reshape
for (i in 1:length(temp)) {mypic[[i]] <- array_reshape(mypic[[i]], c(28, 28,3))}
str(mypic)
OUTPUT
List of 18
$ : num [1:28, 1:28, 1:3] 1 1 1 1 1 1 1 1 1 1 ...
$ : num [1:28, 1:28, 1:3] 1 1 1 1 1 ...
$ : num [1:28, 1:28, 1:3] 1 1 1 1 1 1 1 1 1 1 ...
$ : num [1:28, 1:28, 1:3] 1 1 1 1 1 1 1 1 1 1 ...
$ : num [1:28, 1:28, 1:3] 1 1 1 1 1 1 1 1 1 1 ...
$ : num [1:28, 1:28, 1:3] 1 1 1 1 1 1 1 1 1 1 ...
$ : num [1:28, 1:28, 1:3] 0.953 0.953 0.953 0.953 0.953 ...
$ : num [1:28, 1:28, 1:3] 1 1 1 1 1 1 1 1 1 1 ...
$ : num [1:28, 1:28, 1:3] 1 1 1 1 1 1 1 1 1 1 ...
$ : num [1:28, 1:28, 1:3] 1 1 1 1 1 1 1 1 1 1 ...
$ : num [1:28, 1:28, 1:3] 1 1 1 1 1 1 1 1 1 1 ...
$ : num [1:28, 1:28, 1:3] 1 1 1 1 1 1 1 1 1 1 ...
$ : num [1:28, 1:28, 1:3] 1 1 1 1 1 1 1 1 1 1 ...
$ : num [1:28, 1:28, 1:3] 1 1 1 1 1 1 1 1 1 1 ...
$ : num [1:28, 1:28, 1:3] 1 1 1 1 0.328 ...
$ : num [1:28, 1:28, 1:3] 0.26 0.294 0.312 0.309 0.289 ...
$ : num [1:28, 1:28, 1:3] 0.49 0.49 0.49 0.502 0.502 ...
$ : num [1:28, 1:28, 1:3] 1 1 1 1 1 1 1 1 1 1 ..
通过使用str(mypic)观察前面数据的结构,我们可以看到列表中有 18 个不同的项,分别对应我们开始时的 18 张图像。
接下来,我们将创建训练、验证和测试数据。
训练、验证和测试数据
我们将分别使用自行车、汽车和飞机的前三张图像进行训练,每种类型的第四张图像用于验证,剩下的每种类型的两张图像用于测试。因此,训练数据将包含九张图像,验证数据将包含三张图像,测试数据将包含六张图像。以下是实现此操作的代码:
# Training Data
a <- c(1:3, 7:9, 13:15)
trainx <- NULL
for (i in a) {trainx <- rbind(trainx, mypic[[i]]) }
str(trainx)
OUTPUT
num [1:9, 1:2352] 1 1 1 1 0.953 ...
# Validation data
b <- c(4, 10, 16)
validx <- NULL
for (i in b) {validx <- rbind(validx, mypic[[i]]) }
str(validx)
OUTPUT
num [1:3, 1:2352] 1 1 0.26 1 1 ...
# Test Data
c <- c(5:6, 11:12, 17:18)
testx <- NULL
for (i in c) {testx <- rbind(testx, mypic[[i]])}
str(testx)
OUTPUT
num [1:6, 1:2352] 1 1 1 1 0.49 ...
从前面的代码可以看到,我们将使用rbind函数将我们为每个图像创建的训练、验证和test数据的行进行合并。将九个图像的数据行合并后,trainx的结构表明共有 9 行和 2,352 列。类似地,对于验证数据,我们有 3 行和 2,352 列,对于测试数据,我们有 6 行和 2,352 列。
热编码
对响应变量进行热编码时,我们使用以下代码:
# Labels
trainy <- c(0,0,0,1,1,1,2,2,2)
validy <- c(0,1,2)
testy <- c(0,0,1,1,2,2)
# One-hot encoding
trainLabels <- to_categorical(trainy)
validLabels <- to_categorical(validy)
testLabels <- to_categorical(testy)
trainLabels
OUTPUT
[,1] [,2] [,3]
[1,] 1 0 0
[2,] 1 0 0
[3,] 1 0 0
[4,] 0 1 0
[5,] 0 1 0
[6,] 0 1 0
[7,] 0 0 1
[8,] 0 0 1
[9,] 0 0 1
从前面的代码可以看到以下内容:
-
我们已经将每个图像的目标值存储在
trainy、validy和testy中,其中0、1和2分别表示自行车、汽车和飞机图像。 -
我们通过使用
to_categorical函数对trainy、validy和testy进行了一次热编码。此处的热编码有助于将因子变量转换为零和一的组合。
现在我们已经将数据转换为可以用于开发深度神经网络分类模型的格式,这也是我们在下一节中将要做的事情。
创建并拟合模型
在本节中,我们将开发一个图像分类模型,用于分类自行车、汽车和飞机的图像。我们将首先指定模型架构,然后编译模型,再使用训练数据和验证数据拟合模型。
开发模型架构
在开发模型架构时,我们从创建一个顺序模型开始,然后添加各种层。以下是代码:
# Model architecture
model <- keras_model_sequential()
model %>%
layer_dense(units = 256, activation = 'relu', input_shape = c(2352)) %>%
layer_dense(units = 128, activation = 'relu') %>%
layer_dense(units = 3, activation = 'softmax')
summary(model)
OUTPUT
______________________________________________________________________
Layer (type) Output Shape Param #
======================================================================
dense_1 (Dense) (None, 256) 602368
______________________________________________________________________
dense_2 (Dense) (None, 128) 32896
_____________________________________________________________________
dense_3 (Dense) (None, 3) 387
======================================================================
Total params: 635,651
Trainable params: 635,651
Non-trainable params: 0
_______________________________________________________________________
如前面的代码所示,输入层有 2352 个单元(28 x 28 x 3)。对于初始模型,我们使用两个隐藏层,分别具有 256 和 128 个单元。对于这两个隐藏层,我们将使用 relu 激活函数。对于输出层,我们将使用 3 个单元,因为目标变量有 3 个类别,分别表示自行车、汽车和飞机。该模型的总参数数量为 635,651。
编译模型
在开发完模型架构之后,我们可以使用以下代码编译模型:
# Compile model
model %>% compile(loss = 'categorical_crossentropy',
optimizer = 'adam',
metrics = 'accuracy')
我们使用 categorical_crossentropy 作为损失函数编译模型,因为我们正在进行多类分类。我们分别指定 adam 作为优化器和 accuracy 作为度量标准。
拟合模型
现在我们准备好训练模型了。以下是实现这一点的代码:
# Fit model
model_one <- model %>% fit(trainx,
trainLabels,
epochs = 30,
batch_size = 32,
validation_data = list(validx, validLabels))
plot(model_one)
从前面的代码中,我们可以得出以下结论:
- 我们可以使用存储在
trainx中的independent变量和存储在trainLabels中的target变量来拟合模型。为了防止过拟合,我们将使用validation_data。
请注意,在前几章中,我们使用了 validation_split 并指定了一个百分比,例如 20%;然而,如果我们使用 20% 的 validation_split,它将使用训练数据的最后 20%(所有飞机图像)作为验证数据。
-
这将导致一种情况,即训练数据中没有来自飞机图像的样本,而分类模型将仅基于自行车和汽车图像。
-
因此,得到的图像分类模型会有偏差,并且仅对自行车和汽车图像表现良好。因此,在这种情况下,我们不使用
validation_split函数,而是使用validation_data,并确保训练数据和验证数据中每种类型的样本都得到了充分代表。
以下图表分别展示了训练数据和验证数据在 30 个 epoch 中的损失和准确度:

我们可以从前面的图表中得出以下观察结果:
-
从与准确度相关的图表部分,我们可以看到,从第十八个 epoch 开始,训练数据的准确度值达到了最高值 1。
-
另一方面,基于验证数据的准确度主要保持在三分之二左右,即 66.7%。由于我们有来自三张图像的数据用于验证,如果这三张验证数据的图像都被正确分类,那么报告的准确度将为 1。在这种情况下,三张图像中有两张被正确分类,导致准确度为 66.7%。
-
从处理损失的图表部分,我们可以看到,对于训练数据,损失值从大约 3 下降到 1 以下,经过 8 个周期后下降显著。之后,它们继续下降;然而,损失值的下降速度放缓。
-
基于验证数据,也可以看到一个大致相似的模式。
-
此外,由于损失在计算中使用了概率值,我们观察到与准确度相关的图表相比,损失相关图表呈现出更加明显的趋势。
接下来,我们将更详细地评估模型的图像分类性能,以便了解其行为。
模型评估与预测
在本节中,我们将进行模型评估,并借助预测结果创建训练数据和测试数据的混淆矩阵。我们从使用训练数据评估图像分类性能开始。
训练数据的损失、准确度和混淆矩阵
我们将首先获取训练数据的损失和准确度值,然后使用以下代码创建混淆矩阵:
# Model evaluation
model %>% evaluate(trainx, trainLabels)
OUTPUT
12/12 [==============================] - 0s 87us/step
$loss
[1] 0.055556579307
$acc
[1] 1
# Confusion matrix
pred <- model %>% predict_classes(trainx)
table(Predicted=pred, Actual=trainy)
OUTPUT
Actual
Predicted 0 1 2
0 3 0 0
1 0 3 0
2 0 0 3
如前面的输出所示,损失值和准确度值分别为0.056和1。基于训练数据的混淆矩阵显示,所有九张图像都已正确分类到三个类别中,因此 resulting 的准确度为 1。
训练数据的预测概率
我们现在可以查看该模型为训练数据中所有九张图像提供的三个类别的概率。以下是代码:
# Prediction probabilities
prob <- model %>% predict_proba(trainx)
cbind(prob, Predicted_class = pred, Actual = trainy)
OUTPUT
Predicted_class Actual
[1,] 0.9431666135788 0.007227868307 0.049605518579 0 0
[2,] 0.8056846261024 0.005127847660 0.189187481999 0 0
[3,] 0.9556384682655 0.001881886506 0.042479615659 0 0
[4,] 0.0018005876336 0.988727569580 0.009471773170 1 1
[5,] 0.0002136278927 0.998095452785 0.001690962003 1 1
[6,] 0.0008950306219 0.994426369667 0.004678600468 1 1
[7,] 0.0367377623916 0.010597365908 0.952664911747 2 2
[8,] 0.0568452328444 0.011656147428 0.931498587132 2 2
[9,] 0.0295505002141 0.011442330666 0.959007143974 2 2
在前面的输出中,前三列显示了图像属于自行车、汽车或飞机的概率,这三者的总和为 1。我们可以从输出中做出以下观察:
-
训练数据中第一张图像的分类概率分别为:自行车
0.943,汽车0.007,飞机0.049。由于最高概率对应于第一个类别,因此模型预测的类别是0(自行车),这也是该图像的实际类别。 -
尽管所有 9 张图像都已正确分类,但正确分类的概率从
0.806(图像 2)到0.998(图像 5)不等。 -
对于汽车图像(第 4 行到第 6 行),正确分类的概率从
0.989到0.998,并且所有三张图像的概率都 consistently 很高。因此,当分类汽车图像时,该分类模型表现最佳。 -
对于自行车图像(第 1 到第 3 行),正确分类的概率范围从
0.806到0.956,这表明正确分类自行车图像存在一定难度。 -
对于第二个样本,它代表了一张自行车图像,其第二高的概率为
0.189,即被判定为飞机图像。显然,当模型决定这张图像是自行车还是飞机时,它有些困惑。 -
对于飞机图像(第 7 到第 9 行),正确分类的概率范围从
0.931到0.959,并且所有三张图像的概率都保持在较高水平。
观察预测概率让我们能更深入地分析模型的分类性能,这些是仅通过查看准确率值无法获得的。然而,尽管在训练数据上表现良好是必要的,但它不足以构建一个可靠的图像分类模型。当分类模型出现过拟合问题时,我们很难在测试数据上复制基于训练数据获得的好结果。 因此,检验一个优秀分类模型的真正标准是它在测试数据上的表现。现在让我们回顾一下该模型在测试数据上的图像分类性能。
测试数据的损失、准确率和混淆矩阵
我们现在可以获取测试数据的损失和准确率值,然后使用以下代码创建混淆矩阵:
# Loss and accuracy
model %>% evaluate(testx, testLabels)
OUTPUT
6/6 [==============================] - 0s 194us/step
$loss
[1] 0.5517520905
$acc
[1] 0.8333333
# Confusion matrix
pred <- model %>% predict_classes(testx)
table(Predicted=pred, Actual=testy)
OUTPUT
Actual
Predicted 0 1 2
0 2 0 0
1 0 1 0
2 0 1 2
从前面的输出中可以看到,测试数据中图像的损失值和准确率值分别为0.552和0.833。这些结果略逊色于训练数据的数值;然而,当模型在未见过的数据上进行评估时,性能出现一些下降是可以预期的。混淆矩阵显示有一张图像被错误分类,其中一张汽车图像被误判为飞机图像。因此,基于测试数据的模型准确率为 83.3%,正确分类了六张中的五张。现在我们通过研究基于测试数据图像的概率值,进一步分析模型的预测性能。
测试数据的预测概率
我们现在可以回顾测试数据中所有六张图像的三种类别的概率。以下是代码:
# Prediction probabilities
prob <- model %>% predict_proba(testx)
cbind(prob, Predicted_class = pred, Actual = testy)
OUTPUT
Predicted_class Actual
[1,] 0.587377548218 0.02450981364 0.38811263442 0 0
[2,] 0.532718658447 0.04708640277 0.42019486427 0 0
[3,] 0.115497209132 0.18486714363 0.69963568449 2 1
[4,] 0.001700860681 0.98481327295 0.01348586939 1 1
[5,] 0.230999588966 0.03030913882 0.73869132996 2 2
[6,] 0.112148292363 0.02054920420 0.86730253696 2 2
通过查看这些预测概率,我们可以得出以下观察结果:
-
自行车图像的预测是正确的,如前两个样本所示。然而,预测概率相对较低,分别为
0.587和0.533。 -
汽车图像(第 3 和第 4 行)的结果混合,第 4 个样本被正确预测,并具有
0.985的高概率,而第 3 张汽车图像则被误分类为飞机,概率约为0.7。 -
飞机图像由第五和第六个样本表示。这两张图像的预测概率分别为
0.739和0.867。 -
尽管六张图像中有五张被正确分类,但与模型在训练数据上的表现相比,许多预测概率相对较低。
因此,总体而言,我们可以说模型的性能确实还有进一步提升的空间。在下一节中,我们将探讨如何提高模型的性能。
性能优化技巧和最佳实践
在本节中,我们将探讨一个更深的网络,以提高图像分类模型的性能。我们将查看结果进行对比。
更深的网络
在本节中,用于实验更深网络的代码如下:
# Model architecture
model <- keras_model_sequential()
model %>%
layer_dense(units = 512, activation = 'relu', input_shape = c(2352)) %>%
layer_dropout(rate = 0.1) %>%
layer_dense(units = 256, activation = 'relu') %>%
layer_dropout(rate = 0.1) %>%
layer_dense(units = 3, activation = 'softmax')
summary(model)
OUTPUT
_______________________________________________________________________
Layer (type) Output Shape Param #
=======================================================================
dense_1 (Dense) (None, 512) 1204736
_______________________________________________________________________
dropout_1 (Dropout) (None, 512) 0
_______________________________________________________________________
dense_2 (Dense) (None, 256) 131328
_______________________________________________________________________
dropout_2 (Dropout) (None, 256) 0
_______________________________________________________________________
dense_3 (Dense) (None, 3) 771
=======================================================================
Total params: 1,336,835
Trainable params: 1,336,835
Non-trainable params: 0
_______________________________________________________________________
# Compile model
model %>% compile(loss = 'categorical_crossentropy',
optimizer = 'adam',
metrics = 'accuracy')
# Fit model
model_two <- model %>% fit(trainx,
trainLabels,
epochs = 30,
batch_size = 32,
validation_data = list(validx, validLabels))
plot(model_two)
从前述代码中,我们可以看到以下内容:
-
我们将第一层和第二层隐藏层的单元数分别增加到
512和256。 -
我们还在每个隐藏层后添加了丢弃层,丢弃率为 10%。
-
进行此更改后,参数总数已增加到
1336835。 -
这次,我们还将运行模型进行 50 个周期。我们不会对模型做其他更改。
以下图表提供了 50 个周期内训练数据和验证数据的准确度和损失值:

从前述图表中,我们可以看到以下内容:
-
与之前的模型相比,我们观察到准确度和损失值有一些显著变化。
-
经过 50 个周期后,训练数据和验证数据的准确度均达到了 100%。
-
此外,训练数据和验证数据的损失与准确度曲线的接近度表明,这个图像分类模型不太可能遭遇过拟合问题。
结果
为了进一步探索模型在图像分类性能方面的变化,尤其是那些图形摘要中不容易发现的变化,我们来查看一些数值摘要:
- 我们将首先查看基于训练数据的结果,并将使用以下代码:
# Loos and accuracy
model %>% evaluate(trainx, trainLabels)
OUTPUT
12/12 [==============================] - 0s 198us/step
$loss
[1] 0.03438224643
$acc
[1] 1
# Confusion matrix
pred <- model %>% predict_classes(trainx)
table(Predicted=pred, Actual=trainy)
OUTPUT
Actual
Predicted 0 1 2
0 3 0 0
1 0 3 0
2 0 0 3
从前述输出中,我们可以看到损失值现在已降至0.034,且准确度保持在1.0。我们得到了与之前相同的混淆矩阵结果,因为模型正确分类了所有九张图像,准确度为 100%。
- 为了更深入地了解模型的分类性能,我们使用以下代码和输出:
# Prediction probabilities
prob <- model %>% predict_proba(trainx)
cbind(prob, Predicted_class = pred, Actual = trainy)
OUTPUT
Predicted_class Actual
[1,] 0.97638195753098 0.0071088117547 0.01650915294886 0 0
[2,] 0.89875286817551 0.0019298568368 0.09931717067957 0 0
[3,] 0.98671281337738 0.0004396488657 0.01284754090011 0 0
[4,] 0.00058794603683 0.9992876648903 0.00012432398216 1 1
[5,] 0.00005639552546 0.9999316930771 0.00001191849515 1 1
[6,] 0.00020669832884 0.9997472167015 0.00004611289114 1 1
[7,] 0.03771930187941 0.0022936603054 0.95998704433441 2 2
[8,] 0.08463590592146 0.0022607713472 0.91310334205627 2 2
[9,] 0.03016609139740 0.0019471622072 0.96788680553436 2 2
根据我们从训练数据输出中获得的前述预测概率,我们可以做出以下观察:
-
正确分类现在比之前的模型具有更高的概率值。
-
根据第二行,最低的正确分类概率为
0.899。 -
因此,与前一个模型相比,这个模型在正确分类图像时似乎更加确信。
- 现在,让我们看看这种改进是否也出现在测试数据上。我们将使用以下代码和输出:
# Loss and accuracy
model %>% evaluate(testx, testLabels)
OUTPUT
6/6 [==============================] - 0s 345us/step
$loss
[1] 0.40148338683
$acc
[1] 0.8333333
# Confusion matrix
pred <- model %>% predict_classes(testx)
table(Predicted=pred, Actual=testy)
OUTPUT
Actual
Predicted 0 1 2
0 2 0 0
1 0 1 0
2 0 1 2
如前述输出所示,测试数据的损失值和准确率分别为 0.401 和 0.833。我们确实看到损失值有所改善;然而,准确率值仍与之前相同。通过查看混淆矩阵,我们可以看到这一次,一张汽车的图像被错误分类为飞机。因此,我们没有看到基于混淆矩阵的任何重大差异。
- 接下来,让我们通过以下代码及其输出回顾预测概率:
# Prediction probabilities
prob <- model %>% predict_proba(testx)
cbind(prob, Predicted_class = pred, Actual = testy)
OUTPUT
Predicted_class Actual
[1,] 0.7411330938339 0.015922509134 0.242944419384 0 0
[2,] 0.7733710408211 0.021422179416 0.205206796527 0 0
[3,] 0.3322730064392 0.237866103649 0.429860889912 2 1
[4,] 0.0005808877177 0.999227762222 0.000191345287 1 1
[5,] 0.2163420319557 0.009395645000 0.774262309074 2 2
[6,] 0.1447975188494 0.002772571286 0.852429926395 2 2
使用测试数据的预测概率,我们可以做出以下两点观察:
-
我们看到的模式与在训练数据结果中观察到的模式一致。该模型正确地将测试数据中的图像分类为具有比早期模型(
0.53到0.98)更高的概率(0.74到0.99)。 -
对于测试数据中的第四个样本,模型似乎在自行车和飞机的图像之间感到困惑,而实际上,这张图像是汽车的图像。
因此,总的来说,我们观察到,通过开发更深的神经网络,我们能够提高模型的性能。虽然从准确度计算中无法明显看到性能提升;然而,预测概率的计算使我们能够获得更好的洞察力,并比较模型的表现。
总结
在本章中,我们探索了图像数据和深度神经网络图像分类模型。我们使用了来自 18 张自行车、汽车和飞机图像的数据,并进行了适当的数据处理,使数据能够与 Keras 库兼容。我们将图像数据划分为训练数据、验证数据和测试数据,并随后使用训练数据开发了一个深度神经网络模型,并通过查看训练数据和测试数据的损失、准确率、混淆矩阵和概率值来评估其性能。我们还对模型进行了修改,以提高其分类性能。此外,我们观察到,当混淆矩阵提供相同水平的性能时,预测概率可能有助于提取两个模型之间的细微差异。
在下一章中,我们将介绍使用卷积神经网络(CNNs)开发深度神经网络图像分类模型的步骤,卷积神经网络在图像分类应用中越来越受欢迎。CNN 被认为是图像分类问题的金标准,并且在大规模图像分类应用中非常有效。
第六章:使用卷积神经网络进行图像分类
卷积神经网络(CNNs)是流行的深度神经网络,并且被认为是大规模图像分类任务的金标准。涉及 CNN 的应用包括图像识别与分类、自然语言处理、医学图像分类等。本章我们将继续讨论有响应变量的监督学习情境。本章提供了使用卷积神经网络进行图像分类与识别的步骤,并通过一个易于跟随的实践示例,利用与时尚相关的修改版国家标准与技术研究所(MNIST)数据进行讲解。我们还利用从互联网上下载的时尚物品图像,探讨我们开发的分类模型的泛化潜力。
本章中,我们将更具体地涵盖以下主题:
-
数据准备
-
卷积神经网络中的层
-
拟合模型
-
模型评估与预测
-
性能优化技巧与最佳实践
-
总结
数据准备
在本章中,我们将使用 Keras 和 EBImage 库:
# Libraries
library(keras)
library(EBImage)
让我们开始看一些从互联网上下载的图像。这里有 20 张图像,包括衬衫、包、凉鞋、连衣裙等时尚物品。这些图像是通过 Google 搜索获得的。我们将尝试开发一个图像识别与分类模型,识别这些图像并将其分类到适当的类别中。为了开发这样的模型,我们将使用 fashion-MNIST 时尚物品数据库:
# Read data
setwd("~/Desktop/image20")
temp = list.files(pattern = "*.jpg")
mypic <- list()
for (i in 1:length(temp)) {mypic[[i]] <- readImage(temp[[i]])}
par(mfrow = c(5,4))
for (i in 1:length(temp)) plot(mypic[[i]])
从互联网上下载的 20 张时尚物品图像如下所示:

接下来,让我们看一下包含大量此类时尚物品图像的 fashion-MNIST 数据。
Fashion-MNIST 数据
我们可以使用dataset_fashion_mnist函数从 Keras 获取 fashion-MNIST 数据。看看以下代码及其输出:
# MNIST data
mnist <- dataset_fashion_mnist()
str(mnist)
OUTPUT
List of 2
$ train:List of 2
..$ x: int [1:60000, 1:28, 1:28] 0 0 0 0 0 0 0 0 0 0 ...
..$ y: int [1:60000(1d)] 9 0 0 3 0 2 7 2 5 5 ...
$ test :List of 2
..$ x: int [1:10000, 1:28, 1:28] 0 0 0 0 0 0 0 0 0 0 ...
..$ y: int [1:10000(1d)] 9 2 1 1 6 1 4 6 5 7 ...
从前面的数据结构来看,我们看到它包含 60,000 张训练图像和 10,000 张测试图像。所有这些图像都是 28 x 28 的灰度图像。我们从上一章了解到,图像可以基于颜色和强度表示为数字数据。自变量 x 包含强度值,因变量 y 包含从 0 到 9 的标签。
fashion-MNIST 数据集中的 10 个不同时尚物品被标记为从 0 到 9,如下表所示:
| 标签 | 描述 |
|---|---|
| 0 | T 恤/上衣 |
| 1 | 长裤 |
| 2 | 套头衫 |
| 3 | 连衣裙 |
| 4 | 外套 |
| 5 | 凉鞋 |
| 6 | 衬衫 |
| 7 | 运动鞋 |
| 8 | 包 |
| 9 | 脚踝靴 |
从前面的表格中,我们可以观察到,为这些图像开发分类模型将具有挑战性,因为某些类别将很难区分。
训练与测试数据
我们提取训练图像数据,将其存储在trainx中,并将相应的标签存储在trainy中。同样,我们从测试数据中创建testx和testy。基于trainy的表格显示,训练数据中每种时尚物品恰好有 6,000 张图片,而测试数据中每种时尚物品恰好有 1,000 张图片:
#train and test data
trainx <- mnist$train$x
trainy <- mnist$train$y
testx <- mnist$test$x
testy <- mnist$test$y
table(mnist$train$y, mnist$train$y)
0 1 2 3 4 5 6 7 8 9
0 6000 0 0 0 0 0 0 0 0 0
1 0 6000 0 0 0 0 0 0 0 0
2 0 0 6000 0 0 0 0 0 0 0
3 0 0 0 6000 0 0 0 0 0 0
4 0 0 0 0 6000 0 0 0 0 0
5 0 0 0 0 0 6000 0 0 0 0
6 0 0 0 0 0 0 6000 0 0 0
7 0 0 0 0 0 0 0 6000 0 0
8 0 0 0 0 0 0 0 0 6000 0
9 0 0 0 0 0 0 0 0 0 6000
table(mnist$test$y,mnist$test$y)
0 1 2 3 4 5 6 7 8 9
0 1000 0 0 0 0 0 0 0 0 0
1 0 1000 0 0 0 0 0 0 0 0
2 0 0 1000 0 0 0 0 0 0 0
3 0 0 0 1000 0 0 0 0 0 0
4 0 0 0 0 1000 0 0 0 0 0
5 0 0 0 0 0 1000 0 0 0 0
6 0 0 0 0 0 0 1000 0 0 0
7 0 0 0 0 0 0 0 1000 0 0
8 0 0 0 0 0 0 0 0 1000 0
9 0 0 0 0 0 0 0 0 0 1000
接下来,我们绘制训练数据中的前 64 张图像。请注意,这些是灰度图像数据,每张图像都有黑色背景。由于我们的图像分类模型将基于这些数据,因此我们最初的彩色图像也需要转换为灰度图像。此外,衬衫、外套和连衣裙等图像比较难以区分,这可能会影响我们模型的准确性。我们来看看以下代码:
# Display images
par(mfrow = c(8,8), mar = rep(0, 4))
for (i in 1:84) plot(as.raster(trainx[i,,], max = 255))
par(mfrow = c(1,1))
我们得到训练数据中前 64 张图像的输出,如下所示:

基于训练数据中第一张图片(踝靴)的直方图如下图所示:

左侧的最高柱状条来自低强度数据点,这些数据点捕捉到了图像中的黑色背景。代表踝靴较浅颜色的高强度值在右侧的更高柱状条中有所体现。这些强度值的范围从 0 到 255。
重塑和调整大小
接下来,我们重塑、训练并测试数据。我们还将训练数据和测试数据除以 255,将数值范围从 0-255 变为 0-1。所使用的代码如下:
# Reshape and resize
trainx <- array_reshape(trainx, c(nrow(trainx), 784))
testx <- array_reshape(testx, c(nrow(testx), 784))
trainx <- trainx / 255
testx <- testx / 255
str(trainx)
OUTPUT
num [1:60000, 1:784] 0 0 0 0 0 0 0 0 0 0 ...
前面的trainx结构表明,在对训练数据进行重塑后,我们现在得到了包含 60,000 行和 784 列(28 x 28)的数据。
在将数据除以 255 之后,我们获得了基于训练数据中第一张图像(踝靴)的直方图输出,如下截图所示:

前面的直方图显示,数据点的范围现在已变为 0 到 1 之间。然而,在前一个直方图中观察到的形状没有变化。
独热编码
接下来,我们对存储在trainy和testy中的标签进行独热编码,使用的代码如下:
# One-hot encoding
trainy <- to_categorical(trainy, 10)
testy <- to_categorical(testy, 10)
head(trainy)
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
[1,] 0 0 0 0 0 0 0 0 0 1
[2,] 1 0 0 0 0 0 0 0 0 0
[3,] 1 0 0 0 0 0 0 0 0 0
[4,] 0 0 0 1 0 0 0 0 0 0
[5,] 1 0 0 0 0 0 0 0 0 0
[6,] 0 0 1 0 0 0 0 0 0 0
经独热编码后,训练数据的第一行表示第十类(踝靴)的值为 1。类似地,训练数据的第二行表示第一类(T 恤/上衣)的值为 1。完成前述更改后,时尚-MNIST 数据现在已经准备好用于开发图像识别和分类模型。
卷积神经网络中的层
在这一部分,我们将开发模型架构,然后编译模型。我们还将进行计算,以将卷积网络与全连接网络进行比较。让我们从指定模型架构开始。
模型架构及相关计算
我们首先通过 keras_model_sequential 函数创建模型。模型架构所使用的代码如下:
# Model architecture
model <- keras_model_sequential()
model %>%
layer_conv_2d(filters = 32,
kernel_size = c(3,3),
activation = 'relu',
input_shape = c(28,28,1)) %>%
layer_conv_2d(filters = 64,
kernel_size = c(3,3),
activation = 'relu') %>%
layer_max_pooling_2d(pool_size = c(2,2)) %>%
layer_dropout(rate = 0.25) %>%
layer_flatten() %>%
layer_dense(units = 64, activation = 'relu') %>%
layer_dropout(rate = 0.25) %>%
layer_dense(units = 10, activation = 'softmax')
如前面的代码所示,我们添加了不同的层来开发 CNN 模型。该网络中的输入层基于图像的高度和宽度为 28 x 28 x 1,其中每个维度都是 28。由于我们使用的是灰度图像,颜色通道为 1。由于我们正在构建一个使用灰度图像的深度学习模型,因此这里使用的是二维卷积层。
请注意,在使用灰度图像数据开发图像识别和分类模型时,我们使用的是 2D 卷积层;而对于彩色图像,我们使用的是 3D 卷积层。
让我们来看一下与网络的第一个卷积层相关的一些计算,这将帮助我们理解与全连接层相比,使用卷积层的优势。在 CNN 中,一层的神经元并不是与下一层的所有神经元相连接的。
这里,输入层的图像尺寸为 28 x 28 x 1。为了获得输出形状,我们从 28(输入图像的高度)中减去 3(来自 kernel_size),然后再加 1。这给我们得到 26。最终的输出形状为 26 x 26 x 32,其中 32 是输出滤波器的数量。因此,输出形状的高度和宽度减小,但深度增大。为了计算参数数量,我们使用 3 x 3 x 1 x 32 + 32 = 320,其中 3 x 3 是 kernel_size,1 是图像的通道数,32 是输出滤波器的数量,并且我们还加上了 32 个偏置项。
如果将其与全连接神经网络进行比较,我们会得到一个更大的参数数量。在全连接网络中,28 x 28 x 1 = 784 个神经元将与 26 x 26 x 32 = 21,632 个神经元连接。因此,总参数数量为 784 x 21,632 + 21,632 = 16,981,120。这比卷积层的参数数量大了超过 53,000 倍。反过来,这有助于显著减少处理时间,从而降低处理成本。
每一层的参数数量在以下代码中给出:
# Model summary
summary(model)
__________________________________________________________________
Layer (type Output Shape Param #
==================================================================
conv2d_1 (Conv2D) (None, 26, 26, 32) 320
__________________________________________________________________
conv2d_2 (Conv2D) (None, 24, 24, 64) 18496
__________________________________________________________________
max_pooling2d_1 (MaxPooling2D) (None, 12, 12, 64) 0
__________________________________________________________________
dropout_1 (Dropout) (None, 12, 12, 64) 0
__________________________________________________________________
flatten_1 (Flatten) (None, 9216) 0
__________________________________________________________________
dense_1 (Dense) (None, 64) 589888
__________________________________________________________________
dropout_2 (Dropout) (None, 64) 0
__________________________________________________________________
dense_2 (Dense) (None, 10) 650
==================================================================
Total params: 609,354
Trainable params: 609,354
Non-trainable params: 0
___________________________________________________________________
第二个卷积网络的输出形状为 24 x 24 x 64,其中 64 是输出滤波器的数量。在这里,输出形状的高度和宽度有所减少,但深度增大。为了计算参数数量,我们使用 3 x 3 x 32 x 64 + 64 = 18,496,其中 3 x 3 是 kernel_size,32 是前一层的滤波器数量,64 是输出滤波器的数量,我们还需要加上 64 个偏置项。
下一层是池化层,通常放置在卷积层之后,执行下采样操作。这有助于减少处理时间,也有助于减少过拟合。为了获得输出形状,我们可以将 24 除以 2,其中 2 是我们指定的池化大小。这里的输出形状是 12 x 12 x 64,并且没有添加新的参数。池化层后是一个 dropout 层,具有相同的输出形状,再次没有添加新的参数。
在展平层中,我们通过将三个数字相乘(12 x 12 x 64)来将三维数据转换为一维,得到 9,216。接下来是一个具有 64 个单元的全连接层。这里的参数数量可以通过计算 9216 x 64 + 64 = 589,888 来获得。接下来是另一个 dropout 层,以避免过拟合问题,这里没有添加任何参数。最后,我们有最后一层,这是一个全连接层,具有 10 个单元,表示 10 种时尚单品。这里的参数数量是 64 x 10 + 10 = 650。因此,参数的总数为 609,354。在我们使用的 CNN 架构中,隐藏层使用 relu 激活函数,输出层使用 softmax 激活函数。
编译模型
接下来,我们使用以下代码来编译模型:
# Compile model
model %>% compile(loss = 'categorical_crossentropy',
optimizer = optimizer_adadelta(),
metrics = 'accuracy')
在前面的代码中,损失函数被指定为categorical_crossentropy,因为有 10 个时尚单品类别。对于优化器,我们使用optimizer_adadelta,并使用其推荐的默认设置。Adadelta 是一种自适应学习率的梯度下降方法,正如其名所示,它会随着时间的推移动态调整,并且不需要手动调节学习率。我们还指定了accuracy作为度量指标。
在接下来的部分,我们将为图像识别和分类拟合模型。
拟合模型
为了拟合模型,我们将继续使用之前章节中的格式。以下代码用于拟合模型:
# Fit model
model_one <- model %>% fit(trainx,
trainy,
epochs = 15,
batch_size = 128,
validation_split = 0.2)
plot(model_one)
这里我们使用 20 个 epoch,批量大小为 128,且 20%的训练数据被保留用于验证。由于这里使用的神经网络比前几章中的更复杂,因此每次运行可能需要相对更多的时间。
准确率和损失
拟合模型后,15 个 epoch 的准确率和损失值如下图所示:

我们可以从上面的图中看到,训练准确率持续增加,而验证准确率在最后几个 epoch 基本保持平稳。相反的模式也出现在损失值上。然而,我们没有观察到任何显著的过拟合问题。
现在,让我们评估这个模型,看看使用该模型的预测效果如何。
模型评估与预测
在拟合模型后,我们将根据损失和准确率评估其表现。我们还将创建一个混淆矩阵,以评估所有 10 种时尚物品的分类表现。我们将对训练数据和测试数据进行模型评估和预测。同时,我们还将获取不属于 MNIST 时尚数据集的时尚物品图像,并探讨该模型在新图像上的泛化能力。
训练数据
基于训练数据的损失和准确率分别为 0.115 和 0.960,如下方代码所示:
# Model evaluation
model %>% evaluate(trainx, trainy)
$loss 0.1151372
$acc 0.9603167
接下来,我们将根据预测值和实际值创建一个混淆矩阵:
# Prediction and confusion matrix
pred <- model %>% predict_classes(trainx)
table(Predicted=pred, Actual=mnist$train$y)
OUTPUT
Actual
Predicted 0 1 2 3 4 5 6 7 8 9
0 5655 1 53 48 1 0 359 0 2 0
1 1 5969 2 8 1 0 3 0 0 0
2 50 0 5642 23 219 0 197 0 2 0
3 42 23 20 5745 50 0 50 0 3 0
4 7 1 156 106 5566 0 122 0 4 0
5 0 0 0 0 0 5971 0 6 1 12
6 230 3 121 68 159 0 5263 0 11 0
7 0 0 0 0 0 22 0 5958 3 112
8 15 3 6 2 4 4 6 0 5974 0
9 0 0 0 0 0 3 0 36 0 5876
从前面的混淆矩阵中,我们可以得出以下观察结果:
-
所有 10 个类别在对角线上的正确分类都有较大的值,最低的是第 6 项(衬衫)的 5,263/6,000。
-
最佳的分类表现出现在第 8 项(包),该模型正确分类了 6,000 张包的图像中的 5,974 张。
-
在代表模型误分类的非对角线数字中,最高值为 359,其中第 6 项(衬衫)被错误地分类为第 0 项(T 恤/上衣)。还有 230 次情况,第 0 项(T 恤/上衣)被误分类为第 6 项(衬衫)。因此,该模型确实在区分第 0 项和第 6 项时存在一些困难。
让我们通过计算前五个物品的预测概率来深入分析,如下方代码所示:
# Prediction probabilities
prob <- model %>% predict_proba(trainx)
prob <- round(prob, 3)
cbind(prob, Predicted_class = pred, Actual = mnist$train$y)[1:5,]
OUTPUT
Predicted_class Actual
[1,] 0.000 0.000 0.000 0.000 0 0 0.000 0.001 0 0.999 9 9
[2,] 1.000 0.000 0.000 0.000 0 0 0.000 0.000 0 0.000 0 0
[3,] 0.969 0.000 0.005 0.003 0 0 0.023 0.000 0 0.000 0 0
[4,] 0.023 0.000 0.000 0.968 0 0 0.009 0.000 0 0.000 3 3
[5,] 0.656 0.001 0.000 0.007 0 0 0.336 0.000 0 0.000 0 0
从前面的输出可以看出,所有五个时尚物品都已正确分类。正确的分类概率从 0.656(第五行的第 0 项)到 1.000(第二行的第 0 项)不等。这些概率非常高,能够确保没有混淆地进行正确分类。
现在,让我们看看这种表现是否在测试数据中得以复制。
测试数据
我们从查看基于测试数据的损失和准确率值开始:
# Model evaluation
model %>% evaluate(testx, testy)
$loss 0.240465
$acc 0.9226
我们观察到,与训练数据相比,损失值较高,准确率较低。这与我们之前在验证数据中观察到的类似情况一致。
测试数据的混淆矩阵如下所示:
# Prediction and confusion matrix
pred <- model %>% predict_classes(testx)
table(Predicted=pred, Actual=mnist$test$y)
OUTPUT
Actual
Predicted 0 1 2 3 4 5 6 7 8 9
0 878 0 14 15 0 0 91 0 0 0
1 1 977 0 2 1 0 1 0 2 0
2 22 1 899 9 55 0 65 0 2 0
3 12 14 6 921 14 0 20 0 3 0
4 2 5 34 26 885 0 57 0 0 0
5 1 0 0 0 0 988 0 8 1 6
6 74 1 43 23 43 0 755 0 2 0
7 0 0 0 0 0 6 0 969 3 26
8 10 2 4 4 2 0 11 0 987 1
9 0 0 0 0 0 6 0 23 0 967
从前面的混淆矩阵中,我们可以得出以下观察结果:
-
该模型在第 6 项(衬衫)上最为混淆,存在 91 个实例,其中将时尚物品错误分类为第 0 项(T 恤/上衣)。
-
最佳的图像识别和分类表现是第 5 项(凉鞋),其在 1,000 个预测中正确分类了 988 个。
-
总体而言,混淆矩阵表现出与我们在训练数据中观察到的类似模式。
查看测试数据中前五个物品的预测概率时,我们观察到所有五个预测都是正确的。所有五个物品的预测概率都相当高:
# Prediction probabilities
prob <- model %>% predict_proba(testx)
prob <- round(prob, 3)
cbind(prob, Predicted_class = pred, Actual = mnist$test$y)[1:5,]
OUTPUT
Predicted_class Actual
[1,] 0.000 0 0.000 0 0.000 0 0.000 0 0 1 9 9
[2,] 0.000 0 1.000 0 0.000 0 0.000 0 0 0 2 2
[3,] 0.000 1 0.000 0 0.000 0 0.000 0 0 0 1 1
[4,] 0.000 1 0.000 0 0.000 0 0.000 0 0 0 1 1
[5,] 0.003 0 0.001 0 0.004 0 0.992 0 0 0 6 6
现在,在训练和测试数据的准确度方面都有足够高的分类性能,让我们看看是否可以对这 20 张时尚物品图像做到同样的事情,这些图像是我们在本章开始时所用的。
来自互联网的 20 个时尚物品
我们从桌面读取了 20 张彩色图像,并将其转换为灰度图,以保持与我们迄今为止使用的数据和模型的兼容性。看看以下代码:
setwd("~/Desktop/image20")
temp = list.files(pattern = "*.jpg")
mypic <- list()
for (i in 1:length(temp)) {mypic[[i]] <- readImage(temp[[i]])}
for (i in 1:length(temp)) {mypic[[i]] <- channel(mypic[[i]], "gray")}
for (i in 1:length(temp)) {mypic[[i]] <- 1-mypic[[i]]}
for (i in 1:length(temp)) {mypic[[i]] <- resize(mypic[[i]], 28, 28)}
par(mfrow = c(5,4), mar = rep(0, 4))
for (i in 1:length(temp)) plot(mypic[[i]])
如前所示,我们还将所有 20 张图像调整为 28 x 28,并且最终要分类的 20 张图像如下:

从前面的图表中可以观察到,每一类时尚-MNIST 数据的 10 个类别中都有两个时尚物品:
# Reshape and row-bind
for (i in 1:length(temp)) {mypic[[i]] <- array_reshape(mypic[[i]], c(1,28,28,1))}
new <- NULL
for (i in 1:length(temp)) {new <- rbind(new, mypic[[i]])}
str(new)
OUTPUT
num [1:20, 1:784] 0.0458 0.0131 0 0 0 ...
我们将图像调整为所需的尺寸,然后按行将它们绑定。观察new的结构,我们看到一个 20 x 784 的矩阵。然而,为了得到适当的结构,我们将其进一步调整为 20 x 28 x 28 x 1,如以下代码所示:
# Reshape
newx <- array_reshape(new, c(nrow(new),28,28,1))
newy <- c(0,4,5,5,6,6,7,7,8,8,9,0,9,1,1,2,2,3,3,4)
我们将new重新调整为合适的格式,并将结果保存在newx中。我们使用newy来存储 20 个时尚物品的实际标签。
现在,我们已经准备好使用预测模型,并创建一个混淆矩阵,如以下代码所示:
# Confusion matrix for 20 images
pred <- model %>% predict_classes(newx)
table(Predicted=pred, Actual=newy)
OUTPUT
Actual
Predicted 0 1 2 3 4 5 6 7 8 9
0 1 0 0 0 0 0 0 0 0 0
1 0 1 0 0 0 0 0 0 0 0
2 0 0 1 0 0 0 0 0 0 0
3 1 1 0 2 0 0 0 0 0 2
4 0 0 1 0 1 0 0 0 0 0
5 0 0 0 0 0 0 0 1 0 0
6 0 0 0 0 0 0 2 0 0 0
8 0 0 0 0 1 2 0 1 2 0
从对角线上的数字可以看出,在 20 个物品中只有 10 个被正确分类。这意味着准确率为 50%,与训练数据和测试数据中观察到的 90%以上的准确率相比,这个准确率较低。
接下来,我们使用以下代码以图表的形式总结这些预测,图表包括预测概率、预测类别和实际类别:
# Images with prediction probabilities, predicted class, and actual class
setwd("~/Desktop/image20")
temp = list.files(pattern = "*.jpg")
mypic <- list()
for (i in 1:length(temp)) {mypic[[i]] <- readImage(temp[[i]])}
for (i in 1:length(temp)) {mypic[[i]] <- channel(mypic[[i]], "gray")}
for (i in 1:length(temp)) {mypic[[i]] <- 1-mypic[[i]]}
for (i in 1:length(temp)) {mypic[[i]] <- resize(mypic[[i]], 28, 28)}
predictions <- predict_classes(model, newx)
probabilities <- predict_proba(model, newx)
probs <- round(probabilities, 2)
par(mfrow = c(5, 4), mar = rep(0, 4))
for(i in 1:length(temp)) {plot(mypic[[i]])
legend("topleft", legend = max(probs[i,]),
bty = "n",text.col = "white",cex = 2)
legend("top", legend = predictions[i],
bty = "n",text.col = "yellow", cex = 2)
legend("topright", legend = newy[i],
bty = "",text.col = "darkgreen", cex = 2) }
前面的图表总结了分类模型的性能,结合了预测概率、预测类别和实际类别(model-one):

在前面的图表中,左上角的第一个数字是预测概率,顶部中间的第二个数字是预测类别,顶部右侧的第三个数字是实际类别。观察一些错误分类的情况,值得注意的是,令人惊讶的是,所有凉鞋(物品 5)、运动鞋(物品 7)和短靴(物品 9)的图像都被错误分类了。这些类别的图像在训练数据和测试数据中的分类准确率都很高。这六个错误分类导致了显著较低的准确率。
到目前为止,我们所做的两大关键方面可以总结如下:
-
第一个结果是我们通常会预期的——模型在测试数据上的表现通常比在训练数据上的表现差。
-
第二个结果是一个稍微出乎意料的结果。来自互联网的 20 张时尚物品图像,在使用相同模型时准确率显著降低。
让我们看看是否能制定一种策略或对模型进行更改,以获得更好的性能。我们计划更仔细地查看数据,并找到一种方法,如果可能的话,将我们在训练和测试数据中看到的性能转化为这 20 张新图片的表现。
性能优化建议与最佳实践
在任何数据分析任务中,了解数据是如何收集的都非常重要。通过我们在前一节中开发的模型,测试数据的准确度从 90%以上下降到从互联网上下载的 20 张时尚商品图片的 50%。如果不解决这种差异,该模型将很难对任何未包含在训练或测试数据中的时尚商品进行良好的泛化,因此在实际应用中不会有太大帮助。在这一节中,我们将探索如何改进模型的分类性能。
图像修改
看一下本章开头的 64 张图片,能为我们揭示一些线索。我们注意到,凉鞋、运动鞋和踝靴的图片似乎有一个特定的模式。在所有涉及这些时尚商品的图片中,鞋头总是指向左侧。另一方面,在从互联网上下载的三种鞋类时尚商品的图片中,我们注意到鞋头是指向右侧的。为了解决这个问题,让我们用flop函数修改这 20 张时尚商品图片,使鞋头指向左侧,然后我们可以再次评估模型的分类性能:
# Images with prediction probabilities, predicted class, and actual class setwd("~/Desktop/image20")
temp = list.files(pattern = "*.jpg")
mypic <- list()
for (i in 1:length(temp)) {mypic[[i]] <- readImage(temp[[i]])}
for (i in 1:length(temp)) {mypic[[i]] <- flop(mypic[[i]])}
for (i in 1:length(temp)) {mypic[[i]] <- channel(mypic[[i]], "gray")}
for (i in 1:length(temp)) {mypic[[i]] <- 1-mypic[[i]]}
for (i in 1:length(temp)) {mypic[[i]] <- resize(mypic[[i]], 28, 28)}
predictions <- predict_classes(model, newx)
probabilities <- predict_proba(model, newx)
probs <- round(probabilities, 2)
par(mfrow = c(5, 4), mar = rep(0, 4))
for(i in 1:length(temp)) {plot(mypic[[i]])
legend("topleft", legend = max(probs[i,]),
bty = "",text.col = "black",cex = 1.2)
legend("top", legend = predictions[i],
bty = "",text.col = "darkred", cex = 1.2)
legend("topright", legend = newy[i],
bty = "",text.col = "darkgreen", cex = 1.2) }
以下截图展示了在应用flop(model-one)函数后,预测概率、预测类别和实际类别:

从前面的图表中可以观察到,在改变了时尚商品图片的方向后,我们现在可以正确分类凉鞋、运动鞋和踝靴。在 20 张图片中有 16 张分类正确,准确度提高到 80%,相比之下之前只有 50%的准确度。请注意,这一准确度的提升来自于相同的模型。我们所做的唯一事情就是观察原始数据是如何收集的,并且在新图像数据的使用中保持一致性。接下来,让我们着手修改深度网络架构,看看能否进一步改进结果。
在将预测模型用于将结果推广到新数据之前,回顾数据最初是如何收集的,然后在新数据的格式上保持一致性是一个好主意。
我们鼓励你进一步实验,探索如果将时尚-MNIST 数据中的某些图片更改为其镜像图像,会发生什么。这样是否能帮助更好地泛化,而不需要对新数据做出更改?
对架构的更改
我们通过增加更多的卷积层来修改 CNN 的架构,目的是说明如何添加这样的层。请看下面的代码:
# Model architecture
model <- keras_model_sequential()
model %>%
layer_conv_2d(filters = 32, kernel_size = c(3,3),
activation = 'relu', input_shape = c(28,28,1)) %>%
layer_conv_2d(filters = 32, kernel_size = c(3,3),
activation = 'relu') %>%
layer_max_pooling_2d(pool_size = c(2,2)) %>%
layer_dropout(rate = 0.25) %>%
layer_conv_2d(filters = 64, kernel_size = c(3,3),
activation = 'relu') %>%
layer_conv_2d(filters = 64, kernel_size = c(3,3),
activation = 'relu') %>%
layer_max_pooling_2d(pool_size = c(2,2)) %>%
layer_dropout(rate = 0.25) %>%
layer_flatten() %>%
layer_dense(units = 512, activation = 'relu') %>%
layer_dropout(rate = 0.5) %>%
layer_dense(units = 10, activation = 'softmax')
# Compile model
model %>% compile(loss = 'categorical_crossentropy',
optimizer = optimizer_adadelta(),
metrics = 'accuracy')
# Fit model
model_two <- model %>% fit(trainx,
trainy,
epochs = 15,
batch_size = 128,
validation_split = 0.2)
plot(model_two)
在前面的代码中,对于前两层卷积层,我们使用了 32 个滤波器,每个卷积层;而对于接下来的卷积层,我们使用了 64 个滤波器。每对卷积层后,如前所示,我们添加了池化层和丢弃层。这里的另一个变化是使用了 512 个单元的全连接层。其他设置与之前的网络相似。
以下截图显示了model_two在训练数据和验证数据上的准确率和损失:

基于model_two的图表显示,与model_one相比,训练数据和验证数据在损失和准确度方面的表现更加接近。此外,随着第十五个周期后曲线的平滑,也表明增加周期数不太可能进一步改善分类性能。
训练数据的损失和准确度值如下:
# Loss and accuracy
model %>% evaluate(trainx, trainy)
$loss 0.1587473
$acc 0.94285
基于该模型的损失和准确度值没有显著改善,损失值稍高,而准确度值略低。
以下混淆矩阵总结了预测类别和实际类别:
# Confusion matrix for training data
pred <- model %>% predict_classes(trainx)
table(Predicted=pred, Actual=mnist$train$y)
OUTPUT
Actual
Predicted 0 1 2 3 4 5 6 7 8 9
0 5499 0 58 63 3 0 456 0 4 0
1 2 5936 1 5 3 0 4 0 1 0
2 83 0 5669 13 258 0 438 0 7 0
3 69 52 48 5798 197 0 103 0 6 0
4 3 3 136 49 5348 0 265 0 5 0
5 0 0 0 0 0 5879 0 3 0 4
6 309 6 73 67 181 0 4700 0 2 0
7 0 0 0 0 0 75 0 5943 1 169
8 35 3 15 5 10 3 34 0 5974 2
9 0 0 0 0 0 43 0 54 0 5825
从混淆矩阵中,我们可以得出以下观察结论:
-
它显示模型在项目 6(衬衫)和项目 0(T 恤/上衣)之间存在最大混淆(456 次错误分类)。这种混淆是双向的,既有项目 6 被误分类为项目 0,也有项目 0 被误分类为项目 6。
-
项目 8(包)被分类为最准确的,6,000 个实例中有 5,974 个被正确分类(约 99.6%的准确率)。
-
项目 6(衬衫)在 10 个类别中被分类为准确度最低的,6,000 个实例中有 4,700 个被误分类(约 78.3%的准确率)。
对于测试数据的损失,我们提供了以下的准确率和混淆矩阵:
# Loss and accuracy for the test data
model %>% evaluate(testx, testy)
$loss 0.2233179
$acc 0.9211
# Confusion matrix for test data
pred <- model %>% predict_classes(testx)
table(Predicted=pred, Actual=mnist$test$y)
OUTPUT
Actual
Predicted 0 1 2 3 4 5 6 7 8 9
0 875 1 18 8 0 0 104 0 3 0
1 0 979 0 2 0 0 0 0 0 0
2 19 0 926 9 50 0 78 0 1 0
3 10 14 9 936 35 0 19 0 3 0
4 2 0 30 12 869 0 66 0 0 0
5 0 0 0 0 0 971 0 2 1 2
6 78 3 16 29 45 0 720 0 1 0
7 0 0 0 0 0 18 0 988 1 39
8 16 3 1 4 1 0 13 0 989 1
9 0 0 0 0 0 11 0 10 1 958
从前面的输出中,我们观察到损失低于我们之前模型的结果,而准确度略低于之前的表现。从混淆矩阵中,我们可以得出以下观察结论:
-
它显示模型在项目 6(衬衫)和项目 0(T 恤/上衣)之间存在最大混淆(104 次错误分类)。
-
项目 8(包)被分类为最准确的,1,000 个实例中有 989 个被正确分类(约 98.9%的准确率)。
-
项目 6(衬衫)在 10 个类别中被分类为准确度最低的,1,000 个实例中有 720 个被误分类(约 72.0%的准确率)。
因此,总体上,我们观察到与训练数据时相似的表现。
对于从互联网下载的 20 张时尚商品图片,以下截图总结了模型的表现:

从前面的图表可以看出,这次我们成功地正确分类了 20 张图像中的 17 张。虽然这表现稍好一些,但仍然略低于测试数据中 92%准确率的水平。另外,由于样本量较小,准确率的波动可能会比较大。
在本节中,我们对 20 张新图像进行了修改,并对 CNN 模型架构进行了一些调整,以获得更好的分类性能。
总结
本章中,我们展示了如何使用卷积神经网络(CNN)深度学习模型进行图像识别和分类。我们使用了流行的 fashion-MNIST 数据集进行图像分类模型的训练和测试。我们还讨论了涉及多个参数的计算,并将其与密集连接神经网络所需的参数数量进行了对比。CNN 模型大大减少了所需的参数数量,从而显著节省了计算时间和资源。我们还使用了从互联网上下载的时尚商品图像,测试了基于 fashion-MNIST 数据的分类模型是否能推广到类似的物品上。我们确实注意到,保持训练数据中图像布局的一致性非常重要。此外,我们还展示了如何在模型架构中添加更多的卷积层,以开发更深的 CNN 模型。
到目前为止,我们逐渐从不太深的神经网络模型过渡到更复杂、更深的神经网络模型。我们也主要讨论了属于监督学习方法的应用。在下一章中,我们将介绍另一类有趣的深度神经网络模型——自编码器。我们将介绍涉及自编码器网络的应用,这些应用可以归类为无监督学习方法。
第七章:使用 Keras 应用自编码器神经网络
自编码器网络属于无监督学习方法的范畴,其中没有可用的标注目标值。然而,由于自编码器通常使用某种形式的输入数据作为目标,它们也可以称为自监督学习方法。在本章中,我们将学习如何使用 Keras 应用自编码器神经网络。我们将涵盖自编码器的三种应用:降维、图像去噪和图像修复。本章中的示例将使用时尚物品图像、数字图像以及包含人物的图片。
更具体地说,在本章中,我们将涵盖以下主题:
-
自编码器的类型
-
降维自编码器
-
去噪自编码器
-
图像修复自编码器
自编码器的类型
自编码器神经网络由两大部分组成:
-
第一部分被称为编码器,它将输入数据的维度降低。通常,这是一张图像。当输入图像的数据通过一个将其降维的网络时,网络被迫只提取输入数据中最重要的特征。
-
自编码器的第二部分被称为解码器,它尝试从编码器的输出中重建原始数据。通过指定该网络应尝试匹配的输出,训练自编码器网络。
让我们考虑一些使用图像数据的示例。如果指定的输出是与输入图像相同的图像,那么在训练后,自编码器网络应该提供一张低分辨率的图像,这张图像保留了输入图像的关键特征,但遗漏了一些原始输入图像的细节。此类型的自编码器可用于降维应用。由于自编码器基于能够捕捉数据非线性的神经网络,它们的表现优于仅使用线性函数的方法。下图展示了自编码器网络的编码器和解码器部分:

如果我们训练自编码器,使得输入图像包含噪声或不清晰的内容,而输出则是去除噪声后的相同图像,那么我们就可以创建去噪自编码器。类似地,如果我们使用带有眼镜和不带眼镜的图像,或者带有胡子和不带胡子的图像等输入/输出图像来训练自编码器,那么我们就可以创建有助于图像修复/修改的网络。
接下来,我们将分别通过三个示例来看如何使用自编码器:用于降维、图像去噪和图像修复。我们将从使用自编码器进行降维开始。
降维自编码器
在本节中,我们将使用时尚-MNIST 数据集,指定自编码器模型的架构,编译模型,拟合模型,然后重建图像。请注意,时尚-MNIST 是 Keras 库的一部分。
MNIST 时尚数据集
我们将继续使用 Keras 和 EBImage 库。读取时尚-MNIST 数据的代码如下:
# Libraries
library(keras)
library(EBImage)
# Fashion-MNIST data
mnist <- dataset_fashion_mnist()
str(mnist)
List of 2
$ train:List of 2
..$ x: int [1:60000, 1:28, 1:28] 0 0 0 0 0 0 0 0 0 0 ...
..$ y: int [1:60000(1d)] 9 0 0 3 0 2 7 2 5 5 ...
$ test :List of 2
..$ x: int [1:10000, 1:28, 1:28] 0 0 0 0 0 0 0 0 0 0 ...
..$ y: int [1:10000(1d)] 9 2 1 1 6 1 4 6 5 7 ...
在这里,训练数据有 60,000 张图片,测试数据有 10,000 张时尚商品图片。由于我们将在此示例中使用无监督学习方法,因此我们不会使用训练和测试数据中的标签。
我们将训练图像数据存储在trainx中,将测试图像数据存储在testx中,如下所示:
# Train and test data
trainx <- mnist$train$x
testx <- mnist$test$x
# Plot of 64 images
par(mfrow = c(8,8), mar = rep(0, 4))
for (i in 1:64) plot(as.raster(trainx[i,,], max = 255))
以下是前 64 张时尚商品图片:

接下来,我们将图像数据重塑为合适的格式,如下所示:
# Reshape images
trainx <- array_reshape(trainx, c(nrow(trainx), 28, 28, 1))
testx <- array_reshape(testx, c(nrow(testx), 28, 28, 1))
trainx <- trainx / 255
testx <- testx / 255
在这里,我们还将trainx和testx除以 255,将原本介于 0 到 255 之间的值的范围转换为介于 0 和 1 之间的范围。
编码器模型
为了指定编码器模型的架构,我们将使用以下代码:
# Encoder
input_layer <-
layer_input(shape = c(28,28,1))
encoder <- input_layer %>%
layer_conv_2d(filters = 8,
kernel_size = c(3,3),
activation = 'relu',
padding = 'same') %>%
layer_max_pooling_2d(pool_size = c(2,2),
padding = 'same') %>%
layer_conv_2d(filters = 4,
kernel_size = c(3,3),
activation = 'relu',
padding = 'same') %>%
layer_max_pooling_2d(pool_size = c(2,2),
padding = 'same')
summary(encoder)
Output
Tensor("max_pooling2d_10/MaxPool:0", shape=(?, 7, 7, 4), dtype=float32)
在这里,对于编码器的输入,我们指定了输入层,使其大小为 28 x 28 x 1。使用了两个卷积层,一个具有 8 个过滤器,另一个具有 4 个过滤器。这两个层的激活函数都使用修正线性单元(relu)。卷积层包括padding = 'same',这保持了输入在输出时的高度和宽度。例如,在第一个卷积层之后,输出的高度和宽度为 28 x 28。每个卷积层后面都有池化层。在第一个池化层之后,高度和宽度变为 14 x 14,而在第二个池化层之后,变为 7 x 7。此示例中编码器网络的输出为 7 x 7 x 4。
解码器模型
为了指定解码器模型的架构,我们将使用以下代码:
# Decoder
decoder <- encoder %>%
layer_conv_2d(filters = 4,
kernel_size = c(3,3),
activation = 'relu',
padding = 'same') %>%
layer_upsampling_2d(c(2,2)) %>%
layer_conv_2d(filters = 8,
kernel_size = c(3,3),
activation = 'relu',
padding = 'same') %>%
layer_upsampling_2d(c(2,2)) %>%
layer_conv_2d(filters = 1,
kernel_size = c(3,3),
activation = 'sigmoid',
padding = 'same')
summary(decoder)
Output
Tensor("conv2d_25/Sigmoid:0", shape=(?, 28, 28, 1), dtype=float32)
在这里,编码器模型已经成为解码器模型的输入。对于解码器网络,我们使用了一个类似的结构,第一层卷积层有 4 个过滤器,第二层卷积层有 8 个过滤器。此外,我们现在使用的是上采样层,而不是池化层。第一个上采样层将高度和宽度变为 14 x 14,第二个上采样层将其恢复到原始的高度和宽度 28 x 28。在最后一层,我们使用了 sigmoid 激活函数,确保输出值保持在 0 到 1 之间。
自编码器模型
自编码器模型及其模型摘要,显示每层的输出形状和参数数量如下:
# Autoencoder
ae_model <- keras_model(inputs = input_layer, outputs = decoder)
summary(ae_model)
__________________________________________________________________________
Layer (type) Output Shape Param #
==========================================================================
input_5 (InputLayer) (None, 28, 28, 1) 0
__________________________________________________________________________
conv2d_21 (Conv2D) (None, 28, 28, 8) 80
__________________________________________________________________________
max_pooling2d_9 (MaxPooling2D) (None, 14, 14, 8) 0
__________________________________________________________________________
conv2d_22 (Conv2D) (None, 14, 14, 4) 292
__________________________________________________________________________
max_pooling2d_10 (MaxPooling2D) (None, 7, 7, 4) 0
__________________________________________________________________________
conv2d_23 (Conv2D) (None, 7, 7, 4) 148
___________________________________________________________________________
up_sampling2d_9 (UpSampling2D) (None, 14, 14, 4) 0
___________________________________________________________________________
conv2d_24 (Conv2D) (None, 14, 14, 8) 296
___________________________________________________________________________
up_sampling2d_10 (UpSampling2D) (None, 28, 28, 8) 0
___________________________________________________________________________
conv2d_25 (Conv2D) (None, 28, 28, 1) 73
===========================================================================
Total params: 889
Trainable params: 889
Non-trainable params: 0
____________________________________________________________________________________
在这里,自编码器模型有五个卷积层、两个最大池化层和两个上采样层,除了输入层。此自编码器模型的总参数数量为 889。
编译和拟合模型
接下来,我们将使用以下代码编译并拟合模型:
# Compile model
ae_model %>% compile( loss='mean_squared_error',
optimizer='adam')
# Fit model
model_one <- ae_model %>% fit(trainx,
trainx,
epochs = 20,
shuffle=TRUE,
batch_size = 32,
validation_data = list(testx,testx))
在这里,我们使用均方误差作为损失函数编译模型,并指定adam作为优化器。为了训练模型,我们将使用trainx作为输入和输出。我们将使用textx作为验证集。我们使用 32 的批次大小,并进行 20 次训练。
以下输出显示了训练数据和验证数据的损失值图表:

上面的图表显示了良好的收敛性,并且没有出现过拟合的迹象。
重建图像
为了获得重建图像,我们使用predict_on_batch来使用自编码器模型预测输出。我们使用以下代码来完成这一操作:
# Reconstruct and plot images - train data
rc <- ae_model %>% keras::predict_on_batch(x = trainx)
par(mfrow = c(2,5), mar = rep(0, 4))
for (i in 1:5) plot(as.raster(trainx[i,,,]))
for (i in 1:5) plot(as.raster(rc[i,,,]))
来自训练数据的前五张时尚图像(第一行)和对应的重建图像(第二行)如下:

在这里,正如预期的那样,重建的图像捕捉到了训练图像的关键特征。然而,它忽略了一些更细致的细节。例如,原始训练图像中更清晰可见的徽标在重建图像中变得模糊。
我们还可以查看使用测试数据中的图像绘制的原始图像和重建图像的图表。为此,我们可以使用以下代码:
# Reconstruct and plot images - train data
rc <- ae_model %>% keras::predict_on_batch(x = testx)
par(mfrow = c(2,5), mar = rep(0, 4))
for (i in 1:5) plot(as.raster(testx[i,,,]))
for (i in 1:5) plot(as.raster(rc[i,,,]))
以下图像显示了使用测试数据的原始图像(第一行)和重建图像(第二行):

在这里,重建的图像与训练数据的表现相同。
在这个示例中,我们使用了 MNIST 时尚数据构建了一个自编码器网络,通过保留主要特征并去除涉及细节的特征,帮助降低图像的维度。接下来,我们将探讨自编码器模型的另一个变种,它有助于去除图像中的噪声。
去噪自编码器
在输入图像包含不必要的噪声的情况下,可以训练自编码器网络来去除这些噪声。这是通过将含有噪声的图像作为输入,并提供相同图像的干净版本作为输出来实现的。自编码器网络被训练,使得自编码器的输出尽可能接近目标图像。
MNIST 数据
我们将利用 Keras 包中提供的 MNIST 数据来演示创建去噪自编码器网络的步骤。可以使用以下代码读取 MNIST 数据:
# MNIST data
mnist <- dataset_mnist()
str(mnist)
List of 2
$ train:List of 2
..$ x: int [1:60000, 1:28, 1:28] 0 0 0 0 0 0 0 0 0 0 ...
..$ y: int [1:60000(1d)] 5 0 4 1 9 2 1 3 1 4 ...
$ test :List of 2
..$ x: int [1:10000, 1:28, 1:28] 0 0 0 0 0 0 0 0 0 0 ...
..$ y: int [1:10000(1d)] 7 2 1 0 4 1 4 9 5 9 ...
MNIST 数据的结构表明它包含训练数据和测试数据,以及相应的标签。训练数据包含 60,000 张 0 到 9 的数字图像。类似地,测试数据包含 10,000 张 0 到 9 的数字图像。尽管每张图像都有一个相应的标签来标识该图像,但在这个示例中,标签数据不需要,因此我们将忽略这部分信息。
我们将把训练图像存储在trainx中,把测试图像存储在testx中。为此,我们将使用以下代码:
# Train and test data
trainx <- mnist$train$x
testx <- mnist$test$x
# Plot
par(mfrow = c(8,8), mar = rep(0, 4))
for (i in 1:64) plot(as.raster(trainx[i,,], max = 255))
下图显示了基于 MNIST 中 0 到 9 之间数字图像的 64 张图像,排列成 8 行 8 列:

上述图展示了各种书写风格的手写数字。我们将把这些图像数据重新调整为所需格式,并向其添加随机噪声。
数据准备
接下来,我们将使用以下代码按要求的格式重新调整图像:
# Reshape
trainx <- array_reshape(trainx, c(nrow(trainx),28,28,1))
testx <- array_reshape(testx, c(nrow(testx),28,28,1))
trainx <- trainx / 255
testx <- testx / 255
在这里,我们已将训练数据重新调整为 60,000 x 28 x 28 x 1 的大小,并将测试数据调整为 10,000 x 28 x 28 x 1 的大小。我们还将介于 0 到 255 之间的像素值除以 255,从而得到一个新的范围,介于 0 和 1 之间。
添加噪声
要向训练图像添加噪声,我们需要使用以下代码生成 60,000 × 28 × 28 的随机数,这些随机数介于 0 和 1 之间,采用均匀分布:
# Random numbers from uniform distribution
n <- runif(60000*28*28,0,1)
n <- array_reshape(n, c(60000,28,28,1))
# Plot
par(mfrow = c(8,8), mar = rep(0, 4))
for (i in 1:64) plot(as.raster(n[i,,,]))
在这里,我们将使用均匀分布生成的随机数重新调整大小,以匹配我们训练图像矩阵的维度。结果将以图像的形式呈现,展示添加噪声后的图像。
下图展示了包含噪声的图像:

噪声图像被添加到存储在trainx中的图像中。我们需要将其除以 2,以保持结果trainn的值在 0 和 1 之间。我们可以使用以下代码来实现这一点:
# Adding noise to handwritten images - train data
trainn <- (trainx + n)/2
par(mfrow = c(8,8), mar = rep(0, 4))
for (i in 1:64) plot(as.raster(trainn[i,,,]))
以下图显示了前 64 个训练图像及其噪声:

尽管噪声已被添加到原始手写数字中,但这些数字依然可以辨认。使用去噪自编码器的主要目的是训练一个网络,能够保留手写数字并去除图像中的噪声。
我们将使用以下代码对测试数据重复相同的步骤:
# Adding noise to handwritten images - test data
n1 <- runif(10000*28*28,0,1)
n1 <- array_reshape(n1, c(10000,28,28,1))
testn <- (testx +n1)/2
在这里,我们已向测试图像添加噪声,并将它们存储在testn中。现在,我们可以指定编码器架构。
编码器模型
用于编码器网络的代码如下:
# Encoder
input_layer <-
layer_input(shape = c(28,28,1))
encoder <- input_layer %>%
layer_conv_2d(filters = 32,
kernel_size = c(3,3),
activation = 'relu',
padding = 'same') %>%
layer_max_pooling_2d(pool_size = c(2,2),
padding = 'same') %>%
layer_conv_2d(filters = 32,
kernel_size = c(3,3),
activation = 'relu',
padding = 'same') %>%
layer_max_pooling_2d(pool_size = c(2,2),
padding = 'same')
summary(encoder)
OutputTensor("max_pooling2d_6/MaxPool:0", shape=(?, 7, 7, 32), dtype=float32)
在这里,输入层的大小被指定为 28 x 28 x 1。我们使用了两个每个有 32 个滤波器的卷积层,并且使用了修正线性单元作为激活函数。每个卷积层后都接着池化层。第一次池化层后,高度和宽度变为 14 x 14,第二次池化层后,变为 7 x 7。本例中,编码器网络的输出具有 7 x 7 x 32 的维度。
解码器模型
对于解码器网络,我们保持相同的结构,不同之处在于,我们使用上采样层,而不是池化层。我们可以使用以下代码来实现这一点:
# Decoder
decoder <- encoder %>%
layer_conv_2d(filters = 32,
kernel_size = c(3,3),
activation = 'relu',
padding = 'same') %>%
layer_upsampling_2d(c(2,2)) %>%
layer_conv_2d(filters = 32,
kernel_size = c(3,3),
activation = 'relu',
padding = 'same') %>%
layer_upsampling_2d(c(2,2)) %>%
layer_conv_2d(filters = 1,
kernel_size = c(3,3),
activation = 'sigmoid',
padding = 'same')
summary(decoder)
Output
Tensor("conv2d_15/Sigmoid:0", shape=(?, 28, 28, 1), dtype=float32)
在前面的代码中,第一个上采样层将高度和宽度改变为 14 x 14,第二个上采样层将其恢复到原始的 28 x 28 高度和宽度。在最后一层,我们使用了 sigmoid 激活函数,确保输出值保持在 0 和 1 之间。
自编码器模型
现在,我们可以指定自编码器网络。自编码器的模型和摘要如下所示:
# Autoencoder
ae_model <- keras_model(inputs = input_layer, outputs = decoder)
summary(ae_model)
______________________________________________________________________
Layer (type) Output Shape Param #
======================================================================
input_3 (InputLayer) (None, 28, 28, 1) 0
______________________________________________________________________
conv2d_11 (Conv2D) (None, 28, 28, 32) 320
______________________________________________________________________
max_pooling2d_5 (MaxPooling2D) (None, 14, 14, 32) 0
_______________________________________________________________________
conv2d_12 (Conv2D) (None, 14, 14, 32) 9248
_______________________________________________________________________
max_pooling2d_6 (MaxPooling2D) (None, 7, 7, 32) 0
_______________________________________________________________________
conv2d_13 (Conv2D) (None, 7, 7, 32) 9248
_______________________________________________________________________
up_sampling2d_5 (UpSampling2D) (None, 14, 14, 32) 0
_______________________________________________________________________
conv2d_14 (Conv2D) (None, 14, 14, 32) 9248
_______________________________________________________________________
up_sampling2d_6 (UpSampling2D) (None, 28, 28, 32) 0
________________________________________________________________________
conv2d_15 (Conv2D) (None, 28, 28, 1) 289
========================================================================
Total params: 28,353
Trainable params: 28,353
Non-trainable params: 0
________________________________________________________________________
从前面自编码器网络的摘要中,我们可以看到总共有 28,353 个参数。接下来,我们将使用以下代码编译此模型:
# Compile model
ae_model %>% compile( loss='binary_crossentropy', optimizer='adam')
对于去噪自编码器,binary_crossentropy损失函数比其他选项表现更好。
在编译自编码器模型时,我们将使用binary_crossentropy作为损失函数,因为输入值介于 0 和 1 之间。对于优化器,我们将使用adam。编译模型后,我们准备好进行拟合。
拟合模型
为了训练模型,我们使用存储在trainn中的带噪声图像作为输入,使用存储在trainx中的无噪声图像作为输出。用于拟合模型的代码如下:
# Fit model
model_two <- ae_model %>% fit(trainn,
trainx,
epochs = 100,
shuffle = TRUE,
batch_size = 128,
validation_data = list(testn,testx))
在这里,我们还使用testn和testx来监控验证误差。我们将运行 100 个 epoch,每个批次的大小为 128。网络训练完成后,我们可以使用以下代码获取训练数据和测试数据的损失值:
# Loss for train data
ae_model %>% evaluate(trainn, trainx)
loss
0.07431865
# Loss for test data
ae_model %>% evaluate(testn, testx)
loss
0.07391542
训练数据和测试数据的损失分别为 0.0743 和 0.0739。这两个数字的接近度表明没有过拟合问题。
图像重建
在拟合模型后,我们可以使用以下代码重建图像:
# Reconstructing images - train data
rc <- ae_model %>% keras::predict_on_batch(x = trainn)
# Plot
par(mfrow = c(8,8), mar = rep(0, 4))
for (i in 1:64) plot(as.raster(rc[i,,,]))
在前面的代码中,我们使用ae_model通过提供包含噪声的trainn图像来重建图像。如以下图片所示,我们绘制了前 64 张重建图像,以查看噪声图像是否变得更清晰:

从前面的图表中,我们可以观察到自编码器网络已经成功去除噪声。我们还可以借助ae_model使用以下代码重建测试数据的图像:
# Reconstructing images - test data
rc <- ae_model %>% keras::predict_on_batch(x = testn)
par(mfrow = c(8,8), mar = rep(0, 4))
for (i in 1:64) plot(as.raster(rc[i,,,]))
测试数据中前 64 个手写数字的重建图像如下所示:

在这里,我们可以观察到去噪自编码器在去除 0 到 9 数字图像的噪声方面表现得相当不错。为了更仔细地观察模型的性能,我们可以绘制测试数据中的第一张图片、带噪声的相应图像以及去噪后重建的图像,如下所示:

在前面的截图中,第一张图像是原始图像,而第二张图像是在添加噪声之后得到的图像。自编码器将第二张图像作为输入,并且从模型获得的结果(第三张图像)被调整与第一张图像相匹配。在这里,我们可以看到去噪自编码器网络帮助去除了噪声。请注意,第三张图像无法保留原始图像中的一些细节,而这些细节在第一张图像中是可见的。例如,在原始图像中,数字七在开始和下部看起来比第三张图像中的七稍微粗一些。然而,它成功地从含有噪声的数字七图像中提取出了整体模式。
图像校正
在这个第三个应用中,我们将展示一个例子,在这个例子中我们将开发一个自编码器模型,用于去除图像上某些人工创建的标记。我们将使用 25 张包含横跨图像的黑色线条的图像。读取图像文件并进行相关处理的代码如下:
# Reading images and image processing
setwd("~/Desktop/peoplex")
temp = list.files(pattern="*.jpeg")
mypic <- list()
for (i in 1:length(temp)) {mypic[[i]] <- readImage(temp[i])}
for (i in 1:length(temp)) {mypic[[i]] <- resize(mypic[[i]], 128, 128)}
for (i in 1:length(temp)) {dim(mypic[[i]]) <- c(128, 128,3)}
在前面的代码中,我们从peoplex文件夹读取.jpeg扩展名的图像,并调整这些图像的尺寸,使其高度和宽度为 128 x 128。我们还将尺寸更新为 128 x 128 x 3,因为所有图像都是彩色图像。
需要校正的图像
我们将使用以下代码合并这 25 张图像,然后绘制它们:
# Combine and plot images
trainx <- combine(mypic)
str(trainx)
Formal class 'Image' [package "EBImage"] with 2 slots
..@ .Data : num [1:128, 1:128, 1:3, 1:16] 0.04435 0 0.00357 0.05779 0.05815 ...
..@ colormode: int 2
trainx <- aperm(trainx, c(4,1,2,3)
par(mfrow = c(4,4), mar = rep(0, 4))
for (i in 1:16) plot(as.raster(trainx[i,,,]))
在这里,我们将所有 25 张图像合并后的数据保存到trainx中。通过查看tranix的结构,我们可以看到,在合并图像数据后,尺寸现在变为 128 x 128 x 3 x 16。为了将其更改为所需的格式 16 x 128 x 128 x 3,我们使用aperm函数。然后,我们绘制所有 25 张图像。请注意,如果图像被旋转绘制,它们可以很容易地在任何计算机上调整到正确的方向。以下是带有黑色线条的 25 张图像:

在这个应用中的自编码器模型将使用带有黑色线条的图像作为输入,并且会经过训练以去除黑色线条。
干净的图像
我们还将读取没有黑色线条的相同 25 张图像,并将它们保存在trainy中,如下代码所示:
# Read image files without black line
setwd("~/Desktop/people")
temp = list.files(pattern="*.jpg")
mypic <- list()
for (i in 1:length(temp)) {mypic[[i]] <- readImage(temp[i])}
for (i in 1:length(temp)) {mypic[[i]] <- resize(mypic[[i]], 128, 128)}
for (i in 1:length(temp)) {dim(mypic[[i]]) <- c(128, 128,3)}
trainy <- combine(mypic)
trainy <- aperm(trainy, c(4,1,2,3))
par(mfrow = c(4,4), mar = rep(0, 4))
for (i in 1:16) plot(as.raster(trainy[i,,,]))
par(mfrow = c(1,1))
这里,在调整大小和更改尺寸后,我们像之前一样合并这些图像。我们还需要对尺寸进行一些调整,以获得所需的格式。接下来,我们将绘制所有 25 张干净的图像,如下所示:

在训练自编码器网络时,我们将使用这些干净的图像作为输出。接下来,我们将指定编码器模型架构。
编码器模型
对于编码器模型,我们将使用三个卷积层,分别具有 512、512 和 256 个滤波器,如下所示的代码所示:
# Encoder network
input_layer <- layer_input(shape = c(128,128,3))
encoder <- input_layer %>%
layer_conv_2d(filters = 512, kernel_size = c(3,3), activation = 'relu', padding = 'same') %>%
layer_max_pooling_2d(pool_size = c(2,2),padding = 'same') %>%
layer_conv_2d(filters = 512, kernel_size = c(3,3), activation = 'relu', padding = 'same') %>%
layer_max_pooling_2d(pool_size = c(2,2),padding = 'same') %>%
layer_conv_2d(filters = 256, kernel_size = c(3,3), activation = 'relu', padding = 'same') %>%
layer_max_pooling_2d(pool_size = c(2,2), padding = 'same')
summary(encoder)
Output
Tensor("max_pooling2d_22/MaxPool:0", shape=(?, 16, 16, 256), dtype=float32)
这里,编码器网络的尺寸是 16 x 16 x 256。我们将保持其他特性与之前两个示例中的编码器模型相似。现在,我们将指定自动编码器网络的解码器架构。
解码器模型
对于解码器模型,前 3 个卷积层的滤波器数量分别为 256、512 和 512,如下所示:
# Decoder network
decoder <- encoder %>%
layer_conv_2d(filters = 256, kernel_size = c(3,3), activation = 'relu',padding = 'same') %>%
layer_upsampling_2d(c(2,2)) %>%
layer_conv_2d(filters = 512, kernel_size = c(3,3), activation = 'relu',padding = 'same') %>%
layer_upsampling_2d(c(2,2)) %>%
layer_conv_2d(filters = 512, kernel_size = c(3,3), activation = 'relu',padding = 'same') %>%
layer_upsampling_2d(c(2,2)) %>%
layer_conv_2d(filters = 3, kernel_size = c(3,3), activation = 'sigmoid',padding = 'same')
summary(decoder)
Output
Tensor("conv2d_46/Sigmoid:0", shape=(?, 128, 128, 3), dtype=float32)
在这里,我们使用了上采样层。在最后一个卷积层中,我们使用了 sigmoid 激活函数。在最后一个卷积层中,我们使用了三个滤波器,因为我们处理的是彩色图像。最后,解码器模型的输出维度为 128 x 128 x 3。
编译和拟合模型
现在,我们可以使用以下代码编译和拟合模型:
# Compile and fit model
ae_model <- keras_model(inputs = input_layer, outputs = decoder)
ae_model %>% compile( loss='mse',
optimizer='adam')
model_three <- ae_model %>% fit(trainx,
trainy,
epochs = 100,
batch_size = 128,
validation_split = 0.2)
plot(model_three)
在前面的代码中,我们使用均方误差作为损失函数编译自动编码器模型,并指定adam作为优化器。我们使用包含黑线的图像trainx作为模型输入,使用包含干净图像的trainy作为模型尝试匹配的输出。我们将 epoch 数指定为 100,批次大小为 128。使用 0.2 或 20%的验证拆分,我们将从 25 张图像中选择 20 张用于训练,5 张用于计算验证误差。
以下图表显示了model_three在 100 个 epoch 中训练和验证图像的均方误差:

均方误差的图示显示,随着模型训练的进行,基于训练和验证数据,模型性能有了提升。我们还可以看到,在约 80 到 100 个 epoch 之间,模型的性能趋于平稳。此外,建议增加 epoch 的数量不太可能进一步提升模型性能。
从训练数据重建图像
现在,我们可以使用获得的模型从训练数据中重建图像。为此,我们可以使用以下代码:
# Reconstructing images - training
rc <- ae_model %>% keras::predict_on_batch(x = trainx)
par(mfrow = c(5,5), mar = rep(0, 4))
for (i in 1:25) plot(as.raster(rc[i,,,]))
在前面的代码中,我们使用了predict_on_batch来重建图像,输入的是包含黑线的trainx。所有 25 张重建图像可以在这里看到:

从前面的图示中可以看出,自动编码器模型已经学会了去除输入图像中的黑线。由于自动编码器模型尝试仅输出图像的主要特征,并忽略了某些细节,因此图像会显得有些模糊。
从新数据重建图像
为了使用新数据和未见过的数据测试自动编码器模型,我们将使用 25 张新图像,这些图像上有黑线。为此,我们将使用以下代码:
# 25 new images
setwd("~/Desktop/newx")
temp = list.files(pattern="*.jpg")
mypic <- list()
for (i in 1:length(temp)) {mypic[[i]] <- readImage(temp[i])}
for (i in 1:length(temp)) {mypic[[i]] <- resize(mypic[[i]], 128, 128)}
for (i in 1:length(temp)) {dim(mypic[[i]]) <- c(128, 128,3)}
newx <- combine(mypic)
newx <- aperm(newx, c(4,1,2,3))
par(mfrow = c(4,4), mar = rep(0, 4))
for (i in 1:16) plot(as.raster(newx[i,,,]))
如前面的代码所示,我们读取了新的图像数据,并将所有图像格式化,像之前一样。所有带有黑线的 25 张新图像展示在下图中:

在这里,所有 25 张图片上都有一条黑线。我们将使用这些新图片的数据,并利用我们开发的自动编码器模型来重建图片,去除黑线。用于重建和绘制图片的代码如下:
# Reconstructing images - new images
rc <- ae_model %>% keras::predict_on_batch(x = newx)
par(mfrow = c(5,5), mar = rep(0, 4))
for (i in 1:25) plot(as.raster(rc[i,,,]))
以下截图显示了基于这 25 张带有黑线的新图片,使用自动编码器模型后重建的图像:

上面的截图再次显示,自动编码器模型成功地去除了所有图像中的黑线。然而,正如我们之前观察到的,图像质量较低。这个例子提供了有希望的结果。如果得到的结果也能提供更高质量的图像输出,那么我们可以在多种不同的场景中使用这一方法。例如,我们可以将带眼镜的图像重建为不带眼镜的图像,反之亦然,或者我们可以将一个没有笑容的人的图像重建为带笑容的图像。这样的思路有许多变种,可能具有显著的商业价值。
总结
在本章中,我们介绍了自动编码器网络的三个应用示例。第一种类型的自动编码器涉及一个降维应用。在这里,我们使用了一种自动编码器网络架构,只允许我们学习输入图像的关键特征。第二种类型的自动编码器使用包含数字图像的 MNIST 数据进行说明。我们人为地为数字图像添加噪声,并以这样的方式训练网络,使其学会去除输入图像中的噪声。第三种类型的自动编码器网络涉及图像修复应用。在此应用中,自动编码器网络被训练来去除输入图像中的黑线。
在下一章,我们将介绍另一类深度网络,称为迁移学习,并将其用于图像分类。
第八章:使用迁移学习进行小数据集的图像分类
在前几章中,我们开发了深度学习网络,并探索了与图像数据相关的各种应用示例。与本章讨论的内容相比,前几章的一个主要区别是,我们在前几章中是从零开始开发模型的。
迁移学习可以定义为一种方法,我们重新利用一个已训练的深度网络所学到的知识来解决一个新的但相关的问题。例如,我们可能能够重新利用一个用于分类成千上万种时尚单品的深度学习网络,来开发一个用于分类三种不同款式裙子的深度网络。这种方法类似于我们在现实生活中观察到的情况,教师将多年来获得的知识传授给学生,或教练将经验传授给新球员。另一个例子是,学会骑自行车的经验可以转移到学会骑摩托车,而这又可以用于学习如何开车。
在本章中,我们将在开发图像分类模型时使用预训练的深度网络。预训练模型使我们能够将从更大数据集中学到的有用特征转移到我们感兴趣的模型中,这些模型可能使用一个相似但全新的较小数据集进行开发。使用预训练模型不仅能帮助我们克服由于数据集较小而产生的问题,还能减少开发模型的时间和成本。
为了说明预训练图像分类模型的使用,本章将涵盖以下主题:
-
使用预训练模型进行图像识别
-
使用 CIFAR10 数据集
-
使用 CNN 进行图像分类
-
使用预训练的 RESNET50 模型进行图像分类
-
模型评估与预测
-
性能优化技巧和最佳实践
使用预训练模型进行图像识别
在继续之前,让我们加载三个我们在本节中需要的包:
# Libraries used
library(keras)
library(EBImage)
library(tensorflow)
本章将使用 Keras 和 TensorFlow 库来开发预训练的图像分类模型,而 EBImage 库将用于处理和可视化图像数据。
在 Keras 中,以下预训练的图像分类模型是可用的:
-
Xception
-
VGG16
-
VGG19
-
ResNet50
-
InceptionV3
-
InceptionResNetV2
-
MobileNet
-
MobileNetV2
-
DenseNet
-
NASNet
这些预训练模型是在 ImageNet 数据集上训练的(www.image-net.org/)。ImageNet 是一个庞大的图像数据库,包含了数百万张图像。
我们将首先使用一个名为resnet50的预训练模型来识别图像。以下是我们可以用来利用这个预训练模型的代码:
# Pretrained model
pretrained <- application_resnet50(weights = "imagenet")
summary(pretrained)
在这里,我们将weights指定为"imagenet",这允许我们重用 RESNET50 网络的预训练权重。RESNET50 是一个深度残差网络,具有 50 层深度,包括卷积神经网络层。需要注意的是,如果我们只想使用模型架构而不使用预训练权重,且希望从头开始训练,我们可以将weights指定为null。通过使用summary,我们可以获得 RESNET50 网络的架构。不过,为了节省空间,我们不提供 summary 的输出。该网络的总参数数量为 25,636,712。RESNET50 网络在超过百万张来自 ImageNet 的图片上进行了训练,具备将图像分类到 1,000 个不同类别的能力。
读取图像
让我们从 RStudio 中读取一张狗的图片。以下代码加载一张图像文件,然后获得相应的输出:
使用 RESNET50 网络时,允许的最大目标大小为 224 x 224,允许的最小目标大小为 32 x 32。
# Read image data
setwd("~/Desktop")
img <- image_load("dog.jpg", target_size = c(224,224))
x <- image_to_array(img)
str(x)
OUTPUT
num [1:224, 1:224, 1:3] 70 69 68 73 88 79 18 22 21 20 ...
# Image plot
plot(as.raster(x, max = 255))
# Summary and histogram
summary(x)
OUTPUT
Min. 1st Qu. Median Mean 3rd Qu. Max.
0.0 89.0 150.0 137.7 190.0 255.0
hist(x)
在上面的代码中,我们可以观察到以下内容:
-
使用 Keras 中的
image_load()函数从计算机桌面加载一张大小为 224 x 224 的诺里奇梗犬图片。 -
请注意,原始图像可能不是 224 x 224 大小。然而,在加载图像时指定此尺寸可以让我们轻松调整原始图像的大小,使其具有新的维度。
-
这张图片通过
image_to_array()函数被转换成数字数组。该数组的结构显示其维度为 224 x 224 x 3。 -
数组的摘要显示它包含从零到 255 之间的数字。
以下是 224 x 224 大小的诺里奇梗犬的彩色图片。可以使用 plot 命令来获得该图片:

上面的图片是一只坐着并面向前方的诺里奇梗犬。我们将利用这张图片,检查 RESNET50 模型是否能准确预测图片中的狗的种类。
下面是从数组值生成的直方图:

上面的数组值的直方图显示,强度值从零到 255,大部分值集中在 200 左右。接下来,我们将对图像数据进行预处理。这个直方图可以用来比较图像数据的变化。
对输入进行预处理
我们现在可以对输入数据进行预处理,以便与预训练的 RESNET50 模型一起使用。预处理数据的代码如下:
# Preprocessing of input data
x <- array_reshape(x, c(1, dim(x)))
x <- imagenet_preprocess_input(x)
hist(x)
在上面的代码中,我们可以观察到以下内容:
-
应用
array_reshape()函数后,数组的维度将变为 1 x 224 x 224 x 3。 -
我们使用了
imagenet_preprocess_input()函数来使用预训练模型准备所需格式的数据。
预处理后数据的直方图如下所示:

预处理后的值的直方图显示了位置的变化。大多数值现在集中在 50 到 100 之间。然而,直方图的整体模式没有发生重大变化。
前五个类别
现在,我们可以使用预训练模型通过提供预处理后的图片数据作为输入来进行预测。实现这一目标的代码如下:
# Predictions for top 5 categories
preds <- pretrained %>% predict(x)
imagenet_decode_predictions(preds, top = 5)[[1]]
Output
class_name class_description score
1 n02094258 Norwich_terrier 0.769952953
2 n02094114 Norfolk_terrier 0.126662806
3 n02096294 Australian_terrier 0.046003290
4 n02096177 cairn 0.040896162
5 n02093991 Irish_terrier 0.005021056
在前面的代码中,我们可以观察到以下内容:
-
预测是使用
predict函数进行的,结果包含了 1,000 个不同类别的概率,最高的五个类别及其概率可以使用imagenet_decode_predictions()函数获取。 -
约 0.7699 的最高分数正确识别出图片是诺里奇梗犬。
-
第二高的分数是诺福克梗犬,它与诺里奇梗犬非常相似。
-
预测结果还表明,图片可能是另一种类型的梗犬;然而,这些概率相对较小或可以忽略不计。
在接下来的部分,我们将研究一个更大的图片数据集,而不是单张图片,并使用预训练的网络来开发图像分类模型。
使用 CIFAR10 数据集
为了展示如何使用预训练模型处理新数据,我们将使用 CIFAR10 数据集。CIFAR 代表 加拿大高级研究院,而 10 指的是数据中包含的 10 类图片。CIFAR10 数据集是 Keras 库的一部分,获取它的代码如下:
# CIFAR10 data
data <- dataset_cifar10()
str(data)
OUTPUT
List of 2
$ train:List of 2
..$ x: int [1:50000, 1:32, 1:32, 1:3] 59 154 255 28 170 159 164 28 134 125 ...
..$ y: int [1:50000, 1] 6 9 9 4 1 1 2 7 8 3 ...
$ test :List of 2
..$ x: int [1:10000, 1:32, 1:32, 1:3] 158 235 158 155 65 179 160 83 23 217 ...
..$ y: num [1:10000, 1] 3 8 8 0 6 6 1 6 3 1 ...
在前面的代码中,我们可以观察到以下内容:
-
我们可以使用
dataset_cifar10()函数读取数据集。 -
数据结构显示有 50,000 张带标签的训练图片。
-
它还包含 10,000 张带标签的测试图片。
接下来,我们将使用以下代码从 CIFAR10 中提取训练和测试数据:
# Partitioning the data into train and test
trainx <- data$train$x
testx <- data$test$x
trainy <- to_categorical(data$train$y, num_classes = 10)
testy <- to_categorical(data$test$y, num_classes = 10)
table(data$train$y)
OUTPUT
0 1 2 3 4 5 6 7 8 9
5000 5000 5000 5000 5000 5000 5000 5000 5000 5000
table(data$test$y)
OUTPUT
0 1 2 3 4 5 6 7 8 9
1000 1000 1000 1000 1000 1000 1000 1000 1000 1000
从前面的代码中,我们可以观察到以下内容:
-
我们将训练图片数据保存在
trainx中,测试图片数据保存在testx中。 -
我们还使用
to_categorical()函数对训练集和测试集的标签进行独热编码,并将结果分别保存在trainy和testy中。 -
训练数据表明,这些图片被分类为 10 个不同的类别,每个类别包含 5,000 张图片。
-
同样,测试数据也包含了每个类别 1,000 张图片,共 10 个类别。
作为示例,可以使用以下代码获取训练数据中前 64 张图片的标签:
# Category Labels
data$train$y[1:64,]
[1] 6 9 9 4 1 1 2 7 8 3 4 7 7 2 9 9 9 3 2 6 4 3 6 6 2 6 3 5 4 0 0 9 1
[34] 3 4 0 3 7 3 3 5 2 2 7 1 1 1 2 2 0 9 5 7 9 2 2 5 2 4 3 1 1 8 2
如我们所见,每张图片都使用 0 到 9 之间的数字进行标签。下表展示了 10 种不同类别图片的描述:
| 标签 | 描述 |
|---|---|
| 0 | 飞机 |
| 1 | 汽车 |
| 2 | 鸟类 |
| 3 | 猫 |
| 4 | 鹿 |
| 5 | 狗 |
| 6 | 青蛙 |
| 7 | 马 |
| 8 | Ship |
| 9 | Truck |
请注意,这 10 个类别之间没有重叠。例如,汽车类别指的是轿车和 SUV,而卡车类别仅指大型卡车。
示例图像
我们可以使用以下代码绘制 CIFAR10 训练数据中的前 64 张图像。通过这样做,我们可以初步了解数据集中包含的图像类型:
# Plot of first 64 pictures
par(mfrow = c(8,8), mar = rep(0, 4))
for (i in 1:64) plot(as.raster(trainx[i,,,], max = 255))
par(mfrow = c(1,1))
CIFAR10 数据集中的所有图像都是 32 x 32 的彩色图像。下图展示了一个 8 x 8 网格中的 64 张图像:

从前面的图像中,我们可以看到这些图像有各种背景,并且分辨率较低。此外,有时这些图像并不完全可见,这使得图像分类变得具有挑战性。
预处理与预测
我们可以使用预训练的 RESNET50 模型来识别训练数据中的第二张图像。请注意,由于训练数据中的第二张图像大小为 32 x 32,而 RESNET50 模型是在 224 x 224 的图像上训练的,因此我们需要在应用之前使用的代码前对图像进行调整大小。以下代码用于识别该图像:
# Pre-processing and prediction
x <- resize(trainx[2,,,], w = 224, h = 224)
x <- array_reshape(x, c(1, dim(x)))
x <- imagenet_preprocess_input(x)
preds <- pretrained %>% predict(x)
imagenet_decode_predictions(preds, top = 5)[[1]]
OUTPUT
class_name class_description score
1 n03796401 moving_van 9.988740e-01
2 n04467665 trailer_truck 7.548324e-04
3 n03895866 passenger_car 2.044246e-04
4 n04612504 yawl 2.441246e-05
5 n04483307 trimaran 1.862814e-05
从前面的代码中,我们可以看到,得分最高的类别是 0.9988 的搬家车。其他四个类别的得分相对较低。
使用 CNN 进行图像分类
在本节中,我们将使用 CIFAR10 数据集的一个子集来开发一个基于卷积神经网络的图像分类模型,并评估其分类性能。
数据准备
我们将通过仅使用 CIFAR10 训练和测试数据中的前 2,000 张图像来保持数据大小较小。这将使得图像分类模型能够在普通计算机或笔记本上运行。我们还将把训练和测试图像从 32 x 32 的尺寸调整到 224 x 224 的尺寸,以便能够与预训练模型比较分类性能。以下代码包括了我们在本章之前讲解的必要预处理:
# Selecting first 2000 images
trainx <- data$train$x[1:2000,,,]
testx <- data$test$x[1:2000,,,]
# One-hot encoding
trainy <- to_categorical(data$train$y[1:2000,], num_classes = 10)
testy <- to_categorical(data$test$y[1:2000,] , num_classes = 10)
# Resizing train images to 224x224
x <- array(rep(0, 2000 * 224 * 224 * 3), dim = c(2000, 224, 224, 3))
for (i in 1:2000) { x[i,,,] <- resize(trainx[i,,,], 224, 224) }
# Plot of before/after resized image
par(mfrow = c(1,2), mar = rep(0, 4))
plot(as.raster(trainx[2,,,], max = 255))
plot(as.raster(x[2,,,], max = 255))
par(mfrow = c(1,1))
trainx <- imagenet_preprocess_input(x)
# Resizing test images to 224x224
x <- array(rep(0, 2000 * 224 * 224 * 3), dim = c(2000, 224, 224, 3))
for (i in 1:2000) { x[i,,,] <- resize(testx[i,,,], 224, 224) }
testx <- imagenet_preprocess_input(x)
在前面的代码中,在将图像尺寸从 32 x 32 调整到 224 x 224 时,我们使用了双线性插值,这是 EBImage 包的一部分。双线性插值是将线性插值扩展到两个变量,在本例中是图像的高度和宽度。双线性插值的效果可以通过下图中显示的卡车的前后图像来观察:

在这里,我们可以看到,第二张图像(后图)看起来更加平滑,因为它包含的像素更多,相较于原始图像(第一张图像)。
CNN 模型
我们将从使用一个不那么深的卷积神经网络开始,开发一个图像分类模型。我们将使用以下代码:
# Model architecture
model <- keras_model_sequential()
model %>%
layer_conv_2d(filters = 32, kernel_size = c(3,3), activation = 'relu',
input_shape = c(224,224,3)) %>%
layer_conv_2d(filters = 32, kernel_size = c(3,3), 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.25) %>%
layer_dense(units = 10, activation = 'softmax')
summary(model)
_________________________________________________________________________
Layer (type) Output Shape Param #
=========================================================================
conv2d_6 (Conv2D) (None, 222, 222, 32) 896
_________________________________________________________________________
conv2d_7 (Conv2D) (None, 220, 220, 32) 9248
_________________________________________________________________________
max_pooling2d_22 (MaxPooling2D) (None, 110, 110, 32) 0
_________________________________________________________________________
dropout_6 (Dropout) (None, 110, 110, 32) 0
_________________________________________________________________________
flatten_18 (Flatten) (None, 387200) 0
__________________________________________________________________________
dense_35 (Dense) (None, 256) 99123456
__________________________________________________________________________
dropout_7 (Dropout) (None, 256) 0
__________________________________________________________________________
dense_36 (Dense) (None, 10) 2570
==========================================================================
Total params: 99,136,170
Trainable params: 99,136,170
Non-trainable params: 0
___________________________________________________________________________________________
# Compile
model %>% compile(loss = 'categorical_crossentropy',
optimizer = 'rmsprop',
metrics = 'accuracy')
# Fit
model_one <- model %>% fit(trainx,
trainy,
epochs = 10,
batch_size = 10,
validation_split = 0.2)
从前面的代码中,我们可以观察到以下内容:
-
该网络的总参数数为 99,136,170。
-
在编译模型时,我们使用
categorical_crossentropy作为损失函数,因为响应有 10 个类别。 -
对于优化器,我们指定了
rmsprop,这是一种基于梯度的优化方法,是一种广泛使用的选择,能够提供相当不错的性能。 -
我们用 10 个周期和批次大小为 10 来训练模型。
-
在 2,000 张训练数据图像中,20%(即 400 张图像)用于评估验证错误,剩余 80%(即 1,600 张图像)用于训练。
对model_one训练后准确率和损失值的图表如下:

从之前的图表中,可以得出以下观察结果:
-
准确率和损失值的图表显示,在大约 4 个周期后,训练数据和验证数据的损失和准确率值保持相对稳定。
-
尽管训练数据的准确率接近 100%的高值,但验证数据中的图像似乎对准确率没有影响。
-
此外,训练数据和验证数据的准确率差距似乎较大,表明存在过拟合问题。在评估模型性能时,我们预计图像分类的准确率较低。
请注意,使用 CNN 开发一个合适的图像分类模型需要大量的图像进行训练,因此需要更多的时间和资源。在本章后面,我们将学习如何使用预训练网络来帮助我们克服这个问题。不过,现在我们先继续评估图像分类模型的性能。
模型性能
为了评估模型的性能,我们将对训练数据和测试数据的损失、准确率和混淆矩阵进行计算。
使用训练数据进行性能评估
获取基于训练数据的损失、准确率和混淆矩阵的代码如下:
# Loss and accuracy
model %>% evaluate(trainx, trainy)
$loss
[1] 3.335224
$acc
[1] 0.8455
# Confusion matrix
pred <- model %>% predict_classes(trainx)
table(Predicted=pred, Actual=data$train$y[1:2000,])
Actual
Predicted 0 1 2 3 4 5 6 7 8 9
0 182 2 8 2 9 4 1 2 10 5
1 1 176 3 5 6 5 2 3 4 7
2 1 0 167 4 3 4 3 2 0 1
3 0 0 0 157 2 1 1 2 1 0
4 2 1 5 6 167 4 2 1 0 0
5 2 0 4 4 3 149 3 4 4 3
6 1 1 3 6 5 2 173 5 0 0
7 3 2 4 2 4 3 9 166 0 1
8 10 1 7 1 6 4 2 2 173 5
9 0 8 2 8 9 7 11 12 11 181
在这里,我们可以看到训练数据的损失和准确率值分别为 3.335 和 0.846。混淆矩阵显示出基于训练数据的不错结果。然而,对于某些类型的图像,误分类率较高。例如,来自类别 7(马)的 12 张图像被误分类为类别 9(卡车)。类似地,11 张分别属于类别 6(青蛙)和类别 8(船)的图像,也被误分类为类别 9(卡车)。
使用测试数据进行性能评估
获取基于测试数据的损失、准确率和混淆矩阵的代码如下:
# Loss and accuracy
model %>% evaluate(testx, testy)
$loss
[1] 16.4562
$acc
[1] 0.2325
# Confusion matrix
pred <- model %>% predict_classes(testx)
table(Predicted = pred, Actual = data$test$y[1:2000,])
Actual
Predicted 0 1 2 3 4 5 6 7 8 9
0 82 24 29 17 16 10 17 19 67 19
1 16 65 20 26 18 21 26 26 33 53
2 10 0 26 20 20 18 14 5 1 2
3 6 5 8 21 12 22 9 12 9 3
4 4 8 22 11 22 16 25 9 6 4
5 5 7 12 29 17 29 9 19 4 9
6 6 6 20 17 23 15 51 25 6 13
7 3 10 10 15 21 16 11 37 3 5
8 34 22 20 12 22 2 7 7 61 24
9 30 51 28 31 27 36 47 34 27 71
从之前的输出中,可以得出以下观察结果:
-
测试数据的损失和准确率值分别为 16.456 和 0.232。
-
由于过拟合问题,这些结果没有训练数据中那么令人印象深刻。
尽管我们可以尝试开发一个更深的网络,以改善图像分类结果,或尝试增加训练数据以提供更多的样本进行学习,但在这里,我们将利用预训练网络来获得更好的结果。
使用预训练的 RESNET50 模型进行图像分类
在本节中,我们将使用预训练的 RESNET50 模型来开发一个图像分类模型。我们将使用与前一节相同的训练和测试数据,这样便于比较分类性能。
模型架构
我们将上传不包含顶层的 RESNET50 模型。这将帮助我们定制预训练模型,以便与 CIFAR10 数据一起使用。由于 RESNET50 模型是通过超过 100 万张图像训练得到的,它捕捉到了有用的特征和图像表示,可以与新的但相似且较小的数据一起重用。预训练模型的这种可重用性不仅有助于减少从头开始开发图像分类模型的时间和成本,而且在训练数据相对较小时尤其有用。
用于开发模型的代码如下:
# RESNET50 network without the top layer
pretrained <- application_resnet50(weights = "imagenet",
include_top = FALSE,
input_shape = c(224, 224, 3))
model <- keras_model_sequential() %>%
pretrained %>%
layer_flatten() %>%
layer_dense(units = 256, activation = "relu") %>%
layer_dense(units = 10, activation = "softmax")
summary(model)
_______________________________________________________________________
Layer (type) Output Shape Param #
=======================================================================
resnet50 (Model) (None, 7, 7, 2048) 23587712
________________________________________________________________________
flatten_6 (Flatten) (None, 100352) 0
________________________________________________________________________
dense_12 (Dense) (None, 256) 25690368
________________________________________________________________________
dense_13 (Dense) (None, 10) 2570
========================================================================
Total params: 49,280,650
Trainable params: 49,227,530
Non-trainable params: 53,120
_________________________________________________________________________
上传 RESNET50 模型时,基于彩色图像的数据输入维度指定为 224 x 224 x 3。尽管较小的维度也能工作,但图像维度不能小于 32 x 32 x 3。CIFAR10 数据集中的图像维度是 32 x 32 x 3,但我们已经将它们调整为 224 x 224 x 3,因为这样可以提高图像分类的准确性。
从前面的总结中,我们可以观察到以下几点:
-
RESNET50 网络的输出维度是 7 x 7 x 2,048。
-
我们使用一个扁平层将输出形状更改为一个单列,其中 7 x 7 x 2,048 = 100,352 个元素。
-
添加了一个具有 256 个单元的密集层,并使用
relu激活函数。 -
这个密集层导致了(100,353 x 256)+ 256 = 25,690,368 个参数。
-
最后一个密集层具有 10 个单元,用于 10 个类别的图像,并使用
softmax激活函数。该网络共有 49,280,650 个参数。 -
网络中总参数中,有 49,227,530 个是可训练的参数。
尽管我们可以使用这些所有参数来训练网络,但这并不建议。训练和更新与 RESNET50 网络相关的参数会使我们失去从超过 100 万张图像中学习到的特征带来的好处。我们只使用了 2,000 张图像的数据进行训练,并且有 10 个不同的类别。因此,每个类别只有大约 200 张图像。因此,冻结 RESNET50 网络中的权重非常重要,这将使我们能够获得使用预训练网络的好处。
冻结预训练网络的权重
冻结 RESNET50 网络权重并编译模型的代码如下:
# Freeze weights of resnet50 network
freeze_weights(pretrained)
# Compile
model %>% compile(loss = 'categorical_crossentropy',
optimizer = 'rmsprop',
metrics = 'accuracy')
summary(model)
______________________________________________________
Layer (type) Output Shape Param #
======================================================
resnet50 (Model) (None, 7, 7, 2048) 23587712
______________________________________________________
flatten_6 (Flatten) (None, 100352) 0
______________________________________________________
dense_12 (Dense) (None, 256) 25690368
______________________________________________________
dense_13 (Dense) (None, 10) 2570
======================================================
Total params: 49,280,650
Trainable params: 25,692,938
Non-trainable params: 23,587,712
______________________________________________________
在前面的代码中,我们可以观察到以下几点:
-
为了冻结 RESNET50 网络中的权重,我们使用
freeze_weights()函数。 -
请注意,在冻结预训练网络权重之后,模型需要重新编译。
-
在冻结了 RESNET50 网络的权重后,我们观察到可训练的参数数量从 49,227,530 下降到较低的 25,692,938。
-
这些参数属于我们添加的两个全连接层,将帮助我们定制来自 RESNET50 网络的结果,以便我们能够将其应用于我们正在使用的 CIFAR10 数据的图像。
拟合模型
拟合模型的代码如下:
# Fit model
model_two <- model %>% fit(trainx,
trainy,
epochs = 10,
batch_size = 10,
validation_split = 0.2)
从之前的代码中,我们可以观察到以下情况:
-
我们用 10 个 epoch 和 10 的批次大小训练网络。
-
我们指定 20%(或 400 张图像)用于评估验证损失和验证准确率,剩余的 80%(或 1,600 张图像)用于训练。
训练模型后的准确率和损失值图表如下所示:

从损失和准确率值的图表中,我们可以得出以下观察结果:
-
与之前没有使用预训练模型的图表相比,这里有一个重要的区别。这个图表显示,在第二个 epoch 时,模型的准确率超过了 60%,而在之前的图表中,它的准确率一直低于 25%。由此我们可以看到,使用预训练模型对图像分类有直接影响。
-
基于验证数据的改进相比于训练数据来说较为缓慢。
-
尽管基于验证数据的准确率值逐渐提高,但验证数据的损失值显示出更多的波动。
在接下来的部分,我们将评估模型并评估其预测性能。
模型评估与预测
现在,我们将评估该模型在训练数据和测试数据上的表现。将进行关于损失、准确率和混淆矩阵的计算,以便我们评估模型在图像分类方面的表现。我们还将获取 10 个类别中每个类别的准确率。
使用训练数据的损失、准确率和混淆矩阵
获取训练数据的损失、准确率和混淆矩阵的代码如下:
# Loss and accuracy
model %>% evaluate(trainx, trainy)
$loss
[1] 1.954347
$acc
[1] 0.8785
# Confusion matrix
pred <- model %>% predict_classes(trainx)
table(Predicted=pred, Actual=data$train$y[1:2000,])
Actual
Predicted 0 1 2 3 4 5 6 7 8 9
0 182 0 5 2 3 0 2 0 10 1
1 1 156 1 1 1 0 2 0 4 0
2 2 0 172 3 4 0 4 0 1 0
3 0 0 1 133 2 12 2 1 0 0
4 1 0 8 4 188 3 4 2 0 0
5 1 0 4 22 3 162 1 3 0 0
6 0 0 3 9 3 0 192 1 1 0
7 3 0 5 10 10 5 0 188 0 0
8 5 0 3 3 0 1 0 1 182 0
9 7 35 1 8 0 0 0 3 5 202
# Accuracy for each category
100*diag(tab)/colSums(tab)
0 1 2 3 4
90.09901 81.67539 84.72906 68.20513 87.85047
5 6 7 8 9
88.52459 92.75362 94.47236 89.65517 99.50739
从之前的输出中,我们可以得出以下观察结果:
-
基于训练数据的损失和准确率分别为 1.954 和 0.879。
-
这两个数值都优于之前模型中对应的结果。
-
混淆矩阵显示了相当不错的图像分类表现。
-
在第 9 类(卡车)中,图像分类表现最好,只有一张图像被错误分类为第 0 类(飞机),并且提供了 99.5%的准确率。
-
该模型在第 3 类(猫)上表现最差,这类图像通常被错误分类为第 5 类(狗)或第 7 类(马),并且该类别的准确率仅为 68.2%。
-
在错误分类中,最多的是类别-1(汽车)被错误分类为类别-9(卡车),共有 35 张图片。
接下来,我们将使用测试数据评估模型的性能。
基于测试数据的损失、准确率和混淆矩阵
获取测试数据的损失、准确率和混淆矩阵的代码如下:
# Loss and accuracy
model %>% evaluate(testx, testy)
$loss
[1] 4.437256
$acc
[1] 0.768
# Confusion matrix
pred <- model %>% predict_classes(testx)
table(Predicted = pred, Actual = data$test$y[1:2000,])
Actual
Predicted 0 1 2 3 4 5 6 7 8 9
0 158 1 12 0 5 1 6 2 15 0
1 3 142 0 2 0 2 3 1 9 2
2 2 0 139 8 6 3 6 0 0 0
3 0 0 3 86 5 13 6 1 0 0
4 4 0 14 6 138 5 10 4 1 0
5 0 0 15 47 6 148 2 12 0 0
6 0 0 4 12 9 3 178 0 0 0
7 2 0 4 23 27 9 3 169 0 0
8 13 1 1 5 1 0 0 0 179 2
9 14 54 3 10 1 1 2 4 13 199
# Accuracy for each category
100*diag(tab)/colSums(tab)
0 1 2 3 4
80.61224 71.71717 71.28205 43.21608 69.69697
5 6 7 8 9
80.00000 82.40741 87.56477 82.48848 98.02956
从上述输出中,我们可以得出以下观察结果:
-
基于测试数据的损失和准确率分别为 4.437 和 0.768。
-
尽管基于测试数据的性能不如基于训练数据的结果,但相比于第一个模型的结果,这已是一个显著的改进。
-
混淆矩阵提供了对模型性能的进一步见解。最佳性能出现在类别 9(卡车),共有 199 个正确分类,准确率为 98%。
-
对于测试数据,模型在类别-3(猫)上最容易混淆,这一类别的误分类最多,准确率低至 43.2%。
-
单一类别的最大误分类情况(54 张图片)出现在类别-1(汽车),它被错误分类为类别-9(卡车)。
以 76.8%的准确率来看,可以说这次图像分类的性能还算不错。使用预训练模型让我们得以将训练过的包含超过 100 万张图像的数据模型的学习迁移到包含 2000 张 CIFAR10 数据集图像的新数据上。这相比于从零开始构建图像分类模型要有很大优势,后者需要更多的时间和计算成本。现在我们已经取得了不错的性能,接下来我们可以探讨如何进一步提升它。
性能优化技巧和最佳实践
为了进一步探索图像分类的提升,在这一部分,我们将进行三个实验。第一个实验中,我们将主要使用adam优化器来编译模型。在第二个实验中,我们将通过调整密集层单元数、丢弃层的丢弃率以及拟合模型时的批量大小来进行超参数调优。最后,在第三个实验中,我们将使用另一种预训练网络——VGG16。
使用 adam 优化器进行实验
在这个第一个实验中,我们将使用adam优化器来编译模型。在训练模型时,我们还将把训练轮数增加到 20。
训练模型后的准确率和损失值图如下:

上述模型的损失和准确率图显示,训练数据的相关值在大约六个训练轮后趋于平稳。而对于验证数据,损失值呈逐渐上升趋势,而准确率在第三个训练轮后趋于平稳。
获取测试数据的损失、准确率和混淆矩阵的代码如下:
# Loss and accuracy
model %>% evaluate(testx, testy)
$loss
[1] 4.005393
$acc
[1] 0.7715
# Confusion matrix
pred <- model %>% predict_classes(testx)
table(Predicted = pred, Actual = data$test$y[1:2000,])
Actual
Predicted 0 1 2 3 4 5 6 7 8 9
0 136 0 20 4 2 0 1 5 2 4
1 3 177 1 1 0 0 0 0 2 26
2 7 0 124 2 3 1 3 0 1 0
3 2 0 4 80 7 6 7 2 1 0
4 3 1 18 9 151 4 8 9 0 0
5 2 0 3 58 3 152 4 5 0 3
6 3 2 8 22 8 8 190 0 6 2
7 1 0 14 18 22 14 2 172 0 0
8 36 11 3 5 2 0 1 0 205 12
9 3 7 0 0 0 0 0 0 0 156
# Accuracy for each category
100*diag(tab)/colSums(tab)
0 1 2 3 4
69.38776 89.39394 63.58974 40.20101 76.26263
5 6 7 8 9
82.16216 87.96296 89.11917 94.47005 76.84729
从上述输出中,我们可以做出以下观察:
-
测试数据的损失和准确率分别为 4.005 和 0.772。
-
这些结果相比于
model_two略有改善。 -
混淆矩阵显示了与前一个模型相比,稍有不同的图像分类模式。
-
最好的分类结果出现在类别 8(船)上,在 217 张图像中正确分类了 205 张(准确率 94.5%)。
-
分类性能最差的是类别 3(猫),在 199 张图像中正确预测了 80 张(准确率 40.2%)。
-
最严重的误分类是 58 张来自类别 3(猫)的图片,它们被误分类为类别 5(狗)。
接下来,我们将进行超参数调优实验。
超参数调优
在本实验中,我们将改变密集层中的单元数、dropout 率和批次大小,以获得能够提高分类性能的值。这也展示了一种通过实验获取适当参数值的高效方式。我们将从使用以下代码创建TransferLearning.R文件开始:
# Model with RESNET50
pretrained <- application_resnet50(weights = 'imagenet',
include_top = FALSE,
input_shape = c(224, 224, 3))
# Flags for hyperparameter tuning
FLAGS <- flags(flag_integer("dense_units", 256),
flag_numeric("dropout", 0.1),
flag_integer("batch_size", 10))
# Model architecture
model <- keras_model_sequential() %>%
pretrained %>%
layer_flatten() %>%
layer_dense(units = FLAGS$dense_units, activation = 'relu') %>%
layer_dropout(rate = FLAGS$dropout) %>%
layer_dense(units = 10, activation = 'softmax')
freeze_weights(pretrained)
# Compile
model %>% compile(loss = "categorical_crossentropy",
optimizer = 'adam',
metrics = 'accuracy')
# Fit model
history <- model %>% fit(trainx,
trainy,
epochs = 5,
batch_size = FLAGS$batch_size,
validation_split = 0.2)
在上述代码中,读取预训练模型后,我们声明了三个用于实验的参数标志。现在,我们可以在模型架构(密集单元和 dropout 率)中使用这些标志,并在拟合模型的代码中使用(批次大小)。我们已将训练周期减少为 5,并且在编译模型时保留了adam优化器。我们将把这个 R 文件保存为TransferLearning.R,并保存在计算机桌面上。
运行此实验的代码如下:
# Set working directory
setwd('~/Desktop')
# Hyperparameter tuning
library(tfruns)
runs <- tuning_run("TransferLearning.R",
flags = list(dense_units = c(256, 512),
dropout = c(0.1,0.3),
batch_size = c(10, 30)))
在上述代码中,我们可以看到工作目录已设置为TransferLearning.R文件所在的位置。请注意,本实验的输出也将保存在此目录中。为了运行超参数调优实验,我们将使用tfruns库。对于密集层中的单元数,我们将尝试 256 和 512 作为值。对于 dropout 率,我们将尝试 0.1 和 0.3。最后,对于批次大小,我们将尝试 10 和 30。考虑到有三个参数,每个参数有两个可能的值,总的实验运行次数为 2³ = 8。
从本次实验获得的结果摘录如下:
# Results
runs[,c(6:10)]
Data frame: 8 x 5
metric_val_loss metric_val_acc flag_dense_units flag_dropout flag_batch_size
1 1.1935 0.7525 512 0.3 30
2 0.9521 0.7725 256 0.3 30
3 1.1260 0.8200 512 0.1 30
4 1.3276 0.7950 256 0.1 30
5 1.1435 0.7700 512 0.3 10
6 1.3096 0.7275 256 0.3 10
7 1.3458 0.7850 512 0.1 10
8 1.0248 0.7950 256 0.1 10
上述输出显示了基于验证数据的损失和准确率值,适用于所有 8 次实验运行。为了方便参考,输出还包括了参数值。我们可以从上述输出中做出以下观察:
-
当密集单元数量为 512,dropout 率为 0.1,批次大小为 30 时,获得了最高的准确率值(第 3 行)。
-
另一方面,当密集单元数量为 256,dropout 率为 0.3,批次大小为 10 时,获得了最低的准确率值(第 6 行)。
获取损失、准确率和混淆矩阵的代码如下,适用于实验第 3 行的测试数据:
# Loss and accuracy
model %>% evaluate(testx, testy)
$loss
[1] 1.095251
$acc
[1] 0.7975
# Confusion matrix
pred <- model %>% predict_classes(testx)
(tab <- table(Predicted = pred, Actual = data$test$y[1:2000,]))
Actual
Predicted 0 1 2 3 4 5 6 7 8 9
0 167 5 20 4 5 4 4 15 10 8
1 1 176 0 3 0 0 1 1 2 15
2 3 0 139 9 2 4 8 1 1 0
3 0 0 3 92 6 6 5 0 0 0
4 4 0 20 16 177 12 17 23 0 1
5 0 0 7 50 1 149 1 9 0 0
6 1 0 2 11 2 5 177 1 0 1
7 0 0 0 5 3 4 1 143 0 0
8 16 3 3 5 2 0 1 0 203 6
9 4 14 1 4 0 1 1 0 1 172
# Accuracy for each category
100*diag(tab)/colSums(tab)
0 1 2 3 4 5 6 7
85.20408 88.88889 71.28205 46.23116 89.39394 80.54054 81.94444 74.09326
8 9
93.54839 84.72906
从前面的结果中,我们可以得出以下观察结论:
-
测试数据的损失和准确率均优于我们迄今为止获得的结果。
-
最好的分类结果来自类别 8(船),在 217 张图像中正确分类了 203 张(准确率 93.5%)。
-
分类性能最低的是类别 3(猫),在 199 张图像中正确预测了 92 张(准确率 46.2%)。
-
最严重的误分类是将 50 张类别 3(猫)图像误分类为类别 5(狗)。
在下一次实验中,我们将使用另一个预训练网络:VGG16。
使用 VGG16 作为预训练网络进行实验
在这次实验中,我们将使用一个名为 VGG16 的预训练网络。VGG16 是一个 16 层深的卷积神经网络,可以将图像分类到成千上万的类别。这个网络还使用来自 ImageNet 数据库的超过 100 万张图像进行了训练。模型架构、编译代码及训练代码如下:
# Pretrained model
pretrained <- application_vgg16(weights = 'imagenet',
include_top = FALSE,
input_shape = c(224, 224, 3))
# Model architecture
model <- keras_model_sequential() %>%
pretrained %>%
layer_flatten() %>%
layer_dense(units = 256, activation = "relu") %>%
layer_dense(units = 10, activation = "softmax")
summary(model)
freeze_weights(pretrained)
summary(model)
_________________________________________________________________________
Layer (type) Output Shape Param #
=========================================================================
vgg16 (Model) (None, 7, 7, 512) 14714688
__________________________________________________________________________
flatten (Flatten) (None, 25088) 0
__________________________________________________________________________
dense (Dense) (None, 256) 6422784
__________________________________________________________________________
dense_1 (Dense) (None, 10) 2570
==========================================================================
Total params: 21,140,042
Trainable params: 6,425,354
Non-trainable params: 14,714,688
___________________________________________________________________________
# Compile model
model %>% compile(loss = 'categorical_crossentropy',
optimizer = 'adam',
metrics = 'accuracy')
# Fit model
model_four <- model %>% fit(trainx,
trainy,
epochs = 10,
batch_size = 10,
validation_split = 0.2)
从前面的总结中,我们可以观察到以下几点:
-
该模型有 21,140,042 个参数,在冻结 VGG16 的权重后,训练参数总数降至 6,425,354 个。
-
在编译模型时,我们保留了使用
adam优化器。 -
此外,我们运行了 10 个 epoch 来训练模型。所有其他设置与我们之前使用的模型相同。
训练模型后的准确率和损失值的图表如下:

前面的训练和验证数据的损失与准确率图表表明,在大约四个 epoch 后,模型的表现趋于平稳。与之前的模型相比,验证数据的损失值呈现逐渐上升的趋势。
获取测试数据的损失、准确率和混淆矩阵的代码如下:
# Loss and accuracy
model %>% evaluate(testx, testy)
$loss
[1] 1.673867
$acc
[1] 0.7565
# Confusion matrix
pred <- model %>% predict_classes(testx)
(tab <- table(Predicted = pred, Actual = data$test$y[1:2000,]))
Actual
Predicted 0 1 2 3 4 5 6 7 8 9
0 137 2 12 0 6 0 0 1 11 6
1 9 172 1 0 0 0 0 1 9 21
2 7 0 123 11 11 3 3 5 3 0
3 3 0 11 130 10 35 7 7 0 0
4 7 0 13 5 118 7 10 5 1 0
5 1 0 11 27 3 125 2 7 0 0
6 2 5 20 18 21 8 192 3 4 1
7 6 0 4 6 25 7 2 163 2 1
8 18 6 0 2 4 0 0 1 182 3
9 6 13 0 0 0 0 0 0 5 171
# Accuracy for each category
100*diag(tab)/colSums(tab)
0 1 2 3 4
69.89796 86.86869 63.07692 65.32663 59.59596
5 6 7 8 9
67.56757 88.88889 84.45596 83.87097 84.23645
从前面的输出中,我们可以得出以下观察结论:
-
测试数据的损失和准确率分别为 1.674 和 0.757。
-
混淆矩阵提供了更多的洞察。这款模型在分类类别 6(青蛙)时具有最佳分类准确率 88.9%。
-
另一方面,分类类别 4(鹿)图像时的准确率仅为约 59.6%。
在本节中,我们进行了三种情况的实验:
-
使用
adam优化器稍微改善了结果,并提供了约 77.2%的测试数据准确率。 -
在第二次实验中,超参数调优提供了最佳结果,512 个密集单元、0.1 的 dropout 率和 30 的批量大小。这组参数帮助我们获得了大约 79.8%的测试数据准确率。
-
第三次实验,我们使用了 VGG16 预训练网络,同样获得了不错的结果。然而,测试数据准确率略低于 75.7%。
处理较小数据集的另一种方法是使用数据增强。在这种方法中,通过翻转、旋转、平移等方式修改现有图像,从而创建新的样本。由于图像数据集中的图像并不总是居中的,这种人工创建的新样本有助于我们学习到有用的特征,从而提高图像分类性能。
概述
本章中,我们展示了如何使用预训练的深度神经网络来开发图像分类模型。这些预训练网络经过超过 100 万张图像的训练,捕获了可重复使用的特征,可以应用于类似但新的数据。当使用相对较小的数据集开发图像分类模型时,这一特性尤为重要。此外,预训练网络还节省了计算资源和时间。我们首先使用了 RESNET50 预训练网络来识别一只诺里奇梗犬的图像。随后,我们利用来自 CIFAR10 数据集的 2,000 张图像,展示了将预训练网络应用于相对较小数据集的有效性。我们从头开始构建的初始卷积神经网络模型由于过拟合未能产生有用的结果。
接下来,我们使用了预训练的 RESNET50 网络,并通过在预训练网络上添加两个密集层来定制它以满足我们的需求。我们取得了不错的结果,测试数据的准确率约为 76.8%。尽管预训练模型可以提供更快的结果,并且需要较少的训练轮次,但我们需要探索通过一些实验来提高模型性能。为了探索更好的结果,我们尝试了adam优化器,得到了约 77.2%的测试数据准确率。我们还进行了超参数调优,得到了最佳的超参数设置,其中密集层的单元数为 512,dropout 层的丢弃率为 0.1,模型训练时的批量大小为 30。使用这种组合的图像分类准确率达到了约 79.8%的测试数据准确率。最后,我们实验了预训练的 VGG16 网络,得到了约 75.6%的测试数据准确率。这些实验展示了我们如何探索和提高模型性能。
在下一章中,我们将探讨另一类有趣且流行的深度网络,称为生成对抗网络(GANs)。我们将利用 GANs 来创建新图像。
第九章:使用生成对抗网络创建新图像
本章通过一个实际示例说明了生成对抗网络(GANs)在生成新图像中的应用。到目前为止,本书通过图像数据展示了深度网络在图像分类任务中的应用。然而,在本章中,我们将探索一种有趣且流行的方法,帮助我们创造新图像。生成对抗网络已被应用于生成新图像、改善图像质量、生成新文本和新音乐。GAN 的另一个有趣应用是在异常检测领域。在这种情况下,训练一个 GAN 以生成被认为是正常的数据。当这个网络被用于重建被认为不正常或异常的数据时,结果中的差异可以帮助我们检测到异常的存在。本章将通过生成新图像的示例来探讨这一应用。
更具体地说,本章将涵盖以下主题:
-
生成对抗网络概述
-
处理 MNIST 图像数据
-
开发生成器网络
-
开发判别器网络
-
训练网络
-
评估结果
-
性能优化提示和最佳实践
生成对抗网络概述
GAN 利用两种网络:
-
生成器网络
-
判别器网络
对于生成器网络,输入的是噪声数据,这通常是从标准正态分布中生成的随机数。下面是展示生成对抗网络概述的流程图:

如前面的流程图所示,生成器网络使用噪声数据作为输入,并试图创建我们可以标记为假的图像。这些假图像及其标记为假的标签将作为输入提供给判别器网络。除了带标签的假图像,我们还可以提供带标签的真实图像作为输入给判别器网络。
在训练过程中,判别器网络尝试区分生成器网络创建的假图像和真实图像。在开发生成对抗网络的过程中,这一过程会持续进行,使得生成器网络尽最大努力生成判别器网络无法判断为假的图像。同时,判别器网络也会越来越擅长正确区分真假图像。
当生成器网络学会一致地产生训练数据中没有的图像,而判别器网络无法将其分类为假图像时,就算成功。在本章的真实图像中,我们将使用包含手写数字图像的 MNIST 训练数据。
在接下来的章节中,我们将阐明开发生成对抗网络的步骤,目标是生成手写数字五,数据来自于 MNIST 数据集。
处理 MNIST 图像数据
在本节中,我们将使用 Keras 库,Keras 库中也包括了 MNIST 数据。我们还将使用 EBImage 库,它对于处理图像数据非常有用。MNIST 数据包含了从 0 到 9 的手写图像。让我们来看一下以下代码,以便更好地理解这些数据:
# Libraries and MNIST data
library(keras)
library(EBImage)
mnist <- dataset_mnist()
str(mnist)
List of 2
$ train:List of 2
..$ x: int [1:60000, 1:28, 1:28] 0 0 0 0 0 0 0 0 0 0 ...
..$ y: int [1:60000(1d)] 5 0 4 1 9 2 1 3 1 4 ...
$ test :List of 2
..$ x: int [1:10000, 1:28, 1:28] 0 0 0 0 0 0 0 0 0 0 ...
..$ y: int [1:10000(1d)] 7 2 1 0 4 1 4 9 5 9 ...
从前面的代码中,我们可以做出以下观察:
-
从这些数据的结构来看,我们可以看到训练数据中有 60,000 张图像,测试数据中有 10,000 张图像。
-
这些手写图像的尺寸是 28 x 28,且是黑白图像。这意味着它们只有一个通道。
在本章中,我们只会使用训练数据中的数字五来训练生成对抗网络,并生成新的数字五图像。
训练数据中的数字五
虽然可以开发一个生成对抗网络来生成所有 10 个数字,但对于刚开始的人来说,建议先从一个数字开始。让我们来看一下以下代码:
# Data on digit five
c(c(trainx, trainy), c(testx, testy)) %<-% mnist
trainx <- trainx[trainy==5,,]
str(trainx)
int [1:5421, 1:28, 1:28] 0 0 0 0 0 0 0 0 0 0 ...
summary(trainx)
Min. 1st Qu. Median Mean 3rd Qu. Max.
0.00 0.00 0.00 33.32 0.00 255.00
par(mfrow = c(8,8), mar = rep(0, 4))
for (i in 1:64) plot(as.raster(trainx[i,,], max = 255))
par(mfrow = c(1,1))
如前面的代码所示,我们选择了包含数字五的图像,并将它们保存在trainx中。trainx的结构告诉我们,那里有 5,421 张这样的图像,且它们的尺寸为 28 x 28。总结函数显示,trainx中的值范围从 0 到 255。以下图像展示了训练数据中手写数字五的前 64 张图像:

这些手写图像显示出高度的变化性。这种变化性是预期中的,因为不同的人有不同的书写风格。虽然大多数数字书写清晰,易于识别,但也有一些不太清楚。
数据处理
为了准备接下来的步骤,我们将重塑trainx,使其维度变为 5,421 x 28 x 28 x 1,代码如下所示:
# Reshaping data
trainx <- array_reshape(trainx, c(nrow(trainx), 28, 28, 1))
trainx <- trainx / 255
在这里,我们还将trainx中的值除以 255,得到一个值范围在 0 到 1 之间的数据。数据处理成所需格式后,我们可以继续开发生成器网络的架构。
开发生成器网络
生成器网络将用于从噪声形式提供的数据中生成假图像。在这一部分,我们将开发生成器网络的架构,并通过总结网络来看相关参数。
网络架构
让我们来看一下开发生成器网络架构的代码:
# Generator network
h <- 28; w <- 28; c <- 1; l <- 28
gi <- layer_input(shape = l)
go <- gi %>% layer_dense(units = 32 * 14 * 14) %>%
layer_activation_leaky_relu() %>%
layer_reshape(target_shape = c(14, 14, 32)) %>%
layer_conv_2d(filters = 32,
kernel_size = 5,
padding = "same") %>%
layer_activation_leaky_relu() %>%
layer_conv_2d_transpose(filters = 32,
kernel_size = 4,
strides = 2,
padding = "same") %>%
layer_activation_leaky_relu() %>%
layer_conv_2d(filters = 1,
kernel_size = 5,
activation = "tanh",
padding = "same")
g <- keras_model(gi, go)
在前面的代码中,我们可以观察到以下内容:
-
我们已指定高度(h)、宽度(w)、通道数(c)和潜在维度(l)分别为 28、28、1 和 28。
-
我们已经为生成器输入(gi)指定了输入形状为 28。在训练时,生成器网络将提供一个包含 28 个从标准正态分布中获得的随机数的输入,这些数值仅仅是噪声。
-
接下来,我们已经为生成器网络的输出(go)指定了架构。
-
最后一层是一个卷积 2D 层,激活函数为
tanh。在最后一层,我们将滤波器设置为 1,因为我们不使用彩色图像。 -
请注意,
layer_conv_2d_transpose的大小要求为 28 x 28。 -
生成器输出的维度将是 28 x 28 x 1。
-
其他使用的值,比如滤波器数量、
kernel_size或步幅等,可以稍后进行实验,如果你愿意探索改进结果。 -
gi和go用于生成器网络(g)。
现在,让我们来看一下这个网络的总结。
生成器网络的总结
生成器网络的总结如下:
# Summary of generator network model
summary(g)
____________________________________________________________________________
Layer (type) Output Shape Param #
============================================================================
input_7 (InputLayer) [(None, 28)] 0
____________________________________________________________________________
dense_4 (Dense) (None, 6272) 181888
____________________________________________________________________________
leaky_re_lu_8 (LeakyReLU) (None, 6272) 0
____________________________________________________________________________
reshape_2 (Reshape) (None, 14, 14, 32) 0
____________________________________________________________________________
conv2d_6 (Conv2D) (None, 14, 14, 32) 25632
____________________________________________________________________________
leaky_re_lu_9 (LeakyReLU) (None, 14, 14, 32) 0
____________________________________________________________________________
conv2d_transpose_2 (Conv2DTranspose) (None, 28, 28, 32) 16416
____________________________________________________________________________
leaky_re_lu_10 (LeakyReLU) (None, 28, 28, 32) 0
____________________________________________________________________________
conv2d_7 (Conv2D) (None, 28, 28, 1) 801
============================================================================
Total params: 224,737
Trainable params: 224,737
Non-trainable params: 0
_______________________________________________________________________________________
生成器网络的总结显示了输出的形状和每一层的参数数量。请注意,最终的输出形状是 28 x 28 x 1。生成的假图像将具有这些维度。总体而言,该网络有 224,737 个参数。
现在我们已经指定了生成器网络的结构,接下来可以开发鉴别器网络的架构。
开发鉴别器网络
鉴别器网络将用于分类真假图像。本节将讨论该网络的架构和总结。
架构
开发鉴别器网络架构所用的代码如下:
# Discriminator network
di <- layer_input(shape = c(h, w, c))
do <- di %>%
layer_conv_2d(filters = 64, kernel_size = 4) %>%
layer_activation_leaky_relu() %>%
layer_flatten() %>%
layer_dropout(rate = 0.3) %>%
layer_dense(units = 1, activation = "sigmoid")
d <- keras_model(di, do)
从前面的代码中,我们可以观察到以下几点:
-
我们为输入形状(di)提供了 h = 28,w = 28 和 c = 1。这是训练网络时使用的假图像和真实图像的维度。
-
在鉴别器输出的最后一层(do)中,我们将激活函数指定为
sigmoid,单位数设置为 1,因为图像要么被判定为真实,要么被判定为假。 -
di和do用于鉴别器网络模型(d)。
鉴别器网络的总结
鉴别器网络的总结显示了每一层的输出形状和参数数量:
# Summary of discriminator network model
summary(d)
___________________________________________________
Layer (type) Output Shape Param #
===================================================
input_10 (InputLayer) [(None, 28, 28, 1)] 0
___________________________________________________
conv2d_12 (Conv2D) (None, 25, 25, 64) 1088
____________________________________________________
leaky_re_lu_17 (LeakyReLU) (None, 25, 25, 64) 0
____________________________________________________
flatten_2 (Flatten) (None, 40000) 0
____________________________________________________
dropout_2 (Dropout) (None, 40000) 0
____________________________________________________
dense_7 (Dense) (None, 1) 40001
====================================================
Total params: 41,089
Trainable params: 41,089
Non-trainable params: 0
_____________________________________________________
这里,第一层的输出是 28 x 28 x 1 的大小,这与假图像和真实图像的维度相匹配。总参数量为 41,089。
现在,我们可以使用以下代码编译鉴别器网络模型:
# Compile discriminator network
d %>% compile(optimizer = 'rmsprop',
loss = "binary_crossentropy")
这里,我们已经使用 rmsprop 优化器编译了鉴别器网络。对于损失,我们指定了 binary_crossentropy。
接下来,我们冻结鉴别器网络的权重。请注意,我们在编译鉴别器网络后冻结这些权重,以便它们仅应用于 gan 模型:
# Freeze weights and compile
freeze_weights(d)
gani <- layer_input(shape = l)
gano <- gani %>% g %>% d
gan <- keras_model(gani, gano)
gan %>% compile(optimizer = 'rmsprop',
loss = "binary_crossentropy")
在这里,生成对抗网络的输出(gano)使用了生成器网络和权重被冻结的判别器网络。生成对抗网络(gan)是基于gani和gano的。然后,使用rmsprop优化器编译网络,并将损失函数指定为binary_crossentropy。
现在,我们准备好训练网络了。
网络训练
在这一部分,我们将进行网络训练。在训练过程中,我们将保存假图像并存储损失值,以便回顾训练进展。它们将帮助我们评估网络在生成逼真假图像时的效果。
用于保存假图像和损失值的初始设置
我们将从指定一些训练过程所需的内容开始。让我们看一下以下代码:
# Initial settings
b <- 50
setwd("~/Desktop/")
dir <- "FakeImages"
dir.create(dir)
start <- 1; dloss <- NULL; gloss <- NULL
从前面的代码中,我们可以观察到以下几点:
-
我们将使用 50 的批量大小(b)。
-
我们将在桌面上创建的
FakeImages目录中保存假图像。 -
我们还将使用判别器损失值(dloss)和 GAN 损失值(gloss),这两个值都初始化为
NULL。
训练过程
接下来,我们将训练模型。这里,我们将使用 100 次迭代。让我们来看一下这段代码,它已经总结成五个要点:
# 1\. Generate 50 fake images from noise
for (i in 1:100) {noise <- matrix(rnorm(b*l), nrow = b, ncol= l)}
fake <- g %>% predict(noise)
# 2\. Combine real & fake images
stop <- start + b - 1
real <- trainx[start:stop,,,]
real <- array_reshape(real, c(nrow(real), 28, 28, 1))
rows <- nrow(real)
both <- array(0, dim = c(rows * 2, dim(real)[-1]))
both[1:rows,,,] <- fake
both[(rows+1):(rows*2),,,] <- real
labels <- rbind(matrix(runif(b, 0.9,1), nrow = b, ncol = 1),
matrix(runif(b, 0, 0.1), nrow = b, ncol = 1))
start <- start + b
# 3\. Train discriminator
dloss[i] <- d %>% train_on_batch(both, labels)
# 4\. Train generator using gan
fakeAsReal <- array(runif(b, 0, 0.1), dim = c(b, 1))
gloss[i] <- gan %>% train_on_batch(noise, fakeAsReal)
# 5\. Save fake image
f <- fake[1,,,]
dim(f) <- c(28,28,1)
image_array_save(f, path = file.path(dir, paste0("f", i, ".png")))}
在前面的代码中,我们可以观察到以下几点:
-
我们首先模拟来自标准正态分布的随机数据点,并将结果保存为噪声。然后,我们使用生成器网络
g从包含随机噪声的数据中生成假图像。注意,noise的尺寸为 50 x 28,而fake的尺寸为 50 x 28 x 28 x 1,并且在每次迭代中包含 50 张假图像。 -
我们根据批量大小更新 start 和 stop 的值。在第一次迭代中,start 和 stop 的值分别为 1 和 50;在第二次迭代中,start 和 stop 的值分别为 51 和 100。同样,在第 100 次迭代中,start 和 stop 的值分别为 4,951 和 5,000。由于包含手写数字 5 的
trainx包含超过 5,000 张图像,因此在这 100 次迭代中没有任何图像会被重复。因此,在每次迭代中,都会选择 50 张真实图像并存储在real中,real的尺寸为 50 x 28 x 28。我们使用 reshape 来改变其尺寸为 50 x 28 x 28 x 1,以便与假图像的尺寸匹配。 -
然后,我们创建了一个名为
both的空数组,大小为 100 x 28 x 28 x 1,用于存储真实和伪造的图像数据。both中的前 50 张图像包含伪造数据,而接下来的 50 张图像包含真实图像。我们还生成了 50 个介于 0.9 和 1 之间的随机数,使用均匀分布来作为伪造图像的标签,并生成 50 个介于 0 和 0.1 之间的随机数,作为真实图像的标签。请注意,我们没有使用 0 代表真实图像,1 代表伪造图像,而是引入了一些随机性或噪声。在训练网络时,人工引入标签值中的噪声有助于提升效果。 -
我们使用
both中包含的图像数据和labels中包含的正确类别信息来训练判别器网络。我们还将判别器的损失值保存在dloss中,记录所有 100 次迭代的结果。如果判别器网络能够很好地分类伪造图像和真实图像,那么这个损失值将会较低。 -
我们尝试通过将包含介于 0 和 0.1 之间的随机值的噪声标记为真实图像来欺骗网络。这些产生的损失值会保存在
gloss中,记录所有 100 次迭代的结果。如果网络能够很好地展示伪造图像并将其分类为真实图像,那么这个损失值将会较低。 -
我们保存了每 100 次迭代中的第一张伪造图像,以便我们可以回顾并观察训练过程的影响。
请注意,通常生成对抗网络的训练过程需要大量的计算资源。然而,我们这里使用的示例旨在快速展示这个过程是如何工作的,并在合理的时间内完成训练过程。在 100 次迭代和 8 GB RAM 的计算机上,运行所有代码应该不到一分钟。
审查结果
在本节中,我们将回顾从 100 次迭代中获得的网络损失值。我们还将查看从第一次到第 100 次迭代中使用伪造图像的进展。
判别器和 GAN 损失
我们从 100 次迭代中获得的判别器和 GAN 损失值可以绘制如下图。判别器损失基于伪造图像和真实图像的损失值:

从前面的图表中,我们可以做出以下观察:
-
判别器网络和 GAN 的损失值在前 20 次迭代中显示出较大的波动。这种波动是学习过程的结果。
-
判别器和生成器网络相互竞争,并努力做得比对方更好。当一个网络表现更好时,往往是以另一个网络的代价为前提。这也是为什么,如果将
dloss和gloss绘制在散点图上,我们会期望看到它们之间有一定的负相关关系。虽然这种相关性不一定是完全负相关,但整体模式应该显示出负相关的关系。从长远来看,两个损失值预计会趋于收敛。 -
从 GAN 获得的损失值波动比从判别器网络获得的损失值更大。
-
在约 50 次迭代后,我们注意到判别器的损失值出现了小幅但逐渐增加的趋势。这表明,判别器网络在区分由生成器网络生成的真实与假图像时变得越来越困难。
-
请注意,损失值的增加不一定是负面结果。在这种情况下,这是积极反馈,表明将生成器网络与判别器网络对抗的方式正在产生效果。这意味着生成器网络能够生成越来越像真实图像的假图像,并帮助我们实现主要目标。
假图像
我们将使用以下代码读取假图像并进行绘制:
# Fake image data
library(EBImage)
setwd("~/Desktop/FakeImages")
temp = list.files(pattern = "*.png")
mypic <- list()
for (i in 1:length(temp)) {mypic[[i]] <- readImage(temp[[i]])}
par(mfrow = c(10,10))
for (i in 1:length(temp)) plot(mypic[[i]])
在前面的代码中,我们利用 EBImage 库来处理假图像数据。我们读取了保存在FakeImages目录中的所有 100 张图像。现在,我们可以将所有图像绘制成一个 10 x 10 的网格,如下图所示:

在前面的图像中,展示了每次 100 次迭代中的第一张假图像。从中我们可以做出以下观察:
-
第一行的前十张图像代表了前 10 次迭代。
-
第一张图像仅仅反映了随机噪声。当迭代达到第 10 次时,图像开始捕捉到手写数字“5”的本质。
-
当网络训练经过第 91 到第 100 次迭代时,数字“5”变得更加清晰可见。
在接下来的部分,我们将通过在网络中进行一些更改并观察其对网络训练过程的影响来进行实验。
性能优化技巧和最佳实践
在这一部分,我们将通过在生成器网络和判别器网络中插入额外的卷积层来进行实验。通过这个实验,我们将传达性能优化技巧和最佳实践。
生成器和判别器网络的变化
生成器网络中的变化如下代码所示:
# Generator network
gi <- layer_input(shape = l)
go <- gi %>% layer_dense(units = 32 * 14 * 14) %>%
layer_activation_leaky_relu() %>%
layer_reshape(target_shape = c(14, 14, 32)) %>%
layer_conv_2d(filters = 32,
kernel_size = 5,
padding = "same") %>%
layer_activation_leaky_relu() %>%
layer_conv_2d_transpose(filters = 32,
kernel_size = 4,
strides = 2,
padding = "same") %>%
layer_activation_leaky_relu() %>%
layer_conv_2d(filters = 64,
kernel_size = 5,
padding = "same") %>%
layer_activation_leaky_relu() %>%
layer_conv_2d(filters = 1,
kernel_size = 5,
activation = "tanh",
padding = "same")
g <- keras_model(gi, go)
在这里,我们可以看到,在生成器网络中,我们在倒数第二层之前添加了layer_conv_2d和layer_activation_leaky_relu层。生成器网络的参数总数已增加到 276,801。
判别器网络的变化如下代码所示:
# Discriminator network
di <- layer_input(shape = c(h, w, c))
do <- di %>%
layer_conv_2d(filters = 64, kernel_size = 4) %>%
layer_activation_leaky_relu() %>%
layer_conv_2d(filters = 64, kernel_size = 4, strides = 2) %>%
layer_activation_leaky_relu() %>%
layer_flatten() %>%
layer_dropout(rate = 0.3) %>%
layer_dense(units = 1, activation = "sigmoid")
d <- keras_model(di, do)
在这里,我们在判别器网络的展平层之前添加了layer_conv_2d和layer_activation_leaky_relu层。判别器网络中的参数数量已增加到 148,866 个。我们保持其他一切不变,然后再次训练该网络 100 次迭代。
现在,我们可以评估这些变化的影响。
这些变化对结果的影响
判别器和 GAN 的损失值可以绘制成如下图表:

从前面的图表中,我们可以观察到以下几点:
-
通过增加层数,判别器和 GAN 网络的损失值波动比我们之前获得的结果减少了。
-
在某些迭代中观察到的峰值或高损失值表明,相应的网络在与另一个网络对抗时遇到困难。
-
与判别器网络相关的损失相比,GAN 损失值的波动性仍然较高。
以下图表展示了每 100 次迭代中的第一张假图像:

从前面的图像中,我们可以观察到以下几点:
-
在生成器和判别器网络中增加了卷积层后,网络开始更早生成出类似手写数字五的图像。
-
在之前的网络中,直到大约 70-80 次迭代时,才会出现始终看起来像手写数字五的假图像。
-
由于使用了额外的层,我们可以看到数字五在大约 20-30 次迭代后开始一致地形成,这表明有所改进。
接下来,我们将尝试使用该网络生成另一张手写数字。
生成手写数字八的图像
在本实验中,我们将使用与之前相同的网络架构。然而,我们将使用它来生成手写数字八的图像。该实验中 100 次迭代的判别器和 GAN 损失值可以绘制如下图表:

从前面的图表中,我们可以得出以下观察:
-
判别器和 GAN 的损失值显示出波动性,并且这种波动性随着迭代次数从 1 到 100 的增加而逐渐减小。
-
随着网络训练的进行,GAN 损失值在某些间隔中的高峰逐渐减少。
每次迭代中的第一张假图像的图表如下:

与数字五相比,数字八在开始形成可识别的模式之前需要更多的迭代次数。
在本节中,我们在生成器和判别器网络中实验了额外的卷积层。因此,我们可以得出以下观察:
-
额外的卷积层似乎对更快生成类似手写数字五的假图像有积极影响。
-
尽管本章参考的数据结果还算不错,但对于其他数据,我们可能需要对模型架构进行其他更改。
-
我们还使用了相同架构的网络来生成看起来逼真的手写数字八的假图片。观察到,对于数字八,需要更多的训练迭代才能开始出现可识别的模式。
-
注意,一次生成所有 10 个手写数字的网络可能会更复杂,可能需要更多的迭代次数。
-
类似地,如果我们有颜色图像的尺寸显著大于我们在本章中使用的 28 x 28,我们将需要更多的计算资源,任务也将更具挑战性。
概要
在本章中,我们使用生成对抗网络演示了如何生成单个手写数字的图像。生成对抗网络利用两个网络:生成器和鉴别器网络。生成器网络从包含随机噪声的数据中创建假图像,而鉴别器网络则训练用于区分假图像和真实图像。这两个网络相互竞争,以便创建逼真的假图像。尽管本章提供了使用生成对抗网络生成新图像的示例,但这些网络也被知道在生成新文本或新音乐以及异常检测方面有应用。
在本节中,我们讨论了用于处理图像数据的各种深度学习网络。在下一节中,我们将介绍用于自然语言处理的深度学习网络。
第四章:自然语言处理中的深度学习
本节讨论了如何应用深度神经网络来解决涉及自然语言处理的实际问题。它包括四个章节,展示了如何使用流行的深度学习网络,如循环神经网络(RNNs)、长短期记忆网络(LSTM)和卷积循环神经网络(CRNNs)。
本节包含以下章节:
-
第九章,用于文本分类的深度网络
-
第十章,使用循环神经网络进行文本分类
-
第十一章,使用长短期记忆网络进行文本分类
-
第十二章,使用卷积循环神经网络进行文本分类
第十一章:深度网络用于文本分类
文本数据属于非结构化数据类别。在开发深度网络模型时,由于这类数据的独特性,我们需要完成额外的预处理步骤。在本章中,你将了解开发文本分类模型所需遵循的步骤,并通过易于理解的示例进行说明。文本数据,如客户评论、产品评价和电影评论,在商业中扮演着重要角色,而文本分类是一个重要的深度学习问题。
在本章中,我们将讨论两个文本数据集,学习在开发深度网络分类模型时如何准备文本数据,查看 IMDb 电影评论数据,开发深度网络架构,拟合并评估模型,并讨论一些技巧和最佳实践。具体来说,本章将涵盖以下主题:
-
文本数据集
-
为模型构建准备数据
-
开发深度神经网络
-
模型评估与预测
-
性能优化技巧和最佳实践
文本数据集
文本数据可以用于我们在实践中开发深度网络模型时。此类数据可以从几个公开可用的来源获取。在本节中,我们将介绍两个这样的资源:
-
UCI 机器学习数据集
-
Keras 中的文本数据
UCI 机器学习数据集
以下链接提供了多种数据集,这些数据集包含从产品评论(来自amazon.com)、电影评论(来自IMDB.com)和餐厅评论(来自yelp.com)中提取的文本句子:archive.ics.uci.edu/ml/datasets/Sentiment+Labelled+Sentences。
每个句子根据评论中表达的情感进行标注。情感可以是正面的或负面的。每个网站上都有 500 个正面句子和 500 个负面句子,总共有 3000 个标注句子。这些数据可以用来开发一个情感分类深度网络模型,帮助我们自动将客户评论分类为正面或负面。
以下是一些来自 IMDb 的负面评论示例,这些评论被标注为 0:
-
一部非常非常非常慢节奏、没有方向的电影,讲述了一个迷茫的年轻人
-
不确定是谁更迷茫——是平淡无奇的角色还是观众,几乎一半的观众走了。
-
尝试用黑白色调和巧妙的镜头角度表现艺术性,但电影令人失望——随着演技糟糕、情节和台词几乎没有,变得更加荒谬。
-
几乎没有音乐或任何值得一提的内容
以下是一些来自 IMDb 的正面评论示例,这些评论被标注为 1:
-
电影中最精彩的场景是 Gerardo 试图找到一首一直在他脑海中回荡的歌曲。
-
今天看了这部电影,觉得是个不错的努力,给孩子们传递了很好的信息
-
喜欢吉米·巴菲特饰演科学老师的选角
-
那些小猫头鹰真是太可爱了
-
这部电影展示了佛罗里达州最美的一面,让它看起来非常吸引人
以下是一些亚马逊负面评论的例子,标签为 0:
-
所以,除非我购买转换器,否则我在美国根本无法插上它
-
45 分钟以上的对话必须插着充电器。重大问题!!
-
我必须晃动插头才能正确对接,以获得合适的音量
-
如果你有几十个或几百个联系人,想象一下逐个发送消息的乐趣
-
我建议大家千万不要上当!
以下是一些亚马逊正面评论的例子,标签为 1:
-
好的案例,极好的价值
-
非常适合下巴骨
-
麦克风很棒
-
如果你是 Razr 手机的用户...你一定得拥有这个!
-
而且音质非常好
Keras 中的文本数据
Keras 中有两个文本数据集,如下所示:
-
互联网电影数据库 (IMDb),包含电影评论情感分类数据
-
路透新闻社的主题分类数据
IMDb 的评论数据包含了 25,000 条已经被分类为正面或负面情感的评论。该数据已经预处理,每条评论都被编码为整数序列。路透新闻社的主题分类数据包含了 11,228 条新闻,这些新闻也经过了预处理,每条新闻都被编码为整数序列。这些新闻被分类为 46 个类别或主题,例如牲畜、黄金、住房、就业等。
以下是来自 IMDb 数据中 Keras 的一条正面电影评论示例:
“在这部简洁的改编作品中,奢华的制作价值和扎实的表演令人印象深刻,这部作品改编自简·奥斯汀的讽刺经典,讲述了 18 世纪英格兰阶级之间和内部的婚姻游戏。诺森和帕特罗饰演的朋友角色需要经历种种谎言,最终发现他们彼此相爱。幽默感是一种?美德,能够很好地解释那部经典老旧材料的魅力,虽然原作中的一些严苛部分被稍微调整过了。我喜欢电影的视觉效果和镜头设置,并且觉得它不像 80 年代和 90 年代的其他电影那样过于依赖大量的特写镜头,效果相当好。”
以下是来自 IMDb 数据中 Keras 的一条负面电影评论示例:
"我一生中最糟糕的错误 br br 我在 Target 买了这部电影,价格是 5 美元,因为我想着‘嘿,这是 Sandler 的片子,我可以获得一些廉价的笑料’,结果我错了,完全错了,电影放到一半,我的三个朋友都睡着了,而我还在痛苦中,最糟糕的情节,最糟糕的剧本,最糟糕的电影,我看过的最烂的电影,我差点想把头撞到墙上一个小时,然后停下来,你知道为什么吗?因为撞头的感觉太爽了,我把那部该死的电影丢进垃圾桶,看它烧掉,那感觉比我做过的任何事情都好,直到看完《美国精神病人》、《黑暗军团》和《杀死比尔》才算过得了这段噩梦,我恨你,Sandler,居然还做出这种决定,毁掉了我一天的生活。"
为模型构建准备数据
为了准备数据进行模型构建,我们需要遵循以下步骤:
-
标记化
-
将文本转换为整数
-
填充和截断
为了说明数据准备过程中涉及的步骤,我们将使用一个非常小的文本数据集,该数据集包含五条与 2017 年 9 月发布的苹果 iPhone X 相关的推文。我们将使用这个小数据集来了解数据准备过程中涉及的步骤,然后切换到更大的 IMDb 数据集,以构建深度网络分类模型。以下是我们将存储在t1到t5中的五条推文:
t1 <- "I'm not a huge $AAPL fan but $160 stock closes down $0.60 for the day on huge volume isn't really bearish"
t2 <- "$AAPL $BAC not sure what more dissapointing: the new iphones or the presentation for the new iphones?"
t3 <- "IMO, $AAPL animated emojis will be the death of $SNAP."
t4 <- "$AAPL get on board. It's going to 175\. I think wall st will have issues as aapl pushes 1 trillion dollar valuation but 175 is in the cards"
t5 <- "In the AR vs. VR battle, $AAPL just put its chips behind AR in a big way."
前面的推文包含了大小写字母、标点符号、数字和特殊字符。
标记化
推文中的每个单词或数字都是一个标记,拆分推文为标记的过程叫做标记化。用于执行标记化的代码如下:
tweets <- c(t1, t2, t3, t4, t5)
token <- text_tokenizer(num_words = 10) %>%
fit_text_tokenizer(tweets)
token$index_word[1:3]
$`1`
[1] "the"
$`2`
[1] "aapl"
$`3`
[1] "in"
从前面的代码中,我们可以看到以下内容:
-
我们首先将五条推文保存到
tweets中。 -
对于标记化过程,我们指定了
num_words为10,表示我们希望使用 10 个最频繁的词并忽略其他词。 -
尽管我们指定了要使用
10个最频繁的词,但实际上将使用的整数的最大值是 10 - 1 = 9。 -
我们使用了
fit_text_tokenizer,它自动将文本转换为小写并去除推文中的标点符号。 -
我们观察到,这五条推文中最频繁出现的前三个词是
the、aapl和in。
请注意,频率较高的词语可能对文本分类有用,也可能没有用。
将文本转换为整数序列
以下代码用于将文本转换为整数序列。也提供了输出结果:
seq <- texts_to_sequences(token, tweets)
seq
[[1]]
[1] 4 5 6 2 7 8 1 9 6
[[2]]
[1] 2 4 1 1 8 1
[[3]]
[1] 2 1
[[4]]
[1] 2 9 2 7 3 1
[[5]]
[1] 3 1 2 3 5
从前面的代码中,我们可以看到以下内容:
-
我们使用
texts_to_sequences将推文转换为整数序列。 -
由于我们选择了将最频繁的词作为标记数量为
10,因此每个整数序列中的整数的最大值为 9。 -
对于每条推文,序列中的整数数量少于词语的数量,因为只使用了最频繁的词。
-
整数序列的长度不同,范围从 2 到 9 不等。
-
为了开发分类模型,所有序列需要具有相同的长度。这是通过执行填充或截断来实现的。
填充和截断
使所有整数序列相等的代码如下:
pad_seq <- pad_sequences(seq, maxlen = 5)
pad_seq
[,1] [,2] [,3] [,4] [,5]
[1,] 7 8 1 9 6
[2,] 4 1 1 8 1
[3,] 0 0 0 2 1
[4,] 9 2 7 3 1
[5,] 3 1 2 3 5
从前面的代码中,我们可以看到以下几点:
-
我们使用了
pad_sequences,使所有整数序列的长度一致。 -
当我们将所有序列的最大长度(使用
maxlen)设置为 5 时,长度超过 5 的序列将被截断,长度不足 5 的序列将会填充零。 -
请注意,这里的填充默认设置为“pre”。这意味着当序列长度超过 5 时,截断将作用于序列的开头部分。我们可以从前面的输出中看到这一点,序列的前 4、5、6 和 2 被去除。
-
同样,对于第三个序列,它的长度为 2,已在序列的开头添加了三个零。
可能会有些情况,你希望在整数序列的末尾截断或添加零。实现这一点的代码如下:
pad_seq <- pad_sequences(seq, maxlen = 5, padding = 'post')
pad_seq
[,1] [,2] [,3] [,4] [,5]
[1,] 7 8 1 9 6
[2,] 4 1 1 8 1
[3,] 2 1 0 0 0
[4,] 9 2 7 3 1
[5,] 3 1 2 3 5
在前面的代码中,我们将填充方式指定为post。这种填充方式的影响可以在输出中看到,序列 3 的末尾添加了零,长度不足 5。
开发推文情感分类模型
为了开发一个推文情感分类模型,我们需要为每条推文提供标签。然而,获取准确反映推文情感的标签是具有挑战性的。让我们看一下现有的情感分类词典,并探讨为什么很难获得合适的标签。仅凭五条推文,是无法开发出一个情感分类模型的。然而,这里的重点是观察为每条推文得出合适标签的过程。这将帮助我们理解获取准确标签所面临的挑战。为了自动提取每条推文的情感分数,我们将使用syuzhet包。同时,我们还将使用一些常用的情感词典。国家研究委员会(NRC)词典帮助根据特定单词捕捉不同的情感。我们将使用以下代码来获取这五条推文的情感分数:
library(syuzhet)
get_nrc_sentiment(tweets)
anger anticipation disgust fear joy sadness surprise trust negative positive
1 1 0 0 1 0 0 0 0 0 0
2 0 0 0 0 0 0 0 0 0 0
3 1 1 1 1 1 1 1 0 1 1
4 0 1 0 0 0 0 0 0 0 0
5 1 0 0 0 0 0 0 0 1 0
第一条推文对于愤怒和恐惧的评分都是 1。尽管其中包含单词'bearish',但如果我们阅读这条推文,会发现它实际上是积极的。
让我们看一下以下代码,其中包含单词'bearish'、'death'和'animated'的情感分数:
get_nrc_sentiment('bearish')
anger anticipation disgust fear joy sadness surprise trust negative positive
1 1 0 0 1 0 0 0 0 0 0
get_nrc_sentiment('death')
anger anticipation disgust fear joy sadness surprise trust negative positive
1 1 1 1 1 0 1 1 0 1 0
get_nrc_sentiment('animated')
anger anticipation disgust fear joy sadness surprise trust negative positive
1 0 0 0 0 1 0 0 0 0 1
从前面的代码中,我们可以得出以下结论:
-
第一条推文的整体分数基于单词的斜体形式,除此之外没有其他因素。
-
第三条推文在每个类别的评分中,除了信任外,其他的评分都是 1。
-
从阅读这条推文,我们很明显可以看出,写这条推文的人实际上认为动画表情符号对苹果是正面的,对 Snapchat 则是负面的。
-
情感分数基于这条推文中的两个词:death 和 animated。它们未能捕捉到第三条推文中表达的真正情感,这条推文对苹果非常积极。
当我们手动为每条推文标注负面情感(由 0 表示)和正面情感(由 1 表示)时,我们很可能得到 1、0、1、1 和 1 作为我们的分数。让我们使用以下代码,通过使用syuzhet、bing和afinn词典来获得这些情感分数:
get_sentiment(tweets, method="syuzhet")
[1] 0.00 0.80 -0.35 0.00 -0.25
get_sentiment(tweets, method="bing")
[1] -1 0 -1 -1 0
get_sentiment(tweets, method="afinn")
[1] 4 0 -2 0 0
从syuzhet、bing和afinn词典的结果来看,我们可以观察到以下几点:
-
结果与推文中包含的实际情感有显著差异。因此,尝试自动标注推文并分配合适的情感分数是困难的。
-
我们看到自动标注文本序列是一个具有挑战性的问题。然而,一个解决方案是手动标注大量文本序列,比如推文,然后使用这些标注数据来开发情感分类模型。
-
此外,值得注意的是,情感分类模型只对用于开发该模型的特定类型的文本数据有帮助。
-
不可能为不同的文本情感分类应用使用相同的模型。
开发深度神经网络
虽然我们不会仅基于五条推文开发分类模型,但我们来看一下模型架构的代码:
model <- keras_model_sequential()
model %>% layer_embedding(input_dim = 10,
output_dim = 8,
input_length = 5)
summary(model)
OUTPUT
__________________________________________________________________________________
Layer (type) Output Shape Param #
==================================================================================
embedding_1 (Embedding) (None, 5, 8) 80
==================================================================================
Total params: 80
Trainable params: 80
Non-trainable params: 0
________________________________________________________________________________
print(model$get_weights(), digits = 2)
[[1]]
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8]
[1,] 0.0055 -0.0364 -0.0475 0.049 -0.0139 -0.0114 -0.0452 -0.0298
[2,] 0.0398 -0.0143 -0.0406 0.023 -0.0496 -0.0124 0.0087 -0.0104
[3,] 0.0370 -0.0321 -0.0491 -0.021 -0.0214 0.0391 0.0428 -0.0398
[4,] -0.0257 0.0294 0.0433 0.048 0.0259 -0.0323 -0.0308 0.0224
[5,] -0.0079 -0.0255 0.0164 0.023 -0.0486 0.0273 0.0245 -0.0020
[6,] 0.0372 0.0464 0.0454 -0.020 0.0086 -0.0375 -0.0188 0.0395
[7,] 0.0293 0.0305 0.0130 0.037 -0.0324 -0.0069 -0.0248 0.0178
[8,] -0.0116 -0.0087 -0.0344 0.027 0.0132 0.0430 -0.0196 -0.0356
[9,] 0.0314 -0.0315 0.0074 -0.044 -0.0198 -0.0135 -0.0353 0.0081
[10,] 0.0426 0.0199 -0.0306 -0.049 0.0259 -0.0341 -0.0155 0.0147
从前面的代码,我们可以观察到以下几点:
-
我们使用
keras_model_sequential()初始化了模型。 -
我们将输入维度指定为 10,这是最常见的词汇数。
-
输出维度为 8,这导致参数数量为 10 x 8 = 80。
-
输入长度是整数序列的长度。
-
我们可以使用
model$get_weights()获取这 80 个参数的权重。
请注意,这些权重将在每次初始化模型时发生变化。
获取 IMDb 电影评论数据
现在,我们将使用 IMDb 电影评论数据,其中每条评论的情感已经被标注为正面或负面。以下是从 Keras 访问 IMDb 电影评论数据的代码:
imdb <- dataset_imdb(num_words = 500)
c(c(train_x, train_y), c(test_x, test_y)) %<-% imdb
z <- NULL
for (i in 1:25000) {z[i] <- print(length(train_x[[i]]))}
summary(z)
Min. 1st Qu. Median Mean 3rd Qu. Max.
11.0 130.0 178.0 238.7 291.0 2494.0
从前面的代码,我们可以观察到以下几点:
-
我们使用
train_x和train_y来存储按整数序列和标签表示的正面或负面情感的数据。 -
我们对测试数据也使用了类似的约定。
-
训练数据和测试数据各自包含 25,000 条评论。
-
序列长度的总结显示,基于最常见词汇的电影评论最小长度为 11,最大序列长度为
2494。 -
中位数序列长度为
178。 -
中位数值小于均值,这表明该数据将呈现右偏,并且右侧尾部较长。
训练数据的序列长度的直方图可以如下绘制:

前面的整数序列长度的直方图显示了右偏模式。大多数序列的整数数量少于 500。
接下来,我们将使用以下代码使整数序列的长度相等:
train_x <- pad_sequences(train_x, maxlen = 100)
test_x <- pad_sequences(test_x, maxlen = 100)
从前面的代码中,我们可以观察到以下内容:
-
我们使用了
maxlen为 100 来标准化每个序列的长度为 100 个整数。 -
长度超过 100 的序列将会截断或移除任何多余的整数,长度小于 100 的序列将添加零以人为地增加序列的长度,直到达到 100。我们对训练集和测试集的序列都做这样处理。
现在,我们准备好构建分类模型了。
构建分类模型
对于模型架构和模型摘要,我们将使用以下代码:
model <- keras_model_sequential()
model %>% layer_embedding(input_dim = 500,
output_dim = 16,
input_length = 100) %>%
layer_flatten() %>%
layer_dense(units = 16, activation = 'relu') %>%
layer_dense(units = 1, activation = "sigmoid")
summary(model)
OUTPUT
___________________________________________________________________
Layer (type) Output Shape Param #
===================================================================
embedding_12 (Embedding) (None, 100, 16) 8000
___________________________________________________________________
flatten_3 (Flatten) (None, 1600) 0
___________________________________________________________________
dense_6 (Dense) (None, 16) 25616
___________________________________________________________________
dense_7 (Dense) (None, 1) 17
===================================================================
Total params: 33,633
Trainable params: 33,633
Non-trainable params: 0
___________________________________________________________________
从前面的代码中,我们可以观察到以下内容:
-
在这里,我们在
layer_embedding()之后添加了layer_flatten()。 -
接下来是一个包含 16 个节点的全连接层,并使用
relu激活函数。 -
模型的摘要显示,总共有
33,633个参数。
现在,我们可以编译模型。
编译模型
我们需要使用以下代码来编译模型:
model %>% compile(optimizer = "rmsprop",
loss = "binary_crossentropy",
metrics = c("acc"))
从前面的代码中,我们可以观察到以下内容:
-
我们已经使用了
rmsprop优化器来编译模型。 -
对于损失,我们使用了
binary_crossentropy,因为响应有两个值,即正面或负面。评估指标将使用准确度。
现在,让我们开始拟合模型。
拟合模型
我们需要使用以下代码来拟合模型:
model_1 <- model %>% fit(train_x, train_y,
epochs = 10,
batch_size = 128,
validation_split = 0.2)
plot(model_1)
如前面的代码所示,我们使用train_x和train_y来拟合模型,并使用10个 epoch 和批量大小为128。我们使用 20%的训练数据来评估模型的性能,包括损失值和准确度。拟合模型后,我们得到如下所示的损失和准确度图:

从前面的图表中,我们可以观察到以下内容:
-
损失和准确度图显示,在大约四个 epoch 之后,训练数据和验证数据之间出现了发散。
-
我们观察到训练数据和验证数据在损失和准确度值上出现了发散。
-
我们不会使用这个模型,因为有明确的证据表明存在过拟合问题。
为了克服过拟合问题,我们需要修改前面的代码,使其如下所示:
model <- keras_model_sequential()
model %>% layer_embedding(input_dim = 500,
output_dim = 16,
input_length = 100) %>%
layer_flatten() %>%
layer_dense(units = 16, activation = 'relu') %>%
layer_dense(units = 1, activation = "sigmoid")
model %>% compile(optimizer = "rmsprop",
loss = "binary_crossentropy",
metrics = c("acc"))
model_2 <- model %>% fit(train_x, train_y,
epochs = 10,
batch_size = 512,
validation_split = 0.2)
plot(model_2)
看看前面的代码,我们可以观察到以下内容:
-
我们正在重新运行模型并只做了一个改变:即我们将批量大小增加到 512。
-
我们保持其他设置不变,然后使用训练数据拟合模型。
在拟合模型后,存储在model_2中的损失和准确度值被绘制出来,如下图所示:

从前面的图表中,我们可以观察到以下几点:
-
这一次,损失和准确度值显示出更好的结果。
-
训练和验证的曲线在损失和准确度上更接近彼此。
-
此外,基于验证数据的损失和准确度值并未出现我们在先前模型中观察到的严重恶化,后者的最后三个周期的值是平坦的。
-
我们通过对代码进行一些小的修改,克服了过拟合的问题。
我们将使用这个模型进行评估和预测。
模型评估与预测
现在,我们将使用训练数据和测试数据来评估模型,获取损失、准确度和混淆矩阵。我们的目标是获得一个能够将电影评论中的情感分类为正面或负面的模型。
使用训练数据进行评估
获取训练数据的损失和准确度值的代码如下:
model %>% evaluate(train_x, train_y)
$loss
[1] 0.3745659
$acc
[1] 0.83428
如我们所见,对于训练数据,损失和准确度分别为0.375和0.834。为了更深入地了解模型的情感分类性能,我们需要构建一个混淆矩阵。为此,请使用以下代码:
pred <- model %>% predict_classes(train_x)
table(Predicted=pred, Actual=imdb$train$y)
Actual
Predicted 0 1
0 11128 2771
1 1372 9729
在前面的代码中,我们正在预测训练数据的类别,并将结果与电影评论的实际情感类别进行比较。这通过混淆矩阵进行了总结。我们可以从混淆矩阵中做出以下观察:
-
模型正确预测了 11,128 条电影评论中的负面情感。
-
模型正确预测了 9,729 条电影评论中的正面情感。
-
将正面评论误分类为负面评论的数量较高(2,771),而将具有负面情感的电影评论误分类为正面评论的数量较少(1,372)。
接下来,我们将对测试数据重复这一过程。
使用测试数据进行评估
获取测试数据的损失和准确度值的代码如下:
model %>% evaluate(test_x, test_y)
$loss
[1] 0.4431483
$acc
[1] 0.79356
如我们所见,在测试数据方面,损失和准确度分别为0.443和0.794。这些结果略低于训练数据获得的结果。我们可以使用模型对test数据进行类别预测,并将其与实际的电影评论类别进行比较。这可以总结为一个混淆矩阵,如下所示:
pred1 <- model %>% predict_classes(test_x)
table(Predicted=pred1, Actual=imdb$test$y)
Actual
Predicted 0 1
0 10586 3247
1 1914 9253
从前面的混淆矩阵中,我们可以观察到以下几点:
-
总体而言,该模型在正确预测负面电影评论(10,586 条)方面似乎比预测正面电影评论(9,253 条)更为准确。
-
这种模式与使用训练数据得到的结果一致。
-
此外,尽管测试数据的 79% 准确率已经不错,但仍有改进模型情感分类性能的空间。
在接下来的部分,我们将探讨性能优化的建议和最佳实践。
性能优化的建议和最佳实践
现在我们已经获得了测试数据的电影评论分类准确率,即 79%,我们可以继续努力提高这一准确率。达到这样的改进可能涉及实验模型架构中的参数、编译模型时使用的参数和/或拟合模型时使用的设置。在本节中,我们将通过改变单词序列的最大长度并同时使用与前一模型不同的优化器来进行实验。
实验最大序列长度和优化器
让我们从创建代表电影评论和它们标签的整数序列的train和test数据开始,使用以下代码:
c(c(train_x, train_y), c(test_x, test_y)) %<-% imdb
z <- NULL
for (i in 1:25000) {z[i] <- print(length(train_x[[i]]))}
summary(z)
Min. 1st Qu. Median Mean 3rd Qu. Max.
11.0 130.0 178.0 238.7 291.0 2494.0
在上述代码中,我们正在存储基于训练数据的序列长度在z中。通过这样做,我们可以获得z的汇总信息。从这里,我们可以获得数值汇总值,如最小值、第一四分位数、中位数、平均数、第三四分位数和最大值。单词序列的中位数值为 178。在前几节中,我们在填充序列时使用了最大长度为 100,以便使它们的长度相等。在本实验中,我们将其增加到 200,以便得到一个接近中位数值的数字,如以下代码所示:
imdb <;- dataset_imdb(num_words = 500)
c(c(train_x, train_y), c(test_x, test_y)) %<-% imdb
train_x <- pad_sequences(train_x, maxlen = 200)
test_x <- pad_sequences(test_x, maxlen = 200)
model <- keras_model_sequential()
model %>% layer_embedding(input_dim = 500,
output_dim = 16,
input_length = 200) %>%
layer_flatten() %>%
layer_dense(units = 16, activation = 'relu') %>%
layer_dense(units = 1, activation = "sigmoid")
model %>% compile(optimizer = "adamax",
loss = "binary_crossentropy",
metrics = c("acc"))
model_3 <- model %>% fit(train_x, train_y,
epochs = 10,
batch_size = 512,
validation_split = 0.2)
plot(model_3)
我们将进行的另一个更改是在编译模型时使用adamax优化器。请注意,这是流行的adam优化器的变体。我们保持其他所有内容不变。训练模型后,我们绘制了结果损失和准确率的图表,如下图所示:

从前述损失和准确率的图表中,我们可以观察到以下情况:
-
训练和验证数据的损失和准确率值表明大约在四个时期内有快速改进。
-
经过四个时期,这些改进在训练数据上放缓了。
-
对于验证数据,损失和准确率值在最后几个时期保持不变。
-
该图表未显示任何有关过度拟合的问题。
接下来,我们需要使用以下代码基于测试数据计算损失和准确率:
model %>% evaluate(test_x, test_y)
$loss
[1] 0.3906249
$acc
[1] 0.82468
查看上述代码,我们可以观察到以下情况:
-
基于测试数据,模型的损失和准确率分别为
0.391和0.825。 -
这两个数字都显示了与我们在前一节中获得的性能相比的改进。
要进一步研究模型的情感分类性能,我们可以使用以下代码:
pred1 <- model %>% predict_classes(test_x)
table(Predicted=pred1, Actual=imdb$test$y)
Actual
Predicted 0 1
0 9970 1853
1 2530 10647
根据基于测试数据电影评论的混淆矩阵,我们可以观察到以下情况:
-
现在,负面(9,970 条)和正面电影评论(10,647 条)的正确分类结果更为接近。
-
正确分类正面电影评论的效果略好于负面评论。
-
该模型将负面电影评论误分类为正面评论的比例略高(2,530 条),相比之下,正面评论被误分类为负面评论的比例为 1,853 条。
在这里,通过实验最大序列长度和用于编译模型的优化器类型,提升了情感分类的性能。鼓励你继续进行实验,进一步提高模型的情感分类性能。
总结
本章我们从开发用于文本分类的深度神经网络开始。由于文本数据的独特性,开发深度神经网络情感分类模型之前需要进行一些额外的预处理步骤。我们使用了五条推文的小样本来演示这些预处理步骤,包括分词、将文本数据转换为整数序列,以及填充/截断以确保序列长度一致。我们还强调了自动为文本序列标注正确情感是一个具有挑战性的问题,并且通用的词典可能无法提供有效的结果。
为了开发深度网络情感分类模型,我们切换到了一个较大且现成的 IMDb 电影评论数据集,该数据集作为 Keras 的一部分提供。为了优化模型的性能,我们还尝试了数据准备时的最大序列长度等参数,以及用于编译模型的优化器类型。这些实验取得了不错的结果;然而,我们将继续探索这组数据,以便进一步提高深度网络模型在情感分类任务中的表现。
在下一章,我们将使用递归神经网络分类模型,这种模型更适合处理涉及序列的数据。
第十二章:使用循环神经网络进行文本分类
循环神经网络对于解决涉及序列的数据问题非常有用。一些涉及序列的应用实例包括文本分类、时间序列预测、视频帧序列、DNA 序列以及语音识别。
在本章中,我们将使用循环神经网络开发一个情感(正面或负面)分类模型。我们将从准备数据开始,开发文本分类模型,然后是构建顺序模型、编译模型、拟合模型、评估模型、预测以及使用混淆矩阵评估模型性能。我们还将回顾一些优化情感分类性能的小贴士。
更具体地说,本章将涵盖以下主题:
-
为模型构建准备数据
-
开发一个循环神经网络模型
-
拟合模型
-
模型评估与预测
-
性能优化建议与最佳实践
为模型构建准备数据
在本章中,我们将使用互联网电影数据库(IMDb)的电影评论文本数据,该数据可通过 Keras 包获取。请注意,您不需要从任何地方下载此数据,因为它可以通过我们稍后将讨论的代码轻松地从 Keras 库中获取。此外,这个数据集已经过预处理,文本数据已被转换为整数序列。我们不能直接使用文本数据来构建模型,因此,文本数据转化为整数序列的预处理是开发深度学习网络前的必要步骤。
我们将通过使用dataset_imdb函数加载imdb数据,并使用num_words参数指定最频繁的单词数量为 500。然后,我们将把imdb数据拆分为train和test数据集。让我们看看以下代码来理解这些数据:
# IMDB data
imdb <- dataset_imdb(num_words = 500)
c(c(train_x, train_y), c(test_x, test_y)) %<-% imdb
length(train_x); length(test_x)
[1] 25000
[1] 25000
table(train_y)
train_y
0 1
12500 12500
table(test_y)
test_y
0 1
12500 12500
让我们看看前面的代码:
-
train_x和test_x包含分别表示训练数据和测试数据中评论的整数。 -
类似地,
train_y和test_y包含0和1标签,分别表示负面和正面情感。 -
使用
length函数,我们可以看到train_x和test_x都基于各自的 25,000 条电影评论。 -
train_y和test_y的表格显示训练和测试数据中正面评论(12,500 条)和负面评论(12,500 条)的数量相等。
拥有如此平衡的数据集有助于避免由于类别不平衡问题而导致的偏差。
电影评论中的单词通过唯一的整数来表示,每个分配给单词的整数是基于其在数据集中的总体频率。例如,整数 1 表示最频繁的单词,而整数 2 表示第二频繁的单词,依此类推。此外,整数 0 并不代表任何特定的单词,而是表示一个未知的单词。
让我们使用以下代码查看train_x数据中的第三个和第六个序列:
# Sequence of integers
train_x[[3]]
[1] 1 14 47 8 30 31 7 4 249 108 7 4 2 54 61 369
[17] 13 71 149 14 22 112 4 2 311 12 16 2 33 75 43 2
[33] 296 4 86 320 35 2 19 263 2 2 4 2 33 89 78 12
[49] 66 16 4 360 7 4 58 316 334 11 4 2 43 2 2 8
[65] 257 85 2 42 2 2 83 68 2 15 36 165 2 278 36 69
[81] 2 2 8 106 14 2 2 18 6 22 12 215 28 2 40 6
[97] 87 326 23 2 21 23 22 12 272 40 57 31 11 4 22 47
[113] 6 2 51 9 170 23 2 116 2 2 13 191 79 2 89 2
[129] 14 9 8 106 2 2 35 2 6 227 7 129 113
train_x[[6]]
[1] 1 2 128 74 12 2 163 15 4 2 2 2 2 32 85 156 45
[18] 40 148 139 121 2 2 10 10 2 173 4 2 2 16 2 8 4
[35] 226 65 12 43 127 24 2 10 10
for (i in 1:6) print(length(train_x[[i]]))
Output
[1] 218
[1] 189
[1] 141
[1] 550
[1] 147
[1] 43
从前面的代码和输出中,我们可以观察到以下情况:
-
从第三个电影评论相关的整数序列的输出中,我们可以观察到第三个评论包含 141 个整数,范围从 1(第一个整数)到 369(第 16 个整数)。
-
由于我们将最常见的单词限制在 500 个以内,因此对于第三个评论,不存在大于 500 的整数。
-
同样地,从第六个评论相关的整数序列的输出中,我们可以观察到第六个评论包含 43 个整数,范围从 1(第一个整数)到 226(第 35 个整数)。
-
查看
train_x数据中前六个序列的长度,我们可以观察到电影评论的长度在 43(train 数据中的第六个评论)到 550(train 数据中的第四个评论)之间变化。电影评论长度的这种变化是正常的,并且是预期的。
在我们开发电影评论情感分类模型之前,我们需要找到一种方法,使所有电影评论的整数序列长度相同。我们可以通过填充序列来实现这一点。
填充序列
填充文本序列是为了确保所有序列具有相同的长度。让我们看看以下代码:
# Padding and truncation
train_x <- pad_sequences(train_x, maxlen = 100)
test_x <- pad_sequences(test_x, maxlen = 100)
从前面的代码中,我们可以观察到以下情况:
-
我们可以通过
pad_sequences函数并指定maxlen的值来实现所有整数序列的等长。 -
在这个例子中,我们将每个训练和测试数据中的每个电影评论序列的长度限制为 100。请注意,在填充序列之前,
train_x和test_x的结构是一个包含 25,000 条评论的列表。 -
然而,在填充序列之后,两者的结构都变成了一个 25,000 x 100 的矩阵。通过在填充之前和之后运行
str(train_x)可以轻松验证这一点。
为了观察填充对整数序列的影响,让我们看看以下代码及其输出:
# Sequence of integers
train_x[3,]
[1] 2 4 2 33 89 78 12 66 16 4 360 7 4 58 316 334
[17] 11 4 2 43 2 2 8 257 85 2 42 2 2 83 68 2
[33] 15 36 165 2 278 36 69 2 2 8 106 14 2 2 18 6
[49] 22 12 215 28 2 40 6 87 326 23 2 21 23 22 12 272
[65] 40 57 31 11 4 22 47 6 2 51 9 170 23 2 116 2
[81] 2 13 191 79 2 89 2 14 9 8 106 2 2 35 2 6
[97] 227 7 129 113
train_x[6,]
[1] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[17] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[33] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[49] 0 0 0 0 0 0 0 0 0 1 2 128 74 12 2 163
[65] 15 4 2 2 2 2 32 85 156 45 40 148 139 121 2 2
[81] 10 10 2 173 4 2 2 16 2 8 4 226 65 12 43 127
[97] 24 2 10 10
在train_x填充后,可以在前述代码中看到第三个整数序列的输出。在这里,我们可以观察到以下情况:
-
第三个序列现在的长度是 100。第三个序列原本有 141 个整数,我们可以观察到序列开头的 41 个整数被截断了。
-
另一方面,第六个序列的输出显示出不同的模式。
-
第六个序列原本长度为 43,但现在在序列开头添加了 57 个零,将长度人为地扩展为 100。
-
所有 25,000 个与电影评论相关的整数序列,在每个训练和测试数据中都以类似的方式受到影响。
在接下来的部分中,我们将为递归神经网络开发一个架构,用于开发电影评论情感分类模型。
开发递归神经网络模型
在这一部分中,我们将开发递归神经网络的架构并对其进行编译。让我们来看一下下面的代码:
# Model architecture
model <- keras_model_sequential()
model %>%
layer_embedding(input_dim = 500, output_dim = 32) %>%
layer_simple_rnn(units = 8) %>%
layer_dense(units = 1, activation = "sigmoid")
我们首先通过keras_model_sequential函数初始化模型。然后,我们添加嵌入层和简单的递归神经网络(RNN)层。对于嵌入层,我们指定input_dim为 500,这与我们之前指定的最常见单词数量相同。接下来是一个简单的 RNN 层,隐藏单元的数量指定为 8。
请注意,layer_simple_rnn层的默认激活函数是双曲正切(tanh),它是一个 S 形曲线,输出范围是-1 到+1。
最后一层密集层有一个单元,用来捕捉电影评论的情感(正面或负面),其激活函数为 sigmoid。当输出在 0 和 1 之间时,如本例所示,它便于解释,可以视为一个概率。
请注意,sigmoid 激活函数是一个 S 形曲线,其中输出的范围在 0 到 1 之间。
现在,让我们看一下模型摘要,理解如何计算所需的参数数量。
参数计算
RNN 模型的摘要如下:
# Model summary
model
OUTPUT
Model
________________________________________________________________________
Layer (type) Output Shape Param #
========================================================================
embedding_21 (Embedding) (None, None, 32) 16000
________________________________________________________________________
simple_rnn_23 (SimpleRNN) (None, 8) 328
________________________________________________________________________
dense_24 (Dense) (None, 1) 9
========================================================================
Total params: 16,337
Trainable params: 16,337
Non-trainable params: 0
________________________________________________________________________
嵌入层的参数数量是通过将 500(最常见的单词数量)与 32(输出维度)相乘得到的 16,000。为了计算简单 RNN 层的参数数量,我们使用(h(h+i) + h),其中h代表隐藏单元的数量,i代表该层的输入维度。在这种情况下,这个值是 32。
因此,我们有(8(8 + 32)+8)= 328 个参数。
请注意,如果我们在这里考虑一个全连接的密集层,我们将得到(8 x 32 + 8)= 264 个参数。然而,额外的 64 个参数是由于我们使用递归层来捕捉文本数据中的序列信息。
在递归层中,除了当前输入信息外,前一个输入的信息也被使用,这导致了这些额外的参数,这就是为什么 RNN 比普通的全连接神经网络层更适合处理序列数据的原因。对于最后一层,即密集层,我们有(1 x 8 + 1)= 9 个参数。总的来说,这个架构有 16,337 个参数。
在递归层中,利用来自前一个输入的信息有助于更好地表示文本或类似数据中的序列。
编译模型
编译模型的代码如下:
# Compile model
model %>% compile(optimizer = "rmsprop",
loss = "binary_crossentropy",
metrics = c("acc"))
我们使用rmsprop优化器来编译模型,这对于递归神经网络(RNN)是推荐的。由于电影评论的反馈是二元的(正面或负面),我们使用binary_crossentropy作为损失函数。最后,对于评估指标,我们指定了准确率。
在接下来的部分,我们将使用此架构来开发一个使用递归神经网络的电影评论情感分类模型。
拟合模型
拟合模型的代码如下:
# Fit model
model_one <- model %>% fit(train_x, train_y,
epochs = 10,
batch_size = 128,
validation_split = 0.2)
为了拟合模型,我们将使用 20%的验证数据划分,其中 20,000 条电影评论数据来自训练数据,用于构建模型。剩余的 5,000 条电影评论训练数据则用于评估验证,以损失和准确率的形式进行评估。我们将运行 10 个 epoch,每个 batch 大小为 128。
使用验证数据划分时,需要注意的是,20%的划分使用训练数据的前 80%进行训练,最后 20%的训练数据用于验证。因此,如果前 50%的评论数据是负面的,后 50%是正面的,那么 20%的验证数据划分将使得模型验证仅基于正面评论。因此,在使用验证数据划分之前,我们必须验证这一点,避免出现这种情况,否则会引入显著的偏差。
准确率和损失
使用plot(model_one)显示的训练和验证数据的准确率和损失值(在 10 个 epoch 后)可以在以下图表中看到:

从前面的图表中,可以得出以下结论:
-
训练损失从第 1 个 epoch 到第 10 个 epoch 持续下降。
-
验证损失最初减少,但在第 3 个 epoch 后开始趋于平稳。
-
在相反的方向上,准确率也观察到了类似的模式。
在接下来的部分,我们将评估分类模型,并通过训练和测试数据评估模型的预测性能。
模型评估与预测
首先,我们将基于训练数据评估模型的损失和准确率。同时,我们还将基于训练数据获取混淆矩阵。对于测试数据,也会重复相同的过程。
训练数据
我们将使用evaluate函数来获取损失和准确率值,如以下代码所示:
# Loss and accuracy
model %>% evaluate(train_x, train_y)
$loss
[1] 0.4057531
$acc
[1] 0.8206
从之前的输出可以看出,基于训练数据的损失和准确率分别为 0.406 和 0.821。
使用训练数据进行的预测将用于生成混淆矩阵,如以下代码所示:
# Prediction and confusion matrix
pred <- model %>% predict_classes(train_x)
table(Predicted=pred, Actual=imdb$train$y)
Actual
Predicted 0 1
0 9778 1762
1 2722 10738
通过观察前面的混淆矩阵,可以得出以下结论:
-
有 9,778 条电影评论被正确分类为负面,10,738 条电影评论被正确分类为正面。我们可以观察到,模型在将评论分类为正面或负面方面表现得相当不错。
-
通过查看错误分类,我们还可以观察到,负面电影评论被错误地分类为正面评论的次数为 2,722 次。与将正面评论错误分类为负面评论(1,762 次)相比,这一错误分类的频率较高。
接下来,让我们基于测试数据做一个类似的评估。
测试数据
获取损失和准确率值的代码如下:
# Loss and accuracy
model %>% evaluate(test_x, test_y)
$loss
[1] 0.4669374
$acc
[1] 0.77852
在这里,我们可以看到基于测试数据的损失和准确率分别为 0.467 和 0.778。这些结果略逊色于我们在训练数据中观察到的结果。
接下来,我们将预测测试数据的类别,并使用结果生成混淆矩阵,代码如下所示:
# Prediction and confusion matrix
pred1 <- model %>% predict_classes(test_x)
table(Predicted=pred1, Actual=imdb$test$y)
Actual
Predicted 0 1
0 9134 2171
1 3366 10329
除了整体结果略逊色于我们从训练数据中获得的结果外,我们没有看到训练数据和测试数据之间的重大差异。
在下一节中,我们将探索一些改进模型性能的策略。
性能优化技巧和最佳实践
在开发循环神经网络模型时,我们会遇到需要做出多个与网络相关的决策的情况。这些决策可能包括尝试不同的激活函数,而不是我们之前使用的默认函数。让我们做出这样的更改,并观察它们对电影评论情感分类模型的性能产生什么影响。
在本节中,我们将实验以下四个因素:
-
简单 RNN 层中的单元数量
-
在简单 RNN 层中使用不同的激活函数
-
添加更多的循环层
-
填充序列的最大长度变化
简单 RNN 层中的单元数量
集成此更改并编译/拟合模型的代码如下:
# Model architecture
model <- keras_model_sequential()
model %>%
layer_embedding(input_dim = 500, output_dim = 32) %>%
layer_simple_rnn(units = 32) %>%
layer_dense(units = 1, activation = "sigmoid")
# Compile model
model %>% compile(optimizer = "rmsprop",
loss = "binary_crossentropy",
metrics = c("acc"))
# Fit model
model_two <- model %>% fit(train_x, train_y,
epochs = 10,
batch_size = 128,
validation_split = 0.2)
在这里,我们通过将简单 RNN 层中的单元数量从 8 增加到 32 来改变架构,其他设置保持不变。然后,我们编译并拟合模型,如前面的代码所示。
10 个周期后的准确率和损失值可以在以下图表中看到:

上面的图表显示了以下内容:
-
从第 3 个周期开始,训练数据和验证数据之间的差距显著增大。
-
这明显表明,与前面的图表相比,过拟合程度有所增加,而当时简单 RNN 中的单元数量为 8。
-
这也反映在我们基于这个新模型获得的测试数据上,损失值为 0.585,准确率为 0.757。
现在,让我们尝试在简单 RNN 层中使用不同的激活函数,看看是否能够解决这个过拟合问题。
在简单 RNN 层中使用不同的激活函数
这个变化可以在以下代码中看到:
# Model architecture
model <- keras_model_sequential()
model %>%
layer_embedding(input_dim = 500, output_dim = 32) %>%
layer_simple_rnn(units = 32, activation = "relu") %>%
layer_dense(units = 1, activation = "sigmoid")
# Compile model
model %>% compile(optimizer = "rmsprop",
loss = "binary_crossentropy",
metrics = c("acc"))
# Fit model
model_three <- model %>% fit(train_x, train_y,
epochs = 10,
batch_size = 128,
validation_split = 0.2)
在前面的代码中,我们将简单 RNN 层中的默认激活函数更改为 ReLU 激活函数。我们保持与上一个实验中相同的其他设置。
10 个周期后的准确率和损失值可以在以下图表中看到:

从上面的图表中,我们可以观察到以下几点:
-
现在,损失和准确率的值看起来好多了。
-
现在,基于训练和验证的损失与准确率曲线更加接近。
-
我们使用模型根据获得的测试数据计算损失和准确度值,分别为 0.423 和 0.803。与我们迄今为止获得的结果相比,这显示了更好的效果。
接下来,我们将通过添加更多的循环层进一步实验。这将帮助我们构建一个更深的循环神经网络模型。
添加更多的循环层
现在,我们将通过向当前网络添加两个额外的循环层来进行实验。包含此更改的代码如下:
# Model architecture
model <- keras_model_sequential() %>%
layer_embedding(input_dim = 500, output_dim = 32) %>%
layer_simple_rnn(units = 32,
return_sequences = TRUE,
activation = 'relu') %>%
layer_simple_rnn(units = 32,
return_sequences = TRUE,
activation = 'relu') %>%
layer_simple_rnn(units = 32,
activation = 'relu') %>%
layer_dense(units = 1, activation = "sigmoid")
# Compile model
model %>% compile(optimizer = "rmsprop",
loss = "binary_crossentropy",
metrics = c("acc"))
# Fit model
model_four <- model %>% fit(train_x, train_y,
0 epochs = 10,
batch_size = 128,
validation_split = 0.2)
当我们添加这些额外的循环层时,我们还将return_sequences设置为TRUE。我们保持其他所有设置不变,然后编译并拟合模型。基于训练和验证数据的损失和准确度图表如下:

从前面的图表中,我们可以观察到以下内容:
-
在 10 个 epoch 之后,训练和验证的损失与准确度值表现出合理的接近度,表明没有发生过拟合。
-
基于测试数据计算出的损失和准确度值分别为 0.403 和 0.816,结果显示出明显的改进。
-
这表明,增加更深的循环层确实有助于更好地捕捉电影评论中的词序列。反过来,这也促使我们能够更好地将电影评论的情感分类为积极或消极。
填充序列的最大长度
到目前为止,我们已使用最大长度为 100 来填充电影评论序列的训练和测试数据。我们可以通过以下代码查看train和test数据中电影评论长度的总结:
# Summary of padding sequences
z <- NULL
for (i in 1:25000) {z[i] <- print(length(train_x[[i]]))}
Min. 1st Qu. Median Mean 3rd Qu. Max.
11.0 130.0 178.0 238.7 291.0 2494.0
z <- NULL
for (i in 1:25000) {z[i] <- print(length(test_x[[i]]))}
Min. 1st Qu. Median Mean 3rd Qu. Max.
7.0 128.0 174.0 230.8 280.0 2315.0
从前面的代码中,我们可以得出以下观察结果:
-
从训练数据中电影评论长度的总结中,我们可以看到最短长度为 11,最长长度为 2,494,中位数长度为 178。
-
类似地,测试数据的最短评论长度为 7,最长评论长度为 2,315,中位数长度为 174。
请注意,当最大填充长度低于中位数(例如最大长度为 100 时),我们通常会通过去除超过 100 的单词来截断更多的电影评论。同时,当我们选择的填充最大长度远高于中位数时,将会出现更多的电影评论需要包含零,而较少的评论会被截断。
在这一部分,我们将探讨将电影评论中词序列的最大长度保持在接近中位数值的影响。包含此更改的代码如下:
# IMDB data
c(c(train_x, train_y), c(test_x, test_y)) %<-% imdb
train_x <- pad_sequences(train_x, maxlen = 200)
test_x <- pad_sequences(test_x, maxlen = 200)
# Model architecture
model <- keras_model_sequential() %>%
layer_embedding(input_dim = 500, output_dim = 32) %>%
layer_simple_rnn(units = 32,
return_sequences = TRUE,
activation = 'relu') %>%
layer_simple_rnn(units = 32,
return_sequences = TRUE,
activation = 'relu') %>%
layer_simple_rnn(units = 32,
return_sequences = TRUE,
activation = 'relu') %>%
layer_simple_rnn(units = 32,
activation = 'relu') %>%
layer_dense(units = 1, activation = "sigmoid")
# Compile model
model %>% compile(optimizer = "rmsprop",
loss = "binary_crossentropy",
metrics = c("acc"))
# Fit model
model_five <- model %>% fit(train_x, train_y,
epochs = 10,
batch_size = 128,
validation_split = 0.2)
从前面的代码可以看到,我们在将maxlen指定为 200 后运行模型。我们将其他所有设置保持与model_four相同。
训练数据和验证数据的损失与准确度图表如下:

从前面的图表中,我们可以得出以下观察结果:
-
由于训练和验证数据点非常接近,因此没有过拟合问题。
-
基于测试数据的损失和准确度分别计算为 0.383 和 0.830。
-
在这一阶段,损失和准确度值达到了最佳水平。
基于测试数据的混淆矩阵如下所示:
# Prediction and confusion matrix
pred1 <- model %>% predict_classes(test_x)
table(Predicted=pred1, Actual=imdb$test$y)
Actual
Predicted 0 1
0 10066 1819
1 2434 10681
从混淆矩阵中,我们可以做出以下观察:
-
该分类模型在正确分类电影评论为正面(10,681)时表现略好于正确分类负面(10,066)评论的情况。
-
就被错误分类的评论而言,我们之前观察到的趋势仍然存在,即负面电影评论被模型错误分类为正面评论的情况较多,这在此案例中也是如此。
在本节中,我们尝试了多个单元、激活函数、网络中的递归层数以及填充量,以改进电影评论情感分类模型。您还可以进一步探索的其他因素包括要包含的最常见单词数量和在填充序列时更改最大长度。
总结
在本章中,我们展示了使用递归神经网络模型进行文本情感分类,数据来自 IMDb 电影评论。与常规的全连接网络相比,递归神经网络更适合处理包含序列的数据。文本数据就是我们在本章中使用的一个例子。
通常,深度网络涉及许多因素或变量,这需要一定的实验,涉及在得出有用模型之前,对这些因素的级别进行调整。本章中,我们还开发了五种不同的电影评论情感分类模型。
一种变体的递归神经网络是长短期记忆(LSTM)网络。LSTM 网络能够学习长期依赖关系,并帮助递归网络记住更长时间的输入。
在下一章中,我们将介绍使用 LSTM 网络的应用示例,我们将继续使用 IMDb 电影评论数据,并进一步探索可以提高情感分类模型性能的改进。
第十三章:使用长短期记忆网络进行文本分类
在上一章中,我们使用递归神经网络开发了一个电影评论情感分类模型,针对的是由单词序列构成的文本数据。长短期记忆(LSTM)神经网络是递归神经网络(RNNs)的一种特殊类型,适用于处理包含序列的数据,并提供我们将在下一节讨论的优势。本章将展示如何使用 LSTM 神经网络进行情感分类的步骤。将 LSTM 网络应用于商业问题的步骤可能包括文本数据准备、创建 LSTM 模型、训练模型和评估模型性能。
更具体地说,在本章中,我们将覆盖以下主题:
-
为什么我们使用 LSTM 网络?
-
为模型构建准备文本数据
-
创建长短期记忆网络模型
-
训练 LSTM 模型
-
评估模型性能
-
性能优化技巧和最佳实践
为什么我们使用 LSTM 网络?
在上一章中,我们已经看到,当处理包含序列的数据时,递归神经网络提供了不错的性能。使用 LSTM 网络的一个关键优势在于它们能够解决梯度消失问题,该问题使得在长序列的单词或整数上进行网络训练变得困难。梯度用于更新 RNN 参数,在长序列的单词或整数中,这些梯度变得越来越小,直到实际上无法进行网络训练。LSTM 网络帮助克服了这个问题,使得能够捕捉序列中被大距离分隔的关键词或整数之间的长期依赖性。例如,考虑以下两句,其中第一句较短,第二句则相对较长:
-
句子-1:我喜欢吃巧克力。
-
句子-2:每当有机会时,我喜欢吃巧克力,而通常这样的机会有很多。
在这些句子中,捕捉句子主要意思的两个重要词是like和chocolates。在第一个句子中,like和chocolates离得较近,之间只隔了两个词。而在第二个句子中,这两个词之间隔了多达 14 个词。LSTM 网络旨在处理这些长时间依赖性,通常出现在较长的句子或较长的整数序列中。在本章中,我们重点介绍如何应用 LSTM 网络来开发电影评论情感分类模型。
为模型构建准备文本数据
我们将继续使用上一章中使用的 IMDB 电影评论数据,该数据已经以我们可以用于开发深度网络模型的格式提供,数据处理的需求最小化。
让我们来看一下下面的代码:
# IMDB data
library(keras)
imdb <- dataset_imdb(num_words = 500)
c(c(train_x, train_y), c(test_x, test_y)) %<-% imdb
train_x <- pad_sequences(train_x, maxlen = 200)
test_x <- pad_sequences(test_x, maxlen = 200)
捕捉训练和测试数据的整数序列分别存储在train_x和test_x中。同样,train_y和test_y存储标签,用于表示电影评论是正面还是负面。我们已指定最频繁的单词数量为 500。对于填充,我们使用 200 作为训练和测试数据序列的最大长度。
当整数的实际长度小于 200 时,序列的开头会填充零,以人为地将整数的长度增加到 200。然而,当整数的长度大于 200 时,序列的开头会移除一些整数,以保持总长度为 200。
如前所述,训练集和测试集数据都是平衡的,每个数据集包含 25,000 条电影评论。每条电影评论也都带有正面或负面的标签。
请注意,maxlen的取值会影响模型的表现。如果选择的值太小,序列中的更多单词或整数会被截断。另一方面,如果选择的值太大,序列中的更多单词或整数需要填充,零会被添加进去。避免过度填充或过度截断的一种方法是选择一个接近中位数的值。
创建一个长短期记忆网络模型
在这一部分,我们将从一个简单的 LSTM 网络架构开始,看看如何计算出参数数量。随后,我们将编译模型。
LSTM 网络架构
我们将从一个简单的 LSTM 网络架构流程图开始,如下图所示:

前面的 LSTM 网络流程图突出了架构中的层和使用的激活函数。在 LSTM 层中,使用的是tanh激活函数,这是该层的默认激活函数。在密集层中,使用的是sigmoid激活函数。
让我们来看一下下面的代码和模型的总结:
# Model architecture
model <- keras_model_sequential() %>%
layer_embedding(input_dim = 500, output_dim = 32) %>%
layer_lstm(units = 32) %>%
layer_dense(units = 1, activation = "sigmoid")
model
__________________________________________________________________________
Layer (type) Output Shape Param #
==========================================================================
embedding (Embedding) (None, None, 32) 16000
__________________________________________________________________________
lstm (LSTM) (None, 32) 8320
__________________________________________________________________________
dense (Dense) (None, 1) 33
==========================================================================
Total params: 24,353
Trainable params: 24,353
Non-trainable params: 0
__________________________________________________________________________
除了上一章中用于 RNN 模型的内容外,我们在此示例中将layer_simple_rnn替换为layer_lstm,用于 LSTM 网络。对于嵌入层,我们总共有 16,000 个参数(500 x 32)。以下计算展示了 LSTM 层参数数量的计算方法:
=4 x [LSTM 层单元数 x (LSTM 层单元数 + 输出维度) + LSTM 层单元数]
= 4 x [32(32+32) + 32]
= 8320
对于一个类似的包含 RNN 层的架构,我们将有 2,080 个参数。LSTM 层参数数量的四倍增加也导致了更多的训练时间,因此需要相对更高的处理成本。密集层的参数数量是[(32x1) + 1],即 33。因此,该网络的总参数数量为 24,353。
编译 LSTM 网络模型
用于编译 LSTM 网络模型的代码如下:
# Compile
model %>% compile(optimizer = "rmsprop",
loss = "binary_crossentropy",
metrics = c("acc"))
我们使用rmsprop作为优化器,binary_crossentropy作为损失函数,因为电影评论的响应是二元的,换句话说,它们要么是积极的,要么是消极的。对于评估指标,我们使用分类准确率。在编译模型后,我们准备好进行 LSTM 模型的下一步拟合。
拟合 LSTM 模型
为了训练 LSTM 模型,我们将使用以下代码:
# Fit model
model_one <- model %>% fit(train_x, train_y,
epochs = 10,
batch_size = 128,
validation_split = 0.2)
plot(model_one)
我们将使用训练数据来拟合 LSTM 模型,设定训练周期为 10 个 epoch,批量大小为 128。我们还将保留 20%的训练数据作为验证数据,用于在模型训练过程中评估损失和准确率值。
损失与准确率图
以下截图显示了model_one的损失和准确率图:

基于训练和验证数据的损失与准确率图显示了曲线之间的整体接近性。图中的观察结果如下:
-
两条线之间没有出现显著的分歧,这表明没有出现过拟合问题。
-
增加 epoch 的数量可能不会显著改善模型的性能。
-
然而,基于验证数据的损失和准确率值显示出一定程度的不均匀性或波动性,它们偏离训练损失和准确率的幅度相对较高。
-
特别是在 epoch 4 和 epoch 8 这两个时刻,它们显示了与基于训练数据的损失和准确率的显著偏差。
接下来,我们将继续评估model_one并将其用于预测电影评论的情感。
评估模型性能
在本节中,我们将基于训练数据和测试数据评估模型。我们还将为训练数据和测试数据创建混淆矩阵,以进一步了解模型在电影评论情感分类方面的表现。
使用训练数据评估模型
我们将首先使用以下代码评估训练数据上的模型性能:
# Evaluate
model %>% evaluate(train_x, train_y)
$loss
[1] 0.3749587
$acc
[1] 0.82752
从前面的输出可以看出,对于训练数据,我们得到了0.375的损失值和大约0.828的准确率。考虑到 LSTM 架构相对简单,这已经是一个不错的表现。接下来,我们将使用该模型预测电影评论的情感,并通过以下代码开发混淆矩阵来总结结果:
# Confusion Matrix
pred <- model %>% predict_classes(train_x)
table(Predicted=pred, Actual=imdb$train$y)
Actual
Predicted 0 1
0 9258 1070
1 3242 11430
我们可以从混淆矩阵中得出以下观察结果:
-
观察到该模型在预测积极电影评论(11,430 个正确预测)方面似乎比预测消极电影评论(9,258 个正确预测)更为准确。换句话说,该模型在训练数据中以约 91.4%的正确率(也称为模型的敏感性)正确分类了积极评论。
-
类似地,该模型在训练数据中以约 74.1%的正确率(也称为模型的特异性)正确分类了消极评论。
-
还观察到,负面电影评论被错误分类为正面评论的比例约为三倍(3,242 条评论),而正面评论被错误分类为负面评论的比例为 1,070 条评论。
-
因此,尽管总体而言,该模型似乎在训练数据上表现良好,但深入分析后,我们发现它在正确分类正面电影评论时有一定的偏向,这导致了在正确分类负面评论时准确度较低。
观察到基于训练数据的模型性能是否会在测试数据上出现类似的行为,将会很有趣。
使用测试数据进行模型评估
现在我们将使用测试数据,通过以下代码获得模型的损失和准确率值:
# Evaluate
model %>% evaluate(test_x, test_y)
$loss
[1] 0.3997277
$acc
[1] 0.81992
从前面的输出可以看到,对于测试数据,我们得到的损失值为 0.399,准确率约为 0.819。正如预期的那样,这些值略低于训练数据上获得的结果。然而,它们与基于训练数据的结果足够接近,可以认为该模型行为一致。
获取混淆矩阵的代码如下:
# Confusion Matrix
pred1 <- model %>$ predict_classes(text_x)
table(Predicted=pred1, Actual=imdb$test$y)
Actual
Predicted 0 1
0 9159 1161
1 3341 11339
从上述混淆矩阵中,可以得出以下观察结果:
-
基于使用测试数据的预测生成的混淆矩阵显示了与我们之前在训练数据中观察到的类似模式。
-
该模型在准确分类正面电影评论时似乎表现得更好(约 90.7%),而在正确分类负面评论时则较差(约 73.3%)。
-
因此,当正确分类正面电影评论时,模型在性能上继续显示偏差。
在下一节中,我们将进行一些实验,探索模型电影评论情感分类性能的潜在改进。
性能优化技巧和最佳实践
在本节中,我们将进行三项不同的实验,探索改进的基于 LSTM 的电影评论情感分类模型。这将涉及在编译模型时尝试不同的优化器、在模型架构开发时添加另一个 LSTM 层,以及在网络中使用双向 LSTM 层。
使用 Adam 优化器进行实验
我们将在编译模型时使用 adam(自适应矩估优化)优化器,替代之前使用的 rmsprop(均方根传播)优化器。为了使模型性能的比较更加容易,我们将保持其余部分与之前相同,如以下代码所示:
# Model architecture
model <- keras_model_sequential() %>%
layer_embedding(input_dim = 500, output_dim = 32) %>%
layer_lstm(units = 32) %>%
layer_dense(units = 1, activation = "sigmoid")
# Compile
model %>% compile(optimizer = "adam",
loss = "binary_crossentropy",
metrics = c("acc"))
# Fit model
model_two <- model %>% fit(train_x, train_y,
epochs = 10,
batch_size = 128,
validation_split = 0.2)
plot(model_two)
运行上述代码并训练模型后,每个 epoch 的准确率和损失值被存储在 model_two 中。我们使用 model_two 中的损失和准确率值来生成以下图表:

从前面的损失和准确率图中,我们可以得出以下观察结果:
-
基于训练数据和验证数据的损失与准确率图表相比我们为第一个模型所构建的图表(
model_one)显示出略微改进的模式。 -
在基于
model_one的图表中,我们观察到验证数据的损失和准确率值偶尔出现较大偏差,而这些偏差在基于训练数据的值中并没有出现。在这个图表中,我们没有看到两条线之间的任何重大偏差。 -
此外,基于验证数据的最后几个值的损失和准确率值似乎趋于平稳,表明我们使用的十个训练周期已经足够训练模型,增加训练周期数不太可能帮助提高模型性能。
接下来,我们将使用以下代码获取训练数据的损失、准确率和混淆矩阵:
# Loss and accuracy
model %>% evaluate(train_x, train_y)
$loss
[1] 0.3601628
$acc
[1] 0.8434
pred <- model %>% predict_classes(train_x)
# Confusion Matrix
table(Predicted=pred, Actual=imdb$train$y)
Actual
Predicted 0 1
0 11122 2537
1 1378 9963
从前面的代码输出中,我们可以得出以下观察结果:
-
通过使用
adam优化器,我们获得了训练数据的损失和准确率分别为 0.360 和 0.843。这两个数值相比我们之前使用rmsprop优化器的模型都有所提升。 -
从混淆矩阵中可以观察到另一个区别。该模型在正确分类负面电影评论时的表现较好(正确分类率约为 88.9%),而在正确分类正面评论时的正确分类率约为 79.7%。
-
这种行为与之前模型中的观察结果相反。与正确分类正面评论相比,该模型似乎更偏向于正确分类负面电影评论情感。
在审视完使用训练数据的模型性能后,我们将使用以下代码重复该过程,获取测试数据的损失、准确率和混淆矩阵:
# Loss and accuracy
model %>% evaluate(test_x, test_y)
$loss
[1] 0.3854687
$acc
[1] 0.82868
pred1 <- model %>% predict_classes(test_x)
# Confusion Matrix
table(Predicted=pred1, Actual=imdb$test$y)
Actual
Predicted 0 1
0 10870 2653
1 1630 9847
从前面的代码输出中,我们可以得出以下观察结果:
-
基于测试数据的损失和准确率分别为 0.385 和 0.829。这些基于测试数据的结果相比之前的模型在测试数据上也显示出更好的模型表现。
-
混淆矩阵显示了我们在训练数据中观察到的类似模式。负面电影评论情感在测试数据中的正确分类率约为 86.9%。
-
同样,正面电影评论情感在测试数据中的正确分类率约为 78.8%。
-
这种行为与使用训练数据获得的模型性能一致。
尽管尝试adam优化器改善了整体的电影评论情感分类性能,但它在正确分类某一类别时仍然存在偏差。一个好的模型不仅应当提高整体性能,还应最小化在正确分类某一类别时的偏差。以下代码提供了一个表格,展示了train和test数据中负面和正面评论的数量:
# Number of positive and negative reviews in the train data
table(train_y)
train_y
0 1
12500 12500
# Number of positive and negative review in the test data
table(test_y)
test_y
0 1
12500 12500
从前面的代码输出可以看出,该电影评论数据是平衡的,训练数据和测试数据各包含 25,000 条评论。这些数据在正面和负面评论的数量上也是平衡的。训练集和测试集各包含 12,500 条正面评论和 12,500 条负面评论。因此,提供给模型进行训练的负面或正面评论数量没有偏差。然而,在正确分类负面和正面电影评论时所观察到的偏差,显然是需要改进的地方。
在下一个实验中,我们将尝试添加更多的 LSTM 层,看看是否能获得更好的电影评论情感分类模型。
对具有额外层的 LSTM 网络进行实验
在第二次实验中,为了提高分类模型的性能,我们将添加一个额外的 LSTM 层。让我们来看一下以下代码:
# Model architecture
model <- keras_model_sequential() %>%
layer_embedding(input_dim = 500, output_dim = 32) %>%
layer_lstm(units = 32,
return_sequences = TRUE) %>%
layer_lstm(units = 32) %>%
layer_dense(units = 1, activation = "sigmoid")
# Compiling model
model %>% compile(optimizer = "adam",
loss = "binary_crossentropy",
metrics = c("acc"))
# Fitting model
model_three <- model %>% fit(train_x, train_y,
epochs = 10,
batch_size = 128,
validation_split = 0.2)
# Loss and accuracy plot
plot(model_three)
通过向网络中添加额外的 LSTM 层,如前面的代码所示,两个 LSTM 层的总参数数量将从之前一个 LSTM 层的 24,353 增加到 32,673。这一参数数量的增加也会导致训练时间的增加。我们仍然在编译模型时使用 Adam 优化器。除此之外,我们保持与之前模型相同的其他设置。
下面的屏幕截图显示了该实验中使用的具有两个 LSTM 层的网络架构的简单流程图:

前面的 LSTM 网络流程图突出了架构中的两层及所使用的激活函数。在这两层 LSTM 中,tanh 被用作默认的激活函数。在全连接层中,我们继续使用之前所使用的 sigmoid 激活函数。
在训练模型后,每个周期的准确率和损失值被存储在 model_three 中。我们使用 model_three 中的损失值和准确率来生成以下图表:

从显示的损失和准确率图中,我们可以做出以下观察:
-
损失和准确率的图表没有显示过拟合问题的存在,因为训练数据和验证数据的曲线相互接近。
-
与早期的模型一样,验证数据的损失和准确率似乎在最后几个训练周期保持平稳,这表明十个训练周期足以训练模型,增加训练周期数不太可能改善结果。
我们现在可以使用以下代码来获得训练数据的损失、准确率和混淆矩阵:
# Loss and accuracy
model %>% evaluate(train_x, train_y)
$loss
[1] 0.3396379
$acc
[1] 0.85504
pred <- model %>% predict_classes(train_x)
# Confusion Matrix
table(Predicted=pred, Actual=imdb$train$y)
Actual
Predicted 0 1
0 11245 2369
1 1255 10131
从前面的代码输出中,我们可以做出以下观察:
-
基于训练数据得到的损失值和准确率分别为
0.339和0.855。与之前的两个模型相比,损失和准确率都有所改善。 -
我们可以使用这个模型对训练数据中的每个评论进行预测,将其与实际标签进行比较,然后以混淆矩阵的形式总结结果。
-
对于训练数据,混淆矩阵显示模型正确分类负面电影评论的比例约为 90%,正确分类正面评论的比例约为 81%。
-
所以,尽管模型性能整体有所提高,但我们在正确分类某一类别时,相比另一类别仍然观察到偏差。
在回顾了使用训练数据时模型的表现后,我们现在将使用测试数据重复这个过程。以下是获取损失、准确率和混淆矩阵的代码:
# Loss and accuracy
model %>% evaluate(test_x, test_y)
$loss
[1] 0.3761043
$acc
[1] 0.83664
pred1 <- model %>% predict_classes(test_x)
# Confusion Matrix
table(Predicted=pred1, Actual=imdb$test$y)
Actual
Predicted 0 1
0 10916 2500
1 1584 10000
从前面的代码输出中,我们可以得出以下观察结果:
-
对于测试数据,损失和准确率分别为 0.376 和 0.837。这两个结果都显示出相较于前两个模型,测试数据的分类表现更好。
-
混淆矩阵显示,负面电影评论的正确分类率约为 87.3%,而正面评论的正确分类率约为 80%。
-
因此,这些结果与使用训练数据得到的结果一致,并且显示出与我们在训练数据中观察到的类似偏差。
总结来说,通过添加额外的 LSTM 层,我们能够提高模型的电影评论情感分类性能。然而,我们在正确分类某一类别时,仍然观察到偏差,与另一类别相比。虽然我们在提升模型性能方面取得了一定成功,但仍有提升模型分类性能的空间。
实验使用双向 LSTM 层
双向 LSTM,顾名思义,不仅使用作为输入提供的整数序列,还利用其反向顺序作为额外输入。在某些情况下,这种方法可能有助于通过捕捉数据中可能未被原始 LSTM 网络捕获的有用模式,进一步提高模型的分类性能。
对于本实验,我们将修改第一次实验中的 LSTM 层,如下代码所示:
# Model architecture
model <- keras_model_sequential() %>%
layer_embedding(input_dim = 500, output_dim = 32) %>%
bidirectional(layer_lstm(units = 32)) %>%
layer_dense(units = 1, activation = "sigmoid")
# Model summary
summary(model)
Model
__________________________________________________________
Layer (type) Output Shape Param #
==========================================================
embedding_8 (Embedding) (None, None, 32) 16000
__________________________________________________________
bidirectional_5 (Bidirect (None, 64) 16640
__________________________________________________________
dense_11 (Dense) (None, 1) 65
==========================================================
Total params: 32,705
Trainable params: 32,705
Non-trainable params: 0
__________________________________________________________
从前面的代码输出中,我们可以得出以下观察结果:
-
我们使用双向
()函数将 LSTM 层转换为双向 LSTM 层。 -
这一改变将与 LSTM 层相关的参数数量增加到 16,640,从模型摘要中可以看到这一点。
-
该架构的总参数数量现已增加到 32,705。参数数量的增加将进一步降低网络训练的速度。
这是双向 LSTM 网络架构的简单流程图:

双向 LSTM 网络的流程图展示了嵌入层、双向层和密集层。在双向 LSTM 层中,tanh 被用作激活函数,而密集层使用 sigmoid 激活函数。编译和训练模型的代码如下:
# Compiling model
model %>% compile(optimizer = "adam",
loss = "binary_crossentropy",
metrics = c("acc"))
# Fitting model
model_four <- model %>% fit(train_x, train_y,
epochs = 10,
batch_size = 128,
validation_split = 0.2)
# Loss and accuracy plot
plot(model_four)
从前面的代码可以看出,我们将继续使用 adam 优化器,并保持其他设置与之前相同,进行模型的编译和拟合。
在我们训练模型后,每个 epoch 的准确率和损失值会存储在 model_four 中。我们使用 model_four 中的损失和准确率值来绘制以下图表:

从前面的图表中,我们可以得出以下观察结果:
-
损失和准确率图表没有显示过拟合的任何问题,因为训练和验证的曲线相对接近。
-
图表还显示,我们无需超过十个 epoch 就可以训练这个模型。
我们将使用以下代码获取训练数据的损失值、准确率和混淆矩阵:
# Loss and accuracy
model %>% evaluate(train_x, train_y)
$loss
[1] 0.3410529
$acc
[1] 0.85232
pred <- model %>% predict_classes(train_x)
# Confusion Matrix
table(Predicted=pred, Actual=imdb$train$y)
Actual
Predicted 0 1
0 10597 1789
1 1903 10711
从前面的代码输出中,我们可以得出以下观察结果:
-
对于训练数据,我们得到的损失值和准确率分别为 0.341 和 0.852。这些结果仅比之前的结果稍微差一些,差异不大。
-
这次的混淆矩阵显示了正确分类正面和负面电影评论的表现更加均衡。
-
对于负面电影评论,正确分类率约为 84.8%,对于正面评论,正确分类率约为 85.7%。
-
这个大约 1% 的差异远小于我们在早期模型中观察到的差异。
我们现在将对测试数据重复之前的过程。以下是获取损失、准确率和混淆矩阵的代码:
# Loss and accuracy
model %>% evaluate(test_x, test_y)
$loss
[1] 0.3737377
$acc
[1] 0.83448
pred1 <- model %>% predict_classes(test_x)
#Confusion Matrix
table(Predicted=pred1, Actual=imdb$test$y)
Actual
Predicted 0 1
0 10344 1982
1 2156 10518
从前面的代码输出中,我们可以得出以下观察结果:
-
对于测试数据,损失值和准确率分别为 0.374 和 0.834。
-
混淆矩阵显示,负面评论的正确分类率约为 82.8%。
-
该模型正确分类正面电影评论的准确率约为 84.1%。
-
这些结果与训练数据中获得的结果一致。
双向 LSTM 的实验帮助我们获得了与之前实验中使用两个 LSTM 层时相似的损失和准确率表现。然而,主要的进展在于我们能够在正确分类负面或正面电影评论时,表现出更好的一致性。
本章中,我们使用 LSTM 网络开发了一个电影评论情感分类模型。当数据涉及序列时,LSTM 网络有助于捕捉序列中词语或整数的长期依赖关系。我们通过对模型进行一些修改,实验了四种不同的 LSTM 模型,相关结果总结在以下表格中。
本表总结了四个 LSTM 模型的性能:
| 模型 | LSTM 层 | 优化器 | 数据 | 损失 | 准确率 | 负面评论准确率或特异性 | 正面评论准确率或敏感性 |
|---|---|---|---|---|---|---|---|
| 一 | 1 | rmsprop |
训练 | 0.375 | 82.8% | 74.1% | 91.4% |
| 测试 | 0.399 | 81.9% | 73.3% | 90.7% | |||
| 二 | 1 | adam |
训练 | 0.360 | 84.3% | 88.9% | 79.7% |
| 测试 | 0.385 | 82.9% | 86.9% | 78.8% | |||
| 三 | 2 | adam |
训练 | 0.339 | 85.5% | 90.0% | 81.0% |
| 测试 | 0.376 | 83.7% | 87.3% | 80.0% | |||
| 四 | 双向 | adam |
训练 | 0.341 | 85.2% | 84.8% | 85.7% |
| 测试 | 0.374 | 83.4% | 82.8% | 84.1% |
我们可以从上述表格中做出以下观察:
-
在尝试的四个模型中,双向 LSTM 模型相比其他三个模型提供了更好的性能。它在基于测试数据的损失值方面最低。
-
尽管第四个模型的整体准确率相比第三个模型略低,但其正确分类负面和正面评论的准确性更加稳定,变化范围从 82.8%到 84.1%,即约 1.3%的波动。
-
第三个模型似乎偏向于负面评论,正确分类该类评论的测试数据准确率为 87.3%。对于第三个模型,正面评论在测试数据中的正确分类准确率仅为 80%。因此,第三个模型中负面和正面评论的正确分类差距超过 7%。
-
前两个模型的敏感性和特异性之间的差距甚至更大。
尽管第四个模型提供了良好的结果,但通过进一步实验其他变量,仍然可以探索额外的改进。可以用于进一步实验的变量包括最常见词汇的数量、使用前后填充和/或截断、填充时使用的最大长度、LSTM 层中的单元数,以及在编译模型时选择其他优化器。
概述
本章中,我们展示了使用 LSTM 网络来开发电影评论情感分类模型。我们在上一章中使用的递归神经网络所面临的一个问题是,难以捕捉序列中两个词语/整数之间可能存在的长期依赖关系。长短期记忆(LSTM)网络的设计目的是人为地保留在处理长句子或长整数序列时重要的长期记忆。
在下一章中,我们将继续处理文本数据,并探索卷积递归神经网络(CRNNs)的应用,CRNNs 结合了卷积神经网络(CNNs)和递归神经网络(RNNs)的优点,形成了一个单一的网络。我们将通过一个有趣且公开可用的文本数据集reuter_50_50来说明这种网络的应用。
第十四章:使用卷积递归神经网络进行文本分类
卷积神经网络(CNNs)被发现能够有效捕捉数据中的高层次局部特征。另一方面,递归神经网络(RNNs),例如长短期记忆(LSTM),则在捕捉涉及文本等序列数据的长期依赖性方面表现出色。当我们在同一个模型架构中同时使用 CNN 和 RNN 时,就会产生所谓的卷积递归神经网络(CRNNs)。
本章说明了如何通过结合 RNN 和 CNN 网络的优点,将卷积递归神经网络应用于文本分类问题。这个过程包括文本数据准备、定义卷积递归网络模型、训练模型以及模型评估等步骤。
更具体地说,本章将涵盖以下内容:
-
使用 reuter_50_50 数据集
-
准备模型构建的数据
-
开发模型架构
-
编译和拟合模型
-
评估模型并预测类别
-
性能优化建议和最佳实践
使用 reuter_50_50 数据集
在之前的章节中,当处理文本数据时,我们使用了已经转换为整数序列的数据来开发深度网络模型。本章中,我们将使用需要转换为整数序列的文本数据。我们将从读取将用于展示如何开发文本分类深度网络模型的数据开始。我们还将探索我们将使用的数据集,以便更好地理解它。
在本章中,我们将使用keras、deepviz和readtext库,如下所示的代码:
# Libraries used
library(keras)
library(deepviz)
library(readtext)
为了说明开发卷积递归网络模型的步骤,我们将使用来自 UCI 机器学习库的reuter_50_50文本数据集:archive.ics.uci.edu/ml/datasets/Reuter_50_50#。
该数据集包含两个文件夹中的文本文件,一个文件夹用于训练数据,另一个用于测试数据:
-
存放训练数据的文件夹有 2,500 个文本文件,每个文件包含 50 篇来自 50 位作者的文章。
-
类似地,存放测试数据的文件夹也包含 2,500 个文本文件,每个文件包含 50 篇来自同样 50 位作者的文章。
读取训练数据
我们可以通过访问我们提供的 UCI 机器学习库链接中的Data文件夹来访问reuter_50_50数据集。在这里,我们可以下载C50.zip文件夹。解压后,它包含一个名为C50的文件夹,其中包含C50train和C50test文件夹。首先,我们将使用以下代码读取C50train文件夹中的文本文件:
# Reading Reuters train data
setwd("~/Desktop/C50/C50train")
temp = list.files(pattern="*.*")
k <- 1; tr <- list(); trainx <- list(); trainy <- list()
for (i in 1:length(temp)) {for (j in 1:50)
{ trainy[k] <- temp[i]
k <- k+1}
author <- temp[i]
files <- paste0("~/Desktop/C50/C50train/", author, "/*")
tr <- readtext(files)
trainx <- rbind(trainx, tr)}
trainx <- trainx$text
在前面的代码帮助下,我们可以将来自C50train文件夹的 2,500 篇文章数据读取到trainx中,并将作者的名字信息保存到trainy中。我们首先通过setwd函数将工作目录设置为C50train文件夹。C50train文件夹包含 50 个以作者名字命名的文件夹,每个文件夹里有 50 篇该作者撰写的文章。我们将 k 的值设为 1,并将tr、trainx和trainy初始化为列表。然后,我们创建一个循环,使得每篇文章的作者名字存储在trainy中,而trainx中存储相应的文章。需要注意的是,在读取这 2,500 个文本文件之后,trainx也包含了文件名信息。通过最后一行代码,我们只保留 2,500 篇文本的数据,并移除不需要的文件名信息。
现在,让我们通过以下代码查看train数据中的文本文件 901 的内容:
# Text file 901
trainx[901]
[1] "Drug discovery specialist Chiroscience Group plc said on Monday it is testing two anti-cancer compounds before deciding which will go forward into human trials before the end of the year.\nBoth are MMP inhibitors, the same novel class of drug as British Biotech Plc's potential blockbuster Marimastat, which are believed to stop cancer cells from spreading.\nIn an interview, chief executive John Padfield said Chiroscience hoped to have its own competitor to Marimastat in early trials next year and Phase III trials in 1998."
# Author
trainy[901]
[[1]]
[1] "JonathanBirt"
从前面的代码和输出中,我们可以做出以下观察:
-
trainx中的测试文件 901 包含关于 Chiroscience 集团的药物试验的某些新闻项目 -
这篇短文的作者是 Jonathan Birt
在读取了训练数据的文本文件和作者名字之后,我们可以对测试数据重复这一过程。
读取测试数据
现在,我们将读取位于C50文件夹内的C50test文件夹中的文本文件。我们将使用以下代码来实现:
# Reuters test data
setwd("~/Desktop/C50/C50test")
temp = list.files(pattern="*.*")
k <- 1; tr <- list(); testx <- list(); testy <- list()
for (i in 1:length(temp)) {for (j in 1:50)
{ testy[k] <- temp[i]
k <- k+1}
author <- temp[i]
files <- paste0("~/Desktop/C50/C50test/", author, "/*")
tr <- readtext(files)
testx <- rbind(testx, tr)}
testx <- testx$text
在这里,我们可以看到这段代码中唯一的变化是,我们根据位于C50test文件夹中的测试数据创建了testx和testy。我们从C50test文件夹中读取了 2,500 篇文章到testx,并将作者的名字信息保存到testy中。再次使用最后一行代码,我们只保留了 2,500 篇来自测试数据的文本,并删除了不需要的文件名信息,这些信息对于我们的分析没有用处。
现在我们已经创建了训练数据和测试数据,我们将进行数据预处理,以便能够开发一个作者分类模型。
为模型构建准备数据
在这一部分,我们将准备一些数据,以便能够开发一个作者分类模型。我们将从使用标记将以文章形式存在的文本数据转换为整数序列开始。我们还将进行更改,以通过唯一的整数来识别每个作者。随后,我们将使用填充和截断方法,使表示 50 个作者文章的整数序列达到相同的长度。最后,我们将通过将训练数据划分为训练集和验证集,并对响应变量进行独热编码,结束这一部分。
标记化和将文本转换为整数序列
我们将从进行标记化开始,然后将文本形式的文章转换为整数序列。为此,我们可以使用以下代码:
# Tokenization
token <- text_tokenizer(num_words = 500) %>%
fit_text_tokenizer(trainx)
# Text to sequence of integers
trainx <- texts_to_sequences(token, trainx)
testx <- texts_to_sequences(token, testx)
# Examples
trainx[[7]]
[1] 98 4 41 5 4 2 4 425 5 20 4 9 4 195 5 157 1 18
[19] 87 3 90 3 59 1 169 346 2 29 52 425 6 72 386 110 331 24
[37] 5 4 3 31 3 22 7 65 33 169 329 10 105 1 239 11 4 31
[55] 11 422 8 60 163 318 10 58 102 2 137 329 277 98 58 287 20 81
[73] 3 142 9 6 87 3 49 20 142 2 142 6 2 60 13 1 470 8
[91] 137 190 60 1 85 152 5 6 211 1 3 1 85 11 2 211 233 51
[109] 233 490 7 155 3 305 6 4 86 3 70 4 3 157 52 142 6 282
[127] 233 4 286 11 485 47 11 9 1 386 497 2 72 7 33 6 3 1
[145] 60 3 234 23 32 72 485 7 203 6 29 390 5 3 19 13 55 184
[163] 53 10 1 41 19 485 119 18 6 59 1 169 1 41 10 17 458 91
[181] 6 23 12 1 3 3 10 491 2 14 1 1 194 469 491 2 1 4
[199] 331 112 485 475 16 1 469 1 331 14 2 485 234 5 171 296 1 85
[217] 11 135 157 2 189 1 31 24 4 5 318 490 338 6 147 194 24 347
[235] 386 23 24 32 117 286 161 6 338 25 4 32 2 9 1 38 8 316
[253] 60 153 27 234 496 457 153 20 316 2 254 219 145 117 25 46 27 7
[271] 228 34 184 75 11 418 52 296 1 194 469 180 469 6 1 268 6 250
[289] 469 29 90 6 15 58 175 32 33 229 37 424 36 51 36 3 169 15
[307] 1 7 175 1 319 207 5 4
trainx[[901]]
[1] 74 356 7 9 199 12 11 61 145 31 22 399 79 145 1 133 3 1 28 203
[21] 29 1 319 3 18 101 470 31 29 2 20 5 33 369 116 134 7 2 25 17
[41] 303 2 5 222 100 28 6 5
从前面的代码和输出中,我们可以观察到以下内容:
-
对于分词,我们将
num_words指定为 500,表示我们将使用训练数据中最频繁的 500 个单词。 -
请注意,使用
fit_text_tokenizer会自动将文本转换为小写,并去除任何可以在包含文本数据的文章中看到的标点符号。将文本转换为小写有助于避免单词重复的问题,其中一个可能包含小写字母字符,而另一个则可能包含大写字母字符。标点符号被去除,因为在使用文本作为输入开发作者分类模型时,它不会提供额外的价值。 -
我们使用
texts_to_sequences将文本中最频繁出现的单词转换为整数序列。这样做的原因是将非结构化数据转换为结构化格式,这是深度学习模型所要求的。 -
文本文件 7 的输出显示,共有 314 个整数,范围在 1 到 497 之间。
-
看一下文本文件 901 的输出(这是我们之前回顾过的训练数据中的相同示例),可以看到它包含 48 个介于 1 到 470 之间的整数。原始文本包含超过 80 个单词,而那些不属于最频繁的 500 个单词的词汇没有出现在这个整数序列中。
-
前五个整数,即 74、356、7、9 和 199,分别对应于单词
group、plc、said、on和monday。文本开头的其他单词没有被转换为整数,因为它们不属于文章中最频繁的 500 个单词。
现在,让我们来看一下训练数据和测试数据中每篇文章包含的整数数量。我们可以使用以下代码来实现:
# Integers per article for train data
z <- NULL
for (i in 1:2500) {z[i] <- print(length(trainx[[i]]))}
summary(z)
Min. 1st Qu. Median Mean 3rd Qu. Max.
31.0 271.0 326.0 326.8 380.0 918.0
# Intergers per article for text data
z <- NULL
for (i in 1:2500) {z[i] <- print(length(testx[[i]]))}
summary(z)
Min. 1st Qu. Median Mean 3rd Qu. Max.
39.0 271.0 331.0 329.1 384.0 1001.0
从前面的总结中,我们可以得出以下观察结果:
-
训练数据中每篇文章包含的整数数量范围从 31 到 918,中位数约为 326 个单词。
-
类似地,测试数据中每篇文章包含的整数数量范围从 39 到 1001,且中位数约为 331。
-
如果将最频繁出现的单词数量从 500 增加到更高的值,则单词的中位数也预计会增加。这可能会导致模型架构和参数值需要做适当的调整。例如,每篇文章的单词数量增加可能需要在深度网络中增加更多的神经元。
训练数据中每个文本文件包含整数的直方图如下:

训练数据中文本文件的整数分布直方图显示了整体模式,平均值和中位数大约为 326。该直方图的尾部稍微向较高的值延伸,呈现出轻度右偏或正偏的模式。
现在我们已将文本数据转换为整数序列,我们还将把训练数据和测试数据的标签转换为整数。
将标签转换为整数
在为分类问题开发深度学习网络时,我们总是使用整数形式的响应或标签。训练集和测试集文本数据的作者名称分别存储在 trainy 和 testy 中。trainy 和 testy 都是包含 50 个作者名称的 2,500 项列表。为了将标签转换为整数,我们可以使用以下代码:
# Train and test labels to integers
trainy <- as.factor(unlist(trainy))
trainy <- as.integer(trainy) -1
testy <- as.factor(unlist(testy))
testy <- as.integer(testy) -1
# Saving original labels
trainy_org <- trainy
testy_org <- testy
如我们所见,要将包含作者名称的标签转换为整数,我们需要将其转化为列表,然后使用从 0 到 49 的整数来表示 50 位作者。我们还可以使用 trainy_org 和 testy_org 来保存这些原始整数标签,以备后用。
接下来,我们将进行填充和截断,以确保每篇文章的整数序列长度相等。
序列的填充和截断
在开发作者分类模型时,每个训练和测试文本数据的整数数量需要相等。我们可以通过填充和截断整数序列来实现这一点,如下所示:
# Padding and truncation
trainx <- pad_sequences(trainx, maxlen = 300)
testx <- pad_sequences(testx, maxlen = 300)
dim(trainx)
[1] 2500 300
在这里,我们指定所有序列的最大长度,即 maxlen,为 300。这将截断任何超过 300 个整数的文章序列,并向短于 300 个整数的序列添加零。注意,对于填充和截断,默认设置为 "pre",并且代码中没有特别指明。
这意味着,对于截断和填充,整数序列开头的整数会受到影响。对于序列末尾的填充和/或截断,我们可以在代码中使用 padding = "post" 和/或 truncation = "post"。我们还可以看到,trainx 的维度显示为一个 2,500 x 300 的矩阵。
让我们来看一下训练数据中文本文件 7 和 901 的输出,如下所示:
# Example of truncation
trainx[7,]
[1] 5 157 1 18 87 3 90 3 59 1 169 346 2 29 52 425
[17] 6 72 386 110 331 24 5 4 3 31 3 22 7 65 33 169
[33] 329 10 105 1 239 11 4 31 11 422 8 60 163 318 10 58
[49] 102 2 137 329 277 98 58 287 20 81 3 142 9 6 87 3
[65] 49 20 142 2 142 6 2 60 13 1 470 8 137 190 60 1
[81] 85 152 5 6 211 1 3 1 85 11 2 211 233 51 233 490
[97] 7 155 3 305 6 4 86 3 70 4 3 157 52 142 6 282
[113] 233 4 286 11 485 47 11 9 1 386 497 2 72 7 33 6
[129] 3 1 60 3 234 23 32 72 485 7 203 6 29 390 5 3
[145] 19 13 55 184 53 10 1 41 19 485 119 18 6 59 1 169
[161] 1 41 10 17 458 91 6 23 12 1 3 3 10 491 2 14
[177] 1 1 194 469 491 2 1 4 331 112 485 475 16 1 469 1
[193] 331 14 2 485 234 5 171 296 1 85 11 135 157 2 189 1
[209] 31 24 4 5 318 490 338 6 147 194 24 347 386 23 24 32
[225] 117 286 161 6 338 25 4 32 2 9 1 38 8 316 60 153
[241] 27 234 496 457 153 20 316 2 254 219 145 117 25 46 27 7
[257] 228 34 184 75 11 418 52 296 1 194 469 180 469 6 1 268
[273] 6 250 469 29 90 6 15 58 175 32 33 229 37 424 36 51
[289] 36 3 169 15 1 7 175 1 319 207 5 4
# Example of padding
trainx[901,]
[1] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[17] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[33] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[49] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[65] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[81] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[97] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[113] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[129] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[145] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[161] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[177] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[193] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[209] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[225] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
[241] 0 0 0 0 0 0 0 0 0 0 0 0 74 356 7 9
[257] 199 12 11 61 145 31 22 399 79 145 1 133 3 1 28 203
[273] 29 1 319 3 18 101 470 31 29 2 20 5 33 369 116 134
[289] 7 2 25 17 303 2 5 222 100 28 6 5
从前面的输出中,我们可以得出以下观察结果:
-
文本文件 7,原本有 314 个整数,已经减少到 300 个整数。请注意,这一步移除了序列开头的 14 个整数。
-
文本文件 901,原本有 48 个整数,现在有 300 个整数,这通过在序列的开头添加零来人工使整数总数达到 300 来实现。
接下来,我们将把训练数据划分为训练数据和验证数据,这将在拟合模型时用于训练和评估网络。
数据分割
在训练模型时,我们使用validation_split,该参数使用指定比例的训练数据来评估验证误差。此示例中的训练数据包含来自第一作者的前 50 篇文章,接着是第二作者的 50 篇文章,以此类推。如果我们将validation_split指定为 0.2,模型将基于前 40 位作者的前 80%(或 2000 篇)文章进行训练,而最后 10 位作者的最后 20%(或 500 篇)文章将用于评估验证误差。这样,模型训练时将不会使用最后 10 位作者的输入。为了克服这个问题,我们将使用以下代码随机划分训练数据为训练集和验证集:
# Data partition
trainx_org <- trainx
testx_org <- testx
set.seed(1234)
ind <- sample(2, nrow(trainx), replace = T, prob=c(.8, .2))
trainx <- trainx_org[ind==1, ]
validx <- trainx_org[ind==2, ]
trainy <- trainy_org[ind==1]
validy <- trainy_org[ind==2]
如我们所见,为了将数据划分为训练集和验证集,我们使用了 80:20 的划分比例。我们还使用了set.seed函数以确保结果的可重复性。
在划分训练数据后,我们将对标签进行一热编码,这有助于我们用值为 1 表示正确的作者,用值为 0 表示所有其他作者。
对标签进行一热编码
为了对标签进行一热编码,我们将使用以下代码:
# OHE
trainy <- to_categorical(trainy, 50)
validy <- to_categorical(validy, 50)
testy <- to_categorical(testy, 50)
在这里,我们使用了to_categorical函数对响应变量进行一热编码。我们使用 50 表示 50 个类别的存在,因为这些文章是由 50 位作者所写,我们计划对其进行分类,并使用他们写的文章作为输入。
现在,数据已准备好用于基于作者所写文章的作者分类卷积递归网络模型的开发。
开发模型架构
在这一部分中,我们将在同一网络中使用卷积层和 LSTM 层。卷积递归网络架构可以用一个简单的流程图来表示:

在这里,我们可以看到流程图包含了嵌入层、卷积 1D 层、最大池化层、LSTM 层和全连接层。请注意,嵌入层始终是网络中的第一层,并且常用于涉及文本数据的应用。嵌入层的主要目的是为每个唯一的单词找到一个映射,在我们的例子中是 500,并将其转换为一个较小的向量大小,这个大小我们将使用output_dim指定。在卷积层中,我们将使用relu激活函数。类似地,LSTM 层和全连接层将分别使用tanh和softmax激活函数。
我们可以使用以下代码来开发模型架构。这个代码还包括模型总结的输出:
# Model architecture
model <- keras_model_sequential() %>%
layer_embedding(input_dim = 500,
output_dim = 32,
input_length = 300) %>%
layer_conv_1d(filters = 32,
kernel_size = 5,
padding = "valid",
activation = "relu",
strides = 1) %>%
layer_max_pooling_1d(pool_size = 4) %>%
layer_lstm(units = 32) %>%
layer_dense(units = 50, activation = "softmax")
# Model summary
summary(model)
___________________________________________________________________________
Layer (type) Output Shape Param #
===========================================================================
embedding (Embedding) (None, 300, 32) 16000
___________________________________________________________________________
conv1d (Conv1D) (None, 296, 32) 5152
___________________________________________________________________________
max_pooling1d (MaxPooling1D) (None, 74, 32) 0
___________________________________________________________________________
lstm (LSTM) (None, 32) 8320
___________________________________________________________________________
dense (Dense) (None, 50) 1650
===========================================================================
Total params: 31,122
Trainable params: 31,122
Non-trainable params: 0
___________________________________________________________________________
从前面的代码中,我们可以得出以下结论:
-
我们已将
input_dim指定为 500,这在数据准备过程中作为最常见词的数量使用。 -
对于
output_dim,我们使用了 32,表示嵌入向量的大小。不过需要注意的是,其他数字也可以进行探索,我们将在本章后续的性能优化时进行探索。 -
对于
input_length,我们指定了 300,这是每个序列中整数的数量。
在嵌入层之后,我们添加了一个具有 32 个滤波器的 1D 卷积层。在之前的章节中,我们在处理图像分类问题时使用了 2D 卷积层。在这个例子中,我们的数据涉及到序列,在这种情况下,1D 卷积层更为合适。对于这个层,我们指定了以下内容:
-
1D 卷积窗口的长度通过
kernel_size指定为 5。 -
我们使用
valid进行填充,表示不需要任何填充。 -
我们指定了激活函数为
relu。 -
卷积的步幅已被指定为 1。
卷积层后接池化层。以下是一些关于池化和后续层的注释:
-
卷积层帮助我们提取特征,而卷积层后的池化层帮助我们进行下采样并检测重要特征。
-
在这个例子中,我们指定了池化大小为 4,这意味着输出的大小(74)是输入的四分之一(296)。这一点在模型总结中也可以看到。
-
下一个层是一个具有 32 个单元的 LSTM 层。
-
最后一层是一个具有 50 个单元的密集层,代表 50 个作者,并使用
softmax激活函数。 -
softmax激活函数使得所有 50 个输出的总和为 1,从而可以作为每个作者的概率值。 -
从模型的总结中可以看出,该网络的总参数量为 31,122。
接下来,我们将编译模型,并开始训练。
编译并拟合模型
在这一部分,我们将编译模型,然后使用fit函数在训练和验证数据集上训练模型。我们还将绘制在训练过程中获得的损失和准确性值。
编译模型
编译模型时,我们将使用以下代码:
# Compile model
model %>% compile(optimizer = "adam",
loss = "categorical_crossentropy",
metrics = c("acc"))
在这里,我们指定了adam优化器。由于标签基于 50 个作者,我们使用categorical_crossentropy作为损失函数。对于评估指标,我们指定了作者分类的准确性。
现在模型已经被编译好,准备进行训练。
拟合模型
我们将使用以下代码训练模型:
# Fitting the model
model_one <- model %>% fit(trainx, trainy,
epochs = 30,
batch_size = 32,
validation_data = list(validx, validy))
# Loss and accuracy plot
plot(model_one)
在这里,我们使用trainx作为输入,trainy作为输出来训练模型。模型将训练 30 个 epoch,批次大小为 32。在训练过程中,为了评估每个 epoch 的验证损失和验证准确性,我们使用了先前通过从训练数据中随机抽取大约 20%的数据生成的validx和validy。
基于训练数据和验证数据的损失值与准确度值,在每个 30 个 epoch 中被存储在model_one中。以下是这些数据的图示:

从之前的图表中,我们可以得出以下观察结果:
-
训练数据和验证数据的损失值在从第 1 个到第 30 个 epoch 的过程中逐渐减小。然而,验证数据的损失值相较于训练数据的损失值,随着训练进展的过程中减小的速度较慢。
-
训练数据和验证数据的准确度值呈现出相似的趋势,但方向相反。
-
增加训练过程中 epoch 的数量可能会改善损失值和准确度值;然而,曲线之间的发散度也可能增加,这可能导致过拟合问题。
接下来,我们将评估model_one并使用训练数据和测试数据进行预测。
评估模型并预测类别
在本节中,我们将基于训练数据和测试数据评估模型。我们将通过使用训练数据和测试数据的混淆矩阵,正确分类每个作者来获得准确度,并进一步洞察。我们还将使用条形图来可视化每个作者的准确度。
使用训练数据进行模型评估
首先,我们将使用训练数据评估模型的表现。然后,我们将使用模型预测表示 50 个作者每个类别的标签。评估模型的代码如下:
# Loss and accuracy
model %>% evaluate(trainx, trainy)
$loss
[1] 1.45669
$acc
[1] 0.5346288
在这里,我们可以看到,通过使用训练数据,我们获得了大约 1.457 的损失值和大约 0.535 的准确度。接下来,我们使用模型对训练数据中的文章类别进行预测。然后,我们使用这些预测来计算每个代表 50 个作者的类别的准确度。实现这一目标的代码如下:
# Prediction and confusion matrix
pred <- model %>% predict_classes(trainx_org)
tab <- table(Predicted=pred, Actual=trainy_org)
(accuracy <- 100*diag(tab)/colSums(tab))
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
82 40 30 10 54 46 54 82 8 56 46 36 76 18 52 90 50 56 8 66 80 24 30 46 32
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
46 88 62 22 64 76 2 74 88 72 74 76 86 70 60 86 38 32 0 48 6 24 76 8 22
在之前的代码中,为了节省空间,我们没有打印混淆矩阵的输出,因为它将是一个 50 x 50 的矩阵。然而,我们已经利用混淆矩阵中的信息,通过正确预测每个作者基于其写的文章来得出模型的准确度。我们得到的输出如下:

前述的条形图提供了更多关于模型在每个作者上的表现的洞察:
-
正确分类作者的准确率在第 15 位作者上达到了最高值 90%。
-
正确分类一个作者的准确率对于第 43 位作者来说,达到了最低值 0%。
-
这个模型在正确分类某些作者的文章时存在困难,比如标记为 3、8、18、31、43、45 和 48 的作者。
在使用训练数据评估了模型之后,我们将使用测试数据重复此过程。
使用测试数据进行模型评估
我们将使用以下代码,通过模型从测试数据中获取损失值和准确度值:
# Loss and accuracy
model %>% evaluate(testx, testy)
$loss
[1] 2.460835
$acc
[1] 0.2508
从前面的代码中,我们可以看到,基于测试数据的损失和准确率值分别为 2.461 和 0.251。这两个结果都不如我们基于训练数据得到的结果,通常这是可以预期的。预测每个作者的类别并计算分类的准确性,如以下代码所示,将有助于提供进一步的见解:
# Prediction and confusion matrix
pred1 <- model %>% predict_classes(testx)
tab1 <- table(Predicted=pred1, Actual=testy_org)
(accuracy <- 100*diag(tab1)/colSums(tab1))
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
22 28 2 2 28 14 14 20 6 28 24 8 28 8 46 84 14 36 10 50 40 12 4 22 4
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
18 54 38 12 34 46 0 52 26 48 40 26 84 46 18 24 26 10 0 46 0 4 38 0 10
混淆矩阵中的信息存储在tab1中,用于计算每个作者的文章正确分类的准确率。结果如下:

测试数据的整体准确率大约为 25%,这已经表明基于测试数据的性能明显较差。这一点也可以从前面的柱状图中看到。让我们来看一下我们从中可以得出的观察结果:
-
对于标签为 31、43、45 和 48 的作者,每位作者的 50 篇文章中没有一篇被正确分类。
-
来自标签为 15 和 38 的作者的文章中,超过 80%被正确分类。
从这个初步的例子中,我们可以看到我们的模型分类性能需要进一步改善。我们在训练数据和测试数据中观察到的性能差异也表明了过拟合问题的存在。因此,我们需要对模型架构进行修改,以获得一个不仅能在分类性能上提供更高准确率,而且在训练数据和测试数据之间表现一致的模型。我们将在下一部分中探讨这一点。
性能优化技巧和最佳实践
在本节中,我们将探讨可以对模型架构和其他设置进行的修改,以提高作者分类性能。我们将进行两个实验,在这两个实验中,我们将最常见词汇的数量从 500 增加到 1,500,并将整数序列的长度从 300 增加到 400。对于这两个实验,我们还将在池化层之后添加一个 dropout 层。
尝试减少批量大小
我们将用于此实验的代码如下:
# Model architecture
model <- keras_model_sequential() %>%
layer_embedding(input_dim = 1500,
output_dim = 32,
input_length = 400) %>%
layer_conv_1d(filters = 32,
kernel_size = 5,
padding = "valid",
activation = "relu",
strides = 1) %>%
layer_max_pooling_1d(pool_size = 4) %>%
layer_dropout(0.25) %>%
layer_lstm(units = 32) %>%
layer_dense(units = 50, activation = "softmax")
# Compiling the model
model %>% compile(optimizer = "adam",
loss = "categorical_crossentropy",
metrics = c("acc"))
# Fitting the model
model_two <- model %>% fit(trainx, trainy,
epochs = 30,
batch_size = 16,
validation_data = list(validx, validy))
# Plot of loss and accuracy
plot(model_two)
从前面的代码中,我们可以得出以下观察结论:
-
我们将通过指定
input_dim为 1,500 和input_length为 400 来更新模型架构。 -
我们将把在训练模型时使用的批量大小从 32 减少到 16。
-
为了应对过拟合问题,我们已经添加了一个 dropout 层,丢弃率为 25%。
-
我们保持了所有其他设置与之前模型所用设置相同。
基于训练和验证数据的损失和准确率值,针对 30 个训练周期,每个周期的结果存储在model_two中。结果可以在以下图表中查看:

前面的图表表明,验证数据的损失和准确率值在最后几个周期保持平稳。然而,它们并没有恶化。接下来,我们将基于训练数据和测试数据,使用evaluate函数来获取损失和准确率值,如下所示:
# Loss and accuracy for train data
model %>% evaluate(trainx, trainy)
$loss
[1] 0.3890106
$acc
[1] 0.9133034
# Loss and accuracy for test data
model %>% evaluate(testx, testy)
$loss
[1] 2.710119
$acc
[1] 0.308
从前面的代码和输出中,我们可以观察到训练数据的损失和准确率值相较于前一个模型有了更好的结果。然而,对于测试数据,尽管准确率有所提升,损失值却略微变差。
从每个作者的测试数据中正确分类文章所获得的准确率,可以在以下条形图中看到:

从前面的条形图中,我们可以得出以下观察结果:
-
条形图清晰地展示了与之前的模型相比的改进。
-
在前一个模型中,对于测试数据,我们有四个作者的文章没有被正确分类。然而,现在我们没有任何作者的文章是错误分类的。
在下一个实验中,我们将查看更多的改动,努力进一步提升作者分类性能。
在 CNN 中尝试批量大小、卷积核大小和滤波器
将用于此次实验的代码如下:
# Model architecture
model <- keras_model_sequential() %>%
layer_embedding(input_dim = 1500,
output_dim = 32,
input_length = 400) %>%
layer_conv_1d(filters = 64,
kernel_size = 4,
padding = "valid",
activation = "relu",
strides = 1) %>%
layer_max_pooling_1d(pool_size = 4) %>%
layer_dropout(0.25) %>%
layer_lstm(units = 32) %>%
layer_dense(units = 50, activation = "softmax")
# Compiling the model
model %>% compile(optimizer = "adam",
loss = "categorical_crossentropy",
metrics = c("acc"))
# Fitting the model
model_three <- model %>% fit(trainx, trainy,
epochs = 30,
batch_size = 8,
validation_data = list(validx, validy))
# Loss and accuracy plot
plot(model_three)
从前面的代码中,我们可以得出以下观察结果:
-
我们将卷积核的大小从 5 减小到了 4。
-
我们将卷积层的滤波器数量从 32 增加到了 64。
-
我们在训练模型时将批量大小从 16 减小到 8。
-
我们保持了与之前的模型相同的其他设置。
基于每个 30 个周期的训练和验证数据的损失和准确率值被存储在model_three中。该数据的图表如下:

损失和准确率的图表显示了以下内容:
-
验证数据的准确率在最后几个周期保持平稳,而训练数据的准确率在最后几个周期以较慢的速度增加。
-
基于验证数据的损失值在最后几个周期开始上升,并且训练数据的损失继续下降。
现在,我们将基于训练数据和测试数据,使用evaluate函数来获得损失和准确率的值,如下所示:
# Loss and accuracy for train data
model %>% evaluate(trainx, trainy)
$loss
[1] 0.1093387
$acc
[1] 0.9880419
# Loss and accuracy for test data
model %>% evaluate(testx, testy)
[1] 3.262691
$acc
[1] 0.337
从前面的代码和输出中,我们可以观察到以下几点:
-
基于训练数据的损失和准确率值相比前两个模型有所改进。
-
对于测试数据,尽管与前两个模型相比,损失值更高,但准确率约为 34%,在分类作者文章方面表现出更好的准确性。
以下条形图显示了正确分类测试数据中作者文章的准确率:

从前面的条形图中,我们可以观察到以下几点:
-
正确分类每个作者文章的准确度显示出比之前的两个模型更好的表现,因为我们没有任何作者的准确率为零。
-
当我们使用测试数据比较迄今为止使用的三个模型时,可以看到第一个模型有四个作者的分类准确率达到 50%或更高。然而,对于第二个和第三个模型,分类准确率达到 50%或更高的作者数量分别增加到 8 和 9。
在本节中,我们进行了两项实验,结果表明,模型的作者分类性能可以进一步提高。
总结
本章中,我们展示了基于作者所写文章开发卷积递归神经网络进行作者分类的步骤。卷积递归神经网络将两种网络的优点结合成一个网络。一方面,卷积网络能够从数据中捕捉到高级局部特征;另一方面,递归网络能够捕捉到数据中涉及序列的长期依赖关系。
首先,卷积递归神经网络通过一维卷积层提取特征。然后,这些提取的特征传递给 LSTM 递归层,以获取隐藏的长期依赖关系,接着传递给全连接的密集层。该密集层根据文章中的数据获得正确分类每个作者的概率。尽管我们在作者分类问题中使用了卷积递归神经网络,但这种深度网络也可以应用于其他类型的涉及序列的数据,如自然语言处理、语音和视频相关问题。
下一章将是本书的最后一章,主要讲解一些技巧、窍门以及未来的发展方向。为不同类型数据开发深度学习网络既是艺术也是科学。每一个应用都带来了新的挑战,同时也是我们学习和提升技能的机会。在下一章中,我们将总结一些经验,这些经验在某些应用中非常有用,并能帮助节省大量时间,以便更快地开发出表现优异的模型。
第五部分:前方的道路
本节讨论了将深度学习技术付诸实践的前景以及相关的技巧和窍门。
本节包含以下章节:
- 第十三章,技巧、窍门与前方的道路
第十五章:提示、技巧与前进的道路
在本书中,我们介绍了如何应用各种深度学习网络来开发预测和分类模型。我们所介绍的一些技巧和方法是针对特定应用领域的,并帮助我们在开发的模型中实现更好的预测或分类性能。
在本章中,我们将介绍一些技巧和方法,这些方法在你将这些方法应用于新数据和不同问题时将非常有用。我们将涵盖四个主题。请注意,这些方法在之前的章节中没有介绍,但我们将利用其中的一些示例来说明它们的使用。
在本章中,我们将涵盖以下主题:
-
用于训练性能可视化的 TensorBoard
-
使用 LIME 可视化深度网络模型
-
使用 tfruns 可视化模型训练
-
网络训练的提前停止
用于训练性能可视化的 TensorBoard
对于可视化深度网络训练性能,TensorBoard 是一个有用的工具,作为 TensorFlow 包的一部分提供。我们将重新运行在第二章,多类分类的深度神经网络中使用的深度网络模型,在那里我们使用 CTG 数据开发了一个用于患者的多类分类模型。有关数据处理、模型架构以及编译模型的代码,请参考第二章,多类分类的深度神经网络。
以下是来自第二章,多类分类的深度神经网络的 model_one 代码:
# Fitting model and TensorBoard
setwd("~/Desktop/")
model_one <- model %>% fit(training,
trainLabels,
epochs = 200,
batch_size = 32,
validation_split = 0.2,
callbacks = callback_tensorboard('ctg/one'))
tensorboard('ctg/one')
从前面的代码中,我们可以观察到以下内容:
-
我们已经设置了一个工作目录,这将是存储训练模型结果并在 TensorBoard 上进行可视化的桌面。
-
该模型使用额外的特征回调进行拟合,我们使用
callback_tensorboard函数将数据存储在桌面的ctg/one文件夹中,以便稍后进行可视化。 -
请注意,
ctg目录在模型拟合时会自动创建。 -
最后,
tensorboard函数用于可视化存储在ctg/one文件夹中的数据。
以下截图是 TensorBoard 的内容:

上面的截图显示了训练和验证数据在 200 个周期中的损失和准确率图。这用于训练模型。TensorBoard 上的这个可视化是交互式的,为用户提供了额外的选项,使他们可以在训练过程中探索和理解模型的表现。
正如我们在本书的所有章节中所看到的,所有介绍了各种深度学习方法的章节,都表明提高分类或预测模型的性能需要广泛的实验。为了帮助这种实验,一个使用 TensorBoard 的关键好处是,它允许通过交互式可视化非常轻松地比较模型的性能。
我们从第二章“多类分类的深度神经网络”中运行了三个模型,并将模型训练数据存储在ctg文件夹的two、three和four子文件夹中。运行以下代码以进行 TensorBoard 可视化:
# TensorBoard visualization for multiple models
tensorboard(c('ctg/one', 'ctg/two', 'ctg/three', 'ctg/four'))
上述代码为所有四个模型创建了 TensorBoard 可视化。以下是生成的 TensorBoard 页面的截图:

上述可视化展示了所有四个模型的训练和验证数据的损失值和准确率。以下是我们可以从该图表中得出的几点观察:
-
运行的四个模型的结果以不同的颜色呈现,便于我们快速识别并进行比较。
-
基于验证数据的损失和准确率值比训练数据所显示的结果变化更大。
-
还提供了下载任何图表或相关数据的选项。
可视化具有不同参数值的不同模型,在我们选择深度网络的架构类型、训练轮次、批次大小以及其他感兴趣的模型相关属性时非常有用。它还可以在需要时为我们提供进一步实验的方向,并帮助我们比较当前与过去的结果。
使用 LIME 进行深度网络模型可视化
在我们目前为止在本书中提供的应用示例中,开发分类或预测深度网络模型后,我们进行可视化以查看模型的整体表现。这些评估是使用训练数据和测试数据进行的。这种评估的主要目的是获得对模型表现的整体或全局理解。然而,有时我们希望获得更深入的理解,甚至是针对特定预测的解释。例如,我们可能会对理解哪些主要特征或变量影响了测试数据中的特定预测感兴趣。这样的“局部”解释是局部可解释模型无关解释(LIME)包的重点。LIME 可以帮助我们深入了解每个预测。
用于在 Keras 中进行 LIME 可视化的代码如下:
# LIME package
library(lime)
# Using LIME with keras
model_type.keras.engine.sequential.Sequential <-
function(x, ...) {"classification"}
predict_model.keras.engine.sequential.Sequential <-
function(x,newdata,type, ...) {p <- predict_proba(object=x, x=as.matrix(newdata))
data.frame(p)}
# Create explainer using lime
explainer <- lime(x = data.frame(training),
model = model,
bin_continuous = FALSE)
# Create explanation
explanation <- explain(data.frame(test)[1:5,],
explainer = explainer,
n_labels = 1,
n_features = 4,
kernel_width = 0.5)
testtarget[1:5]
[1] 0 0 0 2 2
如前面的代码所示,我们使用两个函数以便能在 Keras 模型中使用 LIME。在第一个函数中,我们指明将处理的是分类模型。第二个函数用于获取预测概率。在这一部分,我们将使用第二章中的model_one,深度神经网络与多分类问题。然后,我们将使用lime函数与训练数据、模型(即model_one),并指定连续变量的分箱为FALSE。生成的解释器将与explain函数一起使用,在这里我们将指定使用一个标签,并指定每个病例使用四个最重要的特征。我们将核宽度指定为 0.5。我们还可以看到,测试数据中的前三位患者被标记为 0 类,表示他们属于正常患者类别。同样,测试数据中的第 4 和第 5 位患者被标记为 2 类,表示他们属于病理患者类别。
我们通过plot_features(explanation)获得了以下图表:

上面的图表提供了测试数据中前五位患者的个别图表。以下是从这个图表中可以做出的一些观察:
-
所有五位患者都已被正确分类。
-
前三位患者被归类为 0 类,代表正常患者。
-
剩余的两位患者被归类为 2 类,代表病理患者。
-
前三例的预测概率为 0.97 或更高,而第 4 和第 5 位患者的预测概率为 0.72 或更高。
-
该图表显示了对每位患者具体分类起到关键作用的四个最重要特征。
-
具有蓝色条形的特征支持模型的结论,而具有红色条形的特征则与模型的结论相矛盾。
-
X8、X10 和 X20 变量的较高值似乎对患者被分类为病理性具有更大的影响。
-
X12 变量的较高值似乎影响患者被分类为正常。
以下热图可以通过plot_explanations(explanation)获得:

我们可以从之前的热图中做出以下观察:
-
热图使得比较每位患者的不同变量变得更容易,从而有助于理解。
-
它总结了病例、特征和标签组合的结果,并不像前面的图那样提供详细信息。
-
对于 X1 类,或标记为正常的患者(1、2 和 3),所有四个特征(X8、X10、X12 和 X20)具有非常相似的权重。
-
对于 X3 类,或标记为病理的患者(第 4 和第 5 位),所有四个特征(X8、X10、X13 和 X20)具有大致相似的权重。
使用tfruns可视化模型训练
当我们使用 Keras 运行深度网络模型时,可以使用tfruns来可视化损失和准确度图表,以及其他与模型相关的总结。尽管我们也可以在需要时获得图表和相关总结,但使用tfruns的主要优势在于我们可以将它们都集中在一个地方。我们可以使用以下代码来实现这一点:
library(tfruns)
training_run("mlp_ctg.R")
在前面的代码中,引用的R文件包含了从第二章运行model_one的代码,深度神经网络与多类分类。当我们运行代码时,mlp_ctg.R文件可能会存储在计算机中。代码运行后,以下交互式屏幕会自动显示:

在前述屏幕截图中显示的页面提供了以下内容:
-
训练和验证数据的损失值和准确度值的交互式图表
-
基于模型架构的模型总结
-
关于运行的信息,包括完成所有纪元所需的时间
-
基于训练和验证数据的准确度和损失值的数字总结
-
使用的样本、指定的纪元数以及批处理大小
提前停止网络训练
在训练网络时,我们事先指定所需的纪元数,而不知道实际需要多少纪元。如果我们指定的纪元数少于实际所需的纪元数,可能需要通过指定更多的纪元来重新训练网络。另一方面,如果我们指定的纪元数远超过实际需要的数目,则可能会导致过拟合情况,我们可能需要通过减少纪元数来重新训练网络。这种试错法对于每个纪元需要较长时间才能完成的应用来说可能非常耗时。在这种情况下,我们可以使用回调函数,帮助在合适的时机停止网络训练。
为了说明这个问题,让我们使用以下代码,基于第二章中的 CTG 数据,深度神经网络与多类分类,开发一个分类模型:
# Training network for classification with CTG data (chapter-2)
model <- keras_model_sequential()
model %>%
layer_dense(units = 25, activation = 'relu', input_shape = c(21)) %>%
layer_dense(units = 3, activation = 'softmax')
model %>% compile(loss = 'categorical_crossentropy',
optimizer = 'adam',
metrics = 'accuracy')
history <- model %>% fit(training,
trainLabels,
epochs = 50,
batch_size = 32,
validation_split = 0.2)
plot(history)
在前面的代码中,我们已将纪元数指定为 50。训练过程完成后,我们可以绘制训练和验证数据的损失值和准确度值,如下所示:

从前面的图表中,我们可以观察到以下内容:
-
我们可以观察到,验证数据的损失值最初在前几个纪元中下降,然后开始增加。
-
图表还显示,在前几个纪元后,训练和验证数据的损失值开始出现分歧,并趋向于相反的方向。
-
如果我们希望提前停止训练过程,而不是等待 50 个训练周期完成,我们可以使用 Keras 提供的回调功能。
以下代码在训练网络时包含了回调特性,位于 fit 函数中:
# Training network with callback
model <- keras_model_sequential()
model %>%
layer_dense(units = 25, activation = 'relu', input_shape = c(21)) %>%
layer_dense(units = 3, activation = 'softmax')
model %>% compile(loss = 'categorical_crossentropy',
optimizer = 'adam',
metrics = 'accuracy')
history <- model %>% fit(training,
trainLabels,
epochs = 50,
batch_size = 32,
validation_split = 0.2,
callbacks = callback_early_stopping(monitor = "val_loss",
patience = 10))
plot(history)
在之前的代码中,回调函数已包括在内,用于实现早期停止:
-
我们用于监控的度量标准是验证损失值。另一种可以尝试的度量标准是验证准确率,因为我们正在开发一个分类模型。
-
我们已将耐心值设定为 10,这意味着在 10 个训练周期没有改进时,训练过程将自动停止。
损失值和准确率的图表同样有助于我们决定合适的耐心值。以下是损失值和准确率的图表:

正如我们所看到的,这次训练过程并没有运行完所有 50 个训练周期,而是在损失值连续 10 个周期没有改进时停止了。
总结
使用深度学习网络开发分类和预测模型涉及大量实验,以获得高质量的性能模型。为了帮助这个过程,有多种方法非常有助于可视化和控制网络训练。在本章中,我们介绍了四种非常有用的方法。我们看到,TensorBoard 提供了一个工具,可以在训练网络时通过不同的架构和其他模型变化来评估和比较模型的性能。使用 TensorBoard 的优势在于它能够将所有必要的信息以用户友好的方式集中展示在一个地方。有时我们还希望了解在使用分类或预测模型时,特定预测中主要特征或变量是如何受到影响的。在这种情况下,我们可以使用 LIME 来可视化主要特征的影响。
本章中我们介绍的另一个有用技巧是通过 tfruns 实现可视化。在开发深度网络模型时,我们会遇到与特定模型相关的各种图表和摘要。使用 tfruns,我们可以借助互动界面将所有信息可视化地展示在一个地方。另一个在接下来的旅程中非常有用的技巧是使用回调函数,当开发出合适的分类或预测模型时,自动停止训练过程。本章中讨论的所有方法对于接下来的工作都非常有帮助,特别是在你处理复杂且具有挑战性的问题时。


浙公网安备 33010602011771号