经典CNN的实现 ResNet
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块的输入经过两条路径:
- 一条经过两次卷积运算
- 一条经过恒等映射
- 在输出时将二者对应元素值相加作为输出。
上图中的实线和虚线均表示恒等映射;
- 实线表示通道相同,计算方式为 H(x) = F(x) + x
- 虚线表示通道不同,计算方式为 H(x) = F(x) + Wx,其中 W 为卷积操作,目的是调整 x 的维 度(通道数)
注意:1*1卷积操作可通过步长改变特征图尺寸,通过卷积核个数改特征图深度。
这里说明一下1 * 1的卷积运算是如何降低特征深度:下面以5 * 5的卷积运算为例说明这个问题。
假设网络上一层的输出为 128 * 100 * 100 (C * H * W )
- (1)通过 32 * 5 * 5(32 个大小 为 5 * 5 的卷积核)的卷积层(步长为 1、全零填充)后,输出为 32 * 100 * 100 ,卷积层的 参数量为 32 * 5 * 5 * 128 = 102400;
- (2)如果先通过 32 * 1 * 1 的卷积层(输出为 32 * 100 * 100), 再通过 32 * 5 * 5 的卷积层,输出仍为 32 * 100 * 100,但卷积层的参数量变为 32 * 1 * 1 * 128 + 32 * 5 * 5 * 32 = 29696,仅为原参数量的 30 %左右,这就是小卷积核的降维作用。
实现残差结构
为了实现这种残差结构,我们定义一个新的 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()
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
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)
return out
构建出 ResNet 模型
class MyResnet18(Model):
def __init__(self, block_list, initial_filters=64):
super(MyResnet18, self).__init__()
self.num_blocks = len(block_list)
self.block_list = block_list
self.out_filters = initial_filters
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()
for block_id in range(len(block_list)):
for layer_id in range(block_list[block_id]):
if block_id != 0 and layer_id == 0:
block = ResnetBlock(self.out_filters, strides=2, residual_path=True)
else:
block = ResnetBlock(self.out_filters, residual_path=False)
self.blocks.add(block)
self.out_filters *= 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])
|