前言
直接上代碼是最有效的學(xué)習(xí)方式。這篇教程通過由一段簡短的 python 代碼實(shí)現(xiàn)的非常簡單的實(shí)例來講解 BP 反向傳播算法。
?
當(dāng)然,上述程序可能過于簡練了。下面我會將其簡要分解成幾個部分進(jìn)行探討。
第一部分:一個簡潔的神經(jīng)網(wǎng)絡(luò)
一個用 BP 算法訓(xùn)練的神經(jīng)網(wǎng)絡(luò)嘗試著用輸入去預(yù)測輸出。
?
考慮以上情形:給定三列輸入,試著去預(yù)測對應(yīng)的一列輸出。我們可以通過簡單測量輸入與輸出值的數(shù)據(jù)來解決這一問題。這樣一來,我們可以發(fā)現(xiàn)最左邊的一列輸入值和輸出值是完美匹配/完全相關(guān)的。直觀意義上來講,反向傳播算法便是通過這種方式來衡量數(shù)據(jù)間統(tǒng)計(jì)關(guān)系進(jìn)而得到模型的。下面直入正題,動手實(shí)踐。
2 層神經(jīng)網(wǎng)絡(luò):
?
[[ 0.00966449]
[ 0.00786506]
[ 0.99358898]
[ 0.99211957]]
變量 定義說明
X 輸入數(shù)據(jù)集,形式為矩陣,每 1 行代表 1 個訓(xùn)練樣本。
y 輸出數(shù)據(jù)集,形式為矩陣,每 1 行代表 1 個訓(xùn)練樣本。
l0 網(wǎng)絡(luò)第 1 層,即網(wǎng)絡(luò)輸入層。
l1 網(wǎng)絡(luò)第 2 層,常稱作隱藏層。
Syn 第一層權(quán)值,突觸 0 ,連接 l0 層與 l1 層。
0
* 逐元素相乘,故兩等長向量相乘等同于其對等元素分別相乘,結(jié)果為同等長度的向量。
– 元素相減,故兩等長向量相減等同于其對等元素分別相減,結(jié)果為同等長度的向量。
x.dot(y) 若 x 和 y 為向量,則進(jìn)行點(diǎn)積操作;若均為矩陣,則進(jìn)行矩陣相乘操作;若其中之一為矩陣,則進(jìn)行向量與矩陣相乘操作。
正如在“訓(xùn)練后結(jié)果輸出”中看到的,程序正確執(zhí)行!在描述具體過程之前,我建議讀者事先去嘗試?yán)斫獠⑦\(yùn)行下代碼,對算法程序的工作方式有一個直觀的感受。最好能夠在 ipython notebook 中原封不動地跑通以上程序(或者你想自己寫個腳本也行,但我還是強(qiáng)烈推薦 notebook )。下面是對理解程序有幫助的幾個關(guān)鍵地方:
· 對比 l1 層在首次迭代和最后一次迭代時(shí)的狀態(tài)。
· 仔細(xì)察看 “nonlin” 函數(shù),正是它將一個概率值作為輸出提供給我們。
· 仔細(xì)觀察在迭代過程中,l1_error 是如何變化的。
· 將第 36 行中的表達(dá)式拆開來分析,大部分秘密武器就在這里面。
· 仔細(xì)理解第 39 行代碼,網(wǎng)絡(luò)中所有操作都是在為這步運(yùn)算做準(zhǔn)備。
下面,讓我們一行一行地把代碼過一遍。
建議:用兩個屏幕來打開這篇博客,這樣你就能對照著代碼來閱讀文章。在博客撰寫時(shí),我也正是這么做的。
第 1 行:這里導(dǎo)入一個名叫 numpy 的線性代數(shù)工具庫,它是本程序中唯一的外部依賴。
第 4 行:這里是我們的“非線性”部分。雖然它可以是許多種函數(shù),但在這里,使用的非線性映射為一個稱作 “sigmoid” 的函數(shù)。Sigmoid 函數(shù)可以將任何值都映射到一個位于 0 到 1 范圍內(nèi)的值。通過它,我們可以將實(shí)數(shù)轉(zhuǎn)化為概率值。對于神經(jīng)網(wǎng)絡(luò)的訓(xùn)練, Sigmoid 函數(shù)也有其它幾個非常不錯的特性。
?
第 5 行: 注意,通過 “nonlin” 函數(shù)體還能得到 sigmod 函數(shù)的導(dǎo)數(shù)(當(dāng)形參 deriv 為 True 時(shí))。Sigmoid 函數(shù)優(yōu)異特性之一,在于只用它的輸出值便可以得到其導(dǎo)數(shù)值。若 Sigmoid 的輸出值用變量 out 表示,則其導(dǎo)數(shù)值可簡單通過式子 out *(1-out) 得到,這是非常高效的。
若你對求導(dǎo)還不太熟悉,那么你可以這樣理解:導(dǎo)數(shù)就是 sigmod 函數(shù)曲線在給定點(diǎn)上的斜率(如上圖所示,曲線上不同的點(diǎn)對應(yīng)的斜率不同)。有關(guān)更多導(dǎo)數(shù)方面的知識,可以參考可汗學(xué)院的導(dǎo)數(shù)求解教程。
第 10 行:這行代碼將我們的輸入數(shù)據(jù)集初始化為 numpy 中的矩陣。每一行為一個“訓(xùn)練實(shí)例”,每一列的對應(yīng)著一個輸入節(jié)點(diǎn)。這樣,我們的神經(jīng)網(wǎng)絡(luò)便有 3 個輸入節(jié)點(diǎn),4 個訓(xùn)練實(shí)例。
第 16 行:這行代碼對輸出數(shù)據(jù)集進(jìn)行初始化。在本例中,為了節(jié)省空間,我以水平格式( 1 行 4 列)定義生成了數(shù)據(jù)集?!?T” 為轉(zhuǎn)置函數(shù)。經(jīng)轉(zhuǎn)置后,該 y 矩陣便包含 4 行 1 列。同我們的輸入一致,每一行是一個訓(xùn)練實(shí)例,而每一列(僅有一列)對應(yīng)一個輸出節(jié)點(diǎn)。因此,我們的網(wǎng)絡(luò)含有 3 個輸入, 1 個輸出。
第 20 行:為你的隨機(jī)數(shù)設(shè)定產(chǎn)生種子是一個良好的習(xí)慣。這樣一來,你得到的權(quán)重初始化集仍是隨機(jī)分布的,但每次開始訓(xùn)練時(shí),得到的權(quán)重初始集分布都是完全一致的。這便于觀察你的策略變動是如何影響網(wǎng)絡(luò)訓(xùn)練的。
第 23 行:這行代碼實(shí)現(xiàn)了該神經(jīng)網(wǎng)絡(luò)權(quán)重矩陣的初始化操作。用 “syn0” 來代指 “零號突觸”(即“輸入層-第一層隱層”間權(quán)重矩陣)。由于我們的神經(jīng)網(wǎng)絡(luò)只有 2 層(輸入層與輸出層),因此只需要一個權(quán)重矩陣來連接它們。權(quán)重矩陣維度為(3,1),是因?yàn)樯窠?jīng)網(wǎng)絡(luò)有 3 個輸入和 1 個輸出。換種方式來講,也就是 l0 層大小為 3 , l1 層大小為 1 。因此,要想將 l0 層的每個神經(jīng)元節(jié)點(diǎn)與 l1 層的每個神經(jīng)元節(jié)點(diǎn)相連,就需要一個維度大小為(3,1)的連接矩陣。:)
同時(shí),要注意到隨機(jī)初始化的權(quán)重矩陣均值為 0 。關(guān)于權(quán)重的初始化,里面可有不少學(xué)問。因?yàn)槲覀儸F(xiàn)在還只是練習(xí),所以在權(quán)值初始化時(shí)設(shè)定均值為 0 就可以了。
另一個認(rèn)識就是,所謂的“神經(jīng)網(wǎng)絡(luò)”實(shí)際上就是這個權(quán)值矩陣。雖然有“層” l0 和 l1 ,但它們都是基于數(shù)據(jù)集的瞬時(shí)值,即層的輸入輸出狀態(tài)隨不同輸入數(shù)據(jù)而不同,這些狀態(tài)是不需要保存的。在學(xué)習(xí)訓(xùn)練過程中,只需存儲 syn0 權(quán)值矩陣。
第 25 行:本行代碼開始就是神經(jīng)網(wǎng)絡(luò)訓(xùn)練的代碼了。本 for 循環(huán)迭代式地多次執(zhí)行訓(xùn)練代碼,使得我們的網(wǎng)絡(luò)能更好地?cái)M合訓(xùn)練集。
第 28 行:可知,網(wǎng)絡(luò)第一層 l0 就是我們的輸入數(shù)據(jù),關(guān)于這點(diǎn),下面作進(jìn)一步闡述。還記得 X 包含 4 個訓(xùn)練實(shí)例(行)吧?在該部分實(shí)現(xiàn)中,我們將同時(shí)對所有的實(shí)例進(jìn)行處理,這種訓(xùn)練方式稱作“整批”訓(xùn)練。因此,雖然我們有 4 個不同的 l0 行,但你可以將其整體視為單個訓(xùn)練實(shí)例,這樣做并沒有什么差別。(我們可以在不改動一行代碼的前提下,一次性裝入 1000 個甚至 10000 個實(shí)例)。
第 29 行:這是神經(jīng)網(wǎng)絡(luò)的前向預(yù)測階段。基本上,首先讓網(wǎng)絡(luò)基于給定輸入“試著”去預(yù)測輸出。接著,我們將研究預(yù)測效果如何,以至于作出一些調(diào)整,使得在每次迭代過程中網(wǎng)絡(luò)能夠表現(xiàn)地更好一點(diǎn)。
(4 x 3) dot (3 x 1) = (4 x 1)
本行代碼包含兩個步驟。首先,將 l0 與 syn0 進(jìn)行矩陣相乘。然后,將計(jì)算結(jié)果傳遞給 sigmoid 函數(shù)。具體考慮到各個矩陣的維度:
(4 x 3) dot (3 x 1) = (4 x 1)
矩陣相乘是有約束的,比如等式靠中間的兩個維度必須一致。而最終產(chǎn)生的矩陣,其行數(shù)為第一個矩陣的行數(shù),列數(shù)則為第二個矩陣的列數(shù)。
由于裝入了 4 個訓(xùn)練實(shí)例,因此最終得到了 4 個猜測結(jié)果,即一個(4 x 1)的矩陣。每一個輸出都對應(yīng),給定輸入下網(wǎng)絡(luò)對正確結(jié)果的一個猜測。也許這也能直觀地解釋:為什么我們可以“載入”任意數(shù)目的訓(xùn)練實(shí)例。在這種情況下,矩陣乘法仍是奏效的。
第 32 行:這樣,對于每一輸入,可知 l1 都有對應(yīng)的一個“猜測”結(jié)果。那么通過將真實(shí)的結(jié)果(y)與猜測結(jié)果(l1)作減,就可以對比得到網(wǎng)絡(luò)預(yù)測的效果怎么樣。l1_error 是一個有正數(shù)和負(fù)數(shù)組成的向量,它可以反映出網(wǎng)絡(luò)的誤差有多大。
第 36 行:現(xiàn)在,我們要碰到干貨了!這里就是秘密武器所在!本行代碼信息量比較大,所以將它拆成兩部分來分析。
第一部分:求導(dǎo)
nonlin(l1,True)
如果 l1 可表示成 3 個點(diǎn),如下圖所示,以上代碼就可產(chǎn)生圖中的三條斜線。注意到,如在 x=2.0 處(綠色點(diǎn))輸出值很大時(shí),及如在x=-1.0 處(紫色點(diǎn))輸出值很小時(shí),斜線都非常十分平緩。如你所見,斜度最高的點(diǎn)位于 x=0 處(藍(lán)色點(diǎn))。這一特性非常重要。另外也可發(fā)現(xiàn),所有的導(dǎo)數(shù)值都在 0 到 1 范圍之內(nèi)。
?
整體認(rèn)識:誤差項(xiàng)加權(quán)導(dǎo)數(shù)值
?
當(dāng)然,“誤差項(xiàng)加權(quán)導(dǎo)數(shù)值”這個名詞在數(shù)學(xué)上還有更為嚴(yán)謹(jǐn)?shù)拿枋?,不過我覺得這個定義準(zhǔn)確地捕捉到了算法的意圖。 l1_error 是一個(4,1)大小的矩陣,nonlin(l1,True)返回的便是一個(4,1)的矩陣。而我們所做的就是將其“逐元素地”相乘,得到的是一個(4,1)大小的矩陣 l1_delta ,它的每一個元素就是元素相乘的結(jié)果。
當(dāng)我們將“斜率”乘上誤差時(shí),實(shí)際上就在以高確信度減小預(yù)測誤差?;剡^頭來看下 sigmoid 函數(shù)曲線圖!當(dāng)斜率非常平緩時(shí)(接近于 0),那么網(wǎng)絡(luò)輸出要么是一個很大的值,要么是一個很小的值。這就意味著網(wǎng)絡(luò)十分確定是否是這種情況,或是另一種情況。然而,如果網(wǎng)絡(luò)的判定結(jié)果對應(yīng)(x = 0.5,y = 0.5)附近時(shí),它便就不那么確定了。對于這種“似是而非”預(yù)測情形,我們對其做最大的調(diào)整,而對確定的情形則不多做處理,乘上一個接近于 0 的數(shù),則對應(yīng)的調(diào)整量便可忽略不計(jì)。
第 39 行:現(xiàn)在,更新網(wǎng)絡(luò)已準(zhǔn)備就緒!下面一起來看下一個簡單的訓(xùn)練示例。
?
在這個訓(xùn)練示例中,我們已經(jīng)為權(quán)值更新做好了一切準(zhǔn)備。下面讓我們來更新最左邊的權(quán)值(9.5)。
權(quán)值更新量 = 輸入值 * l1_delta
對于最左邊的權(quán)值,在上式中便是 1.0 乘上 l1_delta 的值。可以想得到,這對權(quán)值 9.5 的增量是可以忽略不計(jì)的。為什么只有這么小的更新量呢?是因?yàn)槲覀儗τ陬A(yù)測結(jié)果十分確信,而且預(yù)測結(jié)果有很大把握是正確的。誤差和斜率都偏小時(shí),便意味著一個較小的更新量??紤]所有的連接權(quán)值,這三個權(quán)值的增量都是非常小的。
?
然而,由于采取的是“整批”訓(xùn)練的機(jī)制,因此上述更新步驟是在全部的 4 個訓(xùn)練實(shí)例上進(jìn)行的,這看上去也有點(diǎn)類似于圖像。那么,第 39 行做了什么事情呢?在這簡單的一行代碼中,它共完成了下面幾個操作:首先計(jì)算每一個訓(xùn)練實(shí)例中每一個權(quán)值對應(yīng)的權(quán)值更新量,再將每個權(quán)值的所有更新量累加起來,接著更新這些權(quán)值。親自推導(dǎo)下這個矩陣相乘操作,你便能明白它是如何做到這一點(diǎn)的。
重點(diǎn)結(jié)論:
現(xiàn)在,我們已經(jīng)知曉神經(jīng)網(wǎng)絡(luò)是如何進(jìn)行更新的?;剡^頭來看看訓(xùn)練數(shù)據(jù),作一些深入思考。 當(dāng)輸入和輸出均為 1 時(shí),我們增加它們間的連接權(quán)重;當(dāng)輸入為 1 而輸出為 0 時(shí),我們減小其連接權(quán)重
?
因此,在如下 4 個訓(xùn)練示例中,第一個輸入結(jié)點(diǎn)與輸出節(jié)點(diǎn)間的權(quán)值將持續(xù)增大或者保持不變,而其他兩個權(quán)值在訓(xùn)練過程中表現(xiàn)為同時(shí)增大或者減小(忽略中間過程)。這種現(xiàn)象便使得網(wǎng)絡(luò)能夠基于輸入與輸出間的聯(lián)系進(jìn)行學(xué)習(xí)。
第二部分:一個稍顯復(fù)雜的問題
?
考慮如下情形:給定前兩列輸入,嘗試去預(yù)測輸出列。一個關(guān)鍵點(diǎn)在于這兩列與輸出不存在任何關(guān)聯(lián),每一列都有 50% 的幾率預(yù)測結(jié)果為 1 ,也有 50% 的幾率預(yù)測為 0 。
那么現(xiàn)在的輸出模式會是怎樣呢?看起來似乎與第三列毫不相關(guān),其值始終為 1 。而第 1 列和第 2 列可以有更為清晰的認(rèn)識,當(dāng)其中 1 列值為1(但不同時(shí)為 1 !)時(shí),輸出便為 1 。這邊是我們要找的模式!
以上可以視為一種“非線性”模式,因?yàn)閱蝹€輸入與輸出間不存在一個一對一的關(guān)系。而輸入的組合與輸出間存在著一對一的關(guān)系,在這里也就是列 1 和列 2 的組合。
?
信不信由你,圖像識別也是一種類似的問題。若有 100 張尺寸相同的煙斗圖片和腳踏車圖片,那么,不存在單個像素點(diǎn)位置能夠直接說明某張圖片是腳踏車還是煙斗。單純從統(tǒng)計(jì)角度來看,這些像素可能也是隨機(jī)分布的。然而,某些像素的組合卻不是隨機(jī)的,也就是說,正是這種組合才形成了一輛腳踏車或者是一個人。
我們的策略
由上可知,像素組合后的產(chǎn)物與輸出存在著一對一的關(guān)系。為了先完成這種組合,我們需要額外增加一個網(wǎng)絡(luò)層。第一層對輸入進(jìn)行組合,然后以第一層的輸出作為輸入,通過第二層的映射得到最終的輸出結(jié)果。在給出具體實(shí)現(xiàn)之前,我們來看下這張表格。
?
權(quán)重隨機(jī)初始化好后,我們便得到了層1的隱態(tài)值。注意到什么了嗎?第二列(第二個隱層結(jié)點(diǎn))已經(jīng)同輸出有一定的相關(guān)度了!雖不是十分完美,但也可圈可點(diǎn)。無論你是否相信,尋找這種相關(guān)性在神經(jīng)網(wǎng)絡(luò)訓(xùn)練中占了很大比重。(甚至可以認(rèn)定,這也是訓(xùn)練神經(jīng)網(wǎng)絡(luò)的唯一途徑),隨后的訓(xùn)練要做的便是將這種關(guān)聯(lián)進(jìn)一步增大。syn1 權(quán)值矩陣將隱層的組合輸出映射到最終結(jié)果,而在更新 syn1 的同時(shí),還需要更新 syn0 權(quán)值矩陣,以從輸入數(shù)據(jù)中更好地產(chǎn)生這些組合。
注釋:通過增加更多的中間層,以對更多關(guān)系的組合進(jìn)行建模。這一策略正是廣為人們所熟知的“深度學(xué)習(xí)”,因?yàn)槠湔峭ㄟ^不斷增加更深的網(wǎng)絡(luò)層來建模的。
?
? ? ?
?
一切看起來都如此熟悉!這只是用這樣兩個先前的實(shí)現(xiàn)相互堆疊而成的,第一層(l1)的輸出就是第二層的輸入。唯一所出現(xiàn)的新事物便是第 43 行代碼。
第 43 行:通過對 l2 層的誤差進(jìn)行“置信度加權(quán)”,構(gòu)建 l1 層相應(yīng)的誤差。為了做到這點(diǎn),只要簡單的通過 l2 與 l1 間的連接權(quán)值來傳遞誤差。這種做法也可稱作“貢獻(xiàn)度加權(quán)誤差”,因?yàn)槲覀儗W(xué)習(xí)的是,l1 層每一個結(jié)點(diǎn)的輸出值對 l2 層節(jié)點(diǎn)誤差的貢獻(xiàn)程度有多大。接著,用之前 2 層神經(jīng)網(wǎng)絡(luò)實(shí)現(xiàn)中的相同步驟,對 syn0 權(quán)值矩陣進(jìn)行更新。
第三部分:總結(jié)?
如果你想認(rèn)真弄懂神經(jīng)網(wǎng)絡(luò),給你一點(diǎn)建議:憑借記憶嘗試去重構(gòu)這個網(wǎng)絡(luò)。我知道這聽起來有一些瘋狂,但確實(shí)會有幫助的。如果你想能基于新的學(xué)術(shù)文章創(chuàng)造任意結(jié)構(gòu)的神經(jīng)網(wǎng)絡(luò),或者讀懂不同網(wǎng)絡(luò)結(jié)構(gòu)的樣例程序,我覺得這項(xiàng)訓(xùn)練會是一個殺手锏。即使當(dāng)你在使用一些開源框架時(shí),比如 Torch ,Caffe 或者 Theano ,這也會有所幫助的。
評論