1. 程式人生 > >CNN網絡架構演進:從LeNet到DenseNet

CNN網絡架構演進:從LeNet到DenseNet

深度學習 為什麽 filter i+1 時間 put pat 采樣 相加

卷積神經網絡可謂是現在深度學習領域中大紅大紫的網絡框架,尤其在計算機視覺領域更是一枝獨秀。CNN從90年代的LeNet開始,21世紀初沈寂了10年,直到12年AlexNet開始又再煥發第二春,從ZF Net到VGG,GoogLeNet再到ResNet和最近的DenseNet,網絡越來越深,架構越來越復雜,解決反向傳播時梯度消失的方法也越來越巧妙。新年有假期,就好好總結一波CNN的各種經典架構吧,領略一下CNN的發展歷程中各路大神之間的智慧碰撞之美。

技術分享圖片

上面那圖是ILSVRC歷年的Top-5錯誤率,我們會按照以上經典網絡出現的時間順序對他們進行介紹。

本文將會談到以下經典的卷積神經網絡:

  1. LeNet
  2. AlexNet
  3. ZF
  4. VGG
  5. GoogLeNet
  6. ResNet
  7. DenseNet

開山之作:LeNet

技術分享圖片

閃光點:定義了CNN的基本組件,是CNN的鼻祖。

LeNet是卷積神經網絡的祖師爺LeCun在1998年提出,用於解決手寫數字識別的視覺任務。自那時起,CNN的最基本的架構就定下來了:卷積層、池化層、全連接層。如今各大深度學習框架中所使用的LeNet都是簡化改進過的LeNet-5(-5表示具有5個層),和原始的LeNet有些許不同,比如把激活函數改為了現在很常用的ReLu。

LeNet-5跟現有的conv->pool->ReLU的套路不同,它使用的方式是conv1->pool->conv2->pool2再接全連接層,但是不變的是,卷積層後緊接池化層的模式依舊不變。

技術分享圖片

以上圖為例,對經典的LeNet-5做深入分析:

  1. 首先輸入圖像是單通道的28*28大小的圖像,用矩陣表示就是[1,28,28]
  2. 第一個卷積層conv1所用的卷積核尺寸為5*5,滑動步長為1,卷積核數目為20,那麽經過該層後圖像尺寸變為24,28-5+1=24,輸出矩陣為[20,24,24]。
  3. 第一個池化層pool核尺寸為2*2,步長2,這是沒有重疊的max pooling,池化操作後,圖像尺寸減半,變為12×12,輸出矩陣為[20,12,12]。
  4. 第二個卷積層conv2的卷積核尺寸為5*5,步長1,卷積核數目為50,卷積後圖像尺寸變為8,這是因為12-5+1=8,輸出矩陣為[50,8,8].
  5. 第二個池化層pool2核尺寸為2*2,步長2,這是沒有重疊的max pooling,池化操作後,圖像尺寸減半,變為4×4,輸出矩陣為[50,4,4]。
  6. pool2後面接全連接層fc1,神經元數目為500,再接relu激活函數。
  7. 再接fc2,神經元個數為10,得到10維的特征向量,用於10個數字的分類訓練,送入softmaxt分類,得到分類結果的概率output。

LeNet的Keras實現:

def LeNet():
    model = Sequential()
    model.add(Conv2D(32,(5,5),strides=(1,1),input_shape=(28,28,1),padding=‘valid‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(MaxPooling2D(pool_size=(2,2)))
    model.add(Conv2D(64,(5,5),strides=(1,1),padding=‘valid‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(MaxPooling2D(pool_size=(2,2)))
    model.add(Flatten())
    model.add(Dense(100,activation=‘relu‘))
    model.add(Dense(10,activation=‘softmax‘))
    return model

王者歸來:AlexNet

AlexNet在2012年ImageNet競賽中以超過第二名10.9個百分點的絕對優勢一舉奪冠,從此深度學習和卷積神經網絡名聲鵲起,深度學習的研究如雨後春筍般出現,AlexNet的出現可謂是卷積神經網絡的王者歸來。

技術分享圖片

閃光點:

  • 更深的網絡
  • 數據增廣
  • ReLU
  • dropout
  • LRN

以上圖AlexNet架構為例,這個網絡前面5層是卷積層,後面三層是全連接層,最終softmax輸出是1000類,取其前兩層進行詳細說明。

  1. AlexNet共包含5層卷積層和三層全連接層,層數比LeNet多了不少,但卷積神經網絡總的流程並沒有變化,只是在深度上加了不少。

  2. AlexNet針對的是1000類的分類問題,輸入圖片規定是256×256的三通道彩色圖片,為了增強模型的泛化能力,避免過擬合,作者使用了隨機裁剪的思路對原來256×256的圖像進行隨機裁剪,得到尺寸為3×224×224的圖像,輸入到網絡訓練。

技術分享圖片

  1. 因為使用多GPU訓練,所以可以看到第一層卷積層後有兩個完全一樣的分支,以加速訓練。

  2. 針對一個分支分析:第一層卷積層conv1的卷積核尺寸為11×11,滑動步長為4,卷積核數目為48。卷積後得到的輸出矩陣為[48,55,55]。這裏的55是個難以理解的數字,作者也沒有對此說明,如果按照正常計算的話(224-11)/4+1 != 55的,所以這裏是做了padding再做卷積的,即先padiing圖像至227×227,再做卷積(227-11)/4+1 = 55。這些像素層經過relu1單元的處理,生成激活像素層,尺寸仍為2組48×55×55的像素層數據
    。然後經過歸一化處理,歸一化運算的尺度為5*5。第一卷積層運算結束後形成的像素層的規模為48×27×27。
  3. 輸入矩陣是[48,55,55].接著是池化層,做max pooling操作,池化運算的尺度為3*3,運算的步長為2,則池化後圖像的尺寸為(55-3)/2+1=27。所以得到的輸出矩陣是[48,27,27]。後面層不再重復敘述。

AlexNet用到訓練技巧:

  • 數據增廣技巧來增加模型泛化能力。
  • 用ReLU代替Sigmoid來加快SGD的收斂速度
  • Dropout:Dropout原理類似於淺層學習算法的中集成算法,該方法通過讓全連接層的神經元(該模型在前兩個全連接層引入Dropout)以一定的概率失去活性(比如0.5)失活的神經元不再參與前向和反向傳播,相當於約有一半的神經元不再起作用。在測試的時候,讓所有神經元的輸出乘0.5。Dropout的引用,有效緩解了模型的過擬合。
  • Local Responce Normalization:局部響應歸一層的基本思路是,假如這是網絡的一塊,比如是 13×13×256, LRN 要做的就是選取一個位置,比如說這樣一個位置,從這個位置穿過整個通道,能得到 256 個數字,並進行歸一化。進行局部響應歸一化的動機是,對於這張 13×13 的圖像中的每個位置來說,我們可能並不需要太多的高激活神經元。但是後來,很多研究者發現 LRN 起不到太大作用,因為並不重要,而且我們現在並不用 LRN 來訓練網絡。

AlexNet的Keras實現:

def AlexNet():

    model = Sequential()
    model.add(Conv2D(96,(11,11),strides=(4,4),input_shape=(227,227,3),padding=‘valid‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(MaxPooling2D(pool_size=(3,3),strides=(2,2)))
    model.add(Conv2D(256,(5,5),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(MaxPooling2D(pool_size=(3,3),strides=(2,2)))
    model.add(Conv2D(384,(3,3),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(Conv2D(384,(3,3),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(Conv2D(256,(3,3),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(MaxPooling2D(pool_size=(3,3),strides=(2,2)))
    model.add(Flatten())
    model.add(Dense(4096,activation=‘relu‘))
    model.add(Dropout(0.5))
    model.add(Dense(4096,activation=‘relu‘))
    model.add(Dropout(0.5))
    model.add(Dense(1000,activation=‘softmax‘))
    return model

穩步前行:ZF-Net

ZFNet是2013ImageNet分類任務的冠軍,其網絡結構沒什麽改進,只是調了調參,性能較Alex提升了不少。ZF-Net只是將AlexNet第一層卷積核由11變成7,步長由4變為2,第3,4,5卷積層轉變為384,384,256。這一年的ImageNet還是比較平靜的一屆,其冠軍ZF-Net的名堂也沒其他屆的經典網絡架構響亮。

技術分享圖片

ZF-Net的Keras實現:

def ZF_Net():
    model = Sequential()  
    model.add(Conv2D(96,(7,7),strides=(2,2),input_shape=(224,224,3),padding=‘valid‘,activation=‘relu‘,kernel_initializer=‘uniform‘))  
    model.add(MaxPooling2D(pool_size=(3,3),strides=(2,2)))  
    model.add(Conv2D(256,(5,5),strides=(2,2),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))  
    model.add(MaxPooling2D(pool_size=(3,3),strides=(2,2)))  
    model.add(Conv2D(384,(3,3),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))  
    model.add(Conv2D(384,(3,3),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))  
    model.add(Conv2D(256,(3,3),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))  
    model.add(MaxPooling2D(pool_size=(3,3),strides=(2,2)))  
    model.add(Flatten())  
    model.add(Dense(4096,activation=‘relu‘))  
    model.add(Dropout(0.5))  
    model.add(Dense(4096,activation=‘relu‘))  
    model.add(Dropout(0.5))  
    model.add(Dense(1000,activation=‘softmax‘))  
    return model

越走越深:VGG-Nets

VGG-Nets是由牛津大學VGG(Visual Geometry Group)提出,是2014年ImageNet競賽定位任務的第一名和分類任務的第二名的中的基礎網絡。VGG可以看成是加深版本的AlexNet. 都是conv layer + FC layer,在當時看來這是一個非常深的網絡了,因為層數高達十多層,我們從其論文名字就知道了(《Very Deep Convolutional Networks for Large-Scale Visual Recognition》),當然以現在的目光看來VGG真的稱不上是一個very deep的網絡。

技術分享圖片

上面一個表格是描述的是VGG-Net的網絡結構以及誕生過程。為了解決初始化(權重初始化)等問題,VGG采用的是一種Pre-training的方式,這種方式在經典的神經網絡中經常見得到,就是先訓練一部分小網絡,然後再確保這部分網絡穩定之後,再在這基礎上逐漸加深。表1從左到右體現的就是這個過程,並且當網絡處於D階段的時候,效果是最優的,因此D階段的網絡也就是VGG-16了!E階段得到的網絡就是VGG-19了!VGG-16的16指的是conv+fc的總層數是16,是不包括max pool的層數!

下面這個圖就是VGG-16的網絡結構。

技術分享圖片

由上圖看出,VGG-16的結構非常整潔,深度較AlexNet深得多,裏面包含多個conv->conv->max_pool這類的結構,VGG的卷積層都是same的卷積,即卷積過後的輸出圖像的尺寸與輸入是一致的,它的下采樣完全是由max pooling來實現。

VGG網絡後接3個全連接層,filter的個數(卷積後的輸出通道數)從64開始,然後沒接一個pooling後其成倍的增加,128、512,VGG的註意貢獻是使用小尺寸的filter,及有規則的卷積-池化操作。

閃光點:

  • 卷積層使用更小的filter尺寸和間隔

與AlexNet相比,可以看出VGG-Nets的卷積核尺寸還是很小的,比如AlexNet第一層的卷積層用到的卷積核尺寸就是11*11,這是一個很大卷積核了。而反觀VGG-Nets,用到的卷積核的尺寸無非都是1×1和3×3的小卷積核,可以替代大的filter尺寸。

3×3卷積核的優點:

  • 多個3×3的卷基層比一個大尺寸filter卷基層有更多的非線性,使得判決函數更加具有判決性
  • 多個3×3的卷積層比一個大尺寸的filter有更少的參數,假設卷基層的輸入和輸出的特征圖大小相同為C,那麽三個3×3的卷積層參數個數3×(3×3×C×C)=27CC;一個7×7的卷積層參數為49CC;所以可以把三個3×3的filter看成是一個7×7filter的分解(中間層有非線性的分解)

1*1卷積核的優點:

  • 作用是在不影響輸入輸出維數的情況下,對輸入進行線性形變,然後通過Relu進行非線性處理,增加網絡的非線性表達能力。

VGG-16的Keras實現:

def VGG_16():   
    model = Sequential()
    
    model.add(Conv2D(64,(3,3),strides=(1,1),input_shape=(224,224,3),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(Conv2D(64,(3,3),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(MaxPooling2D(pool_size=(2,2)))
    
    model.add(Conv2D(128,(3,2),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(Conv2D(128,(3,3),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(MaxPooling2D(pool_size=(2,2)))
    
    model.add(Conv2D(256,(3,3),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(Conv2D(256,(3,3),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(Conv2D(256,(3,3),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(MaxPooling2D(pool_size=(2,2)))
    
    model.add(Conv2D(512,(3,3),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(Conv2D(512,(3,3),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(Conv2D(512,(3,3),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(MaxPooling2D(pool_size=(2,2)))
    
    model.add(Conv2D(512,(3,3),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(Conv2D(512,(3,3),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(Conv2D(512,(3,3),strides=(1,1),padding=‘same‘,activation=‘relu‘,kernel_initializer=‘uniform‘))
    model.add(MaxPooling2D(pool_size=(2,2)))
    
    model.add(Flatten())
    model.add(Dense(4096,activation=‘relu‘))
    model.add(Dropout(0.5))
    model.add(Dense(4096,activation=‘relu‘))
    model.add(Dropout(0.5))
    model.add(Dense(1000,activation=‘softmax‘))
    
    return model

大浪推手:GoogLeNet

GoogLeNet在2014的ImageNet分類任務上擊敗了VGG-Nets奪得冠軍,其實力肯定是非常深厚的,GoogLeNet跟AlexNet,VGG-Nets這種單純依靠加深網絡結構進而改進網絡性能的思路不一樣,它另辟幽徑,在加深網絡的同時(22層),也在網絡結構上做了創新,引入Inception結構代替了單純的卷積+激活的傳統操作(這思路最早由Network in Network提出)。GoogLeNet進一步把對卷積神經網絡的研究推上新的高度。

技術分享圖片

閃光點:

  • 引入Inception結構
  • 中間層的輔助LOSS單元
  • 後面的全連接層全部替換為簡單的全局平均pooling

技術分享圖片

上圖結構就是Inception,結構裏的卷積stride都是1,另外為了保持特征響應圖大小一致,都用了零填充。最後每個卷積層後面都立刻接了個ReLU層。在輸出前有個叫concatenate的層,直譯的意思是“並置”,即把4組不同類型但大小相同的特征響應圖一張張並排疊起來,形成新的特征響應圖。Inception結構裏主要做了兩件事:1. 通過3×3的池化、以及1×1、3×3和5×5這三種不同尺度的卷積核,一共4種方式對輸入的特征響應圖做了特征提取。2. 為了降低計算量。同時讓信息通過更少的連接傳遞以達到更加稀疏的特性,采用1×1卷積核來實現降維。

這裏想再詳細談談1×1卷積核的作用,它究竟是怎麽實現降維的。下面圖1是3×3卷積核的卷積,圖2是1×1卷積核的卷積過程。對於單通道輸入,1×1的卷積確實不能起到降維作用,但對於多通道輸入,就不不同了。考慮[50,200,200]的矩陣輸入,我們可以使用20個1×1的卷積核進行卷積,得到輸出[20,200,200]。有人問,我用20個3×3的卷積核不是也能得到[20,200,200]的矩陣輸出嗎,為什麽就1×1的可以降維?我們計算一下卷積參數就知道了,對於1×1的參數總數:20×200×200×(1×1),對於3×3的參數總數:20×200×200×(3×3),可以看出,使用1×1的參數總數僅為3×3的總數的九分之一!這就是降維!

技術分享圖片
技術分享圖片

GoogLeNet網絡結構中有3個LOSS單元,這樣的網絡設計是為了幫助網絡的收斂。在中間層加入輔助計算的LOSS單元,目的是計算損失時讓低層的特征也有很好的區分能力,從而讓網絡更好地被訓練。在論文中,這兩個輔助LOSS單元的計算被乘以0.3,然後和最後的LOSS相加作為最終的損失函數來訓練網絡。

GoogLeNet還有一個閃光點值得一提,那就是將後面的全連接層全部替換為簡單的全局平均pooling,在最後參數會變的更少。而在AlexNet中最後3層的全連接層參數差不多占總參數的90%,使用大網絡在寬度和深度允許GoogleNet移除全連接層,但並不會影響到結果的精度,在ImageNet中實現93.3%的精度,而且要比VGG還要快。

GoogLeNet的Keras實現:

def Conv2d_BN(x, nb_filter,kernel_size, padding=‘same‘,strides=(1,1),name=None):
    if name is not None:
        bn_name = name + ‘_bn‘
        conv_name = name + ‘_conv‘
    else:
        bn_name = None
        conv_name = None

    x = Conv2D(nb_filter,kernel_size,padding=padding,strides=strides,activation=‘relu‘,name=conv_name)(x)
    x = BatchNormalization(axis=3,name=bn_name)(x)
    return x

def Inception(x,nb_filter):
    branch1x1 = Conv2d_BN(x,nb_filter,(1,1), padding=‘same‘,strides=(1,1),name=None)

    branch3x3 = Conv2d_BN(x,nb_filter,(1,1), padding=‘same‘,strides=(1,1),name=None)
    branch3x3 = Conv2d_BN(branch3x3,nb_filter,(3,3), padding=‘same‘,strides=(1,1),name=None)

    branch5x5 = Conv2d_BN(x,nb_filter,(1,1), padding=‘same‘,strides=(1,1),name=None)
    branch5x5 = Conv2d_BN(branch5x5,nb_filter,(1,1), padding=‘same‘,strides=(1,1),name=None)

    branchpool = MaxPooling2D(pool_size=(3,3),strides=(1,1),padding=‘same‘)(x)
    branchpool = Conv2d_BN(branchpool,nb_filter,(1,1),padding=‘same‘,strides=(1,1),name=None)

    x = concatenate([branch1x1,branch3x3,branch5x5,branchpool],axis=3)

    return x

def GoogLeNet():
    inpt = Input(shape=(224,224,3))
    #padding = ‘same‘,填充為(步長-1)/2,還可以用ZeroPadding2D((3,3))
    x = Conv2d_BN(inpt,64,(7,7),strides=(2,2),padding=‘same‘)
    x = MaxPooling2D(pool_size=(3,3),strides=(2,2),padding=‘same‘)(x)
    x = Conv2d_BN(x,192,(3,3),strides=(1,1),padding=‘same‘)
    x = MaxPooling2D(pool_size=(3,3),strides=(2,2),padding=‘same‘)(x)
    x = Inception(x,64)#256
    x = Inception(x,120)#480
    x = MaxPooling2D(pool_size=(3,3),strides=(2,2),padding=‘same‘)(x)
    x = Inception(x,128)#512
    x = Inception(x,128)
    x = Inception(x,128)
    x = Inception(x,132)#528
    x = Inception(x,208)#832
    x = MaxPooling2D(pool_size=(3,3),strides=(2,2),padding=‘same‘)(x)
    x = Inception(x,208)
    x = Inception(x,256)#1024
    x = AveragePooling2D(pool_size=(7,7),strides=(7,7),padding=‘same‘)(x)
    x = Dropout(0.4)(x)
    x = Dense(1000,activation=‘relu‘)(x)
    x = Dense(1000,activation=‘softmax‘)(x)
    model = Model(inpt,x,name=‘inception‘)
    return model

裏程碑式創新:ResNet

2015年何愷明推出的ResNet在ISLVRC和COCO上橫掃所有選手,獲得冠軍。ResNet在網絡結構上做了大創新,而不再是簡單的堆積層數,ResNet在卷積神經網絡的新思路,絕對是深度學習發展歷程上裏程碑式的事件。

技術分享圖片

閃光點:

  • 層數非常深,已經超過百層
  • 引入殘差單元來解決退化問題

從前面可以看到,隨著網絡深度增加,網絡的準確度應該同步增加,當然要註意過擬合問題。但是網絡深度增加的一個問題在於這些增加的層是參數更新的信號,因為梯度是從後向前傳播的,增加網絡深度後,比較靠前的層梯度會很小。這意味著這些層基本上學習停滯了,這就是梯度消失問題。深度網絡的第二個問題在於訓練,當網絡更深時意味著參數空間更大,優化問題變得更難,因此簡單地去增加網絡深度反而出現更高的訓練誤差,深層網絡雖然收斂了,但網絡卻開始退化了,即增加網絡層數卻導致更大的誤差,比如下圖,一個56層的網絡的性能卻不如20層的性能好,這不是因為過擬合(訓練集訓練誤差依然很高),這就是煩人的退化問題。殘差網絡ResNet設計一種殘差模塊讓我們可以訓練更深的網絡。

技術分享圖片

這裏詳細分析一下殘差單元來理解ResNet的精髓。

從下圖可以看出,數據經過了兩條路線,一條是常規路線,另一條則是捷徑(shortcut),直接實現單位映射的直接連接的路線,這有點類似與電路中的“短路”。通過實驗,這種帶有shortcut的結構確實可以很好地應對退化問題。我們把網絡中的一個模塊的輸入和輸出關系看作是y=H(x),那麽直接通過梯度方法求H(x)就會遇到上面提到的退化問題,如果使用了這種帶shortcut的結構,那麽可變參數部分的優化目標就不再是H(x),若用F(x)來代表需要優化的部分的話,則H(x)=F(x)+x,也就是F(x)=H(x)-x。因為在單位映射的假設中y=x就相當於觀測值,所以F(x)就對應著殘差,因而叫殘差網絡。為啥要這樣做,因為作者認為學習殘差F(X)比直接學習H(X)簡單!設想下,現在根據我們只需要去學習輸入和輸出的差值就可以了,絕對量變為相對量(H(x)-就是輸出相對於輸入變化了多少),優化起來簡單很多。

考慮到x的維度與F(X)維度可能不匹配情況,需進行維度匹配。這裏論文中采用兩種方法解決這一問題(其實是三種,但通過實驗發現第三種方法會使performance急劇下降,故不采用):

  • zero_padding:對恒等層進行0填充的方式將維度補充完整。這種方法不會增加額外的參數
  • projection:在恒等層采用1x1的卷積核來增加維度。這種方法會增加額外的參數

技術分享圖片

下圖展示了兩種形態的殘差模塊,左圖是常規殘差模塊,有兩個3×3卷積核卷積核組成,但是隨著網絡進一步加深,這種殘差結構在實踐中並不是十分有效。針對這問題,右圖的“瓶頸殘差模塊”(bottleneck residual block)可以有更好的效果,它依次由1×1、3×3、1×1這三個卷積層堆積而成,這裏的1×1的卷積能夠起降維或升維的作用,從而令3×3的卷積可以在相對較低維度的輸入上進行,以達到提高計算效率的目的。

技術分享圖片

ResNet-50的Keras實現:

def Conv2d_BN(x, nb_filter,kernel_size, strides=(1,1), padding=‘same‘,name=None):
    if name is not None:
        bn_name = name + ‘_bn‘
        conv_name = name + ‘_conv‘
    else:
        bn_name = None
        conv_name = None

    x = Conv2D(nb_filter,kernel_size,padding=padding,strides=strides,activation=‘relu‘,name=conv_name)(x)
    x = BatchNormalization(axis=3,name=bn_name)(x)
    return x

def Conv_Block(inpt,nb_filter,kernel_size,strides=(1,1), with_conv_shortcut=False):
    x = Conv2d_BN(inpt,nb_filter=nb_filter[0],kernel_size=(1,1),strides=strides,padding=‘same‘)
    x = Conv2d_BN(x, nb_filter=nb_filter[1], kernel_size=(3,3), padding=‘same‘)
    x = Conv2d_BN(x, nb_filter=nb_filter[2], kernel_size=(1,1), padding=‘same‘)
    if with_conv_shortcut:
        shortcut = Conv2d_BN(inpt,nb_filter=nb_filter[2],strides=strides,kernel_size=kernel_size)
        x = add([x,shortcut])
        return x
    else:
        x = add([x,inpt])
        return x

def ResNet50():
    inpt = Input(shape=(224,224,3))
    x = ZeroPadding2D((3,3))(inpt)
    x = Conv2d_BN(x,nb_filter=64,kernel_size=(7,7),strides=(2,2),padding=‘valid‘)
    x = MaxPooling2D(pool_size=(3,3),strides=(2,2),padding=‘same‘)(x)
    
    x = Conv_Block(x,nb_filter=[64,64,256],kernel_size=(3,3),strides=(1,1),with_conv_shortcut=True)
    x = Conv_Block(x,nb_filter=[64,64,256],kernel_size=(3,3))
    x = Conv_Block(x,nb_filter=[64,64,256],kernel_size=(3,3))
    
    x = Conv_Block(x,nb_filter=[128,128,512],kernel_size=(3,3),strides=(2,2),with_conv_shortcut=True)
    x = Conv_Block(x,nb_filter=[128,128,512],kernel_size=(3,3))
    x = Conv_Block(x,nb_filter=[128,128,512],kernel_size=(3,3))
    x = Conv_Block(x,nb_filter=[128,128,512],kernel_size=(3,3))
    
    x = Conv_Block(x,nb_filter=[256,256,1024],kernel_size=(3,3),strides=(2,2),with_conv_shortcut=True)
    x = Conv_Block(x,nb_filter=[256,256,1024],kernel_size=(3,3))
    x = Conv_Block(x,nb_filter=[256,256,1024],kernel_size=(3,3))
    x = Conv_Block(x,nb_filter=[256,256,1024],kernel_size=(3,3))
    x = Conv_Block(x,nb_filter=[256,256,1024],kernel_size=(3,3))
    x = Conv_Block(x,nb_filter=[256,256,1024],kernel_size=(3,3))
    
    x = Conv_Block(x,nb_filter=[512,512,2048],kernel_size=(3,3),strides=(2,2),with_conv_shortcut=True)
    x = Conv_Block(x,nb_filter=[512,512,2048],kernel_size=(3,3))
    x = Conv_Block(x,nb_filter=[512,512,2048],kernel_size=(3,3))
    x = AveragePooling2D(pool_size=(7,7))(x)
    x = Flatten()(x)
    x = Dense(1000,activation=‘softmax‘)(x)
    
    model = Model(inputs=inpt,outputs=x)
    return model

繼往開來:DenseNet

自Resnet提出以後,ResNet的變種網絡層出不窮,都各有其特點,網絡性能也有一定的提升。本文介紹的最後一個網絡是CVPR 2017最佳論文DenseNet,論文中提出的DenseNet(Dense Convolutional Network)主要還是和ResNet及Inception網絡做對比,思想上有借鑒,但卻是全新的結構,網絡結構並不復雜,卻非常有效,在CIFAR指標上全面超越ResNet。可以說DenseNet吸收了ResNet最精華的部分,並在此上做了更加創新的工作,使得網絡性能進一步提升。

閃光點:

  • 密集連接:緩解梯度消失問題,加強特征傳播,鼓勵特征復用,極大的減少了參數量

DenseNet 是一種具有密集連接的卷積神經網絡。在該網絡中,任何兩層之間都有直接的連接,也就是說,網絡每一層的輸入都是前面所有層輸出的並集,而該層所學習的特征圖也會被直接傳給其後面所有層作為輸入。下圖是 DenseNet 的一個dense block示意圖,一個block裏面的結構如下,與ResNet中的BottleNeck基本一致:BN-ReLU-Conv(1×1)-BN-ReLU-Conv(3×3) ,而一個DenseNet則由多個這種block組成。每個DenseBlock的之間層稱為transition layers,由BN?>Conv(1×1)?>averagePooling(2×2)組成

技術分享圖片

密集連接不會帶來冗余嗎?不會!密集連接這個詞給人的第一感覺就是極大的增加了網絡的參數量和計算量。但實際上 DenseNet 比其他網絡效率更高,其關鍵就在於網絡每層計算量的減少以及特征的重復利用。DenseNet則是讓l層的輸入直接影響到之後的所有層,它的輸出為:xl=Hl([X0,X1,…,xl?1]),其中[x0,x1,...,xl?1]就是將之前的feature map以通道的維度進行合並。並且由於每一層都包含之前所有層的輸出信息,因此其只需要很少的特征圖就夠了,這也是為什麽DneseNet的參數量較其他模型大大減少的原因。這種dense connection相當於每一層都直接連接input和loss,因此就可以減輕梯度消失現象,這樣更深網絡不是問題

需要明確一點,dense connectivity 僅僅是在一個dense block裏的,不同dense block 之間是沒有dense connectivity的,比如下圖所示。

技術分享圖片

天底下沒有免費的午餐,網絡自然也不例外。在同層深度下獲得更好的收斂率,自然是有額外代價的。其代價之一,就是其恐怖如斯的內存占用。

DenseNet-121的Keras實現:

def DenseNet121(nb_dense_block=4, growth_rate=32, nb_filter=64, reduction=0.0, dropout_rate=0.0, weight_decay=1e-4, classes=1000, weights_path=None):
    ‘‘‘Instantiate the DenseNet 121 architecture,
        # Arguments
            nb_dense_block: number of dense blocks to add to end
            growth_rate: number of filters to add per dense block
            nb_filter: initial number of filters
            reduction: reduction factor of transition blocks.
            dropout_rate: dropout rate
            weight_decay: weight decay factor
            classes: optional number of classes to classify images
            weights_path: path to pre-trained weights
        # Returns
            A Keras model instance.
    ‘‘‘
    eps = 1.1e-5

    # compute compression factor
    compression = 1.0 - reduction

    # Handle Dimension Ordering for different backends
    global concat_axis
    if K.image_dim_ordering() == ‘tf‘:
      concat_axis = 3
      img_input = Input(shape=(224, 224, 3), name=‘data‘)
    else:
      concat_axis = 1
      img_input = Input(shape=(3, 224, 224), name=‘data‘)

    # From architecture for ImageNet (Table 1 in the paper)
    nb_filter = 64
    nb_layers = [6,12,24,16] # For DenseNet-121

    # Initial convolution
    x = ZeroPadding2D((3, 3), name=‘conv1_zeropadding‘)(img_input)
    x = Convolution2D(nb_filter, 7, 7, subsample=(2, 2), name=‘conv1‘, bias=False)(x)
    x = BatchNormalization(epsilon=eps, axis=concat_axis, name=‘conv1_bn‘)(x)
    x = Scale(axis=concat_axis, name=‘conv1_scale‘)(x)
    x = Activation(‘relu‘, name=‘relu1‘)(x)
    x = ZeroPadding2D((1, 1), name=‘pool1_zeropadding‘)(x)
    x = MaxPooling2D((3, 3), strides=(2, 2), name=‘pool1‘)(x)

    # Add dense blocks
    for block_idx in range(nb_dense_block - 1):
        stage = block_idx+2
        x, nb_filter = dense_block(x, stage, nb_layers[block_idx], nb_filter, growth_rate, dropout_rate=dropout_rate, weight_decay=weight_decay)

        # Add transition_block
        x = transition_block(x, stage, nb_filter, compression=compression, dropout_rate=dropout_rate, weight_decay=weight_decay)
        nb_filter = int(nb_filter * compression)

    final_stage = stage + 1
    x, nb_filter = dense_block(x, final_stage, nb_layers[-1], nb_filter, growth_rate, dropout_rate=dropout_rate, weight_decay=weight_decay)

    x = BatchNormalization(epsilon=eps, axis=concat_axis, name=‘conv‘+str(final_stage)+‘_blk_bn‘)(x)
    x = Scale(axis=concat_axis, name=‘conv‘+str(final_stage)+‘_blk_scale‘)(x)
    x = Activation(‘relu‘, name=‘relu‘+str(final_stage)+‘_blk‘)(x)
    x = GlobalAveragePooling2D(name=‘pool‘+str(final_stage))(x)

    x = Dense(classes, name=‘fc6‘)(x)
    x = Activation(‘softmax‘, name=‘prob‘)(x)

    model = Model(img_input, x, name=‘densenet‘)

    if weights_path is not None:
      model.load_weights(weights_path)

    return model


def conv_block(x, stage, branch, nb_filter, dropout_rate=None, weight_decay=1e-4):
    ‘‘‘Apply BatchNorm, Relu, bottleneck 1x1 Conv2D, 3x3 Conv2D, and option dropout
        # Arguments
            x: input tensor 
            stage: index for dense block
            branch: layer index within each dense block
            nb_filter: number of filters
            dropout_rate: dropout rate
            weight_decay: weight decay factor
    ‘‘‘
    eps = 1.1e-5
    conv_name_base = ‘conv‘ + str(stage) + ‘_‘ + str(branch)
    relu_name_base = ‘relu‘ + str(stage) + ‘_‘ + str(branch)

    # 1x1 Convolution (Bottleneck layer)
    inter_channel = nb_filter * 4  
    x = BatchNormalization(epsilon=eps, axis=concat_axis, name=conv_name_base+‘_x1_bn‘)(x)
    x = Scale(axis=concat_axis, name=conv_name_base+‘_x1_scale‘)(x)
    x = Activation(‘relu‘, name=relu_name_base+‘_x1‘)(x)
    x = Convolution2D(inter_channel, 1, 1, name=conv_name_base+‘_x1‘, bias=False)(x)

    if dropout_rate:
        x = Dropout(dropout_rate)(x)

    # 3x3 Convolution
    x = BatchNormalization(epsilon=eps, axis=concat_axis, name=conv_name_base+‘_x2_bn‘)(x)
    x = Scale(axis=concat_axis, name=conv_name_base+‘_x2_scale‘)(x)
    x = Activation(‘relu‘, name=relu_name_base+‘_x2‘)(x)
    x = ZeroPadding2D((1, 1), name=conv_name_base+‘_x2_zeropadding‘)(x)
    x = Convolution2D(nb_filter, 3, 3, name=conv_name_base+‘_x2‘, bias=False)(x)

    if dropout_rate:
        x = Dropout(dropout_rate)(x)

    return x


def transition_block(x, stage, nb_filter, compression=1.0, dropout_rate=None, weight_decay=1E-4):
    ‘‘‘ Apply BatchNorm, 1x1 Convolution, averagePooling, optional compression, dropout 
        # Arguments
            x: input tensor
            stage: index for dense block
            nb_filter: number of filters
            compression: calculated as 1 - reduction. Reduces the number of feature maps in the transition block.
            dropout_rate: dropout rate
            weight_decay: weight decay factor
    ‘‘‘

    eps = 1.1e-5
    conv_name_base = ‘conv‘ + str(stage) + ‘_blk‘
    relu_name_base = ‘relu‘ + str(stage) + ‘_blk‘
    pool_name_base = ‘pool‘ + str(stage) 

    x = BatchNormalization(epsilon=eps, axis=concat_axis, name=conv_name_base+‘_bn‘)(x)
    x = Scale(axis=concat_axis, name=conv_name_base+‘_scale‘)(x)
    x = Activation(‘relu‘, name=relu_name_base)(x)
    x = Convolution2D(int(nb_filter * compression), 1, 1, name=conv_name_base, bias=False)(x)

    if dropout_rate:
        x = Dropout(dropout_rate)(x)

    x = AveragePooling2D((2, 2), strides=(2, 2), name=pool_name_base)(x)

    return x


def dense_block(x, stage, nb_layers, nb_filter, growth_rate, dropout_rate=None, weight_decay=1e-4, grow_nb_filters=True):
    ‘‘‘ Build a dense_block where the output of each conv_block is fed to subsequent ones
        # Arguments
            x: input tensor
            stage: index for dense block
            nb_layers: the number of layers of conv_block to append to the model.
            nb_filter: number of filters
            growth_rate: growth rate
            dropout_rate: dropout rate
            weight_decay: weight decay factor
            grow_nb_filters: flag to decide to allow number of filters to grow
    ‘‘‘

    eps = 1.1e-5
    concat_feat = x

    for i in range(nb_layers):
        branch = i+1
        x = conv_block(concat_feat, stage, branch, growth_rate, dropout_rate, weight_decay)
        concat_feat = merge([concat_feat, x], mode=‘concat‘, concat_axis=concat_axis, name=‘concat_‘+str(stage)+‘_‘+str(branch))

        if grow_nb_filters:
            nb_filter += growth_rate

    return concat_feat, nb_filter

CNN網絡架構演進:從LeNet到DenseNet