神經元
神經元和感覺器本質上是一樣的,隻不過我們說感覺器的時候,它的激活函數是階躍函數;而當我們說神經元時,激活函數往往選擇為sigmoid函數或tanh函數。如下圖所示:

sigmoid函數的定義如下:
将其帶入前面的式子,得到
sigmoid函數是一個非線性函數,值域是(0,1)。函數圖像如下圖所示
sigmoid函數的導數是:
可以看到,sigmoid函數的導數非常有趣,它可以用sigmoid函數自身來表示。這樣,一旦計算出sigmoid函數的值,計算它的導數的值就非常友善。
神經網絡是啥
神經網絡其實就是按照一定規則連接配接起來的多個神經元。上圖展示了一個全連接配接(full connected, fc)神經網絡,通過觀察上面的圖,我們可以發現它的規則包括:
神經元按照層來布局。最左邊的層叫做輸入層,負責接收輸入資料;最右邊的層叫輸出層,我們可以從這層擷取神經網絡輸出資料。輸入層和輸出層之間的層叫做隐藏層,因為它們對于外部來說是不可見的。
同一層的神經元之間沒有連接配接。
第n層的每個神經元和第n-1層的所有神經元相連(這就是full connected的含義),第n-1層神經元的輸出就是第n層神經元的輸入。
每個連接配接都有一個權值。
上面這些規則定義了全連接配接神經網絡的結構。事實上還存在很多其它結構的神經網絡,比如卷積神經網絡(cnn)、循環神經網絡(rnn),他們都具有不同的連接配接規則。
計算神經網絡的輸出
神經網絡實際上就是一個輸入向量到輸出向量的函數,即:
根據輸入計算神經網絡的輸出,需要首先将輸入向量的每個元素的值賦給神經網絡的輸入層的對應神經元,然後根據式1依次向前計算每一層的每個神經元的值,直到最後一層輸出層的所有神經元的值計算完畢。最後,将輸出層每個神經元的值串在一起就得到了輸出向量。
接下來舉一個例子來說明這個過程,我們先給神經網絡的每個單元寫上編号。
如上圖,輸入層有三個節點,我們将其依次編号為1、2、3;隐藏層的4個節點,編号依次為4、5、6、7;最後輸出層的兩個節點編号為8、9。因為我們這個神經網絡是全連接配接網絡,是以可以看到每個節點都和上一層的所有節點有連接配接。比如,我們可以看到隐藏層的節點4,它和輸入層的三個節點1、2、3之間都有連接配接,其連接配接上的權重分别為w41,w42,w43。那麼,我們怎樣計算節點4的輸出值a4呢?
為了計算節點4的輸出值,我們必須先得到其所有上遊節點(也就是節點1、2、3)的輸出值。節點1、2、3是輸入層的節點,是以,他們的輸出值就是輸入向量本身。按照上圖畫出的對應關系,可以看到節點1、2、3的輸出值分别是x1,x2,x3。我們要求輸入向量的次元和輸入層神經元個數相同,而輸入向量的某個元素對應到哪個輸入節點是可以自由決定的,你偏非要把指派給節點2也是完全沒有問題的,但這樣除了把自己弄暈之外,并沒有什麼價值。
一旦我們有了節點1、2、3的輸出值,我們就可以根據式1計算節點4的輸出值a4:
上式的w4b是節點4的偏置項,圖中沒有畫出來。而w41,w42,w43分别為節點1、2、3到節點4連接配接的權重,在給權重wji編号時,我們把目标節點的編号j放在前面,把源節點的編号放在後面。
同樣,我們可以繼續計算出節點5、6、7的輸出值a5,a6,a7。這樣,隐藏層的4個節點的輸出值就計算完成了,我們就可以接着計算輸出層的節點8的輸出值y1:
同理,我們還可以計算出y2的值。這樣輸出層所有節點的輸出值計算完畢,我們就得到了在輸入向量
時,神經網絡的輸出向量
。這裡我們也看到,輸出向量的次元和輸出層神經元個數相同。
神經網絡的矩陣表示
神經網絡的計算如果用矩陣來表示會很友善(當然逼格也更高),我們先來看看隐藏層的矩陣表示。
首先我們把隐藏層4個節點的計算依次排列出來:
接着,定義網絡的輸入向量和隐藏層每個節點的權重向量。令
代入到前面的一組式子,得到:
現在,我們把上述計算的四個式子寫到一個矩陣裡面,每個式子作為矩陣的一行,就可以利用矩陣來表示它們的計算了。令
帶入前面的一組式子,得到:
在式2中,是激活函數,在本例中是sigmoid函數;w是某一層的權重矩陣;x是某層的輸入向量;a是某層的輸出向量。式2說明神經網絡的每一層的作用實際上就是先将輸入向量左乘一個數組進行線性變換,得到一個新的向量,然後再對這個向量逐元素應用一個激活函數。
每一層的算法都是一樣的。比如,對于包含一個輸入層,一個輸出層和三個隐藏層的神經網絡,我們假設其權重矩陣分别為w1,w2,w3,w4,每個隐藏層的輸出分别是a1,a2,a3,神經網絡的輸入為x,神經網絡的輸入為y,如下圖所示:
則每一層的輸出向量的計算可以表示為:
這就是神經網絡輸出值的計算方法。
神經網絡的訓練
現在,我們需要知道一個神經網絡的每個連接配接上的權值是如何得到的。我們可以說神經網絡是一個模型,那麼這些權值就是模型的參數,也就是模型要學習的東西。然而,一個神經網絡的連接配接方式、網絡的層數、每層的節點數這些參數,則不是學習出來的,而是人為事先設定的。對于這些人為設定的參數,我們稱之為超參數(hyper-parameters)。
接下來,我們将要介紹神經網絡的訓練算法:反向傳播算法。
反向傳播算法(back propagation)
我們首先直覺的介紹反向傳播算法,最後再來介紹這個算法的推導。當然讀者也可以完全跳過推導部分,因為即使不知道如何推導,也不影響你寫出來一個神經網絡的訓練代碼。事實上,現在神經網絡成熟的開源實作多如牛毛,除了練手之外,你可能都沒有機會需要去寫一個神經網絡。
我們以監督學習為例來解釋反向傳播算法。在《零基礎入門深度學習(2) - 線性單元和梯度下降》一文中我們介紹了什麼是監督學習,如果忘記了可以再看一下。另外,我們設神經元的激活函數f為sigmoid函數(不同激活函數的計算公式不同,詳情見反向傳播算法的推導一節)。
我們假設每個訓練樣本為(x,t),其中向量x是訓練樣本的特征,而t是樣本的目标值。
首先,我們根據上一節介紹的算法,用樣本的特征x,計算出神經網絡中每個隐藏層節點的輸出ai,以及輸出層每個節點的輸出yi。
然後,我們按照下面的方法計算出每個節點的誤差項:
對于輸出層節點,
其中,等号左邊是節點的誤差項,yi是節點i的輸出值,ti是樣本對應于節點i的目标值。舉個例子,根據上圖,對于輸出層節點8來說,它的輸出值是y1,而樣本的目标值是t1,帶入上面的公式得到節點8的誤差項應該是:
對于隐藏層節點,
其中,ai是節點i的輸出值,wki是節點到它的下一層節點k的連接配接的權重,是節點i的下一層節點k的誤差項。例如,對于隐藏層節點4來說,計算方法如下:
最後,更新每個連接配接上的權值:
類似的,權重的更新方法如下:
偏置項的輸入值永遠為1。例如,節點4的偏置項應該按照下面的方法計算:
我們已經介紹了神經網絡每個節點誤差項的計算和權重更新方法。顯然,計算一個節點的誤差項,需要先計算每個與其相連的下一層節點的誤差項。這就要求誤差項的計算順序必須是從輸出層開始,然後反向依次計算每個隐藏層的誤差項,直到與輸入層相連的那個隐藏層。這就是反向傳播算法的名字的含義。當所有節點的誤差項計算完畢後,我們就可以根據式5來更新所有的權重。
以上就是基本的反向傳播算法,并不是很複雜,您弄清楚了麼?
反向傳播算法的推導
反向傳播算法其實就是鍊式求導法則的應用。然而,這個如此簡單且顯而易見的方法,卻是在roseblatt提出感覺器算法将近30年之後才被發明和普及的。對此,bengio這樣回應道:
接下來,我們用鍊式求導法則來推導反向傳播算法,也就是上一小節的式3、式4、式5。
前方高能預警——接下來是數學公式重災區,讀者可以酌情閱讀,不必強求。
按照機器學習的通用套路,我們先确定神經網絡的目标函數,然後用随機梯度下降優化算法去求目标函數最小值時的參數值。
我們取網絡所有輸出層節點的誤差平方和作為目标函數:
其中,表示是樣本的誤差。
然後,我們用文章《零基礎入門深度學習(2) - 線性單元和梯度下降》中介紹的随機梯度下降算法對目标函數進行優化:
随機梯度下降算法也就是需要求出誤差ed對于每個權重wji的偏導數(也就是梯度),怎麼求呢?
觀察上圖,我們發現權重wji僅能通過影響節點j的輸入值影響網絡的其它部分,設netj是節點j的權重輸入,即
ed是netj的函數,而netj是wji的函數。根據鍊式求導法則,可以得到:
上式中,xji是節點i傳遞給節點j的輸入值,也就是節點的輸出值。
對于
的推導,需要區分輸出層和隐藏層兩種情況。
輸出層權值訓練
考慮上式第一項:
考慮上式第二項:
将第一項和第二項帶入,得到:
如果令
,也就是一個節點的誤差項是網絡誤差對這個節點輸入的偏導數的相反數。帶入上式,得到:
上式就是式3。
将上述推導帶入随機梯度下降公式,得到:
上式就是式5。
隐藏層權值訓練
上式就是式4。
——數學公式警報解除——
至此,我們已經推導出了反向傳播算法。需要注意的是,我們剛剛推導出的訓練規則是根據激活函數是sigmoid函數、平方和誤差、全連接配接網絡、随機梯度下降優化算法。如果激活函數不同、誤差計算方式不同、網絡連接配接結構不同、優化算法不同,則具體的訓練規則也會不一樣。但是無論怎樣,訓練規則的推導方式都是一樣的,應用鍊式求導法則進行推導即可。
神經網絡的實作
現在,我們要根據前面的算法,實作一個基本的全連接配接神經網絡,這并不需要太多代碼。我們在這裡依然采用面向對象設計。
首先,我們先做一個基本的模型:
如上圖,可以分解出5個領域對象來實作神經網絡:
network 神經網絡對象,提供api接口。它由若幹層對象組成以及連接配接對象組成。
layer 層對象,由多個節點組成。
node 節點對象計算和記錄節點自身的資訊(比如輸出值、誤差項等),以及與這個節點相關的上下遊的連接配接。
connection 每個連接配接對象都要記錄該連接配接的權重。
connections 僅僅作為connection的集合對象,提供一些集合操作。
node實作如下:
constnode對象,為了實作一個輸出恒為1的節點(計算偏置項時需要)
layer對象,負責初始化一層。此外,作為node的集合對象,提供對node集合的操作。
connection對象,主要職責是記錄連接配接的權重,以及這個連接配接所關聯的上下遊節點。
connections對象,提供connection集合操作。
network對象,提供api。
至此,實作了一個基本的全連接配接神經網絡。可以看到,同神經網絡的強大學習能力相比,其實作還算是很容易的。
梯度檢查
怎麼保證自己寫的神經網絡沒有bug呢?事實上這是一個非常重要的問題。一方面,千辛萬苦想到一個算法,結果效果不理想,那麼是算法本身錯了還是代碼實作錯了呢?定位這種問題肯定要花費大量的時間和精力。另一方面,由于神經網絡的複雜性,我們幾乎無法事先知道神經網絡的輸入和輸出,是以類似tdd(測試驅動開發)這樣的開發方法似乎也不可行。
辦法還是有滴,就是利用梯度檢查來确認程式是否正确。梯度檢查的思路如下:
對于梯度下降算法:
當然,我們可以重複上面的過程,對每個權重wji都進行檢查。也可以使用多個樣本重複檢查。
至此,會推導、會實作、會抓bug,你已經摸到深度學習的大門了。接下來還需要不斷的實踐,我們用剛剛寫過的神經網絡去識别手寫數字。
神經網絡實戰——手寫數字識别
針對這個任務,我們采用業界非常流行的mnist資料集。mnist大約有60000個手寫字母的訓練樣本,我們使用它訓練我們的神經網絡,然後再用訓練好的網絡去識别手寫數字。
手寫數字識别是個比較簡單的任務,數字隻可能是0-9中的一個,這是個10分類問題。
超參數的确定
我們首先需要确定網絡的層數和每層的節點數。關于第一個問題,實際上并沒有什麼理論化的方法,大家都是根據經驗來拍,如果沒有經驗的話就随便拍一個。然後,你可以多試幾個值,訓練不同層數的神經網絡,看看哪個效果最好就用哪個。嗯,現在你可能明白為什麼說深度學習是個手藝活了,有些手藝很讓人無語,而有些手藝還是很有技術含量的。
不過,有些基本道理我們還是明白的,我們知道網絡層數越多越好,也知道層數越多訓練難度越大。對于全連接配接網絡,隐藏層最好不要超過三層。那麼,我們可以先試試僅有一個隐藏層的神經網絡效果怎麼樣。畢竟模型小的話,訓練起來也快些(剛開始玩模型的時候,都希望快點看到結果)。
輸入層節點數是确定的。因為mnist資料集每個訓練資料是28*28的圖檔,共784個像素,是以,輸入層節點數應該是784,每個像素對應一個輸入節點。
輸出層節點數也是确定的。因為是10分類,我們可以用10個節點,每個節點對應一個分類。輸出層10個節點中,輸出最大值的那個節點對應的分類,就是模型的預測結果。
隐藏層節點數量是不好确定的,從1到100萬都可以。下面有幾個經驗公式:
是以,我們可以先根據上面的公式設定一個隐藏層節點數。如果有時間,我們可以設定不同的節點數,分别訓練,看看哪個效果最好就用哪個。我們先拍一個,設隐藏層節點數為300吧。
對于3層784*300*10的全連接配接網絡,總共有300*(784+1)+10*(300+1)=238510個參數!神經網絡之是以強大,是它提供了一種非常簡單的方法去實作大量的參數。目前百億參數、千億樣本的超大規模神經網絡也是有的。因為mnist隻有6萬個訓練樣本,參數太多了很容易過拟合,效果反而不好。
模型的訓練和評估
mnist資料集包含10000個測試樣本。我們先用60000個訓練樣本訓練我們的網絡,然後再用測試樣本對網絡進行測試,計算識别錯誤率:
我們每訓練10輪,評估一次準确率。當準确率開始下降時(出現了過拟合)終止訓練。
代碼實作
首先,我們需要把mnist資料集處理為神經網絡能夠接受的形式。mnist訓練集的檔案格式可以參考官方網站,這裡不在贅述。每個訓練樣本是一個28*28的圖像,我們按照行優先,把它轉化為一個784維的向量。每個标簽是0-9的值,我們将其轉換為一個10維的one-hot向量:如果标簽值為,我們就把向量的第維(從0開始編号)設定為0.9,而其它維設定為0.1。例如,向量[0.1,0.1,0.9,0.1,0.1,0.1,0.1,0.1,0.1,0.1]表示值2。
下面是處理mnist資料的代碼:
網絡的輸出是一個10維向量,這個向量第個(從0開始編号)元素的值最大,那麼就是網絡的識别結果。下面是代碼實作:
我們使用錯誤率來對網絡進行評估,下面是代碼實作:
最後實作我們的訓練政策:每訓練10輪,評估一次準确率,當準确率開始下降時終止訓練。下面是代碼實作:
在我的機器上測試了一下,1個epoch大約需要9000多秒,是以要對代碼做很多的性能優化工作。訓練要很久很久,可以把它上傳到伺服器上,在tmux的session裡面去運作。為了防止異常終止導緻前功盡棄,我們每訓練10輪,就把獲得參數值儲存在磁盤上,以便後續可以恢複。(代碼略)
小結
至此,你已經完成了又一次漫長的學習之旅。你現在應該已經明白了神經網絡的基本原理,高興的話,你甚至有能力去動手實作一個,并用它解決一些問題。如果感到困難也不要氣餒,這篇文章是一個重要的分水嶺,如果你完全弄明白了的話,在真正的『小白』和裝腔作勢的『大牛』面前吹吹牛是完全沒有問題的。
作為深度學習入門的系列文章,本文也是上半場的結束。在這個半場,你掌握了機器學習、神經網絡的基本概念,并且有能力去動手解決一些簡單的問題(例如手寫數字識别,如果用傳統的觀點來看,其實這些問題也不簡單)。而且,一旦掌握基本概念,後面的學習就容易多了。
在下半場,我們講介紹更多『深度』學習的内容,我們已經講了神經網絡(neutrol network),但是并沒有講深度神經網絡(deep neutrol network)。deep會帶來更加強大的能力,同時也帶來更多的問題。如果不了解這些問題和它們的解決方案,也不能說你入門了『深度』學習。
目前業界有很多開源的神經網絡實作,它們的功能也要強大的多,是以你并不需要事必躬親的去實作自己的神經網絡。我們在上半場不斷的從頭發明輪子,是為了讓你明白神經網絡的基本原理,這樣你就能非常迅速的掌握這些工具。在下半場的文章中,我們改變了政策:不會再去從頭開始去實作,而是盡可能應用現有的工具。
原文位址:https://www.zybuluo.com/hanbingtao/note/476663