零基礎入門深度學習(4) - 卷積神經網絡

往期回顧

在前面的文章中,咱們介紹了全鏈接神經網絡,以及它的訓練和使用。咱們用它來識別了手寫數字,然而,這種結構的網絡對於圖像識別任務來講並非很合適。本文將要介紹一種更適合圖像、語音識別任務的神經網絡結構——卷積神經網絡(Convolutional Neural Network, CNN)。說卷積神經網絡是最重要的一種神經網絡也不爲過,它在最近幾年大放異彩,幾乎全部圖像、語音識別領域的重要突破都是卷積神經網絡取得的,好比谷歌的GoogleNet、微軟的ResNet等,戰勝李世石的AlphaGo也用到了這種網絡。本文將詳細介紹卷積神經網絡以及它的訓練算法,以及動手實現一個簡單的卷積神經網絡。html

 

一個新的激活函數——Relu

最近幾年卷積神經網絡中,激活函數每每不選擇sigmoid或tanh函數,而是選擇relu函數。Relu函數的定義是:python

 

 

 

 

Relu函數圖像以下圖所示:git

Relu函數做爲激活函數,有下面幾大優點:github

  • 速度快 和sigmoid函數須要計算指數和倒數相比,relu函數其實就是一個max(0,x),計算代價小不少。
  • 減輕梯度消失問題 回憶一下計算梯度的公式。其中,是sigmoid函數的導數。在使用反向傳播算法進行梯度計算時,每通過一層sigmoid神經元,梯度就要乘上一個。從下圖能夠看出,函數最大值是1/4。所以,乘一個會致使梯度愈來愈小,這對於深層網絡的訓練是個很大的問題。而relu函數的導數是1,不會致使梯度變小。固然,激活函數僅僅是致使梯度減少的一個因素,但不管如何在這方面relu的表現強於sigmoid。使用relu激活函數可讓你訓練更深的網絡。

  • 稀疏性 經過對大腦的研究發現,大腦在工做的時候只有大約5%的神經元是激活的,而採用sigmoid激活函數的人工神經網絡,其激活率大約是50%。有論文聲稱人工神經網絡在15%-30%的激活率時是比較理想的。由於relu函數在輸入小於0時是徹底不激活的,所以能夠得到一個更低的激活率。
 

全鏈接網絡 VS 卷積網絡

全鏈接神經網絡之因此不太適合圖像識別任務,主要有如下幾個方面的問題:算法

  • 參數數量太多 考慮一個輸入1000*1000像素的圖片(一百萬像素,如今已經不能算大圖了),輸入層有1000*1000=100萬節點。假設第一個隱藏層有100個節點(這個數量並很少),那麼僅這一層就有(1000*1000+1)*100=1億參數,這實在是太多了!咱們看到圖像只擴大一點,參數數量就會多不少,所以它的擴展性不好。
  • 沒有利用像素之間的位置信息 對於圖像識別任務來講,每一個像素和其周圍像素的聯繫是比較緊密的,和離得很遠的像素的聯繫可能就很小了。若是一個神經元和上一層全部神經元相連,那麼就至關於對於一個像素來講,把圖像的全部像素都等同看待,這不符合前面的假設。當咱們完成每一個鏈接權重的學習以後,最終可能會發現,有大量的權重,它們的值都是很小的(也就是這些鏈接其實可有可無)。努力學習大量並不重要的權重,這樣的學習必將是很是低效的。
  • 網絡層數限制 咱們知道網絡層數越多其表達能力越強,可是經過梯度降低方法訓練深度全鏈接神經網絡很困難,由於全鏈接神經網絡的梯度很難傳遞超過3層。所以,咱們不可能獲得一個很深的全鏈接神經網絡,也就限制了它的能力。

那麼,卷積神經網絡又是怎樣解決這個問題的呢?主要有三個思路:數組

  • 局部鏈接 這個是最容易想到的,每一個神經元再也不和上一層的全部神經元相連,而只和一小部分神經元相連。這樣就減小了不少參數。
  • 權值共享 一組鏈接能夠共享同一個權重,而不是每一個鏈接有一個不一樣的權重,這樣又減小了不少參數。
  • 下采樣 可使用Pooling來減小每層的樣本數,進一步減小參數數量,同時還能夠提高模型的魯棒性。

對於圖像識別任務來講,卷積神經網絡經過儘量保留重要的參數,去掉大量不重要的參數,來達到更好的學習效果。網絡

接下來,咱們將詳述卷積神經網絡究竟是何方神聖。架構

 

卷積神經網絡是啥

首先,咱們先獲取一個感性認識,下圖是一個卷積神經網絡的示意圖:app

圖1 卷積神經網絡

 

網絡架構

如圖1所示,一個卷積神經網絡由若干卷積層、Pooling層、全鏈接層組成。你能夠構建各類不一樣的卷積神經網絡,它的經常使用架構模式爲:dom

INPUT -> [[CONV]*N -> POOL?]*M -> [FC]*K

也就是N個卷積層疊加,而後(可選)疊加一個Pooling層,重複這個結構M次,最後疊加K個全鏈接層。

對於圖1展現的卷積神經網絡:

INPUT -> CONV -> POOL -> CONV -> POOL -> FC -> FC

按照上述模式能夠表示爲:

INPUT -> [[CONV]*1 -> POOL]*2 -> [FC]*2

也就是:N=1, M=2, K=2

 

三維的層結構

從圖1咱們能夠發現卷積神經網絡的層結構和全鏈接神經網絡的層結構有很大不一樣。全鏈接神經網絡每層的神經元是按照一維排列的,也就是排成一條線的樣子;而卷積神經網絡每層的神經元是按照三維排列的,也就是排成一個長方體的樣子,有寬度、高度和深度。

對於圖1展現的神經網絡,咱們看到輸入層的寬度和高度對應於輸入圖像的寬度和高度,而它的深度爲1。接着,第一個卷積層對這幅圖像進行了卷積操做(後面咱們會講如何計算卷積),獲得了三個Feature Map。這裏的"3"多是讓不少初學者迷惑的地方,實際上,就是這個卷積層包含三個Filter,也就是三套參數,每一個Filter均可以把原始輸入圖像卷積獲得一個Feature Map,三個Filter就能夠獲得三個Feature Map。至於一個卷積層能夠有多少個Filter,那是能夠自由設定的。也就是說,卷積層的Filter個數也是一個超參數。咱們能夠把Feature Map能夠看作是經過卷積變換提取到的圖像特徵,三個Filter就對原始圖像提取出三組不一樣的特徵,也就是獲得了三個Feature Map,也稱作三個通道(channel)。

繼續觀察圖1,在第一個卷積層以後,Pooling層對三個Feature Map作了下采樣(後面咱們會講如何計算下采樣),獲得了三個更小的Feature Map。接着,是第二個卷積層,它有5個Filter。每一個Fitler都把前面下采樣以後的3個**Feature Map卷積在一塊兒,獲得一個新的Feature Map。這樣,5個Filter就獲得了5個Feature Map。接着,是第二個Pooling,繼續對5個Feature Map進行下采樣**,獲得了5個更小的Feature Map。

圖1所示網絡的最後兩層是全鏈接層。第一個全鏈接層的每一個神經元,和上一層5個Feature Map中的每一個神經元相連,第二個全鏈接層(也就是輸出層)的每一個神經元,則和第一個全鏈接層的每一個神經元相連,這樣獲得了整個網絡的輸出。

至此,咱們對卷積神經網絡有了最基本的感性認識。接下來,咱們將介紹卷積神經網絡中各類層的計算和訓練。

 

卷積神經網絡輸出值的計算

 

卷積層輸出值的計算

咱們用一個簡單的例子來說述如何計算卷積,而後,咱們抽象出卷積層的一些重要概念和計算方法。

假設有一個5*5的圖像,使用一個3*3的filter進行卷積,想獲得一個3*3的Feature Map,以下所示:

爲了清楚的描述卷積計算過程,咱們首先對圖像的每一個像素進行編號,用表示圖像的第行第列元素;對filter的每一個權重進行編號,用表示第行第列權重,用表示filter的偏置項;對Feature Map的每一個元素進行編號,用表示Feature Map的第行第列元素;用表示激活函數(這個例子選擇relu函數做爲激活函數)。而後,使用下列公式計算卷積:

 

 

 

例如,對於Feature Map左上角元素來講,其卷積計算方法爲:

 

 

 

 

計算結果以下圖所示:

接下來,Feature Map的元素的卷積計算方法爲:

 

 

 

 

計算結果以下圖所示:

能夠依次計算出Feature Map中全部元素的值。下面的動畫顯示了整個Feature Map的計算過程:

圖2 卷積計算

上面的計算過程當中,步幅(stride)爲1。步幅能夠設爲大於1的數。例如,當步幅爲2時,Feature Map計算以下:

咱們注意到,當步幅設置爲2的時候,Feature Map就變成2*2了。這說明圖像大小、步幅和卷積後的Feature Map大小是有關係的。事實上,它們知足下面的關係:

 

 

式式

 

在上面兩個公式中,是卷積後Feature Map的寬度;是卷積前圖像的寬度;是filter的寬度;是Zero Padding數量,Zero Padding是指在原始圖像周圍補幾圈0,若是的值是1,那麼就補1圈0;是步幅;是卷積後Feature Map的高度;是卷積前圖像的寬度。式2和式3本質上是同樣的。

之前面的例子來講,圖像寬度,filter寬度,Zero Padding,步幅,則

 

 

 

 

說明Feature Map寬度是2。一樣,咱們也能夠計算出Feature Map高度也是2。

前面咱們已經講了深度爲1的卷積層的計算方法,若是深度大於1怎麼計算呢?其實也是相似的。若是卷積前的圖像深度爲D,那麼相應的filter的深度也必須爲D。咱們擴展一下式1,獲得了深度大於1的卷積計算公式:

 

 

 

在式4中,D是深度;F是filter的大小(寬度或高度,二者相同);表示filter的第層第行第列權重;表示圖像的第層第行第列像素;其它的符號含義和式1是相同的,再也不贅述。

咱們前面還曾提到,每一個卷積層能夠有多個filter。每一個filter和原始圖像進行卷積後,均可以獲得一個Feature Map。所以,卷積後Feature Map的深度(個數)和卷積層的filter個數是相同的。

下面的動畫顯示了包含兩個filter的卷積層的計算。咱們能夠看到7*7*3輸入,通過兩個3*3*3filter的卷積(步幅爲2),獲得了3*3*2的輸出。另外咱們也會看到下圖的Zero padding是1,也就是在輸入元素的周圍補了一圈0。Zero padding對於圖像邊緣部分的特徵提取是頗有幫助的。

以上就是卷積層的計算方法。這裏面體現了局部鏈接和權值共享:每層神經元只和上一層部分神經元相連(卷積計算規則),且filter的權值對於上一層全部神經元都是同樣的。對於包含兩個3*3*3的fitler的卷積層來講,其參數數量僅有(3*3*3+1)*2=56個,且參數數量與上一層神經元個數無關。與全鏈接神經網絡相比,其參數數量大大減小了。

 

用卷積公式來表達卷積層計算

不想了解太多數學細節的讀者能夠跳過這一節,不影響對全文的理解。

式4的表達非常繁冗,最好能簡化一下。就像利用矩陣能夠簡化表達全鏈接神經網絡的計算同樣,咱們利用卷積公式能夠簡化卷積神經網絡的表達。

下面咱們介紹二維卷積公式。

設矩陣,其行、列數分別爲,則二維卷積公式以下:

 

 

 

 

,知足條件

咱們能夠把上式寫成

 

 

 

若是咱們按照式5來計算卷積,咱們能夠發現矩陣A其實是filter,而矩陣B是待卷積的輸入,位置關係也有所不一樣:

從上圖能夠看到,A左上角的值與B對應區塊中右下角的值相乘,而不是與左上角的相乘。所以,數學中的卷積和卷積神經網絡中的『卷積』仍是有區別的,爲了不混淆,咱們把卷積神經網絡中的『卷積』操做叫作互相關(cross-correlation)操做。

卷積和互相關操做是能夠轉化的。首先,咱們把矩陣A翻轉180度,而後再交換A和B的位置(即把B放在左邊而把A放在右邊。卷積知足交換率,這個操做不會致使結果變化),那麼卷積就變成了互相關。

若是咱們不去考慮二者這麼一點點的區別,咱們能夠把式5代入到式4:

 

 

 

其中,是卷積層輸出的feature map。同式4相比,式6就簡單多了。然而,這種簡潔寫法只適合步長爲1的狀況。

 

Pooling層輸出值的計算

Pooling層主要的做用是下采樣,經過去掉Feature Map中不重要的樣本,進一步減小參數數量。Pooling的方法不少,最經常使用的是Max Pooling。Max Pooling實際上就是在n*n的樣本中取最大值,做爲採樣後的樣本值。下圖是2*2 max pooling:

除了Max Pooing以外,經常使用的還有Mean Pooling——取各樣本的平均值。

對於深度爲D的Feature Map,各層獨立作Pooling,所以Pooling後的深度仍然爲D。

 

全鏈接層

全鏈接層輸出值的計算和上一篇文章零基礎入門深度學習(3) - 神經網絡和反向傳播算法講過的全鏈接神經網絡是同樣的,這裏就再也不贅述了。

 

卷積神經網絡的訓練

和全鏈接神經網絡相比,卷積神經網絡的訓練要複雜一些。但訓練的原理是同樣的:利用鏈式求導計算損失函數對每一個權重的偏導數(梯度),而後根據梯度降低公式更新權重。訓練算法依然是反向傳播算法。

咱們先回憶一下上一篇文章零基礎入門深度學習(3) - 神經網絡和反向傳播算法介紹的反向傳播算法,整個算法分爲三個步驟:

  1. 前向計算每一個神經元的輸出值表示網絡的第個神經元,如下同);
  2. 反向計算每一個神經元的偏差項在有的文獻中也叫作敏感度(sensitivity)。它其實是網絡的損失函數對神經元加權輸入的偏導數,即
  3. 計算每一個神經元鏈接權重的梯度(表示從神經元鏈接到神經元的權重),公式爲,其中,表示神經元的輸出。

最後,根據梯度降低法則更新每一個權重便可。

對於卷積神經網絡,因爲涉及到局部鏈接、下采樣的等操做,影響到了第二步偏差項的具體計算方法,而權值共享影響了第三步權重的梯度的計算方法。接下來,咱們分別介紹卷積層和Pooling層的訓練算法。

 

卷積層的訓練

對於卷積層,咱們先來看看上面的第二步,即如何將偏差項傳遞到上一層;而後再來看看第三步,即如何計算filter每一個權值的梯度。

 

卷積層偏差項的傳遞

 
最簡單狀況下偏差項的傳遞

咱們先來考慮步長爲一、輸入的深度爲一、filter個數爲1的最簡單的狀況。

假設輸入的大小爲3*3,filter大小爲2*2,按步長爲1卷積,咱們將獲得2*2的feature map。以下圖所示:

在上圖中,爲了描述方便,咱們爲每一個元素都進行了編號。用表示第層第行第列的偏差項;用表示filter第行第列權重,用表示filter的偏置項;用表示第層第行第列神經元的輸出;用表示第行神經元的加權輸入;用表示第層第行第列的偏差項;用表示第層的激活函數。它們之間的關係以下:

 

 

 

 

上式中,都是數組,是由組成的數組,表示卷積操做。

在這裏,咱們假設第中的每一個值都已經算好,咱們要作的是計算第層每一個神經元的偏差項

根據鏈式求導法則:

 

 

 

 

咱們先求第一項。咱們先來看幾個特例,而後從中總結出通常性的規律。

例1,計算僅與的計算有關:

 

 

 

 

所以:

 

 

 

 

例2,計算的計算都有關:

 

 

 

 

所以:

 

 

 

 

例3,計算的計算都有關:

 

 

 

 

所以:

 

 

 

 

從上面三個例子,咱們發揮一下想象力,不難發現,計算,至關於把第層的sensitive map周圍補一圈0,在與180度翻轉後的filter進行cross-correlation,就能獲得想要結果,以下圖所示:

由於卷積至關於將filter旋轉180度的cross-correlation,所以上圖的計算能夠用卷積公式完美的表達:

 

 

 

 

上式中的表示第層的filter的權重數組。也能夠把上式的卷積展開,寫成求和的形式:

 

 

 

 

如今,咱們再求第二項。由於

 

 

 

 

因此這一項極其簡單,僅求激活函數的導數就好了。

 

 

 

 

將第一項和第二項組合起來,咱們獲得最終的公式:

 

 

 

也能夠將式7寫成卷積的形式:

 

 

 

其中,符號表示element-wise product,即將矩陣中每一個對應元素相乘。注意式8中的都是矩陣。

以上就是步長爲一、輸入的深度爲一、filter個數爲1的最簡單的狀況,卷積層偏差項傳遞的算法。下面咱們來推導一下步長爲S的狀況。

 
卷積步長爲S時的偏差傳遞

咱們先來看看步長爲S與步長爲1的差異。

如上圖,上面是步長爲1時的卷積結果,下面是步長爲2時的卷積結果。咱們能夠看出,由於步長爲2,獲得的feature map跳過了步長爲1時相應的部分。所以,當咱們反向計算偏差項時,咱們能夠對步長爲S的sensitivity map相應的位置進行補0,將其『還原』成步長爲1時的sensitivity map,再用式8進行求解。

 
輸入層深度爲D時的偏差傳遞

當輸入深度爲D時,filter的深度也必須爲D,層的通道只與filter的通道的權重進行計算。所以,反向計算偏差項時,咱們可使用式8,用filter的第通道權重對第層sensitivity map進行卷積,獲得第通道的sensitivity map。以下圖所示:

 
filter數量爲N時的偏差傳遞

filter數量爲N時,輸出層的深度也爲N,第個filter卷積產生輸出層的第個feature map。因爲第層每一個加權輸入都同時影響了第層全部feature map的輸出值,所以,反向計算偏差項時,須要使用全導數公式。也就是,咱們先使用第個filter對第層相應的第個sensitivity map進行卷積,獲得一組N個層的偏sensitivity map。依次用每一個filter作這種卷積,就獲得D組偏sensitivity map。最後在各組之間將N個偏sensitivity map 按元素相加,獲得最終的N個層的sensitivity map:

 

 

 

以上就是卷積層偏差項傳遞的算法,若是讀者還有所困惑,能夠參考後面的代碼實現來理解。

 

卷積層filter權重梯度的計算

咱們要在獲得第層sensitivity map的狀況下,計算filter的權重的梯度,因爲卷積層是權重共享的,所以梯度的計算稍有不一樣。

如上圖所示,是第層的輸出,是第層filter的權重,是第層的sensitivity map。咱們的任務是計算的梯度,即

爲了計算偏導數,咱們須要考察權重的影響。權重項經過影響的值,進而影響。咱們仍然經過幾個具體的例子來看權重項的影響,而後再從中總結出規律。

例1,計算

 

 

 

 

從上面的公式看出,因爲權值共享,權值對全部的都有影響。...的函數,而...又是的函數,根據全導數公式,計算就是要把每一個偏導數都加起來:

 

 

 

 

例2,計算

經過查看的關係,咱們很容易獲得:

 

 

 

 

實際上,每一個權重項都是相似的,咱們不一一舉例了。如今,是咱們再次發揮想象力的時候,咱們發現計算規律是:

 

 

 

 

也就是用sensitivity map做爲卷積核,在input上進行cross-correlation,以下圖所示:

最後,咱們來看一看偏置項的梯度。經過查看前面的公式,咱們很容易發現:

 

 

 

 

也就是偏置項的梯度就是sensitivity map全部偏差項之和。

對於步長爲S的卷積層,處理方法與傳遞**偏差項*是同樣的,首先將sensitivity map『還原』成步長爲1時的sensitivity map,再用上面的方法進行計算。

得到了全部的梯度以後,就是根據梯度降低算法來更新每一個權重。這在前面的文章中已經反覆寫過,這裏就再也不重複了。

至此,咱們已經解決了卷積層的訓練問題,接下來咱們看一看Pooling層的訓練。

 

Pooling層的訓練

不管max pooling仍是mean pooling,都沒有須要學習的參數。所以,在卷積神經網絡的訓練中,Pooling層須要作的僅僅是將偏差項傳遞到上一層,而沒有梯度的計算。

 

Max Pooling偏差項的傳遞

以下圖,假設第層大小爲4*4,pooling filter大小爲2*2,步長爲2,這樣,max pooling以後,第層大小爲2*2。假設第層的值都已經計算完畢,咱們如今的任務是計算第層的值。

咱們用表示第層的加權輸入;用表示第層的加權輸入。咱們先來考察一個具體的例子,而後再總結通常性的規律。對於max pooling:

 

 

 

 

也就是說,只有區塊中最大的纔會對的值產生影響。咱們假設最大的值是,則上式至關於:

 

 

 

 

那麼,咱們不難求得下面幾個偏導數:

 

 

 

 

所以:

 

 

 

 

而:

 

 

 

 

如今,咱們發現了規律:對於max pooling,下一層的偏差項的值會原封不動的傳遞到上一層對應區塊中的最大值所對應的神經元,而其餘神經元的偏差項的值都是0。以下圖所示(假設爲所在區塊中的最大輸出值):

 

Mean Pooling偏差項的傳遞

咱們仍是用前面屢試不爽的套路,先研究一個特殊的情形,再擴展爲通常規律。

如上圖,咱們先來考慮計算。咱們先來看看如何影響

 

 

 

 

根據上式,咱們一眼就能看出來:

 

 

 

 

因此,根據鏈式求導法則,咱們不難算出:

 

 

 

 

一樣,咱們能夠算出

 

 

 

 

如今,咱們發現了規律:對於mean pooling,下一層的偏差項的值會平均分配到上一層對應區塊中的全部神經元。以下圖所示:

上面這個算法能夠表達爲高大上的克羅內克積(Kronecker product)的形式,有興趣的讀者能夠研究一下。

 

 

 

 

其中,是pooling層filter的大小,都是矩陣。

至此,咱們已經把卷積層、Pooling層的訓練算法介紹完畢,加上上一篇文章講的全鏈接層訓練算法,您應該已經具有了編寫卷積神經網絡代碼所須要的知識。爲了加深對知識的理解,接下來,咱們將展現如何實現一個簡單的卷積神經網絡。

 

卷積神經網絡的實現

完整代碼請參考GitHub: https://github.com/hanbt/learn_dl/blob/master/cnn.py (python2.7)

如今,咱們親自動手實現一個卷積神經網絡,以便鞏固咱們所學的知識。

首先,咱們要改變一下代碼的架構,『層』成爲了咱們最核心的組件。這是由於卷積神經網絡有不一樣的層,而每種層的算法都在對應的類中實現。

此次,咱們用到了在python中編寫算法常常會用到的numpy包。爲了使用numpy,咱們須要先將numpy導入:

 
  1. import numpy as np
 

卷積層的實現

 

卷積層初始化

咱們用ConvLayer類來實現一個卷積層。下面的代碼是初始化一個卷積層,能夠在構造函數中設置卷積層的超參數。

 
  1. class ConvLayer(object):
  2. def __init__(self, input_width, input_height,
  3. channel_number, filter_width,
  4. filter_height, filter_number,
  5. zero_padding, stride, activator,
  6. learning_rate):
  7. self.input_width = input_width
  8. self.input_height = input_height
  9. self.channel_number = channel_number
  10. self.filter_width = filter_width
  11. self.filter_height = filter_height
  12. self.filter_number = filter_number
  13. self.zero_padding = zero_padding
  14. self.stride = stride
  15. self.output_width = \
  16. ConvLayer.calculate_output_size(
  17. self.input_width, filter_width, zero_padding,
  18. stride)
  19. self.output_height = \
  20. ConvLayer.calculate_output_size(
  21. self.input_height, filter_height, zero_padding,
  22. stride)
  23. self.output_array = np.zeros((self.filter_number,
  24. self.output_height, self.output_width))
  25. self.filters = []
  26. for i in range(filter_number):
  27. self.filters.append(Filter(filter_width,
  28. filter_height, self.channel_number))
  29. self.activator = activator
  30. self.learning_rate = learning_rate

calculate_output_size函數用來肯定卷積層輸出的大小,其實現以下:

 
  1. @staticmethod
  2. def calculate_output_size(input_size,
  3. filter_size, zero_padding, stride):
  4. return (input_size - filter_size +
  5. 2 * zero_padding) / stride + 1

Filter類保存了卷積層的參數以及梯度,而且實現了用梯度降低算法來更新參數。

 
  1. class Filter(object):
  2. def __init__(self, width, height, depth):
  3. self.weights = np.random.uniform(-1e-4, 1e-4,
  4. (depth, height, width))
  5. self.bias = 0
  6. self.weights_grad = np.zeros(
  7. self.weights.shape)
  8. self.bias_grad = 0
  9. def __repr__(self):
  10. return 'filter weights:\n%s\nbias:\n%s' % (
  11. repr(self.weights), repr(self.bias))
  12. def get_weights(self):
  13. return self.weights
  14. def get_bias(self):
  15. return self.bias
  16. def update(self, learning_rate):
  17. self.weights -= learning_rate * self.weights_grad
  18. self.bias -= learning_rate * self.bias_grad

咱們對參數的初始化採用了經常使用的策略,即:權重隨機初始化爲一個很小的值,而偏置項初始化爲0。

Activator類實現了激活函數,其中,forward方法實現了前向計算,而backward方法則是計算導數。好比,relu函數的實現以下:

 
  1. class ReluActivator(object):
  2. def forward(self, weighted_input):
  3. #return weighted_input
  4. return max(0, weighted_input)
  5. def backward(self, output):
  6. return 1 if output > 0 else 0
 

卷積層前向計算的實現

ConvLayer類的forward方法實現了卷積層的前向計算(即計算根據輸入來計算卷積層的輸出),下面是代碼實現:

 
  1. def forward(self, input_array):
  2. '''
  3. 計算卷積層的輸出
  4. 輸出結果保存在self.output_array
  5. '''
  6. self.input_array = input_array
  7. self.padded_input_array = padding(input_array,
  8. self.zero_padding)
  9. for f in range(self.filter_number):
  10. filter = self.filters[f]
  11. conv(self.padded_input_array,
  12. filter.get_weights(), self.output_array[f],
  13. self.stride, filter.get_bias())
  14. element_wise_op(self.output_array,
  15. self.activator.forward)

上面的代碼裏面包含了幾個工具函數。element_wise_op函數實現了對numpy數組進行按元素操做,並將返回值寫回到數組中,代碼以下:

 
  1. # 對numpy數組進行element wise操做
  2. def element_wise_op(array, op):
  3. for i in np.nditer(array,
  4. op_flags=['readwrite']):
  5. i[...] = op(i)

conv函數實現了2維和3維數組的卷積,代碼以下:

 
  1. def conv(input_array,
  2. kernel_array,
  3. output_array,
  4. stride, bias):
  5. '''
  6. 計算卷積,自動適配輸入爲2D和3D的狀況
  7. '''
  8. channel_number = input_array.ndim
  9. output_width = output_array.shape[1]
  10. output_height = output_array.shape[0]
  11. kernel_width = kernel_array.shape[-1]
  12. kernel_height = kernel_array.shape[-2]
  13. for i in range(output_height):
  14. for j in range(output_width):
  15. output_array[i][j] = (
  16. get_patch(input_array, i, j, kernel_width,
  17. kernel_height, stride) * kernel_array
  18. ).sum() + bias

padding函數實現了zero padding操做:

 
  1. # 爲數組增長Zero padding
  2. def padding(input_array, zp):
  3. '''
  4. 爲數組增長Zero padding,自動適配輸入爲2D和3D的狀況
  5. '''
  6. if zp == 0:
  7. return input_array
  8. else:
  9. if input_array.ndim == 3:
  10. input_width = input_array.shape[2]
  11. input_height = input_array.shape[1]
  12. input_depth = input_array.shape[0]
  13. padded_array = np.zeros((
  14. input_depth,
  15. input_height + 2 * zp,
  16. input_width + 2 * zp))
  17. padded_array[:,
  18. zp : zp + input_height,
  19. zp : zp + input_width] = input_array
  20. return padded_array
  21. elif input_array.ndim == 2:
  22. input_width = input_array.shape[1]
  23. input_height = input_array.shape[0]
  24. padded_array = np.zeros((
  25. input_height + 2 * zp,
  26. input_width + 2 * zp))
  27. padded_array[zp : zp + input_height,
  28. zp : zp + input_width] = input_array
  29. return padded_array
 

卷積層反向傳播算法的實現

如今,是介紹卷積層核心算法的時候了。咱們知道反向傳播算法須要完成幾個任務:

  1. 將偏差項傳遞到上一層。
  2. 計算每一個參數的梯度。
  3. 更新參數。

如下代碼都是在ConvLayer類中實現。咱們先來看看將偏差項傳遞到上一層的代碼實現。

 
  1. def bp_sensitivity_map(self, sensitivity_array,
  2. activator):
  3. '''
  4. 計算傳遞到上一層的sensitivity map
  5. sensitivity_array: 本層的sensitivity map
  6. activator: 上一層的激活函數
  7. '''
  8. # 處理卷積步長,對原始sensitivity map進行擴展
  9. expanded_array = self.expand_sensitivity_map(
  10. sensitivity_array)
  11. # full卷積,對sensitivitiy map進行zero padding
  12. # 雖然原始輸入的zero padding單元也會得到殘差
  13. # 但這個殘差不須要繼續向上傳遞,所以就不計算了
  14. expanded_width = expanded_array.shape[2]
  15. zp = (self.input_width +
  16. self.filter_width - 1 - expanded_width) / 2
  17. padded_array = padding(expanded_array, zp)
  18. # 初始化delta_array,用於保存傳遞到上一層的
  19. # sensitivity map
  20. self.delta_array = self.create_delta_array()
  21. # 對於具備多個filter的卷積層來講,最終傳遞到上一層的
  22. # sensitivity map至關於全部的filter的
  23. # sensitivity map之和
  24. for f in range(self.filter_number):
  25. filter = self.filters[f]
  26. # 將filter權重翻轉180度
  27. flipped_weights = np.array(map(
  28. lambda i: np.rot90(i, 2),
  29. filter.get_weights()))
  30. # 計算與一個filter對應的delta_array
  31. delta_array = self.create_delta_array()
  32. for d in range(delta_array.shape[0]):
  33. conv(padded_array[f], flipped_weights[d],
  34. delta_array[d], 1, 0)
  35. self.delta_array += delta_array
  36. # 將計算結果與激活函數的偏導數作element-wise乘法操做
  37. derivative_array = np.array(self.input_array)
  38. element_wise_op(derivative_array,
  39. activator.backward)
  40. self.delta_array *= derivative_array

expand_sensitivity_map方法就是將步長爲S的sensitivity map『還原』爲步長爲1的sensitivity map,代碼以下:

 
  1. def expand_sensitivity_map(self, sensitivity_array):
  2. depth = sensitivity_array.shape[0]
  3. # 肯定擴展後sensitivity map的大小
  4. # 計算stride爲1時sensitivity map的大小
  5. expanded_width = (self.input_width -
  6. self.filter_width + 2 * self.zero_padding + 1)
  7. expanded_height = (self.input_height -
  8. self.filter_height + 2 * self.zero_padding + 1)
  9. # 構建新的sensitivity_map
  10. expand_array = np.zeros((depth, expanded_height,
  11. expanded_width))
  12. # 從原始sensitivity map拷貝偏差值
  13. for i in range(self.output_height):
  14. for j in range(self.output_width):
  15. i_pos = i * self.stride
  16. j_pos = j * self.stride
  17. expand_array[:,i_pos,j_pos] = \
  18. sensitivity_array[:,i,j]
  19. return expand_array

create_delta_array是建立用來保存傳遞到上一層的sensitivity map的數組。

 
  1. def create_delta_array(self):
  2. return np.zeros((self.channel_number,
  3. self.input_height, self.input_width))

接下來,是計算梯度的代碼。

 
  1. def bp_gradient(self, sensitivity_array):
  2. # 處理卷積步長,對原始sensitivity map進行擴展
  3. expanded_array = self.expand_sensitivity_map(
  4. sensitivity_array)
  5. for f in range(self.filter_number):
  6. # 計算每一個權重的梯度
  7. filter = self.filters[f]
  8. for d in range(filter.weights.shape[0]):
  9. conv(self.padded_input_array[d],
  10. expanded_array[f],
  11. filter.weights_grad[d], 1, 0)
  12. # 計算偏置項的梯度
  13. filter.bias_grad = expanded_array[f].sum()

最後,是按照梯度降低算法更新參數的代碼,這部分很是簡單。

 
  1. def update(self):
  2. '''
  3. 按照梯度降低,更新權重
  4. '''
  5. for filter in self.filters:
  6. filter.update(self.learning_rate)
 

卷積層的梯度檢查

爲了驗證咱們的公式推導和代碼實現的正確性,咱們必需要對卷積層進行梯度檢查。下面是代嗎實現:

 
  1. def init_test():
  2. a = np.array(
  3. [[[0,1,1,0,2],
  4. [2,2,2,2,1],
  5. [1,0,0,2,0],
  6. [0,1,1,0,0],
  7. [1,2,0,0,2]],
  8. [[1,0,2,2,0],
  9. [0,0,0,2,0],
  10. [1,2,1,2,1],
  11. [1,0,0,0,0],
  12. [1,2,1,1,1]],
  13. [[2,1,2,0,0],
  14. [1,0,0,1,0],
  15. [0,2,1,0,1],
  16. [0,1,2,2,2],
  17. [2,1,0,0,1]]])
  18. b = np.array(
  19. [[[0,1,1],
  20. [2,2,2],
  21. [1,0,0]],
  22. [[1,0,2],
  23. [0,0,0],
  24. [1,2,1]]])
  25. cl = ConvLayer(5,5,3,3,3,2,1,2,IdentityActivator(),0.001)
  26. cl.filters[0].weights = np.array(
  27. [[[-1,1,0],
  28. [0,1,0],
  29. [0,1,1]],
  30. [[-1,-1,0],
  31. [0,0,0],
  32. [0,-1,0]],
  33. [[0,0,-1],
  34. [0,1,0],
  35. [1,-1,-1]]], dtype=np.float64)
  36. cl.filters[0].bias=1
  37. cl.filters[1].weights = np.array(
  38. [[[1,1,-1],
  39. [-1,-1,1],
  40. [0,-1,1]],
  41. [[0,1,0],
  42. [-1,0,-1],
  43. [-1,1,0]],
  44. [[-1,0,0],
  45. [-1,0,1],
  46. [-1,0,0]]], dtype=np.float64)
  47. return a, b, cl
  48. def gradient_check():
  49. '''
  50. 梯度檢查
  51. '''
  52. # 設計一個偏差函數,取全部節點輸出項之和
  53. error_function = lambda o: o.sum()
  54. # 計算forward值
  55. a, b, cl = init_test()
  56. cl.forward(a)
  57. # 求取sensitivity map,是一個全1數組
  58. sensitivity_array = np.ones(cl.output_array.shape,
  59. dtype=np.float64)
  60. # 計算梯度
  61. cl.backward(a, sensitivity_array,
  62. IdentityActivator())
  63. # 檢查梯度
  64. epsilon = 10e-4
  65. for d in range(cl.filters[0].weights_grad.shape[0]):
  66. for i in range(cl.filters[0].weights_grad.shape[1]):
  67. for j in range(cl.filters[0].weights_grad.shape[2]):
  68. cl.filters[0].weights[d,i,j] += epsilon
  69. cl.forward(a)
  70. err1 = error_function(cl.output_array)
  71. cl.filters[0].weights[d,i,j] -= 2*epsilon
  72. cl.forward(a)
  73. err2 = error_function(cl.output_array)
  74. expect_grad = (err1 - err2) / (2 * epsilon)
  75. cl.filters[0].weights[d,i,j] += epsilon
  76. print 'weights(%d,%d,%d): expected - actural %f - %f' % (
  77. d, i, j, expect_grad, cl.filters[0].weights_grad[d,i,j])

上面代碼值得思考的地方在於,傳遞給卷積層的sensitivity map是全1數組,留給讀者本身推導一下爲何是這樣(提示:激活函數選擇了identity函數:)。讀者若是還有困惑,請寫在文章評論中,我會回覆。

運行上面梯度檢查的代碼,咱們獲得的輸出以下,指望的梯度和實際計算出的梯度一致,這證實咱們的算法推導和代碼實現確實是正確的。

以上就是卷積層的實現。

 

Max Pooling層的實現

max pooling層的實現相對簡單,咱們直接貼出所有代碼以下:

 
  1. class MaxPoolingLayer(object):
  2. def __init__(self, input_width, input_height,
  3. channel_number, filter_width,
  4. filter_height, stride):
  5. self.input_width = input_width
  6. self.input_height = input_height
  7. self.channel_number = channel_number
  8. self.filter_width = filter_width
  9. self.filter_height = filter_height
  10. self.stride = stride
  11. self.output_width = (input_width -
  12. filter_width) / self.stride + 1
  13. self.output_height = (input_height -
  14. filter_height) / self.stride + 1
  15. self.output_array = np.zeros((self.channel_number,
  16. self.output_height, self.output_width))
  17. def forward(self, input_array):
  18. for d in range(self.channel_number):
  19. for i in range(self.output_height):
  20. for j in range(self.output_width):
  21. self.output_array[d,i,j] = (
  22. get_patch(input_array[d], i, j,
  23. self.filter_width,
  24. self.filter_height,
  25. self.stride).max())
  26. def backward(self, input_array, sensitivity_array):
  27. self.delta_array = np.zeros(input_array.shape)
  28. for d in range(self.channel_number):
  29. for i in range(self.output_height):
  30. for j in range(self.output_width):
  31. patch_array = get_patch(
  32. input_array[d], i, j,
  33. self.filter_width,
  34. self.filter_height,
  35. self.stride)
  36. k, l = get_max_index(patch_array)
  37. self.delta_array[d,
  38. i * self.stride + k,
  39. j * self.stride + l] = \
  40. sensitivity_array[d,i,j]

全鏈接層的實現和上一篇文章相似,在此就再也不贅述了。至此,你已經擁有了實現了一個簡單的卷積神經網絡所須要的基本組件。對於卷積神經網絡,如今有不少優秀的開源實現,所以咱們並不須要真的本身去實現一個。貼出這些代碼的目的是爲了讓咱們更好的瞭解卷積神經網絡的基本原理。

 

卷積神經網絡的應用

 

MNIST手寫數字識別

LeNet-5是實現手寫數字識別的卷積神經網絡,在MNIST測試集上,它取得了0.8%的錯誤率。LeNet-5的結構以下:

關於LeNet-5的詳細介紹,網上的資料不少,所以就再也不重複了。感興趣的讀者能夠嘗試用咱們本身實現的卷積神經網絡代碼去構造並訓練LeNet-5(固然代碼會更復雜一些)。

 

小節

因爲卷積神經網絡的複雜性,咱們寫出了整個系列目前爲止最長的一篇文章,相信讀者也和做者同樣累的要死。卷積神經網絡是深度學習最重要的工具(我猶豫要不要寫上『之一』呢),付出一些辛苦去理解它也是值得的。若是您真正理解了本文的內容,至關於邁過了入門深度學習最重要的一到門檻。在下一篇文章中,咱們介紹深度學習另一種很是重要的工具:循環神經網絡,屆時咱們的系列文章也將完成過半。每篇文章都是一個過濾器,對於堅持到這裏的讀者們,入門深度學習曙光已現,加油。

 

 

參考資料

  1. CS231n Convolutional Neural Networks for Visual Recognition
  2. ReLu (Rectified Linear Units) 激活函數
  3. Jake Bouvrie, Notes on Convolutional Neural Networks, 2006
  4. Ian Goodfellow, Yoshua Bengio, Aaron Courville, Deep Learning, MIT Press, 2016
  5. 轉載自:https://www.zybuluo.com/hanbingtao/note/485480
相關文章
相關標籤/搜索