一、前言

ResNet(Residual Network,残差网络)由微软研究院在 2015 年提出,是深度学习中非常经典的一种网络架构。在深层神经网络中,随着网络层数的加深,模型的训练效果可能会变差,这被称为“退化问题”。ResNet 通过引入“残差连接(skip connection)”巧妙地解决了这一问题,使得网络更容易训练,并能构建更深的网络结构。

本文将通过 PyTorch 从零实现一个 ResNet 模型,主要包括两种残差块(BasicBlock 和 Bottleneck),并模拟实现了类似官方 resnet50 的结构,以帮助更好地理解其内部机制。

二、卷积后特征图尺寸计算公式

在设计 CNN 时,合理计算卷积输出尺寸非常重要,公式如下:
在这里插入图片描述

参数解释

Kernel Size(卷积核)
用于提取局部区域特征。常见如 3x3 或 1x1 卷积核。

Padding(填充)
作用:保持特征图尺寸不变,避免边缘信息损失。
如:padding=1 可在 3x3 卷积下保持尺寸不变。

Stride(步长)
控制卷积核滑动的步伐。stride=2 可实现特征图尺寸减半。

常见设置示例:
stride=1 且 padding=1,则尺寸不变;
stride=2 且 padding=1,尺寸减半。

三、残差块

下图对比了 ResNet 中两种残差结构:
图左为基础残差块(BasicBlock)用于浅层网络,如 ResNet18。由两个连续的 3x3 卷积层组成;图右为瓶颈残差块(Bottleneck)用于深层网络,如 ResNet50。采用 1x1->3x3-> 1x1的结构.
在这里插入图片描述

四、ResNet18 vs ResNet50 网络结构概览

下图是resnet18和 resnet50整体的网络结构
在这里插入图片描述

五、PyTorch 手写实现 ResNet

5.1 基础卷积块(适用于 ResNet18)

该结构是 ResNet 最原始的残差模块,包含两个 3x3 卷积层,残差连接通过 out += identity 实现。

class BasicBlock(nn.Module):
    expansion = 1

    def __init__(self, in_channel, block_channel, stride=1, downsample=None):
        super(BasicBlock, self).__init__()
        self.downsample = downsample
        self.conv1 = nn.Conv2d(in_channel, block_channel, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(block_channel)
        self.relu1 = nn.ReLU()
        self.conv2 = nn.Conv2d(block_channel, block_channel * self.expansion, kernel_size=3, stride=1, padding=1,
                               bias=False)
        self.bn2 = nn.BatchNorm2d(block_channel)
        self.relu2 = nn.ReLU()

    def forward(self, x):
        identity = x
        if self.downsample is not None:
            identity = self.downsample(x)

        out = self.relu1(self.bn1(self.conv1(x)))  # 卷积、批归一化、relu
        out = self.bn2(self.conv2(out))  # 经过两层后的结果和原始输入进行相加,实现残差块的残差连接,
        out += identity
        out = self.relu2(out)  # 可以注意到第二层卷积bn后先加了输入,再去relu
        return out

5.2 Bottleneck(适用于 ResNet50)

class bottleneck(nn.Module):
    expansion = 4  # 扩展通道数至:通道数*expansion

    def __init__(self, in_channel, block_channel, stride=1, downsample=None):
        super(bottleneck, self).__init__()
        self.downsample = downsample
        self.conv1 = nn.Conv2d(in_channel, block_channel, kernel_size=1, stride=stride, bias=False)  # 这里padding = 0
        self.bn1 = nn.BatchNorm2d(block_channel)
        self.relu1 = nn.ReLU()
        self.conv2 = nn.Conv2d(block_channel, block_channel, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(block_channel)
        self.relu2 = nn.ReLU()
        self.conv3 = nn.Conv2d(block_channel, block_channel * self.expansion, kernel_size=1, stride=1, bias=False)
        self.bn3 = nn.BatchNorm2d(block_channel * self.expansion)
        self.relu3 = nn.ReLU()

    def forward(self, x):
        identity = x
        if self.downsample is not None:
            identity = self.downsample(x)

        out = self.relu1(self.bn1(self.conv1(x)))
        out = self.relu2(self.bn2(self.conv2(out)))
        out = self.bn3(self.conv3(out))
        out += identity
        out = self.relu3(out)
        return out
```python

5.3 构建完整 ResNet 网络

我们通过 _make_layer 方法构建多个残差层

class Resnet(nn.Module):
    def __init__(self, in_channel=3, num_classes=100, block=bottleneck, num_blocks=[3, 4, 6, 3]):
        super(Resnet, self).__init__()
        self.in_channel = 64
        self.conv1 = nn.Conv2d(in_channel, 64, kernel_size=7, stride=2, padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.maxpool1 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        self.layer1 = self._make_layer(block, 64,  num_blocks[0], stride=1)
        self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2)
        self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2)
        self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2)

        self.fc = nn.Sequential(
            nn.Linear(512 * block.expansion * 7 * 7, num_classes),
            nn.Softmax(dim=-1)
        )

    def forward(self, x):
        out = self.maxpool1(self.bn1(self.conv1(x)))  # 初始下采样
        out = self.layer1(out)
        out = self.layer2(out)
        out = self.layer3(out)
        out = self.layer4(out)
        out = out.reshape(out.shape[0], -1)  # flatten
        out = self.fc(out)
        return out

    def _make_layer(self, block, block_channel, block_num, stride):
        layers = []
        downsample = nn.Conv2d(self.in_channel, block_channel * block.expansion, kernel_size=1, stride=stride, bias=False)

        layers.append(block(self.in_channel, block_channel, stride, downsample))
        self.in_channel = block_channel * block.expansion

        for _ in range(1, block_num):
            layers.append(block(self.in_channel, block_channel, stride=1))
        return nn.Sequential(*layers)

最终的完整代码如下:

import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision


class BasicBlock(nn.Module):
    expansion = 1

    def __init__(self, in_channel, block_channel, stride=1, downsample=None):
        super(BasicBlock, self).__init__()
        self.downsample = downsample
        self.conv1 = nn.Conv2d(in_channel, block_channel, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(block_channel)
        self.relu1 = nn.ReLU()
        self.conv2 = nn.Conv2d(block_channel, block_channel * self.expansion, kernel_size=3, stride=1, padding=1,
                               bias=False)
        self.bn2 = nn.BatchNorm2d(block_channel)
        self.relu2 = nn.ReLU()

    def forward(self, x):
        identity = x
        if self.downsample is not None:
            identity = self.downsample(x)

        out = self.relu1(self.bn1(self.conv1(x)))  # 卷积、批归一化、relu
        out = self.bn2(self.conv2(out))  # 经过两层后的结果和原始输入进行相加,实现残差块的残差连接,
        out += identity
        out = self.relu2(out)  # 可以注意到第二层卷积bn后先加了输入,再去relu
        return out


# resnet50只用了这种残差块
'''
残差块只有第一个卷积层的步长用形参stride来传,其余地方写固定步长1。
实际上只有在每个layer的第一个卷积层里需要降尺寸。
'''
class bottleneck(nn.Module):
    expansion = 4  # 扩展通道数至:通道数*expansion

    def __init__(self, in_channel, block_channel, stride=1, downsample=None):
        super(bottleneck, self).__init__()
        self.downsample = downsample
        self.conv1 = nn.Conv2d(in_channel, block_channel, kernel_size=1, stride=stride, bias=False)  # 这里padding = 0
        self.bn1 = nn.BatchNorm2d(block_channel)
        self.relu1 = nn.ReLU()
        self.conv2 = nn.Conv2d(block_channel, block_channel, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(block_channel)
        self.relu2 = nn.ReLU()
        self.conv3 = nn.Conv2d(block_channel, block_channel * self.expansion, kernel_size=1, stride=1, bias=False)
        self.bn3 = nn.BatchNorm2d(block_channel * self.expansion)
        self.relu3 = nn.ReLU()

    def forward(self, x):
        identity = x
        if self.downsample is not None:
            identity = self.downsample(x)

        out = self.relu1(self.bn1(self.conv1(x)))
        out = self.relu2(self.bn2(self.conv2(out)))
        out = self.bn3(self.conv3(out))
        out += identity
        out = self.relu3(out)
        return out


class Resnet(nn.Module):
    def __init__(self, in_channel=3, num_classes=100, block=bottleneck, num_blocks=[3, 4, 6, 3]):
        super(Resnet, self).__init__()
        self.in_channel = in_channel
        self.conv1 = nn.Conv2d(in_channel, 64, kernel_size=7, stride=2, padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.maxpool1 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.in_channel = 64
        self.layer1 = self._make_layer(block, 64, num_blocks[0],stride=1)
        self.layer2 = self._make_layer(block, 128, num_blocks[1],stride=2)
        self.layer3 = self._make_layer(block, 256, num_blocks[2],stride=2)
        self.layer4 = self._make_layer(block, 512, num_blocks[3],stride=2)
        self.fc = nn.Sequential(
            nn.Linear(512 * block.expansion*7*7, num_classes),
            nn.Softmax(dim=-1)
        )

    def forward(self, x):
        out = self.maxpool1(self.bn1(self.conv1(x)))  # (1, 3, 224, 224) -> (1, 64, 56, 56)
        out = self.layer1(out)
        out = self.layer2(out)
        out = self.layer3(out)
        out = self.layer4(out)
        out = out.reshape(out.shape[0], -1)  # out = torch.flatten(out, 1)
        out = self.fc(out)
        return out

    def _make_layer(self, block, block_channel, block_num, stride):
        layers = []
        # 注意:第二个layer的输入是256通道个56*56,下采样后是512通道个28*28。
        downsample = nn.Conv2d(self.in_channel, block_channel * block.expansion, kernel_size=1, stride=stride,
                               bias=False)

        # 先加一个带有下采样的layer
        layers += [block(self.in_channel, block_channel, stride=stride, downsample=downsample)]
        self.in_channel = block_channel * block.expansion

        # 再加block_num-1个默认不带下采样的layer,由于输入输出通道数相同,所以不需要下采样
        for _ in range(1, block_num):
            layers += [block(self.in_channel, block_channel, stride=1)]
        return nn.Sequential(*layers)


if __name__ == '__main__':
    x = torch.randn(1, 3, 224, 224)
    my_resnet50 = Resnet()
    resnet50 = torchvision.models.resnet50()  # 看源码对比官方的resnet50

    print(my_resnet50)
    # print(resnet50)

    y = my_resnet50(x)
    print(y.shape)

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐