> For the complete documentation index, see [llms.txt](https://openmmlabbook.gitbook.io/openmmlab-book/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://openmmlabbook.gitbook.io/openmmlab-book/image-classification/modern/resnet.md).

# ResNet

残差网络（Residual Network, ResNet）是何恺明等人于 2015 年提出的一系列网络结构，并借此获得了 ILSVRC 2015 数个项目的冠军。 为了解决深层网络难以训练的问题，ResNet 引入了残差学习的概念，并在网络结构中引入了“超前”连接，让激活值可以经过较少的卷积层就可以传到输出。

![](/files/-MPF80H09nmPzqtz34eD)

实践证明这种方式是非常有效的。 使用残差结构以后，更深层的网络可以训练出更高的分类准确率。

## 背景

### 深层网络的退化问题

在 ResNet 出现之前，改进神经网络的主流逻辑是堆叠更多的层，加深网络的结构。 理论上，堆叠更多的层可以增强网络的表达能力，深层网络应该可以取得不差于浅层网络的性能。 然而实验却给出了相反的结论：使用相同的梯度下降方法对网络进行训练，深层网络取得的分类精度反而低于浅层网络。 这说明，虽然理论上深层网络的表达能力增强了，或者说可表达的函数的空间变大了，但是在这个更大的函数空间中找到最优解却更为困难。

![](/files/-MPF80H1CTeDnpExJT97)

在论文中，作者将这种现象称为深层网络的*退化*（degradation）现象。

## 残差学习

我们可以从理论的角度更清楚地阐述退化问题。 考虑一个浅层的神经网络，它表达了一个含参数的函数 $$f(x; w\_0)$$ 。 如果我们叠加一个新的层 $$g(z, w)$$ ，则这个深了一层的网络表达了两个函数的复合函数，即 $$F(x; w\_0, w) = g(f(x; w\_0); w)$$。

如果我们通过设置参数 $$w$$ 使得 $$g$$ 成为一个恒等函数，例如使用恒等映射对应的卷积核，那么这个深一层的网络就可以表达出所有浅层网络可以表达的函数。 如果我们对使 $$g$$ 产生一些变化，那么复合函数 $$F$$ 就可以表达出更多的函数，而如果这些函数中有可以提高分类精度的函数，那么深层网络就可以在数据集上取得更高的精度。 然而实验结果却意味着，直接学习出这样的 $$g$$ 或许并不容易。

基于这个观察，论文作者提出了残差学习的概念。既然让优化器学习出一个近似恒等的映射 $$g$$ 并不容易，那么我们将 $$g(z; w)$$ 显式表达成一个 $$g(z; w) = z + h(z; w)$$ 的形式，即恒等项与一个复杂项的求和。当 $$h$$ 为恒零函数时， $$g$$ 可以表达一个恒等映射，而作者猜想，通过学习 $$h$$ 得到想要的映射$$g$$的难度要低于直接学习 $$g$ 。这就是残差学习的概念。

## 残差模块

残差形式可以很直接地被一个神经网络表示，我们只需要用若干层叠加的子网络表示 $$h(z; w)$$ 部分，再将这个子网络的输入和输出相加，就可以让这个网络表示 $$z + h(z; w)$$ 。

在 ResNet 结构中，残差块 $$h$$ 的实现方式有两种，一种称为基本模块(basic block)一种称为瓶颈模块(bottleneck block)。

![](/files/-MPF80H4tZBrlFNZL385)

基本模块使用两个卷积层构成残差项，每个卷积层都使用 3x3 的卷积核且附带 BN，第一个卷积层后附带 ReLU，第二个卷积层后不使用 ReLU，而是对求和的结果使用 ReLU 。

瓶颈模块使用三个卷积层构成残差项，其中第一和第三个卷积层使用 1x1 的卷积层对通道数进行变换，中间的卷积层在低通道数的情况下使用 3x3 的卷积核扩展感受野。这种设计主要是为了降低卷积层的运算量，因为卷积层的运算量正比于输入和输出通道数的乘积。与基本模块类似，前两个卷积层后连接有 BN 和 ReLU 层，第三个卷积后连接 BN 层并且在求和后应用 ReLU。

残差模块中的求和是逐个元素进行的。如果我们的卷积层使用 stride=1 和 padding=1 的配置，那么残差模块输出的特征图的形状就和输入特征图的形状相同，自然可以求和。

随着网络层数的加深，我们通常会降低特征图的空间分辨率，并加特征的通道数。例如输入 56x56 大小，64 通道的特征，输出 28x28 大小，128 通道的特征图。

为了保证求和的两个特征图在形状上完全相同，我们需要同时对直通分支和残差分支进行相同的操作。针对残差分支，我们通常在第一个卷积层使用 stride=2 进行空间降采样，并扩展输出的通道数。针对直通分支，我们会额外增加一个 1x1 的卷积，使用 stride=2 降低空间分辨率，并增加输出的通道数。

## 网络结构

一个完整的 ResNet 网络由若干残差模块连接构成，残差块的个数决定了网络的总层数，标准配置有 18、34、50、101、152 五种，在 ResNet-18 和 ResNet-34 中，残差模块使用基本模块，而在深层的 ResNet 结构中，残差模块使用瓶颈模块，以控制网络的总计算量。五种配置的详细结构如下图所示：

![](/files/-MPF80H5Mc_ZTRdP5U56)

虽然层数有所不同，五种配置都可以分为五个阶段。第一个阶段为普通的卷积层，可以将 224x224 的图片转换为 112x112 分辨率、64 通道的特征图。

随后的四个阶段都是由残差模块级联而成的，在每个阶段的开始，我们需要对特征图进行空间降采样，并扩展通道数。其中第二阶段稍有特殊，在阶段开始使用池化进行降采样，其余阶段都是通过 stride=2 的卷积曾进行降采样。

经过五个阶段的降采样，224x224 的图象被缩小为 7x7 分辨率（1/32 比例），512 或 2048 通道的特征图。随后，一个 global average pooling 层和一个全连接层输出 1000 类的条件概率。

五种配置的不同之处仅在于残差模块的选取以及残差模块的个数。

## torchvision 的实现

torchvision 在 [resnet.py](https://github.com/pytorch/vision/blob/master/torchvision/models/resnet.py) 中实现了上述五种标准的 ResNet 结构。 用户可以通过类似 `model = resnet18()` 的代码生成模型的实例。 这份代码实现了 ResNet 中的两种残差模块——基本模块 `BasicBlock` 和瓶颈模块 `Bottleneck`，并根据不同 ResNet 结构中残差模块的数量实现了五种结构。下面简单对实现进行讲解。

### ResNet 的工厂类

`resnet` 子模块对外有五个主要接口： `resnet18`，`resnet34`，`resnet50`，`resnet101`，`resnet152` （还有一些 ResNeXt 相关的，这里暂时也不考虑）分别对应五种不同的 ResNet 结构。这五个接口是五个函数接口，我们以 ResNet18 举例，它的实现如下：

```python
def resnet18(pretrained: bool = False, progress: bool = True, **kwargs: Any) -> ResNet:
    return _resnet('resnet18', BasicBlock, [2, 2, 2, 2], pretrained, progress,
                   **kwargs)
```

当用户调用 `resnet18` 时，程序会根据预先设置的残差模块类型 `BasicBlock` 以及四个阶段中残差模块的个数 `[2, 2, 2, 2]` 根据工厂函数 `_resnet` 构建一个 ResNet18 的实例。 其他函数的实现与 `resnet18` 类似，只不过使用的模块类型和残差模块的个数有所不同。

工厂函数 `_resnet` 中调用了工厂类 `ResNet` 实现 ResNet 的计算逻辑，并处理了加载预训练参数的逻辑。

`ResNet` 类型是 ResNet 模型的主体实现，它的前传函数给出了 ResNet 从输入到输出的计算逻辑，如下所示。 为了清楚，我们将每段代码的具体意义以注释的形式标注在对应的位置。

```python
class ResNet(nn.Module):
    def __init__(
        self,
        block,   # 控制使用 BasicBlock 还是 BottleNeck
        layers,  # 控制每个阶段中使用多少个残差块
        ## 省略一些参数 ##
        ):
        ## 省略一些代码 ##
        # 实现了四个残差的阶段
        self.layer1 = self._make_layer(block, 64, layers[0])
        self.layer2 = self._make_layer(block, 128, layers[1], stride=2,
                                       dilate=replace_stride_with_dilation[0])
        self.layer3 = self._make_layer(block, 256, layers[2], stride=2,
                                       dilate=replace_stride_with_dilation[1])
        self.layer4 = self._make_layer(block, 512, layers[3], stride=2,
                                       dilate=replace_stride_with_dilation[2])
        ## 省略一些代码 ##

    def _make_layer(self, block: Type[Union[BasicBlock, Bottleneck]], planes: int, blocks: int,
                    stride: int = 1, dilate: bool = False):
        ## 省略一些代码 ##

    def _forward_impl(self, x: Tensor) -> Tensor:
        # conv1 或 称为stem 的部分
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        # conv2 到 conv5 的部分
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        # GAP 和分类器的部分
        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)

        return x

    def forward(self, x: Tensor) -> Tensor:
        return self._forward_impl(x)
```

工厂类型 `ResNet` 的实现非常直接，可以很容易地和我们上面讲到的 ResNet 结构对应起来。

conv1 的部分比较简单，conv2 到 conv5 的部分则通过 `_make_layer` 生成，并在 `_forward_impl` 函数中函数中串联起来。 `_make_layer` 将多个残差模块串联在一起，构成一个 ResNet 的 conv 阶段，具体的实现及相应的注释如下：

```python
class ResNet(nn.Module):

    def _make_layer(self, block: Type[Union[BasicBlock, Bottleneck]], planes: int, blocks: int,
                    stride: int = 1, dilate: bool = False) -> nn.Sequential:
        norm_layer = self._norm_layer   # 默认是  nn.BatchNorm2d
        downsample = None

        ## 省略 dilation 的配置部分 ##

        if stride != 1 or self.inplanes != planes * block.expansion:
            # 对如 conv3 到 conv 5，即 layer2 到 layer4，stride 是 2
            # 这时使用 stride=2 的一个 1x1 的卷积层作为直通分支的降采样操作
            downsample = nn.Sequential(
                conv1x1(self.inplanes, planes * block.expansion, stride),
                norm_layer(planes * block.expansion),
            )

        layers = []

        # 第一个模块会根据情况使用降采样
        # block 是 BasicBlock 或 Bottleneck
        layers.append(block(self.inplanes, planes, stride, downsample, self.groups,
                            self.base_width, previous_dilation, norm_layer))

        self.inplanes = planes * block.expansion    # Basic block 的 expansion 是 1，BottleNeck 的是 4

        # 后几个残差模块不适用将采样，stride为默认值1
        for _ in range(1, blocks):
            layers.append(block(self.inplanes, planes, groups=self.groups,
                                base_width=self.base_width, dilation=self.dilation,
                                norm_layer=norm_layer))

        return nn.Sequential(*layers)
```

### 残差模块

下面我们简要介绍下 BasicBlock 的实现，BottleNeck 和 BasicBlock 的实现逻辑类似。 都是通过几个 Conv2D 卷积层串连实现残差部分。 如果需要降采样，则 conv1 的 stride 在 `__init__` 中配置为 2，否则为 1。 同时，直通通路会使用 `_make_layer` 中生成并传入的 `downsample` 模块减半空间分辨率，并倍增通道数。 需要注意的是，最后一个 ReLU 再直通和残差求和之后，而不是求和之前。

```python
class BasicBlock(nn.Module):
    expansion: int = 1

    def __init__(
        self,
        # 忽略部分参数
        stride: int = 1,
        downsample: Optional[nn.Module] = None,
        # 忽略部分参数
    ) -> None:
        # 忽略部分代码
        self.conv1 = conv3x3(inplanes, planes, stride)
        # 忽略部分代码
        self.downsample = downsample
        self.stride = stride

    def forward(self, x: Tensor) -> Tensor:
        identity = x

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

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

        # 如果需要将采样，即 `_make_layer` 函数传入一个 1x1 s=2 的卷积模块时，使用这个卷积模块作为直通通路的计算
        # 如果不需要降采样，直接将输入相加到输出上
        if self.downsample is not None:
            identity = self.downsample(x)

        out += identity

        # 需要注意的是，先求和再计算 ReLU
        out = self.relu(out)

        return out
```

## ResNet 的改进

作为目前最实用的模型之一，ResNet 的一些细节改进也层出不穷。 本节介绍一些针对 ResNet 结构的改进，在通用或者特定的情况下可以提高图像分类准确率。

### 1x1 卷积的降采样

原始的 ResNet 会使用 1x1，stride=2 的卷积层对特征图进行空间上的降采样，产生长宽仅有 1/2 的下一层特征图。 这种降采样方式会在两种场合下使用：

如果使用这种卷积层，前一层特征图上 3/4 的位置的特征值并没有参与运算。 为了能更充分地利用前一层产生的特征，我们可以

1. 针对直通分支，我们先使用 Average Pooling 进行降采样，再使用 1x1 的卷积层转换特征的通道数。
2. 针对残差分支，我们应用 1x1 卷积降低通道数时使用 stride=1，即不进行空间降采样，在第二个卷积层，即 3x3 的卷积层再使用 stride=2 进行降采样。torchvision 工具包采取了这种方式。

这两种改进方式都以少量增加的计算量为代价，实现了对前层特征的充分利用。

针对残差分支改进后的结构通称为 ResNet v1b，针对二者都进行改进的结构称为 ResNet v1d。

### stem 结构

我们称残差块之前的结构为 stem （茎）。 在标准的 RseNet 结构中，stem 由一个卷积层和一个池化层构成，其中卷积核的大小为 7x7。

对 ResNet 结构的一种改进使用 3 个 3x3 的卷积层替代这个 7x7 的卷积层，可以在相同感受野的情况下增加层数并减少参数个数。类似的技巧在 VGGNet 、Inception 中也有所使用。 在原始的 ResNet 结构中，第一个卷积层是 64 通道的，如果使用 3 个 3x3 的卷积层，则前两个卷积层会使用 32 通道，第三个卷积层使用 64 通道。

另外，如果将 ResNet 应用于 CIFAR10 等图像分辨率较小的数据集，也可以使用单个 3x3 卷积代替 7x7 的卷积。

基于 v1b 进行 stem 改进后的结构也通称为 ResNet v1c。

### 提前激活（pre-activation）

ResNet 的提前激活版本是何恺明等人与 2016 年 ECCV 上提出的一种对 ResNet 的改进。在原始的 ResNet 结构中，残差分支和直通分支求和后，会应用 ReLU 激活函数，之后再输入下一个残差模块。 如果我们剔除残差分支，单独考虑直通分支，出去降采样等操作之外，直通分支上有一系列的非线性函数。

论文认为，直通分支对网络的训练是非常重要的，因而将 ReLU 从求和后移动到了残差分支。这样直通分支上就没有任何非线性函数，输入信号也可以通过直通分支传递到网络的任意深度。

这种改进也通称为 ResNet v2。

## 参考资料

1. ResNet 原始论文: [Deep Residual Learning for Image Recognition](https://arxiv.org/abs/1512.03385)
2. 汇总相关改进的论文: [Bag of Tricks for Image Classification with Convolutional Neural Networks](https://arxiv.org/abs/1812.01187)
3. ResNet v2: [Identity Mappings in Deep Residual Networks](https://arxiv.org/pdf/1603.05027v3.pdf)
