本系列文章面向程序員,希望通過(guò)Image Caption Generation,一個(gè)有意思的具體任務(wù),深入淺出地介紹深度學(xué)習(xí)的知識(shí),涉及到很多深度學(xué)習(xí)流行的模型,如CNN,RNN/LSTM,Attention等。本文為第二篇。
作者李理,MDCC2016移動(dòng)開(kāi)發(fā)者大會(huì)人工智能與機(jī)器人專場(chǎng)的出品人,邀請(qǐng)人工智能一線專家擔(dān)任演講嘉賓,從無(wú)人駕駛、智能機(jī)器人、智能應(yīng)用開(kāi)發(fā)實(shí)戰(zhàn)等方面解讀人工智能技術(shù)的內(nèi)涵及其對(duì)移動(dòng)開(kāi)發(fā)工作的影響。
2.機(jī)器學(xué)習(xí)基本概念和前饋神經(jīng)網(wǎng)絡(luò)
2.1機(jī)器學(xué)習(xí)基本概念
大家可能平時(shí)都寫(xiě)過(guò)很多程序,寫(xiě)程序和機(jī)器學(xué)習(xí)的思路可能有一些不同。寫(xiě)程序時(shí),我們是“上帝”,我們規(guī)定計(jì)算機(jī)的每一個(gè)步驟,第一步做什么第二步做什么,我們稱之為算法。我們能夠控制所有的情況,如果出了任何問(wèn)題,肯定都是程序員的責(zé)任。而在機(jī)器學(xué)習(xí)的時(shí)候,我們只是“老師”。我們告訴學(xué)生(計(jì)算機(jī))輸入是什么,輸出是什么,然后期望它能夠?qū)W到和我們類似的知識(shí)。比如我們跟小孩說(shuō)這是狗,那是貓,我們沒(méi)有辦法像上帝那樣拿著“納米手術(shù)刀”去操作人腦神經(jīng)元的連接方式。我們只能不斷的給小孩“訓(xùn)練數(shù)據(jù)”,然后期望他能夠?qū)W會(huì)什么是貓,即使我們覺(jué)得他“學(xué)會(huì)”了識(shí)別貓,我們也沒(méi)有辦法知道他是“怎么”學(xué)會(huì)的,而且同樣的訓(xùn)練過(guò)程可能換一個(gè)人就不好使。
機(jī)器學(xué)習(xí)和人類的學(xué)習(xí)是類似的——我們也是給它訓(xùn)練數(shù)據(jù),然后期望它能學(xué)會(huì)。我們會(huì)給機(jī)器建一個(gè)模型,從數(shù)學(xué)的角度來(lái)說(shuō)一個(gè)模型就是一個(gè)函數(shù),它的輸入一般是一個(gè)向量【當(dāng)然可以是二維的矩陣如圖片或者三維的張量比如視頻】,輸出可以是有限的離散的標(biāo)簽如“貓”,“狗”,這類問(wèn)題我們稱之為分類;而如果輸出是連續(xù)的值比如用這個(gè)模型來(lái)預(yù)測(cè)氣溫,那么我們就稱之為回歸。其實(shí)人類的很多科學(xué)活動(dòng)和日常生活,都是在“學(xué)習(xí)”模型和“應(yīng)用”模型。比如開(kāi)普勒通過(guò)觀測(cè)大量天文數(shù)據(jù)“歸納”出行星的運(yùn)動(dòng)規(guī)律。從本質(zhì)上講,智能就是從“過(guò)去”學(xué)習(xí),然后根據(jù)“現(xiàn)在”來(lái)預(yù)測(cè)可能的將來(lái)并根據(jù)自己的目標(biāo)選擇有利于自己行為。只不過(guò)之前,似乎只有人類能夠從數(shù)據(jù)中“學(xué)習(xí)”出規(guī)律,而人工智能的目標(biāo)就是讓機(jī)器也有類似的學(xué)習(xí)能力。
模型用數(shù)學(xué)來(lái)說(shuō)就是一個(gè)函數(shù),我們?nèi)四X的函數(shù)由神經(jīng)元的連接構(gòu)成,它可能是一個(gè)很復(fù)雜的函數(shù),我們現(xiàn)在還很難徹底研究清楚。神經(jīng)網(wǎng)絡(luò)就是試圖通過(guò)計(jì)算機(jī)來(lái)模擬和借鑒人腦這個(gè)模型,除了我們這里要講的神經(jīng)網(wǎng)絡(luò)之外,機(jī)器學(xué)習(xí)領(lǐng)域還有各種各樣的模型,它們各有特點(diǎn)。但不管形式怎么變化,本質(zhì)都是一個(gè)函數(shù)。一個(gè)(或者更準(zhǔn)確的是一種)模型一般都是一種函數(shù)形式,它有一些“參數(shù)”可以改變。而學(xué)習(xí)的過(guò)程就是不斷調(diào)整這些參數(shù),使得輸出(盡量)接近“正確”的答案。但是一般情況下很難所有的數(shù)據(jù)我們都能預(yù)測(cè)正確,所以一般我們會(huì)定義一個(gè)loss function,可以理解為“錯(cuò)誤”的程度,錯(cuò)的越“離譜”,loss就越大。而我們的目標(biāo)就是調(diào)整參數(shù)使得loss最小。
但是我們是在“訓(xùn)練”數(shù)據(jù)上調(diào)整的參數(shù),那么它能在“測(cè)試”數(shù)據(jù)上也表現(xiàn)的好嗎?這個(gè)就是模型的“泛化”能力了。就和人在學(xué)校學(xué)習(xí)一樣,有的同學(xué)做過(guò)的一模一樣的題就會(huì),但是考試時(shí)稍微改變一下就不會(huì)了,這就是“泛化”能力太差,學(xué)到的不是最本質(zhì)的東西。所以平時(shí)會(huì)定期有一些“模擬考試”,來(lái)檢驗(yàn)學(xué)生是不是真的學(xué)會(huì)了,如果考得不好,那就打回去重新訓(xùn)練模型調(diào)整參數(shù)。這在機(jī)器學(xué)習(xí)里對(duì)應(yīng)的就是validation的階段。最后到最終的考試了,就是最終檢驗(yàn)的時(shí)候了,這個(gè)試卷里的題目是不能提前讓人看到的,只能拿出來(lái)用一次,否則就是作弊了。對(duì)應(yīng)到機(jī)器學(xué)習(xí)里就是test階段。
當(dāng)然這里用通俗的話描述了機(jī)器學(xué)習(xí),主要是有監(jiān)督的學(xué)習(xí)。其實(shí)機(jī)器學(xué)習(xí)還有無(wú)監(jiān)督的學(xué)習(xí)和強(qiáng)化學(xué)習(xí)。前者就是不給答案,只給數(shù)據(jù),讓人總結(jié)規(guī)律;而后者會(huì)有答案,但是答案不是現(xiàn)在就告訴你。我個(gè)人覺(jué)得人類社會(huì)里更多的是監(jiān)督學(xué)習(xí)和強(qiáng)化學(xué)習(xí)。從人類社會(huì)總體來(lái)說(shuō),強(qiáng)化學(xué)習(xí)是獲取新知識(shí)的唯一途徑,也就是向自然學(xué)習(xí),我們做了一個(gè)決策,其好壞可能要很長(zhǎng)一段時(shí)間才能顯現(xiàn)出來(lái)。而學(xué)習(xí)出來(lái)的這些知識(shí)通過(guò)監(jiān)督的方式,通過(guò)家庭和學(xué)校的教育教給下一代。
另外輸出除了簡(jiǎn)單的分為離散和連續(xù),還可以是序列(時(shí)序)的,比如自然語(yǔ)言(文本)是一個(gè)字符串的序列,對(duì)于我們的Image Caption Generation就是生成一個(gè)單詞序列。另外還有更復(fù)雜的輸出,比如parsing,輸出是一棵語(yǔ)法樹(shù)。
2.2多層神經(jīng)網(wǎng)絡(luò)
前面介紹了機(jī)器學(xué)習(xí)的基本概念,接下來(lái)我們就來(lái)學(xué)習(xí)一下神經(jīng)網(wǎng)絡(luò),F(xiàn)在流行的說(shuō)法“深度學(xué)習(xí)”,其實(shí)大多指的就是“深度神經(jīng)網(wǎng)絡(luò)”,那么首先我們先了解一下“淺度神經(jīng)網(wǎng)絡(luò)”,也就是傳統(tǒng)的神經(jīng)網(wǎng)絡(luò)。這里的內(nèi)容主要來(lái)自http://neuralnetworksanddeeplearning.com的前兩章。
2.2.1手寫(xiě)數(shù)字識(shí)別問(wèn)題
我們?cè)趯W(xué)習(xí)一門(mén)新的語(yǔ)言時(shí)會(huì)寫(xiě)一個(gè)hello world程序,而mnist數(shù)據(jù)的手寫(xiě)數(shù)字識(shí)別就是一個(gè)很好的學(xué)習(xí)機(jī)器學(xué)習(xí)(包括深度學(xué)習(xí))的一個(gè)hello world任務(wù)。
計(jì)算機(jī)和人類大腦似乎有很大的不同,很多人類認(rèn)為復(fù)雜的工作計(jì)算機(jī)可能認(rèn)為很簡(jiǎn)單,而人類認(rèn)為很簡(jiǎn)單的事情計(jì)算機(jī)可能非常難處理。比如數(shù)字的計(jì)算,記憶,人類的準(zhǔn)確度和速度都遠(yuǎn)遠(yuǎn)不如計(jì)算機(jī)。但是識(shí)別0-9的手寫(xiě)數(shù)字,我們覺(jué)得很輕而易舉的事情,讓計(jì)算機(jī)程序來(lái)處理卻異常困難。經(jīng)過(guò)數(shù)百萬(wàn)年進(jìn)化的人類視覺(jué)系統(tǒng)在我們大腦沒(méi)有意識(shí)到的時(shí)候就已經(jīng)幫我們完成了數(shù)字的識(shí)別,把那些復(fù)雜的視覺(jué)處理過(guò)程深深的掩藏了起來(lái)。但當(dāng)我們想自己寫(xiě)一個(gè)程序來(lái)識(shí)別數(shù)字的時(shí)候,這些困難才能體現(xiàn)出來(lái)。首先,對(duì)于計(jì)算機(jī)來(lái)說(shuō),它“看到”的不是數(shù)字,甚至不是筆畫(huà)。它“看到”的只是一個(gè)二位的矩陣(數(shù)組),每個(gè)點(diǎn)都是一個(gè)數(shù)字。比如下圖,我們“看到”的是左邊的“貓”,其實(shí)計(jì)算機(jī)“看到”的是右邊的像素灰度值。當(dāng)然我們視覺(jué)系統(tǒng)的視網(wǎng)膜看到的也是類似的一些“數(shù)值”,只不過(guò)我們的視覺(jué)系統(tǒng)已經(jīng)處理了這些信息并且把它識(shí)別成了“貓”(甚至和語(yǔ)言還做了映射)。
MNIST數(shù)據(jù)介紹:MNIST的每個(gè)圖片經(jīng)過(guò)縮放和居中等預(yù)處理之后,大小是28*28,每個(gè)點(diǎn)都是0-255的灰度值,下圖是一些樣例?偣灿60,000個(gè)訓(xùn)練數(shù)據(jù)(0-9共10個(gè)類別,每個(gè)類別6,000個(gè))和10,000個(gè)測(cè)試數(shù)據(jù)。一般會(huì)拿60000個(gè)中的50000個(gè)來(lái)做訓(xùn)練集,而剩下的10000個(gè)用來(lái)做驗(yàn)證集(用來(lái)選擇一些超參數(shù))。
mnist樣例數(shù)據(jù)
如果我們自己來(lái)寫(xiě)一個(gè)“算法”識(shí)別數(shù)字“9”,我們可能會(huì)這么定義:9在上面有個(gè)圓圈,在這個(gè)圓圈的右下部分有一個(gè)豎直的筆畫(huà)。說(shuō)起來(lái)很簡(jiǎn)單,如果用算法來(lái)實(shí)現(xiàn)就很麻煩了:什么是圓圈?每個(gè)人畫(huà)的圓圈都不同,同樣豎直的筆畫(huà)怎么識(shí)別,圓圈和豎直筆畫(huà)連接處怎么尋找,右下是哪?大家如果有興趣可以嘗試一下用上面的方法,其實(shí)最早做數(shù)字識(shí)別就是這樣的思路。
機(jī)器學(xué)習(xí)的思路則不同,它不需要這么細(xì)節(jié)的“指示”計(jì)算機(jī)應(yīng)該怎么做。而是給計(jì)算機(jī)足夠的“訓(xùn)練”樣本,讓它“看”不同的10個(gè)數(shù)字,然后讓它“學(xué)”出來(lái)。前面我們也講了,現(xiàn)在的機(jī)器學(xué)習(xí)一般是一個(gè)參數(shù)化的模型。比如最簡(jiǎn)單的一個(gè)線性模型:f(w;x)=w0+w1*x1+w2*x2.如果我們的輸入有兩個(gè)“特征”x1和x2,那么這個(gè)模型有3個(gè)參數(shù)w0,w1和w2,機(jī)器學(xué)習(xí)的過(guò)程就是選擇“最優(yōu)”的參數(shù)。對(duì)于上面的mnist數(shù)據(jù),輸入就是28*28=784維的向量。
如果用“原始”的輸入作為“特征”,線性的模型很可能學(xué)到一些簡(jiǎn)單的特征,比如它看到1一般是分布在從上到下居中的一些位置,那么對(duì)于這些位置一旦發(fā)現(xiàn)有比較大的灰度值,那么就傾向于判斷成1.如果一個(gè)像素點(diǎn)2也經(jīng)常出現(xiàn),但3不出現(xiàn),那么它就能學(xué)到如果這個(gè)像素出現(xiàn),那么這個(gè)數(shù)字是2和3的可能性就大一些。
但是這樣的“特征”可能不是“本質(zhì)”的,因?yàn)槲覍?xiě)字的時(shí)候筆稍微平移一點(diǎn),那么你之前“學(xué)到”的參數(shù)就可能有問(wèn)題。而更“本質(zhì)”的特征是什么呢?可能還是像之前我們總結(jié)的——9在上面有個(gè)圓圈,在這個(gè)圓圈的右下部分有一個(gè)豎直的筆畫(huà)。我們把識(shí)別一個(gè)數(shù)字的問(wèn)題轉(zhuǎn)化成圓圈和豎直筆畫(huà)的問(wèn)題。傳統(tǒng)的機(jī)器學(xué)習(xí)需要方法來(lái)提取“類似”(但不完全是)基本筆畫(huà)這樣的“特征”,這些特征相對(duì)于像素的特征會(huì)更加“本質(zhì)”。但是要“提取”這些特征需要很多的“領(lǐng)域”知識(shí),比如圖像處理的技術(shù)。所以使用傳統(tǒng)的機(jī)器學(xué)習(xí)方法來(lái)解決問(wèn)題,我們不但需要很多機(jī)器學(xué)習(xí)的知識(shí),而且也需要很多“領(lǐng)域”的知識(shí),同時(shí)擁有這兩方面的知識(shí)是比較難的。
而“深度學(xué)習(xí)”最近之所以火熱,其中很重要的一個(gè)原因就是對(duì)于很多問(wèn)題,我們只需要輸入最原始的信號(hào),比如圖片的像素值,通過(guò)“多層”的網(wǎng)絡(luò),讓底層的網(wǎng)絡(luò)學(xué)習(xí)出“底層”的特征,比如基本的形狀,而中間的層學(xué)習(xí)出抽象一點(diǎn)的特征,比如眼睛鼻子耳朵。而更上的層次識(shí)別出這是一個(gè)貓還是一個(gè)狗。所有這些都是機(jī)器學(xué)習(xí)出來(lái)的,所以基本不需要領(lǐng)域的知識(shí)。
上面的圖就說(shuō)明了這一點(diǎn),而且我們發(fā)現(xiàn)越是底層的特征就越“通用”,不管是貓鼻子還是狗眼睛,可能用到的都是一些基本的形狀,因此我們可以把這些知識(shí)(特征)transfer到別的任務(wù),也就是transfer learning,后面我們講到CNN的時(shí)候還會(huì)提及。
2.2.2單個(gè)神經(jīng)元和多層神經(jīng)網(wǎng)絡(luò)(MLP)
神經(jīng)網(wǎng)絡(luò)從名字來(lái)看是和人類的大腦有些關(guān)系的,而且即使到現(xiàn)在,很多有用的東西如CNN和Attention,都有很多借鑒神經(jīng)科學(xué)研究人腦的結(jié)果的。不過(guò)這里我就不介紹這些東西了,有興趣的讀者可以找一些資料來(lái)了解。
一個(gè)神經(jīng)元如下圖的結(jié)構(gòu):
它的輸入是一個(gè)向量,(x1,x2,x3),輸出是一個(gè)標(biāo)量,一個(gè)實(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時(shí),sigmoid(z)=0.5z趨于無(wú)窮大時(shí),sigmoid(z)趨近于1,z趨于負(fù)無(wú)窮,值趨于0.為什么選擇這樣的激活函數(shù)呢?因?yàn)槭悄M人腦的神經(jīng)元。人腦的神經(jīng)元也是把輸入的信號(hào)做加權(quán)累加,然后看累加和是否超過(guò)一個(gè)“閾值”。如果超過(guò),繼續(xù)向下一個(gè)神經(jīng)元發(fā)送信號(hào),否則就不發(fā)送。因此人腦的神經(jīng)元更像是一個(gè)階躍函數(shù):
最早的感知機(jī)(Perception)其實(shí)用的就是這個(gè)激活函數(shù)。但是它有一個(gè)缺點(diǎn)就是0之外的所有點(diǎn)的導(dǎo)數(shù)都是0,在0點(diǎn)的導(dǎo)數(shù)是無(wú)窮大,所以很難用梯度的方法優(yōu)化。而Sigmoid函數(shù)是處處可導(dǎo)。下面我手工推導(dǎo)了一下,如果大家不熟悉可以試著推導(dǎo)一下Sigmoid函數(shù)的導(dǎo)數(shù),我們后面也會(huì)用到。
我們把許多的單個(gè)神經(jīng)元按照層次組織起來(lái)就是多層的神經(jīng)網(wǎng)絡(luò)。
比如我們的手寫(xiě)數(shù)字識(shí)別,輸入層是784維,就是神經(jīng)網(wǎng)絡(luò)的地一層,然后中間有15個(gè)hidden(因?yàn)槲覀儾恢浪闹担┥窠?jīng)元,然后輸出層是10個(gè)神經(jīng)元。中間隱層的每個(gè)神經(jīng)元的輸入都是784個(gè)原始像素通過(guò)上面的公式加權(quán)累加然后用sigmoid激活。而輸出層的每一個(gè)神經(jīng)元也是中間15個(gè)神經(jīng)元的累加然后激活。上面的圖就是一個(gè)3層的神經(jīng)網(wǎng)絡(luò)。
輸入一個(gè)28*28的圖像,我們得到一個(gè)10維的輸出,那么怎么分類呢?最直接的想法就是把認(rèn)為最大的那個(gè)輸出,比如輸出是(10,11,12,13,14,15,16,17,18,19),那么我們認(rèn)為輸出是9.
當(dāng)然,更常見(jiàn)的做法是最后一次經(jīng)過(guò)線性累加之后并不用Sigmoid函數(shù)激活,而是加一個(gè)softmax的函數(shù),讓10個(gè)輸出加起來(lái)等于1,這樣更像一個(gè)概率。而我們上面的情況,雖然訓(xùn)練數(shù)據(jù)的輸出加起來(lái)是1,但是實(shí)際給一個(gè)其它輸入,輸出加起來(lái)很可能不是1.不過(guò)為了與Nielsen的文章一致,我們還是先用這種方法。
因此,假設(shè)我們有了這些參數(shù)【總共是784*15+15(w0或者叫bias)+15*10+10】,我們很容易通過(guò)上面的公式一個(gè)一個(gè)的計(jì)算出10維的輸出。然后選擇最大的那個(gè)作為我們識(shí)別的結(jié)果。問(wèn)題的難點(diǎn)就在怎么選擇這么多參數(shù),然后使得我們分類的錯(cuò)誤最少。
而我們?cè)趺从?xùn)練呢?對(duì)于一張圖片,假設(shè)它是數(shù)字“1”,那么我們期望它的輸出是(0,1,0,0,0,0,0,0,0,0),所以我們可以簡(jiǎn)單的用最小平方錯(cuò)誤作為損失函數(shù)。不過(guò)你可能會(huì)有些疑問(wèn),我們關(guān)注的指標(biāo)應(yīng)該是分類的“正確率”(或者錯(cuò)誤率),那么我們?yōu)槭裁床恢苯影逊诸惖腻e(cuò)誤率作為損失函數(shù)呢?這樣神經(jīng)網(wǎng)絡(luò)學(xué)習(xí)出來(lái)的參數(shù)就是最小化錯(cuò)誤率。
主要的原因就是錯(cuò)誤率不是參數(shù)的連續(xù)函數(shù)。因?yàn)橐粋(gè)訓(xùn)練數(shù)據(jù)如果分類正確那么就是1,否則就是0,這樣就不是一個(gè)連續(xù)的函數(shù)。比如最簡(jiǎn)單的兩類線性分類器,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í)上對(duì)于這個(gè)例子,只要是w0變小,他們的累加都是小于0的】所以f(x)的值不會(huì)變化,而w0一直增大到使累加和等于0之前都不會(huì)變化,只有大于0時(shí)突然變成1了,然后一直就是1.因此之前的錯(cuò)誤率都是1,然后就突然是0.所以它不是個(gè)連續(xù)的函數(shù)。
因?yàn)槲覀兪褂玫膬?yōu)化算法一般是(隨機(jī))梯度下降的算法,在每次迭代的時(shí)候都是試圖做一個(gè)微小的參數(shù)調(diào)整使得損失變小,但是不連續(xù)的函數(shù)顯然也不可導(dǎo),也就沒(méi)法用這個(gè)算法來(lái)優(yōu)化參數(shù)。
因此我們使用了最小平方誤差(MSE)損失函數(shù)。
y(x)就是神經(jīng)網(wǎng)絡(luò)的輸出,可能寫(xiě)成f(x)大家會(huì)習(xí)慣一點(diǎn)。a是目標(biāo)的輸出,比如當(dāng)前分類是數(shù)字1,那么我們期望的輸出就是(0,1,0,0,0,0,0,0,0,0)。
首先這個(gè)損失函數(shù)是參數(shù)w的連續(xù)函數(shù),因?yàn)閥(x)就是神經(jīng)網(wǎng)絡(luò)的輸出,每個(gè)神經(jīng)元都是它的輸入的線性加權(quán)累加,然后使用sigmoid激活函數(shù)【如果使用最早的階躍函數(shù)就不連續(xù)了,所以后來(lái)使用了Sigmoid函數(shù)】,然后每一層的神經(jīng)元都是用上一層的神經(jīng)元通過(guò)這樣的方式計(jì)算的(只不過(guò)每個(gè)神經(jīng)元的參數(shù)也就是權(quán)重是不同的數(shù)值而已),所以這些連續(xù)函數(shù)的復(fù)合函數(shù)也是連續(xù)的。
其次這個(gè)損失函數(shù)和我們的最終優(yōu)化目標(biāo)是“大致”一致的。比如C(w,b)趨于0時(shí),它就要求y(x)趨于a,那么我們的分類也就趨于正確。當(dāng)然可能存在一種極端的情況,比如有3個(gè)訓(xùn)練數(shù)據(jù),第一組參數(shù),它分類正確了2個(gè)訓(xùn)練數(shù)據(jù),但是錯(cuò)的那1個(gè)錯(cuò)的很“離譜”,也就是y(x)和a差距極大;而第二組參數(shù),他正確分類了1個(gè)訓(xùn)練數(shù)據(jù),但是錯(cuò)的那兩個(gè)都還不算太差。那么這種情況下MSE和正確率并不一致。
2.2.3隨機(jī)梯度下降(Stochastic Gradient Descent)和自動(dòng)求梯度(Automatic Derivatives)
上面說(shuō)了,我們有了一個(gè)參數(shù)化的模型,訓(xùn)練的過(guò)程就是根據(jù)訓(xùn)練數(shù)據(jù)和lossfunction,選擇“最優(yōu)”的參數(shù),使得loss“最小”,這從數(shù)學(xué)上來(lái)講就是一個(gè)優(yōu)化問(wèn)題。這看起來(lái)似乎不是什么值得一提的問(wèn)題,也許你還記得微積分里的知識(shí),極值點(diǎn)的各種充分必要條件,比如必要條件是導(dǎo)數(shù)是0,然后直接把參數(shù)解出來(lái)。但在現(xiàn)實(shí)生活中的函數(shù)遠(yuǎn)比教科書(shū)里學(xué)到的復(fù)雜,很多模型都無(wú)法用解析的方式求出最優(yōu)解。所以現(xiàn)實(shí)的方法就是求“數(shù)值”解,一般最常見(jiàn)的方法就是迭代的方法,根據(jù)現(xiàn)在的參數(shù),我們很小幅度的調(diào)整參數(shù),使得loss變小一點(diǎn)點(diǎn)。然后一步一步的最終能夠達(dá)到一個(gè)最優(yōu)解(一般是局部最優(yōu)解)。那怎么小幅調(diào)整呢?像悶頭蒼蠅那樣隨機(jī)亂試顯然效率極低。因此我們要朝著一個(gè)能使函數(shù)值變小的方向前進(jìn)。而在一個(gè)點(diǎn)能使函數(shù)值變小的方向有無(wú)窮多個(gè),但有一個(gè)方向是下降速度最快的,那就是梯度。因此更常見(jiàn)的方法就是在當(dāng)前點(diǎn)求函數(shù)的梯度,然后朝著梯度的方向下降。朝梯度的方向走多遠(yuǎn)呢?一般走一個(gè)比較小的值是比較安全的,這個(gè)值就是“步長(zhǎng)”。一般剛開(kāi)始隨機(jī)的初始化參數(shù),loss比較大,所以多走一些也沒(méi)關(guān)系,但是到了后面,就不能走太快,否則很容易錯(cuò)過(guò)最優(yōu)的點(diǎn)。
因?yàn)閘oss是所有訓(xùn)練數(shù)據(jù)的函數(shù),所以求loss的梯度需要計(jì)算所有的訓(xùn)練數(shù)據(jù),對(duì)于很多task來(lái)說(shuō),訓(xùn)練數(shù)據(jù)可能上百萬(wàn),計(jì)算一次代價(jià)太大,所以一般會(huì)“隨機(jī)”的采樣少部分?jǐn)?shù)據(jù),比如128個(gè)數(shù)據(jù),求它的梯度。雖然128個(gè)點(diǎn)的梯度和一百萬(wàn)個(gè)的是不一樣的,但是從概率來(lái)講至少是一致的方向而不會(huì)是相反的方向,所以也能使loss變小。當(dāng)然這個(gè)128是可以調(diào)整的,它一般被叫做batchsize,最極端的就是batch是1和一百萬(wàn),那么分別就是onlinelearning和退化到梯度下降。batchsize越大,計(jì)算一次梯度的時(shí)間就越久【當(dāng)然由于GPU和各種類似SSE的指令,一次計(jì)算128個(gè)可能并不比計(jì)算1個(gè)慢多少】,隨機(jī)梯度和真正梯度一致的概率就越大,走的方向就更“正確”;batchsize越小,計(jì)算一次的時(shí)間就越短,但可能方向偏離最優(yōu)的方向就更遠(yuǎn),會(huì)在不是“冤枉路”。但實(shí)際的情況也很難說(shuō)哪個(gè)值是最優(yōu)的,一般的經(jīng)驗(yàn)取值都是幾十到一兩百的范圍,另外因?yàn)橛?jì)算機(jī)都是字節(jié)對(duì)齊,32,64,128這樣的值也許能稍微加快矩陣運(yùn)算的速度。但是實(shí)際也很多人選擇10,50,100這樣的值。
除了常見(jiàn)的隨機(jī)梯度下降,還有不少改進(jìn)的方法,如Momentum,Adagrad等等,有興趣的可以看看http://cs231n.github.io/neural-networks-3/#update,里面還有個(gè)動(dòng)畫(huà),比較了不同方法的收斂速度的比較。
通過(guò)上面的分析,我們把問(wèn)題變成了怎么求loss對(duì)參數(shù)W的梯度。
求梯度有如下4種方法:
手工求解析解
比如f(x)=x^2,df/dx=2*x。然后我們要求f(x)在x=1.5的值,代進(jìn)去就2*1.5=3
數(shù)值解
使用極限的定義:
圖片描述
機(jī)器符號(hào)計(jì)算
讓機(jī)器做符號(hào)運(yùn)算,實(shí)現(xiàn)1的方法,但是機(jī)器如果優(yōu)化的不好的話可能會(huì)有一些不必要的運(yùn)算。
比如x^2+2*x*y+y^2,直接對(duì)x求導(dǎo)數(shù)變成了2*x+2*y,兩次乘法一次加分,但是我們可以合并一下變成2*(x+y),一次乘法一次加分。
自動(dòng)梯度
下面我會(huì)在稍微細(xì)講一下,所以這里暫時(shí)跳過(guò)。
這些方法的優(yōu)缺點(diǎn):
手工求解“數(shù)學(xué)”要求高,有可能水平不夠求不對(duì),但效率應(yīng)該是能最優(yōu)的。
沒(méi)任何函數(shù),甚至沒(méi)有解析導(dǎo)數(shù)的情況下都能使用,缺點(diǎn)是計(jì)算量太大,而且只是近似解【因?yàn)闃O限的定義】,在某些特別不“連續(xù)”的地方可能誤差較大。所以實(shí)際使用是很少,只是用它來(lái)驗(yàn)證其它方法是否正確。
機(jī)器符號(hào)計(jì)算,前面說(shuō)的,依賴于這個(gè)庫(kù)的好壞。
實(shí)際的框架,如TensorFlow就是自動(dòng)梯度,而Theano就是符號(hào)梯度。
2.2.4編程實(shí)戰(zhàn)
通過(guò)上面的介紹,我們其實(shí)就可以實(shí)現(xiàn)一個(gè)經(jīng)典的前饋(feedforward)神經(jīng)網(wǎng)絡(luò)了,這種網(wǎng)絡(luò)結(jié)構(gòu)很簡(jiǎn)單,每一層的輸入是前一層的輸出。輸入層沒(méi)有輸入,它就是原始的信號(hào)輸入。而且上一層的所有神經(jīng)元都會(huì)連接到下一層的所有神經(jīng)元,就像我們剛才的例子,輸入是784,中間層是15,那么就有785*15個(gè)連接【再加上每個(gè)中間節(jié)點(diǎn)有一個(gè)bias】。所以這種網(wǎng)絡(luò)有時(shí)候也加做全連接的網(wǎng)絡(luò)(fullconnected),用來(lái)和CNN這種不是全連接的網(wǎng)絡(luò)有所區(qū)別,另外就是信號(hào)是從前往后傳遞,沒(méi)有反饋,所以也叫前潰神經(jīng)網(wǎng)絡(luò),這是為了和RNN這種有反饋的區(qū)別。
當(dāng)然,我們還沒(méi)有講怎么計(jì)算梯度,也就是損失函數(shù)相對(duì)于每一個(gè)參數(shù)的偏導(dǎo)數(shù)。在下一部分我們會(huì)詳細(xì)討論介紹,這里我們先把它當(dāng)成一個(gè)黑盒的函數(shù)就好了。
1.代碼
我們這里學(xué)習(xí)一下Nielsen提供的代碼。代碼非常簡(jiǎn)潔,只有不到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)建一個(gè)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次迭代,最終在測(cè)試數(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代碼很容易閱讀,即使之前沒(méi)有用過(guò),稍微學(xué)習(xí)兩天也就可以上手,而且大部分機(jī)器學(xué)習(xí)相關(guān)的代碼不會(huì)用到太復(fù)雜的語(yǔ)言特性,基本就是一些數(shù)學(xué)的線性代數(shù)的運(yùn)算。而Python的numpy這個(gè)庫(kù)是用的最多的,后面閱讀代碼的時(shí)候我會(huì)把用到的函數(shù)做一些介紹,繼續(xù)下面的閱讀之前建議花十分鐘閱讀一下http://cs231n。github。io/python-numpy-tutorial/。
3.1mnist_loader.load_data_wrapper函數(shù)
這個(gè)函數(shù)用來(lái)讀取mnist數(shù)據(jù),數(shù)據(jù)是放在data/mnist。pkl。gz。首先這是個(gè)gzip的壓縮文件,是Pickle工具序列化到磁盤(pán)的格式。不熟悉也沒(méi)有關(guān)系,反正我們知道這個(gè)函數(shù)的返回值就行了。
這個(gè)函數(shù)返回三個(gè)對(duì)象,分別代表training_data,validation_data和test_data。
training_data是一個(gè)50,000的list,然后其中的每一個(gè)元素是一個(gè)tuple。tuple的第一個(gè)元素是一個(gè)784維的numpy一維數(shù)組。第二個(gè)元素是10維的數(shù)組,也就是one-hot的表示方法——如果正確的答案是數(shù)字0,那么這個(gè)10維數(shù)組就是(1,0,0,…)。
而validation_data是一個(gè)10,000的list,每個(gè)元素也是一個(gè)tuple。tuple的第一個(gè)元素也是784維的numpy一維數(shù)組。第二個(gè)元素是一個(gè)0-9的數(shù)字,代表正確答案是那個(gè)數(shù)字。
test_data的格式和validation_data一樣。
為什么training_data要是這樣的格式呢?因?yàn)檫@樣的格式計(jì)算loss更方便一些。
3.2Network類的構(gòu)造函數(shù)
我們?cè)谡{(diào)用net=network。Network([784,30,10])時(shí)就到了init函數(shù)。為了減少篇幅,代碼里的注釋我都去掉了,重要的地方我會(huì)根據(jù)自己的理解說(shuō)明,但是有空還是值得閱讀代碼里的注釋。
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ù),我們保存下來(lái)的self。num_layers=3,也就是3層的網(wǎng)絡(luò)。每一層的神經(jīng)元的個(gè)數(shù)保存到self。sizes里。接下來(lái)就是構(gòu)造biases數(shù)組并隨機(jī)初始化。因?yàn)檩斎雽邮菦](méi)有參數(shù)的,所以是foryinsizes[1:],我們使用了numpy的random。randn生成正態(tài)分布的隨機(jī)數(shù)用來(lái)作為參數(shù)的初始值。注意這里生成了2維的隨機(jī)變量;貞浺幌拢绻覀冇30個(gè)hiddenunit,那么bias的個(gè)數(shù)也是30,那就生成一個(gè)30維的1維數(shù)組就行了,為什么要是30*1的二維數(shù)組呢?其實(shí)用1維也可以,不過(guò)為了和weights一致,后面代碼方便,就用二維數(shù)組了。另外weights也是一樣的初始化方法,不過(guò)注意randn(y,x)而不是randn(x,y)。比如對(duì)于我們輸入的[784,30,10],weights分別是30*784和10*30的。當(dāng)然其實(shí)weights矩陣轉(zhuǎn)置一下也可以,就是計(jì)算矩陣乘法的時(shí)候也需要有一個(gè)轉(zhuǎn)置。不同的文獻(xiàn)可能有不同的記法,但是我們?cè)趯?shí)現(xiàn)代碼的時(shí)候只需要隨時(shí)注意矩陣的大小,檢查矩陣乘法滿足乘法的約束就行了,矩陣AB能相乘,必須滿足的條件是B的列數(shù)等于A的函數(shù)就行。
對(duì)于Nielsen的記法,矩陣的每一行就是一個(gè)神經(jīng)元的784個(gè)參數(shù),那么weights(30*784)*input(784*1)就得到30個(gè)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
代碼非常簡(jiǎn)單,這里用到了np。dot,也就是矩陣向量的乘法,此外這里有一個(gè)Sigmoid函數(shù),這個(gè)函數(shù)的輸入是numpy的ndarray,輸出也是同樣大小的數(shù)組,不過(guò)對(duì)于每個(gè)元素都進(jìn)行了sigmoid的計(jì)算。用numpy的術(shù)語(yǔ)就是universalfunction,很多文獻(xiàn)里一般都叫elementwise的function。我覺(jué)得后面這個(gè)名字更直接。
#### 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ù)
這個(gè)函數(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)
第一個(gè)參數(shù)就是training_data。
第二個(gè)參數(shù)就是epochs,也就是總共對(duì)訓(xùn)練數(shù)據(jù)迭代多少次,我們這里是30次迭代。
第三個(gè)參數(shù)是batch大小,我們這里是10,最后一個(gè)參數(shù)是eta,也就是步長(zhǎng),這里是3.0.除了網(wǎng)絡(luò)結(jié)構(gòu)(比如總共多少個(gè)hiddenlayer,每個(gè)hidderlayer多少個(gè)hiddenunit),另外一個(gè)非常重要的參數(shù)就是步長(zhǎng)。前面我們也討論過(guò)了,步長(zhǎng)太小,收斂速度過(guò)慢,步長(zhǎng)太大,可能不收斂。實(shí)際的情況是沒(méi)有一個(gè)萬(wàn)能的準(zhǔn)則,更多的是根據(jù)數(shù)據(jù),不停的嘗試合適的步長(zhǎng)。如果發(fā)現(xiàn)收斂太慢,就適當(dāng)調(diào)大,反之則調(diào)小。所以要訓(xùn)練好一個(gè)神經(jīng)網(wǎng)絡(luò),還是有很多tricky的技巧,包括參數(shù)怎么初始化,激活函數(shù)怎么選擇,比SGD更好的優(yōu)化算法等等。
第四個(gè)參數(shù)test_data是可選的,如果有(我們的例子是穿了進(jìn)來(lái)的),則每次epoch之后都測(cè)試一下。
代碼的大致解釋我用注釋的形式嵌在代碼里了:
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個(gè)訓(xùn)練數(shù)據(jù)分成5,000個(gè)batch,每個(gè)batch包含10個(gè)訓(xùn)練數(shù)據(jù)。
for mini_batch in mini_batches: ## 對(duì)于每個(gè)batch
self.update_mini_batch(mini_batch, eta) ## 使用梯度下降更新參數(shù)
if test_data: ## 如果提供了測(cè)試數(shù)據(jù)
print "Epoch {0}: {1} / {2}".format(
j, self.evaluate(test_data), n_test) ## 評(píng)價(jià)在測(cè)試數(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)
對(duì)于test_data里的每一組(x,y),y是0-9之間的正確答案。而self.feedforward(x)返回的是10維的數(shù)組,我們選擇得分最高的那個(gè)值作為模型的預(yù)測(cè)結(jié)果np。argmax就是返回最大值的下標(biāo)。比如x=[0.3,0.6,0.1,0,…。],那么argmax(x)=1.
因此test_results這個(gè)列表的每一個(gè)元素是一個(gè)tuple,tuple的第一個(gè)是模型預(yù)測(cè)的數(shù)字,而第二個(gè)是正確答案。
所以最后一行返回的是模型預(yù)測(cè)正確的個(gè)數(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是一個(gè)列表,包含兩個(gè)矩陣,分別是30*1和10*1
## 我們先構(gòu)造一個(gè)和self.biases一樣大小的列表,用來(lái)存放累加的梯度(偏導(dǎo)數(shù))
nabla_w = [np.zeros(w.shape) for w in self.weights]
## 同上, weights包含兩個(gè)矩陣,大小分別是30*784和10*30
for x, y in mini_batch:
delta_nabla_b, delta_nabla_w = self.backprop(x, y)
## 對(duì)于一個(gè)訓(xùn)練數(shù)據(jù)(x,y)計(jì)算loss相對(duì)于所有參數(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)]
## 使用這個(gè)batch的梯度和eta(步長(zhǎng))更新參數(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í)不除,其實(shí)沒(méi)有什么區(qū)別,因?yàn)槌瑓?shù)eta會(huì)有所不同,如果不除,那么eta相當(dāng)于是0.3(在eta那里就除了batch的大小了)。
3.6backprop函數(shù)
這個(gè)函數(shù)就是求loss相對(duì)于所有參數(shù)的偏導(dǎo)數(shù),這里先不仔細(xì)講解,等下次我們學(xué)習(xí)梯度的求解方法我們?cè)倩貋?lái)討論,這里可以先了解一下這個(gè)函數(shù)的輸入和輸出,把它當(dāng)成一個(gè)黑盒就行,其實(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)
它的輸入就是一個(gè)訓(xùn)練樣本(x,y)分別是784*1和10*1.輸出就是和self.biases,self.weights一樣大小的列表,然后列表中的每一個(gè)數(shù)組的大小也是一樣。具體到上面的例子,輸出nabla_b包含兩個(gè)矩陣,大小分別是30*1和10*1;nabla_w也包含兩個(gè)矩陣,大小分別是30*784和10*30.