美文网首页目标检测
结合源码分析YOLOv3的训练过程(二)

结合源码分析YOLOv3的训练过程(二)

作者: 海盗船长_coco | 来源:发表于2020-02-05 11:57 被阅读0次

    在上篇博客中https://www.jianshu.com/p/2f89e74b9b3c
    介绍了YOLOv3的网络模型及前向传播过程,知道了网络在不同层的feature map进行预测以获得对大、中、小型目标的检测。对于一幅图片最终会预产生10647个anchor box,得到维度为[1,10647,85]的输出。但是并没有说明YOLOv3的损失函数及训练过程。在论文中也没有给出损失函数的公式,需要通过源码去分析损失函数及训练过程。下图为根据源码得到的损失函数。

    YOLOv3的损失函数

    由于上篇博客中给出的代码没有包含训练的部分,所以给出YOLOv3含训练的pytorch实现:https://github.com/eriklindernoren/PyTorch-YOLOv3

    YOLOLayer检测层

    YOLOLayer负责在13x13,26x26,52x52这三层feature map上进行预测,可以看到其forward前向传播的代码。若为测试集则直接返回预测,否则返回该层的预测和损失值。其中重要的方法为build_targets将在下面讲解。

            if targets is None:  # 测试集则直接返回预测
                return output, 0
            else:  #训练集返回预测和损失
                iou_scores, class_mask, obj_mask, noobj_mask, tx, ty, tw, th, tcls, tconf = build_targets(
                    pred_boxes=pred_boxes,
                    pred_cls=pred_cls,
                    target=targets,
                    anchors=self.scaled_anchors,
                    ignore_thres=self.ignore_thres,
                )
    
                # Loss : Mask outputs to ignore non-existing objects (except with conf. loss)
                loss_x = self.mse_loss(x[obj_mask], tx[obj_mask])
                loss_y = self.mse_loss(y[obj_mask], ty[obj_mask])
                loss_w = self.mse_loss(w[obj_mask], tw[obj_mask])
                loss_h = self.mse_loss(h[obj_mask], th[obj_mask])
                loss_conf_obj = self.bce_loss(pred_conf[obj_mask], tconf[obj_mask])
                loss_conf_noobj = self.bce_loss(pred_conf[noobj_mask], tconf[noobj_mask])
                loss_conf = self.obj_scale * loss_conf_obj + self.noobj_scale * loss_conf_noobj
                loss_cls = self.bce_loss(pred_cls[obj_mask], tcls[obj_mask])
                total_loss = loss_x + loss_y + loss_w + loss_h + loss_conf + loss_cls
                return output, total_loss
    

    以在13x13的feature map上计算为例。预测bounding box的pred_boxes维度为[1,3,13,13,4],预测类别的pred_cls维度为[1,3,13,13,80],假设图片中有两个检测目标,target维度为[2,6],这6维分别表示[img_index,cls,x1,y1,x2,y2],第一个数表示读入图片的编号,anchors为该层anchor box的大小,由于每层有3个不同比例大小的anchor box,所以anchors维度为[3,2],ignore_thres为阈值,设定为0.5。

    区分是否含有object的anchor box

    def build_targets(pred_boxes, pred_cls, target, anchors, ignore_thres):
        ByteTensor = torch.cuda.ByteTensor if pred_boxes.is_cuda else torch.ByteTensor
        FloatTensor = torch.cuda.FloatTensor if pred_boxes.is_cuda else torch.FloatTensor
    
        nB = pred_boxes.size(0)  # 样本数量
        nA = pred_boxes.size(1)  # 通道数
        nC = pred_cls.size(-1)  # 预测类别数量
        nG = pred_boxes.size(2)  # 单元格尺寸(13x13,26x26,52x52)
    
        # Output tensors
        obj_mask = ByteTensor(nB, nA, nG, nG).fill_(0)
        noobj_mask = ByteTensor(nB, nA, nG, nG).fill_(1)
        class_mask = FloatTensor(nB, nA, nG, nG).fill_(0)
        iou_scores = FloatTensor(nB, nA, nG, nG).fill_(0)
        tx = FloatTensor(nB, nA, nG, nG).fill_(0)
        ty = FloatTensor(nB, nA, nG, nG).fill_(0)
        tw = FloatTensor(nB, nA, nG, nG).fill_(0)
        th = FloatTensor(nB, nA, nG, nG).fill_(0)
        tcls = FloatTensor(nB, nA, nG, nG, nC).fill_(0)
    
        # Convert to position relative to box
        target_boxes = target[:, 2:6] * nG
        gxy = target_boxes[:, :2]
        gwh = target_boxes[:, 2:]
        # Get anchors with best iou
        ious = torch.stack([bbox_wh_iou(anchor, gwh) for anchor in anchors])
        best_ious, best_n = ious.max(0)
        # Separate target values
        b, target_labels = target[:, :2].long().t()
        gx, gy = gxy.t()
        gw, gh = gwh.t()
        gi, gj = gxy.long().t()
        # Set masks
        obj_mask[b, best_n, gj, gi] = 1
        noobj_mask[b, best_n, gj, gi] = 0
    
        # Set noobj mask to zero where iou exceeds ignore threshold
        for i, anchor_ious in enumerate(ious.t()):
            noobj_mask[b[i], anchor_ious > ignore_thres, gj[i], gi[i]] = 0
    
        # Coordinates
        tx[b, best_n, gj, gi] = gx - gx.floor()
        ty[b, best_n, gj, gi] = gy - gy.floor()
        # Width and height
        tw[b, best_n, gj, gi] = torch.log(gw / anchors[best_n][:, 0] + 1e-16)
        th[b, best_n, gj, gi] = torch.log(gh / anchors[best_n][:, 1] + 1e-16)
        # One-hot encoding of label
        tcls[b, best_n, gj, gi, target_labels] = 1
        # Compute label correctness and iou at best anchor
        class_mask[b, best_n, gj, gi] = (pred_cls[b, best_n, gj, gi].argmax(-1) == target_labels).float()
        iou_scores[b, best_n, gj, gi] = bbox_iou(pred_boxes[b, best_n, gj, gi], target_boxes, x1y1x2y2=False)
    
        tconf = obj_mask.float()
        return iou_scores, class_mask, obj_mask, noobj_mask, tx, ty, tw, th, tcls, tconf
    

    一、将真实框target的大小映射到13x13的feature map上,获得对应的坐标和大小(Convert to position relative to box)。


    target在feature map上的位置和大小

    二、由于对每层featre map都有3个不同比例大小的anchor box,所以我们要选择与target形状最接近的anchor box,评判指标就是IOU值。下图中最接近的target就是anchor 2了(Get anchors with best iou)。


    选择最佳的anchor box
    三、找到包含target的单元格,因为target的中心点坐标xy通常不是整数,所以通过取整找到对应的单元格,例如[6.2502, 6.4846],对应的单元格为[6,6],且每个单元格有3个anchor box,将第二步中找到的最佳anchor box设置为obj_mask,表示其含有目标值。将下图中的obj_mask[0,1,6,6]设置为1(Set masks)。
    找到对应的单元格并找到最佳的anchor box

    四、对应单元格最佳anchor box设置坐标和宽高的训练目标值。对于坐标值的计算,代码中直接将下图中bx-cx作为tx的回归目标值。( Coordinates,Width and height)。


    转化公式
    五、对应单元格最佳anchor box的label进行one-hot编码,将其cls类别标为1。
    六、计算该位置预测的类别pred_cls是否与真实类别target_labels相同,以及预测框pred_box与真实框target_boxes的IOU值。(Compute label correctness and iou at best anchor),该结果主要用于之后的模型评估阶段,与loss计算没有关系。

    计算总损失

    在13x13大小的feature map上执行完build_targets方法,我们可以从13x13x3个anchor box中找出包含object且与target box最匹配(两者IOU值最高)的anchor box,其余的anchor box为no object。
    对于含有object的anchor box计算其与target的坐标xy,宽高kw的损失值,损失函数为均方误差。

    loss_x = self.mse_loss(x[obj_mask], tx[obj_mask])
    loss_y = self.mse_loss(y[obj_mask], ty[obj_mask])
    loss_w = self.mse_loss(w[obj_mask], tw[obj_mask])
    loss_h = self.mse_loss(h[obj_mask], th[obj_mask])
    

    使用交叉熵函数计算其与target的含有目标的置信度的损失值。对于真实情况tconf只有0,1两种值,因为真实情况只有存在目标和不存在目标两种情况,而pred_conf是一个存在目标的概率值。

    loss_conf_obj = self.bce_loss(pred_conf[obj_mask], tconf[obj_mask])
    

    同样使用交叉熵函数计算与target各个类别置信度的损失值。对于target来说,其class为one-hot编码,因为真实情况下,object只属于80种类别中的一种。

    loss_cls = self.bce_loss(pred_cls[obj_mask], tcls[obj_mask])
    

    对于不含object的anchor box只需计算与target的含有目标的置信度的损失值。

    loss_conf_noobj = self.bce_loss(pred_conf[noobj_mask], tconf[noobj_mask])
    

    对于目标置信度的损失,含object的anchor box与不含object的anchor box所占权重不同。代码中将object_scale设为1,noobject_scale设为100。通过不断下降loss,使那些不含object而认为自己含有object的anchor(表现为pred_conf预测值很大)数量大大减少。

    loss_conf = self.obj_scale * loss_conf_obj + self.noobj_scale * loss_conf_noobj
    

    单层featrue map的总损失为将上述各损失值相加。

    total_loss = loss_x + loss_y + loss_w + loss_h + loss_conf + loss_cls
    

    最终网络的总损失是将各层的feature map相加得到loss,最后通过优化函数不断减少loss来训练网络,得到最终的模型参数。
    参考博客:https://blog.csdn.net/qq_34795071/article/details/92803741

    相关文章

      网友评论

        本文标题:结合源码分析YOLOv3的训练过程(二)

        本文链接:https://www.haomeiwen.com/subject/lhmjxhtx.html