• 优化方法总结以及Adam存在的问题(SGD, Momentum, AdaDelta, Adam, AdamW,LazyAdam)


    优化方法总结以及Adam存在的问题(SGD, Momentum, AdaDelta, Adam, AdamW,LazyAdam)

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
    本文链接:https://blog.csdn.net/yinyu19950811/article/details/90476956

    优化方法概述

    模型优化方法的选择直接关系到最终模型的性能。有时候效果不好,未必是特征的问题或者模型设计的问题,很可能是优化算法的问题,而且好的优化算法还能够帮助加速训练模型。

    深度学习模型的发展进程:
    SGD -> SGDM ->NAG -> AdaGrad -> AdaDelta -> Adam -> Nadam

    1.整体框架

    首先定义:
    待优化参数www, 目标函数:f(w)f(w)f(w),学习率αalphaα
    每次迭代:

    1. 计算目标函数关于参数的梯度:gt=f(wt)g_t=igtriangledown f(w_t)gt=f(wt)
    2. 根据历史梯度计算一阶动量和二阶动量:mt=ϕ(g1,g2,...,gt),Vt=ψ(g1,g2,...,gt)m_t=phi(g_1, g_2, ..., g_t), V_t=psi(g_1, g_2,...,g_t)mt=ϕ(g1,g2,...,gt),Vt=ψ(g1,g2,...,gt)
    3. 计算当前时刻的下降梯度:ηt=αmt/Vt−−√eta_t= alpha cdot m_t/ sqrt{V_t}ηt=αmt/Vt
    4. 根据下降梯度对参数进行更新:wt+1=wtηtw_{t+1}=w_t-eta_twt+1=wtηt

    步骤3、4对于各个算法都是一致的,主要的差别就体现在1和2上。

    1.1 SGD

    没有动量的概念,第三步中 ηt=αgteta_t=alpha cdot g_tηt=αgt
    其中batch_size是一个重要的变量,需要做一个快速尝试,才能够找到能够最有效地减少成本函数的那个,一般是2n2^n2n。
    缺点:

    • 有可能会陷入局部最小值;
    • 不会收敛,最终会一直在最小值附近波动,并不会达到最小值并停留在此;
    • 下降速度慢;
    • 选择合适的learning rate比较困难;
    • 在所有方向上统一的缩放梯度,不适用于稀疏数据

    1.2 Momentum

    SGD下降方法的缺点是参数更新方向只依赖于当前batch计算出的梯度,因此十分的不稳定。为了抑制SGD的震荡,动量认为梯度下降的过程中可以加入惯性。动量梯度下降法运行速度总是快于标准的梯度下降法,其基本思想是在SGD的基础上引入了一阶动量:
    mt=βmt1+(1β)gtm_t=eta m_{t-1}+(1-eta)g_tmt=βmt1+(1β)gt
    一阶动量指的是各个时刻梯度的指数加权平均,约等于11β1frac{1}{1-eta_1}1β11个历史时刻的梯度向量和的平均值,也就是t时刻的下降方向,不仅由当前点的梯度方向决定,还由此前的累积的梯度来决定,βetaβ的经验值一般为0.9,也就是意味着下降方向主要是此前累积的下降方向,并略微偏向当前时刻的下降方向。并利用当前batch微调最终的更新方向。如果当前梯度方向与历史梯度一致,会增强该方向的梯度。如果不一致,能够减少更新。
    优点:

    • 增加了稳定性;
    • 收敛速度更快;
    • 还有一定摆脱局部最优的能力。

    1.2.1 理解指数加权平均

    使得β=0.9eta=0.9β=0.9
    m100=0.9m99+0.1θ100m_{100}=0.9m_{99}+0.1 heta_{100}m100=0.9m99+0.1θ100
    m99=0.9m98+0.1θ99m_{99}=0.9m_{98}+0.1 heta_{99}m99=0.9m98+0.1θ99
    m98=0.9m97+0.1θ98m_{98}=0.9m_{97}+0.1 heta_{98}m98=0.9m97+0.1θ98
    把公式带入会得到:
    m100=0.1θ100+0.10.9θ99+0.1(0.9)2θ98+0.1(0.9)3θ97+0.1(0.9)4θ96m_{100}=0.1 heta_{100}+0.1*0.9 heta_{99}+0.1*(0.9)^2 heta_{98}+0.1*(0.9)^3 heta_{97}+0.1*(0.9)^4 heta_{96}m100=0.1θ100+0.10.9θ99+0.1(0.9)2θ98+0.1(0.9)3θ97+0.1(0.9)4θ96
    可以看出这是一个第100天的数据包含了99,98,97天的数据,而且是一个指数衰减的过程,这些系数的和为1或者逼近于1。到底平均了多少天的数据?0.9100.9^{10}0.910大约为0.35,约等于1efrac{1}{e}e1的值。也就是10天之后曲线的高度下降到13frac{1}{3}31,相当于只关注了过去10天的数据,因为10天后,权重下降到不到当日权重的三分之一。如果β=0.98eta=0.98β=0.98,那么0.98500.98^{50}0.9850大约等于1efrac{1}{e}e1,可以看作平均了50天的数据,由此得到公式平均了大约11βfrac{1}{1-eta}1β1的数据。

    好处:占用极少的内存,每次把最新的公式带入不断覆盖就可以了。虽然不是最精确的,如果直接平均过去天的数据往往会得到更好的估计,但是需要保存最近的温度数据必须占用更多的内存,执行更加复杂,计算成本也更高。

    1.2.2 偏差修正

    计算移动平均数时,初始化m0=0,m1=0.9m0+0.1θ1m_0=0,m_1=0.9m_0+0.1 heta_1m0=0,m1=0.9m0+0.1θ1m1=0.1θ1m_1=0.1 heta_1m1=0.1θ1,显然第一天的值会小很多,估计不准确;而且m2=0.9m1+0.1θ2m_2=0.9m_1+0.1 heta_2m2=0.9m1+0.1θ2,如果带入m1m_1m1然后相乘得到m2=0.90.1θ1+0.1θ2m_2=0.9*0.1 heta_1+0.1 heta_2m2=0.90.1θ1+0.1θ2m2m_2m2要远小于θ1 heta_1θ1θ2 heta_2θ2,所以m2m_2m2不能很好的估计出前两天的数据,而当t足够大时,mtˆ=mthat{m_t}=m_tmt^=mt
    偏差修正能够改正这个问题,特别是在初期。m1/(1βt)=m1/0.1m_1/(1-eta^t)=m_1/0.1m1/(1βt)=m1/0.1后,m1=θ1m_1= heta_1m1=θ1就去除了偏差。而随着t的增加,βteta^tβt接近于0,所以当t很大的时候,偏差修正几乎没有作用。在计算指数加权平均数的大部分时候,都不在乎执行偏差修正,因为大部分人宁愿熬过初始时期,拿到具有偏差的估测,然后继续计算下去。如果关心初始时期的偏差,在刚开始计算指数加权移动平均数的时候,偏差修正能帮助在早期获取更好的估测。

    m˜t=mt1βt1V˜t=Vt1βt2 ilde{m}_{t}=frac{m_{t}}{1-eta_1^t}\ ilde{V}_{t}=frac{V_{t}}{1-eta_2^t}m~t=1β1tmtV~t=1β2tVt

    1.3 AdaGrad

    之前的方法都没有用到二阶动量,二阶动量的出现,才意味着“自适应率”优化算法的到来。SGD以及动量以同样的学习率更新每个参数。神经网络模型往往包含大量的参数,但是这些参数并不会总是用得到,更新频率也不一样。对于经常更新的参数,不希望其被单个样本影响太大,希望学习速率慢一些;对于偶尔更新的参数,了解的信息太少,希望能够从每个偶然出现的样本身上多学一些,即学习速率大一些。

    怎么去度量历史更新频率呢?就是二阶动量:至今为止所有梯度值的平方和:
    Vt=Tt=1g2tV_t=sum_{t=1}^{T}g_t^2Vt=t=1Tgt2
    可以看出,此时实质上的学习率由αalphaα变为了α/Vt−−√alpha/sqrt{V_t}α/Vt,一般为了避免分母为0,会在分母上加一个小的平滑项,因此Vt−−√sqrt{V_t}Vt是恒大于0的。所以如果参数更新频繁,其二阶动量越大,学习率就越小。

    优点:

    • 不同更新频率的参数具有不同的学习率,减少摆动,在稀疏数据场景下表现会非常好;
    • 允许使用一个更大的学习率αalphaα,从而加快算法的学习速度;

    缺点:

    • 因为Vt−−√sqrt{V_t}Vt是不断累积单调递增的,会使得学习率单调递减至0,可能会使得训练过程提前结束,即使后续还有数据也无法学到需要的知识;

    1.4 Nesterov

    SGD 还有一个问题是困在局部最优的沟壑里面震荡。想象一下你走到一个盆地,四周都是略高的小山,你觉得没有下坡的方向,那就只能待在这里了。可是如果你爬上高地,就会发现外面的世界还很广阔。因此,我们不能停留在当前位置去观察未来的方向,而要向前一步、多看一步、看远一些。
    NAG全称Nesterov Accelerated Gradient,是在SGD、SGD-M的基础上的进一步改进,改进点在于步骤1。我们知道在时刻t的主要下降方向是由累积动量决定的,自己的梯度方向说了也不算,那与其看当前梯度方向,不如先看看如果跟着累积动量走了一步,那个时候再怎么走。因此,NAG在步骤1,不计算当前位置的梯度方向,而是计算如果按照累积动量走了一步,那个时候的下降方向:

    gt=f(wtαmt1/Vt1−−−−√)g_t= abla f(w_t-alpha cdot m_{t-1} / sqrt{V_{t-1}})gt=f(wtαmt1/Vt1)

    然后用下一个点的梯度方向,与历史累积动量相结合,计算步骤2中当前时刻的累积动量。

    1.5 AdaDelta/RMSProp

    由于AdaGrad单调递减的学习率变化过于激进,我们考虑一个改变二阶动量计算方法的策略:不累加全部历史梯度,而只关注过去一段时间窗口的下降梯度,很自然的想到之前动量使用的指数加权平均,它所计算的就是过去一段时间的平均值,所以使用这一方法来计算二阶累积动量:
    Vt=β2Vt1+(1β2)g2tV_t=eta_2 *V_{t-1} + (1- eta_2)g_t^{2}Vt=β2Vt1+(1β2)gt2
    这就避免了二阶动量持续累积,导致训练过程提前结束的问题了。

    1.6 Adam(AdaptiVe Moment Estimation)

    谈到这里,Adam的出现就自然而然了,它是前述方法的集大成者。动量在SGD的基础上增加了一阶动量,AdaGrad和AdaDelta在SGD的基础上增加了二阶动量。Adam实际上就是将Momentum和RMSprop集合在一起,把一阶动量和二阶动量都使用起来了,具体方法为:

    Iteration:t=t+1mt=β1mt1+(1β1)gtVt=β2Vt1+(1β2)g2tαt=α1βt2−−−−−√/(1βt1)wt+1=wtαtmtVt√+ϵIteration:\t=t+1\m_{t}=eta_1m_{t-1}+(1-eta_1)g_t \V_{t}=eta_2V_{t-1}+(1-eta_2)g_{t}^{2} \alpha_t=alpha*sqrt{1-eta^t_2}/(1-eta^t_1) \w_{t+1}=w_{t}-alpha_tfrac{m_{t}}{sqrt{V_{t}}+epsilon}Iteration:t=t+1mt=β1mt1+(1β1)gtVt=β2Vt1+(1β2)gt2αt=α1β2t/(1β1t)wt+1=wtαtVt+ϵmt

    本算法中有很多超参数,超参数学习率αalphaα很重要,也经常需要调试。β1eta_1β1β2eta_2β2是加权平均数,用于控制一阶动量和二阶动量。常用的缺省值为0.9和0.999。关于ϵepsilonϵ的选择其实没那么重要,Adam论文的作者建议ϵepsilonϵ为10810^{-8}108,因为它并不会影响算法表现。

    优点:

    • 自动调整参数的学习率;
    • 大幅提升了训练速度;
    • 提高了稳定性;

    1.7 Adam的改进

    1.7.1 Adamw

    Adam有很多的优点,但是在很多数据集上的最好效果还是用SGD with Momentum细调出来的。可见Adam的泛化性并不如SGD with Momentum。https://arxiV.org/pdf/1711.05101.pdf 中提出其中一个重要原因就是Adam中L2正则化项并不像在SGD中那么有效。

    1. L2正则和Weight Decay在Adam这种自适应学习率算法中并不等价,只有在标准SGD的情况下,可以将L2正则和Weight Decay看做一样。特别是,当与自适应梯度相结合时,L2正则化导致具有较大历史参数和/或梯度幅度的权重比使用权重衰减时更小。

    2. 使用Adam优化带L2正则的损失并不有效,如果引入L2正则化项,在计算梯度的时候会加上正则项求梯度的结果。正常的权重衰减是对所有的权重都采用相同的系数进行更新,本身比较大的一些权重对应的梯度也会比较大,惩罚也越大。但由于Adam计算步骤中减去项会有除以梯度平方的累积,使得梯度大的减去项偏小,从而具有大梯度的权重不会像解耦权重衰减那样得到正则化。 这导致自适应梯度算法的L2和解耦权重衰减正则化的不等价。

    而在常见的深度学习库中只提供了L2正则,并没有提供权重衰减的实现。这可能就是导致Adam跑出来的很多效果相对SGD with Momentum有偏差的一个原因。

    Adam with L2 regularization和AdamW的代码:
    在这里插入图片描述
    如果是SGD,L2正则化项和梯度衰减是等同的。但是由于Adam加入了一阶动量和二阶动量,基于包含L2正则化项的梯度来计算一阶动量和二阶动量,使得参数的更新系数就会变化,与单纯的权重衰减就会变得不同。图中红色的为原始的Adam+L2 regularization的方法,如果把line 6,line 7,line 8都带入到line 12,并且假设ηt=1eta_t=1ηt=1:
    θtθt1αβ1mt1+(1β1)(ft+λθt1)Vtˆ√+ϵ heta_t ightarrow heta_{t-1}-alphafrac{eta_1m_{t-1}+(1-eta_1)(igtriangledown f_t+lambda heta_{t-1})}{sqrt{hat{V_t}}+epsilon}θtθt1αVt^+ϵβ1mt1+(1β1)(ft+λθt1)

    可以看出,分子右上角的λθt1lambda heta_{t-1}λθt1向量各个元素被分母的Vtˆ−−√sqrt{hat{V_t}}Vt^项调整了。梯度快速变化的方向上,Vtˆ−−√sqrt{hat{V_t}}Vt^有更大的值,而使得调整后的λθt1Vtˆ√frac{lambda heta_{t-1}}{sqrt{hat{V_t}}}Vt^λθt1更小,在这个方向上参数θ hetaθ被正则化的更少。这显然是不合理的,L2 regularization和weight decay都应该是各向同性的。所以作者提出以绿色的方式来在Adam中正确的引入weight decay的方式,称作AdamW,也就是不让λθt1lambda heta_{t-1}λθt1项被Vtˆ−−√sqrt{hat{V_t}}Vt^调整,使用相同的λlambdaλ来正则化所有的权重,完成了梯度下降与weight decay的解耦。

    大部分的模型都会有L2 regularization约束项,因此很有可能出现Adam的最终效果没有sgd的好。目前bert训练采用的优化方法就是Adamw,对除了layernorm,bias项之外的模型参数做weight decay。

    L2 regularization与Weight decay在SGD时是等价的

    • L2正则化的目的是为了在一定程度上减少模型过拟合的问题。

      fregt(θ)=ft(θ)+λ2θ22f_t^{reg}( heta)=f_t( heta)+frac{lambda'}{2}|| heta||^2_2ftreg(θ)=ft(θ)+2λθ22

      θt+1θtαfregt(θt)=θtαft(θt)αλθt heta_{t+1} ightarrow heta_t-alphaigtriangledown f_t^{reg}( heta_t)= heta_t-alphaigtriangledown f_t( heta_t)-alphalambda' heta_tθt+1θtαftreg(θt)=θtαft(θt)αλθt

    • weight decay
      是在每次更新的梯度基础上减去一个梯度:
      θt+1(1λ)θtαft(θt) heta_{t+1} ightarrow (1-lambda) heta_t-alphaigtriangledown f_t( heta_t)θt+1(1λ)θtαft(θt)

    可以看出当λ=λαlambda'= frac{lambda}{alpha}λ=αλ时,L2 regularization 和 Weight decay 是等价的(仅在使用标准SGD优化时成立)。

    权重衰减(L2正则化项)为什么能够避免模型过拟合的问题?

    • 奥卡姆剃刀法则;
    • 过拟合模型的系数往往非常大,因为过拟合就是需要顾忌每一个点,最终形成的拟合函数波动很大,这就意味在某些小区间里的导数值非常大,也就是系数很大,而通过正则化约束参数的范数使其不要太大,可以在一定程度上减少过拟合情况。

    1.7.2 LazyAdam

    和图像等领域不同,对于NLP之类的任务,每个batch采样到的词有限,每次更新对embedding的梯度估计都是稀疏的。对于 momentum-based 的 Optimizer,现在所有框架的实现都会用当前的 momentum 去更新所有的词,即使这些词在连续的几十步更新里都没有被采样到。这可能会使 Embedding 过拟合。

    LazyAdam是Adam的变体,可以更有效地处理稀疏更新。原始的Adam算法为每个可训练变量维护两个移动平均累加器,累加器在每一步都会更新**。 而此类为稀疏变量提供了更加懒惰的梯度更新处理,它仅更新当前batch中出现的稀疏变量索引的移动平均累加器,而不是更新所有索引的累加器。 与原始的Adam优化器相比,它可以为某些应用提供模型训练吞吐量的大幅改进。 但是它的语义与原始的Adam算法略有不同,可能会导致不同的实验结果。

    AdamOptimizer源码中函数_apply_sparse和_resource_apply_sparse 主要用在稀疏向量的更新操作上,而具体的实现是在函数_apply_sparse_shared中

    LazyAdam的源码:

    ```python/py
     def _apply_sparse(self, grad, Var):
        beta1_power, beta2_power = self._get_beta_accumulators()
        beta1_power = math_ops.cast(beta1_power, Var.dtype.base_dtype)
        beta2_power = math_ops.cast(beta2_power, Var.dtype.base_dtype)
        lr_t = math_ops.cast(self._lr_t, Var.dtype.base_dtype)
        beta1_t = math_ops.cast(self._beta1_t, Var.dtype.base_dtype)
        beta2_t = math_ops.cast(self._beta2_t, Var.dtype.base_dtype)
        epsilon_t = math_ops.cast(self._epsilon_t, Var.dtype.base_dtype)
        lr = (lr_t * math_ops.sqrt(1 - beta2_power) / (1 - beta1_power))
    
        # \(m := beta1 * m + (1 - beta1) * g_t\)
        m = self.get_slot(Var, "m")
        m_t = state_ops.scatter_update(m, grad.indices,
                                       beta1_t * array_ops.gather(m, grad.indices) +
                                       (1 - beta1_t) * grad.Values,
                                       use_locking=self._use_locking)#一阶动量
    
        # \(V := beta2 * V + (1 - beta2) * (g_t * g_t)\)
        V = self.get_slot(Var, "V")
        V_t = state_ops.scatter_update(V, grad.indices,
                                       beta2_t * array_ops.gather(V, grad.indices) +
                                       (1 - beta2_t) * math_ops.square(grad.Values),
                                       use_locking=self._use_locking) #二阶动量
    
        # \(Variable -= learning_rate * m_t / (epsilon_t + sqrt(V_t))\)
        m_t_slice = array_ops.gather(m_t, grad.indices)
        V_t_slice = array_ops.gather(V_t, grad.indices)
        denominator_slice = math_ops.sqrt(V_t_slice) + epsilon_t
        Var_update = state_ops.scatter_sub(Var, grad.indices,
                                           lr * m_t_slice / denominator_slice,
                                           use_locking=self._use_locking)
        return control_flow_ops.group(Var_update, m_t, V_t)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    可以看出公式与Adam都相同,不同的是每次迭代根据当前batch的indices来对一阶动量和二阶动量进行更新。

    1.7.3 Madam

    
    class MaskedAdamOptimizer(adam.AdamOptimizer):
        def _apply_sparse_shared(self, grad, Var, indices, scatter_add):
            beta1_power, beta2_power = self._get_beta_accumulators()
            beta1_power = math_ops.cast(beta1_power, Var.dtype.base_dtype)
            beta2_power = math_ops.cast(beta2_power, Var.dtype.base_dtype)
            lr_t = math_ops.cast(self._lr_t, Var.dtype.base_dtype)
            beta1_t = math_ops.cast(self._beta1_t, Var.dtype.base_dtype)
            beta2_t = math_ops.cast(self._beta2_t, Var.dtype.base_dtype)
            epsilon_t = math_ops.cast(self._epsilon_t, Var.dtype.base_dtype)
            lr = (lr_t * math_ops.sqrt(1 - beta2_power) / (1 - beta1_power))
            # m_t = beta1 * m + (1 - beta1) * g_t
            m = self.get_slot(Var, "m")
            m_scaled_g_Values = grad * (1 - beta1_t)
            m_t = state_ops.assign(m, m * beta1_t,
                                   use_locking=self._use_locking)   #与LazyAdam的不同之处
            with ops.control_dependencies([m_t]):
                m_t = scatter_add(m, indices, m_scaled_g_Values)
            # V_t = beta2 * V + (1 - beta2) * (g_t * g_t)
            V = self.get_slot(Var, "V")
            V_scaled_g_Values = (grad * grad) * (1 - beta2_t)
            V_t = state_ops.assign(V, V * beta2_t, use_locking=self._use_locking)
            with ops.control_dependencies([V_t]):
                V_t = scatter_add(V, indices, V_scaled_g_Values)
            gather_m_t = array_ops.gather(m_t, indices)
            gather_V_t = array_ops.gather(V_t, indices)
            gather_V_sqrt = math_ops.sqrt(gather_V_t)
            Var_update = scatter_add(Var, indices, -lr * gather_m_t / (gather_V_sqrt + epsilon_t))
            return control_flow_ops.group(*[Var_update, m_t, V_t])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    两者在计算移动平均累加器时(一阶动量和二阶动量)有所不同:
    LazyAdam:

    m_t = state_ops.scatter_update(m, grad.indices,
                                       beta1_t * array_ops.gather(m, grad.indices) +
                                       (1 - beta1_t) * grad.Values,
                                       use_locking=self._use_locking)
    
    • 1
    • 2
    • 3
    • 4

    Madam:

    m_scaled_g_Values = grad * (1 - beta1_t)
            m_t = state_ops.assign(m, m * beta1_t,
                                   use_locking=self._use_locking)  
            with ops.control_dependencies([m_t]):
                m_t = scatter_add(m, indices, m_scaled_g_Values)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Madam其实是介于Lazy Adam和 Adam之间的一种方法,其与Lazy Adam唯一的不同在于对一阶动量m和二阶动量 V 进行 decay 的操作,Madam是全部都要 decay,即当前batch没有采样到的变量所对应的之前动量的累积值也要考虑。 而LazyAdam 是只 decay 采样到的embedding。(在计算指数加权平均时,LazyAdam只对当前采样到的变量之前的平均值进行累加,没有采样到的样本不累加,而Madam要全部累加)。

    LazyAdam存在的一个问题是当梯度为0时不更新对应的m和v。实际上当其他权重改变时m和v应该更新。Madam应该是解决了这个问题所以性能变得更好。

    为了更形象的说明它们的差异,通过一个假设的例子来说明,用一阶动量来举例:

    • LazyAdam:
      mt0=(0,0,0,0,0)m_{t_0}=(0,0,0,0,0)mt0=(0,0,0,0,0)
      index1=(1,2,4)index_1=(1,2,4)index1=(1,2,4)
      mt1=β1mt0{1,2,4}+gt1{1,2,4}m_{t_1}=eta_1m_{t_0}{1,2,4}+ g_{t_1}{1,2,4}mt1=β1mt0{1,2,4}+gt1{1,2,4}
      index2=(2,3)index_2=(2,3)index2=(2,3)
      mt2=β1mt1{2,3}+gt2{2,3}m_{t_2}=eta_1m_{t_1}{2,3}+g_{t_2}{2,3}mt2=β1mt1{2,3}+gt2{2,3}
      可以看出如果当前变量梯度为0,那么在计算当前的mtm_tmt时,不会对这些变量之前的动量进行累加更新。
    • Madam:
      mt0=(0,0,0,0,0)m_{t_0}=(0,0,0,0,0)mt0=(0,0,0,0,0)
      index1=(1,2,4)index_1=(1,2,4)index1=(1,2,4)
      mt1=β1mt0+gt1{1,2,4}m_{t_1}=eta_1m_{t_0}+ g_{t_1}{1,2,4}mt1=β1mt0+gt1{1,2,4}
      index2=(2,3)index_2=(2,3)index2=(2,3)
      mt2=β1mt1+gt2{2,3}m_{t_2}=eta_1m_{t_1}+g_{t_2}{2,3}mt2=β1mt1+gt2{2,3}
      而Madam还是会对所有变量的动量进行更新;

    2. 到底是用Adam还是用SGD

    在上一篇中,我们用同一个框架让各类算法对号入座。可以看出,大家都是殊途同归,只是相当于在SGD基础上增加了各类学习率的主动控制。如果不想做精细的调优,那么Adam显然最便于直接拿来上手。但这样的傻瓜式操作并不一定能够适应所有的场合。如果能够深入了解数据,研究员们可以更加自如地控制优化迭代的各类参数,实现更好的效果也并不奇怪。

    SGD虽然训练时间更长,容易陷入鞍点,但是在好的初始化和学习率调度方案的情况下,结果更可靠。SGD现在后期调优时还是经常使用到,但SGD的问题是前期收敛速度慢。SGD前期收敛慢的原因: SGD在更新参数时对各个维度上梯度的放缩是一致的,并且在训练数据分布极不均衡时训练效果很差。而因为收敛慢的问题应运而生的自适应优化算法Adam、AdaGrad、RMSprop 等,但这些自适应的优化算法泛化能力可能比非自适应方法更差,虽然可以在训练初始阶段展现出快速的收敛速度,但其在测试集上的表现却会很快陷入停滞,并最终被 SGD 超过。 实际上,在自然语言处理和计算机视觉方面的一些最新的工作中SGD(或动量)被选为优化器,其中这些实例中SGD 确实比自适应方法表现更好。

    2.1 Adam的罪状

    Adam的罪状一
    这篇是正在深度学习领域顶级会议之一 ICLR 2018 匿名审稿中的 On the ConVergence of Adam and Beyond,探讨了Adam算法的收敛性,通过反例证明了Adam在某些情况下可能会不收敛。
    回忆一下上文提到的各大优化算法的学习率:ηt=α/Vt−−√eta_t=alpha/ sqrt{V_t}ηt=α/Vt
    其中,SGD没有用到二阶动量,因此学习率是恒定的(实际使用过程中会采用学习率衰减策略,因此学习率递减)。AdaGrad的二阶动量不断累积,单调递增,因此学习率是单调递减的。因此,这两类算法会使得学习率不断递减,最终收敛到0,模型也得以收敛。

    但AdaDelta和Adam则不然。二阶动量是固定时间窗口内的累积,随着时间窗口的变化,遇到的数据可能发生巨变,使得 VtV_tVt可能会时大时小,不是单调变化。这就可能在训练后期引起学习率的震荡,导致模型无法收敛。

    这篇文章也给出了一个修正的方法。由于Adam中的学习率主要是由二阶动量控制的,为了保证算法的收敛,可以对二阶动量的变化进行控制,避免上下波动。
    Vt=max(β2Vt1+(1β2)g2t,Vt1)V_t=max(eta_2*V_{t-1}+(1-eta_2)g_t^2,V_{t-1})Vt=max(β2Vt1+(1β2)gt2,Vt1)
    这样就保证了VtVt1||V_t|| geq ||V_{t-1}||VtVt1∣,使得学习率单调递减。

    Adam的罪状二

    深度神经网络往往包含大量的参数,在这样一个维度极高的空间内,非凸的目标函数往往起起伏伏,拥有无数个高地和洼地。有的是高峰,通过引入动量可能很容易越过;但有些是高原,可能探索很多次都出不来,于是停止了训练。

    近期ArxiV上的两篇文章谈到这个问题。第一篇就是前文提到的吐槽Adam最狠的 The Marginal Value of AdaptiVe Gradient Methods in Machine Learning 。文中说到,同样的一个优化问题,不同的优化算法可能会找到不同的答案,但自适应学习率的算法往往找到非常差的答案。他们通过一个特定的数据例子说明,自适应学习率算法可能会对前期出现的特征过拟合,后期才出现的特征很难纠正前期的拟合效果。

    另外一篇是 ImproVing Generalization Performance by Switching from Adam to SGD,进行了实验验证。他们CIFAR-10数据集上进行测试,Adam的收敛速度比SGD要快,但最终收敛的结果并没有SGD好。他们进一步实验发现,主要是后期Adam的学习率太低,影响了有效的收敛。他们试着对Adam的学习率的下界进行控制,发现效果好了很多。

    于是他们提出了一个用来改进Adam的方法:前期用Adam,享受Adam快速收敛的优势;后期切换到SGD,慢慢寻找最优解。这一方法以前也被研究者们用到,不过主要是根据经验来选择切换的时机和切换后的学习率。这篇文章把这一切换过程傻瓜化,给出了切换SGD的时机选择方法,以及学习率的计算方法,效果看起来也不错。

    2.2 AdaBound

    这篇文章对于Adam后期的毛病进行了分析,原因出在自适应方法训练后期不稳定的极端学习率。换句话说,就是自适应学习率训练到后期,学习率出现极端情况,更新参数时有些维度上学习率特别大,有些维度学习率特别小。
    在这里插入图片描述
    我们可以看到,当模型接近收敛时,学习率中有大量的极端值(包含许多小于 0.01 和大于 1000 的情况)。这一现象表明在实际训练中,极端学习率是实际存在的。

    在这里插入图片描述
    该算法的具体做法是对学习率进行动态裁剪,其中ClipClipClip可以将学习率限制在下界ηleta _lηl和上界ηueta_uηu之间。很容易发现,SGD和Adam分别是应用梯度裁剪的特殊情况,学习率为αalpha^*α∗的SGD可视为ηl=ηu=αeta _l=eta _u=alpha^*ηl=ηu=α∗;而Adam可视为ηl=0,ηn=eta_l=0, eta_n=inftyηl=0,ηn=∞。那么,如果使用两个关于t的函数来取代固定值作为新的上下界,其中ηl(t)eta_l(t)ηl(t)从0收敛至αalpha^*α∗,ηu(t)eta_u(t)ηu(t)从infty∞也逐渐收敛至αalpha^*α∗,那么就成功实现了从Adam到SGD的动态过程。在这一设置下,在训练早期由于上下界对学习率的影响很小,算法更加接近于 Adam;而随着时间增长裁减区间越来越收紧,模型的学习率逐渐趋于稳定,在末期更加贴近于 SGD。通过这种方式,它既可以结合自适应方法的好处,快速的初始过程,又能拥有SGD的良好最终泛化属性。

    将该方法与上面提到的将Adam转换为SGD的方法进行对比,其作者提出一种初始时使用Adam,接着在一些特定的步骤将Adam转换为SGD的方法,其受限的是是否有一个固定的转换点来区分Adam和SGD是不确定的。第二,其引入了一个额外的超参数来决定转换时间,不是很容易微调。与之相比,Adabound有两个优点,它一个连续的转换过程而不是一个强硬的转换,而且两个边界函数更加的灵活。

    以下部分是作者使用Pytorch深度学习框架,使用CIFAR-10数据,在ResNet和DensetNet两个神经网络训练,使用各优化算法,以下为对比实验的代码截图:
    在这里插入图片描述
    结论:我们看到自适应方法(AdaGrad,Adam 和AMSGrad)刚开始比非自适应学习率(SGD)有着良好的表现。但是在150epoch之后学习率消失递减,SGD开始表现良好相比自适应方法更出色。从整个表现看我们的自适应方法AdaBound和AMSBound 能够在刚开始(和AdaGrad,Adam 和AMSGrad)快速得到一个较好的表现,也能在后面的epoch比SGD更出色稳定。

    2.3 到底该用Adam还是SGD?

    所以,谈到现在,到底Adam好还是SGD好?这可能是很难一句话说清楚的事情。去看学术会议中的各种paper,用SGD的很多,Adam的也不少,还有很多偏爱AdaGrad或者AdaDelta。可能研究员把每个算法都试了一遍,哪个出来的效果好就用哪个了。

    而从这几篇怒怼Adam的paper来看,多数都构造了一些比较极端的例子来演示了Adam失效的可能性。这些例子一般过于极端,实际情况中可能未必会这样,但这提醒了我们,理解数据对于设计算法的必要性。优化算法的演变历史,都是基于对数据的某种假设而进行的优化,那么某种算法是否有效,就要看你的数据是否符合该算法的胃口了。算法固然美好,数据才是根本。另一方面,Adam之流虽然说已经简化了调参,但是并没有一劳永逸地解决问题,默认参数虽然好,但也不是放之四海而皆准。因此,在充分理解数据的基础上,依然需要根据数据特性、算法特性进行充分的调参实验,找到最优解。

    3. 优化算法的常用tricks

    1. 首先,各大算法孰优孰劣并无定论。如果是刚入门,优先考虑SGD+NesteroV Momentum或者Adam.(Standford 231n : The two recommended updates to use are either SGD+NesteroV Momentum or Adam)
    2. 选择你熟悉的算法——这样你可以更加熟练地利用你的经验进行调参。
      充分了解你的数据——如果模型是非常稀疏的,那么优先考虑自适应学习率的算法。如果在意更快的收敛,并且需要训练较深较复杂的网络时,推荐使用自适应学习率的优化方法。
    3. 根据你的需求来选择——在模型设计实验过程中,要快速验证新模型的效果,可以先用Adam进行快速实验优化;在模型上线或者结果发布前,可以用精调的SGD进行模型的极致优化。
    4. 先用小数据集进行实验——有论文研究指出,随机梯度下降算法的收敛速度和数据集的大小的关系不大。因此可以先用一个具有代表性的小数据集进行实验,测试一下最好的优化算法,并通过参数搜索来寻找最优的训练参数。
    5. 考虑不同算法的组合:先用Adam进行快速下降,而后再换到SGD进行充分的调优。切换策略可以参考本文介绍的方法。
    6. 数据集一定要充分的打散(shuffle):这样在使用自适应学习率算法的时候,可以避免某些特征集中出现,而导致的有时学习过度、有时学习不足,使得下降方向出现偏差的问题。
    7. 训练过程中持续监控训练数据和验证数据上的目标函数值以及精度或者AUC等指标的变化情况。对训练数据的监控是要保证模型进行了充分的训练——下降方向正确,且学习率足够高;对验证数据的监控是为了避免出现过拟合。
    8. 制定一个合适的学习率衰减策略。可以使用定期衰减策略,比如每过多少个epoch就衰减一次;或者利用精度或者AUC等性能指标来监控,当测试集上的指标不变或者下跌时,就降低学习率。

    学习率衰减

    在训练模型的时候,通常会遇到这种情况:我们平衡模型的训练速度和损失后选择了相对合适的学习率,但是训练集的损失下降到一定的程度后就不再下降了,最后最小值在附近摆动,不会精确地收敛,这是因为mini-batch中有噪声。遇到这种情况通常可以通过适当降低学习率来实现。但是,降低学习率又会延长训练所需的时间。学习率衰减就是一种可以平衡这两者之间矛盾的解决方案。

    几种梯度衰减的方式:

    • α=11+decay rateepochα0alpha=frac{1}{1+decay rate*epoch}alpha_0α=1+decay rateepoch1α0 (decay rate为衰减率)
    • 指数衰减:α=0.95epochα0alpha=0.95^{epoch} alpha_0α=0.95epochα0
    • α=kepoch√α0alpha=frac{k}{sqrt{epoch}}alpha_0α=epochkα0
    • 离散下降:一次衰减一半

    参考资料:

    1. 深度学习笔记
    2. L2正则=Weight Decay?并不是这样
    3. 权重衰减(weight decay)与学习率衰减(learning rate decay)
    4. 都9102年了,别再用Adam + L2 regularization了
    5. DECOUPLED WEIGHT DECAY REGULARIZATION
    6. Adam那么棒,为什么还对SGD念念不忘 (2)—— Adam的两宗罪
  • 相关阅读:
    公输盘
    电脑机器刷BIOS
    八皇后问题的实现
    安装msdn出现的问题及解决
    加密推荐书籍
    C++待解
    atan()与atan2()
    Win32/MFC/COM学习推荐书籍
    C++问题
    windows2000 sp4下载
  • 原文地址:https://www.cnblogs.com/think90/p/11515264.html
Copyright © 2020-2023  润新知