ResNet诞生于2015年,当年ImageNet竞赛冠军,Top5错误率为3.57%
论文出处:Kaiming He, Xiangyu Zhang, Shaoqing Ren. Deep Residual Learning for Image Recognition. In CPVR, 2016.
从之前介绍的 4 种 CNN 经典网络结构我们也可以看出,网络层数的发展趋势是不断加深的。 这是由于深度网络本身集成了低层/中层/高层特征和分类器,以多层首尾相连的方式存在,所以可以通过增加堆叠的层数(深度)来丰富特征的层次,以取得更好的效果。
然而,单纯地增加网络的层数会导致**梯度消失(爆炸)**问题,它从根源上导致了函数无法收敛。通过标准初始化(normalized initialization)以及中间标准化层 (intermediate normalization layer),已经可以较好地解决这个问题了,这使得深度为数十层 的网络在反向传播过程中,可以通过随机梯度下降(SGD)的方式开始收敛。 但是,当深度更深的网络也可以开始收敛时,网络退化的问题就显露了出来:随着网络深度的增加,准确率先是达到瓶颈(这是很常见的),然后便开始迅速下降。需要注意的是, 这种退化并不是由过拟合引起的。对于一个深度比较合适的网络来说,继续增加层数反而会导致训练错误率的提升。
例如下面这个例子:
为了应对这个问题,ResNet 引入残差结构
,通过对残差结构的运用,ResNet 使得训练数百层的网络成为了可能,从而具有非常 强大的表征能力。
需要注意的是,ResNet块中的“+”是特征图对应元素值相加(矩阵值相加
),而Inception块中的“+”是沿深度方向叠加(千层蛋糕层数叠加)
也就是说,每个ResNet块的输入经过两条路径:
上图中的实线和虚线均表示恒等映射;
注意:1*1卷积操作可通过步长改变特征图尺寸,通过卷积核个数改特征图深度。
这里说明一下1 * 1的卷积运算是如何降低特征深度:下面以5 * 5的卷积运算为例说明这个问题。
假设网络上一层的输出为 128 * 100 * 100 (C * H * W )
为了实现这种残差结构,我们定义一个新的 ResnetBlock 类。
class ResnetBlock(Model):
def __init__(self,filters,strides=1,residual_path=False):
super(ResnetBlock, self).__init__()
self.filters=filters
self.strides=strides
self.residual_path=residual_path
self.c1=Conv2D(filters,(3,3),strides=strides,padding='same',use_bias=False)
self.b1=BatchNormalization()
self.a1=Activation('relu')
self.c2=Conv2D(filters,(3,3),strides=strides,padding='same',use_bias=False)
self.b2 = BatchNormalization()
# residual_path为True时,对输入进行下采样,即用1x1的卷积核做卷积操作,保证x能和F(x)维度相同,顺利相加
if residual_path:
self.down_c1 = Conv2D(filters, (1, 1), strides=strides, padding='same', use_bias=False)
self.down_b1 = BatchNormalization()
self.a2 = Activation('relu')
def call(self,x):
residual=x # residual等于输入值本身,即residual=x
# 将输入通过卷积、BN层、激活层,计算F(x)
x = self.c1(inputs)
x = self.b1(x)
x = self.a1(x)
x = self.c2(x)
y = self.b2(x)
if self.residual_path:
residual = self.down_c1(x)
residual = self.down_b1(residual)
out = self.a2(y + residual) # 最后输出的是两部分的和,即F(x)+x或F(x)+Wx,再过激活函数
return out
class MyResnet18(Model):
def __init__(self, block_list, initial_filters=64): # block_list表示每个block有几个卷积层
super(MyResnet18, self).__init__()
self.num_blocks = len(block_list) # 共有几个block
self.block_list = block_list
self.out_filters = initial_filters
# 先是一个3*3的卷积
self.c1 = Conv2D(self.out_filters, (3, 3), strides=1, padding='same', use_bias=False)
self.b1 = BatchNormalization()
self.a1 = Activation('relu')
self.blocks = tf.keras.models.Sequential()
# 构建ResNet网络结构
for block_id in range(len(block_list)): # 第几个resnet block
for layer_id in range(block_list[block_id]): # 第几个卷积层
if block_id != 0 and layer_id == 0: # 对除第一个block以外的每个block的输入进行下采样
block = ResnetBlock(self.out_filters, strides=2, residual_path=True)
else:
block = ResnetBlock(self.out_filters, residual_path=False)
self.blocks.add(block) # 将构建好的block加入resnet
self.out_filters *= 2 # 下一个block的卷积核数是上一个block的2倍
self.p1 = tf.keras.layers.GlobalAveragePooling2D()
self.f1 = tf.keras.layers.Dense(10, activation='softmax', kernel_regularizer=tf.keras.regularizers.l2())
def call(self, x):
x = self.c1(x)
x = self.b1(x)
x = self.a1(x)
x = self.blocks(x)
x = self.p1(x)
y = self.f1(x)
return y
model = MyResnet18([2, 2, 2, 2])
x = self.a1(x)
x = self.blocks(x)
x = self.p1(x)
y = self.f1(x)
return y
model = MyResnet18([2, 2, 2, 2])