ResNet50 网络结构搭建(PyTorch)

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=1, stride=1, bias=False)   # H,W不变。C: in_channel -> out_channel
        self.bn1 = nn.BatchNorm2d(num_features=out_channel)
        self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel, kernel_size=3, stride=stride, bias=False, padding=1)  # H/2,W/2。C不变
        self.bn2 = nn.BatchNorm2d(num_features=out_channel)
        self.conv3 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel*self.expansion, kernel_size=1, stride=1, bias=False)   # H,W不变。C: out_channel -> 4*out_channel
        self.bn3 = nn.BatchNorm2d(num_features=out_channel*self.expansion)
        self.relu = nn.ReLU(inplace=True)

        self.downsample = downsample

    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实现)

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        out = self.conv3(out)
        out = self.bn3(out)

        out += identity     # 残差连接
        out = self.relu(out)

        return out

2.2 ResNet50实现

ResNet中,conv1与其后的maxpool是不重复的,需要单独实现,而conv2,3,4,5_x中的则利用make_layer函数实现对基本残差块Bottleneck的堆叠。这里我们单独看一下make_layer是怎么实现的。

def _make_layer(self, block, channel, block_num, stride=1):
    """
        block: 堆叠的基本模块
        channel: 每个stage中堆叠模块的第一个卷积的卷积核个数,对resnet50分别是:64,128,256,512
        block_num: 当期stage堆叠block个数
        stride: 默认卷积步长
    """
        downsample = None   # 用于控制shorcut路的
        if stride != 1 or self.in_channel != channel*block.expansion:   # 对resnet50:conv2中特征图尺寸H,W不需要下采样/2,但是通道数x4,因此shortcut通道数也需要x4。对其余conv3,4,5,既要特征图尺寸H,W/2,又要shortcut维度x4
            downsample = nn.Sequential(
                nn.Conv2d(in_channels=self.in_channel, out_channels=channel*block.expansion, kernel_size=1, stride=stride, bias=False), # out_channels决定输出通道数x4,stride决定特征图尺寸H,W/2
                nn.BatchNorm2d(num_features=channel*block.expansion))

        layers = []  # 每一个convi_x的结构保存在一个layers列表中,i={2,3,4,5}
        layers.append(block(in_channel=self.in_channel, out_channel=channel, downsample=downsample, stride=stride)) # 定义convi_x中的第一个残差块,只有第一个需要设置downsample和stride
        self.in_channel = channel*block.expansion   # 在下一次调用_make_layer函数的时候,self.in_channel已经x4

        for _ in range(1, block_num):  # 通过循环堆叠其余残差块(堆叠了剩余的block_num-1个)
            layers.append(block(in_channel=self.in_channel, out_channel=channel))

        return nn.Sequential(*layers)   # '*'的作用是将list转换为非关键字参数传入

注意:这里我们首先判断是否需要对shortcut支路进行下采样,然后生成了对应的downsample网络结构。判断条件是:stride!=1 (当前阶段需要对特征图下采样)or in_channel != channel * block.expansion(由于进行了下采样,输出特征图通道数x4,不再与输入相等。eg. 对于conv3_x的第一个block,stride=2,因此需要下采样;而对于第二个block,stride=1,in_channel = channel * block.expansion,即512=128 * 4,因此不需要下采样)。然后,我们再将downsample传入block(即Bottleneck)中,作为shortcut支路下采样结构的具体实现。

参考表格中ResNet50的参数进行网络搭建,ResNet类的完整实现如下。

# todo ResNet
class ResNet(nn.Module):
    """
    __init__
        block: 堆叠的基本模块
        block_num: 基本模块堆叠个数,是一个list,对于resnet50=[3,4,6,3]
        num_classes: 全连接之后的分类特征维度
        
    _make_layer
        block: 堆叠的基本模块
        channel: 每个stage中堆叠模块的第一个卷积的卷积核个数,对resnet50分别是:64,128,256,512
        block_num: 当期stage堆叠block个数
        stride: 默认卷积步长
    """
    def __init__(self, block, block_num, num_classes=1000):
        super(ResNet, self).__init__()
        self.in_channel = 64    # conv1的输出维度

        self.conv1 = nn.Conv2d(in_channels=3, out_channels=self.in_channel, kernel_size=7, stride=2, padding=3, bias=False)     # H/2,W/2。C:3->64
        self.bn1 = nn.BatchNorm2d(self.in_channel)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)     # H/2,W/2。C不变
        self.layer1 = self._make_layer(block=block, channel=64, block_num=block_num[0], stride=1)   # H,W不变。downsample控制的shortcut,out_channel=64x4=256
        self.layer2 = self._make_layer(block=block, channel=128, block_num=block_num[1], stride=2)  # H/2, W/2。downsample控制的shortcut,out_channel=128x4=512
        self.layer3 = self._make_layer(block=block, channel=256, block_num=block_num[2], stride=2)  # H/2, W/2。downsample控制的shortcut,out_channel=256x4=1024
        self.layer4 = self._make_layer(block=block, channel=512, block_num=block_num[3], stride=2)  # H/2, W/2。downsample控制的shortcut,out_channel=512x4=2048

        self.avgpool = nn.AdaptiveAvgPool2d((1,1))  # 将每张特征图大小->(1,1),则经过池化后的输出维度=通道数
        self.fc = nn.Linear(in_features=512*block.expansion, out_features=num_classes)

        for m in self.modules():    # 权重初始化
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')

    def _make_layer(self, block, channel, block_num, stride=1):
        downsample = None   # 用于控制shorcut路的
        if stride != 1 or self.in_channel != channel*block.expansion:   # 对resnet50:conv2中特征图尺寸H,W不需要下采样/2,但是通道数x4,因此shortcut通道数也需要x4。对其余conv3,4,5,既要特征图尺寸H,W/2,又要shortcut维度x4
            downsample = nn.Sequential(
                nn.Conv2d(in_channels=self.in_channel, out_channels=channel*block.expansion, kernel_size=1, stride=stride, bias=False), # out_channels决定输出通道数x4,stride决定特征图尺寸H,W/2
                nn.BatchNorm2d(num_features=channel*block.expansion))

        layers = []  # 每一个convi_x的结构保存在一个layers列表中,i={2,3,4,5}
        layers.append(block(in_channel=self.in_channel, out_channel=channel, downsample=downsample, stride=stride)) # 定义convi_x中的第一个残差块,只有第一个需要设置downsample和stride
        self.in_channel = channel*block.expansion   # 在下一次调用_make_layer函数的时候,self.in_channel已经x4

        for _ in range(1, block_num):  # 通过循环堆叠其余残差块(堆叠了剩余的block_num-1个)
            layers.append(block(in_channel=self.in_channel, out_channel=channel))

        return nn.Sequential(*layers)   # '*'的作用是将list转换为非关键字参数传入

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)

        return x

2.3 伪输入测试

我们在建立Bottleneck与ResNet这两个类的基础之上,再在当前脚本的末尾增加如下代码,即可利用伪输入进行网络输出维度测试。注意:这里设定的网络分类个数为1000,可以根据分类任务自行调整。

def resnet50(num_classes=1000):
    return ResNet(block=Bottleneck, block_num=[3, 4, 6, 3], num_classes=num_classes)

if __name__ == '__main__':
    input = torch.randn(1, 3, 224, 224)  # B C H W
    print(input.shape)
    ResNet50 = resnet50(1000)
    output = ResNet50.forward(input)
    print(ResNet50)

3 网络结构总结

下面以表格的形式总结一下ResNet50的网络结构及其中间的维度变换。注意:绿色Downsample实际是与其余三个Conv2d并行的,而其余不含绿色的Block所对的支路其实不需要下采样,因此这里没有列出来。
在这里插入图片描述

  • 63
    点赞
  • 433
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值