nanodet阅读:(2)正负样本定义(ATSS)

一、 前言

本篇博客主要是ATSS部分,这部分个人认为是核心之一,毕竟正负样本的选择很重要,ATSS论文证实,anchor-basedanchor-free性能差异的根本原因在于正负样本的定义,好的正负样本定义方法能在很大程度上降低模型对Anchor Num, Anchor Size的依赖。这点在yolo v5上也得到了证实——“正确的正负样本定义方式能引入更多的高质量正样本,加快拟合 并 提高模型性能”。
首先推荐一个写的很好的 ATSS博客,看完它再看代码会清晰许多。以及我写的ATSS部分代码注释。

二、 正文

根据代码总结的ATSS流程如下:

  1. 遍历每个ground truth,遍历每个输出层,找出每层前topk(超参,nanodet中是9)个L2距离(anchorgt box中心点距离)最小的anchornanodet一共3层输出层,则每个gt会匹配到27个候选anchor,输出数组shape=(27, gt_num)。这些anchor里可能会有重复,但是没关系,下面还有筛选措施;
  2. 计算每个gt和与之对应的27anchorIOU值,shape=(27, gt_num)(注意是与anchor左上右下的坐标做iou,不是和bbox,现在是给anchor做正负样本分类,还没到bbox呢);
  3. 按列计算每个gt对应的27IOU值的均值mean_IOU和标准差std_IOU,两者相加得到每个gt的自适应阈值,shape = (gt_num, );
  4. 从每个gt27anchor中筛选出IOU大于对应自适应阈值的anchor
  5. 再计算每个anchor中心到其对应gt四条边界线的距离,取四个距离中的最小值,过滤掉最小值小于0.01anchor,剩下的就是挑选出的正样本;
  6. 到这步时,可能有些anchor同时匹配了多个gt,此时需选出IOU值最大的那个gt作为匹配对象。即一个anchor只能匹配到一个gt,但是一个gt可以同时被多个anchcor匹配。

从上面流程来看,一个anchor要成为一个正样本,需要满足三个条件:
① 其中心要与任何一个gt中心的距离要排在前topk内(升序);
② 其与gtiou值要大于对应的iou阈值;
③ 其中心与gt四条边的距离都要大于0.01
由此引发疑惑并思考之:
1. IOU阈值为什么取均值与标准差之和?
个人理解:均值代表了候选anchorgt整体匹配程度,越大说明整体匹配度越高,候选anchor的质量也就越好。标准差本代表了数据的分散程度,越大说明候选anchor之间与对应gt匹配度相差越大,即候选anchor之间质量相差越大。二者相加,可以筛除很多低质量候选anchor

2. 为什么要以anchor中心与gt四条边的距离来筛选候选anchor
个人理解ATSS论文里只要求anchor中心在gt内部,即中心到gt边界的距离大于0就够了,代码实现时改为大于0.01,个人理解是借鉴了label smooth的思路,毕竟对于softmax 函数来说,0是很难学习到的。
然后疑问是,为什么anchor中心要在gt内部,因为nanodet是学习anchor中心到gt四条边界的距离,学习出来的四个距离都是正数(见下面代码),如果anchor中心在gt外部,那模型是学习不出来一个负数给它拉回去的。其次,anchor中心在gt内部,意味着anchor有更多的特征来学习。

# 下面是 bbox 输出处理成 bbox 坐标的处理函数
class Integral(nn.Module):
    def __init__(self, reg_max=16):
        super(Integral, self).__init__()
        self.reg_max = reg_max  # 7
        self.register_buffer(
            "project", torch.linspace(0, self.reg_max, self.reg_max + 1)  # 返回一维 tensor = [0, 1, 2, 3, ... reg_max]
        )

    def forward(self, x):  # 输入 x 就是原始 bbox 输出
        x = F.softmax(x.reshape(-1, self.reg_max + 1), dim=1)  # softmax 之后,数据就是 (0, 1)之间了
        x = F.linear(x, self.project.type_as(x)).reshape(-1, 4)  # 与 self.project 做矩阵相乘,返回 (0, reg_max) 之间的数
        return x  # x 中每个元素都正数

最后反问一下,提高要求为gt中心在anchor内部行不行?那这样的话,一个gt在每个输出层上只能与一个anchor相对应,减少了高质量正样本,上文说过,更多的高质量正样本,能加快拟合 并 提高模型性能。
3. 为什么要加上 gt_idx * num_bboxes
个人理解:

# num_gt 是一张图片中 gt 数目,num_bboxes 是 anchor 数目
# 下面是疑惑 1
for gt_idx in range(num_gt):  # (topk * 3, num_gt)
            candidate_idxs[:, gt_idx] += gt_idx * num_bboxes 
ep_bboxes_cx = (  # (num_bboxes, ) -> (1, num_bboxes) -> (num_gt, num_bboxes) -> (num_gt * num_bboxes)
            bboxes_cx.view(1, -1).expand(num_gt, num_bboxes).contiguous().view(-1)  # 注意它的长度是 num_gt * num_bboxes
        )
candidate_idxs = candidate_idxs.view(-1)  # (topk * 3 * num_gt)
l_ = ep_bboxes_cx[candidate_idxs].view(-1, num_gt) - gt_bboxes[:, 0]

candidate_idxs = candidate_idxs.view(-1)

一开始不明白为什么要candidate_idxs[:, gt_idx] += gt_idx * num_bboxes 这样,仔细想了下才搞清楚,这是因为起初candidate_idxs里的值,都是相对的,什么意思呢?看下图吧。
nanodet阅读:(2)正负样本定义(ATSS)_第1张图片

candidate_idxs的数据分布类似于上图左边的样式,它的列数就是当前输入图片中ground truth的个数,它每列元素的含义是离每个ground truth 最近的 topkanchor 的索引值(针对一个输出层而言),即行索引, shape = (topk, num_gt),而nanodet有三个输出层,所以最后candidate_idxs.shape = (topk * 3, num_gt)。但是它每列的索引值范围都是[0, num_bboxes),这就是我说的相对值。而ep_bboxes_cx最后是view(-1)成一个一维向量,这样一来相对位置不能用了,要把行数考虑进去,这也是加上gt_idx * num_bboxes的原因。但奇怪的是candidate_idxs的行数是num_gt,怎么代码里面是num_bboxes,这是因为最后处理的数组是ep_bboxes_cx,它的shape = (num_gt * num_bboxes),行数正好是num_bboxes

你可能感兴趣的:(经验记录,目标检测)