簡(jiǎn)介
本文的目的是不借助任何深度學(xué)習(xí)算法庫(kù)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的3層全連接神經(jīng)網(wǎng)絡(luò)(FCN),并將其用于MNIST手寫(xiě)數(shù)字識(shí)別任務(wù)虑绵。
文章將依次介紹感知器模型挤庇、S型神經(jīng)元寥粹、全連接神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)、梯度下降等內(nèi)容窝趣,并在最后給出完整代碼推正。為了避免打擊讀者的信心,這里可以提前告訴大家缸濒,最后的代碼除掉注釋在100行之內(nèi)足丢。是不是覺(jué)得很興奮?我們不借助其他神經(jīng)網(wǎng)絡(luò)庫(kù)庇配,僅僅使用少于100行的代碼就可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單的全連接神經(jīng)網(wǎng)絡(luò)斩跌,并且它在MNIST數(shù)據(jù)集上的識(shí)別正確率可以達(dá)到96%以上。
接下來(lái)就讓我們開(kāi)始進(jìn)入正題吧讨永!
神經(jīng)網(wǎng)絡(luò)概覽
我們要設(shè)計(jì)的全連接神經(jīng)網(wǎng)絡(luò)的具體功能是滔驶,給定一張手寫(xiě)數(shù)字圖片,神經(jīng)網(wǎng)絡(luò)要能識(shí)別出來(lái)這個(gè)數(shù)字是幾卿闹。
聽(tīng)起來(lái)像很簡(jiǎn)單的任務(wù)是嗎揭糕?但是如果使用傳統(tǒng)的計(jì)算機(jī)程序來(lái)識(shí)別諸如上圖中的數(shù)字,就會(huì)明顯感受到視覺(jué)模式識(shí)別的困難锻霎。因?yàn)橐酝覀兙帉?xiě)的計(jì)算機(jī)程序往往擅長(zhǎng)處理各種提前制定好的規(guī)則著角。比如要讀取一張圖片,我們會(huì)先判斷圖片的格式旋恼,根據(jù)具體的格式解碼出圖像內(nèi)容再顯示出來(lái)吏口。而對(duì)于數(shù)字識(shí)別這個(gè)任務(wù),規(guī)則變得十分模糊與復(fù)雜冰更,如果我們?cè)囍屵@些識(shí)別規(guī)則變得越發(fā)精準(zhǔn)的時(shí)候产徊,就會(huì)很快陷入各種混亂的場(chǎng)景和異常情況中,這并不是一條可行的道路蜀细。
傳統(tǒng)的方式行不通舟铜,那我們?nèi)绾蝸?lái)解決這個(gè)問(wèn)題呢?首先回想一下小時(shí)候我們是怎么學(xué)習(xí)認(rèn)數(shù)字的奠衔,老師會(huì)把數(shù)字0~9挨個(gè)給我們看谆刨,然后告訴我們它們分別是幾。如果我們不小心認(rèn)錯(cuò)了归斤,老師會(huì)即時(shí)糾正我們痊夭,經(jīng)過(guò)一段時(shí)間的練習(xí)和記憶之后,我們就可以認(rèn)得數(shù)字了脏里。
神經(jīng)網(wǎng)絡(luò)也有異曲同工之妙她我,其思想是首先獲取大量的手寫(xiě)數(shù)字去建立一個(gè)訓(xùn)練數(shù)據(jù)集,然后開(kāi)發(fā)一個(gè)可以從這些訓(xùn)練數(shù)據(jù)中進(jìn)行學(xué)習(xí)的系統(tǒng),接著使用訓(xùn)練數(shù)據(jù)集去訓(xùn)練這個(gè)系統(tǒng)番舆,最后系統(tǒng)便能夠識(shí)別手寫(xiě)數(shù)字了根吁。換言之,神經(jīng)網(wǎng)絡(luò)使用訓(xùn)練樣本來(lái)自動(dòng)推斷出識(shí)別手寫(xiě)數(shù)字的規(guī)則合蔽。
根據(jù)上面的描述,可以歸納出構(gòu)成全連接網(wǎng)絡(luò)的幾個(gè)要素:
- 訓(xùn)練數(shù)據(jù)集(MNIST)
- 可以從訓(xùn)練樣本中自學(xué)習(xí)的系統(tǒng)(全連接網(wǎng)絡(luò)結(jié)構(gòu))
- 訓(xùn)練系統(tǒng)的方法(梯度下降+反向傳播)
接下來(lái)會(huì)分別從這幾個(gè)方面來(lái)介紹介返。
MNIST
MNIST是一個(gè)手寫(xiě)數(shù)字集合拴事,包含了60000張測(cè)試圖片和10000張測(cè)試圖片,每張圖片分辨率為28x28圣蝎,像素點(diǎn)數(shù)值取值范圍0~255刃宵。MNIST 的名字來(lái)源于NIST——美國(guó)國(guó)家標(biāo)準(zhǔn)與技術(shù)研究所——收集的兩個(gè)數(shù)據(jù)集改進(jìn)后的子集,關(guān)于MNIST數(shù)據(jù)集更多信息可以自行搜索徘公。
MNIST數(shù)據(jù)集在機(jī)器學(xué)習(xí)牲证、深度學(xué)習(xí)領(lǐng)域的地位類(lèi)似于各類(lèi)編程語(yǔ)言里學(xué)的”Hello World“程序。
我們?cè)O(shè)計(jì)的神經(jīng)網(wǎng)絡(luò)并不會(huì)直接使用原始圖像关面,而是會(huì)將其轉(zhuǎn)換成向量的形式坦袍,即每張?jiān)紙D像可以看做是一個(gè)28x28=784維的向量,向量中的每一維分別代表原始圖像中對(duì)應(yīng)像素點(diǎn)的灰度值等太。
全連接網(wǎng)絡(luò)結(jié)構(gòu)
感知器模型
在介紹全連接網(wǎng)絡(luò)結(jié)構(gòu)之前捂齐,我們先了解一下感知器模型。雖然我們最后的神經(jīng)網(wǎng)絡(luò)中并不包含這個(gè)模型缩抡,但是下面要介紹到的S型神經(jīng)元是由感知器模型演變過(guò)去的奠宜,因此值得花點(diǎn)時(shí)間來(lái)理解下感知器。
上圖中的感知器有3個(gè)輸入瞻想,压真。通常可以有更多或者更少輸入蘑险。緊挨著的3條邊分別代表3個(gè)權(quán)重滴肿,。圓圈里面是一個(gè)Step函數(shù)漠其,Step函數(shù)的圖像如下:
直觀地解釋就是嘴高,對(duì)于每一個(gè)輸入,將其乘上對(duì)應(yīng)的權(quán)值和屎,再累加起來(lái)得到一個(gè)中間結(jié)果拴驮,再將通過(guò)一個(gè)階躍函數(shù),如果大于0柴信,則輸出1套啤;反之,輸出0。這就是一個(gè)感知器所要做的全部事情潜沦。
這是一個(gè)基本的數(shù)學(xué)模型萄涯,你可以將感知器看做是一種可以根據(jù)權(quán)重來(lái)做出決定的機(jī)器。我們可以通過(guò)手動(dòng)改變模型中的權(quán)重以及圓圈中的函數(shù)來(lái)得到不同的感知器模型唆鸡,它們對(duì)同一輸入可以做出不同的決策涝影。
我們將上述的簡(jiǎn)單感知器進(jìn)行組合,得到下面稍微復(fù)雜點(diǎn)的多層感知器模型:
在這個(gè)網(wǎng)絡(luò)中争占,第一列感知器——我們稱(chēng)其為第一層感知器——通過(guò)權(quán)衡輸入依據(jù)做出三個(gè)非常簡(jiǎn)單的決定燃逻。那第二層的感知器呢?每一個(gè)都在權(quán)衡第一層的決策結(jié)果并做出決定。以這種方式臂痕,一個(gè)第二層中的感知器可以比第一層中的做出更復(fù)雜和抽象的決策伯襟。在第三層中的感知器甚至能進(jìn)行更復(fù)雜的決策。以這種方式握童,一個(gè)多層的感知器網(wǎng)絡(luò)可以從事復(fù)雜巧妙的決策姆怪。
那這個(gè)模型跟我們的手寫(xiě)數(shù)字識(shí)別有什么關(guān)系呢?
換個(gè)角度來(lái)思考這個(gè)問(wèn)題澡绩,既然感知器的輸出是0或者1稽揭,那是不是意味著我們其實(shí)就可以用它來(lái)做單一手寫(xiě)數(shù)字圖像分類(lèi)呢?比如判斷一張圖像中的數(shù)字是不是”9“肥卡。如果圖片顯示的是“9”淀衣,就輸出1,否則輸出0召调。很顯然膨桥,我們的感知器一開(kāi)始并不知道如何判斷圖像是不是包含”9“,但是別忘了唠叛,我們可以修改權(quán)值和圓圈中的激活函數(shù)來(lái)讓感知器做出正確的分類(lèi)只嚣。
S型神經(jīng)元
如果我們對(duì)權(quán)值或者激活函數(shù)中的偏置做出的微小改動(dòng)就能夠引起輸出的微小變化,那我們就可以利用這一事實(shí)來(lái)修改權(quán)值和激活函數(shù)艺沼,讓網(wǎng)絡(luò)能夠表現(xiàn)得像我們想要的那樣册舞。假設(shè)網(wǎng)絡(luò)錯(cuò)誤地把一個(gè)“9”的圖像分類(lèi)為“8”,我們能夠計(jì)算出怎么對(duì)權(quán)重和偏置做些小的改動(dòng)障般,這樣網(wǎng)絡(luò)能夠接近于把圖像分類(lèi)為“9”调鲸。然后我們要重復(fù)這個(gè)工作,反復(fù)改動(dòng)權(quán)重和偏置來(lái)產(chǎn)生更好的輸出挽荡,此時(shí)網(wǎng)絡(luò)其實(shí)就是在自我學(xué)習(xí)藐石。
但是我們目前的感知器網(wǎng)絡(luò)無(wú)法做到這一點(diǎn)。因?yàn)楦兄鞯妮敵鲆词?定拟,要么是1于微。對(duì)權(quán)重和偏置的微小改動(dòng)可能會(huì)讓感知器的輸出發(fā)生大反轉(zhuǎn),比如從0變成1。在多層感知器模型里面株依,這樣的反轉(zhuǎn)可能使得與此感知器連接的其余網(wǎng)絡(luò)的行為完全改變驱证。因此,也許”9“可以被正確分類(lèi)恋腕,網(wǎng)絡(luò)在其它圖像上的行為很可能以一些很難控制的方式被完全改變抹锄。這使得逐步修改權(quán)重和偏置來(lái) 讓網(wǎng)絡(luò)接近期望行為變得困難。
因此我們引入一種稱(chēng)為S型神經(jīng)元的新的人工神經(jīng)元來(lái)克服這個(gè)問(wèn)題荠藤,它與感知器類(lèi)似祈远,但是對(duì)權(quán)值和激活函數(shù)的偏置的微小改動(dòng)只會(huì)引起輸出的微笑改變,而不會(huì)像感知器那樣完全反轉(zhuǎn)商源。
可以看到它與感知器模型非常類(lèi)似,S 型神經(jīng)元有多個(gè)輸入谋减,牡彻。這些輸入可以是0和1中的任意值。同樣S型神經(jīng)元對(duì)每個(gè)輸入都有權(quán)重出爹,庄吼,和一個(gè)總的偏置b。但是它的輸出不再是0或者1严就,而是总寻,這里的也寫(xiě)成,的定義如下:
函數(shù)的圖像如下:
為了理解和感知器模型的相似性梢为,假設(shè)是一個(gè)很大的正數(shù)渐行,那么, 而撕氧。即當(dāng)很大并且為正匪蝙,S 型神經(jīng)元的輸出近似為 1,正好和感知器一樣宙枷。 相反地粟害,假設(shè)是一個(gè)很大的負(fù)數(shù)蕴忆。那么。所以當(dāng)是一個(gè)很大的負(fù)數(shù)悲幅,S 型神經(jīng)元的行為也非常近似一個(gè)感知器套鹅。只有在取中間值時(shí),和感知器模型有比較大的偏離汰具。 我們應(yīng)該如何解釋一個(gè)S型神經(jīng)元的輸出呢? 很明顯感知器和 S 型神經(jīng)元之間一個(gè)很大的不同是S型神經(jīng)元不僅僅輸出0或1卓鹿,它可以輸出0和1之間的任何實(shí)數(shù)。假設(shè)我們希望網(wǎng)絡(luò)的輸出是表示”輸入圖像是一個(gè)9“或”輸入圖像不是一個(gè)9“的話留荔,此時(shí)網(wǎng)絡(luò)輸出0或者1會(huì)更容易表示這種情況减牺。此時(shí)我們可以采用以下約定,即以0.5為分界點(diǎn),將S型神經(jīng)元的輸出轉(zhuǎn)換成0或者1拔疚,具體表述如下:
一個(gè)簡(jiǎn)單的分類(lèi)手寫(xiě)數(shù)字的網(wǎng)絡(luò)
在理解基礎(chǔ)神經(jīng)元模型之后肥隆,我們就可以構(gòu)建較為復(fù)雜的神經(jīng)網(wǎng)絡(luò)。本文中我們將使用一個(gè)3層的神經(jīng)網(wǎng)絡(luò)來(lái)識(shí)別單個(gè)數(shù)字稚失,網(wǎng)絡(luò)結(jié)構(gòu)如下:網(wǎng)絡(luò)的第一層是輸入層栋艳,由于我們的輸入數(shù)據(jù)是28x28的圖像,將其攤平成28x28=784維的向量句各,所以輸入層包含有784個(gè)神經(jīng)元吸占。為了簡(jiǎn)化,上圖中忽略了 784 中大部分的輸入神經(jīng)元凿宾。輸入像素是灰度級(jí)的矾屯, 值為 0.0 表示白色,值為 1.0 表示黑色初厚,中間數(shù)值表示逐漸暗淡的灰色件蚕。
網(wǎng)絡(luò)的第二層是一個(gè)隱藏層,僅僅包含了15個(gè)神經(jīng)元产禾,當(dāng)然我們其實(shí)可以根據(jù)需要?jiǎng)討B(tài)調(diào)整這個(gè)參數(shù)以觀察不同隱藏層神經(jīng)元個(gè)數(shù)對(duì)最終結(jié)果的影響排作。
網(wǎng)絡(luò)的第三層是輸出層,包含有 10 個(gè)神經(jīng)元亚情。如果第一個(gè)神經(jīng)元激活妄痪,即輸出 ≈ 1,那么表明網(wǎng)絡(luò)認(rèn)為數(shù)字是一個(gè)”0“楞件。如果第二個(gè)神經(jīng)元激活衫生,就表明網(wǎng)絡(luò)認(rèn)為數(shù)字是一個(gè)”1“。依此類(lèi)推土浸。更確切地說(shuō)障簿, 我們把輸出神經(jīng)元的輸出賦予編號(hào) 0 到 9,并計(jì)算出哪個(gè)神經(jīng)元有最高的激活值栅迄。比如站故,如果編號(hào)為 6 的神經(jīng)元激活,那么我們的網(wǎng)絡(luò)會(huì)猜到輸入的數(shù)字是”6“毅舆,其它神經(jīng)元相同西篓。
梯度下降
有了訓(xùn)練數(shù)據(jù)集和定義好的神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)之后,它將怎樣學(xué)習(xí)識(shí)別數(shù)字呢憋活?
對(duì)于上圖中的神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)岂津,假設(shè)我們隨機(jī)初始化各個(gè)神經(jīng)元的權(quán)值,并且將手寫(xiě)數(shù)字圖片”9“傳入到網(wǎng)絡(luò)中悦即,那么可以預(yù)見(jiàn)網(wǎng)絡(luò)大概率并不會(huì)得到正確答案吮成,但是我們可以通過(guò)不斷調(diào)整權(quán)值來(lái)讓它輸出正確的結(jié)果橱乱。對(duì)于簡(jiǎn)單的模型也許這樣做是可行的,因?yàn)楫吘箍梢哉{(diào)整的權(quán)重?cái)?shù)據(jù)很少粱甫,但是對(duì)于我們定義的網(wǎng)絡(luò)泳叠,它其中包含了784x15+15x10+15+10 = 11935個(gè)參數(shù),手動(dòng)修改簡(jiǎn)直是一場(chǎng)噩夢(mèng)茶宵。
這里的代表的是網(wǎng)絡(luò)中所有權(quán)重的集合,是所有的偏置瞒大,是訓(xùn)練輸入數(shù)據(jù)的個(gè)數(shù)螃征,是輸入對(duì)應(yīng)的標(biāo)簽,是表示當(dāng)輸入為時(shí)神經(jīng)網(wǎng)絡(luò)輸出的向量透敌,求和則是在總的訓(xùn)練輸入上進(jìn)行的盯滚。
我們把稱(chēng)之為二次代價(jià)函數(shù),也稱(chēng)均方誤差或者M(jìn)SE拙泽。
通過(guò)觀察可以發(fā)現(xiàn),損失函數(shù)的值是非負(fù)的裸燎,因?yàn)榍蠛凸街械拿恳豁?xiàng)都是非負(fù)的顾瞻。如果代價(jià)函數(shù) 的值相當(dāng)小,即 德绿,那么意味著荷荤,對(duì)于所有的訓(xùn)練輸入, 接近于神經(jīng)網(wǎng)絡(luò)輸出移稳。相反蕴纳, 當(dāng)很大時(shí)就不怎么好了,那意味著對(duì)于大量地輸入个粱,與輸出相差很大古毛。因此我們的訓(xùn)練算法的目的,是要最小化代價(jià)函數(shù)都许,使稻薇。
現(xiàn)在我們忘掉之前講過(guò)的各種神經(jīng)元、網(wǎng)絡(luò)結(jié)構(gòu)胶征、MNIST等等塞椎,把注意力集中在一個(gè)問(wèn)題上,那就是如何最小化一個(gè)給定的多元函數(shù)睛低?我們使用一種被稱(chēng)之為梯度下降的技術(shù)來(lái)解決這樣的最小化問(wèn)題案狠。
假設(shè)我們要最小化某些函數(shù)服傍,如。它可以是任意的多元實(shí)值函數(shù)骂铁,吹零。 注意我們用 代替了 和 以強(qiáng)調(diào)它可能是任意的函數(shù),這里我們以只有兩個(gè)變量,的二元函數(shù)舉例从铲,圖像如下:
這里你也許會(huì)問(wèn)為什么拿二元函數(shù)舉例瘪校,主要原因是因?yàn)榭梢詫⑵淇梢暬绻瑑蓚€(gè)自變量以上的話名段,我們就無(wú)法畫(huà)出它對(duì)應(yīng)的圖像了阱扬。
從圖中可以一眼看到的最小值在(0,0)處取得,但是通常函數(shù)是一個(gè)復(fù)雜的多元函數(shù)伸辟,遠(yuǎn)不止2個(gè)自變量麻惶,因此看一眼就能找到最小值是不太現(xiàn)實(shí)的,此時(shí)梯度下降法就可以發(fā)揮用處了信夫。
梯度下降法的基本思想可以類(lèi)比為一個(gè)下山的過(guò)程窃蹋。假設(shè)這樣一個(gè)場(chǎng)景:一個(gè)人被困在山上,需要從山上下來(lái)(i.e. 找到山的最低點(diǎn)静稻,也就是山谷)警没。但此時(shí)山上的濃霧很大,導(dǎo)致可視度很低振湾。因此杀迹,下山的路徑就無(wú)法確定,他必須利用自己周?chē)男畔⑷フ业较律降穆窂窖禾隆_@個(gè)時(shí)候树酪,他就可以利用梯度下降算法來(lái)幫助自己下山。具體來(lái)說(shuō)就是大州,以他當(dāng)前的所處的位置為基準(zhǔn)续语,尋找這個(gè)位置最陡峭的地方,然后朝著山的高度下降的地方走厦画,同理疮茄,如果我們的目標(biāo)是上山,也就是爬到山頂根暑,那么此時(shí)應(yīng)該是朝著最陡峭的方向往上走娃豹。然后每走一段距離,都反復(fù)采用同一個(gè)方法购裙,最后就能成功的抵達(dá)山谷懂版。
我們同時(shí)可以假設(shè)這座山最陡峭的地方是無(wú)法通過(guò)肉眼立馬觀察出來(lái)的,而是需要一個(gè)復(fù)雜的工具來(lái)測(cè)量躏率,同時(shí)躯畴,這個(gè)人此時(shí)正好擁有測(cè)量出最陡峭方向的能力民鼓。所以,此人每走一段距離蓬抄,都需要一段時(shí)間來(lái)測(cè)量所在位置最陡峭的方向丰嘉,這是比較耗時(shí)的。那么為了在太陽(yáng)下山之前到達(dá)山底嚷缭,就要盡可能的減少測(cè)量方向的次數(shù)饮亏。這是一個(gè)兩難的選擇,如果測(cè)量的頻繁阅爽,可以保證下山的方向是絕對(duì)正確的路幸,但又非常耗時(shí),如果測(cè)量的過(guò)少付翁,又有偏離軌道的風(fēng)險(xiǎn)简肴。所以需要找到一個(gè)合適的測(cè)量方向的頻率,來(lái)確保下山的方向不錯(cuò)誤百侧,同時(shí)又不至于耗時(shí)太多砰识!
關(guān)于梯度下降法的詳細(xì)解釋可以參考這篇博客,寫(xiě)的非常詳細(xì)佣渴,強(qiáng)烈推薦辫狼。
回到我們的問(wèn)題中來(lái),首先把我們的函數(shù)想象成一個(gè)山谷辛润,現(xiàn)在有一個(gè)人被困在了山上的某一處∨虼Γ現(xiàn)在我們讓這個(gè)人沿著和方向移動(dòng)一個(gè)很小的量,即和频蛔,微積分告訴我們函數(shù)將會(huì)有如下變化:
為了使往更小的方向變化灵迫,我們需要尋找一種選擇和的方法使得為負(fù)秦叛,即讓小人往下山的方向走晦溪。為了弄明白如何選擇,需要定義為的變化的向量挣跋,即是轉(zhuǎn)置符號(hào)三圆。我們也定義的梯度為偏導(dǎo)數(shù)的向量,避咆。我們使用來(lái)表示梯度向量舟肉,即:
有了這些定義,的表達(dá)式可以被重寫(xiě)成:
這個(gè)表達(dá)式解釋了為什么 被稱(chēng)為梯度向量: 把 的變化關(guān)聯(lián)為 的變化查库,正如我們期望的用梯度來(lái)表示路媚,但是這個(gè)方程真正讓我們興奮的是它讓我們看到了如何選取 才能讓 變?yōu)樨?fù)數(shù)。假設(shè)我們選取:
這里的是一個(gè)很小的數(shù)樊销,稱(chēng)之為學(xué)習(xí)率整慎。那么脏款。由于,這就可以保證裤园,即如果我們按照上式的規(guī)則去改變自變量撤师,那么就會(huì)一直減小,即小人會(huì)一直往下山的方向移動(dòng)拧揽。我們令代表改變后的新的自變量剃盾,將上式展開(kāi)得到:
由此我們得到了自變量的更新方式,可以使用它來(lái)計(jì)算下一次的移動(dòng)方向和距離淤袜。如果我們反復(fù)這么做痒谴,那么我們將持續(xù)減小,正如我們希望的饮怯,得到一個(gè)全局的最小值闰歪。
總結(jié)一下,梯度下降算法工作的方式就是重復(fù)計(jì)算梯度 蓖墅,然后沿著相反的方向移動(dòng)库倘。我們可以想象它像這樣:
這里需要介紹一下上面提到的學(xué)習(xí)率,可以看到它的主要功能是用來(lái)控制小人每次移動(dòng)的步長(zhǎng)论矾。如果太大教翩,那么每次的變化很劇烈,即小人每次邁的步子很大贪壳,那么當(dāng)小人很快就要到谷底的時(shí)候饱亿,結(jié)果因?yàn)椴阶舆~得太大,錯(cuò)過(guò)了最低點(diǎn)闰靴,這肯定不是我們希望看到的彪笼。同理,我們也不希望太小蚂且,那意味著每次變化很小配猫,即小人邁的步子很小,行動(dòng)緩慢杏死,這就導(dǎo)致需要花很長(zhǎng)時(shí)間才能到達(dá)谷底泵肄。因此在真正的實(shí)現(xiàn)中,通常是變化的淑翼。比如我們希望在一開(kāi)始比較大腐巢,而快到谷底的時(shí)候稍微變小點(diǎn),即小人最開(kāi)始大步往山下走玄括,當(dāng)快到谷底的時(shí)候冯丙,小心翼翼地挪動(dòng)步伐,防止錯(cuò)過(guò)谷底遭京。
我們解釋了只有兩個(gè)自變量的函數(shù)的梯度下降算法胃惜,但事實(shí)上风宁,即使是一個(gè)具有多個(gè)變量的函數(shù)時(shí),梯度下降算法也能很好地工作蛹疯。假設(shè)是一個(gè)具有m個(gè)變量的多元函數(shù)戒财。那么對(duì)中的自變量的變化,會(huì)變成:
這里的梯度是向量:
自變量的更新規(guī)則為:
至此饮寞,梯度下降算法就介紹完畢了。
隨機(jī)梯度下降算法
我們?cè)趺丛谏窠?jīng)網(wǎng)絡(luò)中使用梯度下降算法去學(xué)習(xí)呢列吼?其思想就是利用梯度下降算法尋找使得我們的全連接神經(jīng)網(wǎng)絡(luò)的損失函數(shù)取得最小值時(shí)的權(quán)重和幽崩。我估計(jì)你肯定忘了我們的損失函數(shù)是什么了,我將其重新寫(xiě)在下面:
相比之前的函數(shù)寞钥,這里相當(dāng)于把原先的自變量和換成了和慌申,而梯度向量則變成了 和。用這些分量來(lái)寫(xiě)梯度下降的更新規(guī)則理郑,可以得到:
注意不要認(rèn)為我們的損失函數(shù)也是只有兩個(gè)變量和哦
你可能注意到了這一節(jié)的標(biāo)題叫做隨機(jī)梯度下降蹄溉,既然上一節(jié)已經(jīng)有了梯度下降,怎么又來(lái)個(gè)隨機(jī)梯度下降您炉?
其中代表的是一個(gè)訓(xùn)練樣本的損失函數(shù)赚爵,也就是是總體的損失函數(shù)是對(duì)每一個(gè)訓(xùn)練樣本的損失函數(shù)的值累加之后再求平均棉胀。故為了計(jì)算梯度,我們需要遍歷整個(gè)訓(xùn)練數(shù)據(jù)集冀膝,對(duì)每一個(gè)訓(xùn)練樣本都計(jì)算梯度唁奢,然后累加之后求平均值。這樣導(dǎo)致的最直接的問(wèn)題就是當(dāng)好訓(xùn)練集很大的時(shí)候窝剖,訓(xùn)練速度會(huì)非常的慢麻掸。
為了加速訓(xùn)練過(guò)程,在實(shí)際使用中枯芬,我們會(huì)采用隨機(jī)梯度下降的算法论笔。它的原理也非常樸素采郎,就是我們不采用整個(gè)數(shù)據(jù)集千所,而是隨機(jī)選取小量訓(xùn)練樣本來(lái)計(jì)算梯度,進(jìn)而估算實(shí)際梯度蒜埋。通過(guò)計(jì)算少量樣本的平均值我們就可以快速得到一個(gè)對(duì)于實(shí)際梯度很好的估算淫痰,這有助于加速梯度下降,進(jìn)而加快訓(xùn)練過(guò)程整份。
再用小人下山的例子來(lái)對(duì)比一下梯度下降算法和隨機(jī)梯度下降算法待错。假設(shè)小人使用梯度下降算法下山籽孙,那意味著小人每走一步之前,都需要經(jīng)過(guò)精確的計(jì)算以便找到最陡峭的方向火俄,然后沿著這個(gè)方向邁出一步犯建,畢竟按照這個(gè)方向下山肯定是最快的。而如果小人使用隨機(jī)梯度算法下山的話瓜客,他并不會(huì)花那么長(zhǎng)的時(shí)間去找到最陡峭的方向适瓦,而是找到一個(gè)大概的方向就行了,然后就快速地邁出一步谱仪。畢竟只要方向基本不錯(cuò)玻熙,最終肯定也能到達(dá)谷底。
隨機(jī)梯度下降通過(guò)隨機(jī)選取小量的 m 個(gè)訓(xùn)練樣本來(lái)工作疯攒。我們將這些隨機(jī)的訓(xùn)練樣本標(biāo)記為 嗦随,并把它們稱(chēng)為一個(gè)小批量數(shù)據(jù)(mini-batch)。假設(shè)樣本數(shù)量 m 足夠大敬尺,我們期望 的平均值大致相等于整個(gè) 的平均值枚尼,即:
證實(shí)了我們可以僅僅計(jì)算隨機(jī)選取的小批量數(shù)據(jù)的梯度來(lái)估算整體梯度。
為了明確地和神經(jīng)網(wǎng)絡(luò)的學(xué)習(xí)聯(lián)系起來(lái)砂吞,假設(shè)和表示我們神經(jīng)網(wǎng)絡(luò)中的權(quán)重和偏置姑原,隨機(jī)梯度下降通過(guò)隨機(jī)地選取小批量的訓(xùn)練樣本來(lái)工作,因此我們可以更改一下梯度下降的更新規(guī)則:
其中兩個(gè)求和符號(hào)是在當(dāng)前小批量數(shù)據(jù)中的所有訓(xùn)練樣本 上進(jìn)行的呜舒。我們隨機(jī)挑選若干個(gè)小批量數(shù)據(jù)集锭汛,然后以此用這些小批量數(shù)據(jù)集去訓(xùn)練網(wǎng)絡(luò),當(dāng)用完了全部的小批量數(shù)據(jù)集之后袭蝗,這被稱(chēng)為完成了一次迭代周期(epoch)唤殴,然后開(kāi)始新一輪的訓(xùn)練迭代周期。
反向傳播
限于篇幅到腥,略過(guò)朵逝,詳情請(qǐng)看這篇博客。
代碼實(shí)踐
本文采用的代碼來(lái)自Github乡范,但是原作者是基于Python2.7寫(xiě)的配名,我用python3將其改寫(xiě)了一遍,并且加上了很多注釋晋辆。
代碼鏈接在https://github.com/HeartbreakSurvivor/ClassicNetworks/tree/master/FCN
總共包含3個(gè)文件渠脉,分別是"mnist_loader.py"、"fc.py"瓶佳、"mnist.pkl.gz"芋膘。
下面展示實(shí)現(xiàn)了神經(jīng)網(wǎng)絡(luò)的fc.py的內(nèi)容:
import random
import numpy as np
import mnist_loader
def sigmoid(z):
"""
Sigmoid激活函數(shù)定義
"""
return 1.0/(1.0 + np.exp(-z))
def sigmoid_prime(z):
"""
Sigmoid函數(shù)的導(dǎo)數(shù),關(guān)于Sigmoid函數(shù)的求導(dǎo)可以自行搜索。
"""
return sigmoid(z)*(1-sigmoid(z))
class FCN(object):
"""
全連接網(wǎng)絡(luò)的純手工實(shí)現(xiàn)
"""
def __init__(self, sizes):
"""
:param sizes: 是一個(gè)列表,其中包含了神經(jīng)網(wǎng)絡(luò)每一層的神經(jīng)元的個(gè)數(shù)为朋,列表的長(zhǎng)度就是神經(jīng)網(wǎng)絡(luò)的層數(shù)臂拓。
舉個(gè)例子,假如列表為[784,30,10]习寸,那么意味著它是一個(gè)3層的神經(jīng)網(wǎng)絡(luò)胶惰,第一層包含784個(gè)神經(jīng)元,第二層30個(gè)霞溪,最后一層10個(gè)童番。
注意,神經(jīng)網(wǎng)絡(luò)的權(quán)重和偏置是隨機(jī)生成的威鹿,使用一個(gè)均值為0剃斧,方差為1的高斯分布。
注意第一層被認(rèn)為是輸入層忽你,它是沒(méi)有偏置向量b和權(quán)重向量w的幼东。因?yàn)槠弥皇怯脕?lái)計(jì)算第二層之后的輸出
"""
self._num_layers = len(sizes) # 記錄神經(jīng)網(wǎng)絡(luò)的層數(shù)
# 為隱藏層和輸出層生成偏置向量b,還是以[784,30,10]為例科雳,那么一共會(huì)生成2個(gè)偏置向量b根蟹,分別屬于隱藏層和輸出層,大小分別為30x1,10x1糟秘。
self._biases = [np.random.randn(y, 1) for y in sizes[1:]]
# 為隱藏層和輸出層生成權(quán)重向量W, 以[784,30,10]為例简逮,這里會(huì)生成2個(gè)權(quán)重向量w,分別屬于隱藏層和輸出層尿赚,大小分別是30x784, 10x30散庶。
self._weights = [np.random.randn(y, x) for x,y in zip(sizes[:-1], sizes[1:])]
# print(self._biases[0].shape)
# print(self._biases[1].shape)
# print(self._weights[0].shape)
# print(self._weights[1].shape)
def feedforward(self, a):
"""
前向計(jì)算,返回神經(jīng)網(wǎng)絡(luò)的輸出凌净。公式如下:
output = sigmoid(w*x+b)
以[784,30,10]為例悲龟,權(quán)重向量大小分別為[30x784, 10x30],偏置向量大小分別為[30x1, 10x1]
輸入向量為 784x1.
矩陣的計(jì)算過(guò)程為:
30x784 * 784x1 = 30x1
30x1 + 30x1 = 30x1
10x30 * 30x1 = 10x1
10x1 + 10x1 = 10x1
故最后的輸出是10x1的向量冰寻,即代表了10個(gè)數(shù)字须教。
:param a: 神經(jīng)網(wǎng)絡(luò)的輸入
"""
for b, w in zip(self._biases, self._weights):
a = sigmoid(np.dot(w, a) + b)
return a
def SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None):
"""
使用小批量隨機(jī)梯度下降來(lái)訓(xùn)練網(wǎng)絡(luò)
:param training_data: training data 是一個(gè)元素為(x, y)元祖形式的列表,代表了訓(xùn)練數(shù)據(jù)的輸入和輸出斩芭。
:param epochs: 訓(xùn)練輪次
:param mini_batch_size: 小批量訓(xùn)練樣本數(shù)據(jù)集大小
:param eta: 學(xué)習(xí)率
:param test_data: 如果test_data被指定轻腺,那么在每一輪迭代完成之后,都對(duì)測(cè)試數(shù)據(jù)集進(jìn)行評(píng)估划乖,計(jì)算有多少樣本被正確識(shí)別了贬养。但是這會(huì)拖慢訓(xùn)練速度。
:return:
"""
if test_data: n_test = len(test_data)
n = len(training_data)
for j in range(epochs):
# 在每一次迭代之前迁筛,都將訓(xùn)練數(shù)據(jù)集進(jìn)行隨機(jī)打亂煤蚌,然后每次隨機(jī)選取若干個(gè)小批量訓(xùn)練數(shù)據(jù)集
random.shuffle(training_data)
mini_batches = [training_data[k:k+mini_batch_size] for k in range(0, n, mini_batch_size)]
# 每次訓(xùn)練迭代周期中要使用完全部的小批量訓(xùn)練數(shù)據(jù)集
for mini_batch in mini_batches:
self.update_mini_batch(mini_batch, eta)
# 如果test_data被指定,那么在每一輪迭代完成之后细卧,都對(duì)測(cè)試數(shù)據(jù)集進(jìn)行評(píng)估尉桩,計(jì)算有多少樣本被正確識(shí)別了
if test_data:
print("Epoch %d: accuracy rate: %.2f%%" % (j, self.evaluate(test_data)/n_test*100))
else:
print("Epoch {0} complete".format(j))
def update_mini_batch(self, mini_batch, eta):
"""
通過(guò)小批量隨機(jī)梯度下降以及反向傳播來(lái)更新神經(jīng)網(wǎng)絡(luò)的權(quán)重和偏置向量
:param mini_batch: 隨機(jī)選擇的小批量
:param eta: 學(xué)習(xí)率
"""
nabla_b = [np.zeros(b.shape) for b in self._biases]
nabla_w = [np.zeros(w.shape) for w in self._weights]
for x,y in mini_batch:
# 反向傳播算法,運(yùn)用鏈?zhǔn)椒▌t求得對(duì)b和w的偏導(dǎo)
delta_nabla_b, delta_nabla_w = self.backprop(x, y)
# 對(duì)小批量訓(xùn)練數(shù)據(jù)集中的每一個(gè)求得的偏導(dǎo)數(shù)進(jìn)行累加
nabla_b = [nb + dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
nabla_w = [nw + dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
# 使用梯度下降得出的規(guī)則來(lái)更新權(quán)重和偏置向量
self._weights = [w - (eta / len(mini_batch)) * nw
for w, nw in zip(self._weights, nabla_w)]
self._biases = [b - (eta / len(mini_batch)) * nb
for b, nb in zip(self._biases, nabla_b)]
def backprop(self, x, y):
"""
反向傳播算法贪庙,計(jì)算損失對(duì)w和b的梯度
:param x: 訓(xùn)練數(shù)據(jù)x
:param y: 訓(xùn)練數(shù)據(jù)x對(duì)應(yīng)的標(biāo)簽
:return: Return a tuple ``(nabla_b, nabla_w)`` representing the
gradient for the cost function C_x. ``nabla_b`` and
``nabla_w`` are layer-by-layer lists of numpy arrays, similar
to ``self.biases`` and ``self.weights``.
"""
nabla_b = [np.zeros(b.shape) for b in self._biases]
nabla_w = [np.zeros(w.shape) for w in self._weights]
# feedforward
activation = x
activations = [x] # list to store all the activations, layer by layer
zs = [] # list to store all the z vectors, layer by layer
for b, w in zip(self._biases, self._weights):
# z = wt*x+b note that the shape of z is the same as the bias
z = np.dot(w, activation) + b
zs.append(z)
activation = sigmoid(z) #pass the result z to activator function --> a = sigmoid(z)
activations.append(activation)
# backward pass
delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])
nabla_b[-1] = delta
nabla_w[-1] = np.dot(delta, activations[-2].transpose())
for l in range(2, self._num_layers):
z = zs[-l]
sp = sigmoid_prime(z)
delta = np.dot(self._weights[-l + 1].transpose(), delta) * sp
nabla_b[-l] = delta
nabla_w[-l] = np.dot(delta, activations[-l - 1].transpose())
return (nabla_b, nabla_w)
def evaluate(self, test_data):
"""
返回神經(jīng)網(wǎng)絡(luò)對(duì)測(cè)試數(shù)據(jù)test_data的預(yù)測(cè)結(jié)果蜘犁,并且計(jì)算其中識(shí)別正確的個(gè)數(shù)
因?yàn)樯窠?jīng)網(wǎng)絡(luò)的輸出是一個(gè)10x1的向量,我們需要知道哪一個(gè)神經(jīng)元被激活的程度最大止邮,
因此使用了argmax函數(shù)以獲取激活值最大的神經(jīng)元的下標(biāo)这橙,那就是網(wǎng)絡(luò)輸出的最終結(jié)果。
"""
test_results = [(np.argmax(self.feedforward(x)), y)
for (x, y) in test_data]
return sum(int(x == y) for (x, y) in test_results)
def cost_derivative(self, output_activations, y):
"""
返回?fù)p失函數(shù)對(duì)a的的偏導(dǎo)數(shù)导披,損失函數(shù)定義 C = 1/2*||y(x)-a||^2
求導(dǎo)的結(jié)果為:
C' = y(x) - a
"""
return (output_activations - y)
if __name__ == "__main__":
# 獲取MNIST訓(xùn)練數(shù)據(jù)集屈扎、驗(yàn)證數(shù)據(jù)集、測(cè)試數(shù)據(jù)集
training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
# 定義一個(gè)3層全連接網(wǎng)絡(luò)撩匕,輸入層有784個(gè)神經(jīng)元鹰晨,隱藏層30個(gè)神經(jīng)元,輸出層10個(gè)神經(jīng)元
fc = FCN([784, 30, 10])
# 設(shè)置迭代次數(shù)30次止毕,mini-batch大小為10模蜡,學(xué)習(xí)率為3,并且設(shè)置測(cè)試集扁凛,即每一輪訓(xùn)練完成之后忍疾,都對(duì)模型進(jìn)行一次評(píng)估。
# 這里的參數(shù)可以根據(jù)實(shí)際情況進(jìn)行修改
fc.SGD(training_data, 30, 10, 3.0, test_data=test_data)
運(yùn)行結(jié)果如下:上述代碼定義了一個(gè)3層的全連接神經(jīng)網(wǎng)絡(luò)谨朝,每層包含的神經(jīng)元個(gè)數(shù)分別為784卤妒、30、10字币。訓(xùn)練迭代周期為30荚孵,小批量數(shù)據(jù)集個(gè)數(shù)為10,學(xué)習(xí)率為3.0纬朝。并且設(shè)置了測(cè)試數(shù)據(jù)集收叶,即每完成一個(gè)訓(xùn)練迭代周期之后,都會(huì)將測(cè)試數(shù)據(jù)集運(yùn)用在神經(jīng)網(wǎng)絡(luò)上用來(lái)計(jì)算手寫(xiě)數(shù)字識(shí)別準(zhǔn)確率共苛,經(jīng)過(guò)30次的迭代周期之后判没,最終達(dá)到了95.25%的識(shí)別正確率。
因?yàn)樯窠?jīng)網(wǎng)絡(luò)的權(quán)重和偏置是隨機(jī)生成的隅茎,故每次試驗(yàn)結(jié)果并不一樣澄峰。感興趣的讀者可以在自己的電腦上跑一下代碼,并且可以修改一下隱藏層神經(jīng)元數(shù)量辟犀、迭代周期俏竞、學(xué)習(xí)率、小批量訓(xùn)練集的數(shù)量等,觀察一下神經(jīng)網(wǎng)絡(luò)計(jì)算出來(lái)的正確率會(huì)發(fā)生什么變化魂毁。
總結(jié)
本文篇幅很長(zhǎng)玻佩,但是若能仔細(xì)閱讀并且對(duì)文中提及的公式都手動(dòng)推導(dǎo)一遍,再結(jié)合代碼調(diào)試一下的話席楚,相信會(huì)對(duì)全連接神經(jīng)網(wǎng)絡(luò)的原理和訓(xùn)練優(yōu)化過(guò)程有較為深刻的理解咬崔。
后記
本文是自己學(xué)習(xí)《神經(jīng)網(wǎng)絡(luò)與深度學(xué)習(xí)》第一章內(nèi)容時(shí)做的筆記。原文講解的非常詳細(xì)生動(dòng)烦秩,但是篇幅也很長(zhǎng)垮斯,我提煉了出其中最主要的部分記錄下來(lái),方便自己查閱以及他人學(xué)習(xí)只祠。
個(gè)人覺(jué)得這本書(shū)寫(xiě)的非常不錯(cuò)兜蠕,以非常淺顯的語(yǔ)言與生動(dòng)的圖例解釋清楚了全連接神經(jīng)網(wǎng)絡(luò)的構(gòu)成、訓(xùn)練抛寝、優(yōu)化牺氨、調(diào)參等。GitHub上有對(duì)應(yīng)的中文版墩剖,感興趣的讀者也可以看這本猴凹。不過(guò)還是非常推薦閱讀英文原版,因?yàn)樵氖腔诰W(wǎng)頁(yè)的岭皂,有很多形象的插圖郊霎、動(dòng)畫(huà)以及在線程序演示等,可以使讀者加深對(duì)全連接網(wǎng)絡(luò)的印象爷绘。