1 訓練
在前面當中我們討論了神經網絡靜態的部分:包括神經網絡結構、神經元類型、數據部分、損失函數部分等。
這個部分我們集中講講動態的部分,主要是訓練的事情,集中在實際工程實踐訓練過程中要注意的一些點,如何找到最合適的參數。
1.1 關於梯度檢驗
之前的博文我們提到過,我們需要比對數值梯度和解析法求得的梯度,實際工程中這個過程非常容易出錯,下面提一些小技巧和注意點:
使用中心化公式,這一點我們之前也說過,使用如下的數值梯度計算公式:
而不是
即使看似上面的形式有着2倍的計算量,但是如果你有興趣用把公式中的\(f(x+h)\)和\(f(x-h)\)做泰勒展開的話,你會發現上面公式出錯率大概是\(O\left(h^{2}\right)\)級別的,而下面公式則是\(O(h)\),注意到\(h\)是很小的數,因此顯然上面的公式要精准得多。
使用相對誤差做比較,這是實際工程中需要提到的另外一點,在我們得到數值梯度\(f_{n}^{\prime}\)和解析梯度\(f_{a}^{\prime}\)之后,我們如何去比較兩者?第一反應是作差\(\left|f_{a}^{\prime}-f_{n}^{\prime}\right|\),或者求平方。
但是用絕對值是不可靠的,假如兩個梯度的絕對值都在1.0左右,那么我們可以認為1e-4這樣一個差值是非常小的,但是如果兩個梯度本身就是1e-4級別的,那這個差值就相當大了。所以我們考慮相對誤差:
加max項的原因很簡單:整體形式變得簡單和對稱。但也別忘了避開分母中兩項都為0的情況。對於相對誤差而言:
- 相對誤差>\(1 e-2\),意味着你的實現肯定是有問題的
- \(1 e-2\)>相對誤差>\(1 e-4\),你會有點擔心
- \(1 e-4\)>相對誤差,基本是OK的,但是要注意極端情況(使用tanh或者softmax時候出現kinks)那還是太大
- \(1 e-7\)>相對誤差,放心大膽使用
隨着神經網絡層數增多,相對誤差是會增大的。這意味着,對於10層的神經網絡,其實相對誤差也許在1e-2級別就已經是可以正常使用的了。
使用雙精度浮點數。如果你使用單精度浮點數計算,那你的實現可能一點問題都沒有,但是相對誤差卻很大。實際工程中出現過,從單精度切到雙精度,相對誤差立馬從1e-2降到1e-8的情況。
要留意浮點數的范圍。一篇很好的文章是"What Every Computer Scientist Should Know About Floating-Point Arithmetic"。我們得保證計算時,所有的數都在浮點數的可計算范圍內,太小的值(比如h)會帶來計算上的問題。
目標函數的不可導點(Kinks)。它指的是一種會導致數值梯度和解析梯度不一致的情況。
會出現在使用ReLU或者類似的神經單元上時,對於很小的負數,比如\(x=-1 e-6\),因為\(x<0\),所以解析梯度是絕對為0的,但是對於數值梯度而言,加入你計算\(f(x+h)\),取的\(h>1 e-6\),那就跳到大於0的部分了,這樣數值梯度就一定和解析梯度不一樣了。
而且這個並不是極端情況,對於一個像CIFAR-10這樣級別的數據集,因為有50000個樣本,同時每個樣本會對應9個錯誤的類別(給損失函數貢獻9個loss值),會有450000個\(\max (0, x)\),會出現很多的kinks。
注意:在計算損失的過程中是可以知道不可導點有沒有被越過的。
在具有\(\max (x, y)\)形式的函數中持續跟蹤所有“贏家”的身份,就可以實現這一點。其實就是看在前向傳播時,到底x和y誰更大。
如果在計算\(f(x+h)\)和\(f(x-h)\)的時候,至少有一個“贏家”的身份變了,那就說明不可導點被越過了,數值梯度會不准確。。
設定步長h要小心。h肯定不能特別大,這個大家都知道。但並不是說h要設定的非常小,其實h設定的非常小也會有問題,因為h太小可能會有數值精度問題。
很有意思的是,有時候在實際情況中h如果從非常小調為\(1e-4\)或者\(1e-6\)反倒會突然計算變得正常。
不要讓正則化項蓋過數據項。有時候會出現這個問題,因為損失函數是數據損失部分與正則化部分的求和。
因此要特別注意正則化部分,你可以想象下,如果它蓋過了數據部分,那么主要的梯度來源於正則化項,那這樣根本就做不到正常的梯度回傳和參數迭代更新。
所以即使在檢查數據部分的實現是否正確,也得先關閉正則化部分(系數\(\lambda\)設為0),再檢查。
注意dropout和其他參數。在檢查數值梯度和解析梯度的時候,如果不把dropout和其他參數都『關掉』的話,兩者之間是一定會有很大差值的。
不過『關掉』它們的負面影響是,沒有辦法檢查這些部分的梯度是否正確。
因此,一個更好的解決方案就是在計算\(f(x+h)\)和\(f(x-h)\)前強制增加一個特定的隨機種子,在計算解析梯度時也同樣如此。
檢查少量的維度。在實際中,梯度可以有上百萬的參數,在這種情況下只能檢查其中一些維度然后假設其他維度是正確的。要小心一點:要保證這些維度的每個參數都檢查對比過了。
1.2 訓練前的檢查工作
在開始訓練之前,我們還得做一些檢查,來確保不會運行了好一陣子,才發現計算代價這么大的訓練其實並不正確。
在初始化之后看一眼loss。其實我們在用很小的隨機數初始化神經網絡后,第一遍計算loss可以做一次檢查數據損失(當然要記得把正則化系數設為0)。
以CIFAR-10為例,如果使用Softmax分類器,我們預測應該可以拿到值為2.302左右的初始loss(因為10個類別,初始概率應該都為0.1,Softmax損失是-ln正確類別的概率):-ln(0.1)=2.302)。
加回正則項,接着我們把正則化系數設為正常的小值,加回正則化項,這時候再算損失loss,應該比剛才要大一些。
試着去擬合一個小的數據集。最后一步,也是很重要的一步,在對大數據集做訓練之前,我們可以先訓練一個小的數據集(比如20張圖片),然后確保你的神經網絡能夠做到0損失loss(當然,是指的正則化系數為0的情況下),因為如果神經網絡實現是正確的,在無正則化項的情況下,完全能夠過擬合這一小部分的數據。
1.3 訓練過程中的監控
開始訓練之后,我們可以通過監控一些指標來了解訓練的狀態。
這些數值輸出的圖表是觀察訓練進程的一扇窗口,是直觀理解不同的超參數設置效果的工具,從而知道如何修改超參數以獲得更高效的學習過程。
1.3.1 損失/loss隨每輪完整迭代后的變化
下面這幅圖表明了不同的學習率下,我們每輪完整迭代(這里的一輪完整迭代指的是所有的樣本都被過了一遍,因為隨機梯度下降中batch size的大小設定可能不同,因此我們不選每次mini-batch迭代為周期)過后的loss應該呈現的變化狀況:

合適的學習率可以保證每輪完整訓練之后,loss都減小,且能在一段時間后降到一個較小的程度。
太小的學習率下loss減小的速度很慢,如果太激進,設置太高的學習率,開始的loss減小速度非常可觀,可是到了某個程度之后就不再下降了,在離最低點一段距離的地方反復,無法下降了。
下圖是實際訓練CIFAR-10的時候,loss的變化情況:

大家可能會注意到上圖的曲線有一些上下跳動,不穩定,這和隨機梯度下降時候設定的batch size有關系。batch size非常小的情況下,會出現很大程度的不穩定,如果batch size設定大一些,會相對穩定一點。
1.3.2 訓練集/驗證集上的准確度
然后我們需要跟蹤一下訓練集和驗證集上的准確度狀況,以判斷分類器所處的狀態(過擬合程度如何):

隨着時間推進,訓練集和驗證集上的准確度都會上升,如果訓練集上的准確度到達一定程度后,兩者之間的差值比較大,那就要注意一下,可能是過擬合現象,如果差值不大,那說明模型狀況良好。
1.3.3 權重:權重更新部分 的比例
最后一個需要留意的量是權重更新幅度和當前權重幅度的比值。
注意是權重更新部分,不一定是計算出來的梯度。(比如訓練用的vanilla sgd,那這個值就是梯度和學習率的乘積)。
最好對於每組參數都獨立地檢查這個比例。
我們沒法下定論,但是在之前的工程實踐中,一個合適的比例大概是1e-3。如果你得到的比例比這個值小很多,那么說明學習率設定太低了,反之則是設定太高了。
1.3.4 每一層的 激勵/梯度值 分布
如果參數初始化不正確,那整個訓練過程會越來越慢,甚至直接停掉。
不過我們可以很容易發現這個問題。表現最明顯的數據是每一層的激勵和梯度的方差(波動狀況)。
舉個例子說,如果初始化不正確,很有可能從前到后逐層的激勵(激勵函數的輸入部分)方差變化是如下的狀況:
# 我們用標准差為0.01均值為0的高斯分布值來初始化權重(這不合理)
Layer 0: Variance: 1.005315e+00
Layer 1: Variance: 3.123429e-04
Layer 2: Variance: 1.159213e-06
Layer 3: Variance: 5.467721e-10
Layer 4: Variance: 2.757210e-13
Layer 5: Variance: 3.316570e-16
Layer 6: Variance: 3.123025e-19
Layer 7: Variance: 6.199031e-22
Layer 8: Variance: 6.623673e-25
大家看一眼上述的數值,就會發現,從前往后,激勵值波動逐層降得非常厲害,這也就意味着反向算法中,計算回傳梯度的時候,梯度都要接近0了,因此參數的迭代更新幾乎就要衰減沒了,顯然不太靠譜。
我們按照上一講中提到的方式正確初始化權重,再逐層看激勵/梯度值的方差,會發現它們的方差衰減沒那么厲害,近似在一個級別:
# 重新正確設定權重:
Layer 0: Variance: 1.002860e+00
Layer 1: Variance: 7.015103e-01
Layer 2: Variance: 6.048625e-01
Layer 3: Variance: 8.517882e-01
Layer 4: Variance: 6.362898e-01
Layer 5: Variance: 4.329555e-01
Layer 6: Variance: 3.539950e-01
Layer 7: Variance: 3.809120e-01
Layer 8: Variance: 2.497737e-01
再看逐層的激勵波動情況,你會發現即使到最后一層,網絡也還是『活躍』的,意味着反向傳播中回傳的梯度值也是夠的,神經網絡是一個積極learning的狀態。
1.3.5 首層的可視化
最后再提一句,如果神經網絡是用在圖像相關的問題上,那么把首層的特征和數據畫出來(可視化)可以幫助我們了解訓練是否正常:

上圖的左右是一個正常和不正常情況下首層特征的可視化對比。
左邊的圖中特征噪點較多,圖像很『渾濁』,預示着可能訓練處於『病態』過程:也許是學習率設定不正常,或者正則化系數設定太低了,或者是別的原因,可能神經網絡不會收斂。
右邊的圖中,特征很平滑和干凈,同時相互間的區分度較大,這表明訓練過程比較正常。
1.4 關於參數更新部分的注意點
當我們確信解析梯度實現正確后,那就該在后向傳播算法中使用它更新權重參數了。就單參數更新這個部分,也是有講究的!
說起來,神經網絡的最優化這個子話題在深度學習研究領域還真是很熱。下面提一下大神們的論文中提到的方法,很多在實際應用中還真是很有效也很常用。
1.4.1 隨機梯度下降與參數更新
vanilla update
這是最簡單的參數更新方式,拿到梯度之后,乘以設定的學習率,用現有的權重減去這個部分,得到新的權重參數(因為梯度表示變化率最大的增大方向,減去這個值之后,損失函數值才會下降)。
記x為權重參數向量x,而梯度為dx,然后我們設定學習率為learning_rate,則最簡單的參數更新如下:
# Vanilla update
x += - learning_rate * dx
當然learning_rate是我們自己設定的一個超變量值(在該更新方法中是全程不變的),而且數學上可以保證,當學習率足夠低的時候,經這個過程迭代后,損失函數不會增加。
Momentum update
這是上面參數更新方法的一種小小的優化,通常說來,在深層次的神經網絡中,收斂效率更高一些(速度更快)。這種參數更新方式源於物理學角度的優化。
# 物理動量角度啟發的參數更新
v = mu * v - learning_rate * dx # 合入一部分附加速度
x += v # 更新參數
這里v是初始化為0的一個值,mu是我們敲定的另外一個超變量(最常見的設定值為0.9,物理含義和摩擦力系數相關)。
一個比較粗糙的理解是,(隨機)梯度下降可以看做從山上下山到山底的過程,這種方式,相當於在下山的過程中,加上了一定的摩擦阻力,消耗掉一小部分動力系統的能量,這樣會比較高效地在山底停住,而不是持續震盪。
其實我們也可以用交叉驗證來選擇最合適的mu值,一般我們會從[0.5, 0.9, 0.95, 0.99]里面選出最合適的。
Nesterov Momentum
這是momentum update的一個不同的版本,最近也用得很火。
據稱,這種參數更新方法,有更好的凸函數和凸優化理論基礎,而實際中的收斂效果也略優於momentum update。
它的思想對應着如下的代碼:
x_ahead = x + mu * v
#考慮到這個時候的x已經有一些變化了
v = mu * v - learning_rate * dx_ahead
x += v
工程上更實用的一個版本是:
v_prev = v # 當前狀態先存儲起來
v = mu * v - learning_rate * dx # 依舊按照Momentum update的方式更新
x += -mu * v_prev + (1 + mu) * v # 新的更新方式
1.4.2 衰減學習率
在實際訓練過程中,隨着訓練過程推進,逐漸衰減學習率是很有必要的。
我們繼續回到下山的場景中,剛下山的時候,可能離最低點很遠,那我步子邁大一點也沒什么關系,可是快到山腳了,我還激進地大步飛奔,一不小心可能就邁過去了。所以還不如隨着下山過程推進,逐步減緩一點點步伐。
不過這個『火候』確實要好好把握,衰減太慢的話,最低段震盪的情況依舊;衰減太快的話,整個系統下降的『動力』衰減太快,很快就下降不動了。下面提一些常見的學習率衰減方式:
步伐衰減:這是很常見的一個衰減模式,每過一輪完整的訓練周期(所有的圖片都過了一遍)之后,學習率下降一些。
比如比較常見的一個衰減率可能是每20輪完整訓練周期,下降10%。不過最合適的值還真是依問題不同有變化。
如果你在訓練過程中,發現交叉驗證集上呈現很高的錯誤率,還一直不下降,你可能就可以考慮考慮調整一下(衰減)學習率了。
指數級別衰減:數學形式為\(\alpha=\alpha_{0} e^{-k t}\),其中\(\alpha_{0}, k\)是需要自己設定的超參數,t 是迭代輪數。
1/t衰減:有着數學形式為\(\alpha=\alpha_{0} /(1+k t)\)的衰減模式,其中\(\alpha_{0}, k\)是需要自己設定的超參數,t 是迭代輪數。
實際工程實踐中,大家還是更傾向於使用步伐衰減,因為它包含的超參數少一些,計算簡單一些,可解釋性稍微高一點。
1.4.3 二次迭代方法
最優化問題里還有一個非常有名的牛頓法,它按照如下的方式進行迭代更新參數:
這里的\(H f(x)\)是Hessian矩陣,是函數的二階偏微分。
而\(\nabla f(x)\)和梯度下降里看到的一樣,是一個梯度向量。
直觀理解是Hessian矩陣描繪出了損失函數的曲度,因此能讓我們更高效地迭代和靠近最低點:乘以Hessian矩陣進行參數迭代會讓在曲度較緩的地方,會用更激進的步長更新參數,而在曲度很陡的地方,步伐會放緩一些。
因此相對一階的更新算法,在這點上它還是有很足的優勢的。
比較尷尬的是,實際深度學習過程中,直接使用二次迭代的方法並不是很實用。原因是直接計算Hessian矩陣是一個非常耗時耗資源的過程。
舉個例子說,一個一百萬參數的神經網絡的Hessian矩陣維度為[1000000*1000000],算下來得占掉3725G的內存。
當然,我們有L-BFGS這種近似Hessian矩陣的算法,可以解決內存問題。
但是L-BFGS一般在全部數據集上計算,而不像我們用的mini-batch SGD一樣在小batch上迭代。
現在有很多人在努力研究這個問題,試圖讓L-BFGS也能以mini-batch的方式穩定迭代更新。但就目前而言,大規模數據上的深度學習很少用到L-BFGS或者類似的二次迭代方法,倒是隨機梯度下降這種簡單的算法被廣泛地使用着。
1.4.4 逐參更新學習率
到目前為止大家看到的學習率更新方式,都是全局使用同樣的學習率。
調整學習率是一件很費時同時也容易出錯的事情,因此大家一直希望有一種學習率自更新的方式,甚至可以細化到逐參數更新。
現在確實有一些這種方法,其中大多數還需要額外的超參數設定,優勢是在大多數超參數設定下,效果都比使用寫死的學習率要好。
Adagrad是Duchi等在論文"Adaptive Subgradient Methods for Online Learning and Stochastic Optimization"中提出的自適應學習率算法。簡單代碼實現如下:
# 假定梯度為dx,參數向量為x
cache += dx**2
x += - learning_rate * dx / np.sqrt(cache + 1e-8)
其中變量cache有着和梯度一樣的維度,然后我們用這個變量持續累加梯度平方。
之后這個值被用作參數更新步驟中的歸一化。
這種方法的好處是,對於高梯度的權重,它們的有效學習率被降低了;而小梯度的權重迭代過程中學習率提升了。
而分母開根號這一步非常重要,不開根號的效果遠差於開根號的情況。
平滑參數1e-8避免了除以0的情況。
RMSprop是一種非常有效,然而好像還沒有被公開發布的自適應學習率更新方法。
有意思的是,現在使用這個方法的人,都引用的大神Geoff Hinton的coursera課程第6節的講義第29頁。
RMSProp方法對Adagrad算法做了一個簡單的優化,以減緩它的迭代強度,它開方的部分cache做了一個平滑處理,大致的示意代碼如下:
cache = decay_rate * cache + (1 - decay_rate) * dx**2
x += - learning_rate * dx / np.sqrt(cache + 1e-8)
這里的decay_rate是一個手動設定的超參數,我們通常會在[0.9, 0.99, 0.999]中取值。需要特別注意的是,x+=這個累加的部分和Adagrad是完全一樣的,但是cache本身是迭代變化的。
另外的方法還有:
- Matthew Zeiler提出的Adadelta
- Adam: A Method for Stochastic Optimization
- Unit Tests for Stochastic Optimization
下圖是上述提到的多種參數更新方法下,損失函數最優化的示意圖:

1.5 超參數的設定與優化
神經網絡的訓練過程中,不可避免地要和很多超參數打交道,這是我們需要手動設定的,大致包括:
- 初始學習率
- 學習率衰減程度
- 正則化系數/強度(包括l2正則化強度,dropout比例)
對於大的深層次神經網絡而言,我們需要很多的時間去訓練。因此在此之前我們花一些時間去做超參數搜索,以確定最佳設定是非常有必要的。
最直接的方式就是在框架實現的過程中,設計一個會持續變換超參數實施優化,並記錄每個超參數下每一輪完整訓練迭代下的驗證集狀態和效果。
實際工程中,神經網絡里確定這些超參數,我們一般很少使用n折交叉驗證,一般使用一份固定的交叉驗證集就可以了。
一般對超參數的嘗試和搜索都是在log域進行的。
例如,一個典型的學習率搜索序列就是learning_rate = 10 ** uniform(-6, 1)。我們先生成均勻分布的序列,再以10為底做指數運算,其實我們在正則化系數中也做了一樣的策略。
比如常見的搜索序列為[0.5, 0.9, 0.95, 0.99]。另外還得注意一點,如果交叉驗證取得的最佳超參數結果在分布邊緣,要特別注意,也許取的均勻分布范圍本身就是不合理的,也許擴充一下這個搜索范圍會有更好的參數。
1.6 模型融合與優化
實際工程中,一個能有效提高最后神經網絡效果的方式是,訓練出多個獨立的模型,在預測階段選結果中的眾數。
模型融合能在一定程度上緩解過擬合的現象,對最后的結果有一定幫助,我們有一些方式可以得到同一個問題的不同獨立模型:
使用不同的初始化參數。先用交叉驗證確定最佳的超參數,然后選取不同的初始值進行訓練,結果模型能有一定程度的差別。
選取交叉驗證排序靠前的模型。在用交叉驗證確定超參數的時候,選取top的部分超參數,分別進行訓練和建模。
選取訓練過程中不同時間點的模型。神經網絡訓練確實是一件非常耗時的事情,因此有些人在模型訓練到一定准確度之后,取不同的時間點的模型去做融合。不過比較明顯的是,這樣模型之間的差異性其實比較小,好處是一次訓練也可以有模型融合的收益。
還有一種常用的有效改善模型效果的方式是,對於訓練后期,保留幾份中間模型權重和最后的模型權重,對它們求一個平均,再在交叉驗證集上測試結果。
通常都會比直接訓練的模型結果高出一兩個百分點。
直觀的理解是,對於碗狀的結構,有很多時候我們的權重都是在最低點附近跳來跳去,而沒法真正到達最低點,而兩個最低點附近的位置求平均,會有更高的概率落在離最低點更近的位置。
2. 總結
- 用一部分的數據測試你梯度計算是否正確,注意提到的注意點。
- 檢查你的初始權重是否合理,在關掉正則化項的系統里,是否可以取得100%的准確度。
- 在訓練過程中,對損失函數結果做記錄,以及訓練集和交叉驗證集上的准確度。
- 最常見的權重更新方式是SGD+Momentum,推薦試試RMSProp自適應學習率更新算法。
- 隨着時間推進要用不同的方式去衰減學習率。
- 用交叉驗證等去搜索和找到最合適的超參數。
- 記得也做做模型融合的工作,對結果有幫助。