wordpress ins插件,台州网站排名优化价格,品牌营销网站建设,网站开发持续更新原文#xff1a;Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow 译者#xff1a;飞龙 协议#xff1a;CC BY-NC-SA 4.0 第十四章#xff1a;使用卷积神经网络进行深度计算机视觉
尽管 IBM 的 Deep Blue 超级计算机在 1996 年击败了国际象棋世界冠军… 原文Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow 译者飞龙 协议CC BY-NC-SA 4.0 第十四章使用卷积神经网络进行深度计算机视觉
尽管 IBM 的 Deep Blue 超级计算机在 1996 年击败了国际象棋世界冠军加里·卡斯帕罗夫但直到最近计算机才能可靠地执行看似微不足道的任务比如在图片中检测小狗或识别口语。为什么这些任务对我们人类来说如此轻松答案在于感知主要发生在我们的意识领域之外在我们大脑中专门的视觉、听觉和其他感觉模块中。当感觉信息达到我们的意识时它已经被赋予高级特征例如当你看到一张可爱小狗的图片时你无法选择不看到小狗不注意到它的可爱。你也无法解释如何识别一个可爱的小狗对你来说这是显而易见的。因此我们不能信任我们的主观经验感知并不是微不足道的要理解它我们必须看看我们的感觉模块是如何工作的。
卷积神经网络CNNs起源于对大脑视觉皮层的研究自上世纪 80 年代以来就被用于计算机图像识别。在过去的 10 年里由于计算能力的增加、可用训练数据的增加以及第十一章中介绍的用于训练深度网络的技巧CNNs 已经成功在一些复杂的视觉任务上实现了超人类表现。它们驱动着图像搜索服务、自动驾驶汽车、自动视频分类系统等。此外CNNs 并不局限于视觉感知它们在许多其他任务上也取得了成功比如语音识别和自然语言处理。然而我们现在将专注于视觉应用。
在本章中我们将探讨 CNNs 的起源它们的构建模块是什么样的以及如何使用 Keras 实现它们。然后我们将讨论一些最佳的 CNN 架构以及其他视觉任务包括目标检测对图像中的多个对象进行分类并在其周围放置边界框和语义分割根据对象所属的类别对每个像素进行分类。
视觉皮层的结构
David H. Hubel 和 Torsten Wiesel 在1958 年对猫进行了一系列实验¹1959 年以及几年后对猴子进行的实验^(3为视觉皮层的结构提供了关键见解这两位作者因其工作于 1981 年获得了诺贝尔生理学或医学奖。特别是他们表明视觉皮层中许多神经元具有小的局部感受野这意味着它们只对位于视觉场有限区域内的视觉刺激做出反应见图 14-1其中五个神经元的局部感受野由虚线圈表示。不同神经元的感受野可能重叠它们共同覆盖整个视觉场。 图 14-1. 视觉皮层中的生物神经元对视觉场中称为感受野的小区域中的特定模式做出反应随着视觉信号通过连续的大脑模块神经元对更大感受野中的更复杂模式做出反应
此外作者们表明一些神经元只对水平线的图像做出反应而另一些只对具有不同方向的线做出反应两个神经元可能具有相同的感受野但对不同的线方向做出反应。他们还注意到一些神经元具有更大的感受野它们对更复杂的模式做出反应这些模式是低级模式的组合。这些观察结果导致了这样一个想法即高级神经元基于相邻低级神经元的输出在图 14-1 中注意到每个神经元只连接到前一层附近的神经元。这种强大的架构能够在视觉领域的任何区域检测各种复杂的模式。
这些对视觉皮层的研究启发了 1980 年引入的neocognitron逐渐演变成我们现在称之为卷积神经网络的东西。一个重要的里程碑是 Yann LeCun 等人在 1998 年发表的一篇论文介绍了著名的LeNet-5架构这种架构被银行广泛用于识别支票上的手写数字。这种架构具有一些你已经了解的构建块如全连接层和 Sigmoid 激活函数但它还引入了两个新的构建块卷积层和池化层。现在让我们来看看它们。
注意
为什么不简单地使用具有全连接层的深度神经网络来进行图像识别任务呢不幸的是尽管这对于小图像例如 MNIST效果很好但对于较大的图像来说由于需要的参数数量巨大它会崩溃。例如一个 100×100 像素的图像有 10,000 个像素如果第一层只有 1,000 个神经元这已经严重限制了传递到下一层的信息量这意味着总共有 1 千万个连接。而这只是第一层。CNN 通过部分连接的层和权重共享来解决这个问题。
卷积层
CNN 最重要的构建块是卷积层第一个卷积层中的神经元不与输入图像中的每个像素相连接就像在前几章讨论的层中那样而只与其感受野中的像素相连接参见图 14-2。反过来第二个卷积层中的每个神经元只与第一层中一个小矩形内的神经元相连接。这种架构允许网络在第一个隐藏层集中于小的低级特征然后在下一个隐藏层中将它们组合成更大的高级特征依此类推。这种分层结构在现实世界的图像中很常见这也是 CNN 在图像识别方面表现出色的原因之一。 图 14-2。具有矩形局部感受野的 CNN 层
注意
到目前为止我们看到的所有多层神经网络都由一长串神经元组成我们必须在将输入图像馈送到神经网络之前将其展平为 1D。在 CNN 中每一层都以 2D 表示这使得更容易将神经元与其对应的输入匹配。
给定层中位于第i行第j列的神经元连接到前一层中位于第i到第i f[h] – 1 行第j到第j f[w] – 1 列的神经元的输出其中f[h]和f[w]是感受野的高度和宽度参见图 14-3。为了使一层具有与前一层相同的高度和宽度通常在输入周围添加零如图中所示。这称为零填充。
还可以通过间隔感受野来将大输入层连接到一个较小的层如图 14-4 所示。这显着降低了模型的计算复杂性。从一个感受野到下一个感受野的水平或垂直步长称为步幅。在图中一个 5×7 的输入层加上零填充连接到一个 3×4 的层使用 3×3 的感受野和步幅为 2在这个例子中步幅在两个方向上是相同的但不一定要这样。上层中位于第i行第j列的神经元连接到前一层中位于第i×s[h]到第i×s[h]f[h]–1 行第j×s[w]到第j×s[w]f[w]–1 列的神经元的输出其中s[h]和s[w]是垂直和水平步幅。 图 14-3。层与零填充之间的连接 图 14-4。使用步幅为 2 降低维度
滤波器
一个神经元的权重可以表示为一个与感受野大小相同的小图像。例如图 14-5 显示了两组可能的权重称为滤波器或卷积核或只是内核。第一个滤波器表示为一个黑色正方形中间有一条垂直白线它是一个 7×7 的矩阵除了中间列全是 1其他都是 0使用这些权重的神经元将忽略其感受野中的所有内容除了中间的垂直线因为所有输入将被乘以 0除了中间的垂直线。第二个滤波器是一个黑色正方形中间有一条水平白线。使用这些权重的神经元将忽略其感受野中的所有内容除了中间的水平线。 图 14-5。应用两个不同的滤波器以获得两个特征图
现在如果一个层中的所有神经元使用相同的垂直线滤波器和相同的偏置项并且您将输入图像输入到网络中如图 14-5 所示底部图像该层将输出左上角的图像。请注意垂直白线得到增强而其余部分变得模糊。类似地如果所有神经元使用相同的水平线滤波器则会得到右上角的图像请注意水平白线得到增强而其余部分被模糊化。因此一个充满使用相同滤波器的神经元的层会输出一个特征图突出显示激活滤波器最多的图像区域。但不用担心您不必手动定义滤波器相反在训练期间卷积层将自动学习其任务中最有用的滤波器上面的层将学会将它们组合成更复杂的模式。
堆叠多个特征图
到目前为止为了简单起见我已经将每个卷积层的输出表示为一个 2D 层但实际上卷积层有多个滤波器您决定有多少个并且每个滤波器输出一个特征图因此在 3D 中更准确地表示请参见图 14-6。每个特征图中的每个像素都有一个神经元并且给定特征图中的所有神经元共享相同的参数即相同的内核和偏置项。不同特征图中的神经元使用不同的参数。神经元的感受野与之前描述的相同但它跨越了前一层的所有特征图。简而言之卷积层同时将多个可训练滤波器应用于其输入使其能够在其输入的任何位置检测多个特征。 图 14-6。两个具有多个滤波器内核的卷积层处理具有三个颜色通道的彩色图像每个卷积层输出一个特征图每个滤波器
注意
所有特征图中的所有神经元共享相同的参数这显著减少了模型中的参数数量。一旦 CNN 学会在一个位置识别模式它就可以在任何其他位置识别它。相比之下一旦全连接的神经网络学会在一个位置识别模式它只能在那个特定位置识别它。
输入图像也由多个子层组成每个颜色通道一个。如第九章中所述通常有三个红色、绿色和蓝色RGB。灰度图像只有一个通道但有些图像可能有更多通道例如捕捉额外光频率如红外线的卫星图像。
具体来说在给定卷积层l中特征图k中第i行、第j列的神经元与前一层l – 1 中位于第i × s[h]至i × s[h] f[h] – 1 行和第j × s[w]至j × s[w] f[w] – 1 列的神经元的输出相连跨所有特征图在第l – 1 层。请注意在同一层中位于相同行i和列j但在不同特征图中的所有神经元与前一层中相同位置的神经元的输出相连。
方程 14-1 总结了前面的解释用一个大数学方程表示它展示了如何计算卷积层中给定神经元的输出。由于所有不同的索引它看起来有点丑陋但它的作用只是计算所有输入的加权和再加上偏置项。
方程 14-1。计算卷积层中神经元的输出
z i,j,k b k ∑ u0 f h -1 ∑ v0 f w -1 ∑ k‘0 f n ’ -1 x i ‘ ,j ’ ,k ‘ × w u,v,k ’ ,k with i ‘ i × s h u j ’ j × s w v
在这个方程中 z[i,] [j,] [k] 是位于卷积层第l层特征图k中第i行、第j列的神经元的输出。 如前所述s[h] 和 s[w] 是垂直和水平步幅f[h] 和 f[w] 是感受野的高度和宽度f[n′] 是前一层第l – 1 层中特征图的数量。 x[i′,] [j′,] [k′] 是位于第l – 1 层第i′行、第j′列、特征图k′或通道k′如果前一层是输入层的神经元的输出。 b[k] 是特征图k在第l层的偏置项。您可以将其视为微调特征图k的整体亮度的旋钮。 w[u,] [v,] [k′,] [k]是层l中特征图k中的任何神经元与其输入之间的连接权重该输入位于行u、列v相对于神经元的感受野以及特征图k′。
让我们看看如何使用 Keras 创建和使用卷积层。
使用 Keras 实现卷积层
首先让我们加载和预处理一些样本图像使用 Scikit-Learn 的load_sample_image()函数和 Keras 的CenterCrop和Rescaling层这些都是在第十三章中介绍的
from sklearn.datasets import load_sample_images
import tensorflow as tfimages load_sample_images()[images]
images tf.keras.layers.CenterCrop(height70, width120)(images)
images tf.keras.layers.Rescaling(scale1 / 255)(images)让我们看一下images张量的形状 images.shape
TensorShape([2, 70, 120, 3])哎呀这是一个 4D 张量我们以前从未见过这个所有这些维度是什么意思嗯有两个样本图像这解释了第一个维度。然后每个图像是 70×120因为这是我们在创建CenterCrop层时指定的大小原始图像是 427×640。这解释了第二和第三维度。最后每个像素在每个颜色通道上保存一个值有三个颜色通道——红色、绿色和蓝色这解释了最后一个维度。
现在让我们创建一个 2D 卷积层并将这些图像输入其中看看输出是什么。为此Keras 提供了一个Convolution2D层别名为Conv2D。在幕后这个层依赖于 TensorFlow 的tf.nn.conv2d()操作。让我们创建一个具有 32 个滤波器的卷积层每个滤波器大小为 7×7使用kernel_size7相当于使用kernel_size(7 , 7)并将这个层应用于我们的两个图像的小批量
conv_layer tf.keras.layers.Conv2D(filters32, kernel_size7)
fmaps conv_layer(images)注意
当我们谈论 2D 卷积层时“2D”指的是空间维度高度和宽度但正如你所看到的该层接受 4D 输入正如我们所看到的另外两个维度是批量大小第一个维度和通道数最后一个维度。
现在让我们看一下输出的形状 fmaps.shape
TensorShape([2, 64, 114, 32])输出形状与输入形状类似有两个主要区别。首先有 32 个通道而不是 3 个。这是因为我们设置了filters32所以我们得到 32 个输出特征图在每个位置的红色、绿色和蓝色的强度代替我们现在有每个位置的每个特征的强度。其次高度和宽度都减小了 6 个像素。这是因为Conv2D层默认不使用任何零填充这意味着我们在输出特征图的两侧丢失了一些像素取决于滤波器的大小。在这种情况下由于卷积核大小为 7我们水平和垂直各丢失 6 个像素即每侧 3 个像素。
警告
默认选项令人惊讶地被命名为paddingvalid实际上意味着根本没有零填充这个名称来自于这样一个事实即在这种情况下每个神经元的感受野严格位于输入内部的有效位置不会超出边界。这不是 Keras 的命名怪癖每个人都使用这种奇怪的命名法。
如果我们设置paddingsame那么输入将在所有侧面填充足够的零以确保输出特征图最终与输入具有相同大小因此这个选项的名称 conv_layer tf.keras.layers.Conv2D(filters32, kernel_size7,
... paddingsame)
...fmaps conv_layer(images)fmaps.shape
TensorShape([2, 70, 120, 32])这两种填充选项在图 14-7 中有所说明。为简单起见这里只显示了水平维度但当然相同的逻辑也适用于垂直维度。
如果步幅大于 1在任何方向上那么输出大小将不等于输入大小即使paddingsame。例如如果设置strides2或等效地strides(2, 2)那么输出特征图将是 35×60垂直和水平方向都减半。图 14-8 展示了当strides2时会发生什么两种填充选项都有。 图 14-7。当strides1时的两种填充选项 图 14-8。当步长大于 1 时即使使用same填充和valid填充可能会忽略一些输入输出也会小得多
如果您感兴趣这是输出大小是如何计算的 当paddingvalid时如果输入的宽度为i[h]那么输出宽度等于(i[h] - f[h] s[h]) / s[h]向下取整。请记住f[h]是卷积核的宽度s[h]是水平步长。除法中的余数对应于输入图像右侧被忽略的列。同样的逻辑也可以用来计算输出高度以及图像底部被忽略的行。 当paddingsame时输出宽度等于i[h] / s[h]向上取整。为了实现这一点在输入图像的左右两侧填充适当数量的零列如果可能的话数量相等或者在右侧多一个。假设输出宽度为o[w]那么填充的零列数为(o[w] - 1) × s[h] f[h] - i[h]。同样的逻辑也可以用来计算输出高度和填充行数。
现在让我们来看一下层的权重在方程 14-1 中被标记为w[u,] [v,] [k’,] [k]和b[k]。就像Dense层一样Conv2D层保存所有层的权重包括卷积核和偏置。卷积核是随机初始化的而偏置初始化为零。这些权重可以通过weights属性作为 TF 变量访问也可以通过get_weights()方法作为 NumPy 数组访问 kernels, biases conv_layer.get_weights()kernels.shape
(7, 7, 3, 32)biases.shape
(32,)kernels数组是 4D 的其形状为[kernel_height, kernel_width, input_channels, output_channels]。biases数组是 1D 的形状为[output_channels]。输出通道的数量等于输出特征图的数量也等于滤波器的数量。
最重要的是需要注意输入图像的高度和宽度不会出现在卷积核的形状中这是因为输出特征图中的所有神经元共享相同的权重正如之前解释的那样。这意味着您可以将任何大小的图像馈送到这一层只要它们至少与卷积核一样大并且具有正确数量的通道在这种情况下为三个。
最后通常情况下您会希望在创建Conv2D层时指定一个激活函数如 ReLU并指定相应的内核初始化器如 He 初始化。这与Dense层的原因相同卷积层执行线性操作因此如果您堆叠多个卷积层而没有任何激活函数它们都等同于单个卷积层它们将无法学习到真正复杂的内容。
正如您所看到的卷积层有很多超参数filters、kernel_size、padding、strides、activation、kernel_initializer等。通常情况下您可以使用交叉验证来找到正确的超参数值但这是非常耗时的。我们将在本章后面讨论常见的 CNN 架构以便让您了解在实践中哪些超参数值效果最好。
内存需求
CNN 的另一个挑战是卷积层需要大量的 RAM。这在训练过程中尤为明显因为反向传播的反向传递需要在前向传递期间计算的所有中间值。
例如考虑一个具有 200 个 5×5 滤波器的卷积层步幅为 1使用same填充。如果输入是一个 150×100 的 RGB 图像三个通道那么参数数量为5×5×31×20015,2001 对应于偏置项与全连接层相比相当小。然而这 200 个特征图中的每一个包含 150×100 个神经元每个神经元都需要计算其 5×5×375 个输入的加权和总共有 2.25 亿次浮点乘法。虽然不像全连接层那么糟糕但仍然相当计算密集。此外如果使用 32 位浮点数表示特征图那么卷积层的输出将占用 200×150×100×329600 万位12 MB的 RAM。而这只是一个实例的情况——如果一个训练批次包含 100 个实例那么这一层将使用 1.2 GB 的 RAM
在推断即对新实例进行预测时一个层占用的 RAM 可以在计算下一层后立即释放因此你只需要两个连续层所需的 RAM。但在训练期间前向传播期间计算的所有内容都需要保留以进行反向传播因此所需的 RAM 量至少是所有层所需 RAM 的总量。
提示
如果由于内存不足错误而导致训练崩溃你可以尝试减小小批量大小。或者你可以尝试使用步幅减少维度去掉一些层使用 16 位浮点数代替 32 位浮点数或者将 CNN 分布在多个设备上你将在第十九章中看到如何做。
现在让我们来看看 CNN 的第二个常见构建块池化层。
池化层
一旦你理解了卷积层的工作原理池化层就很容易理解了。它们的目标是对输入图像进行子采样即缩小以减少计算负载、内存使用和参数数量从而限制过拟合的风险。
就像在卷积层中一样池化层中的每个神经元连接到前一层中有限数量的神经元的输出这些神经元位于一个小的矩形感受野内。你必须像以前一样定义它的大小、步幅和填充类型。然而池化神经元没有权重它所做的只是使用聚合函数如最大值或平均值聚合输入。图 14-9 展示了最大池化层这是最常见的池化层类型。在这个例子中我们使用了一个 2×2 的池化核步幅为 2没有填充。在图 14-9 中的左下角感受野中输入值为 1、5、3、2因此只有最大值 5 传播到下一层。由于步幅为 2输出图像的高度和宽度都是输入图像的一半向下取整因为我们没有使用填充。 图 14-9。最大池化层2×2 池化核步幅 2无填充
注意
池化层通常独立地处理每个输入通道因此输出深度即通道数与输入深度相同。
除了减少计算、内存使用和参数数量之外最大池化层还引入了一定程度的不变性如图 14-10 所示。在这里我们假设亮像素的值低于暗像素的值并考虑三个图像A、B、C通过一个 2×2 内核和步幅 2 的最大池化层。图像 B 和 C 与图像 A 相同但向右移动了一个和两个像素。正如您所看到的图像 A 和 B 的最大池化层的输出是相同的。这就是平移不变性的含义。对于图像 C输出是不同的向右移动一个像素但仍然有 50%的不变性。通过在 CNN 中的几层之间插入一个最大池化层可以在更大的尺度上获得一定程度的平移不变性。此外最大池化还提供了一定程度的旋转不变性和轻微的尺度不变性。这种不变性即使有限在预测不应该依赖这些细节的情况下可能是有用的比如在分类任务中。
然而最大池化也有一些缺点。显然它非常破坏性即使使用一个微小的 2×2 内核和步幅为 2输出在两个方向上都会变小两倍因此其面积会变小四倍简单地丢弃了输入值的 75%。在某些应用中不变性并不理想。以语义分割为例根据像素所属的对象对图像中的每个像素进行分类的任务我们将在本章后面探讨显然如果输入图像向右平移一个像素输出也应该向右平移一个像素。在这种情况下的目标是等变性而不是不变性对输入的微小变化应导致输出的相应微小变化。 图 14-10。对小平移的不变性
使用 Keras 实现池化层
以下代码创建了一个MaxPooling2D层别名为MaxPool2D使用一个 2×2 内核。步幅默认为内核大小因此此层使用步幅为 2水平和垂直。默认情况下它使用valid填充即根本不填充
max_pool tf.keras.layers.MaxPool2D(pool_size2)要创建一个平均池化层只需使用AveragePooling2D别名为AvgPool2D而不是MaxPool2D。正如您所期望的那样它的工作方式与最大池化层完全相同只是计算均值而不是最大值。平均池化层曾经非常流行但现在人们大多使用最大池化层因为它们通常表现更好。这可能看起来令人惊讶因为计算均值通常比计算最大值丢失的信息更少。但另一方面最大池化仅保留最强的特征摆脱了所有无意义的特征因此下一层得到了一个更干净的信号来处理。此外最大池化比平均池化提供更强的平移不变性并且需要稍少的计算。
请注意最大池化和平均池化可以沿深度维度而不是空间维度执行尽管这不太常见。这可以让 CNN 学习对各种特征具有不变性。例如它可以学习多个滤波器每个滤波器检测相同模式的不同旋转例如手写数字参见图 14-11深度最大池化层将确保输出不管旋转如何都是相同的。CNN 也可以学习对任何东西具有不变性厚度、亮度、倾斜、颜色等等。 图 14-11。深度最大池化可以帮助 CNN 学习旋转不变性在这种情况下
Keras 不包括深度最大池化层但实现一个自定义层并不太困难
class DepthPool(tf.keras.layers.Layer):def __init__(self, pool_size2, **kwargs):super().__init__(**kwargs)self.pool_size pool_sizedef call(self, inputs):shape tf.shape(inputs) # shape[-1] is the number of channelsgroups shape[-1] // self.pool_size # number of channel groupsnew_shape tf.concat([shape[:-1], [groups, self.pool_size]], axis0)return tf.reduce_max(tf.reshape(inputs, new_shape), axis-1)这一层将其输入重塑为所需大小的通道组pool_size然后使用tf.reduce_max()来计算每个组的最大值。这种实现假定步幅等于池大小这通常是你想要的。或者您可以使用 TensorFlow 的tf.nn.max_pool()操作并在Lambda层中包装以在 Keras 模型中使用它但遗憾的是此操作不实现 GPU 的深度池化只实现 CPU 的深度池化。
在现代架构中经常看到的最后一种类型的池化层是全局平均池化层。它的工作方式非常不同它只是计算每个整个特征图的平均值就像使用与输入具有相同空间维度的池化核的平均池化层。这意味着它只输出每个特征图和每个实例的一个数字。尽管这当然是极其破坏性的大部分特征图中的信息都丢失了但它可以在输出层之前非常有用稍后您将在本章中看到。要创建这样的层只需使用GlobalAveragePooling2D类别名GlobalAvgPool2D
global_avg_pool tf.keras.layers.GlobalAvgPool2D()这等同于以下Lambda层它计算空间维度高度和宽度上的平均值
global_avg_pool tf.keras.layers.Lambda(lambda X: tf.reduce_mean(X, axis[1, 2]))例如如果我们将这一层应用于输入图像我们将得到每个图像的红色、绿色和蓝色的平均强度 global_avg_pool(images)
tf.Tensor: shape(2, 3), dtypefloat32, numpy
array([[0.64338624, 0.5971759 , 0.5824972 ],[0.76306933, 0.26011038, 0.10849128]], dtypefloat32)现在您知道如何创建卷积神经网络的所有构建模块了。让我们看看如何组装它们。
CNN 架构
典型的 CNN 架构堆叠了几个卷积层每个通常后面跟着一个 ReLU 层然后是一个池化层然后又是几个卷积层ReLU然后是另一个池化层依此类推。随着图像通过网络的传递图像变得越来越小但也通常变得越来越深即具有更多的特征图这要归功于卷积层参见图 14-12。在堆栈的顶部添加了一个常规的前馈神经网络由几个全连接层ReLUs组成最后一层输出预测例如一个 softmax 层输出估计的类别概率。 图 14-12. 典型的 CNN 架构
提示
一个常见的错误是使用太大的卷积核。例如不要使用一个 5×5 的卷积层而是堆叠两个 3×3 的卷积层这将使用更少的参数需要更少的计算并且通常表现更好。一个例外是第一个卷积层它通常可以有一个大的卷积核例如 5×5通常具有 2 或更大的步幅。这将减少图像的空间维度而不会丢失太多信息而且由于输入图像通常只有三个通道因此成本不会太高。
这是如何实现一个基本的 CNN 来处理时尚 MNIST 数据集的在第十章介绍
from functools import partialDefaultConv2D partial(tf.keras.layers.Conv2D, kernel_size3, paddingsame,activationrelu, kernel_initializerhe_normal)
model tf.keras.Sequential([DefaultConv2D(filters64, kernel_size7, input_shape[28, 28, 1]),tf.keras.layers.MaxPool2D(),DefaultConv2D(filters128),DefaultConv2D(filters128),tf.keras.layers.MaxPool2D(),DefaultConv2D(filters256),DefaultConv2D(filters256),tf.keras.layers.MaxPool2D(),tf.keras.layers.Flatten(),tf.keras.layers.Dense(units128, activationrelu,kernel_initializerhe_normal),tf.keras.layers.Dropout(0.5),tf.keras.layers.Dense(units64, activationrelu,kernel_initializerhe_normal),tf.keras.layers.Dropout(0.5),tf.keras.layers.Dense(units10, activationsoftmax)
])让我们来看一下这段代码 我们使用functools.partial()函数在第十一章介绍来定义DefaultConv2D它的作用就像Conv2D但具有不同的默认参数一个小的 3 的内核大小same填充ReLU 激活函数以及相应的 He 初始化器。 接下来我们创建Sequential模型。它的第一层是一个具有 64 个相当大的滤波器7×7的DefaultConv2D。它使用默认的步幅 1因为输入图像不是很大。它还设置input_shape[28, 28, 1]因为图像是 28×28 像素具有单个颜色通道即灰度。当您加载时尚 MNIST 数据集时请确保每个图像具有这种形状您可能需要使用np.reshape()或np.expanddims()来添加通道维度。或者您可以在模型中使用Reshape层作为第一层。 然后我们添加一个使用默认池大小为 2 的最大池化层因此它将每个空间维度除以 2。 然后我们重复相同的结构两次两个卷积层后面跟着一个最大池化层。对于更大的图像我们可以多次重复这个结构。重复次数是一个可以调整的超参数。 请注意随着我们向 CNN 向输出层上升滤波器的数量会翻倍最初为 64然后为 128然后为 256这是有道理的因为低级特征的数量通常相当低例如小圆圈水平线但有许多不同的方法可以将它们组合成更高级别的特征。在每个池化层后将滤波器数量翻倍是一种常见做法由于池化层将每个空间维度除以 2我们可以在下一层中加倍特征图的数量而不用担心参数数量、内存使用或计算负载的激增。 接下来是全连接网络由两个隐藏的密集层和一个密集输出层组成。由于这是一个有 10 个类别的分类任务输出层有 10 个单元并且使用 softmax 激活函数。请注意我们必须在第一个密集层之前扁平化输入因为它期望每个实例的特征是一个 1D 数组。我们还添加了两个 dropout 层每个的 dropout 率为 50%以减少过拟合。
如果您使用sparse_categorical_crossentropy损失编译此模型并将模型拟合到 Fashion MNIST 训练集它应该在测试集上达到超过 92%的准确率。这并不是最先进的但是相当不错显然比我们在第十章中使用密集网络取得的成绩要好得多。
多年来这种基本架构的变体已经被开发出来导致了该领域的惊人进步。这种进步的一个很好的衡量标准是在 ILSVRCImageNet 挑战等比赛中的错误率。在这个比赛中图像分类的前五错误率即系统的前五个预测中没有包括正确答案的测试图像数量从超过 26%下降到不到 2.3%仅仅用了六年。这些图像相当大例如高度为 256 像素有 1000 个类别其中一些非常微妙尝试区分 120 种狗品种。查看获胜作品的演变是了解 CNN 如何工作以及深度学习研究如何进展的好方法。
我们将首先看一下经典的 LeNet-5 架构1998 年然后看一下几位 ILSVRC 挑战的获胜者AlexNet2012GoogLeNet2014ResNet2015和 SENet2017。在此过程中我们还将看一些其他架构包括 XceptionResNeXtDenseNetMobileNetCSPNet 和 EfficientNet。
LeNet-5
LeNet-5 架构可能是最广为人知的 CNN 架构。正如前面提到的它是由 Yann LeCun 在 1998 年创建的并且被广泛用于手写数字识别MNIST。它由表 14-1 中显示的层组成。
表 14-1. LeNet-5 架构
层类型特征图尺寸核大小步幅激活函数Out全连接–10––RBFF6全连接–84––tanhC5卷积1201 × 15 × 51tanhS4平均池化165 × 52 × 22tanhC3卷积1610 × 105 × 51tanhS2平均池化614 × 142 × 22tanhC1卷积628 × 285 × 51tanhIn输入132 × 32–––
正如您所看到的这看起来与我们的时尚 MNIST 模型非常相似一堆卷积层和池化层然后是一个密集网络。也许与更现代的分类 CNN 相比主要的区别在于激活函数今天我们会使用 ReLU 而不是 tanh使用 softmax 而不是 RBF。还有一些其他不太重要的差异但如果您感兴趣可以在本章的笔记本中找到https://homl.info/colab3。Yann LeCun 的网站还展示了 LeNet-5 对数字进行分类的精彩演示。
AlexNet
AlexNet CNN 架构¹¹在 2012 年 ILSVRC 挑战赛中大幅领先它实现了 17%的前五错误率而第二名竞争对手仅实现了 26%AlexaNet 由 Alex Krizhevsky因此得名、Ilya Sutskever 和 Geoffrey Hinton 开发。它类似于 LeNet-5只是更大更深它是第一个直接将卷积层堆叠在一起的模型而不是将池化层堆叠在每个卷积层之上。表 14-2 展示了这种架构。
表 14-2. AlexNet 架构
层类型特征图大小核大小步幅填充激活函数Out全连接–1,000–––SoftmaxF10全连接–4,096–––ReLUF9全连接–4,096–––ReLUS8最大池化2566 × 63 × 32valid–C7卷积25613 × 133 × 31sameReLUC6卷积38413 × 133 × 31sameReLUC5卷积38413 × 133 × 31sameReLUS4最大池化25613 × 133 × 32valid–C3卷积25627 × 275 × 51sameReLUS2最大池化9627 × 273 × 32valid–C1卷积9655 × 5511 × 114validReLUIn输入3RGB227 × 227––––
为了减少过拟合作者使用了两种正则化技术。首先他们在训练期间对 F9 和 F10 层的输出应用了 50%的 dropout 率的 dropout在第十一章中介绍。其次他们通过随机移动训练图像的各种偏移量、水平翻转它们和改变光照条件来执行数据增强。
AlexNet 还在 C1 和 C3 层的 ReLU 步骤之后立即使用了一个竞争性归一化步骤称为局部响应归一化LRN最强烈激活的神经元抑制了位于相邻特征图中相同位置的其他神经元。这种竞争性激活已经在生物神经元中观察到。这鼓励不同的特征图专门化将它们分开并迫使它们探索更广泛的特征最终提高泛化能力。方程 14-2 展示了如何应用 LRN。
方程 14-2. 局部响应归一化LRN
b i a i kα∑ jj low j high a j 2 -β with j high min i r 2 , f n - 1 j low max 0 , i - r 2
在这个方程中 b[i] 是位于特征图i中的神经元的归一化输出在某一行u和列v请注意在这个方程中我们只考虑位于这一行和列的神经元因此u和v没有显示。 a[i] 是 ReLU 步骤后但规范化之前的神经元的激活。 k、α、β和r是超参数。k称为偏置r称为深度半径。 f[n] 是特征图的数量。
例如如果r 2并且一个神经元具有强烈的激活则它将抑制位于其上下特征图中的神经元的激活。
在 AlexNet 中超参数设置为r 5α 0.0001β 0.75k 2。您可以使用tf.nn.local_response_normalization()函数来实现这一步骤如果要在 Keras 模型中使用它可以将其包装在Lambda层中。
由 Matthew Zeiler 和 Rob Fergus 开发的 AlexNet 的一个变体称为ZF Net¹²并赢得了 2013 年 ILSVRC 挑战赛。它本质上是 AlexNet只是调整了一些超参数特征图数量、卷积核大小、步幅等。
GoogLeNet
GoogLeNet 架构由 Google Research 的 Christian Szegedy 等人开发¹³并通过将前五错误率降低到 7%以下赢得了 ILSVRC 2014 挑战。这一出色的性能在很大程度上来自于该网络比以前的 CNN 更深如您将在图 14-15 中看到的。这得益于称为inception 模块的子网络¹⁴它允许 GoogLeNet 比以前的架构更有效地使用参数实际上GoogLeNet 的参数比 AlexNet 少 10 倍大约 600 万个而不是 6000 万个。
图 14-14 显示了 Inception 模块的架构。符号“3×3 1(S)”表示该层使用 3×3 内核步幅 1 和same填充。输入信号首先并行输入到四个不同的层中。所有卷积层使用 ReLU 激活函数。请注意顶部卷积层使用不同的内核大小1×1、3×3 和 5×5使它们能够捕获不同尺度的模式。还要注意每个单独的层都使用步幅 1 和same填充即使是最大池化层因此它们的输出与它们的输入具有相同的高度和宽度。这使得可以在最终的深度连接层即将来自所有四个顶部卷积层的特征图堆叠在一起中沿深度维度连接所有输出。可以使用 Keras 的Concatenate层来实现使用默认的axis-1。 图 14-14。Inception 模块
您可能会想知道为什么 Inception 模块具有具有 1×1 内核的卷积层。毕竟这些层不能捕获任何特征因为它们一次只查看一个像素对吧实际上这些层有三个目的 尽管它们不能捕获空间模式但它们可以捕获沿深度维度即跨通道的模式。 它们被配置为输出比它们的输入更少的特征图因此它们充当瓶颈层意味着它们降低了维度。这降低了计算成本和参数数量加快了训练速度并提高了泛化能力。 每对卷积层[1×1、3×3]和[1×1、5×5]就像一个强大的卷积层能够捕获更复杂的模式。卷积层等效于在图像上扫过一个密集层在每个位置它只查看一个小的感受野而这些卷积层对等于在图像上扫过两层神经网络。
简而言之您可以将整个 Inception 模块视为一个超级卷积层能够输出捕获各种尺度复杂模式的特征图。
现在让我们来看看 GoogLeNet CNN 的架构参见图 14-15。每个卷积层和每个池化层输出的特征图数量在内核大小之前显示。该架构非常深以至于必须用三列来表示但实际上 GoogLeNet 是一个高高的堆叠包括九个 Inception 模块带有旋转顶部的方框。Inception 模块中的六个数字表示模块中每个卷积层输出的特征图数量与图 14-14 中的顺序相同。请注意所有卷积层都使用 ReLU 激活函数。
让我们来看看这个网络 前两层将图像的高度和宽度分别除以 4因此其面积除以 16以减少计算负载。第一层使用大的内核大小7×7以便保留大部分信息。 然后本地响应归一化层确保前面的层学习到各种各样的特征如前面讨论的。 接下来是两个卷积层其中第一个充当瓶颈层。正如前面提到的您可以将这一对看作一个更聪明的单个卷积层。 再次本地响应归一化层确保前面的层捕获各种各样的模式。 接下来一个最大池化层将图像的高度和宽度减少了一半以加快计算速度。 然后是 CNN 的骨干一个高高的堆叠包括九个 Inception 模块交替使用一对最大池化层来降低维度并加快网络速度。 接下来全局平均池化层输出每个特征图的平均值这会丢弃任何剩余的空间信息这没关系因为在那一点上剩下的空间信息并不多。事实上GoogLeNet 的输入图像通常期望为 224×224 像素因此经过 5 个最大池化层后每个将高度和宽度除以 2特征图缩小到 7×7。此外这是一个分类任务而不是定位任务因此物体在哪里并不重要。由于这一层带来的降维不需要在 CNN 的顶部有几个全连接层就像在 AlexNet 中那样这大大减少了网络中的参数数量并限制了过拟合的风险。 最后几层很容易理解用于正则化的 dropout然后是一个具有 1,000 个单元的全连接层因为有 1,000 个类别以及一个 softmax 激活函数来输出估计的类别概率。 图 14-15。GoogLeNet 架构
原始的 GoogLeNet 架构包括两个辅助分类器插在第三和第六个 inception 模块的顶部。它们都由一个平均池化层、一个卷积层、两个全连接层和一个 softmax 激活层组成。在训练过程中它们的损失缩小了 70%被添加到整体损失中。目标是解决梯度消失问题并对网络进行正则化但后来证明它们的效果相对较小。
后来Google 的研究人员提出了 GoogLeNet 架构的几个变体包括 Inception-v3 和 Inception-v4使用略有不同的 inception 模块以实现更好的性能。
VGGNet
在 ILSVRC 2014 挑战赛中的亚军是VGGNetKaren Simonyan 和 Andrew Zisserman来自牛津大学视觉几何组VGG研究实验室开发了一个非常简单和经典的架构它有 2 或 3 个卷积层和一个池化层然后再有 2 或 3 个卷积层和一个池化层依此类推达到 16 或 19 个卷积层取决于 VGG 的变体再加上一个最终的具有 2 个隐藏层和输出层的密集网络。它使用小的 3×3 滤波器但数量很多。
ResNet
Kaiming He 等人在 ILSVRC 2015 挑战赛中使用Residual Network (ResNet)赢得了冠军其前五错误率令人惊叹地低于 3.6%。获胜的变体使用了一个由 152 层组成的极深 CNN其他变体有 34、50 和 101 层。它证实了一个普遍趋势计算机视觉模型变得越来越深参数越来越少。能够训练如此深的网络的关键是使用跳跃连接也称为快捷连接输入到一个层的信号也被添加到堆栈中更高的层的输出中。让我们看看为什么这很有用。
在训练神经网络时目标是使其模拟目标函数h(x)。如果将输入x添加到网络的输出中即添加一个跳跃连接那么网络将被迫模拟f(x) h(x) - x而不是h(x)。这被称为残差学习。 图 14-16。残差学习
当初始化一个常规的神经网络时它的权重接近于零因此网络只会输出接近于零的值。如果添加一个跳跃连接结果网络将只输出其输入的副本换句话说它最初模拟的是恒等函数。如果目标函数与恒等函数相当接近这通常是情况这将大大加快训练速度。
此外如果添加许多跳跃连接即使有几个层尚未开始学习网络也可以开始取得进展参见图 14-17。由于跳跃连接信号可以轻松地在整个网络中传播。深度残差网络可以看作是一堆残差单元RUs其中每个残差单元是一个带有跳跃连接的小型神经网络。
现在让我们看一下 ResNet 的架构参见图 14-18。它非常简单。它的开头和结尾与 GoogLeNet 完全相同除了没有丢弃层中间只是一个非常深的残差单元堆栈。每个残差单元由两个卷积层组成没有池化层使用 3×3 的卷积核和保持空间维度步幅 1same填充的批量归一化BN和 ReLU 激活。 图 14-17。常规深度神经网络左和深度残差网络右 图 14-18。ResNet 架构
请注意每隔几个残差单元特征图的数量会加倍同时它们的高度和宽度会减半使用步幅为 2 的卷积层。当这种情况发生时输入不能直接添加到残差单元的输出中因为它们的形状不同例如这个问题影响了由虚线箭头表示的跳跃连接在图 14-18 中的情况。为了解决这个问题输入通过一个步幅为 2 的 1×1 卷积层并具有正确数量的输出特征图参见图 14-19。 图 14-19。更改特征图大小和深度时的跳跃连接
存在不同变体的架构具有不同数量的层。ResNet-34 是一个具有 34 层的 ResNet仅计算卷积层和全连接层包含 3 个输出 64 个特征图的 RU4 个输出 128 个特征图的 RU6 个输出 256 个特征图的 RU以及 3 个输出 512 个特征图的 RU。我们将在本章后面实现这个架构。
注意
Google 的Inception-v4¹⁸架构融合了 GoogLeNet 和 ResNet 的思想并在 ImageNet 分类中实现了接近 3%的前五错误率。
比 ResNet-152 更深的 ResNet例如 ResNet-152使用略有不同的残差单元。它们不是使用两个具有 256 个特征图的 3×3 卷积层而是使用三个卷积层首先是一个只有 64 个特征图的 1×1 卷积层少了 4 倍它充当瓶颈层如前所述然后是一个具有 64 个特征图的 3×3 层最后是另一个具有 256 个特征图的 1×1 卷积层4 倍 64恢复原始深度。ResNet-152 包含 3 个输出 256 个映射的这样的 RU然后是 8 个输出 512 个映射的 RU一个令人惊叹的 36 个输出 1024 个映射的 RU最后是 3 个输出 2048 个映射的 RU。
Xception
值得注意的是 GoogLeNet 架构的另一个变种Xception代表Extreme Inception由 Keras 的作者 François Chollet 于 2016 年提出并在一个庞大的视觉任务3.5 亿张图片和 1.7 万个类别上明显优于 Inception-v3。就像 Inception-v4 一样它融合了 GoogLeNet 和 ResNet 的思想但是用一个特殊类型的层称为深度可分离卷积层或简称可分离卷积层替换了 inception 模块。这些层在一些 CNN 架构中之前已经被使用过但在 Xception 架构中并不像现在这样核心。常规卷积层使用滤波器试图同时捕捉空间模式例如椭圆和跨通道模式例如嘴鼻子眼睛脸而可分离卷积层则做出了空间模式和跨通道模式可以分别建模的强烈假设见图 14-20。因此它由两部分组成第一部分对每个输入特征图应用一个单一的空间滤波器然后第二部分专门寻找跨通道模式——这只是一个具有 1×1 滤波器的常规卷积层。
由于可分离卷积层每个输入通道只有一个空间滤波器所以应避免在通道较少的层之后使用它们比如输入层尽管图 14-20 中是这样的但那只是为了说明目的。因此Xception 架构以 2 个常规卷积层开始然后剩下的架构只使用可分离卷积总共 34 个再加上一些最大池化层和通常的最终层一个全局平均池化层和一个密集输出层。
你可能会想为什么 Xception 被认为是 GoogLeNet 的一个变种因为它根本不包含任何 inception 模块。嗯正如之前讨论的那样一个 inception 模块包含有 1×1 滤波器的卷积层这些滤波器专门寻找跨通道模式。然而位于它们之上的卷积层是常规卷积层既寻找空间模式又寻找跨通道模式。因此你可以将一个 inception 模块看作是一个常规卷积层同时考虑空间模式和跨通道模式和一个可分离卷积层分别考虑它们之间的中间层。实际上可分离卷积层通常表现更好。 图 14-20。深度可分离卷积层
提示
可分离卷积层使用更少的参数、更少的内存和更少的计算量比常规卷积层通常表现更好。考虑默认使用它们除了在通道较少的层之后比如输入通道。在 Keras 中只需使用SeparableConv2D代替Conv2D这是一个即插即用的替代。Keras 还提供了一个DepthwiseConv2D层实现深度可分离卷积层的第一部分即对每个输入特征图应用一个空间滤波器。
SENet
在 ILSVRC 2017 挑战中获胜的架构是Squeeze-and-Excitation Network (SENet)。这个架构扩展了现有的架构如 inception 网络和 ResNets并提升了它们的性能。这使得 SENet 以惊人的 2.25%的前五错误率赢得了比赛扩展版本的 inception 网络和 ResNets 分别称为SE-Inception和SE-ResNet。提升来自于 SENet 在原始架构的每个 inception 模块或残差单元中添加了一个小型神经网络称为SE 块如图 14-21 所示。 图 14-21. SE-Inception 模块左和 SE-ResNet 单元右
一个 SE 块分析其所附加的单元的输出专注于深度维度不寻找任何空间模式并学习哪些特征通常是最活跃的。然后它使用这些信息来重新校准特征映射如图 14-22 所示。例如一个 SE 块可能学习到嘴巴、鼻子和眼睛通常一起出现在图片中如果你看到嘴巴和鼻子你应该期望也看到眼睛。因此如果该块在嘴巴和鼻子特征映射中看到强烈的激活但在眼睛特征映射中只有轻微的激活它将增强眼睛特征映射更准确地说它将减少不相关的特征映射。如果眼睛有些混淆这种特征映射的重新校准将有助于解决模糊性。 图 14-22. 一个 SE 块执行特征映射重新校准
一个 SE 块由三层组成一个全局平均池化层一个使用 ReLU 激活函数的隐藏密集层以及一个使用 sigmoid 激活函数的密集输出层见图 14-23。 图 14-23. SE 块架构
与之前一样全局平均池化层计算每个特征映射的平均激活例如如果其输入包含 256 个特征映射它将输出 256 个数字表示每个滤波器的整体响应水平。接下来的层是“挤压”发生的地方这一层的神经元数量明显少于 256 个——通常比特征映射的数量少 16 倍例如16 个神经元——因此 256 个数字被压缩成一个小向量例如16 维。这是特征响应分布的低维向量表示即嵌入。这个瓶颈步骤迫使 SE 块学习特征组合的一般表示当我们讨论自编码器时我们将再次看到这个原则在第十七章中。最后输出层接受嵌入并输出一个包含每个特征映射的重新校准向量例如256 个每个数字在 0 到 1 之间。然后特征映射乘以这个重新校准向量因此不相关的特征具有低重新校准分数被缩小而相关的特征具有接近 1 的重新校准分数被保留。
其他值得注意的架构
还有许多其他 CNN 架构可以探索。以下是一些最值得注意的简要概述
ResNeXt²²
ResNeXt 改进了 ResNet 中的残差单元。而最佳 ResNet 模型中的残差单元只包含 3 个卷积层ResNeXt 的残差单元由许多并行堆栈组成例如32 个堆栈每个堆栈有 3 个卷积层。然而每个堆栈中的前两层只使用少量滤波器例如只有四个因此总参数数量与 ResNet 中的相同。然后所有堆栈的输出相加并将结果传递给下一个残差单元以及跳跃连接。
DenseNet²³
DenseNet 由几个密集块组成每个块由几个密集连接的卷积层组成。这种架构在使用相对较少的参数的同时实现了出色的准确性。什么是“密集连接”每一层的输出被馈送为同一块内每一层之后的每一层的输入。例如块中的第 4 层以该块中第 1、2 和 3 层的输出的深度级联作为输入。密集块之间由几个过渡层分隔。
MobileNet²⁴
MobileNets 是精简的模型旨在轻量且快速因此在移动和 Web 应用程序中很受欢迎。它们基于深度可分离卷积层类似于 Xception。作者提出了几个变体以牺牲一点准确性换取更快速和更小的模型。
CSPNet²⁵
交叉阶段部分网络CSPNet类似于 DenseNet但是每个密集块的部分输入直接连接到该块的输出而不经过该块。
EfficientNet²⁶
EfficientNet 可以说是这个列表中最重要的模型。作者提出了一种有效地扩展任何 CNN 的方法通过以原则性的方式同时增加深度层数、宽度每层的滤波器数量和分辨率输入图像的大小。这被称为复合缩放。他们使用神经架构搜索来找到一个适合 ImageNet 的缩小版本具有更小和更少的图像的良好架构然后使用复合缩放来创建这种架构的越来越大的版本。当 EfficientNet 模型推出时它们在所有计算预算中都远远超过了所有现有的模型并且它们仍然是当今最好的模型之一。
理解 EfficientNet 的复合缩放方法有助于更深入地理解 CNN特别是如果您需要扩展 CNN 架构。它基于计算预算的对数度量标记为ϕ如果您的计算预算翻倍则ϕ增加 1。换句话说用于训练的浮点运算数量与 2^(ϕ)成比例。您的 CNN 架构的深度、宽度和分辨率应分别按α(*ϕ*)、*β*(ϕ)和γ^(ϕ)缩放。因子α、β和γ必须大于 1且α β² γ²应接近 2。这些因子的最佳值取决于 CNN 的架构。为了找到 EfficientNet 架构的最佳值作者从一个小的基线模型EfficientNetB0开始固定ϕ 1然后简单地运行了一个网格搜索他们发现α 1.2β 1.1γ 1.1。然后他们使用这些因子创建了几个更大的架构命名为 EfficientNetB1 到 EfficientNetB7对应不断增加的ϕ值。
选择正确的 CNN 架构
有这么多 CNN 架构您如何选择最适合您项目的架构这取决于您最关心的是什么准确性模型大小例如用于部署到移动设备在 CPU 上的推理速度在 GPU 上的推理速度表 14-3 列出了目前在 Keras 中可用的最佳预训练模型您将在本章后面看到如何使用它们按模型大小排序。您可以在https://keras.io/api/applications找到完整列表。对于每个模型表格显示要使用的 Keras 类名在tf.keras.applications包中、模型的大小MB、在 ImageNet 数据集上的 Top-1 和 Top-5 验证准确率、参数数量百万以及在 CPU 和 GPU 上使用 32 张图像的推理时间毫秒使用性能较强的硬件。²⁷ 对于每列最佳值已突出显示。正如您所看到的通常较大的模型更准确但并非总是如此例如EfficientNetB2 在大小和准确性上均优于 InceptionV3。我之所以将 InceptionV3 保留在列表中是因为在 CPU 上它几乎比 EfficientNetB2 快一倍。同样InceptionResNetV2 在 CPU 上速度很快而 ResNet50V2 和 ResNet101V2 在 GPU 上速度极快。
表 14-3。Keras 中可用的预训练模型
类名大小MBTop-1 准确率Top-5 准确率参数CPUmsGPUmsMobileNetV21471.3%90.1%3.5M25.93.8MobileNet1670.4%89.5%4.3M22.63.4NASNetMobile2374.4%91.9%5.3M27.06.7EfficientNetB02977.1%93.3%5.3M46.04.9EfficientNetB13179.1%94.4%7.9M60.25.6EfficientNetB23680.1%94.9%9.2M80.86.5EfficientNetB34881.6%95.7%12.3M140.08.8EfficientNetB47582.9%96.4%19.5M308.315.1InceptionV39277.9%93.7%23.9M42.26.9ResNet50V29876.0%93.0%25.6M45.64.4EfficientNetB511883.6%96.7%30.6M579.225.3EfficientNetB616684.0%96.8%43.3M958.140.4ResNet101V217177.2%93.8%44.7M72.75.4InceptionResNetV221580.3%95.3%55.9M130.210.0EfficientNetB725684.3%97.0%66.7M1578.961.6
希望您喜欢这次对主要 CNN 架构的深入探讨现在让我们看看如何使用 Keras 实现其中一个。
使用 Keras 实现 ResNet-34 CNN
到目前为止大多数描述的 CNN 架构可以很自然地使用 Keras 实现尽管通常您会加载一个预训练网络正如您将看到的。为了说明这个过程让我们使用 Keras 从头开始实现一个 ResNet-34。首先我们将创建一个ResidualUnit层
DefaultConv2D partial(tf.keras.layers.Conv2D, kernel_size3, strides1,paddingsame, kernel_initializerhe_normal,use_biasFalse)class ResidualUnit(tf.keras.layers.Layer):def __init__(self, filters, strides1, activationrelu, **kwargs):super().__init__(**kwargs)self.activation tf.keras.activations.get(activation)self.main_layers [DefaultConv2D(filters, stridesstrides),tf.keras.layers.BatchNormalization(),self.activation,DefaultConv2D(filters),tf.keras.layers.BatchNormalization()]self.skip_layers []if strides 1:self.skip_layers [DefaultConv2D(filters, kernel_size1, stridesstrides),tf.keras.layers.BatchNormalization()]def call(self, inputs):Z inputsfor layer in self.main_layers:Z layer(Z)skip_Z inputsfor layer in self.skip_layers:skip_Z layer(skip_Z)return self.activation(Z skip_Z)正如您所看到的这段代码与图 14-19 非常接近。在构造函数中我们创建所有需要的层图中右侧的主要层和左侧的跳过层仅在步幅大于 1 时需要。然后在call()方法中我们让输入经过主要层和跳过层如果有的话然后我们添加两个输出并应用激活函数。
现在我们可以使用Sequential模型构建一个 ResNet-34因为它实际上只是一长串的层——现在我们有了ResidualUnit类可以将每个残差单元视为一个单独的层。代码与图 14-18 非常相似
model tf.keras.Sequential([DefaultConv2D(64, kernel_size7, strides2, input_shape[224, 224, 3]),tf.keras.layers.BatchNormalization(),tf.keras.layers.Activation(relu),tf.keras.layers.MaxPool2D(pool_size3, strides2, paddingsame),
])
prev_filters 64
for filters in [64] * 3 [128] * 4 [256] * 6 [512] * 3:strides 1 if filters prev_filters else 2model.add(ResidualUnit(filters, stridesstrides))prev_filters filtersmodel.add(tf.keras.layers.GlobalAvgPool2D())
model.add(tf.keras.layers.Flatten())
model.add(tf.keras.layers.Dense(10, activationsoftmax))这段代码中唯一棘手的部分是将ResidualUnit层添加到模型的循环正如前面解释的前 3 个 RU 有 64 个滤波器然后接下来的 4 个 RU 有 128 个滤波器依此类推。在每次迭代中当滤波器的数量与前一个 RU 中的数量相同时我们必须将步幅设置为 1否则我们将其设置为 2然后我们添加ResidualUnit最后我们更新prev_filters。
令人惊讶的是我们只需大约 40 行代码就可以构建赢得 ILSVRC 2015 挑战的模型这既展示了 ResNet 模型的优雅之处也展示了 Keras API 的表现力。实现其他 CNN 架构会有点长但并不难。不过Keras 内置了几种这些架构为什么不直接使用呢
使用 Keras 中的预训练模型
通常您不必手动实现标准模型如 GoogLeNet 或 ResNet因为在tf.keras.applications包中只需一行代码即可获得预训练网络。
例如您可以使用以下代码加载在 ImageNet 上预训练的 ResNet-50 模型
model tf.keras.applications.ResNet50(weightsimagenet)就这些这将创建一个 ResNet-50 模型并下载在 ImageNet 数据集上预训练的权重。要使用它您首先需要确保图像的尺寸正确。ResNet-50 模型期望 224×224 像素的图像其他模型可能期望其他尺寸如 299×299因此让我们使用 Keras 的Resizing层在第十三章中介绍来调整两个示例图像的大小在将它们裁剪到目标纵横比之后
images load_sample_images()[images]
images_resized tf.keras.layers.Resizing(height224, width224,crop_to_aspect_ratioTrue)(images)预训练模型假定图像以特定方式预处理。在某些情况下它们可能期望输入被缩放为 0 到 1或者从-1 到 1 等等。每个模型都提供了一个preprocess_input()函数您可以用它来预处理您的图像。这些函数假设原始像素值的范围是 0 到 255这在这里是正确的
inputs tf.keras.applications.resnet50.preprocess_input(images_resized)现在我们可以使用预训练模型进行预测 Y_proba model.predict(inputs)Y_proba.shape
(2, 1000)像往常一样输出Y_proba是一个矩阵每行代表一个图像每列代表一个类别在本例中有 1,000 个类别。如果您想显示前K个预测结果包括类别名称和每个预测类别的估计概率请使用decode_predictions()函数。对于每个图像它返回一个包含前K个预测结果的数组其中每个预测结果表示为一个包含类别标识符、其名称和相应置信度分数的数组
top_K tf.keras.applications.resnet50.decode_predictions(Y_proba, top3)
for image_index in range(len(images)):print(fImage #{image_index})for class_id, name, y_proba in top_K[image_index]:print(f {class_id} - {name:12s}{y_proba:.2%})输出如下所示
Image #0n03877845 - palace 54.69%n03781244 - monastery 24.72%n02825657 - bell_cote 18.55%
Image #1n04522168 - vase 32.66%n11939491 - daisy 17.81%n03530642 - honeycomb 12.06%正确的类别是 palace 和 dahlia因此模型对第一张图像是正确的但对第二张图像是错误的。然而这是因为 dahlia 不是 1,000 个 ImageNet 类之一。考虑到这一点vase 是一个合理的猜测也许这朵花在花瓶里daisy 也不是一个坏选择因为 dahlias 和 daisies 都属于同一菊科家族。
正如您所看到的使用预训练模型创建一个相当不错的图像分类器非常容易。正如您在表 14-3 中看到的tf.keras.applications中提供了许多其他视觉模型从轻量级快速模型到大型准确模型。
但是如果您想要为不属于 ImageNet 的图像类别使用图像分类器那么您仍然可以通过使用预训练模型来进行迁移学习获益。
用于迁移学习的预训练模型
如果您想构建一个图像分类器但没有足够的数据来从头开始训练它那么通常可以重用预训练模型的较低层正如我们在第十一章中讨论的那样。例如让我们训练一个模型来对花的图片进行分类重用一个预训练的 Xception 模型。首先我们将使用 TensorFlow Datasets在第十三章中介绍加载花卉数据集
import tensorflow_datasets as tfdsdataset, info tfds.load(tf_flowers, as_supervisedTrue, with_infoTrue)
dataset_size info.splits[train].num_examples # 3670
class_names info.features[label].names # [dandelion, daisy, ...]
n_classes info.features[label].num_classes # 5请注意您可以通过设置with_infoTrue来获取有关数据集的信息。在这里我们获取数据集的大小和类的名称。不幸的是只有一个train数据集没有测试集或验证集所以我们需要拆分训练集。让我们再次调用tfds.load()但这次将前 10%的数据集用于测试接下来的 15%用于验证剩下的 75%用于训练
test_set_raw, valid_set_raw, train_set_raw tfds.load(tf_flowers,split[train[:10%], train[10%:25%], train[25%:]],as_supervisedTrue)所有三个数据集都包含单独的图像。我们需要对它们进行批处理但首先我们需要确保它们都具有相同的大小否则批处理将失败。我们可以使用Resizing层来实现这一点。我们还必须调用tf.keras.applications.xception.preprocess_input()函数以适当地预处理图像以供 Xception 模型使用。最后我们还将对训练集进行洗牌并使用预取
batch_size 32
preprocess tf.keras.Sequential([tf.keras.layers.Resizing(height224, width224, crop_to_aspect_ratioTrue),tf.keras.layers.Lambda(tf.keras.applications.xception.preprocess_input)
])
train_set train_set_raw.map(lambda X, y: (preprocess(X), y))
train_set train_set.shuffle(1000, seed42).batch(batch_size).prefetch(1)
valid_set valid_set_raw.map(lambda X, y: (preprocess(X), y)).batch(batch_size)
test_set test_set_raw.map(lambda X, y: (preprocess(X), y)).batch(batch_size)现在每个批次包含 32 个图像所有图像都是 224×224 像素像素值范围从-1 到 1。完美
由于数据集不是很大一点数据增强肯定会有所帮助。让我们创建一个数据增强模型将其嵌入到我们的最终模型中。在训练期间它将随机水平翻转图像稍微旋转它们并调整对比度
data_augmentation tf.keras.Sequential([tf.keras.layers.RandomFlip(modehorizontal, seed42),tf.keras.layers.RandomRotation(factor0.05, seed42),tf.keras.layers.RandomContrast(factor0.2, seed42)
])提示
tf.keras.preprocessing.image.ImageDataGenerator类使从磁盘加载图像并以各种方式增强它们变得容易您可以移动每个图像旋转它重新缩放它水平或垂直翻转它剪切它或者应用任何您想要的转换函数。这对于简单的项目非常方便。然而tf.data 管道并不复杂通常更快。此外如果您有 GPU 并且将预处理或数据增强层包含在模型内部它们将在训练过程中受益于 GPU 加速。
接下来让我们加载一个在 ImageNet 上预训练的 Xception 模型。通过设置include_topFalse来排除网络的顶部。这将排除全局平均池化层和密集输出层。然后我们添加自己的全局平均池化层将其输入设置为基础模型的输出然后是一个具有每个类别一个单元的密集输出层使用 softmax 激活函数。最后我们将所有这些包装在一个 Keras Model中
base_model tf.keras.applications.xception.Xception(weightsimagenet,include_topFalse)
avg tf.keras.layers.GlobalAveragePooling2D()(base_model.output)
output tf.keras.layers.Dense(n_classes, activationsoftmax)(avg)
model tf.keras.Model(inputsbase_model.input, outputsoutput)如第十一章中解释的通常冻结预训练层的权重是一个好主意至少在训练开始时是这样的
for layer in base_model.layers:layer.trainable False警告
由于我们的模型直接使用基础模型的层而不是base_model对象本身设置base_model.trainableFalse不会产生任何效果。
最后我们可以编译模型并开始训练
optimizer tf.keras.optimizers.SGD(learning_rate0.1, momentum0.9)
model.compile(losssparse_categorical_crossentropy, optimizeroptimizer,metrics[accuracy])
history model.fit(train_set, validation_datavalid_set, epochs3)警告
如果你在 Colab 上运行请确保运行时正在使用 GPU选择运行时→“更改运行时类型”在“硬件加速器”下拉菜单中选择“GPU”然后点击保存。可以在没有 GPU 的情况下训练模型但速度会非常慢每个时期几分钟而不是几秒。
在训练模型几个时期后其验证准确率应该达到 80%以上然后停止提高。这意味着顶层现在已经训练得相当好我们准备解冻一些基础模型的顶层然后继续训练。例如让我们解冻第 56 层及以上的层这是 14 个残差单元中第 7 个的开始如果列出层名称你会看到
for layer in base_model.layers[56:]:layer.trainable True不要忘记在冻结或解冻层时编译模型。还要确保使用更低的学习率以避免破坏预训练权重
optimizer tf.keras.optimizers.SGD(learning_rate0.01, momentum0.9)
model.compile(losssparse_categorical_crossentropy, optimizeroptimizer,metrics[accuracy])
history model.fit(train_set, validation_datavalid_set, epochs10)这个模型应该在测试集上达到大约 92%的准确率在几分钟的训练时间内使用 GPU。如果调整超参数降低学习率并进行更长时间的训练应该能够达到 95%至 97%的准确率。有了这个你可以开始在自己的图像和类别上训练出色的图像分类器但计算机视觉不仅仅是分类。例如如果你还想知道图片中花朵的位置在哪里让我们现在来看看。
分类和定位
在图片中定位一个对象可以被表达为一个回归任务如第十章中讨论的预测一个对象周围的边界框一个常见的方法是预测对象中心的水平和垂直坐标以及它的高度和宽度。这意味着我们有四个数字要预测。对模型不需要太多改变我们只需要添加一个具有四个单元的第二个密集输出层通常在全局平均池化层之上并且可以使用 MSE 损失进行训练
base_model tf.keras.applications.xception.Xception(weightsimagenet,include_topFalse)
avg tf.keras.layers.GlobalAveragePooling2D()(base_model.output)
class_output tf.keras.layers.Dense(n_classes, activationsoftmax)(avg)
loc_output tf.keras.layers.Dense(4)(avg)
model tf.keras.Model(inputsbase_model.input,outputs[class_output, loc_output])
model.compile(loss[sparse_categorical_crossentropy, mse],loss_weights[0.8, 0.2], # depends on what you care most aboutoptimizeroptimizer, metrics[accuracy])但是现在我们有一个问题花卉数据集中没有围绕花朵的边界框。因此我们需要自己添加。这通常是机器学习项目中最困难和最昂贵的部分之一获取标签。花时间寻找合适的工具是个好主意。要用边界框注释图像您可能想使用开源图像标注工具如 VGG Image Annotator、LabelImg、OpenLabeler 或 ImgLab或者商业工具如 LabelBox 或 Supervisely。您还可以考虑众包平台如亚马逊机械土耳其如果您有大量图像需要注释。然而设置众包平台、准备发送给工人的表格、监督他们并确保他们产生的边界框的质量是好的这是相当多的工作所以确保这是值得的。Adriana Kovashka 等人撰写了一篇非常实用的论文关于计算机视觉中的众包。我建议您查看一下即使您不打算使用众包。如果只有几百张甚至几千张图像需要标记并且您不打算经常这样做最好自己做使用合适的工具只需要几天时间您还将更好地了解您的数据集和任务。
现在假设您已经为花卉数据集中的每个图像获得了边界框暂时假设每个图像只有一个边界框。然后您需要创建一个数据集其项目将是经过预处理的图像的批次以及它们的类标签和边界框。每个项目应该是一个形式为(images, (class_labels, bounding_boxes))的元组。然后您就可以开始训练您的模型
提示
边界框应该被归一化使得水平和垂直坐标以及高度和宽度的范围都在 0 到 1 之间。此外通常预测高度和宽度的平方根而不是直接预测高度和宽度这样对于大边界框的 10 像素误差不会受到与小边界框的 10 像素误差一样多的惩罚。
均方误差通常作为训练模型的成本函数效果相当不错但不是评估模型如何预测边界框的好指标。这方面最常见的度量是交并比IoU预测边界框与目标边界框之间的重叠区域除以它们的并集的面积参见图 14-24。在 Keras 中它由tf.keras.metrics.MeanIoU类实现。
对单个对象进行分类和定位是很好的但是如果图像中包含多个对象通常在花卉数据集中是这种情况怎么办呢 图 14-24。边界框的 IoU 度量
目标检测
在图像中对多个对象进行分类和定位的任务称为目标检测。直到几年前一种常见的方法是采用一个 CNN该 CNN 经过训练可以对图像中大致位于中心的单个对象进行分类和定位然后在图像上滑动这个 CNN并在每一步进行预测。通常CNN 被训练来预测不仅类别概率和边界框还有一个对象性分数这是估计的概率即图像确实包含一个位于中心附近的对象。这是一个二元分类输出它可以通过具有单个单元的密集输出层产生使用 sigmoid 激活函数并使用二元交叉熵损失进行训练。
注意
有时会添加一个“无对象”类而不是对象性分数但总的来说这并不起作用得很好最好分开回答“是否存在对象”和“对象的类型是什么”这两个问题。
这种滑动 CNN 方法在图 14-25 中有所说明。在这个例子中图像被切成了一个 5×7 的网格我们看到一个 CNN——厚厚的黑色矩形——在所有 3×3 区域上滑动并在每一步进行预测。 图 14-25。通过在图像上滑动 CNN 来检测多个对象
在这个图中CNN 已经对这三个 3×3 区域进行了预测 当查看左上角的 3×3 区域位于第二行第二列的红色阴影网格单元中心时它检测到了最左边的玫瑰。请注意预测的边界框超出了这个 3×3 区域的边界。这完全没问题即使 CNN 看不到玫瑰的底部部分它仍能合理猜测它可能在哪里。它还预测了类别概率给“玫瑰”类别一个很高的概率。最后它预测了一个相当高的物体得分因为边界框的中心位于中心网格单元内在这个图中物体得分由边界框的厚度表示。 当查看下一个 3×3 区域向右移动一个网格单元位于阴影蓝色正方形中心时它没有检测到任何位于该区域中心的花朵因此预测的物体得分非常低因此可以安全地忽略预测的边界框和类别概率。您可以看到预测的边界框也不好。 最后当查看下一个 3×3 区域再向右移动一个网格单元位于阴影绿色单元中心时它检测到了顶部的玫瑰尽管不完美这朵玫瑰没有很好地位于该区域中心因此预测的物体得分并不是很高。
您可以想象将 CNN 滑动到整个图像上会给您总共 15 个预测的边界框以 3×5 的网格组织每个边界框都伴随着其估计的类别概率和物体得分。由于对象的大小可能不同您可能希望再次在更大的 4×4 区域上滑动 CNN以获得更多的边界框。
这种技术相当简单但正如您所看到的它经常会在稍微不同的位置多次检测到相同的对象。需要一些后处理来摆脱所有不必要的边界框。一个常见的方法是称为非极大值抑制。下面是它的工作原理 首先摆脱所有物体得分低于某个阈值的边界框因为 CNN 认为该位置没有对象所以边界框是无用的。 找到具有最高物体得分的剩余边界框并摆脱所有与其重叠很多的其他剩余边界框例如IoU 大于 60%。例如在图 14-25 中具有最大物体得分的边界框是覆盖最左边的玫瑰的厚边界框。与这朵相同玫瑰接触的另一个边界框与最大边界框重叠很多因此我们将摆脱它尽管在这个例子中它在上一步中已经被移除。 重复步骤 2直到没有更多需要摆脱的边界框。
这种简单的目标检测方法效果相当不错但需要多次运行 CNN在这个例子中为 15 次因此速度相当慢。幸运的是有一种更快的方法可以在图像上滑动 CNN使用全卷积网络FCN。
全卷积网络
FCN 的概念最初是由 Jonathan Long 等人在2015 年的一篇论文中提出的用于语义分割根据对象所属的类别对图像中的每个像素进行分类的任务。作者指出可以用卷积层替换 CNN 顶部的密集层。为了理解这一点让我们看一个例子假设一个具有 200 个神经元的密集层位于一个输出 100 个大小为 7×7 的特征图的卷积层的顶部这是特征图的大小而不是卷积核的大小。每个神经元将计算来自卷积层的所有 100×7×7 激活的加权和加上一个偏置项。现在让我们看看如果我们用 200 个大小为 7×7 的滤波器和valid填充的卷积层来替换密集层会发生什么。这一层将输出 200 个大小为 1×1 的特征图因为卷积核恰好是输入特征图的大小而且我们使用valid填充。换句话说它将输出 200 个数字就像密集层一样如果你仔细观察卷积层执行的计算你会注意到这些数字将与密集层产生的数字完全相同。唯一的区别是密集层的输出是一个形状为[批量大小, 200]的张量而卷积层将输出一个形状为[批量大小, 1, 1, 200]的张量。
提示
要将密集层转换为卷积层卷积层中的滤波器数量必须等于密集层中的单元数量滤波器大小必须等于输入特征图的大小并且必须使用valid填充。步幅可以设置为 1 或更多稍后您将看到。
为什么这很重要嗯密集层期望特定的输入大小因为它对每个输入特征有一个权重而卷积层将愉快地处理任何大小的图像但是它期望其输入具有特定数量的通道因为每个卷积核包含每个输入通道的不同权重集。由于 FCN 只包含卷积层和具有相同属性的池化层它可以在任何大小的图像上进行训练和执行
例如假设我们已经训练了一个用于花卉分类和定位的 CNN。它是在 224×224 的图像上训练的并输出 10 个数字 输出 0 到 4 通过 softmax 激活函数发送这给出了类别概率每个类别一个。 输出 5 通过 sigmoid 激活函数发送这给出了物体得分。 输出 6 和 7 代表边界框的中心坐标它们也经过 sigmoid 激活函数以确保它们的范围在 0 到 1 之间。 最后输出 8 和 9 代表边界框的高度和宽度它们不经过任何激活函数以允许边界框延伸到图像的边界之外。
现在我们可以将 CNN 的密集层转换为卷积层。实际上我们甚至不需要重新训练它我们可以直接将密集层的权重复制到卷积层或者在训练之前我们可以将 CNN 转换为 FCN。
现在假设在输出层之前的最后一个卷积层也称为瓶颈层在网络输入 224×224 图像时输出 7×7 特征图参见图 14-26 的左侧。如果我们将 FCN 输入 448×448 图像参见图 14-26 的右侧瓶颈层现在将输出 14×14 特征图。³² 由于密集输出层被使用大小为 7×7 的 10 个滤波器的卷积层替换使用valid填充和步幅 1输出将由 10 个特征图组成每个大小为 8×8因为 14-718。换句话说FCN 将仅处理整个图像一次并输出一个 8×8 的网格其中每个单元包含 10 个数字5 个类别概率1 个物体性分数和 4 个边界框坐标。这就像拿着原始 CNN 并在图像上每行移动 8 步每列移动 8 步。为了可视化这一点想象将原始图像切成一个 14×14 的网格然后在这个网格上滑动一个 7×7 的窗口窗口将有 8×864 个可能的位置因此有 8×8 个预测。然而FCN 方法要更有效因为网络只看一次图像。事实上You Only Look OnceYOLO是一个非常流行的目标检测架构的名称我们将在接下来看一下。 图 14-26。相同的全卷积网络处理小图像左和大图像右
只看一次
YOLO 是由 Joseph Redmon 等人在2015 年的一篇论文中提出的一种快速准确的目标检测架构。³³ 它非常快速可以在视频上实时运行就像在 Redmon 的演示中看到的那样。YOLO 的架构与我们刚讨论的架构非常相似但有一些重要的区别 对于每个网格单元YOLO 只考虑边界框中心位于该单元内的对象。边界框坐标是相对于该单元的其中0, 0表示单元的左上角1, 1表示右下角。然而边界框的高度和宽度可能远远超出单元。 它为每个网格单元输出两个边界框而不仅仅是一个这使得模型能够处理两个对象非常接近以至于它们的边界框中心位于同一个单元格内的情况。每个边界框还附带自己的物体性分数。 YOLO 还为每个网格单元输出一个类别概率分布每个网格单元预测 20 个类别概率因为 YOLO 是在包含 20 个类别的 PASCAL VOC 数据集上训练的。这产生了一个粗糙的类别概率图。请注意模型为每个网格单元预测一个类别概率分布而不是每个边界框。然而可以在后处理期间估计每个边界框的类别概率方法是测量每个边界框与类别概率图中的每个类别匹配的程度。例如想象一张图片中有一个人站在一辆车前面。将会有两个边界框一个大的水平边界框用于车一个较小的垂直边界框用于人。这些边界框的中心可能位于同一个网格单元内。那么我们如何确定应该为每个边界框分配哪个类别呢嗯类别概率图将包含一个“车”类占主导地位的大区域里面将有一个“人”类占主导地位的较小区域。希望车的边界框大致匹配“车”区域而人的边界框大致匹配“人”区域这将允许为每个边界框分配正确的类别。
YOLO 最初是使用 Darknet 开发的Darknet 是由 Joseph Redmon 最初用 C 开发的开源深度学习框架但很快就被移植到了 TensorFlow、Keras、PyTorch 等。多年来不断改进包括 YOLOv2、YOLOv3 和 YOLO9000再次由 Joseph Redmon 等人开发、YOLOv4由 Alexey Bochkovskiy 等人开发、YOLOv5由 Glenn Jocher 开发和 PP-YOLO由 Xiang Long 等人开发。
每个版本都带来了一些令人印象深刻的速度和准确性改进使用了各种技术例如YOLOv3 在一定程度上提高了准确性部分原因在于锚先验利用了某些边界框形状比其他形状更有可能的事实这取决于类别例如人们倾向于具有垂直边界框而汽车通常不会。他们还增加了每个网格单元的边界框数量他们在不同数据集上进行了训练包含更多类别YOLO9000 的情况下最多达到 9,000 个类别按层次结构组织他们添加了跳跃连接以恢复在 CNN 中丢失的一些空间分辨率我们将很快讨论这一点当我们看语义分割时等等。这些模型也有许多变体例如 YOLOv4-tiny它经过优化可以在性能较弱的机器上进行训练并且可以运行得非常快每秒超过 1,000 帧但平均精度均值mAP略低。
许多目标检测模型都可以在 TensorFlow Hub 上找到通常具有预训练权重例如 YOLOv5、SSD、Faster R-CNN和EfficientDet。
SSD 和 EfficientDet 是“一次查看”检测模型类似于 YOLO。EfficientDet 基于 EfficientNet 卷积架构。Faster R-CNN 更复杂图像首先经过 CNN然后输出传递给区域建议网络RPN该网络提出最有可能包含对象的边界框然后为每个边界框运行分类器基于 CNN 的裁剪输出。使用这些模型的最佳起点是 TensorFlow Hub 的出色目标检测教程。
到目前为止我们只考虑在单个图像中检测对象。但是视频呢对象不仅必须在每一帧中被检测到还必须随着时间进行跟踪。现在让我们快速看一下目标跟踪。
目标跟踪
目标跟踪是一项具有挑战性的任务对象移动它们可能随着接近或远离摄像机而变大或变小它们的外观可能会随着转身或移动到不同的光照条件或背景而改变它们可能会被其他对象暂时遮挡等等。
最受欢迎的目标跟踪系统之一是DeepSORT。它基于经典算法和深度学习的组合 它使用Kalman 滤波器来估计给定先前检测的对象最可能的当前位置并假设对象倾向于以恒定速度移动。 它使用深度学习模型来衡量新检测和现有跟踪对象之间的相似度。 最后它使用匈牙利算法将新检测映射到现有跟踪对象或新跟踪对象该算法有效地找到最小化检测和跟踪对象预测位置之间距离的映射组合同时最小化外观差异。
例如想象一个红色球刚从相反方向移动的蓝色球上弹起。根据球的先前位置卡尔曼滤波器将预测球会相互穿过实际上它假设对象以恒定速度移动因此不会预期弹跳。如果匈牙利算法只考虑位置那么它会愉快地将新的检测结果映射到错误的球上就好像它们刚刚相互穿过并交换了颜色。但由于相似度度量匈牙利算法会注意到问题。假设球不太相似算法将新的检测结果映射到正确的球上。
提示
在 GitHub 上有一些 DeepSORT 的实现包括 YOLOv4 DeepSORT 的 TensorFlow 实现https://github.com/theAIGuysCode/yolov4-deepsort。
到目前为止我们已经使用边界框定位了对象。这通常足够了但有时您需要更精确地定位对象例如在视频会议中去除人物背后的背景。让我们看看如何降到像素级别。
语义分割
在语义分割中每个像素根据其所属对象的类别进行分类例如道路、汽车、行人、建筑等如图 14-27 所示。请注意同一类别的不同对象不被区分。例如分割图像右侧的所有自行车最终会成为一个大块像素。这项任务的主要困难在于当图像经过常规 CNN 时由于步幅大于 1 的层它们逐渐失去空间分辨率因此常规 CNN 可能只会知道图像左下角某处有一个人但不会比这更精确。 图 14-27. 语义分割
与目标检测一样有许多不同的方法来解决这个问题有些方法相当复杂。然而在 Jonathan Long 等人于 2015 年提出的一篇关于完全卷积网络的论文中提出了一个相当简单的解决方案。作者首先采用了一个预训练的 CNN并将其转换为 FCN。CNN 对输入图像应用了总步幅为 32即如果将所有大于 1 的步幅相加这意味着最后一层输出的特征图比输入图像小 32 倍。这显然太粗糙了因此他们添加了一个单一的上采样层将分辨率乘以 32。
有几种可用的上采样解决方案增加图像的大小例如双线性插值但这只能在×4 或×8 的范围内工作得相当好。相反他们使用转置卷积层³⁹这相当于首先通过插入空行和列全是零来拉伸图像然后执行常规卷积参见图 14-28。或者有些人更喜欢将其视为使用分数步幅的常规卷积层例如图 14-28 中的步幅为 1/2。转置卷积层可以初始化为执行接近线性插值的操作但由于它是一个可训练的层在训练期间会学习做得更好。在 Keras 中您可以使用Conv2DTranspose层。
注意
在转置卷积层中步幅定义了输入将被拉伸多少而不是滤波器步长的大小因此步幅越大输出就越大与卷积层或池化层不同。 图 14-28. 使用转置卷积层进行上采样
使用转置卷积层进行上采样是可以的但仍然太不精确。为了做得更好Long 等人从较低层添加了跳跃连接例如他们将输出图像上采样了 2 倍而不是 32 倍并添加了具有这种双倍分辨率的较低层的输出。然后他们将结果上采样了 16 倍导致总的上采样因子为 32参见图 14-29。这恢复了在较早的池化层中丢失的一些空间分辨率。在他们最好的架构中他们使用了第二个类似的跳跃连接以从更低的层中恢复更精细的细节。简而言之原始 CNN 的输出经过以下额外步骤上采样×2添加较低层的输出适当比例上采样×2添加更低层的输出最后上采样×8。甚至可以将缩放超出原始图像的大小这可以用于增加图像的分辨率这是一种称为超分辨率的技术。 图 14-29。跳跃层从较低层恢复一些空间分辨率
实例分割类似于语义分割但不是将同一类别的所有对象合并成一个大块而是将每个对象与其他对象区分开来例如它识别每辆自行车。例如由 Kaiming He 等人在2017 年的一篇论文中提出的Mask R-CNN架构通过为每个边界框额外生成一个像素掩码来扩展 Faster R-CNN 模型。因此您不仅可以获得围绕每个对象的边界框以及一组估计的类别概率还可以获得一个像素掩码该掩码定位属于对象的边界框中的像素。该模型可在 TensorFlow Hub 上获得预训练于 COCO 2017 数据集。尽管该领域发展迅速但如果您想尝试最新和最优秀的模型请查看https://paperswithcode.com的最新技术部分。
正如您所看到的深度计算机视觉领域广阔且快速发展每年都会涌现出各种架构。几乎所有这些架构都基于卷积神经网络但自 2020 年以来另一种神经网络架构已进入计算机视觉领域Transformer我们将在第十六章中讨论。过去十年取得的进步令人瞩目研究人员现在正专注于越来越困难的问题例如对抗学习试图使网络更具抗干扰性以防止被设计用来欺骗它的图像、可解释性了解网络为何做出特定分类、现实图像生成我们将在第十七章中回顾、单次学习一个系统只需看到一次对象就能识别该对象、预测视频中的下一帧、结合文本和图像任务等等。
现在进入下一章我们将看看如何使用递归神经网络和卷积神经网络处理序列数据例如时间序列。
练习 相比于完全连接的 DNNCNN 在图像分类方面有哪些优势 考虑一个由三个卷积层组成的 CNN每个卷积层都有 3×3 的内核步幅为 2且具有same填充。最底层输出 100 个特征映射中间层输出 200 个顶层输出 400 个。输入图像是 200×300 像素的 RGB 图像 CNN 中的参数总数是多少 如果我们使用 32 位浮点数那么在对单个实例进行预测时这个网络至少需要多少 RAM 当在一个包含 50 张图像的小批量上进行训练时呢 如果您的 GPU 在训练 CNN 时内存不足您可以尝试哪五种方法来解决这个问题 为什么要添加最大池化层而不是具有相同步幅的卷积层 何时要添加局部响应归一化层 您能否列出 AlexNet 相对于 LeNet-5 的主要创新GoogLeNet、ResNet、SENet、Xception 和 EfficientNet 的主要创新又是什么 什么是全卷积网络如何将密集层转换为卷积层 语义分割的主要技术难点是什么 从头开始构建自己的 CNN并尝试在 MNIST 上实现最高可能的准确性。 使用大型图像分类的迁移学习经过以下步骤 创建一个包含每类至少 100 张图像的训练集。例如您可以根据位置海滩、山脉、城市等对自己的图片进行分类或者您可以使用现有数据集例如来自 TensorFlow 数据集。 将其分为训练集、验证集和测试集。 构建输入管道应用适当的预处理操作并可选择添加数据增强。 在这个数据集上微调一个预训练模型。 按照 TensorFlow 的风格转移教程进行操作。这是使用深度学习生成艺术的有趣方式。
这些练习的解决方案可在本章笔记本的末尾找到网址为https://homl.info/colab3。
1David H. Hubel“不受限制的猫条纹皮层单元活动”《生理学杂志》147 卷1959 年226-238。
2David H. Hubel 和 Torsten N. Wiesel“猫条纹皮层单个神经元的感受野”《生理学杂志》148 卷1959 年574-591。
3David H. Hubel 和 Torsten N. Wiesel“猴子条纹皮层的感受野和功能结构”《生理学杂志》195 卷1968 年215-243。
4福岛邦彦“Neocognitron一种不受位置偏移影响的模式识别机制的自组织神经网络模型”《生物控制论》36 卷1980 年193-202。
5Yann LeCun 等人“基于梯度的学习应用于文档识别”《IEEE 会议录》86 卷第 11 期1998 年2278-2324。
6卷积是一种数学操作它将一个函数滑动到另一个函数上并测量它们逐点乘积的积分。它与傅里叶变换和拉普拉斯变换有深刻的联系并且在信号处理中被广泛使用。卷积层实际上使用交叉相关这与卷积非常相似有关更多详细信息请参见https://homl.info/76。
7为了产生相同大小的输出一个全连接层需要 200×150×100 个神经元每个神经元连接到所有 150×100×3 个输入。它将有 200×150×100×150×100×31≈1350 亿个参数
8在国际单位制SI中1 MB 1,000 KB 1,000×1,000 字节 1,000×1,000×8 位。而 1 MiB 1,024 kiB 1,024×1,024 字节。所以 12 MB ≈ 11.44 MiB。
9我们迄今讨论过的其他内核具有权重但池化内核没有它们只是无状态的滑动窗口。
10Yann LeCun 等人“基于梯度的学习应用于文档识别”《IEEE 会议录》86 卷第 11 期1998 年2278-2324。
11Alex Krizhevsky 等人“使用深度卷积神经网络对 ImageNet 进行分类”《第 25 届国际神经信息处理系统会议论文集》1 卷2012 年1097-1105。
¹² Matthew D. Zeiler 和 Rob Fergus“可视化和理解卷积网络”欧洲计算机视觉会议论文集2014818-833。
¹³ Christian Szegedy 等人“使用卷积深入”IEEE 计算机视觉和模式识别会议论文集20151-9。
¹⁴ 在 2010 年的电影Inception中角色们不断深入多层梦境因此这些模块的名称。
¹⁵ Karen Simonyan 和 Andrew Zisserman“用于大规模图像识别的非常深的卷积网络”arXiv 预印本 arXiv:1409.15562014。
¹⁶ Kaiming He 等人“用于图像识别的深度残差学习”arXiv 预印本 arXiv:1512:033852015。
¹⁷ 描述神经网络时通常只计算具有参数的层。
¹⁸ Christian Szegedy 等人“Inception-v4Inception-ResNet 和残差连接对学习的影响”arXiv 预印本 arXiv:1602.072612016。
¹⁹ François Chollet“Xception深度学习与深度可分离卷积”arXiv 预印本 arXiv:1610.023572016。
²⁰ 这个名称有时可能会有歧义因为空间可分离卷积通常也被称为“可分离卷积”。
²¹ Jie Hu 等人“挤压激励网络”IEEE 计算机视觉和模式识别会议论文集20187132-7141。
²² Saining Xie 等人“聚合残差变换用于深度神经网络”arXiv 预印本 arXiv:1611.054312016。
²³ Gao Huang 等人“密集连接卷积网络”arXiv 预印本 arXiv:1608.069932016。
²⁴ Andrew G. Howard 等人“MobileNets用于移动视觉应用的高效卷积神经网络”arXiv 预印本 arxiv:1704.048612017。
²⁵ Chien-Yao Wang 等人“CSPNet一种可以增强 CNN 学习能力的新骨干”arXiv 预印本 arXiv:1911.119292019。
²⁶ Mingxing Tan 和 Quoc V. Le“EfficientNet重新思考卷积神经网络的模型缩放”arXiv 预印本 arXiv:1905.119462019。
²⁷ 一款 92 核心的 AMD EPYC CPU带有 IBPB1.7 TB 的 RAM 和一款 Nvidia Tesla A100 GPU。
²⁸ 在 ImageNet 数据集中每个图像都映射到WordNet 数据集中的一个单词类别 ID 只是一个 WordNet ID。
²⁹ Adriana Kovashka 等人“计算机视觉中的众包”计算机图形学和视觉基础与趋势 10第 3 期2014177-243。
³⁰ Jonathan Long 等人“用于语义分割的全卷积网络”IEEE 计算机视觉和模式识别会议论文集20153431-3440。
³¹ 有一个小例外使用valid填充的卷积层会在输入大小小于核大小时报错。
³² 这假设我们在网络中只使用了same填充valid填充会减小特征图的大小。此外448 可以被 2 整除多次直到达到 7没有任何舍入误差。如果任何一层使用不同于 1 或 2 的步幅那么可能会有一些舍入误差因此特征图最终可能会变小。
³³ Joseph Redmon 等人“You Only Look Once: Unified, Real-Time Object Detection”《IEEE 计算机视觉与模式识别会议论文集》2016779–788。
³⁴ 您可以在 TensorFlow Models 项目中找到 YOLOv3、YOLOv4 及其微小变体网址为https://homl.info/yolotf。
³⁵ Wei Liu 等人“SSD: Single Shot Multibox Detector”《第 14 届欧洲计算机视觉会议论文集》1201621–37。
³⁶ Shaoqing Ren 等人“Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks”《第 28 届国际神经信息处理系统会议论文集》1201591–99。
³⁷ Mingxing Tan 等人“EfficientDet: Scalable and Efficient Object Detection”arXiv 预印本 arXiv:1911.090702019。
³⁸ Nicolai Wojke 等人“Simple Online and Realtime Tracking with a Deep Association Metric”arXiv 预印本 arXiv:1703.074022017。
³⁹ 这种类型的层有时被称为反卷积层但它不执行数学家所说的反卷积因此应避免使用这个名称。
⁴⁰ Kaiming He 等人“Mask R-CNN”arXiv 预印本 arXiv:1703.068702017。
第十五章使用 RNNs 和 CNNs 处理序列
预测未来是你经常做的事情无论是在结束朋友的句子还是预期早餐时咖啡的味道。在本章中我们将讨论循环神经网络RNNs-一类可以预测未来的网络嗯至少在一定程度上。RNNs 可以分析时间序列数据例如您网站上每日活跃用户的数量您所在城市的每小时温度您家每日的用电量附近汽车的轨迹等等。一旦 RNN 学习了数据中的过去模式它就能利用自己的知识来预测未来当然前提是过去的模式在未来仍然成立。
更一般地说RNNs 可以处理任意长度的序列而不是固定大小的输入。例如它们可以将句子、文档或音频样本作为输入使它们非常适用于自然语言处理应用如自动翻译或语音转文本。
在本章中我们将首先介绍 RNNs 的基本概念以及如何使用时间反向传播来训练它们。然后我们将使用它们来预测时间序列。在此过程中我们将研究常用的 ARMA 模型系列通常用于预测时间序列并将它们用作与我们的 RNNs 进行比较的基准。之后我们将探讨 RNNs 面临的两个主要困难 不稳定的梯度在第十一章中讨论可以通过各种技术来缓解包括循环丢失和循环层归一化。 非常有限的短期记忆可以使用 LSTM 和 GRU 单元进行扩展。
RNNs 并不是处理序列数据的唯一类型的神经网络。对于小序列常规的密集网络可以胜任而对于非常长的序列例如音频样本或文本卷积神经网络也可以表现得相当不错。我们将讨论这两种可能性并通过实现 WaveNet 来结束本章-一种能够处理数万个时间步的 CNN 架构。让我们开始吧
循环神经元和层
到目前为止我们已经专注于前馈神经网络其中激活仅在一个方向中流动从输入层到输出层。循环神经网络看起来非常像前馈神经网络只是它还有指向后方的连接。
让我们看看最简单的 RNN由一个神经元组成接收输入产生输出并将该输出发送回自身如图 15-1左所示。在每个时间步 t也称为帧这个循环神经元接收输入x[(t)]以及来自上一个时间步的自己的输出ŷ[(t–1)]。由于在第一个时间步没有先前的输出通常将其设置为 0。我们可以沿着时间轴表示这个小网络如图 15-1右所示。这被称为将网络展开到时间轴每个时间步表示一个循环神经元。 图 15-1. 一个循环神经元左在时间轴上展开右
您可以轻松创建一个循环神经元层。在每个时间步t每个神经元都接收来自输入向量x[(t)]和上一个时间步的输出向量ŷ[(t–1)]如图 15-2 所示。请注意现在输入和输出都是向量当只有一个神经元时输出是标量。 图 15-2. 一个循环神经元层左在时间轴上展开右
每个递归神经元有两组权重一组用于输入x[(t)]另一组用于上一个时间步的输出ŷ[(t–1)]。让我们称这些权重向量为w[x]和w[ŷ]。如果我们考虑整个递归层而不仅仅是一个递归神经元我们可以将所有权重向量放入两个权重矩阵W[x]和W[ŷ]。
整个递归层的输出向量可以按照你所期望的方式计算如方程 15-1 所示其中b是偏置向量ϕ(·)是激活函数例如ReLU¹。
方程 15-1. 单个实例的递归层输出
ŷ(t)ϕWx⊺x(t)Wŷ⊺ŷ(t-1)b
就像前馈神经网络一样我们可以通过将时间步t的所有输入放入输入矩阵X[(t)]参见方程 15-2来一次性计算整个小批量的递归层输出。
方程 15-2. 一次传递中递归神经元层的所有实例的输出[小批量
Ŷ (t) ϕ X (t) W x Ŷ (t-1) W ŷ b ϕ X (t) Ŷ (t-1) W b with W W x W ŷ
在这个方程中 Ŷ[(t)]是一个m×n[neurons]矩阵包含小批量中每个实例在时间步t的层输出m是小批量中实例的数量n[neurons]是神经元的数量。 X[(t)]是一个m×n[inputs]矩阵包含所有实例的输入n[inputs]是输入特征的数量。 W[x]是一个n[inputs]×n[neurons]矩阵包含当前时间步输入的连接权重。 W[ŷ]是一个n[neurons]×n[neurons]矩阵包含上一个时间步输出的连接权重。 b是一个大小为n[neurons]的向量包含每个神经元的偏置项。 权重矩阵W[x]和W[ŷ]通常垂直连接成一个形状为(n[inputs] n[neurons]) × n[neurons]的单个权重矩阵W参见方程 15-2 的第二行。 符号[X[(t)] Ŷ[(t–1)]]表示矩阵X[(t)]和Ŷ[(t–1)]的水平连接。
注意Ŷ[(t)]是X[(t)]和Ŷ[(t–1)]的函数X[(t–1)]和Ŷ[(t–2)]的函数X[(t–2)]和Ŷ[(t–3)]的函数依此类推。这使得Ŷ[(t)]是自时间t0即X[(0)], X[(1)], …, X[(t)]以来所有输入的函数。在第一个时间步骤t0 时没有先前的输出因此通常假定它们都是零。
记忆单元
由于递归神经元在时间步骤t的输出是前几个时间步骤的所有输入的函数因此可以说它具有一种记忆形式。在时间步骤之间保留一些状态的神经网络的一部分称为记忆单元或简称单元。单个递归神经元或一层递归神经元是一个非常基本的单元只能学习短模式通常约为 10 个步骤长但这取决于任务。在本章后面我们将看一些更复杂和强大的单元类型能够学习更长的模式大约长 10 倍但这也取决于任务。
时间步骤t时的单元状态表示为h[(t)]“h”代表“隐藏”是该时间步骤的一些输入和上一个时间步骤的状态的函数h[(t)] f(x[(t)], h[(t–1)]。在时间步骤t的输出表示为ŷ[(t)]也是前一个状态和当前输入的函数。在我们迄今讨论的基本单元的情况下输出只等于状态但在更复杂的单元中情况并非总是如此如图 15-3 所示。 图 15-3。单元的隐藏状态和输出可能不同
输入和输出序列
RNN 可以同时接受一系列输入并产生一系列输出参见图 15-4 左上方的网络。这种序列到序列网络对于预测时间序列非常有用例如您家每天的用电量您向其提供过去N天的数据并训练它输出将来一天的用电量即从N – 1 天前到明天。
或者您可以向网络提供一系列输入并忽略除最后一个之外的所有输出参见图 15-4 右上方的网络。这是一个序列到向量网络。例如您可以向网络提供与电影评论相对应的一系列单词网络将输出情感分数例如从 0 [讨厌]到 1 [喜爱]。
相反您可以在每个时间步骤反复向网络提供相同的输入向量并让它输出一个序列参见图 15-4 左下方的网络。这是一个向量到序列网络。例如输入可以是一幅图像或 CNN 的输出输出可以是该图像的标题。
最后您可以有一个序列到向量网络称为编码器后面是一个向量到序列网络称为解码器参见图 15-4 的右下方网络。例如这可以用于将一种语言的句子翻译成另一种语言。您将向网络提供一种语言的句子编码器将把这个句子转换成一个单一的向量表示然后解码器将把这个向量解码成另一种语言的句子。这种两步模型称为编码器-解码器²比尝试使用单个序列到序列的 RNN 实时翻译要好得多就像左上角表示的那种一个句子的最后几个词可能会影响翻译的前几个词因此您需要等到看完整个句子后再进行翻译。我们将在第十六章中介绍编码器-解码器的实现正如您将看到的它比图 15-4 所暗示的要复杂一些。 图 15-4. 序列到序列左上、序列到向量右上、向量到序列左下和编码器-解码器右下网络
这种多功能性听起来很有前途但如何训练循环神经网络呢
训练 RNNs
要训练 RNN关键是将其通过时间展开就像我们刚刚做的那样然后使用常规的反向传播参见图 15-5。这种策略称为通过时间的反向传播BPTT。
就像常规反向传播一样首先通过展开的网络进行第一次前向传递由虚线箭头表示。然后使用损失函数ℒ(Y[(0)], Y[(1)], …, Y[(T)]; Ŷ[(0)], Ŷ[(1)], …, Ŷ[(T)])评估输出序列其中Y[(i)]是第i个目标Ŷ[(i)]是第i个预测T是最大时间步长。请注意此损失函数可能会忽略一些输出。例如在序列到向量的 RNN 中除了最后一个输出之外所有输出都会被忽略。在图 15-5 中损失函数仅基于最后三个输出计算。然后该损失函数的梯度通过展开的网络向后传播由实线箭头表示。在这个例子中由于输出Ŷ[(0)]和Ŷ[(1)]没有用于计算损失梯度不会通过它们向后传播它们只会通过Ŷ[(2)]、Ŷ[(3)]和Ŷ[(4)]向后传播。此外由于在每个时间步骤中使用相同的参数W和b它们的梯度将在反向传播过程中被多次调整。一旦反向阶段完成并计算出所有梯度BPTT 可以执行梯度下降步骤来更新参数这与常规反向传播没有区别。 图 15-5. 通过时间反向传播
幸运的是Keras 会为您处理所有这些复杂性您将看到。但在我们到达那里之前让我们加载一个时间序列并开始使用传统工具进行分析以更好地了解我们正在处理的内容并获得一些基准指标。
预测时间序列
好了假设您刚被芝加哥交通管理局聘为数据科学家。您的第一个任务是构建一个能够预测明天公交和轨道乘客数量的模型。您可以访问自 2001 年以来的日常乘客数据。让我们一起看看您将如何处理这个问题。我们将从加载和清理数据开始
import pandas as pd
from pathlib import Pathpath Path(datasets/ridership/CTA_-_Ridership_-_Daily_Boarding_Totals.csv)
df pd.read_csv(path, parse_dates[service_date])
df.columns [date, day_type, bus, rail, total] # shorter names
df df.sort_values(date).set_index(date)
df df.drop(total, axis1) # no need for total, its just bus rail
df df.drop_duplicates() # remove duplicated months (2011-10 and 2014-07)我们加载 CSV 文件设置短列名按日期对行进行排序删除多余的total列并删除重复行。现在让我们看看前几行是什么样子的 df.head()day_type bus rail
date
2001-01-01 U 297192 126455
2001-01-02 W 780827 501952
2001-01-03 W 824923 536432
2001-01-04 W 870021 550011
2001-01-05 W 890426 557917在 2001 年 1 月 1 日芝加哥有 297,192 人乘坐公交车126,455 人乘坐火车。day_type列包含W表示工作日A表示周六U表示周日或假期。
现在让我们绘制 2019 年几个月的公交和火车乘客量数据看看它是什么样子的参见图 15-6
import matplotlib.pyplot as pltdf[2019-03:2019-05].plot(gridTrue, marker., figsize(8, 3.5))
plt.show()图 15-6。芝加哥的日常乘客量
请注意Pandas 在范围中包括起始月份和结束月份因此这将绘制从 3 月 1 日到 5 月 31 日的数据。这是一个时间序列在不同时间步长上具有值的数据通常在规则间隔上。更具体地说由于每个时间步长有多个值因此称为多变量时间序列。如果我们只看bus列那将是一个单变量时间序列每个时间步长有一个值。在处理时间序列时预测未来值即预测是最典型的任务这也是我们将在本章中重点关注的内容。其他任务包括插补填补缺失的过去值、分类、异常检测等。
查看图 15-6我们可以看到每周明显重复的类似模式。这被称为每周季节性。实际上在这种情况下季节性非常强通过简单地复制一周前的值来预测明天的乘客量将产生相当不错的结果。这被称为天真预测简单地复制过去的值来进行预测。天真预测通常是一个很好的基准有时甚至在某些情况下很难超越。
注意
一般来说天真预测意味着复制最新已知值例如预测明天与今天相同。然而在我们的情况下复制上周的值效果更好因为存在强烈的每周季节性。
为了可视化这些天真预测让我们将两个时间序列公交和火车以及相同时间序列向右移动一周即向右移动的时间序列叠加使用虚线。我们还将绘制两者之间的差异即时间t处的值减去时间t - 7 处的值这称为差分参见图 15-7
diff_7 df[[bus, rail]].diff(7)[2019-03:2019-05]fig, axs plt.subplots(2, 1, sharexTrue, figsize(8, 5))
df.plot(axaxs[0], legendFalse, marker.) # original time series
df.shift(7).plot(axaxs[0], gridTrue, legendFalse, linestyle:) # lagged
diff_7.plot(axaxs[1], gridTrue, marker.) # 7-day difference time series
plt.show()不错注意滞后时间序列如何紧密跟踪实际时间序列。当一个时间序列与其滞后版本相关联时我们说该时间序列是自相关的。正如您所看到的大多数差异都相当小除了五月底。也许那时有一个假期让我们检查day_type列 list(df.loc[2019-05-25:2019-05-27][day_type])
[A, U, U]图 15-7。与 7 天滞后时间序列叠加的时间序列顶部以及t和t - 7 之间的差异底部
事实上那时有一个长周末周一是阵亡将士纪念日假期。我们可以使用这一列来改进我们的预测但现在让我们只测量我们任意关注的三个月期间2019 年 3 月、4 月和 5 月的平均绝对误差以获得一个大致的概念 diff_7.abs().mean()
bus 43915.608696
rail 42143.271739
dtype: float64我们的天真预测得到大约 43,916 名公交乘客和约 42,143 名火车乘客的 MAE。一眼看去很难判断这是好是坏所以让我们将预测误差放入透视中通过将它们除以目标值来进行评估 targets df[[bus, rail]][2019-03:2019-05](diff_7 / targets).abs().mean()
bus 0.082938
rail 0.089948
dtype: float64我们刚刚计算的是平均绝对百分比误差MAPE看起来我们的天真预测为公交大约为 8.3%火车为 9.0%。有趣的是火车预测的 MAE 看起来比公交预测的稍好一些而 MAPE 则相反。这是因为公交乘客量比火车乘客量大因此自然预测误差也更大但当我们将误差放入透视时结果表明公交预测实际上略优于火车预测。
提示
MAE、MAPE 和 MSE 是评估预测的最常见指标之一。与往常一样选择正确的指标取决于任务。例如如果您的项目对大误差的影响是小误差的平方倍那么 MSE 可能更可取因为它会严厉惩罚大误差。
观察时间序列似乎没有明显的月度季节性但让我们检查一下是否存在年度季节性。我们将查看 2001 年至 2019 年的数据。为了减少数据窥探的风险我们暂时忽略更近期的数据。让我们为每个系列绘制一个 12 个月的滚动平均线以可视化长期趋势参见图 15-8
period slice(2001, 2019)
df_monthly df.resample(M).mean() # compute the mean for each month
rolling_average_12_months df_monthly[period].rolling(window12).mean()fig, ax plt.subplots(figsize(8, 4))
df_monthly[period].plot(axax, marker.)
rolling_average_12_months.plot(axax, gridTrue, legendFalse)
plt.show()图 15-8。年度季节性和长期趋势
是的确实存在一些年度季节性尽管比每周季节性更嘈杂对于铁路系列而言更为明显而不是公交系列我们看到每年大致相同日期出现高峰和低谷。让我们看看如果绘制 12 个月的差分会得到什么参见图 15-9
df_monthly.diff(12)[period].plot(gridTrue, marker., figsize(8, 3))
plt.show()图 15-9。12 个月的差分
注意差分不仅消除了年度季节性还消除了长期趋势。例如2016 年至 2019 年时间序列中存在的线性下降趋势在差分时间序列中变为大致恒定的负值。事实上差分是一种常用的技术用于消除时间序列中的趋势和季节性研究平稳时间序列更容易这意味着其统计特性随时间保持不变没有任何季节性或趋势。一旦您能够对差分时间序列进行准确的预测只需将先前减去的过去值添加回来就可以将其转换为实际时间序列的预测。
您可能会认为我们只是试图预测明天的乘客量因此长期模式比短期模式更不重要。您是对的但是通过考虑长期模式我们可能能够稍微提高性能。例如2017 年 10 月每日公交乘客量减少了约 2500 人这代表每周减少约 570 名乘客因此如果我们处于 2017 年 10 月底通过从上周复制数值减去 570来预测明天的乘客量是有道理的。考虑趋势将使您的平均预测略微更准确。
现在您熟悉了乘客量时间序列以及时间序列分析中一些最重要的概念包括季节性、趋势、差分和移动平均让我们快速看一下一个非常流行的统计模型家族通常用于分析时间序列。
ARMA 模型家族
我们将从上世纪 30 年代由赫尔曼·沃尔德Herman Wold开发的自回归移动平均ARMA模型开始它通过对滞后值的简单加权和添加移动平均来计算其预测非常类似我们刚刚讨论的。具体来说移动平均分量是通过最近几个预测误差的加权和来计算的。方程 15-3 展示了该模型如何进行预测。
第 15-3 方程。使用 ARMA 模型进行预测
y^(t)∑i1pαiy(t-i)∑i1qθiϵ(t-i)with ϵ(t)y(t)-y^(t)
在这个方程中 ŷ[(t)]是模型对时间步t的预测。 y[(t)]是时间步t的时间序列值。 第一个总和是时间序列过去p个值的加权和使用学习到的权重α[i]。数字p是一个超参数它决定模型应该查看过去多远。这个总和是模型的自回归组件它基于过去的值执行回归。 第二个总和是过去q个预测误差ε[(t)]的加权和使用学习到的权重θ[i]。数字q是一个超参数。这个总和是模型的移动平均组件。
重要的是这个模型假设时间序列是平稳的。如果不是那么差分可能有所帮助。在一个时间步上使用差分将产生时间序列的导数的近似值实际上它将给出每个时间步的系列斜率。这意味着它将消除任何线性趋势将其转换为一个常数值。例如如果你对系列[3, 5, 7, 9, 11]应用一步差分你会得到差分系列[2, 2, 2, 2]。
如果原始时间序列具有二次趋势而不是线性趋势那么一轮差分将不足够。例如系列[1, 4, 9, 16, 25, 36]经过一轮差分后变为[3, 5, 7, 9, 11]但如果你再进行第二轮差分你会得到[2, 2, 2, 2]。因此进行两轮差分将消除二次趋势。更一般地连续运行d轮差分计算时间序列的d阶导数的近似值因此它将消除多项式趋势直到d阶。这个超参数d被称为积分阶数。
差分是 1970 年由乔治·博克斯和格威林·詹金斯在他们的书《时间序列分析》Wiley中介绍的自回归积分移动平均ARIMA模型的核心贡献这个模型运行d轮差分使时间序列更平稳然后应用常规 ARMA 模型。在进行预测时它使用这个 ARMA 模型然后将差分减去的项加回来。
ARMA 家族的最后一个成员是季节性 ARIMASARIMA模型它以与 ARIMA 相同的方式对时间序列建模但另外还为给定频率例如每周建模一个季节性组件使用完全相同的 ARIMA 方法。它总共有七个超参数与 ARIMA 相同的p、d和q超参数再加上额外的P、D和Q超参数来建模季节性模式最后是季节性模式的周期标记为s。超参数P、D和Q就像p、d和q一样但它们用于模拟时间序列在t – s、t – 2s、t – 3s等时刻。
让我们看看如何将 SARIMA 模型拟合到铁路时间序列并用它来预测明天的乘客量。我们假设今天是 2019 年 5 月的最后一天我们想要预测“明天”也就是 2019 年 6 月 1 日的铁路乘客量。为此我们可以使用statsmodels库其中包含许多不同的统计模型包括由ARIMA类实现的 ARMA 模型及其变体
from statsmodels.tsa.arima.model import ARIMAorigin, today 2019-01-01, 2019-05-31
rail_series df.loc[origin:today][rail].asfreq(D)
model ARIMA(rail_series,order(1, 0, 0),seasonal_order(0, 1, 1, 7))
model model.fit()
y_pred model.forecast() # returns 427,758.6在这个代码示例中 我们首先导入ARIMA类然后我们从 2019 年初开始到“今天”获取铁路乘客数据并使用asfreq(D)将时间序列的频率设置为每天在这种情况下这不会改变数据因为它已经是每天的但如果没有这个ARIMA类将不得不猜测频率并显示警告。 接下来我们创建一个ARIMA实例将所有数据传递到“今天”并设置模型超参数order(1, 0, 0)表示p1d0q0seasonal_order(0, 1, 1, 7)表示P0D1Q1s7。请注意statsmodels API 与 Scikit-Learn 的 API 有些不同因为我们在构建时将数据传递给模型而不是将数据传递给fit()方法。 接下来我们拟合模型并用它为“明天”也就是 2019 年 6 月 1 日做出预测。
预测为 427,759 名乘客而实际上有 379,044 名。哎呀我们偏差 12.9%——这相当糟糕。实际上这比天真预测稍微糟糕天真预测为 426,932偏差为 12.6%。但也许那天我们只是运气不好为了检查这一点我们可以在循环中运行相同的代码为三月、四月和五月的每一天进行预测并计算该期间的平均绝对误差
origin, start_date, end_date 2019-01-01, 2019-03-01, 2019-05-31
time_period pd.date_range(start_date, end_date)
rail_series df.loc[origin:end_date][rail].asfreq(D)
y_preds []
for today in time_period.shift(-1):model ARIMA(rail_series[origin:today], # train on data up to todayorder(1, 0, 0),seasonal_order(0, 1, 1, 7))model model.fit() # note that we retrain the model every day!y_pred model.forecast()[0]y_preds.append(y_pred)y_preds pd.Series(y_preds, indextime_period)
mae (y_preds - rail_series[time_period]).abs().mean() # returns 32,040.7啊好多了平均绝对误差约为 32,041比我们用天真预测得到的平均绝对误差42,143显著低。因此虽然模型并不完美但平均而言仍然远远超过天真预测。
此时您可能想知道如何为 SARIMA 模型选择良好的超参数。有几种方法但最简单的方法是粗暴的方法进行网格搜索。对于要评估的每个模型即每个超参数组合您可以运行前面的代码示例仅更改超参数值。通常p、q、P和Q值较小通常为 0 到 2有时可达 5 或 6d和D通常为 0 或 1有时为 2。至于s它只是主要季节模式的周期在我们的情况下是 7因为有强烈的每周季节性。具有最低平均绝对误差的模型获胜。当然如果它更符合您的业务目标您可以用另一个指标替换平均绝对误差。就是这样
为机器学习模型准备数据
现在我们有了两个基线天真预测和 SARIMA让我们尝试使用迄今为止涵盖的机器学习模型来预测这个时间序列首先从基本的线性模型开始。我们的目标是根据过去 8 周56 天的数据来预测明天的乘客量。因此我们模型的输入将是序列通常是生产中的每天一个序列每个序列包含从时间步t - 55 到t的 56 个值。对于每个输入序列模型将输出一个值时间步t 1 的预测。
但我们将使用什么作为训练数据呢嗯这就是诀窍我们将使用过去的每个 56 天窗口作为训练数据每个窗口的目标将是紧随其后的值。
Keras 实际上有一个很好的实用函数称为tf.keras.utils.timeseries_dataset_from_array()帮助我们准备训练集。它以时间序列作为输入并构建一个 tf.data.Dataset在第十三章中介绍包含所需长度的所有窗口以及它们对应的目标。以下是一个示例它以包含数字 0 到 5 的时间序列为输入并创建一个包含所有长度为 3 的窗口及其对应目标的数据集分组成大小为 2 的批次
import tensorflow as tfmy_series [0, 1, 2, 3, 4, 5]
my_dataset tf.keras.utils.timeseries_dataset_from_array(my_series,targetsmy_series[3:], # the targets are 3 steps into the futuresequence_length3,batch_size2
)让我们检查一下这个数据集的内容 list(my_dataset)
[(tf.Tensor: shape(2, 3), dtypeint32, numpyarray([[0, 1, 2],[1, 2, 3]], dtypeint32),tf.Tensor: shape(2,), dtypeint32, numpyarray([3, 4], dtypeint32)),(tf.Tensor: shape(1, 3), dtypeint32, numpyarray([[2, 3, 4]], dtypeint32),tf.Tensor: shape(1,), dtypeint32, numpyarray([5], dtypeint32))]数据集中的每个样本是长度为 3 的窗口以及其对应的目标即窗口后面的值。窗口是[0, 1, 2][1, 2, 3]和[2, 3, 4]它们各自的目标是 34 和 5。由于总共有三个窗口不是批次大小的倍数最后一个批次只包含一个窗口而不是两个。
另一种获得相同结果的方法是使用 tf.data 的Dataset类的window()方法。这更复杂但它给了您完全的控制这将在本章后面派上用场让我们看看它是如何工作的。window()方法返回一个窗口数据集的数据集 for window_dataset in tf.data.Dataset.range(6).window(4, shift1):
... for element in window_dataset:
... print(f{element}, end )
... print()
...
0 1 2 3
1 2 3 4
2 3 4 5
3 4 5
4 5
5在这个例子中数据集包含六个窗口每个窗口相对于前一个窗口向前移动一个步骤最后三个窗口较小因为它们已经到达系列的末尾。通常情况下您会希望通过向window()方法传递drop_remainderTrue来摆脱这些较小的窗口。
window()方法返回一个嵌套数据集类似于一个列表的列表。当您想要通过调用其数据集方法例如对它们进行洗牌或分批处理来转换每个窗口时这将非常有用。然而我们不能直接使用嵌套数据集进行训练因为我们的模型将期望张量作为输入而不是数据集。
因此我们必须调用flat_map()方法它将嵌套数据集转换为平坦数据集包含张量而不是数据集。例如假设{1, 2, 3}表示包含张量 1、2 和 3 序列的数据集。如果展平嵌套数据集{{1, 2}, {3, 4, 5, 6}}您将得到平坦数据集{1, 2, 3, 4, 5, 6}。
此外flat_map()方法接受一个函数作为参数允许您在展平之前转换嵌套数据集中的每个数据集。例如如果您将函数lambda ds: ds.batch(2)传递给flat_map()那么它将把嵌套数据集{{1, 2}, {3, 4, 5, 6}}转换为平坦数据集{[1, 2], [3, 4], [5, 6]}这是一个包含 3 个大小为 2 的张量的数据集。
考虑到这一点我们准备对数据集进行展平处理 dataset tf.data.Dataset.range(6).window(4, shift1, drop_remainderTrue)dataset dataset.flat_map(lambda window_dataset: window_dataset.batch(4))for window_tensor in dataset:
... print(f{window_tensor})
...
[0 1 2 3]
[1 2 3 4]
[2 3 4 5]由于每个窗口数据集恰好包含四个项目对窗口调用batch(4)会产生一个大小为 4 的单个张量。太棒了现在我们有一个包含连续窗口的数据集表示为张量。让我们创建一个小助手函数以便更容易地从数据集中提取窗口
def to_windows(dataset, length):dataset dataset.window(length, shift1, drop_remainderTrue)return dataset.flat_map(lambda window_ds: window_ds.batch(length))最后一步是使用map()方法将每个窗口拆分为输入和目标。我们还可以将生成的窗口分组成大小为 2 的批次 dataset to_windows(tf.data.Dataset.range(6), 4) # 3 inputs 1 target 4dataset dataset.map(lambda window: (window[:-1], window[-1]))list(dataset.batch(2))
[(tf.Tensor: shape(2, 3), dtypeint64, numpyarray([[0, 1, 2],[1, 2, 3]]),tf.Tensor: shape(2,), dtypeint64, numpyarray([3, 4])),(tf.Tensor: shape(1, 3), dtypeint64, numpyarray([[2, 3, 4]]),tf.Tensor: shape(1,), dtypeint64, numpyarray([5]))]正如您所看到的我们现在得到了与之前使用timeseries_dataset_from_array()函数相同的输出稍微费劲一些但很快就会值得。
现在在开始训练之前我们需要将数据分为训练期、验证期和测试期。我们现在将专注于铁路乘客量。我们还将通过一百万分之一的比例缩小它以确保值接近 0-1 范围这与默认的权重初始化和学习率很好地配合
rail_train df[rail][2016-01:2018-12] / 1e6
rail_valid df[rail][2019-01:2019-05] / 1e6
rail_test df[rail][2019-06:] / 1e6注意
处理时间序列时通常希望按时间划分。但在某些情况下您可能能够沿其他维度划分这将使您有更长的时间段进行训练。例如如果您有关于 2001 年至 2019 年间 10,000 家公司财务状况的数据您可能能够将这些数据分割到不同的公司。然而很可能这些公司中的许多将强相关例如整个经济部门可能一起上涨或下跌如果在训练集和测试集中有相关的公司那么您的测试集将不会那么有用因为其泛化误差的度量将是乐观偏倚的。
接下来让我们使用timeseries_dataset_from_array()为训练和验证创建数据集。由于梯度下降期望训练集中的实例是独立同分布的IID正如我们在第四章中看到的那样我们必须设置参数shuffleTrue来对训练窗口进行洗牌但不洗牌其中的内容
seq_length 56
train_ds tf.keras.utils.timeseries_dataset_from_array(rail_train.to_numpy(),targetsrail_train[seq_length:],sequence_lengthseq_length,batch_size32,shuffleTrue,seed42
)
valid_ds tf.keras.utils.timeseries_dataset_from_array(rail_valid.to_numpy(),targetsrail_valid[seq_length:],sequence_lengthseq_length,batch_size32
)现在我们已经准备好构建和训练任何回归模型了
使用线性模型进行预测
让我们首先尝试一个基本的线性模型。我们将使用 Huber 损失通常比直接最小化 MAE 效果更好如第十章中讨论的那样。我们还将使用提前停止
tf.random.set_seed(42)
model tf.keras.Sequential([tf.keras.layers.Dense(1, input_shape[seq_length])
])
early_stopping_cb tf.keras.callbacks.EarlyStopping(monitorval_mae, patience50, restore_best_weightsTrue)
opt tf.keras.optimizers.SGD(learning_rate0.02, momentum0.9)
model.compile(losstf.keras.losses.Huber(), optimizeropt, metrics[mae])
history model.fit(train_ds, validation_datavalid_ds, epochs500,callbacks[early_stopping_cb])该模型达到了约 37,866 的验证 MAE结果可能有所不同。这比天真的预测要好但比 SARIMA 模型要差。⁵
我们能用 RNN 做得更好吗让我们看看
使用简单 RNN 进行预测
让我们尝试最基本的 RNN其中包含一个具有一个循环神经元的单个循环层就像我们在图 15-1 中看到的那样
model tf.keras.Sequential([tf.keras.layers.SimpleRNN(1, input_shape[None, 1])
])Keras 中的所有循环层都期望形状为[批量大小时间步长维度]的 3D 输入其中维度对于单变量时间序列为 1对于多变量时间序列为更多。请记住input_shape参数忽略第一个维度即批量大小由于循环层可以接受任意长度的输入序列因此我们可以将第二个维度设置为None表示“任意大小”。最后由于我们处理的是单变量时间序列我们需要最后一个维度的大小为 1。这就是为什么我们指定输入形状为[None, 1]它表示“任意长度的单变量序列”。请注意数据集实际上包含形状为[批量大小时间步长]的输入因此我们缺少最后一个维度大小为 1但在这种情况下Keras 很友好地为我们添加了它。
这个模型的工作方式与我们之前看到的完全相同初始状态h[(init)]设置为 0并传递给一个单个的循环神经元以及第一个时间步的值x[(0)]。神经元计算这些值加上偏置项的加权和并使用默认的双曲正切函数对结果应用激活函数。结果是第一个输出y[0]。在简单 RNN 中这个输出也是新状态h[0]。这个新状态传递给相同的循环神经元以及下一个输入值x[(1)]并且这个过程重复直到最后一个时间步。最后该层只输出最后一个值在我们的情况下序列长度为 56 步因此最后一个值是y[55]。所有这些都同时为批次中的每个序列执行本例中有 32 个序列。
注意
默认情况下Keras 中的循环层只返回最终输出。要使它们返回每个时间步的一个输出您必须设置return_sequencesTrue如您将看到的。
这就是我们的第一个循环模型这是一个序列到向量的模型。由于只有一个输出神经元输出向量的大小为 1。
现在如果您编译、训练和评估这个模型就像之前的模型一样您会发现它一点用也没有其验证 MAE 大于 100,000哎呀。这是可以预料到的有两个原因 该模型只有一个循环神经元因此在每个时间步进行预测时它只能使用当前时间步的输入值和上一个时间步的输出值。这不足以进行预测换句话说RNN 的记忆极为有限只是一个数字它的先前输出。让我们来数一下这个模型有多少参数由于只有一个循环神经元只有两个输入值整个模型只有三个参数两个权重加上一个偏置项。这对于这个时间序列来说远远不够。相比之下我们之前的模型可以一次查看所有 56 个先前的值并且总共有 57 个参数。 时间序列包含的值从 0 到约 1.4但由于默认激活函数是 tanh循环层只能输出-1 到1 之间的值。它无法预测 1.0 到 1.4 之间的值。
让我们解决这两个问题我们将创建一个具有更大的循环层的模型其中包含 32 个循环神经元并在其顶部添加一个密集的输出层其中只有一个输出神经元没有激活函数。循环层将能够在一个时间步到下一个时间步传递更多信息而密集输出层将把最终输出从 32 维投影到 1 维没有任何值范围约束
univar_model tf.keras.Sequential([tf.keras.layers.SimpleRNN(32, input_shape[None, 1]),tf.keras.layers.Dense(1) # no activation function by default
])现在如果您像之前那样编译、拟合和评估这个模型您会发现其验证 MAE 达到了 27,703。这是迄今为止我们训练过的最佳模型甚至击败了 SARIMA 模型我们做得相当不错
提示
我们只对时间序列进行了归一化没有去除趋势和季节性但模型仍然表现良好。这很方便因为这样可以快速搜索有前途的模型而不用太担心预处理。然而为了获得最佳性能您可能希望尝试使时间序列更加平稳例如使用差分。
使用深度 RNN 进行预测
通常会堆叠多层单元如图 15-10 所示。这给你一个深度 RNN。 图 15-10. 深度 RNN左在时间轴上展开右
使用 Keras 实现深度 RNN 很简单只需堆叠循环层。在下面的示例中我们使用三个SimpleRNN层但我们也可以使用任何其他类型的循环层如LSTM层或GRU层我们将很快讨论。前两个是序列到序列层最后一个是序列到向量层。最后Dense层生成模型的预测您可以将其视为向量到向量层。因此这个模型就像图 15-10 中表示的模型一样只是忽略了Ŷ[(0)]到Ŷ[(t–1_)]的输出并且在Ŷ[(t)]之上有一个密集层输出实际预测
deep_model tf.keras.Sequential([tf.keras.layers.SimpleRNN(32, return_sequencesTrue, input_shape[None, 1]),tf.keras.layers.SimpleRNN(32, return_sequencesTrue),tf.keras.layers.SimpleRNN(32),tf.keras.layers.Dense(1)
])警告
确保对所有循环层设置return_sequencesTrue除非您只关心最后的输出最后一个循环层除外。如果您忘记为一个循环层设置此参数它将输出一个 2D 数组其中仅包含最后一个时间步的输出而不是包含所有时间步输出的 3D 数组。下一个循环层将抱怨您没有以预期的 3D 格式提供序列。
如果您训练和评估这个模型您会发现它的 MAE 约为 31,211。这比两个基线都要好但它并没有击败我们的“更浅”的 RNN。看起来这个 RNN 对我们的任务来说有点太大了。
多变量时间序列预测
神经网络的一个很大的优点是它们的灵活性特别是它们几乎不需要改变架构就可以处理多变量时间序列。例如让我们尝试使用公交和铁路数据作为输入来预测铁路时间序列。事实上让我们也加入日期类型由于我们总是可以提前知道明天是工作日、周末还是假日我们可以将日期类型系列向未来推移一天这样模型就会将明天的日期类型作为输入。为简单起见我们将使用 Pandas 进行此处理
df_mulvar df[[bus, rail]] / 1e6 # use both bus rail series as input
df_mulvar[next_day_type] df[day_type].shift(-1) # we know tomorrows type
df_mulvar pd.get_dummies(df_mulvar) # one-hot encode the day type现在 df_mulvar 是一个包含五列的 DataFrame公交和铁路数据以及包含下一天类型的独热编码的三列请记住有三种可能的日期类型W、A 和 U。接下来我们可以像之前一样继续。首先我们将数据分为三个时期用于训练、验证和测试
mulvar_train df_mulvar[2016-01:2018-12]
mulvar_valid df_mulvar[2019-01:2019-05]
mulvar_test df_mulvar[2019-06:]然后我们创建数据集
train_mulvar_ds tf.keras.utils.timeseries_dataset_from_array(mulvar_train.to_numpy(), # use all 5 columns as inputtargetsmulvar_train[rail][seq_length:], # forecast only the rail series[...] # the other 4 arguments are the same as earlier
)
valid_mulvar_ds tf.keras.utils.timeseries_dataset_from_array(mulvar_valid.to_numpy(),targetsmulvar_valid[rail][seq_length:],[...] # the other 2 arguments are the same as earlier
)最后我们创建 RNN
mulvar_model tf.keras.Sequential([tf.keras.layers.SimpleRNN(32, input_shape[None, 5]),tf.keras.layers.Dense(1)
])请注意与我们之前构建的 univar_model RNN 唯一的区别是输入形状在每个时间步骤模型现在接收五个输入而不是一个。这个模型实际上达到了 22,062 的验证 MAE。现在我们取得了很大的进展
事实上让 RNN 预测公交和铁路乘客量并不太难。您只需要在创建数据集时更改目标将其设置为训练集的 mulvar_train[[bus, rail]][seq_length:]验证集的 mulvar_valid[[bus, rail]][seq_length:]。您还必须在输出 Dense 层中添加一个额外的神经元因为现在它必须进行两次预测一次是明天的公交乘客量另一次是铁路乘客量。就是这样
正如我们在第十章中讨论的那样对于多个相关任务使用单个模型通常比为每个任务使用单独的模型效果更好因为为一个任务学习的特征可能对其他任务也有用而且因为在多个任务中表现良好可以防止模型过拟合这是一种正则化形式。然而这取决于任务在这种特殊情况下同时预测公交和铁路乘客量的多任务 RNN 并不像专门预测其中一个的模型表现得那么好使用所有五列作为输入。尽管如此它对铁路的验证 MAE 达到了 25,330对公交达到了 26,369这还是相当不错的。
提前预测多个时间步
到目前为止我们只预测了下一个时间步的值但我们也可以通过适当更改目标来预测几个步骤之后的值例如要预测两周后的乘客量我们只需将目标更改为比 1 天后提前 14 天的值。但是如果我们想预测接下来的 14 个值呢
第一种选择是取我们之前为铁路时间序列训练的 univar_model RNN让它预测下一个值并将该值添加到输入中就好像预测的值实际上已经发生了然后我们再次使用模型来预测下一个值依此类推如下面的代码所示
import numpy as npX rail_valid.to_numpy()[np.newaxis, :seq_length, np.newaxis]
for step_ahead in range(14):y_pred_one univar_model.predict(X)X np.concatenate([X, y_pred_one.reshape(1, 1, 1)], axis1)在这段代码中我们取验证期间前 56 天的铁路乘客量并将数据转换为形状为 [1, 56, 1] 的 NumPy 数组请记住循环层期望 3D 输入。然后我们重复使用模型来预测下一个值并将每个预测附加到输入系列中沿着时间轴axis1。生成的预测在图 15-11 中绘制。
警告
如果模型在一个时间步骤上出现错误那么接下来的时间步骤的预测也会受到影响错误往往会累积。因此最好只在少数步骤中使用这种技术。 图 15-11。提前 14 步预测一次预测一步
第二个选项是训练一个 RNN 一次性预测接下来的 14 个值。我们仍然可以使用一个序列到向量模型但它将输出 14 个值而不是 1。然而我们首先需要改变目标使其成为包含接下来 14 个值的向量。为此我们可以再次使用timeseries_dataset_from_array()但这次要求它创建没有目标targetsNone的数据集并且具有更长的序列长度为seq_length 14。然后我们可以使用数据集的map()方法对每个序列批次应用自定义函数将其分成输入和目标。在这个例子中我们使用多变量时间序列作为输入使用所有五列并预测未来 14 天的铁路乘客量。
def split_inputs_and_targets(mulvar_series, ahead14, target_col1):return mulvar_series[:, :-ahead], mulvar_series[:, -ahead:, target_col]ahead_train_ds tf.keras.utils.timeseries_dataset_from_array(mulvar_train.to_numpy(),targetsNone,sequence_lengthseq_length 14,[...] # the other 3 arguments are the same as earlier
).map(split_inputs_and_targets)
ahead_valid_ds tf.keras.utils.timeseries_dataset_from_array(mulvar_valid.to_numpy(),targetsNone,sequence_lengthseq_length 14,batch_size32
).map(split_inputs_and_targets)现在我们只需要将输出层的单元数从 1 增加到 14
ahead_model tf.keras.Sequential([tf.keras.layers.SimpleRNN(32, input_shape[None, 5]),tf.keras.layers.Dense(14)
])训练完这个模型后你可以像这样一次性预测接下来的 14 个值
X mulvar_valid.to_numpy()[np.newaxis, :seq_length] # shape [1, 56, 5]
Y_pred ahead_model.predict(X) # shape [1, 14]这种方法效果相当不错。它对于第二天的预测显然比对未来 14 天的预测要好但它不会像之前的方法那样累积误差。然而我们仍然可以做得更好使用一个序列到序列或seq2seq模型。
使用序列到序列模型进行预测
不是只在最后一个时间步训练模型来预测接下来的 14 个值而是在每一个时间步都训练它来预测接下来的 14 个值。换句话说我们可以将这个序列到向量的 RNN 转变为一个序列到序列的 RNN。这种技术的优势在于损失函数将包含 RNN 在每一个时间步的输出而不仅仅是最后一个时间步的输出。
这意味着会有更多的误差梯度通过模型流动它们不需要像以前那样通过时间流动因为它们将来自每一个时间步的输出而不仅仅是最后一个时间步。这将使训练更加稳定和快速。
明确一点在时间步 0模型将输出一个包含时间步 1 到 14 的预测的向量然后在时间步 1模型将预测时间步 2 到 15依此类推。换句话说目标是连续窗口的序列每个时间步向后移动一个时间步。目标不再是一个向量而是一个与输入相同长度的序列每一步包含一个 14 维向量。
准备数据集并不是简单的因为每个实例的输入是一个窗口输出是窗口序列。一种方法是连续两次使用我们之前创建的to_windows()实用函数以获得连续窗口的窗口。例如让我们将数字 0 到 6 的系列转换为包含 4 个连续窗口的数据集每个窗口长度为 3 my_series tf.data.Dataset.range(7)dataset to_windows(to_windows(my_series, 3), 4)list(dataset)
[tf.Tensor: shape(4, 3), dtypeint64, numpyarray([[0, 1, 2],[1, 2, 3],[2, 3, 4],[3, 4, 5]]),tf.Tensor: shape(4, 3), dtypeint64, numpyarray([[1, 2, 3],[2, 3, 4],[3, 4, 5],[4, 5, 6]])]现在我们可以使用map()方法将这些窗口的窗口分割为输入和目标 dataset dataset.map(lambda S: (S[:, 0], S[:, 1:]))list(dataset)
[(tf.Tensor: shape(4,), dtypeint64, numpyarray([0, 1, 2, 3]),tf.Tensor: shape(4, 2), dtypeint64, numpyarray([[1, 2],[2, 3],[3, 4],[4, 5]])),(tf.Tensor: shape(4,), dtypeint64, numpyarray([1, 2, 3, 4]),tf.Tensor: shape(4, 2), dtypeint64, numpyarray([[2, 3],[3, 4],[4, 5],[5, 6]]))]现在数据集包含长度为 4 的输入序列目标是包含下两个步骤的序列每个时间步。例如第一个输入序列是[0, 1, 2, 3]对应的目标是[[1, 2], [2, 3], [3, 4], [4, 5]]这是每个时间步的下两个值。如果你和我一样可能需要几分钟来理解这个概念。慢慢来
注意
也许令人惊讶的是目标值包含在输入中出现的值。这是不是作弊幸运的是完全不是在每一个时间步RNN 只知道过去的时间步它无法向前看。它被称为因果模型。
让我们创建另一个小型实用函数来为我们的序列到序列模型准备数据集。它还会负责洗牌可选和分批处理
def to_seq2seq_dataset(series, seq_length56, ahead14, target_col1,batch_size32, shuffleFalse, seedNone):ds to_windows(tf.data.Dataset.from_tensor_slices(series), ahead 1)ds to_windows(ds, seq_length).map(lambda S: (S[:, 0], S[:, 1:, 1]))if shuffle:ds ds.shuffle(8 * batch_size, seedseed)return ds.batch(batch_size)现在我们可以使用这个函数来创建数据集
seq2seq_train to_seq2seq_dataset(mulvar_train, shuffleTrue, seed42)
seq2seq_valid to_seq2seq_dataset(mulvar_valid)最后我们可以构建序列到序列模型
seq2seq_model tf.keras.Sequential([tf.keras.layers.SimpleRNN(32, return_sequencesTrue, input_shape[None, 5]),tf.keras.layers.Dense(14)
])这几乎与我们之前的模型完全相同唯一的区别是在SimpleRNN层中设置了return_sequencesTrue。这样它将输出一个向量序列每个大小为 32而不是在最后一个时间步输出单个向量。Dense层足够聪明可以处理序列作为输入它将在每个时间步应用以 32 维向量作为输入并输出 14 维向量。实际上获得完全相同结果的另一种方法是使用具有核大小为 1 的Conv1D层Conv1D(14, kernel_size1)。
提示
Keras 提供了一个TimeDistributed层允许您将任何向量到向量层应用于输入序列中的每个向量在每个时间步。它通过有效地重新塑造输入来实现这一点以便将每个时间步视为单独的实例然后重新塑造层的输出以恢复时间维度。在我们的情况下我们不需要它因为Dense层已经支持序列作为输入。
训练代码与往常一样。在训练期间使用所有模型的输出但在训练后只有最后一个时间步的输出才重要其余可以忽略。例如我们可以这样预测未来 14 天的铁路乘客量
X mulvar_valid.to_numpy()[np.newaxis, :seq_length]
y_pred_14 seq2seq_model.predict(X)[0, -1] # only the last time steps output如果评估此模型对t 1 的预测您将发现验证 MAE 为 25,519。对于t 2它为 26,274随着模型试图进一步预测未来性能会逐渐下降。在t 14 时MAE 为 34,322。
提示
您可以结合两种方法来预测多步例如您可以训练一个模型预测未来 14 天然后将其输出附加到输入然后再次运行模型以获取接下来 14 天的预测并可能重复该过程。
简单的 RNN 在预测时间序列或处理其他类型的序列时可能表现得很好但在长时间序列或序列上表现不佳。让我们讨论一下原因并看看我们能做些什么。
处理长序列
要在长序列上训练 RNN我们必须在许多时间步上运行它使展开的 RNN 成为一个非常深的网络。就像任何深度神经网络一样它可能会遇到不稳定的梯度问题如第十一章中讨论的可能需要很长时间来训练或者训练可能不稳定。此外当 RNN 处理长序列时它将逐渐忘记序列中的第一个输入。让我们从不稳定的梯度问题开始看看这两个问题。
解决不稳定梯度问题
许多我们在深度网络中用来缓解不稳定梯度问题的技巧也可以用于 RNN良好的参数初始化更快的优化器辍学等。然而非饱和激活函数例如 ReLU在这里可能不会有太大帮助。实际上它们可能会导致 RNN 在训练过程中更加不稳定。为什么嗯假设梯度下降以一种增加第一个时间步输出的方式更新权重。由于相同的权重在每个时间步使用第二个时间步的输出也可能略有增加第三个时间步也是如此直到输出爆炸——而非饱和激活函数无法阻止这种情况。
您可以通过使用较小的学习率来减少这种风险或者可以使用饱和激活函数如双曲正切这解释了为什么它是默认值。
同样梯度本身也可能爆炸。如果注意到训练不稳定可以监控梯度的大小例如使用 TensorBoard并可能使用梯度裁剪。
此外批量归一化不能像深度前馈网络那样有效地与 RNN 一起使用。实际上您不能在时间步之间使用它只能在循环层之间使用。
更准确地说从技术上讲可以在内存单元中添加一个 BN 层您很快就会看到以便在每个时间步上应用它既在该时间步的输入上也在上一个步骤的隐藏状态上。然而相同的 BN 层将在每个时间步上使用相同的参数而不考虑输入和隐藏状态的实际比例和偏移。实际上这并不会产生良好的结果如 César Laurent 等人在2015 年的一篇论文中所证明的作者发现只有当 BN 应用于层的输入时才略有益处而不是应用于隐藏状态。换句话说当应用于循环层之间即在图 15-10 中垂直地时它略好于什么都不做但不适用于循环层内部即水平地。在 Keras 中您可以通过在每个循环层之前添加一个 BatchNormalization 层来简单地在层之间应用 BN但这会减慢训练速度并且可能帮助不大。
另一种规范化方法在 RNN 中通常效果更好层归一化。这个想法是由 Jimmy Lei Ba 等人在2016 年的一篇论文中提出的它与批归一化非常相似但不同的是层归一化是在特征维度上进行归一化而不是在批次维度上。一个优点是它可以在每个时间步上独立地为每个实例计算所需的统计数据。这也意味着它在训练和测试期间的行为是相同的与 BN 相反它不需要使用指数移动平均来估计训练集中所有实例的特征统计数据就像 BN 那样。与 BN 类似层归一化为每个输入学习一个比例和偏移参数。在 RNN 中它通常在输入和隐藏状态的线性组合之后立即使用。
让我们使用 Keras 在简单内存单元中实现层归一化。为此我们需要定义一个自定义内存单元它就像一个常规层一样只是它的 call() 方法接受两个参数当前时间步的 inputs 和上一个时间步的隐藏 states。
请注意states 参数是一个包含一个或多个张量的列表。在简单的 RNN 单元中它包含一个张量等于上一个时间步的输出但其他单元可能有多个状态张量例如LSTMCell 有一个长期状态和一个短期状态您很快就会看到。一个单元还必须有一个 state_size 属性和一个 output_size 属性。在简单的 RNN 中两者都简单地等于单元的数量。以下代码实现了一个自定义内存单元它将表现得像一个 SimpleRNNCell但它还会在每个时间步应用层归一化
class LNSimpleRNNCell(tf.keras.layers.Layer):def __init__(self, units, activationtanh, **kwargs):super().__init__(**kwargs)self.state_size unitsself.output_size unitsself.simple_rnn_cell tf.keras.layers.SimpleRNNCell(units,activationNone)self.layer_norm tf.keras.layers.LayerNormalization()self.activation tf.keras.activations.get(activation)def call(self, inputs, states):outputs, new_states self.simple_rnn_cell(inputs, states)norm_outputs self.activation(self.layer_norm(outputs))return norm_outputs, [norm_outputs]让我们来看一下这段代码 我们的 LNSimpleRNNCell 类继承自 tf.keras.layers.Layer 类就像任何自定义层一样。 构造函数接受单位数和所需的激活函数并设置 state_size 和 output_size 属性然后创建一个没有激活函数的 SimpleRNNCell因为我们希望在线性操作之后但在激活函数之前执行层归一化。然后构造函数创建 LayerNormalization 层最后获取所需的激活函数。 call()方法首先应用simpleRNNCell它计算当前输入和先前隐藏状态的线性组合并返回结果两次实际上在SimpleRNNCell中输出就等于隐藏状态换句话说new_states[0]等于outputs因此我们可以在call()方法的其余部分安全地忽略new_states。接下来call()方法应用层归一化然后是激活函数。最后它将输出返回两次一次作为输出一次作为新的隐藏状态。要使用此自定义细胞我们只需要创建一个tf.keras.layers.RNN层将其传递给一个细胞实例
custom_ln_model tf.keras.Sequential([tf.keras.layers.RNN(LNSimpleRNNCell(32), return_sequencesTrue,input_shape[None, 5]),tf.keras.layers.Dense(14)
])同样您可以创建一个自定义细胞在每个时间步之间应用 dropout。但有一个更简单的方法Keras 提供的大多数循环层和细胞都有dropout和recurrent_dropout超参数前者定义要应用于输入的 dropout 率后者定义隐藏状态之间的 dropout 率即时间步之间。因此在 RNN 中不需要创建自定义细胞来在每个时间步应用 dropout。
通过这些技术您可以缓解不稳定梯度问题并更有效地训练 RNN。现在让我们看看如何解决短期记忆问题。
提示
在预测时间序列时通常有必要在预测中包含一些误差范围。为此一种方法是使用 MC dropout介绍在第十一章中在训练期间使用recurrent_dropout然后在推断时通过使用model(X, trainingTrue)来保持 dropout 处于活动状态。多次重复此操作以获得多个略有不同的预测然后计算每个时间步的这些预测的均值和标准差。
解决短期记忆问题
由于数据在经过 RNN 时经历的转换每个时间步都会丢失一些信息。过一段时间后RNN 的状态几乎不包含最初输入的任何痕迹。这可能是一个停滞不前的问题。想象一下多莉鱼试图翻译一句长句子当她读完时她已经不记得它是如何开始的。为了解决这个问题引入了各种具有长期记忆的细胞类型。它们已经被证明非常成功以至于基本细胞不再被广泛使用。让我们首先看看这些长期记忆细胞中最受欢迎的LSTM 细胞。
LSTM 细胞
长短期记忆LSTM细胞是由 Sepp Hochreiter 和 Jürgen Schmidhuber 于 1997 年提出的并在多年来逐渐得到了几位研究人员的改进如 Alex GravesHaşim Sak 和 Wojciech Zaremba。如果将 LSTM 细胞视为黑匣子它可以被用作基本细胞只是它的性能会更好训练会更快收敛并且它会检测数据中更长期的模式。在 Keras 中您可以简单地使用LSTM层而不是SimpleRNN层
model tf.keras.Sequential([tf.keras.layers.LSTM(32, return_sequencesTrue, input_shape[None, 5]),tf.keras.layers.Dense(14)
])或者您可以使用通用的tf.keras.layers.RNN层将LSTMCell作为参数传递给它。但是当在 GPU 上运行时LSTM层使用了优化的实现请参阅第十九章因此通常最好使用它RNN层在定义自定义细胞时非常有用就像我们之前做的那样。
那么 LSTM 细胞是如何工作的呢其架构显示在图 15-12 中。如果不看盒子里面的内容LSTM 细胞看起来与常规细胞完全相同只是其状态分为两个向量h[(t)]和c[(t)]“c”代表“cell”。您可以将h[(t)]视为短期状态将c[(t)]视为长期状态。 图 15-12. LSTM 单元
现在让我们打开盒子关键思想是网络可以学习将什么存储在长期状态中什么丢弃以及从中读取什么。当长期状态c[(t–1)]从左到右穿过网络时您可以看到它首先经过一个遗忘门丢弃一些记忆然后通过加法操作添加一些新的记忆通过输入门选择的记忆。结果c[(t)]直接发送出去没有进一步的转换。因此在每个时间步骤一些记忆被丢弃一些记忆被添加。此外在加法操作之后长期状态被复制并通过 tanh 函数传递然后结果由输出门过滤。这产生了短期状态h[(t)]这等于此时间步骤的单元输出y[(t)。现在让我们看看新记忆来自哪里以及门是如何工作的。
首先当前输入向量x[(t)]和先前的短期状态h[(t–1)]被馈送到四个不同的全连接层。它们各自有不同的作用 主要层是输出g[(t)]的层。它通常的作用是分析当前输入x[(t)]和先前短期状态h[(t–1)]。在基本单元中除了这一层外没有其他内容其输出直接发送到y[(t)]和h[(t)]。但在 LSTM 单元中这一层的输出不会直接输出相反其最重要的部分存储在长期状态中其余部分被丢弃。 其他三个层是门控制器。由于它们使用逻辑激活函数输出范围从 0 到 1。正如您所看到的门控制器的输出被馈送到逐元素乘法操作如果它们输出 0则关闭门如果它们输出 1则打开门。具体来说 遗忘门由f[(t)]控制控制着应该擦除长期状态的哪些部分。 输入门由i[(t)]控制控制着应该将g[(t)]的哪些部分添加到长期状态中。 最后输出门由o[(t)]控制控制着长期状态的哪些部分应该在此时间步骤被读取并输出既输出到h[(t)]也输出到y[(t)]。
简而言之LSTM 单元可以学习识别重要的输入这是输入门的作用将其存储在长期状态中保留它直到需要这是遗忘门的作用并在需要时提取它。这解释了为什么这些单元在捕捉时间序列、长文本、音频记录等长期模式方面取得了惊人的成功。
方程 15-4 总结了如何计算单元的长期状态、短期状态以及每个时间步骤的输出针对单个实例整个小批量的方程非常相似。
方程 15-4. LSTM 计算
i (t) σ ( W xi ⊺ x (t) W hi ⊺ h (t-1) b i ) f (t) σ ( W xf ⊺ x (t) W hf ⊺ h (t-1) b f ) o (t) σ ( W xo ⊺ x (t) W ho ⊺ h (t-1) b o ) g (t) tanh ( W xg ⊺ x (t) W hg ⊺ h (t-1) b g ) c (t) f (t) ⊗ c (t-1) i (t) ⊗ g (t) y (t) h (t) o (t) ⊗ tanh ( c (t) )
在这个方程中 W[xi]、W[xf]、W[xo]和W[xg]是每个四层的权重矩阵用于它们与输入向量x[(t)]的连接。 W[hi]、W[hf]、W[ho]和W[hg]是每个四层的权重矩阵用于它们与先前的短期状态h[(t–1)]的连接。 b[i]、b[f]、b[o]和b[g]是每个四层的偏置项。请注意TensorFlow 将b[f]初始化为一个全为 1 的向量而不是 0。这可以防止在训练开始时忘记所有内容。
LSTM 单元有几个变体。一个特别流行的变体是 GRU 单元我们现在将看一下。
GRU 单元
门控循环单元 (GRU)单元见图 15-13由 Kyunghyun Cho 等人在2014 年的一篇论文中提出该论文还介绍了我们之前讨论过的编码器-解码器网络。 图 15-13. GRU 单元
GRU 单元是 LSTM 单元的简化版本看起来表现同样出色这解释了它日益增长的受欢迎程度。这些是主要的简化 两个状态向量合并成一个单一向量h[(t)]。 一个单一的门控制器z[(t)]控制遗忘门和输入门。如果门控制器输出 1则遗忘门打开 1输入门关闭1 - 1 0。如果输出 0则相反发生。换句话说每当必须存储一个记忆时将首先擦除将存储它的位置。这实际上是 LSTM 单元的一个常见变体。 没有输出门完整状态向量在每个时间步输出。然而有一个新的门控制器r[(t)]控制哪部分先前状态将被显示给主层(g[(t)])。
方程 15-5 总结了如何计算每个时间步的单个实例的单元状态。
方程 15-5. GRU 计算
z (t) σ ( W xz ⊺ x (t) W hz ⊺ h (t-1) bz ) r (t) σ ( W xr ⊺ x (t) W hr ⊺ h (t-1) br ) g (t) tanh W xg ⊺ x (t) W hg ⊺ ( r (t) ⊗ h (t-1) ) bg h (t) z (t) ⊗ h (t-1) ( 1 - z (t) ) ⊗ g (t)
Keras 提供了一个tf.keras.layers.GRU层使用它只需要将SimpleRNN或LSTM替换为GRU。它还提供了一个tf.keras.layers.GRUCell以便根据 GRU 单元创建自定义单元。
LSTM 和 GRU 单元是 RNN 成功的主要原因之一。然而虽然它们可以处理比简单 RNN 更长的序列但它们仍然有相当有限的短期记忆并且很难学习 100 个时间步或更多的序列中的长期模式例如音频样本、长时间序列或长句子。解决这个问题的一种方法是缩短输入序列例如使用 1D 卷积层。
使用 1D 卷积层处理序列
在第十四章中我们看到 2D 卷积层通过在图像上滑动几个相当小的卷积核或滤波器产生多个 2D 特征图每个卷积核一个。类似地1D 卷积层在序列上滑动多个卷积核每个卷积核产生一个 1D 特征图。每个卷积核将学习检测单个非常短的连续模式不超过卷积核大小。如果使用 10 个卷积核则该层的输出将由 10 个 1D 序列组成长度相同或者您可以将此输出视为单个 10D 序列。这意味着您可以构建一个由循环层和 1D 卷积层甚至 1D 池化层组成的神经网络。如果使用步幅为 1 和same填充的 1D 卷积层则输出序列的长度将与输入序列的长度相同。但是如果使用valid填充或大于 1 的步幅则输出序列将短于输入序列因此请确保相应调整目标。
例如以下模型与之前的模型相同只是它以一个 1D 卷积层开始通过步幅为 2 对输入序列进行下采样。卷积核的大小大于步幅因此所有输入都将用于计算该层的输出因此模型可以学习保留有用信息仅丢弃不重要的细节。通过缩短序列卷积层可能有助于GRU层检测更长的模式因此我们可以将输入序列长度加倍至 112 天。请注意我们还必须裁剪目标中的前三个时间步实际上卷积核的大小为 4因此卷积层的第一个输出将基于输入时间步 0 到 3第一个预测将是时间步 4 到 17而不是时间步 1 到 14。此外由于步幅我们必须将目标下采样一半
conv_rnn_model tf.keras.Sequential([tf.keras.layers.Conv1D(filters32, kernel_size4, strides2,activationrelu, input_shape[None, 5]),tf.keras.layers.GRU(32, return_sequencesTrue),tf.keras.layers.Dense(14)
])longer_train to_seq2seq_dataset(mulvar_train, seq_length112,shuffleTrue, seed42)
longer_valid to_seq2seq_dataset(mulvar_valid, seq_length112)
downsampled_train longer_train.map(lambda X, Y: (X, Y[:, 3::2]))
downsampled_valid longer_valid.map(lambda X, Y: (X, Y[:, 3::2]))
[...] # compile and fit the model using the downsampled datasets如果您训练和评估此模型您会发现它的性能优于之前的模型略有优势。事实上实际上可以仅使用 1D 卷积层并完全放弃循环层
WaveNet
在2016 年的一篇论文¹⁶ Aaron van den Oord 和其他 DeepMind 研究人员介绍了一种名为WaveNet的新颖架构。他们堆叠了 1D 卷积层每一层的扩张率每个神经元的输入间隔都加倍第一个卷积层一次只能看到两个时间步而下一个卷积层则看到四个时间步其感受野为四个时间步下一个卷积层看到八个时间步依此类推参见图 15-14。通过加倍扩张率较低层学习短期模式而较高层学习长期模式。由于加倍扩张率网络可以非常高效地处理极大的序列。 图 15-14。WaveNet 架构
论文的作者实际上堆叠了 10 个具有 1、2、4、8、…、256、512 扩张率的卷积层然后他们又堆叠了另一组 10 个相同的层扩张率也是 1、2、4、8、…、256、512然后再次堆叠了另一组相同的 10 层。他们通过指出具有这些扩张率的单个 10 个卷积层堆栈将像具有大小为 1,024 的卷积核的超高效卷积层一样速度更快更强大参数数量显著减少。他们还在每一层之前用与扩张率相等的零填充输入序列以保持整个网络中相同的序列长度。
以下是如何实现一个简化的 WaveNet 来处理与之前相同的序列的方法¹⁷
wavenet_model tf.keras.Sequential()
wavenet_model.add(tf.keras.layers.Input(shape[None, 5]))
for rate in (1, 2, 4, 8) * 2:wavenet_model.add(tf.keras.layers.Conv1D(filters32, kernel_size2, paddingcausal, activationrelu,dilation_raterate))
wavenet_model.add(tf.keras.layers.Conv1D(filters14, kernel_size1))这个Sequential模型从一个明确的输入层开始——这比仅在第一层上设置input_shape要简单。然后它继续使用“因果”填充的 1D 卷积层这类似于“相同”填充只是零值仅附加在输入序列的开头而不是两侧。这确保了卷积层在进行预测时不会窥视未来。然后我们添加使用不断增长的扩张率的类似对层1、2、4、8再次是 1、2、4、8。最后我们添加输出层一个具有 14 个大小为 1 的滤波器的卷积层没有任何激活函数。正如我们之前看到的那样这样的卷积层等效于具有 14 个单元的Dense层。由于因果填充每个卷积层输出与其输入序列相同长度的序列因此我们在训练期间使用的目标可以是完整的 112 天序列无需裁剪或降采样。
我们在本节讨论的模型对乘客量预测任务提供了类似的性能但它们在任务和可用数据量方面可能会有很大差异。在 WaveNet 论文中作者在各种音频任务因此该架构的名称上实现了最先进的性能包括文本转语音任务在多种语言中产生令人难以置信的逼真声音。他们还使用该模型逐个音频样本生成音乐。当您意识到一秒钟的音频可能包含成千上万个时间步时这一壮举就显得更加令人印象深刻——即使是 LSTM 和 GRU 也无法处理如此长的序列。
警告
如果您在测试期间评估我们最佳的芝加哥乘客量模型从 2020 年开始您会发现它们的表现远远不如预期为什么呢嗯那时候是 Covid-19 大流行开始的时候这对公共交通产生了很大影响。正如前面提到的这些模型只有在它们从过去学到的模式在未来继续时才能很好地工作。无论如何在将模型部署到生产环境之前请验证它在最近的数据上表现良好。一旦投入生产请确保定期监控其性能。
有了这个您现在可以处理各种时间序列了在第十六章中我们将继续探索 RNN并看看它们如何处理各种 NLP 任务。
练习 您能想到一些序列到序列 RNN 的应用吗序列到向量 RNN 和向量到序列 RNN 呢 RNN 层的输入必须具有多少维度每个维度代表什么输出呢 如果您想构建一个深度序列到序列 RNN哪些 RNN 层应该具有return_sequencesTrue序列到向量 RNN 呢 假设您有一个每日单变量时间序列并且您想要预测接下来的七天。您应该使用哪种 RNN 架构 在训练 RNN 时主要的困难是什么您如何处理它们 您能勾画出 LSTM 单元的架构吗 为什么要在 RNN 中使用 1D 卷积层 您可以使用哪种神经网络架构来对视频进行分类 为 SketchRNN 数据集训练一个分类模型该数据集可在 TensorFlow Datasets 中找到。 下载巴赫赞美诗数据集并解压缩。这是由约翰·塞巴斯蒂安·巴赫创作的 382 首赞美诗。每首赞美诗长 100 至 640 个时间步长每个时间步长包含 4 个整数其中每个整数对应于钢琴上的一个音符的索引除了值为 0表示没有播放音符。训练一个模型——循环的、卷积的或两者兼而有之——可以预测下一个时间步长四个音符给定来自赞美诗的时间步长序列。然后使用这个模型生成类似巴赫的音乐一次一个音符您可以通过给模型提供赞美诗的开头并要求它预测下一个时间步长来实现这一点然后将这些时间步长附加到输入序列并要求模型预测下一个音符依此类推。还要确保查看谷歌的 Coconet 模型该模型用于关于巴赫的一个不错的谷歌涂鸦。
这些练习的解决方案可在本章笔记本的末尾找到网址为https://homl.info/colab3。
¹ 请注意许多研究人员更喜欢在 RNN 中使用双曲正切tanh激活函数而不是 ReLU 激活函数。例如参见 Vu Pham 等人的2013 年论文“Dropout Improves Recurrent Neural Networks for Handwriting Recognition”。基于 ReLU 的 RNN 也是可能的正如 Quoc V. Le 等人的2015 年论文“初始化修正线性单元的循环网络的简单方法”中所示。
² Nal Kalchbrenner 和 Phil Blunsom“循环连续翻译模型”2013 年经验方法自然语言处理会议论文集20131700–1709。
³ 芝加哥交通管理局的最新数据可在芝加哥数据门户上找到。
⁴ 有其他更有原则的方法来选择好的超参数基于分析自相关函数ACF和偏自相关函数PACF或最小化 AIC 或 BIC 指标在第九章中介绍以惩罚使用太多参数的模型并减少过拟合数据的风险但网格搜索是一个很好的起点。有关 ACF-PACF 方法的更多详细信息请查看 Jason Brownlee 的这篇非常好的文章。
⁵ 请注意验证期从 2019 年 1 月 1 日开始因此第一个预测是 2019 年 2 月 26 日八周后。当我们评估基线模型时我们使用了从 3 月 1 日开始的预测但这应该足够接近。
⁶ 随意尝试这个模型。例如您可以尝试预测接下来 14 天的公交和轨道乘客量。您需要调整目标包括两者并使您的模型输出 28 个预测而不是 14 个。
⁷ César Laurent 等人“批量归一化循环神经网络”IEEE 国际声学、语音和信号处理会议论文集20162657–2661。
⁸ Jimmy Lei Ba 等人“层归一化”arXiv 预印本 arXiv:1607.064502016。
⁹ 更简单的方法是继承自SimpleRNNCell这样我们就不必创建内部的SimpleRNNCell或处理state_size和output_size属性但这里的目标是展示如何从头开始创建自定义单元。
¹⁰ 动画电影海底总动员和海底奇兵中一个患有短期记忆丧失的角色。
¹¹ Sepp Hochreiter 和 Jürgen Schmidhuber“长短期记忆”神经计算 9第 8 期1997 年1735–1780。
¹² Haşim Sak 等“基于长短期记忆的大词汇语音识别循环神经网络架构”arXiv 预印本 arXiv:1402.11282014 年。
¹³ Wojciech Zaremba 等“循环神经网络正则化”arXiv 预印本 arXiv:1409.23292014 年。
¹⁴ Kyunghyun Cho 等“使用 RNN 编码器-解码器学习短语表示进行统计机器翻译”2014 年经验方法自然语言处理会议论文集2014 年1724–1734。
¹⁵ 请参阅 Klaus Greff 等的“LSTM搜索空间奥德赛”IEEE 神经网络与学习系统交易 28第 10 期2017 年2222–2232。这篇论文似乎表明所有 LSTM 变体表现大致相同。
¹⁶ Aaron van den Oord 等“WaveNet原始音频的生成模型”arXiv 预印本 arXiv:1609.034992016 年。
¹⁷ 完整的 WaveNet 使用了更多技巧例如类似于 ResNet 中的跳过连接和类似于 GRU 单元中的门控激活单元。有关更多详细信息请参阅本章的笔记本。