Resnet50的代码不是由笔者编写,笔者只对代码进行讲解,方便后续使用。原作者博客链接。 为了节省篇幅这里不贴出代码,请访问原作者GitHub查看代码。
在阅读本博客前请先了解残差网络的结构和原理,推荐博客。
1.ResNet50的网络结构
Resnet50包含两个基本的模块:Conv Block和Identity Block。这两个模块的结构图如下所示:
从图中可以看到,Identity Block的输出和输入必须是有相同的形状(不然残差边和input不能相加),这导致卷积提取到的特征长、宽和维度不能发生变化,而Conv Block在残差边加入了卷积操作,可以对输入矩阵的形状进行调整,使得残差边和卷积可以进行相加。
ResNet50的完整结构图如下图所示:
2. ResNet50
def ResNet50(input_shape=[224,224,3],classes=1000):
# [224,224,3]
img_input = Input(shape=input_shape)
x = ZeroPadding2D((3, 3))(img_input) # [230,230,3]
# [112,112,64]
x = Conv2D(64, (7, 7), strides=(2, 2), name='conv1')(x) #[112,112,64]
x = BatchNormalization(name='bn_conv1')(x)
x = Activation('relu')(x)
# [56,56,64]
x = MaxPooling2D((3, 3), strides=(2, 2))(x)
# [56,56,256]
x = conv_block(x, 3, [64, 64, 256], stage=2, block='a', strides=(1, 1))
x = identity_block(x, 3, [64, 64, 256], stage=2, block='b')
x = identity_block(x, 3, [64, 64, 256], stage=2, block='c')
# [28,28,512]
x = conv_block(x, 3, [128, 128, 512], stage=3, block='a')
x = identity_block(x, 3, [128, 128, 512], stage=3, block='b')
x = identity_block(x, 3, [128, 128, 512], stage=3, block='c')
x = identity_block(x, 3, [128, 128, 512], stage=3, block='d')
# [14,14,1024]
x = conv_block(x, 3, [256, 256, 1024], stage=4, block='a')
x = identity_block(x, 3, [256, 256, 1024], stage=4, block='b')
x = identity_block(x, 3, [256, 256, 1024], stage=4, block='c')
x = identity_block(x, 3, [256, 256, 1024], stage=4, block='d')
x = identity_block(x, 3, [256, 256, 1024], stage=4, block='e')
x = identity_block(x, 3, [256, 256, 1024], stage=4, block='f')
# [7,7,2048]
x = conv_block(x, 3, [512, 512, 2048], stage=5, block='a')
x = identity_block(x, 3, [512, 512, 2048], stage=5, block='b')
x = identity_block(x, 3, [512, 512, 2048], stage=5, block='c')
# 代替全连接层
x = AveragePooling2D((7, 7), name='avg_pool')(x)
# 进行预测
x = Flatten()(x)
x = Dense(classes, activation='softmax', name='fc1000')(x)
model = Model(img_input, x, name='resnet50')
return model
x = ZeroPadding2D((3, 3))(img_input)
:在img_input
的外围进行padding=0的填充,填充数量为3个像素点,最后得到的维度为224+3*2 = 230,所以x的维度为230,230,3。
输入图像的尺寸为[224, 224, 3],经过简单的卷积和池化操作后形状变为[56, 56, 64]。接下来,就是ResNet50网络的重点,Conv Block和Identity Block。下面我们分别以第一次使用Conv Block和Identity Block为例,讲解这两个模块内部是如何操作的。
关于卷积与池化操作时的维度变化可以参考我之前的博客,在这里不做过多赘述,维度变化信息我写在了代码里。
3.Conv Block
Conv Block模块代码如下:
def conv_block(input_tensor, kernel_size, filters, stage, block, strides=(2, 2)):
# 64,64,256
filters1, filters2, filters3 = filters
conv_name_base = 'res' + str(stage) + block + '_branch'
bn_name_base = 'bn' + str(stage) + block + '_branch'
# 降维
x = Conv2D(filters1, (1, 1), strides=strides,
name=conv_name_base + '2a')(input_tensor)
x = BatchNormalization(name=bn_name_base + '2a')(x)
x = Activation('relu')(x)
# 3x3卷积
x = Conv2D(filters2, kernel_size, padding='same',
name=conv_name_base + '2b')(x)
x = BatchNormalization(name=bn_name_base + '2b')(x)
x = Activation('relu')(x)
# 升维
x = Conv2D(filters3, (1, 1), name=conv_name_base + '2c')(x)
x = BatchNormalization(name=bn_name_base + '2c')(x)
# 残差边
shortcut = Conv2D(filters3, (1, 1), strides=strides,
name=conv_name_base + '1')(input_tensor)
shortcut = BatchNormalization(name=bn_name_base + '1')(shortcut)
x = layers.add([x, shortcut])
x = Activation('relu')(x)
return x
第一次调用Conv Block输入的维度为x.shape = [56, 56, 64],传入的参数为:
x = conv_block(x, 3, [64, 64, 256], stage=2, block=‘a’, strides=(1, 1))
则Conv Block内:
kernel_szie = 3
filter1 = 64
filter2 = 64
filter3 = 256
strides = (1,1)
stage和block是用来命名确定该层位置的参数。
为了便于查看,以下函数都直接将参数填入
- 命名
conv_name_base = 'res' + '2' + 'a' + '_branch'
bn_name_base = 'bn' + '2' + 'a' + '_branch'
conv_name_base = ‘res2a_branch’
bn_name_base = ‘bn2a_branch’
- 该模块内的第一次卷积操作
# 降维
x = Conv2D(64, (1, 1), strides=(1,1),
name=conv_name_base + '2a')(input_tensor)
x = BatchNormalization(name=bn_name_base + '2a')(x)
x = Activation('relu')(x)
conv: [56, 56, 64] ==> [56, 56, 64],name = ‘res2a_branch2a’
BatchNor:形状不发生变化,name = ‘bn2a_branch2a’
- 该模块内的第二次卷积操作
# 3x3卷积
x = Conv2D(64, 3, padding='same',
name=conv_name_base + '2b')(x)
x = BatchNormalization(name=bn_name_base + '2b')(x)
x = Activation('relu')(x)
conv: [56, 56, 64] ==> [56, 56, 64], name = ‘res2a_branch2b’
BatchNor:形状不发生变化,name = ‘bn2a_branch2b’
- 该模块内的第三次卷积操作
# 升维
x = Conv2D(256, (1, 1), name=conv_name_base + '2c')(x)
x = BatchNormalization(name=bn_name_base + '2c')(x)
conv: [56, 56, 64] ==> [56, 56, 256], name = ‘res2a_branch2c’
BatchNor:形状不发生变化,name = ‘bn2a_branch2c’
输出矩阵的大小最终又回到了[56, 56, 256]
- 残差边
# 残差边
shortcut = Conv2D(256, (1, 1), strides=(1,1),
name=conv_name_base + '1')(input_tensor)
shortcut = BatchNormalization(name=bn_name_base + '1')(shortcut)
conv: [56, 56, 64] ==> [56, 56, 256], name = ‘res2a_branch1’
BatchNor: 形状不变, name = ‘bn2a_branch1’
最终残差边的输出[56, 56, 256]和卷积层的输出[56, 56, 256]相加,经激活函数后输出。这一个Conv Block的输入和输出形状没有发生变化。
x = layers.add([x, shortcut])
x = Activation('relu')(x)
return x
4. Idnetity Block
def identity_block(input_tensor, kernel_size, filters, stage, block):
filters1, filters2, filters3 = filters
conv_name_base = 'res' + str(stage) + block + '_branch'
bn_name_base = 'bn' + str(stage) + block + '_branch'
# 降维
x = Conv2D(filters1, (1, 1), name=conv_name_base + '2a')(input_tensor)
x = BatchNormalization(name=bn_name_base + '2a')(x)
x = Activation('relu')(x)
# 3x3卷积
x = Conv2D(filters2, kernel_size,padding='same', name=conv_name_base + '2b')(x)
x = BatchNormalization(name=bn_name_base + '2b')(x)
x = Activation('relu')(x)
# 升维
x = Conv2D(filters3, (1, 1), name=conv_name_base + '2c')(x)
x = BatchNormalization(name=bn_name_base + '2c')(x)
x = layers.add([x, input_tensor])
x = Activation('relu')(x)
return x
第一次输入的维度为x.shape = [56, 56, 256],传入的参数为:
x = identity_block(x, 3, [64, 64, 256], stage=2, block=‘b’)
则Conv Block内:
kernel_szie = 3
filter1 = 64
filter2 = 64
filter3 = 256
stage和block是用来命名确定该层位置的参数。
为了便于查看,以下函数都直接将参数填入
- 命名
conv_name_base = 'res' + '2'+ 'b'+ '_branch'
bn_name_base = 'bn' + '2' + 'b' + '_branch'
conv_name_base = ‘res2b_branch’
bn_name_base = ‘bn2b_branch’
- 该模块内的第一次卷积
# 降维
x = Conv2D(64, (1, 1), name=conv_name_base + '2a')(input_tensor)
x = BatchNormalization(name=bn_name_base + '2a')(x)
x = Activation('relu')(x)
conv: [56, 56, 256] ==> [56, 56, 64], name = ‘res2a_branch2a’
BatchNor: 形状不变, name = ‘bn2a_branch2a’
- 该模块内的第二次卷积
# 3x3卷积
x = Conv2D(64, kernel_size,padding='same', name=conv_name_base + '2b')(x)
x = BatchNormalization(name=bn_name_base + '2b')(x)
x = Activation('relu')(x)
conv: [56, 56, 64] ==> [56, 56, 64], name = ‘res2a_branch2b’
BatchNor: 形状不变, name = ‘bn2a_branch2b’
- 该模块内的第三次卷积
# 升维
x = Conv2D(256, (1, 1), name=conv_name_base + '2c')(x)
x = BatchNormalization(name=bn_name_base + '2c')(x)
conv: [56, 56, 64] ==> [56, 56, 256], name = ‘res2a_branch2c’
BatchNor: 形状不变, name = ‘bn2a_branch2c’
- 残差边
Identity Block模块残差边不进行任何操作,将第三次卷积后的结果与输入相加,然后经过激活函数即得到输出结果。
x = layers.add([x, input_tensor])
x = Activation('relu')(x)
return x
ResNet50模型就是将很多个Conv Block和Identity Block进行堆叠,最后得到的输出矩阵大小为[7, 7, 2048],对该矩阵使用kernel_size为7的pooling即可将矩阵变为1维,接着节Dense层进行分类预测。