ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Macro-F1 vs Weighted-F1:不平衡分类评估指标选择指南

Macro-F1 vs Weighted-F1:不平衡分类评估指标选择指南

做分类模型评估的时候,我见过太多人栽在指标选择这一步。模型训练完,classification_report一打印,Macro-F1 和 Weighted-F1 两个数字摆在那儿,一个 0.62,一个 0.89,差了将近三十个点。这时候如果随手挑个好看的写进报告,后面上线翻车几乎是必然的。这两个指标算的都是 F1,名字只差一个词,但在不平衡数据集上,它们描述的几乎是两个不同的世界。这篇内容就把这两个指标掰开揉碎讲清楚:它们各自怎么算、为什么会在不平衡数据上分道扬镳、什么场景该用哪个、以及怎么结合业务目标做最终判断。不管你是刚接触分类评估的新手,还是已经调过几轮模型但一直没搞明白指标差异的老手,看完都能直接拿去用。

1. 先搞清楚这两个指标到底在算什么

1.1 从混淆矩阵出发,F1 的本质是什么

要理解 Macro-F1 和 Weighted-F1 的区别,得先回到 F1 的定义源头。F1 是 Precision(精确率)和 Recall(召回率)的调和平均,公式是F1 = 2 * P * R / (P + R)。用调和平均而不是算术平均,是因为调和平均对极端值更敏感——只要 P 或 R 有一个很低,F1 就会被拉下来。这个特性决定了 F1 天然适合衡量"既要查得准、又要查得全"的场景。

而 P 和 R 本身是从混淆矩阵里算出来的。多分类问题里,每个类别都有自己的 TP、FP、FN,所以每个类别都能算出一个独立的 F1。问题就出在这里:多分类的 F1 不是唯一的,你需要把每个类别的 F1 聚合成一个总分数。Macro 和 Weighted 就是两种不同的聚合方式。

我用一个具体例子贯穿全文,方便你对照。假设一个三分类任务,类别分布是 A 类 1000 个样本、B 类 200 个、C 类 50 个,总共 1250 个样本。模型跑完后各类的 F1 分别是:A 类 0.95、B 类 0.60、C 类 0.30。这个设定很典型——大类学得好,小类学得差,正是不平衡数据集的常见表现。

1.2 Macro-F1:每个类别一票,不管人多人少

Macro-F1 的计算方式简单粗暴:把每个类别的 F1 算出来,然后直接取算术平均。公式是Macro-F1 = (F1_A + F1_B + F1_C) / 3。

代入上面的例子:(0.95 + 0.60 + 0.30) / 3 = 0.6167。

注意这里的关键点:Macro-F1 给每个类别相同的权重,完全无视类别样本量的差异。C 类只有 50 个样本,A 类有 1000 个,但在 Macro-F1 眼里,它们的分量一模一样。这意味着小类的表现会被同等放大——C 类 F1 只有 0.30,直接把总分拉低了三分之一。

这个特性决定了 Macro-F1 的适用场景:当你关心"每个类别是否都被公平对待"时,它是首选。比如医疗诊断里罕见病的识别、工业质检里缺陷类型的分类、内容审核里违规类别的检测,这些场景下小类往往才是业务重点,你绝不能让模型用"放弃小类、保住大类"的方式刷高总分。

提示:Macro-F1 对类别数量敏感。类别越多,单个类别表现差对总分的拖累越明显。如果你有 20 个类别,其中 3 个表现很差,Macro-F1 会明显低于 Weighted-F1,这个差距本身就是信号——说明模型在部分类别上存在系统性缺陷。

1.3 Weighted-F1:按样本量投票,大类别说了算

Weighted-F1 换了个思路:每个类别的 F1 乘以该类别的样本占比,再求和。公式是Weighted-F1 = Σ(F1_i * support_i / N),其中 support_i 是第 i 类的样本数,N 是总样本数。

代入例子:A 类占比 1000/1250 = 0.8,B 类 200/1250 = 0.16,C 类 50/1250 = 0.04。所以Weighted-F1 = 0.95*0.8 + 0.60*0.16 + 0.30*0.04 = 0.76 + 0.096 + 0.012 = 0.868。

看到差距了吗?同一个模型,Macro-F1 是 0.617,Weighted-F1 是 0.868,差了 25 个百分点。原因很直白:Weighted-F1 被大类主导了。A 类占了 80% 的权重,它表现好,总分就好看;C 类只占 4%,它再差也翻不起浪。

这就是 Weighted-F1 的本质——它衡量的是"整体样本层面的平均表现",更接近模型在真实数据分布下的期望性能。如果你的业务场景里类别分布就是真实分布,且每个样本的重要性相同,那 Weighted-F1 是合理的。但如果你关心的是小类,Weighted-F1 会给你虚假的安全感。

1.4 一张表看清两者的计算差异

维度Macro-F1Weighted-F1
聚合方式各类别 F1 算术平均各类别 F1 按样本占比加权平均
类别权重全部相等与样本量成正比
对大类偏好无强
对小类敏感度高低
数值范围通常更低(不平衡时)通常更高(不平衡时)
反映的问题类别间公平性整体样本平均性能

这张表建议存下来。实际工作中我经常遇到有人问"为什么我的 Macro-F1 这么低但准确率很高",答案就在这张表里——准确率和 Weighted-F1 都被大类绑架了,只有 Macro-F1 在替小类说话。

2. 不平衡数据集上,为什么这两个指标会分道扬镳

2.1 不平衡的本质:模型在"偷懒"与指标的盲区

不平衡数据集的核心矛盾是:模型倾向于预测多数类,因为这样能最小化整体损失。假设一个二分类问题,正类占 95%,负类占 5%。模型什么都不学,全预测正类,准确率就有 95%。这时候 Weighted-F1 会是多少?正类 F1 接近 1,负类 F1 是 0(因为负类一个都没预测对),加权后1*0.95 + 0*0.05 = 0.95。看起来很美,但这个模型毫无用处——它完全识别不出负类。

而 Macro-F1 在这种情况下是(1 + 0) / 2 = 0.5,直接暴露了问题。这就是为什么在不平衡场景下,Macro-F1 是更诚实的指标。它不会被大类的漂亮表现掩盖住小类的崩溃。

我踩过的一个坑:早期做某类风险识别项目,正负样本比例大概 1:50。第一版模型 Weighted-F1 有 0.93,团队都很满意,准备上线。后来用 Macro-F1 一算只有 0.51,拆开看小类召回率不到 0.2。如果当时只看 Weighted-F1 就上线,漏掉的风险样本会造成实打实的损失。从那以后,我的评估流程里 Macro-F1 是必看项,Weighted-F1 只作为辅助参考。

2.2 极端不平衡下的数值陷阱

不平衡程度越极端,两个指标的差距越大。我做过一组模拟,固定大类 F1 为 0.95,小类 F1 从 0.9 逐步降到 0.1,看两个指标怎么变:

小类 F1小类占比Macro-F1Weighted-F1差距
0.905%0.9250.94750.0225
0.705%0.8250.93750.1125
0.505%0.7250.92750.2025
0.305%0.6250.91750.2925
0.105%0.5250.90750.3825

看最后一行:小类 F1 已经崩到 0.10 了,Weighted-F1 还有 0.9075,几乎看不出问题。而 Macro-F1 掉到了 0.525,明确告诉你"模型有一半的类别能力不行"。这个表格我经常拿给团队新人看,比讲十遍公式都管用。

注意:不要以为小类占比低就可以忽略。很多业务场景里,小类恰恰是高价值样本——欺诈交易、设备故障、罕见病例,这些类别样本少但单次误判成本极高。用 Weighted-F1 评估这类问题,等于主动蒙上眼睛。

2.3 什么时候 Weighted-F1 反而更合理

说了这么多 Macro-F1 的好话,得客观讲:Weighted-F1 不是一无是处。它的合理性在于——当每个样本的重要性确实相同时,按样本量加权就是正确的聚合方式。

举个场景:一个电商商品分类系统,类别分布本身就不均匀(服饰类多、乐器类少),但业务目标是"整体分类准确率",每个商品分错的代价差不多。这时候 Weighted-F1 更贴近真实业务收益,因为它反映的是"随机抽一个商品,模型分对的期望水平"。Macro-F1 反而会过度惩罚那些样本少但业务上并不特殊的类别。

再比如模型对比阶段,如果你只是想快速判断"新版模型整体是否比旧版好",Weighted-F1 的波动更小、更稳定,适合做粗筛。但粗筛之后,一定要用 Macro-F1 做细看,确认没有牺牲小类换取大类提升。

判断标准可以总结成一句话:看你的业务是"类别公平优先"还是"样本平均优先"。前者用 Macro,后者用 Weighted。拿不准的时候,两个都算,看差距——差距大说明类别间性能不均衡,这时候必须深挖小类。

2.4 指标选择背后的业务逻辑推演

我习惯在项目启动时就定好评估指标,而不是模型跑完再挑。定指标的过程其实是把业务目标翻译成数学语言。具体怎么推演?问三个问题:

第一,错判哪个类别的代价最高?如果小类误判代价高,Macro-F1 权重上调。第二,类别分布在上线后会不会变?如果线上分布和训练集差异大,Weighted-F1 的加权基础就不牢靠,Macro-F1 更稳健。第三,团队考核看什么?如果考核只看总分,大家会不自觉地优化大类,这时候必须用 Macro-F1 把水端平。

这三个问题没有标准答案,但问完你心里就有数了。我见过最糟糕的情况是:业务方要的是小类召回,技术团队报的是 Weighted-F1,两边鸡同鸭讲,上线后才发现指标和需求对不上。提前对齐,比事后扯皮省事得多。

3. 实操:把两个指标算对、看透、用起来

3.1 用 sklearn 一次性算全,别手写公式

实际工作中没人手算 F1,但很多人只会调f1_score的默认参数,连average参数有哪些取值都不清楚。下面这段代码把关键指标一次算全,建议直接抄:

from sklearn.metrics import f1_score, classification_report, confusion_matrix import numpy as np # y_true, y_pred 是你的真实标签和预测标签 # 假设是三分类,标签为 0, 1, 2 # 核心:average 参数决定聚合方式 macro_f1 = f1_score(y_true, y_pred, average='macro') weighted_f1 = f1_score(y_true, y_pred, average='weighted') micro_f1 = f1_score(y_true, y_pred, average='micro') print(f"Macro-F1: {macro_f1:.4f}") print(f"Weighted-F1: {weighted_f1:.4f}") print(f"Micro-F1: {micro_f1:.4f}") # 每个类别的详细指标,必须看 print(classification_report(y_true, y_pred, digits=4)) # 混淆矩阵,定位问题类别 print(confusion_matrix(y_true, y_pred))

几个实操要点。average='macro'就是 Macro-F1,average='weighted'是 Weighted-F1,average='micro'在多分类里等于准确率(因为 micro 把所有类的 TP、FP、FN 汇总后再算,多分类下等价于全局准确率)。classification_report里的support列就是每个类的样本数,加权计算就靠它。

提示:f1_score默认average='binary',只适用于二分类。多分类不显式指定average会报错或给出误导性结果。这个坑我见过不止一个新人踩。

3.2 参数选择背后的计算过程拆解

为什么average='weighted'能得到我们前面手算的 0.868?拆开看 sklearn 内部做了什么:它先对每个类别算 F1,然后按support / total_support加权。用前面的例子验证——A 类 support=1000,B 类 200,C 类 50,total=1250。权重分别是 0.8、0.16、0.04,乘以各自 F1 再求和,正好是 0.868。和手算完全一致。

这里有个容易忽略的细节:Weighted-F1 的权重来自真实标签的分布,不是预测标签的分布。也就是说,它衡量的是"在真实数据分布下"的加权表现。如果你的模型把大量样本误判到某个类,预测分布会偏移,但 Weighted-F1 的权重不受影响。这个设计是合理的,因为我们要评估的是模型在真实分布上的性能,而不是模型自己"以为"的分布。

再补充一个计算细节:当某个类别在预测中从未出现(TP=0 且 FP=0),sklearn 默认该类的 F1 为 0,并且会给出 warning。这种情况在极端不平衡或小类样本极少时会出现,Macro-F1 会被这个 0 严重拉低。遇到这种 warning 不要忽略,它往往意味着模型完全放弃了这个类别。

3.3 结合混淆矩阵定位"谁在拖后腿"

光看两个总分不够,必须下钻到类别级别。我的标准动作是:先看 Macro 和 Weighted 的差距,差距超过 0.1 就拉混淆矩阵,找出 F1 最低的类别,再看它的 FP 和 FN 分布。

继续用前面的例子。C 类 F1 只有 0.30,拉出混淆矩阵发现:C 类 50 个样本里,30 个被预测成 A 类,10 个被预测成 B 类,只有 10 个预测正确。这说明模型对 C 类的特征几乎没学到,倾向于把它归到大类。对应的改进方向很明确:要么给 C 类做重采样/加权,要么检查 C 类的特征是否和其他类有重叠、标注是否有问题。

我整理了一个排查顺序,照着走基本不会漏:

  1. 算 Macro-F1 和 Weighted-F1,记录差距
  2. 差距 > 0.1,进入类别级排查
  3. 按 F1 升序列出所有类别,锁定最差的 2-3 个
  4. 对每个差类别,看混淆矩阵里它主要被误判成谁
  5. 判断是特征问题、样本量问题还是标注问题
  6. 针对性处理后再看 Macro-F1 是否回升

这个流程的价值在于:它把"指标低"这个模糊问题,转化成了"哪个类被误判成哪个类"的具体问题,改进方向自然就出来了。

3.4 交叉验证下的指标稳定性检查

单次划分算出来的 F1 有随机性,尤其小类样本少的时候,换个随机种子结果可能差很多。我的做法是用分层交叉验证,看两个指标的均值和标准差:

from sklearn.model_selection import StratifiedKFold, cross_val_score from sklearn.metrics import make_scorer, f1_score cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) macro_scorer = make_scorer(f1_score, average='macro') weighted_scorer = make_scorer(f1_score, average='weighted') macro_scores = cross_val_score(model, X, y, cv=cv, scoring=macro_scorer) weighted_scores = cross_val_score(model, X, y, cv=cv, scoring=weighted_scorer) print(f"Macro-F1: {macro_scores.mean():.4f} ± {macro_scores.std():.4f}") print(f"Weighted-F1: {weighted_scores.mean():.4f} ± {weighted_scores.std():.4f}")

重点看标准差。如果 Macro-F1 的标准差很大(比如超过 0.05),说明模型对小类的学习不稳定,某些折里小类样本太少导致性能剧烈波动。这时候单次评估的结论不可靠,必须看均值。我遇到过 Macro-F1 均值 0.65 但标准差 0.12 的情况,拆开看发现某一折里小类只有 3 个样本,那一折的 F1 直接是 0。这种波动不是模型问题,是评估设计问题——要么增加数据,要么改用分层更细的划分。

注意:StratifiedKFold的shuffle=True要配合random_state固定,否则每次跑结果不同,没法复现。分层保证每折的类别比例和整体一致,这对不平衡数据是必须的。

4. 常见问题与踩坑实录

4.1 指标虚高:当 Weighted-F1 骗了你

最常见的坑就是前面说的:Weighted-F1 高但模型实际不可用。除了大类主导,还有一种隐蔽情况——类别极度不平衡时,Weighted-F1 会趋近于大类的 F1。如果大类 F1 是 0.98,哪怕所有小类全错,Weighted-F1 也能有 0.93 左右。这个数字放在报告里很漂亮,但业务上小类全漏。

识别方法很简单:把 Macro-F1 和 Weighted-F1 并排看,差距超过 0.15 就要警惕。更直接的办法是看每个类的 recall,小类 recall 低于 0.5 基本可以判定模型没学到小类。我现在的习惯是报告里三个数字一起放:Macro-F1、Weighted-F1、最差类别的 F1。三个数字放一起,任何虚高都藏不住。

4.2 Macro-F1 被"噪声类别"拖累怎么办

反过来也有坑:Macro-F1 太低,但低的原因不是模型差,而是某些类别本身样本极少、标注质量差,属于"噪声类别"。比如某个类只有 5 个样本,还有 2 个标错了,这类别的 F1 天然上不去,却平等地拉低 Macro-F1。

处理思路分两种。如果这些噪声类别业务上不重要,可以在评估时排除,用labels参数指定只算有效类别:

# 只对标签 0 和 1 计算 Macro-F1,排除噪声类别 2 macro_f1_filtered = f1_score(y_true, y_pred, average='macro', labels=[0, 1])

如果噪声类别业务上重要,那就不能排除,得先解决数据和标注问题——补充样本、清洗标注、或者把过少的类别合并。我个人的原则是:Macro-F1 低的时候,先确认是模型问题还是数据问题,别急着调模型。有次花了半天调参,最后发现是某个类的标注规则前后不一致,清洗完标注 Macro-F1 直接涨了 0.08。

4.3 多分类里 Micro-F1 的干扰

很多人会把 Micro-F1 也拉进来比较,这里必须澄清:多分类场景下 Micro-F1 等于准确率,它把所有类的 TP、FP、FN 汇总后统一计算,完全无视类别差异。所以 Micro-F1 在不平衡数据上没有任何诊断价值——它和准确率一样,会被大类主导。

三者关系可以这样记:Micro-F1 看整体(≈准确率),Weighted-F1 看按样本加权的整体,Macro-F1 看类别公平。做不平衡数据的评估,Micro-F1 基本可以不用看,重点在 Macro 和 Weighted 的对比上。

4.4 问题速查表

现象可能原因排查动作解决方向
Weighted-F1 高但 Macro-F1 低小类性能差,被大类掩盖看各类 recall,找最低的类小类重采样/加权/补数据
Macro-F1 异常低存在噪声类别或极少数类看 classification_report 的 support排除噪声类或合并类别
两个指标都低模型整体能力不足看混淆矩阵整体分布换模型/加特征/调参
交叉验证方差大小类样本过少,划分不稳定看各折 F1 标准差增加数据/调整划分策略
某类 F1 为 0 且有 warning该类完全未被预测检查该类样本量和特征检查标注/特征区分度

这张表是我自己排查时用的,基本覆盖了八成以上的指标异常情况。遇到问题先对号入座,能省不少时间。

4.5 几个容易被忽略的实操细节

第一个细节:评估指标要和阈值选择联动。F1 是在固定阈值(通常 0.5)下算的,但不平衡数据往往需要调阈值。调完阈值后 Macro-F1 和 Weighted-F1 都会变,而且变化方向可能不同。我的做法是先定指标,再在验证集上扫阈值,选让目标指标最优的阈值,最后在测试集上确认。

第二个细节:类别权重和评估指标是两回事。训练时用class_weight='balanced'是为了让模型关注小类,评估时用 Macro-F1 是为了衡量小类表现,两者配合使用,但不能互相替代。有人以为训练加了类别权重就不用看 Macro-F1 了,这是误解——权重影响训练,指标反映结果,缺一不可。

第三个细节:报告里写清楚用的是哪个 F1。我审过不少项目文档,只写"F1 达到 0.9",不说是 Macro 还是 Weighted,这种报告没有参考价值。规范写法是"Macro-F1 0.72 / Weighted-F1 0.91",两个都给,读者自己判断。

5. 我的选择框架:三分钟定下用哪个指标

5.1 决策树式的判断流程

讲了这么多,最后给一个可以直接套用的判断流程。拿到一个不平衡分类任务,按顺序问:

第一步,小类是不是业务重点?是,Macro-F1 为主;否,进第二步。第二步,每个样本的重要性是否相同?是,Weighted-F1 为主;否,按业务代价自定义加权。第三步,拿不准?两个都算,差距大于 0.1 时以 Macro-F1 为准做深入排查。

这个流程的核心逻辑是:指标服务于业务目标,不是反过来。先想清楚业务要什么,再选指标,而不是先算出指标再解释业务。我见过太多团队本末倒置,模型跑完看哪个指标高就报哪个,这种评估毫无意义。

5.2 组合指标的使用建议

实际项目里我很少只用一个指标。我的标准组合是:Macro-F1 作为主指标(把关类别公平性),Weighted-F1 作为辅助(看整体水平),最差类别 F1 作为预警(防止个别类崩溃)。三个数字一起看,既能发现整体问题,又能定位局部短板。

如果非要选一个作为唯一优化目标,不平衡数据我选 Macro-F1。理由很实在:Macro-F1 高的时候,Weighted-F1 通常也不会太差(因为大类一般不会太烂);但 Weighted-F1 高的时候,Macro-F1 可能很惨。选 Macro-F1 作为优化目标,相当于给自己上了个保险,逼着模型不能放弃任何类别。

最后分享一个我在实际项目里养成的习惯:每次模型迭代,把 Macro-F1、Weighted-F1、各类 F1 记到一张表里,按版本累积。跑上几个版本后,你能清楚看到每次改动到底提升了哪部分、牺牲了哪部分。这个记录比任何单次评估都有价值,因为它让你看到指标变化的趋势和原因,而不是盯着一个孤立的数字做判断。指标是工具,理解指标背后的数据分布和业务逻辑,才是评估这件事真正的门槛。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进