faster-rcnn的训练流程及其如何训练自己的数据集(二)

接着上篇继续,上篇写到开始调用Solver的train_model函数了。OK ! ! !

3.train_net()中train_model()的调用

前面已经知道了roidb提供了标注信息,imdb提供了一个数据基类,里面有一些工具接口。那么实际网络跑起来的时候,也需要准备图像数据的输入。因此接下来关注Solver的train_model成员函数中每次图像数据是如何生成的。

data_layer = get_data_layer(self.roidb, self.imdb.num_classes)首先生成一个data_layer。进入到get_data_layer()函数中然后layer = RoIDataLayer(roidb, num_classes),调用了RoIDataLayer()函数(该函数在roi_data_layer/layer.py)。类初始化参数为上面得到的roidb和类别数。在RoIDataLayer()函数中,首先初始化对象,在初始化中self._shuffle_roidb_inds()。首先生成了一个len(roidb)长度的标号的乱序(比如说有5张图片,那么一个标号乱序就可以写为[2,3,1,0,4])self._cur = 0初始化当前游标为0。好了此时data_layer生成。

程序接着往下走,接下来是一系列网络的输出,损失定义。后面再看,我们看到真正训练时的每次循环,一句blobs = data_layer.forward()这句已经准备好了传给feed_dict的所有数据,所以很关键。每次forward都调用了   _get_next_minibatch()函数,这个函数又调用了_get_next_minibatch_inds()函数,这个流程是:

(HAS_RPN =True)如果当前游标已经到达标号乱序的尽头,即每张图像都遍历了一遍,就重新生成一个乱序,重置游标为0,然后截取乱序的[self._cur:self._cur + cfg.TRAIN.IMS_PER_BATCH]部分,并令游标向前推进IMS_PER_BATCH。我的程序要求了faster rcnn每个BATCH只有一张图片,因此IMS_PER_BATCH=1。之后回到get_minibatch(minibatch_db, self._num_classes)函数中,在该函数中,random_scale_inds是为batch中的每个图像随机产生一个从SCALE(参数)中取SCALE的下标。这是一个list和roidb list长度一样,因为SCALE=[600],因此每个图像用的scale都是600。BATCH_SIZE = 128(每个batch的boxes的数量),是ROI的batch.注意区分上面的IMS_PER_BATCH= 1是图像的batch.前者要被后者整除。rois_per_image = 128(每张图片的boxes数量),fg_rois_per_image=128*0.25=32(每张图片中前景的boxes的数量)提醒一下,我用绿色标记的为selective_search方法的时候的参数。而实际用的rpn方法。所以上面参数对后面的没影响,只是提一下。

然后im_blob, im_scales = _get_image_blob(roidb, random_scale_inds)调用了_get_image_blob()函数,输入的参数是roidb和random_scale_inds返回的是im_blob和im_scales。首先根据roidb读入图像,如果roidb的flipped属性是True,则水平翻转。opencv通道顺序是BGR,对应的PIXEL_MEANS也要符合这个顺序。在调用prep_im_for_blob(im, cfg.PIXEL_MEANS, target_size,cfg.TRAIN.MAX_SIZE),其中target_size就是图像对应的scale,此处为600,cfg.TRAIN.MAX_SIZE=1000,这个函数先将图像减去均值,然后再尝试用target_size/短边长度=scale,在用scale*长边。如果大于max_size则scale为maxsize/长边长度。在使用opencv将图像resize。该函数返回的是处理后的图像和该图resize所使用的scale。这样循环调用per_im_for_blob后,得到processed_ims和im_scales的list。然后blob = im_list_to_blob(processed_ims)将processed_ims传递个im_list_to_blob函数。这个函数先找到这批图像的最大短边和最大长边,作为每个blob的高和宽,初始化一个BATCH*H*W*3的blob,依次将每个图像填入,这就是blob,空白位置用0填充,因为这里每个图像batch只有一张图像,因此没有空白位置。

再次回到get_minibatch函数,经过上述步骤,我们已经得到了关于图像batch的im_blob(BATCH*H*W*3)和每个图像resize使用的im_scale(list),注意每个im_blob的长宽可能不一样,是因为根据图像设定的。

返回的blob是一个dict。它包含的字段为:'data':im_blob。这就是上面的im_blob。另外还要提取不是背景的gt框,形成一个gt_boxes矩阵。它是从roidb的gt_classes字段中提取标签非0的框号。然后从boxes字段中提取出这些前景框。最后形成一个N*5的矩阵,前四列是框的坐标,第5列是类别标号。另外一个im_info字段,前两维分别是im_blob的高和宽,第三维是scale.

因此最后形成的blob的格式如下:

blobs ={

'data': (np.array,(1*H*W*3)),

'im_info': (1*3,高和宽,scale),

'gt_boxes': ((N*5),前景框,前四维是坐标,最后一维是类别)

}

小总结:至此,数据完全准备好,每次抽取一张图片提供blobs包含图像数据、高宽、scale,还有gt_bboxes


大心脏 ! ! !

网络模型部分

*anchor_target_layer

进入Faster-RCNN_TF/lib/networks/VGGnet_train.py,我们可以看到喂入数据之后,经过了VGG的前5层网络,然后提取特征。通过卷积层得到了rpn_cls_score。然后进入了anchor_target_layer函数中

输入参数:

rpn_cls_score:1*h*w*18(注意tensorflow和其他不一样,默认时通道维度是最后一维),

gt_boxes : N*5

im_info : 1*3

data: 1*H*W*3

运行

_anchors: 9*4,调用generate_anchors()函数使用3个ratio(0.5,1,2)和3个scale(8,16,32)对基础框([0,0,15,15])做变换,得到9种框。

height、width是缩小16倍后的特征图的高和宽,即h,w

shift_x, shift_y:

shift_x = np.arange(0, width) * _feat_stride

shift_y = np.arange(0, height) * _feat_stride

shift_x, shift_y = np.meshgrid(shift_x, shift_y)

shifts = np.vstack((shift_x.ravel(), shift_y.ravel(), shift_x.ravel(), shift_y.ravel())).transpose()

该段代码首先将特征图上的每个点乘16对应回原图的位置,meshgrid将shift_x当成行向量,在0维度堆叠shift_y的长度那么多次,将shift_y当成列向量,在1维度堆叠shift_y那么多次,ravel()将矩阵按行展开。比如,height=4,width=3,那么生成的shifts如下:

array([[ 0, 0, 0, 0], [16, 0, 16, 0], [32, 0, 32, 0], [ 0, 16, 0, 16], [16, 16, 16, 16], [32, 16, 32, 16], [ 0, 32, 0, 32], [16, 32, 16, 32], [32, 32, 32, 32], [ 0, 48, 0, 48], [16, 48, 16, 48], [32, 48, 32, 48]])

从这个例子可以看到一些规律,0维度大小12,就是特征图面积大小,并且逐行逐行看,他是按行主序的顺序遍历特征图上的每一点的。如果将_anchors的每个框和这个shifts的每一行相加,就是得到了所有anchors在原图的位置。shifts就是anchor的偏移量。比如说,取定原图上的左上角第一个点(对应取shifts的第一行),分别加上_anchors的9行,就得到了这个位置的9个anchors,取定原图第一行第二列的点(对应shifts第二行),分别加上_anchors的9行,就又得到了这个位置的9个anchors。那么具体怎么使用矩阵操作相加呢?我们按照代码中的字母来将形状符号化,_anchors的形状是(9,4)=(A, 4),shifts形状是(hw, 4)=(K, 4),我们利用broadcast,先将shifts reshape成(1, K, 4),并转置成(K, 1, 4),_anchors reshape成(1, A, 4),根据broadcast这两个符合规则,可以相加,相加的结果形状是(K, A, 4),这个也是有顺序的,也就是K个通道分别表示特征图上的K个位置,而每个通道的A 4的矩阵就代表这个位置上的A个anchors。这样,我们最终得到了在原图上的AK个anchors的坐标,记为all_anchors。

all_anchors需要进行摔选,控制一个边界参数,anchors = all_anchors[inds_inside, :],保留仅在图像内部的anchors。经过初步筛选,仍记为all_anchors,此时数目应该小于等于A*K个,现在记为all_anchors:Na*4,labels:(Na),计算all_anchors和gt_boxes的overlaps:

    argmax_overlaps = overlaps.argmax(axis=1)
    max_overlaps = overlaps[np.arange(len(inds_inside)), argmax_overlaps]
    gt_argmax_overlaps = overlaps.argmax(axis=0)
    gt_max_overlaps = overlaps[gt_argmax_overlaps,np.arange(overlaps.shape[1])]
    gt_argmax_overlaps = np.where(overlaps == gt_max_overlaps)[0]

经过上述代码之后。argmax_overlaps计算的是每个anchor覆盖gt最多的那个gt_box的序号。max_overlaps计算的是每个anchor对应的这个最大的overlap值。gt_max_overlaps的值记录每个gt,对应与之重合的最大的overlap值,gt_argmax_overlaps记录每个gt对应与之overlap最大的那个anchor的序号。注意,每个gt_box可能有多个anchors与之对应。

labels[max_overlaps < cfg.TRAIN.RPN_NEGATIVE_OVERLAP] = 0将最大的overlap都小于0.3阈值的anchor的标签置为0,labels[gt_argmax_overlaps] = 1将与gt_box对应的overlap最大的那个anchor的标签置为1。注意这一步可能会将上一步的label纠正过来。labels[max_overlaps >= cfg.TRAIN.RPN_POSITIVE_OVERLAP] = 1,将最大的overlap大于等于0.7阈值的anchor的标签都置为1。

如果我们的正负样本太多,我们需要对其进行采样。采样规则:

首先计算num_fg=RPN_FG_FRACTIONRPN_BATCHSIZE=0.5256=128,fg_inds将labels=1的anchor序号抽出来,如果这个数目多于num_fg,那么随机抽取一些disabled掉,即将labels置为-1。然后,num_bg=RPN_BATCH_SIZE-sum(labels==1),同样也抽取labels=0的,多于num_bg的就disabled掉一些,设为-1。通常前景框数目远少于背景框,二者加起来一共256个,注意现在all_anchors的数目还是没有变,labels的大小也和all_anchors数目一样,只不过其中标1和标0的数目加起来是256,还有其他的没有被考虑的就标为-1。

bbox_targets:对于上述的每个筛选后的anchor计算他和对应的最大IoU的gt_box之间的偏移量,_compute_targets(调用bbox_transform)完成这个任务,返回的结果是Na4,分别是(dx, dy, dw, dh),其中dx和dy是(gt_ctr_x-ex_ctr_x)/ex_widths, 即相对偏差;dw和dh是np.log(gt_widths/ex_widths);注意,Faster RCNN回归的目标不是框的真实位置,而是anchor相对于gt box的偏差值,因此在预测时需要利用预测的偏差值计算出真正的框的位置。

bbox_inside_weights是Na 4,正样例(labels=1)的行全是1;其他都是0。bbox_outside_weights也是Na 4,值分情况,如果参数RPN_POSITIVE_WEIGHT<0,那么所有正负样例的行都是1/(Np+Nn);否则如果在(0,1)间,那么正样例权重RPN_POSITIVE_WEIGHT/(Np),负样例权重(1-RPN_POSITIVE_WEIGHT)/(Nn)
最后一步,因为上面的anchor经过筛选,一些在图像内的标号记为inds_inside,即有Na个,而一开始未经任何筛选时总共有KA个,因此我们还是要求回所有KA个anchors对应上述的所有量。_unmap函数完成这个任务,比如labels,它先生成一个KA大小的一维向量,然后向inds_inside的位置填入labels,其他被抛弃掉的不在图像内的anchors的label填充-1;同理,bbox_targets,bbox_inside_weights,bbox_outside_weights也全部都是这种操作,被抛弃掉的对应行填0;这样,这四个变量的形状分别是(KA, ),(KA, 4),(KA, 4),(KA, 4).

再reshape一下,reshape也有技巧,上面已经说到anchors的排列顺序是有规律的,每个位置的A个anchor的信息先排,接下来是下面一个位置,也就是上面的0维度KA的大小可以看成有K组A,而位置变动是行主序的,因此reshape的时候先reshape成(1, h, w, A)或者(1, h, w, A4),再转置成(1, A, h, w)或者(1, A 4, h, w),labels比较特别,最后reshape成(1,1,Ah, w)。最后返回的四个变量格式如下:

​ rpn_labels, (1,1,Ah, w),表示每个位置的每个anchor是正样本(1)还是负样本(0)还是被忽略的(-1)

​ rpn_bbox_targets, (1, A4, h, w),通道表示每个位置的A个anchor的回归目标,无目标的填充0;

​ rpn_bbox_inside_weights, (1, A 4, h, w),未被忽略的,全1,包括正负样本;被忽略的,填0;

​ rpn_bbox_outside_weights, (1, A 4, h, w),未被忽略的,全都是1/(Np+Nn);被忽略的,填0。

小总结:这一层的功能就是给每个位置的9个anchor生成表示正负样本的label和回归的目标值,以及权重,提供给RPN进行训练

参考资料:

http://www.cnblogs.com/Kenneth-Wong/

上一篇:基于Faster-RCNN-TF的gpu运行总结(自己准备数据集)(训练篇)


下一篇:目标检测之R-CNN/Fast R-CNN/Faster R-CNN