qinghuan
← 回到知识探索

AI 直觉 / 动手实验

同一批分类结果,为什么能算出三个总分?

100 张工单里,80 张是咨询。看看“把数量合起来”“每类同样一份”和“按真实数量分份量”,为什么得出不同的 F1。

展开本文目录
  1. 先动手看看
  2. 先看账本:哪类被错分到了哪里
  3. F1 把“找得准”和“找得全”合在一起
  4. 三个总分,三种分配份量的方法
  5. 自己算一遍,再换一种分法
  6. 看总分前,再核对三个条件
  7. 记住这三点
  8. 别把例子用过头
  9. 自己试一小步
  10. 查原始资料
  11. 找一份资料继续练
开始前,会这些就够了
  • 会计算比例和平均数
遇到不熟悉的词,先看这 2 个解释
精确率与召回率
先约定要找的目标类别。精确率看“判为目标的里面,有多少真的符合”;召回率看“实际符合的里面,有多少被找出”。两者分母不同,需要结合误报和漏报代价判断。
F1 的三种平均
micro 先合并各类计数,macro 先算各类 F1 再等份平均,weighted 按各类真实数量加权。
打开完整术语小抄 ↗

客服收到 100 张工单:80 张咨询、15 张退款、5 张故障。一个偷懒的分拣器把所有工单都写成“咨询”,竟然也能分对八成。可退款和故障一张都没找到,只报一个总分就很容易看漏问题。

下面把这批虚构工单当作一本账,每张只有一个真实类别,也只有一个预测类别。我们改变汇总方式,看看它们各自在回答什么;这些数字没有来自真实模型测试。

同一张分类账本,三种总成绩

每张虚构工单只有一个真实类别,也只预测一个类别,始终统计全部三个类别。这里只计算固定数字,没有训练或测试真实模型。

100 张中分对 80 张。micro F1 80.0%,macro F1 29.6%,weighted F1 71.1%。

真实数量是咨询 80、退款 15、故障 5;退款和故障一次都没有被预测。

混淆矩阵:行是真实类别,列是预测类别,数字单位为张
真实 ↓ / 预测 →咨询退款故障真实数量
咨询80 分对0080
退款150 分对015
故障500 分对5

先给每个类别单独算一遍

TP 是这个类别分对的,FP 是其他类别误分进来的,FN 是这个类别被分到别处的。精确率看“分进来的有多准”,召回率看“本来属于它的找回多少”。

  • 咨询:真实 80 张,预测为此类 100 张

    TP = 80,FP = 20,FN = 0

    精确率:TP / (TP + FP)
    80 / 100 = 80.0%
    召回率:TP / (TP + FN)
    80 / 80 = 100.0%
    F1:2TP / (2TP + FP + FN)
    160 / 180 = 88.9%
  • 退款:真实 15 张,预测为此类 0 张

    TP = 0,FP = 0,FN = 15

    精确率:TP / (TP + FP)
    0 / 0 → 按约定记 0(未定义)
    召回率:TP / (TP + FN)
    0 / 15 = 0.0%
    F1:2TP / (2TP + FP + FN)
    0 / 15 = 0.0%

    没有一张被预测为此类,因此精确率没有分母;这不表示它识别得很好。

  • 故障:真实 5 张,预测为此类 0 张

    TP = 0,FP = 0,FN = 5

    精确率:TP / (TP + FP)
    0 / 0 → 按约定记 0(未定义)
    召回率:TP / (TP + FN)
    0 / 5 = 0.0%
    F1:2TP / (2TP + FP + FN)
    0 / 5 = 0.0%

    没有一张被预测为此类,因此精确率没有分母;这不表示它识别得很好。

再决定怎样汇总

micro F1:先合并所有类别的数量
80.0%

总 TP=80,FP=20,FN=20;160 / 200。

macro F1:每个类别同样一份
29.6%

(0.8889 + 0.0000 + 0.0000) / 3。先算每类 F1,再求平均。

weighted F1:按真实数量分配份量
71.1%

(80 × 0.8889 + 15 × 0.0000 + 5 × 0.0000) / 100。权重来自真实数量,不是预测数量。

式子中的小数只为便于阅读而四舍五入,实际计算保留原值。单类分母为零时按 0 计入,仍保留固定三类;全空账本则显示“无数据”。

本例准确率为 80.0%,与 micro F1 相同:每错一张,就在一个类别增加一次误报、另一个类别增加一次漏报。这个等式需要单标签、全部类别以及一致的样本统计范围,不能直接搬到多标签或只选部分类别的报告。

先看账本:哪类被错分到了哪里

把真实类别写在行上,预测类别写在列上,就得到一张“混淆矩阵”。例如退款这一行、咨询这一列写 15,意思是 15 张真实退款被预测成咨询。行列同名的格子才是分对的;一行相加是这一类真实有多少张。

逐类检查时,暂时把某一类当作要找的对象。TP 是找对的,FP 是把别类错算进来的,FN 是本属于这一类却漏到别处的。全判咨询时,咨询的 TP=80、FP=20、FN=0;退款的 TP=0、FP=0、FN=15。

F1 把“找得准”和“找得全”合在一起

精确率问:预测为这一类的工单里,真正属于它的占多少?计算为 TP/(TP+FP)。召回率问:本来属于这一类的工单里,找回了多少?计算为 TP/(TP+FN)。咨询在例子里分别是 80/100 和 80/80。

F1 用 2TP/(2TP+FP+FN) 把两方面合在一个数里,并非把精确率与召回率直接相加除以二。咨询的 F1 是 160/180,约 88.9%;退款和故障虽然没有误报,却全被漏掉,所以各自 F1 都是 0。

三个总分,三种分配份量的方法

micro F1 先把各类的 TP、FP、FN 加起来,再套一次 F1 公式。例子合计 TP=80、FP=20、FN=20,得分 80%。每次错分都会在一个类别留下误报,在另一个类别留下漏报。

macro F1 先算各类 F1,再让每类占同样一份:(88.9%+0+0)/3,约 29.6%。weighted F1 也先算每类 F1,但按真实数量分份量:咨询占 80/100,退款占 15/100,故障占 5/100,结果约 71.1%。权重不是“预测为该类的数量”。

这三个数都按各自规则算对了。micro 更贴近整批有多少判断正确,macro 让小类别也有同等份量,weighted 仍让数量大的类别占更大份量。挑哪个要看你想检查什么;退款错分的实际损失,也不会自动写进这些分数。

自己算一遍,再换一种分法

代码依次计算“全判咨询”“同一批工单部分分回少数类”和“空账本”。第二种分法的真实类别数量仍是 80、15、5,只改预测;因此能直观看见各类成绩和汇总方式的影响。consult、refund、fault 分别表示咨询、退款、故障,n 是真实数量。

本文明确约定:单个类别的某项指标若分母为零,就按 0 计入,同时保留全部三个类别。互动区会标出“未定义”,避免把占位的 0 当成观察结论。若整批没有样本,汇总直接显示“无数据”,不报告零分或满分。

展开收起代码 · 小实验python · 38 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
def report(matrix):
    # Rows are true labels; columns are predictions. Keep all three labels.
    def ratio(a, b):
        return a / b if b else 0.0  # Explicit zero_division=0 convention.

    labels = ["consult", "refund", "fault"]
    rows = []
    for i, label in enumerate(labels):
        tp = matrix[i][i]
        support = sum(matrix[i])
        predicted = sum(row[i] for row in matrix)
        fp, fn = predicted - tp, support - tp
        p, r = ratio(tp, tp + fp), ratio(tp, tp + fn)
        f1 = ratio(2 * tp, 2 * tp + fp + fn)
        rows.append((tp, fp, fn, support, f1))
        print(f"{label}: TP={tp} FP={fp} FN={fn} n={support} "
              f"P={p:.4f} R={r:.4f} F1={f1:.4f}")
    total = sum(row[3] for row in rows)
    if not total:
        print("aggregate: no data")
        return
    tp, fp, fn = (sum(row[i] for row in rows) for i in range(3))
    micro = ratio(2 * tp, 2 * tp + fp + fn)
    macro = sum(row[4] for row in rows) / len(rows)
    weighted = sum(row[3] * row[4] for row in rows) / total
    accuracy = tp / total
    print(f"micro={micro:.4f} macro={macro:.4f} "
          f"weighted={weighted:.4f} accuracy={accuracy:.4f}")


cases = {
    "all-consult": [[80, 0, 0], [15, 0, 0], [5, 0, 0]],
    "mixed": [[72, 6, 2], [4, 9, 2], [1, 1, 3]],
    "empty": [[0, 0, 0], [0, 0, 0], [0, 0, 0]],
}
for name, matrix in cases.items():
    print(name)
    report(matrix)

怎样运行:保存为 averages.py,使用 Python 3.10 或更新版本运行:python averages.py。只用标准库,不联网、不训练模型。

运行后对照结果
all-consult
consult: TP=80 FP=20 FN=0 n=80 P=0.8000 R=1.0000 F1=0.8889
refund: TP=0 FP=0 FN=15 n=15 P=0.0000 R=0.0000 F1=0.0000
fault: TP=0 FP=0 FN=5 n=5 P=0.0000 R=0.0000 F1=0.0000
micro=0.8000 macro=0.2963 weighted=0.7111 accuracy=0.8000
mixed
consult: TP=72 FP=5 FN=8 n=80 P=0.9351 R=0.9000 F1=0.9172
refund: TP=9 FP=7 FN=6 n=15 P=0.5625 R=0.6000 F1=0.5806
fault: TP=3 FP=4 FN=2 n=5 P=0.4286 R=0.6000 F1=0.5000
micro=0.8400 macro=0.6659 weighted=0.8459 accuracy=0.8400
empty
consult: TP=0 FP=0 FN=0 n=0 P=0.0000 R=0.0000 F1=0.0000
refund: TP=0 FP=0 FN=0 n=0 P=0.0000 R=0.0000 F1=0.0000
fault: TP=0 FP=0 FN=0 n=0 P=0.0000 R=0.0000 F1=0.0000
aggregate: no data

看总分前,再核对三个条件

在本文这种单标签多分类、统计全部类别、样本范围一致且不为空的情形下,micro F1 等于准确率,也就是分对张数除以总张数。多标签任务或只统计部分类别时,不要直接使用这个等式。

macro F1 是“各类 F1 的平均”,不是先算平均精确率与平均召回率,再拿这两个平均数算 F1。计算顺序不同,结果一般也不同。比较报告时,还要确认列了哪些类别,以及分母为零的项是按 0 计入还是被排除;类别名单和约定不同,总分也可能变。

报告最好同时保留每类真实数量、精确率、召回率和 F1。一个总分适合快速比较,逐类账本才能告诉你:到底是哪个少数类别没有被照顾到。

记住这三点

  • micro 先合并数量,macro 先算各类 F1 再等份平均,weighted 按各类真实数量加权。
  • 单标签且统计全部类别时,micro F1 等于准确率;少数类别的失败仍可能被大类遮住。
  • 比较总分前,先对齐类别名单和分母为零的规则,并保留逐类成绩。

别把例子用过头

  • macro F1 不是把平均精确率和平均召回率再合成 F1。
  • weighted F1 不自动等于 micro F1,也不是按预测数量加权。
  • 没预测出某个类别,不等于这个类别分得很好;整批无样本也没有模型成绩可报告。

自己试一小步

如果故障工单很少,但漏掉一张的代价很高,你会怎样补充这份成绩报告?

  1. 从第二张矩阵开始,把一张故障从“预测咨询”移到“预测故障”,保持真实数量不变。
  2. 重新计算逐类成绩和三种汇总,观察哪些数字变化明显。
  3. 给故障类单独写一句解释,再说明仅看哪个总分可能遗漏问题。

做到这些,就算完成

  • 是否只改预测位置,保持每一行总数不变?
  • 是否保留三个类别和相同的零分母约定?
  • 是否没有把一次虚构改分宣称为模型能力提升?

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

找一份资料继续练

先选一项,按下面的范围继续学;不必从头读完整门课程。