ResNet50是一个经典的特征提取网络结构,虽然Pytorch已有官方实现,但为了加深对网络结构的理解,还是自己动手敲敲代码搭建一下。需要特别说明的是,笔者是以熟悉网络各层输出维度变化为目的的,只对建立后的网络赋予伪输入并测试各层输出,并没有用图像数据集训练过该网络(后续会用图像数据集测试并更新博客)。
1 预备理论
在动手搭建ResNet50以前,首先需要明确ResNet系列网络的基本结构,其次复习与卷积相关的几个知识点,以便更好地理解网络中间输出维度的变化。
1.1 ResNet系列
1.1.1 几种网络基本配置
ResNet原文中的表格列出了几种基本的网络结构配置:
从上表可以看出,对于不同深度的ResNet有以下几个特点,请特别关注(3)(4):
(1)起始阶段都经历了相同的conv1和maxpool的过程。
(2)不同深度的ResNet都是由基本残差块堆叠而成。 18,34-layer的基本模块记为Basicblock,包含2次卷积;50,101,152layer的基本模块记为Bottleneck,包含3次卷积(1.1.2节会详细说明)。
n-layer确定的情况下,称i阶段为convi_x过程,i∈{2,3,4,5}:
(3)2阶段堆叠的残差块完全相同。 因为输入到输出是56→56,无下采样过程。
(4)3至5阶段堆叠的第一个残差块和其余残差块是不同的。 解释:每个阶段均对特征图像大小进行下采样。以50layer–conv3_x为例,仔细思考残差块的堆叠模式可以发现,下采样过程发生在4个堆叠残差块中的第一个,因为这里实现了特征图尺寸从56→28的过程;而对于其余3个残差块,特征图的维度全部是28→28,因此这3个的结构是完全相同的(如果这里我没有表述清楚,可以参看下面的图和末尾的表)。其余阶段同理。
1.1.2 基本残差块的两种模式
以下将以问答的形式来理解基本残差块的两种模式,由于本文关注ResNet50的实现,因此以下以Bottleneck为例说明,对于Basicblock可以类比。
-
为什么需要下采样?
下采样是才特征提取网络中经常使用的操作,潜在的作用是增强特征的变换不变性,减少特征参数防止过拟合,具体表现为特征图像尺寸逐渐缩小,通道数逐渐增加。
-
为什么Bottleneck有两种模式?
请回顾1.1.1节中的表格,对于绿色交界处,特征图维度不变,而对于红色交界,特征图维度变化,说明这里需要进行一次下采样。以50layer–conv3_x为例,这一阶段共堆叠了4个残差块。红色交界有一次下采样,并且在第一个残差块实现,我们称其为Bottleneck_down;对于后面3个残差块,特征图的尺寸均不发生变化,不进行下采样,记为Bottleneck_norm。具体可以参考下面的红绿线辅助理解。这一规律对于conv3_x, conv4_x, conv5_x都是成立的。
特别的是,对于conv2_x,其堆叠的残差块都是相同的。
-
下采样的卷积实现思路?
在PyTorch中使用nn.Conv2d实现卷积,通常会使用的参数如下:
torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, bias=True)
因此实现下采样会用到如下操作(虽然还不够具体,是个思路雏形):
-
特征图尺寸减半:卷积步长stride=2。
-
特征图通道加倍:卷积核数目out_channels=4*in_channels(因为H/2,W/2,特征图缩小为1/4,所以通道数x4)。
-
-
下采样的具体实现?
参照上面的思路,Bottleneck的两种模式如下:实现的关键点就是我们需要判断出当前位置需要哪种模式,并设置正确的卷积步长。具体实现请参看2.1节。
1.2 2维卷积后的特征图维度变化
下面我们来回顾一下卷积的过程前后输入特征图维度的变化。
下图是一个k x k x C_in大小的卷积核在 H_in x W_in x C_in 大小的特征图上进行二维卷积的过程。对于单个卷积核而言,它将在 H_in x W_in 的平面上进行滑动,并按照一下公式输出一张维度为 H_out x W_out x 1 大小的特征图;而输出特征图的数量取决于卷积核的数量filter_num。
2 代码实现
以下我们分别将Bottleneck和ResNet50作为类来实现,而Bottleneck是ResNet50中堆叠的基本残差块。
2.1 Bottleneck实现
为了实现Bottleneck的两种模式配置,我们需要利用downsample控制shortcut支路特征图尺寸和通道数变换(这里先知道downsample的功能即可,具体实现见ResNet类)。
这里解释一下,downsample是shortcut支路的网络结构。如果当前残差块的输入和输出的特征维度大小相同,那么shortcut的输出直接继承原始输入x就好的(代码中暂存为了identity变量);而如果当前残差块的输入与输出特征图的尺寸大小和通道数不一致,即需要在shortcut支路也完成下采样的操作。
def forward(self, x):
identity = x # 将原始输入暂存为shortcut的输出
if self.downsample is not None:
identity = self.downsample(x) # 如果需要下采样,那么shortcut后:H/2,W/2。C: out_channel -> 4*out_channel(见ResNet中的downsample实现)
以下是Bottleneck类的完整实现,可以对照ResNet50的表格查看。
# todo Bottleneck
class Bottleneck(nn.Module):
"""
__init__
in_channel:残差块输入通道数
out_channel:残差块输出通道数
stride:卷积步长
downsample:在_make_layer函数中赋值,用于控制shortcut图片下采样 H/2 W/2
"""
expansion = 4 # 残差块第3个卷积层的通道膨胀倍率
def __init__(self, in_channel, out_channel, stride=1, downsample=None):
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel, kernel_size