1.6.2 Mask R-CNN详解
1.骨干网络(FPN)
我们介绍过CNN的一个重要特征:深层网络容易响应语义特征,浅层网络容易响应图像特征。但是到了目标检测方向,这个特征便成了一个重要的问题,深层网络虽然能响应语义特征,但是由于特征图的尺寸较小,含有的几何信息并不多,不利于目标检测;浅层网络虽然包含比较多的几何信息,但是图像的语义特征并不多,不利于目标的分类,这个问题在小尺寸目标检测上更为显著,这也是目标检测算法对小目标检测效果普遍不好的重要原因之一。很自然地可以想到,使用合并了的深层和浅层特征来同时满足分类和检测的需求。Mask R-CNN的骨干网络使用的是FPN[21]。FPN使用的是特征金字塔的思想,以解决目标检测场景中小尺寸目标检测困难的问题。
[21] 参见Tsung-Yi Lin、Piotr Dollár、Ross Girshick等人的论文“Feature Pyramid Networks for Object Detection”。
2.两步走策略
Mask R-CNN采用了和Faster R-CNN相同的两步走策略,即先使用RPN提取候选区域。不同于Faster R-CNN中使用分类和回归的多任务回归,Mask R-CNN在其基础上并行添加了一个用于实例分割的掩码(mask)损失函数,所以Mask R-CNN的损失函数可以表示为式(1.15):
(1.15)
式(1.15)中,Lcls表示检测框的分类损失值,Lbox表示预测框的回归损失值,Lmask表示掩码部分的损失值,如图1.23所示。在这里Mask R-CNN使用了近似联合训练,所以损失函数也会加上RPN的分类损失和回归损失。Lcls和Lbox的计算方式与Faster R-CNN相同,下面我们重点讨论掩码损失:Lmask。
图1.23 Mask R-CNN的损失函数
Mask R-CNN将目标分类和实例分割任务进行了解耦,即每个类单独预测一个二值掩码,这种解耦提升了实例分割的效果。从表1.2来看,提升效果还是很明显的,其中AP为平均准确率。
表1.2 Mask R-CNN解耦为分割带来的精度提升
所以,Mask R-CNN基于FCN将ROI映射成一个m×m×nb_class的特征层,例如图1.23中的28×28×80。由于每个候选区域的分割是一个二分类任务,因此Lmask使用的是二值交叉熵损失函数。
loss = K.switch(tf.size(y_true) > 0, K.binary_crossentropy(target=y_true, output=y_pred), tf.constant(0.0))
3.ROI对齐
ROI对齐的提出是为了解决Faster R-CNN中ROI池化的区域不匹配的问题,下面我们来举例说明什么是区域不匹配。ROI池化的区域不匹配问题是由ROI池化过程中的取整操作产生的(见图1.24),我们知道ROI池化是Faster R-CNN中必不可少的一步,因为其会产生长度固定的特征向量,有了长度固定的特征向量才能用softmax计算分类损失。
图1.24 ROI池化的区域不匹配问题
如图1.24所示,输入是一幅800×800的图像,经过一个有5次降采样的CNN,得到大小为25×25的特征图。图中的ROI大小是600×500,经过网络之后对应的区域为,由于无法整除,ROI池化采用向下取整的方式,进而得到ROI的特征图的大小为18×15,这就造成了第一次区域不匹配。
ROI池化的下一步是对特征图分桶,假如我们需要一个7×7的桶,每个桶的大小约为,由于不能整除,ROI池化同样采用了向下取整的方式,从而每个桶的大小为2×2,即整个ROI的特征图的尺寸为14×14。第二次区域不匹配问题因此产生。
对比ROI池化之前的特征图,ROI池化在横向和纵向分别产生了4.75(18.75-14)和1.625(15.625-14)的误差,对于目标分类或者目标检测场景,这几像素的位移或许对结果影响不大,但是分割任务通常要精确到每像素,因此ROI池化是不能应用到Mask R-CNN中的。
为了解决这个问题,Mask R-CNN提出了ROI对齐。ROI对齐并没有取整的过程,可以全程使用浮点数操作,具体步骤如下:
(1)计算ROI的边长,边长不取整;
(2)将ROI均匀分成k×k个桶,每个桶的大小不取整;
(3)每个桶的值为其最邻近的特征图的4个值通过双线性插值(附录A)得到;
(4)使用最大池化或者平均池化得到长度固定的特征向量。
ROI对齐可视化如图1.25所示。
图1.25 ROI对齐可视化
ROI对齐操作通过tf.image.crop_and_resize 函数便可以实现。由于Mask R-CNN使用了FPN作为骨干网络,因此将循环保存每次池化之后的特征图。
tf.image.crop_and_resize(feature_maps[i], level_boxes, box_indices, self.
pool_shape, method="bilinear")