1. 程式人生 > >Loss和神經網路訓練

Loss和神經網路訓練

1.訓練

在前一節當中我們討論了神經網路靜態的部分:包括神經網路結構、神經元型別、資料部分、損失函式部分等。這個部分我們集中講講動態的部分,主要是訓練的事情,集中在實際工程實踐訓練過程中要注意的一些點,如何找到最合適的引數。

1.1 關於梯度檢驗

之前的博文我們提到過,我們需要比對數值梯度和解析法求得的梯度,實際工程中這個過程非常容易出錯,下面提一些小技巧和注意點:

使用中心化公式,這一點我們之前也說過,使用如下的數值梯度計算公式: 


而不是 

即使看似上面的形式有著2倍的計算量,但是如果你有興趣用把公式中的展開的話,你會發現上面公式出錯率大概是級別的,而下面公式則是,注意到
h是很小的數,因此顯然上面的公式要精準得多。

使用相對誤差做比較,這是實際工程中需要提到的另外一點,在我們得到數值梯度和解析梯度之後,我們如何去比較兩者?第一反應是作差對吧,或者頂多求一個平方。但是用絕對值是不可靠的,假如兩個梯度的絕對值都在1.0左右,那麼我們可以認為1e-4這樣一個差值是非常小的,但是如果兩個梯度本身就是1e-4級別的,那這個差值就相當大了。所以我們考慮相對誤差: 


加max項的原因很簡單:整體形式變得簡單和對稱。再提個小醒,別忘了避開分母中兩項都為0的情況。OK,對於相對誤差而言:
  • 相對誤差>1e-2意味著你的實現肯定是有問題的
  • 1e-2>相對誤差>1e-4,你會有點擔心
  • 1e-4>相對誤差,基本是OK的,但是要注意極端情況(使用tanh或者softmax時候出現kinks)那還是太大
  • 1e-7>相對誤差,放心大膽使用

哦,對對,還有一點,隨著神經網路層數增多,相對誤差是會增大的。這意味著,對於10層的神經網路,其實相對誤差也許在1e-2級別就已經是可以正常使用的了。

使用雙精度浮點數。如果你使用單精度浮點數計算,那你的實現可能一點問題都沒有,但是相對誤差卻很大。實際工程中出現過,從單精度切到雙精度,相對誤差立馬從1e-2降到1e-8的情況。

Kinks。它指的是一種會導致數值梯度和解析梯度不一致的情況。會出現在使用ReLU或者類似的神經單元上時,對於很小的負數,比如x=-1e-6,因為x<0,所以解析梯度是絕對為0的,但是對於數值梯度而言,加入你計算,取的h>1e-6,那就跳到大於0的部分了,這樣數值梯度就一定和解析梯度不一樣了。而且這個並不是極端情況哦,對於一個像CIFAR-10這樣級別的資料集,因為有50000個樣本,會有450000個,會出現很多的kinks。

不過我們可以監控裡的2項,比較大的那項如果存在躍過0的情況,那就要注意了。

設定步長h要小心。h肯定不能特別大,這個大家都知道對吧。但我並不是說h要設定的非常小,其實h設定的非常小也會有問題,因為h太小程式可能會有精度問題。很有意思的是,有時候在實際情況中h如果從非常小調為1e-4或者1e-6反倒會突然計算變得正常。

不要讓正則化項蓋過資料項。有時候會出現這個問題,因為損失函式是資料損失部分與正則化部分的求和。因此要特別注意正則化部分,你可以想象下,如果它蓋過了資料部分,那麼主要的梯度來源於正則化項,那這樣根本就做不到正常的梯度回傳和引數迭代更新。所以即使在檢查資料部分的實現是否正確,也得先關閉正則化部分(係數設為0),再檢查。

注意dropout和其他引數。在檢查數值梯度和解析梯度的時候,如果不把dropout和其他引數都『關掉』的話,兩者之間是一定會有很大差值的。不過『關掉』它們的負面影響是,沒有辦法檢查這些部分的梯度是否正確。所以,一個合理的方式是,在計算和之前,隨機初始化x,然後再計算解析梯度。

關於只檢查幾個維度。在實際情況中,梯度可能有上百萬維引數。因此每個維度都檢查一遍就不太現實了,一般都是隻檢查一些維度,然後假定其他的維度也都正確。要小心一點:要保證這些維度的每個引數都檢查對比過了。

1.2 訓練前的檢查工作

在開始訓練之前,我們還得做一些檢查,來確保不會運行了好一陣子,才發現計算代價這麼大的訓練其實並不正確。

在初始化之後看一眼loss。其實我們在用很小的隨機數初始化神經網路後,第一遍計算loss可以做一次檢查(當然要記得把正則化係數設為0)。以CIFAR-10為例,如果使用Softmax分類器,我們預測應該可以拿到值為2.302左右的初始loss(因為10個類別,初始概率應該都未0.1,Softmax損失是-log(正確類別的概率):-ln(0.1)=2.302)。

加回正則項,接著我們把正則化係數設為正常的小值,加回正則化項,這時候再算損失/loss,應該比剛才要大一些。

試著去擬合一個小的資料集。最後一步,也是很重要的一步,在對大資料集做訓練之前,我們可以先訓練一個小的資料集(比如20張圖片),然後看看你的神經網路能夠做到0損失/loss(當然,是指的正則化係數為0的情況下),因為如果神經網路實現是正確的,在無正則化項的情況下,完全能夠過擬合這一小部分的資料。

1.3 訓練過程中的監控

開始訓練之後,我們可以通過監控一些指標來了解訓練的狀態。我們還記得有一些引數是我們認為敲定的,比如學習率,比如正則化係數。

  • 損失/loss隨每輪完整迭代後的變化

下面這幅圖表明瞭不同的學習率下,我們每輪完整迭代(這裡的一輪完整迭代指的是所有的樣本都被過了一遍,因為隨機梯度下降中batch size的大小設定可能不同,因此我們不選每次mini-batch迭代為週期)過後的loss應該呈現的變化狀況: 


loss變化狀況 

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

訓練CIFAR-10的loss狀況 

大家可能會注意到上圖的曲線有一些上下跳動,不穩定,這和隨機梯度下降時候設定的batch size有關係。batch size非常小的情況下,會出現很大程度的不穩定,如果batch size設定大一些,會相對穩定一點。
  • 訓練集/驗證集上的準確度

然後我們需要跟蹤一下訓練集和驗證集上的準確度狀況,以判斷分類器所處的狀態(過擬合程度如何): 


訓練集和驗證集上的準確度曲線 

隨著時間推進,訓練集和驗證集上的準確度都會上升,如果訓練集上的準確度到達一定程度後,兩者之間的差值比較大,那就要注意一下,可能是過擬合現象,如果差值不大,那說明模型狀況良好。
  • 權重:權重更新部分 的比例

最後一個需要留意的量是權重更新幅度和當前權重幅度的比值。注意哦,是權重更新部分,不一定是計算出來的梯度哦(比如訓練用的vanilla sgd,那這個值就是梯度學習率的乘積)。最好對於每組引數都獨立地檢查這個比例。我們沒法下定論,但是在之前的工程實踐中,一個合適的比例大概是1e-3。如果你得到的比例比這個值小很多,那麼說明學習率設定太低了,反之則是設定太高了。

  • 每一層的 激勵/梯度值 分佈

如果引數初始化不正確,那整個訓練過程會越來越慢,甚至直接停掉。不過我們可以很容易發現這個問題。體現最明顯的資料是每一層的激勵和梯度的方差(波動狀況)。舉個例子說,如果初始化不正確,很有可能從前到後逐層的激勵(激勵函式的輸入部分)方差變化是如下的狀況:

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 我們用標準差為0.01均值為0的高斯分佈值來初始化權重(這不合理)</span>
Layer <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>: Variance: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.005315e+00</span>
Layer <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>: Variance: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3.123429e-04</span>
Layer <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>: Variance: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.159213e-06</span>
Layer <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3</span>: Variance: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">5.467721e-10</span>
Layer <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">4</span>: Variance: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2.757210e-13</span>
Layer <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">5</span>: Variance: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3.316570e-16</span>
Layer <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">6</span>: Variance: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3.123025e-19</span>
Layer <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">7</span>: Variance: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">6.199031e-22</span>
Layer <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">8</span>: Variance: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">6.623673e-25</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li></ul>

大家看一眼上述的數值,就會發現,從前往後,激勵值波動逐層降得非常厲害,這也就意味著反向演算法中,計算回傳梯度的時候,梯度都要接近0了,因此引數的迭代更新幾乎就要衰減沒了,顯然不太靠譜。我們按照上一講中提到的方式正確初始化權重,再逐層看激勵/梯度值的方差,會發現它們的方差衰減沒那麼厲害,近似在一個級別:

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 重新正確設定權重:</span>
Layer <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>: Variance: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.002860e+00</span>
Layer <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>: Variance: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">7.015103e-01</span>
Layer <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>: Variance: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">6.048625e-01</span>
Layer <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3</span>: Variance: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">8.517882e-01</span>
Layer <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">4</span>: Variance: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">6.362898e-01</span>
Layer <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">5</span>: Variance: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">4.329555e-01</span>
Layer <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">6</span>: Variance: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3.539950e-01</span>
Layer <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">7</span>: Variance: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3.809120e-01</span>
Layer <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">8</span>: Variance: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2.497737e-01</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li></ul>

再看逐層的激勵波動情況,你會發現即使到最後一層,網路也還是『活躍』的,意味著反向傳播中回傳的梯度值也是夠的,神經網路是一個積極learning的狀態。

  • 首層的視覺化

最後再提一句,如果神經網路是用在影象相關的問題上,那麼把首層的特徵和資料畫出來(視覺化)可以幫助我們瞭解訓練是否正常: 


視覺化首層

上圖的左右是一個正常和不正常情況下首層特徵的視覺化對比。左邊的圖中特徵噪點較多,影象很『渾濁』,預示著可能訓練處於『病態』過程:也許是學習率設定不正常,或者正則化係數設定太低了,或者是別的原因,可能神經網路不會收斂。右邊的圖中,特徵很平滑和乾淨,同時相互間的區分度較大,這表明訓練過程比較正常。

1.4 關於引數更新部分的注意點

當我們確信解析梯度實現正確後,那就該在後向傳播演算法中使用它更新權重引數了。就單引數更新這個部分,也是有講究的:

說起來,神經網路的最優化這個子話題在深度學習研究領域還真是很熱。下面提一下大神們的論文中提到的方法,很多在實際應用中還真是很有效也很常用。

1.4.1 隨機梯度下降與引數更新

vanilla update

這是最簡單的引數更新方式,拿到梯度之後,乘以設定的學習率,用現有的權重減去這個部分,得到新的權重引數(因為梯度表示變化率最大的增大方向,減去這個值之後,損失函式值才會下降)。記x為權重引數向量x,而梯度為dx,然後我們設定學習率為learning_rate,則最簡單的引數更新大家都知道:

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># Vanilla update</span>
x += - learning_rate * dx</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li></ul>

當然learning_rate是我們自己敲定的一個超變數值(在該更新方法中是全程不變的),而且數學上可以保證,當學習率足夠低的時候,經這個過程迭代後,損失函式不會增加。

Momentum update

這是上面引數更新方法的一種小小的優化,通常說來,在深層次的神經網路中,收斂效率更高一些(速度更快)。這種引數更新方式源於物理學角度的優化。

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 物理動量角度啟發的引數更新</span>
v = mu * v - learning_rate * dx <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 合入一部分附加速度</span>
x += v <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 更新引數</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul>

這裡v是初始化為0的一個值,mu是我們敲定的另外一個超變數(最常見的設定值為0.9,物理含義和摩擦力系數相關),一個比較粗糙的理解是,(隨機)梯度下降可以看做從山上下山到山底的過程,這種方式,相當於在下山的過程中,加上了一定的摩擦阻力,消耗掉一小部分動力系統的能量,這樣會比較高效地在山底停住,而不是持續震盪。對了,其實我們也可以用交叉驗證來選擇最合適的mu值,一般我們會從[0.5, 0.9, 0.95, 0.99]裡面選出最合適的。

Nesterov Momentum

這是momentum update的一個不同的版本,最近也用得很火。咳咳,據稱,這種引數更新方法,有更好的凸函式和凸優化理論基礎,而實際中的收斂效果也略優於momentum update。

此處的深層次原理,博主表示智商有點捉急…有興趣的同學可以看看以下的2個材料:

它的思想對應著如下的程式碼:

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">x_ahead = x + mu * v
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 考慮到這個時候的x已經有一些變化了</span>
v = mu * v - learning_rate * dx_ahead
x += v</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li></ul>

工程上更實用的一個版本是:

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">v_prev = v <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 當前狀態先儲存起來</span>
v = mu * v - learning_rate * dx <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 依舊按照Momentum update的方式更新</span>
x += -mu * v_prev + (<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span> + mu) * v <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 新的更新方式</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul>

1.4.2 衰減學習率

在實際訓練過程中,隨著訓練過程推進,逐漸衰減學習率是很有必要的。我們繼續回到下山的場景中,剛下山的時候,可能離最低點很遠,那我步子邁大一點也沒什麼關係,可是快到山腳了,我還激進地大步飛奔,一不小心可能就邁過去了。所以還不如隨著下山過程推進,逐步減緩一點點步伐。不過這個『火候』確實要好好把握,衰減太慢的話,最低段震盪的情況依舊;衰減太快的話,整個系統下降的『動力』衰減太快,很快就下降不動了。下面提一些常見的學習率衰減方式:

  • 步伐衰減:這是很常見的一個衰減模式,每過一輪完整的訓練週期(所有的圖片都過了一遍)之後,學習率下降一些。比如比較常見的一個衰減率可能是每20輪完整訓練週期,下降10%。不過最合適的值還真是依問題不同有變化。如果你在訓練過程中,發現交叉驗證集上呈現很高的錯誤率,還一直不下降,你可能就可以考慮考慮調整一下(衰減)學習率了。
  • 指數級別衰減:數學形式為,其中是需要自己敲定的超引數,是迭代輪數。
  • 1/t衰減:有著數學形式為的衰減模式,其中是需要自己敲定的超引數,是迭代輪數。

實際工程實踐中,大家還是更傾向於使用步伐衰減,因為它包含的超引數少一些,計算簡單一些,可解釋性稍微高一點。

1.4.3 二次迭代方法

最優化問題裡還有一個非常有名的牛頓法,它按照如下的方式進行迭代更新引數:

這裡的是Hessian矩陣,是函式的二階偏微分。而和梯度下降裡看到的一樣,是一個梯度向量。直觀理解是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 逐參更新學習率

到目前為止大家看到的學習率更新方式,都是全域性使用同樣的學習率。調整學習率是一件很費時同時也容易出錯的事情,因此大家一直希望有一種學習率自更新的方式,甚至可以細化到逐引數更新。現在確實有一些這種方法,其中大多數還需要額外的超引數設定,優勢是在大多數超引數設定下,效果都比使用寫死的學習率要好。下面稍微提一下常見的自適應方法(原諒博主底子略弱,沒辦法深入數學細節講解):

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 假定梯度為dx,引數向量為x</span>
cache += dx**<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>
x += - learning_rate * dx / np.sqrt(cache + <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1e-8</span>)</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul>

其中變數cache有著和梯度一樣的維度,然後我們用這個變數持續累加梯度平方。之後這個值被用作引數更新步驟中的歸一化。這種方法的好處是,對於高梯度的權重,它們的有效學習率被降低了;而小梯度的權重迭代過程中學習率提升了。而分母開根號這一步非常重要,不開根號的效果遠差於開根號的情況。平滑引數1e-8避免了除以0的情況。

RMSprop是一種非常有效,然後好像還沒有被公開發布的自適應學習率更新方法。有意思的是,現在使用這個方法的人,都引用的大神Geoff Hinton的coursera課程第6節的slide第29頁。RMSProp方法對Adagrad演算法做了一個簡單的優化,以減緩它的迭代強度,它開方的部分cache做了一個平滑處理,大致的示意程式碼如下:

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">cache = decay_rate * cache + (<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span> - decay_rate) * dx**<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>
x += - learning_rate * dx / np.sqrt(cache + <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1e-8</span>)</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li></ul>

這裡的decay_rate是一個手動敲定的超引數,我們通常會在[0.9, 0.99, 0.999]中取值。需要特別注意的是,x+=這個累加的部分和Adagrad是完全一樣的,但是cache本身是迭代變化的。

另外的方法還有:

下圖是上述提到的多種引數更新方法下,損失函式最優化的示意圖: 


引數更新1 
引數更新2 

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自適應學習率更新演算法。
  • 隨著時間推進要用不同的方式去衰減學習率。
  • 用交叉驗證等去搜索和找到最合適的超引數。
  • 記得也做做模型融合的工作,對結果有幫助。