yolox原理

目录

1 yolox自己的理解(干货)

2 网上别人讲的好的

2.1 Decoupled Head

2.2 Data Augmentation

2.3 Anchor Free 与 Label Assignmen





 

1 yolox自己的理解(干货)

主要讲SimOTA

1,anchor point 

对应anchor box, anchor point 就是最后的特征图假如是3x3, 那么就是9个anchor point , 特征图上每一个点能表示一个框,(即预测一个框), 对应基于anchor的box

2,simOTA

为不同的目标(即GT框)设定不同数目的正样本,大目标设定的正样本数目肯定要比小目标的多,例如西瓜和蚂蚁的例子,

2、正样本特征点的必要条件
在YoloX中,物体的真实框落在哪些特征点内就由该特征点来预测。

对于每一个真实框,我们会求取所有特征点与它的空间位置情况。作为正样本的特征点需要满足以下几个特点:
1、特征点落在物体的真实框内。
2、特征点距离物体中心尽量要在一定半径内。

特点1、2保证了属于正样本的特征点会落在物体真实框内部,特征点中心与物体真实框中心要相近。

上面两个条件仅用作正样本的而初步筛选,然后会再使用了SimOTA方法进行动态的正样本数量分配,具体原理如下:
 

首先是确定正样本的后选区域(后选区域是候选区域,字写错了,后面就没改过来),也就是上面说的:

1、作为正样本的特征点(后面用anchor_point表示)要落在物体的真实框内。
2、作为正样本的特征点距离物体中心要在一定半径内(就是后面说的以GT中心点坐标映射到特征图上的坐标为中心的5x5的区域)

如下图:

图上是以640x640图像输入,3个输出特征图中其中的20x20的特征图

有3个红色的GT框(是GT在特征图上的映射),分别用红色框表示,每个GT框的中心用红色格子表示,黄色框是以GT框中心为中心的5x5的区域,以GT框1为例子:

GT框1与黄色框的并集部分 = 14个蓝色格子+1个红色格子+10个绿色格子 +6个黄色格子共31个格子

注意:并集部分不包括6个紫色格子,因为这6个紫色格子没有超过一半落在GT框内,从上图看只有约1/4部分落在GT框内

GT框1与黄色框的交集部分14个蓝色格子+1个红色格子 共15格子

同理,
GT框2与黄色框的并集部分 = 14个蓝色格子+1个红色格子+10个绿色格子+12个黄色格子共37个格子,

注意:这里并集部分格子不像GT1框的紫色格子,是有超过一半落在GT框内的,所以算在并集里面

GT框2与黄色框的交集部分 = 14个蓝色格子+1个红色格子 共15格子

GT框3与黄色框的并集部分 = 8个蓝色格子+1个红色格子+16个绿色格子共25个格子

GT框3与黄色框的交集部分 = 8个蓝色格子+1个红色格子共9个格子

对于20x20的特征图的后选正样本=3个GT框与对应黄色框的并集部分(其实有疑惑为什么交集不直接作为后选正样本,因为看后面求cost的三项组成,最后一项,表示如果不是在交集部分,直接乘了10000的系数,导致如果不在交集的anchor_point的cost很大,按cost从小到大排序,肯定选不到这些不在交集部分的anchor_point, 所以最终的正样本格子(anchor_point)一定是在交集部分,可能原因是不想这么简单粗暴,不然后面的算[num_gt, 后选正样本个数] 这种形状的IOU矩阵和cost矩阵派不上用场, 也有可能是后面计算出K之后, 假设k大于交集部分格子的数量,举个例子,某个GT框通过simota算出它需要10个正样本,取cost矩阵前十个最小的,但交集部分格子只有4个,那剩下6个可以去并集补充(即使这6个格子的cost很大,因为乘了10000,但没办法,交集格子已经不够了)以上都是自己的想法哈),并集部分也就是共31+37+25=93个正样本,也就是说特征图是400个格子(anchor_point), 只有93个格子是后选正样本。

那么还有另外两个特征图40x40和80x80的也是类似,这里只给出40x40的特征图:

 如上图,还是这3个GT,但是由于是映射到40x40的特征图,所以中心点和宽高相应扩大了2倍,依然是求红色GT框与对应黄色框的并集和交集

GT1与黄色框并集 = 7x15 = 105个格子(绿色+蓝色+红色格子)

GT1与黄色框交集 = 25个格子(即黄色框内的所有格子,蓝色+红色)

GT2与黄色框并集 = 5x19 = 95个格子

GT2与黄色框交集 = 25个格子

GT3与黄色框并集 = 25个格子

GT3与黄色框交集 = 25个格子

所以对于40x40的特征图,后选正样本 = 105+95+25 = 225个正样本

就是说特征图是1600个格子(anchor_point), 只有225个格子是后选正样本。

同理对于80x80的特征图, 假设算的后选正样本 = 500个正样本,

那么对于这3个特征图,20x20, 40x40,80x80, 一共 400+1600+6400 = 8400格子

只有93+225+500 = 818个格子是后选正样本,将这818个格子对应位置赋1,其他位置赋0,得到一个mask向量,

以20x20的特征图为例,可以构建一个20x20的全0 mask矩阵,把93个后选正样本的位置赋1(为0的格子太多了就不赋了)

然后把20x20的mask矩阵拉平成一维的[400,] mask向量

同理40x40的mask矩阵被拉平成一维的[1600,]的mask向量

80x80的mask矩阵被拉平成一维的[6400,]的mask向量

,然后把[6400,] 和[1600,],和[400,] 按顺序concat成[8400, ]的一维mask_向量 ,注意顺序,并不是 [400,] [1600,] [6400]这个顺序,官方代码用fg_mask表示

由于这3个特征图维度太大不好举例说明,下面以2x2和4x4为例子,即20x20的特征图假设是2x2的,40x40的特征图假设是4x4的,那他们对应的mask矩阵假设如下:为1的位置表示是后选正样本:

        

 

 那么拉平,把4x4的拉平成维度为[16, ]的一维向量 , 把2x2的拉平成维度[4,] ,的一维向量

然后合并成维度为[20, ] 的一维mask向量如下:

 所以,上面说的那个fg_mask就是类似这种,只不过它的维度是[8400, ] , 即一行8400列,

而8400也对应网络的输出:

设batch = 2, 类别是VOC数据集,20类,网路输出有三个:

[2, 80, 80, 4+1+20] 即[2, 80, 80, 25]对应80x80特征图,取1张图片 = [80, 80, 25], 拉平 = [6400, 25]

[2, 40, 40, 4+1+20] 即[2, 40, 40, 25]对应40x40特征图, 取1张图片 =  [40, 40, 25],拉平=[1600, 25]

[2, 20, 20, 4+1+20]  即[2, 20, 20, 25]对应20x20特征图,取1张图片 = [20, 20, 25],拉平=[400, 25]

将三者concat网络总输出 = [8400, 25], 和fg_mask [8400, ]是对应的, 只有对应位置为1的地方,就是正样本才会算cls和reg损失, 一张图片的总输出,也就是预测值是[8400, 25] ,分为box坐标预测bboxes_preds_per_image, 分类预测cls_pres_per_image, 和是否有目标预测obj_preds_per_image

box坐标预测 bboxes_preds_per_image = [8400, 4]

分类预测cls_pres_per_image = [8400, 20]

目标预测obj_preds_per_image= [8400, 1]

然后取fg_mask, 8400个输出只取818的正样本的输出

bboxes_preds_per_image = bboxes_preds_per_image[fg_mask] = [818, 4]

cls_pres_ = cls_pres_per_image[fg_mask] = [818, 20]

obj_preds_ = obj_preds_per_image[fg_mask] = [818, 1]

官方原始代码yolo_training.py 在get_assignment函数

fg_mask, is_in_boxes_and_center = self.get_in_boxes_info

除了得到了一个维度为[8400, ] 的 fg_mask

还得到了一个维度为[3, 818]的is_in_boxes_and_center ,3表示张图片的GT数

is_in_boxes_and_center表示是交集部分,每一行表示每一个GT与黄色框的交集部分,这里818太大了,假设是18, 假设上面得到的3个GT框与对应黄色框的交集是9个格子,

其中GT1与对应黄色框的交集是4个格子

其中GT2与对应黄色框的交集是3个格子

其中GT3与对应黄色框的交集是2个格子

则is_in_boxes_and_center 维度是[3, 18], 具体如下:

第一行表示 GT1与对应黄色框的交集是4个格子,所以有4个位置赋1

第一行表示 GT2与对应黄色框的交集是3个格子,所以有3个位置赋1

第一行表示 GT3与对应黄色框的交集是2个格子,所以有2个位置赋1

而每一行的长度是18,是并集的格子个数,即所有后选正样本

接着要算这张图片(都是按batch循环的)的3个gt和818个后选正样本的IOU矩阵,每个GT都要和这818个后选正样本算IOU, 所以得到的是一个形状为[3, 818]的IOU矩阵, 这里还是假设是18,不是818个后选正样本,方便举例,因为818太多了不好画:

        pair_wise_ious      = self.bboxes_iou(gt_bboxes_per_image, bboxes_preds_per_image, False)
  

其中

gt_bboxes_per_image = [3, 4] 表示3个GT框的中心点坐标x,y和宽高w,h (在640x640的输入图片上)

bboxes_preds_per_image = [818, 4] 表示818个预测框的坐标预测内容,。这个预测内容是解码后的预测,不是网络的原始预测,网络原始x,y预测是偏移量,所以送进损失函数的预测是解码后的预测,并不是原始预测的偏移量,

IOU矩阵pair_wise_ious,形状为[3, 18]如下:

而IOU_loss = pair_wise_ious_loss

pair_wise_ious_loss = -torch.log(pair_wise_ious + 1e-8)

pair_wise_ious_loss的shape也是[3, 18]

即算的是3个GT和18个Anchor_point后选正样本的IOU_loss,

然后还会算3个GT和18个Anchor_point后选正样本的的分类loss,即

pair_wise_cls_loss  = F.binary_cross_entropy(cls_preds_.sqrt_(), gt_cls_per_image, reduction="none").sum(-1)

其中cls_pres_ 本来在上面有说过是 [818, 20] ,换成[18, 20] 

然后会复制3份(因为有3个GT), 变成了 [3, 18, 20]

而gt_cls_per_image 本来是[3, 20] 表示每一个GT的cls标签,20类,如下

[[0, 0, ...,1]

[0, 1, ...,0]

[1, 0, ...,0]]

然后会复制18份,因为有18个后选正样本,变成了[3, 18,20]

所以二者做分类损失,求出来的损失也是[3, 18, 20], 然后再对最后一维求和,所以最终的

3个GT和18个Anchor_point后选正样本的的分类loss,即pair_wise_cls_loss = [3, 18]

也是一个3行18列的矩阵, 代表3个GT和18个Anchor_point的分类损失。即每个GT都要和这个18个Anchor_point求分类loss

然后定义cost

cost = pair_wise_cls_loss + 3.0 * pair_wise_ious_loss + 100000.0 * (~is_in_boxes_and_center).float()

cost矩阵由

分类loss,  pair_wise_cls_loss: [3, 18]

iou_loss: pair_wise_ious_loss [3, 18]

以及is_in_boxes_and_center 组成,[3, 18]

10000 * (~is_in_boxes_and_center )表示如果不在is_in_boxes_and_center这个区域(即GT和黄色框的交集),cost就乘10000, 因为后面动态分配原本是根据这个cost从小到大排序来选择正样本的,选cost最小的几个Anchor_point,那么不在is_in_boxes_and_center这个区域,它的cost就很大,就不会被选到。

得到cost矩阵后,该矩阵也是一个[3, 18]的矩阵 , 即计算的是3个GT和18个anchor_point的cost,

计算得到的cost矩阵如下:

 而上面说的is_in_boxes_and_center如下:

is_in_boxes_and_center对应为1的位置的cost比较小,比如为6.4, 5.7的等等,而其他位置的cost都是e+5(b表示100000),

接下来确定每个GT需要多少个正样本,这步是用IOU矩阵求得:

 IOU矩阵

可以看到,is_in_boxes_and_center对应为1的位置(既在GT框中心,又在5x5的黄色区域)的Anchor_point与GT的IOU都比较大一点,其他位置稍微小一点。

对每个GT对应的18个Anchor_point正样本取前10个(也可以取前20,论文说的是10)  IOU最大的的Anchor_point, 然后将这10个最大的IOU相加取整,得到每个GT对应的正样本数目k, 

例如:和GT1的IOU最大的前10个Anchor_point依次是:IOU矩阵第一行红色和绿色的区域,这10个IOU的的和为:0.31+0.79+0.68+0.45+0.48+0.36+0.20+0.37+0.38+0.65+0.64 = 4.93,  取整得到5, 意思是给GT1最终分配5个正样本,那现在GT1对应的有18个后选正样本,取哪5个呢,取cost最小的前5个,看cost矩阵:这个cost矩阵和上面一样,只不过上面很多的e+5其实还有区别的,可能有的1.0001 x 10e+5, 有的是1.0006 x 10e+5, 就还是大小区别的,只不过格子填不下,省略了。

 cost矩阵

 第一行最小的5个cost是6.4, 5.7, 0.99e+5,  2.5,  3.4 这五个Anchor_point, 也就是A3,A4,A14,A15, A16,这5个Anchor_point做为GT1最终正样本,

同理,和GT2的IOU最大的前10个Anchor_point依次是:IOU矩阵第二行红色和绿色的区域,和部分灰色区域, 即0.41+0.14+0.61+0.75+0.28+0.3+0.2+0.7+0+0 = 3.39, 取整=3,所以给GT2分配3个正样本,即k=3,  要从GT2对应的18个后选正样本选cost最小的3个,看cost矩阵第二行, 和GT2 cost最小的是4.8, 3.9, 9.6 这3个Anchor_point, 也就是 A6,A7,A17 作为GT2的最终正样本,

同理和和GT3的IOU最大的前10个Anchor_point依次是:IOU矩阵第三行红色和绿色的区域,和部分灰色区域,即0.04+0.18+0.2+0.02+0.03+0.01 + 0 + 0+0+0 = 0.48 取整的话是0, 但是k最小要是1, 就是为了保证每一个GT至少有一个正样本,这个simOTA是训练过程中一直会有的,就是说,刚开始训练时, GT3可能算出来这个IOU的和只有0.48, 导致它只有一个正样本, 然后训练到后面,这个IOU的和可能是2.4,取整变成2, 分配给它2个正样本,继续训练,又会算出IOU的和为6.1, 分配给它6个正样本,所以SimOTA并不是只算一次的,是在训练过程中多次计算的。显然这里只给GT3分配一个正样本,按cost选择,是选A5这个样本。

还有一种情况,假如同一个Anchor_point分配给了不同的GT, 那么到底由哪个GT和这个Anchor_point匹配呢?答案,哪个GT与这个Anchor_point的cost小,就选哪个GT

动态选择最终的正样本官方代码是这个dynamic_k_matching这个函数:

    def dynamic_k_matching(self, cost, pair_wise_ious, gt_classes, num_gt, fg_mask):
        #-------------------------------------------------------#
        #   cost                [num_gt, fg_mask] [3, 18]
        #   pair_wise_ious      [num_gt, fg_mask] [3, 18]
        #   gt_classes          [num_gt]        
        #   fg_mask             [8400,]
        #   matching_matrix     [num_gt, fg_mask] [3, 18]
        #-------------------------------------------------------#
        matching_matrix         = torch.zeros_like(cost)

        #------------------------------------------------------------#
        #   选取iou最大的n_candidate_k个点
        #   然后求和,判断应该有多少点用于该框预测
        #   topk_ious           [num_gt, n_candidate_k]
        #   dynamic_ks          [num_gt]
        #   matching_matrix     [num_gt, fg_mask]
        #------------------------------------------------------------#
        n_candidate_k           = min(10, pair_wise_ious.size(1))
        topk_ious, _            = torch.topk(pair_wise_ious, n_candidate_k, dim=1)
        dynamic_ks              = torch.clamp(topk_ious.sum(1).int(), min=1)
        
        for gt_idx in range(num_gt):
            #------------------------------------------------------------#
            #   给每个真实框选取最小的动态k个点
            #------------------------------------------------------------#
            _, pos_idx = torch.topk(cost[gt_idx], k=dynamic_ks[gt_idx].item(), largest=False)
            matching_matrix[gt_idx][pos_idx] = 1.0
        # del topk_ious, dynamic_ks, pos_idx

        #------------------------------------------------------------#
        #   anchor_matching_gt  [fg_mask]
        #------------------------------------------------------------#
        anchor_matching_gt = matching_matrix.sum(0)
        if (anchor_matching_gt > 1).sum() > 0:
            #------------------------------------------------------------#
            #   当某一个特征点指向多个真实框的时候
            #   选取cost最小的真实框。
            #------------------------------------------------------------#
            _, cost_argmin = torch.min(cost[:, anchor_matching_gt > 1], dim=0)
            matching_matrix[:, anchor_matching_gt > 1] *= 0.0
            matching_matrix[cost_argmin, anchor_matching_gt > 1] = 1.0
        #------------------------------------------------------------#
        #   fg_mask_inboxes  [fg_mask]
        #   num_fg为正样本的特征点个数
        #------------------------------------------------------------#
        fg_mask_inboxes = matching_matrix.sum(0) > 0.0
        num_fg          = fg_mask_inboxes.sum().item()

        #------------------------------------------------------------#
        #   对fg_mask进行更新 以前是18个候选正样本,经过上面操作后,现在缩减到9个
        #------------------------------------------------------------#
        fg_mask[fg_mask.clone()] = fg_mask_inboxes

        matched_gt_inds     = matching_matrix[:, fg_mask_inboxes].argmax(0)
        gt_matched_classes  = gt_classes[matched_gt_inds]

        pred_ious_this_matching = (matching_matrix * pair_wise_ious).sum(0)[fg_mask_inboxes]
        return num_fg, gt_matched_classes, pred_ious_this_matching, matched_gt_inds
 

首先是构造了全0矩阵matching_matrix矩阵 = [3, 18]

然后,根据每个GT匹配到的正样本,将相应位置赋1,,如下

 matching_matrix矩阵

因为上面已经计算出GT1的最终正样本是A3,A4,A14,A15, A16, 所以matching_matrix矩阵第一行,A3,A4,A14,A15, A16 这5个位置赋1

GT2对应的最终正样本是A6,A7,A17, 所以所以matching_matrix矩阵第二行,A6,A7,A17这3个位置赋1,

GT3对应的最终正样本是A5, 所以所以matching_matrix矩阵第三行,A5这1个位置赋1

而anchor_matching_gt = matching_matrix.sum(0), 即在第列这个维度求和,即把matching_matrix每一列相加得到:

anchor_matching_gt

有一种情况,同一个Anchor_point分配给了不同的GT,

即用上面的方法算出来GT2的最终正样本是A5,A6,A7,A17

用上面的方法算出来GT3的最终正样本是A5

算出来这个时候matching_matrix可能如下:

图中A5这个Anchor_point既与GT2匹配上了,又与GT3匹配上了(这种情况肯定会发生的,即两个目标GT框有重合的部分)那到底A5是作为GT2的正样本来负责预测GT2这个目标框,还是作为GT3的正样本来负责预测GT3这个目标框呢?答案是唯一的,必须只能选一个,要么是GT2的正样本,要么是GT3的正样本,如果是GT2的正样本,那么A5的这个正样本的分类和box坐标标签就是GT2目标框的的cls标签和boxx坐标标签。

代码是这样实现的,如果出现这种情况,那么相应的anchor_matching_gt就变成了:

 当检测到anchor_matching_gt有元素大于1, 这里就是A5现在变成了2大于1,说明出现了这种情况,对应下面代码的第一行的if条件

        if (anchor_matching_gt > 1).sum() > 0:
            _, cost_argmin = torch.min(cost[:, anchor_matching_gt > 1], dim=0)
            matching_matrix[:, anchor_matching_gt > 1] *= 0.0
            matching_matrix[cost_argmin, anchor_matching_gt > 1] = 1.0

再利用第二行代码,将cost中,A5对应的第5列的值取出,并进行比较,计算最小值所对应的行数,以及分数。cost矩阵如下:

 这个为了适配同一列出现了多个1这种情况,稍微改了一下cost矩阵,上面的cost矩阵GT2和A6的是4.8, 这里变成了GT2和A5是4.8

第5列的值是e+5, 4.8, 4.5, 其中4.5最小,且4.5对应的行数 cost_argmin = 3(GT3)

经过第三行代码,将matching_matrix第5列都置0。然后再将第五列的第三行(上面求到的行数cost_argmin )置1,整个变化过程如下:

 matching_matrix矩阵

然后会执行这一句:

fg_mask_inboxes = matching_matrix.sum(0) > 0.0

这里得到的fg_mask_inboxes 是对matching_matrix每一列求和,然后大于0为1,否则为0,其实和上面的anchor_matching_gt 是一样的,比如这个就是如下,形状是[18, ], 也就是说18个后选正样本,只有9个最终的样本位置是1:
 

 

fg_mask_inboxes

 最终正样本的数目num_fg = fg_mask_inboxes.sum().item() 这里就是9

然后对fg_mask进行更新:之前fg_mask是一个[8400, ] 的mask向量, 只有是后选正样本的位置为1,其他为0, 之前是有18个候选正样本位置是1(其实是818,但前面说过一直说过假设是18), 那么更新之后,只有9个位置是1,因为最终只有9个正样本,更新代码如下:

fg_mask[fg_mask.clone()] = fg_mask_inboxes

fg_mask 是一个[8400, ] 的一维向量(1行8400列), fg_mask 只有18个位置是1,表示有18个候选正样本。

fg_mask_inboxex是一个[18,],长度是候选正样本的个数,必须等于18,不然没发做广播机制, fg_mask_inboxex只有9个位置是1,表示有9个最终正样本。

举个例子,这里8400太大了,假设不是8400是20,假设fg_mask = [20, ] ,如下:有18个位置为1,表示候选正样本的位置,

fg_mask

然后fg_mask_inboxex如下:

fg_mask[fg_mask.clone()] = fg_mask_inboxes这一句意思是fg_mask以前18个为1的位置,现在要根据fg_mask_inboxex这个长度为18的向量重新赋值,举个例子:fg_mask_inboxex[0] = 0, 所以fg_mask第1个为1的位置要变成0,fg_mask_inboxex[2] = 1, 所以fg_mask第3个为1的位置要变成1,具体对应关系如下:

所以,fg_mask这个[20, ]的向量从以前有18个位置是1, 现在变成了只有9个位置是1,

fg_mask这个[8400, ] 的mask向量从以前有818个位置是1, 现在只有9个位置是1, 其中818是候选正样本,9是最终的正样本。

示例代码:

import torch

fg_mask = torch.tensor([0,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1]) > 0.0
fg_mask_inboxes = torch.tensor([0,0,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,0]) > 0.0
fg_mask[fg_mask.clone()] = fg_mask_inboxes

# tensor([0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0],dtype=torch.uint8)
print(fg_mask)

然后再给这9个正样本打cls分类标签以及reg坐标标签, 因为只有正样本才计算cls损失和reg损失,而obj损失,正负样本都要计算,

首先是cls分类标签, 根据如下的matching_matrix矩阵可知:

A3这个正样本的cls标签等于GT1的类别, 因为A3被分配给了GT1,负责预测GT1,

同理A4这个正样本的cls类别标签也是GT1的类别

A5这个正样本的cls标签是GT3的类别...

假设GT1类别是狗,类别索引为2,

GT2类别是人,类别索引为3,

GT3类别是人,类别索引为3

所以这9个正样本的,A3,A4,A5, A6,A7,A14,A15,A16,A17 的类别标签用gt_matched_classes表示

gt_matched_classes = [2,2,3,3,  3,2,  2,  2,  3]

matching_matrix矩阵

这个9个正样本的与各自对应GT的IOU设为pred_ious_this_matching

pred_ious_this_matching = (matching_matrix * pair_wise_ious).sum(0)[fg_mask_inboxes]

根据代码:pred_ious_this_matching = matching_matrix * pair_wise_ious.sum(0)[fg_mask_inboxes]

matching_matrix pair_wise_ious如下:


 

 上面像A6这个正样本 和GT2匹配的,但对应的IOU为0, 这应该是不合理的,因为这些数值都是随便填的,并不是真实数值,实际上不会出现这这种情况,匹配到IOU不会为0的,大家明白就好

对应元素相乘得到:

然后对第0维求和matching_matrix * pair_wise_ious.sum(0),就按列相加,得到:

 然后再取matching_matrix * pair_wise_ious.sum(0)[fg_mask_inboxes] , 而fg_mask_inboxes如下:

fg_mask_inboxes

所以,matching_matrix * pair_wise_ious.sum(0)[fg_mask_inboxes],fg_mask_inboxes中为1对应位置的元素,如下,长度为9:

 pred_ious_this_matching

示例代码:

a = torch.tensor([0, 0, 0.78, 0.68, 0.18, 0, 0.75, 0, 0, 0 ,0 ,0, 0, 0, 0.65, 0.64, 0.7 ,0])
fg_mask_inboxes= torch.tensor([0,0,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,0]) > 0.0

c = a[fg_mask_inboxes]
print(c) # tensor([0.7800, 0.6800, 0.1800, 0.0000, 0.7500, 0.0000, 0.6500, 0.6400, 0.7000])

最后就是计算loss的环节了(训练的loss, 不是前面计算cost的loss)

loss = 9个最终正样本的cls_loss + 9个最终正样本的reg_loss + 8400的正负样本的obj_loss

首先是cls_loss

我们前面得到了gt_matched_classes =[2,2,3,3,  3,2,  2,  2,  3], 首先做一步转换:

cls_target = F.one_hot(gt_matched_classes.to(torch.int64), self.num_classes).float() 

假设类别数=5, 那么cls_taeget = 

[[0, 0, 1, 0, 0],

[0, 0, 1, 0, 0],

[0, 0, 0, 1, 0],

[0, 0, 0, 0, 0],

[0, 0, 0, 1, 0],

[0, 0, 1, 0, 0],

[0, 0, 1, 0, 0],

[0, 0, 1, 0, 0],

[0, 0, 0, 1, 0]]

然后再乘以一个pred_ious_this_matching, 前面计算得到的IOU,相当于做了标签平滑

cls_taeget = cls.target * pred_ious_this_matching = 

[[0, 0, 0.79, 0, 0],

[0, 0, 0.68, 0, 0],

[0, 0, 0, 18, 0],

[0, 0, 0, 0, 0],  #本来这个不应该为0,因为pred_ious_this_matching 不会有0的,原因前面说了

[0, 0, 0, 0.75, 0],

[0, 0, 0, 0, 0], #本来这个不应该为0,因为pred_ious_this_matching 不会有0的,原因前面说了

[0, 0, 0.65, 0, 0],

[0, 0, 0.64, 0, 0],

[0, 0, 0, 0.7, 0]]

cls_target = [9,  5]   9个正样本, 5个类别, 这是这9个正样本的类别标签

然后这个9个正样本的坐标标签reg_target,直接找对应的gt_boxes的中心点坐标x,y和宽高w,h

官方代码如下:

reg_target  = gt_bboxes_per_image[matched_gt_inds]

假设3个GT的中心点坐标x,y和宽高w,h, 即xywh是

GT1:  100, 100, 10,  10

GT2:200, 200,  20,20

GT3:300, 300, 30, 30

那么根据matching_matrix 可知,

比如A3的reg_target就是GT1的xywh, 

A5的reg_target就是GT3的xywh, 

A6的reg_target就是GT2的xywh, 

令A3_rt = A3_reg_target

reg_target = [A3_rt, A4_rt, A5_rt, A6_rt, A7_rt, A14_rt, A15_rt, A16_rt, A17_rt ]

=                  [GT1,   GT1,  GT3,  GT2,  GT2,   GT1,     GT1,      GT1,     GT2 ]

reg_target  =  [9 , 4]

[ [100, 100, 10,  10],

[100, 100, 10,  10],

[300, 300, 30, 30],

[200, 200,  20,20],

​​​​​​​[200, 200,  20,20],

[100, 100, 10,  10],

[100, 100, 10,  10],

[100, 100, 10,  10],

[200, 200,  20,20]]

obj_target = fg_mask.unsqueeze(-1) = [8400, 1],

综上所以对于这一张图片来讲,有9个正样本,并且这9个正样本的分类标签cls_target,回归标签reg_target,  是否有目标标签obj_target, 以及fg_mask 分别如下:

cls_target = [9,  5]

reg_target = [9,  4]

obj_target = [8400, 1] 9个是1

fg_mask = [8400, ] 有9个是1

但loss一般是一个batch算的, 假设batch_size是2的话吗,循环到第二张图片,假设对第二张图片,最终simOTA选出了11个正样本,此时

cls_target = [11,  5]

reg_target = [11,  4]

obj_target = [8400, 1] 11个是1

fg_mask = [8400, ] 有11个是1

那么把这两张图片的cls_targe, reg_target ,obj_target , fg_mask 

concat一下吗,得到一个batch的cls_targets, reg_targets, obj_targets, fg_masks

        cls_targets = torch.cat(cls_targets, 0) # [num_fg, num_class]
        reg_targets = torch.cat(reg_targets, 0) # [num_fg, 4]
        obj_targets = torch.cat(obj_targets, 0) # [8400 * batch, 1]
        fg_masks    = torch.cat(fg_masks, 0) # [8400 * batch, ]

cls_targets = [20,  5] # 第一张图片的[9,  5]和第二张图片的[11, 5] 拼接

reg_targets = [20,  4]

obj_targets= [16800, 1]  两个8400拼接,共20个是1

fg_mask = [16800, ]  两个8400拼接,共20个是1

loss计算如下:

        loss_iou    = (self.iou_loss(bbox_preds.view(-1, 4)[fg_masks], reg_targets)).sum()
        loss_obj    = (self.bcewithlog_loss(obj_preds.view(-1, 1), obj_targets)).sum()
        loss_cls    = (self.bcewithlog_loss(cls_preds.view(-1, self.num_classes)[fg_masks], cls_targets)).sum()
        reg_weight  = 5.0
        loss = reg_weight * loss_iou +   + loss_cls

一项一项看:

先看回归loss, 这里用的是iou_loss

loss_iou = self.iou_loss(bbox_preds.view(-1, 4)[fg_masks], reg_targets)

注意这里bbox_preds是网络解码之后的预测值,注意不是网络原始预测,这里解码之后的预测值就是框的中心点坐标xy和宽高wh,但网络实际预测的是相对格子的偏移量,

因为 bbox_preds的中心坐标x,y =  相对于网格的偏移量 +  网格坐标

其中网格坐标是固定的,比如20x20的特征图坐标,

[(0, 0),(0, 1),...(0, 19)

....

(19,0), (19, 1),...,(19,19)]

所以只要bbox_preds 学习的好,说明偏移量预测的才准,不然bbox_preds reg_targets算loss会很大

bbox_preds的shape = [2, 8400, 4] 

2表示batch_size

8400表示8400个框

4表示每个框的中心点坐标xy和宽高wh,  xywh

bbox_preds.view(-1, 4) = [16800, 4]

然后取20个正样本的坐标预测值

bbox_preds.view(-1, 4)[fg_masks] = [20, 4]  表示只取20个最终正样本的中心点坐标xy和宽高wh

这样就和reg_targets的shape一样了, 只有shape一样,才能做loss

所以得到的损失的shape 是 [20, ] , 表示20个正样本的iou_loss 然后再用.sum() 求和,得到一个reg_loss标量值

再看obj损失,用的二元逻辑交叉熵损失

loss_obj    = (self.bcewithlog_loss(obj_preds.view(-1, 1), obj_targets)).sum()

obj_preds是网络预测是否是目标的概率值:[2, 8400, 1]

2表示batch_size

8400表示8400个框

1表示每个框的属于是目标的概率:

self.bcewithlog_loss(obj_preds.view(-1, 1) = [16800, 1]

obj_targets = [16800, 1] , 二者shape相同,

假设  obj_preds = [0.1, 0.6, 0.02, 0.9, 0.7, 0.3]

        obj_targets   = [0,  1,  0,  1,  1,  0] # 为1表示是正样本,即是目标

二者假如损失 = [ 0.1,  0.4,  0.02,  0.1,  0.3,  0.7] 

所以self.bcewithlog_loss(obj_preds.view(-1, 1), obj_targets) 计算完损失shape = [16800, 1]

然后求和得到一个标量obj_loss

再看分类cls损失, 用的二元逻辑交叉熵损失

​​​​​​​​​​​​​​ loss_cls    = (self.bcewithlog_loss(cls_preds.view(-1, self.num_classes)[fg_masks], cls_targets)).sum()

cls_preds是网络预测分类概率值,即具体属于哪个类别的概率= [2, 8400,5]

cls_preds.view(-1, self.num_classes) = [16800,  5]

然后取20个正样本的分类预测值:

cls_preds.view(-1, self.num_classes)[fg_masks] = [20, 5]

cls_targets的shape一致了, 可以计算loss了,

​​​​​​​​​​​​举个例子,假设只有2个正样本(20个太多了,不好举例子)

cls_preds=    [[0.1,  0.6,  0.3,  0.04,  0.06]

                       [0.2,  0.1,  0.25,   0.3,   0.15]

cls_targets   =[[0,     0,      1,   0,     0],

                         [0,    0,       0,   1,    0]] # 这里本来要乘IOU平滑的,举例子就不乘了

二者计算loss得到

self.bcewithlog_loss(cls_preds, cls_targets)

  [[0.01,  0.36,  0.49,  0.02,  0.06]

      [0.04,  0.07,  0.23,   0.49,   0.12] 

shape和cls_preds和cls_targets  一致

所以最后得到的cls分类损失 shape = [20, 5] , 然后求和(两个维度都求和), 得到一个标量cls_loss

然后设置权重,把iou_loss和obj_loss, 以及分类loss做一个加权和, 再除以一个batch正样本总数num_fgs, 这里就是20

但有一点疑惑?

iou_loss和分类cls_loss只算了正样本的loss, 所以除以的是正样本的数量20

但obj_loss算的是16800个正负样本的所有loss为什么obj_loss 也要除以20呢?

个人理解应该用

 (reg_weight * loss_iou + loss_cls)除以20

然后对loss_obj再考虑除以什么做平均, 直接除以16800会不会不太合理, 好像除以20感觉也行

        reg_weight  = 5.0
        loss = reg_weight * loss_iou + loss_obj + loss_cls
        loss = loss / num_fgs

2 网上别人讲的好的

 别人讲的好的文章,yolox作者写的

以下正文部分内容,参考该链接

如何评价旷视开源的YOLOX,效果超过YOLOv5? - 知乎感谢大家对旷视开源的 YOLOX 关注,本篇回答我们邀请了旷视研究院的刘松涛、葛政来为大家解读,小编把话…https://www.zhihu.com/question/473350307/answer/2021031747

回到 YOLOX 设计的具体细节上,我们认为与之前 YOLO 最大的区别在于 Decoupled Head,Data Aug,Anchor Free 和样本匹配这几个地方。下面将从这几个方面展开聊。

2.1 Decoupled Head

 1.1 YOLOX的解耦头结构思考

参考这篇博客YOLOX的解耦头结构思考_望~的博客-CSDN博客_yolox 解耦头

讲的很好总结就是

分类更加关注所提取的特征与已有类别哪一类最为相近,而定位更加关注与GT Box的位置坐标从而进行边界框参数修正。因此如果采取用同一个特征图进行分类和定位,效果会不好,考虑到分类和定位所关注的内容的不同。因此采用不同的分支来进行运算,有利于效果的提升!同时为了避免计算量的大量增加,比如YOLOX的Decoupled Head结构,会先进行1x1的降维操作,然后再接上分类和定位两个分支,并在分类和回归分支里各使用了 2个3x3 卷积, 最终调整到仅仅增加一点点参数, 做一个检测效果和速度的trade-off,这也是很常见的一个思路!

根据表面上看,解耦检测头提升了 YOLOX 的性能和收敛速度,但更深层次的,它为 YOLO 与检测下游任务的一体化带来可能。比如:

1. YOLOX + Yolact/CondInst/SOLO ,实现端侧的实例分割。

2. YOLOX + 34 层输出,实现端侧人体的 17 个关键点检测。

以往或许已经有一些这方面的工作,但这些领域的 SOTA 依然拥有他们特殊的结构设计,使得该领域无法直接享用到 YOLO 系列发展的成果。如今一个打开了枷锁的检测头,我们认为会让 YOLO 与检测的下游任务更深层次的结合,为检测与下游任务的端到端一体化带来一些变化。

2.2 Data Augmentation

Mosaic 经过 YOLOv5 和 v4 的验证,证明其在极强的 baseline 上能带来显著涨点。我们组早期在其他研究上发现,为 Mosaic 配上 Copypaste,依然有不俗的提升。组内的共识是:当模型容量足够大的时候,相对于先验知识(各种 tricks,hand-crafted rules ),更多的后验(数据/数据增强)才会产生本质影响。通过使用 COCO 提供的 ground-truth mask 标注,我们在 YOLOX 上试了试 Copypaste,下表表明,在 48.6mAP 的 YOLOX-Large 模型上,使用 Copypaste 带来0.8%的涨点。

Mosaic 经过 YOLOv5 和 v4 的验证,证明其在极强的 baseline 上能带来显著涨点。我们组早期在其他研究上发现,为 Mosaic 配上 Copypaste,依然有不俗的提升。组内的共识是:当模型容量足够大的时候,相对于先验知识(各种 tricks,hand-crafted rules ),更多的后验(数据/数据增强)才会产生本质影响。通过使用 COCO 提供的 ground-truth mask 标注,我们在 YOLOX 上试了试 Copypaste,下表表明,在 48.6mAP 的 YOLOX-Large 模型上,使用 Copypaste 带来0.8%的涨点。

可 Copypaste 的实现依赖于目标的 mask 标注,而 mask 标注在常规的检测业务上是稀缺的资源。而由于 MixUp 和 Copypaste 有着类似的贴图的行为,还不需要 mask 标注,因此可以让 YOLOX 在没有 mask 标注的情况下吃到 Copypaste 的涨点。不过我们实现的 Mixup,没有原始 Mixup 里的 Bernoulli Distribution 和 Soft Label ,有的仅是 0.5 的常数透明度和 Copypaste 里提到的尺度缩放 ( scale jittering )。 YOLOX 里的 Mixup 有如此明显的涨点,大概是因为它在实现和涨点原理上更接近 Copypaste,而不是原版 Mixup。

Data Augmentation 里面需要强调的一点是: 要在训练结束前的15个 epoch 关掉 Mosaic 和Mixup ,这对于 YOLOX 非常重要。可以想象,Mosaic+Mixup 生成的训练图片,远远脱离自然图片的真实分布,并且 Mosaic 大量的 crop 操作会带来很多不准确的标注框,见下图



2.3 Anchor Free 与 Label Assignmen
 

Anchor Free 的好处是全方位的。1). Anchor Based 检测器为了追求最优性能通常会需要对anchor box 进行聚类分析,这无形间增加了算法工程师的时间成本; 2). Anchor 增加了检测头的复杂度以及生成结果的数量,将大量检测结果从NPU搬运到CPU上对于某些边缘设备是无法容忍的。当然还有; 3). Anchor Free 的解码代码逻辑更简单,可读性更高。

至于为什么 Anchor Free 现在可以上 YOLO ,并且性能不降反升,这与样本匹配有密不可分的联系。与 Anchor Free 比起来,样本匹配在业界似乎没有什么关注度。但是一个好的样本匹配算法可以天然缓解拥挤场景的检测问题( LLA、OTA 里使用动态样本匹配可以在 CrowdHuman 上提升 FCOS 将近 10 个点),缓解极端长宽比的物体的检测效果差的问题,以及极端大小目标正样本不均衡的问题。甚至可能可以缓解旋转物体检测效果不好的问题,这些问题本质上都是样本匹配的问题。在我们的认知中,样本匹配有 4 个因素十分重要:

1) loss/quality/prediction aware :基于网络自身的预测来计算 anchor box 或者 anchor point 与 gt 的匹配关系,充分考虑到了不同结构/复杂度的模型可能会有不同行为,是一种真正的 dynamic 样本匹配。而 loss aware 后续也被发现对于 DeTR 和 DeFCN 这类端到端检测器至关重要。与之相对的,基于 IoU 阈值 /in Grid(YOLOv1)/in Box or Center(FCOS) 都属于依赖人为定义的几何先验做样本匹配,目前来看都属于次优方案。

2) center prior : 考虑到感受野的问题,以及大部分场景下,目标的质心都与目标的几何中心有一定的联系,将正样本限定在目标中心的一定区域内做 loss/quality aware 样本匹配能很好地解决收敛不稳定的问题。

3) 不同目标设定不同的正样本数量( dynamic k ):我们不可能为同一场景下的西瓜和蚂蚁分配同样的正样本数,如果真是那样,那要么蚂蚁有很多低质量的正样本,要么西瓜仅仅只有一两个正样本。Dynamic k 的关键在于如何确定k,有些方法通过其他方式间接实现了动态 k ,比如 ATSS、PAA ,甚至 RetinaNet ,同时,k的估计依然可以是 prediction aware 的,我们具体的做法是首先计算每个目标最接近的10个预测,然后把这个 10 个预测与 gt 的 iou 加起来求得最终的k,很简单有效,对 10 这个数字也不是很敏感,在 5~15 调整几乎没有影响。

4) 全局信息:有些 anchor box/point 处于正样本之间的交界处、或者正负样本之间的交界处,这类 anchor box/point 的正负划分,甚至若为正,该是谁的正样本,都应充分考虑全局信息。

我们在 CVPR 21 年的工作 OTA 充分考虑到了以上 4 点,通过把样本匹配建模成最优传输问题,求得了全局信息下的最优样本匹配方案,欢迎大家阅读原文。但是 OTA 最大的问题是会增加约 20~25 %的额外训练时间,对于动辄 300epoch 的 COCO 训练来说是有些吃不消的,此外 Sinkhorn-Iter 也会占用大量的显存,所以在 YOLOX 上,我们去掉了 OTA 里的最优方案求解过程,保留上面 4 点的前 3 点,简而言之: loss aware dynamic top k。由于相对 OTA 去掉了Sinkhorn-Iter 求最优解的过程,我们把 YOLOX 采用的样本匹配方案称为 SimOTA ( Simplified OTA )。我们在 Condinst 这类实例分割上用过 SimOTA ,获得了 box 近1个点, seg 0.5 左右的涨点。同时在内部 11 个奇奇怪怪,指标五花八门的业务数据上也测试过 SimOTA ,平均下来发现 SimOTA>FCOS>>ATSS ,这些实验都满足我们不去过拟合 COCO 和 COCO style mAP 的初衷。没有复杂的数学公式和原理,不增加额外的计算时间,但是有效。用今年非常流行的句式为这个小节结个尾: Anchor free and dynamic label assignment are all you need.

End2end

端到端( 无需 NMS )是个很诱人的特性,如果预测结果天然就是一个 set ,岂不是完全不用担心后处理和数据搬运?去年有不少相关的工作放出( DeFCN,PSS, DeTR ),但是在 CNN 上实现端到端通常需要增加2个 Conv 才能让特征变的足够稀疏并实现端到端,且要求检测头有足够的表达能力( Decoupled Head 部分已经详细描述了这个问题),从下表中可以看到,在 YOLOX 上实现 NMS Free 的代价是轻微的掉点和明显的掉 FPS 。所以我们没有在最终版本中使用 End2End 特性。

     

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值