前幾篇的鏈接如下:

永遠在你身後:自己動手寫深度神經網路框架(一)

永遠在你身後:自己動手寫深度神經網路框架(二)

永遠在你身後:自己動手寫深度神經網路框架(三)

永遠在你身後:自己動手寫深度神經網路框架(四)

然後是:完整實現

在前面幾篇中已經介紹了一些Layer的正反向計算過程及其推導,並且用它們構建了一個網路

雖然推導的過程是建立在單個訓練樣本的基礎上的,但對於同時使用多個訓練樣本同樣有效,下面來簡單的證明一下:

首先回顧一下線性層的forward計算過程,輸入 x 是一個向量

x=left[egin{array}{llll}{x_{1}} & {x_{2}} & {cdots} & {x_{k}}end{array}
ight]

所有神經元的權重是一個矩陣: W in R^{k 	imes m}

輸出 a 同樣是一個向量

a=left[egin{array}{llll}{a_{1}} & {a_{2}} & {cdots} & {a_{m}}end{array}
ight]

計算過程為:

a=x W

現在將其改成每次訓練多個樣本,先看訓練批量為1的情況;另外, x,a 的符號要做下改動

x^{1}=left[egin{array}{llll}{x_{1,1}} & {x_{1,2}} & {cdots} & {x_{1, k}}end{array}
ight]

a^{1}=left[egin{array}{llll}{a_{1,1}} & {a_{2,1}} & {cdots} & {a_{m, 1}}end{array}
ight]

然後,將輸入輸出都寫成矩陣形式

X=left[x^{1}
ight]

A=left[a^{1}
ight]

很容易可以得到下面的計算過程

A=X W=left[x^{1} W
ight]=left[a^{1}
ight]

現在,設每次訓練n個樣本,這些新增的訓練樣本表示為

x^{2}=left[egin{array}{llll}{x_{2,1}} & {x_{2,2}} & {cdots} & {x_{2, k}}end{array}
ight]

……

x^{n}=left[egin{array}{lll}{x_{n, 1}} & {x_{n, 2}} & {cdots} & {x_{n, k}}end{array}
ight]

所以輸入的矩陣就變成了:

X=left[egin{array}{c}{x^{1}} \ {x^{2}} \ {vdots} \ {x^{n}}end{array}
ight]

輸出的計算過程就變成了:

A=X W=left[egin{array}{c}{x^{1} W} \ {x^{2} W} \ {vdots} \ {x^{n} W}end{array}
ight]=left[egin{array}{c}{a^{1}} \ {a^{2}} \ {vdots} \ {a^{n}}end{array}
ight]

可以看到,即使每次訓練多個樣本,它們之間的計算都是相互獨立的,並不會糾纏在一起

這個是線性層的證明,其他的Layer就不一一說明瞭,再看看backward計算過程,同樣,我們先看訓練樣本數為1的情況

backward中傳進來的參數是損失關於輸出的梯度

frac{partial L}{partial a}=left[frac{partial L}{partial a_{1}} quad dots quad frac{partial L}{partial a_{m}}
ight]

為了方便表示,這裡令 frac{partial L}{partial a}=
abla a

同時已知: frac{partial a}{partial x}=W^{T}

為了將損失繼續傳回上一層,所以需要計算 frac{partial L}{partial x} ,根據鏈式法則可得:

frac{partial L}{partial x}=frac{partial L}{partial a} frac{partial a}{partial x}=
abla a W^{T}=left[frac{partial L}{partial x_{1}} quad cdots quad frac{partial L}{partial x_{k}}
ight]

同樣令 frac{partial L}{partial x}=
abla x

上面是訓練樣本數為1時的情況,第(三)篇有詳細的推導,這裡只是簡單的回顧一下

現在來看訓練樣本數為n的情況,將損失關於輸出的梯度表示成矩陣形式有:


abla A=left[egin{array}{c}{
abla a^{1}} \ {vdots} \ {
abla a^{n}}end{array}
ight]

利用和上面相同的計算過程有:


abla X=
abla A W^{T}=left[egin{array}{c}{
abla a^{1} W^{T}} \ {vdots} \ {
abla a^{n} W^{T}}end{array}
ight]=left[egin{array}{c}{
abla x^{1}} \ {vdots} \ {
abla x^{n}}end{array}
ight]

所以,在反向傳播過程中,多個訓練樣本之間的梯度計算也是獨立的,不會相互影響

其實這個在證明forward以後結論已經很明顯了,不過backward的任務不僅僅是把損失傳給上一層,主要是計算參數的梯度,而且,這個與之前的計算還是有一些不同的,來看一下:


abla W=x^{T} 
abla a=left[egin{array}{c}{x_{1}} \ {x_{2}} \ {vdots} \ {x_{k}}end{array}
ight]left[egin{array}{ccc}{frac{partial L}{partial a_{1}}} & {frac{partial L}{partial a_{2}}} & {cdots} & {frac{partial L}{partial a_{m}}}end{array}
ight]


abla W in R^{mathrm{k} 	imes m}

這是之前推導過的權重關於損失的梯度的公式,計算出來這個梯度矩陣的單個元素的值為:


abla W_{k, m}=x_{k} frac{partial L}{partial a_{m}}

現在看多個訓練樣本的情況:


abla W=X^{T} 
abla A=left[egin{array}{ccc}{x_{1,1}} & {cdots} & {x_{1, n}} \ {vdots} & {ddots} & {vdots} \ {x_{k, 1}} & {cdots} & {x_{k, n}}end{array}
ight]left[egin{array}{ccc}{frac{partial L}{partial a_{1,1}}} & {cdots} & {frac{partial L}{partial a_{1, m}}} \ {vdots} & {ddots} & {vdots} \ {frac{partial L}{partial a_{n, 1}}} & {cdots} & {frac{partial L}{partial a_{n, m}}}end{array}
ight]

雖然變成矩陣與矩陣相乘了,但是算一下維度就可以發現, 
abla W 的各維度大小並沒有改變,依然是: 
abla W in R^{mathrm{k} 	imes m}

不過它其中的元素的值肯定是變了:


abla W_{k, m}=sum_{n} x_{k, n} frac{partial L}{partial a_{n, m}}

通過公式可以知道,其中每一個梯度的值都變成了多個訓練樣本梯度的總和,所以在更新參數時,應該將梯度除以訓練樣本的數量,如下:

W=W-frac{eta}{n} 
abla W

以上是權重的一些變化,偏置也有一些變化,不過也差不過,就不細說了,下面把修改的代碼貼上:

def backward(self, eta):
if self.require_grad:
batch_size = eta.shape[0]
self.W.grad = np.dot(self.x.T, eta) / batch_size
if self.b is not None: self.b.grad = np.sum(eta, axis=0) / batch_size
return np.dot(eta, self.W.T)

可以看到,和之前(三)裡面的代碼對比,eta變成一個二維數組了,其中第一個維度大小就是訓練樣本的數量(batch_size),然後將W和b的梯度都除以batch_size,其他地方的代碼不需要改變

最後,即使你只需要使用一個樣本進行訓練,上述代碼也是兼容的

上面簡單的討論了一下多批量訓練的情況下的計算過程,接下來是時候進入正題了

在上一篇提到了優化器,使用的優化演算法是SGD(Stochastic gradient descent,隨機梯度下降

梯度下降的概念之前已經說過了,也可百度一下,相關的文章有茫茫多

而隨機梯度下降就是指每次使用一個樣本進行訓練,因為只有一個樣本的計算量,所以每次訓練的速度非常快,但是同樣,也正是因為只有一個樣本,偶然性很大,並不一定很好的複合了訓練集的整體分佈,所以,針對某一個樣本進行訓練,下一輪訓練換一個樣本損失甚至有可能不減反增

所以,可以同時使用多個樣本訓練改良上述優化演算法,例如MNIST訓練集數量為60000,那麼每次訓練都使用這60000個樣本進行訓練,這種方法叫做BGD(Batch gradient descent,批量梯度下降

雖然這樣訓練數據肯定是符合訓練集的整體分佈的,但是缺點也是顯而易見的,速度太慢了

所以,可以採取一種折中的辦法,叫做miniBGD(mini Batch gradient descent,小批量梯度下降

前面的代碼也就是使用的這種方法,其實小批量梯度下降不過是在SGD和BGD之間調整了訓練樣本的數量而已,使用miniBGD演算法,但每次訓練的樣本數為1時,它就變成了SGD,同理,如果你每次都將整個訓練集丟進去,那麼它就化身為BGD了

一般,訓練樣本數(batch_size)可以取2的整數次方,如64,128等等

批量訓練說完了,下面來介紹一下動量優化演算法(Momentum)

在之前討論的miniBGD裏,每一輪訓練的梯度都是獨立的,沒有關聯的,比如第一輪使用64個樣本進行訓練,計算得到的是梯度a,在下一輪訓練中換一批樣本進行訓練,得到了梯度b,顯然梯度a和b之間並沒有什麼關聯,它們都是朝著各自的方向更新參數,所以有個很顯然的問題,舉個栗子

第一輪訓練

上圖中箭頭是梯度的方向,紅點代表損失最小的地方,可以看到現在參數們正朝著正確的方向前進著;然後第二輪,換了一批數據進行訓練

第二輪訓練

第二輪訓練的梯度方向如圖中黑色箭頭,雖然使用了多個訓練樣本進行巡,但是顯然不能完全保證梯度一定是100%朝著損失最小的方向進行,所以,接下來的幾輪訓練裏,可能情況如下:

可以看到,誤差總是不可避免,參數們在曲折的道路上朝著目標前進著,這種誤差是的學習的效率降低了,並且為了避免用力過猛導致梯度更新剛剛越過最佳點,你還需要設置很小的學習率,這就進一步降低了學習的速度,代價就是需要進行更多輪的訓練,如果每一輪訓練都很耗時的話,這就必須要有所改變了

下面來看一下動量優化演算法的過程,同樣,假設在某一次訓練中梯度(設為梯度a)如下:

然後照常進行下一輪訓練,得到新一輪的梯度b。但不同的是,並不是直接將該梯度應用到參數更新中,而是按照一定的比例( eta ,比如0.9),與前一輪的梯度進行相加,得到梯度c,公式為:

c=eta a+left( 1 - eta  
ight)b

然後將梯度c應用到參數更新中

例如梯度b本來如上圖黑色箭頭所示,但是按照動量優化演算法就變成了與前一輪梯度按權重相加:

所以,真正應用到這一參數更新中的梯度如下圖中黑色粗箭頭所示:

然後,再下一輪訓練同樣是重複上述操作,將前一輪的梯度按權重累加進來:

可以看到,即使單次的訓練梯度隨機性很大,但是由於仍然保持著前面幾輪梯度的慣性,使得參數還是能夠大體上朝政正確的方向更新。下面看看公式:

設前一輪的梯隊為 
abla W ,本輪的梯度為 
abla W_{n e w} ,則本輪進行參數更新如下:


abla W=eta 
abla W+(1-eta) 
abla W_{n e w}

W=W-eta 
abla W

貼一下關鍵部分代碼:

def update(self):
self.gradients= self.beta * self.gradients + (1 - self.beta) * p.grad
p.data -= self.lr * self.gradients

完整的代碼在開頭的鏈接中有

另外,有一個小小的問題是,在剛剛開始訓練時,所謂前幾輪的累積梯度顯然為0,這時候的梯度就相當於


abla W=0+(1-eta) 
abla W_{n e w}

所以,剛開始訓練是梯度都會比期望值要小

為瞭解決這一問題,可以進行一些修正:


abla W=frac{
abla W}{1-eta^{t}}

上式中t是訓練的次數;因為 eta 是小於0 的數,所以隨著訓練次數的證據,這個修正的影響會越來越小,而彼時 
abla W 也已經累積起來了

當然,你也可以不進行這一步,直接硬抗幾輪訓練,有時也沒什麼影響

討論完了Momentum後,再來說一下RMSprop(Root Mean Square Prop,均方根反向傳播)演算法

該演算法和Momentum類似,同樣有個 eta 值(一般取0.999或0.99),不同之處在於每次累加的都是新的梯度的平方:


abla W=eta 
abla W+(1-eta) 
abla W_{n e w}^{2}

還有更新參數的公式也不同:

W=W-eta frac{W_{	ext {new}}}{sqrt{
abla W}+epsilon}

上式中 epsilon 是為了防止除0而加的一個很小的數(一般為1e-8),該演算法通過對梯度進行開放,使得參數更新的擺動更小。其實,這個演算法一般我也不怎麼用,之所以提出來是因為下面的一種演算法,該演算法結合了上述兩種方法的優點,相當於二者的合體

Adam優化演算法

該演算法包含兩個參數 eta_1,eta_2 ,然後計算兩個累積梯度:


abla W_1=eta_1 
abla W_1+(1-eta_1) 
abla W_{n e w}


abla W_2=eta_2 
abla W_2+(1-eta_2) 
abla W_{n e w}^{2}

同樣,因為剛開始訓練是累積梯度為0,所以要進行修正:


abla W=frac{
abla W}{1-eta^{t}}


abla W_2=frac{
abla W_2}{1-eta_2^{t}}

最後更新參數的方法聰明如你應該想到了:

W=W-eta frac{
abla W_1}{sqrt{
abla W_2}+epsilon}

代碼就不貼了,在GitHub的鏈接中都有完整的實現

最後,再討論一下關於學習率衰減,比如在進行一定次數的訓練之後,此時損失已經較低了,可以認為此時已經離損失最小的點(全局最優)很近了,如下:

接下來的訓練中梯度雖然有些擺動,但是大體還是朝著全局最優的方向前進

並且由於損失的降低,梯度的幅度也會降低

但是,沒法保證梯度100%的朝著全局最優的方向前進,所以它最後會在全局最優的附近遊盪,而且因為誤差始終存在,所以梯度的幅度並不能進一步減小,有時甚至因為梯度較大剛好越過了最優點

為了減少這種情況的發生,可以在損失較低時降低學習率;回顧一下參數更新的公式

W=W-eta 
abla W

可以看到除了梯度本身之外,學習率是一個控制學習速度的重要參數,在開始是時損失較大可以使用較高的學習率(如0.1),在損失很低後,可以減小學習率,就像車輛在快要接近終點之時慢慢的踩剎車,逐步的降低速度,最後正好停在終點,如下圖所示:


推薦閱讀:
相關文章