ResNet详述
簡介
CV常見的卷積神經(jīng)網(wǎng)絡(luò)中,和曾今的VGG一樣,ResNet的提出及其思想對卷積神經(jīng)網(wǎng)絡(luò)的發(fā)展產(chǎn)生了巨大的影響。由于最近的課題需要回顧常見的里程碑式的CNN結(jié)構(gòu),以及分析這些結(jié)構(gòu)的劃時(shí)代意義,所以這里簡單介紹一下ResNet。本項(xiàng)目著重實(shí)現(xiàn)使用Keras搭建ResNet的網(wǎng)絡(luò)結(jié)構(gòu),同時(shí),利用其在數(shù)據(jù)集上進(jìn)行效果評測。
-
論文標(biāo)題
Deep residual learning for image recognition
-
論文地址
https://arxiv.org/abs/1512.03385
-
論文源碼
https://github.com/pytorch/vision/blob/master/torchvision/models/resnet.py(PyTorch實(shí)現(xiàn))
網(wǎng)絡(luò)說明
設(shè)計(jì)背景
ResNet(Residual Neural Network,殘差神經(jīng)網(wǎng)絡(luò))由Kaiming He(何愷明大神)等人在2015年的提出。他們通過特殊的網(wǎng)絡(luò)結(jié)構(gòu)設(shè)計(jì),訓(xùn)練出了一個(gè)152層的深度神經(jīng)網(wǎng)絡(luò),并在ImageNet比賽分類任務(wù)上獲得了冠軍(top-5錯(cuò)誤率3.57%)。
ResNet的提出第一次有事實(shí)地打破了深度網(wǎng)絡(luò)無法訓(xùn)練的難題,其模型的不僅在多個(gè)數(shù)據(jù)集上準(zhǔn)確率得到提高,而且參數(shù)量還比VGG少(其實(shí)縱觀卷積神經(jīng)網(wǎng)絡(luò)這幾年的發(fā)展都是網(wǎng)絡(luò)越來越深、越來越輕量)。
自此之后,很多神經(jīng)網(wǎng)絡(luò)的設(shè)計(jì)都借鑒了ResNet的思想,如Google InceptionV4,DenseNet等,其思想使得卷積模型的性能進(jìn)一步提高。
設(shè)計(jì)思路
以往問題
- 實(shí)驗(yàn)結(jié)果表明,層數(shù)的增加會提高網(wǎng)絡(luò)的學(xué)習(xí)效果(理想情況下),但是,單純增加網(wǎng)絡(luò)深度,神經(jīng)網(wǎng)絡(luò)的學(xué)習(xí)會變得十分困難,因此反而不能得到預(yù)期效果。(論文中提出,這個(gè)原理很容易理解,隨著深度增加,以鏈?zhǔn)椒▌t為基礎(chǔ)的反向傳播會出現(xiàn)難以傳播的問題,這引出很多著名的訓(xùn)練問題,如梯度消失。)
- 通常認(rèn)為神經(jīng)網(wǎng)絡(luò)的深度對其性能影響較大,越深的網(wǎng)絡(luò)往往能得到更好的性能(網(wǎng)絡(luò)越深效果越好是不合理的觀念),但是隨著網(wǎng)絡(luò)的加深,容易出現(xiàn)Degradation問題,即準(zhǔn)確率上升然后飽和繼而下降的現(xiàn)象,導(dǎo)致訓(xùn)練困難,這個(gè)問題通常叫做梯度退化(gradient degradation)。注意,這并非常見的過擬合(overfit)問題,因?yàn)闊o論訓(xùn)練集還是驗(yàn)證集loss都會加大。
解決方法
- 為了解決上述的問題,ResNet提出了一種殘差模塊(residual block)用于解決這個(gè)問題。既然網(wǎng)絡(luò)加深到一定的程度會出現(xiàn)準(zhǔn)確率飽和和準(zhǔn)確率下降問題,那么,不妨在特征傳遞的過程中,讓后續(xù)網(wǎng)絡(luò)層的傳遞媒介影響降低,使用全等映射將輸入直接傳遞給輸出,保證網(wǎng)絡(luò)的性能至少不會下降。
- 在上面的殘差模塊中,提供了兩條道路,一條是經(jīng)過residual映射得到F(x)F(x)F(x),其計(jì)算式可以理解為F(x)=relu(w2?(relu(w1?x)))F(x)=r e l u\left(w_{2} *\left(r e l u\left(w_{1} * x\right)\right)\right)F(x)=relu(w2??(relu(w1??x))),另一條則是直接傳遞xxx本身。將這兩個(gè)結(jié)果合并之后激活傳入下一個(gè)模塊,即輸出H(x)=F(x)+xH(x)=F(x)+xH(x)=F(x)+x(這個(gè)操作沒有加大網(wǎng)絡(luò)運(yùn)算的復(fù)雜度,并且常用的深度學(xué)習(xí)框架很容易執(zhí)行)。當(dāng)網(wǎng)絡(luò)訓(xùn)練已經(jīng)達(dá)到最優(yōu),網(wǎng)絡(luò)的后續(xù)訓(xùn)練將會限制residual網(wǎng)絡(luò)的映射,當(dāng)residual被限制為0時(shí),就只剩下全等映射的x,網(wǎng)絡(luò)也不會因?yàn)樯疃鹊募由钤斐蓽?zhǔn)確率下降了。(這樣,通過限制,下一個(gè)模塊得到的就是本模塊的輸入xxx,緩解深度難以傳遞的狀況。)
ResNet的核心,擬合殘差函數(shù)F=H(x)?g(x)=H(x)?xF=H(x)-g(x)=H(x)-xF=H(x)?g(x)=H(x)?x(選擇g(x)=xg(x)=xg(x)=x是因?yàn)榇藭r(shí)的g(x)g(x)g(x)效果最好)。其中xxx是由全等映射傳遞過去的,而F(x)F(x)F(x)是由residual映射傳遞的,網(wǎng)絡(luò)的訓(xùn)練將對F(x)F(x)F(x)部分residual網(wǎng)絡(luò)的權(quán)重進(jìn)行優(yōu)化更新,學(xué)習(xí)的目標(biāo)不再是完整的輸出H(x)H(x)H(x),而變成了輸出與輸入的差別H(x)?xH(x)-xH(x)?x,F(x)F(x)F(x)也就是殘差。
在整個(gè)ResNet中廣泛使用這種殘差模塊,該模塊使用了兩個(gè)分支的方法,其中一個(gè)分支直接將輸入傳遞到下一層,使得原始信息的更多的保留,解決了深度網(wǎng)絡(luò)信息丟失和信息損耗的問題,這種結(jié)構(gòu)被稱之為shortcut或者skip connections。
由于使用了shortcut,原來需要學(xué)習(xí)逼近的恒等映射H(x)H(x)H(x)變成逼近F(x)=H(x)?xF(x) = H(x) - xF(x)=H(x)?x這個(gè)函數(shù)。論文作者認(rèn)為這兩種表達(dá)的效果是一致的,但是優(yōu)化的難度卻不同,F(x)F(x)F(x)的優(yōu)化比H(x)H(x)H(x)的優(yōu)化簡單很多(該想法源于圖像處理中的殘差向量編碼)。
殘差網(wǎng)絡(luò)
shortcuts
在之前的圖片上,恒等映射xxx使用的為identity shortcuts(同維度元素級相加);事實(shí)上,論文還研究了一種project shortcuts(y=F(x,Wi)+Wsxy=F(x,{W_i})+W_sxy=F(x,Wi?)+Ws?x),主要包含以下三種情況,且通過實(shí)驗(yàn)效果是逐漸變好的。
- 維度無變化則直接相連,維度增加的連接通過補(bǔ)零填充后再連接。shortcuts是恒等的,這個(gè)連接并不會帶來新的參數(shù)。
- 維度無變化則直接相連,維度增加的連接通過投影連接,投影連接會增加參數(shù)。
- 所有連接均采用投影連接。
bottleneck
當(dāng)研究50層以上的深層網(wǎng)絡(luò)時(shí),使用了上圖右邊所示的Bottleneck網(wǎng)絡(luò)結(jié)構(gòu),該結(jié)構(gòu)第一層使用1*1的卷積層來降維,最后一層使用1*1的卷積層來進(jìn)行升維,從而保持與原來輸入同維以便于恒等映射。
網(wǎng)絡(luò)結(jié)構(gòu)
網(wǎng)絡(luò)結(jié)構(gòu)圖
常見的ResNet為50層的ResNet50以及ResNet101和當(dāng)年比賽使用的ResNet152。 結(jié)構(gòu)上,先通過一個(gè)普通的(7,7)卷積層對輸入圖片進(jìn)行特征提取同時(shí)因?yàn)椴介L為2尺寸減半;隨即,通過(3,3)最大池化層進(jìn)一步縮小feature map的尺寸;隨后,送入各個(gè)殘差模塊(論文中命名為conv2_x的網(wǎng)絡(luò)為一個(gè)block,同一個(gè)block有多個(gè)殘差模塊連接)。最后,將特征圖送入全局池化層進(jìn)行規(guī)整,再使用softmax激活進(jìn)行分類,得到概率分布向量。(這里fc層輸出1000類是因?yàn)镮mageNet有1000個(gè)類別)
雖然,ResNet比起VGG19這樣的網(wǎng)絡(luò)深很多,但是運(yùn)算量是遠(yuǎn)少于VGG19等VGGNet的。
網(wǎng)絡(luò)對比
左側(cè)為經(jīng)典VGG19結(jié)構(gòu)圖,中間為類VGG19的34層普通網(wǎng)絡(luò)圖,右側(cè)為帶恒等映射的34層ResNet網(wǎng)絡(luò)圖。其中,黑色實(shí)線代表同一維度下(卷積核數(shù)目相同)的恒等映射,虛線代表不同維度下(卷積核數(shù)目不同)的恒等映射。
訓(xùn)練效果
左側(cè)為普通網(wǎng)絡(luò),右側(cè)為殘差網(wǎng)絡(luò),粗線代表訓(xùn)練損失,細(xì)線代表驗(yàn)證損失。顯然,普通網(wǎng)絡(luò)34層訓(xùn)練損失和驗(yàn)證損失均大于18層,殘差網(wǎng)絡(luò)不存在這個(gè)現(xiàn)象,這說明殘差網(wǎng)絡(luò)確實(shí)有效解決了梯度退化問題(這也是ResNet的初衷)。
代碼實(shí)現(xiàn)
實(shí)際使用各個(gè)深度學(xué)習(xí)框架已經(jīng)封裝了ResNet的幾種主要網(wǎng)絡(luò)結(jié)構(gòu),使用很方便,不建議自己搭建(尤其對于ResNet152這樣很深的網(wǎng)絡(luò))。
下面使用Keras構(gòu)建ResNet34和ResNet50,前者使用identity block作為殘差模塊,后者使用bottleneck block作為殘差模塊,同時(shí)為了防止過擬合且輸出高斯分布,自定義了緊跟BN層的卷積層Conv2D_BN。
網(wǎng)絡(luò)構(gòu)建對照結(jié)構(gòu)表及結(jié)構(gòu)說明即可,這是復(fù)現(xiàn)論文網(wǎng)絡(luò)結(jié)構(gòu)的主要依據(jù)。
def Conv2D_BN(x, filters, kernel_size, strides=(1, 1), padding='same', name=None):if name:bn_name = name + '_bn'conv_name = name + '_conv'else:bn_name = Noneconv_name = Nonex = Conv2D(filters, kernel_size, strides=strides, padding=padding, activation='relu', name=conv_name)(x)x = BatchNormalization(name=bn_name)(x)return xdef identity_block(input_tensor, filters, kernel_size, strides=(1, 1), is_conv_shortcuts=False):""":param input_tensor::param filters::param kernel_size::param strides::param is_conv_shortcuts: 直接連接或者投影連接:return:"""x = Conv2D_BN(input_tensor, filters, kernel_size, strides=strides, padding='same')x = Conv2D_BN(x, filters, kernel_size, padding='same')if is_conv_shortcuts:shortcut = Conv2D_BN(input_tensor, filters, kernel_size, strides=strides, padding='same')x = add([x, shortcut])else:x = add([x, input_tensor])return xdef bottleneck_block(input_tensor, filters=(64, 64, 256), strides=(1, 1), is_conv_shortcuts=False):""":param input_tensor::param filters::param strides::param is_conv_shortcuts: 直接連接或者投影連接:return:"""filters_1, filters_2, filters_3 = filtersx = Conv2D_BN(input_tensor, filters=filters_1, kernel_size=(1, 1), strides=strides, padding='same')x = Conv2D_BN(x, filters=filters_2, kernel_size=(3, 3))x = Conv2D_BN(x, filters=filters_3, kernel_size=(1, 1))if is_conv_shortcuts:short_cut = Conv2D_BN(input_tensor, filters=filters_3, kernel_size=(1, 1), strides=strides)x = add([x, short_cut])else:x = add([x, input_tensor])return xdef ResNet34(input_shape=(224, 224, 3), n_classes=1000):""":param input_shape::param n_classes::return:"""input_layer = Input(shape=input_shape)x = ZeroPadding2D((3, 3))(input_layer)# block1x = Conv2D_BN(x, filters=64, kernel_size=(7, 7), strides=(2, 2), padding='valid')x = MaxPooling2D(pool_size=(3, 3), strides=2, padding='same')(x)# block2x = identity_block(x, filters=64, kernel_size=(3, 3))x = identity_block(x, filters=64, kernel_size=(3, 3))x = identity_block(x, filters=64, kernel_size=(3, 3))# block3x = identity_block(x, filters=128, kernel_size=(3, 3), strides=(2, 2), is_conv_shortcuts=True)x = identity_block(x, filters=128, kernel_size=(3, 3))x = identity_block(x, filters=128, kernel_size=(3, 3))x = identity_block(x, filters=128, kernel_size=(3, 3))# block4x = identity_block(x, filters=256, kernel_size=(3, 3), strides=(2, 2), is_conv_shortcuts=True)x = identity_block(x, filters=256, kernel_size=(3, 3))x = identity_block(x, filters=256, kernel_size=(3, 3))x = identity_block(x, filters=256, kernel_size=(3, 3))x = identity_block(x, filters=256, kernel_size=(3, 3))x = identity_block(x, filters=256, kernel_size=(3, 3))# block5x = identity_block(x, filters=512, kernel_size=(3, 3), strides=(2, 2), is_conv_shortcuts=True)x = identity_block(x, filters=512, kernel_size=(3, 3))x = identity_block(x, filters=512, kernel_size=(3, 3))x = AveragePooling2D(pool_size=(7, 7))(x)x = Flatten()(x)x = Dense(n_classes, activation='softmax')(x)model = Model(inputs=input_layer, outputs=x)return modeldef ResNet50(input_shape=(224, 224, 3), n_classes=1000):""":param input_shape::param n_classes::return:"""input_layer = Input(shape=input_shape)x = ZeroPadding2D((3, 3))(input_layer)# block1x = Conv2D_BN(x, filters=64, kernel_size=(7, 7), strides=(2, 2), padding='valid')x = MaxPooling2D(pool_size=(3, 3), strides=(2, 2), padding='same')(x)# block2x = bottleneck_block(x, filters=(64, 64, 256), strides=(1, 1), is_conv_shortcuts=True)x = bottleneck_block(x, filters=(64, 64, 256))x = bottleneck_block(x, filters=(64, 64, 256))# block3x = bottleneck_block(x, filters=(128, 128, 512), strides=(2, 2), is_conv_shortcuts=True)x = bottleneck_block(x, filters=(128, 128, 512))x = bottleneck_block(x, filters=(128, 128, 512))x = bottleneck_block(x, filters=(128, 128, 512))# block4x = bottleneck_block(x, filters=(256, 256, 1024), strides=(2, 2), is_conv_shortcuts=True)x = bottleneck_block(x, filters=(256, 256, 1024))x = bottleneck_block(x, filters=(256, 256, 1024))x = bottleneck_block(x, filters=(256, 256, 1024))x = bottleneck_block(x, filters=(256, 256, 1024))x = bottleneck_block(x, filters=(256, 256, 1024))# block5x = bottleneck_block(x, filters=(512, 512, 2048), strides=(2, 2), is_conv_shortcuts=True)x = bottleneck_block(x, filters=(512, 512, 2048))x = bottleneck_block(x, filters=(512, 512, 2048))x = AveragePooling2D(pool_size=(7, 7))(x)x = Flatten()(x)x = Dense(n_classes, activation='softmax')(x)model = Model(inputs=input_layer, outputs=x)return model數(shù)據(jù)集使用Caltech101數(shù)據(jù)集,比較性能,不進(jìn)行數(shù)據(jù)增廣(注意刪除干擾項(xiàng))。Batch大小指定為32,使用BN訓(xùn)練技巧,二次封裝Conv2D。 損失函數(shù)使用經(jīng)典分類的交叉熵?fù)p失函數(shù),優(yōu)化函數(shù)使用Adam,激活函數(shù)使用Relu。(這都是比較流行的選擇)
具體結(jié)果見文末Github倉庫根目錄notebook文件。
比較于我之前介紹的VGGNet,顯然,同一個(gè)數(shù)據(jù)集上ResNet訓(xùn)練速度快了很多,在同樣輪次的訓(xùn)練下,驗(yàn)證集到達(dá)的準(zhǔn)確率比VGGNet高很多,這正是驗(yàn)證了比起VGGNet,ResNet計(jì)算量更少(表現(xiàn)為訓(xùn)練速度快),同樣ResNet網(wǎng)絡(luò)模型效果好(表現(xiàn)為驗(yàn)證集準(zhǔn)確率高)。
補(bǔ)充說明
ResNet最核心的就是利用residual block通過轉(zhuǎn)換映射目標(biāo)從而解決梯度退化問題,這才是ResNet的核心,至于具體的網(wǎng)絡(luò)結(jié)構(gòu),不同的場景可能需求的結(jié)構(gòu)不同,應(yīng)當(dāng)明白的是其精髓。本項(xiàng)目源碼開源于我的Github,歡迎Star或者Fork。
總結(jié)