4. 混淆矩阵:TP、FP、TN、FN 的定义与计算
聊到模型精度评估,绕不开一个东西——混淆矩阵。
说实话,我刚入行那会儿,觉得这玩意儿就是个四宫格表格,没啥技术含量。后来在项目里被坑过几次,才明白这四格子里藏着的门道,比一个 mAP 指标丰富得多。
你想想看,一个模型说它准确率 99%,听起来很牛对吧?但如果你的正样本只占 1%,那模型只要全部判负,准确率也是 99%。这种「虚假繁荣」我见过太多次了。所以,咱们得把混淆矩阵讲透。
4.1 四个基本概念:TP、FP、TN、FN
先明确一个前提:我们讨论的是二分类场景。YOLO 的多类别检测,本质上是对每个类别做二分类判断。
四个术语,我习惯用一个「预测 vs 真实」的对照表来记:
| 术语 | 全称 | 真实标签 | 预测结果 | 通俗理解 |
|---|---|---|---|---|
| TP | True Positive | 正样本 | 预测为正 | 模型说「是」,实际也是「是」 |
| FP | False Positive | 负样本 | 预测为正 | 模型说「是」,实际是「否」——误报 |
| TN | True Negative | 负样本 | 预测为负 | 模型说「否」,实际也是「否」 |
| FN | False Negative | 正样本 | 预测为负 | 模型说「否」,实际是「是」——漏检 |
核心记忆口诀:
第一个字母 T/F 表示预测对/错,第二个字母 P/N 表示预测的结果。
TP = 预测对了,结果是正;FP = 预测错了,结果是正。
在 YOLO 的检测任务里,怎么判断一个预测框是 TP 还是 FP?
关键看 IoU(交并比)。我一般设 IoU ≥ 0.5 算匹配成功。如果预测框和某个真实框的 IoU 超过阈值,且类别一致,就算 TP。如果预测框没匹配到任何真实框,或者类别错了,就算 FP。至于 FN,就是那些真实存在但模型没检测出来的目标。
我的经验:
我曾经在一个行人检测项目里,发现 FP 特别高。排查了半天,原来是模型把路边的垃圾桶当成了人。这就是典型的 FP——模型「过于积极」了。
4.2 从四个值到混淆矩阵
混淆矩阵就是把 TP、FP、TN、FN 放到一个 2×2 的表格里。行是真实标签,列是预测结果。
| 预测为正 (Positive) | 预测为负 (Negative) | |
|---|---|---|
| 真实为正 (Positive) | TP | FN |
| 真实为负 (Negative) | FP | TN |
这个矩阵看起来简单,但它能衍生出几乎所有重要的评估指标。比如:
- 准确率 (Accuracy) = (TP + TN) / (TP + TN + FP + FN)
- 精确率 (Precision) = TP / (TP + FP) —— 模型说「是」的时候,到底有多靠谱
- 召回率 (Recall) = TP / (TP + FN) —— 模型把真正的正样本找出了多少
- 特异度 (Specificity) = TN / (TN + FP) —— 模型对负样本的判断能力
注意:
在类别极度不平衡的数据集里,Accuracy 会骗人。我建议你优先看 Precision 和 Recall 的组合,或者直接看 F1-score。
4.3 用 Python 构建混淆矩阵
理论说完了,咱们动手写代码。OpenCV 本身不提供混淆矩阵的计算函数,但我们可以用 NumPy 或者 scikit-learn 来构建。
方法一:手动计算(适合理解原理)
import numpy as np
# 模拟数据:真实标签和预测标签
y_true = np.array([1, 0, 1, 1, 0, 1, 0, 0, 1, 0])
y_pred = np.array([1, 0, 1, 0, 0, 1, 0, 1, 1, 0])
# 初始化矩阵
tp = np.sum((y_true == 1) & (y_pred == 1))
fp = np.sum((y_true == 0) & (y_pred == 1))
tn = np.sum((y_true == 0) & (y_pred == 0))
fn = np.sum((y_true == 1) & (y_pred == 0))
confusion_matrix = np.array([[tp, fn],
[fp, tn]])
print("混淆矩阵:")
print(confusion_matrix)
print(f"TP={tp}, FP={fp}, TN={tn}, FN={fn}")
方法二:用 scikit-learn(推荐,生产环境用这个)
from sklearn.metrics import confusion_matrix
import seaborn as sns
import matplotlib.pyplot as plt
y_true = [1, 0, 1, 1, 0, 1, 0, 0, 1, 0]
y_pred = [1, 0, 1, 0, 0, 1, 0, 1, 1, 0]
cm = confusion_matrix(y_true, y_pred)
print("混淆矩阵:\n", cm)
# 可视化
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=['Negative', 'Positive'],
yticklabels=['Negative', 'Positive'])
plt.xlabel('Predicted')
plt.ylabel('True')
plt.title('Confusion Matrix')
plt.show()
我的习惯:
在 YOLO 的验证脚本里,我会在每个 epoch 结束后,把验证集的预测结果和真实标注做比对,生成混淆矩阵。这样能直观看到模型在哪些类别上容易混淆。
4.4 混淆矩阵的可视化
光有数字不够,可视化才能一眼看出问题。我一般用 Seaborn 的 heatmap,或者用 Matplotlib 自己画。
下面是一个更完整的可视化代码,适合直接用在 YOLO 的评估脚本里:
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
def plot_confusion_matrix(cm, class_names, title='Confusion Matrix'):
"""
绘制混淆矩阵
cm: 混淆矩阵 (numpy array)
class_names: 类别名称列表
"""
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=class_names,
yticklabels=class_names)
plt.title(title, fontsize=16)
plt.xlabel('Predicted Label', fontsize=12)
plt.ylabel('True Label', fontsize=12)
plt.tight_layout()
plt.show()
# 示例:3 类别的混淆矩阵
cm_multi = np.array([[50, 2, 1],
[3, 45, 4],
[2, 3, 40]])
class_names = ['Person', 'Car', 'Bicycle']
plot_confusion_matrix(cm_multi, class_names)
为什么我强调可视化?
有一次我在做交通场景的检测,模型整体 mAP 有 0.78,看起来还行。但画出混淆矩阵后,我发现「摩托车」和「自行车」这两个类别的混淆特别严重——模型经常把摩托车认成自行车。这就是混淆矩阵的价值:它告诉你问题出在哪里,而不是只告诉你好不好。
4.5 多类别场景下的混淆矩阵
YOLO 检测的是多类别,所以我们的混淆矩阵是 N×N 的,N 是类别数。
对于每个类别,我们都可以单独计算它的 TP、FP、TN、FN。做法是:把当前类别当作正类,其他所有类别当作负类。
| 类别 | TP | FP | FN | Precision | Recall |
|---|---|---|---|---|---|
| Person | 120 | 15 | 8 | 0.889 | 0.938 |
| Car | 200 | 25 | 10 | 0.889 | 0.952 |
| Bicycle | 45 | 20 | 12 | 0.692 | 0.789 |
看到没?Bicycle 的 Precision 和 Recall 明显偏低。这时候我就会去检查:是不是训练数据里自行车样本太少?还是自行车和摩托车的特征太像?
避坑指南:
我曾经在计算多类别混淆矩阵时,忘了把「背景」类别排除掉。结果矩阵里多了一行一列,所有指标都算错了。记住:YOLO 的预测中,背景类(置信度低于阈值)不应该参与混淆矩阵的计算。
4.6 本章知识结构图
下面这张图,把混淆矩阵的核心逻辑串起来了:
这张图把整个逻辑链条理清了:从四个基本值,到混淆矩阵,再到衍生指标,最后落到实际应用。你在做模型评估时,就按这个路径走,不会乱。
4.7 小结
混淆矩阵不是终点,它是起点。通过它,你能看到模型的「偏科」情况——哪个类别检测得好,哪个类别容易误报,哪个类别经常漏检。
我个人习惯,每次训练完模型,第一件事就是看混淆矩阵。比看 mAP 直观多了。mAP 告诉你「考了多少分」,混淆矩阵告诉你「哪道题错了」。后者对改进模型更有帮助。