# 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)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://openmmlabbook.gitbook.io/openmmlab-book/image-classification/modern/resnet.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
