我训练的欺诈检测模型准确率高达 99%,我正得意,一看才发现它把所有交易都判成了正常、一笔欺诈都没抓到,我对着类别极度不平衡时 accuracy 完全失真这个坑排查了大半天的复盘
这是一个让我对"评估指标到底在衡量什么"彻底警醒的机器学习坑。它的可怕在于:它给了我一个高得令人陶醉的数字(99% 准确率),让我以为模型棒极了;可这个数字背后,是一个彻头彻尾的废物模型——它什么有用的东西都没学到,只是学会了"无脑猜多数"这一招。
事情起于一个欺诈检测模型。我用历史交易数据训练了一个二分类模型(欺诈 / 正常),训练完一评估,accuracy 高达 99%!我一度欣喜若狂,觉得这模型简直完美。可当我真的去看它的预测结果时,血压上来了:
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
# 数据集: 100000 笔交易, 其中只有 1000 笔是欺诈(欺诈占 1%, 极度不平衡)
# 模型预测后:
print(accuracy_score(y_test, y_pred)) # 0.99 ★ 99%! 看着超棒
# 但看混淆矩阵, 真相暴露:
print(confusion_matrix(y_test, y_pred))
# [[99000 0] ← 99000笔正常, 全判对(判成正常)
# [ 1000 0]] ← 1000笔欺诈, 【全判成了正常!】一笔都没抓到!
#
# 即: 模型把【所有】交易都预测成了"正常(多数类)"
# - 正常的99000笔: 判对(因为本来就正常)
# - 欺诈的1000笔: 全判错(judged为正常)
# → 准确率 = 99000/100000 = 99% ← 高! 但模型对"欺诈"的召回率是 0!
# → 这个99%的模型, 一笔欺诈都抓不到, 完全没用!
我盯着那个混淆矩阵里刺眼的"1000 笔欺诈全判成了正常",彻底清醒了。原来我那个 99% 准确率的模型,根本没有学会区分欺诈和正常;它只是发现了一个"偷懒的捷径"——既然数据里 99% 都是正常交易,那我把所有交易都判成正常,不就能轻松拿到 99% 的准确率了吗?它确实这么做了:对每一笔交易都预测"正常"。结果正常的全判对、欺诈的全判错,准确率高达 99%,可它对"欺诈"这个我真正关心的类别的召回率是 0——一笔欺诈都抓不到。这个看起来"优秀"的模型,在它最该发挥作用的地方,是个彻头彻尾的废物。而把我骗得团团转的,正是 accuracy 这个在"类别不平衡"场景下完全失真的指标。
第一件事:看清真相——类别极度不平衡时,accuracy 会被多数类"绑架"
我去深入理解了"类别不平衡(class imbalance)"以及 accuracy 为何在此失效,才彻底明白这个"高分废物"的成因——当一个类别(正常)占了绝大多数(99%)、另一个类别(欺诈)是极少数(1%)时,accuracy(整体预测对的比例)会被多数类主导/绑架:模型只要"无脑全猜多数类",就能轻松拿到接近多数类占比的高准确率,而完全无视那个我们真正关心的少数类。
类别不平衡与 accuracy 失真的真相
# 1. accuracy(准确率)的定义: 预测正确的样本数 / 总样本数
# - 它衡量的是"整体上, 预测对了多少比例"
# 2. 当类别【极度不平衡】时(如 正常99% : 欺诈1%):
# - 一个"什么都不学、全部预测成多数类(正常)"的模型:
# - 99%的正常样本: 全部判对
# - 1%的欺诈样本: 全部判错
# - accuracy = 99% ← 高得吓人, 但它一个欺诈都没抓到!
# - → accuracy 被占绝大多数的"正常类"【绑架】了:
# 多数类判对就能贡献绝大部分准确率, 少数类判错对总准确率影响微乎其微。
# 3. 问题的本质: 在不平衡场景下, 我们真正关心的往往是【少数类】!
# - 欺诈检测: 我们要抓的是少数的"欺诈"(漏掉欺诈代价大)
# - 疾病诊断: 我们要查出少数的"病人"(漏诊代价大)
# - 而 accuracy 恰恰对少数类"不敏感"——它被多数类主导, 看不出模型对少数类的好坏。
# 4. 所以 accuracy 在不平衡场景下是【误导性指标】:
# 高 accuracy ≠ 模型有用; 一个高 accuracy 的模型可能对关键的少数类完全无能。
# 5. 该看什么指标(对少数类敏感的):
# - 混淆矩阵(confusion matrix): 看清每类各判对判错多少(最直观)
# - Precision(精确率): 判为欺诈的里, 真的是欺诈的比例
# - Recall(召回率): 真欺诈里, 被抓出来的比例 ← 欺诈检测最关心这个!
# - F1: precision和recall的调和平均(综合)
# - AUC-ROC / AUC-PR: 综合衡量(PR曲线对不平衡更敏感)
# 核心: 类别极度不平衡时, accuracy会被多数类绑架(全猜多数类就能高分), 是误导性指标;
# 要看混淆矩阵、precision/recall/F1/AUC等对少数类敏感的指标, 尤其关注我们真正在意的少数类。
真相大白,我恍然大悟。原来 accuracy(预测正确数 / 总数)衡量的是"整体预测对了多少比例";而当类别极度不平衡(正常 99% : 欺诈 1%)时,一个"什么都不学、全猜多数类"的模型——99% 的正常全判对、1% 的欺诈全判错——accuracy 就是 99%,高得吓人,却一个欺诈都没抓到。这是因为 accuracy 被占绝大多数的"正常类"绑架了:多数类判对就贡献了绝大部分准确率,少数类判错对总准确率影响微乎其微。而问题的本质是:在不平衡场景下,我们真正关心的往往是少数类(欺诈、病人——漏掉代价巨大);可 accuracy 恰恰对少数类不敏感,看不出模型对少数类的好坏。所以 accuracy 在不平衡场景下是误导性指标——高 accuracy ≠ 模型有用。该看的是对少数类敏感的指标:混淆矩阵(看清每类判对判错多少)、Precision(判为欺诈里真欺诈的比例)、Recall(真欺诈里被抓出来的比例,欺诈检测最关心)、F1、AUC。
第二件事:正解——换对评估指标,并从数据/算法/阈值层面处理不平衡
搞懂了原理,正解就清晰了:用对少数类敏感的指标评估(precision/recall/F1/AUC + 混淆矩阵),并从数据(重采样)、算法(类别权重)、阈值三个层面处理不平衡。
from sklearn.metrics import classification_report, roc_auc_score
from sklearn.linear_model import LogisticRegression
# ====== 正解一: 用对的指标评估(别只看accuracy) ======
print(classification_report(y_test, y_pred)) # 一次给出每类的 precision/recall/f1
print("AUC:", roc_auc_score(y_test, y_proba)) # AUC(用预测概率, 对不平衡更可靠)
# → 重点看少数类(欺诈)的 recall 和 f1, 而不是总体 accuracy
# ====== 正解二: 算法层面 —— 给少数类更高的权重 ======
model = LogisticRegression(class_weight='balanced') # ★ 自动给少数类更高权重
# class_weight='balanced': 让模型"更重视"少数类的错误, 不再无脑偏向多数类
# (树模型如RandomForest/XGBoost也有 class_weight / scale_pos_weight 参数)
# ====== 正解三: 数据层面 —— 重采样平衡数据 ======
# 过采样少数类(复制/合成更多欺诈样本):
from imblearn.over_sampling import SMOTE
X_res, y_res = SMOTE().fit_resample(X_train, y_train) # SMOTE合成少数类样本
# 或 欠采样多数类(减少正常样本); 或两者结合
# ⚠️ 重采样只在【训练集】上做! 测试集保持真实分布(否则又是数据泄漏)
# ====== 正解四: 阈值层面 —— 调整分类阈值 ======
# 默认阈值0.5可能不适合不平衡数据; 根据业务调整:
y_pred_custom = (y_proba >= 0.3).astype(int) # 降低阈值, 更"宽松"地判为欺诈
# → 提高召回(抓更多欺诈), 代价是精确率下降(误报增多); 按业务权衡
# ====== 正解五: 选指标要贴合业务目标 ======
# - 欺诈/疾病(漏报代价大): 重点优化 Recall(宁可错杀, 不可放过)
# - 垃圾邮件(误报代价大): 重点优化 Precision(别把正常邮件判成垃圾)
# - 二者平衡: 看 F1; 综合排序能力: 看 AUC
# 核心: 不平衡场景用precision/recall/f1/AUC+混淆矩阵评估(关注少数类); 用class_weight、
# 重采样(SMOTE, 只在训练集)、调阈值处理不平衡; 指标选择要贴合"漏报/误报谁代价大"的业务目标。
修复的核心,是"换对评估指标,并从数据/算法/阈值层面处理不平衡"。正解一:用对的指标——classification_report 一次给出每类的 precision/recall/f1,roc_auc_score 看 AUC;重点看少数类(欺诈)的 recall 和 f1,而非总体 accuracy。正解二:算法层面给少数类更高权重——class_weight='balanced' 让模型更重视少数类的错误、不再无脑偏向多数类(树模型有 scale_pos_weight)。正解三:数据层面重采样——SMOTE 过采样合成少数类样本、或欠采样多数类;注意重采样只在训练集做(测试集保持真实分布,否则又是数据泄漏)。正解四:阈值层面——默认 0.5 可能不适合,降低阈值提高召回(抓更多欺诈)、代价是精确率下降,按业务权衡。正解五:指标贴合业务——漏报代价大(欺诈/疾病)优化 Recall、误报代价大(垃圾邮件)优化 Precision、平衡看 F1。归根结底:不平衡场景用 precision/recall/f1/AUC+混淆矩阵评估、关注少数类;用 class_weight、重采样(只在训练集)、调阈值处理不平衡;指标选择贴合业务目标。
第三件事:模型评估相关的其他常见误区
排查后我把模型评估相关的其他常见误区也系统梳理了一遍。
模型评估的其他常见误区
# 1. 不平衡场景只看accuracy(本文): 被多数类绑架。→ 看recall/f1/AUC。
# 2. 只看单一指标: 任何单一指标都有盲区。→ 综合看多个指标+混淆矩阵。
# 3. precision和recall的权衡没想清: 提高一个常牺牲另一个
# → 要根据业务(漏报vs误报谁代价大)决定优先哪个。
# 4. 用准确率比较不同基准: 没和"基线模型(如全猜多数类)"比, 不知道模型到底有没有用。
# → 永远和一个朴素基线对比, 看模型有没有真的超过它。
# 5. 在不平衡的测试集上算的指标没意义化: 要看每类的指标, 别只看宏观。
# 6. 重采样用在了测试集上: 测试集要保持真实分布, 否则评估失真(数据泄漏)。
# 7. 忽略业务代价: 不同的错误代价不同(漏诊 vs 误诊), 评估要体现代价。
# → 可用代价敏感学习, 或自定义结合业务代价的指标。
# 8. 离线指标好就以为线上一定好: 还要考虑分布偏移、线上真实表现。
# 共同根源: "评估"的目的是【真实地衡量模型在它要解决的实际问题上有多好】;
# 而很多误区, 都源于用了"不能真实反映实际问题/业务目标"的指标(如不平衡下的accuracy)。
# 核心: 评估要选"能真实反映业务目标"的指标、综合多指标看、和基线比、关注关键类别、
# 体现业务代价; 别被单一漂亮数字(尤其不平衡下的accuracy)迷惑。
排查让我把评估的其他误区也梳理清了。一、不平衡只看 accuracy(本文)。二、只看单一指标(任何单一指标有盲区,综合看)。三、precision/recall 权衡没想清(按业务定优先)。四、没和基线模型比(永远和"全猜多数类"等朴素基线对比,看有没有真超过)。五、只看宏观不看每类指标。六、重采样用在测试集(测试集要保持真实分布)。七、忽略业务代价(漏诊 vs 误诊代价不同)。八、离线好就以为线上好。它们的共同根源是:"评估"的目的是真实地衡量模型在它要解决的实际问题上有多好;而很多误区都源于用了"不能真实反映实际问题/业务目标"的指标。核心是:评估要选"能真实反映业务目标"的指标、综合多指标、和基线比、关注关键类别、体现业务代价;别被单一漂亮数字迷惑。下面这张图,是这次 99% 废物模型的成因与解法:
第四件事:常见评估指标及其适用场景对照表
这次踩坑后,我把常见的分类评估指标和它们的适用场景整理成一张表,选指标时对照。
| 指标 | 衡量什么 | 适用 / 注意 |
|---|---|---|
| Accuracy 准确率 | 整体判对比例 | 类别均衡时可用; 不平衡时失真 |
| Precision 精确率 | 判为正的里真正的比例 | 误报代价大时重点看 |
| Recall 召回率 | 真正的里被找出的比例 | 漏报代价大时重点看(欺诈/疾病) |
| F1 | P和R的调和平均 | P和R都要兼顾时 |
| AUC-ROC | 综合排序能力 | 常用; 但极不平衡时可能偏乐观 |
| AUC-PR | P-R曲线下面积 | 极不平衡时比ROC更可靠 |
| 混淆矩阵 | 每类判对判错详情 | 最直观, 永远先看它 |
这张表把指标的选择钉死了。核心是:没有一个"万能"的指标——每个指标衡量的是模型某一侧面的能力,有它的适用场景和盲区;选哪个指标,取决于"你的业务真正关心什么"(整体对错?少抓漏的?少冤枉好的?);而混淆矩阵最直观、应该永远先看它。它给我的最大启发是:"用什么指标来衡量",本身就是一个极其重要、且蕴含价值判断的决策——你选择优化哪个指标,就等于选择了"什么样的错误是你更不能容忍的";选 recall 意味着"宁可错杀不可放过"、选 precision 意味着"宁可放过不可错杀"——这背后是实实在在的业务价值取向,而不是一个纯技术的选择。这其实揭示了一个深刻的道理:"度量(metric)"不是中立的——你选择衡量什么、优化什么,就会塑造出什么样的结果("你衡量什么,就得到什么");一个错误的、不贴合真实目标的度量,会引导你的努力(和模型的优化)走向一个"数字漂亮、实则无用甚至有害"的方向。这让我对"定义指标"这件事无比慎重:在动手优化之前,一定要先想清楚"对这个问题,什么才是真正有意义的'好'?用什么指标才能真实地衡量这个'好'?";选对指标,是一切优化努力能"用在刀刃上"的前提——否则你可能在"努力地把一个错误的数字做高"。慎重地选择真正贴合业务目标的度量、警惕"度量塑造结果"的力量——是这个不平衡坑教给我的、关于"如何衡量成功"的深层一课。
第五件事:不平衡本身也是一种信息
这次还让我换个角度想:类别不平衡,有时本身就反映了问题的某种本质。
| 场景 | 不平衡反映了什么 | 启示 |
|---|---|---|
| 欺诈检测 | 欺诈本就是少数(罕见事件) | 不平衡是问题的固有属性, 不是数据缺陷 |
| 疾病筛查 | 患病者本就是少数 | 要专门优化对少数类的识别 |
| 故障预测 | 故障本就罕见 | 少数类样本宝贵, 别轻易丢弃 |
| 极端不平衡 | 少数类太少难以学习 | 可能要异常检测而非分类 |
这张表让我对不平衡有了更辩证的看法。核心是:很多场景下,"类别不平衡"不是数据的缺陷,而是问题本身的固有属性——欺诈、疾病、故障,它们在现实中本来就是少数(罕见事件);这种不平衡,恰恰反映了"我们要找的,是茫茫多数中的那一小撮异常"这个问题的本质。它给我的启发是:面对不平衡,不应该只想着"怎么把数据弄平衡"(重采样),更要理解这个不平衡背后的问题本质,并据此选择合适的方法;当少数类罕见到一定程度(极度不平衡),它可能根本不该被当成"普通的二分类"来做,而更适合用"异常检测(anomaly detection)"的思路——把它看成"从正常模式中识别出偏离的异常"。这让我领悟到一个更普遍的认知:数据的"形状/分布特征"(不平衡、长尾、稀疏、缺失模式),本身就携带着关于"这是个什么问题、该用什么方法"的重要信息;一个好的实践者,会去读懂数据的这些特征,并让方法去适配数据和问题的本质,而不是机械地套用一个通用流程、或粗暴地把数据"掰"成方法喜欢的样子。读懂数据分布背后的问题本质、让方法适配问题而非相反——是这个不平衡坑,在技术之上,带给我的更深的思考。
第六件事:训练分类模型时,我现在的判断习惯
现在每当我训练一个分类模型,我都会按这张图先想清楚:
这张图的精髓,是"先看类别比例,不平衡就别信 accuracy、用对少数类敏感的指标、按业务优化、处理不平衡"。先看各类样本比例:平衡时 accuracy 可参考但仍看混淆矩阵;不平衡就别信 accuracy,看混淆矩阵+precision/recall/f1/AUC,按业务(漏报/误报谁代价大)决定优化 Recall 还是 Precision,用 class_weight/重采样(仅训练集)/调阈值处理不平衡。最后和基线模型对比确认真有提升。这套习惯,让我训模型时,从"看 accuracy 高就高兴"变成了"先看类别比例、用对指标、关注真正在意的类别"——核心始终是:不平衡场景 accuracy 会骗人,用对少数类敏感的指标、贴合业务目标评估。
我立下的几条规矩
这场"99% 准确率的废物模型"的事故,换来了我做机器学习时,刻进骨子里的几条铁律:
- 先看类别比例。不平衡时 accuracy 完全不可信。
- 永远先看混淆矩阵。它最直观地暴露模型对每类的真实表现。
- 不平衡场景看 precision/recall/f1/AUC。关注真正在意的少数类。
- 和基线模型(全猜多数类)对比。确认模型真的学到了东西。
- 处理不平衡:class_weight/重采样/调阈值。重采样只在训练集。
- 指标选择贴合业务。漏报代价大优化 recall,误报代价大优化 precision。
- 警惕"度量塑造结果"。选对衡量什么,努力才用在刀刃上。
附:一段亲眼看清"99%准确率废物模型"的实验
口说无凭。下面这段代码,用一个"什么都不学、全猜多数类"的傻瓜模型,亲眼证明 accuracy 在不平衡下有多能骗人:
import numpy as np
from sklearn.dummy import DummyClassifier
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
from sklearn.model_selection import train_test_split
# 造一个极度不平衡的数据集: 99%是类0(正常), 1%是类1(欺诈)
rng = np.random.RandomState(0)
y = np.r_[np.zeros(9900), np.ones(100)].astype(int) # 9900正常 + 100欺诈
X = rng.randn(len(y), 5) # 特征是纯噪声(和y无关)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, stratify=y, random_state=1)
print("=== 一个'全猜多数类'的傻瓜模型 ===")
dummy = DummyClassifier(strategy="most_frequent") # 永远预测出现最多的类(0)
dummy.fit(Xtr, ytr)
pred = dummy.predict(Xte)
print("accuracy:", accuracy_score(yte, pred)) # ≈ 0.99 ★ 99%! 但它啥也没学
print("混淆矩阵:\n", confusion_matrix(yte, pred))
# [[2970 0] ← 正常全判对
# [ 30 0]] ← 欺诈30个全判错(全判成正常)!
print(classification_report(yte, pred, zero_division=0))
# 类1(欺诈)的 precision=0, recall=0, f1=0 ← 真相: 对欺诈完全无能!
# → accuracy 99%, 但欺诈类的 recall 是 0。这个高分模型一无是处。
print("\n=== 启示: 永远拿模型和这个傻瓜基线比 ===")
# 如果你辛苦训练的模型, accuracy 也就99%上下、和这个"全猜多数类"的傻瓜差不多,
# 那它很可能根本没学到东西! 必须看它在【欺诈类】上的recall/f1有没有显著超过0。
# 核心: 跑一遍, 亲眼看到一个"啥也不学、全猜多数类"的傻瓜模型也能拿99% accuracy、
# 但欺诈recall=0——这就证明了不平衡下accuracy的欺骗性, 以及"和傻瓜基线比"的必要。
这段实验代码,是我这次踩坑后写下的"accuracy 测谎仪"。它最有力的设计,是用了 sklearn 的 DummyClassifier(strategy="most_frequent")——一个明确地、故意地"什么都不学,永远只预测出现最多的那个类"的傻瓜模型;然后让你亲眼看到:就是这么个一无是处的傻瓜,在不平衡数据上 accuracy 也能高达 99%,可它对欺诈类的 recall 是实打实的 0(混淆矩阵里 30 个欺诈全判错)。这一下就把"99% 的 accuracy 可能毫无价值"这件事,从一句抽象的告诫,变成了你能亲手跑出来、亲眼看到的铁证。这正是我想用这段代码,留给每个做分类的人的核心方法:评估一个模型时,永远要给它找一个"傻瓜基线(baseline)"作对照——比如这个"全猜多数类"的 DummyClassifier;只有当你的模型的关键指标,显著地超过了这个傻瓜基线,才能证明它"真的学到了东西、真的有价值"。因为一个指标的"绝对值"(如 99%)往往是没有意义的、甚至是骗人的;有意义的是它的"相对值"——相对于一个"什么都不做/瞎猜"的基线,你的模型到底好了多少;没有基线作参照,你根本无法判断 99% 到底是"了不起"还是"还不如瞎猜"。永远用一个傻瓜基线作对照、看模型相对基线的真实提升而非指标的绝对值——这份"凡事要有参照系"的科学习惯,是我评估一切模型(乃至一切优化效果)时,避免被漂亮数字迷惑的根本法门。
写在最后
回头看,这场由"迷信 accuracy"引发的、99% 废物模型的事故,真正教给我的,远不止"不平衡要看 recall"这一个技巧。它让我对"如何衡量'好'"这件事的深刻性,有了一次刻骨铭心的认识。我栽跟头,根源是我把"一个看起来很高的、很权威的数字(99% accuracy)",直接等同于了"模型很好、问题解决了"。我没有停下来问一个最根本的问题:"这个数字,到底在衡量什么?它衡量的东西,是我真正关心的吗?" accuracy 衡量的是"整体判对的比例",可在欺诈检测这个问题里,我真正关心的根本不是"整体判对多少"(反正绝大多数本来就是正常的),而是"那少数的欺诈,我抓到了多少"——这恰恰是 accuracy 看不见的地方。我用一个"衡量了我不那么关心的东西"的指标,去评判一个"在我真正关心的事情上一败涂地"的模型,自然就被它漂亮的数字骗了。这让我领悟到一个深刻的认知:"衡量正确的东西",比"把衡量的数字做高"重要无数倍;一个错误的、不贴合真实目标的度量,是一切努力的"毒指南针"——它会把你和你的优化,满怀信心地引向一个错误的方向,让你在"把一个无意义的数字越做越高"的路上越走越远,还自我感觉良好。这其实是一个远超机器学习、适用于一切"用指标驱动"领域(业务 KPI、性能优化、产品增长)的根本道理:在追求"把指标做好"之前,必须先极其慎重地确保"这个指标本身,真实地、完整地代表了我想要达成的目标";因为"你衡量什么,就会得到什么"——选错了衡量的标尺,你越努力,可能偏离真正的目标越远。把"选对衡量的标尺"放在"努力把数字做高"之前、永远追问"这个指标真的代表我要的吗"——这,是我用一次 99% 废物模型的事故,换来的、关于机器学习、也关于如何在一切事情上"正确地衡量成功"的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次看到一个漂亮的指标时,先冷静地问一句"它到底在衡量什么?是我真正关心的吗?",那我对着那个 99% 却一笔欺诈都抓不到的模型反思的这大半天,就值了。
—— 别看了 · 2026