卷積神經網絡
1. 預備知識
1.1 神經網絡中為什么要標準化
- 原因在于神經網絡學習過程本質就是為了學習數據分布,一旦訓練數據與測試數據的分布不同,那么網絡的泛化能力也大大降低;另外一方面,一旦每批訓練數據的分布各不相同(batch 梯度下降),那么網絡就要在每次迭代都去學習適應不同的分布,這樣將會大大降低網絡的訓練速度,這也正是為什么我們需要對數據都要做一個歸一化預處理的原因。
- 對于深度網絡的訓練是一個復雜的過程,只要網絡的前面幾層發生微小的改變,那么后面幾層就會被累積放大下去。一旦網絡某一層的輸入數據的分布發生改變,那么這一層網絡就需要去適應學習這個新的數據分布,所以如果訓練過程中,訓練數據的分布一直在發生變化,那么將會影響網絡的訓練速度。
1.2 什么是 LRN
- Local Response Normalization的簡稱是LRN,即局部響應歸一化層。說到為什么要使用LRN就不得不提到神經生物學中的一個概念叫做 lateral inhibition(橫向抑制),簡單來講就是興奮的神經細胞抑制周圍神經細胞的能力。應用到深度神經網絡中,這種橫向抑制的目的是進行局部對比度增強,以便將局部特征在下一層得到表達。LRN是一個非訓練層,即該層中不存在可訓練的參數。假設某一個CNN層的輸出為一個 WxHxC 的張量,要對該張量做LRN我們應該怎么做呢。假設我們取n的大小為3,現在我們要計算(x, y, i)位置上 LRN 后的值,我們便可以以 (x,y, i)點位中心,在channel維度范圍內取一個1xn大小的網格,此時我們便可以通過(x, y, i)位置的數值和其周圍的數值對(x, y, i)位置進行正則化。
1.3 BN
- BN的基本思想其實相當直觀:因為深層神經網絡在做非線性變換前的激活輸入值(就是那個x=WU+B,U是輸入)隨著網絡深度加深或者在訓練過程中,其分布逐漸發生偏移或者變動,之所以訓練收斂慢,一般是整體分布逐漸往非線性函數的取值區間的上下限兩端靠近(對于Sigmoid函數來說,意味著激活輸入值WU+B是大的負值或正值),所以這導致反向傳播時低層神經網絡的梯度消失,這是訓練深層神經網絡收斂越來越慢的本質原因,而BN就是通過一定的規范化手段,把每層神經網絡任意神經元這個輸入值的分布強行拉回到均值為0方差為1的標準正態分布,其實就是把越來越偏的分布強制拉回比較標準的分布,這樣使得激活輸入值落在非線性函數對輸入比較敏感的區域,這樣輸入的小變化就會導致損失函數較大的變化,意思是這樣讓梯度變大,避免梯度消失問題產生,而且梯度變大意味著學習收斂速度快,能大大加快訓練速度。
1.4
卷積的作用
降維,降低了計算復雜度。當某個卷積層輸入的特征數較多,對這個輸入進行卷積運算將產生巨大的計算量;如果對輸入先進行降維,減少特征數后再做卷積計算量就會顯著減少。比如,一張500 * 500* 100 的圖片,在20個filter上做1 * 1的卷積,那么結果的大小為500* 500* 20。
加入非線性,提升網絡的表達能力。比如先進行一次普通的卷積(比如3x3),緊跟再進行一次1x1的卷積,對于某個像素點來說1x1卷積等效于該像素點在所有特征上進行一次全連接的計算,無論是第一個3x3卷積還是新增的1x1卷積,后面都緊跟著激活函數(比如relu)。將兩個卷積串聯,就能組合出更多的非線性特征。
2. 經典卷積神經網絡原理
2.1 LeNet5
-
LeNet5 誕生于 1994 年,是最早的卷積神經網絡之一,并且推動了深度學習領域的發展。LeNet5 的架構基于這樣的觀點:圖像的特征分布在整張圖像上,以及帶有可學習參數的卷積是一種用少量參數在多個位置上提取相似特征的有效方式。在那時候,沒有GPU幫助訓練,甚至CPU的速度也很慢。因此,能夠保存參數以及計算過程是一個關鍵進展,這和將每個像素用作一個大型多層神經網絡的單獨輸入相反。LeNet5 闡述了哪些像素不應該被使用在第一層,因為圖像具有很強的空間相關性,而使用圖像中獨立的像素作為不同的輸入特征則利用不到這些相關性。LeNet5網絡結構如下: LeNet5網絡結構
- LeNet5網絡結構,主要有8層組成:輸入層、C1層、S2層、C3層、S4層、C5層、F6層、輸出層。
- 輸入層(input):這是一個輸入32* 32的單通道的圖像。
- C1層:該層為卷積層。輸入:3232,卷積核:6@55,輸出:6@28*28,步長為1, padding為VALID。
- S2層:該層為下采樣層,也叫池化層,卷積核:6@22,輸出:6@1414。
- C3層:該層為卷積層,卷積核:6@55,輸出:16@1010,步長為1, padding為VALID。
- S4層:該層為池化層,卷積核:16@22,輸出:16@55。
- C5層:該層為卷積層,卷積核:120@55,輸出:120@11,步長為1, padding為VALID。
- F6層: 該層為全連接層,采用Sigmoid 函數進行壓縮,輸出84個神經元。
- 輸出層:該層由徑向基函數(RBF)輸出10個神經元。
- LeNet5 網絡能夠總結為如下幾點:
- 使用卷積。參數較少(局部連接和共享權重),在卷積運算中,能夠使原信息的特征增強,并且降低噪聲。
- 下采樣(subsample)。將每個相鄰區域的四個像素求和變為一個像素,殘生一個縮小1/4的特征映射圖。
- 多層神經網絡(MLP)作為最后的分類器,層與層之間的稀疏連接矩陣避免大的計算成本。
2.2 AlexNet
2012年,Alex Krizhevsky發表了Alexet,它是LeNet的一種更深更寬的版本,并以顯著優勢贏得了困難的 ImageNet 競賽。AlexNet將LeNet的思想擴展到了更大的能學習到遠遠更復雜的對象與對象層次的神經網絡上。
-
AlexNet的特點,首次在CNN中應用ReLU、Dropout和LRN等Trick,同時使用GPU進行運算加速。
- (1)成功使用ReLU作為CNN的激活函數,并驗證其效果在較深的網絡超過了Sigmoid,成功解決了Sigmoid在網絡較深時的梯度彌散問題。基于ReLU的深度卷積網絡比基于tanh和sigmoid的網絡訓練快數倍。使用ReLU后,值域大于等于0,值域是無界的,而tanh、sigmoid函數值域區間
,值域是有界的。所以一般在ReLU之后會做一個normalization。雖然ReLU激活函數在很久之前就被提出了,但是直到AlexNet的出現才將其發揚光大。
- (2)使用Dropout。相對于一般如線性模型使用正則的方法來防止模型過擬合,而在神經網絡中Dropout通過修改神經網絡本身結構來實現。Dropout雖有單獨的論文論述,但是AlexNet將其實用化,通過實踐證實了它的效果。在AlexNet中主要是最后幾個全連接層使用了Dropout。
- (3)使用覆蓋最大池化。此前CNN中普遍使用平均池化,AlexNet全部使用最大池化,避免平均池化的模糊化效果。并且AlexNet中提出讓步長比池化核的尺寸小,這樣池化層的輸出之間會有重疊和覆蓋,提升了特征的豐富性。
- (4)提出了LRN層,對局部神經元的活動創建競爭機制,使得其中響應比較大的值變得相對更大,并抑制其他反饋較小的神經元,增強了模型的泛化能力。
- (5)使用CUDA加速深度卷積網絡的訓練,利用GPU強大的并行計算能力,處理神經網絡訓練時大量的矩陣運算。AlexNet使用了兩塊GTX?580?GPU進行訓練,單個GTX?580只有3GB顯存,這限制了可訓練的網絡的最大規模。因此作者將AlexNet分布在兩個GPU上,在每個GPU的顯存中儲存一半的神經元的參數。因為GPU之間通信方便,可以互相訪問顯存,而不需要通過主機內存,所以同時使用多塊GPU也是非常高效的。同時,AlexNet的設計讓GPU之間的通信只在網絡的某些層進行,控制了通信的性能損耗。
- (6)數據增強,隨機地從
的原始圖像中截取
大小的區域(以及水平翻轉的鏡像),相當于增加了
倍的數據量。如果沒有數據增強,僅靠原始的數據量,參數眾多的CNN會陷入過擬合中,使用了數據增強后可以大大減輕過擬合,提升泛化能力。預測時,則取圖片的四個角加中間共5個位置,并進行左右翻轉,一共獲得10張圖片,對他們進行預測并對10次結果求均值。同時,AlexNet論文中提到了會對圖像的RGB數據進行PCA處理,并對主成分做一個標準差為0.1的高斯擾動,增加一些噪聲,這個Trick可以讓錯誤率再下降1%。
- (1)成功使用ReLU作為CNN的激活函數,并驗證其效果在較深的網絡超過了Sigmoid,成功解決了Sigmoid在網絡較深時的梯度彌散問題。基于ReLU的深度卷積網絡比基于tanh和sigmoid的網絡訓練快數倍。使用ReLU后,值域大于等于0,值域是無界的,而tanh、sigmoid函數值域區間
-
網絡結構
AlexNet網絡結構
2.3 VGG
VGG是Oxford的Visual Geometry Group的組提出的(大家應該能看出VGG名字的由來了)。VGG模型是2014年ILSVRC競賽的第二名,第一名是GoogLeNet。但是VGG模型在多個遷移學習任務中的表現要優于googLeNet。而且,從圖像中提取CNN特征,VGG模型是首選算法。它的缺點在于,參數量有140M之多,需要更大的存儲空間。它證明了增加網絡的深度能夠在一定程度上影響網絡最終的性能。VGG有兩種結構,分別是VGG16和VGG19,兩者并沒有本質上的區別,只是網絡深度不一樣。參考文章
VGG16包含了16個隱藏層(13個卷積層和3個全連接層),VGG19包含了19個隱藏層(16個卷積層和3個全連接層)。VGG網絡的結構非常一致,從頭到尾全部使用的是3x3的卷積和2x2的max pooling。
VGG16相比AlexNet的一個改進是采用使用了3個3x3卷積核來代替7x7卷積核,使用了2個3x3卷積核來代替5* 5卷積核(AlexNet的9×9或11×11過濾器)。對于給定的感受野(與輸出有關的輸入圖片的局部大小),采用堆積的小卷積核是優于采用大的卷積核,因為多層非線性層可以增加網絡深度來保證學習更復雜的模式,而且代價還比較小(參數更少)。
VGGNet則清一色使用3x3卷積。因為卷積不僅涉及到計算量,還影響到感受野。前者關系到是否方便部署到移動端、是否能滿足實時處理、是否易于訓練等,后者關系到參數更新、特征圖的大小、特征是否提取的足夠多、模型的復雜度和參數量等等。作者認為兩個3x3的卷積堆疊獲得的感受野大小,相當一個5x5的卷積;而3個3x3卷積的堆疊獲取到的感受野相當于一個7x7的卷積。
feature map維度的整體變化過程是:先將local信息壓縮,并分攤到channel層級,然后無視channel和local,通過fc這個變換再進一步壓縮為稠密的feature map,這樣對于分類器而言有好處也有壞處,好處是將local信息隱藏于/壓縮到feature map中,壞處是信息壓縮都是有損失的,相當于local信息被破壞了(分類器沒有考慮到,其實對于圖像任務而言,單張feature map上的local信息還是有用的)。
關于池化。特征信息從一開始輸入的224x224x3被變換到7x7x512,從原本較為local的信息逐漸分攤到不同channel上,隨著每次的conv和pool操作打散到channel層級上。不難發現,卷積只增加feature map的通道數,而池化只減少feature map的寬高。如今也有不少做法用大stride卷積去替代池化,未來可能沒有池化。
關于全連接。維度在最后一個卷積后達到7x7x512=25088,緊接著壓縮到4096維,可能是作者認為這個過程太急,又接一個fc4096作為緩沖,同時兩個fc4096后的relu又接dropout0.5去過渡這個過程,因為最后即將給1k-way softmax,所以又接了一個fc1000去降低softmax的學習壓力。VGG最后三個全連接層在形式上完全平移AlexNet的最后三層,超參數上只有最后一層fc有變化:bias的初始值,由AlexNet的0變為0.1,該層初始化高斯分布的標準差,由AlexNet的0.01變為0.005。超參數的變化,提出者自己的感性理解指導認為,以貢獻bias來降低標準差,相當于標準差和bias間trade-off,或許提出者實驗validate發現這個值比之前AlexNet設置的(std=0.01,bias=0)要更好。
關于全連接轉卷積。作者在測試階段把網絡中原本的三個全連接層依次變為1個conv7x7,2個conv1x1,也就是三個卷積層。改變之后,整個網絡由于沒有了全連接層,網絡中間的feature map不會固定,所以網絡對任意大小的輸入都可以處理,因而作者在緊接著的后一句說到: The resulting fully-convolutional net is then applied to the whole (uncropped) image。
1x1卷積核。VGG在最后的三個階段都用到了1x1卷積核,選用1x1卷積核的最直接原因是在維度上繼承全連接,然而作者首先認為1x1卷積可以增加決策函數(decision function,這里的決策函數就是softmax)的非線性能力,非線性是由激活函數ReLU決定的,本身1x1卷積則是線性映射,即將輸入的feature map映射到同樣維度的feature map。
同樣stride下,不同卷積核大小的特征圖和卷積參數差別不大;越大的卷積核計算量越大。
-
VGG網絡結構如下:
VGG網絡結構 -
VGG16 各層的結構和參數如下:
- C1-1層:卷積層
- 輸入:224 x 224 x 3;濾波器:64@3 x 3 x 3;輸出:224 x 224 x 64
- C1-2層:卷積層
- 輸入:224 x 224 x 64;濾波器:64@3 x 3 x 3;輸出:224 x 224 x 64
- P1層:池化層
- 輸入:224 x 224 x 64; 濾波器:64@2 x 2;輸出:112 x 112 x 64
- C2-1層:卷積層
- 輸入:112 x 112 x 64; 濾波器:128@3 x 3 x 64;輸出:112 x 112 x 128
- C2-2層:卷積層
- 輸入:112 x 112 x 64; 濾波器:128@3 x 3 x 64; 輸出:112 x 112 x 128
- P2層:池化層
- 輸入:112 x 112 x 128; 濾波器:128@2 x 2; 輸出:56 x 56 x 128
- C3-1層:卷積層
- 輸入:56 x 56 x 128; 濾波器:256@3 x 3 x 128; 輸出:56 x 56 x 256
- C3-2層:卷積層
- 輸入:56 x 56 x 128; 濾波器:256@3 x 3 x 256; 輸出:56 x 56 x 256
- C3-3層:卷積層
- 輸入:56 x 56 x 256; 濾波器:256@3 x 3 x 256; 輸出:56 x 56 x 256
- P3層:池化層
- 輸入:56 x 56 x 256; 濾波器:256@2 x 2; 輸出:28 x 28 x 256
- C4-1層:卷積層
- 輸入:28 x 28 x 256; 濾波器:512@3 x 3 x 256; 輸出:28 x 28 x 512
- C4-2層:卷積層
- 輸入:28 x 28 x 512; 濾波器:512@3 x 3 x 256; 輸出:28 x 28 x 512
- C4-3層:卷積層
- 輸入:28 x 28 x 512; 濾波器:512@3 x 3 x 256; 輸出:28 x 28 x 512
- P4層:池化層
- 輸入:28 x 28 x 512; 濾波器:512@2 x 2; 輸出:14 x 14 x 512
- C5-1層:卷積層
- 輸入:14 x 14 x 512; 濾波器:512@3 x 3 x 512; 輸出:14 x 14 x 512
- C5-2層:卷積層
- 輸入:14 x 14 x 512; 濾波器:512@3 x 3 x 512; 輸出:14 x 14 x 512
- C5-3層:卷積層
- 輸入:14 x 14 x 512; 濾波器:512@3 x 3 x 512; 輸出:14 x 14 x 512
- P5層:池化層
- 輸入:14 x 14 x 512; 濾波器:512@2 x 2; 輸出:7 x 7 x 512
- F6層:全連接層,高斯分布初始化(std=0.005),bias常數初始化(0.1)
- 輸入:25088;輸出:4096
- F7層是個全連接層,高斯分布初始化(std=0.005),bias常數初始化(0.1)
- 輸入:4096;輸出:4096
- F8層:全連接層,高斯分布初始化(std=0.005),bias常數初始化(0.1)
- 輸入:4096;輸出:1000
- 輸出層:
- soft_max
- C1-1層:卷積層
-
VGG優點
- 小卷積核。將卷積核全部替換為3x3(極少用了1x1)。幾個小濾波器(3x3)卷積層的組合比一個大濾波器(5x5或7x7)卷積層好。
- 小池化核。相比AlexNet的3x3的池化核,VGG全部為2x2的池化核;
- 層數更深,特征圖更寬。由于卷積核專注于擴大通道數、池化專注于縮小寬和高,使得模型架構上更深更寬的同時,計算量的增加放緩;
- 全連接轉卷積。網絡測試階段將訓練階段的三個全連接替換為三個卷積,測試重用訓練時的參數,使得測試得到的全卷積網絡因為沒有全連接的限制,因而可以接收任意寬或高為的輸入。
-
VGG缺點
- VGG耗費更多計算資源,其中絕大多數的參數都是來自于第一個全連接層,VGG有3個全連接層,使用了更多的參數導致更多的內存占用。
2.4 GoogLeNet
-
GoogLeNet是2014年Christian Szegedy提出的一種全新的深度學習結構,在這之前的AlexNet、VGG等結構都是通過增大網絡的深度(層數)來獲得更好的訓練效果,但層數的增加會帶來很多負作用,比如overfit、梯度消失、梯度爆炸等。因此設計出 GoogLeNet——第一個Inception架構,inception的提出追求減少深度來提升訓練結果,能更高效的利用計算資源,在相同的計算量下能提取到更多的特征,從而提升訓練結果。模型結構如下,InceptionNet-v1
inception結構的主要貢獻有兩個:一是使用1x1的卷積來進行升降維和增加信息;二是在多個尺寸上同時進行卷積再聚合。
-
多個尺寸上進行卷積再聚合。InceptionNet-v1網絡結構可以看到對輸入做了4個分支,分別用不同尺寸的filter進行卷積或池化,最后再在特征維度上拼接到一起。這種全新的結構有什么好處呢?Szegedy從多個角度進行了解釋:
解釋1:在直觀感覺上在多個尺度上同時進行卷積,能提取到不同尺度的特征。特征更為豐富也意味著最后分類判斷時更加準確。
-
解釋2:利用稀疏矩陣分解成密集矩陣計算的原理來加快收斂速度。舉個例子圖四左側是個稀疏矩陣(很多元素都為0,不均勻分布在矩陣中),和一個2x2的矩陣進行卷積,需要對稀疏矩陣中的每一個元素進行計算;如果像右圖那樣把稀疏矩陣分解成2個子密集矩陣,再和2x2矩陣進行卷積,稀疏矩陣中0較多的區域就可以不用計算,計算量就大大降低。這個原理應用到inception上就是要在特征維度上進行分解!傳統的卷積層的輸入數據只和一種尺度(比如3x3)的卷積核進行卷積,輸出固定維度(比如256個特征)的數據,所有256個輸出特征基本上是均勻分布在3x3尺度范圍上,這可以理解成輸出了一個稀疏分布的特征集;而inception模塊在多個尺度上提取特征(比如1x1,3x3,5x5),輸出的256個特征就不再是均勻分布,而是相關性強的特征聚集在一起(比如1x1的的96個特征聚集在一起,3x3的96個特征聚集在一起,5x5的64個特征聚集在一起),這可以理解成多個密集分布的子特征集。這樣的特征集中因為相關性較強的特征聚集在了一起,不相關的非關鍵特征就被弱化,同樣是輸出256個特征,inception方法輸出的特征“冗余”的信息較少。用這樣的“純”的特征集層層傳遞最后作為反向計算的輸入,自然收斂的速度更快。稀疏矩陣分解
解釋3:Hebbin赫布原理。Hebbin原理是神經科學上的一個理論,解釋了在學習的過程中腦中的神經元所發生的變化,用一句話概括就是fire togethter, wire together。赫布認為“兩個神經元或者神經元系統,如果總是同時興奮,就會形成一種‘組合’,其中一個神經元的興奮會促進另一個的興奮”。比如狗看到肉會流口水,反復刺激后,腦中識別肉的神經元會和掌管唾液分泌的神經元會相互促進,“纏繞”在一起,以后再看到肉就會更快流出口水。用在inception結構中就是要把相關性強的特征匯聚到一起。這有點類似上面的解釋2,把1x1,3x3,5x5的特征分開。因為訓練收斂的最終目的就是要提取出獨立的特征,所以預先把相關性強的特征匯聚,就能起到加速收斂的作用。
2.5 ResNet
- 殘差網絡是由來自Microsoft Research的4位學者提出的卷積神經網絡,在2015年的ImageNet大規模視覺識別競賽(ILSVRC)中獲得了圖像分類和物體識別的優勝。殘差網絡的特點是容易優化,并且能夠通過增加相當的深度來提高準確率。其內部的殘差塊使用了跳躍連接,緩解了在深度神經網絡中增加深度帶來的梯度消失問題。
2.5.1 背景
- 我們都知道增加網絡的寬度和深度可以很好的提高網絡的性能,深的網絡一般都比淺的的網絡效果好,比如說一個深的網絡A和一個淺的網絡B,那A的性能至少都能跟B一樣,為什么呢?因為就算我們把B的網絡參數全部遷移到A的前面幾層,而A后面的層只是做一個等價的映射,就達到了B網絡的一樣的效果。在深度學習中,網絡層數增多一般會伴著下面幾個問題?
- 梯度彌散或梯度爆炸。對于原來的網絡,如果簡單地增加深度,會導致梯度彌散或梯度爆炸,對于該問題的解決方法是正則化初始化和中間的正則化層(Batch Normalization)。
- 計算資源的消耗。通過GPU集群來解決。
- 過擬合。過擬合通過采集海量數據,并配合Dropout正則化等方法可以有效避免;
- 退化問題。網絡層數增加,但是在訓練集上的準確率卻飽和甚至下降了。這個不能解釋為overfitting,因為overfit應該表現為在訓練集上表現更好才對。退化問題說明了深度網絡不能很簡單地被很好地優化。
- 作者通過實驗:通過淺層網絡等同映射構造深層模型,結果深層模型并沒有比淺層網絡有等同或更低的錯誤率,推斷退化問題可能是因為深層的網絡并不是那么好訓練,也就是求解器很難去利用多層網絡擬合同等函數。
2.5.3 網絡結構
-
下圖展示不同layer的ResNet.
ResNet 不同層數的網絡配置
在使用ResNet網絡結構時,發現層數不斷加深導致訓練集上誤差在增大的現象被消除了,并且在測試集上的表現也變好了。在2015年ResNet推出不久后,Google就借鑒了ResNet的精髓,2016年提出了InceptionV4的Inception-ResNet-V2。在2016年作者有提出了ResNet-V2,V1和V2的主要區別在于,作者發現殘差單元的傳播方式中,前饋和后饋的信息可以直接傳輸,因此將skip connection的非線性激活函數替換為Identity Mappings(y=x),同時v2在每一層都使用了Batch Normalization,新的殘差單元比以前的泛化性能更強了。有學者認為,ResNet類似于一個沒有gates的LSTM網絡,輸入X傳遞到后面層的過程一直發生,而不是學習出來的。也有人認為ResNet的效果類似于在多層網絡間的集成(ensemble)。The Power of Depth for Feedforward Neural Networks證明了加深網絡比加寬網絡更有效。 -
通過同樣是34層的卷積神經網絡,下圖展示VGGNet-19、普通的卷積神經網絡和ResNet的對比,可以看到ResNet和普通神經網絡的最大區別在于,ResNet有很多支線連接到后面的層,使得后面的層可以直接學習殘差,這種結構被稱為shortcut或skip connections。VGGNet-19、普通的卷積神經網絡、ResNet
-
ResNet使用了一個非常深的CNN,有50,101,152,200層等。能夠訓練如此深的網絡的關鍵是使用跳過連接(skip connection,也稱為快捷連接),一個層的輸入信號也被添加到位于下一層的輸出。當訓練一個神經網絡時,目標是使其模擬一個目標函數h(x)。如果將輸入x添加到網絡的輸出中(即添加跳過連接),那么網絡將被迫模擬
而不是h(x)。這被稱為殘留學習(見下圖,右圖是2016年改進后的resnet)。
RESNET短鏈接 -
作者還提出了兩層、三層的殘差學習單元。兩層的殘差學習單元中包含兩個相同輸出通道數的
卷積;而三層的殘差單元,第二層使用了
的卷積,第一和第三層使用了NetworkInNetwork 和 InceptionNet中的
卷積,有先降維再升維的操作。此外,如果網絡的輸入和輸出維度不同,需要對x做一個映射。再連接到后面的維度。
兩層、三層的ResNet殘差模塊
優點
- (1)減少信息丟失。ResNet通過直接將輸入信息短連接到輸出,保護信息的完整性。
- (2)簡化學習目標和難度。整個網絡只需要學習輸入、輸出差別的那一部分。
tensorflow2 代碼實現
關于網絡的實現,網上有很多代碼,大家可以根據不同的框架和編碼習慣,挑選一篇。
# TensorFlow實現LeNet
import tensorflow as tf
def inference(inputs):
# input shape: [batch, height, width, 1]
with tf.variable_scope('conv1'):
weights = tf.Variable(tf.truncated_normal([5, 5, 1, 6], stddev=0.1))
biases = tf.Variable(tf.zeros([6]))
conv1 = tf.nn.conv2d(inputs, weights, strides=[1, 1, 1, 1], padding='VALID')
conv1 = tf.nn.relu(tf.nn.bias_add(conv1, biases))
maxpool2 = tf.nn.max_pool(conv1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1])
with tf.variable_scope('conv3'):
weights = tf.Variable(tf.truncated_normal([5, 5, 6, 16], stddev=0.1))
biases = tf.Variable(tf.zeros([16]))
conv3 = tf.nn.conv2d(maxpool2, weights, strides=[1, 1, 1, 1])
conv3 = tf.nn.relu(tf.nn.bias_add(conv3, biases))
maxpool4 = tf.nn.max_pool(conv3, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1])
with tf.variable_scope('conv5'):
weights = tf.Variable(tf.truncated_normal([5, 5, 6, 16], stddev=0.1))
biases = tf.Variable(tf.zeros([16]))
conv5 = tf.nn.conv2d(maxpool4, weights, strides=[1, 1, 1, 1])
conv5 = tf.nn.relu(tf.nn.bias_add(conv5, biases))
with tf.variable_scope('fc6'):
flat = tf.reshape(conv5, [-1, 120])
weights = tf.Variable(tf.truncated_normal([120, 84], stddev=0.1))
biases = tf.Variable(tf.zeros([84]))
fc6 = tf.nn.matmul(flat, weights) + biases
fc6 = tf.nn.relu(fc6)
with tf.variable_scope('fc7'):
weights = tf.Variable(tf.truncated_normal([84, 10], stddev=0.1))
biases = tf.Variable(tf.zeros([10]))
fc7 = tf.nn.matmul(fc6, weights) + biases
fc7 = tf.nn.softmax(fc7)
return fc7
"""
tf2 搭建ResNet AlexNet 實戰
"""
import tensorflow as tf
from tensorflow.keras.datasets import fashion_mnist
from tensorflow import data as tfdata
import numpy as np
# 將 GPU 的顯存使用策略設置為 “僅在需要時申請顯存空間”。
for gpu in tf.config.experimental.list_physical_devices('GPU'):
tf.config.experimental.set_memory_growth(gpu, True)
# 1、讀取數據
'''由于Imagenet數據集是一個比較龐大的數據集,且網絡的輸入為224*224,所以,我們定義一個方法,
來讀取數據集并將數據resize到224*224的大小'''
class Data_load():
def __init__(self):
fashion_mnist = tf.keras.datasets.fashion_mnist
(self.train_images, self.train_labels), (self.test_images, self.test_labels)\
= fashion_mnist.load_data()
# 數據維度擴充成[n,h,w,c]的模式
self.train_images = np.expand_dims(self.train_images.astype(np.float32) / 255.0, axis=-1)
self.test_images = np.expand_dims(self.test_images.astype(np.float32)/255.0,axis=-1)
# 標簽
self.train_labels = self.train_labels.astype(np.int32)
self.test_labels = self.test_labels.astype(np.int32)
# 訓練和測試的數據個數
self.num_train, self.num_test = self.train_images.shape[0], self.test_images.shape[0]
def get_train_batch(self,batch_size):
# 隨機取batch_size個索引
index = np.random.randint(0, np.shape(self.train_images)[0], batch_size)
# resize
resized_images = tf.image.resize_with_pad(self.train_images[index], 224, 224 )
return resized_images.numpy(), self.train_labels[index]
def get_test_batch(self,batch_size):
index = np.random.randint(0, np.shape(self.test_images)[0], batch_size)
# resize
resized_images = tf.image.resize_with_pad(self.test_images[index], 224, 224 )
return resized_images.numpy(), self.test_labels[index]
# 2、定義模型
def MyAlexNet():
net=tf.keras.Sequential()
net.add(tf.keras.layers.Conv2D(96,11,(4,4),"same",activation="relu"))
net.add(tf.keras.layers.MaxPool2D(pool_size=3, strides=2))
net.add(tf.keras.layers.Conv2D(filters=256, kernel_size=5, padding='same', activation='relu'))
net.add(tf.keras.layers.MaxPool2D(pool_size=3, strides=2))
net.add(tf.keras.layers.Conv2D(filters=384, kernel_size=3, padding='same', activation='relu'))
net.add(tf.keras.layers.Conv2D(filters=384, kernel_size=3, padding='same', activation='relu'))
net.add(tf.keras.layers.Conv2D(filters=256, kernel_size=3, padding='same', activation='relu'))
net.add(tf.keras.layers.MaxPool2D(pool_size=3, strides=2))
net.add(tf.keras.layers.Flatten())
net.add(tf.keras.layers.Dense(512, activation='relu'))# 為了方便訓練 神經元個數改小,原來是1024
net.add(tf.keras.layers.Dropout(0.5))
net.add(tf.keras.layers.Dense(256, activation='relu'))# 為了方便訓練 神經元個數改小,原來是1024
net.add(tf.keras.layers.Dropout(0.5))
net.add(tf.keras.layers.Dense(10, activation='sigmoid'))
return net
def train(num_epoches,net):
optimizer = tf.keras.optimizers.SGD(learning_rate=0.01, momentum=0.0, nesterov=False)
net.compile(optimizer=optimizer,
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
num_iter = data_load.num_train // batch_size
for e in range(num_epoches):
for n in range(num_iter):
x_batch, y_batch = data_load.get_train_batch(batch_size)
test_x_batch, test_y_batch = data_load.get_test_batch(batch_size)
net.fit(x_batch, y_batch,validation_data=(test_x_batch, test_y_batch))
if __name__ == '__main__':
# 加載數據
batch_size=64
data_load=Data_load()
x_train_batch,y_train_batch=data_load.get_train_batch(batch_size)
print("x_batch shape:",x_train_batch.shape,"y_batch shape:", y_train_batch.shape)
# 加載網絡結構
net=MyAlexNet()
X = tf.random.uniform((1,224,224,1))
for layer in net.layers:
X = layer(X)
print(layer.name, 'output shape\t', X.shape)
# 訓練
num_epoches = 1
train(num_epoches, net)
"""
tf2 VGG13 實戰
"""
import tensorflow as tf
from tensorflow.keras import layers, optimizers, datasets, Sequential
import os
os.environ['TF_CPP_MIN_LOG_LEVEL']='2'
tf.random.set_seed(1)
# 定義卷積層
conv_layers = [ # 5 units of conv + max pooling
# unit 1
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'),
# unit 2
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'),
# unit 3
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'),
# unit 4
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'),
# unit 5
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')
]
# 定義全連接層
fc_layers = [
layers.Dense(256, activation=tf.nn.relu),
layers.Dense(128, activation=tf.nn.relu),
layers.Dense(100, activation=None),
]
def preprocess(x, y):
# [0~1]
x = tf.cast(x, dtype=tf.float32) / 255.
y = tf.cast(y, dtype=tf.int32)
return x,y
def main():
# [b, 32, 32, 3] => [b, 1, 1, 512]
conv_net = Sequential(conv_layers)
fc_net = Sequential(fc_layers)
conv_net.build(input_shape=[None, 32, 32, 3])
fc_net.build(input_shape=[None, 512])
optimizer = optimizers.Adam(lr=1e-4)
# [1, 2] + [3, 4] => [1, 2, 3, 4]
variables = conv_net.trainable_variables + fc_net.trainable_variables
for epoch in range(2):
for step, (x,y) in enumerate(train_db):
with tf.GradientTape() as tape:
out = conv_net(x) # [b, 32, 32, 3] => [b, 1, 1, 512]
out = tf.reshape(out, [-1, 512]) # flatten, => [b, 512]
logits = fc_net(out) # [b, 512] => [b, 100]
y_onehot = tf.one_hot(y, depth=100) # [b] => [b, 100]
# compute loss
loss = tf.losses.categorical_crossentropy(y_onehot, logits, from_logits=True)
loss = tf.reduce_mean(loss)
grads = tape.gradient(loss, variables)
optimizer.apply_gradients(zip(grads, variables))
if step %100 == 0:
print(epoch, step, 'loss:', float(loss))
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, 'acc:', acc)
if __name__ == '__main__':
(x,y), (x_test, y_test) = datasets.cifar100.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(128)
test_db = tf.data.Dataset.from_tensor_slices((x_test,y_test))
test_db = test_db.map(preprocess).batch(64)
sample = next(iter(train_db))
print('sample: \n', sample[0].shape, sample[1].shape,
tf.reduce_min(sample[0]), tf.reduce_max(sample[0]))
main()
'''
tf2 搭建ResNet
'''
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, Sequential
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:
self.downsample = Sequential()
self.downsample.add(layers.Conv2D(filter_num, (1, 1), strides=stride))
else:
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)
identity = self.downsample(inputs)
output = layers.add([out, identity])
output = tf.nn.relu(output)
return output
# Res Block 模塊。繼承keras.Model或者keras.Layer都可以
class ResNet(keras.Model):
# 第一個參數layer_dims:[2, 2, 2, 2] 4個Res Block,每個包含2個Basic Block
# 第二個參數num_classes:我們的全連接輸出,取決于輸出有多少類。
def __init__(self, layer_dims, num_classes=100): # [2, 2, 2, 2]
super(ResNet, self).__init__()
# 預處理層;實現起來比較靈活可以加 MAXPool2D,
# 從頭到尾的順序,對多個網絡層的線性堆疊。使用.add()方法將各層添加到模型中
self.stem = Sequential([layers.Conv2D(64, (3, 3), strides=(1, 1), padding='valid'),
layers.BatchNormalization(),
layers.Activation('relu'),
layers.MaxPool2D(pool_size=(2, 2), strides=(1, 1), padding='same')
])
# 創建4個Res Block;
self.layer1 = self.build_resblock(64, layer_dims[0])
self.layer2 = self.build_resblock(128, layer_dims[1], stride=2)
self.layer3 = self.build_resblock(256, layer_dims[2], stride=2)
self.layer4 = self.build_resblock(512, layer_dims[3], stride=2)
# gap:減少參數
self.gap = layers.GlobalAveragePooling2D()
self.fc = layers.Dense(num_classes)
def call(self, inputs, training=None):
# 前向運算
x = self.stem(inputs)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.gap(x) # [b, c]
x = self.fc(x) # [b, 100]
return x
def build_resblock(self, filter_num, blocks, stride=1):
res_blocks = Sequential()
res_blocks.add(BasicBlock(filter_num, stride))
for _ in range(1, blocks):
res_blocks.add(BasicBlock(filter_num, stride=1))
return res_blocks
def resnet18():
return ResNet([2, 2, 2, 2])