【CVPR2020】Revisiting the Sibling Head in Object Detector(TSD)

【CVPR2020】TSD

  • 简要
  • 代码运行
    • 预备(可跳过)
    • 前馈过程
    • target设置
    • 损失函数
  • 实验

TSD : task-aware spatial disentanglement
代码地址 : https://github.com/Sense-X/TSD

简要

该论文提出的方法取得OpenImage Object Detection Challenge 2019 冠军。

  • 数据集
    OpenImages Challenge 2019 目标检测数据集,是 OpenImages V5数据集的一个子集,有174万图片,1460万个bbox和500个类别(5个level,每个level下的类别都有隶属关系)。

  • 出发点
    相比于分类任务,目标检测任务多了一个回归的分支,这两分支几乎共享相同的参数,但其实是有冲突的。IoU-Net里面发现“高的分类分数的location经常预测出不好的bbox”。为了解决这个问题,就再多加一个分支来预测IOU作为定位置信度,但是不对齐的问题仍然存在。(因为提取的特征都是对于一个点,这个点对应到原图不一定就符合“显著的地区用来分类,边缘的地区用来回归”)。Double-Head R-CNN也可以被认为是多加了一个分支,但是因为两个分支的特征是同一个proposal的RoI特征,问题依然存在。所以需要在空间上分解分类和局部化的梯度流(spatially disentangle the gradient flows of classification and localization.)

本文结合代码来阅读(实际上是看了论文我也不太懂)

代码运行

预备(可跳过)

遇到很多问题应该是版本不兼容,按照https://github.com/Sense-X/TSD/blob/master/docs/INSTALL.md最终Python==3.7,PyTorch1.1错误就消失了。

先复习一下RPN的结构。RPN的提出代替了SS,使候选区域提取的时间开销几乎降为0。/mmdet/models/anchor_heads/rpn_head.py

【CVPR2020】Revisiting the Sibling Head in Object Detector(TSD)_第1张图片
由于使用了FPN(c2,c3,c4,c5 — p2,p3,p4,p5,p6,采样率分别是4,8,16,32,64)。这里不使用stage1的输出是因为占内存。ResNet-FPN作为RPN输入的feature map是p2,p3,p4,p5,p6,而作为后续Fast RCNN的输入则是p2,p3,p4,p5,使用p6是因为想获得更大的anchor尺度512×512。接下来的问题就是为生成的proposals(Fast RCNN的输入)选择哪层feature map来得到ROI区域。
在这里插入图片描述
k0是scale=224的ROI所选取的层,RetinaNet论文设为4。也就是224尺度的大小属于p4。

而在实际代码里(mmdet/models/roi_extractors/single_level.py)做了一点变化:

def map_roi_levels(self, rois, num_levels):
        """Map rois to corresponding feature levels by scales.

        - scale < finest_scale * 2: level 0
        - finest_scale * 2 <= scale < finest_scale * 4: level 1
        - finest_scale * 4 <= scale < finest_scale * 8: level 2
        - scale >= finest_scale * 8: level 3

        Args:
            rois (Tensor): Input RoIs of all batch, shape (k, 5). index = 0 which batch_img
            num_levels (int): Total level number.

        Returns:
            Tensor: Level index (0-based) of each RoI, shape (k, )
        """
        scale = torch.sqrt(
            (rois[:, 3] - rois[:, 1] + 1) * (rois[:, 4] - rois[:, 2] + 1))
        target_lvls = torch.floor(torch.log2(scale / self.finest_scale + 1e-6))
        target_lvls = target_lvls.clamp(min=0, max=num_levels - 1).long()
        return target_lvls

self.finest_scale = 56,也就是p2的尺度是56左右,假设生成的ROI尺度分别有32, 64, 128, 256, 512,那么分配的level分别是0,0,1,2,3,也就是p2,p2,p3,p4,p5。

cls分支如果损失函数使用sigmoid的话,通道数要除去背景,通道就是1,也就是binary_cross_entropy,先经过sigmoid函数在进行BCE损失计算。否则就是cross_entroy,此时标签代表属于0或1哪一类,通道就是2。

生成anchor的过程先生成该层特征图对应stride,对应scale(实际上乘以8,这样每个特征图预测的大小scale分别是32, 64, 128, 256, 512)的三个ratio对应的偏差坐标,再加上特征图上个点对应原图的坐标。

# 生成一层的anchor
def grid_anchors(self, featmap_size, stride=16, device='cuda'):
        base_anchors = self.base_anchors.to(device) 
        # 三个ratio下的base anchor(求出该特征图特定stride下, 之后再乘以8)的xmin,ymin,xmax, ymax 
        # 这样每个特征图预测的大小scale分别是32, 64, 128, 256, 512
        feat_h, feat_w = featmap_size
        shift_x = torch.arange(0, feat_w, device=device) * stride # 对应到原图 
        shift_y = torch.arange(0, feat_h, device=device) * stride
        shift_xx, shift_yy = self._meshgrid(shift_x, shift_y) 
        shifts = torch.stack([shift_xx, shift_yy, shift_xx, shift_yy], dim=-1)
        '''
        tensor([[   0,    0,    0,    0],
        [   4,    0,    4,    0],
        [   8,    0,    8,    0],
        ...,
        [ 788, 1212,  788, 1212],
        [ 792, 1212,  792, 1212],
        [ 796, 1212,  796, 1212]], device='cuda:0')

        '''
        shifts = shifts.type_as(base_anchors)
        # first feat_w elements correspond to the first row of shifts
        # add A anchors (1, A, 4) to K shifts (K, 1, 4) to get
        # shifted anchors (K, A, 4), reshape to (K*A, 4)

        all_anchors = base_anchors[None, :, :] + shifts[:, None, :]
        all_anchors = all_anchors.view(-1, 4)
        # first A rows correspond to A anchors of (0, 0) in feature map,
        # then (0, 1), (0, 2), ...
        return all_anchors

生成anchors后先判断是否在图片边界范围内,然后挑选正负样本。RPN生成的proposal作为fast rcnn的输入,同样先挑选正负样本。利用MaxIoUAssigner根据阈值分配正负样本,RandomSampler挑选正负样本,根据阶段的不同参数也不同。

train_cfg = dict(
    rpn=dict(
        assigner=dict(
            type='MaxIoUAssigner',
            pos_iou_thr=0.7,
            neg_iou_thr=0.3,
            min_pos_iou=0.3,
            ignore_iof_thr=-1), # 当gt包含需忽略的bbox时使用,-1表示不忽略
        sampler=dict(
            type='RandomSampler',
            num=256, # 计算rpn的损失
            pos_fraction=0.5,
            neg_pos_ub=-1, # 大于该比例的负样本忽略,-1表示不忽略
            add_gt_as_proposals=False),
        allowed_border=0, 
        pos_weight=-1, #  正样本权重,-1表示不改变原始的权重 原始权重就是1
        debug=False),
    rpn_proposal=dict(
        nms_across_levels=False,
        nms_pre=2000,
        nms_post=2000,
        max_num=2000,
        nms_thr=0.7,
        min_bbox_size=0),
    rcnn=dict(
        assigner=dict(
            type='MaxIoUAssigner',
            pos_iou_thr=0.5,
            neg_iou_thr=0.5,
            min_pos_iou=0.5,
            ignore_iof_thr=-1),
        sampler=dict(
            type='RandomSampler',
            num=512, # 从2000个proposal选512个样本
            pos_fraction=0.25,
            neg_pos_ub=-1,
            add_gt_as_proposals=True),
        pos_weight=-1,
        debug=False))

下面再来看看RoIAlign,经过为一张图的512个proposal分配不同level之后,然后结合这些level的特征图得到ROI的对应的特征(mmdet/models/roi_extractors/single_level.py),再经过RoIAlign操作得到(num_proposal, 256, 7, 7)的特征。

接下来就进入正题,介绍TSD!!

分为三个部分来介绍,前馈过程、target设置、损失函数

前馈过程

【CVPR2020】Revisiting the Sibling Head in Object Detector(TSD)_第2张图片
图中标红的就是前馈过程的输出,保留原来faster rcnn的分支(右),再加了一个TSD分支(左)。其中delta_r感觉像是自适应学习roi的偏移量(只对正的样本进行矫正),之所以是2通道,就是对应了x,y坐标的偏移。

deformable pooling操作如下式所示:
【CVPR2020】Revisiting the Sibling Head in Object Detector(TSD)_第3张图片

target设置

def bbox_target_single_tsd(pos_bboxes, #[num_pos, 4]
                       neg_bboxes, #[num_neg, 4]
                       pos_gt_bboxes, # [num_pos, 4]
                       pos_gt_labels, #[num_neg, 4]
                       rois, # torch.Size([512, 5])
                       delta_c, # torch.Size([512, 98])
                       delta_r, # torch.Size([512, 2])
                       cls_score_, # torch.Size([512, 81])
                       bbox_pred_, # torch.Size([512, 324])
                       TSD_cls_score_,# torch.Size([512, 81])
                       TSD_bbox_pred_,# torch.Size([512, 324])
                       cfg,
                       reg_classes=1,
                       cls_pc_margin=0.2, # 0.3
                       loc_pc_margin=0.2, # 0.3
                       target_means=[.0, .0, .0, .0],
                       target_stds=[1.0, 1.0, 1.0, 1.0]):
    num_pos = pos_bboxes.size(0)
    num_neg = neg_bboxes.size(0)
    num_samples = num_pos + num_neg # 512
    labels = pos_bboxes.new_zeros(num_samples, dtype=torch.long)
    label_weights = pos_bboxes.new_zeros(num_samples)
    bbox_targets = pos_bboxes.new_zeros(num_samples, 4)
    bbox_weights = pos_bboxes.new_zeros(num_samples, 4)

    TSD_labels = pos_bboxes.new_zeros(num_samples, dtype=torch.long)
    TSD_label_weights = pos_bboxes.new_zeros(num_samples)
    TSD_bbox_targets = pos_bboxes.new_zeros(num_samples, 4)
    TSD_bbox_weights = pos_bboxes.new_zeros(num_samples, 4)

    #generte P_r according to delta_r and rois
    w = rois[:,3]-rois[:,1]+1
    h = rois[:,4]-rois[:,2]+1
    scale = 0.1
    rois_r = rois.new_zeros(rois.shape[0],rois.shape[1])
    rois_r[:,0] = rois[:,0]
    rois_r[:,1] = rois[:,1]+delta_r[:,0]*scale*w
    rois_r[:,2] = rois[:,2]+delta_r[:,1]*scale*h
    rois_r[:,3] = rois[:,3]+delta_r[:,0]*scale*w
    rois_r[:,4] = rois[:,4]+delta_r[:,1]*scale*h
    TSD_pos_rois = rois_r[:num_pos]
    pos_rois = rois[:num_pos]

    if num_pos > 0:
        labels[:num_pos] = pos_gt_labels
        TSD_labels[:num_pos] = pos_gt_labels
        pos_weight = 1.0 if cfg.pos_weight <= 0 else cfg.pos_weight # -1
        label_weights[:num_pos] = pos_weight
        TSD_label_weights[:num_pos] = pos_weight
        pos_bbox_targets = bbox2delta(pos_bboxes, pos_gt_bboxes, target_means,
                                      target_stds)
        TSD_pos_bbox_targets = bbox2delta(TSD_pos_rois[:,1:], pos_gt_bboxes, target_means,
                                      target_stds)
        bbox_targets[:num_pos, :] = pos_bbox_targets
        bbox_weights[:num_pos, :] = 1
        TSD_bbox_targets[:num_pos, :] = TSD_pos_bbox_targets
        TSD_bbox_weights[:num_pos, :] = 1

        # compute PC for TSD 
        # 1. compute the PC for classification
        cls_score_soft = F.softmax(cls_score_,dim=1)
        TSD_cls_score_soft = F.softmax(TSD_cls_score_,dim=1)
        cls_pc_margin = torch.tensor(cls_pc_margin).to(labels.device)
        cls_pc_margin = torch.min(1-cls_score_soft[np.arange(len(TSD_labels)),labels],cls_pc_margin).detach() # torch.Size([512])
        pc_cls_loss = F.relu(-(TSD_cls_score_soft[np.arange(len(TSD_labels)),TSD_labels] - cls_score_soft[np.arange(len(TSD_labels)),labels].detach() - cls_pc_margin))

        # 2. compute the PC for localization
        N = bbox_pred_.shape[0]
        bbox_pred_ = bbox_pred_.view(N,-1,4) # torch.Size([512, 81, 4])
        TSD_bbox_pred_ = TSD_bbox_pred_.view(N,-1,4) # torch.Size([512, 81, 4])

        sibling_head_bboxes = delta2bbox(pos_bboxes, bbox_pred_[np.arange(num_pos), labels[:num_pos]], means=target_means, stds=target_stds)
        TSD_head_bboxes = delta2bbox(TSD_pos_rois[:,1:], TSD_bbox_pred_[np.arange(num_pos), TSD_labels[:num_pos]], means=target_means, stds=target_stds)

        ious, gious = iou_overlaps(sibling_head_bboxes, pos_gt_bboxes)
        TSD_ious, TSD_gious = iou_overlaps(TSD_head_bboxes, pos_gt_bboxes)
        loc_pc_margin = torch.tensor(loc_pc_margin).to(ious.device)
        loc_pc_margin = torch.min(1-ious.detach(),loc_pc_margin).detach()
        pc_loc_loss = F.relu(-(TSD_ious - ious.detach() - loc_pc_margin))
        
    if num_neg > 0:
        label_weights[-num_neg:] = 1.
        TSD_label_weights[-num_neg:] = 1.

    return labels, label_weights, bbox_targets, bbox_weights, TSD_labels, TSD_label_weights, TSD_bbox_targets, TSD_bbox_weights, pc_cls_loss, pc_loc_loss

损失函数

如前馈图可见,有6个输出,其中两种分支的cls和bbox损失函数计算方法都一样。就是多了个Progressive constraint( M c l s M_{cls} Mcls M l o c M_{loc} Mloc),在target设置时就已经算了,那么为什么要求这个呢?论文说可以自适应地学习特定于任务的特征表示,从而进行分类和定位。表达式其实可以看作是渐进性约束,即令TSD和传统ROI Pooling主干结果保持一定margin,使得TSD部分的回归分类结果优于sibling head分支的结果。 加粗部分摘抄于https://zhuanlan.zhihu.com/p/126359766
【CVPR2020】Revisiting the Sibling Head in Object Detector(TSD)_第4张图片

实验

【CVPR2020】Revisiting the Sibling Head in Object Detector(TSD)_第5张图片
可见对于不同的backbone涨点明显。

【CVPR2020】Revisiting the Sibling Head in Object Detector(TSD)_第6张图片

你可能感兴趣的:(论文阅读)