TensorFlow官方教程《Neural Networks and Deep Learning》譯(第一章)

– 更新中

譯自:Neural Networks and Deep Learning

成果預展示

如果你能堅持閱讀完本章, 你可以獲得如下的成果:
這裏寫圖片描述

上圖中的命令行窗口輸出爲:

Epoch 0: 9095 / 10000
Epoch 1: 9213 / 10000
Epoch 2: 9299 / 10000
Epoch 3: 9358 / 10000
...
Epoch 27: 9535 / 10000
Epoch 28: 9515/ 10000
Epoch 29: 9536 / 10000
  • 其中每一個 epoch 表示一個神經網絡的訓練小週期。 後面的數字表示從 10000 個測試圖片,該神經網絡成功識別了多少個手寫數字的圖片。

  • 令人興奮的是, 上面的這個 pyhon 程序並沒有使用任何特殊的神經網絡庫, 且它只有短短 74 行代碼(除去註釋和空格排版)。 但是這個簡短的程序可以在沒有人工干預的情況下, 自行從訓練數據中學習, 以 96% 的準確度識別手寫數字。 而且, 在後面的章節中, 我們會將這個程序的識別準確度改進到 99%

這意味通過閱讀本文和理解上面的程序, 你能真正理解神經網絡及深度學習背後的原理, 相信我, 這絕對是一個通俗易懂的教程!

前言

Neural Networks and Deep Learning 是一本免費的網絡書籍,它在 Tensor Flow Playground 的首頁中被推薦爲學習神經網絡和深度學習的入門級教材。 個人閱讀後發現該教程極其清晰易懂。 故特作翻譯, 以作知識分享和自我激勵。 翻譯中可能會加入一些個人的理解和略去一些個人認爲無關緊要的修飾性內容。

該教程主要內容爲

  • 神經網絡, 一種優美的受生物學啓發而設計出的編程模式,使得計算機可以從觀測到的數據中學習
  • 深度學習, 一套利用神經網絡進行學習的強大技術集合。

神經網絡和深度學習目前爲圖像識別, 語音識別, 自然語言處理方面的很多問題提供了最佳的解決方案。 這本書會教你很多神經網絡和深度學習背後的核心概念。

第一章 使用神經網絡識別手寫數字

人類的視覺系統是世界上的衆多奇蹟之一。 考慮如下的手寫數字序列。

這裏寫圖片描述

大部分人可以輕而易舉的識別出圖片中的數字序列爲 504192 。但是這個任務的複雜度顯然被這個“輕而易舉” 所掩蓋。 在我們每一個腦半球, 都有一個初級視覺皮層, 被稱爲 V1, 包含了 1億4千萬神經元, 以及神經元之間數以十億的連接。 但是, 人類的視覺系統不僅涉及到 V1, 還有一系列的視覺皮層 V2, V3,V4 和 V5 , 漸進式地負責更加複雜的圖像處理。 我們可以把我們的大腦當做一個超級計算機, 經過了上成百上千萬年的演化, 完成了對視覺世界的理解。

識別視覺圖像的難度當你在嘗試寫一個程序來識別上述圖片中的數字時顯現出來, 你會發現之前顯而易見的直覺變得難以描述:“ 數字 9 在上方有一個圈, 在右下方有一個豎直的一筆, 可以彎曲也可以不彎曲 ”, 這在算法上是很難表達的。 如果你嘗試更加精確地描述這個規則, 你會很快迷失在大量混亂的異常和特例中。 這種方法看上去毫無希望。

這裏寫圖片描述

神經網絡以一種完全不同的方式來解決這個問題, 其思想是, 獲取大量的手寫數字的樣例, 被稱作訓練樣例, 然後開發出一個可以從訓練樣例中學習的系統。 換句話說,神經網絡使用樣例來自動推理識別手寫數字的規則。 進而, 通過增加訓練的樣例, 神經網絡可以從手寫數字中學習更多, 提高識別的準確度。 因此儘管我們在上圖中只展示了 100 個數字, 但我們可以利用更多的數據樣例構建一個更好的的手寫數字識別系統。

在本章節中, 我們會寫一個程序來實現一個能識別手寫數字的神經網絡。 這個程序只有 73 行, 沒有使用任何特殊的神經網絡庫。 但是這個簡短的程序可以在沒有人工干預的情況下以 96% 的準確度識別手寫數字。 而且, 在後面的章節中, 我們會將這個程序的識別準確度改進到 99%。 事實上, 最好的商用神經網絡已經很成熟, 被商業銀行用來處理支票, 被郵局用來識別地址。

我們專注於手寫圖像的識別, 是因爲這是一個用於學習神經網絡非常傑出的原型問題。 作爲原型問題, 它有如下幾個特點:

  • 具有挑戰性。 想寫出識別手寫數字的程序並不是小菜一碟
  • 沒有過度困難。 並不需要非常複雜的解決方案, 或者大量的算力。
  • 容易在此基礎上衍生出更加高級的技術, 例如深度學習。

在這正本書中, 我們會反覆地來解決手寫圖像識別的問題。 在本教程的後面部分, 我們會討論這些思想如何被應用到計算機視覺, 語音識別, 自然語言處理以及其他領域的問題中。

當然, 如果本章的核心點僅僅是寫一個能夠識別手寫數字的程序, 那應該會很短, 但是我們會沿着這個問題, 去理解神經網絡的很多核心概念與思想, 具體包括

  • 兩種人工神經元: 感知器(perceptron)和 S形神經元(sigmoid neuron)
  • 神經網絡中標準的學習算法: 隨機梯度下降(stochastic gradient descent),

自始至終, 我都會專注於解釋爲什麼事情是以這種方式做, 並且搭建起你對神經網絡的直覺性理解。 這比簡單地展示出基本的機理要花費更多的篇幅, 但是這一切都是值得的, 因爲你可以通過這個過程對神經網絡獲得深刻的理解。 在獲得了所有的學習成果後, 在本章節的末尾, 我們將能夠理解什麼是深度學習以及爲什麼深度學習非常重要。

感知器(Perceptrons)


什麼是神經網絡? 作爲開始, 我會先來解釋一種稱作 感知器(perceptron) 的人工神經元。 感知器是19世紀50年代-60年代, 由科學家 Frank Rosenblatt 受 Warren McCulloch 和 Walter Pitts 的早期工作啓發而開發出的。 今天, 通常會使用其他類型的人工神經元。 在這本書中, 以及在大部分現代的神經網絡工作中, 主要被使用的神經元模型是被稱作 S形神經元(sigmoid neuron)。 我們會很快地學習到 S形神經元。 但是爲了理解爲什麼S形神經元會以那種方式定義, 我們首先得花一些時間來理解感知器。

所以, 感知器是如何工作的? 一個感知器會獲取多個二進制的輸入,x1,x2,...., 然後產生一個單個的二進制輸出。
這裏寫圖片描述

在這個李忠, 感知器有三個輸入 x1,x2,x3 。 一般來說, 感知器可以擁有更多或更少的輸入,Rosenblatt 提出了一種計算輸出的簡單規則。 他引入了權重(weights) , w1,w2,...., 等多個實數來表示不同輸入的重要程度。 神經元的輸出 0 或 1 是由輸入的加權求和是否超過某個閾值 而決定的 。 就像權重一樣, 閾值也是一個實數, 是該神經元的一個參數。 可以用代數更精確地描述爲:

output=0ifi=1wjxjthreshold1ifi=1wjxj>threshold(1)

以上就是所有感知器工作的原理了!

這是一個非常基礎的數學模型。一種理解感知器的方法是, 你可以將其看做一個能夠評估不同因素, 做出決策的裝置。 讓我們舉個栗子,一個不是非常逼真的例子,但是很容易理解。 假設週末即將到來, 你聽說你所在的城市裏將要有一個奶酪嘉年華。 你喜歡奶酪, 想要決定是不是要去參加這個活動。你可能會根據以下三個因素做出決策:

  1. 天氣是否好?
  2. 你的男/女朋友是否會陪你一起去?
  3. 這個活動乘坐公交是否容易到達(你沒有車)?

我們可以用三個二進制變量 x1,x2,x3 來表示這三個因素。 例如, x1=1 表示天氣好, x1=0 表示天氣不好。 類似的, x2=1 表示你的男/女朋友會陪你一起去, x2=0 表示不會陪你一起去。 x3 同理。

現在假設你極度喜奶酪, 以致於即使你的男/女朋友不能夠陪你而且公共交通不易到達活動地點, 你也想去參加這個活動, 但是你非常厭惡壞天氣,如果天氣不好的話, 你是無論如何都不會去參加這個活動。 你可以使用感知器來爲這個決策過程建模。 一種建模的方法是, 設置權重 w1=6,w2=2,w3=2w1 設置的越大表示天氣因素對你來說越重要。 最終,假設你將這個感知器的threshhold 設置成 5 。通過這些設置, 該感知器實現了一個你所期望的決策模型, 輸出 1 表示你要去參加活動(天氣好), 輸出 0 表示你不去參加活動(天氣不好)。

通過調整權重和閾值, 我們可以獲得不同的決策模型。 例如, 假設我們選擇 3 作爲閾值, 那麼當天氣好時或容易乘坐公交到達活動地點且男/女朋友願意陪伴你兩個條件都達成時,決策你應該去參加活動。 調低閾值表示你更願意去參加這個活動。

顯然, 感知器不是一個完整的人類決策模型!但是這個例子展示了一個感知器是如何把不同的因素進行考量進而做出決策。 那麼, 利用下面這個多感知器組成的一個複雜網絡爲複雜而微妙的決策過程建模似乎是可行的:
這裏寫圖片描述

在這個網絡中, 第一列感知器 - 我們稱之爲第一層感知器- 會通過衡量輸入 inputs 的內容 做出三個非常簡單的決策。 那麼第二層感知器起什麼作用? 第二層的每一個感知器會進一步衡量第一層感知器輸出的結果再次做出各自的決策。 和第一層相比, 可以做出更爲複雜,更爲抽象的決策。 繼而, 第三層感知器可以作出更加複雜的決策。 通過這種方式, 一個多層感知器可以參與非常複雜的決策過程。

當我在定義感知器時, 順表說了一句一個感知器只有一個輸出 。 在上面的這個網絡中, 感知器似乎有多個輸出。 事實上, 每個感知器輸出依舊只有一個, 多個輸出的箭頭僅僅是單個輸出被用於多個感知器的輸入的簡便表示。

讓我們來簡化一下描述感知器的方式。條件i=1wjxj>threshold 看上去非常煩瑣, 我們可以做兩個記號上的改變來簡化它。 第一個改變是把i=1wjxj 用點乘的方式表示, 即 wxi=1wjxj , wx 是權重和輸入的向量表示。 第二個改變是, 把閾值挪到等式的另外一側, 改用偏倚量 (bias)來表示。 bthreshold 使用偏倚量而不是閾值來表示後, 感知器的規則可以被重寫爲:

output=0ifi=1wjxj+b01ifi=1wjxj+b>0(2)

你可以認爲, 偏倚量是用於衡量一個感知器輸出 1 的難易度。 顯然, 引入偏倚量 b 僅僅是我們描述感知器方式的一個細微改變。 但是我們之後會看到, 這個改變如何進一步帶來符號表示上的簡化。 在本書的其餘部分, 我們將不會再使用閾值 threshold , 只使用 bias。

我已經將感知器描述爲一個衡量多個因素, 做出決策的方法。 感知器的另一用途是模擬計算機底層的基礎邏輯電路例如 AND, OR, NAND。(譯者注: 大學學過電子電路的童鞋會對此感到非常親切, 沒有學過的童鞋這裏比較困惑也不影響後續內容的理解) 例如, 假設我們有一個感知器接收兩個輸入, 每一個輸入的權重是 -2, 總偏移 bias 是 3 , 如下:
這裏寫圖片描述

這樣, 當我們輸入 00 的時候就會輸出 1 , 因爲(2)0+(2)0+3=1 , 同理, 輸入0110 會輸出1。 但是輸入11 的時候時候會輸出0。 所以, 這個感知器實現了一個與非門(NAND GATE)。

與非門的例子說明我們可以使用感知器來實現簡單的邏輯功能。 事實上, 我們可以利用多感知器的網絡來實現任意的邏輯。 原因是與非門對於計算來說是一個通用的功能,只要有足夠多的與非門, 我們可以搭建出實現任意邏輯計算的功能 (這一點是被嚴謹證明過的, 計算機硬件底層就是由大量與非門實現的)。 例如我們用與非門搭建一個實現 2 bit 加法電路, 這首先需要按位求和, 還需要有一個 bit 來實現進位。
這裏寫圖片描述

上面這個 2 bit 求和電路可以通過如下的感知網絡實現:
這裏寫圖片描述

上圖中的每個感知器的兩個輸入權重 weight 都是 -2 , 偏倚量 bias 都是3 。 注意到我們把負責輸出進位結果的感知器向左移動了一些, 僅僅是爲了更容易畫圖。

上面的感知器網絡中有一個值得關注的細節是, 最左邊的感知器的輸出在最下面的感知器上使用了兩次, 作爲輸入。 當我定義感知器的時候, 我並沒有說過這種把一個輸出兩次用作另外一個感知器的輸入的操作是否被允許。事實上, 這不是特別要緊,如果我們不想允許這種情形, 那麼可以把兩個箭頭合併作一個箭頭, 權重改爲 -4 即可, 如下圖:

這裏寫圖片描述

現在我會把 x1x2 兩個變量用額外的一層感知器 - 輸入層 - 來表示。 :

這裏寫圖片描述

輸入層的感知器只有輸出, 沒有輸入, 如下:
這裏寫圖片描述

這種表示方法只是一種速記法, 它並不是真的意味着一個沒有輸入的感知器。爲了理解, 我們可以假設我們確實有一個無輸入的感知器。 那麼加權求和結果 i=1wjxj 就總是0, 那麼感知器當 b>0 輸出1, 當 b0 輸出 0 。 也就是說感知器僅僅是在簡單的輸出一個固定的值,而不是我們所期待的上圖中的 x1 。 因此, 最好不要把輸入層的感知器當做一個真正的感知器,而是僅僅地把他們當做一些特殊的, 用於定義輸出特定變量值的單元 x1,x2,...

加法器的例子展現瞭如何用感知器網絡來模擬包含很多與非門的邏輯電路哦, 由於與非門對於任意計算邏輯來說都是通用的, 那麼感知器對於任意計算邏輯也是通用的。

感知器的這種邏輯計算通用性即是令人興奮的, 也是讓人失望的。 令人興奮的原因是我們感知器網絡可以和任意一種計算設備一樣強大(目前電子計算的實現基礎都是邏輯電路,量子計算除外)。 但是這個事實也是令人失望的, 因爲感知器似乎也僅僅是一種新型的與非門 , 這可算不上是什麼大的進步。

然而, 事實不止這麼簡單。 因爲我們發現, 可以發明出一種學習算法 來自動的調整人工神經網絡中各個神經元的的權重 weights 和 bias 。 這個調整過程可以在沒有程序員直接干預的情況下,針對外界的反饋刺激做出響應。 這些學習算法使得我們能以一種完全不同於傳統邏輯電路的方式來使用人工神經元 , 即用神經網絡學習去解決那些很難用通過傳統邏輯電路設計而解決的複雜問題。

can learn to build gate network without human intervention

S形神經元(Sigmoid Neurons)


學習算法聽上去非常棒! 但是我們如何爲神經網絡實現這種算法呢? 假設有一個感知器網絡, 我們希望用該網絡來學習解決某個問題。 例如, 神經網絡的輸入時掃描出的手寫數字圖像的原始像素數據。 我們希望神經網絡可以學習權重 weights 和 偏倚量 bias 使得該神經網絡的輸出能夠將數字正確區分。 爲了理解這個學習過程怎樣進行, 先假設我們對網絡的權重 weights (或偏倚量 bias) 作一些輕微的改變。 我們希望看到的是, 這些權重上的輕微改變僅僅對網絡的輸出產生一些輕微的影響。 這之後我們會理解, 這個性質會使得學習成爲可能。 下面是這個改動的圖例展現(很明顯下面這個網絡還太簡單, 不足以進行手寫數字的識別!):

這裏寫圖片描述

如果一個權重 weight ( 或者偏倚量 bias ) 的小改變只會導致最終輸出的輕微變化, 那麼我們就可以利用這個性質來修改權重和偏倚量使得網絡表現的更像我們期待的那樣。 例如, 網絡可能把一個應該被當做數字9 的圖像錯誤識別成了數字8 , 我們可以搞清楚如何微調權重或偏倚量的值, 使得網絡最終的輸出結果可以靠近一點 “9” . 然後我們可以重複這個過程, 不斷地改變權重和偏倚量, 使得網絡輸出的結果越來越好。 這樣, 網絡就實現了學習的過程。

問題是, 上述的過程並不會發生在由感知器構成的神經網絡中。 事實上, 對於任意感知器權重或偏倚量的輕微改變可能會導致那個感知器的輸出完全翻轉, 例如從0到1。 這個結果的翻轉可能會導致其餘網絡的行爲以一種非常複雜的方式完全改變。 所以當你把網絡微調到可以正確識別9的時候, 網絡對於其他圖像的識別行爲會被完全改變, 難以控制。 這使得你很難通過微調權重和偏倚量的方法, 使得網絡逐漸表現出你期待的行爲。可能會有什麼非常聰明的方法可以解決這個問題, 但是這裏並沒顯而易見的方案可以使得感知器網絡進行學習。

這個問題可以通過引入另外一種稱作 S形神經元(sigmoid neruon)的神經元予以解決。 S形神經元和感知器非常相似,可以說是在感知器的基礎上進行了一些改動使得權重和偏倚量的輕微變化只會導致輸出的輕微變化。 這是使得S形神經元組成的神經網絡能夠學習的至關重要的一個性質。

我們以相同的方式來描繪 S形神經元:

這裏寫圖片描述

和感知器一樣, S形神經元擁有輸入 x1,x2,... , 不同的是, 輸入的值不再是 0 或 1, 而是0至1之間的任意值, 如 0.638... 是S形神經元的一個有效輸入。 和感知器一樣, S形神經元的每個輸入也有相應的權重w1,w2,... 和 總體偏倚量 bias , b 。 但是, 輸出的值不再是 0 或 1, 而是 σ(wx+b) , 其中 σ 被稱作S形函數(sigmoid function)或 邏輯函數(logisitic function), 定義爲:

σz11+ez[3]

爲了使得一切更清晰一些, S形神經元的輸出可以被寫作:
11+exp(i=1wjxjb)[4]

- 註解: exp(x 表示指數函數 ex

乍一看, S形神經元似乎和感知器非常不一樣。 S形神經元的代數形式在你還未熟悉它時, 似乎非常晦澀且令人生畏。 但事實上, 感知器和S形神經元有很多相似之處, S形函數的代數形式更多的只是一些技術細節, 而不是一個難於理解的障礙。

爲了理解S形神經元和感知器模型的相似性, 假設zwx+b 是一個很大的正數, 那麼ez0σ(z)1 。 換句話說, 當 z=wx+b 爲一個很大的正值時, 輸出就趨近於1, 其行爲就像感知器一樣。 另一方面, 假設z=wx+b 爲一個很大的負數時,則 ez 那麼, σ(z)0 。 所以當z=wx+b 爲一個非常大的負數時, S形神經元的行爲也趨近於感知器。 只有當z=wx+b 爲中間值時, S形神經元的行爲纔會與感知器有所區別。

σ 函數的代數形式的具體含義到底是什麼? 我們應該如何理解它? 事實上, σ 函數的具體形式並不是那麼重要, 真正重要的是這個函數的形狀:

這裏寫圖片描述

上圖的這個形狀是一個階梯函數( step function )的平滑版。

這裏寫圖片描述

如果σ 函數變成了一個階梯函數, 那麼 S形神經元就會變成一個感知器, 因爲它只能輸出0或1, 且取決於w \cdot x + b 的正負。因此, 通過利用σ 函數, 我們獲得了一個“平滑版”的感知器。
確切來說, σ 函數的平滑性質纔是關鍵的性質,而不是它具體的代數形式。 σ 函數的平滑性意味着權重的細微改變Δwj 和偏倚量的細微改變Δb 僅僅會產生輸出的一個輕微變化Δoutput . 事實上, 微積分告訴我們Δoutput 約等於:

ΔoutputjoutputwjΔwj+outputbΔb

其中 output/wjoutput/b 表示輸出 output 關於 wjb 的偏微分。 不要因爲你不熟悉偏微分而感到焦慮。 儘管這個表達式看上去很複雜, 充滿了偏微分符號, 但它事實只是再闡述一些簡單的事實:Δoutput 是一個關於ΔwjΔb 的線性函數。 這種線性性質使得很容易在權重 w 和 偏倚量 j 上進行一些微調以達到輸出上期待的變化。 所以 , S形神經元在定性方面,擁有了和感知器相同的行爲, 在定量方面, 使得微調權重和偏倚量來改變輸出的操作變得可行。

回到之前的話題, 如果σ 函數的形狀是真正重要的部分, 那麼爲什麼一定要使用公式3 ? 事實上, 在本書的靠後部分, 我們會偶爾考量輸出結果爲 f(wx+b) 的神經元, 其中 f() 稱爲激活函數(activation function)。 使用不同的激活函數產生的變化主要就是公式5中的偏微分值發生了改變, 使用 σ 函數會簡化代數運算, 因爲指數在微分的時候有一些非常可愛的性質。 無論如何, σ 函數在神經網絡領域中是一個被廣泛使用的函數, 也是我們在這本書中將會用得最多的激活函數。

那麼, 如何解讀S形神經元的輸出呢? 顯然, 感知器和S形神經元的一個大的區別是 S形神經元並不是只輸出0或1。 它可以輸出0到1之間的任意實數, 如 0.173...,0.689 都是有效的輸出。這個性質可以在一些場景發揮作用,例如, 我們可能會希望用輸出值來表示一個經過神經網絡處理的像素數據的平均亮度(average intensity)時。 但是, 這個性質有時又會讓人感到討厭, 例如我們希望用輸出來表示被識別的圖像是否爲“9”時, 顯然感知器直接輸出0或1會更加清晰明瞭。 然而, 實際應用中, 我們可以設置, 大於 0.5 的輸出被認爲表示圖像被識別爲 “是 9”, 小於等於 0.5 的輸出表示識別結果“不是 9”。 在使用這種設置時, 我總是會明確的指出來, 避免任何可能產生的困惑。

神經網絡的架構

在下一小節, 我會介紹一個可以出色完成手寫數字分類工作的神經網絡。 在介紹它之前, 解釋一些有關神經網絡架構的術語會很有助於我們進一步學習神經網絡。 假設我們有如下的網絡:
這裏寫圖片描述
正如之前提及的, 神經網絡最左側的一層被稱爲輸入層, 其中的神經元被稱爲輸入神經元(input neurons)。 最右側的一層被稱爲輸出層 (output layer), 在本例中, 包含一個單一的輸出神經元。 中間層被稱爲隱藏層(hidden layer), 因爲在這一層的神經元既不是輸入也不是輸出。 術語“隱藏(hidden)”聽起來好像有點神祕 - 當我第一次聽到這個術語的術語時, 我以爲它一定一位置更深層次的哲學或數學含義 - 但事實上, 它真的只包含“既不是輸出, 也不是輸出”的含義。 上面的網絡僅有一層隱藏層, 但是一些神經網絡中有多層隱藏層, 例如, 如下的四層網絡結構有兩層隱藏層:

這裏寫圖片描述

出於歷史原因, 這種多層的網絡儘管由S形神經元組成, 但有時仍會被稱爲多層感知器(multilayer perceptrons or MLPs), 我不打算在本書中使用 MLP 這個術語, 因爲我覺得它很容易讓人誤解, 在這裏提出來只是希望提醒你它的確存在。

神經網絡輸入層和輸出層的設計通常非常簡單, 例如, 我們希望識別1個數字是否爲9 ,一個很自然的方法是將像素的亮度信息編碼作爲輸入神經元的輸入。 如果圖像是一個 64*64 的灰度圖像, 那麼我們就有 64*64=4096 個輸入神經元, 每一個像素的亮度分佈在 0 到 1 之間。 輸出層就僅僅包含一個神經元, 輸出的值如果小於 0.5 表示 “輸入圖像不是9”, 輸出值大於0.5 表示“輸入圖像是9”。

景觀神經網絡輸入層和輸出層的設計通常很直觀, 但是隱藏層的設計就很需要講究了。 具體來說, 很難把隱藏層的設計用一些簡單的經驗法則來總結。 相反, 神經網絡的研究者開發出了很多針對隱藏層的啓發式設計方法, 以此來幫助人們獲得能按照他們期待運行的神經網絡。 例如, 一些啓發式方法可以用來幫助決策如何在隱藏倉的數量和訓練神經網絡所需要的時間之間作取捨。 我們在本書的後面部分會見到一些這樣的啓發式設計方法。

到目前爲止, 我們已經討論了會把一層神經元的輸出用作另一層神經元輸入的神經網絡。 這種網絡被稱爲前饋(feedforward)神經網絡。 這意味着, 網絡中是不存在迴路的 - 信息總是向前反饋, 不會向後反饋。 如果網絡中有了迴路,最終 σ 函數的輸入就會受到網絡最終輸出結果的影響。 那樣會很難弄懂網絡的工作機理, 所以我們並不允許這種迴路的存在。

然而, 低於有一些其他的人工神經網絡模型允許反向回饋傳播迴路的存在 。這些模型被稱爲 遞歸神經網絡。 遞歸神經網絡的思想是使得神經元的激活態只能維持一段時間,在神經元被激活時, 可以進一步激活其他的神經元, 這些被進一步激活的神經元同樣也只能維持一段時間的激活態。 這樣的過程可以使得神經元一層層地被激活, 像瀑布一樣。 在這種情況下, 反向傳播迴路的存在並不會引發問題, 因爲神經元的輸出對其輸入的影響僅僅會在晚一些的時刻發生, 而不是立刻發生。

遞歸神經網絡的影響力比前向反饋網絡的影響力要小, 一部分原因是尚未發現非常強有效的遞歸神經網絡的學習算法(到目前爲止)。 但是遞歸神經網絡依舊極度有趣且值得研究。因爲從生物神經領域的角度來看, 和前向反饋網絡相比,遞歸神經網絡更接近我們大腦的工作機理。 然而,爲了不擴大我們學習的範圍, 本書我們將專注於更加廣泛使用的前向反饋網絡。

一個簡單的用於區分手寫數字的網絡

定義了神經網絡之後, 讓我們回到手寫數字識別的問題。 我們可以把這個問題分解成兩個子問題。 第一個子問題是, 我們需要設計一種方式來吧一個包含數字圖像序列的圖片分解成多個獨立的圖片。 如下:

這裏寫圖片描述

分割成
這裏寫圖片描述

我們人類可以很容易地解決這個切分問題。 但是要使得電腦程序來正確地切分圖片則很有挑戰。 一旦圖片可以被正確地切分, 程序只需要識別單個的數字就行了。 例如, 我們希望我們的程序識別上述圖片中的第一個數字

這裏寫圖片描述

我們將專注於寫一個程序來解決第二個子問題, 也就是, 識別單個的數字。 這樣做的原因是, 一旦你有了單數字識別器以後, 分解數字序列的問題就不是那麼困難了。 有很多的方法可以解決數字序列分割問題。 一種方法是, 嘗試用不同的方式分割圖片, 然後使用單個數字識別器爲分割出的每個圖片打分。如果單數字識別器可以順利識別所有的分割結果, 則這個分割方案評分較高, 如果單數字識別器在識別分割結果上,出現了很多的困難,無法識別, 那麼這個分割方案得分就低。 這種方法的背後思想是, 如果單數字識別器在識別過程中遇到了困難, 那很有可能是因爲數字序列沒有被正確分割導致的。 這種思想和其相應的變種可以被用來很好地解決圖片切分問題。 所以, 與其擔心數字分割如何解決, 不如專注於開發一個神經網絡來解決更有意思且更有挑戰的問題, 也就是, 識別單個的手寫數字。

爲了識別單個的數字, 我們會使用一個三層神經網絡如下:

這裏寫圖片描述

網絡的輸入層包含了用於編碼像素輸入的神經元。 正如上一小節所提到的, 網絡的訓練數據將會包含很多 28 * 28 像素的手寫數字掃描圖像, 因此輸入層包含了 784=2828 個神經元。 輸入的像素信息是灰度, 取值0代表白, 取值1代表黑。 中間值代表深度不同的灰色。

第二層是網絡的隱藏層, 我們用 n 表示隱藏層的神經元數量, 我們會對 n 實驗不同的取值。 上圖中表示了一個僅僅包含 n=15 神經元的隱藏層。

輸出層裏的神經網絡包含 10 個神經元。 如果第一個神經元激活, 即輸出 output1 , 那麼就表示神經網絡認爲輸入的數字是 “0”, 如果第二個神經元的輸出爲 1 , 則表示神經網絡認爲輸入的圖像爲 “1”, 依次類推。 準確來說, 我們把輸出層的神經元按0-9編號, 然後判斷哪一個神經元輸出的值最大, 輸出值最大的那個神經元的編號會被當做網絡對於輸入圖像的識別結果。

你可能會想, 爲什麼我們要用 10 個輸出神經元。 畢竟我們的目標是讓網絡輸出一個 0-9 的結果來告訴我們輸入的圖像是什麼, 一個看上去很自然的方法是僅僅使用 4 個輸出神經元, 把每一個神經元的輸出當做二進制的一個值。 四個神經元就足夠編碼 0 - 9 了, 因爲 24=16 , 完全足夠表示 10 個值。 那麼, 爲什麼我們還要用 10 個神經元來表示最終的結果呢? 這樣設計的依據來源於經驗: 我們可以將兩種方案都予以嘗試, 有 10 個輸出神經元的網絡比 4 個輸出神經元的網絡有更好的訓練結果。 但是這會引發我們思考, 爲什麼 10 個輸出神經元的網絡工作得更好。 有沒有什麼直覺性的原因可以提前告訴我們爲什麼應該使用一個 10輸出神經元的網絡而非 4 輸出神經元的網絡。

爲了理解其中的原因,從神經網絡工作的最初原理來思考會很有幫助。 以 10 個神經元輸出的例子來思考,首先讓我們專注於第一個輸出神經元, 那個用於判斷輸入數字是否爲 0 的神經元。 它的輸出結果是通過衡量隱藏層神經元輸出結果而得到的。 那麼, 隱藏層神經元在做什麼呢? 讓我們來假設隱藏層的第一個神經元在檢測一個輸入的圖像是否像如下的結果 。

這裏寫圖片描述

該神經元可以通過增加這種情形的像素輸入的權重, 減輕其他情形輸入的權重來識別該類型圖案。 類似的, 讓我們假設隱藏層的第二個, 第三個神經元分別在識別圖像是不是如下的情形。

這裏寫圖片描述

正如你所猜測到的, 這四個部分構成圖像 0 如下:

這裏寫圖片描述

所以當隱藏層的這4 個神經元都被激活時, 我們可以判斷輸入的圖像是 “0”, 當然, 這不是我們用來識別數字爲 0 的唯一證據, 我們可以用很多其他的方式來判斷一個數字是不是 0 (例如, 上述圖像的變形, 或者添加一些干擾後的情形). 但是, 在上圖的這種情形下, 把輸入判斷爲一個 “0” 是一種比較穩妥的做法。

假設神經網絡是按照我們上述的假設來工作的, 那麼我們可以爲 10 輸出神經網絡比 4輸出神經網絡工作得更好給出一個可行的解釋: 如果我們有 4 個神經元作爲輸出, 那麼第一個神經元就是用於判斷輸入數字的二進制表示最高位的結果是什麼。 然而, 並沒有非常容易的方法可以把二進制數字的最高位與上述圖像的形狀關聯起來。 所以很難想象, 有什麼歷史原因使得數字不同部分的形狀和其二進制表示的最高位有什麼緊密的關係。

到目前爲止, 上述這些內容都僅僅是一種啓發式的推斷。 並沒有什麼明確的證據表明這個3層神經網絡必須以我剛剛描述的方式運作, 即由隱藏層神經元來檢測簡單的圖形形狀。 可能一種更爲精巧的學習算法可以發現一種權重分佈使得我們僅僅使用4個輸出神經元也可以得到很好的識別結果。 但是, 作爲一種啓發式的思路, 我剛剛所描述的機理是可以說的通的, 而且可以幫助你在設計好的神經網絡時, 節約很多時間。

利用梯度下降算法進行學習

現在我們已經有了一個自己的神經網絡設計,怎樣使得“學習” 識別手寫數字呢? 我們首先需要一個可供學習的數據集, 即所謂的訓練數據集。 我們會使用 MNIST 數據集, 其中包含了成千上萬的手寫數字掃描圖片, 以及他們的正確識別結果。 之所以叫 MNIST 是因爲這些數據其實是 NIST 美國國家標準與技術研究院 收集的兩個數據集合的經過修改(Modified)後的子集. 下面這些數字就是其中的幾個圖片。

這裏寫圖片描述

正如你說看到的, 這些數字和我們在本章開頭所展現的, 作爲挑戰等待識別的數字一樣。 當然, 在測試我們的神經網絡時, 我們會要求識別不再訓練數據集內的圖片。

MNIST 數據來源於兩個部分。 第一個部分包含 60,000 個圖片, 用來作爲訓練數據 。 這些圖片是從250個人的手寫數字掃描而來, 其中的半數人是人口統計局員工,另一半則是高中生。 這些圖片 28 * 28 像素的灰度圖片。 第二個部分是 10,000個用於作爲測試數據的圖片, 同樣, 這些圖片也是 28 * 28 像素的灰度圖片。 我們將使用測試數據來評估我們的神經網絡“學習”的效果是否足夠好, 這些測試用的數據來源於另外250 個人(依舊是一半人口統計局員工和一半高中生)。 這有助於讓我們相信訓練出的神經網絡可以識別不在訓練數據源之內的人書寫出的數字。

我們會使用記號 x 表示訓練輸入。 把訓練的輸入當做一個 28 * 28 = 784 的空間向量。 向量中的每一項代表其表示的像素位置的灰度。 我們將期望的輸出表示爲 y=y(x) , 其中 y 是一個 10維向量, 例如, 對於一個特定的訓練圖片 x , 識別結果爲 6 則會表示爲 y(x)=(0000001000)T 。 注意到這裏的 T 表示矩陣的轉置操作, 把一個行向量轉置爲列向量。

這裏我們希望擁有一個算法可以幫我們找到合適的權重 weights和 便宜了 biases 使得神經網絡的輸出針對所有訓練輸入的輸出都接近於我們訓練輸入的實際結果 y(x) ( 譯者提示: 這裏的 y(x) 很容易被誤當做神經網絡的輸出 , 但是它其實用來表示我們期待的正確輸出)。 爲了量化我們是否達到了目標, 這裏定義一個 成本函數(有時也被稱爲損失函數或目標函數):

C(w,b)12nxy(x)a2[6]

這裏, w 表示神經網絡中所有的權重組成的集合, b 表示所有的偏移值組成的集合, n 表示訓練輸入的數量。 求和的計算是針對所有的輸入 x 進行的。當然, 輸出 a 取決於 x, w, 和 b , 但是爲了使表達式比較簡單, 我並沒有明確表達這種依賴關係。 記號 v 表示的是常見的求矢量長度的函數。我們將會把 C 稱爲二次成本函數( quadratic cost function)。 有時它也被稱爲均方差(mean squared error)或 MSE。 觀察這個二次成本函數的形式, 我們會發現 C(w,b) 是非負的。 進一步來說, 成本函數 C(w,b) 儘可能變小時, 也就是, C(w,b)0 時, 也就是對於所有的輸入x , 都有 y(x) 約等於輸出 a 。所以我們的訓練算法如果能夠找到一組權重值 w 和偏倚量 b 使得 C(w,b)0 , 就可以認爲是大功告成了。 所以我們的訓練算法的目標就是尋找 w 和 b 的值, 使得 C(w,b) 取最小值。 我們將通過一個被稱之爲 梯度下降 的算法完成這個目標

爲什麼要引入二次成本函數呢? 我們所感興趣的不是怎樣使得正確識別的圖片數量最大化嗎, 爲什麼不直接試着尋找能正確識別圖片數量的最大值呢, 而是間接的來尋找成本函數的最小值呢?

這裏的問題在於, 正確識別的圖片數量並不是一個關於 w 和 b 的平滑函數。 大部分情況下, 對於權重和偏倚量的細微改變, 並不會導致正確識別圖片數量的變化。 這會使得我們很難搞清楚怎麼樣調整權重值和偏倚量, 才能改善我們的神經網絡識別效果。 如果我們使用一個平滑的函數, 例如二次成本函數, 那麼就更容易搞清該如何調整權重和偏倚量以改善神經網絡的效果。 這就是爲什麼我們要專注於如何最小化成本函數, 以及爲什麼通過成本函數, 我們才能檢驗神經網絡識別準確度。

即便知道了我們想使用一個平滑的成本函數, 但你依舊可能好奇, 爲什麼我們使用 等式【6】形式的二次函數而不是一些其他的函數。 這難道是一種特別的選擇嗎? 是不是我們選擇了另一種不同的成本函數就會得到完全不同的結果呢?

這是一個合理的假設,之後我們會重新思考成本函數, 對它做一些改變。 然而, 等式【6】描述的二次成本函數足以幫助我們理解神經網絡的工作基本原理, 所以我們目前會繼續使用這個二次成本函數。

再強調一下,我們的目標是爲我們的神經網絡尋找到一組權重和偏倚量, 使得二次成本函數 C(w,b) 的值最小化。 這已經是一個被轉化的,非常明確的目標了, 但是它還是有者讓人心生畏懼的複雜結構: w 和 b 都是矢量, σ 函數還需要被代入到這個等式中, 神經網絡的結構也沒有在函數體現, 我們的輸入數據集 MNIST 還很複雜等等一系列問題。 但事實上, 我們只要忽略這裏面大部分結構, 僅僅關注如何最小化成本函數這一件事, 我們可以理解大量的內容。 所以, 目前, 我們將暫時忘記成本函數的具體形式, 神經網絡錯綜複雜的連接等等一系列問題。 相反, 讓我們想象我們僅僅擁有了一個變量很多的函數, 而我們想要將這個函數最小化。 我們會開發出一種名爲梯度下降的技巧來解決這個最小化問題。 然後我們會重新回來思考我們真正想要最小化的那個具體的, 神經網絡的成本函數。

好的, 讓我們來想象一下, 我們試圖最小化某個函數 C(v) , 這個可能是一個擁有很多變量的實數函數, v=v1,v2,.... 注意到, 我把 w 和 b 的記號替換成了 v ,以此來強調這裏的 C(v) 可以是任意的一個函數。重要的事情再說一遍, 我們現在並不是在思考公式【6】 所描述的二次成本函數, 而是在思考如何最小化一個一般的, 可以是任意形式的函數。 爲了找到 C(v) 的最小值, 先將 C 想象成一個只有兩個變量v1,v2 的函數會容易很多。

這裏寫圖片描述

我們所希望找到的是, Cv1,v2 的值在什麼位置才能全局最小。 現在, 假如函數的形狀是上圖這樣, 我們就可以通過肉眼直接找出取使得Cv1,v2 取最小值的位置(v1,v2)座標。 這樣的話, 我可能給出了一個太簡單的函數。 但是對於一個一般的函數 C , 它可能是一個擁有衆多變量的,非常複雜的函數,通常是不可能通過畫出函數形狀來直接尋找最小值的。

一種解決這個問題的方法是利用微積分學來分析性地尋找最小值。 我們可以計算出函數的導數來尋找函數取極值的位置。 如果我們運氣好的話, 函數僅僅包含一個或幾個變量時, 這樣是可行的,但是當變量特別多時, 這種方法就會變成噩夢。 而對於神經網絡而言, 我們常常會需要更多的變量,最大的神經網絡擁有依賴於上億的權重值和偏倚量的成本函數, 且形式極爲複雜。 使用微積分的方式沒有辦法尋找其最小值。

好的, 所以微積分並不起作用。 幸運的是, 有一個漂亮的類推幫我們推薦了一種可以很好起作用的算法。 我們現在把函數的形狀想象成一個山谷, 就像上一張圖片一樣。 想象一個球從山谷上滾到了谷底。 或許我們可以用這種想法來尋找到一個函數的最小值? 我們可以隨機的爲這個球尋找一個起始點, 然後模擬一下球滾向谷底的運動方向。 我們可通過計算 C 的導數來完成這個模擬過程。 導數值會告訴我們球所在“山谷”位置的形狀, 繼而讓我們知道球會向哪個方向滾動。

基於我剛纔所說的, 你可能以爲我們在試圖寫出球的牛頓運動方程, 需要考慮摩擦力和重力等等的。 但其實, 我們並不會真的去模擬球的下滾, 我們是在發明一種算法來尋找函數 C 的最小值, 並不是發明一種精確模擬球物理學上的下滾。 這種舉例只是用來激發我們的想象,而不是束縛我們的思考。 所以與其被物理學的複雜細節所困擾, 不如讓我們簡單的問自己一個問題: 如果我們自己做一天上帝, 可以編寫我們自己的物理學法則, 來指示球應該向哪裏滾動, 我們會選擇哪種運動法則使得球總是滾向谷底呢?
爲了使得這個問題更爲明確, 讓我思考一下,當我們將球輕微地移動很少的量 , 即在v1 方向移動很小的量 Δv1 , 在v2 方向移動很小的量 Δv2 。 微積分(不熟悉的童鞋可以百度一下全微分的定理和法則)告訴我們函數 C 會這樣變化:

ΔCCv1Δv1+Cv2Δv2[7]

我們會尋找到一種選擇 Δv1Δv2 的方法, 來使得ΔC 是負值, 這樣也就相當於找到了一種方法讓“球總是向谷底滾”。, 爲了搞清楚該如何選擇 Δv1Δv2 , , 我們定義矢量 Δv(Δv1,Δv2)T 。 這裏 T 還是用來表示轉置, 使得行向量變爲列向量。 我們還會將函數 C 的“梯度”定義爲偏微分矢量, (Cv1,Cv2)T 。 我們用記號 C 表示梯度向量:
C(Cv1,Cv2)T[8]

一瞬間, 我們把改變量記號 ΔC 改寫爲了梯度 C 。 在這裏, 我想澄清一下梯度符號有時會讓人產生困惑的地方。當第一次見到梯度符號時, 人們可能會思考我們該如何理解 這個符號呢? 到底意味着什麼。 事實上, 單純地將 看做一個單一的數學對象, 也就是公式【8】定義的向量是完全ok的。從這個角度而言, 僅僅是一個小旗子一樣的記號, 告訴你 “嘿,C 是一個梯度向量哦 ” 。 不過, C 符號還有一些更高級的角度使得它可以被看做一個獨立的數學實體, 但是目前我們並不需要那種角度。

有了以上的定義後, ΔC 的表達式 【7】 就可以被重寫爲:

ΔCCΔv[9]
  • 譯者注: 對兩個向量執行點乘運算,就是對這兩個向量對應位置的元素一一相乘之後求和的操作,點乘的結果是一個標量。

這個等式可以幫助我們解釋梯度向量 C , Cv 的變化量和 C 的變化量關聯了起來, 就像我們所期待的“梯度”(斜率)所能做到的一樣。 但是這個等式真正令人興奮的地方在於, 它讓我們看到了如何選擇 Δv 才能使得 ΔC 變成負值。 假設我們選擇:

Δv=ηC[10]

這裏, η 是一個很小的正值(被稱之我學習率 learning rate), 那麼等式 【9】則告訴我們 ΔCηCC=ηC2 。 因爲 C20 , 所以這就保證了 ΔC0 , 也就是說,如果我們根據表達式 10 的指示來改變 vC 總是減小的, 而不會增加。 這正是我們想要的性質! 所以我們將會用等式 【10】 來定義我們梯度下降算法中的 “小球運動法則”。 我們會用等式 【10】 來計算 Δv , 然後按照計算出的 Δv 移動“小球“:

vv=vηC[11]

然後我們會再次使用這個規則, 進行下一次移動。 只要一直重複這個動作, 我們就能夠一直減小 C 直到我們達到一個全局最小值。

總結一下, 梯度下降算法工作的方式就是, 不斷地重複計算梯度 C , 然後向梯度正值相反的方向移動, “沿着坡的下方向滾動”。 正如下圖所示:
這裏寫圖片描述

注意到這個梯度下降的規則並不是重新了物理上的移動。 在現實世界中, 一個球在下滾的過程中會有動能, 那個動能會使得球可以從谷底衝上坡, 只有在由摩擦力造成動能轉化的情況下, 才能使得球總是會停在谷底。 對比而言, 我們的規則在選擇變量 Δv 時, 僅僅是在說, “向下走, 立刻” , 是一個尋找最小值的優秀法則。

爲了使得梯度下降算法正確的工作, 我們需要恰當的選擇學習率 (learning rate) η 爲一個足夠小的量, 才能儘可能的模擬等式 9 。 如果我們不這樣做, 我們可能最後會得到 ΔC>0 的結果。 同時我們又不希望 η 過於小, 以至於 Δv 過於小,進而使得梯度下降算法工作的十分緩慢。 在實際的算法實現中, η 通常會變化, 使得等式 9 有一個好的近似值,且使得算法不會太緩慢。 之後我們會看到它具體是如何實現的。

我已經解釋了梯度下降算法在 C 是一個二元函數時, 是如何工作的。 但事實上, 在 C 擁有更多的變量時, 所有的東西依舊成立, 假設 C 是一個包含 m 個變量,v1,v2,...,vm 的函數。 那麼 ΔCΔv=(Δv1,...,Δvm)T 的關係就是(譯者注: 這裏用到的還是全微分定理和相關法則, 但是此處沒有提到判斷 C 函數是否可微的問題。

ΔCCΔv[12]

其中, 梯度 就是:

C(Cv1,...,Cvm)T[13]

正如兩個變量的情況一樣,我們可以選擇:

Δv=ηC[14]

然後,我們就可以保證等式 12 中的 ΔC 是負值。 然後繼續按照規則更新變量 v :

vv=vηC[15]

你可以將認爲這個 v 的更新規則就是梯度下降算法的定義。 它給了我們一種方法, 通過重複地改變變量 v 的位置去尋找函數 C 的最小值。 這個規則並不總是能起作用, 其中有一些環節可能會出問題, 導致梯度下降算法沒辦法找到 C 全局最小值, 這種情況我們會在後續章節裏面探討。 但是, 實踐中, 梯度下降算法的效果通常都是極好的, 在神經網絡中,我們會發現它是一種最小化成本函數極度強大的工具, 進而可以幫助網絡進行學習。

事實上, 從某種角度而言, 梯度下降算法甚至是尋找最小值的最優策略。 假設我們試圖進行一個 Δv 的位移, 來使得 C 減少的儘可能多。 這就等同於在最小化 ΔCCΔv , 我們會限制移動量的大小使得 Δv=ϵ , 其中 ϵ 是某個固定的很小的正值 ϵ>0 。 換句話說, 我們的移動距離是固定的,我們希望找到一個方向能使得這個移動的效果儘可能明顯,也就是移動以後, C 減少的儘可能多。 可以證明,給定限制 v=ϵ 時, 當 Δv=ηC , 其中 η=ϵ/C 時, ΔC=CΔv 可以取到最小值。

譯者證明: 根據柯西施瓦茲不等式, 兩個向量 u, v 有如下不等式關係

uvuv

所以當我們已知 ΔC=CΔvv=ϵ 時,

如果令 Δv=ηC=(ϵ/C)C ,


ΔC=CΔv=C(ϵ/C)C=C2(ϵ/C)=C2(ϵ/C)=ϵC=ΔvC

根據柯西不等式知 ΔvCΔvC , 所以, ΔvC 必然爲 CΔv

人們已經研究調查過很多梯度下降的變種, 包括更接近物理學球下滾模擬的變種算法, 這些球下滾模擬的變種有一些優勢, 但是也有很明顯的缺點: 不可避免地需要計算二階偏微分, 這個計算代價很高。爲了搞清楚爲什麼說他們計算代價很高, 現假設我們要計算二階偏微分 2C/vjvk 。 如果有一千萬個 vj 這種的變量, 那麼我們就需要進行一萬億 (一千萬的平方)次這樣的微分計算, 這樣會產生極大的計算負擔! 雖然這樣說, 但是還是有一些技巧來避免這種問題, 而且尋找梯度下降算法的替代方案也是研究的熱點領域。 但是在這本書中, 我們將使用梯度算法(和其變種)作爲我們進行神經網絡學習的主要方法。

那麼, 到底我們應該該如何應用梯度下降算法來進行神經網絡學習呢。 核心思想是用梯度下降算法來尋找能夠最小化等式【6】 所描述的成本函數的權重 wkbl . 爲了弄清具體如何使用, 讓我們將 wkbl 代入公式後, 重新審視一下梯度下降的更新變量的規則, 換句話說, 我們之前所說的球的“位置”對應的變量現在由 wkbl 構成, 梯度向量 C 由兩個部分 CwkCbl 兩個部分構成。 針對這兩個部分寫出梯度下降算法的變量(“球的位置”)更新規則如下:

wkblwk=wkηCwkbl=blηCbl.(16)(17)
  • 譯者注: 這裏的代入過程,可能會讓人困惑, 因爲神經網絡中的在給定一個輸入 x 以後, 輸出其實是一個的變量爲所有的 w 和 所有的 b 的複雜形式的函數, 其中可能有 k 個權重值 wi=123...,kl 個偏倚量的值 bj=1,2,3,...,l 。 所以其最終的成本函數 C 其實是一個擁有 k+l 個變量的函數。 雖然變量衆多, 但是正如之前所說, 梯度下降的變量更新法則不僅適用於兩個變量的函數, 對於多變量的函數同樣適用。 所以這裏就得到了如上的兩條變量更新規則【16】【17】。

通過重複引用上面的更新規則, 我們可以“滾下坡”, 有望找到找到一個成本函數的最小值, 換句話說, 這是一個可以用來進行神經網絡“學習”的規則。

在應用這個規則時, 會遇到很有挑戰性的問題,我們會在後面的章節裏面探究這些問題。 但是目前, 我只想提到其中的一個問題,爲了理解這個問題是什麼, 讓我們重新回顧一下公式【6】中的二次成本函數。注意到這個成本函數的形式是 C=1nxCx , 它其實是在求成本 Cx 在 x 取不同值時的平均值。 注意到對於單個的訓練輸入 x , 輸出 Cxy(x)a22

譯者再次提醒: y(x) 這裏不是神經網絡會隨權重 w_k 和 偏倚量 b_l 調整而變化的輸出, 而是給定一個 x 後, 我們期望的輸出。 a 纔是會隨權重 w_k 和 偏倚量 b_l 調整而變化的輸出, 這裏的 y(x) 和 a 都是會隨着 x 的變化而改變的

在實踐中, 爲了計算梯度 C ,我們需要爲每個 x 分別計算 Cx , 然後再對他們求平均值, C=1nxCx , 不幸的是, 當訓練輸入數量非常龐大時, 這可能會花費很長時間, 導致“學習”速度非常緩慢。

有一個被稱爲隨機梯度下降的想法可以被用來加速學習過程,這個想法是將一小部門隨機選擇的輸入作爲樣本集, 然後針對這個樣本集計算 Cx , 然後計算這些來自小樣本集的結果的平均值, 我們可以很快地得到一個良好的, 關於真正 C 的估計值。 這可以幫助我們加速梯度下降, 進而加速學習的過程。

爲了使得這個想法更爲清晰準確, 隨機梯度下降算法的具體是這樣工作的:
1. 首先隨機選擇少量 m 個 訓練輸入 X1,X2,...,Xm , 將這些輸入稱爲 “迷你批”mini-batch, 就是一小批數據)。 “迷你批” 的大小 m 需要足夠大, 至少大到滿足如下條件:

mj=1CXjmxCxn=C,(18)

也就是說, m 個輸入的平均值要接近於全體輸入的平均值才行。

上述等式的兩邊互換一下我們就得到:

C1mj=1mCXj,(19)

這個公式表示了我們可以通過計算 “迷你批”輸入的梯度而估計所有輸入的“梯度”。

爲了將上述的結論和我們神經網絡的“學習”連接在一起, 讓我來代入結論, 重新看一下梯度下降的規則:

假設 wkbl 分別表示神經網絡中的多個權重值和多個偏倚量, 那麼隨機梯度下降的更新規則就如下:

wkblwk=wkηmjCXjwkbl=blηmjCXjbl,(20)(21)

這裏的 m 個 X 就是從所有輸入中隨機選出的一個“迷你批”數據集。然後我們會再隨機選出另一個“迷你批”數據集, 再以他們作爲訓練輸入。然後, 繼續進行這個過程, 當我們耗盡了所有的訓練輸入後, 就算完成了一個“紀元”(epoch) 的訓練, 然後會開始新紀元的訓練。
1
上述的這種規則偶爾會隨着成本函數輸入的規模和迷你批對權重和偏倚量的更新次數的規模增長而變化。 在等式【6】中, 我們將總體的成本函數用因子 1n 均攤開來。 但是人們有時會忽略這個因子1n ,將每一個輸入的成本都加起來求總和而不是求平均。 這在訓練輸入的量不能提前確定時是尤其有用的,例如, 訓練的過程中可能會有更多實時產生的新的訓練輸入被生成出來。

類似的, “迷你批更新規則” 【20】和 【21】 有時也會省略 1m 。 概念上, 這好像不會產生什麼影響, 因爲這等同於重新重新調整了 η 的大小, 但是在做一些細節性的對比時, 這種改變是值得小心對待的。

我們可以將隨機梯度下降想成民意調查: 從一個小樣本集中計算梯度要比從所有數據集中計算梯度容易地多, 這就像是做民意調查要比做一輪完整的全民選舉要容易地多一樣。 例如, 如果我們有一個大小 n=60,000 的訓練集, 正如 MNIST 數據集一樣, 選擇一個迷你批的大小 m=10 就意味着我們可以在估計梯度時, 可以提速 6000倍!當然,估計並不是完美的, 會有概率性的抖動, 但是它並不需要是完美的: 我們真正關心的是, 整體上朝着幫助函數 C 減小的方向移動, 這意味着我們並不需要梯度的精確值。 在實踐中, 隨機梯度下降算法是一個被廣泛採用且強有力的神經網絡“學習”技巧, 它也是我們這本書將要開發的大部分“學習”技巧的基礎。
讓我們通過討論一個常常令剛剛接觸梯度下降的人感到困惑的點來結束本節內容。 在神經網絡中, 成本函數 C 是一個衆多變量的函數- 其中包括所有的權重和偏倚量- 所以, 在某種意義上, 它定義了一個很高維度的空間。 人們有的時候會卡在這裏思考: “嘿, 我必須能夠把這些額外的維度具象化(在腦海中形成圖像概念)才行”。 然後他們會開始擔心: “我沒辦法思考四維的空間是什麼樣, 更別說五維或者五千萬維空間”。 我們是不是缺少什麼特別的能力, 一些“真正” 的數學家才具有的能力。 當然, 答案是NO。 即便是最專業的數學家也沒有辦法很好地具象化。 他們所使用的技巧是, 與其去具象化四維空間, 不如嘗試開發出其他的表示四維空間的方式。 這也正是我們之前所做的: 使用線性代數的形式來表示 \Delta C, 進而來搞清楚應該如何移動才能減小 C 。 善於在多維空間思考的人頭腦中包含了一個技巧庫, 其中有很多不同的這種類型的技巧。 那些技巧可能不具備和我們所習慣的可視化的三維空間的簡單性, 但是一旦你也搭建形成了這種技巧庫, 你也可以很好地在高維空間思考。 我不會在這裏贅述太多細節, 但是如果你有興趣的話, 你可能會對這裏有關專業數學家思考高維空間的技巧很感興趣。 儘管這些技巧可能會很複雜,但是其中最好的技巧都是很符合人的直覺且可以理解的, 也可以被任何一個人掌握

python 實現數字識別神經網絡

好的, 讓我們動手寫一個程序利用 MNIST 數據集和隨機梯度下降算法 來“學習”識別手寫數字。 我們將會寫一個短的簡短的 python(版本2.7) 程序, 僅僅包含 74 行代碼!我們第一件要做的事是獲得 MNIST 數據。 如果你是一個 git 用戶, 那你可以通過 clone 下面的本書代碼的 repository 。

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

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

我之前描述 MNIST 數據的時候, 我說過, 它們被楓成了 60,000 個訓練圖片和 10,000 個測試圖片。 這是 MNIST 的官方描述。 但事實上, 我們以一種略微不同的方式劃分數據: 我們不會動測試圖片, 但把 60,000 個訓練圖片劃分爲兩部分。 一部分包含 50,000 張圖片, 我們將用它們來訓練我們的神經網絡, 另外的 10,000 張圖片作爲校驗集。 在這一章裏, 我們不會使用校驗集數據, 但是本書的後面, 我們會發現校驗集的數據在我們試圖搞清該如何設置 超參數 (hyper-parameters)- 例如學習率等等的參數 時會很有用, 這些參數不會被我們的學習算法直接選擇。儘管校驗集數據不是原來 MNIST 規範數據的一部分, 但是很多人都會以這種方式來使用 MNIST 數據, 而且校驗集數據在神經網絡中是很常見的。 從現在開始, 當我提到 “MNIST 訓練數據”, 我就是在指代那 50,000 個圖片。 而不是 MNIST 原來包含的那 60,000 個圖片。

正如我之前標註的, MNIST 數據集是基於兩個由 NIST(美國國家標準委員會) 收集的數據集。 爲了構建 MNIST 數據集 ,NIST 數據集被 Yann LeCun , Corinna Cortes, and Christopher J. C. Burges 這幾個人拆分處理成更加便捷易用的格式。你可以從這裏獲得更多細節 。 這些數據集在我的代碼庫裏被處理成了更容易被 python 加載和操作的形式。 這種形式的數據是我從蒙特利爾大學LIST機器學習實驗室獲得的。

除了 MNIST 數據, 我們還需要一個 python 庫 Numpy, 用於進行更快的線性代數計算。 如果你還沒有安裝 Numpy, 你可以從這裏獲得它

讓我們在給出完整的 python 代碼前先解釋一些神經網絡代碼的核心特性。 這裏的核心類是一個 Network 類, 我們使用這個類來初始化一個 Network 對象。

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 類型的數據 sizes 存儲了神經網絡各層的的神經元個數。 例如, 如果我們要創建一個 在第一層擁有兩個神經元, 在第二層擁有 3 個神經元, 在最後一層擁有 1 個神經元的 Network 對象, 我們會寫出如下的代碼:

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

Network 對象的權重值偏倚量都是用 Numpy np.random.randn 函數隨機初始化的,該函數會生成位置參數 μ=0 和尺度函數 σ=1 的正態分佈。 這樣的隨機初始化給我們的隨機梯度下降算法提供了一個“起點”。 在後面的章節中, 我們會找出更好的方式來初始化權重 weights 和偏倚量 biases, 現在我們暫且先採用這種初始化方案。 注意, Network 的初始化代碼假設第一層神經元是輸入層, 然後省略了爲這一層神經元賦偏倚量 biases 值的操作, 因爲偏倚量只會在非輸入層的神經元計算輸出的時候用到。

還需要注意, 權重 weights 和 偏倚量 biases 的值存儲爲 Numpy 矩陣的 list 對象。所以, 樣例代碼中的 net.weights[1] 是一個 Numpy 矩陣, 存儲了連接第二層和第三層神經元的權重值(不是第一層和第二層, 因爲 python 的list 索引是從 0 開始的 )。 因爲 net.weights[1] 的表示太冗長, 我們索性用 w 表示這個矩陣。這個矩陣中, wjk 是連接第二層第 k 個神經元和第三層第 j 個神經元的權重。 這個jk 的順序看上去有些奇怪,把 jk 的位置互換一下不是更符合人的習慣嗎。 使用這種順序的一個很大好處是第三層神經元的激活向量就是:

a=σ(wa+b).(22)

這個等式中包含了不少的內容, 讓我們把它一一分解開來。 a 是第二層神經元的激活向量。 爲了獲得第三層神經元的激活向量 a , 我們用權重矩陣 w 乘以 a , 並且加上偏倚量向量 b 。 然後對計算出的向量結果wa+b 的每一個元素應用 σ 函數計算。 很容易驗證等式 【22】 和我們之前的等式 【4】的計算規則是一樣的。

搞清楚了以上的內容, 就容易寫出計算一個網絡輸出的代碼了 。 我們從定義 S型函數(sigmoid function)開始:

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

注意到, 當輸入 z 是一個向量或者 Numpy 數組時, Numpy 會自動的對每個元素依次計算。也就是以向量形式進行計算。

然後, 我們向 Network 類里加一個前向反饋傳播的方法, 該方法在給定一個神經網絡的輸入後, 會返回相應的輸出*。 該方法的全部功能就是對每一層計算等式【22】

  • *註解: 該方法假設輸入是一個 (n,1) Numpy ndarray, 而不是一個 (n,) 向量。 這裏, n 是神經網絡的輸入數量。 如果你試着使用 (n,) 向量作爲輸入, 你就會得到奇怪的結果。儘管使用(n,) 向量似乎是一種更加自然的選擇,但使用(n,1) ndarray 使得我們更容易更改代碼來向前一次性傳播多個輸入, 這在有些情況下是非常方便的。

當然, 我們想讓 Network 對象們做的最主要的事情是“學習”。 爲了實現這個目標, 我們需要給他們一個 SGD 方法, 該方法會實現隨機梯度下降算法( Stocastic Gradient Descent)。 下面是代碼, 這段代碼有幾處會比較難懂, 但是我會把他們拆解開來進行講解:

def SGD(self, training_data, epochs, mini_batch_size, eta,
            test_data=None):
        """利用迷你集隨機梯度下降算法(mini-batch stochastic gradient descent)訓練神經網絡。 “訓練數據” 是一個由多個 tuple (x,y) 組成的list(tuple 和 list 都是 python 中的非常基礎的數據結構) . (x, y) 代表着訓練輸入和輸入對應的期望輸出。   其他必選參數的含義都可以根據名字看出來。 如果 test_data 非空, 那麼 network 對象就會在每一個世代 epoch 訓練後, 被測試數據評估一遍, 然後, 當前的進度會被輸出。 在需呀追蹤神經網絡的訓練進度時, 這是很有用的, 但是這會極大的減緩網絡的訓練速度。 """
        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 是由 tuples (x, y) 構成的 list , 代表訓練的輸入和與之相對應的期望輸出。 變量 epochsmini_batch_size 的含義和你期待的一致 - 分別表示要訓練多少個世代(將世代epoch 理解成一個最小粒度的訓練週期即可)以及迷你集(mini-batch) 的大小。eta 就是學習率 η ( 該符號的英文表示就是 eta) 如果可選的參數 如果可選參數 test_data 被提供了, 那麼程序就會對 Network 對象在每一個世代的訓練結束後, 使用測試數據進行評估, 然後輸出結果作爲當前的訓練進度, 但是這會很大幅度的降低訓練速度。

這段代碼是以如下的方式工作的 :

  1. 在每個 epoch 中, 它首先隨機打亂訓練數據, 然後把他們劃分成很多份合適尺寸的 mini-batches 。 這是一種從訓練數據中隨機取樣的簡單方法。

  2. 然後, 對於每一個 mini-batch 我們完成梯度下降的一小步。 這是通過代碼 self.update_mini_batch(mini_batch, eta) 完成的。 該方法僅僅使用 mini_batch 中的數據, 根據單次梯度下降迭代規則來更新神經網絡的權重 weights 和 偏倚量 biases. 下面是 update_mini_batch 方法的代碼。

 def update_mini_batch(self, mini_batch, eta):
        """ 通過對單個迷你數據集 mini-batch 應用梯度下降以及後向擴散, 更新神經網絡的權重 weights 和偏倚量 biases.  mini_batch 是多個 tuple (x,y) 組成的 list。 eta 是學習率"""
        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)

這行代碼調用了一種名爲 反向傳播( backpropogation) 的算法, 該算法是一種快速計算成本函數梯度的算法。 所以 update_mini_batch 可以僅僅通過計算迷你數據集中的每一訓練樣例的梯度來, 然後利用計算出來的梯度來更新 self.weightsself.biases

現在我暫時不打算展示 backprop方法的代碼。 目前, 讓我們就暫且將其當做一個現成可用的黑盒, 返回訓練輸入樣例 x 所對應的成本函數的梯度值。(譯者注: 下一章節會對這裏的實現細節做詳細的說明)

讓我們來看一下整個程序, 包括我之前省略的註釋內容。 除了 self.backprop , 整個程序都是清晰易讀的 - 所有的複雜工作都是在 self.SGDself.update_mini_batch 中完成的, 這兩個方法我們已經解釋過了。 self.backprop 方法利用了一些其他二外的函數來輔助計算梯度, 例如 sigmoid_prime 函數, 該函數計算了 σ 函數的導數。 另外還有 self.cost_derivative方法, 在這裏我暫且先不解釋該方法。 你可以僅僅通過閱讀代碼和文檔註釋來理解這些函數的主要功能。 我們會在下一個章節中來詳細解釋其中的細節。 注意, 雖然代碼看上去很長, 但是大部分代碼都是用來增加代碼可讀性的文檔註釋。 事實上, 整個程序僅僅包含 74 行不含空格,不含註釋的代碼。 所有的代碼可以在GitHub 的這個鏈接 上找到。

""" 
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 , 來進行。 我們可以在 python shell 中通過執行一些命令來完成。

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

當然, 這可以在另外一個單獨 python 程序中完成, 但是如果你一直沿着這條路走下去, 你就會發現在 Python shell 裏面做可能是最簡單的一種方法。

在我們加載了 MNIST 數據後,我麼會搭建一個擁有 30 個隱藏神經元的 Network。 我們會在 import 之前列出的 74 行 Python 程序(程序名爲 network)後進行這項工作。

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

終於, 我們要用梯度下降算法來從 MNIST 數據中進行學習30 個世代 (epoch)的學習了, 我們的迷你數據集 mini-batch 大小設爲 10, 學習率 η=3.0

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

注意到, 如果你一邊閱讀, 一遍執行這個代碼, 你會發現它要花費一段時間才能執行完, 對於一臺典型的(2015年水平)的機器而言, 它可能會花費幾分鐘才能執行完。 我建議你讓代碼跑起來, 然後繼續閱讀, 然後間隙性地去查看代碼的輸出結果。如果你很着急, 你可以通過減少 epochs 的值或減少隱藏神經元的數量或僅僅使用一部分數據集來加速運行。 注意, 真正用於生產環境的代碼會快得多, 這些 python 腳本僅僅是用來幫助你理解神經網絡的工作機理, 而不是一些高性能的代碼。 而且, 一旦我們訓練好了一個網絡, 他就能很快地運行在任意的計算平臺上了。 例如, 一旦我們獲得神經網絡的一組良好的權重值和偏倚量值, 他們可以很容易地被遷移到瀏覽器中的 Javascript 中或是作爲一個手機 App 中。

總而言之, 這裏有一份神經網絡一次訓練的部分輸出。 下面的輸出顯示了測試圖片中有多少圖片在一個世代的訓練後, 被神經網絡正確地識別了出來。 正如你看到的, 在僅僅一個世代的訓練後, 該網絡就能從 10,000 個圖片中正確識別 9,129 個圖片了, 然後正確識別的數量還會隨着訓練繼續增長。

Epoch 0: 9129 / 10000
Epoch 1: 9295 / 10000
Epoch 2: 9348 / 10000
...
Epoch 27: 9528 / 10000
Epoch 28: 9542 / 10000
Epoch 29: 9534 / 10000

也就是說, 訓練出來的神經網絡的識別最大正確率約爲 95% - 95.42 ( Epoch 28) 是它的峯值 ! 這個結果對於首次嘗試可是令人振奮的。 不過, 我要提醒你的是, 你在運行代碼的時候, 你的結果並不一定和我一樣, 因爲我們隨機使用不同的權重值和偏倚量初始化神經網絡的。爲了產生上述的結果, 我取了三次運行中的最好的一次結果。

讓我們將隱藏神經元的數量修改爲 100 後, 重新做如上的實驗。 正如之前所說的, 如果你一邊閱讀, 一邊運行代碼, 你會發現它要花一些時間才能執行完( 在我的機器上, 每一個 training epoch 要花費數 10 秒), 所以一邊閱讀,一邊等待代碼執行是一個很好的選擇。

>>> net = network.Network([784, 100, 10])
>>> net.SGD(training_data, 30, 10, 3.0, test_data=test_data)

當然, 上面的改變將結果改善到了 96.59% 。 至少在當前的案例中, 使用更多的神經元可以幫我們得到更好的結果。

*注: 讀者反饋說, 這個實驗的結果有各種各樣的, 有些情況下, 調整後輸出的結果變得更差了一點。 通過使用第三章的技巧,我們可以很大程度的減少在不同次運行時神經網絡性能表現的差異。

當然, 爲了獲得這種準確率, 我不得不對訓練的 epoch 數量, mini-batch 的大小, 學習率 η 的值做一些特殊的選擇。 正如我之前提到的, 這些值被稱爲神經網絡 “超參數 (hyper-parameters)”, 這樣叫是爲了把他們和其他由神經網絡學習出來的參數(權重 weights 和偏倚量 biases) 區分開。 如果我們選的超參數很差, 那麼我們得到的結果也會相應變差, 例如, 如果我們將學習率設爲 η=0.001

>>> net = network.Network([784, 100, 10])
>>> net.SGD(training_data, 30, 10, 0.001, test_data=test_data)

結果就不那麼好了

Epoch 0: 1139 / 10000
Epoch 1: 1136 / 10000
Epoch 2: 1135 / 10000
...
Epoch 27: 2101 / 10000
Epoch 28: 2123 / 10000
Epoch 29: 2142 / 10000

然而, 你可以看到, 網絡的性能會隨着時間的推移慢慢地變好, 這意味着應該增加學習率, 例如 η=0.01 。 如果我們這樣做, 我們得到了更好的結果, 這意味着我們應該再增大一點學習率。 (如果你做了某種改善結果的操作, 嘗試着多做幾次!)如果我們做了好幾次之後, 我們最終會得到差不多這樣的結果 η=1.0 (可能最終可以調整到 3.0), 這和我們之前的實驗很接近。 所以, 即便我們一開始超參數選的不好, 我們至少可以從結果中獲取足夠的信息以幫助我們選擇更好的超參數。
一般來說, 調試一個神經網絡是很困難的, 尤其是當一開始選擇的超參數結果和隨機選擇超參數的結果差不多時。 假設我們使用了之前效果很好的 30 個神經元網絡結構, 但是設置的學習率爲 \eta=100.0 。

>>> net = network.Network([784, 30, 10])
>>> net.SGD(training_data, 30, 10, 100.0, test_data=test_data)

這樣我們就會離我們想要的結果偏離比較遠了, 學習率也過高:

Epoch 0: 1009 / 10000
Epoch 1: 1009 / 10000
Epoch 2: 1009 / 10000
Epoch 3: 1009 / 10000
...
Epoch 27: 982 / 10000
Epoch 28: 982 / 10000
Epoch 29: 982 / 10000

現在, 讓我們想象我們第一次嘗試遇到的是這種結果。 當然,從先前的試驗中我們知道, 正確的做法是減小學習率。 但是如果我們第一次嘗試就遇到這種結果, 輸出的結果中並沒有什麼信息指示我們應該如何調整參數。 我們可能不僅僅會擔心學習率是不是設置的不合適, 還有可能擔心神經網絡其他的任意一個方面。我們可能回想, 是不是權重和偏倚量被初始化成了一種很難訓練的值。 又或者我們的訓練數據不夠多, 沒辦法完成有效的訓練? 又或者我們訓練的 epoch 不夠多? 又或者, 這種神經網絡結構根本無法識別手寫數字?又或許是學習率太低或是太高了?

當你第一次遇到這種問題是, 你總是無法確定的。 這裏想要告訴你的是, 調試一個神經網絡並不是一件輕而易舉的事, 而且, 就像正常的編程一樣, 其中是由諸多技巧甚至說藝術的。你需要學會調試的藝術才能從神經網絡中獲得好的結果。 更一般的說, 我們需要建立選擇良好的超參數和好的神經網絡架構的直覺。我們會在本書的後面部分花費大篇幅來討論這些問題。 包括我們如何選擇上面的超參數。

之前, 我跳過了 MNIST 數據是如何加載的細節。 這個過程是非常清晰直白的。 爲了完整性, 這裏展示其代碼。 用來村塾 MNIST 數據的數據結果在註釋中有描述, 都很顯而易見,由 Numpy ndarray 對象構成的 tuple, list對象(如果你不熟悉 ndarray的話, 就把它當做向量即可)

"""
mnist_loader
~~~~~~~~~~~~

A library to load the MNIST image data.  For details of the data
structures that are returned, see the doc strings for ``load_data``
and ``load_data_wrapper``.  In practice, ``load_data_wrapper`` is the
function usually called by our neural network code.
"""

#### Libraries
# Standard library
import cPickle
import gzip

# Third-party libraries
import numpy as np

def load_data():
    """Return the MNIST data as a tuple containing the training data,
    the validation data, and the test data.

    The ``training_data`` is returned as a tuple with two entries.
    The first entry contains the actual training images.  This is a
    numpy ndarray with 50,000 entries.  Each entry is, in turn, a
    numpy ndarray with 784 values, representing the 28 * 28 = 784
    pixels in a single MNIST image.

    The second entry in the ``training_data`` tuple is a numpy ndarray
    containing 50,000 entries.  Those entries are just the digit
    values (0...9) for the corresponding images contained in the first
    entry of the tuple.

    The ``validation_data`` and ``test_data`` are similar, except
    each contains only 10,000 images.

    This is a nice data format, but for use in neural networks it's
    helpful to modify the format of the ``training_data`` a little.
    That's done in the wrapper function ``load_data_wrapper()``, see
    below.
    """
    f = gzip.open('../data/mnist.pkl.gz', 'rb')
    training_data, validation_data, test_data = cPickle.load(f)
    f.close()
    return (training_data, validation_data, test_data)

def load_data_wrapper():
    """Return a tuple containing ``(training_data, validation_data,
    test_data)``. Based on ``load_data``, but the format is more
    convenient for use in our implementation of neural networks.

    In particular, ``training_data`` is a list containing 50,000
    2-tuples ``(x, y)``.  ``x`` is a 784-dimensional numpy.ndarray
    containing the input image.  ``y`` is a 10-dimensional
    numpy.ndarray representing the unit vector corresponding to the
    correct digit for ``x``.

    ``validation_data`` and ``test_data`` are lists containing 10,000
    2-tuples ``(x, y)``.  In each case, ``x`` is a 784-dimensional
    numpy.ndarry containing the input image, and ``y`` is the
    corresponding classification, i.e., the digit values (integers)
    corresponding to ``x``.

    Obviously, this means we're using slightly different formats for
    the training data and the validation / test data.  These formats
    turn out to be the most convenient for use in our neural network
    code."""
    tr_d, va_d, te_d = load_data()
    training_inputs = [np.reshape(x, (784, 1)) for x in tr_d[0]]
    training_results = [vectorized_result(y) for y in tr_d[1]]
    training_data = zip(training_inputs, training_results)
    validation_inputs = [np.reshape(x, (784, 1)) for x in va_d[0]]
    validation_data = zip(validation_inputs, va_d[1])
    test_inputs = [np.reshape(x, (784, 1)) for x in te_d[0]]
    test_data = zip(test_inputs, te_d[1])
    return (training_data, validation_data, test_data)

def vectorized_result(j):
    """Return a 10-dimensional unit vector with a 1.0 in the jth
    position and zeroes elsewhere.  This is used to convert a digit
    (0...9) into a corresponding desired output from the neural
    network."""
    e = np.zeros((10, 1))
    e[j] = 1.0
    return e

我在上面說過, 我們的程序的輸出結果很理想。 這意味着什麼? 我是在和什麼作對比? 如果有一些(非神經網絡)比較的基準線測試會更好地理解怎樣纔算是表現理想。 最簡單的比較基線當然是隨機猜數字。 每次猜對的概率只有 10% 。我們比隨機猜做的好多了!

那麼, 如果我們提高我們比較的標準呢, 讓我們嘗試一個極其簡單的想法: 根據一個圖像的“有多黑”來識別它是什麼數字。 例如, 一個 2 肯定是比 1 要暗的, 僅僅因爲有更多的像素被黑色佔據, 如下圖所示。

這裏寫圖片描述

這種想法暗示我們爲每個數字 0,1,2,3,…,9 計算平均灰度。 當一個新的數字被展示出來時, 我們計算一下這個圖片有多黑, 然後選擇最接近的平均灰度所對應的數字作爲其識別結果。 這是一個很簡單的過程, 很容易用代碼寫出來, 所以我不會寫出這個過程的代碼。 如果你有興趣的話, 它就在這個 Github repository裏。 但是, 它比隨機猜還是要好多了, 它可以從 10,000 個圖片中正確猜出 2,225 個 ,也就是說有 22.25% 的準確率。

要想找到一些其他的辦法使得準確率達到 20% - 50% 並不困難。 如果你努力嘗試的話, 你可以得到超過 50% 正確率的結果。 但是要得到更好的結果, 使用現有的成熟的機器學習算法就會很有幫助。 讓我們試着使用目前已知的最好的算法 支持向量機 (support vector machine, SVM). 如果你不熟悉支持向量機, 不要擔心, 我們並不需要理解 SVM 的工作原理。 相反, 我們會使用現成的 python 庫 scikit-learn , 這個庫會提供一個簡單的封裝了 C 語言庫 LIBSVM Python 接口 .

如果我們運行了 sckikit-learn 的默認參數設置的 SVM 分類器,那麼它會從 10,000 個測試圖片中正確識別 9,435 個圖片。 代碼可以從這裏得到。這和我們非常初級的通過圖片的黑度來識別圖片的方法相比有很大的提升。 事實上, SVM 的表現和我們的神經網絡表現幾乎差不多, 僅僅差了一點。 在後面的章節,我會介紹一些新的技巧, 使得我們的神經網絡表現得比 SVM 好的多。

然而, 這裏並不是故事的結尾。 10,000 個圖片中正確識別中 9,435 個圖片的結果用的是 scikit-learn 的默認 SVM 配置。 SVM 有很多可調節的參數, 尋找到能夠改善結果的參數是有可能的。 我不會專門來做這個尋找的操作, 但是我會把你指向一個 Andreas Mueller 寫作的這篇博文裏。 Mueller 展示了一些優化 SVM 參數的工作, 得到了超過 98.5 正確率的結果。 換句話說, 一個優化的好的SVM 在 70 個圖片中僅僅會識別錯一個。 這是很好的結果! 神經網絡能做得更好嗎?

事實上, 神經網絡可以。 目前, 良好設計的神經網絡在識別 MNIST 數據上的表現遠遠超過了其他的技巧, 包括 SVM 。 目前的記錄(2013年) 是從 10,000 個圖片中正確識別 9,979 個圖片。 這是由 Li Wan (譯者目測是一箇中國人( ̄▽ ̄)), Matthew Zeiler, Sixin Zhang(中國人( ̄▽ ̄)) 和 Rob Fergus 創造的。 我們會在本書的後面看到他們使用的大部分技巧。 在那種水平下, 識別效果就接近於人類了, 甚至可以說是更好, 因爲 MNIST 中的一些圖片對人來來說都很難識別, 例如:

這裏寫圖片描述

我詳細你也同意這些圖片很難辨認! 所以,在 MNIST 數據集中存在這種數據的情況下, 能夠在 10,000 個圖片中錯誤分類 21 個是非常出色的成績。 通常, 在編程時, 我們認爲要解決一個複雜的問題, 像識別 MNIST 數字這種問題需要非常複雜的程序。 但是, 即便是剛纔提到的 Li Wan 等人所寫的程序也僅僅只包含很簡單的算法, 這個算法的變種我們會在後續的章節中看到。 所有的複雜度都會從訓練的數據中自動學習到。 在某種意義上來說, 我們的結果和那些更成熟的論文中的結果所展現出的寓意是:

對一些問題而言,
複雜的算法 簡單的學習算法 + 良好的訓練數據

走向深度學習

儘管神經網絡展現了非常出色的表現, 但是這個表現某種程度上是很神祕的。 神經網絡中的權重和偏倚量是被自動發現的。 這意味着我們並不能立刻對神經網絡是如何工作進行解釋。 我們能否找到一種方法解釋我們的神經網絡工作在識別手寫數字時所依據的原則嗎。 而且, 知道了那些原則以後, 我們能做得更好嗎?

爲了使得這個問題更嚴峻, 假設幾十年後, 神經網絡最終產生了人工智能(AI). 我們能否理解智能網絡是如何工作的? 這些智能網絡對我們來說可能是不透明的,擁有我們不能理解的權重和偏倚量, 因爲這些權重和偏倚量的值都是自動學習出來的。 在人工智能的早期研究中, 人們希望構建人工智能所花費的努力也能幫助我們理解智能背後的工作原理, 也有可能是, 人類大腦工作的原理。 但是結果有可能是我們既不理解大腦是怎麼運轉的,也不理解人工智能是如何工作的!

爲了解決這些問題, 讓我們重新思考本章一開始給出人工神經元, 這些神經元是一種衡量因素的方法。 假設我們想決定一個圖片是否爲一張人臉:

這裏寫圖片描述

我們可以用解決手寫數字識別的相同方法來解決這個問題, 將圖片的像素作爲神經網絡的輸入, 把輸出設爲一個單獨的神經元,要麼輸出“沒錯, 這是一個臉”, 要麼輸出“不, 這不是一個臉”

假設我們這樣做了, 但是我們不使用學習算法,而是試着設計手動設計一個網絡, 用於選擇參數和權重。 我們應當如何進行, 現在暫時忘卻所有關於神經網絡的內容, 一個直覺性的方法是把問題分解爲小的子問題: 這個圖片是不是左上角有個眼鏡, 它是不是中間有鼻子, 是不是下部有個嘴巴, 是不是上面有頭髮等等。

如果上面的問題答案都是 “是”, 或者 “可能是”, 那麼我們就判斷這張圖片可能是一個人臉。 相反,如果大部分問題的答案都是否, 那麼圖片有可能就不是一個人臉。

當然, 這只是一種粗略的直覺, 它有很多站不住腳的地方。 可能這個人禿頭, 所以沒有頭髮。 可能僅僅我們只能看到臉的一部分, 或者只是臉的某一個角度, 或是其他的臉的特徵被忽略了。 不過, 這個直覺告訴我們我們可以利用神經網絡來解決這些子問題。 然後我們可以通過合併衆多網絡來構建一個實現人臉檢測的神經網絡。 如下是一個可能的架構, 正方形表示子神經網絡。 注意, 這不是要作爲一個真實解決人臉檢測問題的方法, 而是用來幫我們構建網絡工作機理的直覺的。 架構如下:

這裏寫圖片描述

將子網絡進一步分解也是可以的。 假設我們在解決問題“是否左上角有一個眼睛” , 那麼這個問題可以被分解成 :“是不是有一個眉毛”, “是不是有睫毛”, “是不是有虹膜”等, 但是讓我們保持其簡單性, 這個網絡的問題“是否左上角有眼睛”被分解成:

這裏寫圖片描述

這些問題依舊可以進一步被分解。 最終, 我們會得到一個回答很簡單一個問題的子網絡,以至於它可以僅僅用一個像素來回答。 這些問題, 例如, 可能是關於是否在圖片的某些位置有某種簡單形狀的出現。 這種問題可以被一個直接連接到原始圖片像素的神經元所回答。

最終的結果是一個將複雜問題“這個圖片是否爲一個人臉”拆解開來的神經網絡。 它是通過很多層完成, 前面的層僅僅回答一些簡單而具體的問題, 後面的層構建起一種更加複雜和抽象的概念。 擁有這種多層次結構的神經網絡 - 兩個或更多隱藏層- 被稱爲深度神經網絡。

當然, 我還沒有說如何完成這種遞歸式地神經網絡分解。 很顯然這要手工設計神經網絡的權重和偏倚量是不現實的。 相反, 我們相擁學習算法來從數據中自動學習權重和偏倚量, 進而, 學習到層級性的概念。 19世紀80-90 年代的研究者們試着使用隨機梯度下降算法和後向傳播來訓練深度網絡。 不幸的是, 除了較少的特殊架構神經網絡, 別的都沒能獲得成功。 這些神經網絡的確能進行學習,但是非常緩慢, 在實踐當中, 因爲太過於緩慢沒法發揮作用。

從 2006 年以後, 一系列深度網絡學習成爲可能的技巧被開發出來。 這些深度學習的技巧是基於隨機梯度下降算法和後向傳播算法的, 但是也引進了一些新的想法。 這些技巧使得更深(也更大)的神經網絡有可能被成功訓練。 人們現在會例行公事一般地訓練至少擁有 5-10 個隱藏層的神經網絡。 而且, 這些網絡在很多問題的表現上比淺層次的神經網絡表現要好得多。 原因當然是深度網絡有能力構建起很複雜的層級概念。 這就有點像傳統的編程語言使用模塊化的設計和抽象的思想, 以構建複雜的計算機程序。 把深度網絡和淺層網絡比較就像在比較一門能夠調用底層語言編寫函數的高級語言與底層語言一樣。 傳統編程語言的抽象在神經網絡中以另外一種形式體現了出來, 但是它依舊無可替代的重要。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章