代碼倉庫: https://github.com/brandonlyg/cute-dl
(轉載請註明出處!)python
在上個階段,咱們使用固定學習率優化器訓練識別MNIST手寫數字模型。在後面的示例中將會看到: 若是學習習設置太大,模型將沒法收斂; 若是設置學習率過小模型大機率會收斂速度會很是緩慢。所以必需要要給學習率設置一個合適的值,這個合適的值究竟是什麼須要反覆試驗。
訓練模型的本質是,在由損失函數定義的高緯超平面中儘量地找到最低點。因爲高緯超平面十分複雜,找到全局最低點每每不現實,所以找到一個儘可能接近全局最低點的局部最低點也是能夠的。
因爲模型參數是隨機初始化的,在訓練的初始階段, 可能遠離最低點,也可能距最低點較較近。爲了使模型可以收斂,較小的學習率比較大的學習率更有可能達到目地, 至少不會使模型發散。
理想的狀態下,咱們但願,學習率是動態的: 在遠離最低點的時候有較大的學習率,在靠近最低點的時候有較小的學習率。
學習率算法在訓練過程當中動態調整學習率,試圖使學習率接近理想狀態。常見的學習率優化算法有:git
目前沒有一種理論可以給出定量的結論斷言一種算法比另外一種更好,具體選用哪一種算法視具體狀況而定。
接下來將會詳細討論每種算法的數學性質及實現,爲了方便討論,先給出一些統一的定義:github
其中v是動量\(v_0=0\), γ是動量的衰減率\(γ∈(0,1)\). 如今把\(v_t\)展開看一下\(g_i, i=1,2,...t\)對v_t的影響.算法
個項係數之和的極限狀況:網絡
令\(t=\frac{1}{1-γ}\)則有\(\frac{1}{t}=1-γ\), \(g_i\)的指數加權平均值可用下式表示:框架
若是把學習率α表示爲:\(α=\frac{α}{1-γ}(1-γ)\),所以\(v_t\)能夠當作是最近的\(1-γ\)次迭代梯度的指數加權平均乘以一個縮放量\(\frac{α}{1-γ}\), 這裏的縮放量\(\frac{α}{1-γ}\)纔是真正的學習率參數。
設\(\frac{1}{1-γ}=n\), 最近n次迭代梯度的權重佔總權重的比例爲:函數
當γ=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\)很大時參數更新幅度過大的問題。
比較原始的梯度降低算法和使用動量的梯度降低算法更新參數的狀況:性能
\(g_t \to 0\)時, 有3種可能:學習
當\(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
其中\(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
爲了克服Adagrad積累量不斷增長致使學習率會趨近於0的缺陷, RMSProp算法的設計在Adagrad的基礎上引入了動量思想。
算法設計者給出的推薦參數是γ=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
這個算法的最大特色是不須要全局學習率超參數, 它也引入了動量思想,使用變化量平方的積累量和梯度平方的積累量共同爲\(g_t\)的每一個元素計算獨立的學習率。
這個算法引入了新的量\(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
前面討論的Adagrad, RMSProp和Adadetal算法, 他們使用的加權平均積累量對\(g_t\)的範數影響較大, 對\(g_t\)的方向影響較小, 另外它們也不能緩解\(g_t \to 0\)的狀況。Adam算法同時引入梯度動量和梯度平方動量,理論上能夠克服前面三種算法共有的缺陷的缺陷。
其中\(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中
代碼中有兩個結束訓練的條件:
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步左右便可達到指望性能。這裏的學習率稍大,證實動量算法能夠適應稍大學習率的數學性質。
def fit_use_adagrad(): lr = 0.001 print("fit_use_adagrad lr=%f"%lr) fit('adagrad.png', optimizers.Adagrad(lr))
屢次試驗代表,Adagrad算法的參數最很差調。因爲這個算法的學習率會一直單調遞減, 它只能對模型進行小幅度的優化, 故而這個算法並不適合從頭開始訓練模型,比較適合對預訓練的模型參數進行微調。
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在較大學習率下很容易發散,而在較小學習率下一般會有穩定的良好表現。
def fit_use_adadelta(): dpr = 0.99 print("fit_use_adadelta dpr=%f"%dpr) fit('adadelta.png', optimizers.Adadelta(dpr))
這個算法不須要給出學習率參數。屢次試驗顯示, 在這個簡單模型上, Adadelta算法表現得很是穩定。
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模型的全面支持,接下來將會爲框架添一些層,讓它可以支持卷積神經網絡模型。