第三篇:计算机视觉核心技术#

篇章概述#

本篇深入讲解现代计算机视觉的核心技术,包括经典CNN架构、注意力机制、Transformer以及先进的训练技巧。这些技术是当今计算机视觉领域的基石。

本篇目标#

  1. 掌握现代CNN架构:ResNet、MobileNet、EfficientNet的设计思想和实现
  2. 理解注意力机制:从Self-Attention到Vision Transformer的演进
  3. 掌握训练技巧:数据增强、学习率调度、正则化等高级技术
  4. 实战能力:能够使用预训练模型进行迁移学习和fine-tuning

技术栈#

  • 框架: PyTorch 2.x
  • 模型库: torchvision.models, timm, transformers
  • 数据增强: albumentations
  • 工具: tensorboard, wandb(可选)

章节安排#

第5章:现代CNN架构#

深入讲解ResNet、MobileNet、EfficientNet等经典架构,理解残差连接、深度可分离卷积、复合缩放等核心概念。

核心内容:

  • ResNet残差连接解决梯度消失
  • MobileNet轻量化设计思想
  • EfficientNet复合缩放策略
  • 迁移学习与fine-tuning实战

实战项目: 使用ResNet50在自定义数据集上进行迁移学习

第6章:Attention与Transformer#

从注意力机制的基本原理出发,深入理解Transformer架构,并学习Vision Transformer(ViT)在图像领域的应用。

核心内容:

  • Self-Attention机制原理
  • Multi-Head Attention设计
  • Transformer架构详解
  • Vision Transformer(ViT)实现

实战项目: 使用ViT进行图像分类

第7章:数据增强与训练技巧#

掌握现代深度学习训练的各种技巧,包括数据增强、学习率调度、正则化等,构建高性能训练流程。

核心内容:

  • 传统数据增强:翻转、裁剪、色彩变换
  • 现代数据增强:Mixup、CutMix、AutoAugment
  • 学习率调度:Cosine Annealing、Warmup
  • 完整训练流程设计

实战项目: 构建生产级训练流程

学习路径#

第5章:现代CNN架构
理解残差连接、轻量化设计
第6章:Attention与Transformer
掌握注意力机制、ViT架构
第7章:数据增强与训练技巧
完整训练流程实战

环境准备#

# 安装核心依赖
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
pip install timm  # PyTorch Image Models
pip install transformers  # Hugging Face Transformers
pip install albumentations  # 数据增强
pip install tensorboard  # 可视化
pip install opencv-python pillow matplotlib

# 可选:实验追踪
pip install wandb

性能基准#

不同架构在ImageNet-1K上的性能对比(Top-1准确率):

模型参数量FLOPsTop-1准确率推理速度(GPU)
ResNet-5025.6M4.1G76.2%22ms
MobileNetV3-Large5.4M0.22G75.2%6ms
EfficientNet-B05.3M0.39G77.1%8ms
ViT-B/1686M17.6G81.8%45ms
ViT-L/16307M61.6G82.6%120ms

注: 推理速度基于NVIDIA V100 GPU, batch_size=1

核心概念速查#

残差连接(Residual Connection)#

F(x) = H(x) - x
H(x) = F(x) + x  # 输出 = 残差 + 输入

深度可分离卷积(Depthwise Separable Convolution)#

计算量: 标准卷积 vs 深度可分离卷积
标准: D_K × D_K × M × N × D_F × D_F
深度可分离: D_K × D_K × M × D_F × D_F + M × N × D_F × D_F
压缩比: 1/N + 1/D_K²

Self-Attention#

Attention(Q, K, V) = softmax(QK^T / √d_k) × V

实战项目概览#

项目1:ResNet迁移学习#

  • 数据集: 自定义分类数据集
  • 模型: ResNet-50(ImageNet预训练)
  • 技术: 冻结特征提取器、fine-tuning
  • 文件: chapter05/code/resnet_transfer_learning.py

项目2:ViT图像分类#

  • 数据集: CIFAR-10/自定义数据集
  • 模型: ViT-B/16(ImageNet-21K预训练)
  • 技术: Patch embedding、Position encoding
  • 文件: chapter06/code/vit_classification.py

项目3:高级训练流程#

  • 技术栈: PyTorch + albumentations
  • 功能:
    • 多种数据增强策略
    • 学习率调度(Cosine、Warmup)
    • 混合精度训练(AMP)
    • TensorBoard可视化
  • 文件: chapter07/code/advanced_training.py

最佳实践#

1. 模型选择#

  • 高精度需求: ViT-L、EfficientNet-B7
  • 速度优先: MobileNetV3、EfficientNet-B0
  • 平衡: ResNet-50、EfficientNet-B3

2. 迁移学习策略#

# 小数据集:冻结大部分层
for param in model.parameters():
    param.requires_grad = False
# 只训练分类头

# 中等数据集:冻结早期层
for name, param in model.named_parameters():
    if 'layer1' in name or 'layer2' in name:
        param.requires_grad = False

# 大数据集:全模型fine-tune
for param in model.parameters():
    param.requires_grad = True

3. 学习率设置#

# 迁移学习典型设置
lr_backbone = 1e-4  # 预训练层
lr_head = 1e-3      # 新增层

常见问题#

Q1: ResNet为什么能训练更深的网络?#

A: 残差连接通过恒等映射提供了梯度的直接通路,避免了梯度消失问题。即使某些层学习失败,网络至少可以保持恒等映射。

Q2: ViT为什么需要大量数据?#

A: ViT缺少CNN的归纳偏置(局部性、平移不变性),需要更多数据来学习这些特性。在ImageNet-1K上表现不如ResNet,但在ImageNet-21K上效果更好。

Q3: 如何选择数据增强策略?#

A:

  • 分类任务:RandAugment、AutoAugment
  • 检测任务:Mosaic、Mixup
  • 小数据集:强增强(AutoAugment)
  • 大数据集:基础增强即可

进阶资源#

论文必读#

  1. ResNet: Deep Residual Learning for Image Recognition (CVPR 2016)
  2. MobileNet: MobileNets: Efficient CNNs for Mobile Vision (2017)
  3. EfficientNet: EfficientNet: Rethinking Model Scaling (ICML 2019)
  4. ViT: An Image is Worth 16x16 Words: Transformers for Image Recognition (ICLR 2021)
  5. AutoAugment: AutoAugment: Learning Augmentation Policies (CVPR 2019)

代码仓库#

本篇总结#

通过本篇学习,你将:

  1. 理解现代CNN架构的演进逻辑
  2. 掌握注意力机制和Transformer在视觉领域的应用
  3. 学会使用各种训练技巧提升模型性能
  4. 具备完整的深度学习项目开发能力

下一篇将进入目标检测领域,学习YOLO、Faster R-CNN等经典检测算法。


第5章:现代CNN架构#

本章概述#

本章深入讲解现代CNN架构的设计思想,包括ResNet的残差连接、MobileNet的轻量化设计、EfficientNet的复合缩放策略,并通过迁移学习实战掌握这些模型的使用。

学习目标#

  • 理解ResNet残差连接如何解决梯度消失问题
  • 掌握MobileNet的深度可分离卷积和倒残差结构
  • 学习EfficientNet的复合缩放方法
  • 实战:使用预训练模型进行迁移学习

5.1 ResNet:残差连接革命#

核心问题:网络退化#

在ResNet之前,深度网络面临严重问题:

现象: 56层网络训练误差 > 20层网络训练误差
原因: 不是过拟合(训练误差更高),而是优化困难

退化问题示意:

浅层网络(20层) → 训练误差: 15%
深层网络(56层) → 训练误差: 20% (理论上应该≤15%)

残差连接(Residual Connection)#

核心思想: 学习残差而非直接学习目标映射

传统: H(x) = 目标映射
ResNet: F(x) = H(x) - x
        H(x) = F(x) + x

直观理解:

  • 如果恒等映射是最优的,网络只需学习F(x)=0
  • 比直接学习H(x)=x更容易

残差块(Residual Block)#

基础残差块:

输入x
3×3 Conv → BN → ReLU
3×3 Conv → BN
  + ← x (跳跃连接)
ReLU
输出

瓶颈残差块(Bottleneck):

输入x (256维)
1×1 Conv(64) → BN → ReLU  # 降维
3×3 Conv(64) → BN → ReLU  # 特征提取
1×1 Conv(256) → BN         # 升维
  + ← x
ReLU
输出(256维)

计算量对比:

  • 基础块: 3×3×256×256 × 2 = 1.18M
  • 瓶颈块: 1×1×256×64 + 3×3×64×64 + 1×1×64×256 = 69K
  • 压缩比: 17倍

ResNet架构族#

模型层数参数量FLOPsImageNet Top-1
ResNet-181811.7M1.8G69.8%
ResNet-343421.8M3.7G73.3%
ResNet-505025.6M4.1G76.2%
ResNet-10110144.5M7.8G77.4%
ResNet-15215260.2M11.6G78.3%

ResNet-50架构详解#

输入: 224×224×3
Conv1: 7×7, 64, stride=2 → 112×112×64
MaxPool: 3×3, stride=2 → 56×56×64
Layer1: [1×1,64; 3×3,64; 1×1,256] × 3 → 56×56×256
Layer2: [1×1,128; 3×3,128; 1×1,512] × 4 → 28×28×512
Layer3: [1×1,256; 3×3,256; 1×1,1024] × 6 → 14×14×1024
Layer4: [1×1,512; 3×3,512; 1×1,2048] × 3 → 7×7×2048
AvgPool: 7×7 → 1×1×2048
FC: 1000 classes

实现要点#

1. 维度匹配:

class ResidualBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride, 1)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, 1, 1)
        self.bn2 = nn.BatchNorm2d(out_channels)

        # 如果维度不匹配,使用1×1卷积调整
        self.shortcut = nn.Sequential()
        if stride != 1 or in_channels != out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, 1, stride),
                nn.BatchNorm2d(out_channels)
            )

    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out += self.shortcut(x)  # 残差连接
        out = F.relu(out)
        return out

2. 梯度流动:

反向传播:
∂L/∂x = ∂L/∂H × (∂F/∂x + I)
      = ∂L/∂H × ∂F/∂x + ∂L/∂H

关键: ∂L/∂H 直接传播,不经过中间层!

5.2 MobileNet:轻量化设计#

设计目标#

目标场景: 移动端、嵌入式设备 核心指标:

  • 模型大小 < 10MB
  • 推理速度 < 50ms (CPU)
  • 准确率损失 < 5%

深度可分离卷积(Depthwise Separable Convolution)#

标准卷积:

输入: H × W × M
卷积核: D_K × D_K × M × N
输出: H × W × N

计算量: D_K × D_K × M × N × H × W

深度可分离卷积 = Depthwise + Pointwise:

步骤1: Depthwise卷积:

输入: H × W × M
卷积核: D_K × D_K × 1 × M (每个通道独立卷积)
输出: H × W × M

计算量: D_K × D_K × M × H × W

步骤2: Pointwise卷积:

输入: H × W × M
卷积核: 1 × 1 × M × N
输出: H × W × N

计算量: M × N × H × W

总计算量:

深度可分离: D_K² × M × HW + M × N × HW
标准卷积: D_K² × M × N × HW

压缩比: (D_K² × M + M × N) / (D_K² × M × N)
       = 1/N + 1/D_K²

例: D_K=3, N=128
压缩比 = 1/128 + 1/9 ≈ 0.119 (压缩8.4倍)

MobileNetV1架构#

输入: 224×224×3
Conv: 3×3, 32, stride=2 → 112×112×32
DW: 3×3, 32
PW: 1×1, 64 → 112×112×64
DW: 3×3, 64, stride=2
PW: 1×1, 128 → 56×56×128
DW: 3×3, 128
PW: 1×1, 128 → 56×56×128
... (重复若干次)
AvgPool → 1×1×1024
FC: 1000

MobileNetV2:倒残差结构(Inverted Residual)#

设计思想: 低维→高维→低维

传统残差块: 高维 → 低维 → 高维
倒残差块: 低维 → 高维 → 低维

倒残差块结构:

输入: H × W × C (低维, 如32)
1×1 Conv, C×t (扩展, t=6) → H × W × C×6 (高维, 192)
3×3 DW Conv → H × W × C×6
1×1 Conv, C → H × W × C (压缩回低维)
+ ← 输入 (残差连接)

关键设计:

  1. 扩展因子 t=6: 在高维空间提取特征
  2. 线性瓶颈: 最后一层不使用ReLU(避免信息损失)
  3. 残差连接: 仅在输入输出维度相同时使用

MobileNetV3:神经架构搜索(NAS)#

改进点:

  1. SE模块(Squeeze-and-Excitation):
输入 → Global Pool → FC → ReLU → FC → Sigmoid → 乘回输入
  1. h-swish激活:
h-swish(x) = x × ReLU6(x + 3) / 6
优点: 比ReLU性能好,比swish计算快
  1. 优化首尾结构:
  • 首层: 减少卷积核数量
  • 尾层: 提前进行全局池化

性能对比#

模型参数量FLOPsTop-1(%)推理速度(CPU)
MobileNetV14.2M0.57G70.635ms
MobileNetV23.5M0.30G72.028ms
MobileNetV3-Large5.4M0.22G75.225ms
MobileNetV3-Small2.5M0.06G67.412ms

5.3 EfficientNet:复合缩放#

模型缩放的三个维度#

传统方法: 单独调整一个维度

  • 深度(Depth): 增加层数 (ResNet-50 → ResNet-101)
  • 宽度(Width): 增加通道数 (ResNet-50 → ResNet-50-Wide)
  • 分辨率(Resolution): 增大输入尺寸 (224×224 → 299×299)

复合缩放(Compound Scaling)#

核心思想: 平衡三个维度的缩放

深度: d = α^φ
宽度: w = β^φ
分辨率: r = γ^φ

约束: α × β² × γ² ≈ 2
      α ≥ 1, β ≥ 1, γ ≥ 1

参数解释:

  • φ: 缩放系数(用户指定)
  • α, β, γ: 每个维度的基础缩放因子(通过网格搜索)

为什么是β²和γ²:

  • FLOPs ∝ d × w² × r²
  • 约束条件确保FLOPs增长约为2^φ倍

EfficientNet架构#

基础架构(EfficientNet-B0):

StageOperatorResolutionChannelsLayers
1Conv3×3224×224321
2MBConv1, k3×3112×112161
3MBConv6, k3×3112×112242
4MBConv6, k5×556×56402
5MBConv6, k3×328×28803
6MBConv6, k5×514×141123
7MBConv6, k5×514×141924
8MBConv6, k3×37×73201
9Conv1×1, Pool, FC7×712801

MBConv: MobileNetV2倒残差块 + SE模块

EfficientNet系列#

缩放参数(α=1.2, β=1.1, γ=1.15):

模型φ深度宽度分辨率参数量FLOPsTop-1(%)
B001.01.02245.3M0.39G77.1
B111.21.12407.8M0.70G79.1
B221.41.22609.2M1.0G80.1
B331.61.330012M1.8G81.6
B441.91.438019M4.2G82.9
B552.31.545630M9.9G83.6
B662.81.652843M19G84.0
B773.31.760066M37G84.3

核心优势#

参数效率:

同样83%准确率:
- ResNet-152: 60M参数, 11.6G FLOPs
- EfficientNet-B3: 12M参数, 1.8G FLOPs
压缩比: 5倍参数, 6.4倍FLOPs

5.4 实战:迁移学习与Fine-tuning#

迁移学习策略#

场景分类:

数据集大小与预训练数据相似度策略
小(<1k)冻结所有层,只训练分类头
小(<1k)冻结早期层,微调后期层
中(1k-10k)微调后半部分网络
中(1k-10k)微调大部分网络
大(>10k)全网络微调,小学习率
大(>10k)全网络微调,正常学习率

实现步骤#

1. 加载预训练模型:

import torchvision.models as models

# 方式1: 直接加载
model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)

# 方式2: 使用timm(更多模型)
import timm
model = timm.create_model('resnet50', pretrained=True)

2. 冻结层:

# 冻结所有参数
for param in model.parameters():
    param.requires_grad = False

# 冻结特定层
for name, param in model.named_parameters():
    if 'layer1' in name or 'layer2' in name:
        param.requires_grad = False

3. 替换分类头:

# ResNet
num_classes = 10
model.fc = nn.Linear(model.fc.in_features, num_classes)

# EfficientNet (timm)
model.classifier = nn.Linear(model.classifier.in_features, num_classes)

4. 差异化学习率:

# 方式1: 参数分组
params_to_update = []
params_backbone = []

for name, param in model.named_parameters():
    if param.requires_grad:
        if 'fc' in name or 'classifier' in name:
            params_to_update.append(param)
        else:
            params_backbone.append(param)

optimizer = torch.optim.Adam([
    {'params': params_backbone, 'lr': 1e-4},  # 小学习率
    {'params': params_to_update, 'lr': 1e-3}  # 大学习率
])

Fine-tuning技巧#

1. 渐进式解冻:

# Epoch 1-5: 只训练分类头
# Epoch 6-10: 解冻layer4
# Epoch 11-15: 解冻layer3
# ...

def unfreeze_layer(model, layer_name):
    for name, param in model.named_parameters():
        if layer_name in name:
            param.requires_grad = True

2. 判别式学习率:

# 越深的层学习率越大
lr_mult = [0.1, 0.3, 0.6, 1.0]  # layer1-4
optimizer = torch.optim.SGD([
    {'params': model.layer1.parameters(), 'lr': base_lr * lr_mult[0]},
    {'params': model.layer2.parameters(), 'lr': base_lr * lr_mult[1]},
    {'params': model.layer3.parameters(), 'lr': base_lr * lr_mult[2]},
    {'params': model.layer4.parameters(), 'lr': base_lr * lr_mult[3]},
    {'params': model.fc.parameters(), 'lr': base_lr * 10}
], momentum=0.9)

3. 学习率预热:

from torch.optim.lr_scheduler import LinearLR, SequentialLR, CosineAnnealingLR

warmup_epochs = 5
total_epochs = 100

# 预热阶段
warmup_scheduler = LinearLR(
    optimizer, start_factor=0.1, end_factor=1.0, total_iters=warmup_epochs
)

# 主训练阶段
main_scheduler = CosineAnnealingLR(
    optimizer, T_max=total_epochs - warmup_epochs
)

# 组合调度器
scheduler = SequentialLR(
    optimizer,
    schedulers=[warmup_scheduler, main_scheduler],
    milestones=[warmup_epochs]
)

完整训练流程#

见代码文件: code/chapter05_modern_cnn/resnet_transfer_learning.py

核心流程:

  1. 数据准备与增强
  2. 加载预训练模型
  3. 冻结策略与学习率设置
  4. 训练循环与验证
  5. 模型保存与评估

架构选择指南#

场景匹配#

场景推荐模型理由
服务器端高精度EfficientNet-B7, ViT-L最高精度
服务器端平衡ResNet-50, EfficientNet-B3速度精度平衡
移动端MobileNetV3, EfficientNet-B0轻量快速
边缘设备MobileNetV3-Small极致轻量
实时应用MobileNetV3, EfficientNet-Lite低延迟

性能对比(ImageNet-1K)#

准确率排序:
EfficientNet-B7 (84.3%) > ResNet-152 (78.3%) > ResNet-50 (76.2%) > MobileNetV3 (75.2%)

速度排序(GPU):
MobileNetV3-Small (5ms) > MobileNetV3-Large (8ms) > ResNet-50 (22ms) > EfficientNet-B7 (80ms)

参数量排序:
MobileNetV3-Small (2.5M) < MobileNetV3-Large (5.4M) < ResNet-50 (25.6M) < EfficientNet-B7 (66M)

本章总结#

核心概念#

  1. ResNet残差连接: 解决梯度消失,使训练超深网络成为可能
  2. MobileNet深度可分离卷积: 大幅降低计算量,适合移动端
  3. EfficientNet复合缩放: 平衡深度、宽度、分辨率,实现最佳性能

关键技术#

  • 残差连接: y = F(x) + x
  • 深度可分离卷积: Depthwise + Pointwise
  • 复合缩放: d=α^φ, w=β^φ, r=γ^φ
  • 迁移学习: 冻结+微调

实战能力#

  • 使用torchvision和timm加载预训练模型
  • 设计合理的冻结策略
  • 设置差异化学习率
  • 实现完整的训练流程

进阶方向#

  • 架构搜索: NAS, AutoML
  • 知识蒸馏: 大模型→小模型
  • 剪枝量化: 模型压缩
  • 多任务学习: 共享特征提取器

练习题#

  1. 理论题: 推导深度可分离卷积相比标准卷积的计算量压缩比
  2. 实现题: 从零实现一个基础ResNet-18
  3. 实战题: 在Caltech-101数据集上对比ResNet、MobileNet、EfficientNet的性能
  4. 思考题: 为什么ViT需要更大的数据集,而ResNet在小数据集上表现更好?

参考资料#

论文#

代码资源#


第6章:Attention与Transformer#

本章概述#

本章深入讲解注意力机制和Transformer架构在计算机视觉中的应用。从Self-Attention的基本原理出发,理解Multi-Head Attention的设计,最终掌握Vision Transformer(ViT)的完整实现。

学习目标#

  • 理解Self-Attention机制的数学原理
  • 掌握Multi-Head Attention的设计思想
  • 深入理解Transformer架构
  • 实战:使用ViT进行图像分类

6.1 注意力机制原理#

注意力的直观理解#

人类视觉注意力:

  • 看图片时不会平等关注所有区域
  • 会聚焦到重要的区域(如人脸、物体)
  • 根据上下文动态调整注意力

计算机视觉中的注意力:

  • 让模型学习"关注哪里"
  • 动态加权不同位置的特征
  • 建模长距离依赖关系

Attention机制演进#

1. Soft Attention (2015):

α_i = exp(e_i) / Σ exp(e_j)  # 注意力权重
output = Σ α_i × v_i           # 加权求和

2. Self-Attention (2017):

从序列自身计算注意力
不依赖外部信息

3. Multi-Head Attention (2017):

多个注意力头
捕获不同类型的关系

Self-Attention数学原理#

输入: 序列 X = [x₁, x₂, …, xₙ] ∈ ℝⁿˣᵈ

三个关键矩阵:

Query (查询): Q = XW_Q ∈ ℝⁿˣᵈₖ
Key (键): K = XW_K ∈ ℝⁿˣᵈₖ
Value (值): V = XW_V ∈ ℝⁿˣᵈᵥ

参数: W_Q, W_K ∈ ℝᵈˣᵈₖ, W_V ∈ ℝᵈˣᵈᵥ

Scaled Dot-Product Attention:

Attention(Q, K, V) = softmax(QK^T / √d_k) × V

步骤:
1. 计算相似度: S = QK^T ∈ ℝⁿˣⁿ
2. 缩放: S = S / √d_k (防止梯度消失)
3. 归一化: A = softmax(S) (注意力权重)
4. 加权求和: Output = A × V

为什么要缩放(除以√d_k):

假设 Q, K 的元素独立同分布 N(0, 1)
则 QK^T 的元素方差约为 d_k
除以 √d_k 使方差归一化到 1
避免 softmax 进入饱和区(梯度消失)

Self-Attention示例#

输入序列: “我 爱 自然语言处理”

计算过程:

1. 将每个词映射到Q, K, V:
   Q_我 = [0.2, 0.5, ...], K_我 = [0.3, 0.1, ...], V_我 = [0.8, 0.2, ...]
   ...

2. 计算"我"对所有词的注意力分数:
   score(我, 我) = Q_我 · K_我 = 0.8
   score(我, 爱) = Q_我 · K_爱 = 0.6
   score(我, 自然语言处理) = Q_我 · K_自然语言处理 = 0.2

3. 归一化(softmax):
   α_我→我 = 0.4
   α_我→爱 = 0.5
   α_我→自然语言处理 = 0.1

4. 加权求和:
   Output_我 = 0.4 × V_我 + 0.5 × V_爱 + 0.1 × V_自然语言处理

计算复杂度#

时间复杂度: O(n²d)

  • 计算 QK^T: O(n²d)
  • 计算 AV: O(n²d)
  • 总计: O(n²d)

空间复杂度: O(n²)

  • 注意力矩阵: n×n

问题: 序列长度n大时(如高分辨率图像),复杂度过高

Self-Attention实现#

import torch
import torch.nn as nn
import torch.nn.functional as F

class SelfAttention(nn.Module):
    def __init__(self, embed_dim: int, head_dim: int):
        super().__init__()
        self.embed_dim = embed_dim
        self.head_dim = head_dim

        # Q, K, V投影
        self.q_proj = nn.Linear(embed_dim, head_dim)
        self.k_proj = nn.Linear(embed_dim, head_dim)
        self.v_proj = nn.Linear(embed_dim, head_dim)

        self.scale = head_dim ** -0.5  # 1/√d_k

    def forward(self, x):
        """
        Args:
            x: (batch_size, seq_len, embed_dim)

        Returns:
            (batch_size, seq_len, head_dim)
        """
        B, N, C = x.shape

        # 投影
        Q = self.q_proj(x)  # (B, N, head_dim)
        K = self.k_proj(x)  # (B, N, head_dim)
        V = self.v_proj(x)  # (B, N, head_dim)

        # 计算注意力分数
        attn = (Q @ K.transpose(-2, -1)) * self.scale  # (B, N, N)

        # 归一化
        attn = F.softmax(attn, dim=-1)  # (B, N, N)

        # 加权求和
        out = attn @ V  # (B, N, head_dim)

        return out

6.2 Transformer架构详解#

Transformer整体架构#

原始Transformer(用于NLP):

输入序列
Embedding + Positional Encoding
Encoder (N层):
  - Multi-Head Attention
  - Add & Norm
  - Feed Forward
  - Add & Norm
Decoder (N层):
  - Masked Multi-Head Attention
  - Add & Norm
  - Cross Attention
  - Add & Norm
  - Feed Forward
  - Add & Norm
输出

编码器层(Encoder Layer):

输入 x
Multi-Head Attention → Add & Norm
Feed Forward Network → Add & Norm
输出

Multi-Head Attention#

核心思想: 多个注意力头并行计算,捕获不同类型的关系

架构:

输入 X ∈ ℝⁿˣᵈ
分成h个头,每个头维度 d_k = d/h
Head₁ = Attention(XW_Q1, XW_K1, XW_V1)
Head₂ = Attention(XW_Q2, XW_K2, XW_V2)
...
Headₕ = Attention(XW_Qh, XW_Kh, XW_Vh)
Concat(Head₁, Head₂, ..., Headₕ)
线性投影 W_O
输出 ∈ ℝⁿˣᵈ

数学表达:

MultiHead(Q, K, V) = Concat(head₁, ..., headₕ)W_O

其中:
head_i = Attention(QW_Qi, KW_Ki, VW_Vi)

为什么要多头:

  • 不同的头可以关注不同的特征
  • 类比CNN的多个卷积核
  • 增强模型的表达能力

示例(h=8, d=512):

每个头的维度: d_k = 512/8 = 64
Head 1: 关注局部纹理
Head 2: 关注全局形状
Head 3: 关注颜色
...

位置编码(Positional Encoding)#

问题: Self-Attention对位置不敏感

输入: [A, B, C]
输入: [C, B, A]
注意力矩阵相同(如果Q, K, V投影相同)

解决: 添加位置信息

正弦位置编码(原始Transformer):

PE(pos, 2i) = sin(pos / 10000^(2i/d))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d))

其中:
- pos: 位置索引
- i: 维度索引
- d: 嵌入维度

可学习位置编码(ViT使用):

position_embedding = nn.Parameter(torch.randn(1, num_patches + 1, embed_dim))

Feed-Forward Network (FFN)#

结构:

FFN(x) = max(0, xW₁ + b₁)W₂ + b₂
      = GELU(xW₁ + b₁)W₂ + b₂  (现代变体)

其中:
- W₁: d → d_ff (通常 d_ff = 4d)
- W₂: d_ff → d

作用:

  • 增加非线性
  • 独立处理每个位置
  • 增强表达能力

Layer Normalization#

公式:

LN(x) = γ × (x - μ) / σ + β

其中:
- μ, σ: 沿特征维度计算的均值和标准差
- γ, β: 可学习参数

位置: Pre-LN vs Post-LN

Post-LN (原始):
x = x + Attention(LN(x))

Pre-LN (更稳定):
x = LN(x + Attention(x))

完整Transformer Encoder实现#

class TransformerEncoder(nn.Module):
    def __init__(
        self,
        embed_dim: int = 768,
        num_heads: int = 12,
        mlp_ratio: int = 4,
        dropout: float = 0.1
    ):
        super().__init__()
        self.norm1 = nn.LayerNorm(embed_dim)
        self.attn = nn.MultiheadAttention(
            embed_dim, num_heads, dropout=dropout, batch_first=True
        )
        self.norm2 = nn.LayerNorm(embed_dim)
        self.mlp = nn.Sequential(
            nn.Linear(embed_dim, embed_dim * mlp_ratio),
            nn.GELU(),
            nn.Dropout(dropout),
            nn.Linear(embed_dim * mlp_ratio, embed_dim),
            nn.Dropout(dropout)
        )

    def forward(self, x):
        # Multi-Head Attention
        x = x + self.attn(self.norm1(x), self.norm1(x), self.norm1(x))[0]

        # FFN
        x = x + self.mlp(self.norm2(x))

        return x

6.3 Vision Transformer (ViT)#

ViT核心思想#

将图像视为序列:

  • 图像分成固定大小的patch
  • 每个patch展平成向量
  • 当作序列输入Transformer

ViT架构详解#

完整流程:

输入图像: H × W × C (如 224×224×3)
分割成patch: (H/P) × (W/P) × (P²C)
  例: 224/16 × 224/16 × (16²×3) = 14×14×768
展平: N × (P²C), N = (H/W)² = 196
线性投影: N × D (D=768)
添加 [CLS] token: (N+1) × D
添加位置编码: (N+1) × D
Transformer Encoder × L层
提取 [CLS] token
MLP Head → 分类

关键组件:

1. Patch Embedding:

# 方式1: 展平 + 线性投影
patch_size = 16
num_patches = (224 // 16) ** 2  # 196

# 输入: (B, 3, 224, 224)
x = x.unfold(2, patch_size, patch_size).unfold(3, patch_size, patch_size)
# (B, 3, 14, 14, 16, 16)

x = x.permute(0, 2, 3, 4, 5, 1).reshape(B, num_patches, -1)
# (B, 196, 768)

x = linear_projection(x)
# (B, 196, embed_dim)

# 方式2: 卷积(等价且更快)
self.patch_embed = nn.Conv2d(
    in_channels=3,
    out_channels=embed_dim,
    kernel_size=patch_size,
    stride=patch_size
)

2. CLS Token:

# 可学习的分类token
self.cls_token = nn.Parameter(torch.randn(1, 1, embed_dim))

# 添加到序列开头
cls_tokens = self.cls_token.expand(B, -1, -1)  # (B, 1, embed_dim)
x = torch.cat([cls_tokens, x], dim=1)  # (B, N+1, embed_dim)

3. 位置编码:

# 可学习位置编码
self.pos_embed = nn.Parameter(
    torch.randn(1, num_patches + 1, embed_dim)
)

x = x + self.pos_embed

ViT模型配置#

标准配置:

模型Patch SizeEmbed DimDepthHeadsMLP RatioParams
ViT-Ti/161619212345.7M
ViT-S/1616384126422M
ViT-B/16167681212486M
ViT-B/32327681212488M
ViT-L/1616102424164307M
ViT-H/1414128032164632M

命名规则: ViT-{Size}/{Patch Size}

ViT vs CNN#

归纳偏置(Inductive Bias):

特性CNNViT
局部性强(卷积核)弱(全局注意力)
平移不变性
数据需求中等大量
计算复杂度O(n)O(n²)

性能对比(ImageNet-1K):

小数据集(<100k):
ResNet-50 (76.2%) > ViT-B/16 (75.0%)

大数据集(ImageNet-21K预训练):
ViT-B/16 (84.0%) > ResNet-50 (80.4%)

超大数据集(JFT-300M预训练):
ViT-H/14 (88.5%) >> ResNet-152 (82.0%)

ViT变体#

1. DeiT (Data-efficient ViT):

  • 知识蒸馏
  • 在ImageNet-1K上训练即可
  • 性能接近大规模预训练的ViT

2. Swin Transformer:

  • 层次化架构(类似CNN)
  • 窗口注意力(降低复杂度)
  • 适合密集预测任务

3. PVT (Pyramid Vision Transformer):

  • 金字塔结构
  • 渐进式降采样
  • 多尺度特征

6.4 实战:ViT图像分类#

使用timm库#

加载预训练模型:

import timm

# 查看可用模型
models = timm.list_models('vit*', pretrained=True)
print(models[:5])
# ['vit_base_patch16_224', 'vit_base_patch32_224', ...]

# 加载模型
model = timm.create_model('vit_base_patch16_224', pretrained=True, num_classes=10)

# 查看模型结构
print(model)

完整训练流程#

见代码文件: code/chapter06_transformer/vit_classification.py

关键点:

  1. 数据增强: 更强的增强(RandAugment, CutMix)
  2. 学习率: 较小的学习率(1e-4)
  3. 训练轮数: 更多epoch(300+)
  4. 正则化: Dropout, Stochastic Depth
  5. 优化器: AdamW + 权重衰减

ViT训练技巧#

1. 预训练权重:

# ImageNet-1K预训练
model = timm.create_model('vit_base_patch16_224', pretrained=True)

# ImageNet-21K预训练(更好)
model = timm.create_model('vit_base_patch16_224.augreg_in21k', pretrained=True)

2. 图像尺寸调整:

# 预训练: 224×224
# 微调: 可以用更大尺寸(384×384)
# 需要插值位置编码

def interpolate_pos_embed(pos_embed, orig_size, new_size):
    # pos_embed: (1, N+1, D)
    cls_pos = pos_embed[:, :1, :]  # CLS token
    patch_pos = pos_embed[:, 1:, :]  # Patch positions

    # 插值
    patch_pos = F.interpolate(
        patch_pos.reshape(1, orig_size, orig_size, -1).permute(0, 3, 1, 2),
        size=(new_size, new_size),
        mode='bicubic'
    )
    patch_pos = patch_pos.permute(0, 2, 3, 1).reshape(1, -1, pos_embed.shape[-1])

    return torch.cat([cls_pos, patch_pos], dim=1)

3. 混合精度训练:

from torch.cuda.amp import autocast, GradScaler

scaler = GradScaler()

for images, labels in dataloader:
    with autocast():
        outputs = model(images)
        loss = criterion(outputs, labels)

    scaler.scale(loss).backward()
    scaler.step(optimizer)
    scaler.update()

性能优化#

1. 降低计算复杂度:

  • 使用更小的patch size(ViT-B/32 vs ViT-B/16)
  • 使用Swin Transformer(窗口注意力)

2. 加速训练:

  • 混合精度训练(FP16)
  • 梯度累积
  • 分布式训练

3. 提升精度:

  • 更强的数据增强
  • 知识蒸馏
  • 模型ensemble

注意力可视化#

可视化注意力图#

def visualize_attention(model, image, layer_idx=11, head_idx=0):
    """
    可视化ViT的注意力图

    Args:
        model: ViT模型
        image: 输入图像
        layer_idx: Transformer层索引
        head_idx: 注意力头索引
    """
    model.eval()

    # 注册hook获取注意力权重
    attentions = []

    def hook_fn(module, input, output):
        # output: (attn_weights, attn_output)
        attentions.append(output[0])

    hook = model.blocks[layer_idx].attn.attn_drop.register_forward_hook(hook_fn)

    # 前向传播
    with torch.no_grad():
        _ = model(image)

    hook.remove()

    # 获取注意力权重: (B, num_heads, N+1, N+1)
    attn = attentions[0][0, head_idx, 0, 1:]  # CLS对所有patch的注意力

    # 重塑为2D
    size = int(attn.shape[0] ** 0.5)
    attn_map = attn.reshape(size, size).cpu().numpy()

    # 可视化
    plt.imshow(attn_map, cmap='viridis')
    plt.colorbar()
    plt.title(f'Attention Map - Layer {layer_idx}, Head {head_idx}')
    plt.show()

本章总结#

核心概念#

  1. Self-Attention: 通过Q、K、V矩阵计算序列内部关系
  2. Multi-Head Attention: 多个头捕获不同类型的依赖
  3. Transformer: 完全基于注意力的架构,无卷积
  4. Vision Transformer: 将图像分patch,用Transformer处理

关键公式#

Self-Attention:
Attention(Q, K, V) = softmax(QK^T / √d_k) × V

Multi-Head:
MultiHead(Q, K, V) = Concat(head₁, ..., headₕ)W_O

Positional Encoding:
PE(pos, 2i) = sin(pos / 10000^(2i/d))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d))

ViT优势与挑战#

优势:

  • 全局感受野
  • 可扩展性强
  • 大数据集上性能优异

挑战:

  • 需要大量数据
  • 计算复杂度高(O(n²))
  • 缺少归纳偏置

实战技巧#

  • 使用ImageNet-21K预训练权重
  • 强数据增强(RandAugment, Mixup)
  • 较小学习率 + 更多epoch
  • 混合精度训练加速

练习题#

  1. 理论题: 推导Self-Attention的计算复杂度为O(n²d)
  2. 实现题: 从零实现Multi-Head Attention
  3. 实战题: 在CIFAR-100上对比ResNet和ViT
  4. 思考题: 为什么ViT在大数据集上表现更好?

参考资料#

论文#

代码资源#


第7章:数据增强与训练技巧#

训练优化篇 - 掌握现代深度学习训练的核心技术

本章概览#

本章系统讲解深度学习训练的各种优化技巧,包括数据增强、学习率调度、正则化等,帮助你构建高性能的训练流程。

核心内容

  • 传统与现代数据增强技术
  • 学习率调度策略
  • 正则化与防过拟合
  • 完整训练流程设计

为什么训练技巧如此重要?#

同样的模型架构,不同的训练技巧可能导致5-10%的精度差异!

影响因素:

  1. 数据增强 - 有效扩充训练数据,提升泛化能力
  2. 学习率调度 - 合适的学习率策略加速收敛
  3. 正则化 - 防止过拟合,提升测试集表现
  4. 训练配置 - batch size、优化器选择等

7.1 传统数据增强#

7.1.1 基础几何变换#

PyTorch原生实现

from torchvision import transforms

# 基础增强组合
basic_transforms = transforms.Compose([
    transforms.RandomHorizontalFlip(p=0.5),  # 水平翻转
    transforms.RandomVerticalFlip(p=0.1),    # 垂直翻转(适用于特定任务)
    transforms.RandomRotation(degrees=15),    # 随机旋转
    transforms.RandomResizedCrop(224, scale=(0.8, 1.0)),  # 随机裁剪缩放
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                        std=[0.229, 0.224, 0.225])
])

几何变换对比

变换参数适用场景注意事项
水平翻转p=0.5通用场景文字识别慎用
垂直翻转p=0.1-0.5医学影像、卫星图自然图像少用
旋转±15-30°通用数字、字母慎用
随机裁剪scale=(0.8, 1.0)通用小目标检测少用
透视变换自定义文档、OCR需保持可读性

7.1.2 色彩变换#

color_transforms = transforms.Compose([
    transforms.ColorJitter(
        brightness=0.2,   # 亮度变化 ±20%
        contrast=0.2,     # 对比度变化 ±20%
        saturation=0.2,   # 饱和度变化 ±20%
        hue=0.1          # 色调变化 ±10%
    ),
    transforms.RandomGrayscale(p=0.1),  # 10%概率转灰度
    transforms.GaussianBlur(kernel_size=3, sigma=(0.1, 2.0)),  # 高斯模糊
])

7.1.3 使用Albumentations#

Albumentations 是更强大的数据增强库:

import albumentations as A
from albumentations.pytorch import ToTensorV2

# 高级增强组合
train_transforms = A.Compose([
    # 几何变换
    A.HorizontalFlip(p=0.5),
    A.ShiftScaleRotate(
        shift_limit=0.1,
        scale_limit=0.2,
        rotate_limit=15,
        p=0.5
    ),
    A.RandomResizedCrop(height=224, width=224, scale=(0.8, 1.0)),

    # 色彩变换
    A.OneOf([
        A.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
        A.HueSaturationValue(hue_shift_limit=20, sat_shift_limit=30),
        A.RandomBrightnessContrast(),
    ], p=0.5),

    # 噪声与模糊
    A.OneOf([
        A.GaussianBlur(blur_limit=3),
        A.GaussNoise(var_limit=(10.0, 50.0)),
        A.ISONoise(),
    ], p=0.3),

    # 标准化
    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
    ToTensorV2()
])

# 验证集增强(只做标准化)
val_transforms = A.Compose([
    A.Resize(224, 224),
    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
    ToTensorV2()
])

# 使用方式
class CustomDataset(torch.utils.data.Dataset):
    def __init__(self, image_paths, labels, transform=None):
        self.image_paths = image_paths
        self.labels = labels
        self.transform = transform

    def __getitem__(self, idx):
        image = cv2.imread(self.image_paths[idx])
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        label = self.labels[idx]

        if self.transform:
            augmented = self.transform(image=image)
            image = augmented['image']

        return image, label

7.2 现代数据增强#

7.2.1 Mixup#

核心思想:将两张图像线性混合

def mixup_data(x, y, alpha=0.2):
    """
    Mixup数据增强

    Args:
        x: 输入图像 (B, C, H, W)
        y: 标签 (B,)
        alpha: Beta分布参数

    Returns:
        mixed_x: 混合后的图像
        y_a, y_b: 原始标签对
        lam: 混合比例
    """
    if alpha > 0:
        lam = np.random.beta(alpha, alpha)
    else:
        lam = 1

    batch_size = x.size(0)
    index = torch.randperm(batch_size).to(x.device)

    mixed_x = lam * x + (1 - lam) * x[index, :]
    y_a, y_b = y, y[index]

    return mixed_x, y_a, y_b, lam

def mixup_criterion(criterion, pred, y_a, y_b, lam):
    """Mixup损失函数"""
    return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)

# 训练循环中使用
for images, labels in train_loader:
    images, labels = images.cuda(), labels.cuda()

    # 应用Mixup
    images, labels_a, labels_b, lam = mixup_data(images, labels, alpha=0.2)

    outputs = model(images)
    loss = mixup_criterion(criterion, outputs, labels_a, labels_b, lam)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

7.2.2 CutMix#

核心思想:将一张图像的矩形区域替换为另一张图像

def cutmix_data(x, y, alpha=1.0):
    """
    CutMix数据增强

    Args:
        x: 输入图像 (B, C, H, W)
        y: 标签 (B,)
        alpha: Beta分布参数
    """
    lam = np.random.beta(alpha, alpha)
    batch_size = x.size(0)
    index = torch.randperm(batch_size).to(x.device)

    # 计算剪切区域
    _, _, H, W = x.shape
    cut_ratio = np.sqrt(1 - lam)
    cut_h = int(H * cut_ratio)
    cut_w = int(W * cut_ratio)

    # 随机中心点
    cx = np.random.randint(W)
    cy = np.random.randint(H)

    # 边界框
    bbx1 = np.clip(cx - cut_w // 2, 0, W)
    bby1 = np.clip(cy - cut_h // 2, 0, H)
    bbx2 = np.clip(cx + cut_w // 2, 0, W)
    bby2 = np.clip(cy + cut_h // 2, 0, H)

    # 替换区域
    x[:, :, bby1:bby2, bbx1:bbx2] = x[index, :, bby1:bby2, bbx1:bbx2]

    # 调整lambda(基于实际裁剪面积)
    lam = 1 - ((bbx2 - bbx1) * (bby2 - bby1) / (W * H))

    y_a, y_b = y, y[index]

    return x, y_a, y_b, lam

7.2.3 AutoAugment#

核心思想:使用搜索算法找到最优增强策略

from torchvision.transforms import AutoAugment, AutoAugmentPolicy

# 使用预定义的AutoAugment策略
auto_augment = transforms.Compose([
    transforms.RandomResizedCrop(224),
    transforms.AutoAugment(policy=AutoAugmentPolicy.IMAGENET),  # ImageNet策略
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                        std=[0.229, 0.224, 0.225])
])

# 可选策略
# - AutoAugmentPolicy.IMAGENET  # ImageNet优化
# - AutoAugmentPolicy.CIFAR10   # CIFAR-10优化
# - AutoAugmentPolicy.SVHN      # SVHN优化

7.2.4 RandAugment#

更简单的自动增强

from torchvision.transforms import RandAugment

# N=2: 随机选择2个变换
# M=9: 变换强度(0-30)
rand_augment = transforms.Compose([
    transforms.RandomResizedCrop(224),
    transforms.RandAugment(num_ops=2, magnitude=9),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                        std=[0.229, 0.224, 0.225])
])

7.2.5 增强策略对比#

方法提升幅度计算开销复杂度推荐场景
基础增强+1-2%简单快速实验
Mixup+1-2%简单通用分类
CutMix+1-3%简单定位相关
AutoAugment+1-3%高(搜索)复杂最终模型
RandAugment+1-2.5%简单推荐首选

7.3 学习率调度#

7.3.1 常用调度器#

1. StepLR(阶梯衰减)

from torch.optim.lr_scheduler import StepLR

optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
scheduler = StepLR(optimizer, step_size=30, gamma=0.1)

# 每30个epoch学习率乘以0.1
# 0.1 -> 0.01 -> 0.001 -> ...

2. CosineAnnealingLR(余弦退火)

from torch.optim.lr_scheduler import CosineAnnealingLR

scheduler = CosineAnnealingLR(optimizer, T_max=100, eta_min=1e-6)

# 学习率从初始值余弦衰减到eta_min
# lr = eta_min + 0.5 * (lr_init - eta_min) * (1 + cos(π * t / T_max))

3. CosineAnnealingWarmRestarts(带重启的余弦退火)

from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts

scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2)

# 周期性重启:10, 20, 40, 80... epochs

4. OneCycleLR(一周期策略)

from torch.optim.lr_scheduler import OneCycleLR

scheduler = OneCycleLR(
    optimizer,
    max_lr=0.1,
    steps_per_epoch=len(train_loader),
    epochs=100,
    pct_start=0.3,  # 前30%用于warmup
    anneal_strategy='cos'
)

# 每个batch后更新
for batch in train_loader:
    # ... 训练 ...
    scheduler.step()

7.3.2 Warmup策略#

线性Warmup + Cosine衰减

class WarmupCosineScheduler:
    def __init__(self, optimizer, warmup_epochs, total_epochs, min_lr=1e-6):
        self.optimizer = optimizer
        self.warmup_epochs = warmup_epochs
        self.total_epochs = total_epochs
        self.min_lr = min_lr
        self.base_lr = optimizer.param_groups[0]['lr']

    def step(self, epoch):
        if epoch < self.warmup_epochs:
            # 线性warmup
            lr = self.base_lr * (epoch + 1) / self.warmup_epochs
        else:
            # 余弦衰减
            progress = (epoch - self.warmup_epochs) / (self.total_epochs - self.warmup_epochs)
            lr = self.min_lr + 0.5 * (self.base_lr - self.min_lr) * (1 + math.cos(math.pi * progress))

        for param_group in self.optimizer.param_groups:
            param_group['lr'] = lr

        return lr

# 使用
scheduler = WarmupCosineScheduler(optimizer, warmup_epochs=5, total_epochs=100)

for epoch in range(100):
    lr = scheduler.step(epoch)
    print(f"Epoch {epoch}, LR: {lr:.6f}")
    # ... 训练 ...

7.3.3 调度策略对比#

学习率变化曲线示意:

StepLR:           ___________
                           \___________
                                      \___________

CosineAnnealing:  ‾‾‾‾‾‾‾‾‾‾‾\_____________________/

OneCycle:              /\
                      /  \
                     /    \_______

Warmup+Cosine:       /‾‾‾‾‾‾‾‾‾‾\_____________________
策略特点推荐场景
StepLR简单直观快速实验
CosineAnnealing平滑衰减标准训练
OneCycleLR超级收敛追求快速收敛
Warmup+Cosine稳定训练大模型、Transformer

7.4 正则化与防过拟合#

7.4.1 Weight Decay(L2正则化)#

optimizer = torch.optim.AdamW(
    model.parameters(),
    lr=1e-3,
    weight_decay=0.01  # L2正则化系数
)

# 注意:AdamW是解耦的weight decay,推荐使用
# SGD + weight_decay = L2正则化
# Adam + weight_decay ≠ 严格的L2正则化
# AdamW = 正确的weight decay实现

7.4.2 Dropout#

class ModelWithDropout(nn.Module):
    def __init__(self, num_classes=1000):
        super().__init__()
        self.features = nn.Sequential(
            # ... 卷积层 ...
        )
        self.classifier = nn.Sequential(
            nn.Dropout(p=0.5),  # 训练时随机丢弃50%
            nn.Linear(2048, 512),
            nn.ReLU(),
            nn.Dropout(p=0.3),
            nn.Linear(512, num_classes)
        )

    def forward(self, x):
        x = self.features(x)
        x = x.flatten(1)
        x = self.classifier(x)
        return x

7.4.3 Label Smoothing#

class LabelSmoothingLoss(nn.Module):
    def __init__(self, num_classes, smoothing=0.1):
        super().__init__()
        self.num_classes = num_classes
        self.smoothing = smoothing
        self.confidence = 1.0 - smoothing

    def forward(self, pred, target):
        """
        pred: (B, num_classes) - 未归一化的logits
        target: (B,) - 类别索引
        """
        pred = pred.log_softmax(dim=-1)

        with torch.no_grad():
            # 创建平滑标签
            true_dist = torch.zeros_like(pred)
            true_dist.fill_(self.smoothing / (self.num_classes - 1))
            true_dist.scatter_(1, target.unsqueeze(1), self.confidence)

        return torch.mean(torch.sum(-true_dist * pred, dim=-1))

# 使用
criterion = LabelSmoothingLoss(num_classes=1000, smoothing=0.1)

# 或使用PyTorch内置
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)

7.4.4 Stochastic Depth(随机深度)#

class StochasticDepthResBlock(nn.Module):
    """随机深度残差块"""

    def __init__(self, in_channels, out_channels, survival_prob=0.8):
        super().__init__()
        self.survival_prob = survival_prob
        self.conv1 = nn.Conv2d(in_channels, out_channels, 3, padding=1)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1)
        self.bn2 = nn.BatchNorm2d(out_channels)

    def forward(self, x):
        residual = x

        if self.training:
            # 训练时随机跳过
            if torch.rand(1) > self.survival_prob:
                return residual

        out = F.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))

        if self.training:
            out = out / self.survival_prob  # 缩放补偿

        return F.relu(out + residual)

7.4.5 防过拟合策略总结#

方法适用场景推荐强度
Weight Decay通用0.01-0.1
Dropout全连接层0.3-0.5
Label Smoothing分类任务0.1
数据增强数据量少强增强
Early Stopping验证损失上升patience=10-20
Stochastic Depth深层网络0.8-0.9

7.5 混合精度训练#

7.5.1 PyTorch AMP#

from torch.cuda.amp import autocast, GradScaler

# 初始化
model = Model().cuda()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
scaler = GradScaler()

for epoch in range(num_epochs):
    for images, labels in train_loader:
        images, labels = images.cuda(), labels.cuda()

        optimizer.zero_grad()

        # 自动混合精度前向传播
        with autocast():
            outputs = model(images)
            loss = criterion(outputs, labels)

        # 缩放反向传播
        scaler.scale(loss).backward()

        # 优化器步骤
        scaler.step(optimizer)
        scaler.update()

混合精度优势

指标FP32FP16 (AMP)
显存占用100%~50%
训练速度1x1.5-3x
精度损失基准<0.1%

7.6 实战:完整训练流程#

完整训练脚本#

import torch
import torch.nn as nn
from torch.cuda.amp import autocast, GradScaler
from torch.utils.data import DataLoader
import albumentations as A
from albumentations.pytorch import ToTensorV2
from tqdm import tqdm
import wandb

class Trainer:
    def __init__(
        self,
        model,
        train_loader,
        val_loader,
        optimizer,
        criterion,
        scheduler,
        config
    ):
        self.model = model.cuda()
        self.train_loader = train_loader
        self.val_loader = val_loader
        self.optimizer = optimizer
        self.criterion = criterion
        self.scheduler = scheduler
        self.config = config

        self.scaler = GradScaler() if config.use_amp else None
        self.best_acc = 0.0

    def train_epoch(self, epoch):
        self.model.train()
        total_loss = 0
        correct = 0
        total = 0

        pbar = tqdm(self.train_loader, desc=f"Epoch {epoch}")
        for images, labels in pbar:
            images, labels = images.cuda(), labels.cuda()

            # Mixup (可选)
            if self.config.use_mixup:
                images, labels_a, labels_b, lam = mixup_data(images, labels)

            self.optimizer.zero_grad()

            # 混合精度前向
            if self.config.use_amp:
                with autocast():
                    outputs = self.model(images)
                    if self.config.use_mixup:
                        loss = mixup_criterion(self.criterion, outputs, labels_a, labels_b, lam)
                    else:
                        loss = self.criterion(outputs, labels)

                self.scaler.scale(loss).backward()
                self.scaler.step(self.optimizer)
                self.scaler.update()
            else:
                outputs = self.model(images)
                if self.config.use_mixup:
                    loss = mixup_criterion(self.criterion, outputs, labels_a, labels_b, lam)
                else:
                    loss = self.criterion(outputs, labels)
                loss.backward()
                self.optimizer.step()

            total_loss += loss.item()
            _, predicted = outputs.max(1)
            total += labels.size(0)
            correct += predicted.eq(labels).sum().item()

            pbar.set_postfix({
                'loss': f'{loss.item():.4f}',
                'acc': f'{100.*correct/total:.2f}%'
            })

        return total_loss / len(self.train_loader), correct / total

    @torch.no_grad()
    def validate(self):
        self.model.eval()
        total_loss = 0
        correct = 0
        total = 0

        for images, labels in self.val_loader:
            images, labels = images.cuda(), labels.cuda()

            outputs = self.model(images)
            loss = self.criterion(outputs, labels)

            total_loss += loss.item()
            _, predicted = outputs.max(1)
            total += labels.size(0)
            correct += predicted.eq(labels).sum().item()

        return total_loss / len(self.val_loader), correct / total

    def train(self):
        for epoch in range(self.config.epochs):
            # 训练
            train_loss, train_acc = self.train_epoch(epoch)

            # 验证
            val_loss, val_acc = self.validate()

            # 更新学习率
            self.scheduler.step()
            current_lr = self.optimizer.param_groups[0]['lr']

            # 日志
            print(f"Epoch {epoch}: "
                  f"Train Loss: {train_loss:.4f}, Train Acc: {train_acc*100:.2f}%, "
                  f"Val Loss: {val_loss:.4f}, Val Acc: {val_acc*100:.2f}%, "
                  f"LR: {current_lr:.6f}")

            # W&B记录
            if self.config.use_wandb:
                wandb.log({
                    'epoch': epoch,
                    'train_loss': train_loss,
                    'train_acc': train_acc,
                    'val_loss': val_loss,
                    'val_acc': val_acc,
                    'lr': current_lr
                })

            # 保存最佳模型
            if val_acc > self.best_acc:
                self.best_acc = val_acc
                torch.save({
                    'epoch': epoch,
                    'model_state_dict': self.model.state_dict(),
                    'optimizer_state_dict': self.optimizer.state_dict(),
                    'best_acc': self.best_acc,
                }, 'best_model.pth')
                print(f"✓ Best model saved (Acc: {val_acc*100:.2f}%)")

# 配置
class Config:
    epochs = 100
    batch_size = 32
    lr = 0.001
    weight_decay = 0.01
    warmup_epochs = 5
    use_amp = True
    use_mixup = True
    use_wandb = True

# 使用
config = Config()

# 数据增强
train_transforms = A.Compose([
    A.RandomResizedCrop(224, 224, scale=(0.8, 1.0)),
    A.HorizontalFlip(p=0.5),
    A.RandAugment(num_ops=2, magnitude=9),
    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
    ToTensorV2()
])

# 模型
model = timm.create_model('resnet50', pretrained=True, num_classes=num_classes)

# 优化器
optimizer = torch.optim.AdamW(model.parameters(), lr=config.lr, weight_decay=config.weight_decay)

# 调度器
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=config.epochs)

# 损失函数
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)

# 训练
trainer = Trainer(model, train_loader, val_loader, optimizer, criterion, scheduler, config)
trainer.train()

本章小结#

核心技术要点#

  1. 数据增强

    • 传统:翻转、旋转、色彩变换
    • 现代:Mixup、CutMix、RandAugment
  2. 学习率调度

    • Warmup + Cosine是标准选择
    • OneCycleLR用于快速收敛
  3. 正则化

    • Weight Decay(AdamW推荐)
    • Label Smoothing(分类任务)
    • Dropout(全连接层)
  4. 混合精度

    • AMP训练提速1.5-3倍
    • 显存减少约50%

推荐训练配置#

通用分类任务:
  optimizer: AdamW
  lr: 0.001
  weight_decay: 0.01
  scheduler: CosineAnnealing + Warmup(5 epochs)
  augmentation: RandAugment
  regularization:
    - label_smoothing: 0.1
    - dropout: 0.3
  training:
    - amp: true
    - mixup: alpha=0.2

下一步#

恭喜完成第三篇!你已经掌握了:

  • 现代CNN架构(ResNet、MobileNet、EfficientNet)
  • Transformer与ViT
  • 高级训练技巧

接下来进入第四篇:目标检测与YOLO系列,学习视觉任务的另一个核心领域!


参考资源


更新日期:2025年11月 基于版本:PyTorch 2.x, albumentations 1.4+

[统计组件仅在生产环境显示]