ResNetv1:https://arxiv.org/abs/1512.03385
pytorch 代碼:https://github.com/pytorch/vision/blob/master/torchvision/models/resnet.py
keras 代碼:https://github.com/keras-team/keras-applications/blob/master/keras_applications/resnet.py
1 ResNet v1要解決的問題
深度神經網絡朝着網絡層數越來越深的方向發展。直覺上我們不難得出結論:增加網絡深度後,網絡可以進行更加複雜的特徵提取,因此更深的模型可以取得更好的結果。但事實並非如此,人們發現隨着網絡深度的增加,模型精度並不總是提升,並且這個問題顯然不是由過擬合(overfitting)造成的,因爲網絡加深後不僅測試誤差變高了,它的訓練誤差竟然也變高了。作者提出,這可能是因爲更深的網絡會伴隨梯度消失/爆炸問題,從而阻礙網絡的收斂。作者將這種加深網絡深度但網絡性能卻下降的現象稱爲退化問題(degradation problem)。導致網絡性能下降的原因是梯度消失問題, 神經網絡在反向傳播過程中要不斷地傳播梯度,而當網絡層數加深時,梯度在傳播過程中會逐漸消失(假如採用Sigmoid函數,對於幅度爲1的信號,每向後傳遞一層,梯度就衰減爲原來的0.25,層數越多,衰減越厲害),導致無法對前面網絡層的權重進行有效的調整。
當傳統神經網絡的層數從20增加爲56時,網絡的訓練誤差和測試誤差均出現了明顯的增長,也就是說,網絡的性能隨着深度的增加出現了明顯的退化。ResNet就是爲了解決這種退化問題而誕生的,如圖1所示。
2 ResNet v1解決退化問題的方案
一是通過在網絡中增加BatchNormalization層, 二是構建恆等映射(Identity mapping), 恆等映射的結構如圖2所示.
這裏是第個殘差單元的輸入特徵。是一個與第個殘差單元相關的權重和偏差的集合,是殘差單元內部的層的數量。是殘差函數。函數是元素加和後的操作(ResNet-v1中採用的是ReLU)。函數是恆等映射。
爲什麼是恆等映射呢,20層的網絡是56層網絡的一個子集,56層網絡的解空間包含着20層網絡的解空間。如果我們將56層網絡的最後36層全部短接,這些層進來是什麼出來也是什麼(也就是做一個恆等映射),那這個56層網絡不就等效於20層網絡了嗎,至少效果不會相比原先的20層網絡差吧。同樣是56層網絡,不引入恆等映射爲什麼就不行呢?因爲梯度消失現象使得網絡難以訓練,雖然網絡的深度加深了,但是實際上無法有效訓練網絡,訓練不充分的網絡不但無法提升性能,甚至降低了性能。
關於殘差模塊的詳細思考將通過以下章節進行介紹。
3 ResNet v1網絡結構
ResNet主要有五種主要形式:Res18,Res34,Res50,Res101,Res152;整體網絡結構如圖3所示, 殘差單元如圖4所示, 部分網絡片段如圖5所示。
-
1數據進入網絡後先經過輸入部分(conv1, bn1, relu, maxpool);
-
然後進入中間卷積部分(layer1, layer2, layer3, layer4,這裏的layer對應我們之前所說的stage);
-
最後數據經過一個平均池化和全連接層(avgpool, fc)輸出得到結果;
從layer1-4, 每個層中的第一個殘差模塊的殘差部分採用步長爲2的卷積進行下采樣,所以在shortcut connection中要採用步長爲2的1×1的卷積進行維度的匹配,如圖5中的實線部分, 採用的計算方式爲H(x)=F(x)+Wx,其中W是卷積操作,用來調整x維度的。剩餘其他的殘差單元都採用步長爲1的卷積,shortcut connection直接連接輸入和輸出,如圖5中虛線部分,採用計算方式爲H(x)=F(x)+x。
ResNet v1網絡設計規律
整個ResNet不使用dropout,全部使用BN。此外,回到最初的這張細節圖,我們不難發現一些規律和特點:
- 受VGG的啓發,卷積層主要是3×3卷積;
- 對於相同的輸出特徵圖大小的層,即同一stage,具有相同數量的3x3濾波器;
- 如果特徵地圖大小減半,濾波器的數量加倍以保持每層的時間複雜度;
- 每個stage通過步長爲2的卷積層執行下采樣,而卻這個下采樣只會在每一個stage的第一個卷積完成,有且僅有一次。
- 網絡以平均池化層和softmax的1000路全連接層結束,實際上工程上一般用自適應全局平均池化 (Adaptive Global Average Pooling);
相比傳統的分類網絡,這裏接的是池化,而不是全連接層。池化是不需要參數的,相比於全連接層可以砍去大量的參數。對於一個7x7的特徵圖,直接池化和改用全連接層相比,可以節省將近50倍的參數,作用有二:一是節省計算資源,二是防止模型過擬合,提升泛化能力;
Bottleneck結構和1×1卷積
ResNet50起,就採用Bottleneck結構,主要是引入1x1卷積。我們來看一下這裏的1x1卷積有什麼作用:
- 對通道數進行升維和降維(跨通道信息整合),實現了多個特徵圖的線性組合,同時保持了原有的特徵圖大小;
- 相比於其他尺寸的卷積核,可以極大地降低運算複雜度;
- 如果使用兩個3x3卷積堆疊,只有一個relu,但使用1x1卷積就會有兩個relu,引入了更多的非線性映射;
我們來計算一下1*1卷積的計算量優勢:首先看上圖右邊的bottleneck結構,對於256維的輸入特徵,參數數目:1x1x256x64+3x3x64x64+1x1x64x256=69632,如果同樣的輸入輸出維度但不使用1x1卷積,而使用兩個3x3卷積的話,參數數目爲(3x3x256x256)x2=1179648。簡單計算下就知道了,使用了1x1卷積的bottleneck將計算量簡化爲原有的5.9%,收益超高。
4 ResNet v1殘差單元有效性的理解
從梯度反向傳播理解ResNet的有效性
緊接第二部分公式1和公式2, 如果是一個恆等映射:,我們可以把公式2帶入公式1,得到:
遞歸地,,我們將會有:
對於any deeper unit 和any shollower unit 。公式4展現出了一些很好的特性。(i)的特徵能夠被表示爲l的特徵加一個殘差函數(這個殘差函數的形式爲 ,這表明任意的和之間的模型都是一個殘差。(ii) 對於任意的單元,特徵都可以寫成的形式,特徵是所有殘差函數的的輸出加的和。這和plain網絡相反,plain網絡的特徵是一系列的矩陣向量的乘積(忽略ReLU和BN)。
等式4同樣也具有良好的反向傳播特性。假設損失函數爲,根據反向傳播的鏈式法則可以得到(等式5):
等式5表明梯度能夠可以被分解成兩個部分:其中直接傳遞信息而不涉及任何權重層,而另一部分表示通過權重層傳播的信息。保證了信息能夠直接傳回任意淺單元。等式5同樣表明了在一個mini-batch中梯度不可能出現消失的情況,因爲通常對於一個mini-batch的全部樣本不可能都爲-1。這意味着,哪怕權重是任意小的,也不可能出現梯度消失的情況。
等式4和等式5表明了,在前向和反向傳播階段,信號都能夠直接的從一個單元傳遞到其他任意一個單元。等式4的基礎是是兩個恆等映射:(i) skip connection ,(ii) 是一個恆等映射。
我們從這個表達式5可以看出來:第l層的梯度裏,包含了第L層的梯度,通俗的說就是第L層的梯度直接傳遞給了第l層。因爲梯度消失問題主要是發生在淺層,這種將深層梯度直接傳遞給淺層的做法,有效緩解了深度神經網絡梯度消失的問題。
從降低數據中信息冗餘度的角度理解ResNet有效性
參考:ResNet的本質思考
從模型集成角度理解ResNet有效性
有一些研究表明,深層的殘差網絡可以看做是不同深度的淺層神經網絡的ensemble,訓練完一個深層網絡後,在測試的時候隨機去除某個網絡層,並不會使得網絡的性能有很大的退化,而對於VGG網絡來說,刪減任何一層都會造成模型的性能奔潰,如下圖。
表面的那麼深,因此優化自然也沒有那麼難了。更多細節具體可參見NIPS論文:
Residual Networks Behave Like Ensembles of Relatively Shallow Networks ;
5 ResNet的常見改進
6 ResNet v1代碼
代碼採用tensorflow 2.0 的tf.keras模塊實現的,並參考pytorch相關代碼實現。
"""resnet in tensorflow 2.0 tf.keras
[1] Kaiming He, Xiangyu Zhang, Shaoqing Ren, Jian Sun.
Deep Residual Learning for Image Recognition
https://arxiv.org/abs/1512.03385v1
"""
import os
import tensorflow as tf
import tensorflow.keras as keras
from tensorflow.keras import layers, Sequential
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
class BasicBlock(keras.Model):
expansion = 1
def __init__(self, in_dims, filter_nums, stride=1):
super(BasicBlock, self).__init__()
self.resnet_block = Sequential([layers.Conv2D(filter_nums, (3, 3), strides=stride, padding='same', use_bias=False),
layers.BatchNormalization(),
layers.Activation('relu'),
layers.Conv2D(filter_nums*BasicBlock.expansion, (3, 3), strides=1, padding='same', use_bias=False),
layers.BatchNormalization()])
self.relu = layers.Activation('relu')
if stride != 1 or in_dims != filter_nums*BasicBlock.expansion:
self.shortcut = Sequential([layers.Conv2D(filter_nums, (1, 1), strides=stride, padding='same', use_bias=False),
layers.BatchNormalization()])
else:
self.shortcut = lambda x: x
def call(self, inputs, training=None):
out = self.relu(layers.add([self.resnet_block(inputs), self.shortcut(inputs)]))
return out
class Bottleneck(keras.Model):
expansion = 4
def __init__(self, in_dims, filter_nums, stride):
super(Bottleneck, self).__init__()
self.resnet_block = Sequential([layers.Conv2D(filter_nums, (1, 1), strides=1, use_bias=False),
layers.BatchNormalization(),
layers.Activation('relu'),
layers.Conv2D(filter_nums, (3, 3), strides=stride, padding='same', use_bias=False),
layers.BatchNormalization(),
layers.Activation('relu'),
layers.Conv2D(filter_nums*Bottleneck.expansion, (1, 1), strides=1, use_bias=False),
layers.BatchNormalization()])
self.relu = layers.Activation('relu')
if stride != 1 or in_dims != filter_nums*Bottleneck.expansion:
self.shortcut = Sequential([layers.Conv2D(filter_nums*Bottleneck.expansion, (1, 1), strides=stride, use_bias=False),
layers.BatchNormalization()])
else:
self.shortcut = lambda x: x
def call(self, inputs, training=None):
out = self.relu(layers.add([self.resnet_block(inputs), self.shortcut(inputs)]))
return out
class ResNet(keras.Model):
def __init__(self, block, num_blocks, num_classes=100):
super(ResNet, self).__init__()
self.in_dims = 64
self.conv1 = Sequential([layers.Conv2D(64, (7, 7), strides=2, padding='same', use_bias=False),
layers.BatchNormalization(),
layers.Activation('relu'),
layers.MaxPool2D((3, 3), strides=2, padding='same')])
self.conv2_x = self._make_layer(block, 64, num_blocks[0], stride=1)
self.conv3_x = self._make_layer(block, 128, num_blocks[1], stride=2)
self.conv4_x = self._make_layer(block, 256, num_blocks[2], stride=2)
self.conv5_x = self._make_layer(block, 512, num_blocks[3], stride=2)
self.avg_pool = layers.GlobalAveragePooling2D()
self.fc = layers.Dense(num_classes)
def call(self, inputs, training=None):
out = self.conv1(inputs)
out = self.conv2_x(out)
out = self.conv3_x(out)
out = self.conv4_x(out)
out = self.conv5_x(out)
out = self.avg_pool(out)
out = self.fc(out)
return out
def _make_layer(self, block, filter_num, num_block, stride):
"""make resnet layer, the layer contains more than one residual block.
The conv stride of first block of conv2_x - conv5_x is 2, others are 1.
Args:
block: block type, BasicBLock or Bottleneck.
filter_num: output channels of the layer.
num_block: how many blocks per layer.
stride: the stride of the first block of the layer.
Returns:
return the resnet layer
"""
strides = [1] * (num_block - 1)
layers = []
layers.append(block(self.in_dims, filter_num, stride=stride))
self.in_dims = filter_num * block.expansion
for s in strides:
layers.append(block(self.in_dims, filter_num, s))
return Sequential([*layers])
def resnet18():
"""return the ResNet 18 object"""
return ResNet(BasicBlock, [2, 2, 2, 2])
def resnet34():
"""return the ResNet 34 object"""
return ResNet(BasicBlock, [3, 4, 6, 3])
def resnet50():
"""return the ResNet 50 object"""
return ResNet(Bottleneck, [3, 4, 6, 3])
def resnet101():
"""return the ResNet 101 object"""
return ResNet(Bottleneck, [3, 4, 23, 3])
def resnet152():
"""return the ResNet 152 object"""
return ResNet(Bottleneck, [3, 8, 36, 3])
if __name__ == '__main__':
model = resnet101()
model.build(input_shape=(None, 224, 224, 3))
print(model.predict(tf.ones([3, 224, 224, 3])).shape)
model.summary()
References
1、ResNet v2論文筆記
2、詳解深度學習之經典網絡架構(六):ResNet 兩代(ResNet v1和ResNet v2)
3、對ResNet的一些本質思考
4、ResNet及其變種的結構梳理、有效性分析與代碼解讀
5、https://github.com/weiaicunzai/pytorch-cifar100