Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(四)(2)https://developer.aliyun.com/article/1482422
批量归一化
尽管使用 He 初始化与 ReLU(或其任何变体)可以显著减少训练开始时梯度消失/爆炸问题的危险,但并不能保证它们在训练过程中不会再次出现。
在一篇2015 年的论文中,Sergey Ioffe 和 Christian Szegedy 提出了一种称为批量归一化(BN)的技术,解决了这些问题。该技术包括在模型中在每个隐藏层的激活函数之前或之后添加一个操作。这个操作简单地将每个输入零中心化和归一化,然后使用每层两个新的参数向量进行缩放和移位:一个用于缩放,另一个用于移位。换句话说,该操作让模型学习每个层输入的最佳缩放和均值。在许多情况下,如果将 BN 层作为神经网络的第一层,您就不需要标准化训练集。也就是说,不需要StandardScaler
或Normalization
;BN 层会为您完成(大致上,因为它一次只看一个批次,并且还可以重新缩放和移位每个输入特征)。
为了将输入零中心化和归一化,算法需要估计每个输入的均值和标准差。它通过评估当前小批量输入的均值和标准差来实现这一点(因此称为“批量归一化”)。整个操作在方程式 11-4 中逐步总结。
方程式 11-4. 批量归一化算法
1 . μ B = 1 m B ∑ i=1 m B x (i) 2 . σ B 2 = 1 m B ∑ i=1 m B (x (i) -μ B ) 2 3 . x ^ (i) = x (i) -μ B σ B 2 +ε 4 . z (i) = γ ⊗ x ^ (i) + β
在这个算法中:
- μ[B] 是在整个小批量B上评估的输入均值向量(它包含每个输入的一个均值)。
- m[B] 是小批量中实例的数量。
- σ[B] 是输入标准差的向量,也是在整个小批量上评估的(它包含每个输入的一个标准差)。
- x ^ ^((i)) 是实例i的零中心化和归一化输入向量。
- ε 是一个微小的数字,避免了除以零,并确保梯度不会增长太大(通常为 10^(–5))。这被称为平滑项。
- γ 是该层的输出比例参数向量(它包含每个输入的一个比例参数)。
- ⊗ 表示逐元素乘法(每个输入都会乘以其对应的输出比例参数)。
- β 是该层的输出偏移参数向量(它包含每个输入的一个偏移参数)。每个输入都会被其对应的偏移参数偏移。
- z^((i)) 是 BN 操作的输出。它是输入的重新缩放和偏移版本。
因此,在训练期间,BN 会标准化其输入,然后重新缩放和偏移它们。很好!那么,在测试时呢?嗯,事情并不那么简单。实际上,我们可能需要为单个实例而不是一批实例进行预测:在这种情况下,我们将无法计算每个输入的均值和标准差。此外,即使我们有一批实例,它可能太小,或者实例可能不是独立且同分布的,因此在批次实例上计算统计数据将是不可靠的。一个解决方案可能是等到训练结束,然后通过神经网络运行整个训练集,并计算 BN 层每个输入的均值和标准差。这些“最终”输入均值和标准差可以在进行预测时代替批次输入均值和标准差。然而,大多数批次归一化的实现在训练期间通过使用该层输入均值和标准差的指数移动平均值来估计这些最终统计数据。这就是当您使用BatchNormalization
层时 Keras 自动执行的操作。总之,在每个批次归一化的层中学习了四个参数向量:γ(输出缩放向量)和β(输出偏移向量)通过常规反向传播学习,而μ(最终输入均值向量)和σ(最终输入标准差向量)则使用指数移动平均值进行估计。请注意,μ和σ是在训练期间估计的,但仅在训练后使用(以替换公式 11-4 中的批次输入均值和标准差)。
Ioffe 和 Szegedy 证明了批次归一化显著改善了他们进行实验的所有深度神经网络,从而在 ImageNet 分类任务中取得了巨大的改进(ImageNet 是一个大型图像数据库,被分类为许多类别,通常用于评估计算机视觉系统)。梯度消失问题得到了很大程度的减轻,以至于他们可以使用饱和激活函数,如 tanh 甚至 sigmoid 激活函数。网络对权重初始化也不那么敏感。作者能够使用更大的学习率,显著加快学习过程。具体来说,他们指出:
应用于最先进的图像分类模型,批次归一化在 14 倍更少的训练步骤下实现了相同的准确性,并且以显著的优势击败了原始模型。[…] 使用一组批次归一化的网络,我们在 ImageNet 分类上取得了最佳发布结果:达到 4.9%的前 5 验证错误率(和 4.8%的测试错误率),超过了人类评分者的准确性。
最后,就像一份源源不断的礼物,批次归一化就像一个正则化器,减少了对其他正则化技术(如本章后面描述的 dropout)的需求。
然而,批量归一化确实给模型增加了一些复杂性(尽管它可以消除对输入数据进行归一化的需要,如前面讨论的)。此外,还存在运行时惩罚:由于每一层需要额外的计算,神经网络的预测速度变慢。幸运的是,通常可以在训练后将 BN 层与前一层融合在一起,从而避免运行时惩罚。这是通过更新前一层的权重和偏置,使其直接产生适当规模和偏移的输出来实现的。例如,如果前一层计算XW + b,那么 BN 层将计算γ ⊗ (XW + b - μ) / σ + β(忽略分母中的平滑项ε)。如果我们定义W′ = γ⊗W / σ和b′ = γ ⊗ (b - μ) / σ + β,则方程简化为XW′ + b′。因此,如果我们用更新后的权重和偏置(W′和b′)替换前一层的权重和偏置(W和b),我们可以摆脱 BN 层(TFLite 的转换器会自动执行此操作;请参阅第十九章)。
注意
您可能会发现训练速度相当慢,因为使用批量归一化时,每个时期需要更多的时间。通常,这通常会被 BN 的收敛速度更快所抵消,因此需要更少的时期才能达到相同的性能。总的来说,墙上的时间通常会更短(这是您墙上时钟上测量的时间)。
使用 Keras 实现批量归一化
与 Keras 的大多数事物一样,实现批量归一化是简单直观的。只需在每个隐藏层的激活函数之前或之后添加一个BatchNormalization
层。您还可以将 BN 层添加为模型中的第一层,但通常在此位置使用普通的Normalization
层效果一样好(它的唯一缺点是您必须首先调用其adapt()
方法)。例如,这个模型在每个隐藏层后应用 BN,并将其作为模型中的第一层(在展平输入图像之后):
model = tf.keras.Sequential([ tf.keras.layers.Flatten(input_shape=[28, 28]), tf.keras.layers.BatchNormalization(), tf.keras.layers.Dense(300, activation="relu", kernel_initializer="he_normal"), tf.keras.layers.BatchNormalization(), tf.keras.layers.Dense(100, activation="relu", kernel_initializer="he_normal"), tf.keras.layers.BatchNormalization(), tf.keras.layers.Dense(10, activation="softmax") ])
就这样!在这个只有两个隐藏层的微小示例中,批量归一化不太可能产生很大的影响,但对于更深的网络,它可能产生巨大的差异。
让我们显示模型摘要:
>>> model.summary() Model: "sequential" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= flatten (Flatten) (None, 784) 0 _________________________________________________________________ batch_normalization (BatchNo (None, 784) 3136 _________________________________________________________________ dense (Dense) (None, 300) 235500 _________________________________________________________________ batch_normalization_1 (Batch (None, 300) 1200 _________________________________________________________________ dense_1 (Dense) (None, 100) 30100 _________________________________________________________________ batch_normalization_2 (Batch (None, 100) 400 _________________________________________________________________ dense_2 (Dense) (None, 10) 1010 ================================================================= Total params: 271,346 Trainable params: 268,978 Non-trainable params: 2,368 _________________________________________________________________
正如您所看到的,每个 BN 层都会为每个输入添加四个参数:γ、β、μ和σ(例如,第一个 BN 层会添加 3,136 个参数,即 4×784)。最后两个参数,μ和σ,是移动平均值;它们不受反向传播的影响,因此 Keras 将它们称为“不可训练”¹³(如果您计算 BN 参数的总数,3,136 + 1,200 + 400,然后除以 2,您将得到 2,368,这是该模型中不可训练参数的总数)。
让我们看看第一个 BN 层的参数。其中两个是可训练的(通过反向传播),另外两个不是:
>>> [(var.name, var.trainable) for var in model.layers[1].variables] [('batch_normalization/gamma:0', True), ('batch_normalization/beta:0', True), ('batch_normalization/moving_mean:0', False), ('batch_normalization/moving_variance:0', False)]
BN 论文的作者主张在激活函数之前而不是之后添加 BN 层(就像我们刚刚做的那样)。关于这一点存在一些争论,因为哪种方式更可取似乎取决于任务-您也可以尝试这个来看看哪个选项在您的数据集上效果最好。要在激活函数之前添加 BN 层,您必须从隐藏层中删除激活函数,并在 BN 层之后作为单独的层添加它们。此外,由于批量归一化层包含每个输入的一个偏移参数,您可以在创建时通过传递use_bias=False
来删除前一层的偏置项。最后,通常可以删除第一个 BN 层,以避免将第一个隐藏层夹在两个 BN 层之间。更新后的代码如下:
model = tf.keras.Sequential([ tf.keras.layers.Flatten(input_shape=[28, 28]), tf.keras.layers.Dense(300, kernel_initializer="he_normal", use_bias=False), tf.keras.layers.BatchNormalization(), tf.keras.layers.Activation("relu"), tf.keras.layers.Dense(100, kernel_initializer="he_normal", use_bias=False), tf.keras.layers.BatchNormalization(), tf.keras.layers.Activation("relu"), tf.keras.layers.Dense(10, activation="softmax") ])
BatchNormalization
类有很多可以调整的超参数。默认值通常是可以的,但偶尔您可能需要调整momentum
。当BatchNormalization
层更新指数移动平均值时,该超参数将被使用;给定一个新值v(即,在当前批次上计算的新的输入均值或标准差向量),该层使用以下方程更新运行平均值v^:
v ^ ← v ^ × momentum + v × ( 1 - momentum )
一个良好的动量值通常接近于 1;例如,0.9,0.99 或 0.999。对于更大的数据集和更小的小批量,您希望有更多的 9。
另一个重要的超参数是axis
:它确定应该对哪个轴进行归一化。默认为-1,这意味着默认情况下将归一化最后一个轴(使用在其他轴上计算的均值和标准差)。当输入批次为 2D(即,批次形状为[批次大小,特征])时,这意味着每个输入特征将基于在批次中所有实例上计算的均值和标准差进行归一化。例如,前面代码示例中的第一个 BN 层将独立地归一化(和重新缩放和移位)784 个输入特征中的每一个。如果我们将第一个 BN 层移到Flatten
层之前,那么输入批次将是 3D,形状为[批次大小,高度,宽度];因此,BN 层将计算 28 个均值和 28 个标准差(每个像素列一个,跨批次中的所有实例和列中的所有行计算),并且将使用相同的均值和标准差归一化给定列中的所有像素。还将有 28 个比例参数和 28 个移位参数。如果您仍希望独立处理 784 个像素中的每一个,则应将axis=[1, 2]
。
批量归一化已经成为深度神经网络中最常用的层之一,特别是在深度卷积神经网络中讨论的(第十四章),以至于在架构图中通常被省略:假定在每一层之后都添加了 BN。现在让我们看看最后一种稳定梯度的技术:梯度裁剪。
梯度裁剪
另一种缓解梯度爆炸问题的技术是在反向传播过程中裁剪梯度,使其永远不超过某个阈值。这被称为梯度裁剪。¹⁴ 这种技术通常用于循环神经网络中,其中使用批量归一化是棘手的(正如您将在第十五章中看到的)。
在 Keras 中,实现梯度裁剪只需要在创建优化器时设置clipvalue
或clipnorm
参数,就像这样:
optimizer = tf.keras.optimizers.SGD(clipvalue=1.0) model.compile([...], optimizer=optimizer) • 1 • 2
这个优化器将梯度向量的每个分量剪切到-1.0 和 1.0 之间的值。这意味着损失的所有偏导数(对每个可训练参数)将在-1.0 和 1.0 之间被剪切。阈值是您可以调整的超参数。请注意,这可能会改变梯度向量的方向。例如,如果原始梯度向量是[0.9, 100.0],它主要指向第二轴的方向;但是一旦您按值剪切它,您会得到[0.9, 1.0],它大致指向两个轴之间的对角线。在实践中,这种方法效果很好。如果您希望确保梯度剪切不改变梯度向量的方向,您应该通过设置clipnorm
而不是clipvalue
来按范数剪切。如果其ℓ[2]范数大于您选择的阈值,则会剪切整个梯度。例如,如果设置clipnorm=1.0
,那么向量[0.9, 100.0]将被剪切为[0.00899964, 0.9999595],保持其方向但几乎消除第一个分量。如果您观察到梯度在训练过程中爆炸(您可以使用 TensorBoard 跟踪梯度的大小),您可能希望尝试按值剪切或按范数剪切,使用不同的阈值,看看哪个选项在验证集上表现最好。
重用预训练层
通常不建议从头开始训练一个非常大的 DNN,而不是先尝试找到一个现有的神经网络,完成与您尝试解决的任务类似的任务(我将在第十四章中讨论如何找到它们)。如果找到这样的神经网络,那么通常可以重用大部分层,除了顶部的层。这种技术称为迁移学习。它不仅会显著加快训练速度,而且需要的训练数据明显较少。
假设您可以访问一个经过训练的 DNN,用于将图片分类为 100 个不同的类别,包括动物、植物、车辆和日常物品,现在您想要训练一个 DNN 来分类特定类型的车辆。这些任务非常相似,甚至部分重叠,因此您应该尝试重用第一个网络的部分(参见图 11-5)。
注意
如果您新任务的输入图片与原始任务中使用的图片大小不同,通常需要添加一个预处理步骤,将它们调整为原始模型期望的大小。更一般地说,当输入具有相似的低级特征时,迁移学习效果最好。
图 11-5。重用预训练层
通常应该替换原始模型的输出层,因为它很可能对新任务没有用处,而且可能不会有正确数量的输出。
同样,原始模型的上层隐藏层不太可能像下层那样有用,因为对于新任务最有用的高级特征可能与对原始任务最有用的特征有很大不同。您需要找到要重用的正确层数。
提示
任务越相似,您将希望重用的层次就越多(从较低层次开始)。对于非常相似的任务,尝试保留所有隐藏层,只替换输出层。
首先尝试冻结所有重用的层(即使它们的权重不可训练,以便梯度下降不会修改它们并保持固定),然后训练您的模型并查看其表现。然后尝试解冻顶部一两个隐藏层,让反向传播调整它们,看看性能是否提高。您拥有的训练数据越多,您可以解冻的层次就越多。解冻重用层时降低学习率也很有用:这将避免破坏它们微调的权重。
如果您仍然无法获得良好的性能,并且训练数据很少,尝试删除顶部隐藏层并再次冻结所有剩余的隐藏层。您可以迭代直到找到要重用的正确层数。如果您有大量训练数据,您可以尝试替换顶部隐藏层而不是删除它们,甚至添加更多隐藏层。
使用 Keras 进行迁移学习
让我们看一个例子。假设时尚 MNIST 数据集仅包含八个类别,例如除凉鞋和衬衫之外的所有类别。有人在该数据集上构建并训练了一个 Keras 模型,并获得了相当不错的性能(>90%的准确率)。我们将这个模型称为 A。现在您想要解决一个不同的任务:您有 T 恤和套头衫的图像,并且想要训练一个二元分类器:对于 T 恤(和上衣)为正,对于凉鞋为负。您的数据集非常小;您只有 200 张带标签的图像。当您为这个任务训练一个新模型(我们称之为模型 B),其架构与模型 A 相同时,您获得了 91.85%的测试准确率。在喝早晨咖啡时,您意识到您的任务与任务 A 非常相似,因此也许迁移学习可以帮助?让我们找出来!
首先,您需要加载模型 A 并基于该模型的层创建一个新模型。您决定重用除输出层以外的所有层:
[...] # Assuming model A was already trained and saved to "my_model_A" model_A = tf.keras.models.load_model("my_model_A") model_B_on_A = tf.keras.Sequential(model_A.layers[:-1]) model_B_on_A.add(tf.keras.layers.Dense(1, activation="sigmoid"))
请注意,model_A
和model_B_on_A
现在共享一些层。当您训练model_B_on_A
时,它也会影响model_A
。如果您想避免这种情况,您需要在重用其层之前克隆model_A
。为此,您可以使用clone_model()
克隆模型 A 的架构,然后复制其权重:
model_A_clone = tf.keras.models.clone_model(model_A) model_A_clone.set_weights(model_A.get_weights())
警告
tf.keras.models.clone_model()
仅克隆架构,而不是权重。如果您不使用set_weights()
手动复制它们,那么当首次使用克隆模型时,它们将被随机初始化。
现在您可以为任务 B 训练model_B_on_A
,但由于新的输出层是随机初始化的,它将产生大误差(至少在最初的几个时期),因此会产生大误差梯度,可能会破坏重用的权重。为了避免这种情况,一种方法是在最初的几个时期内冻结重用的层,让新层有时间学习合理的权重。为此,将每个层的trainable
属性设置为False
并编译模型:
for layer in model_B_on_A.layers[:-1]: layer.trainable = False optimizer = tf.keras.optimizers.SGD(learning_rate=0.001) model_B_on_A.compile(loss="binary_crossentropy", optimizer=optimizer, metrics=["accuracy"])
注意
在冻结或解冻层之后,您必须始终编译您的模型。
现在您可以为模型训练几个时期,然后解冻重用的层(这需要重新编译模型)并继续训练以微调任务 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 = tf.keras.optimizers.SGD(learning_rate=0.001) 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))
那么,最终的结论是什么?好吧,这个模型的测试准确率为 93.85%,比 91.85%高出两个百分点!这意味着迁移学习将错误率减少了近 25%:
>>> model_B_on_A.evaluate(X_test_B, y_test_B) [0.2546142041683197, 0.9384999871253967]
您相信了吗?您不应该相信:我作弊了!我尝试了许多配置,直到找到一个表现出强烈改进的配置。如果您尝试更改类别或随机种子,您会发现改进通常会下降,甚至消失或反转。我所做的被称为“折磨数据直到它招认”。当一篇论文看起来过于积极时,您应该持怀疑态度:也许这种花哨的新技术实际上并没有太大帮助(事实上,它甚至可能降低性能),但作者尝试了许多变体并仅报告了最佳结果(这可能仅仅是纯粹的运气),而没有提及他们在过程中遇到了多少失败。大多数情况下,这并不是恶意的,但这是科学中许多结果永远无法重现的原因之一。
为什么我作弊了?事实证明,迁移学习在小型密集网络上效果不佳,可能是因为小型网络学习的模式较少,而密集网络学习的是非常具体的模式,这些模式不太可能在其他任务中有用。迁移学习最适用于深度卷积神经网络,这些网络倾向于学习更通用的特征检测器(特别是在较低层)。我们将在第十四章中重新讨论迁移学习,使用我们刚讨论的技术(这次不会作弊,我保证!)。
无监督预训练
假设您想要解决一个复杂的任务,但您没有太多标记的训练数据,而不幸的是,您找不到一个类似任务训练的模型。不要失去希望!首先,您应该尝试收集更多标记的训练数据,但如果您无法做到,您仍然可以执行无监督预训练(见图 11-6)。事实上,收集未标记的训练示例通常很便宜,但标记它们却很昂贵。如果您可以收集大量未标记的训练数据,您可以尝试使用它们来训练一个无监督模型,例如自编码器或生成对抗网络(GAN;见第十七章)。然后,您可以重复使用自编码器的较低层或 GAN 的鉴别器的较低层,添加顶部的输出层,然后使用监督学习(即使用标记的训练示例)微调最终网络。
正是这种技术在 2006 年由 Geoffrey Hinton 及其团队使用,导致了神经网络的复兴和深度学习的成功。直到 2010 年,无监督预训练(通常使用受限玻尔兹曼机(RBMs;请参阅https://homl.info/extra-anns中的笔记本))是深度网络的标准,只有在消失梯度问题得到缓解后,纯粹使用监督学习训练 DNN 才变得更加普遍。无监督预训练(今天通常使用自编码器或 GAN,而不是 RBMs)仍然是一个很好的选择,当您有一个复杂的任务需要解决,没有类似的可重用模型,但有大量未标记的训练数据时。
请注意,在深度学习的早期阶段,训练深度模型是困难的,因此人们会使用一种称为贪婪逐层预训练的技术(在图 11-6 中描述)。他们首先使用单层训练一个无监督模型,通常是一个 RBM,然后冻结该层并在其顶部添加另一层,然后再次训练模型(实际上只是训练新层),然后冻结新层并在其顶部添加另一层,再次训练模型,依此类推。如今,事情简单得多:人们通常一次性训练完整的无监督模型,并使用自编码器或 GAN,而不是 RBMs。
图 11-6。在无监督训练中,模型使用无监督学习技术在所有数据上进行训练,包括未标记的数据,然后使用监督学习技术仅在标记的数据上对最终任务进行微调;无监督部分可以像这里所示一次训练一层,也可以直接训练整个模型
辅助任务上的预训练
如果您没有太多标记的训练数据,最后一个选择是在一个辅助任务上训练第一个神经网络,您可以轻松获取或生成标记的训练数据,然后重复使用该网络的较低层来执行实际任务。第一个神经网络的较低层将学习特征检测器,很可能可以被第二个神经网络重复使用。
例如,如果您想构建一个识别人脸的系统,您可能只有每个个体的少量图片,显然不足以训练一个良好的分类器。收集每个人数百张照片是不现实的。但是,您可以在网络上收集大量随机人的照片,并训练第一个神经网络来检测两张不同图片是否展示了同一个人。这样的网络将学习良好的人脸特征检测器,因此重用其较低层将允许您训练一个使用很少训练数据的良好人脸分类器。
对于自然语言处理(NLP)应用,您可以下载数百万个文本文档的语料库,并从中自动生成标记数据。例如,您可以随机屏蔽一些单词并训练模型来预测缺失的单词是什么(例如,它应该预测句子“What ___ you saying?”中缺失的单词可能是“are”或“were”)。如果您可以训练模型在这个任务上达到良好的性能,那么它将已经对语言有相当多的了解,您肯定可以在实际任务中重复使用它,并在标记数据上进行微调(我们将在第十五章中讨论更多的预训练任务)。
注意
自监督学习是指从数据本身自动生成标签,例如文本屏蔽示例,然后使用监督学习技术在生成的“标记”数据集上训练模型。
更快的优化器
训练一个非常庞大的深度神经网络可能会非常缓慢。到目前为止,我们已经看到了四种加速训练(并达到更好解决方案)的方法:应用良好的连接权重初始化策略,使用良好的激活函数,使用批量归一化,并重用预训练网络的部分(可能是为辅助任务构建的或使用无监督学习)。另一个巨大的加速来自使用比常规梯度下降优化器更快的优化器。在本节中,我们将介绍最流行的优化算法:动量、Nesterov 加速梯度、AdaGrad、RMSProp,最后是 Adam 及其变体。
动量
想象一颗保龄球在光滑表面上缓坡滚动:它会从慢慢开始,但很快会积累动量,直到最终达到终端速度(如果有一些摩擦或空气阻力)。这就是动量优化的核心思想,由鲍里斯·波利亚克在 1964 年提出。与此相反,常规梯度下降在坡度平缓时会采取小步骤,在坡度陡峭时会采取大步骤,但它永远不会加速。因此,与动量优化相比,常规梯度下降通常要慢得多才能达到最小值。
请记住,梯度下降通过直接减去成本函数J(θ)相对于权重的梯度(∇[θ]J(θ))乘以学习率η来更新权重θ。方程式为θ ← θ - η∇[θ]J(θ)。它不关心先前的梯度是什么。如果局部梯度很小,它会走得很慢。
动量优化非常关注先前梯度是什么:在每次迭代中,它从动量向量 m(乘以学习率η)中减去局部梯度,然后通过添加这个动量向量来更新权重(参见方程 11-5)。换句话说,梯度被用作加速度,而不是速度。为了模拟某种摩擦机制并防止动量增长过大,该算法引入了一个新的超参数β,称为动量,必须设置在 0(高摩擦)和 1(无摩擦)之间。典型的动量值为 0.9。
方程 11-5. 动量算法
1 . m ← β m - η ∇ θ J ( θ ) 2 . θ ← θ + m
您可以验证,如果梯度保持不变,则终端速度(即权重更新的最大大小)等于该梯度乘以学习率η乘以 1 / (1 - β)(忽略符号)。例如,如果β = 0.9,则终端速度等于梯度乘以学习率的 10 倍,因此动量优化的速度比梯度下降快 10 倍!这使得动量优化比梯度下降更快地摆脱高原。我们在第四章中看到,当输入具有非常不同的比例时,成本函数看起来像一个拉长的碗(参见图 4-7)。梯度下降很快下降陡峭的斜坡,但然后需要很长时间才能下降到山谷。相比之下,动量优化将会越来越快地滚动到山谷,直到达到底部(最优解)。在不使用批量归一化的深度神经网络中,上层通常会出现具有非常不同比例的输入,因此使用动量优化会有很大帮助。它还可以帮助跳过局部最优解。
注意
由于动量的原因,优化器可能会稍微超调,然后返回,再次超调,并在稳定在最小值之前多次振荡。这是有摩擦力的好处之一:它消除了这些振荡,从而加快了收敛速度。
在 Keras 中实现动量优化是一件轻而易举的事情:只需使用SGD
优化器并设置其momentum
超参数,然后躺下来赚钱!
optimizer = tf.keras.optimizers.SGD(learning_rate=0.001, momentum=0.9)
动量优化的一个缺点是它增加了另一个需要调整的超参数。然而,在实践中,动量值 0.9 通常效果很好,几乎总是比常规梯度下降更快。
Nesterov 加速梯度
动量优化的一个小变体,由Yurii Nesterov 于 1983 年提出,¹⁶几乎总是比常规动量优化更快。Nesterov 加速梯度(NAG)方法,也称为Nesterov 动量优化,测量成本函数的梯度不是在本地位置θ处,而是稍微向前在动量方向,即θ + βm(参见方程 11-6)。
第 11-6 方程。Nesterov 加速梯度算法
1 . m ← β m - η ∇ θ J ( θ + β m ) 2 . θ ← θ + m
这个小调整有效是因为通常动量向量将指向正确的方向(即朝向最优解),因此使用稍微更准确的梯度测量更有利于使用稍微更远处的梯度,而不是原始位置处的梯度,如您在图 11-7 中所见(其中∇[1]表示在起始点θ处测量的成本函数的梯度,而∇[2]表示在位于θ + βm的点处测量的梯度)。
图 11-7。常规与 Nesterov 动量优化:前者应用动量步骤之前计算的梯度,而后者应用动量步骤之后计算的梯度
如您所见,Nesterov 更新最终更接近最优解。随着时间的推移,这些小的改进累积起来,NAG 最终比常规动量优化快得多。此外,请注意,当动量将权重推过山谷时,∇[1]继续推动更远,而∇[2]则向山谷底部推回。这有助于减少振荡,因此 NAG 收敛更快。
要使用 NAG,只需在创建SGD
优化器时设置nesterov=True
:
optimizer = tf.keras.optimizers.SGD(learning_rate=0.001, momentum=0.9, nesterov=True)
AdaGrad
考虑再次延长碗问题:梯度下降首先快速沿着最陡的斜坡下降,这并不直指全局最优解,然后它非常缓慢地下降到山谷底部。如果算法能够更早地纠正方向,使其更多地指向全局最优解,那将是很好的。AdaGrad算法通过沿着最陡的维度缩小梯度向量来实现这种校正(参见方程 11-7)。
方程 11-7。AdaGrad 算法
1 . s ← s + ∇ θ J ( θ ) ⊗ ∇ θ J ( θ ) 2 . θ ← θ - η ∇ θ J ( θ ) ⊘ s + ε
第一步将梯度的平方累积到向量s中(请记住,⊗符号表示逐元素乘法)。这种向量化形式等同于计算s[i] ← s[i] + (∂J(θ)/∂θ[i])²,对于向量s的每个元素s[i]来说,换句话说,每个s[i]累积了成本函数对参数θ[i]的偏导数的平方。如果成本函数沿第i维陡峭,那么在每次迭代中s[i]将变得越来越大。
第二步几乎与梯度下降完全相同,但有一个重大区别:梯度向量被一个因子s+ε缩小(⊘符号表示逐元素除法,ε是一个平滑项,用于避免除以零,通常设置为 10^(–10))。这个向量化形式等价于同时计算所有参数θ[i]的θi←θi-η∂J(θ)/∂θi/si+ε。
简而言之,这个算法会衰减学习率,但对于陡峭的维度比对于坡度较缓的维度衰减得更快。这被称为自适应学习率。它有助于更直接地指向全局最优(参见图 11-8)。另一个好处是它需要更少的调整学习率超参数η。
图 11-8. AdaGrad 与梯度下降的比较:前者可以更早地纠正方向指向最优点
在简单的二次问题上,AdaGrad 通常表现良好,但在训练神经网络时经常会过早停止:学习率被缩小得太多,以至于算法最终在达到全局最优之前完全停止。因此,即使 Keras 有一个Adagrad
优化器,你也不应该用它来训练深度神经网络(尽管对于简单任务如线性回归可能是有效的)。不过,理解 AdaGrad 有助于理解其他自适应学习率优化器。
RMSProp
正如我们所见,AdaGrad 有减速得太快并且永远无法收敛到全局最优的风险。RMSProp算法¹⁸通过仅累积最近迭代的梯度来修复这个问题,而不是自训练开始以来的所有梯度。它通过在第一步中使用指数衰减来实现这一点(参见方程 11-8)。
方程 11-8. RMSProp 算法
1 . s ← ρ s + ( 1 - ρ ) ∇ θ J ( θ ) ⊗ ∇ θ J ( θ ) 2 . θ ← θ - η ∇ θ J ( θ ) ⊘ s + ε
衰减率ρ通常设置为 0.9。¹⁹ 是的,这又是一个新的超参数,但这个默认值通常效果很好,所以你可能根本不需要调整它。
正如你所期望的,Keras 有一个RMSprop
优化器:
optimizer = tf.keras.optimizers.RMSprop(learning_rate=0.001, rho=0.9)
除了在非常简单的问题上,这个优化器几乎总是比 AdaGrad 表现得更好。事实上,直到 Adam 优化算法出现之前,它一直是许多研究人员首选的优化算法。
亚当
Adam,代表自适应矩估计,结合了动量优化和 RMSProp 的思想:就像动量优化一样,它跟踪过去梯度的指数衰减平均值;就像 RMSProp 一样,它跟踪过去梯度的平方的指数衰减平均值(见 Equation 11-9)。这些是梯度的均值和(未居中)方差的估计。均值通常称为第一时刻,而方差通常称为第二时刻,因此算法的名称。
方程 11-9. Adam 算法
1 . m ← β 1 m - ( 1 - β 1 ) ∇ θ J ( θ ) 2 . s ← β 2 s + ( 1 - β 2 ) ∇ θ J ( θ ) ⊗ ∇ θ J ( θ ) 3 . m^ ← m 1 - β 1 t 4 . s^ ← s 1-β 2 t 5 . θ ← θ + η m^ ⊘ s^ + ε
在这个方程中,t代表迭代次数(从 1 开始)。
如果只看步骤 1、2 和 5,你会注意到 Adam 与动量优化和 RMSProp 的相似之处:β[1]对应于动量优化中的β,β[2]对应于 RMSProp 中的ρ。唯一的区别是步骤 1 计算的是指数衰减平均值而不是指数衰减和,但实际上这些是等价的,除了一个常数因子(衰减平均值只是衰减和的 1 - β[1]倍)。步骤 3 和 4 有点技术细节:由于m和s初始化为 0,在训练开始时它们会偏向于 0,因此这两个步骤将有助于在训练开始时提升m和s。
动量衰减超参数β[1]通常初始化为 0.9,而缩放衰减超参数β[2]通常初始化为 0.999。与之前一样,平滑项ε通常初始化为一个非常小的数字,如 10^(–7)。这些是Adam
类的默认值。以下是如何在 Keras 中创建 Adam 优化器的方法:
optimizer = tf.keras.optimizers.Adam(learning_rate=0.001, beta_1=0.9, beta_2=0.999)
由于 Adam 是一种自适应学习率算法,类似于 AdaGrad 和 RMSProp,它需要较少调整学习率超参数η。您通常可以使用默认值η=0.001,使得 Adam 比梯度下降更容易使用。
提示
如果您开始感到对所有这些不同技术感到不知所措,并想知道如何为您的任务选择合适的技术,不用担心:本章末尾提供了一些实用指南。
最后,值得一提的是 Adam 的三个变体:AdaMax、Nadam 和 AdamW。
AdaMax
Adam 论文还介绍了 AdaMax。请注意,在方程式 11-9 的第 2 步中,Adam 在s中累积梯度的平方(对于最近的梯度有更大的权重)。在第 5 步中,如果我们忽略ε和步骤 3 和 4(这些都是技术细节),Adam 通过s的平方根缩小参数更新。简而言之,Adam 通过时间衰减梯度的ℓ[2]范数缩小参数更新(回想一下,ℓ[2]范数是平方和的平方根)。
AdaMax 用ℓ[∞]范数(一种说法是最大值)替换了ℓ[2]范数。具体来说,它用s←max(β2s, abs(∇θJ(θ)))替换了方程式 11-9 的第 2 步,删除了第 4 步,在第 5 步中,它通过s的因子缩小梯度更新,s是时间衰减梯度的绝对值的最大值。
实际上,这使得 AdaMax 比 Adam 更稳定,但这确实取决于数据集,总体上 Adam 表现更好。因此,如果您在某些任务上遇到 Adam 的问题,这只是另一个您可以尝试的优化器。
Nadam
Nadam 优化是 Adam 优化加上 Nesterov 技巧,因此它通常会比 Adam 收敛速度稍快。在介绍这种技术的研究报告中,研究员 Timothy Dozat 比较了许多不同的优化器在各种任务上的表现,发现 Nadam 通常优于 Adam,但有时会被 RMSProp 超越。
Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(四)(4)https://developer.aliyun.com/article/1482427