给工单分类助手做评测时,只报“八成正确”很容易掩盖一个问题:样本最多的普通咨询表现很好,数量较少的退款和维修请求却一直被分错。宏平均、微平均和加权 F1 都在汇总分类结果,但它们回答的问题不同。先看清每类的真阳性、误报和漏报,再选平均方式,比记住一个漂亮分数更有用。
本文用十条人工构造的工单标签,算清三个类别的 Precision、Recall、F1 及其汇总。练习只需要 Python 标准库,不调用模型、不安装机器学习框架,也不代表任何线上助手的真实效果。最后得到的是一份可复查的指标计算,而不是模型质量证明。
一、先定义评测任务,别急着选指标
本例是单标签三分类:每条工单恰好有一个人工参考标签,模型也恰好输出一个标签。类别为“咨询”“退款”“维修”。它不包含一条工单同时属于多个类别的多标签情形,也没有“待人工判断”这个第四类。后面关于微平均等于准确率的结论,都有这个前提,不能自动套到拒答、多标签或只统计部分类别的评测中。
准备两列数据:人工核验的参考标签和系统预测标签。参考标签需要事先明确规则,例如退款指明确请求退回款项,维修指要求修复已出现的故障。若人工标注本身存在争议,先解决标签边界,再讨论分数;否则模型可能只是与一位标注者的临时偏好不一致。
十条练习数据中,咨询六条、退款两条、维修两条。系统对六条咨询全部预测为咨询;两条退款里一条预测为咨询、一条正确;两条维修里一条预测为咨询、一条正确。因此正确八条,准确率为 8/10。接下来检查:这个八成究竟掩盖了什么。
二、把每个类别临时看作正类
计算“退款”指标时,退款是正类,咨询和维修都视为其他类。真阳性 TP 是正确识别的退款数;误报 FP 是其他类被错判为退款的数量;漏报 FN 是真正退款却被判到其他类的数量。
本例退款有 TP=1、FP=0、FN=1,所以 Precision=1,Recall=1/2。含义是:系统一旦预测退款,这个小样本中都对了;但实际退款只找回了一半。不要把 Precision=1 解释为“所有退款都识别了”,那是召回率的问题。
维修也是 TP=1、FP=0、FN=1。咨询则是 TP=6、FP=2、FN=0,因为被错归为咨询的退款和维修贡献了两个误报。咨询的 Precision=6/8,Recall=1。一个类别没有漏报,并不意味着它没有把其他类别吸进来。
这三组数应与混淆记录对应。每次发现一个错误,就能指出它对哪个类增加了 FP,又对哪个类增加了 FN。若计算器给出分数却找不到对应错误行,先检查标签顺序、样本对齐和类别集合,不要把指标函数当作不可解释的黑盒。
三、F1 为什么不是两个比例的普通平均
F1 是 Precision 与 Recall 的调和平均,也可直接由计数计算:F1 = 2×TP / (2×TP+FP+FN)。公式里的每一项都来自刚才的错误统计,不需要模型输出置信概率。
咨询的 F1 为 12/14,也就是 6/7,约为 0.8571。退款和维修都为 2/3,约为 0.6667。若把退款的 Precision=1 与 Recall=0.5 做算术平均,会得到 0.75;但 F1 为约 0.6667。它会对两者的不平衡给出不同于算术平均的结果。
F1 不是所有业务的统一目标。 对维修漏分造成的损失,可能高于多转一次人工;退款错误也可能涉及不同处理优先级。业务可以同时报告每类召回率、人工复核工作量和错误后果,而不是为了提高一个 F1 就忽略关键错误。
四、三种平均方式分别看什么
宏平均 macro:先算每个类别的 F1,再做不加权平均。本例是 (6/7+2/3+2/3)/3=46/63,约 0.7302。它给予三个类别相同权重,便于观察小类表现;但样本极少的类别也可能使结果波动很大,因此要同时报告每类样本量。
加权平均 weighted:按各类别参考样本数加权。本例是 (6×6/7+2×2/3+2×2/3)/10=82/105,约 0.7810。这里的权重来自参考标签的支持数,不是预测数量,也不是随意设置的业务重要性。某类更重要,应另外说明业务权重或验收要求,不能把官方 weighted 的含义换掉。
微平均 micro:先汇总各类 TP、FP、FN,再计算一次 F1。本例总 TP=8、FP=2、FN=2,所以为 16/20=0.8。不要先把各类 Precision、Recall 平均,再代入 F1 公式,冒充微平均。 汇总顺序是指标含义的一部分。
在本例这种每条只有一个真实类和一个预测类、统计全部类别的任务中,每次误分类都会贡献一次 FP 和一次 FN,因此微平均 F1 与准确率相同。这个关系有前提:如果你只挑退款这个类统计,或者允许一条有多个标签,就不能继续用同样的理由解释结果。
五、运行一个不依赖外部库的小练习
安装 Python 3 后,新建普通 UTF-8 文本文件 metrics_demo.py,保存下面的自编代码。它用 Fraction 保留精确分数,便于检查小样本算术;分母为零时按本练习约定返回零,并在后文说明这个约定的限制。
from fractions import Fraction
labels = ("咨询", "退款", "维修")
truth = ["咨询"] * 6 + ["退款"] * 2 + ["维修"] * 2
pred = ["咨询"] * 6 + ["咨询", "退款", "咨询", "维修"]
def ratio(n, d):
return Fraction(n, d) if d else Fraction(0)
def report(truth, pred, labels):
if not truth or len(truth) != len(pred):
raise ValueError("输入必须非空且逐行对齐")
if len(set(labels)) != len(labels):
raise ValueError("类别不能重复")
if (set(truth) | set(pred)) - set(labels):
raise ValueError("存在未声明的类别")
rows = []
for label in labels:
tp = sum(t == label and p == label for t, p in zip(truth, pred))
fp = sum(t != label and p == label for t, p in zip(truth, pred))
fn = sum(t == label and p != label for t, p in zip(truth, pred))
support = truth.count(label)
rows.append((label, tp, fp, fn, support,
ratio(tp, tp + fp), ratio(tp, tp + fn),
ratio(2 * tp, 2 * tp + fp + fn)))
macro = sum(r[7] for r in rows) / len(labels)
weighted = sum(r[4] * r[7] for r in rows) / len(truth)
tp, fp, fn = (sum(r[i] for r in rows) for i in (1, 2, 3))
micro = ratio(2 * tp, 2 * tp + fp + fn)
accuracy = ratio(sum(t == p for t, p in zip(truth, pred)), len(truth))
return rows, macro, weighted, micro, accuracy
if __name__ == "__main__":
rows, macro, weighted, micro, accuracy = report(truth, pred, labels)
for label, tp, fp, fn, n, precision, recall, f1 in rows:
print(label, "TP/FP/FN", tp, fp, fn, "样本数", n,
"Precision", precision, "Recall", recall, "F1", f1)
print("macro", macro, "weighted", weighted,
"micro", micro, "accuracy", accuracy)在文件所在目录执行 python metrics_demo.py;Windows 上也可以根据安装方式使用 py metrics_demo.py。本例的代码已做本地算术检查,得到宏平均 46/63、加权平均 82/105、微平均和准确率均为 4/5。它没有调用分类模型,因此结果仅说明这组构造标签对应的计算关系。
六、缺失类别与零分母怎样处理
假如一次测试没有维修样本,也没有预测为维修,维修的 F1 分母就会是零。把这个类别纳入宏平均并记零,与完全不纳入平均,会得到不同结果。两者必须明确约定,不能今天自动忽略、明天记零,再把两轮分数直接比较。
官方 scikit-learn 提供 labels、average 和 zero_division 等参数来控制这些行为。本练习固定类别清单、零分母记零,没有复刻库的所有选项。如果改成调用库函数,应明确传入对应设置,尤其不要把默认 binary 平均方式直接用于三分类。
还要区分“没有样本所以无法评估”和“有样本但全部识别失败”。前者说明测试覆盖不足,后者说明在这批样本上确实失败。一个最终显示为零的值可能有不同原因,因此交付报告需要支持数和错误计数,而不只是三位小数。
七、做两个对照,检查自己有没有理解
第一个对照是让系统把十条全部判为咨询。这样仍有六条正确,准确率和微平均都是 0.6;咨询的 F1 是 12/16=0.75,退款和维修都是零,所以宏平均仅为 0.25,加权平均为 0.45。这组结果说明,跟随多数类就能得到看起来不太低的正确率,却完全没有识别另外两类的能力。
第二个对照是把所有预测改成参考标签。每一类都没有误报和漏报,三种 F1 都应为一。再把参考和预测按同样的顺序一起倒置,结果应保持不变,因为分类计数与行的排列无关。但只倒置预测列、参考列保持原样,会破坏逐条对应关系,不能把得到的新分数当成同一实验。
这两个对照也适合排查数据导出错误。实际评测通常有样本编号,合并文件时要按编号对齐,不能因为两个文件行数相同就直接逐行拼接。检查重复编号、丢失记录和未知标签之后再计算指标。代码只能检查长度与标签范围,无法知道第七行究竟是不是同一个工单,数据对应关系仍需在上游保证。
最后把一个类别改名时,要同时更新参考标签、预测标签和类别清单。拼写不同的两个词会被当作不同标签,不能在看到分数下降后私自把它们合并;先确认它们究竟是同义名称还是业务上不同的类别,再记录映射规则并重算全部结果。
八、怎样用这些结果改进助手
先保留原始样本编号、人工标签、预测标签和错误原因。若退款漏分多,检查是标签定义含混、输入缺少上下文,还是系统倾向于把不确定内容归到咨询。不要只把提示词写成“请更准确”,而应针对实际误分案例澄清规则。
修改规则后,用未参与调整的样本复核,并保持类别集合、分母和统计口径一致。练习中的十条数据太少,不能据此宣称系统稳定达到某个水平。实际评测还应覆盖不同表达、少见类别和边界案例,并记录参考标签由谁确认。
一份清楚的报告可以这样写:总共十条,咨询六条、退款和维修各两条;总体八条正确;退款和维修各漏掉一条;宏平均约 0.7302,微平均 0.8,加权平均约 0.7810;尚无真实线上表现结论。这样的描述能让读者知道问题在哪里,也不会让一个平均数代替业务判断。
官方资料
- scikit-learn 分类评测说明:分类计数、平均方式及适用任务。
- f1_score 参数与定义:F1 公式、micro、macro、weighted、labels 与零分母处理。

还没有公开评论,欢迎分享你的想法。