文章導讀:
1. 本書內容
2. 手寫字體識別
3. 感知機
4. Sigmoid神經元
5. 神經網絡的結構
6. 一個用於手寫數字識別的簡單神經網絡
7. 梯度下降學習算法
8. 數字識別神經網絡的實現
9. 關於深度學習
深度學習算是現在機器學習領域非常熱門的方向了,雖然一直有了解並且簡單用過,但是對於其中的詳細原理和來龍去脈都是略知一二,於是一直想系統學習一下該領域的相關知識。《Neural Networks and Deep Learning》是一份非常好的入門材料,講解詳細而且不光是介紹了理論知識,更重要的是介紹了每一步的來龍去脈以及為什么要這樣做。在線文檔是英文版的,我這份總結筆記的很大部分是結合原文根據自己的理解加以提煉翻譯過來的,英文水平有限,出現問題請指正。想看原版完整文檔的同學可以點擊上面的鏈接。
一. 本書內容
傳統的計算機算法在解決問題的時候,通常都是由程序員制定好規則將問題進行分解,一步步進行解決。應用神經網絡我們一般並不需要告訴計算機應該怎么去解決問題,而是只要給到足夠的觀測數據就可以了,它將會自動從這些數據中提取出解決方法。
本書主要的目的是幫助讀者掌握包括深度學習相關技術在內的神經網絡領域的核心知識。在掌握了本書內容后,可以使用深度學習模型解決遇到的問題,更進一步可以設計自己的神經網絡用以解決特定的問題。
當然這本書也不會完全是理論知識,作者通過“手寫數字識別“這個常見但是普通編程方法很難解決的問題介紹了神經網絡的基本知識。通過這本書,他還基於python一步一步實現了一個簡單的神經網絡庫,使得大家在接觸到其他新的神經網絡庫的時候也能很快的理解並讀懂代碼。
二. 手寫字體識別
對於這樣一副圖片,人腦很容易就能識別出來其中的數字。但是對於計算機就沒有這么容易了,按照以前老的解決問題的思路,就是規則的堆砌,比如說“上部分有個圈,右下方有條垂直線,這個數字就是9”,很顯然,對於手寫字體這樣肯定是不切實際的。因為手寫數字太不規范了,不同人的寫法不一樣,很多規則並不通用,通過制定精確的規則來解決這個問題的可行性微乎其微。
神經網絡則不一樣,它能夠從訓練數據自動提煉出可以識別不同數字的“規則”,增加訓練數據量的話,這樣的識別結果通常就會更加准確。在這章的最后,我們會實現一個簡單的神經網絡程序,雖然代碼量不多,但是卻可以取得不錯的96%的准確率。在隨后的章節,我們會逐步優化我們的神經網絡,使它可以取得高達99%的准確率。
手寫數字識別問題很經典,不難理解,而且計算量也不大,在本書的最后,我們也會討論一下怎么講在這個問題上獲得的思想應用到其他的領域。
這章的重點當然不僅僅是實現神經網絡代碼,我們將會了解到很多重要的神經網絡基本概念,包括兩種最重要的神經元(感知機和sigmoid),神經網絡模型最基本的學習算法(隨即梯度下降算法)。作者通過大段的討論着重講解為什么要這樣做,培養大家對於神經網絡的直覺,使讀者可以對神經網絡有更深的理解,為后續理解深度學習打下扎實基礎。
三. 感知機
最簡單的感知機模型如下:
其中$x_1, x_2, x_3...$為模型的輸入,可以是0或1的非數值型變量,也可以是數值型變量,其中每個輸入維度上都有對應的一個權重$w_1, w_2, w_3$,模型的輸出是由$\sum_jw_jx_j$與閾值的大小關系決定的,等價於公式:
$$output = \left\{\begin{aligned} 0 &\quad if \sum_jw_jx_j \leqslant threshold \\ 1 &\quad if \sum_jw_jx_j > threshold \end{aligned}\quad(1)\right.$$
通過這個公式可以看到,感知機的作用很簡單,就是對輸入進行加權求和,然后和閾值對比。比閾值大,表示這個神經元受到激發了,輸出1,否則輸出0,這樣就相當於實現了一個簡單二分類器。
當然這樣簡單的一個神經元肯定無法滿足我們的需求,通常的神經網絡都是有許多個神經元組合而成的網狀結構,類似下圖這樣的:
這里的輸入是以箭頭的形式表示的,通常我們是將其表示為一個特殊的神經元:
該神經元沒有輸入,只有一個固定的輸出就等價於上面input中的一個維度。
其次這個神經網絡是多層的,上一層的輸出作為下一層的輸入。注意到,每個神經元其實只有一個輸出,但是上面圖里面有些神經元卻有多個輸出箭頭,這其實是為了直觀的表示這個神經元的輸出會作為后一層多個神經元的輸入,也就是被使用了多次。
除此之外,我們通常的表示方法並不使用上文中閾值的表達形式,而是使用類似於回歸分析中的intercept的表達形式。我們設定$bias$,$b = -threshold$,這樣的話公式(1)就可以被寫為$$output = \left\{\begin{aligned} 0 &\quad if w*x+b \leqslant 0 \\ 1 &\quad if w*x + b > 0 \end{aligned}\quad(2)\right.$$
其中$bias$可以理解為表示該神經元被激發的難易程度,$bias越大$,越容易被激發。
四. Sigmoid神經元
在介紹第二種重要的神經元sigmoid之前,我們先講一下學習算法的設計思路。假設我們現在有一個用來解決手寫數字識別問題的基於感知機的神經網絡,輸入是手寫圖片展開后的特征向量,輸出是識別的數字,但是其中的參數(weights和bias)是未知的。我們想做的就是通過學習獲得合適的參數使得這個網絡能夠正確的區分手寫數字。
為了理解學習算法是怎么進行的,假設我們對一些weight或者bias引入很小的變化,使得這些小變化會對神經網絡的輸出也產生對應的很小的變化,如下圖所示:
基於這樣的假設,我們可以逐步的調整參數使得神經網絡可以朝着我們需要的方向進行優化,也就是說它對手寫數字的識別越來越准確。例如,在學習過程中,神經網絡將“9”錯分為“8”,我們可以稍微調整weights和biases使得它更偏向於將這個“9”正確分為“9”。我們重復這樣的步驟,直到模型對數字的識別越來越准確。
但是由於感知機的輸出非0即1,任何一個感知機的參數的細微變化都有可能導致該感知機的輸出在0,1上不停的跳變,這樣就會導致后續的網絡結構的輸出可能產生非常大的變化。也就是說,可能這個“9”模型正確識別了,卻會導致其他數字的識別發生非常大的改變,這樣就導致我們的算法很難收斂,很難通過逐漸改變參數進行優化這樣的方法去得到一個穩定的准確的模型。
通過引入sigmoid神經元就可以克服這個問題。sigmoid神經元和感知機類似,但是不同的是,修改它的參數對輸出只會造成很小的影響,不會出現0到1這樣截然不同的變化,因為sigmoid函數是一個連續可導的函數。
sigmoid函數:
$$\sigma(z)=\frac{1}{1+e^{-z}}\quad(3)$$
代入輸入$x_1,x_2,...,$,權重$w_1,w_2,...$和偏差$b$,上式可以寫為:
$$\frac{1}{1+exp(-\sum_jwjxj-b)}\quad (4)$$
雖然從公式上看感知機和sigmoid貌似相差很多,但其實它們兩者有很多相同點,為了理解這些,我們假設$z=w*x+b$,當$z\rightarrow+\infty$時,有$e^{-z}\approx 0$和$\sigma(z)\approx 1$,sigmoid函數達到最大值。另一方面,當$z\rightarrow-\infty$時,有$e^{-z}\approx +\infty$和$\sigma(z)\approx 0$,sigmoid函數達到最小值,這兩種特殊情況都是等價於感知機的情形的。sigmoid和感知機的不同發生在$z=w*x+b$值較小的情況,通過它們的函數圖像可以更直觀的看到這點。
感知機的激活函數是一個階躍函數:
可以看到感知機的激活函數在0處是不可導的,而sigmoid可以看作是一個平滑版的感知機,它的函數處處可導連續。sigmoid函數的平滑性意味着參數的細微改變$\Delta w_j$和$\Delta b$會對輸出產生一個細微的變化$\Delta output$, 利用微分的概念可以近似得到
$$\Delta output \approx \sum_j\frac{\partial output}{\partial w_j}\Delta w_j+\frac{\partial output}{\partial b}\Delta b\quad (5)$$
上面的式子看起來有點復雜,但是它表明一個簡單的事實,就是$\Delta output$和參數改變量$\Delta w_j$和$\Delta b$的線形聯系。這就使得我們可以很容易的找到合適的參數變化量來實現我們需要的在輸出值上的改變。
在后面的章節中,我們會看到sigmoid也只是眾多激活函數中的一種,還存在其他很多合適的激活函數,但是目前來說這些都不重要。對於我們的公式(5)來說,改變激活函數只是在改變它對於變量的偏導數,並不影響我們對於流程的理解。我們先講解sigmoid函數只是指數函數在求導時的簡潔屬性,而且sigmoid也是使用最廣泛的激活函數。
不像感知機的輸出0和1,能夠很直接的表示一個數字是與否,sigmoid函數的輸出是[0,1]區間上的連續實數,但是注意到$\sigma(z)$關於$z$在$(0,0.5)$處是一個非常完美的中心對稱形式,我們很容易想到用0.5作為判斷sigmoid函數輸出的閾值,例如當其輸出不小於0.5時表示“9”,小於0.5時表示它不是“9”。熟悉邏輯回歸的話,也能想到[0,1]的值域還可以當作是一個概率空間。
練習:
問題一:
對於一個感知機網絡,假設我們把它所有的$w_j$和$b$同時乘上一個正數$c>0$,證明這個感知機網絡並沒有改變。
答案:
感知機的激活函數為比較$\sum_jw_jx_j+b$和0的大小(大於0輸出1,小於0輸出0),乘以一個正數$c$以后,$\sum_jcw_jx_j+cb = c(\sum_jw_jx_j+b)$,並不影響它跟0的相對大小關系,當然也不會影響感知機的輸出。
問題二:
對於一個感知機網絡,對於給定的輸入$x$,假設對於其中每個感知機神經元都有$w*x+b\neq 0$.假如我們把所有的感知機神經元替換成sigmoid神經元,然后將所有$w_j$和$b$乘一個正數$c>0$。證明當$c\rightarrow+\infty$時,這些sigmoid模型等價於感知機模型,如果存在一個神經元對當前輸入有$w*x+b=0$呢?
答案:
首先我們知道根據公式(2),感知機的輸出為0和1,對於任意sigmoid函數,有$\frac{1}{1+exp(-c*wx-c*b)}=\frac{1}{1+exp(c*(-wx-b))}$,然后如果$wx+b>0$,由於$c\rightarrow +\infty$,則有$\sigma \rightarrow \frac{1}{1+0}=1$,如果$wx+b<0$,則有$\sigma \rightarrow \frac{1}{1+\infty}=0$,和感知機模型的行為一樣!顯然如果$wx+b=0$,則沒有這樣的結論,因為$\sigma = \frac{1}{1+1} = 0.5$和感知機情形不符合。
五. 神經網絡的結構
下一節中,我們將會引入一個處理手寫數字識別的神經網絡,但在這之前,我們先解釋神經網絡中常用的一些概念。對於這樣一個神經網絡:
其中最左邊的一層被稱為輸入層,最右邊的一層稱為輸出層,其中中間的一層是隱藏層,這里只有一層隱藏層,但是隱藏層是可以有很多層的。
輸入層和輸出層的結構通常可以根據具體問題直接得到。比如說我們要判斷一副手寫數字圖片表示的是“9”還是不是。最簡單的方法,就是把圖片的像素展開為一維向量,也就是對於64*64=4096的圖片,輸入層就是4096個神經元,輸出層只有一個神經元,因為只要判斷是否是“9”。
但是對於隱藏層就沒有這么簡單了,通常很難總結出設計隱藏層的通用方法。於是神經網絡研究者就通過啟發式的方法開發了許多隱藏層的設計。比如,一些啟發式的想法就是通過考慮神經網絡的訓練時間來權衡隱藏層的數量。我們將在本書的后面見到其他的一些啟發式的設計。
到目前為止,我們討論的神經網絡都是上一層的輸出作為后一層的輸入。這種神經網絡也被稱為前饋(feedback)神經網絡,意味着在這種網絡中不存在循環結構。輸出不能通過反饋影響到輸入。
然而也有存在含有反饋的循環神經網絡(recurrent neural networks RNN)。這種模型的設計是考慮到一個神經元受到激發后,激發狀態並不會隨着輸入的改變而立刻變化,而是會存在一段時間,通過反饋影響到輸入。(RNN的詳細情況,我目前也不是很清楚,以后再專門介紹一下。)
RNN相對於前饋神經網絡來說應用沒有那么廣泛,對於RNN的學習算法不夠成熟。但是RNN還是非常有意義的,因為相對於前饋神經網絡來說,它更符合我們人腦的行為。不過,本書還是更關注前饋神經網絡多一點。
六. 一個用於手寫數字識別的簡單神經網絡
在定義了神經網絡后,我們回到手寫數字識別這個問題上。實際中,這個問題其實包含兩個字問題,(1)對一串數字進行分割成一個一個的數字,也就是說確定一串數字中,數字與數字之間的邊界。(2)對每個分割出的數字進行判斷,識別出它表示哪個數字。
第一個問題不是我們的重點,我們主要關注第二個問題,而且我們的訓練數據MNIST也是分割好的數字,一張圖片就是一個數字。 為了解決這樣一個識別問題,我們使用一個三層的神經網絡:
由於MNIST數據都是28*28=784的圖片,所以這里我們使用的神經網絡的輸入層也有784個神經元組成(圖中只畫出了部分),每個維度輸入值的都是該像素點的灰度值,0表示白,1表示黑,0到1之間的小數表示不能灰度的灰色。
第二層為網絡的隱藏層,我們定義它包含$n$個神經元,我們將會測試不同的$n$找出最佳的結構。圖中的例子顯示的是一個比較小的隱藏層結構,只有$n=15$個神經元。
輸出層由10個神經元組成,顯然對應的是0-9的10個數字。假如第一個神經元被激發,輸出1,表示這個數字是0,第二個神經元被激發則表示是1,以此類推。具體的說,它們的輸出是[0,1]上的實數,可能會出現多個神經元被激發的情況,我們取輸出最大的那個神經元代表的數字作為輸出結果。
可能有人會有疑問,是不是只需要四個神經元就夠了,因為類似於二進制編碼,$2^4=16>10$,四個神經元就足夠可以表示10種不同的情形了。為什么我們要用10個呢?這樣是不是顯得效率不夠高?其實最終作出使用10個神經元的決定也是經驗性的,我們可以嘗試不同的結構,最后發現,對於這個問題,10個神經元作為輸出層的模型要好於4個神經元作為輸出層的模型。
但是為什么會出現這樣的情況呢?是否存在直覺能提前告訴我們,我們應該使用10個而不是4個嗎?
為了理解為什么這樣做,我們思考一下這里神經網絡是怎么運行的。先考慮使用10個神經元的輸出。對於第一個輸出神經元,也就是決定數字是否為0的那個輸出神經元,它所做的是對隱藏層的輸出結果進行加權求和。那么隱藏層呢,設想隱藏層第一個神經元是為了檢測圖片中是否含有這樣的模式:
這個神經元可以通過對這些區域的像素點給予大的權重,而對其他區域的像素點給予小的權重實現這樣的功能。同樣的,設想隱藏層中的第二個,第三個,第四個神經元分別可以檢測下面這些圖片特征:
可以看到,這個例子,也就是這四個特征剛好組成了0這個數字:
所以如果這幾個隱藏層的神經元都被激發的話,我們就可以判斷這個數字是0了。當然,我們判斷是0的特征並不是只有這些,實際中0的寫法也是多種多樣的。
這樣的話似乎解釋了為什么使用10個輸出神經元比4個輸出神經元效果好。因為假如用4個輸出的話,就不大好將數字的特征和最終的輸出聯系起來。
不過這其實也是一種啟發式的想法。並沒有什么證據證明這個三層的神經網絡必須按照這種方式進行數字的識別:一個隱藏神經元識別圖像的一部分特征形狀。也許會存在更好的學習算法使得4個神經元輸出的神經網絡一樣可以達到很好的效果。
練習:
如下圖所示,通過在原有三層神經網絡的基礎上再加上一個新的由四個神經元組成的輸出層,就可以實現數字的二進制表達形式的輸出,假設這個三層神經網絡識別准確度非常高,也就是正確數字所在的神經元的激活值不小於0.99,其他神經與激活值小於0.01,試找出最后一層的weights和bias
答案:
這個題目答案並不唯一,考慮到可以這樣:
$0\rightarrow 0001, 1\rightarrow 0010, 2\rightarrow 0011...$,然后發現最后一層第一個神經元為1的時候對應的數字有7,8,9,第二神經元位1的時候對應的數字有3,4,5,以此類推。然后按照這些規律把對應有聯系的地方的權重設置為1,沒聯系的可以設置為0,再稍微調節bias就可以了,這里就不具體算了。
七. 梯度下降學習算法
在設計好了我們的神經網絡之后,它怎樣才能識別數字呢?首先需要通過訓練集進行學習。這里我們使用MNIST數據集。MNIST大家應該都了解過,都是28*28=784像素的圖片。
這里我們定義訓練輸入為$x$,長度為784的一維向量。定義$y=y(x)$為輸入為$x$下正確的輸出,是一個10維的向量。例如:對於數字為6的一副圖片,$y(x) = (0,0,0,0,0,0,1,0,0,0)^T$.
我們想要的是算法可以幫我們找到合適的weights和biases使得我們神經網絡的輸出可以盡可能的接近它的真實值。定義一個損失函數:$$C(w, b) = \frac{1}{2n}\sum_x||y(x)-a||^2\quad (6)$$
這里$w$表示所有權重,$b$表示所有偏差,$n$是訓練數據集的大小,$a$是輸入為$x$時我們的神經網絡的輸出。我們的目的就是找到合適的參數使損失函數的值盡量小。我們選擇這個二次損失函數,而不是考慮正確分類的數量主要是因為這個函數是平滑的,連續可導。我們隨后也會做一些調整,使用其他的平滑函數。
為了解決這個最小化的問題,接下來先介紹梯度下降算法。
假設我們想要最下華函數$C(v)$,函數的參數是$v=v_1,v_2,...$當然這些參數可以是任意的,為了直觀表現我們假設有兩個參數,圖像如下:
這個函數圖像很簡單,直接就能看出來就是曲面的谷底就是最小值所在的地方。但是實際的情況很定存在很多各種各樣非常復雜的函數,不能都能靠肉眼就能直接找出最小值。
另一種方法就是用解析的方法去對函數求導,然后再去求導數為0的解析解,也就是函數極值所在的地方。這個看上去很完美,因為找到的可以是全局的最優解,但是實際情況是函數過於復雜,或者變量非常多,根本就沒法找到解析解,這種方法實際上是不可行的。
那就換個思路,類似於上圖中那樣,我們把函數想象成一個山谷。我們有一個球在山谷里滾動,慢慢的就會滾落到谷底了,也就是函數的極值點。也就是說,我們假設一個初始點,讓這個點模仿這個球的運動在函數上移動,然后就可以找到這樣的極值點了。
要實現這樣的目的,我們就可以對函數求導,函數的導數值就可以表明這個山谷每處的形狀,是上升的還是下降的還是平的,對應的就是導數值大於0,小於0,等於0的地方。
假設我們讓球沿着$v_1$方向移動$\Delta v_1$,沿着$v_2$方向移動$\Delta v_2$。我們得到函數$C$的值的變化為$$\Delta C \approx \frac{\partial C}{\partial v_1}\Delta v_1 + \frac{\partial C}{\partial v_2}\Delta v_2 \quad (7)$$
因為我們的目的是找到$C$的最小值,當然就希望每次的$\Delta C$是負值。設$\Delta v = (\Delta v_1, \Delta v_2)^T$,定義函數$C$在方向$v_1,v_2$上的梯度為
$$\bigtriangledown C= (\frac{\partial C}{\partial v_1}, \frac{\partial C}{\partial v_2})^T \quad (8)$$.
於是我們可以將公式(7)改寫為:$$\Delta C \approx \bigtriangledown C * \Delta v\quad (9)$$
這個公式也解釋了為什么將$\bigtriangledown C$稱為梯度向量,它聯系了$v$的變化與$C$的變化。
接下來回到上面我們的問題,怎么移動$v$,也就是怎么對$\Delta v$取值,才能保證$\Delta C$是負數。不妨取$$\Delta v = -\eta \bigtriangledown C\quad (10)$$
其中$\eta$是一個很小的正數(通常被稱為學習率)。然后$\Delta C \approx -\eta\bigtriangledown C \cdot \bigtriangledown C = -\eta||\bigtriangledown C||^2$,這就可以保證$\Delta C\leqslant 0$, $C$就可以朝着減小的方向一直優化下去。公式(10)即是我們需要的"運動規則",我們每次將位置為$v$的小球移動到:$$v \rightarrow v' = v - \eta \bigtriangledown C \quad (11)$$
總的來說,梯度下降算法,就是我們不停的計算當前位置的梯度$\bigtriangledown C$, 然后朝着相反的方向移動,直到下降到谷底,類似於下圖:
當然梯度下降並不是真實的物理運動,現實中,運動是存在慣性的,以后也會看到有的時候我們也會模擬慣性去解決局部最優的問題。不過這里,我們的選擇就是一路朝的低谷運動,到了就停下,不考慮慣性的影響。
學習率$\eta$的選擇對算法有着很重要的影響,$\eta$必須足夠小才能使公式(9)近似成立,如果$\eta$過大的話會導致$\Delta C>0$,而如果過小的話,又會導致每次梯度下降的過於緩慢。所以實際中,$\eta$是一個變量使得公式(9)成立,並且算法的效率可以接受。
上面介紹2維的情形只是方便從圖像上進行直觀的理解,我們可以很容易的將其推廣到多維的情形。假設$C$是關於$m$個變量$v_1,v_2,...,v_m$的函數,於是有$$\Delta C \approx \bigtriangledown C\cdot \Delta v \quad (12)$$,類似於2維的情形,其中$\Delta v = (\Delta v_1, ..., \Delta v_m)^T$, 梯度$\bigtriangledown C$為:$$\bigtriangledown C = (\frac{\partial C}{\partial v_1},...,\frac{\partial C}{\partial v_m})^T \quad (13)$$
然后我們選擇$$\Delta v = -\eta \bigtriangledown C \quad (14)$$
每次$v$的變化如下$$v \rightarrow v' = v - \eta \bigtriangledown C \quad (15)$$
這個公式也可以被認為是梯度下降算法的定義,它提供了一種迭代的改變$v$的位置去尋找最小值的方法。但是它無法保證總能找到全局最小值,后面的章節會討論這點。
梯度下降算法尋找最小值的優化策略還有另外一種理解方式。假設現在需要在某個方向上移動$\Delta v$使得$C$可以減小最多。因為$\Delta C$是負數,所以等價於最小化$\Delta C \approx \bigtriangledown C \cdot \Delta v$,假設每次移動的距離為一個固定值$||\Delta v||=\epsilon>0$,能夠證明當$\Delta v = -\eta \Delta C$,其中$\eta = \epsilon / ||\bigtriangledown C||,||\Delta v||=\epsilon$時,$\bigtriangledown C \cdot \Delta v$達到最小值。於是梯度下降可以被認為是不斷的朝着使得該位置$C$處減小最多的方向移動一個很小的距離的過程。
練習:
問題一:
證明上面最后一段中梯度下降算法的另一種描述。提示:柯西不等式
答案:
這里其實用不用柯西不等式無所謂,根據高中數學也能理解,兩個向量相乘,如果向量的模都是固定值的話,當這兩個向量方向相反的時候,乘積最小。
$\Delta C \approx \bigtriangledown C \cdot \Delta v = ||\bigtriangledown C|| \cdot ||\Delta v|| \cdot cos\theta$ 其中$cos\theta$是兩個向量的夾角的余弦值,而且$||\bigtriangledown C||$和$||\Delta v||$是固定值所以當$cos\theta = -1$時,值最小,此時有兩個向量方向相反,即$\frac{\Delta v}{||\Delta v||} = -\frac{\bigtriangledown C}{||\bigtriangledown C||}$,於是有$\Delta v = -||\Delta v||\cdot \frac{\bigtriangledown C}{||\bigtriangledown C||} = -\epsilon \cdot \frac{\bigtriangledown C}{||\bigtriangledown C||} = -\eta \bigtriangledown C$其中$\eta = \epsilon/||\bigtriangledown C||$
問題二:
作者解釋了梯度下降算法在二維和多維的情形,給出在一維情形下類似的定義
答案:
這個基本是一樣了,無非二維是三維圖上的最低點,一維就是二維圖上的最低點,類似於一條二次曲線,最小值在導數為零的谷底,一維就是直接對自變量求導數,而多維是對每個自變量求偏導數獲得梯度,迭代的過程中都是朝着導數的反方向移動。
梯度下降算法有很多變種,其中有些更接近真實的物理運動。但是通常情況下,這些算法需要計算$C$對於各個自變量的二階導數,這個運算是非常耗時的,於是便有了各種擬牛頓法等解決這類問題的技巧,不過作為一本入門書,本書中基本只使用梯度下降算法。
公式(15)應用到神經網絡的學習中,得到如下公式:
$$w_k \rightarrow w'_k = w_k - \eta \frac{\partial C}{\partial w_k}\quad (16)$$
$$b_l \rightarrow b'_l = b_l - \eta \frac{\partial C}{\partial b_l} \quad (17)$$
然后不斷地應用這個公式對神經網絡的各個參數進行迭代直到達到停止條件。
讓我們回到損失函數公式(6),可以發現這個損失函數用到了所有的訓練樣本,求它們的損失的平均值。然后在求導數的時候,也會求平均值,即$\bigtriangledown C = \frac{1}{n}\sum_x\bigtriangledown C_x$,即每次迭代都會用到所有的樣本,當訓練樣本過大的時候,就會導致學習的過程非常漫長。
於是便產生了隨機梯度下降(Stochastic gradient descent)這種算法。它的思想是在每次迭代過程求梯度的過程中只用到了隨機選擇的部分訓練樣本,達到加速訓練的目的。
比如它每次隨機選擇$m$個訓練樣本:$X_1, X_2, ..., X_m$,稱其為mini-batch。$m$的選擇通常是使$\bigtriangledown C_{X_j}$和$\bigtriangledown C_x$近似相等,即:
$$\frac{\sum^m_{j=1}\bigtriangledown C_{X_j}}{m}\approx \frac{\sum_x\bigtriangledown C_x}{n} = \bigtriangledown C\quad (18)$$
$$\bigtriangledown C \approx \frac{1}{m}\sum^m_{j=1}\bigtriangledown C_{X_j} \quad (19)$$
這樣就保證了參數更新時的合理性。
於是公式(16),(17)就變成了
$$w_k \rightarrow w'_k = w_k - \frac{\eta}{m} \sum_j\frac{\partial C_{X_j}}{\partial w_k} \quad (20)$$
$$b_l \rightarrow b'_l = b_l - \frac{\eta}{m} \sum_j\frac{\partial C_{X_j}}{\partial b_l} \quad (21)$$
每次只使用了一次mini-batch中的$m$個樣本,當我們遍歷玩所有訓練樣本的時候,相當於完成了一次epoch,需要的話,可以按照同樣的方法進行下一次epoch。
注意到求不求平均其實影響不大,很多情況下前面的$\frac{1}{n},\frac{1}{m}$並不會造成多大的影響,因為我們可以通過增大或者減小學習率來抵消掉影響。
隨機梯度下降算法雖然會存在一定的統計上的波動,但是我們關心的只是下降的方向,並不是梯度的准確值。隨機梯度下降算法在實際中是一種應用最為廣泛神經網絡的優化技術。
練習:
問題:
梯度下降的一個極端版本是采用大小為1的mini-batch。也就是說對每個訓練樣本都要更新一次模型參數。這種方式被稱為online learning。說出這種方式與大小為2的mini-batch的隨機梯度下降算法相比較的一個優點和一個缺點。
答案:
先說優點吧,online learning使用更為靈活,在數據量不足時不需要考慮冷啟動的問題,而且每次更新模型只要考慮當前輸入的一個樣本,計算簡單。而且模型無時無刻都在更新,因此可以處理復雜的真實情況。
缺點,我覺得可能是對異常值敏感,算法可能在有些情況不穩定。我也沒怎么研究過這一塊,不是很清楚。
八. 數字識別神經網絡的實現
接下來就是我們親手去實現代碼這塊了,在此之前先把MNIST數據下載下來。
git clone https://github.com/mnielsen/neural-networks-and-deep-learning.git
不同於一開始說的60000張訓練圖片和10000張測試圖片,我們將訓練圖片分為50000張訓練集和10000張驗證集。這一章我們先不用驗證集,在后面的章節我們將使用它來選擇“超參”。
先來看一下神經網絡類的初始化代碼:
1 class Network(object): 2 3 def __init__(self, sizes): 4 self.num_layers = len(sizes) 5 self.sizes = sizes 6 self.biases = [np.random.randn(y, 1) for y in sizes[1:]] 7 self.weights = [np.random.randn(y, x) 8 for x, y in zip(sizes[:-1], sizes[1:])]
這里sizes是一個列表,包含了每一層的神經元個數。所以如果我們想要創建一個第一層油2個神經元,第二層有3個神經元,第三層有一個神經元的神經網絡的話,可以這樣調用構造函數:
net = Network([2, 3, 1])
6,7行的代碼是對參數進行初始化作為我們接下來梯度下降算法的開始點。注意到第一層沒有進行bias的初始化,因為我們這里默認第一層是輸入層,不需要bias。
注意到weights和biases都是由矩陣組成的list,例如net.weights[1]就是第二層到第三層神經元鏈接的權重。不妨定義該矩陣為$w$,其中$w_{jk}$表示第二層第k個神經元和第三層第j個神經元鏈接的權重。那么第三層的輸出值向量為:$$a' = \sigma (wa+b)\quad (22)$$其中a為第二層的輸出。
練習:
問題:
給出公式(22)的分量形式
答案:
對於第三層中的第j個神經元,它的輸出為:
$$a'_j = \sigma (\sum^K_{k=1}(w_{jk} * a_k) + b_j)$$
其中K為第二層神經元的個數。
有了這些理解,很容易就能寫出計算神經網絡輸出的代碼:
1 def sigmoid(z): 2 return 1.0/(1.0+np.exp(-z))
隨后是feedforward函數,用來在給定輸入a的情況下,計算神經網絡最終的輸出:
1 def feedforward(self, a): 2 """Return the output of the network if "a" is input.""" 3 for b, w in zip(self.biases, self.weights): 4 a = sigmoid(np.dot(w, a)+b) 5 return a
接下來就是算法學習的部分了,梯度下降算法的實現:
1 def SGD(self, training_data, epochs, mini_batch_size, eta, 2 test_data=None): 3 """Train the neural network using mini-batch stochastic 4 gradient descent. The "training_data" is a list of tuples 5 "(x, y)" representing the training inputs and the desired 6 outputs. The other non-optional parameters are 7 self-explanatory. If "test_data" is provided then the 8 network will be evaluated against the test data after each 9 epoch, and partial progress printed out. This is useful for 10 tracking progress, but slows things down substantially.""" 11 if test_data: n_test = len(test_data) 12 n = len(training_data) 13 for j in xrange(epochs): 14 random.shuffle(training_data) 15 mini_batches = [ 16 training_data[k:k+mini_batch_size] 17 for k in xrange(0, n, mini_batch_size)] 18 for mini_batch in mini_batches: 19 self.update_mini_batch(mini_batch, eta) 20 if test_data: 21 print "Epoch {0}: {1} / {2}".format( 22 j, self.evaluate(test_data), n_test) 23 else: 24 print "Epoch {0} complete".format(j)
先看輸入參數,有訓練數據,epoch次數,mini-batch的大小,學習率eta,test_data控制是否需要在每次epoch后評價一下當前模型。
每次epoch時,現將訓練數據打散分配到每個mini-batch起到隨機抽樣的效果,隨后利用eta和當前mini-batch中的數據更新模型參數。這個更新過程就是梯度下降算法的更新過程:
1 def update_mini_batch(self, mini_batch, eta): 2 """Update the network's weights and biases by applying 3 gradient descent using backpropagation to a single mini batch. 4 The "mini_batch" is a list of tuples "(x, y)", and "eta" 5 is the learning rate.""" 6 nabla_b = [np.zeros(b.shape) for b in self.biases] 7 nabla_w = [np.zeros(w.shape) for w in self.weights] 8 for x, y in mini_batch: 9 delta_nabla_b, delta_nabla_w = self.backprop(x, y) 10 nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)] 11 nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)] 12 self.weights = [w-(eta/len(mini_batch))*nw 13 for w, nw in zip(self.weights, nabla_w)] 14 self.biases = [b-(eta/len(mini_batch))*nb 15 for b, nb in zip(self.biases, nabla_b)]
其中大部分的工作其實是由backprop這個函數完成的,也就是我們下一章要講的反向傳播算法。它可以快速的計算損失函數的梯度,剩下的工作只是對當前mini-batch里的訓練數據計算梯度值,然后再更新模型參數。完整的代碼可以從上面那個git命令得到。
有了這些代碼,通過訓練數據訓練出模型后再應用到測試數據上,根據作者給的參數:
net = network.Network([784, 30, 10])
net.SGD(training_data, 30, 10, 3.0, test_data = test_data)
運行了一下結果如下:

Epoch 0: 8335 / 10000 Epoch 1: 8424 / 10000 Epoch 2: 8482 / 10000 Epoch 3: 8493 / 10000 Epoch 4: 8551 / 10000 Epoch 5: 8552 / 10000 Epoch 6: 8554 / 10000 Epoch 7: 8572 / 10000 Epoch 8: 8595 / 10000 Epoch 9: 8602 / 10000 Epoch 10: 8597 / 10000 Epoch 11: 8613 / 10000 Epoch 12: 8611 / 10000 Epoch 13: 8603 / 10000 Epoch 14: 8582 / 10000 Epoch 15: 8628 / 10000 Epoch 16: 8613 / 10000 Epoch 17: 8614 / 10000 Epoch 18: 8614 / 10000 Epoch 19: 8614 / 10000 Epoch 20: 8611 / 10000 Epoch 21: 8613 / 10000 Epoch 22: 8807 / 10000 Epoch 23: 9458 / 10000 Epoch 24: 9486 / 10000 Epoch 25: 9464 / 10000 Epoch 26: 9482 / 10000 Epoch 27: 9445 / 10000 Epoch 28: 9487 / 10000 Epoch 29: 9497 / 10000
雖然開始時並沒有作者運行時的准確率高,不過最后還是達到了95%左右的不錯的准確率。僅僅是第一次測試就有這樣的准確率已經很不錯了,試着將隱藏層的神經元個數改為100,結果獲得了提升到了96%左右,但是運行速度也慢了好多。
除了隱藏層的神經元個數,還有很多參數可以調節。通常來說,給神經網絡調參是一件富有挑戰的事,特別是當你初始參數選的很糟糕的時候。但是這門課程會讓我們知道這其實並不是很重要。我們需要去培養一些直覺,一些如何去選擇超參和合適的模型結構的直覺。我們將會在本書中繼續討論上述參數是怎么選擇出來的。
練習:
問題:
利用代碼創建一個只有兩層的神經網絡,即只有輸入輸出層,訓練后進行測試得到了多少准確率?
答案:
net = network.Network([784, 10])
net.SGD(training_data, 30, 10, 3.0, test_data=test_data)
我測試的是准確率下降了,在84%左右。
之前提到我們的神經網絡算法在測試集上獲得了不錯的准確率94%,那么這個比較的基准是什么呢?如果是胡亂猜,那就是10%的准確率。還有一種是根據圖片中平均灰度值,這個有人試驗過獲得了22.25%的准確率,相比10%已經提高了很多了。其他還有很多可以提高准確率的想法,但是一般要提高到50%以上就要依靠機器學習算法了。SVM直接用在原來的數據上大概獲得了94%的准確,也很不錯,而且在最優化的參數選擇下,SVM可以達到98.5%的非常高的准確率。不過神經網絡也有很大的改進余地,最好的結果更是高達99.79%,而且算法的改進也並不復雜,涉及的都是這篇文章提到的概念。作者也給出了這樣一句話,對於很多問題來說:
sophisticated algorithm ≤ simple learning algorithm + good training data
九. 關於深度學習
雖然我們的神經網絡在數字識別這個問題上獲得了很不錯的表現,但是它還是有一些神秘。因為無論是weights還是biases都是自動學習得到的,對於模型我們沒辦法給出一個合理性的解釋。是否存在一些途徑使我們理解神經網絡究竟是使用哪些原則進行數字識別的呢,如果知道了這些原則,是否可以獲得更高的准確率?
為了回答這些問題,我們先回到一開始神經元的解釋,就是一個對多種跡象(輸入)進行加權求和的過程。假設我們想要判斷下面的圖片中是否存在人臉:
我們可以采用類似於解決手寫字體識別方法解決這個問題,將圖片展開為多維向量作為神經網絡的輸入,然后輸出就一個可以判斷是否是臉的神經元。
不過不同的是,這次我們不用學習算法,我們人為設置權重和偏差。根據直覺我們將問題分解為多個子問題:左上是否有眼睛?右上是否有眼睛?中間是否有鼻子?等等。
然后如果有多個回答是“yes”,我們就認定這是一個臉,否則就不是。
不過這只是我們根據直覺考慮的,它存在很多問題。比如說圖片角度問題就會導致臉部變形,或者眼睛識別不准等等。然而直覺告訴我們,如果可以使用神經網絡解決這些子問題,那么我們也許就可以根據這些子問題的結果來解決人臉識別的問題,類似於下圖中結合多個子神經網絡:
不過這並不是人臉識別技術采用的方法,這里只是幫助我們建立神經網絡是如何運轉的直覺。
然后子神經網絡同樣可以被再次分解:
通過這樣一層層的神經網絡,就可以將問題分解的越來越小。神經網絡通過這種方法,在前面的層上解決一些簡單具體的問題,后面的層上解決更加復雜抽象的問題。這種一層層堆積,多層的結構被稱為深度神經網絡。
當然作者這里並沒有真的去手動設置神經網絡的各個參數,這些參數還是得靠學習算法決定。這里主要是讓大家理解神經網絡的層級這個概念,它一層層的結構對於這個模型意味着什么。
自2006年后,隨着一系列技術的發展,使得深度學習成為可能。在很多問題上,深度學習想較於淺層神經網絡獲得了很大的效果提升。原因當然是,深度網絡可以建立一個更加復雜的層級結構去解決復雜的問題。類似於我們在傳統編程語言中的模塊化設計和抽象設計去實現具有更加復雜功能的程序。
之后的文章可能不會像這篇一樣了,翻譯原文太長了,還是盡量精簡提煉一下吧。。。