青年人的第一個機器學習程序

去年Alaph GO擊敗李世石九段,社會掀起了機器學習技術討論的熱潮,不過很多人對機器學習并不了解,本文借由手寫數字識別問題闡述了機器學習原理,并給出用Python 2.7 編寫的源碼,幫助大家搞清楚什么是機器學習。不過本文介紹的只是機器學習的核心思想,離工程應用還有一定距離。

本文翻譯自Michael Nielsen的著作《Neural Networks and Deep Learning》感興趣的同學可以直接看原版。(翻譯時略作刪減)

以下是原文

人類視覺系統是世界奇跡之一。考慮到以下手寫數字系列:

很多人可以毫不費力地直接識別出這是504192。在 在我們大腦的每個半球,人類有一個主視覺皮層,也稱為V1,包含1.4億個神經元,在眾多神經元之間有數百億的連接。 然而,人類視覺系統不僅僅包含V1,還涉及一系列視覺皮層 :V2,V3,V4和V5 ,這些視覺皮層逐漸進行更復雜的圖像處理。 我們的大腦進過數億年進化而來的超級計算機,進化的結果是我們的大腦可以精準的理解我們身邊的世界。 識別手寫數字并不容易。 然而人類卻非常擅長識別眼睛看到的事物并且,幾乎所有的識別工作都是無意識完成的。 因此,我們通常不會察覺我們的視覺系統解決的問題有多難。

如果嘗試用計算機程序去完成手寫數字識別問題是非常困難的。舉個簡單的例子——識別數字“9”,我們人腦會意識到在9的頂部有一個循環,在右下方有一個垂直的筆畫,然而用計算機語言卻不好實現這樣的分析邏輯。當你試圖枚舉這些規則,使之精確的時候,你很快就會迷失在一個無數例外和邊界條件的混亂之中。

那么神經網絡會怎么做呢?神經網絡會用不同的方式處理該問題——使用大量標識好的手寫數字作為訓練集,然后開發一個可以從這些訓練數據中學習而來的系統。換句話說,神經網絡使用訓練數據自己推斷識別手寫數字的規則。此外,通過增加訓練示例的數量,網絡可以更多地了解手寫,從而提高其準確性。 因此,雖然我只展示了100個訓練數字,也許我們可以通過使用數千甚至數百萬或數十億的訓練樣本來構建一個更好的手寫識別器。

在本文中,我們將編寫一個計算機程序,實現一個學習識別手寫數字的神經網絡。 該程序只有74行,并且不使用特殊的神經網絡庫。 但這個短程序可以識別數字的精度超過96%,無需人為干預。

為了更好的理解神經網絡的原理,在介紹手寫數字識別神經網絡之前,我會介紹兩種重要的人工神經元類型(the perceptron 和 the sigmoid neuron),以及用于神經網絡學習標準學習算法——隨機梯度差分(stochastic gradient descent)

Perceptron

為了解釋什么是神經網絡,首先介紹第一類人工神經元:Perceptron。感知器需要幾個二進制輸入,x1,x2,...并產生一個二進制輸出:

在上圖中,該Perceptron有三個輸入:x1,x2,x3。一般來說,它可以有更多或更少的輸入。Perceptron的輸出(0或者1)由各輸入的加權和是否小于或者大于每個閾值來確定。閾值和權值一樣是一個Perceptron的參數。

式中,xj代表輸入變量,wj表示相應輸入變量的權值。

這是基本的數學模型。 你可以想象Perceptron是一個通過權衡輸入做出決策的裝置。 讓我舉個例子。 這不是一個很現實的例子,但它很容易理解,假設周末即將到來,你已經聽說你的城市將會有一個奶酪節。 你喜歡奶酪,并試圖決定是否去參加節日。 你可以通過權衡三個因素來做出決定:

  1. 當天的天氣是否好?
  2. 你的男朋友或者女朋友是否想陪同你?
  3. 因為你沒有車,你想知道奶酪節附近是否有公交站?

我們可以通過相應的二進制變量x1,x2和x3表示這三個因子。 例如,如果天氣好,x1 = 1,如果天氣不好,x1 = 0。 類似地,如果你的男朋友或女朋友想要去,x2 = 1,如果不是,x2 = 0。 相同的規則也適用于變量x3和是否有公交站。

假設你非常喜歡奶酪,所以你很想去參加奶酪節,不管你的男女朋友是否感興趣或者是否難道到達奶酪節。但是壞天氣會讓奶酪節掃興,所以你不想在壞天氣的時候參加奶酪節。這時你可以用Perceptron模擬這種決策行為。一種方法是為天氣選擇權重w1 = 6,對于其他條件,分別為w2 = 2,w3 = 2。 w1的值越大,表示天氣對你來說很重要,遠遠超過你的男朋友或女朋友是否陪同你一起去,以及附近是否有公交站。 最后,假設您為Perceptron選擇閾值5。 Perceptron通過計算輸入和權值乘機和并同閾值進行比較,每當天氣好時輸出1,而每當天氣壞時輸出0。 這個Perceptron的輸出同我們的假設相符,即天氣壞的時候不過男女朋友是否陪同或者是否有公交站,都不會去奶酪節。

通過改變權重和閾值,我們可以得到不同的決策模型。 例如,假設我們選擇了一個閾值3.然后Perceptron會決定你應該在天氣好的時候或者同時男女朋友同意陪同以及附近有公交站同時成立的時候前往奶酪節。 換句話說,這將是一個不同的決策模式。 降低門檻意味著你更愿意去參加奶酪節。

以上介紹的Perceptron顯然并不是人類決策的完整模型。但是這個例子說明了Perceptron如果權衡不同類型的輸入最終做出決定。并且說明一個復雜的Perceptron網絡可以做出相當微妙的決定:

在上圖的網絡中,我們稱第一列Perceptron為第一層Perceptron,第一列Perceptron通過權值同輸入相乘做了三個簡單的決定。第二層中的Perceptron通過權衡來自第一層決策的結果來做出決定。以這種方式第二層中Perceptron可以在比第一層中Perceptron更復雜更抽象的水平上做出決定。并且甚至可以通過第三層中的Perceptron來做出更復雜的決定。以這種方式,Perceptron的多層網絡可以參與復雜的決策。

我們可以用一種更簡單的方式表示式1,首先我們可以把權值同輸入的乘機和用點乘的方式表示,并且可以把閾值移到等式的左邊這樣就可以寫成下面的樣子。使用b表示閾值的負數,同時我們稱b為偏差(bias)。

你可以認為偏差為衡量激活Perceptron難易程度的標準。對于一個擁有很大偏差值的Perceptron,它是很容易激活的,對于一個擁有絕對值很大的負偏差值來說,激活Perceptron變成一個艱難的任務。

同時,在本文以下的內容中,我們會將神經網絡的輸入看做為一個沒有輸入只有輸出的特殊Perceptron。

Sigmoid Neurons

學習算法聽起來很棒。 但是我們如何能為神經網絡設計這樣的算法呢? 假設我們有一個Perceptron網絡,我們想用它來學習解決一些問題。 例如,網絡的輸入可以是手寫數字掃描得來的數字圖像。 我們希望網絡學習權值和偏差,以便神經網絡可以輸出對手寫數字正確的分類。 那么學習行為到底是怎樣進行的呢,假設我們在網絡中的一些權值(或偏差)做一個小的改變,這些微小的改動會導致網絡的輸出產生一個微小的相應變化。 正如我們將看到的,這個屬性將使學習成為可能。如下圖所示:

如果在權值或者偏差上的微小改動會告知網絡輸出產生微小改動成立的話,我們就可以運用這個事實來修改權值和偏差,讓我們的網絡以我們想要的方式變得更好。

問題是,以上的描述不會發生在Perceptron網絡中,因為權值或者偏差的微小改動會導致單個Perceptron輸出的翻轉(例如從0到1或者從1到0)。這樣的翻轉導致網絡中其他Perceptron的行為變得不可控。所以,雖然你的“9”現在可以正確分類,但是某個微小變動導致對其他數字的分類行為變得不可控制。這使得不可能通過逐漸修改權重和偏差最終使網絡輸出朝著我們期望的方式變化。

不過我們可以通過以下方式解決這個問題——使用另一種人工神經元Sigmoid Neurons。Sigmoid Neurons類似于Perceptron,但是它的權重和偏差出現微小變動的時候,神經元輸出的變動也是微小的。這是使用Sigmoid Neurons的神經網絡可以實現學習行為的關鍵。

一個Sigmoid Neurons可以用下圖表示,同Perceptron并沒有什么不同。

像Perceptron一樣,Sigmoid Neurons同樣接受輸入,但是這些輸入值不再是離散的0或者1,輸入值可以使用0到1之間的任何值,比如說0.638。同Perceptron一樣,Sigmoid Neurons每個輸入都有相應的權值,w1,w2,以及偏差b。但是Sigmoid Neurons的輸出不再是0或者1這樣的離散值了,它變成了σ(w?x+ b),σ的定義如下:

如果我們把神經元的輸入寫進上面的表達式:

其實上面的表達式并不難理解,如果σ函數的變量是一個極大的值,那么σ函數趨向于1,相反的話,σ函數趨向于0。σ函數的曲線如下圖所示。

而代表Perceptron的階梯函數如下圖所示。

實際上,如果σ函數的形式如果是階梯函數的話,那么Sigmoid Neurons就是一個Perceptron——輸出是1還是0取決于輸入w?x+b的正負。通過使用實際的σ函數,我們可以得到平滑的決策函數曲線。事實上,σ函數的平滑性是關鍵。σ函數的平滑性決定了權值的微分Δwj和偏差的微分將在神經元輸出中產生小變化Δoutput。通過微積分我們可以得到Δoutput可近似于下式:

上式告訴我們,Sigmoid Neurons的輸出變化Δoutput是權值變化Δwj和偏差變化Δb的線性函數,這種線性使得容易選擇權值和偏差中的小變化以實現輸出中期望的小變化。雖然Sigmoid Neurons具有許多Perceptron相同的定性行為,但是它更容易找到如何通過改變權值偏差從而改變神經網絡輸出的方法。

神經網絡的架構

為了實現識別手寫數字的任務,在了解識別手寫數字神經網絡實現原理之前,我們先介紹幾個概念。

如前所述,該網絡中最左邊的神經元層稱為輸入層,輸入層內的神經元稱為輸入神經元。 最右邊或者叫輸出層包含輸出神經元,還可以叫做單個輸出神經元。 中間層被稱為隱藏層,因為該層中的神經元既不是輸入也不是輸出。 上面的網絡只有一個隱藏層,但一些網絡有多個隱藏層。 例如,以下四層網絡有兩個隱藏層:

網絡中的輸入和輸出層的設計通常是比較簡單的。 例如,假設我們試圖識別手寫數字圖像是否為“9”。 設計網絡的自然方式是將圖像像素的強度編碼到輸入神經元中。 如果圖像是64乘64灰度圖像,則我們將具有4,096 = 64×64個輸入神經元,其強度在0和1之間適當地縮放。輸出層將僅包含單個神經元,當輸出值小于0.5時表示該圖像不是9,如果輸出值大于0.5時,則表示該圖像為9。

雖然神經網絡的輸入和輸出層的設計通常是直接的,但是隱藏層的設計則非常具有技術性。特別的,目前隱藏層的設計并沒有一些可以遵循的基本原則。 然而,神經網絡研究人員已經為隱藏層開發了許多設計啟發式,這有助于人們從他們的網絡中獲得他們想要的行為。 例如,這種啟發法可以用于幫助確定如何根據訓練網絡所需的時間折衷隱藏層的數量。

如果設計一個可以識別手寫數字的簡單神經網絡

我們已經定義了神經網絡,讓我們回到手寫識別問題。 我們可以把識別手寫數字的問題分成兩個子問題。 首先,我們想要一種將包含許多數字的圖像分割成單獨圖像序列的方法,每個圖像包含一個數字。 例如,我們想將下面的圖像分割成6個獨立的圖像。

我們人類可以輕松解決這個問題,但是這對計算機程序來說就是挑戰性的。一旦我們成功的分割出單獨的數字,我們的程序就可以來對每個單獨數字進行分類了。例如,我們的程序就可以識別出下面是個5。

我們將專注于編寫一個程序來解決第二個問題,即解決獨立手寫數字圖像的識別問題。因為一旦解決了獨立手寫數字圖像識別問題,數字圖像分割問題就不難解決了。一種方法是可以用多種圖像分割方法去分割圖像,然后用手寫數字圖像識別算法對每個分割出來的圖像打分,如果某個分割出來的圖像得分比較高,說明分割出來的圖像是正確的,如果得分很低,則說明分割不正確。這個方法以及不同的分割算法的選擇,可以相當好的解決分割問題。這里我們將專注于開發一個神經網絡。

我們使用一個三層神經網絡來解決數字識別問題。第一層神經元輸入手寫數字圖像每個像素的編碼。由于我們使用的訓練集中手寫數字圖像是28*28像素圖片,所以我們的輸入層包含728=28*28個神經元。為了簡單起見,我省略了下面圖像中大部分輸入神經元。輸入神經元輸入的值是圖像的灰度值,0.0表示白色,1.0表示黑色,在兩個值中間的值表示灰色陰影。

網絡的第二層是隱藏層,我們使用n表示隱藏層中神經元的數量,在該神經網絡中,我們使用15個隱藏層神經元。

網絡的輸出層包含10個神經元,如果第一個神經元被激發,則它的輸出output≈1,則表示神經網絡認為輸入的數字為0。如果第二個輸出神經元被激發,則我們認為神經網絡的輸入為1,以此類推。

你可能想知道為什么我們使用10個輸出神經元。畢竟,神經網絡的目標是告訴我們輸入是什么數字,一個看似自然的方法是使用4個輸出神經元,將每個神經元視為一個二進制位,二進制位取值取決于輸出值是靠近0還是靠近1。四個神經元已經足以編碼10答案。因為2的四次方為16,而我們分類的結果個數只有10個。然而最終證明,對于這個特定問題,具有10個輸出神經元的神經網絡比具有4個輸出神經元的神經網絡可以更好的書別數字。但是這也只是對于這個特定問題,并不是說10個輸出神經元的結果要好于4個輸出神經元。

現在讓我們來討論下,神經網絡為什么可以工作。首先考慮使用10個輸出神經元的情況。其實每個輸出神經元得到的結論是加權隱藏層輸出的結果。我們可以這樣認為:隱藏層中第一個神經元檢測是否存在如下圖像:

它可以通過對與圖像重疊的輸入像素進行大量加權,并且只對其他輸入進行輕微加權來做到這一點。 以類似的方式,讓我們假設為了論證的目的,隱藏層中的第二,第三和第四神經元檢測是否存在以下圖像:

正如你可能已經猜到的,這四個圖像一起組成我們在前面顯示的數字行中看到的0圖像:

所以如果四個隱藏神經網絡都激活了,則說明網絡輸入的數字是0。現在,以上所說都是一些啟發式,沒有說三層神經網絡必須以我所描述隱藏的神經元檢測簡單的組件形狀這種形式運行。不過以上的描述可以幫助我們理解一個神經網絡到底是怎么工作的。

梯度差分法

現在我們設計了一個解決手寫數字識別問題的神經網絡,那么它怎么學會識別數字的呢?首先我們需要一個訓練數據集。我們將使用MNIST數據集,它包含數萬個手寫數字掃描圖像以及其正確的分類。下面是MNIST數據集數據的圖片:

正如你所見到的,這正是上文中我們展示過的數字圖片。當我們用訓練集數據放到神經網絡里面,神經網絡經過學習后,我們還會用訓練集的數據去檢測神經網絡的識別準確度。

MNIST數據分為兩部分。 第一部分包含60,000張圖像用作訓練數據。 這些圖像是掃描250人的手寫樣本,其中一半是美國人口普查局的員工,其中一半是高中生。 圖像是灰度的,尺寸為28×28像素。 MNIST數據集的第二部分是10,000個要用作測試數據的圖像。 同樣,這些是28乘28的灰度圖像。 我們將使用測試數據來評估我們的神經網絡學習識別數字的程度。

我們將使用符號x來表示訓練輸入。 我們將將每個訓練輸入x視為28×28 = 784維向量。 向量中的每個元素表示圖像中單個像素的灰度值。 我們將通過y = y(x)來表示相應的期望輸出,其中y是10維向量。 例如,如果特定訓練圖像x描繪6,則y(x)=(0,0,0,0,0,0,1,0,0,0)T是來自網絡的期望輸出 。 注意,這里的T是轉置操作,將行向量轉換為普通(列)向量。

經過訓練后,我們期望神經網絡找到一個算法,這個算法里面有合適的權值和偏差,使得神經網絡輸出近似于訓練數據集相應的輸入。為了量化這一目標,我們定義了一個目標函數(cast function):

我們稱上式中函數C為二次成本函數(quadratic cost function),有時候我們稱之為均方誤差。上式中,w表示網絡中所有權重的集合,b是所有偏差,n是訓練輸入的總數,a是當輸入為x時,輸入x所對應的真實輸出值。最后,求和計算是對所以的輸入x進行的求和計算。符號∥v∥是表示向量v長度的函數。仔細分析上式可以發現,目標函數C(w,b)是非負的,因為等式右邊所加的每一項都是非負的。此外,當 y(x)正好近似等于所有訓練輸入x的輸出a時,目標函數C(w,b)趨向于0。這證明神經網絡訓練的結果很好。如果相反的,對于每一項輸入x,它的y(x)和真實輸出a差距很多,那么目標函數C(w,b)將會變得非常大。這說明神經網絡訓練的不好。因此,我們訓練神經網絡就是為了使目標函數C(w,b)最小化。換句話說,我們想要找到一組權重和偏差,這使得成本盡可能小。我們將使用稱為梯度差分的算法。

我們為什么要引入目標函數的概念呢? 畢竟,我們不是主要對由網絡是否能過正確分類的手寫數字圖像感興趣嗎? 為什么不嘗試直接最大化識別手寫數字圖像正確的數量,而是要最小化目標函數呢? 其問題是正確分類的圖像的數量不是網絡中的權重和偏差的平滑函數。 在大多數情況下,對權重和偏差進行小的改變不會對正確分類的訓練圖像的數量造成任何改變。 這使得很難找出改變權重和偏差的方法來改進神經網絡的性能。 如果我們使用像目標函數這樣的平滑函數,那么很容易找出權重和偏差改進的方法,從而進一步改進神經網絡的分類效果。 這就是為什么我們首先專注于最小化目標函數。

即使我們想使用平滑的目標函數,你可能仍然想知道為什么我們選擇方程(6)中的形式。方程(6)不是一個特別的選擇嗎? 也許如果我們選擇不同的目標函數,我們會得到一個完全不同的使得目標函數最小化的權重和偏差集合? 這是一個很好的問題,以后我們會介紹其他目標函數形式,不過對于這個問題,方程(6)中的形式是合適的。

那好,讓我們假設我們試圖最小化一個函數C(v)。這可以是很多變量的任何實值函數,v = v1,v2,...。注意,我已經用v替換了w和b符號,以強調這可以是任何函數 。并且,假設C作為只有兩個變量v1和v2的函數有助于我們解決C(v)最小化問題。

我們的目的是找到C(v)的最小值,如果C(v)只有兩個變量v1和v2,則我們可以畫出C(v)的函數圖像,如上圖所示,在上圖中我們很容易找到該函數的最小值,因為很容看出來:)。

那么我們如何從數學上找到解決問題的方法呢?首先第一種方法是微積分,我們可以計算C(v)的導數,然后嘗試用使用它們找到極值點。當C(v)只有一個或者兩個變量的時候,這個問題是容易解決的,但是一旦隨著變量數的增加,求解函數最小值將變成一個噩夢。對于我們的神經網絡,我們通常需要數十億個權值和偏差,所以使用微積分沒有辦法解決這個問題。

那么第二種方法的思想就很奇妙了。我們可以把C(v)函數圖像比作一個山谷,我們想象一個球從山谷的坡上滾下來。我們日常經驗告訴我們,球最終會滾到谷底。我們可以使用這個想法作為我們找到函數最小值的方法。我們隨機選擇一個球的起點,然后模擬球向下滾動到山谷底部的運動。

為了使這個問題更精確,讓我們考慮當我們將球在v1方向上移動一段距離Δv1和在v2方向上移動一段距離Δv2時會發生什么。 微積分告訴我們C的變化如下:

然后,為了讓小球滾落到山谷,我們必須讓 ΔC一直保持負值。為了找出讓ΔC一直保持負值的方法,我們做如下定義:

其中Δv表示變量v1和v2的微小變化,而?C表示C(v)函數的梯度向量。

這樣我們就可以將函數C的變化寫成Δv和?C的形式,我們將方程(7)重寫為:

這個方程有助于解釋為什么?C被稱為梯度向量:?C將v中的變化與C中的變化相關聯,正如我們期望的一種稱為梯度的東西。 但是這個方程真正令人興奮的是,它讓我們知道如何選擇Δv使ΔC為負。 特別是,假設我們選擇

其中η是小的正參數(稱為學習速率)。然后公式(9)告訴我們:

因為∥?C||的平方大于等于0,所以保證了C將總是減小,從不增加。而這正是我們所需要的。因此,我們將使用等式(10)來定義我們的梯度下降算法中球的“運動定律”。 也就是說,我們將使用等式(10)計算Δv的值,然后將球的位置v移動該量:

總之,梯度差分算法的工作方式就是重復計算梯度?C,然后在相反的方向上移動,從而保持一直向山谷滾下去。如下圖所示:

以上,我解釋了兩個變量的C函數如何求得最小值,但是事實上C函數通常有很多參數。不過我們只要做一些小的修改就可以運用以上說明的方法了。

并且我們保證用于ΔC的(近似)表達式(12)將是負的。 這使我們能夠通過重復應用更新規則,使梯度達到最小,即使C是許多變量的函數

現在我們來看一下,梯度差分法如何應用在神經網絡學習中。其實是使用梯度差分算法不斷去找到使得方程(6)中目標函數最小化的權值wk和偏差bl:

通過重復應用此更新規則,我們可以“下山”,并找到目標函數的最小值。 換句話說,這是可以用于在神經網絡中學習的規則。

在實踐中,為了計算梯度?C,我們需要分別為每個訓練輸入x計算梯度?Cx,然后對它們求平均。 不幸的是,當訓練輸入的數量非常大時,這可能需要很長時間,并且因此學習過程非常的緩慢。所以在實際應用中,我們通過隨機選擇小訓練樣本來估計梯度?C。通過對這個小樣本進行平均,我們可以快速得到真實梯度?C的估計。這有助于加速學習過程。

識別手寫數字算法的實現

現在讓我們來寫一個程序來實現識別手寫數字功能。我們將使用到上文中介紹的梯度差分算法和MNIST訓練集。我們將使用一個74行的簡短python 2.7程序。首先我們需要獲取MNIST數據,如果你是一個git用戶,你可以通過復制本文代碼來獲取數據:

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

如果你不使用git,你可以從這里下載到數據和代碼。

在本文中,我已經介紹過MNIST數據,我說它被分為60,000個訓練圖像和10,000個測試圖像。在實際應用中,我們將60,000個訓練圖像分為兩部分,一組50,000個圖像用于訓練我們的神經網絡,另一組10,000圖像用于驗證神經網絡的效果。

除了用到MNIST數據集,我們還用到一個叫做Numpy的python庫,你可以從這里下載。

讓我來解釋一下神經網絡代碼的核心特性,下面給出完整的列表。代碼的核心是一個網絡類,我們用它來表示一個神經網絡。下面是我們用來初始化的代碼:

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:])]

list size 包含相應層中神經元的數量,例如,如果我們要創建一個Network對象,在第一層中有2個神經元,在第二層中有3個神經元,最后一層有1個神經元,我們將使用代碼:

net = Network([2, 3, 1])

Network對象中的biases和weights都是隨機初始化的。使用Numpy中np.random.randn函數生成平均值為0標準差為1的高斯分布。

還要注意的是,權值和偏差被儲存為Numpy矩陣的列表。因此,net.weights [1]儲存的是鏈接第二層和第三層神經元的權值的Numpy矩陣。(這不是第一和第二層,因為Python的列表索引從0開始。)其中,wjk是第二層中的第k個神經元和第三層中的第j個神經元之間的連接的權重。 j和k索引的這種排序可能看起來很奇怪 - 確切地說,交換j和k索引有意義嗎?其實,這樣做的目的是運算單個神經元結果的方便,單個神經元結果我們就可以寫成:

這個公式我們分開來看,a是第二層神經元激活狀態的向量,為了獲取a′ ,我們將a乘以權值矩陣w,并且同偏差的向量b相加。然后我們對向量wa + b中的每個條目元素應用函數σ。 (這被稱為矢量化函數σ)。很容易驗證等式(22)給出與我們早先的規則(等式(4))相同的結果,用于計算Sigmoid Neurons的輸出。

這樣我們就可以很簡單的寫出計算輸出的函數:

def sigmoid(z):
    return 1.0/(1.0+np.exp(-z))

請注意,當輸入z是一個向量或Numpy數組時,Numpy自動以元素方式應用函數Sigmoid,即以向量化形式。

然后,我們向Network類添加一個feedforward方法,給定網絡的輸入a,返回相應的輸出。

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

當然,我們希望我們的網絡對象做的主要事情是學習。 為此,我們給他們一個實現隨機梯度下降的SGD方法。

def SGD(self, training_data, epochs, mini_batch_size, eta,test_data=None):

    """Train the neural network using mini-batch stochastic
    gradient descent.  The "training_data" is a list of tuples
    "(x, y)" representing the training inputs and the desired
    outputs.  The other non-optional parameters are
    self-explanatory.  If "test_data" is provided then the
    network will be evaluated against the test data after each
    epoch, and partial progress printed out.  This is useful for
    tracking progress, but slows things down substantially."""

    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)

training_data是表示訓練輸入和對應的期望輸出的元組(x,y)的列表。變量epochs和mini_batch_size是你期望的 - 要訓練的時期的數量,以及在抽樣時使用的迷你批量的大小。 eta是學習率η。如果提供了可選參數test_data,則程序將在每個訓練時期之后評估網絡,并打印出部分進度。這對于跟蹤進度很有用,但會大大降低效率。

代碼工作如下。在每個迭代期,它開始隨機對訓練數據洗牌,然后將其分割成適當大小的小批量訓練數據。然后對于每個mini_batch,我們應用梯度差分的單步。這通過代碼self.update_mini_batch(mini_batch,eta)來完成,它使用mini_batch中的訓練數據根據梯度差分的單次迭代更新網絡權值和偏差。以下是update_mini_batch方法的代碼:

       def update_mini_batch(self, mini_batch, eta):
        """Update the network's weights and biases by applying
        gradient descent using backpropagation to a single mini batch.
        The "mini_batch" is a list of tuples "(x, y)", and "eta"
        is the learning rate."""
        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:
            delta_nabla_b, delta_nabla_w = self.backprop(x, y)
            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)]
        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)]

絕大部分工作是通過下面這行代碼執行的

delta_nabla_b, delta_nabla_w = self.backprop(x, y)

下面是完整的代碼:

        """
         network.py
         ~~~~~~~~~~

      A module to implement the stochastic gradient descent learning
      algorithm for a feedforward neural network.  Gradients are calculated
      using backpropagation.  Note that I have focused on making the code
      simple, easily readable, and easily modifiable.  It is not optimized,
      and omits many desirable features.
      """

      #### Libraries
     # Standard library
     import random

     # Third-party libraries
      import numpy as np

     class Network(object):

    def __init__(self, sizes):
        """The list ``sizes`` contains the number of neurons in the
        respective layers of the network.  For example, if the list
        was [2, 3, 1] then it would be a three-layer network, with the
        first layer containing 2 neurons, the second layer 3 neurons,
        and the third layer 1 neuron.  The biases and weights for the
        network are initialized randomly, using a Gaussian
        distribution with mean 0, and variance 1.  Note that the first
        layer is assumed to be an input layer, and by convention we
        won't set any biases for those neurons, since biases are only
        ever used in computing the outputs from later layers."""
        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:])]

    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

    def SGD(self, training_data, epochs, mini_batch_size, eta,
            test_data=None):
        """Train the neural network using mini-batch stochastic
        gradient descent.  The ``training_data`` is a list of tuples
        ``(x, y)`` representing the training inputs and the desired
        outputs.  The other non-optional parameters are
        self-explanatory.  If ``test_data`` is provided then the
        network will be evaluated against the test data after each
        epoch, and partial progress printed out.  This is useful for
        tracking progress, but slows things down substantially."""
        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)

    def update_mini_batch(self, mini_batch, eta):
        """Update the network's weights and biases by applying
        gradient descent using backpropagation to a single mini batch.
        The ``mini_batch`` is a list of tuples ``(x, y)``, and ``eta``
        is the learning rate."""
        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:
            delta_nabla_b, delta_nabla_w = self.backprop(x, y)
            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)]
        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):
        """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 = np.dot(w, activation)+b
            zs.append(z)
            activation = 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())
        # Note that the variable l in the loop below is used a little
        # differently to the notation in Chapter 2 of the book.  Here,
        # l = 1 means the last layer of neurons, l = 2 is the
        # second-last layer, and so on.  It's a renumbering of the
        # scheme in the book, used here to take advantage of the fact
        # that Python can use negative indices in lists.
        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)

    def evaluate(self, test_data):
        """Return the number of test inputs for which the neural
        network outputs the correct result. Note that the neural
        network's output is assumed to be the index of whichever
        neuron in the final layer has the highest activation."""
        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):
        """Return the vector of partial derivatives \partial C_x /
        \partial a for the output activations."""
        return (output_activations-y)

     #### Miscellaneous functions
     def 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))

那么如何讓這個程序識別手寫數字圖像呢?首先我們載入MNIST數據。這一步驟我們使用mnist_loader.py來完成。

     >>> import mnist_loader
     >>> training_data, validation_data, test_data = \
     ... mnist_loader.load_data_wrapper()

然后我們建立一個有30個隱藏神經元的網絡。

     >>> import network
     >>> net = network.Network([784, 30, 10])

最后,我們將使用隨機梯度下降學習從MNIST訓練數據超過30個迭代,mini-batch大小為10,學習率η= 3.0

     >>> net.SGD(training_data, 30, 10, 3.0, test_data=test_data)

最后我們將訓練出一個可以識別手寫數字圖像的神經網絡。

    Epoch 0: 9129 / 10000
    Epoch 1: 9295 / 10000
    Epoch 2: 9348 / 10000
    ...
    Epoch 27: 9528 / 10000
    Epoch 28: 9542 / 10000
    Epoch 29: 9534 / 10000
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,048評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,414評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,169評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,722評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,465評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,823評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,813評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,000評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,554評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,295評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,513評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,722評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,125評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,430評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,237評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,482評論 2 379

推薦閱讀更多精彩內容

  • 一張圖告訴你,投資人眼中的商業計劃書該怎么寫? 一份商業計劃在描述自己的企業時應當做到追本溯源、無微不至。它應當闡...
    翻滾吧海闊天空閱讀 472評論 0 0
  • 絲絲寒意山上來, 夜半裹被正愁眠。 正是春寒料峭時, 雪花更勝梨花開。 (3月24日)
    丶杯酒慰風塵閱讀 312評論 0 0
  • 電影《博物館奇妙夜3》中有一個生活在紐約自然博物館的原始人,他面目猙獰,眼神迷茫,聽不懂人類的話語,只能簡單機械的...
    西莉亞閱讀 304評論 0 0