loss_tal_dual.py
utils\loss_tal_dual.py
目录
[2.def smooth_BCE(eps=0.1):](#2.def smooth_BCE(eps=0.1):)
[3.class VarifocalLoss(nn.Module):](#3.class VarifocalLoss(nn.Module):)
[4.class FocalLoss(nn.Module):](#4.class FocalLoss(nn.Module):)
[5.class BboxLoss(nn.Module):](#5.class BboxLoss(nn.Module):)
[6.class ComputeLoss:](#6.class ComputeLoss:)
[7.class ComputeLossLH:](#7.class ComputeLossLH:)
1.所需的库和模块
python
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
2.def smooth_BCE(eps=0.1):
python
# 这段代码定义了一个名为 smooth_BCE 的函数,它用于计算平滑的二元交叉熵损失(Binary Cross-Entropy, BCE)的参数。在机器学习中,二元交叉熵损失函数常用于二分类问题,它衡量的是模型预测的概率分布与真实标签之间的差异。
# 定义了一个函数 smooth_BCE ,它接受一个参数。
# 1.eps :其默认值为 0.1 。这个参数 eps 用于控制平滑的程度。
def smooth_BCE(eps=0.1): # https://github.com/ultralytics/yolov3/issues/238#issuecomment-598028441
# return positive, negative label smoothing BCE targets
# 函数返回两个值。
# 1.0 - 0.5 * eps :这是平滑后的正类(positive class)损失的参数,通常用于计算正样本的损失。
# 0.5 * eps :这是平滑后的负类(negative class)损失的参数,通常用于计算负样本的损失。
return 1.0 - 0.5 * eps, 0.5 * eps
# 在原始的二元交叉熵损失函数中,当预测概率 p 接近0或1时,损失函数的梯度会接近0,这会导致模型在训练过程中难以学习。通过引入 eps 进行平滑,可以使得梯度在这些极端情况下保持一定的非零值,从而改善模型的训练效果。
# 这个函数通过调整 eps 参数,提供了一种平滑二元交叉熵损失函数的方法,有助于改善模型在训练过程中的表现。
3.class VarifocalLoss(nn.Module):
python
# 这段代码定义了一个名为 VarifocalLoss 的类,它是一个 PyTorch 的 nn.Module 子类,用于实现变分焦点损失(Varifocal Loss)。这种损失函数通常用于目标检测任务中,特别是在类别不平衡的情况下。
# 定义了一个名为 VarifocalLoss 的新类,它继承自 PyTorch 的 nn.Module 类。 nn.Module 是所有神经网络模块的基类。
class VarifocalLoss(nn.Module):
# Varifocal loss by Zhang et al. https://arxiv.org/abs/2008.13367 Zhang 等人的变焦损失 https://arxiv.org/abs/2008.13367 。
# VarifocalLoss 类的构造函数。它调用了父类 nn.Module 的构造函数,这是初始化模块时的标准做法。
def __init__(self):
super().__init__()
# VarifocalLoss 类的 forward 方法,它是 PyTorch 模块的前向传播方法。这个方法接受以下参数 :
# 1.pred_score :模型预测的类别分数(logits)。
# 2.gt_score :真实的类别分数(通常是0或1)。
# 3.label :真实的标签,指示每个样本是否为目标类别。
# 4.alpha :一个超参数,用于调整 正负样本 的权重 ,默认值为0.75。
# 5.gamma :另一个超参数,用于调整模型对 难易样本 的关注程度 ,默认值为2.0。
def forward(self, pred_score, gt_score, label, alpha=0.75, gamma=2.0):
# 计算了每个样本的权重。权重的计算考虑了 预测分数 、 真实标签 和 两个超参数 alpha 和 gamma 。对于非目标样本( label 为0),权重与 预测分数 的sigmoid函数和 gamma 次幂成正比;对于目标样本( label 为1),权重与 真实分数 成正比。
weight = alpha * pred_score.sigmoid().pow(gamma) * (1 - label) + gt_score * label
# 使用了 PyTorch 的自动混合精度(Automatic Mixed Precision, AMP)上下文管理器 autocast ,但将 enabled 参数设置为 False 。这是因为二元交叉熵损失函数 binary_cross_entropy_with_logits 不支持自动混合精度。
with torch.cuda.amp.autocast(enabled=False):
# torch.nn.functional.binary_cross_entropy_with_logits(input, target, weight=None, pos_weight=None, reduction='mean')
# F.binary_cross_entropy_with_logits 是 PyTorch 中的一个函数,它计算二元交叉熵损失(Binary Cross Entropy Loss),这个损失函数适用于二分类问题。该函数结合了 Sigmoid 激活函数和二元交叉熵损失计算,使得它在数值上更加稳定,并且减少了计算步骤。
# 参数 :
# input :模型输出的 logits(即未经 Sigmoid 激活的原始输出),形状为 (N, *) ,其中 N 是批次大小, * 表示任意数量的附加维度。
# target :真实标签,形状与 input 相同,值在 [0, 1] 范围内。
# weight :每个样本的权重,可以用来处理不平衡数据集,形状为 (N, *) 。
# pos_weight :正样本的权重,用于处理不平衡数据集中正样本较少的情况,形状为 (1,) 。
# reduction :指定如何应用损失的缩减。可选的值为 'none' 、 'mean' 、 'sum' 。默认为 'mean' ,表示计算所有损失的平均值。
# 返回值 :
# 返回一个标量或张量,取决于 reduction 参数的设置。
# 特点 :
# 内部 Sigmoid : F.binary_cross_entropy_with_logits 在计算损失之前,内部自动应用 Sigmoid 函数将 logits 转换为概率值,因此不需要在外部手动应用 Sigmoid。
# 数值稳定性 :由于结合了 Sigmoid 和损失计算,该函数利用了 log-sum-exp 技巧来提高数值稳定性,这在处理极端值时尤为重要。
# 这个函数是处理二分类问题时的推荐选择,因为它减少了手动应用 Sigmoid 激活的步骤,并且提供了更好的数值稳定性。
# 计算损失值。首先,使用 F.binary_cross_entropy_with_logits 计算每个样本的二元交叉熵损失,然后乘以之前计算的权重。 reduction="none" 参数表示不对损失进行求和或平均,而是返回每个样本的损失。最后,通过对所有样本的加权损失求和来得到总损失。
loss = (F.binary_cross_entropy_with_logits(pred_score.float(), gt_score.float(),
reduction="none") * weight).sum()
# 返回计算出的总损失值。
return loss
# VarifocalLoss 类实现了一个变分焦点损失函数,它通过调整正负样本的权重和关注难易样本的程度,来提高模型在类别不平衡情况下的性能。
4.class FocalLoss(nn.Module):
python
# 这段代码定义了一个名为 FocalLoss 的类,它是一个 PyTorch 的 nn.Module 子类,用于实现焦点损失(Focal Loss)。焦点损失是一种专门为类别不平衡问题设计的损失函数,它通过减少易分类样本的权重并增加难分类样本的权重来改进模型性能。
# 定义了一个名为 FocalLoss 的新类,它继承自 PyTorch 的 nn.Module 类。
class FocalLoss(nn.Module):
# Wraps focal loss around existing loss_fcn(), i.e. criteria = FocalLoss(nn.BCEWithLogitsLoss(), gamma=1.5) 将 焦点损失 包裹在现有的 loss_fcn() 周围,即 criteria = FocalLoss(nn.BCEWithLogitsLoss(),gamma=1.5) 。
# 这段代码是 FocalLoss 类的构造函数 __init__ 的定义,它初始化类的实例变量。
# 定义了 FocalLoss 类的 构造函数,它接受三个参数。
# 1.self :这是一个指向类实例自身的引用,是实例方法的第一个参数。
# 2.loss_fcn :这是一个损失函数对象,通常是 nn.BCEWithLogitsLoss() ,用于计算基本的二元交叉熵损失。
# 3.gamma :这是一个浮点数,表示焦点损失中的 聚焦参数 ,默认值为1.5。
# 4.alpha :这是一个浮点数,表示 类别不平衡 的权重参数,默认值为0.25。
def __init__(self, loss_fcn, gamma=1.5, alpha=0.25):
# 调用了父类 nn.Module 的构造函数,以确保正确地初始化 FocalLoss 类的基类部分。
super().__init__()
# 将传入的 loss_fcn 参数保存为类的实例变量。这个变量必须是一个 nn.BCEWithLogitsLoss 对象,因为 FocalLoss 需要基于这个基本损失函数来计算焦点损失。
self.loss_fcn = loss_fcn # must be nn.BCEWithLogitsLoss()
# 将 gamma 参数保存为类的实例变量,这个变量用于调整焦点损失中 易分类样本的权重 。
self.gamma = gamma
# 将 alpha 参数保存为类的实例变量,这个变量用于平衡类别不平衡问题,给 正负样本分配不同的权重 。
self.alpha = alpha
# 保存了传入的 loss_fcn 对象的 reduction 属性,这个属性决定了损失函数的输出是平均值、总和还是每个样本的损失。
self.reduction = loss_fcn.reduction
# 将 loss_fcn 对象的 reduction 属性设置为 "none" 。这是因为焦点损失需要对每个样本单独计算,然后再应用 alpha 和 gamma 参数进行调整。设置为 "none" 确保基本损失函数不会对损失值进行求和或平均,而是返回每个样本的损失,这样焦点损失就可以逐元素地应用。
self.loss_fcn.reduction = "none" # required to apply FL to each element
# 这段代码初始化了 FocalLoss 类,设置了其基本属性,并调整了内部损失函数的行为,以便能够对每个样本单独计算焦点损失。这样做可以确保焦点损失能够有效地调整易分类和难分类样本的权重,从而提高模型在处理类别不平衡问题时的性能。
# 这段代码是 FocalLoss 类的 forward 方法的定义,它实现了焦点损失(Focal Loss)的计算逻辑。
# 定义了 FocalLoss 类的 forward 方法,它是 PyTorch 模型中进行前向传播的函数。这个方法接受两个参数。
# 1.pred :模型预测的 logits 。
# 2.true :真实的标签。
def forward(self, pred, true):
# 调用了在构造函数中设置的基础损失函数 self.loss_fcn ,传入预测值和真实值,计算出基本的二元交叉熵损失。
loss = self.loss_fcn(pred, true)
# 这两行代码被注释掉了,它们是另一种计算焦点损失的方式,通过指数函数和幂运算来调整损失。这里使用 1.000001 来避免数值计算中的下溢问题。
# 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 TF 实现 https://github.com/tensorflow/addons/blob/v0.7.1/tensorflow_addons/losses/focal_loss.py 。
# 将预测的 logits 通过 sigmoid 函数转换为概率值,这些概率值表示模型预测正类的概率。
pred_prob = torch.sigmoid(pred) # prob from logits
# 计算 p_t ,它是一个根据真实标签调整的预测概率。如果真实标签为正( true 为 1),则 p_t 等于预测概率;如果真实标签为负( true 为 0),则 p_t 等于 1 - pred_prob 。
p_t = true * pred_prob + (1 - true) * (1 - pred_prob)
# 计算 alpha_factor ,它是一个根据真实标签调整的 alpha 值。如果真实标签为正,则 alpha_factor 等于 alpha ;如果真实标签为负,则 alpha_factor 等于 1 - alpha 。
alpha_factor = true * self.alpha + (1 - true) * (1 - self.alpha)
# 计算 modulating_factor ,它是一个调节因子,用于根据预测的准确性调整损失。 gamma 参数控制这个调节因子的强度。
modulating_factor = (1.0 - p_t) ** self.gamma
# 将基本损失乘以 alpha_factor 和 modulating_factor ,从而得到焦点损失。
loss *= alpha_factor * modulating_factor
# 根据 self.reduction 的值决定如何返回损失。
# 如果设置为 "mean",则返回损失的平均值。
if self.reduction == "mean":
return loss.mean()
# 如果设置为 "sum",则返回损失的总和。
elif self.reduction == "sum":
return loss.sum()
# 如果设置为 "none",则返回每个样本的损失。
else: # 'none'
return loss
# forward 方法实现了焦点损失的计算,它首先计算基本的二元交叉熵损失,然后通过 sigmoid 函数将 logits 转换为概率值。接着,它计算了一个调节因子,该因子基于预测的准确性和 gamma 参数来调整损失。最后,它将基本损失乘以 alpha_factor 和 modulating_factor 来得到焦点损失,并根据 reduction 参数返回损失的平均值、总和或每个样本的损失。这种方法特别适用于处理类别不平衡问题,因为它减少了易分类样本的权重,同时增加了难分类样本的权重。
# FocalLoss 类实现了焦点损失函数,它通过调整易分类和难分类样本的权重,来提高模型在类别不平衡情况下的性能。
5.class BboxLoss(nn.Module):
python
# 这段代码定义了一个名为 BboxLoss 的类,它是一个 PyTorch 的 nn.Module 子类,用于计算目标检测任务中的边界框损失。这个类包含了 IoU 损失和可选的 DFL(Distribution Focal Loss)损失。
# 定义了一个名为 BboxLoss 的新类,继承自 nn.Module 。
class BboxLoss(nn.Module):
# 这段代码是 BboxLoss 类的构造函数 __init__ 的定义,它用于初始化类的实例。
# 定义了 BboxLoss 类的构造函数,它接受两个参数。
# 1.reg_max :一个整数参数,表示在边界框回归任务中使用的最大值。这个参数通常用于定义预测边界框时的数值范围。
# 2.use_dfl :一个布尔参数,指示是否使用分布焦点损失(Distribution Focal Loss,简称 DFL)。默认值为 False ,意味着如果不显式指定,DFL 将不被使用。
def __init__(self, reg_max, use_dfl=False):
# 调用了父类 nn.Module 的构造函数,这是 Python 中类的初始化标准做法,确保 BboxLoss 类正确地继承了 nn.Module 的所有属性和方法。
super().__init__()
# 将传入的 reg_max 参数保存为类的实例变量 self.reg_max ,这样在类的其他方法中就可以访问这个值。
self.reg_max = reg_max
# 将传入的 use_dfl 参数保存为类的实例变量 self.use_dfl ,这样可以根据这个值决定是否启用 DFL 损失。
self.use_dfl = use_dfl
# 构造函数 __init__ 的主要作用是初始化 BboxLoss 类的实例,设置 reg_max 和 use_dfl 两个重要的配置参数,这些参数将影响类的 forward 方法中损失计算的行为。通过这种方式, BboxLoss 类可以灵活地适应不同的目标检测任务需求。
# 这段代码是 BboxLoss 类的 forward 方法,它负责计算边界框损失,包括 IoU 损失和可选的 DFL 损失。
# 定义了 BboxLoss 类的 forward 方法,它是 PyTorch 模型中进行前向传播的函数。这个方法接受以下参数 :
# 1.pred_dist :预测的距离分布。
# 2.pred_bboxes :预测的边界框。
# 3.anchor_points :锚点。
# 4.target_bboxes :目标边界框。
# 5.target_scores :目标分数。
# 6.target_scores_sum :目标分数的总和。
# 7.fg_mask :前景掩码,用于区分前景和背景。
def forward(self, pred_dist, pred_bboxes, anchor_points, target_bboxes, target_scores, target_scores_sum, fg_mask):
# 这段代码是 BboxLoss 类的 forward 方法中计算 IoU(交并比)损失的部分。
# iou loss
# 将前景掩码 fg_mask 在最后一个维度上扩展( unsqueeze ),使其从形状 (b, h*w) 变为 (b, h*w, 1) ,然后重复四次,以匹配边界框的四个坐标值,最终形状变为 (b, h*w, 4) 。
bbox_mask = fg_mask.unsqueeze(-1).repeat([1, 1, 4]) # (b, h*w, 4)
# torch.masked_select(input, mask, out=None)
# torch.masked_select 是 PyTorch 中的一个函数,它根据布尔掩码(mask)从输入张量中选择元素。
# 参数 :
# input (Tensor) :需要进行索引操作的输入张量。
# mask (BoolTensor) :要进行索引的布尔掩码,其形状必须与输入张量 input 相同,或者可以广播到输入张量的形状。
# out (Tensor, optional) :指定输出的张量。如果提供,输出结果将被存储在这个张量中,否则将创建一个新的张量来存储结果。
# 功能描述 :
# torch.masked_select 函数返回一个一维张量,其中包含所有在 mask 中对应位置为 True 的 input 张量中的元素。如果 mask 中的元素为 False ,则对应的 input 中的元素不会被选中。
# 返回值 :
# 一个一维张量,包含从输入张量中根据掩码选择的元素。
# 注意事项 :
# 掩码 mask 必须是一个布尔张量,其数据类型为 torch.bool 。
# 掩码 mask 的形状必须与输入张量 input 相同,或者可以广播到输入张量的形状。
# 如果 out 参数被提供,其数据类型必须与 input 中被选中的元素的数据类型相同。
# torch.masked_select 在深度学习中应用广泛,特别是在处理需要根据条件选择数据的场景,如在计算损失函数时选择特定的预测结果,或者在数据预处理时过滤无效的数据等。
# 使用 bbox_mask 从 pred_bboxes 中选择出前景的预测边界框,然后重塑为 (n, 4) 的形状,其中 n 是前景边界框的数量。
pred_bboxes_pos = torch.masked_select(pred_bboxes, bbox_mask).view(-1, 4)
# 使用 bbox_mask 从 target_bboxes 中选择出前景的目标边界框,然后重塑为 (n, 4) 的形状。
target_bboxes_pos = torch.masked_select(target_bboxes, bbox_mask).view(-1, 4)
# 从 target_scores 中选择出前景的分数,并计算总和,然后使用 fg_mask 选择出前景的权重,并在最后一个维度上扩展,使其形状变为 (n, 1) 。
bbox_weight = torch.masked_select(target_scores.sum(-1), fg_mask).unsqueeze(-1)
# 计算预测边界框 pred_bboxes_pos 和目标边界框 target_bboxes_pos 之间的 IoU。这里指定 xywh=False 表示边界框格式不是 (x, y, w, h) 而是 (x1, y1, x2, y2) 。 CIoU=True 表示使用改进的 IoU 损失,即 Complete IoU 损失。
# def bbox_iou(box1, box2, xywh=True, GIoU=False, DIoU=False, CIoU=False, MDPIoU=False, feat_h=640, feat_w=640, eps=1e-7):
# -> 用于计算两个边界框之间的交并比(IoU)以及其变体,如GIoU、DIoU、CIoU和MDPIoU。
# -> return iou - (rho2 / c2 + v * alpha) # CIoU return iou - rho2 / c2 # DIoU return iou - (c_area - union) / c_area # GIoU return iou - d1 / mpdiou_hw_pow - d2 / mpdiou_hw_pow # MPDIoU return iou # IoU
iou = bbox_iou(pred_bboxes_pos, target_bboxes_pos, xywh=False, CIoU=True)
# 计算 IoU 损失,即 1 减去 IoU 值。
loss_iou = 1.0 - iou
# 将 IoU 损失乘以 边界框的权重 。
loss_iou *= bbox_weight
# 计算加权 IoU 损失的总和,并除以 target_scores_sum ,得到平均 IoU 损失。
loss_iou = loss_iou.sum() / target_scores_sum
# 这部分代码实现了 IoU 损失的计算,首先通过前景掩码选择出预测和目标的前景边界框,然后计算这些边界框之间的 IoU 值。接着,计算 IoU 损失,将其乘以边界框权重,并最终通过除以目标分数总和来得到平均 IoU 损失。这个损失值可以用来评估模型预测边界框的准确性,并在训练过程中优化模型。
# 这段代码是 BboxLoss 类的 forward 方法中计算 DFL(Distribution Focal Loss)损失的部分。
# dfl loss
# 检查是否启用了 DFL 损失。如果 self.use_dfl 为 True ,则计算 DFL 损失。
if self.use_dfl:
# 将前景掩码 fg_mask 在最后一个维度上扩展,并重复 (self.reg_max + 1) * 4 次,以匹配距离分布的维度。
# dist_mask 是通过将前景掩码 fg_mask 在最后一个维度上扩展,并重复 (self.reg_max + 1) * 4 次来创建的。具体来说, fg_mask 的形状是 (b, h*w) ,其中 b 是批次大小, h*w 是特征图的高和宽的乘积,表示每个特征点。通过 unsqueeze(-1) 操作, fg_mask 的形状变为 (b, h*w, 1) ,然后通过 repeat([1, 1, (self.reg_max + 1) * 4]) 操作, dist_mask 的形状变为 (b, h*w, (self.reg_max + 1) * 4) 。
dist_mask = fg_mask.unsqueeze(-1).repeat([1, 1, (self.reg_max + 1) * 4])
# 使用 dist_mask 从 pred_dist 中选择出 前景的 预测距离分布 ,然后重塑为 (n, 4, self.reg_max + 1) 的形状,其中 n 是前景边界框的数量。
pred_dist_pos = torch.masked_select(pred_dist, dist_mask).view(-1, 4, self.reg_max + 1)
# 将目标边界框转换为距离表示,使用 bbox2dist 函数。
# def bbox2dist(anchor_points, bbox, reg_max):
# -> 将边界框从 xyxy (左上角和右下角的坐标)格式转换为 ltrb (左、上、右、下的偏移量)格式。使用 torch.cat 将这两个偏移量沿着最后一个维度(即坐标维度)拼接起来,形成 ltrb 格式的距离。然后使用 clamp 函数将距离限制在 [0, reg_max - 0.01] 的范围内,以避免超出回归任务的最大值。
# -> return torch.cat((anchor_points - x1y1, x2y2 - anchor_points), -1).clamp(0, reg_max - 0.01) # dist (lt, rb)
target_ltrb = bbox2dist(anchor_points, target_bboxes, self.reg_max)
# 使用 bbox_mask 从 target_ltrb 中选择出 前景的 目标距离表示 ,然后重塑为 (n, 4) 的形状。
target_ltrb_pos = torch.masked_select(target_ltrb, bbox_mask).view(-1, 4)
# 计算 DFL 损失,使用 _df_loss 方法,并乘以边界框的权重。
loss_dfl = self._df_loss(pred_dist_pos, target_ltrb_pos) * bbox_weight
# 计算加权 DFL 损失的总和,并除以 target_scores_sum ,得到平均 DFL 损失。
loss_dfl = loss_dfl.sum() / target_scores_sum
# 如果 self.use_dfl 为 False ,则将 DFL 损失设置为 0,并将其移动到与 pred_dist 相同的设备上。
else:
loss_dfl = torch.tensor(0.0).to(pred_dist.device)
# 这部分代码实现了 DFL 损失的计算,首先通过前景掩码选择出预测和目标的前景距离分布,然后计算这些距离分布之间的 DFL 损失。接着,计算 DFL 损失,并乘以边界框权重,并最终通过除以目标分数总和来得到平均 DFL 损失。这个损失值可以用来评估模型预测距离分布的准确性,并在训练过程中优化模型。
# 返回 IoU 损失、DFL 损失和 IoU 值。
return loss_iou, loss_dfl, iou
# forward 方法实现了边界框损失的计算,包括 IoU 损失和可选的 DFL 损失。IoU 损失通过计算预测边界框和目标边界框之间的交并比来得到,而 DFL 损失则通过预测的距离分布和目标边界框的距离表示来计算。这个方法可以灵活地根据是否需要 DFL 损失来调整损失计算方式,以适应不同的目标检测任务需求。
# 这段代码定义了 BboxLoss 类中的一个私有方法 _df_loss ,用于计算分布焦点损失(Distribution Focal Loss,简称 DFL)。这个方法是 forward 方法中计算 DFL 损失的一部分。
# 定义了一个名为 _df_loss 的方法,它接受两个参数。
# 1.pred_dist :预测的距离分布。
# 2.target :目标距离。
def _df_loss(self, pred_dist, target):
# 将目标距离转换为整数类型,表示每个边界框的 左边界索引 。 target_right 是 target_left 的下一个索引,表示 右边界 。
target_left = target.to(torch.long)
target_right = target_left + 1
# 计算 左边界 和 右边界 的 权重 。这些权重将用于平衡损失函数中左边界和右边界的贡献,使得损失更加关注目标距离的实际位置。
# 这两行代码是在计算分布焦点损失(DFL)时,用于确定目标距离左右边界的权重。这里的 target 表示目标边界框的左边界或右边界距离(取决于是计算 weight_left 还是 weight_right )。 target_left 和 target_right 分别是目标距离的左边界和右边界索引,它们是从 target 计算得出的整数索引。
# 计算左边界的权重。 target_right 是 target_left + 1 ,表示右边界的索引。将 target_right 转换为浮点数类型后,从它减去 target 的值,得到的结果是左边界和实际目标值之间的距离。这个距离就是左边界的权重,它表示目标值到左边界的距离占一个间隔( target_right 和 target_left 之间的距离)的比例。
weight_left = target_right.to(torch.float) - target
# 计算右边界的权重。由于一个间隔的总权重是 1,所以右边界的权重就是 1 减去左边界的权重。这样, weight_left 和 weight_right 就分别表示了目标值到左边界和右边界的相对距离,它们的和为 1。
weight_right = 1 - weight_left
# torch.nn.functional.cross_entropy(input, target, weight=None, size_average=None, ignore_index=-100, reduce=None, reduction='mean')
# F.cross_entropy 是 PyTorch 中的一个函数,用于计算交叉熵损失。这个函数结合了 softmax 操作和交叉熵损失计算,通常用于多分类问题。
# 参数说明 :
# input :形状为 (N, C) 的张量,其中 N 为 batch size, C 为类别数。这个参数对应于神经网络最后一个全连接层输出的未经 softmax 处理的结果(即 logits)。
# target :形状为 (N,) 的张量,其值是 0 <= targets[i] <= C-1 的元素,其中 C 是类别数。这个参数包含一组给定的真实标签(ground truth)。
# weight (可选) :采用类别平衡的加权方式计算损失值。可以传入一个大小为 (C,) 的张量,其中 weight[j] 是类别 j 的权重。默认为 None 。
# size_average (已弃用) :可以忽略。该参数已经被 reduce 参数取代。
# ignore_index :指定被忽略的目标值的索引。如果目标值等于该索引,则不计算该样本的损失。默认值为 -100 ,即不忽略任何目标值。
# reduce (已弃用) :指定返回的损失值的方式。可以是 "none" (不返回损失值)、 "mean" (返回样本损失值的平均值)和 "sum" (返回样本损失值的总和)。默认值为 "mean" 。
# reduction :与 reduce 参数等价。表示返回的损失值的方式。默认值为 "mean" 。
# 计算过程 :
# Softmax 操作 :F.cross_entropy 函数首先对 input 进行 softmax 操作,以确保每行的元素和为 1,并将其视为概率分布。
# 交叉熵损失计算 :根据 softmax 后的概率和真实标签 target ,计算每个样本的交叉熵损失。
# 权重应用 :如果提供了 weight 参数,会将每个类别的损失乘以其对应的权重。
# 损失聚合 :根据 reduction 参数的设置,对所有样本的损失进行求和、求平均或不进行聚合。
# 这个函数是 PyTorch 中实现多分类交叉熵损失的标准方式,它简化了从 logits 到损失值的计算过程。
# 这种权重计算方法使得损失函数能够根据目标值相对于边界框边界的位置来分配不同的权重,从而更加关注目标值的实际位置。这种方法特别适用于边界框回归任务,因为它允许模型更精细地调整预测值以匹配真实目标值的位置。
# 计算左边界的交叉熵损失。 pred_dist 被重塑为 (-1, self.reg_max + 1) 形状,以便进行交叉熵计算。损失计算后重塑回 target_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
# 计算右边界的交叉熵损失,方法与左边界相同,但使用 target_right 作为目标。
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
# 将 左边界 和 右边界 的损失相加,然后计算最后一个维度(表示每个边界框)的平均值,并保持维度( keepdim=True )以便与其他张量进行广播操作。
return (loss_left + loss_right).mean(-1, keepdim=True)
# _df_loss 方法实现了 DFL 损失的计算,它通过计算预测距离分布与目标距离之间的交叉熵损失,并为左边界和右边界分别计算权重,最终返回每个边界框的平均损失。这种方法特别适用于目标检测中边界框的精细回归,能够提高模型对边界框位置的预测精度。
# BboxLoss 类实现了目标检测中的边界框损失计算,包括 IoU 损失和可选的 DFL 损失。IoU 损失通过计算预测边界框和目标边界框之间的交并比来得到,而 DFL 损失则通过预测的距离分布和目标边界框的距离表示来计算。这个类可以灵活地根据是否需要 DFL 损失来调整损失计算方式。
6.class ComputeLoss:
python
# 这段代码定义了一个名为 ComputeLoss 的类,它用于计算目标检测模型的损失。这个类包含了多个组件,用于计算类别损失、边界框损失和分布焦点损失(DFL)。
# 定义了一个名为 ComputeLoss 的新类。
class ComputeLoss:
# Compute losses
# 这段代码是 ComputeLoss 类的构造函数 __init__ ,它负责初始化类的各种属性和损失函数。
# 定义 ComputeLoss 类的构造函数,接受两个参数。
# 1.model :目标检测模型。
# 2.use_dfl :一个布尔值,指示是否使用分布焦点损失,默认为 True 。
def __init__(self, model, use_dfl=True):
# 获取模型参数所在的设备(CPU或GPU)。
device = next(model.parameters()).device # get model device
# 获取模型的超参数。
h = model.hyp # hyperparameters
# Define criteria 定义标准。
# nn.BCEWithLogitsLoss(weight=None, reduction='mean', pos_weight=None)
# nn.BCEWithLogitsLoss 是 PyTorch 中的一个损失函数类,它结合了 Sigmoid 激活函数和 Binary Cross-Entropy (BCE) 损失函数,被广泛用于二分类问题中。
# 参数 :
# weight (Tensor, optional) : 每个样本的权重,用于处理样本不平衡的情况。默认为 None 。
# reduction (str, optional) : 指定应用于输出的归约方式,可以是 'none' 、 'mean' 或 'sum' 。默认为 'mean' 。
# 'none' : 不进行归约,返回每个样本的损失。
# 'mean' : 返回所有样本损失的平均值。
# 'sum' : 返回所有样本损失的总和。
# pos_weight (Tensor, optional) : 正类的权重,用于处理样本不平衡,特别是当正样本数量远小于负样本时。默认为 None 。
# 计算过程 :
# 1. Sigmoid Activation: 对输入的 logits 进行 sigmoid 激活,使其变为 [0, 1] 之间的概率值。
# 2. Binary Cross Entropy: 使用二元交叉熵损失公式计算损失。
# 3. 归约操作: 根据 reduction 参数对损失值进行平均或求和操作。
# 优点 :
# 数值稳定性 : 由于在内部使用了 Sigmoid 函数,可以避免直接计算 log(1 - p) 时的数值稳定性问题。
# 梯度计算 : 能够自动计算 Sigmoid 函数的梯度,减轻了开发者的负担。
# 联合优化 : 因为 Sigmoid 函数是包含在损失函数内部的,所以可以与其他层一起进行端到端的联合优化。
# 应用场景 :
# 样本不平衡处理 : 通过 weight 和 pos_weight 参数来设置每个类别的权重,从而缓解样本不平衡的问题。
# 多标签分类 : nn.BCEWithLogitsLoss 也可以用于多标签分类问题,只需要把 targets 设置为多个 0/1 值的 tensor 即可。
# 联合优化 : 由于内部已经包含了 Sigmoid 激活函数,可以直接把模型的输出层连接到这个损失函数上,进行端到端的联合优化。
# nn.BCEWithLogitsLoss 是 PyTorch 中一种非常实用的二分类损失函数,它结合了 Sigmoid 激活和二元交叉熵损失,在数值稳定性和梯度计算方面都有所改进,是深度学习实践中的首选之一。
# 定义二元交叉熵损失函数 BCEcls ,用于类别损失计算,并设置正样本权重和不进行损失的自动求和。
# 这行代码在 ComputeLoss 类的构造函数中定义了一个二元交叉熵损失函数 BCEcls ,用于目标检测模型中的类别损失计算。
# nn.BCEWithLogitsLoss :这是 PyTorch 中的一个损失函数类,用于计算二元交叉熵损失,并且适用于带有 logits 输出的模型(即模型输出没有经过 sigmoid 激活函数)。这个损失函数内部会应用 sigmoid 激活函数到输入上,然后计算二元交叉熵。
# pos_weight :这是一个参数,用于平衡正负样本的数量。在目标检测任务中,通常背景(负样本)的数量远多于目标(正样本),因此需要通过 pos_weight 来增加正样本的权重,以缓解类别不平衡问题。 pos_weight 的值通常设置为正样本数量与负样本数量的比值。
# torch.tensor([h["cls_pw"]], device=device) :这里创建了一个 PyTorch 张量,包含超参数 cls_pw 的值,它表示正样本的权重。 h["cls_pw"] 从模型的超参数 h 中获取, device 参数确保张量被创建在正确的设备上(例如 GPU)。
# reduction='none' :这个参数指定损失函数的缩减方式。设置为 'none' 表示不对损失进行求和或平均,而是保留每个样本的损失值。这样做可以在后续的计算中对每个样本的损失进行更细致的操作,例如根据目标的权重进行加权。
# 创建了一个二元交叉熵损失函数 BCEcls ,它将在后续的损失计算中使用,特别是在计算类别损失时。通过设置 pos_weight 和 reduction='none' ,这个损失函数能够适应目标检测任务中的类别不平衡问题,并允许对每个样本的损失进行单独处理。
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 类别标签平滑 https://arxiv.org/pdf/1902.04103.pdf eqn 3 。
# 使用 smooth_BCE 函数计算类别损失的平滑目标值。
# def smooth_BCE(eps=0.1):
# -> 用于计算平滑的二元交叉熵损失(Binary Cross-Entropy, BCE)的参数。函数返回两个值。平滑后的正类(positive class)损失的参数,通常用于计算正样本的损失。平滑后的负类(negative class)损失的参数,通常用于计算负样本的损失。
# -> return 1.0 - 0.5 * eps, 0.5 * eps
self.cp, self.cn = smooth_BCE(eps=h.get("label_smoothing", 0.0)) # positive, negative BCE targets
# Focal loss
# 获取焦点损失的 gamma 超参数。
g = h["fl_gamma"] # focal loss gamma
# 如果 gamma 大于0,则使用焦点损失包装 BCEcls 。
if g > 0:
# class FocalLoss(nn.Module):
# -> 用于实现焦点损失(Focal Loss)。焦点损失是一种专门为类别不平衡问题设计的损失函数,它通过减少易分类样本的权重并增加难分类样本的权重来改进模型性能。
# -> def __init__(self, loss_fcn, gamma=1.5, alpha=0.25):
BCEcls = FocalLoss(BCEcls, g)
# 获取模型的最后一个检测模块。
# def de_parallel(model):
# -> 将一个可能处于并行状态(例如使用 PyTorch 的 DataParallel 或 DistributedDataParallel 包装过的模型)转换回单个 GPU 或 CPU 上的模型。如果 model 是并行模型, 将返回原始的、未并行化的模型 model.module 。如果 model 不是并行模型,直接返回 model 。
# -> return model.module if is_parallel(model) else model
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
# 保存模型参数和超参数。
# 将之前定义的二元交叉熵损失函数 BCEcls 保存为类的实例变量,这样它就可以在类的其他方法中被访问和使用。
self.BCEcls = BCEcls
# 将模型的超参数 h 保存为类的实例变量。这些超参数包括学习率、优化器参数、损失函数参数等,它们在训练过程中用于控制模型的行为。
self.hyp = h
# 保存模型中使用的步长(stride)。在目标检测模型中,不同的特征层有不同的步长,这影响了锚点(anchor)的放置和边界框的尺寸。
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
# 保存模型参数所在的设备(CPU或GPU)。这对于确保所有的计算都在正确的设备上执行是必要的。
self.device = device
# 初始化两个 任务对齐的分配器 ,用于 目标分配 。分配器的参数从环境变量中获取,如果环境变量未设置,则使用默认值。
# TaskAlignedAssigner 是一个用于目标检测任务中目标分配的类。它负责将预测的边界框与真实目标匹配,以便计算损失时能够确定哪些预测框应该对哪些真实框负责。这个过程对于训练目标检测模型至关重要。
# topk :这个参数指定了在每个锚点位置上,应该有多少个预测被考虑用于匹配。这通常是基于置信度得分的前 topk 个预测。
# num_classes :这是模型需要检测的类别数量。目标分配器需要知道这一点,以便为每个类别分配正确的目标。
# alpha 和 beta :这两个参数用于计算匹配过程中的不匹配惩罚。 alpha 和 beta 是用于调整目标分配策略的超参数,它们影响 IoU(交并比)计算和目标分配决策。
# os.getenv('YOLOM', 10) :这里使用 os.getenv 方法从操作系统的环境变量中获取名为 YOLOM 的值。如果该环境变量不存在,则默认使用 10 作为 topk 的值。
# os.getenv('YOLOA', 0.5) 和 os.getenv('YOLOB', 6.0) :类似地,这两个表达式分别获取名为 YOLOA 和 YOLOB 的环境变量值,用于设置 alpha 和 beta 参数。如果这些环境变量不存在,则分别使用 0.5 和 6.0 作为默认值。
# 这两行代码创建了两个 TaskAlignedAssigner 实例, self.assigner 和 self.assigner2 。这两个分配器的配置相同,但它们在损失计算的不同阶段或不同部分中使用,例如在处理不同尺度的特征图或不同阶段的预测时。
# 通过这种方式, ComputeLoss 类能够灵活地配置目标分配策略,并根据环境变量中的配置或默认值来调整目标分配器的行为。这对于适应不同的训练配置和数据集特性非常有用。
# class TaskAlignedAssigner(nn.Module):
# -> 用于在目标检测任务中进行目标分配。这个类的主要作用是为每个预测的边界框分配最匹配的真实目标,并计算相应的目标标签和边界框。
# -> def __init__(self, topk=13, num_classes=80, alpha=1.0, beta=6.0, eps=1e-9):
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.assigner2 = 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)))
# 初始化两个边界框损失计算器,并移动到正确的设备。
# class BboxLoss(nn.Module):
# -> 用于计算目标检测任务中的边界框损失。这个类包含了 IoU 损失和可选的 DFL(Distribution Focal Loss)损失。
# -> def __init__(self, reg_max, use_dfl=False):
self.bbox_loss = BboxLoss(m.reg_max - 1, use_dfl=use_dfl).to(device)
self.bbox_loss2 = BboxLoss(m.reg_max - 1, use_dfl=use_dfl).to(device)
# 初始化投影张量,并移动到正确的设备。
self.proj = torch.arange(m.reg_max).float().to(device) # / 120.0
# 保存是否使用 DFL 的标志。
self.use_dfl = use_dfl
# 构造函数 __init__ 初始化了 ComputeLoss 类,设置了损失函数、分配器和边界框损失计算器等关键组件,并根据模型的超参数和环境变量配置了这些组件。这个类的目的是提供一个统一的接口来计算目标检测模型的损失,包括类别损失、边界框损失和可选的分布焦点损失。
# 这段代码定义了 ComputeLoss 类中的 preprocess 方法,用于预处理目标(targets)数据,使其适配模型的输入要求。
# 定义了 preprocess 方法,它接受三个参数。
# 1.targets :包含目标数据的张量。
# 2.batch_size :批次大小。
# 3.scale_tensor :用于缩放目标边界框的张量。
def preprocess(self, targets, batch_size, scale_tensor):
# 如果 targets 张量中没有目标(即形状的第一个维度为0),则创建一个形状为 (batch_size, 0, 5) 的全零张量 out ,其中5代表每个目标的五个属性(类别标签和四个边界框坐标)。
if targets.shape[0] == 0:
out = torch.zeros(batch_size, 0, 5, device=self.device)
# 如果 targets 张量中有目标。
else:
# 获取每个目标的图像索引,这通常用于区分不同图像中的目标。
i = targets[:, 0] # image index
# 计算每个图像索引出现的次数, counts 张量中包含每个唯一索引的计数。
_, counts = i.unique(return_counts=True)
# 创建一个形状为 (batch_size, counts.max(), 5) 的全零张量 out ,其中 counts.max() 确保 out 的第二维度足够大,可以容纳任一图像中最多的目标数。
out = torch.zeros(batch_size, counts.max(), 5, device=self.device)
# 遍历每个批次,找到与当前批次索引匹配的目标,并将这些目标的属性(不包括图像索引)复制到 out 张量中。
# 这段代码是在 preprocess 方法中的一部分,它负责将目标数据( targets )中的信息按照图像索引分配到预处理输出张量( out )中。
# 这个循环遍历每个批次中的图像。 batch_size 是批次中图像的数量。
for j in range(batch_size):
# 对于每个图像索引 j , matches 是一个布尔掩码,它标识 targets 中哪些目标属于当前索引 j 的图像。这里 i 是从 targets 提取出的图像索引列。
matches = i == j
# 计算属于当前图像索引 j 的目标数量。 n 是匹配掩码 matches 中 True 值的总数。
n = matches.sum()
# 如果 n 大于0,意味着当前批次中存在属于该图像的目标。
if n:
# 将这些目标的信息复制到 out 张量中对应的位置。 targets[matches, 1:] 选择 targets 中属于当前图像索引 j 的所有目标(除去第一个元素,即图像索引),并且排除了图像索引列。 out[j, :n] 指定 out 张量中当前图像索引 j 的行和前 n 列,用于存放这些目标信息。
out[j, :n] = targets[matches, 1:]
# 这段代码的作用是将 targets 张量中的目标根据它们所属的图像索引分配到 out 张量中相应的位置。这样,每个图像的目标信息都被组织在一起,且与它们对应的批次索引相对应。这对于后续的损失计算是必要的,因为每个图像的目标信息需要单独处理。
# 将 out 张量中的目标边界框从中心点坐标(x, y, w, h)转换为角点坐标(x1, y1, x2, y2),并使用 scale_tensor 对边界框进行缩放。
# def xywh2xyxy(x): -> 将边界框的坐标从 (x, y, w, h) 格式转换为 (x1, y1, x2, y2) 格式。函数执行完毕后返回转换后的坐标数组 y 。 -> return y
out[..., 1:5] = xywh2xyxy(out[..., 1:5].mul_(scale_tensor))
# 返回预处理后的 out 张量。
return out
# preprocess 方法的主要作用是将目标数据转换为模型训练中需要的格式,包括处理空目标的情况、按图像索引组织目标、将边界框格式从中心点坐标转换为角点坐标,并应用缩放。这样预处理后的数据可以直接用于损失函数的计算。
# 这段代码定义了 ComputeLoss 类中的 bbox_decode 方法,用于将预测的距离分布转换为边界框坐标。
# 定义了 bbox_decode 方法,它接受两个参数。
# 1.anchor_points :锚点坐标。
# 2.pred_dist :预测的距离分布。
def bbox_decode(self, anchor_points, pred_dist):
# 检查是否启用了分布焦点损失(DFL)。如果启用,将使用不同的解码方式。
if self.use_dfl:
# 获取 pred_dist 张量的形状,分别代表批次大小( b )、锚点数量( a )和通道数( c )。
b, a, c = pred_dist.shape # batch, anchors, channels
# torch.matmul(input, other, *, out=None) -> Tensor
# torch.matmul 是 PyTorch 中的一个函数,用于计算两个张量的矩阵乘法。这个函数支持多种类型的矩阵乘法,包括标准的矩阵乘法、批量矩阵乘法(batch matrix multiplication)以及与分配律结合的矩阵乘法。
# 参数 :
# input :第一个输入张量,可以是一个向量、矩阵或更高维度的张量。
# other :第二个输入张量,形状必须与 input 兼容以进行矩阵乘法。
# out :(可选)输出张量。如果提供,结果将被写入此张量中。
# 返回值 :
# 返回一个新的张量,它是 input 和 other 的矩阵乘法结果。
# 形状要求 :
# 如果 input 是一个 n x m 矩阵, other 是一个 m x p 矩阵,那么结果将是一个 n x p 矩阵。
# 如果 input 是一个 (b, n, m) 张量, other 是一个 (b, m, p) 张量,那么结果将是一个 (b, n, p) 张量,这是一个批量矩阵乘法。
# torch.matmul 是 PyTorch 中实现矩阵乘法的推荐方式,因为它支持自动求导,并且可以利用 PyTorch 的并行计算能力。它在内部优化了计算,使得在 GPU 上运行时更加高效。
# 将 pred_dist 张量重塑为 (b, a, 4, c // 4) 的形状,并在最后一个维度(通道)上应用 softmax 函数。这将对每个锚点的每个边界框维度(例如,左、上、右、下)的概率分布进行归一化。然后,使用 matmul 函数将归一化后的概率分布与 self.proj 相乘, self.proj 是一个从 0 到 m.reg_max 的整数张量,用于将分布转换为实际的距离值。
pred_dist = pred_dist.view(b, a, 4, c // 4).softmax(3).matmul(self.proj.type(pred_dist.dtype))
# 这行代码被注释掉了,它展示了另一种可能的 pred_dist 张量转换方式,但没有被实际使用。
# 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 = (pred_dist.view(b, a, c // 4, 4).softmax(2) * self.proj.type(pred_dist.dtype).view(1, 1, -1, 1)).sum(2)
# 调用 dist2bbox 函数,将预测的距离分布 pred_dist 和锚点 anchor_points 转换为边界框坐标。 xywh=False 参数表示输出的边界框坐标格式为 (x1, y1, x2, y2) ,而不是 (x, y, w, h) 。
# def dist2bbox(distance, anchor_points, xywh=True, dim=-1):
# -> 用于将从锚点(anchor points)出发的边界距离(通常表示为左上角和右下角的坐标)转换为边界框(bounding box)的坐标。将中心点和宽高在指定维度 dim 上拼接起来,返回xywh格式的边界框。将左上角和右下角坐标在指定维度 dim 上拼接起来,返回xyxy格式的边界框。
# -> return torch.cat((c_xy, wh), dim) # xywh bbox / return torch.cat((x1y1, x2y2), dim) # xyxy bbox
return dist2bbox(pred_dist, anchor_points, xywh=False)
# bbox_decode 方法的作用是将预测的距离分布转换为边界框坐标。如果启用了 DFL,它将使用 softmax 函数和 self.proj 将概率分布转换为实际的距离值,然后再将这些距离值转换为边界框坐标。这个过程是目标检测模型中从模型输出到最终边界框预测的关键步骤。
# 这段代码定义了 ComputeLoss 类的 __call__ 方法,它是类的主入口点,用于计算给定模型预测 p 和目标 targets 的损失。
# 定义 __call__ 方法,这是类的实例被调用时执行的方法。它接受四个参数。
# 1.p :这是模型的输出。它通常是一个张量或张量的元组,包含了模型对于输入数据的预测结果。在目标检测模型中, p 包含了边界框的预测坐标、类别预测分数等。
# 2.targets :这是真实目标的数据,通常包含了边界框的标注信息和类别标签。 targets 用于计算损失函数,以衡量模型预测与真实情况之间的差异。
# 3.img :这是一个可选参数,代表输入图像本身。在某些情况下,原始图像可能用于损失计算,例如在某些图像重建任务中,或者用于调试和可视化目的。默认值为 None 。
# 4.epoch :这是一个可选参数,表示当前的训练周期(epoch)。在训练过程中, epoch 可用于动态调整损失函数的行为,例如改变某些超参数的值以实现学习率衰减或其他训练策略。默认值为 0 。
def __call__(self, p, targets, img=None, epoch=0):
# 这段代码是 ComputeLoss 类的 __call__ 方法的一部分,它负责准备和处理模型输出以及目标数据,以便计算损失。
# 初始化一个包含三个元素的张量 loss ,用于存储 边界框损失 、 类别损失 和 分布焦点损失(DFL) 。这个张量被初始化为0,并放置在模型参数所在的设备上。
loss = torch.zeros(3, device=self.device) # box, cls, dfl
# 获取模型输出的特征图。如果 p 是一个元组,那么 feats 是 p 的第一个元素的第一个子元素;否则, feats 就是 p 的第一个元素。
feats = p[1][0] if isinstance(p, tuple) else p[0]
# 获取模型输出的第二组特征图。这通常用于多尺度特征融合。
feats2 = p[1][1] if isinstance(p, tuple) else p[1]
# 将所有特征图 feats 的预测分布和分数在维度2上连接起来,然后分割成 预测分布 pred_distri 和 预测分数 pred_scores 。 self.no 是每个锚点的输出数量, self.reg_max * 4 是每个边界框的回归参数数量, self.nc 是类别数量。
# 这行代码是 ComputeLoss 类的 __call__ 方法中的一部分,用于处理模型输出,将 预测的距离分布( pred_distri )和 预测的类别分数( pred_scores )从模型的特征输出( feats )中分离出来。
# [xi.view(feats[0].shape[0], self.no, -1) for xi in feats] :这是一个列表推导式,它遍历 feats 列表中的每个元素 xi 。 feats 是模型输出的特征图列表,每个特征图 xi 包含了预测的边界框和类别信息。
# xi.view(feats[0].shape[0], self.no, -1) 将每个特征图 xi 重塑为一个新的形状,其中 :
# feats[0].shape[0] :是批次大小(batch size)。
# self.no :是每个网格单元格(anchor)的输出数量。
# -1 :表示自动计算该维度的大小,使得总元素数量保持不变。
# torch.cat(..., 2) :torch.cat 函数用于沿着指定的维度(这里是维度2)连接张量列表。 连接后的张量形状为 (batch_size, self.no, total_elements) ,其中 total_elements 是所有特征图的预测元素总数。
# .split((self.reg_max * 4, self.nc), 1) : .split 函数用于将连接后的张量分割成两个部分。 (self.reg_max * 4, self.nc) 指定了分割的大小,其中 :
# self.reg_max * 4 :是每个边界框的回归参数数量(通常4个值:x, y, w, h)。
# self.nc :是类别数量。
# 分割是沿着维度1进行的,即每个网格单元格的输出被分割成 距离分布 和 类别分数 。
# pred_distri 和 pred_scores 分别包含了预测的距离分布和类别分数,它们可以用于后续的损失计算。
# pred_distri 的形状为 : (batch_size, self.no, self.reg_max * 4) 。
# pred_scores 的形状为 : (batch_size, self.no, self.nc) 。
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) # ❌ ⚠️ 不应该在维度1上进行分割,正确的应该是 在 形状为 (batch_size, self.no, total_elements)张量的 total_elements 维度,即维度2上进行分割。
# 调整 预测分数 和 预测分布 的维度顺序,使其形状为( 批次大小 , 锚点数量 , 类别数量 )和( 批次大小 , 锚点数量 , 回归参数数量 ),并确保数据在内存中是连续的。
pred_scores = pred_scores.permute(0, 2, 1).contiguous()
pred_distri = pred_distri.permute(0, 2, 1).contiguous()
# 对第二组特征图执行与上述相同的操作。
# pred_distri2 的形状为 : (batch_size, self.no, self.reg_max * 4) 。
# pred_scores2 的形状为 : (batch_size, self.no, self.nc) 。
pred_distri2, pred_scores2 = torch.cat([xi.view(feats2[0].shape[0], self.no, -1) for xi in feats2], 2).split(
(self.reg_max * 4, self.nc), 1) # ❌ ⚠️ 不应该在维度1上进行分割,正确的应该是 在 形状为 (batch_size, self.no, total_elements)张量的 total_elements 维度,即维度2上进行分割。
# 调整第二组特征图的预测分数和预测分布的维度顺序,并确保数据在内存中是连续的。
pred_scores2 = pred_scores2.permute(0, 2, 1).contiguous()
pred_distri2 = pred_distri2.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)
# 生成 锚点 和 步长 张量,这些用于后续的边界框解码和损失计算。
# def make_anchors(feats, strides, grid_cell_offset=0.5): -> 用于生成YOLO模型中使用的锚点(anchor points)。使用 torch.cat 函数将 anchor_points 和 stride_tensor 列表中的所有张量连接起来,并返回结果。 -> return torch.cat(anchor_points), torch.cat(stride_tensor)
anchor_points, stride_tensor = make_anchors(feats, self.stride, 0.5)
# 这部分代码负责处理模型输出,包括特征图的处理、预测分布和分数的提取、维度的调整以及锚点的生成。这些步骤是损失计算的基础,确保了数据的正确性和后续步骤的顺利进行。
# 这段代码处理目标检测任务中的目标(targets),将其转换为适合损失计算的格式,并提取出类别标签和边界框。
# targets
# 调用 preprocess 方法对目标数据进行预处理。 preprocess 方法负责将目标数据调整为模型输出的对应格式,包括缩放边界框坐标以匹配输入图像的尺寸。 imgsz[[1, 0, 1, 0]] 是一个张量,包含了每个图像的宽度和高度(以及宽度和高度的副本),用于缩放边界框坐标。
targets = self.preprocess(targets, batch_size, scale_tensor=imgsz[[1, 0, 1, 0]])
# 将预处理后的目标数据 targets 分割成两个部分。 gt_labels 和 gt_bboxes 。 targets 张量的形状是 (batch_size, max_objects, 5) ,其中最后一个维度的前1个元素是类别标签,接下来的4个元素是边界框的坐标(格式为 xyxy ,即左上角和右下角的坐标)。 split 方法按照指定的形状 (1, 4) 沿着第二个维度(索引为2)分割张量。
# gt_labels 和 gt_bboxes 是从预处理后的目标数据 targets 中分割出来的。 targets 的形状是 (batch_size, max_objects, 5) ,其中最后一个维度的前1个元素是类别标签,接下来的4个元素是边界框的坐标。
# 因此,当使用 split((1, 4), 2) 方法沿着第二个维度(索引为2)分割 targets 时,得到 :
# gt_labels 的形状是 (batch_size, max_objects, 1) ,它包含了每个目标的类别标签。
# gt_bboxes 的形状是 (batch_size, max_objects, 4) ,它包含了每个目标的边界框坐标(格式为 xyxy ,即左上角和右下角的坐标)。
gt_labels, gt_bboxes = targets.split((1, 4), 2) # cls, xyxy
# 创建一个布尔掩码 mask_gt ,用于标识哪些目标是有效的(即边界框坐标不全为0)。 gt_bboxes.sum(2, keepdim=True) 计算每个目标边界框坐标的总和,并保持维度不变,结果是形状为 (batch_size, max_objects, 1) 的张量。 gt_bboxes.sum(2, keepdim=True).gt_(0) 检查这个总和是否大于0,从而确定每个目标是否有效(即边界框是否非空)。
mask_gt = gt_bboxes.sum(2, keepdim=True).gt_(0)
# 这段代码处理目标数据,将其分割成类别标签和边界框,并创建一个掩码来标识有效的目标。这些信息将被用于后续的损失计算,确保只有有效的目标被考虑进去。
# 这段代码是 __call__ 方法中的一部分,它负责解码预测的边界框,并使用目标分配器来为每个预测分配真实目标。
# pboxes
# 调用 bbox_decode 方法将预测的距离分布 pred_distri 和锚点 anchor_points 转换为边界框坐标 pred_bboxes 。输出的边界框格式是 xyxy ,即左上角和右下角的坐标,形状为 (b, h*w, 4) ,其中 b 是批次大小, h*w 是特征图上的网格点总数。
pred_bboxes = self.bbox_decode(anchor_points, pred_distri) # xyxy, (b, h*w, 4)
# 对第二组特征图执行相同的操作,得到 pred_bboxes2 。
pred_bboxes2 = self.bbox_decode(anchor_points, pred_distri2) # xyxy, (b, h*w, 4)
# 使用 assigner 对 预测分数 pred_scores 、 预测边界框 pred_bboxes 、 锚点 anchor_points 、 真实标签 gt_labels 、 真实边界框 gt_bboxes 和 真实目标掩码 mask_gt 进行处理,以分配每个预测应该匹配的真实目标。 pred_scores.detach().sigmoid() 计算预测分数的 sigmoid, (pred_bboxes.detach() * stride_tensor).type(gt_bboxes.dtype) 调整预测边界框的尺度并转换数据类型。
# 这行代码是 __call__ 方法中的一部分,它使用一个目标分配器( self.assigner )来为模型的预测分配真实目标。这个过程是目标检测训练中的关键步骤,它决定了哪些预测应该对哪些真实目标负责。
# self.assigner(...) : self.assigner 是一个目标分配器实例,它负责根据预测和真实目标之间的匹配程度来分配目标。 这个分配器会考虑预测的类别分数、边界框与真实目标的匹配程度,并输出每个预测应该负责的真实目标的标签、边界框和分数。
# target_labels :每个预测分配到的真实目标的 类别标签 。
# target_bboxes :每个预测分配到的真实目标的 边界框 。
# target_scores :每个预测分配到的真实目标的 分数 (通常是基于匹配程度的)。
# fg_mask :前景掩码,标识哪些预测是有目标分配的(即非背景的预测)。
# 这个过程确保了模型的预测能够与真实目标正确对应,从而在计算损失时能够准确地衡量模型的性能。
# def forward(self, pd_scores, pd_bboxes, anc_points, gt_labels, gt_bboxes, mask_gt):
# -> 返回 分配的目标标签 ( target_labels )、 目标边界框 ( target_bboxes )、 调整后的目标分数 ( target_scores )和 前景掩码 ( fg_mask )。
# -> return target_labels, target_bboxes, target_scores, fg_mask.bool()
target_labels, target_bboxes, target_scores, fg_mask = self.assigner(
# pred_scores 是模型预测的类别分数。 .detach() 方法用于从当前计算图中分离出 pred_scores ,这意味着对 pred_scores 的后续操作不会影响梯度计算。 .sigmoid() 函数将预测分数通过 sigmoid 函数转换,得到介于 0 和 1 之间的值,表示属于每个类别的概率。
pred_scores.detach().sigmoid(),
# pred_bboxes 是模型预测的边界框。 .detach() 同样用于分离预测的边界框,以便不影响梯度计算。 * stride_tensor 将预测的边界框坐标乘以步长,将它们从 特征图坐标系 转换到 原始图像坐标系 。 .type(gt_bboxes.dtype) 确保预测边界框的数据类型与真实边界框 gt_bboxes 相同,以避免数据类型不匹配的问题。
(pred_bboxes.detach() * stride_tensor).type(gt_bboxes.dtype),
# anchor_points 是在 特征图上 定义的 锚点 (或称为 先验框 )。 * stride_tensor 将锚点坐标乘以步长,转换到原始 图像坐标系 。
anchor_points * stride_tensor,
# gt_labels 真实目标的类别标签。
gt_labels,
# gt_bboxes 真实目标的边界框。
gt_bboxes,
# mask_gt 一个布尔掩码,标识哪些目标是有效的(即边界框不为空)。
mask_gt)
# 对第二组特征图执行相同的操作,得到 target_labels2 、 target_bboxes2 、 target_scores2 和 fg_mask2 。
target_labels2, target_bboxes2, target_scores2, fg_mask2 = self.assigner2(
pred_scores2.detach().sigmoid(),
(pred_bboxes2.detach() * stride_tensor).type(gt_bboxes.dtype),
anchor_points * stride_tensor,
gt_labels,
gt_bboxes,
mask_gt)
# 在目标检测模型中,特征图(feats)通常是原始输入图像经过一系列卷积层和下采样操作后的结果。这意味着特征图上的空间分辨率会低于原始图像,因此特征图上的坐标需要通过步长(stride)来映射回原始图像的坐标空间。
# target_bboxes 包含了从目标分配器 assigner 分配给预测的边界框。这些边界框的坐标是在特征图的坐标空间中的。 stride_tensor 包含了对应于每个特征图层的步长值,这些步长值用于将特征图坐标转换回原始图像坐标。
# 将分配给预测的 真实边界框 target_bboxes 除以步长 stride_tensor ,以将其从特征图坐标转换回原始图像坐标。❌
# ⚠️ 从上面self.assigner()得到的 真实边界框 target_bboxes 是在原始图像坐标系下的坐标,将它除以 步长 stride_tensor 得到的是在特征图坐标系下的坐标。上下文中从self.bbox_decode()得到的 预测边界框 pred_bboxes 它也是特征图坐标系下的坐标。
# ⚠️ 这两个坐标将用于下文self.bbox_loss()中计算损失。
# ⚠️ 在计算损失时涉及真实框与预测框的交并比(IoU)的计算。在计算交并比(IoU)时,真实框和预测框的坐标确实需要在同一坐标系下。通常情况下,这些坐标应该是相对于原始图像的绝对坐标,而不是特征图坐标系下的坐标。
# ⚠️ 我认为应该将 target_bboxes /= stride_tensor 代码改为 pred_bboxes *= stride_tensor ,即将 预测边界框 pred_bboxes 从特征图坐标系转换为原始坐标系,与本就是原始坐标系下的 真实边界框 target_bboxes 参与self.bbox_loss()中损失的计算。
target_bboxes /= stride_tensor
# 计算所有目标分数 target_scores 的总和,并确保至少为1,以避免除以0的情况。
target_scores_sum = max(target_scores.sum(), 1)
# 将第二组特征图的分配给预测的真实边界框 target_bboxes2 除以步长 stride_tensor 。
target_bboxes2 /= stride_tensor
# 计算第二组特征图的所有目标分数 target_scores2 的总和,并确保至少为1。
target_scores_sum2 = max(target_scores2.sum(), 1)
# 这段代码负责解码预测的边界框,使用目标分配器为每个预测分配真实目标,并调整边界框坐标和目标分数。这些步骤是计算损失之前必要的准备工作,确保了预测和真实目标之间的正确匹配。
# 这段代码是 ComputeLoss 类的 __call__ 方法中的一部分,负责计算类别损失(cls loss)和边界框损失(bbox loss),以及分布焦点损失(dfl loss)。
# cls loss
# 类别损失(Class Loss)。
# 这行代码被注释掉了,它提到了一种可能的类别损失计算方法,即 变分焦点损失 (Varifocal Loss, VFL),但在这里没有被使用。
# loss[1] = self.varifocal_loss(pred_scores, target_scores, target_labels) / target_scores_sum # VFL way
# 计算类别损失,使用的是二元交叉熵损失函数 BCEcls 。 pred_scores 是模型预测的类别分数, target_scores 是目标分数(通常是 0 或 1,表示目标的存在)。损失计算后被所有目标分数求和,然后除以 target_scores_sum (目标分数的总和)以得到平均损失。
# def forward(self, pred, true): -> 实现了焦点损失(Focal Loss)的计算逻辑。将基本损失乘以 alpha_factor 和 modulating_factor ,从而得到焦点损失。 -> return loss.mean() / return loss.sum() / return loss
loss[1] = self.BCEcls(pred_scores, target_scores.to(dtype)).sum() / target_scores_sum # BCE
# 将类别损失乘以 0.25,作为损失的缩放因子。
loss[1] *= 0.25
# 对第二组特征图的预测分数 pred_scores2 和目标分数 target_scores2 执行相同的类别损失计算,并将其加到之前的类别损失上。
loss[1] += self.BCEcls(pred_scores2, target_scores2.to(dtype)).sum() / target_scores_sum2 # BCE
# bbox loss
# 边界框损失(Bounding Box Loss)。
# 检查前景掩码 fg_mask 中是否有任何真值(即是否有前景目标)。
if fg_mask.sum():
# 如果存在前景目标,计算边界框损失和分布焦点损失。 bbox_loss 方法接受预测的 距离分布 、 预测的边界框 、 锚点 、 目标边界框 等参数,并返回 边界框损失 、 分布焦点损失 和 交并比(IoU) 。
# def forward(self, pred_dist, pred_bboxes, anchor_points, target_bboxes, target_scores, target_scores_sum, fg_mask): -> 返回 IoU 损失 、 DFL 损失 和 IoU 值。 -> return loss_iou, loss_dfl, iou
loss[0], loss[2], iou = self.bbox_loss(pred_distri,
pred_bboxes,
anchor_points,
target_bboxes,
target_scores,
target_scores_sum,
fg_mask)
# 将边界框损失和分布焦点损失分别乘以 0.25,作为损失的缩放因子。
loss[0] *= 0.25
loss[2] *= 0.25
# 检查第二组特征图的前景掩码 fg_mask2 中是否有任何真值。
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)
# 将第二组特征图的损失加到之前的损失上。
loss[0] += loss0_
loss[2] += loss2_
# 将 边界框损失 乘以 7.5,作为损失的增益因子。
loss[0] *= 7.5 # box gain
# 将 类别损失 乘以 0.5,作为损失的增益因子。
loss[1] *= 0.5 # cls gain
# 将 分布焦点损 失乘以 1.5,作为损失的增益因子。
loss[2] *= 1.5 # dfl gain
# 在目标检测模型的训练中,损失函数的值通常是对整个批次中所有图像的损失的平均值。然而,在某些情况下,我们可能希望将损失的计算基于单个图像,而不是整个批次。将总损失乘以批次大小 batch_size 是一种将平均损失转换回基于单个图像的损失的方法。
# 具体来说,如果损失函数是基于整个批次的平均值计算的,那么在反向传播时,梯度的大小会受到批次大小的影响。较小的批次大小会导致较大的梯度波动,而较大的批次大小则会导致较小的梯度波动。为了保持梯度的稳定性,我们可能希望将损失的计算基于单个图像,这样梯度的大小就不会受到批次大小的影响。
# 通过将总损失乘以 batch_size ,我们实际上是在将平均损失转换回基于单个图像的损失。这样,梯度的大小就不会受到批次大小的影响,从而有助于保持训练过程的稳定性。
# 此外,将损失乘以 batch_size 也使得损失的值更容易解释,因为它表示的是整个批次的总损失,而不是平均损失。这在监控训练过程和调试模型时可能更有用。
# 总之,将总损失乘以 batch_size 是一种将平均损失转换回基于单个图像的损失的方法,有助于保持梯度的稳定性,并使得损失的值更容易解释。
# 返回总损失(所有损失的和乘以批次大小)和分离的损失(不包括梯度计算)。这样,总损失可以用于反向传播,而分离的损失可以用于监控和调试。
return loss.sum() * batch_size, loss.detach() # loss(box, cls, dfl)
# 这段代码负责计算类别损失和边界框损失,以及可选的分布焦点损失。它处理了两组特征图的损失,并应用了不同的缩放因子和增益因子,以平衡不同损失对总损失的贡献。最终返回的总损失用于模型的训练,而分离的损失用于监控和调试。
# __call__ 方法集成了多个步骤,包括预处理目标、解码预测、分配目标和计算损失。它返回总损失和分离的损失,以便在训练过程中优化模型。这个方法的设计使得损失计算既灵活又可定制,能够适应不同的目标检测任务和模型架构。
# ComputeLoss 类集成了多种损失计算方式,包括类别损失、边界框损失和 DFL 损失,用于目标检测模型的训练。它通过一系列方法和属性,如 preprocess 、 bbox_decode 、 __call__ 等,实现了损失的计算和返回。这个类的设计使得损失计算既灵活又可定制,能够适应不同的目标检测任务和模型架构。
7.class ComputeLossLH:
python
# 这段代码定义了一个名为 ComputeLossLH 的类,它用于计算目标检测模型的损失。这个类类似于之前讨论的 ComputeLoss 类的功能,并进行了一些修改和扩展。
# 定义了一个名为 ComputeLossLH 的新类。
class ComputeLossLH:
# Compute losses
# 这段代码是 ComputeLossLH 类的构造函数 __init__ ,它用于初始化类的实例并设置目标检测模型损失计算所需的各种参数和组件。
# 定义 ComputeLossLH 类的构造函数,接受两个参数。
# 1.model :目标检测模型。
# 2.use_dfl :一个布尔值,指示是否使用分布焦点损失,默认为 True 。
def __init__(self, model, use_dfl=True):
# 获取模型参数所在的设备(CPU或GPU)。
device = next(model.parameters()).device # get model device
# 获取模型的超参数。
h = model.hyp # hyperparameters
# Define criteria
# 定义二元交叉熵损失函数 BCEcls ,用于类别损失计算,并设置正样本权重和不进行损失的自动求和。
# nn.BCEWithLogitsLoss : 这是 PyTorch 中的一个损失函数类,用于计算二元交叉熵损失,并且适用于带有 logits 输出的模型(即模型输出没有经过 sigmoid 激活函数)。
# pos_weight :这是一个参数,用于平衡正负样本的数量。在目标检测任务中,通常背景(负样本)的数量远多于目标(正样本),因此需要通过 pos_weight 来增加正样本的权重,以缓解类别不平衡问题。 pos_weight 的值通常设置为正样本数量与负样本数量的比值。
# torch.tensor([h["cls_pw"]], device=device) :这里创建了一个 PyTorch 张量,包含超参数 cls_pw 的值,它表示正样本的权重。 h["cls_pw"] 从模型的超参数 h 中获取, device 参数确保张量被创建在正确的设备上(例如 GPU)。
# reduction='none' :这个参数指定损失函数的缩减方式。设置为 'none' 表示不对损失进行求和或平均,而是保留每个样本的损失值。这样做可以在后续的计算中对每个样本的损失进行更细致的操作,例如根据目标的权重进行加权。
# 这行代码创建了一个二元交叉熵损失函数 BCEcls ,它将在后续的损失计算中使用,特别是在计算类别损失时。通过设置 pos_weight 和 reduction='none' ,这个损失函数能够适应目标检测任务中的类别不平衡问题,并允许对每个样本的损失进行单独处理。
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
# 使用 smooth_BCE 函数计算类别损失的平滑目标值。
# def smooth_BCE(eps=0.1):
# -> 用于计算平滑的二元交叉熵损失(Binary Cross-Entropy, BCE)的参数。函数返回两个值。平滑后的正类(positive class)损失的参数,通常用于计算正样本的损失。平滑后的负类(negative class)损失的参数,通常用于计算负样本的损失。
# -> return 1.0 - 0.5 * eps, 0.5 * eps
self.cp, self.cn = smooth_BCE(eps=h.get("label_smoothing", 0.0)) # positive, negative BCE targets
# Focal loss
# 获取焦点损失的 gamma 超参数。
g = h["fl_gamma"] # focal loss gamma
# 如果 gamma 大于0,则使用焦点损失包装 BCEcls 。
if g > 0:
# class FocalLoss(nn.Module):
# -> 用于实现焦点损失(Focal Loss)。焦点损失是一种专门为类别不平衡问题设计的损失函数,它通过减少易分类样本的权重并增加难分类样本的权重来改进模型性能。
# -> def __init__(self, loss_fcn, gamma=1.5, alpha=0.25):
BCEcls = FocalLoss(BCEcls, g)
# 获取模型的最后一个检测模块。
# def de_parallel(model):
# -> 将一个可能处于并行状态(例如使用 PyTorch 的 DataParallel 或 DistributedDataParallel 包装过的模型)转换回单个 GPU 或 CPU 上的模型。如果 model 是并行模型, 将返回原始的、未并行化的模型 model.module 。如果 model 不是并行模型,直接返回 model 。
# -> return model.module if is_parallel(model) else model
m = de_parallel(model).model[-1] # Detect() module
# 设置不同层的损失平衡系数。
# 这行代码在 ComputeLossLH 类的构造函数中初始化了一个名为 self.balance 的实例变量,它用于存储不同层次(P3-P7)的损失平衡系数。这些系数用于在目标检测模型训练过程中调整不同特征层(不同尺度)的损失贡献。
# 字典推导 : {3: [4.0, 1.0, 0.4]} 这是一个字典,其中键为3,值是一个列表 [4.0, 1.0, 0.4] 。这个列表包含了三个元素,分别对应于不同特征层的损失平衡系数。
# .get(m.nl, [4.0, 1.0, 0.25, 0.06, 0.02]) : .get() 方法用于从字典中获取与键 m.nl 对应的值。如果 m.nl 不是字典的键,则返回默认值 [4.0, 1.0, 0.25, 0.06, 0.02] 。 m.nl 表示模型中的特征层数量。这个值决定了使用哪组损失平衡系数。
# 损失平衡系数 :这些系数用于调整不同特征层的损失贡献。例如,如果模型有5个特征层(P3-P7),则使用 [4.0, 1.0, 0.25, 0.06, 0.02] 作为损失平衡系数。 这些系数通常是基于经验或通过实验确定的,用于优化模型在不同尺度上的性能。
# # P3-P7 :这是一个注释,说明这些损失平衡系数适用于从P3到P7的特征层。在目标检测模型中,P3、P4、P5、P6和P7通常表示不同尺度的特征层,它们对应于不同大小的目标。
# 通过字典查找和默认值设置,为不同数量的特征层配置了相应的损失平衡系数。这些系数将在损失计算中用于调整不同特征层的贡献,以优化模型的整体性能。
self.balance = {3: [4.0, 1.0, 0.4]}.get(m.nl, [4.0, 1.0, 0.25, 0.06, 0.02]) # P3-P7
# 保存模型参数和超参数。
# 将之前定义的 二元交叉熵损失函数 BCEcls 保存为类的实例变量。
self.BCEcls = BCEcls
# 将模型的 超参数 h 保存为类的实例变量。这些超参数可能包括学习率、优化器参数、损失函数参数等,它们在训练过程中用于控制模型的行为。
self.hyp = h
# 保存模型中使用的 步长 ( stride )。在目标检测模型中,不同的特征层可能有不同的步长,这影响了锚点(anchor)的放置和边界框的尺寸。
self.stride = m.stride # model strides
# 保存模型检测的 类别数量 。这对于计算类别损失和处理分类任务是必要的。
self.nc = m.nc # number of classes
# 保存模型中用于目标检测的 层数 。这可能影响损失计算和锚点的配置。
self.nl = m.nl # number of layers
# 保存模型输出的每个 锚点的数量 。这通常指的是每个网格点上锚点的数量。
self.no = m.no
# 保存 回归损失计算中使用的最大值 。这可能用于定义 边界框回归 的数值范围。 m.reg_max == c // 4
self.reg_max = m.reg_max
# 保存模型参数所在的 设备 (CPU或GPU)。这对于确保所有的计算都在正确的设备上执行是必要的。
self.device = device
# 初始化任务对齐的分配器。
# class TaskAlignedAssigner(nn.Module):
# -> 用于在目标检测任务中进行目标分配。这个类的主要作用是为每个预测的边界框分配最匹配的真实目标,并计算相应的目标标签和边界框。
# -> def __init__(self, topk=13, num_classes=80, alpha=1.0, beta=6.0, eps=1e-9):
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)))
# 初始化边界框损失计算器,并移动到正确的设备。
# class BboxLoss(nn.Module):
# -> 用于计算目标检测任务中的边界框损失。这个类包含了 IoU 损失和可选的 DFL(Distribution Focal Loss)损失。
# -> def __init__(self, reg_max, use_dfl=False):
self.bbox_loss = BboxLoss(m.reg_max - 1, use_dfl=use_dfl).to(device)
# 初始化投影张量,并移动到正确的设备。
# 这行代码在 ComputeLossLH 类的构造函数中初始化了一个名为 self.proj 的实例变量,它用于在目标检测模型中处理 边界框回归 任务。
# torch.arange(m.reg_max) :这个函数生成一个从0到 m.reg_max - 1 的整数序列。 m.reg_max 是模型中定义的回归范围的最大值,它决定了边界框可以预测的最大尺寸。
# # / 120.0 :这个注释,表明在某些情况下,这个序列可能需要除以120.0。这可能是为了将回归值缩放到一个特定的范围,或者是根据模型的具体实现来调整尺度。但在代码中并没有实际执行这个除法操作,它只是作为一个提示或备忘。
# self.proj 是一个浮点数张量,包含了从0到 m.reg_max - 1 的序列,并且已经被移动到了模型参数所在的设备上。这个张量在后续的边界框回归计算中会被用作参考或投影的基础。
self.proj = torch.arange(m.reg_max).float().to(device) # / 120.0
# 保存是否使用 DFL 的标志。
self.use_dfl = use_dfl
# 构造函数 __init__ 初始化了 ComputeLossLH 类,设置了损失函数、分配器和边界框损失计算器等关键组件,并根据模型的超参数和环境变量配置了这些组件。这个类的目的是提供一个统一的接口来计算目标检测模型的损失,包括类别损失、边界框损失和可选的分布焦点损失。
# 这段代码定义了 ComputeLossLH 类中的 preprocess 方法,用于预处理目标检测任务中的目标数据。
# 定义 preprocess 方法,它接受三个参数。
# 1.targets :包含目标数据的张量。
# 2.batch_size :批次大小。
# 3.scale_tensor :用于缩放目标边界框的张量。
def preprocess(self, targets, batch_size, scale_tensor):
# 如果 targets 张量中没有目标(即形状的第一个维度为0),则创建一个形状为 (batch_size, 0, 5) 的全零张量 out ,其中5代表每个目标的五个属性(类别标签和四个边界框坐标)。
if targets.shape[0] == 0:
out = torch.zeros(batch_size, 0, 5, device=self.device)
# 如果 targets 张量中有目标,获取每个目标的图像索引,这通常用于区分不同图像中的目标。
else:
i = targets[:, 0] # image index
# 计算每个图像索引出现的次数, counts 张量中包含每个唯一索引的计数。
# 这行代码使用了 PyTorch 的 unique 函数,它包含在 torch 模块中,用于找出张量 i 中的唯一值以及每个唯一值出现的次数。
# i :这是一个从 targets 张量中提取的子张量,包含了每个目标的图像索引。这个索引用于区分属于不同图像的目标。
# i.unique(return_counts=True) : unique 函数返回两个值 ,第一个是包含 i 中所有唯一值的张量,第二个是这些唯一值在原始张量 i 中出现的次数。 return_counts=True 参数告诉 unique 函数返回每个唯一值的出现次数。
# _, counts = i.unique(return_counts=True) :这里使用了解包(unpacking)操作,将 unique 函数返回的两个值分别赋值给两个变量。下划线 _ 是一个惯用的占位符,表示我们不关心第一个返回值(即唯一值本身),只关心第二个返回值(即每个唯一值的出现次数)。 counts 将包含一个张量,其形状为 (b,) ,其中 b 是批次大小,每个元素表示对应图像索引在 i 中出现的次数。
# 这行代码的目的是获取每个图像索引在 i 中出现的次数,并将这些次数保存在 counts 张量中,以便后续操作可以使用这些信息。
_, counts = i.unique(return_counts=True)
# 创建一个形状为 (batch_size, counts.max(), 5) 的全零张量 out ,其中 counts.max() 确保 out 的第二维度足够大,可以容纳任一图像中最多的目标数。
out = torch.zeros(batch_size, counts.max(), 5, device=self.device)
# 遍历每个批次,找到与当前批次索引匹配的目标,并将这些目标的属性(不包括图像索引)复制到 out 张量中。
# 循环遍历每个批次中的图像。 batch_size 是批次中图像的数量。
for j in range(batch_size):
# 对于每个图像索引 j , matches 是一个布尔掩码,它标识 targets 中哪些目标属于当前索引 j 的图像。这里 i 是从 targets 提取出的图像索引列。
matches = i == j
# 计算属于当前图像索引 j 的目标数量。 n 是匹配掩码 matches 中 True 值的总数。
n = matches.sum()
# 如果 n 大于0,意味着当前批次中存在属于该图像的目标。
if n:
# 将这些目标的属性(不包括图像索引)复制到 out 张量中对应的位置。 targets[matches, 1:] 选择 targets 中属于当前图像索引 j 的所有目标(除去第一个元素,即图像索引),并且排除了图像索引列。 out[j, :n] 指定 out 张量中当前图像索引 j 的行和前 n 列,用于存放这些目标信息。
out[j, :n] = targets[matches, 1:]
# 将 out 张量中的边界框从中心点坐标(x, y, w, h)转换为角点坐标(x1, y1, x2, y2),并使用 scale_tensor 对边界框进行缩放。
out[..., 1:5] = xywh2xyxy(out[..., 1:5].mul_(scale_tensor))
# 返回预处理后的 out 张量。
return out
# preprocess 方法的主要作用是将目标数据转换为模型训练中需要的格式,包括处理空目标的情况、按图像索引组织目标、将边界框格式从中心点坐标转换为角点坐标,并应用缩放。这样预处理后的数据可以直接用于损失函数的计算。
# 这段代码定义了 ComputeLossLH 类中的 bbox_decode 方法,用于将预测的距离分布转换为边界框坐标。
# 定义 bbox_decode 方法,它接受两个参数。
# 1.anchor_points :锚点坐标。
# 2.pred_dist :预测的距离分布。
def bbox_decode(self, anchor_points, pred_dist):
# 检查是否启用了分布焦点损失(DFL)。如果启用,将使用不同的解码方式。
if self.use_dfl:
# 获取 pred_dist 张量的形状,分别代表 批次大小 ( b )、 锚点数量 ( a )和 通道数 ( c )。
b, a, c = pred_dist.shape # batch, anchors, channels
# 将 pred_dist 张量重塑为 (b, a, 4, c // 4) 的形状,并在最后一个维度(通道)上应用 softmax 函数。这将对每个锚点的每个边界框维度(例如,左、上、右、下)的概率分布进行归一化。然后,使用 matmul 函数将归一化后的概率分布与 self.proj 相乘, self.proj 是一个从 0 到 m.reg_max 的整数张量,用于将分布转换为实际的距离值。
# 在目标检测模型中,预测的距离分布 pred_dist 通常表示为每个边界框的每个坐标(例如,左、上、右、下)的概率分布。这些概率分布是通过模型的回归层输出的,它们表示了边界框的每个坐标可能取值的概率。
# self.proj 是一个从 0 到 m.reg_max 的整数张量,其中 m.reg_max 是模型中定义的回归范围的最大值。这个张量的每个元素代表了一个可能的距离值。
# 当我们将归一化后的概率分布与 self.proj 相乘时,我们实际上是在计算每个边界框的每个坐标的期望值(或加权平均值)。这个期望值是所有可能的距离值的加权和,权重就是每个距离值的概率。因此,这个操作将概率分布转换为实际的距离值。
# 具体来说,如果 pred_dist 的形状是 (b, a, 4, c // 4) ,其中 c // 4 是每个边界框的每个坐标的概率分布的长度,那么 self.proj 的形状是 (c // 4) 。通过矩阵乘法( matmul ),我们得到一个新的张量,其形状是 (b, a, 4) ,每个元素是对应坐标的实际距离值。
# 这个过程是目标检测模型中从模型输出到最终边界框预测的关键步骤,它允许模型输出的概率分布被转换为实际的边界框坐标。
# 这行代码是 bbox_decode 方法中的一部分,用于将 预测的距离分布 pred_dist 转换为 边界框坐标 。
# pred_dist.view(b, a, 4, c // 4) :
# pred_dist 是模型输出的预测距离分布,其形状为 (b, a, c) ,其中 b 是批次大小, a 是锚点数量, c 是每个锚点的通道数。
# view(b, a, 4, c // 4) 将 pred_dist 重塑为一个新的形状,其中 c // 4 表示每个边界框有 4 个坐标(例如,左、上、右、下),因此将通道数 c 除以 4 来匹配这个结构。
# 这里的维度索引是从0开始计数的,所以 :第一个维度(索引0)是批次大小 b 。 第二个维度(索引1)是锚点数量 a 。 第三个维度(索引2)是边界框的四个坐标(例如,左、上、右、下)。 第四个维度(索引3)是每个坐标的概率分布。
# .softmax(3) :
# softmax(3) 沿着每个边界框的四个坐标的概率分布维度应用的,目的是对每个边界框的四个坐标的概率分布进行归一化。这样,每个边界框的坐标概率之和为1,这符合概率分布的要求。
# .matmul(self.proj.type(pred_dist.dtype)) :
# self.proj 是一个从 0 到 m.reg_max 的整数张量,用于将分布转换为实际的距离值。 m.reg_max 是模型中定义的回归范围的最大值。
# type(pred_dist.dtype) 确保 self.proj 的数据类型与 pred_dist 相同,以便进行矩阵乘法。
# matmul 函数执行矩阵乘法,将归一化后的概率分布与 self.proj 相乘,得到每个边界框的实际距离值。
# 这行代码的作用是将预测的距离分布转换为边界框坐标。首先,通过 view 调整张量的形状以匹配边界框的坐标结构;然后,使用 softmax 对每个边界框的坐标概率进行归一化;最后,通过 matmul 将归一化的概率分布转换为实际的距离值。这个过程是目标检测模型中从模型输出到最终边界框预测的关键步骤。
pred_dist = pred_dist.view(b, a, 4, c // 4).softmax(3).matmul(self.proj.type(pred_dist.dtype))
# 这行代码被注释掉了,它展示了另一种可能的 pred_dist 张量转换方式,但没有被实际使用。
# 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 = (pred_dist.view(b, a, c // 4, 4).softmax(2) * self.proj.type(pred_dist.dtype).view(1, 1, -1, 1)).sum(2)
# 调用 dist2bbox 函数,将预测的 距离分布 pred_dist 和 锚点 anchor_points 转换为边界框坐标。 xywh=False 参数表示输出的边界框坐标格式为 (x1, y1, x2, y2) ,而不是 (x, y, w, h) 。
# def dist2bbox(distance, anchor_points, xywh=True, dim=-1):
# -> 用于将从锚点(anchor points)出发的边界距离(通常表示为左上角和右下角的坐标)转换为边界框(bounding box)的坐标。将中心点和宽高在指定维度 dim 上拼接起来,返回xywh格式的边界框。将左上角和右下角坐标在指定维度 dim 上拼接起来,返回xyxy格式的边界框。
# -> return torch.cat((c_xy, wh), dim) # xywh bbox / return torch.cat((x1y1, x2y2), dim) # xyxy bbox
return dist2bbox(pred_dist, anchor_points, xywh=False)
# bbox_decode 方法的作用是将预测的距离分布转换为边界框坐标。如果启用了 DFL,它将使用 softmax 函数和 self.proj 将概率分布转换为实际的距离值,然后再将这些距离值转换为边界框坐标。这个过程是目标检测模型中从模型输出到最终边界框预测的关键步骤。
# 这段代码是 ComputeLossLH 类的 __call__ 方法,它是类的主入口点,用于计算给定模型预测 p 和目标 targets 的损失。
# 定义 __call__ 方法,这是类的实例被调用时执行的方法。它接受四个参数。
# 1.p :这是模型的输出。它通常是一个张量或张量的元组,包含了模型对于输入数据的预测结果。在目标检测模型中, p 包含了边界框的预测坐标、类别预测分数等。
# 2.targets :这是真实目标的数据,通常包含了边界框的标注信息和类别标签。 targets 用于计算损失函数,以衡量模型预测与真实情况之间的差异。
# 3.img :这是一个可选参数,代表输入图像本身。在某些情况下,原始图像可能用于损失计算,例如在某些图像重建任务中,或者用于调试和可视化目的。默认值为 None 。
# 4.epoch :这是一个可选参数,表示当前的训练周期(epoch)。在训练过程中, epoch 可用于动态调整损失函数的行为,例如改变某些超参数的值以实现学习率衰减或其他训练策略。默认值为 0 。
def __call__(self, p, targets, img=None, epoch=0):
# 这段代码是 ComputeLossLH 类的 __call__ 方法的一部分,它负责准备和处理模型输出以及目标数据,以便计算损失。
# 初始化一个包含三个元素的损失张量,分别用于存储 边界框损失 、 类别损失 和 分布焦点损失 ( DFL ) 。
loss = torch.zeros(3, device=self.device) # box, cls, dfl
# 获取模型输出的特征图。如果 p 是一个元组,那么 feats 是 p 的第一个元素的第一个子元素;否则, feats 就是 p 的第一个元素。
feats = p[1][0] if isinstance(p, tuple) else p[0]
# 获取模型输出的第二组特征图。这通常用于多尺度特征融合。
feats2 = p[1][1] if isinstance(p, tuple) else p[1]
# 将所有特征图 feats 的预测分布和类别分数在维度2上连接起来,然后分割成 预测分布 pred_distri 和 预测分数 pred_scores 。
# 这行代码在 ComputeLossLH 类的 __call__ 方法中用于处理模型输出,将预测的 边界框距离分布 和 类别分数 从特征图中提取出来。
# [xi.view(feats[0].shape[0], self.no, -1) for xi in feats] :
# 这是一个列表推导式,它遍历 feats 列表中的每个特征图 xi 。
# feats[0].shape[0] 获取批次大小(batch size),即每个特征图的第一维大小。
# self.no 是每个网格单元格上的锚点数量。
# xi.view(feats[0].shape[0], self.no, -1) 将每个特征图 xi 重塑为一个新的形状,其中 -1 表示自动计算该维度的大小,使得总元素数量保持不变。
# torch.cat(..., 2) :
# torch.cat 函数用于沿着指定的维度(这里是维度2)连接张量列表。
# 连接后的张量形状为 (batch_size, self.no, total_elements) ,其中 total_elements 是所有特征图的预测元素总数。
# .split((self.reg_max * 4, self.nc), 1) :
# split 函数用于将连接后的张量分割成两个部分。
# (self.reg_max * 4, self.nc) 指定了分割的大小,其中 self.reg_max * 4 是每个边界框的回归参数数量(通常4个值:x, y, w, h)乘以回归范围的最大值, self.nc 是类别数量。
# 分割是沿着维度1进行的,即每个网格单元格的输出被分割成 距离分布 pred_distri 和 类别分数 pred_scores 。
# pred_distri 和 pred_scores 分别包含了预测的 距离分布 和 类别分数 ,它们可以用于后续的损失计算。 pred_distri 的形状为 (batch_size, self.no, self.reg_max * 4) ,而 pred_scores 的形状为 (batch_size, self.no, self.nc) 。
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) # ❌ ⚠️ 不应该在维度1上进行分割,正确的应该是 在 形状为 (batch_size, self.no, total_elements)张量的 total_elements 维度,即维度2上进行分割。
# 调整 预测分数 和 预测分布 的维度顺序,并确保数据在内存中连续。
pred_scores = pred_scores.permute(0, 2, 1).contiguous()
pred_distri = pred_distri.permute(0, 2, 1).contiguous()
# 对第二组特征图执行相同的操作,得到 pred_distri2 和 pred_scores2 。
pred_distri2, pred_scores2 = torch.cat([xi.view(feats2[0].shape[0], self.no, -1) for xi in feats2], 2).split(
(self.reg_max * 4, self.nc), 1) # ❌ ⚠️ 不应该在维度1上进行分割,正确的应该是 在 形状为 (batch_size, self.no, total_elements)张量的 total_elements 维度,即维度2上进行分割。
# 调整第二组特征图的 预测分数 和 预测分布 的维度顺序,并确保数据在内存中连续。
pred_scores2 = pred_scores2.permute(0, 2, 1).contiguous()
pred_distri2 = pred_distri2.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)
# 生成 锚点 和 步长 张量,这些用于后续的边界框解码和损失计算。
# def make_anchors(feats, strides, grid_cell_offset=0.5): -> 用于生成YOLO模型中使用的锚点(anchor points)。使用 torch.cat 函数将 anchor_points 和 stride_tensor 列表中的所有张量连接起来,并返回结果。 -> return torch.cat(anchor_points), torch.cat(stride_tensor)
anchor_points, stride_tensor = make_anchors(feats, self.stride, 0.5)
# 这段代码负责处理模型输出,包括特征图的处理、预测分布和分数的提取、维度的调整以及锚点的生成。这些步骤是损失计算的基础,确保了数据的正确性和后续步骤的顺利进行。
# 这段代码是 ComputeLossLH 类的 __call__ 方法中的一部分,它负责处理目标检测任务中的目标数据。
# targets
# 调用 preprocess 方法对输入的 targets 数据进行预处理。 targets 包含每个目标的 类别标签 和 边界框 信息。 batch_size 是当前批次的大小, imgsz 是一个张量,包含了每个图像的尺寸(高度和宽度),这里使用 imgsz[[1, 0, 1, 0]] 来获取图像的尺寸并作为缩放因子 scale_tensor ,用于调整目标边界框的大小以匹配原始图像的尺寸。
# def preprocess(self, targets, batch_size, scale_tensor): -> 用于预处理目标检测任务中的目标数据。将 out 张量中的边界框从中心点坐标(x, y, w, h)转换为角点坐标(x1, y1, x2, y2),并使用 scale_tensor 对边界框进行缩放。返回预处理后的 out 张量。 -> return out
targets = self.preprocess(targets, batch_size, scale_tensor=imgsz[[1, 0, 1, 0]])
# 将预处理后的 targets 张量分割成两个部分。 gt_labels 和 gt_bboxes 。 targets 的形状是 (batch_size, max_objects, 5) ,其中最后一个维度的前1个元素是类别标签,接下来的4个元素是边界框的坐标(格式为 xyxy ,即左上角和右下角的坐标)。 split 方法按照指定的形状 (1, 4) 沿着第二个维度(索引为2)分割张量。
gt_labels, gt_bboxes = targets.split((1, 4), 2) # cls, xyxy
# 创建一个布尔掩码 mask_gt ,用于标识哪些目标是有效的(即边界框不为空)。 gt_bboxes.sum(2, keepdim=True) 计算每个目标边界框的坐标之和,并保持维度不变,结果是形状为 (batch_size, max_objects, 1) 的张量。 gt_bboxes.sum(2, keepdim=True).gt_(0) 检查这个总和是否大于0,从而确定每个目标是否有效(即边界框是否非空)。
mask_gt = gt_bboxes.sum(2, keepdim=True).gt_(0)
# 这段代码处理目标数据,将其分割成类别标签和边界框,并创建一个掩码来标识有效的目标。这些信息将被用于后续的损失计算,确保只有有效的目标被考虑进去。
# 这段代码是 ComputeLossLH 类的 __call__ 方法中的一部分,它负责解码预测的边界框并使用目标分配器来为每个预测分配真实目标。
# pboxes
# 调用 bbox_decode 方法将预测的 距离分布 pred_distri 和 锚点 anchor_points 转换为 边界框坐标 pred_bboxes 。输出的边界框格式是 xyxy ,即左上角和右下角的坐标,形状为 (b, h*w, 4) ,其中 b 是批次大小, h*w 是特征图上的网格点总数。
# def bbox_decode(self, anchor_points, pred_dist):
# -> 用于将预测的距离分布转换为边界框坐标。调用 dist2bbox 函数,将预测的 距离分布 pred_dist 和 锚点 anchor_points 转换为边界框坐标。 xywh=False 参数表示输出的边界框坐标格式为 (x1, y1, x2, y2) ,而不是 (x, y, w, h) 。
# -> return dist2bbox(pred_dist, anchor_points, xywh=False)
pred_bboxes = self.bbox_decode(anchor_points, pred_distri) # xyxy, (b, h*w, 4)
# 对第二组特征图执行相同的操作,得到 pred_bboxes2 。
pred_bboxes2 = self.bbox_decode(anchor_points, pred_distri2) # xyxy, (b, h*w, 4)
# 使用目标分配器 self.assigner 为预测分配目标。分配器返回 分配的目标标签 target_labels 、 边界框 target_bboxes 、 分数 target_scores 和 前景掩码 fg_mask 。
# def forward(self, pd_scores, pd_bboxes, anc_points, gt_labels, gt_bboxes, mask_gt):
# -> 返回 分配的目标标签 ( target_labels )、 目标边界框 ( target_bboxes )、 调整后的目标分数 ( target_scores )和 前景掩码 ( fg_mask )。
# -> return target_labels, target_bboxes, target_scores, fg_mask.bool()
target_labels, target_bboxes, target_scores, fg_mask = self.assigner(
# 预测分数的 sigmoid 激活。
pred_scores2.detach().sigmoid(),
# 预测边界框乘以步长并转换为 gt_bboxes 的数据类型。
(pred_bboxes2.detach() * stride_tensor).type(gt_bboxes.dtype),
# 锚点乘以步长。
anchor_points * stride_tensor,
# 真实目标的类别标签。
gt_labels,
# 真实目标的边界框。
gt_bboxes,
# 真实目标的掩码。
mask_gt)
# 将分配给预测的目标边界框除以步长,将它们从特征图坐标转换回原始图像坐标。❌
# ⚠️ 从上面self.assigner()得到的 真实边界框 target_bboxes 是在原始图像坐标系下的坐标,将它除以 步长 stride_tensor 得到的是在特征图坐标系下的坐标。上下文中从self.bbox_decode()得到的 预测边界框 pred_bboxes 它也是特征图坐标系下的坐标。
# ⚠️ 这两个坐标将用于下文self.bbox_loss()中计算损失。
# ⚠️ 在计算损失时涉及真实框与预测框的交并比(IoU)的计算。在计算交并比(IoU)时,真实框和预测框的坐标确实需要在同一坐标系下。通常情况下,这些坐标应该是相对于原始图像的绝对坐标,而不是特征图坐标系下的坐标。
# ⚠️ 我认为应该将 target_bboxes /= stride_tensor 代码改为 pred_bboxes *= stride_tensor ,即将 预测边界框 pred_bboxes 从特征图坐标系转换为原始坐标系,与本就是原始坐标系下的 真实边界框 target_bboxes 参与self.bbox_loss()中损失的计算。
target_bboxes /= stride_tensor
# 计算所有目标分数 target_scores 的总和。
target_scores_sum = target_scores.sum()
# 这段代码负责解码预测的边界框,使用目标分配器为每个预测分配真实目标,并调整边界框坐标。这些步骤是计算损失之前必要的准备工作,确保了预测和真实目标之间的正确匹配。
# 这段代码是 ComputeLossLH 类的 __call__ 方法中的一部分,它负责计算类别损失(cls loss)和边界框损失(bbox loss),以及分布焦点损失(dfl loss)。
# cls loss
# 类别损失(Class Loss)。
# 这行代码被注释掉了,它提到了一种可能的类别损失计算方法,即变分焦点损失(Varifocal Loss,VFL),但在这里没有被使用。
# loss[1] = self.varifocal_loss(pred_scores, target_scores, target_labels) / target_scores_sum # VFL way
# 计算类别损失,使用的是二元交叉熵损失函数 BCEcls 。 pred_scores 是模型预测的类别分数, target_scores 是目标分数(通常是 0 或 1,表示目标的存在)。损失计算后被所有目标分数求和,然后除以 target_scores_sum (目标分数的总和)以得到平均损失。
loss[1] = self.BCEcls(pred_scores, target_scores.to(dtype)).sum() / target_scores_sum # BCE
# 将类别损失乘以 0.25,作为损失的缩放因子。
loss[1] *= 0.25
# 对第二组特征图的预测分数 pred_scores2 和目标分数 target_scores 执行相同的类别损失计算,并将其加到之前的类别损失上。
loss[1] += self.BCEcls(pred_scores2, target_scores.to(dtype)).sum() / target_scores_sum # BCE
# 边界框损失(Bounding Box Loss)。
# bbox loss
# 如果存在前景目标( fg_mask.sum() 返回 True),则计算边界框损失和分布焦点损失。 bbox_loss 方法接受预测的距离分布、预测的边界框、锚点、目标边界框等参数,并返回边界框损失、分布焦点损失和交并比(IoU)。然后,这些损失被乘以 0.25。
if fg_mask.sum():
# def forward(self, pred_dist, pred_bboxes, anchor_points, target_bboxes, target_scores, target_scores_sum, fg_mask): -> 返回 IoU 损失 、 DFL 损失 和 IoU 值。 -> return loss_iou, loss_dfl, iou
loss[0], loss[2], iou = self.bbox_loss(pred_distri,
pred_bboxes,
anchor_points,
target_bboxes,
target_scores,
target_scores_sum,
fg_mask)
loss[0] *= 0.25
loss[2] *= 0.25
# 对第二组特征图执行相同的操作,并将损失加到之前的损失上。
if fg_mask.sum():
loss0_, loss2_, iou2 = self.bbox_loss(pred_distri2,
pred_bboxes2,
anchor_points,
target_bboxes,
target_scores,
target_scores_sum,
fg_mask)
loss[0] += loss0_
loss[2] += loss2_
# 将 边界框损 失乘以 7.5,作为损失的增益因子。
loss[0] *= 7.5 # box gain
# 将 类别损失 乘以 0.5,作为损失的增益因子。
loss[1] *= 0.5 # cls gain
# 将 分布焦点 损失乘以 1.5,作为损失的增益因子。
loss[2] *= 1.5 # dfl gain
# 返回总损失(所有损失的和乘以批次大小)和分离的损失(不包括梯度计算)。这样,总损失可以用于反向传播,而分离的损失可以用于监控和调试。
return loss.sum() * batch_size, loss.detach() # loss(box, cls, dfl)
# 这段代码负责计算类别损失和边界框损失,以及可选的分布焦点损失。它处理了两组特征图的损失,并应用了不同的缩放因子和增益因子,以平衡不同损失对总损失的贡献。最终返回的总损失用于模型的训练,而分离的损失用于监控和调试。
# __call__ 方法集成了多种损失计算方式,包括类别损失、边界框损失和可选的分布焦点损失。它处理了两组特征图的损失,并应用了不同的缩放因子和增益因子,以平衡不同损失对总损失的贡献。最终返回的总损失用于模型的训练,而分离的损失用于监控和调试。
# ComputeLossLH 类集成了多种损失计算方式,包括类别损失、边界框损失和可选的分布焦点损失。它处理了两组特征图的损失,并应用了不同的缩放因子和增益因子,以平衡不同损失对总损失的贡献。最终返回的总损失用于模型的训练,而分离的损失用于监控和调试。