共计 2473 个字符,预计需要花费 7 分钟才能阅读完成。
背景痛点:为什么损失函数这么重要?
最近在做一个信用卡欺诈检测的二分类项目时,遇到了一个典型问题:模型对正常交易的识别准确率高达 99.9%,但就是抓不到欺诈交易。后来发现是用了不合适的损失函数导致模型「偷懒」了——因为数据中正常样本占 99%,模型只要永远预测「正常」就能获得很低的整体损失值。这就是错误选择损失函数导致的模型欠拟合典型案例。

二分类任务中常见的坑还有:
- 使用 MSE 损失导致梯度消失(特别是 sigmoid 输出时)
- 类不平衡时未调整 class_weight 导致决策边界偏移
- 错误组合激活函数和损失函数(如用 softmax 配 BCELoss)
数学原理:两大主流损失函数对比
1. 交叉熵损失(BCELoss)
公式看起来简单:
$L = -[y\log(p) + (1-y)\log(1-p)]$
但它的梯度特性很有意思。假设用 sigmoid 激活,对 logits 的梯度为:
$\frac{\partial L}{\partial z} = p – y$
这意味着:
– 当预测完全错误时(y=1,p≈0),梯度绝对值趋近 1
– 预测越接近真实值,梯度越小
2. 合页损失(Hinge Loss)
常用于 SVM 风格的模型:
$L = \max(0, 1 – y\cdot z)$ 其中 $y\in{-1,1}$
它的梯度特性完全不同:
- 当 $y\cdot z \geq 1$ 时,梯度为 0(因为此时样本已在决策边界正确一侧)
- 否则梯度为 $-y$
关键区别:
- 交叉熵对错误分类持续施加梯度
- 合页损失存在「梯度消失区域」(预测足够正确时停止更新)
PyTorch 实战对比
基础代码框架
import torch
from torch import nn
import matplotlib.pyplot as plt
# 显式指定 device
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
1. 带权重的 BCEWithLogitsLoss
处理不平衡数据的经典方案:
pos_weight = torch.tensor([10.0]).to(device) # 假设正样本少 10 倍
criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)
# 前向计算示例
logits = model(inputs) # shape: [batch_size, 1]
loss = criterion(logits.squeeze(), labels.float())
2. Focal Loss 改造
解决难易样本不平衡问题:
class FocalLoss(nn.Module):
def __init__(self, alpha=0.25, gamma=2.0):
super().__init__()
self.alpha = alpha
self.gamma = gamma
self.bce = nn.BCEWithLogitsLoss(reduction='none')
def forward(self, inputs, targets):
# inputs: [N,1], targets: [N]
bce_loss = self.bce(inputs.squeeze(), targets)
pt = torch.exp(-bce_loss) # p when y=1, 1-p otherwise
loss = self.alpha * (1-pt)**self.gamma * bce_loss
return loss.mean()
3. 训练过程可视化
def plot_metrics(history):
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12,4))
ax1.plot(history['train_loss'], label='Train')
ax1.plot(history['val_loss'], label='Val')
ax1.set_title('Loss Curve')
ax2.plot(history['train_acc'], label='Train')
ax2.plot(history['val_acc'], label='Val')
ax2.set_title('Accuracy Curve')
plt.show()
生产环境建议
- 极端类别不平衡处理 :
-
当正负样本比 >1:100 时,建议采用:
# 方法 1:pos_weight 设为逆类别频率 pos_weight = neg_count / pos_count # 方法 2:过采样 + 欠采样组合 -
激活函数匹配原则 :
- BCEWithLogitsLoss 已包含 sigmoid,不要再额外加激活层
- 使用普通 BCELoss 时必须手动 sigmoid
-
Hinge Loss 要求输出层无激活函数
-
多标签分类场景 :
- 每个标签独立二分类:用
nn.BCEWithLogitsLoss - 需要概率校准:添加 label smoothing
# 标签平滑示例 smoothed_labels = labels * (1 - 0.1) + 0.05 # ϵ=0.1
性能对比
在 MNIST 二分类任务(区分数字 0 /1)上的测试结果:
| 损失函数 | GPU 显存占用 | 每 epoch 时间 | 最佳准确率 |
|---|---|---|---|
| BCEWithLogits | 1243MB | 45s | 99.8% |
| Hinge Loss | 1189MB | 43s | 99.2% |
| Focal Loss | 1265MB | 47s | 99.7% |
经验总结
经过多个项目的实践,我的三点核心体会:
- 不要盲目选择损失函数 :交叉熵在大多数情况下表现良好,但当遇到:
- 需要最大间隔分类(如人脸识别)→ 用 Hinge Loss
-
存在极端样本不平衡 → 用 Focal Loss
-
注意概率校准 :如果下游业务需要精确的概率值(如风控评分),务必:
- 避免使用未经校准的 Hinge Loss
-
在验证集上检查概率直方图
-
监控梯度健康度 :特别是使用自定义损失时,建议添加:
# 在训练循环中检查梯度 if torch.isnan(loss).any(): print('WARNING: NaN in loss!')
最后分享一个实用技巧:当不确定用什么损失函数时,先用默认参数的 BCEWithLogitsLoss 快速建立 baseline,再根据其失败模式选择更专业的损失函数。
