深度学习训练优化技巧保姆级笔记

一句话定位:解决 “模型跑通了但效果差、训练慢、总报错” 的终极指南。所有内容都经过实战验证,代码复制就能用,看完直接从 “调包侠” 升级为 “会调参的调包侠”。


🎯 先搞懂:我们到底在优化什么?

一个最形象的比喻

  • 损失函数 = 一座凹凸不平的大山
  • 我们的目标 = 走到山谷底部(损失最小的地方)
  • 模型参数 = 我们在山上的 “位置坐标”
  • 优化器 = 我们的 “下山向导”,负责告诉我们:
    • 往哪个方向走(梯度方向)
    • 每一步走多大(学习率)

普通 SGD 的三大痛点(为什么需要优化)

  1. 震荡大:走 Z 字形路线,绕远路
  2. 收敛慢:步子太小,走半天到不了
  3. 容易卡住:卡在小土坡(局部最优)就以为到了山谷

这一章的所有技巧,都是为了让你的 “向导” 变得更聪明、更稳、更快。


🚀 一、优化器大比拼:谁是最好的下山向导?

核心结论(小白直接抄)

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 为什么不能随便初始化?

❌ 绝对不能做的事
  1. 全 0 初始化:所有参数都一样,反向传播时所有参数更新也一样,等于只有一个神经元在工作

    nn.init.zeros_(linear.weight)  # 绝对不要这么写!
    
  2. 太大的初始化:导致梯度爆炸(loss 变成 nan)

  3. 太小的初始化:导致梯度消失(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 的三大超级优点

  1. 训练更快:可以用更高的学习率(比如从 0.01 升到 0.1)
  2. 不依赖初始化:即使初始化不好,BN 也能让训练正常进行
  3. 抑制过拟合:相当于加了一点正则化,减少过拟合

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')

🎯 五、小白调参顺序(优先级从高到低)

  1. 先让模型跑通:没有报错,loss 能正常下降
  2. 加 BN 层:效果提升最明显,几乎没有副作用
  3. 换优化器:换成 Adam,lr=0.001
  4. 调整学习率:如果 loss 震荡,把 lr 减半;如果 loss 下降慢,把 lr 加倍
  5. 加学习率衰减:StepLR(step_size=20, gamma=0.7)
  6. 调整 batch size:一般 32-256 之间,越大越稳定
  7. 调整网络结构:增加 / 减少隐藏层神经元数量

📌 最后:小白最容易犯的 3 个错误

  1. 忘记梯度清零optimizer.zero_grad(),否则梯度会累加,训练完全错误
  2. 忘记切换模型模式:训练时model.train(),测试时model.eval(),否则 BN 和 Dropout 会出错
  3. 学习率太大:上来就用 0.1 的学习率,导致 loss 直接变成 nan

更多推荐