文章導讀:
1. 一種基於矩陣運算快速計算神經網絡輸出的方法
2. 關於損失函數的兩個假設
3. Hadamard積 - $s\odot t$
4. 反向傳播算法背后的四個基本方程
5. 四個方程的證明(選學)
6. 反向傳播算法
7. 反向傳播算法的代碼實現
8. 反向傳播為什么被認為是快速的算法?
9. 反向傳播概貌
上一章中我們遺留了一個問題,就是在神經網絡的學習過程中,在更新參數的時候,如何去計算損失函數關於參數的梯度。這一章,我們將會學到一種快速的計算梯度的算法:反向傳播算法。
這一章相較於后面的章節涉及到的數學知識比較多,如果閱讀上有點吃力的話也可以完全跳過這一章,把反向傳播當成一個計算梯度的黑盒即可,但是學習這些數學知識可以幫助我們更深入的理解神經網絡。
反向傳播算法的核心目的是對於神經網絡中的任何weight或bias計算損失函數$C$關於它們的偏導數$\frac{\partial C}{\partial w}$. 這個式子能夠幫助我們知道當我們改變$w$或$b$的時候,損失函數$C$是怎么變化的。雖然計算這個式子可能有一點復雜,但是它提供了一種自然的,直觀的解釋,所以說反向傳播算法並不僅僅是一種快速學習算法,它提供給我們具體的見解,幫助我們理解改變神經網絡的參數是如何改變神經網絡行為的。所以說反向傳播算法是很值得我們去深入學習的。
當然跳過這章也沒問題,把反向傳播當作求梯度的黑盒也並不影響閱讀作者后續的章節。雖然后面會有一些涉及到本章知識的地方,不過跳過這些依然不會影響我們理解文章的主要內容。
一. 一種基於矩陣運算快速計算神經網絡輸出的方法
在介紹反向傳播之前,先介紹怎么利用矩陣運算快速的計算神經網絡輸出。其實在上一章對這一塊也提到過,不過不夠詳細。這里再介紹一下,幫助大家逐漸適應基於矩陣運算的表示方式。
我們先引入一個能夠明確表示連接神經網絡中某兩層之間的某兩個神經元的權重的符號:$w^l_{jk}$,它表示第$l-1$層的第$k$個神經元和第$l$層的第$j$個神經元連接的權重。例如下圖中的$w^3_{24}$表示第二層的第4個神經元到第三層的第2個神經元的權重:

這個符號初看起來可能有點復雜冗長而且需要一點功夫去適應這樣的定義。而且大多數人應該有跟我一樣的疑惑就是為什么不用j表示輸入,k表示輸出,而是這樣反其道而行之。在下文中作者將解釋這樣定義的原因。
同樣的我們可以用類似的符號定義神經元的bias和受到激活后的輸出。我們用$b^l_j$表示第$l$層第$j$個神經元的bias,用$a^l_j$表示第$l$層第$j$個神經元的輸出。類似下圖:

有了這些概念,我們可以把$a^l_j$也就是第$l$層第$j$個神經元的輸出同上一層神經元的輸出聯系起來。
$$a^l_j = \sigma (\sum_k w^l_{jk}a^{l-1}_k + b^l_j) \quad (23)$$
這個式子中的求和部分就是對上一層的神經元輸出進行加權求和的過程。
為了用矩陣的形式重寫這個式子,我們定義一個權重矩陣$w^l$表示與第$l$層所有神經元鏈接的權重,這個矩陣的第$j$行,第$k$列的值$w^l_{jk}$表示第$l-1$層第$k$個神經元與第$l$層第$j$個神經元鏈接的權重。同樣的定義一個bias向量$b^l$表示第$l$層神經元的bias,然后$a^l$表示第$l$層神經元的輸出。
借助於函數向量化的思想,就是將一個函數作用於一個向量上等價於將一個函數分別作用於該向量上每一個分量。例如對於函數$f(x)=x^2$,有:
$$f(\begin{bmatrix} 2\\ 3 \end{bmatrix}) = \begin{bmatrix} f(2)\\f(3)\end{bmatrix}=\begin{bmatrix}4\\9\end{bmatrix} \quad (24)$$
有了這些概念就可以將公式(23)改寫為:
$$a^l = \sigma (w^l a^{l-1} + b^l) \quad (25)$$
去掉了那些表示神經元的序號$j$和$k$,這個式子幫助我們更宏觀的理解上一層神經元的輸出是怎么影響下一次神經元的。我們將權重矩陣乘以上一層神經元輸出,再加上這一層神經元自身的bias,再經過$\sigma$函數得到的就是這一層神經元的輸出。
這也是作者定義$w^l_{jk}$時用$k$表示$l-1$層的神經元序列而不是用$j$表示的原因。試想一下,當我們需要計算第$l$層第一個神經元輸出的時候,需要將矩陣$w^l$的第一行所有值$w^l_{1k}$表示的向量和向量$a^{l-1}$相乘,也就是加權求和,所以說這樣定義更符合我們矩陣運算的規則,否則的話在進行計算的時候還需要將權重矩陣$w^l$轉置帶來不必要的麻煩。
相較於糾結神經元間的聯系,用矩陣表示的話更容易理解和感受層級間的聯系。而且矩陣表示還有個好處就是在實際工程中,有很多快速矩陣運算的實現。
在計算公式(25)的時候,我們可以定義一個$l$層的加權輸入的概念:$z^l = w^l a^{l-1} + b^l$,就是在經過$\sigma$函數輸出之前的部分。公式(25)於是可以表示成$a^l = \sigma (z^l)$。$z^l$當然也是一個向量,其中每個分量$z^l_j$表示第$l$層第$j$個神經元的加權輸入。
二. 關於損失函數的兩個假設
反向傳播算法是為了計算損失函數的偏導數$\frac{\partial C}{\partial w}$和$\frac{\partial C}{\partial b}$,為了使算法可行,我們需要對損失函數的形式作兩個假設。在介紹這些假設之前,我們先來看一個最常見的二次損失函數:
$$C=\frac{1}{2n}\sum_x ||y(x)-a^L (x)||^2 \quad (26)$$
其中$n$是訓練樣本總數,對所有訓練樣本損失求平均,$y(x)$是輸入為$x$時對應的真實的輸出,而$L$表示神經網絡的層數,也就是說$a^L (x)$表示的是輸入為$x$時,神經網絡最后一層輸出層的輸出,也就是神經網絡的輸出。
第一個假設是所有訓練樣本總的損失函數可以被表示成單個樣本損失函數和的平均值,即:$C=\frac{1}{n}\sum_x C_x$. 我們很容易可以驗證這個假設對於二次損失函數成立,$C_x = \frac{1}{2}||y-a^L||^2$。這個假設其實大部分時候都是成立,除了對於少數比較另類的損失函數,不過本文並不涉及。
我們需要這個假設的原因是因為方向傳播算法實際是對於單個樣本計算偏導數$\frac{\partial C_x}{\partial w}$和$\frac{\partial C_x}{\partial b}$,隨后再通過對這些單樣本的偏導數求平均作為$\frac{\partial C}{\partial w}$和$\frac{\partial C}{\partial b}$。事實上,在對$w$和$b$求偏導的時候,我們將輸入$x$當作是固定值,所以方便起見,暫時將$C_x$寫作$C$,后面再寫回來。
第二個假設是損失函數可以表示成神經網絡輸出的函數,即$C = C(a^L)$

例如,二次損失函數就滿足這樣的假設,因為對於一個訓練樣本$x$來說,有:
$$C=\frac{1}{2}||y-a^L||^2=\frac{1}{2}\sum_j (y_j - a^L_j)^2 \quad (27)$$
這樣就表示成了輸出的函數,因為對於一個輸入$x$來說,它實際正確的輸出$y$是個固定值,並不是我們可以修改的變量。我們可以改變的只能是通過改變weights和biases來改變神經網絡的輸出$a^L$從而影響到損失函數的值。
三. Hadamard積 - $s\odot t$
反向傳播算法基於一些常見的線性代數操作:向量的相加,向量與矩陣的積等等。其中有一種操作不是很常見,這里簡單介紹一下。假設$s$和$t$是兩個相同維度的向量,我們使用$s\odot t$定義兩個向量中對應分量相乘的操作,即$(s \odot t)_j = s_j t_j$,例如:
$$\begin{bmatrix} 1\\ 2 \end{bmatrix} \odot \begin{bmatrix} 3\\ 4 \end{bmatrix} = \begin{bmatrix} 1*3 \\ 2*4\end{bmatrix}=\begin{bmatrix}3\\8 \end{bmatrix} \quad (28)$$
這樣的乘法操作被稱為Hadamard積或Schur積。
四. 反向傳播算法背后的四個基本方程
反向傳播算法是關於理解改變weights和biases是如何改變損失函數C,也就是計算$\frac{\partial C}{\partial w^l_{jk}}$和$\frac{\partial C}{\partial b^l_j}$。在介紹如何計算這些偏導數之前,先引入一個中間變量,$\delta^l_j$,稱其為第$l$層第$j$個神經元的誤差error,反向傳播算法會先計算這個中間變量,隨后再將其與需要的偏導數關聯起來。
為了明白這個error的定義,想象在我們的神經網絡中有一個小惡魔:

這個小惡魔位於第$l$層第$j$個神經元處,它會在該神經元接收到輸入的時候使壞,在上一層的加權輸入和傳到這個神經元的時候添加一個改變量$\Delta z^l_j$,導致該神經元的輸出就不再是$\sigma (z^l_j)$而變成了$\sigma (z^l_j + \Delta z^l_j)$。這個改變就會一直傳到下去直到最后一個輸出層,使得總的損失函數改變了$\frac{\partial C}{\partial z^l_j}\Delta z^l_j$。
現在假設這個惡魔是個好惡魔,它會通過找到合適的$\Delta z^l_j$來幫我們減小損失函數的值。假設$\frac{\partial C}{\partial z^l_j}$是一個絕對值大的數(可以是正數或者負數),那我們可以取一個和其正負相反的一個數$\Delta z^l_j$來減小損失函數。但是如果$\frac{\partial C}{\partial z^l_j}$是一個接近0的數,這個惡魔通過影響$z^l_j$來減小損失函數的方式就顯得力不從心了。此時,這個惡魔就會說,這個神經元已經接近最優狀態了(當然這是在$\Delta z^l_j$是相對小的值的情況下的,我們會約束這個惡魔的能力,使其只能進行這些比較小的干擾)。這就有點類似於說損失函數在這個神經元上已經達到極值,沒有繼續優化的空間了。
這就給我們一種直覺說$\frac{\partial C}{\partial z^l_j}$可以作為該神經元error的一種評價方式。基於此,我們定義第$l$層第$j$個神經元的error為:
$$\delta^l_j = \frac{\partial C}{\partial z^l_j}\quad (29)$$
反向傳播算法將會對每一層$l$計算$\delta^l$然后再得到對應的$\frac{\partial C}{\partial w^l_{jk}}$和$\frac{\partial C}{\partial b^l_j}$。
也許我們會有疑問為什么這個惡魔影響的是輸入$z^l_j$而不是直接去影響輸出$a^l_j$,那樣我們就可以用$\frac{\partial C}{\partial a^l_j}$作為我們在神經元上error的衡量。實際上這樣做並不會對結果產生影響,只是會使得反向傳播算法的計算公式更復雜一點而已,所以我們繼續使用$\delta^l_j = \frac{\partial C}{\partial z^l_j}$作為error的衡量。
接下來就要介紹反向傳播算法基於的四個方程了。作者在這強調,這些方程是有難度的,一開始不理解也不要灰心。在這章中,我們會多次學習它們,作者還給出了這些方程的簡單證明和偽代碼實現,並且還一步步將偽代碼實現成了python代碼。通過本章的學習,不光是知道這些公式方程,作者還將會使我們對反向傳播方程有直覺上的理解,還有就是人們是怎么發現這些方程的。在這期間,我們會不斷的提及這四個方程,使我們最終將會對它有更深入的理解。
方程一:輸出層的error$\delta ^L$
$$\delta^L_j = \frac{\partial C}{\partial a^L_j}\sigma '(z^L_j) \quad (BP1)$$
根據定義我們可以驗證$\delta^L_j = \frac{\partial C}{\partial z^L_j} = \frac{\partial C}{\partial a^L_j}\frac{\partial a^L_j}{\partial z^L_j} = \frac{\partial C}{\partial a^L_j}\sigma '(z^L_j)$
注意到這個式子的每個部分都不難計算的到,$z^L_j$和$\sigma '(z^L_j)$在計算神經網絡輸出的時候可以得到,左邊的部分在確定了損失函數的形式之后也可以計算得到。
公式(BP1)是針對輸出層上某一個神經元而言的,為了方便反向傳播計算,我們將其改寫為矩陣形式:
$$\delta^L = \bigtriangledown_aC \odot \sigma '(z^L) \quad (BP1a)$$
對於這里的二次損失函數,有$\bigtriangledown_a C = \bigtriangledown_a (\frac{1}{2}\sum_j (y_j - a^L_j)^2) = a^L - y$, 注意這里求導的過程只是針對當前分量$j$求導,其余的分量就為0.於是有:
$$\delta^L = (a^L - y) \odot \sigma'(z^L) \quad (30)$$
方程二:用當前層error表示下一層error
$$\delta^l = ((w^{l+1})^T\delta^{l+1}) \odot \sigma'(z^l) \quad (BP2)$$
這個方程雖然看上去比較復雜,但是每個部分都有很明確的解釋。假設我們知道$l+1$層的error:$\delta^{l+1}$,當同這一層的權重矩陣相乘的時候就類似於將這個error傳到上一層,最后再利用Hadamard積得到$l$層的error。詳細的推導證明在下一小節中。
有了公式(BP1)和公式(BP2)我們就可以通過先計算輸出層的error進而計算每一層的error。
方程三:error等價於損失函數$C$對bias的變化率
$$\frac{\partial C}{\partial b^l_j} = \delta^l_j \quad (BP3)$$
這說明error $\delta^l_j$等價於$\frac{\partial C}{\partial b^l_j}$。於是可以很容易將其寫成:
$$\frac{\partial C}{\partial b} = \delta \quad (31)$$
方程四:損失函數$C$對weights的變化率
$$\frac{\partial C}{\partial w^l_{jk}} = a^{l-1}_k\delta^l_j \quad (BP4)$$
這個方程說明,當我們要計算某兩個神經元鏈接的權重對損失函數影響的時候,可以先計算上一層的$a^{l-1}_k$和下一層的$\delta^l_j$。而這兩個值我們根據先前的知識已經知道怎么計算了。這個方程也可以被寫為:
$$\frac{\partial C}{\partial w} = a_{in}\delta_{out} \quad (32)$$
更直觀的如下圖所示:

從公式(32)中可以看出來,當$a_{in}$很小$a_{in}\approx 0$的時候,$\partial C/\partial w$也會很小,那樣這個權重的學習就會很慢,意味着在梯度下降的學習過程中,這個權重不會發生大的變化。換句話說就是,輸出小的神經元的權重學習也慢。
介紹完了這四個方程,我們再來聊聊一些關於這四個方程的理解。對於輸出層,考慮公式(BP1)中的$\sigma'(z^L_j)$,我們在第一章學習過$\sigma$函數的圖像,知道它在$\sigma (z^L_j)$接近0或1的時候是趨於平的,也就是導數是趨於0的。所以我們就能得出結論,如果對於輸出層的神經元,如果其輸出非常大($\approx 1$)或非常小($\approx 0$),在這種情況下,其參數的更新是非常緩慢的。我們稱這種狀態的輸出神經元為saturated的狀態,這種狀態下,參數停止更新(或更新很慢)。
根據公式(BP2)我們對其他層可以得出同樣的結論,對任一處於saturated狀態的神經元來說,其$\delta^l_j$趨向於很小的值,然后就會導致它的參數的學習會很慢。(當然要是$w^{l+1}\delta^{l+1}$足夠大,即使乘上一個很小的數也能保證積足夠大的話就不是這種情形了,不過作者這里說明的只是普遍的情況)
總之,就是一個神經元在低激活或者高激活狀態,或者說在saturated狀態時,它的參數的更新就會很慢。
上面的一些理解並不難從觀察方程得到,但是它仍然能夠幫助我們更進一步在腦海中構建神經網絡模型。而且我們隨后會發現,證明上述方程不需要用到任何$\sigma$函數的性質,所以我們可以將激活函數換成任何函數。甚至我們可以根據學習的需要設計自己的激活函數。例如,我們使用一個函數$f$,有$f'>0$恆成立,並且不會接近於0,這樣就可以使神經元避免saturated的狀態,就不會減慢參數的學習。在本書的后續章節,我們的確會看到很多使用自己定義的激活函數的例子,現在先讓我們牢記(BP1)-(BP4)這四個方程,只有這樣到時候才能明白為什么要那樣修改激活函數,這樣修改會造成什么樣的影響。

五. 四個方程的證明(選學)
現在要開始上述四個方程的證明了,這些證明主要用到了多元函數微分的鏈式法則,如果對這塊很熟悉的話,完全可以自己自行證明。
先來公式(BP1)的證明,這個其實在我上面介紹的時候已經證明過了,這里在介紹一下作者是怎么證明的。
公式BP1:$$\delta^L_j = \frac{\partial C}{\partial z^L_j} \quad (36)$$
根據鏈式法則有$\delta^L_j = \sum_k \frac{\partial C}{\partial a^L_k}\frac{\partial a^L_k}{\partial z^L_j} \quad (37)$,但是我們知道對於第$k$個神經元的輸出$a^L_k$只決定於第$k$個神經元的輸入$a^L_k$,所以對於$k \neq j$的情況,導數為0,所以可以去掉求和符號(其他項都為0)最后有:
$$\delta^L_j = \frac{\partial C}{\partial a^L_j}\frac{\partial a^L_j}{\partial z^L_j} \quad (38)$$
由於$a^L_j = \sigma (z^L_j)$,所以上式又可以被寫為:
$$\delta^L_j = \frac{\partial C}{\partial a^L_j}\sigma '(z^L_j) \quad (39)$$
公式BP1就得到了證明。
現在證明BP2.根據鏈式法則可以得到:
$$\delta^l_j = \frac{\partial C}{\partial z^l_j} \quad (40)$$
$$\delta^l_j = \sum_k \frac{\partial C}{\partial z^{l+1}_k} \frac{\partial z^{l+1}_k}{\partial z^l_j} \quad (41)$$
$$\delta^l_j = \sum_k \frac{\partial z^{l+1}_k}{\partial z^l_j} \delta^{l+1}_k \quad (42)$$
又根據神經網絡模型知道:
$$z^{l+1}_k = \sum_j w^{l+1}_{kj}a^l_j+b^{l+1}_k = \sum_j w^{l+1}_{kj} \sigma(z^l_j) + b^{l+1}_k \quad (43)$$
然后將其對$z^l_j$求導得到:
$$\frac{\partial z^{l+1}_k}{\partial z^l_j} = w^{l+1}_{kj}\sigma '(z^l_j) \quad(44)$$
將其帶入公式(42)得到:
$$\delta^l_j = \sum_k w^{l+1}_{kj}\delta ^{l+1}_k\sigma '(z^l_j) \quad (45)$$
這樣就完成了BP2的證明。
BP3和BP4的證明留做練習題。
練習:
問題:
證明方程BP3和BP4
答案:
BP3的證明:
根據鏈式法則展開為$\frac{\partial C}{\partial b^l_j} = \sum_k \frac{\partial C}{\partial z^l_k} \frac{\partial z^l_k}{\partial b^l_j} = \sum_k \delta^l_k \frac{\partial (\sum_j w^l_{kj}a^{l-1}_j + b^l_k)}{\partial b^l_j} = \delta^l_j * 1 = \delta^l_j$
因為只有在$k = j$的時候右邊部分才為1,其余均為0,方程BP3就得到了證明。
BP4的證明:
跟BP3幾乎一模一樣,為了避免誤解,這里的下標稍微改一下,$\frac{\partial C}{\partial w^l_{jk}} = \sum_a \frac{\partial C}{\partial z^l_a} \frac{\partial z^l_a}{\partial w^l_{jk}} = \sum_a \delta^l_a \frac{\partial (\sum_b w^l_{ab}a^{l-1}_b + b^l_a)}{\partial w^l_{jk}}$
很顯然,$\frac{\partial b^l_a}{w^l_{jk}}=0$,然后另一部分只有在$a=j,b=k$時才不為0,此時就可以去掉求和符號得到:
$$\frac{\partial C}{\partial w^l_{jk}} = a^{l-1}_k \delta^l_j$$
方程BP4證明完畢。
六. 反向傳播算法
有了這些方程,我們再看看反向傳播算法是如何進行梯度計算的。
1. 設置輸入層的輸出$a^1$為原始輸入$x$
2. 前向依次計算$l=2,3,...,L$層的加權輸入和輸出:$z^l = w^la^{l-1}+b^l$和$a^l = \sigma (z^l)$
3. 輸出層的error $\delta^L$: $\delta^L = \bigtriangledown_a C \odot \sigma '(z^L)$
4. 反向傳播依次計算$l=L-1, L-2, ..., 2$層的error:$\delta^l = ((w^{l+1})^T\delta^{l+1}) \odot \sigma '(z^l)$
5. 根據上面的結果計算各個梯度:$\frac{\partial C}{\partial w^l_{jk}} = a^{l-1}_k\delta^l_j$和$\frac{\partial C}{\partial b^l_j} = \delta^l_j$
從算法的流程也可以明白為什么它被稱為反向傳播算法,因為我們是從最后一層的$\delta^L$開始反向計算前面的$\delta^l$的。因為損失函數是關於神經網絡輸出的函數,所以為了得到損失函數關於前面層的參數的梯度就需要不斷的應用求導鏈式法則,一層層向前推導得到我們需要的關系的表達式。
練習:
問題一:
假設我們對神經網絡中的一個神經元進行替換,使其激活函數不再是sigmoid函數,而是$f(\sum_jw_jx_j + b)$, 這種情況下反向傳播算法應該如何調整?
答案:
只需要修改用到激活函數的部分,首先是該神經元的輸出$a^l_j$用函數$f$求,其次就是上面在求$\sigma '(z^l)$的時候,現在改為函數$f'(z^l_j)$(只影響其中一個分量,因為只替換了一個神經元)
問題二:
將神經網絡中所有的sigmoid激活函數替換為線性激活函數$\sigma (z) = z$,寫出該情形下的反向傳播算法。
答案:
使用這個激活函數的話,一個顯著特征就是$a=z$和$\sigma '(z^L) = \vec{1}$,然后就可以去掉$\odot$操作,因為右邊的向量各分量都為1。其實算法沒什么變化,只是這些特殊情況的激活函數會簡化算法中某些式子。
之前已經提到過,反向傳播算法是基於每一個樣本計算梯度的即$C = C_x$。實際中,經常會涉及到將多個梯度的情況,例如在隨機梯度下降算法中使用反向傳播。例如給定mini-batch大小為$m$,那么算法的做法是:
1. 先針對mini-batch中每一個訓練樣本進行上述反向傳播的計算,計算各個參數(這樣就會出現m組參數)
2. 在梯度下降更新參數的時候對m組梯度取平均后再應用到更新參數的公式中:
$$w^l \rightarrow w^l - \frac{\eta}{m}\sum_x \delta^{x, l} (a^{x, l-1})^T$$
$$bl \rightarrow b^l - \frac{\eta}{m}\sum_x \delta^{x, l}$$
七. 反向傳播算法的代碼實現
理解了反向傳播算法后,我們再來看前一章相關代碼就更容易理解了。
1 class Network(object): 2 ... 3 def update_mini_batch(self, mini_batch, eta): 4 """Update the network's weights and biases by applying 5 gradient descent using backpropagation to a single mini batch. 6 The "mini_batch" is a list of tuples "(x, y)", and "eta" 7 is the learning rate.""" 8 nabla_b = [np.zeros(b.shape) for b in self.biases] 9 nabla_w = [np.zeros(w.shape) for w in self.weights] 10 for x, y in mini_batch: 11 delta_nabla_b, delta_nabla_w = self.backprop(x, y) 12 nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)] 13 nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)] 14 self.weights = [w-(eta/len(mini_batch))*nw 15 for w, nw in zip(self.weights, nabla_w)] 16 self.biases = [b-(eta/len(mini_batch))*nb 17 for b, nb in zip(self.biases, nabla_b)]
之前已經說過,主要的計算部分在第11行的backprop函數,它返回的其實就是在當前樣本下的$\frac{\partial C_x}{\partial b^l_j}$和$\frac{\partial C_X}{\partial w^l_{jk}}$,然后nabla_b和nabla_w是講對應位置的結果疊加,以供在第14行和第16行更新參數時用到平均梯度時使用。下面是backprop函數的代碼:
1 class Network(object): 2 ... 3 def backprop(self, x, y): 4 """Return a tuple "(nabla_b, nabla_w)" representing the 5 gradient for the cost function C_x. "nabla_b" and 6 "nabla_w" are layer-by-layer lists of numpy arrays, similar 7 to "self.biases" and "self.weights".""" 8 nabla_b = [np.zeros(b.shape) for b in self.biases] 9 nabla_w = [np.zeros(w.shape) for w in self.weights] 10 # feedforward 11 activation = x 12 activations = [x] # list to store all the activations, layer by layer 13 zs = [] # list to store all the z vectors, layer by layer 14 for b, w in zip(self.biases, self.weights): 15 z = np.dot(w, activation)+b 16 zs.append(z) 17 activation = sigmoid(z) 18 activations.append(activation) 19 # backward pass 20 delta = self.cost_derivative(activations[-1], y) * \ 21 sigmoid_prime(zs[-1]) 22 nabla_b[-1] = delta 23 nabla_w[-1] = np.dot(delta, activations[-2].transpose()) 24 # Note that the variable l in the loop below is used a little 25 # differently to the notation in Chapter 2 of the book. Here, 26 # l = 1 means the last layer of neurons, l = 2 is the 27 # second-last layer, and so on. It's a renumbering of the 28 # scheme in the book, used here to take advantage of the fact 29 # that Python can use negative indices in lists. 30 for l in xrange(2, self.num_layers): 31 z = zs[-l] 32 sp = sigmoid_prime(z) 33 delta = np.dot(self.weights[-l+1].transpose(), delta) * sp 34 nabla_b[-l] = delta 35 nabla_w[-l] = np.dot(delta, activations[-l-1].transpose()) 36 return (nabla_b, nabla_w) 37 38 ... 39 40 def cost_derivative(self, output_activations, y): 41 """Return the vector of partial derivatives \partial C_x / 42 \partial a for the output activations.""" 43 return (output_activations-y) 44 45 def sigmoid(z): 46 """The sigmoid function.""" 47 return 1.0/(1.0+np.exp(-z)) 48 49 def sigmoid_prime(z): 50 """Derivative of the sigmoid function.""" 51 return sigmoid(z)*(1-sigmoid(z))
現在稍微解釋一下這段代碼:
8,9行是初始化一下需要計算的梯度值。
14-18行的for循環是計算所有的$z^l_j$和$a^l_j$
20行是計算最后一層的error:$\delta^L$,cost_derivate函數計算$\frac{\partial C}{\partial a^L_j}$,在損失函數為二次函數的情況下為$a^L_j - y_j$, sigmoid_prime函數就是計算sigmoid函數的導數。
22行和23行就是損失函數對輸出層的參數的導數。
30行開始的for循環就是根據那四個方程進行反向傳播的過程。
可見只要了解了那四個方程,反向傳播算法的代碼並不難理解。
作者最后強調他實現的代碼並沒有完全實現矩陣化,因為在mini-batch階段,作者是使用for循環遍歷其中的每個樣本的。實際上這一步是可以依靠矩陣運算實現的,矩陣運算比遍歷for循環要快。實際上,大部分的庫中反向傳播的實現都是依靠矩陣運算而不是循環遍歷的。
八. 反向傳播為什么被認為是快速的算法?
我們之前提到反向傳播是一種快速計算梯度的算法,那么為什么稱它快速呢,這是跟什么比才說它快呢?
為了回答這個問題,我們先看另一種計算梯度的方法。考慮損失函數為$C=C(w)$(先不考慮bias),根據導數的概念我們有:
$$\frac{C}{w_j}\approx \frac{C(w+\epsilon e_j) - C(w)}{\epsilon} \quad (46)$$
其中$\epsilon>0$是一個很小的正數,$e_j$是$j$方向上的一個單位向量。這樣的話通過計算分子上的損失函數C的兩個值,就可以得到我們需要的梯度$\frac{\partial C}{\partial w_j}$,同樣的方法我們可以計算$\frac{\partial C}{\partial b}$。
這個方法看起來很完美,而且代碼實現更加容易,似乎比我們的反向傳播算法更好。但是事實是,當我們嘗試實現使用這種方法的時候,就會發現它的運行效率非常低下。假設我們的神經網絡中有一百萬個權重,那樣的話為了計算梯度肯定就需要計算這一百萬個$C(w+\epsilon e_j)$。但是每次計算這個損失函數的值都必須從輸入層開始一層層計算直到輸出層(每個樣本都需要經過這樣的計算)。除此之外當然還需要計算一百萬個$C(w)$,不過這個只需要計算一次神經網絡的輸出即可得到。
反向傳播算法的聰明之處在於我們只需要一次正向遍歷神經網絡和一次反向遍歷神經網絡,就可以計算出所有的$\frac{\partial C}{\partial w_j}$。正向遍歷計算$a^l_j$和$z^l_j$,反向遍歷計算各個$\delta ^l_j$,然后經過簡單計算就得到了需要的梯度值。所以說雖然從形式上可能會覺得反向傳播算法更復雜,但其實它的計算量更少,算法更高效。
自從反向傳播算法被發現,它就解決了許多神經網絡上的問題。但是反向傳播算法也不是萬能的,尤其是在用來訓練擁有非常多隱藏層的深度神經網絡的時候。我們會在本書的后面章節介紹現代計算機和一些前人聰明的想法是怎么使這樣的深度神經網絡的學習變得可能的。
九. 反向傳播概貌
到目前為止,反向傳播還給我們留下了兩個疑團。首先,這個算法本質上做了些什么,我們已經知道了error從輸出層反向傳播這個關鍵步驟。那我們能否了解的更深入一點,能否建立起一種直覺明白在這些矩陣向量運算背后究竟發生了什么?其次,讀懂反向傳播算法及其證明並不困難,但是讀懂並不意味着你能憑空發現這個算法,是否存在合理的直覺或方法可以引導我們去發現這個算法呢?這一節將會圍繞這兩個疑團進行討論。
為了讓我們更直觀的感受算法的行為,我們想象對神經網絡的參數$w^l_{jk}$進行了微調$\Delta w^l_{jk}$,如下圖:

當然這個變化會引起相關聯的神經元的輸出:

這個神經元的輸出的變化又會引起和它相連接的所有的神經元的變化。就這樣一層層的影響,最后就到輸出層了對損失函數直接造成影響:

其中$\Delta C$與權重改變的關系為:
$$\Delta C \approx \frac{\partial C}{\partial w^l_{jk}}\Delta w^l_{jk}\quad (47)$$
這就給我們帶來一種計算$\frac{\partial C}{\partial w^l_{jk}}$的方法:通過給$w^l_{jk}$引入細微的變化,然后再仔細的追蹤這個變化最終對$C$的影響。我們按照這種思路,從權重改變處一層層往輸出層推導,最后應該就可以計算出$\frac{\partial C}{\partial w^j_{jk}}$。
假設$\Delta w^l_{jk}$會引起第$l$層$j$個神經元的輸出改變$\Delta a^l_j$:
$$\Delta a^l_j \approx \frac{\partial a^l_j}{\partial w^l_{jk}}\Delta w^l_jk \quad (48)$$
這個輸出的變化量$\Delta a^l_j$又將會影響到下一層所有的神經元的輸出。假設我們現在只關心下一層的一個神經元$q$:

該神經元輸出$a^{l+1}_q$的改變為:
$$\Delta a^{l+1}_q \approx \frac{\partial a^{l+1}_q}{\partial a^l_j}\Delta a^l_j \quad(49)$$
將其代入公式(48)得到:
$$\Delta a^{l+1}_q \approx \frac{\partial a^{l+1}_q}{\partial a^l_j}\frac{\partial a^l_j}{\partial w^l_{jk}}\Delta w^l_{jk} \quad (50)$$
然后當然$\Delta a^{l+1}_q$又會影響下一層的神經元的輸出,這樣一層層直到輸出層。我們考慮其中的一條路徑,假設經過的神經元輸出為$a^l_j, a^{l+1}_q, ..., a^{L-1}_n, a^L_m$,那么它對$C$的影響為:
$$\Delta C \approx \frac{\partial C}{\partial a^L_m}\frac{\partial a^L_m}{\partial a^{L-1}_n}\frac{\partial a^{L-1}_n}{\partial a^{L-2}_p}...\frac{\partial a^{l+1}_q}{\partial a^l_j}\frac{\partial a^l_j}{\partial w^l_{jk}}\Delta w^l_{jk}\quad (51)$$
當然這只是所有影響$C$路徑中的一條,為了計算總的變化,我們對所有可能路徑的影響進行求和:
$$\Delta C \approx \sum_{mnp...q} \frac{\partial C}{\partial a^L_m}\frac{\partial a^L_m}{\partial a^{L-1}_n}\frac{\partial a^{L-1}_n}{\partial a^{L-2}_p}...\frac{\partial a^{l+1}_q}{\partial a^l_j}\frac{\partial a^l_j}{\partial w^l_{jk}}\Delta w^l_{jk} \quad (52)$$
然后同公式(47)對比就能得到:
$$\frac{\partial C}{\partial w^l_{jk}} = \sum_{mnp...q} \frac{\partial C}{\partial a^L_m}\frac{\partial a^L_m}{\partial a^{L-1}_n}\frac{\partial a^{L-1}_n}{\partial a^{L-2}_p}...\frac{\partial a^{l+1}_q}{\partial a^l_j}\frac{\partial a^l_j}{\partial w^l_{jk}} \quad (53)$$
方程(53)看上去挺復雜,但是它有一個非常好的直觀解釋。這個方程告訴我們,當我們在計算$C$對於權重的變化率時,任意兩個神經元之間的連接都相當於引入了一個變化率,這個變化率就是兩個被連接的神經元的輸出之間的導數即$\frac{\partial a^{l+1}_q}{\partial a^l_j}$, 然后每一條路徑帶來的變化就是這些值的積,總的變化就是所有路徑帶來變化的和,如下圖:

上面給大家提供一種思考,就是當你影響神經網絡一個參數的時候,這期間會發生什么並進而影響最后的損失函數。讓我們簡述一下有這些知識你還可以做哪些更進一步的討論。首先,我們可以獲得方程(53)中所有單個偏導數的明確計算式,這只需要一些積分即可。然后就可以將這個求和的運算改成矩陣相乘的形式。你就會漸漸發現我們現在做的就是反向傳播所做的事情,所以說反向傳播算法可以被認為是計算所有路徑上偏導數積的和的一種方法。或者說,反向傳播提供了一種追蹤權重變化對神經網絡輸出影響的途徑。
反向傳播算法的介紹就到這里了,之前還看到過一篇脫離神經網絡單獨介紹反向傳播的好的博文,以后再抽時間介紹。
