pytorch-ssd源码解读(三)------------multibox_loss(损失函数)

  • pytorch-ssd
  • 本人加的注释版本

一、SSD损失函数

   SSD的损失函数与region proposal方法有个明显的区别,在于SSD并不是针对所有的检测器计算loss。SSD会用一种匹配策略给每个检测器分配一个真实标签(背景为0,前景为对应的物体类别)。指定的标签为前景的预测器称为正样本(正样本全部计算loss)。标签为背景的预测器是负样本,并不是所有的负样本都用来计算loss(原因是每张图片中负样本的数量远远多于正样本,如果全部计算loss,则负样本的loss会主导整个loss)。此时按照预设正负样本比例(论文中为0.3),挑选出一定数量的负样本.

       对于负样本的挑选,论文中称之为"困难样本挖掘",其实就是对负样本按照loss大小排序,选择前n个loss大的负样本进行梯度更新。

   综上,ssd的最终loss就是挑选的正负样本的总loss。

  1.匹配策略

       这部分代码在utils.py的match函数中

      论文中的说法是分为两步:

  • 1是先将真实标签框分配给iou最大的default box,确保每个标签至少有一个default box可以匹配(但是实际上并不能保证,因为可能出现两个物体都和同一个默认框iou最大,此时不可能给一个框匹配两个标签)。
  • 匹配默认框与真实标签iou高于阈值(0.5)的默认框,也就是说只要默认框和哪个ground truth的iou大于0.5,就给这个默认框分配该ground truth(实际上中的做法是如果一个默认框和两个ground truth的iou都大于0.5,则选择iou最大的那个ps:选择先碰到的也可以)

 2. 损失函数

  总体目标损失函数是位置损失(loc)和置信损失(conf)的加权和:

   其中N是匹配的默认框的数量(就是正负样本的数量之和),位置损失是预测框(l)和真实标签值框(g)参数之间的smooth-L1损失。 类似于Faster R-CNN .置信损失是softmax对多类别的损失。α设置为置信损失和位置损失的权重。

pytorch-ssd源码解读(三)------------multibox_loss(损失函数)_第1张图片

   位置损失仅计算正样本的loss(负样本也没得算啊),分别是中心点坐标和长宽的smooth-L1差异。置信度损失对挑选的正负样本都计算。

二、源码解读

1.匹配函数

def match(threshold, truths, priors, variances, labels, loc_t, conf_t, idx):
   

    """
    overlaps Shape为[truths.shape[0],priors.shape[0]],每一行对应truths中的一个框和priors中 
     所有框的iou
    """
    overlaps = jaccard(
        truths,
        point_form(priors)
    )
 
    #每个truth求匹配最好的default_box
    best_prior_overlap, best_prior_idx = overlaps.max(1, keepdim=True)  
    #每个default_box求匹配最好的truth
    best_truth_overlap, best_truth_idx = overlaps.max(0, keepdim=True)   

    best_truth_idx.squeeze_(0)  
    best_truth_overlap.squeeze_(0)
    best_prior_idx.squeeze_(1)
    best_prior_overlap.squeeze_(1)
    
   """
    index_fill_(dim, index, val) → Tensor
    按参数index中的索引数确定的顺序,将原tensor用参数 val值填充。
    确保匹配的框不会因为阈值太低被过滤掉
    best_truth是每一个priors box匹配一个最好的ground truth ,形状为4096
    """
    best_truth_overlap.index_fill_(0, best_prior_idx, 2)  # ensure best prior

    """
    本步骤确保每一个ground truth都能匹配到一个priors box
    下面的代码逻辑上可能有问题,因为best_prior_idx内部可能有重复的数字,这样修改会被后面的数覆盖 
    前面的
    无法解决两个物体匹配到同一个default box的问题
    """
    for j in range(best_prior_idx.size(0)):
        best_truth_idx[best_prior_idx[j]] = j

    matches = truths[best_truth_idx]          # Shape: [num_priors,4]
    conf = labels[best_truth_idx] + 1         # Shape: [num_priors]
    conf[best_truth_overlap < threshold] = 0  # label as background
    loc = encode(matches, priors, variances)
    loc_t[idx] = loc    # [num_priors,4] encoded offsets to learn
    conf_t[idx] = conf  # [num_priors] top class label for each prior

输入参数:

  • threshold 挑选正样本的iou阈值
  • truths 当前图片的object的标记框 Shape: [num_obj, 4]
  • priors default_box的坐标 Shape: [num_priors,4].  每一行内容为[x,y,w,h]
  • variances  与坐标变换有关,暂时不管了
  • labels 当前图片中每个object的类别标签
  • loc_t  匹配后的位置标签(可以理解为函数的输出)
  • conf_t 匹配后的类别标签 (可以理解为函数的输出)
  • idx 当前图片在minibatch中的位置

首先我们看到该函数没有返回值,要理解这并不代表这个函数什么都不做。这是pytorch中Tesnor有一个特性,在子函数中修改Tensor的值,主函数中的Tensor也会跟着变化,因此我们可以把conf_t和loc_t当做该函数的返回值来看,这两个Tensor也就是这个函数的目标。

 jacard函数用来求输入两组box(要求为左上角,右下角的形式)之间的iou。jacard(A,B)的返回值形状为[A.shape[0],B.shape[0]],每一行对应A中的一个框和B中 所有框的iou。

通过对jacard函数返回值在行和列上求最大值可以找到每个truth匹配最好的default_box,和每个default_box匹配最好的truth(有点绕口,但是这两个使用起来是不同的)。

我们最终的目标是给每个default_box匹配一个标签。因此在每个default_box匹配最好的truth的基础上进行修改,将其中truth匹配到最好的default_box项的修改为该truth(而不是按照与该default_box的iou最大的truth)。

然后将iou低于阈值的项设为背景。编码之后得到loc和conf,就是当前图片的最终标签。

2.困难样本挖掘

        # Hard Negative Mining
        """
        先将正样本loss置为0,然后对loss排序(每张图片内部挑选)之后,取前self.negpos_ratio*num_pos个负样本的loss
        """
        loss_c[pos] = 0  # filter out pos boxes for now
        """
        下一步loss_c shape转变为[batch,num_priors]
        下面这种挑选前n个数的操作值得大赞
        """
        loss_c = loss_c.view(num, -1)
        _, loss_idx = loss_c.sort(1, descending=True)
        _, idx_rank = loss_idx.sort(1)
        num_pos = pos.long().sum(1, keepdim=True)
        num_neg = torch.clamp(self.negpos_ratio*num_pos, max=pos.size(1)-1)
        neg = idx_rank < num_neg.expand_as(idx_rank)

        # Confidence Loss Including Positive and Negative Examples
        """
        上面几步的操作就是为获得pos_idx和neg_idx
        conf_data 的shape为[batch,num_priors,num_classes]
        """
        pos_idx = pos.unsqueeze(2).expand_as(conf_data)
        neg_idx = neg.unsqueeze(2).expand_as(conf_data)
        """
        (pos_idx+neg_idx).gt(0)的原因个人猜测可能是因为挑选的正样本和负样本可能会重复,因此将大于1的数变成1.。。。。
        但是经过实验Tensor[mask]中对于mask大于1的数也是可以的

        """
        conf_p = conf_data[(pos_idx+neg_idx).gt(0)].view(-1, self.num_classes)
        targets_weighted = conf_t[(pos+neg).gt(0)]

负样本的选取本质上就是将每个负样本的loss按照从大到小的顺序进行排序之后选择取前n个(负样本是在每个片中选择,而不是minibatch中)。这份代码的两点在于用两个sort排序,在每张图片中挑选出了loss大的前n个样本(难点在于每个图片的你、n并不相同,无法直接排序后用切片操作选取样本)。关于两个sort排序求矩阵的升序或降序元素的位置,请参考我这篇博客。

3.loss计算

首先明确match函数中构建了最终形似的标签,根据网络输出和match构建的最终标签我们已经可以来计算位置损失,直接待用pytorch的smooth_l1_loss()

loss_l = F.smooth_l1_loss(loc_p, loc_t, size_average=False)

然后经过负样本挑选,计算正负样本的置信损失,直接调用pytroch的F.cross_entropy()

loss_c = F.cross_entropy(conf_p, targets_weighted, size_average=False)

整个代码的难点在于正负样本的挑选,以及最终标签的构造。

附:

代码的完整注意请参考我的github

你可能感兴趣的:(pytorch)