4、混淆矩阵:从预测结果生成混淆矩阵,计算TP、TN、FP、FN

做分类任务时,我最常被问到的问题是:“你的模型准确率这么高,能不能上线?” 准确率高就万事大吉吗?老实说,我踩过这个坑。有一次在医疗诊断项目里,模型准确率 98%,但实际把好几个阳性样本全漏了——因为正负样本严重失衡。从那以后,我每次都会掏出混淆矩阵,把 TP、TN、FP、FN 挨个算清楚。

说白了,混淆矩阵就是一张 2×2 的表格,把你预测的结果和真实标签一对比,分成四类:真正例、真负例、假正例、假负例。有了它,你才能精确评估模型的“偏科”情况。

4.1 四个基本概念

我们用一个简单的二分类场景来理解。假设任务是判断邮件是否为垃圾邮件:1 代表垃圾,0 代表正常。

  • TP(True Positive):真实是垃圾,模型也判成垃圾。—— 抓对了。
  • TN(True Negative):真实不是垃圾,模型也判成正常。—— 放对了。
  • FP(False Positive):真实不是垃圾,模型却判成垃圾。—— 误报。
  • FN(False Negative):真实是垃圾,模型却判成正常。—— 漏报。

嗯,你想想看,FP 和 FN 往往是业务痛点的根源。垃圾邮件过滤场景下,FP 可能把重要商务邮件扔进垃圾箱,FN 又让骚扰邮件混进收件箱。哪个代价更大?完全取决于业务。

关键公式(记住,这是后面所有指标的基石):

  • TP = 预测为正且实际为正的样本数
  • TN = 预测为负且实际为负的样本数
  • FP = 预测为正但实际为负的样本数
  • FN = 预测为负但实际为正的样本数

4.2 从预测结果生成混淆矩阵

实际工作中,你手上只有两串数字:y_true(真实标签)和 y_pred(模型预测结果)。怎么变成矩阵?最简单的方法是用 Scikit-learn 的 confusion_matrix。但我个人建议至少手动算一次,把逻辑刻进脑子里。

下面是一个完整示例,模拟了 10 个样本的预测结果:

import numpy as np
from sklearn.metrics import confusion_matrix

# 模拟数据:0=正常,1=垃圾
y_true = [0, 1, 0, 1, 0, 0, 1, 0, 1, 1]
y_pred = [0, 1, 0, 0, 0, 1, 1, 0, 1, 1]

# 手动计算 TP, TN, FP, FN
TP = sum((np.array(y_true)==1) & (np.array(y_pred)==1))
TN = sum((np.array(y_true)==0) & (np.array(y_pred)==0))
FP = sum((np.array(y_true)==0) & (np.array(y_pred)==1))
FN = sum((np.array(y_true)==1) & (np.array(y_pred)==0))

print(f"TP = {TP}, TN = {TN}, FP = {FP}, FN = {FN}")

# 用 sklearn 验证
cm = confusion_matrix(y_true, y_pred)
print("Sklearn 混淆矩阵:")
print(cm)
# 输出顺序:[[TN, FP], [FN, TP]]

运行结果:

TP = 3, TN = 3, FP = 1, FN = 2
Sklearn 混淆矩阵:
[[3 1]
 [2 3]]

这里要注意,sklearn 默认的 confusion_matrix 返回的矩阵是 [[TN, FP], [FN, TP]],千万别搞反。我曾经在汇报时把 TP 和 TN 位置写颠倒了,被领导当场质疑……从那以后我每次都会打印出来核对一下。

4.3 手算 vs 接口调用

接口调用非常方便,但在理解阶段,我强烈建议你手写一次循环。下面这个版本更直观:

def compute_confusion_matrix(y_true, y_pred):
    tp = tn = fp = fn = 0
    for t, p in zip(y_true, y_pred):
        if t == 1 and p == 1:
            tp += 1
        elif t == 0 and p == 0:
            tn += 1
        elif t == 0 and p == 1:
            fp += 1
        elif t == 1 and p == 0:
            fn += 1
    return tp, tn, fp, fn

# 测试
tp, tn, fp, fn = compute_confusion_matrix(y_true, y_pred)
print(f"TP={tp}, TN={tn}, FP={fp}, FN={fn}")

结果当然是一样的。当你手动写过一次后,再去查 sklearn 文档,它的参数(labels, normalize)就会顺手很多。

4.4 混淆矩阵的 SVG 可视化

光看数字不够直观,来画一张图。下面这个 SVG 把四个格子的关系解释得清清楚楚:

混淆矩阵结构图 实际正例 实际负例 预测正例 预测负例 TP 实际正,预测正 FN 实际正,预测负 FP 实际负,预测正 TN 实际负,预测负 💡 对角线(TP+TN)代表预测正确的样本 反对角线(FP+FN)代表预测错误的样本

绿色和蓝色格子里的是正确预测,橙色和红色是错误预测。一目了然,对吧?

4.5 你可能遇到的坑

一个小技巧:如果你的类别标签不是 0/1,而是像 “cat/dog” 这样的字符串,记得先转成有序编码。sklearn 的 confusion_matrix 会按 labels 参数排序,你最好显式传参数:

cm = confusion_matrix(y_true, y_pred, labels=['cat','dog'])

这样矩阵的行和列顺序就和 labels 一致,不会乱。

曾经踩过的雷:在多分类任务中,混淆矩阵不再是 2×2,而是 N×N。TP、TN、FP、FN 的定义会变成“一对多”的形式。比如三分类,每个类别都可以计算自己的TP、FP,但 TN 变成了剩下两个类别的总和。一开始觉得绕,后来我每次画矩阵就先标出对角线,然后算每一行的FN(实际为该类但预测成其他类),每一列的FP(预测为该类但实际不是),逐渐就习惯了。

4.6 理解矩阵背后的直觉

有了 TP、TN、FP、FN,接下来的所有评价指标(准确率、精确率、召回率、F1)都能直接从这四个数里算出来。你想想看,准确率 = (TP+TN) / 总数。但如果患病人群只占 1%,一个模型全部预测为健康,准确率也有 99%,但它是废物——因为它 TP=0。混淆矩阵的第一个价值,就是戳穿这种假象。

我个人习惯在每次训练跑完,先打印混淆矩阵,再看指标。尤其是数据不平衡的场景,光看 Acc 等于自杀。嗯,说到这儿,后面几个章节都会基于这四个数值展开。先把它们焊死在大脑里。

符号 全称 含义 业务影响(举例)
TP True Positive 真正例 垃圾邮件被正确拦截,用户满意
TN True Negative 真负例 正常邮件被放行,用户没损失
FP False Positive 假正例(误报) 正常邮件进垃圾箱,用户可能漏看重要信息
FN False Negative 假负例(漏报) 垃圾邮件进收件箱,用户觉得系统没用

最后再提醒一句:代码跑通只是第一步,看懂混淆矩阵、能从数字里读出模型的实际行为,才是工程师的硬功夫。你试试看,下一次汇报模型效果时,直接把混淆矩阵甩出来,并解释一句 “FP 主要集中在哪些样本上”,瞬间比只说准确率专业得多。