第十三章——卷積神經網絡(CNN)


卷積神經網絡(Convolutional neural networks,CNNs)來源於對大腦視覺皮層的研究,並於1980s開始應用於圖像識別。現如今CNN已經在復雜的視覺任務中取得了巨大成功,比如圖像搜索,自動駕駛,語言自動分類等等。同時CNN也應用於了其他領域,比如語音識別和自然語言處理。

13.1 視覺皮層機理

David H. Hubel和Torsten Wiesel於1958、1959年在貓的身上做實驗,給出了關於視覺皮層結構的深刻見解(作者因此與1981年獲得諾貝爾生物或醫學獎)。特別的,他們指出視覺皮層的許多神經元具有一個很小的局部接受域(local receptive field)。也就是說,這種神經元只會對視野里一個有限的區域做出反應(如圖13-1所示,這5個神經元的局部接受域由5個虛線表示)。不同神經元的局部接受與可能部分重疊,放在一起就構成了整個視野。此外,作者指出一些神經元只對水平線感興趣,而另一些只對線的不同方向做出反應。他們也注意到,一些神經元具有較大的接受域,只會對由低級模式組合成的高級模式做出反應。這些觀察結果帶來了這樣一個理念:高級神經元只以臨近的低級神經元為基礎(如圖13-1,高層神經元只與部分地層神經元連接)。這一強大的結構可以檢測到視覺領域的各種復雜模式。

 

圖13-1 視覺皮層的局部接受域

關於視覺皮層的研究激發了神經認知機(neocognitron,1980年引入),並逐步演變成如今的卷積神經網絡。重要的里程碑是1998年的一篇論文,介紹了著名的LeNet-5架構,並廣泛應用於手寫數字識別。該架構的一些模塊我們已經了解了,比如全連接和sigmoid激活函數,但是還要一些新的模塊:卷積層(convolutional layers)和池化層(pooling layers)。

你也許會有疑問,為什么不用常規的全連接深度神經網絡來完成常規的圖像識別任務呢?不幸的是,它雖然對小圖片(比如MNIST)表現良好,但對於高清大圖就無能為力了。例如,一個$100 \times 100$圖片具有10000像素,即使第一個隱層只有1000神經元(已經極大地減少了傳遞給下層的信息),這意味僅僅是第一個隱層就會帶來一千萬的連接。而CNNs會通過使用部分連接層來解決這一問題。

13.2 卷積層

CNN最重要的構件就是卷積層(卷積是一種數學運算,度量兩個函數乘積的逐點積分。其與傅里葉變換與拉普拉斯變換高度相關,在信號處理中應用廣泛。卷積層實際上使用了互相關,這與卷積是相似的。卷積處理的是連續變量,互相關好像是離散變量。):第一個卷積層的神經元並不和所有的輸入全連接,只與其接受域的像素點連接(如圖13-2)。依次的,第二個卷積層也與第一個卷積層部分相連。這一架構使得第一個隱層專注於低級別特征,然后在隨后的隱層中將低級別特征進行組合。

圖13-2 矩形局部接受域的CNN層

此前的神經網絡處理的都是1D數據,而現在處理的是2D數據,這使得神經元更容易匹配到與其相關的輸入。

位於第$i$行第$j$列的神經元,與之相連的前層神經元是$i$到$i + f_w - 1$行,$j$到$j + f_h - 1$列。其中,$f_h$和$f_w$是接受域的高和寬(如圖13-3)。為了使兩層的高和寬保持一致,需要在輸入層的周圍補零,這被稱作零填充(zero padding)。

 

圖13-3 卷積層連接,以及零填充

通過將接受域隔行划分,可以使得高層的尺寸小於底層,如圖13-4。兩個連續接受域的距離稱為跨度(stride)。一個$5 \times 7$的輸入層連接一個$3 \times 4$的高層,接受域為$3 \times 3$,跨度為2(在這個例子中,兩個方向的跨度是一樣的,這也可以不一樣)。與一個位於$i$行$j$列的高層神經元連接的是$i \times s_w$到$i \times s_w + f_w - 1$行,$j \times s_h$到$j \times s_h + f_h - 1$列的低層神經元。其中,$s_w$和$s_h$是水平跨度和垂直跨度。

圖13-4 通過跨度降維

13.2.1 過濾器

 神經元的權重可以表示接受域內的一個小的影像。例如,圖13-5展示了兩套可能的權重,稱作過濾器(或者卷積核)。第一個過濾器描繪了一個在正中間擁有白色直線的黑塊(這是一個$7 \times 7$的矩陣,除了中間那條豎線是1之外,其他地方都是0)。神經元使用這樣的權重,只會注意到其接受域中間的那條豎線,而忽視其他像素點。第二個過濾器是同樣大小的黑塊,但中間是條白色的水平線。神經元使用這樣的權重,只會注意到其接受域中間的那條直線。

圖13-5 應用兩種不同的過濾器來獲取特征圖

如果一層的神經元使用同樣的垂線過濾器(偏置項也一致),輸入是圖13-5底部的圖像,將會輸出左上角的圖像。相似的,如果使用水平線過濾器,將會得到右上角的圖像,可以看到,水平線增強了,其他部分變模糊了。因此,一層神經元使用同樣的過濾器,可以得到一個特征圖(feature map),將圖像中與過濾器最相似的部分凸顯出來。在訓練過程中,一個CNN根據其任務存在最有用的過濾器,然后學着將其組合成更復雜的模式。

13.2.2 堆疊多重特征圖(Stacking Multiple Feature Maps)

當目前為止,為了簡單起見,我們將每個卷積層都表示為了簡單的2D層。事實上,它是由許多大小相同的特征圖組成,因此用3D表示一個卷積層更精確(如圖13-6)。在一個特征圖中,所有神經元共享同樣的參數(權重和偏置),不過不同的特征圖有不同的參數。神經元的接受域與之前的描述相同,但是被擴展到了前層的所有特征圖。簡而言之,一個卷積層會將多重過濾器同時應用於輸入,以便檢測輸入中任何地方的多重特征。

圖13-6 卷積層有多個特征圖,圖像有三個通道

由於一個特征圖的所有神經元共享同樣的參數,這顯著地降低了模型的參數數量。更重要的是,這意味着CNN在一個地方識別出來一種模式,還能在其他地方識別出這一模式。相反,一個普通的DNN在一個地方識別出了一種模式,它這能在這個特定的地方識別這一模式。

此外,輸入圖像也可能都多個子層組成:一個顏色通道一個子層。灰度圖只用一個通道,但是有個圖可以有更多的通道——例如,衛星圖像可以捕捉到額外的光頻率(比如紅外線)。

具體的,對於第$l$個卷積層的第$k$個特征圖的第$i$行$j$列的神經元,與之連接的底層($l-1$層)神經元位於$i \times s_w$到$i \times s_w + f_w - 1$行,$j \times s_h$到$j \times s_h + f_h - 1$列,穿過所有的特征圖($l-1$層中的)。第$l$層中位於不同特征圖的第$i$行$j$列神經元連接的是完全相同的前層神經元的輸出。

一個卷積層一個神經元的計算公式:

\begin{align*}
z_{i,j,k} = b_k + \sum_{u=1}^{f_h}\sum_{v=1}^{f_w}\sum_{k'=1}^{f_{n'}} x_{i',j',k'} \cdot w_{u,v,k',k}
\end{align*}

需要注意的是,求和的時候並沒有$i$和$j$。每一個特征圖使用的權重是一樣的。所以這不僅僅是第$i$行$j$列神經元的輸出,並沒有特指是哪個神經元的輸出,而是第$l$層第$k$個特征圖中每個神經元輸出的計算公式。理解公式時,我們忽略$i$和$j$即可。

其中,

  • $i' = u \cdot s_w + f_w - 1$
  • $j' = v \cdot s_h + f_h - 1$
  • $z_{i,j,k}$是第$l$卷積層第$k$個特征圖,第$i$行$j$列神經元的輸出。
  • $s_h$和$s_w$分別是垂直和水平跨度,$f_h$和$f_w$分別是接受域的高和寬,$f_{n'}$是前層特征圖的個數($l-1$層)。
  • $x_{i',j',k'}$是第$l-1$層,第$k'$個特征圖(如果前層是輸入層,那就是第$k'$個通道),第$i'$行$j'$列神經元的輸出。
  • $b_k$是第$l$層第$k$個特征圖的偏置項。可以把它看做一個開關,來調整第$k$個特征圖的整體亮度。
  • $w_{u,v,k',k}$是第$l$層第$k$個特征圖中的任意一個神經元,它的第$k'$個特征圖的第$u$行第$v$列的輸入的權重。

13.2.3 TensorFlow實現

相關代碼可參考本書代碼

在TensorFlow中,每個輸入圖像都被表示成形狀為[height, width, channels]的3D張量。一個mini-batch表示成形狀是[mini-batch size, height, width, channels]的4D張量。卷積層的權重表示成形狀為$[f_h, f_w, f_n, f_{n'}] $的4D張量。卷積層偏置表示成形狀為$[f_n]$的1D張量。

我們來看一個簡單的例子。下面代碼首先使用Scikit-Learn的load_sample_images()加載兩幅圖像。然后創建兩個$7 \times 7$的過濾器(與圖13-5相同),然后通過一個卷積層將這兩個過濾器應用於兩幅圖,該卷積層有TensorFlow的conv2d()函數創建(包括0填充和大小為2的跨度)。最后繪制了其中的一個卷積層。

import numpy as np
from sklearn.datasets import load_sample_images

# 加載兩個圖片
dataset = np.array(load_sample_images().images, dtype=np.float32)
batch_size, height, width, channels = dataset.shape

# 創建兩個過濾器
filters_test = np.zeros(shape=(7, 7, channels, 2), dtype=np.float32)
filters_test[:, 3, :, 0] = 1 # 垂線
filters_test[3, :, :, 1] = 1 # 水平線

# 創建一個圖,包括輸入X和使用了兩個過濾器的一個卷積層。
X = tf.placeholder(tf.float32, shape=(None, height, width, channels))
convolution = tf.nn.conv2d(X, filters, strides=[1,2,2,1], padding="SAME")

with tf.Session() as sess:
    output = sess.run(convolution, feed_dict={X: dataset})

plt.imshow(output[0, :, :, 1]) # 繪制第一幅圖的第二個特征圖
plt.show()  

關於conv2d()函數的一些說明:

  • X是mini-batch輸入(一個4D張量,如前所示)
  • filters是一個過濾器集(4D張量,如前所示)
  • strides擁有4個元素的一維數組,中間兩個元素表示垂直和水平跨度。第一和第四個元素目前直接設置為1。以后可能會用來表示批跨度(跳過一些實例),以及通道跨度(跳過先前層的一些通道或者特征圖)。
  • padding有"VALID"、"SAME"兩個選擇:
    • 如果設置為"VALID",卷積層不使用0填充,可能會直接忽略邊緣的一些輸入,如圖13-7所示(為簡單起見,圖中只顯示了中間的水平線)。
    • 如果設置為"SAME",卷積層在必要的時候使用0填充。

圖13-7 填充選項——輸入寬度:13,過濾器寬度:6,跨度:5

不幸的是,卷積層有許多超參數需要調整。必須選擇它們的過濾器數量,以及高、寬、跨度、填充類型。可以用網格搜索選擇超參數,但這比較耗時。在隨后介紹CNN架構師,會給出超參數選擇的一些最佳實踐。

13.2.3 內存要求

CNN的另一個問題是卷積層需要使用大量的RAM,尤其是訓練的時候,因為反向傳播需要前向傳遞的所有中間值。

比如,考慮一個卷積層,過濾器的形狀是$5 \times 5$,需要輸出形狀為$150 \times 100$的200個特征圖(需要使用200個過濾器),其中跨度為1,padding選擇SAME。如果輸入是一個$150 \times 100$的RGB圖像(3個通道),那么參數的個數就是$(5 \times 5 \times 3 + 1) \times 200 = 15200$(+1是由於偏置項),這一數字已經遠小於全連接了。但由於200個特征圖中的每一個都包含$150 \times 100$個神經元,計算每個神經元的輸出時都要用權重乘以$5 \times 5 \times 3 = 75$個輸入然后再加和:這總共是2.25億次浮點乘法運算。此外,如果用32-bit浮點數表示特征圖,這個卷積層輸出占據RAM的空間將達到200 × 150 × 100 × 32 = 96百萬比特(大約11.4 MB)。而這僅僅是一個樣本。如果這個訓練批次有100個樣本,這一層將會使用超過1G的RAM。

進行預測時,只要一層計算完畢,其前層就可以釋放掉。因此所需的RAM只要能容納連個連續層即可。但是在訓練時,每層前向傳播的輸出都要保留下來,以便用於反向傳播時計算梯度。此時所需的RAM至少可以存儲所有層的輸出。

如果訓練時內存溢出,可以減小mini-batch的大小。此外,也可以通過增大跨度,或者減少層級。或者使用16-bit浮點數代替32-bit浮點數。也可以分布式訓練。

13.3 池化層(Pooling Layer)

池化層的目標是通過二次抽樣,來減少計算量、內存使用、參數數量(降低了過擬合的風險)。降低圖像尺寸也使得模型可以忍受一點點圖像移位。和卷積層一樣,池化層也只和前層的部分神經元連接,同樣是一個矩形接受域。也需要定義接受域的尺寸、跨度、填充類型。不過,一個池化神經元沒有權重,僅僅是使用諸如max、mean的聚合函數對輸入進行聚合。 圖13-8展示了一個max池化層,這是最常見的池化層。在這例子中,使用了2 × 2池化核,跨度2,無填充。只用每個核的最大輸入才會傳遞給下一層,其他的輸入被丟棄掉。

圖13-8 Max池化層(2 × 2池化核,跨度2,無填充)

很明顯,每個方向的輸出的尺寸都只有輸入的一半(面積只有1/4),丟掉了輸入值的75%。

池化層一般獨立地工作於每個輸入通道,因此輸出深度和輸入深度是一樣的。隨后我們會越高一些通道,在這種情形下圖像的空間維度不變(寬和高),但是通道會減少。

TensorFlow實現:

[...] # load the image dataset, just like above

# Create a graph with input X plus a max pooling layer
X = tf.placeholder(tf.float32, shape=(None, height, width, channels))
max_pool = tf.nn.max_pool(X, ksize=[1,2,2,1], strides=[1,2,2,1],padding="VALID")

with tf.Session() as sess:
    output = sess.run(max_pool, feed_dict={X: dataset})

plt.imshow(output[0].astype(np.uint8)) # plot the output for the 1st image
plt.show()

其中,ksize是一個四維張量,包含了每個維度池化核的形狀[batch size, height, width, channels]。TensorFlow目前還不支持越過實例,因此ksize[0]必須是1。此外,也不支持同時越過空間維度(寬和高)和深度。因此,或者ksize[1]、ksize[2]同時為1,或者ksize[3]為1。

13.4 CNN架構

典型的CNN架構會堆疊一些卷積層(每個卷積層跟着ReLU層),然后是一個池化層,然后另外一些卷積層(+ReLU), 然后再是池化層,等等。圖像會越來越小,但一般也會越來越深(也就是更多的特征圖)。在堆的最頂端,會有常規的全連接前向神經網絡(+ReLUs),輸出層輸出概率(例如,對於圖像分類問題,softmax層輸出每個類別的概率)。

圖13-9 典型的CNN架構

一個常見的錯誤是使用了尺寸太大的卷積核。堆疊兩個3 × 3的卷積核,可以達到與一個9 × 9卷積核相似的效果,但前者的計算量小得多。

近年來,這一架構的變體逐漸成熟,並取得了巨大的成功。一個衡量標准就是在該類競賽中錯誤率的降低。可以參考ILSVRC ImageNet challenge

我們首先介紹一下經典的LeNet-5架構(1998),然后是ILSVRC挑戰賽的三個獲勝者:AlexNet (2012), GoogLeNet (2014)和ResNet(2015)。

其他的視覺任務:目標檢測和定位,以及圖像分割。在圖像檢測和定位中,這樣網絡一般會輸出一系列包圍盒,將不同的對象包圍起來。例如,Maxine Oquab等人2015年的論文,輸出每個對象類的熱圖(heat map)。Russell Stewart等人2015年的論文,使用CNN檢測人臉,並使用RNN識別邊界。對於圖像分割,可參考這篇論文

13.4.1 LeNet-5

LeNet-5是最廣為人知的CNN架構。在1998年提出比廣泛應用於手寫數字識別(MNIST)。由以下各層組成:

其中,

  • MNIST圖像是28 × 28像素,0填充后是32 × 32像素,並且在輸入之前進行標准化(normalized)。剩下的各層不再填充。
  • 這里的平均池化層(average pooling layers)比常規的稍微復雜:每個神經元求出其輸入均值后,要對該均值乘以一個可學習的系數(每個特征圖一個系數)並加上一個可學習的偏置(也是每個特征圖一個),然后將結果應用於激活函數。
  • C3中的大部分神經元只與S2中的3到4個特征圖連接,而不是全部的6個。
  • 輸出層也有一點特殊,具體可以看論文。

LeNet-5數字分類的demo,可參考Yann LeCun的網站("LENET" 章節)。


后面的AlexNet (2012), GoogLeNet (2014)和ResNet(2015)有時間再補充吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM