文章目錄
深度模型中的優化——momentum|AdaGrad|RMSProp|Adam
在隨機梯度下降中,沿着最速下降方向可以得到最快的下降速度:。其中,是下降步長,也被稱爲學習率。
在常見的SGD算法中,批量梯度下降(BSGD)的算法如下:(引用《深度學習》(花書)P180)
算法:隨機梯度下降(SGD)在第個訓練迭代的更新
Require: 學習率
Require: 初始參數
while 停止準則未滿足 do
從訓練集中採樣個樣本,其中對應的目標爲
計算梯度估計:
應用更新:
end while
PS:花書中把學習率記爲 ,把收斂條件記爲.
下降步長對梯度下降的影響
一個簡單的例子:給定目標函數 ,初始點,其梯度(導數)爲,給定步長,根據上文的更新公式計算,發現;繼續進行下降更新,發現;如此反覆,搜尋結果會反覆在[-1, 1]之間橫跳,且目標函數的值並不下降。
顯然,隨着訓練的推進,我們應當逐漸減小下降步長。保證SGD收斂的一個充分條件是:
對於學習率/下降步長的選擇設置,主要有以下兩種情況:
- 設置過大,會出現劇烈的震盪,甚至完全不收斂;
- 設置過小,學習過程非常緩慢,且容易卡在一個局部最優值。
對於BSGD,一個比較有意思的現象是當訓練的批次樣本較多時,他更容易收斂。在本文中我們只在一維上進行搜索,因此很容易出現不收斂的現象。
學習率優化算法的衡量標準
引入額外誤差(excess error) ,即當前代價函數超出全局最小值的量。在凸問題中,SGD在步迭代之後的額外誤差量級爲,強凸情況下是。
SGD學習率的提升方案
我們以爲例子,先看看它的圖像和梯度:(約定本文中所有的圖橫軸爲迭代次數、縱軸爲代價)
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()
線性衰減學習率
根據SGD的收斂充分條件,我們不難給定一個線性衰減的學習率,設定經過次迭代之後,令保持常數。既有:
通常,設定爲的左右(令),即:;但的設定依舊容易帶來函數收斂過程劇烈震盪或收斂過慢的問題,因此有更多優秀的自自適學習率算法被提出,在深度學習框架中有時把這些算法稱爲優化器。
利用進行定步長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()
其函數圖像如下(可以明顯看到衰減學習率方案收斂得更快,但由於給定了大學習率,因此震盪更劇烈):
動量下降(momentum)
動量下降中引入了物理中慣性的概念,就像一個小球從山坡上滾下時,由於慣性的原因會直接衝過一些小坑,然後滾到山腳。在動量下降方案中,各超參數的更新規則如下:
算法:使用了動量下降方案的SGD
Require: 學習率、動量參數
Require: 初始參數、初始速度
while 沒有達到停止準則 do
從訓練集中採樣個樣本,其中對應的目標爲
計算梯度估計:
計算速度更新:
應用更新:
end while
直觀上理解,如果梯度的方向始終一致,那麼SGD就會在這個方向上不停加速,將原步長每次放大爲上一輪的倍。否則,會產生一個相反的懲罰:
這樣的好處就在於我們能夠在一定情況下衝破局部最優點的陷阱,同時,面對較高的壁壘時比定步長SGD有更好的收斂性質。
在實踐中,的取值一般爲。利用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並沒有什麼太大的優點,甚至給定較大步長時也容易不收斂:
但是,如果我們稍微換一個損失函數/目標函數,效果就會非常明顯:(接下來的實驗都將繼續採用這個函數)
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
該新目標函數的圖像如下,可以看到其有多個局部最優點:
在區間中測量定步長SGD和momentum方案的性能,給定、二者一致的學習率:
可以看到——在未經過細緻調參的情況下,momentum方案衝破了第一個局部最優值繼續下降;但是相對的,定步長SGD就沒那麼好運了(同樣的,學習率線性衰減的SGD也會困於局部最優解)。所以,當我們提到SGD方案時,實際上往往用的是momentum及其改進方案,例如Keras框架就是這麼做的。
Nesterov動量下降
爲了改進momentum方案的額外誤差收斂率,一個比較著名的變種是Nesterov動量下降:
算法:使用了Nesterov動量下降方案的SGD(花書P184)
Require: 學習率、動量參數
Require: 初始參數、初始速度
while 沒有達到停止準則 do
從訓練集中採樣個樣本,其中對應的目標爲
應用臨時更新:
計算梯度估計:
計算速度更新:
應用更新:
end while
它將額外誤差收斂率從改進到了;但是在SGD的情況下,它並沒有改進收斂率。
直觀上理解,Nesterov方案比momentum方案多跨了一步,即,考慮得更多的是物理上的微分概念;在SGD的應用上,個人感覺並沒有太大的性能改進表現,只在全批量下降時有着比較優秀的收斂性能。
AdaGrad
我們注意到,動量下降雖然在一定程度上優化了線性衰減的學習率和定步長SGD方案的弱點,但同時又引入了另一個超參數;而在實踐中,超參數是一個非常難以確定的值。
所以我們希望學習率能在訓練過程中進行自適應,即自我進行調整,以提升收斂率和降低收斂額外誤差同時又不至於引入更多的超參數,減小調參的工作量。之前曾提過“成功失敗法”,即Delta-bar-delta算法的變種,但缺點在於這類算法只有面對全批量下降時才比較有效。
而此處要介紹的AdaGrad算法,能夠獨立地適應所有模型參數的學習率,縮放每個參數反比於其所有梯度歷史平方值總和的平方根。這麼說可能過於拗口,我們直接看算法:
算法:AdaGrad算法(花書P188)
Require: 全局學習率
Require: 初始參數
Require: 小常數(防止進行除法時的值溢出)
初始化累積變量
while 沒有達到停止準則 do
從訓練集中採樣個樣本,其中對應的目標爲
計算梯度估計:
累積平方梯度:
計算參數更新:
應用更新:
end while
花書中對AdaGrad的分析較少,但作爲自適應學習率算法的入門算法,我覺得有必要進行一些分析:
- 和常見的優化算法相比,AdaGrad的策略也是採取先快後慢的策略:隨着算法不斷迭代,會越來越大,整體的學習率越來越小。
- 在凸優化問題中,由於函數總能收斂到一個最小值且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在深度學習模型上表現得不錯,比如採用64x64x1的全連接神經網絡在波士頓房價迴歸問題上的表現如下:
RMSProp
在上面的圖像中,AdaGrad在非凸情況下表現得並不好,收斂時震盪還是比較劇烈的。同時,如果你把上文中AdaGrad的初始學習率調小,你會發現AdaGrad會跟SGD一樣困於某個局部最優值。造成AdaGrad面對梯度壁壘無能爲力的可能性有很多,但最大的可能是搜索在到達一個局部窪地時學習率就收縮得太小了,因此導致搜尋的腳步深陷泥沼。
因此RMSProp改變梯度累積值爲指數加權的移動平均值——拋棄過於遙遠的歷史,使其能夠保證一定的躍出窪地泥沼的能力;但相對的,也會削弱其收斂率,在樣本量較小時甚至完全不收斂。簡單來說,就是把 改爲 ,其中爲衰減速率。
直觀上理解,RSMProp的設計思路是這樣的:梯度波動比較大的函數,它們的方差一定是非常大的,因此直接用梯度除以其二階矩的開方,從而抑制了搜索時的擺動幅度。
算法:RMSProp算法(花書P188)
Require: 全局學習率、衰減速率
Require: 初始參數
Require: 小常數(防止進行除法時的值溢出)
初始化累積變量
while 沒有達到停止準則 do
從訓練集中採樣個樣本,其中對應的目標爲
計算梯度估計:
累積平方梯度:
計算參數更新:
應用更新:
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()
運行結果如下:
有時候,還會爲其加上Nesterov動量,使得其在某些問題上具有更好的性質:
算法:使用Nesterov動量的RMSProp算法(花書P189)
Require: 全局學習率、衰減速率、動量係數
Require: 初始參數、初始速度
Require: 小常數(防止進行除法時的值溢出)
初始化累積變量
while 沒有達到停止準則 do
從訓練集中採樣個樣本,其中對應的目標爲
計算臨時更新:
計算梯度估計:
累積平方梯度:
計算參數更新:
應用更新:
end while
我平常在應用時,並沒有覺得二者有什麼特別大的差別,因此老老實實用RMSProp就行了,還能少計算一些參數和引入超參數。可能對於一些較爲瘋狂的調參俠而言加了Nesterov,給了他們更多的微調空間。
Adam
Adam的詞源來自adaptive moments,顯然這又是一種動量方案和RMSProp的變種算法。從個人經驗上來看,Adam往往更加地Robust,算是我用得比較多的一種優化方案。
算法:Adam算法(花書P189)
Require: 全局學習率
Require: 矩估計的指數衰減速率(建議默認0.9和0.9999)
Require: 初始參數
Require: 小常數(防止進行除法時的值溢出)
初始化時間序列、一階和二階矩變量
while 沒有達到停止準則 do
從訓練集中採樣個樣本,其中對應的目標爲
計算梯度估計:
更新一階矩估計:
更新二階矩估計:
修正一階矩偏差:
修正二階矩偏差:
計算參數更新:
應用更新:
end while
觀察這個算法,一階矩和二階矩可以簡單理解成均值和方差,由於二者初始值都是,因此需要在剛開始時進行放大修正防止其向偏置。最後計算參數更新時,實際上用了一階矩和二階矩進行了估算。
矩估計可以看我的另外一篇文章:數理知識:參數估計——點估計、區間估計及置信區間
在吳恩達的公開課中,他給出他對Adam的理解,他認爲Adam是momentum和RMSProp的一種結合:若把一個優化問題看成超空間上的谷地,搜索過程會在某個方向上來回搖擺,我們把這個方向記爲縱軸;相反的,直達谷底的方向即爲橫軸。我們要做的就是抑制縱軸上的移動距離、加大在橫軸上的移動距離。Adam和momentum對梯度的均值進行估計,使得縱軸上的擺動幅度減小;而對二階矩即方差的抑制,可以進一步減小搖擺,相對的就是加大了橫向搜索距離。
常用方案的對比總結
這裏再快速回顧一下以上各種算法的特點。
算法名 | 引入新的超參數 | 容易困於窪地 | 初始學習率/收斂率 | Cheat Sheet |
---|---|---|---|---|
線性衰減學習率 | 是 | 可以較大 | ||
momentum | 否 | 較小 | ||
Nesterov | 否 | 較小 | ||
AdaGrad | / | 是 | 大 | |
RMSProp | 否 | 較大 | ||
Adam | 否 | 大 |