深度学习快速参考-全-
深度学习快速参考(全)
零、前言
《深度学习快速参考》演示了使用深度学习的快速实用方法。 它着重于现实生活中的问题,并且仅提供了足够的理论和数学知识来加深读者对该主题的理解。 深度学习是机器学习中令人兴奋的快速节奏分支,但它也是一个可以涉足的领域。 在这个领域,每天都会进行大量的详细而复杂的研究,而这可能会令人不知所措。 在本书中,我着重向您传授将深度学习应用于各种实际问题的技能。 我对这本书的最大希望是,它将为您提供使用深度学习技术解决机器学习问题所需的工具。
这本书是给谁的
我是一名实践中的数据科学家,我在写这本书时牢记其他实践中的数据科学家和机器学习工程师。 如果您是应用深度学习的软件工程师,那么这本书也很适合您。
如果您是一名深度学习研究人员,那么这本书并不适合您。 但是,您仍然应该拿起副本,以便批评这本书缺乏证明和数学上的严格性。
如果您是一名学者或教育家,那么这本书绝对适合您。 在过去的 3 年中,我在伊利诺伊大学斯普林菲尔德分校教授了数据科学的调查数据(去草原之星!),这样做,我有机会启发了许多未来的机器学习人员。 这种经历启发了我创作这本书。 我认为这样的书是帮助学生提高对一个非常复杂的主题的兴趣的好方法。
本书涵盖的内容
第 1 章“深度学习的基础知识”,回顾了有关神经网络操作的一些基础知识,涉及了优化算法,讨论了模型验证,并讨论了建立开发环境的内容。 适用于构建深度神经网络。
第 2 章“使用深度学习解决回归问题”,您可以构建非常简单的神经网络来解决回归问题,并研究更深更复杂的模型对这些问题的影响。
第 3 章“使用 TensorBoard 监视网络训练”让您立即开始使用 TensorBoard,这是监视和调试未来模型的绝佳应用。
第 4 章“使用深度学习解决二分类问题”帮助您使用深度学习解决二分类问题。
第 5 章“使用 Keras 解决多分类问题”,带您进行多分类并探讨它们之间的区别。 它还讨论了管理过拟合和最安全的选择。
第 6 章“超参数优化”显示了两种独立的模型调整方法,一种是众所周知的且经过实战测试的方法,而另一种是最新方法。
第 7 章“从头开始训练 CNN”教您如何使用卷积网络对图像进行分类。
第 8 章“使用预训练的 CNN 的迁移学习”描述了如何应用迁移学习来从图像分类器中获得惊人的表现,即使数据很少。
第 9 章“从头开始训练 RNN”,讨论 RNN 和 LSTMS,以及如何将其用于时间序列预测问题。
第 10 章“从头开始用词嵌入训练 LSTM”继续我们关于 LSTM 的讨论,这次讨论的是自然语言分类任务。
第 11 章“训练 Seq2Seq 模型”帮助我们使用序列对模型进行序列化以进行机器翻译。
第 12 章“使用深度强化学习”引入了深度强化学习,并构建了可以为自治智能体提供动力的深度 Q 网络。
第 13 章“生成对抗网络”解释了如何使用生成对抗网络生成令人信服的图像。
充分利用这本书
- 我假设您已经对更传统的数据科学和预测建模技术(例如线性/逻辑回归和随机森林)有丰富的经验。 如果这是您第一次学习机器学习,那么对您来说可能有点困难。
- 我还假定您至少具有使用 Python 进行编程的经验,或者至少具有其他编程语言(如 Java 或 C++)。
- 深度学习是计算密集型的,我们在这里构建的某些模型需要 NVIDIA GPU 在合理的时间内运行。 如果您没有快速的 GPU,则可能希望在 Amazon Web Services 或 Google Cloud Platform 上使用基于 GPU 的云实例。
使用约定
本书中使用了许多文本约定。
CodeInText:指示文本,数据库表名称,文件夹名称,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄中的代码字。 这是一个示例:“这正是ModelCheckpoint回调为我们所做的。”
代码块设置如下:
def binary_accuracy(y_true, y_pred):
return K.mean(K.equal(y_true, K.round(y_pred)), axis=-1)
当我们希望引起您对代码块特定部分的注意时,相关的行或项目以粗体显示:
def build_network(input_features=None):
inputs = Input(shape=(input_features,), name="input")
x = Dense(32, activation='relu', name="hidden1")(inputs)
x = Dense(32, activation='relu', name="hidden2")(x)
x = Dense(32, activation='relu', name="hidden3")(x)
x = Dense(32, activation='relu', name="hidden4")(x)
x = Dense(16, activation='relu', name="hidden5")(x)
prediction = Dense(1, activation='linear', name="final")(x)
model = Model(inputs=inputs, outputs=prediction)
model.compile(optimizer='adam', loss='mean_absolute_error')
return model
任何命令行输入或输出的编写方式如下:
model-weights.00-0.971304.hdf5
model-weights.02-0.977391.hdf5
model-weights.05-0.985217.hdf5
粗体:表示新术语,重要单词或您在屏幕上看到的单词。 例如,菜单或对话框中的单词会出现在这样的文本中。 这是一个示例:“从管理面板中选择系统信息。”
警告或重要提示如下所示。
提示和技巧如下所示。
一、深度学习的基础
欢迎使用《深度学习快速参考》! 在本书中,我将尝试使需要解决深度学习问题的数据科学家,机器学习工程师和软件工程师更容易使用,实用和使用深度学习技术。 如果您想训练自己的深度神经网络并且陷入困境,那么本指南很有可能会有所帮助。
本书动手了,旨在作为实用指南,可以帮助您快速解决问题。 它主要供需要使用深度学习解决问题的经验丰富的机器学习工程师和数据科学家使用。 除了本章(其中提供了一些我们将要开始使用的术语,框架和背景知识)之外,它并不意味着要按顺序阅读。 每章均包含一个实际示例,并附有代码,一些最佳实践和安全选择。 我们希望您能跳到所需的章节并开始使用。
本书不会深入研究深度学习和神经网络的理论。 有许多可以提供这种背景知识的精彩书籍,我强烈建议您至少阅读其中一本(也许是参考书目,也可以只是建议)。 我们希望提供足够的理论和数学直觉来帮助您入门。
我们将在本章介绍以下主题:
- 深度神经网络架构
- 深度学习的优化算法
- 深度学习框架
- 构建用于深度学习的数据集
深度神经网络架构
深度神经网络架构的结构可能会因网络的应用而有很大差异,但它们都有一些基本组件。 在本节中,我们将简要讨论这些组件。
在本书中,我将深度神经网络定义为一个具有多个隐藏层的网络。 除此之外,我们不会尝试将成员限制为深度学习俱乐部。 因此,我们的网络可能只有不到 100 个神经元,甚至可能有数百万个。 我们可能会使用特殊的神经元层,包括卷积和循环层,但尽管如此,我们仍将所有这些都称为神经元。
神经元
神经元是神经网络的原子单位。 有时这是受到生物学启发的。 但是,这是另一本书的主题。 神经元通常排列成层。 在本书中,如果我指的是特定的神经元,则将使用符号n[k]^l,其中l是神经元所在的层, k是神经元编号 。 由于我们将使用遵循第 0 个表示法的编程语言,因此我的表示法也将基于第 0 个表示法。
大多数神经元的核心是两个共同起作用的函数:线性函数和激活函数。 让我们从较高的角度看一下这两个组成部分。
神经元线性函数
神经元的第一部分是线性函数,其输出是输入的总和,每个输入乘以一个系数。 这个函数实际上或多或少是线性回归。 这些系数通常在神经网络中称为权重。 例如,给定某些神经元,其输入特征为x1,x2和x3,输出z,则此线性分量或神经元线性函数将简单地为:

在给定数据的情况下,θ[1], θ[2], ..., θ[n]是权重或系数,b是偏差项。
神经元激活函数
神经元的第二个函数是激活函数,其任务是在神经元之间引入非线性。 Sigmoid 激活是一种常用的激活,您可能会通过逻辑回归来熟悉它。 它将神经元的输出压缩到输出空间,其中z的非常大的值被驱动为 1,而z的非常小的值被驱动为 0。
sigmoid 函数如下所示:


事实证明,激活函数对于中间神经元非常重要。 没有它,可以证明一堆具有线性激活的神经元(实际上不是激活,或更正式地说是z = z的激活函数)实际上只是一个线性函数。
在这种情况下,单个线性函数是不理想的,因为在许多情况下,我们的网络可能未针对当前问题指定。 也就是说,由于输入特征和目标变量之间的非线性关系(我们正在预测),网络无法很好地对数据建模。
不能用线性函数建模的函数的典型示例是排他的OR函数,如下图所示:

其他常见的激活函数是tanh函数和 ReLu 或整流线性激活。
双曲正切或tanh函数如下所示:


对于中间层,tanh通常比 Sigmoid 更好。 您可能会看到,tanh的输出将在[-1, 1]之间,而 Sigmoid 曲线的输出将为[0, 1]。 这种额外的宽度可为消失或爆炸的梯度问题提供一定的弹性,我们将在后面详细介绍。 到目前为止,仅需知道消失的梯度问题就可以使网络在早期的层中收敛非常慢(如果有的话)。 因此,使用tanh的网络趋于比使用 Sigmoid 激活的网络收敛更快。 也就是说,它们仍然不如 ReLu 快。
ReLu,或直线激活,简单定义为:

这是一个安全的赌注,我们在本书中的大部分时间都会使用它。 ReLu 不仅易于计算和微分,而且还可以抵抗消失的梯度问题。 ReLu 的唯一缺点是它的一阶导数未精确定义为 0。包括泄漏的 ReLu 在内的变体在计算上更加困难,但针对此问题更健壮。
为了完整起见,以下是 ReLu 的一些明显图表:

深度学习中的损失和成本函数
每个机器学习模型实际上都是从成本函数开始的。 简单来说,成本函数可让您衡量模型对训练数据的拟合程度。 在本书中,我们将损失函数定义为训练集中单个观测值的拟合正确性。 这样,成本函数通常将是整个训练集中损失的平均值。 稍后,当我们介绍每种类型的神经网络时,我们将重新讨论损失函数。 但是,请快速考虑线性回归的成本函数作为示例:

在这种情况下,损失函数为(y_hat - y)^2,这实际上是平方误差。 因此,我们的cost函数J实际上只是均方误差,或整个数据集的均方误差的平均值。 按照惯例,添加了项 1/2 以使某些微积分更干净。
正向传播过程
正向传播是我们尝试使用单个观测值中存在的特征预测目标变量的过程。 想象一下,我们有一个两层神经网络。 在正向传播过程中,我们将从观察中出现的特征x[1], x[2], ..., x[n]开始,然后将这些特征乘以它们在第 1 层中的关联系数,并为每个神经元添加一个偏差项。 之后,我们会将输出发送到神经元的激活。 之后,输出将被发送到下一层,依此类推,直到到达网络的末端,然后剩下网络的预测:

反向传播过程
一旦正向传播完成,我们就可以对每个数据点进行网络预测。 我们也知道数据点的实际值。 通常,将预测定义为y_hat,而将目标变量的实际值定义为y。
一旦y和y_hat都已知,就可以使用成本函数计算网络误差。 回想一下,代价函数是loss函数的平均值。
为了使学习在网络中发生,网络的误差信号必须从最后一层到最后一层通过网络层向后传播。 我们反向传播的目标是在网络中向后传播该误差信号,同时随着信号的传播使用误差信号来更新网络权重。 在数学上,要做到这一点,我们需要对权重进行微调,以使成本函数最小,从而最小化成本函数。 此过程称为梯度下降。
梯度是误差函数相对于网络内每个权重的偏导数。 可以使用链法则和上面各层的梯度逐层计算每个权重的梯度。
一旦知道了每一层的梯度,我们就可以使用梯度下降算法来最小化cost函数。
梯度下降将重复此更新,直到网络的误差最小化并且该过程收敛为止:

梯度下降算法将梯度乘以称为alpha的学习率,然后从每个权重的当前值中减去该值。 学习率是一个超参数。
随机和小批量梯度下降
上一节中描述的算法假定整个数据集都进行正向和相应的反向传递,因此将其称为批梯度下降。
进行梯度下降的另一种可能方法是一次使用一个数据点,并随着我们的更新网络权重。 此方法可能有助于加快网络可能停止收敛的鞍点附近的收敛速度。 当然,仅单个点的误差估计可能无法很好地近似于整个数据集的误差。
解决此问题的最佳解决方案是使用小型批量梯度下降,其中我们将采用称为小型批量的数据的随机子集来计算误差并更新网络权重。 这几乎总是最好的选择。 它还有一个额外的好处,即可以将非常大的数据集自然地拆分为多个块,这些块可以更容易地在计算机的内存中甚至跨计算机的内存中进行管理。
这是对神经网络最重要部分之一的极高层次的描述,我们认为这与本书的实际性质相符。 实际上,大多数现代框架都为我们处理了这些步骤。 但是,至少在理论上,它们无疑是值得了解的。 我们鼓励读者在时间允许的情况下更深入地进行向前和向后传播。
深度学习的优化算法
梯度下降算法不是唯一可用于优化网络权重的优化算法,但它是大多数其他算法的基础。 虽然了解每种优化算法都有可能获得博士学位,但我们将为一些最实用的内容专门介绍几句话。
梯度下降和动量
通过使用具有动量的梯度下降,可以通过增加方向学习的速度来加快梯度下降,从而使梯度在方向上保持恒定,而在方向缓慢学习时,梯度会在方向上波动。 它允许梯度下降的速度增加。
动量的工作原理是引入速度项,并在更新规则中使用该项的加权移动平均值,如下所示:


在动量的情况下,最通常将β设置为 0.9,通常这不是需要更改的超参数。
RMSProp 算法
RMSProp 是另一种算法,可以通过跨网络权重表示的多维空间,通过在某些方向上加快学习速度,并在其他方向上抑制振荡来加快梯度下降:



这具有在vₜ大的方向上进一步减少振荡的效果。
Adam 优化器
Adam 是已知表现最好的优化器之一,这是我的首选。 它可以很好地解决各种问题。 它将动量和 RMSProp 的最佳部分组合到一个更新规则中:




其中ε很小,可以防止被 0 除。
亚当通常是一个不错的选择,当您进行原型设计时,这是一个很好的起点,因此,从亚当开始可以节省一些时间。
深度学习框架
虽然仅使用 Python 的numpy从头开始构建和训练深度神经网络是绝对可能的,但这将花费大量的时间和代码。 在几乎每种情况下,使用深度学习框架都更加实用。
在本书中,我们将使用 TensorFlow 和 Keras 来使开发深度神经网络变得更加轻松和快捷。
什么是 TensorFlow?
TensorFlow 是一个可用于快速构建深度神经网络的库。 在 TensorFlow 中,我们到目前为止已涵盖的数学运算被表示为节点。 这些节点之间的边缘是张量或多维数据数组。 给定定义为图和损失函数的神经网络,TensorFlow 可以自动计算网络的梯度并优化图以最小化损失函数。
TensorFlow 是 Google 在 2015 年发布的一个开源项目。此后,它已经获得了很大的关注,并拥有庞大的用户社区。 虽然 TensorFlow 提供 Java,C++,Go 和 Python 的 API,但我们仅介绍 Python API。 本书使用了 Python API,因为它既是最常用的,也是开发新模型时最常用的 API。
通过在一个或多个图形处理单元上执行这些计算,TensorFlow 可以大大加快计算速度。 GPU 计算提供的加速已成为现代深度学习中的必要条件。
什么是 Keras?
尽管在 TensorFlow 中构建深度神经网络要比从头开始做起来容易得多,但 TensorFlow 仍然是一个非常底层的 API。 Keras 是一个高级 API,允许我们使用 TensorFlow(或 Theano 或 Microsoft 的 CNTK)快速构建深度学习网络。
用 Keras 和 TensorFlow 构建的模型是便携式的,也可以在本机 TensorFlow 中进行训练或使用。 TensorFlow 中构建的模型可以加载到 Keras 中并在其中使用。
TensorFlow 的流行替代品
那里还有许多其他很棒的深度学习框架。 我们之所以选择 Keras 和 TensorFlow,主要是因为其受欢迎程度,易用性,支持的可用性以及生产部署的准备就绪。 无疑还有其他有价值的选择。
我最喜欢的 TensorFlow 替代品包括:
- Apache MXNet:一个非常高表现的框架,带有一个名为 Gluon 的新命令式接口
- PyTorch:Facebook 最初开发的一种非常新颖且有希望的架构
- CNTK:也可以与 Keras 一起使用的 Microsoft 深度学习框架
尽管我确实坚信 Keras 和 TensorFlow 是本书的正确选择,但我也想承认这些出色的框架以及每个项目对领域做出的贡献。
TensorFlow 和 Keras 的 GPU 要求
在本书的其余部分,我们将使用 Keras 和 TensorFlow。 我们将探索的大多数示例都需要 GPU 来加速。 包括 TensorFlow 在内的大多数现代深度学习框架都使用 GPU 极大地加速了网络训练期间所需的大量计算。 如果没有 GPU,我们讨论的大多数模型的训练时间将过长。
如果您没有安装有 GPU 的计算机,则可以从包括 Amazon 的 Amazon Web Services 和 Google 的 Google Cloud Platform 在内的各种云提供商处租用基于 GPU 的计算实例。 对于本书中的示例,我们将在运行 Ubuntu Server 16.04 的 Amazon EC2 中使用p2.xlarge实例。 p2.xlarge 实例提供了具有 2,496 个 CUDA 内核的 Nvidia Tesla K80 GPU,这将使我们在本书中显示的模型的运行速度甚至比非常高端的台式计算机所能达到的速度快得多。
安装 Nvidia CUDA 工具包和 cuDNN
由于您可能会在深度学习工作中使用基于云的解决方案,因此我提供了一些说明,这些说明可帮助您在 Ubuntu Linux 上快速启动并运行,Ubuntu Linux 在各个云提供商中普遍可用。 也可以在 Windows 上安装 TensorFlow 和 Keras。 从 TensorFlow v1.2 开始,TensorFlow 不幸地不支持 OSX 上的 GPU。
在使用 GPU 之前,必须先安装 NVidia CUDA 工具包和 cuDNN 。 我们将安装 CUDA Toolkit 8.0 和 cuDNN v6.0,建议与 TensorFlow v1.4 一起使用。 在您阅读完本段之前,很有可能会发布新版本,因此,请访问 www.tensorflow.org 以获取最新的必需版本。
我们将从在 Ubuntu 上安装build-essential包开始,该包包含编译 C++ 程序所需的大部分内容。 代码在这里给出:
sudo apt-get update
sudo apt-get install build-essential
接下来,我们可以下载并安装 CUDA Toolkit。 如前所述,我们将安装 8.0 版及其相关补丁。 您可以在这个页面中找到最适合您的 CUDA 工具包。
wget https://developer.nvidia.com/compute/cuda/8.0/Prod2/local_installers/cuda_8.0.61_375.26_linux-run
sudo sh cuda_8.0.61_375.26_linux-run # Accept the EULA and choose defaults
wget https://developer.nvidia.com/compute/cuda/8.0/Prod2/patches/2/cuda_8.0.61.2_linux-run
sudo sh cuda_8.0.61.2_linux-run # Accept the EULA and choose defaults
CUDA 工具包现在应该安装在以下路径中:/usr/local/cuda。 您需要添加一些环境变量,以便 TensorFlow 可以找到它。 您可能应该考虑将这些环境变量添加到~/.bash_profile,以便在每次登录时进行设置,如以下代码所示:
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/cuda/lib64"
export CUDA_HOME="/usr/local/cuda"
此时,您可以通过执行以下命令来测试一切是否正常:nvidia-smi。 输出应类似于以下内容:
$nvidia-smi
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 375.26 Driver Version: 375.26 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
|===============================+======================+======================|
| 0 Tesla K80 Off | 0000:00:1E.0 Off | 0 |
| N/A 41C P0 57W / 149W | 0MiB / 11439MiB | 99% Default |
+-------------------------------+----------------------+----------------------+
最后,我们需要安装 cuDNN,这是 NVIDIA CUDA 深度神经网络库。
首先,将 cuDNN 下载到本地计算机。 为此,您需要在 NVIDIA 开发人员网络中注册为开发人员。 您可以在 cuDNN 主页 上找到 cuDNN。 将其下载到本地计算机后,可以使用scp将其移至 EC2 实例。 虽然确切的说明会因云提供商的不同而有所差异,但是您可以在这个页面中找到有关通过 SSH/SCP 连接到 AWS EC2 的其他信息。 。
将 cuDNN 移至 EC2 映像后,可以使用以下代码解压缩文件:
tar -xzvf cudnn-8.0-linux-x64-v6.0.tgz
最后,使用以下代码将解压缩的文件复制到其适当的位置:
sudo cp cuda/include/cudnn.h /usr/local/cuda/include/
sudo cp cuda/lib64/* /usr/local/cuda/lib64
我不清楚为什么 CUDA 和 cuDNN 分别分发,为什么 cuDNN 需要注册。 cuDNN 的下载过程和手动安装过于复杂,这确实是深度学习中最大的谜团之一。
安装 Python
我们将使用virtualenv创建一个隔离的 Python 虚拟环境。 尽管这不是严格必要的,但这是一种极好的实践。 这样,我们会将该项目的所有 Python 库保存在一个独立的隔离环境中,该环境不会干扰系统 Python 的安装。 此外,virtualenv环境将使以后打包和部署我们的深度神经网络更加容易。
首先,使用 Ubuntu 中的 aptitude 包管理器安装Python,pip和virtualenv。 以下是代码:
sudo apt-get install python3-pip python3-dev python-virtualenv
现在,我们可以为我们的工作创建虚拟环境。 我们将所有虚拟环境文件保存在名为~/deep-learn的文件夹中。 您可以自由选择该虚拟环境的任何名称。 以下代码显示了如何创建虚拟环境:
virtualenv --no-site-packages -p python3 ~/deep-learn
如果您是一位经验丰富的 Python 开发人员,您可能已经注意到我已将环境设置为默认为 Python3.x。 肯定不是必须的,并且 TensorFlow/Keras 都支持 Python 2.7。 也就是说,作者感到 Python 社区有道德义务支持现代版本的 Python。
现在已经创建了虚拟环境,您可以按以下方式激活它:
$source ~/deep-learn/bin/activate
(deep-learn)$ # notice the shell changes to indicate the virtualenv
此时,每次登录时都需要激活要使用的虚拟环境。如果您想始终输入刚刚创建的虚拟环境,可以将source命令添加到~/.bash_profile。
现在我们已经配置了虚拟环境,我们可以根据需要在其中添加 Python 包。 首先,请确保我们具有 Python 包管理器pip的最新版本:
easy_install -U pip
最后,我建议安装 IPython,它是一个交互式 Python shell,可简化开发。
pip install ipython
就是这样。 现在我们准备安装 TensorFlow 和 Keras。
安装 TensorFlow 和 Keras
在我们共同完成所有工作之后,您将很高兴看到现在安装 TensorFlow 和 Keras 多么简单。
让我们开始安装 TensorFlow
TensorFlow 的安装可以使用以下代码完成:
pip install --upgrade tensorflow-gpu
确保pip install tensorflow-gpu。 如果您通过 pip 安装 TensorfFow(不带-gpu),则将安装仅 CPU 版本。
在安装 Keras 之前,让我们测试一下 TensorFlow 安装。 为此,我将使用 TensorFlow 网站和 IPython 解释器中的一些示例代码。
通过在 bash 提示符下键入 IPython ,启动 IPython 解释程序。 IPython 启动后,让我们尝试导入 TensorFlow。 输出如下所示:
In [1]: import tensorflow as tf
In [2]:
如果导入 TensorFlow 导致错误,请对到目前为止已执行的步骤进行故障排除。 大多数情况下,当无法导入 TensorFlow 时,可能未正确安装 CUDA 或 cuDNN。
现在我们已经成功安装了 TensorFlow,我们将在 IPython 中运行一小段代码,以验证我们可以在 GPU 上运行计算:
a = tf.constant([1.0,</span> 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3], name='a')
b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b')
c = tf.matmul(a, b)
sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))
print(sess.run(c))
如果一切顺利,我们将看到许多迹象表明正在使用我们的 GPU。 我在此处提供了一些输出,并重点介绍了提请您注意的证据。 根据硬件,您的输出可能会有所不同,但是您应该看到类似的证据,如下所示:
/job:localhost/replica:0/task:0/device:GPU:0 -> device: 0, name: Tesla K80, pci bus id: 0000:00:1e.0, compute capability: 3.7
MatMul: (MatMul): /job:localhost/replica:0/task:0/device:GPU:0
: I tensorflow/core/common_runtime/placer.cc:874] MatMul: (MatMul)/job:localhost/replica:0/task:0/device:GPU:0
b: (Const): /job:localhost/replica:0/task:0/device:GPU:0
: I tensorflow/core/common_runtime/placer.cc:874] b: (Const)/job:localhost/replica:0/task:0/device:GPU:0
a: (Const): /job:localhost/replica:0/task:0/device:GPU:0
: I tensorflow/core/common_runtime/placer.cc:874] a: (Const)/job:localhost/replica:0/task:0/device:GPU:0
[[ 22\. 28.]
[ 49\. 64.]]
在前面的输出中,我们可以看到张量a和b以及矩阵乘法运算已分配给 GPU。 如果访问 GPU 出现问题,则输出可能如下所示:
I tensorflow/core/common_runtime/placer.cc:874] b_1: (Const)/job:localhost/replica:0/task:0/device:CPU:0
a_1: (Const): /job:localhost/replica:0/task:0/device:CPU:0
I tensorflow/core/common_runtime/placer.cc:874] a_1: (Const)/job:localhost/replica:0/task:0/device:CPU:0
在这里我们可以看到张量b_1和a_1被分配给 CPU 而不是 GPU。 如果发生这种情况,说明您的 TensorFlow,CUDA 或 cuDNN 安装存在问题。
如果到目前为止,您已经安装了 TensorFlow。 剩下的唯一任务是安装 Keras。
可以在以下代码的帮助下完成 Keras 的安装:
pip install keras
就是这样! 现在我们准备在 Keras 和 TensorFlow 中构建深度神经网络。
这可能是创建快照甚至是 EC2 实例的 AMI 的好时机,因此您不必再次进行此安装。
构建用于深度学习的数据集
与您可能已经使用的其他预测模型相比,深度神经网络非常复杂。 考虑一个具有 100 个输入的网络,两个具有 30 个神经元的隐藏层以及一个逻辑输出层。 该网络将具有 3,930 个可学习的参数以及优化所需的超参数,这是一个非常小的例子。 大型卷积神经网络将具有数亿个可学习的参数。 所有这些参数使得深度神经网络在学习结构和模式方面如此惊人。 但是,这也使过度安装成为可能。
深度学习中的偏差和方差误差
您可能熟悉典型预测模型中的所谓偏差/方差折衷。 如果您不在,我们将在此处提供快速提醒。 在传统的预测模型中,当我们尝试从偏差中发现误差并从方差中发现误差时,通常会有一些折衷。 因此,让我们看看这两个误差是什么:
- 偏差误差:偏差误差是模型引入的误差。 例如,如果您尝试使用线性模型对非线性函数建模,则模型将在指定的下为,并且偏差误差会很高。
- 方差误差:方差误差是由训练数据中的随机性引起的误差。 当我们很好地拟合训练分布以至于我们的模型不再泛化时,我们就过拟合或引入了方差误差。
在大多数机器学习应用中,我们寻求找到一些折衷方案,以最小化偏差误差,同时引入尽可能小的方差误差。 我之所以这么说是因为深度神经网络的一大优点是,在很大程度上,偏差和方差可以彼此独立地进行操纵。 但是,这样做时,我们将需要非常谨慎地构造训练数据。
训练,验证和测试数据集
在本书的其余部分中,我将把我的数据分为三个独立的集合,分别称为训练,验证和测试。 从总数据集中抽取为随机样本的这三个单独的数据集的结构和大小将大致如此。

训练数据集将按预期用于训练网络。
验证数据集将用于查找理想的超参数并测量过拟合。 在周期结束时,即网络有机会观察训练集中的每个数据点时,我们将对验证集进行预测。 该预测将用于监视过拟合,并将帮助我们知道网络何时完成训练。 像这样在每个周期末尾使用验证设置与典型用法有些不同。 有关保留验证的更多信息,请参考 Hastie 和 Tibshirani 撰写的《统计学习的特征》。
一旦完成所有训练,就将使用测试数据集,以根据网络未看到的一组数据准确地测量模型表现。
验证和测试数据来自同一数据集非常重要。 训练数据集匹配验证和测试不太重要,尽管那仍然是理想的。 例如,如果使用图像增强(对训练图像进行较小的修改以尝试扩大训练集大小),则训练集分布可能不再与验证集分布匹配。 这是可以接受的,并且只要验证和测试来自同一分布,就可以充分测量网络表现。
在传统的机器学习应用中,习惯上将 10% 到 20% 的可用数据用于验证和测试。 在深度神经网络中,通常情况是我们的数据量很大,以至于我们可以用更小的验证和测试集来充分测量网络表现。 当数据量达到数以千万计的观测值时,将 98%,1%,1% 的拆分完全合适。
在深度神经网络中管理偏差和方差
现在,我们已经定义了如何构造数据并刷新偏差和方差,现在让我们考虑如何控制深度神经网络中的偏差和方差。
- 高偏差:在训练集上进行预测时,具有高偏差的网络将具有非常高的错误率。 该模型在拟合数据方面表现不佳。 为了减少偏差,您可能需要更改网络架构。 您可能需要添加层,神经元或两者。 使用卷积或循环网络可能可以更好地解决您的问题。
当然,有时由于信号不足或非常困难的问题而导致问题偏高,因此请务必以合理的速度校准您的期望(我喜欢从对人的准确率进行校准开始)。
- 高方差:具有低偏差误差的网络很好地拟合了训练数据; 但是,如果验证误差大于测试误差,则网络已开始过拟合训练数据。 减少差异的两种最佳方法是添加数据并向网络添加正则化。
添加数据很简单,但并非总是可能的。 在整本书中,我们将介绍适用的正则化技术。 我们将讨论的最常见的正则化技术是 L2 正则化,丢弃法和批量归一化。
K 折交叉验证
如果您有机器学习的经验,您可能想知道为什么我会选择通过 K 折交叉验证而不是保留(训练/验证/测试)验证。 训练深度神经网络是一项非常昂贵的操作,并且非常简单地讲,针对每个我们想探索的超参数训练 K 个神经网络通常不太实用。
我们可以确信,在给定的验证和测试集足够大的情况下,留出验证会做得很好。 在大多数情况下,我们希望在有大量数据的情况下应用深度学习,从而获得足够的值和测试集。
最终,这取决于您。 稍后我们将看到,Keras 提供了 scikit-learn 接口,该接口可将 Keras 模型集成到 scikit-learn 管道中。 这使我们能够执行 K 折,分层 K 折,甚至使用 K 折进行网格搜索。 有时在训练深层模型时使用 K 折 CV 是可行且适当的。 也就是说,在本书的其余部分中,我们将重点介绍使用留出验证。
总结
希望本章能够使您对深度神经网络架构和优化算法有所了解。 因为这是快速参考,所以我们没有做太多的详细介绍,我鼓励读者对这里可能是新手或陌生的任何材料进行更深入的研究。
我们讨论了 Keras 和 TensorFlow 的基础知识,以及为什么我们在本书中选择了这些框架。 我们还讨论了 CUDA,cuDNN,Keras 和 TensorFlow 的安装和配置。
最后,我们介绍了本书其余部分将使用的留出验证方法,以及为什么对于大多数深度神经网络应用,我们都更喜欢 K 折 CV。
当我们在以后的章节中重新审视这些主题时,我们将大量参考本章。 在下一章中,我们将开始使用 Keras 解决回归问题,这是构建深度神经网络的第一步。
二、使用深度学习解决回归问题
在本章中,我们将构建一个简单的多层感知器(MLP),它是具有单个隐藏层的神经网络的奇特名称,用于解决回归问题。 然后,我们将深入研究具有多个隐藏层的深度神经网络。 在此过程中,我们将探索模型的表现和过拟合。 所以,让我们开始吧!
我们将在本章介绍以下主题:
- 回归分析和深度神经网络
- 将深度神经网络用于回归
- 在 Keras 中建立 MLP
- 在 Keras 中建立深度神经网络
- 保存和加载经过训练的 Keras 模型
回归分析和深度神经网络
在经典回归分析中,我们使用线性模型来学习一组独立变量和因变量之间的关系。 在找到这种关系时,我们希望能够在给定自变量值的情况下预测因变量的值。
进行回归分析的第二个重要原因是要了解当所有其他自变量保持恒定时单个自变量对因变量的影响。 传统多元线性回归的一大优点是线性模型的其他条件不变属性。 我们可以通过使用与该自变量关联的学习权重来解释单个自变量对因变量的影响,而无需考虑其他自变量。 这种解释充其量是具有挑战性的,需要我们对我们的数据和模型做出很多假设。 但是,它通常非常有用。
深度神经网络很难解释,尽管尝试这样做是一个活跃的研究领域。
有关介绍深度神经网络的当前状态的介绍,请查看 Montavon 等人的《解释和理解深度神经网络的方法》。
将神经网络用于回归的好处
在本章的其余部分,我们将重点介绍使用深度神经网络进行预测。 与使用传统的多元线性回归进行比较时,您会很高兴地发现我们的神经网络具有以下优势:
- 我们不需要选择或筛选特征。 神经网络是功能强大的特征工程机器,可以了解哪些特征是相关的,而忽略了无关的特征。
- 给定足够复杂的网络,还可以学习特征交互(例如,除了
x₁和x₂的独立效应,x[1] * x[2]的效应)) - 您可能现在已经猜到了,我们还可以学习更高阶的多项式关系(例如
x[2]^3) - 最后,只要我们确保最终激活可以对分布进行建模,我们就不必只对正态分布建模或对非正态分布使用不同的模型。
将神经网络用于回归时要考虑的缺点
但这并不是所有的彩虹和小猫,使用神经网络解决这些真正简单的问题也有一些弊端。 最明显的缺点是:
- 如前所述,神经网络不容易解释。
- 当具有许多特征和大量数据时,神经网络最有效。 许多简单的回归问题还不够大,无法真正从神经网络中受益。
- 在很多情况下,传统的多元回归或树模型(例如梯度提升树)在此类问题上的表现将优于神经网络。 越复杂,就越适合神经网络。
将深度神经网络用于回归
既然您已经希望了解为什么(不希望)使用深度神经网络进行回归,那么我将向您展示如何做到这一点。 虽然它不像在 scikit-learn 中使用线性回归器那样简单,但我认为使用 Keras 会很容易。 最重要的是,Keras 将允许您快速迭代模型架构而无需更改大量代码。
如何规划机器学习问题
在构建新的神经网络时,我建议每次都遵循相同的基本步骤。
深度神经网络很快就会变得非常复杂。 进行一点计划和组织,大大加快您的工作流程!
以下是构建深度神经网络的步骤:
- 概述您要解决的问题。
- 确定模型的输入和输出。
- 选择
cost函数和指标。 - 创建一个初始的网络架构。
- 训练和调整网络。
定义示例问题
在我们的示例问题中,我们将使用 P. Cortez 等人创建的葡萄酒质量数据集。 考虑到白酒的其他 10 个化学特性,我们将预测白葡萄酒数据中所含酒精的百分比。
此数据集中总共有 4,898 个观测值或元素,对于经典回归问题而言可能很大,但对于深度学习问题而言却很小。
一些快速的探索性数据分析将告诉我们,我们将用来预测酒精含量的 10 个化学特征在不同尺度上都是连续变量。
加载数据集
虽然可能不是机器学习问题中最有趣的部分,但加载数据是重要的一步。 我将在这里介绍我的数据加载方法,以便您可以了解如何处理数据集。
from sklearn.preprocessing import StandardScaler
import pandas as pd
TRAIN_DATA = "./data/train/train_data.csv"
VAL_DATA = "./data/val/val_data.csv"
TEST_DATA = "./data/test/test_data.csv"
def load_data():
"""Loads train, val, and test datasets from disk"""
train = pd.read_csv(TRAIN_DATA)
val = pd.read_csv(VAL_DATA)
test = pd.read_csv(TEST_DATA)
# we will use sklearn's StandardScaler to scale our data to 0 mean, unit variance.
scaler = StandardScaler()
train = scaler.fit_transform(train)
val = scaler.transform(val)
test = scaler.transform(test)
# we will use a dict to keep all this data tidy.
data = dict()
data["train_y"] = train[:, 10]
data["train_X"] = train[:, 0:9]
data["val_y"] = val[:, 10]
data["val_X"] = val[:, 0:9]
data["test_y"] = test[:, 10]
data["test_X"] = test[:, 0:9]
# it's a good idea to keep the scaler (or at least the mean/variance) so we can unscale predictions
data["scaler"] = scaler
return data
当我从 csv,excel 甚至是 DBMS 中读取数据时,第一步通常是将其加载到 pandas 数据框中。
标准化我们的数据很重要,这样每个特征都应具有可比的范围,并且所有这些范围都应位于激活函数的范围之内。 在这里,我使用了 Scikit-Learn 的StandardScaler完成此任务。
这为我们提供了一个形状完整的数据集(4898, 10)。 我们的目标变量alcohol的百分比介于 8% 和 14.2% 之间。
在加载数据之前,我已经对数据进行了随机采样并将其划分为train,val和test数据集,因此我们在这里不必担心。
最后,load_data()函数返回一个字典,该字典将所有内容保持整齐并放在一个位置。 如果您以后看到我参考数据[X_train],则知道我正在参考训练数据集,该数据集已存储在数据字典中。
。 该项目的代码和数据均可在该书的 GitHub 网站上找到。
定义成本函数
对于回归任务,最常见的成本函数是均方根误差(RMSE)和平均绝对误差(MAE)。 我将在这里使用 MAE。 定义如下:

很简单,MAE 是数据集中所有示例的平均无符号误差。 与 RMSE 非常相似; 但是,我们使用y和y_hat之间的差的绝对值代替平均平方误差的平方根:

您可能想知道 MAE 与更熟悉的 RMSE 有何不同。 如果误差在数据集中均匀分布,则 RMSE 和 MAE 将相等。 如果数据集中有非常大的离群值,则 RMSE 将比 MAE 大得多。 您选择的成本函数应适合您的用例。 关于可解释性,MAE 比 RMSE 更具解释性,因为它是实际的平均误差。
在 Keras 中建立 MLP
Keras 使用模型对象的实例来包含神经网络。 对于熟悉 scikit-learn 的人来说,这可能是相当熟悉的。 略有不同的是 Keras 模型包含一组层。 这一组层需要由我们定义。 只需很少的代码,就可以在网络架构中实现惊人的灵活性。
Keras 当前有两个用于构建模型的 API。 在我的示例中,我将使用函数式 API。 它稍微冗长一些,但可以提供更多的灵活性。 我建议尽可能使用函数式 API。
我们的 MLP 将需要一个输入层,一个隐藏层和一个输出层。
输入层形状
由于我们已经确定了输入,因此我们知道输入矩阵的行数等于数据集中的数据元素/观测值的数量,并且列数等于变量/特征的数量。 输入矩阵的形状为(观察数量 x 10 个特征)。 TensorFlow 和 Keras 可以在定义数据集中元素的数量时使用None作为占位符,而不是定义数据集中或小批量中的确切记录数。
如果看到 Keras 或 TensorFlow 模型层形状中使用了None维度,则它实际上表示任意维度,该维度可以采用任何正整数值。
隐藏层形状
我们的隐藏层将从 32 个神经元开始。 在这一点上,我们不知道需要多少神经元。 这确实是一个超参数,以后可以进行探索和调整。 为给定问题确定合适的网络架构是深度学习领域的一个开放问题。
由于隐藏层中这 32 个神经元中的每一个都将其激活输出到输出层,因此隐藏层的形状将为(10, 32)。
输出层形状
我们的最后一层将由单个神经元组成,使用来自隐藏层的 32 个输入,将为每个观察值预测单个输出值y_hat。
将所有各层放在一起,我们的 MLP 网络结构将如下所示:

神经网络架构
现在我们已经定义了输入和输出,我们可以看一下网络的代码。
from keras.layers import Input, Dense
from keras.models import Model
def build_network(input_features=None):
inputs = Input(shape=(input_features,), name="input")
x = Dense(32, activation='relu', name="hidden")(inputs)
prediction = Dense(1, activation='linear', name="final")(x)
model = Model(inputs=inputs, outputs=prediction)
model.compile(optimizer='adam', loss='mean_absolute_error')
return model
这里的所有都是它的! 然后,我们可以使用此代码,只需调用它即可构建适合于我们问题的神经网络实例,如下所示:
model = build_network(input_features=10)
但是,在开始之前,让我们回顾一下前面代码中的一些有趣的部分:
- 每层链接到到它上面的层。 每层都是可调用的,并返回张量。 例如,当隐藏层调用它时,我们的隐藏层绑定到输入层:
x = Dense(32, activation='relu', name="hidden")(inputs)
- 我们最后一层的激活函数是线性的。 这与不使用任何激活(这是我们要进行回归)相同。
- Keras 模型需要使用
.compile()进行编译。 - 在编译调用期间,您需要定义将要使用的成本函数和优化器。 正如我们所讨论的,在此示例中,我已将 MAE 用于成本函数。 我使用具有默认参数的 Adam 作为我的优化程序,我们在第 1 章中已经介绍了这一点。很可能我们最终将希望调整 Adam 的学习速度。 这样做非常简单:您只需要定义一个自定义
adam实例,然后使用该实例即可:
from keras.optimizers import Adam
adam_optimizer = Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.0)
model.compile(optimizer=adam_optimizer, loss='mean_absolute_error')
训练 Keras 模型
现在我们的网络已经构建和编译,剩下的就是训练它了。 就像 Python 的 scikit-learn 一样,您可以通过在模型实例上调用.fit()来做到这一点,如以下代码所示:
model.fit(x=data["train_X"], y=data["train_y"], batch_size=32, epochs=200, verbose=1, validation_data=(data["val_X"], data["val_y"]))
让我们来看一下 Keras fit方法所采用的一些重要参数。 我将假设您熟悉小批量梯度下降和训练周期,但如果不熟悉,请查看第 1 章“深度学习的基础知识”, 概述。 Keras 拟合模型中的重要参数如下:
batch_size:Keras 的默认批次大小为 32。批次大小是 Keras 将使用的迷你批次的大小。 当然,这意味着 Keras 假设您要使用小批量梯度下降。 如果由于某种原因不想使用小批量梯度,可以设置batch_size=None。epochs:一个周期只是整个训练集的单次通过。 在实践中,您需要在训练网络时对其进行监视,以了解网络何时收敛,因此epochs是一个易于学习的超参数。 稍后,我们将看到可以在每个周期甚至比最后一个周期更好的每个周期保存模型的权重。 一旦知道如何做到这一点,我们就可以选择我们认为最好的周期,并实现一种基于人的早期停止。validation_data:在这里,我们指定验证集。 在每个阶段结束时,Keras 将在验证集上测试模型,并使用损失函数和您指定的任何其他指标输出结果。 另外,您可以将validation_split设置为浮点值,以指定要用于验证的训练组的百分比。 这两个选项都可以正常工作,但是在数据集拆分方面,我希望讲得很明确。verbose:这有点不言而喻; 但是,值得一提。verbose=1输出一个进度条,显示当前周期的状态,在周期结束时,Keras 将输出训练和验证损失。 也可以将verbose设置为 2(每个小批量输出损失信息),将其设置为 0(使 Keras 保持静音)。
评估模型的表现
现在我们的 MLP 已经过训练,我们可以开始了解它的表现。 为此,我将对Train,Val和Test数据集进行预测。 相同的代码如下:
print("Model Train MAE: " + str(mean_absolute_error(data["train_y"], model.predict(data["train_X"]))))
print("Model Val MAE: " + str(mean_absolute_error(data["val_y"], model.predict(data["val_X"]))))
print("Model Test MAE: " + str(mean_absolute_error(data["test_y"], model.predict(data["test_X"]))))
对于我们的 MLP,这是我们做得如何:
Model Train MAE: 0.190074701809
Model Val MAE: 0.213255747475
Model Test MAE: 0.199885450841
请记住,我们的数据已缩放为 0 均值和单位方差。 Train MAE是0.19,而我们的Val MAE是0.21。 这两个误差彼此之间非常接近,所以过分适合并不是我太在意的事情。 因为我预计会有一些我看不到的过拟合(通常是更大的问题),所以我认为此模型可能有太多偏差。 换句话说,我们可能无法足够紧密地拟合数据。 发生这种情况时,我们需要为我们的模型添加更多的层,更多的神经元或两者。 我们需要更深入。 让我们接下来做。
我们可以尝试通过以更多神经元的形式向网络添加参数来减少网络偏差。 虽然您可能会开始尝试优化优化器,但通常最好先找到自己熟悉的网络架构。
在 Keras 中建立深度神经网络
更改模型就像重新定义我们先前的build_network()函数一样容易。 我们的输入层将保持不变,因为我们的输入没有更改。 同样,输出层应保持不变。
我将通过添加其他隐藏层将参数添加到我们的网络中。 我希望通过添加这些隐藏层,我们的网络可以了解输入和输出之间更复杂的关系。 我将从添加四个其他隐藏层开始; 前三个将具有 32 个神经元,第四个将具有 16 个神经元。其外观如下:

以下是在 Keras 中构建模型的相关代码:
def build_network(input_features=None):
inputs = Input(shape=(input_features,), name="input")
x = Dense(32, activation='relu', name="hidden1")(inputs)
x = Dense(32, activation='relu', name="hidden2")(x)
x = Dense(32, activation='relu', name="hidden3")(x)
x = Dense(32, activation='relu', name="hidden4")(x)
x = Dense(16, activation='relu', name="hidden5")(x)
prediction = Dense(1, activation='linear', name="final")(x)
model = Model(inputs=inputs, outputs=prediction)
model.compile(optimizer='adam', loss='mean_absolute_error')
return model
如所承诺的,我们的代码几乎没有改变。 我将其他行加粗了。 我们其余的代码可以保持不变。 但是,随着网络复杂性的增加,您通常必须训练更长的时间(更多的时间)。
测量深度神经网络表现
在这个问题上,深层网络真的比 MLP 好吗? 让我们找出答案! 训练了 500 个周期后,模型的效果如下:
Model Train MAE: 0.0753991873787
Model Val MAE: 0.189703853999
Model Test MAE: 0.190189985043
我们可以看到Train MAE现在从0.19减少到0.075。 我们大大降低了网络的偏差。
但是,我们的差异增加了。 训练误差和验证误差之间的差异要大得多。 我们的Val集误差确实略有下降,这很好; 但是,训练误差和验证误差之间的巨大差距表明我们开始过度适应训练集。
在这种情况下,减少差异的最直接方法是添加其他训练数据或应用诸如 L2 正则化或丢弃法之类的正则化技术,我们将在下一章中介绍。
对于高方差网络,更多的数据通常是最佳解决方案。 如果有可能收集更多数据,那可能就是花费时间的最佳位置。
建立网络后,我想直观地检查误差,以了解网络对验证集分布进行建模的程度。 这通常会带来见解,这将有助于我改进模型。 对于回归模型,我想绘制验证集的预测值和实际值的直方图。 让我们看看我的表现如何。 该图如下,供您参考:

总体而言,我认为该模型正在相当接近地预测实际分布。 似乎实际的验证数据集比预测的数据集向左移动(较小的值)要多一些,这可能是一个重要的见解。 换句话说,网络可能会预测葡萄酒的酒精含量高于平均水平,尤其是在酒精含量较低的情况下。 更仔细地检查验证数据可能会建议我们如何收集更多的训练数据。
调整模型超参数
现在,我们已经针对该问题训练了 MLP 和六层深度神经网络,现在可以调整和优化模型超参数了。
我们将在第 6 章“超参数优化”中讨论深度模型调整。 您可以使用多种策略为模型选择最佳参数。 您可能已经注意到,我们仍然可以优化许多可能的参数和超参数。
如果要完全调整此模型,则应执行以下操作:
- 试验隐藏层的数量。 看来五个可能太多,而一个可能还不够。
- 试验每个隐藏层相对于层数的神经元数量。
- 尝试添加丢弃或正则化。
- 尝试通过尝试使用 SGD 或 RMS 属性而不是 Adam 或通过对 Adam 使用不同的学习率来进一步减少模型误差。
深度神经网络有许多活动部分,有时要达到最佳状态是一个疲惫的概念。 您必须确定您的模型是否足够好。
保存和加载经过训练的 Keras 模型
您不太可能会训练一个深层的神经网络,然后将其应用到同一脚本中。 最有可能的是,您将需要训练网络,然后保存结构和权重,以便可以将其用于设计用于对新数据进行评分的面向生产的应用中。 为此,您需要能够保存和加载模型。
在 Keras 中保存模型非常简单。 您可以使用模型实例的.save()方法将网络结构和权重保存到hdf5文件,如以下代码所示:
model.save("regression_model.h5")
这就是全部。 从磁盘加载模型非常简单。 此处提供了执行此操作的代码供您参考:
from keras.models import load_model
model = load_model("regression_model.h5")
总结
当您考虑深度学习时,您可能会想到令人印象深刻的复杂计算机视觉问题,但是即使对于像这样的简单回归问题,深度神经网络也可能有用。 希望我已经证明了这一点,同时还介绍了 Keras 语法并向您展示了如何构建一个非常简单的网络。
随着我们的继续,我们将遇到更多的复杂性。 更大的网络,更复杂的成本函数以及高维输入数据。 但是,我在本章中使用的过程在大多数情况下将保持不变。 在每种情况下,我们都将概述问题,确定输入和输出,选择成本函数,创建网络架构,最后训练和调整模型。
如果考虑以下因素,则在深度神经网络中通常可以独立地控制和减少偏差和方差:
- 偏差:可以通过增加模型复杂度来减少此偏差。 其他神经元或层将有所帮助。 添加数据并不能真正帮助减少偏差。
- 方差:可以通过添加数据或正则化来减少此变化。
在下一章中,我们将讨论如何使用 TensorBoard 更快地对深度神经网络进行优化和故障排除。
三、使用 TensorBoard 监控网络训练
在本章中,我将向您展示如何使用 TensorBoard 帮助更快更轻松地训练深度神经网络。 我认为 TensorBoard 是一个很棒的工具,经常被忽略,而它又常常被拖到脚注或上一章中。 现在,让我们看一下 TensorBoard,以便我们可以立即开始利用它。
我们将在本章介绍以下主题:
- TensorBoard 的简要概述
- 设置 TensorBoard
- 将 Keras 连接到 TensorBoard
- 使用 TensorBoard
TensorBoard 的简要概述
TensorBoard 是一个基于 Web 的应用,可以帮助您可视化 TensorFlow 中创建的深度神经网络的指标,参数和结构。 它将帮助您更快,更轻松地调试和优化深度神经网络。
正如您现在可能已经猜到的那样,深度神经网络可能变得相当复杂。 不幸的是,这意味着很多事情可能出错。 众所周知,我时不时地会犯一个错误,而当错误发生在一个深度神经网络内部时,该深度神经网络位于一个框架内,该框架在另一个框架上运行,在一个 GPU 上运行,很难找到这些错误。 他们。 TensorBoard 可能是您需要在其他本来很暗的房间中发现问题的手电筒。 TensorBoard 将允许您在训练网络时监视指标和参数的变化,这可以大大加快故障排除速度。
TensorBoard 也非常适合优化。 借助 TensorBoard,您可以直观地比较多个模型运行。 这使您可以试验不断变化的架构和超参数,然后相对于网络的其他运行评估那些变化。 所有这一切都可能在每个周期发生,因此如果您愿意,您可以取消效果不佳的模型运行,从而节省了时间和金钱。 您可以在这个页面上阅读有关 TensorBoard 的更多信息。
设置 TensorBoard
TensorBoard 是一个独立的 Web 应用。 您将通过网络浏览器使用它。 设置需要两个步骤。 首先,我们将设置 TensorBoard 以可视化在 TensorFlow 和 Keras 中构建的网络,然后我们将设置 Keras 与 TensorBoard 共享信息。
本节介绍 TensorBoard 的设置。 接下来的内容将涉及修改 Keras 代码以与 TensorBoard 共享信息。
安装 TensorBoard
如果您已经安装了 TensorFlow,则您的机器上可能已经安装了 Tensorboard。 万一您可以安装和更新 TensorBoard,可以使用pip进行安装,就像 Keras 和 TensorFlow 一样。 要安装它,只需运行以下命令:
pip install -U tensorboard
TensorBoard 如何与 Keras/TensorFlow 交互
TensorBoard 和 TensorFlow 使用公共日志目录共享信息。 在 Keras 和 TensorFlow 训练中,Keras 将指标和激活直方图(稍后将对此进行详细介绍)写入您指定的日志目录中。 现在,让我们使用以下代码在主目录中为该示例创建一个日志目录:
mkdir ~/ch3_tb_log
运行 TensorBoard
剩下的就是启动 TensorBoard 进程。 我们可以使用以下代码启动 TensorBoard:
tensorboard --logdir ~/ch3_tb_log --port 6006
您可能已经猜到了,--logdir指定我们刚刚创建的目录,--port 6006指定 TensorBoard 将在其上运行的端口。 端口6006是默认端口。 但是,您可以使用所需的任何端口。
现在,您应该可以通过将浏览器指向http://<ip address>:6006来导航到 TensorBoard URL。
如果使用的是云服务,则可能还需要调整防火墙或安全规则,以允许通过端口6006连接到服务器。 在 Amazon Web Services(AWS)上,您可以通过编辑与您的 EC2 实例关联的安全组中的入站规则来执行此操作:

您可能不希望像我上面那样允许全世界范围内的开放访问。 这只是一个测试实例,因此我不太关心安全性,无论如何我都喜欢过着危险的生活。
如果一切正常,您应该看到一个空的 TensorBoard,如下所示:

不用担心,我们很快就会填满。
将 Keras 连接到 TensorBoard
现在 TensorBoard 已启动并正在运行,剩下的就是告诉 Keras 将 TensorBoard 日志写入我们上面指定的目录。 幸运的是,这确实很容易实现,它为我们提供了一个很好的机会来了解 Keras 中称为 Keras 回调的特殊函数类。
引入 Keras 回调
Keras 中的回调是可以在训练过程中运行的函数。 他们可以做各种伟大的事情,例如在某个周期之后节省模型权重,记录事情,更改超参数或方便地编写 TensorBoard 日志文件。 您甚至可以创建自己的自定义回调。
在下一节中,我们将使用 TensorBoard 回调。 但是,我鼓励您在这个页面上查看 Keras 中可用的所有回调。
TensorBoard 回调是可以在模型训练之前进行配置和实例化的对象。 我们将创建这些回调的列表。 一旦创建了要用于深度神经网络的回调列表,我们就可以将该列表作为参数传递给模型的.fit()方法。 然后,将在每个周期或 Keras 适当时使用这些回调。 在我们继续下一个示例时,这将更有意义。
创建一个 TensorBoard 回调
在本章中,我通过复制第 2 章“开始使用深度学习来解决回归问题”的网络和数据。 我们将做一些简单的添加来添加 TensorBoard 回调。 让我们从修改我们首先构建的mlp开始。
首先,我们需要使用以下代码导入 TensorBoard 回调类:
from keras.callbacks import TensorBoard
然后,我们将启动回调。 我喜欢在创建所有回调的函数中执行此操作,以使事情精心制作和整理。 下面的create_callbacks()函数将返回我们将传递给.fit()的所有回调的列表。 在这种情况下,它将返回一个包含一个元素的列表:
def create_callbacks():
tensorboard_callback = TensorBoard(log_dir='~/ch3_tb_log/mlp',
histogram_freq=1, batch_size=32, write_graph=True,
write_grads=False)
return [tensorboard_callback]
在继续之前,我们先介绍一下这里使用的一些参数:
log_dir:这是我们将为 TensorBoard 写入日志文件的路径。
您可能已经注意到,我正在将 MLP 网络的 TensorBoard 回调的日志写入~/ch_3_tb_log/mlp,这将在我们为 TensorBoard 指定的目录下创建一个新的目录mlp。 这是故意的。 我们将配置在第 2 章,“使用深度学习解决回归问题”训练的深度神经网络模型,以登录到单独的目录~/ch_3_tb_log/dnn。 这样做将使我们能够比较两个模型的运行。
histogram_freq:这指定我们将多长时间计算一次激活和权重的直方图(以周期为单位)。 它的默认值为 0,这会使日志更小,但不会生成直方图。 我们将介绍为什么以及何时您会对直方图感兴趣。batch_size:这是用于计算直方图的批量大小。 默认为 32。write_graph:此函数为布尔值。 这将告诉 TensorBoard 可视化网络图。 这可能非常方便,但也会使日志变得很大。write_grads:此函数也是布尔值。 这将告诉 TensorBoard 也计算梯度的直方图。
由于 TensorFlow 会自动为您计算梯度,因此很少使用。 但是,如果您要使用自定义激活或费用,它可能是出色的故障排除工具。
TensorBoard 回调可以接受用于在图像上运行神经网络或通过使用嵌入式层的其他参数。 我们将在本书的后面介绍这两个方面。 如果您对这些函数感兴趣,请访问 TensorBoard API 文档。
现在,我们只需要创建回调列表,并将mlp与callbacks参数匹配即可。 看起来像这样:
callbacks = create_callbacks()
model.fit(x=data["train_X"], y=data["train_y"], batch_size=32,
epochs=200, verbose=1, validation_data=(data["val_X"],
data["val_y"]), callbacks=callbacks)
为了清楚起见,我将新参数加粗了。
在继续使用 TensorBoard 之前,我将以与检测mlp相同的方式来检测深度神经网络。 唯一的代码更改是我们将 TensorBoard 日志写入的目录。 下面给出了实现该方法的方法,供您参考:
def create_callbacks():
tensorboard_callback = TensorBoard(log_dir='./ch3_tb_log/dnn',
histogram_freq=1, batch_size=32, write_graph=True, write_grads=False)
return [tensorboard_callback]
其余代码将相同。 现在,让我们再次训练每个网络,看看 TensorBoard。
使用 TensorBoard
现在我们已经完全配置了 TensorBoard 并告诉我们的网络如何向其发送日志数据,我们可以开始利用它了。 在本章的其余部分,我将向您展示一些我最喜欢的使用 TensorBoard 的方式。 TensorBoard 的功能不只此而已,我们将在本书的其余部分中重新讨论其他功能。
可视化训练
由于我们已在第 2 章“使用了深度学习解决回归问题”中使用这两种模型编写了日志数据,因此可以使用 TensorBoard 以图形方式比较这两种模型。 打开 TensorBoard 并转到SCALARS选项卡。 您应该会看到类似这样的内容。 您可能需要单击loss和val_loss来展开图形:

张量板显示模型的损失图和val_loss图
如果您查看屏幕的左下角,则应注意,我们创建的每个目录都有与之关联的运行。 两者均处于选中状态。 这意味着在我们的图形上,我们将看到两个模型的输出。
TensorBoard 可以容纳许多运行,并且您可以通过正则表达式过滤它们(例如^dnn将显示所有以dnn开头的运行)。 这意味着,如果您通过许多实验或运行(例如超参数优化)来搜索最佳模型,则可以在明确并一致地命名运行,并包含有意义的超参数和架构信息的情况下,以这个名字快速浏览它们!
这些图上的默认 X 比例尺是周期。 Y 值是我们选择的损失函数,即 MAE。 您可以单击图形以浏览它们并拖动以缩放。
看到这样的图,我们真的可以看到每个网络的相对偏差和方差。 虽然模型之间在训练损失方面有很好的分离,但深度神经网络在验证集上只得到了一点点改善,这表明我们已经进入了过拟合的领域。
可视化网络图
虽然能够查看我们的训练过程并比较模型显然很不错,但这并不是 TensorBoard 所能做的。 我们还可以使用它来可视化网络结构。 在这里,我导航到GRAPHS并提出了深度神经网络的结构:

TensorBoard 显示深度神经网络的结构
训练节点代表输入张量,默认情况下,正是这个巨型章鱼以某种无益的方式连接到图的其余部分。 要解决此问题,您只需单击该节点,然后单击从主图中删除。 然后将其移到侧面。
可视化损坏的网络
TensorBoard 是一个出色的故障排除工具。 为了证明这一点,我将复制我们的深度神经网络并将其破坏! 幸运的是,打破神经网络真的很容易。 相信我,我已经无意间做了这件事,以至于我现在基本上是专家。
想象一下,您刚刚训练了一个新的神经网络,并且看到损失看起来像这样:

该网络的损失函数被卡住,并且比我们之前的运行要高得多。 什么地方出了错?
导航到 TensorBoard 的HISTOGRAMS部分,并可视化第一个隐藏层。 让我们比较两个网络中隐藏层 1 的权重直方图:

显示两个网络中隐藏层 1 的权重直方图的屏幕截图
对于标记为 dnn 的网络的偏差和权重,您将看到权重分布在整个图中。 您甚至可以说每个分布都可能是正态分布。
您也可以在“分布”部分比较权重和偏差。 两者都以略有不同的方式呈现大多数相同的信息。
现在,看看我们破碎的网络的权重和偏置。 并不是这样分散,实际上的权重基本上是相同的。 网络并不是真正的学习。 该层中的每个神经元看起来或多或少都是相同的。 如果您查看其他隐藏层,则会看到更多相同的层。
您可能想知道我是怎么做到的。 您很幸运,我会分享我的秘密。 毕竟,您永远都不知道何时需要断开自己的网络。 为了解决问题,我将网络中的每个神经元初始化为完全相同的值。 发生这种情况时,每个神经元在反向传播期间收到的误差是完全相同的,并且更改的方式也完全相同。 网络然后无法破坏对称性。 以随机方式将权重初始化到深度神经网络非常重要,如果您违反了该规则,就会发生这种情况!
遇到问题时,可以像这样完全使用 TensorBoard。 请记住,我们的深度神经网络有 4033,在深度学习领域中,它仍然可以算作很小的。 使用 TensorBoard,我们能够直观地检查 4033 个参数并确定问题。 TensorBoard 是一个用于深度学习的暗室中的神奇手电筒。
总结
在本章中,我们讨论了如何安装,配置和使用 TensorBoard。 我们讨论了如何使用 TensorBoard 在 TensorBoard 的SCALARS部分中的每个周期检查模型的损失函数,从而直观地比较模型。 然后,我们使用 TensorsBoard 的GRAPHS部分来可视化网络结构。 最后,我们通过查看直方图向您展示了如何使用 TensorBoard 进行故障排除。
在下一章中,我们将研究如何使用 Keras 和 TensorFlow 解决二分类问题,从而扩展我们的深度学习技巧。
四、使用深度学习解决二分类问题
在本章中,我们将使用 Keras 和 TensorFlow 解决棘手的二分类问题。 我们将首先讨论深度学习对此类问题的利弊,然后我们将继续使用与第 2 章“学习解决回归问题”中使用的相同框架建立解决方案。 最后,我们将更深入地介绍 Keras 回调,甚至使用自定义回调来实现每个周期的受试者工作特征的曲线下面积(ROC AUC)指标。
我们将在本章介绍以下主题:
- 二分类和深度神经网络
- 案例研究 – 癫痫发作识别
- 在 Keras 中建立二分类器
- 在 Keras 中使用检查点回调
- 在自定义回调中测量 ROC AUC
- 测量精度,召回率和 f1 得分
二分类和深度神经网络
二分类问题(例如回归问题)是非常常见的机器学习任务。 如此之多,以至于任何一本有关深度学习的书都无法完整覆盖。 可以肯定的是,我们还没有真正达到深度神经网络的甜蜜点,但是我们进展顺利。 在开始编写代码之前,让我们谈谈在选择深度神经网络来解决此类问题时应考虑的权衡。
深度神经网络的好处
与更传统的分类器(例如逻辑回归模型)或什至基于树的模型(例如随机森林或梯度提升机)相比,深度神经网络有一些不错的优点。
与回归一样,在第 2 章“使用深度学习解决回归问题”中,我们不需要选择或筛选特征。 在本章选择的问题中,有 178 个输入变量。 每个输入变量都是来自标记为x1..x178的脑电图(EEG)的特定输入。 即使您是医生,也很难理解这么多特征与目标变量之间的关系。 这些特征中的某些特征很可能是不相关的,而这些变量和目标之间可能存在一些更高级别的交互,这是一个更好的机会。 如果使用传统模型,则经过特征选择步骤后,我们将获得最佳模型表现。 使用深度神经网络时不需要这样做。
深度神经网络的缺点
正如我们在第 2 章“使用深度学习解决回归问题”所述,深度神经网络不容易解释。 虽然深度神经网络是出色的预测器,但要理解它们为何得出自己的预测并不容易。 需要重复的是,当任务是要了解哪些特征与目标的变化最相关时,深度神经网络并不是工作的工具。 但是,如果目标是原始预测能力,则应考虑使用深度神经网络。
我们还应该考虑复杂性。 深度神经网络是具有许多参数的复杂模型。 找到最佳的神经网络可能需要花费时间和实验。 并非所有问题都能确保达到如此复杂的水平。
在现实生活中,我很少使用深度学习作为结构化数据问题的第一个解决方案。 我将从可能可行的最简单模型开始,然后根据问题的需要迭代进行深度学习。 当问题域包含图像,音频或文本时,我更有可能从深度学习开始。
案例研究 – 癫痫发作识别
您可能已经猜到了,我们将要解决二分类问题。 我们将使用与在第 2 章“使用深度学习解决回归问题”建立的框架相同的框架来计划问题,并根据需要对其进行修改。 您可以在本书的 GitHub 存储库中的第 4 章“使用深度学习解决回归问题”,找到本章的完整代码。
定义我们的数据集
我们将在本章中使用的数据集称为癫痫发作识别数据集。 数据最初来自Andrzejak RG 等人在 Phys 上发表的论文《指示脑电活动的时间序列中的非线性确定性和有限维结构:对记录区域和大脑状态的依赖性》。您可以在 UCI 机器学习存储库中找到数据。
我们的目标是创建一个深度神经网络,根据输入特征,该网络可以预测患者是否有癫痫发作。
加载数据
我们可以使用以下函数加载本章中使用的数据。 它与我们在第 2 章中使用的函数非常相似,但是适用于此数据集。
from sklearn.preprocessing import StandardScaler
def load_data():
"""Loads train, val, and test datasets from disk"""
train = pd.read_csv(TRAIN_DATA)
val = pd.read_csv(VAL_DATA)
test = pd.read_csv(TEST_DATA)
# we will use a dict to keep all this data tidy.
data = dict()
data["train_y"] = train.pop('y')
data["val_y"] = val.pop('y')
data["test_y"] = test.pop('y')
# we will use sklearn's StandardScaler to scale our data to 0 mean, unit variance.
scaler = StandardScaler()
train = scaler.fit_transform(train)
val = scaler.transform(val)
test = scaler.transform(test)
data["train_X"] = train
data["val_X"] = val
data["test_X"] = test
# it's a good idea to keep the scaler (or at least the mean/variance) so we can unscale predictions
data["scaler"] = scaler
return data
模型输入和输出
该数据集中有 11,500 行。 数据集的每一行包含 178 个数据点,每个数据点代表 1 秒钟的 EEG 记录样本和相应的患者状态,跨 100 个不同患者生成。
数据集中有五个患者状态。 但是,状态 2 至状态 5 的患者未发生癫痫发作。 状态 1 的患者正在发作。
我已经修改了原始数据集,通过将状态 2-5 更改为 0 级(表示无癫痫发作)和将 1 级(表示有癫痫发作)将状态重新定义为二分类问题。
与第 2 章“使用深度学习解决回归问题”中的回归问题一样,我们将使用 80% 的训练,10% 的 val,10% 的测试分割。
成本函数
我们需要分类器来预测癫痫发作的可能性,即类别 1。这意味着我们的输出将被限制为[0, 1],就像在传统的逻辑回归模型中一样。 在这种情况下,我们的成本函数将是二元交叉熵,也称为对数损失。 如果您以前使用过分类器,那么您可能很熟悉此数学运算; 但是,作为复习,我将在这里包括。
对数损失的完整公式如下所示:

这可能更简单地看作是两个函数的集合,对于情况y[i] = 0和y[i] = 1,一个函数:

当y[i] = 1,

当y[i] = 0。
对数函数在这里用于产生单调函数(一个一直在增加或减少的函数),我们可以轻松微分它。 与所有成本函数一样,我们将调整网络参数以最小化网络成本。
使用指标评估表现
除了loss函数之外,Keras 还使我们可以使用度量标准来帮助判断模型的表现。 虽然最大程度地降低损失是有好处的,但在给定loss函数的情况下,我们如何期望模型执行效果并不是特别明显。 度量标准并不用于训练模型,它们只是用来帮助我们了解当前状态。
尽管损失对我们而言并不重要,但准确率却对我们而言意义重大。 我们人类非常了解准确率。
Keras 定义二元精度如下:
def binary_accuracy(y_true, y_pred):
return K.mean(K.equal(y_true, K.round(y_pred)), axis=-1)
这实际上只是将正确答案的数量除以总答案的一种聪明方法,这是我们自从上学初期就可能一直在做的一项工作,目的是计算出考试的成绩。
您可能想知道我们的数据集是否平衡,因为准确率对于不平衡的数据集而言效果很差。 实际上这是不平衡的。 只有五分之一的数据集是类 1。我们将 ROC AUC 分数作为自定义回调来计算,以解决此问题。 在 Keras 中未将 ROC 用作度量标准,因为度量标准是针对每个小型批次计算的,并且 ROC AUC 分数并非真正由小型批次定义。
在 Keras 中建立二分类器
既然我们已经定义了问题,输入,期望的输出和成本函数,我们就可以在 Keras 中快速编写其余代码。 我们唯一缺少的是网络架构。 我们将很快讨论更多。 关于 Keras 的我最喜欢的事情之一是调整网络架构有多么容易。 如您所见,在找到最佳架构之前,可能需要进行大量实验。 如果是这样,那么易于更改的框架会使您的工作变得更加轻松!
输入层
和以前一样,我们的输入层需要知道数据集的维度。 我喜欢在一个函数中构建整个 Keras 模型,并允许该函数传递回已编译的模型。 现在,此函数仅接受一个参数,即特征数。 以下代码用于定义输入层:
def build_network(input_features=None):
# first we specify an input layer, with a shape == features
inputs = Input(shape=(input_features,), name="input")
隐藏层
我们已经定义了输入,这很容易。 现在我们需要确定网络架构。 我们如何知道应该包括多少层以及应该包含多少个神经元? 我想给你一个公式。 我真的会。 不幸的是,它不存在。 实际上,有些人正在尝试构建可以学习其他神经网络的最佳架构的神经网络。 对于我们其余的人,我们将不得不尝试,寻找自己或借用别人的架构。
如果我们使用的神经元过多会怎样?
如果我们使网络架构过于复杂,则会发生两件事:
- 我们可能会开发一个高方差模型
- 该模型将比不太复杂的模型训练得慢
如果我们增加许多层,我们的梯度将变得越来越小,直到前几层几乎没有训练为止,这就是梯度消失问题。 我们离那还很遥远,但是我们稍后会讨论。
用说唱传奇克里斯托弗·华莱士(又名臭名昭著的 B.I.G.)的话来说,我们遇到的神经元越多,看到的问题就越多。 话虽如此,方差可以通过丢弃法,正则化和提早停止进行管理,GPU 计算的进步使更深层次的网络成为可能。
如果我必须在神经元太多或太少的网络之间进行选择,而我只能尝试一个实验,那么我宁愿选择稍微过多的神经元。
如果我们使用的神经元太少会怎样?
想象一下,我们没有隐藏层,只有输入和输出的情况。 我们在第 1 章“深度学习的基础知识”中讨论了该架构,在此我们展示了如何无法为XOR函数建模。 这样的网络架构无法对数据中的任何非线性进行建模,因此无法通过网络进行建模。 每个隐藏层都为特征工程越来越复杂的交互提供了机会。
如果选择的神经元太少,则结果可能如下:
- 真正快速的神经网络
- 那有很高的偏差,而且预测不是很好
选择隐藏层架构
因此,既然我们了解选择太多参数而不是选择太多参数的价格和行为,那么从哪里开始呢? 据我所知,剩下的只是实验。
测量这些实验可能很棘手。 如果像我们的早期网络一样,您的网络训练很快,那么可以在多种架构中实现诸如交叉验证之类的东西,以评估每种架构的多次运行。 如果您的网络需要很长时间进行训练,则可能会留下一些统计上不太复杂的信息。 我们将在第 6 章“超参数优化”中介绍网络优化。
一些书籍提供了选择神经网络架构的经验法则。 我对此表示怀疑和怀疑,您当然不会在这里找到一个。
为我们的示例编码隐藏层
对于我们的示例问题,我将使用五个隐藏层,因为我认为特征之间存在许多交互。 我的直觉主要基于领域知识。 阅读数据描述后,我知道这是时间序列的横截面切片,并且可能是自动相关的。
我将从第一层的 128 个神经元开始(略小于我的输入大小),然后在接近输出时减半到 16 个神经元。 这完全不是凭经验,它仅基于我自己的经验。 我们将使用以下代码定义隐藏层:
x = Dense(128, activation='relu', name="hidden1")(inputs)
x = Dense(64, activation='relu', name="hidden2")(x)
x = Dense(64, activation='relu', name="hidden3")(x)
x = Dense(32, activation='relu', name="hidden4")(x)
x = Dense(16, activation='relu', name="hidden5")(x)
在每一层中,我都使用relu激活,因为它通常是最好和最安全的选择,但是要确保这也是可以试验的超参数。
输出层
最后,我们需要网络的输出层。 我们将使用以下代码定义输出层:
prediction = Dense(1, activation='sigmoid', name="final")(x)
在此示例中,我们正在构建一个二分类器,因此我们希望我们的网络输出观察结果属于类 1 的概率。幸运的是,sigmoid激活将精确地做到这一点,将网络输出限制在 0 到 1 之间。
放在一起
将所有代码放在一起,剩下的就是编译我们的 Keras 模型,将binary_crossentrophy指定为我们的loss函数,将accuracy指定为我们希望在训练过程中监控的指标。 我们将使用以下代码来编译我们的 Keras 模型:
def build_network(input_features=None):
inputs = Input(shape=(input_features,), name="input")
x = Dense(128, activation='relu', name="hidden1")(inputs)
x = Dense(64, activation='relu', name="hidden2")(x)
x = Dense(64, activation='relu', name="hidden3")(x)
x = Dense(32, activation='relu', name="hidden4")(x)
x = Dense(16, activation='relu', name="hidden5")(x)
prediction = Dense(1, activation='sigmoid', name="final")(x)
model = Model(inputs=inputs, outputs=prediction)
model.compile(optimizer='adam', loss='binary_crossentropy',
metrics=["accuracy"])
return model
训练我们的模型
现在我们已经定义了模型,我们都准备对其进行训练。 我们的操作方法如下:
input_features = data["train_X"].shape[1]
model = build_network(input_features=input_features)
model.fit(x=data["train_X"], y=data["train_y"], batch_size=32, epochs=20, verbose=1, validation_data=(data["val_X"], data["val_y"]), callbacks=callbacks)
如果您已经阅读第 2 章“使用深度学习解决回归问题”,则应该看起来很熟悉。 在大多数情况下,实际上是相同的。 回调列表包含 TensorBoard 回调,因此让我们观看我们的网络训练 20 个周期,看看会发生什么:

尽管我们的训练损失继续下降,但我们可以看到val_loss到处都在跳跃。 大约在第八个周期之后,我们就过拟合了。
有几种方法可以减少网络差异并管理这种过拟合,下一章将介绍大多数方法。 但是,在开始之前,我想向您展示一些有用的东西,称为检查点回调。
在 Keras 中使用检查点回调
在第 2 章“使用深度学习解决回归问题”中,我们看到了.save()方法,该方法使我们可以在完成训练后保存 Keras 模型。 但是,如果我们可以不时地将权重写入磁盘,以便在上一个示例中及时返回,并在模型开始过拟合之前保存其版本,那不好吗? 然后,我们可以就此停止,并使用网络的最低方差版本。
这正是ModelCheckpoint回调为我们所做的。 让我们来看看:
checkpoint_callback = ModelCheckpoint(filepath="./model-weights.{epoch:02d}-{val_acc:.6f}.hdf5", monitor='val_acc', verbose=1, save_best_only=True)
ModelCheckpoint将为我们执行的工作是按计划的时间间隔保存模型。 在这里,我们告诉ModelCheckpoint每当我们达到新的最佳验证精度(val_acc)时都要保存模型的副本。 我们也可以监视验证损失或我们指定的任何其他指标。
文件名字符串将包含周期编号和运行的验证准确率。
当我们再次训练模型时,我们可以看到正在创建以下文件:
model-weights.00-0.971304.hdf5
model-weights.02-0.977391.hdf5
model-weights.05-0.985217.hdf5
因此,我们可以看到在第 5 个阶段之后,我们无法达到val_acc的最佳水平,并且没有编写检查点。 然后,我们可以返回并从检查点 5 加载权重,并使用最佳模型。
这里有一些大的假设,将第 5 期称为最佳。 您可能需要多次运行网络,尤其是在您的数据集相对较小的情况下,就像本书中的早期示例一样。 我们可以肯定地说,这个结果将是不稳定的。
顺便说一下,这是防止过拟合的非常简单的方法。 我们可以选择使用方差太大之前发生的模型检查点。 这是做类似提前停止的一种方法,这意味着当我们看到模型没有改善时,我们会在指定的周期数之前停止训练。
在自定义回调中测量 ROC AUC
让我们再使用一个回调。 这次,我们将构建一个自定义的回调,以在每个周期结束时在训练集和测试集上计算曲线下的接收器工作特征区域(ROC AUC)。
在 Keras 中创建自定义回调实际上非常简单。 我们需要做的就是创建一个固有的Callback类,并覆盖所需的方法。 由于我们想在每个周期结束时计算 ROC AUC 分数,因此我们将在_epoch_end上覆盖:
from keras.callbacks import Callback
class RocAUCScore(Callback):
def __init__(self, training_data, validation_data):
self.x = training_data[0]
self.y = training_data[1]
self.x_val = validation_data[0]
self.y_val = validation_data[1]
super(RocAUCScore, self).__init__()
def on_epoch_end(self, epoch, logs={}):
y_pred = self.model.predict(self.x)
roc = roc_auc_score(self.y, y_pred)
y_pred_val = self.model.predict(self.x_val)
roc_val = roc_auc_score(self.y_val, y_pred_val)
print('\n *** ROC AUC Score: %s - roc-auc_val: %s ***' %
(str(roc), str(roc_val)))
return
现在,我们已经创建了新的自定义回调,我们可以将其添加到回调创建器函数中,如以下代码所示:
def create_callbacks(data):
tensorboard_callback = TensorBoard(log_dir=os.path.join(os.getcwd(),
"tb_log", "5h_adam_20epochs"), histogram_freq=1, batch_size=32,
write_graph=True, write_grads=False)
roc_auc_callback = RocAUCScore(training_data=(data["train_X"],
data["train_y"]), validation_data=(data["val_X"], data["val_y"]))
checkpoint_callback = ModelCheckpoint(filepath="./model-weights.
{epoch:02d}-{val_acc:.6f}.hdf5", monitor='val_acc',verbose=1,
save_best_only=True)
return [tensorboard_callback, roc_auc_callback, checkpoint_callback]
这里的所有都是它的! 您可以用相同的方式实现其他任何指标。
测量精度,召回率和 f1 得分
正如您可能对其他二分类器有丰富的经验一样,我认为用几句话讨论如何创建与更传统的二分类器一起使用的一些常规指标是明智的。
Keras 函数式 API 与 scikit-learn 中可能使用的 API 之间的区别是.predict()方法的行为。 当使用 Keras 时,对于n个样本中的每个,.predict()将返回k类概率的nxk矩阵。 对于二分类器,将只有一列,即类别 1 的类别概率。这使 Keras .predict()更像 scikit-learn 中的.predict_proba()。
在计算精度,召回率或其他基于类的指标时,您需要通过选择一些操作点来转换.predict()输出,如以下代码所示:
def class_from_prob(x, operating_point=0.5):
x[x >= operating_point] = 1
x[x < operating_point] = 0
return x
完成此操作后,您可以随意重用sklearn.metric中的典型指标,如以下代码所示:
y_prob_val = model.predict(data["val_X"])
y_hat_val = class_from_prob(y_prob_val)
print(classification_report(data["val_y"], y_hat_val))
总结
在本章中,我们讨论了使用深度神经网络作为二分类器。 我们花了很多时间讨论网络架构的设计选择,并提出了这样的想法,即搜索和试验是当前选择架构的最佳方法。
我们学习了如何在 Keras 中使用检查点回调来使我们能够及时返回并找到具有所需表现特征的模型版本。 然后,我们在训练的模型中创建并使用了自定义回调来衡量 ROC AUC 得分。 我们总结了如何将 Keras .predict()方法与sklearn.metrics中的传统指标结合使用。
在下一章中,我们将研究多分类,我们将更多地讨论如何防止过拟合。
五、使用 Keras 解决多分类问题
在本章中,我们将使用 Keras 和 TensorFlow 来处理具有许多自变量的 10 类多分类问题。 和以前一样,我们将讨论使用深度学习解决此问题的利弊; 但是,您不会发现很多缺点。 最后,我们将花费大量时间讨论控制过拟合的方法。
我们将在本章介绍以下主题:
- 多分类和深度神经网络
- 案例研究 – 手写数字分类
- 在 Keras 中建立多分类器
- 通过丢弃控制方差
- 通过正则化控制方差
多分类和深度神经网络
这里是! 我们终于找到了有趣的东西! 在本章中,我们将创建一个深度神经网络,该网络可以将观察结果分类为多个类别,这是神经网络确实发挥出色的地方之一。 让我们再谈一些关于深度神经网络对此类问题的好处。
就像我们都在谈论同一件事一样,让我们在开始之前定义多分类。 想象我们有一个分类器,该分类器将各种水果的权重作为输入,并根据给定的权重来预测水果。 输出可能恰好是一组类(苹果,香蕉,芒果等)中的一个类。 这是多分类,不要与多标签混淆,在这种情况下,模型可能会预测一组标签是否将应用于互不排斥的观察结果。
优点
当我们需要预测大量类时,相对于其他模型,深度神经网络的确是出色的执行者。 当输入向量中的特征数量变大时,神经网络自然适合。 当这两种情况都集中在同一个问题上时,我可能就是从那里开始的。 这正是我们将在本章中要研究的案例研究中看到的问题的类型。
缺点
和以前一样,更简单的模型可能会比深度学习模型做的更好或更好。 在所有其他条件都相同的情况下,您可能应该支持更简单的模型。 但是,随着类数的增加,深度神经网络复杂性的弊端通常会减少。 为了容纳许多类,许多其他模型的实现必须变得非常复杂,有些模型甚至可能需要优化作为超参数用于模型的多类策略。
案例研究 - 手写数字分类
我们将使用多分类网络来识别手写数字的相应类。 与以前一样,如果您想继续阅读,可以在本书的 Git 存储库中的Chapter05下找到本章的完整代码。
问题定义
MNIST数据集已成为几乎规范的神经网络数据集。 该数据集由 60,000 个手写数字组成的图像,属于代表它们各自数字(0, 1, 2 ... 9)的 10 类。 由于此数据集变得如此普遍,因此许多深度学习框架都在 API 中内置了 MNIST 加载方法。 TensorFlow 和 Keras 都拥有一个,我们将使用 Keras MNIST 加载器使我们的生活更轻松。 但是,如果您想从原始数据中获取数据,或者想进一步了解 MNIST 的历史,可以在这个页面中找到更多信息。
模型输入和输出
我们的数据集已被划分为一个训练集,该训练集的大小为 50,000 个观察值,一个测试集为 10,000 个观察值。 我将从训练集中获取最后 5,000 个观察值,并将其用作验证集。
拼合输入
每个输入观察都是一个 28 像素乘 28 像素的黑白图像。 像这样的一幅图像在磁盘上表示为28x28的矩阵,其值介于 0 到 255 之间,其中每个值都是该像素中黑色的强度。 至此,我们只知道如何在二维向量上训练网络(稍后我们将学习一种更好的方法); 因此我们将这个28x28矩阵展平为1 x 784输入向量。
一旦我们堆叠了所有这些1x784向量,就剩下50,000 x 784训练集。
如果您对卷积神经网络有丰富的经验,那么您可能现在正在翻白眼,如果您还没有,那么很快就会有更好的方法,但是不要太快地跳过本章。 我认为扁平化的MNIST是一个非常好的数据集,因为它的外观和行为与我们在许多投入领域(例如,物联网,制造业,生物,制药和医疗用例)中遇到的许多复杂的现实生活问题非常相似)。
类别输出
我们的输出层将为每个类包含一个神经元。 每个类别的关联神经元将经过训练,以将该类别的概率预测为介于 0 和 1 之间的值。我们将使用一种称为 softmax 的特殊激活,以确保所有这些输出总和为 1,我们将介绍 softmax 的详细信息。
这意味着我们将需要为我们的类创建一个二元/分类编码。 例如,如果我们使y = [0, 3, 2, 1]并对其进行分类编码,则将具有如下矩阵y:

幸运的是,Keras 为我们提供了方便的功能来进行这种转换。
成本函数
我们将使用的成本函数称为多项式交叉熵。 多项式交叉熵实际上只是在第 4 章“使用 Keras 进行二分类”中看到的二元交叉熵函数的概括。
让我们一起看看它们,而不只是显示分类交叉熵。 我要断言它们是平等的,然后解释原因:

前面的等式是正确的(m = 2时)
好吧,别害怕。 我知道,这是一堆数学。 绝对交叉熵方程是一直存在于右边的方程。 二元交叉熵紧随其后。 现在,设想m = 2的情况。 在这种情况下,您可能会发现,j = 0和j = 1的y[ij]log(p[ij])的和,对于i中的每个值,等于来自二元交叉熵的结果。 希望这种减少足以使分类交叉熵有意义。 如果没有,我建议选择一些值并进行编码。 只需一秒钟,稍后您将感谢我!
指标
分类交叉熵是一个很好的成本函数,但实际上并不能告诉我们很多我们可以从网络中获得的预测质量。 不幸的是,像 ROC AUC 这样的二分类指标也对我们没有太大帮助,因为我们超越了二分类 AUC 的定义并没有。
鉴于缺少更好的指标,我将使用准确率作为人类可以理解的训练指标。 幸运的是,在这种情况下,我的数据集是平衡的。 正如您所期望的那样,准确率是指真实值与预测值的匹配次数除以数据集的总大小。
训练结束后,我将使用 scikit-learn 的分类报告向我们显示每个类的精确度和召回率。 如果您愿意,也可以为此使用混淆矩阵。
在 Keras 中建立多分类器
由于我们现在有一个定义明确的问题,因此可以开始对其进行编码。 如前所述,这次我们必须对输入和输出进行一些转换。 在我们建立网络的过程中,我将向您展示这些内容。
载入 MNIST
对我们来说幸运的是,在 Keras 中内置了一个 MNIST 加载函数,该函数可以检索 MNIST 数据并为我们加载。 我们需要做的就是导入keras.datasets.mnist并使用load_data()方法,如以下代码所示:
(train_X, train_y), (test_X, test_y) = mnist.load_data()
train_X的形状为50,000 x 28 x 28。正如我们在“模型输入和输出”部分中所述,我们将需要将28x28矩阵展平为 784 个元素向量。 NumPy 使这变得非常容易。 以下代码说明了此技术:
train_X = train_X.reshape(-1, 784)
有了这种方式,我们应该考虑扩展输入。 以前,我们使用 scikit-learn 的StandardScaler。 MNIST 不需要这样做。 由于我们知道每个像素都在 0 到 255 的相同范围内,因此我们可以通过除以255轻松地将值转换为 0 和 1 之间的值,然后在执行操作之前将数据类型显式转换为float32,如以下代码所示:
train_X = train_X.astype('float32')
train_X /= 255
正如我们在“模型输入和输出”部分中所述,在加载数据时,我们可能应该将因变量向量转换为分类向量。 为此,我们将在以下代码的帮助下使用keras.utils.to_categorical():
train_y = to_categorical(train_y)
这样,我们的数据就可以进行训练了!
输入层
我们的输入层实际上与之前的示例保持不变,但我将在此处包括它以使其成为适当的快速参考:
def build_network(input_features=None):
inputs = Input(shape=(input_features,), name="input")
隐藏层
我将使用带有512神经元的第一个隐藏层。 这比输入向量的 784 个元素略小,但这完全不是规则。 同样,此架构只是一个开始,并不一定是最好的。 然后,我将在第二和第三隐藏层中浏览大小,如以下代码所示:
x = Dense(512, activation='relu', name="hidden1")(inputs)
x = Dense(256, activation='relu', name="hidden2")(x)
x = Dense(128, activation='relu', name="hidden3")(x)
输出层
我们的输出层将包含 10 个神经元,一个观察值可能属于其中的每个可能的类。 这对应于我们在y向量上使用to_categorical()时施加的编码:
prediction = Dense(10, activation='softmax', name="output")(x)
如您所见,我们正在使用的激活称为 softmax。 让我们讨论一下softmax是什么,以及为什么有用。
Softmax 激活
想象一下,如果不是使用深层神经网络,而是使用k个逻辑回归,其中每个回归都预测单个类中的成员。 逻辑回归的集合(每个类一个)如下所示:

使用这组逻辑回归的问题是每个逻辑回归的输出都是独立的。 想象一下,在我们的集合中,这些逻辑回归中的一些不确定其所属类别的成员资格,从而导致多个答案在P(Y = k) = 0.5附近。 这使我们无法将这些输出用作k类中类成员资格的总体概率,因为它们不一定总和为 1。
Softmax 压缩所有这些逻辑回归的输出,使它们的总和为 1,从而将其用作整体类成员的概率,从而为我们提供了帮助。
softmax函数如下所示:

(对于j = 1至k类,其中zj / zk是属于k的逻辑回归)
因此,如果将softmax函数放在我们先前的回归集的前面,我们将得到一组类别概率,它们合计为 1,可以用作 k 个类别中成员资格的概率。 这改变了我们的整体函数,如下所示:

先前的函数通常称为多项式逻辑回归。 它有点像一层,仅输出和神经网络。 我们不再频繁使用多项式逻辑回归。 但是,我们当然可以一直使用softmax函数。 对于本书中的大多数多分类问题,我们将使用softmax,因此值得理解。
如果您像我一样,并且发现所有数学知识都难以阅读,那么在代码中查看softmax可能会更容易。 因此,在继续操作之前,请使用以下代码段进行操作:
def softmax(z):
z_exp = [math.exp(x) for x in z]
sum_z_exp = sum(z_exp)
softmax = [round(i / sum_z_exp, 3) for i in z_exp]
return softmax
让我们快速尝试一个例子。 想象一下,我们有一组逻辑输出,如下所示:
z = np.array([0.9, 0.8, 0.2, 0.1, 0.5])
如果应用softmax,我们可以轻松地将这些输出转换为相对的类概率,如下所示:
print(softmax(z))
[0.284, 0.257, 0.141, 0.128, 0.19]
放在一起
现在我们已经涵盖了各个部分,让我们看一下我们的整个网络。 这看起来与我们之前在本书中介绍的模型相似。 但是,我们使用的损失函数categorical_crossentropy在本章的“成本函数”部分中介绍了。
我们将使用以下代码定义网络:
def build_network(input_features=None):
# first we specify an input layer, with a shape == features
inputs = Input(shape=(input_features,), name="input")
x = Dense(512, activation='relu', name="hidden1")(inputs)
x = Dense(256, activation='relu', name="hidden2")(x)
x = Dense(128, activation='relu', name="hidden3")(x)
prediction = Dense(10, activation='softmax', name="output")(x)
model = Model(inputs=inputs, outputs=prediction)
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=["accuracy"])
return model
训练
现在我们已经定义了神经网络并加载了数据,剩下的就是训练它了。
在本书中以及本书的其他几个示例中,我使用的是称为数据的字典,以绕过train_X,val_X和test_X等各种数据集。 我使用这种表示法来保持代码的可读性,并且因为传递整个字典的必要性经常高于没有。
这是我将如何训练我们刚刚建立的模型的方法。
model = build_network(data["train_X"].shape[1])
model.fit(x=data["train_X"], y=data["train_y"],
batch_size=30,
epochs=50,
validation_data=(data["val_X"], data["val_y"]),
verbose=1,
callbacks=callbacks)
我正在使用与以前相同的回调。 我没有使用我们在第 4 章“使用 Keras 进行二分类”中构建的 ROC AUC 回调,因为 ROC AUC 没有为多分类器明确定义。
存在一些针对该问题的创造性解决方案。 例如,通过成对分析近似多类 ROC 和 ROC 表面下体积都是出色的论文,都可以解决这个问题。 但是,实际上,这些方法及其度量标准很少在 R 中使用,最常在 R 中实现。因此,到目前为止,让我们坚持使用多类准确率,并且远离 R。
让我们观看 TensorBoard 在我们的模型训练中:

在阅读下一段之前,请花点时间思考一下这些图形在告诉我们什么。 得到它了? 好的,让我们继续。
因此,这是一个熟悉的情况。 我们的训练损失正在继续下降,而我们的验证损失正在上升。 我们过拟合。 虽然当然可以选择提前停止,但让我向您展示一些处理过拟合的新技巧。 让我们在下一部分中查看丢弃法和 l2 正则化。 但是,在进行此操作之前,我们应该研究如何使用多类网络来测量准确率和进行预测。
在多类模型中使用 scikit-learn 指标
和以前一样,我们可以借鉴 scikit-learn 的指标来衡量我们的模型。 但是,为此,我们需要从模型的y的分类输出中进行一些简单的转换,因为 scikit-learn 需要使用类标签,而不是二元类指示器。
为了取得飞跃,我们将使用以下代码开始进行预测:
y_softmax = model.predict(data["test_X"])
然后,我们将选择概率最大的类的索引,使用以下代码将其方便地作为该类:
y_hat = y_softmax.argmax(axis=-1)
然后,我们可以像以前一样使用 scikit-learn 的分类报告。 相同的代码如下:
from sklearn.metrics import classification_report
print(classification_report(test_y, y_hat))
现在,我们实际上可以查看所有 10 个类的精度,召回率和 f1 得分。 下图说明了sklearn.metrics.classification_report()的输出:

通过丢弃控制方差
减少深度神经网络过拟合的一种非常好的方法是采用一种称为丢弃法的技术。 丢弃法完全按照其说的去做,它使神经元脱离隐藏层。 运作方式如下。
通过每个小批量,我们将随机选择关闭每个隐藏层中的节点。 想象一下,我们在某个隐藏层中实现了丢弃,并且我们选择了丢弃率为 0.5。 这意味着,对于每个小批量,对于每个神经元,我们都掷硬币以查看是否使用该神经元。 这样,您可能会随机关闭该隐藏层中大约一半的神经元:

如果我们一遍又一遍地执行此操作,就好像我们正在训练许多较小的网络。 模型权重保持相对较小,每个较小的网络不太可能过拟合数据。 这也迫使每个神经元减少对其他神经元的依赖。
丢弃法效果惊人,可以很好地解决您可能遇到的许多(如果不是大多数)深度学习问题的过拟合问题。 如果您具有高方差模型,则丢弃是减少过拟合的好选择。
Keras 包含一个内置的Dropout层,我们可以轻松地在网络中使用它来实现Dropout。 Dropout层将简单地随机关闭前一层神经元的输出,以使我们轻松地改造网络以使用Dropout。 要使用它,除了我们正在使用的其他层类型之外,我们还需要首先导入新层,如以下代码所示:
from keras.layers import Input, Dense, Dropout
然后,我们只需将Dropout层插入模型,如以下代码所示:
def build_network(input_features=None):
# first we specify an input layer, with a shape == features
inputs = Input(shape=(input_features,), name="input")
x = Dense(512, activation='relu', name="hidden1")(inputs)
x = Dropout(0.5)(x)
x = Dense(256, activation='relu', name="hidden2")(x)
x = Dropout(0.5)(x)
x = Dense(128, activation='relu', name="hidden3")(x)
x = Dropout(0.5)(x)
prediction = Dense(10, activation='softmax', name="output")(x)
model = Model(inputs=inputs, outputs=prediction)
model.compile(optimizer='adam', loss='categorical_crossentropy',
metrics=["accuracy"])
return model
这是我们先前使用的确切模型; 但是,我们在每个Dense层之后都插入了Dropout层,这是我通常在实现丢弃时开始的方式。 像其他模型架构决策一样,您可以选择仅在某些层,所有层或没有层中实现丢弃。 您还可以选择更改退出/保留概率; 但是,我确实建议从 0.5 开始,因为它通常效果很好。
一个安全的选择是在每一层都退出,保持概率为 0.5。 不错的第二种尝试是仅在第一层使用丢弃。
让我们用丢弃法训练我们的新模型,看看它与我们的第一次尝试相比如何:

首先让我们看一下验证准确率。 使用丢弃模型的训练速度与未规范模型的训练速度一样快,但是在这种情况下,它的确似乎很快就开始加速。 看看在第 44 个周期的验证准确率。它比非正规模型略好。
现在,让我们看看验证损失。 您可以看到丢弃法对模型过拟合的影响,而且确实非常明显。 虽然仅转换为最终产品的少量改进,但丢弃法表现相当不错,可以防止我们的验证损失提升。
通过正则化控制方差
正则化是控制过拟合的另一种方法,当模型中的各个权重增大时会对其进行惩罚。 如果您熟悉线性模型(例如线性和逻辑回归),那么它与在神经元级别应用的技术完全相同。 可以使用两种形式的正则化,称为 L1 和 L2,来对神经网络进行正则化。 但是,由于 L2 正则化计算效率更高,因此几乎总是在神经网络中使用它。
快速地,我们需要首先规范化成本函数。 如果我们将C₀,分类交叉熵作为原始成本函数,则正规化的cost函数将如下所示:

这里,λ是可以增加或减少以更改应用的正则化量的正则化参数。 此正则化参数会惩罚较大的权重值,从而使网络总体上希望具有较小的权重。
要更深入地了解神经网络中的正则化,请查看 Michael Nielsen 的《神经网络和深度学习》的第 3 章。
可以将正则化应用于 Keras 层中的权重,偏差和激活。 我将使用带有默认参数的 L2 演示此技术。 在以下示例中,我将正则化应用于每个隐藏层:
def build_network(input_features=None):
# first we specify an input layer, with a shape == features
inputs = Input(shape=(input_features,), name="input")
x = Dense(512, activation='relu', name="hidden1", kernel_regularizer='l2') \
(inputs)
x = Dense(256, activation='relu', name="hidden2", kernel_regularizer='l2')(x)
x = Dense(128, activation='relu', name="hidden3", kernel_regularizer='l2')(x)
prediction = Dense(10, activation='softmax', name="output")(x)
model = Model(inputs=inputs, outputs=prediction)
model.compile(optimizer='adam', loss='categorical_crossentropy',
metrics=["accuracy"])
return model
因此,让我们将默认的 L2 正则化与其他两个模型进行比较。 下图显示了比较:

不幸的是,我们的新 L2 正则化网络很容易找到。 在这种情况下,似乎 L2 正则化效果很好。 我们的网络现在偏差严重,对其他两个方面的了解还不够。
如果我真的确定要使用正则化来解决此问题,那么我将首先更改正则化率并尝试找到更合适的值,但我们相距甚远,我对此表示怀疑,我们会做得比我们更好 dropout模型。
总结
在本章中,我们实际上已经开始了解深度神经网络在进行多分类时的威力。 我们详细介绍了softmax函数,然后我们构建并训练了一个网络来将手写数字分为 10 个各自的类别。
最后,当我们注意到模型过拟合时,我们尝试同时使用丢弃和 L2 正则化来减少模型的方差。
到目前为止,您已经看到深度神经网络需要很多选择,关于架构的选择,学习率,甚至是正则化率。 我们将在下一章中学习如何优化这些选择。
六、超参数优化
使用深度神经网络的最大缺点之一是它们具有许多应优化的超参数,以使网络发挥最佳表现。 在前面的每个章节中,我们都遇到但没有涵盖超参数估计的挑战。 超参数优化是一个非常重要的话题。 在大多数情况下,这是一个未解决的问题,尽管我们不能涵盖本书的全部主题,但我认为它仍然值得一章。
在本章中,我将为您提供一些我认为是选择超参数的实用建议。 可以肯定的是,由于本章是基于我自己的经验,因此本章可能会有些偏颇和偏颇。 我希望经验会有所帮助,同时也带您进一步对该主题进行调查。
我们将在本章介绍以下主题:
- 是否应该将网络架构视为超参数?
- 我们应该优化哪些超参数?
- 超参数优化策略
是否应该将网络架构视为超参数?
在构建最简单的网络时,我们必须对网络架构做出各种选择。 我们应该使用 1 个隐藏层还是 1,000 个? 每层应包含多少个神经元? 他们都应该使用relu激活函数还是tanh? 我们应该在每个隐藏层上还是仅在第一层上使用丢弃? 在设计网络架构时,我们必须做出许多选择。
在最典型的情况下,我们穷举搜索每个超参数的最佳值。 但是,要穷举搜索网络架构并不容易。 实际上,我们可能没有时间或计算能力。 我们很少看到研究人员通过穷举搜索来寻找最佳架构,因为选择的数量非常多,而且存在不只一个正确的答案。 取而代之的是,我们看到该领域的研究人员通过实验尝试建立已知的架构,以尝试创建新的新颖架构并改善现有架构。
因此,在介绍详尽搜索超参数的策略之前,让我们看一下两种推论出合理的,甚至不是最佳的网络架构的策略。
找到一个巨人然后站在他的肩膀上
沙特尔的伯纳德(Bernard of Chartres)被赋予了通过借鉴他人的发现来学习的概念。 但是,正是艾萨克·牛顿(Isaac Newton)说:“如果我进一步观察,那就是站在巨人的肩膀上。” 要明确的是,这正是我在这里建议的。
如果我要设计一个用于新的深度学习问题的网络架构,我要做的第一件事就是尝试找到一个令人满意的方式,以前已经解决了类似的问题。 尽管可能没有人能够解决您面临的任务,但可能存在类似的情况。
很可能存在几种可能的解决方案。 如果是这样,并且在时间允许的情况下,每次运行几次的平均结果可能会告诉您哪个运行效果最好。 当然,在这里我们发现自己很快进入了研究领域。
添加,直到过拟合,然后进行正则化
希望通过寻找类似问题的架构,您至少接近适合您的架构。 您如何做才能进一步优化网络架构?
- 在多个实验运行中,添加层和/或神经元,直到您的网络开始针对问题过拟合。 在深度学习中,添加单元,直到您不再具有高偏差模型为止。
- 一旦开始过拟合,您就会发现一些网络架构能够很好地拟合训练数据,甚至可能拟合得很好。 在这一点上,您应该集中精力通过使用丢弃,正则化,提早停止等方法来减少方差。
这种方法通常归因于著名的神经网络研究员 Geoffrey Hinton。 这是一个有趣的想法,因为它使过拟合不是要避免的事情,而是构建网络架构的良好第一步。
尽管没有规则可供我们选择最佳网络架构,并且可能存在许多最佳架构,但我发现这种策略在实践中对我来说非常有效。
实用建议
如果您对上述内容不太了解,我同意。 这对我也不是,我也不希望那样。 您当然可以在一组预定义的配置之间搜索最佳的网络架构,这也是正确的方法。 实际上,它可以说是更正确,更严格。 此过程旨在为您提供实用的建议,以帮助您在尽可能短的时间内达到最佳状态。
我们应该优化哪些超参数?
即使您遵循我的建议并选择了一个足够好的架构,您也可以并且仍然应该尝试在该架构中搜索理想的超参数。 我们可能要搜索的一些超参数包括:
- 我们选择的优化器。 到目前为止,我一直在使用 Adam,但是 rmsprop 优化器或调整良好的 SGD 可能会更好。
- 每个优化器都有一组我们可能需要调整的超参数,例如学习率,动量和衰减。
- 网络权重初始化。
- 神经元激活。
- 正则化参数(例如丢弃概率)或 12 正则化中使用的正则化参数。
- 批次大小。
如上所述,这不是详尽的清单。 当然,您可以尝试更多的选择,包括在每个隐藏层中引入可变数量的神经元,每层中丢弃概率的变化等等。 就像我们一直暗示的那样,超参数的可能组合是无限的。 这些选择也很可能并非独立于网络架构,添加和删除层可能会为这些超参数中的任何一个带来新的最佳选择。
超参数优化策略
在本章的这一点上,我们建议,在大多数情况下,尝试我们可能想尝试的每个超参数组合在计算上都是不可能的,或者至少是不切实际的。 深度神经网络肯定会花费很长时间进行训练。 尽管您可以并行处理问题并投入计算资源,但搜索超参数的最大限制可能仍然是时间。
如果时间是我们最大的限制,并且我们无法合理地探索拥有的所有可能性,那么我们将必须制定一种策略,使我们在拥有的时间内获得最大的效用。
在本节的其余部分,我将介绍一些用于超参数优化的常用策略,然后向您展示如何使用我最喜欢的两种方法在 Keras 中优化超参数。
通用策略
在所有机器学习模型中都有一套通用的超参数优化策略。 从总体上讲,这些策略包括:
- 网格搜索
- 随机搜索
- 贝叶斯优化
- 遗传算法
- 机器学习的超参数
网格搜索只是尝试尝试所有事物,或者至少尝试离散事物,然后报告我们用蛮力找到的最佳超参数的最佳组合。 可以保证在我们确定的参数空间中找到最佳解决方案,以及其他较差的解决方案。
网格搜索对于深度学习并不是很实用。 除了最基本的深度神经网络,我们无法现实地探索所有可能参数的每个可能值。 使用随机搜索,我们从每个参数分布中随机抽样,并尝试其中的n,其中(n x每个示例训练时间)是我们愿意分配给这个问题的时间预算。
贝叶斯优化方法使用以前的观察结果来预测接下来要采样的超参数集。 尽管贝叶斯优化方法通常胜过蛮力技术,但目前的研究表明,与穷举方法相比,表现提升较小。 此外,由于贝叶斯方法取决于先前的经验,因此无论如何都不会令人尴尬地并行进行。
遗传算法是机器学习中非常有趣且活跃的研究领域。 但是,我目前的观点是,它们也不是深度神经网络参数优化的理想选择,因为它们再次依赖于先前的经验。
该领域中的一些最新研究着眼于训练神经网络,该神经网络可以预测给定网络架构的最佳参数。 可以参数化模型的模型的想法当然非常有趣,这是一个值得密切关注的地方。 这也可能是我们获得天网的方式。 只有时间证明一切。
在 scikit-learn 中使用随机搜索
使用 scikit-learn 可以轻松实现网格搜索和随机搜索。 在此示例中,我们将使用 Keras 的KerasClassifier类包装模型并使其与 scikit-learn API 兼容。 然后,我们将使用 scikit-learn 的RandomSearchCV类进行超参数搜索。
为此,我们将从稍微更改现在熟悉的模型构建函数开始。 我们将使用我们要搜索的超参数对其进行参数化,如以下代码所示:
def build_network(keep_prob=0.5, optimizer='adam'):
inputs = Input(shape=(784,), name="input")
x = Dense(512, activation='relu', name="hidden1")(inputs)
x = Dropout(keep_prob)(x)
x = Dense(256, activation='relu', name="hidden2")(x)
x = Dropout(keep_prob)(x)
x = Dense(128, activation='relu', name="hidden3")(x)
x = Dropout(keep_prob)(x)
prediction = Dense(10, activation='softmax', name="output")(x)
model = Model(inputs=inputs, outputs=prediction)
model.compile(optimizer=optimizer, loss='categorical_crossentropy',
metrics=["accuracy"])
return model
在此示例中,我想搜索一个理想的丢弃值,并且我想尝试几个不同的优化器。 为了实现这一点,我需要将它们作为参数包含在函数中,以便可以通过我们的随机搜索方法对其进行更改。 当然,我们可以使用相同的方法来参数化和测试许多其他网络架构选择,但是我们在这里保持简单。
接下来,我们将创建一个函数,该函数返回一个字典,其中包含我们想搜索的所有可能的超参数及其值空间,如以下代码所示:
def create_hyperparameters():
batches = [10, 20, 30, 40, 50]
optimizers = ['rmsprop', 'adam', 'adadelta']
dropout = np.linspace(0.1, 0.5, 5)
return {"batch_size": batches, "optimizer": optimizers,
"keep_prob": dropout}
剩下的就是使用RandomSearchCV将这两部分连接在一起。 首先,我们将模型包装到keras.wrappers.scikit_learn.KerasClassifier中,以便与 scikit-learn 兼容,如以下代码所示:
model = KerasClassifier(build_fn=build_network, verbose=0)
接下来,我们将使用以下代码获得超参数字典:
hyperparameters = create_hyperparameters()
然后,最后,我们将创建一个RandomSearchCV对象,该对象将用于搜索模型的参数空间,如以下代码所示:
search = RandomizedSearchCV(estimator=model, param_distributions=hyperparameters, n_iter=10, n_jobs=1, cv=3, verbose=1)
拟合此RandomizedSearchCV对象后,它将从参数分布中随机选择值并将其应用于模型。 它将执行 10 次(n_iter=10),并且将尝试每种组合 3 次,因为我们使用了 3 倍交叉验证。 这意味着我们将总共拟合模型 30 次。 使用每次运行的平均准确率,它将返回最佳模型作为类属性.best_estimator,并且将返回最佳参数作为.best_params_。
为了适合它,我们只需调用它的fit方法,就好像它是一个模型一样,如以下代码所示:
search.fit(data["train_X"], data["train_y"])
print(search.best_params_)
在 Tesla K80 GPU 实例上,在上述网格上拟合第 5 章,“使用 Keras 进行多分类”所使用的 MNIST 模型。 在完成本节之前,让我们看一下搜索的一些输出,如以下代码所示:
Using TensorFlow backend.
Fitting 3 folds for each of 10 candidates, totalling 30 fits
tensorflow/core/common_runtime/gpu/gpu_device.cc:1030] Found device 0 with properties:
name: Tesla K80 major: 3 minor: 7 memoryClockRate(GHz): 0.8235
pciBusID: 0000:00:1e.0
totalMemory: 11.17GiB freeMemory: 11.10GiB
tensorflow/core/common_runtime/gpu/gpu_device.cc:1120] Creating TensorFlow device (/device:GPU:0) -> (device: 0, name: Tesla K80, pci bus id: 0000:00:1e.0, compute capability: 3.7)
[Parallel(n_jobs=1)]: Done 30 out of 30 | elapsed: 8.8min finished
{'keep_prob': 0.20000000000000001, 'batch_size': 40, 'optimizer': 'adam'}
如您在此输出中看到的,在 10 次运行中,加粗的超参数似乎是表现最好的集合。 当然,我们当然可以运行更多的迭代,并且我们可能会找到一个更好的选择。 我们的预算仅由时间,耐心以及云帐户附带的信用卡决定。
Hyperband
Hyperband 是一项超参数优化技术,由 Lisha Li,Kevin Jamieson,Guilia DeSalvo,Afshin Rostamizadeh 和 Ameet Talwalker 于 2016 年在伯克利开发。 您可以在这里阅读他们的原始论文。
想象一下,就像我们在RandomSearchCV中所做的那样,随机采样许多潜在的超参数集。 完成RandomSearchCV后,它将选择一个单一的超参数配置作为其采样的最优值。 Hyperband 利用这样的思想,即即使经过少量迭代,最佳的超参数配置也可能会胜过其他配置。 Hyperband 中的乐队来自土匪,指的是基于多臂土匪技术(用于优化竞争选择之间的资源分配以优化表现为目标的技术)的勘探与开发。
使用 Hyperband,我们可以尝试一些可能的配置集(n),仅训练一次迭代。 作者将迭代一词留作多种可能的用途。 但是,我将周期作为迭代。 一旦完成第一个训练循环,就将根据表现对结果进行配置。 然后,对该列表的上半部分进行大量迭代的训练。 然后重复进行减半和剔除的过程,我们得到了一些非常小的配置集,我们将针对在搜索中定义的完整迭代次数进行训练。 与在每种可能的配置中搜索最大周期相比,此过程使我们在更短的时间内获得了最佳超参数集。
在本章的 GitHub 存储库中,我在hyperband.py中包括了hyperband算法的实现。 此实现主要源自 FastML 的实现,您可以在这个页面中找到。 要使用它,您需要首先实例化一个hyperband对象,如以下代码所示:
from hyperband import Hyperband
hb = Hyperband(data, get_params, try_params)
Hyperband 构造器需要三个参数:
data:到目前为止,我在示例中一直在使用的数据字典get_params:用于从我们正在搜索的超参数空间中采样的函数的名称try_param:可用于评估n_iter迭代的超参数配置并返回损失的函数的名称
在下面的示例中,我实现了get_params以在参数空间中以统一的方式进行采样:
def get_params():
batches = np.random.choice([5, 10, 100])
optimizers = np.random.choice(['rmsprop', 'adam', 'adadelta'])
dropout = np.random.choice(np.linspace(0.1, 0.5, 10))
return {"batch_size": batches, "optimizer": optimizers,
"keep_prob": dropout}
如您所见,所选的超参数配置将作为字典返回。
接下来,可以实现try_params以在超参数配置上针对指定的迭代次数拟合模型,如下所示:
def try_params(data, num_iters, hyperparameters):
model = build_network(keep_prob=hyperparameters["keep_prob"],
optimizer=hyperparameters["optimizer"])
model.fit(x=data["train_X"], y=data["train_y"],
batch_size=hyperparameters["batch_size"],
epochs=int(num_iters))
loss = model.evaluate(x=data["val_X"], y=data["val_y"], verbose=0)
return {"loss": loss}
try_params函数返回一个字典,可用于跟踪任何数量的度量; 但是,由于它用于比较运行,因此需要损失。
通过在对象上调用.run()方法,hyperband对象将通过我们上面描述的算法运行。
results = hb.run()
在这种情况下,results将是每次运行,其运行时间和测试的超参数的字典。 因为即使这种高度优化的搜索都需要花费大量时间,并且 GPU 时间也很昂贵,所以我将 MNIST 搜索的结果包括在本章的 GitHub 存储库的hyperband-output-mnist.txt中,可以在以下位置找到。
总结
超参数优化是从我们的深度神经网络获得最佳效果的重要一步。 寻找搜索超参数的最佳方法是机器学习研究的一个开放而活跃的领域。 尽管您当然可以将最新技术应用于自己的深度学习问题,但您需要在决策中权衡实现的复杂性和搜索运行时间。
有一些与网络架构有关的决策可以肯定地进行详尽地搜索,但是,如我上面提供的那样,一组启发式方法和最佳实践可能使您足够接近甚至减少搜索参数的数量。
最终,超参数搜索是一个经济问题,任何超参数搜索的第一部分都应考虑您的计算时间和个人时间预算,以试图找出最佳的超参数配置。
本章总结了深度学习的基础。 在下一章中,我们将从计算机视觉入手,介绍神经网络的一些更有趣和更高级的应用。
七、从头开始训练 CNN
深度神经网络彻底改变了计算机视觉。 实际上,我认为在最近几年中计算机视觉的进步已经使深层神经网络成为许多消费者每天使用的东西。 我们已经在第 5 章“使用 Keras 进行多分类”中使用计算机视觉分类器,其中我们使用了深度网络对手写数字进行分类。 现在,我想向您展示卷积层如何工作,如何使用它们以及如何在 Keras 中构建自己的卷积神经网络以构建更好,功能更强大的深度神经网络来解决计算机视觉问题。
我们将在本章介绍以下主题:
- 卷积介绍
- 在 Keras 中训练卷积神经网络
- 使用数据增强
卷积介绍
经过训练的卷积层由称为过滤器的许多特征检测器组成,这些特征检测器在输入图像上滑动作为移动窗口。 稍后我们将讨论过滤器内部的内容,但现在它可能是一个黑匣子。 想象一个已经训练过的过滤器。 也许该过滤器已经过训练,可以检测图像中的边缘,您可能会认为这是黑暗与明亮之间的过渡。 当它经过图像时,其输出表示它检测到的特征的存在和位置,这对于第二层过滤器很有用。 稍微扩展一下我们的思想实验,现在想象第二个卷积层中的一个过滤器,它也已经被训练过了。 也许这个新层已经学会了检测直角,其中存在由上一层找到的两个边缘。 不断地我们去; 随着我们添加层,可以了解更多复杂的特征。 特征层次结构的概念对于卷积神经网络至关重要。 下图来自 Honglak Lee 等人的《使用卷积深度信念网络的无监督学习层次表示》[2011],非常好地说明了特征层次结构的概念:

这是一种非常强大的技术,它比我们先前在 MNIST 上使用的深度学习flatten和classify方法具有多个优势。 我们将在短期内讨论这些内容,但首先让我们深入了解过滤器。
卷积层如何工作?
在上一节中,我说过卷积层是一组充当特征检测器的过滤器。 在我们深入探讨该架构之前,让我们回顾一下卷积实际上是什么的数学。
让我们首先手动将以下4 x 4矩阵与3 x 3矩阵卷积,我们将其称为过滤器。 卷积过程的第一步是获取过滤器与4 x 4矩阵的前九个框的按元素乘积:

完成此操作后,我们将过滤器滑到一行上并执行相同的操作。 最后,我们将过滤器向下滑动,然后再次滑动。 卷积过程一旦完成,将使我们剩下2x2矩阵,如下图所示:

从技术上讲,这不是卷积,而是互相关。 按照惯例,我们将其称为卷积,并且就我们的目的而言,差异确实很小。
三维卷积
MNIST 是一个灰度示例,我们可以将每个图像表示为二维矩阵中从 0 到 255 的像素强度值。 但是,大多数时候,我们将使用彩色图像。 彩色图像实际上是三维矩阵,其中维是图像高度,图像宽度和颜色。 这将为图像中的每个像素生成一个矩阵,分别具有红色,蓝色和绿色值。
虽然我们先前展示的是二维过滤器,但我们可以通过在(高度,宽度,3(颜色))矩阵与3 x 3 x 3之间进行卷积来将其思想轻松转换为三个维度。 过滤。 最后,当我们在矩阵的所有三个轴上进行逐元素乘积运算时,仍然剩下二维输出。 提醒一下,这些高维矩阵通常称为张量,而我们正在做的就是使它们流动。
卷积层
之前我们已经讨论了由多个线性函数单元以及一些非线性(例如relu)组成的深度神经网络层。 在卷积层中,每个单元都是一个过滤器,结合了非线性。 例如,可以在 Keras 中定义卷积层,如下所示:
from keras.layers import Conv2D
Conv2D(64, kernel_size=(3,3), activation="relu", name="conv_1")
在此层中,有 64 个独立的单元,每个单元都有3 x 3 x 3过滤器。 卷积操作完成后,每个单元都会像传统的完全连接层中那样为输出添加偏置和非线性(稍后会详细介绍该术语)。
在继续之前,让我们快速浏览一下示例的维度,以便确保我们都在同一页面上。 想象一下,我们有一个32 x 32 x 3的输入图像。 现在,我们将其与上述卷积层进行卷积。 该层包含 64 个过滤器,因此输出为30 x 30 x 64。 每个过滤器输出一个30 x 30矩阵。
卷积层的好处
因此,现在您希望对卷积层的工作原理有所了解,让我们讨论为什么我们要进行所有这些疯狂的数学运算。 为什么我们要使用卷积层而不是以前使用的普通层?
假设我们确实使用了普通层,以得到与之前讨论的相同的输出形状。 我们从32 x 32 x 3图像开始,所以总共有 3,072 个值。 我们剩下一个30 x 30 x 64矩阵。 总共有 57,600 个值。 如果我们要使用完全连接的层来连接这两个矩阵,则该层将具有 176,947,200 个可训练参数。 那是 1.76 亿。
但是,当我们使用上面的卷积层时,我们使用了 64 个3 x 3 x 3过滤器,这将导致 1,728 个可学习权重加 64 个偏差(总共 1,792 个参数)。
因此,显然卷积层需要的参数要少得多,但是为什么这很重要呢?
参数共享
由于过滤器是在整个图像中使用的,因此过滤器会学会检测特征,而不管其在图像中的位置如何。 事实证明,这非常有用,因为它为我们提供了平移不变性,这意味着我们可以检测到重要的内容,而不管其在整个图像中的朝向。
回想一下 MNIST,不难想象我们可能想检测 9 的循环,而不管它在照片中的位置如何。 提前思考,想象一个将图片分类为猫或汽车的分类器。 容易想象有一组过滤器可以检测出像汽车轮胎一样复杂的东西。 无论轮胎的方向在图像中的什么位置,检测该轮胎都是有用的,因为轮胎之类的东西强烈表明该图像不是猫(除非图像是驾驶汽车的猫)。
本地连接
过滤器由于其固定大小而着重于相邻像素之间的连通性。 这意味着他们将最强烈地学习本地特征。 当与其他过滤器以及层和非线性结合使用时,这使我们逐渐关注更大,更复杂的特征。 确实需要这种局部化特征的堆叠,这也是卷积层如此之大的关键原因。
池化层
除了卷积层,卷积神经网络通常使用另一种类型的层,称为池化层。 当添加卷积层时,使用池化层来减少卷积网络的维数,这会减少过拟合。 它们具有使特征检测器更坚固的附加好处。
池化层将矩阵划分为非重叠部分,然后通常在每个区域中采用最大值(在最大池化的情况下)。 可替代地,可以采用平均值。 但是,目前很少使用。 下图说明了此技术:

如我们所料,池化层在 Keras 中很容易实现。 以下代码可用于池化各层:
from keras.layers import MaxPooling2D
pool1 = MaxPooling2D(pool_size=(2, 2), name="pool_1")
在这里,我们将池窗口定义为2 x 2。
尽管我们之前没有讨论过填充,但是在某些架构中,通常将卷积层或池化层的输入填充为 0,以使输出尺寸等于输入。 Keras 的卷积层和池化层中的默认值都是有效填充,这意味着按惯例没有填充。 如果需要,参数padding="same"将应用填充。
批量标准化
批量规范化有助于我们的网络整体表现更好,学习速度更快。 批量规范化在应用中也很容易理解。 但是,为什么它起作用,仍然受到研究人员的争议。
使用批量归一化时,对于每个小批量,我们可以在每个非线性之后(或之前)对那个批量进行归一化,使其平均值为 0,单位方差。 这使每一层都可以从中学习标准化输入,从而使该层的学习效率更高。
批归一化层很容易在 Keras 中实现,本章的示例将在每个卷积层之后使用它们。 以下代码用于批量规范化:
from keras.layers import BatchNormalization
x = BatchNormalization(name="batch_norm_1")
在 Keras 中训练卷积神经网络
现在我们已经介绍了卷积神经网络的基础知识,是时候构建一个了。 在本案例研究中,我们将面对一个众所周知的问题,即 CIFAR-10。 该数据集由 Alex Krizhevsky,Vinod Nair 和 Geoffrey Hinton 创建。
输入
CIFAR-10 数据集由属于 10 类的 60,000 张32 x 32彩色图像组成,每类 6,000 张图像。 我将使用 50,000 张图像作为训练集,使用 5,000 张图像作为验证集,并使用 5,000 张图像作为测试集。
卷积神经网络的输入张量层将为(N, 32, 32, 3),我们将像以前一样将其传递给build_network函数。 以下代码用于构建网络:
def build_network(num_gpu=1, input_shape=None):
inputs = Input(shape=input_shape, name="input")
输出
该模型的输出将是 0-9 之间的类别预测。 我们将使用与 MNIST 相同的 10 节点softmax。 令人惊讶的是,我们的输出层没有任何变化。 我们将使用以下代码来定义输出:
output = Dense(10, activation="softmax", name="softmax")(d2)
成本函数和指标
在第 5 章中,我们使用分类交叉熵作为多分类器的损失函数。 这只是另一个多分类器,我们可以继续使用分类交叉熵作为我们的损失函数,并使用准确率作为度量。 我们已经开始使用图像作为输入,但是幸运的是我们的成本函数和指标保持不变。
卷积层
如果您开始怀疑此实现中是否会有任何不同之处,那就是这里。 我将使用两个卷积层,分别进行批量规范化和最大池化。 这将要求我们做出很多选择,当然我们以后可以选择作为超参数进行搜索。 不过,最好先让某些东西开始工作。 正如 Donald Knuth 所说,过早的优化是万恶之源。 我们将使用以下代码片段定义两个卷积块:
# convolutional block 1
conv1 = Conv2D(64, kernel_size=(3,3), activation="relu", name="conv_1")(inputs)
batch1 = BatchNormalization(name="batch_norm_1")(conv1)
pool1 = MaxPooling2D(pool_size=(2, 2), name="pool_1")(batch1)
# convolutional block 2
conv2 = Conv2D(32, kernel_size=(3,3), activation="relu", name="conv_2")(pool1)
batch2 = BatchNormalization(name="batch_norm_2")(conv2)
pool2 = MaxPooling2D(pool_size=(2, 2), name="pool_2")(batch2)
因此,很明显,我们在这里有两个卷积块,它们由一个卷积层,一个批量规范化层和一个池化层组成。
在第一块中,我使用具有relu激活函数的 64 个3 x 3过滤器。 我使用的是有效(无)填充,跨度为 1。批量规范化不需要任何参数,并且实际上不是可训练的。 池化层使用2 x 2池化窗口,有效填充和跨度为 2(窗口尺寸)。
第二个块几乎相同。 但是,我将过滤器数量减半为 32。
尽管在该架构中有许多旋钮可以转动,但我首先要调整的是卷积的内核大小。 内核大小往往是一个重要的选择。 实际上,一些现代的神经网络架构(例如 Google 的 Inception)使我们可以在同一卷积层中使用多个过滤器大小。
全连接层
经过两轮卷积和合并后,我们的张量变得相对较小和较深。 在pool_2之后,输出尺寸为(n, 6, 6, 32)。
我们希望在这些卷积层中提取此6 x 6 x 32张量表示的相关图像特征。 为了使用这些特征对图像进行分类,在进入最终输出层之前,我们将将该张量连接到几个完全连接的层。
在此示例中,我将使用 512 神经元完全连接层,256 神经元完全连接层以及最后的 10 神经元输出层。 我还将使用丢弃法来帮助防止过拟合,但只有一点点! 该过程的代码如下,供您参考:
from keras.layers import Flatten, Dense, Dropout
# fully connected layers
flatten = Flatten()(pool2)
fc1 = Dense(512, activation="relu", name="fc1")(flatten)
d1 = Dropout(rate=0.2, name="dropout1")(fc1)
fc2 = Dense(256, activation="relu", name="fc2")(d1)
d2 = Dropout(rate=0.2, name="dropout2")(fc2)
我之前没有提到上面的flatten层。 flatten层完全按照其名称的含义执行。 将flattens,n x 6 x 6 x 32张量flattens转换为n x 1152向量。 这将作为全连接层的输入。
Keras 中的多 GPU 模型
许多云计算平台可以提供包含多个 GPU 的实例。 随着我们模型的规模和复杂性的增长,您可能希望能够跨多个 GPU 并行化工作负载。 这在本机 TensorFlow 中可能涉及到一些过程,但是在 Keras 中,这只是一个函数调用。
正常构建模型,如以下代码所示:
model = Model(inputs=inputs, outputs=output)
然后,我们借助以下代码将该模型传递给keras.utils.multi_gpu_model:
model = multi_gpu_model(model, num_gpu)
在此示例中,num_gpu是我们要使用的 GPU 的数量。
训练
将模型放在一起,并结合我们新的 CUDA GPU 功能,我们提出了以下架构:
def build_network(num_gpu=1, input_shape=None):
inputs = Input(shape=input_shape, name="input")
# convolutional block 1
conv1 = Conv2D(64, kernel_size=(3,3), activation="relu",
name="conv_1")(inputs)
batch1 = BatchNormalization(name="batch_norm_1")(conv1)
pool1 = MaxPooling2D(pool_size=(2, 2), name="pool_1")(batch1)
# convolutional block 2
conv2 = Conv2D(32, kernel_size=(3,3), activation="relu",
name="conv_2")(pool1)
batch2 = BatchNormalization(name="batch_norm_2")(conv2)
pool2 = MaxPooling2D(pool_size=(2, 2), name="pool_2")(batch2)
# fully connected layers
flatten = Flatten()(pool2)
fc1 = Dense(512, activation="relu", name="fc1")(flatten)
d1 = Dropout(rate=0.2, name="dropout1")(fc1)
fc2 = Dense(256, activation="relu", name="fc2")(d1)
d2 = Dropout(rate=0.2, name="dropout2")(fc2)
# output layer
output = Dense(10, activation="softmax", name="softmax")(d2)
# finalize and compile
model = Model(inputs=inputs, outputs=output)
if num_gpu > 1:
model = multi_gpu_model(model, num_gpu)
model.compile(optimizer='adam', loss='categorical_crossentropy',
metrics=["accuracy"])
return model
我们可以使用它来构建我们的模型:
model = build_network(num_gpu=1, input_shape=(IMG_HEIGHT, IMG_WIDTH, CHANNELS))
然后,我们可以满足您的期望:
model.fit(x=data["train_X"], y=data["train_y"],
batch_size=32,
epochs=200,
validation_data=(data["val_X"], data["val_y"]),
verbose=1,
callbacks=callbacks)
在我们训练该模型时,您会注意到过拟合是一个紧迫的问题。 即使只有相对较小的两个卷积层,我们也已经有点过拟合了。
您可以从以下图形中看到过拟合的影响:

不足为奇,50,000 次观察不是很多数据,尤其是对于计算机视觉问题。 在实践中,计算机视觉问题得益于非常大的数据集。 实际上,Chen Sun 指出,附加数据倾向于以数据量的对数线性帮助计算机视觉模型。 不幸的是,在这种情况下,我们无法真正找到更多数据。 但是也许我们可以做些。 接下来让我们讨论数据增强。
使用数据增强
数据增强是一种将变换应用于图像并使用原始图像和变换后的图像进行训练的技术。 想象一下,我们有一个训练类,里面有一只猫:

如果将水平翻转应用于此图像,我们将得到如下所示的内容:

当然,这是完全相同的图像,但是我们可以将原始图像和转换图像用作训练示例。 这不像我们训练中的两只猫那么好。 但是,它的确使我们可以告诉计算机,无论猫面对什么方向,猫都是猫。
在实践中,我们可以做的不仅仅是水平翻转。 当有意义时,我们也可以垂直翻转,移动和随机旋转图像。 这使我们能够人为地放大我们的数据集,并使它看起来比实际的更大。 当然,您只能将其推到目前为止,但这是在存在少量数据的情况下防止过拟合的一个非常强大的工具。
Keras ImageDataGenerator
不久前,进行图像增强的唯一方法是对转换进行编码,并将其随机应用于训练集,然后将转换后的图像保存在磁盘上(上下坡,在雪中)。 对我们来说幸运的是,Keras 现在提供了ImageDataGenerator类,可以在我们训练时即时应用转换,而无需手工编码转换。
我们可以通过实例化ImageDataGenerator来创建一个数据生成器对象,如下所示:
def create_datagen(train_X):
data_generator = ImageDataGenerator(
rotation_range=20,
width_shift_range=0.02,
height_shift_range=0.02,
horizontal_flip=True)
data_generator.fit(train_X)
return data_generator
在此示例中,我同时使用了移位,旋转和水平翻转。 我只使用很小的移位。 通过实验,我发现更大的变化太多了,而且我的网络实际上无法学到任何东西。 您的经验会随着您的问题而变化,但是我希望较大的图像更能容忍移动。 在这种情况下,我们使用 32 个像素的图像,这些图像非常小。
用生成器训练
如果您以前没有使用过生成器,则它就像迭代器一样工作。 每次调用ImageDataGenerator .flow()方法时,它都会产生一个新的训练小批量,并将随机变换应用于所馈送的图像。
Keras Model类带有.fit_generator()方法,该方法使我们可以使用生成器而不是给定的数据集:
model.fit_generator(data_generator.flow(data["train_X"], data["train_y"], batch_size=32),
steps_per_epoch=len(data["train_X"]) // 32,
epochs=200,
validation_data=(data["val_X"], data["val_y"]),
verbose=1,
callbacks=callbacks)
在这里,我们用生成器替换了传统的x和y参数。 最重要的是,请注意steps_per_epoch参数。 您可以从训练集中任意采样替换次数,并且每次都可以应用随机变换。 这意味着我们每个周期可以使用的迷你批数比数据还多。 在这里,我将仅根据观察得到的样本数量进行采样,但这不是必需的。 如果可以,我们可以并且应该将这个数字提高。
在总结之前,让我们看一下这种情况下图像增强的好处:

如您所见,仅一点点图像增强确实帮助了我们。 不仅我们的整体精度更高,而且我们的网络过拟合的速度也慢得多。 如果您的计算机视觉问题只包含少量数据,那么图像增强就是您想要做的事情。
总结
在本章中,我们快速介绍了许多基础知识。 我们讨论了卷积层及其如何用于神经网络。 我们还介绍了批量规范化,池化层和数据增强。 最后,我们使用 Keras 从零开始训练卷积神经网络,然后使用数据增强对该网络进行改进。
我们还讨论了如何基于数据的渴望计算机视觉的深度神经网络问题。 在下一章中,我将向您展示迁移学习,这是我最喜欢的技术之一。 这将帮助您快速解决计算机视觉问题,并获得惊人的结果并且数据量更少。
八、将预训练的 CNN 用于迁移学习
迁移学习很棒。 实际上,在一本充满奇妙事物的书中,这可能是我必须告诉您的最奇妙的事物。 如果没有,那也许至少是我可以教给您的最有用和最实用的深度学习技术。 迁移学习可以帮助您解决深度学习问题,尤其是计算机视觉问题,而涉及问题范围的数据和数据却很少。 在本章中,我们将讨论什么是迁移学习,什么时候应该使用它,最后讨论如何在 Keras 中进行迁移学习。
我们将在本章介绍以下主题:
- 迁移学习概述
- 何时应使用迁移
- 源/目标数量和相似性的影响
- Keras 的迁移学习
迁移学习概述
在第 7 章和“卷积神经网络”中,我们训练了约 50,000 个观测值的卷积神经网络,并且由于网络和问题的复杂性,在开始训练的短短几个周期后,我们过拟合了。 如果您还记得的话,我曾评论说我们的训练集中有 50,000 个观察结果对于计算机视觉问题不是很大。 确实如此。 计算机视觉问题喜欢数据,而我们可以提供给他们的数据越多,它们的表现就越好。
我们可能认为计算机视觉技术最先进的深度神经网络通常在称为 ImageNet 的数据集上进行训练。 ImageNet数据集是包含 120 万张图像的 1,000 个分类器。 这还差不多! 如此庞大的数据集使研究人员能够构建真正复杂的深度神经网络,以检测复杂的特征。 当然,在 120 万张图像上训练有时具有 100 多个层的模型的价格很高。 训练可能需要数周和数月,而不是数小时。
但是,如果我们可以从一个最先进的,多层的,经过数百万张图像训练的网络开始,然后仅使用少量数据将该网络应用于我们自己的计算机视觉问题,该怎么办? 那就是迁移学习!
要使用迁移学习,我们将执行以下步骤:
- 从训练非常大的复杂计算机视觉问题的模型开始; 我们将其称为我们的源域
- 删除网络的最后一层(
softmax层),并可能删除其他完全连接的层 - 将最后几层替换为适合我们新问题的层,我们将其称为目标域
- 冻结所有已训练的层,使其权重不变
- 在目标域数据上训练网络
如果我们在这里停止,这通常被称为特征提取,因为我们正在使用在源域上训练的网络来提取目标域的视觉特征。 然后,我们使用栓接到该特征提取网络上的相对较小的神经网络来执行目标域任务。 根据我们的目标和数据集,这可能就足够了。
可选地,我们将通过解冻一些或所有冻结的层来微调整个网络,然后通常以很小的学习率再次进行训练。 我们将在短期内讨论何时使用微调,但是请确保我们涵盖了首先使用迁移学习的一些原因。
何时应使用迁移
当您的数据有限且存在解决类似问题的网络时,迁移学习会非常有效。 您可以使用迁移学习将最先进的网络和大量数据带入一个其他小的问题。 那么,什么时候应该使用迁移学习? 随时可以! 但是,我希望您首先考虑两个规定。 我们将在以下各节中讨论它们。
数据有限
关于计算机视觉和迁移学习,我最常被问到的问题是:我必须拥有多少张图像? 这是一个很难回答的问题,因为,正如我们将在下一节中看到的那样,更多通常更好。 一个更好的问题可能是:我可以使用几张图像来充分解决我的业务问题?
那么,我们的数据集有多有限? 尽管远非科学,但我已经建立了使用多达 2,000 张图像进行二分类任务的有用模型。 更简单的任务和更多样化的图像集通常可以在较小的数据集下获得更令人满意的结果。
根据经验,您至少需要几千张某类的图像,而通常最好使用 10 至 2 万张图像。
常见问题域
如果您的目标域至少与源域有些相似,那么迁移学习会很有效。 例如,假设您正在将图像分类为包含猫或狗。 有许多ImageNet训练有素的图像分类器非常适合用于此类型或问题。
相反,让我们想象我们的问题是将 CT 扫描或 MRI 归类为是否包含肿瘤。 此目标域与ImageNet源域非常不同。 这样,虽然使用迁移学习可能(并且可能会)有好处,但我们将需要更多的数据,并且可能需要进行一些微调才能使网络适应此目标域。
源/目标数量和相似性的影响
直到最近,很少有人研究数据量和源/目标域相似性对迁移学习表现的影响。 但是,这是一个对迁移学习的可用性很重要的主题,也是我撰写的主题。 在我的同事撰写的《调查数据量和域相似性对迁移学习应用的影响》中,对这些主题进行了一些实验。 这就是我们发现的东西。
更多数据总是有益的
Google 研究人员在《重新研究深度学习周期数据的不合理有效性》中进行的几次实验中,构建了一个内部数据集,其中包含 3 亿个观测值,显然比ImageNet大得多。 然后,他们在该数据集上训练了几种最先进的架构,从而使模型显示的数据量从 1000 万增加到 3000 万,1 亿,最后是 3 亿。 通过这样做,他们表明模型表现随用于训练的观察次数的对数线性增加,这表明在源域中,更多的数据总是有帮助。
但是目标域呢? 我们使用了一些类似于我们在迁移学习过程中可能使用的类型的数据集重复了 Google 实验,包括我们将在本章稍后使用的Dogs versus Cats数据集。 我们发现,在目标域中,模型的表现随用于训练的观察次数的对数线性增加,就像在源域中一样。 更多数据总是有帮助的。
源/目标域相似度
迁移学习的独特之处在于您担心源域和目标域之间的相似度。 经过训练以识别人脸的分类器可能不会轻易迁移到识别各种架构的目标领域。 我们进行了源和目标尽可能不同的实验,以及源和目标域非常相似的实验。 毫不奇怪,当迁移学习应用中的源域和目标域非常不同时,与相似时相比,它们需要更多的数据。 它们也需要更多的微调,因为当域在视觉上非常不同时,特征提取层需要大量的学习。
Keras 的迁移学习
与本书中的其他示例不同,在这里我们将需要涵盖目标域问题,源域问题以及我们正在使用的网络架构。 我们将从目标域的概述开始,这是我们要解决的问题。 然后,我们将介绍网络最初经过训练的源域,并简要介绍我们将使用的网络架构。 然后,我们将在本章的其余部分中将问题联系在一起。 我们需要分别考虑两个域,因为它们的大小和相似性与网络表现密切相关。 目标和源的类型越近,结果越好。
目标域概述
在本章的示例中,我将使用 Kaggle 的Dogs versus Cats数据集。 该数据集包含 25,000 张猫和狗的图像。 每个类别之间达到完美平衡,每个类别 12,500。 可以从这里下载数据集。
这是一个二分类问题。 每张照片都包含狗或猫,但不能同时包含两者。
该数据集由 Jeremy Elson 等人于 2007 年组装。 ,它目前托管在 www.kaggle.com 上。 它是完全免费下载和用于学术用途的,但是它确实需要一个 Kaggle 帐户并接受其最终用户许可。 一样,这是一个了不起的数据集,因此我在此处包括使用说明。
源域概述
我们将从在 ImageNet 上训练的深度神经网络开始。 如果您从“迁移学习概述”部分中回顾过,ImageNet是一个 1,000 类分类器,训练了大约 120 万张图像。 ImageNet数据集中都包含狗和猫的图像,因此在这种情况下,我们的目标域实际上与我们的源域非常相似。
源网络架构
我们将使用 Inception-V3 网络架构。 与您到目前为止在本书中所看到的相比,Inception 架构很有趣并且非常复杂。 如果您从第 7 章,“卷积神经网络”中回想起,我们必须围绕网络架构做出的决定之一就是选择过滤器大小。 对于每一层,我们必须决定是否应使用例如3 x 3过滤器,而不是5 x 5过滤器。 当然,也许根本就不需要另一次卷积。 也许像池化之类的东西可能更合适。 因此,如果我们在每一层都做所有事情,该怎么办? 这就是开始的动机。
该架构基于一系列模块或称为初始模块的构建块。 在每个初始模块中,先前的激活都赋予1 x 1卷积,3 x 3卷积,5 x 5卷积和最大池化层。 然后将输出连接在一起。
Inception-V3 网络由几个相互堆叠的 Inception 模块组成。 最后两层都完全连接,输出层是 1,000 个神经元 softmax。
通过使用keras.applications.inception_v3中的InceptionV3类,我们可以加载 Inception-V3 网络及其权重。 Keras 的网络动物园中有几个流行的网络,它们都位于keras.applications内部。 只需多一点工作就可以加载在 TensorFlow 中创建的模型。 也可以转换在其他架构中训练的模型,但这不在快速参考的范围之内。
要加载 Inception,我们只需要实例化一个InceptionV3对象,它是 Keras 模型,如以下代码所示:
from keras.applications.inception_v3 import InceptionV3
base_model = InceptionV3(weights='imagenet', include_top=False)
您可能会注意到,我们在这里说了include_top=False,这表明我们不需要网络的顶层。 这免除了我们手动清除它们的工作。 第一次运行此代码时,它将下载 Inception-V3 网络架构并保存权重并将其缓存给我们。 现在,我们只需要添加我们自己的完全连接的层即可。
迁移网络架构
我们将用更适合我们的用例的全连接层替换最后两层。 由于我们的问题是二分类,因此我们将使用激活sigmoid的单个神经元替换输出层,如以下代码所示:
# add a global spatial average pooling layer
x = base_model.output
x = GlobalAveragePooling2D()(x)
# let's add a fully-connected layer
x = Dense(1024, activation='relu')(x)
# and a logistic layer
predictions = Dense(1, activation='sigmoid')(x)
# this is the model we will train
model = Model(inputs=base_model.input, outputs=predictions)
注意,我们在这里使用GlobalAveragePooling2D层。 该层将前一层的 4D 输出平坦化为 2D 层,通过求平均将其适合于我们的全连接层。 通过指定pooling='avg' or 'max'来加载基本模型时,也可以完成此操作。 这是您如何处理此问题的电话。
至此,我们已经准备好训练网络。 但是,在执行此操作之前,我们需要记住冻结基本模型中的层,以免新的完全连接的层疯狂地试图学习时它们的权重不变。 为此,我们可以使用以下代码遍历各层并将其设置为不可训练:
for layer in base_model.layers:
layer.trainable = False
数据准备
我们将首先从 Kaggle 下载数据,然后将train.zip解压缩到本书的Chapter08目录中。 现在,您将拥有一个名为train/的目录,其中包含 25,000 张图像。 每个名称都将类似于cat.number.jpg。
我们想移动这些数据,以便我们为训练,验证和测试创建单独的目录。 这些目录中的每一个都应具有猫和狗的目录。 这都是非常无聊且平凡的工作,因此,我创建了data_setup.py来为您执行此操作。 一旦运行它,数据将在本章的其余部分中全部格式化。
完成后,您将拥有一个具有以下结构的数据目录:

数据输入
快速浏览图像应使您确信我们的图像的分辨率和大小均不同。 正如您从第 7 章,“卷积神经网络”,所了解的那样,我们需要这些图像的大小与神经网络的输入张量一致。 这是一个非常现实的问题,您将经常面对计算机视觉任务。 虽然当然可以使用 ImageMagick 之类的程序来批量调整图像大小,但 Keras ImageDataGenerator类可用于快速调整图像大小,这就是我们要做的。
Inception-V3 期望299 x 299 x 3图像。 我们可以在数据生成器中指定此目标大小,如以下代码所示:
train_datagen = ImageDataGenerator(rescale=1./255)
val_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
train_data_dir,
target_size=(img_width, img_height),
batch_size=batch_size,
class_mode='binary')
validation_generator = val_datagen.flow_from_directory(
val_data_dir,
target_size=(img_width, img_height),
batch_size=batch_size,
class_mode='binary')
如果需要,我们当然可以在这里使用数据增强,但是我们实际上并不需要它。
我们在这里最有趣的事情可能是使用数据生成器的flow_from_directory()方法。 此方法采用一条路径,并根据该路径生成一批图像。 它为我们完成了将映像从磁盘中取出的所有工作。 由于它一次执行一批,因此即使不需要时,我们甚至不必将所有 50,000 个图像保留在 RAM 中。 很酷吧?
训练(特征提取)
对于此模型,我们将训练两次。 对于第一轮训练,我们将通过冻结网络的训练来进行 10 个周期的特征提取,仅调整完全连接的层权重,如我们在“迁移网络架构”部分中所讨论的。 然后,在下一部分中,我们将解冻某些层并再次进行训练,对另外 10 个周期进行微调,如以下代码所示:
data_dir = "data/train/"
val_dir = "data/val/"
epochs = 10
batch_size = 30
model = build_model_feature_extraction()
train_generator, val_generator = setup_data(data_dir, val_dir)
callbacks_fe = create_callbacks(name='feature_extraction')
# stage 1 fit
model.fit_generator(
train_generator,
steps_per_epoch=train_generator.n // batch_size,
epochs=epochs,
validation_data=val_generator,
validation_steps=val_generator.n // batch_size,
callbacks=callbacks_fe,
verbose=1)
scores = model.evaluate_generator(val_generator, steps=val_generator.n // batch_size)
print("Step 1 Scores: Loss: " + str(scores[0]) + " Accuracy: " + str(scores[1]))
在前面的示例中,我们使用ImageDataGenerator的n属性来了解可用于生成器的图像总数,并将每个周期的步骤定义为该数目除以批量大小。
此代码的其余部分应该很熟悉。
如前所述,我们只需要训练大约 10 个周期。 现在,让我们看一下 TensorBoard 中的训练过程:

如您所见,即使经过一个周期,网络的表现仍然非常好。 直到大约第 7 个阶段,我们都取得了非常微弱的表现提升。在第 7 个阶段,我们达到了最佳表现,导致 0.9828 的精度和 0.0547 的损失。
训练(微调)
为了微调网络,我们需要解冻一些冻结的层。 您可以解冻多少层,并且可以解冻任意数量的网络。 实际上,在大多数情况下,我们仅看到解冻最顶层的好处。 在这里,我仅解冻最后一个初始块,该块从图的249层开始。 以下代码描述了此技术:
def build_model_fine_tuning(model, learning_rate=0.0001, momentum=0.9):
for layer in model.layers[:249]:
layer.trainable = False
for layer in model.layers[249:]:
layer.trainable = True
model.compile(optimizer=SGD(lr=learning_rate,
momentum=momentum), loss='binary_crossentropy', metrics=
['accuracy'])
return model
另请注意,我对随机梯度下降使用的学习率非常低,因此需要进行微调。 重要的是,此时应缓慢移动重物,以免在错误的方向上发生太大的飞跃。 我不建议使用adam或rmsprop进行微调。 以下代码描述了微调机制:
callbacks_ft = create_callbacks(name='fine_tuning')
# stage 2 fit
model = build_model_fine_tuning(model)
model.fit_generator(
train_generator,
steps_per_epoch=train_generator.n // batch_size,
epochs=epochs,
validation_data=val_generator,
validation_steps=val_generator.n // batch_size,
callbacks=callbacks_ft,
verbose=2)
scores = model.evaluate_generator(val_generator, steps=val_generator.n // batch_size)
print("Step 2 Scores: Loss: " + str(scores[0]) + " Accuracy: " + str(scores[1]))
我们可以再次查看 TensorBoard 图,以了解我们在进行微调后是否能得到任何收益:

毫无疑问,我们的模型确实可以改进,但是只有很少的改进。 虽然规模很小,但您会注意到验证损失正在努力改善,并且可能显示出一些过拟合的迹象。
在这种情况下,微调几乎没有带来任何好处,但并非总是如此。 在此示例中,目标域和源域非常相似。 如前所述,由于源域和目标域不同,您从微调中获得的收益将增加。
总结
在本章中,我们介绍了迁移学习,并演示了如何使用在源域上进行预训练的网络如何极大地缩短训练时间,并最终改善我们的深度神经网络的表现。 我希望您喜欢这项技术,它是我的最爱之一,因为它非常实用,而且我通常会从中获得很好的效果。
在下一章中,我们将从计算机视觉过渡到可以记住先前输入的网络,使它们成为预测序列中下一项的理想选择。
九、从头开始训练 RNN
循环神经网络(RNN)是为建模顺序数据而构建的一组神经网络。 在最后几章中,我们研究了使用卷积层从图像中学习特征。 当我们想从所有相关的值中学习特征时,循环层同样有用: xₜ, x[t-1],x[t-2],x[t-3]。
在本章中,我们将讨论如何将 RNN 用于时间序列问题,这无疑是涉及按时间或时间顺序排列的一系列数据点的问题。
我们将在本章介绍以下主题:
- 循环神经网络介绍
- 时间序列问题
- 将 LSTM 用于时间序列预测
循环神经网络介绍
如果定义不清楚,我们来看一个例子:一个股票行情指示器,我们可以在其中观察股票价格随时间的变化,例如以下屏幕快照中的 Alphabet Inc.,这是时间序列的一个示例:

在下一章中,我们将讨论使用循环神经网络为语言建模,这是另一种类型的序列,即单词序列。 由于您正在阅读本书,因此无疑已经对语言顺序有了一些直觉。
如果您不熟悉时间序列,您可能想知道是否可以使用普通的多层感知器来解决时间序列问题。 您当然可以做到; 但是,实际上,使用循环网络几乎总是可以得到更好的结果。 也就是说,循环神经网络在序列建模方面还有其他两个优点:
- 他们可以比普通的 MLP 更轻松地学习很长的序列
- 他们可以处理不同长度的序列
当然,这给我们提出了一个重要的问题...
是什么使神经元循环?
循环神经网络具有循环,可以使信息从一个预测持续到下一个预测。 这意味着每个神经元的输出取决于网络的当前输入和先前的输出,如下图所示:

如果我们将这个图跨时间展平,它将看起来更像下图。 网络通知本身的想法是“循环”一词的来源,尽管作为 CS 专业,我始终将其视为循环神经网络。

在上图中,我们可以看到神经元A接受输入x[t0]并输出h[t0]在时间步 0 处。然后在时间步 1,神经元使用输入x[t1]以及来自其上一个时间步的信号来输出h[t1]。 现在在时间步骤 2,它认为它是输入x[t2]以及上一个时间步骤的信号,该信号可能仍包含时间步骤 0 的信息。我们继续这种方式,直到到达序列中的最后一个时间步,网络逐步增加其内存。
标准 RNN 使用权重矩阵将前一个时间步的信号与当前时间步的输入和隐藏权重矩阵的乘积混合。 在通过非线性函数(通常是双曲正切函数)进行馈送之前,将所有这些函数组合在一起。 对于每个时间步骤,它看起来像:



此处t是前一个时间步输出和当前时间步输入的线性组合,均由权重矩阵W和U进行参数化。 一旦计算出t,它就具有非线性函数,最常见的是双曲正切hₜ。 最后,神经元的输出oₜ将hₜ与权重矩阵结合在一起,V和a偏置,c偏置。
当您查看此结构时,请尝试想象一下一种情况,在该情况下,您很早就需要一些非常重要的信息。 随着序列的延长,重要的早期信息被遗忘的可能性就更高,因为新信号会轻易地压倒旧信息。 从数学上讲,单元的梯度将消失或爆炸。
这是标准 RNN 的主要缺点。 在实践中,传统的 RNN 难以按顺序学习真正的长期交互作用。 他们很健忘!
接下来,让我们看一下可以克服此限制的长短期内存网络。
长期短期记忆网络
每当需要循环网络时,长期短期记忆网络(LSTM)都能很好地工作。 您可能已经猜到了,LSTM 在学习长期交互方面很出色。 实际上,这就是他们的设计意图。
LSTM 既可以积累先前时间步骤中的信息,又可以选择何时忘记一些不相关的信息,而选择一些新的更相关的信息。
例如,考虑序列In highschool I took Spanish. When I went to France I spoke French.。 如果我们正在训练一个网络来预测France一词,那么记住French并有选择地忘记Spanish是非常重要的,因为上下文已经发生了变化。 当序列的上下文发生变化时,LSTM 可以有选择地忘记某些事情。
为了完成这种选择性的长期记忆,LSTM 实现了一个“忘记门”,该门使 LSTM 成为了称为门控神经网络的神经网络家族的成员。 该遗忘门允许 LSTM 有选择地学习何时应从其长期存储器中丢弃信息。
LSTM 的另一个关键特性是内部自循环,使设备可以长期积累信息。 除了我们在 RNN 中看到的循环之外,还使用了该循环,可以将其视为时间步之间的外部循环。
相对于我们已经看到的其他神经元,LSTM 非常复杂,如下图所示:

每个 LSTM 单元展开时,都有一个时间段t的输入,称为xₜ,一个输出,称为oₜ以及从上一个时间步C[t-1]到下一个Cₜ进行存储的存储器总线C。
除这些输入外,该单元还包含多个门。 我们已经提到的第一个是忘记门,在图中标记为Fₜ:

该门的输出(将在 0 和 1 之间)逐点乘以C[t-1]。 这允许门调节从C[t-1]到Cₜ的信息流。
下一个门,即输入门iₜ与函数候选Cₜ结合使用。 候选Cₜ学习可以添加到内存状态的向量。 输入门了解总线C中的哪些值得到更新。 下式说明iₜ和候选Cₜ:


我们取iₜ和候选Cₜ的点积,决定添加到总线C的对象, 使用Fₜ决定要忘记什么之后,如以下公式所示:

最后,我们将决定获取输出的内容。 输出主要来自内存总线C; 但是,它被另一个称为输出门的门过滤。 以下公式说明了输出:


尽管很复杂,但 LSTM 在各种问题上都非常有效。 尽管存在 LSTM 的多个变体,但在大多数任务上仍基本认为该基本实现是最新技术。
这些任务之一是预测时间序列中的下一个值,这就是我们将在本章中使用的 LSTM。 但是,在我们开始将 LSTM 应用于时间序列之前,必须对时间序列分析和更传统的方法进行简短的复习。
时间上的反向传播
训练 RNN 要求反向传播的实现略有不同,即在整个时间(BPTT)中称为反向传播。
与正常反向传播一样,BPTT 的目标是使用整体网络误差,通过梯度来调整每个神经元/单元对它们对整体误差的贡献的权重。 总体目标是相同的。
但是,当使用 BPTT 时,我们对误差的定义会稍有变化。 正如我们刚刚看到的,可以通过几个时间步长展开神经元循环。 我们关心所有这些时间步长的预测质量,而不仅仅是终端时间步长,因为 RNN 的目标是正确预测序列,因为逻辑单元误差定义为所有时间步长上展开的误差之和。
使用 BPTT 时,我们需要总结所有时间步骤中的误差。 然后,在计算完该总体误差后,我们将通过每个时间步的梯度来调整单元的权重。
这迫使我们明确定义将展开 LSTM 的程度。 在下面的示例中,您将看到这一点,当我们创建一组特定的时间步长时,将为每个观察值进行训练。
您选择反向传播的步骤数当然是超参数。 如果您需要从序列中很远的地方学习一些东西,显然您必须在序列中包括很多滞后。 您需要能够捕获相关期间。 另一方面,捕获太多的时间步长也不可取。 该网络将变得非常难以训练,因为随着时间的流逝,梯度会变得非常小。 这是前面几章中描述的梯度消失问题的另一个实例。
如您想象的那样,您可能想知道是否选择太大的时间步会使程序崩溃。 如果梯度驱动得太小以至于变为 NaN,那么我们将无法完成更新操作。 解决此问题的一种常见且简便的方法是在某些上下阈值之间固定梯度,我们将其称为梯度裁剪。 默认情况下,所有 Keras 优化器均已启用梯度剪切。 如果您的梯度被剪裁,则在该时间范围内网络可能不会学到很多东西,但是至少您的程序不会崩溃。
如果 BPTT 看起来确实令人困惑,请想象一下 LSTM 处于展开状态,其中每个时间步都有一个单元。 对于该网络结构,该算法实际上与标准反向传播几乎相同,不同之处在于所有展开的层均共享权重。
时间序列问题回顾
时间序列问题是涉及按时间顺序放置的一系列数据点的问题。 我们通常将这些数据点表示为一组:

通常,我们在时间序列分析中的目标是预测。 但是,使用时间序列当然还可以执行许多其他有趣的事情,而这不在本书的讨论范围之内。 预测实际上只是回归的一种特殊形式,我们的目标是根据给定的先前点x[t-1], ..., x[t-n]来预测某个点xₜ或点x[t], x[t+1], x[t+2], ..., x[t+n]。 当时间序列自动关联时,我们可以执行此操作,这意味着数据点与其自身关联一个或多个时间上的点(称为滞后)。 自相关性越强,预测就越容易。
在许多书中,时间序列问题用y表示,而不是用x表示,以暗示我们通常关心预测给定自身的变量 y 的想法。
库存和流量
在计量经济学时间序列中,数量通常被定义为库存或流量。 库存度量是指特定时间点的数量。 例如,2008 年 12 月 31 日的 SP500 的值是库存测量值。 流量测量是一段时间间隔内的速率。 美国股票市场从 2009 年到 2010 年的增长率是一种流量度量。
最经常进行预测时,我们会关注预测流量。 如果我们将预测想象为一种特定的回归,那么我们偏爱流量的第一个也是最明显的原因是,流量估计更有可能是插值而不是外推,而且插值几乎总是更安全。 此外,大多数时间序列模型都具有平稳性的假设。 固定时间序列是其统计属性(均值,方差和自相关)随时间恒定的序列。 如果我们使用一定数量的库存测量,则会发现大多数现实世界中的问题远非静止不动。
使用 LSTM 进行时间序列分析时,虽然没有假设(读取规则)需要平稳性,但根据实际经验,我发现对相对固定的数据进行训练的 LSTM 更加健壮。 使用 LSTM 进行时间序列预测时,几乎在所有情况下,一阶差分就足够了。
将库存数量转换为流量数量非常简单。 如果您具有n个点,则可以创建具有一阶差分的n-1流量测量值,其中,对于每个值t'[n],我们通过从tₙ中减去t[n-1]来进行计算,从而得出跨时间间隔的两次测量的变化率,如以下公式所示:

例如,如果我们在三月份拥有价值 80 美元的股票,而在四月份突然价值 100 美元,则该股票的流率将为 20 美元。
一阶微分不能保证平稳的时间序列。 我们可能还需要删除季节或趋势。 趋势消除是专业预测员日常生活的重要组成部分。 如果我们使用传统的统计模型进行预测,则需要做更多的工作。 虽然我们没有涵盖这些内容的页面,但我们可能还需要执行二阶差分,季节性趋势下降或更多操作。 增强 Dickey-Fuller(ADF)测试是一种统计测试,通常用于确定我们的时间序列是否实际上是静止的。 如果您想知道时间序列是否稳定,可以使用增强的 Dickey-Fuller 检验来检查。 但是,对于 LSTM,一阶微分通常可能就足够了。 只需了解网络最肯定会学习您数据集中剩余的季节和周期。
ARIMA 和 ARIMAX 预测
值得一提的是自回归综合移动平均值(ARIMA)模型,因为它们传统上用于时间序列预测。 虽然我显然是深度神经网络的忠实拥护者(事实上,我写过关于它们的书),但我建议从 ARIMA 开始并逐步进行深度学习。 在许多情况下,ARIMA 的表现将优于 LSTM。 当数据稀疏时尤其如此。
从可能可行的最简单模型开始。 有时这将是一个深层的神经网络,但通常情况会更简单一些,例如线性回归或 ARIMA 模型。 该模型的复杂性应通过其提供的提升来证明,通常越简单越好。 尽管整本书中多次重申,但在时间序列预测中,这一说法比其他任何话题都更为真实。
ARIMA 模型是三个部分的组合。 AR,即自回归部分,是根据自身的自相关性对序列进行建模的部分。 MA 部分尝试对时间序列中的本地突发事件或冲击建模。 I 部分涵盖了差异,我们刚刚介绍了差异。 ARIMA 模型通常采用三个超参数p,d和q,分别对应于建模的自回归滞后的数量,微分度和模型的移动平均部分的顺序。
ARIMA 模型在 R 的auto.arima()和预测包中实现得很好,这可能是使用 R 语言的唯一很好的理由之一。
ARIMAX 模型允许在时间序列模型中包含一个或多个协变量。 您问这种情况下的协变量是多少? 这是一个附加时间序列,也与因变量相关,可用于进一步改善预测表现。
交易员的常见做法是尝试通过使用另一种商品的一个或多个滞后以及我们预测的商品的自回归部分来预测某些商品的价值。 在这种情况下,ARIMAX 模型将很有用。
如果您有许多具有复杂的高阶交互作用的协变量,那么您已进入 LSTM 的最佳预测时间序列。 在本书的开头,我们讨论了多层感知器如何对输入变量之间的复杂相互作用进行建模,从而为我们提供了自动特征工程,该工程提供了线性或逻辑回归的提升。 此属性可以继续使用 LSTM 进行具有许多输入变量的时间序列预测。
如果您想全面了解 ARIMA,ARIMAX 和时间序列预测,建议从 Rob J. Hyndman 的博客 Hyndsight 开始。
将 LSTM 用于时间序列预测
在本章中,我们将通过使用 2017 年 1 月至 5 月的比特币分钟价格来预测 2017 年 6 月美元的比特币分钟价格。我知道这听起来确实很赚钱,但是在您购买那条船之前,我建议您通读本章的最后; 说起来容易做起来难,甚至建模起来也容易。
即使我们能够使用这种模型在美元和比特币之间创造套利潜力(由于效率低下而导致两个市场之间的价格差异),但由于存在延迟,围绕比特币制定交易策略可能极其复杂。 在完成比特币交易中。 在撰写本文时,比特币交易的平均交易时间超过一个小时! 任何交易策略都应考虑这种“非流动性”。
和以前一样,本书的 Git 存储库中的Chapter09下提供了本章的代码。 文件data/bitcoin.csv包含数年的比特币价格。 基于以下假设,即前几年的市场行为与 2017 年加密货币流行后的行为无关,我们将仅使用几个月的价格信息作为模型。
数据准备
对于此示例,我们将不使用验证集,而是将测试集用作验证集。 在处理此类预测问题时,验证成为一项具有挑战性的工作,因为训练数据从测试数据中获取的越多,执行效果越差的可能性就越大。 另一方面,这并不能为过度安装提供太多保护。
为了使事情简单,在这里我们将只使用一个测试集,并希望最好。
在继续之前,让我们看一下将要进行的数据准备的总体流程。 为了使用此数据集训练 LSTM,我们需要:
- 加载数据集并将周期时间转换为熊猫日期时间。
- 通过对日期范围进行切片来创建训练和测试集。
- 差分我们的数据集。
- 将差异缩放到更接近我们的激活函数的程度。 我们将使用 -1 到 1,因为我们将使用
tanh作为激活 - 创建一个训练集,其中每个目标
xₜ都有一系列与之相关的滞后x[t-1], ..., x[t-n]。 在此训练集中,您可以将xₜ视为我们的典型因变量y。 滞后序列x[t-1], ..., x[t-n]可以看作是典型的X训练矩阵。
我将在接下来的主题中介绍每个步骤,并在进行过程中显示相关的代码。
加载数据集
从磁盘加载数据集是一项相当简单的工作。 如前所述,我们将按日期对数据进行切片。 为此,我们需要将数据集中的 Unix 周期时间转换为可分割的日期。 可以通过pandas to_datetime()方法轻松实现,如以下代码所示:
def read_data():
df = pd.read_csv("./data/bitcoin.csv")
df["Time"] = pd.to_datetime(df.Timestamp, unit='s')
df.index = df.Time
df = df.drop(["Time", "Timestamp"], axis=1)
return df
按日期切片和测试
现在,我们的数据帧已通过datetime时间戳编制索引,因此我们可以构造基于日期的切片函数。 为此,我们将定义一个布尔掩码,并使用该掩码选择现有的数据框。 虽然我们可以肯定地将其构造成一行,但我认为以这种方式阅读起来要容易一些,如以下代码所示:
def select_dates(df, start, end):
mask = (df.index > start) & (df.index <= end)
return df[mask]
现在我们可以使用日期来获取数据框的某些部分,我们可以使用以下代码通过几次调用这些函数轻松地创建训练和测试数据框:
df = read_data()
df_train = select_dates(df, start="2017-01-01", end="2017-05-31")
df_test = select_dates(df, start="2017-06-01", end="2017-06-30")
在使用这些数据集之前,我们需要对它们进行区别,如下所示。
差分时间序列
Pandas 数据框最初是为对时间序列数据进行操作而创建的,幸运的是,由于对数据集进行差分是时间序列中的一种常见操作,因此很容易内置。但是,根据良好的编码习惯,我们将围绕我们的一阶差分运算包装一个函数。 请注意,我们将用 0 填充无法进行一阶差分的所有空间。以下代码说明了此技术:
def diff_data(df):
df_diffed = df.diff()
df_diffed.fillna(0, inplace=True)
return df_diffed
通过差分数据集,我们将这个问题(库存问题)转移到了流量问题。 在比特币投放中,流量可能会很大,因为比特币的价值会在数分钟之间发生很大变化。 我们将通过缩放数据集来解决此问题。
缩放时间序列
在此示例中,我们将使用MinMaxScaler将每个差异数据点缩放为最小值为 -1 且最大值为 1 的比例。这将使我们的数据与双曲线正切函数(tanh ),这是我们针对该问题的激活函数。 我们将使用以下代码缩放系列:
def scale_data(df, scaler=None):
scaled_df = pd.DataFrame(index=df.index)
if not scaler:
scaler = MinMaxScaler(feature_range=(-1,1))
scaled_df["Price"] = scaler.fit_transform(df.Close.values.reshape(-1,1))
return scaler, scaled_df
请注意,此函数可以选择使用已经适合的缩放器。 这使我们能够将训练定标器应用到我们的测试仪上。
创建滞后的训练集
对于每个训练示例,给定一系列延迟x[t-1], ..., x[t-n],我们希望训练网络以预测值xₜ。 理想的延迟数是一个超参数,因此需要进行一些实验。
如前所述,以这种方式构造输入是 BPTT 算法的要求。 我们将使用以下代码来训练数据集:
def lag_dataframe(data, lags=1):
df = pd.DataFrame(data)
columns = [df.shift(i) for i in range(lags, 0, -1)]
columns.append(df)
df = pd.concat(columns, axis=1)
df.fillna(0, inplace=True)
cols = df.columns.tolist()
for i, col in enumerate(cols):
if i == 0:
cols[i] = "x"
else:
cols[i] = "x-" + str(i)
cols[-1] = "y"
df.columns = cols
return df
例如,如果我们用lags = 3调用lag_dataframe,我们期望数据集返回x[t-1], x[t-2], x[t-3]。 我发现很难理解这样的滞后代码,因此,如果您也这样做,您并不孤单。 我建议运行它并建立一些熟悉的操作。
在选择数量滞后时,在将模型部署到生产环境时,您可能还需要考虑要等待多少个滞后才能做出预测。
输入形状
Keras 期望 LSTM 的输入是一个三维张量,看起来像:

第一个维度显然是我们拥有的观测值的数量,并且我们可以预期。
第二维对应于使用lag_dataframe函数时我们选择的滞后次数。 这是我们要给 Keras 做出预测的时间步数。
第三维是该时间步中存在的特征数。 在我们的示例中,我们将使用一个,因为每个时间步只有一个特征,即该时间步的比特币价格。
在继续阅读之前,请仔细考虑此处定义三维矩阵给您的威力。 我们绝对可以将数百个其他时间序列作为预测该时间序列的特征。 通过这样做以及使用 LSTM,我们可以免费获得这些特征之间的特征工程。 正是这种功能使 LSTM 在金融领域变得如此令人兴奋。
对于当前的问题,我们将需要将二维矩阵转换为三维矩阵。 为此,我们将使用 NumPy 的便捷reshape函数,如以下代码所示:
X_train = np.reshape(X_train.values, (X_train.shape[0], X_train.shape[1], 1))
X_test = np.reshape(X_test.values, (X_test.shape[0], X_test.shape[1], 1))
数据准备
在此示例中,我们做了很多转换。 在继续进行训练之前,我认为最好将两者结合起来。 如此处所示,我们将使用另一个函数将所有这些步骤联系在一起:
def prep_data(df_train, df_test, lags):
df_train = diff_data(df_train)
scaler, df_train = scale_data(df_train)
df_test = diff_data(df_test)
scaler, df_test = scale_data(df_test, scaler)
df_train = lag_dataframe(df_train, lags=lags)
df_test = lag_dataframe(df_test, lags=lags)
X_train = df_train.drop("y", axis=1)
y_train = df_train.y
X_test = df_test.drop("y", axis=1)
y_test = df_test.y
X_train = np.reshape(X_train.values, (X_train.shape[0], X_train.shape[1], 1))
X_test = np.reshape(X_test.values, (X_test.shape[0], X_test.shape[1], 1))
return X_train, X_test, y_train, y_test
此函数采用训练和测试数据帧,并应用差分,缩放和滞后代码。 然后,将这些数据帧重新调整为我们熟悉的X和y张量,以进行训练和测试。
现在,我们可以使用几行代码将这些转换粘合在一起,从而从加载数据到准备进行训练和测试,它们可以:
LAGS=10
df = read_data()
df_train = select_dates(df, start="2017-01-01", end="2017-05-31")
df_test = select_dates(df, start="2017-06-01", end="2017-06-30")
X_train, X_test, y_train, y_test = prep_data(df_train, df_test, lags=LAGS)
这样,我们就可以开始训练了。
网络输出
我们的网络将输出一个单一值,该值是在前一分钟内给定分钟内比特流价格的缩放流量或预期变化。
我们可以使用单个神经元获得此输出。 该神经元可以在 Keras 密集层中实现。 它将多个 LSTM 神经元的输出作为输入,我们将在下一部分中介绍。 最后,此神经元的激活可以是tanh,因为我们已将数据缩放到与双曲正切函数相同的比例,如下所示:
output = Dense(1, activation='tanh', name='output')(lstm2)
网络架构
我们的网络将使用两个 Keras LSTM 层,每个层具有 100 个 LSTM 单元:
inputs = Input(batch_shape=(batch_shape, sequence_length,
input_dim), name="input")
lstm1 = LSTM(100, activation='tanh', return_sequences=True,
stateful=True, name='lstm1')(inputs)
lstm2 = LSTM(100, activation='tanh', return_sequences=False,
stateful=True, name='lstm2')(lstm1)
output = Dense(1, activation='tanh', name='output')(lstm2)
要特别注意return_sequences参数。 连接两个 LSTM 层时,您需要前一个 LSTM 层来输出序列中每个时间步的预测,以便下一个 LSTM 层的输入是三维的。 但是,我们的密集层仅需要二维输出即可预测其执行预测的确切时间步长。
有状态与无状态 LSTM
在本章的前面,我们讨论了 RNN 跨时间步长维护状态或内存的能力。
使用 Keras 时,可以用两种方式配置 LSTM,即有状态和无状态。
默认为无状态配置。 使用无状态 LSTM 配置时,每批 LSTM 单元存储器都会重置。 这使得批量大小成为非常重要的考虑因素。 当您正在学习的序列彼此不依赖时,无状态效果最佳。 下一个单词的句子级预测可能是何时使用无状态的一个很好的例子。
有状态配置会在每个周期重置 LSTM 单元存储器。 当训练集中的每个序列取决于其之前的序列时,最常使用此配置。 如果句子级别的预测对于无状态配置可能是一项好任务,那么文档级别的预测对于有状态模型可能是一项好任务。
最终,这种选择取决于问题,并且可能需要在测试每个选项时进行一些试验。
对于此示例,我已经测试了每个选项,并选择使用有状态模型。 当我们考虑问题的背景时,这可能不足为奇。
训练
尽管此时的情况似乎有很大不同,但是训练 LSTM 实际上与训练典型横截面问题的深度神经网络没有什么不同:
LAGS=10
df = read_data()
df_train = select_dates(df, start="2017-01-01", end="2017-05-31")
df_test = select_dates(df, start="2017-06-01", end="2017-06-30")
X_train, X_test, y_train, y_test = prep_data(df_train, df_test, lags=LAGS)
model = build_network(sequence_length=LAGS)
callbacks = create_callbacks("lstm_100_100")
model.fit(x=X_train, y=y_train,
batch_size=100,
epochs=10,
callbacks=callbacks)
model.save("lstm_model.h5")
在准备好数据之后,我们使用我们已经遍历的架构实例化一个网络,然后按预期对其进行拟合。
在这里,我使用的是有状态的 LSTM。 有状态 LSTM 的一个实际好处是,与无状态 LSTM 相比,它们倾向于在更少的时间进行训练。 如果要将其重构为无状态 LSTM,则在网络完成学习之前可能需要 100 个周期,而此处我们仅使用 10 个周期。
测量表现
在有状态的配置中经过 10 个星期之后,我们的损失已经停止改善,并且我们的网络也受到了良好的训练,如下图所示:

我们拥有一个合适的网络,似乎已经学到了一些东西。 现在,我们可以对比特币的价格流做出某种预测。 如果我们能做好,我们所有人都会非常富有。 在去买那栋豪宅之前,我们可能应该测量一下模型的表现。
财务模型的最终检验是这个问题:“您愿意在上面花钱吗?”很难回答这个问题,因为在时间序列问题中衡量表现可能具有挑战性。
一种衡量表现的非常简单的方法是使用均方根误差来评估y_test与X_test预测之间的差异。 我们最肯定可以做到这一点,如以下代码所示:
RMSE = 0.0801932157201
0.08 是一个好分数吗? 让我们通过比较我们的预测与 6 月份比特币流量的实际值,开始对商品的调查。 这样做可能会使我们对模型的表现有直观的了解,这是我始终建议的一种做法:

我们用绿色表示的预测有很多不足之处。 我们的模型已经学会了预测平均流量,但是在匹配完整信号方面确实做得很差。 甚至有可能我们只是在学习一种趋势,因为我们所做的努力不那么激烈。 我认为我们可能不得不把那栋豪宅推迟更长的时间,但是我们走了正确的道路。
考虑到我们的预测,即仅给出比特币的先前价值,该模型就可以解释尽可能多的比特币价格。 我们可能在建模时间序列的自回归部分方面做得相当不错。 但是,可能有许多不同的外部因素影响比特币的价格。 美元的价值,其他市场的动向,也许最重要的是,围绕比特币的嗡嗡声或信息流通,都可能在美元的价格中发挥重要作用。
这就是 LSTM 用于时间序列预测的功能真正发挥作用的地方。 通过添加附加的输入特征,所有这些信息都可以在某种程度上轻松地添加到模型中,希望可以解释越来越多的整个图片。
但是,让我再破一次您的希望。 对表现进行更彻底的调查还将包括考虑模型相对于某些幼稚模型所提供的提升。 此简单模型的典型选择可能包括称为随机游走模型,指数平滑模型的模型,或者可能使用朴素的方法,例如使用上一个时间步长作为当前时间步长的预测。 如下图所示:

在此图中,我们将红色的预测与一个模型进行比较,在模型中,我们仅将前一分钟用作绿色的下一分钟的预测。 以蓝色表示的实际价格几乎完美地覆盖了这个朴素的模型。 我们的 LSTM 预测不如幼稚模型好。 仅使用最后一分钟的价格来预测当前分钟的价格会更好。 尽管我坚持认为我们走在正确的道路上,但在那艘船成为我们的船之前,我们还有很长的路要走。
对任何商品建模非常困难。 对于这种类型的问题,使用深度神经网络是可以肯定的,但是这个问题并不容易。 我加入了这个也许详尽的解释,以便如果您决定走这条路,便会明白自己的目标。
就是说,当您使用 LSTM 套利金融市场时,请记住给小费。
总结
在本章中,我们讨论了使用循环神经网络来预测序列中的下一个元素。 我们既涵盖了一般的 RNN,也涵盖了特定的 LSTM,我们专注于使用 LSTM 预测时间序列。 为了确保我们了解将 LSTM 用于时间序列的好处和挑战,我们简要回顾了时间序列分析的一些基础知识。 我们还花了几分钟讨论传统的时间序列模型,包括 ARIMA 和 ARIMAX。
最后,我们介绍了一个具有挑战性的用例,其中我们使用 LSTM 来预测比特币的价格。
在下一章中,我们将继续使用 RNN,现在将重点放在自然语言处理任务上,并介绍嵌入层的概念。
十、使用词嵌入从头开始训练 LSTM
到目前为止,我们已经看到了深度学习在结构化数据,图像数据甚至时间序列数据中的应用示例。 似乎唯一正确的方法是继续进行自然语言处理(NLP)作为下一步。 机器学习和人类语言之间的联系非常有趣。 深度学习已像计算机视觉一样,以指数方式加快了该领域的发展速度。 让我们从 NLP 的简要概述开始,并在本章中将要完成的一些任务开始。
我们还将在本章中介绍以下主题:
- 自然语言处理入门
- 向量化文本
- 词嵌入
- Keras 嵌入层
- 用于自然语言处理的一维 CNN
- 文档分类的案例研究
自然语言处理入门
NLP 领域广阔而复杂。 从技术上讲,人类语言与计算机科学之间的任何交互都可能属于此类。 不过,为了便于讨论,我将 NLP 限于分析,理解,有时生成人类语言。
从计算机科学的起源开始,我们就对 NLP 着迷,因为它是通向强大人工智能的门户。 1950 年,艾伦·图灵(Alan Turing)提出了图灵测试,其中涉及一台计算机,它很好地模仿了一个人,使其与另一个人无法区分,以此作为机器智能的度量标准。 从那时起,我们一直在寻找帮助机器理解人类语言的聪明方法。 在此过程中,我们开发了语音到文本的转录,人类语言之间的自动翻译,文档的自动汇总,主题建模,命名实体标识以及各种其他用例。
随着我们对 NLP 的了解不断增长,我们发现 AI 应用在日常生活中变得越来越普遍。 聊天机器人作为客户服务应用已变得司空见惯,最近,它们已成为我们的个人数字助理。 在撰写本文时,我可以要求 Alexa 在我的购物清单中添加一些内容或演奏一些流畅的爵士乐。 自然语言处理以一种非常有趣和强大的方式将人类连接到计算机。
在本章中,我将专注于理解人类语言,然后使用这种理解进行分类。 我实际上将进行两个分类案例研究,一个涉及语义分析,另一个涉及文档分类。 这两个案例研究为深度学习的应用提供了巨大的机会,而且它们确实非常相似。
语义分析
语义分析从技术上讲是对语言含义的分析,但是通常当我们说语义分析时,我们是在谈论理解作者的感受。 语义分类器通常试图将某些话语分类为积极,消极,快乐,悲伤,中立等。
讽刺是我最喜欢的语言之一,这使这成为一个具有挑战性的问题。 人类语言中有许多微妙的模式,这些对于计算机学习来说是非常具有挑战性的。 但是挑战并不意味着没有可能。 只要有一个好的数据集,这个任务就很有可能实现。
要成功解决此类问题,需要一个好的数据集。 虽然我们当然可以在整个互联网上找到大量的人类对话,但其中大多数没有标签。 查找带标签的病例更具挑战性。 解决此问题的早期尝试是收集包含表情符号的 Twitter 数据。 如果一条推文中包含:),则认为该推文是肯定的。 这成为 Jimmy Lin 和 Alek Kolcz 在 Twitter 上的大规模机器学习中引用的知名表情符号技巧。
这种类型的分类器的大多数业务应用都是二元的,我们尝试在其中预测客户是否满意。 但是,那当然不是对这种语言模型的限制。 只要我们有用于此类事物的标签,我们就可以为其他音调建模。 我们甚至可能尝试衡量某人的声音或语言中的焦虑或困扰; 但是,解决音频输入超出了本章的范围。
进一步挖掘数据的尝试包括使用与正面和负面电影评论相关的语言以及与在线购物产品评论相关的语言。 这些都是很好的方法。 但是,在使用这些类型的数据源对来自不同域的文本进行分类时,应格外小心。 您可能会想到,电影评论或在线购买中使用的语言可能与 IT 帮助台客户支持电话中使用的语言完全不同。
当然,我们当然可以对情绪进行更多的分类。 在下一节中,我们将讨论文档分类的更一般的应用。
文档分类
文档分类与情感分析密切相关。 在这两种情况下,我们都使用文本将文档分类。 实际上,这只是改变的原因。 文档分类就是根据文档的类型对文档进行分类。 世界上最明显,最常见的文档分类系统是垃圾邮件过滤器,但它还有许多其他用途。
我最喜欢的文档分类用途之一是解决“联邦主义者论文”的原始作者的辩论。 亚历山大·汉密尔顿(Alexander Hamilton),詹姆斯·麦迪逊(James Madison)和约翰·杰伊(John Jay)在 1787 年和 1788 年以化名 Publius 出版了 85 篇文章,支持批准美国宪法。 后来,汉密尔顿提供了一份清单,详细列出了每篇论文的作者在 1804 年与亚伦·伯尔(Aaron Burr)进行致命的对决之前。麦迪逊(Madison)在 1818 年提供了自己的清单,这在作者身份上引起了争执,此后学者一直在努力解决。 虽然大多数人都同意有争议的作品是麦迪逊的作品,但是关于两者之间的合作仍存在一些理论。 将这 12 个有争议的文档归类为 Madison 还是 Hamilton,已经成为许多数据科学博客的不二之选。 正式而言,Glenn Fung 的论文《有争议的联邦主义者论文:通过凹面最小化进行 SVM 特征选择》 涵盖了相当严格的主题。
文档分类的最后一个示例可能是围绕了解文档的内容并规定操作。 想象一下一个分类器,它可能会读取有关法律案件的一些信息,例如请愿/投诉和传票,然后向被告提出建议。 然后,我们的假想系统可能会说:鉴于我在其他类似情况下的经验,您可能想解决。
情感分析和文档分类是基于计算机理解自然语言的能力的强大技术。 但是,当然,这引出了一个问题,我们如何教计算机阅读?
向量化文本
机器学习模型(包括深度神经网络)吸收数字信息并产生数字输出。 自然语言处理的挑战自然就变成了将单词转换成数字。
我们可以通过多种方式将单词转换为数字。 所有这些方法都满足相同的目标,即将某些单词序列转换为数字向量。 有些方法比其他方法更好,因为有时进行转换时,翻译中可能会失去一些含义。
NLP 术语
让我们从定义一些通用术语开始,以便消除它们使用可能引起的任何歧义。 我知道,由于您可以阅读,因此您可能会对这些术语有所了解。 如果这看起来很古怪,我深表歉意,但是我保证,这将立即与我们接下来讨论的模型有关:
- 词:我们将使用的大多数系统的原子元素。 尽管确实存在某些字符级模型,但我们今天不再讨论它们。
- 句子:表达陈述,问题等的单词集合。
- 文档:文档是句子的集合。 它可能是一个句子,或更可能是多个句子。
- 语料库:文档的集合。
词袋模型
词袋(BoW)模型是 NLP 模型,实际上忽略了句子结构和单词放置。 在“单词袋”模型中,我们将每个文档视为单词袋。 很容易想到这一点。 每个文档都是一个包含大量单词的容器。 我们忽略句子,结构以及哪个词排在前或后。 我们对文档中包含“非常”,“很好”和“不好”这两个词的事实感到关注,但是我们并不真正在意“好”而不是“坏”。
词袋模型很简单,需要相对较少的数据,并且考虑到该模型的朴素性,其运行效果非常好。
注意,这里使用模型表示表示。 我并不是在特定意义上指深度学习模型或机器学习模型。 相反,在这种情况下,模型是表示文本的一种方式。
给定一个由一组单词组成的文档,则需要定义一种策略来将单词转换为数字。 稍后我们将介绍几种策略,但首先我们需要简要讨论词干,词形化和停用词。
词干,词根去除和停用词
词干和词根去除是两种不同但非常相似的技术,它们试图将每个单词还原为基本形式,从而简化了语言模型。 例如,如果要阻止猫的各种形式,我们将在此示例中进行转换:
cat, cats, cat's, cats' -> cat
限制词法化和词干化之间的差异成为我们进行此转换的方式。 提取是通过算法完成的。 当应用于同一个单词的多种形式时,提取的根在大多数情况下应相同。 这个概念可以与词条反义化形成对比,词条反义化使用具有已知基础的词汇表并考虑如何使用该词。
词干处理通常比词条化处理快得多。 Porter 提取器在很多情况下都可以很好地工作,因此您可以将其作为提取的第一个安全选择。
停用词是在该语言中非常常见的词,但几乎没有语义。 典范示例是the一词。 我在上一句话中只使用了 3 次,但实际上只保留了一次意思。 通常,我们会删除停用词,以使输入内容更加稀疏。
大部分 BoW 模型都受益于词干,词根化和删除停用词。 有时,我们很快将要讨论的词嵌入模型也可以从词干提取或词义化中受益。 词嵌入模型很少会受益于停用词的删除。
计数和 TF-IDF 向量化
计数向量化和词频逆文档频率(TF-IDF)是两种策略,将词袋转换成适合机器学习算法输入的特征向量。
计数向量化采用我们的一组单词,并创建一个向量,其中每个元素代表语料库词汇中的一个单词。 自然,一组文档中唯一单词的数量可能会很大,并且许多文档可能不包含语料库中存在的单词的任何实例。 在这种情况下,使用稀疏矩阵表示这些类型的字向量通常是非常明智的。 当一个单词出现一次或多次时,计数向量化器将简单地对该单词出现在文档中的次数进行计数,然后将该计数放置在代表该单词的位置。
使用计数向量化器,整个语料库可以表示为二维矩阵,其中每一行是一个文档,每一列是一个单词,然后每个元素就是该单词在文档中的计数。
在继续之前,让我们先看一个简单的例子。 想象一个具有两个文档的语料库:
docA = "the cat sat on my face"
docB = "the dog sat on my bed"
语料库词汇为:
{'bed', 'cat', 'dog', 'face', 'my', 'on', 'sat', 'the'}
因此,如果我们要为该语料库创建一个计数嵌入,它将看起来像这样:
bed |
cat |
dog |
face |
my |
on |
sat |
the |
|
|---|---|---|---|---|---|---|---|---|
| 文件 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 1 |
| 文件 1 | 1 | 0 | 1 | 0 | 1 | 1 | 1 | 1 |
这就是计数向量化。 这是我们工具箱中最简单的向量化技术。
计数向量化的问题在于我们使用了很多根本没有太多意义的单词。 实际上,英语中最常用的单词(the)占我们所讲单词的 7%,是第二个最受欢迎的单词(of)出现频率的两倍。 语言中单词的分布是幂律分布,这是称为 Zipf 定律的基础。 如果我们从计数中构造文档矩阵,那么最终得到的数字将包含很多信息,除非我们的目标是查看谁最经常使用the。
更好的策略是根据单词在文档中的相对重要性对单词进行加权。 为此,我们可以使用 TF-IDF。
一个单词的 TF-IDF 分数是:

在此公式中:

这个公式:

如果我们要为同一语料库计算 TF-IDF 矩阵,它将看起来像这样:
bed |
cat |
dog |
face |
my |
on |
sat |
the |
|
|---|---|---|---|---|---|---|---|---|
| 文件 0 | 0 | 0.116 | 0 | 0.116 | 0 | 0 | 0 | 0 |
| 文件 1 | 0.116 | 0 | 0.116 | 0 | 0 | 0 | 0 | 0 |
您可能会注意到,通过对单词频率乘以逆文档频率进行加权,我们取消了所有文档中出现的单词,从而放大了不同的单词。 文件 0 全部关于猫和脸,而文件 1 全部关于狗和床。 这正是我们对许多分类器所要的。
词嵌入
词袋模型具有一些不理想的属性,值得注意的是。
我们之前研究过的词袋模型的第一个问题是它们没有考虑单词的上下文。 他们并没有真正考虑文档中单词之间存在的关系。
第二个相关问题是向量空间中单词的分配有些随意。 可能无法捕获有关语料库词汇中两个单词之间的关系的信息。 例如,虽然鳄鱼和鳄鱼都是相似的具有许多特征的生物,但已经学会处理鳄鱼的单词的模型几乎无法利用鳄鱼学到的知识(爬行动物学家讨厌邮件) 。
最后,由于语料库的词汇量可能很大,并且可能不会出现在所有文档中,因此 BoW 模型往往会产生非常稀疏的向量。
单词嵌入模型通过为每个单词学习一个向量来解决这些问题,其中每个语义相似的单词都映射到(嵌入)附近的点。 另外,与 BoW 模型相比,我们将在更小的向量空间中表示整个词汇表。 这提供了降维效果,并为我们提供了一个更小,更密集的向量,该向量可以捕获单词的语义值。
词嵌入模型在现实文档分类问题和语义分析问题中通常比词袋模型具有很大的提升,因为这种能力可以保留词相对于语料库中其他词的语义值。
一个简单的例子
如果您不熟悉单词嵌入,那么您现在可能会感到有些迷茫。 挂在那儿,它很快就会变得清晰起来。 让我们尝试一个具体的例子。
使用流行的单词嵌入模型word2vec,我们可以从单词cat开始,找到它的 384 元素向量,如以下输出代码所示:
array([ 5.81600726e-01, 3.07168198e+00, 3.73339128e+00,
2.83814788e-01, 2.79787600e-01, 2.29124355e+00,
-2.14855480e+00, -1.22236431e+00, 2.20581269e+00,
1.81546474e+00, 2.06929898e+00, -2.71712840e-01,...
我缩短了输出,但您明白了。 此模型中的每个单词都将转换为 384 个元素的向量。 可以对这些向量进行比较,以评估数据集中单词的语义相似性。
现在我们有了猫的向量,我将计算狗和蜥蜴的词向量。 我建议猫比蜥蜴更像狗。 我应该能够测量猫向量和狗向量之间的距离,然后测量猫向量和蜥蜴向量之间的距离。 尽管有许多方法可以测量向量之间的距离,但余弦相似度可能是单词向量最常用的方法。 在下表中,我们正在比较猫与狗和蜥蜴的余弦相似度:
dog |
lizard |
|
|---|---|---|
cat |
0.74 | 0.63 |
不出所料,在我们的向量空间中,猫的含义比蜥蜴更接近狗。
通过预测学习单词嵌入
单词嵌入是通过使用专门为该任务构建的神经网络来计算的。 我将在这里介绍该网络的概述。 一旦计算了某些语料库的词嵌入,它们便可以轻松地重用于其他应用,因此使该技术成为迁移学习的候选者,类似于我们在第 8 章“使用预先训练的 CNN 的迁移学习”中介绍的技术。
当我们完成了对该词嵌入网络的训练后,我们网络中单个隐藏层的权重将成为我们词嵌入的查找表。 对于词汇表中的每个单词,我们将学习该单词的向量。
该隐藏层将包含比输入空间少的神经元,从而迫使网络学习输入层中存在的信息的压缩形式。 这种架构非常类似于自编码器。 但是,该技术围绕着一项任务,该任务帮助网络学习向量空间中每个单词的语义值。
我们将用来训练嵌入网络的任务是预测某些目标词出现在距训练词距离窗口内的概率。 例如,如果koala是我们的输入词,而marsupials是我们的目标词,则我们想知道这两个词彼此靠近的可能性。
此任务的输入层将是词汇表中每个单词的一个热编码向量。 输出层将是相同大小的softmax层,如下图所示:

该网络导致隐藏层的形状为权重矩阵[词汇 x 神经元]。 例如,如果我们的语料库中有 20,000 个唯一单词,而隐藏层中有 300 个神经元,那么我们的隐藏层权重矩阵将为20,000 x 300。将这些权重保存到磁盘后,我们将拥有一个 300 元素向量,可用于代表每个词。 然后,在训练其他模型时,可以使用这些向量表示单词。
当然,除此以外,还有更多的训练词嵌入网络的方法,而我故意过分简化了快速参考样式。
如果您想了解更多信息,我建议您先阅读 Mikolov 等人的《单词和短语的分布式表示及其组成》。 本文介绍了一种流行的创建单词嵌入的方法,称为word2vec。
通过计数学习单词嵌入
学习单词嵌入的另一种方法是通过计数。 用于词表示的全局向量或 GloVe 是 Pennington 等人创建的算法。
GloVe 通过创建单词共现的非常大的矩阵来工作。 对于某些语料库,这实际上是两个单词彼此相邻出现的次数的计数。 该算法的作者根据单词的接近程度来加权此计数,以使彼此接近的单词对每个计数的贡献更大。 一旦创建了这个共现矩阵,它将分解为一个较小的空间,从而生成一个单词 x 特征较大的矩阵。
有趣的是,word2vec和 GloVe 的结果非常相似,可以互换使用。 由 60 亿个单词的数据集预先构建的 GloVe 向量由斯坦福大学分发,是单词向量的常用来源。 本章稍后将使用 GloVe 向量。
从文本到文档
如果您一直在仔细阅读,您可能会注意到我尚未消除的鸿沟。 词嵌入模型为每个词创建一个向量。 相比之下,BoW 模型为每个文档创建一个向量。 那么,我们如何使用词嵌入模型进行文档分类呢?
一种幼稚的方法可能是获取文档中所有单词的向量并计算均值。 我们可能将此值解释为文档的平均语义值。 在实践中,通常使用此解决方案,并且可以产生良好的结果。 但是,它并不总是优于 BoW 嵌入模型。 考虑短语dog bites man和man bites dog。 希望您会同意我的观点,这是两个截然不同的陈述。 但是,如果我们对它们的词向量进行平均,它们将具有相同的值。 这使我们提出了一些其他策略,可以用来设计文档中的特征,例如使用每个向量的均值,最大值和最小值。
Le 和 Mikolov 在《句子和文档的分布式表示》中提出了一种从单词到文档的更好的想法。 基于word2vec的思想,本文将段落标识符添加到我们描述的用于学习单词向量的神经网络的输入中。 使用文本中的单词以及文档 ID 可以使网络学习将可变长度文档嵌入向量空间中。 该技术称为 doc2vec,它可以很好地用作主题建模以及为模型创建输入特征的技术。
最后,许多深度学习框架都包含了嵌入层的概念。 嵌入层使您可以了解嵌入空间,这是网络正在执行的总体任务的一部分。 使用深度神经网络时,嵌入层可能是向量化文本的最佳选择。 接下来让我们看一下嵌入层。
Keras 嵌入层
Keras 嵌入层允许我们学习输入词的向量空间表示,就像我们在训练模型时在word2vec中所做的那样。 使用函数式 API,Keras 嵌入层始终是网络中的第二层,紧随输入层之后。
嵌入层需要以下三个参数:
input_dim:语料库的词汇量。output_dim:我们要学习的向量空间的大小。 这将对应于word2vec隐藏层中神经元的数量。input_length:我们将在每次观察中使用的文字数量。 在下面的示例中,我们将根据需要发送的最长文本使用固定大小,并将较小的文档填充为 0。
嵌入层将为每个输入文档输出 2D 矩阵,该矩阵包含input_length指定的每个单词的一个向量。
例如,我们可能有一个如下所示的嵌入层:
Embedding(input_dim=10000, output_dim=128, input_length=10)
在这种情况下,该层的输出将是形状为10 x 128的 2D 矩阵,其中每个文档的 10 个单词将具有与之关联的 128 元素向量。
这样的单词序列可以作为 LSTM 的出色输入。 LSTM 层可以紧随嵌入层。 就像上一章一样,我们可以将嵌入层中的这 10 行视为 LSTM 的顺序输入。 在本章的第一个示例中,我将使用 LSTM,因此,如果您在未阅读第 9 章“从头开始训练 RNN”的情况下,则请花一点时间重新了解 LSTM 的操作,可以在此处找到。
如果我们想将嵌入层直接连接到密集层,则需要对其进行展平,但您可能不想这样做。 如果您有序列文本,通常使用 LSTM 是更好的选择。 我们还有另外一个有趣的选择。
用于自然语言处理的一维 CNN
回顾第 7 章,“从头开始训练 CNN”时,我们使用了卷积在图像区域上滑动窗口以学习复杂的视觉特征。 这使我们能够学习重要的局部视觉特征,而不管这些特征在图片中的位置,然后随着我们的网络越来越深入,逐步地学习越来越复杂的特征。 我们通常在 2D 或 3D 图像上使用3 x 3或5 x 5过滤器。 如果您对卷积层及其工作原理的理解感到生疏,则可能需要阅读第 7 章“从头开始训练 CNN”。
事实证明,我们可以对一系列单词使用相同的策略。 在这里,我们的 2D 矩阵是嵌入层的输出。 每行代表一个单词,并且该行中的所有元素都是其单词向量。 继续前面的示例,我们将有一个 10 x 128 的向量,其中连续有 10 个单词,每个单词都由 128 个元素的向量空间表示。 我们当然可以在这些单词上滑动过滤器。
卷积过滤器的大小针对 NLP 问题而改变。 当我们构建网络来解决 NLP 问题时,我们的过滤器将与单词向量一样宽。 过滤器的高度可以变化,通常在 2 到 5 之间。高度为 5 表示我们一次要在五个字上滑动过滤器。
事实证明,对于许多 NLP 问题,CNN 可以很好地运行,并且比 LSTM 快得多。 很难就何时使用 RNN/LSTM 和何时使用 CNN 给出确切的规则。 通常,如果您的问题需要状态,或者从很远的序列中学习到一些东西,那么使用 LSTM 可能会更好。 如果您的问题需要检测描述文本的特定单词集或文档的语义感觉,那么 CNN 可能会更快甚至更好地解决您的问题。
文档分类的案例研究
由于我已经提出了两种可行的文档分类方法,因此本章将包含两个单独的文档分类示例。 两者都将使用嵌入层。 一个将使用 LSTM,另一个将使用 CNN。
我们还将比较学习嵌入层与从其他人的权重开始采用迁移学习方法之间的表现。
这两个示例的代码都可以在本书的 Git 存储库中的Chapter10文件夹中找到。 某些数据和 GloVe 向量将需要分别下载。 有关说明,请参见代码中的注释。
Keras 嵌入层和 LSTM 的情感分析
本章的第一个案例研究将演示情绪分析。 在此示例中,我们将应用本章中学到的大多数内容。
我们将使用从互联网电影数据库(IMDB)内置于 Keras 中的数据集。 该数据集包含 25,000 条电影评论,每条评论均按情感标记。 正面评论标记为 1,负面评论标记为 0。此数据集中的每个单词均已替换为标识该单词的整数。 每个评论都被编码为单词索引序列。
我们的目标是仅使用评论中的文字将电影评论分为正面评论或负面评论。
准备数据
因为我们使用的是内置数据集,所以 Keras 会处理大量的日常工作,这些工作涉及标记,词干,停用词以及将词标记转换为数字标记的工作。 keras.datasets.imbd将为我们提供一个列表列表,每个列表包含一个长度可变的整数序列,这些整数表示审阅中的单词。 我们将使用以下代码定义数据:
def load_data(vocab_size):
data = dict()
data["vocab_size"] = vocab_size
(data["X_train"], data["y_train"]), (data["X_test"], data["y_test"]) =
imdb.load_data(num_words=vocab_size)
return data
我们可以通过调用load_data并为词汇表选择最大大小来加载数据。 在此示例中,我将使用 20,000 个单词作为词汇量。
如果需要手动执行此操作,以使示例代码可以解决您自己的问题,则可以使用keras.preprocessing.text.Tokenizer类,我们将在下一个示例中介绍该类。 我们将使用以下代码加载数据:
data = load_data(20000)
下一步,我希望这些序列中的每个序列都具有相同的长度,并且我需要此列表列表为 2D 矩阵,其中每个评论是一行,每列是一个单词。 为了使每个列表大小相同,我将用 0 填充较短的序列。 我们稍后将使用的 LSTM 将学习忽略那些 0,这对于我们当然非常方便。
这种填充操作相当普遍,因此已内置在 Keras 中。 我们可以通过以下代码使用keras.preprocessing.sequence.pad_sequences完成此操作:
def pad_sequences(data):
data["X_train"] = sequence.pad_sequences(data["X_train"])
data["sequence_length"] = data["X_train"].shape[1]
data["X_test"] = sequence.pad_sequences(data["X_test"], maxlen=data["sequence_length"])
return data
调用此函数会将列表列表转换为等长序列,并方便地将列表列表转换为 2D 矩阵,如下所示:
data = pad_sequences(data)
输入和嵌入层架构
在上一章中,我们使用时间序列中的一组滞后训练了 LSTM。 在这里,我们的滞后实际上是序列中的单词。 我们将使用这些词来预测审阅者的情绪。 为了从单词序列到考虑这些单词的语义值的输入向量,我们可以使用嵌入层。
使用 Keras 函数式 API,嵌入层始终是网络中输入层之后的第二层。 让我们看一下这两层如何结合在一起:
input = Input(shape=(sequence_length,), name="Input")
embedding = Embedding(input_dim=vocab_size, output_dim=embedding_dim,
input_length=sequence_length, name="embedding")(input)
我们的输入层需要知道序列长度,该长度与输入矩阵中的列数相对应。
嵌入层将使用输入层。 但是,它需要知道整体语料库词汇量,我们将这些词嵌入到的向量空间的大小以及序列长度。
我们定义的词汇量为 20,000 个单词,数据的序列长度为 2,494,并且指定的嵌入维数为 100。
将所有这些放在一起,嵌入层将从每个文件的 20,000 个输入热向量到每个文档的2,494 x 100 2D 矩阵,从而为序列中的每个单词嵌入向量空间。 随着模型的学习,嵌入层将不断学习。 很酷吧?
LSTM 层
我将在这里只使用一个 LSTM 层,只有 10 个神经元,如以下代码所示:
lstm1 = LSTM(10, activation='tanh', return_sequences=False,
dropout=0.2, recurrent_dropout=0.2, name='lstm1')(embedding)
为什么要使用这么小的 LSTM 层? 就像您将要看到的那样,该模型将因过拟合而陷入困境。 甚至只有 10 个 LSTM 单元也能很好地学习训练数据。 解决此问题的方法可能是添加数据,但实际上不能添加数据,因此保持网络结构简单是一个好主意。
这导致我们使用丢弃法。 我将在这一层同时使用丢弃法和经常性丢弃。 我们还没有谈论经常性丢弃的问题,所以让我们现在解决它。 以这种方式应用于 LSTM 层的常规过滤器将随机掩盖 LSTM 的输入。 循环丢弃会随机打开和关闭 LSTM 单元/神经元中展开的单元之间的内存。 与往常一样,丢弃是一个超参数,您需要搜索最佳值。
因为我们的输入是基于文档的,并且因为没有任何上下文,所以我们需要记住在文档之间,这是使用无状态 LSTM 的绝佳时机。
输出层
在此示例中,我们预测了二元目标。 和以前一样,我们可以使用具有单个 Sigmoid 神经元的密集层来完成此二分类任务:
output = Dense(1, activation='sigmoid', name='sigmoid')(lstm1)
放在一起
现在,我们了解了组成部分,现在来看整个网络。 该网络显示在以下代码中,以供您参考:
def build_network(vocab_size, embedding_dim, sequence_length):
input = Input(shape=(sequence_length,), name="Input")
embedding = Embedding(input_dim=vocab_size,
output_dim=embedding_dim, input_length=sequence_length,
name="embedding")(input)
lstm1 = LSTM(10, activation='tanh', return_sequences=False,
dropout=0.2, recurrent_dropout=0.2, name='lstm1')(embedding)
output = Dense(1, activation='sigmoid', name='sigmoid')(lstm1)
model = Model(inputs=input, outputs=output)
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
return model
与其他二分类任务一样,我们可以使用二元交叉熵。 请注意,因为我们正在将 LSTM 层连接到密集层,所以我们需要将return_sequences设置为False,正如我们在第 9 章,“从头训练”中讨论的那样。
为了使这部分代码可重用,我们使词汇量,嵌入维数和序列长度可配置。 如果要搜索超参数,则还可能希望参数化dropout,recurrent_dropout和 LSTM 神经元的数量。
训练网络
现在,我的情绪分析网络已经建立,现在该进行训练了:
data = load_data(20000)
data = pad_sequences(data)
model = build_network(vocab_size=data["vocab_size"],
embedding_dim=100,
sequence_length=data["sequence_length"])
callbacks = create_callbacks("sentiment")
model.fit(x=data["X_train"], y=data["y_train"],
batch_size=32,
epochs=10,
validation_data=(data["X_test"], data["y_test"]),
callbacks=callbacks)
像这样将我所有的训练参数和数据保存在一个字典中,实际上只是一个样式问题,而与函数无关。 您可能希望单独处理所有事情。 我喜欢对所有内容使用字典,因为它使我无法来回传递大量参数。
由于我们使用的是无状态 LSTM,因此我们将在每个批次中重置单元存储器。 我的信念是,我们可以在不损失任何罚款的情况下重置文档之间的单元状态,因此批量大小实际上与表现有关。 我在这里使用了 32 个观察批,但是只要 GPU 内存允许,128 个观察批会产生相似的结果,并且表现会有所提高。
表现
从下面的屏幕截图中,让我们看一下我们的网络运行情况。 检查这些图时,请密切注意y轴上的刻度。 虽然挥杆动作看起来很戏剧性,但幅度并不大:

这里首先要注意的是,在第 1 阶段,网络正在做的相当不错。 此后,它迅速开始过拟合。 总体而言,我认为我们的结果相当不错。 在第 1 阶段,我们会在验证集上正确预测约 86% 的时间的情绪。
尽管此案例研究涵盖了本章到目前为止已讨论的许多主题,但让我们再来看一个可以在嵌入层使用预训练的单词向量与我们学习的单词向量进行比较的地方。
有和没有 GloVe 的文档分类
在此示例中,我们将使用一个比较著名的文本分类问题,称为 news20。 在此问题中,我们获得了 19,997 个文档,每个文档都属于一个新闻组。 我们的目标是使用帖子的文本来预测该文本所属的新闻组。对于我们中间的千禧一代,新闻组是 Reddit 的先驱(但可能更接近伟大的 -Reddit 的曾祖父)。 这些新闻组涵盖的主题差异很大,包括政治,宗教和操作系统等主题,您应避免在礼貌的公司中讨论所有这些主题。 这些职位相当长,语料库中有 174,074 个独特的单词。
这次,我将构建模型的两个版本。 在第一个版本中,我们将使用嵌入层,并且将学习嵌入空间,就像在前面的示例中一样。 在第二个版本中,我将使用 GloVe 向量作为嵌入层的权重。 然后,我将花一些时间比较和对比这两种方法。
最后,在此示例中,我们将使用一维 CNN 代替 LSTM。
准备数据
当使用这样的文本文档时,可能需要很多平凡的代码才能使您到达想要的位置。 我将这个示例作为解决问题的一种方式。 一旦了解了这里发生的事情,就可以在将来的问题中重用其中的大部分内容并缩短开发时间,因此值得考虑。
以下函数将进入 20 个新闻组文本所在的顶级目录。 在该目录中,将有 20 个单独的目录,每个目录都有文件。 每个文件都是新闻组帖子:
def load_data(text_data_dir, vocab_size, sequence_length, validation_split=0.2):
data = dict()
data["vocab_size"] = vocab_size
data["sequence_length"] = sequence_length
# second, prepare text samples and their labels
print('Processing text dataset')
texts = [] # list of text samples
labels_index = {} # dictionary mapping label name to numeric id
labels = [] # list of label ids
for name in sorted(os.listdir(text_data_dir)):
path = os.path.join(text_data_dir, name)
if os.path.isdir(path):
label_id = len(labels_index)
labels_index[name] = label_id
for fname in sorted(os.listdir(path)):
if fname.isdigit():
fpath = os.path.join(path, fname)
if sys.version_info < (3,):
f = open(fpath)
else:
f = open(fpath, encoding='latin-1')
t = f.read()
i = t.find('\n\n') # skip header
if 0 < i:
t = t[i:]
texts.append(t)
f.close()
labels.append(label_id)
print('Found %s texts.' % len(texts))
data["texts"] = texts
data["labels"] = labels
return data
对于每个目录,我们将使用目录名称并将其添加到将其映射为数字的字典中。 这个数字将成为我们想要预测的值,我们的标签。 我们将把标签列表保留在data["labels"]中。
同样,对于文本,我们将打开每个文件,仅解析相关文本,而忽略有关谁在信息中张贴的垃圾邮件。 然后,我们将文本存储在data["texts"]中。 顺便说一句,删除标头中标识新闻组的部分非常重要。 那是作弊!
最后,我们剩下一个文本列表和一个相应的标签列表。 但是,此时,这些文本都是字符串。 我们需要做的下一件事是将这些字符串拆分为单词标记,将这些标记转换为数字标记,并填充序列,以使它们具有相同的长度。 这几乎是我们在前面的示例中所做的; 但是,在我们之前的示例中,数据已预先加标记。 我将使用此函数来完成任务,如以下代码所示:
def tokenize_text(data):
tokenizer = Tokenizer(num_words=data["vocab_size"])
tokenizer.fit_on_texts(data["texts"])
data["tokenizer"] = tokenizer
sequences = tokenizer.texts_to_sequences(data["texts"])
word_index = tokenizer.word_index
print('Found %s unique tokens.' % len(word_index))
data["X"] = pad_sequences(sequences, maxlen=data["sequence_length"])
data["y"] = to_categorical(np.asarray(data["labels"]))
print('Shape of data tensor:', data["X"].shape)
print('Shape of label tensor:', data["y"].shape)
# texts and labels aren't needed anymore
data.pop("texts", None)
data.pop("labels", None)
return data
在这里,我们获取该文本列表,并使用keras.preprocessing.text.Tokenizer将其标记化。 之后,我们将它们填充为相等的长度。 最后,我们将数字标签转换为one_hot格式,就像 Keras 在其他多分类问题中一样。
我们几乎完成了数据处理。 但是,最后,我们需要获取文本和标签,然后将数据随机分成训练,验证和测试集,如以下代码所示。 我没有太多数据需要处理,因此我将在此处选择test和val。 如果样本太小,可能无法很好地理解实际模型的表现,因此在执行此操作时要格外小心:
def train_val_test_split(data):
data["X_train"], X_test_val, data["y_train"], y_test_val = train_test_split(data["X"],
data["y"],
test_size=0.2,
random_state=42)
data["X_val"], data["X_test"], data["y_val"], data["y_test"] = train_test_split(X_test_val,
y_test_val,
test_size=0.25,
random_state=42)
return data
加载预训练的单词向量
正如我刚才提到的,我将使用 Keras 嵌入层。 对于模型的第二个版本,我们将使用本章前面介绍的 GloVe 字向量来初始化嵌入层的权重。 为此,我们将需要从磁盘加载这些权重,并将它们放入合适的 2D 矩阵中,该层可用作权重。 我们将在这里介绍该操作。
下载 GloVe 向量时,您会发现在将下载文件解压缩到的目录中有几个文本文件。每个文件都对应一组单独的尺寸。 但是,在所有情况下,这些载体都是使用包含 60 亿个唯一单词的相同通用语料库开发的(因此标题为GloVe.6B)。 我将演示如何使用glove.6B.100d.txt文件。 在glove.6B.100d.txt中,每行都是单个单词向量。 在该行上,您将找到该单词和与其相关联的 100 维向量。 单词和向量的元素存储为文本,并用空格分隔。
为了使这些数据进入可用状态,我们将从磁盘加载开始。 然后,我们将线分为第一部分,单词和向量的元素。 完成此操作后,我们将向量转换为数组。 最后,我们将单词作为该值的键将数组作为值存储在字典中。 以下代码说明了此过程:
def load_word_vectors(glove_dir):
print('Indexing word vectors.')
embeddings_index = {}
f = open(os.path.join(glove_dir, 'glove.6B.100d.txt'),
encoding='utf8')
for line in f:
values = line.split()
word = values[0]
coefs = np.asarray(values[1:], dtype='float32')
embeddings_index[word] = coefs
f.close()
print('Found %s word vectors.' % len(embeddings_index))
return embeddings_index
运行此命令后,我们将有一个名为embeddings_index的字典,其中包含 GloVe 单词作为键,其向量作为值。 Keras 嵌入层需要 2D 矩阵作为输入,但是不需要字典,因此我们需要使用以下代码将字典操纵为矩阵:
def embedding_index_to_matrix(embeddings_index, vocab_size, embedding_dim, word_index):
print('Preparing embedding matrix.')
# prepare embedding matrix
num_words = min(vocab_size, len(word_index))
embedding_matrix = np.zeros((num_words, embedding_dim))
for word, i in word_index.items():
if i >= vocab_size:
continue
embedding_vector = embeddings_index.get(word)
if embedding_vector is not None:
# words not found in embedding index will be all-zeros.
embedding_matrix[i] = embedding_vector
return embedding_matrix
我知道所有这些烦恼似乎都是可怕的,但确实如此,但是 GloVe 的作者在如何分配这些单词向量方面非常有心。 他们希望使使用任何一种编程语言的任何人都可以使用这些向量,为此,文本格式将受到人们的赞赏。 此外,如果您是一名实践中的数据科学家,您将习惯于此!
现在,我们将向量表示为 2D 矩阵,现在可以在 Keras 嵌入层中使用它们了。 我们的准备工作已经完成,所以现在让我们建立网络。
输入和嵌入层架构
我们在这里格式化 API 的方式与前面的示例稍有不同。 这种略有不同的结构将使在嵌入层中使用预训练向量更加容易。 我们将在以下各节中讨论这些结构性更改。
没有 GloVe 向量
让我们演示没有先训练词向量的embedding层的代码。 此代码应与上一个示例中的代码几乎相同:
sequence_input = Input(shape=(sequence_length,), dtype='int32')
embedding_layer = Embedding(input_dim=vocab_size,
output_dim=embedding_dim,
input_length=sequence_length,
name="embedding")(sequence_input)
带有 GloVe 向量
现在,将其与包含以 2D 矩阵编码的预先训练的 GloVe 向量的代码进行比较:
sequence_input = Input(shape=(sequence_length,), dtype='int32')
embedding_layer = Embedding(input_dim=vocab_size,
output_dim=embedding_dim,
weights=[embedding_matrix],
input_length=sequence_length,
trainable=False,
name="embedding")(sequence_input)
在大多数情况下,此代码看起来是等效的。 有两个主要区别:
- 我们初始化层权重以包含在我们与
weights=[embedding_matrix]组装的 GloVe 矩阵中。 - 我们还将层设置为
trainable=False。 这将阻止我们更新权重。 您可能希望以与微调权重相似的方式微调权重,该方式类似于我们在第 8 章“使用预训练的 CNN”进行的迁移学习中构建的 CNN,但是在大多数情况下, 不必要或没有帮助。
卷积层
对于一维卷积,层可以使用keras.layers.Conv1D。 我们将需要使用MaxPooling1D层以及Conv1D层,如以下代码所示:
x = Conv1D(128, 5, activation='relu')(embedding_layer)
x = MaxPooling1D(5)(x)
x = Conv1D(128, 5, activation='relu')(x)
x = MaxPooling1D(5)(x)
x = Conv1D(128, 5, activation='relu')(x)
x = GlobalMaxPooling1D()(x)
对于Conv1D层,第一个整数参数是单元数,第二个是过滤器大小。 我们的过滤器只有一维,因此命名为 1D 卷积。 上例中的窗口大小为 5。
我正在使用的MaxPooling1D层也将使用 5 的窗口大小。相同的规则适用于一维实现中的池化层。
在最后一个卷积层之后,我们应用GlobalMaxPooling1D层。 该层是最大池化的特殊实现,它将获取最后一个Conv1D层(一个[batch x 35 x 128]张量)的输出,并跨时间步长将其合并到[batch x 128]。 这通常是在 NLP 网络中完成的,其目的类似于在基于图像的卷积网络中使用Flatten()层。 该层充当卷积层和密集层之间的桥梁。
输出层
此示例中的输出层看起来像其他任何多分类。 我在输出层之前也包括了一个密集层,如以下代码所示:
x = Dense(128, activation='relu')(x)
preds = Dense(20, activation='softmax')(x)
放在一起
和以前一样,我们将在此处显示整个神经网络结构。 请注意,此结构适用于包含 GloVe 向量的模型版本:
def build_model(vocab_size, embedding_dim, sequence_length, embedding_matrix):
sequence_input = Input(shape=(sequence_length,), dtype='int32')
embedding_layer = Embedding(input_dim=vocab_size,
output_dim=embedding_dim,
weights=[embedding_matrix],
input_length=sequence_length,
trainable=False,
name="embedding")(sequence_input)
x = Conv1D(128, 5, activation='relu')(embedding_layer)
x = MaxPooling1D(5)(x)
x = Conv1D(128, 5, activation='relu')(x)
x = MaxPooling1D(5)(x)
x = Conv1D(128, 5, activation='relu')(x)
x = GlobalMaxPooling1D()(x)
x = Dense(128, activation='relu')(x)
preds = Dense(20, activation='softmax')(x)
model = Model(sequence_input, preds)
model.compile(loss='categorical_crossentropy',
optimizer='adam',
metrics=['accuracy'])
return model
我在这里再次使用adam,categorical_crossentropy和accuracy。 尽管本章介绍了许多新主题,但希望能看到保持不变的感觉会有些安慰。
训练
将所有代码放在一起,只需几行就可以完成训练,如以下代码所示:
glove_dir = os.path.join(BASE_DIR, 'glove.6B')
text_data_dir = os.path.join(BASE_DIR, '20_newsgroup')
embeddings_index = load_word_vectors(glove_dir)
data = load_data(text_data_dir, vocab_size=20000, sequence_length=1000)
data = tokenize_text(data)
data = train_val_test_split(data)
data["embedding_dim"] = 100
data["embedding_matrix"] = embedding_index_to_matrix(embeddings_index=embeddings_index,
vocab_size=data["vocab_size"],
embedding_dim=data["embedding_dim"],
word_index=data["tokenizer"].word_index)
callbacks = create_callbacks("newsgroups-pretrained")
model = build_model(vocab_size=data["vocab_size"],
embedding_dim=data['embedding_dim'],
sequence_length=data['sequence_length'],
embedding_matrix=data['embedding_matrix'])
model.fit(data["X_train"], data["y_train"],
batch_size=128,
epochs=10,
validation_data=(data["X_val"], data["y_val"]),
callbacks=callbacks)
请注意,我们只训练 10 个周期,因此将这个问题的损失降到最低不会花很长时间。
表现
而我们在这里处于关键时刻。 让我们看看我的表现如何。 更重要的是,让我们将 GloVe 向量与该问题的学习向量进行比较。
以下屏幕截图中的橙色线对应于学习的嵌入层,蓝色线对应于 GloVe 向量:

GloVe 预先训练的网络不仅学习得更快,而且在每个周期都表现得更好。 总体而言,这些网络似乎在学习文档分类任务方面做得很好。 大约在第五个周期之后,它们都开始过拟合。 但是,GloVe 模型比没有使用 GloVe 训练的网络更能防止过拟合。
通常,我建议尽可能在任何地方使用迁移学习。 图片和文字都是如此。
如果通过这些示例与我一起工作,我建议您对 LSTM 尝试同样的问题。 我认为使用 LSTM 时,您会发现该问题更加难以解决,并且难以解决过拟合问题。
总结
在本章中,我们以一般形式以及在情感分析的特定情况下研究了文档分类。 在此过程中,我们涵盖了很多 NLP 主题,包括 Word 袋模型,向量空间模型以及每个模型的相对优点。 我们还研究了使用 LSTM 和 1D 卷积进行文本分析。 最后,我们训练了两个单独的文档分类器,并通过实际示例应用了我们讨论的所有内容。
在下一章中,我们将讨论一个非常酷的自然语言模型,该模型将允许我们实际生成单词,称为序列到序列模型。
十一、训练 Seq2Seq 模型
在上一章中,我们讨论了文档分类以及文档分类的一种特殊情况,称为情感分类。 这样做时,我们不得不谈论很多关于向量化的知识。
在本章中,我们将继续谈论解决 NLP 问题,但是除了分类之外,我们将生成新的单词序列。
我们将在本章介绍以下主题:
- 序列到序列模型
- 机器翻译
序列到序列模型
到目前为止,我们所研究的网络已经做了一些真正令人惊奇的事情。 但是它们都有一个很大的局限性:它们只能应用于输出具有固定且众所周知的大小的问题。
序列到序列模型能够将输入序列映射到具有可变长度的输出序列。
您可能还会看到术语序列到序列,甚至 Seq2Seq。 这些都是序列到序列模型的术语。
当使用序列到序列模型时,我们将引入一个序列并交换出一个序列。 这些序列的长度不必相同。 序列到序列模型使我们能够学习输入序列和输出序列之间的映射。
序列到序列模型可能在许多应用中有用,我们接下来将讨论这些应用。
序列到序列模型的应用
序列到序列模型具有许多实际应用。
也许最实际的应用是机器翻译。 我们可以使用机器翻译将一种语言的短语作为输入,并输出另一种语言的短语。 机器翻译是我们越来越依赖的一项重要服务。 得益于计算机视觉和机器翻译的进步,我们可以听不懂的语言,或者用不懂的语言查看标志,并且几乎可以立即在智能手机上获得不错的翻译。 序列到序列的网络确实使我们非常接近道格拉斯·亚当(Douglas Adam)想象的《银河系漫游指南》中的通天鱼。
问答也可以全部或部分通过序列到序列模型来完成,在这里我们可以将问题想象为输入序列,将答案想象为输出序列。 回答问题最普遍的应用是聊天。 如果您通过呼叫中心为企业提供支持,则每天会有成千上万甚至数百万个问题/答案对通过电话传递。 对于序列到序列聊天机器人来说,这是完美的训练。
我们可以利用这种问答方式的多种细微形式。 每天,我收到大约 34 亿封电子邮件。 其中,我可能只需要阅读 20-30(这是一个分类任务)。 但是,我对这些电子邮件的回复很少新颖。 我几乎可以肯定地创建一个序列到序列的网络,该网络可以为我写电子邮件,或者至少起草回复。 我认为我们已经开始看到这种行为已经内置在我们最喜欢的电子邮件程序中,并且肯定会出现更加全自动的响应。
序列到序列网络的另一个重要用途是自动文本摘要。 想象一下一组研究论文或大量期刊文章。 所有这些论文可能都有摘要。 这只是另一个翻译问题。 给定一些论文,我们可以使用序列到序列网络生成摘要。 网络可以学习以这种方式总结文档。
在本章的后面,我们将实现一个序列到序列的网络来进行机器翻译。 不过,在进行此操作之前,让我们了解一下这种网络架构是如何工作的。
序列到序列模型架构
理解序列到序列模型架构的关键是要理解该架构是为了允许输入序列的长度与输出序列的长度而变化的。 然后可以使用整个输入序列来预测长度可变的输出序列。
为此,网络被分为两个独立的部分,每个部分都包含一个或多个 LSTM 层,这些层负责一半的任务。 如果您想对其操作进行复习,我们在第 9 章“从头开始训练 RNN”中讨论了 LSTM。 我们将在以下各节中了解这两个部分。
编码器和解码器
序列到序列模型由两个单独的组件组成,一个编码器和一个解码器:
- 编码器:模型的编码器部分采用输入序列,并返回输出和网络的内部状态。 我们并不在乎输出。 我们只想保留编码器的状态,即输入序列的内存。
- 解码器:然后,模型的解码器部分将来自编码器的状态(称为上下文或条件)作为输入。 然后,根据前一个时间步长的输出,可以预测每个时间步长的目标序列。
然后,编码器和解码器如下图所示一起工作,获取输入序列并生成输出序列。 如您所见,我们使用特殊字符表示序列的开始和结束。
我们知道,一旦序列字符的结尾(我称之为<EOS>)结束,就停止生成输出:

尽管此示例涵盖了机器翻译,但是序列到序列学习的其他应用却以相同的方式工作。
字符与文本
可以在字符级别或单词级别建立序列到序列模型。 单词级序列到序列模型将单词作为输入的原子单位,而字符级模型将字符作为输入的原子单位。
那么,您应该使用哪个呢? 通常,最好的结果是从单词级模型中获得的。 就是说,预测序列中最可能出现的下一个单词需要softmax层与问题的词汇量一样宽。 这导致了非常广泛的,高度尺寸的问题。
字符级模型要小得多。 字母表中有 26 个字母,但大约有 171,000 个英文单词是常用的。
对于本章中提出的问题,我将使用字符级模型,因为我重视您的 AWS 预算。 转换为单词非常简单,其中大部分复杂性都在数据准备中,这是留给读者的练习。
监督强迫
如上图所示,当预测序列y[t(n)]某个位置的输出时,我们使用y[t(n-1)]作为 LSTM 的输入。 然后,我们使用此时间步骤的输出来预测y[t(n+1)]。
训练中这样做的问题是,如果y[t(n-1)]错误,则y[t(n)]将更加错误。 错误不断增加的链条会使事情变得非常缓慢。
解决该问题的一个显而易见的解决方案是将每个时间步长的每个序列预测替换为该时间步长的实际正确序列。 因此,我们将使用训练集中的实际值,而不是对y[t(n-1)]使用 LSTM 预测。
通过使用这个概念,我们可以促进模型的训练过程,这恰好被称为监督强迫。
教师强迫有时会使我们的模型难以可靠地生成训练中看不到的序列,但总的来说,该技术可能会有所帮助。
注意
注意是可以在序列到序列模型中实现的另一种有用的训练技巧。 注意使解码器在输入序列的每个步骤中都能看到隐藏状态。 这使网络可以专注于(或关注)特定的输入,这可以加快训练速度并可以提高模型的准确率。 注意通常是一件好事。 但是,在撰写本文时,Keras 尚未内置注意力。尽管如此,Keras 目前确实有一个拉取请求正在等待自定义注意层。 我怀疑很快就会在 Keras 中建立对关注的支持。
翻译指标
知道翻译是否良好很难。 机器翻译质量的通用度量标准称为双语评估研究(BLEU),它最初是由 Papineni 等人在《BLEU:一种自动评估机器翻译的方法》中创建的。 BLEU 是基于 ngram 的分类精度的改进应用。 如果您想使用 BLEU 来衡量翻译质量,TensorFlow 团队已经发布了一个脚本,该脚本可以根据给定的地面真实翻译和机器预测翻译的语料来计算 BLEU 分数。 您可以在这里找到该脚本。
机器翻译
Je ne parle pasfrançais,那就是你怎么说我不会说英语的法语。 大约两年前,我发现自己在巴黎,几乎不会说法语。 在我去之前,我已经看过一本书,听过一些 DVD,但是即使经过几个月的练习,我对法语的掌握还是很可悲的。 然后,在旅途的第一个早晨,我醒来,走进附近的boulangerie(法国或法式面包店)吃早餐和早晨咖啡。 我说Bonjour, parlez-vous anglais?,他们一点也不讲英语,或者也许他们正在享受我的奋斗。 无论哪种方式,当我的早餐取决于我对法语的掌握时,我都会比过去更有动力去争取Je voudrais un pain au chocolat(翻译:我想要其中一种美味的巧克力面包)。 在最终成本函数(我的胃)的驱动下,我很快学会了在英语序列和法语序列之间进行映射。
在本案例研究中,我们将教计算机讲法语。 在几个小时的训练中,该模型将比我说法语更好。 考虑一下,这真是太神奇了。 我将训练一台计算机来执行我自己无法完成的任务。 当然,也许您确实会说法语,但这并不会给您留下深刻的印象,在这种情况下,我将美国著名演员亚当·桑德勒(Adam Sandler)称为比利·麦迪逊(Billy Madison):好吧,对我来说很难,所以退缩!
该示例的大部分来自于弗朗索瓦·乔勒(Francois Chollet)的博客文章,标题为《序列到序列学习的十分钟介绍》。 尽管我怀疑自己是否可以改进这项工作,但我希望使用本示例的目的是花一点点多一点的时间看一下序列到序列的网络,以使您掌握实现自己的所有知识。
与往常一样,本章的代码可以在本书的 Git 存储库中的Chapter11下找到。 您可以在这个页面中找到此示例所需的数据,该文件将存档许多双语句子对的数据集,我们将在后面详细讨论。 我要使用的文件是 fra-eng.zip 。 这是英语/法语句子对的集合。 如果需要,您可以轻松选择其他语言,而无需进行太多修改。
在本案例研究中,我们将构建一个网络,该网络可以在给定一些英语句子的情况下学习法语句子。 这将是一个具有老师强迫作用的字符级序列到序列模型。
我希望最终得到的是看起来很像翻译服务的东西,您可以在网上找到它或下载到手机上。
了解数据
我们正在使用的数据是一个文本文件。 每行都有一个英文短语及其法语翻译,并用一个选项卡分隔,如以下代码所示:
Ignore Tom. Ignorez Tom.
(我不确定Tom对数据集的作者做了什么...)
通常,每行英语翻译都有重复的法语翻译行。 当有多种常用方法翻译英语短语时,会发生这种情况。 看下面的代码例如:
Go now. Va, maintenant.
Go now. Allez-y maintenant.
Go now. Vas-y maintenant.
由于我们正在构建一个字符级序列到序列模型,因此需要将数据加载到内存中,然后对每个输入和输出在字符级进行热编码。 那是困难的部分。 让我们接下来做。
加载数据
加载此数据涉及很多工作。 阅读本文时,您可能想参考代码块。
以下代码中的第一个for循环将遍历整个输入文件或调用load_data()时指定的一些样本。 我这样做是因为您可能没有 RAM 来加载整个数据集。 多达 10,000 个示例,您可能会获得良好的结果; 但是,多多益善。
当我们逐行浏览输入文件时,我们一次要执行几项操作:
- 我们将每个法语翻译包装在
'\t'中,以开始该短语,并在'\n'中,以结束它。 这对应于我在序列到序列图中使用的<SOS>和<EOS>标签。 当我们要生成翻译序列时,这将允许我们使用'\t'作为输入来为解码器设定种子。 - 我们将每一行分为英语输入和其各自的法语翻译。 这些存储在列表
input_texts和target_texts中。 - 最后,我们将输入文本和目标文本的每个字符添加到一个集合中。 这些集称为
input_characters和target_characters。 当需要对短语进行热编码时,我们将使用这些集合。
循环完成后,我们会将字符集转换为排序列表。 我们还将创建名为num_encoder_tokens和num_decoder_tokens的变量,以保存每个列表的大小。 稍后我们也将需要这些以进行单热编码。
为了将输入和目标输入矩阵,我们需要像上一章一样,将短语填充到最长短语的长度。 为此,我们需要知道最长的短语。 我们将其存储在max_encoder_seq_length和max_decoder_seq_length中,如以下代码所示:
def load_data(num_samples=50000, start_char='\t', end_char='\n', data_path='data/fra-eng/fra.txt'):
input_texts = []
target_texts = []
input_characters = set()
target_characters = set()
lines = open(data_path, 'r', encoding='utf-8').read().split('\n')
for line in lines[: min(num_samples, len(lines) - 1)]:
input_text, target_text = line.split('\t')
target_text = start_char + target_text + end_char
input_texts.append(input_text)
target_texts.append(target_text)
for char in input_text:
if char not in input_characters:
input_characters.add(char)
for char in target_text:
if char not in target_characters:
target_characters.add(char)
input_characters = sorted(list(input_characters))
target_characters = sorted(list(target_characters))
num_encoder_tokens = len(input_characters)
num_decoder_tokens = len(target_characters)
max_encoder_seq_length = max([len(txt) for txt in input_texts])
max_decoder_seq_length = max([len(txt) for txt in target_texts])
print('Number of samples:', len(input_texts))
print('Number of unique input tokens:', num_encoder_tokens)
print('Number of unique output tokens:', num_decoder_tokens)
print('Max sequence length for inputs:', max_encoder_seq_length)
print('Max sequence length for outputs:', max_decoder_seq_length)
return {'input_texts': input_texts, 'target_texts': target_texts,
'input_chars': input_characters, 'target_chars':
target_characters, 'num_encoder_tokens': num_encoder_tokens,
'num_decoder_tokens': num_decoder_tokens,
'max_encoder_seq_length': max_encoder_seq_length,
'max_decoder_seq_length': max_decoder_seq_length}
加载数据后,我们将在字典中返回所有这些信息,这些信息可以传递给一个函数,该函数将对每个短语进行热编码。 让我们接下来做。
单热编码
在此函数中,我们将使用刚刚构建的字典,并对每个短语的文本进行热编码。
一旦完成,我们将剩下三个字典。 它们每个的尺寸为[文本数 * 最大序列长度 * 标记]。 如果您停顿一下,回想一下第 10 章“使用单词嵌入从零开始训练 LSTM”的更简单的时间,您会发现这确实与我们在其他 NLP 模型中使用的相同,我们在输入端完成它。 我们将使用以下代码定义单热编码:
def one_hot_vectorize(data):
input_chars = data['input_chars']
target_chars = data['target_chars']
input_texts = data['input_texts']
target_texts = data['target_texts']
max_encoder_seq_length = data['max_encoder_seq_length']
max_decoder_seq_length = data['max_decoder_seq_length']
num_encoder_tokens = data['num_encoder_tokens']
num_decoder_tokens = data['num_decoder_tokens']
input_token_index = dict([(char, i) for i, char in
enumerate(input_chars)])
target_token_index = dict([(char, i) for i, char in
enumerate(target_chars)])
encoder_input_data = np.zeros((len(input_texts),
max_encoder_seq_length, num_encoder_tokens), dtype='float32')
decoder_input_data = np.zeros((len(input_texts),
max_decoder_seq_length, num_decoder_tokens), dtype='float32')
decoder_target_data = np.zeros((len(input_texts),
max_decoder_seq_length, num_decoder_tokens), dtype='float32')
for i, (input_text, target_text) in enumerate(zip(input_texts,
target_texts)):
for t, char in enumerate(input_text):
encoder_input_data[i, t, input_token_index[char]] = 1.
for t, char in enumerate(target_text):
# decoder_target_data is ahead of decoder_input_data by one
timestep
decoder_input_data[i, t, target_token_index[char]] = 1.
if t > 0:
# decoder_target_data will be ahead by one timestep
# and will not include the start character.
decoder_target_data[i, t - 1, target_token_index[char]] = 1.
data['input_token_index'] = input_token_index
data['target_token_index'] = target_token_index
data['encoder_input_data'] = encoder_input_data
data['decoder_input_data'] = decoder_input_data
data['decoder_target_data'] = decoder_target_data
return data
我们在此代码中创建了三个训练向量。 在继续之前,我想确保我们了解以下所有向量:
encoder_input_data是形状为number_of_pairs,max_english_sequence_length,number_of_english_characters的 3D 矩阵。decoder_input_data是形状(number_of_pairs,max_french_sequence_length,number_of_french_characters)的 3d 矩阵。decoder_output_data与decoder_input_data相同,仅向前移了一个时间步。 这意味着decoder_input_data[:, t+1, :]等于decoder_output_data[:, t, :]。
前面的每个向量都是字符层上整个短语的一个热编码表示。 这意味着,如果我们输入的短语是 Go! 向量的第一步是为文本中每个可能的英文字符包含一个元素。 除g设置为 1 以外,其他每个元素都将设置为0。
我们的目标是使用encoder_input_data和decoder_input数据作为输入特征,训练序列至序列模型来预测decoder_output_data。
终于完成了数据准备,因此我们可以开始构建序列到序列的网络架构。
用于训练的网络架构
在此示例中,我们实际上将使用两种单独的架构,一种用于训练,另一种用于推理。 我们将从推理模型训练中使用训练过的层。 虽然实际上我们为每种架构使用了相同的部分,但是为了使事情更清楚,我将分别展示每个部分。 以下是我们将用来训练网络的模型:
encoder_input = Input(shape=(None, num_encoder_tokens), name='encoder_input')
encoder_outputs, state_h, state_c = LSTM(lstm_units, return_state=True,
name="encoder_lstm")(encoder_input)
encoder_states = [state_h, state_c]
decoder_input = Input(shape=(None, num_decoder_tokens), name='decoder_input')
decoder_lstm = LSTM(lstm_units, return_sequences=True,
return_state=True, name="decoder_lstm")
decoder_outputs, _, _ = decoder_lstm(decoder_input, initial_state=encoder_states)
decoder_dense = Dense(num_decoder_tokens, activation='softmax',
name='softmax_output')
decoder_output = decoder_dense(decoder_outputs)
model = Model([encoder_input, decoder_input], decoder_output)
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
如果我们将放大编码器,则会看到相当标准的 LSTM。 不同之处在于,我们从编码器(return_state=True)获取状态,如果将 LSTM 连接到密集层,通常不会这样做。 这些状态是我们将在encoder_states中捕获的状态。 我们将使用它们为解码器提供上下文或条件。
在解码器方面,我们设置的decoder_lstm与我们先前构建 Keras 层的方式略有不同,但实际上只是语法略有不同。
看下面的代码:
decoder_lstm = LSTM(lstm_units, return_sequences=True,
return_state=True, name="decoder_lstm")
decoder_outputs, _, _ = decoder_lstm(decoder_input, initial_state=encoder_states)
其功能与以下代码相同:
decoder_outputs, _, _ = LSTM(lstm_units, return_sequences=True,
return_state=True, name="decoder_lstm")(decoder_input, initial_state=encoder_states)
我这样做的原因在推理架构中将变得显而易见。
请注意,解码器将编码器的隐藏状态作为其初始状态。 然后将解码器输出传递到预测decoder_output_data的softmax层。
最后,我们将定义训练模型,我将其创造性地称为model,该模型将encoder_input_data和decoder_input数据作为输入并预测decoder_output_data。
用于推理的网络架构
为了在给定输入序列的情况下预测整个序列,我们需要稍微重新安排一下架构。 我怀疑在 Keras 的未来版本中,这将变得更简单,但是从今天起这是必需的步骤。
为什么需要有所不同? 因为我们没有推断的decoder_input_data教师向量。 我们现在独自一人。 因此,我们将必须进行设置,以便我们不需要该向量。
让我们看一下这种推理架构,然后逐步执行代码:
encoder_model = Model(encoder_input, encoder_states)
decoder_state_input_h = Input(shape=(lstm_units,))
decoder_state_input_c = Input(shape=(lstm_units,))
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]
decoder_outputs, state_h, state_c = decoder_lstm(
decoder_input, initial_state=decoder_states_inputs)
decoder_states = [state_h, state_c]
decoder_outputs = decoder_dense(decoder_outputs)
decoder_model = Model(
[decoder_input] + decoder_states_inputs,
[decoder_outputs] + decoder_states)
首先,我们从构建编码器模型开始。 该模型将采用一个输入序列,并返回我们在先前模型中训练过的 LSTM 的隐藏状态。
然后,解码器模型具有两个输入,即h和c隐藏状态,这些状态限制了其从编码器模型派生的输出。 我们统称为decoder_states_inputs。
我们可以从上面重用decoder_lstm; 但是,这次我们不会丢弃状态state_h和state_c。 我们将把它们与目标的softmax预测一起作为网络输出传递。
现在,当我们推断出一个新的输出序列时,我们可以在预测第一个字符之后获得这些状态,然后将它们通过softmax预测传递回 LSTM,以便 LSTM 可以预测另一个字符。 我们将重复该循环,直到解码器生成一个'\n'信号为止,该信号已到达<EOS>。
我们将很快看一下推理代码。 现在,让我们看看如何训练和序列化此模型集合。
放在一起
按照本书的传统,我将在这里向您展示该模型的整个架构如何融合在一起:
def build_models(lstm_units, num_encoder_tokens, num_decoder_tokens):
# train model
encoder_input = Input(shape=(None, num_encoder_tokens),
name='encoder_input')
encoder_outputs, state_h, state_c = LSTM(lstm_units,
return_state=True, name="encoder_lstm")(encoder_input)
encoder_states = [state_h, state_c]
decoder_input = Input(shape=(None, num_decoder_tokens),
name='decoder_input')
decoder_lstm = LSTM(lstm_units, return_sequences=True,
return_state=True, name="decoder_lstm")
decoder_outputs, _, _ = decoder_lstm(decoder_input,
initial_state=encoder_states)
decoder_dense = Dense(num_decoder_tokens, activation='softmax',
name='softmax_output')
decoder_output = decoder_dense(decoder_outputs)
model = Model([encoder_input, decoder_input], decoder_output)
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
encoder_model = Model(encoder_input, encoder_states)
decoder_state_input_h = Input(shape=(lstm_units,))
decoder_state_input_c = Input(shape=(lstm_units,))
decoder_states_inputs = [decoder_state_input_h,
decoder_state_input_c]
decoder_outputs, state_h, state_c = decoder_lstm(
decoder_input, initial_state=decoder_states_inputs)
decoder_states = [state_h, state_c]
decoder_outputs = decoder_dense(decoder_outputs)
decoder_model = Model(
[decoder_input] + decoder_states_inputs,
[decoder_outputs] + decoder_states)
return model, encoder_model, decoder_model
请注意,我们将在此处返回所有三个模型。 训练完训练模型后,我将使用keras model.save()方法序列化这三个方法。
训练
我们终于准备好训练我们的序列到序列网络。 以下代码首先调用我们所有的数据加载函数,创建回调,然后拟合模型:
data = load_data()
data = one_hot_vectorize(data)
callbacks = create_callbacks("char_s2s")
model, encoder_model, decoder_model = build_models(256, data['num_encoder_tokens'], data['num_decoder_tokens'])
print(model.summary())
model.fit(x=[data["encoder_input_data"], data["decoder_input_data"]],
y=data["decoder_target_data"],
batch_size=64,
epochs=100,
validation_split=0.2,
callbacks=callbacks)
model.save('char_s2s_train.h5')
encoder_model.save('char_s2s_encoder.h5')
decoder_model.save('char_s2s_decoder.h5')
您会注意到,我以前没有像通常那样定义验证或测试集。 这次,按照博客文章中给出的示例,我将让 Keras 随机选择 20% 的数据作为验证,这在示例中可以很好地工作。 如果要使用此代码实际进行机器翻译,请使用单独的测试集。
训练模型适合后,我将保存所有三个模型,并将它们再次加载到为推理而构建的单独程序中。 我这样做是为了使代码保持简洁,因为推理代码本身非常复杂。
让我们来看看这个模型的 100 个周期的模型训练:

如您所见,我们在第 20 个周期开始过拟合。虽然损失持续减少,但val_loss却在增加。 在这种情况下,模型检查指向可能无法正常工作,因为在训练结束之前我们不会序列化推理模型。 因此,理想情况下,我们应该再训练一次,将训练的周期数设置为略大于 TensorBoard 中观察到的最小值。
推理
现在我们有了训练有素的模型,我们将实际生成一些翻译。
总体而言,推理步骤如下:
- 加载数据并再次向量化(我们需要字符到索引的映射以及一些转换进行测试)
- 使用字符对字典进行索引,我们将创建字符字典的反向索引,因此一旦我们预测了正确的字符,我们就可以从数字返回到字符
- 选择一些输入序列进行翻译,然后通过编码器运行,获取状态
- 将状态和
<SOS>字符'\t'发送到解码器。 - 循环,获取每个下一个字符,直到解码器生成
<EOS>或'\n'
加载数据
我们可以从训练脚本中导入load_data和one_hot_vectorize函数,以相同的方式调用这些方法,如以下代码所示:
data = load_data()
data = one_hot_vectorize(data)
创建反向索引
解码器将预测正确字符的索引,该索引将是解码器的softmax输出的argmax。 我们将需要能够将索引映射到字符。 您可能还记得,数据字典中已经有一个字符到索引的映射,所以我们只需要反转它即可。 逆转字典非常简单,如下所示:
def create_reverse_indicies(data):
data['reverse_target_char_index'] = dict(
(i, char) for char, i in data["target_token_index"].items())
return data
然后,我们可以如下调用此函数:
data = create_reverse_indicies(data)
载入模型
我们可以使用keras.models.load_model加载保存在训练脚本中的模型。 我创建了此助手来完成该任务。 我们将使用以下代码加载模型:
def load_models():
model = load_model('char_s2s.h5')
encoder_model = load_model('char_s2s_encoder.h5')
decoder_model = load_model('char_s2s_decoder.h5')
return [model, encoder_model, decoder_model]
我们可以调用以下函数来加载所有三个模型:
model, encoder_model, decoder_model = load_models()
翻译序列
现在,我们准备对一些输入序列进行采样并进行翻译。 在示例代码中,我们使用前 100 个双语对进行翻译。 一个更好的测试可能是在整个空间中随机抽样,但是我认为这个简单的循环说明了这一过程:
for seq_index in range(100):
input_seq = data["encoder_input_data"][seq_index: seq_index + 1]
decoded_sentence = decode_sequence(input_seq, data, encoder_model,
decoder_model)
print('-')
print('Input sentence:', data['input_texts'][seq_index])
print('Correct Translation:', data['target_texts']
[seq_index].strip("\t\n"))
print('Decoded sentence:', decoded_sentence)
在这段代码中,我们将encoder_input_data的一个观察值用作decode_sequence的输入。 decode_sequence将传回解码器认为正确翻译的序列。 我们还需要将其传递给编码器和解码器模型,以便能够完成其工作。下面的翻译更加有趣,因为学习的短语未与
有了解码器预测后,就可以将其与输入和正确的转换进行比较。
当然,我们还没有完成,因为我们还没有探讨decode_sequence方法的工作方式。 接下来。
解码序列
解码器需要执行以下两项操作:
- 来自编码器的状态。
- 输入信号开始预测的翻译。 我们将在一个热向量中向其发送
'\t',因为这是我们的<SOS>字符。
为了获得编码器状态,我们只需要使用以下代码将要翻译的短语的向量化版本发送到编码器:
states_value = encoder_model.predict(input_seq)
为了启动解码器,我们还需要一个包含<SOS>字符的热向量。 这段代码将我们带到了那里:
target_seq = np.zeros((1, 1, data['num_decoder_tokens']))
target_seq[0, 0, data['target_token_index']['\t']] = 1.
现在,我们准备使用以下代码设置一个解码器循环,该循环将生成我们的翻译短语:
stop_condition = False
decoded_sentence = ''
while not stop_condition:
output_tokens, h, c = decoder_model.predict(
[target_seq] + states_value)
sampled_token_index = np.argmax(output_tokens[0, -1, :])
sampled_char = data["reverse_target_char_index"][sampled_token_index]
decoded_sentence += sampled_char
if (sampled_char == '\n' or
len(decoded_sentence) > data['max_decoder_seq_length']):
stop_condition = True
target_seq = np.zeros((1, 1, data['num_decoder_tokens']))
target_seq[0, 0, sampled_token_index] = 1.
states_value = [h, c]
首先要注意的是,我们一直循环到stop_condition = True。 这在解码器生成'\n'时发生。
第一次通过循环,我使用<SOS>向量和我们在循环外部创建的编码器的状态调用了decoder_model的预测方法。
当然,output_tokens将包含解码器可以预测的每个字符的softmax预测。 通过取output_tokens的argmax,我们将获得最大softmax值的索引。 方便地,我可以使用之前创建的reverse_target_char_index将其转换回关联的字符,这是一个在索引和字符之间转换的字典。
接下来,我们将该字符附加到decode_sequence字符串。
接下来,我们可以检查该字符是否为'\n'并触发stop_condition为True。
最后,我们将创建一个新的target_seq,其中包含解码器生成的最后一个字符,以及一个包含解码器隐藏状态的列表。 现在,我们准备再次重复循环。
我们的解码器将遵循此过程,直到生成解码序列为止。
翻译示例
只是为了好玩,我在这里提供了一些尝试的翻译。 所有这些都来自训练集的前面,这意味着我正在对training数据集进行预测,因此这些转换可能会使模型看起来比实际更好。
我们的第一版翻译使您对我们的期望有所了解,并且该网络做得很好:
输入句子:Help!
正确翻译:À l'aide!
解码后的句子:À l'aide!
后续的翻译更加有趣,因为学习的短语未与任何训练短语相关联。 短语Vas-tu immédiatement!转换为类似You go immediately的字词。这非常相似,甚至可能正确:
输入句子:Go on.
正确的翻译: Poursuis.
解码后的句子: Vas-tu immédiatement!
输入句子:Go on.
正确的翻译:Continuez.
解码后的句子: Vas-tu immédiatement!
输入句子:Go on.
正确的翻译: Poursuivez.
解码后的句子: Vas-tu immédiatement!
当然,有很多方法可以说相同的事情,这使得网络变得更加困难:
输入句子:Come on!
正确的翻译: Allez !
解码后的句子: Allez !
输入句子:Come on!
正确的翻译: Allez !
解码后的句子: Allez !
输入句子:Come on.
正确的翻译:Viens!
解码后的句子: Allez!
输入句子:Come on.
正确的翻译:Venez!
解码后的句子: Allez!
总结
在本章中,我们介绍了序列到序列模型的基础知识,包括它们如何工作以及如何使用它们。 希望我们已经向您展示了一个功能强大的工具,可用于机器翻译,问题解答和聊天应用。
如果您已经做到了,那就好。 您已经看到了很多深度学习的应用,并且发现自己正处于深层神经网络应用的最先进的钟形曲线的右边。
在下一章中,我将向您展示另一个高级主题的示例,即深度强化学习或深度 Q 学习,并向您展示如何实现自己的深度 Q 网络。
在此之前,请放松!
十二、深度强化学习
在本章中,我们将以略有不同的方式使用深度神经网络。 我们将要构建一个智能体,而不是预测一个类的成员,估计一个值,甚至生成一个序列。 尽管机器学习和人工智能这两个术语经常互换使用,但在本章中,我们将讨论人工智能作为一种可以感知其环境的智能体,并采取步骤在该环境中实现某些目标。
想象一个可以玩象棋或围棋之类策略游戏的特工。 构建神经网络来解决此类游戏的一种非常幼稚的方法可能是使用一种网络架构,在该架构中,我们对每个可能的棋盘/棋子组合进行热编码,然后预测每个可能的下一个动作。 尽管该网络庞大而复杂,但可能做得并不好。 要很好地玩国际象棋,您不仅要考虑下一步,而且还要考虑接下来的步伐。 在不确定的情况下,我们的智能体将需要考虑给定未来行动的最佳下一步行动。
这是一个令人兴奋的领域。 正是在智能体领域,研究人员才朝着人工智能或强大的 AI 迈进,这是创建可以执行人类任何智力任务的智能体的崇高目标。 强 AI 的概念通常与弱 AI 形成对比,弱 AI 是解决某些单个任务或应用的能力。
对于作者(我)和读者(您)而言,本章将是一个挑战,因为强化学习理应拥有自己的书,并且需要总结在数学,心理学和计算机科学方面所做的工作。 因此,请原谅快速参考处理,并知道我在为您提供足够的信息,而在接下来的部分中将不多说。
强化学习,马尔可夫决策过程和 Q 学习是智能体的基础,我们接下来将讨论这些内容。
我们将在本章中讨论以下主题:
- 强化学习概述
- Keras 强化学习框架
- 在 Keras 中建立强化学习智能体
强化学习概述
强化学习基于智能体的概念。 智能体通过观察某种状态然后采取行动来与其环境进行交互。 当智能体采取行动在状态之间移动时,它会以奖励信号的形式接收有关其行动良好性的反馈。 这个奖励信号是强化学习中的强化。 这是一个反馈循环,智能体可以使用它来学习其选择的优势。 当然,奖励可以是正面的,也可以是负面的(惩罚)。
想象一下,无人驾驶汽车是我们正在制造的智能体。 在行驶过程中,它不断收到动作的奖励信号。 留在车道内可能会产生积极的报酬,而在行人上奔跑可能会给智能体带来非常消极的报酬。 当面临选择留在行人或撞到行人的选择时,智能体将希望学会以避开行人为代价,避开行人,损失车道线奖励,以避免更大的行人碰撞惩罚。
强化学习概念的核心是状态,行为和奖励的概念。 我已经讨论过奖励,所以让我们谈谈行动和状态。 动作是智能体在观察到某种状态时可以执行的操作。 如果我们的特工正在玩一个简单的棋盘游戏,那么该动作将由该特工轮到它来做。 然后转弯就是座席的状态。 为了解决这些问题,我们将在这里着眼于一个智能体可以采取的行动始终是有限的和离散的。 下图说明了此概念:

此反馈循环的一个步骤可以用数学方式表示为:

动作会在原始状态s和下一个状态s'的智能体之间进行转换,智能体会在其中获得一些奖励r。 智能体选择动作的方式称为智能体策略,通常称为pi。
强化学习的目的是找到一系列动作,使行动者从一个状态到另一个状态,并获得尽可能多的报酬。
马尔可夫决策过程
我们构筑的这个世界恰好是马尔可夫决策过程(MDP),它具有以下属性:
- 它具有一组有限的状态,
S - 它具有一组有限的动作
A P[a](s, s')是采取行动A将在状态s和状态s'之间转换的概率R[a](s, s')是s和s'之间过渡的直接奖励。γ ∈ [0, 1]是折扣因子,这是我们相对于当前奖励对未来奖励的折扣程度(稍后会详细介绍)
一旦我们有了确定每个状态要采取的操作的策略函数pi,MDP 就解决了,成为了马尔可夫链。
好消息是,有一个警告就完全有可能完美解决 MDP。 需要注意的是,必须知道 MDP 的所有回报和概率。 事实证明,这种警告相当重要,因为在大多数情况下,由于智能体的环境混乱或至少不确定,因此智能体不知道所有的回报和状态更改概率。
Q 学习
想象一下,我们有一些函数Q,可以估计出采取行动的回报:

对于某些状态s以及动作a,它会根据状态为该动作生成奖励。 如果我们知道环境带来的所有回报,那么我们就可以遍历Q并选择能够为我们带来最大回报的行动。 但是,正如我们在上一节中提到的那样,我们的智能体不知道所有的奖励状态和状态概率。 因此,我们的Q函数需要尝试近似奖励。
我们可以使用称为 Bellman 公式的递归定义的Q函数来近似此理想的Q函数:

在这种情况下, r₀是下一个动作的奖励,然后在下一个动作上(递归地)递归使用Q函数确定该行动的未来奖励。 为此,我们将γ作为相对于当前奖励的未来奖励的折扣。 只要伽玛小于 1,它就不会使我们的奖励序列变得无限大。 更明显地,与当前状态下的相同奖励相比,未来状态下的奖励的值要低。 具体来说,如果有人今天给您 100 美元,明天给您 100 美元,您应该立即拿走这笔钱,因为明天不确定。
如果我们尽最大的努力让我们的智能体经历每种可能的状态转换,并使用此函数来估计我们的报酬,我们将得出我们试图近似的理想Q函数。
无限状态空间
对Q函数的讨论使我们陷入了传统强化学习的重要局限。 您可能还记得,它假设状态空间是有限且离散的。 不幸的是,这不是我们生活的世界,也不是我们的智能体在很多时候会发现自己的环境。 考虑一个可以打乒乓球的经纪人。 状态空间的重要组成部分是乒乓球的速度,它当然不是离散的。 像我们不久将要看到的那样,可以看到的特工会看到一个图像,该图像是一个很大的连续空间。
我们讨论的 Bellman 方程将要求我们在状态与状态之间转移时保持经验奖励的大矩阵。 但是,当面对连续的状态空间时,这是不可能的。 可能的状态本质上是无限的,我们不能创建无限大小的矩阵。
幸运的是,我们可以使用深度神经网络来近似Q函数。 这可能不会让您感到惊讶,因为您正在阅读一本深度学习书,因此您可能猜测深度学习必须在某个地方出现。 就是那个地方
深度 Q 网络
深层 Q 网络(DQN)是近似Q函数的神经网络。 他们将状态映射到动作,并学会估计每个动作的Q值,如下图所示:

我们可以使用深度神经网络作为函数来逼近该矩阵,而不是尝试存储一个无限大的矩阵,而是将奖励从连续状态空间映射到动作。 这样,我们可以将神经网络用作智能体的大脑。 但这一切都导致我们提出一个非常有趣的问题。 我们如何训练这个网络?
在线学习
当我们的智能体通过采取行动从一个状态过渡到另一个状态时,它会得到奖励。 智能体可以通过使用每个状态,动作和奖励作为训练输入来在线学习。 在执行每个操作后,该智能体将更新其神经网络权重,并希望在此过程中变得更聪明。 这是在线学习的基本思想。 智能体就像您和我一样,不断学习。
这种朴素的在线学习的缺点有些明显,有两个方面:
- 经历之后,我们就会放弃经验。
- 我们所经历的经验彼此高度相关,我们将过度适应最新的经验。 有趣的是,这也是人类遭受的苦难,称为可用性偏差。
我们可以通过使用内存和经验重放来解决这些问题。
记忆和经验重放
当我们引入有限存储空间的概念时,可以找到针对这两个问题的巧妙解决方案,该存储空间用于存储智能体具有的一组经验。 在每个状态下,我们都可以借此机会记住状态,行动和奖励。 然后,智能体可以通过从内存中采样一个随机小批量并使用该小批量更新 DQN 权重,定期重放这些经验。
这种重放机制使智能体能够以一般的方式从更长远的经验中学习,因为它是从内存中的那些经验中随机采样的,而不是仅使用最近的经验来更新整个网络。
利用与探索
通常,我们希望智能体遵循贪婪策略,这意味着我们希望智能体采取具有最大Q值的操作。 在学习网络的同时,我们不希望它总是贪婪地表现。 如果这样做,它将永远不会探索新的选择,也不会学习新的东西。 因此,我们需要我们的智能体偶尔执行不符合规定的策略。
平衡这种探索的最佳方法是一个持续不断的研究主题,并且已经使用了很长时间。 但是,我们将使用的方法非常简单。 智能体每次执行操作时,我们都会生成一个随机数。 如果该数字等于或小于某个阈值ε,则智能体将采取随机措施。 这称为 ε 贪婪策略。
智能体第一次启动时,对世界了解不多,应该探索更多。 随着智能体变得越来越聪明,它可能应该减少探索并更多地使用其对环境的了解。 为此,我们只需要在训练时逐渐降低ε。 在我们的示例中,我们将每转降低ε的衰减率,以使它随每个动作线性减小。
综上所述,我们有一个线性退火 ε - 贪心 Q 策略,说起来既简单又有趣。
DeepMind
至少没有提到 Mnih 等人的论文《和深度强化学习一起玩 Atari》,就不会完成关于强化学习的讨论。 然后是 DeepMind,现在是 Google。 在这篇具有里程碑意义的论文中,作者使用了卷积神经网络来训练深度 Q 网络来玩 Atari 2600 游戏。 他们从 Atari 2600 游戏中获取原始像素输出,将其缩小一点,将其转换为灰度,然后将其用作网络的状态空间输入。 为了使计算机了解屏幕上对象的速度和方向,他们使用了四个图像缓冲区作为深度 Q 网络的输入。
作者能够创建一个智能体,该智能体能够使用完全相同的神经网络架构玩 7 个 Atari 2600 游戏,并且在其中三个游戏上,该智能体要比人类更好。 后来又扩大到 49 场比赛,其中大多数比赛都比人类出色。 本文是迈向通用 AI 的非常重要的一步,它实际上是目前在强化学习中开展的许多研究的基础。
Keras 强化学习框架
在这一点上,我们应该有足够的背景知识来开始建立深层的 Q 网络,但是仍然需要克服很大的障碍。
实现利用深度强化学习的智能体可能是一个很大的挑战,但是最初由 Matthias Plappert 编写的 Keras-RL 库使其变得更加容易。 我将使用他的库来为本章介绍的智能体提供支持。
当然,如果没有环境,我们的经纪人将不会有太多的乐趣。 我将使用 OpenAI 体育馆,该体育馆提供许多环境,包括状态和奖励函数,我们可以轻松地使用它们来构建供智能体探索的世界。
安装 Keras-RL
Keras-RL 可以通过 PIP 安装。 但是,我建议从项目 GitHub 存储库中安装它,因为代码可能会更新一些。 为此,只需克隆存储库并按以下方式运行python setup.py install:
git clone https://github.com/matthiasplappert/keras-rl.git
cd keras-rl
python setup.py install
安装 OpenAI Gym
OpenAI 体育场可作为点子安装。 我将使用他们的Box2D和atari环境中的示例。 您可以使用以下代码安装它们:
pip install gym
pip install gym[atari]
pip install gym[Box2D]
使用 OpenAI Gym
使用 OpenAI 体育场确实使深度强化学习变得容易。 Keras-RL 将完成大部分艰苦的工作,但是我认为值得单独走遍体育馆,这样您才能了解智能体如何与环境互动。
环境是可以实例化的对象。 例如,要创建CartPole-v0环境,我们只需要导入体育场并创建环境,如以下代码所示:
import gym
env = gym.make("CartPole-v0")
现在,如果我们的智能体想要在那种环境中行动,它只需要发送一个action并返回一个状态和一个reward,如下所示:
next_state, reward, done, info = env.step(action)
该智能体可以通过使用循环与环境进行交互来播放整个剧集。 此循环的每次迭代都对应剧集中的单个步骤。 当智能体从环境接收到“完成”信号时,剧集结束。
在 Keras 中建立强化学习智能体
好消息,我们终于可以开始编码了。 在本部分中,我将演示两种名为 CartPole 和 Lunar Lander 的 Keras-RL 智能体。 我选择这些示例是因为它们不会消耗您的 GPU 和云预算来运行。 它们可以很容易地扩展到 Atari 问题,我在本书的 Git 存储库中也包括了其中之一。 您可以照常在Chapter12文件夹中找到所有这些代码。 让我们快速讨论一下这两种环境:
- CartPole:CartPole 环境由平衡在推车上的杆组成。 智能体必须学习如何在立柱下方的推车移动时垂直平衡立柱。 给智能体指定了推车的位置,推车的速度,杆的角度和杆的旋转速度作为输入。 智能体可以在推车的任一侧施加力。 如果电线杆与垂直线的夹角下降超过 15 度,我们的经纪人就此告吹。
- Lunar Lander:Lunar Lander 的环境更具挑战性。 特工必须将月球着陆器降落在着陆垫上。 月亮的表面会发生变化,着陆器的方位也会在每个剧集发生变化。 该智能体将获得一个八维数组,用于描述每个步骤中的世界状态,并且可以在该步骤中执行四个操作之一。 智能体可以选择不执行任何操作,启动其主引擎,启动其左向引擎或启动其右向引擎。
CartPole
CartPole 智能体将使用一个相当适度的神经网络,即使没有 GPU,您也应该能够相当迅速地进行训练。 我们将一如既往地从模型架构开始。 然后,我们将定义网络的内存,探索策略,最后训练智能体。
CartPole 神经网络架构
三个具有 16 个神经元的隐藏层实际上可能足以解决这个简单的问题。 这个模型非常类似于我们在本书开始时使用的一些基本模型。 我们将使用以下代码来定义模型:
def build_model(state_size, num_actions):
input = Input(shape=(1,state_size))
x = Flatten()(input)
x = Dense(16, activation='relu')(x)
x = Dense(16, activation='relu')(x)
x = Dense(16, activation='relu')(x)
output = Dense(num_actions, activation='linear')(x)
model = Model(inputs=input, outputs=output)
print(model.summary())
return model
输入将是一个1 x 状态空间向量,每个可能的动作都有一个输出神经元,它将预测每个步骤该动作的Q值。 通过获取输出的argmax,我们可以选择Q值最高的动作,但是我们不必自己做,因为 Keras-RL 会为我们做。
记忆
Keras-RL 为我们提供了一个名为rl.memory.SequentialMemory的类,该类提供了快速有效的数据结构,我们可以将智能体的经验存储在以下位置:
memory = SequentialMemory(limit=50000, window_length=1)
我们需要为此存储对象指定一个最大大小,它是一个超参数。 随着新的经验添加到该内存中并变得完整,旧的经验会被遗忘。
策略
Keras-RL 提供了一个称为rl.policy.EpsGreedyQPolicy的 ε-贪婪 Q 策略,我们可以用来平衡利用与探索。 当智能体程序向世界前进时,我们可以使用rl.policy.LinearAnnealedPolicy来衰减ε,如以下代码所示:
policy = LinearAnnealedPolicy(EpsGreedyQPolicy(), attr='eps', value_max=1., value_min=.1, value_test=.05, nb_steps=10000)
在这里我们要说的是,我们要从ε的值 1 开始,并且不小于 0.1,同时测试我们的随机数是否小于 0.05。 我们将步数设置为 .1 到 10,000 之间,Keras-RL 为我们处理衰减数学。
智能体
定义了模型,内存和策略后,我们现在就可以创建一个深度 Q 网络智能体,并将这些对象发送给该智能体。 Keras RL 提供了一个称为rl.agents.dqn.DQNAgent的智能体类,我们可以为此使用它,如以下代码所示:
dqn = DQNAgent(model=model, nb_actions=num_actions, memory=memory, nb_steps_warmup=10,
target_model_update=1e-2, policy=policy)
dqn.compile(Adam(lr=1e-3), metrics=['mae'])
此时,其中两个参数target_model_update和nb_steps_warmup可能还不熟悉:
nb_steps_warmup:确定我们开始进行经验重放之前需要等待的时间,如果您还记得的话,这是我们实际上开始训练网络的时间。 这使我们积累了足够的经验来构建适当的小批量生产。 如果您为此参数选择的值小于批量大小,则 Keras RL 将抽样替换。target_model_update:Q函数是递归的,当智能体更新它的网络以获取Q(s, a)时,更新也影响其对Q(s', a)所做的预测。 这会导致网络非常不稳定。 大多数深度 Q 网络实现解决此限制的方法是使用目标网络,该目标网络是未经训练的深度 Q 网络的副本,而经常被新副本替换。target_model_update参数控制这种情况发生的频率。
训练
Keras RL 提供了多个类似 Keras 的回调,可以方便地进行模型检查指向和记录。 我将在下面使用这两个回调。 如果您想查看 Keras-RL 提供的更多回调,可以在以下位置找到它们。 您还可以找到可用于创建自己的 Keras-RL 回调的回调类。
我们将使用以下代码来训练我们的模型:
def build_callbacks(env_name):
checkpoint_weights_filename = 'dqn_' + env_name + '_weights_{step}.h5f'
log_filename = 'dqn_{}_log.json'.format(env_name)
callbacks = [ModelIntervalCheckpoint(checkpoint_weights_filename, interval=5000)]
callbacks += [FileLogger(log_filename, interval=100)]
return callbacks
callbacks = build_callbacks(ENV_NAME)
dqn.fit(env, nb_steps=50000,
visualize=False,
verbose=2,
callbacks=callbacks)
一旦构建了智能体的回调,我们就可以使用.fit()方法来拟合DQNAgent,就像使用 Keras 模型一样。 在此示例中,请注意visualize参数。 如果将visualize设置为True,我们将能够观察智能体与环境的交互。 但是,这大大减慢了训练的速度。
结果
在前 250 个剧集之后,我们将看到剧集的总奖励接近 200,剧集步骤的总奖励也接近 200。这意味着智能体已学会平衡购物车上的杆位,直到环境结束最多 200 个步骤 。
观看我们的成功当然很有趣,因此我们可以使用DQNAgent .test()方法评估某些剧集。 以下代码用于定义此方法:
dqn.test(env, nb_episodes=5, visualize=True)
在这里,我们设置了visualize=True,以便我们可以看到我们的智能体平衡杆位,如下图所示:

我们走了,那是一根平衡杆! 好吧,我知道,我承认平衡手推车上的电线杆并不是那么酷,所以让我们再做一个轻量级的例子。 在此示例中,我们将把月球着陆器降落在月球上,希望它将给您留下深刻的印象。
Lunar Lander
感谢 Keras-RL,我们用于 Lunar Lander 的智能体几乎与 CartPole 相同,除了实际的模型架构和一些超参数更改外。 Lunar Lander 的环境有八个输入而不是四个输入,我们的智能体现在可以选择四个操作而不是两个。
如果您受到这些示例的启发,并决定尝试构建 Keras-RL 网络,请记住,超参数选择非常非常重要。 对于 Lunar Lander 智能体,对模型架构的最小更改导致我的智能体无法学习环境解决方案。 使网络正确运行是一项艰巨的工作。
Lunar Lander 网络架构
我的 Lunar Lander 智能体程序的架构仅比 CartPole 的架构稍微复杂一点,对于相同的三个隐藏层仅引入了几个神经元。 我们将使用以下代码来定义模型:
def build_model(state_size, num_actions):
input = Input(shape=(1, state_size))
x = Flatten()(input)
x = Dense(64, activation='relu')(x)
x = Dense(32, activation='relu')(x)
x = Dense(16, activation='relu')(x)
output = Dense(num_actions, activation='linear')(x)
model = Model(inputs=input, outputs=output)
print(model.summary())
return model
在此问题的情况下,较小的架构会导致智能体学习控制和悬停着陆器,但实际上并未着陆。 当然,由于我们要对每个剧集的每个步骤进行小批量更新,因此我们需要仔细权衡复杂性与运行时和计算需求之间的关系。
记忆和策略
CartPole 的内存和策略可以重复使用。 我相信,通过进一步调整线性退火策略中的步骤,可能会提高智能体训练的速度,因为该智能体需要采取更多的步骤来进行训练。 但是,为 CartPole 选择的值似乎可以很好地工作,因此这是留给读者的练习。
智能体
从以下代码中可以看出,Lunar Lander DQNAgent再次相同,只是学习率小得多。
dqn = DQNAgent(model=model, nb_actions=num_actions, memory=memory, nb_steps_warmup=10, target_model_update=1e-2, policy=policy)
dqn.compile(Adam(lr=0.00025), metrics=['mae'])
训练
在训练该特工时,您会注意到它学会做的第一件事是将着陆器悬停,并避免着陆。 当着陆器最终着陆时,它会收到非常高的奖励,成功着陆时为 +100,坠毁时为 -100。 这种 -100 的奖励是如此之强,以至于智能体一开始宁愿因悬停而受到小额罚款。 我们的探员要花很多时间才能得出这样的提示:良好的着陆总比没有良好着陆好,因为坠机着陆非常糟糕。
可以塑造奖励信号来帮助座席更快地学习,但这超出了本书的范围。 有关更多信息,请查看奖励塑造。
由于这种对坠机着陆的极端负面反馈,网络需要花费相当长的一段时间才能学会着陆。 在这里,我们正在运行五十万个训练步骤,以传达我们的信息。 我们将使用以下代码来训练智能体:
callbacks = build_callbacks(ENV_NAME)
dqn.fit(env, nb_steps=1000000,
visualize=False,
verbose=2,
callbacks=callbacks)
您可以通过调整参数gamma(默认值为 0.99)来进一步改进此示例。 如果您从Q函数中调用,此参数会减少或增加Q函数中将来奖励的影响。
结果
我在 Git 一章中包含了 Lunar Lander 的权重,并创建了一个脚本,该脚本在启用可视化的情况下运行这些权重dqn_lunar_lander_test.py。 它加载经过训练的模型权重并运行 10 集。 在大多数情况下,特工能够以惊人的技能和准确率将月球着陆器降落在其着陆板上,如以下屏幕截图所示:

希望这个例子可以说明,尽管深层 Q 网络并不是火箭科学,但仍可用于控制火箭。
总结
斯坦福大学只教授强化学习的整个课程。 可能只写了一本关于强化学习的书,实际上已经做了很多次。 我希望本章能够向您展示足够的知识,让您开始解决强化学习问题。
当我解决“月球着陆器”问题时,很容易让我的头脑从玩具问题到利用深层 Q 网络驱动的特工进行实际太空探索而徘徊。 我希望本章为您做同样的事情。
在下一章中,我将向您展示深度神经网络的最后一种用法,我们将研究可以生成新图像,数据点甚至音乐的网络,称为生成对抗网络。
十三、生成对抗网络
尽管我在本书中花了很多时间谈论分类或估计的网络,但在本章中,我将向您展示一些具有创建能力的深度神经网络。 生成对抗网络(GAN)通过两个内部深层网络之间的内部竞争来学习如何做到这一点,我们将在下面讨论。 在深度卷积生成对抗网络(DCGAN)的情况下,这是我将在本章中重点介绍的 GAN 类型,该网络将学习创建类似于训练数据集的图像。
我们将在本章介绍以下主题:
- GAN 概述
- 深度卷积 GAN 架构
- GAN 如何失败
- GAN 的安全选择
- 使用 Keras GAN 生成 MNIST 图像
- 使用 Keras GAN 生成 CIFAR-10 图像
GAN 概述
生成对抗网络都是关于生成新内容的。 GAN 能够学习一些分布并从该分布创建新样本。 该样本可能只是我们训练数据中未出现的直线上的新点,但也可能是非常复杂的数据集中的新点。 GAN 已用于生成新的音乐,声音和图像。 根据 Yann LeCun 所说,《对抗训练是切片以来最酷的事情》。 我不确定切片面包是否特别酷,但是 Yann LeCun 是一个非常酷的家伙,所以我会信守诺言。 无论如何,GAN 都非常受欢迎,虽然它可能不如我们在业务环境中涵盖的其他一些主题那么实用,但在我们对深度学习技术的调查中值得考虑。
2014 年,伊恩·古德费洛(Ian Goodfellow)等人。 撰写了一篇名为生成对抗网络的论文,提出了使用两个深度网络进行对抗训练的框架,每个都尝试打败对方。 该框架由两个独立的网络组成:判别器和生成器。
判别器正在查看来自训练集的真实数据和来自生成器的假数据。 它的工作是将每一个作为传入数据实例分类为真实还是伪造。
生成器试图使判别器误以为所生成的数据是真实的。
生成器和判别器被锁定在一个游戏中,它们各自试图超越彼此。 这种竞争驱使每个网络不断改进,直到最终判别器将生成器的输出与训练集中的数据区分开。 当生成器和判别器都正确配置时,它们将达到纳什均衡,在纳什均衡中,两者都无法找到优势。
深度卷积 GAN 架构
关于 GAN 的论文很多,每篇都提出了新的新颖架构和调整。 但是,它们中的大多数至少在某种程度上基于深度卷积 GAN(DCGAN)。 在本章的其余部分中,我们将重点介绍这种模型,因为当您采用此处未介绍的新的令人兴奋的 GAN 架构(例如条件 GAN(CGAN),Stack GAN,InfoGAN 或 Wasserstein GAN),或者可能还有一些其他的新变种,您可能会选择接下来看看。
DCGAN 由 Alex Radford,Luke Metz 和 Soumith Chintala 在论文《深度卷积生成对抗网络》中提出。
接下来让我们看一下 DCGAN 的总体架构。
对抗训练架构
GAN 的整体架构如下图所示。 生成器和判别器分别是单独的深度神经网络,为了易于使用,将它们简化为黑匣子。 我们将很快介绍它们的各个架构,但首先,我想着重介绍它们的交互方式:

给生成器一个随机噪声向量(z),并创建一个输出G(z)(对于 DCGAN,这是一个图像),希望它能欺骗判别器。
判别器既得到实际训练数据(X),又得到生成器输出G(z)。 要做的是确定其输入实际上是真实的概率P(X)。
判别器和生成器都在栈中一起训练。 随着一个方面的改进,另一个方面也有所改进,直到希望生成器产生如此好的输出,从而使判别器不再能够识别该输出与训练数据之间的差异。
当然,在您准备好构建自己的 GAN 之前,我们还要介绍更多细节。 接下来,让我们更深入地研究生成器。
生成器架构
在此示例中,我们使用适合于生成28 x 28灰度图像的层大小,这正是我们稍后在 MNIST 示例中将要执行的操作。 如果您以前没有使用过生成器,那么生成器的算法可能会有些棘手,因此我们将在遍历每一层时进行介绍。 下图显示了架构:

生成器的输入只是100 x 1的随机向量,我们将其称为噪声向量。 当此噪声向量是从正态分布生成时,GAN 往往工作得最好。
网络的第一层是密集的并且完全连接。 它为我们提供了一种建立线性代数的方法,以便最终得到正确的输出形状。 对于每个卷积块,我们最终将第一轴和第二轴(最终将成为图像的高度和宽度的行和列)加倍,而通道数逐渐缩小到 1。我们最终需要输出的高度和宽度为 28。因此,我们将需要从7 x 7 x 128张量开始,以便它可以移动到14 x 14,然后最终是28 x 28。 为此,我们将密集层的大小设置为128 x 7 x 7神经元或 6,272 单元。 这使我们可以将密集层的输出重塑为7 x 7 x 128。 如果现在看来这还不算什么,请不用担心,在编写代码后,这才有意义。
在完全连接的层之后,事情变得更加简单。 就像我们一直一样,我们正在使用卷积层。 但是,这次我们反向使用它们。 我们不再使用最大池来缩减样本量。 取而代之的是,我们进行上采样,在学习视觉特征时使用卷积来构建我们的网络,并最终输出适当形状的张量。
通常,生成器中最后一层的激活是双曲正切,并且训练图像矩阵中的元素被归一化为 -1 和 1 之间。这是我将在整章中提到的众多 GAN 黑魔法之一。 研究人员已经发现了一些经验证明可以帮助构建稳定的 GAN 的黑魔法,Soumith Chintala 可以在此 Git 上找到大多数黑客,而 Soumith Chintala 也是 DCGAN 原始论文的作者之一。 深度学习研究的世界无疑是一个很小的领域。
判别器架构
判别器的架构更像我们在前几章中已经看到的。 它实际上只是一个典型的图像分类器,如下图所示。 输出是 Sigmoid 的,因为判别器将预测输入图像是真实图像集的成员的概率。 判别器正在解决二分类问题:

现在,我们已经介绍了 DCGAN 的架构以及它的各个层次,下面让我们看一下如何训练框架。
DCGAN
DCGAN 框架是使用迷你批量来进行训练的,这与我之前在本书中对网络进行训练的方式相同。 但是,稍后在构建代码时,您会注意到我们正在构建一个训练循环,该循环明确控制每个更新批量的情况,而不仅仅是调用models.fit()方法并依靠 Keras 为我们处理它。 我这样做是因为 GAN 训练需要多个模型来更新同一批次中的权重,所以它比我们以前所做的单个参数更新要稍微复杂一些。
对 DCGAN 进行训练的过程分为两步,每批次进行一次。
步骤 1 – 训练判别器
批量训练 DCGAN 的第一步是在实际数据和生成的数据上训练判别器。 赋予真实数据的标签显然是1,而用于假数据的标签则是0。
步骤 2 – 训练栈
判别器更新权重后,我们将判别器和生成器一起训练为一个模型。 这样做时,我们将使判别器的权重不可训练,将其冻结在适当的位置,但仍允许判别器将梯度反向传播到生成器,以便生成器可以更新其权重。
对于训练过程中的这一步,我们将使用噪声向量作为输入,这将导致生成器生成图像。 判别器将显示该图像,并要求预测该图像是否真实。 下图说明了此过程:

判别器将提出一些预测,我们可以称之为y_hat。 此栈的loss函数将是二元交叉熵,并且我们将loss函数的标签传递为 1,我们可以考虑y。 如您在本书前面所提到的, y和y_hat之间的loss转换为梯度,然后通过判别器传给生成器。 这将更新生成器权重,使它可以从判别者对问题空间的了解中受益,以便它可以学习创建更逼真的生成图像。
然后重复这两个步骤,直到生成器能够创建与训练集中的数据相似的数据,使得判别器无法再将两个数据集区分开,这成为了一个猜谜游戏。 判别器。 此时,生成器将不再能够改进。 当我们找到纳什均衡时,就对网络进行了训练。
GAN 如何失败
至少可以说,训练 GAN 是一件棘手的事情。 训练 GAN 失败的方法有很多种。 实际上,在撰写本章时,我发现自己大大扩展了亵渎向量的词汇量,同时还花了一点时间在云 GPU 上! 在本章稍后向您展示两个可用的 GAN 之前,让我们考虑可能发生的故障以及如何修复这些问题。
稳定性
训练 GAN 需要在判别器和生成器之间进行仔细的平衡。 判别器和生成器都在争夺深度网络优势。 另一方面,他们也需要彼此学习和成长。 为了使它起作用,任何一个都不能压倒另一个。
在不稳定的 GAN 中,判别器可能会使生成器过载,并绝对确定生成器是假的。 损失为零,并且没有可用于发送到生成器的梯度,因此它不再可以改善。 网络游戏结束。 解决此问题的最佳方法是降低判别器的学习率。 您也可以尝试减少整个判别器架构中神经元的数量。 但是,您可能会在训练过程的后期错过这些神经元。 最终,调整网络架构和超参数是避免这种情况的最佳方法。
当然,这可能是相反的方式,如模式崩溃的情况。
模式崩溃
模式崩溃是 GAN 失败的类似且相关的方式。 在模式崩溃中,生成器在多模式分布中学习一种模式,并选择始终使用该方法来利用判别器。 如果您的训练集中有鱼和小猫,并且您的生成器仅生成奇怪的小猫而没有鱼,则您经历了模式崩溃。 在这种情况下,增加判别器的威力可能会有所帮助。
GAN 的安全选择
我之前已经提到过 Soumith Chintala 的 GAN 黑魔法 Git,当您试图使 GAN 稳定时,这是一个很好的起点。 既然我们已经讨论了训练稳定的 GAN 会有多么困难,让我们来谈谈一些安全的选择,这些选择可能会帮助您成功找到自己的地方。 尽管有很多技巧,但以下是本章中尚未涵盖的我的主要建议:
- 批量规范:使用批量规范化时,请为真实数据和伪数据构造不同的微型批量,并分别进行更新。
- 泄漏的 ReLU:泄漏的 ReLU 是 ReLU 激活函数的变异。 回想一下 ReLU 函数是
f(x) = max(0, x)。
但是,泄漏的 ReLU 可以表示为:

当设备不工作时,泄漏的 ReLU 允许非常小的非零梯度。 这可以消除消失的梯度,当我们像在判别器和生成器的组合中那样将多个层堆叠在一起时,这总是一个问题。
- 在生成器中使用丢弃:这将产生噪声并防止模式崩溃。
- 使用软标签:对于真实示例,请使用介于 0.7 和 1 之间的标签,对于伪示例,请使用介于 0 和 0.3 之间的标签。 这种噪声有助于保持信息从判别器流向生成器。
在本章的其他地方,我们还将介绍许多其他的 GAN 黑魔法。 但是,我认为在成功实现 GAN 时,这几项技巧是最重要的。
使用 Keras GAN 生成 MNIST 图像
我们之前曾与 MNIST 合作,但是这次我们将使用 GAN 生成新的 MNIST 图像。 训练 GAN 可能需要很长时间。 但是,此问题很小,可以在几个小时内在大多数笔记本电脑上运行,这是一个很好的例子。 稍后,我们将把这个例子扩展到 CIFAR-10 图像。
我在这里使用的网络架构已被许多人发现并进行了优化,包括 DCGAN 论文的作者以及像 ErikLinder-Norén 这样的人,他是 GAN 实现的优秀集合,称为 Keras GAN 作为我在此处使用的代码的基础。 如果您想知道我是如何在这里使用的架构选择的,这些就是我试图站在肩膀上的巨人。
加载数据集
MNIST数据集由 60,000 个手绘数字(从 0 到 9)组成。Keras 为我们提供了一个内置加载程序,可将其分为 50,000 个训练图像和 10,000 个测试图像。 我们将使用以下代码加载数据集:
from keras.datasets import mnist
def load_data():
(X_train, _), (_, _) = mnist.load_data()
X_train = (X_train.astype(np.float32) - 127.5) / 127.5
X_train = np.expand_dims(X_train, axis=3)
return X_train
您可能已经注意到,我没有返回任何标签或测试数据集。 我将只使用训练数据集。 不需要标签,因为我要使用的唯一标签是0代表假货,1代表真货。 这些是真实的图像,因此将在判别器上将它们全部分配为标签 1。
创建生成器
生成器使用了一些新的层,我们将在本节中讨论这些层。 首先,有机会略读以下代码:
def build_generator(noise_shape=(100,)):
input = Input(noise_shape)
x = Dense(128 * 7 * 7, activation="relu")(input)
x = Reshape((7, 7, 128))(x)
x = BatchNormalization(momentum=0.8)(x)
x = UpSampling2D()(x)
x = Conv2D(128, kernel_size=3, padding="same")(x)
x = Activation("relu")(x)
x = BatchNormalization(momentum=0.8)(x)
x = UpSampling2D()(x)
x = Conv2D(64, kernel_size=3, padding="same")(x)
x = Activation("relu")(x)
x = BatchNormalization(momentum=0.8)(x)
x = Conv2D(1, kernel_size=3, padding="same")(x)
out = Activation("tanh")(x)
model = Model(input, out)
print("-- Generator -- ")
model.summary()
return model
我们以前没有使用过UpSampling2D层。 该层将增加输入张量的行和列,从而使通道保持不变。 它通过重复输入张量中的值来实现。 默认情况下,它将使输入加倍。 如果给UpSampling2D层一个7 x 7 x 128输入,它将给我们一个14 x 14 x 128输出。
通常,当我们构建一个 CNN 时,我们从一个非常高和宽的图像开始,并使用卷积层来获得一个非常深但又不高又不宽的张量。 在这里,我将相反。 我将使用一个密集层并进行重塑,以7 x 7 x 128张量开始,然后将其加倍两次后,剩下28 x 28张量。 由于我需要灰度图像,因此可以使用具有单个单元的卷积层来获得28 x 28 x 1输出。
这种生成器运算法则有点令人反感,乍一看似乎很尴尬,但是经过几个小时的痛苦之后,您就会掌握它了!
创建判别器
判别符实际上在很大程度上与我之前谈到的任何其他 CNN 相同。 当然,我们应该谈论一些新事物。 我们将使用以下代码来构建判别器:
def build_discriminator(img_shape):
input = Input(img_shape)
x =Conv2D(32, kernel_size=3, strides=2, padding="same")(input)
x = LeakyReLU(alpha=0.2)(x)
x = Dropout(0.25)(x)
x = Conv2D(64, kernel_size=3, strides=2, padding="same")(x)
x = ZeroPadding2D(padding=((0, 1), (0, 1)))(x)
x = (LeakyReLU(alpha=0.2))(x)
x = Dropout(0.25)(x)
x = BatchNormalization(momentum=0.8)(x)
x = Conv2D(128, kernel_size=3, strides=2, padding="same")(x)
x = LeakyReLU(alpha=0.2)(x)
x = Dropout(0.25)(x)
x = BatchNormalization(momentum=0.8)(x)
x = Conv2D(256, kernel_size=3, strides=1, padding="same")(x)
x = LeakyReLU(alpha=0.2)(x)
x = Dropout(0.25)(x)
x = Flatten()(x)
out = Dense(1, activation='sigmoid')(x)
model = Model(input, out)
print("-- Discriminator -- ")
model.summary()
return model
首先,您可能会注意到形状奇怪的zeroPadding2D()层。 第二次卷积后,我们的张量从28 x 28 x 3变为7 x 7 x 64。 这一层使我们回到偶数,在行和列的一侧都加零,这样我们的张量现在为8 x 8 x 64。
更不寻常的是同时使用批量规范化和丢弃法。 通常,这两层不能一起使用。 但是,就 GAN 而言,它们似乎确实使网络受益。
创建栈式模型
现在我们已经组装了generator和discriminator,我们需要组装第三个模型,这是两个模型的栈,在discriminator损失的情况下,我们可以用来训练生成器。
为此,我们可以创建一个新模型,这次使用以前的模型作为新模型中的层,如以下代码所示:
discriminator = build_discriminator(img_shape=(28, 28, 1))
generator = build_generator()
z = Input(shape=(100,))
img = generator(z)
discriminator.trainable = False
real = discriminator(img)
combined = Model(z, real)
注意,在建立模型之前,我们将判别器的训练属性设置为False。 这意味着对于该模型,在反向传播期间,我们将不会更新判别器的权重。 正如我们在“栈式训练”部分中提到的,我们将冻结这些权重,仅将生成器的权重与栈一起移动。 判别器将单独训练。
现在,所有模型都已构建,需要对其进行编译,如以下代码所示:
gen_optimizer = Adam(lr=0.0002, beta_1=0.5)
disc_optimizer = Adam(lr=0.0002, beta_1=0.5)
discriminator.compile(loss='binary_crossentropy',
optimizer=disc_optimizer,
metrics=['accuracy'])
generator.compile(loss='binary_crossentropy', optimizer=gen_optimizer)
combined.compile(loss='binary_crossentropy', optimizer=gen_optimizer)
如果您会注意到,我们将创建两个自定义 Adam 优化器。 这是因为很多时候,我们只想更改判别器或生成器的学习率,从而减慢一个或另一个的学习速度,以至于我们得到一个稳定的 GAN,而后者却无法胜任另一个。 您还会注意到我正在使用beta_1 = 0.5。 这是我发扬光大并取得成功的 DCGAN 原始论文的推荐。 从原始 DCGAN 论文中可以发现,0.0002 的学习率也是一个很好的起点。
训练循环
以前,我们曾很奢侈地在模型上调用.fit(),让 Keras 处理将数据分成小批和为我们训练的痛苦过程。
不幸的是,因为我们需要为一个批量器对判别器和堆叠模型一起执行单独的更新,所以我们将不得不用老式的方式来做一些循环。 这就是过去一直做的事情,因此虽然可能需要做更多的工作,但它的确使我感到怀旧。 以下代码说明了训练技术:
num_examples = X_train.shape[0]
num_batches = int(num_examples / float(batch_size))
half_batch = int(batch_size / 2)
for epoch in range(epochs + 1):
for batch in range(num_batches):
# noise images for the batch
noise = np.random.normal(0, 1, (half_batch, 100))
fake_images = generator.predict(noise)
fake_labels = np.zeros((half_batch, 1))
# real images for batch
idx = np.random.randint(0, X_train.shape[0], half_batch)
real_images = X_train[idx]
real_labels = np.ones((half_batch, 1))
# Train the discriminator (real classified as ones and
generated as zeros)
d_loss_real = discriminator.train_on_batch(real_images,
real_labels)
d_loss_fake = discriminator.train_on_batch(fake_images,
fake_labels)
d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)
noise = np.random.normal(0, 1, (batch_size, 100))
# Train the generator
g_loss = combined.train_on_batch(noise, np.ones((batch_size, 1)))
# Plot the progress
print("Epoch %d Batch %d/%d [D loss: %f, acc.: %.2f%%] [G loss:
%f]" %
(epoch,batch, num_batches, d_loss[0], 100 * d_loss[1], g_loss))
if batch % 50 == 0:
save_imgs(generator, epoch, batch)
可以肯定,这里发生了很多事情。 和以前一样,让我们逐个细分。 首先,让我们看一下生成噪声向量的代码:
noise = np.random.normal(0, 1, (half_batch, 100))
fake_images = generator.predict(noise)
fake_labels = np.zeros((half_batch, 1))
这段代码生成了一个噪声向量矩阵(我们之前将其称为z)并将其发送到生成器。 它返回了一组生成的图像,我称之为伪图像。 我们将使用它们来训练判别器,因此我们要使用的标签为 0,表示这些实际上是生成的图像。
注意,这里的形状是half_batch x 28 x 28 x 1。 half_batch正是您所想的。 我们将创建一半的生成图像,因为另一半将是真实数据,我们将在下一步进行组装。 要获取真实图像,我们将在X_train上生成一组随机索引,并将X_train的切片用作真实图像,如以下代码所示:
idx = np.random.randint(0, X_train.shape[0], half_batch)
real_images = X_train[idx]
real_labels = np.ones((half_batch, 1))
是的,在这种情况下,我们正在抽样更换。 它确实可以解决,但可能不是实现小批量训练的最佳方法。 但是,它可能是最简单,最常见的。
由于我们正在使用这些图像来训练判别器,并且由于它们是真实图像,因此我们将它们分配为1作为标签,而不是0。 现在我们已经组装了判别器训练集,我们将更新判别器。 还要注意,我们没有使用我们之前讨论的软标签。 那是因为我想让事情尽可能地容易理解。 幸运的是,在这种情况下,网络不需要它们。 我们将使用以下代码来训练判别器:
# Train the discriminator (real classified as ones and generated as zeros)
d_loss_real = discriminator.train_on_batch(real_images, real_labels)
d_loss_fake = discriminator.train_on_batch(fake_images, fake_labels)
d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)
请注意,这里我使用的是判别符的train_on_batch()方法。 这是我第一次在本书中使用此方法。 train_on_batch()方法正好执行一轮正向和反向传播。 每次我们调用它时,它都会从模型的先前状态更新一次模型。
另请注意,我正在分别对真实图像和伪图像进行更新。 这是我先前在“生成器架构”部分中引用的 GAN 黑魔法 Git 上给出的建议。 尤其是在训练的早期阶段,当真实图像和伪图像来自完全不同的分布时,如果我们将两组数据放在同一更新中,则批量归一化将导致训练问题。
现在,判别器已经更新,是时候更新生成器了。 这是通过更新组合栈间接完成的,如以下代码所示:
noise = np.random.normal(0, 1, (batch_size, 100))
g_loss = combined.train_on_batch(noise, np.ones((batch_size, 1)))
为了更新组合模型,我们创建了一个新的噪声矩阵,这次它将与整个批次一样大。 我们将其用作栈的输入,这将使生成器生成图像,并使用判别器评估该图像。 最后,我们将使用1标签,因为我们想在实际图像和生成的图像之间反向传播误差。
最后,训练循环报告epoch/batch处的判别器和生成器损失,然后每epoch中的每 50 批,我们将使用save_imgs生成示例图像并将其保存到磁盘,如以下代码所示:
print("Epoch %d Batch %d/%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" %
(epoch,batch, num_batches, d_loss[0], 100 * d_loss[1], g_loss))
if batch % 50 == 0:
save_imgs(generator, epoch, batch)
save_imgs函数使用生成器在运行时创建图像,因此我们可以看到工作的成果。 我们将使用以下代码来定义save_imgs:
def save_imgs(generator, epoch, batch):
r, c = 5, 5
noise = np.random.normal(0, 1, (r * c, 100))
gen_imgs = generator.predict(noise)
gen_imgs = 0.5 * gen_imgs + 0.5
fig, axs = plt.subplots(r, c)
cnt = 0
for i in range(r):
for j in range(c):
axs[i, j].imshow(gen_imgs[cnt, :, :, 0], cmap='gray')
axs[i, j].axis('off')
cnt += 1
fig.savefig("images/mnist_%d_%d.png" % (epoch, batch))
plt.close()
它通过创建噪声矩阵并检索图像矩阵来仅使用生成器。 然后,使用matplotlib.pyplot将这些图像保存到5 x 5网格中的磁盘上。
模型评估
当您构建深层神经网络来创建图像时,好坏有点主观。 让我们看一下训练过程的一些示例,以便您可以亲自了解 GAN 如何开始学习如何生成 MNIST。
这是第一个周期的第一批网络。 显然,此时生成器对生成 MNIST 并不了解。 只是噪音,如下图所示:

但是只有 50 个批次,正在发生一些事情,如下面的图像所示:

在 200 个批次的周期 0 之后,我们几乎可以看到数字,如下图所示:

一个完整的周期后,这是我们的生成器。 我认为这些生成的数字看起来不错,而且我可以看到判别符可能会被它们欺骗。 在这一点上,我们可能会继续改善一点,但是随着计算机生成一些令人信服的 MNIST 数字,我们的 GAN 似乎已经发挥了作用,如下图所示:

尽管大多数代码是相同的,但在结束本章之前,让我们再看一个使用彩色图像的示例。
使用 Keras GAN 生成 CIFAR-10 图像
虽然网络架构在很大程度上保持不变,但我认为有必要向您展示一个使用彩色图像的示例,并在 Git 中提供示例,以便在想要将 GAN 应用于您的 GAN 时有一些起点。 自己的数据。
CIFAR-10是一个著名的数据集,包含 60,000 张32 x 32 x 3 RGB 彩色图像,分布在 10 个类别中。 这些类别是飞机,汽车,鸟类,猫,鹿,狗,青蛙,马,船和卡车。 希望以后看到生成的图像时,您可能会看到一些可以想象的东西,就像那些对象。
加载 CIFAR-10
加载数据集几乎完全相同,因为 Keras 还使用以下代码为CIFAR-10提供了一个加载器:
from keras.datasets import cifar10
def load_data():
(X_train, y_train), (X_test, y_test) = cifar10.load_data()
X_train = (X_train.astype(np.float32) - 127.5) / 127.5
return X_train
创建生成器
生成器需要产生32 x 32 x 3图像。 这需要对我们的网络架构进行两项细微更改,您可以在此处看到它们:
input = Input(noise_shape)
x = Dense(128 * 8 * 8, activation="relu")(input)
x = Reshape((8, 8, 128))(x)
x = BatchNormalization(momentum=0.8)(x)
x = UpSampling2D()(x)
x = Conv2D(128, kernel_size=3, padding="same")(x)
x = Activation("relu")(x)
x = BatchNormalization(momentum=0.8)(x)
x = UpSampling2D()(x)
x = Conv2D(64, kernel_size=3, padding="same")(x)
x = Activation("relu")(x)
x = BatchNormalization(momentum=0.8)(x)
x = Conv2D(3, kernel_size=3, padding="same")(x)
out = Activation("tanh")(x)
model = Model(input, out)
由于我们需要在 32 处结束,并且我们将两次上采样,因此我们应该从 8 开始。这可以通过将密集层及其相应的重塑层从128 * 7 * 7更改为128 * 8 * 8来轻松实现。
由于我们的图像现在包含三个通道,因此最后的卷积层也需要包含三个通道,而不是一个。 这里的所有都是它的; 我们现在可以生成彩色图像!
创建判别器
判别符几乎完全不变。 输入层需要从28 x 28 x 1更改为32 x 32 x 3。 另外ZeroPadding2D可以毫无问题地删除,因为没有它的层算术就可以工作。
训练循环
训练循环保持不变,区别器构建调用除外,该调用需要与 CIFAR-10 图像大小相对应的新尺寸,如以下代码所示:
discriminator = build_discriminator(img_shape=(32, 32, 3))
当从一个数据集移动到另一个数据集时,通常会需要调整我们的学习率或网络架构。 幸运的是,在此示例中并非如此。
模型评估
CIFAR-10数据集当然更加复杂,并且网络具有更多的参数。 因此,事情将需要更长的时间。 这是在周期 0(批次 300)中我们的图像的样子:

我可能开始看到一些边缘,但是看起来并不像什么。 但是,如果我们等待几个周期,我们显然处在松鼠和怪异的鱼类地区。 我们可以看到一些东西正在成形,只是有些模糊,如下图所示:

下图显示了 12 个周期后的生成器:

我看到分辨率很低的鸟,鱼,甚至还有飞机和卡车。 当然,我们还有很长的路要走,但是我们的网络已经学会了创建图像,这非常令人兴奋。
总结
在本章中,我们研究了 GAN 以及如何将其用于生成新图像。 我们学习了一些很好地构建 GAN 的规则,甚至学习了模拟 MNIST 和 CIFAR-10 图像。 毫无疑问,您可能已经在媒体上看到了一些由 GANs 制作的惊人图像。 在阅读了本章并完成了这些示例之后,您将拥有执行相同操作的工具。 我希望您可以采纳这些想法并加以调整。 剩下的唯一限制是您自己的想象力,数据和 GPU 预算。
在这本书中,我们涵盖了深度学习的许多应用,从简单的回归到生成对抗网络。 我对这本书的最大希望是,它可以帮助您实际使用深度学习技术,而其中的许多技术已经存在于学术界和研究领域,而这超出了实践数据科学家或机器学习工程师的能力。 在此过程中,我希望我能就如何构建更好的深度神经网络以及何时使用深度网络(而不是更传统的模型)提供一些建议。 如果您在这 13 章中一直跟着我,请多多关照。
“我们都是手工艺品的学徒,没人能成为大师。”
——欧内斯特·海明威

浙公网安备 33010602011771号