第一次处理信用卡欺诈检测数据时,我盯着屏幕上的统计数字愣住了——正常交易记录占比99.8%,欺诈交易仅占0.2%。这种极端不平衡的数据就像让小学生参加高考,模型很快学会了一个"万能答案":永远预测"正常交易",准确率就能达到99.8%!但这显然不是我们想要的结果。
不平衡数据在真实业务场景中比比皆是:医疗领域的罕见病诊断、制造业的缺陷产品检测、网络安全中的异常流量识别...传统交叉熵损失函数在这些场景下会陷入"多数派暴政",导致模型对少数类视而不见。我曾用标准交叉熵训练过一个医疗数据集,模型对肺炎的召回率只有可怜的12%,而健康样本的准确率却高达98%——这种"偏科"模型在临床上毫无价值。
问题的本质在于损失函数的计算方式。假设数据集中有1000个负样本和10个正样本,每个样本对总损失的贡献是均等的。即使模型把所有正样本都预测错误,带来的损失增加也只有10个单位,而正确预测负样本却能减少1000单位损失——模型自然会选择"弃车保帅"。
想象你在玩跷跷板:一端坐着体重较轻的少数类(比如欺诈交易),另一端是胖胖的多数类(正常交易)。标准损失函数就像把支点放在正中间,结果多数类永远把少数类翘在高空中。加权损失函数的妙处在于移动支点位置——给轻的一端加砝码(增加权重),直到两边达到平衡。
数学上,加权交叉熵损失可以表示为:
python复制Loss = -Σ [w_i * y_i * log(p_i)]
其中w_i就是我们要为每个类别精心设计的权重。这个简单的改动让模型意识到:错判一个欺诈交易的成本,可能相当于错判100个正常交易。
在实践中,我测试过三种常见的权重分配方法:
逆频率加权:最直观的方法
python复制weight = total_samples / (class_counts * num_classes)
比如在1000个样本中,A类100个,B类800个,C类100个,那么权重就是[10, 1.25, 10]
平滑逆频率:防止极端权重
python复制weight = (total_samples + α) / (class_counts + β)
加入平滑因子避免某个类别的权重过大
代价敏感学习:根据业务需求定制
医疗诊断中,漏诊癌症的成本可能是误诊的10倍:复制weight = [1, 10] # 阴性=1, 阳性=10
下表对比了这三种方法在一个电商异常订单检测中的效果:
| 方法 | 召回率提升 | 准确率下降 | 训练稳定性 |
|---|---|---|---|
| 标准交叉熵 | 基准 | 基准 | 高 |
| 逆频率加权 | +45% | -8% | 中 |
| 平滑逆频率(α=1) | +38% | -5% | 高 |
| 代价敏感(1:5) | +52% | -12% | 低 |
处理信用卡欺诈这类二分类问题时,BCEWithLogitsLoss是我的首选。它巧妙地将Sigmoid激活和交叉熵损失合二为一,特别要注意它的两个权重参数:
python复制import torch
import torch.nn as nn
# 方法1:使用weight参数(同时控制正负类)
class_weights = torch.tensor([0.1, 0.9]) # 负类=0.1, 正类=0.9
criterion = nn.BCEWithLogitsLoss(weight=class_weights)
# 方法2:使用pos_weight(仅放大正类)
pos_weight = torch.tensor([9.0]) # 正类权重是负类的9倍
criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)
这里有个坑我踩过:当同时指定weight和pos_weight时,PyTorch会优先使用pos_weight。曾经有个项目里我两个参数都设了,调试半天才发现权重没生效。
对于医疗影像分类这种多分类场景,CrossEntropyLoss的加权改造更灵活:
python复制# 计算类权重
class_counts = [100, 800, 100] # 三类样本量
total_samples = sum(class_counts)
weights = torch.tensor([total_samples/c for c in class_counts])
# 归一化权重(可选)
weights = weights / weights.sum() * len(weights)
criterion = nn.CrossEntropyLoss(weight=weights)
最近在一个皮肤癌分类项目中,加入权重后模型对罕见黑色素瘤的识别率从15%提升到了67%。关键技巧是权重归一化——让各类权重之和等于类别数,既保持平衡又避免梯度爆炸。
当数据集既不平衡又存在标注噪声时,可以组合使用label_smoothing和类权重:
python复制criterion = nn.CrossEntropyLoss(
weight=weights,
label_smoothing=0.1 # 平滑系数
)
这相当于给模型加了"双重保险":权重解决类别不平衡,标签平滑防止过拟合。在CT影像分类中,这种组合使模型在测试集上的F1分数提升了22%。
针对难样本挖掘,我常对标准加权损失进行Focal Loss改造:
python复制class WeightedFocalLoss(nn.Module):
def __init__(self, weights, gamma=2):
super().__init__()
self.weights = weights
self.gamma = gamma
def forward(self, inputs, targets):
BCE_loss = F.cross_entropy(inputs, targets, reduction='none')
pt = torch.exp(-BCE_loss)
focal_loss = (1-pt)**self.gamma * BCE_loss
return (focal_loss * self.weights[targets]).mean()
这个自定义损失在工业缺陷检测中表现惊艳——既照顾了类别不平衡,又让模型更关注难分类的缺陷样本。
切记:准确率在不平衡数据中毫无意义!我推荐这些指标组合:
最近评审一个论文时,作者声称模型在罕见病检测上达到99%准确率——细看才发现只是记住了多数类。这就是为什么我坚持要在验证集上逐类别检查指标。
加权损失可能带来训练不稳定,我的经验是:
python复制# 示例:带梯度裁剪的训练循环
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # 梯度裁剪
optimizer.step()
在金融风控系统中,这些技巧帮助我们将欺诈检测的召回率稳定在85%以上,同时保持误报率低于3%。