摘要
本文概述了常见的梯度下降优化算法的不同变种,分析了初始化在优化过程中的重要性以及如何初始化,最后列举出不同优化算法的具体公式,计算过程。
优化概述
下面概述一下常见的优化算法,优化算法的核心是梯度下降,不同优化算法改进的地方在于梯度的方向和大小。可以将优化算法粗分为两大类,一类是改变方向的 Momentum,一类是改变学习率即梯度大小的 adagrad,最常用的 Adam 结合了这两类的优点。
- SGD:梯度下降。这里有几个概念需要辨析:GD 使用全部的样本累积梯度,用累积的梯度更新权值;SGD 每次用一个样本,计算梯度,更新权值;mini-batch SGD,使用一个 batch 的样本累积梯度,更新权值,一般 SGD 的实现用的就是一个 batch 来更新权值,而不是一个样本。
- Momentum:保存一个称之为动量的变量,不断累加梯度到动量上,最后动量更新权值。我们可以将梯度视为力,如果每一步都朝着同个方向前进,那么累加的力就不会抵消,还会因为累加的效果可以在合力方向前进得更快,如果梯度某个方向上发生了震荡,即力的方向经常发生改变,那么力会不断抵消,因为在累加,所以这一步可能和上一步的力发生了抵消,在震荡的方向上将改变的少。可以阅读 [5] 相关章节,用了一个小球的例子,挺形象的。
- Nesterov momentum:和 Momentum 差不多,区别在于,累加的梯度计算。Momentum 使用的梯度是当前的梯度,而 Nesterov 使用的是“假想下一步位置的梯度” ,先用动量更新权值,然后计算新的位置,在新的位置上计算梯度,最后使用这个梯度去更新原来的动量,再用更新后的动量更新权值。[1]
- adagrad:动态调整学习率,累加梯度的平方和,再用这个值计算学习率,累加的梯度越大,学习率就越小,因此 adagrad 的问题是,学习到后面,学习率越来越小,直接学不动了。
- RMSProp:改进了 adagrad 的问题,RMSProp 不再是简简单单直接累加梯度,而是加权求和,权值之和为 1,设置旧的累积梯度为 (eta),新的梯度为 (1 - eta)。 [2]
- adadelta:同样是为了改进 adagrad 的问题,[3] 给出了详细的公式,adadelta 在 RMSProp 的基础上,改进了学习率的计算,这个方法甚至不需要手动设置学习率,在 pytorch 的实现中,学习率是可选的,默认是 1。
- Adam:最常用的优化器了,结合了 Momentum 和 adagrad 的思想,计算动量和累加梯度的平方和,接着就是一顿算,根据动量算方向和大小,根据累加梯度的平方和算学习率。
- Yogi:改进 Adam 的问题,在累加梯度的平方时,这个梯度可能很大(blows up),使到 Adam 不能收敛(even in convex setting),结合公式看一下就知道为什么了,因为这个梯度大,学习率也大,后面看公式就知道了。Yogi 改进的点在于累加梯度的平方和那个公式。链接 [4] 的公式很详细。动量和累加梯度有一个初始值,Yogi 还建议了使用一个 batch 来设置初始值。
关于学习率的讨论
学习率太大,不能收敛;学习率太小,学得慢或者陷入局部最小。学习率衰减,如果学习率一直都保持那么大,会在最小值周围震荡,而到达不了最小值。
初始化
权值初始化。[5] 这本书总结了一个最佳实践:
当激活函数为 sigmoid 或者 tanh 等 S 型曲线函数时,权重初始化使用 Xaiver 初始值。
当激活函数使用 ReLU 时,权重初始化使用 He 初始值;
如果不用这两种方法,还可以使用正态分布来初始化。不能使用一个常量来初始化所有的权值,因为如果使用了常量,那么每一层节点的输出都是一样的,最后一层每个节点输出也一样。这导致了梯度也是一样的,之后每次更新权值也一样,这样之后,不管怎么更新,整个神经网络权值还是一个量。
为什么要进行 Xaiver 初始化、He 初始化?一个原因是解决梯度消失的问题,另一个是表达力受限的问题,很多节点都输出相同的值,那么可以由一个神经元表达同样的事情。Xaiver 和 He 初始化,本质还是正态分布,只不过使用了不同的标准差。Xaiver 的标准差取决于前一层的节点数,He 还要再乘个 2。[6] 的讨论之处,权值初始化的问题,Batch Normalization 已经很好地解决了。
正态分布初始化的问题
下面简要分析一下正态分布初始化存在的问题,具体看 [5] 的 P178 ~ P182,[5] 将 “正态分布” 的数据在一个 “正态分布初始化” 的权值的 5 层 MLP 网络上进行了前向传播。
- 标准差为 1,输出偏向 0 或 1,因为使用的是 Sigmoid,在输出偏向 0 或 1 时,梯度很小,想象一下 Sigmoid 的图像,如果输出接近 0 或 1,曲线是不是很平缓呢。梯度在反向传播的过程中,经过连乘,就没有了,即存在梯度消失的问题。
- 标准差为 0.01,输出全部都集中在 0.5 附近,输出太集中,存在的问题是表达力受限,因为多个节点输出都是相同的值。
Xaiver 初始化存在的问题
Xaiver 在使用 ReLU 作为激活函数的场景下,存在一些问题。输出值偏向 0,在反向传播的时候,梯度也会偏向 0,随着层的加深,会出现梯度消失的问题。
Q: 为什么输出值偏小,在反向传播的时候,梯度也会偏向 0 呢?
我们可以假设有这样一个网络,输入 x,中间有两层。
我们可以计算 (W_1) 的梯度,下面的公式定性地理解,本渣渣意识到矩阵并不能随便用链式法则,不过这里为了定性分析,还是可以的,你也可以将 (W_1) 视为某一个具体的权值变量,而不是矩阵。
其中最后一项微分可以得到 (x),即上一层的输出。如果上一层的输出小了,那么梯度也会小,即偏向 0。叠多几层,梯度就消失了。
优化的公式
当前阶段,本渣渣只是知其然,不知所以然。天知道这些公式背后隐藏着多少的道理,为什么要用动量,为什么要累积梯度的平方?不过,虽然不知道为什么,但是至少要求自己知道怎么算。
这里先声明几个符号:
- (W),表示网络的权值。
- (g_t),表示第 t 步的梯度,一般公式是 (frac{partial L}{partial W}),如果是 mini-batch,那么就是一个累积的梯度。
- (v_t),表示第 t 步的动量,一般是梯度的累积。更新公式为 $v_t = alpha v_{t-1} - eta g_t $
- (h_t),表示第 t 步的梯度平方和的累积。一般的更新公式为,(h_t = h_{t-1} + g_t^2)
具体的计算,最好结合代码 [7] 来看:后面很多开方、除法操作,都是对矩阵的逐元素进行。
SGD
梯度下降,权值朝着梯度的反方向去。
Momentum
Nesterov momentum
先用上一步的动量,更新权值。
使用 (hat{W}) 进行一次前向传播,重新计算梯度 (g_t)。
更新动量:
再更新权值:
adagrad
看到了下面公式,就知道为什么 adagrad 学久了就学不动了吧,梯度累积的越多,越学不动。看公式理解 adagrad,你会发现开根号的操作,这个操作应用到张量上的意义是什么呢?其实开根号在这里是逐元素操作,即对张量中的每个元素开根号,于是对应的权重学习率就不同了。这正是 adagrad 和以往的不同之处,每个参数都一个学习率,而不是所有参数都使用一个学习率。
RMSProp
改进梯度累积的公式。
adadelta
改进了梯度的计算,注意到 adadelta 中的 delta 了吧,下面的公式真有 (Delta),至于如何计算,真的建议看 [7] 的代码,简单,清晰明了。再说一次,以下矩阵的开方和除法都是逐元素操作。
Adam
结合了 Momentum 和 adagrad 的思想
Yogi
在累加梯度的平方时,这个梯度可能很大(blows up),使到 Adam 不能收敛(even in convex setting),因为这个梯度大,学习率也变大,结合上面的公式看看。
于是有人提出了下面的公式进行改进
[4] 提到了上述改进的问题:
Whenever (g_t^2) has high variance or updates are sparse, (h_t) might forget past values too quickly.
翻译翻译,当 (g_t^2) 方差较大的时候,(h_t) 前面的值可能很快就被忘记了,意思就是 (h_t) 前面的值在 (h_t) 中所占的比重减少了。为了解决这个问题,Yogi 提出了如下的公式。
经过这个改进之后,(h_t) 可以变大,而对应到 Adam 中 (g_t') 的学习率会变小,这解决了 Adam 存在的问题。
总结
如上总结了常用的优化算法,本渣渣知其然,不知所以然,背后有许多为什么等待着探索。当前阶段,搞清楚每一种优化算法的优缺点,如何计算即可。此外,权值的初始化是一个比较重要的细节,需要稍加留意,在训练太慢的时候,可以检查一下。
参考链接
[1] https://zhuanlan.zhihu.com/p/73264637
[2] https://towardsdatascience.com/understanding-rmsprop-faster-neural-network-learning-62e116fcf29a
[3] https://d2l.ai/chapter_optimization/adadelta.html
[4] https://d2l.ai/chapter_optimization/adam.html
[5] 深度学习入门:斋藤康毅
[6] https://www.quora.com/Does-Xavier-initialization-work-well-when-the-activation-function-is-a-ReLu
[7] https://github.com/Lasagne/Lasagne/blob/master/lasagne/updates.py