李理:從Image Caption Generation理解深度學習(part II)

書接上文:李理:從Image Caption Generation理解深度學習(part I)

2. 機器學習基本概念和前饋神經網絡

2.1 機器學習基本概念

大家可能平時都寫過很多程序,寫程序和機器學習的思路可能有一些不同。寫程序時,我們是“上帝”,我們規定計算機的每一個步驟,第一步做什么第二步做什么,我們稱之為算法。我們能夠控制所有的情況,如果出了任何問題,肯定都是程序員的責任。而在機器學習的時候,我們只是“老師”。我們告訴學生(計算機)輸入是什么,輸出是什么,然后期望它能夠學到和我們類似的知識。比如我們跟小孩說這是狗,那是貓,我們沒有辦法像上帝那樣拿著“納米手術刀”去操作人腦神 經元的連接方式。我們只能不斷的給小孩“訓練數據”,然后期望他能夠學會什么是貓,即使我們覺得他“學會”了識別貓,我們也沒有辦法知道他是“怎么”學會 的,而且同樣的訓練過程可能換一個人就不好使。

機器學習和人類的學習是類似的——我們也是給它訓練數據,然后期望它能學會。我們會給機器建一個模型,從數學的角度來說一個模型就是一個函數,它的輸入一般是一個向量【當然可以是二維的矩陣如圖片或者三維的張量比如視頻】,輸出可以是有限的離散的標簽如“貓”,“狗”,這類問題我們稱之為分類;而如果輸出 是連續的值比如用這個模型來預測氣溫,那么我們就稱之為回歸。其實人類的很多科學活動和日常生活,都是在“學習”模型和“應用”模型。比如開普勒通過觀測 大量天文數據“歸納”出行星的運動規律。從本質上講,智能就是從“過去”學習,然后根據“現在”來預測可能的將來并根據自己的目標選擇有利于自己行為。只不過之前,似乎只有人類能夠從數據中“學習”出規律,而人工智能的目標就是讓機器也有類似的學習能力。

模型用數學來說就是一個函數,我們人腦的函數由神經元的連接構成,它可能是一個很復雜的函數,我們現在還很難徹底研究清楚。神經網絡就是試圖通過計算機來 模擬和借鑒人腦這個模型,除了我們這里要講的神經網絡之外,機器學習領域還有各種各樣的模型,它們各有特點。但不管形式怎么變化,本質都是一個函數。一個(或者更準確的是一種)模型一般都是一種函數形式,它有一些“參數”可以改變。而學習的過程就是不斷調整這些參數,使得輸出(盡量)接近“正確”的答案。 但是一般情況下很難所有的數據我們都能預測正確,所以一般我們會定義一個loss function,可以理解為“錯誤”的程度,錯的越“離譜”,loss就越大。而我們的目標就是調整參數使得loss最小。

但是我們是在“訓練”數據上調整的參數,那么它能在“測試”數據上也表現的好嗎?這個就是模型的“泛化”能力了。就和人在學校學習一樣,有的同學做過的一 模一樣的題就會,但是考試時稍微改變一下就不會了,這就是“泛化”能力太差,學到的不是最本質的東西。所以平時會定期有一些“模擬考試”,來檢驗學生是不 是真的學會了,如果考得不好,那就打回去重新訓練模型調整參數。這在機器學習里對應的就是validation的階段。最后到最終的考試了,就是最終檢驗 的時候了,這個試卷里的題目是不能提前讓人看到的,只能拿出來用一次,否則就是作弊了。對應到機器學習里就是test階段。

當然這里用通俗的話描述了機器學習,主要是有監督的學習。其實機器學習還有無監督的學習和強化學習。前者就是不給答案,只給數據,讓人總結規律;而后者會有答案,但是答案不是現在就告訴你。我個人覺得人類社會里更多的是監督學習和強化學習。從人類社會總體來說,強化學習是獲取新知識的唯一途徑,也就是向自 然學習,我們做了一個決策,其好壞可能要很長一段時間才能顯現出來。而學習出來的這些知識通過監督的方式,通過家庭和學校的教育教給下一代。

另外輸出除了簡單的分為離散和連續,還可以是序列(時序)的,比如自然語言(文本)是一個字符串的序列 ,對于我們的Image Caption Generation就是生成一個單詞序列。另外還有更復雜的輸出,比如parsing,輸出是一棵語法樹。

2.2 多層神經網絡

前面介紹了機器學習的基本概念,接下來我們就來學習一下神經網絡。現在流行的說法“深度學習”,其實大多指的就是“深度神經網絡”,那么首先我們先了解一下“淺度神經網絡”,也就是傳統的神經網絡。這里的內容主要來自http://neuralnetworksanddeeplearning.com的前兩章。

2.2.1 手寫數字識別問題

我們在學習一門新的語言時會寫一個hello world程序,而mnist數據的手寫數字識別就是一個很好的學習機器學習(包括深度學習)的一個hello world任務。

計算機和人類大腦似乎有很大的不同,很多人類認為復雜的工作計算機可能認為很簡單,而人類認為很簡單的事情計算機可能非常難處理。比如數字的計算,記憶,人類的準確度和速度都遠遠不如計算機。但是識別0-9的手寫數字,我們覺得很輕而易舉的事情,讓計算機程序來處理卻異常困難。經過數百萬年進化的人類視覺系統在我們大腦沒有意識到的時候就已經幫我們完成了數字的識別,把那些復雜的視覺處理過程深深的掩藏了起來。但當我們想自己寫一個程序來識別數字的時候,這些困難才能體現出來。首先,對于計算機來說,它“看到”的不是數字,甚至不是筆畫。它“看到”的只是一個二位的矩陣(數組),每個點都是一個數字。比如下圖,我們“看到”的是左邊的“貓”,其實計算機“看到”的是右邊的像素灰度值。當然我們視覺系統的視網膜看到的也是類似的一些“數值”,只不過我們的視覺系統已經處理了這些信息并且把它識別成了“貓”(甚至和語言還做了映射)。

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

mnist樣例數據

如果我們自己來寫一個“算法”識別數字“9”,我們可能會這么定義:9在上面有個圓圈,在這個圓圈的右下部分有一個豎直的筆畫。說起來很簡單,如果用算法 來實現就很麻煩了:什么是圓圈?每個人畫的圓圈都不同,同樣豎直的筆畫怎么識別,圓圈和豎直筆畫連接處怎么尋找,右下是哪?大家如果有興趣可以嘗試一下用 上面的方法,其實最早做數字識別就是這樣的思路。

機器學習的思路則不同,它不需要這么細節的“指示”計算機應該怎么做。而是給計算機足夠的“訓練”樣本,讓它“看”不同的10個數字,然后讓它“學”出 來。前面我們也講了,現在的機器學習一般是一個參數化的模型。比如最簡單的一個線性模型:f(w;x)=w0+ w1*x1+w2*x2。如果我們的輸入有兩個“特征”x1和x2,那么這個模型有3個參數w0,w1和w2,機器學習的過程就是選擇“最優”的參數。對 于上面的mnist數據,輸入就是28*28=784維的向量。

如果用“原始”的輸入作為“特征”,線性的模型很可能學到一些簡單的特征,比如它看到1一般是分布在從上到下居中的一些位置,那么對于這些位置一旦發現有比較大的灰度值,那么就傾向于判斷成1。如果一個像素點2也經常出現,但3不出現,那么它就能學到如果這個像素出現,那么這個數字是2和3的可能性就大一些。

但是這樣的“特征”可能不是“本質”的,因為我寫字的時候筆稍微平移一點,那么你之前“學到”的參數就可能有問題。而更“本質”的特征是什么呢?可能還是像之前我們總結的——9在上面有個圓圈,在這個圓圈的右下部分有一個豎直的筆畫。我們把識別一個數字的問題轉化成圓圈和豎直筆畫的問題。傳統的機器學習需要方法來提取“類似”(但不完全是)基本筆畫這樣的“特征”,這些特征相對于像素的特征會更加“本質”。但是要“提取”這些特征需要很多的“領域”知識,比如圖像處理的技術。所以使用傳統的機器學習方法來解決問題,我們不但需要很多機器學習的知識,而且也需要很多“領域”的知識,同時擁有這兩方面的知識是比較難的。

而“深度學習”最近之所以火熱,其中很重要的一個原因就是對于很多問題,我們只需要輸入最原始的信號,比如圖片的像素值,通過“多層”的網絡,讓底層的網絡學習出“底層”的特征,比如基本的形狀,而中間的層學習出抽象一點的特征,比如眼睛鼻子耳朵。而更上的層次識別出這是一個貓還是一個狗。所有這些都是機器學習出來的,所以基本不需要領域的知識。

上面的圖就說明了這一點,而且我們發現越是底層的特征就越“通用”,不管是貓鼻子還是狗眼睛,可能用到的都是一些基本的形狀,因此我們可以把這些知識(特征)transfer到別的任務,也就是transfer learning,后面我們講到CNN的時候還會提及。

2.2.2 單個神經元和多層神經網絡(MLP)

神經網絡從名字來看是和人類的大腦有些關系的,而且即使到現在,很多有用的東西如CNN和Attention,都有很多借鑒神經科學研究人腦的結果的。不過這里我就不介紹這些東西了,有興趣的讀者可以找一些資料來了解。

一個神經元如下圖的結構:

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

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

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

我們把許多的單個神經元按照層次組織起來就是多層的神經網絡。

比如我們的手寫數字識別,輸入層是784維,就是神經網絡的地一層,然后中間有15個hidden(因為我們不知道它的值)神經元,然后輸出層是10個神經元。中間隱層的每個神經元的輸入都是784個原始像素通過上面的公式加權累加然后用sigmoid激活。而輸出層的每一個神經元也是中間15個神經元的累加然后激活。上面的圖就是一個3層的神經網絡。

輸入一個28*28的圖像,我們得到一個10維的輸出,那么怎么分類呢?最直接的想法就是把認為最大的那個輸出,比如輸出是(10,11,12,13,14,15,16,17,18,19),那么我們認為輸出是9。

當然,更常見的做法是最后一次經過線性累加之后并不用Sigmoid函數激活,而是加一個softmax的函數,讓10個輸出加起來等于1,這樣更像一個 概率。而我們上面的情況,雖然訓練數據的輸出加起來是1,但是實際給一個其它輸入,輸出加起來很可能不是1。不過為了與Nielsen的文章一致,我們還 是先用這種方法。

因此,假設我們有了這些參數【總共是784*15 + 15(w0或者叫bias) + 15*10 + 10】,我們很容易通過上面的公式一個一個的計算出10維的輸出。然后選擇最大的那個作為我們識別的結果。問題的難點就在怎么 選擇這么多參數,然后使得我們分類的錯誤最少。

而我們怎么訓練呢?對于一張圖片,假設它是數字“1”,那么我們期望它的輸出是(0,1,0,0,0,0,0,0,0,0),所以我們可以簡單的用最小平方錯誤作為損失函數。不過你可能會有些疑問,我們關注的指標應該是分類的“正確率”(或者錯誤率),那么我們為什么不直接把分類的錯誤率作為損失函數呢?這樣神經網絡學習出來的參數就是最小化錯誤率。

主要的原因就是錯誤率不是參數的連續函數。因為一個訓練數據如果分類正確那么就是1,否則就是0,這樣就不是一個連續的函數。比如最簡單的兩類線性分類器,f(x)=w0+w1*x1+w2*x2。如果f(x)>0我們分類成類別1;否則我們分類成類別2。如果當前的w0+w1*x1+w2*x2<0,我們很小的調整w0(或者w1,w2),w0+w1*x1+w2*x2仍然小于0,【事實上對于這個例子,只要是w0變小,他們的累加都是小于0的】所以f(x)的值不會變化,而w0一直增大到使累加和等于0之前都不會變化,只有大于0時突然變成1了,然后一直就是1。因此之前的錯誤率都是1,然后就突然是0。所以它不是個連續的函數。

因為我們使用的優化算法一般是(隨機)梯度下降的算法,在每次迭代的時候都是試圖做一個微小的參數調整使得損失變小,但是不連續的函數顯然也不可導,也就沒法用這個算法來優化參數。

因此我們使用了最小平方誤差(MSE)損失函數。

y(x)就是神經網絡的輸出,可能寫成f(x)大家會習慣一點。a是目標的輸出,比如當前分類是數字1,那么我們期望的輸出就是(0,1,0,0,0,0,0,0,0,0)。

首先這個損失函數是參數w的連續函數,因為y(x)就是神經網絡的輸出,每個神經元都是它的輸入的線性加權累加,然后使用sigmoid激活函數【如果使用最早的階躍函數就不連續了,所以后來使用了Sigmoid函數】,然后每一層的神經元都是用上一層的神經元通過這樣的方式計算的(只不過每個神經元的參數也就是權重是不同的數值而已),所以這些連續函數的復合函數也是連續的。

其次這個損失函數和我們的最終優化目標是“大致”一致的。比如C(w,b)趨于0時,它就要求y(x)趨于a,那么我們的分類也就趨于正確。當然可能存在一種極端的情況,比如有3個訓練數據,第一組參數,它分類正確了2個訓練數據,但是錯的那1個錯的很“離譜”,也就是y(x)和a差距極大;而第二組參數,他正確分類了1個訓練數據,但是錯的那兩個都還不算太差。那么這種情況下MSE和正確率并不一致。

2.2.3 隨機梯度下降(Stochastic Gradient Descent)和自動求梯度(Automatic Derivatives)

上面說了,我們有了一個參數化的模型,訓練的過程就是根據訓練數據和loss function,選擇“最優”的參數,使得loss“最小”,這從數學上來講就是一個優化問題。這看起來似乎不是什么值得一提的問題,也許你還記得微積 分里的知識,極值點的各種充分必要條件,比如必要條件是導數是0,然后直接把參數解出來。但在現實生活中的函數遠比教科書里學到的復雜,很多模型都無法用 解析的方式求出最優解。所以現實的方法就是求“數值”解,一般最常見的方法就是迭代的方法,根據現在的參數,我們很小幅度的調整參數,使得loss變小一 點點。然后一步一步的最終能夠達到一個最優解(一般是局部最優解)。那怎么小幅調整呢?像悶頭蒼蠅那樣隨機亂試顯然效率極低。因此我們要朝著一個能使函數 值變小的方向前進。而在一個點能使函數值變小的方向有無窮多個,但有一個方向是下降速度最快的,那就是梯度。因此更常見的方法就是在當前點求函數的梯度, 然后朝著梯度的方向下降。朝梯度的方向走多遠呢?一般走一個比較小的值是比較安全的,這個值就是“步長”。一般剛開始隨機的初始化參數,loss比較大, 所以多走一些也沒關系,但是到了后面,就不能走太快,否則很容易錯過最優的點。

因為loss是所有訓練數據的函數,所以求loss的梯度需要計算所有的訓練數據,對于很多task來說,訓練數據可能上百萬,計算一次代價太大,所以一 般會“隨機”的采樣少部分數據,比如128個數據,求它的梯度。雖然128個點的梯度和一百萬個的是不一樣的,但是從概率來講至少是一致的方向而不會是相 反的方向,所以也能使loss變小。當然這個128是可以調整的,它一般被叫做batch size,最極端的就是batch是1和一百萬,那么分別就是online learning和退化到梯度下降。batch size越大,計算一次梯度的時間就越久【當然由于GPU和各種類似SSE的指令,一次計算128個可能并不比計算1個慢多少】,隨機梯度和真正梯度一致 的概率就越大,走的方向就更“正確”;batch size越小,計算一次的時間就越短,但可能方向偏離最優的方向就更遠,會在不是“冤枉路”。但實際的情況也很難說哪個值是最優的,一般的經驗取值都是幾 十到一兩百的范圍,另外因為計算機都是字節對齊,32,64,128這樣的值也許能稍微加快矩陣運算的速度。但是實際也很多人選擇10,50,100這樣 的值。

除了常見的隨機梯度下降,還有不少改進的方法,如Momentum,Adagrad等等,有興趣的可以看看http://cs231n.github.io/neural-networks-3/#update,里面還有個動畫,比較了不同方法的收斂速度的比較。

通過上面的分析,我們把問題變成了怎么求loss對參數W的梯度。

求梯度有如下4種方法:

手工求解析解

比如 f(x)=x^2, df/dx=2*x。然后我們要求f(x)在x=1.5的值,代進去就2*1.5=3

數值解

使用極限的定義:

機器符號計算

讓機器做符號運算,實現1的方法,但是機器如果優化的不好的話可能會有一些不必要的運算。

比如 x^2 + 2*x*y + y^2,直接對x求導數變成了 2*x + 2*y,兩次乘法一次加分,但是我們可以合并一下變成2*(x+y),一次乘法一次加分。

自動梯度

下面我會在稍微細講一下,所以這里暫時跳過。

這些方法的優缺點:

手工求解“數學”要求高,有可能水平不夠求不對,但效率應該是能最優的。

沒任何函數,甚至沒有解析導數的情況下都能使用,缺點是計算量太大,而且只是近似解【因為極限的定義】,在某些特別不“連續”的地方可能誤差較大。所以實際使用是很少,只是用它來驗證其它方法是否正確。

機器符號計算,前面說的,依賴于這個庫的好壞。

實際的框架,如TensorFlow就是自動梯度,而Theano就是符號梯度。

2.2.4 編程實戰

通過上面的介紹,我們其實就可以實現一個經典的前饋(feed forward)神經網絡了,這種網絡結構很簡單,每一層的輸入是前一層的輸出。輸入層沒有輸入,它就是原始的信號輸入。而且上一層的所有神經元都會連接到下一層的所有神經元,就像我們剛才的例子,輸入是784,中間層是15,那么就有785*15個連接【再加上每個中間節點有一個bias】。所以這種網絡有時候也加做全連接的網絡(full connected),用來和CNN這種不是全連接的網絡有所區別,另外就是信號是從前往后傳遞,沒有反饋,所以也叫前潰神經網絡,這是為了和RNN這種有反饋的區別。

當然,我們還沒有講怎么計算梯度,也就是損失函數相對于每一個參數的偏導數。在下一部分我們會詳細討論介紹,這里我們先把它當成一個黑盒的函數就好了。

代碼

我們這里學習一下Nielsen提供的代碼。代碼非常簡潔,只有不到100行代碼。

https://github.com/mnielsen/neural-networks-and-deep-learning

git clonehttps://github.com/mnielsen/neural-networks-and-deep-learning.git

運行

創建一個 test_network1.py,輸入如下代碼:

import mnist_loaderimport networktraining_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)

保存后直接運行 Python test_network1.py。這里我們讓他進行了30次迭代,最終在測試數據上的準確率大概在95%左右(當然因為隨機初始化參數不同,最終的結果可能有所不同)

Epoch0:8250/10000Epoch1:8371/10000Epoch2:9300/10000......Epoch28:9552/10000Epoch29:9555/10000

3. 代碼閱讀

Python代碼很容易閱讀,即使之前沒有用過,稍微學習兩天也就可以上手,而且大部分機器學習相關的代碼不會用到太復雜的語言特性,基本就是一些數學的線性代數的運算。而Python的numpy這個庫是用的最多的,后面閱讀代碼的時候我會把用到的函數做一些介紹,繼續下面的閱讀之前建議花十分鐘閱讀一下http://cs231n.github.io/python-numpy-tutorial/

3.1 mnist_loader.load_data_wrapper函數

這個函數用來讀取mnist數據,數據是放在data/mnist.pkl.gz。首先這是個gzip的壓縮文件,是Pickle工具序列化到磁盤的格式。不熟悉也沒有關系,反正我們知道這個函數的返回值就行了。

這個函數返回三個對象,分別代表training_data,validation_data和test_data。

training_data是一個50,000的list,然后其中的每一個元素是一個tuple。tuple的第一個元素是一個784維的numpy一維數組。第二個元素是10維的數組,也就是one-hot的表示方法——如果正確的答案是數字0,那么這個10維數組就是(1, 0, 0, …)。

而validation_data是一個10,000的list,每個元素也是一個tuple。tuple的第一個元素也是784維的numpy一維數組。第二個元素是一個0-9的數字,代表正確答案是那個數字。

test_data的格式和validation_data一樣。

為什么training_data要是這樣的格式呢?因為這樣的格式計算loss更方便一些。

3.2 Network類的構造函數

我們在調用net = network.Network([784, 30, 10])時就到了init函數。為了減少篇幅,代碼里的注釋我都去掉了,重要的地方我會根據自己的理解說明,但是有空還是值得閱讀代碼里的注釋。

classNetwork(object):def__init__(self, sizes):self.num_layers = len(sizes)? ? ? ? self.sizes = sizes? ? ? ? self.biases = [np.random.randn(y,1)foryinsizes[1:]]? ? ? ? self.weights = [np.random.randn(y, x)forx, yinzip(sizes[:-1], sizes[1:])]

比如上面的參數,我們保存下來的self.num_layers=3,也就是3層的網絡。每一層的神經元的個數保存到self.sizes里。接下來就是構造biases數組并隨機初始化。因為輸入層是沒有參數的,所以是for y in sizes[1:],我們使用了numpy的random.randn生成正態分布的隨機數用來作為參數的初始值。注意這里生成了2維的隨機變量。回憶一下,如果我們有30個hidden unit,那么bias的個數也是30,那就生成一個30維的1維數組就行了,為什么要是30*1的二維數組呢?其實用1維也可以,不過為了和weights一致,后面代碼方便,就用二維數組了。另外weights也是一樣的初始化方法,不過注意randn(y,x)而不是randn(x,y)。比如對于我們輸入的[784,30,10],weights分別是30*784和10*30的。當然其實weights矩陣轉置一下也可以,就是計算矩陣乘法的時候也需要有一個轉置。不同的文獻可能有不同的記法,但是我們在實現代碼的時候只需要隨時注意矩陣的大小,檢查矩陣乘法滿足乘法的約束就行了,矩陣AB能相乘,必須滿足的條件是B的列數等于A的函數就行。

對于Nielsen的記法,矩陣的每一行就是一個神經元的784個參數,那么weights(30*784) * input(784*1)就得到30個hidden unit的加權累加。

3.3 feedforward函數

給點輸入a(784維),計算最終神經網絡的輸出(10維)。

deffeedforward(self, a):"""Return the output of the network if ``a`` is input."""forb, winzip(self.biases, self.weights):? ? ? ? a = sigmoid(np.dot(w, a)+b)returna

代碼非常簡單,這里用到了np.dot,也就是矩陣向量的乘法,此外這里有一個Sigmoid函數,這個函數的輸入是numpy的ndarray,輸出也是同樣大小的數組,不過對于每個元素都進行了sigmoid的計算。用numpy的術語就是universal function,很多文獻里一般都叫elementwise的function。我覺得后面這個名字更直接。

#### Miscellaneous functionsdef sigmoid(z):"""The sigmoid function."""return1.0/(1.0+np.exp(-z))defsigmoid_prime(z):"""Derivative of the sigmoid function."""returnsigmoid(z)*(1-sigmoid(z))

上面就是Sigmoid函數,另外也把sigmoid_prime,也就是Sigmoid的導數放在了一起【不記得的話看前面Sigmoid的導數的推導】。

3.4 SGD函數

這個函數是訓練的入口,比如我們之前的訓練代碼:

net.SGD(training_data,30,10,3.0, test_data=test_data)defSGD(self, training_data, epochs, mini_batch_size, eta,

test_data=None):iftest_data: n_test = len(test_data)? ? n = len(training_data)forjinxrange(epochs):? ? ? ? random.shuffle(training_data)? ? ? ? mini_batches = [? ? ? ? ? ? training_data[k:k+mini_batch_size]forkinxrange(0, n, mini_batch_size)]formini_batchinmini_batches:? ? ? ? ? ? self.update_mini_batch(mini_batch, eta)iftest_data:print"Epoch {0}: {1} / {2}".format(? ? ? ? ? ? ? ? j, self.evaluate(test_data), n_test)else:print"Epoch {0} complete".format(j)

第一個參數就是training_data。

第二個參數就是epochs,也就是總共對訓練數據迭代多少次,我們這里是30次迭代。

第三個參數是batch大小,我們這里是10,最后一個參數是eta,也就是步長,這里是3.0。除了網絡結構(比如總共多少個hidden layer,每個hidder layer多少個hidden unit),另外一個非常重要的參數就是步長。前面我們也討論過了,步長太小,收斂速度過慢,步長太大,可能不收斂。實際的情況是沒有一個萬能的準則,更多的是根據數據,不停的嘗試合適的步長。如果發現收斂太慢,就適當調大,反之則調小。所以要訓練好一個神經網絡,還是有很多tricky的技巧,包括參數怎么初始化,激活函數怎么選擇,比SGD更好的優化算法等等。

第四個參數test_data是可選的,如果有(我們的例子是穿了進來的),則每次epoch之后都測試一下。

代碼的大致解釋我用注釋的形式嵌在代碼里了:

forjinxrange(epochs):## 一共進行 epochs=30 輪迭代random.shuffle(training_data)## 訓練數據隨機打散mini_batches = [? ? ? ? ? ? training_data[k:k+mini_batch_size]forkinxrange(0, n, mini_batch_size)]## 把50,000個訓練數據分成5,000個batch,每個batch包含10個訓練數據。formini_batchinmini_batches:## 對于每個batchself.update_mini_batch(mini_batch, eta)## 使用梯度下降更新參數iftest_data:## 如果提供了測試數據print"Epoch {0}: {1} / {2}".format(? ? ? ? ? ? ? ? j, self.evaluate(test_data), n_test)## 評價在測試數據上的準確率else:? ? ? ? ? ? print"Epoch {0} complete".format(j)

下面是evaluate函數:

defevaluate(self, test_data):test_results = [(np.argmax(self.feedforward(x)), y)for(x, y)intest_data]returnsum(int(x == y)for(x, y)intest_results)

對于test_data里的每一組(x,y),y是0-9之間的正確答案。而self.feedforward(x)返回的是10維的數組,我們選擇得分最高的那個值作為模型的預測結果np.argmax就是返回最大值的下標。比如x=[0.3, 0.6, 0.1, 0, ….],那么argmax(x) = 1。

因此test_results這個列表的每一個元素是一個tuple,tuple的第一個是模型預測的數字,而第二個是正確答案。

所以最后一行返回的是模型預測正確的個數。

3.5 update_mini_batch函數

defupdate_mini_batch(self, mini_batch, eta):nabla_b = [np.zeros(b.shape)forbinself.biases]? ? nabla_w = [np.zeros(w.shape)forwinself.weights]forx, yinmini_batch:? ? ? ? delta_nabla_b, delta_nabla_w = self.backprop(x, y)? ? ? ? nabla_b = [nb+dnbfornb, dnbinzip(nabla_b, delta_nabla_b)]? ? ? ? nabla_w = [nw+dnwfornw, dnwinzip(nabla_w, delta_nabla_w)]? ? self.weights = [w-(eta/len(mini_batch))*nwforw, nwinzip(self.weights, nabla_w)]? ? self.biases = [b-(eta/len(mini_batch))*nbforb, nbinzip(self.biases, nabla_b)]

它的輸入參數是mini_batch【size=10的tuple(x,y)】和eta【3.0】。

defupdate_mini_batch(self, mini_batch, eta):nabla_b = [np.zeros(b.shape)forbinself.biases]## 回憶一下__init__,biases是一個列表,包含兩個矩陣,分別是30*1和10*1## 我們先構造一個和self.biases一樣大小的列表,用來存放累加的梯度(偏導數)nabla_w = [np.zeros(w.shape)forwinself.weights]## 同上, weights包含兩個矩陣,大小分別是30*784和10*30forx, yinmini_batch:? ? ? ? delta_nabla_b, delta_nabla_w = self.backprop(x, y)## 對于一個訓練數據(x,y)計算loss相對于所有參數的偏導數## 因此delta_nabla_b和self.biases, nabla_b是一樣大小(shape)## 同樣delta_nabla_w和self.weights,nabla_w一樣大小nabla_b = [nb+dnbfornb, dnbinzip(nabla_b, delta_nabla_b)]## 把bias的梯度累加到nabla_b里nabla_w = [nw+dnwfornw, dnwinzip(nabla_w, delta_nabla_w)]## 把weight的梯度累加到nable_w里self.weights = [w-(eta/len(mini_batch))*nwforw, nwinzip(self.weights, nabla_w)]## 使用這個batch的梯度和eta(步長)更新參數weightsself.biases = [b-(eta/len(mini_batch))*nbforb, nbinzip(self.biases, nabla_b)]## 更新biases## 這里更新參數是除了batch的大小(10),有的人實現時不除,其實沒有什么區別,因為超參數eta會有所不同,如果不除,那么eta相當于是0.3(在eta那里就除了batch的大小了)。

3.6 backprop函數

這個函數就是求loss相對于所有參數的偏導數,這里先不仔細講解,等下次我們學習梯度的求解方法我們再回來討論,這里可以先了解一下這個函數的輸入和輸出,把它當成一個黑盒就行,其實它的代碼也很少,但是如果不知道梯度的公式,也很難明白。

def backprop(self,x,y):nabla_b = [np.zeros(b.shape) for binself.biases]? ? nabla_w = [np.zeros(w.shape) for winself.weights]# feedforwardactivation = xactivations = [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 linxrange(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,y)分別是784*1和10*1。輸出就是和self.biases,self.weights一樣大小的列表,然后列表中的每一個數組的大小也是一樣。具體到上面的例子,輸出nabla_b包含兩個矩陣,大小分別是30*1和10*1;nabla_w也包含兩個矩陣,大小分別是30*784和10*30。

未完待續

作者簡介:李理,目前就職于環信,即時通訊云平臺和全媒體智能客服平臺,在環信從事智能客服和智能機器人相關工作,致力于用深度學習來提高智能機器人的性能。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容