1. 从一次模型评估翻车说起为什么Accuracy会骗人刚入行做分类模型那会儿我特别迷信准确率。只要accuracy冲到0.95以上就觉得模型稳了可以打包上线。直到有一次做一个设备异常检测的项目正样本占比不到2%我训出来的模型准确率高达98.6%兴冲冲拿给团队看结果一跑混淆矩阵正样本一个都没召回来——模型把所有样本全判成了正常。那一刻我才真正理解在不平衡数据面前Accuracy就是个彻头彻尾的骗子。这件事之后我开始认真研究F1-score以及它在多分类场景下的两个变体Macro-F1和Micro-F1。这三个指标几乎成了我后来做任何分类任务的标准配置。但说实话我见过太多人在这上面栽跟头有人把Macro-F1和Micro-F1混着用有人不知道Weighted-F1和Macro-F1的区别还有人拿到一个0.9的F1就以为模型很好结果一看每个类别的F1发现小类别全军覆没。这篇内容我想把F1-score这条线彻底讲透。从最基础的Precision和Recall怎么算到F1为什么是它们的调和平均而不是算术平均再到多分类下Macro、Micro、Weighted三种聚合方式的本质区别最后落到实际项目里到底该选哪个指标、怎么用代码算、怎么根据指标反推模型问题。不管你是刚接触机器学习评估的新手还是已经调过不少模型但指标这块一直模模糊糊的从业者这篇应该都能帮你把这块知识补扎实。核心关键词就三个F1-score、Macro-F1、Micro-F1。但我会把Precision、Recall、混淆矩阵、Weighted-F1这些周边概念一并串起来讲因为它们本来就是一条链上的东西单独拎出来讲反而容易断片。2. Precision和RecallF1-score的两块基石2.1 从一个医疗筛查场景理解这两个指标要讲清楚F1必须先讲清楚Precision精确率和Recall召回率。我用一个具体的场景来说。假设某机构做一种疾病的初筛1000个人里有10个真正患病。模型筛出来20个人是阳性其中8个是真的患病另外12个是健康人被误判。这时候Precision精确率 筛出来阳性的人里真正患病的比例 8/20 0.4。它回答的问题是你说是阳性的这些人里有多少是真的Recall召回率 真正患病的人里被筛出来的比例 8/10 0.8。它回答的问题是真正患病的人里你找出来了多少这两个指标天然存在张力。如果你把筛查标准放得很松所有人都判阳性Recall直接拉满到1.0但Precision会惨不忍睹反过来如果你只对极其确定的才判阳性Precision可能很高但大量真正的患者会被漏掉Recall就崩了。这里有个很多人一开始会绕晕的点Precision和Recall的分母完全不同。Precision的分母是模型预测为正的样本数Recall的分母是真实为正的样本数。记住这一点后面多分类聚合的时候就不会乱。2.2 为什么F1用调和平均而不是算术平均既然Precision和Recall是一对矛盾体我们自然想用一个数把它们综合起来。最直觉的做法是取算术平均(PR)/2。但F1用的是调和平均$$F1 \frac{2 \cdot P \cdot R}{P R}$$为什么不用算术平均关键在于调和平均对短板更敏感。举个例子P1.0R0.1算术平均 (1.0 0.1)/2 0.55看起来还行调和平均 2×1.0×0.1/(1.00.1) 0.2/1.1 ≈ 0.18直接暴露了问题调和平均的特性就是只要有一个值很低整体就会被拉得很低。这正好符合我们对分类模型的要求——Precision和Recall不能偏科偏科就说明模型有问题。算术平均会掩盖这种偏科调和平均不会。我个人的经验是当你看到一个模型的F1明显低于P和R的算术平均时基本可以断定这个模型在某一侧严重偏科这时候就该去翻混淆矩阵看看到底是哪出了问题。2.3 一个容易踩的坑F1高不代表模型好这里必须泼一盆冷水。F1-score是一个综合指标但它同样可以被刷。在极度不平衡的数据上如果模型对少数类完全放弃只把多数类做好F1依然可能看起来不错——因为多数类的样本量大对整体指标的贡献占主导。我踩过的一个坑是某次做文本分类5个类别其中一类占了85%的样本。模型训完F1有0.87我一度以为可以交付了。后来按类别拆开看那个占85%的大类F1是0.95剩下四个小类F1全在0.3以下。整体F1被大类硬生生撑起来了。这就是为什么多分类场景下光看一个总的F1是不够的必须看Macro-F1和Micro-F1。这也是下一节要展开的核心。3. 多分类下的三种聚合方式Macro、Micro、Weighted的本质区别3.1 多分类为什么不能直接套二分类F1二分类只有一个正类Precision和Recall各算一个值F1也就一个值。但多分类有N个类别每个类别都有自己的Precision和Recall那最后怎么汇总成一个数这就产生了不同的聚合策略。假设我们有3个类别A、B、C。对于每个类别我们都可以把它当成是不是A的二分类问题算出一组P、R、F1。这样我们就得到了3组F1值。问题来了怎么把这3个F1合成一个不同的合成方式就是Macro、Micro、Weighted的区别。它们没有谁对谁错只是关注点不同用错了场景就会得出误导性的结论。3.2 Macro-F1每个类别一视同仁Macro-F1的做法最简单粗暴把每个类别的F1算出来然后直接取算术平均。$$Macro\text{-}F1 \frac{1}{N}\sum_{i1}^{N} F1_i$$它的核心特点是不考虑每个类别的样本量所有类别权重相同。不管A类有10000个样本还是只有10个样本在Macro-F1眼里都一样重要。这意味着什么意味着Macro-F1对小类别特别敏感。如果某个小类别模型学得很差F1只有0.1哪怕它只有5个样本也会把Macro-F1拉下来一大截。反过来如果所有类别都做得比较均衡Macro-F1就会比较好看。所以Macro-F1适合什么场景当你关心每一个类别尤其是那些样本少的类别时。比如医学影像里罕见病的识别、工业质检里的稀有缺陷检测这些场景下小类别恰恰是最重要的Macro-F1就是首选指标。3.3 Micro-F1被大类别主导的全局指标Micro-F1的算法完全不同。它不先算每个类别的F1再平均而是把所有类别的TP、FP、FN先加起来用全局的TP、FP、FN算出一个总的Precision和Recall再算F1。$$Micro\text{-}P \frac{\sum TP_i}{\sum TP_i \sum FP_i}, \quad Micro\text{-}R \frac{\sum TP_i}{\sum TP_i \sum FN_i}$$$$Micro\text{-}F1 \frac{2 \cdot Micro\text{-}P \cdot Micro\text{-}R}{Micro\text{-}P Micro\text{-}R}$$关键点来了在多分类问题中每个样本只属于一个类别且预测也只选一个类别Micro-F1在数值上等于Accuracy。这不是巧合因为在这种设定下全局的TP之和就是预测正确的样本数而TPFP或TPFN就是总样本数所以Micro-Precision Micro-Recall Accuracy。这个结论我第一次看到的时候挺震惊的但推导一遍就明白了。它的实际含义是Micro-F1会被样本量大的类别主导。大类做得好Micro-F1就高小类做得再差因为样本少对全局TP、FP、FN的影响也小所以Micro-F1几乎不受影响。3.4 Weighted-F1按样本量加权的折中方案Weighted-F1是Macro-F1的一个变体。它也是先算每个类别的F1但不是简单平均而是按每个类别的样本量加权平均$$Weighted\text{-}F1 \sum_{i1}^{N} \frac{n_i}{n_{total}} \cdot F1_i$$其中$n_i$是第i类的样本数。这样样本多的类别权重大样本少的类别权重小。它介于Macro和Micro之间比Macro更偏向大类别但不像Micro那样完全被大类别吞没。3.5 三种聚合方式对比一张表看清差异指标计算方式权重特点对小类别的敏感度典型适用场景Macro-F1各类F1算术平均所有类别等权极高类别均衡、或小类别重要Micro-F1全局TP/FP/FN算F1按样本量隐式加权极低≈Accuracy关心整体正确率Weighted-F1各类F1按样本量加权样本量加权中等类别不平衡但无特别偏好一个实用的判断口诀Macro看短板Micro看大盘Weighted看折中。拿到一组指标先看Macro和Micro的差距。差距大说明类别间表现不均衡小类别拖后腿了差距小说明各类别表现比较一致。3.6 一个具体数字例子把三者算清楚光说理论容易飘我用一组具体数字把三种F1都算一遍。假设3分类混淆矩阵如下行是真实列是预测真实\预测ABC合计A801010100B520530C10106080先算每个类别的P、R、F1A类TP80FP51015FN101020。P80/95≈0.842R80/1000.8F12×0.842×0.8/(0.8420.8)≈0.820B类TP20FP101020FN5510。P20/400.5R20/30≈0.667F12×0.5×0.667/(0.50.667)≈0.571C类TP60FP10515FN101020。P60/750.8R60/800.75F12×0.8×0.75/(0.80.75)≈0.774现在算三种聚合Macro-F1 (0.820 0.571 0.774)/3 ≈0.722Micro-F1全局TP802060160全局FP15201550全局FN20102050。Micro-P160/210≈0.762Micro-R160/210≈0.762Micro-F1≈0.762等于Accuracy160/210≈0.762验证了前面的结论Weighted-F1 (100/210)×0.820 (30/210)×0.571 (80/210)×0.774 ≈ 0.390 0.082 0.295 ≈0.767可以看到B类表现最差F1只有0.571Macro-F1被它拉得最低0.722而Micro-F1和Weighted-F1因为B类样本少受影响较小。这就是三种聚合方式差异的直观体现。4. 手写代码把三种F1算一遍不依赖sklearn也能搞懂4.1 为什么建议先手写一遍很多人用sklearn.metrics.f1_score用得很顺手但一旦指标对不上就开始抓瞎。我的建议是至少手写一遍Macro和Micro的计算逻辑。手写的过程会逼你搞清楚TP、FP、FN在多分类下到底怎么定义也会让你明白为什么Micro-F1等于Accuracy。这个理解一旦建立以后看任何指标都不会再迷糊。下面这段代码不依赖sklearn纯用numpy实现逻辑清晰可以直接跑。import numpy as np def compute_f1_metrics(y_true, y_pred, num_classes): 手写计算多分类的Macro-F1、Micro-F1、Weighted-F1 y_true, y_pred: 一维数组元素为类别索引 0 ~ num_classes-1 # 初始化每个类别的TP、FP、FN TP np.zeros(num_classes) FP np.zeros(num_classes) FN np.zeros(num_classes) for c in range(num_classes): # 第c类真实为c且预测为c - TP TP[c] np.sum((y_true c) (y_pred c)) # 第c类真实不为c但预测为c - FP FP[c] np.sum((y_true ! c) (y_pred c)) # 第c类真实为c但预测不为c - FN FN[c] np.sum((y_true c) (y_pred ! c)) # 每个类别的Precision、Recall、F1 precision TP / (TP FP 1e-10) recall TP / (TP FN 1e-10) f1_per_class 2 * precision * recall / (precision recall 1e-10) # Macro-F1各类F1算术平均 macro_f1 np.mean(f1_per_class) # Micro-F1全局TP/FP/FN total_TP np.sum(TP) total_FP np.sum(FP) total_FN np.sum(FN) micro_p total_TP / (total_TP total_FP 1e-10) micro_r total_TP / (total_TP total_FN 1e-10) micro_f1 2 * micro_p * micro_r / (micro_p micro_r 1e-10) # Weighted-F1按各类样本量加权 support TP FN # 每类真实样本数 weights support / np.sum(support) weighted_f1 np.sum(weights * f1_per_class) return { per_class_f1: f1_per_class, macro_f1: macro_f1, micro_f1: micro_f1, weighted_f1: weighted_f1, accuracy: np.mean(y_true y_pred) } # 用上一节的例子验证 y_true np.array([0]*100 [1]*30 [2]*80) # 构造预测A类80对10错10错B类20对C类60对 y_pred np.array( [0]*80 [1]*10 [2]*10 # A类真实样本的预测 [0]*5 [1]*20 [2]*5 # B类真实样本的预测 [0]*10 [1]*10 [2]*60 # C类真实样本的预测 ) result compute_f1_metrics(y_true, y_pred, num_classes3) for k, v in result.items(): print(k, v)跑出来的结果应该和上一节手算的一致Macro-F1≈0.722Micro-F1≈0.762Weighted-F1≈0.767Accuracy≈0.762。Micro-F1和Accuracy相等这一点在这段代码里会得到直接验证比看公式推导印象深得多。4.2 用sklearn对照验证手写完之后再用sklearn对一遍确认自己没算错from sklearn.metrics import f1_score, accuracy_score print(Macro-F1:, f1_score(y_true, y_pred, averagemacro)) print(Micro-F1:, f1_score(y_true, y_pred, averagemicro)) print(Weighted-F1:, f1_score(y_true, y_pred, averageweighted)) print(Accuracy:, accuracy_score(y_true, y_pred))average参数就是控制聚合方式的macro、micro、weighted分别对应三种。还有一个samples那是多标签场景用的多分类用不上别搞混。一个我踩过的坑f1_score默认的average参数是binary只适用于二分类。如果你做多分类却忘了改averagesklearn会直接报错或者给出错误结果。这个坑我见过不止一个人踩尤其是从二分类任务切到多分类的时候。4.3 混淆矩阵指标异常时的第一现场指标算出来只是结果要定位问题还得看混淆矩阵。我习惯在算完F1之后立刻打一张归一化的混淆矩阵一眼就能看出哪个类别被误判成了哪个类别。from sklearn.metrics import confusion_matrix import matplotlib.pyplot as plt import seaborn as sns cm confusion_matrix(y_true, y_pred, normalizetrue) sns.heatmap(cm, annotTrue, fmt.2f, cmapBlues) plt.xlabel(Predicted) plt.ylabel(True) plt.title(Normalized Confusion Matrix) plt.show()normalizetrue是按真实类别归一化也就是每一行加起来等于1对角线上的值就是每个类别的Recall。这样看比原始计数直观得多尤其当类别样本量差异很大时。5. 指标选型实战什么场景该盯哪个F15.1 类别均衡时三个指标差别不大随便看如果各个类别的样本量差不多Macro、Micro、Weighted算出来会很接近。这种情况下用哪个都行我一般直接看Macro-F1因为它对每个类别都公平不会因为某个类别样本稍多就偏袒它。5.2 类别严重不平衡时Macro和Micro必须一起看这是最容易出问题的场景。我的习惯是同时打印Macro-F1和Micro-F1然后看它们的差值。如果Macro-F1远低于Micro-F1说明小类别表现很差模型在偏科。这时候要去看小类别的Recall大概率是模型把小类别样本都判成了大类。如果两者接近说明各类别表现比较均衡模型没有明显偏科。举个我实际遇到的例子一个5分类的工业缺陷检测任务最大类占70%最小类占2%。模型训完Micro-F1有0.91看着挺好但Macro-F1只有0.63。一查混淆矩阵最小类几乎全被判成了最大类。后来通过加类权重、过采样小类别把Macro-F1拉到了0.82Micro-F1略降到0.89。虽然Micro降了一点但模型实际可用性大幅提升——因为那些小类别恰恰是真正需要检出的缺陷。5.3 业务上小类别更重要时Macro-F1是唯一标准有些场景下小类别才是业务核心。比如金融风控里的欺诈交易、医疗里的罕见病、安全领域的异常行为这些类别样本天然稀少但漏检代价极高。这种场景下Micro-F1再高也没意义必须盯Macro-F1甚至要单独看每个关键类别的Recall。我一般会为这类场景单独建一个关键类别指标表把每个关键类别的P、R、F1列出来而不是只看一个汇总数。因为汇总数会掩盖问题而业务方真正关心的是那类最重要的样本你抓出来了多少。5.4 一个决策流程图用文字描述选指标的逻辑其实可以总结成几句话先看类别是否均衡。均衡就直接看Macro-F1。不均衡先看Macro和Micro的差值。差值大说明小类别有问题重点优化小类别。如果业务上所有类别同等重要以Macro-F1为准。如果业务上只关心整体正确率以Micro-F1即Accuracy为准。如果业务上大类别更重要看Weighted-F1。无论哪种情况都要拆开看每个类别的F1不要只看汇总。我个人的铁律任何多分类任务汇报结果时至少给出Macro-F1、Micro-F1和每个类别的F1。只给一个数要么是不懂要么是想藏问题。6. 那些年我在F1上踩过的坑和总结的经验6.1 坑一把Micro-F1当成更好的Macro-F1这是我见过最普遍的误解。很多人觉得Micro-F1数值更高就以为它更好。其实它们衡量的根本不是一回事。Micro-F1高只说明整体预测对的样本多不代表每个类别都好。在类别不平衡时Micro-F1高完全可能是因为模型把大类做好了小类全放弃。数值高低不代表指标优劣只代表关注点不同。6.2 坑二忽略样本量为0的类别如果某个类别在验证集里一个样本都没有Macro-F1计算时会出现除零问题。sklearn默认会给出警告并把该类F1设为0但这个0会拉低Macro-F1造成误导。我的做法是在算指标前先检查每个类别的support样本量为0的类别要么从评估中剔除要么单独说明不要让它默默影响结果。6.3 坑三用F1做模型选择的唯一依据F1是个好指标但不能唯F1论。我见过有人为了刷高F1把分类阈值调得特别极端结果模型在实际业务里完全没法用。F1只是评估的一部分还要结合业务成本、推理延迟、模型稳定性一起看。尤其是当FP和FN的业务代价不对称时比如漏检一个缺陷的代价远大于误报单纯优化F1可能不是最优解这时候应该考虑带权重的F-beta分数。6.4 经验F-beta分数是F1的推广F1是Precision和Recall的等权调和平均。如果业务上更看重Recall比如宁可误报不可漏报可以用F-beta分数beta1时偏向Recallbeta1时偏向Precision。F1就是beta1的特例。sklearn里用fbeta_scorebeta参数控制偏向。这个在实际项目里比F1更灵活值得掌握。6.5 经验交叉验证时看F1的均值和方差单次划分验证集算出来的F1波动可能很大尤其是小数据集。我习惯用分层K折交叉验证把每折的Macro-F1都算出来看均值和标准差。如果标准差很大说明模型对数据划分敏感这时候单次的F1再高也不可信。均值高、方差小才是真的稳。from sklearn.model_selection import StratifiedKFold, cross_val_score from sklearn.metrics import make_scorer, f1_score cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) scorer make_scorer(f1_score, averagemacro) scores cross_val_score(model, X, y, cvcv, scoringscorer) print(Macro-F1 per fold:, scores) print(Mean:, scores.mean(), Std:, scores.std())这段代码是我做模型评估的标准动作之一。StratifiedKFold保证每折里各类别比例一致make_scorer把Macro-F1包装成可用的评分器。跑完看均值和标准差比单次评估靠谱得多。6.6 经验把指标和业务对齐而不是和论文对齐最后说一个观念上的东西。F1、Macro-F1、Micro-F1这些都是工具工具是为业务服务的。我见过太多团队为了在内部评审里刷高指标选了一个和业务目标不匹配的指标结果模型上线后效果一塌糊涂。正确的做法是先想清楚业务上最怕什么——是漏检还是误报是大类还是小类——然后选对应的指标去优化。指标选对了优化方向才不会跑偏。就拿我自己那个设备异常检测项目来说最后我们定的核心指标是异常类的Recall和Macro-F1而不是整体Accuracy。因为漏掉一个异常设备的代价远高于误报几个正常设备。指标一换整个优化方向就清晰了加类权重、调阈值、针对性过采样每一步都有的放矢。这套东西说起来不复杂但真正在项目里用对、用顺还是得踩几个坑才能体会。希望这篇能把F1这条线给你讲透下次再看到Macro-F1和Micro-F1你能一眼看出它们各自在说什么。
阅读完成 · 觉得有帮助?