Skip to content

필사 모드: ResNet 论文全面解析:残差连接(Residual Connection)如何突破深度学习的深度极限

中文
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

1. 论文概要

"Deep Residual Learning for Image Recognition"是 2015 年由 Microsoft Research 的 Kaiming He、Xiangyu Zhang、Shaoqing Ren、Jian Sun 发表的论文。该论文获得了 CVPR 2016 的 Best Paper Award,截至 2025 年引用数已超过 20 万次,是深度学习历史上最具影响力的论文之一。

这篇论文所解决的问题很明确。"网络越深,性能理应越好,为什么反而会变差?"——针对这个简单问题所提出的答案Residual Learning Framework(残差学习框架),成功训练出了一个 152 层的网络,在 ImageNet ILSVRC 2015 Classification Task 上以 top-5 error 3.57%的成绩夺得第一名。这一数字大幅低于人类的识别错误率(约 5.1%)。

ResNet 并不只是在 Image Classification 上夺冠。同年它还在 ImageNet Detection、ImageNet Localization、COCO Detection、COCO Segmentation 等共计 5 个赛道上全部拿下第一。而这篇论文提出的 Skip Connection(Shortcut Connection)此后成为 Transformer、BERT、GPT、Diffusion Model 等几乎所有现代深度学习架构的核心组成部分。


2. 论文背景:深度的困境

2.1 深层网络的时代

自 2012 年 AlexNet(8 层)在 ImageNet Challenge 上夺冠以来,深度学习社区一直遵循着"网络越深=性能越好"这一直觉。事实上,这个直觉在很大程度上是对的。

年份模型层数Top-5 Error (%)
2012AlexNet816.4
2013ZFNet814.8
2014VGGNet197.3
2014GoogLeNet (Inception v1)226.7
2015ResNet1523.57

VGGNet(2014)仅使用 3x3 Convolution 就把深度堆到了 19 层,GoogLeNet 则借助名为 Inception Module 的并行结构构建出 22 层的网络。这两个模型都用实验证明了"深度对性能起决定性作用"。

2.2 VGGNet 的启示与局限

VGGNet 在架构设计上确立了一条重要原则:与其用一个大卷积核(5x5、7x7),不如堆叠多个小卷积核(3x3)——这样既能获得相同的 Receptive Field,又能减少参数量,还能在中间插入更多非线性激活函数来提升表达能力。

然而 19 层实际上已经是 VGGNet 的极限。从 VGG-16 到 VGG-19,性能提升幅度已经大幅缩小,若再继续加深,性能反而会变差。参数量也是个问题:VGG-19 需要约 1.44 亿个参数、19.6 billion FLOPs 的计算量。

2.3 GoogLeNet(Inception)的思路

GoogLeNet 从与 VGGNet 不同的方向切入深度问题。它设计了并行执行 1x1、3x3、5x5 Convolution 的 Inception Module,并用 1x1 Convolution 削减通道数以节省计算量。虽然只有 22 层,却用比 VGGNet 更少的参数(约 500 万个)实现了更低的 Error Rate。

然而 Inception Module 的复杂结构在可扩展性上存在局限。仅靠单纯增加层数很难进一步提升性能。

2.4 根本性的问题

到这个阶段,社区所面对的根本性问题是这样的:

"有没有办法能够自由地增加网络的深度?"

而 ResNet 正是对这一问题的回答。


3. Degradation 问题的发现

3.1 Deeper != Better

ResNet 论文最重要的贡献之一,就是清晰地定义并用实验证明了Degradation 问题

直觉上讲,在一个浅层网络之上添加执行 Identity Mapping 的层,性能至少应该不低于原来的浅层网络——因为新增的层只需什么都不做、原样输出输入即可。因此,更深网络的 Training Error 理应不会高于浅层网络。

但现实并非如此。论文在 CIFAR-10 和 ImageNet 上都观察到:不带 Shortcut 的普通网络(Plain Network)中,56 层模型的 Training Error 反而比 20 层模型更高。这并非 Overfitting 问题——如果是 Overfitting,应该是 Training Error 低而只有 Validation Error 高。Training Error 本身偏高,意味着优化(Optimization)本身就很困难

3.2 与 Vanishing/Exploding Gradient 的区别

Degradation 问题与 Vanishing Gradient、Exploding Gradient 问题是不同的现象。

Vanishing/Exploding Gradient已经在很大程度上被 Batch Normalization、He Initialization 等方法解决。事实上,论文中的 Plain Network 也应用了这些方法,网络确实收敛了,问题在于收敛点的性能低于浅层网络。

Training Error56-layer plain>Training Error20-layer plain\text{Training Error}_{56\text{-layer plain}} > \text{Training Error}_{20\text{-layer plain}}

这一现象说明,即便 Gradient 传播良好,在深层网络中让非线性层堆叠去学出 Identity Mapping 本身也是极其困难的。

3.3 Construction Argument

论文提出的核心论证是Construction Argument(构造性论证)。不妨这样思考:

  1. 假设存在一个浅层网络 AA
  2. AA 之上添加若干执行 Identity Mapping 的层,构造出深层网络 BB
  3. 由于新增的层是恒等函数,BB 的性能理应至少与 AA 相同。
  4. 因此深层网络 BB 的 Training Error 不应高于 AA

但在实际实验中,BB 的 Training Error 却高于 AA。这说明当前基于 SGD 的 Optimizer 找不到这样的解。问题不在于模型的表达能力,而在于优化的难度


4. 核心思路:Residual Learning

4.1 核心直觉

如果 Degradation 问题的根源在于"难以学出 Identity Mapping",那解决方案就很简单:把 Identity Mapping 显式地内置到网络中即可。

设原网络中某个 block 需要学习的函数为 H(x)\mathcal{H}(\mathbf{x})。原本的目标是直接学习这个 H(x)\mathcal{H}(\mathbf{x})。如果 H(x)=x\mathcal{H}(\mathbf{x}) = \mathbf{x}(Identity Mapping),那么用非线性层堆叠去学出这个函数会很困难。

ResNet 的核心思路是,不直接学习 H(x)\mathcal{H}(\mathbf{x}),而是重新构造问题去学习残差(Residual):

F(x):=H(x)x\mathcal{F}(\mathbf{x}) := \mathcal{H}(\mathbf{x}) - \mathbf{x}

由此:

H(x)=F(x)+x\mathcal{H}(\mathbf{x}) = \mathcal{F}(\mathbf{x}) + \mathbf{x}

如果最优映射接近 Identity,那么把 F(x)\mathcal{F}(\mathbf{x}) 学成 0,要比把 H(x)\mathcal{H}(\mathbf{x}) 学成 Identity 容易得多——因为把非线性层的权重初始化为 0 或让它学得接近 0,是很自然的事情。

4.2 Residual Block 的结构

Residual Block 具有如下结构:

y=F(x,{Wi})+x\mathbf{y} = \mathcal{F}(\mathbf{x}, \{W_i\}) + \mathbf{x}

其中:

  • x\mathbf{x}:block 的输入
  • F(x,{Wi})\mathcal{F}(\mathbf{x}, \{W_i\}):需要学习的残差函数(2~3 个 Convolution 层)
  • y\mathbf{y}:block 的输出

F(x,{Wi})+x\mathcal{F}(\mathbf{x}, \{W_i\}) + \mathbf{x} 中的加法(++)以 Element-wise Addition 的方式完成,这一连接被称为Shortcut ConnectionSkip Connection。该运算完全不需要额外参数,计算量的增加也可以忽略不计。

对于拥有 2 个层的 Residual Block:

F=W2σ(W1x)\mathcal{F} = W_2 \sigma(W_1 \mathbf{x})

其中 σ\sigma 是 ReLU 激活函数,Bias 为表述简便省略。

4.3 Dimension Mismatch 的处理

F(x)\mathcal{F}(\mathbf{x})x\mathbf{x} 的维度不一致时(即 Feature Map 通道数发生变化的 Downsampling 阶段),无法直接相加。为此论文使用了Linear Projection

y=F(x,{Wi})+Wsx\mathbf{y} = \mathcal{F}(\mathbf{x}, \{W_i\}) + W_s \mathbf{x}

其中 WsW_s 是用于对齐维度的 Projection Matrix。论文中实验了三种方案:

  • Option A:用 Zero-padding 对齐维度(不增加参数)
  • Option B:仅在维度变化时使用 1x1 Convolution Projection
  • Option C:所有 Shortcut 都使用 1x1 Convolution Projection

实验结果显示,三种方案都明显优于 Plain Network,而方案之间的差异很小。这说明 Projection 并不是解决 Degradation 问题的关键,Identity Shortcut 本身才是关键所在。最终版 ResNet 出于内存与计算效率的考虑采用了Option B


5. 数学分析:Gradient Flow

5.1 Forward Propagation

来分析一下 Residual Block 的 Forward Propagation。设第 ll 个 Residual Block 的输出为 xl\mathbf{x}_l,则:

xl+1=xl+F(xl,Wl)\mathbf{x}_{l+1} = \mathbf{x}_l + \mathcal{F}(\mathbf{x}_l, W_l)

将其递归展开,可得任意深层 LL 处的输出为:

xL=xl+i=lL1F(xi,Wi)\mathbf{x}_L = \mathbf{x}_l + \sum_{i=l}^{L-1} \mathcal{F}(\mathbf{x}_i, W_i)

这个公式的含义非常重要:任意深层 LL 的 Feature,都可以表示为浅层 ll 的 Feature,加上二者之间所有 Residual Function 的和。 在 Plain Network 中,这体现为矩阵乘法的连乘(Chain of Matrix Multiplications),而在 ResNet 中则体现为加法的形式。

5.2 Backward Propagation 与 Gradient Highway

接下来看核心的 Backward Propagation。设 Loss 为 L\mathcal{L},根据 Chain Rule:

Lxl=LxLxLxl\frac{\partial \mathcal{L}}{\partial \mathbf{x}_l} = \frac{\partial \mathcal{L}}{\partial \mathbf{x}_L} \cdot \frac{\partial \mathbf{x}_L}{\partial \mathbf{x}_l}

代入前面推导出的 Forward 公式:

Lxl=LxL(1+xli=lL1F(xi,Wi))\frac{\partial \mathcal{L}}{\partial \mathbf{x}_l} = \frac{\partial \mathcal{L}}{\partial \mathbf{x}_L} \cdot \left(1 + \frac{\partial}{\partial \mathbf{x}_l} \sum_{i=l}^{L-1} \mathcal{F}(\mathbf{x}_i, W_i)\right)

这个公式的关键在于常数项 1。Gradient 可以经由这条通路,从任意层直接流向 Loss。无论 xli=lL1F(xi,Wi)\frac{\partial}{\partial \mathbf{x}_l} \sum_{i=l}^{L-1} \mathcal{F}(\mathbf{x}_i, W_i) 这一项变得多小,有了常数项 1,Gradient 也不会完全消失。

这正是 ResNet 形成Gradient Highway的原理。在 Plain Network 中,Gradient 必须与每一层的权重矩阵相乘:

Lxl=i=lL1xi+1xiLxL\frac{\partial \mathcal{L}}{\partial \mathbf{x}_l} = \prod_{i=l}^{L-1} \frac{\partial \mathbf{x}_{i+1}}{\partial \mathbf{x}_i} \cdot \frac{\partial \mathcal{L}}{\partial \mathbf{x}_L}

在这种连乘形式下,只要每一项都比 1 略小一点,Gradient 就会呈指数级衰减。而在 ResNet 的求和形式中,不会出现这种问题。

5.3 为什么学习"Residual"更容易

再从数学上给出一个更直观的解释。如果网络的每一层最优情况下只对输入施加微小的变化(这在深层网络中是很自然的假设),那么残差函数 F(x)\mathcal{F}(\mathbf{x}) 就应该输出接近 0 的值。

由于权重矩阵被初始化为接近 0,训练初期每个 Residual Block 执行的都近似于 Identity 映射。这可以解读为:深层网络在训练初期表现得像浅层网络,随后逐步让每个 block 学到有用的变换。


6. 架构详解

6.1 整体结构概览

ResNet 以 VGGNet 的设计理念为基础,在其上加入了 Shortcut Connection。所有 ResNet 变体都具有以下共同结构:

  1. conv1:7x7 Convolution,stride 2,64 filters,BatchNorm,ReLU
  2. Max Pooling:3x3,stride 2
  3. conv2_x ~ conv5_x:Residual Block 的堆叠
  4. Global Average Pooling:将 Feature Map 缩减为 1x1
  5. Fully Connected Layer:1000-class Softmax

当 Feature Map 的尺寸减半时(conv3_x、conv4_x、conv5_x 各自的第一个 block),通道数会翻倍。Downsampling 通过 stride 2 的 Convolution 完成。

6.2 Basic Block(ResNet-18、ResNet-34)

Basic Block 由 2 个 3x3 Convolution 组成。

Input (C channels)
  |
  ├──→ 3x3 Conv, C filters, BN, ReLU
  |    3x3 Conv, C filters, BN
  |
  └──→ (Identity Shortcut)
  |
  + ←── Element-wise Addition
  |
  ReLU
  |
Output (C channels)

每个 Convolution 之后都会应用 Batch Normalization,ReLU 则在 Addition 之后应用。

6.3 Bottleneck Block(ResNet-50、ResNet-101、ResNet-152)

50 层以上的 ResNet 出于计算效率的考虑,采用Bottleneck结构,由 3 个 Convolution(1x1、3x3、1x1)组成。

Input (4C channels)
  |
  ├──→ 1x1 Conv, C filters, BN, ReLU    (通道压缩: 4C → C)
  |    3x3 Conv, C filters, BN, ReLU    (空间处理)
  |    1x1 Conv, 4C filters, BN          (通道恢复: C → 4C)
  |
  └──→ (Identity Shortcut)
  |
  + ←── Element-wise Addition
  |
  ReLU
  |
Output (4C channels)

Bottleneck 的核心思路是先用 1x1 Convolution 把通道数压缩到 1/4,再执行开销较大的 3x3 Convolution,最后再用 1x1 Convolution 把通道数恢复回来。得益于这一结构,ResNet-50 虽然比 ResNet-34 更深,计算量(FLOPs)却维持在相近水平。

6.4 架构对比表

LayerOutput SizeResNet-18ResNet-34ResNet-50ResNet-101ResNet-152
conv1112x1127x7, 64, stride 27x7, 64, stride 27x7, 64, stride 27x7, 64, stride 27x7, 64, stride 2
pool56x563x3 max pool, stride 23x3 max pool, stride 23x3 max pool, stride 23x3 max pool, stride 23x3 max pool, stride 2
conv2_x56x56[3x3, 64] x2[3x3, 64] x3[1x1, 64; 3x3, 64; 1x1, 256] x3[1x1, 64; 3x3, 64; 1x1, 256] x3[1x1, 64; 3x3, 64; 1x1, 256] x3
conv3_x28x28[3x3, 128] x2[3x3, 128] x4[1x1, 128; 3x3, 128; 1x1, 512] x4[1x1, 128; 3x3, 128; 1x1, 512] x4[1x1, 128; 3x3, 128; 1x1, 512] x8
conv4_x14x14[3x3, 256] x2[3x3, 256] x6[1x1, 256; 3x3, 256; 1x1, 1024] x6[1x1, 256; 3x3, 256; 1x1, 1024] x23[1x1, 256; 3x3, 256; 1x1, 1024] x36
conv5_x7x7[3x3, 512] x2[3x3, 512] x3[1x1, 512; 3x3, 512; 1x1, 2048] x3[1x1, 512; 3x3, 512; 1x1, 2048] x3[1x1, 512; 3x3, 512; 1x1, 2048] x3
1x1Global Average Pool, 1000-d FC, Softmax

6.5 参数量与计算量

ModelLayersParametersFLOPs
VGG-1919144M19.6B
ResNet-181811.7M1.8B
ResNet-343421.8M3.6B
ResNet-505025.6M3.8B
ResNet-10110144.5M7.6B
ResNet-15215260.2M11.3B

值得注意的是,ResNet-152 比 VGG-19 深 8 倍,计算量却更少,参数量还不到一半。这是因为 VGGNet 把大部分参数用在了最后的 Fully Connected Layer 上,而 ResNet 借助 Global Average Pooling 大幅削减了 FC Layer 的参数量。


7. 实验结果

7.1 ImageNet Classification

Plain Network 的 Degradation 确认

论文首先在不带 Shortcut 的 Plain Network 上确认了 Degradation 问题。

ModelTop-1 Error (%)Top-5 Error (%)
Plain-1827.94-
Plain-3428.54-

34 层的 Plain Network 展现出比 18 层更高 0.6%的 Error Rate。这正是 Degradation 问题。

Residual Network 的效果

在相同结构上仅添加 Shortcut Connection 后的结果:

ModelTop-1 Error (%)Top-5 Error (%)
ResNet-1827.88-
ResNet-3425.03-

ResNet-34 比 ResNet-18 实现了低 2.85%的 Error Rate。Plain Network 中观察到的 Degradation 问题被完全消除,深度增加所带来的性能提升也清晰地显现出来。

Bottleneck ResNet 的结果(10-crop Testing)

ModelTop-1 Error (%)Top-5 Error (%)
ResNet-5022.856.71
ResNet-10121.756.05
ResNet-15221.435.71

ResNet-152 单模型的 Top-5 Error 为 4.49%(Multi-scale、Multi-crop),6 个模型 Ensemble 后的结果为3.57%,凭此在 ImageNet ILSVRC 2015 Classification 中夺得第一名。

与 VGG、GoogLeNet 的对比

ModelTop-5 Error (%)Ensemble Top-5 Error (%)
VGG-167.3-
GoogLeNet6.7-
ResNet-152 (single model)4.49-
ResNet Ensemble (6 models)-3.57

7.2 CIFAR-10 实验

在 CIFAR-10 数据集(32x32 图像,10 个类别)上也确认了 Degradation 问题,并验证了 ResNet 的效果。CIFAR-10 版的 ResNet 与 ImageNet 版不同,第一层是 3x3 Convolution,并在 3 个 Stage(Feature Map 尺寸分别为 32x32、16x16、8x8)中各使用 {n, n, n} 个 Residual Block。

ModelLayersError (%)
ResNet-20208.75
ResNet-32327.51
ResNet-44447.17
ResNet-56566.97
ResNet-1101106.43
ResNet-120212027.93

一直到 110 层,性能都在持续提升。1202 层的网络虽然 Training Error 更低,但 Test Error 却高于 110 层——论文将其分析为 Overfitting。原因是 19.4M 的参数量对于该数据集(50,000 张训练图像)而言过于庞大。论文没有应用 Regularization(如 Dropout),并提到应用之后有望改善。

7.3 COCO Object Detection & Segmentation

ResNet 的效果不仅限于 Image Classification,在 Object Detection 与 Segmentation 上也得到了验证。

PASCAL VOC & COCO Detection

将 Faster R-CNN 的 Backbone 从 VGG-16 换成 ResNet-101 后的结果:

  • COCO Detection:以 mAP@[.5, .95] 为基准,相较 VGG-16 提升 6.0%(相对提升 28%)
  • ILSVRC 2015 Detection Task 第一名
  • COCO 2015 Detection Task 第一名

COCO Segmentation

  • COCO 2015 Segmentation Task 第一名

这些结果证明了 ResNet 并非只是一个专用于 Classification 的模型,而是能作为通用 Feature Extractor,在各类 Vision Task 中提供强大性能。


8. 实现细节

8.1 He Initialization

ResNet 论文的作者 Kaiming He 在 ResNet 之前就已经提出过一种适用于 ReLU 网络的权重初始化方法("Delving Deep into Rectifiers",He et al.,2015)。

WN(0,2nin)W \sim \mathcal{N}\left(0, \sqrt{\frac{2}{n_{in}}}\right)

其中 ninn_{in} 是该层的输入单元数。考虑到 ReLU 会把输入的一半(负数部分)置零,方差被设为 2nin\frac{2}{n_{in}}。Xavier Initialization(1nin\frac{1}{n_{in}})适用于 Sigmoid/Tanh,但在 ReLU 下会出现方差逐渐减小的问题。

He Initialization 让每一层的输出方差保持恒定,从而在 Forward Pass 中避免信号消失或爆炸。

8.2 Batch Normalization

ResNet 在每个 Convolution 层之后都应用了Batch Normalization(BN)。

x^i=xiμBσB2+ϵ\hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}} yi=γx^i+βy_i = \gamma \hat{x}_i + \beta

其中:

  • μB\mu_BσB2\sigma_B^2:Mini-batch 的均值与方差
  • γ\gammaβ\beta:可学习的 Scale 与 Shift 参数
  • ϵ\epsilon:用于数值稳定性的小常数

BN 的作用如下:

  • 缓解 Internal Covariate Shift:对每一层的输入分布做归一化,稳定训练过程
  • Regularization 效果:以 Mini-batch 为单位的归一化会引入轻微噪声,起到 Regularizer 的作用
  • 允许更高的 Learning Rate:由于分布更稳定,可以使用更高的 Learning Rate

ResNet 中 BN 的排列顺序是Conv → BN → ReLU(Post-activation)。这一顺序后来在后续研究(Pre-activation ResNet)中得到改进。

8.3 Training Schedule

在 ImageNet 上的训练设置:

HyperparameterValue
OptimizerSGD with Momentum
Momentum0.9
Weight Decay0.0001
Batch Size256
Initial Learning Rate0.1
LR Schedule每 30 个 epoch 衰减为 1/10
Total Epochs~90
Data AugmentationRandom Crop (224x224), Horizontal Flip, Color Jittering
PreprocessingPer-pixel Mean Subtraction

训练时图像先在 [256, 480] 范围内随机缩放,再 Random Crop 为 224x224。测试时使用 10-crop Testing(4 个角 + 中心 + 各自的 Horizontal Flip),在 Multi-scale Testing 中则以 {224, 256, 384, 480, 640} 尺寸执行 Fully Convolutional 推理。

8.4 Dropout 的缺席

有意思的是,ResNet不使用 Dropout。这是因为 Batch Normalization 已提供了足够的 Regularization 效果,而 Bottleneck 结构本身也限制了参数量。Global Average Pooling 同样大幅削减了 FC Layer 的参数量,降低了 Overfitting 的风险。


9. PyTorch 实现

9.1 Basic Block

import torch
import torch.nn as nn

class BasicBlock(nn.Module):
    """用于 ResNet-18、ResNet-34 的 Basic Residual Block"""
    expansion = 1  # 输出通道数 = 输入通道数 * expansion

    def __init__(self, in_channels, out_channels, stride=1, downsample=None):
        super(BasicBlock, self).__init__()
        # 第一个 3x3 Conv (可通过 stride 实现 downsampling)
        self.conv1 = nn.Conv2d(
            in_channels, out_channels, kernel_size=3,
            stride=stride, padding=1, bias=False
        )
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)

        # 第二个 3x3 Conv
        self.conv2 = nn.Conv2d(
            out_channels, out_channels, kernel_size=3,
            stride=1, padding=1, bias=False
        )
        self.bn2 = nn.BatchNorm2d(out_channels)

        # Shortcut: 维度不一致时用 1x1 Conv 做 projection
        self.downsample = downsample

    def forward(self, x):
        identity = x

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

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

        # Shortcut Connection
        if self.downsample is not None:
            identity = self.downsample(x)

        out += identity  # F(x) + x
        out = self.relu(out)

        return out

9.2 Bottleneck Block

class Bottleneck(nn.Module):
    """用于 ResNet-50、ResNet-101、ResNet-152 的 Bottleneck Block"""
    expansion = 4  # 输出通道数 = 中间通道数 * 4

    def __init__(self, in_channels, mid_channels, stride=1, downsample=None):
        super(Bottleneck, self).__init__()
        out_channels = mid_channels * self.expansion

        # 1x1 Conv: 通道压缩 (Squeeze)
        self.conv1 = nn.Conv2d(
            in_channels, mid_channels, kernel_size=1, bias=False
        )
        self.bn1 = nn.BatchNorm2d(mid_channels)

        # 3x3 Conv: 空间处理 (可通过 stride 实现 downsampling)
        self.conv2 = nn.Conv2d(
            mid_channels, mid_channels, kernel_size=3,
            stride=stride, padding=1, bias=False
        )
        self.bn2 = nn.BatchNorm2d(mid_channels)

        # 1x1 Conv: 通道恢复 (Expand)
        self.conv3 = nn.Conv2d(
            mid_channels, out_channels, kernel_size=1, bias=False
        )
        self.bn3 = nn.BatchNorm2d(out_channels)

        self.relu = nn.ReLU(inplace=True)
        self.downsample = downsample

    def forward(self, x):
        identity = x

        # 1x1 → BN → ReLU
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        # 3x3 → BN → ReLU
        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        # 1x1 → BN
        out = self.conv3(out)
        out = self.bn3(out)

        # Shortcut Connection
        if self.downsample is not None:
            identity = self.downsample(x)

        out += identity  # F(x) + x
        out = self.relu(out)

        return out

9.3 完整 ResNet 模型

class ResNet(nn.Module):
    def __init__(self, block, layers, num_classes=1000):
        """
        Args:
            block: BasicBlock 或 Bottleneck
            layers: 各 stage 的 block 数量 [conv2_x, conv3_x, conv4_x, conv5_x]
            num_classes: 分类类别数
        """
        super(ResNet, self).__init__()
        self.in_channels = 64

        # conv1: 7x7, stride 2
        self.conv1 = nn.Conv2d(
            3, 64, kernel_size=7, stride=2, padding=3, bias=False
        )
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        # conv2_x ~ conv5_x
        self.layer1 = self._make_layer(block, 64, layers[0], stride=1)
        self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
        self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
        self.layer4 = self._make_layer(block, 512, layers[3], stride=2)

        # Classification Head
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512 * block.expansion, num_classes)

        # He Initialization
        self._initialize_weights()

    def _make_layer(self, block, mid_channels, num_blocks, stride):
        downsample = None
        out_channels = mid_channels * block.expansion

        # 第一个 block 需要 downsampling 时
        if stride != 1 or self.in_channels != out_channels:
            downsample = nn.Sequential(
                nn.Conv2d(
                    self.in_channels, out_channels,
                    kernel_size=1, stride=stride, bias=False
                ),
                nn.BatchNorm2d(out_channels),
            )

        layers = []
        layers.append(block(self.in_channels, mid_channels, stride, downsample))
        self.in_channels = out_channels

        for _ in range(1, num_blocks):
            layers.append(block(self.in_channels, mid_channels))

        return nn.Sequential(*layers)

    def _initialize_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                # He Initialization (fan_out mode)
                nn.init.kaiming_normal_(
                    m.weight, mode='fan_out', nonlinearity='relu'
                )
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)

    def forward(self, x):
        # Stem
        x = self.conv1(x)       # 224x224 → 112x112
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)     # 112x112 → 56x56

        # Residual Stages
        x = self.layer1(x)      # 56x56
        x = self.layer2(x)      # 28x28
        x = self.layer3(x)      # 14x14
        x = self.layer4(x)      # 7x7

        # Classification Head
        x = self.avgpool(x)     # 1x1
        x = torch.flatten(x, 1)
        x = self.fc(x)

        return x


# 模型创建函数
def resnet18(num_classes=1000):
    return ResNet(BasicBlock, [2, 2, 2, 2], num_classes)

def resnet34(num_classes=1000):
    return ResNet(BasicBlock, [3, 4, 6, 3], num_classes)

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

def resnet101(num_classes=1000):
    return ResNet(Bottleneck, [3, 4, 23, 3], num_classes)

def resnet152(num_classes=1000):
    return ResNet(Bottleneck, [3, 8, 36, 3], num_classes)

9.4 使用示例

# 创建 ResNet-50 并执行 Forward Pass
model = resnet50(num_classes=1000)
x = torch.randn(1, 3, 224, 224)  # Batch=1, RGB, 224x224
output = model(x)
print(f"Output shape: {output.shape}")  # torch.Size([1, 1000])

# 确认参数数量
total_params = sum(p.numel() for p in model.parameters())
print(f"Total parameters: {total_params:,}")
# ResNet-50: 约 25,557,032 (25.6M)

# 使用 PyTorch 官方 Pre-trained 模型
import torchvision.models as models
resnet50_pretrained = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)

10. Pre-activation ResNet: Identity Mappings in Deep Residual Networks

10.1 后续论文的动机

ResNet 论文发表后不久,同一批作者(He et al.)在 2016 年的 ECCV 上发表了"Identity Mappings in Deep Residual Networks"。该论文重新调整了 Residual Block 内部的运算顺序,证明了这样能更有效地训练更深的网络(1001 层)。

10.2 Original vs Pre-activation

Original ResNet(Post-activation)

xl+1=ReLU(F(xl)+xl)\mathbf{x}_{l+1} = \text{ReLU}(\mathcal{F}(\mathbf{x}_l) + \mathbf{x}_l)

在这种结构中,ReLU 位于 Addition 之后,因此经过 Shortcut 路径的信号也会受到 ReLU 的影响。这使得 Identity Mapping 不再是完全的恒等映射,从而削弱了 Gradient Highway 的效果。

Pre-activation ResNet

xl+1=xl+F(BN(ReLU(xl)))\mathbf{x}_{l+1} = \mathbf{x}_l + \mathcal{F}(\text{BN}(\text{ReLU}(\mathbf{x}_l)))

将运算顺序调整为BN → ReLU → Conv → BN → ReLU → Conv。这样一来,Shortcut 路径就成为纯粹的 Identity Mapping

10.3 结构对比

[Original ResNet]                [Pre-activation ResNet]
Input ──┐                        Input ──┐
        │                                │
     Conv                             BN
        │                                │
      BN                             ReLU
        │                                │
     ReLU                            Conv
        │                                │
     Conv                             BN
        │                                │
      BN                             ReLU

   (+) ←┘ shortcut                   Conv
        │                                │
     ReLU                           (+) ←┘ shortcut
        │                                │
     Output                          Output

10.4 数学上的优势

Pre-activation 结构下的 Forward Propagation 为:

xL=xl+i=lL1F(xi)\mathbf{x}_L = \mathbf{x}_l + \sum_{i=l}^{L-1} \mathcal{F}(\mathbf{x}_i)

Backward Propagation 为:

Lxl=LxL(1+xli=lL1F(xi))\frac{\partial \mathcal{L}}{\partial \mathbf{x}_l} = \frac{\partial \mathcal{L}}{\partial \mathbf{x}_L} \cdot \left(1 + \frac{\partial}{\partial \mathbf{x}_l} \sum_{i=l}^{L-1} \mathcal{F}(\mathbf{x}_i)\right)

由于 Shortcut 是纯粹的 Identity,常数项 1 得以精确保留。而在 Original ResNet 中,由于 ReLU 的介入,这个常数项并不精确等于 1。

10.5 实验结果

ModelCIFAR-10 Error (%)CIFAR-100 Error (%)
ResNet-110 (original)6.43-
ResNet-1001 (original)~7.61-
ResNet-1001 (pre-activation)4.6222.71

Pre-activation 结构在极深网络(1001 层)上相较原始结构展现出了显著的性能提升。这一结果用实验证实了:纯粹的 Identity Mapping 对 Gradient Flow 起着决定性的作用。

10.6 PyTorch 实现

class PreActBasicBlock(nn.Module):
    """Pre-activation Basic Block (BN → ReLU → Conv)"""
    expansion = 1

    def __init__(self, in_channels, out_channels, stride=1, downsample=None):
        super(PreActBasicBlock, self).__init__()
        self.bn1 = nn.BatchNorm2d(in_channels)
        self.relu = nn.ReLU(inplace=True)
        self.conv1 = nn.Conv2d(
            in_channels, out_channels, kernel_size=3,
            stride=stride, padding=1, bias=False
        )
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(
            out_channels, out_channels, kernel_size=3,
            stride=1, padding=1, bias=False
        )
        self.downsample = downsample

    def forward(self, x):
        identity = x

        # Pre-activation: BN → ReLU → Conv
        out = self.bn1(x)
        out = self.relu(out)

        if self.downsample is not None:
            identity = self.downsample(out)

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

        out += identity
        return out

11. ResNet 的影响与后续研究

ResNet 提出的 Residual Learning 范式,此后成为了无数架构创新的基础。来看几项主要的后续研究。

11.1 ResNeXt (2017)

"Aggregated Residual Transformations for Deep Neural Networks" — Xie et al., Facebook AI Research

ResNeXt 在 ResNet 的 Residual Block 中引入了一个新维度——Cardinality(分组数)。在一个 block 内并行执行多条变换路径,再将结果相加。

F(x)=i=1CTi(x)\mathcal{F}(\mathbf{x}) = \sum_{i=1}^{C} \mathcal{T}_i(\mathbf{x})

其中 CC 为 Cardinality(例如 32),Ti\mathcal{T}_i 是各路径的变换。在实现上通过Grouped Convolution高效处理。

ResNeXt-101 (32x4d) 在与 ResNet-101 相同计算量下取得了更高的准确率,这说明相比 Width(通道数)和 Depth(层数),Cardinality 是更有效的一个维度。

11.2 DenseNet (2017)

"Densely Connected Convolutional Networks" — Huang et al., Cornell/Facebook

DenseNet 是把 Residual Connection 推向极致所得到的架构:每一层都与之前的所有层直接相连。与 ResNet 使用 Element-wise Addition 不同,DenseNet 使用Channel-wise Concatenation

xl=Hl([x0,x1,...,xl1])\mathbf{x}_l = \mathcal{H}_l([\mathbf{x}_0, \mathbf{x}_1, ..., \mathbf{x}_{l-1}])

这一结构最大化了 Feature Reuse,并提升了参数效率。DenseNet-121 用比 ResNet-50 更少的参数达成了相近的性能。

11.3 SENet (2018)

"Squeeze-and-Excitation Networks" — Hu et al., Momenta

SENet 在 Residual Block 中加入了用于建模通道间关系的 SE Module,通过学习各通道的重要度来重新调整权重。

s=σ(W2ReLU(W1GAP(x)))\mathbf{s} = \sigma(\mathbf{W}_2 \cdot \text{ReLU}(\mathbf{W}_1 \cdot \text{GAP}(\mathbf{x}))) x~=sx\tilde{\mathbf{x}} = \mathbf{s} \odot \mathbf{x}

其中 GAP 为 Global Average Pooling,σ\sigma 为 Sigmoid,\odot 为 Channel-wise Multiplication。SENet 在 ILSVRC 2017 Classification 上以 Top-5 Error 2.251%夺得第一名。

11.4 EfficientNet (2019)

"EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks" — Tan & Le, Google Brain

EfficientNet 提出了 Compound Scaling 方法,同时、均衡地缩放网络的 Width、Depth、Resolution。它以 MBConv(Mobile Inverted Bottleneck)block 为基础,而这一 block 同样使用了 Residual Connection。

depth:d=αϕ,width:w=βϕ,resolution:r=γϕ\text{depth}: d = \alpha^\phi, \quad \text{width}: w = \beta^\phi, \quad \text{resolution}: r = \gamma^\phi s.t.αβ2γ22\text{s.t.} \quad \alpha \cdot \beta^2 \cdot \gamma^2 \approx 2

EfficientNet-B7 用比 ResNet 少 8.4 倍的参数,达成了当时最高水平的 ImageNet 准确率。

11.5 ConvNeXt (2022)

"A ConvNet for the 2020s" — Liu et al., Facebook AI Research

ConvNeXt 把 Vision Transformer(ViT)的设计原则应用到 CNN 上,打造出一个"现代化的 ResNet"。该研究以 ResNet-50 为起点,依次施加了如下改动:

  1. 训练方案现代化(300 epoch、AdamW、Mixup、Cutmix 等)
  2. Stage Ratio 变更:(3, 4, 6, 3) → (3, 3, 9, 3)
  3. "Patchify" Stem:7x7 Conv → 4x4 Conv, stride 4
  4. ResNeXt 式的 Grouped Convolution
  5. Inverted Bottleneck
  6. 大卷积核尺寸:3x3 → 7x7 Depthwise Conv
  7. 激活函数:ReLU → GELU
  8. 归一化方式:BN → Layer Normalization

ConvNeXt-T 达成了与 Swin-T 相当的性能(82.1% top-1 accuracy),证明了纯 CNN 架构依然能够与 Transformer 竞争。这项研究再次确认了 ResNet 的设计是多么坚实的基础。


12. 现代架构中的 Residual Connection

12.1 Transformer 中的 Skip Connection

Vaswani et al. 在"Attention Is All You Need"(2017)中提出的 Transformer 架构,在所有 Sub-layer 中都使用了 Residual Connection

Output=LayerNorm(x+SubLayer(x))\text{Output} = \text{LayerNorm}(\mathbf{x} + \text{SubLayer}(\mathbf{x}))

其中 SubLayer 是 Multi-Head Attention 或 Feed-Forward Network。基于与 ResNet 中被证明的相同原理,若没有这一 Residual Connection,训练深层 Transformer 几乎是不可能的。

12.2 Pre-LayerNorm 与 Post-LayerNorm

在 Transformer 中,也存在与 ResNet 的 Pre-activation vs Post-activation 类似的争论。

Post-LayerNorm(原始 Transformer):

xl+1=LN(xl+SubLayer(xl))\mathbf{x}_{l+1} = \text{LN}(\mathbf{x}_l + \text{SubLayer}(\mathbf{x}_l))

Pre-LayerNorm(GPT-2 以后的标准做法):

xl+1=xl+SubLayer(LN(xl))\mathbf{x}_{l+1} = \mathbf{x}_l + \text{SubLayer}(\text{LN}(\mathbf{x}_l))

Pre-LayerNorm 与 ResNet 的 Pre-activation 遵循同样的原理,通过让 Shortcut 路径保持纯粹的 Identity 来改善 Gradient Flow。GPT-2、GPT-3 等大多数最新的 Large Language Model 都采用 Pre-LayerNorm。

12.3 Diffusion Model 中的 Residual Connection

Denoising Diffusion Probabilistic Model(DDPM)的 U-Net 架构中,每个 Residual Block 同样使用了 Skip Connection。U-Net 中 Encoder-Decoder 之间的 Long Skip Connection,与 block 内部的 Residual Skip Connection 相结合,让不同尺度的 Feature 得到了有效利用。

12.4 Vision Transformer (ViT)

ViT(Vision Transformer)把图像切分为 16x16 的 patch,再输入 Transformer Encoder。每个 Transformer block 自然都使用了 Residual Connection,若没有它,训练 12 层以上的 ViT 会非常困难。

12.5 核心教训

ResNet 留下的最重要遗产,并非某一具体架构,而是Residual Connection 这一设计原则。这一原则可以概括如下:

  1. 把 Identity Mapping 设为默认值:即便网络什么都学不到,也至少要能把输入原样传递下去。
  2. 确保 Gradient Highway:要让 Gradient 能够从 Loss 直接流向每一层,铺出这样一条通路。
  3. 深度是一种自由度:有了 Residual Connection,增加网络深度总是有利,或至少不会带来损失。

无论是 CNN、Transformer、Diffusion Model 还是 State Space Model,这一原则都普遍适用,不受架构种类的限制。


13. 局限与批评

13.1 Feature Reuse 的低效性

根据 Veit et al.(2016)"Residual Networks Behave Like Ensembles of Relatively Shallow Networks"的研究,ResNet 中的大部分层实际上是通过非常短的路径(Shallow Path)来传递信息,而经过很深路径的贡献微乎其微。这对"152 层是否被高效利用"提出了疑问。

13.2 Feature Map 的 Element-wise Addition

DenseNet 的作者主张,ResNet 的 Element-wise Addition 可能会造成信息损失,基于 Concatenation 的 DenseNet 能够实现更高效的 Feature Reuse。不过 Concatenation 存在内存占用急剧增加的问题。

13.3 Computational Overhead

虽然 Global Average Pooling 与 Bottleneck 结构削减了参数量,但极深的 ResNet(如 ResNet-152)实际推理速度并不一定比 VGGNet 更快。Memory Access Cost 与 Sequential Dependency 可能会成为实质性的瓶颈。


14. 总结

ResNet 是深度学习史上最重要的论文之一,其贡献可以概括如下:

  1. 发现并定义了 Degradation 问题:清晰地阐明了深层网络中 Training Error 上升的现象。

  2. Residual Learning Framework:通过 F(x)+x\mathcal{F}(\mathbf{x}) + \mathbf{x} 结构显式引入 Identity Mapping,成功训练出了数百层的网络。

  3. Gradient Highway 理论:给出了 Skip Connection 直接传播 Gradient 的数学机制。

  4. Bottleneck 结构:借助 1x1 Convolution 的通道压缩/恢复,同时兼顾了深度与效率。

  5. 压倒性的实验结果:在 ImageNet(3.57% top-5 error)、CIFAR-10、COCO Detection/Segmentation 上全面超越了此前所有方法。

  6. 通用的设计原则:Residual Connection 已成为 Transformer、Diffusion Model 等现代深度学习所有主要架构中不可或缺的组成部分。

一次简单的加法运算(+x+ \mathbf{x})突破了深度学习的深度极限,并为此后十年的 AI 发展奠定了基础。ResNet 是"最简单的想法往往最强大"这一道理的典型例证。


15. 参考资料

  1. He, K., Zhang, X., Ren, S., & Sun, J. (2016). Deep Residual Learning for Image Recognition. CVPR 2016. arXiv:1512.03385

  2. He, K., Zhang, X., Ren, S., & Sun, J. (2016). Identity Mappings in Deep Residual Networks. ECCV 2016. arXiv:1603.05027

  3. He, K., Zhang, X., Ren, S., & Sun, J. (2015). Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification. ICCV 2015. arXiv:1502.01852

  4. Simonyan, K., & Zisserman, A. (2014). Very Deep Convolutional Networks for Large-Scale Image Recognition. ICLR 2015. arXiv:1409.1556

  5. Szegedy, C., et al. (2015). Going Deeper with Convolutions. CVPR 2015. arXiv:1409.4842

  6. Ioffe, S., & Szegedy, C. (2015). Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift. ICML 2015. arXiv:1502.03167

  7. Xie, S., et al. (2017). Aggregated Residual Transformations for Deep Neural Networks. CVPR 2017. arXiv:1611.05431

  8. Huang, G., et al. (2017). Densely Connected Convolutional Networks. CVPR 2017. arXiv:1608.06993

  9. Hu, J., et al. (2018). Squeeze-and-Excitation Networks. CVPR 2018. arXiv:1709.01507

  10. Tan, M., & Le, Q. (2019). EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks. ICML 2019. arXiv:1905.11946

  11. Liu, Z., et al. (2022). A ConvNet for the 2020s. CVPR 2022. arXiv:2201.03545

  12. Vaswani, A., et al. (2017). Attention Is All You Need. NeurIPS 2017. arXiv:1706.03762

  13. Veit, A., et al. (2016). Residual Networks Behave Like Ensembles of Relatively Shallow Networks. NeurIPS 2016. arXiv:1605.06431

  14. KaimingHe/deep-residual-networks. GitHub Repository

  15. ILSVRC 2015 Results. ImageNet Challenge

현재 단락 (1/531)

"Deep Residual Learning for Image Recognition"是 2015 年由 Microsoft Research 的 Kaiming He、Xiangyu Zhan...

작성 글자: 0원문 글자: 26,035작성 단락: 0/531