跳至内容
训练技巧与调优实战

训练技巧与调优实战

同样的模型架构,训练技巧的差距能让准确率相差 5–10%。这篇系统梳理优化器选择、学习率调度、正则化、混合精度训练等核心技巧,每条都给出可直接用的代码。

优化器:Adam 之后的选择

SGD vs Adam vs AdamW

import torch.optim as optim

# SGD + Momentum:计算机视觉经典选择,配合 lr warmup 效果出色
# 调得好能比 Adam 精度更高,但调参更麻烦
optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9,
                      weight_decay=1e-4, nesterov=True)

# Adam:默认首选,自适应学习率,调参简单
optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5)

# AdamW:Adam + 正确的权重衰减(推荐替代 Adam)
# Adam 的 weight_decay 实现有 bug(作用在梯度上而非参数上),AdamW 修复了这个问题
optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-2)
2025 年的经验值:NLP / 多模态用 AdamW;计算机视觉竞赛用 SGD + Cosine;快速实验和结构化数据用 Adam。默认从 AdamW 开始。

学习率调度

学习率是最重要的超参数,一成不变的 lr 不是最优的。

Warmup + Cosine 退火(最常用组合)

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

optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-2)

# Warmup:前 5 轮 lr 从 1e-4 线性增加到 1e-3(防止初期参数乱跳)
warmup_scheduler = LinearLR(optimizer, start_factor=0.1, end_factor=1.0, total_iters=5)
# 主调度:之后余弦退火(lr 从 1e-3 平滑降至 1e-5)
cosine_scheduler = CosineAnnealingLR(optimizer, T_max=95, eta_min=1e-5)
# 按顺序执行
scheduler = SequentialLR(optimizer, [warmup_scheduler, cosine_scheduler],
                         milestones=[5])

for epoch in range(100):
    train_one_epoch(...)
    scheduler.step()
    print(f"Epoch {epoch}: lr = {optimizer.param_groups[0]['lr']:.2e}")

OneCycleLR:超快收敛

# OneCycleLR:先升后降的三角形学习率,通常 10-20 轮就能收敛
scheduler = optim.lr_scheduler.OneCycleLR(
    optimizer,
    max_lr=1e-2,               # 学习率峰值
    steps_per_epoch=len(train_loader),
    epochs=30,
    pct_start=0.3,             # 30% 时间用于 warmup
    anneal_strategy="cos",
)

# 注意:OneCycleLR 每个 batch 调用一次,不是每个 epoch
for epoch in range(30):
    for X, y in train_loader:
        ...
        optimizer.step()
        scheduler.step()   # 每 batch 调用

手动查找最优学习率(LR Finder)

from torch_lr_finder import LRFinder

optimizer = optim.AdamW(model.parameters(), lr=1e-7)
lr_finder = LRFinder(model, optimizer, criterion, device=device)
lr_finder.range_test(train_loader, end_lr=10, num_iter=100)
lr_finder.plot()  # 找曲线损失下降最陡处对应的 lr
lr_finder.reset()
# 通常选「损失最低点前一个数量级」作为初始 lr

正则化技巧

Dropout 的正确用法

class RegularizedNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(256, 512),
            nn.ReLU(),
            nn.Dropout(p=0.3),    # 训练时随机关闭 30% 神经元

            nn.Linear(512, 256),
            nn.ReLU(),
            nn.Dropout(p=0.3),

            nn.Linear(256, 10),
        )

    def forward(self, x):
        return self.net(x)

# 关键:训练和评估模式要切换
model.train()  # Dropout 生效
model.eval()   # Dropout 关闭(用全部神经元)

Label Smoothing:让模型不过于自信

# 普通 CrossEntropyLoss:正确类目标概率 1.0,其余 0.0
# Label Smoothing:正确类目标概率 0.9,其余均分剩余 0.1
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)

Stochastic Depth(随机深度)

class StochasticDepthResBlock(nn.Module):
    """训练时随机跳过整个残差块,相当于集成了不同深度的网络"""
    def __init__(self, block, survival_prob=0.8):
        super().__init__()
        self.block = block
        self.survival_prob = survival_prob

    def forward(self, x):
        if self.training:
            if torch.rand(1).item() > self.survival_prob:
                return x  # 跳过这个块
        return x + self.block(x) * (1 / self.survival_prob)

数据增强

import torchvision.transforms as T
from torchvision.transforms import v2  # 新版 API,支持批量增强

# 图像分类的强增强组合
train_transform = T.Compose([
    T.RandomResizedCrop(224, scale=(0.08, 1.0)),
    T.RandomHorizontalFlip(),
    T.RandAugment(num_ops=2, magnitude=9),  # 自动策略搜索的增强
    T.ToTensor(),
    T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
    T.RandomErasing(p=0.25),  # 随机擦除一块区域
])

# Mixup:把两张图像按比例混合(标签也混合)
mixup = v2.MixUp(num_classes=1000, alpha=0.2)

for X, y in train_loader:
    X, y = mixup(X, y)  # y 变成了软标签
    ...

混合精度训练(FP16 / BF16)

混合精度把计算换到 FP16(半精度),显存占用减半,训练速度提升 1.5–3 倍,精度几乎不损失:

import torch
from torch.cuda.amp import GradScaler, autocast

scaler = GradScaler()  # 用于防止 FP16 下溢(梯度太小变成 0)

for X, y in train_loader:
    X, y = X.to(device), y.to(device)
    optimizer.zero_grad()

    with autocast(dtype=torch.float16):  # 这个 with 块内自动用 FP16 计算
        output = model(X)
        loss   = criterion(output, y)

    scaler.scale(loss).backward()        # 放大梯度(防止下溢)
    scaler.unscale_(optimizer)
    nn.utils.clip_grad_norm_(model.parameters(), 1.0)
    scaler.step(optimizer)               # 缩放回 FP32 再更新参数
    scaler.update()
A100 / H100 等新一代 GPU 对 BF16(Brain Float 16)更友好——数值范围和 FP32 一样,不需要 GradScaler:autocast(dtype=torch.bfloat16)。有 A100+ 就用 BF16;V100 及以下用 FP16。

梯度累积:模拟大 batch 训练

显存不够,无法用大 batch?用梯度累积模拟:

accumulation_steps = 8   # 等效 batch_size = 实际 batch × 8

for i, (X, y) in enumerate(train_loader):
    X, y = X.to(device), y.to(device)

    with autocast():
        loss = criterion(model(X), y) / accumulation_steps  # 每步 loss 除以累积步数

    scaler.scale(loss).backward()

    if (i + 1) % accumulation_steps == 0:
        scaler.unscale_(optimizer)
        nn.utils.clip_grad_norm_(model.parameters(), 1.0)
        scaler.step(optimizer)
        scaler.update()
        optimizer.zero_grad()    # 每 accumulation_steps 步才清零

梯度裁剪

RNN / Transformer 容易梯度爆炸,训练前加一行:

nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# 或对单个参数组
nn.utils.clip_grad_value_(model.parameters(), clip_value=0.5)

调参策略

调参顺序超参数推荐范围说明
1学习率1e-4 ~ 1e-2最重要,先用 LR Finder 确定
2batch_size32 ~ 512影响梯度方差,大 batch 需提高 lr
3weight_decay1e-4 ~ 1e-1AdamW 下适当放大
4dropout0.1 ~ 0.5过拟合时调大
5训练轮数按 early stopping不要硬猜,靠验证集决定

调参原则:每次只改一个超参数,对比前后效果;建立实验记录(用 MLflow 或 W&B)。

完整训练模板

def train_model(model, train_loader, val_loader, config):
    optimizer = optim.AdamW(model.parameters(),
                            lr=config["lr"], weight_decay=config["wd"])
    scheduler = optim.lr_scheduler.OneCycleLR(
        optimizer, max_lr=config["lr"],
        steps_per_epoch=len(train_loader), epochs=config["epochs"]
    )
    scaler    = GradScaler()
    criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
    best_val_acc = 0.0

    for epoch in range(config["epochs"]):
        model.train()
        for X, y in train_loader:
            X, y = X.to(device), y.to(device)
            optimizer.zero_grad()
            with autocast():
                loss = criterion(model(X), y)
            scaler.scale(loss).backward()
            scaler.unscale_(optimizer)
            nn.utils.clip_grad_norm_(model.parameters(), 1.0)
            scaler.step(optimizer)
            scaler.update()
            scheduler.step()

        val_acc = evaluate(model, val_loader)
        if val_acc > best_val_acc:
            best_val_acc = val_acc
            torch.save(model.state_dict(), "best_model.pt")

    return best_val_acc

一句话小结

好的训练 = AdamW + Warmup-Cosine 调度 + Label Smoothing + 混合精度 + 梯度裁剪。这五件套是 2025 年的「默认最佳实践」,不需要每次从零摸索,直接用模板跑起来,再按需微调。

最后更新于