目录
0 前言
1 Gradient descent variants梯度下降的变体
1.1 Batch gradient descent批量梯度下降,又名 Vanilla gradient descent
1.2 Stochastic gradient descent 随机梯度下降
1.3 Mini-batch gradient descent 小批量梯度下降
2 SGD的缺点
3 高级梯度下降优化算法
3.1 Momentum
3.2 Nesterov accelerated gradient (NAG)
3.3 Adagrad
3.4 RMSprop
3.5 Adadelta
3.6 Adam
4 算法可视化
5 如何选择优化器
6 优化 SGD 的其他手段
6.1 打乱(Shuffle)和 Curriculum Learning
6.2 批量标准化(Batch Normalization)
6.3 早停(Early Stopping)
6.4 梯度噪声(Gradient Noise)
梯度下降法是目前最流行的优化算法之一,也是目前最常用的神经网络优化方法。同时,每一个最先进的深度学习库都包含各种算法的实现来优化梯度下降(如caffe,keras)。然而,这些算法经常被用作黑箱优化器,因为它们的优点和缺点很难得到实际的解释。这篇博客旨在介绍各种梯度下降算法,这将帮助你使用它们。
我们先来看看梯度下降法的不同变体。然后,我们将简要总结训练过程中的挑战。随后,我们将介绍最常见的优化算法,展示它们解决这些挑战的机理,以及如何推导它们的更新规则。我们还将简要介绍在并行和分布式环境中优化梯度下降的算法和体系结构。最后,我们将考虑其他有助于优化梯度下降的策略。
梯度下降是一种通过在目标函数梯度的反方向上更新参数来最小化目标函数的方法.
学习速率η决定我们达到局部最小值的步长。换句话说,我们沿着由目标函数创建的表面斜坡的方向向下直到到达一个山谷。
梯度下降法有三种变体,它们的区别在于我们用多少数据来计算目标函数的梯度。根据数据量的不同,我们需要权衡参数更新的准确性和执行一次更新所需的时间。
用整个训练数据集计算目标函数的梯度参数θ
由于我们需要计算整个数据集的梯度来执行一次更新,批量梯度下降可能非常缓慢,而且需要把整个数据集放入内存张,数据量很大时,非常棘手。批量梯度下降也不允许我们在线更新我们的模型。
批量梯度下降的代码实现
for i in range(n_epochs):
params_grad = evaluate_gradient(loss_function, data, params)
params = params - lr * params_grad
对于预先定义的多个epoch,我们首先计算关于整个数据集的损失函数的梯度向量params_grad。注意,当前的一些深度学习库提供了自动求导,可以有效地计算一些参数梯度。
然后,我们按照梯度的反方向更新参数,学习速率决定我们执行的更新的大小。保证了批梯度下降收敛于凸误差曲面的全局最小值和非凸曲面的局部最小值。
相比之下,随机梯度下降(SGD)对每个训练示例和标号进行参数更新:
批量梯度下降对大数据集会有一些冗余计算,因为它在每次参数更新前重新计算类似示例的梯度。SGD通过每次执行一个更新来消除这种冗余。因此,它通常更快,也可以用来在线学习。
SGD会以较大的方差执行频繁的更新,导致目标函数剧烈波动,如下图所示。
当批量梯度下降收敛到参数的盆地时,SGD的波动一方面使其跳跃到新的、可能更好的局部极小值。另一方面,这最终会使收敛复杂化到最小值,因为SGD会持续快速的更新。然而,研究表明,当我们缓慢降低学习速率时,SGD表现出与批量梯度下降相同的收敛行为,几乎可以肯定分别收敛到非凸优化和凸优化的局部或全局最小值。
它的代码只是在训练示例上添加一个循环,并计算关于每个样本的梯度。值得注意的是,我们在每个阶段都要重新打乱训练数据。
for i in range(n_epochs):
np.random.shuffle(data)
for example in data:
params_grad = evaluate_gradient(loss_function, example, params)
params = params - lr * params_grad
小批量批梯度下降具有以上两者的优点,并对每一个小批量执行更新。
这中方法,a)降低了参数更新的方差,可以获得更稳定的收敛;
b)可以利用最先进的深度学习库中常见的高度优化的矩阵优化方法,这些库中的方法对小批量样本计算梯度非常有效。
小批量的batch_size的大小范围在50到256之间,但是对于不同的应用场景下可以有所不同。比如当你的显存不够时,就把batch_size调小。如32,16
在训练神经网络时,小批量梯度下降法是最常用的算法,因此小批量梯度下降也成为SGD.
在代码中,我们不再迭代每个示例,而是迭代大小为50的小批量样本
for i in range(n_epochs):
np.random.shuffle(data)
for batch in get_batches(data, batch_size=50):
params_grad = evaluate_gradient(loss_function, batch, params) / batch_size
params = params - lr * params_grad
然而,小批量梯度下降法并不能保证良好的收敛性,仍存在一些问题
下面,我们将概述一些深度学习社区广泛使用的算法,以应对上述缺点。我们不会讨论在实践中对于高维数据集计算不可行的算法,例如二阶方法,如牛顿法。
SGD 在 ravines 的情况下容易被困住, ravines 就是曲面的一个方向比另一个方向更陡,这时 SGD 会发生震荡而迟迟不能接近极小值:
动量法是一种在相关方向上加速SGD并抑制振荡的方法,如图3所示。它通过添加一个分数γ更新矢量的过去时间步当前更新矢量
其中是动量参数。也就是说,当前时间步长的权值向量的变化取决于当前梯度和前一步权值的变化。
直觉上,使用动量项的基本原理是,当误差函数表面有一个狭长的山谷时,SGD在最陡的方向下降速度特别慢。在这种情况下,梯度的方向几乎垂直于山谷的长轴。因此,梯度运动在短轴的方向上来回摆动,只在山谷的长轴上缓慢移动。动量项帮助平均了沿短轴的振荡,同时也增加了沿长轴的贡献。
γ通常设置为0.9.
若设为0.5,0.9,0.99,分别表示增加速到2倍,10倍,100倍于SGD的算法
过小的话,效果不明显,过大的话,动量过大,可能会错过最优解。
本质上,当使用动量时,我们可以设想把球推下山坡。球在滚下坡时积累的动量,使得变得越来越快(直到它达到最终速度如果有空气阻力,即γ< 1)。同样的事情也发生在我们的参数更新上:动量项在与梯度方向相同的维度上增加,而在与梯度方向不同的维度上减少更新。因此,我们得到更快的收敛和减少振荡。
for i in range(nb_epochs):
np.random.shuffle(data)
for batch in get_batches(data, batch_size=50):
params_grad = evaluate_gradient(loss_function, batch, params) / batch_size
v = momentum * v + lr * params_grad
params = params - v
然而,一个球从山上滚下来,盲目地沿着山坡滚,是非常不令人满意的。我们想要一个更聪明的球,一个知道它要去哪里的球,这样它就知道在山坡再次向上倾斜之前减速。
Nesterov加速梯度(NAG)[7]是一种给动量项这种先见之明的方法。我们知道,我们将使用我们的动量项移动参数。
计算因此给了我们一个近似的下一个参数.
我们现在可以有效地展望未来我们不通过计算梯度当前的参数θ的梯度而是一个近似的未来参数位置。
举个栗子,我们设置了动量项γ值约为0.9。
蓝色是 Momentum 的过程,会先计算当前的梯度(small blue vector),然后在更新后的累积梯度后会有一个大的跳跃(big blue vector)。
而 NAG 会先在前一步的累积梯度上(brown vector)有一个大的跳跃,然后衡量一下梯度做一下修正(red vector),这种预期的更新可以避免我们走的太快。
现在,我们能够根据误差函数的斜率调整更新,并相应地加快SGD的速度,我们还希望根据每个单独的参数调整更新,以根据其重要性执行更大或更小的更新。
for i in range(nb_epochs):
np.random.shuffle(data)
v = 0
for batch in get_batches(data, batch_size=50):
params_grad = evaluate_gradient(loss_function, batch, params - momentum * v) / batch_size
v = momentum * v + lr * params_grad
params = params - v
Adagrad是一种基于梯度的优化算法,它根据参数调整学习率,针对与频繁出现的特征相关的参数执行更小的更新(即低学习率),针对与不频繁的特相关的参数执行更大的更新(即高学习率)。因此,它非常适合处理稀疏数据。
Adagrad能显著提高SGD的鲁棒性,并将其用于谷歌的大型神经网络训练,其中包括在Youtube视频中识别猫。此外,还有研究者使用Adagrad来训练GloVe词嵌入,因为不常见的单词需要比常见的更大的更新。
以前,我们对所有参数使用相同的学习速率η执行一个更新。而Adagrad使用不同的学习速率在每一个时刻为每一个参数执行更新。为了简洁起见,我们使用来表示时间步长的梯度。是目标函数关于时间步长和参数的偏导数
SGD在每个时间步长对每个参数执行一次更新
而Adagrad更新时,基于过去的梯度修正学习速率,然后会在每个时间步长对每个参数进行更新
是一个对角矩阵,其中每个对角矩阵的元素是 t 时刻参数 的梯度平方和。ϵ是一个平滑项,避免除零的情况(通常为1e-8)。
用向量化的表示为
表示矩阵向量乘积
Adagrad的主要优点之一是它不需要手动调整学习速率。大多数实现都使用默认值0.01。
Adagrad的主要缺点是其在分母上的平方梯度的累加:因为每增加一项都是正的,所以在训练过程中累积的和会不断增加。这反过来又会导致学习速率下降,最终变得无穷小,这时算法就不能再获得额外的知识了。下面的算法旨在解决这个缺陷。
eps_stable = 1e-7
for i in range(nb_epochs):
np.random.shuffle(data)
sqr = 0.0
for batch in get_batches(data, batch_size=50):
params_grad = evaluate_gradient(loss_function, batch, params) / batch_size
sqr += param_grad **2
div = lr * param_grad / np.sqrt(sqr + eps_stable)
params = params - div
RMSprop 是由 Geoff Hinton 在他 Coursera 课程中提出的一种适应性学习率方法,至今仍未被公开发表。
RMSprop 法和 Adadelta 法几乎同时被发展出来。他们 解决 Adagrad 激进的学习率缩减问题。实际上,RMSprop 和我们推导出的 Adadelta 法第一个更规则相同:
RMSprop 也将学习率除以了一个指数衰减的衰减均值。Hinton 建议设定 为 0.9,对而言,0.001 是一个较好的默认值。
eps_stable = 1e-7
for i in range(nb_epochs):
np.random.shuffle(data)
sqr = 0.0
for batch in get_batches(data, batch_size=50):
params_grad = evaluate_gradient(loss_function, batch, params) / batch_size
sqr = gamma * sqr + (1 - gamma) * np.square(param_grad)
div = lr * param_grad / np.sqrt(sqr + eps_stable)
params = params - div
Adadelta是Adagrad的扩展,它旨在解决它学习率不断单调下降的问题。比计算之前所有梯度值的平方和,Adadelta 法仅计算在一个大小为w的时间区间内梯度值的累积和。当然,Adadelta并不是低效地存储w个以前的平方梯度,梯度的总和被递归地定义为所有过去的平方梯度的衰减平均值。
t时刻的运行平均值取决于之前的平均值和当前梯度: (γ和动量法中的意义相同)
替换Adagrad的为过去平方梯度的衰减平均值
因为分母就是梯度的均方根RMS,因此重新命名为
作者还注意到,在该更新中(在 SGD、动量法或者 Adagrad 也类似)的单位并不一致,也就是说,更新值的量纲与参数值的假设量纲并不一致。为改进这个问题,他们定义了另外一种指数衰减的衰减均值,他是基于参数更新的平方而非梯度的平方来定义的:
因此,参数更新的均方根误差为
因为 值未知,所以我们使用t-1时刻的参数更新的均方根来近似。将前述规则中的学习率替换为,我们最终得到了 Adadelta 法的更新规则:
有了 Adadelta 法,我们甚至不需要预设一个默认学习率,因为它已经从我们的更新规则中被删除了。
eps_stable = 1e-7
for i in range(nb_epochs):
np.random.shuffle(data)
g_sqr = 0.0
p_sqr = 0.0
for batch in get_batches(data, batch_size=50):
params_grad = evaluate_gradient(loss_function, batch, params) / batch_size
# 梯度按元素平方后做加权平均
g_sqr = roh * g_sqr + (1 - rho) * (params_grad**2)
params_delta = np.sqrt(p_sqr + eps_stable)/np.sqrt(g_sqr + eps_stable) * params_grad
# 参数按元素平方后做加权平均
p_sqr = roh * p_sqr + (1 - rho) * (params_delta**2)
params = params - params_delta
Adaptive Moment Estimation (Adam) 是另一种为每个参数计算自适应学习率的方法。
除了存储类似 Adadelta 法或 RMSprop 中指数衰减的过去梯度平方均值 外,Adam 法也存储像动量法中的指数衰减的过去梯度值均值 :
和 分别是梯度的一阶矩(均值)和二阶矩(方差),这也就是该方法名字的来源。因为当 和 一开始被初始化为 0 向量时,Adam 的作者观察到,该方法会有趋向 0 的偏差,尤其是在最初的几步或是在衰减率很小(即 和 接近 1)的情况下。
因此,他们使用偏差纠正系数,来修正一阶矩和二阶矩的偏差:
然后,使用这些来更新参数,更新规则很我们在 Adadelta 和 RMSprop 法中看到的一样,服从 Adam 的更新规则:
作者认为参数的默认值应设为:0.9 for,0.999 for , and 1e-8 for ϵ. 。
经验表明,Adam 在实践中表现很好,和其他适应性学习算法相比也比较不错。
eps_stable = 1e-8
t = 1
for i in range(nb_epochs):
np.random.shuffle(data)
m = 0.0
v = 0.0
for batch in get_batches(data, batch_size=50):
params_grad = evaluate_gradient(loss_function, batch, params) / batch_size
# 梯度做加权平均
m = beta1 * m + (1 - beta1) * (params_grad)
# 梯度按元素平方后做加权平均
v = beta2 * v + (1 - beta2) * np.square(params_delta)
# 为了减轻 v 和 s 被初始化为 0 在迭代初期对计算指数加权移动平均的影响,做偏差修正
m_modify = m / (1 - beta1 ** t)
v_modify = v / (1 - beta2 ** t)
div = lr * v_modify / np.sqrt(m_modify + eps_stable)
params = params - div
t += 1
如下的两个动画给了我们关于特定优化算法在优化过程中行为的直观感受。
在图 5 中,我们可以看到,在损失函数的等高线图中,优化器的位置随时间的变化情况。注意到,Adagrad、 Adadelta 及 RMSprop 法几乎立刻就找到了正确前进方向并以相似的速度很快收敛。而动量法和 NAG 法,则找错了方向,如图所示,让小球沿着梯度下降的方向前进。但 NAG 法能够很快改正它的方向向最小指出前进,因为他能够往前看并对前面的情况做出响应。
图5 损失表面轮廓上的SGD优化
图 6 展现了各算法在鞍点附近的表现。如上面所说,这对对于 SGD 法、动量法及 NAG 法制造了一个难题。他们很难打破对称性带来的壁垒,尽管最后两者设法逃脱了鞍点。而 Adagrad 法、RMSprop 法及 Adadelta 法都能快速的沿着负斜率的方向前进。
图6 :鞍点的SGD优化
如我们所见,适应性学习率方法,也就是 Adagrad 法、Adadelta 法 、RMSprop 法及 Adam 法最适合处理上述情况,并有最好的收敛效果。
如果你的输入数据较为稀疏(sparse),那么使用适应性学习率类型的算法会有助于你得到好的结果。此外,使用该方法的另一好处是,你在不调参、直接使用默认值的情况下,就能得到最好的结果。
总的来说,RMSprop是Adagrad的扩展,它解决了学习速率急剧下降的问题。Adadelta 法于 RMSprop 法大致相同,除了前者使用了参数更新的均方根。而 Adam 法,则基于 RMSprop 法添加了偏差修正项和动量项。在我们的讨论范围中,RMSprop、Adadelta 及 Adam 法都是非常相似地算法,在相似地情况下都能做的很好。。总的来说,Adam 也许是总体来说最好的选择。
有趣的是,很多最新的论文,都直接使用了(不带动量项的)Vanilla SGD 法,配合一个简单的学习率(退火)列表,或者带动量的Mini-batch SGD。如论文所示,这些 SGD 最终都能帮助他们找到一个最小值,但会花费远多于上述方法的时间。并且这些方法非常依赖于鲁棒的初始化值及退火列表。因此,如果你非常在你的模型能快速收敛,或是你需要训练一个深度或复杂模型,你可能需要选择上述的适应性模型。
最后,我们将讨论一些其他手段,他们可以与前述的方法搭配使用,并能进一步提升 SGD 的效果。
总体而言,我们希望避免训练样本以某种特定顺序传入到我们的学习模型中,因为这会向我们的算法引入偏差。因此,在每次迭代后,对对训练数据集中的样本进行打乱(shuffle),会是一个不错的注意。
另一方面,在某些情况下,我们会需要解决难度逐步提升的问题。那么,按照一定的顺序遍历训练样本,会有助于提高学习效果及加快收敛速度。这种构建特定遍历顺序的方法,叫做Curriculum Learning。
研究表明,使用的二者结合混合的表现好于单一方法。shuffle不断地打乱数据,反而增加了学习过程的难度。
我们通常设置我们参数初值的均值和方差分别为 0 和1,以帮助模型进行学习。随着学习过程的进行,每个参数被不同程度地更新,相应地,参数的正则化特征也随之失去了。因此,随着训练网络的越来越深,训练的速度会越来越慢,变化值也会被放大。
批量标准化 [18] 对每小批数据都重新进行标准化,并也会在操作中逆传播(back-propgate)变化量。在模型中加入批量标准化后,我们能使用更高的学习率且不要那么在意初始化参数。此外,批量标准化还可以看作是一种正则化手段,能够减少(甚至去除)Dropout法的使用。
在训练过程中,你应该时刻关注模型在验证集上的误差情况,并且在改误差没有明显改进的时候停止训练。
在每次梯度的更新中,向其中加入一个服从高斯分布 的噪声值:
并按照如下的方式修正方差:
他们指出,这种方式能够提升神经网络在没有很好的初始化前提下的鲁棒性,并能帮助训练特别是深层、复杂的神经网络。他们发现,加入噪声项之后,模型更有可能跳出并找到在深度模型中频繁出现的局部最小值。
本文部分翻译自参考文献,文中的代码只是帮大家理解优化算法的内容,迁移性不强。
自知水平有限,如果有理解和翻译的不准确的地方,欢迎指出,不胜感激 >_<
参考文献:
arXiv:1609.04747