幾種優化方法總結---Momentum,AdaGrad,RMSProp,Adam

SGD

SGD的全稱是stochastic gradient descent,隨機梯度下降,是相對batch gradient descent提出來的。

batch gradient descent:將所有訓練樣本全部投入計算dw,db。好處:這種計算方式求出來的dw和db是最真實的。壞處:如果訓練樣本特別大,比如100,000個樣本,這樣會導致很長時間才能計算出一次倒數然後進行一次梯度下降,導致訓練特別緩慢。另外計算機的內存也無法存儲這麼大的矩陣進行計算。

stochastic gradient descent:用每一個訓練樣本計算dw和db。好處:一個樣本計算導數非常快,可以很快的進行一次梯度下降。壞處:一個樣本計算的導數與真實導數相差非常大,可能會導致在最小值附近徘徊而無法接近最小值,另外由於只用一個樣本計算導數,導致遍歷所有樣本需要進行一個很大的循環,從整體上而言失去了矩陣計算帶來的運算加速。

所以可以使用一種折中的方法,mini-batch gradient descent,現在提到SGD一般指的是這種方法。mini batch一般選擇16,32,64等,根據計算機內存決定。這樣計算一次導數比較快,所以可以比較快的進行一次梯度下降,而且也有矩陣計算帶來的運算加速。

SGD公式表示

\Theta_t = \Theta_t - \eta *d\Theta_{t-1}

\Theta表示所有的參數,t代表第t次梯度下降。

tensorflow接口調用

tf.train.GradientDescentOptimizer(learning_rate).minimize(loss)

SGD優化效率不高

以下圖爲例,如果使用SGD優化方式,初始點在紅色點出,優化過程會出現圖中的zigzag的線路,以非常迂迴的方式到達最優點。

爲何會出現上圖這樣的曲線?

對上圖而言,假設橫軸是w1,縱軸是w2,那麼這個圖形如果從立體上來想象,應該是橫軸的梯度比較平滑開口很大,縱軸的梯度比較陡峭開口很小。那麼dw2比較大,dw1會比較小。所以在梯度更新的時候縱軸方向梯度更新會很大,橫軸方向梯度變化會比較小,這樣很可能w2因爲步子大跳到了另一個方向,而w1因爲步子小而正常的向前邁進了一小步。所以就會形成上圖中的折線。

 

SGD+Momentum

Momentum是動量,通過指數加權平均來累積之前的動量,從而給下降的過程來帶一點衝力。

公式表達

v_t = \beta *v_{t-1} +(1-\beta )*d\Theta_t

\Theta_t = \Theta_t - \eta*v_t

v_t就是通過指數加權平均的方式累積的動量,\beta是一個超參數,一般使用0.9。

其實還有很多其他的表達方式,比如tensorflow源碼中的實現方式:

  ```
  accumulation = momentum * accumulation + gradient
  variable -= learning_rate * accumulation
  ```

區別就在於gradient之前是否要乘以(1-\beta)。

另外還有一種表示方法,比如keras和cs231n都是使用這一種方式

# Momentum update
v = mu * v - learning_rate * dx # integrate velocity
x += v # integrate position

tensorflow接口調用

tf.train.MomentumOptimizer(learning_rate, momentum).minimize(loss)

爲何加上Momentum會使SGD加速?

還是以下面這張圖片來說明

如果\beta設置爲0.9,按照指數加權平均的意義,大概是平均了10次的導數值,對於dw1因爲每次導數都是同一個方向,所以加權平均後變化不大,但是對於dw2的方向是一直在變化,一次向上一次向下,而如果取了之前的10次數據進行了平均,正負數抵消部分,會讓dw2變化的波動變小。所以可能會沿着黃色的曲線趨近最小值,減少了大幅度的波動而加速了優化過程。

 

Nesterov

Nesterov是對標準動量的方式加了一個修正因子,因爲用指數加權平均求出v_t後,梯度下降是針對這個v_t進行的,所以修正的方式就是將這個v_t代入函數中計算,然後用這個新的結果來對下一次的\Theta求導。

公式表達

就是相對之前的Momentum的公式稍作了改動

表達1

x_ahead = x + mu * v_prev
# evaluate dx_ahead (the gradient at x_ahead instead of at x)
v = mu * v_prev - learning_rate * dx_ahead
x += v

差別就在於dx與dx_ahead的差別,這就是修正的方式。但是這裏有個問題,如果我們更新了x_head,要對它求導數,就需要將函數用x_head代入進行前向計算,然後再反向求導,這樣導致計算過程多了一倍。所以一般代碼實現中並不是這樣操作的,而是使用如下的公式:

表達2

v_prev = v # back this up
v = mu * v_prev - learning_rate * dx # velocity update stays the same
x += -mu * v_prev + (1 + mu) * v # position update changes form

將公式整理一下可以得到下面的式子:

表達3

x += mu * mu * v_prev - learning_rate * (1 + mu) * dx

這樣就避免了對dx_head求導,但是爲何可以從表達1推導出表達3呢?可以參考下面的過程

已知:

\hat{x}_t = x_t + \mu * v_t (1)

v_{t+1}= \mu * v_t- \alpha * d\hat{x}_t (2)

x_{t+1} = x_t + v_{t+1} (3)

由(1)得到==>\hat{x}_{t+1} = x_{t+1} + \mu * v_{t+1}

代入(3)得到==>\hat{x}_{t+1} = x_t+v_{t+1}+ \mu * v_{t+1}

代入(1)和(3)==>\hat{x}_{t+1} = \hat{x}_t - \mu * v_t+(1+\mu) * (\mu * v_t- \alpha * d\hat{x}_t)

==>\hat{x}_{t+1} = \hat{x}_t + \mu^{2} * v_t- \alpha *(1+\mu)* d\hat{x}_t,如果令\hat{x}_{t+1} = x_{t+1}就相當於表達3的式子

tensorflow接口

tf.train.MomentumOptimizer(learning_rate, momentum, use_nesterov=True).minimize(loss)

與Momentum的接口一樣,只是對一個參數指定爲True

無論是Momentum還是Nesterov,他們的目的都是改變導數的方向,通過指數加權平均的方式讓導數的方向變化邊緩,從而達到加速優化的目的。

 

AdaGrad

AdaGrad是adaptive gradient的縮寫,主要目的是對不同的參數設置不同的學習率,如果參數的導數比較大,設置學習率小一些,如果參數的導數比較小,設置學習率大一些,用這種方式來減少抖動。

公式

# Assume the gradient dx and parameter vector x
cache += dx**2
x += - learning_rate * dx / (np.sqrt(cache) + eps)

從公式中可以看出學習率由以前的learning_rate變成了learning_rate/(np.sqrt(cache) + eps),eps一般是10^{-7},是爲了防止除以0而添加的一個非常小的偏差。

如果dx比較大,那麼cache會大一些,然後leanring_rate除以比較大的cache的平方根就會讓dx的學習率變小;相反如果dx比較小,cache會小一些,learning_rate除以比較小的cache的平方根會讓dx的學習率大一些。

tensorflow調用

tf.train.AdagradOptimizer(learning_rate).minimize(loss)

但是AdaGrad有個很嚴重的缺陷,就是一直累加dx的平方會導致cache無法避免的越來越大,從而導致學習率衰減到接近0而失去學習的能力。

 

Adadelta

Adadelta是對AdaGrad的改進,使用指數加權平均來替代cache無限制的累加dx的平方。

公式

Sdw = rho*Sdw + (1 - rho)*(dW)^2
Vdw = sqrt((delta_w + epsilon) / (Sdw + epsilon))*dW
W -= Vdw
delta_w = rho*delta_w + (1 - rho)*(Vdw)^2

可以看出去掉了學習率,進行自適應。

tensorflow接口

tf.train.AdadeltaOptimizer(learning_rate=1.0).minimize(loss)

tensorflow中還是保留了學習率的設置,但是如果要忽略學習率可以將學習率直接設置爲1.0

learning_rate: A `Tensor` or a floating point value. The learning rate.
To match the exact form in the original paper use 1.0.

 

RMSProp

RMS是root mean square的縮寫,RMSProp也是AdaGrad的一種改進,但是相對Adadelta來說更加簡單和直觀。

公式

cache = decay_rate * cache + (1 - decay_rate) * dx**2
x += - learning_rate * dx / (np.sqrt(cache) + eps)

也是使用了指數加權平均來計算cache,這樣可以防止cache過大而使學習率降的太低。decay默認是0.9

tensorflow接口

tf.train.RMSPropOptimizer(learning_rate, decay).minimize(loss)

 

Adam

Adam就是將Momentum和RMSProp結合起來的算上,也就是說既從方向上來修正導數也從步伐大小上來修正導數。

公式

# t is your iteration counter going from 1 to infinity
m = beta1*m + (1-beta1)*dx
mt = m / (1-beta1**t)
v = beta2*v + (1-beta2)*(dx**2)
vt = v / (1-beta2**t)
x += - learning_rate * mt / (np.sqrt(vt) + eps)

m是Momentum的計算,beta1一般是0.9,mt是對m進行偏差修正,v是RMSProp的計算,beta2一般是0.999,vt是對v進行偏差修正。

tensorflow接口

tf.train.AdamOptimizer(learning_rate, beta1, beta2).minimize(loss)

總結

這麼多優化方法使用哪種更合適?實際上是無法比較這些算法中的優劣的,有些優化算法可能在某些問題中更好,在有些問題中表現一般。但是現在用的比較多的就是Adam,因爲它融合了兩類優化算法的思想。但是也有文章表明Adam可能由於學習率不斷的變化,時大時小而在最後比較難達到最優解。所以有些論文中可能會使用Adam在訓練初期使用,後期再使用SGD進行精調。相關內容可以參考:Adam的兩宗罪

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章