深度学习中优化算法小结

  终于可以开始讲优化算法了(写博客真是太花时间了,不过对于自我总结还是很有帮助的),本篇博客主要参照《Deep Learing》第8章,《深度学习实战》第5章以及清华博士大佬的一篇知乎文章《一个框架看懂优化算法》。
  首先,我们先回顾一下优化算法的发展历程:SDG SGDM NAG AdaGrad RMSProp Adam Nadam。我们平时Google,百度会有很多关于这些算法的原理,各算法是怎么一步步演变的文章,而清华大佬换了一个思路,只用一个框架就对所有优化算法进行了梳理,当时看得我茅塞顿开,这里给大家分享一下。

一个框架

  我们首先定义:待优化参数: w w ,目标函数: L(f(x(i);w),y(i)) L ( f ( x ( i ) ; w ) , y ( i ) ) ,学习率: ϵ ϵ
  然后开始迭代优化过程,在每个 epoch e p o c h t t
Step1:计算目标函数关于当前参数的梯度

gt=L(f(x(i);wt),y(i)) g t = ∇ L ( f ( x ( i ) ; w t ) , y ( i ) )
Step2:根据历史梯度计算一阶动量和二阶动量
mt=ϕ(g1,g2,,gt) m t = ϕ ( g 1 , g 2 , … , g t )
Vt=ψ(g1,g2,,gt) V t = ψ ( g 1 , g 2 , … , g t )
Step3:计算当前时刻的下降梯度
ηt=ϵmtVt η t = ϵ ⋅ m t V t
Step4:根据当前的下降梯度对参数进行更新
wt+1=wtηt w t + 1 = w t − η t
  不同优化算法的Step3和Step4都是一样的,主要差别体现在Step1和Step2上。接下来我们就按照这个框架来解析一下各个优化算法。

随机梯度下降(Stochastic Gradient Descent,SGD)

  原始的梯度下降是沿着整个训练集的梯度方向下降,因为每次都需要计算所有样本的梯度,计算量可想而知非常大。而SGD是每次按照数据生成分布(独立同分布)抽取 m m 个样本作为一个小批量,通过计算它们的梯度均值,可以得到梯度的无偏估计。通过这种方式,可以得到很大程度上的加速:

gt=1mi=1mL(f(x(i);w),y(i))w g t = 1 m ∑ i = 1 m ∂ L ( f ( x ( i ) ; w ) , y ( i ) ) ∂ w
  SGD中没有动量的概念,即 mt=gt;Vt=I2 m t = g t ; V t = I 2 ,代入Step3,下降梯度就是最简单的 ηt=ϵgt η t = ϵ ⋅ g t 。至于小批量样本的抽取,仍然是那个老问题——我们无从得知数据真实的生成分布,因此我们在实践中只能尽量保证抽取的随机性(训练集一定要打乱)。这种随机获取mini-batch的方式给算法引入了噪声源,即使算法到达了最优解附近,噪声也不会消失,这也就形成了在最优解附近震荡的现象。为了缓解这种现象,在实践中,有必要随着时间的推移逐渐降低学习率。
  因为梯度下降算法下降的方向为局部最速方向,即它的下降方向在每一个下降点一定与对应的等高线的切线垂直,这导致算法优化通过的路径是锯齿状的,因此只基于梯度的优化算法(包括梯度下降和SGD)最大缺点就是下降速度慢,而且可能在沟壑两边持续震荡,停留在局部最优点。

动量(SGD with Momentum,SGDM)

  为了抑制SGD的震荡,SGDM认为梯度下降过程可以加入惯性。下坡的时候,如果发现是陡坡,就利用惯性跑的快一些,反之则慢一些。SGDM在SGD基础上引入了一阶动量。
  在SGD的梯度下降过程中,每一步走多远是简单的梯度乘以学习率;而在动量学习算法中,一步走多远不仅和当前梯度有关,还和过去的梯度积累有关,步长取决于梯度序列的大小和排序,当许多连续的梯度指向相同的方向时,步长最大。

mt=β1mt1+(1β1)gt m t = β 1 ⋅ m t − 1 + ( 1 − β 1 ) ⋅ g t
一阶动量其实就是各个时刻梯度方向的指数移动平均值,约等于最近 11β1 1 1 − β 1 个时刻的梯度向量和的平均值。 β1 β 1 在实践中一般取0.5、0.9和0.99,和学习率一样, β1 β 1 也会随着时间不断调整,一般初始值是一个较小的值,因为一开始没有积累什么学习经验,所以以当前找到的梯度方向为准;随后慢慢增大 β1 β 1 的值,下降方向主要是此前积累的下降方向,避免最后因为优化超平面的波动陷入持续震荡。动量法是令梯度直接指向最优解的策略之一。
  SGDM是一阶动量算法,二阶动量依然为 Vt=I2 V t = I 2

牛顿动量法(Nesterov Acceleration Gradient,NAG)

  SGD的一个问题是当其困在局部最优的沟壑里面震荡。举个栗子,想象一下你进入到一个盆地,四周都是略高的小山,你觉得没有下坡的方向,那就只能呆在这里了。可是如果你爬上高低,就会发现外面的世界还很广阔。因此我们不能停留在当前位置去观察未来的方向,而要想前一步,多看一步,看远一些。
  在SGDM中,当前时刻的主要下降方向是由积累动量决定的,当前时刻自己的梯度方向说了也不算,那不如先看看沿着积累动量多走一步,那个时候怎么走。NAG在Step1中,不是计算当前位置的梯度方向,而是计算继续沿着动量积累方向多走一步的位置处的梯度方向:

gt=L(f(x(i);wt+1) g t = ∇ L ( f ( x ( i ) ; w t + 1 )
然后用这个表示下一步梯度方向的梯度域历史积累动量结合,计算Step2中当前时刻的积累动量。
  NAG收敛速度更好,防止算法过快且增加了反应性(多走一步的方式具有前瞻性,能及时“刹车”)。NAG也没有用到二阶动量。

AdaGrad(自适应梯度)

  之前的方法所有参数都使用同一个学习率,但损失函数的下降通常高度敏感于参数空间中的某些方向。一阶动量算法可以在一定程度上缓解这些问题,但这样又引入了另一个超参数。如果我们相信方向敏感度在某种程度上是轴对齐的,那么每个参数设置不同的学习率,在整个学习过程中自动适应这些学习率是有道理的。从更直观的角度看,模型中往往包含大量的参数,这些参数并不是总会用的到,对于经常更新的参数,我们已经积累了大量关于它的知识,不希望被单个样本影像太大,希望学习速率慢一些;对于偶尔更新的参数,我们了解的信息太少,希望能从每个偶然出现的样本身上多学一些,即学习速率大一些。二阶动量的出现,意味着“自适应学习率”优化算法时代的到来。
  那么如何去度量一个参数的历史更新频率呢?那就是二阶动量,是至今为止所有梯度值的平方和:

Vt=i=1tg2i V t = ∑ i = 1 t g i 2
回顾Step3中的下降梯度公式,可以看出,此时学习率实质上变成了 ϵVt ϵ V t ,一般为了避免分母为0,会在分母上加上一个小的平滑项,此时梯度下降更新公式如下:
ηt=ϵmtVt+δ η t = ϵ ⋅ m t V t + δ
此时,当参数更新越频繁,则 Vt V t 越大,学习率就越低,是不是很简单?
  AdaGrad在稀疏数据场景下表现的非常好,但因为 Vt V t 是单调递增的,会使学习率单调递减至0,可能会使训练过程提前结束,即使后续有数据也无法学到必要的知识。所以AdaGrad也对初始值敏感(如果一开始梯度就很大,那么极有可能很快就停止学习)。

RMSProp(均方差传播)

  AdaGrad对历史梯度一视同仁,简单的把所有梯度的平方加起来来衡量参数的更新频率,这中导致学习率单调递减的方式过于激进,RMSProp对其进行了改进。
  RMSProp在AdaGrad基础上引入衰减因子,距离当前时刻越远的梯度影响越小,即主要关注一段时间窗口内的下降梯度:

Vt=β2Vt1+(1β2)g2t V t = β 2 ⋅ V t − 1 + ( 1 − β 2 ) ⋅ g t 2
如果最近一段时间某个参数更新的很频繁,那么久选择较小的更新幅度;如果最近一段时间几乎没有更新,则选择大幅度更新一下。这样就避免了二阶动量持续积累,导致训练过程提早结束的问题。

Adam

  讲到这里,Adam的出现就很自然而然了——它是前述方法的集大成者。SGDM在SGD基础上增加了一阶动量来抑制震荡现象,AdaGrad和RMSProp在SGD基础上增加了二阶动量实现各个参数学习率的自适应调整。将一阶动量和二阶动量都用起来就是Adam了。

mt=β1mt1+(1β1)gtVt=β2Vt1+(1β2)g2t m t = β 1 ⋅ m t − 1 + ( 1 − β 1 ) ⋅ g t V t = β 2 ⋅ V t − 1 + ( 1 − β 2 ) ⋅ g t 2
在实际应用中,参数的经验值是 β1=0.9β2=0.999 β 1 = 0.9 , β 2 = 0.999 m0 m 0 V0 V 0 一般初始化为0,这就会导致在初期 mt m t Vt V t 都会接近0,这是有问题的。因此我们常常会用下面的式子进行误差修正:
m~t=mt1βt1V~t=Vt1βt2 m ~ t = m t 1 − β 1 t V ~ t = V t 1 − β 2 t
于是在初期( t t 较小)的时候 mt m t Vt V t 会除以一个小于1的数,得到了放大,避免了初期接近0的情况;随着迭代的进行, βt0 β t ≈ 0 ,有 m~tmt m ~ t ≈ m t V~tVt V ~ t ≈ V t ,在后期就不需要对一阶二阶动量进行放大了。

Nadam

  有了前面的基础,Nadam就不需要怎么展开了。Nadam就是在Adam的基础上引入了Nestrov,即在计算梯度时多看一步。


Adam的缺陷

  到这里,大概明白了为什么说Adam/Nadam是目前最主流最好用的方法。但是人无完人,Adam依然存在两个很重要的缺陷。
1. 可能不收敛
  回忆一下之前的各个优化算法,SGD没有用到二阶动量,因此学习率是恒定的(实际使用中会采用学习率递减)。AdaGrad是二阶动量不断积累,最终学习率衰减到0,模型得以收敛。
  而RMSProp和Adam则不一样,二阶动量是固定时间窗口内的积累,随着时间窗口的变化,遇到的数据可能发生巨变,使得 Vt V t 可能会时大时小,不是单调变化。这就可能导致在训练后期引起学习率的震荡,导致模型无法收敛。
2. 可能错过全局最优解
  深度神经网络往往包含大量的参数,在这样一个维度极高的空间内,非凸的目标函数往往起起伏伏,拥有无数个高地和洼地。高峰通过引入动量可能很容易越过,但是对于高原地区,可能探索很多次都出不来,于是停止了训练。
  有一篇文章《The Marginal Value of Adaptive Gradient Methods in Machine Learning》提到,同一个优化问题,不同优化算法可能会找到不同的答案,但是自适应学习率算法的结果往往比较差,因为自适应学习率算法可能会对前期出现的特征过拟合,后期才出现的特征很难纠正前期的拟合效果。另一篇文章《Improving Generalization Performance by Switching from Adam to SGD》提到,Adam收敛速度比SGD快很多,但是最终收敛的结果不如SGD好,主要是因为后期Adam学习率太低,影响了有效的收敛,如果对Adam的学习率下界进行控制,效果会好很多。
  基于这两点,出现了一种方法——先用Adam快速下降,然后在用SGD调优。问题就在于什么时候切换优化算法(切换得太晚的话Adam已经掉到沟里去了)和切换算法后使用什么样的学习率。有兴趣的朋友可以去读上面第二篇论文,这里就不展开了。


二阶优化方法

  本篇博客的第三部分,来在简单介绍一下二阶优化方法(我自己弄懂后再补一篇博客),前面提到的都是只是用了梯度这个一阶信息,属于一阶优化方法。

牛顿法

  最广泛使用的二阶方法是牛顿法。牛顿法就是每次用 H1 H − 1 重新调整梯度,就会直接跳到极小值,对于目标函数是凸的情况,牛顿法会下降的非常快。但是深度学习中目标函数通常都是非凸的,牛顿法往往会被吸引到临界点处,这是有问题的,可以通过正则化Hessian矩阵来避免。不过因为涉及到计算Hessian矩阵,当模型参数非常多的时候,计算量即非常可怕了。

共轭梯度(CG)

  共轭梯度是一种通过迭代下降的共轭方向来避免Hessian矩阵求逆计算的方法。该方法灵感来源于对梯度下降的改善。因为梯度下降每次的下降方向都是和当前等高线切线的垂直方向,是锯齿形的下降过程。这个过程是一个相当低效的来回往复,因为每次下降方向的改变都有可能在某种程度上撤销了之前取得的进展。而共轭方向则不会有这个问题,所以相比梯度下降会快很多,在 k k 维空间中,至多经过 k k 次搜索就能达到极小值。

BFGS

  BFGS和共轭梯度很想,只不过是使用了一个更直接的方法近似牛顿更新。牛顿法的主要计算难点在于计算Hessian的逆 H1 H − 1 ,而拟牛顿法采用的是使用矩阵 Mt M t 迭代地低秩更新精度以更好的近似 H1 H − 1 。和CG不同的是,BFGS花费较少的时间改进每个方向的线搜索。但是BFGS任然需要 O(n2) O ( n 2 ) de 空间存储Hessian逆矩阵 M M ,不适用于百万数量级参数的大型深度学习模型。

L-BFGS

  L-BFGS是轻量级的BFGS方法,通过避免存储完整的Hessian逆近似 M M ,存储代价显著降低。

你可能感兴趣的:(机器学习和深度学习)