1. 程式人生 > >使用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檔案,開始編寫程式碼。先來編寫從第一層到第二層之間的程式碼:

import numpy as npclassFullyConnect:    def__init__(self, l_x, l_y):  # 兩個引數分別為輸入層的長度和輸出層的長度        self.weights = np.random.randn(l_y, l_x)  # 使用隨機數初始化引數        self.bias = np.random.randn(1# 使用隨機數初始化引數    defforward(self, x):        self.x = x  # 把中間結果儲存下來,以備反向傳播時使用        self.y = np.dot(self.weights, x) + self.bias  # 計算w11*a1+w12*a2+bias1        return self.y  # 將這一層計算的結果向前傳遞    defbackward(self, d):        self.dw = d * self.x  # 根據鏈式法則,將反向傳遞回來的導數值乘以x,得到對引數的梯度        self.db = d        self.dx = d * self.weights        return self.dw, self.db  # 返回求得的引數梯度,注意這裡如果要繼續反向傳遞梯度,應該返回self.dx

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

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

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

classSigmoid:    def__init__(self):  # 無引數,不需初始化        pass    defsigmoid(self, x):        return 1 / (1 + np.exp(-x))    defforward(self, x):        self.x = x        self.y = self.sigmoid(x)        return self.y    defbackward(self):  # 這裡sigmoid是最後一層,所以從這裡開始反向計算梯度        sig = self.sigmoid(self.x)        self.dx = sig * (1 - sig)        return self.dx  # 反向傳遞梯度

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

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

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

defmain():    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: ', dxif __name__ == '__main__':    main()

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

此處輸入圖片的描述

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

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

完整程式碼檔案下載:

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

2.6 層次化的網路結構

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

三、實驗總結

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

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

四、課後作業

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