三、分类损失函数:交叉熵损失、Hinge损失与平方合页损失

分类任务最核心的问题是什么?

让模型输出的分数和真实标签“对齐”。但怎么才叫对齐?不同的损失函数,定义了不同的对齐标准。我这些年调模型踩得最深的坑,往往是选错了损失函数,结果模型怎么训都不收敛。下面我们一个一个掰开来看。

3.1 交叉熵损失(Cross-Entropy Loss)

这是目前分类问题里最常用的损失。它的核心思想是:把模型输出的 logits 转成概率分布(softmax),然后和真实的 one-hot 标签算 KL 散度。

公式很简洁:

L_CE = - Σ y_i * log(p_i)

其中 y_i 是真实标签(0或1),p_i 是 softmax 后的概率。对于二分类,退化为:

L_BCE = - [y * log(p) + (1 - y) * log(1 - p)]
我个人的习惯:对于多分类,直接使用 nn.CrossEntropyLoss(PyTorch)或 tf.losses.SparseCategoricalCrossentropy。这些 API 内部已经把 softmax 和损失计算合并了,数值更稳定。

交叉熵的优点很明显——梯度形式简单,更新方向正确。尤其是当模型预测和真实标签差距很大时,损失会很大,梯度也很大,能快速拉回模型。

但有一个坑:对噪声标签极度敏感。我曾经在一个医疗影像数据集上,因为标注员标错了几百张图,交叉熵直接让模型过拟合到那些噪声上,准确率死活上不去。后来换了鲁棒损失才搞定。

我曾经遇到过:用交叉熵训一个1000类分类模型,loss 曲线降到0.1后就纹丝不动了。查了半天发现是最后一个全连接层的 bias 初始化为0,导致某些类几乎不被激活。换成nn.Linear(..., bias=False),然后手动把 bias 初始化为 torch.log(1/1000),loss 立马往下掉。

3.2 Hinge损失(Hinge Loss)

Hinge 损失最早用在大间隔分类器(SVM)里。它的想法是:我不光要让分类正确,还要让正确类的分数比其他类高出至少一个“边距”(通常取1)。

对于二分类,标签 y ∈ {−1, +1},输出分数 f(x):

L_hinge = max(0, 1 - y * f(x))

看公式就知道:如果 y*f(x) ≥ 1,损失为0;否则损失线性增长。它只惩罚那些“不够安全”的样本。

特性 交叉熵 Hinge
输出解释性 概率意义明确 分数无概率解释
对离群点敏感度 高(指数惩罚) 中(线性惩罚)
梯度特性 处处非零梯度 对正确样本梯度为0
常用场景 深度学习分类、语言模型 SVM、度量学习

你想想看,Hinge 损失会让模型只关注边界附近的样本(支持向量)。这在小样本场景下反而能防止过拟合。但它在深度网络里不太流行,因为梯度很稀疏——很多样本的损失已经是0了,模型不再从它们身上学习。

避坑指南:我在做人脸识别时试过用 Hinge 损失来训练分类器,结果发现训练过程中大量样本的 loss 都是0,模型几乎不更新,收敛极慢。后来改成加一个 temperature 缩放后再算 Hinge,才缓解了这个问题。

3.3 平方合页损失(Squared Hinge Loss)

这是 Hinge 损失的一个变体。区别在于:当样本违规时,惩罚不再是线性,而是平方。

L_squared_hinge = max(0, 1 - y * f(x))^2

为什么会有这个版本?因为在 Hinge 损失下,离群点会产生巨大的梯度,偶尔会把模型带偏。平方项让监督惩罚更平滑,梯度是 2·max(0, 1-yf),而不是恒为 0 或 1。

我对比过三者在 CIFAR-10 上的训练曲线:

# 伪代码示意
import torch.nn as nn

# 交叉熵
ce_loss = nn.CrossEntropyLoss()(logits, labels)

# Hinge(需手动实现)
scores = logits.gather(1, labels.unsqueeze(1)).squeeze()
other_scores = logits.max(1)[0]  # 近似,实际需减去正确类
hinge_loss = torch.clamp(1 - scores + other_scores, min=0).mean()

# Squared Hinge
squared_hinge_loss = (torch.clamp(1 - scores + other_scores, min=0) ** 2).mean()

我项目中实际的选择策略:如果标签干净、类别均衡,无脑用交叉熵。如果标签有噪声、类别不均衡,可以试试平方合页。如果模型需要输出概率,则交叉熵是必选项。

3.4 三者的对比与选择逻辑

现在我们把三种损失的核心差异画成一张图,方便你建立直观感受。

分类损失函数核心脉络 交叉熵损失 Hinge损失 平方合页损失 概率输出 对噪声敏感 处处非零梯度 分数间隔 线性惩罚 稀疏梯度 分数间隔 平方惩罚 梯度连续 选择建议 标签干净 → 交叉熵 | 标签有噪声 → 平方合页 | 需要概率解释 → 交叉熵 间隔分类需求 → Hinge(SVM风格) | 小样本、大边距 → Hinge变体 三条路径对应三种不同的“对齐标准”

3.5 实际中的典型代码配置

我用 PyTorch 比较多,给你看看常用的写法:

# 交叉熵(最常用)
criterion_ce = nn.CrossEntropyLoss()
loss = criterion_ce(logits, labels)

# Hinge(二分类,标签需转为±1)
def hinge_loss(logits, labels_binary):
    # labels_binary: -1 或 1
    return torch.mean(torch.clamp(1 - labels_binary * logits, min=0))

# 平方合页
def squared_hinge_loss(logits, labels_binary):
    return torch.mean(torch.clamp(1 - labels_binary * logits, min=0) ** 2)

注意:对于多分类的 Hinge,常见做法是:正确类的分数至少要比任何错误类的分数大1,即多分类 Hinge 损失:

def multiclass_hinge(logits, labels):
    # labels: 0..C-1
    batch_size = logits.size(0)
    correct_scores = logits.gather(1, labels.unsqueeze(1)).squeeze(1)
    # 计算错误类中最大的分数
    wrong_scores = logits.clone()
    wrong_scores[range(batch_size), labels] = -1e9
    max_wrong = wrong_scores.max(dim=1)[0]
    loss = torch.clamp(1 - correct_scores + max_wrong, min=0).mean()
    return loss

小心数值精度!当 correct_scores 远大于 max_wrong 时,clamp 全部取0,梯度消失。如果你发现 loss 突然跳到0且不再变化,别担心,那只意味着模型已经把所有样本都分类正确且间隔足够大——训练可以停了。

3.6 训练稳定性的经验分享

损失函数不仅影响最终精度,更影响训练过程的稳定性。我自己的经验是:

  • 交叉熵:最稳定,学习率不好调时也基本能收敛。但如果出现 loss 震荡,可以考虑 label smoothing。
  • Hinge:配合 SGD 不加 momentum 时,训练曲线容易骤降骤升。建议用 Adam 或带 momentum 的 SGD。
  • 平方合页:梯度平方后比 Hinge 更平滑,但 Loss 值会偏大。需要适当降低学习率(我通常降到 Hinge 学习率的 1/2)。

记得有一次我切换了一个数据集,把分类损失从交叉熵换成平方合页,LR 忘了改,结果 loss 直接炸到 NaN。排查半天才发现问题。所以,切换损失函数后,务必重新扫一遍学习率。

好了,三种分类损失就讲到这里。你需要记住的是:没有绝对最好的损失,只有最合适的场景。下回遇到分类问题,先问问自己——你的标签干净吗?你需要概率输出吗?你对间隔有要求吗?答案会帮你选出正确的那个。