這是極簡單的人工神經(jīng)網(wǎng)絡(luò)解說
導(dǎo)言
我不會(huì)機(jī)器學(xué)習(xí),但上個(gè)月我在 GitHub 上發(fā)現(xiàn)了一個(gè) 極簡、入門級(jí)的神經(jīng)網(wǎng)絡(luò)教程 , 示例代碼為 Go 語言 。它簡潔易懂能用一行公式說明白的道理,不多寫一句廢話,我看后大呼過癮。
這么好的東西得讓更多人看到,但原文是英文的無法直接分享,所以得先聯(lián)系作者拿到翻譯的授權(quán),然后由小熊熊翻譯了這個(gè)項(xiàng)目,最后才有您看到的這篇文章。過程艱辛耗時(shí)一個(gè)月實(shí)屬不易,如果您看完覺得還不錯(cuò),歡迎點(diǎn)贊、分享給更多人。
內(nèi)容分為兩部分:
-
第一部分: 最簡單的人工神經(jīng)網(wǎng)絡(luò)
-
第二部分: 最基礎(chǔ)的反向傳播算法
人工神經(jīng)網(wǎng)絡(luò)是人工智能的基礎(chǔ),只有夯實(shí)基礎(chǔ),才能玩轉(zhuǎn) AI 魔法!
溫馨提示
:公式雖多但只是看起來唬人,實(shí)際耐下心讀并不難懂。下面正文開始!
一、最簡單的人工神經(jīng)網(wǎng)絡(luò)
通過理論和代碼解釋和演示的最簡單的人工神經(jīng)網(wǎng)絡(luò)。
示例代碼:https://github.com/gokadin/ai-simplest-network
理論
模擬神經(jīng)元
受人腦工作機(jī)制的啟發(fā),人工神經(jīng)網(wǎng)絡(luò)有著相互連接的模擬神經(jīng)元,用于存儲(chǔ)模式和相互溝通。一個(gè)模擬神經(jīng)元最簡單的形式是有一個(gè)或多個(gè)輸入值和一個(gè)輸出值,其中每個(gè)有一個(gè)權(quán)重。
拿最簡單的來說,輸出值就是輸入值乘以權(quán)重之后的總和。
一個(gè)簡單的例子
網(wǎng)絡(luò)的作用在于通過多個(gè)參數(shù)模擬一個(gè)復(fù)雜的函數(shù),從而可以在給定一系列輸入值的時(shí)候得到一個(gè)特定的輸出值,而這些參數(shù)通常是我們自身難以擬定的。
假設(shè)我們現(xiàn)在的一個(gè)網(wǎng)絡(luò)有兩個(gè)輸入值,,它們對應(yīng)兩個(gè)權(quán)重值和。
現(xiàn)在我們需要調(diào)整權(quán)重值,從而使得它們可以產(chǎn)生我們預(yù)設(shè)的輸出值。
在初始化時(shí),因?yàn)槲覀儾恢獣宰顑?yōu)值,往往是對權(quán)重隨機(jī)賦值,這里我們?yōu)榱撕唵?,將它們都初始化?1 。
這種情況下,我們得到的就是
誤差值
如果輸出值與我們期望的輸出值不一致,那就有了誤差。
例如,如果我們希望目標(biāo)值是,那么這里相差值就是
通常我們會(huì)采用方差(也就是代價(jià)函數(shù))來衡量誤差:
如果有多套輸入輸出值,那么誤差就是每組方差的平均值。
我們用方差來衡量得到的輸出值與我們期望的目標(biāo)值之間的差距。通過平方的形式就可以去除負(fù)偏離值的影響,更加凸顯那些偏離較大的偏差值(不管正負(fù))。
為了糾正誤差,我們需要調(diào)整權(quán)重值,以使得結(jié)果趨近于我們的目標(biāo)值。在我們這個(gè)例子中,將從 1.0 降到 0.5 就可以達(dá)到目標(biāo),因?yàn)?/p>
然而,神經(jīng)網(wǎng)絡(luò)往往涉及到許多不同的輸入和輸出值,這種情況下我們就需要一個(gè)學(xué)習(xí)算法來幫我們自動(dòng)完成這一步。
梯度下降
現(xiàn)在是要借助誤差來幫我們找到應(yīng)該被調(diào)整的權(quán)重值,從而使得誤差最小化。但在這之前,讓我們了解一下梯度的概念。
什么是梯度?
梯度本質(zhì)上是指向一個(gè)函數(shù)最大斜率的矢量。我們采用來表示梯度,簡單說來,它就是函數(shù)變量偏導(dǎo)數(shù)的矢量形式。
對于一個(gè)雙變量函數(shù),它采用如下形式表示:
讓我們用一些數(shù)字來模擬一個(gè)簡單的例子。假設(shè)我們有一個(gè)函數(shù)是,那么梯度將是
什么是梯度下降?
下降則可以簡單理解為通過梯度來找到我們函數(shù)最大斜率的方向,然后通過反方向小步幅的多次嘗試,從而找到使函數(shù)全局(有時(shí)是局部)誤差值最小的權(quán)重。
我們采用一個(gè)稱為 學(xué)習(xí)率 的常量來表示這個(gè)反方向的小步幅,在公式中我們用來進(jìn)行表征。
如果取值太大,那有可能直接錯(cuò)過最小值,但如果取值太小,那我們的網(wǎng)絡(luò)就會(huì)花費(fèi)更久的時(shí)間來學(xué)習(xí),而且也有可能陷入一個(gè)淺局部最小值。
對于我們例子中的兩個(gè)權(quán)重值和,我們需要找到這兩個(gè)權(quán)重值相較于誤差函數(shù)的梯度
還記得我們上面的公式和嗎?對于和,我們可以將其帶入并通過微積分中的鏈?zhǔn)角髮?dǎo)法則來分別計(jì)算其梯度
簡潔起見,后面我們將采用這個(gè)術(shù)語來表示。
一旦我們有了梯度,將我們擬定的學(xué)習(xí)率帶入,可以通過如下方式來更新權(quán)重值:
然后重復(fù)這個(gè)過程,直到誤差值最小并趨近于零。
代碼示例
附帶的示例采用梯度下降法,將如下數(shù)據(jù)集訓(xùn)練成有兩個(gè)輸入值和一個(gè)輸出值的神經(jīng)網(wǎng)絡(luò):
一旦訓(xùn)練成功,這個(gè)網(wǎng)絡(luò)將會(huì)在輸入兩個(gè) 1 時(shí)輸出 ~0,在輸入 1 和 0 時(shí),輸出 ~1 。
怎么運(yùn)行?
Go
- PS D:\github\ai-simplest-network-master\src> go build -o bin/test.exe
- PS D:\github\ai-simplest-network-master\bin> ./test.exe
- err: 1.7930306267024234
- err: 1.1763080417089242
- ……
- err: 0.00011642621631266815
- err: 0.00010770190838306002
- err: 9.963134967988221e-05
- Finished after 111 iterations
- Results ----------------------
- [1 1] => [0.007421243532258703]
- [1 0] => [0.9879921757260246]
Docker
- docker build -t simplest-network .
- docker run --rm simplest-network
二、最基礎(chǔ)的反向傳播算法
反向傳播(英語:Backpropagation,縮寫為 BP)是“誤差反向傳播”的簡稱,是一種與最優(yōu)化方法(如梯度下降法)結(jié)合使用的,用來訓(xùn)練人工神經(jīng)網(wǎng)絡(luò)的常見方法。
反向傳播技術(shù)可以用來訓(xùn)練至少有一個(gè)隱藏層的神經(jīng)網(wǎng)絡(luò)。下面就來從理論出發(fā)結(jié)合代碼拿下 反向傳播算法 。
示例代碼:https://github.com/gokadin/ai-backpropagation
理論
感知機(jī)介紹
感知機(jī)是這樣一個(gè)處理單元:它接受輸入, 采用激活函數(shù)對其進(jìn)行轉(zhuǎn)換,并輸出結(jié)果。
在一個(gè)神經(jīng)網(wǎng)絡(luò),輸入值是前一層節(jié)點(diǎn)輸出值的權(quán)重加成總和,再加上前一層的誤差:
如果我們把誤差當(dāng)作層中另外的一個(gè)常量為 -1 的節(jié)點(diǎn),那么我們可以簡化這個(gè)公式為
激活函數(shù)
為什么我們需要激活函數(shù)呢?如果沒有,我們每個(gè)節(jié)點(diǎn)的輸出都會(huì)是線性的,從而讓整個(gè)神經(jīng)網(wǎng)絡(luò)都會(huì)是基于輸入值的一個(gè)線性運(yùn)算后的輸出。因?yàn)榫€性函數(shù)組合仍然是線性的,所以必須要引入非線性函數(shù),才能讓神經(jīng)網(wǎng)絡(luò)有區(qū)別于線性回歸模型。
針對,典型的激活函數(shù)有以下形式:
Sigmoid 函數(shù) :
線性整流函數(shù):
tanh 函數(shù):
反向傳播
反向傳播算法可以用來訓(xùn)練人工神經(jīng)網(wǎng)絡(luò),特別是針對具有多于兩層的網(wǎng)絡(luò)。
原理是采用 forward pass 來計(jì)算網(wǎng)絡(luò)輸出值和誤差,再根據(jù)誤差梯度反向更新輸入層的權(quán)重值。
術(shù)語
-
分別是 I, J, K 層節(jié)點(diǎn)的輸入值。
-
分別是 I, J, K 層節(jié)點(diǎn)的輸出值。
-
是 K 輸出節(jié)點(diǎn)的期望輸出值。
-
分別是 I 到 J 層和 J 到 K 層的權(quán)重值。
-
代表 T 組關(guān)聯(lián)中當(dāng)前的一組關(guān)聯(lián)。
在下面的示例中,我們將對不同層節(jié)點(diǎn)采用以下激活函數(shù):
-
輸入層 -> 恒等函數(shù)
-
隱藏層 -> Sigmoid 函數(shù)
-
輸出層 -> 恒等函數(shù)
The forward pass
在 forward pass 中,我們在輸入層進(jìn)行輸入,在輸出層得到結(jié)果。
對于隱藏層的每個(gè)節(jié)點(diǎn)的輸入就是輸入層輸入值的加權(quán)總和:
因?yàn)殡[藏層的激活函數(shù)是 sigmoid,那么輸出將會(huì)是:
同樣,輸出層的輸入值則是
因?yàn)槲覀冑x予了恒等函數(shù)做為激活函數(shù),所以這一層的輸出將等同于輸入值。
一旦輸入值通過網(wǎng)絡(luò)進(jìn)行傳播,我們就可以計(jì)算出誤差值。如果有多套關(guān)聯(lián),還記得我們第一部分學(xué)習(xí)的方差嗎?這里,我們就可以采用平均方差來計(jì)算誤差。
The backward pass
現(xiàn)在我們已經(jīng)得到了誤差,就可以通過反向傳輸,來用誤差來修正網(wǎng)絡(luò)的權(quán)重值。
通過第一部分的學(xué)習(xí),我們知道對權(quán)重的調(diào)整可以基于誤差對權(quán)重的偏導(dǎo)數(shù)乘以學(xué)習(xí)率,即如下形式
我們通過鏈?zhǔn)椒▌t計(jì)算出誤差梯度,如下:
因此,對權(quán)重的調(diào)整即為
對于多個(gè)關(guān)聯(lián),那么對權(quán)重的調(diào)整將為每個(gè)關(guān)聯(lián)的權(quán)重調(diào)整值之和
類似地,對于隱藏層之間的權(quán)重調(diào)整,繼續(xù)以上面的例子為例,輸入層和第一個(gè)隱藏層之間的權(quán)重調(diào)整值為
那么,基于所有關(guān)聯(lián)的權(quán)重調(diào)整即為每次關(guān)聯(lián)計(jì)算得到的調(diào)整值之和
計(jì)算
這里, 我們對可以再做進(jìn)一步的探索。上文中,我們看到。
對前半部分的,我們可以有
對后半部分的,因?yàn)槲覀冊谶@一層采用了 sigmoid 函數(shù),我們知道,sigmoid 函數(shù)的導(dǎo)數(shù)形式是,因此,有
綜上,便可以得到的計(jì)算公式如下
算法總結(jié)
首先,對網(wǎng)絡(luò)權(quán)重值賦予一個(gè)小的隨機(jī)值。
重復(fù)以下步驟,直到誤差為0 :
-
對每次關(guān)聯(lián),通過神經(jīng)網(wǎng)絡(luò)向前傳輸,得到輸出值
-
計(jì)算每個(gè)輸出節(jié)點(diǎn)的誤差 ()
-
疊加計(jì)算每個(gè)輸出權(quán)重的梯度 ()
-
計(jì)算隱藏層每個(gè)節(jié)點(diǎn)的()
-
疊加計(jì)算每個(gè)隱藏層權(quán)重的梯度 ()
-
-
更新所有權(quán)重值,重置疊加梯度 ()
圖解反向傳播
在這個(gè)示例中,我們通過真實(shí)數(shù)據(jù)來模擬神經(jīng)網(wǎng)絡(luò)中的每個(gè)步驟。輸入值是[1.0, 1.0],輸出期望值為 [0.5]。為了簡化,我們將初始化權(quán)重設(shè)為 0.5 (雖然在實(shí)際操作中,經(jīng)常會(huì)采用隨機(jī)值)。對于輸入、隱藏和輸出層,我們分別采用恒等函數(shù)、 sigmoid 函數(shù) 和恒等函數(shù)作為激活函數(shù),學(xué)習(xí)率則定為 0.01 。
Forward pass
運(yùn)算一開始,我們將輸入層的節(jié)點(diǎn)輸入值設(shè)為。
因?yàn)槲覀儗斎雽硬捎玫氖呛愕群瘮?shù)作為激活函數(shù),因此有。
接下來,我們通過對前一層的加權(quán)總和將網(wǎng)絡(luò)向前傳遞到 J 層,如下
然后,我們將 J 層節(jié)點(diǎn)的值輸入到 sigmoid 函數(shù)(,將代入,得到 0.731)進(jìn)行激活。
最后,我們將這個(gè)結(jié)果傳遞到最后的輸出層。
因?yàn)槲覀冚敵鰧拥募せ詈瘮?shù)也是恒等函數(shù),因此
Backward pass
反向傳播的第一步,是計(jì)算輸出節(jié)點(diǎn)的,
采用計(jì)算 J 和 K 兩層節(jié)點(diǎn)間的權(quán)重梯度:
接下來,以同樣的方法計(jì)算每個(gè)隱藏層的值(在本示例中,僅有一個(gè)隱藏層):
針對 I 和 J 層節(jié)點(diǎn)權(quán)重計(jì)算其梯度為:
最后一步是用計(jì)算出的梯度更新所有的權(quán)重值。注意這里如果我們有多于一個(gè)的關(guān)聯(lián),那么便可以針對每組關(guān)聯(lián)的梯度進(jìn)行累計(jì),然后更新權(quán)重值。
可以看到這個(gè)權(quán)重值變化很小,但如果我們用這個(gè)權(quán)重再跑一遍 forward pass,一般來說將會(huì)得到一個(gè)比之前更小的誤差。讓我們現(xiàn)在來看下……
第一遍我們得到的,采用新的權(quán)重值計(jì)算得到。
由此,,而。
可見,誤差得到了減??!盡管減少值很小,但對于一個(gè)真實(shí)場景也是很有代表性的。按照該算法重復(fù)運(yùn)行,一般就可以將誤差最終減小到0,那么便完成了對神經(jīng)網(wǎng)絡(luò)的訓(xùn)練。
代碼示例
本示例中,將一個(gè) 2X2X1 的網(wǎng)絡(luò)訓(xùn)練出 XOR 運(yùn)算符的效果。
這里,f 是針對隱藏層的 sigmoid 激活函數(shù)。
注意,XOR 運(yùn)算符是不能通過第一部分中的線性網(wǎng)絡(luò)進(jìn)行模擬的,因?yàn)閿?shù)據(jù)集分布是非線性的。也就是你不能通過一條直線將 XOR 的四個(gè)輸入值正確劃分到兩類中。如果我們將 sigmoid 函數(shù)換為恒等函數(shù),這個(gè)網(wǎng)絡(luò)也將是不可行的。
講完這么多,輪到你自己來動(dòng)手操作啦!試試采用不同的激活函數(shù)、學(xué)習(xí)率和網(wǎng)絡(luò)拓?fù)洌纯葱Ч绾危?/p>
感謝原文作者的授權(quán):