yolov3源码分析

  • Post author:
  • Post category:其他


代码主要分四个部分

  1. 数据读取与处理
  2. 网络代码
  3. loss计算
  4. 预测解码

    以下主要对上面4个部分的关键代码进行详解,同时记录自己当前的一些疑问


    1. 数据读取与处理


    后面再补


    2. 网络代码


    后面再补


    3. loss计算


    本文代码主要简介pytorch版本的yolov3,和源码及ultralytics版本都有所出入,主要去思考其中的细节。

    本节主要从三个函数进行讲解

    1. loss主函数 forward:loss计算过程

      先贴一张loss公式图

      在这里插入图片描述

      yolov3的loss分为三个组要部分:定位损失,置信度损失,类别损失。

      由图中可知边框损失使用mse loss, (2-w * h) 是为了平衡大框和小框带来的影响,yolov1使用宽高的开方进行平衡,类别损失使用二分类交叉熵损失,解除了类别之间的互斥性。置信度仍然使用mse loss计算
     # l代表特征尺度的索引
    
 bs = input.size(0)
 in_h = input.size(2)
 in_w = input.size(3)

 #计算当前特征层的步长
 stride_h = self.input_shape[0] / in_h
 stride_w = self.input_shape[1] / in_w

 #将anchor缩放到当前特征图尺寸
 scales_anchors = [(a_w / stride_w, a_h / stride_h)  for a_w, a_h in self.anchors]

 prediction = input.view(bs,len(self.anchors_mask[l]), self.bbox_attrs,  in_h, in_w).premute(0,1,3,4,2).contiguous()

 #获取预测结果,其中x,y,conf,class_conf需要归一化
 x = torch.sigmoid(prediction[...,0])
 y = torch.sigmoid(prediction[...,1])
 w = prediction[...,2]
 h = prediction[...,3]
 conf = torch.sigmoid(prediction[...,4])
 pred_cls = torch.sigmoid(prediction[...,5:])

 #样本匹配获取mask
 y_true, noobj_mask, box_loss_scale = self.get_target(l, targets, scales_anchors, in_h, in_w)

 #yolov3中存在忽略样本,忽略样本不计算loss,获得其相应mask
 noobj_mask,pred_boxes = self.get_ignore(l, x, y, h, w, targets, scales_anchors, in_h, in_w, noobj_mask)


 if self.cuda:
     y_true = y_true.type_as(x)
     noobj_mask = noobj_mask.type_as(x)
     box_loss_scale = box_loss_scale.type_as(x)

 #yolov3解决大小样本对loss贡献不一致问题的方式,不理解为什么用2- 不用1-
 box_loss_scale = 2 - box_loss_scale

 loss = 0
 obj_mask = y_true[...,4] ==1

 n = torch.sum(obj_mask)
 if n != 0:
 	#pytorch版本中加入了giou进行样本匹配,同时在部分代码中使用anchor和ground truth的
 	高宽比进行正负样本选择,应该是借鉴的yolov5.
     if self.giou:
         giou = self.box_giou(pred_boxes, y_true[...,:4]).type_as(x)
         loss_loc = torch.mean(1-giou)[obj_mask]
     else:
#代码实现中x,y的loss使用的交叉熵进行计算
         loss_x = torch.mean(self.BCELoss(x[obj_mask], y_true[..., 0][obj_mask]) * box_loss_scale[obj_mask])
         loss_y = torch.mean(self.BCELoss(y[obj_mask], y_true[..., 1][obj_mask]) * box_loss_scale[obj_mask])
         loss_w = torch.mean(self.MSELoss(w[obj_mask], y_true[..., 2][obj_mask]) * box_loss_scale[obj_mask])
         loss_h = torch.mean(self.MSELoss(h[obj_mask], y_true[..., 3][obj_mask]) * box_loss_scale[obj_mask])

         # box损失权重乘上0.1   暂时不清楚为什么使用0.1
         loss_loc = (loss_x + loss_y + loss_h + loss_w) * 0.1

     loss_cls = torch.mean(self.BCELoss(pred_cls[obj_mask], y_true[..., 5:][obj_mask]))

     #定位需要经过两次权重?
     loss += loss_loc * self.box_ratio + loss_cls * self.cls_ratio
            #这里就有一个问题了,忽略样本怎么控制
loss_conf = torch.mean(self.BCELoss(conf, obj_mask.type_as(conf)[noobj_mask.bool() |obj_mask]))

   #使用balance控制三个特征层对置信度loss的权重,[0.4,1,4]为超参数搜索得到,实际项目中可自行调整
loss += loss_conf * self.balance[l] * self.obj_ratio

从代码中可以看出,中心点x,y使用bce loss,宽高使用MSE loss,同时计算loss时使用了obj_mask,(含义参考get_target()) 即挑选的正样本产生边框损失,分类损失使用BCE loss,仍然使用obj_mask。置信度损失使用BCE loss,这里使用obj_mask作为target,表明正样本的置信度为1, 负样本的置信度为0,同时将noobj_mask和obj_mask做或运算,因为在noobj_mask,负样本的mask为1,忽略样本的mask为0,因此进行或运算后,负样本和正样本的mask均为1,忽略样本的mask为0,正好符合了忽略样本不参与loss计算的原则。

3. get_target():正负样本匹配

batch_target = torch.zeros_like(targets[b])
		
#x,y的坐标相对于对特征图的大小
batch_target[:, [0, 2]] = targets[b][:, [0, 2]] * in_w
batch_target[:, [1, 3]] = targets[b][:, [1, 3]] * in_h
batch_target[:, 4] = targets[b][:, 4]
batch_target = batch_target.cpu()

# gt_box (num_true_box, 4)
gt_box = torch.FloatTensor(torch.cat((torch.zeros((batch_target.size(0),2)), batch_target[:, 2:4]), 1 ))

# anchor_shape (9,4)
anchor_shapes = torch.FloatTensor(torch.cat((torch.zeros(len(anchors), 2), torch.FloatTensor(anchors)) ,1 ))

# 计算每个真实框和9个anchor之间的iou,并获得最大iou的索引
#(num_true_box, 9)--> (num_true_box, 1)
best_ns = torch.argmax(self.calculate_iou(gt_box, anchor_shapes), dim=-1)

首先将ground_truth的坐标映射成特征图大小,然后计算anchor和ground_truth的iou,这里由于anchor只有宽高,因此在gt_box和anchor_shape前面将x,y用0进行初始化。因此大家在很多博客都可以看到 “计算iou时,不考虑坐标只考虑宽高”的说法,因为此时计算iou用的时ground_truth和anchor。

 for t, best_n in enumerate(best_ns):
     if best_n not in self.anchors_mask[l]:
          continue

      #判断由哪一个anchor预测当前的ground_true
      k = self.anchors_mask[l].index(best_n)

      i = torch.floor(batch_target[t, 0]).long()
      j = torch.floor(batch_target[t, 1]).long()

      c = batch_target[t, 4].long()

      noobj_mask[b, k, j, i] = 0
      if not self.giou:

          y_true[b, k, j, i, 0] = batch_target[t, 0] - i.float()
          y_true[b, k, j, i, 1] = batch_target[t, 1] - j.float()
          y_true[b, k, j, i, 2] = torch.log(batch_target[t, 2] / anchors[best_n][0])
          y_true[b, k, j, i, 3] = torch.log(batch_target[t, 3] / anchors[best_n][1])
          y_true[b, k, j, i, 4] = 1
          y_true[b, k, j, i, c + 5] = 1

      else:

          #giou为什么不一样
          y_true[b, k, j, i, 0] = batch_target[t, 0]
          y_true[b, k, j, i, 1] = batch_target[t, 1]
          y_true[b, k, j, i, 2] = batch_target[t, 2]
          y_true[b, k, j, i, 3] = batch_target[t, 3]
          y_true[b, k, j, i, 4] = 1
          y_true[b, k, j, i, c + 5] = 1

      box_loss_scale[b, k, j, i] = batch_target[t, 2] * batch_target[t, 3] / in_h / in_w

获得与ground_truth最大iou的anchor之后,这里获得了anchor的索引k,将对应noobj_mask设置为0,同时真实框减去grid的坐标,宽高进行反解码,映射到特征层大小,置信度置为1,对应的class_id置为1,当使用giou loss进行计算时,是直接计算预测框和ground_truth的iou 因此不需要映射到特征层,直接保存对应结果就行,同时将计算box_loss_scale并归一化。

5. get_ignore():忽略样本计算


  1. 预测解码


    后面再补“



版权声明:本文为m0_43421420原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。