深度学习研讨会-全-
深度学习研讨会(全)
原文:
annas-archive.org/md5/3c80ace5a5884b58b8fa9251691b3a28译者:飞龙
序言
关于本书
你是否对深度学习如何驱动智能应用程序感到着迷,例如自动驾驶汽车、虚拟助手、人脸识别设备和聊天机器人,用于处理数据并解决复杂问题?无论你是否熟悉机器学习,或是初学者,《深度学习工作坊》都将通过有趣的示例和练习,帮助你轻松理解深度学习。
本书首先强调了深度学习、机器学习和人工智能之间的关系,并通过实践练习帮助你熟悉 TensorFlow 2.0 的编程结构。你将了解神经网络、感知器的结构以及如何使用 TensorFlow 创建和训练模型。随后,本书将让你通过使用 Keras 进行图像识别练习,探索计算机视觉的基础知识。随着进展,你将能够通过实现文本嵌入和使用流行的深度学习解决方案进行数据排序,使你的模型更加强大。最后,你将掌握双向递归神经网络(RNNs),并构建生成对抗网络(GANs)用于图像合成。
在本书的最后,你将掌握使用 TensorFlow 和 Keras 构建深度学习模型所需的关键技能。
受众
如果你对机器学习感兴趣,并希望使用 TensorFlow 和 Keras 创建和训练深度学习模型,这个工作坊非常适合你。掌握 Python 及其包,并具备基本的机器学习概念,将帮助你快速学习这些主题。
章节概览
第一章,深度学习的构建块,讨论了深度学习的实际应用。一个这样的应用包括一个可以立即运行的动手代码示例,用于识别互联网上的图像。通过实践练习,你还将学习到 TensorFlow 2.0 的关键代码实现,这将帮助你在接下来的章节中构建令人兴奋的神经网络模型。
第二章,神经网络,教你人工神经网络的结构。通过使用 TensorFlow 2.0,你不仅会实现一个神经网络,还会训练它。你将建立多个不同配置的深度神经网络,从而亲身体验神经网络的训练过程。
第三章,卷积神经网络(CNNs)与图像分类,涵盖了图像处理、其工作原理以及如何将这些知识应用于卷积神经网络(CNNs)。通过实践练习,你将创建和训练 CNN 模型,用于识别手写数字甚至水果的图像。你还将学习一些关键概念,如池化层、数据增强和迁移学习。
第四章,文本的深度学习 - 嵌入层,带你进入自然语言处理的世界。你将首先进行文本预处理,这是处理原始文本数据时的一项重要技能。你将实现经典的文本表示方法,如独热编码和 TF-IDF 方法。在本章的后续部分,你将学习嵌入层,并使用 Skip-gram 和连续词袋算法生成你自己的词嵌入。
第五章,序列的深度学习,展示了如何处理一个经典的序列处理任务——股票价格预测。你将首先创建一个基于递归神经网络(RNN)的模型,然后实现一个基于 1D 卷积的模型,并将其与该 RNN 模型的表现进行比较。你将通过结合 RNN 和 1D 卷积,创建一个混合模型。
第六章,LSTM,GRU 和高级 RNN,回顾了 RNN 的实际缺点,以及长短期记忆(LSTM)模型如何帮助克服这些问题。你将构建一个分析电影评论情感的模型,并研究门控循环单元(GRU)的内部工作原理。在本章中,你将创建基于普通 RNN、LSTM 和 GRU 的模型,并在章末比较它们的表现。
第七章,生成对抗网络,介绍了生成对抗网络(GANs)及其基本组件。通过实践练习,你将使用 GANs 生成一个模拟由正弦函数生成的数据分布。你还将了解深度卷积 GANs,并在练习中实现它们。章节的后期,你将创建能够以令人信服的准确度复制图像的 GANs。
约定
文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名的显示方式如下:
"使用 mnist.load_data() 加载 MNIST 数据集"
屏幕上看到的词汇(例如,在菜单或对话框中)以相同的格式显示。
一块代码设置如下:
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
train_scaled = scaler.fit_transform(train_data)
test_scaled = scaler.transform(test_data)
新术语和重要单词的显示方式如下:
预处理的第一步不可避免是分词—将原始输入文本序列分割为词元。长代码片段会被截断,GitHub 上相应的代码文件名称会被放置在截断代码的顶部。指向完整代码的永久链接会被放置在代码片段下方。应如下所示:
Exercise7.04.ipynb
# Function to generate real samples
def realData(loc,batch):
"""
loc is the random location or mean
around which samples are centered
"""
# Generate numbers to right of the random point
xr = np.arange(loc,loc+(0.1*batch/2),0.1)
xr = xr[0:int(batch/2)]
# Generate numbers to left of the random point
xl = np.arange(loc-(0.1*batch/2),loc,0.1)
The complete code for this step can be found at https://packt.live/3iIJHVS.
代码展示
跨多行的代码通过反斜杠(\)分割。当代码执行时,Python 会忽略反斜杠,并将下一行的代码视为当前行的直接延续。
例如:
history = model.fit(X, y, epochs=100, batch_size=5, verbose=1, \
validation_split=0.2, shuffle=False)
在代码中加入注释,帮助解释特定的逻辑。单行注释使用 # 符号表示,如下所示:
# Print the sizes of the dataset
print("Number of Examples in the Dataset = ", X.shape[0])
print("Number of Features for each example = ", X.shape[1])
多行注释使用三引号括起来,如下所示:
"""
Define a seed for the random number generator to ensure the
result will be reproducible
"""
seed = 1
np.random.seed(seed)
random.set_seed(seed)
设置你的环境
在我们详细了解本书内容之前,我们需要设置一些特定的软件和工具。在接下来的部分中,我们将展示如何完成这些设置。
硬件要求
为了获得最佳的用户体验,我们推荐至少 8 GB 的内存。
在你的系统上安装 Anaconda
本书中的所有练习和活动将在 Jupyter Notebooks 中执行。要在 Windows、macOS 或 Linux 上安装 Jupyter,我们首先需要安装 Anaconda。安装 Anaconda 还将安装 Python。
-
前往
www.anaconda.com/distribution/安装 Anaconda Navigator,它是你可以访问本地 Jupyter Notebook 的界面。 -
现在,根据你的操作系统(Windows、macOS 或 Linux),你需要下载 Anaconda 安装程序。首先选择你的操作系统,然后选择 Python 版本。本书推荐使用最新版本的 Python。
![图 0.1:Anaconda 主屏幕]()
图 0.1:Anaconda 主屏幕
-
要检查 Anaconda Navigator 是否正确安装,请在应用程序中查找
Anaconda Navigator。查找下面显示的图标。然而,请注意,图标的外观可能会根据你的操作系统有所不同。![图 0.2 Anaconda Navigator 图标]()
图 0.2 Anaconda Navigator 图标
-
点击图标打开 Anaconda Navigator。首次加载可能需要一些时间,但安装成功后,你应该会看到一个类似的界面:

图 0.3 Anaconda Navigator 图标
启动 Jupyter Notebook
要通过 Anaconda Navigator 启动 Jupyter Notebook,请按照以下步骤操作:
-
打开 Anaconda Navigator。你应该会看到如下屏幕:
![图 0.4:Anaconda 安装屏幕]()
图 0.4:Anaconda 安装屏幕
-
现在,点击
Jupyter Notebook面板下的Launch按钮以启动本地系统上的 notebook:

图 0.5:Jupyter Notebook 启动选项
你已经成功将 Jupyter Notebook 安装到你的系统中。你也可以通过在终端或 Anaconda 提示符中运行命令jupyter notebook来打开 Jupyter Notebook。
安装库
pip 会随 Anaconda 一起预安装。一旦 Anaconda 安装在你的计算机上,所有必需的库可以通过 pip 安装,例如 pip install numpy。或者,你也可以使用 pip install –r requirements.txt 安装所有必需的库。你可以在 packt.live/303E4dD 找到 requirements.txt 文件。
练习和活动将在 Jupyter Notebooks 中执行。Jupyter 是一个 Python 库,可以像其他 Python 库一样安装——也就是使用 pip install jupyter,但幸运的是,它已经随 Anaconda 一起预安装。要打开一个 notebook,只需在终端或命令提示符中运行 jupyter notebook 命令。
安装 TensorFlow 2.0
在安装 TensorFlow 2.0 之前,请确保你已经在系统中安装了最新版本的 pip。你可以通过以下命令检查:
pip --version
要安装 TensorFlow 2.0,你系统中的 pip 版本必须大于 19.0。你可以在 Windows、Linux 或 macOS 上使用以下命令升级 pip 版本:
pip install --upgrade pip
升级后,使用以下命令在 Windows、Linux 或 macOS 上安装 TensorFlow:
pip install --upgrade tensorflow
在 Linux 和 macOS 上,如果需要提升权限,请使用以下命令:
sudo pip install --upgrade tensorflow
注意
TensorFlow 不支持 Windows 上的 Python 2.7。
安装 Keras
要在 Windows、macOS 或 Linux 上安装 Keras,请使用以下命令:
pip install keras
在 Linux 和 macOS 上,如果需要提升权限,请使用以下命令:
sudo pip install keras
访问代码文件
你可以在 packt.live/3edmwj4 找到本书的完整代码文件。你还可以通过使用位于 packt.live/2CGCWUz 的互动实验环境,在浏览器中直接运行许多活动和练习。
我们尽力支持所有活动和练习的交互式版本,但我们也推荐进行本地安装,以防该支持不可用的情况。
注意
本书包含某些从 CSV 文件读取数据的代码片段。假设这些 CSV 文件与 Jupyter Notebook 存储在同一文件夹中。如果你将它们存储在其他位置,你需要修改路径。
如果你在安装过程中遇到任何问题或有疑问,请通过电子邮件联系我们:workshops@packt.com。
第一章:1. 深度学习的构建模块
介绍
在本章中,你将了解深度学习及其与人工智能和机器学习的关系。我们还将学习一些重要的深度学习架构,如多层感知器、卷积神经网络、递归神经网络和生成对抗网络。随着我们深入学习,你将通过实践体验 TensorFlow 框架,并使用它来实现一些线性代数操作。最后,我们将了解优化器的概念。通过利用它们来解决二次方程式,我们将理解优化器在深度学习中的作用。到本章结束时,你将对深度学习的概念和如何使用 TensorFlow 进行编程有一个清晰的了解。
介绍
你刚刚从每年的假期回来。作为一个活跃的社交媒体用户,你忙着将照片上传到你最喜欢的社交媒体应用。当照片上传后,你注意到应用会自动识别你的面部并几乎瞬间标记你。事实上,它甚至在群体照片中也能做到这一点。即使在一些光线较差的照片中,你也注意到应用大多数时候能正确标记你。那应用程序是如何学习做这些事情的呢?
要在照片中识别一个人,应用程序需要准确的信息,如此人的面部结构、骨骼结构、眼睛颜色以及许多其他细节。但当你使用这个照片应用程序时,你并不需要将所有这些细节明确地提供给应用程序。你所做的只是上传照片,应用程序就会自动开始识别你。那应用程序是如何知道这些细节的呢?
当你第一次将照片上传到应用时,应用程序会要求你标记自己。当你手动标记自己时,应用程序会自动“学习”关于你面部的所有信息。然后,每次你上传照片时,应用程序就会利用它学到的信息来识别你。当你在应用错误标记你时,手动标记自己能够帮助它改进。
该应用程序能够在最小化人工干预的情况下学习新细节并自我改进,这得益于深度学习(DL)的强大功能。深度学习是人工智能(AI)的一部分,通过识别标记数据中的模式帮助机器学习。但等一下,这不就是机器学习(ML)的功能吗?那么,深度学习和机器学习之间有什么区别呢?人工智能、机器学习和深度学习等领域之间的交集点是什么?让我们快速了解一下。
人工智能、机器学习与深度学习
人工智能是计算机科学的一个分支,旨在开发能够模拟人类智能的机器。人类智能可以简化为基于来自我们五感——视力、听力、触觉、嗅觉和味觉——的输入来做出决策。AI 并不是一个新领域,自 1950 年代以来就已有发展。此后,这个领域经历了多次高潮与低谷。进入 21 世纪,随着计算能力的飞跃、数据的丰富和对理论基础的更好理解,AI 迎来了复兴。机器学习和深度学习是 AI 的子领域,并且越来越多地被交替使用。
下图展示了 AI、ML 和 DL 之间的关系:

图 1.1:AI、ML 和 DL 之间的关系
机器学习
机器学习是 AI 的一个子集,通过识别数据中的模式并提取推论来执行特定任务。从数据中得出的推论随后用于预测未知数据的结果。机器学习与传统计算机编程在解决特定任务的方法上有所不同。在传统的计算机编程中,我们编写并执行特定的业务规则和启发式算法来获得期望的结果。然而,在机器学习中,这些规则和启发式算法并没有被明确编写。这些规则和启发式算法是通过提供数据集进行学习的。用于学习这些规则和启发式算法的数据集称为训练数据集。整个学习和推断的过程称为训练。
学习规则和启发式算法是通过使用不同的算法来完成的,这些算法采用统计模型来实现这一目的。这些算法利用多种数据表示方式进行学习。每种数据的表示方式称为示例。示例中的每个元素称为特征。以下是著名的 IRIS 数据集的一个示例(archive.ics.uci.edu/ml/datasets/Iris)。该数据集表示了不同种类的鸢尾花,基于不同的特征,如萼片和花瓣的长度与宽度:

图 1.2:IRIS 数据集的样本数据
在前面的数据集中,每一行数据代表一个例子,每一列是一个特征。机器学习算法利用这些特征从数据中推断出结论。模型的准确性,以及预测结果的可靠性,很大程度上依赖于数据的特征。如果提供给机器学习算法的特征能够很好地代表问题陈述,那么得到好结果的机会就会很高。一些常见的机器学习算法包括线性回归、逻辑回归、支持向量机、随机森林和XGBoost。
尽管传统的机器学习算法在许多应用场景中都很有用,但它们在获得优异结果时,非常依赖于特征的质量。特征的创建是一门耗时的艺术,且需要大量的领域知识。然而,即便拥有全面的领域知识,仍然存在将这些知识转化为特征的局限性,进而无法很好地封装数据生成过程中的细微差别。此外,随着机器学习所解决问题的复杂性增加,特别是非结构化数据(如图像、语音、文本等)的出现,几乎不可能创建能够表示复杂函数的特征,这些复杂函数反过来又生成数据。因此,往往需要找到一种不同的方法来解决复杂问题,这时深度学习就派上了用场。
深度学习
深度学习是机器学习的一个子集,是一种称为人工神经网络(ANN)的算法的扩展。神经网络并不是一种新现象。神经网络的创建可以追溯到 20 世纪 40 年代的上半期。神经网络的开发灵感来自于对人类大脑运作方式的了解。从那时起,这一领域经历了几次高潮和低谷。一个重新激发人们对神经网络兴趣的关键时刻是由该领域的巨头们,如 Geoffrey Hinton,提出的反向传播算法。正因为如此,Hinton 被广泛认为是“深度学习的教父”。我们将在第二章《神经网络》中深入讨论神经网络。
多层(深层)人工神经网络(ANNs)是深度学习的核心。深度学习模型的一个显著特点是其能够从输入数据中学习特征。与传统的机器学习不同,后者需要手动创建特征,深度学习擅长从多个层次学习不同的特征层级。例如,假设我们使用一个深度学习模型来检测人脸。模型的初始层会学习面部的低级近似特征,如面部的边缘,如图 1.3所示。每个后续层会将前一层的特征组合起来,形成更复杂的特征。在人脸检测的例子中,如果初始层学会了检测边缘,后续层将这些边缘组合起来,形成面部的一部分,如鼻子或眼睛。这个过程在每一层继续进行,直到最后一层生成一个完整的人脸图像:

图 1.3:用于检测人脸的深度学习模型
注意
上述图片来自于一篇流行的研究论文:Lee, Honglak & Grosse, Roger & Ranganath, Rajesh & Ng, Andrew. (2011). 无监督学习层次表示与卷积深度置信网络. Commun. ACM. 54. 95-103. 10.1145/2001269.2001295.
深度学习技术在过去十年中取得了巨大的进步。多个因素促使了深度学习技术的指数增长,其中最重要的因素是大量数据的可用性。数字时代,随着越来越多设备的互联,产生了大量数据,特别是非结构化数据。这反过来促进了深度学习技术的大规模应用,因为它们非常适合处理大量的非结构化数据。
深度学习崛起的另一个重要因素是计算基础设施的进步。深度学习模型通常包含大量层次和数百万个参数,因此需要强大的计算能力。图形处理单元(GPU)和张量处理单元(TPU)等计算层次的进步,以合理的成本提供了强大的计算能力,从而推动了深度学习的广泛应用。
深度学习的普及还得益于不同框架的开源,这些框架用于构建和实现深度学习模型。2015 年,Google Brain 团队开源了 TensorFlow 框架,自那时以来,TensorFlow 已成长为最受欢迎的深度学习框架之一。其他主要的框架包括 PyTorch、MXNet 和 Caffe。本书将使用 TensorFlow 框架。
在我们深入探讨深度学习的构建块之前,让我们通过一个简短的演示来实际体验深度学习模型的强大功能。你不需要了解演示中的所有代码。只需按照指示操作,你就能快速了解深度学习的基本能力。
使用深度学习分类图像
在接下来的练习中,我们将分类一个披萨的图像,并将分类结果的文本转换为语音。为了对图像进行分类,我们将使用一个预训练的模型。文本转语音将使用一个免费提供的 API——Google 文本转语音(gTTS)来完成。在开始之前,让我们先了解一些这个演示的关键构建块。
预训练模型
训练一个深度学习模型需要大量的计算资源和时间,并且需要庞大的数据集。然而,为了促进研究和学习,深度学习社区也提供了在大数据集上训练好的模型。这些预训练模型可以下载并用于预测,或者用于进一步训练。在本次演示中,我们将使用一个名为ResNet50的预训练模型。这个模型与 Keras 包一起提供。这个预训练模型能够预测我们日常生活中遇到的 1,000 种不同类型的物体,比如鸟类、动物、汽车等。
Google 文本转语音 API
Google 已经将其文本转语音算法开放供有限使用。我们将使用这个算法将预测的文本转换为语音。
演示所需的先决条件包
为了让这个演示正常工作,你需要在机器上安装以下包:
-
TensorFlow 2.0
-
Keras
-
gTTS
请参考前言以了解安装前两个包的过程。安装 gTTS 将在练习中展示。接下来,让我们深入了解演示。
练习 1.01:图像和语音识别演示
在本次练习中,我们将演示使用深度学习模型进行图像识别和语音转文本的转换。此时,你可能无法理解代码中的每一行,这将在后续讲解中解释。现在,只需执行代码,了解使用 TensorFlow 构建深度学习和人工智能应用程序有多么简单。按照以下步骤完成本次练习:
-
打开一个 Jupyter Notebook 并命名为练习 1.01。关于如何启动 Jupyter Notebook 的详细信息,请参阅前言。
-
导入所有必需的库:
from tensorflow.keras.preprocessing.image import load_img from tensorflow.keras.preprocessing.image import img_to_array from tensorflow.keras.applications.resnet50 import ResNet50 from tensorflow.keras.preprocessing import image from tensorflow.keras.applications.resnet50 \ import preprocess_input from tensorflow.keras.applications.resnet50 \ import decode_predictions \ ) to split the logic across multiple lines. When the code is executed, Python will ignore the backslash, and treat the code on the next line as a direct continuation of the current line.这里简要描述我们将要导入的包:
load_img:将图像加载到 Jupyter Notebook 中img_to_array:将图像转换为 NumPy 数组,这是 Keras 所需的格式preprocess_input:将输入转换为模型可以接受的格式decode_predictions:将模型预测的数值输出转换为文本标签Resnet50:这是一个预训练的图像分类模型 -
创建一个预训练的
Resnet模型实例:mymodel = ResNet50()下载过程中你应收到类似以下的消息:
![图 1.4:加载 Resnet50]()
图 1.4:加载 Resnet50
Resnet50是一个预训练的图像分类模型。对于首次使用者,下载模型到你的环境中需要一些时间。 -
从互联网上下载一张披萨的图片,并将其保存在运行 Jupyter Notebook 的同一文件夹中。将图片命名为
im1.jpg。注意
你还可以通过此链接下载我们使用的图片:
packt.live/2AHTAC9 -
使用以下命令加载待分类的图片:
myimage = load_img('im1.jpg', target_size=(224, 224))如果你将图片保存在另一个文件夹中,则必须提供图片所在位置的完整路径,代替
im1.jpg命令。例如,如果图片保存在D:/projects/demo中,代码应如下所示:myimage = load_img('D:/projects/demo/im1.jpg', \ target_size=(224, 224)) -
我们通过以下命令来显示图片:
myimage上述命令的输出将如下所示:
![图 1.5:加载图片后显示的输出]()
图 1.5:加载图片后显示的输出
-
将图片转换为
numpy数组,因为模型期望它是这种格式:myimage = img_to_array(myimage) -
将图片调整为四维格式,因为这是模型期望的格式:
myimage = myimage.reshape((1, 224, 224, 3)) -
通过运行
preprocess_input()函数准备图片以供提交:myimage = preprocess_input(myimage) -
运行预测:
myresult = mymodel.predict(myimage) -
预测结果是一个数字,需要将其转换为相应的文本格式标签:
mylabel = decode_predictions(myresult) -
接下来,键入以下代码以显示标签:
mylabel = mylabel[0][0] -
使用以下代码打印标签:
print("This is a : " + mylabel[1])如果到目前为止你已正确按照步骤操作,输出结果将如下所示:
This is a : pizza模型已成功识别我们的图片。很有趣,不是吗?接下来的几个步骤,我们将进一步处理,将这个结果转化为语音。
小贴士
虽然我们在这里使用了一张披萨的图片,但你可以使用任何图片来进行模型测试。我们建议你多次尝试使用不同的图片进行此练习。
-
准备要转换为语音的文本:
sayit="This is a "+mylabel[1] -
安装
gtts包,该包用于将文本转换为语音。可以在 Jupyter Notebook 中按如下方式实现:!pip install gtts -
导入所需的库:
from gtts import gTTS import os上述代码将导入两个库。一个是
gTTS,即 Google 文本转语音服务,这是一个基于云的开源 API,用于将文本转换为语音。另一个是os库,用于播放生成的音频文件。 -
调用
gTTSAPI 并将文本作为参数传递:myobj = gTTS(text=sayit)注意
运行上述步骤时,你需要保持在线状态。
-
保存生成的音频文件。该文件将保存在运行 Jupyter Notebook 的主目录中。
myobj.save("prediction.mp3")注意
你还可以通过在文件名之前指定绝对路径来设置保存位置;例如,
(myobj.save('D:/projects/prediction.mp3')。 -
播放音频文件:
os.system("prediction.mp3")如果你正确地遵循了前面的步骤,你将听到
This is a pizza的语音。注
要访问此特定部分的源代码,请参考
packt.live/2ZPZx8B。你也可以在
packt.live/326cRIu在线运行这个示例。你必须执行整个笔记本才能获得预期的结果。
在这个练习中,我们学习了如何通过使用公共可用的模型并用几行代码在 TensorFlow 中构建深度学习模型。现在你已经体验了深度学习,让我们继续前进,了解深度学习的不同构建块。
深度学习模型
大多数流行的深度学习模型的核心是人工神经网络(ANN),其灵感来源于我们对大脑工作原理的认识。虽然没有任何单一模型可以称为完美,但不同的模型在不同的场景下表现更好。在接下来的章节中,我们将了解一些最突出的模型。
多层感知器
多层感知器(MLP)是一种基本的神经网络类型。MLP 也被称为前馈网络。以下图所示可以看到 MLP 的表示:

图 1.6:MLP 表示
多层感知器(MLP,或任何神经网络)的基本构建块之一是神经元。一个网络由多个神经元连接到后续的层。非常基础的 MLP 由输入层、隐藏层和输出层组成。输入层的神经元数量与输入数据相等。每个输入神经元将与隐藏层的所有神经元相连接。最终的隐藏层将与输出层连接。MLP 是一个非常有用的模型,可以尝试应用于各种分类和回归问题。MLP 的概念将在第二章,神经网络中详细介绍。
卷积神经网络
卷积神经网络(CNN)是一类深度学习模型,主要用于图像识别。当我们讨论 MLP 时,我们看到每一层的神经元都与后续层的每个神经元相连接。然而,CNN 采用了不同的方法,并没有使用这种完全连接的架构。相反,CNN 从图像中提取局部特征,然后将这些特征传递到后续层。
CNN 在 2012 年崭露头角,当时名为 AlexNet 的架构在一个名为ImageNet 大规模视觉识别挑战赛(ILSVRC)的顶级竞赛中获胜。ILSVRC 是一个大规模计算机视觉竞赛,全球各地的团队竞相争夺最佳计算机视觉模型的奖项。在 2012 年的研究论文《ImageNet 分类与深度卷积神经网络》(papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks)中,Alex Krizhevsky 等人(多伦多大学)展示了 CNN 架构的真正强大力量,最终赢得了 2012 年 ILSVRC 挑战赛。下图展示了AlexNet模型的结构,这是一个 CNN 模型,其卓越的性能使得 CNN 在深度学习领域声名鹊起。尽管这个模型的结构看起来可能对你来说比较复杂,但在第三章《卷积神经网络图像分类》中,这种 CNN 网络的工作原理会被详细讲解:

图 1.7:AlexNet 模型的 CNN 架构
注意
上述图表来源于著名的研究论文:Krizhevsky, Alex & Sutskever, Ilya & Hinton, Geoffrey. (2012). ImageNet 分类与深度卷积神经网络。神经信息处理系统。25. 10.1145/3065386。
自 2012 年以来,许多突破性的 CNN 架构扩展了计算机视觉的可能性。一些著名的架构有 ZFNet、Inception(GoogLeNet)、VGG 和 ResNet。
CNN 应用最为显著的一些用例如下:
-
图像识别和光学字符识别(OCR)
-
社交媒体上的人脸识别
-
文本分类
-
自动驾驶汽车的物体检测
-
医疗健康领域的图像分析
使用深度学习的另一个巨大好处是,你不必总是从零开始构建模型——你可以使用他人已经构建的模型,并将其用于自己的应用。这就是所谓的“迁移学习”,它使你能够从活跃的深度学习社区中受益。
我们将在第三章《卷积神经网络图像分类》中应用迁移学习于图像处理,并详细了解 CNN 及其动态。
循环神经网络
在传统的神经网络中,输入与输出是相互独立的。然而,在语言翻译等场景中,单词前后存在依赖关系,因此需要理解单词出现顺序的动态特性。这个问题通过一种被称为循环神经网络(RNNs)的网络类别得到了解决。RNNs 是一类深度学习网络,其中前一步的输出作为当前步骤的输入。RNN 的一个显著特点是隐藏层,它能够记住序列中其他输入的信息。以下图可以看到 RNN 的高级表示。你将在第五章,深度学习与序列中深入了解这些网络的内部工作原理:

图 1.8:RNN 的结构
RNN 架构有不同的类型。其中一些最著名的类型是长短时记忆网络(LSTM)和门控循环单元(GRU)。
RNN 的一些重要应用案例如下:
-
语言建模与文本生成
-
机器翻译
-
语音识别
-
生成图像描述
RNN 将在第五章,深度学习与序列和第六章,LSTMs、GRUs 及高级 RNN中详细讲解。
生成对抗网络
生成对抗网络(GANs)是一种能够生成与任何真实数据分布相似的数据分布的网络。深度学习的先驱之一 Yann LeCun 曾表示,GANs 是过去十年中深度学习领域最具前景的想法之一。
举个例子,假设我们想从随机噪声数据生成狗的图像。为此,我们训练一个 GAN 网络,使用真实的狗的图像和噪声数据,直到我们生成的图像看起来像真实的狗的图像。以下图解释了 GAN 的基本概念。在这个阶段,你可能还不完全理解这个概念。它将在第七章,生成对抗网络中详细讲解。

图 1.9:GANs 的结构
注意
上述图表来源于一篇流行的研究论文:Barrios, Buldain, Comech, Gilbert & Orue (2019)。利用深度学习方法进行局部放电分类——最近进展综述(doi.org/10.3390/en12132485)。
GANs 是一个重要的研究领域,并且有许多应用案例。以下是一些 GANs 的有用应用:
-
图像翻译
-
文本到图像合成
-
生成视频
-
艺术修复
GANs 将在第七章,生成对抗网络中详细讲解。
深度学习的可能性和前景是巨大的。深度学习应用已无处不在,成为我们日常生活的一部分。以下是一些显著的例子:
-
聊天机器人
-
机器人
-
智能音响(例如 Alexa)
-
虚拟助手
-
推荐引擎
-
无人机
-
自动驾驶汽车或自动化车辆
这种不断扩展的可能性画布使它成为数据科学家工具箱中的一个重要工具。本书将逐步引导你进入深度学习的奇妙世界,并使你能够将其应用于现实世界的场景。
TensorFlow 简介
TensorFlow 是由 Google 开发的深度学习库。在撰写本书时,TensorFlow 是迄今为止最流行的深度学习库。最初,它由 Google 内部的一个团队——Google Brain 团队开发,用于内部使用,并于 2015 年开源。Google Brain 团队开发了像 Google Photos 和 Google Cloud Speech-to-Text 这样的流行应用,这些应用基于 TensorFlow,属于深度学习应用。TensorFlow 1.0 于 2017 年发布,并在短时间内超越了其他现有的库,如 Caffe、Theano 和 PyTorch,成为最受欢迎的深度学习库。它被认为是行业标准,几乎每个从事深度学习的组织都在使用它。TensorFlow 的一些关键特点如下:
-
它可以与所有常见的编程语言一起使用,如 Python、Java 和 R。
-
它可以部署在多个平台上,包括 Android 和 Raspberry Pi。
-
它可以以高度分布的模式运行,因此具有高度的可扩展性。
在经历了长时间的 Alpha/Beta 发布后,TensorFlow 2.0 的最终版本于 2019 年 9 月 30 日发布。TF2.0 的重点是使深度学习应用的开发更加简便。接下来我们将一起了解 TensorFlow 2.0 框架的基础知识。
张量
在 TensorFlow 程序中,每个数据元素都叫做张量。张量是向量和矩阵在更高维度下的表示。张量的秩表示其维度。以下是一些常见的数据形式,以张量的形式表示:
标量
标量是秩为 0 的张量,它只有大小。
例如,[ 12 ] 是一个大小为 12 的标量。
向量
向量是秩为 1 的张量。
例如,[ 10 , 11, 12, 13]。
矩阵
矩阵是秩为 2 的张量。
例如,[ [10,11] , [12,13] ]。这个张量有两行两列。
秩为 3 的张量
这是一个三维张量。例如,图像数据通常是一个三维张量,具有宽度、高度和通道数作为其三个维度。以下是一个三维张量的例子,即它有两行、三列和三个通道:

图 1.10:三维张量
张量的形状由一个数组表示,表示每个维度中的元素个数。例如,如果一个张量的形状是 [2,3,5],这意味着该张量有三个维度。如果这是图像数据,则此形状表示该张量有两行、三列和五个通道。我们还可以从形状中获取秩。在这个例子中,张量的秩是三,因为有三个维度。下面的图示进一步说明了这一点:

图 1.11:张量的秩和形状示例
常量
常量用于存储在程序执行过程中不会被改变或修改的值。创建常量有多种方式,最简单的一种如下:
a = tf.constant (10)
这将创建一个初始化为 10 的张量。请记住,常量的值不能通过重新赋值来更新或修改。另一个示例如下:
s = tf.constant("Hello")
在这一行中,我们正在将一个字符串实例化为常量。
变量
变量用于存储在程序执行过程中可以更新和修改的数据。我们将在第二章,神经网络中详细讨论这一点。创建变量有多种方式,最简单的一种如下:
b=tf.Variable(20)
在前面的代码中,变量b被初始化为20。请注意,在 TensorFlow 中,与常量不同,Variable 这个术语的首字母是大写的。
变量可以在程序执行过程中重新赋予不同的值。变量可以用于赋值任何类型的对象,包括标量、向量和多维数组。以下是如何在 TensorFlow 中创建一个维度为 3 x 3 的数组的示例:
C = tf.Variable([[1,2,3],[4,5,6],[7,8,9]])
这个变量可以初始化为一个 3 x 3 的矩阵,如下所示:

图 1.12:3 x 3 矩阵
现在我们已经了解了 TensorFlow 的一些基本概念,接下来让我们学习如何将它们付诸实践。
在 TensorFlow 中定义函数
在 Python 中可以使用以下语法创建函数:
def myfunc(x,y,c):
Z=x*x*y+y+c
return Z
使用特殊运算符def来初始化一个函数,接着是函数的名称myfunc,以及函数的参数。在前面的示例中,函数体位于第二行,最后一行返回输出。
在接下来的练习中,我们将学习如何使用之前定义的变量和常量来实现一个简单的函数。
练习 1.02:实现一个数学方程
在本练习中,我们将使用 TensorFlow 求解以下数学方程:

图 1.13:使用 TensorFlow 求解的数学方程
我们将使用 TensorFlow 来求解它,如下所示:
X=3
Y=4
虽然有多种方法可以实现这一点,但在本练习中我们只会探索其中的一种方法。按照以下步骤完成此练习:
-
打开一个新的 Jupyter Notebook,并将其重命名为Exercise 1.02。
-
使用以下命令导入 TensorFlow 库:
import tensorflow as tf -
现在,让我们解这个方程。为此,你需要创建两个变量,
X和Y,并分别将它们初始化为给定的值3和4:X=tf.Variable(3) Y=tf.Variable(4) -
在我们的方程中,
2的值没有变化,因此我们将它作为常量存储,代码如下:C=tf.constant(2) -
定义一个函数来解决我们的方程:
def myfunc(x,y,c): Z=x*x*y+y+c return Z -
通过传递
X,Y和C作为参数来调用该函数。我们将把该函数的输出存储在一个名为result的变量中:result=myfunc(X,Y,C) -
使用
tf.print()函数打印结果:tf.print(result)输出结果如下:
42注意
若要访问该部分的源代码,请参阅
packt.live/2ClXKjj。你也可以在
packt.live/2ZOIN1C上运行这个示例。你必须执行整个 Notebook 才能得到预期的结果。
在这个练习中,我们学习了如何定义和使用一个函数。熟悉 Python 编程的人会注意到,这与正常的 Python 代码没有太大区别。
在本章的其余部分,我们将通过学习一些基本的线性代数,并熟悉一些常见的向量运算,为下一章的神经网络做准备,这样理解神经网络就会更加容易。
使用 TensorFlow 进行线性代数
在神经网络中使用的最重要的线性代数主题是矩阵乘法。在这一节中,我们将解释矩阵乘法的原理,并使用 TensorFlow 的内置函数解决一些矩阵乘法的示例。这对下一章神经网络的准备工作至关重要。
矩阵乘法是如何工作的?你可能在高中时学过这个内容,但让我们快速回顾一下。
假设我们需要执行两个矩阵 A 和 B 之间的矩阵乘法,其中我们有以下内容:

图 1.14:矩阵 A

图 1.15:矩阵 B
第一步是检查一个 2x3 的矩阵乘以一个 3x2 的矩阵是否可能。矩阵乘法有一个前提条件。记住 C=R,即第一个矩阵的列数(C)应当等于第二个矩阵的行数(R)。并且要记住顺序很重要,这也是为什么 A x B 不等于 B x A。在这个例子中,C=3,R=3。所以,乘法是可能的。
结果矩阵的行数将与 A 相同,列数将与 B 相同。因此,在这种情况下,结果将是一个 2x2 的矩阵。
要开始乘法运算,取 A 的第一行(R1)和 B 的第一列(C1)的元素:

图 1.16:矩阵 A(R1)

图 1.17:矩阵 B(C1)
获取按元素乘积的和,即 (1 x 7) + (2 x 9) + (3 x 11) = 58。这个将是结果 2 x 2 矩阵中的第一个元素。我们暂时称这个为不完整矩阵 D(i):

图 1.18:不完整矩阵 D(i)
重复此操作,使用 A 的第一行(R1)和 B 的第二列(C2):

图 1.19:矩阵 A 的第一行

图 1.20:矩阵 B 的第二列
获取对应元素的乘积之和,即 (1 x 8) + (2 x 10) + (3 x 12) = 64。这个将是结果矩阵中的第二个元素:

图 1.21:矩阵 D(i) 的第二个元素
使用第二行重复相同的操作,以得到最终结果:

图 1.22:矩阵 D
相同的矩阵乘法可以通过 TensorFlow 中的内置方法 tf.matmul() 来执行。需要相乘的矩阵必须作为变量传递给模型,如下例所示:
C = tf.matmul(A,B)
在前面的例子中,A 和 B 是我们要进行乘法运算的矩阵。我们通过使用 TensorFlow 来练习这个方法,进行我们手动计算过的两个矩阵的乘法。
练习 1.03:使用 TensorFlow 进行矩阵乘法
在这个练习中,我们将使用 tf.matmul() 方法通过 tensorflow 进行两个矩阵的乘法运算。按照以下步骤完成此练习:
-
打开一个新的 Jupyter Notebook,并将其重命名为 Exercise 1.03。
-
导入
tensorflow库并创建两个变量X和Y,它们是矩阵。X是一个 2 x 3 的矩阵,Y是一个 3 x 2 的矩阵:import tensorflow as tf X=tf.Variable([[1,2,3],[4,5,6]]) Y=tf.Variable([[7,8],[9,10],[11,12]]) -
打印并显示
X和Y的值,确保矩阵正确创建。我们首先打印X的值:tf.print(X)输出结果如下:
[[1 2 3] [4 5 6]]现在,让我们打印
Y的值:tf.print(Y)输出结果如下:
[[7 8] [9 10] [11 12]] -
通过调用 TensorFlow 的
tf.matmul()函数执行矩阵乘法:c1=tf.matmul(X,Y)为了显示结果,打印
c1的值:tf.print(c1)输出结果如下:
[[58 64] [139 154]] -
让我们通过改变矩阵的顺序来执行矩阵乘法:
c2=tf.matmul(Y,X)为了显示结果,让我们打印
c2的值:tf.print(c2)结果输出如下:
[[39 54 69] [49 68 87] [59 82 105]]请注意,由于我们改变了顺序,结果不同。
注意
要访问该特定部分的源代码,请参考
packt.live/3eevyw4。你还可以在线运行这个示例,网址为
packt.live/2CfGGvE。你必须执行整个 Notebook 才能得到预期的结果。
在这个练习中,我们学习了如何在 TensorFlow 中创建矩阵,以及如何执行矩阵乘法。这在我们创建自己的神经网络时将会非常有用。
reshape 函数
如名称所示,reshape 函数可以改变张量的形状,将其从当前形状转换为新的形状。例如,你可以将一个 2 × 3 的矩阵重塑为 3 × 2 的矩阵,如下所示:

图 1.23:重塑后的矩阵
让我们考虑以下 2 × 3 的矩阵,它是我们在前一个练习中定义的:
X=tf.Variable([[1,2,3],[4,5,6]])
我们可以使用以下代码打印矩阵的形状:
X.shape
从以下输出中,我们可以看到形状,这是我们已经知道的:
TensorShape([2, 3])
现在,要将 X 重塑为一个 3 × 2 的矩阵,TensorFlow 提供了一个方便的函数,叫做 tf.reshape()。该函数通过以下参数来实现:
tf.reshape(X,[3,2])
在前面的代码中,X 是需要重塑的矩阵,[3,2] 是 X 矩阵需要重塑成的新形状。
重塑矩阵是实现神经网络时常用的操作。例如,在使用 CNN 处理图像时,图像必须是 3 维的,也就是说,它必须有三个维度:宽度、高度和深度。如果我们的图像是一个只有两个维度的灰度图像,那么 reshape 操作就能派上用场,来添加第三个维度。在这种情况下,第三个维度的大小将是 1:

图 1.24:使用 reshape() 改变维度
在前面的图中,我们将一个形状为 [5,4] 的矩阵重新调整为形状为 [5,4,1] 的矩阵。在接下来的练习中,我们将使用 reshape() 函数将一个 [5,4] 矩阵进行重塑。
在实现 reshape() 函数时,有一些重要的注意事项:
-
新形状中的元素总数应与原始形状中的元素总数相等。例如,你可以将一个 2 × 3 的矩阵(总共 6 个元素)重塑为 3 × 2 的矩阵,因为新形状也有 6 个元素。但是,你不能将它重塑为 3 × 3 或 3 × 4。
-
reshape()函数不应与transpose()混淆。在reshape()中,矩阵元素的顺序保持不变,元素在新形状中按照相同的顺序重新排列。然而,在transpose()的情况下,行变成列,列变成行。因此,元素的顺序会发生变化。 -
reshape()函数不会改变原始矩阵,除非你将新形状赋值给它。否则,它只是显示新形状,而并不会实际更改原始变量。例如,假设x的形状是 [2,3],你只是执行了tf.reshape(x,[3,2])。当你再次检查x的形状时,它依然是 [2,3]。为了实际改变形状,你需要将新形状赋值给它,像这样:x=tf.reshape(x,[3,2])
让我们在接下来的练习中尝试在 TensorFlow 中实现 reshape()。
练习 1.04:使用 TensorFlow 中的 reshape() 函数重塑矩阵
在本练习中,我们将使用reshape()函数将一个[5,4]的矩阵重塑为[5,4,1]的形状。这个练习将帮助我们理解如何使用reshape()来改变张量的秩。按照以下步骤完成此练习:
-
打开一个 Jupyter Notebook 并将其重命名为练习 1.04。然后,导入
tensorflow并创建我们想要重塑的矩阵:import tensorflow as tf A=tf.Variable([[1,2,3,4], \ [5,6,7,8], \ [9,10,11,12], \ [13,14,15,16], \ [17,18,19,20]]) -
首先,我们将打印变量
A,以检查它是否已正确创建,使用以下命令:tf.print(A)输出结果如下:
[[1 2 3 4] [5 6 7 8] [9 10 11 12] [13 14 15 16] [17 18 19 20]] -
让我们打印一下
A的形状,以确保正确:A.shape输出结果如下:
TensorShape([5, 4])当前它的秩是 2。我们将使用
reshape()函数将其秩更改为 3。 -
现在,我们将使用以下命令将
A重塑为形状[5,4,1]。我们加入了print命令,以便查看输出结果:tf.print(tf.reshape(A,[5,4,1]))我们将得到以下输出:
[[[1] [2] [3] [4]] [[5] [6] [7] [8]] [[9] [10] [11] [12]] [[13] [14] [15] [16]] [[17] [18] [19] [20]]]这按预期工作。
-
让我们看看
A的新形状:A.shape输出结果如下:
TensorShape([5, 4])我们可以看到
A仍然具有相同的形状。记得我们讨论过,为了保存新的形状,我们需要将其赋值给自己。我们将在下一步中执行此操作。 -
在这里,我们将新的形状赋给
A:A = tf.reshape(A,[5,4,1]) -
让我们再检查一次
A的新形状:A.shape我们将看到以下输出:
TensorShape([5, 4, 1])到目前为止,我们不仅重新塑造了矩阵,还将其秩从 2 更改为 3。在下一步中,让我们打印出
A的内容,以确保无误。 -
让我们看看
A现在包含了什么:tf.print(A)输出结果,如预期的那样,将如下所示:
[[[1] [2] [3] [4]] [[5] [6] [7] [8]] [[9] [10] [11] [12]] [[13] [14] [15] [16]] [[17] [18] [19] [20]]]注意
要访问此特定部分的源代码,请参考
packt.live/3gHvyGQ。您也可以在
packt.live/2ZdjdUY上在线运行这个例子。您必须执行整个 Notebook 才能获得所需的结果。
在本练习中,我们学习了如何使用reshape()函数。通过reshape(),我们可以改变张量的秩和形状。我们还了解到,重塑矩阵会改变矩阵的形状,但不会改变矩阵中元素的顺序。另一个我们学到的重要内容是,重塑的维度必须与矩阵中的元素数量对齐。了解了reshape函数后,我们将继续学习下一个函数——Argmax。
argmax 函数
现在,让我们了解argmax函数,它在神经网络中经常使用。argmax返回矩阵或张量沿某个特定轴的最大值位置。需要注意的是,它并不会返回最大值本身,而是返回最大值的索引位置。
例如,如果x = [1,10,3,5],那么tf.argmax(x)将返回 1,因为最大值(在这种情况下是 10)位于索引位置 1。
注意
在 Python 中,索引是从 0 开始的。所以,考虑到前面的x例子,元素 1 的索引为 0,10 的索引为 1,依此类推。
现在,假设我们有以下内容:

图 1.25:示例矩阵
在这种情况下,argmax必须与axis参数一起使用。当axis等于 0 时,它返回每列中最大值的位置,如下图所示:

图 1.26:沿轴 0 进行的 argmax 操作
如您所见,第一列的最大值是 9,因此在这种情况下,索引为 2。同样,若我们查看第二列,最大值是 5,其索引为 0。在第三列,最大值为 8,因此索引为 1。如果我们在前述矩阵上运行argmax函数并将axis设置为 0,我们将得到以下输出:
[2,0,1]
当axis = 1 时,argmax返回每行最大值的位置,如下所示:

图 1.27:沿轴 1 进行的 argmax 操作
沿着行移动,我们在索引 1 处有 5,在索引 2 处有 8,在索引 0 处有 9。如果我们在前述矩阵上运行argmax函数,并将axis设置为 1,我们将得到以下输出:
[1,2,0]
现在,让我们尝试在矩阵上实现argmax。
练习 1.05:实现 argmax()函数
在这个练习中,我们将使用argmax函数在给定矩阵的轴 0 和轴 1 上找到最大值的位置。请按照以下步骤完成此练习:
-
导入
tensorflow并创建以下矩阵:import tensorflow as tf X=tf.Variable([[91,12,15], [11,88,21],[90, 87,75]]) -
让我们打印
X并查看矩阵的样子:tf.print(X)输出将如下所示:
[[91 12 15] [11 88 21] [90 87 75]] -
打印
X的形状:X.shape输出将如下所示:
TensorShape([3, 3]) -
现在,让我们使用
argmax在保持axis为0的情况下找到最大值的位置:tf.print(tf.argmax(X,axis=0))输出将如下所示:
[0 1 2]参考步骤 2中的矩阵,我们可以看到,沿着列移动,第一列中最大值(91)的索引是 0。同样,第二列中最大值(88)的索引是 1。最后,第三列中最大值(75)的索引是 2。因此,我们得到了上述输出。
-
现在,让我们将
axis改为1:tf.print(tf.argmax(X,axis=1))输出将如下所示:
[0 1 0]
再次参考步骤 2中的矩阵,如果我们沿着行移动,第一行中的最大值是 91,索引为 0。同样,第二行中的最大值是 88,索引为 1。最后,第三行的最大值是 75,索引又是 0。
注意
要访问此特定部分的源代码,请参阅packt.live/2ZR5q5p。
您还可以在packt.live/3eewhNO在线运行此示例。您必须执行整个 Notebook 才能获得所需的结果。
在本次练习中,我们学习了如何使用argmax函数来找到张量给定轴上最大值的位置。这将在后续章节中用于使用神经网络进行分类时。
优化器
在我们研究神经网络之前,让我们先了解另一个重要的概念,那就是优化器。优化器广泛应用于训练神经网络,因此理解其应用非常重要。在本章中,让我们对优化器的概念做一个基本的介绍。正如你可能已经知道的,机器学习的目的是找到一个函数(以及它的参数),该函数将输入映射到输出。
举个例子,假设一个数据分布的原始函数是以下形式的线性函数(线性回归):
Y = mX + b
在这里,Y是因变量(标签),X是自变量(特征),m和b是模型的参数。使用机器学习解决这个问题就是学习m和b这两个参数,从而得出将X与Y联系起来的函数形式。一旦这些参数被学习到,如果我们给定一个新的X值,我们就可以计算或预测Y的值。在学习这些参数的过程中,优化器发挥了作用。学习过程包括以下几个步骤:
-
假设
m和b是一些任意的随机值。 -
在这些假设的参数下,对于给定的数据集,估算每个
X变量的Y值。 -
找到
Y的预测值和与X变量相关的实际值之间的差异。这个差异称为损失函数或代价函数。损失的大小将取决于我们最初假设的参数值。如果假设与实际值相差甚远,那么损失就会很大。通过改变或调整参数的初始假设值,使得损失函数最小化,就能接近正确的参数。这一改变参数值以减少损失函数的过程称为优化。
在深度学习中,有不同类型的优化器。一些最常用的优化器包括随机梯度下降、Adam 和 RMSprop。优化器的详细功能和内部工作原理将在第二章,神经网络中进行描述,但在这里,我们将看到它们如何应用于解决一些常见问题,比如简单线性回归。在本章中,我们将使用一个非常流行的优化器——Adam。我们可以使用以下代码在 TensorFlow 中定义 Adam 优化器:
tf.optimizers.Adam()
一旦定义了优化器,我们可以使用以下代码来最小化损失:
optimizer.minimize(loss,[m,b])
[m,b]是优化过程中会被改变的参数。现在,让我们使用优化器通过 TensorFlow 训练一个简单的线性回归模型。
练习 1.06:使用优化器进行简单线性回归
在这个练习中,我们将学习如何使用优化器训练一个简单的线性回归模型。我们将首先假设一个线性方程 w*x + b 中的任意值(w 和 b)。通过优化器,我们将观察这些参数值是如何变化的,以便得到正确的参数值,从而映射输入值(x)与输出(y)之间的关系。使用优化后的参数值,我们将预测一些给定输入值(x)的输出(y)。完成此练习后,我们将看到,由优化参数预测的线性输出与实际输出值非常接近。按照以下步骤完成此练习:
-
打开一个 Jupyter Notebook 并将其重命名为 Exercise 1.06。
-
导入
tensorflow,创建变量并将其初始化为 0。这里,我们假设这两个参数的值都为零:import tensorflow as tf w=tf.Variable(0.0) b=tf.Variable(0.0) -
定义一个线性回归模型的函数。我们之前学习了如何在 TensorFlow 中创建函数:
def regression(x): model=w*x+b return model -
准备数据,以特征(
x)和标签(y)的形式呈现:x=[1,2,3,4] y=[0,-1,-2,-3] -
定义
loss函数。在此案例中,loss是预测值与标签值之间差的绝对值:loss=lambda:abs(regression(x)-y) -
创建一个学习率为
.01的Adam优化器实例。学习率定义了优化器应以多快的速度改变假设的参数。我们将在后续章节中讨论学习率:optimizer=tf.optimizers.Adam(.01) -
通过运行优化器 1,000 次迭代来训练模型,以最小化损失:
for i in range(1000): optimizer.minimize(loss,[w,b]) -
打印训练好的
w和b参数的值:tf.print(w,b)输出将如下所示:
-1.00371706 0.999803364我们可以看到,
w和b参数的值已经从假设的原始值 0 发生了变化。这正是优化过程中的操作。更新后的参数值将用于预测Y的值。注意
优化过程是随机的(具有随机概率分布),因此你可能得到与这里打印的值不同的
w和b的值。 -
使用训练好的模型通过输入
x值来预测输出。模型预测的值与标签值(y)非常接近,这意味着模型已经训练得非常精确:tf.print(regression([1,2,3,4]))上述命令的输出将如下所示:
[-0.00391370058 -1.00763083 -2.01134801 -3.01506495]注意
要访问此特定部分的源代码,请参考
packt.live/3gSBs8b。你也可以在线运行这个示例,链接地址是
packt.live/2OaFs7C。你必须执行整个 Notebook 才能得到期望的结果。
在本次练习中,我们学习了如何使用优化器来训练一个简单的线性回归模型。在此过程中,我们看到初始假设的参数值如何更新,以获得真实的参数值。通过使用真实的参数值,我们能够得到接近实际值的预测。理解如何应用优化器将帮助你后续训练神经网络模型。
现在我们已经看过优化器的使用,让我们将学到的知识应用到下一个活动中,通过优化函数来解一个二次方程。
活动 1.01:使用优化器解二次方程
在本次活动中,你将使用优化器来解下面的二次方程:

图 1.28:一个二次方程
完成本活动所需的高层次步骤如下:
-
打开一个新的 Jupyter Notebook,并导入必要的包,就像我们在之前的练习中所做的那样。
-
初始化变量。请注意,在此示例中,
x是你需要初始化的变量。你可以将其初始化为 0。 -
使用
lambda函数构造loss函数。loss函数将是你要解的二次方程。 -
使用学习率为
.01的Adam优化器。 -
在不同的迭代中运行优化器并最小化损失。你可以从 1,000 次迭代开始,然后在随后的试验中增加迭代次数,直到获得你想要的结果。
-
打印优化后的
x值。
预期的输出如下:
4.99919891
请注意,虽然你的实际输出可能略有不同,但它应该接近于 5。
注意
本活动的详细步骤、解决方案和附加评论,请参见第 388 页。
总结
本章到此结束。让我们回顾一下我们迄今为止学到的内容。我们首先了解了 AI、机器学习和深度学习之间的关系。接着,我们通过分类一张图片实现了深度学习的演示,并利用 Google API 实现了文本到语音的转换。随后,我们简要介绍了不同的深度学习应用场景和类型,如 MLP、CNN、RNN 和 GANs。
在下一部分中,我们介绍了 TensorFlow 框架,并了解了其中的一些基本构件,如张量及其秩和形状。我们还使用 TensorFlow 实现了不同的线性代数操作,如矩阵乘法。在本章后半部分,我们执行了一些有用的操作,如 reshape 和 argmax。最后,我们介绍了优化器的概念,并使用优化器解决了数学表达式问题。
现在我们已经为深度学习打下了基础,并向你介绍了 TensorFlow 框架,接下来你将可以深入探索神经网络的迷人世界。在下一章中,你将接触到神经网络,接下来的章节将深入探讨更多深度学习的概念。我们希望你能享受这段迷人的旅程。
第二章:2. 神经网络
概述
本章从介绍生物神经元开始,看看人工神经网络如何受生物神经网络的启发。我们将研究一个简单的单层神经元(称为感知器)的结构和内部工作原理,并学习如何在 TensorFlow 中实现它。接着,我们将构建多层神经网络来解决更复杂的多类分类任务,并讨论设计神经网络时的实际考虑。随着我们构建深度神经网络,我们将转向 Keras,在 Python 中构建模块化且易于定制的神经网络模型。到本章结束时,你将能熟练地构建神经网络来解决复杂问题。
介绍
在上一章中,我们学习了如何在 TensorFlow 中实现基本的数学概念,如二次方程、线性代数和矩阵乘法。现在,我们已经掌握了基础知识,让我们深入了解人工神经网络(ANNs),它们是人工智能和深度学习的核心。
深度学习是机器学习的一个子集。在监督学习中,我们经常使用传统的机器学习技术,如支持向量机或基于树的模型,其中特征是由人工明确设计的。然而,在深度学习中,模型会在没有人工干预的情况下探索并识别标记数据集中的重要特征。人工神经网络(ANNs),受生物神经元的启发,具有分层表示,这有助于它们从微小的细节到复杂的细节逐步学习标签。以图像识别为例:在给定的图像中,ANN 能够轻松识别诸如明暗区域这样的基本细节,也能识别更复杂的结构,如形状。尽管神经网络技术在识别图像中的物体等任务中取得了巨大成功,但它们的工作原理是一个黑箱,因为特征是隐式学习的。深度学习技术已经证明在解决复杂问题(如语音/图像识别)方面非常强大,因此被广泛应用于行业,如构建自动驾驶汽车、Google Now 和许多其他应用。
现在我们已经了解了深度学习技术的重要性,我们将采取一种务实的逐步方法,结合理论和实际考虑来理解构建基于深度学习的解决方案。我们将从神经网络的最小组件——人工神经元(也称为感知器)开始,逐步增加复杂性,探索多层感知器(MLPs)以及更先进的模型,如递归神经网络(RNNs)和卷积神经网络(CNNs)。
神经网络与感知器的结构
神经元是人类神经系统的基本构建块,它在全身传递电信号。人脑由数十亿个相互连接的生物神经元组成,它们通过开启或关闭自己,不断发送微小的电二进制信号互相通信。神经网络的普遍含义是相互连接的神经元的网络。在当前的背景下,我们指的是人工神经网络(ANNs),它们实际上是基于生物神经网络的模型。人工智能这一术语源自于自然智能存在于人脑(或任何大脑)这一事实,而我们人类正在努力模拟这种自然智能。尽管人工神经网络受到生物神经元的启发,但一些先进的神经网络架构,如卷积神经网络(CNNs)和递归神经网络(RNNs),并没有真正模仿生物神经元的行为。然而,为了便于理解,我们将首先通过类比生物神经元和人工神经元(感知机)来开始。
生物神经元的简化版本在图 2.1中表示:

图 2.1:生物神经元
这是一个高度简化的表示。它有三个主要组件:
-
树突,接收输入信号
-
细胞体,信号在其中以某种形式进行处理
-
尾部状的轴突,通过它神经元将信号传递到下一个神经元
感知机也可以用类似的方式表示,尽管它不是一个物理实体,而是一个数学模型。图 2.2展示了人工神经元的高级表示:

图 2.2:人工神经元的表示
在人工神经元中,和生物神经元一样,有一个输入信号。中央节点将所有信号合并,如果信号超过某个阈值,它将触发输出信号。感知机的更详细表示在图 2.3中展示。接下来的章节将解释这个感知机的每个组件:

图 2.3:感知机的表示
感知机有以下组成部分:
-
输入层
-
权重
-
偏置
-
网络输入函数
-
激活函数
让我们通过考虑一个OR表格数据集,详细查看这些组件及其 TensorFlow 实现。
输入层
每个输入数据的示例都会通过输入层传递。参考图 2.3中的表示,根据输入示例的大小,节点的数量将从x1 到xm 不等。输入数据可以是结构化数据(如 CSV 文件)或非结构化数据,如图像。这些输入,x1 到xm,被称为特征(m表示特征的数量)。我们通过一个例子来说明这一点。
假设数据以如下表格形式呈现:

图 2.4:样本输入和输出数据——OR 表
在这里,神经元的输入是列 x1 和 x2,它们对应于一行。此时可能很难理解,但暂时可以接受这样一个事实:在训练过程中,数据是以迭代的方式,一次输入一行。我们将使用 TensorFlow Variable 类如下表示输入数据和真实标签(输出 y):
X = tf.Variable([[0.,0.],[0.,1.],\
[1.,0.],[1.,1.]], \
tf.float32)
y = tf.Variable([0, 1, 1, 1], tf.float32)
权重
权重与每个神经元相关联,输入特征决定了每个输入特征在计算下一个节点时应有的影响力。每个神经元将与所有输入特征连接。在这个例子中,由于有两个输入(x1 和 x2),且输入层与一个神经元连接,所以会有两个与之相关联的权重:w1 和 w2。权重是一个实数,可以是正数或负数,数学上表示为 Variable 类,如下所示:
number_of_features = x.shape[1]
number_of_units = 1
Weight = tf.Variable(tf.zeros([number_of_features, \
number_of_units]), \
tf.float32)
权重的维度如下:输入特征的数量 × 输出大小。
偏置
在图 2.3中,偏置由 b 表示,称为加性偏置。每个神经元都有一个偏置。当 x 为零时,也就是说没有来自自变量的信息输入时,输出应该仅为 b。像权重一样,偏置也是一个实数,网络必须学习偏置值才能得到正确的预测结果。
在 TensorFlow 中,偏置与输出大小相同,可以表示如下:
B = tf.Variable(tf.zeros([1, 1]), tf.float32)
净输入函数
净输入函数,也常被称为输入函数,可以描述为输入与其对应权重的乘积之和,再加上偏置。数学上表示如下:

图 2.5:数学形式的净输入函数
这里:
-
xi:输入数据——x1 到 xm
-
wi:权重——w1 到 wm
-
b:加性偏置
如你所见,这个公式涉及到输入及其相关的权重和偏置。可以以向量化的形式写出,我们可以使用矩阵乘法,这是我们在第一章《深度学习基础》中学过的内容。我们将在开始代码演示时看到这一点。由于所有变量都是数字,净输入函数的结果只是一个数字,一个实数。净输入函数可以通过 TensorFlow 的 matmul 功能轻松实现,如下所示:
z = tf.add(tf.matmul(X, W), B)
W 代表权重,X 代表输入,B 代表偏置。
激活函数(G)
网络输入函数(z)的输出作为输入传递给激活函数。激活函数将网络输入函数(z)的输出压缩到一个新的输出范围,具体取决于激活函数的选择。有各种激活函数,如 Sigmoid(逻辑函数)、ReLU 和 tanh。每个激活函数都有其优缺点。我们将在本章稍后深入探讨激活函数。现在,我们先从 Sigmoid 激活函数开始,也叫逻辑函数。使用 Sigmoid 激活函数时,线性输出z被压缩到一个新的输出范围(0,1)。激活函数在层与层之间提供了非线性,这使得神经网络能够逼近任何连续函数。
Sigmoid 函数的数学公式如下,其中G(z)是 Sigmoid 函数,右边的公式详细说明了关于z的导数:

图 2.6:Sigmoid 函数的数学形式
如你在图 2.7中所见,Sigmoid 函数是一个大致为 S 形的曲线,其值介于 0 和 1 之间,无论输入是什么:

图 2.7:Sigmoid 曲线
如果我们设定一个阈值(比如0.5),我们可以将其转换为二进制输出。任何大于或等于.5的输出被视为1,任何小于.5的值被视为0。
TensorFlow 提供了现成的激活函数,如 Sigmoid。可以如下在 TensorFlow 中实现 Sigmoid 函数:
output = tf.sigmoid(z)
现在我们已经看到了感知机的结构以及其在 TensorFlow 中的代码表示,让我们把所有组件结合起来,构建一个感知机。
TensorFlow 中的感知机
在 TensorFlow 中,可以通过定义一个简单的函数来实现感知机,如下所示:
def perceptron(X):
z = tf.add(tf.matmul(X, W), B)
output = tf.sigmoid(z)
return output
在一个非常高的层次上,我们可以看到输入数据通过网络输入函数。网络输入函数的输出会传递给激活函数,激活函数反过来给我们预测的输出。现在,让我们逐行看一下代码:
z = tf.add(tf.matmul(X, W), B)
网络输入函数的输出存储在z中。让我们通过进一步分解,将结果分为两部分来看,即tf.matmul中的矩阵乘法部分和tf.add中的加法部分。
假设我们将X和W的矩阵乘法结果存储在一个名为m的变量中:
m = tf.matmul(X, W)
现在,让我们考虑一下如何得到这个结果。例如,假设X是一个行矩阵,如[ X1 X2 ],而W是一个列矩阵,如下所示:

图 2.8:列矩阵
回想一下上一章提到的,tf.matmul会执行矩阵乘法。因此,结果是这样的:
m = x1*w1 + x2*w2
然后,我们将输出m与偏置B相加,如下所示:
z = tf.add(m, B)
请注意,我们在前一步所做的与简单地将两个变量 m 和 b 相加是一样的:
m + b
因此,最终的输出是:
z = x1*w1 + x2*w2 + b
z 将是净输入函数的输出。
现在,让我们考虑下一行:
output= tf.sigmoid(z)
正如我们之前学到的,tf.sigmoid 是 Sigmoid 函数的现成实现。前一行计算的净输入函数的输出(z)作为输入传递给 Sigmoid 函数。Sigmoid 函数的结果就是感知器的输出,范围在 0 到 1 之间。在训练过程中,稍后将在本章中解释,我们将数据批量地输入到这个函数中,函数将计算预测值。
练习 2.01:感知器实现
在本练习中,我们将为 OR 表格实现一个感知器。在 TensorFlow 中设置输入数据并冻结感知器的设计参数:
-
让我们导入必要的包,在我们的案例中是
tensorflow:import tensorflow as tf -
在 TensorFlow 中设置
OR表格数据的输入数据和标签:X = tf.Variable([[0.,0.],[0.,1.],\ [1.,0.],[1.,1.]], \ dtype=tf.float32) print(X)正如你在输出中看到的,我们将得到一个 4 × 2 的输入数据矩阵:
<tf.Variable 'Variable:0' shape=(4, 2) dtype=float32, numpy=array([[0., 0.], [0., 1.], [1., 0.], [1., 1.]], dtype=float32)> -
我们将在 TensorFlow 中设置实际的标签,并使用
reshape()函数将y向量重塑为一个 4 × 1 的矩阵:y = tf.Variable([0, 1, 1, 1], dtype=tf.float32) y = tf.reshape(y, [4,1]) print(y)输出是一个 4 × 1 的矩阵,如下所示:
tf.Tensor( [[0.] [1.] [1.] [1.]], shape=(4, 1), dtype=float32) -
现在让我们设计感知器的参数。
神经元数量(单位) = 1
特征数量(输入) = 2(示例数量 × 特征数量)
激活函数将是 Sigmoid 函数,因为我们正在进行二元分类:
NUM_FEATURES = X.shape[1] OUTPUT_SIZE = 1在上面的代码中,
X.shape[1]将等于2(因为索引是从零开始的,1指的是第二个索引,它的值是2)。 -
在 TensorFlow 中定义连接权重矩阵:
W = tf.Variable(tf.zeros([NUM_FEATURES, \ OUTPUT_SIZE]), \ dtype=tf.float32) print(W)权重矩阵本质上将是一个列矩阵,如下图所示。它将具有以下维度:特征数量(列数) × 输出大小:
![图 2.9:列矩阵]()
](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_02_09.jpg)
<tf.Variable 'Variable:0' shape=(2, 1) dtype=float32, \ numpy=array([[0.], [0.]], dtype=float32)> -
现在创建偏置的变量:
B = tf.Variable(tf.zeros([OUTPUT_SIZE, 1]), dtype=tf.float32) print(B)每个神经元只有一个偏置,因此在这种情况下,偏置只是一个单元素数组中的一个数字。然而,如果我们有一个包含 10 个神经元的层,那么它将是一个包含 10 个数字的数组——每个神经元对应一个。
这将导致一个零行矩阵,包含一个单一元素,如下所示:
<tf.Variable 'Variable:0' shape=(1, 1) dtype=float32, numpy=array([[0.]], dtype=float32)> -
现在我们已经有了权重和偏置,下一步是执行计算,得到净输入函数,将其输入到激活函数中,然后得到最终输出。让我们定义一个名为
perceptron的函数来获取输出:def perceptron(X): z = tf.add(tf.matmul(X, W), B) output = tf.sigmoid(z) return output print(perceptron(X))输出将是一个 4 × 1 的数组,包含我们感知器的预测结果:
tf.Tensor( [[0.5] [0.5] [0.5] [0.5]], shape=(4, 1), dtype=float32)如我们所见,预测结果并不十分准确。我们将在接下来的章节中学习如何改进结果。
注意
要访问该特定部分的源代码,请参考
packt.live/3feF7MO。你也可以在
packt.live/2CkMiEE上运行这个示例。你必须执行整个 Notebook 才能获得预期的结果。
在这个练习中,我们实现了一个感知机,它是单个人工神经元的数学实现。请记住,这只是模型的实现,我们还没有进行任何训练。在下一节中,我们将看到如何训练感知机。
训练感知机
要训练一个感知机,我们需要以下组件:
-
数据表示
-
层
-
神经网络表示
-
损失函数
-
优化器
-
训练循环
在前一节中,我们讲解了大部分前面的组件:perceptron(),它使用线性层和 sigmoid 层来执行预测。我们在前一节中使用输入数据和初始权重与偏差所做的工作被称为前向传播。实际的神经网络训练涉及两个阶段:前向传播和反向传播。我们将在接下来的几步中详细探讨它们。让我们从更高的层次来看训练过程:
-
神经网络遍历所有训练样本的训练迭代被称为一个 Epoch。这是需要调整的超参数之一,以便训练神经网络。
-
在每一轮传递中,神经网络都会进行前向传播,其中数据从输入层传输到输出层。正如在练习 2.01,感知机实现中所看到的,输入被馈送到感知机。输入数据通过网络输入函数和激活函数,生成预测输出。预测输出与标签或真实值进行比较,计算误差或损失。
-
为了让神经网络学习(即调整权重和偏差以做出正确预测),需要有一个损失函数,它将计算实际标签和预测标签之间的误差。
-
为了最小化神经网络中的误差,训练循环需要一个优化器,它将基于损失函数来最小化损失。
-
一旦计算出误差,神经网络就会查看网络中哪些节点对误差产生了影响,以及影响的程度。这对于在下一轮训练中提高预测效果至关重要。这个向后传播误差的方式被称为反向传播(backpropagation)。反向传播利用微积分中的链式法则,以反向顺序传播误差(误差梯度),直到达到输入层。在通过网络反向传播误差时,它使用梯度下降法根据之前计算的误差梯度对网络中的权重和偏差进行微调。
这个循环会持续进行,直到损失最小化。
让我们在 TensorFlow 中实现我们讨论过的理论。回顾一下 练习 2.01,感知机实现,在该练习中,我们创建的感知机只进行了一个前向传播。我们得到了以下预测结果,并且发现我们的感知机没有学到任何东西:
tf.Tensor(
[[0.5]
[0.5]
[0.5]
[0.5]], shape=(4, 1), dtype=float32)
为了让我们的感知机学习,我们需要一些额外的组件,例如训练循环、损失函数和优化器。让我们看看如何在 TensorFlow 中实现这些组件。
TensorFlow 中的感知机训练过程
在下一个练习中,当我们训练模型时,我们将使用 随机梯度下降(SGD)优化器来最小化损失。TensorFlow 提供了一些更高级的优化器。我们将在后续部分讨论它们的优缺点。以下代码将使用 TensorFlow 实例化一个随机梯度下降优化器:
learning_rate = 0.01
optimizer = tf.optimizers.SGD(learning_rate)
perceptron 函数负责前向传播。对于误差的反向传播,我们使用了一个优化器。Tf.optimizers.SGD 创建了一个优化器实例。SGD 会在每个输入数据的示例上更新网络的参数——权重和偏置。我们将在本章后续部分更详细地讨论梯度下降优化器的工作原理。我们还会讨论 0.01 参数的意义,该参数被称为学习率。学习率是 SGD 为了达到损失函数的全局最优解而采取的步伐的大小。学习率是另一个超参数,需要调节以训练神经网络。
以下代码可用于定义训练周期、训练循环和损失函数:
no_of_epochs = 1000
for n in range(no_of_epochs):
loss = lambda:abs(tf.reduce_mean(tf.nn.\
sigmoid_cross_entropy_with_logits\
(labels=y,logits=perceptron(X))))
optimizer.minimize(loss, [W, B])
在训练循环中,损失是通过损失函数计算的,损失函数被定义为一个 lambda 函数。
tf.nn.sigmoid_cross_entropy_with_logits 函数计算每个观测值的损失值。它接受两个参数:Labels = y 和 logit = perceptron(x)。
perceptron(X) 返回预测值,这是输入 x 的前向传播结果。这个结果与存储在 y 中的相应标签值进行比较。使用 Tf.reduce_mean 计算平均值,并取其大小。使用 abs 函数忽略符号。Optimizer.minimize 会根据损失值调整权重和偏置,这是误差反向传播的一部分。
使用新的权重和偏置值再次执行前向传播。这个前向和反向过程会持续进行,直到我们定义的迭代次数结束。
在反向传播过程中,只有当损失小于上一个周期的损失时,权重和偏置才会被更新。否则,权重和偏置保持不变。通过这种方式,优化器确保尽管它会执行所需的迭代次数,但只会存储那些损失最小的 w 和 b 值。
我们将训练的轮数设置为 1,000 次迭代。设置训练轮数没有固定的经验法则,因为轮数是一个超参数。那么,我们如何知道训练是否成功呢?
当我们看到权重和偏置的值发生变化时,我们可以得出结论,训练已经发生。假设我们使用了练习 2.01中的OR数据进行训练,并应用了感知机实现,我们会看到权重大致等于以下值:
[[0.412449151]
[0.412449151]]
偏置值可能是这样的:
0.236065879
当网络已经学习,即权重和偏置已经更新时,我们可以使用scikit-learn包中的accuracy_score来查看它是否做出了准确的预测。我们可以通过如下方式来测量预测的准确性:
from sklearn.metrics import accuracy_score
print(accuracy_score(y, ypred))
在这里,accuracy_score接收两个参数——标签值(y)和预测值(ypred)——并计算准确率。假设结果是1.0,这意味着感知机的准确率为 100%。
在下一个练习中,我们将训练感知机来执行二分类任务。
练习 2.02:感知机作为二分类器
在上一节中,我们学习了如何训练感知机。在本练习中,我们将训练感知机来近似一个稍微复杂一些的函数。我们将使用随机生成的外部数据,数据有两个类别:类别0和类别1。我们训练后的感知机应该能够根据类别来分类这些随机数:
注意
数据存储在名为data.csv的 CSV 文件中。你可以通过访问packt.live/2BVtxIf从 GitHub 下载该文件。
-
导入所需的库:
import tensorflow as tf import pandas as pd from sklearn.metrics import confusion_matrix from sklearn.metrics import accuracy_score import matplotlib.pyplot as plt %matplotlib inline除了
tensorflow,我们还需要pandas来从 CSV 文件读取数据,confusion_matrix和accuracy_score来衡量训练后感知机的准确性,以及matplotlib来可视化数据。 -
从
data.csv文件中读取数据。该文件应与运行此练习代码的 Jupyter Notebook 文件在同一路径下。否则,在执行代码之前你需要更改路径:df = pd.read_csv('data.csv') -
检查数据:
df.head()输出将如下所示:
![图 2.10:DataFrame 的内容]()
图 2.10:DataFrame 的内容
如你所见,数据有三列。
x1和x2是特征,而label列包含每个观测的标签0或1。查看这种数据的最佳方式是通过散点图。 -
使用
matplotlib绘制图表来可视化数据:plt.scatter(df[df['label'] == 0]['x1'], \ df[df['label'] == 0]['x2'], \ marker='*') plt.scatter(df[df['label'] == 1]['x1'], \ df[df['label'] == 1]['x2'], marker='<')输出将如下所示:
![图 2.11:外部数据的散点图]()
图 2.11:外部数据的散点图
这显示了数据的两个不同类别,通过两种不同的形状来表示。标签为
0的数据用星号表示,而标签为1的数据用三角形表示。 -
准备数据。这一步骤不仅仅是神经网络特有的,你在常规机器学习中也一定见过。在将数据提交给模型进行训练之前,你需要将数据分割为特征和标签:
X_input = df[['x1','x2']].values y_label = df[['label']].valuesx_input包含特征x1和x2。末尾的值将其转换为矩阵格式,这是创建张量时所期望的输入格式。y_label包含矩阵格式的标签。 -
创建 TensorFlow 变量用于特征和标签,并将它们转换为
float类型:x = tf.Variable(X_input, dtype=tf.float32) y = tf.Variable(y_label, dtype=tf.float32) -
剩下的代码是用来训练感知器的,我们在练习 2.01,感知器实现中看过:
Exercise2.02.ipynb Number_of_features = 2 Number_of_units = 1 learning_rate = 0.01 # weights and bias weight = tf.Variable(tf.zeros([Number_of_features, \ Number_of_units])) bias = tf.Variable(tf.zeros([Number_of_units])) #optimizer optimizer = tf.optimizers.SGD(learning_rate) def perceptron(x): z = tf.add(tf.matmul(x,weight),bias) output = tf.sigmoid(z) return output The complete code for this step can be found at https://packt.live/3gJ73bY.注意
上述代码片段中的
#符号表示代码注释。注释被添加到代码中以帮助解释特定的逻辑部分。 -
显示
weight和bias的值,以展示感知器已经被训练过:tf.print(weight, bias)输出结果如下:
[[-0.844034135] [0.673354745]] [0.0593947917] -
将输入数据传递进去,检查感知器是否正确分类:
ypred = perceptron(x) -
对输出结果进行四舍五入,转换成二进制格式:
ypred = tf.round(ypred) -
使用
accuracy_score方法来衡量准确性,正如我们在之前的练习中所做的那样:acc = accuracy_score(y.numpy(), ypred.numpy()) print(acc)输出结果如下:
1.0该感知器给出了 100%的准确率。
-
混淆矩阵帮助评估模型的性能。我们将使用
scikit-learn包来绘制混淆矩阵。cnf_matrix = confusion_matrix(y.numpy(), \ ypred.numpy()) print(cnf_matrix)输出结果将如下所示:
[[12 0] [ 0 9]]所有的数字都位于对角线上,即,12 个值对应于类别 0,9 个值对应于类别 1,这些都被我们训练好的感知器正确分类(该感知器已达到 100%的准确率)。
注意
要查看这个具体部分的源代码,请参考
packt.live/3gJ73bY。你也可以在网上运行这个示例,访问
packt.live/2DhelFw。你必须执行整个 Notebook 才能得到预期的结果。
在本练习中,我们将感知器训练成了一个二分类器,并且表现得相当不错。在下一个练习中,我们将看到如何创建一个多分类器。
多分类器
一个可以处理两类的分类器被称为二分类器,就像我们在之前的练习中看到的那样。一个可以处理多于两类的分类器被称为多分类器。我们无法使用单一神经元来构建多分类器。现在我们从一个神经元转变为一个包含多个神经元的层,这对于多分类器是必需的。
一层多个神经元可以被训练成一个多分类器。这里详细列出了一些关键点。你需要的神经元数量等于类别的数量;也就是说,对于一个 3 类的分类器,你需要 3 个神经元;对于一个 10 类的分类器,你需要 10 个神经元,依此类推。
如我们在二分类中看到的,我们使用 sigmoid(逻辑层)来获取 0 到 1 范围内的预测。在多类分类中,我们使用一种特殊类型的激活函数,称为Softmax激活函数,以获得每个类别的概率,总和为 1。使用 sigmoid 函数进行多类分类时,概率不一定加起来为 1,因此更倾向使用 Softmax。
在实现多类分类器之前,让我们先探索 Softmax 激活函数。
Softmax 激活函数
Softmax 函数也被称为归一化指数函数。正如归一化一词所暗示的,Softmax 函数将输入归一化为一个总和为 1 的概率分布。从数学角度来看,它表示为:

图 2.12:Softmax 函数的数学形式
为了理解 Softmax 的作用,让我们使用 TensorFlow 内置的softmax函数并查看输出。
所以,对于以下代码:
values = tf.Variable([3,1,7,2,4,5], dtype=tf.float32)
output = tf.nn.softmax(values)
tf.print(output)
输出结果将是:
[0.0151037546 0.00204407098 0.824637055
0.00555636082 0.0410562605 0.111602485]
如您所见,输出中values输入被映射到一个概率分布,且总和为 1。注意,7(原始输入值中的最大值)获得了最高的权重,0.824637055。这正是 Softmax 函数的主要用途:专注于最大值,并抑制低于最大值的值。此外,如果我们对输出求和,结果将接近 1。
详细说明该示例,假设我们想构建一个包含 3 个类别的多类分类器。我们将需要连接到 Softmax 激活函数的 3 个神经元:

图 2.13:在多类分类设置中使用的 Softmax 激活函数
如图 2.13所示,x1、x2 和x3 是输入特征,它们经过每个神经元的网络输入函数,这些神经元具有与之相关的权重和偏置(Wi,j 和 bi)。最后,神经元的输出被送入通用的 Softmax 激活函数,而不是单独的 sigmoid 函数。Softmax 激活函数输出 3 个类别的概率:P1、P2和P3。由于 Softmax 层的存在,这三个概率的总和将为 1。
正如我们在前一部分看到的,Softmax 突出最大值并抑制其余的值。假设一个神经网络被训练来将输入分类为三个类别,对于给定的输入集,输出为类别 2;那么它会说P2具有最高值,因为它经过了 Softmax 层。如下面的图所示,P2具有最高值,这意味着预测是正确的:

图 2.14:概率 P2 最大
相关概念是独热编码。由于我们有三个不同的类别,class1、class2和class3,我们需要将类别标签编码为便于操作的格式;因此,应用独热编码后,我们会看到如下输出:

图 2.15:三个类别的独热编码数据
这样可以使结果快速且容易解释。在这种情况下,值最高的输出被设置为 1,所有其他值设置为 0。上述例子的独热编码输出将如下所示:

图 2.16:独热编码的输出概率
训练数据的标签也需要进行独热编码。如果它们格式不同,则需要在训练模型之前将其转换为独热编码格式。让我们进行一次关于独热编码的多类分类练习。
练习 2.03:使用感知机进行多类分类
为了执行多类分类,我们将使用鸢尾花数据集(archive.ics.uci.edu/ml/datasets/Iris),该数据集包含 3 个类别,每个类别有 50 个实例,每个类别代表一种鸢尾花。我们将使用一个包含三个神经元的单层,采用 Softmax 激活函数:
注意
你可以通过这个链接从 GitHub 下载数据集:packt.live/3ekiBBf。
-
导入所需的库:
import tensorflow as tf import pandas as pd from sklearn.metrics import confusion_matrix from sklearn.metrics import accuracy_score import matplotlib.pyplot as plt %matplotlib inline from pandas import get_dummies你应该熟悉所有这些导入,因为它们在前一个练习中已使用过,除了
get_dummies。此函数将给定的标签数据转换为相应的独热编码格式。 -
加载
iris.csv数据:df = pd.read_csv('iris.csv') -
让我们查看数据的前五行:
df.head()输出结果如下:
![图 2.17:DataFrame 的内容]()
图 2.17:DataFrame 的内容
-
使用散点图可视化数据:
plt.scatter(df[df['species'] == 0]['sepallength'],\ df[df['species'] == 0]['sepalwidth'], marker='*') plt.scatter(df[df['species'] == 1]['sepallength'],\ df[df['species'] == 1]['sepalwidth'], marker='<') plt.scatter(df[df['species'] == 2]['sepallength'], \ df[df['species'] == 2]['sepalwidth'], marker='o')结果图如下所示。x轴表示花萼长度,y轴表示花萼宽度。图中的形状表示三种鸢尾花的品种,setosa(星形)、versicolor(三角形)和 virginica(圆形):
![图 2.18:鸢尾花数据散点图]()
图 2.18:鸢尾花数据散点图
如可视化所示,共有三个类别,用不同的形状表示。
-
将特征和标签分开:
x = df[['petallength', 'petalwidth', \ 'sepallength', 'sepalwidth']].values y = df['species'].valuesvalues将把特征转换为矩阵格式。 -
通过对类别进行独热编码来准备数据:
y = get_dummies(y) y = y.valuesget_dummies(y)将把标签转换为独热编码格式。 -
创建一个变量来加载特征,并将其类型转换为
float32:x = tf.Variable(x, dtype=tf.float32) -
使用三个神经元实现
感知机层:Number_of_features = 4 Number_of_units = 3 # weights and bias weight = tf.Variable(tf.zeros([Number_of_features, \ Number_of_units])) bias = tf.Variable(tf.zeros([Number_of_units])) def perceptron(x): z = tf.add(tf.matmul(x, weight), bias) output = tf.nn.softmax(z) return output这段代码看起来与单一感知机实现非常相似。只是将
Number_of_units参数设置为3。因此,权重矩阵将是 4 x 3,偏置矩阵将是 1 x 3。另一个变化是在激活函数中:
Output=tf.nn.softmax(x)我们使用的是
softmax而不是sigmoid。 -
创建一个
optimizer实例。我们将使用Adam优化器。在这一点上,你可以将Adam视为一种改进版的梯度下降法,它收敛速度更快。我们将在本章稍后详细讲解:optimizer = tf.optimizers.Adam(.01) -
定义训练函数:
def train(i): for n in range(i): loss=lambda: abs(tf.reduce_mean\ (tf.nn.softmax_cross_entropy_with_logits(\ labels=y, logits=perceptron(x)))) optimizer.minimize(loss, [weight, bias])再次说明,代码看起来与单神经元实现非常相似,唯一的不同是损失函数。我们使用的是
softmax_cross_entropy_with_logits,而不是sigmoid_cross_entropy_with_logits。 -
运行训练
1000次迭代:train(1000) -
打印权重值以查看它们是否发生了变化。这也是我们感知器在学习的一个标志:
tf.print(weight)输出显示我们感知器学习到的权重:
[[0.684310317 0.895633 -1.0132345] [2.6424644 -1.13437736 -3.20665336] [-2.96634197 -0.129377216 3.2572844] [-2.97383809 -3.13501668 3.2313652]] -
为了测试准确率,我们将特征输入以预测输出,然后使用
accuracy_score计算准确率,就像在前面的练习中一样:ypred=perceptron(x) ypred=tf.round(ypred) accuracy_score(y, ypred)输出为:
0.98它的准确率达到了 98%,非常不错。
注意
若要访问此特定部分的源代码,请参考
packt.live/2Dhes3U。你也可以在线运行此示例,网址是
packt.live/3iJJKkm。你必须执行整个 Notebook 才能得到期望的结果。
在这个练习中,我们使用感知器进行了多类分类。接下来,我们将进行一个更复杂、更有趣的手写数字识别数据集的案例研究。
MNIST 案例研究
现在,我们已经了解了如何训练单个神经元和单层神经网络,接下来让我们看看更现实的数据。MNIST 是一个著名的案例研究。在下一个练习中,我们将创建一个 10 类分类器来分类 MNIST 数据集。不过,在那之前,你应该对 MNIST 数据集有一个充分的了解。
修改版国家标准与技术研究院(MNIST)是指由 Yann LeCun 领导的团队在 NIST 使用的修改数据集。这个项目的目标是通过神经网络进行手写数字识别。
在开始编写代码之前,我们需要了解数据集。MNIST 数据集已经集成到 TensorFlow 库中。它包含了 70,000 张手写数字 0 到 9 的图像:

图 2.19:手写数字
当我们提到图像时,你可能会认为它们是 JPEG 文件,但实际上它们不是。它们是以像素值的形式存储的。从计算机的角度来看,图像就是一堆数字。这些数字是从 0 到 255 之间的像素值。这些图像的维度是 28 x 28。图像是以 28 x 28 矩阵的形式存储的,每个单元包含从 0 到 255 之间的实数。这些是灰度图像(通常称为黑白图像)。0 表示白色,1 表示完全黑色,中间的值表示不同深浅的灰色。MNIST 数据集分为 60,000 张训练图像和 10,000 张测试图像。
每张图片都有一个标签,标签范围从 0 到 9。下一次练习中,我们将构建一个 10 类分类器来分类手写的 MNIST 图片。
练习 2.04:分类手写数字
在本练习中,我们将构建一个由 10 个神经元组成的单层 10 类分类器,采用 Softmax 激活函数。它将有一个 784 像素的输入层:
-
导入所需的库和包,就像我们在前面的练习中做的那样:
import tensorflow as tf import pandas as pd from sklearn.metrics import accuracy_score import matplotlib.pyplot as plt %matplotlib inline from pandas import get_dummies -
创建 MNIST 数据集的实例:
mnist = tf.keras.datasets.mnist -
加载 MNIST 数据集的
train和test数据:(train_features, train_labels), (test_features, test_labels) = \ mnist.load_data() -
对数据进行归一化:
train_features, test_features = train_features / 255.0, \ test_features / 255.0 -
将二维图像展平为行矩阵。因此,一个 28 × 28 像素的图像将被展平为
784,使用reshape函数:x = tf.reshape(train_features,[60000, 784]) -
创建一个
Variable,并将其类型转换为float32:x = tf.Variable(x) x = tf.cast(x, tf.float32) -
创建标签的独热编码并将其转换为矩阵:
y_hot = get_dummies(train_labels) y = y_hot.values -
创建一个包含
10个神经元的单层神经网络,并训练1000次:Exercise2.04.ipynb #defining the parameters Number_of_features = 784 Number_of_units = 10 # weights and bias weight = tf.Variable(tf.zeros([Number_of_features, \ Number_of_units])) bias = tf.Variable(tf.zeros([Number_of_units])) The complete code for this step can be accessed from https://packt.live/3efd7Yh. -
准备测试数据以评估准确率:
# Prepare the test data to measure the accuracy. test = tf.reshape(test_features, [10000, 784]) test = tf.Variable(test) test = tf.cast(test, tf.float32) test_hot = get_dummies(test_labels) test_matrix = test_hot.values -
通过将测试数据传入网络来进行预测:
ypred = perceptron(test) ypred = tf.round(ypred) -
计算准确率:
accuracy_score(test_hot, ypred)预测的准确率是:
0.9304注意
若要访问此部分的源代码,请参考
packt.live/3efd7Yh。你也可以在线运行此示例,地址是
packt.live/2Oc83ZW。你必须执行整个 Notebook 才能获得期望的结果。
在这个练习中,我们展示了如何创建一个单层多神经元神经网络,并将其训练为一个多类分类器。
下一步是构建一个多层神经网络。然而,在此之前,我们必须了解 Keras API,因为我们使用 Keras 来构建密集神经网络。
Keras 作为高级 API
在 TensorFlow 1.0 中,有多个 API,比如 Estimator、Contrib 和 layers。而在 TensorFlow 2.0 中,Keras 与 TensorFlow 紧密集成,提供了一个用户友好的、高级的 API,具有模块化、可组合且易于扩展的特性,可以用来构建和训练深度学习模型。这也使得开发神经网络代码变得更加简单。让我们来看它是如何工作的。
练习 2.05:使用 Keras 进行二分类
在这个练习中,我们将使用 Keras API 实现一个非常简单的二分类器,只有一个神经元。我们将使用与练习 2.02、感知机作为二分类器中相同的data.csv文件:
注意
数据集可以通过访问以下 GitHub 链接进行下载:https://packt.live/2BVtxIf。
-
导入所需的库:
import tensorflow as tf import pandas as pd import matplotlib.pyplot as plt %matplotlib inline # Import Keras libraries from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense在代码中,
Sequential是我们将使用的 Keras 模型类型,因为它非常容易向其中添加层。Dense是将要添加的层类型。这些是常规的神经网络层,而不是稍后将使用的卷积层或池化层。 -
导入数据:
df = pd.read_csv('data.csv') -
检查数据:
df.head()以下是输出结果:
![图 2.20:DataFrame 的内容]()
图 2.20:DataFrame 的内容
-
使用散点图可视化数据:
plt.scatter(df[df['label'] == 0]['x1'], \ df[df['label'] == 0]['x2'], marker='*') plt.scatter(df[df['label'] == 1]['x1'], \ df[df['label'] == 1]['x2'], marker='<')生成的图形如下,x轴表示
x1值,y 轴表示x2值:![图 2.21:数据的散点图]()
图 2.21:数据的散点图
-
通过分离特征和标签并设置
tf变量来准备数据:x_input = df[['x1','x2']].values y_label = df[['label']].values -
创建一个神经网络模型,由一个神经元和一个 sigmoid 激活函数组成:
model = Sequential() model.add(Dense(units=1, input_dim=2, activation='sigmoid'))mymodel.add(Dense())中的参数如下:units是该层神经元的数量;input_dim是特征的数量,在此案例中为2;activation是sigmoid。 -
一旦模型创建完成,我们使用
compile方法传入训练所需的额外参数,如优化器类型、损失函数等:model.compile(optimizer='adam', \ loss='binary_crossentropy',\ metrics=['accuracy'])在这个案例中,我们使用了
adam优化器,这是梯度下降优化器的增强版,损失函数是binary_crossentropy,因为这是一个二分类器。metrics参数几乎总是设置为['accuracy'],用于显示如训练轮数、训练损失、训练准确度、测试损失和测试准确度等信息。 -
现在模型已准备好进行训练。然而,使用
summary函数检查模型配置是个好主意:model.summary()输出将如下所示:
![图 2.22:顺序模型摘要]()
图 2.22:顺序模型摘要
-
通过调用
fit()方法来训练模型:model.fit(x_input, y_label, epochs=1000)它接受特征和标签作为数据参数,并包含训练的轮数,在此案例中为
1000。模型将开始训练,并会持续显示状态,如下所示:![图 2.23:使用 Keras 的模型训练日志]()
图 2.23:使用 Keras 的模型训练日志
-
我们将使用 Keras 的
evaluate功能来评估模型:model.evaluate(x_input, y_label)输出结果如下:
21/21 [==============================] - 0s 611us/sample - loss: 0.2442 - accuracy: 1.0000 [0.24421504139900208, 1.0]如你所见,我们的 Keras 模型训练得非常好,准确率达到了 100%。
注意
要访问此特定章节的源代码,请参考
packt.live/2ZVV1VY。你也可以在线运行此示例,网址是
packt.live/38CzhTc。你必须执行整个笔记本才能得到期望的结果。
在本练习中,我们学习了如何使用 Keras 构建感知机。正如你所见,Keras 使代码更加模块化、更具可读性,而且参数调整也更为简便。在下一节中,我们将学习如何使用 Keras 构建多层或深度神经网络。
多层神经网络或深度神经网络
在前面的示例中,我们开发了一个单层神经网络,通常称为浅层神经网络。其示意图如下所示:

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_02_24.jpg)
图 2.24:浅层神经网络
一层神经元不足以解决更复杂的问题,如人脸识别或物体检测。你需要堆叠多个层,这通常被称为创建深度神经网络。其示意图如下所示:

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_02_26.jpg)
图 2.25:深度神经网络
在我们跳入代码之前,让我们试着理解一下这个过程是如何工作的。输入数据被馈送到第一层的神经元。需要注意的是,每个输入都会馈送到第一层的每个神经元,并且每个神经元都有一个输出。第一层每个神经元的输出会被馈送到第二层的每个神经元,第二层每个神经元的输出会被馈送到第三层的每个神经元,依此类推。
因此,这种网络也被称为密集神经网络或全连接神经网络。还有其他类型的神经网络,其工作原理不同,比如卷积神经网络(CNN),但这些内容我们将在下一章讨论。每一层中神经元的数量没有固定规则,通常通过试错法来确定,这个过程叫做超参数调优(我们将在本章后面学习)。然而,在最后一层神经元的数量上,是有一些限制的。最后一层的配置如下所示:

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_02_26.jpg)
图 2.26:最后一层配置
ReLU 激活函数
在我们实现深度神经网络的代码之前,最后需要了解一下 ReLU 激活函数。这是多层神经网络中最常用的激活函数之一。
ReLU 是 Rectified Linear Unit(修正线性单元)的缩写。ReLU 函数的输出总是一个非负值,且大于或等于 0:

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_02_27.jpg)
图 2.27:ReLU 激活函数
ReLU 的数学表达式是:

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_02_25.jpg)
图 2.28:ReLU 激活函数
ReLU 收敛速度比 sigmoid 激活函数快得多,因此它是目前最广泛使用的激活函数。几乎所有的深度神经网络都使用 ReLU。它被应用于除最后一层外的所有层,最后一层则使用 sigmoid 或 Softmax。
ReLU 激活函数是 TensorFlow 内置提供的。为了了解它是如何实现的,我们给 ReLU 函数输入一些示例值,看看输出:
values = tf.Variable([1.0, -2., 0., 0.3, -1.5], dtype=tf.float32)
output = tf.nn.relu(values)
tf.print(output)
输出如下:
[1 0 0 0.3 0]
如你所见,所有正值都被保留,负值被压制为零。接下来我们将在下一个练习中使用这个 ReLU 激活函数来完成多层二分类任务。
练习 2.06:多层二分类器
在本次练习中,我们将使用在练习 2.02中使用的data.csv文件来实现一个多层二分类器,感知机作为二分类器。
我们将构建一个深度神经网络二分类器,配置如下:输入层有 2 个节点,包含 2 个隐藏层,第一个层有 50 个神经元,第二个层有 20 个神经元,最后是一个神经元,用于进行最终的二分类预测:
注意
数据集可以通过以下链接从 GitHub 下载:https://packt.live/2BVtxIf .
-
导入所需的库和包:
import tensorflow as tf import pandas as pd import matplotlib.pyplot as plt %matplotlib inline ##Import Keras libraries from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense -
导入并检查数据:
df = pd.read_csv('data.csv') df.head()输出如下:
![图 2.29:数据的前五行]()
图 2.29:数据的前五行
-
使用散点图可视化数据:
plt.scatter(df[df['label'] == 0]['x1'], \ df[df['label'] == 0]['x2'], marker='*') plt.scatter(df[df['label'] == 1]['x1'], \ df[df['label'] == 1]['x2'], marker='<')结果输出如下,x 轴显示
x1值,y 轴显示x2值:![图 2.30:给定数据的散点图]()
图 2.30:给定数据的散点图
-
通过分离特征和标签并设置
tf变量来准备数据:x_input = df[['x1','x2']].values y_label = df[['label']].values -
构建
Sequential模型:model = Sequential() model.add(Dense(units = 50,input_dim=2, activation = 'relu')) model.add(Dense(units = 20 , activation = 'relu')) model.add(Dense(units = 1,input_dim=2, activation = 'sigmoid'))以下是几个需要考虑的要点。我们提供了第一层的输入细节,然后对所有中间层使用 ReLU 激活函数,如前所述。此外,最后一层只有一个神经元,并且使用 sigmoid 激活函数来进行二分类。
-
使用
compile方法提供训练参数:model.compile(optimizer='adam', \ loss='binary_crossentropy', metrics=['accuracy']) -
使用
summary函数检查model配置:model.summary()输出将如下所示:
![图 2.31:使用 Keras 深度神经网络模型总结]()
图 2.31:使用 Keras 深度神经网络模型总结
在模型总结中,我们可以看到,总共有
1191个参数——权重和偏置——需要在隐藏层到输出层之间进行学习。 -
通过调用
fit()方法训练模型:model.fit(x_input, y_label, epochs=50)请注意,在这种情况下,模型在
50个 epoch 内达到了 100% 的准确率,而单层模型大约需要 1,000 个 epoch:![图 2.32:多层模型训练日志]()
图 2.32:多层模型训练日志
-
让我们评估模型的性能:
model.evaluate(x_input, y_label)输出如下:
21/21 [==============================] - 0s 6ms/sample - loss: 0.1038 - accuracy: 1.0000 [0.1037961095571518, 1.0]我们的模型现在已经训练完成,并且展示了 100% 的准确率。
注意
要访问此特定部分的源代码,请参阅
packt.live/2ZUkM94。你也可以在
packt.live/3iKsD1W在线运行这个示例。你必须执行整个 Notebook 才能获得期望的结果。
在这个练习中,我们学习了如何使用 Keras 构建一个多层神经网络。这是一个二分类器。在下一个练习中,我们将为 MNIST 数据集构建一个深度神经网络,用于多类分类器。
练习 2.07:使用 Keras 在 MNIST 上实现深度神经网络
在这个练习中,我们将通过实现一个深度神经网络(多层)来对 MNIST 数据集进行多类分类,其中输入层包含 28 × 28 的像素图像,展平成 784 个输入节点,后面有 2 个隐藏层,第一个隐藏层有 50 个神经元,第二个隐藏层有 20 个神经元。最后,会有一个 Softmax 层,包含 10 个神经元,因为我们要将手写数字分类为 10 个类别:
-
导入所需的库和包:
import tensorflow as tf import pandas as pd import matplotlib.pyplot as plt %matplotlib inline # Import Keras libraries from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense from tensorflow.keras.layers import Flatten -
加载 MNIST 数据:
mnist = tf.keras.datasets.mnist (train_features,train_labels), (test_features,test_labels) = \ mnist.load_data()train_features包含的是 28 x 28 像素值形式的训练图像。train_labels包含训练标签。类似地,test_features包含 28 x 28 像素值形式的测试图像,test_labels包含测试标签。 -
对数据进行归一化:
train_features, test_features = train_features / 255.0, \ test_features / 255.0图像的像素值范围为 0-255。我们需要通过将它们除以 255 来对这些值进行归一化,使其范围从 0 到 1。
-
构建
sequential模型:model = Sequential() model.add(Flatten(input_shape=(28,28))) model.add(Dense(units = 50, activation = 'relu')) model.add(Dense(units = 20 , activation = 'relu')) model.add(Dense(units = 10, activation = 'softmax'))有几点需要注意。首先,在这个案例中,第一层实际上并不是一层神经元,而是一个
Flatten函数。它将 28 x 28 的图像展平成一个包含784的一维数组,这个数组会输入到第一个隐藏层,隐藏层有50个神经元。最后一层有10个神经元,对应 10 个类别,并使用softmax激活函数。 -
使用
compile方法提供训练参数:model.compile(optimizer = 'adam', \ loss = 'sparse_categorical_crossentropy', \ metrics = ['accuracy'])注意
这里使用的损失函数与二分类器不同。对于多分类器,使用以下损失函数:
sparse_categorical_crossentropy,当标签未经过 one-hot 编码时使用,如本例所示;以及categorical_crossentropy,当标签已进行 one-hot 编码时使用。 -
使用
summary函数检查模型配置:model.summary()输出如下:
![图 2.33:深度神经网络摘要]()
](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_02_33.jpg)
图 2.33:深度神经网络摘要
在模型摘要中,我们可以看到总共有 40,480 个参数——权重和偏差——需要在隐藏层到输出层之间进行学习。
-
通过调用
fit方法来训练模型:model.fit(train_features, train_labels, epochs=50)输出将如下所示:
![图 2.34:深度神经网络训练日志]()
](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_02_34.jpg)
图 2.34:深度神经网络训练日志
-
通过调用
evaluate()函数来测试模型:model.evaluate(test_features, test_labels)输出将是:
10000/10000 [==============================] - 1s 76us/sample - loss: 0.2072 - accuracy: 0.9718 [0.20719025060918111, 0.9718]现在模型已经训练并测试完成,在接下来的几个步骤中,我们将用一些随机选择的图像进行预测。
-
从测试数据集中加载一张随机图像。我们选择第 200 张图像:
loc = 200 test_image = test_features[loc] -
让我们使用以下命令查看图像的形状:
test_image.shape输出结果为:
(28,28)我们可以看到图像的形状是 28 x 28。然而,模型期望的是三维输入。我们需要相应地重塑图像。
-
使用以下代码来重塑图像:
test_image = test_image.reshape(1,28,28) -
让我们调用模型的
predict()方法,并将输出存储在一个名为result的变量中:result = model.predict(test_image) print(result)result以 10 个概率值的形式输出,如下所示:[[2.9072076e-28 2.1215850e-29 1.7854708e-21 1.0000000e+00 0.0000000e+00 1.2384960e-15 1.2660366e-34 1.7712217e-32 1.7461657e-08 9.6417470e-29]] -
最高值的位置就是预测结果。让我们使用在上一章中学到的
argmax函数来查找预测结果:result.argmax()在这个例子中,它是
3:3 -
为了检查预测是否正确,我们检查相应图像的标签:
test_labels[loc]再次,值是
3:3 -
我们也可以使用
pyplot可视化图像:plt.imshow(test_features[loc])输出将如下所示:
![图 2.35:测试图像可视化]()
图 2.35:测试图像可视化
这表明预测是正确的。
注意
要访问该部分的源代码,请参考packt.live/2O5KRgd。
你也可以在packt.live/2O8JHR0上在线运行这个示例。你必须执行整个 Notebook 才能获得期望的结果。
在本练习中,我们使用 Keras 创建了一个多层多类神经网络模型,用于对 MNIST 数据进行分类。通过我们构建的模型,我们能够正确预测一个随机的手写数字。
探索神经网络的优化器和超参数
训练神经网络以获得良好的预测结果需要调整许多超参数,例如优化器、激活函数、隐藏层的数量、每层神经元的数量、训练轮次和学习率。让我们逐一讨论每一个超参数,并详细解释它们。
梯度下降优化器
在之前名为TensorFlow 中的感知机训练过程的部分中,我们简要提到了梯度下降优化器,但没有深入讨论其工作原理。现在是时候稍微详细了解一下梯度下降优化器了。我们将提供一个直观的解释,而不涉及数学细节。
梯度下降优化器的作用是最小化损失或误差。为了理解梯度下降是如何工作的,可以这样类比:想象一个人站在山顶,想要到达山脚。训练开始时,损失很大,就像山顶的高度。优化器的工作就像这个人从山顶走到山谷底部,或者说,走到山的最低点,而不是走到山的另一边。
记得我们在创建优化器时使用的学习率参数吗?它可以与人们下坡时采取的步伐大小进行比较。如果这些步伐很大,刚开始时是没问题的,因为这样可以更快地下坡,但一旦接近山谷底部,如果步伐过大,就会跨过山谷的另一边。然后,为了重新下到山谷底部,这个人会尝试回到原地,但又会再次跨越到另一边。结果就是在两边来回移动,始终无法到达山谷底部。
另一方面,如果一个人采取非常小的步伐(非常小的学习率),他们将永远无法到达山谷底部;换句话说,模型将永远无法收敛。因此,找到一个既不太小也不太大的学习率非常重要。然而,不幸的是,目前没有一种经验法则可以提前知道正确的值应该是多少——我们只能通过试验和错误来找到它。
梯度基优化器主要有两种类型:批量梯度下降和随机梯度下降。在我们深入讨论这两种之前,先回顾一下一个训练周期(epoch)的含义:它表示神经网络遍历所有训练样本的一次训练迭代。
-
在一个训练周期内,当我们减少所有训练样本的损失时,这就是批量梯度下降。它也被称为全批量梯度下降。简单来说,遍历完一个完整批次后,我们会采取一步来调整网络的权重和偏置,以减少损失并改善预测。还有一种类似的方法叫做小批量梯度下降,它是在遍历数据集的一个子集后调整权重和偏置的过程。
-
与批量梯度下降不同,当我们每次迭代只取一个样本时,就有了随机梯度下降(SGD)。随机一词告诉我们这里涉及随机性,在这种情况下,就是随机选择的批量。
尽管随机梯度下降(SGD)相对有效,但还有一些先进的优化器可以加速训练过程。它们包括带动量的 SGD、Adagrad 和 Adam。
消失梯度问题
在感知机训练部分,我们了解了神经网络的前向传播和反向传播。当神经网络进行前向传播时,误差梯度是相对于真实标签计算的,之后进行反向传播,查看神经网络的哪些参数(权重和偏置)对误差的贡献以及贡献的大小。误差梯度从输出层传播到输入层,计算每个参数的梯度,最后一步是执行梯度下降步骤,根据计算得到的梯度调整权重和偏置。随着误差梯度的反向传播,计算得出的每个参数的梯度逐渐变小,直到更低(初始)层次。这种梯度的减小意味着权重和偏置的变化也变得越来越小。因此,我们的神经网络很难找到全局最小值,并且结果不好。这就是所谓的梯度消失问题。这个问题出现在使用 sigmoid(逻辑)函数作为激活函数时,因此我们使用 ReLU 激活函数来训练深度神经网络模型,以避免梯度问题并改善结果。
超参数调优
像机器学习中的其他模型训练过程一样,我们可以进行超参数调优,以提高神经网络模型的性能。其中一个参数是学习率。其他参数如下:
-
迭代次数:增加迭代次数通常能提高准确性并降低损失
-
层数:增加层数可以提高准确性,正如我们在 MNIST 练习中看到的那样
-
每层的神经元数量:这也会提高准确性
再次强调,我们无法事先知道正确的层数或每层神经元的数量。必须通过试错法来找出。需要注意的是,层数越多,每层的神经元数量越多,所需的计算能力就越大。因此,我们从最小的数字开始,逐步增加层数和神经元数量。
过拟合与 Dropout
神经网络具有复杂的架构和过多的参数,容易在所有数据点上进行过拟合,包括噪声标签,从而导致过拟合问题,并使神经网络无法在未见过的数据集上进行良好的泛化。为了解决这个问题,有一种技术叫做dropout:

图 2.36:Dropout 说明
在该技术中,训练过程中会随机停用一定数量的神经元。要停用的神经元数量通过百分比的形式作为参数提供。例如,Dropout = .2 表示该层中 20% 的神经元将在训练过程中随机停用。相同的神经元不会被多次停用,而是在每个训练周期中停用不同的神经元。然而,在测试过程中,所有神经元都会被激活。
下面是我们如何使用 Keras 将 Dropout 添加到神经网络模型中的示例:
model.add(Dense(units = 300, activation = 'relu')) #Hidden layer1
model.add(Dense(units = 200, activation = 'relu')) #Hidden Layer2
model.add(Dropout(.20))
model.add(Dense(units = 100, activation = 'relu')) #Hidden Layer3
在这种情况下,Hidden Layer2 添加了 20% 的 dropout。并不需要将 dropout 添加到所有层。作为数据科学家,您可以进行实验并决定 dropout 值应该是多少,以及需要多少层。
注意
有关 dropout 的更详细解释可以在 Nitish Srivastava 等人的论文中找到,点击此链接即可查看:www.jmlr.org/papers/volume15/srivastava14a/srivastava14a.pdf。
随着我们进入本章的尾声,让我们通过以下活动来测试目前为止学到的内容。
活动 2.01:构建一个多层神经网络来分类声纳信号
在此活动中,我们将使用 Sonar 数据集(archive.ics.uci.edu/ml/datasets/Connectionist+Bench+(Sonar,+Mines+vs.+Rocks)),该数据集通过将声纳信号以不同角度和条件反射到金属圆柱体上获得。您将构建一个基于神经网络的分类器,用于区分从金属圆柱体反射回来的声纳信号(矿物类)和从大致圆柱形的岩石反射回来的声纳信号(岩石类)。我们建议使用 Keras API 来使您的代码更加易读和模块化,这样您可以轻松地尝试不同的参数:
注意
您可以通过以下链接下载 sonar 数据集:packt.live/31Xtm9M。
-
第一步是理解数据,以便您能确定这是二分类问题还是多分类问题。
-
一旦您理解了数据和需要执行的分类类型,下一步就是网络配置:神经元的数量、隐藏层的数量、使用哪个激活函数等等。
回顾我们迄今为止讨论的网络配置步骤。让我们再强调一个关键点——激活函数部分:对于输出(最后一层),我们使用 sigmoid 进行二分类,使用 Softmax 进行多分类。
-
打开
sonar.csv文件以探索数据集,并查看目标变量是什么。 -
分离输入特征和目标变量。
-
对数据进行预处理,使其兼容神经网络。提示:one-hot 编码。
-
使用 Keras 定义一个神经网络,并使用正确的损失函数进行编译。
-
打印出模型总结以验证网络参数和 注意事项。
你应通过使用这些步骤设计一个合适的多层神经网络,以期获得 95%以上的准确率。
注意
本活动的详细步骤、解决方案和额外评论已在第 390 页呈现。
总结
在本章中,我们首先研究了生物神经元,然后转向人工神经元。我们了解了神经网络的工作原理,并采用实践的方法构建了单层和多层神经网络来解决监督学习任务。我们研究了感知机的工作原理,它是神经网络的一个单元,直到可以进行多类分类的深度神经网络。我们看到 Keras 如何使得使用最少的代码轻松创建深度神经网络。最后,我们研究了在构建成功的神经网络时需要考虑的实际因素,这涉及了诸如梯度下降优化器、过拟合和 Dropout 等重要概念。
在下一章,我们将进入更高层次,构建一个更复杂的神经网络,称为 CNN,它广泛应用于图像识别。
第三章:3. 卷积神经网络(CNNs)进行图像分类
介绍
在本章中,我们将学习卷积神经网络(CNNs)和图像分类。首先,我们将介绍 CNN 的架构以及如何实现它们。接着,我们将通过实践,使用 TensorFlow 开发图像分类器。最后,我们将讨论迁移学习和微调的概念,并了解如何使用最先进的算法。
到本章结束时,你将对 CNN 有一个清晰的理解,并了解如何使用 TensorFlow 进行编程。
介绍
在前几章中,我们学习了传统的神经网络和一些模型,比如感知机。我们了解了如何在结构化数据上训练这些模型,以进行回归或分类任务。现在,我们将学习如何将这些模型应用扩展到计算机视觉领域。
不久前,计算机被认为是只能处理明确定义和逻辑任务的计算引擎。另一方面,人类则更加复杂,因为我们拥有五种基本感官,帮助我们看事物、听声音、触摸物体、品尝食物和闻气味。计算机只是可以进行大量逻辑操作的计算器,但它们无法处理复杂的数据。与人类的能力相比,计算机有着明显的局限性。
曾经有一些原始的尝试,通过处理和分析数字图像来“赋予计算机视觉”。这个领域被称为计算机视觉。但直到深度学习的出现,我们才看到了一些令人难以置信的进展和成果。如今,计算机视觉领域已经取得了如此显著的进展,以至于在某些情况下,计算机视觉 AI 系统能够比人类更快、更准确地处理和解释某些类型的图像。你可能听说过这样一个实验:中国的 15 位医生与 BioMind AI 公司的深度学习系统进行比赛,试图从 X 光片中识别脑肿瘤。AI 系统用了 15 分钟准确预测了 225 张输入图像中的 87%,而医生们用了 30 分钟,在同一组图像上获得了 66%的准确率。
我们都听说过自动驾驶汽车,它们可以根据交通状况自动做出正确决策,或者无人机可以检测到鲨鱼并自动向救生员发送警报。所有这些令人惊叹的应用都要归功于 CNN 的最新发展。
计算机视觉可以分为四个不同的领域:
-
图像分类,我们需要在图像中识别主要物体。
-
图像分类和定位,我们需要在图像中识别并用边界框定位主要物体。
-
目标检测,我们需要在图像中识别多个物体并用边界框进行标注。
-
图像分割,我们需要识别图像中物体的边界。
下图显示了四个领域之间的差异:

图 3.1:计算机视觉四个领域之间的差异
本章我们将只讨论图像分类,它是卷积神经网络(CNN)最广泛应用的领域。这包括车牌识别、手机拍摄图片的自动分类,或为搜索引擎在图像数据库中创建元数据等内容。
注意
如果您正在阅读本书的印刷版,可以通过访问以下链接下载并浏览本章中部分图像的彩色版本:packt.live/2ZUu5G2
数字图像
人类通过眼睛看到事物,将光转化为电信号,然后由大脑处理。但计算机没有物理眼睛来捕捉光线。它们只能处理由位(0 或 1)组成的数字信息。因此,为了“看到”,计算机需要图像的数字化版本。
数字图像是由二维像素矩阵构成的。对于灰度图像,每个像素的值介于 0 到 255 之间,表示其强度或灰度级别。数字图像可以由一个通道(用于黑白图像)或三个通道(红、蓝、绿通道,用于彩色图像)组成:

图 3.2:图像的数字表示
数字图像的特点是其尺寸(高度、宽度和通道):
-
高度: 表示垂直方向上的像素数量。
-
宽度: 表示水平方向上的像素数量。
-
通道: 表示通道的数量。如果只有一个通道,图像将是灰度图。如果有三个通道,图像将是彩色图。
以下数字图像的尺寸为(512,512,3)。

图 3.3:数字图像的尺寸
图像处理
现在我们知道了数字图像是如何表示的,接下来讨论计算机如何利用这些信息找到用于分类图像或定位物体的模式。因此,为了从图像中获取任何有用或可操作的信息,计算机必须将图像解析为可识别或已知的模式。与任何机器学习算法一样,计算机视觉需要一些特征来学习模式。
与结构化数据不同,结构化数据中的每个特征在预先定义并存储在不同的列中,而图像并没有遵循任何特定模式。例如,无法说第三行总是包含动物的眼睛,或者左下角总是表示一个红色的圆形物体。图像可以是任何东西,并不遵循任何结构。因此,它们被视为非结构化数据。
然而,图像确实包含特征。它们包含不同的形状(线条、圆圈、矩形等)、颜色(红色、蓝色、橙色、黄色等)以及与不同类型物体相关的特定特征(头发、车轮、叶子等)。我们的眼睛和大脑可以轻松分析和解释所有这些特征,并识别图像中的对象。因此,我们需要为计算机模拟相同的分析过程。这就是图像滤波器(也称为卷积核)发挥作用的地方。
图像滤波器是专用于检测定义模式的小型矩阵。例如,我们可以有一个仅检测垂直线条的滤波器,另一个仅用于水平线条。计算机视觉系统在图像的每个部分运行这些滤波器,并生成一个新图像,突出显示检测到的模式。这类生成的图像称为特征图。使用边缘检测滤波器的特征图示例如下图所示:

图 3.4:垂直边缘特征图示例
这些滤波器广泛应用于图像处理中。如果您以前使用过 Adobe Photoshop(或任何其他图像处理工具),您很可能已经使用过诸如高斯和锐化之类的滤镜。
卷积运算
现在我们了解了图像处理的基础知识,我们可以开始我们的 CNN 之旅。正如我们之前提到的,计算机视觉依赖于将滤波器应用于图像的像素,以识别不同的模式或特征并生成特征图。但是这些滤波器是如何应用到图像的像素上的呢?您可以猜测这背后有某种数学操作,您是完全正确的。这个操作被称为卷积。
卷积操作由两个阶段组成:
-
两个矩阵的逐元素乘积
-
矩阵元素的总和
让我们看一个如何对矩阵 A 和 B 进行卷积的示例:

图 3.5:矩阵示例
首先,我们需要对矩阵 A 和 B 进行逐元素乘法。结果将得到另一个矩阵 C,其数值如下:
-
第一行,第一列:5 × 1 = 5
-
第一行,第二列:10 × 0 = 0
-
第一行,第三列:15 × (-1) = -15
-
第二行,第一列:10 × 2 = 20
-
第二行,第二列:20 × 0 = 0
-
第二行,第三列:30 × (-2) = -60
-
第三行,第一列:100 × 1 = 100
-
第三行,第二列:150 × 0 = 0
-
第三行,第三列:200 × (-1) = -200
注意
逐元素乘法与标准矩阵乘法不同,后者在行和列级别操作而不是在每个元素上操作。
最后,我们只需对矩阵 C 的所有元素进行求和,得到如下结果:
5+0-15+20+0-60+100+0-200 = -150
在矩阵 A 和 B 的整个卷积操作的最终结果如下图所示,为-150:

图 3.6:卷积操作的顺序
在这个例子中,矩阵 B 实际上是一个叫做 Sobel 的滤波器(或卷积核),用于检测垂直线条(还有一个变体用于检测水平线条)。矩阵 A 将是图像的一部分,其尺寸与滤波器相同(这是执行逐元素相乘所必须的)。
注意
滤波器通常是一个方阵,例如(3,3)或(5,5)。
对于 CNN,滤波器实际上是训练过程中学习到的参数(即由 CNN 定义的)。因此,将要使用的每个滤波器的值将由 CNN 本身设置。这是我们在学习如何训练 CNN 之前需要了解的一个重要概念。
练习 3.01:实现卷积操作
在本次练习中,我们将使用 TensorFlow 对两个矩阵:[[1,2,3],[4,5,6],[7,8,9]]和[[1,0,-1],[1,0,-1],[1,0,-1]]实现卷积操作。请按照以下步骤完成此练习:
-
打开一个新的 Jupyter Notebook 文件,并命名为
Exercise 3.01。 -
导入
tensorflow库:import tensorflow as tf -
从第一个矩阵
([[1,2,3],[4,5,6],[7,8,9]])创建一个名为A的张量,并打印它的值:A = tf.Variable([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) A输出将如下所示:
<tf.Variable 'Variable:0' shape=(3, 3) dtype=int32, numpy=array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])> -
从第一个矩阵
([[1,0,-1],[1,0,-1],[1,0,-1]])创建一个名为B的张量,并打印它的值:B = tf.Variable([[1, 0, -1], [1, 0, -1], [1, 0, -1]]) B输出将如下所示:
<tf.Variable 'Variable:0' shape=(3, 3) dtype=int32, numpy=array([[ 1, 0, -1], [ 1, 0, -1], [ 1, 0, -1]])> -
使用
tf.math.multiply()对A和B进行逐元素相乘。将结果保存到mult_out并打印出来:mult_out = tf.math.multiply(A, B) mult_out预期的输出将如下所示:
<tf.Tensor: id=19, shape=(3, 3), dtype=int32, numpy=array([[ 1, 0, -3], [ 4, 0, -6], [ 7, 0, -9]])> -
使用
tf.math.reduce_sum()对mult_out进行逐元素求和。将结果保存到conv_out并打印出来:conv_out = tf.math.reduce_sum(mult_out) conv_out预期的输出将如下所示:
<tf.Tensor: id=21, shape=(), dtype=int32, numpy=-6>对两个矩阵
[[1,2,3],[4,5,6],[7,8,9]]和[[1,0,-1],[1,0,-1],[1,0,-1]]进行卷积操作的结果是-6。注意
要访问此特定部分的源代码,请参考
packt.live/320pEfC。你也可以在
packt.live/2ZdeLFr上在线运行此示例。你必须执行整个 Notebook 才能得到期望的结果。
在本次练习中,我们使用了 TensorFlow 的内置函数对两个矩阵进行了卷积操作。
步幅
到目前为止,我们已经学会了如何执行单次卷积操作。我们了解到,卷积操作使用一个特定大小的滤波器,例如(3, 3),即 3×3,并将其应用于相似大小的图像部分。如果我们有一个大图像,比如(512, 512)大小,我们实际上只需要看图像的一个非常小的部分。
在一次次处理图像的过程中,我们需要对整个图像空间进行相同的卷积操作。为此,我们将应用一个叫做滑动的技术。顾名思义,滑动就是将滤波器应用到上次卷积操作的相邻区域:我们只需要滑动滤波器并继续应用卷积。
如果我们从图像的左上角开始,我们可以每次将滤波器滑动一个像素到右边。当我们到达右边缘时,可以将滤波器向下滑动一个像素。我们重复这个滑动操作,直到对图像的整个区域应用卷积:

图 3.7:步长示例
我们不仅可以每次滑动 1 个像素,还可以选择更大的滑动窗口,比如 2 个或 3 个像素。定义此滑动窗口大小的参数称为步长(stride)。步长值越大,重叠的像素就越少,但得到的特征图尺寸会更小,因此会丢失一些信息。
在前面的示例中,我们在一幅水平分割的图像上应用了 Sobel 滤波器,左边有深色值,右边有白色值。得到的特征图中间有较高的值(800),这表示 Sobel 滤波器在该区域找到了垂直线。这就是滑动卷积如何帮助检测图像中特定模式的方式。
填充
在前一节中,我们学习了如何通过像素滑动使滤波器遍历图像的所有像素。结合卷积操作,这个过程有助于检测图像中的模式(即提取特征)。
对图像应用卷积将导致一个特征图,其尺寸比输入图像小。可以使用一种叫做填充(padding)的技术,确保特征图与输入图像具有相同的尺寸。它通过在图像边缘添加一个像素值为0的像素层来实现:

图 3.8:填充示例
在前面的示例中,输入图像的尺寸是(6,6)。一旦填充,它的尺寸增加到(8,8)。现在,我们可以使用大小为(3,3)的滤波器在其上应用卷积:

图 3.9:填充卷积示例
对填充图像进行卷积后的结果图像,其尺寸为(6,6),这与原始输入图像的尺寸完全相同。得到的特征图在图像的中间有较高的值,正如前面的未填充示例一样。因此,滤波器仍然可以在图像中找到相同的模式。但现在你可能会注意到,在左边缘有非常低的值(-800)。这实际上是可以接受的,因为较低的值意味着滤波器在该区域没有找到任何模式。
以下公式可以用来计算卷积后特征图的输出尺寸:

图 3.10:计算特征图输出尺寸的公式
在这里,我们有以下内容:
-
w:输入图像的宽度 -
h:输入图像的高度 -
p:每个边缘用于填充的像素数量 -
f:滤波器大小 -
s:步幅中的像素数
让我们将这个公式应用到前面的例子中:
-
w= 6 -
h= 6 -
p= 1 -
f= 3 -
s= 1
然后,按照以下方式计算输出维度:

图 3.11:输出—特征图的维度
所以,结果特征图的维度是(6,6)。
卷积神经网络
在第二章,神经网络中,你学习了传统的神经网络,比如感知机,这些网络由全连接层(也叫稠密层)组成。每一层由执行矩阵乘法的神经元组成,然后进行非线性变换,使用激活函数。
CNN 实际上与传统的神经网络非常相似,但它们使用的是卷积层,而不是全连接层。每个卷积层会有一定数量的滤波器(或称为核),这些滤波器会对输入图像执行卷积操作,步幅固定,可以选择是否填充,并且可以在后面跟上激活函数。
CNN 广泛应用于图像分类,在这种情况下,网络需要预测给定输入的正确类别。这与传统机器学习算法中的分类问题完全相同。如果输出只能来自两个不同的类别,那么就是二分类,例如识别狗与猫。如果输出可以是多个类别,那么就是多分类,例如识别 20 种不同的水果。
为了进行这样的预测,CNN 模型的最后一层需要是一个全连接层,并根据预测问题的类型使用相应的激活函数。你可以使用以下激活函数列表作为经验法则:

图 3.12:激活函数列表
为了更好地理解其结构,下面是一个简单 CNN 模型的示意图:

图 3.13:简单 CNN 模型的结构
我们已经学到了很多关于卷积神经网络(CNN)的知识。在开始第一个练习之前,我们还需要了解一个概念,以便缩短 CNN 的训练时间:池化层。
池化层
池化层用于减少卷积层特征图的维度。那么,为什么我们需要进行这种下采样呢?一个主要原因是减少网络中计算的数量。添加多层不同滤波器的卷积操作会显著影响训练时间。此外,减少特征图的维度可以消除一些噪声,帮助我们专注于检测到的模式。通常,我们会在每个卷积层后面添加池化层,以减少特征图的大小。
池化操作的作用与滤波器非常相似,但与卷积操作不同,它使用诸如平均值或最大值(最大值是当前 CNN 架构中最广泛使用的函数)等聚合函数。例如,最大池化会查看特征图的一个特定区域,并找出其像素的最大值。然后,它会执行一个步幅操作,找到邻近像素中的最大值。它会重复这个过程,直到处理完整个图像:

图 3.14:在输入图像上使用步幅为 2 的最大池化
在上述示例中,我们使用了大小为(2,2)、步幅为 2 的最大池化(这是最常用的池化函数)。我们查看特征图的左上角,并从像素值 6、8、1 和 2 中找出最大值,得到结果 8。然后,我们按照步幅 2 滑动最大池化,针对像素值 6、1、7 和 4 执行相同的操作。我们重复对底部组的相同操作,得到了一个大小为(2,2)的新特征图。
带有最大池化的 CNN 模型将如下所示:

图 3.15:带有最大池化的 CNN 架构示例
例如,上述模型可用于识别手写数字(从 0 到 9)。这个模型有三层卷积层,后面跟着一个最大池化层。最后的几层是全连接层,负责做出检测到的数字的预测。
添加池化层的开销远小于计算卷积。这就是它们能加速训练时间的原因。
使用 TensorFlow 和 Keras 的 CNN
到目前为止,你已经了解了很多关于 CNN 如何在背后工作的信息。现在,终于到了展示如何实现我们所学的内容的时候了。我们将使用 TensorFlow 2.0 中的 Keras API。
Keras API 提供了一个高层次的 API,用于构建你自己的 CNN 架构。我们来看看我们将在 CNN 中使用的主要类。
首先,为了创建一个卷积层,我们需要实例化一个Conv2D()类,并指定卷积核的数量、大小、步幅、填充方式和激活函数:
from tensorflow.keras import layers
layers.Conv2D(64, kernel_size=(3, 3), stride=(2,2), \
padding="same", activation="relu")
在前面的示例中,我们创建了一个卷积层,使用了64个大小为(3, 3)的卷积核,步幅为2,填充方式为same,使得输出维度与输入维度相同,并且激活函数为 ReLU。
注意
你可以通过访问 TensorFlow 的文档网站,了解更多关于这个类的信息:www.tensorflow.org/api_docs/python/tf/keras/layers/Conv2D
为了添加一个最大池化层,你需要使用MaxPool2D()类,并指定其维度和步幅,如下所示的代码片段:
from tensorflow.keras import layers
layers.MaxPool2D(pool_size=(3, 3), strides=1)
在上面的代码片段中,我们实例化了一个大小为(3,3),步幅为1的最大池化层。
注意
你可以通过访问 TensorFlow 的文档网站了解更多关于这个类的信息:www.tensorflow.org/api_docs/python/tf/keras/layers/MaxPool2D
对于全连接层,我们将使用Dense()类并指定单元数量和激活函数:
from tensorflow.keras import layers
layers.Dense(units=1, activation='sigmoid')
上述代码向我们展示了如何创建一个具有1个输出单元并使用sigmoid作为激活函数的全连接层。
最后,在操作输入数据时,我们可能需要在将其输入到 CNN 模型之前更改其维度。如果我们使用 NumPy 数组,可以使用reshape方法(如在第一章,深度学习的构建模块中所示),具体如下:
features_train.reshape(60000, 28, 28, 1)
在这里,我们已经将features_train的维度转换为(60000, 28, 28, 1),这对应于格式(观察数量,高度,宽度,通道)。在处理灰度图像时,需要添加通道维度。本例中,灰度图像的维度(28,28)将被重塑为(28,28,1),总共有60000张图像。
在 TensorFlow 中,你可以使用reshape方法,如下所示:
from tensorflow.keras import layers
layers.Reshape((60000, 28, 28, 1))
现在我们已经学习了如何在 TensorFlow 中设计一个 CNN,接下来是将这些知识应用于著名的 MNIST 数据集。
注意
你可以通过访问 TensorFlow 的文档网站了解更多关于 Reshape 的信息:www.tensorflow.org/api_docs/python/tf/keras/layers/Reshape
练习 3.02:使用 KERAS 和 CNN 识别手写数字(MNIST)
在本练习中,我们将处理 MNIST 数据集(在第二章,神经网络中我们已经用过它),该数据集包含手写数字的图像。不过这一次,我们将使用 CNN 模型。这个数据集最初由深度学习领域最著名的研究者之一 Yann Lecun 分享。我们将构建一个 CNN 模型,并训练它识别手写数字。这个 CNN 模型将由两个卷积层组成,每个卷积层有 64 个卷积核,后面接着两个全连接层,分别包含 128 和 10 个单元。
TensorFlow 直接通过其 API 提供此数据集。执行以下步骤以完成此练习:
注意
你可以通过访问 TensorFlow 的网站了解更多关于这个数据集的信息:www.tensorflow.org/datasets/catalog/mnist
-
打开一个新的 Jupyter Notebook 文件,并将其命名为
Exercise 3.02。 -
导入
tensorflow.keras.datasets.mnist为mnist:import tensorflow.keras.datasets.mnist as mnist -
使用
mnist.load_data()加载mnist数据集,并将结果保存到(features_train, label_train), (features_test, label_test)中:(features_train, label_train), (features_test, label_test) = \ mnist.load_data() -
打印
label_train的内容:label_train预期输出将如下所示:
array([5, 0, 4, ..., 5, 6, 8], dtype=uint8)标签列包含与 10 个手写数字对应的数字值:
0到9。 -
打印训练集的形状:
features_train.shape预期输出如下所示:
(60000, 28, 28)训练集由
60000个形状为28×28的观测数据组成。 -
打印测试集的
shape:features_test.shape预期输出如下所示:
(10000, 28, 28)测试集由
10000个形状为28×28的观测数据组成。 -
将训练集和测试集的形状调整为
(number_observations, 28, 28, 1):features_train = features_train.reshape(60000, 28, 28, 1) features_test = features_test.reshape(10000, 28, 28, 1) -
通过将
features_train和features_test除以255来标准化它们:features_train = features_train / 255.0 features_test = features_test / 255.0 -
导入
numpy作为np,tensorflow作为tf,并从tensorflow.keras导入layers:import numpy as np import tensorflow as tf from tensorflow.keras import layers -
使用
np.random_seed()和tf.random.set_seed()分别将8设为numpy和tensorflow的种子:np.random.seed(8) tf.random.set_seed(8)注意
设置种子后,结果可能仍然会略有不同。
-
实例化一个
tf.keras.Sequential()类,并将其保存到名为model的变量中:model = tf.keras.Sequential() -
实例化一个
layers.Conv2D()类,设定64个形状为(3,3)的卷积核,activation='relu',input_shape=(28,28,1),并将其保存到名为conv_layer1的变量中:conv_layer1 = layers.Conv2D(64, (3,3), activation='relu', \ input_shape=(28, 28, 1)) -
实例化一个
layers.Conv2D()类,设定64个形状为(3,3)的卷积核,activation='relu',并将其保存到名为conv_layer2的变量中:conv_layer2 = layers.Conv2D(64, (3,3), activation='relu') -
实例化一个
layers.Flatten()类,设定128个神经元,activation='relu',并将其保存到名为fc_layer1的变量中:fc_layer1 = layers.Dense(128, activation='relu') -
实例化一个
layers.Flatten()类,设定10个神经元,activation='softmax',并将其保存到名为fc_layer2的变量中:fc_layer2 = layers.Dense(10, activation='softmax') -
使用
.add()将你刚刚定义的四个层添加到模型中,在每个卷积层之间添加一个大小为(2,2)的MaxPooling2D()层,并在第一个全连接层之前添加一个Flatten()层来展平特征图:model.add(conv_layer1) model.add(layers.MaxPooling2D(2, 2)) model.add(conv_layer2) model.add(layers.MaxPooling2D(2, 2)) model.add(layers.Flatten()) model.add(fc_layer1) model.add(fc_layer2) -
实例化一个
tf.keras.optimizers.Adam()类,设置学习率为0.001,并将其保存到名为optimizer的变量中:optimizer = tf.keras.optimizers.Adam(0.001) -
使用
.compile()编译神经网络,设置loss='sparse_categorical_crossentropy',optimizer=optimizer,metrics=['accuracy']:model.compile(loss='sparse_categorical_crossentropy', \ optimizer=optimizer, metrics=['accuracy']) -
打印模型摘要:
model.summary()预期输出如下所示:
![图 3.16:模型摘要]()
图 3.16:模型摘要
上述摘要显示该模型有超过 240,000 个需要优化的参数。
-
使用训练集拟合神经网络,并指定
epochs=5,validation_split=0.2,verbose=2:model.fit(features_train, label_train, epochs=5,\ validation_split = 0.2, verbose=2)预期输出如下所示:
![图 3.17:训练输出]()
图 3.17:训练输出
我们在 48,000 个样本上训练了我们的 CNN,并使用了 12,000 个样本作为验证集。在训练了五个 epoch 后,我们在训练集上获得了
0.9951的准确率,在验证集上获得了0.9886的准确率。我们的模型有些过拟合。 -
让我们评估模型在测试集上的表现:
model.evaluate(features_test, label_test)预期输出如下所示:
10000/10000 [==============================] - 1s 86us/sample - loss: 0.0312 - accuracy: 0.9903 [0.03115778577708088, 0.9903]通过这个,我们在测试集上达到了
0.9903的准确率。注意
要访问此特定部分的源代码,请参考
packt.live/2W2VLYl。您还可以在
packt.live/3iKAVGZ上在线运行此示例。您必须执行整个 Notebook 才能获得期望的结果。
在本次练习中,我们设计并训练了一个 CNN 架构,用于识别来自 MNIST 数据集的手写数字图像,并取得了几乎完美的成绩。
数据生成器
在之前的练习中,我们在 MNIST 数据集上构建了第一个多类 CNN 分类器。我们将整个数据集加载到模型中,因为数据集并不大。但是对于更大的数据集,我们无法这样做。幸运的是,Keras 提供了一个名为 data generator 的 API,允许我们以批次的方式加载和转换数据。
数据生成器在图像分类中也非常有用。有时,图像数据集以文件夹的形式存在,并且为训练集、测试集和不同的类别预定义了结构(属于同一类别的所有图像将存储在同一个文件夹中)。数据生成器 API 能够理解这种结构,并将相关图像和对应的信息正确地提供给 CNN 模型。这将为您节省大量时间,因为您不需要构建自定义管道来从不同的文件夹加载图像。
此外,数据生成器可以将图像划分为多个批次,并按顺序将它们提供给模型。您不必将整个数据集加载到内存中就可以进行训练。让我们看看它们是如何工作的。
首先,我们需要从 tensorflow.keras.preprocessing 中导入 ImageDataGenerator 类:
from tensorflow.keras.preprocessing.image \
import ImageDataGenerator
然后,我们可以通过提供所有想要执行的图像转换来实例化它。在以下示例中,我们将仅通过将所有训练集图像除以 255 来对图像进行归一化,这样所有像素值都将在 0 到 1 之间:
train_imggen = ImageDataGenerator(rescale=1./255)
在这一步骤中,我们将通过使用 .flow_from_directory() 方法来创建一个数据生成器,并指定训练目录的路径、batch_size、图像的 target_size、是否打乱数据以及类别类型:
train_datagen = train_imggen.\
flow_from_directory(batch_size=32, \
directory=train_dir, \
shuffle=True, \
target_size=(100, 100), \
class_mode='binary')
注意
您需要为验证集创建一个单独的数据生成器。
最后,我们可以通过提供训练集和验证集的数据生成器、训练周期数和每个周期的步数来使用 .fit_generator() 方法训练我们的模型,步数是图像总数除以批次大小(取整)的结果:
model.fit_generator(train_data_gen, \
steps_per_epoch=total_train // batch_size, \
epochs=5, validation_data=val_data_gen, \
validation_steps=total_val // batch_size)
该方法与您之前看到的 .fit() 方法非常相似,但不同之处在于,它不是一次性将整个数据集训练完,而是通过我们定义的数据生成器按批次训练图像。步数定义了处理整个数据集所需的批次数量。
数据生成器非常适合从文件夹中加载数据,并按批次将图像输入到模型中。但它们也可以执行一些数据处理操作,如以下章节所示。
练习 3.03:使用数据生成器分类猫狗图像
在本练习中,我们将使用猫狗数据集,该数据集包含狗和猫的图像。我们将为训练集和验证集构建两个数据生成器,并构建一个 CNN 模型来识别狗或猫的图像。请按照以下步骤完成本练习:
注意
我们使用的数据集是 Kaggle 猫狗数据集的修改版:www.kaggle.com/c/dogs-vs-cats/data。这个修改版只使用了 25,000 张图像的子集,并由 Google 提供,链接为storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip。
-
打开一个新的 Jupyter Notebook 文件,并将其命名为
Exercise 3.03: -
导入
tensorflow库:import tensorflow as tf -
创建一个名为
file_url的变量,包含数据集的链接:file_url = 'https://github.com/PacktWorkshops'\ '/The-Deep-Learning-Workshop/raw/master'\ '/Chapter03/Datasets/Exercise3.03'\ '/cats_and_dogs_filtered.zip'注意
在上述步骤中,我们使用的是存储在
packt.live/3jZKRNw的数据库。如果您将数据集存储在其他 URL,请相应地更改高亮的路径。注意下面字符串中的斜杠。记住,反斜杠(\)用于跨多行分隔代码,而正斜杠(/)是 URL 的一部分。 -
使用
tf.keras.get_file下载数据集,参数为'cats_and_dogs.zip'、origin=file_url、extract=True,并将结果保存到一个名为zip_dir的变量中:zip_dir = tf.keras.utils.get_file('cats_and_dogs.zip', \ origin=file_url, extract=True) -
导入
pathlib库:import pathlib -
创建一个名为
path的变量,使用pathlib.Path(zip_dir).parent获取cats_and_dogs_filtered目录的完整路径:path = pathlib.Path(zip_dir).parent / 'cats_and_dogs_filtered' -
创建两个变量,分别命名为
train_dir和validation_dir,它们分别保存训练集和验证集文件夹的完整路径:train_dir = path / 'train' validation_dir = path / 'validation' -
创建四个变量,分别命名为
train_cats_dir、train_dogs_dir、validation_cats_dir和validation_dogs_dir,它们分别保存训练集和验证集中猫狗文件夹的完整路径:train_cats_dir = train_dir / 'cats' train_dogs_dir = train_dir /'dogs' validation_cats_dir = validation_dir / 'cats' validation_dogs_dir = validation_dir / 'dogs' -
导入
os包。我们将在下一步中需要它来统计文件夹中的图像数量:import os -
创建两个变量,分别命名为
total_train和total_val,它们将获取训练集和验证集中的图像数量:total_train = len(os.listdir(train_cats_dir)) \ + len(os.listdir(train_dogs_dir)) total_val = len(os.listdir(validation_cats_dir)) \ + len(os.listdir(validation_dogs_dir)) -
从
tensorflow.keras.preprocessing导入ImageDataGenerator:from tensorflow.keras.preprocessing.image\ import ImageDataGenerator -
实例化两个
ImageDataGenerator类并命名为train_image_generator和validation_image_generator。这两个生成器将通过将图像像素值除以255来进行重缩放:train_image_generator = ImageDataGenerator(rescale=1./255) validation_image_generator = ImageDataGenerator(rescale=1./255) -
创建三个变量,分别命名为
batch_size、img_height和img_width,它们的值分别为16、100和100:batch_size = 16 img_height = 100 img_width = 100 -
使用
.flow_from_directory()创建一个名为train_data_gen的数据生成器,并指定批量大小、训练文件夹的路径、shuffle=True、目标大小为(img_height, img_width),并将类模式设置为binary:train_data_gen = train_image_generator.flow_from_directory\ (batch_size=batch_size, directory=train_dir, \ shuffle=True, \ target_size=(img_height, img_width), \ class_mode='binary') -
使用
.flow_from_directory()创建一个名为val_data_gen的数据生成器,并指定批量大小、验证文件夹的路径、shuffle=True、目标大小为(img_height, img_width),以及类别模式为binary:val_data_gen = validation_image_generator.flow_from_directory\ (batch_size=batch_size, \ directory=validation_dir, \ target_size=(img_height, img_width), \ class_mode='binary') -
导入
numpy为np,tensorflow为tf,以及从tensorflow.keras导入layers:import numpy as np import tensorflow as tf from tensorflow.keras import layers -
使用
np.random_seed()和tf.random.set_seed()分别将8(这个值完全是任意的)设置为numpy和tensorflow的seed:np.random.seed(8) tf.random.set_seed(8) -
实例化一个
tf.keras.Sequential()类到一个名为model的变量中,包含以下层:一个具有64个3形状卷积核的卷积层,ReLU作为激活函数,并指定所需的输入维度;一个最大池化层;一个具有128个3形状卷积核的卷积层,ReLU作为激活函数;一个最大池化层;一个展平层;一个具有128个单元的全连接层,ReLU作为激活函数;一个具有1个单元的全连接层,sigmoid作为激活函数。代码如下所示:
model = tf.keras.Sequential([ layers.Conv2D(64, 3, activation='relu', \ input_shape=(img_height, img_width ,3)),\ layers.MaxPooling2D(),\ layers.Conv2D(128, 3, activation='relu'),\ layers.MaxPooling2D(),\ layers.Flatten(),\ layers.Dense(128, activation='relu'),\ layers.Dense(1, activation='sigmoid')]) -
实例化一个
tf.keras.optimizers.Adam()类,学习率为0.001,并将其保存为名为optimizer的变量:optimizer = tf.keras.optimizers.Adam(0.001) -
使用
.compile()编译神经网络,设置loss='binary_crossentropy', optimizer=optimizer, metrics=['accuracy']:model.compile(loss='binary_crossentropy', \ optimizer=optimizer, metrics=['accuracy']) -
使用
.summary()打印模型摘要:model.summary()预期输出将如下所示:
![图 3.18:模型总结]()
图 3.18:模型总结
前面的摘要显示该模型有超过
8,700,000个参数需要优化。 -
使用
fit_generator()训练神经网络,并提供训练和验证数据生成器,epochs=5,每个 epoch 的步骤数,以及验证步骤数:model.fit_generator(train_data_gen, \ steps_per_epoch=total_train // batch_size, \ epochs=5, \ validation_data=val_data_gen,\ validation_steps=total_val // batch_size)预期输出将如下所示:
![图 3.19:训练输出]()
图 3.19:训练输出
注意
预期的输出将接近所示的结果。由于权重初始化时存在一些随机性,你的准确度值可能会有所不同。
我们已经训练了我们的 CNN 五个 epoch,训练集的准确率为0.85,验证集的准确率为0.7113。我们的模型过拟合严重。你可能需要尝试不同的架构进行训练,看看是否能够提高这个分数并减少过拟合。你还可以尝试给模型输入你选择的猫狗图片,并查看输出的预测结果。
注意
要访问该特定部分的源代码,请参考packt.live/31XQmp9。
你也可以在线运行这个例子,网址是packt.live/2ZW10tW。你必须执行整个 Notebook 才能得到预期的结果。
数据增强
在前面的部分中,您已经了解到数据生成器可以完成大量繁重的工作,例如从文件夹而不是列数据中为神经网络处理数据。到目前为止,我们已经看到了如何创建它们,从结构化文件夹加载数据,并按批次将数据提供给模型。我们仅对其执行了一个图像转换:重新缩放。然而,数据生成器可以执行许多其他图像转换。
但是为什么我们需要执行数据增强呢?答案非常简单:为了防止过拟合。通过进行数据增强,我们增加了数据集中的图像数量。例如,对于一张图像,我们可以生成 10 种不同的变体。因此,您的数据集大小将增加 10 倍。
此外,通过数据增强,我们拥有一组具有更广泛视觉范围的图像。例如,自拍照可以从不同角度拍摄,但是如果您的数据集只包含方向直的自拍照片,您的 CNN 模型将无法正确解释具有不同角度的其他图像。通过执行数据增强,您帮助模型更好地泛化到不同类型的图像。然而,正如您可能已经猜到的那样,它也有一个缺点:数据增强会增加训练时间,因为您需要执行额外的数据转换。
让我们快速看一下我们可以做的一些不同类型的数据增强。
水平翻转
水平翻转会返回一个水平翻转的图像:

图 3.20: 水平翻转示例
垂直翻转
垂直翻转会垂直翻转图像:

图 3.21: 垂直翻转示例
缩放
图像可以被放大,并提供不同大小的图像对象:

图 3.22: 缩放示例
水平移动
水平移动,顾名思义,将在水平轴上移动图像,但保持图像尺寸不变。通过这种转换,图像可能会被裁剪,需要生成新像素来填补空白。常见的技术是复制相邻像素或用黑色像素填充该空间:

图 3.23: 水平移动示例
垂直移动
垂直移动与水平移动类似,但是沿垂直轴:

图 3.24: 垂直移动示例
旋转
图像可以像这样以特定角度旋转:

图 3.25: 旋转示例
剪切
剪切变换是通过沿边缘轴移动图像的一边来实现的。执行此操作后,图像会从矩形变形为平行四边形:

图 3.26:剪切示例
使用 Keras,所有这些数据变换技术都可以添加到 ImageDataGenerator 中:
from tensorflow.keras.preprocessing.image import ImageDataGenerator
ImageDataGenerator(rescale=1./255, \
horizontal_flip=True, zoom_range=0.2, \
width_shift_range=0.2, \
height_shift_range=0.2, \
shear_range=0.2, rotation_range=40, \
fill_mode='nearest')
现在我们对数据增强有了大致的了解,接下来让我们看看如何在以下练习中将其应用到模型中。
练习 3.04:使用数据增强进行图像分类(CIFAR-10)
在这个练习中,我们将使用 CIFAR-10 数据集(加拿大高级研究院数据集),该数据集包含了 60,000 张属于 10 个不同类别的图像:飞机、汽车、鸟类、猫、鹿、狗、青蛙、马、船和卡车。我们将构建一个 CNN 模型并使用数据增强来识别这些类别。请按照以下步骤完成这个练习:
注意
你可以在 TensorFlow 的官方网站上了解更多关于此数据集的信息:www.tensorflow.org/api_docs/python/tf/keras/datasets/cifar10。
-
打开一个新的 Jupyter Notebook 文件,并将其命名为
Exercise 3.04。 -
导入
tensorflow.keras.datasets.cifar10:from tensorflow.keras.datasets import cifar10 -
使用
cifar10.load_data()加载 CIFAR-10 数据集,并将结果保存到(features_train, label_train), (features_test, label_test):(features_train, label_train), (features_test, label_test) = \ cifar10.load_data() -
打印
features_train的形状:features_train.shape预期输出将如下所示:
(50000, 32, 32, 3)训练集包含了
50000张尺寸为(32,32,3)的图像。 -
创建三个变量,分别命名为
batch_size、img_height和img_width,并将它们的值分别设置为16、32和32:batch_size = 16 img_height = 32 img_width = 32 -
从
tensorflow.keras.preprocessing导入ImageDataGenerator:from tensorflow.keras.preprocessing.image import ImageDataGenerator -
创建一个名为
train_img_gen的ImageDataGenerator实例,并应用数据增强:重缩放(除以 255)、width_shift_range=0.1、height_shift_range=0.1以及水平翻转:train_img_gen = ImageDataGenerator\ (rescale=1./255, width_shift_range=0.1, \ height_shift_range=0.1, horizontal_flip=True) -
创建一个名为
val_img_gen的ImageDataGenerator实例,并应用重缩放(除以 255):val_img_gen = ImageDataGenerator(rescale=1./255) -
使用
.flow()方法创建一个名为train_data_gen的数据生成器,并指定训练集的批量大小、特征和标签:train_data_gen = train_img_gen.flow\ (features_train, label_train, \ batch_size=batch_size) -
使用
.flow()方法创建一个名为val_data_gen的数据生成器,并指定测试集的批量大小、特征和标签:val_data_gen = train_img_gen.flow\ (features_test, label_test, \ batch_size=batch_size) -
导入
numpy为np,tensorflow为tf,以及从tensorflow.keras导入layers:import numpy as np import tensorflow as tf from tensorflow.keras import layers -
使用
np.random_seed()和tf.random.set_seed()设置8作为numpy和tensorflow的随机种子:np.random.seed(8) tf.random.set_seed(8) -
实例化一个
tf.keras.Sequential()类,并将其赋值给名为model的变量,使用以下层:一个具有64个3核心的卷积层,ReLU 激活函数,以及必要的输入维度;一个最大池化层;一个具有128个3核心的卷积层,ReLU 激活函数;一个最大池化层;一个展平层;一个具有128单元和 ReLU 激活函数的全连接层;一个具有10单元和 Softmax 激活函数的全连接层。代码如下所示:
model = tf.keras.Sequential([ layers.Conv2D(64, 3, activation='relu', \ input_shape=(img_height, img_width ,3)), \ layers.MaxPooling2D(), \ layers.Conv2D(128, 3, activation='relu'), \ layers.MaxPooling2D(), \ layers.Flatten(), \ layers.Dense(128, activation='relu'), \ layers.Dense(10, activation='softmax')]) -
实例化一个
tf.keras.optimizers.Adam()类,设置学习率为0.001,并将其保存为名为optimizer的变量:optimizer = tf.keras.optimizers.Adam(0.001) -
使用
.compile()编译神经网络,参数为loss='sparse_categorical_crossentropy', optimizer=optimizer, metrics=['accuracy']:model.compile(loss='sparse_categorical_crossentropy', \ optimizer=optimizer, metrics=['accuracy']) -
使用
fit_generator()训练神经网络,并提供训练和验证数据生成器,epochs=5,每个 epoch 的步数,以及验证步数:model.fit_generator(train_data_gen, \ steps_per_epoch=len(features_train) \ // batch_size, \ epochs=5, \ validation_data=val_data_gen, \ validation_steps=len(features_test) \ // batch_size)预期输出将如下所示:
![图 3.27:模型的训练日志]()
](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_03_27.jpg)
图 3.27:模型的训练日志
注意
要访问该特定部分的源代码,请参考 packt.live/31ZLyQk。
你也可以在线运行这个示例,访问 packt.live/2OcmahS。你必须执行整个 Notebook 才能获得预期的结果。
在这个练习中,我们在 5 个 epoch 上训练了我们的 CNN,并在训练集上获得了 0.6713 的准确度,在验证集上获得了 0.6582 的准确度。我们的模型略微过拟合,但准确度相当低。你可能希望尝试不同的架构,看看是否可以通过例如增加卷积层来提高这个分数。
注意
上述练习的预期输出将接近于图示(图 3.27)。由于权重初始化中的一些随机性,你可能会看到略有不同的准确度值。
活动 3.01:基于 Fashion MNIST 数据集构建多类分类器
在这个活动中,你将训练一个卷积神经网络(CNN)来识别属于 10 个不同类别的服装图像。你将应用一些数据增强技术来减少过拟合的风险。你将使用由 TensorFlow 提供的 Fashion MNIST 数据集。请执行以下步骤来完成该活动:
注意
原始数据集由 Han Xiao 分享。你可以在 TensorFlow 网站上阅读更多关于该数据集的信息:www.tensorflow.org/datasets/catalog/mnist
-
从 TensorFlow 导入 Fashion MNIST 数据集。
-
重塑训练集和测试集。
-
创建一个数据生成器,并应用以下数据增强:
rescale=1./255, rotation_range=40, width_shift_range=0.1, height_shift_range=0.1, shear_range=0.2, zoom_range=0.2, horizontal_flip=True, fill_mode='nearest' -
创建神经网络架构,包含以下层次:一个卷积层
Conv2D(64, (3,3), activation='relu'),后接MaxPooling2D(2,2);一个卷积层Conv2D(64, (3,3), activation='relu'),后接MaxPooling2D(2,2);一个展平层;一个全连接层Dense(128, activation=relu);一个全连接层Dense(10, activation='softmax')。 -
指定一个学习率为
0.001的 Adam 优化器。 -
训练模型。
-
在测试集上评估模型。
预期的输出如下:

图 3.28:模型的训练日志
训练集和验证集的预期准确率应该在0.82左右。
注意
本活动的详细步骤、解决方案及附加评论会在第 394 页展示。
保存和恢复模型
在上一节中,我们学习了如何使用数据增强技术生成图像的不同变体。这将增加数据集的大小,但也有助于模型在更多样化的图像上训练,并帮助它更好地泛化。
一旦你训练完模型,你很可能希望将其部署到生产环境中并使用它进行实时预测。为此,你需要将模型保存为文件。然后,预测服务可以加载该文件,并将其作为 API 或数据科学工具进行使用。
模型有不同的组件可以保存:
-
模型的架构,包括所有使用的网络和层
-
模型的训练权重
-
包含损失函数、优化器和度量标准的训练配置
在 TensorFlow 中,你可以保存整个模型或将这些组件分别保存。接下来我们来学习如何操作。
保存整个模型
要将所有组件保存为一个单独的文件,可以使用以下代码:
model.save_model(filepath='path_to_model/cnn_model')
要加载保存的模型,可以使用以下代码:
loaded_model = tf.keras.models.load_model\
(filepath='path_to_model/cnn_model')
仅保存架构
你可以仅保存模型的架构作为json对象。然后,你需要使用json包将其保存到文件中,如下所示的代码片段所示:
import json
config_json = model.to_json()
with open('config.json', 'w') as outfile:
json.dump(config_json, outfile)
然后,你将使用json包将其加载回来:
import json
with open('config.json') as json_file:
config_data = json.load(json_file)
loaded_model = tf.keras.models.model_from_json(config_data)
仅保存权重
你可以仅保存模型的权重,方法如下:
model.save_weights('path_to_weights/weights.h5')
然后,在实例化新模型的架构后,你将加载保存的权重:
new_model.load_weights('path_to_weights/weights.h5')
如果你希望将来进一步训练模型,这尤其有用。你可以加载保存的权重,并继续训练你的模型,进一步更新其权重。
注意
.h5 是 TensorFlow 默认使用的文件扩展名。
迁移学习
到目前为止,我们已经学到了很多关于设计和训练我们自己 CNN 模型的知识。但正如你可能已经注意到的那样,我们的一些模型表现得并不好。这可能有多个原因,例如数据集太小,或者我们的模型需要更多的训练。
但是,训练一个 CNN 需要大量的时间。如果我们能重用一个已经训练好的现有架构,那就太好了。幸运的是,确实存在这样的选项,它被称为迁移学习。TensorFlow 提供了多个在 ImageNet 数据集(超过 1400 万张图片)上训练的最先进模型的实现。
注意
你可以在 TensorFlow 文档中找到可用的预训练模型列表:www.tensorflow.org/api_docs/python/tf/keras/applications
为了使用预训练模型,我们需要导入它实现的类。这里,我们将导入一个VGG16模型:
import tensorflow as tf
from tensorflow.keras.applications import VGG16
接下来,我们将定义我们数据集中图像的输入维度。假设我们有(100,100, 3)尺寸的图像:
img_dim = (100, 100, 3)
然后,我们将实例化一个VGG16模型:
base_model = VGG16(input_shape=img_dim, \
weights='imagenet', include_top=True)
现在,我们有了一个在ImageNet数据集上训练的VGG16模型。include_top=True参数用于指定我们将使用相同的最后几层来预测 ImageNet 的 20,000 个类别的图像。
现在,我们可以使用这个预训练模型进行预测:
base_model.predict(input_img)
但是,如果我们想使用这个预训练模型来预测与 ImageNet 不同的类别怎么办?在这种情况下,我们需要替换预训练模型中用于预测的最后几个全连接层,并在新的类别上进行训练。这些最后的几层被称为模型的顶部(或头部)。我们可以通过指定include_top=False来实现:
base_model = VGG16(input_shape=img_dim, \
weights='imagenet', include_top=False)
之后,我们需要冻结这个模型,以防止它被训练(也就是说,它的权重将不会被更新):
base_model.trainable = False
然后,我们将创建一个新的全连接层,参数由我们选择。在这个示例中,我们将添加一个具有20个单元和softmax激活函数的Dense层:
prediction_layer = tf.keras.layers.Dense(20, activation='softmax')
然后,我们将把新的全连接层添加到我们的基础模型中:
new_model = tf.keras.Sequential([base_model, prediction_layer])
最后,我们将训练这个模型,但只有最后一层的权重会被更新:
optimizer = tf.keras.optimizers.Adam(0.001)
new_model.compile(loss='sparse_categorical_crossentropy', \
optimizer=optimizer, metrics=['accuracy'])
new_model.fit(features_train, label_train, epochs=5, \
validation_split = 0.2, verbose=2)
我们刚刚从一个预训练模型创建了一个新模型,并进行了适应,使其能够对我们自己的数据集进行预测。我们通过根据我们想要进行的预测替换最后几层来实现这一点。然后,我们仅训练这些新层来做出正确的预测。通过迁移学习,我们利用了VGG16模型在 ImageNet 上训练得到的现有权重。这为我们节省了大量的训练时间,并且可以显著提高模型的性能。
微调
在上一部分中,我们学习了如何应用迁移学习并使用预训练模型在我们自己的数据集上进行预测。通过这种方法,我们冻结了整个网络,仅训练了最后几层,它们负责做出预测。卷积层保持不变,因此所有的滤波器都是预先设置好的,你只需要重用它们。
但是,如果您使用的数据集与 ImageNet 差异很大,这些预训练的过滤器可能并不相关。在这种情况下,即使使用迁移学习,也无法帮助您的模型准确预测正确的结果。对此有一种解决方案,即只冻结网络的一部分,并训练模型的其他部分,而不仅仅是冻结顶层,就像我们使用迁移学习时一样。
在网络的早期层中,过滤器通常非常通用。例如,在这个阶段,您可能会找到检测水平或垂直线的过滤器。靠近网络末端(靠近顶部或头部)的过滤器通常更具体地适应您正在训练的数据集。因此,这些就是我们希望重新训练的层。让我们来看看如何在 TensorFlow 中实现这一点。
首先,让我们实例化一个预训练的VGG16模型:
base_model = VGG16(input_shape=img_dim, \
weights='imagenet', include_top=False)
我们需要设置层的阈值,以便它们被冻结。在本例中,我们将冻结前 10 层:
frozen_layers = 10
然后,我们将遍历这些层,并逐个冻结它们:
for layer in base_model.layers[:frozen_layers]:
layer.trainable = False
然后,我们将向基础模型中添加自定义的全连接层:
prediction_layer = tf.keras.layers.Dense(20, activation='softmax')
new_model = tf.keras.Sequential([base_model, prediction_layer])
最后,我们将训练这个模型:
optimizer = tf.keras.optimizers.Adam(0.001)
new_model.compile(loss='sparse_categorical_crossentropy', \
optimizer=optimizer, metrics=['accuracy'])
new_model.fit(features_train, label_train, epochs=5, \
validation_split = 0.2, verbose=2)
在这种情况下,我们的模型将训练并更新我们定义的阈值层中的所有权重。它们将使用预训练的权重作为第一次迭代的初始化值。
通过这种被称为微调的技术,您仍然可以利用预训练模型,通过部分训练使其适应您的数据集。
活动 3.02:使用迁移学习进行水果分类
在本活动中,我们将训练一个卷积神经网络(CNN)来识别属于 120 个不同类别的水果图片。我们将使用迁移学习和数据增强来完成此任务。我们将使用 Fruits 360 数据集(arxiv.org/abs/1712.00580),该数据集最初由 Horea Muresan、Mihai Oltean 共享,Fruit recognition from images using deep learning, Acta Univ. Sapientiae, Informatica Vol. 10, Issue 1, pp. 26-42, 2018。
它包含了超过 82,000 张不同类型水果的 120 种图片。我们将使用这个数据集的一个子集,包含超过 16,000 张图片。请按照以下步骤完成本活动:
注意
数据集可以在这里找到:packt.live/3gEjHsX
-
使用 TensorFlow 导入数据集并解压文件。
-
创建一个数据生成器,并应用以下数据增强:
rescale=1./255, rotation_range=40, width_shift_range=0.1, height_shift_range=0.1, shear_range=0.2, zoom_range=0.2, horizontal_flip=True, fill_mode='nearest' -
从 TensorFlow 加载一个预训练的
VGG16模型。 -
在
VGG16的顶部添加两个全连接层:一个带有Dense(1000, activation='relu')的全连接层和一个带有Dense(120, activation='softmax')的全连接层。 -
指定一个学习率为
0.001的 Adam 优化器。 -
训练模型。
-
在测试集上评估模型。
期望的准确率应该在训练集和验证集上大约为0.89到0.91。输出结果将类似于此:

图 3.29:活动的预期输出
注意
本活动的详细步骤,以及解决方案和额外的评论,已在第 398 页展示。
总结
在本章中,我们从计算机视觉和图像处理的介绍开始,了解了这种技术的不同应用、数字图像的表示方式,并通过滤波器对其进行了分析。
然后,我们深入探讨了 CNN 的基本元素。我们了解了卷积操作是什么,滤波器在检测模式中的作用,以及步幅和填充的用途。在理解这些构建块之后,我们学习了如何使用 TensorFlow 设计 CNN 模型,并构建了自己的 CNN 架构来识别手写数字。
之后,我们学习了数据生成器,并了解了它们如何将图像批次输入到我们的模型中,而不是加载整个数据集。我们还学习了它们如何执行数据增强变换,以扩展图像的多样性,帮助模型更好地进行泛化。
最后,我们了解了如何保存模型及其配置,还学习了如何应用迁移学习和微调。这些技术对于重用预训练模型并将其调整为适应自己的项目和数据集非常有用。这将为你节省大量时间,因为你无需从头开始训练模型。
在下一章中,你将学习另一个非常有趣的主题,它用于自然语言处理:嵌入(embeddings)。
第四章:4. 深度学习与文本——嵌入
概述
在本章中,我们将开始深入探讨自然语言处理(NLP)在文本中的应用。我们将通过使用自然语言工具包(Natural Language Toolkit)对原始文本数据进行预处理,过程中我们会对原始文本进行分词,并去除标点符号和停用词。随着本章内容的展开,我们将实现经典的文本表示方法,如独热编码(one-hot encoding)和TF-IDF方法。本章展示了词嵌入(word embeddings)的强大功能,并解释了基于深度学习的嵌入方法。我们将使用Skip-gram和Continuous Bag of Words算法来生成我们自己的词嵌入。我们将探讨嵌入的特性、算法的不同参数,并生成短语的向量。到本章结束时,你将能够处理文本数据,开始使用预训练模型的词嵌入,甚至创建你自己的嵌入模型。
介绍
Siri 是如何知道在你要求她“播放 80 年代的一首轻柔歌曲”时该做什么的?谷歌是如何在不到一秒钟的时间里找到与你搜索的关键词最相关的结果的?你的翻译应用是如何几乎瞬间将德语翻译成英语的?你的电子邮件客户端是如何保护你并自动识别所有那些恶意的垃圾邮件/网络钓鱼邮件的?所有这些问题的答案,以及驱动许多其他惊人应用的技术,都归功于自然语言处理(NLP)。
到目前为止,我们一直在处理结构化的数字数据——例如作为数字矩阵的图像。在本章中,我们将开始讨论如何处理文本数据,并解锁所需的技能,以利用这片未结构化信息的“金矿”。本章将讨论一个关键的概念——表示,特别是使用嵌入(embeddings)。我们将讨论表示的注意事项并实现相关方法。我们将从最简单的表示方法开始,最终讨论词嵌入——这是一种表示文本数据的强大方法。词嵌入将帮助你在结合深度学习方法时,在 NLP 任务中获得最先进的结果。
自然语言处理(NLP)是一个旨在帮助机器理解自然(人类)语言的领域。如以下图所示,NLP 处于语言学、计算机科学和人工智能的交汇点:

图 4.1:NLP 的应用场景
这是一个广泛的领域——想一想语言(口语和书面语言)使用的所有地方。NLP 使得并推动了前述图中列举的各类应用,包括以下内容:
-
将文档分类到不同类别(文本分类)
-
语言之间的翻译,例如,从德语到英语(序列到序列学习)
-
自动分类推文或电影评论的情感(情感分析)
-
24/7 即时回复你的查询的聊天机器人
在我们继续之前,我们需要承认并欣赏,NLP 不是一项简单的任务。考虑以下句子:“The boy saw a man with a telescope.”
谁拿着望远镜?男孩是用望远镜看见那个人的吗?还是那个人身上带着望远镜?这句话本身有歧义,我们无法单凭它解决这个问题,也许更多的上下文能帮助我们搞清楚。
让我们再考虑一下这个句子:“Rahim convinced Mohan to buy a television for himself.” 这台电视是给谁买的——是给 Rahim 还是 Mohan?这又是一个歧义问题,我们或许可以通过更多的上下文来解决,但对于机器/程序来说,依然可能非常困难。
让我们再看一个例子:“Rahim has quit skydiving.” 这句话意味着 Rahim 曾做过相当多的跳伞。句子中有一个假设,这对机器来说很难推断出来。
语言是一个复杂的系统,使用符号(单词/术语),并以多种方式将它们组合起来传达思想。理解语言并不总是那么容易,其中有很多原因。歧义无疑是最大的问题:单词在不同的语境中可能有不同的含义。再加上潜台词、不同的视角等等,我们永远无法确定不同的人是否以相同的方式理解同样的词语。一首诗可以被不同的读者以多种方式解读,每个读者都带着自己独特的视角和对世界的理解来解读这首诗。
深度学习与自然语言处理
深度学习的兴起对许多领域产生了积极的影响,NLP(自然语言处理)也不例外。现在你可以理解,深度学习方法为我们带来了前所未有的高准确度,这帮助我们在许多领域取得了进步。NLP 中有多个任务从深度学习方法中获益匪浅。过去,情感预测、机器翻译和聊天机器人等应用需要大量人工干预。而现在,借助深度学习和 NLP,这些任务已经完全自动化,并且展现出了令人印象深刻的表现。图 4.2 中展示的简单高层次视图表明了深度学习如何用于处理自然语言。深度学习不仅为我们提供了机器可以理解的自然语言的优秀表示,还为 NLP 任务提供了非常强大的建模方法。

图 4.2:深度学习在自然语言处理中的应用
话虽如此,我们需要小心,避免低估让机器执行涉及人类语言任务的难度以及 NLP 领域的挑战。深度学习并未解决 NLP 中的所有挑战,但它的确在许多 NLP 任务的处理方式上带来了范式的转变,并推动了一些应用的发展,使得本来困难的任务变得对任何人来说都易于操作和实现。我们将在第五章,序列的深度学习中执行其中的一些任务。
其中一个关键任务是文本数据表示——简单来说,就是将原始文本转换为模型可以理解的形式。词嵌入是一种基于深度学习的方法,它改变了游戏规则,并提供了非常强大的文本表示。我们将在本章后面详细讨论嵌入,并创建我们自己的嵌入。首先,让我们动手处理一些文本,进行一些非常重要的数据准备。
开始处理文本数据
让我们开始将一些测试数据导入 Python。首先,我们将创建一些自己的玩具数据,熟悉工具。然后,我们将使用路易斯·卡罗尔的经典作品《爱丽丝梦游仙境》,该作品可以通过古腾堡计划(gutenberg.org)获取。幸运的是,我们可以通过自然语言工具包(NLTK)轻松访问它,这是一款用于从零开始进行 NLP 的极佳库。
注意
本章的代码实现可以在packt.live/3gEgkSP找到。本章中的所有代码都必须在一个单一的 Jupyter Notebook 中运行。
NLTK 应该包含在 Anaconda 发行版中。如果没有,你可以在命令行中使用以下命令安装 NLTK:
pip install nltk
这个方法在 Windows 上应该可行。对于 macOS 和 Linux,你可以使用以下命令:
$ sudo pip install -U nltk
我们的虚拟数据可以通过以下命令创建(这里我们使用 Jupyter Notebooks;你也可以使用任何界面):
raw_txt = """Welcome to the world of Deep Learning for NLP! \
We're in this together, and we'll learn together. \
NLP is amazing, \
and Deep Learning makes it even more fun. \
Let's learn!"""
我们有一个名为raw_txt的字符串变量,其中存储了文本,因此,现在我们已经准备好开始处理它。
文本预处理
文本预处理是指将文本数据准备好以进行主要分析/建模的过程。无论你的最终目标是什么——可能是情感分析、分类、聚类或其他众多目标——你都需要清理原始文本数据并准备好进行分析。这是任何涉及自然语言处理(NLP)的应用的第一步。
我们所说的清理是什么意思?什么时候文本数据算是准备好了?我们知道,在日常生活中遇到的文本数据往往非常杂乱(想一想社交媒体、产品评论、服务评论等等),并且存在各种各样的不完美之处。根据手头的任务和你正在处理的数据类型,你关心的不完美之处会有所不同,而清理的意义也会有所不同。举个例子,在某些应用中,预处理可能仅仅意味着“将句子分割成单独的术语”。你在这里采取的步骤将对最终分析结果产生影响。让我们更详细地讨论这一点。
分词
预处理的第一步不可避免的是分词——将原始输入文本序列拆分成词元。简单来说,它就是将原始文本拆分成你想要处理的基本元素。这个词元可以是段落、句子、单词,甚至是字符。如果你想将段落拆分成句子,那么你会将段落分词成句子。如果你想将句子中的单词分开,那么你会将句子分词成单词。
对于我们的原始文本,首先,我们想要将句子分开。为此,我们在 Python 中有多种选择——这里,我们将使用 NLTK 中的 tokenize API。
注意
本书中我们将始终使用 Jupyter Notebooks,这是我们推荐的工具。不过,随时可以使用任何你喜欢的 IDE。
在使用 API 之前,我们必须import nltk并下载punkt句子分词器。然后,我们需要导入tokenize库。所有这些可以通过以下命令完成:
import nltk
nltk.download('punkt')
from nltk import tokenize
tokenize API 提供了提取不同级别词元(句子、单词或字符)的工具,适用于不同类型的数据(也有一个非常实用的推文分词器)。我们将在这里使用sent_tokenize()方法。sent_tokenize()方法将输入文本拆分成句子。让我们看看它的实际效果:
tokenize.sent_tokenize(raw_txt)
这应该会给我们以下的独立句子:
['Welcome to the world of Deep Learning for NLP!',
"We're in this together, and we'll learn together.",
'NLP is amazing, and Deep Learning makes it even more fun.',
"Let's learn!"]
从输出结果来看,sent_tokenize()似乎做得相当不错。它正确地识别了句子边界,并如预期般给出了四个句子。为了方便处理,我们将结果赋给一个变量,并检查结果及其组成部分的数据类型:
txt_sents = tokenize.sent_tokenize(raw_txt)
type(txt_sents), len(txt_sents)
以下是前面代码的输出:
(list, 4)
如我们所见,它是一个包含四个元素的列表,每个元素都是一个字符串,表示一个句子。
我们可以尝试使用word_tokenize()方法将句子拆分为单个单词。这个方法将给定的句子拆分成其组成的单词。它使用智能规则来判断单词的边界。为了方便起见,我们可以使用列表推导(在 Python 中,推导是一种简洁构建新序列的方法):
txt_words = [tokenize.word_tokenize(sent) for sent in txt_sents]
type(txt_words), type(txt_words[0])
上面的命令会给我们以下输出:
(list, list)
输出如预期 - 结果列表的元素本身是列表,其中包含组成句子的单词。让我们也打印出结果的前两个元素:
print(txt_words[:2])
输出将如下所示:
[['Welcome', 'to', 'the', 'world', 'of',
'Deep', 'Learning', 'for', 'NLP', '!'],
['We', "'re", 'in', 'this', 'together',
',', 'and', 'we', "'ll", 'learn', 'together', '.']]
句子已被分解为单独的单词。我们还可以看到像“we'll”这样的缩写已被分解为组成部分,即“we”和“'ll”。所有标点符号(逗号、句号、感叹号等)都是单独的标记。如果我们希望删除它们,这对我们非常方便,我们稍后会这样做。
标准化大小写
另一个常见的步骤是标准化大小写 - 我们通常不希望“car”、“CAR”、“Car”和“caR”被视为不同的实体。为此,我们通常将所有文本转换为小写(如果需要,我们也可以将其转换为大写)。
在 Python 中,所有字符串都有一个lower()方法,因此将字符串变量(strvar)转换为小写就像strvar.lower()这样简单。
注意
我们本可以在分词之前一开始就使用这个方法,这将非常简单,比如raw_txt = raw_txt.lower()。
我们将在将数据分词为单独的句子后使用lower()方法来标准化我们的数据。我们将通过以下命令来实现这一点:
txt_sents = [sent.lower() for sent in txt_sents]
txt_words = [tokenize.word_tokenize(sent) for sent in txt_sents]
让我们打印几个句子,看看结果是什么样的:
print(txt_words[:2])
输出将如下所示:
[['welcome', 'to', 'the', 'world', 'of',
'deep', 'learning', 'for', 'nlp', '!'],
['we', "'re", 'in', 'this', 'together',
',', 'and', 'we', "'ll", 'learn', 'together', '.']]
我们可以看到,输出这次所有的术语都是小写的。我们已经取得了原始文本,将其分成句子,标准化大小写,然后将其分解成单词。现在,我们拥有了所有我们需要的标记,但我们似乎仍然有很多标点符号作为标记,我们需要摆脱它们。让我们继续进行更多的“清理工作”。
删除标点符号
我们可以看到,数据目前所有标点符号都作为单独的标记存在。再次提醒,可能会有标点符号很重要的任务。例如,在进行情感分析时,也就是预测文本的情感是积极的还是消极的时候,感叹号可能会增加价值。对于我们的任务,让我们去掉这些标点符号,因为我们只关心语言的表达。为此,我们需要一个列表,其中包含我们要删除的所有标点符号。幸运的是,Python 的字符串基础库中有这样一个列表,我们可以简单地导入并分配给一个列表变量:
from string import punctuation
list_punct = list(punctuation)
print(list_punct)
你应该得到以下输出:
['!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',',
'-', '.', '/', ':', ';', '<', '=', '>', '?', '@',
'[', '\\', ']', '^', '_', '`', '{', '|', '}', '~']
所有通常的标点符号都在这里。如果有任何其他要删除的标点符号,您可以简单地将它们添加到list_punct变量中。
我们可以定义一个函数来从给定的标记列表中删除标点符号。该函数将期望一个标记列表,从中将可用于list_punct变量的标记删除:
def drop_punct(input_tokens):
return [token for token in input_tokens \
if token not in list_punct]
我们可以使用以下命令在一些虚拟标记上测试这个:
drop_punct(["let",".","us",".","go","!"])
我们得到了以下结果:
['let', 'us', 'go']
该功能按预期工作。现在,我们需要将我们在上一部分中修改的txt_words变量传递给刚刚创建的drop_punct函数。我们将把结果存储在一个名为txt_words_nopunct的新变量中:
txt_words_nopunct = [drop_punct(sent) for sent in txt_words]
print(txt_words_nopunct)
我们将得到以下输出:
[['welcome', 'to', 'the', 'world', 'of',
'deep', 'learning', 'for', 'nlp'],
['we', "'re", 'in', 'this', 'together', 'and',
'we', "'ll", 'learn', 'together'],
['nlp', 'is', 'amazing', 'and',
'deep', 'learning', 'makes', 'it', 'even', 'more', 'fun'],
['let', "'s", 'learn']]
如前面的输出所示,我们创建的函数已经从原始文本中删除了所有标点符号。现在,数据看起来更干净了,因为没有了标点符号,但我们仍然需要去除无信息的词汇。我们将在下一部分讨论这个问题。
删除停用词
在日常语言中,我们有很多词汇,它们并没有增加太多的信息/价值*。这些通常被称为“停用词”。我们可以将这些词分为两大类:
-
一般/功能性:这些是语言中的填充词,它们并没有提供很多信息,但帮助连接其他有信息的词汇,以形成有意义的句子,例如“the”、“an”、“of”等等。
-
上下文:这些不是一般的功能性词汇,但鉴于上下文,它们并没有提供很多信息。如果你正在处理关于手机的评论,而所有评论都在讨论手机,那么“手机”这个词本身可能没有太多信息。
注意
“价值”的概念在每个任务中有所不同。像“the”和“and”这样的功能性词汇可能对于自动文档分类到主题中并不重要,但对于其他应用,比如词性标注(识别动词、形容词、名词、代词等)则非常重要。
功能性停用词已经方便地集成到 NLTK 中。我们只需要导入它们,然后可以将它们存储在一个变量中。一旦存储,就可以像访问 Python 列表一样访问它们。让我们导入它们并看看我们有多少个这样的词:
import nltk
nltk.download("stopwords")
from nltk.corpus import stopwords
list_stop = stopwords.words("english")
len(list_stop)
我们将看到以下输出:
179
我们可以看到我们有 179 个内置的停用词。让我们也打印出其中一些:
print(list_stop[:50])
输出将如下所示:
['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves',
'you', "you're", "you've", "you'll", "you'd", 'your',
'yours', 'yourself', 'yourselves', 'he', 'him', 'his',
'himself', 'she', "she's", 'her', 'hers', 'herself',
'it', "it's", 'its', 'itself', 'they', 'them',
'their', 'theirs', 'themselves', 'what', 'which', 'who', 'whom',
'this', 'that', "that'll", 'these', 'those', 'am', 'is', 'are',
'was', 'were', 'be']
我们可以看到这些术语大多数是非常常用的“填充”词,它们在语言中有“功能性”作用,并没有提供太多信息。
现在,删除停用词的方法与我们删除标点符号的方法相同。
练习 4.01:分词、大小写规范化、标点符号和停用词移除
在这个练习中,我们将从数据中删除停用词,并应用我们到目前为止学到的所有内容。我们将首先执行分词(句子和单词);然后执行大小写规范化,接着是标点符号和停用词的删除。
注意
在开始这个练习之前,请确保你正在使用一个 Jupyter Notebook,并且已经下载了punkt句子分词器和stopwords语料库,如文本预处理部分所示。
这次我们将保持代码简洁。我们将定义和处理raw_txt变量。让我们开始吧:
-
运行以下命令来导入
nltk及其tokenize模块:import nltk from nltk import tokenize -
定义
raw_txt变量,使其包含文本"欢迎来到深度学习与自然语言处理的世界!我们一起前行,一起学习。自然语言处理令人惊叹,深度学习让它更有趣。让我们开始学习!":raw_txt = """Welcome to the world of deep learning for NLP! \ We're in this together, and we'll learn together. \ NLP is amazing, \ and deep learning makes it even more fun. \ Let's learn!""" -
使用
sent_tokenize()方法将原始文本分割为单独的句子,并将结果存储在一个变量中。在分词之前,使用lower()方法将字符串转换为小写:txt_sents = tokenize.sent_tokenize(raw_txt.lower())注意
我们刚创建的
txt_sents变量将在本章后续部分中继续使用。 -
使用列表推导式,应用
word_tokenize()方法将每个句子分解成其组成单词:txt_words = [tokenize.word_tokenize(sent) for sent in txt_sents] -
从
string模块导入punctuation并将其转换为列表:from string import punctuation stop_punct = list(punctuation) -
从 NLTK 导入英语的内建停用词,并将其保存到一个变量中:
from nltk.corpus import stopwords stop_nltk = stopwords.words("english") -
创建一个包含标点符号和 NLTK 停用词的组合列表。请注意,我们可以一次性删除它们:
stop_final = stop_punct + stop_nltk -
定义一个函数,移除输入句子中的停用词和标点符号,输入为一个标记集合:
def drop_stop(input_tokens): return [token for token in input_tokens \ if token not in stop_final] -
通过对分词后的句子应用函数来移除冗余的标记,并将结果存储在一个变量中:
txt_words_nostop = [drop_stop(sent) for sent in txt_words] -
打印数据中第一个清理后的句子:
print(txt_words_nostop[0])停用词被移除后,结果将如下所示:
['welcome', 'world', 'deep', 'learning', 'nlp']注意
要访问此特定部分的源代码,请参阅
packt.live/2VVNEgf。您还可以在
packt.live/38Gr54r上在线运行这个示例。您必须执行整个 Notebook 才能获得期望的结果。
在这个练习中,我们完成了迄今为止学到的所有清理步骤。这次我们将某些步骤结合起来,使代码更加简洁。这些是处理文本数据时非常常见的步骤。您可以尝试通过定义一个返回处理结果的函数,进一步优化和模块化代码。我们鼓励您尝试一下。
到目前为止,清理过程中的步骤是移除了我们评估中不太有用的标记。但我们还有一些事情可以做,以进一步改善我们的数据——我们可以尝试运用对语言的理解来合并标记,识别具有相同含义的标记,并去除更多冗余。两种常见的做法是词干提取(stemming)和词形还原(lemmatization)。
注意
在本练习中创建的变量将在本章的后续部分中继续使用。确保先完成本练习再继续进行接下来的练习和活动。
词干提取与词形还原
"Eat"、"eats"、"eating"、"ate"——它们不都是同一个单词的变体吗,都指的是同一个动作?在大多数文本和口语中,我们通常会遇到同一个单词的多个形式。通常情况下,我们不希望这些形式被当作独立的词项来处理。如果查询是"red shoes"或"red shoe",搜索引擎需要返回相似的结果——否则会带来糟糕的搜索体验。我们承认这种情况非常常见,因此我们需要一种策略来处理这些情况。那么,我们应该如何处理一个单词的变体呢?一种合理的方法是将它们映射到一个共同的词项,这样它们就会被视为相同。
词干提取是一种基于规则的方法,通过将单词简化为其“词干”来实现标准化。词干是单词在加上任何词缀(用于形成变体的元素)之前的根本形式。这种方法相当简单——去掉后缀就得到词干。一个流行的算法是Porter 词干提取算法,它应用一系列这样的规则:

图 4.3:Porter 词干提取算法基于规则的方法示例
注意
Porter 词干提取算法的完整规则集可以在snowball.tartarus.org/algorithms/porter/stemmer.html找到。
让我们看看 Porter 词干提取算法的实际操作。我们从 NLTK 的 'stem' 模块导入 PorterStemmer 函数并创建一个实例:
from nltk.stem import PorterStemmer
stemmer_p = PorterStemmer()
请注意,词干提取器是作用于单个词项的,而不是整个句子的。让我们看看词干提取器如何处理单词"driving":
print(stemmer_p.stem("driving"))
输出结果如下:
drive
让我们看看如何将这个方法应用于整个句子。请注意,我们需要先对句子进行分词:
txt = "I mustered all my drive, drove to the driving school!"
以下代码用于对句子进行分词,并对每个词项应用词干提取器:
tokens = tokenize.word_tokenize(txt)
print([stemmer_p.stem(word) for word in tokens])
输出结果如下:
['I', 'muster', 'all', 'my', 'drive', ',', 'drove', 'to',
'the', 'drive', 'school', '!']
我们可以看到,词干提取器已经正确地将"mustered"减少为"muster"并将"driving"减少为"drive",而" drove"则没有被改变。此外,请注意,词干提取器的结果不一定是一个有效的英语单词。
词形还原是一种更复杂的方法,它参考词典并找到单词的有效词根形式(词形)。词形还原在提供单词词性时效果最佳——它考虑到单词的角色并返回适当的形式。词形还原的输出总是有效的英语单词。然而,词形还原的计算代价非常高,而且为了使其效果良好,需要词性标签,而词性标签通常在数据中是不可用的。我们来看一下它的简要使用。首先,从nltk.stem导入WordNetLemmatizer并实例化它:
nltk.download('wordnet')
from nltk.stem import WordNetLemmatizer
lemmatizer = WordNetLemmatizer()
让我们在词项ponies上应用lemmatizer:
lemmatizer.lemmatize("ponies")
以下是输出结果:
'pony'
对于我们的讨论,词干提取就足够了。词干提取的结果可能并不总是有效的单词。例如,poni是ponies的词干,但它不是一个有效的英语单词。此外,可能会有一些不准确之处,但对于将单词映射到共同词形的目标而言,这种粗略的方法是完全有效的。
练习 4.02:对我们的数据进行词干提取
在这个练习中,我们将继续进行数据预处理。在之前的练习中,我们去除了停用词和标点符号。现在,我们将使用 Porter 词干提取算法来提取词干。由于我们将使用之前创建的txt_words_nostop变量,让我们继续使用在练习 4.01中创建的同一个 Jupyter Notebook,标记化、大小写规范化、标点符号和停用词移除。此时,该变量将包含以下文本:
[['welcome', 'world', 'deep', 'learning', 'nlp'],
["'re", 'together', "'ll", 'learn', 'together'],
['nlp', 'amazing', 'deep', 'learning', 'makes', 'even', 'fun'],
['let', "'s", 'learn']]
完成此练习的步骤如下:
-
使用以下命令从 NLTK 导入
PorterStemmer:from nltk.stem import PorterStemmer -
实例化词干提取器:
stemmer_p = PorterStemmer() -
对
txt_words_nostop中的第一句话应用词干提取器:print([stemmer_p.stem(token) for token in txt_words_nostop[0]])当我们打印结果时,得到以下输出:
['welcome has been changed to welcom and learning to learn. This is consistent with the rules of the Porter stemming algorithm. -
对数据中的所有句子应用词干提取器。你可以使用循环,或者使用嵌套列表推导:
txt_words_stem = [[stemmer_p.stem(token) for token in sent] \ for sent in txt_words_nostop] -
使用以下命令打印输出:
txt_words_stem输出将如下所示:
[['welcom', 'world', 'deep', 'learn', 'nlp'], ["'re", 'togeth', "'ll", 'learn', 'togeth'], ['nlp', 'amaz', 'deep', 'learn', 'make', 'even', 'fun'], ['let', "'s", 'learn']]
看起来词干提取器已经做了很多修改。许多单词已经不再有效,但仍然可以识别,这没关系。
注意
若要访问此特定部分的源代码,请参考packt.live/2VVNEgf。
你也可以在线运行此示例,访问packt.live/38Gr54r。你必须执行整个 Notebook 才能获得预期的结果。
在这个练习中,我们使用了 Porter 词干提取算法来提取我们标记化数据的词干。词干提取作用于单个词,因此需要在标记化成词之后进行。词干提取将一些词语缩减为其基本形式,这些形式不一定是有效的英语单词。
词干提取与词形还原之外的内容
除了词干提取和词形还原之外,还有许多特定的方法可以处理单词变体。我们有像语音哈希这样的方法来识别由发音引起的单词拼写变体。接着,还有拼写校正来识别和纠正拼写错误。另一个潜在的步骤是缩写处理,使得television和TV被视为相同的单词。这些步骤的结果可以通过进行领域特定的术语处理进一步增强。你大概明白了……有很多可能的步骤,具体是否使用,取决于你的数据和应用的关键性。
不过,一般来说,我们一起完成的这些步骤已经足够了——大小写规范化、标记化、停用词和标点符号移除,接着是词干提取/词形还原。这些是大多数 NLP 应用程序中常见的步骤。
使用 NLTK 下载文本语料库
到目前为止,我们已经在我们创建的虚拟数据上执行了这些步骤。现在,是时候在一个更大、更真实的文本上尝试我们新学到的技能了。首先,让我们获取这个文本——路易斯·卡罗尔的经典作品《爱丽丝梦游仙境》,它通过古腾堡计划提供并可通过 NLTK 访问。
你可能需要通过 NLTK 下载 'gutenberg' 语料库。首先,使用以下命令导入 NLTK:
import nltk
然后,使用 nltk.download() 命令打开一个应用程序,也就是 NLTK 下载器 界面(如下截图所示):
nltk.download()
我们可以看到该应用程序有多个标签。点击 语料库 标签:

图 4.4:NLTK 下载器
在 语料库 标签中,向下滚动直到找到 gutenberg。如果状态是 未安装,请点击左下角的 下载 按钮。那将安装 gutenberg 语料库:

图 4.5:NLTK 下载器的语料库标签
关闭界面。现在,你可以直接从 NLTK 获取一些经典文本。我们将读取文本并将其存储在一个变量中:
alice_raw = nltk.corpus.gutenberg.raw('carroll-alice.txt')
该文本存储在 alice_raw 中,这是一个大字符串。我们来看一下这个字符串的前几个字符:
alice_raw[:800]
输出结果如下:
"[Alice's Adventures in Wonderland by Lewis Carroll 1865]
\n\nCHAPTER I. Down the Rabbit-Hole\n\nAlice was beginning
to get very tired of sitting by her sister on the\nbank,
and of having nothing to do: once or twice she had peeped
into the\nbook her sister was reading, but it had no pictures
or conversations in\nit, 'and what is the use of a book,'
thought Alice 'without pictures or\nconversation?'
\n\nSo she was considering in her own mind
(as well as she could, for the\nhot day made her feel
very sleepy and stupid), whether the pleasure\nof making
a daisy-chain would be worth the trouble of getting up
and\npicking the daisies, when suddenly a White Rabbit
with pink eyes ran\nclose by her.\n\nThere was nothing
so VERY remarkable in that; nor did Alice think
it so\nVERY much out of the way to hear the Rabbit"
我们可以在输出中看到原始文本,它包含了我们预期的常见不完美——大小写不一、停用词、标点符号等等。
我们准备好了。让我们通过一个活动来测试我们的技能。
活动 4.01:‘爱丽丝梦游仙境’文本的文本预处理
在这个活动中,你将把到目前为止学到的所有预处理步骤应用到一个更大、更真实的文本中。我们将处理存储在 alice_raw 变量中的《爱丽丝梦游仙境》文本:
alice_raw[:800]
当前的文本看起来是这样的:
"[Alice's Adventures in Wonderland by Lewis Carroll 1865]
\n\nCHAPTER I. Down the Rabbit-Hole\n\nAlice was beginning
to get very tired of sitting by her sister on the\nbank,
and of having nothing to do: once or twice she had peeped
into the\nbook her sister was reading, but it had no pictures
or conversations in\nit, 'and what is the use of a book,'
thought Alice 'without pictures or\nconversation?
'\n\nSo she was considering in her own mind
(as well as she could, for the\nhot day made her feel
very sleepy and stupid), whether the pleasure\nof making
a daisy-chain would be worth the trouble of getting up
and\npicking the daisies, when suddenly a White Rabbit
with pink eyes ran\nclose by her.\n\nThere was nothing
so VERY remarkable in that; nor did Alice think
it so\nVERY much out of the way to hear the Rabbit"
在这个活动结束时,你将完成数据的清洗和分词,去除许多不完美之处,去除停用词和标点符号,并对数据应用词干提取。
注意
在开始这个活动之前,确保你已经安装了 gutenberg 语料库,并创建了 alice_raw 变量,正如前一节 使用 NLTK 下载文本语料库 所示。
以下是你需要执行的步骤:
-
在同一个 Jupyter Notebook 中,使用
'alice_raw'变量中的原始文本。将原始文本转换为小写。 -
对句子进行分词。
-
从
string模块导入标点符号,从 NLTK 导入停用词。 -
创建一个变量,用于保存上下文中的停用词,即
--和said。 -
创建一个包含标点符号、NLTK 停用词和上下文停用词的主停用词列表,以便去除这些词。
-
定义一个函数,从任何输入的句子(已分词)中删除这些标记。
-
使用 NLTK 的
PorterStemmer算法对结果进行词干提取。 -
打印出结果中的前五个句子。
注意
本活动的详细步骤、解决方案和额外的评论内容在第 405 页上有介绍。
预期的输出结果是这样的:
[['alic', "'s", 'adventur', 'wonderland', 'lewi', 'carrol',
'1865', 'chapter', 'i.', 'rabbit-hol', 'alic', 'begin',
'get', 'tire', 'sit', 'sister', 'bank', 'noth', 'twice',
'peep', 'book', 'sister', 'read', 'pictur', 'convers',
"'and", 'use', 'book', 'thought', 'alic', "'without",
'pictur', 'convers'],
['consid', 'mind', 'well', 'could', 'hot', 'day', 'made',
'feel', 'sleepi', 'stupid', 'whether', 'pleasur', 'make',
'daisy-chain', 'would', 'worth', 'troubl', 'get', 'pick',
'daisi', 'suddenli', 'white', 'rabbit',
'pink', 'eye', 'ran', 'close'],
['noth', 'remark', 'alic', 'think', 'much', 'way', 'hear',
'rabbit', 'say', "'oh", 'dear'],
['oh', 'dear'],
['shall', 'late']]
让我们看看到目前为止我们取得了哪些成果,以及未来还有哪些挑战。
到目前为止,我们已经学习了如何进行文本预处理——即为我们的主要分析/模型准备文本数据的过程。我们从原始文本数据开始,这些数据可能存在许多不完美的地方。我们学习了如何处理这些不完美,现在已经能够熟练地处理文本数据,并为进一步的分析做好准备。这是任何自然语言处理应用中的重要第一步。因此,我们从原始文本数据开始,得到了清洁的数据。接下来呢?
接下来的部分非常重要,因为它对你的分析质量有着极大的影响。它被称为表示(Representation)。让我们来讨论一下这个问题。
文本表示的考虑因素
我们已经将原始输入数据处理成了干净的文本。现在,我们需要将这些干净的文本转换为预测模型可以理解的格式。那么,预测模型到底能理解什么呢?它能理解不同的单词吗?它能像我们一样阅读单词吗?它能处理我们提供的文本吗?
到现在为止,你应该明白模型是通过数字来工作的。模型的输入是一串数字。它不理解图像,但可以处理表示图像的矩阵和数字。处理图像的关键思想是将图像转换为数字,并从中生成特征。对于文本来说,想法是一样的:我们需要将文本转换为数字,这些数字将作为模型的特征。
表示(Representation)的核心就是将文本转换为模型能够理解的数字/特征。这听起来似乎没什么难的,对吧?如果你这么认为,那么请考虑一下:输入特征对于任何建模任务都至关重要,而表示正是创建这些特征的过程。它对模型的结果有着极为重要的影响,是你应当特别关注的一个过程。
那么,你应该如何进行文本表示呢?如果有的话,什么是“最佳”的文本表示方法?让我们来讨论几种方法。
文本表示的经典方法
文本表示方法随着时间的推移经历了显著的演变,神经网络和深度神经网络的出现对我们当前的文本表示方式产生了重大影响(稍后会详细讲解)。我们确实走过了很长的一段路:从手工构建特征,标记某个单词是否出现在文本中,到创建强大的表示方法,如词嵌入。尽管有许多方法,其中一些更适合特定任务,我们将在 Python 中讨论几种主要的经典方法,并实际操作它们。
独热编码(One-Hot Encoding)
一热编码可能是文本表示中最直观的方法之一。一个词的一热编码特征是该术语是否出现在文本中的二进制指示符。这是一种简单且容易解释的方法——判断一个词是否存在。为了更好地理解这一点,让我们看看我们去除词干之前的示例文本,并观察一热编码如何作用于特定的目标术语,例如 nlp。
让我们使用以下命令查看当前文本的样子:
txt_words_nostop
我们可以看到文本如下所示:
[['welcome', 'world', 'deep', 'learning', 'nlp'],
["'re", 'together', "'ll", 'learn', 'together'],
['nlp', 'amazing', 'deep', 'learning', 'makes', 'even', 'fun'],
['let', "'s", 'learn']]
我们感兴趣的词是 nlp。它的一热编码特征将如下所示:

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_04_06.jpg)
图 4.6:'nlp' 的一热编码特征
我们可以看到该特征为 1,但仅在包含术语 nlp 的句子中为 1,否则为 0。我们可以为每个我们感兴趣的词创建这样的指示变量。所以,如果我们对三个术语感兴趣,我们可以为每个术语创建这样的特征:

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_04_07.jpg)
图 4.7:'nlp'、'deep' 和 'learn' 的一热编码特征
让我们通过一个练习使用 Python 重新创建这个过程。
练习 4.03:为我们的数据创建一热编码
在这个练习中,我们将复制前面的示例。目标术语是 nlp、deep 和 learn。我们将使用我们自己的函数为这些术语创建一热编码特征,并将结果存储在一个 numpy 数组中。
同样,我们将使用在 练习 4.01 中创建的 txt_words_nostop 变量,即 分词、大小写标准化、标点符号处理和停用词移除。所以,你需要在同一个 Jupyter Notebook 中继续进行此练习。按照以下步骤完成本练习:
-
打印出
txt_words_nostop变量,看看我们正在处理的内容:print(txt_words_nostop)输出将如下所示:
[['welcome', 'world', 'deep', 'learning', 'nlp'], ["'re", 'together', "'ll", 'learn', 'together'], ['nlp', 'amazing', 'deep', 'learning', 'makes', 'even', 'fun'], ['let', "'s", 'learn']] -
定义一个包含目标术语的列表,即
"nlp","deep","learn":target_terms = ["nlp","deep","learn"] -
定义一个函数,该函数接受一个单一的分词句子,并根据目标术语是否出现在文本中返回
0或1。注意,输出的长度固定为3:def get_onehot(sent): return [1 if term in sent else 0 for term in target_terms]我们正在遍历目标术语,并检查它们是否出现在输入句子中。
-
对文本中的每个句子应用该函数,并将结果存储在一个变量中:
one_hot_mat = [get_onehot(sent) for sent in txt_words_nostop] -
导入
numpy,根据结果创建一个numpyarray,并打印出来:import numpy as np np.array(one_hot_mat)数组的输出如下:
array([[1, 1, 0], [0, 0, 1], [1, 1, 0], [0, 0, 1]])
我们可以看到输出包含四行,每行对应一个句子。数组中的每一列包含一个目标术语的一热编码。对于 "learn" 来说,其值是 0, 1, 0, 1,这与我们的预期一致。
注意
要访问此特定部分的源代码,请参见 packt.live/2VVNEgf。
你也可以在 packt.live/38Gr54r 上在线运行这个示例。你必须执行整个笔记本才能获得预期的结果。
在这个练习中,我们展示了如何使用独热编码从文本中生成特征。示例使用了一个目标术语列表。当你有一个非常明确的目标,知道哪些术语是有用的时,这种方法可能会有效。事实上,直到几年前,这正是人们广泛采用的方法,人们从文本中手工提取特征。在许多情况下,这并不可行——因为我们并不确切知道哪些术语是重要的,所以我们对大量术语(5000、10000,甚至更多)使用独热编码。
另一个方面是术语的存在/缺失是否足以应对大多数情况。我们是否不想包含更多的信息?也许是术语的频率,而不仅仅是它的存在,或者甚至可能是其他更智能的度量?让我们看看这如何工作。
术语频率
我们讨论过,独热编码只是表示某个术语是否存在。这里有一个合理的观点:术语的频率也很重要。可能在文档中出现次数更多的术语对该文档更为重要。也许通过术语的频率来表示比仅仅通过指示符更好。频率方法很简单——对于每个术语,统计它在特定文本中出现的次数。如果某个术语在文档/文本中缺失,它的频率为 0。我们对词汇表中的所有术语都做这个处理。因此,我们的特征数等于词汇表中单词的数量(这个数量我们可以选择;它可以被看作是一个超参数)。我们应该注意,在预处理步骤后,我们正在处理的“术语”是可能在语言中不是真正有效单词的标记(token):
注意:
词汇表是我们最终模型中将使用的所有术语的超集。词汇表大小是指词汇表中唯一术语的数量。你可能在原始文本中有 20000 个唯一的术语,但选择使用最频繁的 10000 个术语;这就是有效的词汇表大小。
考虑以下图像;如果我们有N个文档,并且在我们的工作词汇表中有V(t1, t2, t3 … tV)个词,那么数据的表示将是一个N × V维度的矩阵。

图 4.8:文档-术语矩阵
这个矩阵就是我们的文档-术语矩阵(DTM)——其中每一行代表一个文档,每一列代表一个术语。单元格中的值可以代表某种度量(计数或其他任何度量)。在这一部分,我们将处理术语频率。
我们可以再次创建自己的函数,但我们有一个非常方便的工具叫做'CountVectorizer',它在scikit-learn中可用,我们将使用它。让我们先通过导入该工具来熟悉它:
from sklearn.feature_extraction.text import CountVectorizer
向量化器可以处理原始文本,也可以处理分词后的数据(如我们的案例)。为了处理原始文本,我们将使用以下代码,在其中我们将从原始文本(txt_sents)创建一个包含词频的文档-术语矩阵(DTM)。
在我们开始之前,先快速查看一下这个变量的内容:
txt_sents
输出应该如下所示:
['welcome to the world of deep learning for nlp!',
"we're in this together, and we'll learn together.",
'nlp is amazing, and deep learning makes it even more fun.',
"let's learn!"]
注意
如果在处理活动 4.01时,txt_sents变量的内容被覆盖了,《爱丽丝梦游仙境》文本的文本预处理,你可以重新访问练习 4.01的第 3 步,标记化、大写规范化、标点符号和停用词去除,并重新定义该变量,使其内容与前面的输出匹配。
现在,让我们实例化矢量化器。请注意,我们需要提供词汇表的大小。这样会从数据中选择前* n *个术语来创建矩阵:
vectorizer = CountVectorizer(max_features = 5)
我们在这里选择了五个术语;结果将包含矩阵中的五列。让我们在数据上训练('fit')矢量化器:
vectorizer.fit(txt_sents)
现在,矢量化器已经学习了一个词汇表——前五个术语,并为词汇表中的每个术语创建了一个索引。让我们看看这个词汇表:
vectorizer.vocabulary_
前面的属性给出了如下输出:
{'deep': 1, 'we': 4, 'together': 3, 'and': 0, 'learn': 2}
我们可以看到已经选择的术语(前五个)。
现在,让我们将矢量化器应用于数据以创建 DTM。一个小细节:矢量化器的结果是一个稀疏矩阵。为了查看它,我们将其转换为数组:
txt_dtm = vectorizer.fit_transform(txt_sents)
txt_dtm.toarray()
看一下输出:
array([[0, 1, 0, 0, 0],
[1, 0, 1, 2, 2],
[1, 1, 0, 0, 0],
[0, 0, 1, 0, 0]], dtype=int64)
第二个文档(第二行)的最后两个术语的频率为2。那两个术语是什么呢?索引 3 和 4 分别是术语'together'和'we'。让我们打印出原始文本,看看输出是否符合预期:
txt_sents
输出将如下所示:
['welcome to the world of deep learning for nlp!',
"we're in this together, and we'll learn together.",
'nlp is amazing, and deep learning makes it even more fun.',
"let's learn!"]
这正如我们预期的那样,看起来计数矢量化器工作得很好。
请注意,矢量化器也对句子进行了标记化。如果你不想这样,并且希望使用预处理过的标记(txt_words_stem),只需要传递一个虚拟的标记化器和预处理器给CountVectorizer。让我们看看这样做的效果。首先,我们创建一个什么都不做的函数,它只会返回标记化后的句子/文档:
def do_nothing(doc):
return doc
现在,我们将实例化矢量化器,使用这个函数作为预处理器和标记化器:
vectorizer = CountVectorizer(max_features=5,
preprocessor=do_nothing,
tokenizer=do_nothing)
在这里,我们使用标记化器的fit_transform()方法一步完成数据拟合和转换,然后查看结果。该方法在对数据进行拟合时,会将唯一的术语识别为词汇表,然后在转换时统计并返回每个文档中每个术语的出现次数。让我们看看它是如何工作的:
txt_dtm = vectorizer.fit_transform(txt_words_stem)
txt_dtm.toarray()
输出数组将如下所示:
array([[0, 1, 1, 1, 0],
[1, 0, 1, 0, 2],
[0, 1, 1, 1, 0],
[0, 0, 1, 0, 0]], dtype=int64)
我们可以看到,输出与之前的结果不同。这种差异是预期的吗?为了理解,我们来看一下矢量化器的词汇:
vectorizer.vocabulary_
输出将如下所示:
{'deep': 1, 'learn': 2, 'nlp': 3, 'togeth': 4, "'ll": 0}
我们正在处理预处理后的数据,记得吗?我们已经移除了停用词并进行了词干提取。让我们试着打印一下输入数据,确保没有问题:
txt_words_stem
输出将如下所示:
[['welcom', 'world', 'deep', 'learn', 'nlp'],
["'re", 'togeth', "'ll", 'learn', 'togeth'],
['nlp', 'amaz', 'deep', 'learn', 'make', 'even', 'fun'],
['let', "'s", 'learn']]
我们可以看到,DTM(文档-词项矩阵)是根据新词汇表和预处理后获得的词频进行工作的。
所以,这是第二种从文本数据生成特征的方法,即使用术语的频率。在下一部分,我们将介绍另一种非常流行的方法。
TF-IDF 方法
文档中术语的高频率是否意味着该词对文档非常重要?不一定。如果该术语在所有文档中都很常见呢?在文本数据处理中有一个常见的假设:如果一个术语出现在所有文档中,那么它对于当前文档的区分度可能不大,或者说它对当前文档的意义不大。这个假设看起来是合理的。再举个例子,假设我们在处理手机评论时遇到“手机”这个词。这个词可能出现在大多数评论中。但如果你的任务是识别评论中的情感,这个词可能不会提供太多有用的信息。
我们可以提高在文档中出现但在整个数据中较为罕见的术语的重要性,降低在大多数文档中出现的术语的重要性。
TF-IDF 方法,即词频-逆文档频率,将逆文档频率(IDF)定义如下:

图 4.9:TF-IDF 的公式
n是文档的总数,而df(t)是包含术语t的文档数。这作为一个因子来调整术语频率。可以看出,它正如我们所期望的那样工作——它增加了稀有术语的重要性,减少了常见术语的重要性。请注意,这个公式有不同的变体,但我们将坚持使用scikit-learn所采用的方法。像CountVectorizer一样,TF-IDF 向量化器对句子进行分词并学习词汇表,但它返回的是调整后的(乘以 IDF 的)术语频率,而不是文档中术语的原始计数。
现在,让我们将这个有趣的新方法应用到我们的数据上。
练习 4.04:使用 TF-IDF 生成文档-词矩阵
在本次练习中,我们将实现从文本生成特征的第三种方法——TF-IDF。我们将使用 scikit-learn 的TfidfVectorizer工具,并为我们的原始文本数据创建文档-词矩阵(DTM)。由于我们使用的是本章之前创建的txt_sents变量,因此我们需要使用相同的 Jupyter Notebook。当前变量中的文本如下所示:
['welcome to the world of deep learning for nlp!',
"we're in this together, and we'll learn together.",
'nlp is amazing, and deep learning makes it even more fun.',
"let's learn!"]
注意
如果在处理活动 4.01(《爱丽丝梦游仙境》文本的文本预处理)时,txt_sents变量的内容被覆盖,可以重新访问练习 4.01的步骤 3(分词、大小写标准化、标点符号和停用词移除),并重新定义该变量,以便其内容与之前的输出匹配。
以下是需要执行的步骤:
-
从
scikit learn导入TfidfVectorizer工具:from sklearn.feature_extraction.text import TfidfVectorizer -
使用词汇表大小为
5来实例化vectorizer:vectorizer_tfidf = TfidfVectorizer(max_features=5) -
在原始数据
txt_sents上拟合vectorizer:vectorizer_tfidf.fit(txt_sents) -
打印出
vectorizer学习到的词汇表:vectorizer_tfidf.vocabulary_训练后的词汇表如下所示:
{'deep': 1, 'we': 4, 'together': 3, 'and': 0, 'learn': 2}请注意,词汇表与计数向量化器的词汇表相同。这是预期的。我们并没有改变词汇表;我们只是调整了它在文档中的重要性。
-
使用训练好的向量化器转换数据:
txt_tfidf = vectorizer_tfidf.transform(txt_sents) -
打印出结果的文档-术语矩阵(DTM):
txt_tfidf.toarray()输出结果如下:
array([[0\. , 1\. , 0\. , 0\. , 0\. ], [0.25932364, 0\. , 0.25932364, 0.65783832, 0.65783832], [0.70710678, 0.70710678, 0\. , 0\. , 0\. ], [0\. , 0\. , 1\. , 0\. , 0\. ]])我们可以清楚地看到,输出值与频率不同,并且小于 1 的值表明在与 IDF 相乘后,许多值已经降低。
-
我们还需要查看每个词汇表中术语的 IDF 值,以检查该因子是否如我们预期的那样起作用。使用
idf_属性打印术语的 IDF 值:vectorizer_tfidf.idf_输出结果如下:
array([1.51082562, 1.51082562, 1.51082562, 1.91629073, 1.91629073])
术语'and'、'deep'和'learn'的 IDF 较低,而术语'together'和'we'的 IDF 较高。这正如我们所预期的——术语'together'和'we'只出现在一个文档中,而其他词汇出现在两个文档中。因此,TF-IDF 方法确实赋予了稀有词汇更高的重要性。
注意
要访问此特定部分的源代码,请参考 packt.live/2VVNEgf。
你也可以在网上运行此示例,访问 packt.live/38Gr54r。你必须执行整个 Notebook 才能获得所需的结果。
在这个练习中,我们看到了如何使用 TF-IDF 方法来表示文本。我们还看到了该方法如何通过注意到 IDF 值在高频词汇中的较低值来降低更频繁的术语的重要性。最后,我们得到了一个包含术语 TF-IDF 值的文档-术语矩阵(DTM)。
总结经典方法
我们刚刚看了三种经典的文本表示方法。我们从独热编码开始,其中术语的特征只是标记其在文档中的存在。基于计数/频率的方法试图通过使用术语在文档中的频率来增加术语的重要性。TF-IDF 方法试图使用术语的"归一化"重要性值,考虑术语在文档中的常见程度。
到目前为止我们讨论的三种方法都属于"词袋模型"表示方法。那么,为什么它们被称为"词袋模型"呢?原因有几个。第一个原因是它们不会保留词汇的顺序——一旦进入词袋,术语/词汇的位置就不重要。第二个原因是这种方法保留了每个单独术语的特征。所以,对于每个文档来说,从某种意义上讲,你有一个"混合的词汇袋",或者为了简单起见,称为"词袋"。
所有三种方法的结果具有N × V的维度,其中N是文档数量,V是词汇表的大小。请注意,三种表示方式都非常稀疏——一个典型的句子非常短(可能只有 20 个单词),但词汇表的大小通常在数千个单词左右,导致大多数 DTM 的单元格都是 0。这看起来并不理想。好吧,还有这种表示方法的其他几个缺点,稍后我们会看到,这些缺点导致了基于深度学习的表示方法的成功。接下来,我们来讨论这些思想。
文本的分布式表示
为什么词嵌入如此受欢迎?为什么我们说它们非常强大?是什么让它们如此特别?为了理解和欣赏词嵌入,我们需要承认迄今为止表示方法的不足之处。
"人行道"和"侧道"是同义词。你认为我们迄今为止讨论的方法能够捕捉到这一信息吗?好吧,你可以手动将"侧道"替换为"人行道",这样两者最终就会有相同的词标记,但你能为语言中的所有同义词做这件事吗?
"热"和"冷"是反义词。之前的词袋模型表示能够捕捉到这一点吗?那么"狗"是"动物"的一种类型呢?"驾驶舱"是"飞机"的一部分呢?区分狗的叫声和树皮的声音呢?你能手动处理所有这些情况吗?
上述所有例子都是术语之间的“语义关联”——简而言之,它们的含义在某种程度上是相互关联的。词袋模型无法捕捉到这些关联。这就是分布式语义学的作用所在。分布式语义学的核心思想是,具有相似分布的词语具有相似的含义。
一个快速、有趣的小测验:从以下文本中猜测小毛孩这个词的意思:
"我一个月前收养了一只年轻的波斯小毛孩。像所有的小毛孩一样,它喜欢抓背部,讨厌水,但和其他小毛孩不同,它在抓老鼠方面完全失败了。"
你可能猜得对:小毛孩是指猫。这个很简单,对吧?
但你是怎么做到的呢?文中从未使用过“猫”这个词。你查看了上下文(围绕“小毛孩”的词语),并根据你对语言和世界的理解,推测这些词通常和猫相关。你直觉地运用了这个概念:具有相似意义的词语出现在相似的语境中。如果“小毛孩”和“猫”出现在相似的语境中,它们的意思一定相似。
"你将通过一个词所处的环境了解这个词的含义。"
这句由约翰·弗斯(John Firth)所说的名言很经典,现在已被过度使用,但正因为如此它才被过度使用。让我们看看这个概念是如何在词嵌入中运用的。
词嵌入与词向量
词嵌入是将每个术语表示为一个低维度的向量。术语的独热编码表示也是一个向量,但其维度通常达到几千。词嵌入/词向量具有更低的维度,并且来自基于分布语义的方法——本质上,表示捕捉到的是这样一种观念:具有相似意义的词语出现在相似的上下文中。
词向量试图捕捉术语的含义。这一思想使得它们非常强大,当然,前提是它们已经被正确创建。使用词向量时,像加法/减法向量和点积这样的向量操作是可能的,而且有一些非常有趣的意义。另一个非常棒的特性是,含义相似的项在空间上更接近。这一切都导致了一些令人惊奇的结果。
一个非常有趣的结果是,词向量可以在类比任务上表现得非常好。类比任务被定义为这种格式的任务——“a 与 b 之关系,x 与 ? 的关系是?”——也就是说,找到一个与 x 之间关系相同的实体,正如 b 与 a 之间的关系。举个例子,如果你问“man 对 uncle 如 woman 对 ?”,结果会是 "aunt"(稍后会详细讲解)。你还可以发现术语之间的语义规律——术语与术语集之间的关系。让我们看看下图,它基于词向量/嵌入,帮助我们更好地理解:

图 4.10:术语之间的语义关系
上述图示展示了一些例子。向量可以具有很高的维度(最多可达 300 甚至更多),因此进行二维的降维处理以便进行可视化。两个术语之间的虚线连接表示术语之间的关系。连接的方向是关键。在左侧面板中,我们可以看到,连接 slow 和 slower 的线段与连接 short 和 shorter 的线段是平行的。这是什么意思?这意味着词嵌入学到的知识是,short 和 shorter 之间的关系与 slow 和 slower 之间的关系是相同的。同样,嵌入学习到的知识是,clearer 和 clearest 之间的关系与 darker 和 darkest 之间的关系是相同的。很有趣吧?
类似地,图 4.10 的右侧展示了嵌入学到的知识:sir 和 madam 之间的关系与 king 和 queen 之间的关系是相同的。嵌入还捕捉到了术语之间的其他语义关联,这些我们在上一节中已经讨论过了。是不是很棒?
这在我们之前讨论过的方法中是做不到的。词嵌入真正围绕术语的“意义”进行工作。希望你已经能够欣赏到词向量的实用性和强大功能。如果你还不相信,我们很快就会用它们,并亲自看到这一切。
要生成词嵌入,我们可以使用几种不同的算法。我们将讨论两种主要的算法,并介绍一些其他常见的算法。我们将看到分布式语义方法是如何被用来推导这些词嵌入的。
word2vec
在学校时,为了测试我们是否理解某些词汇的意思,语言老师常常使用一种非常流行的技巧:“填空题”。根据周围的词汇,我们需要找出最适合填补空白的词。如果你理解了词汇的意思,你就能做得很好。想一想——这不正是分布式语义吗?
在 ‘furbaby’ 示例中,你可以预测出词汇'cat',因为你理解了与之相关的上下文和词汇。这个练习实际上就是一个“填空”练习。你能填补这个空白,因为你理解了'cat'的意思。
如果你能够根据某个词汇周围的上下文预测出该词,那么你就理解了这个词的意思。
这个简单的思想正是word2vec算法背后的公式。word2vec算法/过程实际上是一个预测练习,一种庞大的“填空”练习。简而言之,算法的工作原理如下:
给定上下文词汇,预测缺失的目标词。
就是这样。word2vec算法根据上下文预测目标词。接下来我们来理解这些是如何被定义的。
以句子“波斯猫吃鱼并讨厌洗澡”为例。我们将上下文定义为目标词左右一定数量的词,对于本例来说,假设'cat'是目标词,我们选择其左右两边的两个词作为上下文:

图 4.11:“cat”作为目标词
这五个词汇共同形成一个“窗口”,目标词位于中央,上下文词汇则围绕其周围。在这个例子中,由于我们选择了目标词左右各两个词作为上下文,因此窗口大小为 2(稍后会详细介绍这些参数)。窗口是滑动的,它会在句子中的词汇上滑动。下一个窗口将以'eats'为中心,而'cat'则成为新的上下文词汇:

图 4.12:目标词的窗口
C1、C2、C3 和 C4 表示每个窗口的上下文。在 C3 中,“fish”是目标词,它是通过“cat”、“eats”、“and”和“hates”这些词汇预测出来的。公式已经很清楚了,但模型是如何学习这些表示的呢?我们接下来讨论这个问题:

图 4.13:带有示例的 CBOW 架构
上图所示的模型使用了一个带有单一隐藏层的神经网络。输出层是用于目标词的,并且是通过V个输出进行独热编码,每个词一个输出——预测的词是'cat',它会在输出中变成'hot'。上下文词的输入层大小也是V,但它对上下文中的所有词都会有响应。隐藏层的维度是V x D(其中D是向量的维度)。这个隐藏层就是学习这些神奇词表示的地方。请注意,只有一个输入层,正如权重矩阵W所示。
在网络训练的过程中,随着每次迭代,预测目标词的准确性逐步提高,隐藏层的参数也在不断更新。这些参数实际上是每个词的 D 维向量。这个 D 维向量就是我们该词的词嵌入。当迭代完成后,我们将学到词汇表中所有词的词嵌入。很棒,不是吗?
我们刚才讨论的方法是用于训练词向量的 CBOW 方法。上下文是一个简单的词袋(正如我们在上一节讨论的经典方法中提到的;顺序不重要,记住这一点),因此得名。还有另一种流行的方法,叫做 Skip-gram 方法,它与 CBOW 方法相反——它是根据中心词来预测上下文词汇。这个方法一开始可能看起来不太直观,但效果很好。稍后我们会在本章中讨论 CBOW 和 Skip-gram 方法的结果差异:

图 4.14:Skip-gram 架构
让我们在 Python 中实际操作一下 CBOW 方法。我们将创建自己的词嵌入,并评估是否能够得到我们之前所声称的惊人结果。
训练我们自己的词嵌入
word2vec算法有许多不同的软件包实现。我们将使用Gensim中的实现,它是处理许多自然语言处理任务的优秀工具包。Gensim 中的 word2vec 实现接近 Mikolov 等人在 2013 年发布的原始论文(arxiv.org/pdf/1301.3781.pdf)。Gensim 还支持其他词嵌入算法;稍后我们会详细讨论。
如果你没有安装 Gensim,可以通过在 Jupyter Notebook 中输入以下命令来安装:
!pip install gensim
我们将使用的数据集是text8语料库(mattmahoney.net/dc/textdata.html),它来自维基百科的前十亿个字符。因此,它应该涵盖各种主题的数据,而不是某一个特定领域。方便的是,Gensim 提供了一个工具(downloader API)来读取这些数据。在从 Gensim 导入downloader工具后,让我们来读取这些数据:
import gensim.downloader as api
dataset = api.load("text8")
此步骤下载text8数据,下载时间可能较长,具体取决于您的网络连接情况。或者,数据可以通过以下链接下载并使用 Gensim 中的Text8Corpus工具读取,如以下代码所示:packt.live/3gKXU2D。
from gensim.models import word2vec
dataset = word2vec.Text8Corpus("text8")
text8数据现在作为一个可迭代对象,可以直接传递给word2vec算法。
在训练嵌入之前,为了使结果具有可重复性,我们使用 NumPy 将随机数生成的种子设置为1:
np.random.seed(1)
注意
尽管我们已设置了种子,但结果的变化仍然有其他原因。其中一些是由于您的系统上 Python 版本可能使用的内部哈希种子。使用多个核心也可能导致结果变化。无论如何,尽管您看到的值可能不同,结果的顺序可能有所变化,但您看到的输出应该与我们的输出大致一致。请注意,这适用于本章中与词向量相关的所有实际操作。
现在,让我们通过使用word2Vec方法来训练我们的第一个词嵌入:
model = word2vec.Word2Vec(dataset)
这可能需要一两分钟,或者更短,具体取决于您的系统。完成后,我们将得到训练好的词向量,并可以使用多个方便的工具来处理这些词向量。让我们来访问某个术语的词向量/嵌入表示:
print(model.wv["animal"])
输出结果如下所示:

图 4.15: "动物"的嵌入表示
你拥有一系列的数字——表示该术语的向量。让我们来计算这个向量的长度:
len(model.wv["animal"])
向量的长度如下所示:
100
每个术语的表示现在是一个长度为 100 的向量(这个长度是一个超参数,我们可以更改;我们使用了默认设置来开始)。任何术语的向量都可以像之前那样访问。另一个方便的工具是most_similar()方法,它帮助我们找到与目标术语最相似的术语。让我们看看它的实际应用:
model.wv.most_similar("animal")
输出结果如下所示:
[('insect', 0.7598186135292053),
('animals', 0.729228138923645),
('aquatic', 0.6679497957229614),
('insects', 0.6522265672683716),
('organism', 0.6486647725105286),
('mammal', 0.6478426456451416),
('eating', 0.6435647010803223),
('ants', 0.6415578722953796),
('humans', 0.6414449214935303),
('feces', 0.6313734650611877)]
输出结果是一个元组列表,每个元组包含一个术语及其与“animal”术语的相似度得分。
我们可以看到insect(昆虫)、animals(动物)、insects(昆虫)、和mammal(哺乳动物)是与“animal”(动物)最相似的术语。这看起来是一个非常好的结果,对吧?但相似度是如何计算的呢?单词是通过向量来表示的,而这些向量试图捕捉意义——术语之间的相似度实际上是它们对应向量之间的相似度。most_similar()方法使用余弦相似度来计算向量之间的相似度,并返回具有最高值的术语。结果中每个术语对应的值是与目标词向量的余弦相似度。
在这里,余弦相似度测量很合适,因为我们期望在意义上相似的术语在空间上是在一起的。余弦相似度是向量之间夹角的余弦值。具有相似意义和表示的术语将具有接近 0 的角度和接近 1 的相似度分数,而完全无关的意义将具有接近 90 的角度和接近 0 的余弦相似度。让我们看看模型在"幸福"相关的顶级术语上学到了什么:
model.wv.most_similar("happiness")
最相似的项目结果如下(最相似的排在前面):
[('humanity', 0.7819231748580933),
('perfection', 0.7699881792068481),
('pleasure', 0.7422512769699097),
('righteousness', 0.7402842044830322),
('desires', 0.7374188899993896),
('dignity', 0.7189303040504456),
('goodness', 0.7103697657585144),
('fear', 0.7047020196914673),
('mankind', 0.7046756744384766),
('salvation', 0.6990150213241577)]
人类,人类,善良,正义和同情 -- 我们在这里学到了一些生活经验。看起来它已经学到了许多人似乎一辈子也搞不清楚的东西。请记住,这只是一系列矩阵乘法。
词嵌入中的语义规律
我们之前提到这些表示捕捉到了语言中的规律,并且很擅长解决简单的类比任务。向量嵌入之间的偏移似乎捕捉到了单词之间的类比关系。因此,例如,"king" - "man" + "woman" 期望结果是 "queen"。让我们看看我们在text8语料库上训练的模型是否也理解了一些规律。
我们将在这里使用most_similar()方法,该方法允许我们相互添加和减去向量。我们将提供'king'和'woman'作为要添加的向量,使用'man'来从结果中减去,然后查看与生成向量最相似的五个术语:
model.wv.most_similar(positive=['woman', 'king'], \
negative=['man'], topn=5)
输出将如下所示:
[('queen', 0.6803990602493286),
('empress', 0.6331825852394104),
('princess', 0.6145625114440918),
('throne', 0.6131302714347839),
('emperor', 0.6064509153366089)]
最高结果是'queen'。看起来模型捕捉到了这些规律。让我们试试另一个例子。"Man" 对应 "uncle",就如同 "woman" 对应于?或者用数学形式表达,uncle - man + woman = ? 的结果是最接近的向量是什么?
model.wv.most_similar(positive=['uncle', 'woman'], \
negative=['man'], topn=5)
以下是前述代码的输出:
[('aunt', 0.8145735263824463),
('grandmother', 0.8067640066146851),
('niece', 0.7993890643119812),
('wife', 0.7965766787528992),
('widow', 0.7914236187934875)]
看起来效果不错。请注意,所有前五个结果都是女性性别的。因此,我们取出了'uncle',去除了男性元素,添加了女性元素,现在我们得到了一些非常好的结果。
让我们看看一些其他矢量算术的例子。我们也可以对两个不同术语的矢量进行平均,以得到短语的矢量。让我们自己试试吧。
注意
通过对个体向量的平均值来创建短语向量仅仅是到达短语向量的众多方式之一。变化范围从加权平均到更复杂的数学函数。
练习 4.05:短语的向量
在这个练习中,我们将通过对两个不同短语get happy和make merry的个体向量取平均值来创建向量。我们将找到这些短语表示之间的相似性。您需要在我们已经在本章节中使用的同一个 Jupyter Notebook 中继续这个练习。按照以下步骤完成此练习:
-
提取术语"get"的向量并将其存储在一个变量中:
v1 = model.wv['get'] -
提取"happy"的向量并将其存储在一个变量中:
v2 = model.wv['happy'] -
将两个向量的元素逐项平均,创建一个向量,
(v1 + v2)/2。这是我们整个短语“get happy”的向量:res1 = (v1+v2)/2 -
类似地,提取"make"和"merry"的向量:
v1 = model.wv['make'] v2 = model.wv['merry'] -
通过对单个向量取平均值来创建该短语的向量:
res2 = (v1+v2)/2 -
使用模型中的
cosine_similarities()方法,计算这两个向量之间的余弦相似度:model.wv.cosine_similarities(res1, [res2])余弦相似度的结果如下:
array([0.5798107], dtype=float32)
结果是一个大约为0.58的余弦相似度,这是一个正值,比0要高得多。这意味着模型认为短语“get happy”和“make merry”的含义相似。还不错吧?我们不仅仅用了简单的平均值,还可以使用加权平均,或者提出更复杂的方法来组合单个向量。
注意
要访问此特定部分的源代码,请参考packt.live/2VVNEgf。
你也可以在线运行这个示例,网址是packt.live/38Gr54r。你必须执行整个笔记本才能获得期望的结果。
在这个练习中,我们看到如何使用向量运算来表示短语,而不是单个术语,我们发现含义仍然被捕捉到了。这带来了一个非常重要的教训——词嵌入的向量运算是有意义的。
这些向量运算操作作用于术语的含义,产生了一些非常有趣的结果。
我们希望你现在能理解词嵌入的强大功能。我们意识到这些结果仅来自一些矩阵乘法,并且只需要花费一分钟时间来训练我们的数据集。词嵌入几乎是神奇的,令人愉快的是,简单的预测公式竟然能产生如此强大的表示。
当我们之前创建单词向量时,并没有过多关注控制项/参数。它们有很多,但只有一些对表示质量有显著影响。接下来我们将了解word2vec算法的不同参数,并亲自查看更改这些参数的效果。
参数的影响 —— 向量的“大小”
word2vec算法的size参数是每个术语向量的长度。默认情况下,正如我们之前看到的,这个长度是 100。我们将尝试减少这个参数,并评估结果的差异(如果有的话)。这次我们将size设置为 30 并重新训练词嵌入:
model = word2vec.Word2Vec(dataset, size=30)
现在,让我们检查之前的类比任务,也就是king - man + woman:
model.wv.most_similar(positive=['woman', 'king'], \
negative=['man'], topn=5)
这应该会给我们以下输出:
[('emperor', 0.8314059972763062),
('empress', 0.8250986933708191),
('son', 0.8157491683959961),
('prince', 0.8060941696166992),
('archbishop', 0.8003251552581787)]
我们可以看到queen没有出现在前五个结果中。看起来,使用非常低的维度时,我们没有在表示中捕捉到足够的信息。
参数的影响 —— “窗口大小”
window size 参数定义了上下文;具体来说,窗口大小是指在构建上下文时,目标词左右两侧的词汇数量。这个参数的影响并不是特别明显。一般观察是,当你使用更大的窗口大小(比如 20)时,最相似的词似乎是与目标词一起使用的词,并不一定具有相似的意义。另一方面,减少窗口大小(比如设为 2)会返回那些在意义上非常相似的词,并且在很多情况下是同义词。
Skip-gram 与 CBOW
选择 Skip-gram 和 CBOW 作为学习算法的方式是通过设置 sg = 1 来选择 Skip-gram(默认值是 sg = 0,即 CBOW)。回顾一下,Skip-gram 方法是基于中心目标词来预测上下文词。而这正好与 CBOW 相反,CBOW 是通过上下文词来预测目标词。那么我们该如何在两者之间做出选择呢?一个比另一个有什么优势吗?为了亲自验证,让我们使用 Skip-gram 训练词嵌入,并将一些结果与 CBOW 的结果进行比较。首先,拿 CBOW 的一个具体例子来说。我们将通过不指定 size 参数来重新创建默认向量大小的 CBOW 词向量。Oeuvre 是指艺术家/表演者的创作总和。我们将查看不常见词 oeuvre 的最相似词:
model = word2vec.Word2Vec(dataset)
model.wv.most_similar("oeuvre", topn=5)
以下是最相似的词汇:
[('baglione', 0.7203884124755859),
('chateaubriand', 0.7119786143302917),
('kurosawa', 0.6956337690353394),
('swinburne', 0.6926312446594238),
('poetess', 0.6910216808319092)]
我们可以看到,大多数结果是艺术家的名字(swinburne、kurosawa 和 baglione)或食物菜肴(chateaubriand)。前五个结果中没有任何一个与目标词的意义相近。现在,让我们使用 Skip-gram 方法重新训练我们的向量,并查看在同一任务上的结果:
model_sg = word2vec.Word2Vec(dataset, sg=1)
model_sg.wv.most_similar("oeuvre", topn=5)
这给我们带来了以下输出:
[('masterful', 0.8347533345222473),
('orchestration', 0.8149941563606262),
('mussorgsky', 0.8116796016693115),
('showcasing', 0.8080146312713623),
('lithographs', 0.805435299873352)]
我们可以看到,最相似的词在意义上要更接近(masterful、orchestration、showcasing)。因此,Skip-gram 方法似乎对于稀有词更有效。
为什么会这样呢?CBOW 方法通过有效地对所有上下文词进行平均,平滑了大量的分布统计(记住,所有上下文词一起作为输入),而 Skip-gram 则没有。在数据集较小的情况下,CBOW 进行的平滑处理是值得期待的。如果你拥有一个小型或中等大小的数据集,并且你关心稀有词的表示,那么 Skip-gram 是一个不错的选择。
训练数据的影响
在训练词向量时,一个非常重要的决定是使用的基础数据。模式和相似性将从你提供给算法的数据中学习,我们预期模型会从不同领域、不同设置等的数据中以不同的方式学习。为了更好地理解这一点,我们加载来自不同背景的不同语料库,看看嵌入是如何变化的。
Brown 语料库是一个通用文本集合,收集自 15 个不同的主题,使其具有广泛性(包括政治、宗教、书籍、音乐及其他许多主题)。它包含 500 个文本样本和约 100 万字。 "电影"语料库包含来自 IMDb 的电影评论数据。这两个语料库都可以在 NLTK 中使用。
练习 4.06:在不同数据集上训练词向量
在本练习中,我们将基于 Brown 语料库和 IMDb 电影评论语料库训练我们自己的词向量。我们将评估所学习的表示法差异及其底层训练数据的影响。按照以下步骤完成本练习:
-
从 NLTK 导入 Brown 和 IMDb 电影评论语料库:
nltk.download('brown') nltk.download('movie_reviews') from nltk.corpus import brown, movie_reviews -
这些语料库提供了一个方便的方法
sent()来提取单独的句子和单词(已标记的句子,可以直接传递给word2vec算法)。由于这两个语料库比较小,我们使用 Skip-gram 方法来创建嵌入:model_brown = word2vec.Word2Vec(brown.sents(), sg=1) model_movie = word2vec.Word2Vec(movie_reviews.sents(), sg=1)现在我们有了两个在不同上下文中学习到的相同术语的词嵌入。让我们看看在 Brown 语料库上训练的模型中,
money的最相似词是什么。 -
打印出从 Brown 语料库中学习到的模型中,与
money最相似的 前五个词:model_brown.wv.most_similar('money', topn=5)以下是前面代码的输出:
[('job', 0.8477444648742676), ('care', 0.8424298763275146), ('friendship', 0.8394286632537842), ('risk', 0.8268661499023438), ('permission', 0.8243911862373352)]我们可以看到,排名第一的词是
'job',这也合理。让我们看看该模型在电影评论中学到了什么。 -
打印出与
money最相似的五个词,它们来自从电影语料库学习的模型:model_movie.wv.most_similar('money', topn=5)以下是排名靠前的词:
[('cash', 0.7299771904945374), ('ransom', 0.7130625247955322), ('record', 0.7028014063835144), ('risk', 0.6977001428604126), ('paid', 0.6940697431564331)]
排名靠前的词是 cash 和 ransom。考虑到电影评论中使用的语言,这一点并不令人惊讶。
注意
要访问此部分的源代码,请参阅 packt.live/2VVNEgf。
你也可以在线运行这个示例,网址为 packt.live/38Gr54r。你必须执行整个 Notebook 才能获得期望的结果。
在本练习中,我们使用不同的数据集创建了词向量,并发现相同术语的表示和所学习的关联很大程度上受到底层数据的影响。因此,选择数据时要谨慎。
使用预训练的词向量
到目前为止,我们使用可访问的小数据集训练了我们自己的词嵌入。斯坦福 NLP 小组已经在 60 亿个标记和 40 万个词汇表项上训练了词嵌入。单独来说,我们可能没有足够的资源来处理这样的规模。幸运的是,斯坦福 NLP 小组慷慨地将这些训练好的词嵌入公开,使像我们这样的人可以从他们的工作中受益。这些训练好的嵌入可以在 GloVe 页面上找到 (nlp.stanford.edu/projects/glove/)。
关于 GloVe 的简要说明:用于训练的方法略有不同。目标已经修改,使得相似的词汇在空间中更为接近,以一种更为显式的方式。你可以在 GloVe 项目页面上阅读详细信息(nlp.stanford.edu/projects/glove/),该页面也链接到原始的提案论文。最终结果,然而,在性能上与 word2vec 非常相似。
我们将从 GloVe 项目页面下载glove.6B.zip文件。该文件包含 50D、100D、200D 和 300D 向量。我们在这里将使用 100D 向量。请解压文件,并确保你在工作目录中有文本文件。这些训练好的向量以文本文件形式提供,格式略有不同。我们将使用 Gensim 中可用的glove2word2vec工具,将其转换为 Gensim 可以轻松加载的格式:
from gensim.scripts.glove2word2vec import glove2word2vec
glove_input_file = 'glove.6B.100d.txt'
word2vec_output_file = 'glove.6B.100d.w2vformat.txt'
glove2word2vec(glove_input_file, word2vec_output_file)
我们指定了输入和输出文件,并运行了glove2word2vec工具。顾名思义,该工具将 GloVe 格式的词向量转换为word2vec格式。之后,word2vec模型就可以轻松理解这些嵌入。现在,我们来加载从文本文件(已重新格式化)中获取的keyed词向量:
from gensim.models.keyedvectors import KeyedVectors
glove_model = KeyedVectors.load_word2vec_format\
("glove.6B.100d.w2vformat.txt", binary=False)
完成这一步后,我们已经在模型中加入了 GloVe 嵌入,并且拥有了与 word2vec 嵌入模型相同的所有实用工具。接下来,我们来看看与"money"最相似的前几个词:
glove_model.most_similar("money", topn=5)
输出结果如下:
[('funds', 0.8508071899414062),
('cash', 0.848483681678772),
('fund', 0.7594833374023438),
('paying', 0.7415367364883423),
('pay', 0.740767240524292)]
为了收尾,我们还检查一下这个模型在 king 和 queen 任务上的表现:
glove_model.most_similar(positive=['woman', 'king'], \
negative=['man'], topn=5)
以下是前述代码的输出:
[('queen', 0.7698541283607483),
('monarch', 0.6843380928039551),
('throne', 0.6755737066268921),
('daughter', 0.6594556570053101),
('princess', 0.6520533561706543)]
现在我们已经将这些嵌入加入模型中,我们可以像之前处理我们创建的嵌入那样操作它们,并可以受益于贡献组织提供的更大数据集、词汇库和处理能力。
嵌入中的偏差——警告
在讨论规律性和类比时,我们看到了以下例子:
king – man + woman = queen
很高兴看到这些嵌入通过学习文本数据来捕捉这些规律性。我们再试一个与职业相关的例子。让我们看看与doctor – man + woman最接近的词:
model.wv.most_similar(positive=['woman', 'doctor'], \
negative=['man'], topn=5)
关于前五个结果的输出将如下所示:
[('nurse', 0.6464251279830933),
('child', 0.5847542881965637),
('teacher', 0.569127082824707),
('detective', 0.5451491475105286),
('boyfriend', 0.5403486490249634)]
结果不是我们想要的。医生是男性,而女性则是护士?我们再试另一个例子。这次,让我们看看模型对于女性和“聪明”对应男性的看法:
model.wv.most_similar(positive=['woman', 'smart'], \
negative=['man'], topn=5)
我们得到了以下前五个结果:
[('cute', 0.6156168580055237),
('dumb', 0.6035820245742798),
('crazy', 0.5834532976150513),
('pet', 0.582811713218689),
('fancy', 0.5697714686393738)]
我们可以看到,前几个词是'cute'、'dumb'和'crazy'。这实在不好。
这里发生了什么?这个看似很棒的表示方法是性别歧视吗?word2vec 算法是否存在性别歧视?结果的词向量中确实存在偏见,但想想这些偏见是从哪里来的。问题出在基础数据上,它在使用'nurse'表示女性的上下文中,而'doctor'则用于男性。因此,偏见来源于基础文本,而不是算法本身。
这个话题最近引起了广泛关注,围绕如何评估并消除学习到的嵌入中的偏见的研究仍在进行中,但一个好的方法是从一开始就避免数据中的偏见。如果你在 YouTube 评论上训练词嵌入,不要惊讶于它们包含各种极端的偏见。最好避免使用你怀疑包含偏见的文本数据。
其他值得注意的词嵌入方法
我们主要使用了 word2vec 方法,并简要了解了 GloVe 方法。虽然这些是最流行的方法,但还有一些其他值得一提的方法:
FastText:由Facebook 的人工智能研究(FAIR)实验室创建,它利用子词信息来丰富词嵌入。你可以在官方页面上阅读更多内容(research.fb.com/downloads/fasttext/)。
WordRank:将嵌入问题视为词排名问题。它在多个任务中的表现与 word2vec 相似。你可以在arxiv.org/abs/1506.02761上阅读更多相关内容。
除了这些,一些流行的库现在已经提供了预训练的嵌入(SpaCy 就是一个很好的例子)。选择很多。我们不能在这里详细讨论这些选择,但请务必探索这些选项。
在本章中,我们讨论了许多关于表示的观点。现在,让我们通过一个活动来实现这些想法。
活动 4.02:爱丽丝梦游仙境的文本表示
在前一个活动中,我们对文本进行了分词和基本的预处理。在本活动中,我们将通过使用文本表示方法来推进这一过程。你将从数据中创建自己的嵌入,并查看我们所拥有的关系类型。你还将利用预训练的嵌入来表示文本中的数据。
注意
请注意,在继续本活动之前,你需要完成活动 4.01、《爱丽丝梦游仙境》文本预处理。在该活动中,我们对文本进行了停用词移除。
你需要执行以下步骤:
我们将继续使用之前在活动 4.01、《爱丽丝梦游仙境》文本预处理中使用的相同 Jupyter Notebook。我们将处理在该活动中获得的去除停用词步骤的结果(假设它存储在名为alice_words_nostop的变量中)。打印出结果中的前三个句子。
-
从 Gensim 中导入
word2vec并使用默认参数训练你的词嵌入。 -
查找与
rabbit最相似的词语。 -
使用窗口大小为 2,重新训练词向量。
-
查找与
rabbit最相似的词语。 -
使用 Skip-gram 方法,并将窗口大小设为
5,重新训练词向量。 -
查找与
rabbit最相似的词语。 -
通过对
white和rabbit的词向量进行平均,找到white rabbit这个短语的表示。 -
通过对
mad和hatter的词向量进行平均,找到mad hatter的表示。 -
计算这两个短语之间的余弦相似度。
-
加载预训练的 100 维 GloVe 词嵌入。
-
查找
white rabbit和mad hatter的表示。 -
计算这两个短语之间的余弦相似度。余弦相似度有变化吗?
通过本次活动,我们将得到自己训练的词向量,这些词向量基于《爱丽丝梦游仙境》进行训练,并且可以表示文本中出现的术语。
注意
本活动的详细步骤、解决方案以及额外的评论内容可以在 407 页找到。
总结
本章开始时,我们讨论了文本数据的特殊性,以及歧义性如何使自然语言处理(NLP)变得困难。我们提到,处理文本时有两个关键的概念——预处理和表示。我们讨论了预处理中的许多任务,即如何清理和准备数据以便进行分析。我们还看到了去除数据中不完美部分的各种方法。
表示是下一个重要方面——我们理解了表示文本时需要考虑的因素,以及如何将文本转化为数字。我们回顾了各种方法,从经典方法开始,包括独热编码、基于计数的方法和 TF-IDF 方法。
词嵌入是一种全新的文本表示方法,借鉴了分布式语义学的思想——在相似上下文中出现的术语具有相似的含义。word2vec 算法巧妙地利用了这个思想,通过设定预测问题来预测目标词语,给定上下文来预测目标词语。它使用神经网络进行预测,并在此过程中学习词汇的向量表示。
我们发现这些表示非常令人惊讶,因为它们似乎能够捕捉到意义,而且简单的算术运算给出了非常有趣且富有意义的结果。你甚至可以通过词向量来创建短语,甚至是句子或文档的表示。这为后续我们在更复杂的深度学习架构中使用词嵌入技术处理 NLP 任务打下了基础。
在下一章,我们将继续探索序列,应用深度学习方法,如递归神经网络(RNN)和一维卷积(1D 卷积)。
第五章:5. 深度学习在序列中的应用
概述
在本章中,我们将实现基于深度学习的序列建模方法,在了解处理序列时需要注意的事项后开始。我们将从递归神经网络(RNNs)开始,这是一个直观的序列处理方法,已提供了最先进的结果。接着我们将讨论并实现 1D 卷积作为另一种方法,并对比其与 RNN 的效果。我们还将把 RNN 与 1D 卷积结合在一个混合模型中。我们将在一个经典的序列处理任务——股票价格预测上使用这些模型。到本章结束时,你将能够熟练地实现深度学习序列方法,特别是普通 RNN 和 1D 卷积,并为更先进的基于 RNN 的模型奠定基础。
引言
假设你正在处理文本数据,目标是构建一个模型,检查一个句子是否语法正确。考虑以下句子:“words? while sequence be this solved of can the ignoring”。 这个问题没有意义,对吧?那么,下面的句子怎么样?“Can this be solved while ignoring the sequence of words?”
突然间,文本变得完全有意义。那么,关于处理文本数据我们得出什么结论呢?序列很重要。
在评估一个句子是否语法正确的任务中,序列是非常重要的。忽略序列的模型会在任务中惨败。这个任务的性质要求你分析术语的顺序。
在上一章中,我们处理了文本数据,讨论了表示的相关思想,并创建了我们自己的词向量。文本和自然语言数据有一个重要的特性——它们具有顺序性。文本数据是序列数据的一种例子,但序列无处不在:从语音到股价,从音乐到全球气温。在本章中,我们将开始以一种考虑元素顺序的方式处理顺序数据。我们将从 RNN 开始,这是一种深度学习方法,利用数据的序列性为诸如机器翻译、情感分析、推荐系统和时间序列预测等任务提供有见地的结果。然后,我们将研究如何使用卷积处理序列数据。最后,我们将看到如何将这些方法结合在一个强大的深度学习架构中。过程中,我们还将构建一个基于 RNN 的股票价格预测模型。
与序列的工作
让我们看一个例子,以更清楚地说明序列建模的重要性。任务是预测某公司未来 30 天的股票价格。提供给你的数据是今天的股票价格。你可以在以下图表中看到,y 轴表示股票价格,x 轴表示日期。这样的数据足够吗?

图 5.1:仅用 1 天数据的股票价格
当然,单一数据点,即某一天的价格,无法预测接下来 30 天的价格。我们需要更多的信息。特别是,我们需要关于过去的信息——股票价格在过去几天/月/年的走势。因此,我们请求并获得了三年的数据:

图 5.2:使用历史数据的股票价格预测
这样看起来更有用,对吧?通过观察过去的趋势和数据中的一些模式,我们可以对未来的股票价格进行预测。因此,通过观察过去的趋势,我们可以大致了解股票在接下来几天的走势。如果没有序列,这是无法做到的。再次强调,序列很重要。
在实际应用中,比如机器翻译,你需要考虑数据中的序列。忽略序列的模型只能在某些任务中起到有限作用;你需要一种真正能够充分利用序列中信息的方法。但在讨论这些架构的工作原理之前,我们需要回答一个重要问题:序列到底是什么?
虽然词典中的“序列”定义是相当直白的,但我们需要能够自己识别序列,并决定是否需要考虑序列。为了理解这个概念,让我们回到我们看到的第一个例子:“words? while sequence be this solved of can the ignoring” 和 “can this be solved while ignoring the sequence of words?”
当你将有意义的句子中的词语顺序打乱时,它就变得没有意义,丧失了所有或大部分信息。这可以是测试序列的一种简单有效的方法:如果你打乱了元素,它还合理吗?如果答案是否定的,那你手中就有一个序列。虽然序列无处不在,这里有一些序列数据的例子:语言、音乐、电影剧本、音乐视频、时间序列数据(股票价格、商品价格等),以及患者的生存概率。
时间序列数据——股票价格预测
我们将开始构建自己的股票价格预测模型。股票价格预测任务的目标是构建一个基于历史价格能够预测第二天股票价格的模型。正如我们在前一节中看到的,这个任务要求我们考虑数据中的顺序。我们将预测苹果公司(Apple Inc.)的股票价格。
注意
我们将使用从纳斯达克网站获取的经过清理的苹果历史股票数据:www.nasdaq.com/market-activity/stocks/aapl/historical。数据集可以从以下链接下载:packt.live/325WSKR。
确保将文件(AAPL.csv)放在你的工作目录中,并开始一个新的 Jupyter Notebook 来编写代码。你必须在同一个 Jupyter Notebook 中运行所有练习和主题部分的代码。
让我们从理解数据开始。我们将加载所需的库,然后加载并绘制数据。你可以使用以下命令加载必要的库,并使用单元格魔法命令(%matplotlib inline)将图像内联显示:
import pandas as pd, numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
接下来,我们将使用 Pandas 的 read_csv() 方法加载 .csv 文件到一个 DataFrame(inp0),并通过 pandas DataFrame 的 head 方法查看一些记录:
inp0 = pd.read_csv('AAPL.csv')
inp0.head()
你应该得到以下输出:

图 5.3:AAPL 数据集的前五条记录
我们可以看到,第一条记录是 2020 年 1 月 17 日,是数据中最接近的日期(截至本书写作时的最新数据)。按照 pandas DataFrame 的惯例,第一条记录的索引为 0(索引是行的标识符,每一行都有一个索引值)。Open 表示股票开盘时的价格,High 表示当天股票的最高价格,而 Low 和 Close 分别代表最低价格和收盘价。我们还记录了当天的交易量。
让我们还通过以下命令查看数据集的最后几条记录:
inp0.tail()
记录如下所示:

图 5.4:AAPL 数据集的底部五条记录
从前面的表格中我们可以看到,我们有从 2010 年 1 月 25 日到 2020 年 1 月 17 日的每日开盘价、最高价、最低价和收盘价,以及成交量。对于我们的目的,我们关心的是收盘价。
练习 5.01:可视化我们的时间序列数据
在这个练习中,我们将从数据中提取收盘价,进行必要的格式化,并绘制时间序列图,以更好地理解数据。确保你已经阅读了前面的章节并加载了数据,同时导入了相关库。按照以下步骤完成本练习:
-
如果你还没有导入所需的库,可以使用以下命令:
import pandas as pd, numpy as np import matplotlib.pyplot as plt %matplotlib inline -
从 GitHub 下载名为
AAPL.csv的文件 (packt.live/325WSKR) 并将其加载到一个 DataFrame 中:inp0 = pd.read_csv('AAPL.csv') -
使用
plot方法将Close列绘制成折线图,观察其模式,并指定Date列为 X 轴:inp0.plot("Date", "Close") plt.show()这个图的绘制如下,X 轴显示的是收盘价,Y 轴表示日期:
![图 5.5:收盘价图]()
图 5.5:收盘价图
从图中可以看到,最新的值首先被绘制(在左侧)。为了便于绘制和处理,我们将反转数据。我们将通过按索引(记住索引为最新记录的 0)降序排列 DataFrame 来实现这一点。
-
通过按索引排序 DataFrame 反转数据。重新绘制收盘价,并将
Date作为X 轴:inp0 = inp0.sort_index(ascending=False) inp0.plot("Date", "Close") plt.show()收盘价将按以下方式绘制:
![图 5.6:反转数据后的趋势]()
图 5.6:反转数据后的趋势
这按预期工作。我们可以看到最新的值已被绘制到右侧。
-
从 DataFrame 中提取
Close列的值,作为numpy数组,并通过array.reshape(-1,1)调整为一列:ts_data = inp0.Close.values.reshape(-1,1) -
使用 matplotlib 绘制值的线形图。无需担心标记日期;数据的顺序是清楚的(matplotlib 会使用索引,起始点为 0):
plt.figure(figsize=[14,5]) plt.plot(ts_data) plt.show()得到的趋势如下,X 轴表示索引,Y 轴显示收盘价:
![图 5.7:每日股价趋势]()
图 5.7:每日股价趋势
这就是我们的序列数据的样子。数据没有明显的连续趋势;价格曾在一段时间内上涨,之后股价波动不定。模式不简单。我们可以看到在短期内(可能是按月)有一定的季节性变化。总体来说,模式相当复杂,数据中没有明显且易于识别的周期性变化可以利用。这个复杂的序列就是我们要处理的——使用历史数据预测某一天的股价。
注意
要访问此特定部分的源代码,请参考packt.live/2ZctArW。
您也可以在packt.live/38EDOEA上在线运行此示例。必须执行整个 Notebook 才能获得期望的结果。
在这个练习中,我们加载了股价数据。为了便于处理,我们反转了数据,并提取了收盘价(Close列)。我们绘制了数据图,以直观地检查数据中的趋势和模式,意识到数据中并没有明显的模式可以利用。
注意
是否将数据视为序列也取决于当前的任务。如果任务不需要序列中的信息,那么可能就不需要将其视为序列。
本章将专注于那些需要或能从数据序列中获益的任务。这是如何实现的?我们将在接下来的章节中找出答案,讨论 RNN 背后的直觉和方法。
循环神经网络
我们的大脑如何处理一个句子?让我们试着理解一下我们的大脑在阅读句子时如何处理它。你看到句子中的一些词汇,然后需要识别句子中包含的情感(正面、负面、中性)。让我们来看第一个词汇—— "I":

图 5.8:第一个词汇的情感分析
"I" 是中性的,因此我们的分类(中性)是合适的。让我们看一下下一个词汇:

图 5.9:包含两个词汇的情感分析
加入了 "can't" 这个词后,我们需要更新对情感的评估。"I" 和 "can't" 一起通常具有负面含义,因此我们当前的评估被更新为“负面”,并标记为一个叉号。让我们看一下接下来的几个词汇:

图 5.10:包含四个词汇的情感分析
在增加两个词汇之后,我们仍然预测句子具有负面情感。根据目前的信息,"I can't find any" 是一个合理的判断。让我们看一下最后一个词汇:

图 5.11:添加最终词汇后的情感分析
随着最后一个词汇的加入,我们的预测完全被推翻了。突然间,我们现在认为这是一个积极的表达。每加入一个新词汇,你的评估都会更新,是不是?你的大脑收集所有现有的信息,并做出评估。当新词汇到达时,当前的评估会被更新。这个过程正是 RNN 模拟的过程。
那么,是什么使网络“具有递归性”?关键思想是不仅处理新信息,还保留迄今为止接收到的信息。在 RNN 中,通过使输出不仅依赖于新的输入值,还依赖于当前的“状态”(即迄今为止捕获的信息),从而实现这一点。为了更好地理解这一点,让我们看看标准的前馈神经网络如何处理一个简单的句子,并与 RNN 的处理方式进行比较。
考虑情感分类任务(正面或负面),例如输入句子“life is good”。在标准前馈网络中,句子中所有词汇对应的输入会一起传递给网络。如下面的图示所示,输入数据是句子中所有词汇的组合表示,这些词汇已经传递到网络的隐藏层中。所有的词汇一起被考虑,用于将句子的情感分类为正面:

图 5.12:用于情感分类的标准前馈网络
相比之下,RNN 会逐字处理句子。如以下图所示,词项 "life" 的第一个输入在 t=0 时传递给隐藏层。隐藏层提供了一些输出值,但这还不是句子的最终分类,而只是隐藏层的中间值。此时尚未进行分类:

图 5.13: RNN 在 t=0 时处理第一个词项
下一项 "is" 及其对应的输入在 t=1 时被处理,并传递给隐藏层。如以下图所示,此时,隐藏层还会考虑 t=0 时隐藏层的中间输出,这实际上是对应于 "life" 这一项的输出。此时,隐藏层的输出将有效地考虑新的输入("is")和前一时间步的输入("life"):

图 5.14: t=1 时的网络
在 t=1 时步之后,隐藏层的输出有效地包含了来自 "life" 和 "is" 的信息,实际上保持了迄今为止的输入信息。在 t=2 时,下一项数据,即 "good",被传递到隐藏层。以下图所示,隐藏层将使用新的输入数据,以及来自 t=1 时隐藏层的输出,生成一个输出。这个输出有效地考虑了到目前为止的所有输入,并按照它们在输入文本中的出现顺序进行处理。直到整个句子被处理完毕,才会做出最终的分类(在此为“积极”):

图 5.15: 在 t=2 时处理整个句子的输出
循环 – RNN 的核心部分
RNN 的一个常见部分是使用“循环”,如以下图所示。所谓循环,是指一种保留“状态”值的机制,包含迄今为止的信息,并将其与新的输入一起使用:

图 5.16: 带循环的 RNN
如以下图所示,这是通过简单地虚拟复制隐藏层并在下一个时间步使用它来完成的,即在处理下一个输入时。如果逐词处理句子,这意味着对于每个词项,保存隐藏层的输出(时间 t-1),当新词项在时间 t 到来时,将处理隐藏层的输出(时间 t)及其前一状态(时间 t-1)。实际上,过程就是如此简单:

图 5.17: 复制隐藏层状态
为了更清楚地了解 RNN 的工作原理,让我们扩展视图,从图 5.15开始,我们可以看到输入句子是如何按词处理的。我们将理解 RNN 与标准前馈网络有何不同。
被虚线框突出显示的部分应该对你很熟悉——它代表了带有隐藏层(虚线矩形)的标准前馈网络。输入数据从左到右流经网络的深度,使用前馈权重 WF 来提供输出——这与标准的前馈网络完全相同。递归部分是数据从底部到顶部的流动,跨越时间步:

图 5.18:RNN 架构
对于所有隐藏层,输出也沿着时间维度传播到下一个时间步。或者,对于时间步 t 和深度 l 的隐藏层,输入如下:
-
来自同一时间步的前一个隐藏层的数据
-
来自前一个时间步的相同隐藏层的数据
仔细查看前面的图示,以理解 RNN 的工作原理。隐藏层的输出可以通过以下方式推导出来:

图 5.19:在 RNN 中计算激活值
公式的第一部分,WF(l)at(l-1),对应于前馈计算的结果,也就是将前馈权重 (WF) 应用于来自前一层的输出 (at(l-1))。第二部分对应于递归计算,即将递归权重 (WR(l)) 应用于来自前一个时间步的同一层的输出 (at-1(l))。此外,与所有神经网络层一样,还有一个偏置项。应用激活函数后,该结果成为时间 t 和深度 l 层的输出 (at(l))。
为了使这个概念更加具体,我们将使用 TensorFlow 实现一个简单 RNN 的前馈步骤。
练习 5.02:使用 TensorFlow 实现简单 RNN 的前馈传递
在这个练习中,我们将使用 TensorFlow 执行一个简单 RNN 的操作,其中有一个隐藏层和两个时间步。通过执行一次传递,我们的意思是计算时间步 t=0 时隐藏层的激活值,然后使用该输出以及 t=1 时的新输入(应用适当的递归和前馈权重)来获得 t=1 时的输出。启动一个新的 Jupyter Notebook 进行此练习并执行以下步骤:
-
导入 TensorFlow 和 NumPy。使用
numpy设置随机种子为0以使结果具有可复现性:import numpy as np import tensorflow as tf np.random.seed(0) tf.random.set_seed(0) -
定义
num_inputs和num_neurons常量,分别表示输入的数量(2)和隐藏层中神经元的数量(3):num_inputs = 2 num_neurons = 3在每个时间步,我们将有两个输入。我们将它们称为
xt0和xt1。 -
定义权重矩阵的变量。我们需要两个——一个用于前馈权重,另一个用于递归权重。随机初始化它们:
Wf = tf.Variable(tf.random.normal\ (shape=[num_inputs, num_neurons])) Wr = tf.Variable(tf.random.normal\ (shape=[num_neurons, num_neurons]))注意递归权重的维度——它是一个方阵,行/列的数量等于隐藏层神经元的数量。
-
添加偏置变量(以使激活函数更好地拟合数据),其值为隐藏层神经元数量的零:
b = tf.Variable(tf.zeros([1,num_neurons])) -
创建数据——
xt0的三个示例(两个输入,三个示例),为[[0,1], [2,3], [4,5]],xt1为[[100,101], [102,103], [104,105]]——作为numpy数组,类型为float32(与 TensorFlow 默认浮动表示的dtype一致):xt0_batch = np.array([[0,1],[2,3],[4,5]]).astype(np.float32) xt1_batch = np.array([[100, 101],[102, 103],\ [104,105]]).astype(np.float32) -
定义一个名为
forward_pass的函数,用于对给定数据(即xt0,xt1)应用前向传播。使用tanh作为激活函数。时间 t=0 时的输出应该仅由Wf和xt0得到。时间 t=1 时的输出必须使用yt0和递归权重Wf,并且使用新的输入xt1。该函数应返回两个时间步的输出:def forward_pass(xt0, xt1): yt0 = tf.tanh(tf.matmul(xt0, Wf) + b) yt1 = tf.tanh(tf.matmul(yt0, Wr) + tf.matmul(xt1, Wf) + b) return yt0, yt1注意,在时间步 0 时这里没有递归权重;它只会在第一个时间步之后才会起作用。
-
通过调用
forward_pass函数并传入创建的数据(xt0_batch,xt1_batch),执行前向传播,并将输出存储在变量yt0_output和yt1_output中:yt0_output, yt1_output = forward_pass(xt0_batch, xt1_batch) -
使用 TensorFlow 的
print函数打印输出值yt0_output和yt1_output:tf.print(yt0_output)时间 t=0 时的输出如下所示。注意,由于 TensorFlow 执行随机初始化,您看到的结果可能会略有不同:
[[-0.776318431 -0.844548464 0.438419849] [-0.0857750699 -0.993522227 0.516408086] [0.698345721 -0.999749422 0.586677969]] -
现在,打印
yt1_output的值:tf.print(yt1_output)时间 t=1 时的输出如下所示。再次说明,由于随机初始化值的不同,您看到的结果可能会略有不同,但所有值应接近 1 或 -1:
[[1 -1 0.999998629] [1 -1 0.999998331] [1 -1 0.999997377]]我们可以看到,时间 t=1 时的最终输出是一个 3x3 的矩阵——表示三种数据实例的隐藏层三个神经元的输出。
注意
要访问该特定部分的源代码,请参阅
packt.live/2ZctArW。您也可以在
packt.live/38EDOEA在线运行此示例。您必须执行整个 Notebook 才能获得期望的结果。注意
尽管我们已为
numpy和tensorflow设置了种子以实现可复现的结果,但仍然有许多因素会导致结果的变化。尽管您看到的值可能不同,但您看到的输出应该与我们的结果大致一致。
在这个练习中,我们手动执行了一个简单 RNN 的两个时间步的前向传播。我们看到,它仅仅是将前一个时间步的隐藏层输出作为输入传递给下一个时间步。现在,您实际上不需要手动执行这些步骤——Keras 使得构建 RNN 非常简单。我们将使用 Keras 来进行股票价格预测模型。
RNN 的灵活性和多样性
在练习 5.2,使用 TensorFlow 实现简单 RNN 的前向传递中,我们在每个时间步使用了两个输入,并且每个时间步都有一个输出。但这并不总是必须的。RNN 提供了很多灵活性。首先,你可以有单个/多个输入以及输出。此外,你不必在每个时间步都有输入和输出。
你可以有以下情况:
-
在不同时间步的输入,输出仅在最后一步得到
-
单个输入,多个时间步的输出
-
在多个时间步,输入和输出(长度相等或不等)
RNN 架构具有巨大的灵活性,这种灵活性使其非常多才多艺。让我们看看一些可能的架构以及它们的一些潜在应用:

图 5.20:多个时间步的输入,输出仅在最后一步得到
你可以在多个时间步输入数据,例如在一个序列中(或一个或多个输入),并且输出仅在最后一个时间步生成预测,如前面的图所示。在每个时间步,隐藏层基于上一层的前馈输出和上一时间步副本的循环输出进行操作。但是在中间时间步并不会进行预测。预测只会在处理完整个输入序列后进行——这与我们在图 5.15("生活是美好"的例子)中看到的过程相同。文本分类应用广泛使用这种架构——情感分类(正面/负面)、将邮件分类为垃圾邮件/正常邮件、识别评论中的仇恨言论、自动审核购物平台上的产品评论等。
时间序列预测(例如,股票价格)也利用这种架构,其中处理过去的几个值来预测一个未来的值:

图 5.21:单步输入,多步输出
前面的图示例了另一种架构,其中输入在一个时间步接收,但输出是在多个时间步得到的。围绕生成的应用——根据给定关键词生成图像、根据给定关键词生成音乐(作曲家)或根据给定关键词生成一段文本——都基于这种架构。
你也可以在每个时间步产生与输入相对应的输出,如下图所示。从本质上讲,这种模型将帮助你对序列中的每个输入元素进行预测。这样任务的一个例子是词性标注——对于句子中的每个词,我们识别该词是名词、动词、形容词还是其他词性。
来自自然语言处理的另一个例子是命名实体识别(NER),在这里,对于文本中的每个词语,目标是检测它是否代表一个命名实体,然后将其分类为组织、人物、地点或其他类别(如果是的话):

图 5.22:每个时间步的多个输出
在前面的架构中,我们为每个输入元素都提供了一个输出。在许多情况下,这种方法并不适用,我们需要一种架构,能够处理输入和输出长度不同的情况,如下图所示。想想语言之间的翻译。一句英语在德语中是否一定有相同数量的词汇?通常答案是否定的。对于这种情况,下面的架构提供了“编码器”和“解码器”的概念。输入序列对应的信息存储在编码器网络的最终隐藏层中,该层本身包含循环层。
这个表示/信息由解码器网络处理(同样,这也是递归的),它输出翻译后的序列:

图 5.23:输入和输出长度不同的架构
对于所有这些架构,你还可以有多个输入,使得 RNN 模型更加多功能。例如,在进行股票价格预测时,你可以在多个时间步长内提供多个输入(公司过去的股价、股市指数、原油价格以及你认为相关的任何其他信息),RNN 将能够处理并利用所有这些输入。这也是 RNN 广受欢迎的原因之一,它们改变了我们今天处理序列数据的方式。当然,你还可以利用深度学习的所有预测能力。
股票价格预测数据的准备
对于我们的股票价格预测任务,我们将使用过去几天的数据,通过输入到 RNN 中来预测某一天给定股票的价值。在这里,我们有一个单一的输入(单一特征),跨越多个时间步,并且只有一个输出。我们将采用图 5.20中的 RNN 架构。
注
在本章中,我们将继续使用同一个 Jupyter Notebook 来绘制我们的时间序列数据(除非另有说明)。
到目前为止,我们已经查看了数据并理解了我们处理的内容。接下来,我们需要为模型准备数据。第一步是对数据进行训练集和测试集的划分。由于这是时间序列数据,我们不能随意选择点来分配训练集和测试集。我们需要保持顺序。对于时间序列数据,我们通常保留数据的前一部分用于训练,最后一部分用于测试。在我们的案例中,我们将前 75%的记录作为训练数据,最后 25%的记录作为测试数据。以下命令将帮助我们获得所需的训练集大小:
train_recs = int(len(ts_data) * 0.75)
这是我们在训练集中的记录数量。我们可以按以下方式分割数据集:
train_data = ts_data[:train_recs]
test_data = ts_data[train_recs:]
len(train_data), len(test_data)
训练集和测试集的长度如下所示:
(1885, 629)
接下来,我们需要对股票数据进行缩放。为此,我们可以使用sklearn中的 min-max 缩放器。MinMaxScaler将数据缩放到 0 到 1(包括 0 和 1)之间——数据中的最高值映射为 1。我们将在训练数据上拟合并转换缩放器,然后只对测试数据进行转换:
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
train_scaled = scaler.fit_transform(train_data)
test_scaled = scaler.transform(test_data)
下一个重要步骤是格式化数据,以获取每个实例的“特征”。我们需要定义一个“回溯期”——即从历史数据中用来预测下一个值的天数。以下代码将帮助我们定义一个返回目标值y(某一天的股票价格)和X(回溯期内每天的值)的函数:
def get_lookback(inp, look_back):
y = pd.DataFrame(inp)
dataX = [y.shift(i) for i in range(1, look_back+1)]
dataX = pd.concat(dataX, axis=1)
dataX.fillna(0, inplace = True)
return dataX.values, y.values
这个函数接收一个数据集(实际上是一个数字序列),并根据提供的回溯期,添加历史中的相应值。它通过每次移动序列并将其与结果连接来完成此操作。该函数返回当天的股票价格作为y,以及回溯期内的值(已偏移的值)作为我们的特征。现在,我们可以定义一个回溯期,并查看将该函数应用于我们的数据后的结果:
look_back = 10
trainX, trainY = get_lookback(train_scaled, look_back=look_back)
testX, testY = get_lookback(test_scaled, look_back= look_back)
尝试以下命令来检查结果数据集的形状:
trainX.shape, testX.shape
输出结果如下:
((1885, 10), (629, 10))
如预期,每个样本有 10 个特征,分别对应过去 10 天的数据。我们拥有训练数据和测试数据的历史数据。有了这些,数据准备工作就完成了。在开始构建第一个 RNN 模型之前,让我们进一步了解一下 RNN。
注意
我们在这里创建的trainX和trainY变量将在接下来的练习中使用。所以,请确保你在同一个 Jupyter Notebook 中运行本章节的代码。
RNN 中的参数
要计算 RNN 层中的参数数量,我们来看看一个通用的隐藏层:

图 5.24:递归层的参数
隐藏层从前一个隐藏层的同一时间步获取输入,同时也从自身的先前时间步获取输入。如果输入层(前一个隐藏层)是 m 维的,那么我们需要n×m个权重/参数,其中n是 RNN 层中神经元的数量。对于输出层,如果输出的维度是k,则权重的维度为n×k。递归权重始终是一个维度为n×n的方阵——因为输入的维度与该层本身相同。
因此,任何 RNN 层的参数数量为n² + nk + nm,其中我们有以下内容:
-
n:隐藏(当前)层的维度 -
m:输入层的维度 -
k:输出层的维度
训练 RNN
如何在 RNN 中前向传播信息现在应该已经很清楚。如果不清楚,请参考图 5.19中的方程式。新的信息沿着网络的深度以及时间步传播,使用每个步骤的前一个隐藏状态。训练 RNN 的另外两个关键方面如下:
-
定义损失:我们知道如何为标准神经网络定义损失;也就是说,它只有一个输出。对于 RNN,如果输出是单一时间步(例如,文本分类),则损失的计算方式与标准神经网络相同。但我们知道,RNN 可以在多个时间步上有输出(例如,在词性标注或机器翻译中)。那么,如何在多个时间步上定义损失呢?一种非常简单且流行的方法是将所有步骤的损失加总。整个序列的损失计算为所有时间步损失的总和。
-
反向传播:反向传播误差现在需要我们跨越时间步进行,因为还有时间维度。我们已经看到,损失被定义为每个时间步的损失之和。通常的链式法则应用有助于我们解决这个问题;我们还需要将每个时间步的梯度在时间上进行求和。这个过程有一个非常吸引人的名字:通过时间的反向传播(BPTT)。
注意
训练过程的详细处理和涉及的数学超出了本书的范围。我们需要理解的基本概念就足够了。
现在,让我们继续使用 Keras 构建我们的第一个 RNN 模型。在本章中,我们将介绍 Keras 中可用的两个新层,并理解它们的功能和用途。我们需要的第一个层是SimpleRNN层。
要从 Keras 导入所有必要的工具,您可以使用以下代码:
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers \
import SimpleRNN, Activation, Dropout, Dense, Reshape
SimpleRNN 层是最简单的基本 RNN 层。它接受一个序列,并将神经元的输出反馈作为输入。此外,如果我们希望在这个 RNN 层后跟随另一个 RNN 层,我们可以选择将序列作为输出。让我们来看一下其中的一些选项。
?SimpleRNN:SimpleRNN 层的签名如下:

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_05_25.jpg)
图 5.25:SimpleRNN 层的签名
我们可以看到,Keras 的层也具有所有常规/标准层的选项,可以让你指定激活函数、初始化、丢弃等。
RNN 层期望输入数据具有特定格式。由于我们可能有多个特征的多时间步输入数据,输入格式应该能够明确表达这一要求。期望的输入形状是 (look_back, 特征数量)。它期望每个特征都有相同的回溯历史。
在我们的案例中,我们只有一个特征,回溯期为 10。因此,预期的输入形状是 (10, 1)。请注意,我们当前每个输入都是一个包含 10 个值的列表,所以我们需要确保它被理解为 (10, 1)。我们将使用重塑层来完成此操作。重塑层需要输入形状和目标形状。让我们通过实例化并添加重塑层来开始构建模型。
注意
即使我们已经设置了 numpy 和 tensorflow 的种子以实现可重复的结果,但仍然有许多变化的因素,因此你可能会得到与我们不同的结果。这适用于我们将在此处使用的所有模型。虽然你看到的值可能不同,但你看到的输出应该与我们大致相符。如果模型表现差异很大,你可能需要调整训练轮数 —— 原因在于神经网络中的权重是随机初始化的,因此你和我们可能从不同的起点开始,经过不同的轮次训练后可能会达到相似的状态。
练习 5.03:构建我们的第一个普通 RNN 模型
在这个练习中,我们将构建我们的第一个普通 RNN 模型。我们将有一个重塑层,接着是一个 SimpleRNN 层,最后是一个用于预测的全连接层。我们将使用之前创建的格式化数据 trainX 和 trainY,以及从 Keras 初始化的层。请执行以下步骤来完成此练习:
-
从 Keras 中收集必要的工具。使用以下代码进行操作:
from tensorflow.keras.models import Sequential from tensorflow.keras.layers \ import SimpleRNN, Activation, Dropout, Dense, Reshape -
实例化
Sequential模型:model = Sequential() -
添加一个
Reshape层,以使数据符合 (look_back,1) 的格式:model.add(Reshape((look_back,1), input_shape = (look_back,)))注意
Reshape层的参数。目标形状是(lookback, 1),正如我们所讨论的。 -
添加一个具有 32 个神经元的
SimpleRNN层,并指定输入形状。请注意,我们选择了一个任意数量的神经元,因此你可以尝试不同的数量:model.add(SimpleRNN(32, input_shape=(look_back, 1))) -
添加一个大小为 1 的
Dense层:model.add(Dense(1)) -
添加一个
Activation层,并使用线性激活函数:model.add(Activation('linear')) -
使用
adam优化器和mean_squared_error(因为我们预测的是一个实数值量)来编译模型:model.compile(loss='mean_squared_error', optimizer='adam') -
打印模型的摘要:
model.summary()摘要将如下所示:

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_05_26.jpg)
图 5.26:SimpleRNN 模型总结
请注意SimpleRNN层中的参数数量。结果正如我们预期的那样。
注意
要访问此特定部分的源代码,请参考packt.live/2ZctArW。
您还可以在线运行这个示例,网址为packt.live/38EDOEA。您必须执行整个 Notebook 才能获得预期结果。
在本次练习中,我们使用单层普通 RNN 架构定义了我们的模型结构。与我们之前为图像数据构建的模型相比,这确实是一个非常简单的模型。接下来,让我们看看这个模型在当前任务中的表现如何。
模型训练和性能评估
我们已经定义并编译了模型。下一步是通过在训练数据上拟合模型来学习模型参数。我们可以使用批量大小为 1,验证集比例为 10%,并且只训练三轮(epoch)。我们尝试了不同的轮次值,发现三轮时模型效果最佳。以下代码将帮助我们使用fit()方法训练模型:
model.fit(trainX, trainY, epochs=3, batch_size=1, \
verbose=2, validation_split=0.1)
输出如下:

图 5.27:训练输出
我们可以看到,损失值已经相当低了。我们在这里训练了模型,并没有做任何仔细的超参数调优。可以看到,对于这个数据集,三轮就足够了,而且我们这里尽量保持简单。模型训练完成后,我们现在需要评估在训练集和测试集上的表现。
为了使我们的代码更模块化,我们将定义两个函数——一个用于打印训练集和测试集上的 RMS 误差,另一个函数用于绘制测试数据的预测值以及数据中的原始值。我们首先定义第一个函数,使用math中的sqrt函数来获取模型evaluate方法提供的mean_squared_error的平方根。函数定义如下:
import math
def get_model_perf(model_obj):
score_train = model_obj.evaluate(trainX, trainY, verbose=0)
print('Train RMSE: %.2f RMSE' % (math.sqrt(score_train)))
score_test = model_obj.evaluate(testX, testY, verbose=0)
print('Test RMSE: %.2f RMSE' % (math.sqrt(score_test)))
要查看我们的模型表现如何,我们需要将model对象提供给此方法。可以按如下方式进行:
get_model_perf(model)
输出如下:
Train RMSE: 0.02 RMSE
Test RMSE: 0.03 RMSE
这些值看起来相当低(坦率地说,我们这里没有真正的基准,但考虑到我们的输出值范围从 0 到 1,这些值似乎是不错的)。但这是一个汇总统计值,我们已经知道数据中的值变化相当大。更好的做法是通过视觉评估模型的表现,将实际值与测试期的预测值进行比较。以下代码将帮助我们定义一个函数,用来绘制给定模型对象的预测值:
def plot_pred(model_obj):
testPredict = \
scaler.inverse_transform(model_obj.predict(testX))
pred_test_plot = ts_data.copy()
pred_test_plot[:train_recs+look_back,:] = np.nan
pred_test_plot[train_recs+look_back:,:] = \
testPredict[look_back:]
plt.plot(ts_data)
plt.plot(pred_test_plot, "--")
首先,函数对测试数据进行预测。由于这些数据已经进行了缩放,因此在绘制图表之前,我们需要应用逆变换将数据恢复到原始尺度。该函数以实线绘制实际值,虚线绘制预测值。让我们使用这个函数来直观评估模型的表现。我们只需将模型对象传递给plot_pred函数,如以下代码所示:
%matplotlib inline
plt.figure(figsize=[10,5])
plot_pred(model)
显示的图表如下所示:

图 5.28:预测值与实际值的对比
上图展示了模型的预测值(虚线)与实际值(实线)的对比。看起来不错,对吧?在这个尺度下,预测值和实际值几乎完全重叠——预测曲线几乎完美地拟合了实际值。乍一看,似乎模型表现得非常好。
但在我们自我庆祝之前,让我们回顾一下我们工作的粒度——我们正在使用 10 个数据点来预测第二天的股价。当然,在这个尺度下,即使我们仅仅取简单的平均值,图表看起来也会很惊人。我们需要大大放大,以便更好地理解。让我们放大一下,使得各个数据点能够清晰可见。我们将使用%matplotlib notebook单元魔法命令来使图表具有互动性,并放大图表中对应于索引2400至2500的数值:
%matplotlib notebook
plot_pred(model)
注意
如果下面呈现的图表因某种原因未正确显示,请多运行几次包含%matplotlib notebook的单元。或者,你也可以使用%matplotlib inline来代替%matplotlib notebook。
输出结果如下,虚线表示预测值,实线表示实际值:

图 5.29:预测的放大视图
即使在放大后,结果依然相当不错。所有的变化都被很好地捕捉到。仅使用一个含 32 个神经元的 RNN 层就能获得这样的结果,非常不错。那些使用传统方法进行时间序列预测的人会对 RNN 在这个任务中的效果感到兴奋(就像我们一样)。
我们已经了解了什么是 RNN,并通过我们的股价预测模型,看到即便是一个非常简单的模型也能在序列预测任务中展现出强大的预测能力。我们之前提到,使用 RNN 是一种处理序列的方式。还有另一种值得注意的处理序列的方法,那就是使用卷积。我们将在下一节中进行探讨。
一维卷积用于序列处理
在前面的章节中,你已经看到深度神经网络是如何通过卷积受益的——你了解了卷积神经网络(ConvNets)以及它们是如何用于图像处理的,并且你看到了它们如何帮助解决以下问题:
-
减少参数的数量
-
学习图像的“局部特征”
有趣的是,这一点可能不太明显,卷积神经网络在序列处理任务中也能发挥很大的作用。与 2D 卷积不同,我们可以对序列数据使用 1D 卷积。1D 卷积是如何工作的呢?让我们来看一下:

图 5.30:使用 1D 卷积生成特征
在第三章,使用卷积神经网络进行图像分类中,我们看到过滤器在图像的情况下如何工作,从输入图像中提取“补丁”,并为我们提供输出“特征”。在 1D 的情况下,过滤器从输入序列中提取子序列,并将它们与权重相乘,以给出输出特征的值。如前图所示,过滤器从序列的开始到结束(从上到下)移动。这样,1D 卷积神经网络就提取了局部补丁。与 2D 情况一样,在这里学到的补丁/特征也可以在序列中的不同位置识别出来。当然,和 2D 卷积一样,你也可以选择 1D 卷积的过滤器大小和步幅。如果使用步幅大于 1,1D 卷积神经网络也可以显著减少特征的数量。
注意
当作为第一层应用于文本数据时,1D 卷积提取的“局部特征”是针对词组的特征。过滤器大小为 2 时,可以提取二元词组(bi-grams),为 3 时提取三元词组(tri-grams),依此类推。较大的过滤器大小会学习更大的词组。
你还可以对 1D 数据应用池化操作——最大池化或平均池化,以进一步对特征进行下采样。这样,你可以显著减少处理序列时的有效长度。长输入序列可以被缩减到更小、更易管理的长度。这无疑有助于提高速度。
我们知道这在速度上带来了好处。但 1D 卷积神经网络(convnets)在处理序列时表现如何呢?1D 卷积神经网络在翻译和文本分类等任务中取得了非常好的结果。它们在音频生成和其他基于序列预测的任务中也表现出色。
1D 卷积神经网络在我们进行股票价格预测的任务中表现如何?想一想——考虑一下我们能获得什么样的特征,以及我们是如何处理序列的。如果你不确定,那也没关系——我们将使用基于 1D 卷积神经网络的模型来完成任务,并在接下来的练习中亲自验证。
练习 5.04:构建基于 1D 卷积的模型
在这个练习中,我们将构建第一个基于 1D 卷积神经网络的模型,并评估其性能。我们将使用一个 Conv1D 层,后跟 MaxPooling1D。我们将继续使用之前的同一数据集和笔记本。请执行以下步骤以完成这个练习:
-
从 Keras 中导入与 1D 卷积相关的层:
from tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten -
初始化一个
Sequential模型,并添加一个Reshape层,将每个实例重塑为一个向量(look_back, 1):model_conv = Sequential() model_conv.add(Reshape((look_back,1), \ input_shape = (look_back,))) -
添加一个包含五个滤波器(大小为 5)并使用
relu作为激活函数的 Conv1D 层:model_conv.add(Conv1D(5, 5, activation='relu'))请注意,我们使用的滤波器比序列长度少。在许多其他应用中,序列可能远长于我们的示例。滤波器的数量通常远低于输入序列的长度。
-
添加一个池化大小为 5 的 Maxpooling1D 层:
model_conv.add(MaxPooling1D(5)) -
使用
Flatten层将输出展平:model_conv.add(Flatten()) -
添加一个包含单个神经元的
Dense层,并添加一个线性激活层:model_conv.add(Dense(1)) model_conv.add(Activation('linear')) -
打印出模型的摘要:
model_conv.summary()模型的摘要如下:
![图 5.31: 模型摘要]()
图 5.31: 模型摘要
注意 Conv1D 层输出的维度——6 x 5。这是预期的——对于大小为 5 的滤波器,我们得到 6 个特征。另外,看看参数的总数。只有 36,确实是一个非常小的数字。
-
将模型编译,损失函数为
mean_squared_error,优化器为adam,然后在训练数据上训练 5 个 epoch:model_conv.compile(loss='mean_squared_error', optimizer='adam') model_conv.fit(trainX, trainY, epochs=5, \ batch_size=1, verbose=2, validation_split=0.1)您应该看到以下输出:
![图 5.32: 训练和验证损失]()
图 5.32: 训练和验证损失
从前面的截图中,我们可以看到 1D 卷积模型的验证损失也相当低。我们需要查看这种性能是否与普通 RNN 的性能相当。让我们评估一下模型的性能,看看是否符合我们的预期。
-
使用
get_model_perf函数获取训练集和测试集的 RMSE:get_model_perf(model_conv)输出如下:
Train RMSE: 0.04 RMSE Test RMSE: 0.05 RMSE这略高于普通 RNN 模型的性能。接下来,让我们可视化一下预测结果。
-
使用
plot_pred函数,绘制预测值和实际值:%matplotlib inline plt.figure(figsize=[10,5]) plot_pred(model_conv)模型的输出如下,虚线表示预测值,实线表示实际值:
![图 5.33: 绘制预测值和实际值]()
图 5.33: 绘制预测值和实际值
这与 RNN 模型的预测图非常相似(图 5.29)。但我们现在认识到,更好的评估需要交互式可视化,并放大到每个点都可见的尺度。让我们使用 matplotlib 的交互式绘图功能,通过 notebook 后端并使用
%matplotlib单元格魔法命令来进行放大。 -
再次绘制,并进行交互式操作,放大最后 100 个数据点:
%matplotlib notebook plot_pred(model_conv)输出如下:
![图 5.34: 放大显示预测值]()
图 5.34: 放大显示预测值
注意
如果前面的图表由于某些原因没有正确显示,请运行包含%matplotlib notebook的单元格几次。或者,您也可以使用%matplotlib inline来代替%matplotlib notebook。
上图显示了预测(虚线)和实际值(实线)的更详细视图。在这个尺度上看,情况并不太好。输出非常平滑,几乎看起来像某种平均化。发生了什么?这符合您的预期吗?能解释一下这个输出吗?
注意
要访问此特定部分的源代码,请参阅packt.live/2ZctArW。
您也可以在packt.live/38EDOEA在线运行此示例。为了获得预期的结果,您必须执行整个笔记本。
在这个练习中,我们为股价预测构建并训练了基于 1D 卷积的模型。我们发现参数数量非常低,训练时间也大大缩短。
1D 卷积网络的性能
要解释上一个练习的结果,我们需要理解在使用 Conv1D 层提取子序列后发生了什么。数据中的序列正在被捕获,也就是说,在各个过滤器中。但是在此之后,序列是否被保留,我们是否真正利用了数据中的序列?不,我们没有。一旦提取了补丁,它们就被独立处理了。因此,性能并不是很好的原因就在于此。
那么,为什么我们之前说 1D 卷积网络在序列任务上表现出色?如何使它们在我们的任务中表现良好?1D 卷积网络在文本等任务中表现非常好,特别是分类任务,其中短而局部的序列非常重要,而在整个序列(比如 200 个项)的顺序中遵循并不会带来巨大的好处。对于时间序列任务,我们需要整个序列的顺序。有方法可以诱导序列考虑,例如对时间序列任务,但效果不是很好。
使用 1D 卷积网络与 RNN
我们看到了 1D 卷积网络的优势 – 速度快、特征减少、参数数量少、学习局部特征等等。我们也看到了 RNN 提供了非常强大和灵活的序列处理架构,但参数很多,训练成本高昂。一种可能的方法是结合两者 – 在初始层中利用 1D 卷积网络的表征和特征减少的优势,在后续层中利用 RNN 的序列处理能力。让我们尝试一下适用于我们的任务。
Exercise 5.05: 构建混合模型(1D 卷积和 RNN)
在这个练习中,我们将构建一个模型,结合使用 1D 卷积和 RNN,并评估性能变化。创建一个混合模型非常简单——我们从卷积层开始,卷积层的输出是一个序列的特征。这个序列可以直接输入到 RNN 层中。因此,结合 1D 卷积和 RNN 就像在 Conv1D 层后面接一个 RNN 层一样简单。我们将在同一个 Jupyter Notebook 中继续这个练习。按照以下步骤完成本次练习:
-
初始化一个顺序模型,添加一个
Reshape层(如前面的练习所示),然后添加一个包含五个过滤器和过滤器大小为 3 的Conv1D层:model_comb = Sequential() model_comb.add(Reshape((look_back,1), \ input_shape = (look_back,))) model_comb.add(Conv1D(5, 3, activation='relu')) -
接下来,添加一个包含 32 个神经元的
SimpleRNN层,后跟一个Dense层和一个Activation层:model_comb.add(SimpleRNN(32)) model_comb.add(Dense(1)) model_comb.add(Activation('linear')) -
打印出模型摘要:
model_comb.summary()输出将如下所示:
![图 5.35:混合(1D 卷积和 RNN)模型的摘要]()
图 5.35:混合(1D 卷积和 RNN)模型的摘要
Conv1D 层的输出是 8 × 5——来自 5 个过滤器的 8 个特征。总体参数数量略高于纯 RNN 模型。这是因为我们处理的序列大小非常小。如果处理更大的序列,我们将看到参数的减少。现在,让我们编译并训练模型。
-
编译并在训练数据上训练模型,训练 3 个周期:
model_comb.compile(loss='mean_squared_error', optimizer='adam') model_comb.fit(trainX, trainY, epochs=3, \ batch_size=1, verbose=2, validation_split=0.1)模型训练输出如下:
![图 5.36:训练和验证损失]()
图 5.36:训练和验证损失
让我们首先通过查看 RMSE 来评估性能。我们不指望这对我们的例子有太大帮助,但作为一种良好的实践,还是将其打印出来。
-
使用
get_model_perf函数打印训练集和测试集的 RMSE:get_model_perf(model_comb)你将得到以下输出:
Train RMSE: 0.02 RMSE Test RMSE: 0.03 RMSE这些值看起来较低,但只有仔细观察才能帮助我们评估模型的性能。
-
在交互模式下绘制预测与实际值的对比,并放大最后 100 个点:
%matplotlib notebook plot_pred(model_comb)前面命令的输出将如下所示:
![图 5.37:混合模型的绘图]()
图 5.37:混合模型的绘图
以下是预测结果的放大视图:

图 5.38:预测结果的放大视图
注意
如果下面展示的图形由于某些原因没有正确显示,请多次运行包含 %matplotlib notebook 的单元。或者,你也可以使用 %matplotlib inline 代替 %matplotlib notebook。
该结果非常好。对于测试数据,预测(虚线)与实际值(实线)非常接近——不仅捕捉了水平变化,还很好地捕捉了细微的变化。当 1D 卷积网络从序列中提取补丁时,也有一些有效的正则化发生。这些特征被按序列输入到 RNN 中,RNN 利用其强大的计算能力提供我们看到的输出。将 1D 卷积网络与 RNN 结合确实是有价值的。
注意
要访问该特定部分的源代码,请参考packt.live/2ZctArW。
你也可以在packt.live/38EDOEA上在线运行这个示例。你必须执行整个 Notebook 才能得到期望的结果。
在本次练习中,我们看到如何将 1D 卷积网络和 RNN 结合形成一个混合模型,该模型能够提供高性能。我们承认,尝试这种组合在序列处理任务中是有价值的。
活动 5.01:使用普通 RNN 模型预测 IBM 股票价格
我们已经看到 RNN 的实际应用,现在可以欣赏它们在序列预测任务中所带来的强大能力。我们还看到,RNN 与 1D 卷积网络结合能够提供出色的结果。接下来,让我们将这些思路应用到另一个股票价格预测任务中,这次预测 IBM 的股票价格。数据集可以从packt.live/3fgmqIL下载。你将可视化数据并理解其中的模式。根据你对数据的理解,选择一个回溯期并构建一个基于 RNN 的预测模型。该模型将包含 1D 卷积网络和普通 RNN 层。你还将使用 dropout 来防止过拟合。
执行以下步骤以完成此练习:
-
加载
.csv文件,反转索引,并绘制时间序列(Close列)以便可视化检查。 -
从 DataFrame 中提取
Close的值作为numpy数组,并使用matplotlib进行绘制。 -
将最后 25%的数据指定为测试数据,前 75%的数据作为训练数据。
-
使用
sklearn中的MinMaxScaler对训练数据和测试数据进行缩放。 -
使用我们在本章定义的
get_lookback函数,通过 15 的回溯期获取训练数据和测试数据的回溯数据。 -
从 Keras 导入所有必要的层来使用普通 RNN(
SimpleRNN、Activation、Dropout、Dense和Reshape)和 1D 卷积(Conv1D)。另外,导入mean_squared_error。 -
构建一个包含 1D 卷积层(5 个大小为 3 的滤波器)和一个具有 32 个神经元的 RNN 层的模型。在 RNN 层之后添加 25%的 dropout。打印模型的摘要。
-
使用
mean_squared_error损失函数和adam优化器编译模型。在训练数据上进行五个 epochs 的拟合,验证集划分比例为 10%,批大小为 1。 -
使用
get_model_perf方法,打印模型的 RMSE。 -
绘制预测结果——展示整体视图以及一个放大的视图以便更精细地评估性能。
预测结果(虚线)与实际结果(实线)的放大视图应如下所示:

图 5.39:预测结果的放大视图
注意
本活动的详细步骤以及解决方案和附加评论,详见第 410 页。
总结
在本章中,我们讨论了处理序列时的注意事项。有几个任务要求我们利用序列中包含的信息,而对于这些任务,序列无关模型的表现会很差。我们看到,使用 RNN 是进行序列建模的一种非常强大的方法——该架构显式地处理序列,并考虑到迄今为止积累的信息和新输入,以生成输出。即使是非常简单的 RNN 架构,在我们的股价预测任务中也表现得非常好。我们得到了使用经典方法需要付出大量努力才能取得的那种结果。
我们还看到,1D 卷积可以应用于序列预测任务。像 2D 卷积用于图像一样,1D 卷积可以学习序列中的局部特征。我们建立了一个 1D 卷积模型,但在我们的任务中表现不太理想。最终我们建立的模型结合了 1D 卷积和 RNN,并在股价预测任务中取得了优异的结果。
在下一章中,我们将讨论一些变种的 RNN 模型,它们更加强大。我们还将讨论提取 RNN 概念潜在能力的架构。我们将把这些“强化版 RNN”应用于自然语言处理中的一个重要任务——情感分类。
第六章:6. LSTM、GRU 和高级 RNN
概述
在本章中,我们将研究并实现一些高级模型和普通循环神经网络(RNN)的变体,这些模型克服了 RNN 的一些实际缺点,并且目前是表现最好的深度学习模型之一。我们将从理解普通 RNN 的缺点开始,看看长短期记忆网络(LSTM)这一新颖的理念是如何克服这些缺点的。接着,我们将实现一个基于门控循环单元(GRU)的模型。我们还将研究双向 RNN 和堆叠 RNN,并探索基于注意力机制的模型。到本章结束时,您将构建并评估这些模型在情感分类任务中的表现,并亲自观察选择不同模型时的权衡。
介绍
假设您正在处理一款手机的产品评论,任务是将评论中的情感分类为正面或负面。您遇到了一条评论说:“这款手机没有很好的相机,也没有令人惊艳的显示效果,没有优秀的电池续航,没有良好的连接性,也没有其他让它成为最棒的特性。”现在,当您读到这条评论时,您可以轻松识别出评论的情感是负面的,尽管其中有许多正面的词句,比如“优秀的电池续航”和“让它成为最棒的”。您理解到,文本开头的“没有”一词否定了之后的所有内容。
到目前为止,我们创建的模型能否识别出这种情况中的情感?可能不能,因为如果您的模型没有意识到句子开头的“没有”一词是很重要的,并且需要与几词之后的输出强相关,那么它们就无法正确识别情感。这不幸的是,普通 RNN 的一个重大缺点。
在上一章中,我们探讨了几种用于处理序列的深度学习方法,即一维卷积和 RNN。我们看到,RNN 是极为强大的模型,能够为我们提供灵活性,处理各种序列任务。我们看到的普通 RNN 已经进行了大量研究。现在,我们将研究一些基于 RNN 的进阶方法,创建出新的、强大的模型,克服 RNN 的缺点。我们将研究 LSTM、GRU、堆叠和双向 LSTM,以及基于注意力机制的模型。我们将把这些模型应用于情感分类任务,结合第四章:文本的深度学习—词嵌入和第五章:序列的深度学习中讨论的概念。
长期依赖/影响
我们在上一节看到的手机评论示例就是一个长期依赖/影响的例子——一个序列中的术语/值对后续许多术语/值的评估产生影响。考虑以下示例,你需要填补一个缺失的国家名称:“在一所顶尖的德国大学为她的牙科硕士课程提供录取后,Hina 非常兴奋,迫不及待地想要开始这一新的人生阶段,并且等不及要在月底前订机票去 ____。”
正确答案当然是德国,要得出这个答案,你需要理解“German”一词的重要性,这个词出现在句子的开头,并影响句子结尾的结果。这是另一个长期依赖的例子。下图展示了答案“Germany”如何受到句子开头出现的“German”一词的长期依赖:

图 6.1:长期依赖
为了获得最佳结果,我们需要能够处理长期依赖问题。在深度学习模型和 RNN 的背景下,这意味着学习(或误差反向传播)需要在多个时间步长上平稳且有效地进行。说起来容易做起来难,主要是因为消失梯度问题。
消失梯度问题
训练标准前馈深度神经网络时面临的最大挑战之一是消失梯度问题(如第二章,神经网络所讨论的)。随着模型层数的增加,将误差从输出层反向传播到最初的层变得越来越困难。接近输出层的层将“学习”/更新得较快,但当误差传播到最初的层时,其值会大大减少,对初始层的参数几乎没有或完全没有影响。
对于 RNN 来说,这个问题更为复杂,因为参数不仅需要沿深度更新,还需要在时间步长上更新。如果输入有一百个时间步长(这在处理文本时并不罕见),网络需要将误差(在第 100 个时间步长计算出的)一直反向传播到第一个时间步长。对于普通的 RNN,这项任务可能有些难以应对。这时,RNN 的变体就显得尤为重要。
注意
训练深度网络的另一个实际问题是梯度爆炸问题,其中梯度值变得非常高——高到系统无法表示的程度。这个问题有一个相当简单的解决方法,叫做“梯度裁剪”,即限制梯度值的范围。
文本分类的序列模型
在第五章,序列的深度学习中,我们了解到 RNN 在序列建模任务中表现非常出色,并且在与文本相关的任务中提供了高性能。在本章中,我们将使用普通的 RNN 和 RNN 的变体来进行情感分类任务:处理输入序列并预测情感是正面还是负面。
我们将使用 IMDb 评论数据集进行此任务。该数据集包含 50,000 条电影评论及其情感——25,000 条极具极性(polar)的电影评论用于训练,25,000 条用于测试。使用该数据集的原因如下:
-
只需一个命令,就可以方便地加载 Keras(分词版本)。
-
该数据集通常用于测试新方法/模型。这应该帮助你轻松地将自己的结果与其他方法进行比较。
-
数据中的较长序列(IMDb 评论可能非常长)帮助我们更好地评估 RNN 变体之间的差异。
让我们开始构建第一个模型,使用普通的 RNN,然后将未来的模型表现与普通 RNN 的表现进行基准比较。我们从数据预处理和模型格式化开始。
加载数据
注意
确保你在本章中的所有练习和示例代码都在同一个 Jupyter Notebook 中进行。请注意,本节的代码将加载数据集。为了确保后续的所有练习和示例代码都能正常工作,请确保不要跳过这一节。你可以通过packt.live/31ZPO2g访问完整的练习代码。
首先,你需要启动一个新的 Jupyter Notebook,并从 Keras 数据集中导入imdb模块。请注意,除非另有说明,本章其余的代码和练习应继续在同一个 Jupyter Notebook 中进行:
from tensorflow.keras.datasets import imdb
导入该模块后,加载数据集(已分词并分为训练集和测试集)就像运行imdb.load_data一样简单。我们需要提供的唯一参数是我们希望使用的词汇表大小。回顾一下,词汇表大小是我们希望在建模过程中考虑的独特词汇数量。当我们指定词汇表大小V时,我们就会使用数据中排名前V的词汇。在这里,我们将为我们的模型指定一个 8,000 的词汇表大小(一个任意选择;你可以根据需要修改)并使用load_data方法加载数据,如下所示:
vocab_size = 8000
(X_train, y_train), (X_test, y_test) = imdb.load_data\
(num_words=vocab_size)
让我们检查一下X_train变量,看看我们正在处理什么。让我们打印出它的类型和构成元素的类型,并查看其中一个元素:
print(type(X_train))
print(type(X_train[5]))
print(X_train[5])
我们将看到以下输出:
<class 'numpy.ndarray'>
<class 'list'>
[1, 778, 128, 74, 12, 630, 163, 15, 4, 1766, 7982, 1051,
2, 32, 85, 156, 45, 40,
148, 139, 121, 664, 665, 10, 10, 1361, 173, 4, 749, 2, 16,
3804, 8, 4, 226, 65,
12, 43, 127, 24, 2, 10, 10]
X_train变量是一个numpy数组——数组的每个元素都是表示单个评论文本的列表。文本中的词汇以数字令牌的形式出现,而不是原始令牌。这是一种非常方便的格式。
下一步是定义序列的最大长度,并将所有序列限制为这个最大长度。我们将使用200——在这种情况下是一个任意选择——来快速开始模型构建过程,以避免网络变得过于庞大,而且200个时间步长足以展示不同的 RNN 方法。让我们定义maxlen变量:
maxlen = 200
下一步是使用 Keras 的pad_sequences工具将所有序列调整为相同的长度。
注意
*****理想情况下,我们会分析序列的长度并找出一个覆盖大多数评论的长度。在本章结束时的活动中,我们将执行这些步骤,结合当前章节的内容,还将结合第四章,文本的深度学习——嵌入和第五章,序列的深度学习的概念,最终将这些内容整合到一个活动中。
数据的分阶段处理与预处理
Keras 中sequences模块的pad_sequences工具帮助我们将所有序列调整到指定长度。如果输入序列短于指定长度,该工具会用保留的标记(表示空白/缺失)对序列进行填充。如果输入序列长于指定长度,工具会截断序列以限制其长度。在以下示例中,我们将对测试集和训练集应用pad_sequences工具:
from tensorflow.keras import preprocessing
X_train = preprocessing.sequence.pad_sequences\
(X_train, maxlen=maxlen)
X_test = preprocessing.sequence.pad_sequences\
(X_test, maxlen=maxlen)
为了理解这些步骤的结果,我们来看一下训练数据中特定实例的输出:
print(X_train[5])
处理后的实例如下:

图 6.2:pad_sequences 的结果
我们可以看到结果的开头有很多0。正如你可能已经推测的,这些是pad_sequences工具进行的填充,因为输入序列短于200。默认情况下,填充会加在序列的开头。对于小于指定限制的序列,默认情况下会从左侧进行截断——也就是说,最后的200个术语将被保留。输出中的所有实例现在都有200个术语。数据集现在已经准备好进行建模。
注意
该工具的默认行为是将填充添加到序列的开头,并从左侧截断。这些可能是重要的超参数。如果你认为序列的前几个术语对预测最为重要,可以通过将"truncating"参数设置为"post"来截断后面的术语。同样,若希望在序列的末尾进行填充,可以将"padding"设置为"post"。
嵌入层
在第四章,深度学习文本——嵌入中,我们讨论过不能将文本直接输入神经网络,因此需要良好的表示。我们讨论过嵌入(低维度、密集的向量)是表示文本的好方法。为了将嵌入传递到神经网络的层中,我们需要使用嵌入层。
嵌入层的功能是双重的:
-
对于任何输入词项,进行查找并返回其词嵌入/向量
-
在训练过程中,学习这些词嵌入
查找部分很简单——词嵌入以V × D的矩阵形式存储,其中V是词汇表的大小(考虑的唯一词项的数量),D是每个向量的长度/维度。下图展示了嵌入层。输入词项“life”被传递到嵌入层,嵌入层进行查找并返回相应长度为D的向量。这个表示life的向量被传递到隐藏层。
那么,训练预测模型时,学习这些嵌入是什么意思呢?难道词嵌入不是通过使用像word2vec这样的算法学习的吗?它尝试基于上下文词汇预测中心词(记得我们在第四章,深度学习文本——嵌入中讨论的 CBOW 架构)?嗯,既是又不是:

图 6.3:嵌入层
word2vec方法的目标是学习一个能够捕捉词义的表示。因此,基于上下文预测目标词是一个完美的目标设定。在我们的例子中,目标不同——我们希望学习有助于我们最好地预测文本情感的表示。因此,学习一个明确朝着我们目标工作的表示是有意义的。
嵌入层始终是模型中的第一层。你可以根据需要选择任意架构(在我们的例子中是 RNN)。我们会随机初始化嵌入层中的向量,实际上就是嵌入层中的权重。在训练过程中,权重会根据预测结果进行更新,以便更好地预测结果。学习到的权重,因此也学习到的词向量,随后会根据任务进行调整。这是一个非常有用的步骤——为什么要使用通用表示,而不是将其调整到你的任务上呢?
Keras 中的嵌入层有两个主要参数:
-
input_dim:词汇表中唯一词项的数量,即词汇表的大小 -
output_dim: 嵌入的维度/词向量的长度
input_dim 参数需要设置为所使用词汇表的大小。output_dim 参数指定每个词项的嵌入向量的长度。
请注意,Keras 中的嵌入层也允许你使用自定义的权重矩阵。这意味着你可以在嵌入层中使用预训练的词嵌入(例如 GloVe,或你在其他模型中训练的词嵌入)。GloVe 模型已经在数十亿个词汇上进行了训练,因此利用这一强大的通用表示可能会非常有用。
注意
如果你使用预训练的词嵌入,你还可以选择使其在模型中可训练——本质上,使用 GloVe 词嵌入作为起点,并对其进行微调,以适应你的任务。这是文本迁移学习的一个很好的例子。
构建普通 RNN 模型
在下一个练习中,我们将使用普通的 RNN 构建我们的第一个情感分类模型。我们将使用的模型架构如下图所示,图中展示了模型如何处理输入句子 "Life is good",其中单词 "Life" 出现在时间步 T=0,而 "good" 则出现在时间步 T=2。模型将逐个处理输入,使用嵌入层查找词嵌入,并将其传递到隐层。当最后一个单词 "good" 在时间步 T=2 被处理时,分类操作将完成。我们将使用 Keras 来构建并训练模型:

图 6.4:使用嵌入层和 RNN 的架构
练习 6.01:构建并训练一个用于情感分类的 RNN 模型
在这个练习中,我们将构建并训练一个用于情感分类的 RNN 模型。最初,我们将定义递归层和预测层的架构,并评估模型在测试数据上的表现。接着,我们将添加嵌入层和一些 dropout,并通过添加 RNN 层、dropout 和一个全连接层来完成模型定义。然后,我们将检查模型在测试数据上的预测准确度,以评估模型的泛化能力。请按照以下步骤完成这个练习:
-
我们首先设置
numpy和tensorflow的随机数生成种子,以尽可能地获得可复现的结果。我们将导入numpy和tensorflow,并使用以下命令设置种子:import numpy as np import tensorflow as tf np.random.seed(42) tf.random.set_seed(42)注意
尽管我们已经为
numpy和tensorflow设置了种子,以实现可复现的结果,但仍有许多因素会导致结果的变化,因此你可能得到与我们不同的结果。这适用于我们今后使用的所有模型。虽然你看到的数值可能不同,但你看到的输出应该在很大程度上与我们的输出一致。如果模型的性能差异较大,你可能需要调整训练的轮次——这是因为神经网络中的权重是随机初始化的,所以你和我们的初始点可能会有所不同,而通过训练不同轮次,可能会达到类似的结果。 -
现在,让我们继续通过导入所有必要的包和层,并使用以下命令初始化一个名为
model_rnn的顺序模型:from tensorflow.keras.models import Sequential from tensorflow.keras.layers \ import SimpleRNN, Flatten, Dense, Embedding, \ SpatialDropout1D, Dropout model_rnn = Sequential() -
现在,我们需要指定嵌入层。
input_dim参数需要设置为vocab_size变量。对于output_dim参数,我们选择32。回顾第四章,文本的深度学习——嵌入,这是一个超参数,你可能需要对其进行实验以获得更好的结果。让我们通过以下命令指定嵌入层并使用丢弃层(以减少过拟合):model_rnn.add(Embedding(vocab_size, output_dim=32)) model_rnn.add(SpatialDropout1D(0.4))请注意,这里使用的丢弃层是
SpatialDropout1D——该版本执行与常规丢弃层相同的功能,但它不是丢弃单个元素,而是丢弃整个一维特征图(在我们的案例中是向量)。 -
向模型中添加一个包含
32个神经元的SimpleRNN层(该选择是随意的;另一个可调节的超参数):model_rnn.add(SimpleRNN(32)) -
接下来,添加一个
40%丢弃率的丢弃层(再次,这是一个随意的选择):model_rnn.add(Dropout(0.4)) -
添加一个带有
sigmoid激活函数的全连接层,以完成模型架构。这是输出层,用于进行预测:model_rnn.add(Dense(1, activation='sigmoid')) -
编译模型并查看模型总结:
model_rnn.compile(loss='binary_crossentropy', \ optimizer='rmsprop', metrics=['accuracy']) model_rnn.summary()模型总结如下:
![图 6.5:简单 RNN 模型的总结]()
图 6.5:简单 RNN 模型的总结
我们可以看到共有
258,113个参数,其中大部分位于嵌入层。这是因为在训练过程中,词嵌入被学习——因此我们在学习嵌入矩阵,其维度为vocab_size(8000) × output_dim(32)。接下来,我们继续训练模型(使用我们观察到的在此数据和架构中提供最佳结果的超参数)。
-
使用批量大小为
128的训练数据对模型进行训练,训练10个 epoch(这两个都是你可以调节的超参数)。使用0.2的验证集分割比例——监控这个过程将帮助我们了解模型在未见数据上的表现:history_rnn = model_rnn.fit(X_train, y_train, \ batch_size=128, \ validation_split=0.2, \ epochs = 10)最后五个 epoch 的训练输出如下。根据你的系统配置,这一步可能需要的时间会长于或短于我们所用的时间:
![图 6.6:训练简单 RNN 模型——最后五个 epoch]()
图 6.6:训练简单 RNN 模型——最后五个 epoch
从训练输出中可以看到,验证准确率达到了大约 86%。接下来,我们对测试集进行预测并检查模型的性能。
-
使用模型的
predict_classes方法对测试数据进行预测,并使用sklearn的accuracy_score方法:y_test_pred = model_rnn.predict_classes(X_test) from sklearn.metrics import accuracy_score print(accuracy_score(y_test, y_test_pred))测试的准确率如下:
0.85128我们可以看到,模型表现得相当不错。我们使用了一个简单的架构,
32个神经元,词汇表大小仅为8000。调整这些和其他超参数可能会得到更好的结果,我们鼓励你这样做。注意
若要访问此特定部分的源代码,请参考
packt.live/31ZPO2g。您也可以在
packt.live/2Oa2trm上在线运行此示例。您必须执行整个 Notebook 才能获得预期的结果。
在这个练习中,我们已经了解了如何构建基于 RNN 的文本模型。我们看到了如何使用嵌入层为当前任务推导单词向量。这些单词向量是每个传入术语的表示,它们会被传递到 RNN 层。我们已经看到,甚至一个简单的架构也能给我们带来良好的结果。现在,让我们讨论一下如何使用该模型对新的、未见过的评论进行预测。
对未见数据进行预测
现在,您已经在一些数据上训练了模型并评估了它在测试数据上的表现,接下来要学习的是如何使用此模型来预测新数据的情感。毕竟,这就是模型的目的——能够预测模型从未见过的数据的情感。实际上,对于任何以原始文本形式存在的新评论,我们都应该能够对其情感进行分类。
关键步骤是创建一个流程/管道,将原始文本转换为预测模型能够理解的格式。这意味着新文本需要经过与用于训练模型的文本数据相同的预处理步骤。预处理函数需要为任何输入的原始文本返回格式化后的文本。该函数的复杂性取决于对训练数据执行的步骤。如果分词是唯一的预处理步骤,那么该函数只需要执行分词操作。
我们的模型(model_rnn)是在 IMDB 评论数据集上训练的,这些评论数据已经经过分词、大小写转换、去除标点、定义词汇表大小并转换为索引序列。我们为 RNN 模型准备数据的函数/管道需要执行相同的步骤。让我们着手创建我们自己的函数。首先,让我们创建一个新变量,名为"inp_review",它包含文本"An excellent movie",使用以下代码。这是包含原始评论文本的变量:
inp_review = "An excellent movie!"
文本中的情感是积极的。如果模型工作得足够好,它应该将情感预测为积极的。
首先,我们必须将文本分词为其组成术语,规范化大小写并去除标点符号。为此,我们需要使用以下代码从 Keras 导入text_to_word_sequence工具:
from tensorflow.keras.preprocessing.text \
import text_to_word_sequence
为了检查它是否按预期工作,我们可以将其应用于inp_review变量,如下所示的代码:
text_to_word_sequence(inp_review)
分词后的句子如下所示:
['an', 'excellent', 'movie']
我们可以看到,它的效果正如预期——大小写已经被标准化,句子已经被分词,标点符号也已从输入文本中移除。下一步将是为数据使用一个定义好的词汇表。这将需要使用我们在加载数据时 TensorFlow 使用的相同词汇表。词汇表和术语到索引的映射可以通过imdb模块中的get_word_index方法加载(我们使用该方法加载代码)。以下代码可以用来将词汇表加载到名为word_map的字典中:
word_map = imdb.get_word_index()
这个字典包含了大约 88.6K 个术语的映射,这些术语在原始评论数据中是可用的。我们加载了一个词汇表大小为8000的数据,从而使用了映射中的前8000个索引。让我们创建一个有限词汇表的映射,这样我们就可以使用与训练数据相同的术语/索引。我们将通过根据索引对word_map变量进行排序,并选择前8000个术语来限制映射,如下所示:
vocab_map = dict(sorted(word_map.items(), \
key=lambda x: x[1])[:vocab_size])
词汇映射将是一个包含8000个术语词汇表索引映射的字典。使用这个映射,我们将通过查找每个术语并返回相应的索引,将分词后的句子转换为术语索引序列。通过以下代码,我们将定义一个接受原始文本、应用text_to_word_sequence工具、从vocab_map进行查找,并返回相应整数序列的函数:
def preprocess(review):
inp_tokens = text_to_word_sequence(review)
seq = []
for token in inp_tokens:
seq.append(vocab_map.get(token))
return seq
我们可以像这样将此函数应用于inp_review变量:
preprocess(inp_review)
输出如下:
[32, 318, 17]
这是与原始文本对应的术语索引序列。请注意,数据现在与我们加载的 IMDb 数据格式相同。这个索引序列可以被输入到 RNN 模型中(使用predict_classes方法)以分类情感,如下所示。如果模型的表现足够好,它应该预测情感为正面:
model_rnn.predict_classes([preprocess(inp_review)])
输出预测为1(正面),正如我们预期的那样:
array([[1]])
让我们将此函数应用于另一条原始文本评论,并将其提供给模型进行预测。我们将更新inp_review变量,使其包含文本"Don't watch this movie – poor acting, poor script, bad direction." 这条评论的情感是负面的。我们希望模型将其分类为负面情感:
inp_review = "Don't watch this movie"\
" - poor acting, poor script, bad direction."
让我们将预处理函数应用于inp_review变量,并使用以下代码进行预测:
model_rnn.predict_classes([preprocess(inp_review)])
预测结果为0,如图所示:
array([[0]])
预测的情感是负面的,这正是我们期望模型的行为。
我们以函数的形式将此流程应用于单条评论,但你可以很容易地将其应用于一整个评论集合,以使用模型进行预测。现在,你已经准备好使用我们训练的 RNN 模型来分类任何新的评论的情感。
注意
我们在这里构建的管道是专门针对这个数据集和模型的。这不是一个通用的处理功能,不能用于任何模型的预测。使用的词汇、进行的清理、模型学习到的模式——这些都是针对这个任务和数据集的。对于任何其他模型,你需要根据情况创建自己的管道。
这种高级方法同样可以用于为其他模型构建处理管道。根据数据、预处理步骤以及模型部署的设置,管道可能会有所不同。所有这些因素也会影响你可能想要在模型构建过程中包括的步骤。因此,我们鼓励你在开始整个建模过程时就立即开始考虑这些方面。
我们已经看到了如何使用训练好的 RNN 模型对未见过的数据进行预测,从而让我们理解了端到端的过程。在接下来的部分,我们将开始研究 RNN 的变种。到目前为止我们讨论的实现相关的思路适用于所有后续的模型。
LSTM、GRU 及其他变种
普通 RNN 的理念非常强大,并且架构已经展现出巨大的潜力。正因如此,研究人员对 RNN 架构进行了实验,寻找克服一个主要缺陷(梯度消失问题)并充分发挥 RNN 优势的方法。这导致了 LSTM 和 GRU 的出现,它们如今实际上已经取代了 RNN。事实上,现在我们提到 RNN 时,通常指的是 LSTM、GRU 或它们的变种。
这是因为这些变种专门设计来处理梯度消失问题并学习长距离依赖。两种方法在大多数序列建模任务中显著优于普通 RNN,特别是对于长序列,差异更为明显。题为 使用 RNN 编码器-解码器进行统计机器翻译学习短语表示 的论文(可在 arxiv.org/abs/1406.1078 获取)对普通 RNN、LSTM 和 GRU 的性能进行了实证分析。这些方法是如何克服普通 RNN 缺陷的呢?我们将在下一节中详细讨论 LSTM 时理解这一点。
LSTM
让我们思考一下这个问题。了解了普通 RNN 的架构后,我们如何调整它,或者说,能做什么改变以捕捉长期依赖关系呢?我们不能增加更多的层;这样做肯定会适得其反,因为每增加一层都会加重问题。一个想法(可以参考 pubmed.ncbi.nlm.nih.gov/9377276),由 Sepp Hochreiter 和 Jurgen Schmidhuber 于 1997 年提出,是使用一个显式的值(状态),它不经过激活函数。如果我们有一个自由流动且不经过激活函数的单元(对应于普通 RNN 的神经元)值,这个值可能会帮助我们建模长期依赖性。这就是 LSTM 的第一个关键区别——一个显式的单元状态。
单元状态可以被视为一种在多个时间步中识别和存储信息的方式。本质上,我们将某个值识别为网络的长期记忆,它帮助我们更好地预测输出,并且我们会小心地保持这个值,直到需要为止。
那么,我们如何调节单元状态的流动呢?我们如何决定何时更新其值以及更新多少呢?为此,Hochreiter 和 Schmidhuber 提出了使用 门控机制 来调节何时以及如何更新单元状态的值。这是 LSTM 的另一个关键区别。自由流动的单元状态与调节机制的结合,使得 LSTM 在处理长序列时表现极为出色,赋予了它强大的预测能力。
注释
对于 LSTM 的内部工作原理和相关的数学处理,本书无法做出详细的讨论。对于那些有兴趣深入阅读的读者,packt.live/3gL42Ib 是一个很好的参考资料,可以帮助你更好地理解 LSTM。
让我们理解一下 LSTM 工作原理背后的直觉。下图展示了 LSTM 单元的内部结构。除了通常的输出——隐藏状态,LSTM 单元还会输出一个单元“状态”。隐藏状态保存的是短期记忆,而单元状态则保存的是长期记忆:

图 6.7:LSTM 单元
这种对内部结构的看法可能会让人感到畏惧,因此我们将从一个更抽象的角度来理解,正如在图 6.8中所示。首先需要注意的是,对单元状态进行的唯一操作是两个线性操作——乘法和加法。单元状态并没有经过任何激活函数。这就是我们之前所说的单元状态自由流动的原因。这个自由流动的设置也叫做“常数误差旋转”(Constant Error Carousel)——这个名称你不需要记住。
FORGET 模块的输出与单元状态相乘。由于这个模块的输出值介于 0 和 1 之间(由 Sigmoid 激活函数建模),将其与单元状态相乘将调节前一个单元状态应忘记多少。如果 FORGET 模块输出 0,则前一个单元状态会完全被遗忘;如果输出 1,则单元状态完全保留。需要注意的是,FORGET 门的输入包括来自前一时间步的隐层输出 (ht-1) 和当前时间步的新输入 x`t(对于网络中的深层,可能是来自前一隐层的输出):

图 6.8: LSTM 单元的抽象视图
在前面的图中,我们可以看到,在单元状态与 FORGET 模块的结果相乘之后,接下来的决策是要更新单元状态的多少。这一部分来自于 UPDATE 模块的输出,并将其加到(注意加号)处理后的单元状态上。这样,处理后的单元状态就会被更新。这就是对前一个单元状态 (Ct-1) 执行的所有操作,最终得出新的单元状态 (Ct) 作为输出。这就是如何调节单元的长期记忆。单元还需要更新隐状态。这个操作发生在 OUTPUT 模块,基本上与普通 RNN 中的更新相同。唯一的区别是,显式的单元状态会与 Sigmoid 输出相乘,形成最终的隐状态 ht`。
现在我们已经理解了各个独立的模块/门控机制,接下来让我们在以下详细图中标出它们。这将帮助我们理解这些门控机制如何共同作用,调控 LSTM 中信息的流动:

图 6.9: LSTM 单元解释
为了使这个示例更具具体性,我们来看下图,了解单元状态是如何更新的。我们假设前一个单元状态 (Ct-1) 为 5。这个值有多少应被传递,取决于 FORGET 门的输出值。FORGET 门的输出值会与前一个单元状态 Ct-1 相乘。在这种情况下,FORGET模块的输出值是0.5,结果是传递 2.5 作为处理后的单元状态。接着,这个值 (2.5) 会遇到来自 UPDATE门的加法。由于UPDATE门的输出值为-0.8,所以加法的结果是 1.7。这就是最终更新的单元状态 Ct,它会传递给下一个时间步:

图 6.10: LSTM 单元状态更新示例
LSTM 中的参数
LSTM 是基于普通 RNN 构建的。如果你简化 LSTM 并移除所有门,仅保留tanh函数用于隐藏状态的更新,你将得到一个普通 RNN。在 LSTM 中,信息——即时间t的新输入数据和时间t-1的先前隐藏状态(xt 和ht-1)——通过的激活数量是普通 RNN 中通过的激活数量的四倍。激活分别在遗忘门中应用一次,在更新门中应用两次,在输出门中应用一次。因此,LSTM 中的权重/参数数量是普通 RNN 的四倍。
在第五章,深度学习与序列,在标题为循环神经网络(RNN)中的参数的章节中,我们计算了一个普通 RNN 的参数数量,并发现我们已经有了相当多的参数可以使用(n² + nk + nm,其中n是隐藏层神经元的数量,m是输入的数量,k是输出层的维度)。使用 LSTM 时,我们看到这个数字是普通 RNN 的四倍。无需多言,LSTM 中有大量的参数,这不一定是件好事,尤其是在处理较小的数据集时。
练习 6.02:基于 LSTM 的情感分类模型
在这个练习中,我们将构建一个简单的基于 LSTM 的模型来预测我们数据的情感。我们将继续使用之前的设置(即单元数、嵌入维度、dropout 等)。因此,你必须在相同的 Jupyter Notebook 中继续这个练习。按照以下步骤完成此练习:
-
从 Keras
layers导入 LSTM 层:from tensorflow.keras.layers import LSTM -
实例化顺序模型,添加适当维度的嵌入层,并添加 40%的空间 dropout:
model_lstm = Sequential() model_lstm.add(Embedding(vocab_size, output_dim=32)) model_lstm.add(SpatialDropout1D(0.4)) -
添加一个包含
32单元的 LSTM 层:model_lstm.add(LSTM(32)) -
添加 dropout 层(
40%的 dropout)和全连接层,编译模型并打印模型概述:model_lstm.add(Dropout(0.4)) model_lstm.add(Dense(1, activation='sigmoid')) model_lstm.compile(loss='binary_crossentropy', \ optimizer='rmsprop', metrics=['accuracy']) model_lstm.summary()模型概述如下:
![图 6.11:LSTM 模型概述]()
图 6.11:LSTM 模型概述
从模型概述中我们可以看到,LSTM 层中的参数数量为
8320。快速检查可以确认,这正是我们在练习 6.01中看到的普通 RNN 层参数数量的四倍,构建与训练用于情感分类的 RNN 模型,这与我们的预期一致。接下来,让我们用训练数据来拟合模型。 -
在训练数据上拟合
5个周期(这为模型提供了最佳结果),批量大小为128:history_lstm = model_lstm.fit(X_train, y_train, \ batch_size=128, \ validation_split=0.2, \ epochs=5)训练过程的输出如下:
![图 6.12:LSTM 训练输出]()
图 6.12:LSTM 训练输出
请注意,LSTM 的训练时间比普通 RNN 要长得多。考虑到 LSTM 的架构和庞大的参数数量,这是可以预期的。另外,值得注意的是,验证集上的准确率明显高于普通 RNN。接下来,让我们查看测试数据上的表现,看看准确率得分。
-
对测试集进行预测并打印准确率得分:
y_test_pred = model_lstm.predict_classes(X_test) print(accuracy_score(y_test, y_test_pred))准确率如下所示:
0.87032我们得到的准确率(87%)比使用普通 RNN(85.1%)得到的准确率有了显著提高。看起来,额外的参数和来自单元状态的额外预测能力对于我们的任务非常有帮助。
注意
要访问此部分的源代码,请参考
packt.live/31ZPO2g。你也可以在线运行这个例子,网址是
packt.live/2Oa2trm。你必须执行整个 Notebook 才能获得期望的结果。
在这个练习中,我们展示了如何使用 LSTM 进行文本的情感分类。训练时间明显较长,参数的数量也更多。但最终,即使是这个简单的架构(没有进行任何超参数调优)也比普通的 RNN 得到了更好的结果。我们鼓励你进一步调优超参数,以充分发挥强大的 LSTM 架构的优势。
LSTM 与普通 RNN 的对比
我们看到,LSTM 是在普通 RNN 的基础上构建的,主要目的是解决梯度消失问题,以便能够建模长范围的依赖关系。从下图可以看出,普通 RNN 仅传递隐藏状态(短期记忆),而 LSTM 同时传递隐藏状态和显式的单元状态(长期记忆),这赋予了它更多的能力。因此,当 LSTM 处理词语“good”时,递归层还会传递持有长期记忆的单元状态:

图 6.13:普通 RNN(左)和 LSTM(右)
实际上,这是否意味着你总是需要使用 LSTM?这个问题的答案,正如数据科学尤其是深度学习中的大多数问题一样,是“取决于情况”。为了理解这些考虑,我们需要了解 LSTM 与普通 RNN 相比的优缺点。
LSTM 的优点:
-
更强大,因为它使用了更多的参数和一个显式的单元状态
-
更好地建模长范围的依赖关系
LSTM 的缺点:
-
更多的参数
-
训练时间更长
-
更容易发生过拟合
如果你需要处理长序列,LSTM 会是一个不错的选择。如果你有一个小数据集,而且你处理的序列很短(<10),那么使用普通的 RNN 可能是可以的,因为它具有较少的参数(尽管你也可以尝试 LSTM,确保使用正则化来避免过拟合)。一个拥有大量数据且包含长序列的数据集,可能会从像 LSTM 这样的强大模型中提取最大价值。请注意,训练 LSTM 计算开销大且耗时,因此如果你有一个非常大的数据集,训练 LSTM 可能不是最实际的方法。当然,所有这些陈述仅作为指导——最好的方法是根据你的数据和任务选择最合适的方式。
门控循环单元
在前面的章节中,我们看到 LSTM 有很多参数,看起来比普通的 RNN 复杂得多。你可能在想,这些明显的复杂性真的有必要吗?LSTM 能不能在不失去显著预测能力的情况下简化一点?研究人员曾经也有同样的疑问,并且在 2014 年,Kyunghyun Cho 和他们的团队在他们的论文中提出了 GRU,作为 LSTM 在机器翻译中的替代方案(arxiv.org/abs/1406.1078)。
GRU 是 LSTM 的简化形式,旨在减少参数数量,同时保留 LSTM 的强大功能。在语音建模和语言建模任务中,GRU 提供与 LSTM 相同的性能,但具有更少的参数和更快的训练时间。
在 GRU 中做出的一个重要简化是省略了显式的单元状态。这听起来有些违反直觉,因为自由流动的单元状态正是赋予 LSTM 其强大能力的因素,对吧?其实,赋予 LSTM 强大能力的并不是单元状态本身,而是单元状态的自由流动性?确实,如果单元状态也要经过激活函数,LSTM 可能不会取得如此大的成功:

图 6.14:门控循环单元
所以,自由流动的值是关键的区分思想。GRU 通过允许隐藏状态自由流动来保留这一思想。让我们看看前面的图,以理解这意味着什么。GRU 允许隐藏状态自由地通过。换句话说,GRU 实际上将单元状态(如 LSTM 中的单元状态)的思想带到了隐藏状态。
我们仍然需要调节隐藏状态的流动,因此我们仍然需要门控机制。GRU 将遗忘门和更新门合并成一个更新门。为了理解背后的动机,请考虑这一点——如果我们忘记了一个单元状态,并且没有更新它,我们到底在做什么?也许拥有一个单一的更新操作是有意义的。这是架构中的第二个主要区别。
由于这两个变化,GRU 使数据经过了三个激活层,而不是 LSTM 中的四个激活层,从而减少了参数的数量。虽然 GRU 仍然拥有普通 RNN 的三倍参数,但它只有 LSTM 的 75% 的参数,这是一项值得欢迎的改进。我们仍然可以让信息在网络中自由流动,这应该能帮助我们建模长程依赖。
让我们看看基于 GRU 的模型在情感分类任务上的表现如何。
练习 6.03:基于 GRU 的情感分类模型
在本练习中,我们将构建一个简单的基于 GRU 的模型来预测数据中的情感。我们将继续使用之前的相同设置(即单元数、嵌入维度、丢弃率等)。在模型中使用 GRU 代替 LSTM 就像在添加层时将 "LSTM" 替换为 "GRU" 一样简单。按照以下步骤完成本练习:
-
从 Keras
layers中导入GRU层:from tensorflow.keras.layers import GRU -
实例化顺序模型,添加适当维度的嵌入层,并加入 40% 的空间丢弃层:
model_gru = Sequential() model_gru.add(Embedding(vocab_size, output_dim=32)) model_gru.add(SpatialDropout1D(0.4)) -
添加一个具有 32 个单元的 GRU 层。将
reset_after参数设置为False(这是 TensorFlow 2 实现的一个小细节,为了与普通 RNN 和 LSTM 的实现保持一致):model_gru.add(GRU(32, reset_after=False)) -
添加丢弃层(40%)和全连接层,编译模型,并打印模型摘要:
model_gru.add(Dropout(0.4)) model_gru.add(Dense(1, activation='sigmoid')) model_gru.compile(loss='binary_crossentropy', \ optimizer='rmsprop', metrics=['accuracy']) model_gru.summary()模型摘要如下:
![图 6.15:GRU 模型总结]()
](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_06_15.jpg)
图 6.15:GRU 模型总结
从 GRU 模型的摘要中,我们可以看到 GRU 层的参数数量为
6240。你可以检查,这正好是我们在 练习 6.01,《构建与训练情感分类的 RNN 模型》中看到的普通 RNN 层参数的三倍,也是我们在 练习 6.02,《基于 LSTM 的情感分类模型》中看到的 LSTM 层参数的 0.75 倍——这再次符合我们的预期。接下来,让我们在训练数据上拟合模型。 -
在训练数据上训练四个 epoch(这给我们最佳结果):
history_gru = model_gru.fit(X_train, y_train, \ batch_size=128, \ validation_split=0.2, \ epochs = 4)训练过程的输出如下所示:
![图 6.16:GRU 训练输出]()
](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_06_16.jpg)
图 6.16:GRU 训练输出
注意,训练 GRU 的时间比普通 RNN 长,但比 LSTM 快。验证准确度优于普通 RNN,并且接近 LSTM 的水平。让我们看看模型在测试数据上的表现如何。
-
在测试集上进行预测并打印准确度评分:
y_test_pred = model_gru.predict_classes(X_test) accuracy_score(y_test, y_test_pred)准确度如下所示:
0.87156我们可以看到,GRU 模型的准确度(87.15%)与 LSTM 模型(87%)非常相似,且高于普通 RNN。这是一个重要的点——GRU 是 LSTM 的简化版本,旨在以更少的参数提供类似的准确性。我们的练习表明这一点是正确的。
注意
若要访问此特定部分的源代码,请参阅
packt.live/31ZPO2g。你也可以在线运行这个示例,访问
packt.live/2Oa2trm。你必须执行整个 Notebook 才能得到预期结果。
在本次练习中,我们看到如何使用 GRU 进行文本情感分类。训练时间稍低于 LSTM 模型,而且参数数量也更少。即使是这个简单的架构(没有任何超参数调整)也给出了比普通 RNN 模型更好的结果,并且与 LSTM 模型的结果相似。
LSTM 与 GRU
那么,你应该选择哪个呢?LSTM 有更多的参数,并且设计了一个显式的单元状态来存储长期记忆。GRU 参数较少,这意味着训练更快,同时它也具有自由流动的单元状态,使其能够建模长距离的依赖关系。
2014 年,Junyoung Chung、Yoshua Bengio 及其团队对音乐建模和语音建模任务进行的实证评估(可在arxiv.org/abs/1412.3555获取)表明,LSTM 和 GRU 都明显优于普通的 RNN。他们还发现,在性能方面,GRU 与 LSTM 不分上下。他们指出,调整超参数,如层大小,可能比选择 LSTM 或 GRU 更为重要。
2018 年,Gail Weiss、Yoav Goldberg 及其团队展示并得出结论:LSTM 在需要无限计数的任务中优于 GRU,也就是说,处理任意长度序列的任务。2018 年,Google Brain 团队也展示了 LSTM 在机器翻译任务中的表现优于 GRU。这让我们思考,LSTM 所带来的额外优势可能在某些应用中非常有用。
双向 RNN
我们刚刚看的 RNN 模型——LSTM、GRU——确实很强大,并且在序列处理任务中提供了极好的结果。现在,让我们讨论如何让它们更强大,以及那些带来深度学习惊人成就的方法,你一定听说过。
让我们从双向 RNN 的概念开始。这个概念适用于所有类型的 RNN,包括但不限于 LSTM 和 GRU。双向 RNN 可以同时处理序列的正向和反向,使网络可以获得关于序列的前向和后向信息,提供更加丰富的上下文:

图 6.17:双向 LSTM
双向模型本质上是并行使用两个 RNN——一个作为"前向层",另一个作为"后向层"。如前图所示,前向层按元素顺序处理序列。对于句子"Life is good",前向层首先处理"Life",然后是"is",接着是"good"——这与常规的 RNN 层没有区别。后向层则反转这个顺序——它首先处理"good",然后是"is",最后是"Life"。在每一步中,前向层和后向层的状态会被连接起来形成输出。
哪种任务最能从这种架构中受益?查看上下文的两个方面有助于解决当前词语的歧义。当我们阅读类似"The stars"的句子时,我们不确定这里的"stars"指的是什么——是天空中的星星,还是电影明星?但当我们看到后续的词语并阅读"The stars at the movie premiere"时,我们就能确定这句话是指电影明星。那些最能从这种设置中受益的任务包括机器翻译、词性标注、命名实体识别和词预测等。双向 RNN 在一般文本分类任务中也表现出了性能提升。让我们将基于双向 LSTM 的模型应用到情感分类任务中。
练习 6.04:基于双向 LSTM 的情感分类模型
在这个练习中,我们将使用双向 LSTM 来预测我们数据的情感。我们将使用 Keras 的双向包装器来创建 LSTM 的双向层(你也可以通过简单地将LSTM替换为GRU来创建一个双向 GRU 模型)。请按照以下步骤完成这个练习:
-
从 Keras
layers中导入Bidirectional层。这个层本质上是一个包装器,可以用于其他 RNN:from tensorflow.keras.layers import Bidirectional -
实例化顺序模型,添加具有适当维度的嵌入层,并添加 40%的空间丢弃:
model_bilstm = Sequential() model_bilstm.add(Embedding(vocab_size, output_dim=32)) model_bilstm.add(SpatialDropout1D(0.4)) -
将
Bidirectional包装器添加到一个具有32单元的 LSTM 层:model_bilstm.add(Bidirectional(LSTM(32))) -
添加丢弃层(40%)和密集层,编译模型,并打印模型摘要:
model_bilstm.add(Dropout(0.4)) model_bilstm.add(Dense(1, activation='sigmoid')) model_bilstm.compile(loss='binary_crossentropy', \ optimizer='rmsprop', metrics=['accuracy']) model_bilstm.summary()总结如下:
![图 6.18:双向 LSTM 模型总结]()
图 6.18:双向 LSTM 模型总结
请注意前面截图中显示的模型参数。不出所料,双向 LSTM 层有
16640个参数——是 LSTM 层(8320个参数)数量的两倍,正如练习 6.02,基于 LSTM 的情感分类模型所展示的。这是普通 RNN 参数数量的八倍。接下来,让我们在训练数据上拟合该模型。 -
用
128的批大小训练数据四个周期:history_bilstm = model_bilstm.fit(X_train, y_train, \ batch_size=128, \ validation_split=0.2, \ epochs = 4)训练的输出如下:
![图 6.19:双向 LSTM 训练输出]()
图 6.19:双向 LSTM 训练输出
请注意,正如我们预期的那样,训练双向 LSTM 比常规 LSTM 花费的时间要长得多,比普通 RNN 的时间还要长好几倍。验证准确度似乎更接近 LSTM 的准确度。
-
在测试集上进行预测并打印准确度得分:
y_test_pred = model_bilstm.predict_classes(X_test) accuracy_score(y_test, y_test_pred)准确度如下:
0.877
我们在这里得到的准确度(87.7%)比 LSTM 模型的准确度略有提高,LSTM 的准确度为 87%。同样,你可以进一步调整超参数,以最大限度地发挥这种强大架构的优势。请注意,我们的参数数量是 LSTM 模型的两倍,是普通 RNN 的八倍。使用大型数据集可能会使性能差异更加明显。
注意
要访问此特定章节的源代码,请参考packt.live/31ZPO2g。
你也可以在线运行这个示例,网址是packt.live/2Oa2trm。你必须执行整个笔记本才能获得期望的结果。
堆叠 RNN
现在,让我们看看我们可以采取的另一种方法,以从 RNN 中提取更多的能力。在本章中我们查看的所有模型中,我们都使用了单层 RNN 层(普通 RNN、LSTM 或 GRU)。深入学习,即添加更多层,通常有助于前馈网络,这样我们可以在更深的层中学习更复杂的模式/特征。尝试这种思路在递归网络中也有其价值。事实上,堆叠 RNN 确实似乎为我们提供了更多的预测能力。
以下图示例了一个简单的两层堆叠 LSTM 模型。堆叠 RNN 仅仅意味着将一个 RNN 层的输出馈送到另一个 RNN 层。RNN 层可以输出序列(即在每个时间步的输出),这些输出可以像任何输入序列一样传递给后续的 RNN 层。在代码实现方面,堆叠 RNN 就是简单地从一层返回序列,并将其作为输入提供给下一个 RNN 层,即紧接着的下一层:

图 6.20:两层堆叠 RNN
让我们通过在情感分类任务中使用堆叠 RNN(LSTM)来看看它的实际效果。
练习 6.05:基于堆叠 LSTM 的情感分类模型
在本练习中,我们将通过堆叠两个 LSTM 层,进一步深入 RNN 架构,以预测我们数据的情感。我们将继续使用之前练习中的相同设置(单元数量、嵌入维度、丢弃率等)用于其他层。请按照以下步骤完成本练习:
-
实例化顺序模型,添加具有适当维度的嵌入层,并添加 40% 的空间丢弃:
model_stack = Sequential() model_stack.add(Embedding(vocab_size, output_dim=32)) model_stack.add(SpatialDropout1D(0.4)) -
添加一个具有
32个单元的 LSTM 层。确保在 LSTM 层中将return_sequences设置为True。这样将返回 LSTM 在每个时间步的输出,之后可以将其传递到下一个 LSTM 层:model_stack.add(LSTM(32, return_sequences=True)) -
增加一个具有
32个单元的 LSTM 层。这次,你不需要返回序列。你可以将return_sequences选项设置为False,或者完全跳过该选项(默认值是False):model_stack.add(LSTM(32, return_sequences=False)) -
添加 dropout(50%的 dropout;这是因为我们正在构建一个更复杂的模型)和 dense 层,编译模型并打印模型摘要:
model_stack.add(Dropout(0.5)) model_stack.add(Dense(1, activation='sigmoid')) model_stack.compile(loss='binary_crossentropy', \ optimizer='rmsprop', \ metrics=['accuracy']) model_stack.summary()汇总如下:
![图 6.21:堆叠 LSTM 模型概述]()
图 6.21:堆叠 LSTM 模型概述
请注意,堆叠 LSTM 模型的参数数量与双向模型相同。让我们在训练数据上拟合模型。
-
在训练数据上进行四个 epoch 的拟合:
history_stack = model_stack.fit(X_train, y_train, \ batch_size=128, \ validation_split=0.2, \ epochs = 4)训练的输出如下:
![图 6.22:堆叠 LSTM 训练输出]()
图 6.22:堆叠 LSTM 训练输出
训练堆叠 LSTM 花费的时间比训练双向 LSTM 少。验证准确率似乎接近双向 LSTM 模型的准确率。
-
在测试集上进行预测并打印准确率:
y_test_pred = model_stack.predict_classes(X_test) accuracy_score(y_test, y_test_pred)准确率如下所示:
0.87572
87.6%的准确率比 LSTM 模型(87%)有所提高,且几乎与双向模型(87.7%)相同。考虑到我们使用的是相对较小的数据集,这对于常规 LSTM 模型的性能来说是一个相当显著的提升。数据集越大,越能从这些复杂的架构中获益。尝试调整超参数,以便最大限度地发挥这种强大架构的优势。
注意
要访问此特定部分的源代码,请参考packt.live/31ZPO2g。
你也可以在packt.live/2Oa2trm上在线运行此示例。你必须执行整个 Notebook 才能获得预期的结果。
汇总所有模型
在本章中,我们研究了不同变种的 RNN——从普通 RNN 到 LSTM 再到 GRU。我们还看了双向方法和堆叠方法在使用 RNN 时的应用。现在是时候全面地回顾一下,并对这些模型进行比较了。让我们看下表,它比较了五种模型在参数、训练时间和性能(即在我们的数据集上的准确性水平)方面的差异:

图 6.23:比较五个模型
注意
如本章前面所提到的,在实际操作过程中,你可能会得到与上面显示的不同的数值;然而,你获得的测试准确率应该与我们的结果大致相符。如果模型的表现差异较大,你可能需要调整 epoch 的数量。
普通的 RNN 在参数量和训练时间上是最低的,但其准确性也是所有模型中最低的。这与我们的预期一致——我们正在处理的是 200 字符长的序列,我们知道普通 RNN 不太可能表现好,而门控 RNN(LSTM、GRU)更为适合。实际上,LSTM 和 GRU 的表现明显优于普通 RNN。但这种准确性是以显著更高的训练时间和几倍的参数量为代价的,使得这些模型更容易过拟合。
堆叠和双向处理的方式似乎在预测能力上提供了增量效益,但这需要以显著更长的训练时间和几倍的参数为代价。堆叠和双向的方式为我们提供了最高的准确性,即使是在这个小数据集上。
尽管性能结果特定于我们的数据集,但我们在这里看到的性能梯度是相当常见的。堆叠和双向模型在今天许多解决方案中都出现了,这些解决方案在各种任务中提供了最先进的结果。随着数据集的增大以及处理更长序列时,我们预计模型性能差异会更大。
注意力模型
注意力模型最早由 Dzmitry Bahdanau、KyungHyun Cho 和 Yoshua Bengio 在 2015 年底的具有影响力的开创性论文中提出(arxiv.org/abs/1409.0473),该论文展示了英法翻译的最先进结果。从那时起,这一思想已被广泛应用于许多序列处理任务,并取得了巨大的成功,注意力模型也变得越来越流行。尽管本书不涉及详细的解释和数学处理,但我们可以理解这一思想的直觉,这一思想被许多深度学习领域的大佬视为我们对序列建模方法的重大进展。
注意力背后的直觉可以通过它最初为翻译任务开发的例子来最好地理解。当一个新手翻译长句子时,他们并不会一次性翻译整个句子,而是将原始句子拆分成更小、更易管理的部分,从而按顺序为每个部分生成翻译。对于每个部分,都有一部分是最重要的,需要特别关注的部分,即你需要集中注意力的地方:

图 6.24:简化的注意力概念
上面的图展示了一个简单的例子,我们将句子“Azra is moving to Berlin”翻译成法语。法语翻译是“Azra déménage à Berlin”。为了得到法语翻译中的第一个词“Azra”,我们需要主要关注原句中的第一个词(由浅灰色线条标示),或许稍微关注第二个词(由深灰色线条标示)——这些词被赋予了更高的重要性(权重)。剩余的部分并不相关。类似地,为了生成输出中的词“déménage”,我们需要关注词“is”和“moving”。每个词对输出词的影响程度由权重表示。这就是所谓的对齐。
以下图展示了这些对齐关系,来源于原始论文(arxiv.org/abs/1409.0473)。它美妙地展示了模型识别出输出中每个词项最重要的部分。网格中单元格的浅色表示该列对应输入词项的权重较高。我们可以看到,对于输出词项"marin",模型正确地识别出"marine"是最重要的输入词项。类似地,它识别出"environment"是"environnement"的最重要词项,"known"是"connu"的最重要词项,依此类推。挺酷的,不是吗?

图 6.25:模型学习到的对齐
尽管注意力模型最初是为翻译任务设计的,但这些模型已经成功地应用于许多其他任务。需要注意的是,注意力模型的参数非常多。这些模型通常应用于双向 LSTM 层,并为重要性值添加额外的权重。参数数量庞大使得模型更容易过拟合,这意味着它们需要更大的数据集才能发挥其强大的能力。
更多 RNN 变种
在本章中,我们已经看到了许多 RNN 的变种——涵盖了所有主要的变种以及一些未来可能会流行的变种。序列建模及其相关架构是一个热门的研究领域,每年都有很多新进展。许多变种旨在创建更轻量的模型,具有更少的参数,且不像当前的 RNN 那样对硬件要求高。时钟型 RNN(CWRNNs)是最近的一项发展,并且取得了巨大成功。还有层次化注意力网络,该网络基于注意力机制的思想,但最终也提出不应将 RNN 作为构建模块。在这个激动人心的领域里,充满了各种新动向,所以一定要保持警觉,关注下一个重要的想法。
活动 6.01:亚马逊产品评论情感分析
到目前为止,我们已经看过 RNN 的变体,并用它们来预测 IMDb 数据集上的电影评论情感。在这个活动中,我们将构建一个基于亚马逊产品评论的情感分类模型。数据包含了多个类别产品的评论。原始数据集可以在snap.stanford.edu/data/web-Amazon.html找到,它非常庞大,因此我们为本次活动采样了 50,000 条评论。
注意
采样的数据集已经被分割为训练集和测试集,可以在packt.live/3iNTUjN找到。
本次活动将汇总我们在本章中讨论的概念和方法,以及第四章,文本的深度学习——嵌入和第五章,序列的深度学习中讨论的内容。你将从进行详细的文本清理和预处理开始,为深度学习模型做好准备。你还将使用嵌入来表示文本。在预测部分,你将使用堆叠 LSTM(两层)和两个全连接层。
为了方便(并且有意识地处理),你还将使用 TensorFlow(Keras)中的Tokenizer API 将清理后的文本转换为相应的序列。Tokenizer结合了NLTK中的分词器功能和vectorizer(CountVectorizer / TfIdfVectorizer),它首先对文本进行分词,然后从数据集中学习词汇表。让我们通过以下命令创建一些玩具数据来实际操作:
sents = ["life is good", "good life", "good"]
可以通过以下命令导入Tokenizer,实例化它并在玩具数据上进行拟合:
tok = Tokenizer()
tok.fit_on_texts(sents)
一旦词汇表在玩具数据上进行训练(每个术语的索引已经学习),我们就可以将输入文本转换为相应的术语索引序列。让我们使用分词器的texts_to_sequences方法将玩具数据转换为相应的索引序列:
tok.texts_to_sequences(sents)
我们将得到以下输出:
[[2, 3, 1], [1, 2], [1]]
现在,数据格式与我们在本章中使用的 IMDb 数据集相同,可以以类似的方式进行处理。
有了这些,你现在准备好开始了。以下是你需要遵循的高层步骤,来完成本次活动:
-
读取训练集和测试集的数据文件(
Amazon_reviews_train.csv和Amazon_reviews_test.csv)。检查数据集的形状,并打印出训练数据中的前五条记录。 -
为了便于处理,请将训练集和测试集的原始文本和标签分开。打印出训练文本中的前两个评论。你应该有以下四个变量:
train_raw包含训练数据的原始文本,train_labels是训练数据的标签,test_raw包含测试数据的原始文本,test_labels是测试数据的标签。 -
使用 NLTK 的
word_tokenize标准化大小写并对测试和训练文本进行分词(当然,在使用之前需要先导入——提示:使用列表推导式可以使代码更简洁)。打印训练数据中的第一条评论,检查分词是否成功。如果之前没有使用过分词器,请从 NLTK 下载punkt。 -
导入 NLTK 内置的
stopwords和字符串模块中的标点符号。定义一个函数(drop_stop)以从任何输入的分词句子中移除这些标记。如果之前没有使用过stopwords,请从 NLTK 下载它。 -
使用定义的函数(
drop_stop),从训练和测试文本中移除冗余的停用词。打印处理后的训练文本中的第一条评论,检查函数是否生效。 -
使用 NLTK 中的
Porter Stemmer对训练和测试数据的标记进行词干提取。 -
为训练和测试评论创建字符串。这将帮助我们利用 Keras 中的工具创建和填充序列。创建
train_texts和test_texts变量。打印经过处理的训练数据中的第一条评论以确认它。 -
从 Keras 的文本预处理工具(
keras.preprocessing.text)中导入Tokenizer模块。定义词汇表大小为10000,并用此词汇表实例化分词器。 -
在训练文本上拟合分词器。这个过程与第四章 深度学习文本——词嵌入中的
CountVectorizer类似,训练词汇表。拟合后,使用分词器的texts_to_sequences方法对训练和测试集进行序列化。打印训练数据中第一条评论的序列。 -
我们需要找到在模型中处理的最佳序列长度。获取训练集评论的长度,并绘制长度的直方图。
-
数据现在与我们在本章中使用的 IMDb 数据格式相同。使用
100的序列长度(定义maxlen = 100变量),使用 Keras 预处理工具中的sequence模块的pad_sequences方法(keras.preprocessing.sequence)将训练和测试数据的序列长度限制为100。检查训练数据的结果形状。 -
为了构建模型,从 Keras 中导入所有必要的层(
embedding、spatialdropout、LSTM、dropout和dense),并导入Sequential模型。初始化Sequential模型。 -
添加一个包含
32向量大小(output_dim)的嵌入层。添加一个40%的空间 Dropout。 -
构建一个堆叠的 LSTM 模型,包含
2层,每层64个单元。添加一个40%Dropout 层。 -
添加一个包含
32个神经元、relu激活函数的全连接层,然后是一个50%的 Dropout 层,接着是另一个包含32个神经元、relu激活函数的全连接层,再跟随一个50%的 Dropout 层。 -
添加最后一个包含一个神经元的全连接层,使用
sigmoid激活函数,并编译模型。打印模型摘要。 -
使用
20%的验证集和128的批量大小,在训练数据上拟合模型,训练5个周期。 -
使用模型的
predict_classes方法在测试集上进行预测。然后,使用scikit-learn中的accuracy_score方法计算测试集上的准确率,并打印出混淆矩阵。
使用前述的参数,你应该能够得到大约86%的准确率。通过调整超参数,你应该能够获得显著更高的准确率。
注意
本活动的详细步骤,以及解决方案和额外的评论,已在第 416 页呈现。
总结
在本章中,我们首先了解了为什么普通 RNN 对于非常长的序列不实用——主要原因是消失梯度问题,它使得建模长范围依赖关系变得不切实际。我们看到 LSTM 作为一种更新,它在处理长序列时表现非常好,但它相当复杂,并且有大量的参数。GRU 是一个很好的替代方案,它是 LSTM 的简化版,并且在较小的数据集上表现良好。
然后,我们开始探索如何通过使用双向 RNN 和堆叠的 RNN 层来从这些 RNN 中提取更多的能力。我们还讨论了注意力机制,这是一种重要的新方法,在翻译中提供了最先进的结果,但也可以应用于其他序列处理任务。所有这些都是极其强大的模型,改变了许多任务的执行方式,并为产生最先进结果的模型奠定了基础。随着该领域的积极研究,我们预计随着更多新颖的变种和架构的发布,情况只会变得更好。
现在我们已经讨论了各种强大的建模方法,在下一章中,我们将准备好讨论深度学习领域中一个非常有趣的话题,它使得人工智能能够具有创造力——生成对抗网络。
第七章:7. 生成对抗网络
引言
在本章中,您将探索深度学习领域中另一个有趣的主题:生成对抗网络(GANs)。您将介绍 GANs 及其基本组件,以及它们的一些用例。本章将通过创建一个 GAN 来生成由正弦函数产生的数据分布,为您提供实践经验。您还将介绍深度卷积 GANs,并进行一个生成 MNIST 数据分布的练习。通过本章的学习,您将测试您对 GANs 的理解,生成 MNIST 时尚数据集。
引言
创造力的力量一直是人类思维的专属领域。这是被宣称为人类思维与人工智能领域之间主要差异之一的事实。然而,在最近的过去,深度学习一直在迈向创造力之路上迈出婴儿步伐。想象一下,你在梵蒂冈的西斯廷教堂,目瞪口呆地仰望米开朗基罗永恒不朽的壁画,希望你的深度学习模型能够重新创作出类似的作品。嗯,也许 10 年前,人们会嗤之以鼻。但现在不同了——深度学习模型在重现不朽作品方面取得了巨大进展。这类应用得益于一类称为生成对抗网络(GANs)的网络。
许多应用都依靠 GANs 实现。请看下面的图像:

图 7.1:使用 GANs 进行图像翻译
注意:
上述图像来源于标题为《带条件对抗网络的图像到图像翻译》的研究论文:Phillip Isola、Jun-Yan Zhu、Tinghui Zhou、Alexei A. Efros,详见arxiv.org/pdf/1611.07004.pdf。
上述图像展示了输入图像如何通过 GAN 转换为看起来与真实图像非常相似的图像。这种 GAN 的应用称为图像翻译。
除了这些例子,许多其他用例也开始受到关注。其中一些显著的用例如下:
-
用于数据增强的合成数据生成
-
生成卡通人物
-
文本到图像翻译
-
三维对象生成
无论时间如何流逝,GANs 的应用越来越成为主流。
那么,GANs 究竟是什么?GANs 的内在动态是什么?如何从完全不相关的分布中生成图像或其他数据分布?在本章中,我们将找到这些问题的答案。
在上一章,我们学习了循环神经网络(RNNs),一种用于序列数据的深度学习网络。在本章中,我们将开启一场关于 GAN 的精彩探索之旅。首先,我们将介绍 GAN。然后,我们将重点介绍生成一个与已知数学表达式相似的数据分布。接下来,我们将介绍深度卷积 GANs(DCGANs)。为了验证我们的生成模型的效果,我们将生成一个类似于 MNIST 手写数字的数据分布。我们将从学习 GAN 开始这段旅程。
注意
根据你的系统配置,本章中的一些练习和活动可能需要较长时间才能执行完成。
生成对抗网络的关键组件
GANs 用于从随机噪声数据中创建一个数据分布,并使其看起来类似于真实的数据分布。GANs 是一类深度神经网络,由两个相互竞争的网络组成。其中一个网络叫做生成器网络,另一个叫做判别器网络。这两个网络的功能是相互竞争,生成一个尽可能接近真实概率分布的概率分布。举一个生成新概率分布的例子,假设我们有一组猫狗的图片(真实图片)。使用 GAN,我们可以从一个非常随机的数字分布中生成一组不同的猫狗图片(虚假图片)。GAN 的成功之处在于生成最佳的猫狗图片集,以至于人们很难分辨虚假图片和真实图片。
另一个 GAN 可以派上用场的例子是在数据隐私领域。尤其是在金融和医疗等领域,公司的数据非常敏感。然而,在某些情况下,可能需要将数据共享给第三方进行研究。为了保持数据的机密性,公司可以使用 GAN 生成与现有数据集相似的数据集。在许多业务场景中,GAN 都可以发挥重要作用。
让我们通过绘制一些 GAN 的组件来更好地理解 GAN,如下图所示:

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_07_02.jpg)
图 7.2:GAN 结构示例
上面的图提供了 GAN 组件的简要概述,并展示了它们如何帮助从真实图片生成虚假图片。让我们在前述图的背景下理解这个过程:
-
前面图中左上角的图片集合代表了真实数据的概率分布(例如,MNIST、猫狗图片、人脸图片等)。
-
图表左下部分显示的生成网络从随机噪声分布中生成虚假图像(概率分布)。
-
经过训练的鉴别网络对输入的图像进行分类,判断其真伪。
-
反馈循环(菱形框)通过反向传播算法向生成器网络提供反馈,从而优化生成模型的参数。
-
参数会持续优化,直到鉴别网络无法区分虚假图像和真实图像为止。
现在我们已经对每个组件有了概述,让我们通过问题陈述更深入地理解它们。
问题陈述 – 生成与给定数学函数类似的分布
在这个问题中,我们将使用 GAN 生成类似于数学函数数据分布的分布。我们将使用简单的正弦波函数来生成真实数据,训练 GAN 生成与从已知数学函数生成的数据类似的虚假数据分布。在解决这个问题陈述的过程中,我们将逐步构建每个所需的组件。
我们将按照以下图解中详细说明的步骤,采用教学方法来执行该过程:

图 7.3:从已知函数构建 GAN 的四步过程
现在,让我们逐个探索这些过程。
过程 1 – 从已知函数生成真实数据
要开始我们的旅程,我们需要一个真实数据分布。这个数据分布将包括两个特征 – 第一个是序列,第二个是序列的正弦值。第一个特征是等间隔的数据点序列。为了生成这个序列,我们需要从正态分布随机生成一个数据点,然后找到等间隔序列中的其他数值。第二个特征将是第一个特征的sine()。这两个特征将构成我们的真实数据分布。在进入生成真实数据集的练习之前,让我们看一下我们将在此过程中使用的numpy库中的一些函数。
随机数生成
首先,我们将使用以下函数从正态分布中生成一个随机数:
numpy.random.normal(loc,scale,size)
此函数接受三个参数:
-
loc: 这是数据分布的均值。 -
scale: 这是数据分布的标准差。 -
size: 这定义了我们想要的数据点数量。
将数据排列成序列
要将数据排列成序列,我们使用以下函数:
numpy.arange(start,end,spacing)
参数如下:
-
start: 序列应从此点开始。 -
end: 序列结束的点。 -
spacing:序列中每个连续数字之间的间隔。例如,如果我们从 1 开始,生成一个间隔为0.1的序列,那么序列将如下所示:
1, 1.1,1.2 ……..
生成正弦波
要生成一个数字的正弦值,我们使用以下命令:
numpy.sine()
让我们在接下来的练习中使用这些概念,学习如何生成一个真实的数据分布。
练习 7.01:从已知函数生成数据分布
在本练习中,我们将从一个简单的正弦函数生成数据分布。通过完成此练习,你将学习如何从正态分布中生成一个随机数,并使用这个随机数作为中心生成一个等间隔的数据序列。这个序列将是第一个特征。第二个特征将通过计算第一个特征的sin()值来创建。按照以下步骤完成此练习:
-
打开一个新的 Jupyter Notebook,并将其命名为
Exercise 7.01。运行以下命令以导入必要的库:# Importing the necessary library packages import numpy as np -
从均值为 3、标准差为 1 的正态分布中生成一个随机数:
""" Generating a random number from a normal distribution with mean 3 and sd = 1 """ np.random.seed(123) loc = np.random.normal(3,1,1) loc array([1.9143694]) -
使用之前生成的随机数作为中点,我们将在中点的左右两侧生成相等数量的数字序列。我们将生成 128 个数字。因此,我们在中点的左右两侧各取 64 个数字,间隔为 0.1。以下代码生成了中点右侧的序列:
# Generate numbers to right of the mid point xr = np.arange(loc,loc+(0.1*64),0.1) -
生成中点左侧的 64 个数字:
# Generate numbers to left of the random point xl = np.arange(loc-(0.1*64),loc,0.1) -
将这两个序列连接起来,生成第一个特征:
# Concatenating both these numbers X1 = np.concatenate((xl,xr)) print(X1)你应该得到类似下面的输出:
![图 7.4:等间隔数字序列]()
](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_07_04.jpg)
图 7.4:等间隔数字序列
上述是
128个等间隔数字的分布。这个序列将成为我们数据分布的第一个特征。 -
生成第二个特征,即第一个特征的
sin()值:# Generate second feature X2 = np.sin(X1) -
绘制分布图:
# Plot the distribution import matplotlib.pyplot as plot plot.plot(X1, X2) plot.xlabel('Data Distribution') plot.ylabel('Sine of data distribution') plot.show()你应该得到以下输出:
![图 7.5:正弦函数的图表]()
](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_07_05.jpg)
图 7.5:正弦函数的图表
上述图表显示了你试图使用 GANs 模仿的分布。
-
在连接这些特征之前,先对每个特征进行重塑:
# Reshaping the individual data sets X1 = X1.reshape(128,1) X2 = X2.reshape(128,1) -
将这两个特征连接起来,形成一个单一的 DataFrame:
# Concatenate both features to form the real data set realData = np.concatenate((X1,X2),axis=1) realData.shape你应该得到以下输出:
(128, 2)注意
要访问此特定部分的源代码,请参考
packt.live/3gHhv42。你也可以在
packt.live/2O62M6r在线运行此示例。你必须执行整个 Notebook 才能得到期望的结果。
在这个练习中,我们从一个数学函数创建了一个数据分布。稍后我们将使用这个数据分布来训练 GAN 生成类似的分布。在生产环境中,你会获得一个真实的数据集,类似于 MNIST 或Imagenet数据集。在本例中,我们的真实数据集是一个已知的数学函数。稍后在本章中,我们将使用一些随机噪声数据,并训练 GAN 使这些随机噪声数据类似于这个真实的数据分布。
现在我们已经看到真实数据分布,接下来的部分将专注于创建一个基本的生成网络。
过程 2 – 创建一个基本的生成网络
在之前的过程里,我们做了一个例子来生成来自已知函数的分布。正如我们之前提到的,生成网络的目的是从任意分布中采样数据,并将这些数据转换为生成的样本,使其看起来与已知分布相似。
生成网络实现这一点的方式是通过生成器、判别器和训练过程的动态关系。生成网络的成功取决于它能否创建出判别器无法区分的数据分布——换句话说,判别器无法判断这个分布是否是假的。生成网络能够创建可以欺骗判别器的分布的能力,是通过训练过程获得的。我们稍后会详细讲解判别器和训练过程。现在,让我们看看如何构建一个生成器网络,以便从某些随机分布中生成虚假的数据分布。
构建生成网络
生成网络是通过训练神经网络将任意分布转换为类似已知分布的网络。我们可以使用任何类型的神经网络来构建这个生成器网络,比如多层感知机(MLPs)、卷积神经网络(CNNs)等。输入这些网络的数据是我们从任意分布中采样得到的样本。在这个例子中,我们将使用 MLP 来构建生成网络。在我们开始构建网络之前,让我们回顾一下你在前几章中学到的一些神经网络的基本构件。我们将使用 Keras 库来构建网络。
Sequential()
如你所知,神经网络由不同层次的节点组成,这些节点之间有连接。Sequential() API 是你在 Keras 中创建这些层的机制。Sequential() API 可以通过以下代码实例化:
from tensorflow.keras import Sequential
Genmodel= Sequential()
在代码的第一部分,Sequential() 类从 tensorflow.Keras 模块中导入。然后,它在第二行代码中被实例化为变量 model。
核心初始化器
在第二章,神经网络中,你已经学习过训练过程涉及更新神经网络的权重和偏置,以便有效地学习将输入映射到输出的函数。在训练过程的第一步,我们初始化权重和偏置的某些值。这些值将在反向传播阶段进一步更新。在接下来的练习中,权重和偏置的初始化通过一个名为he_uniform的参数完成。一个核初始化器将作为参数添加到网络中。
密集层
神经网络中每一层的基本动态是层的权重与层输入的矩阵乘法(点积),并进一步加上偏置。这可以通过dot(X,W) + B方程表示,其中X是输入,W是权重或核,B是偏置。神经网络的这个操作是通过 Keras 中的密集层实现的,代码实现如下:
from tensorflow.keras.layers import Dense
Genmodel.add(Dense(hidden_layer,activation,\
kernel_initializer,input_dim))
Genmodel.add(Dense(hidden_layer,activation,kernel_initializer))
注意
上面的代码块仅用于解释代码是如何实现的。以当前形式运行时,可能无法得到理想的输出。目前,尝试完全理解语法;我们将在练习 7.02中实际应用这段代码,构建生成网络。
如你所见,我们在之前创建的Sequential()类(Genmodel)实例化时添加了一个密集层。定义一个密集层时需要给出的关键参数如下:
-
(hidden_layer):如你所知,隐藏层是神经网络中的中间层。隐藏层的节点数定义为第一个参数。 -
(activation):另一个参数是将要使用的激活函数类型。激活函数将在下一节中详细讨论。 -
(kernel_initializer):用于层的核初始化器的种类在密集层中定义。 -
(input_dim):对于网络的第一层,我们必须定义输入的维度(input_dim)。对于后续的层,这个值会根据每层的输出维度自动推导出来。
激活函数
如你所知,激活函数为神经元的输出引入了非线性。在神经网络中,激活函数会紧跟在密集层之后。密集层的输出就是激活函数的输入。接下来的练习中会使用不同的激活函数,具体如下:
-
ReLU:这代表修正线性单元。这种激活函数只输出正值,所有负值将输出为零。这是最广泛使用的激活函数之一。
-
ELU:这代表指数线性单元。它与 ReLU 非常相似,不同之处在于它也能输出负值。
-
线性:这是一种直线型激活函数。在这个函数中,激活值与输入成正比。
-
SELU:这代表缩放指数线性单元。这种激活函数是一种相对较少使用的函数。它启用了一个叫做内部归一化的理念,确保前一层的均值和方差保持不变。
-
Sigmoid:这是一种非常标准的激活函数。Sigmoid 函数将任何输入压缩到 0 和 1 之间。因此,Sigmoid 函数的输出也可以被视为概率分布,因为其值在 0 和 1 之间。
现在我们已经了解了一些网络的基本构建模块,接下来让我们在下一个练习中构建我们的生成网络。
在开始练习之前,让我们看看下一个练习在整体结构中所处的位置。在练习 7.01,从已知函数生成数据分布中,我们从已知的数学函数(即 sine() 函数)生成了数据分布。我们通过将第一个特征按等间距排列,然后使用第一个特征的 sine() 函数创建第二个特征,从而生成了整个分布。所以我们实际上控制了这个数据集的创建过程。这就是为什么这被称为真实数据分布,因为数据是从已知函数生成的。生成对抗网络(GAN)的最终目标是将随机噪声分布转化为看起来像真实数据分布的东西;即将一个随机分布转化为结构化的 sine() 分布。这将在以后的练习中实现。然而,作为第一步,我们将创建一个生成网络,用于生成随机噪声分布。这就是我们在下一个练习中要做的事情。
练习 7.02:构建生成网络
在这个练习中,我们将构建一个生成网络。生成网络的目的是从随机噪声数据生成虚假的数据分布。我们将通过生成随机数据点作为生成器网络的输入来实现这一点。然后,我们将逐层构建一个六层网络。最后,我们将预测网络的输出并绘制输出分布。这些数据分布将是我们的虚假分布。按照以下步骤完成此练习:
-
打开一个新的 Jupyter Notebook,并将其命名为
练习 7.02。导入以下库包:# Importing the library packages import tensorflow as tf import numpy as np from numpy.random import randn from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense from matplotlib import pyplot -
在这一步,我们定义网络的输入特征和输出特征的数量:
# Define the input features and output features infeats = 10 outfeats = 2我们将有 10 个特征作为输入,输出将是两个特征。输入特征
10是随意选择的。输出特征2是根据我们的真实数据集选择的,因为我们的真实数据集包含两个特征。 -
现在,我们将生成一批随机数。我们的批处理大小将为
128:# Generate a batch of random numbers batch = 128 genInput = randn(infeats * batch)我们可以选择任何批次大小。选择
128作为批次大小,以考虑到我们现有的计算资源。由于输入大小为 10,我们应生成 128 × 10 个随机数。此外,在前面的代码中,randn()是生成随机数的函数。在该函数内部,我们指定需要多少数据点,在我们的例子中是 (128 × 10)。 -
让我们使用以下代码将随机数据重新塑形为我们想要的输入格式:
# Reshape the data genInput = genInput.reshape(batch,infeats) print(genInput.shape)你应该会得到以下输出:
(128, 10) -
在此步骤中,我们将定义生成器。该网络将包含六层:
# Defining the Generator model Genmodel = Sequential() Genmodel.add(Dense(32,activation = 'linear',\ kernel_initializer='he_uniform',\ input_dim=infeats)) Genmodel.add(Dense(32,activation = 'relu',\ kernel_initializer='he_uniform')) Genmodel.add(Dense(64,activation = 'elu',\ kernel_initializer='he_uniform')) Genmodel.add(Dense(32,activation = 'elu',\ kernel_initializer='he_uniform')) Genmodel.add(Dense(32,activation = 'selu',\ kernel_initializer='he_uniform')) Genmodel.add(Dense(outfeats,activation = 'selu'))从网络中,我们可以看到,在第一层,我们定义了输入的维度,即 10;而在最后一层,我们定义了输出维度,即 2\。这基于我们在 步骤 4 中生成的输入数据维度(10)以及我们想要的输出特征,这与真实数据分布的特征数量相似。
-
我们可以使用
model.summary()函数调用查看该网络的概述:# Defining the summary of the network Genmodel.summary()你应该会得到以下输出:
![图 7.6:生成器网络概述]()
图 7.6:生成器网络概述
从总结中,你可以看到每一层输出的形状。例如,密集层的输出形状为 (批次大小,
32),因为第一隐藏层有32个神经元。形状层中的None表示示例的数量,在本例中指的是输入批次大小。第一层的 352 是参数的大小,包括权重和偏差。权重矩阵的大小为 (10 × 32),因为第一层的输入数量是 10,而下一层(隐藏层 1)有 32 个神经元。偏差的数量是 (32 × 1),这将等于第一层中隐藏层神经元的数量。因此,总共有 320 + 32 = 352 个参数。第二层则为 (32 × 32) + (32 × 1) = 1,056,以此类推,后续层也是如此。 -
现在我们已经定义了生成器网络,接下来让我们从网络中生成输出。我们可以使用
predict()函数来实现:# Generating fake samples from network fakeSamps = Genmodel.predict(genInput) fakeSamps.shape你应该会得到以下输出:
(128, 2)我们可以看到,生成器函数的输出会生成一个具有两个特征的样本,以及与给定批次大小相等的多个示例。
-
绘制分布图:
# Plotting the fake distribution from matplotlib import pyplot pyplot.scatter(fakeSamps[:,0],fakeSamps[:,1]) pyplot.xlabel('Feature 1 of the distribution') pyplot.ylabel('Feature 2 of the distribution') pyplot.show()你应该得到类似于以下的输出。请注意,建模将是随机的,因此你可能不会得到相同的输出:
![图 7.7:虚假数据分布图]()
图 7.7:虚假数据分布图
正如我们所看到的,已经生成了非常随机的数据。正如你将在接下来的练习中看到的,这些随机数据将被转换,以便看起来像真实的数据分布。
注意
要访问该部分的源代码,请参阅 packt.live/2W0FxyZ。
你也可以在packt.live/2WhZpOn上在线运行这个示例。你必须执行整个 Notebook 才能得到预期的结果。
在这个练习中,我们定义了一个包含六层的生成器网络,并从生成器网络中生成了第一个假样本。你可能在想我们是如何得到这六层的。那么,激活函数的选择呢?其实,网络架构是在大量实验之后得出的,目的是解决这个问题。找到合适的架构没有真正的捷径。我们必须通过实验不同的参数,如层数、激活函数类型等,来确定最优架构。
为判别器网络奠定基础
在之前的练习中,我们定义了生成器网络。现在,在定义判别器网络之前,是时候为此做一些准备了。看一下我们从生成器网络得到的输出,可以看到数据点是随机分布的。让我们退后一步,评估一下我们真正的方向。在我们介绍生成网络时,我们提到我们希望生成网络的输出与我们试图模仿的真实分布相似。换句话说,我们希望生成网络的输出看起来与真实分布的输出相似,如下图所示:

图 7.8:真实数据分布
我们可以看到,生成器网络当前生成的分布与我们希望模仿的分布相差甚远。你认为这是什么原因呢?嗯,原因很明显;我们还没有进行任何训练。你可能也注意到,网络中没有包含优化器函数。Keras 中的优化器函数是通过compile()函数定义的,如以下代码所示,其中我们定义了损失函数的类型以及我们希望采用的优化器:
model.compile(loss='binary_crossentropy',\
optimizer='adam',metrics=['accuracy'])
我们故意排除了compile()函数。稍后,当我们介绍 GAN 模型时,我们将使用compile()函数来优化生成器网络。所以,请耐心等待。现在,我们将继续进行过程的下一步,即定义判别器网络。
过程 3 – 判别器网络
在之前的过程中,我们介绍了生成网络,一个生成假样本的神经网络。鉴别器网络也是另一个神经网络,尽管它的功能与生成器网络不同。鉴别器的作用是识别给定样本是真实的还是假的。用一个类比来说,如果生成器网络是一个制造假币的骗子,那么鉴别器网络就是识别假币的超级警察。一旦被超级警察抓住,骗子将尝试完善他们的伎俩,制作更好的伪币,以便能欺骗超级警察。然而,超级警察也会接受大量训练,了解不同货币的细微差别,并努力完善识别假币的技能。我们可以看到,这两个主角始终处于对立的状态。这也是为什么这个网络被称为生成对抗网络的原因。
从前面的类比中获得启示,训练鉴别器类似于超级警察经过更多训练来识别假币。鉴别器网络就像你在机器学习中学到的任何二分类器。在训练过程中,鉴别器将提供两类样本,一类来自真实分布,另一类来自生成器分布。每一类样本都会有各自的标签。真实分布的标签是“1”,而假分布的标签是“0”。经过训练后,鉴别器必须正确地分类样本是来自真实分布还是假分布,这是一个典型的二分类问题。
实现鉴别器网络
鉴别器网络的核心结构与我们在前一部分实现的生成器网络相似。构建鉴别器网络的完整过程如下:
-
生成真实分布的批次。
-
使用生成器网络,生成假分布的批次。
-
用这两种分布的样本训练鉴别器网络。真实分布的标签是 1,而假分布的标签是 0。
-
评估鉴别器的性能。
在步骤 1 和 步骤 2 中,我们需要生成真实和假分布的批次。这就需要利用我们在练习 7.01中构建的真实分布,从已知函数生成数据分布,以及我们在练习 7.02中开发的生成器网络,构建生成网络。由于我们需要使用这两种分布,因此将它们打包成两种函数类型,以便高效地训练鉴别器网络会更方便。让我们来看看我们将要构建的两种函数类型。
生成真实样本的函数
这个用于生成真实样本的函数内容与我们在 练习 7.01 中开发的代码相同,从已知函数生成数据分布。唯一需要注意的补充内容是输入数据的标签。正如我们之前所述,真实样本的标签将是 1。因此,作为标签,我们将生成一个大小与批量大小相同的 1 的数组。numpy 中有一个实用函数可以用来生成一系列 1,叫做 np.ones((batch,1))。这将生成一个大小等于批量大小的 1 的数组。我们来回顾一下这个函数中的不同步骤:
-
生成均匀分布的数字,位于随机数的左右两侧。
-
将两组特征合并,得到一个与我们所需批量大小相等的序列。这是我们的第一个特征。
-
通过对我们在 步骤 2 中生成的第一个特征应用
sine()函数来生成第二个特征。 -
将两个特征的形状调整为
(batch,1),然后沿列连接它们。这将得到一个形状为(batch,2)的数组。 -
使用
np.ones((batch,1))函数生成标签。标签数组的维度将是(batch,1)。
我们将提供给函数的参数是随机数和批量大小。在 步骤 1 中需要注意一个细微的变化,由于我们希望生成一个长度等于批量大小的序列,我们将取与批量大小的一半(batch size /2)等长的均匀分布的数字。这样,当我们将左右两边的序列组合起来时,我们就得到了一个等于所需批量大小的序列。
生成假样本的函数
生成假样本的函数将与我们在 练习 7.02 中开发的函数相同,构建生成对抗网络。不过,我们将把它分成三个独立的函数。将 练习 7.02 中的代码分成三个独立的函数是为了在训练过程中提高便利性和效率。我们来看看这三个函数:
-
randn()函数。输出将是一个形状为 (batch,input features) 的数组。该函数的参数是batch size和input feature size。 -
输入特征大小和输出特征大小。 -
使用
numpy生成 0,即np.zeros((batch,1))。
我们来看看这三个函数的完整过程:
-
使用 函数 1 生成假输入。
-
使用生成器模型函数(函数 2)来预测假输出。
-
使用
np.zeros()函数生成标签,即一系列 0。这是 函数 3 的一部分。
第三个函数的参数是 生成器模型、批量大小 和 输入特征大小。
构建判别器网络
判别器网络将按照与生成器网络相同的方式构建;也就是说,它将使用Sequential()类、全连接层、激活函数和初始化函数来创建。唯一值得注意的例外是,我们还将在compile()函数中添加优化层。在优化层中,我们将定义损失函数,这里将使用binary_crossentropy,因为判别器网络是一个二分类网络。对于优化器,我们将使用adam optimizer,因为它非常高效且是一个非常流行的选择。
训练判别器网络
现在我们已经了解了实现判别器网络的所有组件,接下来让我们看看训练判别器网络的步骤:
-
生成一个随机数,然后使用生成真实样本的函数生成一批真实样本及其标签。
-
使用第三个函数生成虚假样本及其标签,该函数将使用前面定义的其他函数来生成虚假样本。
-
使用
train_on_batch()函数,用一批真实样本和虚假样本来训练判别器模型。 -
步骤1到3会针对我们希望训练运行的轮数重复执行。这是通过一个
for循环来实现的,循环的次数是训练的轮数。 -
在每个中间步骤中,我们使用
evaluate()函数计算模型在虚假样本和真实样本上的准确性,并打印出模型的准确度。
现在我们已经了解了实现判别器网络的步骤,接下来我们将在下一个练习中实现它。
练习 7.03:实现判别器网络
在本次练习中,我们将构建判别器网络,并在真实样本和虚假样本上训练该网络。请按照以下步骤完成此练习:
-
打开一个新的 Jupyter Notebook,并命名为
Exercise 7.03。导入以下库包:# Import the required library functions import tensorflow as tf import numpy as np from numpy.random import randn from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense from matplotlib import pyplot -
让我们定义一个函数,生成我们真实数据分布的特征。此函数的返回值将是真实数据集及其标签:
Exercise7.03.ipynb # Function to generate real samples def realData(loc,batch): """ loc is the random location or mean around which samples are centred """ """ Generate numbers to right of the random point """ xr = np.arange(loc,loc+(0.1*batch/2),0.1) xr = xr[0:int(batch/2)] """ Generate numbers to left of the random point """ xl = np.arange(loc-(0.1*batch/2),loc,0.1) The complete code for this step can be found at https://packt.live/3fe02j3.我们在这里定义的函数包含了在练习 7.01中用于生成
sine()波形数据集的代码,练习 7.01是从已知函数生成数据分布部分的内容。这个函数的输入是随机数和批次大小。一旦提供了随机数,系列将按照我们在练习 7.01中遵循的相同过程生成。我们还会为真实数据分布生成标签,这些标签为 1。最终的返回值将是两个特征和标签。 -
让我们定义一个名为
fakeInputs的函数,用于为生成器函数生成输入(这是函数 1,我们在生成虚假样本的函数部分中进行了说明):# Function to generate inputs for generator function def fakeInputs(batch,infeats): """ Sample data points equal to (batch x input feature size) from a random distribution """ genInput = randn(infeats * batch) # Reshape the input X = genInput.reshape(batch ,infeats) return X在这个函数中,我们正在生成我们需要的格式的随机数
([batch size , input features])。这个函数生成的是从随机分布中采样的伪数据作为返回值。 -
接下来,我们将定义一个函数,该函数将返回一个生成器模型:
# Function for the generator model def genModel(infeats,outfeats): #Defining the Generator model Genmodel = Sequential() Genmodel.add(Dense(32,activation = 'linear',\ kernel_initializer='he_uniform',\ input_dim=infeats)) Genmodel.add(Dense(32,activation = 'relu',\ kernel_initializer='he_uniform')) Genmodel.add(Dense(64,activation = 'elu',\ kernel_initializer='he_uniform')) Genmodel.add(Dense(32,activation = 'elu',\ kernel_initializer='he_uniform')) Genmodel.add(Dense(32,activation = 'selu',\ kernel_initializer='he_uniform')) Genmodel.add(Dense(outfeats,activation = 'selu')) return Genmodel这与我们在 练习 7.02 中实现的模型相同,构建生成对抗网络。该函数的返回值将是生成器模型。
-
以下函数将用于使用生成器模型创建伪样本:
# Function to create fake samples using the generator model def fakedataGenerator(Genmodel,batch,infeats): # first generate the inputs to the model genInputs = fakeInputs(batch,infeats) """ use these inputs inside the generator model to generate fake distribution """ X_fake = Genmodel.predict(genInputs) # Generate the labels of fake data set y_fake = np.zeros((batch,1)) return X_fake,y_fake在上述代码中,我们正在实现 函数 3,该函数在 生成伪样本的函数 部分中已经介绍过。如你所见,我们调用了在 步骤 4 中定义的生成器模型作为输入,同时包括了批次大小和输入特征。这个函数的返回值是生成的伪数据及其标签(
0)。 -
现在,让我们定义在刚刚创建的函数中将使用的参数:
""" Define the arguments like batch size,input feature size and output feature size """ batch = 128 infeats = 10 outfeats = 2 -
让我们使用以下代码构建判别器模型:
# Define the discriminator model Discmodel = Sequential() Discmodel.add(Dense(16, activation='relu',\ kernel_initializer = 'he_uniform',\ input_dim=outfeats)) Discmodel.add(Dense(16,activation='relu' ,\ kernel_initializer = 'he_uniform')) Discmodel.add(Dense(16,activation='relu' ,\ kernel_initializer = 'he_uniform')) Discmodel.add(Dense(1,activation='sigmoid')) # Compiling the model Discmodel.compile(loss='binary_crossentropy',\ optimizer='adam', metrics=['accuracy'])判别模型的构建方式类似于我们在生成器网络中所做的。请注意,最后一层的激活函数将是 Sigmoid,因为我们需要一个关于输出是实际网络还是伪网络的概率。
-
打印判别网络的摘要:
# Print the summary of the discriminator model Discmodel.summary()你应该获得以下输出:
![图 7.9:模型摘要]()
图 7.9:模型摘要
从摘要中,我们可以看到基于我们定义的架构,网络的大小。我们可以看到前面三层全连接层每层有 16 个神经元,这是我们在 步骤 7 中构建判别网络时定义的。最后一层只有一个输出,因为这是一个 Sigmoid 层。它输出的是数据分布是否真实的概率(
1表示真实,0表示伪造)。 -
调用将在训练过程中使用的生成器模型函数:
# Calling the Generator model function Genmodel = genModel(infeats,outfeats) Genmodel.summary()你应该获得以下输出:
![图 7.10:模型摘要]()
图 7.10:模型摘要
你会注意到,架构与我们在 练习 7.02 中开发的完全相同,构建生成对抗网络。
-
现在,我们需要定义训练网络的周期数,如下所示:
# Defining the number of epochs nEpochs = 20000 -
现在,让我们开始训练判别网络:
Exercise7.03.ipynb
# Train the discriminator network
for i in range(nEpochs):
# Generate the random number for generating real samples
loc = np.random.normal(3,1,1)
"""
Generate samples equal to the bath size
from the real distribution
"""
x_real, y_real = realData(loc,batch)
#Generate fake samples using the fake data generator function
x_fake, y_fake = fakedataGenerator(Genmodel,batch,infeats)
The complete code for this step can be found at https://packt.live/3fe02j3.
在这里,我们对真实数据和伪数据的训练进行 20,000 次周期的迭代。周期数是通过一定的实验得出的。我们应该尝试不同的周期值,直到得到较好的准确度。每经过 4,000 次周期,我们打印模型在真实数据集和伪数据集上的准确度。打印频率是任意的,基于你希望看到的图表数量,以检查训练进度。训练后,你将看到判别器达到非常好的准确率。
你应该获得类似于以下的输出:
Real accuracy:0.265625,Fake accuracy:0.59375
Real accuracy:1.0,Fake accuracy:0.828125
Real accuracy:1.0,Fake accuracy:0.90625
Real accuracy:1.0,Fake accuracy:0.9453125
Real accuracy:1.0,Fake accuracy:0.9453125
注意
由于我们在这里使用的是随机值,因此你得到的输出可能与这里看到的不同,每次运行结果也会有所不同。
从准确度水平来看,我们可以看到判别器最初在识别真实数据集时非常优秀(准确度 = 1),而对假数据集的准确度较低。经过大约 4,000 个周期后,我们看到判别器已经能够很好地区分假数据集和真实数据集,因为它们的准确度都接近 1.0。
注意
要访问这个特定部分的源代码,请参考packt.live/3fe02j3。
你也可以在线运行这个示例,访问packt.live/2ZYiYMG。你必须执行整个 Notebook 才能获得预期的结果。
在这个练习中,我们定义了不同的辅助函数,并构建了判别器函数。最后,我们在真实数据和假数据上训练了判别器模型。在训练过程结束时,我们看到判别器能够非常好地区分真实数据集和假数据集。训练完判别器网络后,接下来就是构建 GAN 的高潮部分。
过程 4 – 实现 GAN
我们终于到了我们一直在等待的时刻。在之前的三个过程中,我们逐步构建了 GAN 的所有构建模块,如假数据生成器、真实数据生成器、生成器网络和判别器网络。GAN 实际上是这些构建模块的整合。GAN 中的真正难题是我们如何将这些组件互相整合。我们现在就来处理这个问题。
整合所有构建模块
在构建鉴别器网络时,我们生成了真实样本和假样本,并在训练过程中将它们输入到鉴别器中。训练过程使得鉴别器变得“聪明”,从而能够正确地识别什么是假的,什么是真的。从概率的角度来看,这意味着当鉴别器接收到假样本时,它会预测接近“0”的概率,而当样本是真实的时,它会预测接近“1”的概率。然而,使鉴别器变聪明并不是我们的最终目标。我们的最终目标是让生成器模型变聪明,使它开始生成看起来像真实样本的例子,并在此过程中欺骗鉴别器。这可以通过训练生成器并更新其参数(即权重和偏置)来实现,使它能够生成看起来像真实样本的样本。然而,仍然存在一个问题,因为在生成器网络中,我们没有包括优化器步骤,因此生成器网络本身无法进行训练。解决这个问题的方法是构建另一个网络(我们称之为Ganmodel),它将生成器和鉴别器按顺序连接起来,然后在新网络中包含一个优化器函数,使其在反向传播时更新其组成部分的参数。用伪代码表示,这个网络大致如下:
Ganmodel = Sequential()
# First adding the generator model
Ganmodel.add(Genmodel)
"""
Next adding the discriminator model
without training the parameters
"""
Ganmodel.add(Discmodel)
# Compile the model for loss to optimise the Generator model
Ganmodel.compile(loss='binary_crossentropy',optimizer = 'adam')
在这个模型中,生成器模型将生成假样本并输入到鉴别器模型中,鉴别器模型将根据样本生成一个概率,判断该样本是假的还是现实的。根据样本的标签,它会有一定的损失,这个损失会通过鉴别器传播到生成器,更新两个模型的参数。换句话说,基于损失,反向传播算法将根据损失对每个参数的梯度来更新该参数。因此,这将解决我们没有为生成器定义优化器函数的问题。
然而,这个网络还有一个问题。我们的鉴别器网络已经训练过,并且在我们单独训练鉴别器网络时已经变得非常聪明。我们不希望在这个新网络中再次训练鉴别器模型并让它变得更聪明。这个问题可以通过定义我们不希望训练鉴别器参数来解决。经过这个新修改,Ganmodel将如下所示:
# First define that discriminator model cannot be trained
Discmodel.trainable = False
Ganmodel = Sequential()
# First adding the generator model
Ganmodel.add(Genmodel)
"""
Next adding the discriminator model
without training the parameters
"""
Ganmodel.add(Discmodel)
# Compile the model for loss to optimise the Generator model
Ganmodel.compile(loss='binary_crossentropy',optimizer = 'adam')
通过设置 Discmodel.trainable = False,我们告诉网络在反向传播时不更新鉴别器网络的参数。因此,鉴别器网络将在反向传播阶段充当传递错误的中介,传递给生成器网络。
如果你认为我们所有的问题都已经解决,那你可能会大吃一惊。我们知道,当判别器模型遇到假分布时,它会将概率预测为接近0的值。我们也知道,假数据集的标签也是0。所以,从损失的角度来看,传播回生成器的损失会非常小。由于损失非常小,随后的生成器模型参数更新也会非常微小。这将无法使生成器生成类似真实样本的样本。生成器只有在生成了较大损失并将其传播到生成器时,才能学习,从而使得其参数朝着真实参数的方向更新。那么,如何使损失变大呢?如果我们不将假样本的标签定义为0,而是将其定义为1,会怎么样呢?如果我们这么做,判别器模型像往常一样会预测假样本的概率接近 0。然而,现在我们有了一个情况,即损失函数会变大,因为标签是 1。当这个大的损失函数被传播回生成器网络时,参数会显著更新,这将使生成器变得更聪明。随后,生成器将开始生成更像真实样本的样本,这也就达到了我们的目标。
这个概念可以通过下图来解释。在这里,我们可以看到,在训练的初始阶段,假数据的概率接近零(0.01),而我们为假数据指定的标签是1。这将确保我们获得一个较大的损失,这个损失会反向传播到生成器网络:

图 7.11:GAN 过程
现在我们已经看过了 GAN 模型的动态,接下来我们将把所有部分结合起来,定义我们将遵循的过程,以构建 GAN。
构建 GAN 的过程
GAN 的完整过程就是将我们构建的所有部分结合成一个合乎逻辑的顺序。我们将使用我们在定义判别器函数时构建的所有函数。此外,我们还将创建新的函数,例如用于判别器网络的函数和用于 GAN 模型的另一个函数。所有这些函数将在特定的时刻被调用,以构建 GAN 模型。端到端的过程将如下所示:
-
定义生成真实数据分布的函数。这个函数与我们在练习 7.03中为判别器网络开发的判别器网络实现函数相同。
-
定义为生成假样本创建的三个函数。这些函数包括用于生成假输入的函数、用于生成器网络的函数,以及用于生成假样本和标签的函数。这些函数与我们在练习 7.03中为判别器网络开发的判别器网络实现函数相同。
-
创建一个新的判别器网络函数,就像我们在Exercise 7.03中创建的判别器网络的实现一样。此函数将以输出特征(2)作为输入,因为真实数据集和假数据集都有两个特征。此函数将返回判别器模型。
-
按照我们在上一节中开发的伪代码,创建一个新的 GAN 模型函数(过程 4 – 构建 GAN)。此函数将以生成器模型和判别器模型作为输入。
-
启动训练过程。
训练过程
这里的训练过程类似于我们在Exercise 7.03中实现的判别器网络的实现,用于判别器网络的训练过程。训练过程的步骤如下:
-
生成一个随机数,然后使用生成真实样本的函数生成一批真实样本及其标签。
-
使用我们描述的关于生成假样本的第三个函数,生成一批假样本及其标签。第三个函数将使用其他函数来生成假样本。
-
使用
train_on_batch()函数训练判别器模型,使用真实样本和假样本的批次。 -
生成另一批假输入来训练 GAN 模型。这些假样本是通过假样本生成过程中的函数 1生成的。
-
为假样本生成标签,目的是欺骗判别器。这些标签将是 1,而不是 0。
-
使用
train_on_batch()函数训练 GAN 模型,使用假样本及其标签,如步骤 4和5中所述。 -
步骤 1 到 6 会根据我们希望训练运行的周期数重复。这是通过在周期数上使用
for循环完成的。 -
在每个中间步骤中,我们使用
evaluate()函数计算模型在假样本和真实样本上的准确性。模型的准确度也会打印出来。 -
我们还会在某些时期生成输出图。
现在我们已经看到了训练 GAN 的完整过程,让我们深入到Exercise 7.04,实现 GAN,并实现这一过程。
练习 7.04:实现 GAN
在本练习中,我们将通过实现我们在上一节讨论的过程来构建和训练 GAN。按照以下步骤完成此练习:
-
打开一个新的 Jupyter Notebook 并命名为
Exercise 7.04。导入以下库:# Import the required library functions import tensorflow as tf import numpy as np from numpy.random import randn from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense from matplotlib import pyplot -
让我们创建一个函数来生成真实样本:
Exercise7.04.ipynb # Function to generate real samples def realData(loc,batch): """ loc is the random location or mean around which samples are centred """ # Generate numbers to right of the random point xr = np.arange(loc,loc+(0.1*batch/2),0.1) xr = xr[0:int(batch/2)] # Generate numbers to left of the random point xl = np.arange(loc-(0.1*batch/2),loc,0.1) The complete code for this step can be found on https://packt.live/3iIJHVS我们在这里创建的函数遵循与我们在Exercise 7.01中实现的从已知函数生成数据分布相同的过程。此函数的输入是随机数和批次大小。我们从此函数中获得包含我们特征的真实数据分布,以及真实数据分布的标签作为返回值。此函数的返回值是实际数据集及其标签。
-
在这里,让我们定义一个生成输入给生成器网络的函数:
# Function to generate inputs for generator function def fakeInputs(batch,infeats): """ Sample data points equal to (batch x input feature size) from a random distribution """ genInput = randn(infeats * batch) # Reshape the input X = genInput.reshape(batch ,infeats) return X该函数生成从随机分布采样的虚假数据作为输出。
-
现在,让我们继续定义构建生成器网络的函数:
# Function for the generator model def genModel(infeats,outfeats): # Defining the Generator model Genmodel = Sequential() Genmodel.add(Dense(32,activation = 'linear',\ kernel_initializer='he_uniform',\ input_dim=infeats)) Genmodel.add(Dense(32,activation = 'relu',\ kernel_initializer='he_uniform')) Genmodel.add(Dense(64,activation = 'elu',\ kernel_initializer='he_uniform')) Genmodel.add(Dense(32,activation = 'elu',\ kernel_initializer='he_uniform')) Genmodel.add(Dense(32,activation = 'selu',\ kernel_initializer='he_uniform')) Genmodel.add(Dense(outfeats,activation = 'selu')) return Genmodel这是我们在练习 7.02,构建生成网络中构建的相同函数。此函数返回生成器模型。
-
在此步骤中,我们将定义一个函数,使用生成器网络创建虚假样本:
# Function to create fake samples using the generator model def fakedataGenerator(Genmodel,batch,infeats): # first generate the inputs to the model genInputs = fakeInputs(batch,infeats) """ use these inputs inside the generator model to generate fake distribution """ X_fake = Genmodel.predict(genInputs) # Generate the labels of fake data set y_fake = np.zeros((batch,1)) return X_fake,y_fake我们在这里定义的函数将随机数据分布作为输入(传递给我们在前一步定义的生成器网络),并生成虚假分布。虚假分布的标签是 0,它也在函数内部生成。换句话说,来自此函数的输出是虚假数据集及其标签。
-
现在,让我们定义将在不同函数中使用的参数:
""" Define the arguments like batch size,input feature size and output feature size """ batch = 128 infeats = 10 outfeats = 2 -
接下来,让我们将鉴别器模型构建为一个函数:
# Discriminator model as a function def discModel(outfeats): Discmodel = Sequential() Discmodel.add(Dense(16, activation='relu',\ kernel_initializer = 'he_uniform',\ input_dim=outfeats)) Discmodel.add(Dense(16,activation='relu' ,\ kernel_initializer = 'he_uniform')) Discmodel.add(Dense(16,activation='relu' ,\ kernel_initializer = 'he_uniform')) Discmodel.add(Dense(1,activation='sigmoid')) # Compiling the model Discmodel.compile(loss='binary_crossentropy',\ optimizer='adam',metrics=['accuracy']) return Discmodel网络架构将类似于我们在练习 7.03,实现鉴别器网络中开发的架构。此函数将返回鉴别器。
-
打印鉴别器网络的摘要:
# Print the summary of the discriminator model Discmodel = discModel(outfeats) Discmodel.summary()你应该得到以下输出:
![图 7.12:鉴别器模型摘要]()
图 7.12:鉴别器模型摘要
该输出与我们在练习 7.03,实现鉴别器网络中实现的网络所收到的输出相同,我们在其中定义了鉴别器函数。
-
调用生成器模型函数以供训练过程使用:
# Calling the Generator model function Genmodel = genModel(infeats,outfeats) Genmodel.summary()你应该得到以下输出:
![图 7.13:生成器模型摘要]()
图 7.13:生成器模型摘要
你会注意到,架构与我们在练习 7.02,构建生成网络中开发的相同。
-
在开始训练之前,让我们可视化虚假数据分布。为此,我们使用
fakedataGenerator()函数生成虚假数据集,然后使用pyplot可视化它:# Let us visualize the initial fake data x_fake, _ = fakedataGenerator(Genmodel,batch,infeats) # Plotting the fake data using pyplot pyplot.scatter(x_fake[:, 0], x_fake[:, 1], color='blue') # Adding x and y labels pyplot.xlabel('Feature 1 of the distribution') pyplot.ylabel('Feature 2 of the distribution') pyplot.show()你应该得到类似于以下的输出。请注意,数据生成本质上是随机的,因此你可能不会得到相同的图:
![图 7.14:来自虚假输入分布的图]()
图 7.14:来自虚假输入分布的图
从前面的图表中可以看到,数据分布相当随机。我们需要将这些随机数据转换为类似于正弦波的形式,这才是我们的真实数据分布。
-
现在,让我们将 GAN 模型定义为一个函数。该函数类似于我们在过程 4中开发的伪代码,在那里我们定义了 GAN。GAN 是生成器模型和鉴别器模型的包装模型。请注意,我们在此函数中将鉴别器模型定义为不可训练:
""" Define the combined generator and discriminator model, for updating the generator """ def ganModel(Genmodel,Discmodel): # First define that discriminator model cannot be trained Discmodel.trainable = False Ganmodel = Sequential() # First adding the generator model Ganmodel.add(Genmodel) """ Next adding the discriminator model without training the parameters """ Ganmodel.add(Discmodel) # Compile the model for loss to optimise the Generator model Ganmodel.compile(loss='binary_crossentropy',optimizer = 'adam') return Ganmodel该函数将返回 GAN 模型。
-
现在,让我们调用 GAN 函数。请注意,GAN 模型的输入是先前定义的生成器模型和判别器模型:
# Initialise the gan model gan_model = ganModel(Genmodel,Discmodel) -
打印 GAN 模型的总结:
# Print summary of the GAN model gan_model.summary()你应该得到以下输出:
![图 7.15:GAN 模型的总结]()
图 7.15:GAN 模型的总结
请注意,GAN 模型中每一层的参数等同于生成器和判别器模型的参数。GAN 模型仅仅是对这两个模型的包装。
-
让我们定义训练网络的轮数(epochs):
# Defining the number of epochs nEpochs = 20000 -
现在,我们开始训练网络的过程:
Exercise7.04.ipynb
# Train the GAN network
for i in range(nEpochs):
# Generate the random number for generating real samples
loc = np.random.normal(3,1,1)
"""
Generate samples equal to the bath size
from the real distribution
"""
x_real, y_real = realData(loc,batch)
#Generate fake samples using the fake data generator function
x_fake, y_fake = fakedataGenerator(Genmodel,batch,infeats)
# train the discriminator on the real samples
Discmodel.train_on_batch(x_real, y_real)
# train the discriminator on the fake samples
Discmodel.train_on_batch(x_fake, y_fake)
The complete code for this step can be found at https://packt.live/3iIJHVS
需要注意的是,判别器模型与假样本和真实样本的训练,以及 GAN 模型的训练是同时进行的。唯一的区别是,训练 GAN 模型时不会更新判别器模型的参数。另一个需要注意的是,在 GAN 内部,假样本的标签将为 1。这是为了生成大的损失项,这些损失项会通过判别器网络进行反向传播,从而更新生成器的参数。
注意:
请注意,底部倒数第三行代码(filename = 'GAN_Training_Plot%03d.png' % (i))每隔 2,000 个轮次保存一次图表。图表将保存在与你的 Jupyter Notebook 文件位于同一文件夹中。你也可以指定保存图表的路径。可以通过以下方式完成:
filename = 'D:/Project/GAN_Training_Plot%03d.png' % (i)
你可以通过packt.live/2W1FjaI访问通过本练习生成的图表。
你应该得到类似以下所示的输出。由于预测是随机的(也就是说,它们是随机的),你可能无法得到与本示例中相同的图表。你的值可能会有所不同;然而,它们会与这里显示的结果相似:
Real accuracy:0.2421875,Fake accuracy:0.0234375
Real accuracy:0.625,Fake accuracy:0.609375
Real accuracy:0.6484375,Fake accuracy:0.9609375
Real accuracy:0.84375,Fake accuracy:0.734375
Real accuracy:0.3671875,Fake accuracy:0.734375
Real accuracy:0.53125,Fake accuracy:0.703125
Real accuracy:0.578125,Fake accuracy:0.640625
Real accuracy:0.640625,Fake accuracy:0.8203125
Real accuracy:0.515625,Fake accuracy:0.7109375
Real accuracy:0.5625,Fake accuracy:0.859375
从前面的输出可以看出,真实数据集的准确率逐渐下降,而假数据集的准确率则在上升。在理想情况下,判别器网络的准确率应该在 0.5 左右,这意味着判别器对于一个样本是假的还是现实的感到非常困惑。现在,让我们看看在不同轮次下生成的一些图表,了解数据点如何收敛并趋近于真实的函数。以下是输入到生成对抗网络(GAN)之前的随机数据点的分布图(步骤 10):

图 7.16:来自假输入分布的图表
注意数据的分布,数据点大多集中在均值为 0 的地方。这是因为这些随机点是从均值为 0、标准差为 1 的正态分布中生成的。现在,使用这些原始数据,让我们研究随着生成器训练,假数据集的变化过程。
注意
要访问此部分的源代码,请参考packt.live/3iIJHVS。
你也可以在线运行这个例子,网址是packt.live/3gF5DPW。你必须执行整个 Notebook 才能获得期望的结果。
以下三个图展示了虚假数据分布与真实数据分布的进展情况。x轴表示特征 1,而y轴表示特征 2。在图中,红色点表示真实数据分布的数据,蓝色点表示虚假数据分布的数据。从以下图中可以看到,在2000代时,虚假数据的分布已经进入了该范围,但仍未与真实数据分布的形态对齐。

图 7.17:在2000代时,虚假数据分布与真实数据分布的对比图
到了10000代时,即生成器已经训练到一半左右,数据已经趋向与真实数据分布的汇聚:

图 7.18:在10000代时,虚假数据分布与真实数据分布的对比图
到了18000代时,我们可以看到大多数数据点已经与真实数据分布对齐,这表明 GAN 模型的训练效果相当不错。

图 7.19:在18000代时,虚假数据分布与真实数据分布的对比图
然而,我们可以看到在x = 4之后的数据点比左边的更多噪声。造成这种情况的原因之一可能是我们在训练GAN(第 10 步)之前生成的随机数据分布,数据主要集中在-2和4之间。这些数据与目标分布(正弦波)在相同范围内对齐,并且在x = 4右侧目标分布的对齐稍显不稳定。然而,你还应注意,完全对齐目标分布是一个极其困难的任务,涉及到不同模型架构的实验以及更多的实验。我们鼓励你在架构的不同组件中进行实验和创新,以使分布更好地对齐。
注意
我们在上述实验中获得的结果每次运行代码时都会有所不同。
这标志着我们逐步构建 GAN 的完整过程的结束。通过一系列的实验,我们了解了 GAN 的定义、组成部分,以及它们如何协同工作来训练一个 GAN。接下来,我们将运用所学的知识,利用不同的数据集开发更先进的 GAN。
深度卷积 GAN
在前面的章节中,我们实现了一个基于多层感知器(MLP)的生成对抗网络(GAN)。如你在前几章中所学,MLP 具有全连接层,这意味着每一层的所有神经元都与下一层的所有神经元相连接。因此,MLP 也被称为全连接层。我们在前一节开发的 GAN 也可以称为全连接生成对抗网络(FCGAN)。在本节中,我们将学习另一种架构,称为深度卷积生成对抗网络(DCGANs)。顾名思义,这种架构基于你在第四章《文本深度学习——嵌入》中学到的卷积神经网络(CNN)。让我们重新回顾一下 DCGAN 的一些基本构建块。
DCGAN 的构建块
DCGAN 的大多数构建块与在第三章《卷积神经网络的图像分类》中引入 CNN 时学到的类似。让我们回顾一下其中的一些重要内容。
卷积层
如你在第三章《卷积神经网络的图像分类》中所学,卷积操作涉及过滤器或卷积核在输入图像上移动,以生成一组特征图。在 Keras 中,卷积层可以通过以下代码实现:
from tensorflow.keras import Sequential
model = Sequential()
model.add(Conv2D(64, kernel_size=(5, 5),\
strides=(2,2), padding='same'))
注意
上面的代码块仅用于解释代码是如何实现的。以当前的形式运行时,可能不会得到理想的输出。目前,尽量完全理解语法,我们很快将把这段代码付诸实践。
在前面的代码中,Sequential()类是从tensorflow.keras模块中导入的。然后,在第二行代码中将其实例化为一个变量 model。通过定义过滤器的数量、核的大小、所需的步幅和填充指示器,将卷积层添加到Sequential()类中。在前面的代码行中,64 表示特征图的数量。kernel_size值为(5,5)表示将用于卷积输入图像并生成特征图的过滤器的大小。strides值为(2,2)表示在生成特征图的过程中,过滤器每次水平和垂直地移动两个单元格。padding = 'same'表示我们希望卷积操作的输出与输入具有相同的大小。
注意:
选择使用的架构,例如过滤器的数量、卷积核的大小、步幅等,是一种艺术形式,可以通过大量的实验在特定领域中掌握。
激活函数
在前一部分中,我们实现了一些激活函数,如 ReLU、ELU、SELU 和线性激活函数。在本节中,我们将介绍另一种叫做 LeakyReLU 的激活函数。LeakyReLU 是 ReLU 的另一种变体。与 ReLU 不允许任何负值不同,LeakyReLU 允许一个由因子α控制的小的非零梯度。这个因子α控制负值的梯度斜率。
上采样操作
在 CNN 中,图像通过最大池化和卷积操作等方式被下采样到更低的维度。然而,在 GAN 中,生成器网络的动态运作方向与卷积操作相反;也就是说,从较低或较粗的维度开始,我们需要将图像转换为更密集的形式(即,更多的维度)。一种实现方法是通过一种叫做UpSampling的操作。在此操作中,输入维度被加倍。让我们通过一个小例子来更详细地了解这个操作。
以下代码可用于导入所需的库文件。专门用于UpSampling的函数是keras.layers中的UpSampling2D:
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import UpSampling2D
以下代码创建了一个简单的模型,在UpSampling层中接受形状为(3,3,1)的数组作为输入:
# A model for UpSampling2d
model = Sequential()
model.add(UpSampling2D(input_shape=(3,3,1)))
model.summary()
输出将如下所示:

图 7.20:UpSampling2D 模型摘要
从摘要中,我们可以看到输出已经被加倍至(None, 6,6,1),其中中间的两个维度被加倍。为了理解这一变化如何影响形状为(3,3,1)的数组,我们需要定义一个(3,3)大小的数组,如下所示:
# Defining an array of shape (3,3)
import numpy as np
X = np.array([[1,2,3],[4,5,6],[7,8,9]])
X.shape
输出将如下所示:
(3, 3)
我们定义的数组只有两个维度。然而,模型输入需要四个维度,维度的顺序是(examples, width, height, channels)。我们可以使用reshape()函数来创建额外的维度,如下所示:
# Reshaping the array
X = X.reshape((1,3,3,1))
X.shape
输出将如下所示:
(1, 3, 3, 1)
我们可以使用以下代码通过我们创建的UpSampling模型进行一些预测,并观察结果数组的维度:
# Predicting with the model
y = model.predict(X)
# Printing the output shape
y[0,:,:,0]
输出将如下所示:

图 7.21:未采样模型的输出形状
从前面的输出中,我们可以看到结果数组是如何被转换的。正如我们所见,每个输入都被加倍以得到结果数组。我们将在练习 7.05中使用UpSampling方法,实现 DCGAN。
转置卷积
转置卷积与我们刚才看到的UpSampling方法不同。UpSampling更多的是输入值的简单翻倍。然而,转置卷积在训练阶段学习到的权重。转置卷积的工作方式类似于卷积操作,但方向相反。转置卷积通过核大小和步幅的组合来扩展输入的维度,而不是减小维度。正如第三章《卷积神经网络的图像处理》中所学,步幅是我们卷积或移动过滤器以获得输出时的步长。我们还可以通过padding = 'same'参数来控制转置卷积的输出,就像在卷积操作中一样。
让我们看看一个代码示例,了解转置卷积是如何工作的。
首先,我们需要导入必要的库文件。与转置卷积操作相关的函数是keras.layers中的Conv2DTranspose:
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv2DTranspose
现在,我们可以创建一个简单的模型,在转置卷积层中处理形状为(3,3,1)的图像:
# A model for transpose convolution
model = Sequential()
model.add(Conv2DTranspose(1,(4,4),(2,2),\
input_shape=(3,3,1),padding='same'))
model.summary()
在转置卷积层中,第一个参数(1)是滤波器的数量。第二个参数(4,4)是核的大小,最后一个参数(2,2)是步幅。使用padding = 'same'时,输出将不依赖于核的大小,而是步幅和输入维度的倍数。前面的代码生成的摘要如下:

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_07_22.jpg)
图 7.22:模型摘要
从摘要中,我们可以看到输出已经翻倍为(None, 6,6,1),这就像是将步幅与输入维度相乘一样(None, 2 × 3, 2 × 3, 1)。
现在,让我们看看一个形状为(1,3,3,1)的实际数组发生了什么变化。记住,我们之前也创建过这个数组:
# Defining an array of shape (3,3)
X = np.array([[1,2,3],[4,5,6],[7,8,9]])
X = X.reshape((1,3,3,1))
X.shape
输出结果如下:
(1, 3, 3, 1)
为了生成转置数组,我们需要使用我们创建的转置卷积模型进行一些预测。通过打印形状,我们还可以观察到结果数组的维度:
# Predicting with the model
y = model.predict(X)
# Printing the shape
print(y.shape)
# Printing the output shape
y[0,:,:,0]
输出结果如下:

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/dl-ws/img/B15385_07_22.jpg)
图 7.23:变换后的数组
注意
你得到的输出可能与我们上面展示的不同。
从前面的输出中,我们可以看到生成的数组是如何被转化的。生成数组中的值是内核权重与输入图像之间动态关系的最终结果。
现在我们已经看过一些 DCGAN 的基本构建块,接下来我们将在下一个练习中构建它。
使用 DCGAN 生成手写图像
现在,我们将尝试使用 DCGAN 生成一个类似于手写数字数据的数据分布。我们将使用 MNIST 手写数字数据集作为真实数据集。这个数据集包含 60,000 个训练样本,所有样本都是从 0 到 9 的手写数字图像。这个 GAN 的实现过程将与练习 7.04,实现 GAN中的过程相似,我们在其中实现了已知函数的 GAN。让我们来看一下我们将遵循的步骤来解决这个问题。
首先,我们需要定义一个用于生成真实数据分布的函数:
# Get the MNIST data
(X_train, _), (_, _) = mnist.load_data()
上述函数将从 MNIST 数据集生成真实数据分布。训练集和测试集可以通过 mnist.load_data() 函数生成。使用该函数,我们将获得所有相关数据集,格式为 (X_train,y_train),(X_test,y_test)。由于我们只需要 X_train 数据,因此我们不会将其他数据集存储在变量中。
MNIST 数据是二维的,即 (宽度,高度)。由于我们需要三维数据 (宽度,高度,通道) 进行卷积操作,因此我们需要使用 np.newaxis 函数创建第三维。请注意,第一个维度将是样本的数量:
# Reshaping the input data to include channel
X = X_train[:,:,:,np.newaxis]
# Generating a batch of data
imageBatch = X[np.random.randint(0, X.shape[0], size=batch)]
另一个过程是生成训练数据的批次。为了生成数据批次,我们需要在 0 和训练集中的样本数量之间随机抽取一些整数。样本的大小将等于我们希望的批次大小。其实现如下:
# Generating a batch of data
imageBatch = X[np.random.randint(0, X.shape[0], size=batch)]
我们只会返回 X 变量。批次的标签值将是 1,并将在训练过程中单独生成。
然后,我们需要定义三个函数,用于生成假样本。这些函数包括生成假输入的函数、生成器网络的函数以及生成假样本和标签的函数。大多数这些函数与我们在练习 7.04,实现 GAN中开发的函数相同。生成器模型将被构建为一个卷积模型,并间歇性地使用Up-Sampling/Converse2Dtranspose操作。
接下来,我们需要为判别器网络创建一个新函数。这个判别器模型将再次是一个卷积模型,最后一层是一个 sigmoid 层,在其中输出一个概率值,即图像为真实图像还是假图像的概率。判别器模型的输入维度将是从 MNIST 生成的图像和假图像的维度,这些维度为(batch size, 28, 28, 1)。
GAN 模型将与我们在练习 7.04,实现 GAN中创建的模型相同。此函数将以生成器模型和判别器模型作为输入。
训练过程
训练过程将与我们在练习 7.04,实现 GAN中实现的过程类似。训练过程的步骤如下:
-
使用生成真实数据集的函数,生成一批 MNIST 数据。
-
使用函数 3,生成一个假样本批次,假样本生成函数中有详细描述。
-
将真实样本和假样本拼接成一个 DataFrame。这将成为判别模型的输入变量。
-
标签将是一系列的 1 和 0,对应于之前拼接的真实数据和假数据。
-
使用
train_on_batch()函数,通过X变量和标签训练判别模型。 -
为训练 GAN 模型生成另一个批次的假输入。这些假样本是使用假样本生成过程中函数 1生成的。
-
为假样本生成标签,这些标签的目的是欺骗判别器。这些标签将是 1,而不是 0。
-
使用
train_on_batch()函数,利用假样本及其标签训练 GAN 模型,如步骤 6和7所述。 -
步骤 1到步骤 8会根据我们希望训练运行的 epoch 数量重复。这是通过对 epoch 数量进行
for循环实现的。 -
在每个中间步骤中,我们都会计算判别模型的准确性。
-
我们还会在某些 epoch 时生成输出图。
现在我们已经了解了训练 DCGAN 背后的完整过程,让我们深入到下一个练习中,实际实现这个过程。
练习 7.05:实现 DCGAN
在本练习中,我们将构建并训练一个 DCGAN,使用 MNIST 数据集。我们将使用 MNIST 数据集作为真实数据分布,然后从随机分布中生成假数据。之后,我们将训练 GAN 生成类似 MNIST 数据集的内容。按照以下步骤完成本练习:
-
打开一个新的 Jupyter Notebook,并命名为
练习 7.05。导入以下库包和 MNIST 数据集:# Import the required library functions import numpy as np import matplotlib.pyplot as plt from matplotlib import pyplot import tensorflow as tf from tensorflow.keras.layers import Input from tensorflow.keras.initializers import RandomNormal from tensorflow.keras.models import Model, Sequential from tensorflow.keras.layers \ import Reshape, Dense, Dropout, Flatten,Activation from tensorflow.keras.layers import LeakyReLU,BatchNormalization from tensorflow.keras.layers \ import Conv2D, UpSampling2D,Conv2DTranspose from tensorflow.keras.datasets import mnist from tensorflow.keras.optimizers import Adam -
定义一个函数,用于生成真实数据集。真实数据集是从 MNIST 数据中生成的:
# Function to generate real data samples def realData(batch): # Get the MNIST data (X_train, _), (_, _) = mnist.load_data() # Reshaping the input data to include channel X = X_train[:,:,:,np.newaxis] # normalising the data X = (X.astype('float32') - 127.5)/127.5 # Generating a batch of data imageBatch = X[np.random.randint(0, X.shape[0], size=batch)] return imageBatch该函数的返回值是 MNIST 数据的批次。请注意,我们通过减去
127.5(这是像素值的最大值 255 的一半)并除以相同的数值来对输入数据进行归一化。这有助于更快地收敛解决方案。 -
现在,让我们从 MNIST 数据集中生成一组图像:
# # Generating a batch of images mnistData = realData(25) -
接下来,让我们使用
matplotlib可视化这些图:# Plotting the image for j in range(5*5): pyplot.subplot(5,5,j+1) # turn off axis pyplot.axis('off') pyplot.imshow(mnistData[j,:,:,0],cmap='gray_r')你应该会得到类似于以下的输出:
![图 7.24:可视化数据 – 来自数据集的数字]()
图 7.24:可视化数据 – 来自数据集的数字
从输出中,我们可以看到一些数字的可视化。我们可以看到图像在白色背景中居中显示。
注意
当你运行代码时,所显示的数字会与我们在这里展示的有所不同。
-
定义一个函数,用于为生成器网络生成输入。假数据将是从均匀分布中生成的随机数据点:
# Function to generate inputs for generator function def fakeInputs(batch,infeats): #Generate random noise data with shape (batch,input features) x_fake = np.random.uniform(-1,1,size=[batch,infeats]) return x_fake -
让我们定义构建生成器网络的函数。构建生成器网络类似于构建任何 CNN 网络。在这个生成器网络中,我们将使用
UpSampling方法:Exercise7.05.ipynb # Function for the generator model def genModel(infeats): # Defining the Generator model Genmodel = Sequential() Genmodel.add(Dense(512,input_dim=infeats)) Genmodel.add(Activation('relu')) Genmodel.add(BatchNormalization()) # second layer of FC => RElu => BN layers Genmodel.add(Dense(7*7*64)) Genmodel.add(Activation('relu')) The complete code for this step can be found on https://packt.live/2ZPg8cJ.在模型中,我们可以看到逐步使用转置卷积操作。初始输入的维度为 100。通过一系列转置卷积操作,这个维度逐渐增加,最终达到所需的图像大小:batch size x 28 x 28。
-
接下来,我们定义一个函数来创建假样本。在这个函数中,我们只返回
X变量:# Function to create fake samples using the generator model def fakedataGenerator(Genmodel,batch,infeats): # first generate the inputs to the model genInputs = fakeInputs(batch,infeats) """ use these inputs inside the generator model to generate fake distribution """ X_fake = Genmodel.predict(genInputs) return X_fake这个函数的返回值是伪造的数据集。
-
定义我们将使用的参数,并给出生成器网络的总结:
# Define the arguments like batch size and input feature batch = 128 infeats = 100 Genmodel = genModel(infeats) Genmodel.summary()你应该得到以下输出:
![图 7.25 模型总结]()
图 7.25 模型总结
从总结中,请注意每次转置卷积操作后输入维度的变化。最终,我们得到了一个与真实数据集维度相同的输出:(None, 28, 28, 1)。
-
在训练之前,让我们使用生成器函数生成一个假样本:
# Generating a fake sample and printing the shape fake = fakedataGenerator(Genmodel,batch,infeats) fake.shape你应该得到以下输出:
(128, 28, 28, 1) -
现在,让我们绘制生成的假样本:
# Plotting the fake sample plt.imshow(fake[1, :, :, 0], cmap='gray_r') plt.xlabel('Fake Sample Image')你应该得到一个类似下面的输出:
![图 7.26:假样本图像的绘制]()
图 7.26:假样本图像的绘制
这是训练前假样本的图像。训练后,我们希望这些样本看起来像我们之前在本练习中可视化的 MNIST 样本。
-
现在,让我们把判别器模型作为函数来构建。这个网络将是一个 CNN 网络,就像你在第三章中学习的内容,使用卷积神经网络进行图像分类:
Exercise7.05.ipynb # Descriminator model as a function def discModel(): Discmodel = Sequential() Discmodel.add(Conv2D(32,kernel_size=(5,5),strides=(2,2), \ padding='same',input_shape=(28,28,1))) Discmodel.add(LeakyReLU(0.2)) # second layer of convolutions Discmodel.add(Conv2D(64, kernel_size=(5,5), \ strides=(2, 2), padding='same')) The complete code for this step can be found on https://packt.live/2ZPg8cJ.在判别器网络中,我们包含了所有必要的层,比如卷积操作和 LeakyReLU 激活函数。请注意,最后一层是一个 sigmoid 层,因为我们希望输出是一个样本为真实(1)或假(0)的概率。
-
打印判别器网络的总结:
# Print the summary of the discriminator model Discmodel = discModel() Discmodel.summary()你应该得到以下输出:
![图 7.27:模型架构总结]()
图 7.27:模型架构总结
上面的截图展示了模型架构的总结。这是基于我们使用
Sequential类实现的不同层。例如,我们可以看到第一层有 32 个滤波器图,第二层有 64 个滤波器图,最后一层有一个输出,对应于 sigmoid 激活函数。 -
接下来,将 GAN 模型定义为一个函数:
""" Define the combined generator and discriminator model, for updating the generator """ def ganModel(Genmodel,Discmodel): # First define that discriminator model cannot be trained Discmodel.trainable = False Ganmodel = Sequential() # First adding the generator model Ganmodel.add(Genmodel) """ Next adding the discriminator model without training the parameters """ Ganmodel.add(Discmodel) # Compile the model for loss to optimise the Generator model Ganmodel.compile(loss='binary_crossentropy',\ optimizer = 'adam') return GanmodelGAN 模型的结构与我们在练习 7.04中开发的结构类似,实现 GAN。
-
现在,是时候调用 GAN 函数了。请注意,GAN 模型的输入是之前定义的生成器和判别器模型:
# Initialise the gan model gan_model = ganModel(Genmodel,Discmodel) # Print summary of the GAN model gan_model.summary()从前面的代码中,我们可以看到 GAN 模型的输入是先前定义的生成器和判别器模型。你应该得到如下输出:
![图 7.28:模型摘要]()
图 7.28:模型摘要
请注意,GAN 模型每一层的参数等同于生成器和判别器模型的参数。GAN 模型只是我们先前定义的模型的一个封装。
-
定义训练网络的周期数:
# Defining the number of epochs nEpochs = 5000 -
现在,让我们训练 GAN:
Exercise7.05.ipynb # Train the GAN network for i in range(nEpochs): """ Generate samples equal to the bath size from the real distribution """ x_real = realData(batch) #Generate fake samples using the fake data generator function x_fake = fakedataGenerator(Genmodel,batch,infeats) # Concatenating the real and fake data X = np.concatenate([x_real,x_fake]) #Creating the dependent variable and initializing them as '0' Y = np.zeros(batch * 2) The full code for this step can be found at https://packt.live/2ZPg8cJ.从前面的代码中,我们可以看到,判别器模型使用假样本和真实样本进行训练,而 GAN 模型的训练是同时进行的。唯一的区别是 GAN 模型的训练在不更新判别器模型参数的情况下继续进行。另一个需要注意的点是,在 GAN 内部,假样本的标签将是 1,以生成较大的损失项,这些损失项将通过判别器网络反向传播以更新生成器参数。我们还显示了每 10 个训练周期后 GAN 的预测概率。在计算概率时,我们将真实数据和假数据样本结合在一起,然后取预测概率的平均值。我们还保存了一份生成图像的副本。
Discriminator probability:0.6213402152061462 Discriminator probability:0.7360671758651733 Discriminator probability:0.6130768656730652 Discriminator probability:0.5046337842941284 Discriminator probability:0.5005484223365784 Discriminator probability:0.50015789270401 Discriminator probability:0.5000558495521545 Discriminator probability:0.5000174641609192 Discriminator probability:0.5000079274177551 Discriminator probability:0.4999823570251465 Discriminator probability:0.5000027418136597 Discriminator probability:0.5000032186508179 Discriminator probability:0.5000043511390686 Discriminator probability:0.5000077486038208注意
前面代码的输出可能与您运行代码时得到的结果不完全一致。
从测试数据的预测概率中,我们可以看到,值徘徊在
.55附近。这表明判别器对于图像是假的还是实的感到困惑。如果判别器确定图像是真的,它会预测接近 1 的概率,而如果确定图像是假的,它则会预测接近 0 的概率。在我们的案例中,概率大约在.55 附近,表明生成器正在学习生成与真实图像相似的图像,这使得判别器感到困惑。判别器的准确率接近 50%的值是理想值。 -
现在,让我们在训练过程后生成假图像并可视化它们:
# Images predicted after training x_fake = fakedataGenerator(Genmodel,25,infeats) # Visualizing the plots for j in range(5*5): pyplot.subplot(5,5,j+1) # turn off axis pyplot.axis('off') pyplot.imshow(x_fake[j,:,:,0],cmap='gray_r')输出结果如下:
![图 7.29:训练后预测的图像]()
图 7.29:训练后预测的图像
我们可以看到,从训练后的生成器模型生成的图像与真实的手写数字非常相似。
注意
若要访问此部分的源代码,请参考packt.live/2ZPg8cJ。
本节目前没有在线交互示例,需在本地运行。
在这个练习中,我们开发了一个 GAN,用于生成类似 MNIST 手写数字的分布。在接下来的部分,我们将分析在每个训练周期生成的图像。
样本图分析
现在,让我们看看前一个练习的输出样本图,看看生成的图像是什么样子的。完成前一个练习后,这些图像应该已经保存在与你的 Jupyter Notebook 位于同一路径下的一个名为 handwritten 的子文件夹中:

图 7.30: 经过 10 次迭代后的样本图
上述图像是经过 10 次迭代后的生成结果。我们可以看到这些图像更像是随机噪声。然而,我们也可以看到图像中开始形成一些白色斑块,这表明 GAN 正在学习真实图像的一些特征:

图 7.31: 经过 500 次迭代后的样本图
上述图像是经过 500 次迭代后的结果。从这些图像中,我们可以看到一些类似真实图像的特征。我们可以看到,真实图像的白色背景正在形成。我们还可以看到图像中的分布开始集中在图像的中央:

图 7.32: 经过 2,000 次迭代后的样本图
上图是经过 2,000 次迭代后的结果。我们可以看到,许多数字开始出现;例如,8、5、3、9、4、7、0 等等。我们还可以看到,图像的暗色区域开始变得更加明显。现在,让我们看看最后一次迭代生成的图像:

图 7.33: 经过 5,000 次迭代后的样本图
在这个阶段需要问的一个问题是,这些图像完美吗?绝对不是。继续训练更多的轮次会进一步改善结果吗?不一定。要获得完美的图像需要长时间的训练,并且需要对不同的模型架构进行实验。你可以将此作为一个挑战,通过选择架构和模型参数来改善输出。
GANs 是一个非常活跃的研究领域,它们所打开的可能性指向了计算机逐渐变得具有创造力的方向。然而,在实现 GANs 时存在一些常见问题。让我们来看一下其中的一些。
GANs 的常见问题
GANs 是难以训练和稳定的网络。GANs 有不同的失败模式。让我们来看看一些常见的失败模式。
模式崩溃
GANs 中一种非常常见的失败模式,尤其是在多模态数据上,是所谓的 模式崩溃。这指的是生成器仅学习数据分布中的某些特定种类。例如,在 MNIST 数据分布中,如果 GAN 训练后只生成一个特定的数字(比如 5),那么这就是模式崩溃的表现。
对抗模式崩溃的一种方法是根据不同的类别对数据进行分组,并相应地训练判别器。这样可以使判别器具备识别数据中不同模式的能力。
收敛失败
GAN 中的另一个显著故障模式是收敛失败。在这种故障模式下,网络无法收敛,损失值在训练阶段始终无法稳定。一些研究人员为解决这一问题,采用的方法包括向判别网络添加噪声,并通过正则化技术对判别器权重进行惩罚。
尽管训练和构建 GAN 面临许多挑战,但它仍然是深度学习领域最活跃的研究领域之一。GAN 所带来的承诺和应用使其成为深度学习中最受追捧的领域之一。现在我们已经为 GAN 奠定了一些基础,让我们用所学的知识为一个不同的数据集构建一个 GAN。
活动 7.01:为 MNIST 时尚数据集实现 DCGAN
在这个活动中,你将实现一个 DCGAN 来生成类似于 MNIST 时尚数据集中图像的图像。MNIST 时尚数据集与练习 7.05中实现的手写数字图像数据集类似。该数据集由 10 种不同的时尚配饰的灰度图像组成,共有 60,000 个训练样本。以下是该数据集中包含的图像样本:

图 7.34:MNIST 时尚数据集样本
本活动的目标是构建一个 GAN 并生成类似于时尚数据集的图像。这个活动的高层次步骤将类似于练习 7.05中的步骤,即你实现了一个用于手写数字的 DCGAN。你将分两部分完成这个活动,首先是创建相关函数,然后是训练模型。
生成关键函数:在这里,你将创建所需的函数,如生成器函数和判别器函数:
-
定义一个生成真实数据分布的函数。这个函数必须从 MNIST 时尚数据集中生成真实的数据分布。可以使用以下代码导入时尚数据集:
from tensorflow.keras.datasets import fashion_mnist训练集可以使用
fashion_mnist.load_data()函数生成。注意:
或者,你可以从
packt.live/2X4xeCL下载数据集。 -
定义三个函数,用于生成假样本;即生成假输入的函数、生成器网络的函数以及生成假样本和标签的函数。在生成器函数中使用Converse2Dtranspose操作。
-
为判别网络创建一个新函数。
-
创建 GAN 模型。你可以参考练习 7.05,实现 DCGAN,了解如何实现这一点。
训练过程:你将遵循与练习 7.05,实现 DCGAN类似的过程:
-
使用生成真实数据集的函数生成一批 MNIST 数据。
-
使用生成假样本的第三个函数生成一批假样本。
-
将真实样本和假样本合并成一个 DataFrame,并生成它们的标签。
-
使用
train_on_batch()函数训练判别器模型,使用X变量和标签。 -
生成另一批用于训练 GAN 模型的假输入,以及它们的标签。
-
使用
train_on_batch()函数训练 GAN 模型,使用假样本及其标签。 -
重复训练约 5,000 个周期。
-
在每一个中间步骤中,计算判别器模型的准确性。
你得到的判别器概率应该接近0.5。预期的输出将是一个生成的图像,看起来与这里展示的图像相似:

图 7.35:此活动的预期输出
注意:
本活动的详细步骤,包含解决方案和额外的注释,见第 426 页。
总结
你已经从了解深度学习中最有前景的领域之一,走了很长一段路。让我们回顾一下本章中学到的一些概念。
我们从了解 GAN 是什么及其主要应用开始了这一章。然后,我们进一步了解了 GAN 的各种构建模块,如真实数据集、假数据集、判别器操作、生成器操作和 GAN 操作。
我们执行了一个问题陈述,逐步构建了一个全连接 GAN(FCGAN)来解决一个实际的函数问题。在构建 GAN 的过程中,我们还实现了创建真实数据集、创建假数据集、创建生成器网络、创建判别器网络的练习,最后将所有这些单独的组件组合成 GAN。我们可视化了不同的图形,并理解了生成的数据分布如何模仿真实数据分布。
在下一节中,我们了解了 DCGAN 的概念。我们还学习了 DCGAN 中的一些独特概念,如上采样和转置卷积。我们实现了一个用于 MNIST 数字手写图像的 GAN,并使用 DCGAN 可视化了我们生成的假图像。最后,我们还在一个活动中为 MNIST 时尚数据集实现了 DCGAN。
在打下基础后,接下来的问题是,我们从哪里开始?生成对抗网络(GAN)本身就是一个庞大的领域,最近围绕它有很多热议。首先,调整你已经学习过的模型会是一个不错的起点,可以通过调整架构、激活函数以及尝试其他参数(如批量归一化)来进行优化。在尝试不同变种的现有模型后,接下来就可以探索其他网络,例如最小二乘生成对抗网络(LSGAN)和瓦瑟斯坦生成对抗网络(WGAN)。接着,还有一个大领域是条件生成对抗网络(conditional GAN),比如条件生成对抗网络(cGan)、InfoGAN、辅助分类器生成对抗网络(AC-GAN)和半监督生成对抗网络(SGAN)。完成这些后,你将为学习更高级的主题,如 CycleGAN、BigGAN 和 StyleGAN,奠定基础。
本章也为你在本书中的精彩旅程画上了句号。首先,你了解了什么是深度学习以及深度学习可以实现的不同应用场景。随后,你学习了神经网络的基础知识,神经网络是深度学习的基础。从那里,你开始学习诸如卷积神经网络(CNN)等高级技术,它们是图像识别等应用场景的主要技术。与此同时,你还学习了循环神经网络(RNN),它们可以用于处理序列数据。最后,你接触了生成对抗网络(GAN),这一类网络正在该领域掀起波澜。现在,你已经掌握了这套工具,是时候将所学应用到你的领域了。可能性和机会是无穷无尽的。我们需要做的就是巩固当前的学习,并一步步向前迈进。祝你在深度学习领域的旅程中不断攀登新高峰,取得成功。
附录
1. 深度学习的构建模块
活动 1.01:使用优化器求解二次方程
解决方案
让我们来解以下二次方程:

图 1.29:需要解的二次方程
我们已经知道,这个二次方程的解是 x=5。
我们可以使用优化器来解决这个问题。对于优化器,x 是变量,代价函数是左侧表达式,如下所示:

图 1.30:左侧表达式
优化器将找到 x 的值,使得表达式最小——在这种情况下是 0。请注意,这仅适用于像这种完美平方的二次方程。左侧的表达式是一个完美的平方,可以通过以下方程来解释:

图 1.31:完美的平方
现在,让我们来看一下解决这个问题的代码:
-
打开一个新的 Jupyter Notebook 并将其重命名为 Activity 1.01。
-
导入
tensorflow:import tensorflow as tf -
创建变量
x并将其初始化为 0.0:x=tf.Variable(0.0) -
将
loss函数构建为一个lambda函数:loss=lambda:abs(x**2-10*x+25) -
创建一个学习率为
.01的优化器实例:optimizer=tf.optimizers.Adam(.01) -
运行优化器进行 10,000 次迭代。您可以从较小的数字开始,如 1,000,然后逐渐增加迭代次数,直到得到解:
for i in range(10000): optimizer.minimize(loss,x) -
打印
x的值:tf.print(x)输出结果如下:
4.99919891
这是我们二次方程的解。需要注意的是,无论迭代次数多少,您永远不会得到一个完美的 5。
注意
要访问这个特定部分的源代码,请参考 packt.live/3gBTFGA。
您还可以在线运行此示例,网址是 packt.live/2Dqa2Id。您必须执行整个 Notebook 才能获得预期的结果。
2. 神经网络
活动 2.01:构建一个多层神经网络来分类声呐信号
解决方案
让我们看看解决方案是怎样的。记住——这是一个解,但可能会有许多变种:
-
导入所有需要的库:
import tensorflow as tf import pandas as pd from sklearn.preprocessing import LabelEncoder # Import Keras libraries from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense -
加载并检查数据:
df = pd.read_csv('sonar.csv') df.head()输出结果如下:
![图 2.37:sonar.csv 的内容]()
图 2.37:sonar.csv 的内容
可以观察到,有 60 个特征,目标变量有两个值——Rock 和 Mine。
这意味着这是一个二元分类问题。让我们在构建神经网络之前先准备数据。
-
分离特征和标签:
X_input = df.iloc[:, :-1] Y_label = df['Class'].values在这段代码中,
X_input选择了所有列中的所有行,除了Class列,而Y_label仅选择了Class列。 -
标签是文本格式的。我们需要将它们编码为数字,然后才能在模型中使用:
labelencoder_Y = LabelEncoder() Y_label = labelencoder_Y.fit_transform(Y_label) Y_label = Y_label.reshape([208, 1])最后的
reshape函数会将标签转换为矩阵格式,这是模型所期望的。 -
使用 Keras 构建多层模型:
model = Sequential() model.add(Dense(300,input_dim=60, activation = 'relu')) model.add(Dense(200, activation = 'relu')) model.add(Dense(100, activation = 'relu')) model.add(Dense(1, activation = 'sigmoid'))你可以尝试调整层数和神经元数量,但最后一层只能有一个神经元,并使用 sigmoid 激活函数,因为这是一个二分类器。
-
设置训练参数:
model.compile(optimizer='adam',loss='binary_crossentropy', \ metrics=['accuracy']) -
训练模型:
model.fit(X_input, Y_label, epochs=30)截断的输出结果大致如下:
Train on 208 samples Epoch 1/30 208/208 [==============================] - 0s 205us/sample - loss: 0.1849 - accuracy: 0.9038 Epoch 2/30 208/208 [==============================] - 0s 220us/sample – loss: 0.1299 - accuracy: 0.9615 Epoch 3/30 208/208 [==============================] - 0s 131us/sample – loss: 0.0947 - accuracy: 0.9856 Epoch 4/30 208/208 [==============================] - 0s 151us/sample – loss: 0.1046 - accuracy: 0.9712 Epoch 5/30 208/208 [==============================] - 0s 171us/sample – loss: 0.0952 - accuracy: 0.9663 Epoch 6/30 208/208 [==============================] - 0s 134us/sample – loss: 0.0777 - accuracy: 0.9856 Epoch 7/30 208/208 [==============================] - 0s 129us/sample – loss: 0.1043 - accuracy: 0.9663 Epoch 8/30 208/208 [==============================] - 0s 142us/sample – loss: 0.0842 - accuracy: 0.9712 Epoch 9/30 208/208 [==============================] - 0s 155us/sample – loss: 0.1209 - accuracy: 0.9423 Epoch 10/30 208/208 [==============================] - ETA: 0s - loss: 0.0540 - accuracy: 0.98 - 0s 334us/sample - los -
让我们评估训练好的模型并检查其准确性:
model.evaluate(X_input, Y_label)输出如下:
208/208 [==============================] - 0s 128us/sample – loss: 0.0038 - accuracy: 1.0000 [0.003758653004367191, 1.0]如你所见,我们成功训练了一个多层二分类神经网络,并在 30 个周期内达到了 100%的准确率。
注意
要访问该部分的源代码,请参考
packt.live/38EMoDi。你还可以在线运行这个示例,网址为
packt.live/2W2sygb。你必须执行整个 Notebook 才能获得预期的结果。
3. 使用卷积神经网络(CNN)进行图像分类
活动 3.01:基于 Fashion MNIST 数据集构建多分类器
解决方案
-
打开一个新的 Jupyter Notebook。
-
从
tensorflow.keras.datasets.fashion_mnist导入:from tensorflow.keras.datasets import fashion_mnist -
使用
fashion_mnist.load_data()加载 Fashion MNIST 数据集,并将结果保存到(features_train, label_train), (features_test, label_test):(features_train, label_train), (features_test, label_test) = \ fashion_mnist.load_data() -
打印训练集的形状:
features_train.shape输出结果如下:
(60000, 28, 28)训练集包含
60000张大小为28乘28的图像。我们需要对其进行重塑并添加通道维度。 -
打印测试集的形状:
features_test.shape输出结果如下:
(10000, 28, 28)测试集包含
10000张大小为28乘28的图像。我们需要对其进行重塑并添加通道维度。 -
将训练集和测试集重塑为维度
(number_rows, 28, 28, 1):features_train = features_train.reshape(60000, 28, 28, 1) features_test = features_test.reshape(10000, 28, 28, 1) -
创建三个变量
batch_size、img_height和img_width,分别赋值为16、28和28:batch_size = 16 img_height = 28 img_width = 28 -
从
tensorflow.keras.preprocessing导入ImageDataGenerator:from tensorflow.keras.preprocessing.image \ import ImageDataGenerator -
创建一个名为
train_img_gen的ImageDataGenerator,并进行数据增强:rescale=1./255, rotation_range=40, width_shift_range=0.1, height_shift_range=0.1, shear_range=0.2, zoom_range=0.2, horizontal_flip=True, fill_mode='nearest':train_img_gen = ImageDataGenerator(rescale=1./255, \ rotation_range=40, \ width_shift_range=0.1, \ height_shift_range=0.1, \ shear_range=0.2, \ zoom_range=0.2, \ horizontal_flip=True, \ fill_mode='nearest') -
创建一个名为
val_img_gen的ImageDataGenerator,进行重缩放(除以 255):val_img_gen = ImageDataGenerator(rescale=1./255) -
创建一个名为
train_data_gen的数据生成器,使用.flow()并指定批量大小、特征和来自训练集的标签:train_data_gen = train_img_gen.flow(features_train, \ label_train, \ batch_size=batch_size) -
创建一个名为
val_data_gen的数据生成器,使用.flow()并指定批量大小、特征和来自测试集的标签:val_data_gen = train_img_gen.flow(features_test, \ label_test, \ batch_size=batch_size) -
导入
numpy为np,导入tensorflow为tf,并从tensorflow.keras导入layers:import numpy as np import tensorflow as tf from tensorflow.keras import layers -
使用
np.random_seed()和tf.random.set_seed()将8设置为numpy和tensorflow的随机种子:np.random.seed(8) tf.random.set_seed(8) -
将
tf.keras.Sequential()类实例化为名为model的变量,具有以下层:具有形状为3的64个核的卷积层,ReLU作为激活函数和必要的输入维度;一个最大池化层;具有形状为3的128个核的卷积层和ReLU作为激活函数;一个最大池化层;一个展平层;一个具有128单元和ReLU作为激活函数的全连接层;一个具有10单元和softmax作为激活函数的全连接层。代码应如下所示:
model = tf.keras.Sequential\ ([layers.Conv2D(64, 3, activation='relu', \ input_shape=(img_height, \ img_width ,1)), \ layers.MaxPooling2D(), \ layers.Conv2D(128, 3, \ activation='relu'), \ layers.MaxPooling2D(),\ layers.Flatten(), \ layers.Dense(128, \ activation='relu'), \ layers.Dense(10, \ activation='softmax')]) -
将
tf.keras.optimizers.Adam()类实例化为名为optimizer的变量,并将学习率设置为0.001:optimizer = tf.keras.optimizers.Adam(0.001) -
使用
.compile()编译神经网络,参数为loss='sparse_categorical_crossentropy', optimizer=optimizer, metrics=['accuracy']:model.compile(loss='sparse_categorical_crossentropy', \ optimizer=optimizer, metrics=['accuracy']) -
使用
fit_generator()拟合神经网络,并提供训练和验证数据生成器,epochs=5,每个 epoch 的步数以及验证步数:model.fit_generator(train_data_gen, \ steps_per_epoch=len(features_train) \ // batch_size, \ epochs=5, \ validation_data=val_data_gen, \ validation_steps=len(features_test) \ // batch_size)预期输出如下:
![图 3.30:模型训练日志
![B15385_03_30.jpg
图 3.30:模型训练日志
我们在五个 epochs 上训练了我们的 CNN,在训练集和验证集上分别获得了 0.8271 和 0.8334 的准确度分数。我们的模型没有过拟合,并且取得了相当高的分数。在五个 epochs 后,准确度仍在增加,所以如果继续训练,可能会获得更好的结果。这是您可以自行尝试的内容。
注意
要访问此特定部分的源代码,请参阅 packt.live/2ObmA8t。
您还可以在 packt.live/3fiyyJi 上在线运行此示例。必须执行整个 Notebook 才能获得所需的结果。
活动 3.02:使用迁移学习进行水果分类
解决方案
-
打开一个新的 Jupyter Notebook。
-
导入
tensorflow并将其命名为tf:import tensorflow as tf -
创建一个名为
file_url的变量,其中包含指向数据集的链接:file_url = 'https://github.com/PacktWorkshops'\ '/The-Deep-Learning-Workshop'\ '/raw/master/Chapter03/Datasets/Activity3.02'\ '/fruits360.zip'注意
在上述步骤中,我们使用的是存储在
packt.live/3eePQ8G的数据集。如果您将数据集存储在其他任何 URL,请相应更改突出显示的路径。 -
使用
tf.keras.get_file并将'fruits360.zip', origin=file_url, extract=True作为参数下载数据集,并将结果保存到名为zip_dir的变量中:zip_dir = tf.keras.utils.get_file('fruits360.zip', \ origin=file_url, \ extract=True) -
导入
pathlib库:import pathlib -
使用
pathlib.Path(zip_dir).parent创建一个名为path的变量,其中包含到fruits360_filtered目录的完整路径:path = pathlib.Path(zip_dir).parent / 'fruits360_filtered' -
创建两个名为
train_dir和validation_dir的变量,分别指向训练 (Training) 和验证 (Test) 文件夹的完整路径:train_dir = path / 'Training' validation_dir = path / 'Test' -
创建两个名为
total_train和total_val的变量,分别获取训练集和验证集的图像数量,即11398和4752:total_train = 11398 total_val = 4752 -
从
tensorflow.keras.preprocessing导入ImageDataGenerator:from tensorflow.keras.preprocessing.image \ import ImageDataGenerator -
创建一个名为
train_img_gen的ImageDataGenerator,并进行数据增强:rescale=1./255, rotation_range=40, width_shift_range=0.1, height_shift_range=0.1, shear_range=0.2, zoom_range=0.2, horizontal_flip=True, fill_mode='nearest':train_img_gen = ImageDataGenerator(rescale=1./255, \ rotation_range=40, \ width_shift_range=0.1, \ height_shift_range=0.1, \ shear_range=0.2, \ zoom_range=0.2, \ horizontal_flip=True, \ fill_mode='nearest') -
创建一个名为
val_img_gen的ImageDataGenerator,并进行重新缩放(通过除以 255):val_img_gen = ImageDataGenerator(rescale=1./255) -
创建四个变量,分别为
batch_size、img_height、img_width和channel,其值分别为16、100、100和3:batch_size=16 img_height = 100 img_width = 100 channel = 3 -
使用
.flow_from_directory()创建一个名为train_data_gen的数据生成器,并指定批量大小、训练文件夹和目标尺寸:train_data_gen = train_image_generator.flow_from_directory\ (batch_size=batch_size, \ directory=train_dir, \ target_size=(img_height, img_width)) -
使用
.flow_from_directory()创建一个名为val_data_gen的数据生成器,并指定批量大小、验证文件夹和目标尺寸:val_data_gen = validation_image_generator.flow_from_directory\ (batch_size=batch_size, \ directory=validation_dir, \ target_size=(img_height, img_width)) -
导入
numpy为np,tensorflow为tf,以及从tensorflow.keras导入layers:import numpy as np import tensorflow as tf from tensorflow.keras import layers -
使用
np.random_seed()和tf.random.set_seed()将8设置为numpy和tensorflow的种子:np.random.seed(8) tf.random.set_seed(8) -
从
tensorflow.keras.applications导入VGG16:from tensorflow.keras.applications import VGG16 -
使用以下参数将
VGG16模型实例化为一个名为base_model的变量:base_model = VGG16(input_shape=(img_height, \ img_width, channel), \ weights='imagenet', \ include_top=False) -
使用
.trainable属性将此模型设置为不可训练:base_model.trainable = False -
打印此
VGG16模型的摘要:base_model.summary()预期的输出将如下所示:
![图 3.31:模型概述]()
图 3.31:模型概述
此输出显示了
VGG16的架构。我们可以看到总共有14,714,688个参数,但没有可训练的参数。这是预期的,因为我们已经冻结了模型的所有层。 -
使用
tf.keras.Sequential()创建一个新模型,通过添加基础模型和以下层:Flatten()、Dense(1000, activation='relu')以及Dense(120, activation='softmax')。将此模型保存到名为model的变量中:model = tf.keras.Sequential([base_model, \ layers.Flatten(), \ layers.Dense(1000, \ activation='relu'), \ layers.Dense(120, \ activation='softmax')]) -
使用
0.001作为学习率实例化tf.keras.optimizers.Adam()类,并将其保存到名为optimizer的变量中:optimizer = tf.keras.optimizers.Adam(0.001) -
使用
.compile()编译神经网络,设置loss='categorical_crossentropy', optimizer=optimizer, metrics=['accuracy']:model.compile(loss='categorical_crossentropy', \ optimizer=optimizer, metrics=['accuracy']) -
使用
fit_generator()训练神经网络,并提供训练和验证数据生成器、epochs=5、每个 epoch 的步数以及验证步数。此模型的训练可能需要几分钟时间:model.fit_generator(train_data_gen, \ steps_per_epoch=len(features_train) \ // batch_size, \ epochs=5, \ validation_data=val_data_gen, \ validation_steps=len(features_test) \ // batch_size)预期的输出将如下所示:
![图 3.32:预期输出]()
图 3.32:预期输出
在这里,我们使用迁移学习对预训练的VGG16模型进行定制,以便其适应我们的水果分类数据集。我们用自己的全连接层替换了模型的头部,并在五个 epoch 上训练了这些层。我们在训练集上获得了0.9106的准确率,在测试集上获得了0.8920的准确率。考虑到训练此模型所用的时间和硬件,这些结果相当显著。你可以尝试微调此模型,看看是否能获得更好的分数。
注意
若要访问此特定部分的源代码,请参阅packt.live/2DsVRCl。
本节目前没有在线交互示例,需要在本地运行。
4. 深度学习文本 - 词嵌入
活动 4.01:‘爱丽丝梦游仙境’文本的预处理
解决方案
你需要执行以下步骤:
注意
在开始这个活动之前,请确保你已经定义了alice_raw变量,正如在使用 NLTK 下载文本语料库这一节中所示。
-
将数据转换为小写并拆分成句子:
txt_sents = tokenize.sent_tokenize(alice_raw.lower()) -
对句子进行分词:
txt_words = [tokenize.word_tokenize(sent) for sent in txt_sents] -
从
string模块导入punctuation,从 NLTK 导入stopwords:from string import punctuation stop_punct = list(punctuation) from nltk.corpus import stopwords stop_nltk = stopwords.words("english") -
创建一个变量来存储上下文停用词
--和said:stop_context = ["--", "said"] -
创建一个主列表,用于去除包含标点符号、NLTK 停用词和上下文停用词的词语:
stop_final = stop_punct + stop_nltk + stop_context -
定义一个函数,从任何输入句子(已分词)中删除这些标记:
def drop_stop(input_tokens): return [token for token in input_tokens \ if token not in stop_final] -
从分词后的文本中删除
stop_final中的词语:alice_words_nostop = [drop_stop(sent) for sent in txt_words] print(alice_words_nostop[:2])下面是前两句的样子:
[['alice', "'s", 'adventures', 'wonderland', 'lewis', 'carroll', '1865', 'chapter', 'i.', 'rabbit-hole', 'alice', 'beginning', 'get', 'tired', 'sitting', 'sister', 'bank', 'nothing', 'twice', 'peeped', 'book', 'sister', 'reading', 'pictures', 'conversations', "'and", 'use', 'book', 'thought', 'alice', "'without", 'pictures', 'conversation'], ['considering', 'mind', 'well', 'could', 'hot', 'day', 'made', 'feel', 'sleepy', 'stupid', 'whether', 'pleasure', 'making', 'daisy-chain', 'would', 'worth', 'trouble', 'getting', 'picking', 'daisies', 'suddenly', 'white', 'rabbit', 'pink', 'eyes', 'ran', 'close']] -
使用 NLTK 的
PorterStemmer算法,对结果进行词干提取。打印出前五个句子:from nltk.stem import PorterStemmer stemmer_p = PorterStemmer() alice_words_stem = [[stemmer_p.stem(token) for token in sent] \ for sent in alice_words_nostop] print(alice_words_stem[:5])输出结果如下:
[['alic', "'s", 'adventur', 'wonderland', 'lewi', 'carrol', '1865', 'chapter', 'i.', 'rabbit-hol', 'alic', 'begin', 'get', 'tire', 'sit', 'sister', 'bank', 'noth', 'twice', 'peep', 'book', 'sister', 'read', 'pictur', 'convers', "'and", 'use', 'book', 'thought', 'alic', "'without", 'pictur', 'convers'], ['consid', 'mind', 'well', 'could', 'hot', 'day', 'made', 'feel', 'sleepi', 'stupid', 'whether', 'pleasur', 'make', 'daisy-chain', 'would', 'worth', 'troubl', 'get', 'pick', 'daisi', 'suddenli', 'white', 'rabbit', 'pink', 'eye', 'ran', 'close'], ['noth', 'remark', 'alic', 'think', 'much', 'way', 'hear', 'rabbit', 'say', "'oh", 'dear'], ['oh', 'dear'], ['shall', 'late']]注意
要访问这一部分的源代码,请参阅
packt.live/2VVNEgf。你也可以在线运行这个示例,网址是
packt.live/38Gr54r。你必须执行整个笔记本才能得到预期的结果。
活动 4.02:‘爱丽丝梦游仙境’的文本表示
解决方案
你需要执行以下步骤:
-
从活动 4.01,‘爱丽丝梦游仙境’文本预处理中,打印删除停用词后的前三个句子。这是你将要处理的数据:
print(alice_words_nostop[:3])输出结果如下:
[['alice', "'s", 'adventures', 'wonderland', 'lewis', 'carroll', '1865', 'chapter', 'i.', 'rabbit-hole', 'alice', 'beginning', 'get', 'tired', 'sitting', 'sister', 'bank', 'nothing', 'twice', 'peeped', 'book', 'sister', 'reading', 'pictures', 'conversations', "'and", 'use', 'book', 'thought', 'alice', "'without", 'pictures', 'conversation'], ['considering', 'mind', 'well', 'could', 'hot', 'day', 'made', 'feel', 'sleepy', 'stupid', 'whether', 'pleasure', 'making', 'daisy-chain', 'would', 'worth', 'trouble', 'getting', 'picking', 'daisies', 'suddenly', 'white', 'rabbit', 'pink', 'eyes', 'ran', 'close'], ['nothing', 'remarkable', 'alice', 'think', 'much', 'way', 'hear', 'rabbit', 'say', "'oh", 'dear']] -
从 Gensim 导入
word2vec并使用默认参数训练词向量:from gensim.models import word2vec model = word2vec.Word2Vec(alice_words_nostop) -
找到与
rabbit最相似的5个词:model.wv.most_similar("rabbit", topn=5)输出结果如下:
[('alice', 0.9963310360908508), ('little', 0.9956872463226318), ('went', 0.9955698251724243), ("'s", 0.9955658912658691), ('would', 0.9954401254653931)] -
使用
window大小为2,重新训练词向量:model = word2vec.Word2Vec(alice_words_nostop, window=2) -
找到与
rabbit最相似的词:model.wv.most_similar("rabbit", topn=5)输出结果如下:
[('alice', 0.9491485357284546), ("'s", 0.9364748001098633), ('little', 0.9345826506614685), ('large', 0.9341927170753479), ('duchess', 0.9341296553611755)] -
使用窗口大小为
5的 Skip-gram 方法重新训练词向量:model = word2vec.Word2Vec(alice_words_nostop, window=5, sg=1) -
找到与
rabbit最相似的词:model.wv.most_similar("rabbit", topn=5)输出结果如下:
[('gardeners', 0.9995723366737366), ('end', 0.9995588064193726), ('came', 0.9995309114456177), ('sort', 0.9995298385620117), ('upon', 0.9995272159576416)] -
通过平均
white和rabbit的词向量,找到white rabbit的表示:v1 = model.wv['white'] v2 = model.wv['rabbit'] res1 = (v1+v2)/2 -
通过平均
mad和hatter的词向量,找到mad hatter的表示:v1 = model.wv['mad'] v2 = model.wv['hatter'] res2 = (v1+v2)/2 -
计算这两个短语之间的余弦相似度:
model.wv.cosine_similarities(res1, [res2])这给我们以下的值:
array([0.9996213], dtype=float32) -
使用格式化的键值向量加载预训练的 100 维 GloVe 词向量:
from gensim.models.keyedvectors import KeyedVectors glove_model = KeyedVectors.load_word2vec_format\ ("glove.6B.100d.w2vformat.txt", binary=False) -
找到
white rabbit和mad hatter的表示:v1 = glove_model['white'] v2 = glove_model['rabbit'] res1 = (v1+v2)/2 v1 = glove_model['mad'] v2 = glove_model['hatter'] res2 = (v1+v2)/2 -
计算这两个短语之间的
cosine相似度。cosine相似度有变化吗?glove_model.cosine_similarities(res1, [res2])以下是前面代码的输出结果:
array([0.4514577], dtype=float32)
在这里,我们可以看到,两个短语 "mad hatter" 和 "white rabbit" 之间的余弦相似度在 GloVe 模型中较低。这是因为 GloVe 模型在其训练数据中看到这些术语的次数没有书中那么多。在书中,mad 和 hatter 经常一起出现,因为它们组成了一个重要角色的名字。当然,在其他上下文中,我们不会经常看到 mad 和 hatter 一起出现。
注意
要访问此特定部分的源代码,请参考 packt.live/2VVNEgf。
本节目前没有在线互动示例,需在本地运行。
5. 深度学习与序列
活动 5.01:使用普通 RNN 模型预测 IBM 股票价格
解决方案
-
导入必要的库,加载
.csv文件,反转索引,并绘制时间序列(Close列)以进行可视化检查:import pandas as pd, numpy as np import matplotlib.pyplot as plt inp0 = pd.read_csv("IBM.csv") inp0 = inp0.sort_index(ascending=False) inp0.plot("Date", "Close") plt.show()输出将如下所示,收盘价绘制在 Y 轴 上:
![图 5.40:IBM 股票价格趋势]()
图 5.40:IBM 股票价格趋势
-
从 DataFrame 中提取
Close值作为numpy数组,并使用matplotlib绘制它们:ts_data = inp0.Close.values.reshape(-1,1) plt.figure(figsize=[14,5]) plt.plot(ts_data) plt.show()结果趋势如下,索引绘制在 X 轴 上:
![图 5.41:股票价格数据可视化]()
图 5.41:股票价格数据可视化
-
将最后 25% 的数据分配为测试数据,前 75% 的数据分配为训练数据:
train_recs = int(len(ts_data) * 0.75) train_data = ts_data[:train_recs] test_data = ts_data[train_recs:] len(train_data), len(test_data)输出将如下所示:
(1888, 630) -
使用
sklearn中的MinMaxScaler,对训练数据和测试数据进行缩放:from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler() train_scaled = scaler.fit_transform(train_data) test_scaled = scaler.transform(test_data) -
使用我们在本章前面定义的
get_lookback函数(参见 准备股票价格预测数据 部分),使用 10 的回溯期获取训练集和测试集的回溯数据:look_back = 10 trainX, trainY = get_lookback(train_scaled, look_back=look_back) testX, testY = get_lookback(test_scaled, look_back= look_back) trainX.shape, testX.shape输出将如下所示:
((1888, 10), (630, 10)) -
从 Keras 中导入所有必要的层,用于使用普通 RNN(
SimpleRNN、Activation、Dropout、Dense和Reshape)和 1D 卷积(Conv1D)。同时从sklearn导入mean_squared_error度量:from tensorflow.keras.models import Sequential from tensorflow.keras.layers import SimpleRNN, Activation, Dropout, Dense, Reshape, Conv1D from sklearn.metrics import mean_squared_error -
使用一个包含 5 个 3x3 卷积核的 1D 卷积层和一个包含 32 个神经元的 RNN 层构建模型。在 RNN 层后添加 25% 的 dropout。打印模型摘要:
model_comb = Sequential() model_comb.add(Reshape((look_back,1), \ input_shape = (look_back,))) model_comb.add(Conv1D(5, 3, activation='relu')) model_comb.add(SimpleRNN(32)) model_comb.add(Dropout(0.25)) model_comb.add(Dense(1)) model_comb.add(Activation('linear')) model.summary()输出将如下所示:
![图 5.42:模型摘要]()
图 5.42:模型摘要
-
使用
mean_squared_error损失函数和adam优化器编译模型。在训练数据上进行五个周期的训练,验证集占 10%,批量大小为 1:model_comb.compile(loss='mean_squared_error', \ optimizer='adam') model_comb.fit(trainX, trainY, epochs=5, \ batch_size=1, verbose=2, \ validation_split=0.1)输出将如下所示:
![图 5.43:训练和验证损失]()
图 5.43:训练和验证损失
-
使用
get_model_perf方法,打印模型的 RMSE:get_model_perf(model_comb)输出将如下所示:
Train RMSE: 0.03 RMSE Test RMSE: 0.03 RMSE -
绘制预测结果——整体视图和放大视图:
%matplotlib notebook plt.figure(figsize=[10,5]) plot_pred(model_comb)我们应该看到以下的预测图(虚线)与实际值图(实线)对比:
![图 5.44:预测与实际值对比]()
图 5.44:预测值与实际值的对比
放大的视图如下:

图 5.45:预测值(虚线)与实际值(实线)对比——详细视图
我们可以看到,模型在捕捉细微的模式上做得非常出色,并且在预测每日股票价格方面表现非常好。
注意
若要访问本节的源代码,请参阅 packt.live/2ZctArW。
你还可以在线运行此示例,网址为 packt.live/38EDOEA。你必须执行整个 Notebook 才能获得预期的结果。
6. LSTM、GRU 及高级 RNN
活动 6.01:亚马逊产品评论的情感分析
解决方案
-
读取
train和test数据集的文件。检查数据集的形状,并打印出train数据的前5条记录:import pandas as pd, numpy as np import matplotlib.pyplot as plt %matplotlib inline train_df = pd.read_csv("Amazon_reviews_train.csv") test_df = pd.read_csv("Amazon_reviews_test.csv") print(train_df.shape, train_df.shape) train_df.head(5)数据集的形状和头部信息如下:
![图 6.26:训练数据集中的前五条记录]()
图 6.26:训练数据集中的前五条记录
-
为了方便处理,在处理时将原始文本和标签从
train和test数据集中分开。你应该有4个变量,如下:train_raw包含训练数据的原始文本,train_labels包含训练数据的标签,test_raw包含测试数据的原始文本,test_labels包含测试数据的标签。打印出train文本中的前两条评论。train_raw = train_df.review_text.values train_labels = train_df.label.values test_raw = test_df.review_text.values test_labels = test_df.label.values train_raw[:2]前述代码的输出结果如下:
![图 6.27:来自训练数据集的原始文本]()
图 6.27:来自训练数据集的原始文本
-
使用 NLTK 的
word_tokenize来规范化大小写并对测试和训练文本进行分词(当然,记得先导入它——提示:使用列表推导式使代码更简洁)。如果你之前没有使用过分词器,可以从nltk下载punkt。打印训练数据集中的第一条评论,检查分词是否正常工作。import nltk nltk.download('punkt') from nltk.tokenize import word_tokenize train_tokens = [word_tokenize(review.lower()) \ for review in train_raw] test_tokens = [word_tokenize(review.lower()) \ for review in test_raw] print(train_tokens[0])分词后的数据如下所示:
![图 6.28:来自训练数据集的分词评论]()
图 6.28:来自
train数据集的分词评论 -
导入任何停用词(内建于 NLTK)和来自 string 模块的标点符号。定义一个函数(
drop_stop)来从任何输入的分词句子中移除这些标记。如果你之前没有使用过stopwords,可以从 NLTK 下载它:from string import punctuation stop_punct = list(punctuation) nltk.download("stopwords") from nltk.corpus import stopwords stop_nltk = stopwords.words("english") stop_final = stop_punct + stop_nltk def drop_stop(input_tokens): return [token for token in input_tokens \ if token not in stop_final] -
使用定义的函数(
drop_stop)从train和test文本中移除多余的停用词。打印出处理后的train文本中的第一条评论,检查该函数是否有效:train_tokens_no_stop = [drop_stop(sent) \ for sent in train_tokens] test_tokens_no_stop = [drop_stop(sent) \ for sent in test_tokens] print(train_tokens_no_stop[0])我们将得到如下输出:
['stuning', 'even', 'non-gamer', 'sound', 'track', 'beautiful', 'paints', 'senery', 'mind', 'well', 'would', 'recomend', 'even', 'people', 'hate', 'vid', 'game', 'music', 'played', 'game', 'chrono', 'cross', 'games', 'ever', 'played', 'best', 'music', 'backs', 'away', 'crude', 'keyboarding', 'takes', 'fresher', 'step', 'grate', 'guitars', 'soulful', 'orchestras', 'would', 'impress', 'anyone', 'cares', 'listen', '^_^'] -
使用 NLTK 的
PorterStemmer对train和test数据的标记进行词干提取:from nltk.stem import PorterStemmer stemmer_p = PorterStemmer() train_tokens_stem = [[stemmer_p.stem(token) for token in sent] \ for sent in train_tokens_no_stop] test_tokens_stem = [[stemmer_p.stem(token) for token in sent] \ for sent in test_tokens_no_stop] print(train_tokens_stem[0])结果应按如下方式打印:
['stune', 'even', 'non-gam', 'sound', 'track', 'beauti', 'paint', 'seneri', 'mind', 'well', 'would', 'recomend', 'even', 'peopl', 'hate', 'vid', 'game', 'music', 'play', 'game', 'chrono', 'cross', 'game', 'ever', 'play', 'best', 'music', 'back', 'away', 'crude', 'keyboard', 'take', 'fresher', 'step', 'grate', 'guitar', 'soul', 'orchestra', 'would', 'impress', 'anyon', 'care', 'listen', '^_^'] -
为
train和text评论创建字符串。这将帮助我们使用 Keras 中的工具来创建和填充序列。创建train_texts和test_texts变量。打印处理后的train数据中的第一条评论,以确认这一点:train_texts = [" ".join(txt) for txt in train_tokens_stem] test_texts = [" ".join(txt) for txt in test_tokens_stem] print(train_texts[0])上述代码的结果如下:
stune even non-gam sound track beauti paint seneri mind well would recommend even peopl hate vid game music play game chrono cross game ever play best music back away crude keyboard take fresher step grate guitar soul orchestra would impress anyon care listen ^_^ -
从 Keras 的文本预处理工具(
keras.preprocessing.text)中导入Tokenizer模块。定义一个10000的词汇量大小,并使用此词汇量实例化 tokenizer:from tensorflow.keras.preprocessing.text import Tokenizer vocab_size = 10000 tok = Tokenizer(num_words=vocab_size) -
在
train文本上拟合 tokenizer。这与 第四章 深度学习用于文本 – 嵌入 中的CountVectorizer类似,并训练词汇表。拟合后,使用 tokenizer 的texts_to_sequences方法对train和test数据集进行处理,生成它们的序列。打印训练数据中第一条评论的序列:tok.fit_on_texts(train_texts) train_sequences = tok.texts_to_sequences(train_texts) test_sequences = tok.texts_to_sequences(test_texts) print(train_sequences[0])编码后的序列如下:
[22, 514, 7161, 85, 190, 184, 1098, 283, 20, 11, 1267, 22, 56, 370, 9682, 114, 41, 71, 114, 8166, 1455, 114, 51, 71, 29, 41, 58, 182, 2931, 2153, 75, 8167, 816, 2666, 829, 719, 3871, 11, 483, 120, 268, 110] -
我们需要找到处理模型的序列的最佳长度。获取
train数据集中评论的长度列表,并绘制长度的直方图:seq_lens = [len(seq) for seq in train_sequences] plt.hist(seq_lens) plt.show()长度分布如下:
![图 6.29: 文本长度直方图]()
图 6.29: 文本长度直方图
-
现在,数据与我们在本章中使用的 IMDb 数据格式相同。使用
100的序列长度(定义maxlen = 100变量),并使用 Keras 的预处理工具(keras.preprocessing.sequence)中的pad_sequences方法,将train和test数据的序列限制为100。检查训练数据结果的形状:maxlen = 100 from tensorflow.keras.preprocessing.sequence import pad_sequences X_train = pad_sequences(train_sequences, maxlen=maxlen) X_test = pad_sequences(test_sequences, maxlen=maxlen) X_train.shape形状如下:
(25000, 100) -
要构建模型,从 Keras 导入所有必要的层(
embedding,spatial dropout,LSTM,dropout和dense),并导入Sequential模型。初始化Sequential模型:from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Embedding, SpatialDropout1D, Dropout, GRU, LSTM model_lstm = Sequential() -
添加一个
32维向量大小(output_dim)的嵌入层。添加一个40%丢弃率的空间丢弃层:model_lstm.add(Embedding(vocab_size, output_dim=32)) model_lstm.add(SpatialDropout1D(0.4)) -
构建一个具有
2层,每层64个单元的堆叠 LSTM 模型。添加一个40%丢弃率的 dropout 层:model_lstm.add(LSTM(64, return_sequences=True)) model_lstm.add(LSTM(64, return_sequences=False)) model_lstm.add(Dropout(0.4)) -
添加一个具有
32个神经元的 dense 层,使用relu激活函数,然后是一个50%丢弃率的 dropout 层,接着是另一个具有32个神经元的 dense 层,使用relu激活函数,最后再添加一个丢弃率为50%的 dropout 层:model_lstm.add(Dense(32, activation='relu')) model_lstm.add(Dropout(0.5)) model_lstm.add(Dense(32, activation='relu')) model_lstm.add(Dropout(0.5)) -
添加一个最终的 dense 层,包含一个具有
sigmoid激活函数的神经元,并编译模型。打印模型摘要:model_lstm.add(Dense(1, activation='sigmoid')) model_lstm.compile(loss='binary_crossentropy', \ optimizer='rmsprop', \ metrics=['accuracy']) model_lstm.summary()模型的摘要如下:
![图 6.30: 堆叠 LSTM 模型摘要]()
图 6.30: 堆叠 LSTM 模型摘要
-
使用
20%的验证集拆分和128的批量大小在训练数据上拟合模型。训练5个epochs:history_lstm = model_lstm.fit(X_train, train_labels, \ batch_size=128, \ validation_split=0.2, \ epochs = 5)我们将获得以下训练输出:
![图 6.31: 堆叠 LSTM 模型训练输出]()
图 6.31: 堆叠 LSTM 模型训练输出
-
使用模型的
predict_classes方法对测试集进行预测。然后,打印混淆矩阵:from sklearn.metrics import accuracy_score, confusion_matrix test_pred = model_lstm.predict_classes(X_test) print(confusion_matrix(test_labels, test_pred))我们将获得以下结果:
[[10226, 1931], [ 1603, 11240]] -
使用
scikit-learn中的accuracy_score方法,计算测试集的准确率。print(accuracy_score(test_labels, test_pred))我们得到的准确率是:
0.85864
如我们所见,准确率约为 86%,并且查看混淆矩阵(步骤 18的输出),模型在预测两类时都做得相当不错。我们在没有进行任何超参数调优的情况下得到了这个准确率。你可以调整超参数,以获得显著更高的准确率。
注意
要访问该特定部分的源代码,请参考packt.live/3fpo0YI。
你也可以在 packt.live/2Wi75QH 上在线运行这个例子。你必须执行整个 Notebook,才能得到所需的结果。
7. 生成对抗网络
活动 7.01:为 MNIST 时尚数据集实现 DCGAN
解决方案
-
打开一个新的 Jupyter Notebook,并将其命名为
Activity 7.01。导入以下库包:# Import the required library functions import numpy as np import matplotlib.pyplot as plt from matplotlib import pyplot import tensorflow as tf from tensorflow.keras.layers import Input from tensorflow.keras.initializers import RandomNormal from tensorflow.keras.models import Model, Sequential from tensorflow.keras.layers \ import Reshape, Dense, Dropout, Flatten,Activation from tensorflow.keras.layers import LeakyReLU,BatchNormalization from tensorflow.keras.layers import Conv2D, UpSampling2D,Conv2DTranspose from tensorflow.keras.datasets import fashion_mnist from tensorflow.keras.optimizers import Adam -
创建一个函数,用于从时尚 MNIST 数据中生成真实数据样本:
# Function to generate real data samples def realData(batch): # Get the MNIST data (X_train, _), (_, _) = fashion_mnist.load_data() # Reshaping the input data to include channel X = X_train[:,:,:,np.newaxis] # normalising the data to be between 0 and 1 X = (X.astype('float32') - 127.5)/127.5 # Generating a batch of data imageBatch = X[np.random.randint(0, X.shape[0], \ size=batch)] return imageBatch该函数的输出是 MNIST 数据批次。请注意,我们通过减去
127.5(这是最大像素值的一半)并除以相同的值来规范化输入数据。这有助于更快地收敛解决方案。 -
现在,让我们从 MNIST 数据集生成一组图像:
# Generating a set of sample images fashionData = realData(25)你应该得到以下输出:
![图 7.36:从 MNIST 生成图像]()
图 7.36:从 MNIST 生成图像
-
现在,让我们使用
matplotlib来可视化这些图像:# for j in range(5*5): pyplot.subplot(5,5,j+1) # turn off axis pyplot.axis('off') pyplot.imshow(fashionData[j,:,:,0],cmap='gray_r')你应该得到一个类似于这里所示的输出:
![图 7.37:绘制的图像]()
图 7.37:绘制的图像
从输出中,我们可以看到几件时尚商品的可视化。我们可以看到这些图像位于白色背景的中央。这些图像就是我们将尝试重建的对象。
-
现在,让我们定义生成生成器网络输入的函数。输入是从随机均匀分布中生成的随机数据点:
# Function to generate inputs for generator function def fakeInputs(batch,infeats): # Generate random noise data with shape (batch,input features) x_fake = np.random.uniform(-1,1,size=[batch,infeats]) return x_fake这个函数生成的是从随机分布中采样的假数据作为输出。
-
让我们定义一个构建生成器网络的函数:
Activity7.01.ipynb # Function for the generator model def genModel(infeats): # Defining the Generator model Genmodel = Sequential() Genmodel.add(Dense(512,input_dim=infeats)) Genmodel.add(Activation('relu')) Genmodel.add(BatchNormalization()) # second layer of FC => RElu => BN layers Genmodel.add(Dense(7*7*64)) Genmodel.add(Activation('relu')) Genmodel.add(BatchNormalization()) The complete code for this step can be found at https://packt.live/3fpobDm构建生成器网络与构建任何 CNN 网络类似。在这个生成器网络中,我们将使用转置卷积方法来对图像进行上采样。在这个模型中,我们可以看到转置卷积的逐步使用。最初的输入维度是 100,这就是我们的输入特征。MNIST 数据集的维度是批量大小 x 28 x 28。因此,我们已经对数据进行了两次上采样,以便得到输出为批量大小 x 28 x 28。
-
接下来,我们定义一个将用于创建假样本的函数:
# Function to create fake samples using the generator model def fakedataGenerator(Genmodel,batch,infeats): # first generate the inputs to the model genInputs = fakeInputs(batch,infeats) """ use these inputs inside the generator model \ to generate fake distribution """ X_fake = Genmodel.predict(genInputs) return X_fake在这个函数中,我们只返回
X变量。该函数的输出是假的数据集。 -
定义我们将在许多函数中使用的参数,并附上生成器网络的摘要:
# Define the arguments like batch size and input feature batch = 128 infeats = 100 Genmodel = genModel(infeats,) Genmodel.summary()你应该得到以下输出:
![图 7.38:生成模型总结]()
图 7.38:生成模型总结
从总结中可以看到,每次转置卷积操作时输入噪声的维度是如何变化的。最终,我们得到的输出维度与真实数据集相同,
(None, 28, 28, 1)。 -
让我们使用生成器函数生成一个训练前的假样本:
# Generating a fake sample and printing the shape fake = fakedataGenerator(Genmodel,batch,infeats) fake.shape你应该得到以下输出:
(128, 28, 28, 1) -
现在,让我们绘制生成的假样本:
# Plotting the fake sample plt.imshow(fake[1, :, :, 0], cmap='gray_r')你应该得到类似以下的输出:
![图 7.39:假样本的输出]()
图 7.39:假样本的输出
这是训练前假样本的图像。在训练之后,我们希望这些样本看起来像我们在本活动中之前可视化的 MNIST 时尚样本。
-
将判别器模型构建为一个函数。网络架构将类似于 CNN 架构:
Activity7.01.ipynb # Descriminator model as a function def discModel(): Discmodel = Sequential() Discmodel.add(Conv2D(32,kernel_size=(5,5),strides=(2,2),\ padding='same',input_shape=(28,28,1))) Discmodel.add(LeakyReLU(0.2)) # second layer of convolutions Discmodel.add(Conv2D(64, kernel_size=(5,5), strides=(2, 2), \ padding='same')) Discmodel.add(LeakyReLU(0.2)) The full code for this step can be found at https://packt.live/3fpobDm在判别器网络中,我们已经包含了所有必要的层,如卷积操作和
LeakyReLU。请注意,最后一层是 sigmoid 层,因为我们希望输出的是样本是否真实的概率(1 表示真实,0 表示假)。 -
打印判别器网络的总结:
# Print the summary of the discriminator model Discmodel = discModel() Discmodel.summary()你应该得到以下输出:
![图 7.40:判别器模型总结]()
图 7.40:判别器模型总结
-
将 GAN 模型定义为一个函数:
# Define the combined generator and discriminator model, for updating the generator def ganModel(Genmodel,Discmodel): # First define that discriminator model cannot be trained Discmodel.trainable = False Ganmodel = Sequential() # First adding the generator model Ganmodel.add(Genmodel) """ Next adding the discriminator model without training the parameters """ Ganmodel.add(Discmodel) """ Compile the model for loss to optimise the Generator model """ Ganmodel.compile(loss='binary_crossentropy',\ optimizer = 'adam') return GanmodelGAN 模型的结构与我们在练习 7.05中开发的结构相似,实现 DCGAN。
-
现在,是时候调用 GAN 函数了:
# Initialise the GAN model gan_model = ganModel(Genmodel,Discmodel) # Print summary of the GAN model gan_model.summary()请注意,GAN 模型的输入是之前定义的生成器模型和判别器模型。你应该得到以下输出:
![图 7.41:GAN 模型总结]()
图 7.41:GAN 模型总结
请注意,GAN 模型中每一层的参数等同于生成器和判别器模型的参数。GAN 模型只是我们之前定义的两个模型的封装器。
-
使用以下代码定义训练网络的 epoch 数:
# Defining the number of epochs nEpochs = 5000 -
现在,我们可以开始训练网络的过程:
Activity7.01.ipynb # Train the GAN network for i in range(nEpochs): """ Generate samples equal to the batch size from the real distribution """ x_real = realData(batch) #Generate fake samples using the fake data generator function x_fake = fakedataGenerator(Genmodel,batch,infeats) # Concatenating the real and fake data X = np.concatenate([x_real,x_fake]) #Creating the dependent variable and initializing them as '0' Y = np.zeros(batch * 2) The complete code for this step can be found on https://packt.live/3fpobDm这里需要注意的是,判别器模型使用真实和假样本的训练与 GAN 模型的训练是同时进行的。唯一的区别是,GAN 模型的训练不会更新判别器模型的参数。另一个需要注意的是,在 GAN 内部,假样本的标签将是 1,以生成较大的损失项,这些损失项将通过判别器网络反向传播,以更新生成器参数。我们还会在每 50 个 epochs 时显示 GAN 的预测概率。在计算概率时,我们结合一个真实数据样本和一个假数据样本,然后取预测概率的均值。我们还会保存生成的图像副本。
你应该得到类似以下的输出:
Discriminator probability:0.5276428461074829 Discriminator probability:0.5038391351699829 Discriminator probability:0.47621315717697144 Discriminator probability:0.48467564582824707 Discriminator probability:0.5270703434944153 Discriminator probability:0.5247280597686768 Discriminator probability:0.5282968282699585我们还来看一下在不同训练周期生成的一些图表:
![图 7.42:训练过程中生成的图像]()
图 7.42:训练过程中生成的图像
从前面的图表中,我们可以看到训练过程的进展。我们看到在第 100 个周期时,图表大部分仍是噪声;到第 600 个周期时,时尚物品的形态开始变得更加明显;在第 1,500 个周期时,我们可以看到假图像与时尚数据集非常相似。
注意:
你可以通过访问
packt.live/2W1FjaI更仔细地查看这些图像。 -
现在,让我们看看训练后生成的图像:
# Images generated after training x_fake = fakedataGenerator(Genmodel,25,infeats) # Displaying the plots for j in range(5*5): pyplot.subplot(5,5,j+1) # turn off axis pyplot.axis('off') pyplot.imshow(x_fake[j,:,:,0],cmap='gray_r')你应该得到类似以下的输出:
![图 7.43:训练过程后生成的图像]()
图 7.43:训练过程后生成的图像
从训练准确率水平可以看到,判别器模型的准确率大约在 0.50 左右,这就是理想的范围。生成器的目的是创造看起来像真实图像的假图像。当生成器生成的图像与真实图像非常相似时,判别器会混淆图像是来自真实分布还是假分布。这一现象体现在判别器的准确率大约为 50%,这是理想的水平。
注意:
要访问此特定部分的源代码,请参考packt.live/3fpobDm。
本节目前没有在线互动示例,需要在本地运行。






















































































浙公网安备 33010602011771号