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
由于使用了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 &#