我一直在找一份簡明的神經(jīng)網(wǎng)絡(luò)入門,然而在中文圈里并沒有找到。直到我看到了這份162行的Python實現(xiàn),以及對應(yīng)的油管視頻之后,我才覺得這就是我需要的極簡入門資料。這份極簡入門筆記不需要突觸的圖片做裝飾,也不需要贅述神經(jīng)網(wǎng)絡(luò)的發(fā)展歷史;要推導(dǎo)有推導(dǎo),要代碼有代碼,關(guān)鍵是,它們還對得上。對于欠缺的背景知識,利用斯坦福大學(xué)的神經(jīng)網(wǎng)絡(luò)wiki進(jìn)行了補全。
單個神經(jīng)元
神經(jīng)網(wǎng)絡(luò)是多個“神經(jīng)元”(感知機(jī))的帶權(quán)級聯(lián),神經(jīng)網(wǎng)絡(luò)算法可以提供非線性的復(fù)雜模型,它有兩個參數(shù):權(quán)值矩陣{Wl}和偏置向量{bl},不同于感知機(jī)的單一向量形式,{Wl}是復(fù)數(shù)個矩陣,{bl}是復(fù)數(shù)個向量,其中的元素分別屬于單個層,而每個層的組成單元,就是神經(jīng)元。
神經(jīng)元
神經(jīng)網(wǎng)絡(luò)是由多個“神經(jīng)元”(感知機(jī))組成的,每個神經(jīng)元圖示如下:
這其實就是一個單層感知機(jī),其輸入是由和+1組成的向量,其輸出為,其中f是一個激活函數(shù),模擬的是生物神經(jīng)元在接受一定的刺激之后產(chǎn)生興奮信號,否則刺激不夠的話,神經(jīng)元保持抑制狀態(tài)這種現(xiàn)象。這種由一個閾值決定兩個極端的函數(shù)有點像示性函數(shù),然而這里采用的是Sigmoid函數(shù),其優(yōu)點是連續(xù)可導(dǎo)。
Sigmoid函數(shù)
常用的Sigmoid有兩種——
單極性Sigmoid函數(shù)
或者寫成
其圖像如下
雙極性Sigmoid函數(shù)
或者寫成
把第一個式子分子分母同時除以ez,令x=-2z就得到第二個式子了,換湯不換藥。
其圖像如下
從它們兩個的值域來看,兩者名稱里的極性應(yīng)該指的是正負(fù)號。從導(dǎo)數(shù)來看,它們的導(dǎo)數(shù)都非常便于計算:
對于有,對于tanh,有。
視頻作者Ryan還擔(dān)心觀眾微積分學(xué)的不好,細(xì)心地給出了1/(1+e^-x)求導(dǎo)的過程:
一旦知道了f(z),就可以直接求f'(z),所以說很方便。
本Python實現(xiàn)使用的就是1/(1+e^-x)
- def sigmoid(x):
- """
- sigmoid 函數(shù),1/(1+e^-x)
- :param x:
- :return:
- """
- return 1.0/(1.0+math.exp(-x))
- def dsigmoid(y):
- """
- sigmoid 函數(shù)的導(dǎo)數(shù)
- :param y:
- :return:
- """
- return y * (1 - y)
也可以使用雙曲正切函數(shù)tanh
- def sigmoid(x):
- """
- sigmoid 函數(shù),tanh
- :param x:
- :return:
- """
- return math.tanh(x)
其導(dǎo)數(shù)對應(yīng)于:
- def dsigmoid(y):
- """
- sigmoid 函數(shù)的導(dǎo)數(shù)
- :param y:
- :return:
- """
- return 1.0 - y ** 2
神經(jīng)網(wǎng)絡(luò)模型
神經(jīng)網(wǎng)絡(luò)就是多個神經(jīng)元的級聯(lián),上一級神經(jīng)元的輸出是下一級神經(jīng)元的輸入,而且信號在兩級的兩個神經(jīng)元之間傳播的時候需要乘上這兩個神經(jīng)元對應(yīng)的權(quán)值。例如,下圖就是一個簡單的神經(jīng)網(wǎng)絡(luò):
其中,一共有一個輸入層,一個隱藏層和一個輸出層。輸入層有3個輸入節(jié)點,標(biāo)注為+1的那個節(jié)點是偏置節(jié)點,偏置節(jié)點不接受輸入,輸出總是+1。
定義上標(biāo)為層的標(biāo)號,下標(biāo)為節(jié)點的標(biāo)號,則本神經(jīng)網(wǎng)絡(luò)模型的參數(shù)是:,其中是第l層的第j個節(jié)點與第l+1層第i個節(jié)點之間的連接參數(shù)(或稱權(quán)值);表示第l層第i個偏置節(jié)點。這些符號在接下來的前向傳播將要用到。
前向傳播
雖然標(biāo)題是《(誤差)后向傳播神經(jīng)網(wǎng)絡(luò)入門》,但這并不意味著可以跳過前向傳播的學(xué)習(xí)。因為如果后向傳播對應(yīng)訓(xùn)練的話,那么前向傳播就對應(yīng)預(yù)測(分類),并且訓(xùn)練的時候計算誤差也要用到預(yù)測的輸出值來計算誤差。
定義為第l層第i個節(jié)點的激活值(輸出值)。當(dāng)l=1時,。前向傳播的目的就是在給定模型參數(shù)的情況下,計算l=2,3,4…層的輸出值,直到最后一層就得到最終的輸出值。具體怎么算呢,以上圖的神經(jīng)網(wǎng)絡(luò)模型為例:
這沒什么稀奇的,核心思想是這一層的輸出乘上相應(yīng)的權(quán)值加上偏置量代入激活函數(shù)等于下一層的輸入,一句大白話,所謂中文偽碼。
另外,追求好看的話可以把括號里面那個老長老長的加權(quán)和定義為一個參數(shù):表示第l層第i個節(jié)點的輸入加權(quán)和,比如。那么該節(jié)點的輸出可以寫作。
于是就得到一個好看的形式:
在這個好看的形式下,前向傳播可以簡明扼要地表示為:
在Python實現(xiàn)中,對應(yīng)如下方法:
- def runNN(self, inputs):
- """
- 前向傳播進(jìn)行分類
- :param inputs:輸入
- :return:類別
- """
- if len(inputs) != self.ni - 1:
- print 'incorrect number of inputs'
- for i in range(self.ni - 1):
- self.ai[i] = inputs[i]
- for j in range(self.nh):
- sum = 0.0
- for i in range(self.ni):
- sum += ( self.ai[i] * self.wi[i][j] )
- self.ah[j] = sigmoid(sum)
- for k in range(self.no):
- sum = 0.0
- for j in range(self.nh):
- sum += ( self.ah[j] * self.wo[j][k] )
- self.ao[k] = sigmoid(sum)
- return self.ao
其中,ai、ah、ao分別是輸入層、隱藏層、輸出層,而wi、wo則分別是輸入層到隱藏層、隱藏層到輸出層的權(quán)值矩陣。在本Python實現(xiàn)中,將偏置量一并放入了矩陣,這樣進(jìn)行線性代數(shù)運算就會方便一些。
后向傳播
后向傳播指的是在訓(xùn)練的時候,根據(jù)最終輸出的誤差來調(diào)整倒數(shù)第二層、倒數(shù)第三層……第一層的參數(shù)的過程。
符號定義
在Ryan的講義中,符號定義與斯坦福前向傳播講義相似但略有不同:
:第l層第j個節(jié)點的輸入。
:從第l-1層第i個節(jié)點到第l層第j個節(jié)點的權(quán)值。
:Sigmoid函數(shù)。
:第l層第j個節(jié)點的偏置。
:第l層第j個節(jié)點的輸出。
:輸出層第j個節(jié)點的目標(biāo)值(Target value)。
輸出層權(quán)值調(diào)整
給定訓(xùn)練集和模型輸出(這里沒有上標(biāo)l是因為這里在討論輸出層,l是固定的),輸出層的輸出誤差(或稱損失函數(shù)吧)定義為:
其實就是所有實例對應(yīng)的誤差的平方和的一半,訓(xùn)練的目標(biāo)就是最小化該誤差。怎么最小化呢?看損失函數(shù)對參數(shù)的導(dǎo)數(shù)唄。
將E的定義代入該導(dǎo)數(shù):
無關(guān)變量拿出來:
看到這里大概明白為什么非要把誤差定義為誤差平方和的一半了吧,就是為了好看,數(shù)學(xué)家都是外貌協(xié)會的。
將=(輸出層的輸出等于輸入代入Sigmoid函數(shù))這個關(guān)系代入有:
對Sigmoid求導(dǎo)有:
要開始耍小把戲了,由于輸出層第k個節(jié)點的輸入等于上一層第j個節(jié)點的輸出乘上,即=,而上一層的輸出是與到輸出層的權(quán)值變量無關(guān)的,可以看做一個常量,是線性關(guān)系。所以對求權(quán)值變量的偏導(dǎo)數(shù)直接等于,也就是說:=()=。
然后將上面用過的=代進(jìn)去就得到最終的:
為了表述方便將上式記作:
其中:
隱藏層權(quán)值調(diào)整
依然采用類似的方法求導(dǎo),只不過求的是關(guān)于隱藏層和前一層的權(quán)值參數(shù)的偏導(dǎo)數(shù):
老樣子:
還是老樣子:
還是把Sigmoid弄進(jìn)去:
把=代進(jìn)去,并且將導(dǎo)數(shù)部分拆開:
又要耍把戲了,輸出層的輸入等于上一層的輸出乘以相應(yīng)的權(quán)值,亦即=,于是得到:
把最后面的導(dǎo)數(shù)挪到前面去,接下來要對它動刀了:
再次利用=,這對j也成立,代進(jìn)去:
再次利用=,j換成i,k換成j也成立,代進(jìn)去:
利用剛才定義的,最終得到:
其中:
我們還可以仿照的定義來定義一個,得到:
其中
偏置的調(diào)整
因為沒有任何節(jié)點的輸出流向偏置節(jié)點,所以偏置節(jié)點不存在上層節(jié)點到它所對應(yīng)的權(quán)值參數(shù),也就是說不存在關(guān)于權(quán)值變量的偏導(dǎo)數(shù)。雖然沒有流入,但是偏置節(jié)點依然有輸出(總是+1),該輸出到下一層某個節(jié)點的時候還是會有權(quán)值的,對這個權(quán)值依然需要更新。
我們可以直接對偏置求導(dǎo),發(fā)現(xiàn):
原視頻中說?O/?θ=1,這是不對的,作者也在講義中修正了這個錯誤,?O/?θ=O(1–O)。
然后再求,,后面的導(dǎo)數(shù)等于,代進(jìn)去有
其中,
。
后向傳播算法步驟
-
隨機(jī)初始化參數(shù),對輸入利用前向傳播計算輸出。
-
對每個輸出節(jié)點按照下式計算delta:
-
對每個隱藏節(jié)點按照下式計算delta:
-
計算梯度,并更新權(quán)值參數(shù)和偏置參數(shù):。這里的是學(xué)習(xí)率,影響訓(xùn)練速度。
后向傳播算法實現(xiàn)
- def backPropagate(self, targets, N, M):
- """
- 后向傳播算法
- :param targets: 實例的類別
- :param N: 本次學(xué)習(xí)率
- :param M: 上次學(xué)習(xí)率
- :return: 最終的誤差平方和的一半
- """
- # http://www./watch?v=aVId8KMsdUU&feature=BFa&list=LLldMCkmXl4j9_v0HeKdNcRA
- # 計算輸出層 deltas
- # dE/dw[j][k] = (t[k] - ao[k]) * s'( SUM( w[j][k]*ah[j] ) ) * ah[j]
- output_deltas = [0.0] * self.no
- for k in range(self.no):
- error = targets[k] - self.ao[k]
- output_deltas[k] = error * dsigmoid(self.ao[k])
- # 更新輸出層權(quán)值
- for j in range(self.nh):
- for k in range(self.no):
- # output_deltas[k] * self.ah[j] 才是 dError/dweight[j][k]
- change = output_deltas[k] * self.ah[j]
- self.wo[j][k] += N * change + M * self.co[j][k]
- self.co[j][k] = change
- # 計算隱藏層 deltas
- hidden_deltas = [0.0] * self.nh
- for j in range(self.nh):
- error = 0.0
- for k in range(self.no):
- error += output_deltas[k] * self.wo[j][k]
- hidden_deltas[j] = error * dsigmoid(self.ah[j])
- # 更新輸入層權(quán)值
- for i in range(self.ni):
- for j in range(self.nh):
- change = hidden_deltas[j] * self.ai[i]
- # print 'activation',self.ai[i],'synapse',i,j,'change',change
- self.wi[i][j] += N * change + M * self.ci[i][j]
- self.ci[i][j] = change
- # 計算誤差平方和
- # 1/2 是為了好看,**2 是平方
- error = 0.0
- for k in range(len(targets)):
- error = 0.5 * (targets[k] - self.ao[k]) ** 2
- return error
注意不同于上文的單一學(xué)習(xí)率,這里有兩個學(xué)習(xí)率N和M。N相當(dāng)于上文的,而M則是在用上次訓(xùn)練的梯度更新權(quán)值時的學(xué)習(xí)率。這種同時考慮最近兩次迭代得到的梯度的方法,可以看做是對單一學(xué)習(xí)率的改進(jìn)。
另外,這里并沒有出現(xiàn)任何更新偏置的操作,為什么?
因為這里的偏置是單獨作為一個偏置節(jié)點放到輸入層里的,它的值(輸出,沒有輸入)固定為1,它的權(quán)值已經(jīng)自動包含在上述權(quán)值調(diào)整中了。
如果將偏置作為分別綁定到所有神經(jīng)元的許多值,那么則需要進(jìn)行偏置調(diào)整,而不需要權(quán)值調(diào)整(此時沒有偏置節(jié)點)。
哪個方便,當(dāng)然是前者了,這也導(dǎo)致了大部分神經(jīng)網(wǎng)絡(luò)實現(xiàn)都采用前一種做法。
完整的實現(xiàn)
已開源到了Github上:https://github.com/hankcs/neural_net
這一模塊的原作者是Neil Schemenauer,我做了些注釋。
直接運行bpnn.py即可得到輸出:
- Combined error 0.171204877501
- Combined error 0.190866985872
- Combined error 0.126126875154
- Combined error 0.0658488960415
- Combined error 0.0353249077599
- Combined error 0.0214428399072
- Combined error 0.0144886807614
- Combined error 0.0105787745309
- Combined error 0.00816264126944
- Combined error 0.00655731212209
- Combined error 0.00542964723539
- Combined error 0.00460235328667
- Combined error 0.00397407912435
- Combined error 0.00348339081276
- Combined error 0.00309120476889
- Combined error 0.00277163178862
- Combined error 0.00250692771135
- Combined error 0.00228457151714
- Combined error 0.00209550313514
- Combined error 0.00193302192499
- Inputs: [0, 0] --> [0.9982333356008245] Target [1]
- Inputs: [0, 1] --> [0.9647325217906978] Target [1]
- Inputs: [1, 0] --> [0.9627966274767186] Target [1]
- Inputs: [1, 1] --> [0.05966109502803293] Target [0]
IBM利用Neil Schemenauer的這一模塊(舊版)做了一個識別代碼語言的例子,我將其更新到新版,已經(jīng)整合到了項目中。
要運行測試的話,執(zhí)行命令
- code_recognizer.py testdata.200
即可得到輸出:
- ERROR_CUTOFF = 0.01
- INPUTS = 20
- ITERATIONS = 1000
- MOMENTUM = 0.1
- TESTSIZE = 500
- OUTPUTS = 3
- TRAINSIZE = 500
- LEARNRATE = 0.5
- HIDDEN = 8
- Targets: [1, 0, 0] -- Errors: (0.000 OK) (0.001 OK) (0.000 OK) -- SUCCESS!
值得一提的是,這里的HIDDEN = 8指的是隱藏層的節(jié)點個數(shù),不是層數(shù),層數(shù)多了就變成DeepLearning了。
Reference
http://code./recipes/578148-simple-back-propagation-neural-network-in-python-s/
https://www./watch?v=aVId8KMsdUU&feature=BFa&list=LLldMCkmXl4j9_v0HeKdNcRA
The back-propagation algorithm.pdf
http://ufldl./wiki/index.php/%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C
轉(zhuǎn)載須注明:碼農(nóng)場 ? 反向傳播神經(jīng)網(wǎng)絡(luò)極簡入門