優化方法總結以及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=\bigtriangledown 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=αgt\eta_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=\beta m_{t-1}+(1-\beta)g_tmt=βmt1+(1β)gt
一階動量指的是各個時刻梯度的指數加權平均,約等於11β1\frac{1}{1-\beta_1}1β11個歷史時刻的梯度向量和的平均值,也就是t時刻的下降方向,不僅由當前點的梯度方向決定,還由此前的累積的梯度來決定,β\betaβ的經驗值一般為0.9,也就是意味着下降方向主要是此前累積的下降方向,並略微偏向當前時刻的下降方向。並利用當前batch微調最終的更新方向。如果當前梯度方向與歷史梯度一致,會增強該方向的梯度。如果不一致,能夠減少更新。
優點:

  • 增加了穩定性;
  • 收斂速度更快;
  • 還有一定擺脫局部最優的能力。

1.2.1 理解指數加權平均

使得β=0.9\beta=0.9β=0.9
m100=0.9m99+0.1θ100m_{100}=0.9m_{99}+0.1\theta_{100}m100=0.9m99+0.1θ100
m99=0.9m98+0.1θ99m_{99}=0.9m_{98}+0.1\theta_{99}m99=0.9m98+0.1θ99
m98=0.9m97+0.1θ98m_{98}=0.9m_{97}+0.1\theta_{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\theta_{100}+0.1*0.9\theta_{99}+0.1*(0.9)^2\theta_{98}+0.1*(0.9)^3\theta_{97}+0.1*(0.9)^4\theta_{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,約等於1e\frac{1}{e}e1的值。也就是10天之后曲線的高度下降到13\frac{1}{3}31,相當於只關注了過去10天的數據,因為10天后,權重下降到不到當日權重的三分之一。如果β=0.98\beta=0.98β=0.98,那么0.98500.98^{50}0.9850大約等於1e\frac{1}{e}e1,可以看作平均了50天的數據,由此得到公式平均了大約11β\frac{1}{1-\beta}1β1的數據。

好處:占用極少的內存,每次把最新的公式帶入不斷覆蓋就可以了。雖然不是最精確的,如果直接平均過去天的數據往往會得到更好的估計,但是需要保存最近的溫度數據必須占用更多的內存,執行更加復雜,計算成本也更高。

1.2.2 偏差修正

計算移動平均數時,初始化m0=0,m1=0.9m0+0.1θ1m_0=0,m_1=0.9m_0+0.1\theta_1m0=0,m1=0.9m0+0.1θ1m1=0.1θ1m_1=0.1\theta_1m1=0.1θ1,顯然第一天的值會小很多,估計不准確;而且m2=0.9m1+0.1θ2m_2=0.9m_1+0.1\theta_2m2=0.9m1+0.1θ2,如果帶入m1m_1m1然后相乘得到m2=0.90.1θ1+0.1θ2m_2=0.9*0.1\theta_1+0.1\theta_2m2=0.90.1θ1+0.1θ2m2m_2m2要遠小於θ1\theta_1θ1θ2\theta_2θ2,所以m2m_2m2不能很好的估計出前兩天的數據,而當t足夠大時,mtˆ=mt\hat{m_t}=m_tmt^=mt
偏差修正能夠改正這個問題,特別是在初期。m1/(1βt)=m1/0.1m_1/(1-\beta^t)=m_1/0.1m1/(1βt)=m1/0.1后,m1=θ1m_1=\theta_1m1=θ1就去除了偏差。而隨着t的增加,βt\beta^tβt接近於0,所以當t很大的時候,偏差修正幾乎沒有作用。在計算指數加權平均數的大部分時候,都不在乎執行偏差修正,因為大部分人寧願熬過初始時期,拿到具有偏差的估測,然后繼續計算下去。如果關心初始時期的偏差,在剛開始計算指數加權移動平均數的時候,偏差修正能幫助在早期獲取更好的估測。

m˜t=mt1βt1V˜t=Vt1βt2\tilde{m}_{t}=\frac{m_{t}}{1-\beta_1^t}\\\tilde{V}_{t}=\frac{V_{t}}{1-\beta_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=\nabla 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=\beta_2 *V_{t-1} + (1- \beta_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}=\beta_1m_{t-1}+(1-\beta_1)g_t \\V_{t}=\beta_2V_{t-1}+(1-\beta_2)g_{t}^{2} \\\alpha_t=\alpha*\sqrt{1-\beta^t_2}/(1-\beta^t_1) \\w_{t+1}=w_{t}-\alpha_t\frac{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α很重要,也經常需要調試。β1\beta_1β1β2\beta_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=1\eta_t=1ηt=1:
θtθt1αβ1mt1+(1β1)(ft+λθt1)Vtˆ√+ϵ\theta_t \rightarrow \theta_{t-1}-\alpha\frac{\beta_1m_{t-1}+(1-\beta_1)(\bigtriangledown f_t+\lambda \theta_{t-1})}{\sqrt{\hat{V_t}}+\epsilon}θtθt1αVt^+ϵβ1mt1+(1β1)(ft+λθt1)

可以看出,分子右上角的λθt1\lambda \theta_{t-1}λθt1向量各個元素被分母的Vtˆ−−√\sqrt{\hat{V_t}}Vt^項調整了。梯度快速變化的方向上,Vtˆ−−√\sqrt{\hat{V_t}}Vt^有更大的值,而使得調整后的λθt1Vtˆ√\frac{\lambda\theta_{t-1}}{\sqrt{\hat{V_t}}}Vt^λθt1更小,在這個方向上參數θ\thetaθ被正則化的更少。這顯然是不合理的,L2 regularization和weight decay都應該是各向同性的。所以作者提出以綠色的方式來在Adam中正確的引入weight decay的方式,稱作AdamW,也就是不讓λθt1\lambda \theta_{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}(\theta)=f_t(\theta)+\frac{\lambda'}{2}||\theta||^2_2ftreg(θ)=ft(θ)+2λθ22

    θt+1θtαfregt(θt)=θtαft(θt)αλθt\theta_{t+1} \rightarrow \theta_t-\alpha\bigtriangledown f_t^{reg}(\theta_t)=\theta_t-\alpha\bigtriangledown f_t(\theta_t)-\alpha\lambda'\theta_tθt+1θtαftreg(θt)=θtαft(θt)αλθt

  • weight decay
    是在每次更新的梯度基礎上減去一個梯度:
    θt+1(1λ)θtαft(θt)\theta_{t+1} \rightarrow (1-\lambda) \theta_t-\alpha\bigtriangledown f_t(\theta_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}=\beta_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}=\beta_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}=\beta_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}=\beta_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(\beta_2*V_{t-1}+(1-\beta_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可以將學習率限制在下界ηl\eta _lηl和上界ηu\eta_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α0\alpha=\frac{1}{1+decay\ rate*epoch}\alpha_0α=1+decay rateepoch1α0 (decay rate為衰減率)
  • 指數衰減:α=0.95epochα0\alpha=0.95^{epoch} \alpha_0α=0.95epochα0
  • α=kepoch√α0\alpha=\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的兩宗罪


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM