
评估报告上写着 accuracy 0.983评审会上没人提异议模型顺利上线。三周之后业务方找过来说这套风控规则一个坏账都没拦住。回头翻评估日志才发现测试集里坏样本只占 1.7%模型把所有样本都判成了正常光靠这一招就拿到了 98.3% 的准确率。真正例、假正例、真负例、假负例这四个格子没数清楚精确度和召回率一个都没看准确率再漂亮也只是个自欺欺人的数字。这篇就把这四个概念从背定义拉到能上手用的层面不管你是刚学机器学习的学生、要做模型验收的工程师还是只想搞懂业务报表里那几个指标含义的产品看完都能自己算、自己判断该盯哪个数。1. 那份准确率 98% 的报告问题出在哪1.1 一个把所有样本都判成正常的分类器先把场景摆出来。假设你在做交易异常检测测试集一万笔交易其中真正异常的只有 170 笔剩下 9830 笔都正常。现在有一个懒惰的模型它对任何输入都输出正常。算一下它的准确率9830 / 10000 98.3%。这个数字放在任何一份汇报 PPT 里都不难看。但它抓到了几个异常零个。业务方要的是把那 170 笔捞出来模型却一笔都没报。准确率这个指标在这里完全失效了因为它把预测对正常样本和预测对异常样本当成了同等重要的事而现实中这两件事的价值差着几个数量级。这就是类别不平衡场景下最常见的评估陷阱当某一类样本占绝对多数时准确率会被多数类绑架。你优化准确率模型最省力的做法就是永远预测多数类。想绕开这个坑就必须把预测对这件事拆开看——拆成四种情况也就是混淆矩阵的四个格子。1.2 混淆矩阵把对与错拆成四个格子混淆矩阵Confusion Matrix这个名字起得挺形象它就是把模型搞混的地方摊开给你看。二分类问题里每个样本的最终结果只有四种可能对应四个格子真正例True Positive, TP事实是正类模型也判成正类。判对了。假正例False Positive, FP事实是负类模型却判成正类。误报。真负例True Negative, TN事实是负类模型也判成负类。判对了。假负例False Negative, FN事实是正类模型却判成负类。漏报。四个格子加起来等于样本总数TP FP TN FN N。这个恒等式看着简单但它是后面所有指标的根基——精确度、召回率、F1、特异度、ROC 曲线全都是从这四个数里做加减乘除变出来的。换句话说只要你把这四个格子数对了剩下的指标都是算术题。注意不同教材、不同工具里混淆矩阵的行列摆放顺序并不统一。有的把预测值放行、真实值放列有的正好反过来。看别人的图表之前先确认一下行列标签否则 TP 和 TN 很容易看反。2. 四个名字为什么这么起拆词就拆明白了2.1 第一个字说对错第二个字说预测成了什么这四个中文译名其实翻译得相当讲究只要会拆词就不会记混。规则是前一个字真/假描述这次预测对不对后两个字正例/负例描述模型预测成了什么。按这个规则套一遍真正例预测成了正例而且这个判断是真的对的→ 事实也是正例。假正例预测成了正例但这个判断是假的错的→ 事实是负例。真负例预测成了负例而且这个判断是真的对的→ 事实也是负例。假负例预测成了负例但这个判断是假的错的→ 事实是正例。关键在于真正例/假正例里的正负说的是预测结果的标签不是事实的标签。很多人记混就是因为下意识把假正例理解成假的正例以为事实是正例只是判错了——恰恰相反假正例的事实是负例是模型硬把一个负例抬成了正例。2.2 拿垃圾邮件过滤器当例子过一遍抽象定义记不住就挂一个具体场景。假设你训练了一个垃圾邮件过滤器垃圾邮件是正类正常邮件是负类。一封真垃圾邮件被拦进垃圾箱 →真正例。你想要的拦截成功。一封正常邮件被误判成垃圾、扔进垃圾箱 →假正例。误杀用户会骂人。一封正常邮件被正确放进收件箱 →真负例。风平浪静。一封真垃圾邮件被放进了收件箱 →假负例。漏网之鱼用户会烦。再换一个医疗筛查的场景患病是正类病人被正确诊断为患病 → 真正例。健康人被误诊为患病 → 假正例虚惊一场要复查。健康人被正确判为健康 → 真负例。病人被漏诊为健康 → 假负例这个最要命。同一个模型结构换一个业务场景FP 和 FN 的严重程度完全不同。垃圾邮件场景里 FP 更讨厌疾病筛查场景里 FN 更致命。这也是后面选择指标时要看业务的原因。2.3 一张表把四个格子钉在脑子里把上面的内容压成一张表行是模型预测列是客观事实事实正例事实负例预测正例真正例 TP假正例 FP预测负例假负例 FN真负例 TN对角线上的 TP 和 TN 是判对的反对角线上的 FP 和 FN 是判错的。这份表建议在草稿纸上画三遍比看十遍文字描述都管用。我自己的习惯是在代码注释里直接写死这个布局免得过两个月回头改动时行列搞混。还有一个容易忽略的点正例到底是哪一类是你自己定义的不是数据自带的。sklearn 默认认为标签值大的那一类或者标签为 1 的那一类是正例。如果你想评估的是少数类而不是标签为 1 的类就得手动指定pos_label否则算出来的精确度和召回率可能刚好是反的这种错误非常隐蔽报表看起来一切正常结论却完全颠倒。3. 精确度与召回率两个问题两种问法3.1 精确度问的是你报出来的有多少靠谱精确度Precision有些地方也叫查准率公式是Precision TP / (TP FP)看分母所有被模型判为正例的样本。也就是说精确度评价的是模型报出来的这批正例有多少是真的。拿医院打比方。医生给一批病人做了手术精确度问的就是这刀开下去的人里有多少是真的需要开刀的。分母是被开了刀的人分子是开对了的人。如果精确度低说明医生乱开刀的多把很多不需要手术的人也推上了手术台。精确度关心的是误报FP。FP 越多分母越大而分子不变精确度就往下跌。所以精确度这个指标天生对狼来了很敏感。3.2 召回率问的是该找出来的你找到了多少召回率Recall也叫查全率公式是Recall TP / (TP FN)看分母所有事实上为正例的样本。召回率评价的是这批真正该被找出来的样本模型找出来了多少。还用医院的例子。召回率问的是所有真正患病的病人里有多少被诊断出来了。分母是所有真病人分子是被揪出来的病人。召回率低说明漏诊多有病的人没查出来混在健康人群里走了。召回率关心的是漏报FN。FN 越多分母不变而分子越小召回率就往下跌。所以召回率这个指标天生对漏网之鱼很敏感。3.3 分母的差别就是全部的差别很多人搞不清这两个指标问题都出在没盯住分母。把公式并排写出来看Precision TP / (TP FP)分母是预测为正例的总数也就是混淆矩阵里预测正例那一行的和。Recall TP / (TP FN)分母是事实为正例的总数也就是混淆矩阵里事实正例那一列的和。一个看行一个看列这是它们最本质的区别。所以有个很好用的记忆法精确度沿着预测方向横着看召回率沿着事实方向竖着看。你只要在草稿纸上画好矩阵用尺子横着划一下算精确度竖着划一下算召回率基本不会错。顺便提两个常跟它俩一起出现的量。一个是特异度Specificity TN / (TN FP)衡量负例里有多少被判对可以理解成负类视角的召回率。另一个是真正例率 TPR TP / (TP FN)你会发现它和召回率的公式一模一样——是的召回率就是 TPR只是换了个名字、换了个使用场合画 ROC 曲线的时候一般叫 TPR。而假正例率FPR FP / (FP TN)分母是全部真实的负例。这几个量互相之间的换算关系很紧密记住一个通常就能推出其余几个。4. 为什么这两个数总在互相拆台4.1 阈值决定模型多大胆的那根滑杆绝大多数分类模型输出的不是一个硬邦邦的类别而是一个概率或者分数。比如逻辑回归给你 0.73梯度提升树给你 -1.2 的 logit。真正把它变成正例/负例的是你设的那条阈值线。阈值就是模型胆子大小的旋钮阈值调低 → 判为正例的门槛变松 → 更多样本被报出来 → 抓到的真阳性变多召回率上升但顺手带进来的假阳性也变多精确度下降。阈值调高 → 只有非常有把握的样本才敢报正 → 报出来的几乎都是真的精确度上升但很多真阳性因为分数不够被压下去了召回率下降。这就解释了为什么这两个指标总在打架它们共享同一个分子 TP但分母一个盯着你报了多少FP 惩罚一个盯着你漏了多少FN 惩罚。你想扩大战果就必然引入噪声你想保证质量就必然放过一些目标。不存在一个阈值能同时让两者都最优除非模型本身足够强。4.2 手算一遍把阈值滑动看明白光讲道理不够直观拿一组具体的分数算一遍。假设有 10 个样本真实标签和模型打分如下1 为正类0 为负类样本真实标签模型分数A10.90B10.80C00.70D10.60E00.55F10.45G00.40H00.30I10.20J00.10真实的状况是正例 5 个A、B、D、F、I负例 5 个C、E、G、H、J。取阈值 0.7严格大于 0.7 才判正只有 A0.90和 B0.80被报出来。TP 2A、B 果然是正例FP 0没有误报FN 5 - 2 3D、F、I 被漏掉Precision 2 / (2 0) 1.00Recall 2 / 5 0.40阈值卡得严报出来的全对但十成里只抓到了四成。取阈值 0.3宽松大于等于 0.3 才判正A、B、C、D、E、F、G、H 都被报出来。TP 4A、B、D、FFP 4C、E、G、H 都是假的FN 1只剩 I 被漏掉Precision 4 / 8 0.50Recall 4 / 5 0.80阈值一放松召回率从 0.40 涨到 0.80精确度从 1.00 掉到 0.50。这组数字把 P-R 权衡演示得清清楚楚没有一个阈值是对的只有更适合当前业务的。手动算这一遍的价值在于你会真正理解为什么模型评估报告里要给出 PR 曲线每个阈值算一组 P、R连成一条线而不是只报一个数。PR 曲线就是把上面这个实验从两个阈值扩展到全部可能的阈值画出来的一条轨迹。4.3 F1 是妥协不是解药P 和 R 打架想用一个数把它俩捏起来最常见的做法是F1 分数F1-ScoreF1 2 × Precision × Recall / (Precision Recall)这是精确度和召回率的调和平均。为什么用调和平均而不是普通的算术平均因为算术平均对极端值太宽容。假设 Precision 1.0、Recall 0.1算术平均是 0.55看着还不错但实际上这个模型基本没检出任何东西。换成调和平均算一下2 × 1.0 × 0.1 / (1.0 0.1) 0.18一下子把短板暴露出来了。调和平均的特点是被小的那个数拖后腿正好符合两个指标都要过得去才算好模型的诉求。如果业务上对某一侧更敏感还有带权版本Fββ 大于 1常见取 2时更看重召回率β 小于 1常见取 0.5时更看重精确度。F2 常用于安全检测、疾病筛查这类漏一个代价很大的场景。不过要提醒一句F1 是个妥协产物它把两个有明确业务含义的数字压成了一个没有明确业务含义的数字。汇报给业务方的时候我不建议只丢一个 F1 过去还是要把 P 和 R 分开报再说清楚当前阈值是怎么选的。F1 适合在你需要横向对比多个模型、快速排序的时候用。5. 该盯精确度还是召回率算一下错一次的代价5.1 两种错法的代价从来不对称选指标的本质是比较FP 的代价和FN 的代价。这两个代价在几乎所有真实业务里都不相等有时甚至差好几个数量级。一个朴素的判断方法问业务方一句话——报错了和被漏掉你更怕哪个更怕报错FP 代价高→ 优先保精确度。更怕漏掉FN 代价高→ 优先保召回率。5.2 宁可错杀和宁可放过的典型场景优先保召回率的场景宁可错杀不可放过疾病初筛漏诊一个病人可能延误治疗甚至危及生命误诊一个健康人只是多做几项复查代价可控。反欺诈预警漏掉一笔欺诈交易是真金白银的损失误报一笔正常交易顶多让人工审核一下。召回阶段的信息检索搜索引擎的第一轮抽取宁可多召回一些候选精确排序交给后面几轮做。工业设备异常预警漏掉一次设备异常可能导致整条产线停机误报一次只是多一次巡检。优先保精确度的场景宁可放过不可错杀垃圾邮件拦截把所有正常邮件误判成垃圾用户直接不用了漏掉几封垃圾邮件用户手动删一下就行。自动化决策/自动执行类系统比如自动扣款、自动封号误触发一次就是事故宁可先不动交给人工兜底。司法或信用类辅助判断把无辜的人卷进来代价远大于放过个别案例。推荐系统的强干预推送一条明显不相关或者冒犯性的内容用户对产品的信任会直接打折。5.3 一张对照表帮你快速定位业务场景更怕哪种错首要指标辅助指标疾病初筛漏诊 FN召回率F2、特异度垃圾邮件拦截误杀 FP精确度F0.5、FPR欺诈交易预警漏报 FN召回率PR-AUC搜索召回阶段漏召回 FN召回率RecallK自动扣款/自动封禁误触发 FP精确度精确度 人工复核量推荐系统两边都要F1 或加权 Fβ线上 A/B 指标表格只能给个起点真实项目里我还是建议用钱算一遍。假设每笔误报的人工审核成本是 5 元每笔漏报的平均损失是 500 元那么 FN 的代价是 FP 的 100 倍模型阈值就该往低了压让召回率优先。把代价量化成货币单位比空泛地争论该不该提高召回有效得多这也是我在实际项目里最常用的说服业务方的手段。5.4 PR 曲线和 ROC 曲线什么时候看哪个评估报告里常见的另外两个图是 PR 曲线和 ROC 曲线。ROC 曲线横轴是 FPR纵轴是 TPR也就是召回率。它的好处是对类别不平衡相对不敏感因为 FPR 的分母是全部真实负例样本里负例再多也不会让 FPR 数值结构崩掉。PR 曲线横轴是召回率纵轴是精确度。它直接暴露了精确度在不平衡数据上的表现。经验上的取舍是当正例非常稀少比如低于 5%的时候优先看 PR 曲线和 PR-AUC。因为此时 ROC 曲线会被大量的真负例撑得很漂亮AUC 看着 0.95实际精确度可能只有 0.2。ROC-AUC 高不代表模型在少数类上有用这在异常检测类项目里是高频踩坑点。6. 从二分类走到多分类宏平均和微平均在平均什么6.1 一对多拆解每个类别轮流当正例现实中大部分问题是多分类比如新闻分类有 10 个类别。多分类的 P/R 是通过**一对多One-vs-Rest**的方式算出来的把第 1 类当正例、其余 9 类当负例算一组 P 和 R再把第 2 类当正例算一组如此循环 10 次得到 10 组指标。关键问题是这 10 组数字怎么合成一个能写进报告的结论这就是宏平均和微平均的分歧所在。6.2 宏平均与微平均一个按类投票一个按样本说话宏平均Macro Average把每个类别的 P、R 分别算出来然后直接求算术平均。每个类别无论样本多少权重都一样。微平均Micro Average先不分类别把所有类别的 TP 加在一起、FP 加在一起、FN 加在一起得到全局的 TP、FP、FN再套公式算一次。效果上等价于把每个样本平等看待。这两者在样本分布均衡时差别不大一旦不均衡就会严重打架。举个例子3 个类别A 类 1000 个样本B 类 10 个C 类 10 个。模型在 A 类上表现很好P0.95, R0.95在 B、C 两个小类上表现很差P0.3, R0.2宏平均三个类别等权每个占 1/3小类的糟糕表现被放大了结果会被拉低到 0.4 左右。微平均A 类样本占绝对多数全局指标基本被 A 类主导结果接近 0.94。同一份预测结果宏平均 0.4、微平均 0.94差了两倍还多。如果你关心的是小类的识别效果比如罕见病、长尾商品就该看宏平均如果关心的是整体吞吐的准确程度看微平均更合适。还有第三种叫加权平均Weighted Average按每个类别的样本数加权是宏平均和微平均之间的折中。sklearn 的classification_report三种都会给你但很多人只看最后一行微平均把真正暴露问题的小类数字忽略了。6.3 多分类下真正例假正例怎么数从每个类别轮流当正例这个角度看多分类的混淆矩阵其实就是把二分类的四个格子扩展成了 K×K 的矩阵。第 i 行第 j 列的元素表示事实是第 i 类、被预测成第 j 类的样本数。对角线上的数字i j就是每一类的真正例数 TP。第 i 行的非对角元素之和是事实第 i 类但被预测成别的类总数也就是第 i 类的假负例 FN。第 j 列的非对角元素之和是事实不是第 j 类却被预测成第 j 类总数也就是第 j 类的假正例 FP。所以看多分类报告时最该盯的不是那些对角线上的大数字而是非对角线上的异常值——哪两类之间频繁混淆往往暗示这两类的特征空间有重叠或者标注标准本身就不清晰。这个信息比一个总体的 F1 有用得多也是我在模型调优阶段最先看的地方。7. 代码实操手算和库函数对不上时怎么查7.1 用 sklearn 跑一遍和手算结果对齐理论过完落到代码上验证一遍。下面这段就是用刚才那 10 个样本的数据验证第 4 节手算的结果import numpy as np from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score y_true np.array([1, 1, 0, 1, 0, 1, 0, 0, 1, 0]) scores np.array([0.90, 0.80, 0.70, 0.60, 0.55, 0.45, 0.40, 0.30, 0.20, 0.10]) for thr in (0.7, 0.3): y_pred (scores thr).astype(int) cm confusion_matrix(y_true, y_pred) print(f阈值 {thr} 混淆矩阵:\n{cm}) # 注意 pos_label 和 zero_division 要显式指定否则警告一堆 print(Precision:, precision_score(y_true, y_pred, pos_label1, zero_division0)) print(Recall: , recall_score(y_true, y_pred, pos_label1, zero_division0)) print(F1: , f1_score(y_true, y_pred, pos_label1, zero_division0)) print(- * 30)跑出来阈值 0.7 时 Precision 1.0、Recall 0.4阈值 0.3 时 Precision 0.5、Recall 0.8和第 4 节手算的完全一致。这种先手算再跑库的对照习惯非常值得养成尤其在你第一次接触新指标的时候它可以帮你确认自己对公式的理解和库实现的定义是一致的。confusion_matrix返回的矩阵布局是[[TN, FP], [FN, TP]]跟第 2 节那张表行是预测、列是事实的摆放不一样。第一次看很容易看反建议打印之后立刻用纸笔对照确认一遍。7.2 几个把我坑得比较惨的地方说几个实际排查过的坑都是文档里写得不显眼、但踩上去很疼的第一正例标签的定义。precision_score默认把 1 当正例如果标签是字符串或别的数字要么报错要么悄悄按错误的类别算。我遇到过一次标签是yes/no的项目同事直接跑precision_score代码没报错但自动把no当正例因为字母序算出来的精确度和预期完全反了白排查了两小时。解决办法是永远显式传pos_label。第二average参数的默认行为。二分类时averagebinary是对的多分类如果不显式传averagemacro或weighted会直接报错或者给出一个你并不想要的结果。这个参数在多分类项目里必须写死。第三predict出来的结果本身就是某个阈值下的硬分类。很多人以为model.predict()是一个客观结果其实对二分类模型来说它就是概率大于 0.5 之后做的截断。你想调整 P/R 平衡改阈值就行不需要重新训练模型。但要注意如果阈值不是 0.5那就不能用predict了得自己拿predict_proba算。第四zero_division参数。当某个类别一个样本都没被预测出来时精确度会出现 0/0sklearn 会给你个警告并默认填 0。这在长尾数据里非常常见看起来是 0 分但其实是没预测两者的业务含义完全不同。7.3 一张自查清单验收前过一遍模型交付前我一般会按这个清单过一遍也能省掉不少返工检查项为什么重要正例是哪一个类别是否显式指定指定错了指标全反测试集的正负比例是多少决定该看 PR 还是 ROC阈值是怎么选的和业务约定一致吗影响上线后的实际 P/R多分类的 average 参数是什么宏/微差异可能是几倍类别不平衡时是否看了宏平均小数类效果会被掩盖是否同时报了精度和召回单一指标无法反映全貌模型评估这件事说到底就是把预测对了这四个字拆开算清楚。真正例、假正例、真负例、假负例这四个格子是所有指标的根精确度和召回率分别从预测行和事实列两个方向盯住它们。我自己的习惯是拿到任何一个分类问题先在纸上画一遍矩阵标出 FP 和 FN 分别对应业务里的什么损失再去决定优化哪个方向。这么做之后和业务方的沟通顺畅多了因为你谈的不再是精确度 0.85而是误报成本和漏报成本哪个更高。指标是给人做决策用的脱离了业务语境再精确的数字也说明不了什么。