YOLOv8 | 代码逐行解析(五) | YOLOv8中损失函数计算的详解包含Cls和Bbox计算的解析,小白必看(下)

一、本文介绍

本文给大家带来的是YOLOv8中的损失函数计算的完整解析,内容包括v8DetectionLoss的解析,以及BboxLoss的解析,如果你相对损失函数的计算原理,本文内容绝对会对你有所帮助,全文内容包含1万两千字,手打分析文字超过5000字,全部为干货内容,包含示例解释辅助大家理解,对于小白来说十分适合阅读,以下图片内容为文章中部分的解析截图,本文内容为独家整理和理解全网无第二份。

欢迎大家订阅我的专栏一起学习YOLO!    

专栏目录:YOLOv8改进有效系列目录 | 包含卷积、主干、检测头、注意力机制、Neck上百种创新机制

专栏回顾:YOLOv8改进系列专栏——本专栏持续复习各种顶会内容——科研必备    


目录

一、本文介绍

二、代码解析

2.1  v8DetectionLoss解析

2.2  BboxLoss

2.3  bbox_iou

2.4 DFL 

2.5 make_anchors

2.6 dist2bbox

三、本文总结


二、代码解析

本文内容在观看之前需要大家先阅读了我上一个检测头的分析因为两篇文章内容有些概念需要理解。

YOLOv8 | 代码逐行解析(四) | YOLOv8中从检测头到损失函数计算的详解,小白必看(上)


2.1  v8DetectionLoss解析

 YOLOv8损失函数计算的代码在'ultralytics/utils/loss.py'中(下面的代码),我们可以在下面的文件中找到下面的代码,代码的解析我已经在代码中注释给出大家可以在其中看到。

class v8DetectionLoss:"""Criterion class for computing training losses."""# 这个类就是我们YOLOv8目标检测的损失函数!def __init__(self, model, tal_topk=10):  # model must be de-paralleled"""Initializes v8DetectionLoss with the model, defining model-related properties and BCE loss function."""device = next(model.parameters()).device  # # 获取模型所在的设备h = model.args  # 超参数列表,这个model.args返回的就是我们ultralytics/cfg/default.yaml文件下的所有参数设定!m = model.model[-1]  # 这一步就是获取我们的模型最后一层(检测头,上一章已经讲过了),这里获取过来就是为了获取其中的参数.self.bce = nn.BCEWithLogitsLoss(reduction="none")  # 二分类交叉熵损失self.hyp = h # 这个h就是我们上面的所有超参数!self.stride = m.stride  # 这个参数大家需要看我上一章文章解析的部分才明白,这个是特张图图像的缩放比例不懂的可以返回去看看.self.nc = m.nc  # number of classes  我们数据集中的类别数量self.no = m.nc + m.reg_max * 4 # 这个也解释过了在上一章! nc是类别数量 reg_max是每个位置信息的预测数量, 4 代表四个位置!# x - 边界框的中心点 x 坐标# y - 边界框的中心点 y 坐标# w - 边界框的宽度# h - 边界框的高度self.reg_max = m.reg_max  # DFL 通道数量 (每个预测框的回归输出通道数)self.device = device # 模型设备self.use_dfl = m.reg_max > 1 # 一个简单的判断其实没人必要大家不用理会!self.assigner = TaskAlignedAssigner(topk=tal_topk, num_classes=self.nc, alpha=0.5, beta=6.0) # 任务对齐分配器self.bbox_loss = BboxLoss(m.reg_max).to(device) # 边界框损失计算函数self.proj = torch.arange(m.reg_max, dtype=torch.float, device=device) # 投影张量def preprocess(self, targets, batch_size, scale_tensor):"""Preprocesses the target counts and matches with the input batch size to output a tensor."""nl, ne = targets.shape  # 获取 targets 的形状if nl == 0:  # 如果没有目标out = torch.zeros(batch_size, 0, ne - 1, device=self.device)  # 返回一个空张量else:i = targets[:, 0]  # 获取图像索引_, counts = i.unique(return_counts=True)  # 获取每个图像的目标计数counts = counts.to(dtype=torch.int32)out = torch.zeros(batch_size, counts.max(), ne - 1, device=self.device)  # 初始化输出张量for j in range(batch_size):  # 遍历每个批次matches = i == j  # 匹配当前批次的目标n = matches.sum()  # 目标数量if n:out[j, :n] = targets[matches, 1:]  # 填充目标out[..., 1:5] = xywh2xyxy(out[..., 1:5].mul_(scale_tensor))  # 转换坐标return outdef bbox_decode(self, anchor_points, pred_dist):"""从锚点和分布预测中解码出预测的目标边界框坐标。参数:anchor_points (torch.Tensor): 锚点坐标,形状为 [num_anchors, 2]。pred_dist (torch.Tensor): 预测的边界框分布,形状为 [batch_size, num_anchors, num_channels]。返回:torch.Tensor: 解码后的边界框坐标,形状为 [batch_size, num_anchors, 4]。"""if self.use_dfl:b, a, c = pred_dist.shape  # 获取 batch 大小、锚点数量和通道数# 将预测分布变形为 [batch_size, num_anchors, 4, num_channels // 4],并在通道维度上应用 softmaxpred_dist = pred_dist.view(b, a, 4, c // 4).softmax(3).matmul(self.proj.type(pred_dist.dtype))# pred_dist = pred_dist.view(b, a, c // 4, 4).transpose(2,3).softmax(3).matmul(self.proj.type(pred_dist.dtype))# pred_dist = (pred_dist.view(b, a, c // 4, 4).softmax(2) * self.proj.type(pred_dist.dtype).view(1, 1, -1, 1)).sum(2)return dist2bbox(pred_dist, anchor_points, xywh=False)def __call__(self, preds, batch):"""Calculate the sum of the loss for box, cls and dfl multiplied by batch size."""loss = torch.zeros(3, device=self.device)  # box, cls, dfl 初始化损失张量初始全是0feats = preds[1] if isinstance(preds, tuple) else preds # 这里有两种不同情况!# 情况1(训练时)在检测头代码中我们如果是训练则直接return x不知道大家还记得不,不记得需要回去看看!# 在情况1时我们此时返回的是list列表,其中返回的形状是我的nc=25# (batch_size, nc + reg_max * 4=89, 80, 80)# (batch_size, nc + reg_max * 4=89, 40, 40)# (batch_size, nc + reg_max * 4=89, 20, 20)# 情况2(训练时的验证阶段)此时我们返回的是元组# 元组里面包含了上面的x除此之外还额外包含我们预测的信息# 形状为(batch_size * 2, nc + 四个位置的信息 = 29, 20 * 20 + 40 * 40 + 80 * 80 = 8400)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)# 上面的一行代码操作含义是首先执行xi.view(feats[0].shape[0], self.no, -1) for xi in feats# 这是将# (batch_size, nc + reg_max * 4=89, 80, 80)# (batch_size, nc + reg_max * 4=89, 40, 40)# (batch_size, nc + reg_max * 4=89, 20, 20)# 进行处理成[batch_size, 89, 80 * 80 = 6400]# 进行处理成[batch_size, 89, 40 * 40 = 1600]# 进行处理成[batch_size, 89, 20 * 20 = 400]# 之后进行torch.cat([xi.view(feats[0].shape[0], self.no, -1) for xi in feats], 2)# 变为[batch_size, 89, 6400 + 1600 + 400 = 8400] 大家需要明白cat的意义, dim =2# 之后进行torch.cat([xi.view(feats[0].shape[0], self.no, -1) for xi in feats], 2).split((self.reg_max * 4, self.nc), 1)# 其中的split会将tensor进行分割沿着维度1变为如果这个都不明白说明大家的Pytorch学的不是很扎实!#(batch_size, 64, 8400)#(batch_size, 25, 8400)# 然后会将上面两个tensor分别按照顺序赋值给pred_distri, pred_scores,这两个变量代表的信息如下:# pred_distri表示模型预测的每个锚点位置的边界框分布(共8400个锚点)# pred_scores表示模型预测的每个锚点位置的分类分数 (共8400个锚点)pred_scores = pred_scores.permute(0, 2, 1).contiguous() # 张量转置操作,(batch_size, 25, 8400)变为形状为 (batch_size, 8400, 25)pred_distri = pred_distri.permute(0, 2, 1).contiguous() # 张量转置操作,(batch_size, 64, 8400)变为形状为 (batch_size, 8400, 64)dtype = pred_scores.dtype # 获得设备大家不用理batch_size = pred_scores.shape[0] # 获得第一个位置的形状即 batch_sizeimgsz = torch.tensor(feats[0].shape[2:], device=self.device, dtype=dtype) * self.stride[0]  # image size (h,w)# 这个imgsz是一个我们模型真实输入的大小的一个的tensor 大部分默认的即(640,640)anchor_points, stride_tensor = make_anchors(feats, self.stride, 0.5) # 这里在检测头里也调用了之前讲过了# 上面这一步的含义涉及到了代码make_anchors其返回的两个变量我们需要理解一下self.anchors, self.strides# anchor_points形状为(2, 8400)其中的含义是坐标信息(中心点的坐标!!),# stride_tensor为我们的比例信息每一个像素点的# 这是参数的含义不懂得返回上一章看看可以!# Targetstargets = torch.cat((batch["batch_idx"].view(-1, 1), batch["cls"].view(-1, 1), batch["bboxes"]), 1)# batch["batch_idx"]:批次中每个目标对应的图像索引 1个数字# batch["cls"]:每个目标的类别标签 1个数字# batch["bboxes"]:每个目标的边界框坐标 4个数字# 这一行代码的主要目的是将批次中的图像索引、类别标签和边界框坐标拼接在一起,形成一个新的张量 targets,其中每行包含一个目标的完整信息。# 我举例一个(图像索引, 类别标签, x1, y1, x2, y2) 其中x1y1x2y2 为边界框的左上点和右下点的坐标两个点确定一个边界框!targets = self.preprocess(targets.to(self.device), batch_size, scale_tensor=imgsz[[1, 0, 1, 0]])# 上面一行代码是对targets进一步处理方便我们后面进行操作!预处理步骤包括将目标数据移动到设备上、重新调整目标数据的格式和坐标转换。# 返回的targets张量形状为三维(batch_size, 批次中最多目标的数量, 每个目标的信息维度(类别和边界框信息))# 需要注意的是这里涉及到补0的操作, 因为第二个维度上是批次中最多目标的数量, 那么batch_size代表不同的图片,目标数量不足批次中最多目标的数量的照片,多余的数量位置就会补0!gt_labels, gt_bboxes = targets.split((1, 4), 2)  # cls, xyxy 这一步是将我们上面得到的 targets第2个维度即(每个目标的信息维度(类别和边界框信息))进行拆分开为cls(真实的类别标签)和 xyxy(边界框信息)# gt_labels = [batch_size, max_num_target=批次中最多目标的数量, 1] 其中1是类别信息# gt_bboxes = [batch_size, max_num_targets=批次中最多目标的数量, 4] 其中4是边界框信息mask_gt = gt_bboxes.sum(2, keepdim=True).gt_(0.0)# 这行代码的作用是创建一个掩码张量 mask_gt,用于标识哪些目标是有效的(即有实际的边界框),哪些目标是无效的(即填充的零)。# sum(2, keepdim=True):沿着第 2 维(即每个边界框的坐标维度)进行求和,保持维度不变。# 这样会将每个边界框的 4 个坐标值相加,如果该目标是有效的边界框,和将大于 0;如果该目标是填充的零,和将等于 0。# Pboxespred_bboxes = self.bbox_decode(anchor_points, pred_distri)  # xyxy, (b, h*w, 4)# 这行代码的作用是调用 bbox_decode 方法,将预测的边界框分布 (pred_distri)# 解码为实际的边界框坐标 (pred_bboxes),具体返回的是左上角和右下角的坐标 (xyxy) 格式# 其中bbox_decode我在上一个文章已经讲过了大家可以去看,这里不在重复解释了。# pred_bboxes:形状为 [batch_size, 8400, 4] 的张量,表示解码后的实际边界框坐标(左上角和右下角即xyxy已经和前面保持一致了)_, target_bboxes, target_scores, fg_mask, _ = self.assigner(pred_scores.detach().sigmoid(),(pred_bboxes.detach() * stride_tensor).type(gt_bboxes.dtype),anchor_points * stride_tensor,gt_labels,gt_bboxes,mask_gt,)# self.assigner 是一个 TaskAlignedAssigner 对象,其 forward 方法用于将预测结果与真实目标进行匹配。# 上面这几行代码需要解释一下,但是比较麻烦其代码内部处理我不过多的解释了内容有200多行全是计算,有兴趣的可以看一下,我这里只解释一下输入和输出的参数含义!"""输入(共6个输入值)1.pred_scores:表示模型预测的每个锚点位置的分类分数 形状为:(batch_size, 8400, 25)2.(pred_bboxes.detach() * stride_tensor).type(gt_bboxes.dtype)pred_bboxes:解码后的边界框坐标,形状为 [batch_size, 8400, 4]。detach():从计算图中分离出预测的边界框,避免反向传播时更新它们。* stride_tensor:将边界框坐标乘以步幅张量 stride_tensor,恢复到原图尺度。type(gt_bboxes.dtype):将边界框的类型转换为与 gt_bboxes 相同的类型。3.anchor_points * stride_tensoranchor_points:锚点位置,形状为 [num_anchors, 2]将锚点位置乘以步幅张量 stride_tensor,恢复到原图尺度4.gt_labels:真实目标的类别标签5.gt_bboxes:真实目标的边界框坐标6.mask_gt:掩码张量,标识哪些目标是有效的""""""输出(共5个返回值)target_labels, 这里用_代替了:形状为 [batch_size, num_anchors],包含每个锚点的目标标签target_bboxes:形状为 [batch_size, num_anchors, 4],包含每个锚点的目标边界框target_scores:形状为 [batch_size, num_anchors, num_classes],包含每个锚点的目标得分。fg_mask:形状为 [batch_size, num_anchors],标识哪些锚点是前景(即有效的目标, 正样本)。fg_mask作用:标识哪些锚点是前景(正样本),哪些是背景(负样本)。正样本:锚点被分配给一个真实的目标,表示这个锚点负责检测这个目标。负样本:锚点未被分配给任何目标,表示这个锚点不负责检测任何目标target_gt_idx, 这里用_代替了:形状为 [batch_size, num_anchors],包含每个锚点对应的真实目标索引。"""target_scores_sum = max(target_scores.sum(), 1) # 求和# 这行代码通过计算 target_scores 张量中所有分数的总和,并确保这个总和至少为 1,来避免在后续的损失计算中出现除以零的情况。# Cls loss# loss[1] = self.varifocal_loss(pred_scores, target_scores, target_labels) / target_scores_sum  # VFL wayloss[1] = self.bce(pred_scores, target_scores.to(dtype)).sum() / target_scores_sum  # BCE# 这一步就是计算cls损失了,就是我们打印在控制台的cls Loss,# 损失计算概念给大家简单解释一下,简单来说求是真实值和预测值求差值!# 所以我们需要输入标签的预测值pred_scores和真实值target_scores来求bce二分类交叉熵损失.# 但是后面跟了.sum() / target_scores_sum大家会发现,这是因为我们上面的pred_scores, target_scores为每一个锚点的计算值返回.# 然后我们求和之后在除以张量中所有分数的总和, 这是为了标准化损失,避免由于正样本数量不同而造成的影响(就是为了避免极端值的出现会对模型的导引很大也是为了稳定训练)。# Bbox lossif fg_mask.sum(): # 对fg_mask进行.sum()求和上面讲了这个fg_mask里面是布尔值,但是也是可以求和的True为1, False为0, 如果有一个是正样本是 1 则会进行边界框损失计算, 这是python基础问题怕大家有疑问解释一下!target_bboxes /= stride_tensor# 将目标边界框的坐标除以步幅,恢复到特征图尺度(这里可以看到stride这个参数其实很重要的需要辅助我们真是图和特征图之间相互转化大家需要理解!)loss[0], loss[2] = self.bbox_loss(pred_distri, pred_bboxes, anchor_points, target_bboxes, target_scores, target_scores_sum, fg_mask)# 上面一行代码在下面的内容中讲了loss[0] *= self.hyp.box  # box gain 边界框损失loss[1] *= self.hyp.cls  # cls gain 分类损失loss[2] *= self.hyp.dfl  # dfl gain DFL损失(之前讲过作用了上一章不懂的可以回去看看)# 到此我们就全部分析了,损失函数计算完剩下的就是反向传播了就是黑盒内容了.return loss.sum() * batch_size, loss.detach()  # loss(box, cls, dfl)


2.2  BboxLoss

在上面v8DetectionLoss中我们还有部分内容没有解析到,下面是BboxLoss的解析,

class BboxLoss(nn.Module):"""在训练期间计算训练损失的准则类。"""def __init__(self, reg_max=16):"""初始化 BboxLoss 模块,设置正则化最大值和 DFL 设置。参数:reg_max (int): 回归的最大值。如果大于 1,则使用 DFL。"""super().__init__()self.dfl_loss = DFLoss(reg_max) if reg_max > 1 else None  # 如果 reg_max > 1,则初始化 DFLossdef forward(self, pred_dist, pred_bboxes, anchor_points, target_bboxes, target_scores, target_scores_sum, fg_mask):"""计算 IoU 损失和可选的 DFL 损失。参数:pred_dist (Tensor): 预测的边界框分布。pred_bboxes (Tensor): 预测的边界框。anchor_points (Tensor): 锚点。target_bboxes (Tensor): 目标(真实)边界框。target_scores (Tensor): 每个锚点的目标(真实)分数。target_scores_sum (Tensor): 目标分数的总和。fg_mask (Tensor): 指示正样本的前景掩码。返回:Tuple[Tensor, Tensor]: IoU 损失和 DFL 损失。"""# 计算前景样本(正样本)的权重weight = target_scores.sum(-1)[fg_mask].unsqueeze(-1)# 计算 IoU 损失iou = bbox_iou(pred_bboxes[fg_mask], target_bboxes[fg_mask], xywh=False, CIoU=True)# 大家可能最想知道这里的bbox_iou是如何计算的毕竟我们很多人都修改了损失函数,但是这里面有很多的选项,# 我只讲其中的一些关键部分不可能每一个损失的计算都讲解,有兴趣其实可以看对应损失函数的论文即可。loss_iou = ((1.0 - iou) * weight).sum() / target_scores_sum# 对iou进行加权、求和和标准化,得到最终的 IoU 损失值。这种方法确保了损失的计算考虑了样本的置信度,并且损失值是标准化的# 如果使用 DFL,则计算 DFL 损失if self.dfl_loss: # DFL损失的计算这里不多讲了之前讲过了在上一章target_ltrb = bbox2dist(anchor_points, target_bboxes, self.dfl_loss.reg_max - 1)loss_dfl = self.dfl_loss(pred_dist[fg_mask].view(-1, self.dfl_loss.reg_max), target_ltrb[fg_mask]) * weightloss_dfl = loss_dfl.sum() / target_scores_sumelse:loss_dfl = torch.tensor(0.0).to(pred_dist.device) # 执行不到return loss_iou, loss_dfl # 返回损失值

下面的内容在上一章已经介绍过了,给大家做个回顾,不需要的可以跳过了本文内容到此就结束了!


2.3  bbox_iou

在BboxLoss中还涉及到bbox_iou的计算也就是我们平时修改损失函数的代码,这一部分大家可以仔细看看涉及到IoU的计算.

def bbox_iou(box1, box2, xywh=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-7):"""计算 box1 (1, 4) 和 box2 (n, 4) 的 IoU。参数:box1 (torch.Tensor): 表示单个边界框的张量,形状为 (1, 4)。box2 (torch.Tensor): 表示 n 个边界框的张量,形状为 (n, 4)。xywh (bool, optional): 如果为 True,输入的框格式为 (x, y, w, h)。如果为 False,输入的框格式为 (x1, y1, x2, y2)。默认值为 True(但是我们这里是False,因为外部给设置为False了)不知道大家记不记得我前面讲了坐标为xyxy的形式。GIoU (bool, optional): 如果为 True,计算广义 IoU。默认值为 False。DIoU (bool, optional): 如果为 True,计算距离 IoU。默认值为 False。CIoU (bool, optional): 如果为 True,计算完全 IoU。默认值为 False。eps (float, optional): 防止除零的小值。默认值为 1e-7。返回:(torch.Tensor): 根据指定的标志返回 IoU、GIoU、DIoU 或 CIoU 值。"""# 获取边界框的坐标if xywh:  # 从 xywh 转换为 xyxy 坐标转换内容都是公式不多解释了。(x1, y1, w1, h1), (x2, y2, w2, h2) = box1.chunk(4, -1), box2.chunk(4, -1)w1_, h1_, w2_, h2_ = w1 / 2, h1 / 2, w2 / 2, h2 / 2b1_x1, b1_x2, b1_y1, b1_y2 = x1 - w1_, x1 + w1_, y1 - h1_, y1 + h1_b2_x1, b2_x2, b2_y1, b2_y2 = x2 - w2_, x2 + w2_, y2 - h2_, y2 + h2_else:  # x1, y1, x2, y2 = box1b1_x1, b1_y1, b1_x2, b1_y2 = box1.chunk(4, -1)b2_x1, b2_y1, b2_x2, b2_y2 = box2.chunk(4, -1)w1, h1 = b1_x2 - b1_x1, b1_y2 - b1_y1 + epsw2, h2 = b2_x2 - b2_x1, b2_y2 - b2_y1 + eps# 交集区域, 这段代码的作用是计算两个边界框的交集区域面积。具体来说,它通过计算两个边界框在 x 轴和 y 轴方向上的重叠部分,进而求出交集区域的面积# 大家可以想象两个正方形然和交际的部分内容,如果你懂IoU那么对这种描述应该是有一个内心绘图的.inter = (b1_x2.minimum(b2_x2) - b1_x1.maximum(b2_x1)).clamp_(0) * (b1_y2.minimum(b2_y2) - b1_y1.maximum(b2_y1)).clamp_(0)# 并集区域union = w1 * h1 + w2 * h2 - inter + eps# Intersection over Union (IoU) 在中文中的翻译是交并比# 那么计算公式就很明显了并集面积/交集面积就是下面# IoUiou = inter / unionif CIoU or DIoU or GIoU:# 这里就属于各种损失函数的计算了,大家看各自的论文内容解释就行了。# 最小包围盒的宽度cw = b1_x2.maximum(b2_x2) - b1_x1.minimum(b2_x1)# 最小包围盒的高度ch = b1_y2.maximum(b2_y2) - b1_y1.minimum(b2_y1)if CIoU or DIoU:  # 距离或完全 IoU https://arxiv.org/abs/1911.08287v1c2 = cw.pow(2) + ch.pow(2) + eps  # 最小包围盒对角线的平方rho2 = ((b2_x1 + b2_x2 - b1_x1 - b1_x2).pow(2) + (b2_y1 + b2_y2 - b1_y1 - b1_y2).pow(2)) / 4  # 中心距离的平方if CIoU:  # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47v = (4 / math.pi**2) * ((w2 / h2).atan() - (w1 / h1).atan()).pow(2)with torch.no_grad():alpha = v / (v - iou + (1 + eps))return iou - (rho2 / c2 + v * alpha)  # CIoUreturn iou - rho2 / c2  # DIoUc_area = cw * ch + eps  # 最小包围盒的面积return iou - (c_area - union) / c_area  # GIoU https://arxiv.org/pdf/1902.09630.pdfreturn iou  # IoU

部分内容回顾!

 

2.4 DFL 

DFL是一个用于对象检测的损失函数模块,主要用于提高边界框回归的精度。它的核心思想是将每个预测的边界框参数(如 x, y, w, h)分解为多个通道,然后通过 softmax 操作得到一个分布,并计算分布的积分来预测实际值。

下面是部分代码解析!

class DFL(nn.Module):"""Integral module of Distribution Focal Loss (DFL).Proposed in Generalized Focal Loss https://ieeexplore.ieee.org/document/9792391"""def __init__(self, c1=16):"""Initialize a convolutional layer with a given number of input channels."""super().__init__()self.conv = nn.Conv2d(c1, 1, 1, bias=False).requires_grad_(False)x = torch.arange(c1, dtype=torch.float)self.conv.weight.data[:] = nn.Parameter(x.view(1, c1, 1, 1))self.c1 = c1

c1:表示输入通道数,默认为 16。
self.conv:定义了一个卷积层,输入通道数为 c1`,输出通道数为 1,卷积核大小为 1x1。这个卷积层的权重初始化为 0 到 c1-1 的浮点数,并且不更新requires_grad_(False)不更新梯度的意思代表。
self.c1:保存输入通道数。

def forward(self, x):"""Applies a transformer layer on input tensor 'x' and returns a tensor."""b, _, a = x.shape  # batch, channels, anchorsreturn self.conv(x.view(b, 4, self.c1, a).transpose(2, 1).softmax(1)).view(b, 4, a)

输入 x:形状为 [batch_size, channels, anchors]的张量,其中 channels为 4× c1,每个边界框参数(如 x, y, w, h)都有 c1个通道(这里和我们前面的解释一致)。

下面的操作为正向传播中最后一行代码的解析! 

1.变形操作: 

   x.view(b, 4, self.c1, a)

将 x变形为 [batch_size, 4, c1, anchors]的张量,其中 4 表示四个边界框参数(x, y, w, h),c1 是每个参数的通道数。

2. 转置操作:

.transpose(2, 1)

将张量转置为 [batch_size, c1, 4, anchors],将通道维度 c1 和参数维度 4交换。

3. softmax 操作:

   .softmax(1)

在通道维度 c1上应用 softmax,得到每个参数的概率分布。

4. 卷积操作:

self.conv(...)

使用卷积层 self.conv将分布的积分计算出来。卷积层的权重初始化为 0 到 c1-1,这样卷积操作实际上计算的是这些通道的加权平均值。

5. 变形回原始形状:

.view(b, 4, a)

最终将输出变形为 [batch_size, 4, anchors],即每个锚点的四个边界框参数。

具体操作流程

1. 输入形状:假设输入张量 x的形状为 [2, 64, 100](batch_size=2,channels=64,anchors=100),其中 64 是 4 × c1(每个参数 16 个通道)。
2. 变形:将输入张量变形为 [2, 4, 16, 100],表示每个锚点的四个参数(x, y, w, h),每个参数有 16 个通道。
3. 转置:将张量转置为 [2, 16, 4, 100]。
4. softmax:在通道维度 16 上应用 softmax,得到每个参数的概率分布。
5. 卷积:使用卷积层计算分布的积分,得到每个参数的加权平均值。
6. 输出形状:将输出变形为 [2, 4, 100],即每个锚点的四个边界框参数。

DFL 模块通过将每个边界框参数分解为多个通道,使用 softmax 获得概率分布,然后通过卷积计算分布的积分来预测实际的边界框参数。这样的方法可以提高边界框回归的精度。


2.5 make_anchors

 下面的代码是我们检测头代码中详解没有解析的内容大家可以去上面找一下!

def make_anchors(feats, strides, grid_cell_offset=0.5):"""Generate anchors from features."""anchor_points, stride_tensor = [], []assert feats is not Nonedtype, device = feats[0].dtype, feats[0].devicefor i, stride in enumerate(strides):_, _, h, w = feats[i].shapesx = torch.arange(end=w, device=device, dtype=dtype) + grid_cell_offset  # shift xsy = torch.arange(end=h, device=device, dtype=dtype) + grid_cell_offset  # shift ysy, sx = torch.meshgrid(sy, sx, indexing="ij") if TORCH_1_10 else torch.meshgrid(sy, sx)anchor_points.append(torch.stack((sx, sy), -1).view(-1, 2))stride_tensor.append(torch.full((h * w, 1), stride, dtype=dtype, device=device))return torch.cat(anchor_points), torch.cat(stride_tensor)

 好的,让我们具体解释一下每个 anchor 如何表示特征图中每个单元格的位置(需要先理解的概念)。

什么是 Anchor

在对象检测中,anchor 是一个预定义的边界框模板,用于在特征图的每个单元格(即特征图的每个位置)上进行预测。每个 anchor 具有固定的大小和形状,模型通过调整这些 anchors 来拟合实际的物体边界框。

特征图和输入图像

假设我们有一个输入图像和对应的特征图:

  • 输入图像大小:640x640
  • 特征图大小:80x80

特征图的每个单元格对应于输入图像的一个区域。具体来说,特征图的一个单元格覆盖输入图像的 8x8 像素(假设 stride 为 8,前面讲过如何计算就是放缩比例640 ÷ 80 = 8)。

Anchor 在特征图中的位置

当我们在特征图上生成 anchors 时,每个单元格中心都会有一个 anchor。anchor 的位置用单元格的坐标表示(例如,特征图的第 (i, j) 个单元格,这些其实是图像基础的内容)。### 具体实现

在 make_anchors函数中,我们通过以下步骤生成 anchors(代码中内容的解析):

1. 生成网格点坐标

sx = torch.arange(end=w, device=device, dtype=dtype) + grid_cell_offset  # shift x
sy = torch.arange(end=h, device=device, dtype=dtype) + grid_cell_offset  # shift y
  • torch.arange(end=w, device=device, dtype=dtype)生成从 0 到 w-1 的序列,表示特征图宽度方向上的索引。
  • torch.arange(end=h, device=device, dtype=dtype)生成从 0 到 h-1 的序列,表示特征图高度方向上的索引。
  • grid_cell_offset(默认为 0.5)用于将网格点移动到单元格的中心。

2. 生成网格坐标的所有组合

sy, sx = torch.meshgrid(sy, sx, indexing="ij") if TORCH_1_10 else torch.meshgrid(sy, sx)
  • torch.meshgrid(sy, sx)生成网格点的所有组合,即每个单元格的中心坐标。

3. 组合并展平

anchor_points.append(torch.stack((sx, sy), -1).view(-1, 2))
  • torch.stack((sx, sy), -1)将 x 和 y 坐标堆叠在一起,形成形状为 (h, w, 2)的张量。
  • .view(-1, 2) 将其展平为形状为 (h * w, 2)的二维张量,其中每一行表示一个 anchor 的中心点坐标。

例子说明(用具体例子给大家阐述一下辅助大家理解一下)

假设我们有一个 3x3 的特征图,其单元格的中心坐标如下:
(0.5, 0.5)   (1.5, 0.5)   (2.5, 0.5)
(0.5, 1.5)   (1.5, 1.5)   (2.5, 1.5)
(0.5, 2.5)   (1.5, 2.5)   (2.5, 2.5)

使用 make_anchors函数生成的 anchor points 将是一个形状为 (9, 2)的张量 (数学内容),每一行表示一个单元格的中心坐标(其中的.5为偏移量!):
[[0.5, 0.5],
 [1.5, 0.5],
 [2.5, 0.5],
 [0.5, 1.5],
 [1.5, 1.5],
 [2.5, 1.5],
 [0.5, 2.5],
 [1.5, 2.5],
 [2.5, 2.5]]

总结

每个 anchor 表示特征图中每个单元格的位置,这些位置是特征图网格点的中心坐标。通过 make_anchors函数,我们生成了这些 anchors,并将它们用于对象检测模型的边界框预测,同时还有sride为放缩比例同时计算。


2.6 dist2bbox

下面的代码为位置坐标信息的解码操作!

def dist2bbox(distance, anchor_points, xywh=True, dim=-1):"""Transform distance(ltrb) to box(xywh or xyxy)."""lt, rb = distance.chunk(2, dim)x1y1 = anchor_points - ltx2y2 = anchor_points + rbif xywh:c_xy = (x1y1 + x2y2) / 2wh = x2y2 - x1y1return torch.cat((c_xy, wh), dim)  # xywh bboxreturn torch.cat((x1y1, x2y2), dim)  # xyxy bbox

这段代码的作用是将距离转换为边界框坐标,可以选择转换为中心点坐标和宽高(xywh)或者左上角和右下角坐标(xyxy)。

def dist2bbox(distance, anchor_points, xywh=True, dim=-1):"""Transform distance(ltrb) to box(xywh or xyxy)."""
  • distance:表示从 anchor points 到边界框的左、上、右、下边界的距离(left, top, right, bottom)。
  • anchor_points:anchor points 的坐标。
  • xywh:布尔值,决定输出边界框的格式,默认为 `True,表示输出为中心点坐标和宽高(xywh),否则输出为左上角和右下角坐标(xyxy)。
  • dim:指明在哪个维度上切分距离张量,默认值为 -1。

核心逻辑

1. 切分距离张量

lt, rb = distance.chunk(2, dim)

将 distance 张量分为左右两个部分:

  • lt:left, top 距离
  • rb:right, bottom 距离
  • chunk(2, dim) 表示在指定维度 dim 上将 distance切分为 2 个张量。

2. 计算左上角和右下角坐标

x1y1 = anchor_points - lt
x2y2 = anchor_points + rb
  • x1y1:左上角坐标,由 anchor points 减去 left 和 top 距离得到。
  • x2y2:右下角坐标,由 anchor points 加上 right 和 bottom 距离得到。

 3. 转换为指定格式的边界框

 如果 xywh 为 True,转换为中心点坐标和宽高:

if xywh:c_xy = (x1y1 + x2y2) / 2wh = x2y2 - x1y1return torch.cat((c_xy, wh), dim)  # xywh bbox
  • c_xy:中心点坐标,由左上角和右下角坐标的平均值计算得到。
  • wh:宽高,由右下角坐标减去左上角坐标计算得到。

最终返回中心点坐标和宽高的组合。

如果 xywh为 False,返回左上角和右下角坐标:

return torch.cat((x1y1, x2y2), dim)  # xyxy bbox

最终返回左上角和右下角坐标的组合。

示例

假设 distance为一个形状为 [N, 4] 的张量,其中 N是 anchor points 的数量,4表示 left, top, right, bottom 距离。anchor_points为一个形状为 [N, 2]的张量,表示每个 anchor 的坐标。

例子:转换为 xywh

distance = torch.tensor([[1, 1, 2, 2], [2, 2, 3, 3]])
anchor_points = torch.tensor([[5, 5], [10, 10]])
xywh = Truebbox = dist2bbox(distance, anchor_points, xywh)

步骤:
1. 切分 distance为 lt和 rb:

  • lt:[[1, 1], [2, 2]]
  • rb:[[2, 2], [3, 3]]

2. 计算 x1y1和 x2y2:

  • x1y1:[[4, 4], [8, 8]]
  • x2y2:[[7, 7], [13, 13]]

3. 转换为中心点坐标和宽高:

  • c_xy:[[5.5, 5.5], [10.5, 10.5]]
  • wh:[[3, 3], [5, 5]]

4. 返回的 bbox为 [[5.5, 5.5, 3, 3], [10.5, 10.5, 5, 5]]

例子:转换为 xyxy

xywh = Falsebbox = dist2bbox(distance, anchor_points, xywh)

步骤:
1. 切分 distance为 lt 和rb:

  • lt:[[1, 1], [2, 2]]
  • rb:[[2, 2], [3, 3]]

2. 计算 x1y1和 x2y2:

  • x1y1:[[4, 4], [8, 8]]
  • x2y2:[[7, 7], [13, 13]]

3. 返回的bbox为 [[4, 4, 7, 7], [8, 8, 13, 13]]

总结

dist2bbox`函数将表示 left, top, right, bottom 距离的张量转换为边界框坐标,可以选择转换为中心点坐标和宽高(xywh)或者左上角和右下角坐标(xyxy)。通过这个函数,模型可以从预测的距离值中计算出实际的边界框坐标。

到此本文的内容就全部完事了,主要内容都在三个代码块中给出了解析! 


三、本文总结

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

专栏回顾:YOLOv8改进系列专栏——本专栏持续复习各种顶会内容——科研必备

d2e5d4828bd84bc79d11a9bd3ef13a35.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/867965.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(十) Docker compose 本地部署 apollo

文章目录 1、apollo2、数据库准备3、启动后会用到的几个地址4、docker-compose运行 apollo方式一:使用容器 hostName 作为网络媒介方式二:使用端口映射固定 ip 作为网络媒介 6、客户端 1、apollo https://www.apolloconfig.com/#/zh/deployment/quick-s…

使用策略模式加工厂模式实现数据上传时的不同云平台的处理

在处理不同云存储的 SDK 和接口调用时可以使用策略模式加工厂模式实现了灵活的云存储上传处理 策略模式:可以为每个云存储(如阿里云、腾讯云等)创建一个策略类,实现统一的接口,来封装不同的上传逻辑。工厂模式&#x…

【代码管理的必备工具:Git的基本概念与操作详解】

一、Git 初识 1.提出问题 不知道你工作或学习时,有没有遇到这样的情况:我们在编写各种⽂档时,为了防止⽂档丢失,更改失误,失误后能恢复到原来的版本,不得不复制出⼀个副本,比如: “…

敏感词匹配DFA算法

算法简介与场景介绍 DFA算法,中文全称为确定性有穷自动机。它的基本思想是构建一个有穷自动机,当用户输入文本时,通过自动机的状态转换来快速匹配敏感词。具体特征是,有一个有效状态的集合和一些从一个状态通向另一个状态的边&am…

VuePress 的更多配置

现在,读者应该对 VuePress、主题和插件等有了基本的认识,除了插件,VuePress 自身也有很多有用的配置,这里简单说明下。 ‍ ‍ VuePress 的介绍 在介绍了 VuePress 的基本使用、主题和插件的概念之后,我们再来看看官…

简介空间复杂度

我们承接上一篇博客。我们写了时间复杂度之后,我们就要来介绍一下另一个相关复杂度了。空间复杂度。我觉得大家应该对空间复杂度认识可能比较少一些。我就是这样,我很少看见题目中有明确要求过空间复杂度的。但确实有这个是我们不可忽视的,所…

PXIe-7976【K410T】

起售价 RMB 152,880.00 块RAM(BRAM): 28620 kbit 动态RAM(DRAM): 2 GB FPGA: Kintex-7 410T PXI背板链路: PCI-Express Gen2 x 8 FPGA片: 63550 DSP片: 1540

ChatGPT-4o大语言模型优化、本地私有化部署、从0-1搭建、智能体构建等高级进阶

目录 第一章 ChatGPT-4o使用进阶 第二章 大语言模型原理详解 第三章 大语言模型优化 第四章 开源大语言模型及本地部署 第五章 从0到1搭建第一个大语言模型 第六章 智能体(Agent)构建 第七章 大语言模型发展趋势 第八章 总结与答疑讨论 更多应用…

昇思25天学习打卡营第12天|FCN图像语义分割

文章目录 昇思MindSpore应用实践基于MindSpore的FCN图像语义分割1、FCN 图像分割简介2、构建 FCN 模型3、数据预处理4、模型训练自定义评价指标 Metrics 5、模型推理结果 Reference 昇思MindSpore应用实践 本系列文章主要用于记录昇思25天学习打卡营的学习心得。 基于MindSpo…

LaTeX教程(014)-LaTeX文档结构(14)

LaTeX教程(014)- LaTeX \LaTeX LATE​X文档结构(14) 2.3.3 multitoc - 将目录设置为多栏 multitoc包的使用方法相当简单,只需要调用这个包,并将要设置为多栏(默认是双栏)的目录指定到包选项中即可。如\usepackage[toc]{multitoc},设置的就是…

Springboot使用WebSocket发送消息

1. 创建springboot项目&#xff0c;引入spring-boot-starter-websocket依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency>完整项目依赖 <?xml ver…

Vue 使用 @click 绑定点击事件

https://andi.cn/page/621505.html

Python【打包exe文件两步到位】

Python打包Exe 安装 pyinstaller&#xff08;pip install pyinstaller&#xff09; 执行打包命令&#xff08;pyinstaller demo.py&#xff09; 打完包会生成 dist 文件夹&#xff0c;如下如

基于开源AI数据框架LlamaIndex构建上下文增强型LLA应用

引言 “将你的企业数据转化为可用于实际生产环境的LLM应用程序&#xff0c;”LlamaIndex主页用60号字体这样高亮显示。其副标题是“LlamaIndex是构建LLM应用程序的领先数据框架。”我不太确定它是否是业内领先的数据框架&#xff0c;但我认为它是一个与LangChain和Semantic Ker…

快行线冷链——创新仓配、共赢未来 主题沙龙在京成功举办

2024年7月7日下午&#xff0c;由京营建联盟、北京市大兴区餐饮行业协会、北京快行线冷链物流联合主办的“创新仓配&#xff0c;共赢未来——相信拥抱的力量”主题沙龙活动在北京篝野城市露营空间隆重举行。本次活动定向邀请了餐饮老板、餐饮企业供应链负责人及冻品经销商和相关…

电子电气架构 --- 智能座舱万物互联

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明自己,无利益不试图说服别人,是精神上的节…

C语言指针函数指针

跟着这篇文章重新理解了一下&#xff1a;彻底攻克C语言指针 有一个例子感觉可以拿出来看看&#xff1a; char *(*c[10])(int **p); * 这段声明定义了一个长度为10的数组c&#xff0c;数组中的每个元素都是指向函数的指针。每个函数接受一个类型为int **&#xff08;指向指向…

拆分Transformer注意力,韩国团队让大模型解码提速20倍|大模型AI应用开始小规模稳步爆发|周伯文:大模型也有幻觉,全球AI创新指数公布

拆分Transformer注意力&#xff0c;韩国团队让大模型解码提速20倍AI正在颠覆AI上市不到两年&#xff0c;蜗牛游戏可能要退市了&#xff1f;世界人工智能大会结束了&#xff0c;百花齐放&#xff0c;但也群魔乱舞“串联OLED”被苹果带火了&#xff0c;比OLED强在哪里&#xff1f…

智能物联网鱼缸

硬件部分及接线图 工具 继电器、开发板、物联网os、云平台 微信小程序 结构&#xff1a;images、pages两个为主体。 标题头部分 <view class"container"> <view class"head_box"> <image src"/images/面性鱼缸.png"><…

【C++】 解决 C++ 语言报错:Invalid Use of Incomplete Type

文章目录 引言 在 C 编程中&#xff0c;“Invalid Use of Incomplete Type” 是一种常见错误。此错误通常在程序试图使用未完全定义的类或结构时发生。这种错误不仅会导致编译失败&#xff0c;还可能导致程序行为不可预测。本文将详细探讨无效使用不完整类型的成因、检测方法及…