loss.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743
  1. # Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
  2. import torch
  3. import torch.nn as nn
  4. import torch.nn.functional as F
  5. from ultralytics.utils.metrics import OKS_SIGMA
  6. from ultralytics.utils.ops import crop_mask, xywh2xyxy, xyxy2xywh
  7. from ultralytics.utils.tal import RotatedTaskAlignedAssigner, TaskAlignedAssigner, dist2bbox, dist2rbox, make_anchors
  8. from ultralytics.utils.torch_utils import autocast
  9. from .metrics import bbox_iou, probiou
  10. from .tal import bbox2dist
  11. class VarifocalLoss(nn.Module):
  12. """
  13. Varifocal loss by Zhang et al.
  14. https://arxiv.org/abs/2008.13367.
  15. """
  16. def __init__(self):
  17. """Initialize the VarifocalLoss class."""
  18. super().__init__()
  19. @staticmethod
  20. def forward(pred_score, gt_score, label, alpha=0.75, gamma=2.0):
  21. """Computes varfocal loss."""
  22. weight = alpha * pred_score.sigmoid().pow(gamma) * (1 - label) + gt_score * label
  23. with autocast(enabled=False):
  24. loss = (
  25. (F.binary_cross_entropy_with_logits(pred_score.float(), gt_score.float(), reduction="none") * weight)
  26. .mean(1)
  27. .sum()
  28. )
  29. return loss
  30. class FocalLoss(nn.Module):
  31. """Wraps focal loss around existing loss_fcn(), i.e. criteria = FocalLoss(nn.BCEWithLogitsLoss(), gamma=1.5)."""
  32. def __init__(self):
  33. """Initializer for FocalLoss class with no parameters."""
  34. super().__init__()
  35. @staticmethod
  36. def forward(pred, label, gamma=1.5, alpha=0.25):
  37. """Calculates and updates confusion matrix for object detection/classification tasks."""
  38. loss = F.binary_cross_entropy_with_logits(pred, label, reduction="none")
  39. # p_t = torch.exp(-loss)
  40. # loss *= self.alpha * (1.000001 - p_t) ** self.gamma # non-zero power for gradient stability
  41. # TF implementation https://github.com/tensorflow/addons/blob/v0.7.1/tensorflow_addons/losses/focal_loss.py
  42. pred_prob = pred.sigmoid() # prob from logits
  43. p_t = label * pred_prob + (1 - label) * (1 - pred_prob)
  44. modulating_factor = (1.0 - p_t) ** gamma
  45. loss *= modulating_factor
  46. if alpha > 0:
  47. alpha_factor = label * alpha + (1 - label) * (1 - alpha)
  48. loss *= alpha_factor
  49. return loss.mean(1).sum()
  50. class DFLoss(nn.Module):
  51. """Criterion class for computing DFL losses during training."""
  52. def __init__(self, reg_max=16) -> None:
  53. """Initialize the DFL module."""
  54. super().__init__()
  55. self.reg_max = reg_max
  56. def __call__(self, pred_dist, target):
  57. """
  58. Return sum of left and right DFL losses.
  59. Distribution Focal Loss (DFL) proposed in Generalized Focal Loss
  60. https://ieeexplore.ieee.org/document/9792391
  61. """
  62. target = target.clamp_(0, self.reg_max - 1 - 0.01)
  63. tl = target.long() # target left
  64. tr = tl + 1 # target right
  65. wl = tr - target # weight left
  66. wr = 1 - wl # weight right
  67. return (
  68. F.cross_entropy(pred_dist, tl.view(-1), reduction="none").view(tl.shape) * wl
  69. + F.cross_entropy(pred_dist, tr.view(-1), reduction="none").view(tl.shape) * wr
  70. ).mean(-1, keepdim=True)
  71. class BboxLoss(nn.Module):
  72. """Criterion class for computing training losses during training."""
  73. def __init__(self, reg_max=16):
  74. """Initialize the BboxLoss module with regularization maximum and DFL settings."""
  75. super().__init__()
  76. self.dfl_loss = DFLoss(reg_max) if reg_max > 1 else None
  77. def forward(self, pred_dist, pred_bboxes, anchor_points, target_bboxes, target_scores, target_scores_sum, fg_mask):
  78. """IoU loss."""
  79. weight = target_scores.sum(-1)[fg_mask].unsqueeze(-1)
  80. iou = bbox_iou(pred_bboxes[fg_mask], target_bboxes[fg_mask], xywh=False, CIoU=True)
  81. loss_iou = ((1.0 - iou) * weight).sum() / target_scores_sum
  82. # DFL loss
  83. if self.dfl_loss:
  84. target_ltrb = bbox2dist(anchor_points, target_bboxes, self.dfl_loss.reg_max - 1)
  85. loss_dfl = self.dfl_loss(pred_dist[fg_mask].view(-1, self.dfl_loss.reg_max), target_ltrb[fg_mask]) * weight
  86. loss_dfl = loss_dfl.sum() / target_scores_sum
  87. else:
  88. loss_dfl = torch.tensor(0.0).to(pred_dist.device)
  89. return loss_iou, loss_dfl
  90. class RotatedBboxLoss(BboxLoss):
  91. """Criterion class for computing training losses during training."""
  92. def __init__(self, reg_max):
  93. """Initialize the BboxLoss module with regularization maximum and DFL settings."""
  94. super().__init__(reg_max)
  95. def forward(self, pred_dist, pred_bboxes, anchor_points, target_bboxes, target_scores, target_scores_sum, fg_mask):
  96. """IoU loss."""
  97. weight = target_scores.sum(-1)[fg_mask].unsqueeze(-1)
  98. iou = probiou(pred_bboxes[fg_mask], target_bboxes[fg_mask])
  99. loss_iou = ((1.0 - iou) * weight).sum() / target_scores_sum
  100. # DFL loss
  101. if self.dfl_loss:
  102. target_ltrb = bbox2dist(anchor_points, xywh2xyxy(target_bboxes[..., :4]), self.dfl_loss.reg_max - 1)
  103. loss_dfl = self.dfl_loss(pred_dist[fg_mask].view(-1, self.dfl_loss.reg_max), target_ltrb[fg_mask]) * weight
  104. loss_dfl = loss_dfl.sum() / target_scores_sum
  105. else:
  106. loss_dfl = torch.tensor(0.0).to(pred_dist.device)
  107. return loss_iou, loss_dfl
  108. class KeypointLoss(nn.Module):
  109. """Criterion class for computing training losses."""
  110. def __init__(self, sigmas) -> None:
  111. """Initialize the KeypointLoss class."""
  112. super().__init__()
  113. self.sigmas = sigmas
  114. def forward(self, pred_kpts, gt_kpts, kpt_mask, area):
  115. """Calculates keypoint loss factor and Euclidean distance loss for predicted and actual keypoints."""
  116. d = (pred_kpts[..., 0] - gt_kpts[..., 0]).pow(2) + (pred_kpts[..., 1] - gt_kpts[..., 1]).pow(2)
  117. kpt_loss_factor = kpt_mask.shape[1] / (torch.sum(kpt_mask != 0, dim=1) + 1e-9)
  118. # e = d / (2 * (area * self.sigmas) ** 2 + 1e-9) # from formula
  119. e = d / ((2 * self.sigmas).pow(2) * (area + 1e-9) * 2) # from cocoeval
  120. return (kpt_loss_factor.view(-1, 1) * ((1 - torch.exp(-e)) * kpt_mask)).mean()
  121. class v8DetectionLoss:
  122. """Criterion class for computing training losses."""
  123. def __init__(self, model, tal_topk=10): # model must be de-paralleled
  124. """Initializes v8DetectionLoss with the model, defining model-related properties and BCE loss function."""
  125. device = next(model.parameters()).device # get model device
  126. h = model.args # hyperparameters
  127. m = model.model[-1] # Detect() module
  128. self.bce = nn.BCEWithLogitsLoss(reduction="none")
  129. self.hyp = h
  130. self.stride = m.stride # model strides
  131. self.nc = m.nc # number of classes
  132. self.no = m.nc + m.reg_max * 4
  133. self.reg_max = m.reg_max
  134. self.device = device
  135. self.use_dfl = m.reg_max > 1
  136. self.assigner = TaskAlignedAssigner(topk=tal_topk, num_classes=self.nc, alpha=0.5, beta=6.0)
  137. self.bbox_loss = BboxLoss(m.reg_max).to(device)
  138. self.proj = torch.arange(m.reg_max, dtype=torch.float, device=device)
  139. def preprocess(self, targets, batch_size, scale_tensor):
  140. """Preprocesses the target counts and matches with the input batch size to output a tensor."""
  141. nl, ne = targets.shape
  142. if nl == 0:
  143. out = torch.zeros(batch_size, 0, ne - 1, device=self.device)
  144. else:
  145. i = targets[:, 0] # image index
  146. _, counts = i.unique(return_counts=True)
  147. counts = counts.to(dtype=torch.int32)
  148. out = torch.zeros(batch_size, counts.max(), ne - 1, device=self.device)
  149. for j in range(batch_size):
  150. matches = i == j
  151. if n := matches.sum():
  152. out[j, :n] = targets[matches, 1:]
  153. out[..., 1:5] = xywh2xyxy(out[..., 1:5].mul_(scale_tensor))
  154. return out
  155. def bbox_decode(self, anchor_points, pred_dist):
  156. """Decode predicted object bounding box coordinates from anchor points and distribution."""
  157. if self.use_dfl:
  158. b, a, c = pred_dist.shape # batch, anchors, channels
  159. pred_dist = pred_dist.view(b, a, 4, c // 4).softmax(3).matmul(self.proj.type(pred_dist.dtype))
  160. # pred_dist = pred_dist.view(b, a, c // 4, 4).transpose(2,3).softmax(3).matmul(self.proj.type(pred_dist.dtype))
  161. # 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)
  162. return dist2bbox(pred_dist, anchor_points, xywh=False)
  163. def __call__(self, preds, batch):
  164. """Calculate the sum of the loss for box, cls and dfl multiplied by batch size."""
  165. loss = torch.zeros(3, device=self.device) # box, cls, dfl
  166. feats = preds[1] if isinstance(preds, tuple) else preds
  167. pred_distri, pred_scores = torch.cat([xi.view(feats[0].shape[0], self.no, -1) for xi in feats], 2).split(
  168. (self.reg_max * 4, self.nc), 1
  169. )
  170. pred_scores = pred_scores.permute(0, 2, 1).contiguous()
  171. pred_distri = pred_distri.permute(0, 2, 1).contiguous()
  172. dtype = pred_scores.dtype
  173. batch_size = pred_scores.shape[0]
  174. imgsz = torch.tensor(feats[0].shape[2:], device=self.device, dtype=dtype) * self.stride[0] # image size (h,w)
  175. anchor_points, stride_tensor = make_anchors(feats, self.stride, 0.5)
  176. # Targets
  177. targets = torch.cat((batch["batch_idx"].view(-1, 1), batch["cls"].view(-1, 1), batch["bboxes"]), 1)
  178. targets = self.preprocess(targets.to(self.device), batch_size, scale_tensor=imgsz[[1, 0, 1, 0]])
  179. gt_labels, gt_bboxes = targets.split((1, 4), 2) # cls, xyxy
  180. mask_gt = gt_bboxes.sum(2, keepdim=True).gt_(0.0)
  181. # Pboxes
  182. pred_bboxes = self.bbox_decode(anchor_points, pred_distri) # xyxy, (b, h*w, 4)
  183. # dfl_conf = pred_distri.view(batch_size, -1, 4, self.reg_max).detach().softmax(-1)
  184. # dfl_conf = (dfl_conf.amax(-1).mean(-1) + dfl_conf.amax(-1).amin(-1)) / 2
  185. _, target_bboxes, target_scores, fg_mask, _ = self.assigner(
  186. # pred_scores.detach().sigmoid() * 0.8 + dfl_conf.unsqueeze(-1) * 0.2,
  187. pred_scores.detach().sigmoid(),
  188. (pred_bboxes.detach() * stride_tensor).type(gt_bboxes.dtype),
  189. anchor_points * stride_tensor,
  190. gt_labels,
  191. gt_bboxes,
  192. mask_gt,
  193. )
  194. target_scores_sum = max(target_scores.sum(), 1)
  195. # Cls loss
  196. # loss[1] = self.varifocal_loss(pred_scores, target_scores, target_labels) / target_scores_sum # VFL way
  197. loss[1] = self.bce(pred_scores, target_scores.to(dtype)).sum() / target_scores_sum # BCE
  198. # Bbox loss
  199. if fg_mask.sum():
  200. target_bboxes /= stride_tensor
  201. loss[0], loss[2] = self.bbox_loss(
  202. pred_distri, pred_bboxes, anchor_points, target_bboxes, target_scores, target_scores_sum, fg_mask
  203. )
  204. loss[0] *= self.hyp.box # box gain
  205. loss[1] *= self.hyp.cls # cls gain
  206. loss[2] *= self.hyp.dfl # dfl gain
  207. return loss.sum() * batch_size, loss.detach() # loss(box, cls, dfl)
  208. class v8SegmentationLoss(v8DetectionLoss):
  209. """Criterion class for computing training losses."""
  210. def __init__(self, model): # model must be de-paralleled
  211. """Initializes the v8SegmentationLoss class, taking a de-paralleled model as argument."""
  212. super().__init__(model)
  213. self.overlap = model.args.overlap_mask
  214. def __call__(self, preds, batch):
  215. """Calculate and return the loss for the YOLO model."""
  216. loss = torch.zeros(4, device=self.device) # box, cls, dfl
  217. feats, pred_masks, proto = preds if len(preds) == 3 else preds[1]
  218. batch_size, _, mask_h, mask_w = proto.shape # batch size, number of masks, mask height, mask width
  219. pred_distri, pred_scores = torch.cat([xi.view(feats[0].shape[0], self.no, -1) for xi in feats], 2).split(
  220. (self.reg_max * 4, self.nc), 1
  221. )
  222. # B, grids, ..
  223. pred_scores = pred_scores.permute(0, 2, 1).contiguous()
  224. pred_distri = pred_distri.permute(0, 2, 1).contiguous()
  225. pred_masks = pred_masks.permute(0, 2, 1).contiguous()
  226. dtype = pred_scores.dtype
  227. imgsz = torch.tensor(feats[0].shape[2:], device=self.device, dtype=dtype) * self.stride[0] # image size (h,w)
  228. anchor_points, stride_tensor = make_anchors(feats, self.stride, 0.5)
  229. # Targets
  230. try:
  231. batch_idx = batch["batch_idx"].view(-1, 1)
  232. targets = torch.cat((batch_idx, batch["cls"].view(-1, 1), batch["bboxes"]), 1)
  233. targets = self.preprocess(targets.to(self.device), batch_size, scale_tensor=imgsz[[1, 0, 1, 0]])
  234. gt_labels, gt_bboxes = targets.split((1, 4), 2) # cls, xyxy
  235. mask_gt = gt_bboxes.sum(2, keepdim=True).gt_(0.0)
  236. except RuntimeError as e:
  237. raise TypeError(
  238. "ERROR ❌ segment dataset incorrectly formatted or not a segment dataset.\n"
  239. "This error can occur when incorrectly training a 'segment' model on a 'detect' dataset, "
  240. "i.e. 'yolo train model=yolov8n-seg.pt data=coco8.yaml'.\nVerify your dataset is a "
  241. "correctly formatted 'segment' dataset using 'data=coco8-seg.yaml' "
  242. "as an example.\nSee https://docs.ultralytics.com/datasets/segment/ for help."
  243. ) from e
  244. # Pboxes
  245. pred_bboxes = self.bbox_decode(anchor_points, pred_distri) # xyxy, (b, h*w, 4)
  246. _, target_bboxes, target_scores, fg_mask, target_gt_idx = self.assigner(
  247. pred_scores.detach().sigmoid(),
  248. (pred_bboxes.detach() * stride_tensor).type(gt_bboxes.dtype),
  249. anchor_points * stride_tensor,
  250. gt_labels,
  251. gt_bboxes,
  252. mask_gt,
  253. )
  254. target_scores_sum = max(target_scores.sum(), 1)
  255. # Cls loss
  256. # loss[1] = self.varifocal_loss(pred_scores, target_scores, target_labels) / target_scores_sum # VFL way
  257. loss[2] = self.bce(pred_scores, target_scores.to(dtype)).sum() / target_scores_sum # BCE
  258. if fg_mask.sum():
  259. # Bbox loss
  260. loss[0], loss[3] = self.bbox_loss(
  261. pred_distri,
  262. pred_bboxes,
  263. anchor_points,
  264. target_bboxes / stride_tensor,
  265. target_scores,
  266. target_scores_sum,
  267. fg_mask,
  268. )
  269. # Masks loss
  270. masks = batch["masks"].to(self.device).float()
  271. if tuple(masks.shape[-2:]) != (mask_h, mask_w): # downsample
  272. masks = F.interpolate(masks[None], (mask_h, mask_w), mode="nearest")[0]
  273. loss[1] = self.calculate_segmentation_loss(
  274. fg_mask, masks, target_gt_idx, target_bboxes, batch_idx, proto, pred_masks, imgsz, self.overlap
  275. )
  276. # WARNING: lines below prevent Multi-GPU DDP 'unused gradient' PyTorch errors, do not remove
  277. else:
  278. loss[1] += (proto * 0).sum() + (pred_masks * 0).sum() # inf sums may lead to nan loss
  279. loss[0] *= self.hyp.box # box gain
  280. loss[1] *= self.hyp.box # seg gain
  281. loss[2] *= self.hyp.cls # cls gain
  282. loss[3] *= self.hyp.dfl # dfl gain
  283. return loss.sum() * batch_size, loss.detach() # loss(box, cls, dfl)
  284. @staticmethod
  285. def single_mask_loss(
  286. gt_mask: torch.Tensor, pred: torch.Tensor, proto: torch.Tensor, xyxy: torch.Tensor, area: torch.Tensor
  287. ) -> torch.Tensor:
  288. """
  289. Compute the instance segmentation loss for a single image.
  290. Args:
  291. gt_mask (torch.Tensor): Ground truth mask of shape (n, H, W), where n is the number of objects.
  292. pred (torch.Tensor): Predicted mask coefficients of shape (n, 32).
  293. proto (torch.Tensor): Prototype masks of shape (32, H, W).
  294. xyxy (torch.Tensor): Ground truth bounding boxes in xyxy format, normalized to [0, 1], of shape (n, 4).
  295. area (torch.Tensor): Area of each ground truth bounding box of shape (n,).
  296. Returns:
  297. (torch.Tensor): The calculated mask loss for a single image.
  298. Notes:
  299. The function uses the equation pred_mask = torch.einsum('in,nhw->ihw', pred, proto) to produce the
  300. predicted masks from the prototype masks and predicted mask coefficients.
  301. """
  302. pred_mask = torch.einsum("in,nhw->ihw", pred, proto) # (n, 32) @ (32, 80, 80) -> (n, 80, 80)
  303. loss = F.binary_cross_entropy_with_logits(pred_mask, gt_mask, reduction="none")
  304. return (crop_mask(loss, xyxy).mean(dim=(1, 2)) / area).sum()
  305. def calculate_segmentation_loss(
  306. self,
  307. fg_mask: torch.Tensor,
  308. masks: torch.Tensor,
  309. target_gt_idx: torch.Tensor,
  310. target_bboxes: torch.Tensor,
  311. batch_idx: torch.Tensor,
  312. proto: torch.Tensor,
  313. pred_masks: torch.Tensor,
  314. imgsz: torch.Tensor,
  315. overlap: bool,
  316. ) -> torch.Tensor:
  317. """
  318. Calculate the loss for instance segmentation.
  319. Args:
  320. fg_mask (torch.Tensor): A binary tensor of shape (BS, N_anchors) indicating which anchors are positive.
  321. masks (torch.Tensor): Ground truth masks of shape (BS, H, W) if `overlap` is False, otherwise (BS, ?, H, W).
  322. target_gt_idx (torch.Tensor): Indexes of ground truth objects for each anchor of shape (BS, N_anchors).
  323. target_bboxes (torch.Tensor): Ground truth bounding boxes for each anchor of shape (BS, N_anchors, 4).
  324. batch_idx (torch.Tensor): Batch indices of shape (N_labels_in_batch, 1).
  325. proto (torch.Tensor): Prototype masks of shape (BS, 32, H, W).
  326. pred_masks (torch.Tensor): Predicted masks for each anchor of shape (BS, N_anchors, 32).
  327. imgsz (torch.Tensor): Size of the input image as a tensor of shape (2), i.e., (H, W).
  328. overlap (bool): Whether the masks in `masks` tensor overlap.
  329. Returns:
  330. (torch.Tensor): The calculated loss for instance segmentation.
  331. Notes:
  332. The batch loss can be computed for improved speed at higher memory usage.
  333. For example, pred_mask can be computed as follows:
  334. pred_mask = torch.einsum('in,nhw->ihw', pred, proto) # (i, 32) @ (32, 160, 160) -> (i, 160, 160)
  335. """
  336. _, _, mask_h, mask_w = proto.shape
  337. loss = 0
  338. # Normalize to 0-1
  339. target_bboxes_normalized = target_bboxes / imgsz[[1, 0, 1, 0]]
  340. # Areas of target bboxes
  341. marea = xyxy2xywh(target_bboxes_normalized)[..., 2:].prod(2)
  342. # Normalize to mask size
  343. mxyxy = target_bboxes_normalized * torch.tensor([mask_w, mask_h, mask_w, mask_h], device=proto.device)
  344. for i, single_i in enumerate(zip(fg_mask, target_gt_idx, pred_masks, proto, mxyxy, marea, masks)):
  345. fg_mask_i, target_gt_idx_i, pred_masks_i, proto_i, mxyxy_i, marea_i, masks_i = single_i
  346. if fg_mask_i.any():
  347. mask_idx = target_gt_idx_i[fg_mask_i]
  348. if overlap:
  349. gt_mask = masks_i == (mask_idx + 1).view(-1, 1, 1)
  350. gt_mask = gt_mask.float()
  351. else:
  352. gt_mask = masks[batch_idx.view(-1) == i][mask_idx]
  353. loss += self.single_mask_loss(
  354. gt_mask, pred_masks_i[fg_mask_i], proto_i, mxyxy_i[fg_mask_i], marea_i[fg_mask_i]
  355. )
  356. # WARNING: lines below prevents Multi-GPU DDP 'unused gradient' PyTorch errors, do not remove
  357. else:
  358. loss += (proto * 0).sum() + (pred_masks * 0).sum() # inf sums may lead to nan loss
  359. return loss / fg_mask.sum()
  360. class v8PoseLoss(v8DetectionLoss):
  361. """Criterion class for computing training losses."""
  362. def __init__(self, model): # model must be de-paralleled
  363. """Initializes v8PoseLoss with model, sets keypoint variables and declares a keypoint loss instance."""
  364. super().__init__(model)
  365. self.kpt_shape = model.model[-1].kpt_shape
  366. self.bce_pose = nn.BCEWithLogitsLoss()
  367. is_pose = self.kpt_shape == [17, 3]
  368. nkpt = self.kpt_shape[0] # number of keypoints
  369. sigmas = torch.from_numpy(OKS_SIGMA).to(self.device) if is_pose else torch.ones(nkpt, device=self.device) / nkpt
  370. self.keypoint_loss = KeypointLoss(sigmas=sigmas)
  371. def __call__(self, preds, batch):
  372. """Calculate the total loss and detach it."""
  373. loss = torch.zeros(5, device=self.device) # box, cls, dfl, kpt_location, kpt_visibility
  374. feats, pred_kpts = preds if isinstance(preds[0], list) else preds[1]
  375. pred_distri, pred_scores = torch.cat([xi.view(feats[0].shape[0], self.no, -1) for xi in feats], 2).split(
  376. (self.reg_max * 4, self.nc), 1
  377. )
  378. # B, grids, ..
  379. pred_scores = pred_scores.permute(0, 2, 1).contiguous()
  380. pred_distri = pred_distri.permute(0, 2, 1).contiguous()
  381. pred_kpts = pred_kpts.permute(0, 2, 1).contiguous()
  382. dtype = pred_scores.dtype
  383. imgsz = torch.tensor(feats[0].shape[2:], device=self.device, dtype=dtype) * self.stride[0] # image size (h,w)
  384. anchor_points, stride_tensor = make_anchors(feats, self.stride, 0.5)
  385. # Targets
  386. batch_size = pred_scores.shape[0]
  387. batch_idx = batch["batch_idx"].view(-1, 1)
  388. targets = torch.cat((batch_idx, batch["cls"].view(-1, 1), batch["bboxes"]), 1)
  389. targets = self.preprocess(targets.to(self.device), batch_size, scale_tensor=imgsz[[1, 0, 1, 0]])
  390. gt_labels, gt_bboxes = targets.split((1, 4), 2) # cls, xyxy
  391. mask_gt = gt_bboxes.sum(2, keepdim=True).gt_(0.0)
  392. # Pboxes
  393. pred_bboxes = self.bbox_decode(anchor_points, pred_distri) # xyxy, (b, h*w, 4)
  394. pred_kpts = self.kpts_decode(anchor_points, pred_kpts.view(batch_size, -1, *self.kpt_shape)) # (b, h*w, 17, 3)
  395. _, target_bboxes, target_scores, fg_mask, target_gt_idx = self.assigner(
  396. pred_scores.detach().sigmoid(),
  397. (pred_bboxes.detach() * stride_tensor).type(gt_bboxes.dtype),
  398. anchor_points * stride_tensor,
  399. gt_labels,
  400. gt_bboxes,
  401. mask_gt,
  402. )
  403. target_scores_sum = max(target_scores.sum(), 1)
  404. # Cls loss
  405. # loss[1] = self.varifocal_loss(pred_scores, target_scores, target_labels) / target_scores_sum # VFL way
  406. loss[3] = self.bce(pred_scores, target_scores.to(dtype)).sum() / target_scores_sum # BCE
  407. # Bbox loss
  408. if fg_mask.sum():
  409. target_bboxes /= stride_tensor
  410. loss[0], loss[4] = self.bbox_loss(
  411. pred_distri, pred_bboxes, anchor_points, target_bboxes, target_scores, target_scores_sum, fg_mask
  412. )
  413. keypoints = batch["keypoints"].to(self.device).float().clone()
  414. keypoints[..., 0] *= imgsz[1]
  415. keypoints[..., 1] *= imgsz[0]
  416. loss[1], loss[2] = self.calculate_keypoints_loss(
  417. fg_mask, target_gt_idx, keypoints, batch_idx, stride_tensor, target_bboxes, pred_kpts
  418. )
  419. loss[0] *= self.hyp.box # box gain
  420. loss[1] *= self.hyp.pose # pose gain
  421. loss[2] *= self.hyp.kobj # kobj gain
  422. loss[3] *= self.hyp.cls # cls gain
  423. loss[4] *= self.hyp.dfl # dfl gain
  424. return loss.sum() * batch_size, loss.detach() # loss(box, cls, dfl)
  425. @staticmethod
  426. def kpts_decode(anchor_points, pred_kpts):
  427. """Decodes predicted keypoints to image coordinates."""
  428. y = pred_kpts.clone()
  429. y[..., :2] *= 2.0
  430. y[..., 0] += anchor_points[:, [0]] - 0.5
  431. y[..., 1] += anchor_points[:, [1]] - 0.5
  432. return y
  433. def calculate_keypoints_loss(
  434. self, masks, target_gt_idx, keypoints, batch_idx, stride_tensor, target_bboxes, pred_kpts
  435. ):
  436. """
  437. Calculate the keypoints loss for the model.
  438. This function calculates the keypoints loss and keypoints object loss for a given batch. The keypoints loss is
  439. based on the difference between the predicted keypoints and ground truth keypoints. The keypoints object loss is
  440. a binary classification loss that classifies whether a keypoint is present or not.
  441. Args:
  442. masks (torch.Tensor): Binary mask tensor indicating object presence, shape (BS, N_anchors).
  443. target_gt_idx (torch.Tensor): Index tensor mapping anchors to ground truth objects, shape (BS, N_anchors).
  444. keypoints (torch.Tensor): Ground truth keypoints, shape (N_kpts_in_batch, N_kpts_per_object, kpts_dim).
  445. batch_idx (torch.Tensor): Batch index tensor for keypoints, shape (N_kpts_in_batch, 1).
  446. stride_tensor (torch.Tensor): Stride tensor for anchors, shape (N_anchors, 1).
  447. target_bboxes (torch.Tensor): Ground truth boxes in (x1, y1, x2, y2) format, shape (BS, N_anchors, 4).
  448. pred_kpts (torch.Tensor): Predicted keypoints, shape (BS, N_anchors, N_kpts_per_object, kpts_dim).
  449. Returns:
  450. kpts_loss (torch.Tensor): The keypoints loss.
  451. kpts_obj_loss (torch.Tensor): The keypoints object loss.
  452. """
  453. batch_idx = batch_idx.flatten()
  454. batch_size = len(masks)
  455. # Find the maximum number of keypoints in a single image
  456. max_kpts = torch.unique(batch_idx, return_counts=True)[1].max()
  457. # Create a tensor to hold batched keypoints
  458. batched_keypoints = torch.zeros(
  459. (batch_size, max_kpts, keypoints.shape[1], keypoints.shape[2]), device=keypoints.device
  460. )
  461. # TODO: any idea how to vectorize this?
  462. # Fill batched_keypoints with keypoints based on batch_idx
  463. for i in range(batch_size):
  464. keypoints_i = keypoints[batch_idx == i]
  465. batched_keypoints[i, : keypoints_i.shape[0]] = keypoints_i
  466. # Expand dimensions of target_gt_idx to match the shape of batched_keypoints
  467. target_gt_idx_expanded = target_gt_idx.unsqueeze(-1).unsqueeze(-1)
  468. # Use target_gt_idx_expanded to select keypoints from batched_keypoints
  469. selected_keypoints = batched_keypoints.gather(
  470. 1, target_gt_idx_expanded.expand(-1, -1, keypoints.shape[1], keypoints.shape[2])
  471. )
  472. # Divide coordinates by stride
  473. selected_keypoints /= stride_tensor.view(1, -1, 1, 1)
  474. kpts_loss = 0
  475. kpts_obj_loss = 0
  476. if masks.any():
  477. gt_kpt = selected_keypoints[masks]
  478. area = xyxy2xywh(target_bboxes[masks])[:, 2:].prod(1, keepdim=True)
  479. pred_kpt = pred_kpts[masks]
  480. kpt_mask = gt_kpt[..., 2] != 0 if gt_kpt.shape[-1] == 3 else torch.full_like(gt_kpt[..., 0], True)
  481. kpts_loss = self.keypoint_loss(pred_kpt, gt_kpt, kpt_mask, area) # pose loss
  482. if pred_kpt.shape[-1] == 3:
  483. kpts_obj_loss = self.bce_pose(pred_kpt[..., 2], kpt_mask.float()) # keypoint obj loss
  484. return kpts_loss, kpts_obj_loss
  485. class v8ClassificationLoss:
  486. """Criterion class for computing training losses."""
  487. def __call__(self, preds, batch):
  488. """Compute the classification loss between predictions and true labels."""
  489. preds = preds[1] if isinstance(preds, (list, tuple)) else preds
  490. loss = F.cross_entropy(preds, batch["cls"], reduction="mean")
  491. loss_items = loss.detach()
  492. return loss, loss_items
  493. class v8OBBLoss(v8DetectionLoss):
  494. """Calculates losses for object detection, classification, and box distribution in rotated YOLO models."""
  495. def __init__(self, model):
  496. """Initializes v8OBBLoss with model, assigner, and rotated bbox loss; note model must be de-paralleled."""
  497. super().__init__(model)
  498. self.assigner = RotatedTaskAlignedAssigner(topk=10, num_classes=self.nc, alpha=0.5, beta=6.0)
  499. self.bbox_loss = RotatedBboxLoss(self.reg_max).to(self.device)
  500. def preprocess(self, targets, batch_size, scale_tensor):
  501. """Preprocesses the target counts and matches with the input batch size to output a tensor."""
  502. if targets.shape[0] == 0:
  503. out = torch.zeros(batch_size, 0, 6, device=self.device)
  504. else:
  505. i = targets[:, 0] # image index
  506. _, counts = i.unique(return_counts=True)
  507. counts = counts.to(dtype=torch.int32)
  508. out = torch.zeros(batch_size, counts.max(), 6, device=self.device)
  509. for j in range(batch_size):
  510. matches = i == j
  511. if n := matches.sum():
  512. bboxes = targets[matches, 2:]
  513. bboxes[..., :4].mul_(scale_tensor)
  514. out[j, :n] = torch.cat([targets[matches, 1:2], bboxes], dim=-1)
  515. return out
  516. def __call__(self, preds, batch):
  517. """Calculate and return the loss for the YOLO model."""
  518. loss = torch.zeros(3, device=self.device) # box, cls, dfl
  519. feats, pred_angle = preds if isinstance(preds[0], list) else preds[1]
  520. batch_size = pred_angle.shape[0] # batch size, number of masks, mask height, mask width
  521. pred_distri, pred_scores = torch.cat([xi.view(feats[0].shape[0], self.no, -1) for xi in feats], 2).split(
  522. (self.reg_max * 4, self.nc), 1
  523. )
  524. # b, grids, ..
  525. pred_scores = pred_scores.permute(0, 2, 1).contiguous()
  526. pred_distri = pred_distri.permute(0, 2, 1).contiguous()
  527. pred_angle = pred_angle.permute(0, 2, 1).contiguous()
  528. dtype = pred_scores.dtype
  529. imgsz = torch.tensor(feats[0].shape[2:], device=self.device, dtype=dtype) * self.stride[0] # image size (h,w)
  530. anchor_points, stride_tensor = make_anchors(feats, self.stride, 0.5)
  531. # targets
  532. try:
  533. batch_idx = batch["batch_idx"].view(-1, 1)
  534. targets = torch.cat((batch_idx, batch["cls"].view(-1, 1), batch["bboxes"].view(-1, 5)), 1)
  535. rw, rh = targets[:, 4] * imgsz[0].item(), targets[:, 5] * imgsz[1].item()
  536. targets = targets[(rw >= 2) & (rh >= 2)] # filter rboxes of tiny size to stabilize training
  537. targets = self.preprocess(targets.to(self.device), batch_size, scale_tensor=imgsz[[1, 0, 1, 0]])
  538. gt_labels, gt_bboxes = targets.split((1, 5), 2) # cls, xywhr
  539. mask_gt = gt_bboxes.sum(2, keepdim=True).gt_(0.0)
  540. except RuntimeError as e:
  541. raise TypeError(
  542. "ERROR ❌ OBB dataset incorrectly formatted or not a OBB dataset.\n"
  543. "This error can occur when incorrectly training a 'OBB' model on a 'detect' dataset, "
  544. "i.e. 'yolo train model=yolov8n-obb.pt data=dota8.yaml'.\nVerify your dataset is a "
  545. "correctly formatted 'OBB' dataset using 'data=dota8.yaml' "
  546. "as an example.\nSee https://docs.ultralytics.com/datasets/obb/ for help."
  547. ) from e
  548. # Pboxes
  549. pred_bboxes = self.bbox_decode(anchor_points, pred_distri, pred_angle) # xyxy, (b, h*w, 4)
  550. bboxes_for_assigner = pred_bboxes.clone().detach()
  551. # Only the first four elements need to be scaled
  552. bboxes_for_assigner[..., :4] *= stride_tensor
  553. _, target_bboxes, target_scores, fg_mask, _ = self.assigner(
  554. pred_scores.detach().sigmoid(),
  555. bboxes_for_assigner.type(gt_bboxes.dtype),
  556. anchor_points * stride_tensor,
  557. gt_labels,
  558. gt_bboxes,
  559. mask_gt,
  560. )
  561. target_scores_sum = max(target_scores.sum(), 1)
  562. # Cls loss
  563. # loss[1] = self.varifocal_loss(pred_scores, target_scores, target_labels) / target_scores_sum # VFL way
  564. loss[1] = self.bce(pred_scores, target_scores.to(dtype)).sum() / target_scores_sum # BCE
  565. # Bbox loss
  566. if fg_mask.sum():
  567. target_bboxes[..., :4] /= stride_tensor
  568. loss[0], loss[2] = self.bbox_loss(
  569. pred_distri, pred_bboxes, anchor_points, target_bboxes, target_scores, target_scores_sum, fg_mask
  570. )
  571. else:
  572. loss[0] += (pred_angle * 0).sum()
  573. loss[0] *= self.hyp.box # box gain
  574. loss[1] *= self.hyp.cls # cls gain
  575. loss[2] *= self.hyp.dfl # dfl gain
  576. return loss.sum() * batch_size, loss.detach() # loss(box, cls, dfl)
  577. def bbox_decode(self, anchor_points, pred_dist, pred_angle):
  578. """
  579. Decode predicted object bounding box coordinates from anchor points and distribution.
  580. Args:
  581. anchor_points (torch.Tensor): Anchor points, (h*w, 2).
  582. pred_dist (torch.Tensor): Predicted rotated distance, (bs, h*w, 4).
  583. pred_angle (torch.Tensor): Predicted angle, (bs, h*w, 1).
  584. Returns:
  585. (torch.Tensor): Predicted rotated bounding boxes with angles, (bs, h*w, 5).
  586. """
  587. if self.use_dfl:
  588. b, a, c = pred_dist.shape # batch, anchors, channels
  589. pred_dist = pred_dist.view(b, a, 4, c // 4).softmax(3).matmul(self.proj.type(pred_dist.dtype))
  590. return torch.cat((dist2rbox(pred_dist, pred_angle, anchor_points), pred_angle), dim=-1)
  591. class E2EDetectLoss:
  592. """Criterion class for computing training losses."""
  593. def __init__(self, model):
  594. """Initialize E2EDetectLoss with one-to-many and one-to-one detection losses using the provided model."""
  595. self.one2many = v8DetectionLoss(model, tal_topk=10)
  596. self.one2one = v8DetectionLoss(model, tal_topk=1)
  597. def __call__(self, preds, batch):
  598. """Calculate the sum of the loss for box, cls and dfl multiplied by batch size."""
  599. preds = preds[1] if isinstance(preds, tuple) else preds
  600. one2many = preds["one2many"]
  601. loss_one2many = self.one2many(one2many, batch)
  602. one2one = preds["one2one"]
  603. loss_one2one = self.one2one(one2one, batch)
  604. return loss_one2many[0] + loss_one2one[0], loss_one2many[1] + loss_one2one[1]