Skip to content

训练:损失、优化器与调参 ​

这一篇是「能不能训出来」的分水岭。

架构是公开的,数据是你的,能拉开差距的是:知道训练为什么失败,以及怎么一项项排除。

一、损失函数:你在优化什么 ​

任务损失PyTorch
多分类交叉熵nn.CrossEntropyLoss()
二分类BCE(带 logits 版更稳)nn.BCEWithLogitsLoss()
回归MSE / L1 / SmoothL1nn.MSELoss() / nn.L1Loss()
类别不平衡加权交叉熵CrossEntropyLoss(weight=...)
排序/对比InfoNCE、Triplet手写
生成(LLM)下一个 token 的交叉熵同 CrossEntropy,位移一位
python
# 类别不平衡:按频次倒数加权
counts = torch.bincount(y_train)
w = (1.0 / counts).float()
w = w / w.sum() * len(w)
criterion = nn.CrossEntropyLoss(weight=w, label_smoothing=0.1)

label_smoothing=0.05~0.1 通常能小幅提分并缓解过自信,几乎无成本。

二、优化器:选哪个 ​

优化器特点建议
SGD + momentum泛化常更好,对 lr 敏感CV 任务、追求极致精度
AdamW自适应、好调、默认首选90% 场景起点
RMSProp老牌自适应少见
Lion / 8-bit Adam省显存 / 新式大模型微调
python
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.01)

weight_decay 要排除 bias 与 norm 层参数(进阶但重要):

python
decay, no_decay = [], []
for n, p in model.named_parameters():
    if not p.requires_grad: continue
    (no_decay if p.ndim < 2 else decay).append(p)
optimizer = torch.optim.AdamW([
    {"params": decay, "weight_decay": 0.01},
    {"params": no_decay, "weight_decay": 0.0},
], lr=3e-4)

三、学习率:最重要的超参 ​

经验起点 ​

场景lr
从头训小 CNN1e-3(SGD 可到 1e-2)
Adam/AdamW 微调1e-4 ~ 3e-4
全参数微调大模型1e-5 ~ 5e-5
LoRA 微调1e-4 ~ 2e-4
线性探测(只训分类头)1e-3

调度策略 ​

python
# 预热 + 余弦退火:现代训练标配
scheduler = torch.optim.lr_scheduler.OneCycleLR(
    optimizer, max_lr=1e-3,
    total_steps=len(train_dl) * epochs, pct_start=0.1,
)

for epoch in range(epochs):
    for xb, yb in train_dl:
        ...
        optimizer.step()
        scheduler.step()        # OneCycle 按 step 更新!
策略何时用
常数快速实验
余弦退火通用,收尾更平滑
OneCycle单次训练冲最好结果
ReduceLROnPlateau指标停滞时降 lr(省心)
线性预热Transformer / 大模型必备(防早期发散)

找学习率(1 分钟法) ​

从一个极小值开始每步指数放大,记录 loss,选 loss 下降最陡的那段中点。

python
lrs, losses = [], []
lr = 1e-7
for step, (xb, yb) in enumerate(train_dl):
    if step > 200: break
    for g in optimizer.param_groups: g["lr"] = lr
    loss = loss_fn(model(xb), yb)
    optimizer.zero_grad(); loss.backward(); optimizer.step()
    lrs.append(lr); losses.append(loss.item())
    lr *= 1.15
# 画图:横轴 log(lr),纵轴 loss,取最低点前 1~2 个数量级

四、正则化:对抗过拟合 ​

手段做法说明
数据增强随机裁剪/翻转/色彩抖动/ Mixup最有效,几乎必做
Dropoutnn.Dropout(0.1~0.3)小数据有用,大模型常关闭
Weight decay1e-2 ~ 5e-2与 AdamW 配合
早停验证指标 N 轮不升就停必做,省时间还防过拟合
标签平滑0.05~0.1便宜有效
减小模型降层数/宽度最后手段
python
from torchvision import transforms
train_tf = transforms.Compose([
    transforms.RandomResizedCrop(224, scale=(0.7, 1.0)),
    transforms.RandomHorizontalFlip(),
    transforms.ColorJitter(0.2, 0.2, 0.2),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),
])

验证集不能做增强

验证/测试只用 Resize + CenterCrop + Normalize。加了随机增强,指标会抖动且不可比。

五、故障诊断表(最有价值的部分) ​

现象最可能原因排查顺序
loss 完全不降lr 太大/太小;标签与输出错位;忘了 zero_grad用 1 个 batch 过拟合测试(见下)
loss 变 nanlr 太大;除零;log(0);fp16 溢出降 lr、加 eps、用 bf16
训练集 loss 高、验证也高欠拟合 / 模型太小 / 特征不足加大模型、训更久
训练集极低、验证高过拟合增强、正则、早停、更多数据
验证指标剧烈震荡batch 太小 / lr 太大 / 验证集太小增大 batch、降 lr
训练正常,推理结果全错忘了 eval();预处理不一致;没 to(device)对比训练与推理的预处理
显存 OOMbatch 太大 / 未 no_grad / 残留计算图降 batch、梯度累积、del + empty_cache

万能的「单批过拟合测试」 ​

任何训练异常,先做这个 —— 3 分钟定位 90% 的 bug:

python
xb, yb = next(iter(train_dl))
model.train()
for i in range(200):
    loss = loss_fn(model(xb), yb)
    optimizer.zero_grad(); loss.backward(); optimizer.step()
    if i % 40 == 0: print(i, loss.item())
# 期望:loss 迅速趋近 0,训练准确率 100%
  • 能过拟合一个 batch → 模型与训练流程没问题,问题在数据/容量/正则
  • 不能 → 代码有 bug(形状错、标签错、lr 错、梯度被切断)

六、混合精度与梯度累积 ​

python
# 混合精度:省显存、提速,几乎不掉精度
scaler = torch.amp.GradScaler("cuda")

for xb, yb in train_dl:
    with torch.autocast("cuda", dtype=torch.bfloat16):   # A100+ 用 bf16
        loss = loss_fn(model(xb), yb)
    optimizer.zero_grad()
    scaler.scale(loss).backward()
    scaler.step(optimizer)
    scaler.update()

# 梯度累积:显存不够时模拟大 batch
ACCUM = 4
for i, (xb, yb) in enumerate(train_dl):
    loss = loss_fn(model(xb), yb) / ACCUM
    loss.backward()
    if (i + 1) % ACCUM == 0:
        optimizer.step(); optimizer.zero_grad()

七、一个可复用的训练骨架 ​

python
def train(model, train_dl, val_dl, epochs=20, lr=3e-4, patience=3):
    device = next(model.parameters()).device
    opt = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=0.01)
    sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=epochs)
    crit = nn.CrossEntropyLoss(label_smoothing=0.1)

    best, best_state, bad = 0.0, None, 0
    history = []

    for ep in range(epochs):
        model.train()
        tr_loss = 0.0
        for xb, yb in train_dl:
            xb, yb = xb.to(device), yb.to(device)
            loss = crit(model(xb), yb)
            opt.zero_grad(); loss.backward()
            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)   # 防梯度爆炸
            opt.step()
            tr_loss += loss.item() * xb.size(0)
        sched.step()

        val_acc = evaluate(model, val_dl, device)
        history.append((ep, tr_loss / len(train_dl.dataset), val_acc))
        print(f"ep{ep+1} loss={history[-1][1]:.4f} val_acc={val_acc:.4f}")

        if val_acc > best:
            best, bad = val_acc, 0
            best_state = {k: v.clone() for k, v in model.state_dict().items()}
        else:
            bad += 1
            if bad >= patience:
                print(f"early stop at ep{ep+1}, best={best:.4f}")
                break

    if best_state: model.load_state_dict(best_state)
    return history

@torch.no_grad()
def evaluate(model, dl, device):
    model.eval()
    c = t = 0
    for xb, yb in dl:
        c += (model(xb.to(device)).argmax(1).cpu() == yb).sum().item()
        t += yb.size(0)
    return c / t

这段代码请背下来。 它覆盖了你未来 80% 的实验需求。

通过标准 ​

下一步 ​

→ CNN:卷积与视觉任务

AI 学习路线 · 从入门到专家