这是从《深度学习知识框架》里拆出来的专题文章,专门讲训练工程。
很多人学深度学习时,前向传播、反向传播都能背下来,但一到自己训练模型就会遇到这些问题:
- 损失不下降
- 验证集效果越来越差
- 学习率不知道怎么设
- 调了很多超参数,结果还是不稳定
这些问题,往往不是“模型不会”,而是训练工程没掌握。
目录
1. 先学会看训练曲线
训练曲线是你调模型时最重要的仪表盘。
通过同时观察训练损失和验证损失,你通常可以很快定位问题:
| 曲线形态 | 诊断结论 | 特征描述 |
|---|---|---|
| 两条线同步下降后稳定 | 正常收敛 | 理想状态,无需过多干预 |
| 训练损失下降,验证损失回升 | 过拟合 | 模型开始记训练集细节,泛化变差 |
| 两条线都居高不下 | 欠拟合 | 模型、特征或训练轮数都不够 |
| 损失剧烈震荡 | 学习率过大 | 更新步子太大,来回跨过最优点 |
| 损失极缓慢下降 | 学习率过小 | 训练推进太慢,收敛效率很低 |
某步突然暴增到 NaN | 梯度爆炸 / 数值不稳 | 需要立刻查学习率、输入和梯度 |
一个很实用的习惯:每次训练都把
train loss / val loss / train acc / val acc记录下来。没有曲线,调参基本靠猜。
2. 过拟合解法工具箱
过拟合的本质是:模型把训练集里的噪声也记住了。
最常见的思路有两类:
2.1 从数据和训练流程下手
数据增强
通过翻转、裁剪、旋转、颜色抖动等方式,让模型看到更多变体。
这样做的意义不是“真把数据变多了”,而是强迫模型学习不变的规律,而不是死记某张图的细节。
早停(Early Stopping)
当验证集损失连续几轮不再改善,就停止训练并保留最佳权重。
它的优点很简单:成本低、见效快、非常适合新手。
2.2 从模型和参数下手
Dropout
训练时随机关闭一部分神经元,避免网络过度依赖少数特征。
nn.Dropout(p=0.5)
经验值:
- 全连接层:
0.3 ~ 0.5 - 卷积层:
0.1 ~ 0.2
L2 正则化(weight_decay)
在损失函数里额外惩罚过大的权重:
loss = original_loss + λ * ||W||²
PyTorch 里通常直接写在优化器里:
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)
BatchNorm
BatchNorm 会把每层输入的分布稳定在更可控的范围内,让训练更稳、收敛更快。
它的核心好处有两个:
- 降低训练不稳定问题
- 允许你使用更积极的学习率
缩小模型规模
如果数据量很少,就别一开始上超大模型。
小数据 + 大模型,最容易过拟合。
最常见有效组合: 数据增强 + Dropout + Early Stopping。
3. 学习率调度策略
学习率几乎是最重要的超参数,没有之一。
| 调度策略 | 描述 | 适用场景 |
|---|---|---|
| 固定学习率 | 全程不变 | 快速实验、跑通代码 |
| 阶梯衰减 | 每隔若干轮乘以一个因子 | 传统 CV 任务,常配 StepLR |
| 余弦退火 | 按余弦曲线平滑下降 | 现代训练里很常见 |
| Warmup + 衰减 | 先小步热身,再逐渐衰减 | Transformer、大模型训练 |
| 循环学习率 | 在一个范围内周期波动 | 想尝试跳出局部极小值时 |
实战推荐
- 一般图像任务:优先试 余弦退火
- Transformer / 大模型:常用 Warmup + 余弦退火
- 不知道初始学习率:用 LR Finder 先扫一遍
一个非常实用的经验:如果损失震荡很大,先把学习率缩小 10 倍试试。
4. 正则化详解
正则化的核心思想就一句话:别让模型走极端。
4.1 L1 和 L2 的区别
| | L2 正则(权重衰减) | L1 正则 |
| -------- | ----------------------- | ------------------------ | --- | --- |
| 惩罚项 | Σ w² | Σ | w | |
| 效果 | 权重变小、更均匀 | 权重更稀疏,很多会变成 0 |
| 使用频率 | 深度学习里最常见 | 特征选择更常见 |
| PyTorch | weight_decay 直接支持 | 一般需要手动实现 |
一句话记忆:L2 让权重变小,L1 让权重变零。
4.2 Dropout 的直觉
Dropout 的做法可以理解成:训练时故意“拆掉”一部分神经元,逼着网络别只靠单一路径做判断。
这会让模型更像在做“多条子网络的集成”,因此更不容易过拟合。
4.3 BatchNorm 的作用
BatchNorm 主要解决的是:训练过程中,每一层输入分布不断变化,导致后续层很难学。
它会做 4 步:
① 计算均值 μ
② 计算方差 σ²
③ 做归一化
④ 用可学习参数 γ、β 再做缩放和平移
所以它既保持了训练稳定性,又不会限制模型表达能力。
4.4 三者怎么配合看
| 技术 | 解决的核心问题 | 作用位置 |
|---|---|---|
| L2 正则 | 权重过大,边界太复杂 | 损失函数 / 优化器 |
| Dropout | 神经元彼此依赖过强 | 网络层之间 |
| BatchNorm | 层间分布漂移、训练不稳 | 每层输出附近 |
5. 数据处理流程与数据增强
5.1 完整数据管道
一个比较标准的流程是:
原始数据
→ 数据清洗
→ 训练/验证/测试切分
→ 归一化 / 标准化
→ 数据增强(只对训练集)
→ DataLoader
→ 模型输入
这里最容易踩的坑是:
数据增强只对训练集做,验证集和测试集只做确定性的预处理。
5.2 归一化
| 方法 | 公式 | 典型用途 |
|---|---|---|
| Min-Max 归一化 | x' = (x - min) / (max - min) | 图像像素缩放到 [0, 1] |
| Z-score 标准化 | x' = (x - μ) / σ | 深度学习里更常见 |
注意:μ 和 σ 应该从训练集统计出来,不能把验证集和测试集的数据提前泄露进去。
5.3 常见图像增强方法
| 类型 | 具体方法 | 作用 |
|---|---|---|
| 几何变换 | 翻转、旋转、裁剪、缩放 | 提升位置和视角鲁棒性 |
| 颜色变换 | 亮度 / 对比度 / 饱和度抖动 | 适应不同光照环境 |
| 遮挡类 | Cutout、GridMask | 防止模型依赖局部区域 |
| 混合类 | Mixup、CutMix | 常用于竞赛提分 |
5.4 一个常见的 PyTorch 例子
from torchvision import transforms
train_transform = transforms.Compose([
transforms.Resize((256, 256)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomCrop(224),
transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.2),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
])
val_transform = transforms.Compose([
transforms.Resize((256, 256)),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
])
6. 调参系统方法与诊断
6.1 调参顺序:从粗到细
比较稳的做法是:
- 先验证训练管道没 bug:拿 100 个样本,看看能不能故意过拟合到训练损失接近 0。
- 先找学习率:别一开始就乱调十几个超参数。
- 一次只改一个变量:不然你根本不知道哪项改动起作用。
- 记录实验结果:用 TensorBoard、W&B 或 Excel 都行。
6.2 超参数优先级
| 优先级 | 超参数 | 建议起点 |
|---|---|---|
| 最高 | 学习率 | Adam: 1e-3 / SGD: 1e-2 ~ 1e-1 |
| 高 | Batch size | 32 / 64 / 128 |
| 高 | 模型容量 | 从小模型开始 |
| 高 | 训练轮次 | 搭配 Early Stopping |
| 中低 | Dropout | 0.3 ~ 0.5 |
| 中低 | weight_decay | 1e-4 ~ 5e-4 |
6.3 梯度健康状态监控
total_norm = 0
for p in model.parameters():
if p.grad is not None:
total_norm += p.grad.data.norm(2).item() ** 2
total_norm = total_norm ** 0.5
print(f"梯度范数: {total_norm:.4f}")
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
| 梯度范数 | 诊断 | 处理方式 |
|---|---|---|
0.01 ~ 1.0 | 正常 | 无需干预 |
< 1e-5 | 梯度消失 | 换 ReLU、残差连接、BatchNorm |
> 100 | 梯度爆炸 | 梯度裁剪、降学习率、加 Warmup |
6.4 症状速查表
| 观察到的现象 | 诊断 | 优先尝试的解法 |
|---|---|---|
| 训练损失从头就不降 | 管道错误 | 先检查数据、标签、归一化 |
损失变 NaN | 数值不稳定 | 梯度裁剪、学习率降 10 倍 |
| 损失剧烈震荡 | 学习率太大 | 缩小学习率、加 Warmup |
| 损失下降太慢 | 学习率太小 | 放大学习率、尝试 Adam |
| 训练好,验证差 | 过拟合 | 增强、Dropout、L2、早停 |
| 训练和验证都差 | 欠拟合 | 更大模型、更多 epoch、减弱正则 |
7. 最后的实战建议
如果你想真正把这些训练工程知识用起来,最推荐的练手项目是:
CIFAR-10图像分类- 手写一个简化版
ResNet - 加上数据增强、
SGD + momentum、余弦退火和保存最优权重
我已经把这部分单独拆成了一篇完整实战文:
👉 PyTorch 实战 CIFAR-10:手写 ResNet 并把准确率做到 94% 左右
训练工程的本质,不是死记参数,而是学会“看现象 → 做诊断 → 小步验证”。一旦养成这个习惯,你训练模型会顺手很多。