本身動手實現深度學習框架-5 使用學習率優化器加快模型訓練速度

代碼倉庫: https://github.com/brandonlyg/cute-dl
(轉載請註明出處!)python

目標

  1. 增長學習率優化器, 加快模型在小學習率下模型的訓練速度。
  2. 使用MNIST數據集比較同一個模型使用不一樣學習率優化器的表現。

常見的學習率優化算法

        在上個階段,咱們使用固定學習率優化器訓練識別MNIST手寫數字模型。在後面的示例中將會看到: 若是學習習設置太大,模型將沒法收斂; 若是設置學習率過小模型大機率會收斂速度會很是緩慢。所以必需要要給學習率設置一個合適的值,這個合適的值究竟是什麼須要反覆試驗。
        訓練模型的本質是,在由損失函數定義的高緯超平面中儘量地找到最低點。因爲高緯超平面十分複雜,找到全局最低點每每不現實,所以找到一個儘可能接近全局最低點的局部最低點也是能夠的。
        因爲模型參數是隨機初始化的,在訓練的初始階段, 可能遠離最低點,也可能距最低點較較近。爲了使模型可以收斂,較小的學習率比較大的學習率更有可能達到目地, 至少不會使模型發散。
        理想的狀態下,咱們但願,學習率是動態的: 在遠離最低點的時候有較大的學習率,在靠近最低點的時候有較小的學習率。
        學習率算法在訓練過程當中動態調整學習率,試圖使學習率接近理想狀態。常見的學習率優化算法有:git

  • 動量算法。
  • Adagrad算法。
  • RMSProp算法。
  • Adadelta算法。
  • Adam算法。

        目前沒有一種理論可以給出定量的結論斷言一種算法比另外一種更好,具體選用哪一種算法視具體狀況而定。
        接下來將會詳細討論每種算法的數學性質及實現,爲了方便討論,先給出一些統一的定義:github

  • 模型經歷一次向前傳播和一次反向傳播稱訓練稱爲一次迭代。用t表示模型當前的迭代次數,t=0,1,2,...。 當t=0是表示模型處於初始狀態。
  • g表示反向傳播的梯度, \(g_t\)是第t次迭的梯度。其餘量的表示方式和g相同。
  • w表示模型學習的參數。α表示學習率超參數。
  • 符號a ⊙ v若是a和v都是向量, a, v必須有相同的維度a ⊙ v表示他們相同位置上的元素相乘,結果是和a,v具備相同維度的向量. 若是a是標量v是向量a ⊙ v表示向量的標量乘法等價於av。

動量算法

數學原理

\[\begin{matrix} v_t = v_{t-1}γ + αg_t \\ w = w - v_t\\ \end{matrix} \]

        其中v是動量\(v_0=0\), γ是動量的衰減率\(γ∈(0,1)\). 如今把\(v_t\)展開看一下\(g_i, i=1,2,...t\)對v_t的影響.算法

\[v_t = α(γ^{t-1}g_1 + γ^{t-2}g_2 + ... + γg_{t-1} + g_t) \]

        個項係數之和的極限狀況:網絡

\[\lim_{t \to ∞} \sum_{i=1}^t γ^{t-i} = \frac{1}{1-γ} \]

        令\(t=\frac{1}{1-γ}\)則有\(\frac{1}{t}=1-γ\), \(g_i\)的指數加權平均值可用下式表示:框架

\[\bar v_t = \frac{1}{t}(γ^{t-1}g_1 + γ^{t-2}g_2 + ... + γg_{t-1} + g_t) = (1-γ)(γ^{t-1}g_1 + γ^{t-2}g_2 + ... + γg_{t-1} + g_t) \]

        若是把學習率α表示爲:\(α=\frac{α}{1-γ}(1-γ)\),所以\(v_t\)能夠當作是最近的\(1-γ\)次迭代梯度的指數加權平均乘以一個縮放量\(\frac{α}{1-γ}\), 這裏的縮放量\(\frac{α}{1-γ}\)纔是真正的學習率參數。
        設\(\frac{1}{1-γ}=n\), 最近n次迭代梯度的權重佔總權重的比例爲:函數

\[(\sum_{i=1}^{n} γ^i) / \frac{1}{1-γ} = \frac{\frac{1-γ^n}{1-γ}}{\frac{1}{1-γ}} = 1-γ^n = 1 - γ^{\frac{1}{1-γ}} \]

        當γ=0.9時, \(1 - γ^{10}≈0.651\), 就是說, 這個時候, 最近的10次迭代佔總權重的比例約爲65.1%, 換言之\(v_t\)的值的數量級由最近10次迭代權重值決定。
        當咱們設置超參數γ,α時, 能夠認爲取了最近\(\frac{1}{1-γ}\)次迭代梯度的指數加權平均值爲動量積累量,而後對這個積累量做\(\frac{α}{1-γ}\)倍的縮放。 例如: γ=0.9, α=0.01, 表示取最近10次的加權平均值,而後將這個值縮小到原來的0.1倍。
        動量算法可以有效地緩解\(g_t \to 0\)時參數更新緩慢的問題和當\(g_t\)很大時參數更新幅度過大的問題。
        比較原始的梯度降低算法和使用動量的梯度降低算法更新參數的狀況:性能

\[\begin{matrix} w = w - αg_t & (1) & 原始梯度降低算法 \\ w = w - v_t & (2) & 動量算法 \end{matrix} \]

        \(g_t \to 0\)時, 有3種可能:學習

  1. 當前位置是超平面的一個全局最低點。是指望的理想收斂位置,(1)式會中止更新參數, (2)式會使參數在這個位置周圍以愈來愈小的幅度震盪, 最終收斂到這個位置。
  2. 當前位置是超平面的一個局部最低點。(1)式會停留在這個位置, 最終學習到一個不理想的參數。(2)式中\(v_t\)因爲積累了最近n步的勢能, 會衝過這個區域, 繼續尋找更優解。
  3. 當前位置是超平面的一個鞍點。 (1)式會停留在這個位置, 最終學習到一個不可用的參數。(2)式中\(v_t\)因爲記錄了最近n步的勢能, 會衝過這個區域, 繼續尋找更優解

        當\(g_t\)很大時(1)式致使參數大幅度的更新, 會大機率致使模型發散。(2)式當γ=0.9時, \(g_t\)\(v_t\)的影響權重是0.1; 式當γ=0.99時,\(g_t\)\(v_t\)的影響權重是0.01, 相比於\(g_{t-1}\)\(g_t\)的增長幅度, \(v_{t-1}\)\(v_t\)增長幅度要小的多, 參數更新也會平滑許多。優化

實現

        文件: cutedl/optimizers.py, 類名:Momentum.

def update_param(self, param):
      #pdb.set_trace()
      if not hasattr(param, 'momentum'):
          #爲參數添加動量屬性
          param.momentum = np.zeros(param.value.shape)

      param.momentum = param.momentum * self.__dpr + param.gradient * self.__lr

      param.value -= param.momentum

Adagrad算法

數學原理

\[\begin{matrix} s_t = s_{t-1} + g_t ⊙ g_t \\ Δw_t = \frac{α}{\sqrt{s_t} + ε} ⊙ g_t \\ w = w - Δw_t \end{matrix} \]

        其中\(s_t\)是梯度平方的積累量, \(ε=10^{-6}\)用來保持數值的穩定, Δw_t是參數的變化量。\(s_t\)的每一個元素都是正數, 隨着迭代次數增長, 數值會愈來愈大,相應地\(\frac{α}{\sqrt{s_t} + ε}\)的值會愈來愈小。\(\frac{α}{\sqrt{s_t} + ε}\)至關於爲\(g_t\)中的每一個元素計算獨立的的學習率, 使\(Δw_t\)中的每一個元素位於(-1, 1)區間內,隨着訓練次數的增長會向0收斂。這意味着\(||Δw_t||\)會愈來愈小, 迭代次數比較大時, \(||Δw_t|| \to 0\), 參數w將不會有更新。相比於動量算法, Adagrad算法調整學習率的方向比較單一, 只會往更小的方向上調整。α不能設置較大的值, 由於在訓練初期\(s_t\)的值會很小, \(\frac{α}{\sqrt{s_t} + ε}\)會放大α的值, 致使較大的學習率,從而致使模型發散。

實現

        文件: cutedl/optimizers.py, 類名:Adagrad.

def update_param(self, param):
      #pdb.set_trace()
      if  not hasattr(param, 'adagrad'):
          #添加積累量屬性
          param.adagrad = np.zeros(param.value.shape)

      a = 1e-6
      param.adagrad += param.gradient ** 2
      grad = self.__lr/(np.sqrt(param.adagrad) + a) * param.gradient
      param.value -= grad

RMSProp算法

數學原理

        爲了克服Adagrad積累量不斷增長致使學習率會趨近於0的缺陷, RMSProp算法的設計在Adagrad的基礎上引入了動量思想。

\[\begin{matrix} s_t = s_{t-1}γ + g_t ⊙ g_t(1-γ) \\ Δw_t = \frac{α}{\sqrt{s_t} + ε} ⊙ g_t \\ w = w - Δw_t \end{matrix} \]

        算法設計者給出的推薦參數是γ=0.99, 即\(s_t\)是最近100次迭代梯度平方的積累量, 因爲計算變化量時使用的是\(\sqrt{s_t}\), 對變化量的影響只至關於最近10次的梯度積累量。
        的Adagrad相似, \(s_t\)\(g_t\)的方向影響較小, 但對\(||g_t||\)大小影響較大,會把它縮小到(-1, 1)區間內, 不一樣的是不會單調地把\(||g_t||\)收斂到0, 從而克服了Adagrad的缺陷。

實現

        文件: cutedl/optimizers.py, 類名:RMSProp.

def update_param(self, param):
      #pdb.set_trace()
      if not hasattr(param, 'rmsprop_storeup'):
          #添加積累量屬性
          param.rmsprop_storeup = np.zeros(param.value.shape)

      a = 1e-6

      param.rmsprop_storeup = param.rmsprop_storeup * self.__sdpr + (param.gradient**2) * (1-self.__sdpr)
      grad = self.__lr/(np.sqrt(param.rmsprop_storeup) + a) * param.gradient

      param.value -= grad

Adadelta算法

數學原理

        這個算法的最大特色是不須要全局學習率超參數, 它也引入了動量思想,使用變化量平方的積累量和梯度平方的積累量共同爲\(g_t\)的每一個元素計算獨立的學習率。

\[\begin{matrix} s_t = s_{t-1}γ + g_t ⊙ g_t(1-γ) \\ Δw_t = \frac{\sqrt{d_{t-1}} + ε}{\sqrt{s_t} + ε} ⊙ g_t \\ d_t = d_{t-1}γ + Δw_t ⊙ Δw_t(1-γ)\\ w = w - Δw_t \end{matrix} \]

        這個算法引入了新的量\(d_t\), 是變化量平方的積累量, 表示最近n次迭代的參數變化量平方的加權平均. \(ε=10^{-6}\). 推薦的超參數值是γ=0.99。這個算法和RMSProp相似, 只是用\(\sqrt{d_{t-1}}\)代替了學習率超參數α。

實現

        文件: cutedl/optimizers.py, 類名:Adadelta.

def update_param(self, param):
      #pdb.set_trace()
      if not hasattr(param, 'adadelta_storeup'):
          #添加積累量屬性
          param.adadelta_storeup = np.zeros(param.value.shape)

      if not hasattr(param, "adadelta_predelta"):
          #添加上步的變化量屬性
          param.adadelta_predelta = np.zeros(param.value.shape)

      a = 1e-6

      param.adadelta_storeup = param.adadelta_storeup * self.__dpr + (param.gradient**2)*(1-self.__dpr)
      grad = (np.sqrt(param.adadelta_predelta)+a)/(np.sqrt(param.adadelta_storeup)+a) * param.gradient
      param.adadelta_predelta = param.adadelta_predelta * self.__dpr + (grad**2)*(1-self.__dpr)

      param.value -= grad

Adam算法

數學原理

        前面討論的Adagrad, RMSProp和Adadetal算法, 他們使用的加權平均積累量對\(g_t\)的範數影響較大, 對\(g_t\)的方向影響較小, 另外它們也不能緩解\(g_t \to 0\)的狀況。Adam算法同時引入梯度動量和梯度平方動量,理論上能夠克服前面三種算法共有的缺陷的缺陷。

\[\begin{matrix} v_t = v_{t-1}γ_1 + g_t(1-γ_1) \\ s_t = s_{t-1}γ_2 + g_t ⊙ g_t(1-γ_2) \\ \hat{v_t} = \frac{v_t}{1 - γ_1^t} \\ \hat{s_t} = \frac{s_t}{1 - γ_2^t} \\ Δw_t = \frac{α\hat{v_t}}{\sqrt{s_t} + ε} \\ w = w - Δw_t \end{matrix} \]

        其中\(v_t\)和動量算法中的\(v_t\)含義同樣,\(s_t\)和RMSProp算法的\(s_t\)含義同樣, 對應的超參數也有同樣的推薦值\(γ_1=0.9\), \(γ_2=0.99\)。用於穩定數值的\(ε=10^{-8}\). 比較特別的是\(\hat{v_t}\)\(\hat{s_t}\), 他們是對\(v_t\)\(s_t\)的一個修正。以\(\hat{v_t}\)爲例, 當t比較小的時候, \(\hat{v_t}\)近似於最近\(\frac{1}{1-γ}\)次迭代梯度的加權和而不是加權平均, 當t比較大時, \(1-γ^t \to 1\), 從而使\(\hat{v_t} \to v_t\)。也就是所\(\hat{v_t}\)時對對迭代次數較少時\(v_t\)值的修正, 防止在模型訓練的開始階段產生過小的學習率。\(\hat{s_t}\)的做用和\(\hat{v_t}\)是相似的。

實現

        文件: cutedl/optimizers.py, 類名:Adam.

def update_param(self, param):
      #pdb.set_trace()
      if not hasattr(param, 'adam_momentum'):
          #添加動量屬性
          param.adam_momentum = np.zeros(param.value.shape)

      if not hasattr(param, 'adam_mdpr_t'):
          #mdpr的t次方
          param.adam_mdpr_t = 1

      if not hasattr(param, 'adam_storeup'):
          #添加積累量屬性
          param.adam_storeup = np.zeros(param.value.shape)

      if not hasattr(param, 'adam_sdpr_t'):
          #動量sdpr的t次方
          param.adam_sdpr_t = 1

      a = 1e-8
      #計算動量
      param.adam_momentum = param.adam_momentum * self.__mdpr + param.gradient * (1-self.__mdpr)
      #誤差修正
      param.adam_mdpr_t *= self.__mdpr
      momentum = param.adam_momentum/(1-param.adam_mdpr_t)

      #計算積累量
      param.adam_storeup = param.adam_storeup * self.__sdpr + (param.gradient**2) * (1-self.__sdpr)
      #誤差修正
      param.adam_sdpr_t *= self.__sdpr
      storeup = param.adam_storeup/(1-param.adam_sdpr_t)

      grad = self.__lr * momentum/(np.sqrt(storeup)+a)
      param.value -= grad

不一樣學習率對訓練模型的影響

        接下來咱們仍然使用上個階段的模型作爲示例, 使用不一樣的優化算法訓練模型,對比差異。代碼在examples/mlp/mnist-recognize.py中
        代碼中有兩個結束訓練的條件:

  1. 連續20次驗證沒有獲得更小的驗證偏差,表示模型模型已經沒法進一步優化或者已經開始發散了,結束訓練。
  2. 連續20次驗證模型驗證正確率都在91%以上,表示模型性能已經達到預期目標且是收斂的,結束訓練。

不使用優化算法的狀況

使用較小的學習率

def fit0():
    lr = 0.0001
    print("fit1 lr:", lr)
    fit('0.png', optimizers.Fixed(lr))


        較小的固定學習率0.0001可使模型穩定地收斂,但收斂速度很慢, 訓練接近100萬步, 最後因爲收斂速度太慢而中止訓練。

使用較大的學習率

def fit1():
    lr = 0.2
    print("fit0 lr:", lr)
    fit('1.png', optimizers.Fixed(lr))


        較大的固定學習率0.2, 模型在訓練7萬步左右的時候因發散而中止訓練。模型進度開始下降: 最大驗證正確率爲:0.8445, 結束時的驗證正確率爲:0.8438.

適當的學習率

def fit2():
    lr = 0.01
    print("fit2 lr:", lr)
    fit('2.png', optimizers.Fixed(lr))


        經過屢次試驗, 找到了一個合適的學習率0.01, 這時模型只需訓練28000步左右便可達到指望性能。

動量算法優化器

def fit_use_momentum():
    lr = 0.002
    dpr = 0.9
    print("fit_use_momentum lr=%f, dpr:%f"%(lr, dpr))
    fit('momentum.png', optimizers.Momentum(lr, dpr))


        這裏的真實學習率爲\(\frac{0.002}{1-0.9} = 0.02\)。模型訓練23000步左右便可達到指望性能。這裏的學習率稍大,證實動量算法能夠適應稍大學習率的數學性質。

Adagrad算法優化器

def fit_use_adagrad():
    lr = 0.001
    print("fit_use_adagrad lr=%f"%lr)
    fit('adagrad.png', optimizers.Adagrad(lr))


        屢次試驗代表,Adagrad算法的參數最很差調。因爲這個算法的學習率會一直單調遞減, 它只能對模型進行小幅度的優化, 故而這個算法並不適合從頭開始訓練模型,比較適合對預訓練的模型參數進行微調。

RMSProp算法優化器

def fit_use_rmsprop():
    sdpr = 0.99
    lr=0.0001
    print("fit_use_rmsprop lr=%f sdpr=%f"%(lr, sdpr))
    fit('rmsprop.png', optimizers.RMSProp(lr, sdpr))


        這裏給出的是較小的學習率0.0001。屢次試驗代表, RMSProp在較大學習率下很容易發散,而在較小學習率下一般會有穩定的良好表現。

Adadelta算法優化器

def fit_use_adadelta():
    dpr = 0.99
    print("fit_use_adadelta dpr=%f"%dpr)
    fit('adadelta.png', optimizers.Adadelta(dpr))


        這個算法不須要給出學習率參數。屢次試驗顯示, 在這個簡單模型上, Adadelta算法表現得很是穩定。

Adam算法優化器

def fit_use_adam():
    lr = 0.0001
    mdpr = 0.9
    sdpr = 0.99
    print("fit_use_adam lr=%f, mdpr=%f, sdpr=%f"%(lr, mdpr, sdpr))
    fit('adam.png', optimizers.Adam(lr, mdpr, sdpr))


        只用這個算法在較小學習率0.0001的狀況下20000步左右便可完成訓練且最終達到了92.4%的驗證準確率。

總結

        這個階段爲框架添加了常見的學習率優化算法,並在同一個模型上進行驗證,對比。我發現即便不使用優化算法,用固定的學習率, 只要給出「合適」的學習率參數,仍然可以獲得理想的訓練速度, 但很難肯定怎樣纔算「適合」。 學習率算法給出了參數調整的大體方向,通常來講較小的學習率都不會有問題,至少不會使模型發散,而後能夠經過調整衰減率來加快訓練速度,而衰減率有比較簡單數學性質可讓咱們在調整它的時知道這樣調整意味着什麼。         目前爲止cute-dl框架已經實現了對簡單MLP模型的全面支持,接下來將會爲框架添一些層,讓它可以支持卷積神經網絡模型。

相關文章
相關標籤/搜索