一.概述
作者提出的多尺度的object detection算法:FPN(feature pyramid networks)。原来多数的object detection算法都是只采用顶层特征做预测,但我们知道低层的特征语义信息比较少,但是目标位置准确;高层的特征语义信息比较丰富,但是目标位置比较粗略。另外虽然也有些算法采用多尺度特征融合的方式,但是一般是采用融合后的特征做预测,而本文不一样的地方在于预测是在不同特征层独立进行的。
这篇论文的特征金字塔网络(Figure1[d])做法很简单,如下图所示。把低分辨率、高语义信息的高层特征和高分辨率、低语义信息的低层特征进行自上而下的侧边连接,使得所有尺度下的特征都有丰富的语义信息。这种结构是在CNN网络中完成的,和前文提到的基于图片的金字塔结构不同,而且完全可以替代它。
(a).特征图像金字塔:先对原始图像构造图像金字塔,然后在图像金字塔的每一层提出不同的特征,然后进行相应的预测(BB的位置)。这种方法的缺点是计算量大,需要大量的内存;优点是可以获得较好的检测精度。它通常会成为整个算法的性能瓶颈,由于这些原因,当前很少使用这种算法。
(b).利用最后一个卷积层上的feature map来进行预测。这种方法存在于大多数深度网络中,比如VGG、ResNet、Inception,它们都是利用深度网络的最后一层特征来进行分类。这种方法的优点是速度快、需要内存少。它的缺点是我们仅仅关注深层网络中最后一层的特征,却忽略了其它层的特征,但是细节信息可以在一定程度上提升检测的精度。
(c).同时利用低层特征和高层特征,分别在不同的层同时进行预测,这是因为我的一幅图像中可能具有多个不同大小的目标,区分不同的目标可能需要不同的特征,对于简单的目标我们仅仅需要浅层的特征就可以检测到它,对于复杂的目标我们就需要利用复杂的特征来检测它。整个过程就是首先在原始图像上面进行深度卷积,然后分别在不同的特征层上面进行预测。它的优点是在不同的层上面输出对应的目标,不需要经过所有的层才输出对应的目标(即对于有些目标来说,不需要进行多余的前向操作),这样可以在一定程度上对网络进行加速操作,同时可以提高算法的检测性能。它的缺点是获得的特征不鲁棒,都是一些弱特征(因为很多的特征都是从较浅的层获得的)。这种网络如yolo3_v3和SSD。
(d).首先我们在输入的图像上进行深度卷积,然后对Layer2上面的特征进行降维操作(即添加一层1x1的卷积层),对Layer4上面的特征就行上采样操作,使得它们具有相应的尺寸,然后对处理后的Layer2和处理后的Layer4执行加法操作(对应元素相加),将获得的结果输入到Layer5中去。其背后的思路是为了获得一个强语义信息,这样可以提高检测性能。认真的你可能观察到了,这次我们使用了更深的层来构造特征金字塔,这样做是为了使用更加鲁棒的信息;除此之外,我们将处理过的低层特征和处理过的高层特征进行累加,这样做的目的是因为低层特征可以提供更加准确的位置信息,而多次的降采样和上采样操作使得深层网络的定位信息存在误差,因此我们将其结合其起来使用,这样我们就构建了一个更深的特征金字塔,融合了多层特征信息,并在不同的特征进行输出。
二.FPN详解
如下图Fig2。上面一个带有skip connection的网络结构在预测的时候是在finest level(自顶向下的最后一层)进行的,简单讲就是经过多次上采样并融合特征到最后一步,拿最后一步生成的特征做预测。而下面一个网络结构和上面的类似,区别在于预测是在每一层中独立进行的。
1.Bottom-up pathway(自下而上的路径)
CNN的前馈计算就是自下而上的路径,特征图经过卷积核计算,通常是越变越小的,也有一些特征层的输出和原来大小一样,称为“相同网络阶段”(same network stage )。对于本文的特征金字塔,作者为每个阶段定义一个金字塔级别, 然后选择每个阶段的最后一层的输出作为特征图的参考集。 这种选择是很自然的,因为每个阶段的最深层应该具有最强的特征。具体来说,对于ResNets,作者使用了每个阶段的最后一个残差结构的特征激活输出。将这些残差模块输出表示为{C2, C3, C4, C5},对应于conv2,conv3,conv4和conv5的输出,并且注意它们相对于输入图像具有{4, 8, 16, 32}像素的步长。考虑到内存占用,没有将conv1包含在金字塔中。
2.Top-down pathway and lateral connections(自上而下的路径和横向连接)
自上而下的路径(the top-down pathway )是如何去结合低层高分辨率的特征呢?方法就是,把更抽象,语义更强的高层特征图进行上取样,然后把该特征横向连接(lateral connections )至前一层特征,因此高层特征得到加强。值得注意的是,横向连接的两层特征在空间尺寸上要相同。这样做应该主要是为了利用底层的定位细节信息。
Figure 3显示连接细节。把高层特征做2倍上采样(最邻近上采样法),然后将其和对应的前一层特征结合(前一层要经过1 * 1的卷积核才能用,目的是改变channels,应该是要和后一层的channels相同),结合方式就是做像素间的加法。重复迭代该过程,直至生成最精细的特征图。迭代开始阶段,作者在C5层后面加了一个1 * 1的卷积核来产生最粗略的特征图,最后,作者用3 * 3的卷积核去处理已经融合的特征图(为了消除上采样的混叠效应),以生成最后需要的特征图。{C2, C3, C4, C5}层对应的融合特征层为{P2, P3, P4, P5},对应的层空间尺寸是相通的。
金字塔结构中所有层级共享分类层(回归层),就像featurized image pyramid 中所做的那样。作者固定所有特征图中的维度(通道数,表示为d)。作者在本文中设置d = 256,因此所有额外的卷积层(比如P2)具有256通道输出。 这些额外层没有用非线性(博主:不知道具体所指),而非线性会带来一些影响。
3.下面从代码角度进行详细的解析
1.上采样然后相加的程序如下:
P5 = KL.Conv2D(256, (1, 1), name='fpn_c5p5')(C5)
C5是 resnet最顶层的输出,它会先通过一个1*1的卷积层,同时把通道数转为256,得到FPN 的最上面的一层 P5。
2.上采样机和3横向连接如下:
P4 = KL.Add(name="fpn_p4add") ([KL.UpSampling2D(size=(2, 2), name="fpn_p5upsampled")(P5), KL.Conv2D(256,(1, 1), name='fpn_c4p4')(C4)])
这里可以很明显的看到,P4就是上采样之后的 P5加上1*1 卷积之后的 C4,这里的横向连接实际上就是像素加法,先把 P5和C4转换到一样的尺寸,再直接进行相加。
注意这里对从 resnet抽取的特征图做的是 1*1 的卷积:
1x1的卷积我认为有三个作用:
- 使bottom-up对应层降维至256;
- 缓冲作用,防止梯度直接影响bottom-up主干网络,更稳定;
- 组合特征。
3.bottom-up和top-down的过程如下:
# 先从 resnet 抽取四个不同阶段的特征图 C2-C5。 _, C2, C3, C4, C5 = resnet_graph(input_image, config.BACKBONE,stage5=True, train_bn=config.TRAIN_BN) # Top-down Layers 构建自上而下的网络结构 # 从 C5开始处理,先卷积来转换特征图尺寸 P5 = KL.Conv2D(256, (1, 1), name='fpn_c5p5')(C5) # 上采样之后的P5和卷积之后的 C4像素相加得到 P4,后续的过程就类似了 P4 = KL.Add(name="fpn_p4add")([ KL.UpSampling2D(size=(2, 2), name="fpn_p5upsampled")(P5), KL.Conv2D(256, (1, 1),name='fpn_c4p4')(C4)]) P3 = KL.Add(name="fpn_p3add")([ KL.UpSampling2D(size=(2, 2), name="fpn_p4upsampled")(P4), KL.Conv2D(256, (1, 1), name='fpn_c3p3')(C3)]) P2 = KL.Add(name="fpn_p2add")([ KL.UpSampling2D(size=(2, 2),name="fpn_p3upsampled")(P3), KL.Conv2D(256, (1, 1), name='fpn_c2p2')(C2)]) # P2-P5最后又做了一次3*3的卷积,作用是消除上采样带来的混叠效应 # Attach 3x3 conv to all P layers to get the final feature maps. P2 = KL.Conv2D(256, (3, 3), padding="SAME", name="fpn_p2")(P2) P3 = KL.Conv2D(256, (3, 3), padding="SAME",name="fpn_p3")(P3) P4 = KL.Conv2D(256, (3, 3), padding="SAME",name="fpn_p4")(P4) P5 = KL.Conv2D(256, (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) # 注意 P6是用在 RPN 目标区域提取网络里面的,而不是用在 FPN 网络 # Note that P6 is used in RPN, but not in the classifier heads. rpn_feature_maps = [P2, P3, P4, P5, P6] # 最后得到了5个融合了不同层级特征的特征图列表;
注意 P6是用在 RPN 目标区域提取网络里面的,而不是用在 FPN 网络;
另外这里 P2-P5最后又做了一次3*3的卷积,作用是消除上采样带来的混叠效应。
4.上面得到的5个融合了不同层级的特征图怎么使用
可以看到,这里只使用2-5四个特征图:
for i, level in enumerate(range(2, 6)): # 先找出需要在第 level 层计算ROI ix = tf.where(tf.equal(roi_level, level)) level_boxes = tf.gather_nd(boxes, ix) # Box indicies for crop_and_resize. box_indices = tf.cast(ix[:, 0], tf.int32) # Keep track of which box is mapped to which level box_to_level.append(ix) # Stop gradient propogation to ROI proposals level_boxes = tf.stop_gradient(level_boxes) box_indices = tf.stop_gradient(box_indices) # Crop and Resize # From Mask R-CNN paper: "We sample four regular locations, so # that we can evaluate either max or average pooling. In fact, # interpolating only a single value at each bin center (without # pooling) is nearly as effective." # # Here we use the simplified approach of a single value per bin, # which is how it's done in tf.crop_and_resize() # Result: [batch * num_boxes, pool_height, pool_width, channels] # 使用 tf.image.crop_and_resize 进行 ROI pooling pooled.append(tf.image.crop_and_resize( feature_maps[i], level_boxes, box_indices, self.pool_shape, method="bilinear"))
对每个 box,都提取其中每一层特征图上该box对应的特征,然后组成一个大的特征列表pooled。
5.金字塔结构中所有层级共享分类层是怎么回事?
# ROI Pooling # Shape: [batch, num_boxes, pool_height, pool_width, channels] # 得到经过 ROI pooling 之后的特征列表 x = PyramidROIAlign([pool_size, pool_size], name="roi_align_classifier")([rois, image_meta] + feature_maps) # 将上面得到的特征列表送入 2 个1024通道数的卷积层以及 2 个 rulu 激活层 # Two 1024 FC layers (implemented with Conv2D for consistency) x = KL.TimeDistributed(KL.Conv2D(1024, (pool_size, pool_size), padding="valid"), name="mrcnn_class_conv1")(x) x = KL.TimeDistributed(BatchNorm(), name='mrcnn_class_bn1')(x, training=train_bn) x = KL.Activation('relu')(x) x = KL.TimeDistributed(KL.Conv2D(1024, (1, 1)), name="mrcnn_class_conv2")(x) x = KL.TimeDistributed(BatchNorm(), name='mrcnn_class_bn2')(x, training=train_bn) x = KL.Activation('relu')(x) shared = KL.Lambda(lambda x: K.squeeze(K.squeeze(x, 3), 2), name="pool_squeeze")(x) # 分类层 # Classifier head mrcnn_class_logits = KL.TimeDistributed(KL.Dense(num_classes), name='mrcnn_class_logits')(shared) mrcnn_probs = KL.TimeDistributed(KL.Activation("softmax"), name="mrcnn_class")(mrcnn_class_logits) # BBOX 的位置偏移回归层 # BBox head # [batch, boxes, num_classes * (dy, dx, log(dh), log(dw))] x = KL.TimeDistributed(KL.Dense(num_classes * 4, activation='linear'), name='mrcnn_bbox_fc')(shared) # Reshape to [batch, boxes, num_classes, (dy, dx, log(dh), log(dw))] s = K.int_shape(x) mrcnn_bbox = KL.Reshape((s[1], num_classes, 4), name="mrcnn_bbox")(x)
三.利用FPN构建Faster R-CNN
1.FPN for RPN
RPN是Faster R-CNN中用于区域选择的子网络,RPN是在一个13 * 13 * 256的特征图上应用9种不同尺度的anchor,本篇论文另辟蹊径,把特征图弄成多尺度的,然后固定每种特征图对应的anchor尺寸,很有意思。也就是说,作者在每一个金字塔层级应用了单尺度的anchor,{P2, P3, P4, P5, P6}分别对应的anchor尺度为{32^2, 64^2, 128^2, 256^2, 512^2 },当然目标不可能都是正方形,本文仍然使用三种比例{1:2, 1:1, 2:1},所以金字塔结构中共有15种anchors。这里可以看到浅层的featuremap上采用较小尺度的anchor,用于检测小目标,而深层的featuremap上采用较大尺度的anchor,用于检测大目标。
从图上看出各阶层共享后面的分类网络。这也是强调为什么各阶层输出的channel必须一致的原因,这样才能使用相同的参数,达到共享的目的。
正负样本的界定和Faster RCNN差不多:如果某个anchor和一个给定的ground truth有最高的IOU或者和任意一个Ground truth的IOU都大于0.7,则是正样本。如果一个anchor和任意一个ground truth的IOU都小于0.3,则为负样本。
2.FPN for Fast R-CNN
rpn产生的多尺度的proposal对应不同尺度的featuremap,那么当这些proposal输入到fast rcnn做后续的检测的时候,首先要做的就是找到proposal对应的featuremap(多尺度需要将不同大小的roi,对应到特征金字塔的不同的层)。设ROI的尺度为w*h(注意:该值是在输入原图上的值)则其对应的featuremap尺度如下:
其中224是ImageNet预训练模型的输入图片大小。k0是当ROI的大小为224*224时所对应的featuremap。(默认在faster rcnn中k0=4)。然后对于每一个proposal都进行7*7的RoI pooling,然后再连接两个1024*1024的fc层+relu层,然后做最后的分类和坐标回归。
3.FPN for Faster R-CNN
将以上两步综合起来即可,总结如下:
- 首先,选择一张需要处理的图片,然后对该图片进行预处理操作;
- 然后,将处理过的图片送入预训练的特征网络中(如ResNet等),即构建所谓的bottom-up网络;
- 接着,如图5所示,构建对应的top-down网络(即对层4进行上采样操作,先用1x1的卷积对层2进行降维处理,然后将两者相加(对应元素相加),最后进行3x3的卷积操作,最后);
- 接着,在图中的4、5、6层上面分别进行RPN操作,即一个3x3的卷积后面分两路,分别连接一个1x1的卷积用来进行分类和回归操作;
- 接着,将上一步获得的候选ROI分别输入到4、5、6层上面分别进行ROI Pool操作(固定为7x7的特征);
- 最后,在上一步的基础上面连接两个1024层的全连接网络层,然后分两个支路,连接对应的分类层和回归层;
四.效果分析
1.fpn for rpn
以下是引入fpn对于rpn的效果提升:
从(a)(b)(c)的对比可以看出FRN的作用确实很明显。另外(a)和(b)的对比可以看出高层特征并非比低一层的特征有效。
(d)表示只有横向连接,而没有自顶向下的过程,也就是仅仅对自底向上的每一层结果做一个1*1的横向连接和3*3的卷积得到最终的结果,从feature列可以看出预测还是分层独立的,如上面图3的结构。作者推测(d)的结果并不好的原因在于在自底向上的不同层之间的semantic gaps比较大。
(e)表示有自顶向下的过程,但是没有横向连接,即向下过程没有融合原来的特征。这样效果也不好的原因在于目标的location特征在经过多次降采样和上采样过程后变得更加不准确。
(f)采用finest level层做预测,即经过多次特征上采样和融合到最后一步生成的特征用于预测,主要是证明金字塔分层独立预测的表达能力。显然finest level的效果不如FPN好,原因在于PRN网络是一个窗口大小固定的滑动窗口检测器,因此在金字塔的不同层滑动可以增加其对尺度变化的鲁棒性。另外(f)有更多的anchor,说明增加anchor的数量并不能有效提高准确率。
2.fpn for fast rcnn
类似于 RPN 的实验,对比了原有网络,以及不同改变 FPN 结构的 Fast RCNN 实验,实验结果为:
实验发现 FPN 筛选 ROI 区域,同样对于 Fast RCNN 的小物体检测精度有大幅提升。同时,FPN 的每一步都必不可少。
3.fpn for faster rcnn
最后,FPN 对比整个 Faster RCNN 的实验结果如下:
4.对比其他单模型方法
最后,放一个fpn+faster rcnn整体结构的总结图: