赞
踩
Hello,大家好我是Snu77,这里给大家带来的是YOLOv9系列的第一篇改进策略,本文的内容是用了YOLOv9官方集成的MPDIoU损失来给大家进行改进 | MPDIoU需要图片的真实大小,但是官方并没有传递这个参数,在训练的阶段还好因为YOLO系列是固定图片大小训练,但是在训练时的验证确是不固定的所以我们需要将图片的真实大小传递进去但是官方并没有做这个功能,但是它预留了两个参数位可以看出我们这么做是没有任何问题的。
PS:最近群里很多读者都在催更YOLOv9系列(不下于二十人),这里再次讲一下没有更新的原因,一是因为YOLOv9的论文还没有收录,二是目前官方的项目中Bug众多,其次最重要的就是YOLOv9并没有放出其轻量化的版本(这对于众多算力有限的读者是一个很严重的问题),在一个以前的许多机制放在YOLOv9上的效果可能并不好因为V9的各方面比较完美。所以我们的改进策略专栏会从损失函数切入,随着今年更多的定会机制推出我们会将更多真实有效的改进放在YOLOv9上,助力大家发表论文。
目录
问题提出:文章指出,在目标检测和实例分割的过程中,传统的边界框回归(BBR)损失函数难以优化预测框和真实框在宽高比相同但具体尺寸不同时的情况,下面是描述现有的边界框回归的方法的计算因素总结(包括GIoU、DIoU、CIoU和EIoU)的计算因素。这些度量方法是用于评估和优化边界框回归模型性能的关键工具。虽然文章没有直接展示下图的内容,但它们包括以下几个方面:
GIoU(Generalized IoU):除了传统的IoU(交并比)之外,GIoU还考虑了边界框之间的包含关系和空间分布。
DIoU(Distance IoU):在IoU的基础上,DIoU还考虑了边界框中心点之间的距离,以改进对齐和尺度不一致的情况。
CIoU(Complete IoU):结合了DIoU的特点,并加入了宽高比的考虑,进一步提高了对边界框的精确度。
EIoU(Expected IoU):这是一种更高级的度量方法,考虑了预测边界框与真实边界框之间的预期相似度。
文章提出的MPDIoU是在这些现有度量方法的基础上发展起来的,旨在通过直接最小化预测框和真实框之间的关键点距离,提供一种易于实现的解决方案,用于计算两个轴对齐矩形之间的MPDIoU
MPDIoU的提出:为了克服这一挑战,文章提出了一种新的边界框相似度度量方法——MPDIoU(Minimum Point Distance Intersection over Union)。MPDIoU是基于水平矩形的最小点距离来计算的,能够综合考虑重叠区域、中心点距离以及宽度和高度的偏差。
下图展示了两种不同的边界框回归结果情况。其中,绿色框代表真实的边界框,而红色框代表预测的边界框。在这两种情况下,传统的损失函数(如GIoU、DIoU、CIoU和EIoU)计算出的损失值是相同的,但是使用MPDIoU方法计算出的损失值却有所不同。这说明传统方法在某些特定情况下可能无法区分不同的预测结果,而MPDIoU能更准确地反映预测框和真实框之间的差异。
这个发现突显了MPDIoU在处理边界框回归问题上的优势,尤其是在区分具有相同宽高比但不同尺寸或位置的边界框时。MPDIoU通过直接计算预测框和真实框之间的关键点距离,提供了更精确的损失度量方法。
这些因素包括如何在训练阶段通过最小化损失函数来使模型预测的边界框接近其真实边界框。具体来说,每个预测的边界框
通过最小化以下损失函数来逼近其真实边界框:
其中, 是真实边界框的集合,而 是回归深度模型的参数。文章中提出的损失函数公式为:
实验验证:通过在多个数据集(如PASCAL VOC、MS COCO和IIIT5k)上对YOLACT和YOLOv7等模型的训练和测试,文章验证了MPDIoU和LMPDIoU在实际应用中的有效性。实验结果显示,这种新的损失函数在多个方面优于传统的损失函数,尤其是在处理具有相似宽高比但不同尺寸的边界框时。
下面是一些检测效果对比图
总结:文章通过引入MPDIoU,提供了一种新的视角来优化目标检测中的边界框回归问题,同时通过实验验证了其在提高检测模型准确性方面的有效性。
PS:损失函数都是各种公式,数学推导这方面我能力确实不太行,大家有兴趣的可以自行推到看看。
这里还有一个问题,我们这里改进的是以YOLOv9的代码DualDetect版本进行修改,如果你用的是GELAN版本的那么修改教程都是一样的。
我们第一步需要找到一个文件'utils/loss_tal_dual.py',我们找到这个文件之后,按照找到如下图所示的代码。
下面的样子是修改之后的,大家用我下面给的代码替换红框内的代码即可。
- # bbox loss
- if fg_mask.sum():
- loss[0], loss[2], iou = self.bbox_loss(pred_distri,
- pred_bboxes,
- anchor_points,
- target_bboxes,
- target_scores,
- target_scores_sum,
- fg_mask, imgsz[0], imgsz[1])
- loss[0] *= 0.25
- loss[2] *= 0.25
- if fg_mask2.sum():
- loss0_, loss2_, iou2 = self.bbox_loss2(pred_distri2,
- pred_bboxes2,
- anchor_points,
- target_bboxes2,
- target_scores2,
- target_scores_sum2,
- fg_mask2, imgsz[0], imgsz[1])
下面的图片为修改之后的样子,大家可以看好了,不要修改错了!!
第二步我们还是这个文件'utils/loss_tal_dual.py'并没有离开这个文件请注意!我们需要网上翻页面,找到图片所示的代码,下面的代码是未作修改的!
我们将下面我给的代码替换掉整个红框内的代码即可!
- def forward(self, pred_dist, pred_bboxes, anchor_points, target_bboxes, target_scores, target_scores_sum, fg_mask, h, w):
- # iou loss
- bbox_mask = fg_mask.unsqueeze(-1).repeat([1, 1, 4]) # (b, h*w, 4)
- pred_bboxes_pos = torch.masked_select(pred_bboxes, bbox_mask).view(-1, 4)
- target_bboxes_pos = torch.masked_select(target_bboxes, bbox_mask).view(-1, 4)
- bbox_weight = torch.masked_select(target_scores.sum(-1), fg_mask).unsqueeze(-1)
-
- iou = bbox_iou(pred_bboxes_pos, target_bboxes_pos, xywh=False, CIoU=False, MDPIoU=True, feat_h=h, feat_w=w)
- loss_iou = 1.0 - iou
-
- loss_iou *= bbox_weight
- loss_iou = loss_iou.sum() / target_scores_sum
-
- # dfl loss
- if self.use_dfl:
- dist_mask = fg_mask.unsqueeze(-1).repeat([1, 1, (self.reg_max + 1) * 4])
- pred_dist_pos = torch.masked_select(pred_dist, dist_mask).view(-1, 4, self.reg_max + 1)
- target_ltrb = bbox2dist(anchor_points, target_bboxes, self.reg_max)
- target_ltrb_pos = torch.masked_select(target_ltrb, bbox_mask).view(-1, 4)
- loss_dfl = self._df_loss(pred_dist_pos, target_ltrb_pos) * bbox_weight
- loss_dfl = loss_dfl.sum() / target_scores_sum
- else:
- loss_dfl = torch.tensor(0.0).to(pred_dist.device)
-
- return loss_iou, loss_dfl, iou
下面的图片未修改之后的样子,图片中我也标记了我们修改了何处的地方!
到此就修改完成了,我们将CioU设置为False,将MPDIoU设置为True,将真是边界框大小传入给损失函数即可(官方自带集成了MPDIoU所以我们不需要修改损失函数的内部)我们之后运行自己的yaml文件运行的就是MPDIoU了!
针对于运行GELAN.yaml文件的同学,修改教程其实和上面一样,只是步骤一的时候大家是修改一处即可,这里我提供‘utils/loss_tal.py’文件的修改完成版本给该群体的用于大家完整替换即可!
此处请注意,此处是'utils/loss_tal.py'的损失函数修改代码,和上面的教程不是一个!!
我们将下面的代码完全替换 utils/loss_tal.py文件的代码之后运行的就是MPDIoU了!
- import os
-
- import torch
- import torch.nn as nn
- import torch.nn.functional as F
-
- from utils.general import xywh2xyxy
- from utils.metrics import bbox_iou
- from utils.tal.anchor_generator import dist2bbox, make_anchors, bbox2dist
- from utils.tal.assigner import TaskAlignedAssigner
- from utils.torch_utils import de_parallel
-
-
- def smooth_BCE(eps=0.1): # https://github.com/ultralytics/yolov3/issues/238#issuecomment-598028441
- # return positive, negative label smoothing BCE targets
- return 1.0 - 0.5 * eps, 0.5 * eps
-
-
- class VarifocalLoss(nn.Module):
- # Varifocal loss by Zhang et al. https://arxiv.org/abs/2008.13367
- def __init__(self):
- super().__init__()
-
- def forward(self, pred_score, gt_score, label, alpha=0.75, gamma=2.0):
- weight = alpha * pred_score.sigmoid().pow(gamma) * (1 - label) + gt_score * label
- with torch.cuda.amp.autocast(enabled=False):
- loss = (F.binary_cross_entropy_with_logits(pred_score.float(), gt_score.float(),
- reduction="none") * weight).sum()
- return loss
-
-
- class FocalLoss(nn.Module):
- # Wraps focal loss around existing loss_fcn(), i.e. criteria = FocalLoss(nn.BCEWithLogitsLoss(), gamma=1.5)
- def __init__(self, loss_fcn, gamma=1.5, alpha=0.25):
- super().__init__()
- self.loss_fcn = loss_fcn # must be nn.BCEWithLogitsLoss()
- self.gamma = gamma
- self.alpha = alpha
- self.reduction = loss_fcn.reduction
- self.loss_fcn.reduction = "none" # required to apply FL to each element
-
- def forward(self, pred, true):
- loss = self.loss_fcn(pred, true)
- # p_t = torch.exp(-loss)
- # loss *= self.alpha * (1.000001 - p_t) ** self.gamma # non-zero power for gradient stability
-
- # TF implementation https://github.com/tensorflow/addons/blob/v0.7.1/tensorflow_addons/losses/focal_loss.py
- pred_prob = torch.sigmoid(pred) # prob from logits
- p_t = true * pred_prob + (1 - true) * (1 - pred_prob)
- alpha_factor = true * self.alpha + (1 - true) * (1 - self.alpha)
- modulating_factor = (1.0 - p_t) ** self.gamma
- loss *= alpha_factor * modulating_factor
-
- if self.reduction == "mean":
- return loss.mean()
- elif self.reduction == "sum":
- return loss.sum()
- else: # 'none'
- return loss
-
-
- class BboxLoss(nn.Module):
- def __init__(self, reg_max, use_dfl=False):
- super().__init__()
- self.reg_max = reg_max
- self.use_dfl = use_dfl
-
- def forward(self, pred_dist, pred_bboxes, anchor_points, target_bboxes, target_scores, target_scores_sum, fg_mask, h, w):
- # iou loss
- bbox_mask = fg_mask.unsqueeze(-1).repeat([1, 1, 4]) # (b, h*w, 4)
- pred_bboxes_pos = torch.masked_select(pred_bboxes, bbox_mask).view(-1, 4)
- target_bboxes_pos = torch.masked_select(target_bboxes, bbox_mask).view(-1, 4)
- bbox_weight = torch.masked_select(target_scores.sum(-1), fg_mask).unsqueeze(-1)
-
- iou = bbox_iou(pred_bboxes_pos, target_bboxes_pos, xywh=False, CIoU=False, MDPIoU=True, feat_h=h, feat_w=w)
- loss_iou = 1.0 - iou
-
- loss_iou *= bbox_weight
- loss_iou = loss_iou.sum() / target_scores_sum
-
- # dfl loss
- if self.use_dfl:
- dist_mask = fg_mask.unsqueeze(-1).repeat([1, 1, (self.reg_max + 1) * 4])
- pred_dist_pos = torch.masked_select(pred_dist, dist_mask).view(-1, 4, self.reg_max + 1)
- target_ltrb = bbox2dist(anchor_points, target_bboxes, self.reg_max)
- target_ltrb_pos = torch.masked_select(target_ltrb, bbox_mask).view(-1, 4)
- loss_dfl = self._df_loss(pred_dist_pos, target_ltrb_pos) * bbox_weight
- loss_dfl = loss_dfl.sum() / target_scores_sum
- else:
- loss_dfl = torch.tensor(0.0).to(pred_dist.device)
-
- return loss_iou, loss_dfl, iou
-
- def _df_loss(self, pred_dist, target):
- target_left = target.to(torch.long)
- target_right = target_left + 1
- weight_left = target_right.to(torch.float) - target
- weight_right = 1 - weight_left
- loss_left = F.cross_entropy(pred_dist.view(-1, self.reg_max + 1), target_left.view(-1), reduction="none").view(
- target_left.shape) * weight_left
- loss_right = F.cross_entropy(pred_dist.view(-1, self.reg_max + 1), target_right.view(-1),
- reduction="none").view(target_left.shape) * weight_right
- return (loss_left + loss_right).mean(-1, keepdim=True)
-
-
- class ComputeLoss:
- # Compute losses
- def __init__(self, model, use_dfl=True):
- device = next(model.parameters()).device # get model device
- h = model.hyp # hyperparameters
-
- # Define criteria
- BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h["cls_pw"]], device=device), reduction='none')
-
- # Class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3
- self.cp, self.cn = smooth_BCE(eps=h.get("label_smoothing", 0.0)) # positive, negative BCE targets
-
- # Focal loss
- g = h["fl_gamma"] # focal loss gamma
- if g > 0:
- BCEcls = FocalLoss(BCEcls, g)
-
- m = de_parallel(model).model[-1] # Detect() module
- self.balance = {3: [4.0, 1.0, 0.4]}.get(m.nl, [4.0, 1.0, 0.25, 0.06, 0.02]) # P3-P7
- self.BCEcls = BCEcls
- self.hyp = h
- self.stride = m.stride # model strides
- self.nc = m.nc # number of classes
- self.nl = m.nl # number of layers
- self.no = m.no
- self.reg_max = m.reg_max
- self.device = device
-
- self.assigner = TaskAlignedAssigner(topk=int(os.getenv('YOLOM', 10)),
- num_classes=self.nc,
- alpha=float(os.getenv('YOLOA', 0.5)),
- beta=float(os.getenv('YOLOB', 6.0)))
- self.bbox_loss = BboxLoss(m.reg_max - 1, use_dfl=use_dfl).to(device)
- self.proj = torch.arange(m.reg_max).float().to(device) # / 120.0
- self.use_dfl = use_dfl
-
- def preprocess(self, targets, batch_size, scale_tensor):
- if targets.shape[0] == 0:
- out = torch.zeros(batch_size, 0, 5, device=self.device)
- else:
- i = targets[:, 0] # image index
- _, counts = i.unique(return_counts=True)
- out = torch.zeros(batch_size, counts.max(), 5, device=self.device)
- for j in range(batch_size):
- matches = i == j
- n = matches.sum()
- if n:
- out[j, :n] = targets[matches, 1:]
- out[..., 1:5] = xywh2xyxy(out[..., 1:5].mul_(scale_tensor))
- return out
-
- def bbox_decode(self, anchor_points, pred_dist):
- if self.use_dfl:
- b, a, c = pred_dist.shape # batch, anchors, channels
- pred_dist = pred_dist.view(b, a, 4, c // 4).softmax(3).matmul(self.proj.type(pred_dist.dtype))
- # pred_dist = pred_dist.view(b, a, c // 4, 4).transpose(2,3).softmax(3).matmul(self.proj.type(pred_dist.dtype))
- # pred_dist = (pred_dist.view(b, a, c // 4, 4).softmax(2) * self.proj.type(pred_dist.dtype).view(1, 1, -1, 1)).sum(2)
- return dist2bbox(pred_dist, anchor_points, xywh=False)
-
- def __call__(self, p, targets, img=None, epoch=0):
- loss = torch.zeros(3, device=self.device) # box, cls, dfl
- feats = p[1] if isinstance(p, tuple) else p
- pred_distri, pred_scores = torch.cat([xi.view(feats[0].shape[0], self.no, -1) for xi in feats], 2).split(
- (self.reg_max * 4, self.nc), 1)
- pred_scores = pred_scores.permute(0, 2, 1).contiguous()
- pred_distri = pred_distri.permute(0, 2, 1).contiguous()
-
- dtype = pred_scores.dtype
- batch_size, grid_size = pred_scores.shape[:2]
- imgsz = torch.tensor(feats[0].shape[2:], device=self.device, dtype=dtype) * self.stride[0] # image size (h,w)
- anchor_points, stride_tensor = make_anchors(feats, self.stride, 0.5)
-
- # targets
- targets = self.preprocess(targets, batch_size, scale_tensor=imgsz[[1, 0, 1, 0]])
- gt_labels, gt_bboxes = targets.split((1, 4), 2) # cls, xyxy
- mask_gt = gt_bboxes.sum(2, keepdim=True).gt_(0)
-
- # pboxes
- pred_bboxes = self.bbox_decode(anchor_points, pred_distri) # xyxy, (b, h*w, 4)
-
- target_labels, target_bboxes, target_scores, fg_mask = self.assigner(
- pred_scores.detach().sigmoid(),
- (pred_bboxes.detach() * stride_tensor).type(gt_bboxes.dtype),
- anchor_points * stride_tensor,
- gt_labels,
- gt_bboxes,
- mask_gt)
-
- target_bboxes /= stride_tensor
- target_scores_sum = max(target_scores.sum(), 1)
-
- # cls loss
- # loss[1] = self.varifocal_loss(pred_scores, target_scores, target_labels) / target_scores_sum # VFL way
- loss[1] = self.BCEcls(pred_scores, target_scores.to(dtype)).sum() / target_scores_sum # BCE
-
- # bbox loss
- if fg_mask.sum():
- loss[0], loss[2], iou = self.bbox_loss(pred_distri,
- pred_bboxes,
- anchor_points,
- target_bboxes,
- target_scores,
- target_scores_sum,
- fg_mask, imgsz[0], imgsz[1])
-
- loss[0] *= 7.5 # box gain
- loss[1] *= 0.5 # cls gain
- loss[2] *= 1.5 # dfl gain
-
- return loss.sum() * batch_size, loss.detach() # loss(box, cls, dfl)
到此本文的正式分享内容就结束了,在这里给大家推荐我的YOLOv9改进有效涨点专栏,本专栏目前为新开的,后期我会根据各种最新的前沿顶会进行论文复现,也会对一些老的改进机制进行补充,如果大家觉得本文帮助到你了,订阅本专栏(目前免费订阅,后期不迷路),关注后续更多的更新~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。