在Camvid数据集上复现UNet图像分割,在eval_seg.py中求解混淆矩阵confusion遇到如下代码:
pred_label = pred_label.flatten() # (360, 480) -> (172800, )
gt_label = gt_label.flatten() # (360, 480) -> (172800, )
# Dynamically expand the confusion matrix if necessary.
lb_max = np.max((pred_label, gt_label))
if lb_max >= n_class:
expanded_confusion = np.zeros(
(lb_max + 1, lb_max + 1), dtype=np.int64)
expanded_confusion[0:n_class, 0:n_class] = confusion
n_class = lb_max + 1
confusion = expanded_confusion
# Count statistics from valid pixels.
# 把2d矩阵变为1d进行编号排序,n_cls*gt是确定在哪一行,+pred表示真实标签在这一行的第几列
# 统计完成后再reshape回2d矩阵, 正好使得每个ij能够对应
mask = gt_label >= 0
confusion += np.bincount(
n_class * gt_label[mask].astype(int) + pred_label[mask], # (172800, )
minlength=n_class ** 2
).reshape((n_class, n_class))
个人对于拉平ground_truth和pre_label并使用bincount求解confusion比较迷惑。网上对于这个coding trick的解释大多是从bincount角度正向解释,在此从confusion出发进行理解。
首先,根据混淆矩阵的定义,我们需要绘制一个以gt为row,pre为col的矩阵。这个矩阵里每一个元素的值可以理解为gt的第i类(行)被预测为j类(列)的次数(根据混淆矩阵的定义,对角线就是分类正确的类别)。
对于这个统计问题,最直接的解法就是以gt为依据遍历全部的pre。以图像为例,如果在gt=1的像素处的预测结果为pre=2,混淆矩阵将会在(1,2)处+1,显然这样的方式效率太低。
那么如果可以定义一个由唯一确定的统计量(最好是更低维),我们只需要对gt和pre进行相同的变换就可以通过统计这个量出现的次数来确定整个混淆矩阵的值。
联想到使用元素在矩阵拉平后的位置索引来唯一确定:
考虑到类别(cls)一般是以0作为索引的起点:
这样的好处是,统计索引的个数后可以直接还原为混淆矩阵的大小。以10分类的混淆矩阵元素(2,2)为例:它在矩阵拉平后的位置索引=2*10+2=22。那么只需要统计gt和pre的所有组合变换后值为22的点的个数即可,统计完毕后逆向reshape成混淆矩阵(2,2)位置。推广到一般矩阵形式:
于是有了如下操作:
confusion += np.bincount(
n_class * gt_label[mask].astype(int) + pred_label[mask], # (172800, )
minlength=n_class ** 2
).reshape((n_class, n_class))
其中gt_label和pre_label是拉平转置后的ground truth和分类结果,使用bincount方法实现索引的统计,最后reshape还原为混淆矩阵的大小。
(不足之处,还请见谅~)