深度学习系列教程之第六章
深度学习训练优化技巧保姆级笔记
一句话定位:解决 “模型跑通了但效果差、训练慢、总报错” 的终极指南。所有内容都经过实战验证,代码复制就能用,看完直接从 “调包侠” 升级为 “会调参的调包侠”。
🎯 先搞懂:我们到底在优化什么?
一个最形象的比喻
- 损失函数 = 一座凹凸不平的大山
- 我们的目标 = 走到山谷底部(损失最小的地方)
- 模型参数 = 我们在山上的 “位置坐标”
- 优化器 = 我们的 “下山向导”,负责告诉我们:
- 往哪个方向走(梯度方向)
- 每一步走多大(学习率)
普通 SGD 的三大痛点(为什么需要优化)
- 震荡大:走 Z 字形路线,绕远路
- 收敛慢:步子太小,走半天到不了
- 容易卡住:卡在小土坡(局部最优)就以为到了山谷
这一章的所有技巧,都是为了让你的 “向导” 变得更聪明、更稳、更快。
🚀 一、优化器大比拼:谁是最好的下山向导?
核心结论(小白直接抄)
✅ 90% 的情况,直接用 Adam,lr=0.001
✅ 效果不好再试带动量的 SGD
✅ 所有优化器都要加学习率衰减
1.1 动量法(Momentum):给下山加个 “惯性”
🎯 解决什么问题?
普通 SGD 震荡太大,就像你下山时每一步都重新看方向,容易走回头路。
🗣️ 大白话原理
不仅看当前这一步的坡度,还保留之前所有步子的方向。就像滚雪球,越滚越快,既能冲过小土坡,又能走得更平稳。
✅ 最佳实践代码
import torch.optim as optim
# 普通SGD(不推荐)
optimizer = optim.SGD(model.parameters(), lr=0.01)
# 带动量的SGD(momentum=0.9是行业标准,几乎不用改)
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
⚠️ 避坑提醒
- momentum 不要设太大(超过 0.95),否则容易冲过山谷
- 配合学习率衰减使用效果更好
📌 一句话总结
加了惯性的 SGD,走得更稳更快。
1.2 学习率衰减:越靠近山谷,步子越小
🎯 为什么必须要有?
- 学习率太大:一步跨过大山,永远到不了山谷(loss 震荡不下降)
- 学习率太小:走得太慢,训练 100 轮还没收敛(loss 下降极慢)
最佳策略:一开始用大步子快速接近山谷,快到的时候用小步子精细调整。
✅ 三种最常用的衰减方式(按推荐顺序)
| 方法 | 特点 | 适用场景 | 代码 |
|---|---|---|---|
| 等间隔衰减(StepLR) | 每隔固定轮数,学习率乘以一个系数 | 绝大多数情况通用 | StepLR(optimizer, step_size=20, gamma=0.7) |
| 指定间隔衰减(MultiStepLR) | 在你指定的轮数点衰减 | 知道模型大概什么时候收敛 | MultiStepLR(optimizer, milestones=[10,30,50], gamma=0.5) |
| 指数衰减(ExponentialLR) | 每一轮都按指数衰减 | 需要快速降低学习率 | ExponentialLR(optimizer, gamma=0.99) |
✅ 完整使用示例(必看)
from torch.optim.lr_scheduler import StepLR
# 1. 定义优化器
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 2. 定义学习率调度器:每隔20个epoch,学习率乘以0.7
lr_scheduler = StepLR(optimizer, step_size=20, gamma=0.7)
# 3. 训练循环
for epoch in range(epochs):
# 训练代码(前向传播、计算损失、反向传播、更新参数)
train_one_epoch(model, train_loader, loss_fn, optimizer)
# 验证代码
validate(model, val_loader, loss_fn)
# ⚠️ 这行一定要加!每轮结束后更新学习率
lr_scheduler.step()
# 打印当前学习率(方便调试)
print(f"当前学习率: {optimizer.param_groups[0]['lr']:.6f}")
⚠️ 避坑提醒
- 学习率调度器的
step()要放在epoch 结束后,不是 batch 结束后 - 初始学习率不要太大(Adam 建议 0.001,SGD 建议 0.01)
- 衰减系数 gamma 不要太小(低于 0.5),否则学习率降得太快
📌 一句话总结
大步子赶路,小步子找家。
1.3 Adam:目前最好用的 “全能型” 优化器
🎯 为什么是首选?
结合了动量法(惯性)和RMSProp(自适应学习率)的所有优点:
- 自动给每个参数调整合适的学习率
- 收敛速度快,震荡小
- 几乎不用调参,默认参数效果就很好
✅ 最佳实践代码
# betas=(0.9, 0.999)是官方推荐的默认值,不用改!
optimizer = optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.999))
⚠️ 避坑提醒
- Adam 的初始学习率建议 0.001,不要用 SGD 的 0.01
- 极少数情况 Adam 会过拟合,这时可以换成带动量的 SGD
📌 一句话总结
小白闭着眼睛用 Adam 就对了。
1.4 优化器终极对比表
| 优化器 | 核心思想 | 优点 | 缺点 | 推荐指数 |
|---|---|---|---|---|
| SGD | 只看当前梯度 | 简单、理论基础好 | 震荡大、收敛慢 | ⭐⭐ |
| 动量 SGD | 当前梯度 + 惯性 | 收敛更快、震荡小 | 还是需要调学习率 | ⭐⭐⭐ |
| Adam | 动量 + 自适应学习率 | 几乎不用调参、收敛快、效果好 | 偶尔过拟合 | ⭐⭐⭐⭐⭐ |
| RMSProp | 改进 AdaGrad | 自适应学习率 | 比 Adam 稍差 | ⭐⭐⭐ |
🔧 二、参数初始化:好的开始是成功的一半
2.1 为什么不能随便初始化?
❌ 绝对不能做的事
-
全 0 初始化:所有参数都一样,反向传播时所有参数更新也一样,等于只有一个神经元在工作
nn.init.zeros_(linear.weight) # 绝对不要这么写! -
太大的初始化:导致梯度爆炸(loss 变成 nan)
-
太小的初始化:导致梯度消失(loss 几乎不变,模型不动)
✅ 好的初始化标准
- 所有参数都是随机的(打破对称性)
- 数值范围合适(既不太大也不太小)
- 每一层的输出方差和输入方差差不多
2.2 常用初始化方法(按推荐顺序)
✅ 1. He 初始化(Kaiming 初始化)⭐⭐⭐⭐⭐
-
适用激活函数:ReLU、LeakyReLU、PReLU(目前最常用的激活函数)
-
原理:根据输入神经元数量自动调整初始化范围,完美适配 ReLU
-
代码:
import torch.nn as nn # 定义一个线性层 linear = nn.Linear(784, 50) # He正态分布初始化(推荐首选) nn.init.kaiming_normal_(linear.weight, mode='fan_in', nonlinearity='relu') # 偏置初始化为0(默认就是这个,不用手动写) nn.init.zeros_(linear.bias)
✅ 2. Xavier 初始化(Glorot 初始化)
-
适用激活函数:Sigmoid、Tanh(现在用得少了)
-
代码:
nn.init.xavier_normal_(linear.weight)
✅ 3. 正态分布初始化
-
当上面两种都不适用时用,标准差设为 0.01
-
代码:
nn.init.normal_(linear.weight, mean=0.0, std=0.01)
2.3 初始化方法选择指南
| 激活函数 | 推荐初始化方法 |
|---|---|
| ReLU/LeakyReLU/PReLU | He 初始化 |
| Sigmoid/Tanh | Xavier 初始化 |
| 其他 | 正态分布(std=0.01) |
📌 一句话总结
用 ReLU 就用 He 初始化,用 Tanh 就用 Xavier 初始化。
🎯 三、Batch Normalization:让训练更稳定的神器
3.1 解决什么核心问题?
内部协变量偏移—— 大白话就是:训练过程中,每一层的输入分布一直在变。网络要不断适应新的分布,导致训练慢、不稳定。
就像你每天上学的路都在变,今天是平路,明天是山路,后天是水路,你肯定走不快。
3.2 BN 的三大超级优点
- 训练更快:可以用更高的学习率(比如从 0.01 升到 0.1)
- 不依赖初始化:即使初始化不好,BN 也能让训练正常进行
- 抑制过拟合:相当于加了一点正则化,减少过拟合
3.3 ✅ 正确使用方法(位置最重要!)
黄金位置:线性层 / 卷积层之后,激活函数之前
# ❌ 错误写法:激活函数在BN前面
model = nn.Sequential(
nn.Linear(784, 50),
nn.ReLU(),
nn.BatchNorm1d(50) # 错了!
)
# ✅ 正确写法:BN在激活函数前面
model = nn.Sequential(
nn.Linear(784, 50),
nn.BatchNorm1d(50), # 对了!
nn.ReLU()
)
不同层对应的 BN
- 全连接层 →
nn.BatchNorm1d - 图像卷积层 →
nn.BatchNorm2d - 视频 3D 卷积 →
nn.BatchNorm3d
3.4 ⚠️ 避坑提醒
- 训练时用
model.train(),测试时用model.eval(),BN 在这两种模式下行为不同 - 不要在太小的 batch size(<16)下用 BN,统计的均值方差不准
- BN 层不要和 Dropout 一起用太多,容易导致训练不稳定
📌 一句话总结
每个线性层 / 卷积层后面都加 BN,效果立竿见影。
📝 四、小白训练模型的黄金模板(直接复制)
把上面所有技巧整合到一个完整的训练代码里,手写数字识别、图像分类等入门任务直接用。
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader
from torch.optim.lr_scheduler import StepLR
# ---------------------- 1. 定义超参数 ----------------------
lr = 0.001 # Adam默认学习率
epochs = 50 # 训练轮数
batch_size = 256 # 批次大小
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# ---------------------- 2. 加载数据(替换成你的数据) ----------------------
# 这里用手写数字识别举例,你可以替换成自己的数据加载代码
from common.load_data import get_digit_data
x_train, x_test, y_train, y_test = get_digit_data()
# 构建数据集和数据加载器
train_dataset = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_dataset = TensorDataset(x_test, y_test)
val_loader = DataLoader(val_dataset, batch_size=batch_size)
# ---------------------- 3. 定义模型(集成所有优化技巧) ----------------------
model = nn.Sequential(
# 第一层:线性层 + BN + ReLU
nn.Linear(784, 50),
nn.BatchNorm1d(50),
nn.ReLU(),
# 第二层:线性层 + BN + ReLU
nn.Linear(50, 100),
nn.BatchNorm1d(100),
nn.ReLU(),
# 输出层
nn.Linear(100, 10)
).to(device)
# He初始化(只初始化线性层的权重)
for m in model.modules():
if isinstance(m, nn.Linear):
nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu')
nn.init.zeros_(m.bias)
# ---------------------- 4. 定义损失函数、优化器、学习率调度器 ----------------------
loss_fn = nn.CrossEntropyLoss() # 多分类任务用这个
optimizer = optim.Adam(model.parameters(), lr=lr) # 首选Adam
lr_scheduler = StepLR(optimizer, step_size=20, gamma=0.7) # 学习率衰减
# ---------------------- 5. 训练循环 ----------------------
for epoch in range(epochs):
# 训练模式
model.train()
train_loss = 0.0
train_acc = 0.0
for inputs, targets in train_loader:
inputs, targets = inputs.to(device), targets.to(device)
# 前向传播
outputs = model(inputs)
loss = loss_fn(outputs, targets)
# 反向传播 + 更新参数
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 统计损失和准确率
train_loss += loss.item() * inputs.size(0)
_, preds = torch.max(outputs, 1)
train_acc += torch.sum(preds == targets.data)
# 计算平均损失和准确率
train_loss /= len(train_dataset)
train_acc /= len(train_dataset)
# 验证模式
model.eval()
val_loss = 0.0
val_acc = 0.0
with torch.no_grad(): # 验证时不需要计算梯度
for inputs, targets in val_loader:
inputs, targets = inputs.to(device), targets.to(device)
outputs = model(inputs)
loss = loss_fn(outputs, targets)
val_loss += loss.item() * inputs.size(0)
_, preds = torch.max(outputs, 1)
val_acc += torch.sum(preds == targets.data)
val_loss /= len(val_dataset)
val_acc /= len(val_dataset)
# 更新学习率
lr_scheduler.step()
# 打印结果
print(f'Epoch {epoch+1}/{epochs}')
print(f'Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.4f}')
print(f'Val Loss: {val_loss:.4f} | Val Acc: {val_acc:.4f}')
print(f'Current LR: {optimizer.param_groups[0]["lr"]:.6f}')
print('-' * 50)
# 保存模型
torch.save(model.state_dict(), 'best_model.pth')
🎯 五、小白调参顺序(优先级从高到低)
- 先让模型跑通:没有报错,loss 能正常下降
- 加 BN 层:效果提升最明显,几乎没有副作用
- 换优化器:换成 Adam,lr=0.001
- 调整学习率:如果 loss 震荡,把 lr 减半;如果 loss 下降慢,把 lr 加倍
- 加学习率衰减:StepLR(step_size=20, gamma=0.7)
- 调整 batch size:一般 32-256 之间,越大越稳定
- 调整网络结构:增加 / 减少隐藏层神经元数量
📌 最后:小白最容易犯的 3 个错误
- 忘记梯度清零:
optimizer.zero_grad(),否则梯度会累加,训练完全错误 - 忘记切换模型模式:训练时
model.train(),测试时model.eval(),否则 BN 和 Dropout 会出错 - 学习率太大:上来就用 0.1 的学习率,导致 loss 直接变成 nan
更多推荐

所有评论(0)