CE损失函数深度解析:从数学原理到PyTorch实战

1次阅读
没有评论

共计 2082 个字符,预计需要花费 6 分钟才能阅读完成。

image.webp

背景痛点

在分类任务中,选择合适的损失函数至关重要。很多初学者会自然而然地想到使用均方误差(MSE)损失,因为它简单直观。但实际上,MSE 损失在分类问题中存在几个明显的理论缺陷:

CE 损失函数深度解析:从数学原理到 PyTorch 实战

  • MSE 损失对异常值过于敏感,容易受到离群点的影响
  • 当使用 sigmoid/softmax 激活时,MSE 会导致梯度消失问题,特别是在预测接近 0 或 1 时
  • MSE 假设误差服从高斯分布,而分类问题中我们更关心的是概率分布的差异

这些缺陷使得 MSE 在分类任务中表现不佳,而交叉熵(CE)损失则成为更自然的选择。

数学原理

交叉熵源于信息论中的 KL 散度(Kullback-Leibler divergence),它衡量两个概率分布之间的差异。对于真实分布 P 和预测分布 Q,交叉熵定义为:

$$
H(P,Q) = -\sum_x P(x)\log Q(x)
$$

在分类问题中,我们通常有:

  1. 真实分布:one-hot 编码的标签,即 $P(y)=1$ 当且仅当 y 是真实类别
  2. 预测分布:模型输出的 softmax 概率 $Q(y)=\frac{e^{z_y}}{\sum_j e^{z_j}}$

因此,CE 损失可以简化为:

$$
L = -\log(\frac{e^{z_y}}{\sum_j e^{z_j}})
$$

这个公式有几个重要特性:

  • 当预测概率接近 1 时,损失趋近于 0
  • 当预测概率接近 0 时,损失趋近于无穷大
  • 梯度与误差成正比,避免了 MSE 的梯度消失问题

实现对比

1. 手动实现

import torch
import torch.nn.functional as F

def manual_ce_loss(logits: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
    """
    logits: (batch_size, num_classes)
    targets: (batch_size,) 包含类别索引
    """
    log_probs = F.log_softmax(logits, dim=1)  # 对 logits 取 log_softmax
    return F.nll_loss(log_probs, targets)     # 负对数似然损失

2. 直接调用 nn.CrossEntropyLoss

ce_loss = torch.nn.CrossEntropyLoss()
loss = ce_loss(logits, targets)  # 内部自动组合了 softmax+log+nll_loss

3. 带标签平滑的实现

def label_smoothing_ce(logits: torch.Tensor, targets: torch.Tensor, 
                      epsilon: float = 0.1) -> torch.Tensor:
    num_classes = logits.size(1)
    # 将 one-hot 标签转换为平滑分布
    smoothed_targets = torch.full_like(logits, epsilon / (num_classes - 1))
    smoothed_targets.scatter_(1, targets.unsqueeze(1), 1 - epsilon)

    log_probs = F.log_softmax(logits, dim=1)
    return -(smoothed_targets * log_probs).sum(dim=1).mean()

实验环节

我们使用 PyTorch Lightning 在 CIFAR-10 上比较不同学习率下的 CE 损失曲线:

  1. 准备数据集和数据加载器
  2. 定义简单的 CNN 模型
  3. 使用不同的学习率 (0.1, 0.01, 0.001) 训练
  4. 记录并可视化训练过程中的损失变化

实验结果表明:

  • 学习率过大 (0.1) 时,损失震荡剧烈
  • 学习率过小 (0.001) 时,收敛速度慢
  • 适中学习率 (0.01) 能在稳定性和收敛速度间取得平衡

生产建议

1. 处理数值溢出

当 logits 绝对值很大时,直接计算 softmax 可能导致数值不稳定。解决方案:

logits = logits - logits.max(dim=1, keepdim=True)[0]  # 减去最大值

2. 类别不平衡处理

# 根据类别频率计算权重
class_weights = 1.0 / torch.tensor(class_counts)
loss = torch.nn.CrossEntropyLoss(weight=class_weights)

3. 多标签分类

对于多标签问题(一个样本可能属于多个类别),应该使用 sigmoid+BCE:

loss = torch.nn.BCEWithLogitsLoss()

4. 分布式训练

在 DDP 训练中,需要确保所有进程的损失计算一致:

torch.distributed.all_reduce(loss, op=torch.distributed.ReduceOp.SUM)
loss = loss / torch.distributed.get_world_size()

延伸思考

当验证集 loss 下降但准确率停滞时,可能的原因包括:

  1. 模型学会了某些欺骗性的模式(如利用背景信息)
  2. 数据存在标注噪声
  3. 验证集分布与训练集有差异
  4. 模型容量不足,无法学习更有意义的特征

这个问题没有标准答案,欢迎在评论区分享你的见解和经验!

正文完
 0
评论(没有评论)