R-FCN解读

"R-FCN: Object Detection via egion-based Fully Convolutional Networks"

Posted by Sundrops on November 25, 2017

最近一直做检测,发现检测领域好多好玩的东西啊,R-FCN是msra dai老师和kaiming做的,insight很赞,这次翻出来再学习一下。最近旷视科技又发了light RCNN,检测这领域真是日新月异。

Motivation

虽然fast rcnn共享了每个roi的feature map, faster rcnn利用rpn也使proposal的生成共享了feature map,已经比之前的rcnn减少了大量计算方法,快了很多。但是roi pooling后面的多个fc,每个roi之间是没有共享计算的,而且fc的参数巨多,在之前的faster rcnn源码解析中也都讲过,train时有128个送到后面,test时有300个送到后面,大量的重复计算浪费了很多时间。

观察到ResNet和GoogLeNet,他们只在最后一层f是全连接的,不像vgg有隐层fc6 fc7,所以很自然的方法,我们能不能把roi pooling后面的隐层fc去掉,rpn共享前面所有的卷积,roi pooling后直接跟一个fc,但是效果是不行的,所以kaiming的ResNet附录里A. Object Detection Baselines的做法是共享前91个卷积(相当于vgg的前13个卷积),后面10个卷积依然不共享(相当于vgg的隐层fc)。kaiming为什么这样做呢?因为分类是要求translation invariance,而检测要求translation variance,这就矛盾了,所以需要通过roi pooling这种与位置相关的层打破translation invariance,roi后面的卷积就有了translation-invariance性,但是这样还是降低了速度。(后来的faster rcnn v3时是仅仅把vgg的特征换成resnet的,roi 后面还是fc6 fc7 fc8)

所以这篇论文就想能不能共享所有的计算,并提出了Region-based Fully Convolutional Network (R-FCN)

这里写图片描述 (以RoIpooling为分界线,分为两部分)

# ResNet101 prototxt
layer {
	bottom: "res5c"
	top: "pool5"
	name: "pool5"
	type: "Pooling"
	pooling_param {
		kernel_size: 7
		stride: 1
		pool: AVE
	}
}
layer {
	bottom: "pool5"
	top: "fc1000"
	name: "fc1000"
	type: "InnerProduct"
	inner_product_param {
		num_output: 1000
	}
}
layer {
	bottom: "fc1000"
	top: "prob"
	name: "prob"
	type: "Softmax"
}

Approach

这里写图片描述

在最后一个卷积res5c后,添加一个1*1的卷积生成k^2(C+1)个channel的位置敏感的分类feature和k^2(C+1)个channel的回归feature。以分类为例,这k^2个score map对应k^2个格子的位置。以k=3,左上角为例,黄色是C+1个channel,每个channel对应一个类别在左上角的score map,经过经过这个pool之后得到(C+1)*k*k,然后vote(avg or max pool),得到(C+1)*1*1,直接softmax,没有fc或者卷积等计算,所有roi都贡献了计算。注意还有个

# conv_new_1
# relu1是C1-C5后加了relu后的feature。
# ResNet-101 has 100 convolutional layers followed by global average pooling and a 1000-class fc layer
# R-FCN把global pool和fc都去掉,加了个1*1的卷积降维,然后这101个卷积都贡献参数和计算
conv_new_1 = mx.sym.Convolution(data=relu1, kernel=(1, 1), num_filter=1024, name="conv_new_1", lr_mult=3.0)
relu_new_1 = mx.sym.Activation(data=conv_new_1, act_type='relu', name='relu1')

# rfcn_cls/rfcn_bbox
# rfcn_cls: k*k(C+1)个channel rfcn_bbox: k*k*4(C+1)个channe
rfcn_cls = mx.sym.Convolution(data=relu_new_1, kernel=(1, 1), num_filter=7*7*num_classes, name="rfcn_cls")

rfcn_bbox = mx.sym.Convolution(data=relu_new_1, kernel=(1, 1), num_filter=7*7*4*num_reg_classes, name="rfcn_bbox")

psroipooled_cls_rois = mx.contrib.sym.PSROIPooling(name='psroipooled_cls_rois', data=rfcn_cls, rois=rois, group_size=7, pooled_size=7,output_dim=num_classes, spatial_scale=0.0625)

psroipooled_loc_rois = mx.contrib.sym.PSROIPooling(name='psroipooled_loc_rois', data=rfcn_bbox, rois=rois, group_size=7, pooled_size=7, output_dim=8, spatial_scale=0.0625)
# vote
cls_score = mx.sym.Pooling(name='ave_cls_scors_rois', data=psroipooled_cls_rois, pool_type='avg', global_pool=True, kernel=(7, 7))

bbox_pred = mx.sym.Pooling(name='ave_bbox_pred_rois', data=psroipooled_loc_rois, pool_type='avg', global_pool=True, kernel=(7, 7))

cls_score = mx.sym.Reshape(name='cls_score_reshape', data=cls_score, shape=(-1, num_classes))

bbox_pred = mx.sym.Reshape(name='bbox_pred_reshape', data=bbox_pred, shape=(-1, 4 * num_reg_classes))

# softmax计算loss
cls_prob = mx.sym.SoftmaxOutput(name='cls_prob', data=cls_score, label=label, normalization='valid', grad_scale=1.0)

bbox_loss_ = bbox_weight * mx.sym.smooth_l1(name='bbox_loss_', scalar=1.0, data=(bbox_pred - bbox_target))

Position-sensitive RoI pooling

rpn生成的每个proposal对应到这k^2(C+1)个channel的score map上得到每个roi,然后将每个roi划分为k*k个bin,每个bin是位置相关的,比如左上角的bin,只在黄色层(C+1个channel)对应位置做average或max pooling,注意是对应位置,只在黄色层的左上角做,不是在黄色层整个roi上做。比如下面这个只看person这个channel,第一个图是左上角的bin,只在第一个图roi的左上角红色框做pool,不是在这个图的整个roi绿色框做pool。 这里的k相当于roi pooling的pool height/width 这里写图片描述

//使用CUDA多线程计算  
CUDA_KERNEL_LOOP(index, nthreads) {   //index为最终score map上所有,共有(C+1)*k*k个值  
// The output is in order (n, ctop, ph, pw),类似于图像的BIL逐行扫描  
int pw = index % pooled_width;   //score map上第i=[0,k-1]列  
int ph = (index / pooled_width) % pooled_height;   //score map上第j=[0,k-1]行  
int ctop = (index / pooled_width / pooled_height) % output_dim;   //score map上第ctop个层(class)  
int n = index / pooled_width / pooled_height / output_dim;   //第n个roi  
// ......
int gw = pw;  
int gh = ph;  
//ctop*group_size*group_size+gh*gh*group_size+gw,计算得到的是第ctop类的(ph,pw)位置索引  
//例如,score map上第ctop=1类的第(i,j)=(1,1)位置,c=1*49+1*7+1,对于feature map上第c个颜色层中(实际包含C=21层)的第2(ctop+1)层  
int c = (ctop*group_size + gh)*group_size + gw;    
// 移动到该层做average pool
// 这是cuda代码,每次计算score map上一个值,如左上角

其他细节

  1. ResNet101的res5c是2048channel,为了减少计算量,先用了个1*1的卷积降维到了1024,然后才有position-sensitive的score map
  2. 加了洞算法,调整了步长,使最户的feature map为1/16
  3. 经过position-sensitive pooling后得到了k^2(C+1)的feature(类似于roipooling后的7*7*512的feature),后面两个分支,一个是分支首先average/max pooling到1*1*(C+1),然后softmax得到分类,另一个是坐标回归,生成4k^2-d的向量,然后做average/max pooling到4-d。此处为了简便,做的是类别无关的回归,但是类别相关的回归(生成4k^2C-d的向量)也是适用的

实验

这里写图片描述

  • Naive Faster R-CNN 就是开始说的直接ResNet拿过来,roi pooling后无隐层fc,区别于kaiming的roi pooling后有10个卷积(76.4% mAP)
  • Class-specific RPN,之前的rpn只分为是否是物体,这里直把proposal分为具体哪一个类别
  • R-FCN without position-sensitivity,即k设置为1,类似于分割直接产生C+1个channel,每个channel代表一个类别的score map。这是最直接的做法了

根据表可知R-FCN和标准的faster rcnn(76.4% mAP)差不多了,在roi后没有用任何带参数的层,证明 ps-RoIpooling可以编码到有用的用于定位的空间信息,ps: Class-specific RPN近似于fast rcnn中sliding window的特殊形式

这里写图片描述

这两个表说明,R-FCN节省了很多训练和测试时间,且和标准的faster rcnn相比精度还要高一点点

这里写图片描述

这两个表说明这个策略泛化性很强,无论是不同深度的网络还是不同的proposal的提取方式,该论文的策略都适用。


参考: msracver/Deformable-ConvNets