和DeepMind數(shù)據(jù)科學(xué)家、Udacity深度學(xué)習(xí)導(dǎo)師Andrew Trask一起,基于Numpy手寫神經(jīng)網(wǎng)絡(luò),更深刻地理解反向傳播這一概念。
總結(jié):基于可以嘗試和修改的玩具代碼,我能取得最好的學(xué)習(xí)效果。本教程基于一個非常簡單的玩具樣例(簡短的Python代碼實現(xiàn))介紹反向傳播這一概念。
如果你對我的文章感興趣,歡迎在推特上關(guān)注 電子說,也歡迎給我反饋。
直接給我代碼
-
X = np.array([ [0,0,1],[0,1,1],[1,0,1],[1,1,1] ])
-
y = np.array([[0,1,1,0]]).T
-
syn0 =2*np.random.random((3,4)) -1
-
syn1 =2*np.random.random((4,1)) -1
-
forjinxrange(60000):
-
l1 =1/(1+np.exp(-(np.dot(X,syn0))))
-
l2 =1/(1+np.exp(-(np.dot(l1,syn1))))
-
l2_delta = (y - l2)*(l2*(1-l2))
-
l1_delta = l2_delta.dot(syn1.T) * (l1 * (1-l1))
-
syn1 += l1.T.dot(l2_delta)
-
syn0 += X.T.dot(l1_delta)
其他語言:D、C++、CUDA
不過,上面的代碼有點過于凝練了……讓我們循序漸進(jìn)。
一、微型玩具網(wǎng)絡(luò)
基于反向傳播的神經(jīng)網(wǎng)絡(luò)嘗試通過輸入來預(yù)測輸出。
嘗試通過上表的輸入預(yù)測輸出。我們可以通過測量輸入值和輸出值的統(tǒng)計數(shù)據(jù)來解決這一問題。如果我們這么干,我們會發(fā)現(xiàn)最左邊的輸入和輸出完全相關(guān)(perfectly correlated)。最簡單形式的反向傳播,就是通過類似的測量統(tǒng)計數(shù)據(jù)的方式來建立模型。
雙層神經(jīng)網(wǎng)絡(luò)
-
importnumpyasnp
-
# sigmoid函數(shù)
-
defnonlin(x,deriv=False):
-
if(deriv==True):
-
returnx*(1-x)
-
return1/(1+np.exp(-x))
-
# 輸入數(shù)據(jù)集
-
X = np.array([ [0,0,1],
-
[0,1,1],
-
[1,0,1],
-
[1,1,1] ])
-
# 輸出數(shù)據(jù)集
-
y = np.array([[0,0,1,1]]).T
-
# 設(shè)置隨機數(shù)種子使計算結(jié)果是確定的
-
# (實踐中這是一個很好的做法)
-
np.random.seed(1)
-
# 隨機初始化權(quán)重(均值0)
-
syn0 =2*np.random.random((3,1)) -1
-
foriterinxrange(10000):
-
# 前向傳播
-
l0 = X
-
l1 = nonlin(np.dot(l0,syn0))
-
# 差多少?
-
l1_error = y - l1
-
# 誤差乘以sigmoid在l1處的斜率
-
l1_delta = l1_error * nonlin(l1,True)
-
# 更新權(quán)重
-
syn0 += np.dot(l0.T,l1_delta)
-
print"訓(xùn)練后輸出:"
-
printl1
-
訓(xùn)練后輸出:
-
[[0.00966449]
-
[0.00786506]
-
[0.99358898]
-
[0.99211957]]
如你所見,“訓(xùn)練后輸出:"下面打印出的結(jié)果說明這一網(wǎng)絡(luò)是有效的!?。≡谖颐枋鲞@個過程之前,我建議你嘗試探索以上代碼以獲得一些它如何工作的感性認(rèn)識。以上代碼應(yīng)該可以在Jupyter Notebook中直接運行(或者你也可以以腳本的形式運行,但我強烈建議使用notebook)。下面是探索代碼的一些提示:
-
比較第一次迭代和最后一次迭代后的l1
-
查看nonlin函數(shù)定義。該函數(shù)給出一個概率作為輸出。
-
查看迭代過程中l(wèi)1_error的值是如何改變的。
-
仔細(xì)看看第36行。這段代碼的奧秘主要藏在此處。
-
查看第39行。網(wǎng)絡(luò)中的一切是為這個操作準(zhǔn)備的。
讓我們逐行討論代碼。
建議:在兩塊屏幕上打開本文,這樣你就可以一邊查看代碼,一邊閱讀文章了。我寫作下文的時候基本上就是這么做的。
01行:導(dǎo)入numpy。numpy是一個線性代數(shù)庫。這是我們唯一的依賴。
04行:這是我們的“非線性”。非線性可以有多種選擇,這里我們選用的是sigmoid. sigmoid函數(shù)將任何值映射到0到1之間的值。我們使用sigmoid將數(shù)字轉(zhuǎn)換為概率。sigmoid函數(shù)還有其他一些有利于訓(xùn)練神經(jīng)網(wǎng)絡(luò)的性質(zhì)。
05行:注意這個函數(shù)同時可以生成sigmoid的導(dǎo)數(shù)(當(dāng)deriv=True時)。sigmoid函數(shù)有一個非常棒的特性是它的輸出可以用來創(chuàng)建它的導(dǎo)數(shù)。如果sigmoid的輸出是變量out的話,它的導(dǎo)數(shù)是out * (1 - out),非常高效。
如果你不熟悉導(dǎo)數(shù),只需把它想象成sigmoid函數(shù)在給定的點上的斜率(如上圖所示,不同點的斜率不同)。想要了解更多關(guān)于導(dǎo)數(shù)的知識,可以參考Khan Academy的導(dǎo)數(shù)教程。
10行:初始化輸入數(shù)據(jù)集為numpy矩陣。每行是一個“訓(xùn)練樣本”。每列對應(yīng)一個輸入節(jié)點。因此,我們有3個輸入節(jié)點和4個訓(xùn)練樣本。
16行:初始化輸出數(shù)據(jù)集。這里,我水平地生成了數(shù)據(jù)集(1行4列),以節(jié)省字符。.T是轉(zhuǎn)置函數(shù)。轉(zhuǎn)置之后,y矩陣有4行1列。和輸入一樣,每行是一個訓(xùn)練樣本,每列(僅有一列)是一個輸出節(jié)點。所以,我們的網(wǎng)絡(luò)有3個輸入和1個輸出。
20行:設(shè)置隨機數(shù)種子是一個很好的做法。數(shù)字仍然是隨機分布的,但它們在每次訓(xùn)練中將以完全一致的方式隨機分布。這更便于觀察你的改動對網(wǎng)絡(luò)的影響。
23行:這是神經(jīng)網(wǎng)絡(luò)的權(quán)重矩陣。它命名為syn0,意味著它是“突觸(synapse)零”。由于我們的網(wǎng)絡(luò)只有兩層(輸入和輸出),我們只需要一個權(quán)重矩陣就可以連接兩者。它的維度是(3, 1),因為我們有3個輸入和1個輸出。另一種看待它的方式是l0的尺寸是3,l1的尺寸是1. 我們需要將l0中的每個節(jié)點連接到l1中的每個節(jié)點,因而我們需要一個維度(3, 1)的矩陣。
同時注意,隨機初始化時的均值為零。權(quán)重初始化有不少理論。就目前而言,可以簡單地把這一做法(權(quán)重初始化均值為零)看成是最佳實踐。
另外需要注意的是,“神經(jīng)網(wǎng)絡(luò)”實際上就是這個矩陣。我們有神經(jīng)網(wǎng)絡(luò)層l0和l1,但它們其實是基于數(shù)據(jù)集創(chuàng)建的短暫值。我們并不保存它們。所有的學(xué)習(xí)所得都儲存在syn0矩陣中。
25行:網(wǎng)絡(luò)訓(xùn)練代碼從這里開始。這是一個“迭代”訓(xùn)練代碼的for循環(huán),優(yōu)化網(wǎng)絡(luò)以擬合數(shù)據(jù)集。
28行:第一層網(wǎng)絡(luò)l1直接就是數(shù)據(jù)。因此我們在這里明確聲明這一點。還記得X包含4個訓(xùn)練樣本(行)嗎?在這一實現(xiàn)中,我們將同時處理所有樣本。這被稱為“全batch”(full batch)訓(xùn)練。因此,我們有4個不同的l0行,但是如有必要,我們可以將它看成一個單獨的訓(xùn)練樣本。在這里這些沒有區(qū)別。(如果需要,我們可以加載1000甚至10000行數(shù)據(jù),而不用修改任何代碼)。
29行:這是我們的預(yù)測步驟?;旧?,我們首先讓網(wǎng)絡(luò)“嘗試”基于輸入預(yù)測輸出。我們接著研究它的表現(xiàn),從而加以調(diào)整,讓它在下一個迭代中表現(xiàn)更好。
這一行包含兩小步。首先矩陣l0和syn0相乘。接著將輸出傳給sigmoid函數(shù)。算下維度:
-
(4x3) dot (3x1) = (4x1)
矩陣乘法是有序的,滿足等式兩邊的維度必須一致。因此最終生成的矩陣的行數(shù)等于第一個矩陣的行數(shù),列數(shù)等于第二個矩陣的列數(shù)。
由于我們載入了4個訓(xùn)練樣本,我們最終對正確答案做了4次猜測,一個(4 x 1)矩陣。每個輸出對應(yīng)與網(wǎng)絡(luò)對給定輸入的猜測。也許讀到這里你能很直觀地理解為什么我們之前說如有必要可以載入任意數(shù)目的訓(xùn)練樣本,因為矩陣乘法仍然可以工作。
32行:好了,l1根據(jù)每個輸入作出了“猜測”。我們可以通過從猜測(l1)中減去真實答案(y)來看看它的表現(xiàn)如何。l1_error只是一個由正數(shù)和負(fù)數(shù)組成的向量,反映了網(wǎng)絡(luò)離正確還差多少。
36行:現(xiàn)在是關(guān)鍵時刻!這是整個模型的奧秘所在!這一行里發(fā)生了很多事情,所以讓我們進(jìn)一步把它分成兩部分。
第一部分:導(dǎo)數(shù)
-
nonlin(l1,True)
如果l1表示下圖中的三點,那么上面的代碼生成了下圖曲線的斜率。注意,像x=2.0這樣很高的值(綠點)和像x=-1.0這樣很低的值(紫點)具有相對平緩的斜率。x=0(藍(lán)點)處的斜率最高。這起到了非常重要的作用。同時注意所有的導(dǎo)數(shù)都在0和1之間。
整個語句:誤差加權(quán)導(dǎo)數(shù)
-
l1_delta = l1_error * nonlin(l1,True)
“誤差加權(quán)導(dǎo)數(shù)”有數(shù)學(xué)上更精確的說法,但我覺得這個名字捕捉到了背后的直覺。l1_error是一個(4,1)矩陣。nonlin(l1, True)返回一個(4,1)矩陣。我們將它們逐元素相乘。這返回一個(4,1)矩陣l1_delta。
當(dāng)我們將“斜率”乘以錯誤時,我們降低高信度預(yù)測的錯誤。再看一遍sigmoid圖像!如果斜率實在很平緩(接近0),那么這個網(wǎng)絡(luò)或者具有一個非常高的值,或者具有一個非常低的值。這意味著網(wǎng)絡(luò)十分自信。然而,如果網(wǎng)絡(luò)猜測的值接近(x=0, y=0.5),那么它不是那么自信。我們更劇烈地更新那些缺乏信心的預(yù)測,同時傾向于通過乘以一個接近零的數(shù)字保留那些自信的預(yù)測。
行39:一切就緒,我們可以更新網(wǎng)絡(luò)了!讓我們看下單個訓(xùn)練樣本。
對這個樣本而言,我們已經(jīng)準(zhǔn)備好更新權(quán)重了。讓我們更新最左邊的權(quán)重(9.5)。
-
weight_update = input_value * l1_delta
對最左邊的權(quán)重而言,這將是1.0 * L1_delta。推測起來,這會非常輕微地增加9.5. 為什么只增加一點點?好吧,預(yù)測已經(jīng)相當(dāng)自信了,而且預(yù)測的結(jié)果也基本正確。低誤差和低斜率意味著非常小的更新。考慮所有3個權(quán)重。這3個權(quán)重都將略微增加。
譯者注:上圖左側(cè)標(biāo)注L1、L2分別應(yīng)為L0、L1然而,因為我們使用“全batch”配置,我們將對所有4個訓(xùn)練樣本執(zhí)行上述操作。因此,這個過程更像上圖所示。所以,第39行做了什么?它為每個訓(xùn)練樣本的每個權(quán)重計算權(quán)重更新,累加起來,然后更新權(quán)重,這些都是在一行之內(nèi)完成的。探索矩陣乘法,你將看到它是如何做到的!
奧秘
所以,既然我們已經(jīng)了解網(wǎng)絡(luò)是如何更新的,讓我們回過頭來看一下訓(xùn)練數(shù)據(jù)然后反思一下。當(dāng)輸入和輸出都是1的時候,我們增加兩者之間的權(quán)重。當(dāng)輸入是1、輸出是0時,我們減少兩者之間的權(quán)重。
因此,在上表的4個訓(xùn)練樣本中,第一個輸入與輸出之間的權(quán)重將持續(xù)增加或保持不變,而其他兩個權(quán)重將發(fā)現(xiàn)自己在不同的樣本上一會兒增加,一會兒下降(因而無法取得進(jìn)展)。這一現(xiàn)象導(dǎo)致我們的網(wǎng)絡(luò)基于輸入和輸出之間的相關(guān)性進(jìn)行學(xué)習(xí)。
二、稍微加大難度
考慮基于三個輸入欄預(yù)測輸出欄。妙在沒有一列和輸出是相關(guān)的。每列有50%的機會預(yù)測1,50%的機會預(yù)測0.
那么,模式是什么呢?看起來結(jié)果完全和第三列無關(guān),這一列的值永遠(yuǎn)是1. 然而,第1列和第2列組合起來看比較清楚。如果第1列和第2列有一列是1(但兩列不同為1?。?,那么輸出是1. 這是我們的模式。
這被認(rèn)為是一個“非線性”模式,因為輸入和輸出之間沒有直接的一一對應(yīng)關(guān)系。相反,存在輸入的組合和輸出的一一對應(yīng)關(guān)系,也就是第1列和第2列。
不管你信不信,圖像識別是一個類似的問題。如果有100張尺寸相同的煙斗和自行車的圖像,沒有任何單獨的像素位置和自行車或煙斗的存在性直接相關(guān)。從純統(tǒng)計學(xué)角度看,像素可能是隨機的。然而,特定的像素組合不是隨機的,也就是說,像素的組合形成了自行車或煙斗的圖像。
我們的策略
為了將我們的像素組合成和輸出具有一一對應(yīng)關(guān)系的東西,我們需要增加一個網(wǎng)絡(luò)層。我們的第一層網(wǎng)絡(luò)將組合輸入,第二層網(wǎng)絡(luò)以第一層網(wǎng)絡(luò)的輸出作為輸入,并將其輸入映射到輸出。在我們跳到實現(xiàn)之前,先看下這個表格。
如果我們隨機初始化權(quán)重,我們將得到如上表所示的l1的隱藏狀態(tài)值。注意到?jīng)]有?隱藏權(quán)重的第二項(第二個隱藏節(jié)點)與輸出已經(jīng)有隱約的相關(guān)性!它并不完美,但確實存在。信不信由你,這是神經(jīng)網(wǎng)絡(luò)訓(xùn)練中很重要的一部分。(一個有爭議的觀點認(rèn)為這是神經(jīng)網(wǎng)絡(luò)訓(xùn)練的唯一方式。)下面的訓(xùn)練將放大這一相關(guān)性。它將同時更新syn1和syn0,更新syn1以便將隱藏權(quán)重映射到輸出,更新syn0以便更好地基于輸入產(chǎn)生權(quán)重!
注意:這個增加更多網(wǎng)絡(luò)層以建模關(guān)系的更多組合的領(lǐng)域稱為“深度學(xué)習(xí)”,得名自建模時采用的越來越深的網(wǎng)絡(luò)層。
三層神經(jīng)網(wǎng)絡(luò)
-
importnumpyasnp
-
defnonlin(x,deriv=False):
-
if(deriv==True):
-
returnx*(1-x)
-
return1/(1+np.exp(-x))
-
X = np.array([[0,0,1],
-
[0,1,1],
-
[1,0,1],
-
[1,1,1]])
-
y = np.array([[0],
-
[1],
-
[1],
-
[0]])
-
np.random.seed(1)
-
# 隨機初始化權(quán)重(均值0)
-
syn0 =2*np.random.random((3,4)) -1
-
syn1 =2*np.random.random((4,1)) -1
-
forjinxrange(60000):
-
# 前向傳播,層0、1、2
-
l0 = X
-
l1 = nonlin(np.dot(l0,syn0))
-
l2 = nonlin(np.dot(l1,syn1))
-
# 離目標(biāo)值還差多少?
-
l2_error = y - l2
-
if(j%10000) ==0:
-
print"Error:"+ str(np.mean(np.abs(l2_error)))
-
# 目標(biāo)值在哪個方向?
-
# 我們很確定嗎?如果確定,不要改變太多。
-
l2_delta = l2_error*nonlin(l2,deriv=True)
-
# 根據(jù)權(quán)重 ,每個l1值對l2誤差的貢獻(xiàn)有多大?
-
l1_error = l2_delta.dot(syn1.T)
-
# 目標(biāo)l1在哪個方向?
-
# 我們很確定嗎?如果確定,不要改變太多。
-
l1_delta = l1_error * nonlin(l1,deriv=True)
-
syn1 += l1.T.dot(l2_delta)
-
syn0 += l0.T.dot(l1_delta)
-
Error:0.496410031903
-
Error:0.00858452565325
-
Error:0.00578945986251
-
Error:0.00462917677677
-
Error:0.00395876528027
-
Error:0.00351012256786
建議:在兩塊屏幕上打開本文,這樣你就可以一邊查看代碼,一邊閱讀文章了。我寫作下文的時候基本上就是這么做的。
所有的一切看起來應(yīng)該很相似!這其實就是將2個之前的實現(xiàn)堆疊在一起。l1的輸出是l2的輸入。唯一不同的是第43行。
行43:使用l2的“信度加權(quán)誤差”來確立l1的誤差。為了做到這一點,它簡單地將權(quán)重間的誤差從l2傳給l1. 你可以把它的結(jié)果叫作“貢獻(xiàn)加權(quán)誤差”,因為我們學(xué)習(xí)l1中的每個節(jié)點對l2中的誤差的“貢獻(xiàn)”有多少。這一步驟稱為“反向傳播”,算法也是因此得名的。我們接著更新syn0,正如我們在雙層實現(xiàn)中所做的那樣。
三、結(jié)語和以后的工作
我的建議
如果你對神經(jīng)網(wǎng)絡(luò)的態(tài)度是嚴(yán)肅的,那我有一個建議。嘗試基于回憶重建這個網(wǎng)絡(luò)。我知道這也許聽起來有點瘋狂,但它切切實實很有幫助。如果你希望能夠根據(jù)最新的學(xué)術(shù)論文創(chuàng)建任意的神經(jīng)網(wǎng)絡(luò)架構(gòu),或者閱讀和理解不同架構(gòu)的代碼樣例,我認(rèn)為這是一個殺手級練習(xí)。即使你使用Torch、Caffe、Theano之類的框架,我仍然認(rèn)為這是有用的。我在進(jìn)行這一練習(xí)之前和神經(jīng)網(wǎng)絡(luò)打過好多年交道,這個練習(xí)是我在這一領(lǐng)域所做的最好的投資(而且它花不了多久)。
以后的工作
這個玩具例子仍然需要一些掛件才能真正解決當(dāng)前最先進(jìn)的架構(gòu)。如果你打算進(jìn)一步改進(jìn)你的網(wǎng)絡(luò),下面是一些值得了解的概念。(也許我以后的文章會涉及其中部分內(nèi)容。)
-
神經(jīng)網(wǎng)絡(luò)
+關(guān)注
關(guān)注
42文章
4777瀏覽量
100965
原文標(biāo)題:基于Numpy實現(xiàn)神經(jīng)網(wǎng)絡(luò):反向傳播
文章出處:【微信號:jqr_AI,微信公眾號:論智】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論