终于可以开始讲优化算法了(写博客真是太花时间了,不过对于自我总结还是很有帮助的),本篇博客主要参照《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:计算目标函数关于当前参数的梯度
原始的梯度下降是沿着整个训练集的梯度方向下降,因为每次都需要计算所有样本的梯度,计算量可想而知非常大。而SGD是每次按照数据生成分布(独立同分布)抽取 m m 个样本作为一个小批量,通过计算它们的梯度均值,可以得到梯度的无偏估计。通过这种方式,可以得到很大程度上的加速:
为了抑制SGD的震荡,SGDM认为梯度下降过程可以加入惯性。下坡的时候,如果发现是陡坡,就利用惯性跑的快一些,反之则慢一些。SGDM在SGD基础上引入了一阶动量。
在SGD的梯度下降过程中,每一步走多远是简单的梯度乘以学习率;而在动量学习算法中,一步走多远不仅和当前梯度有关,还和过去的梯度积累有关,步长取决于梯度序列的大小和排序,当许多连续的梯度指向相同的方向时,步长最大。
SGD的一个问题是当其困在局部最优的沟壑里面震荡。举个栗子,想象一下你进入到一个盆地,四周都是略高的小山,你觉得没有下坡的方向,那就只能呆在这里了。可是如果你爬上高低,就会发现外面的世界还很广阔。因此我们不能停留在当前位置去观察未来的方向,而要想前一步,多看一步,看远一些。
在SGDM中,当前时刻的主要下降方向是由积累动量决定的,当前时刻自己的梯度方向说了也不算,那不如先看看沿着积累动量多走一步,那个时候怎么走。NAG在Step1中,不是计算当前位置的梯度方向,而是计算继续沿着动量积累方向多走一步的位置处的梯度方向:
之前的方法所有参数都使用同一个学习率,但损失函数的下降通常高度敏感于参数空间中的某些方向。一阶动量算法可以在一定程度上缓解这些问题,但这样又引入了另一个超参数。如果我们相信方向敏感度在某种程度上是轴对齐的,那么每个参数设置不同的学习率,在整个学习过程中自动适应这些学习率是有道理的。从更直观的角度看,模型中往往包含大量的参数,这些参数并不是总会用的到,对于经常更新的参数,我们已经积累了大量关于它的知识,不希望被单个样本影像太大,希望学习速率慢一些;对于偶尔更新的参数,我们了解的信息太少,希望能从每个偶然出现的样本身上多学一些,即学习速率大一些。二阶动量的出现,意味着“自适应学习率”优化算法时代的到来。
那么如何去度量一个参数的历史更新频率呢?那就是二阶动量,是至今为止所有梯度值的平方和:
AdaGrad对历史梯度一视同仁,简单的把所有梯度的平方加起来来衡量参数的更新频率,这中导致学习率单调递减的方式过于激进,RMSProp对其进行了改进。
RMSProp在AdaGrad基础上引入衰减因子,距离当前时刻越远的梯度影响越小,即主要关注一段时间窗口内的下降梯度:
讲到这里,Adam的出现就很自然而然了——它是前述方法的集大成者。SGDM在SGD基础上增加了一阶动量来抑制震荡现象,AdaGrad和RMSProp在SGD基础上增加了二阶动量实现各个参数学习率的自适应调整。将一阶动量和二阶动量都用起来就是Adam了。
有了前面的基础,Nadam就不需要怎么展开了。Nadam就是在Adam的基础上引入了Nestrov,即在计算梯度时多看一步。
到这里,大概明白了为什么说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已经掉到沟里去了)和切换算法后使用什么样的学习率。有兴趣的朋友可以去读上面第二篇论文,这里就不展开了。
本篇博客的第三部分,来在简单介绍一下二阶优化方法(我自己弄懂后再补一篇博客),前面提到的都是只是用了梯度这个一阶信息,属于一阶优化方法。
最广泛使用的二阶方法是牛顿法。牛顿法就是每次用 H−1 H − 1 重新调整梯度,就会直接跳到极小值,对于目标函数是凸的情况,牛顿法会下降的非常快。但是深度学习中目标函数通常都是非凸的,牛顿法往往会被吸引到临界点处,这是有问题的,可以通过正则化Hessian矩阵来避免。不过因为涉及到计算Hessian矩阵,当模型参数非常多的时候,计算量即非常可怕了。
共轭梯度是一种通过迭代下降的共轭方向来避免Hessian矩阵求逆计算的方法。该方法灵感来源于对梯度下降的改善。因为梯度下降每次的下降方向都是和当前等高线切线的垂直方向,是锯齿形的下降过程。这个过程是一个相当低效的来回往复,因为每次下降方向的改变都有可能在某种程度上撤销了之前取得的进展。而共轭方向则不会有这个问题,所以相比梯度下降会快很多,在 k k 维空间中,至多经过 k k 次搜索就能达到极小值。
BFGS和共轭梯度很想,只不过是使用了一个更直接的方法近似牛顿更新。牛顿法的主要计算难点在于计算Hessian的逆 H−1 H − 1 ,而拟牛顿法采用的是使用矩阵 Mt M t 迭代地低秩更新精度以更好的近似 H−1 H − 1 。和CG不同的是,BFGS花费较少的时间改进每个方向的线搜索。但是BFGS任然需要 O(n2) O ( n 2 ) de 空间存储Hessian逆矩阵 M M ,不适用于百万数量级参数的大型深度学习模型。
L-BFGS是轻量级的BFGS方法,通过避免存储完整的Hessian逆近似 M M ,存储代价显著降低。