接下來介紹一種非常重要的神經網絡——卷積神經網絡。這種神經網絡在計算機視覺領域取得了重大的成功,而且在自然語言處理等其它領域也有很好的應用。深度學習受到大家的關注很大一個原因就是Alex等人實現的AlexNet(一種深度卷積神經網絡)在LSVRC-2010 ImageNet這個比賽中取得了非常好的成績。此後,卷積神經網絡及其變種被廣泛應用於各種圖像相關任務。
這裡主要參考了Neural Networks and Deep Learning和cs231n的課程來介紹CNN,兩部分都會有理論和代碼。前者會用theano來實現,而後者會使用我們前一部分介紹的自動梯度來實現。下面首先介紹Michael Nielsen的部分(其實主要是翻譯,然後加一些我自己的理解)。
前面的話
如果讀者自己嘗試了上一部分的代碼,調過3層和5層全連接的神經網絡的參數,我們會發現神經網絡的層數越多,參數(超參數)就越難調。但是如果參數調得好,深的網絡的效果確實比較淺的好(這也是為什麼我們要搞深度學習的原因)。所以深度學習有這樣的說法:「三個 bound 不如一個 heuristic,三個 heuristic 不如一個trick」。以前搞機器學習就是feature engineering加調參,現在就剩下調參了。網絡的結構,參數的初始化,learning_rate,迭代次數等等都會影響最終的結果。有興趣的同學可以看看Michael Nielsen這個電子書的相應章節,cs231n的Github資源也有介紹,另外《Neural Networks: Tricks of the Trade》這本書,看名字就知道講啥的了吧。
不過我們還是回到正題「卷積神經網絡」吧。
CNN簡介
在之前的章節我們使用了神經網絡來解決手寫數字識別(MNIST)的問題。我們使用了全連接的神經網絡,也就是前一層的每一個神經元都會連接到後一層的每一個神經元,如果前一層有m個節點,後一層有n個,那麼總共有m*n條邊(連接)。連接方式如下圖所示:
具體來講,對於輸入圖片的每一個像素,我們把它的灰度值作為對應神經元的輸入。對於28 28的圖像來說,我們的網絡有784個輸入神經元。然後我們訓練這個網絡的weights和biases來使得它可以正確的預測對應的數字。
我們之前設計的神經網絡工作的很好:在MNIST手寫識別數據集上我們得到了超過98%的準確率。但是仔細想一想的話,使用全連接的網絡來識別圖像有一些奇怪。因為這樣的網絡結構沒有考慮圖像的空間結構。比如,它對於空間上很近或者很遠的像素一樣的對待。這些空間的概念【比如7字會出現某些像素在某個水平方向同時灰度值差不多,也就是上面的那一橫】必須靠網絡從訓練數據中推測出來【但是如果訓練數據不夠而且圖像沒有做居中等歸一化的話,如果訓練數據的7的一橫都出現在圖像靠左的地方,而測試數據把7寫到右下角,那麼網絡很可能學不到這樣的特徵】。那為什麼我們不能設計一直網絡結構考慮這些空間結構呢?這樣的想法就是下面我們要討論的CNN的思想。
這種神經網絡利用了空間結構,因此非常適合用來做圖片分類。這種結構訓練也非常的快,因此也可以訓練更「深」的網絡。目前,圖像識別大都使用深層的卷積神經網絡及其變種。
卷積神經網絡有3個基本的idea:局部感知域(Local Recpetive Field),權值共享和池化(Pooling)。下面我們來一個一個的介紹它們。
局部感知域
在前面圖示的全連接的層裡,輸入是被描述成一列神經元。而在卷積網絡裡,我們把輸入看成28 28方格的二維神經元,它的每一個神經元對應於圖片在這個像素點的強度(灰度值),如下圖所示:
和往常一樣,我們把輸入像素連接到隱藏層的神經元。但是我們這裡不再把輸入的每一個像素都連接到隱藏層的每一個神經元。與之不同,我們把很小的相臨近的區域內的輸入連接在一起。
更加具體的來講,隱藏層的每一個神經元都會與輸入層一個很小的區域(比如一個5 5的區域,也就是25個像素點)相連接。隱藏對於隱藏層的某一個神經元,連接如下圖所示:
輸入圖像的這個區域叫做那個隱藏層神經元的局部感知域。這是輸入像素的一個小窗口。每個連接都有一個可以學習的權重,此外還有一個bias。你可以把那個神經元想像成用來分析這個局部感知域的。
我們然後在整個輸入圖像上滑動這個局部感知域。對於每一個局部感知域,都有一個隱藏層的神經元與之對應。為了具體一點的展示,我們首先從最左上角的局部感知域開始:
然後我們向右滑動這個局部感知域:
以此類推,我們可以構建出第一個隱藏層。注意,如果我們的輸入是28 28,並且使用5 5的局部關注域,那麼隱藏層是24 24。因為我們只能向右和向下移動23個像素,再往下移動就會移出圖像的邊界了。【說明,後面我們會介紹padding和striding,從而讓圖像在經過這樣一次卷積處理後尺寸可以不變小】
這裡我們展示了一次向右/下移動一個像素。事實上,我們也可以使用一次移動不止一個像素【這個移動的值叫stride】。比如,我們可以一次向右/下移動兩個像素。在這篇文章裡,我們只使用stride為1來實驗,但是請讀者知道其他人可能會用不同的stride值。
共享權值
之前提到過每一個隱藏層的神經元有一個5 5的權值。這24 24個隱藏層對應的權值是相同的。也就是說,對於隱藏層的第j,k個神經元,輸出如下:
這裡, 是激活函數,可以是我們之前提到的sigmoid函數。b是共享的bias,Wl,m 是5 5的共享權值。ax,y 是輸入在x,y的激活。
【從這個公式可以看出,權值是5 5的矩陣,不同的局部感知域使用這一個參數矩陣和bias】
這意味著這一個隱藏層的所有神經元都是檢測同一個特徵,只不過它們位於圖片的不同位置而已。比如這組weights和bias是某個局部感知域學到的用來識別一個垂直的邊。那麼預測的時候不管這條邊在哪個位置,它都會被某個對於的局部感知域檢測到。更抽象一點,卷積網絡能很好的適應圖片的位置變化:把圖片中的貓稍微移動一下位置,它仍然知道這是一隻貓。
因為這個原因,我們有時把輸入層到隱藏層的映射叫做特徵映射(feature map)。我們把定義特徵映射的權重叫做共享的權重(shared weights),bias叫做共享的bias(shared bais)。這組weights和bias定義了一個kernel或者filter。
上面描述的網絡結構只能檢測一種局部的特徵。為了識別圖片,我們需要更多的特徵映射。隱藏一個完整的卷積神經網絡會有很多不同的特徵映射:
在上面的例子裡,我們有3個特徵映射。每個映射由一個5 5的weights和一個biase確定。因此這個網絡能檢測3種特徵,不管這3個特徵出現在圖像的那個局部感知域裡。
為了簡化,上面之展示了3個特徵映射。在實際使用的卷積神經網絡中我們會使用非常多的特徵映射。早期的一個卷積神經網絡——LeNet-5,使用了6個特徵映射,每一個都是5 5的局部感知域,來識別MNIST數字。因此上面的例子和LeNet-5很接近。後面我們開發的卷積層將使用20和40個特徵映射。下面我們先看看模型學習到的一些特徵:
這20個圖片對應了20個不同的特徵映射。每個映射是一個5 5的圖像,對應於局部感知域的5 5個權重。顏色越白(淺)說明權值越小(一般都是負的),因此對應像素對於識別這個特徵越不重要。顏色越深(黑)說明權值越大,對應的像素越重要。
那麼我們可以從這些特徵映射裡得出什麼結論呢?很顯然這裡包含了非隨機的空間結構。這說明我們的網絡學到了一些空間結構。但是,也很難說它具體學到了哪些特徵。我們學到的不是一個 Gabor濾波器 的。事實上有很多研究工作嘗試理解機器到底學到了什麼樣的特徵。如果你感興趣,可以參考Matthew Zeiler 和 Rob Fergus在2013年的論文 Visualizing and Understanding Convolutional Networks。
共享權重和bias的一大好處是它極大的減少了網絡的參數數量。對於每一個特徵映射,我們只需要 25=5 5 個權重,再加一個bias。因此一個特徵映射只有26個參數。如果我們有20個特徵映射,那麼只有20 26=520個參數。如果我們使用全連接的神經網絡結構,假設隱藏層有30個神經元(這並不算很多),那麼就有784*30個權重參數,再加上30個bias,總共有23,550個參數。換句話說,全連接的網絡比卷積網絡的參數多了40倍。
當然,我們不能直接比較兩種網絡的參數,因為這兩種模型有本質的區別。但是,憑直覺,由於卷積網絡有平移不變的特性,為了達到相同的效果,它也可能使用更少的參數。由於參數變少,卷積網絡的訓練速度也更快,從而相同的計算資源我們可以訓練更深的網絡。
「卷積」神經網絡是因為公式(1)裡的運算叫做「卷積運算」。更加具體一點,我們可以把公式(1)裡的求和寫成卷積:$a^1 = \sigma(b + w * a^0)$。*在這裡不是乘法,而是卷積運算。這裡不會討論卷積的細節,所以讀者如果不懂也不要擔心,這裡只不過是為了解釋卷積神經網絡這個名字的由來。【建議感興趣的讀者參考colah的博客文章 《Understanding Convolutions》】
池化(Pooling)
除了上面的卷積層,卷積神經網絡也包括池化層(pooling layers)。池化層一般都直接放在卷積層後面池化層的目的是簡化從卷積層輸出的信息。
更具體一點,一個池化層把卷積層的輸出作為其輸入並且輸出一個更緊湊(condensed)的特徵映射。比如,池化層的每一個神經元都提取了之前那個卷積層的一個2 2區域的信息。更為具體的一個例子,一種非常常見的池化操作叫做Max-pooling。在Max-Pooling中,這個神經元選擇2 2區域裡激活值最大的值,如下圖所示:
注意卷積層的輸出是24 24的,而池化後是12 12的。
就像上面提到的,卷積層通常會有多個特徵映射。我們會對每一個特徵映射進行max-pooling操作。因此,如果一個卷積層有3個特徵映射,那麼卷積加max-pooling後就如下圖所示:
我們可以把max-pooling看成神經網絡關心某個特徵在這個區域裡是否出現。它忽略了這個特徵出現的具體位置。直覺上看,如果某個特徵出現了,那麼這個特徵相對於其它特徵的精確位置是不重要的【精確位置不重要,但是大致的位置是重要的,比如識別一個貓,兩隻眼睛和鼻子有一個大致的相對位置關係,但是在一個2 2的小區域裡稍微移動一下眼睛,應該不太影響我們識別一隻貓,而且它還能解決圖像拍攝角度變化,扭曲等問題】。而且一個很大的好處是池化可以減少特徵的個數【2 2的max-pooling讓特徵的大小變為原來的1/4】,因此減少了之後層的參數個數。
Max-pooling不是唯一的池化方法。另外一種常見的是L2 Pooling。這種方法不是取2 2區域的最大值,而是2 2區域的每個值平方然後求和然後取平方根。雖然細節有所不同,但思路和max-pooling是類似的:L2 Pooling也是從卷積層壓縮信息的一種方法。在實踐中,兩種方法都被廣泛使用。有時人們也使用其它的池化方法。如果你真的想嘗試不同的方法來提供性能,那麼你可以使用validation數據來嘗試不同池化方法然後選擇最合適的方法。但是這裡我們不在討論這些細節。【Max-Pooling是用的最多的,甚至也有人認為Pooling並沒有什麼卵用。深度學習一個問題就是很多經驗的tricks由於沒有太多理論依據,只是因為最早的人用了,而且看起來效果不錯(但可能換一個數據集就不一定了),所以後面的人也跟著用。但是過了沒多久又被認為這個trick其實沒啥用】
放到一起
現在我們可以把這3個idea放到一起來構建一個完整的卷積神經網絡了。它和之前我們看到的結構類似,不過增加了一個有10個神經元的輸出層,這個層的每個神經元對應於0-9直接的一個數字:
這個網絡的輸入的大小是28 28,每一個輸入對於MNIST圖像的一個像素。然後使用了3個特徵映射,局部感知域的大小是5 5。這樣得到3 24 24的輸出。然後使用對每一個特徵映射的輸出應用2 2的max-pooling,得到3 12 12的輸出。
最後一層是全連接的網絡,3 12 12個神經元會連接到輸出10個神經元中的每一個。這和之前介紹的全連接神經網絡是一樣的。
卷積結構和之前的全連接結構有很大的差別。但是整體的圖景是類似的:一個神經網絡有很多神經元,它們的行為有weights和biase確定。並且整體的目標也是類似的:使用訓練數據來訓練網絡的weights和biases使得網絡能夠儘量好的識別圖片。
和之前介紹的一樣,這裡我們仍然使用隨機梯度下降來訓練。不過反向傳播算法有所不同。原因是之前bp算法的推導是基於全連接的神經網絡。不過幸運的是求卷積和max-pooling的導數是非常簡單的。如果你想了解細節,請自己推導。【這篇文章不會介紹CNN的梯度求解,後面實現使用的是theano,後面介紹CS231N的CNN是會介紹怎麼自己來基於自動求導來求這個梯度,而且還會介紹高效的算法,感興趣的讀者請持續關注】
CNN實戰
前面我們介紹了CNN的基本理論,但是沒有講怎麼求梯度。這裡的代碼是用theano來自動求梯度的。我們可以暫時把cnn看出一個黑盒,試試用它來識別MNIST的數字。後面的文章會介紹theano以及怎麼用theano實現CNN。
代碼
首先得到代碼: git clone
安裝theano
參考這裡 ;如果是ubuntu的系統,可以參考這裡 ;如果您的機器有gpu,請安裝好cuda以及讓theano支持gpu。
默認的network3.py的第52行是 GPU = True,如果您的機器沒有gpu,請把這一行改成GPU = False
baseline
首先我們實現一個baseline的系統,我們構建一個只有一個隱藏層的3層全連接網絡,隱藏層100個神經元。我們訓練時60個epoch,使用learning rate $\eta = 0.1$,batch大小是10,沒有正則化:
得到的分類準確率是97.8%。這是在test_data上的準確率,這個模型使用訓練數據訓練,並根據validation_data來選擇當前最好的模型。使用validation數據來可以避免過擬合。讀者運行時可能結果會有一些差異,因為模型的參數是隨機初始化的。
改進版本1
我們首先在輸入的後面增加一個卷積層。我們使用5 5的局部感知域,stride等於1,20個特徵映射。然後接一個2 2的max-pooling層。之後接一個全連接的層,最後是softmax(仿射變換加softmax):
在這種網絡結構中,我們可以認為卷積和池化層可以學會輸入圖片的局部的空間特徵,而全連接的層整合全局的信息,學習出更抽象的特徵。這是卷積神經網絡的常見結構。
下面是代碼:
【注意圖片的大小,開始是(mini_batch_size, 1, 28 ,28),經過一個20個5 5的卷積池層後變成了(mini_batch_size, 20, 24,24),然後在經過2 2的max-pooling後變成了(mini_batch_size, 20, 12, 12),然後接全連接層的時候可以理解成把所以的特徵映射展開,也就是20 12 12,所以FullyConnectedLayer的n_in是20 12 12】
這個模型得到98.78%的準確率,這相對之前的97.8%是一個很大的提高。事實上我們的錯誤率減少了1/3,這是一個很大的提高。【準確率很高的時候就看錯誤率的減少,這樣比較有成就感,哈哈】
如果要用gpu,可以把上面的命令保存到一個文件test.py,然後:
在這個網絡結構中,我們吧卷積和池化層看出一個整體。這只是一種習慣。network3.py會把它們當成一個整體,每個卷積層後面都會跟一個池化層。但實際的一些卷積神經網絡並不都要接池化層。
改進版本2
我們再加入第二個卷積-池化層。這個卷積層插入在第一個卷積層和全連接層中間。我們使用同樣的5 5的局部感知域和2 2的max-pooling。代碼如下:
【注意圖片的大小,開始是(mini_batch_size, 1, 28 ,28),經過一個20個5 5的卷積池層後變成了(mini_batch_size, 20, 24,24),然後在經過2 2的max-pooling後變成了(mini_batch_size, 20, 12, 12)。然後是40個5*5的卷積層,變成了(mini_batch_size, 40, 8, 8),然後是max-pooling得到(mini_batch_size, 40, 4, 4)。然後是全連接的層】
這個模型得到99.6%的準確率!
這裡有兩個很自然的問題。第一個是:加第二個卷積-池化層有什麼意義呢?事實上,你可以認為第二個卷積層的輸入是12*12的」圖片「,它的」像素「代表某個局部特徵。【比如你可以認為第一個卷積層識別眼睛鼻子,而第二個卷積層識別臉,不同生物的臉上面鼻子和眼睛的相對位置是有意義的】
這是個看起來不錯的解釋,那麼第二個問題來了:第一個卷積層的輸出是不同的20個不同的局部特徵,因此第二個卷積層的輸入是20 12 12。這就像我們輸入了20個不同的」圖片「,而不是一個」圖片「。那第二個卷積層的神經元學到的是什麼呢?【如果第一層的卷積網絡能識別」眼睛「,」鼻子「,」耳朵「。那麼第二層的」臉「就是2個眼睛,2個耳朵,1個鼻子,並且它們滿足一定的空間約束。所以第二層的每一個神經元需要連接第一層的每一個輸出,如果第二層只連接」眼睛「這個特徵映射,那麼只能學習出2個眼睛,3個眼睛這樣的特徵,那就沒有什麼用處了】
改進版本3
使用ReLU激活函數。ReLU的定義是:
使用ReLU後準確率從99.06%提高到99.23%。從作者的經驗來看,ReLU總是要比sigmoid激活函數要好。
但為什麼ReLU就比sigmoid或者tanh要好呢?目前並沒有很好的理論介紹。ReLU只是在最近幾年開始流行起來的。為什麼流行的原因是經驗:有一些人嘗試了ReLU,然後在他們的任務裡取得了比sigmoid好的結果,然後其他人也就跟風。理論上沒有人證明ReLU是更好的激活函數。【所以說深度學習有很多tricks,可能某幾年就流行起來了,但過幾年又有人認為這些tricks沒有意義。比如最早的pretraining,現在幾乎沒人用了。】
改進版本4
擴展數據。
深度學習非常依賴於數據。我們可以根據任務的特點」構造「新的數據。一種簡單的方法是把訓練數據裡的數字進行一下平移,旋轉等變換。雖然理論上卷積神經網絡能學到與位置無關的特徵,但如果訓練數據裡數字總是出現在固定的位置,實際的模型也不一定能學到。所以我們構造一些這樣的數據效果會更好。
expand_mnist.py這個腳本就會擴展數據。它只是簡單的把圖片向上下左右各移動了一個像素。擴展後訓練數據從50000個變成了250000個。
接下來我們用擴展後的數據來訓練模型:
這個模型的準確率是99.37%。擴展數據看起來非常trival,但是卻極大的提高了識別準確率。
改進版本5
接下來還有改進的辦法嗎?我們的全連接層只有100個神經元,增加神經元有幫助嗎? 作者嘗試了300和1000個神經元的全連接層,得到了99.46%和99.43%的準確率。相對於99.37%並沒有本質的提高。
那再加一個全連接的層有幫助嗎?我們來嘗試一下:
在第一個全連接的層之後有加了一個100個神經元的全連接層。得到的準確率是99.43%,把這一層的神經元個數從100增加到300個和1000個得到的準確率是99.48 %和99.47%。有一些提高但是也不明顯。
為什麼增加更多層提高不多呢,按說它的表達能力變強了,可能的原因是過擬合。那怎麼解決過擬合呢?一種方法就是dropout。drop的詳細解釋請參考這裡。簡單來說,dropout就是在訓練的時候隨機的讓一些神經元的激活「丟失」,這樣網絡就能學到更加魯棒的特徵,因為它要求某些神經元」失效「的情況下網絡仍然能工作,因此就不會那麼依賴某一些神經元,而是每個神經元都有貢獻。
下面是在兩個全連接層都加入50%的dropout:
使用dropout後,我們得到了99.60%的一個模型。
這裡有兩點值得注意:
訓練的epoch變成了40.因為dropout減少了過擬合,所以我們不需要60個epoch。
全連接層使用了1000個神經元。因為dropout會丟棄50%的神經元,所以從直覺來看1000個神經元也相當於只有500個。如果過用100個神經元感覺太少了點。作者經過驗證發現有了dropout用1000個比300個的效果好。
改進版本6
ensemble多個神經網絡。作者分別訓練了5個神經網絡,每一個都達到了99.6%的準確率,然後用它們來投票,得到了99.67%準確率的模型。
這是一個非常不錯的模型了,10000個測試數據只有33個是錯誤的,我們把錯誤的圖片都列舉了出來:
圖片的右上角是正確的分類,右下角是模型的分類。可以發現有些錯誤可能人也會犯,因為有些數字人也很難分清楚。
【為什麼只對全連接的層使用dropout?】
如果讀者仔細的閱讀代碼,你會發現我們只對全連接層進行了dropout,而卷積層沒有。當然我們也可以對卷積層進行dropout。但是沒有必要。因為卷積層本身就有防止過擬合的能力。原因是權值共享強制網絡學到的特徵是能夠應用到任何位置的特徵。這讓它不太容易學習到特別局部的特徵。因此也就沒有必要對它進行的dropout了。
更進一步
感興趣的讀者可以參考這裡,列舉了MNIST數據集的最好結果以及對應的論文。目前最好的結果是99.79%
What s Next?
接下來的文章會介紹theano,一個非常流行的深度學習框架,然後會講解network3.py,也就是怎麼用theano實現CNN。敬請關注。
責任編輯:MY
(原標題:環信人工智慧專家李理:詳解卷積神經網絡)