共计 2082 个字符,预计需要花费 6 分钟才能阅读完成。
背景痛点
在分类任务中,选择合适的损失函数至关重要。很多初学者会自然而然地想到使用均方误差(MSE)损失,因为它简单直观。但实际上,MSE 损失在分类问题中存在几个明显的理论缺陷:

- 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)
$$
在分类问题中,我们通常有:
- 真实分布:one-hot 编码的标签,即 $P(y)=1$ 当且仅当 y 是真实类别
- 预测分布:模型输出的 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 损失曲线:
- 准备数据集和数据加载器
- 定义简单的 CNN 模型
- 使用不同的学习率 (0.1, 0.01, 0.001) 训练
- 记录并可视化训练过程中的损失变化
实验结果表明:
- 学习率过大 (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 下降但准确率停滞时,可能的原因包括:
- 模型学会了某些欺骗性的模式(如利用背景信息)
- 数据存在标注噪声
- 验证集分布与训练集有差异
- 模型容量不足,无法学习更有意义的特征
这个问题没有标准答案,欢迎在评论区分享你的见解和经验!
正文完
发表至: 未分类
近一天内
