GoogLeNet是谷歌(Google)研究出來的深度網絡結構,為什么不叫“GoogleNet”,而叫“GoogLeNet”,據說是為了向“LeNet”致敬,因此取名為“GoogLeNet”,所以我們這里題目就叫GoogLeNet。后面我們為了方便就叫inception Net。
Google Inception Net 首次出現在 ILSVRC 2014的比賽中(和VGGNet 同年),就以較大優勢取得了第一名。那一屆比賽中的 Inception Net 通常被稱為inception V1,它最大的特點就是控制了計算量和參數量的同時,獲得了非常好的分類性能——top-5 錯誤率 6.67%,只有 AlexNet的一半不到。Inception V1 有22 層深,比 AlexNet的8層或者 VGGNet的19層還要更深。但其大小卻比AlexNet和VGG小很多,計算量只有 15億次浮點運算,同時只有500萬的參數量,僅為 AlexNet 參數量(6000萬)的 1/12,卻可以達到遠勝於 AlexNet的准確率,可以說是非常優秀且非常實用的模型。因此在內存或計算資源有限時,GoogLeNet是比較好的選擇;從模型結果來看,GoogLeNet的性能更加優越。
1,GoogLeNet 是如何進一步提升性能?
GoogLeNet 帶來的性能提升很大程度上要歸功於“降維”,也就是卷積分解的一種。考慮到網絡鄰近的激活單元高度相關,因此聚合之前進行降維可以得到類似於局部特征的東西。接下來主要討論其他的卷積分解方法。既然Inception網絡是全卷積,卷積計算變少也就意味着計算量變小,這些多出來的計算資源可以來增加 filter-bank 的尺寸大小。
一般來說,提升網絡性能最直接的辦法就是增加網絡深度和寬度,深度指網絡層次數量、寬度指神經元數量。但這種方式存在以下問題:
- (1)參數太多,如果訓練數據集有限,很容易產生過擬合;
- (2)網絡越大、參數越多,計算復雜度越大,難以應用;
- (3)網絡越深,容易出現梯度彌散問題(梯度越往后穿越容易消失),難以優化模型。
解決這些問題的方法當然就是在增加網絡深度和寬度的同時減少參數,為了減少參數,自然就想到將全連接變成稀疏連接。但是在實現上,全連接變成稀疏連接后實際計算量並不會有質的提升,因為大部分硬件是針對密集矩陣計算優化的,稀疏矩陣雖然數據量少,但是計算所消耗的時間卻很難減少。
如何減少參數?
- 第一步通過2個3*3的卷積核來代替一個5*5的卷積核,感受野相同的情況下,兩個3*3的卷積核的參數為2*3*3=18,而5*5卷積核的參數為25個;
- 在卷積之前通過1*1的卷積核來降低feature map維度,之后再卷積;
- 將n*n的卷積核替換為1*n和n*1兩個卷積核。
2,GoogLeNet 的特點
2.1,參數更少
GoogLeNet 參數為500萬個,AlexNet參數個數為 GoogLeNet 的12倍,VGGNet參數又是 AlexNet的3倍。
2.2,性能更好
占用更少的內存和計算資源,且模型結果的性能卻更加優越。
Inception 歷經了 V1,V2,V3,V4等多個版本的發展,不斷趨於完善,下面一一進行介紹。
3,稀疏結構和Hebbian原理的學習
人腦神經元的連接是稀疏的,因此研究者認為大型神經網絡的合理連接方式應該也是稀疏的。稀疏結構是非常適合神經網絡的一種結構,尤其是對非常大型,非常深的神經網絡,可以減輕過擬合並降低計算量,例如卷積神經網絡就是稀疏的連接。Inception Net的主要目標就是找到最優的稀疏結構單元(即Inception Module),論文中提到其稀疏結構基於 Hebbian原理,這里簡單解釋一下Hebbian原理:神經反射活動的持續與重復會導致神經元連接穩定性的持久提升,當兩個神經元細胞 A 和B 距離很近,並且A 參與了對B重復,持續的興奮,那么某些代謝會導致A將作為能使B興奮的細胞。總結一下即“一起發射的神經元會連接一起”(Cells that fire together, were together),學習過程中的刺激會使神經元間的突觸強度增加。受 Hebbian原理啟發,另一篇文章 Provable Bounds for learning Some Deep Representations 提出,如果數據集的概率分布可以被一個很大很稀疏的神經網絡所表達,那么構築這個網絡的最佳方法時逐層構築網絡:將上一層高度相關(correlated)的節點聚類,並將聚類出來的每一個小簇(cluster)連接到一起,如下圖所示,這個相關性高的節點應該被連接在一起的結論,即使從神經網絡的角度對 Hebbian 原理有效性的證明。
因此一個“好”的稀疏結構,應該是符合 Hebbian原理的,我們應該把相關性高的一簇神經元節點連接在一起。在普通的數據集中,這可能需要對神經元節點聚類,但是在圖片數據中,天然的就是臨近區域的數據相關性高,因此相鄰的像素點被卷積操作連接在一起。而我們可能有多個卷積核,在同一空間位置但在不同通道的卷積核的輸出結果相關性極高。因此,一個1*1的卷積就可以很自然的把這些相關性很高的,在同一個空間位置但是不同通道的特征連接在一起,這就是為什么1*1卷積這么頻繁的被應用到 Inception Net 中的原因。1*1 卷積所連接的節點的相關性是最高的,而稍微大一點尺寸的卷積,比如 3*3 5*5 的卷積所連接的節點的相關性是最高的,而稍微大一點的卷積,比如 3*3,5*5的卷積所連接的節點相關性也很高,因此也可以適當地使用一些大尺寸的卷積,增加多樣性(diversity)。最后 Inception Module 通過4個分支中不同尺寸的 1*1 3*3 5*5 等小型卷積將相關性很高的節點連接在一起,就完成了其設計初衷,構建出了很高效的符合 Hebbian原理的稀疏結構。
在Inception Module 中,通常 1*1 卷積的比例(輸出通道數占比)最高,3*3 卷積和 5*5 卷積稍低。而在整個網絡中,會有多個堆疊的 Inception Module ,我們希望靠后的 Inception Module 可以捕捉更高階的抽象特征,因此靠后的 Inception Module 的卷積的空間幾何度應該逐漸降低,這樣可以捕獲更大面積的特征。因此,越靠后的 Inception Module 中,3*3 5*5 這兩個大面積的 卷積核的占比(輸出通道數)應該更多。
inception Net 有22層深,除了最后一層的輸出,其中間節點的分類效果也很好。因此在 Inception Net中,還使用到了輔助分類節點(auxiliary classifiers),即將中間某一層的輸出用作分類,並按一個較小的權重(0.3)加到最終分類結果中。這樣相當於做了模型融合,同時給網絡增加了反向傳播的梯度信息,也提供了額外的正則化,對於整個 Inception Net的訓練很有裨益。
當年的 Inception V1還是跑在TensorFlow 的前輩 DistBelief 上的,並且只允許在CPU上,當時使用了異步的 SGD 訓練,學習速率每迭代8個epoch 降低4%,同時,Inception V1也使用了 Multi-Scale,Multi-Crop 登上護具增強方法,並在不同的采樣數據上訓練了7個模型進行融合,得到了最后的 ILSVRC
2014 的比賽成績——top -5 錯誤率 6.67%。
4,inception V1
通過設計一個稀疏網絡結構,但是能夠產生稠密的數據,既能增加神經網絡表現,又能保證計算資源的使用效率。谷歌提出了最原始的Inception的基本結構:
該結構將CNN 中常用的卷積(1*1,3*3, 5*5),池化操作(3*3)堆疊在一起(卷積,池化后的尺寸相同,將通道相加),一方面增加了網絡的寬度,另一方面也增加了網絡對尺寸的適應性。
網絡卷積層中的網絡能夠提取輸入的每一個細節信息,同時 5*5 的濾波器也能夠覆蓋大部分接受層的輸入。還可以進行一個池化操作,以減少空間大小,降低過度擬合。在這些層之上,在每一個卷積層后都要做一個ReLU操作,以增加網絡的非線性特征。
然而這個Inception原始版本,所有的卷積核都在上一層的所有輸出上來做,而那個5*5的卷積核所需要的計算量就太大了,造成了特征圖的厚度很大,為了避免這種情況,在3*3, 5*5前,max_pooling 后分別加上了 1*1 的卷積核,以起到了降低特征圖厚度的作用,這也就形成了 Inception V1的網絡結構,如下圖所示:
對上面的 inception模塊的四個並行線路解釋如下:
1.一個 1 x 1 的卷積,一個小的感受野進行卷積提取特征 2.一個 1 x 1 的卷積加上一個 3 x 3 的卷積,1 x 1 的卷積降低輸入的特征 通道,減少參數計算量,然后接一個 3 x 3 的卷積做一個較大感受野的卷積 3.一個 1 x 1 的卷積加上一個 5 x 5 的卷積,作用和第二個一樣 4.一個 3 x 3 的最大池化加上 1 x 1 的卷積,最大池化改變輸入的特征排列, 1 x 1 的卷積進行特征提取
下面學習 Inception Module 的基本結構,其中有4個分支:第一個分支對輸入進行 1*1 的卷積,這其實也是 NIN 中提出的一個重要結構,1*1 的卷積是一個非常優秀的結構,它可以跨通道組織信息,提高網絡的表達能力,同時可以對輸出通道升維和降維。可以看到 Inception Module 的四個分支都用到了 1*1 卷積,來進行低成本(計算量比 3*3 小很多)的跨通道的特征變換。第二個分支先使用了 1*1卷積,然后連接 3*3 卷積,相當於進行了兩次特征變換。第三個分支類似,先是 1*1 卷積。然后連接 5*5 卷積,最后一個分支則是 3*3 最大池化后直接使用 1*1 卷積。我們可以發現,有的分支只使用 1*1 卷積,有的分支使用了其他尺寸的卷積時也會再使用 1*1 卷積,這是因為 1*1 卷積的性價比很高,用很小的計算量就能增加一層特征變換和非線性化, Inception Module 的四個分支在最后通過一個聚合操作合並(在輸出通道數這個維度上聚合)。 Inception Module 的四個分支在最后通過一個聚合操作合並(在輸出通道數這個維度上聚合)。 Inception Module 中包含了3種不同尺寸的卷積和1個最大池化,增加了網絡對不同尺度的適應性,這一部分和 Multi-Scale 的思想類似。早期計算機視覺的研究中,受靈長類神經視覺系統的啟發,Serre 使用不同尺寸的 Gabor 濾波器處理不同的圖片,Inception V1借鑒了這種思想。inception V1的論文中指出, Inception Module 可以讓網絡的深度和寬度高效果地擴充,提升准確率且不至於過擬合。
4.1 輔助分類器
inception v1 引入了輔助分類器的概念,以改善非常深的網絡的收斂。最初的動機是將有用的梯度推向較低層,使其立即有用,並通過抵抗非常深的網絡中的消失梯度問題來提高訓練過程中的收斂。有趣的是,我們發現輔助分類器在訓練早期並沒有導致改善收斂:在兩個模型達到高精度之前,有無側邊網絡的訓練進度看起來幾乎相同。接近訓練結束,輔助分支網絡開始超越沒有任何分支的網絡的准確性,達到了更高的穩定水平。
另外,inception V1在網絡的不同階段使用了兩個側分支。移除更下面的輔助分支對網絡的最終質量沒有任何不利影響。再加上前一段的觀察結果,這意味着這些分支有助於演變低級特征很可能是不適當的。相反,我們認為輔助分類器起着正則化項的作用。這是由於如果側分枝是批標准化的(BN)或具有丟棄層(Dropout),則網絡的主分類器性能更好。這也為推測BN作為正則化項給出了一個弱支持證據。
4.2 1*1 的卷積核有什么用呢?
GoogLeNet性能優異很大程度在於使用了降維。降維可以看做卷積網絡的因式分解。例如1*1 卷積層后跟着 3*3卷積層。
1*1 卷積的主要目的是為了減少維度,還用於修正線性激活(ReLU)。比如,上一層的輸出為100*100*128,經過具有256個通道的5*5卷積層之后(stride=1, pad=2),輸出數據為100*100*256,其中,卷積層的參數為 128*5*5*256=819200。而加入上一層輸出先經過具有32個通道的1*1卷積層,再經過具有 256 個輸出的 5*5 卷積層,那么輸出數據仍為 100*100*256,但卷積參數量已經減少為 128*1*1*32 + 32*5*5*256 = 204800,大約減少了 4倍。
4.3 Inception V1降低參數量的目的
1,參數越多模型越龐大,需要供模型學習的數據量就越大,而且目前高質量的數據非常昂貴;
2,參數越多,耗費的計算資源也會更大;
inception V1 參數少但是效果好的原因除了模型層數更深,表達能力更強外,還有兩點:一是去除最后的全連接層,用全局平均池化層(即將圖片尺寸變為1*1)來取代它。全連接層幾乎占據了 AlexNet 或者 VGGNet中90%的參數量,而且會引起過擬合,去除全連接層后模型訓練更快並且減輕了過擬合。用全局平均池化層取代全連接層的做法借鑒了 Network in Network(以下簡稱 NIN)論文。二是 Inception V1中精心設計的 Inception Module 提高了參數的利用效率,其結構如圖所示。這一部分也借鑒了NIN的思想,形象的解釋就是 inception Module本身如同大網絡中的一個小網絡,其結構可以反復堆疊在一起形成大網絡。不過 Inception V1比 NIN 更進一步的時增加了分支網絡,NIN則主要是級聯的卷積層和 MLPConv層。一般來說卷積層要提升表達能力,主要依靠增加輸出通道數,但副作用是計算量增大和過擬合。每一個輸出通道對應一個濾波器,同一個濾波器共享參數,只能提取一類特征,因此一個輸出通道只能做一種特征處理。而 NIN中的 MLPConv 則擁有更強大的能力,允許在輸出通道之間組合信息,因此效果明顯。可以說,MLPConv 基本等效於普通卷積層后再連接 1*1 的卷積和 ReLU激活函數。
基於 Inception 構建了 GoogLeNet 的網絡結構如下(共 22 層):
對上圖說明如下:
(1) GoogLeNet 采用了模塊化的結構(Inception結構),方便增添和修改;
(2)網絡最后采用了 average pooling (平均池化)來代替全連接層,該想法來自於 NIN(Network in Network),事實證明這樣可以將准確率提高 0.6%。但是,實際在最后還是加了一個全連接層,主要是為了方便對輸出進行靈活調整;
(3)雖然移除了全連接層,但是網絡中依然使用了Dropout;
(4)為了避免梯度小時,網絡額外增加兩個輔助的 softmax 用於前向傳導梯度(輔助分類器)。輔助分類器是將中間某一層的輸出用作分類,並按一個較小的權重(0.3)加到最終分類結果中,這樣相當於做了模型融合,同時給網絡增加了反向傳播的梯度信號,也提供了額外的正則化,對於整個網絡的訓練很有裨益。而在實際測試的時候,這兩個額外的 softmax 會被去掉。
所以總結來說就是:inception V1 參數少但是效果好的原因除了模型層數更深,表達能力更強外,還有兩點:一是去除最后的全連接層,用全局平均池化層(即將圖片尺寸變為1*1)來取代它。全連接層幾乎占據了 AlexNet 或者 VGGNet中90%的參數量,而且會引起過擬合,去除全連接層后模型訓練更快並且減輕了過擬合。用全局平均池化層取代全連接層的做法借鑒了 Network in Network(以下簡稱 NIN)論文。二是 Inception V1中精心設計的 Inception Module 提高了參數的利用效率,其結構如上圖所示。這一部分也借鑒了NIN的思想,形象的解釋就是 inception Module本身如同大網絡中的一個小網絡,其結構可以反復堆疊在一起形成大網絡。不過 Inception V1比 NIN 更進一步的時增加了分支網絡,NIN則主要是級聯的卷積層和 MLPConv層。一般來說卷積層要提升表達能力,主要依靠增加輸出通道數,但副作用是計算量增大和過擬合。每一個輸出通道對應一個濾波器,同一個濾波器共享參數,只能提取一類特征,因此一個輸出通道只能做一種特征處理。而 NIN中的 MLPConv 則擁有更強大的能力,允許在輸出通道之間組合信息,因此效果明顯。可以說,MLPConv 基本等效於普通卷積層后再連接 1*1 的卷積和 ReLU激活函數。
GoogLeNet 的網絡結構圖細節如下:
注意:上表的“#3*3 reduce”,“# 5*5 reduce” 表示在 3*3 , 5*5 卷積操作之前使用了 1*1 卷積的數量。
4.3 GoogLeNet 網絡結構明細表解析
0、輸入
原始輸入圖像為224x224x3,且都進行了零均值化的預處理操作(圖像每個像素減去均值)。
1、第一層(卷積層)
使用7x7的卷積核(滑動步長2,padding為3),64通道,輸出為112x112x64,卷積后進行ReLU操作
經過3x3的max pooling(步長為2),輸出為((112 - 3+1)/2)+1=56,即56x56x64,再進行ReLU操作
2、第二層(卷積層)
使用3x3的卷積核(滑動步長為1,padding為1),192通道,輸出為56x56x192,卷積后進行ReLU操作
經過3x3的max pooling(步長為2),輸出為((56 - 3+1)/2)+1=28,即28x28x192,再進行ReLU操作
3a、第三層(Inception 3a層)
分為四個分支,采用不同尺度的卷積核來進行處理
(1)64個1x1的卷積核,然后RuLU,輸出28x28x64
(2)96個1x1的卷積核,作為3x3卷積核之前的降維,變成28x28x96,然后進行ReLU計算,再進行128個3x3的卷積(padding為1),輸出28x28x128
(3)16個1x1的卷積核,作為5x5卷積核之前的降維,變成28x28x16,進行ReLU計算后,再進行32個5x5的卷積(padding為2),輸出28x28x32
(4)pool層,使用3x3的核(padding為1),輸出28x28x192,然后進行32個1x1的卷積,輸出28x28x32。
將四個結果進行連接,對這四部分輸出結果的第三維並聯,即64+128+32+32=256,最終輸出28x28x256
3b、第三層(Inception 3b層)
(1)128個1x1的卷積核,然后RuLU,輸出28x28x128
(2)128個1x1的卷積核,作為3x3卷積核之前的降維,變成28x28x128,進行ReLU,再進行192個3x3的卷積(padding為1),輸出28x28x192
(3)32個1x1的卷積核,作為5x5卷積核之前的降維,變成28x28x32,進行ReLU計算后,再進行96個5x5的卷積(padding為2),輸出28x28x96
(4)pool層,使用3x3的核(padding為1),輸出28x28x256,然后進行64個1x1的卷積,輸出28x28x64。
將四個結果進行連接,對這四部分輸出結果的第三維並聯,即128+192+96+64=480,最終輸出輸出為28x28x480
第四層(4a,4b,4c,4d,4e)、第五層(5a,5b)……,與3a、3b類似,在此就不再重復。
從GoogLeNet 的實驗結果來看,效果很明顯,差錯率比 MSRA,VGG等模型都要低,對比結果如下圖:
5,Inception V2
GoogLeNet憑借其優秀的表現,得到了很多研究人員的學習和使用,因此GoogLeNet團隊又對其進行了進一步地發掘改進,產生了升級版本的GoogLeNet。GoogLeNet設計的初衷就是要又准又快,而如果只是單純的堆疊網絡雖然可以提高准確率,但是會導致計算效率有明顯的下降,所以如何在不增加過多計算量的同時提高網絡的表達能力就成為了一個問題。
Inception V2版本的解決方案就是修改Inception的內部計算邏輯,提出了比較特殊的“卷積”計算結構。
inception V2學習了VGGNet,用兩個 3*3 的卷積代替了 5*5 的大卷積(用以降低參數量並減輕過擬合),還提出了 Batch Normalization(以下簡稱 BN)方法。BN 是一個非常有效地正則化方法,可以讓大型卷積網絡的訓練速度加快很多倍,同時收斂后的分類准確率也可以得到大幅的提高。BN在用於神經網絡某層時,會對每一個 mini-batch 數據的內部進行標准化(normalization)處理,使輸出規范化到 N(0,1)的正態分布,減少了 Internal Convarate Shift(內部神經元分布的改變)。BN 的論文指出,傳統的深度神經網絡在訓練時,每一層的輸入的分布都在變化,導致訓練變得困難,我們只能使用一個很小的學習速率解決這個問題。而對每一層使用BN之后,我們就可以有幸的解決這個問題,學習速率可以增大很多倍,達到之前的准確率所需要的迭代次數只有 1/14,訓練時間大大縮短。而達到之前的准確率后,可以繼續訓練,並最終遠超於 Inception V1模型的性能——Top-5 錯誤率 4.8%,已經優於人眼水平。因為BN某種意義上還起到了正則化的作用。所以可以減少或者取消 Dropout,簡化網絡結構。
當然,只是單純的使用 BN獲得的增益還不明顯,還需要一些相應的調整:增大學習速率並加快學習衰減速度以適用 BN 規范化后的數據;去掉 Dropout並減輕 L2 正則(因 BN已起到正則化的作用);去掉 LRN;更徹底地對訓練樣本進行 shuffle;減少數據增強過程中隊數據的光學畸變(因為BN訓練更快,每個樣本被訓練的次數更少,因此更真實的樣本對訓練更有幫助)。在使用了這些措施后,Inception V2在訓練達到 Inception V1的准確率時快了14倍,並且模型在收斂時的准確率上限更高。
5.1,卷積分解(Factorizing Convolutions)
卷積核大,計算量也是平方的增大。大尺寸的卷積核可以帶來更大的感受野,但也意味着會產生更多的參數,比如5x5卷積核的參數有25個,3x3卷積核的參數有9個,前者是后者的25/9=2.78倍。因此,GoogLeNet團隊提出可以用2個連續的3x3卷積層組成的小網絡來代替單個的5x5卷積層,即在保持感受野范圍的同時又減少了參數量,雖然5*5的卷積可以捕捉到更多的臨近關聯信息,但是兩個3*3組合起來,能觀察到的“視野” 就和 5*5 的一樣了,如下圖:
那么這種替代方案會造成表達能力的下降嗎?通過大量的實驗表明,並不會造成表達缺失。
可以看出,大卷積核完全可以由一系列的3*3 卷積核來替代,那能不能再分解得更小一些呢?GoogLeNet團隊考慮了 n*1 的卷積核,其實,對於分解的卷積層,實驗表明非線性激活比線性激活更好,所以,以上的卷積分解還不是最優策略,3*3卷積還可以進一步分解成1*3 和 3*1 ,兩個卷積分別捕捉不同方向的而信息,參數只有之前的6/9。其實,這個可以推廣到 n*n 卷積的情況,n*n 卷積因式分解為 1*n 和 n*1。這個方法在網絡前面部分似乎表現欠佳,但在中間層起到很好的效果。
上述結果表明,大於3*3的卷積濾波器可能不是通常有用的,因為他們總是可以簡化為3*3卷積層序列。我們仍然可以問這個問題,是否應該把他們分解成更小的,例如2*2的卷積。然而,通過使用非對稱卷積,可以做出甚至比2*2更好的效果,即 n*1。例如使用3*1卷積后接一個1*3卷積,相當於以與3*3卷積相同的感受野滑動兩層網絡(如下圖)。如果輸入和輸出老濾波器的數量相等,那么對於相同數量的輸出濾波器,兩層解決方案便宜33%。相比之下,將3*3卷積分解為兩個2*2卷積表示僅節省了11%的計算量。
如下圖所示,用 3個 3*1 取代 3*3 卷積:
在理論上,我們可以進一步論證,可以通過1*n 卷積和后面接一個 n*1 卷積替換任何 n*n 卷積,並且隨着 n 增長,計算成本節省顯著增加(如下圖所示)。實際上,GoogLeNet團隊發現在網絡的前期試驗這種分解效果並不好,但是對於中等網格尺寸(在 m*m 特征圖上,其中 m 范圍在12 到20之間),其給出了非常好的結果。在這個水平上,通過使用 1*7 卷積,然后是 7*1 卷積可以獲得非常好的結果。
5.2,降低特征圖尺寸
假設有一個 d*d*k 的特征圖,為了轉換成 d/2 * d/2 *2k 大小,可以先用 1*1 卷積變成 d*d*2k,再進行池化,這樣的計算量很大,而先池化再增加通道則會出現 representational bottlenecks 的問題。
傳統上,卷積網絡使用一些池化操作來縮減特種圖的網絡大小。為了避免表示瓶頸,在應用最大池化或平均池化之前,需要擴展網絡濾波器的激活維度。例如,開始需要一個帶有K個濾波器的 d*d 網絡,如果我們想要達到一個帶有 2k 個濾波器的 d/2 * d/2 網格,我們首先需要用 2k 個濾波器計算步長為1的卷積,然后應用一個額外的池化步驟。這意味着總體計算成本由在較大的網格上使用 2d2k2 次運算的昂貴卷積支配。一種可能性是轉換為帶有卷積的池化,因此導致 2(d/2)2k2 次運算,將計算成本降低為原來的四分之一。然而,由於表示的整體維度下降到 (d/2)2k ,會導致表示能力較弱的網格(如下圖所示)。這會產生一個表示瓶頸。我們建議另一種變體,其甚至進一步降低了計算成本,同時消除了表示瓶頸(下下圖所示),而不是這樣做。我們可以使用兩個平行的步長為2的塊:P和C。P是一個池化層(平均池化或最大池化)的激活,兩者都是步長為2。
一般情況下,如果想讓圖像縮小,可以有如下兩種方式:
先池化再作Inception卷積,或者先作Inception卷積再作池化。但是方法一(左圖)先作pooling(池化)會導致特征表示遇到瓶頸(特征缺失),方法二(右圖)是正常的縮小,但計算量很大(右邊的計算量昂貴3倍)。為了同時保持特征表示且降低計算量,將網絡結構改為下圖,使用兩個並行化的模塊來降低計算量(卷積、池化並行執行,再進行合並)。
使用 Inception V2作改進版的 GoogLeNet,網絡結構如下:
注意:上表中的 Figure 5 指的時沒有進化的 Inception ,Figure 6 指的時小卷積版的 Inception (用 3*3 卷積核代替 5*5 卷積核),Figure 7 是指不對稱版的 Inception(用1*n, n*1 卷積核代替 n*n 卷積核)。把7*7卷積替換為3個3*3卷積。包含3個inception部分。第一部分為35*35*288,使用了2個3*3 卷積代替了傳統的 5*5 ;第二部分減少了 feature map,增多了 filters,為17*17*768,使用了 n*1 ---> 1*n 結構,第三部分多了 filter,使用了卷積池化並行結構。網絡有42層,但是計算量只有GoogleNet 的2.5 倍。
經試驗,模型結果與舊的 GoogLeNet 相比有較大的提升,如下表所示:
6,Inception V3
Inception V3 網絡則主要有兩方面的改造:一是引入了分解( Factorization into small convilutions )的思想,將一個較大的二維卷積拆成兩個較小的一維卷積,比如將 7*7 卷積拆成 1*7 卷積和 7*1卷積,或者將 3*3 卷積拆成 1*3 卷積和 3*3 卷積,如下圖所示,一方面節省了大量參數,加速運算並減輕了過擬合(比將7*7 卷積拆成1*7 卷積和 7*1 卷積,比拆成3個3*3 卷積更節省參數),同時增加了一層非線性擴展模型表達能力。論文中指出,這種非對稱的卷積結構拆分,其結果比對稱的拆為幾個相同的曉娟及核下過更明顯,可以處理更多,更豐富的空間特征,增加特征多樣性。
另一方面,Inception V3優化了Inception Module的結構,現在 Inception Module有 35*35,17*17和8*8三種不同的結構,如下圖所示,這些 Inception Module只在網絡的后部出現,前部還是普通的卷積層。並且 Inception V3除了在Inception Module中使用分支,還在分支中使用了分支(8*8 的結構中),可以說是 Network In Network In Network,網絡輸入從224x224變為了299x299。
分析:因此問題依然存在:如果計算量保持不變,更高的輸入分辨率會有多少幫助?
普遍的看法是,使用更高分辨率感受野的模型傾向於導致顯著改進的識別性能。
為了這個目的我們進行了以下三個實驗:
- 1)步長為2,大小為299×299的感受野和最大池化。
- 2)步長為1,大小為151×151的感受野和最大池化。
- 3)步長為1,大小為79×79的感受野和第一層之后沒有池化。
所有三個網絡具有幾乎相同的計算成本。雖然第三個網絡稍微便宜一些,但是池化層的成本是無足輕重的(在總成本的1%以內)。在每種情況下,網絡都進行了訓練,直到收斂,並在ImageNet ILSVRC 2012分類基准數據集的驗證集上衡量其質量。結果如表所示。雖然分辨率較低的網絡需要更長時間去訓練,但最終結果卻與較高分辨率網絡的質量相當接近。
當感受野尺寸變化時,識別性能的比較,但計算代價是不變的。但是,如果只是單純地按照輸入分辨率減少網絡尺寸,那么網絡的性能就會差得多。
總結:
Inception V3網絡主要有兩方面的改造:一是引入了Factorization into small convolutions的思想,將一個較大的二維卷積拆成兩個較小的一維卷積,比如將77卷積拆成17卷積和71卷積,或者將33卷積拆成13卷積核31卷積。一方面節約了大量參數,加快運算並減輕過擬合,同時增加了一層非線性擴展模型表達能力。論文中指出,這種非對稱的卷積結構拆分,其結果比對稱地拆分為幾個相同的小卷積核效果更明顯,可以處理更多、更豐富的空間特征,增加特征多樣性。
另一方面,Inception V3優化了Inception Module的結構,現在Inception Module有35*35、17*17和8*8三種不同結構。這些Inception Module只在網絡的后部出現,前面還是普通的卷積層。並且Inception V3除了在Inception Module中使用分支,還在分支中使用了分支(8*8的結構中,可以說是Network In Network 。
7,Inception V4
Inception V4研究了Inception模塊與殘差連接的結合。ResNet結構大大地加深了網絡深度,還極大地提升了訓練速度,同時性能也有提升。
Inception V4主要利用殘差連接(Residual Connection)來改進V3結構,得到Inception-ResNet-v1,Inception-ResNet-v2,Inception-v4網絡。
ResNet的殘差結構如下:
將該結構與 Inception 相結合(即Inception V3結合了微軟的ResNet),變為下圖:
通過20個類似的模塊組合,Inception-ResNet 構建如下:
8,TensorFlow實現Inception V3
本文主要實現的是 Inception V3,其整個網絡結果如圖所示。由於 Google Inception Net V3 相對比較復雜,所以這里使用 tf.contrib.slim輔助設計這個網絡。contrib.slim 中一些功能和組件可以大大減少設計 Inception Net 的代碼量,我們只需要使用少量代碼就可以構建好 有 42層深的 Inception V3。
首先定義一個簡單的函數 trunc_normal,產生截斷的正態分布。下面代碼主要來自TensorFlow的開源實現。
#_*_coding:utf-8_*_ import tensorflow as tf slim = tf.contrib.slim trunc_normal = lambda stddev: tf.truncated_normal_initializer(0.0, stddev)
下面定義函數 inception_v3_arg_scope,用來生成網絡中經常用到的函數的默認參數,比如卷積的激活函數,權重初始化方式,標准化器等。設置L2正則的 weight_decay 默認值為0.00004,標准差 stddev 默認值為 0.1,參數 batch_norm_var_collection 默認值為 moving_vars。接下來,定義 batch normalization 的參數字典,定義其衰減稀疏 decay 為 0.9997,epsilon 為 0.001,updates_collections 為 tf.GrpahKeys.UPDATE_OPS,然后字典 varibales_collections 中 beta 和 gamma 均設置為 None,moving_mean和 moving_variance 均設置為前面的 batch_norm_var_collection。
接下來使用 slim.arg_scope,這是一個非常有用的工具,它可以給函數的參數自動賦予某些默認值。例如,這句 with slim.arg_scope([slim.conv2d, slim.fully_connected],weights_regularizer = slim.l2_regularizer(weight_decay)),會對 [slim.conv2d, slim.fully_connected] 這兩個函數的參數自動賦值,將參數 weights_regularizer的值默認設為 slim.l2_regularizer(weight_decay)。使用了 slim.arg_scope 后就不需要每次都重復設置參數了,只需要在有修改時設置。接下來,嵌套一個 slim.arg_scope,對卷積層生成函數 slim.conv2d 的幾個參數 賦予默認值,其權重初始化器 weights_initializer 設置為 trunc_normal(stddev),激活函數設置為 ReLU,標准化器設置為 slim.batch_norm,標准化器的參數設置為前面定義的 batch_norm_params。最后返回定義好的 scope。
因為事先定義好了 slim.conv2d中的各種默認參數,包括激活函數和標准化器,因此后面定義一個卷積層將會變得非常方便。我們可以用一行代碼定義一個卷積層,整體代碼會變得非常簡潔美觀,同時設計網絡的工作量也會大大減輕。
#_*_coding:utf-8_*_ import tensorflow as tf slim = tf.contrib.slim trunc_normal = lambda stddev: tf.truncated_normal_initializer(0.0, stddev) def inception_v3_arg_scope(weight_decay=0.00004, stddev=0.1, batch_norm_var_collection='moving_vars'): batch_norm_params = { 'decay': 0.9997, 'epsilon': 0.001, 'updates_collections': tf.GraphKeys.UPDATE_OPS, 'variables_collections': { 'beta': None, 'gamma': None, 'moving_mean': [batch_norm_var_collection], 'moving_variance': [batch_norm_var_collection], } } with slim.arg_scope([slim.conv2d, slim.fully_connected], weights_regularizer=slim.l2_regularizer(weight_decay)): with slim.arg_scope( [slim.conv2d], weights_initializer=tf.truncated_normal_initializer(stddev=stddev), activation_fn=tf.nn.relu, normalizer_fn=slim.batch_norm, normalizer_params=batch_norm_params ) as sc: return sc
接下來我們就定義函數 inception_v3_base,它可以生成 Inception V3 網絡的卷積部分,參數 Inputs 為輸入的圖片數據的 tensor,scope為包含了函數默認參數的環境。我們定義一個字典表 end_points,用來保存某些關鍵節點供以后使用。接着再使用 slim.arg_scope,對 slim,conv2d, slim.max_pool2d 和 slim_avg_pool2d 這三個函數的參數設置默認值,將 stride 設為1 ,padding 設為 VALID。下面正式開始定義 Inception V3的網絡結構,首先是前面的非 Inception Module的卷積層。這里直接使用 slim.conv2d創建卷積層,slim.conv2d的第一個參數為輸入的 tensor,第二個參數為輸出的通道數,第三個參數為卷積核尺寸,第四個參數為步長 stride,第五個參數為padding模式。我們的第一個卷積層的輸出通道數為32,卷積核尺寸為3*3,步長為2,padding模式則是默認的VALID。后面的幾個卷積層采用相同的形式,按照論文中的定義,逐層定義好網絡結構。因為使用了 slim 及 slim.arg_scope,我們一行代碼就可以定義好的一個卷積層,相比之前 AlexNet的實現中使用好幾行代碼定義一個卷積層,或者 VGGNet 中專門寫一個函數來定義卷積層,都更加方便。
我們可以觀察到,在前面幾個普通的非 Inception Module 的卷積層中,主要使用了 3*3 的小卷積核,這是充分借鑒了 VGGNet 的結構,同時,Inception V3論文中也提出了 Factorization into small convolutions 思想,利用兩個1維卷積模擬大尺寸的2維卷積,減少參數數量同時增加非線性。前面幾層卷積中還有一層1*1卷積,這也是前面提到的Inception Module 中經常使用的結果之一,可低成本的跨通道的對特征進行組合。另外可以看到,除了第一個卷積層步長為2,其余的卷積層步長均為1,而池化層則是尺寸為3*3,步長為2的重疊最大池化,這是AlexNet中使用過的結構。網絡的輸入數據尺寸為 299*299*3,在經歷3個步長為2的層之后,尺寸最后縮小為 35*35*192,空間尺寸大大降低,但是輸出通道增加了很多。這部分代碼中一共有 5個卷積層,2個池化層,實現了對輸入圖片數據的尺寸壓縮,並對圖片特征進行了抽象。
def inception_v3_base(inputs, scope=None): end_points = {} with tf.variable_scope(scope, 'InceptionV3', [inputs]): with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d], stride=1, padding='VALID'): # 一共5個卷積層,兩個池化層,實現了對輸入圖片數據的尺寸壓縮,並對圖片特征進行了抽象 net = slim.conv2d(inputs, 32, [3, 3], stride=2, scope='Conv2d_1a_3x3') net = slim.conv2d(net, 32, [3, 3], scope='Conv2d_2a_3x3') net = slim.conv2d(net, 64, [3, 3], padding='SAME', scope='Conv2d_2b_3x3') net = slim.max_pool2d(net, [3, 3], stride=2, scope='MaxPool_3a_3x3') net = slim.conv2d(net, 80, [1, 1], scope='Conv2d_3b_1x1') net = slim.conv2d(net, 192, [3, 3], scope='conv2d_4a_3x3') net = slim.max_pool2d(net, [3, 3], stride=2, scope='MaxPool_5a_3x3')
接下來就是將三個連續的 Inception模塊組,這三個 Inception 模塊組中各自分別由多個 Inception Module,這部分的網絡結構即是Inception V3 的精華所在。每個Inception模塊組內部的幾個Inception Module 結構非常類似,但是存在一些細節不同。
第1個Inception 模塊組包含了3個結構類似的 Inception Module,他們的結構和上面分解的第一幅圖非常相似。其中第一個 Inception Module的名稱為Mixed_5d。我們先使用 slim.arg_scope 設置所有 Inception 模塊組的默認參數,將所有卷積層,最大池化,平均池化層的步長設為1,padding 模式設為SAME。然后設置這個 Inception Module的 variable_scope 名稱為 Mixed_5d。這個Inception Module中有4個分支,從 Branch_0 到Branch_3,第一個分支為有64輸出通道的1*1卷積;第二個分支為有48輸出通道的1*1卷積,連接有64輸出通道的5*5卷積;第三個分支為有64輸出通道的1*1卷積,再連續2個有96輸出通道的 3*3 卷積;第四個分支為3*3 的平均池化,連接有32 輸出通道的1*1 卷積。最后,使用 tf.concat 將4個分支的輸出合並在一起(在第3個維度合並,即輸出通道上合並),生成這個Inception Module 的最終輸出。因為這里所有的層步長均為1,並且 padding模式為SAME,所以圖片的尺寸並不會縮小,依然維持在 35*35。不過通道數增加了,4個分支的輸出通道數之和 64+64+96+32=256,即最終輸出的tensor尺寸為35*35*256.這里需要注意,第一個 Inception模塊組中所有 Inception Module輸出的圖片尺寸均為35*35,但是后兩個 Inception Module 的通道數會發生變化。
with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d], stride=1, padding='SAME'): with tf.variable_scope('Mixed_5b'): with tf.variable_scope('Branch_0'): branch_0 = slim.conv2d(net, 64, [1, 1], scope='Conv2d_0a_1x1') with tf.variable_scope("Branch_1"): branch_1 = slim.conv2d(net, 48, [1, 1], scope='Conv2d_0a_1x1') branch_1 = slim.conv2d(branch_1, 64, [5, 5], scope='Conv2d_0b_5x5') with tf.variable_scope('Branch_2'): branch_2 = slim.conv2d(net, 64, [1, 1], scope='Conv2d_0a_1x1') branch2 = slim.conv2d(branch_2, 96, [3, 3], scope='COnv2d_0b_1x1') branch2 = slim.conv2d(branch_2, 96, [3, 3], scope='COnv2d_0c_1x1') with tf.variable_scope("Branch_3"): branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3') branch_3 = slim.conv2d(branch_3, 32, [1, 1], scope='Conv2d_0b_1x1') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
接下來是第一個Inception模塊組的第2個 Inception Module——Mixed_5c,這里依然使用前面設置的默認參數:步長為1,padding模式為SAME。這個Inception Module同樣有4個分支,唯一不同的是第4個分支最后接的是64輸出通道的1*1卷積,而此前是32輸出通道。因此,我們輸出 tensor 的最終尺寸為35*35*288,輸出通道數相比之前增加了32。
with tf.variable_scope('Mixed_5c'): with tf.variable_scope("Branch_0"): branch_0 = slim.conv2d(net, 64, [1, 1], scope='Conv2d_0a_1x1') with tf.variable_scope("Branch_1"): branch_1 = slim.conv2d(net, 48, [1, 1], scope='Conv2d_0b_1x1') branch_1 = slim.conv2d(branch_1, 64, [5, 5], scope='Conv_1_0c_5x5') with tf.variable_scope("Branch_2"): branch_2 = slim.conv2d(net, 64, [1, 1], scope='Conv2d_0a_1x1') branch_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0b_3x3') branhc_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0c_3x3') with tf.variable_scope("Branch_3"): branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3') branch_3 = slim.conv2d(branch_3, 64, [1, 1], scope='Conv2d_0b_1x1') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
而第一個 Inception 模塊組的第3個Inception Module——Mixed_5d 和上一個 Inception Module完全相同,4個分支的結構,參數一模一樣,輸出 tensor 的尺寸也為 35*35*288。
with tf.variable_scope('Mixed_5d'): with tf.variable_scope("Branch_0"): branch_0 = slim.conv2d(net, 64, [1, 1], scope='Conv2d_0a_1x1') with tf.variable_scope("Branch_1"): branch_1 = slim.conv2d(net, 48, [1, 1], scope='Conv2d_0a_1x1') branch_1 = slim.conv2d(branch_1, 64, [5, 5], scope='Conv2d_0b_5x5') with tf.variable_scope("Branch_2"): branch_2 = slim.conv2d(net, 64, [1, 1], scope='Conv2d_0a_1x1') branch_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0b_3x3') branch_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0c_3x3') with tf.variable_scope("Branch_3"): branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3') branch_3 = slim.conv2d(branch_3, 64, [1, 1], scope='Conv2d_0b_1x1') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
第二個 Inception 模塊組是一個非常大的模塊組,包含了5個 Inception Module,其中第二個到第五個Inception Module的結構非常類似,他們的結構如因式分解圖第二幅所示。其中第一個Inception Module名稱為Mixed_6a ,它包含3個分支。第一個分支是一個384輸出通道的3*3卷積,這個分支的通道數一下就超過了之前的通道數之和。不過步長為2,因此圖片尺寸將會被壓縮,且padding模式為VALID,所以圖片尺寸縮小為17*17;第二個分支有三層,分布是一個64輸出通道的1*1卷積和兩個96輸出通道的3*3 卷積。這里需注意,最后一層的步長為2,padding模式為 VALID,因此圖片尺寸也被壓縮,本分支最終輸出的 tensor尺寸為17*17*96。最后依然是使用 tf.concat 將三個分支在輸出通道上合並,最后的輸出尺寸為 17*17*(384+96+256)=17*17*768。在第二個 Inception 模塊組中,5個Inception Module輸出tensor的尺寸將全部定格為 17*17*768,即圖片尺寸和輸出通道數都沒有發生變化。
with tf.variable_scope('Mixed_6a'): with tf.variable_scope("Branch_0"): branch_0 = slim.conv2d(net, 384, [3, 3], strides=2, padding='VALID', scope='Conv2d_0a_1x1') with tf.variable_scope("Branch_1"): branch_1 = slim.conv2d(net, 64, [1, 1], scope='Conv2d_0a_1x1') branch_1 = slim.conv2d(branch_1, 96, [3, 3], scope='Conv2d_0b_3x3') branch_1 = slim.conv2d(branch_1, 96, [3, 3], scope='Conv2d_1a_3x3') with tf.variable_scope("Branch_2"): branch_2 = slim.conv2d(net, 64, [3, 3], strides=2, padding='VALID', scope='MaxPool_1a_3x3') net = tf.concat([branch_0, branch_1, branch_2], 3)
接下來是第2個Inception模塊組的第二個 Inception Module——Mixed_6b,它有4個分支。第一個分支是一個簡單的192輸出通道的1*1卷積;第二個分支由三個卷積層組成,第一層是128輸出通道的1*1 卷積,第二層是128通道數的1*7卷積,第三次是192輸出通道數的7*1卷積。這里既是前面提到的Factorization into small convolutions 思想,串聯的1*7 卷積和 7*1 卷積相當於合成了一個7*7 卷積,不過參數量大大減少了(只有后者的2/7)並減輕了過擬合,同時多了一個激活函數增強了非線性特征變換;第3個分支一下子擁有了5個卷積層,分別是128輸出通道的1*1卷積,128輸出通道的7*1卷積,128輸出通道的1*7卷積,128輸出通道的7*1卷積和192輸出通達的1*7卷積。這個分支可以算是利用 Factorization into small convolutions 的典范,反復的將7*7卷積進行拆分;最后,第四個分支是一個3*3的平均池化層,再連接192輸出通道的1*1卷積。最后將4個分支合並,這一層輸出 tensor 的尺寸即為 17*17*(192+192+192+192)=17*17*768。
with tf.variable_scope('Mixed_6b'): with tf.variable_scope("Branch_0"): branch_0 = slim.conv2d(net, 192, [1, 1], scope='Conv2d_0a_1x1') with tf.variable_scope("Branch_1"): branch_1 = slim.conv2d(net, 128, [1, 1], scope='Conv2d_0a_1x1') branch_1 = slim.conv2d(branch_1, 128, [1, 7], scope='Conv2d_0b_1x7') branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1') with tf.variable_scope("Branch_2"): branch_2 = slim.conv2d(net, 128, [1, 1], scope='Conv2d_0a_1x1') branch_2 = slim.conv2d(branch_2, 128, [7, 1], scope='Conv2d_0b_7x1') branch_2 = slim.conv2d(branch_2, 128, [1, 7], scope='Conv2d_0c_1x7') branch_2 = slim.conv2d(branch_2, 128, [7, 1], scope='Conv2d_0d_7x1') branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0e_1x7') with tf.variable_scope("Branch_3"): branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3') branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x1') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
然后是我們第二個Inception模塊組的第三個Inception Module——Mixed_6c。Mixed_6c和前面一個Inception Module非常相似,只有一個地方不同,即第二個分支和第三個分支中前幾個卷積層的輸出通道數不同,從128變成了160,但是這兩個分支的最終輸出通道數不變,都是192。其他地方則完全一致。需要注意的是,我們的網絡每經過一個Inception Module ,即使輸出 tensor尺寸不變,但是特征都相當於被重新精煉了一遍,其中豐富的卷積和非線性化對提升網絡性能幫助很大。
with tf.variable_scope('Mixed_6c'): with tf.variable_scope("Branch_0"): branch_0 = slim.conv2d(net, 192, [1, 1], scope='Conv2d_0a_1x1') with tf.variable_scope("Branch_1"): branch_1 = slim.conv2d(net, 160, [1, 1], scope='Conv2d_0a_1x1') branch_1 = slim.conv2d(branch_1, 160, [1, 7], scope='Conv2d_0b_1x7') branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1') with tf.variable_scope("Branch_2"): branch_2 = slim.conv2d(net, 160, [1, 1], scope='Conv2d_0a_1x1') branch_2 = slim.conv2d(branch_2, 160, [7, 1], scope='Conv2d_0b_7x1') branch_2 = slim.conv2d(branch_2, 160, [1, 7], scope='Conv2d_0c_1x7') branch_2 = slim.conv2d(branch_2, 160, [7, 1], scope='Conv2d_0d_7x1') branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0e_1x7') with tf.variable_scope("Branch_3"): branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3') branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x1') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
Mixed_6d 和前面的 Mixed_6c 完全一致,目的是通過Inception Module 精心設計的結構增加卷積和非線性,提煉特征。
with tf.variable_scope('Mixed_6d'): with tf.variable_scope("Branch_0"): branch_0 = slim.conv2d(net, 192, [1, 1], scope='Conv2d_0a_1x1') with tf.variable_scope("Branch_1"): branch_1 = slim.conv2d(net, 160, [1, 1], scope='Conv2d_0a_1x1') branch_1 = slim.conv2d(branch_1, 160, [1, 7], scope='Conv2d_0b_1x7') branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1') with tf.variable_scope("Branch_2"): branch_2 = slim.conv2d(net, 160, [1, 1], scope='Conv2d_0a_1x1') branch_2 = slim.conv2d(branch_2, 160, [7, 1], scope='Conv2d_0b_7x1') branch_2 = slim.conv2d(branch_2, 160, [1, 7], scope='Conv2d_0c_1x7') branch_2 = slim.conv2d(branch_2, 160, [7, 1], scope='Conv2d_0d_7x1') branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0e_1x7') with tf.variable_scope("Branch_3"): branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3') branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x1') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
Mixed_6e 也和前面兩個 Inception Module完全一致。這是第二個Inception 模塊組的最后一個Inception Module。我們將 Mixed_6e 存儲於 end_points中,作為 Auxiliary Classifier 輔助模型的分類。
with tf.variable_scope('Mixed_6e'): with tf.variable_scope("Branch_0"): branch_0 = slim.conv2d(net, 192, [1, 1], scope='Conv2d_0a_1x1') with tf.variable_scope("Branch_1"): branch_1 = slim.conv2d(net, 192, [1, 1], scope='Conv2d_0a_1x1') branch_1 = slim.conv2d(branch_1, 192, [1, 7], scope='Conv2d_0b_1x7') branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1') with tf.variable_scope("Branch_2"): branch_2 = slim.conv2d(net, 192, [1, 1], scope='Conv2d_0a_1x1') branch_2 = slim.conv2d(branch_2, 192, [7, 1], scope='Conv2d_0b_7x1') branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0c_1x7') branch_2 = slim.conv2d(branch_2, 192, [7, 1], scope='Conv2d_0d_7x1') branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0e_1x7') with tf.variable_scope("Branch_3"): branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3') branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x1') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3) end_points['Mixed_6e'] = net
第3個 Inception 模塊組包含了三個 Inception Module,其中后兩個 Inception Module的結構非常類似,他們的結構如因式分解的第三幅圖所示。其中第一個Inception Module的名稱為 Mixed_7a,包含了3個分支。第一個分支是192輸出通道的1*1 卷積,再接320輸出通道數的3*3卷積,不過步長為2,padding模式為VALID,因此圖片尺寸縮小為8*8;第二個分支有4個卷積層,分別是192 輸出通道的1*1卷積,192輸出通道的1*7卷積,192輸出通道的7*1卷積,以及192輸出通道的3*3 卷積。注意最后一個卷積層同樣步長為2,padding為 VALID,因此最后輸出的tensor尺寸為 8*8*192;第三個分支則是一個3*3 的最大池化層,步長為2,padding為VALID,而池化層不會對輸出通道產生改變,因此這個分支的輸出尺寸為 8*8*768。最后,我們將3個分支在輸出通道上合並,輸出tensor尺寸為 8*8*(320+192+768)=8*8*1280。從這個Inception Module開始,輸出的圖片尺寸又被縮小了,同時通道數也增加了,tensor的總size在持續下降中。
with tf.variable_scope('Mixed_7a'): with tf.variable_scope("Branch_0"): branch_0 = slim.conv2d(net, 192, [1, 1], scope='Conv2d_0a_1x1') branch_0 = slim.conv2d(net, 320, [3, 3], stride=2, padding='VALID', scope='Conv2d_1a_3x3') with tf.variable_scope("Branch_1"): branch_1 = slim.conv2d(net, 192, [1, 1], scope='Conv2d_0a_1x1') branch_1 = slim.conv2d(branch_1, 192, [1, 7], scope='Conv2d_0b_1x7') branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1') with tf.variable_scope("Branch_2"): branch_2 = slim.max_pool2d(net, [3, 3], stride=2, padding='VALID', scope='MaxPool_1a_3x3') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
接下來是第三個Inception 模塊組的第二個Inception Module,它有四個分支。第一個分支是一個簡單的320輸出通道和1*1卷積;第二個分支先是1個384輸出通道的1*1卷積,隨后在分支內開了兩個分支,這兩個分支分別是 384輸出通道的 3*1 卷積,然后使用 tf.concat 合並兩個分支,得到的輸出 tensor尺寸為 8*8*(384+384)=8*8*768;第三個分支更復雜,先是 448輸出通道的1*1卷積,然后是 384輸出通道的 3*3 卷積,然后同樣在分支內拆成兩個分支,分別是384輸出通道的 1*3 卷積和 384輸出通道的 3*1 卷積,最后合並得到 8*8*768 的輸出tensor;第四個分支是在一個 3*3 卷積,然后同樣在分支內拆成兩個分支,分別是 384輸出通道的 1*3 卷積和 384輸出通道的 3*1卷積,最后合並德達 8*8*768的輸出tensor;第四個分支是在一個 3*3 的平均池化層后接一個 192 輸出通道的 1*1 卷積。最后,將這個非常復雜的 Inception Module 的四個分支合並在一起,得到的輸出tensor 尺寸為 8*8*(320+768+78+192)=8*8*2048。到這個Inception Module,輸出通道數從 1280 增加到 2048。
with tf.variable_scope('Mixed_7b'): with tf.variable_scope("Branch_0"): branch_0 = slim.conv2d(net, 320, [1, 1], scope='Conv2d_0a_1x1') with tf.variable_scope("Branch_1"): branch_1 = slim.conv2d(net, 384, [1, 1], scope='Conv2d_0a_1x1') branch_1 = tf.concat([ slim.conv2d(branch_1, 384, [1, 3], scope='Conv2d_0b_1x3'), slim.conv2d(branch_1, 384, [3, 1], scope='Conv2d_0c_3x1') ], 3) with tf.variable_scope("Branch_2"): branch_2 = slim.conv2d(net, 448, [1, 1], scope='COnv2d_0a_1x1') branch_2 = slim.conv2d(branch_2, 384, [3, 3], scope='Conv2d_0b_3x3') branch_2 = tf.concat([ slim.conv2d(branch_2, 384, [1, 3], scope='Conv2d_0c_1x3'), slim.conv2d(branch_2, 384, [3, 1], scope='Conv2d_0d_3x1') ], 3) with tf.variable_scope("Branch_3"): branch_3 = slim.avg_pool2d(net, [3, 3], scope='COnv2d_0a_3x3') branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x3') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3)
Mixed_7c 是第三個 Inception 模塊組的最后一個 Inception Module,不過他們和前面的 Mixed_7b 是完全一致的,輸出 tensor 也是8*8*2048。最后,我們返回這個Inception Module的結果,作為 inception_v3_base 函數的最終輸出。
with tf.variable_scope('Mixed_7c'): with tf.variable_scope("Branch_0"): branch_0 = slim.conv2d(net, 320, [1, 1], scope='Conv2d_0a_1x1') with tf.variable_scope("Branch_1"): branch_1 = slim.conv2d(net, 384, [1, 1], scope='Conv2d_0a_1x1') branch_1 = tf.concat([ slim.conv2d(branch_1, 384, [1, 3], scope='Conv2d_0b_1x3'), slim.conv2d(branch_1, 384, [3, 1], scope='Conv2d_0c_3x1') ], 3) with tf.variable_scope("Branch_2"): branch_2 = slim.conv2d(net, 448, [1, 1], scope='COnv2d_0a_1x1') branch_2 = slim.conv2d(branch_2, 384, [3, 3], scope='Conv2d_0b_3x3') branch_2 = tf.concat([ slim.conv2d(branch_2, 384, [1, 3], scope='Conv2d_0c_1x3'), slim.conv2d(branch_2, 384, [3, 1], scope='Conv2d_0d_3x1') ], 3) with tf.variable_scope("Branch_3"): branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3') branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x3') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3) return net, end_points
至此,Inception V3 網絡的核心部分,即卷積層部分就完成了。回憶一下 Inception V3的網絡結構:首先是五個卷積層和兩個池化層交替的普通結構,然后是三個Inception 模塊組,每個模塊組內包含多個結構類似的 Inception Module。設計 Inception Net的一個重要原則是,圖片尺寸是不斷縮小的,從299*299 通過五個步長為2的卷積層或池化層后,縮小為8*8,同時,輸出通道數持續增加,從一開始的3(RGB的三色)到 2048.從這里可以看出,每一層卷積,池化或 Inception模塊組的目的都是將空間結構簡化,同時將空間信息轉化為高階抽象的特征信息,即將空間的維度轉化為通道的維度。這一過程同時也使每層輸出 tensor的總size持續下降,降低了計算量。我們可能也發現了 Inception Module的規律,一般情況下有四個分支,第一個分支一般是1*1卷積,第二個分支一般是1*1卷積再接分解后(factorized)的1 x n 和 n x 1卷積,第三個分支和第二個分支類似,但是一般更深一點,第四個分支一般具有最大池化或平均池化。因此,Inception Module是通過組合比較簡單的特征抽象(分支1),比較復雜的特征抽象(分支2和分支3)和一個簡化結構的池化層(分支4),一共四種不同程度的特征抽象和變換來選擇地保留不同層次的高階特征,這樣可以最大程度地豐富網絡的表達能力。
接下來,我們來實現 Inception V3 網絡的最后一部分——全局平均池化,Softmax和 Auxiliary Logits。先看函數 Inception V3 的輸入參數,num_classes 即最后需要分類的數據量,這里默認的1000是ILSVRC比賽數據集的種類數;is_training 標志是否是訓練過程,對Batch Normalization 和 Dropout 有影響,只有在訓練時Batch Normalization 和 Dropout 才會被啟用;dropout_keep_prob 即訓練時 Dropout所需保留節點的比例,默認為 0.8;predicetion_fn 是最后用來進行分類的函數,這里默認是使用 slim.softmax;spatial_squeeze 參數標志是否對輸出進行 squeeze 操作(即去除維數為1的維度,比如 5*3*1轉為 5*3);reuse標志是否會對網絡和Variable 進行重復使用;最后,scope為包含了函數默認參數的環境。首先,使用 tf.variable_scope 定義網絡的 name 和 reuse 等參數的默認值,然后使用 slim.arg_scope定義 Batch Normalization 和 Dropout 的 is_training 標志的默認值。最后,使用前面定義好的 inception_v3_base 構築整個網絡的卷積部分,拿到最后一層的輸出net和重要節點的字典表 end_points。
def inception_v3(inputs, num_classes=1000, is_training=True, dropout_keep_prob=0.8, prediction_fn=slim.softmax, spatial_squeeze=True, reuse=None, scope='InceptionV3'): with tf.variable_scope(scope, 'InceptionV3', [inputs, num_classes], reuse=reuse) as scope: with slim.arg_scope([slim.batch_norm, slim.dropout], is_training=is_training): net, end_points = inception_v3_base(inputs, scope=scope)
接下來處理 Auxiliary Logits 這部分的邏輯, Auxiliary Logits 作為輔助分類的節點,對分類結果預測有很大幫助,先使用 slim.arg_scope將卷積,最大池化,平均池化的默認步長設為1,默認padding模式設為 SAME 。然后通過 end_points 取到 Mixed_6e,並在Mixed_6e之后再接一個5*5 的平均池化,步長為3,padding設為 VALID,這樣輸出的尺寸就從 17*17*768 變為 5*5*768。接着連接一個 128輸出通道的1*1 卷積和一個 768輸出通道的 5*5卷積,這里權重初始化方式重設為標准差為 0.01的正態分布,padding模式設為 VALID,輸出尺寸為 1*1*768。然后再連接一個輸出通道數為 num_classes的 1*1 卷積,不設激活函數和規范化函數,權重初始化重設為標准差為 0.001的正態分布,這樣輸出變為了1*1*1000。接下來,使用 tf.squeeze函數消除輸出 tensor 中前兩個為1 的維度,最后將輔助分類節點的輸出 aux_logits儲存到字典表 end_points中。
with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d], stride=1, padding='SAME'): aux_logits = end_points['Mixed_6e'] with tf.variable_scope('AuxLogits'): aux_logits = slim.avg_pool2d( aux_logits, [5, 5], stride=3, padding='VALID', scope='AvfPool_1a_5x5' ) aux_logits = slim.conv2d(aux_logits, 128, [1, 1], scope='COnv2d_1b_1x1') aux_logits = slim.conv2d( aux_logits, 768, [5, 5], weights_initializer=trunc_normal(0.01), padding='VALID', scope='COnv2d_2a_5x5' ) aux_logits = slim.conv2d( aux_logits, num_classes, [1, 1], activation_fn=None, normalizer_fn=None, weights_initializer=trunc_normal(0.001), scope='Conv2d_2b_1x1' ) if spatial_squeeze: aux_logits = tf.squeeze(aux_logits, [1, 2], name='SpatialSqueeze') end_points['AuxLogits'] = aux_logits
下面處理正常的分類預測的邏輯。我們直接對Mixed_7e即最后一個卷積層的輸出進行一個8*8全局平均池化,padding模式為 VALID,這樣輸出 tensor的尺寸就變為了 1*1*2048。然后連接一個 Dropout層,節點保留率為 dropout_keep_prob。接着連接一個輸出通道數為 1000 的1*1 卷積,激活函數和規范化函數設為空。下面使用 tf.squeeze去除輸出 tensor中維度為1的維度。再連接一個Softmax對結果進行分類預測。最后返回輸出結果Logits和包含輔助節點的 end_points。
with tf.variable_scope('Logits'): net = slim.avg_pool2d(net, [8, 8], padding='VALID', scope='AvgPool_1a_8x8') net = slim.dropout(net, keep_prob=dropout_keep_prob, scope='Dropout_1b') end_points['PreLogits'] = net logits = slim.conv2d(net, num_classes, [1, 1], activation_fn=None, normalizer_fn=None, scope='Conv2d_1c_1x1') if spatial_squeeze: logits = tf.squeeze(logits, [1, 2], name='SpatialSqueeze') end_points['Logits'] = logits end_points['Predictions'] = prediction_fn(logits, scope='Predictions') return logits, end_points
至此,整個Inception V3網絡的構建就完成了。Inception V3是一個非常復雜,精妙的模型,其中用到了非常多值錢積累下來的設計大型卷積網絡的經驗和技巧。不過,雖然Inception V3論文中給出了設計卷積網絡的幾個原則,但是其中很多超參數的選擇,包含層數,卷積核的尺寸,池化的位置,步長的大小,Factorization使用的時機,以及分支的設計,都很難一一解釋。目前,我們只能認為深度學習,尤其是大型卷積網絡的設計,是一門實驗學科,其中需要大量的探索和實踐。我們很難正面某種網絡結構一定更好,更多的是通過實驗積累下來的經驗總結出一些結論。深度學習的研究中,理論正面部分依然是短板,但是通過實驗得到的結論通常也具有不錯的推廣性,在其他數據集上泛化性良好。
下面對 Inception v3進行運算性能測試,這里使用的 time_tensorflow_run 函數和 ALexNet一樣,這里直接寫代碼,不重復說明了。因為Inception V3網絡結構較大,所以依然令 batch_size 為32,以便GPU顯存不夠,圖片尺寸設為 299*299,並用 tf.random_uniform 生成隨機圖片數據作為Input。接着,我們使用 slim.arg_sope 加載前面定義好的 inception_v3_arg_scope() ,在這個scope中包含了 Batch Normalization 的默認參數,以及激活函數和參數初始化方式的默認值。然后在這個 arg_scope 下,調用 inception_v3函數,並傳入 Inputs,獲取logits 和 end_points。下面創建Session並初始化全部模型參數,最后我們設置測試的 batch數量為100,並使用 time_tensorflow_run 測試 Inception V3網絡的 forward 性能。
def time_tensorflow_run(session, target, info_string): num_steps_burn_in = 10 total_duration = 0.0 # 記錄總時間 total_duration_squared = 0.0 # 記錄平方和total_duration_squared用於計算方差 for i in range(num_batches + num_steps_burn_in): start_time = time.time() _ = session.run(target) duration = time.time() - start_time if i >= num_steps_burn_in: if not i % 10: print('%s: stpe %d, duration=%.3f'%(datetime.now(), i-num_steps_burn_in, duration)) total_duration += duration total_duration_squared += duration * duration mn = total_duration / num_batches vr = total_duration_squared / num_batches - mn*mn sd = math.sqrt(vr) print('%s: %s across %d steps, %.3f +/- %.3f sec / batch'%(datetime.now(), info_string, num_batches, mn, sd))
代碼2:
if __name__ == '__main__': batch_size = 32 height, width = 299, 299 inputs = tf.random_uniform((batch_size, height, width, 3)) with slim.arg_scope(inception_v3_arg_scope()): logits, end_points = inception_v3(inputs, is_training=False) init = tf.global_variables_initializer() sess = tf.Session() sess.run(init) num_batches = 100 time_tensorflow_run(sess, logits, 'Forward')
從結果來看,Inception V3 網絡的 forward性能不錯,在GPU的環境下,每個batch(包含32張圖片)預測耗時僅為0.071s。雖然輸入圖片的面積比 VGGNet的 224*224大了 78%,但是 forward速度卻比 VGGNet的0.072s更快。這主要歸功於其較小的參數量,Inception V3網絡僅有 2500 萬個參數,雖然比 Inception V1的700萬多了很多,不過任然不到 AlexNet 的6000萬參數量的一半,相比於 VGGNet的 1.4億參數量就更少了,這對一個42層深的大型網絡來說極為不易的。同時,整個網絡的浮點計算量僅為50億次,雖也比 Inception V1的15億次大了不少,但是相比 VGGNet仍然不算大。較小的計算量讓 Inception V3網絡變得非常實用,我們可以將其輕松的移到普通的服務器上提供快速響應的服務,甚至是移植到收集上進行實時的圖像識別。
2019-09-16 09:47:11.291271: step 0, duration=0.072 2019-09-16 09:47:12.007825: step 10, duration=0.072 2019-09-16 09:47:12.723585: step 20, duration=0.072 2019-09-16 09:47:13.437683: step 30, duration=0.071 2019-09-16 09:47:14.151189: step 40, duration=0.071 2019-09-16 09:47:14.864866: step 50, duration=0.071 2019-09-16 09:47:15.579139: step 60, duration=0.071 2019-09-16 09:47:16.291750: step 70, duration=0.071 2019-09-16 09:47:17.005981: step 80, duration=0.071 2019-09-16 09:47:17.721084: step 90, duration=0.071 2019-09-16 09:47:18.362285: Forward across 100 steps, 0.007 +/- 0.021 sec / batch
Inception V3作為一個極深的卷積神經網絡,擁有非常精妙的設計和構造,整個網絡的結構和分支非常復雜,我們平時可能不必設計這么復雜的網絡的,但是Inception V3中讓然有許多CNN的思想和 Trick值得借鑒。
(1) Factorization into small convolutions 很有效,可以降低參數量,減輕過擬合,增加網絡非線性的表達能力。
(2)卷積網絡從輸入到輸出,應該讓圖片尺寸逐漸減少,輸出通道數逐漸增加,即讓空間結構簡化,將空間信息轉化為高階抽象的特征信息。
(3)Inception Module用多個分支提取不同抽象程度的高階特征的思路很有效,可以豐富網絡的表達能力。
完整代碼如下:
import tensorflow as tf slim = tf.contrib.slim trunc_normal = lambda stddev: tf.truncated_normal_initializer(0.0, stddev) def inception_v3_base(inputs, scope=None): end_points = {} with tf.variable_scope(scope, 'InceptionV3', [inputs]): with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d], stride=1, padding='VALID'): # 299 x 299 x 3 net = slim.conv2d(inputs, 32, [3, 3], stride=2, scope='Conv2d_1a_3x3') # 149 x 149 x 32 net = slim.conv2d(net, 32, [3, 3], scope='Conv2d_2a_3x3') # 147 x 147 x 32 net = slim.conv2d(net, 64, [3, 3], padding='SAME', scope='Conv2d_2b_3x3') # 147 x 147 x 64 net = slim.max_pool2d(net, [3, 3], stride=2, scope='MaxPool_3a_3x3') # 73 x 73 x 64 net = slim.conv2d(net, 80, [1, 1], scope='Conv2d_3b_1x1') # 73 x 73 x 80. net = slim.conv2d(net, 192, [3, 3], scope='Conv2d_4a_3x3') # 71 x 71 x 192. net = slim.max_pool2d(net, [3, 3], stride=2, scope='MaxPool_5a_3x3') # 35 x 35 x 192. # Inception blocks with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d], stride=1, padding='SAME'): # mixed: 35 x 35 x 256. with tf.variable_scope('Mixed_5b'): with tf.variable_scope('Branch_0'): branch_0 = slim.conv2d(net, 64, [1, 1], scope='Conv2d_0a_1x1') with tf.variable_scope('Branch_1'): branch_1 = slim.conv2d(net, 48, [1, 1], scope='Conv2d_0a_1x1') branch_1 = slim.conv2d(branch_1, 64, [5, 5], scope='Conv2d_0b_5x5') with tf.variable_scope('Branch_2'): branch_2 = slim.conv2d(net, 64, [1, 1], scope='Conv2d_0a_1x1') branch_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0b_3x3') branch_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0c_3x3') with tf.variable_scope('Branch_3'): branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3') branch_3 = slim.conv2d(branch_3, 32, [1, 1], scope='Conv2d_0b_1x1') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3) # mixed_1: 35 x 35 x 288. with tf.variable_scope('Mixed_5c'): with tf.variable_scope('Branch_0'): branch_0 = slim.conv2d(net, 64, [1, 1], scope='Conv2d_0a_1x1') with tf.variable_scope('Branch_1'): branch_1 = slim.conv2d(net, 48, [1, 1], scope='Conv2d_0b_1x1') branch_1 = slim.conv2d(branch_1, 64, [5, 5], scope='Conv_1_0c_5x5') with tf.variable_scope('Branch_2'): branch_2 = slim.conv2d(net, 64, [1, 1], scope='Conv2d_0a_1x1') branch_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0b_3x3') branch_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0c_3x3') with tf.variable_scope('Branch_3'): branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3') branch_3 = slim.conv2d(branch_3, 64, [1, 1], scope='Conv2d_0b_1x1') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3) # mixed_2: 35 x 35 x 288. with tf.variable_scope('Mixed_5d'): with tf.variable_scope('Branch_0'): branch_0 = slim.conv2d(net, 64, [1, 1], scope='Conv2d_0a_1x1') with tf.variable_scope('Branch_1'): branch_1 = slim.conv2d(net, 48, [1, 1], scope='Conv2d_0a_1x1') branch_1 = slim.conv2d(branch_1, 64, [5, 5], scope='Conv2d_0b_5x5') with tf.variable_scope('Branch_2'): branch_2 = slim.conv2d(net, 64, [1, 1], scope='Conv2d_0a_1x1') branch_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0b_3x3') branch_2 = slim.conv2d(branch_2, 96, [3, 3], scope='Conv2d_0c_3x3') with tf.variable_scope('Branch_3'): branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3') branch_3 = slim.conv2d(branch_3, 64, [1, 1], scope='Conv2d_0b_1x1') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3) # mixed_3: 17 x 17 x 768. with tf.variable_scope('Mixed_6a'): with tf.variable_scope('Branch_0'): branch_0 = slim.conv2d(net, 384, [3, 3], stride=2, padding='VALID', scope='Conv2d_1a_1x1') with tf.variable_scope('Branch_1'): branch_1 = slim.conv2d(net, 64, [1, 1], scope='Conv2d_0a_1x1') branch_1 = slim.conv2d(branch_1, 96, [3, 3], scope='Conv2d_0b_3x3') branch_1 = slim.conv2d(branch_1, 96, [3, 3], stride=2, padding='VALID', scope='Conv2d_1a_1x1') with tf.variable_scope('Branch_2'): branch_2 = slim.max_pool2d(net, [3, 3], stride=2, padding='VALID', scope='MaxPool_1a_3x3') net = tf.concat([branch_0, branch_1, branch_2], 3) # mixed4: 17 x 17 x 768. with tf.variable_scope('Mixed_6b'): with tf.variable_scope('Branch_0'): branch_0 = slim.conv2d(net, 192, [1, 1], scope='Conv2d_0a_1x1') with tf.variable_scope('Branch_1'): branch_1 = slim.conv2d(net, 128, [1, 1], scope='Conv2d_0a_1x1') branch_1 = slim.conv2d(branch_1, 128, [1, 7], scope='Conv2d_0b_1x7') branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1') with tf.variable_scope('Branch_2'): branch_2 = slim.conv2d(net, 128, [1, 1], scope='Conv2d_0a_1x1') branch_2 = slim.conv2d(branch_2, 128, [7, 1], scope='Conv2d_0b_7x1') branch_2 = slim.conv2d(branch_2, 128, [1, 7], scope='Conv2d_0c_1x7') branch_2 = slim.conv2d(branch_2, 128, [7, 1], scope='Conv2d_0d_7x1') branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0e_1x7') with tf.variable_scope('Branch_3'): branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3') branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x1') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3) # mixed_5: 17 x 17 x 768. with tf.variable_scope('Mixed_6c'): with tf.variable_scope('Branch_0'): branch_0 = slim.conv2d(net, 192, [1, 1], scope='Conv2d_0a_1x1') with tf.variable_scope('Branch_1'): branch_1 = slim.conv2d(net, 160, [1, 1], scope='Conv2d_0a_1x1') branch_1 = slim.conv2d(branch_1, 160, [1, 7], scope='Conv2d_0b_1x7') branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1') with tf.variable_scope('Branch_2'): branch_2 = slim.conv2d(net, 160, [1, 1], scope='Conv2d_0a_1x1') branch_2 = slim.conv2d(branch_2, 160, [7, 1], scope='Conv2d_0b_7x1') branch_2 = slim.conv2d(branch_2, 160, [1, 7], scope='Conv2d_0c_1x7') branch_2 = slim.conv2d(branch_2, 160, [7, 1], scope='Conv2d_0d_7x1') branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0e_1x7') with tf.variable_scope('Branch_3'): branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3') branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x1') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3) # mixed_6: 17 x 17 x 768. with tf.variable_scope('Mixed_6d'): with tf.variable_scope('Branch_0'): branch_0 = slim.conv2d(net, 192, [1, 1], scope='Conv2d_0a_1x1') with tf.variable_scope('Branch_1'): branch_1 = slim.conv2d(net, 160, [1, 1], scope='Conv2d_0a_1x1') branch_1 = slim.conv2d(branch_1, 160, [1, 7], scope='Conv2d_0b_1x7') branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1') with tf.variable_scope('Branch_2'): branch_2 = slim.conv2d(net, 160, [1, 1], scope='Conv2d_0a_1x1') branch_2 = slim.conv2d(branch_2, 160, [7, 1], scope='Conv2d_0b_7x1') branch_2 = slim.conv2d(branch_2, 160, [1, 7], scope='Conv2d_0c_1x7') branch_2 = slim.conv2d(branch_2, 160, [7, 1], scope='Conv2d_0d_7x1') branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0e_1x7') with tf.variable_scope('Branch_3'): branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3') branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x1') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3) # mixed_7: 17 x 17 x 768. with tf.variable_scope('Mixed_6e'): with tf.variable_scope('Branch_0'): branch_0 = slim.conv2d(net, 192, [1, 1], scope='Conv2d_0a_1x1') with tf.variable_scope('Branch_1'): branch_1 = slim.conv2d(net, 192, [1, 1], scope='Conv2d_0a_1x1') branch_1 = slim.conv2d(branch_1, 192, [1, 7], scope='Conv2d_0b_1x7') branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1') with tf.variable_scope('Branch_2'): branch_2 = slim.conv2d(net, 192, [1, 1], scope='Conv2d_0a_1x1') branch_2 = slim.conv2d(branch_2, 192, [7, 1], scope='Conv2d_0b_7x1') branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0c_1x7') branch_2 = slim.conv2d(branch_2, 192, [7, 1], scope='Conv2d_0d_7x1') branch_2 = slim.conv2d(branch_2, 192, [1, 7], scope='Conv2d_0e_1x7') with tf.variable_scope('Branch_3'): branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3') branch_3 = slim.conv2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x1') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3) end_points['Mixed_6e'] = net # mixed_8: 8 x 8 x 1280. with tf.variable_scope('Mixed_7a'): with tf.variable_scope('Branch_0'): branch_0 = slim.conv2d(net, 192, [1, 1], scope='Conv2d_0a_1x1') branch_0 = slim.conv2d(branch_0, 320, [3, 3], stride=2, padding='VALID', scope='Conv2d_1a_3x3') with tf.variable_scope('Branch_1'): branch_1 = slim.conv2d(net, 192, [1, 1], scope='Conv2d_0a_1x1') branch_1 = slim.conv2d(branch_1, 192, [1, 7], scope='Conv2d_0b_1x7') branch_1 = slim.conv2d(branch_1, 192, [7, 1], scope='Conv2d_0c_7x1') branch_1 = slim.conv2d(branch_1, 192, [3, 3], stride=2, padding='VALID', scope='Conv2d_1a_3x3') with tf.variable_scope('Branch_2'): branch_2 = slim.max_pool2d(net, [3, 3], stride=2, padding='VALID', scope='MaxPool_1a_3x3') net = tf.concat([branch_0, branch_1, branch_2], 3) # mixed_9: 8 x 8 x 2048. with tf.variable_scope('Mixed_7b'): with tf.variable_scope('Branch_0'): branch_0 = slim.conv2d(net, 320, [1, 1], scope='Conv2d_0a_1x1') with tf.variable_scope('Branch_1'): branch_1 = slim.conv2d(net, 384, [1, 1], scope='Conv2d_0a_1x1') branch_1 = tf.concat([ slim.conv2d(branch_1, 384, [1, 3], scope='Conv2d_0b_1x3'), slim.conv2d(branch_1, 384, [3, 1], scope='Conv2d_0b_3x1')], 3) with tf.variable_scope('Branch_2'): branch_2 = slim.conv2d(net, 448, [1, 1], scope='Conv2d_0a_1x1') branch_2 = slim.conv2d( branch_2, 384, [3, 3], scope='Conv2d_0b_3x3') branch_2 = tf.concat([ slim.conv2d(branch_2, 384, [1, 3], scope='Conv2d_0c_1x3'), slim.conv2d(branch_2, 384, [3, 1], scope='Conv2d_0d_3x1')], 3) with tf.variable_scope('Branch_3'): branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3') branch_3 = slim.conv2d( branch_3, 192, [1, 1], scope='Conv2d_0b_1x1') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3) # mixed_10: 8 x 8 x 2048. with tf.variable_scope('Mixed_7c'): with tf.variable_scope('Branch_0'): branch_0 = slim.conv2d(net, 320, [1, 1], scope='Conv2d_0a_1x1') with tf.variable_scope('Branch_1'): branch_1 = slim.conv2d(net, 384, [1, 1], scope='Conv2d_0a_1x1') branch_1 = tf.concat([ slim.conv2d(branch_1, 384, [1, 3], scope='Conv2d_0b_1x3'), slim.conv2d(branch_1, 384, [3, 1], scope='Conv2d_0c_3x1')], 3) with tf.variable_scope('Branch_2'): branch_2 = slim.conv2d(net, 448, [1, 1], scope='Conv2d_0a_1x1') branch_2 = slim.conv2d( branch_2, 384, [3, 3], scope='Conv2d_0b_3x3') branch_2 = tf.concat([ slim.conv2d(branch_2, 384, [1, 3], scope='Conv2d_0c_1x3'), slim.conv2d(branch_2, 384, [3, 1], scope='Conv2d_0d_3x1')], 3) with tf.variable_scope('Branch_3'): branch_3 = slim.avg_pool2d(net, [3, 3], scope='AvgPool_0a_3x3') branch_3 = slim.conv2d( branch_3, 192, [1, 1], scope='Conv2d_0b_1x1') net = tf.concat([branch_0, branch_1, branch_2, branch_3], 3) return net, end_points def inception_v3(inputs, num_classes=1000, is_training=True, dropout_keep_prob=0.8, prediction_fn=slim.softmax, spatial_squeeze=True, reuse=None, scope='InceptionV3'): with tf.variable_scope(scope, 'InceptionV3', [inputs, num_classes], reuse=reuse) as scope: with slim.arg_scope([slim.batch_norm, slim.dropout], is_training=is_training): net, end_points = inception_v3_base(inputs, scope=scope) # Auxiliary Head logits with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d], stride=1, padding='SAME'): aux_logits = end_points['Mixed_6e'] with tf.variable_scope('AuxLogits'): aux_logits = slim.avg_pool2d( aux_logits, [5, 5], stride=3, padding='VALID', scope='AvgPool_1a_5x5') aux_logits = slim.conv2d(aux_logits, 128, [1, 1], scope='Conv2d_1b_1x1') # Shape of feature map before the final layer. aux_logits = slim.conv2d( aux_logits, 768, [5, 5], weights_initializer=trunc_normal(0.01), padding='VALID', scope='Conv2d_2a_5x5') aux_logits = slim.conv2d( aux_logits, num_classes, [1, 1], activation_fn=None, normalizer_fn=None, weights_initializer=trunc_normal(0.001), scope='Conv2d_2b_1x1') if spatial_squeeze: aux_logits = tf.squeeze(aux_logits, [1, 2], name='SpatialSqueeze') end_points['AuxLogits'] = aux_logits # Final pooling and prediction with tf.variable_scope('Logits'): net = slim.avg_pool2d(net, [8, 8], padding='VALID', scope='AvgPool_1a_8x8') # 1 x 1 x 2048 net = slim.dropout(net, keep_prob=dropout_keep_prob, scope='Dropout_1b') end_points['PreLogits'] = net # 2048 logits = slim.conv2d(net, num_classes, [1, 1], activation_fn=None, normalizer_fn=None, scope='Conv2d_1c_1x1') if spatial_squeeze: logits = tf.squeeze(logits, [1, 2], name='SpatialSqueeze') # 1000 end_points['Logits'] = logits end_points['Predictions'] = prediction_fn(logits, scope='Predictions') return logits, end_points def inception_v3_arg_scope(weight_decay=0.00004, stddev=0.1, batch_norm_var_collection='moving_vars'): batch_norm_params = { 'decay': 0.9997, 'epsilon': 0.001, 'updates_collections': tf.GraphKeys.UPDATE_OPS, 'variables_collections': { 'beta': None, 'gamma': None, 'moving_mean': [batch_norm_var_collection], 'moving_variance': [batch_norm_var_collection], } } with slim.arg_scope([slim.conv2d, slim.fully_connected], weights_regularizer=slim.l2_regularizer(weight_decay)): with slim.arg_scope( [slim.conv2d], weights_initializer=trunc_normal(stddev), activation_fn=tf.nn.relu, normalizer_fn=slim.batch_norm, normalizer_params=batch_norm_params) as sc: return sc from datetime import datetime import math import time def time_tensorflow_run(session, target, info_string): num_steps_burn_in = 10 total_duration = 0.0 total_duration_squared = 0.0 for i in range(num_batches + num_steps_burn_in): start_time = time.time() _ = session.run(target) duration = time.time() - start_time if i >= num_steps_burn_in: if not i % 10: print('%s: step %d, duration = %.3f' % (datetime.now(), i - num_steps_burn_in, duration)) total_duration += duration total_duration_squared += duration * duration mn = total_duration / num_batches vr = total_duration_squared / num_batches - mn * mn sd = math.sqrt(vr) print('%s: %s across %d steps, %.3f +/- %.3f sec / batch' % (datetime.now(), info_string, num_batches, mn, sd)) if __name__ == '__main__': batch_size = 32 height, width = 299, 299 inputs = tf.random_uniform((batch_size, height, width, 3)) with slim.arg_scope(inception_v3_arg_scope()): logits, end_points = inception_v3(inputs, is_training=False) init = tf.global_variables_initializer() sess = tf.Session() sess.run(init) num_batches = 100 time_tensorflow_run(sess, logits, "Forward")
本文是學習GoogLeNet網絡的筆記,參考了《tensorflow實戰》這本書中關於GoogLeNet的章節,寫的非常好,所以在此做了筆記,侵刪。
而且本文在學習中,摘抄了下面博客的GoogLeNet筆記,也寫的通俗易通:https://www.zybuluo.com/rianusr/note/1419006
https://my.oschina.net/u/876354/blog/1637819
在學習后,確實對GoogLeNet 理解了不少,在此很感謝! 侵刪,謝謝
強烈建議:
2014至2016年,GoogLeNet團隊發表了多篇關於GoogLeNet的經典論文《Going deeper with convolutions》、《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》、《Rethinking the Inception Architecture for Computer Vision》、《Inception-v4, Inception-ResNet and the Impact of Residual Connections on Learning》,在這些論文中對Inception v1、Inception v2、Inception v3、Inception v4 等思想和技術原理進行了詳細的介紹,建議閱讀這些論文以全面了解GoogLeNet。
inception-4 論文地址:https://arxiv.org/pdf/1602.07261.pdf
inception-3 論文地址:https://arxiv.org/pdf/1409.4842v1.pdf
Rethinking the Inception Architecture for Computer Vision, 3.5% test error :http://arxiv.org/abs/1512.00567