Task 3-损失函数设计

写在前面~
感谢Datawhale的小伙伴用心编写提供了这么好的教程:动手学CV-Pytorch,把之前很难懂的一些问题讲得很明了~
再推荐一下这篇文章:SSD算法详解。由于损失函数的设计采用的是SSD的策略,这篇文章详细的补充了SSD的相关知识。

0. 学习内容及任务描述

光知道模型的结构,以及模型最终会输出什么是不够的,还需要懂得通过合理的设置损失函数和一些相关的训练技巧,让模型向着正确的方向学习,从而预测出我们想要的结果。
因此在task3中,需要学习损失函数设计,对应《动手学CV-Pytorch》3.5节:
损失函数

学习任务:
学习anchor和GT目标框的匹配策略
了解并学习损失函数的定义,并思考这样设计的道理
了解在线难例挖掘的训练技巧

1. Matching Strategy

我们分配了许多prior bboxes,我们要想让其预测类别和目标框信息,我们先要知道每个prior bbox和哪个目标对应,从而才能判断预测的是否准确,从而将训练进行下去。

不同方法 ground truth boxes 与 prior bboxes 的匹配策略大致都是类似的,但是细节会有所不同。这里我们采用SSD中的匹配策略,具体如下:

  • 第一个原则: 从ground truth box出发,寻找与每一个ground truth box有最大的jaccard overlap的prior bbox,这样就能保证每一个groundtruth box一定与一个prior bbox对应起来(jaccard overlap就是IOU)。 反之,若一个prior bbox没有与任何ground truth进行匹配,那么该prior bbox只能与背景匹配,就是负样本。

在这里插入图片描述

  • 缺点:一个图片中ground truth是非常少的,而prior bbox却很多,如果仅按第一个原则匹配,很多prior bbox会是负样本,正负样本极其不平衡,所以需要第二个原则。

  • 第二个原则: 从prior bbox出发,对剩余的还没有配对的prior bbox与任意一个ground truth box尝试配对,只要两者之间的jaccard overlap大于阈值(一般是0.5),那么该prior bbox也与这个ground truth进行匹配。

  • 一个ground truth可能与多个prior box,但是一个prior box 不可以与多个ground truth 相匹配。(一个prior bbox只能匹配一个ground truth,如果多个ground truth与某个prior bbox的 IOU 大于阈值,那么prior bbox只与IOU最大的那个ground truth进行匹配。)

  • 注意:第二个原则一定在第一个原则之后进行【思考】 如果某个ground truth所对应最大IOU的prior bbox小于阈值,并且所匹配的prior bbox却与另外一个ground truth的IOU大于阈值,那么该prior bbox应该匹配谁,答案应该是前者。首先要确保每个ground truth一定有一个prior bbox与之匹配

用一个示例来说明上述的匹配原则:
在这里插入图片描述
图像中有7个红色的框代表先验框,黄色的是ground truths,在这幅图像中有三个真实的目标。按照前面列出的步骤将生成以下匹配项
在这里插入图片描述

2. Loss Function

损失函数是如何设计的捏?
总体的目标损失函数 定义为 置信度损失( L c o n f L_{conf} Lconf定位损失( L l o c L_{loc} Lloc加权和
L ( x ,   c ,   l ,   g ) = 1 N ( L c o n f ( x ,   c ) + α L l o c ( x ,   l ,   g ) ) L(x,\ c,\ l,\ g)=\frac {1} {N}(L_{conf}(x,\ c) +\alpha L_{loc}(x,\ l,\ g) ) L(x, c, l, g)=N1(Lconf(x, c)+αLloc(x, l, g))
其中

  • N : G r o u n d   T r u t h 对 应 的 p r i o r   b b o x 的 数 量 。 若 N = 0 , 则 设 l o s s 为 0 N:Ground\ Truth对应的prior\ bbox的数量。若N=0,则设 loss为0 NGround Truthprior bboxN=0loss0
  • α : 用 于 调 整 L c o n f 和 L l o c 之 间 的 比 例 , 默 认 α = 1 \alpha :用于调整L_{conf}和L_{loc}之间的比例,默认 \alpha =1 αLconfLlocα=1

L c o n f L_{conf} Lconf是在多类别置信度 (C) 上的softmax loss,公式如下:
L c o n f ( x ,   c ) = − ∑ i ∈ P o s N x i j p   l o g ( c ^   i p ) − ∑ i ∈ N e g l o g ( c ^   i 0 ) , w h e r e   c ^   i p = e x p ( c i p ) ∑ p e x p ( c i p ) L_{conf}(x,\ c) =-\displaystyle\sum_{i\in Pos}^{N}{x_{ij}^{p}\ log(\hat{c}\ _{i}^{p} )}-\displaystyle\sum_{i\in Neg}{log(\hat{c}\ _{i}^{0} )} ,\quad where\ \hat{c}\ _{i}^{p}=\frac{exp(c_{i}^{p})}{\sum_{p} exp(c_{i}^{p})} Lconf(x, c)=iPosNxijp log(c^ ip)iNeglog(c^ i0),where c^ ip=pexp(cip)exp(cip)

  • 其中 i i i指代搜索框序号, j j j 指代真实框序号, p p p指代类别序号, p = 0 p=0 p=0表示背景。
  • x i j p = { 1 , 0 } x_{ij}^{p}=\{1,0\} xijp={1,0} 中取 1 表示第 i i i 个prior bbox皮匹配到第 j j j 个GT box,而这个 GT box 的类别为 p p p
  • x i p x_{i}^{p} xip 表示第 i i i 个搜索框对应类别 p p p 的预测概率。
  • 公式前半部分是 正样本(Pos) 的损失,即分类为某个类别的损失(不包括背景)
    后半部分是 负样本(Neg) 的损失,也就是判别类别为背景的损失

L l o c L_{loc} Lloc是典型的 s m o o t h   L 1   l o s s smooth\ L_1\ loss smooth L1 loss,公式如下:
L l o c ( x ,   l ,   g ) = ∑ i ∈ P o s ,   m ∈ c x , c y , w , h N ∑ x i j k s m o o t h L 1 ( l i m − g ^ j m ) g ^ j c x = g j c x − d i c x d i w g ^ j c y = g j c y − d i c y d i h g ^ j w = l o g ( g j w d i w ) g ^ j h = l o g ( g j h d i h ) L_{loc}(x,\ l,\ g)=\displaystyle\sum_{i\in Pos, \ m\in {c_x,c_y,w,h}}^{N}{\sum{x_{ij}^{k}smooth_{L_1}(l_{i}^{m}-\hat{g}_{j}^{m})}} \\ \hat{g}_{j}^{c_x}=\frac{{g}_{j}^{c_x}-{d}_{i}^{c_x}}{d_{i}^{w}}\\ \hat{g}_{j}^{c_y}=\frac{{g}_{j}^{c_y}-{d}_{i}^{c_y}}{d_{i}^{h}} \\ \hat{g}_{j}^{w}=log(\frac{{g}_{j}^{w}}{d_{i}^{w}}) \\ \hat{g}_{j}^{h}=log(\frac{{g}_{j}^{h}}{d_{i}^{h}}) Lloc(x, l, g)=iPos, mcx,cy,w,hNxijksmoothL1(limg^jm)g^jcx=diwgjcxdicxg^jcy=dihgjcydicyg^jw=log(diwgjw)g^jh=log(dihgjh)
s m o o t h L 1 ( x ) = { 0.5 x 2 i f   ∣ x ∣ < 1 ∣ x ∣ − 0.5 o t h e r w i s e smooth_{L_1}(x)=\left\{ \begin{array}{rcl} 0.5x^2 & & {if \ |x| <1}\\ |x|-0.5 & & {otherwise} \end{array} \right. smoothL1(x)={0.5x2x0.5if x<1otherwise

其中, l l l 为预测框, g g g 为ground truth。和Faster R-CNN类似, ( c x ,   c y ) (c_x,\ c_y) (cx, cy) 为要回归到的默认框 d d d 的中心, ( w ,   h ) (w,\ h) (w, h) 为该默认框的宽和高。由于 x i j k x_{ij}^k xijk 的存在,所以位置误差仅针对正样本进行计算。

更详细的解释如下图所示:
在这里插入图片描述

3. Hard Negative Mining

一般情况下 negative prior bboxes数量 >> positive prior bboxes数量,直接训练会导致网络过于重视负样本,预测效果很差。

为了保证正负样本尽量平衡,我们这里使用SSD使用的在线难分样本挖掘策略(hard negative mining),即依据confidience loss对属于负样本的prior bbox进行排序,只挑选其中confidience loss高的bbox进行训练,将正负样本的比例控制在 p o s i t i v e : n e g a t i v e = 1 : 3 positive:negative=1:3 positivenegative=1:3核心作用就是只选择负样本中容易被分错类的困难负样本来进行网络训练,来保证正负样本的平衡和训练的有效性

举个栗子:假设在这 441 个 prior bbox 里,经过匹配后得到正样本先验框 P P P个,负样本先验框 441 − P 441−P 441P个。将负样本prior bbox按照prediction loss从大到小顺序排列后选择最高的 M M M个prior bbox。这个 M M M需要根据我们设定的正负样本的比例确定,比如我们约定正负样本比例为 1 : 3 1:3 1:3时。我们就取 M = 3 P M=3P M=3P,这 M M M个loss最大的负样本难例将会被作为真正参与计算loss的prior bboxes,其余的负样本将不会参与分类损失的loss计算。

3.1 具体过程

正负样本区分:(通过定位信息(iou)初步划分,而后通过loss把hard example留下,loss越大越hard)

3.1.1 正样本获得

已经确定 default box,结合 ground truth,下一步要将 default box 匹配到 ground truth 上。

根据第一节Matching Strategy中的两个原则,从 groud truth box 出发给每个 groud truth box 找到了最近(IOU最大)的 default box 放入候选正样本集。然后再从 default box 出发为 default box 集中寻找与 groundtruth box 满足 I O U > 0.5 IOU>0.5 IOU>0.5 的的default box放入候选正样本集。

3.1.2 负样本获得

在生成 prior boxes 之后,会产生很多个符合 ground truth box 的 positive boxes(候选正样本集),但同时,不符合 ground truth boxes 也很多,而且这个 negative boxes(候选负样本集),远多于 positive boxes。这会造成 negative boxes、positive boxes 之间的不均衡。训练时难以收敛。

3.1.3 难分样本挖掘

将每一个GT上对应prior boxes的分类loss 进行排序。

对于候选正样本集:选择loss最高的 m m m 个 prior box 与候选正样本集匹配 (box 索引同时存在于这两个集合里则匹配成功),匹配不成功则从候选正样本集中删除这个正样本(因为这个正样本loss太低,易被识别,所以不在难例里,已经很接近 ground truth box 了,不需要再训练);

对于候选负样本集:选择loss最高的 m m m 个 prior box 与候选负样本集匹配,匹配成功的则留下来作为最后的负样本,不成功剔除出候选负样本集,因为他们loss不够大,易被识别为背景,训练起来没难度没提升空间。

3.1.4 Hard example mining

Example mining 是选择出特定样本来计算损失函数;从实际问题出发 hard example 应该就是指定位较困难或分类较困难或者两者都困难的候选框。

Hard negative example 或 Hard positive example 的定义需要首先确定某个候选框是 negative example 还是 positive example。比如 SSD 中将与任意 g t b b o x gt_{bbox} gtbbox 的 IOU 超过给定阈值 overlap_threshold(SSD 默认为 0.5)的当作正样本,即前景类别为正样本,背景类别为负样本。比如,极端的例子,当图像中没有 g t b b o x gt_{bbox} gtbbox 时,那么所有的 default bboxes 都是 negative example。

SSD 中 negative mining 只计算分类损失而不计算定位损失,而 hard example mining 对分类损失和定位损失都进行计算

3.1.5 SSD 的 negative mining 的过程为:

1)生成default bboxes,每个检测器对应特征图生成的default boxes数目为nn6或nn4;

2)匹配default boxes,将每个 default box 与 ground truth 匹配,保证每个ground truth 都能对应多个default boxes,避免漏检;

3)衡量default boxes,当第 i i i 个 default box 被匹配到第 j j j g t b b o x gt_{bbox} gtbbox,那么计算其属于背景类别的 softmax loss 或 cross entropy loss 值
S i = e V i ∑ i C e V i S_i=\frac{e^{V_i}}{\sum_{i}^{C}e^{V_i}} Si=iCeVieVi
其中 V i V_i Vi是分类器前级输出单元的输出, i i i表示类别索引,总的类别个数为 C C C S i S_i Si表示的是当前元素的指数与所有元素指数和的比值。Softmax将多分类的输出数值转化为相对概率,更容易理解和比较。

4)筛选default boxes, 计算完所有default boxes 的分类 loss后,按照 loss 排序,选择 loss 较大且与任意 g t b b o x gt_{bbox} gtbbox 之间的 IoU小于 阈值neg_overlap 的样本中的前 3*num_positive 个负样本(保证留下的负样本“够坏”,同时保证1:3的正负比例)。

而后,这正负比为1:3的部分default boxes就是筛选全体default box后剩下的prior boxes,用这些prior box作为参考,对所有预测框其进行分类和回归,进行反向传播更新网络参数,即训练。

4. 小结

本小节介绍的内容围绕如何进行训练展开,主要是3块:

  • 先验框与GT框的匹配策略
  • 损失函数计算
  • 难例挖掘

这3部分是需要结合在一起理解,我们再整个梳理下计算loss的步骤

  1. 先验框与GT框的匹配
    按照我们介绍的方案,为每个先验框都分配好类别,确定是正样本还是负样本。

  2. 计算loss
    按照我们定义的损失函数计算 分类loss目标框回归loss
    负样本不计算目标框的回归loss

  3. 难例挖掘
    上面计算的loss中分类loss的部分还不是最终的loss。因为负样本先验框过多,我们要按一定的预设比例,一般是1:3,将loss最高的那部分负样本先验框拿出来,其余的负样本忽略,重新计算分类loss。

在这里插入图片描述

完整loss计算过程的代码如下。

class MultiBoxLoss(nn.Module):
    """
    The loss function for object detection.
    对于Loss的计算,完全遵循SSD的定义,即 MultiBox Loss

    This is a combination of:
    (1) a localization loss for the predicted locations of the boxes.
    (2) a confidence loss for the predicted class scores.
    """

    def __init__(self, priors_cxcy, threshold=0.5, neg_pos_ratio=3, alpha=1.):
        super(MultiBoxLoss, self).__init__()
        self.priors_cxcy = priors_cxcy
        self.priors_xy = cxcy_to_xy(priors_cxcy)
        self.threshold = threshold
        self.neg_pos_ratio = neg_pos_ratio
        self.alpha = alpha

        self.smooth_l1 = nn.L1Loss()
        self.cross_entropy = nn.CrossEntropyLoss(reduce=False)

    def forward(self, predicted_locs, predicted_scores, boxes, labels):
        """
        Forward propagation.

        :param predicted_locs: predicted locations/boxes w.r.t the 441 prior boxes, a tensor of dimensions (N, 441, 4)
        :param predicted_scores: class scores for each of the encoded locations/boxes, a tensor of dimensions (N, 441, n_classes)
        :param boxes: true  object bounding boxes in boundary coordinates, a list of N tensors
        :param labels: true object labels, a list of N tensors
        :return: multibox loss, a scalar
        """
        batch_size = predicted_locs.size(0)
        n_priors = self.priors_cxcy.size(0)
        n_classes = predicted_scores.size(2)

        assert n_priors == predicted_locs.size(1) == predicted_scores.size(1)

        true_locs = torch.zeros((batch_size, n_priors, 4), dtype=torch.float).to(device)  # (N, 441, 4)
        true_classes = torch.zeros((batch_size, n_priors), dtype=torch.long).to(device)  # (N, 441)

        # For each image
        for i in range(batch_size):
            n_objects = boxes[i].size(0)

            overlap = find_jaccard_overlap(boxes[i], self.priors_xy)  # (n_objects, 441)

            # For each prior, find the object that has the maximum overlap
            overlap_for_each_prior, object_for_each_prior = overlap.max(dim=0)  # (441)

            # We don't want a situation where an object is not represented in our positive (non-background) priors -
            # 1. An object might not be the best object for all priors, and is therefore not in object_for_each_prior.
            # 2. All priors with the object may be assigned as background based on the threshold (0.5).

            # To remedy this -
            # First, find the prior that has the maximum overlap for each object.
            _, prior_for_each_object = overlap.max(dim=1)  # (N_o)

            # Then, assign each object to the corresponding maximum-overlap-prior. (This fixes 1.)
            object_for_each_prior[prior_for_each_object] = torch.LongTensor(range(n_objects)).to(device)

            # To ensure these priors qualify, artificially give them an overlap of greater than 0.5. (This fixes 2.)
            overlap_for_each_prior[prior_for_each_object] = 1.

            # Labels for each prior
            label_for_each_prior = labels[i][object_for_each_prior]  # (441)
            # Set priors whose overlaps with objects are less than the threshold to be background (no object)
            label_for_each_prior[overlap_for_each_prior < self.threshold] = 0  # (441)

            # Store
            true_classes[i] = label_for_each_prior

            # Encode center-size object coordinates into the form we regressed predicted boxes to
            true_locs[i] = cxcy_to_gcxgcy(xy_to_cxcy(boxes[i][object_for_each_prior]), self.priors_cxcy)  # (441, 4)

        # Identify priors that are positive (object/non-background)
        positive_priors = true_classes != 0  # (N, 441)

        # LOCALIZATION LOSS

        # Localization loss is computed only over positive (non-background) priors
        loc_loss = self.smooth_l1(predicted_locs[positive_priors], true_locs[positive_priors])  # (), scalar

        # Note: indexing with a torch.uint8 (byte) tensor flattens the tensor when indexing is across multiple dimensions (N & 441)
        # So, if predicted_locs has the shape (N, 441, 4), predicted_locs[positive_priors] will have (total positives, 4)

        # CONFIDENCE LOSS

        # Confidence loss is computed over positive priors and the most difficult (hardest) negative priors in each image
        # That is, FOR EACH IMAGE,
        # we will take the hardest (neg_pos_ratio * n_positives) negative priors, i.e where there is maximum loss
        # This is called Hard Negative Mining - it concentrates on hardest negatives in each image, and also minimizes pos/neg imbalance

        # Number of positive and hard-negative priors per image
        n_positives = positive_priors.sum(dim=1)  # (N)
        n_hard_negatives = self.neg_pos_ratio * n_positives  # (N)

        # First, find the loss for all priors
        conf_loss_all = self.cross_entropy(predicted_scores.view(-1, n_classes), true_classes.view(-1))  # (N * 441)
        conf_loss_all = conf_loss_all.view(batch_size, n_priors)  # (N, 441)

        # We already know which priors are positive
        conf_loss_pos = conf_loss_all[positive_priors]  # (sum(n_positives))

        # Next, find which priors are hard-negative
        # To do this, sort ONLY negative priors in each image in order of decreasing loss and take top n_hard_negatives
        conf_loss_neg = conf_loss_all.clone()  # (N, 441)
        conf_loss_neg[positive_priors] = 0.  # (N, 441), positive priors are ignored (never in top n_hard_negatives)
        conf_loss_neg, _ = conf_loss_neg.sort(dim=1, descending=True)  # (N, 441), sorted by decreasing hardness
        hardness_ranks = torch.LongTensor(range(n_priors)).unsqueeze(0).expand_as(conf_loss_neg).to(device)  # (N, 441)
        hard_negatives = hardness_ranks < n_hard_negatives.unsqueeze(1)  # (N, 441)
        conf_loss_hard_neg = conf_loss_neg[hard_negatives]  # (sum(n_hard_negatives))

        # As in the paper, averaged over positive priors only, although computed over both positive and hard-negative priors
        conf_loss = (conf_loss_hard_neg.sum() + conf_loss_pos.sum()) / n_positives.sum().float()  # (), scalar

        # return TOTAL LOSS
        return conf_loss + self.alpha * loc_loss

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值