第三篇:计算机视觉核心技术#
篇章概述#
本篇深入讲解现代计算机视觉的核心技术,包括经典CNN架构、注意力机制、Transformer以及先进的训练技巧。这些技术是当今计算机视觉领域的基石。
本篇目标#
- 掌握现代CNN架构:ResNet、MobileNet、EfficientNet的设计思想和实现
- 理解注意力机制:从Self-Attention到Vision Transformer的演进
- 掌握训练技巧:数据增强、学习率调度、正则化等高级技术
- 实战能力:能够使用预训练模型进行迁移学习和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准确率):
| 模型 | 参数量 | FLOPs | Top-1准确率 | 推理速度(GPU) |
|---|---|---|---|---|
| ResNet-50 | 25.6M | 4.1G | 76.2% | 22ms |
| MobileNetV3-Large | 5.4M | 0.22G | 75.2% | 6ms |
| EfficientNet-B0 | 5.3M | 0.39G | 77.1% | 8ms |
| ViT-B/16 | 86M | 17.6G | 81.8% | 45ms |
| ViT-L/16 | 307M | 61.6G | 82.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 = True3. 学习率设置#
# 迁移学习典型设置
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)
- 大数据集:基础增强即可
进阶资源#
论文必读#
- ResNet: Deep Residual Learning for Image Recognition (CVPR 2016)
- MobileNet: MobileNets: Efficient CNNs for Mobile Vision (2017)
- EfficientNet: EfficientNet: Rethinking Model Scaling (ICML 2019)
- ViT: An Image is Worth 16x16 Words: Transformers for Image Recognition (ICLR 2021)
- AutoAugment: AutoAugment: Learning Augmentation Policies (CVPR 2019)
代码仓库#
- timm: https://github.com/huggingface/pytorch-image-models
- torchvision: https://github.com/pytorch/vision
- transformers: https://github.com/huggingface/transformers
- albumentations: https://github.com/albumentations-team/albumentations
本篇总结#
通过本篇学习,你将:
- 理解现代CNN架构的演进逻辑
- 掌握注意力机制和Transformer在视觉领域的应用
- 学会使用各种训练技巧提升模型性能
- 具备完整的深度学习项目开发能力
下一篇将进入目标检测领域,学习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架构族#
| 模型 | 层数 | 参数量 | FLOPs | ImageNet Top-1 |
|---|---|---|---|---|
| ResNet-18 | 18 | 11.7M | 1.8G | 69.8% |
| ResNet-34 | 34 | 21.8M | 3.7G | 73.3% |
| ResNet-50 | 50 | 25.6M | 4.1G | 76.2% |
| ResNet-101 | 101 | 44.5M | 7.8G | 77.4% |
| ResNet-152 | 152 | 60.2M | 11.6G | 78.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 out2. 梯度流动:
反向传播:
∂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: 1000MobileNetV2:倒残差结构(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 (压缩回低维)
↓
+ ← 输入 (残差连接)关键设计:
- 扩展因子 t=6: 在高维空间提取特征
- 线性瓶颈: 最后一层不使用ReLU(避免信息损失)
- 残差连接: 仅在输入输出维度相同时使用
MobileNetV3:神经架构搜索(NAS)#
改进点:
- SE模块(Squeeze-and-Excitation):
输入 → Global Pool → FC → ReLU → FC → Sigmoid → 乘回输入- h-swish激活:
h-swish(x) = x × ReLU6(x + 3) / 6
优点: 比ReLU性能好,比swish计算快- 优化首尾结构:
- 首层: 减少卷积核数量
- 尾层: 提前进行全局池化
性能对比#
| 模型 | 参数量 | FLOPs | Top-1(%) | 推理速度(CPU) |
|---|---|---|---|---|
| MobileNetV1 | 4.2M | 0.57G | 70.6 | 35ms |
| MobileNetV2 | 3.5M | 0.30G | 72.0 | 28ms |
| MobileNetV3-Large | 5.4M | 0.22G | 75.2 | 25ms |
| MobileNetV3-Small | 2.5M | 0.06G | 67.4 | 12ms |
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):
| Stage | Operator | Resolution | Channels | Layers |
|---|---|---|---|---|
| 1 | Conv3×3 | 224×224 | 32 | 1 |
| 2 | MBConv1, k3×3 | 112×112 | 16 | 1 |
| 3 | MBConv6, k3×3 | 112×112 | 24 | 2 |
| 4 | MBConv6, k5×5 | 56×56 | 40 | 2 |
| 5 | MBConv6, k3×3 | 28×28 | 80 | 3 |
| 6 | MBConv6, k5×5 | 14×14 | 112 | 3 |
| 7 | MBConv6, k5×5 | 14×14 | 192 | 4 |
| 8 | MBConv6, k3×3 | 7×7 | 320 | 1 |
| 9 | Conv1×1, Pool, FC | 7×7 | 1280 | 1 |
MBConv: MobileNetV2倒残差块 + SE模块
EfficientNet系列#
缩放参数(α=1.2, β=1.1, γ=1.15):
| 模型 | φ | 深度 | 宽度 | 分辨率 | 参数量 | FLOPs | Top-1(%) |
|---|---|---|---|---|---|---|---|
| B0 | 0 | 1.0 | 1.0 | 224 | 5.3M | 0.39G | 77.1 |
| B1 | 1 | 1.2 | 1.1 | 240 | 7.8M | 0.70G | 79.1 |
| B2 | 2 | 1.4 | 1.2 | 260 | 9.2M | 1.0G | 80.1 |
| B3 | 3 | 1.6 | 1.3 | 300 | 12M | 1.8G | 81.6 |
| B4 | 4 | 1.9 | 1.4 | 380 | 19M | 4.2G | 82.9 |
| B5 | 5 | 2.3 | 1.5 | 456 | 30M | 9.9G | 83.6 |
| B6 | 6 | 2.8 | 1.6 | 528 | 43M | 19G | 84.0 |
| B7 | 7 | 3.3 | 1.7 | 600 | 66M | 37G | 84.3 |
核心优势#
参数效率:
同样83%准确率:
- ResNet-152: 60M参数, 11.6G FLOPs
- EfficientNet-B3: 12M参数, 1.8G FLOPs
压缩比: 5倍参数, 6.4倍FLOPs5.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 = False3. 替换分类头:
# 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 = True2. 判别式学习率:
# 越深的层学习率越大
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
核心流程:
- 数据准备与增强
- 加载预训练模型
- 冻结策略与学习率设置
- 训练循环与验证
- 模型保存与评估
架构选择指南#
场景匹配#
| 场景 | 推荐模型 | 理由 |
|---|---|---|
| 服务器端高精度 | 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)本章总结#
核心概念#
- ResNet残差连接: 解决梯度消失,使训练超深网络成为可能
- MobileNet深度可分离卷积: 大幅降低计算量,适合移动端
- EfficientNet复合缩放: 平衡深度、宽度、分辨率,实现最佳性能
关键技术#
- 残差连接:
y = F(x) + x - 深度可分离卷积: Depthwise + Pointwise
- 复合缩放:
d=α^φ, w=β^φ, r=γ^φ - 迁移学习: 冻结+微调
实战能力#
- 使用torchvision和timm加载预训练模型
- 设计合理的冻结策略
- 设置差异化学习率
- 实现完整的训练流程
进阶方向#
- 架构搜索: NAS, AutoML
- 知识蒸馏: 大模型→小模型
- 剪枝量化: 模型压缩
- 多任务学习: 共享特征提取器
练习题#
- 理论题: 推导深度可分离卷积相比标准卷积的计算量压缩比
- 实现题: 从零实现一个基础ResNet-18
- 实战题: 在Caltech-101数据集上对比ResNet、MobileNet、EfficientNet的性能
- 思考题: 为什么ViT需要更大的数据集,而ResNet在小数据集上表现更好?
参考资料#
论文#
- Deep Residual Learning for Image Recognition (ResNet, CVPR 2016)
- MobileNets: Efficient CNNs for Mobile Vision (MobileNetV1, 2017)
- MobileNetV2: Inverted Residuals and Linear Bottlenecks (CVPR 2018)
- Searching for MobileNetV3 (ICCV 2019)
- EfficientNet: Rethinking Model Scaling for CNNs (ICML 2019)
代码资源#
- torchvision.models: https://pytorch.org/vision/stable/models.html
- timm: https://github.com/huggingface/pytorch-image-models
- EfficientNet官方实现: https://github.com/tensorflow/tpu/tree/master/models/official/efficientnet
第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 out6.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 x6.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_embedViT模型配置#
标准配置:
| 模型 | Patch Size | Embed Dim | Depth | Heads | MLP Ratio | Params |
|---|---|---|---|---|---|---|
| ViT-Ti/16 | 16 | 192 | 12 | 3 | 4 | 5.7M |
| ViT-S/16 | 16 | 384 | 12 | 6 | 4 | 22M |
| ViT-B/16 | 16 | 768 | 12 | 12 | 4 | 86M |
| ViT-B/32 | 32 | 768 | 12 | 12 | 4 | 88M |
| ViT-L/16 | 16 | 1024 | 24 | 16 | 4 | 307M |
| ViT-H/14 | 14 | 1280 | 32 | 16 | 4 | 632M |
命名规则: ViT-{Size}/{Patch Size}
ViT vs CNN#
归纳偏置(Inductive Bias):
| 特性 | CNN | ViT |
|---|---|---|
| 局部性 | 强(卷积核) | 弱(全局注意力) |
| 平移不变性 | 强 | 弱 |
| 数据需求 | 中等 | 大量 |
| 计算复杂度 | 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
关键点:
- 数据增强: 更强的增强(RandAugment, CutMix)
- 学习率: 较小的学习率(1e-4)
- 训练轮数: 更多epoch(300+)
- 正则化: Dropout, Stochastic Depth
- 优化器: 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()本章总结#
核心概念#
- Self-Attention: 通过Q、K、V矩阵计算序列内部关系
- Multi-Head Attention: 多个头捕获不同类型的依赖
- Transformer: 完全基于注意力的架构,无卷积
- 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
- 混合精度训练加速
练习题#
- 理论题: 推导Self-Attention的计算复杂度为O(n²d)
- 实现题: 从零实现Multi-Head Attention
- 实战题: 在CIFAR-100上对比ResNet和ViT
- 思考题: 为什么ViT在大数据集上表现更好?
参考资料#
论文#
- Attention Is All You Need (Transformer, NeurIPS 2017)
- An Image is Worth 16x16 Words (ViT, ICLR 2021)
- Training data-efficient image transformers (DeiT, 2021)
- Swin Transformer (ICCV 2021)
代码资源#
- timm: https://github.com/huggingface/pytorch-image-models
- ViT官方实现: https://github.com/google-research/vision_transformer
- Swin Transformer: https://github.com/microsoft/Swin-Transformer
第7章:数据增强与训练技巧#
训练优化篇 - 掌握现代深度学习训练的核心技术
本章概览#
本章系统讲解深度学习训练的各种优化技巧,包括数据增强、学习率调度、正则化等,帮助你构建高性能的训练流程。
核心内容:
- 传统与现代数据增强技术
- 学习率调度策略
- 正则化与防过拟合
- 完整训练流程设计
为什么训练技巧如此重要?#
同样的模型架构,不同的训练技巧可能导致5-10%的精度差异!
影响因素:
- 数据增强 - 有效扩充训练数据,提升泛化能力
- 学习率调度 - 合适的学习率策略加速收敛
- 正则化 - 防止过拟合,提升测试集表现
- 训练配置 - 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, label7.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, lam7.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... epochs4. 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 x7.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()混合精度优势:
| 指标 | FP32 | FP16 (AMP) |
|---|---|---|
| 显存占用 | 100% | ~50% |
| 训练速度 | 1x | 1.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()本章小结#
核心技术要点#
数据增强
- 传统:翻转、旋转、色彩变换
- 现代:Mixup、CutMix、RandAugment
学习率调度
- Warmup + Cosine是标准选择
- OneCycleLR用于快速收敛
正则化
- Weight Decay(AdamW推荐)
- Label Smoothing(分类任务)
- Dropout(全连接层)
混合精度
- 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系列,学习视觉任务的另一个核心领域!
参考资源:
- mixup: Beyond Empirical Risk Minimization
- CutMix: Regularization Strategy
- RandAugment: Practical automated data augmentation
- Albumentations Documentation
更新日期:2025年11月 基于版本:PyTorch 2.x, albumentations 1.4+