第五章 卷积神经网络
一、卷积原理
1.1 卷积操作
卷积 (Convolution)是卷积神经网络(CNN)的核心运算。对图像等高维数据,全连接层有几个根本问题:参数爆炸(224×224 图像展平后 5 万维,接到 1024 维隐藏层就有 5000 万参数)、无法利用平移不变性(猫在图像左上角或右下角都应是同一只猫)、丢失空间结构信息(像素之间的相对位置)。
卷积的两个关键设计:
- 局部连接(Local Connectivity):每个神经元只与输入的一个小区域(感受野,Receptive Field)相连,而不是全部输入。这模拟了「视觉皮层神经元只对视野中一小块区域敏感」的生物学事实。
- 参数共享(Parameter Sharing):同一个卷积核(滤波器)在输入的所有位置上使用同一组权重。这直接利用了「猫的检测器不需要为图像的每个位置学习一个独立版本」的平移不变性。
二维卷积 。设输入为 X∈RH×WX \in \mathbb{R}^{H \times W}X∈RH×W,卷积核为 K∈Rkh×kwK \in \mathbb{R}^{k_h \times k_w}K∈Rkh×kw,则在位置 (i,j)(i, j)(i,j) 的输出为:
Y(i,j)=∑u=0kh−1∑v=0kw−1X(i+u,j+v)⋅K(u,v) Y(i, j) = \sum_{u=0}^{k_h-1} \sum_{v=0}^{k_w-1} X(i+u, j+v) \cdot K(u, v) Y(i,j)=u=0∑kh−1v=0∑kw−1X(i+u,j+v)⋅K(u,v)
更一般地,深度方向上对所有输入通道求加权和:
Yc′(i,j)=∑c=1Cin∑u,vXc(i+u,j+v)⋅Kc,c′(u,v)+bc′ Y_{c'}(i, j) = \sum_{c=1}^{C_{in}} \sum_{u, v} X_c(i+u, j+v) \cdot K_{c, c'}(u, v) + b_{c'} Yc′(i,j)=c=1∑Cinu,v∑Xc(i+u,j+v)⋅Kc,c′(u,v)+bc′
其中 c′c'c′ 是输出通道索引。每个输出通道 c′c'c′ 对应一个卷积核 K:,c′∈RCin×kh×kwK_{:, c'} \in \mathbb{R}^{C_{in} \times k_h \times k_w}K:,c′∈RCin×kh×kw。
PyTorch 实现:
python
import torch
import torch.nn as nn
# 二维卷积
conv = nn.Conv2d(
in_channels=3, # 输入通道数(如 RGB 图像为 3)
out_channels=64, # 输出通道数(即卷积核个数)
kernel_size=3, # 卷积核尺寸 3x3,也可写 (3, 3)
stride=1, # 步幅
padding=1, # 填充
bias=True, # 是否使用偏置(默认 True)
)
x = torch.randn(8, 3, 224, 224) # (B, C, H, W)
y = conv(x) # (8, 64, 224, 224)
关键参数:
in_channels:输入通道数。对图像,RGB 为 3;对特征图,等于上一层的out_channels。out_channels:本层卷积核数量,也是输出通道数。kernel_size:卷积核边长,常用 1、3、5、7;3×3 是最常见的「甜点」尺寸。stride:步幅,默认 1;大于 1 时会下采样。padding:输入四周的零填充,默认 0;padding=1(对应 3×3 卷积)可保持输入输出尺寸一致。dilation:空洞卷积的膨胀率,默认 1;>1 时扩大感受野而不增加参数。groups:分组卷积,默认 1;= in_channels 时为 Depthwise 卷积。
卷积核尺寸的偏好。两个 3×3 卷积堆叠的感受野相当于一个 5×5,但参数更少(2 × 3² = 18 vs 5² = 25)、非线性更强(经过两次 ReLU)。VGG 论文系统比较了 3×3、5×5、7×7 组合,结论是「多层小卷积核」优于「单层大卷积核」。1×1 卷积虽不感受空间,但能跨通道做线性组合,常用于通道数调整(GoogLeNet、ResNet 的 Bottleneck)。
感受野(Receptive Field)。输出特征图上每个像素「看到」输入图像的面积。感受野随深度按乘法增长:两个 3×3 卷积堆叠,第二层每个像素的感受野是 5×5(对第一层输出),对应原始输入是 5×5。理解感受野对设计架构(确保深层神经元能「看到」足够大的区域)与诊断模型(为什么某个目标检测不到)都很重要。
反卷积(Transposed Convolution) 。又称转置卷积或分数步长卷积,常用于上采样(语义分割、生成模型)。它不是卷积的严格逆运算,而是把输入的每个元素与卷积核做加权求和后散布到更大的输出中。注意:反卷积的输出可能存在「棋盘格伪影」,现代实践中常被 Upsample + Conv 或 PixelShuffle 替代。
1.2 填充与步幅
填充(Padding)。在输入四周补零(或镜像、反射等其他策略),让卷积后输出尺寸可与输入相同,或精确控制到某个目标尺寸。
设输入 Hi×WiH_i \times W_iHi×Wi,卷积核 kh×kwk_h \times k_wkh×kw,步幅 sss,填充 ppp (单边宽度,总填充为 2p2p2p),则输出尺寸为:
Ho=⌊Hi+2p−khs⌋+1 H_{o} = \left\lfloor \frac{H_i + 2p - k_h}{s} \right\rfloor + 1 Ho=⌊sHi+2p−kh⌋+1
常见配置:
| 配置 | 效果 | 用途 |
|---|---|---|
kernel=3, padding=1, stride=1 |
尺寸不变 | 通用特征提取 |
kernel=5, padding=2, stride=1 |
尺寸不变 | 早期 CNN(LeNet、AlexNet) |
kernel=3, padding=0, stride=1 |
尺寸减小 2 | 缩小特征图 |
kernel=3, padding=1, stride=2 |
尺寸减半 | 下采样 |
kernel=1, padding=0, stride=1 |
尺寸不变,仅通道调整 | 1×1 卷积 |
为什么需要 padding=1 保持尺寸 。深层 CNN 堆叠几十层,若每层都缩小,几层后特征图就太小(224 → 222 → 220 → ...),且边缘信息被逐渐「磨掉」。保持尺寸 + 配合步幅下采样,是现代 CNN 的标准做法。
PyTorch 自动 padding 。nn.Conv2d 的 padding 参数接受整数(均匀填充)或元组(分别为 H/W 指定)。功能相同的 nn.ZeroPad2d 等模块可显式填充,但不常用。
步幅(Stride) 。卷积核在输入上每次移动的距离。stride > 1 等价于先卷积再下采样,比「卷积 + MaxPool」节省计算。在 ResNet 的 Bottleneck 中,降采样常用 stride=2 的 3×3 卷积实现。
相同填充(Same Padding)与有效填充(Valid Padding)。这两个术语常在论文里出现:
- Same Padding :填充后输出尺寸 = 输入尺寸(
padding = (k-1)/2当stride=1)。 - Valid Padding:不填充,输出尺寸 < 输入尺寸。
计算示例。输入 5×5,卷积核 3×3:
padding=0, stride=1:输出 3×3。padding=1, stride=1:输出 5×5(保持)。padding=0, stride=2:输出 2×2(不规则时按 floor 计算)。padding=1, stride=2:输出 3×3。
1.3 池化
**池化(Pooling)**是下采样手段之一,对特征图局部区域做汇聚,降低空间分辨率。早期 CNN 把它当作标配,现代架构中部分被「步幅卷积」替代。
最大池化(Max Pooling)。取局部区域的最大值:
text
输入 (单通道 4x4):
1 3 2 4
5 6 1 2
7 8 3 0
2 4 1 5
2x2 MaxPool, stride=2:
6 4
8 5
平均池化(Average Pooling)。取局部区域的平均值,平滑但容易模糊。
全局平均池化(Global Average Pooling, GAP) 。对每个通道整个特征图求平均,得到 CCC 维向量,直接用于分类。这是现代 CNN 取代全连接层的标配(Lin et al., 2013, NIN)。优点:
- 显著减少参数(全连接层经常占总参数的 80%+)。
- 强化特征图与类别置信度的对应关系(每个通道对应一个类别)。
- 对输入尺寸更鲁棒。
PyTorch:
python
max_pool = nn.MaxPool2d(kernel_size=2, stride=2) # 默认 stride=kernel_size
avg_pool = nn.AvgPool2d(kernel_size=2, stride=2)
gap = nn.AdaptiveAvgPool2d(1) # 输出固定为 1x1,与输入尺寸无关
自适应池化(Adaptive Pooling) 。nn.AdaptiveAvgPool2d(output_size) 根据目标输出尺寸自动计算 kernel 与 stride,无论输入多大都能得到目标尺寸。在不同尺寸输入的迁移学习或检测中非常有用。
池化 vs 步幅卷积。两者都能降低空间分辨率,效果类似但有差异:
- 池化是无参数的固定操作,不增加模型容量。
- 步幅卷积有可学习参数,能学到更复杂的下采样。
- 实践中:ResNet 之后,大多数架构用
stride=2的卷积替代 MaxPool ;但首层前的MaxPool2d(3, 2)(AlexNet 风格)仍常见。
为什么仍需要池化。即使有步幅卷积,池化仍有独特价值:
- 强化局部平移不变性(对小幅平移/噪声不敏感)。
- 显著降低计算量(尤其早期层)。
- 某些语义任务(显著性检测、传统特征)更友好。
二、经典网络
2.1 LeNet 与 AlexNet
LeNet-5(LeCun et al., 1998)是 CNN 的开山之作,用于手写数字识别(MNIST)。结构:
text
Input(32x32) → Conv(5x5, 6) → AvgPool → Conv(5x5, 16) → AvgPool → FC(120) → FC(84) → Output(10)
关键贡献:
- 提出「Conv → Pool → Conv → Pool → FC」的基本范式。
- 验证了「端到端学习」在手写数字识别上的可行性。
- 受限于当时算力(无 GPU),网络规模很小。
AlexNet(Krizhevsky et al., 2012)是深度学习时代的起点。在 ImageNet 2012 比赛上,以 15.3% top-5 错误率夺冠,远低于第二名的 26.2%。
关键创新:
- ReLU 激活取代 Sigmoid,缓解梯度消失,让更深网络可训练。
- Dropout 减少过拟合。
- 双 GPU 训练(当时单卡显存不够),这间接催生了现代分布式训练。
- 数据增强(Random Crop、Horizontal Flip、Color Jitter)。
- Local Response Normalization(LRN):现在已被 BN 取代。
- 使用了 5 个卷积层 + 3 个全连接层,约 6000 万参数。
python
import torchvision.models as models
alexnet = models.alexnet(weights='AlexNet_Weights.DEFAULT')
AlexNet 证明了「更深 + GPU + 大量数据」能让 CNN 显著超越传统方法,直接引发了 2012 年起的深度学习浪潮。
ZFNet(Zeiler & Fergus, 2013)是 AlexNet 的改进版,主要调整了超参数(第一层 11×11 卷积改为 7×7,步幅从 4 改为 2),并用可视化技术理解 CNN 内部表征,是「可解释性」早期工作。
2.2 VGG 与 ResNet
VGG(Simonyan & Zisserman, 2014, Visual Geometry Group, Oxford)系统研究了「深度」对 CNN 性能的影响,提出用「多层小卷积核堆叠」代替「大卷积核」。
核心设计:全部使用 3×3 卷积 + 2×2 MaxPool,堆叠 13-19 层。VGG-16 与 VGG-19 是最常用的版本。
贡献与影响:
- 证明「小卷积核深堆叠」效果优于「大卷积核浅堆叠」。
- 结构简单规整,易于理解和实现,成为后续很多工作的 baseline。
- 缺点:参数巨大(VGG-16 有 1.38 亿参数,大部分在全连接层),推理慢,显存占用大。
- 现代实践中已被 ResNet 等取代。
ResNet(He et al., 2015, Deep Residual Learning for Image Recognition, CVPR 2016 Best Paper)是 CNN 发展史上的关键转折,首次让「超百层」CNN 真正可训练,并横扫 ILSVRC 2015 全部赛道(分类、检测、定位、COCO 检测/分割)。
核心问题 :随着深度增加,「退化问题(degradation problem)」出现------更深网络的训练误差反而比浅层网络更高。这与过拟合不同(训练误差本身在增加),也不是普通的梯度消失(BN 已经在用)。它表明深层网络「难以优化」。
核心思想:残差连接 。把需要学习的映射 H(x)H(x)H(x) 改为 F(x)+xF(x) + xF(x)+x,其中 FFF 是堆叠的卷积层,+x+x+x 是恒等快捷连接(identity shortcut):
y=F(x,{Wi})+x y = F(x, \{W_i\}) + x y=F(x,{Wi})+x
- 若最优 HHH 接近恒等映射,网络只需让 F→0F \to 0F→0------比「让 H→xH \to xH→x」容易得多(因为 F=0F=0F=0 是 L2 正则化的自然吸引点)。
- 反向传播时,梯度 ∂L∂x\frac{\partial \mathcal{L}}{\partial x}∂x∂L 至少包含来自 shortcut 的 ∂L∂y\frac{\partial \mathcal{L}}{\partial y}∂y∂L 一份(不会因为 FFF 的梯度消失而归零)。
结构。ResNet 仍是 4-stage backbone,每 stage 由若干 residual block 堆叠。每个 stage 第一个 block 做下采样(stride=2 + 通道翻倍),后续 block 保持尺寸。
两种 block:
- BasicBlock(用于 ResNet-18/34):两个 3×3 卷积。
- Bottleneck(用于 ResNet-50/101/152):1×1(降维)→ 3×3 → 1×1(升维),减少参数与计算。
主流 ResNet 变体:
| 模型 | Block 类型 | 堆叠数 | 层数 | 参数量 |
|---|---|---|---|---|
| ResNet-18 | BasicBlock | (2,2,2,2) | 18 | 11.7M |
| ResNet-34 | BasicBlock | (3,4,6,3) | 34 | 21.8M |
| ResNet-50 | Bottleneck | (3,4,6,3) | 50 | 25.6M |
| ResNet-101 | Bottleneck | (3,4,23,3) | 101 | 44.5M |
| ResNet-152 | Bottleneck | (3,8,36,3) | 152 | 60.2M |
ResNet-50 是「性价比」最高的选择------参数比 ResNet-152 少很多,效果接近。
ResNet 在 ImageNet 2015 上以 3.57% top-5 错误率(6 模型集成)夺冠,首次明确超越人类水平(约 5%)。
残差连接的深远影响 。残差连接不仅解决了 CNN 的深度问题,其「y=F(x)+xy = F(x) + xy=F(x)+x」公式几乎一字不差地迁移到了 Transformer 的每个子层(后接 LayerNorm):
y=LayerNorm(x+Sublayer(x)) y = \text{LayerNorm}(x + \text{Sublayer}(x)) y=LayerNorm(x+Sublayer(x))
没有 ResNet 的「优化可训练性」遗产,Transformer 的几十层到几百层堆叠根本无法实现。残差连接是现代深度学习架构的隐藏支柱。
预激活 ResNet(He et al., 2016, Identity Mappings in Deep Residual Networks)将 BN+ReLU 放在卷积之前(Conv → BN → ReLU 顺序变成 BN → ReLU → Conv),让 shortcut 路径「完全干净」(不被 ReLU 污染),支持 1001 层网络的稳定训练。
ResNet 之后的重要 CNN:
- ResNeXt(Xie et al., 2017):引入「分组卷积」与「cardinality」概念,在同等参数下表现优于 ResNet。
- Wide ResNet(Zagoruyko & Komodakis, 2016):证明「加宽」比「加深」更划算,50 层宽 ResNet 可超过 152 层窄 ResNet。
- DenseNet(Huang et al., 2017):每一层都连接到所有后续层,极致特征复用,但显存占用大。
- MobileNet / EfficientNet:面向移动端的高效 CNN(深度可分离卷积、神经架构搜索)。
- ConvNeXt(Liu et al., 2022):把 Swin Transformer 的设计思想回迁到纯 CNN,在 ImageNet 上达到与 Swin 相当的水平,证明「现代」CNN 仍可与 Transformer 比肩。
python
import torchvision.models as models
resnet50 = models.resnet50(weights='ResNet50_Weights.DEFAULT')
三、实践要点
3.1 迁移学习
迁移学习 (Transfer Learning)是现代深度学习的核心工程范式之一:在大数据集 (如 ImageNet)上预训练的模型,在目标任务上微调,通常能显著优于从零训练。
为什么有效。预训练模型学到了「通用的视觉特征」(边缘、纹理、部件),这些特征对大多数视觉任务都有用。微调只需让模型把通用特征适配到任务特定的语义上。
两种主要策略:
1. 特征提取(Feature Extraction)。冻结预训练 backbone,只训练新加的分类头:
python
import torchvision.models as models
import torch.nn as nn
backbone = models.resnet50(weights='ResNet50_Weights.DEFAULT')
for param in backbone.parameters():
param.requires_grad = False # 冻结 backbone
# 替换最后一层
backbone.fc = nn.Linear(backbone.fc.in_features, num_classes)
# backbone.fc 默认为可训练(requires_grad=True)
- 适用:数据极少(数百到数千样本)、任务与预训练数据相似。
- 优点:训练快,显存小,不易过拟合。
- 缺点:在数据量充足时效果上限低于微调全部参数。
2. 微调(Fine-Tuning)。解冻全部(或部分)参数,用较小的学习率在目标数据上继续训练:
python
backbone = models.resnet50(weights='ResNet50_Weights.DEFAULT')
# 全部参数默认 requires_grad=True,直接训练即可
backbone.fc = nn.Linear(backbone.fc.in_features, num_classes)
# 关键:对 backbone 用小学习率(如新分类头的 1/10)
optimizer = torch.optim.AdamW([
{'params': backbone.fc.parameters(), 'lr': 1e-3},
{'params': [p for n, p in backbone.named_parameters() if 'fc' not in n], 'lr': 1e-4},
], weight_decay=0.01)
- 适用:数据量中等(数千到数万)、任务与预训练数据有差异。
- 优点:上限更高。
- 缺点:训练慢,显存大,有破坏预训练特征的风险(学习率过大时)。
3. 渐进解冻(Progressive Unfreezing)。先冻结 backbone,训练分类头 → 解冻最后几个 stage,继续训练 → 解冻全部,微调。NLP 预训练(ULMFiT, Howard & Ruder, 2018)首先提出,在视觉中也常有效。
学习率的选择(微调的核心):
- 预训练模型参数:用小学习率(10−510^{-5}10−5 到 10−410^{-4}10−4),保留预训练知识。
- 新分类头:用大学习率(10−310^{-3}10−3 到 10−410^{-4}10−4),快速适配任务。
- 通用原则:学习率比从零训练小 10-100 倍。
数据量与策略对应:
| 目标数据量 | 推荐策略 |
|---|---|
| < 1000 张 | 特征提取(冻结 backbone) |
| 1000 - 10,000 | 微调顶层 + 渐进解冻 |
| > 10,000 | 全部微调 |
| > 100,000 | 考虑从零训练(若任务与预训练差异大) |
输入尺寸的匹配。预训练模型通常在 224×224 上训练。微调时如果用 384×384,有两种方式:
- 直接 resize 输入到 384×384(简单,但需要 backbone 能处理任何尺寸,全卷积网络 OK,带 FC 的需要 Global Average Pooling 替换 FC)。
- 保持 224×224,简单粗暴。
预处理必须匹配 。ImageNet 预训练模型用 mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] 做 Normalize。微调时必须用相同的统计量,否则模型会「看到」完全不同的输入分布,效果严重下降。若目标数据集与 ImageNet 差异大,可考虑用目标数据集本身的统计量重新 Normalize(但需要重新微调,不能直接用预训练权重)。
常用预训练模型库:
torchvision.models:ResNet、VGG、DenseNet、EfficientNet、ViT、Swin 等,集成度高,适合图像任务。timm(PyTorch Image Models):Ross Wightman 维护,200+ 架构,持续更新,是研究界事实标准。- Hugging Face
transformers:ViT、CLIP、Swin 等,适合多模态与 Transformer 架构。
保存与加载:
python
# 保存
torch.save({
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'epoch': epoch,
'best_acc': best_acc,
}, 'checkpoint.pth')
# 加载
checkpoint = torch.load('checkpoint.pth', map_location='cpu')
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
只保存权重(不保存优化器状态)更轻量,适合纯推理部署:torch.save(model.state_dict(), 'model.pth')。
迁移学习的「保命」检查清单:
- 确认输入预处理(mean/std、resize 尺寸)与预训练时一致。
- 确认最后一层输出维度匹配目标类别数。
- 用小学习率起步,密切观察 loss 曲线(若 loss 上升说明学习率过大)。
- 数据量小时,先冻结 backbone 训练分类头,再解冻。
- 监控验证集指标,早停(Early Stopping) 防止过拟合。
3.2 检测与分割(概念级)
本教程以图像分类为主线,但 CNN 的另外两大视觉任务------目标检测 与语义分割------值得建立概念级理解(细节超出本书范围):
目标检测(Object Detection) 。回答"图里有什么、在哪里":输出数量不定的 (类别, 边界框, 置信度) 集合。与分类的本质区别:输出不定长且含空间定位。
- 两阶段(Faster R-CNN 家族):先用区域建议网络(RPN)筛出"可能有物体"的候选框,再对每个候选做分类与框精修。准但慢。
- 单阶段(YOLO、SSD):一次前向直接在密集网格/锚点上回归框与类别。快,实时检测的主流。
- Transformer 路线(DETR, 2020):把检测建模为集合预测,用二分图匹配取代手工设计的锚点与后处理。
- 共同难点:正负样本极度不平衡(一张图的背景框远多于目标框)、分类与框回归的多任务权衡。
语义分割(Semantic Segmentation)。回答"每个像素属于什么类别":输出与输入同尺寸的逐像素类别图。
- 核心矛盾:分类骨干不断下采样(分辨率越来越低),而分割需要像素级输出。
- 编码器-解码器结构 :骨干当编码器,解码器逐级上采样恢复分辨率;跳跃连接(skip connection) 把编码器的高分辨率浅层特征直接传给解码器,补偿下采样丢失的空间细节。U-Net(Ronneberger et al., 2015)是这一结构的代表------它也是第八章扩散模型的主流骨干。
- 实例分割(Mask R-CNN 等)进一步区分"同一类别的不同个体"。
三任务对比:
| 任务 | 输出 | 代表模型 | 共享方式 |
|---|---|---|---|
| 分类 | 单标签 | ResNet、ViT | 骨干直接输出 |
| 检测 | 不定数量的框 + 类别 | YOLO、Faster R-CNN、DETR | 骨干 + 检测头 |
| 分割 | 逐像素类别图 | U-Net、DeepLab | 骨干 + 解码器 |
理解"预训练骨干 + 任务特定头"这一模式,就抓住了现代视觉的工程结构------这也是 §3.1 迁移学习如此重要的原因。