本系列文章面向程序員,希望通過Image Caption Generation,一個有意思的具體任務(wù),深入淺出地介紹深度學(xué)習(xí)的知識,涉及到很多深度學(xué)習(xí)流行的模型,如CNN,RNN/LSTM,Attention等。本文為第二篇。
作者李理,MDCC2016移動開發(fā)者大會人工智能與機(jī)器人專場的出品人,邀請人工智能一線專家擔(dān)任演講嘉賓,從無人駕駛、智能機(jī)器人、智能應(yīng)用開發(fā)實(shí)戰(zhàn)等方面解讀人工智能技術(shù)的內(nèi)涵及其對移動開發(fā)工作的影響。
2.機(jī)器學(xué)習(xí)基本概念和前饋神經(jīng)網(wǎng)絡(luò)
2.1機(jī)器學(xué)習(xí)基本概念
大家可能平時都寫過很多程序,寫程序和機(jī)器學(xué)習(xí)的思路可能有一些不同。寫程序時,我們是“上帝”,我們規(guī)定計(jì)算機(jī)的每一個步驟,第一步做什么第二步做什么,我們稱之為算法。我們能夠控制所有的情況,如果出了任何問題,肯定都是程序員的責(zé)任。而在機(jī)器學(xué)習(xí)的時候,我們只是“老師”。我們告訴學(xué)生(計(jì)算機(jī))輸入是什么,輸出是什么,然后期望它能夠?qū)W到和我們類似的知識。比如我們跟小孩說這是狗,那是貓,我們沒有辦法像上帝那樣拿著“納米手術(shù)刀”去操作人腦神經(jīng)元的連接方式。我們只能不斷的給小孩“訓(xùn)練數(shù)據(jù)”,然后期望他能夠?qū)W會什么是貓,即使我們覺得他“學(xué)會”了識別貓,我們也沒有辦法知道他是“怎么”學(xué)會的,而且同樣的訓(xùn)練過程可能換一個人就不好使。
機(jī)器學(xué)習(xí)和人類的學(xué)習(xí)是類似的——我們也是給它訓(xùn)練數(shù)據(jù),然后期望它能學(xué)會。我們會給機(jī)器建一個模型,從數(shù)學(xué)的角度來說一個模型就是一個函數(shù),它的輸入一般是一個向量【當(dāng)然可以是二維的矩陣如圖片或者三維的張量比如視頻】,輸出可以是有限的離散的標(biāo)簽如“貓”,“狗”,這類問題我們稱之為分類;而如果輸出是連續(xù)的值比如用這個模型來預(yù)測氣溫,那么我們就稱之為回歸。其實(shí)人類的很多科學(xué)活動和日常生活,都是在“學(xué)習(xí)”模型和“應(yīng)用”模型。比如開普勒通過觀測大量天文數(shù)據(jù)“歸納”出行星的運(yùn)動規(guī)律。從本質(zhì)上講,智能就是從“過去”學(xué)習(xí),然后根據(jù)“現(xiàn)在”來預(yù)測可能的將來并根據(jù)自己的目標(biāo)選擇有利于自己行為。只不過之前,似乎只有人類能夠從數(shù)據(jù)中“學(xué)習(xí)”出規(guī)律,而人工智能的目標(biāo)就是讓機(jī)器也有類似的學(xué)習(xí)能力。
模型用數(shù)學(xué)來說就是一個函數(shù),我們?nèi)四X的函數(shù)由神經(jīng)元的連接構(gòu)成,它可能是一個很復(fù)雜的函數(shù),我們現(xiàn)在還很難徹底研究清楚。神經(jīng)網(wǎng)絡(luò)就是試圖通過計(jì)算機(jī)來模擬和借鑒人腦這個模型,除了我們這里要講的神經(jīng)網(wǎng)絡(luò)之外,機(jī)器學(xué)習(xí)領(lǐng)域還有各種各樣的模型,它們各有特點(diǎn)。但不管形式怎么變化,本質(zhì)都是一個函數(shù)。一個(或者更準(zhǔn)確的是一種)模型一般都是一種函數(shù)形式,它有一些“參數(shù)”可以改變。而學(xué)習(xí)的過程就是不斷調(diào)整這些參數(shù),使得輸出(盡量)接近“正確”的答案。但是一般情況下很難所有的數(shù)據(jù)我們都能預(yù)測正確,所以一般我們會定義一個loss function,可以理解為“錯誤”的程度,錯的越“離譜”,loss就越大。而我們的目標(biāo)就是調(diào)整參數(shù)使得loss最小。
但是我們是在“訓(xùn)練”數(shù)據(jù)上調(diào)整的參數(shù),那么它能在“測試”數(shù)據(jù)上也表現(xiàn)的好嗎?這個就是模型的“泛化”能力了。就和人在學(xué)校學(xué)習(xí)一樣,有的同學(xué)做過的一模一樣的題就會,但是考試時稍微改變一下就不會了,這就是“泛化”能力太差,學(xué)到的不是最本質(zhì)的東西。所以平時會定期有一些“模擬考試”,來檢驗(yàn)學(xué)生是不是真的學(xué)會了,如果考得不好,那就打回去重新訓(xùn)練模型調(diào)整參數(shù)。這在機(jī)器學(xué)習(xí)里對應(yīng)的就是validation的階段。最后到最終的考試了,就是最終檢驗(yàn)的時候了,這個試卷里的題目是不能提前讓人看到的,只能拿出來用一次,否則就是作弊了。對應(yīng)到機(jī)器學(xué)習(xí)里就是test階段。
當(dāng)然這里用通俗的話描述了機(jī)器學(xué)習(xí),主要是有監(jiān)督的學(xué)習(xí)。其實(shí)機(jī)器學(xué)習(xí)還有無監(jiān)督的學(xué)習(xí)和強(qiáng)化學(xué)習(xí)。前者就是不給答案,只給數(shù)據(jù),讓人總結(jié)規(guī)律;而后者會有答案,但是答案不是現(xiàn)在就告訴你。我個人覺得人類社會里更多的是監(jiān)督學(xué)習(xí)和強(qiáng)化學(xué)習(xí)。從人類社會總體來說,強(qiáng)化學(xué)習(xí)是獲取新知識的唯一途徑,也就是向自然學(xué)習(xí),我們做了一個決策,其好壞可能要很長一段時間才能顯現(xiàn)出來。而學(xué)習(xí)出來的這些知識通過監(jiān)督的方式,通過家庭和學(xué)校的教育教給下一代。
另外輸出除了簡單的分為離散和連續(xù),還可以是序列(時序)的,比如自然語言(文本)是一個字符串的序列,對于我們的Image Caption Generation就是生成一個單詞序列。另外還有更復(fù)雜的輸出,比如parsing,輸出是一棵語法樹。
2.2多層神經(jīng)網(wǎng)絡(luò)
前面介紹了機(jī)器學(xué)習(xí)的基本概念,接下來我們就來學(xué)習(xí)一下神經(jīng)網(wǎng)絡(luò)。現(xiàn)在流行的說法“深度學(xué)習(xí)”,其實(shí)大多指的就是“深度神經(jīng)網(wǎng)絡(luò)”,那么首先我們先了解一下“淺度神經(jīng)網(wǎng)絡(luò)”,也就是傳統(tǒng)的神經(jīng)網(wǎng)絡(luò)。這里的內(nèi)容主要來自http://neuralnetworksanddeeplearning.com的前兩章。
2.2.1手寫數(shù)字識別問題
我們在學(xué)習(xí)一門新的語言時會寫一個hello world程序,而mnist數(shù)據(jù)的手寫數(shù)字識別就是一個很好的學(xué)習(xí)機(jī)器學(xué)習(xí)(包括深度學(xué)習(xí))的一個hello world任務(wù)。
計(jì)算機(jī)和人類大腦似乎有很大的不同,很多人類認(rèn)為復(fù)雜的工作計(jì)算機(jī)可能認(rèn)為很簡單,而人類認(rèn)為很簡單的事情計(jì)算機(jī)可能非常難處理。比如數(shù)字的計(jì)算,記憶,人類的準(zhǔn)確度和速度都遠(yuǎn)遠(yuǎn)不如計(jì)算機(jī)。但是識別0-9的手寫數(shù)字,我們覺得很輕而易舉的事情,讓計(jì)算機(jī)程序來處理卻異常困難。經(jīng)過數(shù)百萬年進(jìn)化的人類視覺系統(tǒng)在我們大腦沒有意識到的時候就已經(jīng)幫我們完成了數(shù)字的識別,把那些復(fù)雜的視覺處理過程深深的掩藏了起來。但當(dāng)我們想自己寫一個程序來識別數(shù)字的時候,這些困難才能體現(xiàn)出來。首先,對于計(jì)算機(jī)來說,它“看到”的不是數(shù)字,甚至不是筆畫。它“看到”的只是一個二位的矩陣(數(shù)組),每個點(diǎn)都是一個數(shù)字。比如下圖,我們“看到”的是左邊的“貓”,其實(shí)計(jì)算機(jī)“看到”的是右邊的像素灰度值。當(dāng)然我們視覺系統(tǒng)的視網(wǎng)膜看到的也是類似的一些“數(shù)值”,只不過我們的視覺系統(tǒng)已經(jīng)處理了這些信息并且把它識別成了“貓”(甚至和語言還做了映射)。

MNIST數(shù)據(jù)介紹:MNIST的每個圖片經(jīng)過縮放和居中等預(yù)處理之后,大小是28*28,每個點(diǎn)都是0-255的灰度值,下圖是一些樣例。總共有60,000個訓(xùn)練數(shù)據(jù)(0-9共10個類別,每個類別6,000個)和10,000個測試數(shù)據(jù)。一般會拿60000個中的50000個來做訓(xùn)練集,而剩下的10000個用來做驗(yàn)證集(用來選擇一些超參數(shù))。

mnist樣例數(shù)據(jù)
如果我們自己來寫一個“算法”識別數(shù)字“9”,我們可能會這么定義:9在上面有個圓圈,在這個圓圈的右下部分有一個豎直的筆畫。說起來很簡單,如果用算法來實(shí)現(xiàn)就很麻煩了:什么是圓圈?每個人畫的圓圈都不同,同樣豎直的筆畫怎么識別,圓圈和豎直筆畫連接處怎么尋找,右下是哪?大家如果有興趣可以嘗試一下用上面的方法,其實(shí)最早做數(shù)字識別就是這樣的思路。
機(jī)器學(xué)習(xí)的思路則不同,它不需要這么細(xì)節(jié)的“指示”計(jì)算機(jī)應(yīng)該怎么做。而是給計(jì)算機(jī)足夠的“訓(xùn)練”樣本,讓它“看”不同的10個數(shù)字,然后讓它“學(xué)”出來。前面我們也講了,現(xiàn)在的機(jī)器學(xué)習(xí)一般是一個參數(shù)化的模型。比如最簡單的一個線性模型:f(w;x)=w0+w1*x1+w2*x2.如果我們的輸入有兩個“特征”x1和x2,那么這個模型有3個參數(shù)w0,w1和w2,機(jī)器學(xué)習(xí)的過程就是選擇“最優(yōu)”的參數(shù)。對于上面的mnist數(shù)據(jù),輸入就是28*28=784維的向量。
如果用“原始”的輸入作為“特征”,線性的模型很可能學(xué)到一些簡單的特征,比如它看到1一般是分布在從上到下居中的一些位置,那么對于這些位置一旦發(fā)現(xiàn)有比較大的灰度值,那么就傾向于判斷成1.如果一個像素點(diǎn)2也經(jīng)常出現(xiàn),但3不出現(xiàn),那么它就能學(xué)到如果這個像素出現(xiàn),那么這個數(shù)字是2和3的可能性就大一些。
但是這樣的“特征”可能不是“本質(zhì)”的,因?yàn)槲覍懽值臅r候筆稍微平移一點(diǎn),那么你之前“學(xué)到”的參數(shù)就可能有問題。而更“本質(zhì)”的特征是什么呢?可能還是像之前我們總結(jié)的——9在上面有個圓圈,在這個圓圈的右下部分有一個豎直的筆畫。我們把識別一個數(shù)字的問題轉(zhuǎn)化成圓圈和豎直筆畫的問題。傳統(tǒng)的機(jī)器學(xué)習(xí)需要方法來提取“類似”(但不完全是)基本筆畫這樣的“特征”,這些特征相對于像素的特征會更加“本質(zhì)”。但是要“提取”這些特征需要很多的“領(lǐng)域”知識,比如圖像處理的技術(shù)。所以使用傳統(tǒng)的機(jī)器學(xué)習(xí)方法來解決問題,我們不但需要很多機(jī)器學(xué)習(xí)的知識,而且也需要很多“領(lǐng)域”的知識,同時擁有這兩方面的知識是比較難的。
而“深度學(xué)習(xí)”最近之所以火熱,其中很重要的一個原因就是對于很多問題,我們只需要輸入最原始的信號,比如圖片的像素值,通過“多層”的網(wǎng)絡(luò),讓底層的網(wǎng)絡(luò)學(xué)習(xí)出“底層”的特征,比如基本的形狀,而中間的層學(xué)習(xí)出抽象一點(diǎn)的特征,比如眼睛鼻子耳朵。而更上的層次識別出這是一個貓還是一個狗。所有這些都是機(jī)器學(xué)習(xí)出來的,所以基本不需要領(lǐng)域的知識。

上面的圖就說明了這一點(diǎn),而且我們發(fā)現(xiàn)越是底層的特征就越“通用”,不管是貓鼻子還是狗眼睛,可能用到的都是一些基本的形狀,因此我們可以把這些知識(特征)transfer到別的任務(wù),也就是transfer learning,后面我們講到CNN的時候還會提及。
2.2.2單個神經(jīng)元和多層神經(jīng)網(wǎng)絡(luò)(MLP)
神經(jīng)網(wǎng)絡(luò)從名字來看是和人類的大腦有些關(guān)系的,而且即使到現(xiàn)在,很多有用的東西如CNN和Attention,都有很多借鑒神經(jīng)科學(xué)研究人腦的結(jié)果的。不過這里我就不介紹這些東西了,有興趣的讀者可以找一些資料來了解。
一個神經(jīng)元如下圖的結(jié)構(gòu):

它的輸入是一個向量,(x1,x2,x3),輸出是一個標(biāo)量,一個實(shí)數(shù)。z=w0+w1*x1+w2*x2+w3*x3.z是輸入的加權(quán)累加,權(quán)值是w1,w2,w3,w0是bias,輸出output=f(z)。函數(shù)f一般叫做激活函數(shù)。最早流行的激活函數(shù)是Sigmoid函數(shù),當(dāng)然現(xiàn)在更流行Relu和它的改進(jìn)版本。Sigmoid函數(shù)的公式和圖形如下:

當(dāng)z=0時,sigmoid(z)=0.5z趨于無窮大時,sigmoid(z)趨近于1,z趨于負(fù)無窮,值趨于0.為什么選擇這樣的激活函數(shù)呢?因?yàn)槭悄M人腦的神經(jīng)元。人腦的神經(jīng)元也是把輸入的信號做加權(quán)累加,然后看累加和是否超過一個“閾值”。如果超過,繼續(xù)向下一個神經(jīng)元發(fā)送信號,否則就不發(fā)送。因此人腦的神經(jīng)元更像是一個階躍函數(shù):

最早的感知機(jī)(Perception)其實(shí)用的就是這個激活函數(shù)。但是它有一個缺點(diǎn)就是0之外的所有點(diǎn)的導(dǎo)數(shù)都是0,在0點(diǎn)的導(dǎo)數(shù)是無窮大,所以很難用梯度的方法優(yōu)化。而Sigmoid函數(shù)是處處可導(dǎo)。下面我手工推導(dǎo)了一下,如果大家不熟悉可以試著推導(dǎo)一下Sigmoid函數(shù)的導(dǎo)數(shù),我們后面也會用到。

我們把許多的單個神經(jīng)元按照層次組織起來就是多層的神經(jīng)網(wǎng)絡(luò)。

比如我們的手寫數(shù)字識別,輸入層是784維,就是神經(jīng)網(wǎng)絡(luò)的地一層,然后中間有15個hidden(因?yàn)槲覀儾恢浪闹担┥窠?jīng)元,然后輸出層是10個神經(jīng)元。中間隱層的每個神經(jīng)元的輸入都是784個原始像素通過上面的公式加權(quán)累加然后用sigmoid激活。而輸出層的每一個神經(jīng)元也是中間15個神經(jīng)元的累加然后激活。上面的圖就是一個3層的神經(jīng)網(wǎng)絡(luò)。
輸入一個28*28的圖像,我們得到一個10維的輸出,那么怎么分類呢?最直接的想法就是把認(rèn)為最大的那個輸出,比如輸出是(10,11,12,13,14,15,16,17,18,19),那么我們認(rèn)為輸出是9.
當(dāng)然,更常見的做法是最后一次經(jīng)過線性累加之后并不用Sigmoid函數(shù)激活,而是加一個softmax的函數(shù),讓10個輸出加起來等于1,這樣更像一個概率。而我們上面的情況,雖然訓(xùn)練數(shù)據(jù)的輸出加起來是1,但是實(shí)際給一個其它輸入,輸出加起來很可能不是1.不過為了與Nielsen的文章一致,我們還是先用這種方法。
因此,假設(shè)我們有了這些參數(shù)【總共是784*15+15(w0或者叫bias)+15*10+10】,我們很容易通過上面的公式一個一個的計(jì)算出10維的輸出。然后選擇最大的那個作為我們識別的結(jié)果。問題的難點(diǎn)就在怎么選擇這么多參數(shù),然后使得我們分類的錯誤最少。
而我們怎么訓(xùn)練呢?對于一張圖片,假設(shè)它是數(shù)字“1”,那么我們期望它的輸出是(0,1,0,0,0,0,0,0,0,0),所以我們可以簡單的用最小平方錯誤作為損失函數(shù)。不過你可能會有些疑問,我們關(guān)注的指標(biāo)應(yīng)該是分類的“正確率”(或者錯誤率),那么我們?yōu)槭裁床恢苯影逊诸惖腻e誤率作為損失函數(shù)呢?這樣神經(jīng)網(wǎng)絡(luò)學(xué)習(xí)出來的參數(shù)就是最小化錯誤率。
主要的原因就是錯誤率不是參數(shù)的連續(xù)函數(shù)。因?yàn)橐粋訓(xùn)練數(shù)據(jù)如果分類正確那么就是1,否則就是0,這樣就不是一個連續(xù)的函數(shù)。比如最簡單的兩類線性分類器,f(x)=w0+w1*x1+w2*x2.如果f(x)>0我們分類成類別1;否則我們分類成類別2.如果當(dāng)前的w0+w1*x1+w2*x2<0,我們很小的調(diào)整w0(或者w1,w2),w0+w1*x1+w2*x2仍然小于0,【事實(shí)上對于這個例子,只要是w0變小,他們的累加都是小于0的】所以f(x)的值不會變化,而w0一直增大到使累加和等于0之前都不會變化,只有大于0時突然變成1了,然后一直就是1.因此之前的錯誤率都是1,然后就突然是0.所以它不是個連續(xù)的函數(shù)。
因?yàn)槲覀兪褂玫膬?yōu)化算法一般是(隨機(jī))梯度下降的算法,在每次迭代的時候都是試圖做一個微小的參數(shù)調(diào)整使得損失變小,但是不連續(xù)的函數(shù)顯然也不可導(dǎo),也就沒法用這個算法來優(yōu)化參數(shù)。
因此我們使用了最小平方誤差(MSE)損失函數(shù)。

y(x)就是神經(jīng)網(wǎng)絡(luò)的輸出,可能寫成f(x)大家會習(xí)慣一點(diǎn)。a是目標(biāo)的輸出,比如當(dāng)前分類是數(shù)字1,那么我們期望的輸出就是(0,1,0,0,0,0,0,0,0,0)。
首先這個損失函數(shù)是參數(shù)w的連續(xù)函數(shù),因?yàn)閥(x)就是神經(jīng)網(wǎng)絡(luò)的輸出,每個神經(jīng)元都是它的輸入的線性加權(quán)累加,然后使用sigmoid激活函數(shù)【如果使用最早的階躍函數(shù)就不連續(xù)了,所以后來使用了Sigmoid函數(shù)】,然后每一層的神經(jīng)元都是用上一層的神經(jīng)元通過這樣的方式計(jì)算的(只不過每個神經(jīng)元的參數(shù)也就是權(quán)重是不同的數(shù)值而已),所以這些連續(xù)函數(shù)的復(fù)合函數(shù)也是連續(xù)的。
其次這個損失函數(shù)和我們的最終優(yōu)化目標(biāo)是“大致”一致的。比如C(w,b)趨于0時,它就要求y(x)趨于a,那么我們的分類也就趨于正確。當(dāng)然可能存在一種極端的情況,比如有3個訓(xùn)練數(shù)據(jù),第一組參數(shù),它分類正確了2個訓(xùn)練數(shù)據(jù),但是錯的那1個錯的很“離譜”,也就是y(x)和a差距極大;而第二組參數(shù),他正確分類了1個訓(xùn)練數(shù)據(jù),但是錯的那兩個都還不算太差。那么這種情況下MSE和正確率并不一致。
2.2.3隨機(jī)梯度下降(Stochastic Gradient Descent)和自動求梯度(Automatic Derivatives)
上面說了,我們有了一個參數(shù)化的模型,訓(xùn)練的過程就是根據(jù)訓(xùn)練數(shù)據(jù)和lossfunction,選擇“最優(yōu)”的參數(shù),使得loss“最小”,這從數(shù)學(xué)上來講就是一個優(yōu)化問題。這看起來似乎不是什么值得一提的問題,也許你還記得微積分里的知識,極值點(diǎn)的各種充分必要條件,比如必要條件是導(dǎo)數(shù)是0,然后直接把參數(shù)解出來。但在現(xiàn)實(shí)生活中的函數(shù)遠(yuǎn)比教科書里學(xué)到的復(fù)雜,很多模型都無法用解析的方式求出最優(yōu)解。所以現(xiàn)實(shí)的方法就是求“數(shù)值”解,一般最常見的方法就是迭代的方法,根據(jù)現(xiàn)在的參數(shù),我們很小幅度的調(diào)整參數(shù),使得loss變小一點(diǎn)點(diǎn)。然后一步一步的最終能夠達(dá)到一個最優(yōu)解(一般是局部最優(yōu)解)。那怎么小幅調(diào)整呢?像悶頭蒼蠅那樣隨機(jī)亂試顯然效率極低。因此我們要朝著一個能使函數(shù)值變小的方向前進(jìn)。而在一個點(diǎn)能使函數(shù)值變小的方向有無窮多個,但有一個方向是下降速度最快的,那就是梯度。因此更常見的方法就是在當(dāng)前點(diǎn)求函數(shù)的梯度,然后朝著梯度的方向下降。朝梯度的方向走多遠(yuǎn)呢?一般走一個比較小的值是比較安全的,這個值就是“步長”。一般剛開始隨機(jī)的初始化參數(shù),loss比較大,所以多走一些也沒關(guān)系,但是到了后面,就不能走太快,否則很容易錯過最優(yōu)的點(diǎn)。
因?yàn)閘oss是所有訓(xùn)練數(shù)據(jù)的函數(shù),所以求loss的梯度需要計(jì)算所有的訓(xùn)練數(shù)據(jù),對于很多task來說,訓(xùn)練數(shù)據(jù)可能上百萬,計(jì)算一次代價太大,所以一般會“隨機(jī)”的采樣少部分?jǐn)?shù)據(jù),比如128個數(shù)據(jù),求它的梯度。雖然128個點(diǎn)的梯度和一百萬個的是不一樣的,但是從概率來講至少是一致的方向而不會是相反的方向,所以也能使loss變小。當(dāng)然這個128是可以調(diào)整的,它一般被叫做batchsize,最極端的就是batch是1和一百萬,那么分別就是onlinelearning和退化到梯度下降。batchsize越大,計(jì)算一次梯度的時間就越久【當(dāng)然由于GPU和各種類似SSE的指令,一次計(jì)算128個可能并不比計(jì)算1個慢多少】,隨機(jī)梯度和真正梯度一致的概率就越大,走的方向就更“正確”;batchsize越小,計(jì)算一次的時間就越短,但可能方向偏離最優(yōu)的方向就更遠(yuǎn),會在不是“冤枉路”。但實(shí)際的情況也很難說哪個值是最優(yōu)的,一般的經(jīng)驗(yàn)取值都是幾十到一兩百的范圍,另外因?yàn)橛?jì)算機(jī)都是字節(jié)對齊,32,64,128這樣的值也許能稍微加快矩陣運(yùn)算的速度。但是實(shí)際也很多人選擇10,50,100這樣的值。
除了常見的隨機(jī)梯度下降,還有不少改進(jìn)的方法,如Momentum,Adagrad等等,有興趣的可以看看http://cs231n.github.io/neural-networks-3/#update,里面還有個動畫,比較了不同方法的收斂速度的比較。
通過上面的分析,我們把問題變成了怎么求loss對參數(shù)W的梯度。
求梯度有如下4種方法:
手工求解析解
比如f(x)=x^2,df/dx=2*x。然后我們要求f(x)在x=1.5的值,代進(jìn)去就2*1.5=3
數(shù)值解
使用極限的定義:
圖片描述
機(jī)器符號計(jì)算
讓機(jī)器做符號運(yùn)算,實(shí)現(xiàn)1的方法,但是機(jī)器如果優(yōu)化的不好的話可能會有一些不必要的運(yùn)算。
比如x^2+2*x*y+y^2,直接對x求導(dǎo)數(shù)變成了2*x+2*y,兩次乘法一次加分,但是我們可以合并一下變成2*(x+y),一次乘法一次加分。
自動梯度
下面我會在稍微細(xì)講一下,所以這里暫時跳過。
這些方法的優(yōu)缺點(diǎn):
手工求解“數(shù)學(xué)”要求高,有可能水平不夠求不對,但效率應(yīng)該是能最優(yōu)的。
沒任何函數(shù),甚至沒有解析導(dǎo)數(shù)的情況下都能使用,缺點(diǎn)是計(jì)算量太大,而且只是近似解【因?yàn)闃O限的定義】,在某些特別不“連續(xù)”的地方可能誤差較大。所以實(shí)際使用是很少,只是用它來驗(yàn)證其它方法是否正確。
機(jī)器符號計(jì)算,前面說的,依賴于這個庫的好壞。
實(shí)際的框架,如TensorFlow就是自動梯度,而Theano就是符號梯度。
2.2.4編程實(shí)戰(zhàn)
通過上面的介紹,我們其實(shí)就可以實(shí)現(xiàn)一個經(jīng)典的前饋(feedforward)神經(jīng)網(wǎng)絡(luò)了,這種網(wǎng)絡(luò)結(jié)構(gòu)很簡單,每一層的輸入是前一層的輸出。輸入層沒有輸入,它就是原始的信號輸入。而且上一層的所有神經(jīng)元都會連接到下一層的所有神經(jīng)元,就像我們剛才的例子,輸入是784,中間層是15,那么就有785*15個連接【再加上每個中間節(jié)點(diǎn)有一個bias】。所以這種網(wǎng)絡(luò)有時候也加做全連接的網(wǎng)絡(luò)(fullconnected),用來和CNN這種不是全連接的網(wǎng)絡(luò)有所區(qū)別,另外就是信號是從前往后傳遞,沒有反饋,所以也叫前潰神經(jīng)網(wǎng)絡(luò),這是為了和RNN這種有反饋的區(qū)別。
當(dāng)然,我們還沒有講怎么計(jì)算梯度,也就是損失函數(shù)相對于每一個參數(shù)的偏導(dǎo)數(shù)。在下一部分我們會詳細(xì)討論介紹,這里我們先把它當(dāng)成一個黑盒的函數(shù)就好了。
1.代碼
我們這里學(xué)習(xí)一下Nielsen提供的代碼。代碼非常簡潔,只有不到100行代碼。
https://github。com/mnielsen/neural-networks-and-deep-learning
gitclonehttps://github.com/mnielsen/neural-networks-and-deep-learning.git
2.運(yùn)行
創(chuàng)建一個test_network1.py,輸入如下代碼:
import mnist_loader
import network
training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
net = network.Network([784, 30, 10])
net.SGD(training_data, 30, 10, 3.0, test_data=test_data)
保存后直接運(yùn)行Pythontest_network1.py。這里我們讓他進(jìn)行了30次迭代,最終在測試數(shù)據(jù)上的準(zhǔn)確率大概在95%左右(當(dāng)然因?yàn)殡S機(jī)初始化參數(shù)不同,最終的結(jié)果可能有所不同)
Epoch 0: 8250 / 10000
Epoch 1: 8371 / 10000
Epoch 2: 9300 / 10000
......
Epoch 28: 9552 / 10000
Epoch 29: 9555 / 10000
3.代碼閱讀
Python代碼很容易閱讀,即使之前沒有用過,稍微學(xué)習(xí)兩天也就可以上手,而且大部分機(jī)器學(xué)習(xí)相關(guān)的代碼不會用到太復(fù)雜的語言特性,基本就是一些數(shù)學(xué)的線性代數(shù)的運(yùn)算。而Python的numpy這個庫是用的最多的,后面閱讀代碼的時候我會把用到的函數(shù)做一些介紹,繼續(xù)下面的閱讀之前建議花十分鐘閱讀一下http://cs231n。github。io/python-numpy-tutorial/。
3.1mnist_loader.load_data_wrapper函數(shù)
這個函數(shù)用來讀取mnist數(shù)據(jù),數(shù)據(jù)是放在data/mnist。pkl。gz。首先這是個gzip的壓縮文件,是Pickle工具序列化到磁盤的格式。不熟悉也沒有關(guān)系,反正我們知道這個函數(shù)的返回值就行了。
這個函數(shù)返回三個對象,分別代表training_data,validation_data和test_data。
training_data是一個50,000的list,然后其中的每一個元素是一個tuple。tuple的第一個元素是一個784維的numpy一維數(shù)組。第二個元素是10維的數(shù)組,也就是one-hot的表示方法——如果正確的答案是數(shù)字0,那么這個10維數(shù)組就是(1,0,0,…)。
而validation_data是一個10,000的list,每個元素也是一個tuple。tuple的第一個元素也是784維的numpy一維數(shù)組。第二個元素是一個0-9的數(shù)字,代表正確答案是那個數(shù)字。
test_data的格式和validation_data一樣。
為什么training_data要是這樣的格式呢?因?yàn)檫@樣的格式計(jì)算loss更方便一些。
3.2Network類的構(gòu)造函數(shù)
我們在調(diào)用net=network。Network([784,30,10])時就到了init函數(shù)。為了減少篇幅,代碼里的注釋我都去掉了,重要的地方我會根據(jù)自己的理解說明,但是有空還是值得閱讀代碼里的注釋。
class Network(object):
def __init__(self, sizes):self.num_layers = len(sizes)
self.sizes = sizes
self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
self.weights = [np.random.randn(y, x)
for x, y in zip(sizes[:-1], sizes[1:])]
比如上面的參數(shù),我們保存下來的self。num_layers=3,也就是3層的網(wǎng)絡(luò)。每一層的神經(jīng)元的個數(shù)保存到self。sizes里。接下來就是構(gòu)造biases數(shù)組并隨機(jī)初始化。因?yàn)檩斎雽邮菦]有參數(shù)的,所以是foryinsizes[1:],我們使用了numpy的random。randn生成正態(tài)分布的隨機(jī)數(shù)用來作為參數(shù)的初始值。注意這里生成了2維的隨機(jī)變量。回憶一下,如果我們有30個hiddenunit,那么bias的個數(shù)也是30,那就生成一個30維的1維數(shù)組就行了,為什么要是30*1的二維數(shù)組呢?其實(shí)用1維也可以,不過為了和weights一致,后面代碼方便,就用二維數(shù)組了。另外weights也是一樣的初始化方法,不過注意randn(y,x)而不是randn(x,y)。比如對于我們輸入的[784,30,10],weights分別是30*784和10*30的。當(dāng)然其實(shí)weights矩陣轉(zhuǎn)置一下也可以,就是計(jì)算矩陣乘法的時候也需要有一個轉(zhuǎn)置。不同的文獻(xiàn)可能有不同的記法,但是我們在實(shí)現(xiàn)代碼的時候只需要隨時注意矩陣的大小,檢查矩陣乘法滿足乘法的約束就行了,矩陣AB能相乘,必須滿足的條件是B的列數(shù)等于A的函數(shù)就行。
對于Nielsen的記法,矩陣的每一行就是一個神經(jīng)元的784個參數(shù),那么weights(30*784)*input(784*1)就得到30個hiddenunit的加權(quán)累加。
3.3feedforward函數(shù)
給點(diǎn)輸入a(784維),計(jì)算最終神經(jīng)網(wǎng)絡(luò)的輸出(10維)。
def feedforward(self, a):
"""Return the output of the network if ``a`` is input."""for b, w in zip(self.biases, self.weights):
a = sigmoid(np.dot(w, a)+b)
return a
代碼非常簡單,這里用到了np。dot,也就是矩陣向量的乘法,此外這里有一個Sigmoid函數(shù),這個函數(shù)的輸入是numpy的ndarray,輸出也是同樣大小的數(shù)組,不過對于每個元素都進(jìn)行了sigmoid的計(jì)算。用numpy的術(shù)語就是universalfunction,很多文獻(xiàn)里一般都叫elementwise的function。我覺得后面這個名字更直接。
#### Miscellaneous functionsdef sigmoid(z):
"""The sigmoid function."""return 1.0/(1.0+np.exp(-z))
def sigmoid_prime(z):
"""Derivative of the sigmoid function."""return sigmoid(z)*(1-sigmoid(z))
上面就是Sigmoid函數(shù),另外也把sigmoid_prime,也就是Sigmoid的導(dǎo)數(shù)放在了一起【不記得的話看前面Sigmoid的導(dǎo)數(shù)的推導(dǎo)】。
3.4SGD函數(shù)
這個函數(shù)是訓(xùn)練的入口,比如我們之前的訓(xùn)練代碼:
net.SGD(training_data, 30, 10, 3.0, test_data=test_data)
def SGD(self, training_data, epochs, mini_batch_size, eta,
test_data=None):
if test_data: n_test = len(test_data)
n = len(training_data)
for j in xrange(epochs):
random.shuffle(training_data)
mini_batches = [
training_data[k:k+mini_batch_size]
for k in xrange(0, n, mini_batch_size)]
for mini_batch in mini_batches:
self.update_mini_batch(mini_batch, eta)
if test_data:
print "Epoch {0}: {1} / {2}".format(
j, self.evaluate(test_data), n_test)
else:
print "Epoch {0} complete".format(j)
第一個參數(shù)就是training_data。
第二個參數(shù)就是epochs,也就是總共對訓(xùn)練數(shù)據(jù)迭代多少次,我們這里是30次迭代。
第三個參數(shù)是batch大小,我們這里是10,最后一個參數(shù)是eta,也就是步長,這里是3.0.除了網(wǎng)絡(luò)結(jié)構(gòu)(比如總共多少個hiddenlayer,每個hidderlayer多少個hiddenunit),另外一個非常重要的參數(shù)就是步長。前面我們也討論過了,步長太小,收斂速度過慢,步長太大,可能不收斂。實(shí)際的情況是沒有一個萬能的準(zhǔn)則,更多的是根據(jù)數(shù)據(jù),不停的嘗試合適的步長。如果發(fā)現(xiàn)收斂太慢,就適當(dāng)調(diào)大,反之則調(diào)小。所以要訓(xùn)練好一個神經(jīng)網(wǎng)絡(luò),還是有很多tricky的技巧,包括參數(shù)怎么初始化,激活函數(shù)怎么選擇,比SGD更好的優(yōu)化算法等等。
第四個參數(shù)test_data是可選的,如果有(我們的例子是穿了進(jìn)來的),則每次epoch之后都測試一下。
代碼的大致解釋我用注釋的形式嵌在代碼里了:
for j in xrange(epochs): ## 一共進(jìn)行 epochs=30 輪迭代
random.shuffle(training_data) ## 訓(xùn)練數(shù)據(jù)隨機(jī)打散
mini_batches = [
training_data[k:k+mini_batch_size]
for k in xrange(0, n, mini_batch_size)] ## 把50,000個訓(xùn)練數(shù)據(jù)分成5,000個batch,每個batch包含10個訓(xùn)練數(shù)據(jù)。
for mini_batch in mini_batches: ## 對于每個batch
self.update_mini_batch(mini_batch, eta) ## 使用梯度下降更新參數(shù)
if test_data: ## 如果提供了測試數(shù)據(jù)
print "Epoch {0}: {1} / {2}".format(
j, self.evaluate(test_data), n_test) ## 評價在測試數(shù)據(jù)上的準(zhǔn)確率
else:
print "Epoch {0} complete".format(j)
下面是evaluate函數(shù):
def evaluate(self, test_data):
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)
對于test_data里的每一組(x,y),y是0-9之間的正確答案。而self.feedforward(x)返回的是10維的數(shù)組,我們選擇得分最高的那個值作為模型的預(yù)測結(jié)果np。argmax就是返回最大值的下標(biāo)。比如x=[0.3,0.6,0.1,0,…。],那么argmax(x)=1.
因此test_results這個列表的每一個元素是一個tuple,tuple的第一個是模型預(yù)測的數(shù)字,而第二個是正確答案。
所以最后一行返回的是模型預(yù)測正確的個數(shù)。
3.5update_mini_batch函數(shù)
def update_mini_batch(self, mini_batch, eta):
nabla_b = [np.zeros(b.shape) for b in self.biases]
## 回憶一下__init__,biases是一個列表,包含兩個矩陣,分別是30*1和10*1
## 我們先構(gòu)造一個和self.biases一樣大小的列表,用來存放累加的梯度(偏導(dǎo)數(shù))
nabla_w = [np.zeros(w.shape) for w in self.weights]
## 同上, weights包含兩個矩陣,大小分別是30*784和10*30
for x, y in mini_batch:
delta_nabla_b, delta_nabla_w = self.backprop(x, y)
## 對于一個訓(xùn)練數(shù)據(jù)(x,y)計(jì)算loss相對于所有參數(shù)的偏導(dǎo)數(shù)
## 因此delta_nabla_b和self.biases, nabla_b是一樣大小(shape)
## 同樣delta_nabla_w和self.weights,nabla_w一樣大小
nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
## 把bias的梯度累加到nabla_b里
nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
## 把weight的梯度累加到nable_w里
self.weights = [w-(eta/len(mini_batch))*nw
for w, nw in zip(self.weights, nabla_w)]
## 使用這個batch的梯度和eta(步長)更新參數(shù)weights
self.biases = [b-(eta/len(mini_batch))*nb
for b, nb in zip(self.biases, nabla_b)]
## 更新biases
## 這里更新參數(shù)是除了batch的大小(10),有的人實(shí)現(xiàn)時不除,其實(shí)沒有什么區(qū)別,因?yàn)槌瑓?shù)eta會有所不同,如果不除,那么eta相當(dāng)于是0.3(在eta那里就除了batch的大小了)。
3.6backprop函數(shù)
這個函數(shù)就是求loss相對于所有參數(shù)的偏導(dǎo)數(shù),這里先不仔細(xì)講解,等下次我們學(xué)習(xí)梯度的求解方法我們再回來討論,這里可以先了解一下這個函數(shù)的輸入和輸出,把它當(dāng)成一個黑盒就行,其實(shí)它的代碼也很少,但是如果不知道梯度的公式,也很難明白。
def backprop(self, x, y):
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
# feedforwardactivation = x
activations = [x] # list to store all the activations, layer by layerzs = [] # list to store all the z vectors, layer by layerfor b, w in zip(self.biases, self.weights):
z = np.dot(w, activation)+b
zs.append(z)
activation = sigmoid(z)
activations.append(activation)
# backward passdelta = 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 xrange(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)
它的輸入就是一個訓(xùn)練樣本(x,y)分別是784*1和10*1.輸出就是和self.biases,self.weights一樣大小的列表,然后列表中的每一個數(shù)組的大小也是一樣。具體到上面的例子,輸出nabla_b包含兩個矩陣,大小分別是30*1和10*1;nabla_w也包含兩個矩陣,大小分別是30*784和10*30.