【论文解读】CNN深度卷积神经网络-ResNet

1.简介
深度残差网络(deep residual network)是2015年微软何凯明团队发表的一篇名为:《Deep Residual Learning for Image Recognition》的论文中提出的一种全新的网络结构,其核心模块是残差块residual block。正是由于残差块结构的出现使得深度神经网络模型的层数可以不断加深到100层、1000层甚至更深,从而使得该团队在当年的ILSVRC 2015分类竞赛中取得卓越成绩,也深刻地影响了以后的很多深度神经网络的结构设计。
残差网络的成功不仅表现在其在ILSVRC 2015竞赛中的卓越效果,更是因为残差块skip connection/shorcut这样优秀的思想和设计,使得卷积网络随着层数加深而导致的模型退化问题能被够大幅解决,使模型深度提高一个数量级,到达上百、上千层。
1.1网络加深的后果
在残差块这样的结构引入之前,如果一个神经网络模型的深度太深,可能会带来梯度消失和梯度爆炸的问题(如下图),随着一些正则化方法的应用可以缓解此问题,但是随着layer深度的继续加深,又带来了模型退化这样的问题。而添加了带shortcut的残差块结构之后,使得整个深度神经网络的层数可以大幅增加,变得更【深】,从而有时会带来更好的训练效果。
1.2 网络模型的退化
下图为CIFER10在20层和56层普通网络结构下测试和训练过程中的损失。可见,仅仅简单地加深网络,并没有带来精度的提升,相反会导致网络模型的退化。
那么,这里就有一个问题,为什么加深网络会带来退化问题?即使新增的这些layer什么都没学习,保持恒等输出(所有的weight都设为1),那么按理说网络的精度也应该 = 原有未加深时的水平,如果新增的layer学习到了有用特征,那么必然加深过后的模型精度会 > 未加深的原网络。看起来对于网络的精度加深后都应该 >= 加深前才对啊 ?
实际上,让新增的layer保持什么都不学习的恒等状态,恰恰很困难,因为在训练过程中每一层layer都通过线性修正单元relu处理,而这样的处理方式必然带来特征的信息损失(不论是有效特征or冗余特征)。所以上述假设的前提是不成立的,简单的堆叠layer必然会带来退化问题。
1.3 退化问题的解决
到此,何凯明团队创新地提出了残差块的构想,通过shortcut/skip connection这样的方式(最初出现在highway network中),绕过这些新增的layer,既然保持新增layer的identity恒等性很困难,那就直接绕过它们,通过shortcut通路来保持恒等。如图2所示:

3.2 残差块

在本文中,我们通过引入深度残差学习框架解决降级问题,而不是希望每个堆叠的层都直接适合所需的基础映射,而是明确让这些层适合残差映射。 形式上,将所需的基础映射表示为H(x),我们让堆叠的非线性层(图中的weight layer)适合F(x) := H(x) - x的另一个映射。 原始映射将重铸为F(x) + x。 我们假设与优化原始,未引用的映射相比,更容易优化残留映射。 极端地,如果identity特征映射是最佳的,则将残差推为零比通过用一叠非线性层拟合特征映射要容易。

普通网络
上图中间的是34层的基准普通网络,包含34个带权层(conv+fc)。其设计参考了VGG-19,卷积层的filter尺寸多为3×3,并遵循两个设计原则:1.如果输出的特征图尺寸减半,则filter数量加倍 2.如果输出的特征图尺寸不变,filter数量也不变。stride保持为2,网络结束之前会经过全局平均池化层,再连接一个1000路的softmax分类。
论文指出残差网络比VGG网络有着更少的过滤器和更低的复杂度,34层的基准网络有36亿个FLOP(乘加),而VGG-19有196亿个FLOP,仅占其18%。
残差网络
上图最右边是在中间普通网络基础上,增加了shortcut,使其变为了残差网络。这里有两种shortcut,实线shortcut和虚线shortcut(投影projection)。当输入和输出维度相同时,使用实线shortcut,当维度增加时使用虚线shortcut。当维度改变使用虚线shortcut时,我们为了匹配维度有2种做法:
- (A) shortcut任执行恒等映射,增加额外的0项来增加维度
- (B)使用等式
中的方式,通过1×1卷积来改变维度
3.4 实现
对ImageNet的残差网络实现参照了VGGNet及其在ImageNet中的实现。
图像resize,并水平翻转,随机剪裁减去RGB均值像素,颜色增强等处理;
BN批量正则化;
batch size设为256;
随机梯度下降SGD;
初始学习率为0.1当loss稳定后除以10;
weight decay设为0.0001;
动量0.9;
值得注意的是,在该实现中并没有使用Dropout方法。
4.实验
4.1 ImageNet分类
作者在ImageNet2012分类数据集评估了残差网络,128万张训练图,5万张验证图,1000个分类,在测试服务器上通过10万张测试图评估得到了top-1和top-5错误率。


普通网络
上图左边可看出,在普通网络的对比中,34层的比18层误差大,存在模型退化现象。
残差网络
通过对比左右两幅图+table2可以发现:当模型深度不是很深时(18)层,普通网络和ResNet网络的准确率差不多,表示通过一些初始化和正则化方法,可以降低普通卷积神经网络的过拟合;但当层数不断加深,就不可避免地出现模型退化现象,而此时ResNet可以在此情况下很好地解决退化问题。
作者团队不仅实验了34层的ResNet更实验了多种残差块,多种深度的残差网络,结构如下:

其中,以50层layer中的残差块为例,由于其输入输出尺寸不同,需要1×1卷积进行维度转换,这种残差block表现出上窄下宽的形状,被称为“瓶颈”残差块。具体如下图:

不同深度ResNet的对比

可以看见,在ResNet网络中,并没有看到模型退化的现象,反而随着layer深度加深,网络的精度越高。
各种经典网络对比

5.代码实现
这里使用tensorflow2.0实现ResNet-18网络结构,完整训练可参考:残差网络(ResNet)。我们先看一下ResNet-18的网络结构:

ResNet的前两层跟之前介绍的GoogLeNet中的一样:在输出通道数为64、步幅为2的7×7卷积层后接步幅为2的3×3的最大池化层。不同之处在于ResNet每个卷积层后增加的批量归一化层。然后接了4种类型的共计8个残差块,每种类型的2个残差块形成一个堆叠。最后加了全局平均池化和1000路的softmax输出。
5.1定义残差块
残差块沿用了VGG的3×3的卷积核尺寸,且有两种结构:初入 = 输出;输入!=输出。针对输入和输出shape不一样的情况,需要增加一个1×1卷积变换维度。残差块内的卷积层经过卷积后接BN批量归一化层,然后经过ReLU激活。
定义残差块实现类Residual:
import tensorflow as tf
from tensorflow.keras import layers,activations
class Residual(tf.keras.Model):
def __init__(self, num_channels, use_1x1conv=False, strides=1, **kwargs):
super(Residual, self).__init__(**kwargs)
self.conv1 = layers.Conv2D(num_channels,
padding='same',
kernel_size=3,
strides=strides)
self.conv2 = layers.Conv2D(num_channels, kernel_size=3,padding='same')
# 如果需要变换维度,则增加1×1卷积层
if use_1x1conv:
self.conv3 = layers.Conv2D(num_channels,
kernel_size=1,
strides=strides)
else:
self.conv3 = None
self.bn1 = layers.BatchNormalization()
self.bn2 = layers.BatchNormalization()
def call(self, X):
Y = activations.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
return activations.relu(Y + X)
5.2定义残差块stack
ResNet-18总计4个残差块stack,每个stack包含两个残差块。下面定义残差块stack
class ResnetBlock(tf.keras.layers.Layer):
def __init__(self,num_channels, num_residuals, first_block=False,**kwargs):
super(ResnetBlock, self).__init__(**kwargs)
self.listLayers=[]
for i in range(num_residuals):
if i == 0 and not first_block:
self.listLayers.append(Residual(num_channels, use_1x1conv=True, strides=2))
else:
self.listLayers.append(Residual(num_channels))
def call(self, X):
for layer in self.listLayers.layers:
X = layer(X)
return X
5.3定义ResNet
class ResNet(tf.keras.Model):
def __init__(self,num_blocks,**kwargs):
super(ResNet, self).__init__(**kwargs)
self.conv=layers.Conv2D(64, kernel_size=7, strides=2, padding='same')
self.bn=layers.BatchNormalization()
self.relu=layers.Activation('relu')
self.mp=layers.MaxPool2D(pool_size=3, strides=2, padding='same')
self.resnet_block1=ResnetBlock(64,num_blocks[0], first_block=True)
self.resnet_block2=ResnetBlock(128,num_blocks[1])
self.resnet_block3=ResnetBlock(256,num_blocks[2])
self.resnet_block4=ResnetBlock(512,num_blocks[3])
self.gap=layers.GlobalAvgPool2D()
self.fc=layers.Dense(units=1000,activation=tf.keras.activations.softmax)
def call(self, x):
x=self.conv(x)
x=self.bn(x)
x=self.relu(x)
x=self.mp(x)
x=self.resnet_block1(x)
x=self.resnet_block2(x)
x=self.resnet_block3(x)
x=self.resnet_block4(x)
x=self.gap(x)
x=self.fc(x)
return x
mynet=ResNet([2,2,2,2])
# 观察ResNet网络的输出
X = tf.random.uniform(shape=(1, 224, 224 , 3))
for layer in mynet.layers:
X = layer(X)
print(layer.name, 'output shape:\t', X.shape)
输出:
conv2d output shape: (1, 112, 112, 64)
batch_normalization output shape: (1, 112, 112, 64)
activation output shape: (1, 112, 112, 64)
max_pooling2d output shape: (1, 56, 56, 64)
resnet_block output shape: (1, 56, 56, 64)
resnet_block_1 output shape: (1, 28, 28, 128)
resnet_block_2 output shape: (1, 14, 14, 256)
resnet_block_3 output shape: (1, 7, 7, 512)
global_average_pooling2d output shape: (1, 512)
dense output shape: (1, 1000)
6.总结
残差网络的出现使人们摆脱了【深度】的束缚,大幅改善了深度神经网络中的模型退化问题,使网络层数从数十层跃升至几百上千层,大幅提高了模型精度,通用性强适合各种类型的数据集和任务。残差块和shortcut这种优秀的设计也极大影响了后面的网络结构发展。