Onestage的Anchor生成原理-以Retinanet为例

onestage的anchor生成原理-以Retinanet为例

目前基于onestage的算法一再刷新COCO数据,从19年的Cornernet到Centernet,从今年的ATSS到PAA,再到GFL,VFL,这些算法基本上都是源自基础的onestage之上改进而来,弄清楚整个onestage流程,对于这些算法的理解具有极大的好处。本文从基于onestage的代表RetinaNet出发,基于mmdetection的代码,详细的将前向推理以及训练阶段整个流程为各位较为完整的梳理一遍。

Onstage(例如retinanet)几句话就能说完:
首先就是前向阶段:
Backbone提取特征。送进head做框的分类/回归,再进行一些必要的后处理之后(NMS和阈值过滤)输出结果。

x = self.extract_feat(img)

backbone提取特征

outs = self.bbox_head(x)

送进retinanet头得到结果

Outs来源是所在head的输出:主要有两部分,一个是分类分数:

self.num_anchors * self.cls_out_channels

从分类分数可以很容易的看到,比如9个框,11个类,那么就会有9x11个socre,每个框11个类的score。输入是backbone输出的feature通过分类分支的输出,输出就是框的个数*类别。

还有一个就是回归的偏移,输入是backbone输出的feature通过回归分支的输出,输出就是框的个数x4,这个4应该就是偏移和尺度缩放。
self.num_anchors x4

这一块总结起来就是:

  1. Images input到backbone,输出featuremap(假设输出是256x100x100,每个点9个框,11类)。
  2. Featuremap分别进到分类分支,和回归分支,分类分支输出featuresizex9个框x类别数量,99x100x100,或者可以这么理解(11x9)x100x100,输出100x100x9个框,每个框11个score,9万个框,回归分支就是4(两个方向偏移+两个方向缩放)x9个框x100x100
    也就是36x100x100
cls_score = self.retina_cls(cls_feat)
bbox_pred = self.retina_reg(reg_feat)
return cls_score, bbox_pred

cls_score就是99x100x100 bbox_pred就是36x100x100,通过conv层指定输出shape很容易做到·
这个是前向阶段,因为会生成9万个框,绝大多数框都只会有背景,没有前景(所谓的正负样本不均衡)。所以要做sample和nms,同时输出只是偏移和缩放,不是真正的box两个个点四个坐标,所以需要把output转换成可以在images上画框的两个点(左上xy和右下xy)。

Retinahead继承了anchorhead函数,所有baseanchor的detection都会继承他,主要是bbox的生成。Sample,以及一些后处理

class RetinaHead(AnchorHead)

最后的list通过调用AnchorHead的get_bboxes实现

bbox_list = self.bbox_head.get_bboxes(
    *outs, img_metas, rescale=rescale)

这里的outs是cls_scores和预测的bbox偏移,缩放,img_metas是图像的相关信息,例如图像路径,原始shape等等,是一个dict。

接下来看get_bboxes是怎么运行的

def get_bboxes(self,
               cls_scores,
               bbox_preds,
               img_metas,
               cfg=None,
               rescale=False,
               with_nms=True):

get_bboxs函数的作用是根据backbone和neck(例如FPN)输出的featuremap,生成一大堆真正的框。例如输入100x100,生成9个框,那么就会生成100x100x9=90000个候选框,如果有FPN,就是每一层的feature生成框打成list,然后再对候选框做nms。

接下来就是,anchor_generator,也就是框的具体生成过程(核心就是base_anchor的生成)

self.anchor_generator.grid_anchors

一句话说清楚步骤:mmdetection的anchor生成是基于base_anchor,首先在feature的左上角生成对应的base_anchor,然后进行偏移得到剩下的框

接下来详细解释:
base_anchor的生成依赖于gen_base_anchors方法。

具体的anchor生成在gen_single_level_base_anchors

下面简单介绍一下anchor生成函数:

def gen_single_level_base_anchors(self,
                                  base_size,
                                  scales,
                                  ratios,
                                  center=None):
    """Generate base anchors of a single level.

    Args:
        base_size (int | float): Basic size of an anchor.
        scales (torch.Tensor): Scales of the anchor.
        ratios (torch.Tensor): The ratio between between the height
            and width of anchors in a single level.
        center (tuple[float], optional): The center of the base anchor
            related to a single feature grid. Defaults to None.

    Returns:
        torch.Tensor: Anchors in a single-level feature maps.
    """
    w = base_size
    h = base_size
    if center is None:
        x_center = self.center_offset * w
        y_center = self.center_offset * h
    else:
        x_center, y_center = center

    h_ratios = torch.sqrt(ratios)
    w_ratios = 1 / h_ratios
    if self.scale_major:
        ws = (w * w_ratios[:, None] * scales[None, :]).view(-1)
        hs = (h * h_ratios[:, None] * scales[None, :]).view(-1)
    else:
        ws = (w * scales[:, None] * w_ratios[None, :]).view(-1)
        hs = (h * scales[:, None] * h_ratios[None, :]).view(-1)

    # use float anchor and the anchor's center is aligned with the
    # pixel center
    base_anchors = [
        x_center - 0.5 * ws, y_center - 0.5 * hs, x_center + 0.5 * ws,
        y_center + 0.5 * hs
    ]
    base_anchors = torch.stack(base_anchors, dim=-1)

    return base_anchors

输入主要由三个参数构成:
base size:anchor大小 一般为8,如果加了FPN就是16 32 64类推(主要还是看下采样多少倍,方便转换成原图)
ratios:anchor 高宽比 一般为0.5 1.0 2.0
scales:anchor缩放比例 4.0000, 5.0397, 6.3496(mmdetection)
三种长宽比,三种尺度,所以就可以生成3X3=9种框

那么base_anchor的生成就非常的直观了:

基于basesize(8,如果用到FPN就是[8,16,24…])。和长宽比以及缩放尺度,得到左上角的9个框坐标,(左上,右下两个点的xy坐标),具体计算公式如上代码所示,很清楚了。

生成base_anchor之后,由于每个点9种anchor,对于每个点来说都是base_anchor,所以几万个anchor的生成只需要对base_anchor进行一个平移就可以了,mmdetection基于
single_level_grid_anchors函数实现:

来看看single_level_grid_anchors怎么做的:
输入:上面讲的base_anchor, backbone输出的featuremapsize(如果是FPN就是一个list),

def single_level_grid_anchors(self,
                              base_anchors,
                              featmap_size,
                              stride=(16, 16),
                              device='cuda'):

    feat_h, feat_w = featmap_size
    # convert Tensor to int, so that we can covert to ONNX correctlly
    feat_h = int(feat_h)
    feat_w = int(feat_w)
    shift_x = torch.arange(0, feat_w, device=device) * stride[0]
    shift_y = torch.arange(0, feat_h, device=device) * stride[1]
    # shift_x shift_y分别为两个方向的偏移矩阵
    shift_xx, shift_yy = self._meshgrid(shift_x, shift_y)
    shifts = torch.stack([shift_xx, shift_yy, shift_xx, shift_yy], dim=-1)
    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

首选通过torch.arange生成[0,1,2,……99]的tensor,然后乘以下采样倍数(8,16…)得到x方向和y方向偏移矩阵,shift_x,shift_y,通过repeat得到shift_xx,shift_yy,

最后的几万个anchor就是通过刚才生成的9个base_anchor与两个方向偏移矩阵想加得到最后的几万个框的左上,右下两个点的四个坐标。返回的all_anchors.

至此框也生成了,在刚才通过分类分支,和回归分支也得到了几万个框的分类score和偏移量缩放尺度等信息,下面就是一个配对过程。

Mmdetection通过_get_bboxes_single函数进行配对和nms,下面简单说明一下怎么做的:

for cls_score, bbox_pred, anchors in zip(cls_score_list,
                                         bbox_pred_list, mlvl_anchors):

核心就是这个for循环,通过zip为每个anchor进行配对,对每个anchor分配对应各个类的score,偏移,缩放。然后选取分类score中最大的topk(1000个框),然后将这1000个框送进nms进行筛选,最后通过分数阈值再次筛选就是最后反映在images上的框了。

Train阶段:

上面主要讲的是测试阶段,anchor的生成过程,下面主要介绍训练阶段,重点是anchor的sample,以及loss处理过程:

首先看retinanet的过程,很简单,backbone提取feature,然后进retinahead,配合gtbox和gtlabel,算loss,然后反向传播更新模型参数:

def forward_train(self,img,img_metas,gt_bboxes,gt_labels,gt_bboxes_ignore=None):            x = self.extract_feat(img)
 losses = self.bbox_head.forward_train(x, img_metas, gt_bboxes,gt_labels, gt_bboxes_ignore)
return losses

两句话就搞定,核心是内部怎么运行的:

这就是mmdetection的结构特性决定的,retinanet这样的base_anchor模型都需要生成anchor,sample acnhor以及相关nms等后处理,所以mmdetection就专门写了一个

class AnchorHead

来实现这个操作,写相关算法的head时候直接继承anchorhead这个类就可以了,同理,anchorhead做为一个head和很多densehead很多方法类似,所以底下还需要继承

BaseDenseHead

这个类主要就干一个事情,就是算相关的loss:刚才retinanet_head的forward_train就出自这里:

def forward_train(self,
                  x,
                  img_metas,
                  gt_bboxes,
                  gt_labels=None,
                  gt_bboxes_ignore=None,
                  proposal_cfg=None,
                  **kwargs):
       outs = self(x)

    if gt_labels is None:
        loss_inputs = outs + (gt_bboxes, img_metas)
    else:
        loss_inputs = outs + (gt_bboxes, gt_labels, img_metas)
    losses = self.loss(*loss_inputs, gt_bboxes_ignore=gt_bboxes_ignore)
    if proposal_cfg is None:
        return losses
    else:
        proposal_list = self.get_bboxes(*outs, img_metas, cfg=proposal_cfg)
        return losses, proposal_list

OK,下面就是整个train阶段的loss计算部分了:

说起retinanet,大家都知道focal loss,知道focal loss能够很好的解决正负样本不均衡,还有些也知道正负样本不均衡的原因是featurmap一个点生成3x3=9个框,几万个框,大部分没有前景只是背景,所以背景box算的loss占据主导地位,导致训练效果很差。

那么接下来就介绍,这个过程到底发生了什么:
回到刚才backbone得到featuremap进retinanet_head,这里和前向一样的,首先生成一大堆框(为每张图片,框的生成原理前面以及讲过了),(框的个数是featuremap wxhx9),然后
,读取images的label(训练阶段,input就是train_dataset的images和label,label就是image上的box位置以及类别),然后通过

def get_targets()

进行处理,这块代码我就不贴了,没什么意思,就是对输入的anchor和label等等进行一个相关的处理,例如把多层的anchor(如果用了FPN就把几层cat一起,不管哪一层都是框),然后喂到

def _get_targets_single

里面去,那么第一个处理就是对框的处理,前面说过,anchor的生成是基于baseanchor,左上00点生成了3x3=9个框,然后生成偏移向量,baseanchor+偏移矩阵得到所有的框左上右下两个点的四个坐标。这就会有个问题,有些框会超过images的边界,有些框可能会因为坐标的问题,导致小于0的坐标出现,所以在

def anchor_inside_flags()

中对每个框的四个坐标进行了一些处理,找到不合法的坐标的框,合法True,不合法False
,判断条件就是框四个坐标是否超出图片边界。生成对应的一个flag_list,代码不难,很直观。

def anchor_inside_flags(flat_anchors,
                        valid_flags,
                        img_shape,
                        allowed_border=0):
       img_h, img_w = img_shape[:2])
    if allowed_border >= 0:
        inside_flags = valid_flags & \
            (flat_anchors[:, 0] >= -allowed_border) & \
            (flat_anchors[:, 1] >= -allowed_border) & \
            (flat_anchors[:, 2] < img_w + allowed_border) & \
            (flat_anchors[:, 3] < img_h + allowed_border)
    else:
        inside_flags = valid_flags
    return inside_flags

完了之后就计算生成的box和labe提供的
Gtbox的iou,方便进入下一步运算:
因为生成的框太多了(根据类的不同和输入size的不同会有大概几万到十几万个框),毫无疑问这么多框绝大部分都是负样本。所以接下来做的就是算,每个框与gtbox的最大IOU,以及每个Gtbox和anchor之间存在的最大IOU,目的其实就是为了得到这么多框,哪些负样本,哪些正样本(的index),Retinanet为例子,一般大于0.5认为是正样本(这个框带前景),小于0.4认为是负样本(没有前景)。

一般来说假设一张图片只有一个框(label),那么生成的anchor最后只有几十是正样本,剩下绝大部分都是负样本。

那么现在框也有了,前面backbone输出的featuremap通过分类分支和回归分支得到了每个框的各个类别score,以及偏移/缩放信息,通过生成的框以及这些信息,就可以得到整个images的候选框,然后计算每个框与gtbox的iou,通过iou分出正负样本,最后送进分类loss和回归loss,以Retinanet举例,分类loss就是著名的focal loss(各类博客讲focal loss的实在是太多了这里就不重点讲) 回归loss就是smoothL1
Loss,得到loss之后打成dict输出反向传播更新模型。

至此一个典型的onestage检测模型的前向和训练细节就介绍完了,个人能力有限,里面一些细节回过神来感觉还是没有把灵魂讲到位,详细理解还是要看mmdetection的相关源码,以后有时间我再填填坑,我还是觉得做算法,还是要把细节都理解到位,这样做结构优化或者其他必要的修改就会比较舒服。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值