🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎
📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
🖍foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟👋
在第 10 章中,我们介绍了人工神经网络并训练了我们的第一个深度神经网络。但它们是浅层网络,只有几个隐藏层。如果您需要解决一个复杂的问题,例如在高分辨率图像中检测数百种类型的对象,该怎么办?您可能需要训练更深的 DNN,可能有 10 层或更多,每层包含数百个神经元,由数十万个连接连接。训练深度 DNN 不是在公园里散步。以下是您可能遇到的一些问题:
您可能会遇到棘手的梯度消失问题或相关的梯度爆炸问题。这是当梯度在训练期间向后流过 DNN 时变得越来越小或越来越大的时候。这两个问题都使得低层很难训练。
对于如此庞大的网络,您可能没有足够的训练数据,或者标记成本可能太高。
训练可能非常缓慢。
具有数百万个参数的模型将严重存在过度拟合训练集的风险,尤其是在没有足够的训练实例或它们过于嘈杂的情况下。
在本章中,我们将逐一介绍这些问题并提出解决这些问题的技术。我们将从探索梯度消失和爆炸问题及其一些最流行的解决方案开始。接下来,我们将研究迁移学习和无监督预训练,即使在标记数据很少的情况下,它们也可以帮助您处理复杂的任务。然后我们将讨论可以极大地加速训练大型模型的各种优化器。最后,我们将介绍一些用于大型神经网络的流行正则化技术。
使用这些工具,您将能够训练非常深的网络。欢迎来到深度学习!
梯度消失/爆炸问题
作为我们在第 10 章讨论过,反向传播算法的工作原理是从输出层到输入层,沿途传播误差梯度。一旦算法计算了成本函数关于网络中每个参数的梯度,它就会使用这些梯度通过梯度下降步骤更新每个参数。
不幸的是,随着算法深入到较低层,梯度通常会变得越来越小。结果,梯度下降更新使较低层的连接权重几乎没有变化,并且训练永远不会收敛到一个好的解决方案。我们称之为梯度消失问题。在某些情况下,可能会发生相反的情况:梯度会越来越大,直到层的权重更新非常大并且算法发散。这个是梯度爆炸问题,它出现在循环神经网络中(参见第 15 章)。更一般地说,深度神经网络受到不稳定梯度的影响。不同的层可能以不同的速度学习。
这种不幸的行为在很久以前就被经验观察到了,这也是深度神经网络在 2000 年代初大多被抛弃的原因之一。目前尚不清楚是什么原因导致在训练 DNN 时梯度如此不稳定,但 Xavier Glorot 和 Yoshua Bengio在2010 年的一篇论文中揭示了一些启示。1作者发现了一些嫌疑人,包括流行的组合逻辑 sigmoid 激活函数和当时最流行的权重初始化技术(即均值为 0,标准差为 1 的正态分布)。简而言之,他们表明,使用这种激活函数和这种初始化方案,每一层输出的方差远大于其输入的方差。在网络中前进,每层之后方差不断增加,直到激活函数在顶层饱和。实际上,逻辑函数的平均值为 0.5,而不是 0(双曲正切函数的平均值为 0,并且在深度网络中的表现略好于逻辑函数),这种饱和度实际上变得更糟了。
查看逻辑激活函数(参见图 11-1),您可以看到当输入变大(负或正)时,函数在 0 或 1 处饱和,导数非常接近 0。因此,当反向传播开始时它几乎没有通过网络传播回来的梯度;并且随着反向传播通过顶层向下传播,存在的小梯度不断被稀释,因此对于较低层实际上没有任何剩余。
图 11-1。逻辑激活函数饱和
Glorot 和 He 初始化
在他们的论文 Glorot 和 Bengio 提出了一种显着缓解不稳定梯度问题的方法。他们指出,我们需要信号在两个方向上正确流动:在进行预测时是正向的,而在反向传播梯度时是反向的。我们不希望信号消失,也不希望它爆炸和饱和。为了使信号正确流动,作者认为我们需要每一层输出的方差等于其输入的方差2,并且我们需要梯度在流过一层之前和之后的方差相等。反向(如果您对数学细节感兴趣,请查看论文)。它实际上不可能同时保证两者,除非该层具有相同数量的输入和神经元(这些数字称为该层的扇入和扇出),但 Glorot 和 Bengio 提出了一个很好的折衷方案,该折衷方案已被证明非常有效在实践中很好:每一层的连接权重必须按照公式 11-1中的描述随机初始化,其中fan avg = ( fan in + fan out )/2。这个初始化策略称为Xavier 初始化或Glorot 初始化,以论文第一作者命名。
公式 11-1。Glorot 初始化(使用逻辑激活函数时)

如果将公式 11-1中的fan avg替换为fan in,您将得到 Yann LeCun 在 1990 年代提出的初始化策略。他称之为LeCun 初始化。Genevieve Orr 和 Klaus-Robert Müller 甚至在他们 1998 年出版的《神经网络:交易技巧》(Springer)一书中推荐了它。LeCun 初始化相当于扇入=扇出时的 Glorot 初始化。研究人员花了十多年的时间才意识到这个技巧的重要性。使用 Glorot 初始化可以大大加快训练速度,这是导致深度学习成功的技巧之一。
一些论文3为不同的激活函数提供了类似的策略。这些策略的区别仅在于方差的大小以及它们是使用fan avg还是fan in,如表 11-1所示(对于均匀分布,只需计算r=3p2)。ReLU 激活函数(及其变体,包括稍后描述的 ELU 激活)的初始化策略有时称为He 初始化,以论文的第一作者命名。SELU 激活功能将在本章后面解释。它应该与 LeCun 初始化一起使用(最好使用正态分布,正如我们将看到的)。
初始化 | 激活函数 | σ²(Normal) |
---|---|---|
Glorot | None, tanh, logistic, softmax | 1 / fanavg |
He | ReLU and variants | 2 / fanin |
LeCun | SELU | 1 / fanin |
默认情况下,Keras 使用具有均匀分布的 Glorot 初始化。创建层时,您可以通过设置kernel_initializer="he_uniform"
或kernel_initializer="he_normal"
这样将其更改为 He 初始化:
keras.layers.Dense(10, activation="relu", kernel_initializer="he_normal")
如果您希望 He 具有均匀分布但基于fan avg而不是fan in的初始化,您可以使用如下VarianceScaling
初始化器:
he_avg_init = keras.initializers.VarianceScaling(scale=2., mode='fan_avg',
distribution='uniform')
keras.layers.Dense(10, activation="sigmoid", kernel_initializer=he_avg_init)
非饱和激活函数
一Glorot 和 Bengio 在 2010 年的论文中的见解是,梯度不稳定的问题部分是由于激活函数选择不当造成的。在那之前,大多数人都认为,如果大自然母亲选择在生物神经元中使用粗略的 sigmoid 激活函数,那么它们一定是一个很好的选择。但事实证明,其他激活函数在深度神经网络中的表现要好得多——特别是 ReLU 激活函数,主要是因为它不会对正值饱和(而且计算速度很快)。
不幸的是,ReLU 激活函数并不完美。它存在一个被称为死亡 ReLU的问题:在训练期间,一些神经元实际上“死亡”,这意味着它们停止输出除 0 以外的任何东西。在某些情况下,您可能会发现网络的一半神经元已经死亡,特别是如果您使用很大的学习率。当一个神经元的权重被调整到其输入的加权总和对于训练集中的所有实例都是负数时,一个神经元就会死亡。发生这种情况时,它只是一直输出零,而梯度下降不再影响它,因为当输入为负时,ReLU 函数的梯度为零。4
至解决这个问题,您可能想要使用 ReLU 函数的变体,例如leaky ReLU。该函数定义为 LeakyReLU α ( z ) = max( αz , z )(见图 11-2)。超参数α定义了函数“泄漏”的程度:它是z < 0 时函数的斜率,通常设置为 0.01。这个小斜率确保泄漏的 ReLU 永远不会死亡;他们可以进入长时间的昏迷,但他们有机会最终醒来。2015 年论文5比较了 ReLU 激活函数的几个变体,其结论之一是泄漏变体总是优于严格的 ReLU 激活函数。事实上,设置α = 0.2(大泄漏)似乎比α = 0.01(小泄漏)产生更好的性能。这论文还评估了随机泄漏 ReLU (RReLU),其中α在训练期间在给定范围内随机选取,并在测试期间固定为平均值。RReLU 也表现得相当好,并且似乎充当了正则化器(降低了过度拟合训练集的风险)。最后,纸评估了参数泄漏 ReLU (PReLU),其中α被授权在训练期间学习(而不是作为超参数,它成为可以像任何其他参数一样通过反向传播修改的参数)。据报道,PReLU 在大型图像数据集上的性能明显优于 ReLU,但在较小的数据集上,它存在过度拟合训练集的风险。
图 11-2。Leaky ReLU:与 ReLU 类似,但负值斜率较小
最后但同样重要的是,Djork-Arné Clevert 等人在2015 年发表的一篇论文。6提出新的激活称为指数线性单元(ELU) 的函数在作者的实验中优于所有 ReLU 变体:训练时间减少,神经网络在测试集上表现更好。图 11-3显示了该函数,公式 11-2显示了它的定义。
公式 11-2。ELU 激活函数

图 11-3。ELU 激活函数
ELU 激活函数看起来很像 ReLU 函数,但有一些主要区别:
当z < 0时它取负值,这使得该单元的平均输出更接近于 0,并有助于缓解梯度消失问题。超参数α定义了当z是一个大的负数时 ELU 函数接近的值。它通常设置为 1,但您可以像任何其他超参数一样调整它。
它对于z < 0具有非零梯度,从而避免了死神经元问题。
如果α等于 1,则函数在任何地方都是平滑的,包括z = 0 附近,这有助于加速梯度下降,因为它不会在z = 0 的左右反弹。
ELU 激活函数的主要缺点是计算速度比 ReLU 函数及其变体慢(由于使用指数函数)。它在训练期间更快的收敛速度弥补了缓慢的计算,但在测试时,ELU 网络仍然比 ReLU 网络慢。
然后,Günter Klambauer 等人在2017 年发表的一篇论文7。介绍了Scaled ELU (SELU) 激活函数:顾名思义,它是 ELU 激活函数的缩放变体。作者表明,如果您构建一个仅由一堆密集层组成的神经网络,并且如果所有隐藏层都使用 SELU 激活函数,那么网络将自我归一化:在训练过程中,每一层的输出将倾向于保持平均值为 0,标准差为 1,从而解决了梯度消失/爆炸问题。因此,SELU 激活函数通常明显优于此类神经网络(尤其是深度神经网络)的其他激活函数。然而,发生自归一化有几个条件(参见论文的数学证明):
输入特征必须标准化(平均值 0 和标准差 1)。
每个隐藏层的权重必须使用 LeCun 正常初始化进行初始化。在 Keras 中,这意味着设置
kernel_initializer="lecun_normal"
.网络的架构必须是顺序的。不幸的是,如果您尝试在非序列架构中使用 SELU,例如循环网络(参见第 15 章)或具有跳过连接的网络(即跳过层的连接,例如在 Wide & Deep 网络中),将无法保证自归一化,因此 SELU 不一定会优于其他激活函数。
该论文仅在所有层都是密集的情况下保证自归一化,但一些研究人员注意到 SELU 激活函数也可以提高卷积神经网络的性能(参见第 14 章)。
小费
那么,你应该为你的深度神经网络的隐藏层使用哪个激活函数?尽管您的里程会有所不同,但通常 SELU > ELU >leaky ReLU(及其变体)> ReLU > tanh >logistic。如果网络的架构阻止它进行自归一化,那么 ELU 可能比 SELU 执行得更好(因为 SELU 在z = 0 时不平滑)。如果您非常关心运行时延迟,那么您可能更喜欢leaky ReLU。如果你不想再调整另一个超参数,你可以使用默认的αKeras 使用的值(例如,泄漏 ReLU 为 0.3)。如果您有空闲时间和计算能力,您可以使用交叉验证来评估其他激活函数,例如如果您的网络过度拟合则使用 RReLU,如果您有大量训练集则使用 PReLU。也就是说,由于 ReLU 是(到目前为止)最常用的激活函数,许多库和硬件加速器都提供了特定于 ReLU 的优化;因此,如果速度是您的首要任务,ReLU 可能仍然是最佳选择。
要使用泄漏的 ReLU 激活函数,请创建一个LeakyReLU
层并将其添加到您的模型中,就在您想要应用它的层之后:
model = keras.models.Sequential([
[...]
keras.layers.Dense(10, kernel_initializer="he_normal"),
keras.layers.LeakyReLU(alpha=0.2),
[...]
])
对于 PReLU,替换LeakyReLU(alpha=0.2)
为PReLU()
. 目前在 Keras 中没有正式的 RReLU 实现,但是你可以相当容易地实现自己的(要了解如何做到这一点,请参阅第 12 章末尾的练习)。
对于 SELU 激活,设置activation="selu"
和kernel_initializer="lecun_normal"
创建层时:
layer = keras.layers.Dense(10, activation="selu",
kernel_initializer="lecun_normal")
批量标准化
尽管将 He 初始化与 ELU(或 ReLU 的任何变体)一起使用可以显着降低训练开始时梯度消失/爆炸问题的危险,但不能保证它们不会在训练期间再次出现。
在2015 年的一篇论文中,8 Sergey Ioffe 和 Christian Szegedy 提出一种称为批量标准化(BN) 的技术可以解决这些问题。该技术包括在模型中在每个隐藏层的激活函数之前或之后添加一个操作。此操作只是将每个输入归零并归一化,然后使用每层的两个新参数向量来缩放和移动结果:一个用于缩放,另一个用于移动。换句话说,该操作让模型学习每个层输入的最佳比例和平均值。在很多情况下,如果你添加一个 BN 层作为你的神经网络的第一层,你就不需要标准化你的训练集(例如,使用 a StandardScaler
);BN 层会为您完成(嗯,大约,因为它一次只查看一个批次,并且它还可以重新缩放和移动每个输入特征)。
为了对输入进行零中心化和归一化,算法需要估计每个输入的均值和标准差。它通过评估当前小批量输入的平均值和标准差来实现(因此称为“批量标准化”)。公式 11-3逐步总结了整个操作。
公式 11-3。批量归一化算法

在这个算法中:
μ B是输入均值的向量,在整个小批量B上进行评估(每个输入包含一个均值)。
σ B是输入标准差的向量,也在整个 mini-batch 上进行评估(每个输入包含一个标准差)。
m B是小批量中的实例数。
X^( i )是零中心和归一化输入的向量,例如i。
γ是层的输出尺度参数向量(每个输入包含一个尺度参数)。
⊗ 表示逐元素乘法(每个输入乘以其对应的输出比例参数)。
β是层的输出偏移(偏移)参数向量(每个输入包含一个偏移参数)。每个输入都由其相应的移位参数偏移。
z ( i )是 BN 操作的输出。它是输入的重新缩放和移位版本。
因此,在训练期间,BN 对其输入进行标准化,然后对其进行重新缩放和偏移。好的!考试的时候呢?好吧,事情没那么简单。实际上,我们可能需要对单个实例而不是批量实例进行预测:在这种情况下,我们将无法计算每个输入的均值和标准差。而且,即使我们有一批实例,也可能太小,或者实例可能不是独立同分布的,所以对批实例的计算统计是不可靠的。一种解决方案可能是等到训练结束,然后通过神经网络运行整个训练集,并计算 BN 层的每个输入的均值和标准差。然后,在进行预测时,可以使用这些“最终”输入均值和标准差来代替批量输入均值和标准差。然而,大多数批量标准化的实现在训练期间通过使用层输入均值和标准差的移动平均值来估计这些最终统计数据。这是 Keras 在您使用BatchNormalization
层。总而言之,在每个批归一化层中学习了四个参数向量:γ(输出尺度向量)和β(输出偏移向量)通过常规反向传播学习,μ(最终输入均值向量)和σ(最终输入标准偏差向量)使用指数移动平均值估计。请注意,μ和σ是在训练期间估计的,但它们仅在训练后使用(以替换公式 11-3中的批量输入均值和标准差)。
Ioffe 和 Szegedy 证明 Batch Normalization 显着改善了他们试验的所有深度神经网络,从而导致 ImageNet 分类任务的巨大改进(ImageNet 是一个分类为许多类别的大型图像数据库,通常用于评估计算机视觉系统)。梯度消失问题大大减少,以至于他们可以使用饱和激活函数,例如 tanh 甚至逻辑激活函数。网络对权重初始化也不太敏感。作者能够使用更大的学习率,显着加快了学习过程。具体来说,他们注意到:
应用于最先进的图像分类模型,Batch Normalization 以少 14 倍的训练步骤实现了相同的准确度,并且大大超过了原始模型。[...] 使用批量归一化网络的集合,我们改进了 ImageNet 分类的最佳已发布结果:达到 4.9% 的 top-5 验证错误(和 4.8% 的测试错误),超过了人类评估者的准确度。
最后,就像不断给予的礼物一样,Batch Normalization 就像一个正则化器,减少了对其他正则化技术的需求(例如 dropout,本章稍后将介绍)。
然而,批量标准化确实给模型增加了一些复杂性(尽管它可以消除对输入数据进行标准化的需要,正如我们之前讨论的那样)。此外,存在运行时损失:由于每一层都需要额外的计算,神经网络的预测速度会变慢。幸运的是,通常可以在训练后将 BN 层与前一层融合,从而避免运行时损失。这是通过更新前一层的权重和偏差来完成的,以便它直接产生适当比例和偏移的输出。例如,如果前一层计算XW + b,那么 BN 层将计算γ ⊗( XW + b – μ )/ σ+ β(忽略分母中的平滑项ε)。如果我们定义W ′ = γ ⊗ W / σ和b ′ = γ ⊗( b – μ )/ σ + β,则方程简化为XW ′ + b ′。因此,如果我们用更新后的权重和偏差(W ′和b ′)替换前一层的权重和偏差(W和b),我们可以摆脱 BN 层(TFLite 的转换器会自动执行此操作;参见第 19 章)。
笔记
你可能会发现训练相当慢,因为当您使用 Batch Normalization 时,每个 epoch 都需要更多时间。这通常被 BN 的收敛速度更快的事实抵消,因此达到相同性能需要更少的 epoch。总而言之,挂墙时间通常会更短(这是你墙上的时钟测量的时间)。
使用 Keras 实现批量标准化
作为对于 Keras 的大多数事情,实现 Batch Normalization 既简单又直观。只需BatchNormalization
在每个隐藏层的激活函数之前或之后添加一个层,并可选择添加一个 BN 层以及模型中的第一层。例如,此模型在每个隐藏层之后应用 BN,并将其作为模型中的第一层(在展平输入图像之后):
model = keras.models.Sequential([
keras.layers.Flatten(input_shape=[28, 28]),
keras.layers.BatchNormalization(),
keras.layers.Dense(300, activation="elu", kernel_initializer="he_normal"),
keras.layers.BatchNormalization(),
keras.layers.Dense(100, activation="elu", kernel_initializer="he_normal"),
keras.layers.BatchNormalization(),
keras.layers.Dense(10, activation="softmax")
])
就这样!在这个只有两个隐藏层的小例子中,批量标准化不太可能产生非常积极的影响;但对于更深层次的网络,它可以产生巨大的差异。
让我们显示模型摘要:
>>> model.summary()
Model: "sequential_3"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
flatten_3 (Flatten) (None, 784) 0
_________________________________________________________________
batch_normalization_v2 (Batc (None, 784) 3136
_________________________________________________________________
dense_50 (Dense) (None, 300) 235500
_________________________________________________________________
batch_normalization_v2_1 (Ba (None, 300) 1200
_________________________________________________________________
dense_51 (Dense) (None, 100) 30100
_________________________________________________________________
batch_normalization_v2_2 (Ba (None, 100) 400
_________________________________________________________________
dense_52 (Dense) (None, 10) 1010
=================================================================
Total params: 271,346
Trainable params: 268,978
Non-trainable params: 2,368
如您所见,每个 BN 层为每个输入添加四个参数:γ、β、μ和σ(例如,第一个 BN 层添加 3,136 个参数,即 4 × 784)。最后两个参数μ和σ是移动平均线;它们不受反向传播的影响,因此 Keras 称它们为“不可训练” 9(如果计算 BN 参数的总数,3,136 + 1,200 + 400,然后除以 2,则得到 2,368,即不可训练的总数-此模型中的可训练参数)。
我们来看第一个BN层的参数。两个是可训练的(通过反向传播),两个不是:
>>> [(var.name, var.trainable) for var in model.layers[1].variables]
[('batch_normalization_v2/gamma:0', True),
('batch_normalization_v2/beta:0', True),
('batch_normalization_v2/moving_mean:0', False),
('batch_normalization_v2/moving_variance:0', False)]
现在,当您在 Keras 中创建 BN 层时,它还会创建两个操作,这些操作将在训练期间的每次迭代中被 Keras 调用。这些操作将更新移动平均线。由于我们使用的是 TensorFlow 后端,因此这些操作是 TensorFlow 操作(我们将在第 12 章讨论 TF 操作):
>>> model.layers[1].updates
[<tf.Operation 'cond_2/Identity' type=Identity>,
<tf.Operation 'cond_3/Identity' type=Identity>]
BN 论文的作者主张在激活函数之前添加 BN 层,而不是之后(就像我们刚刚所做的那样)。对此存在一些争论,因为哪个更可取似乎取决于任务——您也可以对此进行试验,看看哪个选项最适合您的数据集。要在激活函数之前添加 BN 层,您必须从隐藏层中移除激活函数,并将它们作为单独的层添加到 BN 层之后。此外,由于批量标准化层的每个输入都包含一个偏移参数,因此您可以从前一层中删除偏差项(只需use_bias=False
在创建时传递):
model = keras.models.Sequential([
keras.layers.Flatten(input_shape=[28, 28]),
keras.layers.BatchNormalization(),
keras.layers.Dense(300, kernel_initializer="he_normal", use_bias=False),
keras.layers.BatchNormalization(),
keras.layers.Activation("elu"),
keras.layers.Dense(100, kernel_initializer="he_normal", use_bias=False),
keras.layers.BatchNormalization(),
keras.layers.Activation("elu"),
keras.layers.Dense(10, activation="softmax")
])
这个BatchNormalization
类有很多你可以调整的超参数。默认值通常没问题,但您可能偶尔需要调整momentum
. 该超参数由BatchNormalization
层在更新指数移动平均线时使用;给定一个新值v(即,在当前批次上计算的输入均值或标准差的新向量),该层更新运行平均值在^使用以下等式:

良好的动量值通常接近 1;例如,0.9、0.99 或 0.999(对于较大的数据集和较小的小批量,您需要更多的 9)。
另一个重要的超参数是axis
:它确定应该对哪个轴进行归一化。它默认为 –1,这意味着默认情况下它将标准化最后一个轴(使用跨其他轴计算的平均值和标准差)。当输入批次是 2D 时(即批次形状为 [批次大小,特征]),这意味着每个输入特征将根据批次中所有实例计算的均值和标准差进行归一化。例如,前面代码示例中的第一个 BN 层将独立地对 784 个输入特征中的每一个进行归一化(以及重新缩放和移位)。如果我们将第一个 BN 层移到该层之前Flatten
,那么输入批次将是 3D,形状为 [批次大小、高度、宽度]; 因此,BN 层将计算 28 个均值和 28 个标准差(每列像素 1 个,跨批次中的所有实例和列中的所有行计算),并且它将使用相同的均值对给定列中的所有像素进行归一化和标准差。也将只有 28 个比例参数和 28 个移位参数。相反,如果您仍想独立处理 784 个像素中的每一个,那么您应该设置axis=[1, 2]
.
请注意,BN 层在训练期间和训练后不执行相同的计算:它在训练期间使用批量统计数据和训练后的“最终”统计数据(即移动平均值的最终值)。让我们看一下这个类的源代码,看看它是如何处理的:
class BatchNormalization(keras.layers.Layer):
[...]
def call(self, inputs, training=None):
[...]
该call()
方法是执行计算的方法;如您所见,它有一个额外的training
参数,None
默认设置为,但fit()
方法1
在训练期间将其设置为。如果你需要编写一个自定义层,并且它在训练和测试期间的行为必须不同,请在方法中添加一个training
参数,并在call()
方法中使用这个参数来决定要计算什么10(我们将在第 12 章讨论自定义层)。
BatchNormalization
已经成为深度神经网络中最常用的层之一,以至于在图中经常省略它,因为假设在每一层之后都添加了 BN。但是张宏毅等人最近的一篇论文11 。可能会改变这个假设:通过使用一种新颖的固定更新(fixup)权重初始化技术,作者设法在没有 BN 的情况下训练了一个非常深的神经网络(10,000 层!),在复杂图像分类上实现了最先进的性能任务。但是,由于这是一项前沿研究,您可能需要等待更多研究来证实这一发现,然后再放弃 Batch Normalization。
渐变剪裁
其他缓解梯度爆炸问题的流行技术是在反向传播期间裁剪梯度,使其永远不会超过某个阈值。这称为渐变剪裁。12这种技术最常用于循环神经网络,因为批量标准化在 RNN 中使用起来很棘手,我们将在第 15 章中看到。对于其他类型的网络,BN 通常就足够了。
在 Keras 中,实现Gradient Clipping 只是在创建优化器时设置clipvalue
or参数的问题,如下所示:clipnorm
optimizer = keras.optimizers.SGD(clipvalue=1.0)
model.compile(loss="mse", optimizer=optimizer)
此优化器会将梯度向量的每个分量裁剪为–1.0和 1.0 之间的值。这意味着损失的所有偏导数(关于每个可训练参数)将被限制在 –1.0 和 1.0 之间。阈值是您可以调整的超参数。请注意,它可能会改变梯度向量的方向。例如,如果原始梯度向量为 [0.9, 100.0],则它主要指向第二个轴的方向;但是一旦你按值裁剪它,你会得到 [0.9, 1.0],它大致指向两个轴之间的对角线。在实践中,这种方法效果很好。如果要确保 Gradient Clipping 不会改变梯度向量的方向,则应通过设置clipnorm
而不是clipvalue
. 如果它的 ℓ 2范数大于您选择的阈值,这将剪切整个渐变。例如,如果您设置clipnorm=1.0
,则向量 [0.9, 100.0] 将被裁剪为 [0.00899964, 0.9999595],保持其方向但几乎消除了第一个分量。如果您观察到梯度在训练期间爆炸(您可以使用 TensorBoard 跟踪梯度的大小),您可能需要尝试使用不同阈值的值裁剪和范数裁剪,并查看哪个选项在验证集上表现最佳.
重用预训练层
它从头开始训练一个非常大的 DNN 通常不是一个好主意:相反,您应该始终尝试找到一个现有的神经网络来完成与您尝试处理的任务类似的任务(我们将在第 1章讨论如何找到它们14),然后重用该网络的较低层。这个技术称为迁移学习。它不仅会大大加快训练速度,而且需要的训练数据也大大减少。
假设您可以访问经过训练的 DNN,可以将图片分类为 100 个不同的类别,包括动物、植物、车辆和日常用品。您现在想要训练 DNN 对特定类型的车辆进行分类。这些任务非常相似,甚至部分重叠,因此您应该尝试重用第一个网络的部分内容(参见图 11-4)。
图 11-4。重用预训练层
笔记
如果新任务的输入图片与原始任务中使用的图片大小不同,通常必须添加预处理步骤以将它们调整为原始模型预期的大小。更一般地说,当输入具有相似的低级特征时,迁移学习效果最好。
原始模型的输出层通常应该被替换,因为它很可能对新任务根本没有用,甚至可能没有正确数量的输出用于新任务。
类似地,原始模型的上层隐藏层不太可能像下层那样有用,因为对新任务最有用的高级特征可能与对原始任务最有用的特征有很大不同. 您想找到合适的层数以重复使用。
小费
任务越相似,您想要重用的层就越多(从较低的层开始)。对于非常相似的任务,请尝试保留所有隐藏层并仅替换输出层。
首先尝试冻结所有重复使用的层(即,使它们的权重不可训练,以便梯度下降不会修改它们),然后训练您的模型并查看它的性能。然后尝试解冻一两个顶层隐藏层,让反向传播调整它们,看看性能是否有所提高。您拥有的训练数据越多,您可以解冻的层数就越多。解冻重用层时降低学习率也很有用:这将避免破坏它们的微调权重。
如果您仍然无法获得良好的性能,并且您的训练数据很少,请尝试删除顶部隐藏层并再次冻结所有剩余的隐藏层。您可以进行迭代,直到找到要重用的正确层数。如果你有大量的训练数据,你可以尝试替换顶部的隐藏层而不是删除它们,甚至添加更多的隐藏层。
使用 Keras 进行迁移学习
我们看看吧举个例子。假设 Fashion MNIST 数据集仅包含 8 个类别,例如,除了凉鞋和衬衫之外的所有类别。有人在该集上构建并训练了一个 Keras 模型,并获得了相当好的性能(> 90% 的准确度)。让我们将此模型称为 A。您现在想要处理不同的任务:您有凉鞋和衬衫的图像,并且您想要训练二元分类器(正=衬衫,负=凉鞋)。您的数据集很小;您只有 200 个标记图像。当您为此任务训练一个与模型 A 具有相同架构的新模型(我们称之为模型 B)时,它的性能相当好(97.2% 的准确率)。但由于这是一项容易得多的任务(只有两个类),因此您希望获得更多。早上喝咖啡时,您意识到您的任务与任务 A 非常相似,所以迁移学习可能会有所帮助?让我们来了解一下!
首先,您需要加载模型 A 并基于该模型的层创建一个新模型。让我们重用除输出层之外的所有层:
model_A = keras.models.load_model("my_model_A.h5")
model_B_on_A = keras.models.Sequential(model_A.layers[:-1])
model_B_on_A.add(keras.layers.Dense(1, activation="sigmoid"))
请注意,model_A
现在model_B_on_A
共享一些图层。当你训练model_B_on_A
时,它也会有影响model_A
。如果您想避免这种情况,则需要在重用其层之前进行克隆。 model_A
为此,您使用 克隆模型 A 的架构clone_model()
,然后复制其权重(因为clone_model()
不克隆权重):
model_A_clone = keras.models.clone_model(model_A)
model_A_clone.set_weights(model_A.get_weights())
现在你可以为任务 B 进行训练model_B_on_A
,但是由于新的输出层是随机初始化的,它会产生很大的错误(至少在前几个 epoch 期间),所以会有很大的错误梯度,可能会破坏重用的权重。为了避免这种情况,一种方法是在前几个 epoch 冻结重用层,给新层一些时间来学习合理的权重。为此,请将每一层的trainable
属性设置为False
并编译模型:
for layer in model_B_on_A.layers[:-1]:
layer.trainable = False
model_B_on_A.compile(loss="binary_crossentropy", optimizer="sgd",
metrics=["accuracy"])
笔记
在冻结或解冻图层后,您必须始终编译您的模型。
现在您可以将模型训练几个 epoch,然后解冻重用层(这需要重新编译模型)并继续训练以微调任务 B 的重用层。解冻重用层后,通常是个好主意为了降低学习率,再次避免损坏重用的权重:
history = model_B_on_A.fit(X_train_B, y_train_B, epochs=4,
validation_data=(X_valid_B, y_valid_B))
for layer in model_B_on_A.layers[:-1]:
layer.trainable = True
optimizer = keras.optimizers.SGD(lr=1e-4) # the default lr is 1e-2
model_B_on_A.compile(loss="binary_crossentropy", optimizer=optimizer,
metrics=["accuracy"])
history = model_B_on_A.fit(X_train_B, y_train_B, epochs=16,
validation_data=(X_valid_B, y_valid_B))
那么,最终的判决是什么?好吧,这个模型的测试准确率是 99.25%,这意味着迁移学习将错误率从 2.8% 降低到几乎 0.7%!那是四倍!
>>> model_B_on_A.evaluate(X_test_B, y_test_B)
[0.06887910133600235, 0.9925]
你确信吗?你不应该:我作弊了!我尝试了许多配置,直到找到一个表现出强大改进的配置。如果您尝试更改类或随机种子,您会发现改进通常会下降,甚至消失或逆转。我所做的被称为“折磨数据直到它承认为止”。当一篇论文看起来太积极时,你应该怀疑:也许华丽的新技术实际上并没有太大帮助(事实上,它甚至可能会降低性能),但作者尝试了许多变体并且只报告了最好的结果(这可能是由于纯粹的运气),没有提及他们在途中遇到了多少失败。大多数情况下,这根本不是恶意的,但它是许多科学成果永远无法复制的部分原因。
我为什么作弊?事实证明,迁移学习在小型密集网络上效果不佳,大概是因为小型网络学习的模式很少,而密集网络学习的模式非常具体,这在其他任务中不太可能有用。迁移学习最适用于深度卷积神经网络,它倾向于学习更通用的特征检测器(尤其是在较低层中)。我们将在第 14 章重新讨论迁移学习,使用我们刚刚讨论过的技术(这次不会有作弊,我保证!)。
无监督预训练
认为您想处理一项没有太多标记训练数据的复杂任务,但不幸的是,您找不到针对类似任务进行训练的模型。不要失去希望!首先,你应该尝试收集更多有标签的训练数据,但如果你不能,你仍然可以进行无监督的预训练(见图 11-5)。事实上,收集未标记的训练示例通常很便宜,但标记它们却很昂贵。如果你能收集到大量未标记的训练数据,你可以尝试用它来训练无监督模型,例如自动编码器或生成对抗网络(参见第 17 章))。然后,您可以重用自动编码器的较低层或 GAN 判别器的较低层,在顶部添加任务的输出层,并使用监督学习(即使用标记的训练示例)微调最终网络。
Geoffrey Hinton 和他的团队在 2006 年使用的正是这种技术,它导致了神经网络的复兴和深度学习的成功。直到 2010 年,无监督预训练——通常使用受限玻尔兹曼机(RBM;参见附录 E)——是深度网络的规范,只有在梯度消失问题得到缓解之后,纯粹使用监督学习来训练 DNN 才变得更加普遍。当您需要解决复杂的任务、没有可以重复使用的类似模型、标记的训练数据很少但有大量未标记的训练数据时,无监督预训练(今天通常使用自动编码器或 GAN 而不是 RBM)仍然是一个不错的选择。
请注意,在深度学习的早期,很难训练深度模型,所以人们会使用一种称为贪婪逐层预训练的技术(如图 11-5 所示)。他们首先会训练一个单层的无监督模型,通常是一个 RBM,然后他们将冻结该层并在其上添加另一个层,然后再次训练模型(实际上只是训练新层),然后冻结新层并在其上添加另一个层,再次训练模型, 等等。如今,事情要简单得多:人们通常一次性训练完整的无监督模型(即,在图 11-5中,直接从第三步开始)并使用自动编码器或 GAN,而不是 RBM。
图 11-5。在无监督训练中,使用无监督学习技术在未标记数据(或所有数据)上训练模型,然后使用监督学习技术对标记数据进行微调以完成最终任务;无监督部分可以一次训练一层,如这里所示,也可以直接训练完整模型
辅助任务的预训练
如果您没有太多带标签的训练数据,最后一个选择是在辅助任务上训练第一个神经网络,您可以轻松获得或生成带标签的训练数据,然后将该网络的较低层重用于您的实际任务。第一个神经网络的较低层将学习可能被第二个神经网络重用的特征检测器。
例如,如果你想建立一个系统来识别人脸,你可能只有几张每个人的照片——显然不足以训练一个好的分类器。收集每个人的数百张照片是不切实际的。但是,您可以在网络上收集大量随机人物的照片,并训练第一个神经网络来检测两张不同的照片是否以同一个人为特征。这样的网络将学习好的人脸特征检测器,因此重用它的较低层将允许您训练一个使用很少训练数据的好的人脸分类器。
对于自然语言处理(NLP) 应用程序,您可以下载数百万个文本文档的语料库,并从中自动生成标记数据。例如,您可以随机屏蔽一些单词并训练一个模型来预测缺失的单词是什么(例如,它应该预测句子“What ___ you say?”中缺失的单词可能是“are”或“were” )。如果您可以训练一个模型以在此任务上达到良好的性能,那么它已经对语言了解很多,您当然可以将它重用于您的实际任务并在您的标记数据上对其进行微调(我们将讨论更多预训练第 15 章中的任务)。
更快的优化器
训练一个非常大的深度神经网络可能会非常缓慢。到目前为止,我们已经看到了四种加速训练(并获得更好的解决方案)的方法:对连接权重应用良好的初始化策略、使用良好的激活函数、使用 Batch Normalization 以及重用预训练网络的部分(可能构建在辅助任务或使用无监督学习)。另一个巨大的速度提升来自使用比常规梯度下降优化器更快的优化器。在本节中,我们将介绍最流行的算法:动量优化、Nesterov 加速梯度、AdaGrad、RMSProp,最后是 Adam 和 Nadam优化。
动量优化
想象一个保龄球在光滑的表面上沿着缓坡滚下:它会慢慢开始,但会很快获得动量,直到最终达到最终速度(如果有一些摩擦或空气阻力)。这是动量优化背后的非常简单的想法,由 Boris Polyak 在 1964 年提出。13相比之下,常规梯度下降只会在斜坡上采取小的、规则的步骤,因此算法将需要更多的时间才能到达底部。
回想一下,梯度下降通过直接减去成本函数J ( θ ) 关于权重 (∇ θ J ( θ )) 乘以学习率 η的梯度来更新权重θ。方程为:θ ← θ – η ∇ θ J ( θ )。它不关心早期的渐变是什么。如果局部梯度很小,它会非常缓慢。
动量优化非常关心之前的梯度是什么:在每次迭代中,它从动量向量 m中减去局部梯度(乘以学习率η),并通过添加这个动量向量来更新权重(参见公式 11-4) . 换句话说,梯度用于加速,而不是速度。为了模拟某种摩擦机制并防止动量增长过大,该算法引入了一个新的超参数β,称为动量,它必须设置在 0(高摩擦)和 1(无摩擦)之间。典型的动量值为 0.9。
公式 11-4。动量算法

您可以轻松验证如果梯度保持不变,则终端速度(即权重更新的最大大小)等于该梯度乘以学习率η乘以 1/(1– β )(忽略符号) . 例如,如果β = 0.9,那么终端速度等于梯度乘以学习率的 10 倍,因此动量优化最终比梯度下降快 10 倍!这允许动量优化比梯度下降更快地摆脱高原。我们在第 4 章中看到,当输入具有非常不同的尺度时,成本函数将看起来像一个拉长的碗(见图 4-7)。Gradient Descent下陡坡的速度非常快,但是下山需要很长时间。相比之下,动量优化会越来越快地滚下山谷,直到到达底部(最优)。在不使用 Batch Normalization 的深度神经网络中,上层通常最终会得到具有非常不同尺度的输入,因此使用动量优化有很大帮助。它还可以帮助超越局部最优。
笔记
由于动量,优化器可能会稍微过冲,然后回来,再次过冲,并像这样振荡很多次,然后稳定在最小值。这就是在系统中有一点摩擦是好事的原因之一:它消除了这些振荡,从而加速了收敛。
在 Keras 中实现动量优化是一件轻而易举的事:只需使用SGD
优化器并设置其momentum
超参数,然后躺下获利!
optimizer = keras.optimizers.SGD(lr=0.001, momentum=0.9)
动量优化的一个缺点是它添加了另一个超参数来调整。但是,动量值 0.9 通常在实践中效果很好,并且几乎总是比常规梯度下降更快。
Nesterov 加速梯度
一动量优化的小变体,由Yurii Nesterov 在 1983 年提出,14几乎总是比普通动量快优化。Nesterov 加速梯度(NAG) 方法,也称为Nesterov 动量优化,不是在局部位置θ处测量成本函数的梯度,而是在θ + β m处的动量方向稍前(参见公式 11-5)。
公式 11-5。Nesterov 加速梯度算法

这个小调整是有效的,因为通常动量矢量将指向正确的方向(即,朝向最优),因此使用在该方向上测量得更远一点的梯度而不是原始梯度会稍微更准确位置,如图 11-6 所示(其中∇ 1表示在起点θ处测量的成本函数的梯度,∇ 2表示位于θ + β m处的点的梯度)。
如您所见,Nesterov 更新最终稍微接近最佳值。一段时间后,这些小的改进加起来,最终 NAG 比常规动量优化快得多。此外,请注意,当动量推动权重越过山谷时,∇ 1会继续推动越过山谷,而 ∇ 2会向后推向山谷底部。这有助于减少振荡,因此 NAG 收敛得更快。
NAG 通常比常规动量优化更快。要使用它,只需nesterov=True
在创建SGD
优化器时设置:
optimizer = keras.optimizers.SGD(lr=0.001, momentum=0.9, nesterov=True)
图 11-6。常规与 Nesterov 动量优化:前者应用在动量步骤之前计算的梯度,而后者应用在之后计算的梯度
AdaGrad
考虑再次出现拉长碗问题:梯度下降首先快速下降最陡峭的斜坡,该斜坡并不直接指向全局最优值,然后非常缓慢地下降到谷底。如果该算法能够更早地纠正其方向以更加指向全局最优值,那就太好了。AdaGrad算法15通过沿最陡的维度按比例缩小梯度向量来实现此校正(参见公式11-6)。
公式 11-6。AdaGrad 算法

第一步将梯度的平方累加到向量s中(回想一下,⊗ 符号表示逐元素乘法)。这种向量化形式等价于计算向量s的每个元素s i的s i ← s i + (∂ J ( θ ) / ∂ θ i ) 2;换句话说,每个s i都会累积成本函数关于参数θ i的偏导数的平方。如果代价函数沿第i个维度是陡峭的,那么s i在每次迭代中都会变得越来越大。
在简而言之,这个算法会降低学习率,但是对于陡峭的维度,它比对于具有平缓坡度的维度更快。这称为自适应学习率。它有助于将生成的更新更直接地指向全局最优值(见图 11-7)。另一个好处是它需要的学习率超参数η的调整要少得多。
图 11-7。AdaGrad vs Gradient Descent:前者可以更早地纠正其方向以指向最优
AdaGrad 经常在简单的二次问题上表现良好,但在训练神经网络时往往过早停止。学习率被大幅缩减,以至于算法最终在达到全局最优之前完全停止。因此,即使 Keras 有一个Adagrad
优化器,你也不应该用它来训练深度神经网络(不过,它对于线性回归等更简单的任务可能很有效)。尽管如此,了解 AdaGrad 有助于掌握其他自适应学习率优化器。
RMSProp
正如我们所见,AdaGrad 冒着放慢速度的风险下降得太快了,永远不会收敛到全局最优值。RMSProp算法16通过仅累积最近迭代的梯度(而不是自训练开始以来的所有梯度)来解决此问题。它通过在第一步中使用指数衰减来实现(参见公式 11-7)。
公式 11-7。RMSProp 算法

衰减率β通常设置为 0.9。是的,它又是一个新的超参数,但是这个默认值通常效果很好,所以你可能根本不需要调整它。
正如你所料,Keras 有一个RMSprop
优化器:
optimizer = keras.optimizers.RMSprop(lr=0.001, rho=0.9)
请注意,rho
参数对应于公式 11-7中的β。除了在非常简单的问题上,这个优化器几乎总是比 AdaGrad 表现得更好。事实上,在 Adam 优化出现之前,它是许多研究人员的首选优化算法。
Adam and Nadam Optimization
17岁的亚当代表自适应矩估计,结合了动量优化和RMSProp的思想:就像动量优化一样,它跟踪过去梯度的指数衰减平均值;就像 RMSProp 一样,它跟踪过去平方梯度的指数衰减平均值(参见公式 11-8)。18
公式 11-8。Adam 算法

在这个等式中,t表示迭代次数(从 1 开始)。
如果您只看步骤 1、2 和 5,您会注意到 Adam 与动量优化和 RMSProp 非常相似。唯一的区别是第 1 步计算的是指数衰减平均值而不是指数衰减和,但这些实际上是等价的,除了一个常数因子(衰减平均值只是衰减和的 1 - β 1倍)。第 3 步和第 4 步有点技术细节:由于m和s初始化为 0,因此在训练开始时它们会偏向 0,因此这两个步骤将有助于在训练开始时提升m和s。
动量衰减超参数β 1通常初始化为 0.9,而缩放衰减超参数β 2通常初始化为 0.999。如前所述,平滑项ε通常初始化为一个很小的数字,例如 10 –7。这些是Adam
类的默认值(准确地说,epsilon
默认为None
,它告诉 Keras 使用keras.backend.epsilon()
,默认为 10 –7;您可以使用 更改它keras.backend.set_epsilon()
)。以下是使用 Keras 创建 Adam 优化器的方法:
optimizer = keras.optimizers.Adam(lr=0.001, beta_1=0.9, beta_2=0.999)
由于 Adam 是一种自适应学习率算法(如 AdaGrad 和 RMSProp),因此它需要较少的学习率超参数η调整。您通常可以使用默认值η = 0.001,这使得 Adam 比 Gradient Descent 更易于使用。
小费
如果您开始对所有这些不同的技术感到不知所措,并且想知道如何为您的任务选择合适的技术,请不要担心:本章末尾提供了一些实用指南。
最后,值得一提的是 Adam 的两个变体:
AdaMax
但愿如此
Nadam 优化是 Adam 优化加上 Nesterov 技巧,因此它的收敛速度通常比 Adam 略快。在他介绍这项技术的报告中,19研究员 Timothy Dozat 在各种任务上比较了许多不同的优化器,发现 Nadam 通常优于 Adam,但有时优于 RMSProp。
警告
自适应优化方法(包括 RMSProp、Adam 和 Nadam 优化)通常很棒,可以快速收敛到一个好的解决方案。然而,Ashia C. Wilson 等人在2017 年发表的一篇论文20。表明它们可以导致在某些数据集上泛化较差的解决方案。因此,当您对模型的性能感到失望时,请尝试使用普通的 Nesterov 加速梯度:您的数据集可能只是对自适应梯度过敏。还要查看最新的研究,因为它发展迅速。
全部到目前为止讨论的优化技术仅依赖于一阶偏导数(雅可比行列式)。优化文献还包含基于二阶偏导数(Hessians,雅可比矩阵的偏导数)的惊人算法。不幸的是,这些算法很难应用于深度神经网络,因为每个输出有n 2 Hessians(其中n是参数的数量),而不是只有n每个输出的雅可比行列式。由于 DNN 通常有数以万计的参数,二阶优化算法通常甚至不适合内存,即使它们适合,计算 Hessians 也太慢了。
训练稀疏模型
全部刚刚介绍的优化算法会产生密集模型,这意味着大多数参数将是非零的。如果您在运行时需要一个速度极快的模型,或者如果您需要它占用更少的内存,您可能更愿意使用稀疏模型来代替。
实现这一点的一种简单方法是像往常一样训练模型,然后去掉微小的权重(将它们设置为零)。请注意,这通常不会导致模型非常稀疏,并且可能会降低模型的性能。
更好的选择是在训练期间应用强 ℓ 1正则化(我们将在本章后面看到),因为它会推动优化器将尽可能多的权重归零(如第 4 章的“套索回归”中所述)。
如果这些技术仍然不够用,请查看TensorFlow 模型优化工具包 (TF-MOT),它提供了一个修剪 API,能够在训练期间根据连接的大小迭代删除连接。
表 11-2比较了我们目前讨论过的所有优化器(* 是坏的,**是一般的,***是好的)。
班级 | 收敛速度 | 收敛质量 |
---|---|---|
|
* |
*** |
|
** |
*** |
|
** |
*** |
|
*** |
*(过早停止) |
|
*** |
** 或者 *** |
|
*** |
** 或者 *** |
|
*** |
** 或者 *** |
|
*** |
** 或者 *** |
学习率调度
发现良好的学习率非常重要。如果你将它设置得太高,训练可能会出现分歧(正如我们在“梯度下降”中讨论的那样)。如果设置得太低,训练最终会收敛到最优,但需要很长时间。如果你把它设置得太高,一开始它会很快取得进展,但最终会在最佳值附近跳舞,永远不会真正安定下来。如果您的计算预算有限,您可能不得不在训练正确收敛之前中断训练,从而产生次优解决方案(见图 11-8)。
图 11-8。各种学习率 η 的学习曲线
正如我们在第 10 章中所讨论的,你可以通过训练模型数百次迭代,以指数方式将学习率从一个非常小的值增加到一个非常大的值,然后查看学习曲线并选择一个合适的学习率。学习率略低于学习曲线开始回升的那个。然后,您可以重新初始化您的模型并使用该学习率对其进行训练。
但是你可以比恒定学习率做得更好:如果你从一个大的学习率开始,然后在训练停止快速进展时降低它,你可以比使用最佳的恒定学习率更快地找到一个好的解决方案。有许多不同的策略可以在训练期间降低学习率。从低学习率开始,提高它,然后再降低它也是有益的。这些策略被称为学习计划(我们在第 4 章中简要介绍了这个概念)。这些是最常用的学习时间表:
电源调度
放学习率是迭代次数t的函数:η ( t ) = η 0 / (1 + t / s ) c。初始学习率η 0、功率c(通常设置为 1)和步长s是超参数。每一步学习率都会下降。经过s步,下降到η 0 / 2。再经过s步,下降到η 0 / 3,然后下降到η 0 / 4,然后η 0/5,以此类推。正如你所看到的,这个时间表首先下降得很快,然后越来越慢。当然,功率调度需要调整η 0和s(可能还有c)。
指数调度
放学习率η ( t ) = η 0 0.1 t/s。每s步,学习率将逐渐下降 10 倍。虽然功率调度越来越慢地降低学习率,但指数调度每s步将其降低 10 倍。
分段常数调度
利用多个时期的恒定学习率(例如, 5 个时期的η 0 = 0.1),然后另一个时期的较小学习率(例如, 50 个时期的η 1 = 0.001),依此类推。虽然这个解决方案可以很好地工作,但它需要摆弄才能找出正确的学习率序列以及每个学习率的使用时间。
性能调度
措施每N步验证错误(就像提前停止一样),当错误停止下降时,将学习率降低λ倍。
1周期调度
相反与其他方法相比,1cycle(由 Leslie Smith 在2018 年的一篇论文21中介绍)从增加初始学习率η 0开始,在训练中途线性增长到η 1 。然后它在训练的后半部分再次将学习率线性降低到η 0 ,通过将学习率降低几个数量级(仍然是线性的)来完成最后几个 epoch。最大学习率η 1的选择与我们用来寻找最佳学习率的方法相同,初始学习率η 0选择大约低 10 倍。使用动量时,我们首先从高动量(例如,0.95)开始,然后在训练的前半部分将其降低到较低的动量(例如,线性降低到 0.85),然后将其恢复到训练后半段的最大值(例如 0.95),以该最大值结束最后几个 epoch。史密斯做了许多实验,表明这种方法通常能够大大加快训练速度并达到更好的性能。例如,在流行的 CIFAR10 图像数据集上,这种方法仅在 100 个 epoch 内就达到了 91.9% 的验证准确率,而不是通过标准方法(具有相同的神经网络架构)在 800 个 epoch 内达到 90.3% 的准确率。
Andrew Senior 等人在2013 年发表的论文22。比较了一些最流行的学习计划在使用动量优化训练深度神经网络进行语音识别时的性能。作者得出结论,在这种情况下,性能调度和指数调度都表现良好。他们偏爱指数调度,因为它易于调整,并且收敛到最佳解决方案的速度稍快(他们还提到它比性能调度更容易实现,但在 Keras 中,这两个选项都很容易)。也就是说,1cycle 方法似乎表现得更好。
在 Keras 中实现功率调度是最简单的选择:只需decay
在创建优化器时设置超参数:
optimizer = keras.optimizers.SGD(lr=0.01, decay=1e-4)
这decay
是s的倒数(将学习率除以一个单位所需的步数),Keras 假设c等于 1。
指数调度和分段调度也很简单。您首先需要定义一个函数,该函数采用当前 epoch 并返回学习率。例如,让我们实现指数调度:
def exponential_decay_fn(epoch):
return 0.01 * 0.1**(epoch / 20)
如果您不想硬编码η 0和s,您可以创建一个返回配置函数的函数:
def exponential_decay(lr0, s):
def exponential_decay_fn(epoch):
return lr0 * 0.1**(epoch / s)
return exponential_decay_fn
exponential_decay_fn = exponential_decay(lr0=0.01, s=20)
接下来,创建一个LearningRateScheduler
回调,为其提供调度函数,并将此回调传递给该fit()
方法:
lr_scheduler = keras.callbacks.LearningRateScheduler(exponential_decay_fn)
history = model.fit(X_train_scaled, y_train, [...], callbacks=[lr_scheduler])
将在每个 epoch 开始LearningRateScheduler
时更新优化器的learning_rate
属性。通常每个 epoch 更新一次学习率就足够了,但是如果你希望它更频繁地更新,例如在每一步,你总是可以编写自己的回调(参见 notebook 的“Exponential Scheduling”部分的例子) . 如果每个 epoch 有很多步,那么在每一步更新学习率是有意义的。或者,您可以使用keras.optimizers.schedules
稍后描述的方法。
schedule 函数可以选择将当前学习率作为第二个参数。例如,以下调度函数将先前的学习率乘以 0.1 1/20,这导致相同的指数衰减(除了衰减现在从 epoch 0 而不是 1 开始):
def exponential_decay_fn(epoch, lr):
return lr * 0.1**(1 / 20)
此实现依赖于优化器的初始学习率(与之前的实现相反),因此请确保对其进行适当设置。
当您保存模型时,优化器及其学习率会随之保存。这意味着使用这个新的计划功能,您可以加载一个训练好的模型并继续训练它停止的地方,没问题。但是,如果您的 schedule 函数使用该参数,事情就不那么简单了epoch
:epoch 不会被保存,并且每次调用该fit()
方法时它都会重置为 0。如果您要从中断的地方继续训练模型,这可能会导致非常大的学习率,这可能会损坏模型的权重。一种解决方案是手动设置fit()
方法的initial_epoch
参数,以便epoch
从正确的值开始。
对于分段常量调度,您可以使用如下的调度函数(如前所述,您可以根据需要定义更通用的函数;示例参见笔记本的“分段常量调度”部分),然后创建LearningRateScheduler
回调使用这个函数并将其传递给fit()
方法,就像我们对指数调度所做的那样:
def piecewise_constant_fn(epoch):
if epoch < 5:
return 0.01
elif epoch < 15:
return 0.005
else:
return 0.001
对于性能调度,请使用ReduceLROnPlateau
回调。例如,如果您将以下回调传递给该fit()
方法,则每当最佳验证损失连续五个时期没有改善时,它将学习率乘以 0.5(其他选项可用;请查看文档以获取更多详细信息):
lr_scheduler = keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=5)
最后,tf. 大声提供了另一种实现学习率调度的方法:使用 中可用的调度定义学习率keras.optimizers.schedules
,然后将此学习率传递给任何优化器。这种方法在每一步而不是在每个时期更新学习率。例如,这里是如何实现与exponential_decay_fn()
我们之前定义的函数相同的指数调度:
s = 20 * len(X_train) // 32 # number of steps in 20 epochs (batch size = 32)
learning_rate = keras.optimizers.schedules.ExponentialDecay(0.01, s, 0.1)
optimizer = keras.optimizers.SGD(learning_rate)
这很好也很简单,而且当你保存模型时,学习率和它的时间表(包括它的状态)也会被保存。然而,这种方法不是 Keras API 的一部分。它特定于 tf.keras。
至于 1cycle 方法,实现没有特别困难:只需创建一个自定义回调,在每次迭代中修改学习率(您可以通过更改来更新优化器的学习率self.model.optimizer.lr
)。有关示例,请参见笔记本的“1Cycle 调度”部分。
通过正则化避免过拟合
用四个参数我可以适应一头大象,用五个我可以让他摆动他的鼻子。
John von Neumann,Enrico Fermi 在Nature 427中引用
与数千的参数,你可以适应整个动物园。深度神经网络通常有数万个参数,有时甚至数百万个。这给了他们难以置信的自由度,意味着他们可以适应各种各样的复杂数据集。但是这种巨大的灵活性也使得网络容易过度拟合训练集。我们需要正则化。
我们已经在第 10 章中实现了最好的正则化技术之一:提前停止。此外,尽管 Batch Normalization 旨在解决不稳定的梯度问题,但它也起到了很好的正则化器的作用。在本节中,我们将研究其他流行的神经网络正则化技术:ℓ 1和 ℓ 2正则化、dropout 和最大范数正则化。
ℓ 1和 ℓ 2正则化
就像您在第 4 章中对简单线性模型所做的那样,您可以使用 ℓ 2正则化来约束神经网络的连接权重,和/或如果您想要一个稀疏模型(许多权重等于 0),则可以使用 ℓ 1正则化。以下是如何将 ℓ 2正则化应用于 Keras 层的连接权重,使用正则化因子 0.01:
layer = keras.layers.Dense(100, activation="elu",
kernel_initializer="he_normal",
kernel_regularizer=keras.regularizers.l2(0.01))
该l2()
函数返回一个正则化器,它将在训练期间的每一步调用以计算正则化损失。然后将其添加到最终损失中。如您所料,您可以只使用keras.regularizers.l1()
ℓ 1正则化;如果您想要 ℓ 1和 ℓ 2正则化,请使用keras.regularizers.l1_l2()
(指定两个正则化因子)。
由于您通常希望将相同的正则化器应用于网络中的所有层,并在所有隐藏层中使用相同的激活函数和相同的初始化策略,因此您可能会发现自己在重复相同的参数。这使得代码丑陋且容易出错。为避免这种情况,您可以尝试重构代码以使用循环。另一种选择是使用 Python 的functools.partial()
函数,它允许您为任何可调用对象创建一个瘦包装器,并带有一些默认参数值:
from functools import partial
RegularizedDense = partial(keras.layers.Dense,
activation="elu",
kernel_initializer="he_normal",
kernel_regularizer=keras.regularizers.l2(0.01))
model = keras.models.Sequential([
keras.layers.Flatten(input_shape=[28, 28]),
RegularizedDense(300),
RegularizedDense(100),
RegularizedDense(10, activation="softmax",
kernel_initializer="glorot_uniform")
])
Dropout
Dropout是深度神经网络最流行的正则化技术之一。它由 Geoffrey Hinton 在 2012 年的一篇论文23中提出,并在 Nitish Srivastava 等人2014 年的一篇论文24中进一步详细说明,并且已被证明非常成功:即使是最先进的神经网络也获得了 1只需添加 dropout 即可提高 –2% 的准确度。这听起来可能不是很多,但是当模型已经有 95% 的准确度时,获得 2% 的准确度提升意味着将错误率降低近 40%(从 5% 错误降低到大约 3%)。
这是一个相当简单的算法:在每个训练步骤中,每个神经元(包括输入神经元,但始终不包括输出神经元)都有一个暂时“退出”的概率p ,这意味着它在这个训练步骤中将被完全忽略,但它可能在下一步中处于活动状态(见图 11-9)。超参数p称为dropout rate,它通常设置在 10% 到 50% 之间:在循环神经网络中接近 20-30%(参见第 15 章),在卷积神经网络中接近 40-50%(参见第 14 章)。训练后,神经元不再掉落。这就是全部(除了我们将立即讨论的技术细节)。
图 11-9。使用 dropout 正则化,在每次训练迭代中,一个或多个层中所有神经元的随机子集(输出层除外)被“丢弃”;这些神经元在本次迭代中输出 0(由虚线箭头表示)
起初令人惊讶的是,这种破坏性技术完全有效。如果让员工每天早上扔硬币来决定是否上班,公司会表现得更好吗?好吧,谁知道呢;也许会!公司将被迫调整其组织;它不能依靠任何一个人来操作咖啡机或执行任何其他关键任务,因此这种专业知识必须分散在几个人身上。员工必须学会与许多同事合作,而不仅仅是少数几个。公司将变得更有弹性。如果一个人退出,它不会有太大的不同。目前尚不清楚这个想法是否真的适用于公司,但它肯定适用于神经网络。用 dropout 训练的神经元不能与其相邻的神经元共同适应;它们必须尽可能自己有用。他们也不能过度依赖少数几个输入神经元。他们必须注意每个输入神经元。他们最终对输入的细微变化不太敏感。最后,你会得到一个更健壮的网络,可以更好地泛化。
另一种理解 dropout 威力的方法是认识到在每个训练步骤都会生成一个独特的神经网络。由于每个神经元可以存在或不存在,因此总共有 2 N个可能的网络(其中N是可丢弃神经元的总数)。这是一个巨大的数字,几乎不可能对同一个神经网络进行两次采样。一旦你运行了 10,000 个训练步骤,你实际上已经训练了 10,000 个不同的神经网络(每个只有一个训练实例)。这些神经网络显然不是独立的,因为它们共享许多权重,但它们仍然是不同的。由此产生的神经网络可以看作是所有这些较小神经网络的平均集合。
小费
在实践中,您通常可以仅将 dropout 应用于顶部一到三层(不包括输出层)的神经元。
有一个小而重要的技术细节。假设p = 50%,在这种情况下,在测试期间,一个神经元连接到的输入神经元数量将是训练期间(平均)的两倍。为了弥补这一事实,我们需要在训练后将每个神经元的输入连接权重乘以 0.5。如果我们不这样做,每个神经元将获得大约两倍于网络训练的总输入信号,并且不太可能表现良好。更一般地,我们需要乘以每个输入连接权重由训练后的保持概率(1 - p ) 决定。或者,我们可以将每个神经元的输出除以训练期间的保持概率(这些替代方案并不完全等价,但它们同样有效)。
至使用 Keras 实现 dropout,可以使用keras.layers.Dropout
layer。在训练期间,它随机丢弃一些输入(将它们设置为 0)并将剩余的输入除以保持概率。训练后,它什么都不做;它只是将输入传递到下一层。以下代码在每Dense
一层之前应用 dropout 正则化,使用 0.2 的 dropout 率:
model = keras.models.Sequential([
keras.layers.Flatten(input_shape=[28, 28]),
keras.layers.Dropout(rate=0.2),
keras.layers.Dense(300, activation="elu", kernel_initializer="he_normal"),
keras.layers.Dropout(rate=0.2),
keras.layers.Dense(100, activation="elu", kernel_initializer="he_normal"),
keras.layers.Dropout(rate=0.2),
keras.layers.Dense(10, activation="softmax")
])
警告
由于 dropout 仅在训练期间处于活动状态,因此比较训练损失和验证损失可能会产生误导。特别是,模型可能过度拟合训练集,但具有相似的训练和验证损失。因此,请确保在没有 dropout 的情况下评估训练损失(例如,在训练之后)。
如果你观察到模型过拟合,你可以提高 dropout 率。相反,如果模型不适合训练集,您应该尝试降低 dropout 率。它还可以帮助增加大层的辍学率,并减少小层的辍学率。此外,许多最先进的架构仅在最后一个隐藏层之后使用 dropout,因此如果完全 dropout 太强,您可能想尝试一下。
Dropout 确实会显着减慢收敛速度,但如果调整得当,它通常会产生更好的模型。因此,通常值得花费额外的时间和精力。
Monte Carlo (MC) Dropout
在2016 年,Yarin Gal 和 Zoubin Ghahramani的一篇论文25添加了更多使用 dropout 的充分理由:
首先,该论文在 dropout 网络(即
Dropout
在每个权重层之前包含一层的神经网络)和近似贝叶斯推理之间建立了深刻的联系,26为 dropout 提供了坚实的数学依据。其次,作者介绍了一种称为MC Dropout的强大技术,它可以提高任何经过训练的 dropout 模型的性能,而无需重新训练甚至完全修改它,提供了对模型不确定性的更好测量,而且非常简单实施。
如果这一切听起来像是“一个奇怪的把戏”广告,那么看看下面的代码。它是MC Dropout的完整实现,提升了我们之前训练的 dropout 模型,而无需重新训练:
y_probas = np.stack([model(X_test_scaled, training=True)
for sample in range(100)])
y_proba = y_probas.mean(axis=0)
请注意,model(X)
它类似于model.predict(X)
返回张量而不是 NumPy 数组,并且它支持training
参数。在此代码示例中,设置training=True
可确保Dropout
图层保持活动状态,因此所有预测都会有所不同。我们只需对测试集进行 100 个预测,然后将它们堆叠起来。对模型的每次调用都会返回一个矩阵,其中每个实例一行,每个类一列。因为测试集中有 10,000 个实例和 10 个类,所以这是一个形状为 [10000, 10] 的矩阵。我们堆叠了 100 个这样的矩阵,因此y_probas
是一个形状为 [100, 10000, 10] 的数组。一旦我们对第一个维度( axis=0
) 进行平均,我们得到y_proba
,一个形状为 [10000, 10] 的数组,就像我们通过单个预测得到的一样。就这样!对带有 dropout 的多个预测进行平均,可以得到一个 Monte Carlo 估计值,该估计通常比带有 dropout 的单个预测的结果更可靠。例如,让我们看看模型对 Fashion MNIST 测试集中第一个实例的预测,去掉 dropout:
>>> np.round(model.predict(X_test_scaled[:1]), 2)
array([[0. , 0. , 0. , 0. , 0. , 0. , 0. , 0.01, 0. , 0.99]],
dtype=float32)
该模型似乎几乎可以肯定该图像属于第 9 类(踝靴)。你应该相信它吗?怀疑的余地真的那么小吗?将此与激活 dropout 时的预测进行比较:
>>> np.round(y_probas[:, :1], 2)
array([[[0. , 0. , 0. , 0. , 0. , 0.14, 0. , 0.17, 0. , 0.68]],
[[0. , 0. , 0. , 0. , 0. , 0.16, 0. , 0.2 , 0. , 0.64]],
[[0. , 0. , 0. , 0. , 0. , 0.02, 0. , 0.01, 0. , 0.97]],
[...]
这讲述了一个非常不同的故事:显然,当我们激活 dropout 时,模型不再确定。它似乎仍然更喜欢 9 级,但有时它对 5 级(凉鞋)和 7 级(运动鞋)犹豫不决,考虑到它们都是鞋类,这是有道理的。一旦我们对第一个维度进行平均,我们就会得到以下 MC Dropout 预测:
>>> np.round(y_proba[:1], 2)
array([[0. , 0. , 0. , 0. , 0. , 0.22, 0. , 0.16, 0. , 0.62]],
dtype=float32)
模型仍然认为这张图片属于第 9 类,但只有 62% 的置信度,这似乎比 99% 合理得多。另外,确切地知道它认为可能还有哪些其他类很有用。您还可以查看概率估计的标准差:
>>> y_std = y_probas.std(axis=0)
>>> np.round(y_std[:1], 2)
array([[0. , 0. , 0. , 0. , 0. , 0.28, 0. , 0.21, 0.02, 0.32]],
dtype=float32)
显然,概率估计存在很大差异:如果您正在构建一个风险敏感系统(例如,医疗或金融系统),您可能应该极其谨慎地对待这种不确定的预测。你绝对不会把它当作一个 99% 的自信预测。此外,模型的准确率从 86.8 小幅提升到 86.9:
>>> accuracy = np.sum(y_pred == y_test) / len(y_test)
>>> accuracy
0.8694
笔记
您使用的 Monte Carlo 样本数(本例中为 100 个)是您可以调整的超参数。它越高,预测及其不确定性估计就越准确。但是,如果加倍,推理时间也会加倍。此外,在一定数量的样本之上,您会注意到几乎没有改进。因此,您的工作是根据您的应用程序在延迟和准确性之间找到正确的权衡。
如果您的模型包含在训练期间以特殊方式表现的其他层(例如BatchNormalization
层),那么您不应该像我们刚才那样强制训练模式。相反,您应该将Dropout
图层替换为以下MCDropout
类:27
class MCDropout(keras.layers.Dropout):
def call(self, inputs):
return super().call(inputs, training=True)
在这里,我们只是将Dropout
层子类化并重写call()
方法以强制其training
参数为True
(参见第 12 章)。同样,您可以改为MCAlphaDropout
通过子类化来定义一个类AlphaDropout
。如果您从头开始创建模型,则只需使用MCDropout
而不是Dropout
. 但是,如果您有一个已经使用 训练过的模型Dropout
,您需要创建一个与现有模型相同的新模型,只是它将Dropout
层替换为MCDropout
,然后将现有模型的权重复制到您的新模型中。
简而言之,MC Dropout 是一种很棒的技术,可以提升 dropout 模型并提供更好的不确定性估计。当然,由于它只是训练期间的常规 dropout,它也起到了正则化器的作用。
最大范数正则化
其他神经网络中流行的正则化技术称为最大范数正则化:对于每个神经元,它限制传入连接的权重w使得 ∥ w ∥ 2 ≤ r,其中r是最大范数超参数,∥ · ∥ 2是 ℓ 2范数。
最大范数正则化不会在整体损失函数中添加正则化损失项。相反,它通常通过在每个训练步骤之后计算 ∥ w ∥ 2并在需要时重新调整 w 来实现(w ← w r / ” w ” 2)。
减少r会增加正则化的数量并有助于减少过度拟合。Max-norm 正则化也可以帮助缓解不稳定的梯度问题(如果你没有使用 Batch Normalization)。
要在 Keras 中实现最大范数正则化,请将kernel_constraint
每个隐藏层的参数设置为max_norm()
具有适当最大值的约束,如下所示:
keras.layers.Dense(100, activation="elu", kernel_initializer="he_normal",
kernel_constraint=keras.constraints.max_norm(1.))
在每次训练迭代之后,模型的fit()
方法将调用返回的对象max_norm()
,将层的权重传递给它,并获得重新调整的权重作为回报,然后替换层的权重。正如您将在第 12 章中看到的,如果需要,您可以定义自己的自定义约束函数并将其用作kernel_constraint
. 您还可以通过设置bias_constraint
参数来约束偏差项。
该max_norm()
函数有一个axis
默认为 的参数0
。一层Dense
通常具有形状权重 [输入数,神经元数],因此使用axis=0
意味着最大范数约束将独立应用于每个神经元的权重向量。如果你想对卷积层使用 max-norm(参见第 14 章),请确保适当地设置max_norm()
约束的axis
参数(通常是axis=[0, 1, 2]
)。
摘要和实用指南
在本章我们涵盖了广泛的技术,您可能想知道应该使用哪些技术。这取决于任务,目前还没有明确的共识,但我发现表 11-3中的配置在大多数情况下都可以正常工作,不需要太多的超参数调整。也就是说,请不要将这些默认设置视为硬性规则!
超参数 | 默认值 |
---|---|
内核初始化器 |
他初始化 |
激活功能 |
向上 |
正常化 |
浅则无;深度批量标准化 |
正则化 |
提前停止(+ℓ 2 reg. 如果需要) |
优化器 |
动量优化(或 RMSProp 或 Nadam) |
学习率时间表 |
1个周期 |
如果网络是一个简单的密集层堆栈,那么它可以自归一化,你应该使用表 11-4中的配置来代替。
超参数 | 默认值 |
---|---|
内核初始化器 |
He initialization |
激活功能 |
ELU |
正常化 |
无(自标准化) |
正则化 |
Early stopping (+ℓ2 reg. if needed) |
优化器 |
动量优化(或 RMSProp 或 Nadam) |
学习率时间表 |
1个周期 |
不要忘记规范化输入特征!如果你能找到一个解决类似问题的预训练神经网络,你也应该尝试重用部分预训练神经网络,或者如果你有很多未标记的数据,则使用无监督预训练,或者如果你有很多标记数据,则对辅助任务使用预训练类似任务的数据。
虽然之前的指南应涵盖大多数情况,但这里有一些例外情况:
如果您需要一个稀疏模型,您可以使用 ℓ 1正则化(并且可以在训练后将微小的权重归零)。如果您需要更稀疏的模型,可以使用 TensorFlow 模型优化工具包。这将破坏自标准化,因此在这种情况下您应该使用默认配置。
如果您需要低延迟模型(执行闪电般快速预测的模型),您可能需要使用更少的层,将 Batch Normalization 层折叠到前面的层中,并可能使用更快的激活函数,例如leaky ReLU 或仅 ReLU . 拥有稀疏模型也会有所帮助。最后,您可能希望将浮点精度从 32 位降低到 16 位甚至 8 位(请参阅“将模型部署到移动或嵌入式设备”)。再次,查看 TF-MOT。
如果您正在构建对风险敏感的应用程序,或者推理延迟在您的应用程序中不是很重要,则可以使用 MC Dropout 来提高性能并获得更可靠的概率估计以及不确定性估计。
有了这些指南,您现在就可以训练非常深的网络了!我希望您现在确信仅使用 Keras 可以走很长一段路。但是,有时您可能需要拥有更多控制权。例如,编写自定义损失函数或调整训练算法。对于这种情况,您将需要使用 TensorFlow 的低级 API,您将在下一章中看到。
练习
只要使用 He 初始化随机选择该值,是否可以将所有权重初始化为相同的值?
将偏置项初始化为 0 可以吗?
列举 SELU 激活函数相对于 ReLU 的三个优势。
在哪些情况下,您希望使用以下每个激活函数:SELU、leaky ReLU(及其变体)、ReLU、tanh、logistic 和 softmax?
如果在使用优化器
momentum
时将超参数设置得太接近 1(例如 0.99999),会发生什么情况?SGD
说出生成稀疏模型的三种方法。
辍学会减慢训练速度吗?它会减慢推理速度(即对新实例进行预测)吗?MC辍学呢?
练习在 CIFAR10 图像数据集上训练深度神经网络:
构建一个包含 20 个隐藏层的 DNN,每个隐藏层 100 个神经元(太多了,但这是本练习的重点)。使用 He 初始化和 ELU 激活函数。
使用 Nadam 优化和提前停止,在 CIFAR10 数据集上训练网络。您可以使用
keras.datasets.cifar10.load_data()
. 该数据集由 60,000 个 32 × 32 像素的彩色图像(50,000 个用于训练,10,000 个用于测试)和 10 个类别组成,因此您需要一个具有 10 个神经元的 softmax 输出层。每次更改模型的架构或超参数时,请记住搜索正确的学习率。现在尝试添加 Batch Normalization 并比较学习曲线:它的收敛速度是否比以前更快?它会产生更好的模型吗?它如何影响训练速度?
尝试用 SELU 替换 Batch Normalization,并进行必要的调整以确保网络自归一化(即标准化输入特征、使用 LeCun 正态初始化、确保 DNN 仅包含一系列密集层等)。
尝试使用 alpha dropout 规范化模型。然后,在不重新训练您的模型的情况下,看看您是否可以使用 MC Dropout 获得更好的准确度。
使用 1cycle 调度重新训练您的模型,看看它是否提高了训练速度和模型准确性。
附录 A中提供了这些练习的解决方案。
1Xavier Glorot 和 Yoshua Bengio,“了解训练深度前馈神经网络的难度” ,第 13 届人工智能与统计国际会议论文集(2010 年):249-256。
2打个比方:如果你将麦克风放大器的旋钮设置得太接近零,人们就听不到你的声音,但如果你把它设置得太接近最大值,你的声音就会饱和,人们听不懂你在说什么. 现在想象一下这样的放大器链:它们都需要正确设置,以便您的声音在链的末端发出响亮而清晰的声音。您的声音必须以与传入时相同的幅度从每个放大器中传出。
3例如,Kaiming He 等人,“Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification” ,2015 年 IEEE 计算机视觉国际会议论文集(2015 年):1026-1034。
4除非它是第一个隐藏层的一部分,否则死去的神经元有时可能会复活:梯度下降确实可能会调整下面层中的神经元,使死去的神经元输入的加权和再次为正。
5Bing Xu 等人,“卷积网络中整流激活的实证评估”,arXiv 预印本 arXiv:1505.00853 (2015)。
6Djork-Arné Clevert 等人,“Fast and Accurate Deep Network Learning by Exponential Linear Units (ELUs)” ,国际学习表示会议论文集(2016 年)。
7Günter Klambauer 等人,“自归一化神经网络” ,第 31 届神经信息处理系统国际会议论文集(2017 年):972-981。
8Sergey Ioffe 和 Christian Szegedy,“批量标准化:通过减少内部协变量偏移来加速深度网络训练” ,第 32 届机器学习国际会议论文集(2015 年):448-456。
9但是,它们是在训练期间根据训练数据进行估计的,因此可以说它们是可训练的。在 Keras 中,“不可训练”实际上意味着“不受反向传播影响”。
10Keras API 还指定了一个在训练期间和其他情况keras.backend.learning_phase()
下应该返回的函数。1
0
11Hongyi Zhang 等人,“Fixup Initialization: Residual Learning without Normalization”,arXiv 预印本 arXiv:1901.09321 (2019)。
12Razvan Pascanu 等人,“On the Difficulty of Training Recurrent Neural Networks” ,第 30 届机器学习国际会议论文集(2013 年):1310-1318。
13Boris T. Polyak,“加速迭代方法收敛的一些方法”,苏联计算数学和数学物理4,第 4 期。5(1964):1-17。
14Yurii Nesterov,“一种收敛速度为 O(1/k) 的无约束凸最小化问题的方法2),”文件 AN USSR 269 (1983): 543–547。
15John Duchi 等人,“在线学习和随机优化的自适应次梯度方法” ,机器学习研究杂志12(2011):2121-2159。
16该算法由 Geoffrey Hinton 和 Tijmen Tieleman 于 2012 年创建,并由 Geoffrey Hinton 在他的 Coursera 神经网络课程中提出(幻灯片:https ://homl.info/57 ;视频:https ://homl.info/58 )。有趣的是,由于作者没有写论文来描述该算法,研究人员经常在他们的论文中引用“第 6 课中的幻灯片 29”。
17Diederik P. Kingma 和 Jimmy Ba,“Adam:一种随机优化方法”,arXiv 预印本 arXiv:1412.6980 (2014)。
18这些是对梯度的均值和(非中心)方差的估计。均值通常称为一阶矩,而方差通常称为二阶矩,因此称为算法的名称。
19Timothy Dozat,“将 Nesterov Momentum 融入 Adam”(2016 年)。
20Ashia C. Wilson 等人,“机器学习中自适应梯度方法的边际价值” ,神经信息处理系统进展30(2017):4148–4158。
21Leslie N. Smith,“A Disciplined Approach to Neural Network Hyper-Parameters: Part 1 — Learning Rate, Batch Size, Momentum, and Weight Decay”,arXiv 预印本 arXiv:1803.09820 (2018)。
22Andrew Senior 等人,“An Empirical Study of Learning Rates in Deep Neural Networks for Speech Recognition” ,IEEE 国际声学、语音和信号处理会议论文集(2013 年):6724-6728。
23Geoffrey E. Hinton 等人,“通过防止特征检测器的共同适应来改进神经网络”,arXiv 预印本 arXiv:1207.0580 (2012)。
24Nitish Srivastava 等人,“Dropout:防止神经网络过度拟合的简单方法” ,机器学习研究杂志15(2014):1929-1958。
25Yarin Gal 和 Zoubin Ghahramani,“作为贝叶斯近似的辍学:表示深度学习中的模型不确定性” ,第 33 届机器学习国际会议论文集(2016 年):1050-1059。
26具体来说,他们表明训练 dropout 网络在数学上等同于在称为深度高斯过程的特定类型概率模型中的近似贝叶斯推理。
27此类MCDropout
将适用于所有 Keras API,包括 Sequential API。如果只关心Functional API 或Subclassing API,则不必创建MCDropout
类;您可以创建一个常规Dropout
图层并使用training=True
.