使用python實現深度神經網絡 3(轉)


 

使用python實現深度神經網絡 3

快速計算梯度的魔法--反向傳播算法

快速計算梯度的魔法--反向傳播算法

一、實驗介紹

1.1 實驗內容

第一次實驗最后我們說了,我們已經學習了深度學習中的模型model(神經網絡)、衡量模型性能的損失函數和使損失函數減小的學習算法learn(梯度下降算法),還了解了訓練數據data的一些概念。但是還沒有解決梯度下降算法中如何求損失函數梯度的問題。

本次實驗課,我們就來學習一個能夠快速計算梯度的算法--反向傳播算法(backpropogate algorithm),這個算法在神經網絡中非常重要,同時這個算法也非常巧妙,非常好玩。

我們還會在本次實驗課中用代碼實現反向傳播算法。

1.2 實驗知識點

  • 鏈式法則與“計算圖”的概念
  • 反向傳播算法

1.3 實驗環境

  • python 2.7
  • numpy 1.12.1

二、實驗步驟

2.1 計算梯度的數值方法

第一次實驗我留的一個課后作業里問你是否能夠想出一個求解梯度的辦法,其實不難想到一種簡單的辦法就是使用“數值法”計算梯度。
辦法很簡單,就是對於損失函數中的一個初始取值為a0的參數a,先計算當前的損失函數值J0,再保持其他參數不變,而使a改變一個很小的量,比如變成a0+0.000001,再求改變之后的損失函數值J1。然后(J1-J0)/0.000001就是J對於a的偏導的近似值。我們對每一個參數采用類似的方法求偏導,最后將偏導的值組成一個向量,即為梯度向量。
這個辦法看上去很簡單,但卻無法應用在實際的神經網絡當中。一方面的原因是,我們很難知道對參數的改變,有多小才算足夠小,即我們很難保證最后求出的梯度是准確的。
另一方面的原因是,這種方法計算量太大,現在的神經網絡中經常會有上億個參數,而這里每求一個分量的偏導都要把所有參數值代入損失函數求兩次損失函數值,而且每個分量都要執行這樣的計算。相當於每計算一次梯度需要2x1億x1億次計算,而梯度下降算法又要求我們多次(可能是上萬次)計算梯度。這樣巨大的計算量即使是超級計算機也很難承受(世界第一的“神威·太湖之光”超級計算機峰值性能為12.5億億次/秒,每秒也只能計算大概6次梯度)。

所以,我們需要更加高效准確的算法來計算梯度,而反向傳播算法正好能滿足我們的需求。

2.2 “計算圖(compute graph)”與鏈式法則

其實如果你已經理解了鏈式法則,那么可以說,你幾乎已經學會反向傳播算法了。讓人感到很愉快對不對,好像什么都還沒做,我們就已經掌握了一個名字看起來有些嚇人的算法。
為了幫助我們真正理解反向傳播算法,我們先來看一下什么是“計算圖”,我們以第一次實驗提到的sigmoid函數為例:

它的計算圖,是這樣的:

此處輸入圖片的描述

我們將sigmoid函數視為一個復合函數,並將其中的每一個子函數都視為一個節點,每個節點按照復合函數實際的運算順序鏈接起來,最終得到的F其實就是sigmoid函數本身。

根據求導法則,我們可以求得每一個節點對它直接子節點的導函數:

此處輸入圖片的描述

最重要的地方來了,再根據求導鏈式法則,我們現在可以輕易寫出圖中任意一個高層節點對其任意后代節點的導函數:只需要把連接它們的路徑上的所有部分導函數都乘起來就可以了。
比如:

dF/dC=(dF/dE)*(dE/dC)=(-1/E^2)*1=-1/E^2
dF/dA=(dF/dE)*(dE/dC)*(dC/dB)*(dB/dA)=(-1/E^2)*(1)*(e^B)*(-1)=e^B/E^2

2.3 反向傳播算法

到這里反向傳播算法已經呼之欲出了,對於一個具體的參數值,我們只需要把每個節點的值代入求得的導函數公式就可以求得導數(偏導數),進而得到梯度。
這很簡單,我們先從計算圖的底部開始向上,逐個節點計算函數值並保存下來。這個步驟,叫做前向計算(forward)

然后,我們從計算圖的頂部開始向下,逐步計算損失函數對每個子節點的導函數,代入前向計算過程中得到的節點值,得到導數值。這個步驟,叫做反向傳播(backward)或者更明確一點叫做反向梯度傳播
我們來具體實踐一下,對於上圖中的sigmoid函數,計算x=0時的導數:
前向計算:

A=0, B=0, C=1, D=1, E=2, F=-1/4

反向傳播:

dF/dE=-1/E^2=-1/2^2=-1/4
dF/dC=dF/dE*dE/dC=-1/4
dF/dB=dF/dC*dC/dB=-1/4*e^B=-1/4*1=-1/4
dF/dA=dF/dB*dB/dA=-1/4*(-1)=1/4

以上就是反向傳播算法的全部內容。對於有1億個參數的損失函數,我們只需要2*1億次計算就可以求出梯度。復雜度大大降低,速度將大大加快。

2.4 將sigmoid視為一個整體

sigmoid函數中沒有參數,在實際的神經網絡中,我們都是將sigmoid函數視為一個整體來對待,沒必要求它的內部節點的導函數。
sigmoid函數的導函數是什么呢?你可以自己求導試試,實際上sigmoid(x)'=sigmoid(x)*(1-sigmoid(x))

2.5 反向傳播算法--動手實現

激動人心的時刻到了,我們終於要開始用python代碼實現深度神經網絡的過程,這里我們打算對第一次實驗中的神經網絡示例圖中的“復合函數”編寫反向傳播算法。不過為了循序漸進,我們考慮第一層(輸入層)只有兩個節點,第二層只有一個節點的情況,即如下圖:

此處輸入圖片的描述

注意我們將sigmoid函數圖像放在了b1節點后面,代表我們這里對b1運用sigmoid函數得到了最終的輸出h1。

如果你對自己比較有信心,可以不看接下來實現的代碼,自己動手試一試。

我們可以先把圖中包含的函數表達式寫出來,方便我們之后寫代碼參考:
b1=w11*a1+w12*a2+bias1
h1=sigmoid(b1)
h1=sigmoid(w11*a1+w12*a2+bias1)

現在我們創建bp.py文件,開始編寫代碼。先來編寫從第一層到第二層之間的代碼:

 1 import numpy as np
 2 
 3  
 4 
 5 class FullyConnect:
 6 
 7     def __init__(self, l_x, l_y):  # 兩個參數分別為輸入層的長度和輸出層的長度
 8 
 9         self.weights = np.random.randn(l_y, l_x)  # 使用隨機數初始化參數
10 
11         self.bias = np.random.randn(1)  # 使用隨機數初始化參數
12 
13  
14 
15     def forward(self, x):
16 
17         self.x = x  # 把中間結果保存下來,以備反向傳播時使用
18 
19         self.y = np.dot(self.weights, x) + self.bias  # 計算w11*a1+w12*a2+bias1
self.y = np.dot(self.weights, x.T) + self.bias  # 計算w11*a1+w12*a2+bias1#上面一行貌似不對,應該用這行
21 return self.y # 將這一層計算的結果向前傳遞 22 23 24 25 def backward(self, d): 26 27 self.dw = d * self.x # 根據鏈式法則,將反向傳遞回來的導數值乘以x,得到對參數的梯度 28 29 self.db = d 30 31 self.dx = d * self.weights 32 33 return self.dw, self.db # 返回求得的參數梯度,注意這里如果要繼續反向傳遞梯度,應該返回self.dx

 

注意在神經網絡中,我們將層與層之間的每個點都有連接的層叫做全連接(fully connect)層,所以我們將這里的類命名為FullyConnect
上面的代碼非常清楚簡潔,我們的全連接層完成了三個工作:

  1. 隨機初始化網絡參數
  2. 根據x計算這層的輸出y,並前向傳遞給下一層
  3. 運用求導鏈式法則,將前面的網絡層向后傳遞的導數值與本層的相關數值相乘,得到最后一層對本層參數的梯度。注意這里如果要繼續反向傳遞梯度(如果后面還有別的層的話),backward()應該返回self.dx

然后是第二層的輸入到最后的輸出之間的代碼,也就是我們的sigmoid層:

 1 class Sigmoid:
 2 
 3     def __init__(self):  # 無參數,不需初始化
 4 
 5         pass
 6 
 7  
 8 
 9     def sigmoid(self, x):
10 
11         return 1 / (1 + np.exp(-x))
12 
13  
14 
15     def forward(self, x):
16 
17         self.x = x
18 
19         self.y = self.sigmoid(x)
20 
21         return self.y
22 
23  
24 
25     def backward(self):  # 這里sigmoid是最后一層,所以從這里開始反向計算梯度
26 
27         sig = self.sigmoid(self.x)
28 
29         self.dx = sig * (1 - sig)
30 
31         return self.dx  # 反向傳遞梯度

 

由於我們要多次使用sigmoid函數,所以我們單獨的把sigmoid寫成了類的一個成員函數。

我們這里同樣完成了三個工作。只不過由於Sigmoid層沒有參數,所以不需要進行參數初始化。同時由於這里需要反向傳播梯度,所以backward()函數必須返回self.dx

把上面的兩層拼起來,就完成了我們的總體的網絡結構:

def main():

    fc = FullyConnect(2, 1)

    sigmoid = Sigmoid()

    x = np.array([[1], [2]])

    print 'weights:', fc.weights, ' bias:', fc.bias, ' input: ', x

 

    # 執行前向計算

    y1 = fc.forward(x)

    y2 = sigmoid.forward(y1)

    print 'forward result: ', y2

 

    # 執行反向傳播

    d1 = sigmoid.backward()

    dx = fc.backward(d1)

    print 'backward result: ', dx

 

 

if __name__ == '__main__':

    main()

 請你自行運行上面的代碼,並修改輸入的x值。觀察輸出的中間值和最終結果,並手動驗證我們計算的梯度是否正確。

如果你發現你不知道如何手動計算驗證結果,那說明你還沒有理解反向傳播算法的原理,請回過頭去再仔細看一下之前的講解。

這里給出完整代碼的下載鏈接,但我還是希望你能盡量自己嘗試寫出代碼,至少自己動手將上面的代碼重新敲一遍。這樣學習效果會好得多。

完整代碼文件下載:

wget http://labfile.oss.aliyuncs.com/courses/814/bp.py

 

 

2.6 層次化的網絡結構

上面的代碼將每個網絡層寫在不同的類里,並且類里面的接口都是一致的(forward 和 backward),這樣做有很多好處,一是最大程度地降低了不同模塊之間的耦合程度,如果某一個層里面的代碼需要修改,則只需要修改該層的代碼就夠了,不需要關心其他層是怎么實現的。另一方面可以完全自由地組合不同的網絡層(我們最后會介紹神經網絡里其他種類的網絡層)。
實際上,目前很多用於科研和工業生產的深度學習框架很多都是采用這種結構,你可以找一個深度學習框架(比如caffe)看看它的源碼,你會發現里面就是這樣一個個寫好的網絡層。

三、實驗總結

本次實驗,我們完全地掌握了梯度下降算法中的關鍵--反向傳播算法。至此,神經網絡中最基本的東西你已經全部掌握了。你現在完全可以自己嘗試構建神經網絡並使用反向傳播算法優化網絡中的參數。
如果你把到此為止講的東西差不多都弄懂了,那非常恭喜你,你應該為自己感到驕傲。如果你暫時還有些東西沒有理解,不要氣餒,回過頭去仔細看看,到網上查查資料,如果實在無法理解,問問我們實驗樓的助教,我相信你最終也能理解。
本次實驗,我們學習了:

  • 使用計算圖理解反向傳播算法
  • 層次化的神經網絡結構

四、課后作業

    1. [選做]請你自己嘗試將我們上面實現的第二層網絡的節點改為2個(或多個),注意這里涉及到對矩陣求導,如果你沒學過相關知識可能無法下手。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM