loss.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. # Ultralytics YOLO 🚀, AGPL-3.0 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 TaskAlignedAssigner, dist2bbox, make_anchors
  8. from .metrics import bbox_iou
  9. from .tal import bbox2dist
  10. class VarifocalLoss(nn.Module):
  11. """Varifocal loss by Zhang et al. https://arxiv.org/abs/2008.13367."""
  12. def __init__(self):
  13. """Initialize the VarifocalLoss class."""
  14. super().__init__()
  15. def forward(self, pred_score, gt_score, label, alpha=0.75, gamma=2.0):
  16. """Computes varfocal loss."""
  17. weight = alpha * pred_score.sigmoid().pow(gamma) * (1 - label) + gt_score * label
  18. with torch.cuda.amp.autocast(enabled=False):
  19. loss = (F.binary_cross_entropy_with_logits(pred_score.float(), gt_score.float(), reduction='none') *
  20. weight).mean(1).sum()
  21. return loss
  22. # Losses
  23. class FocalLoss(nn.Module):
  24. """Wraps focal loss around existing loss_fcn(), i.e. criteria = FocalLoss(nn.BCEWithLogitsLoss(), gamma=1.5)."""
  25. def __init__(self, ):
  26. super().__init__()
  27. def forward(self, pred, label, gamma=1.5, alpha=0.25):
  28. """Calculates and updates confusion matrix for object detection/classification tasks."""
  29. loss = F.binary_cross_entropy_with_logits(pred, label, reduction='none')
  30. # p_t = torch.exp(-loss)
  31. # loss *= self.alpha * (1.000001 - p_t) ** self.gamma # non-zero power for gradient stability
  32. # TF implementation https://github.com/tensorflow/addons/blob/v0.7.1/tensorflow_addons/losses/focal_loss.py
  33. pred_prob = pred.sigmoid() # prob from logits
  34. p_t = label * pred_prob + (1 - label) * (1 - pred_prob)
  35. modulating_factor = (1.0 - p_t) ** gamma
  36. loss *= modulating_factor
  37. if alpha > 0:
  38. alpha_factor = label * alpha + (1 - label) * (1 - alpha)
  39. loss *= alpha_factor
  40. return loss.mean(1).sum()
  41. class BboxLoss(nn.Module):
  42. def __init__(self, reg_max, use_dfl=False):
  43. """Initialize the BboxLoss module with regularization maximum and DFL settings."""
  44. super().__init__()
  45. self.reg_max = reg_max
  46. self.use_dfl = use_dfl
  47. def forward(self, pred_dist, pred_bboxes, anchor_points, target_bboxes, target_scores, target_scores_sum, fg_mask):
  48. """IoU loss."""
  49. weight = target_scores.sum(-1)[fg_mask].unsqueeze(-1)
  50. iou = bbox_iou(pred_bboxes[fg_mask], target_bboxes[fg_mask], xywh=False, CIoU=True)
  51. loss_iou = ((1.0 - iou) * weight).sum() / target_scores_sum
  52. # DFL loss
  53. if self.use_dfl:
  54. target_ltrb = bbox2dist(anchor_points, target_bboxes, self.reg_max)
  55. loss_dfl = self._df_loss(pred_dist[fg_mask].view(-1, self.reg_max + 1), target_ltrb[fg_mask]) * weight
  56. loss_dfl = loss_dfl.sum() / target_scores_sum
  57. else:
  58. loss_dfl = torch.tensor(0.0).to(pred_dist.device)
  59. return loss_iou, loss_dfl
  60. @staticmethod
  61. def _df_loss(pred_dist, target):
  62. """Return sum of left and right DFL losses."""
  63. # Distribution Focal Loss (DFL) proposed in Generalized Focal Loss https://ieeexplore.ieee.org/document/9792391
  64. tl = target.long() # target left
  65. tr = tl + 1 # target right
  66. wl = tr - target # weight left
  67. wr = 1 - wl # weight right
  68. return (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).mean(-1, keepdim=True)
  70. class KeypointLoss(nn.Module):
  71. def __init__(self, sigmas) -> None:
  72. super().__init__()
  73. self.sigmas = sigmas
  74. def forward(self, pred_kpts, gt_kpts, kpt_mask, area):
  75. """Calculates keypoint loss factor and Euclidean distance loss for predicted and actual keypoints."""
  76. d = (pred_kpts[..., 0] - gt_kpts[..., 0]) ** 2 + (pred_kpts[..., 1] - gt_kpts[..., 1]) ** 2
  77. kpt_loss_factor = (torch.sum(kpt_mask != 0) + torch.sum(kpt_mask == 0)) / (torch.sum(kpt_mask != 0) + 1e-9)
  78. # e = d / (2 * (area * self.sigmas) ** 2 + 1e-9) # from formula
  79. e = d / (2 * self.sigmas) ** 2 / (area + 1e-9) / 2 # from cocoeval
  80. return kpt_loss_factor * ((1 - torch.exp(-e)) * kpt_mask).mean()
  81. # Criterion class for computing Detection training losses
  82. class v8DetectionLoss:
  83. def __init__(self, model): # model must be de-paralleled
  84. device = next(model.parameters()).device # get model device
  85. h = model.args # hyperparameters
  86. m = model.model[-1] # Detect() module
  87. self.bce = nn.BCEWithLogitsLoss(reduction='none')
  88. self.hyp = h
  89. self.stride = m.stride # model strides
  90. self.nc = m.nc # number of classes
  91. self.no = m.no
  92. self.reg_max = m.reg_max
  93. self.device = device
  94. self.use_dfl = m.reg_max > 1
  95. self.assigner = TaskAlignedAssigner(topk=10, num_classes=self.nc, alpha=0.5, beta=6.0)
  96. self.bbox_loss = BboxLoss(m.reg_max - 1, use_dfl=self.use_dfl).to(device)
  97. self.proj = torch.arange(m.reg_max, dtype=torch.float, device=device)
  98. def preprocess(self, targets, batch_size, scale_tensor):
  99. """Preprocesses the target counts and matches with the input batch size to output a tensor."""
  100. if targets.shape[0] == 0:
  101. out = torch.zeros(batch_size, 0, 5, device=self.device)
  102. else:
  103. i = targets[:, 0] # image index
  104. _, counts = i.unique(return_counts=True)
  105. counts = counts.to(dtype=torch.int32)
  106. out = torch.zeros(batch_size, counts.max(), 5, device=self.device)
  107. for j in range(batch_size):
  108. matches = i == j
  109. n = matches.sum()
  110. if n:
  111. out[j, :n] = targets[matches, 1:]
  112. out[..., 1:5] = xywh2xyxy(out[..., 1:5].mul_(scale_tensor))
  113. return out
  114. def bbox_decode(self, anchor_points, pred_dist):
  115. """Decode predicted object bounding box coordinates from anchor points and distribution."""
  116. if self.use_dfl:
  117. b, a, c = pred_dist.shape # batch, anchors, channels
  118. pred_dist = pred_dist.view(b, a, 4, c // 4).softmax(3).matmul(self.proj.type(pred_dist.dtype))
  119. # pred_dist = pred_dist.view(b, a, c // 4, 4).transpose(2,3).softmax(3).matmul(self.proj.type(pred_dist.dtype))
  120. # 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)
  121. return dist2bbox(pred_dist, anchor_points, xywh=False)
  122. def __call__(self, preds, batch):
  123. """Calculate the sum of the loss for box, cls and dfl multiplied by batch size."""
  124. loss = torch.zeros(3, device=self.device) # box, cls, dfl
  125. feats = preds[1] if isinstance(preds, tuple) else preds
  126. pred_distri, pred_scores = torch.cat([xi.view(feats[0].shape[0], self.no, -1) for xi in feats], 2).split(
  127. (self.reg_max * 4, self.nc), 1)
  128. pred_scores = pred_scores.permute(0, 2, 1).contiguous()
  129. pred_distri = pred_distri.permute(0, 2, 1).contiguous()
  130. dtype = pred_scores.dtype
  131. batch_size = pred_scores.shape[0]
  132. imgsz = torch.tensor(feats[0].shape[2:], device=self.device, dtype=dtype) * self.stride[0] # image size (h,w)
  133. anchor_points, stride_tensor = make_anchors(feats, self.stride, 0.5)
  134. # targets
  135. targets = torch.cat((batch['batch_idx'].view(-1, 1), batch['cls'].view(-1, 1), batch['bboxes']), 1)
  136. targets = self.preprocess(targets.to(self.device), batch_size, scale_tensor=imgsz[[1, 0, 1, 0]])
  137. gt_labels, gt_bboxes = targets.split((1, 4), 2) # cls, xyxy
  138. mask_gt = gt_bboxes.sum(2, keepdim=True).gt_(0)
  139. # pboxes
  140. pred_bboxes = self.bbox_decode(anchor_points, pred_distri) # xyxy, (b, h*w, 4)
  141. _, target_bboxes, target_scores, fg_mask, _ = self.assigner(
  142. pred_scores.detach().sigmoid(), (pred_bboxes.detach() * stride_tensor).type(gt_bboxes.dtype),
  143. anchor_points * stride_tensor, gt_labels, gt_bboxes, mask_gt)
  144. target_scores_sum = max(target_scores.sum(), 1)
  145. # cls loss
  146. # loss[1] = self.varifocal_loss(pred_scores, target_scores, target_labels) / target_scores_sum # VFL way
  147. loss[1] = self.bce(pred_scores, target_scores.to(dtype)).sum() / target_scores_sum # BCE
  148. # bbox loss
  149. if fg_mask.sum():
  150. target_bboxes /= stride_tensor
  151. loss[0], loss[2] = self.bbox_loss(pred_distri, pred_bboxes, anchor_points, target_bboxes, target_scores,
  152. target_scores_sum, fg_mask)
  153. loss[0] *= self.hyp.box # box gain
  154. loss[1] *= self.hyp.cls # cls gain
  155. loss[2] *= self.hyp.dfl # dfl gain
  156. return loss.sum() * batch_size, loss.detach() # loss(box, cls, dfl)
  157. # Criterion class for computing training losses
  158. class v8SegmentationLoss(v8DetectionLoss):
  159. def __init__(self, model): # model must be de-paralleled
  160. super().__init__(model)
  161. self.nm = model.model[-1].nm # number of masks
  162. self.overlap = model.args.overlap_mask
  163. def __call__(self, preds, batch):
  164. """Calculate and return the loss for the YOLO model."""
  165. loss = torch.zeros(4, device=self.device) # box, cls, dfl
  166. feats, pred_masks, proto = preds if len(preds) == 3 else preds[1]
  167. batch_size, _, mask_h, mask_w = proto.shape # batch size, number of masks, mask height, mask width
  168. pred_distri, pred_scores = torch.cat([xi.view(feats[0].shape[0], self.no, -1) for xi in feats], 2).split(
  169. (self.reg_max * 4, self.nc), 1)
  170. # b, grids, ..
  171. pred_scores = pred_scores.permute(0, 2, 1).contiguous()
  172. pred_distri = pred_distri.permute(0, 2, 1).contiguous()
  173. pred_masks = pred_masks.permute(0, 2, 1).contiguous()
  174. dtype = pred_scores.dtype
  175. imgsz = torch.tensor(feats[0].shape[2:], device=self.device, dtype=dtype) * self.stride[0] # image size (h,w)
  176. anchor_points, stride_tensor = make_anchors(feats, self.stride, 0.5)
  177. # targets
  178. try:
  179. batch_idx = batch['batch_idx'].view(-1, 1)
  180. targets = torch.cat((batch_idx, batch['cls'].view(-1, 1), batch['bboxes']), 1)
  181. targets = self.preprocess(targets.to(self.device), batch_size, scale_tensor=imgsz[[1, 0, 1, 0]])
  182. gt_labels, gt_bboxes = targets.split((1, 4), 2) # cls, xyxy
  183. mask_gt = gt_bboxes.sum(2, keepdim=True).gt_(0)
  184. except RuntimeError as e:
  185. raise TypeError('ERROR ❌ segment dataset incorrectly formatted or not a segment dataset.\n'
  186. "This error can occur when incorrectly training a 'segment' model on a 'detect' dataset, "
  187. "i.e. 'yolo train model=yolov8n-seg.pt data=coco128.yaml'.\nVerify your dataset is a "
  188. "correctly formatted 'segment' dataset using 'data=coco128-seg.yaml' "
  189. 'as an example.\nSee https://docs.ultralytics.com/tasks/segment/ for help.') from e
  190. # pboxes
  191. pred_bboxes = self.bbox_decode(anchor_points, pred_distri) # xyxy, (b, h*w, 4)
  192. _, target_bboxes, target_scores, fg_mask, target_gt_idx = self.assigner(
  193. pred_scores.detach().sigmoid(), (pred_bboxes.detach() * stride_tensor).type(gt_bboxes.dtype),
  194. anchor_points * stride_tensor, gt_labels, gt_bboxes, mask_gt)
  195. target_scores_sum = max(target_scores.sum(), 1)
  196. # cls loss
  197. # loss[1] = self.varifocal_loss(pred_scores, target_scores, target_labels) / target_scores_sum # VFL way
  198. loss[2] = self.bce(pred_scores, target_scores.to(dtype)).sum() / target_scores_sum # BCE
  199. if fg_mask.sum():
  200. # bbox loss
  201. loss[0], loss[3] = self.bbox_loss(pred_distri, pred_bboxes, anchor_points, target_bboxes / stride_tensor,
  202. target_scores, target_scores_sum, fg_mask)
  203. # masks loss
  204. masks = batch['masks'].to(self.device).float()
  205. if tuple(masks.shape[-2:]) != (mask_h, mask_w): # downsample
  206. masks = F.interpolate(masks[None], (mask_h, mask_w), mode='nearest')[0]
  207. for i in range(batch_size):
  208. if fg_mask[i].sum():
  209. mask_idx = target_gt_idx[i][fg_mask[i]]
  210. if self.overlap:
  211. gt_mask = torch.where(masks[[i]] == (mask_idx + 1).view(-1, 1, 1), 1.0, 0.0)
  212. else:
  213. gt_mask = masks[batch_idx.view(-1) == i][mask_idx]
  214. xyxyn = target_bboxes[i][fg_mask[i]] / imgsz[[1, 0, 1, 0]]
  215. marea = xyxy2xywh(xyxyn)[:, 2:].prod(1)
  216. mxyxy = xyxyn * torch.tensor([mask_w, mask_h, mask_w, mask_h], device=self.device)
  217. loss[1] += self.single_mask_loss(gt_mask, pred_masks[i][fg_mask[i]], proto[i], mxyxy, marea) # seg
  218. # WARNING: lines below prevents Multi-GPU DDP 'unused gradient' PyTorch errors, do not remove
  219. else:
  220. loss[1] += (proto * 0).sum() + (pred_masks * 0).sum() # inf sums may lead to nan loss
  221. # WARNING: lines below prevent Multi-GPU DDP 'unused gradient' PyTorch errors, do not remove
  222. else:
  223. loss[1] += (proto * 0).sum() + (pred_masks * 0).sum() # inf sums may lead to nan loss
  224. loss[0] *= self.hyp.box # box gain
  225. loss[1] *= self.hyp.box / batch_size # seg gain
  226. loss[2] *= self.hyp.cls # cls gain
  227. loss[3] *= self.hyp.dfl # dfl gain
  228. return loss.sum() * batch_size, loss.detach() # loss(box, cls, dfl)
  229. def single_mask_loss(self, gt_mask, pred, proto, xyxy, area):
  230. """Mask loss for one image."""
  231. pred_mask = (pred @ proto.view(self.nm, -1)).view(-1, *proto.shape[1:]) # (n, 32) @ (32,80,80) -> (n,80,80)
  232. loss = F.binary_cross_entropy_with_logits(pred_mask, gt_mask, reduction='none')
  233. return (crop_mask(loss, xyxy).mean(dim=(1, 2)) / area).mean()
  234. # Criterion class for computing training losses
  235. class v8PoseLoss(v8DetectionLoss):
  236. def __init__(self, model): # model must be de-paralleled
  237. super().__init__(model)
  238. self.kpt_shape = model.model[-1].kpt_shape
  239. self.bce_pose = nn.BCEWithLogitsLoss()
  240. is_pose = self.kpt_shape == [17, 3]
  241. nkpt = self.kpt_shape[0] # number of keypoints
  242. sigmas = torch.from_numpy(OKS_SIGMA).to(self.device) if is_pose else torch.ones(nkpt, device=self.device) / nkpt
  243. self.keypoint_loss = KeypointLoss(sigmas=sigmas)
  244. def __call__(self, preds, batch):
  245. """Calculate the total loss and detach it."""
  246. loss = torch.zeros(5, device=self.device) # box, cls, dfl, kpt_location, kpt_visibility
  247. feats, pred_kpts = preds if isinstance(preds[0], list) else preds[1]
  248. pred_distri, pred_scores = torch.cat([xi.view(feats[0].shape[0], self.no, -1) for xi in feats], 2).split(
  249. (self.reg_max * 4, self.nc), 1)
  250. # b, grids, ..
  251. pred_scores = pred_scores.permute(0, 2, 1).contiguous()
  252. pred_distri = pred_distri.permute(0, 2, 1).contiguous()
  253. pred_kpts = pred_kpts.permute(0, 2, 1).contiguous()
  254. dtype = pred_scores.dtype
  255. imgsz = torch.tensor(feats[0].shape[2:], device=self.device, dtype=dtype) * self.stride[0] # image size (h,w)
  256. anchor_points, stride_tensor = make_anchors(feats, self.stride, 0.5)
  257. # targets
  258. batch_size = pred_scores.shape[0]
  259. batch_idx = batch['batch_idx'].view(-1, 1)
  260. targets = torch.cat((batch_idx, batch['cls'].view(-1, 1), batch['bboxes']), 1)
  261. targets = self.preprocess(targets.to(self.device), batch_size, scale_tensor=imgsz[[1, 0, 1, 0]])
  262. gt_labels, gt_bboxes = targets.split((1, 4), 2) # cls, xyxy
  263. mask_gt = gt_bboxes.sum(2, keepdim=True).gt_(0)
  264. # pboxes
  265. pred_bboxes = self.bbox_decode(anchor_points, pred_distri) # xyxy, (b, h*w, 4)
  266. pred_kpts = self.kpts_decode(anchor_points, pred_kpts.view(batch_size, -1, *self.kpt_shape)) # (b, h*w, 17, 3)
  267. _, target_bboxes, target_scores, fg_mask, target_gt_idx = self.assigner(
  268. pred_scores.detach().sigmoid(), (pred_bboxes.detach() * stride_tensor).type(gt_bboxes.dtype),
  269. anchor_points * stride_tensor, gt_labels, gt_bboxes, mask_gt)
  270. target_scores_sum = max(target_scores.sum(), 1)
  271. # cls loss
  272. # loss[1] = self.varifocal_loss(pred_scores, target_scores, target_labels) / target_scores_sum # VFL way
  273. loss[3] = self.bce(pred_scores, target_scores.to(dtype)).sum() / target_scores_sum # BCE
  274. # bbox loss
  275. if fg_mask.sum():
  276. target_bboxes /= stride_tensor
  277. loss[0], loss[4] = self.bbox_loss(pred_distri, pred_bboxes, anchor_points, target_bboxes, target_scores,
  278. target_scores_sum, fg_mask)
  279. keypoints = batch['keypoints'].to(self.device).float().clone()
  280. keypoints[..., 0] *= imgsz[1]
  281. keypoints[..., 1] *= imgsz[0]
  282. for i in range(batch_size):
  283. if fg_mask[i].sum():
  284. idx = target_gt_idx[i][fg_mask[i]]
  285. gt_kpt = keypoints[batch_idx.view(-1) == i][idx] # (n, 51)
  286. gt_kpt[..., 0] /= stride_tensor[fg_mask[i]]
  287. gt_kpt[..., 1] /= stride_tensor[fg_mask[i]]
  288. area = xyxy2xywh(target_bboxes[i][fg_mask[i]])[:, 2:].prod(1, keepdim=True)
  289. pred_kpt = pred_kpts[i][fg_mask[i]]
  290. kpt_mask = gt_kpt[..., 2] != 0
  291. loss[1] += self.keypoint_loss(pred_kpt, gt_kpt, kpt_mask, area) # pose loss
  292. # kpt_score loss
  293. if pred_kpt.shape[-1] == 3:
  294. loss[2] += self.bce_pose(pred_kpt[..., 2], kpt_mask.float()) # keypoint obj loss
  295. loss[0] *= self.hyp.box # box gain
  296. loss[1] *= self.hyp.pose / batch_size # pose gain
  297. loss[2] *= self.hyp.kobj / batch_size # kobj gain
  298. loss[3] *= self.hyp.cls # cls gain
  299. loss[4] *= self.hyp.dfl # dfl gain
  300. return loss.sum() * batch_size, loss.detach() # loss(box, cls, dfl)
  301. def kpts_decode(self, anchor_points, pred_kpts):
  302. """Decodes predicted keypoints to image coordinates."""
  303. y = pred_kpts.clone()
  304. y[..., :2] *= 2.0
  305. y[..., 0] += anchor_points[:, [0]] - 0.5
  306. y[..., 1] += anchor_points[:, [1]] - 0.5
  307. return y
  308. class v8ClassificationLoss:
  309. def __call__(self, preds, batch):
  310. """Compute the classification loss between predictions and true labels."""
  311. loss = torch.nn.functional.cross_entropy(preds, batch['cls'], reduction='sum') / 64
  312. loss_items = loss.detach()
  313. return loss, loss_items