YOLOv11改进 | 损失函数改进篇 | 2024最新高质量的目标检测边界框回归损失Unified-IoU、FocalUIoU、FocalInvUIoU(设置动态epoch参数)

一、本文介绍

本文给大家带来的改进机制是最近新提出的高质量的 目标检测 边界框 回归损失 Unified-IoU ,其通过动态调整 模型 对不同质量预测框的关注,优化目标检测中的边界框回归精度。UIoU引入了 Focal Box 方法,通过缩放预测框与真实框分配权重, 并采用了退火策略(引入动态参数epoch) ,逐渐将模型的 注意力 从低质量预测框转移到高质量预测框,平衡了训练速度与检测精度。其还有一定的解决样本不平衡问题,同时该 损失函数 可以和现有的任何边界框回归损失函数进行结合,例如ShapeIoU和其结合可以形成二次创新,它是一种类似于之前的Inner的机制.

本文的修改涉及到7处,因为UIoU涉及到一个动态的实时参数epoch,这个参数不是定值,同时 本文提供了论文里所提到的多种组合方式的实现方法包括UioU,FocalUIoU,以及论文中说提升最大的Focal-inv-UIoU。


目录

一、本文介绍

二、Unified-IoU原理介绍

三、核心代码

3.1 代码一

3.2 代码二

四、使用方法

4.1 修改一

4.2 修改二

4.3 修改三

4.4 修改四

4.5 修改五

4.6 修改六

4.7 修改七

4.8  修改八

五、使用方法

5.1 步骤一

5.2 步骤二

5.3 检验是否使用了UioU

六、本文总结


二、Unified-IoU原理介绍

官方论文地址: 官方论文地址点击此处即可跳转.

官方代码地址: 官方代码地址点击此处即可跳转

Unified-IoU (UIoU),它主要针对高质量的目标检测进行优化。传统的IoU(交并比)损失函数只考虑预测框与真实框的几何差异,而UIoU通过动态调整模型对低质量与高质量预测框的关注来提升精度。文章的核心思想在于动态权重分配(代码中提供了多个权重分配方法),使模型在训练初期专注于低质量预测框加快收敛速度,而在训练后期转向高质量预测框以提升最终的检测精度。

主要贡献:
1. 动态权重分配:在 模型训练 过程中,UIoU对不同质量的预测框分配不同的权重,特别是对高质量预测框给予更多关注。
2. Focal Box方法:通过缩放预测框和真实框,分配不同的损失权重,提升边界框回归的性能。

3. 退火策略:引入一个动态超参数“ratio”,在训练初期放大预测框以专注低质量预测框,而在训练后期缩小预测框以关注高质量预测框。

4. 借鉴Focal Loss:在UIoU中采用了双重注意机制,以优化不同质量预测框的权重分配。

实验结果:
VOC2007 & COCO2017数据集:UIoU在这些数据集上表现优异,特别是在较高IoU阈值下表现出显著的检测精度提升。
CityPersons数据集:UIoU在面对密集数据集时遇到了一些困难,后通过引入改进版的 Focal-inv(反向Focal Loss)来解决问题,大幅提高了检测精度。

总结:
UIoU能够有效提升模型在高质量目标检测任务中的表现,通过动态调整训练速度与精度之间的平衡,使得模型在多个数据集上表现优异。该方法为未来的目标检测任务提供了新的思路,尤其是在密集数据场景中,他还能解决一定样本不平衡问题。


三、核心代码

下面代码一和代码二的使用方式看章节四!

3.1 代码一

  1. import numpy as np
  2. import torch, math
  3. class WIoU_Scale:
  4. ''' monotonous: {
  5. None: origin v1
  6. True: monotonic FM v2
  7. False: non-monotonic FM v3
  8. }
  9. momentum: The momentum of running mean'''
  10. iou_mean = 1.
  11. monotonous = False
  12. _momentum = 1 - 0.5 ** (1 / 7000)
  13. _is_train = True
  14. def __init__(self, iou):
  15. self.iou = iou
  16. self._update(self)
  17. @classmethod
  18. def _update(cls, self):
  19. if cls._is_train: cls.iou_mean = (1 - cls._momentum) * cls.iou_mean + \
  20. cls._momentum * self.iou.detach().mean().item()
  21. @classmethod
  22. def _scaled_loss(cls, self, gamma=1.9, delta=3):
  23. if isinstance(self.monotonous, bool):
  24. if self.monotonous:
  25. return (self.iou.detach() / self.iou_mean).sqrt()
  26. else:
  27. beta = self.iou.detach() / self.iou_mean
  28. alpha = delta * torch.pow(gamma, beta - delta)
  29. return beta / alpha
  30. return 1
  31. def bbox_iou(box1, box2, epoch=1, xywh=True, GIoU=False, DIoU=False, CIoU=False, SIoU=False, EIoU=False, WIoU=False,
  32. UIoU=False, Focal=False, alpha=1, gamma=0.5, scale=False, eps=1e-7):
  33. # Returns Intersection over Union (IoU) of box1(1,4) to box2(n,4)
  34. # Get the coordinates of bounding boxes
  35. if xywh: # transform from xywh to xyxy
  36. (x1, y1, w1, h1), (x2, y2, w2, h2) = box1.chunk(4, -1), box2.chunk(4, -1)
  37. w1_, h1_, w2_, h2_ = w1 / 2, h1 / 2, w2 / 2, h2 / 2
  38. b1_x1, b1_x2, b1_y1, b1_y2 = x1 - w1_, x1 + w1_, y1 - h1_, y1 + h1_
  39. b2_x1, b2_x2, b2_y1, b2_y2 = x2 - w2_, x2 + w2_, y2 - h2_, y2 + h2_
  40. else: # x1, y1, x2, y2 = box1
  41. b1_x1, b1_y1, b1_x2, b1_y2 = box1.chunk(4, -1)
  42. b2_x1, b2_y1, b2_x2, b2_y2 = box2.chunk(4, -1)
  43. w1, h1 = b1_x2 - b1_x1, (b1_y2 - b1_y1).clamp(eps)
  44. w2, h2 = b2_x2 - b2_x1, (b2_y2 - b2_y1).clamp(eps)
  45. # ----------------------------------------------------------------------------------------------------------------
  46. # UIoU
  47. if UIoU:
  48. # define the center point for scaling
  49. epoch = epoch + 1
  50. bb1_xc = x1
  51. bb1_yc = y1
  52. bb2_xc = x2
  53. bb2_yc = y2
  54. # attenuation mode of hyperparameter "ratio"
  55. linear = True
  56. cosine = False
  57. fraction = False
  58. # assuming that the total training epochs are 300, the "ratio" changes from 2 to 0.5
  59. if linear:
  60. ratio = -0.005 * epoch + 2
  61. elif cosine:
  62. ratio = 0.75 * math.cos(math.pi * epoch / 300) + 1.25
  63. elif fraction:
  64. ratio = 200 / (epoch + 100)
  65. else:
  66. ratio = 0.5
  67. ww1, hh1, ww2, hh2 = w1 * ratio, h1 * ratio, w2 * ratio, h2 * ratio
  68. bb1_x1, bb1_x2, bb1_y1, bb1_y2 = bb1_xc - (ww1 / 2), bb1_xc + (ww1 / 2), bb1_yc - (hh1 / 2), bb1_yc + (hh1 / 2)
  69. bb2_x1, bb2_x2, bb2_y1, bb2_y2 = bb2_xc - (ww2 / 2), bb2_xc + (ww2 / 2), bb2_yc - (hh2 / 2), bb2_yc + (hh2 / 2)
  70. # assign the value back to facilitate subsequent calls
  71. w1, h1, w2, h2 = ww1, hh1, ww2, hh2
  72. b1_x1, b1_x2, b1_y1, b1_y2 = bb1_x1, bb1_x2, bb1_y1, bb1_y2
  73. b2_x1, b2_x2, b2_y1, b2_y2 = bb2_x1, bb2_x2, bb2_y1, bb2_y2
  74. # print(epoch)
  75. CIoU = True
  76. # ---------------------------------------------------------------------------------------------------------------
  77. # Intersection area
  78. inter = (b1_x2.minimum(b2_x2) - b1_x1.maximum(b2_x1)).clamp(0) * \
  79. (b1_y2.minimum(b2_y2) - b1_y1.maximum(b2_y1)).clamp(0)
  80. # Union Area
  81. union = w1 * h1 + w2 * h2 - inter + eps
  82. # WIoU needs to set scale to "True"
  83. if scale:
  84. self = WIoU_Scale(1 - (inter / union))
  85. # IoU
  86. # iou = inter / union # ori iou
  87. iou = torch.pow(inter / (union + eps), alpha) # alpha iou
  88. if CIoU or DIoU or GIoU or EIoU or SIoU or WIoU:
  89. cw = b1_x2.maximum(b2_x2) - b1_x1.minimum(b2_x1) # convex (smallest enclosing box) width
  90. ch = b1_y2.maximum(b2_y2) - b1_y1.minimum(b2_y1) # convex height
  91. if CIoU or DIoU or EIoU or SIoU or WIoU: # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1
  92. c2 = (cw ** 2 + ch ** 2) ** alpha + eps # convex diagonal squared
  93. rho2 = (((b2_x1 + b2_x2 - b1_x1 - b1_x2) ** 2 + (
  94. b2_y1 + b2_y2 - b1_y1 - b1_y2) ** 2) / 4) ** alpha # center dist ** 2
  95. if CIoU: # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47
  96. v = (4 / math.pi ** 2) * (torch.atan(w2 / h2) - torch.atan(w1 / h1)).pow(2)
  97. with torch.no_grad():
  98. alpha_ciou = v / (v - iou + (1 + eps))
  99. if Focal:
  100. return iou - (rho2 / c2 + torch.pow(v * alpha_ciou + eps, alpha)), torch.pow(inter / (union + eps),
  101. gamma) # Focal_CIoU
  102. else:
  103. return iou - (rho2 / c2 + torch.pow(v * alpha_ciou + eps, alpha)) # CIoU
  104. elif EIoU:
  105. rho_w2 = ((b2_x2 - b2_x1) - (b1_x2 - b1_x1)) ** 2
  106. rho_h2 = ((b2_y2 - b2_y1) - (b1_y2 - b1_y1)) ** 2
  107. cw2 = torch.pow(cw ** 2 + eps, alpha)
  108. ch2 = torch.pow(ch ** 2 + eps, alpha)
  109. if Focal:
  110. return iou - (rho2 / c2 + rho_w2 / cw2 + rho_h2 / ch2), torch.pow(inter / (union + eps),
  111. gamma) # Focal_EIou
  112. else:
  113. return iou - (rho2 / c2 + rho_w2 / cw2 + rho_h2 / ch2) # EIou
  114. elif SIoU:
  115. # SIoU Loss https://arxiv.org/pdf/2205.12740.pdf
  116. s_cw = (b2_x1 + b2_x2 - b1_x1 - b1_x2) * 0.5 + eps
  117. s_ch = (b2_y1 + b2_y2 - b1_y1 - b1_y2) * 0.5 + eps
  118. sigma = torch.pow(s_cw ** 2 + s_ch ** 2, 0.5)
  119. sin_alpha_1 = torch.abs(s_cw) / sigma
  120. sin_alpha_2 = torch.abs(s_ch) / sigma
  121. threshold = pow(2, 0.5) / 2
  122. sin_alpha = torch.where(sin_alpha_1 > threshold, sin_alpha_2, sin_alpha_1)
  123. angle_cost = torch.cos(torch.arcsin(sin_alpha) * 2 - math.pi / 2)
  124. rho_x = (s_cw / cw) ** 2
  125. rho_y = (s_ch / ch) ** 2
  126. gamma = angle_cost - 2
  127. distance_cost = 2 - torch.exp(gamma * rho_x) - torch.exp(gamma * rho_y)
  128. omiga_w = torch.abs(w1 - w2) / torch.max(w1, w2)
  129. omiga_h = torch.abs(h1 - h2) / torch.max(h1, h2)
  130. shape_cost = torch.pow(1 - torch.exp(-1 * omiga_w), 4) + torch.pow(1 - torch.exp(-1 * omiga_h), 4)
  131. if Focal:
  132. return iou - torch.pow(0.5 * (distance_cost + shape_cost) + eps, alpha), torch.pow(
  133. inter / (union + eps), gamma) # Focal_SIou
  134. else:
  135. return iou - torch.pow(0.5 * (distance_cost + shape_cost) + eps, alpha) # SIou
  136. elif WIoU:
  137. if Focal:
  138. raise RuntimeError("WIoU do not support Focal.")
  139. elif scale:
  140. return getattr(WIoU_Scale, '_scaled_loss')(self), (1 - iou) * torch.exp(
  141. (rho2 / c2)), iou # WIoU https://arxiv.org/abs/2301.10051
  142. else:
  143. return iou, torch.exp((rho2 / c2)) # WIoU v1
  144. if Focal:
  145. return iou - rho2 / c2, torch.pow(inter / (union + eps), gamma) # Focal_DIoU
  146. else:
  147. return iou - rho2 / c2 # DIoU
  148. c_area = cw * ch + eps # convex area
  149. if Focal:
  150. return iou - torch.pow((c_area - union) / c_area + eps, alpha), torch.pow(inter / (union + eps),
  151. gamma) # Focal_GIoU https://arxiv.org/pdf/1902.09630.pdf
  152. else:
  153. return iou - torch.pow((c_area - union) / c_area + eps, alpha) # GIoU https://arxiv.org/pdf/1902.09630.pdf
  154. if Focal:
  155. return iou, torch.pow(inter / (union + eps), gamma) # Focal_IoU
  156. else:
  157. return iou # IoU


3.2 代码二

# 注意需要把xywh设置为True否则会报错x1未定义.

  1. def forward(self, pred_dist, pred_bboxes, anchor_points, target_bboxes, target_scores, target_scores_sum, fg_mask, epoch):
  2. """IoU loss."""
  3. weight = target_scores.sum(-1)[fg_mask].unsqueeze(-1)
  4. iou = bbox_iou(pred_bboxes[fg_mask], target_bboxes[fg_mask], xywh=True, epoch=epoch, CIoU=False, UIoU=True, Focal=False)
  5. # UIoU设置为True则代码使用UIoU, Focal同时设置为True 则设置的是FocalUIoU
  6. if type(iou) is tuple:
  7. if len(iou) == 2:
  8. # increased the weight of low/high IoU
  9. loss_iou = ((1 - iou[1].detach().squeeze()) * (1 - iou[0].squeeze()) * weight).sum() / target_scores_sum # Focal
  10. # lbox += (iou[1].detach().squeeze() * (1 - iou[0].squeeze())* weight).sum() / target_scores_sum # Focal-inv
  11. # 这里有两种方法,大家可以自行尝试,这里的Focal-inv也是文章中提出的.
  12. else:
  13. loss_iou = (iou[0] * iou[1] * weight).sum() / target_scores_sum
  14. else:
  15. loss_iou = ((1.0 - iou) * weight).sum() / target_scores_sum # iou loss
  16. # DFL loss
  17. if self.dfl_loss:
  18. target_ltrb = bbox2dist(anchor_points, target_bboxes, self.dfl_loss.reg_max - 1)
  19. loss_dfl = self.dfl_loss(pred_dist[fg_mask].view(-1, self.dfl_loss.reg_max), target_ltrb[fg_mask]) * weight
  20. loss_dfl = loss_dfl.sum() / target_scores_sum
  21. else:
  22. loss_dfl = torch.tensor(0.0).to(pred_dist.device)
  23. return loss_iou, loss_dfl


四、使用方法

使用方法按照下面来操作,注意本文的修改位置涉及多个文件多个位置,比较复杂大家一定要仔细观看.


4.1 修改一

修改一我们找到文件 ultralytics /utils/metrics.py 将代码一按照下图进行修改.


4.2 修改二

第二步我们找到文件ultralytics/utils/loss.py按照下图进行修改.


4.3 修改三

第三步就是一些比较不常见的修改了,我们找到文件ultralytics/engine/trainer.py.按照下图进行修改,独创修改内容.


4.4 修改四

第四步找到文件ultralytics/nn/tasks.py,按照下图进行修改.


4.5 修改五

第五步找到文件ultralytics/nn/tasks.py,按照下图进行修改.


4.6 修改六

找到文件ultralytics/utils/loss.py进行修改,按照下图进行修改.


4.7 修改七

找到文件ultralytics/utils/loss.py进行修改,按照下图进行修改.


五、使用方法

上面修改完了,本文的任何一部修改错误都会导致修改失败.

5.1 步骤一

使用方法我们找到修改二修改的位置.如下图进行修改.使用方法都贴在了途中.


5.2 检验是否使用了UioU

大家一直困惑自己使用使用了对应的IoU,可能对于基础不太好的同学不会debug去看代码,这里给大家提供一种方法,也来检验本文的添加方法使用成功.

找到文件ultralytics/utils/metrics.py


本文内容全部为个人总结,抄袭的时候想想别人的劳动成果,谢谢.


六、本文总结

到此本文的正式分享内容就结束了,在这里给大家推荐我的YOLOv110改进有效涨点专栏,本专栏目前为新开的平均质量分98分,后期我会根据各种最新的前沿顶会进行论文复现,也会对一些老的改进机制进行补充 如果大家觉得本文帮助到你了,订阅本专栏,关注后续更多的更新~

​​​