誤差反向傳播法--高效計算權重參數的梯度

目錄

前言

 計算圖

計算圖求解示例

計算圖的優點

反向傳播

思考一個問題

鏈式法則

計算圖的反向傳播

鏈式法則和計算圖

加法節點的反向傳播

乘法節點的反向傳播

“購買水果”問題的反向傳播

激活函數(層)的反向傳播

 激活函數ReLU的反向傳播

激活函數Sigmoid的反向傳播

Affine/softmax激活函數的反向傳播

Affine層

softmax-with-loss層的反向傳播

誤差反向傳播法的Python實現

乘法層的Python實現

加法層的Python實現

購買水果問題的Python實現

激活函數層的Python實現

ReLU層的Python實現

sigmoid層的Python實現

Affine層的Python實現

softmax-with-loss層的Python實現

誤差反向傳播法的Python總體實現

小結


前言

計算梯度的傳統方法一般採用基於數值微分實現,如下式:

                                                    

\frac{df\left ( x \right )}{dx}=\lim_{h\rightarrow 0}\frac{f\left ( x+h \right )-f\left ( x \right )}{h}

雖然數值微分方法比較直觀、簡單、易於理解,但是計算比較費時間,不適合需頻繁計算導數的場合,比如多層神經網絡中權重參數的梯度計算。前面我們講過利用數值微分計算了神經網絡中的損失函數關於權重參數的梯度\frac{\partial L}{\partial W},而本專題我們將介紹“誤差反向傳播法”,實現損失函數關於權重參數梯度\frac{\partial L}{\partial W}的高效計算。本主題將從簡單的問題開始,逐步深入,最終直達誤差反向傳播法。

理解誤差反向傳播法,一般有基於數學式的方法和基於計算圖的方法。前者比較常見、簡潔和嚴密,但是不直觀,因此筆者選擇了更加直觀、易於理解的計算圖來講解誤差反向傳播法。

 計算圖

 計算圖,即將計算過程用圖形表示出來,當然,這裏的圖形指的是具有數據結構(和流程圖類似的圖形),一般有節點和連接節點的邊(線)組成,如圖1所示。

圖1 計算圖

 圖1中圓圈O表示節點,圈內的符號表示計算符號(比如加減乘除),箭頭的指向表示節點計算結果的傳遞方向(圖1爲從左向右傳播,即正向傳播),直線上方一般放置中間計算結果(如100)。下面我們基於計算圖來分析一個具體的示例。

計算圖求解示例

問題描述:

小明在超市買了2個蘋果、3個橘子。其中,蘋果每個100日元,橘子每個150日元。消費稅是10%,請計算支付金額。

首先,我們採用傳統的數學思路來計算支付金額:2個蘋果,單價爲100日元,因此購買蘋果花了200日元;3個橘子,單價150日元,因此,夠買橘子花了450日元;因此購買蘋果和橘子一共花了650日元,由於消費稅是10%,所以支付金額爲:650+650·10%=715日元。

下面我們採用基於計算圖來計算支付金額,先直接給出計算圖如圖2所示,再分析。

圖2  基於計算圖計算支付金額

 

由計算圖分析得到的結果和傳統方法分析得到的結果一樣,均爲715日元。現在我們結合圖2來分析該計算圖,計算圖的輸入有蘋果單價、蘋果數量、橘子單價、橘子數量、消費稅,中間的計算結果均放置(保存)在直線上面,這裏需要強調的是初始輸入量的值我們也視爲中間結果,比如蘋果單價視爲輸入量(實際等效於一個變量),而100爲中間變量(等效於一個實例值)。改圖一共有4個節點,其中3節點運算爲乘法運算,1個節點運算爲加法運算。計算方向爲從左至右,這是一種正方向的傳播,簡稱爲正向傳播。指向節點的箭頭可以視爲輸入,比如上圖中輸入至加法節點的有兩個輸入量(蘋果總價和橘子總價),當然輸入量是沒有限制的。圖中消費稅的中間計算過程爲1.1(1+10%),這麼做的目的主要是可以直接和水果總價進行乘法運算,當然讀者可以自行設計圖2中的消費稅這一節點。

我們不難理解,正向傳播就是從計算圖出發點到結束點的傳播,既然有正向傳播,那麼應該也有反向傳播。是的,從右向左的傳播就是我們後面將重點關注的反向傳播

計算圖的優點

通過上面給出的計算圖求解實際問題的示例可知,計算圖的特徵是可以通過傳遞“局部計算”獲得最終結果。所謂“局部”,指的是無論全局發生了什麼,都能只根據與自己相關的信息輸出接下來的結果。假設上面的例子中,購買水果總共花費了650日元,我們不關心650日元是通過什麼樣的計算得到的,只關心把650日元作爲該節點的輸出並和其他節點進行運算。換句話說,各個節點處只需進行與自己有關的計算,不用考慮全局。無論全局是多麼複雜的計算,都可以通過局部計算使各個節點致力於簡單的計算,從而簡化問題。

另一個優點是,利用計算圖可以將中間的計算結果全部保存起來(比如200、450、650....),爲反向傳播的計算提供已知數據。

反向傳播

思考一個問題

在上面的問題中,我們計算了購買蘋果和橘子時加上消費稅最終需要支付的金額。假設我們想知道蘋果價格的上漲會在多大程度上影響最終的支付金額,即求“支付金額關於蘋果價格的導數”。設蘋果的價格爲x,支付金額爲L,則相當於求\frac{\partial L}{\partial x}。這個導數的值表示當蘋果的價格稍微上漲時,支付金額會增加多少。

首先,我們利用傳統的數學解題思路來求解,假設蘋果價格上漲了\Delta x日元,支付金額增加了\Delta L日元,則有:

                     \Delta L=\left \{ \left ( 100+\Delta x \right )\cdot 2+150\cdot 3 \right \}\cdot 1.1-\left ( 100\cdot 2+150\cdot 3 \right )\cdot 1.1=2.2\Delta x

通過數學解題思路,我們得到了支付金額關於蘋果的價格的導數爲2.2,即蘋果價格上漲1日元,則最終的支付金額將會增加2.2日元。現在我們先直接給出利用反向傳播法分析得到的結果。圖中加粗的箭頭表示反向傳播,箭頭下面的結果表示“局部導數”,也就是說,反向傳播傳遞的是導數。從圖中可知,支付金額關於蘋果單價的導數的值是2.2,這和數學解題思路得到的答案一樣。當然,除了求關於蘋果的價格的導數,其他的比如支付金額關於消費稅的導數、支付金額關於橘子價格的導數等問題也可以採用同樣的方式算出來。

圖3  反向傳播求支付金額關於蘋果單價的導數

從圖3中還可發現,計算中途求得的導數的結果(比如1.1)可以被共享,從而高效地計算多個導數。因此,計算圖可以通過正向傳播和反向傳播高效地計算各個變量的導數值。反向傳播傳遞導數的原理,是基於鏈式法則。

鏈式法則

反向傳播將局部導數從右到左進行傳遞的原理是基於鏈式法則,要理解鏈式法則,我們還得從複合函數說起。複合函數是由多個函數構成的函數。比如z=\left ( x+y \right )^{2}是由下面的兩個式子構成的。

                                                         z=t^{2}

                                                         t=x+y                                                                          (1)

這裏,鏈式法則是關於符合函數的導數的性質,如下:

如果某個函數由複合函數表示,則該複合函數的導數可以用構成複合函數的各個函數的導數的乘積表示。

例如,\frac{\partial z}{\partial x}可以用\frac{\partial z}{\partial t}\frac{\partial t}{\partial x}的乘積表示。即:

                                                                 \frac{\partial z}{\partial x}=\frac{\partial z}{\partial t}\frac{\partial t}{\partial x}

現在使用鏈式法則,我們來求式(1)的導數\frac{\partial z}{\partial x}。首先要求它的局部導數:

                                               \frac{\partial z}{\partial t}=2t

                                              \frac{\partial t}{\partial x}=1                                                                                     (2)

所以\frac{\partial z}{\partial x}的導數爲:

                                        \frac{\partial z}{\partial x}=\frac{\partial z}{\partial t}\frac{\partial t}{\partial x}=2t\cdot 1=2\left ( x+y \right )

計算圖的反向傳播

假設存在y=f\left ( x \right )的計算,則這個計算的反向傳播如圖4所示。

圖4  計算圖的反向傳播

如圖所示,反向傳播的計算順序是:將信號E乘以節點的局部導數\frac{\partial y}{\partial x},然後將結果傳遞給下一個節點。這裏所說的局部導數是指正向傳播中y=f\left ( x \right )的導數,也就是\frac{\partial y}{\partial x},比如y=f\left ( x \right )=x^{2},則局部導數爲\frac{\partial y}{\partial x}=2x。把這個局部導數乘以上游傳過來的值(本例中的E),然後傳遞給前面的節點。(這裏給大家說一下,如果是神經網絡,那麼最上游應該是損失函數)。

這就是反向傳播的計算程序,結合鏈式法則可以高效地求出多個導數的值。

鏈式法則和計算圖

現在我們用計算圖的方法把式(1)的鏈式法則表示出來。如圖5所示,這裏我們用“**2”表示平方運算。

圖5  式(2)的計算圖:沿着與正方向相反的方向,乘上局部導數後傳遞

反向傳播時,“**2”節點的輸入是\frac{\partial z}{\partial z},將其乘以局部導數\frac{\partial z}{\partial t}(因爲正向傳播時輸入是t,輸出是z,所以這個節點的局部導數是\frac{\partial z}{\partial t}),然後傳遞給下一個節點。這裏需要提醒的是,反向傳播最開始的信號\frac{\partial z}{\partial z}在前面的數學式中沒有出現,因爲\frac{\partial z}{\partial z}=1。根據鏈式法則,最左邊的反向傳播結果\frac{\partial z}{\partial z}\frac{\partial z}{\partial t}\frac{\partial t}{\partial x}=\frac{\partial z}{\partial t}\frac{\partial t}{\partial x}=\frac{\partial z}{\partial x}成立,對應於“z關於x的導數”。

現在我們把式(2)的結果代入到圖5中,可得\frac{\partial y}{\partial x}=2\left ( x+y \right ),如圖6所示。

圖6  反向傳播的結果

到這裏,讀者也許會生產疑問:反向傳播過程中的數字1是怎麼得到的,下面的內容將爲大家解釋這個問題。

加法節點的反向傳播

加法節點指的是節點運算爲加法運算,以 z=x+y爲例,則z關於xy的導數爲

                                                      \frac{\partial z}{\partial x}=1

                                                     \frac{\partial z}{\partial y}=1

假設 z=x+y通過某種運算的結果爲L,則加法節點的正向傳播和反向傳播的計算圖如下:

圖7 加法節點的反向傳播將上游的值原封不動地輸出到下游

 我們通過解析性求導,得到z關於xy的導數均爲1,因此計算圖中,反向傳播將上游傳過來的導數值(本例中是\frac{\partial L}{\partial z},因爲正向傳播的輸入爲z,輸出爲L)乘以1,然後傳向下遊。也就是說,加法節點的反向傳播只乘以1,所以輸入的值會原封不動地流向下一個節點。

乘法節點的反向傳播

假設有z=xy,則z關於xy的導數爲:

                                                                             \frac{\partial z}{\partial x}=y

                                                                             \frac{\partial z}{\partial y}=x

用計算圖表示乘法節點的正向傳播和反向傳播如圖8所示。

圖8 乘法節點反向傳播將上游的值乘以正向傳播時的輸入信號的“翻轉值”後傳遞給下游

 乘法的反向傳播會將上游的值乘以正向傳播時的輸入信號的'翻轉值"後傳遞給下游。翻轉值表示一種翻轉關係,正向傳播時信號是x的話,反向傳播時則是y;正向傳播時信號是y的話,反向傳播時則是x。這裏需要提醒大家的是,加法的反向傳播只是將上游的值傳遞給下游,並不需要正向傳播的輸入信號。而乘法的反向傳播需要正向傳播時的輸入信號值,因此要實現乘法節點的反向傳播時,需要保存正向傳播的輸入信號。

“購買水果”問題的反向傳播

現在我們回到前面給出的問題“購買水果,求支付金額”,因爲我們已經介紹了加法和乘法的反向傳播,所以我們試着來分析“購買水果”的反向傳播,即求包括金額關於蘋果單價的導數等其他變量的導數。讀者只需記住兩點:加法的反向傳播將上游傳遞來的值會原封不動地傳遞給下游;乘法的反向傳播會將輸入信號翻轉後傳遞給下游。因此“購買水果”的反向傳播的計算圖如圖9所示。

圖9購買水果的反向傳播

可知,蘋果的價格的導數爲2.2,橘子的價格的導數爲3.3(說明橘子的價格的波動比蘋果價格的波動對最終的支付金額的影響更大),消費稅的導數是650(消費稅的1是100%,水果的價格的1是1日元,所以才形成了這麼大的消費稅的導數)。

激活函數(層)的反向傳播

 激活函數ReLU的反向傳播

激活函數ReLU的表達式如下式(3):

                                       y=\left\{\begin{matrix} x & \left ( x>0 \right )\\ 0& \left ( x\leq 0 \right ) \end{matrix}\right.                                                 (3)

y關於x的導數如式(4):

                                      \frac{\partial y}{\partial x}=\left\{\begin{matrix} 1 & \left ( x>0 \right )\\ 0& \left ( x\leq 0 \right ) \end{matrix}\right.                                                 (4)

由式(4)可知,如果正向傳播時的輸入x大於0,則反向傳播會將上游的值原封不動地傳遞給下游。如果正向傳播時的x小於等於0,則反向傳播中傳給下游的信號將停止在此處,即反向傳播的值爲0。用計算圖表示如圖9所示。

圖9 ReLU層的計算圖

激活函數Sigmoid的反向傳播

sigmoid函數的表達式如式(5)所示。

                                                  y=\frac{\mathrm{1} }{\ 1+exp\left ( -x \right )}                                    (5)

其計算圖如圖10所示。

圖10 sigmoid函數的計算圖

說明一下,式(5)的計算由局部計算的傳播構成,“exp”節點會進行y=exp\left ( x \right )的計算,“/”會進行y=\frac{\mathrm{1} }{\ x}的計算。下面我們來分析圖10的計算圖的反向傳播。

第一步:

節點“/”表示y=\frac{\mathrm{1} }{\ x}的計算,則它的導數如式(6)所示。

                                                                    \frac{\partial y}{\partial x}=-\frac{\mathrm{1} }{\ x^{2}}=-y^{2}                                                        (6)

可知,“/”節點運算時的反向傳播會將上游的值乘以-y^{2}(正向傳播的輸出的平方乘以-1後的值)後,再傳給下游。計算圖如圖11所示。

圖11 除法節點的反向傳播的計算圖

第二步:

“+”節點將上游的值原封不動地傳給下游。計算圖如圖12所示。

圖 12 加法節點的反向傳播的計算圖

第三步:

“exp”節點表示y=exp\left ( x \right ),則它的導數如式(7)所示。

                                                     \frac{\partial y}{\partial x}=exp(x)=y                                                      (7)

可知,“exp”節點的反向傳播將上游的值乘以正向傳播時的輸出y(這個例子的輸出是exp\left ( -x \right ))後,再傳給下游。計算圖如圖13所示。

圖13 指數運算節點的反向傳播的計算圖

第四步:

“x”節點的反向傳播將正向傳播時的值翻轉後做乘法運算,因此計算圖如圖14所示。

圖14  sigmoid函數的反向傳播的計算圖

綜上,sigmoid函數的反向傳播的輸出爲\frac{\partial L}{\partial y}y^{2}exp(-x),這個值會傳遞給下游的節點。我們發現,\frac{\partial L}{\partial y}y^{2}exp(-x)該值可只根據正向傳播時的輸入x和輸出y就可以計算出來。所以,sigmoid函數的反向傳播可以簡化爲如圖15所示的計算圖。

圖15  sigmoid函數的反向傳播的計算圖(簡潔版)

簡潔後的反向傳播可以忽視中間計算過程,因此大幅度提高了計算效率。其實,我們可以對\frac{\partial L}{\partial y}y^{2}exp(-x)作進一步的處理,如式(8)所示。

因此,sigmoid函數的反向傳播只需根據正向傳播的輸出就能計算出來,這裏我們選擇圖16所示的計算圖作爲sigmoid函數的反向傳播的最終計算圖。

圖 16 sigmoid函數的計算圖:只需正向傳播的輸出y計算反向傳播

Affine/softmax激活函數的反向傳播

Affine層

在前面的專題講解中,我們介紹了計算加權信號的總和,即輸入信號x與權重w的乘積之和,再加上偏置b。在實現過程中,我們利用了矩陣的乘積運算(Numpy庫中的np.dot())來計算了神經元(節點)加權和,即Y=np.dot(X,W)+B,然後將Y經激活函數轉換後,傳遞給下一層。這就是神經網絡的正向傳播的流程。一般地,神經網絡的正向傳播涉及矩陣的乘積運算(信號的加權和計算)的過程(變換),我們稱爲Affine層

Affine層:

神經網絡的正向傳播中進行的矩陣的乘積運算在幾何學領域被稱爲“仿射變換”,它包括一次線性變換和一次平移,分別對應神經網絡的加權和運算\sum xw與加偏置預算\left ( \sum xw\right )+b。在這裏,我們將進行仿射變換的處理實現爲“Affine”層。

圖17爲神經網絡正向傳播的Affine層的計算圖,我們需要注意的是,圖中的變量均爲矩陣形式,所以在進行矩陣運算時,要注意矩陣的形狀是否正確。這裏我們假設了各變量矩陣的形狀,注意這裏的計算圖中各節點間傳遞的是矩陣,不是標量。

圖17 Affine層的計算圖

通過Affine層的正向傳播,我們如何求它的反向傳播呢?在這裏我們需要記住兩點:第一點是x、w、bwb均爲變量,不是常量;第二點是節點中的運算步驟和以標量爲對象的計算圖相同。因此,我們很容易得到如圖18所示的反向傳播的計算圖。

圖18  Affine層的反向傳播

圖18中的反向傳播的加法節點將上游傳遞來的值原封不動地傳遞給下游。"dot"節點可以看做乘法節點,但又有區別,即它是矩陣乘法,所以在考慮將上游傳遞來的值乘以正向傳播的翻轉值的同時,還要注意矩陣的形狀。這裏我們可以肯定的是:\frac{\partial L}{\partial x}\frac{\partial L}{\partial Y}W的某種乘積關係,而\frac{\partial L}{\partial W}\frac{\partial L}{\partial Y}X的某種乘積關係。因此,仔細分析可知:

W^{T}W的轉置,比如W的形狀爲(2,3),則W^{T}的形狀就是(3,2)。所以圖18中Affine層的反向傳播的完整的計算圖如圖19所示:

圖19 Affine層的反向傳播的計算圖

當然,這裏介紹的Affine層的輸入X是以單個數據爲對象的,如果我們將N個數據樣本(假設數據的特徵有2個,則X的形狀爲(N,2))一起進行正向傳播,即批版本的Affine層。那麼它的計算圖如圖20所示。

圖20 批版本的Affine層的計算圖

softmax-with-loss層的反向傳播

神經網絡涉及輸入信號與權重參數的乘積的加權和(即Affine層)、激活函數、輸出層激活函數(softmax)和損失函數(主要使用交叉熵誤差)。在這之前,我們已經介紹了Affine層和激活函數的反向傳播,下面我們將softmax層和損失函數一起作爲對象來分析它們的反向傳播的計算圖。在這之前,我們以手寫數字識別爲例,回顧神經網絡的推理過程。示意圖如圖21所示。

圖21 手寫數字識別信號傳遞過程

圖21中,softmax層將輸入值正規化(輸出值的和調整爲1)之後再輸出,此外,手寫數字識別要進行10類分類,所以向softmax層的輸入也有10個。輸入圖像爲“0”,得分爲10.1分,經softmax層轉換爲0.991。

一般情況下,我們會把softmax層和損失函數一起考慮,由於softmax-with-loss層比較複雜,這裏我們直接給出其正向和反向傳播的簡易計算圖如圖22所示。具體的分析過程後面我們會專門花一個專題來講。

圖22 簡易版的softmax-with-loss層的計算圖

這裏我們重點關注反向傳播的結果。softmax層的反向傳播得到了(y_{1}-t_{1},y_{2}-t_{2},y_{3}-t_{3})這樣漂亮的結果。由於(y_{1},y_{2},y_{3})是softmax層的輸出,\left (t_{1},t_{2},t_{3} \right )是監督數據,所以(y_{1}-t_{1},y_{2}-t_{2},y_{3}-t_{3})是softmax層的輸出和監督標籤的差分。神經網絡的反向傳播會把這個差分表示的誤差傳遞給前面的層,這是神經網絡學習中的重要性質。

神經網絡的學習的目的就是通過調整權重參數,使神經網絡的輸出(softmax層的輸出)接近監督標籤。因此,必須將神經網絡的輸出與監督標籤的誤差高效地傳遞給前面的層。前面的(y_{1}-t_{1},y_{2}-t_{2},y_{3}-t_{3})直截了當地表示了當前神經網絡的輸出與監督標籤的誤差。比如監督標籤(0,1,0),softmax層的輸出是(0.3,0.2,0.5)。由於正確解標籤處的概率是20%,這時候神經網絡未能進行正確的識別。此時,softmax層的反向傳播傳遞的是(0.3,-0.8,0.5)這樣一個大的誤差。這個大的誤差會向前面的層傳播,所以softmax層前面的層會從這個大的誤差中學習到“大”的內容。

使用交叉熵誤差作爲softmax函數的損失函數後,反向傳播得到(y_{1}-t_{1},y_{2}-t_{2},y_{3}-t_{3})這樣漂亮的結果。實際上,這樣的結果並不是偶然的,而是爲了得到這樣的結果,特意設計了交叉熵誤差函數。

誤差反向傳播法的Python實現

乘法層的Python實現

這裏我們把乘法節點的計算圖用“乘法層”(MulLayer),在Python中用類表示,類中有兩個方法(函數),正向傳播forward(),和反向傳播backward()。代碼如下:

# coding: utf-8


class MulLayer:
    def __init__(self):
        self.x = None
        self.y = None

    def forward(self, x, y):
        self.x = x
        self.y = y                
        out = x * y

        return out

    def backward(self, dout):
        dx = dout * self.y     #翻轉x和y
        dy = dout * self.x

        return dx, dy

代碼中,__init__()會初始化實例變量x和y,它們主要用來保存正向傳播時的輸入值。forward()接收x和y兩個參數,將它們相乘後輸出。backward()將從上游傳來的導數dout乘以正向傳播的翻轉值,然後傳給下游。

加法層的Python實現

# coding: utf-8



class AddLayer:
    def __init__(self):
        pass

    def forward(self, x, y):
        out = x + y

        return out

    def backward(self, dout):
        dx = dout * 1
        dy = dout * 1

        return dx, dy

由於加法節點的反向傳播不需要輸入值,所以__init()__中無特意執行語句。forward()接收x和y,將它們相加後輸出。backword()將上游傳來的導數dout原封不動地傳遞給下游。

購買水果問題的Python實現

# coding: utf-8

apple = 100
apple_num = 2
orange = 150
orange_num = 3
tax = 1.1

# layer
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()

# forward
apple_price = mul_apple_layer.forward(apple, apple_num)  # (1)
orange_price = mul_orange_layer.forward(orange, orange_num)  # (2)
all_price = add_apple_orange_layer.forward(apple_price, orange_price)  # (3)
price = mul_tax_layer.forward(all_price, tax)  # (4)

# backward
dprice = 1
dall_price, dtax = mul_tax_layer.backward(dprice)  # (4)
dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price)  # (3)
dorange, dorange_num = mul_orange_layer.backward(dorange_price)  # (2)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)  # (1)

print("price:", int(price))  #715
print("dApple:", dapple)     #2.2
print("dApple_num:", int(dapple_num))  #110
print("dOrange:", dorange)             #3.3
print("dOrange_num:", int(dorange_num)) #165
print("dTax:", dtax)                    #650

激活函數層的Python實現

ReLU層的Python實現

# coding: utf-8


class Relu:
    def __init__(self):
        self.mask = None

    def forward(self, x):
        self.mask = (x <= 0)
        out = x.copy()
        out[self.mask] = 0

        return out

    def backward(self, dout):
        dout[self.mask] = 0
        dx = dout

        return dx

需要提醒大家的是,神經網絡的層的實現中,一般假定forward()和backward()的參數是numPy數組。代碼中變量mask是由true/false構成的NumPy數組,它會正向傳播時的輸入x的元素中小於等於0的地方保存爲true,大於0的地方保存爲false。

sigmoid層的Python實現

class Sigmoid:
    def __init__(self):
        self.out = None

    def forward(self, x):
        out = sigmoid(x)
        self.out = out
        return out

    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.out

        return dx

正向傳播時將輸出保存到了變量out中,反向傳播時,使用該變量out進行計算。

Affine層的Python實現

class Affine:
    def __init__(self, W, b):
        self.W =W
        self.b = b
        
        self.x = None
        self.original_x_shape = None
        # 權重和偏置參數的導數
        self.dW = None
        self.db = None

    def forward(self, x):
        # 對應張量
        self.original_x_shape = x.shape
        x = x.reshape(x.shape[0], -1)
        self.x = x

        out = np.dot(self.x, self.W) + self.b

        return out

    def backward(self, dout):
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)
        
        dx = dx.reshape(*self.original_x_shape)  # 還原輸入數據的形狀(對應張量)
        return dx

需要注意的是,Affine的實現考慮了輸入數據爲張量(四維數據)的情況。

softmax-with-loss層的Python實現

# coding: utf-8
import numpy as np 

def softmax(x):
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis=0)
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        return y.T 

    x = x - np.max(x) # 溢出對策
    return np.exp(x) / np.sum(np.exp(x))

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    # 監督數據是one-hot-vector的情況下,轉換爲正確解標籤的索引
    if t.size == y.size:
        t = t.argmax(axis=1)
             
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size


class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None
        self.y = None # softmax的輸出
        self.t = None # 監督數據

    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)
        
        return self.loss

    def backward(self, dout=1):
        batch_size = self.t.shape[0]
        if self.t.size == self.y.size: # 監督數據是one-hot-vector的情況
            dx = (self.y - self.t) / batch_size
        else:
            dx = self.y.copy()
            dx[np.arange(batch_size), self.t] -= 1
            dx = dx / batch_size
        
        return dx

誤差反向傳播法的Python總體實現

神經網絡中有合適的權重和偏置,調整權重和偏置以便擬合訓練數據的過程稱爲學習。神經網絡的學習一般分爲以下四個步驟:

(1)從訓練數據中隨機選擇一部分數據

(2)計算損失函數關於各個權重參數的梯度(採用誤差反向傳播法)

(3)將權重參數沿梯度方向進行微小的更新

(4) 重複步驟1至步驟3

下面的代碼完成了2層神經網絡的實現 

# coding: utf-8
import numpy as np
from collections import OrderedDict
# coding: utf-8
import numpy as np

#這裏被調用的部分函數可在之前的專題中查找




def numerical_gradient(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)
    
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]
        x[idx] = float(tmp_val) + h
        fxh1 = f(x) # f(x+h)
        
        x[idx] = tmp_val - h 
        fxh2 = f(x) # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2*h)
        
        x[idx] = tmp_val # 還原值
        it.iternext()   
        
    return grad


class TwoLayerNet:

    def __init__(self, input_size, hidden_size, output_size, weight_init_std = 0.01):
        # 初始化權重
        self.params = {}
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size) 
        self.params['b2'] = np.zeros(output_size)

        # 生成層
        self.layers = OrderedDict()
        self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
        self.layers['Relu1'] = Relu()
        self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])

        self.lastLayer = SoftmaxWithLoss()
        
    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)
        
        return x
        
    # x:輸入數據, t:監督數據
    def loss(self, x, t):
        y = self.predict(x)
        return self.lastLayer.forward(y, t)
    
    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        if t.ndim != 1 : t = np.argmax(t, axis=1)
        
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy
        
    # x:輸入數據, t:監督數據
    def numerical_gradient(self, x, t):
        loss_W = lambda W: self.loss(x, t)
        
        grads = {}
        grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
        
        return grads
        
    def gradient(self, x, t):
        # forward
        self.loss(x, t)

        # backward
        dout = 1
        dout = self.lastLayer.backward(dout)
        
        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 設定
        grads = {}
        grads['W1'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
        grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db

        return grads

代碼中使用了OrderDict()函數,它是有序字典,即它可以記住向字典裏添加元素的順序。因此,神經網絡的正向傳播只需按照添加元素的順序調用各層的forward()方法就可以完成處理,而反向傳播只需要按照相反的順序調用各層即可。

我們構造了神經網絡之後,就可以進行學習了,在前面的專題我們講過神經網絡的學習,其中介紹了用數值微分的方法求梯度,而這裏我們則採用誤差反向傳播法求梯度。除此之外,程序幾乎一樣。神經網絡的學習的Python實現如下:

# 讀入數據
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1

train_loss_list = []
train_acc_list = []
test_acc_list = []

iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    # 梯度
    #grad = network.numerical_gradient(x_batch, t_batch)  #之前講過的數值微分求梯度函數
    grad = network.gradient(x_batch, t_batch)            #誤差反向傳播法求梯度
    
    # 更新
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]
    
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
    
    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print(train_acc, test_acc)

小結

 本章我們介紹了計算圖,並使用計算圖介紹了神經網絡的誤差反向傳播法,並以層爲單位實現了神經網絡中的處理。通過將數據正向和反向地傳播,可以高效地計算權重參數的梯度。

歡迎關注微信公衆號“Python生態智聯”,學知識,享生活!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章