《機器學習實戰:基於Scikit-Learn、Keras和TensorFlow》第14章 使用卷積神經網路實現深度電腦視覺

  • 2019 年 12 月 20 日
  • 筆記

儘管IBM的深藍超級電腦在1996年擊敗了國際象棋世界冠軍加里·卡斯帕羅夫,但直到最近電腦才能從圖片中認出小狗,或是識別出說話時的單詞。為什麼這些任務對人類反而毫不費力呢?原因在於,感知過程不屬於人的自我意識,而是屬於專業的視覺、聽覺和其它大腦感官模組。當感官資訊抵達意識時,資訊已經具有高級特徵了:例如,當你看一張小狗的圖片時,不能選擇不可能,也不能迴避的小狗的可愛。你解釋不了你是如何識別出來的:小狗就是在圖片中。因此,我們不能相信主觀經驗:感知並不簡單,要明白其中的原理,必須探究感官模組。

卷積神經網路(CNN)起源於人們對大腦視神經的研究,自從1980年代,CNN就被用於影像識別了。最近幾年,得益於算力提高、訓練數據大增,以及第11章中介紹過的訓練深度網路的技巧,CNN在一些非常複雜的視覺任務上取得了超出人類表現的進步。CNN支撐了圖片搜索、無人駕駛汽車、自動影片分類,等等。另外,CNN也不再限於視覺,比如:語音識別和自然語言處理,但這一章只介紹視覺應用。

本章會介紹CNN的起源,CNN的基本組件以及TensorFlow和Keras實現方法。然後會討論一些優秀的CNN架構,和一些其它的視覺任務,比如目標識別(分類圖片中的多個物體,然後畫框)、語義分割(按照目標,對每個像素做分類)。

視神經結構

David H. Hubel 和 Torsten Wiesel 在1958年和1959年在貓的身上做了一系列研究,對視神經中樞做了研究(並在1981年榮獲了諾貝爾生理學或醫學獎)。特別的,他們指出視神經中的許多神經元都有一個局部感受野(local receptive field),也就是說,這些神經元只對有限視覺區域的刺激作反應(見圖14-1,五個神經元的局部感受野由虛線表示)。不同神經元的感受野或許是重合的,拼在一起就形成了完整的視覺區域。

另外,David H. Hubel 和 Torsten Wiesel指出,有些神經元只對橫線有反應,而其它神經元可能對其它方向的線有反應(兩個神經元可能有同樣的感受野,但是只能對不同防線的線有反應)。他們還注意到,一些神經元有更大的感受野,可以處理更複雜的圖案,複雜圖案是由低級圖案構成的。這些發現啟發人們,高級神經元是基於周邊附近低級神經元的輸出(圖14-1中,每個神經元只是連著前一層的幾個神經元)。這樣的架構可以監測出視覺區域中各種複雜的圖案。

圖14-1 視神經中生物神經元可以對感受野中的圖案作反應;當視神經訊號上升時,神經元可以反應出更大感受野中的更為複雜的圖案

對視神經的研究在1980年啟發了神經認知學,後者逐漸演變成了今天的卷積神經網路。Yann LeCun等人再1998年發表了一篇里程碑式的論文,提出了著名的LeNet-5架構,被銀行廣泛用來識別手寫支票的數字。這個架構中的一些組件,我們已經學過了,比如全連接層、sigmod激活函數,但CNN還引入了兩個新組件:卷積層和池化層。

筆記:為什麼不使用全連接層的深度神經網路來做影像識別呢?這是因為,儘管這種方案在小圖片(比如MNIST)任務上表現不錯,但由於參數過多,在大圖片任務上表現不佳。舉個例子,一張100 × 100像素的圖片總共有10000個像素點,如果第一層有1000個神經元(如此少的神經元,已經限制資訊的傳輸量了),那麼就會有1000萬個連接。這僅僅是第一層的情況。CNN是通過部分連接層和權重共享解決這個問題的。

卷積層

卷積層是CNN最重要的組成部分:第一個卷積層的神經元,不是與圖片中的每個像素點都連接,而是只連著局部感受野的像素(見圖14-2)。同理,第二個卷積層中的每個神經元也只是連著第一層中一個小方形內的神經元。這種架構可以讓第一個隱藏層聚焦於小的低級特徵,然後在下一層組成大而高級的特徵,等等。這種層級式的結構在真實世界的圖片很常見,這是CNN能在圖片識別上取得如此成功的原因之一。

圖14-2 有方形局部感受野的CNN層

筆記:我們目前所學過的所有多層神經網路的層,都是由一長串神經元組成的,所以在將圖片輸入給神經網路之前,必須將圖片打平成1D的。在CNN中,每個層都是2D的,更容易將神經元和輸入做匹配。

位於給定層第i行、第j列的神經元,和前一層的第i行到第i + fh – 1行、第j列到第j + fw – 1列的輸出相連,fh和fw是感受野的高度和寬度(見圖14-3)。為了讓卷積層能和前一層有相同的高度和寬度,通常給輸入加上0,見圖,這被稱為零填充(zero padding)。

圖14-3 卷積層和零填充的連接

也可以通過間隔感受野,將大輸入層和小卷積層連接起來,見圖14-4。這麼做可以極大降低模型的計算複雜度。一個感受野到下一個感受野的便宜距離稱為步長。在圖中,5 × 7 的輸入層(加上零填充),連接著一個3 × 4的層,使用 3 × 3 的感受野,步長是2(這個例子中,寬和高的步長都是2,但也可以不同)。位於上層第i行、第j列的神經元,連接著前一層的第i × shi × sh + fh – 1行、第j × swj × sw + fw – 1列的神經元的輸出,sh和sw分別是垂直和水平步長。

圖14-2 使用大小為2的步長降維

過濾器

神經元的權重可以表示為感受野大小的圖片。例如,圖14-5展示了兩套可能的權重(稱為權重,或卷積核)。第一個是黑色的方形,中央有垂直白線(7 × 7的矩陣,除了中間的豎線都是1,其它地方是0);使用這個矩陣,神經元只能注意到中間的垂直線(因為其它地方都乘以0了)。第二個過濾器也是黑色的方形,但是中間是水平的白線。使用這個權重的神經元只會注意中間的白色水平線。

如果卷積層的所有神經元使用同樣的垂直過濾器(和同樣的偏置項),給神經網路輸入圖14-5中最底下的圖片,卷積層輸出的是左上的圖片。可以看到,圖中垂直的白線得到了加強,其餘部分變模糊了。相似的,右上的圖是所有神經元都是用水平線過濾器的結果,水平的白線加強了,其餘模糊了。因此,一層的全部神經元都用一個過濾器,就能輸出一個特徵映射(feature map),特徵映射可以高亮圖片中最為激活過濾器的區域。當然,不用手動定義過濾器:卷積層在訓練中可以自動學習對任務最有用的過濾器,上面的層則可以將簡單圖案組合為複雜圖案。

圖14-5 應用兩個不同的過濾器,得到兩張不同的特徵映射

堆疊多個特徵映射

簡單起見,前面都是將每個卷積層的輸出用2D層來表示的,但真實的卷積層可能有多個過濾器(過濾器數量由你確定),每個過濾器會輸出一個特徵映射,所以表示成3D更準確(見圖14-6)。每個特徵映射的每個像素有一個神經元,同一特徵映射中的所有神經元有同樣的參數(即,同樣的權重和偏置項)。不同特徵映射的神經元的參數不同。神經元的感受野和之前描述的相同,但擴展到了前面所有的特徵映射。總而言之,一個卷積層同時對輸入數據應用多個可訓練過濾器,使其可以檢測出輸入的任何地方的多個特徵。

筆記:同一特徵映射中的所有神經元共享一套參數,極大地減少了模型的參數量。當CNN認識了一個位置的圖案,就可以在任何其它位置識別出來。相反的,當常規DNN學會一個圖案,只能在特定位置識別出來。

輸入影像也是有多個子層構成的:每個顏色通道,一個子層。通常是三個:紅,綠,藍(RGB)。灰度圖只有一個通道,但有些圖可能有多個通道 —— 例如,衛星圖片可以捕捉到更多的光譜頻率(比如紅外線)。

圖14-6 有多個特徵映射的卷積層,有三個顏色通道的影像

特別的,位於卷積層l的特徵映射k的第i行、第j列的神經元,它連接的是前一層l-1i × shi × sh + fh – 1行、j × swj × sw + fw – 1列的所有特徵映射。不同特徵映射中,位於相同i行、j列的神經元,連接著前一層相同的神經元。

等式14-1用一個大等式總結了前面的知識:如何計算卷積層中給定神經元的輸出。因為索引過多,這個等式不太好看,它所做的其實就是計算所有輸入的加權和,再加上偏置項。

等式14-1 計算卷積層中給定神經元的輸出

在這個等式中:

  • zi, j, k是卷積層l中第i行、第j列、特徵映射k的輸出。
  • sh 和 sw 是垂直和水平步長,fh 和 fw 是感受野的高和寬,fn'是前一層l-1的特徵映射數。
  • xi', j', k'是卷積層l-1中第i'行、第j'列、特徵映射k'的輸出(如果前一層是輸入層,則為通道k')。
  • bk是特徵映射k的偏置項。可以將其想像成一個旋鈕,可以調節特徵映射k的明亮度。
  • wu, v, k′ ,k是層l的特徵映射k的任意神經元,和位於行u、列v(相對於神經元的感受野)、特徵映射k'的輸入,兩者之間的連接權重。

TensorFlow實現

在TensorFlow中,每張輸入圖片通常都是用形狀為[高度,寬度,通道]的3D張量表示的。一個小批次則為4D張量,形狀是[批次大小,高度,寬度,通道]。卷積層的權重是4D張量,形狀是 [fh, fw, fn′, fn] 。卷積層的偏置項是1D張量,形狀是 [fn] 。

看一個簡單的例子。下面的程式碼使用Scikit-Learn的load_sample_image()載入了兩張圖片,一張是中國的寺廟,另一張是花,創建了兩個過濾器,應用到了兩張圖片上,最後展示了一張特徵映射:

from sklearn.datasets import load_sample_image    # 載入樣本圖片  china = load_sample_image("china.jpg") / 255  flower = load_sample_image("flower.jpg") / 255  images = np.array([china, flower])  batch_size, height, width, channels = images.shape    # 創建兩個過濾器  filters = np.zeros(shape=(7, 7, channels, 2), dtype=np.float32)  filters[:, 3, :, 0] = 1  # 垂直線  filters[3, :, :, 1] = 1  # 水平線    outputs = tf.nn.conv2d(images, filters, strides=1, padding="same")    plt.imshow(outputs[0, :, :, 1], cmap="gray") # 畫出第1張圖的第2個特徵映射  plt.show()

逐行看下程式碼:

  • 每個顏色通道的像素強度是用0到255來表示的,所以直接除以255,將其縮放到區間0到1內。
  • 然後創建了兩個7 × 7的過濾器(一個有垂直正中白線,另一個有水平正中白線)。
  • 使用tf.nn.conv2d()函數,將過濾器應用到兩張圖片上。這個例子中使用了零填充(padding="same"),步長是1。
  • 最後,畫出一個特徵映射(相似與圖14-5中的右上圖)。

tf.nn.conv2d()函數這一行,再多說說:

  • images是一個輸入的小批次(4D張量)。
  • filters是過濾器的集合(也是4D張量)。
  • strides等於1,也可以是包含4個元素的1D數組,中間的兩個元素是垂直和水平步長(sh 和 sw),第一個和最後一個元素現在必須是1。以後可以用來指定批次步長(跳過實例)和通道步長(跳過前一層的特徵映射或通道)。
  • padding必須是"same""valid"
  • 如果設為"same",卷積層會使用零填充。輸出的大小是輸入神經元的數量除以步長,再取整。例如:如果輸入大小是13,步長是5(見圖14-7),則輸出大小是3(13 / 5 = 2.6,再向上圓整為3),零填充盡量在輸入上平均添加。當strides=1時,層的輸出會和輸入有相同的空間維度(寬和高),這就是same的來歷。
  • 如果設為"valid",卷積層就不使用零填充,取決於步長,可能會忽略圖片的輸入圖片的底部或右側的行和列,見圖14-7(簡單舉例,只是顯示了水平維度)。這意味著每個神經元的感受野位於嚴格確定的圖片中的位置(不會越界),這就是valid的來歷。

圖14-7 Padding="same」 或 「valid」(輸入寬度13,過濾器寬度6,步長5)

這個例子中,我們手動定義了過濾器,但在真正的CNN中,一般將過濾器定義為可以訓練的變數,好讓神經網路學習哪個過濾器的效果最好。使用keras.layers.Conv2D層:

conv = keras.layers.Conv2D(filters=32, kernel_size=3, strides=1,                             padding="same", activation="relu")

這段程式碼創建了一個有32個過濾器的Conv2D層,每個過濾器的形狀是3 × 3,步長為1(水平垂直都是1),和"same"填充,輸出使用ReLU激活函數。可以看到,卷積層的超參數不多:選擇過濾器的數量,過濾器的高和寬,步長和填充類型。和以前一樣,可以使用交叉驗證來找到合適的超參數值,但很耗時間。後面會討論常見的CNN架構,可以告訴你如何挑選超參數的值。

記憶體需求

CNN的另一個問題是卷積層需要很高的記憶體。特別是在訓練時,因為反向傳播需要所有前向傳播的中間值。

比如,一個有5 × 5個過濾器的卷積層,輸出200個特徵映射,大小為150 × 100,步長為1,零填充。如果如數是150 × 100 的RGB圖片(三通道),則參數總數是(5 × 5 × 3 + 1) × 200 = 15200,加1是考慮偏置項。相對於全連接層,參數少很多了。但是200個特徵映射,每個都包含150 × 100個神經元,每個神經元都需要計算5 × 5 × 3 = 75個輸入的權重和:總共是2.25億個浮點數乘法運算。雖然比全連接層少點,但也很耗費算力。另外,如果特徵映射用的是32位浮點數,則卷積層輸出要佔用200 × 150 × 100 × 32 = 96 百萬比特(12MB)的記憶體。這僅僅是一個實例,如果訓練批次有100個實例,則要使用1.2 GB的記憶體。

在做推斷時(即,對新實例做預測),下一層計算完,前一層佔用的記憶體就可以釋放掉記憶體,所以只需要兩個連續層的記憶體就夠了。但在訓練時,前向傳播期間的所有結果都要保存下來以為反向傳播使用,所以消耗的記憶體是所有層的記憶體佔用總和。

提示:如果因為記憶體不夠發生訓練終端,可以降低批次大小。另外,可以使用步長降低緯度,或去掉幾層。或者,你可以使用16位浮點數,而不是32位浮點數。或者,可以將CNN分布在多台設備上。

接下來,看看CNN的第二個組成部分:池化層。

池化層

明白卷積層的原理了,池化層就容易多了。池化層的目的是對輸入圖片做降取樣(即,收縮),以降低計算負載、記憶體消耗和參數的數量(降低過擬合)。

和卷積層一樣,池化層中的每個神經元也是之和前一層的感受野里的有限個神經元相連。和前面一樣,必須定義感受野的大小、步長和填充類型。但是,池化神經元沒有權重,它所要做的是使用聚合函數,比如最大或平均,對輸入做聚合。圖14-8展示了最為常用的最大池化層。在這個例子中,使用了一個2 × 2的池化核,步長為2,沒有填充。只有感受野中的最大值才能進入下一層,其它的就丟棄了。例如,在圖14-8左下角的感受野中,輸入值是1、5、3、2,所以只有最大值5進入了下一層。因為步長是2,輸出圖的高度和寬度是輸入圖的一半(因為沒有用填充,向下圓整)。

圖14-8 最大池化層(2 × 2的池化核,步長為2,沒有填充)

筆記:池化層通常獨立工作在每個通道上,所以輸出深度和輸入深度相同。

除了可以減少計算、記憶體消耗、參數數量,最大池化層還可以帶來對小偏移的不變性,見圖14-9。假設亮像素比暗像素的值小,用2 × 2核、步長為2的最大池化層處理三張圖(A、B、C)。圖B和C的圖案與A相同,只是分別向右移動了一個和兩個像素。可以看到,A、B經過池化層處理後的結果相同,這就是所謂的平移不變性。對於圖片C,輸出有所不同:向右偏移了一個像素(但仍然有50%沒變)。在CNN中每隔幾層就插入一個最大池化層,可以帶來更大程度的平移不變性。另外,最大池化層還能帶來一定程度的旋轉不變性和縮放不變性。當預測不需要考慮平移、旋轉和縮放時,比如分類任務,不變性可以有一定益處。

圖14-9 小平移不變性

但是,最大池化層也有缺點。首先,池化層破壞了資訊:即使感受野的核是2 × 2,步長是2,輸出在兩個方向上都損失了一半,總共損失了75%的資訊。對於某些任務,不變性不可取。比如語義分割(將像素按照對象分類):如果輸入圖片向右平移了一個像素,輸出也應該向右平移一個降速。此時強調的就是等價:輸入發生小變化,則輸出也要有對應的小變化。

TensorFlow實現

用TensorFlow實現最大池化層很簡單。下面的程式碼實現了最大池化層,核是2 × 2。步長默認等於核的大小,所以步長是2(水平和垂直步長都是2)。默認使用"valid"填充:

max_pool = keras.layers.MaxPool2D(pool_size=2)

要創建平均池化層,則使用AvgPool2D。平均池化層和最大池化層很相似,但計算的是感受野的平均值。平均池化層在過去很流行,但最近人們使用最大池化層更多,因為最大池化層的效果更好。初看很奇怪,因為計算平均值比最大值損失的資訊要少。但是從反面看,最大值保留了最強特徵,去除了無意義的特徵,可以讓下一層獲得更清楚的資訊。另外,最大池化層提供了更強的平移不變性,所需計算也更少。

池化層還可以沿著深度方向做計算。這可以讓CNN學習到不同特徵的不變性。比如。CNN可以學習多個過濾器,每個過濾器檢測一個相同的圖案的不同旋轉(比如手寫字,見圖14-10),深度池化層可以使輸出相同。CNN還能學習其它的不變性:厚度、明亮度、扭曲、顏色,等等。

圖14-10 深度最大池化層可以讓CNN學習到多種不變性

Keras沒有深度方向最大池化層,但TensorFlow的低級API有:使用tf.nn.max_pool(),指定核的大小、步長(4元素的元組):元組的前三個值應該是1,表明沿批次、高度、寬度的步長是1;最後一個值,是深度方向的步長 —— 比如3(深度步長必須可以整除輸入深度;如果前一個層有20個特徵映射,步長3就不成):

output = tf.nn.max_pool(images,                          ksize=(1, 1, 1, 3),                          strides=(1, 1, 1, 3),                          padding="valid")

如果想將這個層添加到Keras模型中,可以將其包裝進Lambda層(或創建一個自定義Keras層):

depth_pool = keras.layers.Lambda(      lambda X: tf.nn.max_pool(X, ksize=(1, 1, 1, 3), strides=(1, 1, 1, 3),                               padding="valid"))

最後一中常見的池化層是全局平均池化層。它的原理非常不同:它計算整個特徵映射的平均值(就像是平均池化層的核的大小和輸入的空間維度一樣)。這意味著,全局平均池化層對於每個實例的每個特徵映射,只輸出一個值。雖然這麼做對資訊的破壞性很大,卻可以用來做輸出層,後面會看到例子。創建全局平均池化層的方法如下:

global_avg_pool = keras.layers.GlobalAvgPool2D()

它等同於下面的Lambda層:

global_avg_pool = keras.layers.Lambda(lambda X: tf.reduce_mean(X, axis=[1, 2]))

介紹完CNN的組件之後,來看看如何將它們組合起來。

CNN架構

CNN的典型架構是將幾個卷積層疊起來(每個卷積層後面跟著一個ReLU層),然後再疊一個池化層,然後再疊幾個卷積層(+ReLU),接著再一個池化層,以此類推。圖片在流經神經網路的過程中,變得越來越小,但得益於卷積層,卻變得越來越深(特徵映射變多了),見圖14-11。在CNN的頂部,還有一個常規的前饋神經網路,由幾個全連接層(+ReLU)組成,最終層輸出預測(比如,一個輸出類型概率的softmax層)。

圖14-11 典型的CNN架構

提示:常犯的錯誤之一,是使用過大的卷積核。例如,要使用一個卷積層的核是5 × 5,再加上兩個核為3 × 3的層:這樣參數不多,計算也不多,通常效果也更好。第一個卷積層是例外:可以有更大的卷積核(例如5 × 5),步長為2或更大:這樣可以降低圖片的空間維度,也沒有損失很多資訊。

下面的例子用一個簡單的CNN來處理Fashion MNIST數據集(第10章介紹過):

model = keras.models.Sequential([      keras.layers.Conv2D(64, 7, activation="relu", padding="same",                          input_shape=[28, 28, 1]),      keras.layers.MaxPooling2D(2),      keras.layers.Conv2D(128, 3, activation="relu", padding="same"),      keras.layers.Conv2D(128, 3, activation="relu", padding="same"),      keras.layers.MaxPooling2D(2),      keras.layers.Conv2D(256, 3, activation="relu", padding="same"),      keras.layers.Conv2D(256, 3, activation="relu", padding="same"),      keras.layers.MaxPooling2D(2),      keras.layers.Flatten(),      keras.layers.Dense(128, activation="relu"),      keras.layers.Dropout(0.5),      keras.layers.Dense(64, activation="relu"),      keras.layers.Dropout(0.5),      keras.layers.Dense(10, activation="softmax")  ])

逐行看下程式碼:

  • 第一層使用了64個相當大的過濾器(7 × 7),但沒有用步長,因為輸入圖片不大。還設置了input_shape=[28, 28, 1],因為圖片是28 × 28像素的,且是單通道(即,灰度)。
  • 接著,使用了一個最大池化層,核大小為2.
  • 接著,重複做兩次同樣的結構:兩個卷積層,跟著一個最大池化層。對於大圖片,這個結構可以重複更多次(重複次數是超參數)。
  • 要注意,隨著CNN向著輸出層的靠近,過濾器的數量一直在提高(一開始是64,然後是128,然後是256):這是因為低級特徵的數量通常不多(比如,小圓圈或水平線),但將其組合成為高級特徵的方式很多。通常的做法是在每個池化層之後,將過濾器的數量翻倍:因為池化層對空間維度除以了2,因此可以將特徵映射的數量翻倍,且不用擔心參數數量、記憶體消耗、算力的增長。
  • 然後是全連接網路,由兩個隱藏緊密層和一個緊密輸出層組成。要注意,必須要打平輸入,因為緊密層的每個實例必須是1D數組。還加入了兩個dropout層,丟失率為50%,以降低過擬合。

這個CNN可以在測試集上達到92%的準確率。雖然不是頂尖水平,但也相當好了,效果比第10章用的方法好得多。

過去幾年,這個基礎架構的變體發展迅猛,取得了驚人的進步。衡量進步的一個指標是ILSVRC ImageNet challenge的誤差率。在六年期間,這項賽事的前五誤差率從26%降低到了2.3%。前五誤差率的意思是,預測結果的前5個最高概率的圖片不包含正確結果的比例。測試圖片相當大(256個像素),有1000個類,一些圖的差別很細微(比如區分120種狗的品種)。學習ImageNet冠軍程式碼是學習CNN的好方法。

我們先看看經典的LeNet-5架構(1998),然後看看三個ILSVRC競賽的冠軍:AlexNet(2012)、GoogLeNet(2014)、ResNet(2015)。

LeNet-5

LeNet-5 也許是最廣為人知的CNN架構。前面提到過,它是由Yann LeCun在1998年創造出來的,被廣泛用於手寫字識別(MNIST)。它的結構如下:

表14-1 LeNet-5架構

有一些點需要注意:

  • MNIST圖片是28 × 28像素的,但在輸入給神經網路之前,做了零填充,成為32 × 32像素,並做了歸一化。後面的層不用使用任何填充,這就是為什麼當圖片在網路中傳播時,圖片大小持續縮小。
  • 平均池化層比一般的稍微複雜點:每個神經元計算輸入的平均值,然後將記過乘以一個可學習的係數(每個映射一個係數),在加上一個可學習的偏置項(也是每個映射一個),最後使用激活函數。
  • C3層映射中的大部分神經元,只與S2層映射三個或四個神經元全連接(而不是6個)。
  • 輸出層有點特殊:不是計算輸入和權重矢量的矩陣積,而是每個神經元輸出輸入矢量和權重矢量的歐氏距離的平方。每個輸出衡量圖片屬於每個數字類的概率程度。這裡適用交叉熵損失函數,因為對錯誤預測懲罰更多,可以產生更大的梯度,收斂更快。

Yann LeCun 的 網站展示了LeNet-5做數字分類的例子。

AlexNet

AlexNet CNN 架構以極大優勢,贏得了2012 ImageNet ILSVRC冠軍:它的Top-5誤差率達到了17%,第二名只有26%!它是由Alex Krizhevsky、Ilya Sutskever 和 Geoffrey Hinton發明的。AlexNet和LeNet-5很相似,只是更大更深,是首個將卷積層堆疊起來的網路,而不是在每個卷積層上再加一個池化層。表14-2展示了其架構:

表14-2 AlexNet架構

為了降低過擬合,作者使用了兩種正則方法。首先,F8和F9層使用了dropout,丟棄率為50%。其次,他們通過隨機距離偏移訓練圖片、水平翻轉、改變亮度,做了數據增強。

數據增強 數據增強是通過生成許多訓練實例的真實變種,來人為增大訓練集。因為可以降低過擬合,成為了一種正則化方法。生成出來的實例越真實越好:最理想的情況,人們無法區分增強圖片是原生的還是增強過的。簡單的添加白雜訊沒有用,增強修改要是可以學習的(白雜訊不可學習)。 例如,可以輕微偏移、旋轉、縮放原生圖,再添加到訓練集中(見圖14-12)。這麼做可以使模型對位置、方向和物體在圖中的大小,有更高的容忍度。如果想讓模型對不同光度有容忍度,可以生成對比度不同的照片。通常,還可以水平翻轉圖片(文字不成、不對稱物體也不成)。通過這些變換,可以極大的增大訓練集。

圖14-12 從原生圖生成新的訓練實例

AlexNet還在C1和C3層的ReLU之後,使用了強大的歸一化方法,稱為局部響應歸一化(LRN):激活最強的神經元抑制了相同位置的相鄰特徵映射的神經元(這樣的競爭性激活也在生物神經元上觀察到了)。這麼做可以讓不同的特徵映射專業化,特徵範圍更廣,提升泛化能力。等式14-2展示了如何使用LRN。

等式14-2 局部響應歸一化(LRN)

這這個等式中:

  • bI是特徵映射i的行uv的神經元的歸一化輸出(注意等始中沒有出現行uv)。
  • aI是ReLu之後,歸一化之前的激活函數。
  • k、α、β和r是超參。k是偏置項,r是深度半徑。
  • fn是特徵映射的數量。

例如,如果r=2,且神經元有強激活,能抑制其他相鄰上下特徵映射的神經元的激活。

在AlexNet中,超參數是這麼設置的:r = 2,α = 0.00002,β = 0.75,k = 1。可以通過tf.nn.local_response_normalization()函數實現,要想用在Keras模型中,可以包裝進Lambda層。

AlexNet的一個變體是ZF Net,是由Matthew Zeiler和Rob Fergus發明的,贏得了2013年的ILSVRC。它本質上是對AlexNet做了一些超參數的調節(特徵映射數、核大小,步長,等等)。

GoogLeNet

GoogLeNet 架構是Google Research的Christian Szegedy及其同事發明的,贏得了ILSVRC 2014冠軍,top-5誤差率降低到了7%以內。能取得這麼大的進步,很大的原因是它的網路比之前的CNN更深(見圖14-14)。這歸功於被稱為創始模組(inception module)的子網路,它可以讓GoogLeNet可以用更高的效率使用參數:實際上,GoogLeNet的參數量比AlexNet小10倍(大約是600萬,而不是AlexNet的6000萬)。

圖14-13展示了一個創始模組的架構。「3 × 3 + 1(S)」的意思是層使用的核是3 × 3,步長是1,"same"填充。先複製輸入訊號,然後輸入給4個不同的層。所有卷積層使用ReLU激活函數。注意,第二套卷積層使用了不同的核大小(1 × 1、3 × 3、5 × 5),可以讓其捕捉不同程度的圖案。還有,每個單一層的步長都是1,都是零填充(最大池化層也同樣),因此它們的輸出和輸入有同樣的高度和寬度。這可以讓所有輸出在最終深度連接層,可以沿著深度方向連起來(即,將四套卷積層的所有特徵映射堆疊起來)。這個連接層可以使用用tf.concat()實現,其axis=3(深度方向的軸)。

圖14-13 創始模組

為什麼創始模組有核為1 × 1的卷積層呢?這些層捕捉不到任何圖案,因為只能觀察一個像素?事實上,這些層有三個目的:

  • 儘管不能捕捉空間圖案,但可以捕捉沿深度方向的圖案。
  • 這些曾輸出的特徵映射比輸入少,是作為瓶頸層來使用的,意味它們可以降低維度。這樣可以減少計算和參數量、加快訓練,提高泛化能力。
  • 每一對卷積層([1 × 1, 3 × 3] 和 [1 × 1, 5 × 5])就像一個強大的單一卷積層,可以捕捉到更複雜的圖案。事實上,這對卷積層可以掃過兩層神經網路。

總而言之,可以將整個創始模組當做一個卷積層,可以輸出捕捉到不同程度、更多複雜圖案的特徵映射。

警告:每個卷積層的卷積核的數量是一個超參數。但是,這意味著每添加一個創始層,就多了6個超參數。

來看下GoogLeNet的架構(見圖14-14)。每個卷積層、每個池化層輸出的特徵映射的數量,展示在核大小的前面。因為比較深,只好擺成三列。GoogLeNet實際是一列,一共包括九個創始模組(帶有陀螺標誌)。創始模組中的六個數表示模組中的每個卷積層輸出的特徵映射數(和圖14-13的順序相同)。注意所有卷積層使用ReLU激活函數。

圖14-14 GoogLeNet的架構

這個網路的結構如下:

  • 前兩個層將圖片的高和寬除以了4(所以面積除以了16),以減少計算。第一層使用的核很大,可以保留大部分資訊。
  • 接下來,局部響應歸一化層可以保證前面的層可以學到許多特徵。
  • 後面跟著兩個卷積層,前面一層作為瓶頸層。可以將這兩層作為一個卷積層。
  • 然後,又是一個局部響應歸一化層。
  • 接著,最大池化層將圖片的高度和寬度除以2,以加快計算。
  • 然後,是九個創始模組,中間插入了兩個最大池化層,用來降維提速。
  • 接著,全局平均池化層輸出每個特徵映射的平均值:可以丟棄任何留下的空間資訊,可以這麼做是因為此時留下的空間資訊也不多了。事實上GoogLeNet的輸入圖片一般是224 × 224像素的,經過5個最大池化層後,每個池化層將高和寬除以2,特徵映射降為7 × 7。另外,這是一個分類任務,不是定位任務,所以對象在哪無所謂。得益於該層降低了維度,就不用的網路的頂部(像AlexNet那樣)加幾個全連接層了,這麼做可以極大減少參數數量,降低過擬合。
  • 最後幾層很明白:dropout層用來正則,全連接層(因為有1000個類,所以有1000個單元)和softmax激活函數用來產生估計類的概率。

架構圖經過輕微的簡化:原始GoogLeNet架構還包括兩個輔助的分類器,位於第三和第六創始模組的上方。它們都是由一個平均池化層、一個卷積層、兩個全連接層和一個softmax激活層組成。在訓練中,它們的損失(縮減70%)被添加到總損失中。它們的目標是對抗梯度消失,對網路做正則。但是,後來的研究顯示它們的作用很小。

Google的研究者後來又提出了幾個GoogLeNet的變體,包括Inception-v3和Inception-v4,使用的創始模組略微不同,性能更好。

VGGNet

ILSVRC 2014年的亞軍是VGGNet,作者是來自牛津大學Visual Geometry Group(VGC)的Karen Simonyan 和 Andrew Zisserman。VGGNet的架構簡單而經典,2或3個卷積層和1個池化層,然後又是2或3個卷積層和1個池化層,以此類推(總共達到16或19個卷積層)。最終加上一個有兩個隱藏層和輸出層的緊密網路。VGGNet只用3 × 3的過濾器,但數量很多。

ResNet

何凱明使用Residual Network (或 ResNet)贏得了ILSVRC 2015的冠軍,top-5誤差率降低到了3.6%以下。ResNet的使用了極深的卷積網路,共152層(其它的變體有1450或152層)。反映了一個總體趨勢:模型變得越來越深,參數越來越少。訓練這樣的深度網路的方法是使用跳連接(也被稱為快捷連接):輸入訊號添加到更高層的輸出上。

當訓練神經網路時,目標是使網路可以對目標函數h(x)建模。如果將輸入x添加給網路的輸出(即,添加一個跳連接),則網路就要對f(x) = h(x) – x建模,而不是h(x)。這被稱為殘差學習(見圖14-15)。

圖14-15 殘差學習

初始化一個常規神經網路時,它的權重接近於零,所以輸出值也接近於零。如果添加跳連接,網路就會輸出一個輸入的複製;換句話說,網路一開始是對恆等函數建模。如果目標函數與恆等函數很接近(通常會如此),就能極大的加快訓練。

另外,如果添加多個跳連接,就算有的層還沒學習,網路也能正常運作(見圖14-16)。多虧了跳連接,訊號可以在整個網路中流動。深度殘差網路,可以被當做殘差單元(RU)的堆疊,其中每個殘差單元是一個有跳連接的小神經網路。

圖14-16 常規神經網路(左)和深度殘差網路(右)

來看看ResNet的架構(見圖14-17)。特別簡單。開頭和結尾都很像GoogLeNet(只是沒有的dropout層),中間是非常深的殘差單元的堆砌。每個殘差單元由兩個卷積層(沒有池化層!)組成,有批歸一化和ReLU激活,使用3 × 3的核,保留空間維度(步長等於1,零填充)。

圖14-17 ResNet架構

注意到,每經過幾個殘差單元,特徵映射的數量就會翻倍,同時高度和寬度都減半()卷積層的步長為2。發生這種情況時,因為形狀不同(見圖14-17中虛線的跳連接),輸入不能直接添加到殘差單元的輸出上。要解決這個問題,輸入要經過一個1 × 1的卷積層,步長為2,特徵映射數不變(見圖14-18)。

圖14-18 改變特徵映射大小和深度時的跳連接

ResNet-34是有34個層(只是計數了卷積層和全連接層)的ResNet,有3個輸出64個特徵映射的殘差單元,4個輸出128個特徵映射的殘差單元,6個輸出256個特徵映射的殘差單元,3個輸出512個特徵映射的殘差單元。本章後面會實現這個網路。

ResNet通常比這個架構要深,比如ResNet-152,使用了不同的殘差單元。不是用3 × 3的輸出256個特徵映射的卷積層,而是用三個卷積層:第一是1 × 1的卷積層,只有64個特徵映射(少4倍),作為瓶頸層使用;然後是1 × 1的卷積層,有64個特徵映射;最後是另一個1 × 1的卷積層,有256個特徵映射,恢復原始深度。ResNet-152含有3個這樣輸出256個映射的殘差單元,8個輸出512個映射的殘差單元,36個輸出1024個映射的殘差單元,最後是3個輸出2048個映射的殘差單元。

筆記:Google的Inception-v4融合了GoogLeNet和ResNet,使ImageNet的top-5誤差率降低到接近3%。

Xception

另一個GoogLeNet架構的變體是Xception(Xception的意思是極限創始,Extreme Inception)。它是由François Chollet(Keras的作者)在2016年提出的,Xception在大型視覺任務(3.5億張圖、1.7萬個類)上超越了Inception-v3。和Inception-v4很像,Xception融合了GoogLeNet和ResNet,但將創始模組替換成了一個特殊類型的層,稱為深度可分卷積層(或簡稱為可分卷積層)。深度可分卷積層在以前的CNN中出現過,但不像Xception這樣處於核心。常規卷積層使用過濾器同時獲取空間圖案(比如,橢圓)和交叉通道圖案(比如,嘴+鼻子+眼睛=臉),可分卷積層的假設是空間圖案和交叉通道圖案可以分別建模(見圖14-19)。因此,可分卷積層包括兩部分:第一個部分對於每個輸入特徵映射使用單空間過濾器,第二個部分只針對交叉通道圖案 —— 就是一個過濾器為1 × 1的常規卷積層。

圖14-19 深度可分卷積層

因為可分卷積層對每個輸入通道只有一個空間過濾器,要避免在通道不多的層之後使用可分卷積層,比如輸入層(這就是圖14-19要展示的)。出於這個原因,Xception架構一開始有2個常規卷積層,但剩下的架構都使用可分卷積層(共34個),加上一些最大池化層和常規的末端層(全局平均池化層和緊密輸出層)。

為什麼Xception是GoogLeNet的變體呢,因為它並沒有創始模組?正像前面討論的,創始模組含有過濾器為1 × 1的卷積層:只針對交叉通道圖案。但是,它們上面的常規卷積層既針對空間、也針對交叉通道圖案。所以可以將創始模組作為常規卷積層和可分卷積層的中間狀態。在實際中,可分卷積層表現更好。

提示:相比於常規卷積層,可分卷積層使用的參數、記憶體、算力更少,性能也更好,所以應默認使用後者(除了通道不多的層)。

ILSVRC 2016的冠軍是香港中文大學的CUImage團隊。他們結合使用了多種不同的技術,包括複雜的對象識別系統,稱為GBD-Net,top-5誤差率達到3%以下。儘管結果很經驗,但方案相對於ResNet過於複雜。另外,一年後,另一個簡單得多的架構取得了更好的結果。

SENet

ILSVRC 2017年的冠軍是擠壓-激活網路(Squeeze-and-Excitation Network (SENet))。這個架構拓展了之前的創始模組和ResNet,提高了性能。SENet的top-5誤差率達到了驚人的2.25%。經過拓展之後的版本分別稱為SE-創始模組和SE-ResNet。性能提升來自於SENet在原始架構的每個單元(比如創始模組或殘差單元)上添加了一個小的神經網路,稱為SE塊,見圖14-20。

圖14-20 SE-創始模組(左)和SE-ResNet(右)

SE分析了單元輸出,只針對深度方向,它能學習到哪些特徵總是一起活躍的。然後根據這個資訊,重新調整特徵映射,見圖14-21。例如,SE可以學習到嘴、鼻子、眼睛經常同時出現在圖片中:如果你看見了罪和鼻子,通常是期待看見眼睛。所以,如果SE塊發向嘴和鼻子的特徵映射有強激活,但眼睛的特徵映射沒有強激活,就會提升眼睛的特徵映射(更準確的,會降低無關的特徵映射)。如果眼睛和其它東西搞混了,特徵映射重調可以解決模糊性。

圖14-21 SE快做特徵重調

SE塊由三層組成:一個全局平均池化層、一個使用ReLU的隱含緊密層、一個使用sigmoid的緊密輸出層(見圖14-22)。

圖14-22 SE塊的結構

和之前一樣,全局平均池化層計算每個特徵映射的平均激活:例如,如果它的輸入包括256個特徵映射,就會輸出256個數,表示對每個過濾器的整體響應水平。下一個層是「擠壓」步驟:這個層的神經元數遠小於256,通常是小於特徵映射數的16倍(比如16個神經元)—— 因此256個數被壓縮金小矢量中(16維)。這是特徵響應的地位矢量表徵(即,嵌入)。這一步作為瓶頸,能讓SE塊強行學習特徵組合的通用表徵(第17章會再次接觸這個原理)。最後,輸出層使用這個嵌入,輸出一個重調矢量,每個特徵映射(比如,256)包含一個數,都位於0和1之間。然後,特徵映射乘以這個重調矢量,所以無關特徵(其重調分數小)就被弱化了,就剩下相關特徵(重調分數接近於1)了。

用Karas實現ResNet-34 CNN

目前為止介紹的大多數CNN架構的實現並不難(但經常需要載入預訓練網路)。接下來用Keras實現ResNet-34。首先,創建ResidualUnit層:

class ResidualUnit(keras.layers.Layer):      def __init__(self, filters, strides=1, activation="relu", **kwargs):          super().__init__(**kwargs)          self.activation = keras.activations.get(activation)          self.main_layers = [              keras.layers.Conv2D(filters, 3, strides=strides,                                  padding="same", use_bias=False),              keras.layers.BatchNormalization(),              self.activation,              keras.layers.Conv2D(filters, 3, strides=1,                                  padding="same", use_bias=False),              keras.layers.BatchNormalization()]          self.skip_layers = []          if strides > 1:              self.skip_layers = [                  keras.layers.Conv2D(filters, 1, strides=strides,                                      padding="same", use_bias=False),                  keras.layers.BatchNormalization()]        def call(self, inputs):          Z = inputs          for layer in self.main_layers:              Z = layer(Z)          skip_Z = inputs          for layer in self.skip_layers:              skip_Z = layer(skip_Z)          return self.activation(Z + skip_Z)

可以看到,這段程式碼和圖14-18很接近。在構造器中,創建了所有需要的層:主要的層位於圖中右側,跳躍層位於左側(只有當步長大於1時需要)。在call()方法中,我們讓輸入經過主層和跳躍層,然後將輸出相加,再應用激活函數。

然後,使用Sequential模型搭建ResNet-34,ResNet-34就是一連串層的組合(將每個殘差單元作為一個單一層):

model = keras.models.Sequential()  model.add(keras.layers.Conv2D(64, 7, strides=2, input_shape=[224, 224, 3],                                padding="same", use_bias=False))  model.add(keras.layers.BatchNormalization())  model.add(keras.layers.Activation("relu"))  model.add(keras.layers.MaxPool2D(pool_size=3, strides=2, padding="same"))  prev_filters = 64  for filters in [64] * 3 + [128] * 4 + [256] * 6 + [512] * 3:      strides = 1 if filters == prev_filters else 2      model.add(ResidualUnit(filters, strides=strides))      prev_filters = filters  model.add(keras.layers.GlobalAvgPool2D())  model.add(keras.layers.Flatten())  model.add(keras.layers.Dense(10, activation="softmax"))

這段程式碼中唯一麻煩的地方,就是添加ResidualUnit層的循環部分:前3個RU有64個過濾器,接下來的4個RU有128個過濾器,以此類推。如果過濾器數和前一RU層相同,則步長為1,否則為2。然後添加ResidualUnit,然後更新prev_filters

不到40行程式碼就能搭建出ILSVRC 2015年冠軍模型,既體現出ResNet的優美,也展現了Keras API的表達力。實現其他CNN架構也不困難。但是Keras內置了其中一些架構,一起嘗試下。

使用Keras的預訓練模型

通常來講,不用手動實現GoogLeNet或ResNet這樣的標準模型,因為keras.applications中已經包含這些預訓練模型了,只需一行程式碼就成。例如,要載入在ImageNet上預訓練的ResNet-50模型,使用下面的程式碼就行:

model = keras.applications.resnet50.ResNet50(weights="imagenet")

僅此而已!這樣就能穿件一個ResNet-50模型,並下載在ImageNet上預訓練的權重。要使用它,首先要保證圖片有正確的大小。ResNet-50模型要用224 × 224像素的圖片(其它模型可能是299 × 299),所以使用TensorFlow的tf.image.resize()函數來縮放圖片:

images_resized = tf.image.resize(images, [224, 224])

提示:tf.image.resize()不會保留寬高比。如果需要,可以裁剪圖片為合適的寬高比之後,再進行縮放。兩步可以通過tf.image.crop_and_resize()來實現。

預訓練模型的圖片要經過特別的預處理。在某些情況下,要求輸入是0到1,有時是-1到1,等等。每個模型提供了一個preprocess_input()函數,來對圖片做預處理。這些函數假定像素值的範圍是0到255,因此需要乘以255(因為之前將圖片縮減到0和1之間):

inputs = keras.applications.resnet50.preprocess_input(images_resized * 255)

現在就可以用預訓練模型做預測了:

Y_proba = model.predict(inputs)

和通常一樣,輸出Y_proba是一個矩陣,每行是一張圖片,每列是一個類(這個例子中有1000類)。如果想展示top K 預測,要使用decode_predictions()函數,將每個預測出的類的名字和概率包括進來。對於每張圖片,返回top K預測的數組,每個預測表示為包含類標識符、名字和置信度的數組:

top_K = keras.applications.resnet50.decode_predictions(Y_proba, top=3)  for image_index in range(len(images)):      print("Image #{}".format(image_index))      for class_id, name, y_proba in top_K[image_index]:          print("  {} - {:12s} {:.2f}%".format(class_id, name, y_proba * 100))      print()

輸出如下:

Image #0    n03877845 - palace       42.87%    n02825657 - bell_cote    40.57%    n03781244 - monastery    14.56%    Image #1    n04522168 - vase         46.83%    n07930864 - cup          7.78%    n11939491 - daisy        4.87%

正確的類(monastery 和 daisy)出現在top3的結果中。考慮到,這是從1000個類中挑出來的,結果相當不錯。

可以看到,使用預訓練模型,可以非常容易的創建出一個效果相當不錯的圖片分類器。keras.applications中其它視覺模型還有幾種ResNet的變體,GoogLeNet的變體(比如Inception-v3 和 Xception),VGGNet的變體,MobileNet和MobileNetV2(移動設備使用的輕量模型)。

如果要使用的圖片分類器不是給ImageNet圖片做分類的呢?這時,還是可以使用預訓練模型來做遷移學習。

使用預訓練模型做遷移學習

如果想創建一個圖片分類器,但沒有足夠的訓練數據,使用預訓練模型的低層通常是不錯的主意,就像第11章討論過的那樣。例如,使用預訓練的Xception模型訓練一個分類花的圖片的模型。首先,使用TensorFlow Datasets載入數據集(見13章):

import tensorflow_datasets as tfds    dataset, info = tfds.load("tf_flowers", as_supervised=True, with_info=True)  dataset_size = info.splits["train"].num_examples # 3670  class_names = info.features["label"].names # ["dandelion", "daisy", ...]  n_classes = info.features["label"].num_classes # 5

可以通過設定with_info=True來獲取數據集資訊。這裡,獲取到了數據集的大小和類名。但是,這裡只有"train"訓練集,沒有測試集和驗證集,所以需要分割訓練集。TF Datasets提供了一個API來做這項工作。比如,使用數據集的前10%作為測試集,接著的15%來做驗證集,剩下的75%來做訓練集:

test_split, valid_split, train_split = tfds.Split.TRAIN.subsplit([10, 15, 75])    test_set = tfds.load("tf_flowers", split=test_split, as_supervised=True)  valid_set = tfds.load("tf_flowers", split=valid_split, as_supervised=True)  train_set = tfds.load("tf_flowers", split=train_split, as_supervised=True)

然後,必須要預處理圖片。CNN的要求是224 × 224的圖片,所以需要縮放。還要使用Xception的preprocess_input()函數來預處理圖片:

def preprocess(image, label):      resized_image = tf.image.resize(image, [224, 224])      final_image = keras.applications.xception.preprocess_input(resized_image)      return final_image, label

對三個數據集使用這個預處理函數,打散訓練集,給所有的數據集添加批次和預提取:

batch_size = 32  train_set = train_set.shuffle(1000)  train_set = train_set.map(preprocess).batch(batch_size).prefetch(1)  valid_set = valid_set.map(preprocess).batch(batch_size).prefetch(1)  test_set = test_set.map(preprocess).batch(batch_size).prefetch(1)

如果想做數據增強,可以修改訓練集的預處理函數,給訓練圖片添加一些轉換。例如,使用tf.image.random_crop()隨機裁剪圖片,使用tf.image.random_flip_left_right()做隨機水平翻轉,等等(參考notebook的「使用預訓練模型做遷移學習」部分)。

提示:keras.preprocessing.image.ImageDataGenerator可以方便地從硬碟載入圖片,並用多種方式來增強:偏移、旋轉、縮放、翻轉、裁剪,或使用任何你想做的轉換。對於簡單項目,這麼做很方便。但是,使用tf.data管道的好處更多:從任何數據源高效讀取圖片(例如,並行);操作數據集;如果基於tf.image運算編寫預處理函數,既可以用在tf.data管道中,也可以用在生產部署的模型中(見第19章)。

然後載入一個在ImageNet上預訓練的Xception模型。通過設定include_top=False,排除模型的頂層:排除了全局平均池化層和緊密輸出層。我們然後根據基本模型的輸出,添加自己的全局平均池化層,然後添加緊密輸出層(沒有一個類就有一個單元,使用softmax激活函數)。最後,創建Keras模型:

base_model = keras.applications.xception.Xception(weights="imagenet",                                                    include_top=False)  avg = keras.layers.GlobalAveragePooling2D()(base_model.output)  output = keras.layers.Dense(n_classes, activation="softmax")(avg)  model = keras.Model(inputs=base_model.input, outputs=output)

第11章介紹過,最好凍結預訓練層的權重,至少在訓練初期如此:

for layer in base_model.layers:      layer.trainable = False

筆記:因為我們的模型直接使用了基本模型的層,而不是base_model對象,設置base_model.trainable=False沒有任何效果。

最後,編譯模型,開始訓練:

optimizer = keras.optimizers.SGD(lr=0.2, momentum=0.9, decay=0.01)  model.compile(loss="sparse_categorical_crossentropy", optimizer=optimizer,                metrics=["accuracy"])  history = model.fit(train_set, epochs=5, validation_data=valid_set)

警告:訓練過程非常慢,除非使用GPU。如果沒有GPU,應該在Colab中運行本章的notebook,使用GPU運行時(是免費的!)。見指導,https://github.com/ageron/handson-ml2

模型訓練幾個周期之後,它的驗證準確率應該可以達到75-80%,然後就沒什麼提升了。這意味著上層訓練的差不多了,此時可以解凍所有層(或只是解凍上邊的層),然後繼續訓練(別忘在冷凍和解凍層是編譯模型)。此時使用小得多的學習率,以避免破壞預訓練的權重:

for layer in base_model.layers:      layer.trainable = True    optimizer = keras.optimizers.SGD(lr=0.01, momentum=0.9, decay=0.001)  model.compile(...)  history = model.fit(...)

訓練要花不少時間,最終在測試集上的準確率可以達到95%。有個模型,就可以訓練出驚艷的圖片分類器了!電腦視覺除了分類,還有其它任務,比如,想知道花在圖片中的位置,該怎麼做呢?

分類和定位

第10章討論過,定點陣圖片中的物體可以表達為一個回歸任務:預測物體的範圍框,一個常見的方法是預測物體中心的水平和垂直坐標,和其高度和寬度。不需要大改模型,只要再添加一個有四個單元的緊密輸出層(通常是在全局平均池化層的上面),可以用MSE損失訓練:

base_model = keras.applications.xception.Xception(weights="imagenet",                                                    include_top=False)  avg = keras.layers.GlobalAveragePooling2D()(base_model.output)  class_output = keras.layers.Dense(n_classes, activation="softmax")(avg)  loc_output = keras.layers.Dense(4)(avg)  model = keras.Model(inputs=base_model.input,                      outputs=[class_output, loc_output])  model.compile(loss=["sparse_categorical_crossentropy", "mse"],                loss_weights=[0.8, 0.2], # depends on what you care most about                optimizer=optimizer, metrics=["accuracy"])

但現在有一個問題:花數據集中沒有圍繞花的邊框。因此,我們需要自己加上。這通常是機器學習任務中最難的部分:獲取標籤。一個好主意是花點時間來找合適的工具。給圖片加邊框,可供使用的開源圖片打標籤工具包括VGG Image Annotator,、LabelImg,、OpenLabeler 或 ImgLab,或是商業工具,比如LabelBox或Supervisely。還可以考慮眾包平台,比如如果有很多圖片要標註的話,可以使用Amazon Mechanical Turk。但是,建立眾包平台、準備數據格式、監督、保證品質,要做不少工作。如果只有幾千張圖片要打標籤,又不是頻繁來做,最好選擇自己來做。Adriana Kovashka等人寫了一篇實用的電腦視覺方面的關於眾包的論文,建議讀一讀。

假設你已經給每張圖片的花都獲得了邊框。你需要創建一個數據集,它的項是預處理好的圖片的批次,加上類標籤和邊框。每項應該是一個元組,格式是(images, (class_labels, bounding_boxes))。然後就可以準備訓練模型了!

提示:邊框應該做歸一化,讓中心的橫坐標、縱坐標、寬度和高度的範圍變成0到1之間。另外,最好是預測高和寬的平方根,而不是直接預測高和寬:大邊框的10像素的誤差,相比於小邊框的10像素的誤差,不會懲罰那麼大。

MSE作為損失函數來訓練模型效果很好,但不是評估模型預測邊框的好指標。最常見的指標是交並比(Intersection over Union (IoU)):預測邊框與目標邊框的重疊部分,除以兩者的並集(見圖14-23)。在tf,keras中,交並比是用tf.keras.metrics.MeanIoU類來實現的。

圖14-23 交並比指標

完成了分類並定位單一物體,但如果圖片中有多個物體該怎麼辦呢(常見於花數據集)?

目標檢測

分類並定點陣圖片中的多個物體的任務被稱為目標檢測。幾年之前,使用的方法還是用定位單一目標的CNN,然後將其在圖片上滑動,見圖14-24。在這個例子中,圖片被分成了6 × 8的網格,CNN(粗黑實線矩形)的範圍是3 × 3。 當CNN查看圖片的左上部分時,檢測到了最左邊的玫瑰花,向右滑動一格,檢測到的還是同樣的花。又滑動一格,檢測到了最上的花,再向右一格,檢測到的還是最上面的花。你可以繼續滑動CNN,查看所有3 × 3的區域。另外,因為目標的大小不同,還需要用不同大小的CNN來觀察。例如,檢測完了所有3 × 3的區域,可以繼續用4 × 4的區域來檢測。

圖14-24 通過滑動CNN來檢測多個目標

這個方法非常簡單易懂,但是也看到了,它會在不同位置、多次檢測到同樣的目標。需要後處理,去除沒用的邊框,常見的方法是非極大值抑制(non-max suppression)。步驟如下:

  1. 首先,給CNN添加另一個對象性輸出,來估計花確實出現在圖片中的概率(或者,可以添加一個「沒有花」的類,但通常不好使)。必須要使用sigmoid激活函數,可以用二元交叉熵損失函數來訓練。然後刪掉對象性分數低於某閾值的所有邊框:這樣能刪掉所有不包含花的邊框。
  2. 找到對象性分數最高的邊框,然後刪掉所有其它與之大面積重疊的邊框(例如,IoU大於60%)。例如,在圖14-24中,最大對象性分數的邊框出現在最上面花的粗賓匡(對象性分數用邊框的粗細來表示)。另一個邊框和這個邊框重合很多,所以將其刪除。
  3. 重複這兩個步驟,直到沒有可以刪除的邊框。

用這個簡單的方法來做目標檢測的效果相當不錯,但需要運行CNN好幾次,所以很慢。幸好,有一個更快的方法來滑動CNN:使用全卷積網路(fully convolutional network,FCN)。

全卷積層

FCN是Jonathan Long在2015年的一篇論文匯總提出的,用於語義分割(根據所屬目標,對圖片中的每個像素點進行分類)。作者指出,可以用卷積層替換CNN頂部的緊密層。要搞明白,看一個例子:假設一個200個神經元的緊密層,位於卷積層的上邊,卷積層輸出100個特徵映射,每個大小是7 × 7(這是特徵映射的大小,不是核大小)。每個神經元會計算卷積層的100 × 7 × 7個激活結果的加權和(加上偏置項)。現在將緊密層替換為卷積層,有200個過濾器,每個大小為7 × 7,"valid"填充。這個層能輸出200個特徵映射,每個是1 × 1(因為核大小等於輸入特徵映射的大小,並且使用的是"valid"填充)。換句話說,會產生200個數,和緊密層一樣;如果仔細觀察卷積層的計算,會發現這些書和緊密層輸出的數一模一樣。唯一不同的地方,緊密層的輸出的張量形狀是 [批次大小, 200],而卷積層的輸出的張量形狀是 [批次大小, 1, 1, 200]。

提示:要將緊密層變成卷積層,卷積層中的過濾器的數量,必須等於緊密層的神經元數,過濾器大小必須等於輸入特徵映射的大小,必須使用"valid"填充。步長可以是1或以上。

為什麼這點這麼重要?緊密層需要的是一個具體的輸入大小(因為它的每個輸入特徵都有一個權重),卷積層卻可以處理任意大小的圖片(但是,它也希望輸入有一個確定的通道數,因為每個核對每個輸入通道包含一套不同的權重集合)。因為FCN只包含卷積層(和池化層,屬性相同),所以可以在任何大小的圖片上訓練和運行。

舉個例子,假設已經訓練好了一個用於分類和定位的CNN。圖片大小是224 × 224,輸出10個數:輸出0到4經過softmax激活函數,給出類的概率;輸出5經過邏輯激活函數,給出對象性分數;輸出6到9不經過任何激活函數,表示邊框的中心坐標、高和寬。

現在可以將緊密層轉換為卷積層。事實上,不需要再次訓練,只需將緊密層的權重複制到卷積層中。另外,可以在訓練前,將CNN轉換成FCN。

當輸入圖片為224 × 224時(見圖14-25的左邊),假設輸出層前面的最後一個卷積層(也被稱為瓶頸層)輸出7 × 7的特徵映射。如果FCN的輸入圖片是448 × 448(見圖14-25的右邊),瓶頸層會輸出14 × 14的特徵映射。因為緊密輸出層被替換成了10個使用大小為7 × 7的過濾器的卷積層,"valid"填充,步長為1,輸出會有10個特徵映射,每個大小為8 × 8(因為14 – 7 + 1 = 8)。換句話說,FCN只會處理整張圖片一次,會輸出8 × 8的網格,每個格子有10個數(5個類概率,1個對象性分數,4個邊框參數)。就像之前滑動CNN那樣,每行滑動8步,每列滑動8步。再形象的講一下,將原始圖片切分成14 × 14的網格,然後用7 × 7的窗口在上面滑動,窗口會有8 × 8 = 64個可能的位置,也就是64個預測。但是,FCN方法又非常高效,因為只需觀察圖片一次。事實上,「只看一次」(You Only Look Once,YOLO)是一個非常流行的目標檢測架構的名字,下面介紹。

圖14-25 相同的FCN處理小圖片(左)和大圖片(右)

只看一次(YOLO)

YOLO是一個非常快且準確的目標檢測框架,是Joseph Redmon在2015年的一篇論文中提出的,2016年優化為YOLOv2,2018年優化為YOLOv3。速度快到甚至可以在實時影片中運行,可以看Redmon的這個例子(要翻牆)

YOLOv3的架構和之前討論過的很像,只有一些重要的不同點:

  • 每個網格輸出5個邊框(不是1個),每個邊框都有一個對象性得分。每個網格還輸出20個類概率,是在PASCAL VOC數據集上訓練的,這個數據集有20個類。每個網格一共有45個數:5個邊框,每個4個坐標參數,加上5個對象性分數,加上20個類概率。
  • YOLOv3不是預測邊框的絕對坐標,而是預測相對於網格坐標的偏置量,(0, 0)是網格的左上角,(1, 1)是網格的右下角。對於每個網格,YOLOv3是被訓練為只能預測中心位於網格的邊框(邊框通常比網格大得多)。YOLOv3對邊框坐標使用邏輯激活函數,以保證其在0到1之間。
  • 開始訓練神經網路之前,YOLOv3找了5個代表性邊框維度,稱為錨定框(anchor box)(或稱為前邊框)。它們是通過K-Means演算法(見第9章)對訓練集邊框的高和寬計算得到的。例如,如果訓練圖片包含許多行人,一個錨定框就會獲取行人的基本維度。然後當神經網路對每個網格預測5個邊框時,實際是預測如何縮放每個錨定框。比如,假設一個錨定框是100個像素高,50個像素寬,神經網路可能的預測是垂直放大到1.5倍,水平縮小為0.9倍。結果是150 × 45的邊框。更準確的,對於每個網格和每個錨定框,神經網路預測其垂直和水平縮放參數的對數。有了錨定框,可以更容易預測出邊框,因為可以更快的學到邊框的樣子,速度也會更快。
  • 神經網路是用不同規模的圖片來訓練的:每隔幾個批次,網路就隨機調訓新照片維度(從330 × 330到608 × 608像素)。這可以讓網路學到不同的規模。另外,還可以在不同規模上使用YOLOv3:小圖比大圖快但準確性差。

還可能有些有意思的創新,比如使用跳連接來恢復一些在CNN中損失的空間解析度,後面討論語義分割時會討論。在2016年的這篇論文中,作者介紹了使用層級分類的YOLO9000模型:模型預測視覺層級(稱為詞樹,WordTree)中的每個節點的概率。這可以讓網路用高置信度預測圖片表示的是什麼,比如狗,即便不知道狗的品種。建議閱讀這三篇論文:不僅文筆不錯,還給出不少精彩的例子,介紹深度學習系統是如何一點一滴進步的。

平均精度均值(mean Average Precision,mAP) 目標檢測中非常常見的指標是平均精度均值。「平均均值」聽起來啰嗦了。要弄明白這個指標,返回到第3章中的兩個分類指標:精確率和召回率。取捨關係:召回率越高,精確率就越低。可以在精確率/召回率曲線上看到。將這條曲線歸納為一個數,可以計算曲線下面積(AUC)。但精確率/召回率曲線上有些部分,當精確率上升時,召回率也上升,特別是當召回率較低時(可以在圖3-5的頂部看到)。這就是產生mAP的激勵之一。

圖3-5 精確率vs召回率 假設當召回率為10%時,分類器的精確率是90%,召回率為20%時,精確率是96%。這裡就沒有取捨關係:使用召回率為20%的分類器就好,因為此時精確率更高。所以當召回率至少有10%時,需要找到最高精確率,即96%。因此,一個衡量模型性能的方法是計算召回率至少為0%時,計算最大精確率,再計算召回率至少為10%時的最大精確率,再計算召回率至少為20%時的最大精確率,以此類推。最後計算這些最大精確率的平均值,這個指標稱為平均精確率(Average Precision (AP))。當有超過兩個類時,可以計算每個類的AP,然後計算平均AP(即,mAP)。就是這樣! 在目標檢測中,還有另外一個複雜度:如果系統檢測到了正確的類,但是定位錯了(即,邊框不對)?當然不能將其作為正預測。一種方法是定義IOU閾值:例如,只有當IOU超過0.5時,預測才是正確的。相應的mAP表示為[email protected](或mAP@50%,或AP50)。在一些比賽中(比如PASCAL VOC競賽),就是這麼做的。在其它比賽中(比如,COCO),mAP是用不同IOU閾值(0.50, 0.55, 0.60, …, 0.95)計算的。最終指標是所有這些mAP的均值(表示為AP@[.50:.95] 或 AP@[.50:0.05:.95]),這是均值的均值。

一些YOLO的TensorFlow實現可以在GitHub上找到。可以看看Zihao Zang 用 TensorFlow 2 實現的項目。TensorFlow Models項目中還有其它目標檢測模型;一些還傳到了TF Hub,比如SSDFaster-RCNN,這兩個都很流行。SSD也是一個「一次」檢測模型,類似於YOLO。Faster R-CNN複雜一些:圖片先經過CNN,然後輸出經過區域提議網路(Region Proposal Network,RPN),RPN對邊框做處理,更容易圈住目標。根據CNN的裁剪輸出,每個邊框都運行這一個分類器。

檢測系統的選擇取決於許多因素:速度、準確率、預訓練模型是否可用、訓練時間、複雜度,等等。論文中有許多指標表格,但測試環境的變數很多。技術進步也很快,很難比較出哪個更適合大多數人,並且有效期可以長過幾個月。

語義分割

在語義分割中,每個像素根據其所屬的目標來進行分類(例如,路、汽車、行人、建築物,等等),見圖14-26。注意,相同類的不同目標是不做區分的。例如,分割圖片的右側的所有自行車被歸類為一坨像素。這個任務的難點是當圖片經過常規CNN時,會逐漸丟失空間解析度(因為有的層的步長大於1);因此,常規的CNN可以檢測出圖片的左下有一個人,但不知道準確的位置。

和目標檢測一樣,有多種方法來解決這個問題,其中一些比較複雜。但是,之前說過,Jonathan Long等人在2015年的一篇論文中提出樂意簡單的方法。作者先將預訓練的CNN轉變為FCN,CNN使用32的總步長(即,將所有大於1的步長相加)作用到輸入圖片上,最後一層的輸出特徵映射比輸入圖片小32倍。這樣過於粗糙,所以添加了一個單獨的上取樣層,將解析度乘以32。

圖14-26 語義分割

有幾種上取樣(增加圖片大小)的方法,比如雙線性插值,但只在×4 或 ×8時好用。Jonathan Long等人使用了轉置卷積層:等價於,先在圖片中插入空白的行和列(都是0),然後做一次常規卷積(見圖14-27)。或者,有人將其考慮為常規卷積層,使用分數步長(比如,圖14-27中是1/2)。轉置卷積層一開始的表現和線性插值很像,但因為是可訓練的,在訓練中會變得更好。在tf.keras中,可以使用Conv2DTranspose層。

圖14-27 使用轉置卷積層做上取樣

筆記:在轉置卷積層中,步長定義為輸入圖片被拉伸的倍數,而不是過濾器步長。所以步長越大,輸出也就越大(和卷積層或池化層不同)。

TensorFlow卷積運算 TensorFlow還提供了一些其它類型的卷積層: keras.layers.Conv1D:為1D輸入創建卷積層,比如時間序列或文本,第15章會見到。 keras.layers.Conv3D:為3D輸入創建卷積層,比如3D PET掃描。 dilation_rate:將任何卷積層的dilation_rate超參數設為2或更大,可以創建有孔卷積層。等價於常規卷積層,加上一個膨脹的、插入了空白行和列的過濾器。例如,一個1 × 3的過濾器[[1,2,3]],膨脹4倍,就變成了[[1, 0, 0, 0, 2, 0, 0, 0, 3]]。這可以讓卷積層有一個更大的感受野,卻沒有增加計算量和額外的參數。 tf.nn.depthwise_conv2d():可以用來創建深度方向卷積層(但需要自己創建參數)。它將每個過濾器應用到每個獨立的輸入通道上。因此,因此,如果有fn個過濾器和fn'個輸入通道,就會輸出fn×fn'個特徵映射。

這個方法行得通,但還是不夠準確。要做的更好,作者從低層開始就添加了跳連接:例如,他們使用因子2(而不是32)對輸出圖片做上取樣,然後添加一個低層的輸出。然後對結果做因子為16的上取樣,總的上取樣因子為32(見圖14-28)。這樣可以恢復一些在早期池化中丟失的空間解析度。在他們的最優架構中,他們使用了兩個相似的跳連接,以從更低層恢復更小的細節。

總之,原始CNN的輸出又經過了下面的步驟:上取樣×2,加上一個低層的輸出(形狀相同),上取樣×2,加上一個更低層的輸出,最後上取樣×8。甚至可以放大,超過原圖大小:這個方法可以用來提高圖片的解析度,這個技術成為超-解析度。

圖14-28 跳連接可以從低層恢復一些空間解析度

許多GitHub倉庫提供了語義分割的TensorFlow實現,還可以在TensorFlow Models中找到預訓練的實例分割模型。實例分割和語義分割類似,但不是將相同類的所有物體合併成一坨,而是將每個目標都分開(可以將每輛自行車都分開)。目前,TensorFlow Models中可用的實例分割時基於Mask R-CNN架構的,是在2017年的一篇論文中提出的:通過給每個邊框做一個像素罩,拓展Faster R-CNN模型。所以不僅能得到邊框,還能獲得邊框中像素的像素罩。

可以發現,深度電腦視覺領域既寬廣又發展迅速,每年都會產生新的架構,都是基於卷積神經網路的。最近幾年進步驚人,研究者們現在正聚焦于越來越難的問題,比如對抗學習(可以讓網路對具有欺騙性的圖片更有抵抗力),可解釋性(理解為什麼網路做出這樣的分類),實時影像生成(見第17章),一次學習(觀察一次,就能認出目標呃系統)。一些人在探索全新的架構,比如Geoffrey Hinton的膠囊網路(見影片,notebook中有對應的程式碼)。下一章會介紹如何用循環神經網路和卷積神經網路來處理序列數據,比如時間序列。

練習

  1. 對於圖片分類,CNN相對於全連接DNN的優勢是什麼?
  2. 考慮一個CNN,有3個卷積層,每個都是3 × 3的核,步長為2,零填充。最低的層輸出100個特徵映射,中間的輸出200個特徵映射,最上面的輸出400個。輸入圖片是200 × 300像素的RGB圖。這個CNN的總參數量是多少?如果使用32位浮點數,做與測試需要多少記憶體?批次是50張圖片,訓練時的記憶體消耗是多少?
  3. 如果訓練CNN時GPU記憶體不夠,解決該問題的5種方法是什麼?
  4. 為什麼使用最大池化層,而不是同樣步長的卷積層?
  5. 為什麼使用局部響應歸一化層?
  6. AlexNet想對於LeNet-5的創新在哪裡?GoogLeNet、ResNet、SENet、Xception的創新又是什麼?
  7. 什麼是全卷積網路?如何將緊密層轉變為卷積層?
  8. 語義分割的主要技術難點是什麼?
  9. 從零搭建你的CNN,並在MNIST上達到儘可能高的準確率。
  10. 使用遷移學習來做大圖片分類,經過下面步驟:

a. 創建每個類至少有100張圖片的訓練集。例如,你可以用自己的圖片基於地點來分類(沙灘、山、城市,等等),或者使用現成的數據集(比如從TensorFlow Datasets)。

b. 將其分成訓練集、驗證集、訓練集。

c. 搭建輸入管道,包括必要的預處理操作,最好加上數據增強。

d. 在這個數據集上,微調預訓練模型。

  1. 嘗試下TensorFlow的風格遷移教程。用深度學習生成藝術作品很有趣。