原文链接:https://zhuanlan.zhihu.com/p/42745788
个人非常喜欢何凯明的文章,两个原因,1) 简单,2) 好用。对比目前科研届普遍喜欢把问题搞复杂,通过复杂的算法尽量把审稿人搞蒙从而提高论文的接受率的思想,无论是著名的残差网络还是这篇 Mask R-CNN,大神的论文尽量遵循著名的奥卡姆剃刀原理:即在所有能解决问题的算法中,选择最简单的那个。霍金在出版《时间简史》中说 “书里每多一个数学公式,你的书将会少一半读者”。Mask R-CNN 更是过分到一个数学公式都没有,而是通过对问题的透彻的分析,提出针对性非常强的解决方案,下面我们来一睹 Mask R-CNN 的真容。
动机
语义分割和物体检测是计算机视觉领域非常经典的两个重要应用。在语义分割领域,FCN[2]是代表性的算法;在物体检测领域,代表性的算法是Faster R-CNN[3]。很自然的会想到,结合 FCN 和 Faster R-CNN 不仅可以是模型同时具有物体检测和语义分割两个功能,还可以是两个功能互相辅助,共同提高模型精度,这便是 Mask R-CNN 的提出动机。Mask R-CNN 的结构如图 1
图 1:Mask R-CNN 框架图
如图 1 所示,Mask R-CNN 分成两步:
- 使用 RPN 网络产生候选区域;
- 分类,bounding box,掩码预测的多任务损失。
在Fast R-CNN的解析文章中,我们介绍 Fast R-CNN 采用 ROI 池化来处理候选区域尺寸不同的问题。但是对于语义分割任务来说,一个非常重要的要求便是特征层和输入层像素的一对一,ROI 池化显然不满足该要求。为了改进这个问题,作者仿照 STN [4]中提出的双线性插值提出了 ROIAlign,从而使 Faster R-CNN 的特征层也能进行语义分割。
下面我们结合代码详细解析 Mask R-CNN,代码我使用的是基于 TensorFlow 和 Keras 实现的版本:https://github.com/matterport/Mask_RCNN。
Mask R-CNN 详解
1. 骨干架构(FPN)
在第一章中,我们介绍过卷积网络的一个重要特征:深层网络容易响应语义特征,浅层网络容易响应图像特征。但是到了物体检测领域,这个特征便成了一个重要的问题,高层网络虽然能响应语义特征,但是由于 Feature Map 的尺寸较小,含有的几何信息并不多,不利于物体检测;浅层网络虽然包含比较多的几何信息,但是图像的语义特征并不多,不利于图像的分类,这个问题在小尺寸物体检测上更为显著和,这也就是为什么物体检测算法普遍对小物体检测效果不好的最重要原因之一。很自然地可以想到,使用合并了的深层和浅层特征来同时满足分类和检测的需求。
Mask R-CNN 的骨干框架使用的是该团队在 CVPR2017 的另外一篇文章 FPN[5]。FPN 使用的是图像金字塔的思想以解决物体检测场景中小尺寸物体检测困难的问题,传统的图像金字塔方法(图 2.a)采用输入多尺度图像的方式构建多尺度的特征,该方法的最大问题便是识别时间为单幅图的 k 倍,其中 k 是缩放的尺寸个数。Faster R-CNN 等方法为了提升检测速度,使用了单尺度的 Feature Map(图 2.b),但单尺度的特征图限制了模型的检测能力,尤其是训练集中覆盖率极低的样本(例如较大和较小样本)。不同于 Faster R-CNN 只使用最顶层的 Feature Map,SSD[6]利用卷积网络的层次结构,从 VGG 的第 conv4_3 开始,通过网络的不同层得到了多尺度的 Feature Map(图 2.c),该方法虽然能提高精度且基本上没有增加测试时间,但没有使用更加低层的 Feature Map,然而这些低层次的特征对于检测小物体是非常有帮助的。
针对上面这些问题,FPN 采用了 SSD 的金字塔内 Feature Map 的形式。与 SSD 不同的是,FPN 不仅使用了 VGG 中层次深的 Feature Map,并且浅层的 Feature Map 也被应用到 FPN 中。并通过自底向上(bottom-up),自顶向下(top-down)以及横向连接(lateral connection)将这些 Feature Map 高效的整合起来,在提升精度的同时并没有大幅增加检测时间(图 2.d)。
通过将 Faster R-CNN 的 RPN 和 Fast R-CNN 的骨干框架换成 FPN,Faster R-CNN 的平均精度从 51.7% 提升到 56.9%。
图 2:金字塔特征的几种形式。
FPN 的代码出现在./mrcnn/model.py
中,核心代码如下:
# Build the shared convolutional layers.
# Bottom-up Layers
# Returns a list of the last layers of each stage, 5 in total.
# Don't create the thead (stage 5), so we pick the 4th item in the list.
if callable(config.BACKBONE):
_, C2, C3, C4, C5 = config.BACKBONE(input_image, stage5=True, train_bn=config.TRAIN_BN)
else:
_, C2, C3, C4, C5 = resnet_graph(input_image, config.BACKBONE, stage5=True, train_bn=config.TRAIN_BN)
# Top-down Layers
# TODO: add assert to varify feature map sizes match what's in config
P5 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c5p5')(C5)
P4 = KL.Add(name="fpn_p4add")([
KL.UpSampling2D(size=(2, 2), name="fpn_p5upsampled")(P5),
KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c4p4')(C4)])
P3 = KL.Add(name="fpn_p3add")([
KL.UpSampling2D(size=(2, 2), name="fpn_p4upsampled")(P4),
KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c3p3')(C3)])
P2 = KL.Add(name="fpn_p2add")([
KL.UpSampling2D(size=(2, 2), name="fpn_p3upsampled")(P3),
KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c2p2')(C2)])
# Attach 3x3 conv to all P layers to get the final feature maps.
P2 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p2")(P2)
P3 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p3")(P3)
P4 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p4")(P4)
P5 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p5")(P5)
# P6 is used for the 5th anchor scale in RPN. Generated by
# subsampling from P5 with stride of 2.
P6 = KL.MaxPooling2D(pool_size=(1, 1), strides=2, name="fpn_p6")(P5)
# Note that P6 is used in RPN, but not in the classifier heads.
rpn_feature_maps = [P2, P3, P4, P5, P6]
mrcnn_feature_maps = [P2, P3, P4, P5]
1.1 自底向上路径
自底向上方法反映在上面代码的第 6 行或者第 8 行,自底向上即是卷积网络的前向过程,在 Mask R-CNN 中,用户可以根据配置文件选择使用 ResNet-50 或者 ResNet-101。代码中的resnet_graph
就是一个残差块网络,其返回值 C2,C3,C4,C5,是每次池化之后得到的 Feature Map,该函数也实现在./mrcnn/model.py
中(代码片段 2)。需要注意的是在残差网络中,C2,C3,C4,C5 经过的降采样次数分别是 2,3,4,5 即分别对应原图中的步长分别是 4,8,16,32。
def resnet_graph(input_image, architecture, stage5=False, train_bn=True):
"""Build a ResNet graph.
architecture: Can be resnet50 or resnet101
stage5: Boolean. If False, stage5 of the network is not created
train_bn: Boolean. Train or freeze Batch Norm layres
"""
assert architecture in ["resnet50", "resnet101"]
# Stage 1
x = KL.ZeroPadding2D((3, 3))(input_image)
x = KL.Conv2D(64, (7, 7), strides=(2, 2), name='conv1', use_bias=True)(x)
x = BatchNorm(name='bn_conv1')(x, training=train_bn)
x = KL.Activation('relu')(x)
C1 = x = KL.MaxPooling2D((3, 3), strides=(2, 2), padding="same")(x)
# Stage 2
x = conv_block(x, 3, [64, 64, 256], stage=2, block='a', strides=(1, 1), train_bn=train_bn)
x = identity_block(x, 3, [64, 64, 256], stage=2, block='b', train_bn=train_bn)
C2 = x = identity_block(x, 3, [64, 64, 256], stage=2, block='c', train_bn=train_bn)
# Stage 3
x = conv_block(x, 3, [128, 128, 512], stage=3, block='a', train_bn=train_bn)
x = identity_block(x, 3, [128, 128, 512], stage=3, block='b', train_bn=train_bn)
x = identity_block(x, 3, [128, 128, 512], stage=3, block='c', train_bn=train_bn)
C3 = x = identity_block(x, 3, [128, 128, 512], stage=3, block='d', train_bn=train_bn)
# Stage 4
x = conv_block(x, 3, [256, 256, 1024], stage=4, block='a', train_bn=train_bn)
block_count = {"resnet50": 5, "resnet101": 22}[architecture]
for i in range(block_count):
x = identity_block(x, 3, [256, 256, 1024], stage=4, block=chr(98 + i), train_bn=train_bn)
C4 = x
# Stage 5
if stage5:
x = conv_block(x, 3, [512, 512, 2048], stage=5, block='a', train_bn=train_bn)
x = identity_block(x, 3, [512, 512, 2048], stage=5, block='b', train_bn=train_bn)
C5 = x = identity_block(x, 3, [512, 512, 2048], stage=5, block='c', train_bn=train_bn)
else:
C5 = None
return [C1, C2, C3, C4, C5]
这里之所以没有使用 C1,是考虑到由于 C1 的尺寸过大,训练过程中会消耗很多的显存。
1.2 自顶向下路径和横向连接
通过自底向上路径,FPN 得到了四组 Feature Map。浅层的 Feature Map 如 C2 含有更多的纹理信息,而深层的 Feature Map 如 C5 含有更多的语义信息。为了将这四组倾向不同特征的 Feature Map 组合起来,FPN 使用了自顶向下及横向连接的策略,图 3。
图 3:FPN 的自顶向上路径和横向连接
残差网络得到的 C1-C5 由于经历了不同的降采样次数,所以得到的 Feature Map 的尺寸也不同。为了提升计算效率,首先 FPN 使用
进行了降维,得到 P5,然后使用双线性插值进行上采样,将 P5 上采样到和 C4 相同的尺寸。
之后,FPN 也使用
卷积对 P4 进行了降维,由于降维并不改变尺寸大小,所以 P5 和 P4 具有相同的尺寸,FPN 直接把 P5 单位加到 P4 得到了更新后的 P4。基于同样的策略,我们使用 P4 更新 P3,P3 更新 P2。这整个过程是从网络的顶层向下层开始更新的,所以叫做自顶向下路径。
FPN 使用单位加的操作来更新特征,这种单位加操作叫做横向连接。由于使用了单位加,所以 P2,P3,P4,P5 应该具有相同数量的 Feature Map(源码中该值为 256),所以 FPN 使用了
卷积进行降维。
在更新完 Feature Map 之后,FPN 在 P2,P3,P4,P5 之后均接了一个
卷积操作(代码片段 1 第 22-25 行),该卷积操作是为了减轻上采样的混叠效应(aliasing effect)。
2. 两步走策略
Mask R-CNN 采用了和 Faster R-CNN 相同的两步走策略,即先使用 RPN 提取候选区域,关于 RPN 的详细介绍,可以参考 Faster R-CNN 一文。不同于 Faster R-CNN 中使用分类和回归的多任务回归,Mask R-CNN 在其基础上并行添加了一个用于语义分割的 Mask 损失函数,所以 Mask R-CNN 的损失函数可以表示为下式。
上式中,
表示 bounding box 的分类损失值,
表示 bounding box 的回归损失值,
表示 mask 部分的损失值,图 4。在这份源码中,作者使用了近似联合训练(Approximate Joint Training),所以损失函数会由也会加上 RPN 的分类和回归 loss。这一部分代码在./mrcnn/model.py
的 2004-2025 行。
和
的计算方式与 Faster R-CNN 相同,下面我们重点讨论
。
图 4:Mask R-CNN 的损失函数
在进行掩码预测时,FCN 的分割和预测是同时进行的,即要预测每个像素属于哪一类。而 Mask R-CNN 将分类和语义分割任务进行了解耦,即每个类单独的预测一个位置掩码,这种解耦提升了语义分割的效果,从图 5 上来看,提升效果还是很明显的。
图 5:Mask R-CNN 解耦分类和分割的精度提升
所以 Mask R-CNN 基于 FCN 将 ROI 区域映射成为一个
(FCN 是
)的特征层,例如他图 4 中的
。由于每个候选区域的分割是一个二分类任务,所以
使用的是二值交叉熵(binary_crossentropy
)损失函数,对应的代码为(1182-1184 行)
loss = K.switch(tf.size(y_true) > 0,
K.binary_crossentropy(target=y_true, output=y_pred),
tf.constant(0.0))
顾名思义,二值交叉熵即用于二分类的交叉熵损失函数,该损失一般配合
激活函数使用(第 1006 行)。
3. RoIAlign
ROIAlign 的提出是为了解决 Faster R-CNN 中 RoI Pooling 的区域不匹配的问题,下面我们来举例说明什么是区域不匹配。ROI Pooling 的区域不匹配问题是由于 ROI Pooling 过程中的取整操作产生的(图 6),我们知道 ROI Pooling 是 Faster R-CNN 中必不可少的一步,因为其会产生长度固定的特征向量,有了长度固定的特征向量才能进行 softmax 计算分类损失。
如下图,输入是一张
的图片,经过一个有 5 次降采样的卷机网络,得到大小为
的 Feature Map。图中的 ROI 区域大小是
,经过网络之后对应的区域为
,由于无法整除,ROI Pooling 采用向下取整的方式,进而得到 ROI 区域的 Feature Map 的大小为
,这就造成了第一次区域不匹配。
RoI Pooling 的下一步是对 Feature Map 分 bin,加入我们需要一个
的 bin,每个 bin 的大小为
,由于不能整除,ROI 同样采用了向下取整的方式,从而每个 bin 的大小为
,即整个 RoI 区域的 Feature Map 的尺寸为
。第二次区域不匹配问题因此产生。
对比 ROI Pooling 之前的 Feature Map,ROI Pooling 分别在横向和纵向产生了 4.75 和 1.625 的误差,对于物体分类或者物体检测场景来说,这几个像素的位移或许对结果影响不大,但是语义分割任务通常要精确到每个像素点,因此 ROI Pooling 是不能应用到 Mask R-CNN 中的。
图 6:ROI Pooling 的区域不匹配问题
为了解决这个问题,作者提出了 RoIAlign。RoIAlign 并没有取整的过程,可以全程使用浮点数操作,步骤如下:
- 计算 RoI 区域的边长,边长不取整;
- 将 ROI 区域均匀分成
个 bin,每个 bin 的大小不取整; - 每个 bin 的值为其最邻近的 Feature Map 的四个值通过双线性插值得到;
- 使用 Max Pooling 或者 Average Pooling 得到长度固定的特征向量。
上面步骤如图 7 所示。
图 7:RoIAlign 可视化
RoIAlign 操作通过tf.image.crop_and_resize
一个函数便可以实现,在./mrcnn/model.py 的第 421-423 行。由于 Mask R-CNN 使用了 FPN 作为骨干架构,所以使用了循环保存每次 Pooling 之后的 Feature Map。
tf.image.crop_and_resize(feature_maps[i], level_boxes, box_indices, self.pool_shape, method="bilinear")
总结
Mask R-CNN 是一个很多 state-of-the-art 算法的合成体,并非常巧妙的设计了这些模块的合成接口:
- 使用残差网络作为卷积结构;
- 使用 FPN 作为骨干架构;
- 使用 Faster R-CNN 的物体检测流程:RPN+Fast R-CNN;
- 增加 FCN 用于语义分割。
Mask R-CNN 设计的主要接口有:
- 将 FCN 和 Faster R-CNN 合并,通过构建一个三任务的损失函数来优化模型;
- 使用 RoIAlign 优化了 RoI Pooling,解决了 Faster R-CNN 在语义分割中的区域不匹配问题。
Reference
[1] He K, Gkioxari G, Dollár P, et al. Mask r-cnn[C]//Computer Vision (ICCV), 2017 IEEE International Conference on. IEEE, 2017: 2980-2988.
[2] J. Long, E. Shelhamer, and T. Darrell. Fully convolutional networks for semantic segmentation. In CVPR, 2015. 1, 3, 6
[3] S. Ren, K. He, R. Girshick, and J. Sun. Faster R-CNN: Towards real-time object detection with region proposal networks. In NIPS, 2015. 1, 2, 3, 4, 7
[4] M. Jaderberg, K. Simonyan, A. Zisserman, and K. Kavukcuoglu. Spatial transformer networks. In NIPS, 2015. 4
[5] T.-Y. Lin, P. Dollar, R. Girshick, K. He, B. Hariharan, and ´ S. Belongie. Feature pyramid networks for object detection. In CVPR, 2017. 2, 4, 5, 7
[6] Liu W, Anguelov D, Erhan D, et al. Ssd: Single shot multibox detector[C]//European conference on computer vision. Springer, Cham, 2016: 21-37.
附录 A: 双线性插值
双线性插值即在二维空间上按维度分别进行线性插值。
线性插值:已知在直线上两点
,
,则在
区间内任意一点
满足等式
即已知
的情况下,
的计算方式为:
双线性插值:双线性插值即在二维空间的每个维度分别进行线性插值,如图 8
图 8:双线性插值
已知二维空间中 4 点
,
,
,
,我们要求的是空间中一点中
的值
。
首先在
轴上进行线性插值据得到
和
:
在根据
和
在
轴上进行线性插值