首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【Debug日志 | 交叉熵与softmax】

【Debug日志 | 交叉熵与softmax】

原创
作者头像
九年义务漏网鲨鱼
发布2025-09-27 14:41:50
发布2025-09-27 14:41:50
12500
代码可运行
举报
文章被收录于专栏:tencent cloudtencent cloud
运行总次数:0
代码可运行

交叉熵前先 softmax?训练慢、梯度稀、指标上不去的元凶复盘(附可复现实验与修复模板)

在我们做多类单选分类(B, C × B)的场景中,通常的做法是把模型输出先过一层 softmax,再送进 CrossEntropyLoss;或者在用了 label smoothing 的同时仍然手动 softmax。一开始似乎也能降点 loss,但很快收敛变慢、梯度稀得离谱,验证集准确率长期不上 70%

❓ Bug 现象

  • 训练初期 loss 能降,之后平台化或震荡;验证准确率长时间不上升。
  • 梯度范数偏小且波动大,出现“怎么调学习率都不对”的错觉。
  • 打印 logits 发现其数值范围被人为压扁(经 softmax 后再被 cross_entropy 误当 logits 二次 softmax)。
  • 混用 label smoothing 时更糟,等效上是双重平滑。

📽️ 场景复现

代码语言:javascript
代码运行次数:0
运行
复制
import torch, torch.nn as nn, torch.nn.functional as F
torch.manual_seed(0)

class MLP(nn.Module):
    def __init__(self, d_in=20, d_hidden=64, n_class=5):
        super().__init__()
        self.f = nn.Sequential(nn.Linear(d_in, d_hidden), nn.ReLU(), nn.Linear(d_hidden, n_class))
    def forward(self, x): return self.f(x)  # 返回原始 logits

def make_loader(n=6000, bs=64):
    # 构造线性可分的 5 类问题(可快速收敛)
    X = torch.randn(n, 20)
    w = torch.randn(20, 5); b = torch.randn(5)
    y = (X @ w + b).argmax(dim=1)
    ds = torch.utils.data.TensorDataset(X, y)
    return torch.utils.data.DataLoader(ds, batch_size=bs, shuffle=True, drop_last=True)

def train(bug=True, steps=300, smoothing=0.1):
    model = MLP()
    opt = torch.optim.AdamW(model.parameters(), lr=3e-3)
    loader = make_loader()
    it = iter(loader)
    losses, accs = [], []

    for step in range(1, steps+1):
        try: x, y = next(it)
        except StopIteration:
            it = iter(loader); x, y = next(it)

        logits = model(x)                   # [B, C]
        if bug:
            # 错误示范:先 softmax,再交叉熵;还叠加 label_smoothing → 双重平滑
            prob = torch.softmax(logits, dim=-1)
            loss = F.cross_entropy(prob, y, label_smoothing=smoothing)
        else:
            # 正确示范:直接把 logits 给交叉熵;平滑由函数内部完成
            loss = F.cross_entropy(logits, y, label_smoothing=smoothing)

        opt.zero_grad(set_to_none=True)
        loss.backward()
        opt.step()

        with torch.no_grad():
            pred = logits.argmax(dim=1)     # 指标计算时才用 argmax/softmax
            acc = (pred == y).float().mean().item()
        losses.append(loss.item()); accs.append(acc)

        if step % 50 == 0:
            # 观测梯度与 logits 的范围
            gnorm = 0.0
            for p in model.parameters():
                if p.grad is not None: gnorm += p.grad.norm().item()
            print(f"[{'BUG' if bug else 'FIX'}] step={step:03d} loss={loss.item():.3f} acc={acc:.3f} gnorm≈{gnorm:.2f} "
                  f"logits_range=({float(logits.min()):.2f},{float(logits.max()):.2f})")

if __name__ == "__main__":
    print("== 错误用法:softmax -> CrossEntropy ==")
    train(bug=True)
    print("\n== 正确用法:logits -> CrossEntropy ==")
    train(bug=False)

你会看到

  • 错误用法下,loss 降得慢,gnorm 偏小且波动,acc 提升缓慢。
  • 正确用法下,loss 稳定下降,acc 很快超过错误用法。

后果说明

  • CrossEntropyLoss 内部做的是 log_softmax + NLLLoss;你提前 softmax 等于让“概率”再次被 softmax,梯度更加平滑、可分性被削弱。
  • 若再叠加 label smoothing,会把目标与预测两端同时平滑,难上加难。
  • 早期阶段看似“更稳”,本质是把学习信号给抹平了。

Debug 过程

  1. 检查损失输入的数值范围 打印传入 cross_entropy 前张量的 min/max。如果落在 0,1 且各样本和为 1,那你传进去的是概率而不是 logits。
  2. 审核损失定义 CrossEntropyLoss 期望未归一化的分数(logits),不要手动 softmax。若你确实需要基于概率的损失,请使用自定义的 soft-target 版本(例如对 Mixup/CutMix)。
  3. 观察梯度 在几层关键网络上挂 register_hook 打印梯度范数。错误用法下梯度更小、更稀,变化慢。
  4. 回归最小可复现 用上面的脚本做 A/B;若你的项目中仍看不出差异,极大可能是其它地方还有正则或调度配置问题,请先把这些对齐。

代码修改

  1. 训练时不要 softmax,直接把 logits 喂给交叉熵
代码语言:javascript
代码运行次数:0
运行
复制
logits = model(x)                      # [B, C]
loss = F.cross_entropy(logits, y, label_smoothing=0.1)  # 可选平滑
  1. 只在推理或指标阶段使用 softmax/argmax
代码语言:javascript
代码运行次数:0
运行
复制
with torch.no_grad():
    prob = torch.softmax(logits, dim=-1)
    pred = prob.argmax(dim=-1)
  1. 若是软标签(Mixup/CutMix 等),用 soft cross-entropy 而非 CrossEntropyLoss
代码语言:javascript
代码运行次数:0
运行
复制
def soft_cross_entropy(logits, soft_targets):
    logp = F.log_softmax(logits, dim=-1)
    return -(soft_targets * logp).sum(dim=-1).mean()
  1. 若你喜欢 NLLLoss 的写法,搭配 log_softmax 而不是 softmax
代码语言:javascript
代码运行次数:0
运行
复制
logp = F.log_softmax(logits, dim=-1)
loss = F.nll_loss(logp, y)  # 与 cross_entropy 等价

Q & A

  • 什么时候该用 softmax 仅在推理或需要概率的地方,例如阈值化、可视化、蒸馏的 teacher 分布。训练的 CE 不需要你显式 softmax。
  • label smoothing 会不会和 CE 冲突 不冲突,CE 内部对 logits 做 log_softmax,再把 one-hot 目标平滑成混合分布;但请不要同时对预测也 softmax。
  • 多标签任务能用 CE 吗 多标签是多个独立二分类,应该用 BCEWithLogitsLoss,B, C × B, C。CE 用于单标签多类,B, C × B。

结语

交叉熵前手动 softmax 看似“更符合直觉”,却会在训练中两次归一化分布,让可分性与梯度被抹平。把 softmax 从训练损失路径中移除,只在评估阶段使用;对需要的软目标采用 soft cross-entropy 或 KLDivLoss。配合上面的可复现实验与守卫函数,这个常年高频的误用可以一次性根治。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 交叉熵前先 softmax?训练慢、梯度稀、指标上不去的元凶复盘(附可复现实验与修复模板)
    • ❓ Bug 现象
    • 📽️ 场景复现
    • Debug 过程
    • 代码修改
    • Q & A
    • 结语
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档