【论文解读-目标检测】DeFCN
参考资料
End-to-End Object Detection with Fully Convolutional Network
简介
什么是one to many?
可以发现,我们常规的两阶段或单阶段检测方法主要分为两个过程,第一阶段,我们先对于物体抛出尽可能多的假设,例如RPN的作用以及FCOS密集预测初始框。第二阶段,我们会拿初始框label assign选出候选框,然后拿候选框以及他所关联的语义信息来修正框的位置,最后得到一系列围绕在GT周围的预测框,最后再通过NMS选出最优的检测框。
在整个过程中,第一阶段就是one to many的过程,第二阶段就是many to one的过程,其中one表示了实际真实框的数量。为什么会需要中间的many阶段,是因为我们离开不了label assign的过程。所以只要存在label assign,我们就会需要一个many to one的过程。(如NMS, RelationNet,如CenterNet的max pooling)
什么是one to one?
字面理解,即是我们对于每个目标就只需要有一个初始框,他最后的结果就直接对应该目标的最终检测框。什么是end to end?
文章将这个标准进一步进行了拉高,NMS也包含进入了人为操作的环节,所以只要存在nms就不算是真正意义上的端到端。
问题挖掘
- one-to-one需要网络输出的feature非常sharp,这对CNN提出了较严苛的要求(这也是Transformer的优势);
- one-to-many带来了更强的监督和更快的收敛速度。
3D Max Filtering提升CNN
3D Max Filtering 基于一个intuition(paper中没有提到):
卷积是线性滤波器,学习max操作是比较困难的。此外,我们在FCOS上做了实验,发现duplicated predictions基本来自于5×5的邻域内,所以最简单的做法就是在网络中嵌入最常见的非线性滤波器max pooling。因为对于同一个实例,重复预测的特征是具有相似性的。
另外,NMS是所有feature map一起做的,但网络在结构上缺少层间的抑制,所以我们希望max pooling是跨层的。
One-to-many auxiliary loss加强监督
针对第二点监督不够强、收敛速度慢,我们依旧采用one-to-many assignment设计了auxiliary loss做监督,该loss只包含分类loss,没有回归loss。assignment本身没什么可说的,appendix的实验也表明多种做法都可以work。这里想提醒大家的是注意看Figure 2的乘法,它是auxiliary loss可以work的关键。在乘法前的一路加上one-to-many auxiliary loss,乘法后是one-to-one的常规loss。由于10=0,11=1,我们只需要大致保证one-to-one assignment的正样本在one-to-many中依然是正样本即可。
实验分析
hand-designed one2one的性能
- anchor/center分别指对于每个gt选取1个初始状态(分别对应IoU选取和center point distance选取),可以发现无论是哪种方式和原本的FCOS相差3个点,但是由于候选框少了很多,去掉NMS也不会掉很多点。同时也会增加recall当没有NMS时。
- 同时这个表格反映了一个问题,就是,当我们采用one to one策略时,的确还是用了NMS精度更高,这说明我们的框学的还是不够好,提取的特征不太好。另一个问题就是由于正样本初始框变少了,监督信息也就会更弱,如何平衡也是个问题,毕竟和baseline还差3个点的gap。
- 手工设计的one to one不好的原因:手工给每个实例选择的正样本初始位置可能是次优的,会加大收敛难度。从表中可以发现POTO不仅提升了性能而且缩小了w/NMS和w/oNMS的差距。
图示各模块的作用
- POTO可以抑制重复性的响应,帮助one to one更好的回归
- 3DMF可以更好的区分不同尺度instance在不同feature上的响应
- Aux loss可以进一步提升性能
一点小插曲
- 虽然方法很好,但其实速度比原始FCOS慢,精度也没FCOS好,本文最大的意义还是在于抛砖引玉,提供了舍弃NMS的一个思路。
代码分析
开源工程基于pytorch架构,然后旷世自研的深度学习框架cvpods。
POTO label assignment实现
# 提取出分类得分,以及decode后box的iou得分 prob = box_cls_per_image[:, targets_per_image.gt_classes].t() boxes = self.shift2box_transform.apply_deltas(box_delta_per_image, shifts_over_all_feature_maps) iou = pairwise_iou(gt_boxes, Boxes(boxes)) # 利用iou和分类得分算出质量矩阵() quality = prob ** (1 - self.poto_alpha) * iou ** self.poto_alpha ... # maximize=True意思是寻找综合最大得分的匹配结果(1行代表一个instance,1列代表候选框的综合得分) # 即shift idxs就是该gt被分配到的最好候选框,one to one # (若是false就是计算分派问题) gt_idxs, shift_idxs = linear_sum_assignment(quality.cpu().numpy(), maximize=True) num_fg += len(shift_idxs) num_gt += len(targets_per_image)
3DMaxFiltering实现
构建生成kernel的tower
self.max3d = MaxFiltering(in_channels,
kernel_size=cfg.MODEL.POTO.FILTER_KERNEL_SIZE,
tau=cfg.MODEL.POTO.FILTER_TAU)卷积box subbranch的feature获得kernel,这里其实很像condinst/solov2给像素上核的思想
filters = [self.filter(x) for x in self.max3d(filter_subnet)]
然后在计算质量矩阵的时候,乘上filter系数
box_filter = torch.cat([permute_to_N_HWA_K(x, 1) for x in box_filter], dim=1) box_cls = box_cls.sigmoid_() * box_filter.sigmoid_()
