ResNet与残差网络

深度网络的梯度畅通之道
深度学习 卷积神经网络 ResNet 残差学习 计算机视觉

核心主题: ResNet残差网络原理、架构与工程实现

主要内容: 退化问题分析、残差学习理论、残差块实现(BasicBlock/Bottleneck)、ResNet系列架构、经典变体(PreActResNet/WideResNet/ResNeXt/DenseNet)、残差连接作用机制

关键词: ResNet, 残差连接, Skip Connection, 退化问题, Bottleneck, DenseNet, ResNeXt, 恒等映射, 梯度消失

一、概述

ResNet(Residual Network,残差网络)由何恺明(Kaiming He)等人在2015年提出,以论文《Deep Residual Learning for Image Recognition》正式发表。该网络在当年的ILSVRC(ImageNet Large Scale Visual Recognition Challenge)中以3.57%的Top-5错误率夺冠,这一成绩甚至超过了人类辨识水平的约5%错误率。ResNet的提出是深度学习发展史上的里程碑式事件——它从根本上解决了非常深的网络难以训练的问题,使得上百层乃至上千层的深度网络成为可能。

在ResNet出现之前,VGGNet已经证明了网络深度对模型性能的重要性,但VGG最多只能堆叠到19层。GoogleNet(Inception)通过"网中网"结构达到了22层,但进一步加深时同样面临训练困难。ResNet的核心突破在于提出了残差学习(Residual Learning)框架,通过引入短路连接(Skip Connection),让网络可以轻松扩展到152层甚至1000层以上,同时保持梯度顺畅传播。

ResNet的历史意义

  • ILSVRC 2015冠军:Top-5错误率 3.57%,超越人类水平
  • 里程碑式创新:残差学习框架改变了深度网络的构建范式
  • 广泛影响:残差连接思想渗透到几乎所有深度学习领域(CV、NLP、语音等)
  • 学术认可:论文引用量超过20万次,是深度学习领域引用量最高的论文之一
  • 实用价值:ResNet系列成为计算机视觉任务的标准骨干网络

如今,残差连接已成为深度神经网络的事实标准组件。无论是Transformer中的残差连接、扩散模型中的跳跃连接,还是GAN中生成器和判别器的架构,都能看到残差连接思想的身影。深入理解ResNet,就是理解现代深度学习架构设计的基石。

二、退化问题

2.1 深度增加的悖论

在深度学习理论中,我们通常认为增加网络深度可以提高模型表达能力,从而获得更好的性能。然而,实验发现了一个令人困惑的现象:当网络深度增加到一定程度后,训练精度和测试精度同时出现下降。下图描述了这一现象——对于一个20层的普通网络和一个56层的普通网络,后者在训练集和测试集上的误差均高于前者,而非我们期望的"深层网络至少不差于浅层网络"。

退化问题的定义

退化(Degradation)是指随着网络深度增加,模型精度先饱和后迅速下降的现象。这并不是过拟合(因为训练误差也同步增大),也不是梯度消失/爆炸(因为实验中使用了BN层和合理的初始化),而是一种深层网络优化困难的体现。

2.2 退化 vs 过拟合 vs 梯度消失

退化问题与常见的深度学习问题有着本质区别,需要清晰辨析:

现象 训练误差 验证误差 根本原因
过拟合 模型过度记忆训练数据
梯度消失 反向传播梯度指数级衰减
退化 高(偏高) 高(同步偏高) 深层网络优化困难,恒等映射难以直接学习

关键区别在于:过拟合时训练误差低而验证误差高,但退化问题时训练误差和验证误差同时升高——网络层数增多反而让模型"学不动了"。这说明退化并非统计泛化问题,而是优化问题

2.3 退化问题的原因分析

那么,为什么深层网络难以优化?何恺明在论文中给出了精辟的分析:

"Consider a shallower architecture and its deeper counterpart that adds more layers onto it. There exists a solution by construction to the deeper model: the added layers are identity mapping, and the other layers are copied from the learned shallower model. The existence of this constructed solution indicates that a deeper model should produce no higher training error than its shallower counterpart. But experiments show that our current solvers are unable to find solutions that are comparably good or better than the constructed solution."

核心论点:对于一个浅层网络,我们总可以在其后面添加若干恒等映射层(Identity Mapping)来构造一个深层网络,且该深层网络至少不差于浅层网络。这意味着深层网络的"理论上界"是优于或等于浅层网络的。但实践中,SGD等优化器难以直接学到恒等映射——多层非线性堆叠后,网络参数稍偏离恒等映射就会造成信号扭曲,梯度信号被干扰,最终导致训练困难。换句话说,不是深层网络"表达能力不够",而是"优化器找不到最优解"。

退化问题的启示

  • 深层网络的解空间包含浅层网络(通过恒等映射),因此理论上界更高
  • 但SGD等一阶优化器无法有效利用这一性质
  • 根本阻碍在于:多层非线性层难以精准逼近恒等映射
  • 解决方案:显式地让网络学习残差,而非直接学习恒等映射

三、残差学习

3.1 核心思想

残差学习的出发点非常直观:与其让多层非线性层直接拟合期望的映射 H(x),不如让它们拟合残差映射 F(x) = H(x) - x。这样,原本的映射 H(x) 就被重新表述为 F(x) + x,其中 x 通过短路连接(Skip Connection)直接传递。

H(x) = F(x) + x    ⟺    F(x) = H(x) - x

残差学习的关键洞察在于:当最优映射接近恒等映射时,学习残差比学习完整映射容易得多。如果最优解就是恒等映射 H(x) = x,那么普通网络需要学习出恒等映射函数(在多层非线性层中非常困难),而残差网络只需要让 F(x) 趋向于0(通过将权重趋向于0即可轻松实现)。退一步说,即便最优解偏离恒等映射,学习一个围绕恒等映射的扰动(残差)也远比从头学习完整映射容易

3.2 恒等映射(Identity Mapping)

恒等映射是指输入等于输出的映射关系:y = x。在ResNet中,短路连接实现了恒等映射——它将输入 x 直接传送到残差块的输出端,与残差映射 F(x) 相加。这一简单操作带来了巨大的好处:

3.3 残差映射 F(x) = H(x) - x

残差映射是残差块中可学习部分要拟合的目标。令 H(x) 表示期望的底层映射(desired underlying mapping),即网络在这一层应该学到的理想函数。常规网络直接让堆叠的非线性层去拟合 H(x),而残差网络转而让它们拟合:

F(x) = H(x) - x

这样设计的好处体现在数学上:

  1. 求导便利:∂H/∂x = ∂F/∂x + 1,梯度中多了一个常数项"1",确保梯度不会消失
  2. 参数高效:F(x) 相对于 H(x) 通常有更小的幅值和变化范围,参数更容易学习
  3. 初始化友好:将权重初始化为接近0的小值时,F(x) ≈ 0,整个残差块初始状态接近恒等映射

3.4 短路连接(Skip Connection)

短路连接是残差学习的物理实现。形式上,残差块可以表示为:

# 残差块的数学形式 # y = F(x, {W_i}) + x # 其中: # x —— 输入 # F(x, {W_i}) —— 残差映射(可学习的堆叠层) # + x —— 短路连接,逐元素相加 # y —— 输出(经过ReLU激活后)

当输入和输出的维度不匹配时(例如跨层降采样导致特征图尺寸变化),需要在短路连接上做维度适配:

y = F(x, {W_i}) + W_s · x

其中 W_s 是一个线性投影(通常用1x1卷积实现),用于将 x 的维度变换到与 F(x) 匹配。

短路连接的三大作用

  • 梯度高速公路:反向传播时,梯度可以不经过任何权重层直接回传到网络前端
  • 前向信息保护:前向传播时,底层信息可以通过短路连接无损传递到高层
  • 优化简化:将复杂映射分解为"恒等 + 残差",极大降低学习难度

四、残差块实现

4.1 BasicBlock(基础残差块)

BasicBlock 是 ResNet 中最基本的残差单元,由两个 3x3 卷积层组成,主用于浅层网络(ResNet18 和 ResNet34)。每个卷积层后跟 Batch Normalization 和 ReLU 激活函数。结构可表示为:

input → [3x3 Conv, BN, ReLU] → [3x3 Conv, BN] → + input → ReLU → output

以下是 PyTorch 的完整实现:

import torch import torch.nn as nn import torch.nn.functional as F class BasicBlock(nn.Module): """ResNet基础残差块(用于ResNet18/34)""" expansion = 1 # 输出通道数相对于输入通道数的倍数 def __init__(self, in_channels, out_channels, stride=1, downsample=None): super().__init__() # 第一个3x3卷积:可能进行降采样(stride != 1) self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(out_channels) # 第二个3x3卷积:保持空间尺寸和通道数 self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(out_channels) # 降采样模块:用于适配短路连接的维度 self.downsample = downsample def forward(self, x): identity = x # 保存输入作为恒等映射 # 残差路径 out = self.conv1(x) out = self.bn1(out) out = F.relu(out, inplace=True) out = self.conv2(out) out = self.bn2(out) # 维度适配(如果需要) if self.downsample is not None: identity = self.downsample(x) # 残差连接:恒等映射 + 残差映射 out += identity out = F.relu(out, inplace=True) return out

4.2 Bottleneck(瓶颈残差块)

Bottleneck 是更深层ResNet(ResNet50/101/152)使用的残差块结构,核心设计是"降维-卷积-升维"的瓶颈结构,有效降低了参数量和计算量。由三个卷积层组成:

input → [1x1 Conv, BN, ReLU] ↓通道  →  [3x3 Conv, BN, ReLU]  →  [1x1 Conv, BN] ↑通道  →  + input → ReLU → output

设计思路:先用 1x1 卷积将高维输入压缩到较低维度(如256→64),用 3x3 卷积在低维空间做特征提取,最后用 1x1 卷积恢复维度(64→256)。3x3卷积在低维空间计算,大幅减少了参数量

class Bottleneck(nn.Module): """Bottleneck残差块(用于ResNet50/101/152)""" # 输出通道相对于输入通道的扩展倍数 # 例如 in_channels=256, Bottleneck输出为 256*expansion = 1024 expansion = 4 def __init__(self, in_channels, out_channels, stride=1, downsample=None): super().__init__() # 瓶颈中间通道数 = out_channels(非 expansion 后) mid_channels = out_channels # 1x1 降维:in_channels -> mid_channels self.conv1 = nn.Conv2d(in_channels, mid_channels, kernel_size=1, stride=1, bias=False) self.bn1 = nn.BatchNorm2d(mid_channels) # 3x3 特征提取:stride 控制空间降采样 self.conv2 = nn.Conv2d(mid_channels, mid_channels, kernel_size=3, stride=stride, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(mid_channels) # 1x1 升维:mid_channels -> out_channels * expansion self.conv3 = nn.Conv2d(mid_channels, out_channels * self.expansion, kernel_size=1, stride=1, bias=False) self.bn3 = nn.BatchNorm2d(out_channels * self.expansion) self.downsample = downsample def forward(self, x): identity = x # 降维 → 特征提取 → 升维 out = F.relu(self.bn1(self.conv1(x)), inplace=True) out = F.relu(self.bn2(self.conv2(out)), inplace=True) out = self.bn3(self.conv3(out)) if self.downsample is not None: identity = self.downsample(x) out += identity out = F.relu(out, inplace=True) return out

4.3 1x1卷积的作用

在Bottleneck和维度变换场景下,1x1卷积扮演了关键角色,其作用包括:

1x1卷积的三大功能

  • 通道升降维:通过控制卷积核数量,灵活调整特征图的通道数,是"瓶颈"设计的关键组件
  • 跨通道信息融合:1x1卷积在空间维度上不操作,但在通道维度上进行全连接式的线性组合,促进不同通道间的信息交互
  • 降采样通路适配:当短路连接需要维度变换时,用 1x1 卷积(stride可调)实现快捷投影 W_s·x

4.4 BN + ReLU的顺序

关于 Batch Normalization 和 ReLU 的顺序,ResNet原始论文采用 Conv → BN → ReLU → Conv → BN 的排列,这与常规设计中 Conv → BN → ReLU 一致。但后续研究表明,Pre-Activation 变体(BN → ReLU → Conv)可以进一步提升性能(详见第六节)。两者对比如下:

# ===== 原始ResNet(Post-Activation)===== # Conv → BN → ReLU → Conv → BN → Add → ReLU class PostActBlock(nn.Module): def __init__(self, ...): self.conv1 = nn.Conv2d(...) self.bn1 = nn.BatchNorm2d(...) self.conv2 = nn.Conv2d(...) self.bn2 = nn.BatchNorm2d(...) def forward(self, x): identity = x out = F.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) out += identity out = F.relu(out) return out # ===== Pre-Activation变体 ===== # BN → ReLU → Conv → BN → ReLU → Conv → Add class PreActBlock(nn.Module): def __init__(self, ...): self.bn1 = nn.BatchNorm2d(...) self.conv1 = nn.Conv2d(...) self.bn2 = nn.BatchNorm2d(...) self.conv2 = nn.Conv2d(...) def forward(self, x): identity = x out = self.bn1(x) out = F.relu(out) out = self.conv1(out) out = self.bn2(out) out = F.relu(out) out = self.conv2(out) out += identity # 注意:Add之后不再跟ReLU(否则会破坏恒等映射的非负性) return out

五、ResNet系列架构

5.1 系列对比总览

ResNet 家族包含多种不同深度的变体,最常用的有 ResNet18、ResNet34、ResNet50、ResNet101 和 ResNet152。它们的核心区别在于残差块的类型和数量。下表展示了各型号的完整架构对比:

层名 输出尺寸 ResNet18 ResNet34 ResNet50 ResNet101 ResNet152
conv1 112×112 7×7, 64, stride 2
56×56 3×3 max pool, stride 2
conv2_x 56×56 [3×3, 64] ×2 [3×3, 64] ×3 [1×1, 64; 3×3, 64; 1×1, 256] ×3 [1×1, 64; 3×3, 64; 1×1, 256] ×3 [1×1, 64; 3×3, 64; 1×1, 256] ×3
conv3_x 28×28 [3×3, 128] ×2 [3×3, 128] ×4 [1×1, 128; 3×3, 128; 1×1, 512] ×4 [1×1, 128; 3×3, 128; 1×1, 512] ×4 [1×1, 128; 3×3, 128; 1×1, 512] ×8
conv4_x 14×14 [3×3, 256] ×2 [3×3, 256] ×6 [1×1, 256; 3×3, 256; 1×1, 1024] ×6 [1×1, 256; 3×3, 256; 1×1, 1024] ×23 [1×1, 256; 3×3, 256; 1×1, 1024] ×36
conv5_x 7×7 [3×3, 512] ×2 [3×3, 512] ×3 [1×1, 512; 3×3, 512; 1×1, 2048] ×3 [1×1, 512; 3×3, 512; 1×1, 2048] ×3 [1×1, 512; 3×3, 512; 1×1, 2048] ×3
1×1 Global Average Pooling, 1000-d FC, Softmax
FLOPs (×10⁹) 1.8 3.6 3.8 7.6 11.3
参数量 11.17M 21.29M 25.56M 44.55M 60.19M

5.2 Bottleneck 设计的优势

Bottleneck 设计是 ResNet50 及以上深度网络的精髓所在。为什么 50 层的 ResNet50 参数量(25.56M)仅略多于 34 层的 ResNet34(21.29M),但深度和性能却大幅提升?关键就在于 Bottleneck 的参数效率

# BasicBlock 的参数量(假设输入输出通道均为256) # conv1: 3*3*256*256 = 589,824 # conv2: 3*3*256*256 = 589,824 # 总计: ~1.18M # Bottleneck 的参数量(假设输入=256,输出通道=256) # conv1 (1x1 降维): 1*1*256*64 = 16,384 # conv2 (3x3 特征): 3*3*64*64 = 36,864 # conv3 (1x1 升维): 1*1*64*256 = 16,384 # 总计: ~69K (仅为 BasicBlock 的 1/17!)

在同样的计算预算下,Bottleneck可以使用更深的网络,同时在中间的低维空间(瓶颈层)进行复杂特征变换。这就是"窄而深"比"宽而浅"更高效的原因——深层网络可以用更少的参数实现更强的非线性表达能力。

5.3 完整的 ResNet50 实现

基于上述构建块,以下是完整的 ResNet50 网络结构实现:

class ResNet(nn.Module): """通用ResNet框架,支持多种深度配置""" def __init__(self, block, layers, num_classes=1000): super().__init__() self.in_channels = 64 # 初始卷积层:7x7大卷积核快速降采样 self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False) self.bn1 = nn.BatchNorm2d(64) self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) # 四个残差阶段,每个阶段包含多个残差块 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) # 全局平均池化 + 全连接分类头 self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) self.fc = nn.Linear(512 * block.expansion, num_classes) # 权重初始化 for m in self.modules(): if isinstance(m, nn.Conv2d): 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 _make_layer(self, block, out_channels, num_blocks, stride): """创建一个包含多个残差块的阶段""" downsample = None # 当尺寸或通道变化时,需要降采样短路连接 if stride != 1 or self.in_channels != out_channels * block.expansion: downsample = nn.Sequential( nn.Conv2d(self.in_channels, out_channels * block.expansion, kernel_size=1, stride=stride, bias=False), nn.BatchNorm2d(out_channels * block.expansion), ) layers = [] # 当前阶段的第一个残差块可能执行降采样 layers.append(block(self.in_channels, out_channels, stride, downsample)) self.in_channels = out_channels * block.expansion # 后续残差块保持空间尺寸和通道数 for _ in range(1, num_blocks): layers.append(block(self.in_channels, out_channels)) return nn.Sequential(*layers) def forward(self, x): x = self.maxpool(F.relu(self.bn1(self.conv1(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 # ===== 不同深度的ResNet构造函数 ===== def resnet18(): return ResNet(BasicBlock, [2, 2, 2, 2]) def resnet34(): return ResNet(BasicBlock, [3, 4, 6, 3]) def resnet50(): return ResNet(Bottleneck, [3, 4, 6, 3]) def resnet101(): return ResNet(Bottleneck, [3, 4, 23, 3]) def resnet152(): return ResNet(Bottleneck, [3, 8, 36, 3])

六、ResNet变体

ResNet 的成功催生了大量后续改进工作,以下是最具影响力的几个变体:

6.1 PreActResNet(预激活残差网络)

何恺明团队在后续论文《Identity Mappings in Deep Residual Networks》中系统分析了残差块的激活顺序,提出了 Pre-Activation 结构。核心改进是:将 BN 和 ReLU 放在卷积层之前,而非之后。

PreActResNet 的优势

  • 恒等映射更"干净":Add 路径上不再经过 ReLU(ReLU会过滤负值,破坏信息完整性),梯度可以无损传播
  • 正则化效果:Pre-activation 将 BN 置于权重之前,起到了隐式正则化的作用
  • 训练更稳定:在超深网络(1000层以上)中表现出更好的训练稳定性
  • 一致提升:在 CIFAR-10/100 等数据集上,PreActResNet 比原始ResNet一致提升 1-2%

6.2 WideResNet(宽度残差网络)

Sergey Zagoruyko 在2016年提出 WideResNet,质疑了"深度至上"的设计理念。其核心发现是:增加残差块的宽度(通道数)比增加深度更高效

WideResNet 的设计要点

  • 宽度因子 k:将每个残差块的通道数乘以 k 倍(通常 k=2, 5, 10)
  • 降低深度:使用较少的残差块(如 28 层)配合较大的宽度
  • Dropout 引入:在每个残差块的最后添加 Dropout 层防止过拟合
  • 性能收益:WRN-28-10(28层, k=10)在 CIFAR-10 上达到 3.8% 错误率,超越当时的 ResNet-1001
class WideBasicBlock(nn.Module): """WideResNet的加宽基础残差块""" def __init__(self, in_channels, out_channels, stride=1, dropout_rate=0.0): super().__init__() self.bn1 = nn.BatchNorm2d(in_channels) 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.dropout = nn.Dropout(dropout_rate) # 短路连接降采样 if stride != 1 or in_channels != out_channels: self.shortcut = nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False), ) else: self.shortcut = nn.Identity() def forward(self, x): out = self.conv1(F.relu(self.bn1(x))) out = self.dropout(out) out = self.conv2(F.relu(self.bn2(out))) return out + self.shortcut(x)

6.3 ResNeXt(分组卷积残差网络)

ResNeXt 是 ResNet 与 Inception 思想的融合之作,由谢赛宁和何恺明在 2017 年提出。其核心创新是引入了 分组卷积(Grouped Convolution)Cardinality(基数) 的概念。

Cardinality(基数)—— ResNeXt 的核心维度

传统上我们通过两个维度来设计网络:深度(Depth)宽度(Width)。ResNeXt 增加了第三个维度——基数(Cardinality),即一个残差块中并行路径的数量。

具体来说,ResNeXt 将残差块内部的 3x3 卷积替换为 C 组并行的分组卷积,每组独立计算特征,最后通过拼接(Concatenation)合并结果。实验表明:增大 Cardinality 比增大 Depth 或 Width 更有效地提升模型性能

  • ResNeXt-101(32×4d):32组分组卷积,每组4通道,在 ImageNet 上 Top-1 错误率 20.7%
  • 在相同参数量下,增大 Cardinality 比增大 Width 带来更大的精度提升
class ResNeXtBottleneck(nn.Module): """ResNeXt瓶颈块,引入分组卷积""" expansion = 2 # ResNeXt的expansion通常为2 def __init__(self, in_channels, out_channels, stride=1, cardinality=32, base_width=4): super().__init__() # 每组宽度 = base_width,总中间通道数 = cardinality * base_width mid_channels = cardinality * base_width # 1x1 降维(不分组) self.conv1 = nn.Conv2d(in_channels, mid_channels, kernel_size=1, bias=False) self.bn1 = nn.BatchNorm2d(mid_channels) # 3x3 分组卷积(核心创新) self.conv2 = nn.Conv2d(mid_channels, mid_channels, kernel_size=3, stride=stride, padding=1, groups=cardinality, bias=False) self.bn2 = nn.BatchNorm2d(mid_channels) # 1x1 升维 self.conv3 = nn.Conv2d(mid_channels, out_channels * self.expansion, kernel_size=1, bias=False) self.bn3 = nn.BatchNorm2d(out_channels * self.expansion) # 短路连接 if stride != 1 or in_channels != out_channels * self.expansion: self.shortcut = nn.Sequential( nn.Conv2d(in_channels, out_channels * self.expansion, kernel_size=1, stride=stride, bias=False), nn.BatchNorm2d(out_channels * self.expansion), ) else: self.shortcut = nn.Identity() def forward(self, x): out = F.relu(self.bn1(self.conv1(x))) out = F.relu(self.bn2(self.conv2(out))) out = self.bn3(self.conv3(out)) return F.relu(out + self.shortcut(x))

6.4 DenseNet(密集连接网络)

黄高等人(2017)提出了 DenseNet,将残差连接的思路推向极致:每一层的输入来自前面所有层的输出拼接。与ResNet的逐元素相加不同,DenseNet采用通道维度上的拼接(Concatenation)。

DenseNet vs ResNet

特性 ResNet DenseNet
连接方式 逐元素相加 (Add) 通道拼接 (Concat)
信息流 间接传递(需通过相加融合) 直接传递(所有前层特征可见)
参数效率 较高 极高(每层只学习少量特征图)
特征复用 隐式特征复用 显式特征复用(通过拼接)
显存消耗 较低 较高(需保存所有中间特征)
class DenseLayer(nn.Module): """DenseNet的密集连接层:BN-ReLU-Conv(1x1)-BN-ReLU-Conv(3x3)""" def __init__(self, in_channels, growth_rate=32): super().__init__() # 瓶颈层:先1x1降维(4*growth_rate),再3x3提取特征 self.bn1 = nn.BatchNorm2d(in_channels) self.conv1 = nn.Conv2d(in_channels, 4 * growth_rate, kernel_size=1, bias=False) self.bn2 = nn.BatchNorm2d(4 * growth_rate) self.conv2 = nn.Conv2d(4 * growth_rate, growth_rate, kernel_size=3, padding=1, bias=False) def forward(self, x): out = self.conv1(F.relu(self.bn1(x))) out = self.conv2(F.relu(self.bn2(out))) # 将新特征拼接到已有特征上(而非相加) return torch.cat([x, out], dim=1) # DenseNet的关键特性: # - growth_rate(增长率):每层新产生的特征图数量,通常32或48 # - 每层输入通道数 = in_channels + (layer_idx) * growth_rate # - 随着层数加深,输入通道数线性增长,因此需要Transition层压缩 # - Transition层:1x1 Conv降通道 + 2x2 AvgPool降空间尺寸

七、残差连接的作用机制

残差连接之所以如此强大,其背后涉及梯度传播、优化景观和模型集成等多方面的理论解释。以下从三个角度深入分析。

7.1 梯度流畅通(Gradient Flow)

这是残差连接最直观的作用。在反向传播中,设损失函数为 L,残差块的输出为 y = F(x) + x,则梯度可以通过链式法则计算:

∂L/∂x = ∂L/∂y · ∂y/∂x = ∂L/∂y · (∂F/∂x + 1)

梯度中多了一个常数项 "1",这一项保证了即使 ∂F/∂x 非常小(权重层梯度消失),梯度 ∂L/∂x 也可以通过短路连接中的 ∂L/∂y 直接传播到前层,不会衰减。在非常深的网络中,这一性质意味着:

7.2 隐式集成(Implicit Ensemble)

何恺明等人在后续研究中揭示了一个深刻洞察:ResNet 可以看作多个浅层网络的隐式集成。由于短路连接的存在,前向传播时信息可以选择"经过权重层"或"跳过权重层",这意味着网络中存在指数级数量的有效路径。

有效路径分析

  • 对于一个 n 个残差块的网络,共有 2ⁿ 条有效路径(每个残差块可选择"经过"或"跳过")
  • 实验发现:大部分路径的梯度贡献来自较短路径(仅经过少量残差块)
  • 非常长的路径虽然存在,但梯度贡献极小,几乎不参与训练
  • 因此,ResNet 本质上是在训练大量浅层网络的集成,而非一个单一的极深网络
  • 这解释了为什么 ResNet 对个别层失效具有鲁棒性——因为有大量替代路径

这一发现彻底改变了我们对残差网络的理解。ResNet 训练的不是一个"极深网络",而是大量浅层网络的隐式集成,每个子网络只使用全部残差块的一个子集。这种"集成"不需要额外的训练开销,是短路连接结构的自然副产品。

"ResNet behaves like an ensemble of relatively shallow networks, not a single deep network. The residual connections enable the training signal to flow through many different paths, and the effective depth of these paths is much shallower than the total depth." — Veit et al., 2016

7.3 前向传播保护

除了反向传播的梯度优势外,残差连接在前向传播中也起到了关键的保护作用。多层非线性变换可能导致信息在传递过程中逐渐失真或衰减,但短路连接提供了信息的高速公路

残差连接总结:一石三鸟

一个看似简单的跳过连接设计,同时解决了深度网络面临的三大挑战:

  1. 优化问题:通过恒等映射降低学习难度,让网络可以选择性学习"残差"
  2. 梯度问题:通过短路连接提供梯度直通车,消除梯度消失
  3. 退化问题:通过隐式集成机制,让深层网络发挥真正的深度优势

八、核心要点总结

九、进一步思考

残差连接看似简单,却深刻地改变了深度学习的面貌。回顾 ResNet 的发展历程,我们可以提炼出以下思考:

1. 简单的力量

ResNet 的核心创新——y = F(x) + x——只需要一行代码实现。这提醒我们,深度学习的重大突破有时并非来自复杂的数学推导,而是来自对问题本质的深刻洞察和简洁优雅的解决方案。

2. 恒等映射的普适性

残差连接的思想已渗透到几乎所有深度学习领域。以 Transformer 为例,每个自注意力层和前馈网络层之后都跟随着残差连接 + Layer Normalization。没有残差连接,Transformer 堆叠到 6 层以上就会难以训练,更不可能有今天的 GPT、BERT 等大模型。可以说,残差连接是为深度学习"解锁深度"的钥匙。

3. 设计空间的维度

从 ResNet 到 ResNeXt,我们看到网络设计空间从「深度、宽度」二维扩展到了「深度、宽度、基数」三维。这提示我们:当代深度学习架构设计正从"堆积木"走向"多维度的系统化设计"。搜索最优架构的自动化方法(如 NAS)也在这一框架下产生。

4. 工程实践建议

  • 在搭建深度网络时,始终使用残差连接——无论任务类型如何,残差连接带来的收益远大于其微小开销
  • 选择网络深度时,优先考虑 ResNet50 作为 backbone,它在性能和计算量之间取得了良好的平衡
  • 在资源受限场景,优先降低深度而非宽度——WideResNet 的结论说明宽度更影响性能
  • 处理小尺寸输入(如 CIFAR)时,移除初始的 7x7 卷积和 MaxPool,改用 3x3 卷积保持分辨率
  • 对于超深网络(超过 100 层),使用 Pre-Activation 结构以获得更稳定的梯度流
  • 使用 torchvision.models 中预训练的 ResNet 进行迁移学习,通常能取得最优效果