如果你跑过几个分类模型,大概率对这四个词又爱又恨:recall、precision、accuracy、f1score,有时候还冒出一个hmean。看着公式都不长,真正填数据的时候却总容易绕晕——为什么 F1 叫 hmean?为什么准确率那么高模型却不行?为什么明明两个模型的 accuracy 一模一样,业务效果却差出一大截?
这篇文章我把这几个指标彻底讲透,包括混淆矩阵的每个格子怎么用、F1 为什么取调和平均值而不是算术平均、hmean 和 F1 到底是什么关系,以及我在实际项目中踩过的坑和总结出来的排查思路。后面还会讲一个很多人忽略的点:什么时候accuracy会天然等于recall,以及如果遇到这种情况,你的评估流程是不是出了问题。准备点耐心,我尽量用大白话把这些概念串起来,看完你就不用再翻论文了。
1. 先把混淆矩阵刻在脑子里
1.1 一张表看懂预测结果
很多教程上来就丢公式,但公式背得再熟,落到真实数据上照样用错。我建议你先盯着下面这张表看一分钟,后面所有指标都是从这四个格子里算出来的:
- 真正例(TP):实际为正,预测也为正
- 假正例(FP):实际为负,但预测为正(误报)
- 假负例(FN):实际为正,但预测为负(漏报)
- 真负例(TN):实际为负,预测也为负
表格形式就是:
| 预测为正 | 预测为负 |
|---|---|
| 实际为正 | TP |
| 实际为负 | FP |
记住一件事情:所有评估指标,本质上都是在数这四个格子里的样本。 diagnostic 里有个经典说法,FP 是假警报,FN 是漏网之鱼。业务场景不同,你对这两种错误的容忍度完全不同,这就是后续所有指标选择分歧的根源。
1.2 别只盯着准确率,它比你想象中虚
accuracy(准确率)的公式是:
accuracy = (TP + TN) / (TP + FP + FN + TN)也就是所有预测对的样本除以总样本数。听起来很直觉对不对?但我在实际项目里见过太多人只报这个数。举一个极端例子,你要预测一个罕见病,10000 个人里只有 10 个病人。我写一个愚蠢至极的模型,对所有人都输出“无病”,那么它预测对了 9990 个,accuracy = 99.9%。看着相当漂亮吧?但这个模型其实什么都没学会,它把一个病人都没找出来。
这就是 accuracy 的第一个大坑:在类别不平衡的数据上,它会被多数类“垫高”。你甚至会看到一个模型在测试集上 accuracy 高达 0.95,但业务方一用就骂人,因为少数类(往往是最有价值的那些样本)一个都没抓住。
所以我给团队定的规矩是:看 accuracy 之前,先看类别分布。如果正负样本比超过 1:10 甚至 1:100,accuracy 就没资格当唯一指标。它不是没用,而是太容易被“作弊”,你必须搭配后面几个指标一起看。
1.3 accuracy 和 recall 何时会相等?这里有个隐藏信号
热度词里有句“accuracy 和 recall 值相同”,很多人觉得这是巧合。但根据我经验,这通常不是巧合,而是数据分布的特殊信号。
我们推导一下。令 accuracy = recall:
(TP + TN) / (TP + FP + FN + TN) = TP / (TP + FN)交叉相乘后化简,最终会得到TN / (TN + FP) = TP / (TP + FN),翻译一下:真负率等于召回率。这在数学上是完全可能的,但如果你发现一个二分类模型在正常数据集上这两个数恰好相等(比如都是 0.78),那多半是以下两种情况之一:
第一种,测试集本身近似“对称”:负例数据量很多,模型对负例的识别率刚好与正例的召回率一致。这个有点凑巧,但并不是说模型有多好,只是一个数学上的平衡点。
第二种,更常见也更麻烦:你计算的时候把类别标签搞反了。比如你追的是正类(记为 1),实际上模型输出 1 表示负类,你按 1 算 recall,0 算 accuracy,两个数字就可能因为混淆矩阵翻转而出现某个巧合相等点。我在一次代码 review 中就遇到过这个情况——排查了好久,最后发现数据集里label字段把 0 和 1 的定义写反了,整个评估指标全乱套。
所以如果你的 accuracy 和 recall 出现高度一致或完全相等,不要开心,先检查数据 pipeline。它可能是真实巧合,但也极可能是标签定义错误的报错信号。详见后面第六部分的排查表。
2. Precision 和 Recall,一对天生的冤家
2.1 一句话讲清两个指标
precision(精确率)关注的是“我预测出来的正样本里,有多少是真正例”。公式:
precision = TP / (TP + FP)recall(召回率)关注的是“所有真正的正样本里,我找回了多少”。公式:
recall = TP / (TP + FN)用两句人话:
- precision 高 = 你报出来的基本都是对的,但你可能会漏掉一些没报。
- recall 高 = 你基本上把真正例都捞出来了,但可能也捞进来一堆误报。
我一般用一个检索场景来类比,你搜“苹果”,搜索引擎给你返回 100 条结果,其中 30 条是水果苹果,这就是你的 precision = 30%。但整个互联网上真正的水果苹果网页有 1000 个,你只找到了 30 个,所以 recall = 3%。搜索引擎一般两个都要优化,但不同的产品定位会有不同侧重。
2.2 两者关系图:跷跷板效应
precision 和 recall 通常是互相拉扯的。你把模型阈值调低,让更多样本被预测为正,recall 会上升,但 FP 也会增加,precision 就会下降。反过来阈值调高,precision 上升,recall 下降。
这个“跷跷板”太经典了,我在实际调参时经常把它当作第一性原理来看。见到某个模型 precision 极高但 recall 极低,先别质疑模型烂,看看是不是阈值定得太苛刻。而如果你只看单一指标,根本不可能发现这种失衡。
画一张 PR 曲线(precision-recall curve),横轴是 recall,纵轴是 precision,曲线越靠近右上角,模型综合表现越好。这个曲线在类别不平衡时比 ROC 曲线更真实,因为 ROC 受负例占比影响比较大,PR 曲线则对正例的变化更敏感。
2.3 到底什么时候看 precision,什么时候看 recall?
听我的,不要机械地记“哪个重要”,要想业务代价。
垃圾邮件过滤场景:把正常邮件误判成垃圾邮件,用户会暴怒,这个 FP 的代价很高,所以优先保证 precision。
癌症早期筛查场景:漏掉一个真正的病人,可能耽误治疗窗口,这个 FN 的代价极高,所以优先保证 recall。
搜索引擎、推荐系统、风控模型,各场景下的代价函数都不一样。但核心逻辑是一致的:你愿意为“误报”付多少钱,愿意为“漏报”付多少钱,谁代价高就优先优化谁。
所以在跟业务对齐的时候,我会先确认两个问题:预测错误的类型有哪些?每一种错误会产生什么后果?搞清楚了再看指标。
3. F1 Score(hmean)到底在算什么
3.1 为什么是调和平均数?
F1的全称就是 precision 和 recall 的调和平均数 (harmonic mean),所以它也被写作hmean。
先上公式:
F1 = 2 * (precision * recall) / (precision + recall)这个公式就是调和平均数的标准形式。你可能要问,为什么不直接取算术平均?比如 precision = 0.9, recall = 0.1,算术平均是 0.5,但 F1 是2 * 0.9 * 0.1 / (0.9 + 0.1) = 0.18。看到了吗,F1 会把偏科的那个指标狠狠惩罚一顿。
调和平均数的特点是:它更受较小值影响。如果你希望两个指标都不能掉链子,用算术平均不合适,因为 0.9 和 0.1 平均下来还能有 0.5,仿佛还行;但 F1 = 0.18 就直接告诉你这个模型不行。这也是 F1 经常被用作综合指标的原因——它能简洁地反映 precision 与 recall 是否“双双在线”。
3.2 F1 为什么又被记成 hmean
很多论文代码里直接把 F1 写成hmean(task),其实它们是一回事。hmean是 harmonic mean 的缩写,就是调和平均。F1 恰好是 precision 与 recall 的 hmean,所以有的库函数取名f1_score,有的取名hmean,你看文档的时候别被吓到,本质上都是同一个统计量。
还需要注意一点,hmean 这个概念不局限于二分类。在多标签多任务里,也可以对不同类别的 F1 再做一次宏平均或微平均,有的实现会把这个平均过程继续称为 hmean。这种情况我后面专门用一节讲。
3.3 F1 不是万能的,什么时候它也失真?
F1 虽然好用,但它其实暗含了一个假设:precision 和 recall 的代价相等。现实里这两个指标的代价往往不相等。比如风控场景中漏掉一张欺诈卡和误杀一张正常卡,损失可能差十倍,你用 F1 做唯一标准,模型调出来的方向就不是业务最优解。
另外,当数据极度不平衡时,F1 也可能失真。比如正例只有 1%,模型只挑了几个最有把握的预测为正,precision 可能很高,recall 很低,F1 不高不低,但你仍然不知道模型对多数类表现如何。这时候我通常建议同时看一眼 confusion matrix,或者用 macro-F1 按类别平均,避免被单一指标误导。
4. 实操环节:从零计算一套完整指标
4.1 用一个小例子走一遍流程
这里我构造一个二分类案例,真实场景可以理解为“判断交易是否异常”。我们有 100 个样本,其中 20 个真实为异常。模型预测结果如下:
- TP = 12
- FP = 3
- FN = 8
- TN = 77
手工推一遍:
- accuracy = (12 + 77) / 100 = 0.89
- precision = 12 / (12 + 3) = 0.8
- recall = 12 / (12 + 8) = 0.6
- F1 = 2 * 0.8 * 0.6 / (0.8 + 0.6) = 0.96 / 1.4 ≈ 0.686
注意,accuracy 看着有 0.89,但 recall 只有 0.6,也就是说 20 个真实异常中有 8 个漏掉了。如果这是诈骗检测,等于放跑了 40% 的坏人,模型是不合格的。
4.2 用 Python 代码实际算一次
在 sklearn 里执行这些指标非常直接:
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score y_true = [1, 0, 1, 1, 0, 1, 0, 0, 1, 0] y_pred = [1, 0, 0, 1, 0, 1, 1, 0, 1, 0] print("accuracy:", accuracy_score(y_true, y_pred)) print("precision:", precision_score(y_true, y_pred)) print("recall:", recall_score(y_true, y_pred)) print("f1:", f1_score(y_true, y_pred))这里唯一需要留意的是pos_label参数。sklearn 很多指标函数里默认pos_label=1,如果你的正类是 0,不指定的话指标会反着算。类不平衡时,还要注意average参数:binary、macro、micro、weighted,不同模式算出来的结果差异很大,后面会展开。
4.3 什么时候应该用 macro-F1,什么时候用 micro-F1?
多分类场景下,F1 可以按不同方式聚合:
macro:每个类别算一个 F1,再取算术平均。它对小类别更公平,不会被大类淹没。micro:把所有类别的 TP、FP、FN 总数加在一起算 F1。它更偏向类别多的样本,相当于“样本级”的综合指标。weighted:macro 的加权版,权重是类别样本占比。
我推荐在不平衡多分类任务里,多报一个macro-F1。因为 micro-F1 很容易被大类带偏,当大类 accuracy 很高、小类全面拉胯时,micro-F1 可能看起来还不错,但实际小类几乎不可用。macrolavg 把每个类别一视同仁,才能暴露小类问题。
5. 常见评估误区和踩坑经验
5.1 只有准确率没有混淆矩阵,等于白做
我见过不少项目汇报的 PPT 上只写一个 accuracy,然后写“模型表现良好”。每次我都刨根问底:FP 多少,FN 多少?这时候很多人答不上来,因为根本没存混淆矩阵。
我强烈建议,跑完测试之后第一件事不是打印 accuracy,而是打印 confusion matrix。你一眼就能看出模型到底错在哪里,是容易把 A 类错分成 B 类,还是把负例大量误报。这是后面所有调优工作的事实基础。
用 sklearn 输出也很简单:
from sklearn.metrics import confusion_matrix cm = confusion_matrix(y_true, y_pred) print(cm)5.2 测试集和训练集分布不一致,指标全是假的
这个坑特别隐蔽。你训练时数据分布是 1:1,到了线上数据分布变成 1:20,模型在测试集上的 precision、recall 再高,上线也会翻车。更麻烦的是,线上数据无法实时拿到真标签,你很难快速评估模型效果。
我实践中常用“分层抽样”来保证测试集与训练集具有相似的类别分布。如果数据有强烈的时间特性,不要随机打乱后切分,而要用前一段时间训练、后一段时间验证,否则模型会在“时间穿越”上虚假刷分。
5.3 F1 高不代表模型可上线,你还要看业务阈值
模型输出的概率分数,在二分类任务里往往以 0.5 为默认阈值。但实际上 0.5 未必是业务最优切分点。比如你的模型输出 [0.49, 0.51, 0.52, 0.1, 0.2],阈值 0.5 只把 0.51 和 0.52 预测为正,召回率很低。如果调低阈值到 0.3,就能捞回更多真正例,代价是 FP 增多。
这个调阈值的思路,本质上就是在 PR 曲线上寻找权衡点。很多调参手段比如调整 class weight,在模型训练阶段影响概率分布,而阈值可以在模型训练完之后单独调。我经常先跑出概率,再用验证集找最佳阈值,而不是死守默认 0.5。
5.4 指标在代码里的隐藏坑:label 顺序不一致
看这一行代码:
print(precision_score(y_true, y_pred, average='binary'))如果y_true和y_pred中正类不是 1,而是字符串字符串比如"yes"/"no",sklearn 会直接报错。而如果你把类别从 0/1 改成 1/2,不指定pos_label,算出来的也是完全不同的 precision。
我建议在脚本里加一句断言,提前确认标签编码符合预期:
assert set(y_true) == {0, 1}, "label must be binary 0/1"尤其当你跑过多轮实验,y_true来源于不同版本数据时,标签很容易被漏改。这个坑我踩过不止一次。
6. 一个典型的“准确率等于召回率”排查实录
6.1 问题现场描述
之前有个同学跑二分类实验,拿到的结果让他很困惑:
- accuracy = 0.72
- precision = 0.83
- recall = 0.72
- F1 = 0.77
他说:“你看 accuracy 和 recall 刚好一样,是不是我模型已经收敛到某种最优状态了?”
我第一反应是:先看 confusion matrix。
6.2 排查过程
打印混淆矩阵后,发现:
- TP = 216
- FP = 44
- FN = 36
- TN = 124
于是 accuracy = (216 + 124) / (420) = 0.8095 左右,等等,这跟报告里的 0.72 对不上。明显他代码里计算 accuracy 时用的分母不对,或者 y_pred 和 y_true 没对齐。
再检查后,发现他把测试集里一批样本的顺序打乱了,y_pred和y_true索引错位。索引错位会同时影响所有指标,只是不同指标对错误样本分布的敏感性不一样,导致某个巧合下 recall 刚好等于原来的 accuracy。
所以,当多个指标出现明显的不合理巧合时,优先怀疑数据对齐问题,而不是模型优化问题。
6.3 排查建议清单
下面这张表是我个人排查评估指标异常时的速查表,分享给你:
| 现象 | 优先排查项 | 具体操作 |
|---|---|---|
| accuracy 和 recall 一模一样 | 标签定义、数据对齐 | 打印 confusion matrix,检查 TP/FP/FN/TN 是否合理;检查 y_true 与 y_pred 长度和索引 |
| precision 异常高,recall 异常低 | 阈值设置过高 | 降低阈值,或者画 PR 曲线寻找平衡点 |
| F1 很高但业务效果差 | 类别不平衡/代价不对称 | 换 macro-F1;看各类别 recall;对少数类单独评估 |
| 测试集表现好,线上差 | 数据分布不一致 | 分层抽样;按时间划分训练/测试集 |
| 多分类指标爆炸或异常 | average 参数设置错误 | 明确用 macro/micro/weighted,注意正类编码 |
7. 工程实践中的几个工具与技巧
7.1 用 classification_report 快速看全貌
sklearn 里有一行代码可以同时输出 precision、recall、f1-score、support,非常方便:
from sklearn.metrics import classification_report print(classification_report(y_true, y_pred, target_names=['class_0', 'class_1']))这会按类别输出每一项指标,并且自动计算 macro avg 和 weighted avg。我建议每个实验跑完后先看这个 report,而不是只打印一个指标。
7.2 交叉验证时不要直接算平均值
很多人做 K 折交叉验证时,会把每一折的指标求算术平均。这在大多数情况下没问题,但有一种情况会失真:某几折的正例特别少,precision 和 recall 波动很大,简单平均后方差极大。
更好的做法是:把每一折的预测概率保存下来,然后在全部验证样本上统一计算一次指标。或者更严谨点,先保存每一折的 TP、FP、FN、TN,再把它们汇总成一张大混淆矩阵,最后基于汇总矩阵计算指标。这样算出来的指标更接近模型在真实数据分布上的表现。
7.3 别忘了看一眼 ROC 和 AUC,但它们和 F1 不是一回事
AUROC(area under the ROC curve)衡量的是模型对正负样本排序能力的综合表现,它基本不依赖分类阈值。而 F1 依赖你最终选择的阈值,因此两者经常会出现 ROC 很高但 F1 很低的情况——特别是当最佳阈值不在 0.5 附近时。
所以完整评估一个模型我习惯这样做:
- 先看 ROC-AUC,确定模型有没有排序能力。
- 再画 PR 曲线,找到业务可接受的 precision/recall 平衡点。
- 在平衡点上汇报 accuracy、precision、recall、F1,同时附上混淆矩阵。
只报一个指标,永远是片面的。工程师的价值不只是把模型跑出来,还要把模型到底行不行说清楚。
8. 后续扩展与经验收尾
我自己做模型评估到现在,最大的一个体会是:先确定业务目标,再决定看什么指标。技术指标的选择从来不是纯数学问题,而是目标导向的决策过程。你如果连“漏一个客户损失多少”“误杀一个用户损失多少”都没搞清楚,那不管选 accuracy 还是 F1,都只是在自欺欺人。
另一个实用习惯:每次实验把指标结果连同数据版本、特征版本、代码版本一起记录。我吃过亏,同一个模型跑出两组差异很大的指标,最后发现是特征工程代码分支合并时把旧的归一化代码带了进去。这类问题不靠好记性,靠流程和记录。
如果你还有精力继续深挖,建议做两件事:第一,把阈值扫描脚本写出来,把 precision、recall、F1 随阈值变化的曲线都可视化,这能帮你彻底理解指标之间的相互作用;第二,试一下多标签分类下的 hmean / F1 聚合方式,很多论文里提到的macro hmean和micro hmean就是在这个逻辑上扩展出来的。搞懂了二分类的 F1,再看多标签的 hmean,你会发现都是老朋友。
最后再分享一个小技巧,评估完模型存结果的时候,我习惯把混淆矩阵存成 CSV 或图片一起归档,而不是只存一个标量。这样三个月后再来复盘,你能立刻知道当初模型的短板到底在哪里,而不必再跑一遍实验。这个习惯帮我节省了大量“考古”时间,强烈推荐你也试试。