1. 程式人生 > >一文看懂梯度下降演算法的演化(含程式碼實現)

一文看懂梯度下降演算法的演化(含程式碼實現)

目錄

0 前言

0 前言

梯度下降法是目前最流行的優化演算法之一,也是目前最常用的神經網路優化方法。同時,每一個最先進的深度學習庫都包含各種演算法的實現來優化梯度下降(如caffe,keras)。然而,這些演算法經常被用作黑箱優化器,因為它們的優點和缺點很難得到實際的解釋。這篇部落格旨在介紹各種梯度下降演算法,這將幫助你使用它們。

我們先來看看梯度下降法的不同變體。然後,我們將簡要總結訓練過程中的挑戰。隨後,我們將介紹最常見的優化演算法,展示它們解決這些挑戰的機理,以及如何推導它們的更新規則。我們還將簡要介紹在並行和分散式環境中優化梯度下降的演算法和體系結構。最後,我們將考慮其他有助於優化梯度下降的策略。

梯度下降是一種通過在目標函式梯度\Delta _{\theta }J(\theta )的反方向上更新引數來最小化目標函式J(\theta )的方法.

學習速率η決定我們達到區域性最小值的步長。換句話說,我們沿著由目標函式建立的表面斜坡的方向向下直到到達一個山谷。

1 Gradient descent variants梯度下降的變體

梯度下降法有三種變體,它們的區別在於我們用多少資料來計算目標函式的梯度。根據資料量的不同,我們需要權衡引數更新的準確性和執行一次更新所需的時間。

1.1 Batch gradient descent批量梯度下降,又名 Vanilla gradient descent

用整個訓練資料集計算目標函式的梯度引數θ

由於我們需要計算整個資料集的梯度來執行一次更新,批量梯度下降可能非常緩慢

,而且需要把整個資料集放入記憶體張,資料量很大時,非常棘手。批量梯度下降也不允許我們線上更新我們的模型。

批量梯度下降的程式碼實現

for i in range(n_epochs):
    params_grad = evaluate_gradient(loss_function, data, params)
    params = params - lr * params_grad

對於預先定義的多個epoch,我們首先計算關於整個資料集的損失函式的梯度向量params_grad。注意,當前的一些深度學習庫提供了自動求導,可以有效地計算一些引數梯度。

然後,我們按照梯度的反方向更新引數,學習速率決定我們執行的更新的大小。保證了批梯度下降收斂於凸誤差曲面的全域性最小值和非凸曲面的區域性最小值。

1.2 Stochastic gradient descent 隨機梯度下降

相比之下,隨機梯度下降(SGD)對每個訓練示例x^{(i)}和標號y^{(i)}進行引數更新:

批量梯度下降對大資料集會有一些冗餘計算,因為它在每次引數更新前重新計算類似示例的梯度。SGD通過每次執行一個更新來消除這種冗餘。因此,它通常更快,也可以用來線上學習。

SGD會以較大的方差執行頻繁的更新,導致目標函式劇烈波動,如下圖所示。

當批量梯度下降收斂到引數的盆地時,SGD的波動一方面使其跳躍到新的、可能更好的區域性極小值。另一方面,這最終會使收斂複雜化到最小值,因為SGD會持續快速的更新。然而,研究表明,當我們緩慢降低學習速率時,SGD表現出與批量梯度下降相同的收斂行為,幾乎可以肯定分別收斂到非凸優化和凸優化的區域性或全域性最小值。

它的程式碼只是在訓練示例上新增一個迴圈,並計算關於每個樣本的梯度。值得注意的是,我們在每個階段都要重新打亂訓練資料。

for i in range(n_epochs):
    np.random.shuffle(data)
    for example in data:
        params_grad = evaluate_gradient(loss_function, example, params)
        params = params - lr * params_grad

1.3 Mini-batch gradient descent 小批量梯度下降

小批量批梯度下降具有以上兩者的優點,並對每一個小批量執行更新。

這中方法,a)降低了引數更新的方差,可以獲得更穩定的收斂;

                  b)可以利用最先進的深度學習庫中常見的高度優化的矩陣優化方法,這些庫中的方法對小批量樣本計算梯度非常有效。

小批量的batch_size的大小範圍在50到256之間,但是對於不同的應用場景下可以有所不同。比如當你的視訊記憶體不夠時,就把batch_size調小。如32,16

在訓練神經網路時,小批量梯度下降法是最常用的演算法,因此小批量梯度下降也成為SGD.

在程式碼中,我們不再迭代每個示例,而是迭代大小為50的小批量樣本

for i in range(n_epochs):
    np.random.shuffle(data)
    for batch in get_batches(data, batch_size=50):
        params_grad = evaluate_gradient(loss_function, batch, params) / batch_size
        params = params - lr * params_grad

2 SGD的缺點

然而,小批量梯度下降法並不能保證良好的收斂性,仍存在一些問題

  • 1. 選擇一個合適的學習速度是很困難的。學習速率太小會導致緩慢的收斂,而學習速率太大則會阻礙收斂,導致損失函式在最小值附近波動,甚至發散。
  • 2. 學習率計劃表 learning_rate_schedule  試圖在訓練過程中調整學習率,例如退火,即根據預先定義的計劃表減少學習率,或當各時期之間目標的變化低於閾值時。但是,這些排程和閾值必須提前定義,因此無法適應資料集的特徵。
  • 3. 相同的學習率適用於所有引數更新。如果我們的資料是稀疏的,並且我們的特性具有非常不同的頻率,我們可能不想將所有的特性更新到相同的程度,而是對很少發生的特性執行更大的更新。
  • 4. 會陷入無數次的區域性極小值,或鞍點。這些鞍點通常被相同誤差的平臺所包圍,這使得SGD很難逃脫,因為梯度在所有維度上都接近於零。

3 高階梯度下降優化演算法

下面,我們將概述一些深度學習社群廣泛使用的演算法,以應對上述缺點。我們不會討論在實踐中對於高維資料集計算不可行的演算法,例如二階方法,如牛頓法。

3.1 Momentum

SGD 在 ravines 的情況下容易被困住, ravines 就是曲面的一個方向比另一個方向更陡,這時 SGD 會發生震盪而遲遲不能接近極小值:

動量法是一種在相關方向上加速SGD並抑制振盪的方法,如圖3所示。它通過新增一個分數γ更新向量的過去時間步當前更新向量

其中\eta是動量引數。也就是說,當前時間步長的權值向量的變化取決於當前梯度和前一步權值的變化。

直覺上,使用動量項的基本原理是,當誤差函式表面有一個狹長的山谷時,SGD在最陡的方向下降速度特別慢。在這種情況下,梯度的方向幾乎垂直於山谷的長軸。因此,梯度運動在短軸的方向上來回擺動,只在山谷的長軸上緩慢移動。動量項幫助平均了沿短軸的振盪,同時也增加了沿長軸的貢獻。

γ通常設定為0.9. 

若設為0.5,0.9,0.99,分別表示增加速到2倍,10倍,100倍於SGD的演算法

過小的話,效果不明顯,過大的話,動量過大,可能會錯過最優解。

本質上,當使用動量時,我們可以設想把球推下山坡。球在滾下坡時積累的動量,使得變得越來越快(直到它達到最終速度如果有空氣阻力,即γ< 1)。同樣的事情也發生在我們的引數更新上:動量項在與梯度方向相同的維度上增加,而在與梯度方向不同的維度上減少更新。因此,我們得到更快的收斂和減少振盪。

for i in range(nb_epochs):
    np.random.shuffle(data)
    for batch in get_batches(data, batch_size=50):
        params_grad = evaluate_gradient(loss_function, batch, params) / batch_size
        v = momentum * v + lr * params_grad
        params = params - v

3.2 Nesterov accelerated gradient (NAG)

然而,一個球從山上滾下來,盲目地沿著山坡滾,是非常不令人滿意的。我們想要一個更聰明的球,一個知道它要去哪裡的球,這樣它就知道在山坡再次向上傾斜之前減速。

Nesterov加速梯度(NAG)[7]是一種給動量項這種先見之明的方法。我們知道,我們將使用我們的動量項\gamma \upsilon _{t-1}移動引數\theta

計算\theta - \gamma \upsilon _{t-1}因此給了我們一個近似的下一個引數.

我們現在可以有效地展望未來我們不通過計算梯度當前的引數θ的梯度而是一個近似的未來引數位置。

舉個栗子,我們設定了動量項γ值約為0.9。

藍色是 Momentum 的過程,會先計算當前的梯度(small blue vector),然後在更新後的累積梯度後會有一個大的跳躍(big blue vector)。 

而 NAG 會先在前一步的累積梯度上(brown vector)有一個大的跳躍,然後衡量一下梯度做一下修正(red vector),這種預期的更新可以避免我們走的太快。

現在,我們能夠根據誤差函式的斜率調整更新,並相應地加快SGD的速度,我們還希望根據每個單獨的引數調整更新,以根據其重要性執行更大或更小的更新。

for i in range(nb_epochs):
    np.random.shuffle(data)
    v = 0
    for batch in get_batches(data, batch_size=50):
        params_grad = evaluate_gradient(loss_function, batch, params - momentum * v) / batch_size
        v = momentum * v + lr * params_grad 
        params = params - v

3.3 Adagrad

Adagrad是一種基於梯度的優化演算法,它根據引數調整學習率,針對與頻繁出現的特徵相關的引數執行更小的更新(即低學習率),針對與不頻繁的特相關的引數執行更大的更新(即高學習率)。因此,它非常適合處理稀疏資料

Adagrad能顯著提高SGD的魯棒性,並將其用於谷歌的大型神經網路訓練,其中包括在Youtube視訊中識別貓。此外,還有研究者使用Adagrad來訓練GloVe詞嵌入,因為不常見的單詞需要比常見的更大的更新。

以前,我們對所有引數\theta使用相同的學習速率η執行一個更新。而Adagrad使用不同的學習速率在每一個時刻t為每一個引數\theta _{i}執行更新。為了簡潔起見,我們使用g_{t}來表示時間步長t的梯度。g_{t,i}是目標函式關於時間步長t和引數\theta _{i}的偏導數

SGD在每個時間步長t對每個引數\theta _{i}執行一次更新

而Adagrad更新時,基於過去的梯度修正學習速率\eta,然後會在每個時間步長t對每個引數\theta _{i}進行更新

G_{t}是一個對角矩陣,其中每個對角矩陣的元素i,i是 t 時刻引數 \theta _{i} 的梯度平方和。ϵ是一個平滑項,避免除零的情況(通常為1e-8)。

用向量化的表示為

\odot表示矩陣向量乘積

Adagrad的主要優點之一是它不需要手動調整學習速率。大多數實現都使用預設值0.01。

Adagrad的主要缺點是其在分母上的平方梯度的累加:因為每增加一項都是正的,所以在訓練過程中累積的和會不斷增加。這反過來又會導致學習速率下降,最終變得無窮小,這時演算法就不能再獲得額外的知識了。下面的演算法旨在解決這個缺陷。

eps_stable = 1e-7
for i in range(nb_epochs):
    np.random.shuffle(data)
    sqr = 0.0
    for batch in get_batches(data, batch_size=50):
        params_grad = evaluate_gradient(loss_function, batch, params) / batch_size
        sqr += param_grad **2
        div = lr * param_grad / np.sqrt(sqr + eps_stable)
        params = params - div

3.4 RMSprop

RMSprop 是由 Geoff Hinton 在他 Coursera 課程中提出的一種適應性學習率方法,至今仍未被公開發表。

RMSprop 法和 Adadelta 法幾乎同時被髮展出來。他們 解決 Adagrad 激進的學習率縮減問題。實際上,RMSprop 和我們推匯出的 Adadelta 法第一個更規則相同:

RMSprop 也將學習率除以了一個指數衰減的衰減均值。Hinton 建議設定 為 0.9,對\eta而言,0.001 是一個較好的預設值。

eps_stable = 1e-7
for i in range(nb_epochs):
    np.random.shuffle(data)
    sqr = 0.0
    for batch in get_batches(data, batch_size=50):
        params_grad = evaluate_gradient(loss_function, batch, params) / batch_size
        sqr =  gamma * sqr + (1 - gamma) * np.square(param_grad) 
        div = lr * param_grad / np.sqrt(sqr + eps_stable)
        params = params - div

3.5 Adadelta

Adadelta是Adagrad的擴充套件,它旨在解決它學習率不斷單調下降的問題。比計算之前所有梯度值的平方和,Adadelta 法僅計算在一個大小為w的時間區間內梯度值的累積和。當然,Adadelta並不是低效地儲存w個以前的平方梯度,梯度的總和被遞迴地定義為所有過去的平方梯度的衰減平均值。

t時刻的執行平均值E[g^{2}]_{t}取決於之前的平均值和當前梯度: (γ和動量法中的意義相同)

替換Adagrad的G_{t}為過去平方梯度的衰減平均值E[g^{2}]_{t}

因為分母就是梯度的均方根RMS,因此重新命名為

作者還注意到,在該更新中(在 SGD、動量法或者 Adagrad 也類似)的單位並不一致,也就是說,更新值的量綱與引數值的假設量綱並不一致。為改進這個問題,他們定義了另外一種指數衰減的衰減均值,他是基於引數更新的平方而非梯度的平方來定義的:

因此,引數更新的均方根誤差為

因為 RMS[\Delta \theta ^{2}]_{t}值未知,所以我們使用t-1時刻的引數更新的均方根來近似。將前述規則中的學習率\eta替換為RMS[\Delta \theta ^{2}]_{t-1},我們最終得到了 Adadelta 法的更新規則:

有了 Adadelta 法,我們甚至不需要預設一個預設學習率,因為它已經從我們的更新規則中被刪除了。

eps_stable = 1e-7
for i in range(nb_epochs):
    np.random.shuffle(data)
    g_sqr = 0.0
    p_sqr = 0.0
    for batch in get_batches(data, batch_size=50):
        params_grad = evaluate_gradient(loss_function, batch, params) / batch_size

        # 梯度按元素平方後做加權平均
        g_sqr = roh * g_sqr + (1 - rho) * (params_grad**2)

        params_delta = np.sqrt(p_sqr + eps_stable)/np.sqrt(g_sqr + eps_stable) * params_grad 

        # 引數按元素平方後做加權平均
        p_sqr = roh * p_sqr + (1 - rho) * (params_delta**2)

        params = params - params_delta

3.6 Adam

Adaptive Moment Estimation (Adam) 是另一種為每個引數計算自適應學習率的方法。

除了儲存類似 Adadelta 法或 RMSprop 中指數衰減的過去梯度平方均值 外,Adam 法也儲存像動量法中的指數衰減的過去梯度值均值 :

m_{t}\upsilon _{t}分別是梯度的一階矩(均值)和二階矩(方差),這也就是該方法名字的來源。因為當m_{t}\upsilon _{t}一開始被初始化為 0 向量時,Adam 的作者觀察到,該方法會有趨向 0 的偏差,尤其是在最初的幾步或是在衰減率很小(即 \beta1\beta 2 接近 1)的情況下。

因此,他們使用偏差糾正係數,來修正一階矩和二階矩的偏差:

然後,使用這些來更新引數,更新規則很我們在 Adadelta 和 RMSprop 法中看到的一樣,服從 Adam 的更新規則:

作者認為引數的預設值應設為:0.9 for\beta1,0.999 for \beta 2, and  1e-8 for ϵ. 。

經驗表明,Adam 在實踐中表現很好,和其他適應性學習演算法相比也比較不錯。

eps_stable = 1e-8
t = 1
for i in range(nb_epochs):
    np.random.shuffle(data)
    m = 0.0
    v = 0.0
    for batch in get_batches(data, batch_size=50):
        params_grad = evaluate_gradient(loss_function, batch, params) / batch_size

        # 梯度做加權平均
        m = beta1 * m + (1 - beta1) * (params_grad)
        # 梯度按元素平方後做加權平均
        v = beta2 * v + (1 - beta2) * np.square(params_delta) 
        # 為了減輕 v 和 s 被初始化為 0 在迭代初期對計算指數加權移動平均的影響,做偏差修正
        m_modify = m / (1 - beta1 ** t)
        v_modify = v / (1 - beta2 ** t)

        div = lr * v_modify / np.sqrt(m_modify + eps_stable)
        params = params - div
        t += 1

4 演算法視覺化

如下的兩個動畫給了我們關於特定優化演算法在優化過程中行為的直觀感受。

在圖 5 中,我們可以看到,在損失函式的等高線圖中,優化器的位置隨時間的變化情況。注意到,Adagrad、 Adadelta 及 RMSprop 法幾乎立刻就找到了正確前進方向並以相似的速度很快收斂。而動量法和 NAG 法,則找錯了方向,如圖所示,讓小球沿著梯度下降的方向前進。但 NAG 法能夠很快改正它的方向向最小指出前進,因為他能夠往前看並對前面的情況做出響應。

                                           圖5  損失表面輪廓上的SGD優化

圖 6 展現了各演算法在鞍點附近的表現。如上面所說,這對對於 SGD 法、動量法及 NAG 法制造了一個難題。他們很難打破對稱性帶來的壁壘,儘管最後兩者設法逃脫了鞍點。而 Adagrad 法、RMSprop 法及 Adadelta 法都能快速的沿著負斜率的方向前進。

                                     圖6 :鞍點的SGD優化

如我們所見,適應性學習率方法,也就是 Adagrad 法、Adadelta 法 、RMSprop 法及 Adam 法最適合處理上述情況,並有最好的收斂效果。

5 如何選擇優化器

如果你的輸入資料較為稀疏(sparse),那麼使用適應性學習率型別的演算法會有助於你得到好的結果。此外,使用該方法的另一好處是,你在不調參、直接使用預設值的情況下,就能得到最好的結果。

總的來說,RMSprop是Adagrad的擴充套件,它解決了學習速率急劇下降的問題。Adadelta 法於 RMSprop 法大致相同,除了前者使用了引數更新的均方根。而 Adam 法,則基於 RMSprop 法添加了偏差修正項和動量項。在我們的討論範圍中,RMSprop、Adadelta 及 Adam 法都是非常相似地演算法,在相似地情況下都能做的很好。。總的來說,Adam 也許是總體來說最好的選擇。

有趣的是,很多最新的論文,都直接使用了(不帶動量項的)Vanilla SGD 法,配合一個簡單的學習率(退火)列表,或者帶動量的Mini-batch SGD。如論文所示,這些 SGD 最終都能幫助他們找到一個最小值,但會花費遠多於上述方法的時間。並且這些方法非常依賴於魯棒的初始化值及退火列表。因此,如果你非常在你的模型能快速收斂,或是你需要訓練一個深度或複雜模型,你可能需要選擇上述的適應性模型。

6 優化 SGD 的其他手段

最後,我們將討論一些其他手段,他們可以與前述的方法搭配使用,並能進一步提升 SGD 的效果。

6.1 打亂(Shuffle)和 Curriculum Learning

總體而言,我們希望避免訓練樣本以某種特定順序傳入到我們的學習模型中,因為這會向我們的演算法引入偏差。因此,在每次迭代後,對對訓練資料集中的樣本進行打亂(shuffle),會是一個不錯的注意。

另一方面,在某些情況下,我們會需要解決難度逐步提升的問題。那麼,按照一定的順序遍歷訓練樣本,會有助於提高學習效果及加快收斂速度。這種構建特定遍歷順序的方法,叫做Curriculum Learning。

研究表明,使用的二者結合混合的表現好於單一方法。shuffle不斷地打亂資料,反而增加了學習過程的難度。

6.2 批量標準化(Batch Normalization)

我們通常設定我們引數初值的均值和方差分別為 0 和1,以幫助模型進行學習。隨著學習過程的進行,每個引數被不同程度地更新,相應地,引數的正則化特徵也隨之失去了。因此,隨著訓練網路的越來越深,訓練的速度會越來越慢,變化值也會被放大。

批量標準化 [18] 對每小批資料都重新進行標準化,並也會在操作中逆傳播(back-propgate)變化量。在模型中加入批量標準化後,我們能使用更高的學習率且不要那麼在意初始化引數。此外,批量標準化還可以看作是一種正則化手段,能夠減少(甚至去除)Dropout法的使用。

6.3 早停(Early Stopping)

在訓練過程中,你應該時刻關注模型在驗證集上的誤差情況,並且在改誤差沒有明顯改進的時候停止訓練。

6.4 梯度噪聲(Gradient Noise)

在每次梯度的更新中,向其中加入一個服從高斯分佈 N(0,\sigma_t^2)的噪聲值:

並按照如下的方式修正方差:

他們指出,這種方式能夠提升神經網路在沒有很好的初始化前提下的魯棒性,並能幫助訓練特別是深層、複雜的神經網路。他們發現,加入噪聲項之後,模型更有可能跳出並找到在深度模型中頻繁出現的區域性最小值。

本文部分翻譯自參考文獻,文中的程式碼只是幫大家理解優化演算法的內容,遷移性不強。

自知水平有限,如果有理解和翻譯的不準確的地方,歡迎指出,不勝感激  >_<

參考文獻: