<EYD與機器學習>十三 CNN

<EYD與機器學習>十三 CNN

來自專欄 EYD與機器學習5 人贊了文章

各位知乎兒大家好,這是<EYD與機器學習>專欄讀書筆記系列的第十三篇文章,這篇文章以《Hands-on Machine Learning with Scikit-Learn and TensorFlow》(後面簡稱為HMLST)第十三章的內容為主線,其間也會加入我們成員的一些感受和想法與大家分享。

第十三章:Convolutional Neural Networks

在介紹HMLST前面幾章的時候我們一起學習了神經網路的基礎知識,訓練方法以及在TensorFlow框架下的應用。這次我們開始學習第一個真正意義上的神經網路演算法——卷積神經網路。

卷積神經網路,簡稱CNN,最早來源於對人腦的視覺皮層的研究,並在20世紀80年代被應用在圖像識別領域。最近幾年,由於人類計算能力的顯著提升,訓練數據的增加以及神經網路訓練演算法的不斷進步,CNN在很多複雜的視覺任務中都超過了人類,並且被廣泛應用在自動駕駛、人臉識別等領域。不僅如此,CNN還成功的應用在語音識別和NLP的領域。在這裡我們主要關注CNN在視覺領域的應用。

在這一章我們會介紹CNN的由來,CNN的組成部分,如何在TensorFlow下實現以及現有的一些優秀的CNN結構。

13.1 視覺皮質的結構

David H. Hubel 和 Torsten Wiesel 在1958和1959年用貓進行了一系列實驗,對視覺皮層的結構進行了重要說明。他們揭示了很多神經元在視覺皮層上有一小塊『局部感知域』,也就是說每個神經元只對位於視野有限區域的視覺刺激作出反應。如下圖所示:

不同神經元的感知域可能會重疊,但是這些感知域加在一起就會覆蓋整個視野。除此之外,兩位科學家表示一些神經元只對由水平線條構成的圖像有反應,有些神經元對其他方向的線條有反應。同時不同的神經元感知域的大小可能不同,擁有大的感知域的神經元會對那些由簡單模式構成的複雜模式有反應。正如上圖所示,深層的神經元是在附近淺層神經元的輸出的基礎上工作的,這也就是為什麼圖中神經元並不是全連接的,而是部分連接。

拋開那些專業術語,我們可以這樣來理解上述的視覺結構,人所看到的房子並不是作為一個整體一起被傳送和處理的,而是被很多個特徵提取器給拆分成了一些簡單的特徵,之後傳遞給後面的神經元進行處理,處理的過程可以理解為堆積木的過程,神經元將這些簡單的特徵進行映射和組合,慢慢的形成了許多複雜的特徵,最後映射出視野中的那個房子。

在1998年,Yann LeCun, Léon Bottou, Yoshua Bengio和Patrick Haffner的論文中提出了著名的LeNet-5 網路,該網路就是基於上述的視覺結構提出的。LeNet-5 的一些組成部分我們已經見過了,例如全連接層,Sigmoid激活函數,但是它還引入了卷積層和池化層這兩個重要的結構,接下來我們一起了解一下。

13.2 卷積層

卷積層是CNN最重要的組成部分:位於第一個卷積層的神經元並不是和輸入圖像的所有像素相連,而是只與那些在它感知域內的像素點相連。如下圖所示:

以此類推,位於第二個卷積層的神經元也只與第一層中位於一個小的矩形區域的部分神經元相連。這樣的結構能夠實現將第一個卷積層提取的低級特徵融合,並將它們在下一層中映射為更高級的特徵,並在各個層之間以此類推。

如下圖所示,位於某層第i 行第j 列的神經元,與前一層中第i~i+f_{h}-1 行第j~j+f_{w}-1 列的這塊區域內的神經元相連,其中f_{h}f_{w} 為前一層局部感知域的高和寬。為了讓前後兩層的大小相同,一般會在前一層的周圍加上零,這被稱為zero padding。

上圖中的局部感知域是緊挨著相互有重疊的,但是我們也可以通過使不同神經元的感知域隔行對齊來實現後一層神經元個數小於前一層神經元個數,如下圖所示:

圖中兩個感知域邊緣的距離被稱為stride,這裡在h w 方向都為2。在圖中,輸入層的大小為57,其中包含周圍的零。與其連接的神經層大小為3*4,每個神經元的感知域大小為3*3,stride為2。此時,位於後面那一層第i 行第j 列的神經元,與位於前面一行的第i* s_{h}~i* s_{h} +f_{h}-1 行第j* s_{w} ~j* s_{w}+f_{w}-1 列的這塊區域內的神經元相連, s_{h}s_{w} 分別是h w 方向的stride。

13.2.1 卷積核(filters)

從上述的結構中我們可以看出,每一個神經元的所有權重都可以被看做一張小圖片,其大小與該神經元對應的感知域大小相等,這些權重被稱為卷積核。如下圖所示:

圖中給出了兩中簡單的卷積核形式:第一種為豎直卷積核,在一個7*7的矩陣中,只有中間豎直白色的那一條為1,其餘部分均為0(黑色為0,白色為1)。神經元利用這些權重就可以實現忽略感知域中位於白線部分以外的部分,而只保留白線所對應的部分。第二種卷積核同理,不過是中間的水平白線為1,其餘為0。

假如我們將所有神經元的權重都設置為圖中第一種卷積核的形式(神經元的bias都相同),輸入圖像如上圖中底部的圖片所示,那麼會得到左上角所示的圖像,我們可以發現左上角的圖片與原圖片相比,豎直方向的線條被增強了,而水平方向變模糊了。同理,使用第二種帶有水平『白線』的卷積核則對水平方向的線條增強了,而豎直方向變模糊了。因此,如果某層的神經元使用相同的卷積核的話那麼就會得到一種特徵映射,映射的結果就是加強了圖像中與卷積核相似的特徵,而忽視了其他特徵。在訓練時,CNN會尋找對任務最有益的那些卷積核,同時學習著將學到的特徵組合成更加複雜的特徵。

13.2.2 疊加多個特徵映射

為了簡化問題,我們之前所展示的CNN都是一個簡單的二維網路,其實真實的CNN更像一個「三維」的網路。如下圖所示:

在實際操作中,我們會使用多個卷積核,每一個卷積核都會得到一種特徵映射。在每個特種映射中所有的神經元共享相同的參數,但是在不同的特徵映射之間參數則不同。簡而言之,CNN會對圖像施加多個卷積核,從而對圖像進行特徵提取。每一個卷積核都是施加在圖像的所有區域上的(不是一次性覆蓋圖像,而是通過移動來覆蓋),所以經過每一個卷積核的處理之後,圖像都經過一次特徵映射,多個卷積核的作用累加在一起就可以實現對圖像中各種特徵的提取。

同時,灰度圖只有一個通道,圖像如果是彩色的,那麼它會有R、G、B三個通道。還有其他的圖像類型可能會有更多的通道,例如衛星圖。

具體地,假設有一個位於第L 層的第k 個特徵映射的神經元,其位置為第i 行第j 列。與這個神經元連接的是第L-1 層所有特徵映射中位於第 i*s_{w}~i*s_{w}+f_{w}-1 行第 j*s_{h}~j*s_{h}+f_{h}-1 列區域內的神經元。下面的公式具體解釋了一個卷積層神經元的輸入輸出:

其中:

  • z_{i,j,k} 表示一個位於第L 層的第k 個特徵映射,位置為第i 行第j 列的神經元的輸出。
  • s_{h}s_{w} 是豎直方向和水平方向的stride, f_{h}f_{w} 是感知域的高和寬。 f_{n^{,}}是前一層(L-1)特徵映射的個數 。
  • x_{i^{,},j^{,},k^{,}} 表示一個位於第L-1 層的第 k^{,}個特徵映射,位置為第 i^{,} 行第 j^{,}列的神經元的輸出。
  • b_{k} 是第L 層的第k 個特徵映射的偏置項。
  • w_{u,v,k^{,},k} 是局部感知域中第u 行第v 列的神經元(不同於卷積層的坐標,這裡是指在感知域中的坐標)的權重,第L 層的第k 個特徵映射中的所有神經元和第L-1 層的第 k^{,}個特徵映射中的神經元都是用這個權重來連接。

13.2.3 在TensorFlow下實現

在TensorFlow中,每一個輸入圖片都被表示為一個三維的tensor,形狀為:[height, width, channels]。批量輸入時則會一起表示為一個四維的tensor,形狀為:[mini-batch size, height, width, channels]。卷積層的權重也被表示為一個四維tensor,形狀為:[ f_{h}f_{w}

f_{n}f_{n^{,}} ],而偏置項則被表示為一個一維tensor,形狀為:[ f_{n} ]。

下面我們來看一個簡單的例子,下面的代碼首先通過Scikit-Learn的 load_sample_images()函數載入了兩張圖片,之後建立兩個2.1節提到的7*7的卷積核,一個帶有豎直的『白線』一個帶有水平的『白線』,然後將這兩個卷積核通過TensorFlow的conv2d() 函數應用在載入的兩張圖片上,最後畫出經過這兩個卷積核特徵映射之後的圖片。

import numpy as npfrom sklearn.datasets import load_sample_images# Load sample imagesdataset = np.array(load_sample_images().images, dtype=np.float32)batch_size, height, width, channels = dataset.shape# Create 2 filtersfilters_test = np.zeros(shape=(7, 7, channels, 2), dtype=np.float32)filters_test[:, 3, :, 0] = 1 # vertical linefilters_test[3, :, :, 1] = 1 # horizontal line# Create a graph with input X plus a convolutional layer applying the 2 filtersX = 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]) # plot 1st images 2nd feature mapplt.show()

上述代碼大部分都比較簡單,我們也已經見過了,現在我來著重解釋一下conv2d()

這個函數(tf.nn.conv2d | TensorFlow):

  • X是輸入的批量圖片(如之前解釋的,X是一個四維的tensor)
  • filters是卷積核的集合(如之前解釋的,filters也是一個四維的tensor)
  • strides 是一個包含了四個元素的一維數組,中間的兩個元素用來控制豎直方向和水平方向的strides(s_{h}s_{w}),第一個和第二個元素通常設為1,當我們想設置一個batch的間隔或者通道的間隔時會用到。
  • padding 必須設置為"VALID" 或 "SAME":

——假如設置為「VALID」則卷積層就不會使用『零』來填充圖片邊緣,這樣在進行卷積操作的時候可能會忽略掉圖像底部或右邊的某些行或者列。如下圖所示(圖中只給出了在水平方向進行卷積的情況,豎直方向也是如此)。

——假如設置為「SAME」則卷積層就會使用『零』來填充圖片邊緣(如果需要的話)。在這種情況下,輸出神經元的個數等於輸入神經元個數除以stride並向上取整(如下圖就為13/5)。其中填充的零會儘可能均勻的添加在輸入的周圍。

對於使用CNN,很帶技巧性的一點就是設置超參數:卷積核的個數、卷積核的形狀、stride的大小和padding的類型等,一般情況下我們可以通過交叉驗證來確定參數,但是這是很費時的,後續我們會介紹一些CNN可結構,從中我們可以學習到這些超參數的作用。

13.2.4 內存需求

除了上述的超參數問題,CNN尤其是卷積層是十分耗費內存的,尤其是在訓練過程中,由於反向傳播時需要用到前向過程的中間值,這使得需要存儲的數據量變得很大。接下來我們學習CNN的第二個重要組成部分:pooling layer(池化層),池化層的使用可以極大的緩解內存問題,而且還對特徵提取有很大幫助。

13.3 池化層

池化層的目的是對圖像進行降採樣,從而減少計算量,減少內存消耗以及參數的數量(可以減少過擬合的風險)。減少輸入圖像的尺寸同時還可以是神經網路能夠對圖像偏移的敏感度降低。

與卷積層相似,每一個池化層的神經元都只與前一層的矩形區域內的有限數量神經元相連,我們與可以定義這個區域的大小,stride和padding類型。然而不同的是,池化層的神經元沒有權重,這些神經元要做的就是使用聚合函數來對神經元的輸入進行聚合,例如max pooling或者average pooling。下圖演示了max pooling的操作過程,所用的池化核(pooling kernel)大小為2*2,stride為2,沒有padding。在每次進入池化核範圍的輸入只有最大值得到保留,而其他值都被拋棄了。

其實從信息的完整程度來看,池化層是十分有害的,因為從上面的例子可以看出,即使是使用2*2(面積很小)的池化核,stride為2,輸出圖片的大小都變為了原圖的四分之一大小,也就是說丟失了75%的信息。

一個池化層會分別應用在輸入的各個通道上,所以輸出的深度和輸入深度是相同的。不過我們也可以選擇在通道這個維度上進行池化,那麼圖像的大小(寬和高)不會變化,但是通道數減少了。

在TensorFlow下使用一個池化層是十分簡單的,下面的代碼實現了stride為2,池化核大小為2*2,沒有padding的池化層,並將其用在了數據集上:

[...] # load the image dataset, just like above# Create a graph with input X plus a max pooling layerX = 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 imageplt.show()

其中max_pool函數的ksize參數表示在輸入tensor(形狀為[batch size, height, width, channels])的四個維度上的池化核大小。在本書成書時,TensorFlow還不支持在多個樣本間進行池化,所以ksize[0]必須為1。同時,TensorFlow還不支持在實際空間維度(圖像的寬和高)與深度之間同時進行池化。所以ksize[1]和 ksize[2]必須同時設為1,或者ksize[3]設為1。如果想要使用平均池化(average pooling layer),只需要將max_pooling替換為avg_pool。

13.4 幾種流行的CNN結構

典型的CNN結構首先堆疊一些卷積層(每一個卷積層後面一般會跟隨著ReLU層),之後是一個池化層,然後又是卷積層(+ReLU層),再是池化層,一直重複到達設計的層數。在最後階段,會加入適當層數的全連接層,將前面幾層提取的特徵進行組合和映射,構建分類器。如下圖所示,圖像在網路中傳遞的過程中會變得越來越小,並且卷積層和池化層會越來越深(因為卷積核並不只有一個)。

在近幾年,CNN得到了迅猛的發展,很多團隊提出了具有創造力的結構,並且在各中視覺任務中都有不錯的表現,下面我們來一起學習一下CNN框架下各種優異的結構。

13.4.1 LeNet-5

LeNet-5 可能是最廣為人知的一種CNN結構,它是1998年由Yann LeCun等人提出的,並成功的應用在手寫數字識別(數據集為MNIST)的任務中。它的組成部分如下表所示:

對於上表的參數,有幾點需要解釋:

  • MNIST數據集中的圖片為2828,但是這裡作者將他們用零進行了padding,因此變為了32*32,並且在輸入到網路之前還將圖片進行了規範化。在之後的過程中就不在進行padding了,因此圖片的大小在不斷減小。
  • 表中用到了平均池化(average pooling layers),不同於最大池化,每個神經元計算自己輸入的平均值,之後乘上一個可學習的參數(每個映射對應一個),再加上偏置項(每個映射對應一個)然後傳遞給激活函數。
  • 在C3中的大多數神經元只與S2中的三四個映射相連,而不是與全部的六個映射相連。
  • 輸出層使用的徑向基函數,而不是現在流行的交叉熵損失函數,因為在那個年代交叉熵損失函數還沒有廣泛利用。

13.4.2 AlexNet

AlexNet 是2012年ImageNet ILSVRC挑戰賽的冠軍,它達到了17%的top-5錯誤率,而當時的第二名只有26%。AlexNet的提出者是Alex Krizhevsky (所以叫AlexNet ), Ilya Sutskever, 和 Geoffrey Hinton。AlexNet的結構與 LeNet-5的結構很相似,但是層數更多,損失函數不同,並且在具體細節上有較大的差異。下表示AlexNet的結構:

由於層數的增加,模型的複雜度顯著提升,因此作者使用了兩種正則化的方法來減緩過擬合:首先,在訓練時,在F8 和 F9這兩個全連接層使用了dropout;另外,作者在訓練時對數據進行了增強,通過旋轉,剪切以及調整亮度等方法擴充了數據。

AlexNet 在C1和C3層使用了local response normalization(LRN),也就是局部響應歸一化。局部響應歸一化原理是仿造生物學上活躍的神經元對相鄰神經元的抑制現象(側抑制),在演算法中也就是某一個高度激活的神經元會抑制它附近的神經元,但是這些神經元是相鄰特徵映射中的。這樣能夠特化各個特徵映射,使各種映射能夠專一化,從而搜尋到更多的特徵。下面的公式給出了LRN的具體計算:

  • b_{i}是位於第 i 個特徵映射的神經元歸一化之後的輸出,其中神經元的坐標為第u行第v列,因為在方程中我們考慮的是各個特徵映射的同一個坐標的神經元輸出,所以這裡省略了行列坐標。
  • a_{i}是經過ReLU之後的輸出,還沒有進行LRN
  • kalpha,etar 為超參數,k 被稱為偏置,r 為深度半徑
  • f_{n} 為特徵映射的數量

在TensorFlow下,我們可以通過local_response_normalization() 來實現LRN。

13.4.3 GoogLeNet

GoogLeNet 由Christian Szegedy 等人提出,並且贏得了ILSVRC 2014挑戰賽,它將top-5錯誤率提升到了低於7%。GoogLeNet 在當時可以取得這麼大的進步一部分源於它的層數較之前面的結構有極大的提升,而網路層數的提升又源於inception modules的使用,inception modules 使GoogLeNet 對參數的利用率比之前的結構提高了很多(GoogLeNet 的參數數量大概是AlexNet的十分之一 )。

下圖介紹了inception module的具體結構,其中「3 × 3 +2(S)」 是指這一層使用3*3的核,stride為2,padding類型為『SAME』。首先,輸入圖像被輸入給四個不同的層,所有的卷積層使用的都是ReLU激活函數。其次,第二類卷積層使用了不同尺寸的卷積核(1 × 1, 3 × 3, 和 5 × 5),這使得它們可以捕捉不同尺度的特徵。同時,每一個單獨的卷積層使用的stride均為1,padding也都為『SAME』,所以它們的輸出都和輸入有著同樣的大小,這也使得它們能夠在最後的depth concat layer(將四個卷積層的特徵映射疊加起來)連接在一起。

在inception module 中有一點很特別,那就是使用了size為1*1 的卷積核,這些層有兩個作用:

  • 第一個作用是降維,這些層能夠輸出比輸入小很多的圖像,這使得後續使用大尺寸卷積核時計算負荷小很多。
  • 第二,我們來同時觀察兩層卷積網路([1 × 1, 3 × 3] 和 [1 × 1, 5 × 5]),其實它們相當於是一個更加強有力的卷積層,能夠捕捉更加複雜的特徵。

簡而言之,我們可以將inception module 當做一個能夠捕捉多尺度複雜特徵的卷積層,接下來我們來一起學習一下GoogLeNet 的結構 。如下圖所示,網路很深,包含九個inception module。每個卷積層使用的都是ReLU激活函數。

讓我們來一起看一下網路內部的情況:

  • 網路的前兩層將圖片在高和寬的方向都縮小四倍,也就是將圖片的面積縮小到了1/16,加速了訓練。
  • 之後的LRN層保證網路能夠學習到很多豐富的特徵。
  • 然後連接著兩個卷積層,第一層是1*1的卷積核,後面是3*3的卷積核,也就是我們上面介紹的那種情況,可以提取更加複雜的特徵。
  • 之後又是一個LRN。
  • 接著是一個池化層,高和寬都減少2,從而加速了訓練。
  • 接下來就是九個疊加的inception modules,中間插入了最大池化來減小圖像面積。
  • 下面連接的是平均池化層,padding類型為「VALID」,輸出的是1*1的特徵映射,這也被稱為是全局平均池化(global average pooling)。這種策略可以有效的推動前面的那些層來產生能夠有效代表各個類的特徵,同時也就減少了後續全連接層的數量(對比AlexNet),這也實現了緩解過擬合。
  • 最後的幾層是全連接層,進行了dropout來正則化,最後使用softmax來預測各個類別的概率。

13.4.4 ResNet

在這裡我們最後介紹的是殘差網路(the Residual Network),是2015 ILSVRC挑戰賽的冠軍,由Kaiming He等人提出 。ResNet將top-5錯誤率提成到了低於3.6%,這在當時十分驚艷,同時網路的層數也到達了152層。如此深的網路,我們之前提到的那些深度網路容易出現的問題(如梯度彌散等)是很難解決的。但是這裡,殘差網路引入了一種新的訓練技巧:skip connections(也被稱為shortcut connections)。當訓練一個網路時,我們的目的是對目標函數h(x)建模,然而殘差學習的目標不是對h(x)建模,而是對f(x) = h(x) – x 建模,也就是將輸入x加入到網路的輸出中去。如下圖所示:

當我們對一個常規的神經網路初始化時,它的權重會接近於零,所以輸出也接近於零。當加入skip connection時,網路相當於將輸入的副本輸出了,換言之,它最初是在對恆等函數建模,這樣會極大的加速訓練。

此外,如果加入很多skip connections,即使網路中的一些層沒有開始學習,網路也可以開展進程。如下圖所示:

由於skip connections的存在,信號可以輕鬆的貫穿整個網路。整個殘差網路可以看做是多個殘差單元疊加在一起,每個殘差單元都是一個帶有skip connection的小型神經網路。

接下來我們來看殘差網路的結構,如下圖所示:

其結構很簡單,與GoogLeNet有很多相似的地方,不過其中疊加的是殘差單元。每個殘差單元由帶有 Batch Normalization (BN)和ReLU激活函數的兩層卷積層組成,卷積核大小為3*3,stride為 1,padding類型為「SAME」,如下圖所示:

13.5 總結

卷積網路在近十年發展的十分迅猛,並且與計算機視覺的研究相輔相成,各種新的結構不斷的提出,例如本文沒有介紹的VGGNet,Inception-v4等等。但是萬丈高樓平地起,我們在學習時還是要著眼於這些複雜網路背後的知識。其實我們可以發現本質的知識並沒有太大變化,只是引入了一些技巧,以及創造了各種知識不同的運用方法,這些東西只有我們能夠理解和掌握了基礎原理之後才能做到,所以希望大家能夠沉下心來好好的學習最基礎的東西。

在下一篇文章中我們將向大家介紹循環神經網路(RNN),以及如何在TensorFlow這個框架下實現一些基礎的功能。同時歡迎大家對本專欄的文章進行勘誤,我們會虛心接受大家的建議 和意見。

——Double_D 編輯

完整代碼:

ageron/handson-ml?

github.com圖標
推薦閱讀:

TAG:卷積神經網路CNN | 深度學習DeepLearning | 神經網路 |