查看全课大纲(第 12 / 40 节)
- 1.从规则到学习:深度学习解决什么问题
- 2.表示学习与端到端训练
- 3.张量、计算图与自动微分
- 4.训练三要素:数据、模型、目标函数
- 5.从感知机到多层感知机
- 6.反向传播:手推一个两层网络
- 7.激活函数与初始化
- 8.优化器:SGD → Momentum → Adam
- 9.正则化:权重衰减、Dropout、BN/LN
- 10.卷积的直觉与参数共享
- 11.池化、感受野与下采样
- 12.经典网络演进:LeNet → AlexNet → VGG → ResNet
- 13.残差连接与归一化为什么有效
- 14.迁移学习与特征可视化
- 15.RNN 与 BPTT
- 16.LSTM/GRU:门控如何缓解梯度问题
- 17.Seq2Seq 与编码器-解码器
- 18.注意力前史:Bahdanau 加性注意力
- 19.Self-Attention:QKV 与缩放点积
- 20.多头注意力:并行的多种关系
- 21.位置编码:正弦与可学习
- 22.残差、LayerNorm 与前馈层
- 23.从零实现一个最小 Transformer block
- 24.训练稳定性、复杂度与常见误区
- 25.自监督与语言建模目标
- 26.BERT:掩码语言模型与双向编码
- 27.GPT:自回归生成与缩放定律
- 28.指令微调与对齐:SFT → RLHF/DPO
- 29.参数高效微调:LoRA 与适配器
- 30.自编码器与 VAE
- 31.GAN:对抗训练与模式崩塌
- 32.扩散模型:前向加噪与反向去噪
- 33.扩散模型的应用与采样加速
- 34.对比学习与 CLIP
- 35.ViT:把图像当序列
- 36.多模态架构:对齐、融合与生成
- 37.能力边界、幻觉与评测
- 38.PyTorch 2.x 训练循环与混合精度
- 39.实验管理、评测与复现
- 40.推理部署与成本:量化/蒸馏概览
经典网络演进:LeNet → AlexNet → VGG → ResNet
卷积网络如何越变越深?从 LeNet 到 ResNet 的演进逻辑
小象实战讲义 · 深度学习基础:从神经网络到 Transformer
学完本节,你将能清晰地回答:为什么深度卷积网络会从 LeNet 的 5 层发展到 ResNet 的 152 层?这背后的核心设计思想(如小卷积核、残差连接)是如何解决网络加深带来的梯度消失和退化问题的?你将掌握 VGG、ResNet 等经典架构的关键模块,并能用 PyTorch 实现一个基础的残差块。
本节承接前两讲的「卷积的直觉与参数共享」和「池化、感受野与下采样」,把卷积、池化这些零件拼成完整的网络,再看它们如何一代代变深。后续章节将基于这些特征提取器,进入序列模型和 Transformer 的学习。
💡 核心导读
- 网络加深的困境:单纯堆叠层数会导致梯度消失和网络退化(训练误差不降反升),这是深度网络设计的核心挑战。
- VGG 的深度化策略:通过堆叠多个 3×3 小卷积核来替代大卷积核,在增加网络深度的同时,减少了参数量并增强了非线性。
- ResNet 的残差学习:核心创新是引入“恒等快捷连接”,让网络层学习输入与输出之间的残差,这使得训练极深的网络(如152层)成为可能。
- 演进脉络:LeNet(奠基)→ AlexNet(深度化实践)→ VGG(小卷积核堆叠)→ ResNet(残差学习),每一步都旨在构建更深、更易训练的网络。
🧭 前置知识
- 卷积操作、步长、填充的概念及输出形状计算。
- 激活函数(ReLU)与池化(MaxPooling)的作用。
- 基本的 PyTorch 张量操作和
nn.Module的用法。 - 链式求导法则(理解梯度反向传播即可,不展开矩阵求导)。
从 LeNet 到 AlexNet:深度化的初步尝试
LeNet-5(LeCun et al., 1998)是首个成功应用的卷积神经网络,用于手写数字识别。它总共只有 7 层、约 6 万个参数,结构极浅。然而,把它直接搬到更大规模、更复杂的图像数据集(如 ImageNet)上时,深度和宽度都远远不够。
AlexNet(Krizhevsky, Sutskever & Hinton, 2012, NIPS)在 LeNet 的基础上做出了多项关键改进,开启了深度卷积网络的时代:
- 使用 ReLU 激活函数:替代 Sigmoid,有效缓解了深层网络的梯度消失问题,加快了训练速度。
- 使用 Dropout:在全连接层随机丢弃部分神经元,减少了过拟合。
- 使用重叠最大池化:池化窗口(3×3)步长(2)小于窗口尺寸,产生重叠,提升了特征的丰富性。
- 局部响应归一化(LRN):在相邻的特征图通道间进行归一化,模拟神经元的侧抑制机制。但后续研究(如 VGG 论文)表明其提升有限且增加计算量,现代网络已较少使用。
- 数据增强与多 GPU 训练:首次大规模使用 GPU 并行训练,并采用裁剪、翻转等数据增强技术。
AlexNet 的成功证明了增加网络深度和宽度对提升性能的有效性,但其结构仍有优化空间,例如第一层使用了较大的 11×11 卷积核。
VGGNet:小卷积核的深度堆叠
VGGNet(Simonyan & Zisserman, 2014,arXiv:1409.1556)的核心思想非常简洁:使用更小的卷积核(3×3),通过堆叠更多的卷积层来构建更深的网络。
为什么是 3×3 卷积核?
- 相同的感受野:两个堆叠的 3×3 卷积层,其有效感受野相当于一个 5×5 卷积层;三个堆叠的 3×3 卷积层,其有效感受野相当于一个 7×7 卷积层。
- 更少的参数:假设输入和输出通道数均为
C。- 一个 7×7 卷积层的参数量为:。
- 三个 3×3 卷积层的参数量为:。
- 参数量减少了约 45%。
- 更多的非线性:每个卷积层后都跟随一个 ReLU 激活函数,堆叠更多层引入了更多的非线性变换,使模型的判别能力更强。
VGG 网络配置
VGG 论文探索了从 11 层(A)到 19 层(E)的不同配置。最常用的是 VGG-16(配置 D)和 VGG-19(配置 E)。其结构具有高度一致性:卷积层均为 3×3,填充为 1(保持空间尺寸);池化层均为 2×2 最大池化,步长为 2(尺寸减半);通道数在每次池化后翻倍,但翻到 512 就封顶(64→128→256→512→512)。
下面我们用 PyTorch 实现一个简化的 VGG 块,感受其“堆叠”的设计:
import torch
import torch.nn as nn
torch.manual_seed(0) # 固定随机种子,保证结果可复现
class VGGBlock(nn.Module):
"""一个简单的 VGG 块,包含两个 3x3 卷积 + ReLU"""
def __init__(self, in_channels, out_channels):
super().__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1)
self.relu1 = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
self.relu2 = nn.ReLU(inplace=True)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
def forward(self, x):
x = self.conv1(x)
x = self.relu1(x)
x = self.conv2(x)
x = self.relu2(x)
x = self.pool(x)
return x
# 模拟一个输入图像 (batch=2, channel=3, height=32, width=32);真实 VGG 用 224×224,
# 这里缩小尺寸只为在 CPU 上快速跑通形状变化
input_tensor = torch.randn(2, 3, 32, 32)
block = VGGBlock(in_channels=3, out_channels=64)
output_tensor = block(input_tensor)
print("输入形状:", tuple(input_tensor.shape))
print("VGG块输出形状:", tuple(output_tensor.shape))
print("参数量(该块):", sum(p.numel() for p in block.parameters()))输入形状: (2, 3, 32, 32)
VGG块输出形状: (2, 64, 16, 16)
参数量(该块): 38720从输出可以看到,经过一个 VGG 块(两次卷积+一次池化),特征图尺寸减半,通道数增加。通过堆叠多个这样的块,VGG 构建了非常深的网络。VGG-16 在 ILSVRC-2014 上拿到分类任务第二名、定位任务第一名(分类冠军是 GoogLeNet),但它结构极简、权重迁移性很好,至今仍常被当作骨干网络或特征提取器;代价是全连接层参数量巨大(VGG-16 约 1.38 亿参数,其中 1.24 亿在三个全连接层里),网络加深后训练也明显变难。
ResNet:残差学习破解深度训练难题
随着网络深度增加,一个反直觉的现象出现了:网络退化(degradation)。即更深的网络在训练集上的误差反而比更浅的网络更高,这并非过拟合导致。ResNet(He et al., 2015,arXiv:1512.03385) 通过引入残差学习(Residual Learning) 巧妙地解决了这一问题。
这里要区分两个常被混淆的概念。过拟合是训练误差低、验证误差高;退化是训练误差本身也随深度增加而升高——网络不是学多了,而是学不动了。原论文的对照实验很说明问题:在 CIFAR-10 上,56 层网络的训练误差反而高于 20 层网络;若只是过拟合,更深的网络训练误差不该更差。更关键的是,这个 56 层网络并非没有能力表示 20 层的映射——把多出来的 36 层设成恒等映射,它至少能追平 20 层。所以问题不在容量,而在优化:深堆叠的非线性让“学出恒等映射”这件事本身都变得困难。
从反向传播看,这个解释很直接。把残差块写成 ,展开到第 层得到 ,于是
括号里的 1 就是那条恒等通路:无论中间多少层把梯度衰减到什么程度,梯度都能沿这条通路几乎无损地传回去。普通网络没有这个 1,梯度只能靠一串雅可比矩阵连乘,很容易趋近于 0。
残差块与恒等快捷连接
核心思想:不再让堆叠的层直接学习目标映射 ,而是学习残差映射 。那么原始映射就变为 。
在实现上,这通过“恒等快捷连接(Identity Shortcut)”完成:将块的输入 直接加到块的输出 上。这要求 的输出形状与 相同。
假设:如果最优的映射接近恒等映射(即 ),那么让网络学习残差 比学习一个恒等映射更容易。
一个基础的残差块(针对特征图尺寸不变的情况)实现如下:
import torch
import torch.nn as nn
torch.manual_seed(0)
class BasicBlock(nn.Module):
"""基础的残差块,适用于特征图尺寸不变的情况"""
def __init__(self, in_channels, out_channels):
super().__init__()
# 两个 3x3 卷积,不改变尺寸(padding=1)
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
# 快捷连接:如果输入输出通道数相同,则直接相加;否则需要 1x1 卷积进行投影
self.shortcut = nn.Sequential()
if in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False),
nn.BatchNorm2d(out_channels)
)
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)
# 关键步骤:添加快捷连接
out += self.shortcut(identity)
out = self.relu(out)
return out
def shortcut_desc(block):
"""用一句话描述快捷连接的类型,避免打印整段 repr 撑爆行宽。"""
if len(block.shortcut) == 0:
return "恒等映射(空 Sequential)"
return "1x1 卷积 + BN(通道投影)"
# 测试残差块
# 情况1:输入输出通道相同
x1 = torch.randn(2, 64, 56, 56)
block1 = BasicBlock(64, 64)
out1 = block1(x1)
print("情况1 - 输入输出通道相同")
print(" 输入形状:", tuple(x1.shape))
print(" 输出形状:", tuple(out1.shape))
print(" 快捷连接是:", shortcut_desc(block1))
# 情况2:输入输出通道不同
x2 = torch.randn(2, 64, 56, 56)
block2 = BasicBlock(64, 128)
# 注意:当通道数变化时,通常伴随下采样(stride=2)。这里为简化,仍保持尺寸。
# 实际 ResNet 中,下采样由第一个卷积的 stride=2 和 shortcut 中的 1x1 conv stride=2 完成。
out2 = block2(x2)
print("\n情况2 - 输入输出通道不同")
print(" 输入形状:", tuple(x2.shape))
print(" 输出形状:", tuple(out2.shape))
print(" 快捷连接是:", shortcut_desc(block2))情况1 - 输入输出通道相同
输入形状: (2, 64, 56, 56)
输出形状: (2, 64, 56, 56)
快捷连接是: 恒等映射(空 Sequential)
情况2 - 输入输出通道不同
输入形状: (2, 64, 56, 56)
输出形状: (2, 128, 56, 56)
快捷连接是: 1x1 卷积 + BN(通道投影)Bottleneck:让 152 层还算得动
BasicBlock 是两层 3×3,适合 ResNet-18/34。到 50 层以上,ResNet 改用 bottleneck 结构:1×1 降维 → 3×3 → 1×1 升维。设输入输出通道都是 256、中间压到 64,参数量约为 万;若直接用两个 3×3(256→256),则是 万,差了一个数量级。1×1 卷积在这里不是“看邻域”,而是跨通道的信息重排与降维。
两个常被忽略的细节
一是快捷连接的两种形态:通道与尺寸都一致时用恒等映射(不引入参数);通道翻倍或下采样时用 1×1 卷积 + BN 做投影,保证相加时形状对齐——下面第二个代码块演示的就是后者。二是原版 ResNet 的残差块是 post-activation(Conv→BN→ReLU 之后再相加);后来的 pre-activation 版本把 BN/ReLU 移到卷积之前(He et al., 2016,arXiv:1603.05027),恒等通路更干净、梯度更好传,深层网络更容易训。
残差网络的优势
- 解决梯度消失:反向传播时,梯度可以通过快捷连接直接传回更早的层,避免了在多层非线性变换中逐渐消失。
- 缓解网络退化:即使堆叠的层没有学到有用的特征,残差块至少能执行恒等映射,保证性能不会差于浅层网络。
- 实现极深网络:基于残差块,ResNet 成功训练了 152 层的网络。原论文里 ResNet-152 单模型的 ImageNet top-5 错误率是 4.49%,把多个深度的残差网络集成起来后达到 3.57%,拿下 ILSVRC 2015 分类任务第一名。
📝 动手练一练
问题:请编写代码,构造一个包含两个 BasicBlock 的迷你 ResNet 第一阶段。输入为 (2, 3, 32, 32) 的张量,首先经过一个 nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3) 和 nn.MaxPool2d(kernel_size=3, stride=2, padding=1),然后通过两个 BasicBlock(64, 64)。计算最终输出的形状,并统计整个迷你网络的可训练参数量。
👉 点击查看参考答案
思路:按照描述顺序组合网络层。第一个卷积和池化用于快速下采样。两个 BasicBlock 保持通道数和尺寸不变。参数量用 sum(p.numel() for p in model.parameters()) 手动累加即可,不必引入额外依赖。
import torch
import torch.nn as nn
torch.manual_seed(0)
# 复用前面定义的 BasicBlock
class BasicBlock(nn.Module):
def __init__(self, in_channels, out_channels):
super().__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
self.shortcut = nn.Sequential()
if in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False),
nn.BatchNorm2d(out_channels)
)
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)
out += self.shortcut(identity)
out = self.relu(out)
return out
class MiniResNetStage1(nn.Module):
def __init__(self):
super().__init__()
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)
self.block1 = BasicBlock(64, 64)
self.block2 = BasicBlock(64, 64)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.block1(x)
x = self.block2(x)
return x
# 构造网络和输入
model = MiniResNetStage1()
input_tensor = torch.randn(2, 3, 32, 32)
output_tensor = model(input_tensor)
print("输入形状:", tuple(input_tensor.shape))
print("输出形状:", tuple(output_tensor.shape))
# 计算参数量
total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print("可训练参数量:", total_params)输入形状: (2, 3, 32, 32)
输出形状: (2, 64, 8, 8)
可训练参数量: 157504解释:输入先经 stride=2 的 7×7 卷积(32 → 16),再经 stride=2 的最大池化(16 → 8);两个 BasicBlock 保持 8×8 的尺寸不变。参数量集中在第一层卷积(3×64×7×7)和两个残差块里的 3×3 卷积,与输入尺寸无关——正因为如此,同一个网络结构才能直接套用到 224×224 的真实图像上(那时输出是 56×56,参数量完全一样)。
常见错误
| 错误理解或写法 | 现象或报错 | 原因 | 正确做法 |
|---|---|---|---|
在残差块中忘记添加快捷连接,直接返回 F(x)。 | 网络性能下降,深度增加时训练困难,退化问题重现。 | 失去了残差学习“恒等映射”的保障,网络退化为普通的深层网络。 | 确保在 forward 中执行 out = F(x) + shortcut(x)。 |
当快捷连接需要改变通道数或下采样时,仅使用恒等连接(shortcut 为空)。 | 运行时错误:RuntimeError: The size of tensor a (64) must match the size of tensor b (128) ...。 | 张量形状不匹配,无法进行逐元素相加。 | 通过 1x1 卷积(必要时 stride=2)将 identity 投影到与 F(x) 相同的形状。 |
| 将 Batch Normalization 层放在残差相加之后。 | 可能影响训练稳定性,与原始 ResNet 论文的实现不一致。 | 原始 ResNet 设计中,每个卷积后立即进行 BN 和 ReLU,相加后再进行一次 ReLU。 | 遵循 Conv -> BN -> ReLU -> Conv -> BN -> ADD -> ReLU 的顺序。 |
本章小结
- AlexNet 通过 ReLU、Dropout 和数据增强等技巧,首次证明了深度卷积网络在大规模图像识别上的巨大潜力。
- VGGNet 确立了使用堆叠的 3×3 小卷积核来构建深度网络的范式,在增加深度的同时控制了参数量。
- ResNet 提出的残差学习和恒等快捷连接,是训练极深度网络的关键突破,解决了梯度消失和网络退化问题。
- 网络演进的核心逻辑是:在增加深度以提升模型容量的同时,必须通过精巧的设计(小卷积核、残差块)来保证优化的可行性和稳定性。
📋 行动清单
- 在代码中实现一个
BasicBlock,并尝试将其中的两个 3x3 卷积替换为三个 3x3 卷积,观察参数量和输出形状的变化。 - 修改
MiniResNetStage1,在第一个BasicBlock中进行下采样(将stride设为 2),并调整快捷连接使其匹配,验证输出形状。 - (选做)查阅
torchvision.models.resnet18的源码,对比其BasicBlock实现与本节示例的异同。
延伸阅读
Gradient-Based Learning Applied to Document Recognition(LeCun et al., 1998, Proceedings of the IEEE)—— LeNet-5 原始论文 - https://doi.org/10.1109/5.726791ImageNet Classification with Deep Convolutional Neural Networks(Krizhevsky, Sutskever & Hinton, 2012, NIPS)—— AlexNet 原始论文 - https://papers.nips.cc/paper_files/paper/2012/hash/c399862d3b9d6b76c8436e924a68c45b-Abstract.htmlVery Deep Convolutional Networks for Large-Scale Image Recognition(Simonyan & Zisserman, 2014, arXiv:1409.1556)—— VGG 原始论文 - https://arxiv.org/abs/1409.1556Deep Residual Learning for Image Recognition(He et al., 2015, arXiv:1512.03385)—— ResNet 原始论文 - https://arxiv.org/abs/1512.03385
—— 小象教研组