一、为什么评估分类模型不能只看"准确率"
一个极端的例子:1000 个样本里只有 10 个欺诈交易,模型什么都不做、全部猜"正常",准确率就有 99%——但这个模型完全没用。
所以,评估分类模型的核心问题是:用什么视角看"准不准",取决于业务更怕哪种错误。
二、一切的原点:混淆矩阵
预测为正 预测为负 ┌──────────┬──────────────┬──────────────┐ 实际为正 │ TP(真正例) │ FN(假负例) │ ├──────────┼──────────────┼──────────────┤ 实际为负 │ FP(假正例) │ TN(真负例) │ └──────────┴──────────────┴──────────────┘| 名称 | 俗称 | 含义 |
|---|---|---|
| TP | 抓对了 | 实际是正,模型也判正 |
| FN | 漏了 | 实际是正,模型却判负 |
| FP | 冤枉了 | 实际是负,模型却判正 |
| TN | 正确排除 | 实际是负,模型也判负 |
fromsklearn.metricsimportconfusion_matrix,ConfusionMatrixDisplay cm=confusion_matrix(y_test,y_pred)disp=ConfusionMatrixDisplay(cm,display_labels=['正常','欺诈'])disp.plot()所有后续指标都从这四个数派生出来。
三、基础指标
1. 准确率(Accuracy)
Accuracy=TP+TNTP+FP+FN+TN\text{Accuracy} = \frac{TP + TN}{TP + FP + FN + TN}Accuracy=TP+FP+FN+TNTP+TN
含义:所有样本中,模型判对了多少比例。
什么时候好用:类别比较平衡、两种错误代价差不多时。
什么时候失效:类别严重不平衡——正例只占 1% 时,全猜负就有 99% 的准确率。
2. 精确率(Precision)
Precision=TPTP+FP\text{Precision} = \frac{TP}{TP + FP}Precision=TP+FPTP
含义:模型说"这是正例"的那些里面,多少真的是正例。
通俗理解:我报上去的嫌疑人里,多少是真犯。
| 关注精确率的场景 | 原因 |
|---|---|
| 垃圾邮件过滤 | 把正常邮件误判为垃圾邮件,用户很愤怒 |
| 搜索引擎排序 | 前几条结果不相关,用户就不信任了 |
| 推荐系统 | 推了用户不喜欢的东西,体验差 |
3. 召回率(Recall / Sensitivity / TPR)
Recall=TPTP+FN\text{Recall} = \frac{TP}{TP + FN}Recall=TP+FNTP
含义:真正的正例中,模型找出来多少。
通俗理解:所有真犯里,我抓到了多少。
| 关注召回率的场景 | 原因 |
|---|---|
| 癌症筛查 | 漏掉一个病人比误报严重百倍 |
| 欺诈检测 | 漏掉一笔欺诈,损失可能巨大 |
| 故障预警 | 漏报警比误报代价高得多 |
4. 精确率与召回率的关系
它们是此消彼长的。阈值调低 → 召回率升、精确率降;阈值调高 → 精确率升、召回率降。
阈值 = 0.9: Precision=0.95 Recall=0.30 (很谨慎,只报很有把握的) 阈值 = 0.5: Precision=0.80 Recall=0.70 (默认平衡点) 阈值 = 0.2: Precision=0.50 Recall=0.95 (宁可错杀不放过)Precision 1.0 |\ | \ | \___________ | 0.0 +────────────── Recall 0.0 1.05. F1-Score
F1=2×Precision×RecallPrecision+RecallF1 = \frac{2 \times Precision \times Recall}{Precision + Recall}F1=Precision+Recall2×Precision×Recall
含义:精确率和召回率的调和平均。
为什么用调和平均而不用算术平均?调和平均对极端值更敏感——一个指标很低就会把 F1 拉下来,不像算术平均可以"一个 0.99 一个 0.01 算出 0.50"看起来还行。
Precision=0.99, Recall=0.01 算术平均 = 0.50 (看着还行,实际很烂) 调和平均 = 0.02 (真实反映了问题)6. Fβ-Score(带权重的 F 值)
Fβ=(1+β2)×Precision×Recallβ2×Precision+RecallF_\beta = \frac{(1+\beta^2) \times Precision \times Recall}{\beta^2 \times Precision + Recall}Fβ=β2×Precision+Recall(1+β2)×Precision×Recall
| β 值 | 侧重 | 场景 |
|---|---|---|
| β=1 | 精确率和召回率同等重要 | 通用 |
| β=2 | 召回率重要性是精确率的 2 倍 | 癌症筛查、欺诈检测 |
| β=0.5 | 精确率重要性是召回率的 2 倍 | 垃圾邮件过滤 |
fromsklearn.metricsimportf1_score,fbeta_score f1=f1_score(y_test,y_pred)f2=fbeta_score(y_test,y_pred,beta=2)# 更重召回f05=fbeta_score(y_test,y_pred,beta=0.5)# 更重精确四、基于概率排序的指标
前面那些指标都基于"硬判断"(0 或 1),但模型输出的是概率。概率本身的质量也很重要。
1. ROC 曲线
把阈值从 1.0 慢慢降到 0.0,每个阈值都有一组 (FPR, TPR),连起来就是 ROC 曲线。
TPR (真正率,即召回率) 1.0 | ___________ | / | / ← 曲线越贴近左上角越好 | / | / | / ← 对角线 = 随机猜测 0.0 +────────────────── FPR (假正率) 0.0 1.0- 好模型的曲线贴近左上角
- 随机模型是对角线
- 比随机还差的模型在左下角(反转一下就能用)
2. AUC(Area Under ROC Curve)
ROC 曲线下的面积,取值 [0, 1]。
| AUC | 解读 |
|---|---|
| 0.5 | 跟瞎猜没区别 |
| 0.6 | 很弱 |
| 0.7 | 还行 |
| 0.8 | 不错 |
| 0.9 | 很好 |
| 1.0 | 完美(大概率过拟合了) |
AUC 的概率含义:随机取一个正样本和一个负样本,模型给正样本的分数高于负样本的概率就是 AUC。
fromsklearn.metricsimportroc_auc_score,roc_curve y_prob=model.predict_proba(X_test)[:,1]auc=roc_auc_score(y_test,y_prob)fpr,tpr,thresholds=roc_curve(y_test,y_prob)importmatplotlib.pyplotasplt plt.plot(fpr,tpr,label=f'AUC={auc:.3f}')plt.plot([0,1],[0,1],'--',color='gray')plt.xlabel('FPR')plt.ylabel('TPR')plt.legend()plt.show()AUC 的优点:不依赖特定阈值,衡量的是模型整体的"排序能力"。
AUC 的盲区:当正例极少时(如 0.5%),AUC 可能看起来很高(0.95),但实际_precision_可能只有 10%——因为 FPR 稍微上升一点就意味着大量误报。
3. PR 曲线与 AP
当正例极度稀少时,PR 曲线比 ROC 更有参考价值。
Precision 1.0 |\ | \ | \____ | \______ | \____ 0.0 +────────────────── Recall 0.0 1.0AP(Average Precision):PR 曲线下面积。
fromsklearn.metricsimportaverage_precision_score,precision_recall_curve ap=average_precision_score(y_test,y_prob)precision,recall,_=precision_recall_curve(y_test,y_prob)plt.plot(recall,precision,label=f'AP={ap:.3f}')plt.xlabel('Recall')plt.ylabel('Precision')plt.legend()plt.show()为什么极度不平衡时看 PR?因为 ROC 的横轴 FPR 即使从 0.01 涨到 0.02,在 99% 负例的基数下,误报数量也翻倍了——但曲线看起来几乎没变。PR 曲线对这种变化敏感得多。
4. 对数损失(Log Loss)
LogLoss=−1n∑i=1n[yilog(pi)+(1−yi)log(1−pi)]\text{LogLoss} = -\frac{1}{n}\sum_{i=1}^{n}[y_i \log(p_i) + (1-y_i)\log(1-p_i)]LogLoss=−n1i=1∑n[yilog(pi)+(1−yi)log(1−pi)]
衡量概率预测的"靠谱程度"——不只看排没排对,还看给的概率是否准确。
| 情况 | 预测概率 | 实际 | Log Loss |
|---|---|---|---|
| 很确信且对了 | 0.99 | 1 | 0.01 |
| 对了但犹豫 | 0.51 | 1 | 0.67 |
| 很确信但错了 | 0.99 | 0 | 4.60 |
| 犹豫且错了 | 0.51 | 0 | 0.67 |
关键:确信但判错的惩罚 >> 犹豫但判错。所以 Log Loss 驱动模型不仅要判对,还要"有把握才下结论"。
fromsklearn.metricsimportlog_loss ll=log_loss(y_test,y_prob)五、多分类场景的评估
宏观 vs 微观 vs 加权
三种聚合方式,把每个类的指标合成一个全局指标:
| 方式 | 做法 | 特点 |
|---|---|---|
| macro | 每个类分别算,再简单平均 | 大类小类权重相同,对小类表现敏感 |
| weighted | 按各类样本量加权平均 | 大类影响更大 |
| micro | 把所有类的 TP/FP/FN 合在一起算 | 等同于整体准确率(多分类时) |
fromsklearn.metricsimportclassification_reportprint(classification_report(y_test,y_pred,target_names=['体育','财经','娱乐','科技']))输出示例:
precision recall f1-score support 体育 0.92 0.89 0.90 200 财经 0.88 0.85 0.86 180 娱乐 0.80 0.82 0.81 150 科技 0.85 0.90 0.87 170 accuracy 0.87 700 macro avg 0.86 0.87 0.86 700 weighted avg 0.87 0.87 0.87 700- macro avg:四个类的 F1 简单平均 = (0.90+0.86+0.81+0.87)/4
- weighted avg:按各类数量加权 → 大类的表现权重更高
多分类 AUC
| 方式 | 思路 |
|---|---|
| One-vs-Rest (OvR) | 每个类 vs 其余所有类各算一次 AUC,再平均 |
| One-vs-One (OvO) | 每对类别算一次 AUC,再平均 |
fromsklearn.metricsimportroc_auc_score# OvR 方式auc_ovr=roc_auc_score(y_test,y_prob,multi_class='ovr')# OvO 方式auc_ovo=roc_auc_score(y_test,y_prob,multi_class='ovo')六、阈值的选择
模型输出概率,默认用 0.5 做阈值——但 0.5 未必是最优的。
方法 1:按业务代价选择
漏判一个欺诈的平均损失 = 5000 元 误报一笔正常的平均成本 = 50 元 代价比 = 100:1 → 阈值应该调得很低(如 0.1),宁可多报方法 2:F1 最优阈值
fromsklearn.metricsimportf1_score best_f1,best_thr=0,0.5forthrinnp.arange(0.1,0.9,0.01):y_pred_thr=(y_prob>=thr).astype(int)f1=f1_score(y_test,y_pred_thr)iff1>best_f1:best_f1,best_thr=f1,thrprint(f"最优阈值:{best_thr:.2f}, F1:{best_f1:.4f}")方法 3:Youden’s J 统计量
J=TPR−FPRJ = TPR - FPRJ=TPR−FPR
选让 J 最大的阈值,即 ROC 曲线上离对角线最远的点。
fpr,tpr,thresholds=roc_curve(y_test,y_prob)j_scores=tpr-fpr best_idx=np.argmax(j_scores)best_threshold=thresholds[best_idx]七、交叉验证:让评估更可靠
单次 train/test 划分有偶然性,交叉验证用多轮评估取平均,结果更稳。
K 折交叉验证
原始数据分成 5 份: 第1轮:[测试][训练][训练][训练][训练] → 算 AUC₁ 第2轮:[训练][测试][训练][训练][训练] → 算 AUC₂ 第3轮:[训练][训练][测试][训练][训练] → 算 AUC₃ 第4轮:[训练][训练][训练][测试][训练] → 算 AUC₄ 第5轮:[训练][训练][训练][训练][测试] → 算 AUC₅ 最终 AUC = mean(AUC₁..₅) ± std(AUC₁..₅)fromsklearn.model_selectionimportcross_val_score scores=cross_val_score(model,X,y,cv=5,scoring='roc_auc')print(f"AUC:{scores.mean():.4f}±{scores.std():.4f}")分层 K 折(StratifiedKFold):每折内正负比例一致,不平衡数据必用。
fromsklearn.model_selectionimportStratifiedKFold cv=StratifiedKFold(n_splits=5,shuffle=True,random_state=42)scores=cross_val_score(model,X,y,cv=cv,scoring='roc_auc')八、指标选择指南
按数据特征选
| 数据特征 | 首选指标 | 辅助指标 |
|---|---|---|
| 类别平衡 | Accuracy | F1, AUC |
| 类别不平衡 | AUC + F1 | AP, Recall@目标Precision |
| 极度稀疏(正例 <1%) | AP | F2, PR 曲线 |
| 多分类 | macro-F1 | 混淆矩阵 + 各类 F1 |
按业务目标选
| 业务目标 | 首选指标 | 原因 |
|---|---|---|
| 漏判代价高(欺诈、癌症) | Recall @ 固定 Precision | 控制误报上限,最大化召回 |
| 误报代价高(垃圾邮件、封号) | Precision @ 固定 Recall | 控制漏判上限,最大化精确 |
| 排序场景(推荐、搜索) | AUC / AP | 只关心排序对不对 |
| 概率需要准确(保险定价) | Log Loss / Brier Score | 概率校准很重要 |
| 通用、不确定 | F1 + AUC 一起看 | 互补视角 |
九、完整评估代码模板
importnumpyasnpimportmatplotlib.pyplotaspltfromsklearn.metricsimport(classification_report,confusion_matrix,roc_auc_score,roc_curve,average_precision_score,precision_recall_curve,log_loss,fbeta_score)fromsklearn.model_selectionimportcross_val_score,StratifiedKFolddefevaluate_model(model,X_test,y_test,model_name="Model"):"""分类模型全面评估"""y_prob=model.predict_proba(X_test)[:,1]y_pred=(y_prob>=0.5).astype(int)# 1. 基础指标print(f"\n{'='*50}")print(f"{model_name}评估报告")print(f"{'='*50}")print(classification_report(y_test,y_pred,digits=4))# 2. 关键单值指标auc=roc_auc_score(y_test,y_prob)ap=average_precision_score(y_test,y_prob)ll=log_loss(y_test,y_prob)f2=fbeta_score(y_test,y_pred,beta=2)print(f"AUC:{auc:.4f}")print(f"AP:{ap:.4f}")print(f"Log Loss:{ll:.4f}")print(f"F2:{f2:.4f}")# 3. 混淆矩阵cm=confusion_matrix(y_test,y_pred)print(f"\n混淆矩阵:\n{cm}")# 4. 绘图fig,axes=plt.subplots(1,2,figsize=(12,5))# ROC 曲线fpr,tpr,_=roc_curve(y_test,y_prob)axes[0].plot(fpr,tpr,label=f'AUC={auc:.3f}')axes[0].plot([0,1],[0,1],'--',color='gray')axes[0].set_xlabel('FPR')axes[0].set_ylabel('TPR')axes[0].set_title('ROC Curve')axes[0].legend()# PR 曲线prec,rec,_=precision_recall_curve(y_test,y_prob)axes[1].plot(rec,prec,label=f'AP={ap:.3f}')axes[1].set_xlabel('Recall')axes[1].set_ylabel('Precision')axes[1].set_title('PR Curve')axes[1].legend()plt.tight_layout()plt.show()return{'auc':auc,'ap':ap,'log_loss':ll,'f2':f2}# 使用results=evaluate_model(rf_model,X_test,y_test,"随机森林")十、常见坑与避坑
| 坑 | 现象 | 解法 |
|---|---|---|
| 只看 Accuracy | 不平衡数据下 99% 准确率但模型没用 | 必须同时看 F1/AUC |
| 用测试集调参 | 测试集上效果很好,上线后崩了 | 留出验证集调参,测试集只看一次 |
| 阈值永远是 0.5 | 业务需要高召回但用了 0.5 阈值 | 根据业务代价调阈值 |
| 不平衡数据不看 PR | AUC=0.95 但 Precision 只有 5% | 正例稀少时务必看 PR 曲线和 AP |
| 交叉验证不分层 | 某折里几乎没有正例,指标不稳定 | 用StratifiedKFold |
| 多分类只看 macro | 微小类别表现很差被掩盖 | 混淆矩阵逐类检查 |
| 不看概率校准 | 预测概率 0.7 但实际正例率只有 0.4 | 画校准曲线(calibration curve) |
最后一句话:没有万能指标。选什么指标,取决于你的业务更怕哪种错误。先想清楚"漏判和误报哪个代价更大",再选指标和调阈值,比无脑追 AUC 有效得多。