CNN on TensorFlow
本文大部分內容均參考于:
An Intuitive Explanation of Convolutional Neural Networks
知乎:「為什么 ReLU 要好過于 tanh 和 sigmoid function?」
Deep MNIST for Experts
TensorFlow Python API 「tf.nn」
Build a Multilayer Convolutional Network
在 TensorFlow 官方的 tutorials 中,我們使用 softmax 模型在 MNIST 數據集上得到的結果只有 91% 的正確率,實在太糟糕。所以,我們將使用一個稍微復雜的模型:CNN(卷積神經網絡)來改善實驗效果。在 CNN 主要有四個操作:
- 卷積
- 非線性處理(ReLU)
- 池化或者亞采樣
- 分類(全連接層)
這些操作對于各個卷積神經網絡來說都是基本組件,因此理解它們的工作原理有助于充分了解卷積神經網絡。下面我們將會嘗試理解各步操作背后的原理。
What's CNN
Convolution
卷積的主要目的是為了從輸入圖像中提取特征。卷積可以通過從輸入的一小塊數據中學到圖像的特征,并可以保留像素間的空間關系。讓我們舉個例子來嘗試理解一下卷積是如何處理圖像的:
正如我們上面所說,每張圖像都可以看作是像素值的矩陣??紤]一下一個 5 x 5 的圖像,它的像素值僅為 0 或者 1(注意對于灰度圖像而言,像素值的范圍是 0 到 255,下面像素值為 0 和 1 的綠色矩陣僅為特例):

同時,考慮下另一個 3 x 3 的矩陣,如下所示:

接下來,5 x 5 的圖像和 3 x 3 的矩陣的卷積可以按下圖所示的動畫一樣計算:

現在停下來好好理解下上面的計算是怎么完成的。我們用橙色的矩陣在原始圖像(綠色)上滑動,每次滑動一個像素(也叫做「步長」),在每個位置上,我們計算對應元素的乘積(兩個矩陣間),并把乘積的和作為最后的結果,得到輸出矩陣(粉色)中的每一個元素的值。注意,3 x 3 的矩陣每次步長中僅可以看到輸入圖像的一部分。
在 CNN 的術語中,3x3 的矩陣叫做「濾波器」(filter) 或「核」(kernel) 或者 「特征檢測器」(feature detector),通過在圖像上滑動濾波器并計算點乘得到矩陣叫做「卷積特征」(Convolved Feature) 或者 「激活圖」(Activation Map) 或者 「特征圖」(Feature Map)。記住,濾波器在原始輸入圖像上的作用是特征檢測器。
從上面圖中的動畫可以看出,對于同樣的輸入圖像,不同值的濾波器將會生成不同的特征圖。比如,對于下面這張輸入圖像:

在下表中,我們可以看到不同濾波器對上圖卷積的效果。正如表中所示,通過在卷積操作前修改濾波矩陣的數值,我們可以進行諸如邊緣檢測、銳化和模糊等操作 —— 這表明不同的濾波器可以從圖中檢測到不同的特征,比如邊緣、曲線等。

另一個直觀理解卷積操作的好方法是看下面這張圖的動畫:

濾波器(紅色框)在輸入圖像滑過(卷積操作),生成一個特征圖。另一個濾波器(綠色框)在同一張圖像上卷積可以得到一個不同的特征圖。注意卷積操作可以從原圖上獲取局部依賴信息。同時注意這兩個不同的濾波器是如何從同一張圖像上生成不同的特征圖。記住上面的圖像和兩個濾波器僅僅是我們上面討論的數值矩陣。
在實踐中,CNN 會在訓練過程中學習到這些濾波器的值(盡管我們依然需要在訓練前指定諸如濾波器的個數、濾波器的大小、網絡架構等參數)。我們使用的濾波器越多,提取到的圖像特征就越多,網絡所能在未知圖像上識別的模式也就越好。
特征圖的大小(卷積特征)由下面三個參數控制,我們需要在卷積前確定它們:
深度(Depth):深度對應的是卷積操作所需的濾波器個數。在下圖的網絡中,我們使用三個不同的濾波器對原始圖像進行卷積操作,這樣就可以生成三個不同的特征圖。你可以把這三個特征圖看作是堆疊的 2d 矩陣,那么,特征圖的「深度」就是 3。
步長(Stride):步長是我們在輸入矩陣上滑動濾波矩陣的像素數。當步長為 1 時,我們每次移動濾波器一個像素的位置。當步長為 2 時,我們每次移動濾波器會跳過 2 個像素。步長越大,將會得到更小的特征圖。
零填充(Zero-padding):有時,在輸入矩陣的邊緣使用零值進行填充,這樣我們就可以對輸入圖像矩陣的邊緣進行濾波。零填充的一大好處是可以讓我們控制特征圖的大小。使用零填充的也叫做泛卷積,不適用零填充的叫做嚴格卷積。

ReLU
ReLU表示修正線性單元(Rectified Linear Unit),是一個非線性操作。

-
為什么要引入非線性激勵函數?
如果不用激勵函數(其實相當于激勵函數是 $f(x) = x$ ),在這種情況下你每一層輸出都是上層輸入的線性函數,很容易驗證,無論你神經網絡有多少層,輸出都是輸入的線性組合,與沒有隱層效果相當,這種情況就是最原始的感知機(Perceptron)了。
正因為上面的原因,我們決定引入非線性函數作為激勵函數,這樣深層神經網絡就有意義了(不再是輸入的線性組合,可以逼近任意函數)。最早的想法是 sigmoid 函數或者 tanh 函數,輸出有界,很容易充當下一層輸入(以及一些人的生物解釋balabala)。
-
為什么要引入 ReLU 而不是其他的非線性函數(例如 Sigmoid 函數)?
- 采用 sigmoid 等函數,算激活函數時(指數運算),計算量大,反向傳播求誤差梯度時,求導涉及除法,計算量相對大,而采用Relu激活函數,整個過程的計算量節省很多。
- 對于深層網絡,sigmoid 函數反向傳播時,很容易就會出現梯度消失的情況(在sigmoid接近飽和區時,變換太緩慢,導數趨于0,這種情況會造成信息丟失),從而無法完成深層網絡的訓練。
- Relu 會使一部分神經元的輸出為 0,這樣就造成了網絡的稀疏性,并且減少了參數的相互依存關系,緩解了過擬合問題的發生(以及一些人的生物解釋balabala)。
當然現在也有一些對 relu 的改進,比如 prelu,random relu等,在不同的數據集上會有一些訓練速度上或者準確率上的改進,具體的可以找相關的paper看。
(多加一句,現在主流的做法,會多做一步 batch normalization,盡可能保證每一層網絡的輸入具有相同的分布。而最新的 paper,他們在加入bypass connection 之后,發現改變 batch normalization 的位置會有更好的效果。)
-
ReLU 的優點與缺點?
優點:
- 解決了gradient vanishing問題 (在正區間)
- 計算速度非???,只需要判斷輸入是否大于0
- 收斂速度遠快于sigmoid和tanh
缺點:
- ReLU 的輸出不是 zero-centered
- Dead ReLU Problem,指的是某些神經元可能永遠不會被激活,導致相應的參數永遠不能被更新。有兩個主要原因可能導致這種情況產生: (1) 非常不幸的參數初始化,這種情況比較少見 (2) learning rate 太高導致在訓練過程中參數更新太大,不幸使網絡進入這種狀態。解決方法是可以采用 Xavier 初始化方法,以及避免將 learning rate 設置太大或使用 adagrad 等自動調節 learning rate 的算法。
幾十年的機器學習發展中,我們形成了這樣一個概念:非線性激活函數要比線性激活函數更加先進。
尤其是在布滿 Sigmoid 函數的 BP 神經網絡,布滿徑向基函數的 SVM 神經網絡中,往往有這樣的幻覺,非線性函數對非線性網絡貢獻巨大。
該幻覺在 SVM 中更加嚴重。核函數的形式并非完全是 SVM 能夠處理非線性數據的主力功臣(支持向量充當著隱層角色)。
那么在深度網絡中,對非線性的依賴程度就可以縮一縮。另外,在上一部分提到,稀疏特征并不需要網絡具有很強的處理線性不可分機制。
綜合以上兩點,在深度學習模型中,使用簡單、速度快的線性激活函數可能更為合適。
ReLU 操作可以從下面的圖中理解。這里的輸出特征圖也可以看作是“修正”過的特征圖。

所謂麻雀雖小,五臟俱全,ReLU雖小,但也是可以改進的。
ReLU的種類
ReLU的區分主要在負數端,根據負數端斜率的不同來進行區分,大致如下圖所示。

普通的ReLU負數端斜率是0,Leaky ReLU則是負數端有一個比較小的斜率,而PReLU則是在后向傳播中學習到斜率。而Randomized Leaky ReLU則是使用一個均勻分布在訓練的時候隨機生成斜率,在測試的時候使用均值斜率來計算。
效果
其中,NDSB 數據集是 Kaggle 的比賽,而 RReLU 正是在這次比賽中嶄露頭角的。

通過上述結果,可以看到四點:
- 對于 Leaky ReLU 來說,如果斜率很小,那么與 ReLU 并沒有大的不同,當斜率大一些時,效果就好很多。
- 在訓練集上,PReLU 往往能達到最小的錯誤率,說明 PReLU 容易過擬合。
- 在 NSDB 數據集上 RReLU 的提升比 cifar10 和 cifar100 上的提升更加明顯,而 NSDB 數據集比較小,從而可以說明,RReLU在與過擬合的對抗中更加有效。
- 對于 RReLU 來說,還需要研究一下隨機化得斜率是怎樣影響訓練和測試過程的。
參考文獻
[1]. Xu B, Wang N, Chen T, et al. Empirical evaluation of rectified activations in convolutional network[J]. arXiv preprint arXiv:1505.00853, 2015.
Pooling
空間池化(Spatial Pooling)(也叫做亞采用或者下采樣)降低了各個特征圖的維度,但可以保持大部分重要的信息??臻g池化有下面幾種方式:最大化、平均化、加和等等。
對于最大池化(Max Pooling),我們定義一個空間鄰域(比如,2x2 的窗口),并從窗口內的修正特征圖中取出最大的元素。除了取最大元素,我們也可以取平均(Average Pooling)或者對窗口內的元素求和。在實際中,最大池化被證明效果更好一些。
下面的圖展示了使用 2x2 窗口在修正特征圖(在卷積 + ReLU 操作后得到)使用最大池化的例子。

我們以 2 個元素(也叫做“步長”)滑動我們 2x2 的窗口,并在每個區域內取最大值。如上圖所示,這樣操作可以降低我們特征圖的維度。
在下圖展示的網絡中,池化操作是分開應用到各個特征圖的(注意,因為這樣的操作,我們可以從三個輸入圖中得到三個輸出圖)。

下圖展示了我們在 ReLU 操作之后得到的修正特征圖的池化操作的效果:

池化函數可以逐漸降低輸入表示的空間尺度。特別地,Pooling 的好處是:
使輸入表示(特征維度)變得更小,并且網絡中的參數和計算的數量更加可控的減小,因此,可以控制過擬合。
使網絡對于輸入圖像中更小的變化、冗余和變換變得不變性(輸入的微小冗余將不會改變池化的輸出——因為我們在局部鄰域中使用了最大化/平均值的操作)。
-
幫助我們獲取圖像最大程度上的尺度不變性(準確的詞是“不變性”)。它非常的強大,因為我們可以檢測圖像中的物體,無論它們位置在哪里。
?
到目前為止我們了解了卷積、ReLU 和池化是如何操作的。理解這些層是構建任意 CNN 的基礎是很重要的。正如下圖所示,我們有兩組卷積、ReLU & 池化層 —— 第二組卷積層使用六個濾波器對第一組的池化層的輸出繼續卷積,得到一共六個特征圖。接下來對所有六個特征圖應用 ReLU。接著我們對六個修正特征圖分別進行最大池化操作。
這些層一起就可以從圖像中提取有用的特征,并在網絡中引入非線性,減少特征維度,同時保持這些特征具有某種程度上的尺度變化不變性。

第二組池化層的輸出作為全連接層的輸入,接下來我們將介紹全連接層。
Connect
全連接層是傳統的多層感知器,在輸出層使用的是 softmax 激活函數(也可以使用其他像 SVM 的分類器,但在本文中只使用 softmax)?!溉B接」(Fully Connected) 這個詞表明前面層的所有神經元都與下一層的所有神經元連接。
卷積和池化層的輸出表示了輸入圖像的高級特征。全連接層的目的是為了使用這些特征把輸入圖像基于訓練數據集進行分類。比如,在下面圖中我們進行的圖像分類有四個可能的輸出結果(注意下圖并沒有顯示全連接層的節點連接)。

除了分類,添加一個全連接層也(一般)是學習這些特征的非線性組合的簡單方法。從卷積和池化層得到的大多數特征可能對分類任務有效,但這些特征的組合可能會更好。
從全連接層得到的輸出概率和為 1。這個可以在輸出層使用 softmax 作為激活函數進行保證。softmax 函數輸入一個任意大于 0 值的矢量,并把它們轉換為零一之間的數值矢量,其和為一。
Use Backpropagation to Train whole network
正如上面討論的,卷積 + 池化層的作用是從輸入圖像中提取特征,而全連接層的作用是分類器。
注意在下面的圖中,因為輸入的圖像是船,對于船這一類的目標概率是 1,而其他三類的目標概率是 0,即
輸入圖像 = 船
-
目標矢量 = [0, 0, 1, 0]
完整的卷積網絡的訓練過程可以總結如下:
- 第一步:我們初始化所有的濾波器,使用隨機值設置參數/權重
- 第二步:網絡接收一張訓練圖像作為輸入,通過前向傳播過程(卷積、ReLU 和池化操作,以及全連接層的前向傳播),找到各個類的輸出概率
- 我們假設船這張圖像的輸出概率是 [0.2, 0.4, 0.1, 0.3]
- 因為對于第一張訓練樣本的權重是隨機分配的,輸出的概率也是隨機的
- 第三步:在輸出層計算總誤差(計算 4 類的和)
- Total Error = ∑ ? (target probability – output probability) 2
- 第四步:使用反向傳播算法,根據網絡的權重計算誤差的梯度,并使用梯度下降算法更新所有濾波器的值/權重以及參數的值,使輸出誤差最小化
- 權重的更新與它們對總誤差的占比有關
- 當同樣的圖像再次作為輸入,這時的輸出概率可能會是 [0.1, 0.1, 0.7, 0.1],這就與目標矢量 [0, 0, 1, 0] 更接近了
- 這表明網絡已經通過調節權重/濾波器,可以正確對這張特定圖像的分類,這樣輸出的誤差就減小了
- 像濾波器數量、濾波器大小、網絡結構等這樣的參數,在第一步前都是固定的,在訓練過程中保持不變——僅僅是濾波器矩陣的值和連接權重在更新
- 第五步:對訓練數據中所有的圖像重復步驟 1 ~ 4
上面的這些步驟可以訓練 ConvNet —— 這本質上意味著對于訓練數據集中的圖像,ConvNet 在更新了所有權重和參數后,已經優化為可以對這些圖像進行正確分類。
當一張新的(未見過的)圖像作為 ConvNet 的輸入,網絡將會再次進行前向傳播過程,并輸出各個類別的概率(對于新的圖像,輸出概率是使用已經在前面訓練樣本上優化分類的參數進行計算的)。如果我們的訓練數據集非常的大,網絡將會(有希望)對新的圖像有很好的泛化,并把它們分到正確的類別中去。
注 1: 上面的步驟已經簡化,也避免了數學詳情,只為提供訓練過程的直觀內容。
注 2:在上面的例子中我們使用了兩組卷積和池化層。然而請記住,這些操作可以在一個 ConvNet 中重復多次。實際上,現在有些表現最好的 ConvNet 擁有多達十幾層的卷積和池化層!同時,每次卷積層后面不一定要有池化層。如下圖所示,我們可以在池化操作前連續使用多個卷積 + ReLU 操作。還有,請注意 ConvNet 的各層在下圖中是如何可視化的。

Visualization on CNN
一般而言,越多的卷積步驟,網絡可以學到的識別特征就越復雜。比如,ConvNet 的圖像分類可能在第一層從原始像素中檢測出邊緣,然后在第二層使用邊緣檢測簡單的形狀,接著使用這些形狀檢測更高級的特征,比如更高層的人臉。下面的圖中展示了這些內容——我們使用卷積深度置信網絡學習到的特征,這張圖僅僅是用來證明上面的內容(這僅僅是一個例子:真正的卷積濾波器可能會檢測到對我們毫無意義的物體)。

Adam Harley 創建了一個卷積神經網絡的可視化結果,使用的是 MNIST 手寫數字的訓練集。我強烈建議使用它來理解 CNN 的工作原理。
我們可以在下圖中看到網絡是如何識別輸入 「8」 的。注意下圖中的可視化并沒有單獨展示 ReLU 操作。

輸入圖像包含 1024 個像素(32 x 32 大小),第一個卷積層(卷積層 1)由六個獨特的 5x5 (步長為 1)的濾波器組成。如圖可見,使用六個不同的濾波器得到一個深度為六的特征圖。
卷積層 1 后面是池化層 1,在卷積層 1 得到的六個特征圖上分別進行 2x2 的最大池化(步長為 2)的操作。你可以在池化層上把鼠標移動到任意的像素上,觀察在前面卷積層(如上圖所示)得到的 4x4 的小格。你會發現 4x4 小格中的最大值(最亮)的像素將會進入到池化層。

池化層 1 后面的是六個 5x5 (步長為 1)的卷積濾波器,進行卷積操作。后面就是池化層 2,進行 2x2 的最大池化(步長為 2)的操作。這兩層的概念和前面描述的一樣。
接下來我們就到了三個全連接層。它們是:
- 第一個全連接層有 120 個神經元
- 第二層全連接層有 100 個神經元
- 第三個全連接層有 10 個神經元,對應 10 個數字——也就做輸出層
注意在下圖中,輸出層中的 10 個節點的各個都與第二個全連接層的所有 100 個節點相連(因此叫做全連接)。
同時,注意在輸出層那個唯一的亮的節點是如何對應于數字 “8” 的——這表明網絡把我們的手寫數字正確分類(越亮的節點表明從它得到的輸出值越高,即,8 是所有數字中概率最高的)。

同樣的 3D 可視化可以在這里看到。
Other ConvNet
卷積神經網絡從上世紀 90 年代初期開始出現。我們上面提到的 LeNet 是早期卷積神經網絡之一。其他有一定影響力的架構如下所示:
- LeNet (1990s): 本文已介紹。
- 1990s to 2012:在上世紀 90 年代后期至 2010 年初期,卷積神經網絡進入孵化期。隨著數據量和計算能力的逐漸發展,卷積神經網絡可以處理的問題變得越來越有趣。
- AlexNet (2012) – 在 2012,Alex Krizhevsky (與其他人)發布了 AlexNet,它是比 LeNet 更深更寬的版本,并在 2012 年的 ImageNet 大規模視覺識別大賽(ImageNet Large Scale Visual Recognition Challenge,ILSVRC)中以巨大優勢獲勝。這對于以前的方法具有巨大的突破,當前 CNN 大范圍的應用也是基于這個工作。
- ZF Net (2013) – ILSVRC 2013 的獲勝者是來自 Matthew Zeiler 和 Rob Fergus 的卷積神經網絡。它以 ZFNet (Zeiler & Fergus Net 的縮寫)出名。它是在 AlexNet 架構超參數上進行調整得到的效果提升。
- GoogLeNet (2014) – ILSVRC 2014 的獲勝者是來自于 Google 的 Szegedy等人的卷積神經網絡。它的主要貢獻在于使用了一個 Inception 模塊,可以大量減少網絡的參數個數(4M,AlexNet 有 60M 的參數)。
- VGGNet (2014) – 在 ILSVRC 2014 的領先者中有一個 VGGNet 的網絡。它的主要貢獻是展示了網絡的深度(層數)對于性能具有很大的影響。
- ResNets (2015) – 殘差網絡是何凱明(和其他人)開發的,并贏得 ILSVRC 2015 的冠軍。ResNets 是當前卷積神經網絡中最好的模型,也是實踐中使用 ConvNet 的默認選擇(截至到 2016 年五月)。
- DenseNet (2016 八月) – 近來由 Gao Huang (和其他人)發表的,the Densely Connected Convolutional Network 的各層都直接于其他層以前向的方式連接。DenseNet 在五種競爭積累的目標識別基準任務中,比以前最好的架構有顯著的提升。可以在這里看 Torch 實現。
CNN on TensorFlow
Tensorflow 在卷積和池化上有很強的靈活性。我們改如何處理邊界?步長應該設多大?在這個實例里,我們會一直使用 vanilla 版本。我們的卷積網絡選用步長(stride size)為 1,邊距(padding size)為 0 的模板,保證輸出和輸入是同一個大小(嚴格卷積)。我們的池化選用簡單傳統的 $2 \times 2$ 大小的模板作為 max pooling(最大池化)。為了使代碼更簡潔,我們把這部分抽象成一個函數:
def conv2d(x, W):
return tf.nn.conv2d(x, W, strides = [1, 1, 1, 1], padding = 'SAME')
def max_pool_2x2(x):
return tf.nn.max_pool(x, ksize = [1, 2, 2, 1], strides = [1, 2, 2, 1], padding = 'SAME')
Convolution Layer on TensorFlow
卷積操作是使用一個二維的卷積核在一個批處理的圖片上進行不斷掃描。具體操作就是將一個卷積和在每張圖片上按照一個合適的尺寸在每個通道上面進行掃描。為了達到更好的卷積效率,需要在不同的通道和不同的卷積核之間進行權衡。
-
conv2d
:任意的卷積核,能同時在不同的通道上面進行卷積操作。 -
depthwise_conv2d
:卷積核能相互獨立地在自己的通道上面進行卷積操作。 -
separable_conv2d
:在縱深卷積depthwise filter
之后進行逐點卷積separable filter
。
注意:雖然這些操作被稱之為「卷積」操作,但是嚴格地來說,他們只是互相關,因為卷積核沒有做一個逆向的卷積過程。
卷積核的卷積過程是按照 strides
參數來確定的,比如 strides = [1, 1, 1, 1]
表示卷積核對每個像素點進行卷積,即在二維屏幕上面,兩個軸方向的步長都是 1。strides = [1, 2, 2, 1]
表示卷積核對每隔一個像素點進行卷積,即在二維屏幕上面,兩個軸方向的步長都是 2。
如果我們暫不考慮通道這個因素,那么卷積操作的空間含義定義如下:如果輸入數據是一個四維的 input
,數據維度是[batch, in_height, in_width, ...]
,卷積核也是一個四維的卷積核,數據維度是[filter_height, filter_width, ...]
,那么,對于輸出數據的維度 shape(output)
,這取決于填充參數padding
的設置:
-
padding = 'SAME'
:向下取舍,僅適用于全尺寸操作,即輸入數據維度和輸出數據維度相同。out_height = ceil(float(in_height) / float(strides[1])) out_width = ceil(float(in_width) / float(strides[2]))
-
padding = 'VALID'
:向上取舍,適用于部分窗口,即輸入數據維度和輸出數據維度不同。out_height = ceil(float(in_height - filter_height + 1) / float(strides[1])) out_width = ceil(float(in_width - filter_width + 1) / float(strides[2]))
output[b, i, j, :] =
sum_{di, dj} input[b, strides[1] * i + di, strides[2] * j + dj, ...] *
filter[di, dj, ...]
因為,input
數據是一個四維的,每一個通道上面是一個向量input[b, i, j, :]
。對于conv2d
,這些向量會被卷積核filter[di, dj, :, :]
相乘而產生一個新的向量。對于depthwise_conv_2d
,每個標量分量input[b, i , j, k]
將在 k
個通道上面獨立地被卷積核 filter[di, dj, k]
進行卷積操作,然后把所有得到的向量進行連接組合成一個新的向量。
tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None, data_format=None, name=None)
這個函數的作用是對一個四維的輸入數據 input
和四維的卷積核 filter
進行操作,然后對輸入數據進行一個二維的卷積操作,最后得到卷積之后的結果。
給定的輸入 tensor 的維度是 [batch, in_height, in_width, in_channels]
,卷積核 tensor 的維度是[filter_height, filter_width, in_channels, out_channels]
,具體卷積操作如下:
- 將卷積核的維度轉換成一個二維的矩陣形狀
[filter_height * filter_width* in_channels, output_channels]
- 對于每個批處理的圖片,我們將輸入 tensor 轉換成一個臨時的數據維度
[batch, out_height, out_width, filter_height * filter_width * in_channels]
- 對于每個批處理的圖片,我們右乘以卷積核,得到最后的輸出結果。
更加具體的表示細節為,如果采用默認的 NHWC data_format形式:
output[b, i, j, k] =
sum_{di, dj, q} input[b, strides[1] * i + di, strides[2] * j + dj, q] *
filter[di, dj, q, k]
所以我們注意到,必須要有strides[0] = strides[3] = 1
。在大部分處理過程中,卷積核的水平移動步數和垂直移動步數是相同的,即strides = [1, stride, stride, 1]
。
使用例子:
import numpy as np
import tensorflow as tf
input_data = tf.Variable(np.random.rand(10, 6, 6, 3), dtype = np.float32)
filter_data = tf.Variable(np.random.rand(2, 2, 3, 1), dtype = np.float32)
y = tf.nn.conv2d(input_data, filter_data, strides = [1, 1, 1, 1], padding = 'SAME')
with tf.Session() as sess:
init = tf.initialize_all_variables()
sess.run(init)
print(sess.run(y))
print(sess.run(tf.shape(y)))
輸入參數:
-
input
: 一個Tensor
。數據類型必須是float32
或者float64
。 -
filter
: 一個Tensor
。數據類型必須是input
相同。 -
strides
: 一個長度是 4 的一維整數類型數組,每一維度對應的是 input 中每一維的對應移動步數,比如,strides[1]
對應input[1]
的移動步數。 -
padding
: 一個字符串,取值為SAME
或者VALID
。 -
use_cudnn_on_gpu
: 一個可選布爾值,默認情況下是True
。 -
data_format
:一個可選string
,NHWC
或者NCHW
。默認是用NHWC
。主要是規定了輸入 tensor 和輸出 tensor 的四維形式。如果使用NHWC
,則數據以[batch, in_height, in_width, in_channels]
存儲;如果使用NCHW
,則數據以[batch, in_channels, in_height, in_width]
存儲。 -
name
: (可選)為這個操作取一個名字。
輸出參數:
- 一個
Tensor
,數據類型是input
相同。
Pooling Layer on TensorFlow
池化操作是利用一個矩陣窗口在輸入張量上進行掃描,并且將每個矩陣窗口中的值通過取最大值,平均值或者其他方法來減少元素個數。每個池化操作的矩陣窗口大小是由 ksize
來指定的,并且根據步長參數 strides
來決定移動步長。比如,如果 strides
中的值都是1,那么每個矩陣窗口都將被使用。如果 strides
中的值都是2,那么每一維度上的矩陣窗口都是每隔一個被使用。以此類推。
更具體的輸出結果是:
output[i] = reduce( value[ strides * i: strides * i + ksize ] )
輸出數據維度是:
shape(output) = (shape(value) - ksize + 1) / strides
其中,取舍方向取決于參數 padding
:
-
padding = 'SAME'
: 向下取舍,僅適用于全尺寸操作,即輸入數據維度和輸出數據維度相同。 -
padding = 'VALID
: 向上取舍,適用于部分窗口,即輸入數據維度和輸出數據維度不同。
tf.nn.avg_pool(value, ksize, strides, padding , data_format='NHWC', name=None)
這個函數的作用是計算池化區域中元素的平均值。
使用例子:
import numpy as np
import tensorflow as tf
input_data = tf.Variable( np.random.rand(10,6,6,3), dtype = np.float32 )
filter_data = tf.Variable( np.random.rand(2, 2, 3, 10), dtype = np.float32)
y = tf.nn.conv2d(input_data, filter_data, strides = [1, 1, 1, 1], padding = 'SAME')
output = tf.nn.avg_pool(value = y, ksize = [1, 2, 2, 1], strides = [1, 1, 1, 1], padding = 'SAME')
with tf.Session() as sess:
init = tf.initialize_all_variables()
sess.run(init)
print(sess.run(output))
print(sess.run(tf.shape(output)))
輸入參數:
-
value
: 一個四維的Tensor
。數據維度是[batch, height, width, channels]
。數據類型是float32
,float64
,qint8
,quint8
,qint32
。 -
ksize
: 一個長度不小于 4 的整型數組。每一位上面的值對應于輸入數據張量中每一維的窗口對應值。 -
strides
: 一個長度不小于 4 的整型數組。該參數指定滑動窗口在輸入數據張量每一維上面的步長。 -
padding
: 一個字符串,取值為SAME
或者VALID
。 -
data_format
:一個可選string
,NHWC
或者NCHW
。默認是用NHWC
。主要是規定了輸入 tensor 和輸出 tensor 的四維形式。如果使用NHWC
,則數據以[batch, in_height, in_width, in_channels]
存儲;如果使用NCHW
,則數據以[batch, in_channels, in_height, in_width]
存儲。 -
name
: (可選)為這個操作取一個名字。
輸出參數:
- 一個Tensor,數據類型和value相同。
tf.nn.max_pool(value, ksize, strides, padding, data_format='NHWC', name=None)
這個函數的作用是計算 pooling 區域中元素的最大值。
tf.nn.max_pool_with_argmax(input, ksize, strides, padding, Targmax=None, name=None)
這個函數的作用是計算池化區域中元素的最大值和該最大值所在的位置。
因為在計算位置 argmax
的時候,我們將 input
鋪平了進行計算,所以,如果 input = [b, y, x, c]
,那么索引位置是 `( ( b * height + y ) * width + x ) * channels + c
查看源碼,該API只能在GPU環境下使用,所以我沒有測試下面的使用例子,如果你可以測試,請告訴我程序是否可以運行。
源碼展示:
REGISTER_KERNEL_BUILDER(Name("MaxPoolWithArgmax")
.Device(DEVICE_GPU)
.TypeConstraint<int64>("Targmax")
.TypeConstraint<float>("T"),
MaxPoolingWithArgmaxOp<Eigen::GpuDevice, float>);
REGISTER_KERNEL_BUILDER(Name("MaxPoolWithArgmax")
.Device(DEVICE_GPU)
.TypeConstraint<int64>("Targmax")
.TypeConstraint<Eigen::half>("T"),
MaxPoolingWithArgmaxOp<Eigen::GpuDevice, Eigen::half>);
使用例子:
import numpy as np
import tensorflow as tf
input_data = tf.Variable( np.random.rand(10,6,6,3), dtype = tf.float32 )
filter_data = tf.Variable( np.random.rand(2, 2, 3, 10), dtype = np.float32)
y = tf.nn.conv2d(input_data, filter_data, strides = [1, 1, 1, 1], padding = 'SAME')
output, argmax = tf.nn.max_pool_with_argmax(input = y, ksize = [1, 2, 2, 1], strides = [1, 1, 1, 1], padding = 'SAME')
with tf.Session() as sess:
init = tf.initialize_all_variables()
sess.run(init)
print(sess.run(output))
print(sess.run(tf.shape(output)))
輸入參數:
-
input
: 一個四維的Tensor
。數據維度是[batch, height, width, channels]
。數據類型是float32
。 -
ksize
: 一個長度不小于 4 的整型數組。每一位上面的值對應于輸入數據張量中每一維的窗口對應值。 -
strides
: 一個長度不小于 4 的整型數組。該參數指定滑動窗口在輸入數據張量每一維上面的步長。 -
padding
: 一個字符串,取值為SAME
或者VALID
。 -
Targmax
: 一個可選的數據類型:tf.int32
或者tf.int64
。默認情況下是tf.int64
。 -
name
: (可選)為這個操作取一個名字。
輸出參數:
一個元祖張量 (output, argmax)
:
-
output
: 一個Tensor
,數據類型是float32
。表示池化區域的最大值。 -
argmax
: 一個Tensor
,數據類型是Targmax
。數據維度是四維的。
Weight Initialization
所以,為了創建這個模型,我們需要創建大量的權重和偏置項,這個模型中的權重在初始化的時候應該加入少量的噪聲來打破對稱性以及避免 0 梯度。由于我們使用的是 ReLU 神經元,因此比較好的做法是用一個較小的正數來初始化偏置項,以避免神經元節點輸出恒為 0 的問題(dead neurons)。為了不再建立模型的時候反復做初始化操作,我們定義兩個函數用于初始化。
def conv2d(x, W):
return tf.nn.conv2d(x, W, strides = [1, 1, 1, 1], padding = 'SAME')
def max_pool_2x2(x):
return tf.nn.max_pool(x, ksize = [1, 2, 2, 1], strides = [1, 2, 2, 1], padding = 'SAME')
def weight_variable(shape):
initial = tf.truncated_normal(shape, stddev = 0.1)
return tf.Variable(initial)
def bias_variable(shape):
initial = tf.constant(0.1, shape = shape)
return tf.Variable(initial)
第一層
接下來,我們開始實現第一層。它由一個卷積層接一個 max_pooling 最大池化層完成。卷積在每個 5x5 的 patch 中算出 32 個特征。卷積的權重張量形狀是 [5, 5, 1, 32]
,前兩個維度是 patch 的大小,接著是輸入的通道數目,最后是輸出的通道數目。而對于每一個輸出通道都有一個對應的偏置量。
W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])
為了用這一層,我們把 x 變成一個 4d 的向量,其第 2、第 3 維對應圖片的寬度、高度,最后一位代表圖片的顏色通道(因為是灰度圖,所以這里的通道數為 1,如果是 RBG 彩色圖,則為 3)。
x_image = tf.reshape(x, [-1, 28, 28, 1])
之后,我們把 x_image 和權值向量進行卷積,加上偏置項,然后應用 ReLU 激活函數,最后進行 max pooling。
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)
第二層
為了構建一個更深的網絡,我們會把幾個類似的層堆疊起來。第二層中,每個 5x5 的 patch 會得到 64 個特征。
W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)
密集連接層
現在,圖片尺寸減小到 7x7,我們加入一個有 1024 個神經元的全連接層,用于處理整個圖片。我們把池化層輸出的張量 reshape 成一些向量,乘上權重矩陣,加上偏置,然后對其使用 ReLU。
W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])
h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
Dropout
為了減少過擬合,我們在輸出層之前加入dropout。我們用一個 placeholder 來代表一個神經元的輸出在 dropout 中保持不變的概率。這樣我們可以在訓練過程中啟用 dropout,在測試過程中關閉 dropout。 TensorFlow 的tf.nn.dropout 操作除了可以屏蔽神經元的輸出外,還會自動處理神經元輸出值的 scale。所以用 dropout 的時候可以不用考慮 scale。
keep_prob = tf.placeholder("float")
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
輸出層
最后我們添加一個 softmax 層,就像前面的單層 softmax regression 一樣。
W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])
y_conv=tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)
訓練和評估模型
這個模型的效果如何呢?
為了進行訓練和評估,我們使用與之前簡單的單層 SoftMax 神經網絡模型幾乎相同的一套代碼,只是我們會用更加復雜的 ADAM 優化器來做梯度最速下降,在 feed_dict 中加入額外的參數 keep_prob 來控制 dropout 比例。然后每 100 次迭代輸出一次日志。
cross_entropy = -tf.reduce_sum(y_*tf.log(y_conv))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
sess.run(tf.initialize_all_variables())
for i in range(20000):
batch = mnist.train.next_batch(50)
if i%100 == 0:
train_accuracy = accuracy.eval(feed_dict={
x:batch[0], y_: batch[1], keep_prob: 1.0})
print "step %d, training accuracy %g"%(i, train_accuracy)
train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})
print "test accuracy %g"%accuracy.eval(feed_dict={
x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0})