深度模型中的優化——momentum|AdaGrad|RMSProp|Adam


深度模型中的優化——momentum|AdaGrad|RMSProp|Adam

在隨機梯度下降中,沿着最速下降方向可以得到最快的下降速度:xt+1=xtλf(xt)x_{t+1}=x_t-\lambda \triangledown f(x_t)。其中,λ\lambda是下降步長,也被稱爲學習率

在常見的SGD算法中,批量梯度下降(BSGD)的算法如下:(引用《深度學習》(花書)P180)

算法:隨機梯度下降(SGD)在第kk個訓練迭代的更新


Require: 學習率 ϵk\epsilon_k

Require: 初始參數 θ\theta

while 停止準則未滿足 do

​ 從訓練集中採樣mm個樣本x(1),,x(m){x^{(1)},\dots,x^{(m)}},其中x(i)x^{(i)}對應的目標爲y(i)y^{(i)}

​ 計算梯度估計: g^+1mθiL(f(x(i);θ),y(i))\hat g \leftarrow +\frac{1}{m}\triangledown_\theta\sum_i{L(f(x^{(i)};\theta), y^{(i)})}

​ 應用更新: θθϵg^\theta \leftarrow \theta - \epsilon \hat g

end while


PS:花書中把學習率記爲 ϵ\epsilon,把收斂條件記爲λ\lambda.

下降步長對梯度下降的影響

一個簡單的例子:給定目標函數 f(x)=x2f(x)=x^2,初始點x0=1x_0=1,其梯度(導數)爲2x2x,給定步長λ=1\lambda=1,根據上文的更新公式計算x1=x0λ×2x_1=x_0-\lambda \times 2,發現f(x1)=f(x0)f(x_1)=f(x_0);繼續進行下降更新,發現x2=1=x0x_2=1=x_0\dots;如此反覆,搜尋結果會反覆在[-1, 1]之間橫跳,且目標函數的值並不下降。

顯然,隨着訓練的推進,我們應當逐漸減小下降步長。保證SGD收斂的一個充分條件是:
k=1ϵk=, AND k=1ϵk2< \sum_{k=1}^\infin \epsilon_k=\infin, \ AND \ \sum_{k=1}^\infin \epsilon^2_k<\infin
對於學習率/下降步長的選擇設置,主要有以下兩種情況:

  • 設置過大,會出現劇烈的震盪,甚至完全不收斂;
  • 設置過小,學習過程非常緩慢,且容易卡在一個局部最優值。

對於BSGD,一個比較有意思的現象是當訓練的批次樣本較多時,他更容易收斂。在本文中我們只在一維上進行搜索,因此很容易出現不收斂的現象。

學習率優化算法的衡量標準

引入額外誤差(excess error) J(θ)minθJ(θ)J(\theta)-min_\theta J(\theta),即當前代價函數超出全局最小值的量。在凸問題中,SGD在kk步迭代之後的額外誤差量級爲O(1k)O(\frac{1}{\sqrt{k}}),強凸情況下是1k\frac{1}{k}

SGD學習率的提升方案

我們以f(x)=sin(x)+x2f(x)=\sin(x)+x^2爲例子,先看看它的圖像和梯度:(約定本文中所有的圖橫軸爲迭代次數、縱軸爲代價)

import numpy as np
import matplotlib.pyplot as plt


def func(x):
    return np.sin(x) + np.power(x, 2)


def grad(x):
    return np.cos(x) + 2 * x


xes = np.arange(-5, 5, 0.1)
plt.plot(xes, func(xes), label="f(x)=sin(x)+x^2")
plt.plot(xes, [0 for i in xes], '--')
plt.plot(xes, grad(xes), label="f'(x)=cos(x)+2*x")
plt.legend()
plt.show()

origin.png

線性衰減學習率

根據SGD的收斂充分條件,我們不難給定一個線性衰減的學習率α=kτ\alpha=\frac{k}{\tau},設定經過τ\tau次迭代之後,令ϵ\epsilon保持常數。既有:
ϵk=(1α)ϵ0+αϵτ \epsilon_k=(1-\alpha)\epsilon_0+\alpha\epsilon_\tau
通常,設定ϵτ\epsilon_\tauϵ0\epsilon_01%1\%左右(令τ=100\tau=100),即:ϵk=(10.99α01)ϵ0\epsilon_k=(1-0.99\mathop{\alpha}\limits_{0\to1})\epsilon_0;但ϵ0\epsilon_0的設定依舊容易帶來函數收斂過程劇烈震盪或收斂過慢的問題,因此有更多優秀的自自適學習率算法被提出,在深度學習框架中有時把這些算法稱爲優化器

利用xt+1=xtλf(xt)x_{t+1}=x_t-\lambda \triangledown f(x_t)進行定步長SDG和衰減學習率的下降:

epsilon = 1e-1  # 停止準則
lambda0 = 0.1  # 學習率(定長模式下如果給定較大的學習率可能不收斂)
x0, x1 = -150, 100
costs = []
while abs(x1 - x0) >= epsilon:
    costs.append(func(x0))
    
    temp = x1
    x1 = x0 - lambda0 * grad(x0)
    x0 = temp
print("x* = ", x0)
plt.plot(range(min(len(costs), 30)), costs[0:30], label="sgd")


epsilon = 1e-1  # 停止準則
tau, k = 100, 1
lambda0 = 1  # 學習率(可以直接給定一個較大的學習率也能保證收斂)
lambda_tau = lambda0 / tau
x0, x1 = -150, 100
costs, l0 = [], lambda0
while abs(x1 - x0) >= epsilon:
    costs.append(func(x0))

    temp = x1
    x1 = x0 - l0 * grad(x0)
    x0 = temp

    alpha = min(k/tau, 1)
    l0 = (1-alpha)*lambda0 + alpha*lambda_tau # 進行衰減
    k += 1
print("x* = ", x0)
plt.plot(range(min(len(costs), 30)), costs[0:30], label="sgd-decay")

plt.legend()
plt.show()

其函數圖像如下(可以明顯看到衰減學習率方案收斂得更快,但由於給定了大學習率,因此震盪更劇烈):

sgd.png

動量下降(momentum)

動量下降中引入了物理中慣性的概念,就像一個小球從山坡上滾下時,由於慣性的原因會直接衝過一些小坑,然後滾到山腳。在動量下降方案中,各超參數的更新規則如下:

算法:使用了動量下降方案的SGD


Require: 學習率ϵ\epsilon、動量參數α\alpha

Require: 初始參數θ\theta、初始速度vv

while 沒有達到停止準則 do

​ 從訓練集中採樣mm個樣本x(1),,x(m){x^{(1)},\dots,x^{(m)}},其中x(i)x^{(i)}對應的目標爲y(i)y^{(i)}

​ 計算梯度估計: g+1mθiL(f(x(i);θ),y(i))g \leftarrow +\frac{1}{m}\triangledown_\theta\sum_i{L(f(x^{(i)};\theta), y^{(i)})}

​ 計算速度更新: vαvϵgv \leftarrow \alpha v - \epsilon g

​ 應用更新: θθ+v\theta \leftarrow \theta + v

end while


直觀上理解,如果梯度的方向始終一致,那麼SGD就會在這個方向上不停加速,將原步長每次放大爲上一輪的1α1-\alpha倍。否則,會產生一個相反的懲罰:

momentum_fig.jpg

這樣的好處就在於我們能夠在一定情況下衝破局部最優點的陷阱,同時,面對較高的壁壘時比定步長SGD有更好的收斂性質。

在實踐中,α\alpha的取值一般爲0.5,0.9,0.990.5,0.9,0.99。利用python實現:

epsilon = 1e-1  # 停止準則
lambda0 = 0.5  # 給定與sgd相同的學習率
alpha = 0.5
v = 1 / (1-alpha)
x0, x1 = -150, 100
costs = []
while abs(x1 - x0) >= epsilon:
    costs.append(func(x0))
    
    temp = x1
    v = alpha * v - lambda0 * grad(x0)
    x1 = x0 + v
    x0 = temp
print("x* = ", x0)
plt.plot(range(min(len(costs), 50)), costs[0:50], label="momentum")

plt.legend()
plt.show()

對比圖像其實很難看到momentum在收斂性上相比於定步長SGD並沒有什麼太大的優點,甚至給定較大步長時也容易不收斂:

sgd_vs_momentum.png

但是,如果我們稍微換一個損失函數/目標函數f(x)=xsin(x3)10+x2100f(x)=\frac{x\sin(\frac{x}{3})}{10}+\frac{x^2}{100},效果就會非常明顯:(接下來的實驗都將繼續採用這個函數)

def func(x):
    return 0.1 * x * np.sin(0.3 * x) + 0.01 * x ** 2
def grad(x):
    return 0.01 * np.cos(0.3 * x) * x + 0.2 * np.sin(0.3 * x) + 0.02 * x

該新目標函數的圖像如下,可以看到其有多個局部最優點:

new_loss.png

在區間[50,50][-50,50]中測量定步長SGD和momentum方案的性能,給定α=0.9 ,v=0.1\alpha=0.9\ ,v=0.1、二者一致的學習率ϵ=0.1\epsilon=0.1

sgd_vs_momentum2.png

可以看到——在未經過細緻調參的情況下,momentum方案衝破了第一個局部最優值55.36-55.36繼續下降;但是相對的,定步長SGD就沒那麼好運了(同樣的,學習率線性衰減的SGD也會困於局部最優解)。所以,當我們提到SGD方案時,實際上往往用的是momentum及其改進方案,例如Keras框架就是這麼做的。

Nesterov動量下降

爲了改進momentum方案的額外誤差收斂率,一個比較著名的變種是Nesterov動量下降:

算法:使用了Nesterov動量下降方案的SGD(花書P184)


Require: 學習率ϵ\epsilon、動量參數α\alpha

Require: 初始參數θ\theta、初始速度vv

while 沒有達到停止準則 do

​ 從訓練集中採樣mm個樣本x(1),,x(m){x^{(1)},\dots,x^{(m)}},其中x(i)x^{(i)}對應的目標爲y(i)y^{(i)}

​ 應用臨時更新: θ^θ+αv\hat\theta \leftarrow\theta + \alpha v

​ 計算梯度估計: g+1mθ^iL(f(x(i);θ^),y(i))g \leftarrow +\frac{1}{m}\triangledown_{\hat\theta}\sum_i{L(f(x^{(i)};\hat\theta), y^{(i)})}

​ 計算速度更新: vαvϵgv \leftarrow \alpha v - \epsilon g

​ 應用更新: θθ+v\theta \leftarrow \theta + v

end while


它將額外誤差收斂率從O(1k)O(\frac{1}{k})改進到了O(1k2)O(\frac{1}{k^2});但是在SGD的情況下,它並沒有改進收斂率。

直觀上理解,Nesterov方案比momentum方案多跨了一步,即θ^θ+αv\hat\theta \leftarrow\theta + \alpha v,考慮得更多的是物理上的微分概念;在SGD的應用上,個人感覺並沒有太大的性能改進表現,只在全批量下降時有着比較優秀的收斂性能。

AdaGrad

我們注意到,動量下降雖然在一定程度上優化了線性衰減的學習率和定步長SGD方案的弱點,但同時又引入了另一個超參數α\alpha;而在實踐中,超參數是一個非常難以確定的值。

所以我們希望學習率能在訓練過程中進行自適應,即自我進行調整,以提升收斂率和降低收斂額外誤差同時又不至於引入更多的超參數,減小調參的工作量。之前曾提過“成功失敗法”,即Delta-bar-delta算法的變種,但缺點在於這類算法只有面對全批量下降時才比較有效。

而此處要介紹的AdaGrad算法,能夠獨立地適應所有模型參數的學習率,縮放每個參數反比於其所有梯度歷史平方值總和的平方根。這麼說可能過於拗口,我們直接看算法:

算法:AdaGrad算法(花書P188)


Require: 全局學習率ϵ\epsilon

Require: 初始參數θ\theta

Require: 小常數δ=106\delta=10^{-6}(防止進行除法時的值溢出)

初始化累積變量r=0r=0

while 沒有達到停止準則 do

​ 從訓練集中採樣mm個樣本x(1),,x(m){x^{(1)},\dots,x^{(m)}},其中x(i)x^{(i)}對應的目標爲y(i)y^{(i)}

​ 計算梯度估計: g1mθiL(f(x(i);θ),y(i))g \leftarrow \frac{1}{m}\triangledown_{\theta}\sum_i{L(f(x^{(i)};\theta), y^{(i)})}

​ 累積平方梯度: rr+ggr \leftarrow r + g \bigodot g

​ 計算參數更新: θ=ϵδ+rg\triangle\theta = - \frac{\epsilon}{\sqrt{\delta+r}}\bigodot g

​ 應用更新: θθ+θ\theta \leftarrow \theta + \triangle\theta

end while


花書中對AdaGrad的分析較少,但作爲自適應學習率算法的入門算法,我覺得有必要進行一些分析:

  • 和常見的優化算法相比,AdaGrad的策略也是採取先快後慢的策略:隨着算法不斷迭代,rr會越來越大,整體的學習率越來越小。
  • 在凸優化問題中,由於函數總能收斂到一個最小值且AdaGrad對梯度懸崖有着很強的抑制能力,因此表現得相當不錯,較少出現不收斂的情況。
  • 較爲直觀地說,AdaGrad應用於凸優化問題時,能夠給定一個巨大的學習率參數的同時保證收斂,利於加快解決優化問題時的搜尋速度。但其面對梯度壁壘時應對能力較弱,有可能被困於某個具備最優解。
epsilon = 1e-1  # 停止準則
lambda0 = 100  # 學習率,AdaGrad往往需要一個極大的初始學習率
r = 0
x0, x1 = -50, -40
costs = []
while abs(x1 - x0) >= epsilon:
    costs.append(func(x0))
    g = grad(x0)

    temp = x1
    r =  r + g ** 2
    x1 = x0 - lambda0 / np.sqrt(1e-6 + r) * g
    x0 = temp
print("x* = ", x0)
plt.plot(range(min(len(costs), 50)), costs[0:50] label="adagrad")

得益於AdaGrad對梯度爆炸有着很好的抑制作用,所以實際上你可以給定一個很大的初始學習率,它在自我迭代的過程中也總能保證收斂。

adagrad.png

值得一說的是,AdaGrad在深度學習模型上表現得不錯,比如採用64x64x1的全連接神經網絡在波士頓房價迴歸問題上的表現如下:

sgd_adagrad_boston.png

RMSProp

在上面的圖像中,AdaGrad在非凸情況下表現得並不好,收斂時震盪還是比較劇烈的。同時,如果你把上文中AdaGrad的初始學習率調小,你會發現AdaGrad會跟SGD一樣困於某個局部最優值。造成AdaGrad面對梯度壁壘無能爲力的可能性有很多,但最大的可能是搜索在到達一個局部窪地時學習率就收縮得太小了,因此導致搜尋的腳步深陷泥沼。

因此RMSProp改變梯度累積值rr爲指數加權的移動平均值——拋棄過於遙遠的歷史,使其能夠保證一定的躍出窪地泥沼的能力;但相對的,也會削弱其收斂率,在樣本量較小時甚至完全不收斂。簡單來說,就是把 rr+ggr \leftarrow r + g \bigodot g 改爲 rρr+(1ρ)ggr \leftarrow \rho r + (1-\rho)g \bigodot g,其中ρ\rho爲衰減速率。

直觀上理解,RSMProp的設計思路是這樣的:梯度波動比較大的函數,它們的方差一定是非常大的,因此直接用梯度除以其二階矩的開方,從而抑制了搜索時的擺動幅度。

算法:RMSProp算法(花書P188)


Require: 全局學習率ϵ\epsilon、衰減速率ρ\rho

Require: 初始參數θ\theta

Require: 小常數δ=106\delta=10^{-6}(防止進行除法時的值溢出)

初始化累積變量r=0r=0

while 沒有達到停止準則 do

​ 從訓練集中採樣mm個樣本x(1),,x(m){x^{(1)},\dots,x^{(m)}},其中x(i)x^{(i)}對應的目標爲y(i)y^{(i)}

​ 計算梯度估計: g1mθiL(f(x(i);θ),y(i))g \leftarrow \frac{1}{m}\triangledown_{\theta}\sum_i{L(f(x^{(i)};\theta), y^{(i)})}

​ 累積平方梯度: rρr+(1ρ)ggr \leftarrow \rho r + (1-\rho)g \bigodot g

​ 計算參數更新: θ=ϵδ+rg\triangle\theta = - \frac{\epsilon}{\sqrt{\delta+r}}\bigodot g

​ 應用更新: θθ+θ\theta \leftarrow \theta + \triangle\theta

end while


epsilon = 1e-1  # 停止準則
lambda0 = 10  # 學習率,給定一個相比於AdaGrad的學習率,其也能快速收斂和突破梯度壁壘
r, rho = 0, 0.5
x0, x1 = -50, -40

costs = []
while len(costs) < 1000 and abs(x1 - x0) >= epsilon:
    costs.append(func(x0))
    g = grad(x0)

    temp = x1
    r = rho * r + (1-rho)*g ** 2
    x1 = x0 - lambda0 / np.sqrt(1e-6 + r) * g
    x0 = temp

print("x* = ", x0)

plt.plot(range(min(len(costs), 100)), costs[0:100], label="RMSProp")

plt.legend()
plt.show()

運行結果如下:

rmsprop.png

有時候,還會爲其加上Nesterov動量,使得其在某些問題上具有更好的性質:

算法:使用Nesterov動量的RMSProp算法(花書P189)


Require: 全局學習率ϵ\epsilon、衰減速率ρ\rho、動量係數α\alpha

Require: 初始參數θ\theta、初始速度vv

Require: 小常數δ=106\delta=10^{-6}(防止進行除法時的值溢出)

初始化累積變量r=0r=0

while 沒有達到停止準則 do

​ 從訓練集中採樣mm個樣本x(1),,x(m){x^{(1)},\dots,x^{(m)}},其中x(i)x^{(i)}對應的目標爲y(i)y^{(i)}

​ 計算臨時更新: θ^θ+αv\hat \theta \leftarrow \theta + \alpha v

​ 計算梯度估計: g1mθ^iL(f(x(i);θ^),y(i))g \leftarrow \frac{1}{m}\triangledown_{\hat\theta}\sum_i{L(f(x^{(i)};\hat \theta), y^{(i)})}

​ 累積平方梯度: rρr+(1ρ)ggr \leftarrow \rho r + (1-\rho)g \bigodot g

​ 計算參數更新: v=αvϵδ+rgv = \alpha v- \frac{\epsilon}{\sqrt{\delta+r}}\bigodot g

​ 應用更新: θθ+v\theta \leftarrow \theta + v

end while


我平常在應用時,並沒有覺得二者有什麼特別大的差別,因此老老實實用RMSProp就行了,還能少計算一些參數和引入超參數。可能對於一些較爲瘋狂的調參俠而言加了Nesterov,給了他們更多的微調空間。

Adam

Adam的詞源來自adaptive moments,顯然這又是一種動量方案和RMSProp的變種算法。從個人經驗上來看,Adam往往更加地Robust,算是我用得比較多的一種優化方案。

算法:Adam算法(花書P189)


Require: 全局學習率ϵ=0.001\epsilon=0.001

Require: 矩估計的指數衰減速率ρ1,ρ2[0,1)]\rho_1,\rho_2\in[0,1)](建議默認0.9和0.9999)

Require: 初始參數θ\theta

Require: 小常數δ=108\delta=10^{-8}(防止進行除法時的值溢出)

初始化時間序列t=0t=0、一階和二階矩變量s=0,r=0s=0,r=0

while 沒有達到停止準則 do

tt+1t \leftarrow t +1

​ 從訓練集中採樣mm個樣本x(1),,x(m){x^{(1)},\dots,x^{(m)}},其中x(i)x^{(i)}對應的目標爲y(i)y^{(i)}

​ 計算梯度估計: g1mθiL(f(x(i);θ),y(i))g \leftarrow \frac{1}{m}\triangledown_{\theta}\sum_i{L(f(x^{(i)};\theta), y^{(i)})}

​ 更新一階矩估計: sρ1s+(1ρ1)gs \leftarrow \rho_1s+(1-\rho_1)g

​ 更新二階矩估計: rρ2r+(1ρ2)ggr \leftarrow \rho_2r + (1-\rho_2)g \bigodot g

​ 修正一階矩偏差: s^s1ρ1t\hat s \leftarrow \frac{s}{1-\rho^t_1}

​ 修正二階矩偏差: r^r1ρ2t\hat r \leftarrow \frac{r}{1-\rho^t_2}

​ 計算參數更新: θ=ϵs^r^+δ\triangle\theta = -\epsilon \frac{\hat s}{\sqrt{\hat r}+\delta}

​ 應用更新: θθ+θ\theta \leftarrow \theta + \triangle\theta

end while


觀察這個算法,一階矩和二階矩可以簡單理解成均值和方差,由於二者初始值都是00,因此需要在剛開始時進行放大修正防止其向00偏置。最後計算參數更新時,實際上用了一階矩和二階矩進行了估算。

矩估計可以看我的另外一篇文章:數理知識:參數估計——點估計、區間估計及置信區間

在吳恩達的公開課中,他給出他對Adam的理解,他認爲Adam是momentum和RMSProp的一種結合:若把一個優化問題看成超空間上的谷地,搜索過程會在某個方向上來回搖擺,我們把這個方向記爲縱軸;相反的,直達谷底的方向即爲橫軸。我們要做的就是抑制縱軸上的移動距離、加大在橫軸上的移動距離。Adam和momentum對梯度的均值進行估計,使得縱軸上的擺動幅度減小;而對二階矩即方差的抑制,可以進一步減小搖擺,相對的就是加大了橫向搜索距離。

常用方案的對比總結

這裏再快速回顧一下以上各種算法的特點。

算法名 引入新的超參數 容易困於窪地 初始學習率/收斂率 Cheat Sheet
線性衰減學習率 α,τ\alpha,\tau 可以較大 ϵk=(1α)ϵ0+αϵτ\epsilon_k=(1-\alpha)\epsilon_0+\alpha\epsilon_{\tau}
momentum α,v\alpha,v 較小 vαvϵgθθ+vv \leftarrow \alpha v - \epsilon g \\ \theta \leftarrow \theta + v
Nesterov α,v\alpha,v 較小 g^grad(θ+αv)\hat g \leftarrow grad(\theta + \alpha v)
AdaGrad / rr+ggθθϵδ+rgr \leftarrow r + g \bigodot g \\ \theta \leftarrow \theta - \frac{\epsilon}{\delta + \sqrt{r}} \bigodot g
RMSProp ρ\rho 較大 rρr+(1ρ)ggr \leftarrow \rho r + (1-\rho) g \bigodot g
Adam ρ1,ρ2\rho_1,\rho_2 sρ1s+(1ρ1)grρ1r+(1ρ1)ggx^x1ρxtθθϵs^δ+r^s \leftarrow \rho_1 s + (1-\rho_1)g \\ r \leftarrow \rho_1 r + (1-\rho_1)g\bigodot g \\ \hat x \leftarrow \frac{x}{1-\rho^t_x} \\ \theta \leftarrow \theta - \epsilon \frac{\hat s}{\delta + \sqrt{\hat r}}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章