MIOU,即平均交并比(Mean Intersection over Union),是评价语义分割模型性能的一个重要指标。它计算的是预测结果中每一类结果的交并比的平均值。假设绿色为真实的标签值,粉红色为预测的结果。
所谓交并比,就是这两种数据的相交元素数量/两种数据的并集的元素数量,如下图所示:
下面我们来一下MIOU的计算方法。
-
IOU的计算
要计算平均交并比,就要首先计算每一类的交并比。交并比(Intersection over Union)是衡量预测值与真实值相似度的一个指标。第i类IOU的计算公式如下:
其中,TP代表真正例,即某类别i的像素正确预测为该类别的数量;
FP代表假正例,即非类别i的像素预测为该类别的数量;
FN代表假负例,即类别i的像素预测为非该类别的数量;
获取到这几个值之后就可以计算出交并比IOU了。 - 混淆矩阵的计算
混淆矩阵是一个n*n的矩阵(n是类别数),用于表示实际类别与预测类别之间的关系。混淆矩阵的对角线元素表示正确预测的数量,而其他元素表示错误预测的数量。根据周志华老师的《机器学习》,混淆矩阵如下:
根据混淆矩阵,可以很方便的计算出TP,FN和FP,进而计算出IOU和MIOU。
混淆矩阵的计算,有好几种算法,我这里选了三种方法介绍一下。
第一种是网上很流行的numpy.bincount方法:
numpy.bincount是一个计算某个数组中,索引出现次数的函数。代码举例会更清楚。
import numpy as np
a = [4,5,4,1,3,5,2]
np.bincount(a)
# 输出:array([0, 1, 1, 1, 2, 2], dtype=int64)
这里,最大值是5,所以类别是0~6之间的6类,返回6个结果,每个位置代表该位置索引出现的次数。这里索引0出现0次,索引1在a中出现1次,索引2在a中出现1次,索引3在a中出现1次,索引4在a中出现2次,索引5在a中出现2次,所以结果是[0,1,1,1,2,2]。
bincount有一个参数minlength,minlength是为了强行设定类别数,比如数组a的最大值只是到5,但是我认为类别数有8类,那么我就可以设定minlength=8,结果当然是后面几类的索引值都是0了。
np.bincount(a, minlength=8)
# 输出:array([0, 1, 1, 1, 2, 2, 0, 0], dtype=int64)
'''
得到n*n的混淆矩阵
a:真实标签label
b:预测结果pred
n:类别数
'''
def confusion_matrix(a, b, n):
#k就是为了防止出错的
k = (a >= 0) & (a < n)
return np.bincount(n * a[k].astype(int) + b[k], minlength=n**2).reshape(n, n)
这个函数中,k只是为了防止运算出错,其实如果a,b是标签值和预测的结果的话,k可以不需要。这里首先把结果扩充到n * n大小,最后再reshape到n * n,就是为了生成一个n * n的矩阵。而其中的值用n乘上a的值,再加上b的值,其实仅仅是为了计算方便而已。假设a是0,b也是0,那么a乘上n后还是0,加上b还是0,所以索引0就会存在,如果a和b不同,则会有不同的值出现,也就是混淆矩阵其他位置上的值。假设a是1,n是3,那么n * a还是3,b如果是0,那就是和就是3,b如果是1,那和就是4。
这么说可能不大好理解,我举个实际例子,假设有个a和b,类别是3类,用0,1,2表示。
a = np.array([0,1,0,2,1,0,2,2,1])
b = np.array([0,2,0,2,1,0,1,2,1])
这里有三类,0,1,2,所以n=3,minlength=9,我们可以手动计算一下这9个值,根据函数中的算法: [3 * 0 + 0, 3 * 1 + 2, 3 * 0 + 0, 3 * 2 + 2, 3 * 1 + 1, 3 * 0 + 0, 3 * 2 + 1, 3 * 2 + 2, 3 * 1 + 1] ,
结果就是 [0, 5, 0, 8, 4, 0, 7, 8, 4] ,
再做一个bincount就是 [3, 0, 0, 0, 2, 1, 0, 1, 2] ,转成3 * 3的矩阵,就是 [[3,0,0] [0,2,1] [0,1,2]] 这个就是混淆矩阵。用程序验证一下。
confusion_matrix(a,b,3)
#输出:
array([[3, 0, 0],
[0, 2, 1],
[0, 1, 2]], dtype=int64)
和我们自己计算的结果一致。
第二种计算混淆矩阵的方法,就比较直观,统计出标签值和预测值的各种结果的总和数,就是混淆矩阵,直接上代码:
import numpy as np
def confusion_matrix_np(y_true, y_pred):
"""
使用NumPy计算混淆矩阵。
参数:
- y_true: 真实标签的数组。
- y_pred: 预测标签的数组。
返回:
- 混淆矩阵的NumPy数组。
"""
# 确保y_true和y_pred是NumPy数组
y_true = np.asarray(y_true)
y_pred = np.asarray(y_pred)
# 检查y_true和y_pred的长度是否相同
if y_true.shape[0] != y_pred.shape[0]:
raise ValueError("y_true和y_pred的长度必须相同")
labels = np.unique(np.concatenate((y_true, y_pred)))
# 初始化混淆矩阵
n_labels = len(labels)
conf_mat = np.zeros((n_labels, n_labels), dtype=np.int64)
# 填充混淆矩阵
for i, l1 in enumerate(labels):
for j, l2 in enumerate(labels):
# 混淆矩阵每个元素的值就是该位置标签值和预测值符合条件的总数
# 比如:conf_mat[0,0]就是真实值和预测值都是0的总数,
# conf_mat[0,1]就是真实标签值是1,但预测值是0的元素总数
conf_mat[i, j] = np.sum((y_true == l1) & (y_pred == l2))
return conf_mat
cm = confusion_matrix_np(a, b)
print(cm)
# 输出
[[3 0 0]
[0 2 1]
[0 1 2]]
这里可以看到,结果和我们之前用bincount计算的结果一致。
第三种方法最简单,直接安装一个sklearn包,用sklearn提供的计算混淆矩阵的函数计算得到。
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(a, b)
print(cm)
# 输出:
[[3 0 0]
[0 2 1]
[0 1 2]]
可以看到,结果也是一样的。
有个混淆矩阵,我们可以计算IOU了。
IOU的计算,在混淆矩阵中,就是把该类所在行的值列的值求和,然后减去对角线上的值,因为对角线上的值在求和的时候会被计算两次,所以要减去一次,然后用对角线的值去除以这个和就是IOU的值。下面是计算过程:
第一类的IOU:3 / ((3+0+0)+(3+0+0)-3) = 1
第二类的IOU:2 / ((0+2+1)+(0+2+1)-2) = 0.5
第三类的IOU:2 / ((0+1+2)+(0+1+2)-2) = 0.5
如果用程序的话,我们可以一次性计算出来,其实就是做了一个批处理,本质上和我们的计算过程是一样的:
def iou(cm):
return np.diag(cm) / (cm.sum(0) + cm.sum(1) - np.diag(cm))
iou(cm)
#输出:array([1. , 0.5, 0.5])
有了各个类别的IOU,就可以很方便的求得MIOU了。
miou = np.mean(iou(cm))
miou
# 输出:0.6666666666666666