欺诈率 0.1%、故障率 1%、发病率 0.01%——这些场景的正样本少到直接训会「学成聋子」。类别不平衡不是加几行代码就能绕过的,它要求你重新思考采样、损失、模型与评估一整条链路。
不平衡的本质与度量
不平衡问题的核心是少数类样本太少,模型学不到它的模式。当正样本占 0.1%,一个「永远预测负」的分类器准确率就有 99.9%——这就是为什么准确率在这个场景没有意义。
先建立正确的度量框架:
- 混淆矩阵:TP/FP/FN/TN 四格是一切指标的地基。
- PR-AUC(Precision-Recall 曲线下面积):少数类检索场景的第一指标,对类别分布不敏感。
- F1:精度与召回率的调和平均,平衡点。
- Recall@K:Top-K 里召回多少正样本,广告/推荐/风控场景最实用。
# 用 PR-AUC 而非 accuracy 评估少数类
from sklearn.metrics import average_precision_score
# ap = average_precision_score(y_true, y_score)
# 一个"全猜负"的模型: accuracy 99.9% 但 PR-AUC 极低 → 问题被立即暴露
规则很简单:凡是少数类重要,就抛弃 accuracy,看 PR-AUC / Recall@K。评估指标选错,调优方向全错。
重采样:欠采样、过采样与 SMOTE
最直接的应对是改数据分布:
- 欠采样(Undersampling):丢弃多数类样本,训练快但丢信息。进阶用「保留多数类中离决策边界最近的」或集成多份欠采样子集(EasyEnsemble)。
- 过采样(Oversampling):复制少数类样本,简单但会过拟合(同一批样本反复出现)。
- SMOTE(Synthetic Minority Oversampling):在少数类样本之间插值合成新样本,缓解过拟合。它假定「少数类样本间连线处大概率仍是少数类」。
# SMOTE 使用示例
from imblearn.over_sampling import SMOTE
from imblearn.pipeline import Pipeline
pipe = Pipeline([
("smote", SMOTE(sampling_strategy=0.3, k_neighbors=5)),
("clf", RandomForestClassifier(...)),
])
# sampling_strategy=0.3 → 过采样后正负比 3:10
SMOTE 的变体针对不同痛点:Borderline-SMOTE 在边界样本附近合成(更贴决策边界)、ADASYN 按「难学程度」分配生成数量(对难区分的少数类多合成)。要留意 SMOTE 的假设在高维稀疏数据(如文本 BoW)上不成立——邻居距离失真,合成样本可能落入「非少数类区域」。
集成采样与混合策略
单一重采样往往不够,工程上常用集成与混合:
- 混合采样:过采样少数类 + 欠采样多数类同时做,两个方向各退一步,保留更多信息。
- EasyEnsemble / BalanceCascade:把多数类随机分成 K 份,每份配全部少数类训练一个子模型,最后集成投票。既保留多数类信息又避免过采样过拟合。
- 重采样 + 模型正则:过采样后少数类反复出现会过拟合,配合 dropout/正则/早停可缓解。
# EasyEnsemble 思想(示意)
# for i in range(K):
# 子集 = 多数类随机抽样一份 + 全部少数类
# 子模型 = train(子集)
# 最终预测 = 投票/平均
经验法则:样本量少用过采样 + 正则;样本量够用集成欠采样;再叠上模型侧(损失/加权)手段,多管齐下。
代价敏感学习与 Focal Loss
改数据之外,还可以改损失函数让模型「更在意少数类」:
- 类别加权:
class_weight把少数类 loss 乘大权重,等价于隐式过采样,最易实现。 - Focal Loss:在交叉熵基础上,对已分对的样本降权(
(1-p)^γ),让模型把注意力集中到难分类的少数类上。γ 越大对易样本抑制越强。 - 代价敏感决策阈值:训练后按业务成本调阈值——不是死守 0.5。
# Focal Loss 的简化实现
def focal_loss(logits, target, gamma=2.0, alpha=0.25):
ce = F.binary_cross_entropy_with_logits(logits, target)
p = torch.sigmoid(logits)
pt = torch.where(target == 1, p, 1 - p)
return (alpha * (1 - pt) ** gamma * ce).mean()
阈值调优是常被忽略但回报最高的一步:训练完在验证集上扫阈值,找到业务成本最优的切点。比如欺诈误报一次成本 5 元、漏报一次成本 100 元,最优阈值会把召回率拉到很高而接受一些误报——这比任何采样技巧都直接。
异常检测的统计方法
类别不平衡的一种特殊情况是异常检测(Anomaly Detection):正样本(异常)太少甚至没有标签,转为无监督/半监督建模。经典方法分层:
- 统计方法:Z-score、IQR、Mahalanobis 距离——单/低维数据快速基线。
- 隔离森林(Isolation Forest):用随机切分「孤立点更易被切出来」的思想,高维鲁棒,是首选基线。
- 单类 SVM(One-Class SVM):学习「正常」的边界,高维核技巧但有参数敏感性。
- 深度方法:Autoencoder 重构误差(重构失败 = 异常)、Deep SVDD、基于分布的方法。
# 隔离森林快速示例
from sklearn.ensemble import IsolationForest
model = IsolationForest(contamination=0.01) # 预期异常比例
model.fit(X_normal)
scores = model.decision_function(X) # 负分越低越异常
隔离森林快、可解释、少调参,是异常检测的第一基线;单类 SVM 适合有清晰的「正常边界」;深度方法(AE/VAE)适合高维与复杂结构数据,但需要更多数据与调参。
半监督与标签稀缺
异常检测常常几乎没有异常标签(异常百年一遇,谁能提前标出来)。三条路:
- 仅正例学习(PU Learning):只有正常样本可标注,其余未知——用「有标记正常 + 大量未标记」训练,识别偏离正常分布的点。
- 预训练 + 微调:用大量正常数据预训练特征提取器,再对少量异常样本微调分类头,few-shot 也能学出异常模式。
- 规则先导 + 模型增强:先用规则/专家经验捞出「疑似异常」当种子正样本,再进模型迭代。领域知识进数据,比纯统计强得多。
# 半监督流程示意
# 1) 用领域规则捞疑似异常种子集(几十~几百条)
# 2) 训练异常检测器(隔离森林/AE)→ 对全量打分
# 3) 高分但未被规则覆盖的 → 人工复核 → 回流种子集
# 4) 迭代 2~3 轮,检出能力显著提升
核心思想是让少量标注撬动大量未标注:先模型召回候选,再人工复核,人工反馈回流再训。这也是数据飞轮在异常检测场景的落地。
生产评估与成本建模
不平衡场景的评估必须把业务成本写进指标:
# 成本矩阵:误报(FN漏报)与漏报(FP误报)的成本
# 漏报一个欺诈: -1000 元 误报一个正常用户: -5 元(客服成本+体验)
# 最优阈值 = argmin( total_cost = FP × 5 + FN × 1000 )
- 阈值按成本定:扫阈值找成本最小点,而不是 F1 最高点。
- 按群体分别评估:少数类内部可能还分层(低频欺诈 vs 高频刷单),分组看 PR。
- 线上 A/B:指标好看不等于线上有效,用 A/B 验证实际业务增益(降损、降客诉)。
- 漂移联动:异常模式会演化,定期重训 + 漂移监控,防止模型「过期」。
总结
类别不平衡与异常检测的完整打法:度量上抛弃 accuracy 改 PR-AUC,数据上重采样(SMOTE/EasyEnsemble),损失上类别加权与 Focal Loss,模型上隔离森林/单类 SVM/AE 分层选型,标签上走半监督与数据飞轮,评估上按业务成本定阈值。记住:不平衡不是模型的错,是「少数类太少 + 评估错了」的组合。先纠正评估,再逐层调数据与损失,最后用成本矩阵做决策,才能把 0.1% 的少数类真正捞出来。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。