TensorFlow 2.0深度學習算法實戰---第10章 卷積神經網絡

當前人工智能還未達到人類 5 歲水平,不過在感知方面進步飛快。未來在機器語音、視覺識別領域,五到十年內超越人類沒有懸念。−沈向洋

我們已經介紹了神經網絡的基礎理論、TensorFlow 的使用方法以及最基本的全連接層網絡模型,對神經網絡有了較爲全面、深入的理解。但是對於深度學習,我們尚存一絲疑惑。深度學習的深度是指網絡的層數較深,一般有 5 層以上,而目前所介紹的神經網絡層數大都實現爲 5 層之內。那麼深度學習與神經網絡到底有什麼區別和聯繫呢

本質上深度學習和神經網絡所指代的是同一類算法。1980 年代,基於生物神經元數學模型的多層感知機(Multi-Layer Perceptron,簡稱 MLP)實現的網絡模型就被叫作神經網絡。由於當時的計算能力受限、數據規模較小等因素,神經網絡一般只能訓練到很少的層數,我們把這種規模的神經網絡叫做淺層神經網絡(Shallow Neural Network)。淺層神經網絡不太容易輕鬆提取數據的高層特徵,表達能力一般,雖然在諸如數字圖片識別等簡單任務上取得不錯效果,但很快被 1990 年代新提出的支持向量機所超越。

加拿大多倫多大學教授 Geoffrey Hinton 長期堅持神經網絡的研究,但由於當時支持向量機的流行,神經網絡相關的研究工作遇到了重重阻礙。2006 年,Geoffrey Hinton 提出了一種逐層預訓練的算法,可以有效地初始化 Deep Belief Networks(DBN)網絡,從而使得訓練大規模、深層數(上百萬的參數量)的神經網絡成爲可能。在論文中,GeoffreyHinton 把深層的神經網絡叫做 Deep Neural Network,這一塊的研究也因此稱爲 DeepLearning(深度學習)。由此看來,深度學習和神經網絡本質上指代大體一致,深度學習更側重於深層次的神經網絡的相關研究。深度學習的“深度”將在本章的相關網絡模型上得到淋漓盡致的體現。

在學習更深層次的網絡模型之前,首先我們來探討這樣一個問題:1980 年代時神經網絡的理論研究基本已經到位,爲什麼卻沒能充分發掘出深層網絡的巨大潛力?通過對這個問題的討論,我們引出本章的核心內容:卷積神經網絡。這也是層數可以輕鬆達到上百層的一類神經網絡。

10.1 全連接網絡的問題

首先我們來分析全連接網絡存在的問題。考慮一個簡單的 4 層全連接層網絡,輸入是28 × 28打平後爲 784 節點的手寫數字圖片向量,中間三個隱藏層的節點數都是 256,輸出層的節點數是 10,如圖 10.1 所示。
在這裏插入圖片描述
通過 TensorFlow 快速地搭建此網絡模型,添加 4 個 Dense 層,並使用 Sequential 容器封裝爲一個網絡對象:

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers,Sequential,losses,optimizers,datasets

# 創建4層全連接網絡
model=keras.Sequential([
    layers.Dense(256,activation='relu'),
    lsyers.Dense(256,activation='relu'),
    layers.Dense(256,activation='relu'),
    layers.Dense(10),
])
#build模型,並打印模型信息
model.build(input_shape=(4,784))
model.summary()

利用 summary()函數打印出模型每一層的參數量統計結果,如表 10.1 所示。網絡的參數量是怎麼計算的呢?對於每一條連接線的權值標量,視作一個參數,因此對輸入節點數爲𝑛,輸出節點數爲𝑚的全連接層來說,𝑾張量包含的參數量共有𝑛 ∙ 𝑚個,𝒃向量包含的參數量有𝑚個,則全連接層的總參數量爲𝑛 ∙ 𝑚 + 𝑚。以第一層爲例,輸入特徵長度爲 784,輸出特徵長度爲 256,當前層的參數量爲 784 ∙ 256 + 256= 200960 ,同樣的方法可以計算第二、三、四層的參數量分別爲: 65792、 65792、5702 ,總參數量約 34 萬個。

在計算機中,如果將單個權值保存爲 float 類型的變量,至少需要佔用 4 個字節內存(Python 語言中float 佔用內存更多),那麼 34 萬個網絡參數至少需要約 1.34MB 內存。也就是說,單就存儲網絡的參數就需要 1.34MB 內存,實際上,網絡的訓練過程中還需要緩存計算圖模型、梯度信息、輸入和中間計算結果等,其中梯度相關運算佔用資源非常多。
在這裏插入圖片描述
那麼訓練這樣一個網絡到底需要多少內存呢?我們可以在現代 GPU 設備上簡單模擬一下資源消耗情況。在 TensorFlow 中,如果不設置顯存佔用方式,那麼默認會佔用全部顯存。這裏將 TensorFlow 的顯存使用方式設置爲按需分配,觀測其真實佔用的 GPU 顯存資源情況,代碼如下:

#獲取所有GPU設備列表
gpus=tf.config.experimental.list_physical_devices('GPU')
if gpus:
    try:
        # 設置GPU顯存設備爲按需分配,增長式
        for gpu in gpus:
            tf.config.experimental.set_memory_growth(gpu,True)
    except RuntimeError as e:
        # 異常處理
        print(e)

上述代碼插入在 TensorFlow 庫導入後、模型創建前的位置,通過tf.config.experimental.set_memory_growth(gpu, True)設置 TensorFlow 按需申請顯存資源,這樣 TensorFlow 佔用的顯存大小即爲運算需要的數量。在 Batch Size 設置爲 32 的情況下,x訓練時我們觀察到顯存佔用了約 708MB,內存佔用約 870MB。由於現代深度學習框架設計考量不一樣,這個數字僅做參考。即便如此,我們也能感受到 4 層的全連接層的計算代價並不小。

回到 1980 年代,1.3MB 的網絡參數量是什麼概念呢?1989 年,Yann LeCun 在手寫郵政編碼識別的論文中採用了一臺 256KB 內存的計算機實現了他的算法,這臺計算機還配備了一塊 AT&T DSP-32C 的 DSP 計算卡(浮點數計算能力約爲 25MFLOPS)。對於 1.3MB的網絡參數,256KB 內存的計算機連網絡參數都尚且裝載不下,更別提網絡訓練了。由此可見,全連接層較高的內存佔用量嚴重限制了神經網絡朝着更大規模、更深層數方向的發展。

10.1.1 局部相關性

接下來我們探索如何避免全連接網絡的參數量過大的缺陷。爲了便於討論,我們以圖片類型數據爲輸入的場景爲例。對於 2D 的圖片數據,在進入全連接層之前,需要將矩陣數據打平成 1D 向量,然後每個像素點與每個輸出節點兩兩相連,我們把連接關係非常形象地對應到圖片的像素位置上,如圖 10.2 所示。
在這裏插入圖片描述
可以看出,網絡層的每個輸出節點都與所有的輸入節點相連接,用於提取所有輸入節點的特徵信息,這種稠密的連接方式是全連接層參數量大、計算代價高的根本原因。全連接層也稱爲稠密連接層(Dense Layer),輸出與輸入的關係爲:
在這裏插入圖片描述
其中nodes(𝐼)表示 I 層的節點集合。

那麼,輸出節點是否有必要和全部的輸入節點相連接呢?有沒有一種近似的簡化模型呢?我們可以分析輸入節點對輸出節點的重要性分佈,僅考慮較重要的一部分輸入節點,而拋棄重要性較低的部分節點,這樣輸出節點只需要與部分輸入節點相連接,表達爲:
在這裏插入圖片描述
其中top(I,j,kI,j,k)表示 II 層中對於 JJ 層中的jj號節點重要性最高的前kk個節點集合。通過這種方式,可以把全連接層的IJ‖I‖ ∙ ‖J‖個權值連接減少到kJk ∙ ‖J‖個,其中I‖I‖J‖J‖分佈表示 IIJJ 層的節點數量。

那麼問題就轉變爲探索II層輸入節點對於jj號輸出節點的重要性分佈。然而找出每個中間節點的重要性分佈是件非常困難的事情,我們可以針對於具體問題,利用先驗知識把這個問題進一步簡化。

在現實生活中,存在着大量以位置或距離作爲重要性分佈衡量標準的數據,比如和自己居住更近的人更有可能對自己影響更大(位置相關),股票的走勢預測應該更加關注近段時間的數據趨勢(時間相關),圖片每個像素點和周邊像素點的關聯度更大(位置相關)。

以2D 圖片數據爲例,如果簡單地認爲與當前像素歐式距離(Euclidean Distance)小於和等於k2\frac{k}{\sqrt{2}}的像素點重要性較高,歐式距離大於k2\frac{k}{\sqrt{2}}到像素點重要性較低,那麼我們就很輕鬆地簡化了每個像素點的重要性分佈問題。

如圖 10.3 所示,以實心網格所在的像素爲參考點,它周邊歐式距離小於或等於k2\frac{k}{\sqrt{2}}的像素點以矩形網格表示,網格內的像素點重要性較高,網格外的像素點較低。這個高寬爲𝑘的窗口稱爲感受野(Receptive Field),它表了每個像素對於中心像素的重要性分佈情況,網格內的像素纔會被考慮,網格外的像素對於中心像素會被簡單地忽略。
在這裏插入圖片描述
這種基於距離的重要性分佈假設特性稱爲局部相關性,它只關注和自己距離較近的部分節點,而忽略距離較遠的節點。在這種重要性分佈假設下,全連接層的連接模式變成了如圖 10.4 所示,輸出節點𝑗只與以𝑗爲中心的局部區域(感受野)相連接,與其它像素無連接。
在這裏插入圖片描述
利用局部相關性的思想,我們把感受野窗口的高、寬記爲𝑘(感受野的高、寬可以不相等,爲了便與表達,這裏只討論高寬相等的情況),當前位置的節點與大小爲𝑘的窗口內的所有像素相連接,與窗口外的其它像素點無關,此時網絡層的輸入輸出關係表達如下:
在這裏插入圖片描述
其中dist(𝑖 𝑗)表示節點𝑖、𝑗之間的歐式距離。

10.1.2 權值共享

每個輸出節點僅與感受野區域內𝑘 × 𝑘個輸入節點相連接,輸出層節點數爲J‖J‖,則當前層的參數量爲𝑘 × 𝑘 × J‖J‖,相對於全連接層的I‖I‖ ×J‖J‖,考慮到𝑘一般取值較小,如 1、3、5 等,𝑘 × 𝑘 ≪ I‖I‖,因此成功地將參數量減少了很多。

能否再將參數量進一步減少,比如只需要𝑘 × 𝑘個參數即可完成當前層的計算?答案是肯定的,通過權值共享的思想,對於每個輸出節點ojo_{j},均使用相同的權值矩陣𝑾,那麼無論輸出節點的數量J‖J‖是多少,網絡層的參數量總是𝑘 × 𝑘。如圖 10.5 所示,在計算左上角位置的輸出像素時,使用權值矩陣:
在這裏插入圖片描述
與對應感受野內部的像素相乘累加,作爲左上角像素的輸出值;在計算右下方感受野區域時,共享權值參數𝑾,即使用相同的權值參數𝑾相乘累加,得到右下角像素的輸出值,此時網絡層的參數量只有3 × 3 = 9個,且與輸入、輸出節點數無關。
在這裏插入圖片描述
通過運用局部相關性權值共享的思想,我們成功把網絡的參數量從I×J‖I‖ × ‖J‖減少到K×kK × k(準確地說,是在單輸入通道、單卷積核的條件下)。這種共享權值的“局部連接層”網絡其實就是卷積神經網絡。接下來我們將從數學角度介紹卷積運算,進而正式學習卷積神經網絡的原理與計算方法。

10.1.3 卷積運算

在局部相關性的先驗下,我們提出了簡化的“局部連接層”,對於窗口𝑘 × 𝑘內的所有像素,採用權值相乘累加的方式提取特徵信息,每個輸出節點提取對應感受野區域的特徵信息。這種運算其實是信號處理領域的一種標準運算:離散卷積運算。離散卷積運算在計算機視覺中有着廣泛的應用,這裏給出卷積神經網絡層從數學角度的闡述。

在信號處理領域,1D 連續信號的卷積運算被定義兩個函數的積分:函數𝑓(𝜏)、函數𝑔(𝜏),其中𝑔(𝜏)經過了翻轉𝑔(−𝜏)和平移後變成𝑔(𝑛 − 𝜏)。卷積的“卷”是指翻轉平移操作,“積”是指積分運算,1D 連續卷積定義爲:
(fg)(n)=f(τ)g(nτ)dτ(f \otimes g)(n)=\int_{-\infty}^{\infty} f(\tau) g(n-\tau) \mathrm{d} \tau
離散卷積將積分運算換成累加運算:
(fg)(n)=τ=f(τ)g(nτ)(f \otimes g)(n)=\sum_{\tau=-\infty}^{\infty} f(\tau) g(n-\tau)
至於卷積爲什麼要這麼定義,限於篇幅不做深入闡述。我們重點介紹 2D 離散卷積運算。在計算機視覺中,卷積運算基於 2D 圖片函數𝑓(𝑚,𝑛)和 2D 卷積核𝑔(𝑚,𝑛),其中𝑓(𝑖,𝑗)和𝑔(𝑖,𝑗)僅在各自窗口有效區域存在值,其它區域視爲 0,如圖 10.6 所示。此時的 2D 離散卷積定義爲:
[fg](m,n)=i=j=f(i,j)g(mi,nj)[f \otimes g](m, n)=\sum_{i=-\infty}^{\infty} \sum_{j=-\infty}^{\infty} f(i, j) g(m-i, n-j)
在這裏插入圖片描述
我們來詳細介紹 2D 離散卷積運算。首先,將卷積核𝑔(𝑖 , 𝑗)函數翻轉(沿着𝑥和𝑦方向各翻轉一次),變成𝑔(−𝑖 ,−𝑗)。當(𝑚 ,𝑛) = (−1, −1 )時,𝑔(− 1− 𝑖 ,− 1− 𝑗)表示卷積核函數翻轉後再向左、向上各平移一個單元,此時:
[fg](1,1)=i=j=f(i,j)g(1i,1j)=i[1,1]j[1,1]f(i,j)g(1i,1j)\begin{aligned} [f \otimes g](-1,-1) &=\sum_{i=-\infty}^{\infty} \sum_{j=-\infty}^{\infty} f(i, j) g(-1-i,-1-j) \\ &=\sum_{i \in[-1,1]} \sum_{j \in[-1,1]} f(i, j) g(-1-i,-1-j) \end{aligned}
2D 函數只在𝑖 ∈[-1,1] , 𝑗 ∈[-1,1] 存在有效值,其它位置爲 0。按照計算公式,我們可以得到 𝑓⨂𝑔 ( 0,−1 ) =7 ,如下圖 10.7 所示:
在這裏插入圖片描述
同樣的方法,(𝑚 ,𝑛) = ( 0,−1 )時:
[fg](0,1)=i[1,1]j[1,1]f(i,j)g(0i,1j)[f \otimes g](0,-1)=\sum_{i \in[-1,1] j \in[-1,1]} f(i, j) g(0-i,-1-j)
即卷積核翻轉後再向上平移一個單元后對應位置相乘累加, 𝑓⨂𝑔 (0, −1 ) =7 ,如圖 10.8所示。
在這裏插入圖片描述
即卷積核翻轉後再向右、向上各平移一個單元后對應位置相乘累加, 𝑓⨂𝑔 (1, −1 ) = 1,如圖 10.9 所示。
在這裏插入圖片描述
當(𝑚 𝑛) = (−1,0 )時:
[fg](1,0)=i[1,1]j[1,1]f(i,j)g(1i,j)[f \otimes g](-1,0)=\sum_{i \in[-1,1] j \in[-1,1]} f(i, j) g(-1-i,-j)
即卷積核翻轉後再向左平移一個單元后對應位置相乘累加, 𝑓⨂𝑔 (−1,0 ) = 1,如圖 10.10所示。
在這裏插入圖片描述
按照此種方式循環計算,可以計算出函數 𝑓⨂𝑔 (𝑚 𝑚) 𝑚 ∈[-1,1] , 𝑛 ∈[-1,1]的所有值,如下圖 10.11 所示。
在這裏插入圖片描述
至此,我們成功完成圖片函數與卷積核函數的卷積運算,得到一個新的特徵圖。

回顧“權值相乘累加”的運算,我們把它記爲 [𝑓 ∙ 𝑔] (𝑚 ,𝑛):
[fg](m,n)=i[w/2,w/2]j[h/2,h/2]f(i,j)g(im,jn)[f \cdot g](m, n)=\sum_{i \in[-w / 2, w / 2]} \sum_{j \in[-h / 2, h / 2]} f(i, j) g(i-m, j-n)
仔細比較它與標準的 2D 卷積運算不難發現,在“權值相乘累加”中的卷積核函數𝑔(𝑚 ,𝑛),並沒有經過翻轉。只不過對於神經網絡來說,目標是學到一個函數𝑔(𝑚 ,𝑛)使得ℒ越小越好,至於𝑔(𝑚, 𝑛)是不是恰好就是卷積運算中定義的“卷積核”函數並不十分重要,因爲我們並不會直接利用它。在深度學習中,函數𝑔(𝑚 ,𝑛)統一稱爲卷積核(Kernel),有時也叫 Filter、Weight 等。由於始終使用𝑔(𝑚 ,𝑛)函數完成卷積運算,卷積運算其實已經實現了權值共享的思想。

我們來小結 2D 離散卷積運算流程:每次通過移動卷積核,並與圖片對應位置處的感受野像素相乘累加,得到此位置的輸出值。卷積核即是行、列爲𝑘大小的權值矩陣𝑾,對應到特徵圖上大小爲𝑘的窗口即爲感受野,感受野與權值矩陣𝑾相乘累加,得到此位置的輸出值。通過權值共享,我們從左上方逐步向右、向下移動卷積核,提取每個位置上的像素特徵,直至最右下方,完成卷積運算。可以看出,兩種理解方式殊途同歸,從數學角度理解,卷積神經網絡即是完成了 2D 函數的離散卷積運算;從局部相關與權值共享角度理解,也能得到一樣的效果。通過這兩種角度,我們既能直觀理解卷積神經網絡的計算流程,又能嚴謹地從數學角度進行推導。正是基於卷積運算,卷積神經網絡才能如此命名。

在計算機視覺領域,2D 卷積運算能夠提取數據的有用特徵,通過特定的卷積核與輸入圖片進行卷積運算,獲得不同特徵的輸出圖片,如下表 10.2 所示,列舉了一些常見的卷積核及其效果樣片。
在這裏插入圖片描述

10.2 卷積神經網絡

卷積神經網絡通過充分利用局部相關性權值共享的思想,大大地減少了網絡的參數量,從而提高訓練效率,更容易實現超大規模的深層網絡。2012 年,加拿大多倫多大學Alex Krizhevsky 將深層卷積神經網絡應用在大規模圖片識別挑戰賽 ILSVRC-2012 上,在ImageNet 數據集上取得了 15.3% 的 Top-5 錯誤率,排名第一,相對於第二名在 Top-5 錯誤率上降低了 10.9%,這一巨大突破引起了業界強烈關注,卷積神經網絡迅速成爲計算機視覺領域的新寵,隨後在一系列的任務中,基於卷積神經網絡的形形色色的模型相繼被提出,並在原有的性能上取得了巨大提升。

現在我們來介紹卷積神經網絡層的具體計算流程。以 2D 圖片數據爲例,卷積層接受高、寬分別爲ℎ、𝑤,通道數爲cinc_{in}的輸入特徵圖𝑿,在coutc_{out}個高、寬都爲𝑘,通道數爲cinc_{in}的卷積核作用下,生成高、寬分別爲ℎ′、𝑤′,通道數爲coutc_{out}的特徵圖輸出。需要注意的是,卷積核的高寬可以不等,爲了簡化討論,這裏僅討論高寬都爲𝑘的情況,之後可以輕鬆推廣到高、寬不等的情況。

我們首先從單通道輸入、單卷積核的情況開始討論,然後推廣至多通道輸入、單卷積核,最後討論最常用,也是最複雜的多通道輸入、多個卷積核的卷積層實現。

10.2.1 單通道輸入和單卷積核

首先討論單通道輸入cinc_{in}=1 ,如灰度圖片只有灰度值一個通道,單個卷積核coutc_{out}=1的情況。以輸入𝑿爲 5×5 的矩陣,卷積核爲3 × 3的矩陣爲例,如下圖 10.12 所示。與卷積核同大小的感受野(輸入𝑿上方的綠色方框)首先移動至輸入𝑿最左上方,選中輸入𝑿上3 × 3的感受野元素,與卷積核(圖片中間3 × 3方框)對應元素相乘:
在這裏插入圖片描述
⨀符號表示哈達馬積(Hadamard Product),即矩陣的對應元素相乘,它與矩陣相乘符號@是矩陣的二種最爲常見的運算形式。運算後得到3 × 3的矩陣,這 9 個數值全部相加:
11+01+2+6+02+4=7-1-1+0-1+2+6+0-2+4=7
得到標量 7,寫入輸出矩陣第一行、第一列的位置,如圖 10.12 所示。
在這裏插入圖片描述
完成第一個感受野區域的特徵提取後,感受野窗口向右移動一個步長單位(Strides,記爲𝑠,默認爲 1),選中圖 10.13 中綠色方框中的 9 個感受野元素,按照同樣的計算方法,與卷積覈對應元素相乘累加,得到輸出 10,寫入第一行、第二列位置。
在這裏插入圖片描述
感受野窗口再次向右移動一個步長單位,選中圖 10.14 中綠色方框中的元素,並與卷積核相乘累加,得到輸出 3,並寫入輸出的第一行、第三列位置,如圖 10.14 所示。
在這裏插入圖片描述
此時感受野已經移動至輸入𝑿的有效像素的最右邊,無法向右邊繼續移動(在不填充無效元素的情況下),因此感受野窗口向下移動一個步長單位(𝑠 = 1),並回到當前行的行首位置,繼續選中新的感受野元素區域,如圖 10.15 所示,與卷積核運算得到輸出-1。此時的感受野由於經過向下移動一個步長單位,因此輸出值-1 寫入第二行、第一列位置。
在這裏插入圖片描述
按照上述方法,每次感受野向右移動𝑠 = 1個步長單位,若超出輸入邊界,則向下移動𝑠 =1 個步長單位,並回到行首,直到感受野移動至最右邊、最下方位置,如下圖 10.16 所示。

每次選中的感受野區域元素,和卷積覈對應元素相乘累加,並寫入輸出的對應位置。最終輸出我們得到一個3 × 3的矩陣,比輸入 5×5 略小,這是因爲感受野不能超出元素邊界的緣故。可以觀察到,卷積運算的輸出矩陣大小由卷積核的大小𝑘,輸入𝑿的高寬ℎ/𝑤,移動步長𝑠,是否填充等因素共同決定。這裏爲了演示計算過程,預繪製了一個與輸入等大小的網格,並不表示輸出高寬爲 5×5 ,這裏的實際輸出高寬只有3 × 3。
在這裏插入圖片描述
現在我們已經介紹了單通道輸入、單個卷積核的運算流程。實際的神經網絡輸入通道數量往往較多,接下來我們將學習多通道輸入、單個卷積核的卷積運算方法。

10.2.2 多通道輸入和單卷積核

多通道輸入的卷積層更爲常見,比如彩色的圖片包含了 R/G/B 三個通道,每個通道上面的像素值表示 R/G/B 色彩的強度。
下面我們以 3 通道輸入、單個卷積核爲例,將單通道輸入的卷積運算方法推廣到多通道的情況。如圖 10.17 中所示,每行的最左邊 5×5 的矩陣表示輸入𝑿的 1~3 通道,第 2 列的3 × 3矩陣分別表示卷積核的 1~3 通道,第 3 列的矩陣表示當前通道上運算結果的中間矩陣,最右邊一個矩陣表示卷積層運算的最終輸出。
在這裏插入圖片描述
在多通道輸入的情況下,卷積核的通道數需要和輸入𝑿的通道數量相匹配,卷積核的第𝑖個通道和𝑿的第𝑖個通道運算,得到第𝑖箇中間矩陣,此時可以視爲單通道輸入與單卷積核的情況,所有通道的中間矩陣對應元素再次相加,作爲最終輸出。

具體的計算流程如下:在初始狀態,如圖 10.17 所示,每個通道上面的感受野窗口同步落在對應通道上面的最左邊、最上方位置,每個通道上感受野區域元素與卷積覈對應通道上面的矩陣相乘累加,分別得到三個通道上面的輸出 7、-11、-1 的中間變量,這些中間變量相加得到輸出-5,寫入對應位置。

隨後,感受野窗口同步在𝑿的每個通道上向右移動𝑠 = 1個步長單位,此時感受野區域元素如下圖 10.18 所示,每個通道上面的感受野與卷積覈對應通道上面的矩陣相乘累加,得到中間變量 10、20、20,全部相加得到輸出 50,寫入第一行、第二列元素位置。
在這裏插入圖片描述
以此方式同步移動感受野窗口,直至最右邊、最下方位置,此時全部完成輸入和卷積核的卷積運算,得到3 × 3的輸出矩陣,如圖 10.19 所示。
在這裏插入圖片描述
整個的計算示意圖如下圖 10.20 所示,輸入的每個通道處的感受野均與卷積核的對應通道相乘累加,得到與通道數量相等的中間變量,這些中間變量全部相加即得到當前位置的輸出值。輸入通道的通道數量決定了卷積核的通道數一個卷積核只能得到一個輸出矩陣,無論輸入𝑿的通道數量
在這裏插入圖片描述
一般來說,一個卷積核只能完成某種邏輯的特徵提取,當需要同時提取多種邏輯特徵時,可以通過增加多個卷積核來得到多種特徵,提高神經網絡的表達能力,這就是多通道輸入、多卷積核的情況。

10.2.3 多通道輸入、多卷積核

多通道輸入、多卷積核是卷積神經網絡中最爲常見的形式,前面我們已經介紹了單卷積核的運算過程,每個卷積核和輸入𝑿做卷積運算,得到一個輸出矩陣。當出現多卷積核時,第𝑖 (𝑖 ∈[1, 𝑛] ,𝑛爲卷積核個數)個卷積核與輸入𝑿運算得到第𝑖個輸出矩陣(也稱爲輸出張量𝑶的通道𝑖),最後全部的輸出矩陣在通道維度上進行拼接(Stack 操作,創建輸出通道數的新維度),產生輸出張量𝑶,𝑶包含了𝑛個通道數。

以 3 通道輸入、2 個卷積核的卷積層爲例。第一個卷積核與輸入𝑿運算得到輸出𝑶的第一個通道,第二個卷積核與輸入𝑿運算得到輸出𝑶的第二個通道,如下圖 10.21 所示,輸出的兩個通道拼接在一起形成了最終輸出𝑶。每個卷積核的大小𝑘、步長𝑠、填充設定等都是統一設置,這樣才能保證輸出的每個通道大小一致,從而滿足拼接的條件。
在這裏插入圖片描述

10.2.4 步長

在卷積運算中,如何控制感受野佈置的密度呢?對於信息密度較大的輸入,如物體數量很多的圖片,爲了儘可能的少漏掉有用信息,在網絡設計的時候希望能夠較密集地佈置感受野窗口;對於信息密度較小的輸入,比如全是海洋的圖片,可以適量的減少感受野窗口的數量。感受野密度的控制手段一般是通過移動步長(Strides)實現的。

步長是指感受野窗口每次移動的長度單位,對於 2D 輸入來說,分爲沿𝑥(向右)方向和𝑦(向下)方向的移動長度。爲了簡化討論,這裏只考慮𝑥/𝑦方向移動步長相同的情況,這也是神經網絡中最常見的設定。如下圖 10.22 所示,綠色實線代表的感受野窗口的位置是當前位置,綠色虛線代表是上一次感受野所在位置,從上一次位置移動到當前位置的移動長度即是步長的定義。圖 10.22 中感受野沿𝑥方向的步長爲 2,表達爲步長𝑠 = 2。
在這裏插入圖片描述
當感受野移動至輸入𝑿右邊的邊界時,感受野向下移動一個步長𝑠 = 2,並回到行首。如下圖 10.23 所示,感受野向下移動 2 個單位,並回到行首位置,進行相乘累加運算。
在這裏插入圖片描述
循環往復移動,直至達到最下方、最右邊邊緣位置,如圖 10.24 所示,最終卷積層輸出的高寬只有2 × 2。對比前面𝑠 =1 的情形,輸出高寬由3 × 3降低爲2 × 2,感受野的數量減少爲僅 4 個。
在這裏插入圖片描述
可以看到,通過設定步長𝑠,可以有效地控制信息密度的提取。當步長設計的較小時,感受野以較小幅度移動窗口,有利於提取到更多的特徵信息,輸出張量的尺寸也更大;當步長設計的較大時,感受野以較大幅度移動窗口,有利於減少計算代價,過濾冗餘信息,輸出張量的尺寸也更小

10.2.5 填充

經過卷積運算後的輸出𝑶的高寬一般會小於輸入𝑿的高寬,即使是步長𝑠 =1 時,輸出𝑶的高寬也會略小於輸入𝑿高寬。在網絡模型設計時,有時希望輸出𝑶的高寬能夠與輸入𝑿的高寬相同,從而方便網絡參數的設計、殘差連接等。爲了讓輸出𝑶的高寬能夠與輸入𝑿的相等,一般通過在原輸入𝑿的高和寬維度上面進行填充(Padding)若干無效元素操作,得到增大的輸入𝑿′。通過精心設計填充單元的數量,在𝑿′上面進行卷積運算得到輸出𝑶的高寬可以和原輸入𝑿相等,甚至更大。

如下圖 10.25 所示,在高/行方向的上(Top)、下(Bottom)方向,寬/列方向的左(Left)、右(Right)均可以進行不定數量的填充操作,填充的數值一般默認爲 0,也可以填充自定義的數據。圖 10.25 中上、下方向各填充 1 行,左、右方向各填充 2 列,得到新的輸入𝑿′。
在這裏插入圖片描述
那麼添加填充後的卷積層怎麼運算呢?同樣的方法,僅僅是把參與運算的輸入從𝑿換成了填充後得到的新張量𝑿′。如下圖 10.26 所示,感受野的初始位置在填充後的𝑿′的左上方,完成相乘累加運算,得到輸出 1,寫入輸出張量的對應位置。
在這裏插入圖片描述
移動步長𝑠 = 1個單位,重複運算邏輯,得到輸出 0,如圖 10.27 所示。
在這裏插入圖片描述
循環往復,最終得到 × 的輸出張量,如圖 10.28 所示。
在這裏插入圖片描述
通過精心設計的 Padding 方案,即上下左右各填充一個單位,記爲𝑝 =1 ,我們可以得到輸出𝑶和輸入𝑿的高、寬相等的結果;在不加 Padding 的情況下,如下圖 10.29 所示,只能得到3 × 3的輸出𝑶,略小於輸入𝑿。
在這裏插入圖片描述
卷積神經層的輸出尺寸[b,h,W,COut]\left[b, h^{\prime}, W^{\prime}, C_{O u t}\right]由卷積核的數量CoutC_{out},卷積核的大小𝑘,步長𝑠,填充數𝑝(只考慮上下填充數量php_{h}相同,左右填充數量pwp_{w}相同的情況)以及輸入𝑿的高寬ℎ/𝑤共同決定,它們之間的數學關係可以表達爲:
h=h+2phks+1h^{\prime}=\left\lfloor\frac{h+2 \cdot p_{h}-k}{s}\right\rfloor+1
w=w+2pwks+1w^{\prime}=\left\lfloor\frac{w+2 \cdot p_{w}-k}{s}\right\rfloor+1
其中php_{h}pwp_{w}分別表示高、寬方向的填充數量,⌊∙⌋表示向下取整。以上面的例子爲例,ℎ =𝑤 = 5,𝑘 = 3,php_{h}=pwp_{w}=1 ,𝑠 =1 ,輸出的高寬分別爲:
h=5+2131+1=4+1=5w=5+2131+1=4+1=5\begin{array}{l} h^{\prime}=\left\lfloor\frac{5+2 * 1-3}{1}\right\rfloor+1=\lfloor 4\rfloor+1=5 \\ w^{\prime}=\left\lfloor\frac{5+2 * 1-3}{1}\right\rfloor+1=\lfloor 4\rfloor+1=5 \end{array}
在 TensorFlow 中,在𝑠 =1 時,如果希望輸出𝑶和輸入𝑿高、寬相等,只需要簡單地設置參數 padding=”SAME”即可使 TensorFlow 自動計算 padding 數量,非常方便。

10.3 卷積層實現

在 TensorFlow 中,既可以通過自定義權值的底層實現方式搭建神經網絡,也可以直接調用現成的卷積層類的高層方式快速搭建複雜網絡。我們主要以 2D 卷積爲例,介紹如何實現卷積神經網絡層。

10.3.1 自定義權值

在 TensorFlow 中,通過tf.nn.conv2d函數可以方便地實現 2D 卷積運算。tf.nn.conv2d基於輸入𝑿: [b,h,w,cin]\left[b, h, w, c_{i n}\right]和卷積核𝑾: [k,k,cin,cout]\left[k, k, c_{in}, c_{out}\right]進行卷積運算,得到輸出𝑶 [b,h,w,cout]\left[b, h^{\prime}, w^{\prime}, c_{o u t}\right],其中cinc_{in}表示輸入通道數,coutc_{out}表示卷積核的數量,也是輸出特徵圖的通道數。例如:

x=tf.random.normal([2,5,5,3])#模擬輸入,3通道,高寬爲5
#需要根據[k,k,cin,cout]格式創建張量W,4個3×3大小卷積核
w=tf.random.normal([3,3,3,4])
#步長爲1,padding爲0
out=tf.nn.conv2d(x,w,strides=1,padding=[[0,0],[0,0],[0,0],[0,0]])
#輸出張量的shape
print(out.shape)

(2, 3, 3, 4)

其中 padding 參數的設置格式爲:

padding=[[0,0],[,],[,],[0,0]]

例如,上下左右各填充一個單位,則 padding 參數設置爲 ,實現如下:

x=tf.random.normal([2,5,5,3])#模擬輸入,3通道,高寬爲5
#需要根據[k,k,cin,cout]格式創建張量W,4個3×3大小卷積核
w=tf.random.normal([3,3,3,4])
#步長爲1,padding爲0
out=tf.nn.conv2d(x,w,strides=1,padding=[[0,0],[1,1],[1,1],[0,0]])
#輸出張量的shape
print(out.shape)

TensorShape([2, 5, 5, 4])

特別地,通過設置參數 padding='SAME'、strides=1可以直接得到輸入、輸出同大小的卷積層,其中 padding 的具體數量由 TensorFlow 自動計算並完成填充操作。例如:

x=tf.random.normal([2,5,5,3])#模擬輸入,3通道,高寬爲5
#需要根據[k,k,cin,cout]格式創建張量W,4個3×3大小卷積核
w=tf.random.normal([3,3,3,4])
#步長爲,padding設置爲輸出、輸入同大小
# 需要注意的是,padding=same只有在strides=1時纔是同大小
out=tf.nn.conv2d(x,w,strides=1,padding='SAME')
#輸出張量的shape
print(out.shape)

Out[3]: TensorShape([2, 5, 5, 4])

當𝑠 >1 時,設置 padding='SAME'將使得輸出高、寬將成1S\frac{1}{S}倍地減少。例如:

x=tf.random.normal([2,5,5,3])#模擬輸入,3通道,高寬爲5
#需要根據[k,k,cin,cout]格式創建張量W,4個3×3大小卷積核
w=tf.random.normal([3,3,3,4])
# 高寬先padding成可以整除3的最小整數6,然後6按3倍減少,得到2×2
out=tf.nn.conv2d(x,w,strides=3,padding='SAME')
#輸出張量的shape
print(out.shape)

TensorShape([2, 2, 2, 4])

卷積神經網絡層與全連接層一樣,可以設置網絡帶偏置向量。tf.nn.conv2d 函數是沒有實現偏置向量計算的,添加偏置只需要手動累加偏置張量即可。例如:

# 根據[count]格式創建偏置向量
b=tf.zeros([4])
# 在卷積輸出上疊加偏置向量,它會自動broadcasting爲[b,h',w',count]
out=out+b
10.3.2 卷積層類

通過卷積層類 layers.Conv2D 可以不需要手動定義卷積核𝑾和偏置𝒃張量,直接調用類實例即可完成卷積層的前向計算,實現更加高層和快捷。
在TensorFlow中,API 的命名有一定的規律,首字母大寫的對象一般表示類,全部小寫的一般表示函數,如 layers.Conv2D表示卷積層類,nn.conv2d表示卷積運算函數。使用類方式會(在創建類時或 build 時)自動創建需要的權值張量和偏置向量等,用戶不需要記憶卷積核張量的定義格式,因此使用起來更簡單方便,但是靈活性也略低。函數方式的接口需要自行定義權值和偏置等,更加靈活和底層。

在新建卷積層類時,只需要指定卷積核數量參數 filters卷積核大小 kernel_size,步長strides,填充 padding 等即可。如下創建了 4 個3 × 3大小的卷積核的卷積層,步長爲 1,padding 方案爲’SAME’:

layer = layers.Conv2D(4,kernel_size=3,strides=1,padding='SAME')

如果卷積核高寬不等,步長行列方向不等,此時需要將 kernel_size 參數設計爲 tuple格式(khk_{h}, kwk_{w}),strides 參數設計爲(shs_{h} ,sws_{w})。如下創建 4 個3 ×4 大小的卷積核,豎直方向移動步長shs_{h}= 2,水平方向移動步長sws_{w}=1 :

layer = layers.Conv2D(4,kernel_size=(3,4),strides=(2,1),padding='SAME')

創建完成後,通過調用實例(的__call__方法)即可完成前向計算,例如:

#創建卷積層類
x=tf.random.normal([2,5,5,3])
layer=layers.Conv2D(4,kernel_size=3,strides=1,padding='SAME')
out=layer(x)#前向計算
print(out.shape)

TensorShape([2, 5, 5, 4])

在類 Conv2D 中,保存了卷積核張量𝑾和偏置𝒃,可以通過類成員 trainable_variables直接返回𝑾和𝒃的列表。例如:

# 返回所有待優化張量列表
print(layer.trainable_variables)

[<tf.Variable 'conv2d/kernel:0' shape=(3, 3, 3, 4) dtype=float32, numpy=
array([[[[ 0.13485974, -0.22861657, 0.01000655, 0.11988598],
 [ 0.12811887, 0.20501086, -0.29820845, -0.19579397],
 [ 0.00858489, -0.24469738, -0.08591779, -0.27885547]],<tf.Variable 'conv2d/bias:0' shape=(4,) dtype=float32, numpy=array([0., 0.,
0., 0.], dtype=float32)>]

通過調用 layer.trainable_variables 可以返回 Conv2D 類維護的𝑾和𝒃張量,這個類成員在獲取網絡層的待優化變量時非常有用。也可以直接調用類實例 layer.kernellayer.bias名訪問𝑾和𝒃張量。

10.4 LeNet-5 實戰

1990 年代,Yann LeCun 等人提出了用於手寫數字和機器打印字符圖片識別的神經網絡,被命名爲 LeNet-5。LeNet-5 的提出,使得卷積神經網絡在當時能夠成功被商用,廣泛應用在郵政編碼、支票號碼識別等任務中。
下圖 10.30 是 LeNet-5 的網絡結構圖,它接受32 × 32大小的數字、字符圖片,經過第一個卷積層得到 [b,28,28,6] 形狀的張量,經過一個向下採樣層,張量尺寸縮小到[b,14,14,6] ,經過第二個卷積層,得到[b,10,10,16]形狀的張量,同樣經過下采樣層,張量尺寸縮小到[b,5,5,16] ,在進入全連接層之前,先將張量打成[b,400] 的張量,送入輸出節點數分別爲 120、84 的 2 個全連接層,得到[b,84]的張量,最後通過 Gaussian connections 層。
在這裏插入圖片描述
現在看來,LeNet-5 網絡層數較少(2 個卷積層和 2 個全連接層),參數量較少,計算代價較低,尤其在現代 GPU 的加持下,數分鐘即可訓練好 LeNet-5 網絡。

我們在 LeNet-5 的基礎上進行了少許調整,使得它更容易在現代深度學習框架上實現。首先我們將輸入𝑿形狀由32 × 32調整爲28 × 28,然後將 2 個下采樣層實現爲最大池化層(降低特徵圖的高、寬,後續會介紹),最後利用全連接層替換掉 Gaussian connections層。下文統一稱修改的網絡也爲 LeNet-5 網絡。網絡結構圖如圖 10.31 所示。
在這裏插入圖片描述
我們基於 MNIST 手寫數字圖片數據集訓練 LeNet-5 網絡,並測試其最終準確度。前面已經介紹瞭如何在 TensorFlow 中加載 MNIST 數據集,此處不再贅述。

首先通過 Sequential 容器創建 LeNet-5,代碼如下:

from tensorflow.keras import Sequential

network=Sequential([
    layers.Conv2D(6,kernel_size=3,strides=1),#第一個卷積層,6個3*3卷積核
    layers.MaxPooling2D(pool_size=2,strides=2),# 寬和高各減半的池化層
    layers.ReLU(),#激活函數
    layers.Conv2D(16,kernel_size=3,strides=1),#第二個卷積層,16個3*3卷積核
    layers.MaxPooling2D(pool_size=2,strides=2),#寬高各減半的池化層
    layers.ReLU(),#激活函數
    layers.Flatten(),#打平層,方便全連接層處理

    layers.Dense(120,activation='relu'),#全連接層,120個節點
    layers.Dense(84,activation='relu'),#全連接層,84節點
    layers.Dense(10)#全連接層,10個節點
])
#build一次網絡模型,給輸入X的形狀,其中4爲隨意給的batchsz
network.build(input_shape=(4,28,28,1))
#統計網絡信息
print(network.summary())


Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            multiple                  60        
_________________________________________________________________
max_pooling2d (MaxPooling2D) multiple                  0         
_________________________________________________________________
re_lu (ReLU)                 multiple                  0         
_________________________________________________________________
conv2d_2 (Conv2D)            multiple                  880       
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 multiple                  0         
_________________________________________________________________
re_lu_1 (ReLU)               multiple                  0         
_________________________________________________________________
flatten (Flatten)            multiple                  0         
_________________________________________________________________
dense (Dense)                multiple                  48120     
_________________________________________________________________
dense_1 (Dense)              multiple                  10164     
_________________________________________________________________
dense_2 (Dense)              multiple                  850       
=================================================================
Total params: 60,074
Trainable params: 60,074
Non-trainable params: 0

通過summary()函數統計出每層的參數量,打印出網絡結構信息和每層參數量詳情,如表
10.3 所示,我們可以與全連接網絡的參數量表 10.1 進行比較。
在這裏插入圖片描述
可以看到,卷積層的參數量非常少,主要的參數量集中在全連接層。由於卷積層將輸入特徵維度降低很多,從而使得全連接層的參數量不至於過大,整個模型的參數量約 60K,而表 10.1 中的全連接網絡參數量達到了 34 萬個,因此通過卷積神經網絡可以顯著降低網絡參數量,同時增加網絡深度

在訓練階段,首先將數據集中 shape 爲[b,28,28]的輸入𝑿增加一個維度,調整 shape 爲[b,28,28,1]送入模型進行前向計算,得到輸出張量 output,shape 爲[b,10] 。我們新建交叉熵損失函數類(沒錯,損失函數也能使用類方式)用於處理分類任務,通過設定from_logits=True 標誌位將 softmax 激活函數實現在損失函數中,不需要手動添加損失函數,提升數值計算穩定性。代碼如下:

#導入誤差計算,優化器模塊
from tensorflow.keras import losses,optimizers
#創建損失函數的類,在實際計算時直接調用類實例即可
criteon=losses.CategoricalCrossentropy(from_logits=True)

訓練部分實現如下:

#構建梯度記錄環境
with tf.GradientTape() as tape:
    # 插入通道維度,=> [b,28,28,1]
    x=tf.expand_dims(x,axis=3)
    # 前向計算,獲得10類別的概率分佈,[b,784]
    out=network(x)
    # 真實標籤 one-hot編碼,[b]==>[b,10]
    y_onehot=tf.one_hot(y,depth=10)
    # 計算交叉熵損失函數,標量
    loss=criteon(y_onehot,out)

獲得損失值後,通過 TensorFlow 的梯度記錄器 tf.GradientTape()來計算損失函數 loss 對網絡參數 network.trainable_variables 之間的梯度,並通過 optimizer 對象自動更新網絡權值參數。代碼如下:

#自動計算梯度
    grads=tape.gradient(loss,network.trainable_varibales)
    # 自動跟新參數
    optimizers.apply_gradients(zip(grads,network.trainable_varibales))

重複上述步驟若干次後即可完成訓練工作。

在測試階段,由於不需要記錄梯度信息,代碼一般不需要寫在 with tf.GradientTape() as tape 環境中。前向計算得到的輸出經過 softmax 函數後,代表了網絡預測當前圖片輸入𝒙屬於類別𝑖的概率𝑃(𝒙標籤是𝑖|𝑥),𝑖 ∈[0, 9] 。通過 argmax 函數選取概率最大的元素所在的索引,作爲當前𝒙的預測類別,與真實標註𝑦比較,通過計算比較結果中間 True 的數量並求和來統計預測正確的樣本的個數,最後除以總樣本的個數,得出網絡的測試準確度。代碼如下:

 #記錄預測正確的數量,總樣本數量
    correct,total=0,0
    for x,y in db_test:#遍歷所有訓練樣本
        # 插入通道數,=>[b,28,28,1]
        x=tf.expand_dims(x,axis=3)
        # 前向計算,獲得10類別的預測分佈,[b,784]=>[b,10]
        out=network(x)
        # 真實的流程先經過softmax,再argmax
        # 但是由於softmax不改變元素的大小相對關係,故省去。
        pred=tf.argmax(out,asix=-1)
        y=tf.cast(y.tf.int64)
        # 統計預測正確數量
        correct+=float(tf.reduce_sum(tf.cast(tf.equal(pred,y),tf.float32)))
        # 統計預測樣本總數
        total+=x.shape[0]
    # 計算準確率
    print('test acc:',correct/total)

在數據集上面循環訓練 30 個 Epoch 後,網絡的訓練準確度達到了 98.1%,測試準確度也達到了 97.7%。對於非常簡單的手寫數字圖片識別任務,古老的 LeNet-5 網絡已經可以取得很好的效果,但是稍複雜一點的任務,比如彩色動物圖片識別,LeNet-5 性能就會急劇下降。

接下來,我們舉一個例子,總結以下 上述所有的知識點。

import tensorflow as tf
# 導入誤差計算,優化器模塊
from tensorflow.keras import Sequential, layers, losses, datasets, optimizers

# 獲取 GPU 設備列表
gpus = tf.config.experimental.list_physical_devices('GPU')
if gpus:
    try:
        # 設置 GPU 爲增長式佔用
        for gpu in gpus:
            tf.config.experimental.set_memory_growth(gpu, True)
    except RuntimeError as e:
        # 打印異常
        print(e)


def preprocess(x, y):
    # [b, 28, 28], [b]
    print(x.shape, y.shape)
    x = tf.cast(x, dtype=tf.float32) / 255.
    x = tf.reshape(x, [-1, 28, 28])
    y = tf.cast(y, dtype=tf.int64)

    return x, y


def load_dataset():
    (x, y), (x_test, y_test) = datasets.mnist.load_data()

    batchsz = 128
    train_db = tf.data.Dataset.from_tensor_slices((x, y))
    train_db = train_db.shuffle(1000).batch(batchsz).map(preprocess)

    test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test))
    test_db = test_db.shuffle(1000).batch(batchsz).map(preprocess)

    return train_db, test_db


def build_network():
    network = Sequential([  # 網絡容器
        layers.Conv2D(6, kernel_size=3, strides=1),  # 第一個卷積層, 6 個 3x3 卷積核
        layers.MaxPooling2D(pool_size=2, strides=2),  # 高寬各減半的池化層
        layers.ReLU(),  # 激活函數
        layers.Conv2D(16, kernel_size=3, strides=1),  # 第二個卷積層, 16 個 3x3 卷積核
        layers.MaxPooling2D(pool_size=2, strides=2),  # 高寬各減半的池化層
        layers.ReLU(),  # 激活函數
        layers.Flatten(),  # 打平層,方便全連接層處理
        layers.Dense(120, activation='relu'),  # 全連接層, 120 個節點
        layers.Dense(84, activation='relu'),  # 全連接層, 84 節點
        layers.Dense(10)  # 全連接層, 10 個節點
    ])
    # build 一次網絡模型,給輸入 X 的形狀,其中 4 爲隨意給的 batchsz
    network.build(input_shape=(4, 28, 28, 1))
    # 統計網絡信息
    network.summary()
    return network


def train(train_db, network, criteon, optimizer, epoch_num):
    for epoch in range(epoch_num):
        correct, total, loss = 0, 0, 0
        for step, (x, y) in enumerate(train_db):
            # 構建梯度記錄環境
            with tf.GradientTape() as tape:
                # 插入通道維度, =>[b,28,28,1]
                x = tf.expand_dims(x, axis=3)
                # 前向計算,獲得 10 類別的概率分佈, [b, 784] => [b, 10]
                out = network(x)
                pred = tf.argmax(out, axis=-1)
                # 真實標籤 one-hot 編碼, [b] => [b, 10]
                y_onehot = tf.one_hot(y, depth=10)
                # 計算交叉熵損失函數,標量
                loss += criteon(y_onehot, out)
                # 統計預測正確數量
                correct += float(tf.reduce_sum(tf.cast(tf.equal(pred, y), tf.float32)))
                # 統計預測樣本總數
                total += x.shape[0]


            # 自動計算梯度
            grads = tape.gradient(loss, network.trainable_variables)
            # 自動更新參數
            optimizer.apply_gradients(zip(grads, network.trainable_variables))

        print(epoch, 'loss=', float(loss), 'acc=', correct / total)

    return network


def predict(test_db, network):
    # 記錄預測正確的數量,總樣本數量
    correct, total = 0, 0
    for x, y in test_db:  # 遍歷所有訓練集樣本
        # 插入通道維度, =>[b,28,28,1]
        x = tf.expand_dims(x, axis=3)
        # 前向計算,獲得 10 類別的預測分佈, [b, 784] => [b, 10]
        out = network(x)
        # 真實的流程時先經過 softmax,再 argmax
        # 但是由於 softmax 不改變元素的大小相對關係,故省去
        pred = tf.argmax(out, axis=-1)
        y = tf.cast(y, tf.int64)
        # 統計預測正確數量
        correct += float(tf.reduce_sum(tf.cast(tf.equal(pred, y), tf.float32)))
        # 統計預測樣本總數
        total += x.shape[0]

    # 計算準確率
    print('test acc:', correct / total)


def main():
    epoch_num = 30

    train_db, test_db = load_dataset()
    network = build_network()
    # 創建損失函數的類,在實際計算時直接調用類實例即可
    criteon = losses.CategoricalCrossentropy(from_logits=True)
    optimizer = optimizers.RMSprop(0.001)
    network = train(train_db, network, criteon, optimizer, epoch_num)
    predict(test_db, network)


if __name__ == '__main__':
    main()

輸出結果如下,可見訓練效果還不錯。

(None, 28, 28) (None,)
(None, 28, 28) (None,)
Model: "sequential_4"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_8 (Conv2D)            multiple                  60        
_________________________________________________________________
max_pooling2d_8 (MaxPooling2 multiple                  0         
_________________________________________________________________
re_lu_8 (ReLU)               multiple                  0         
_________________________________________________________________
conv2d_9 (Conv2D)            multiple                  880       
_________________________________________________________________
max_pooling2d_9 (MaxPooling2 multiple                  0         
_________________________________________________________________
re_lu_9 (ReLU)               multiple                  0         
_________________________________________________________________
flatten_4 (Flatten)          multiple                  0         
_________________________________________________________________
dense_12 (Dense)             multiple                  48120     
_________________________________________________________________
dense_13 (Dense)             multiple                  10164     
_________________________________________________________________
dense_14 (Dense)             multiple                  850       
=================================================================
Total params: 60,074
Trainable params: 60,074
Non-trainable params: 0
_________________________________________________________________
0 loss= 135.04367065429688 acc= 0.91455
1 loss= 40.35447692871094 acc= 0.9736333333333334
2 loss= 28.032947540283203 acc= 0.9815166666666667
3 loss= 21.78510093688965 acc= 0.9854833333333334
4 loss= 17.67403221130371 acc= 0.9877666666666667
5 loss= 14.373819351196289 acc= 0.9902333333333333
6 loss= 11.992131233215332 acc= 0.9917166666666667
7 loss= 10.650928497314453 acc= 0.9926333333333334
8 loss= 8.893635749816895 acc= 0.99365
9 loss= 7.410651206970215 acc= 0.9949666666666667
10 loss= 6.5429840087890625 acc= 0.9955
11 loss= 5.767464637756348 acc= 0.9957833333333334
12 loss= 5.1342244148254395 acc= 0.9964833333333334
13 loss= 4.76762056350708 acc= 0.99665
14 loss= 4.071495056152344 acc= 0.9971666666666666
15 loss= 3.3102405071258545 acc= 0.99785
16 loss= 3.527087450027466 acc= 0.9975333333333334
17 loss= 2.926004409790039 acc= 0.99795
18 loss= 2.8832032680511475 acc= 0.99815
19 loss= 2.596499443054199 acc= 0.99825
20 loss= 2.322714328765869 acc= 0.9984166666666666
21 loss= 2.272839069366455 acc= 0.9983833333333333
22 loss= 2.3042337894439697 acc= 0.99825
23 loss= 2.036952018737793 acc= 0.9984666666666666
24 loss= 1.6261500120162964 acc= 0.99885
25 loss= 1.768705129623413 acc= 0.9987833333333334
26 loss= 2.0271694660186768 acc= 0.99875
27 loss= 1.9127095937728882 acc= 0.9985
28 loss= 1.650101900100708 acc= 0.9988833333333333
29 loss= 1.422216534614563 acc= 0.9989666666666667
test acc: 0.9877

10.5 表示學習

我們已經介紹完卷積神經網絡層的工作原理與實現方法,複雜的卷積神經網絡模型也是基於卷積層的堆疊構成的。在過去的一段時間內,研究人員發現網絡層數越深,模型的表達能力越強,也就越有可能取得更好的性能。那麼層層堆疊的卷積網絡到底學到了什麼特徵,使得層數越深,網絡的表達能力越強呢?

2014 年,Matthew D. Zeiler 等人嘗試利用可視化的方法去理解卷積神經網絡到底學到了什麼。通過將每層的特徵圖利用“反捲積”網絡(Deconvolutional Network)映射回輸入圖片,即可查看學到的特徵分佈,如圖 10.32 所示。

可以觀察到,第二層的特徵對應到邊、角、色彩等底層圖像提取;第三層開始捕獲到紋理這些中層特徵;第四、五層呈現了物體的部分特徵,如小狗的臉部、鳥類的腳部等高層特徵。通過這些可視化的手段,我們可以一定程度上感受卷積神經網絡的特徵學習過程。
在這裏插入圖片描述
在這裏插入圖片描述
圖片數據的識別過程一般認爲也是表示學習(Representation Learning)的過程,從接受到的原始像素特徵開始,逐漸提取邊緣、角點等底層特徵,再到紋理等中層特徵,再到頭部、物體部件等高層特徵,最後的網絡層基於這些學習到的抽象特徵表示(Representation)做分類邏輯的學習。學習到的特徵越高層、越準確,就越有利於分類器的分類,從而獲得較好的性能。從表示學習的角度來理解,卷積神經網絡通過層層堆疊來逐層提取特徵,網絡訓練的過程可以看成特徵的學習過程,基於學習到的高層抽象特徵可以方便地進行分類任務。

應用表示學習的思想,訓練好的卷積神經網絡往往能夠學習到較好的特徵,這種特徵的提取方法一般是通用的。比如在貓、狗任務上學習到頭、腳、身軀等特徵的表示,在其它動物上也能夠一定程度上使用。基於這種思想,可以將在任務 A 上訓練好的深層神經網絡的前面數個特徵提取層遷移到任務 B 上,只需要訓練任務 B 的分類邏輯(表現爲網絡的最末數層),即可取得非常好的效果,這種方式是遷移學習的一種,從神經網絡角度也稱爲網絡微調(Fine-tuning)。

10.6 梯度傳播

在完成手寫數字圖片識別實戰後,我們對卷積神經網絡的使用有了初步的瞭解。現在我們來解決一個關鍵問題,卷積層通過移動感受野的方式實現離散卷積操作,那麼它的梯度傳播是怎麼進行的呢?

考慮一簡單的情形,輸入爲3 × 3的單通道矩陣,與一個2 × 2的卷積核,進行卷積運算,輸出結果打平後直接與虛構的標註計算誤差,如圖 10.33 所示。我們來討論這種情況下的梯度更新方式。
在這裏插入圖片描述
首先推導出輸出張量𝑶的表達形式:
在這裏插入圖片描述
W00W_{00}的梯度計算爲例,通過鏈式法則分解:
Lw00=i{00,01,10,11}Loioiw00\frac{\partial \mathcal{L}}{\partial w_{00}}=\sum_{i \in\{00,01,10,11\}} \frac{\partial \mathcal{L}}{\partial o_{i}} \frac{\partial o_{i}}{\partial w_{00}}
其中LOi\frac{\partial \mathcal{L}}{\partial O_{i}}可直接由誤差函數推導出來,我們直接來考慮Oiwi\frac{\partial O_{i}}{\partial w_{i}},例如:

o00w00=(x00w00+x01w01+x10w10+x11w11+b)w00=x00\frac{\partial o_{00}}{\partial w_{00}}=\frac{\partial\left(x_{00} w_{00}+x_{01} w_{01}+x_{10} w_{10}+x_{11} w_{11}+b\right)}{w_{00}}=x_{00}
同樣的方法,可以推導出:
在這裏插入圖片描述
可以觀察到,通過循環移動感受野的方式並沒有改變網絡層可導性,同時梯度的推導也並不複雜,只是當網絡層數增大以後,人工梯度推導將變得十分的繁瑣。不過不需要擔心,深度學習框架可以幫我們自動完成所有參數的梯度計算與更新,我們只需要設計好網絡結構即可。

10.7 池化層

在卷積層中,可以通過調節步長參數𝑠實現特徵圖的高寬成倍縮小,從而降低了網絡的參數量。實際上,除了通過設置步長,還有一種專門的網絡層可以實現尺寸縮減功能,它就是這裏要介紹的池化層(Pooling Layer)。
在這裏插入圖片描述

池化層同樣基於局部相關性的思想,通過從局部相關的一組元素中進行採樣或信息聚合,從而得到新的元素值。特別地,最大池化層(Max Pooling)從局部相關元素集中選取最大的一個元素值,平均池化層(Average Pooling)從局部相關元素集中計算平均值並返回。以 5×5 輸入𝑿的最大池化層爲例,考慮池化感受野窗口大小𝑘 = 2,步長𝑠 =1 的情況,如下圖 10.34 所示。綠色虛線方框代表第一個感受野的位置,感受野元素集合爲
{1,1,1,2}\{1,-1,-1,-2\}
在最大池化採樣的方法下,通過
x=max({1,1,1,2})=1x^{\prime}=\max (\{1,-1,-1,-2\})=1
計算出當前位置的輸出值爲 1,並寫入對應位置。
若採用的是平均池化操作,則此時的輸出值應爲
x=avg({1,1,1,2})=0.75x^{\prime}=\operatorname{avg}(\{1,-1,-1,-2\})=-0.75
計算完當前位置的感受野後,與卷積層的計算步驟類似,將感受野按着步長向右移動若干單位,此時的輸出
x=max(1,0,2,2)=2x^{\prime}=\max (-1,0,-2,2)=2
同樣的方法,逐漸移動感受野窗口至最右邊,計算出輸出x=max(2,0,3,1)=1x^{\prime}=\max (2,0,3,1)=1,此時窗口已經到達輸入邊緣,按照卷積層同樣的方式,感受野窗口向下移動一個步長,並回到行首,如圖 10.35。
在這裏插入圖片描述
循環往復,直至最下方、最右邊,獲得最大池化層的輸出,長寬爲 4×4 ,略小於輸入𝑿的高寬,如圖 10.36。
在這裏插入圖片描述
由於池化層沒有需要學習的參數,計算簡單,並且可以有效減低特徵圖的尺寸,非常適合圖片這種類型的數據,在計算機視覺相關任務中得到了廣泛的應用。

通過精心設計池化層感受野的高寬𝑘和步長𝑠參數,可以實現各種降維運算。比如,一種常用的池化層設定是感受野大小𝑘 = 2,步長𝑠 = 2,這樣可以實現輸出只有輸入高寬一半的目的。如下圖 10.37、圖 10.38 所示,感受野𝑘 = 3,步長𝑠 = 2,輸入𝑿高寬爲 × ,輸出𝑶高寬只有2 × 2。
在這裏插入圖片描述
在這裏插入圖片描述

10.8 BatchNorm 層

卷積神經網絡的出現,網絡參數量大大減低,使得幾十層的深層網絡成爲可能。然而,在殘差網絡出現之前,網絡的加深使得網絡訓練變得非常不穩定,甚至出現網絡長時間不更新甚至不收斂的現象,同時網絡對超參數比較敏感,超參數的微量擾動也會導致網絡的訓練軌跡完全改變。

2015 年,Google 研究人員 Sergey Ioffe 等提出了一種參數標準化(Normalize)的手段,並基於參數標準化設計了 Batch Nomalization(簡寫爲 BatchNorm,或 BN)層。BN 層的提出,使得網絡的超參數的設定更加自由,比如更大的學習率、更隨意的網絡初始化等,同時網絡的收斂速度更快,性能也更好。BN 層提出後便廣泛地應用在各種深度網絡模型上,卷積層、BN 層、ReLU 層、池化層一度成爲網絡模型的標配單元塊,通過堆疊 ConvBN-ReLU-Pooling 方式往往可以獲得不錯的模型性能。

首先我們來探索,爲什麼需要對網絡中的數據進行標準化操作?這個問題很難從理論層面解釋透徹,即使是 BN 層的作者給出的解釋也未必讓所有人信服。與其糾結其緣由,不如通過具體問題來感受數據標準化後的好處。

考慮 Sigmoid 激活函數和它的梯度分佈,如下圖 10.39 所示,Sigmoid 函數在𝑥 ∈[−2 ,2] 區間的導數在 [0.1, 0.25]區間分佈;當𝑥 > 2 或𝑥 < −2時,Sigmoid 函數的導數變得很小,逼近於 0,從而容易出現梯度彌散現象。爲了避免因爲輸入較大或者較小而導致Sigmoid 函數出現梯度彌散現象,將函數輸入𝑥標準化映射到 0 附近的一段較小區間將變得非常重要,可以從圖 10.39 看到,通過標準化重映射後,值被映射在 0 附近,此處的導數值不至於過小,從而不容易出現梯度彌散現象。這是使用標準化手段受益的一個例子。
在這裏插入圖片描述
我們再看另一個例子。考慮 2 個輸入節點的線性模型,如圖 10.40(a)所示:
L=a=x1w1+x2w2+b\mathcal{L}=a=x_{1} w_{1}+x_{2} w_{2}+b
討論如下 2 種輸入分佈下的優化問題:
❑ 輸入𝑥1 ∈[1,10] ,𝑥2 ∈[1,10]
❑ 輸入𝑥1 ∈[1,10] ,𝑥2 ∈[100,1000]
由於模型相對簡單,可以繪製出 2 種𝑥1、𝑥2下,函數的損失等高線圖,圖 10.40(b)是𝑥1 ∈
1,10] 、𝑥2 ∈[100,1000]時的某條優化軌跡線示意,圖 10.40©是𝑥1 ∈[1,10] 、𝑥2 ∈[1,10]時的某條優化軌跡線示意,圖中的圓環中心即爲全局極值點。
在這裏插入圖片描述
考慮到
在這裏插入圖片描述
當𝑥1、𝑥2輸入分佈相近時,Lw1\frac{\partial \mathcal{L}}{\partial w_{1}}Lw2\frac{\partial \mathcal{L}}{\partial w_{2}}偏導數值相當,函數的優化軌跡如圖 10.40©所示;
當𝑥1、𝑥2輸入分佈差距較大時,比如𝑥1 ≪ 𝑥2,則
Lw1Lw2\frac{\partial \mathcal{L}}{\partial w_{1}} \ll \frac{\partial \mathcal{L}}{\partial w_{2}}
損失函數等勢線在w2w_{2}軸更加陡峭,某條可能的優化軌跡如圖 10.40(b)所示。對比 2 條優化
線可以觀察到,𝑥1、𝑥2分佈相近時圖 10.40©中收斂更加快速,優化軌跡更理想。

通過上述的 2 個例子,我們能夠經驗性歸納出:網絡層輸入𝑥分佈相近,並且分佈在較小範圍內時(如 0 附近),更有利於函數的優化。那麼如何保證輸入𝑥的分佈相近呢?數據標準化可以實現此目的,通過數據標準化操作可以將數據𝑥映射到𝑥̂:
x^=xμrσr2+ϵ\hat{x}=\frac{x-\mu_{r}}{\sqrt{\sigma_{r}^{2}+\epsilon}}
其中μr\mu_{r}、𝜎𝑟2來自統計的所有數據的均值和方差,𝜖是爲防止出現除 0 錯誤而設置的較小數字,如 1e − 8。

在基於 Batch 的訓練階段,如何獲取每個網絡層所有輸入的統計數據μr\mu_{r}σr2\sigma_{r}^{2}?考慮 Batch 內部的均值μB\mu_{B}和方差σB2\sigma_{B}^{2}
μB=1mi=1mxi\mu_{B}=\frac{1}{m} \sum_{i=1}^{m} x_{i}
σB2=1mi=1m(xiμB)2\sigma_{B}^{2}=\frac{1}{m} \sum_{i=1}^{m}\left(x_{i}-\mu_{B}\right)^{2}
可以視爲近似於μr\mu_{r}σr2\sigma_{r}^{2},其中𝑚爲 Batch 樣本數。因此,在訓練階段,通過
x^train =xtrain μBσB2+ϵ\hat{x}_{\text {train }}=\frac{x_{\text {train }}-\mu_{B}}{\sqrt{\sigma_{B}^{2}+\epsilon}}
標準化輸入,並記錄每個 Batch 的統計數據μB\mu_{B}和方差σB2\sigma_{B}^{2},用於統計真實的全局μr\mu_{r}和方差σr2\sigma_{r}^{2}

在測試階段,根據記錄的每個 Batch 的μB\mu_{B}σB2\sigma_{B}^{2}估計出所有訓練數據的μr\mu_{r}σr2\sigma_{r}^{2},按着
x^test =xtest μrσr2+ϵ\hat{x}_{\text {test }}=\frac{x_{\text {test }}-\mu_{r}}{\sqrt{\sigma_{r}^{2}+\epsilon}}
將每層的輸入標準化。

上述的標準化運算並沒有引入額外的待優化變量,μr\mu_{r}σr2\sigma_{r}^{2}μB\mu_{B}σB2\sigma_{B}^{2}均由統計得到,不需要參與梯度更新。實際上,爲了提高 BN 層的表達能力,BN 層作者引入了“scale and shift”技巧,將𝑥̂變量再次映射變換:
x~=x^γ+β\tilde{x}=\hat{x} \cdot \gamma+\beta

其中𝛾參數實現對標準化後的𝑥̂再次進行縮放,𝛽參數實現對標準化的𝑥̂進行平移,不同的是,𝛾、𝛽參數均由反向傳播算法自動優化,實現網絡層“按需”縮放平移數據的分佈的目的。

下面我們來學習在 TensorFlow 中實現的 BN 層的方法。

10.8.1 前向傳播

我們將 BN 層的輸入記爲𝑥,輸出記爲𝑥̃。分訓練階段和測試階段來討論前向傳播過程。

訓練階段:首先計算當前 Batch 的μB\mu_{B}σB2\sigma_{B}^{2},根據
x~train =xtrain μBσB2+ϵγ+β\tilde{x}_{\text {train }}=\frac{x_{\text {train }}-\mu_{B}}{\sqrt{\sigma_{B}^{2}+\epsilon}} \cdot \gamma+\beta
計算 BN 層的輸出。

同時按照
μr momentum μr+(1 momentum )μBσr2 momentum σr2+(1 momentum )σB2\begin{array}{c} \mu_{r} \leftarrow \text { momentum } \cdot \mu_{r}+(1-\text { momentum }) \cdot \mu_{B} \\ \sigma_{r}^{2} \leftarrow \text { momentum } \cdot \sigma_{r}^{2}+(1-\text { momentum }) \cdot \sigma_{B}^{2} \end{array}
迭代更新全局訓練數據的統計值μr\mu_{r}σr2\sigma_{r}^{2},其中 momentum 是需要設置一個超參數,用於平衡μr\mu_{r}σr2\sigma_{r}^{2}的更新幅度:當momentum =0 時,μr\mu_{r}σr2\sigma_{r}^{2}直接被設置爲最新一個 Batch 的μB\mu_{B}σB2\sigma_{B}^{2};當momentum =1時,μr\mu_{r}σr2\sigma_{r}^{2}保持不變,忽略最新一個 Batch 的μB\mu_{B}σB2\sigma_{B}^{2},在TensorFlow 中,momentum 默認設置爲 0.99。

測試階段:BN 層根據
x~test =xtest μrσr2+ϵγ+β\tilde{x}_{\text {test }}=\frac{x_{\text {test }}-\mu_{r}}{\sqrt{\sigma_{r}^{2}+\epsilon}} * \gamma+\beta
計算輸出𝑥̃𝑡𝑒𝑠𝑡,其中μr,σr2,γ,β\mu_{r}, \sigma_{r}^{2}, \gamma, \beta均來自訓練階段統計或優化的結果,在測試階段直接使用,並不會更新這些參數。

10.8.2 反向更新

在訓練模式下的反向更新階段,反向傳播算法根據損失ℒ求解梯度Lγ\frac{\partial \mathcal{L}}{\partial \gamma}Lβ\frac{\partial \mathcal{L}}{\partial \beta},並按着梯度更新法則自動優化𝛾、𝛽參數。

需要注意的是,對於 2D 特徵圖輸入𝑿: [b,ℎ, 𝑤, 𝑐],BN 層並不是計算每個點的μB,σB2\mu_{B}, \sigma_{B}^{2},而是在通道軸𝑐上面統計每個通道上面所有數據的μB,σB2\mu_{B}, \sigma_{B}^{2},因此μB,σB2\mu_{B}, \sigma_{B}^{2}是每個通道上所有其它維度的均值和方差。以 shape 爲 [100,32, 32, 3 ]的輸入爲例,在通道軸𝑐上面的均值計算如下:

# 構造輸入
x=tf.random.normal([100,32,32,3])
#將其他維度合併,僅保留通道維度
x=tf.reshape(x,[-1,3])
#計算其他維度的均值
ub=tf.reduce_mean(x,axis=0)
print(ub)

<tf.Tensor: id=62, shape=(3,), dtype=float32, numpy=array([-0.00222636, -
0.00049868, -0.00180082], dtype=float32)>

數據有𝑐個通道數,則有𝑐個均值產生。

除了在𝑐軸上面統計數據μB,σB2\mu_{B}, \sigma_{B}^{2}的方式,我們也很容易將其推廣至其它維度計算均值的方式,如圖 10.41 所示:

❑ Layer Norm:統計每個樣本的所有特徵的均值和方差
❑ Instance Norm:統計每個樣本的每個通道上特徵的均值和方差
❑ Group Norm:將𝑐通道分成若干組,統計每個樣本的通道組內的特徵均值和方差

上面提到的 Normalization 方法均由獨立的幾篇論文提出,並在某些應用上驗證了其相當或者優於 BatchNorm 算法的效果。由此可見,深度學習算法研究並非難於上青天,只要多思考、多鍛鍊算法工程能力,人人都有機會發表創新性成果。
在這裏插入圖片描述

10.8.3 BN 層實現

在 TensorFlow 中,通過 layers.BatchNormalization()類可以非常方便地實現 BN 層:

# 創建 BN 層
layer=layers.BatchNormalization()

與全連接層、卷積層不同,BN 層的訓練階段和測試階段的行爲不同,需要通過設置training 標誌位來區分訓練模式還是測試模式。

以 LeNet-5 的網絡模型爲例,在卷積層後添加 BN 層,代碼如下:

network=Sequential([
    layers.Conv2D(6,kernel_size=3,strides=1),
    #插入BN層
    layers.BatchNormalization(),
    layers.MaxPooling2D(pool_size=2,strides=2),
    layers.ReLU(),

    layers.Conv2D(16,kernel_size=3,strides=1),
    #插入BN層
    layers.BatchNormalization(),
    layers.MaxPooling2D(pool_size=2,strides=2),
    layers.ReLU(),

    layers.Flatten(),

    layer.Dense(120,activation='relu'),
    #此處也可以插入BN層
    layers.Dense(84,activation='relu'),
    #此處也可以插入BN層
    layers.Dense(10)
])

在訓練階段,需要設置網絡的參數 training=True 以區分 BN 層是訓練還是測試模型,代碼如下:

with tf.GradientTape() as tape:
    # 插入通道維度
    x=tf.expand_dims(x,axis=3)
    # 前向計算,設置計算模式:[b,784]=>[b,10]
    out=network(x,training=True)

在測試階段,需要設置 training=False,避免 BN 層採用錯誤的行爲,代碼如下:

with tf.GradientTape() as tape:
    # 插入通道維度
    x=tf.expand_dims(x,axis=3)
    # 前向計算,設置計算模式:[b,784]=>[b,10]
    out=network(x,training=False)

10.9 經典卷積網絡

自 2012 年 AlexNet的提出以來,各種各樣的深度卷積神經網絡模型相繼被提出,其中比較有代表性的有 VGG 系列 ,GoogLeNet 系列,ResNet 系列 ,DenseNet系列等,他們的網絡層數整體趨勢逐漸增多。

以網絡模型在 ILSVRC 挑戰賽 ImageNet數據集上面的分類性能表現爲例,如圖 10.42 所示,在 AlexNet 出現之前的網絡模型都是淺層的神經網絡,Top-5 錯誤率均在 25%以上,AlexNet 8 層的深層神經網絡將 Top-5 錯誤率降低至 16.4%,性能提升巨大,後續的 VGG、GoogleNet 模型繼續將錯誤率降低至6.7%;ResNet 的出現首次將網絡層數提升至 152 層,錯誤率也降低至 3.57%。
在這裏插入圖片描述
本節將重點介紹這幾種網絡模型的特點。

10.9.1 AlexNet

2012 年,ILSVRC12 挑戰賽 ImageNet 數據集分類任務的冠軍 Alex Krizhevsky 提出了 8層的深度神經網絡模型 AlexNet,它接收輸入爲224 × 224 大小的彩色圖片數據,經過五個卷積層和三個全連接層後得到樣本屬於 1000 個類別的概率分佈。
爲了降低特徵圖的維度,AlexNet 在第 1、2、5 個卷積層後添加了 Max Pooling 層,如圖 10.43 所示,網絡的參數量達到了 6000 萬個。爲了能夠在當時的顯卡設備 NVIDIA GTX 580(3GB 顯存)上訓練模型,Alex Krizhevsky 將卷積層、前 2 個全連接層等拆開在兩塊顯卡上面分別訓練,最後一層合併到一張顯卡上面,進行反向傳播更新。AlexNet 在 ImageNet 取得了 15.3%的 Top-5 錯誤率,比第二名在錯誤率上降低了 10.9%。

AlexNet 的創新之處在於:
❑ 層數達到了較深的 8 層。

❑ 採用了 ReLU 激活函數,過去的神經網絡大多采用 Sigmoid 激活函數,計算相對複雜,容易出現梯度彌散現象。

❑ 引入 Dropout 層。Dropout 提高了模型的泛化能力,防止過擬合。
在這裏插入圖片描述

10.9.2 VGG 系列

AlexNet 模型的優越性能啓發了業界朝着更深層的網絡模型方向研究。2014 年,ILSVRC14 挑戰賽 ImageNet 分類任務的亞軍牛津大學 VGG 實驗室提出了 VGG11、VGG13、VGG16、VGG19 等一系列的網絡模型(圖 10.45),並將網絡深度最高提升至 19層 。
在這裏插入圖片描述
以 VGG16 爲例,它接受224 × 224 大小的彩色圖片數據,經過 2 個 Conv-ConvPooling 單元,和 3 個 Conv-Conv-Conv-Pooling 單元的疊,最後通過 3 層全連接層輸出當前圖片分別屬於 1000 類別的概率分佈,如圖 10.44 所示。VGG16 在 ImageNet 取得了7.4%的 Top-5 錯誤率,比 AlexNet 在錯誤率上降低了 7.9%。
在這裏插入圖片描述
VGG 系列網絡的創新之處在於:
❑ 層數提升至 19 層。

❑ 全部採用更小的3 × 3卷積核,相對於 AlexNet 中 7×7 的卷積核,參數量更少,計算代價更低。

❑ 採用更小的池化層2 × 2窗口和步長𝑠 = 2,而 AlexNet 中是步長𝑠 = 2、3 × 3的池化窗口。

10.9.3 GoogLeNet

3 × 3的卷積核參數量更少,計算代價更低,同時在性能表現上甚至更優越,因此業界開始探索卷積核最小的情況: 1×1卷積核。如下圖 10.46 所示,輸入爲 3 通道的 5×5 圖片,與單個 1×1 的卷積核進行卷積運算,每個通道的數據與對應通道的卷積核運算,得到3 個通道的中間矩陣,對應位置相加得到最終的輸出張量。
對於輸入 shape 爲[b, ℎ,𝑤 ,CinC_{in}] ,1×1 卷積層的輸出爲[b, ℎ,𝑤 ,CoutC_{out}] ,其中CinC_{in}爲輸入數據的通道數,CoutC_{out}爲輸出數據的通道數,也是 1×1 卷積核的數量。 1×1卷積核的一個特別之處在於,它可以不改變特徵圖的寬高,而只對通道數𝑐進行變換
在這裏插入圖片描述
2014 年,ILSVRC14 挑戰賽的冠軍 Google 提出了大量採用3 × 3和 1×1 卷積核的網絡模型:GoogLeNet,網絡層數達到了 22 層。雖然 GoogLeNet 的層數遠大於 AlexNet,但是它的參數量卻只有 AlexNet 1/12,同時性能也遠好於 AlexNet。在 ImageNet 數據集分類任務上,GoogLeNet 取得了 6.7%的 Top-5 錯誤率,比 VGG16 在錯誤率上降低了 0.7%。

GoogLeNet 網絡採用模塊化設計的思想,通過大量堆疊 Inception 模塊,形成了複雜的網絡結構。如下圖 10.47 所示,Inception 模塊的輸入爲𝑿,通過 4 個子網絡得到 4 個網絡輸出,在通道軸上面進行拼接合並,形成 Inception 模塊的輸出。這 4 個子網絡是:
❑ 1×1 卷積層。
❑ 1×1 卷積層,再通過一個3 × 3卷積層。
❑ 1×1 卷積層,再通過一個 5×5 卷積層。
❑ 3 × 3最大池化層,再通過 1×1 卷積層。
在這裏插入圖片描述
GoogLeNet 的網絡結構如圖 10.48 所示,其中紅色框中的網絡結構即爲圖 10.47的網絡結構。
在這裏插入圖片描述

10.10 CIFAR10 與 VGG13 實戰

MNIST 是機器學習最常用的數據集之一,但由於手寫數字圖片非常簡單,並且MNIST 數據集只保存了圖片灰度信息,並不適合輸入設計爲 RGB 三通道的網絡模型。本節將介紹另一個經典的圖片分類數據集:CIFAR10

CIFAR10 數據集由加拿大 Canadian Institute For Advanced Research 發佈,它包含了飛機、汽車、鳥、貓等共 10 大類物體的彩色圖片,每個種類收集了 6000 張32 × 32大小圖片,共 6 萬張圖片。其中 5 萬張作爲訓練數據集,1 萬張作爲測試數據集。每個種類樣片如圖 10.49 所示。
在這裏插入圖片描述
在TensorFlow中,同樣地,不需要手動下載、解析和加載 CIFAR10 數據集,通過datasets.cifar10.load_data()函數就可以直接加載切割好的訓練集和測試集。例如:

def preprocess(x, y):
    # [0~1]
    x = 2 * tf.cast(x, dtype=tf.float32) / 255. - 1
    y = tf.cast(y, dtype=tf.int32)
    return x, y


(x,y),(x_test,y_test)=datasets.cifar10.load_data()
#刪除y的一個維度,[b,1]=>[b]
y=tf.squeeze(y,axis=1)
y_test=tf.squeeze(y_test,axis=1)
print(x.shape,y.shape,x_test.shape,y_test.shape)
#構建訓練集對象,隨機打亂,預處理,批量化
train_db=tf.data.Dataset.from_tensor_slices((x,y))
train_db=train_db.shuffle(1000).batch(128).map(preprocess)
#構建測試集對象,預處理,批量化
test_db=tf.data.Dataset.from_tensor_slices((x_test,y_test))
test_db=test_db.batch(128).map(preprocess)
# 從訓練集中採樣一個Batch,並觀察
sample=next(iter(train_db))
print('sample:',sample[0].shape,sample[1].shape,tf.reduce_min(sample[0]),tf.reduce_max(sample[0]))

(50000, 32, 32, 3) (50000,) (10000, 32, 32, 3) (10000,)
sample: (128, 32, 32, 3) (128,) tf.Tensor(-1.0, shape=(), dtype=float32) tf.Tensor(1.0, shape=(), dtype=float32)

TensorFlow 會自動將數據集下載在 C:\Users\用戶名.keras\datasets 路徑下,用戶可以查看,也可手動刪除不需要的數據集緩存。上述代碼運行後,得到訓練集的𝑿和𝒚形狀爲:(50000, 32, 32, 3)和(50000),測試集的𝑿和𝒚形狀爲(10000, 32, 32, 3)和(10000),分別代表了圖片大小爲32 × 32,彩色圖片,訓練集樣本數爲 50000,測試集樣本數爲 10000。

CIFAR10 圖片識別任務並不簡單,這主要是由於 CIFAR10 的圖片內容需要大量細節才能呈現,而保存的圖片分辨率僅有32 × 32,使得部分主體信息較爲模糊,甚至人眼都很難分辨。淺層的神經網絡表達能力有限,很難訓練優化到較好的性能,本節將基於表達能力更強的 VGG13 網絡,根據我們的數據集特點修改部分網絡結構,完成 CIFAR10 圖片識別。修改如下:

❑ 將網絡輸入調整爲32 × 32。原網絡輸入爲224 × 224 ,導致全連接層輸入特徵維度過大,網絡參數量過大。
❑ 3 個全連接層的維度調整爲 [256,64,10] ,滿足 10 分類任務的設定。

圖 10.50 是調整後的 VGG13 網絡結構,我們統稱之爲 VGG13 網絡模型
在這裏插入圖片描述
我們將網絡實現爲 2 個子網絡:卷積子網絡和全連接子網絡。卷積子網絡由 5 個子模塊構成,每個子模塊包含了 Conv-Conv-MaxPooling 單元結構,代碼如下:

# 先創建包含多網絡層的列表
conv_layers=[
    #Conv-Conv-Pooling單元1
    #64個3*3卷積核,輸入輸出同大小
    layers.Conv2D(64,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.Conv2D(64,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    #高寬減半
    layers.Maxpool2D(pool_size=[2,2],strides=2,padding='same'),

    #Conv-Conv-Pooling單元2,輸出通道提升至128,高寬大小減半
    layers.Conv2D(128,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.Conv2D(128,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    #高寬減半
    layers.Maxpool2D(pool_size=[2,2],strides=2,padding='same'),

    # Conv-Conv-Pooling單元3,輸出通道提升至256,高寬大小減半
    layers.Conv2D(256, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
    layers.Conv2D(256, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
    # 高寬減半
    layers.Maxpool2D(pool_size=[2, 2], strides=2, padding='same'),

    # Conv-Conv-Pooling單元4,輸出通道提升至512,高寬大小減半
    layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
    layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
    # 高寬減半
    layers.Maxpool2D(pool_size=[2, 2], strides=2, padding='same'),

    # Conv-Conv-Pooling單元5,,輸出通道提升至512,高寬大小減半
    layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
    layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
    # 高寬減半
    layers.Maxpool2D(pool_size=[2, 2], strides=2, padding='same'),
]

# 利用前面創建的層列表構建網絡容器
conv_net=Sequential(conv_layers)

全連接子網絡包含了 3 個全連接層,每層添加 ReLU 非線性激活函數,最後一層除外。代碼如下:

# 創建 3 層全連接層子網絡
#創建3層全連接層子網絡
fc_net=Sequential([
    layers.Dense(256,activation=tf.nn.relu),
    layers.Dense(128,activation='relu'),
    layers.Dense(10,activation=None),
])

子網絡創建完成後,通過如下代碼查看網絡的參數量:

# build2 個子網絡,並打印網絡參數信息
conv_net.build(input_shape=[4, 32, 32, 3])
10.11 卷積層變種 41
fc_net.build(input_shape=[4, 512])
conv_net.summary()
fc_net.summary()

卷積網絡總參數量約爲 940 萬個,全連接網絡總參數量約爲 17.7 萬個,網絡總參數量約爲950 萬個,相比於原始版本的 VGG13 參數量減少了很多。

由於我們將網絡實現爲 2 個子網絡,在進行梯度更新時,需要合併 2 個子網絡的待優化參數列表。代碼如下:

#列表合併,合併2個子網絡的參數
variables=conv_net.trainable_varibales+fc_net.trainable_varibales
# 對所有參數求梯度
grads=tape.gradient(loss,variables)
#自動更新
optimizer.apply_gradients(zip(grads,varibales))

運行 cifar10_train.py 文件即可開始訓練模型,在訓練完 50 個 Epoch 後,網絡的測試準確率達到了 77.5%。

下面我們就用代碼總結以下上述知識點。

import os
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras import layers,optimizers,datasets,Sequential

plt.rcParams['font.size'] = 16
plt.rcParams['font.family'] = ['STKaiti']
plt.rcParams['axes.unicode_minus'] = False

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
tf.random.set_seed(2345)

os.environ['TF_ENABLE_GPU_GARBAGE_COLLECTION'] = 'false'

# 獲取 GPU 設備列表
gpus = tf.config.experimental.list_physical_devices('GPU')
if gpus:
    try:
        # 設置 GPU 爲增長式佔用
        for gpu in gpus:
            tf.config.experimental.set_memory_growth(gpu, True)
    except RuntimeError as e:
        # 打印異常
        print(e)


def preprocess(x, y):
    # [0~1]
    x = 2 * tf.cast(x, dtype=tf.float32) / 255. - 1
    y = tf.cast(y, dtype=tf.int32)
    return x, y

def load_dataset():
    # 在線下載,加載CIFAR10數據集
    (x,y),(x_test,y_test)=datasets.cifar10.load_data()
    # 刪除y的一個維度,[b,1]=>[b]
    y=tf.squeeze(y,axis=1)
    y_test=tf.squeeze(y_test,axis=1)
    # 打印訓練集和測試集的形狀
    print(x.shape,y.shape,x_test.shape,y_test.shape)
    #構建訓練集對象,隨機打亂,預處理,批量化
    train_db=tf.data.Dataset.from_tensor_slices((x,y))
    train_db=train_db.shuffle(1000).map(preprocess).batch(128)
    # 構建測試集對象,隨機打亂,預處理,批量化
    test_db=tf.data.Dataset.from_tensor_slices((x_test,y_test))
    test_db=test_db.map(preprocess).batch(64)
    # 從訓練集中採樣一個Batch,並觀察
    sample=next(iter(train_db))
    print('sample:', sample[0].shape, sample[1].shape, tf.reduce_min(sample[0]), tf.reduce_max(sample[0]))
    return train_db, test_db

def build_network():
    # 先創建包含多網絡層的列表
    conv_layers=[
        # 5 units of conv + max pooling
        # Conv-Conv-Pooling 單元 1
        # 64 個 3x3 卷積核, 輸入輸出同大小
        layers.Conv2D(64, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
        layers.Conv2D(64, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
        # 高寬減半
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),

        # Conv-Conv-Pooling 單元 2,輸出通道提升至 128,高寬大小減半
        layers.Conv2D(128, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
        layers.Conv2D(128, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),

        # Conv-Conv-Pooling 單元 3,輸出通道提升至 256,高寬大小減半
        layers.Conv2D(256, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
        layers.Conv2D(256, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),

        # Conv-Conv-Pooling 單元 4,輸出通道提升至 512,高寬大小減半
        layers.Conv2D(512, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
        layers.Conv2D(512, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),

        # Conv-Conv-Pooling 單元 5,輸出通道提升至 512,高寬大小減半
        layers.Conv2D(512, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
        layers.Conv2D(512, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same')
    ]

    # 利用前面創建的層列表構建網絡容器
    conv_net=Sequential(conv_layers)

    # 創建3層全連接層子網絡
    fc_net = Sequential([
        layers.Dense(256, activation=tf.nn.relu),
        layers.Dense(128, activation=tf.nn.relu),
        layers.Dense(10, activation=None),
    ])

    # build 2個子網絡,並打印網絡參數信息
    conv_net.build(input_shape=[None,32,32,3])
    fc_net.build(input_shape=(None,512))
    conv_net.summary()
    fc_net.summary()

    return conv_net,fc_net

def train(conv_net,fc_net,train_db,optimizer,varibales,epoch):
    for step,(x,y) in enumerate(train_db):
        with tf.GradientTape() as tape:
            #[b,32,32,3]=>[b,1,1,512]
            out=conv_net(x)
            # flatten,=>[b,512]
            out=tf.reshape(out,[-1,512])
            #[b,512]=>[b,10]
            logits=fc_net(out)
            #[b]=>[b,10]
            y_onehot=tf.one_hot(y,depth=10)
            # comput loss
            loss=tf.losses.categorical_crossentropy(y_onehot,logits,from_logits=True)
            loss=tf.reduce_sum(loss)

        # 對所有參數求梯度
        grads=tape.gradient(loss,varibales)
        #自動更新
        optimizer.apply_gradients(zip(grads,varibales))

        if step %100==0:
            print(epoch,step,'loss:',float(loss))

    return conv_net,fc_net

def predict(conv_net,fc_net,test_db,epoch):
    total_num=0
    total_correct=0
    for x,y in test_db:
        out=conv_net(x)
        out=tf.reshape(out,[-1,512])
        logits=fc_net(out)
        prob=tf.nn.softmax(logits,axis=1)
        pred=tf.argmax(prob,axis=1)
        pred=tf.cast(pred,dtype=tf.int32)

        correct=tf.cast(tf.equal(pred,y),dtype=tf.int32)
        correct=tf.reduce_sum(correct)

        total_num+=x.shape[0]
        total_correct+=int(correct)

    acc=total_correct/total_num
    print(epoch,'axx:',acc)
    return acc
def main():
    epoch_num=50

    train_db,test_db=load_dataset()
    conv_net,fc_net=build_network()
    optimizer=optimizers.Adam(lr=1e-4)

    #列表合併,合併2個子網絡的參數
    variables=conv_net.trainable_variables+fc_net.trainable_variables

    accs=[]
    for epoch in range(epoch_num):
        conv_net,fc_net=train(conv_net,fc_net,train_db,optimizer,variables,epoch)
        acc=predict(conv_net,fc_net,test_db,epoch)
        accs.append(acc)
    
    x=range(epoch_num)
    plt.title('準確率')
    plt.plot(x,accs,color='blue')
    plt.xlabel('Epoch')
    plt.ylabel('Accuracy')
    
    #plt.savefig('cifar10-vgg13-accuracy.png')
    
if __name__=='__main__':
    main() 

在這裏插入圖片描述

10.11 卷積層變種

卷積神經網絡的研究產生了各種各樣優秀的網絡模型,還提出了各種卷積層的變種,本節將重點介紹數種典型的卷積層變種。

10.11.1 空洞卷積

普通的卷積層爲了減少網絡的參數量,卷積核的設計通常選擇較小的 1×1 和3 × 3感受野大小。小卷積核使得網絡提取特徵時的感受野區域有限,但是增大感受野的區域又會增加網絡的參數量和計算代價,因此需要權衡設計。

空洞卷積(Dilated/Atrous Convolution)的提出較好地解決這個問題,空洞卷積在普通卷積的感受野上增加一個 Dilation Rate 參數,用於控制感受野區域的採樣步長,如下圖10.51 所示:當感受野的採樣步長 Dilation Rate 爲 1 時,每個感受野採樣點之間的距離爲1,此時的空洞卷積退化爲普通的卷積;當 Dilation Rate 爲 2 時,感受野每 2 個單元採樣一個點,如圖 10.51 中間的綠色方框中綠色格子所示,每個採樣格子之間的距離爲 2;同樣的方法,圖 10.51 右邊的 Dilation Rate 爲 3,採樣步長爲 3。儘管 Dilation Rate 的增大會使得感受野區域增大,但是實際參與運算的點數仍然保持不變。
在這裏插入圖片描述
以輸入爲單通道的 7×7 張量,單個3 × 3卷積核爲例,如下圖 10.52 所示。在初始位置,感受野從最上、最右位置開始採樣,每隔一個點採樣一次,共採集 9 個數據點,如圖10.52 中綠色方框所示。這 9 個數據點與卷積核相乘運算,寫入輸出張量的對應位置。
在這裏插入圖片描述
卷積核窗口按着步長爲𝑠 =1 向右移動一個單位,如圖 10.53 所示,同樣進行隔點採樣,共採樣 9 個數據點,與卷積核完成相乘累加運算,寫入輸出張量對應位置,直至卷積核移動至最下方、最右邊位置。需要注意區分的是,卷積核窗口的移動步長𝑠和感受野區域的採樣步長 Dilation Rate 是不同的概念。
在這裏插入圖片描述
空洞卷積在不增加網絡參數的條件下,提供了更大的感受野窗口。但是在使用空洞卷積設置網絡模型時,需要精心設計 Dilation Rate 參數來避免出現網格效應,同時較大的Dilation Rate 參數並不利於小物體的檢測、語義分割等任務。

在 TensorFlow 中,可以通過設置 layers.Conv2D()類的 dilation_rate 參數來選擇使用普通卷積還是空洞卷積。例如:

x = tf.random.normal([1,7,7,1]) # 模擬輸入
# 空洞卷積,1 個 3x3 的卷積核
layer = layers.Conv2D(1,kernel_size=3,strides=1,dilation_rate=2)
out = layer(x) # 前向計算
out.shape

TensorShape([1, 3, 3, 1])

當 dilation_rate 參數設置爲默認值 1 時,使用普通卷積方式進行運算;當 dilation_rate參數大於 1 時,採樣空洞卷積方式進行計算。

10.11.2 轉置卷積

轉置卷積(Transposed Convolution,或 Fractionally Strided Convolution,部分資料也稱之爲反捲積/Deconvolution,實際上反捲積在數學上定義爲卷積的逆過程,但轉置卷積並不能恢復出原卷積的輸入,因此稱爲反捲積並不妥當)通過在輸入之間填充大量的 padding 來實現輸出高寬大於輸入高寬的效果,從而實現向上採樣的目的,如圖 10.54 所示。我們先介紹轉置卷積的計算過程,再介紹轉置卷積與普通卷積的聯繫

爲了簡化討論,我們此處只討論輸入ℎ = 𝑤,即輸入高寬相等的情況。
在這裏插入圖片描述

𝒐 + 𝟐𝒑 − 𝒌爲𝒔倍數

考慮輸入爲2 × 2的單通道特徵圖,轉置卷積核爲3 × 3大小,步長𝑠 = 2,填充𝑝 =0的例子。首先在輸入數據點之間均勻插入𝑠 −1 個空白數據點,得到3 × 3的矩陣,如圖 10.55第 2 個矩陣所示,根據填充量在3 × 3矩陣周圍填充相應𝑘 − 𝑝 −1 = 3 − 0−1 = 2行/列,此時輸入張量的高寬爲 7×7 ,如圖 10.55 中第 3 個矩陣所示。
在這裏插入圖片描述
在 7×7 的輸入張量上,進行3 × 3卷積核,步長𝑠′ =1 ,填充𝑝 =0 的普通卷積運算(注意,此階段的普通卷積的步長𝑠′始終爲 1,與轉置卷積的步長𝑠不同),根據普通卷積的輸出計算公式,得到輸出大小爲:
o=i+2pks+1=7+2031+1=5\left.o=| \frac{i+2 * p-k}{s^{\prime}}\right\rfloor+1=\left\lfloor\frac{7+2 * 0-3}{1}\right\rfloor+1=5
5×5 大小的輸出。我們直接按照此計算流程給出最終轉置卷積輸出與輸入關係。在𝑜 +
2𝑝 − 𝑘爲 s 倍數時,滿足關係
o=(i1)s+k2po=(i-1) s+k-2 p
轉置卷積並不是普通卷積的逆過程,但是二者之間有一定的聯繫,同時轉置卷積也是基於普通卷積實現的。在相同的設定下,輸入𝒙經過普通卷積運算後得到𝒐 = Conv(𝒙),我們將𝒐送入轉置卷積運算後,得到𝒙′ = ConvTranspose(𝒐),其中𝒙′ ≠ 𝒙,但是𝒙′與𝒙形狀相同。我們可以用輸入爲 5×5 ,步長𝑠 = 2,填充𝑝 =0 ,3 × 3卷積核的普通卷積運算進行驗證演示,如下圖 10.56 所示。
在這裏插入圖片描述
可以看到,將轉置卷積的輸出 5×5 在同設定條件下送入普通卷積,可以得到2 × 2的輸出,此大小恰好就是轉置卷積的輸入大小,同時我們也觀察到,輸出的2 × 2矩陣並不是轉置卷積輸入的2 × 2矩陣。轉置卷積與普通卷積並不是互爲逆過程,不能恢復出對方的輸入內容,僅能恢復出等大小的張量。因此稱之爲反捲積並不貼切。

基於 TensorFlow 實現上述例子的轉置卷積運算,代碼如下:

#創建X矩陣,高寬各爲5*5
x=tf.range(25)+1
# reshape成合法維度的張量
x=tf.reshape(x,[1,5,5,1])
x=tf.cast(x,tf.float32)
# 創建固定內容的卷積核矩陣
w=tf.constant([[-1,2,-3.],[4,-5,6],[-7,8,-9]])
# 調整爲合法維度的張量
w=tf.expand_dims(w,axis=2)
w=tf.expand_dims(w,axis=3)
# 進行普通卷積運算
out=tf.nn.conv2d(x,w,strides=2,padding='VALID')
print(out)

<tf.Tensor: id=14, shape=(1, 2, 2, 1), dtype=float32, numpy=
array([[[[ -67.],
 [ -77.]],
 [[-117.],
 [-127.]]]], dtype=float32)>

現在我們將普通卷積的輸出作爲轉置卷積的輸入,驗證轉置卷積的輸出是否爲 5×5 ,代碼如下:

# 普通卷積的輸出作爲轉置卷積的輸入,進行轉置卷積運算
xx = tf.nn.conv2d_transpose(out, w, strides=2,padding='VALID',output_shape=[1,5,5,1])
# 輸出的高寬爲 5x5

<tf.Tensor: id=117, shape=(5, 5), dtype=float32, numpy=
array([[ 67., -134., 278., -154., 231.],
[ -268., 335., -710., 385., -462.],
[ 586., -770., 1620., -870., 1074.],
[ -468., 585., -1210., 635., -762.],
[ 819., -936., 1942., -1016., 1143.]], dtype=float32)>

可以看到,轉置卷積能夠恢復出同大小的普通卷積的輸入,但轉置卷積的輸出並不等同於普通卷積的輸入。

𝒐 + 𝟐𝒑 − 𝒌不爲𝒔倍數

讓我們更加深入地分析卷積運算中輸入與輸出大小關係的一個細節。考慮卷積運算的輸出表達式:
o=i+2pks+1o=\left\lfloor\frac{i+2 * p-k}{s}\right\rfloor+1
當步長s > 時,i+2pks\left\lfloor\frac{i+2 * p-k}{s}\right\rfloor向下取整運算使得出現多種不同輸入尺寸𝑖對應到相同的輸出尺寸𝑜上。舉個例子,考慮輸入大小爲 6×6 ,卷積核大小爲3 × 3,步長爲1的卷積運算,代碼如下:

x=tf.random.normal([1,6,6,1])
# 6*6的輸入經過普通卷積
out=tf.nn.conv2d(x,w,strides=2,padding='VALID')
print(out)

<tf.Tensor: id=21, shape=(1, 2, 2, 1), dtype=float32, numpy=
array([[[[ 20.438847 ],
[ 19.160788 ]],
[[ 0.8098897],
[-28.30303 ]]]], dtype=float32)>

此種情況也能獲得2 × 2大小的卷積輸出,與圖 10.56 中可以獲得相同大小的輸出。因此,不同輸入大小的卷積運算可能獲得相同大小的輸出。考慮到卷積與轉置卷積輸入輸出大小關係互換,從轉置卷積的角度來說,輸入尺寸𝑖經過轉置卷積運算後,可能獲得不同的輸出𝑜大小。因此通過在圖 10.55 中填充𝑎行、𝑎列來實現不同大小的輸出𝑜,從而恢復普通卷積不同大小的輸入的情況,其中𝑎關係爲:
a=(o+2pk)%sa=(o+2 p-k) \%s
此時轉置卷積的輸出變爲:
o=(i1)s+k2p+ao=(i-1) s+k-2 p+a
在 TensorFlow 中間,不需要手動指定𝑎參數,只需要指定輸出尺寸即可,TensorFlow會自動推導需要填充的行列數𝑎,前提是輸出尺寸合法。例如:

xx=tf.nn.conv2d_transpose(out,w,strides=2,padding='VALID',output_shape=[1,6,6,1])
print(xx)

<tf.Tensor: id=23, shape=(1, 6, 6, 1), dtype=float32, numpy=
array([[[[ -20.438847 ],
[ 40.877693 ],
[ -80.477325 ],
[ 38.321575 ],
[ -57.48236 ],
[ 0. ]],...

通過改變參數 output_shape=[1,5,5,1]也可以獲得高寬爲 × 的張量。

矩陣角度

轉置卷積的轉置是指卷積核矩陣𝑾產生的稀疏矩陣𝑾′在計算過程中需要先轉置wTw'^{T},再進行矩陣相乘運算,而普通卷積並沒有轉置𝑾′的步驟。這也是它被稱爲轉置卷積的名字由來。

考慮普通 Conv2d 運算:𝑿和𝑾,需要根據 strides 將卷積核在行、列方向循環移動獲取參與運算的感受野的數據,串行計算每個窗口處的“相乘累加”值,計算效率極低。爲了加速運算,在數學上可以將卷積核𝑾根據 strides 重排成稀疏矩陣𝑾′,再通過𝑾′@𝑿′一次完成運算(實際上,𝑾′矩陣過於稀疏,導致很多無用的 0 乘運算,很多深度學習框架也不是通過這種方式實現的)。

以 4 行 4 列的輸入𝑿,高寬爲3 × 3,步長爲 1,無 padding 的卷積核𝑾的卷積運算爲例,首先將𝑿打平成𝑿′,如圖 10.57 所示。
在這裏插入圖片描述
然後將卷積核𝑾轉換成稀疏矩陣𝑾′,如圖 10.58 所示。
在這裏插入圖片描述
此時通過一次矩陣相乘即可實現普通卷積運算:
O=W@X\boldsymbol{O}^{\prime}=\boldsymbol{W}^{\prime} \boldsymbol{@} \boldsymbol{X}^{\prime}
如果給定𝑶,希望能夠生成與𝑿同形狀大小的張量,怎麼實現呢?將𝑾′轉置後與圖 10.57
方法重排後的𝑶′完成矩陣相乘即可:
X=WT@O\boldsymbol{X}^{\prime}=\boldsymbol{W}^{\prime \mathrm{T}} @ \boldsymbol{O}^{\prime}
得到的𝑿′通過 Reshape 操作變爲與原來的輸入𝑿尺寸一致,但是內容不同。例如𝑶′的 shape爲[4,1] ,𝑾′T的 shape 爲[16,4] ,矩陣相乘得到𝑿′的 shape 爲[16,1],Reshape 後即可產生[4,4] 形狀的張量。由於轉置卷積在矩陣運算時,需要將𝑾′轉置後才能與轉置卷積的輸入𝑶′矩陣相乘,故稱爲轉置卷積。

轉置卷積具有“放大特徵圖”的功能,在生成對抗網絡、語義分割等中得到了廣泛應用,如 DCGAN 中的生成器通過堆疊轉置卷積層實現逐層“放大”特徵圖,最後獲得十分逼真的生成圖片。
在這裏插入圖片描述

轉置卷積實現

在 TensorFlow 中,可以通過 nn.conv2d_transpose 實現轉置卷積運算。我們先通過nn.conv2d 完成普通卷積運算。注意轉置卷積的卷積核的定義格式爲 [𝑘 ,𝑘 ,coutc_{out},cinc_{in} ]。例如:


#創建4*4大小的輸入
x=tf.range(16)+1
x=tf.reshape(x,[1,4,4,1])
#x=tf.cast(x,tf.float32)
#創建3*3卷積核
w=tf.constant([[-1,2,-3],[4,-5,6],[-7,8,-9]])
w=tf.expand_dims(w,axis=2)
w=tf.expand_dims(w,axis=3)
# 普通卷積運算
out=tf.nn.conv2d(x,w,strides=1,padding='VALID')
print(out)

<tf.Tensor: id=42, shape=(1,2,2,1), dtype=float32, numpy=
array([[-56., -61.],
 [-76., -81.]], dtype=float32)>

在保持 strides=1padding=’VALID’,卷積核不變的情況下,我們通過卷積核 w 與輸出 out 的轉置卷積運算嘗試恢復與輸入 x 相同大小的高寬張量,代碼如下:

xx=tf.nn.conv2d_transpose(out,w,strides=1,padding='VALID',output_shape=[1,4,4,1])
xx=tf.squeeze(xx)
print(xx)

<tf.Tensor: id=44, shape=(4, 4), dtype=float32, numpy=
array([[ 56., -51., 46., 183.],
 [-148., -35., 35., -123.],
 [ 88., 35., -35., 63.],
 [ 532., -41., 36., 729.]], dtype=float32)>

可以看到,轉置卷積生成了 × 的特徵圖,但特徵圖的數據與輸入 x 並不相同。

在使用 tf.nn.conv2d_transpose 進行轉置卷積運算時,需要額外手動設置輸出的高寬。tf.nn.conv2d_transpose 並不支持自定義 padding 設置,只能設置爲 VALID 或者 SAME。

當設置 padding=’VALID’時,輸出大小表達爲:
o=(i1)s+ko=(i-1) s+k
當設置 padding=’SAME’時,輸出大小表達爲:
o=iso=i \cdot s
如果讀者對轉置卷積的原理細節暫時無法理解,可以牢記上述兩個表達式即可。例如,2 × 2的轉置卷積輸入與3 × 3的卷積核運算,strides=1,padding=’VALID’時,輸出大小爲:
h=w=(21)1+3=4h^{\prime}=w^{\prime}=(2-1) \cdot 1+3=4
2 × 2的轉置卷積輸入與3 × 3的卷積核運算,strides=3,padding=’SAME’時,輸出大小爲:
h=w=23=6h^{\prime}=w^{\prime}=2 \cdot 3=6
轉置卷積也可以和其他層一樣,通過 layers.Conv2DTranspose 類創建一個轉置卷積層,然後調用實例即可完成前向計算:

# 創建轉置卷積類
layer = layers.Conv2DTranspose(1,kernel_size=3,strides=1,padding='VALID')
xx2 = layer(out) # 通過轉置卷積層
xx2

<tf.Tensor: id=130, shape=(1, 4, 4, 1), dtype=float32, numpy=
array([[[[ 9.7032385 ],
[ 5.485071 ],
[ -1.6490463 ],
[ 1.6279562 ]],...
10.11.3 分離卷積

這裏以深度可分離卷積(Depth-wise Separable Convolution)爲例。普通卷積在對多通道輸入進行運算時,卷積核的每個通道與輸入的每個通道分別進行卷積運算,得到多通道的特徵圖,再對應元素相加產生單個卷積核的最終輸出,如圖 10.60 所示。
在這裏插入圖片描述
分離卷積的計算流程則不同,卷積核的每個通道與輸入的每個通道進行卷積運算,得到多個通道的中間特徵,如圖 10.61 所示。這個多通道的中間特徵張量接下來進行多個 1×1 卷積核的普通卷積運算,得到多個高寬不變的輸出,這些輸出在通道軸上面進行拼接,從而產生最終的分離卷積層的輸出。可以看到,分離卷積層包含了兩步卷積運算,第一步卷積運算是單個卷積核,第二個卷積運算包含了多個卷積核。
在這裏插入圖片描述
那麼採用分離卷積有什麼優勢呢?一個很明顯的優勢在於,同樣的輸入和輸出,採用Separable Convolution的參數量約是普通卷積的1/3。考慮上圖中的普通卷積和分離卷積的例
子。普通卷積的參數量是
3334=1083 \cdot 3 \cdot 3 \cdot 4=108
分離卷積的第一部分參數量是
3331=273 \cdot 3 \cdot 3 \cdot 1=27
第二部分參數量是
1134=141 \cdot 1 \cdot 3 \cdot 4=14
分離卷積的總參數量只有39,但是卻能實現普通卷積同樣的輸入輸出尺寸變換。分離卷積在 Xception 和 MobileNets 等對計算代價敏感的領域中得到了大量應用。

10.12 深度殘差網絡

AlexNet、VGG、GoogLeNet 等網絡模型的出現將神經網絡的發展帶入了幾十層的階段,研究人員發現網絡的層數越深,越有可能獲得更好的泛化能力。但是當模型加深以後,網絡變得越來越難訓練,這主要是由於梯度彌散和梯度爆炸現象造成的。在較深層數的神經網絡中,梯度信息由網絡的末層逐層傳向網絡的首層時,傳遞的過程中會出現梯度接近於 0 或梯度值非常大的現象。網絡層數越深,這種現象可能會越嚴重。

那麼怎麼解決深層神經網絡的梯度彌散和梯度爆炸現象呢?一個很自然的想法是,既然淺層神經網絡不容易出現這些梯度現象,那麼可以嘗試給深層神經網絡添加一種回退到淺層神經網絡的機制。當深層神經網絡可以輕鬆地回退到淺層神經網絡時,深層神經網絡可以獲得與淺層神經網絡相當的模型性能,而不至於更糟糕。

通過在輸入和輸出之間添加一條直接連接的 Skip Connection可以讓神經網絡具有回退的能力。以 VGG13 深度神經網絡爲例,假設觀察到 VGG13 模型出現梯度彌散現象,而10 層的網絡模型並沒有觀測到梯度彌散現象,那麼可以考慮在最後的兩個卷積層添加 Skip Connection,如圖 10.62 中所示。通過這種方式,網絡模型可以自動選擇是否經由這兩個卷積層完成特徵變換,還是直接跳過這兩個卷積層而選擇 Skip Connection,亦或結合兩個卷積層和 Skip Connection 的輸出。
在這裏插入圖片描述
2015 年,微軟亞洲研究院何凱明等人發表了基於 Skip Connection 的深度殘差網絡(Residual Neural Network,簡稱 ResNet)算法,並提出了 18 層、34 層、50 層、101層、152 層的 ResNet-18、ResNet-34、ResNet-50、ResNet-101 和 ResNet-152 等模型,甚至成功訓練出層數達到 1202 層的極深層神經網絡。ResNet 在 ILSVRC 2015 挑戰賽 ImageNet數據集上的分類、檢測等任務上面均獲得了最好性能,ResNet 論文至今已經獲得超 25000的引用量,可見 ResNet 在人工智能行業的影響力。

10.12.1 ResNet 原理

ResNet 通過在卷積層的輸入和輸出之間添加 Skip Connection 實現層數回退機制,如下圖 10.63 所示,輸入𝒙通過兩個卷積層,得到特徵變換後的輸出ℱ(𝒙),與輸入𝒙進行對應元素的相加運算,得到最終輸出ℋ(𝒙):
H(x)=x+F(x)\mathcal{H}(x)=x+\mathcal{F}(x)
ℋ(𝒙)叫作殘差模塊(Residual Block,簡稱 ResBlock)。由於被 Skip Connection 包圍的卷積神經網絡需要學習映射ℱ(𝒙) = ℋ(𝒙) − 𝒙,故稱爲殘差網絡

爲了能夠滿足輸入𝒙與卷積層的輸出ℱ(𝒙)能夠相加運算,需要輸入𝒙的 shape 與ℱ(𝒙)的shape 完全一致。當出現 shape 不一致時,一般通過在 Skip Connection 上添加額外的卷積運算環節將輸入𝒙變換到與ℱ(𝒙)相同的 shape,如圖 10.63 中identity(𝒙)函數所示,其中identity(𝒙)以 1×1 的卷積運算居多,主要用於調整輸入的通道數。
在這裏插入圖片描述
下圖 10.64 對比了 34 層的深度殘差網絡、34 層的普通深度網絡以及 19 層的 VGG 網絡結構。可以看到,深度殘差網絡通過堆疊殘差模塊,達到了較深的網絡層數,從而獲得了訓練穩定、性能優越的深層網絡模型。
在這裏插入圖片描述

10.12.2 ResBlock 實現

深度殘差網絡並沒有增加新的網絡層類型,只是通過在輸入和輸出之間添加一條 Skip Connection,因此並沒有針對 ResNet 的底層實現。在 TensorFlow 中通過調用普通卷積層即可實現殘差模塊

首先創建一個新類,在初始化階段創建殘差塊中需要的卷積層、激活函數層等,首先新建ℱ(𝑥)卷積層,代碼如下:

class BasicBlock(layers.Layer):
    # 殘差模塊類
    def __init__(self,filter_num,stride=1):
        super(BasicBlock,self).__init__()
        #f(x)包含了2個普通卷積層,創建卷積層1
        self.conv1=layers.Conv2D(filter_num,(3,3),strides=stride,padding='same')
        self.bn1=layers.BatchNormalzation()
        self.relu=layers.Activation('relu')
        # 創建卷積層2
        self.conv2=layers.Conv2D(filter_num,(3,3),strides=1,padding='same')
        self.bn2=layers.BatchNormalzation()

當ℱ(𝒙)的形狀與𝒙不同時,無法直接相加,我們需要新建identity(𝒙)卷積層,來完成𝒙的形狀轉換。緊跟上面代碼,實現如下:

        if stride!=1:#插入identify層
            self.downsample=Sequential()
            self.downsample.add(layers.Conv2D(filter_num,(1,1),strides=stride))
        else:# 否則,直接連接
            self.downsample=lambda x:x

在前向傳播時,只需要將ℱ(𝒙)與identity(𝒙)相加,並添加 ReLU 激活函數即可。前向計算函數代碼下:

    def call(self,inputs,training=None):
        out=self.conv1(inputs)
        out=self.bn1(out)
        out=self.relu(out)
        out=self.conv2(out)
        out=self.bn2(out)
        #輸入通過identify()轉換
        identify=self.downsample(inputs)
        # f(x)+x運算
        output=layers.add([out,identify])
        # 再通過激活函數並返回
        output=tf.nn.relu(output)
        return output

10.13 DenseNet

Skip Connection 的思想在 ResNet 上面獲得了巨大的成功,研究人員開始嘗試不同的Skip Connection 方案,其中比較流行的就是 DenseNet 。DenseNet 將前面所有層的特徵圖信息通過 Skip Connection 與當前層輸出進行聚合,與 ResNet 的對應位置相加方式不同,DenseNet 採用在通道軸𝑐維度進行拼接操作,聚合特徵信息。

如下圖 10.65 所示,輸入X0X_{0} 通過H1H_{1}卷積層得到輸出X1X_{1}X1X_{1}X0X_{0} 在通道軸上進行拼接,得到聚合後的特徵張量,送入H2H_{2}卷積層,得到輸出X2X_{2},同樣的方法,𝑿2與前面所有層的特徵信息X1X_{1}X0X_{0}進行聚合,再送入下一層。如此循環,直至最後一層的輸出X4X_{4}和前面所有層的特徵信息:{Xi}i=0,1,2,3\left\{\boldsymbol{X}_{\boldsymbol{i}}\right\}_{i=0,1,2,3}進行聚合得到模塊的最終輸出。這樣一種基於 SkipConnection 稠密連接的模塊叫做 Dense Block
在這裏插入圖片描述
DenseNet 通過堆疊多個 Dense Block 構成複雜的深層神經網絡,如圖 10.66 所示。
在這裏插入圖片描述
圖 10.67 比較了不同版本的 DenseNet 的性能、DenseNet 與 ResNet 的性能比較,以及DenseNet 與 ResNet 訓練曲線。
在這裏插入圖片描述

10.14 CIFAR10 與 ResNet18 實戰

本節我們將實現 18 層的深度殘差網絡 ResNet18,並在 CIFAR10 圖片數據集上訓練與測試。並將與 13 層的普通神經網絡 VGG13 進行簡單的性能比較。

標準的 ResNet18 接受輸入爲224 × 224 大小的圖片數據,我們將 ResNet18 進行適量調整,使得它輸入大小爲32 × 32,輸出維度爲 10。調整後的 ResNet18 網絡結構如圖 10.68所示。
在這裏插入圖片描述
首先實現中間兩個卷積層,Skip Connection 1x1 卷積層的殘差模塊。代碼如下:

class BasicBlock(layers.Layer):
    # 殘差模塊
    def __init__(self,filter_num,stride=1):
        super(BasicBlock,self).__init__()
        #第一個卷積單元
        self.conv1=layers.Conv2D(filter_num,(3,3),strides=stride,padding='same')
        self.bn1=layers.BatchNormalization()
        self.relu=layers.Activation('relu')
        #第二個卷積單元
        self.conv2=layers.Conv2D(filter_num,(3,3),strides=1,padding='same')
        self.bn2=layers.BatchNormalization()

        if stride!=1:#通過1*1卷積完成shape匹配
            self.downsample=Sequential()
            self.downsample.add(layers.Conv2D(filter_num,(1,1),strides=stride))
        else:# shape匹配,直接短接
            self.downsample=lambda x:x


    def call(self,inputs,training=None):
        #前向計算函數
        #[b,h,w,c],通過第一個卷積單元
        out=self.conv1(inputs)
        out=self.bn1(out)
        out=self.relu(out)
        #通過第二個卷積單元
        out=self.conv2(out)
        out=self.bn2(out)
        #通過identify模塊
        identify=self.downsample(inputs)
        #2條路徑輸出直接相加
        output=layers.add([out,identify])
        output=tf.nn.relu(output)#激活函數
        
        return output

在設計深度卷積神經網絡時,一般按照特徵圖高寬ℎ/𝑤逐漸減少,通道數𝑐逐漸增大的經驗法則。可以通過堆疊通道數逐漸增大的 Res Block 來實現高層特徵的提取,通過build_resblock 可以一次完成多個殘差模塊的新建。代碼如下:

     def build_resblock(self,filter_num,blocks,stride=1):
        # 輔助函數,堆疊filter_num個BasicBlock
        res_blocks=Sequential()
        #只有第一個BasicBlock的步長可能不爲1,實現下采樣
        res_blocks.add(BasicBlock(filter_num,stride))
        
        for _ in range(1,blocks):#其他BasicBlock步長都爲1
            res_blocks.add(BasicBlock(filter_num,stride=1))
            
        return res_blocks

下面我們來實現通用的 ResNet 網絡模型。代碼如下:

class ResNet(keras.Model):

    def build_resblock(self, filter_num, blocks, stride=1):
        # 輔助函數,堆疊filter_num個BasicBlock
        res_blocks = Sequential()
        # 只有第一個BasicBlock的步長可能不爲1,實現下采樣
        res_blocks.add(BasicBlock(filter_num, stride))

        for _ in range(1, blocks):  # 其他BasicBlock步長都爲1
            res_blocks.add(BasicBlock(filter_num, stride=1))

        return res_blocks

    # 通用的ResNet實現類
    def __init__(self,layer_dims,num_classes=10):
        super(ResNet,self).__init__()
        #根網絡,預處理
        self.stem=Sequential([
            layers.Conv2D(64,(3,3),strides=(1,1)),
            layers.BatchNormalization(),
            layers.Activation('relu'),
            layers.MaxPool2D(pool_size=(2,2),strides=(1,1),padding='same')
        ])
        #堆疊4個Block,每個Block包含了多個BasicBlock,設置步長不一樣
        self.layer1=self.build_resblock(64,layer_dims[0])
        self.layer2=self.build_resblock(128,layer_dims[1],strifdes=2)
        self.layer3=self.build_resblock(256,layer_dims[2],strides=2)
        self.layer4=self.build_resblock(512,layer_dims[3],strides=2)

        #通過Pooling層將高寬降低爲1*1
        self.avgpool=layers.GlobalAveragePooling2D()
        self.fc=layers.Dense(num_classes)

    def call(self,inputs,training=None):
        # 前向計算:通過根網絡
        x=self.stem(inputs)
        # 一次通過4個模塊
        x=self.layer1(x)
        x=self.layer2(x)
        x=self.layer3(x)
        x=self.layer4(x)

        #通過池化層
        x=self.avgpool(x)
        #通過全連接層
        x=self.fc(x)

通過調整每個 Res Block 的堆疊數量和通道數可以產生不同的 ResNet,如通過 64-64-128-128-256-256-512-512 通道數配置,共 8 個 Res Block,可得到 ResNet18 的網絡模型。每個ResBlock 包含了 2 個主要的卷積層,因此卷積層數量是8 ∙ 2 =16,加上網絡末尾的全連接層,共 18 層。創建 ResNet18 和 ResNet34 可以簡單實現如下:

 def resnet18():
        #通過調整模塊內部BasicBlock的數量和配置實現不同的ResNet
        return ResNet([2,2,2,2])

 def resnet34():
       #通過調整模塊內部BasicBlock的數量和配置實現不同的ResNet
       return ResNet([3,4,6,3])

下面完成 CIFAR10 數據集的加載工作,代碼如下:

(x,y), (x_test, y_test) = datasets.cifar10.load_data() # 加載數據集
y = tf.squeeze(y, axis=1) # 刪除不必要的維度
y_test = tf.squeeze(y_test, axis=1) # 刪除不必要的維度
print(x.shape, y.shape, x_test.shape, y_test.shape)
train_db = tf.data.Dataset.from_tensor_slices((x,y)) # 構建訓練集
# 隨機打散,預處理,批量化
train_db = train_db.shuffle(1000).map(preprocess).batch(512)
test_db = tf.data.Dataset.from_tensor_slices((x_test,y_test)) #構建測試集
# 隨機打散,預處理,批量化
test_db = test_db.map(preprocess).batch(512)10 章 卷積神經網絡 58
# 採樣一個樣本
sample = next(iter(train_db))
print('sample:', sample[0].shape, sample[1].shape,
 tf.reduce_min(sample[0]), tf.reduce_max(sample[0]))

數據的預處理邏輯比較簡單,直接將數據範圍映射到 − 區間。這裏也可以基於ImageNet 數據圖片的均值和標準差做標準化處理。代碼如下:

def preprocess(x, y):
 # 將數據映射到-1~1
 x = 2*tf.cast(x, dtype=tf.float32) / 255. - 1
 y = tf.cast(y, dtype=tf.int32) # 類型轉換
 return x,y

網絡訓練邏輯和普通的分類網絡訓練部分一樣,固定訓練 50 個 Epoch。代碼如下:

 for epoch in range(50): # 訓練 epoch
 for step, (x,y) in enumerate(train_db):
 with tf.GradientTape() as tape:
 # [b, 32, 32, 3] => [b, 10],前向傳播
 logits = model(x)
 # [b] => [b, 10],one-hot 編碼
 y_onehot = tf.one_hot(y, depth=10)
 # 計算交叉熵
 loss = tf.losses.categorical_crossentropy(y_onehot, logits,
from_logits=True)
 loss = tf.reduce_mean(loss)
 # 計算梯度信息
 grads = tape.gradient(loss, model.trainable_variables)
 # 更新網絡參數
 optimizer.apply_gradients(zip(grads, model.trainable_variables))

ResNet18 的網絡參數量共 1100 萬個,經過 50 個 Epoch 後,網絡的準確率達到了79.3%。我們這裏的實戰代碼比較精簡,在精挑超參數、數據增強等手段加持下,準確率可以達到更高。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章