head.py
ultralytics\nn\modules\head.py
目录
head.py
1.所需的库和模块
2.class Detect(nn.Module):
3.class Segment(Detect):
4.class OBB(Detect):
5.class Pose(Detect):
6.class Classify(nn.Module):
7.class WorldDetect(Detect):
8.class RTDETRDecoder(nn.Module):
9.class v10Detect(Detect):
1.所需的库和模块
# Ultralytics YOLO ????, AGPL-3.0 license
"""Model head modules."""
import copy
import math
import torch
import torch.nn as nn
from torch.nn.init import constant_, xavier_uniform_
from ultralytics.utils.tal import TORCH_1_10, dist2bbox, dist2rbox, make_anchors
from .block import DFL, BNContrastiveHead, ContrastiveHead, Proto
from .conv import Conv
from .transformer import MLP, DeformableTransformerDecoder, DeformableTransformerDecoderLayer
from .utils import bias_init_with_prob, linear_init
__all__ = "Detect", "Segment", "Pose", "Classify", "OBB", "RTDETRDecoder", "v10Detect"
2.class Detect(nn.Module):
# 这段代码定义了一个名为 Detect 的类,它代表YOLOv8目标检测模型的检测头。
# 定义了一个名为 Detect 的新类,它继承自PyTorch的 nn.Module 类,用于构建神经网络模型的检测头部分。
class Detect(nn.Module):
# YOLOv8 检测模型的检测头。
"""YOLOv8 Detect head for detection models."""
# 一个类属性,表示是否使用动态网格重建。在目标检测中,动态网格可以帮助模型更好地适应不同尺寸的目标。
dynamic = False # force grid reconstruction
# 一个类属性,表示是否处于导出模式。在导出模式下,模型可能会禁用一些特定功能,以确保模型可以被导出到其他平台或格式。
export = False # export mode
# 一个类属性,表示是否是端到端模式。端到端模式意味着模型从输入到输出是连续的,没有中间的干预或调整。
end2end = False # end2end
# 一个类属性,表示模型在检测时能够处理的最大检测目标数量。
max_det = 300 # max_det
# 一个类属性,用于存储模型输入的形状。在实际使用中,这个属性可能会被设置为具体的值,以适应不同的输入尺寸。
shape = None
# torch.empty(*size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
# torch.empty 是 PyTorch 库中的一个函数,它用于创建一个指定形状(shape)和数据类型的张量(tensor),但不初始化其值。这意味着张量中的值是任意的,通常是内存中已有的值,因此不应该依赖于 torch.empty 创建的张量的初始值。
# 参数解释 :
# size :一个整数的元组,表示张量的形状。
# out :一个可选参数,用于指定输出张量的张量。如果提供,该函数将尝试将结果写入此张量。
# dtype :一个可选参数,用于指定张量的数据类型。如果不指定,将使用默认的数据类型(通常是 torch.float32 )。
# layout :一个可选参数,用于指定张量的内存布局。默认是 torch.strided ,表示使用标准步幅布局。
# device :一个可选参数,用于指定张量应该被分配在哪个设备上(例如,CPU或GPU)。
# requires_grad :一个布尔值,用于指定是否需要计算张量的梯度。默认为 False 。
# 返回值 :
# 创建的张量。
# 请注意,由于 torch.empty 不初始化张量的值,所以打印出来的张量 x 将包含随机的内存值。这个函数通常用于性能优化,因为它比用零或其他值填充张量的函数(如 torch.zeros 或 torch.ones )更快,特别是在初始化一个大型张量时,因为不需要实际写入值。
# 一个类属性,用于存储锚点(anchors)。锚点是目标检测中用于预测目标位置的参考框。这里初始化为一个空的PyTorch张量。
anchors = torch.empty(0) # init
# 一个类属性,用于存储模型在不同特征层的步长(strides)。步长影响特征图的尺寸和目标检测的精度。这里初始化为一个空的PyTorch张量。
strides = torch.empty(0) # init
# 这个 Detect 类是一个框架,它定义了YOLOv8检测头的一些基本属性和行为。
# 这段代码定义了一个YOLOv8检测层的构造函数,它初始化了一个用于目标检测的神经网络模块。
def __init__(self, nc=80, ch=()):
# 使用指定数量的类和通道初始化 YOLOv8 检测层。
"""Initializes the YOLOv8 detection layer with specified number of classes and channels."""
# 是类的构造函数,接收以下参数 :
# 1.nc :类别数量,默认为80。
# 2.ch :一个元组,表示每个检测层的输入通道数。
super().__init__()
# 存储类别数量。
self.nc = nc # number of classes
# 计算检测层的数量,即 ch 元组的长度。
self.nl = len(ch) # number of detection layers
# 定义DFL(Dynamic Feature Loader 动态特征加载器)通道的最大数量。
self.reg_max = 16 # DFL channels (ch[0] // 16 to scale 4/8/12/16/20 for n/s/m/l/x)
# 计算每个锚点的输出数量,包括类别概率和边界框坐标。
self.no = nc + self.reg_max * 4 # number of outputs per anchor
# 初始化一个张量,用于存储每个检测层的步长。
self.stride = torch.zeros(self.nl) # strides computed during build
# 计算两个中间通道数 c2 和 c3 ,它们将用于后续的卷积层。
# max((16, ch[0] // 4, self.reg_max * 4)) :这个表达式计算 c2 的值,即三个数中的最大值 :
# 16 :一个固定的最小通道数。
# ch[0] // 4 : ch 元组中的第一个元素除以4的结果。这通常用于减少通道数, ch[0] 代表第一个检测层的输入通道数。
# self.reg_max * 4 : reg_max 属性乘以4的结果。 reg_max 是DFL通道数的上限。
# max 函数将返回这三个值中的最大值,确保 c2 不会低于一个合理的数值。
# max(ch[0], min(self.nc, 100)) :这个表达式计算 c3 的值,即两个数中的最大值 :
# ch[0] :与上面相同,代表第一个检测层的输入通道数。
# min(self.nc, 100) : self.nc (类别数量)和100中的最小值。这确保了即使类别数量很多, c3 的值也不会超过100。
# max 函数将返回这两个值中的最大值,确保 c3 在类别数量和100之间取一个较大的值。
# 综上所述,这行代码的目的是在不同的数值之间取最大值,以确定两个关键的通道数 c2 和 c3 ,这些通道数将用于后续的卷积层设计。这样做可以确保网络在不同尺度的特征图上都能有效地进行特征提取和目标检测。
c2, c3 = max((16, ch[0] // 4, self.reg_max * 4)), max(ch[0], min(self.nc, 100)) # channels
# 创建一个模块列表 cv2 ,包含多个序列模块。每个序列模块包含两个3x3的卷积层,后跟一个1x1的卷积层,用于预测边界框坐标。
self.cv2 = nn.ModuleList(
nn.Sequential(Conv(x, c2, 3), Conv(c2, c2, 3), nn.Conv2d(c2, 4 * self.reg_max, 1)) for x in ch
)
# 创建另一个模块列表 cv3 ,包含多个序列模块。每个序列模块包含两个3x3的卷积层,后跟一个1x1的卷积层,用于预测类别概率。
self.cv3 = nn.ModuleList(nn.Sequential(Conv(x, c3, 3), Conv(c3, c3, 3), nn.Conv2d(c3, self.nc, 1)) for x in ch)
# 如果 self.reg_max 大于1,则创建一个DFL模块,否则使用一个恒等模块。
# class DFL(nn.Module):
# -> 分布焦点损失 (DFL) 的积分模块。实现了一个用于处理对象检测任务中类别预测的模块,通过将类别预测和质量估计合并到一个向量中,使用一个向量来表示边界框位置的任意分布,并从分布向量中提取区分性特征描述符以实现更可靠的质量估计。
# -> def __init__(self, c1=16):
# -> def forward(self, x):
# -> return self.conv(x.view(b, 4, self.c1, a).transpose(2, 1).softmax(1)).view(b, 4, a)
self.dfl = DFL(self.reg_max) if self.reg_max > 1 else nn.Identity()
# 如果模型处于端到端模式,则执行以下操作。
if self.end2end:
# 深复制 cv2 模块列表,用于端到端训练。
self.one2one_cv2 = copy.deepcopy(self.cv2)
# 深复制 cv3 模块列表,用于端到端训练。
self.one2one_cv3 = copy.deepcopy(self.cv3)
# 这个构造函数初始化了YOLOv8检测层的主要组件,包括用于预测边界框坐标和类别概率的卷积层,以及可选的DFL模块。这些组件将被用于后续的目标检测任务。 end2end 属性控制是否创建额外的模块用于端到端训练。
# 这段代码定义了 Detect 类的 forward 方法,它是PyTorch模型中用于执行前向传播的函数。
# 定义了 Detect 类的前向传播方法,接收一个参数。
# x :是一个包含多个输入张量的列表,每个张量对应于不同尺度的特征图。
def forward(self, x):
# 连接并返回预测的边界框和类概率。
"""Concatenates and returns predicted bounding boxes and class probabilities."""
# 检查是否处于端到端模式。
if self.end2end:
# 如果是端到端模式,则调用 forward_end2end 方法,并返回其结果。
return self.forward_end2end(x)
# 循环遍历每个检测层。
for i in range(self.nl):
# 对于每个检测层,使用 cv2 和 cv3 模块处理输入特征图 x[i] ,然后将这两个结果在通道维度(维度1)上进行拼接。 cv2 模块预测边界框坐标,而 cv3 模块预测类别概率。
x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1)
# 检查是否处于训练模式。
if self.training: # Training path
# 如果是训练模式,则直接返回拼接后的结果列表 x 。
return x
# 如果是推理模式,则调用 _inference 方法对拼接后的结果进行进一步处理,得到最终的预测结果 y 。
y = self._inference(x)
# 根据是否处于导出模式决定返回值。
# 如果是导出模式( self.export 为 True ),则只返回预测结果 y 。
# 如果不是导出模式,则返回一个元组 (y, x) ,包含预测结果 y 和原始输入特征图列表 x 。
return y if self.export else (y, x)
# 这个方法的核心功能是将不同尺度的特征图通过检测层处理,得到预测的边界框和类别概率,然后根据训练或推理模式返回相应的结果。在训练模式下,它直接返回每个检测层的输出;在推理模式下,它还会进行额外的推理步骤来生成最终的检测结果。
# 这段代码定义了 Detect 类的 forward_end2end 方法,它是用于在端到端模式下执行前向传播的函数。
# 定义了 Detect 类的 forward_end2end 方法,接收一个参数 x ,它是一个包含多个输入张量的列表,每个张量对应于不同尺度的特征图。
def forward_end2end(self, x):
# 执行 v10Detect 模块的前向传递。
"""
Performs forward pass of the v10Detect module.
Args:
x (tensor): Input tensor.
Returns:
(dict, tensor): If not in training mode, returns a dictionary containing the outputs of both one2many and one2one detections.
If in training mode, returns a dictionary containing the outputs of one2many and one2one detections separately.
"""
# tensor.detach()
# 在PyTorch中, detach() 方法用于将一个张量(Tensor)从当前计算图中分离出来,使其不再参与梯度计算。这对于评估模型、保存中间结果或者进行损失函数的某些操作时非常有用,尤其是当你不希望某些张量影响梯度回传时。
# 参数 :无参数。
# 返回值 :
# 返回一个新的张量,与原张量共享数据但不会追踪梯度。
# 方法描述 :
# 当你调用 detach() 方法时,PyTorch会返回一个新的张量,这个张量与原始张量共享内存数据,但是它不会在反向传播中计算梯度。这通常用于以下情况 :
# 1. 评估模型:在评估阶段,你不希望计算梯度,可以使用 detach() 来减少内存消耗。
# 2. 保存中间结果:在训练过程中,你可能需要保存一些中间结果,但不希望这些结果影响梯度计算。
# 3. 损失计算:在自定义损失函数中,有时需要对某些张量进行操作,但不希望这些操作影响梯度回传。
# 创建一个新的列表 x_detach ,其中包含从原始输入列表 x 中分离出来的张量。分离操作( detach )意味着这些张量将 不会在反向传播中被计算梯度 ,这通常用于评估或推理阶段,以避免影响梯度计算。
x_detach = [xi.detach() for xi in x]
# 创建一个列表 one2one ,其中包含使用 one2one_cv2 和 one2one_cv3 模块处理 x_detach 张量的结果。这些模块是 cv2 和 cv3 的深复制,用于端到端训练。这里,每个检测层的输出在通道维度(维度1)上进行拼接。
one2one = [
torch.cat((self.one2one_cv2[i](x_detach[i]), self.one2one_cv3[i](x_detach[i])), 1) for i in range(self.nl)
]
# 循环遍历每个检测层。
for i in range(self.nl):
# 对于每个检测层,使用 cv2 和 cv3 模块处理输入特征图 x[i] ,然后将这两个结果在通道维度(维度1)上进行拼接。
x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1)
# 检查是否处于训练模式。
if self.training: # Training path
# 如果是训练模式,则返回一个字典,包含 one2many 和 one2one 两个键,分别对应于 x 和 one2one 的结果。
return {"one2many": x, "one2one": one2one}
# 如果是推理模式,则调用 _inference 方法对 one2one 的结果进行进一步处理,得到最终的预测结果 y 。
y = self._inference(one2one)
# 对预测结果 y 进行后处理,包括置换维度和应用最大检测数量 self.max_det 和类别数量 self.nc 。
y = self.postprocess(y.permute(0, 2, 1), self.max_det, self.nc)
# 根据是否处于导出模式决定返回值。
# 如果是导出模式( self.export 为 True ),则只返回预测结果 y 。
# 如果不是导出模式,则返回一个元组,包含预测结果 y 和一个字典,字典中包含 one2many 和 one2one 的结果。
return y if self.export else (y, {"one2many": x, "one2one": one2one})
# 这个方法的核心功能是在端到端模式下处理输入特征图,得到预测的边界框和类别概率,并根据训练或推理模式返回相应的结果。在训练模式下,它返回两个检测路径的结果;在推理模式下,它还会进行额外的推理和后处理步骤来生成最终的检测结果。
# 这段代码定义了 Detect 类的 _inference 方法,它是用于从多尺度特征图中解码预测的边界框和类别概率的函数。
# 定义了 Detect 类的 _inference 方法,接收一个参数。
# x :它是一个包含多个特征图的张量列表。
def _inference(self, x):
# 根据多级特征图解码预测的边界框和类概率。
"""Decode predicted bounding boxes and class probabilities based on multiple-level feature maps."""
# Inference path
# 获取第一个特征图的形状,这里假设所有特征图的形状相同。
shape = x[0].shape # BCHW
# 将所有特征图在第三个维度(宽度)上拼接起来,形成一个大的特征图。
x_cat = torch.cat([xi.view(shape[0], self.no, -1) for xi in x], 2)
# 检查是否需要动态重建网格或当前特征图形状与之前的形状不同。
if self.dynamic or self.shape != shape:
# 调用 make_anchors 函数生成锚点和步长,并将它们转置以匹配特征图的形状。
# def make_anchors(feats, strides, grid_cell_offset=0.5):
# -> 它用于从特征图生成锚点(anchors)。锚点是在目标检测中用于预测目标位置的参考框。将所有特征图的 锚点 和 步长张量 分别在第一个维度上拼接起来,并返回结果。
# -> return torch.cat(anchor_points), torch.cat(stride_tensor)
self.anchors, self.strides = (x.transpose(0, 1) for x in make_anchors(x, self.stride, 0.5))
# 更新特征图的形状。
self.shape = shape
# 检查是否需要导出模型,并根据导出格式决定如何处理边界框和类别概率的分割。
if self.export and self.format in {"saved_model", "pb", "tflite", "edgetpu", "tfjs"}: # avoid TF FlexSplitV ops
# 提取边界框预测。
box = x_cat[:, : self.reg_max * 4]
# 提取类别概率预测。
cls = x_cat[:, self.reg_max * 4 :]
# 如果不是导出模式或导出格式不需要特殊处理,则直接分割边界框和类别概率。
else:
# 在通道维度上分割特征图,分别得到边界框和类别概率。
box, cls = x_cat.split((self.reg_max * 4, self.nc), 1)
# 检查是否需要为特定的导出格式预计算归一化因子以增加数值稳定性。
if self.export and self.format in {"tflite", "edgetpu"}:
# Precompute normalization factor to increase numerical stability
# See https://github.com/ultralytics/ultralytics/issues/7371
grid_h = shape[2]
grid_w = shape[3]
# 计算网格大小。
grid_size = torch.tensor([grid_w, grid_h, grid_w, grid_h], device=box.device).reshape(1, 4, 1)
# 计算归一化因子。
norm = self.strides / (self.stride[0] * grid_size)
# 使用归一化的锚点和边界框预测解码边界框。
# def decode_bboxes(self, bboxes, anchors):
# -> 它用于将模型输出的边界框预测(通常是相对于锚点的偏移量)解码成实际的边界框坐标。作用是将模型的输出转换为可以直接用于非极大值抑制(NMS)和后续处理的边界框坐标。
# -> return dist2bbox(bboxes, anchors, xywh=not self.end2end, dim=1)
dbox = self.decode_bboxes(self.dfl(box) * norm, self.anchors.unsqueeze(0) * norm[:, :2])
# 如果不需要特殊处理,则直接解码边界框。
else:
# 使用DFL模块处理边界框预测,并使用锚点和步长解码边界框。
dbox = self.decode_bboxes(self.dfl(box), self.anchors.unsqueeze(0)) * self.strides
# 将解码后的边界框和sigmoid激活后的类别概率在通道维度上拼接,并返回结果。
return torch.cat((dbox, cls.sigmoid()), 1)
# 这个方法的核心功能是将特征图中的边界框和类别概率预测解码成最终的检测结果,包括边界框的位置和类别。这个过程涉及到特征图的拼接、锚点的生成、边界框的解码以及类别概率的激活。
# 这段代码定义了 Detect 类中的 bias_init 方法,它用于初始化检测头(Detect)中的偏置(biases)。这个初始化过程特别重要,因为它可以帮助模型在训练初期更快地收敛。
# 定义了 Detect 类的 bias_init 方法。
def bias_init(self):
# 初始化Detect()偏差,警告:需要步幅可用性。
"""Initialize Detect() biases, WARNING: requires stride availability."""
# 将 self 赋值给局部变量 m ,以便在方法内部使用。
m = self # self.model[-1] # Detect() module
# cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1
# ncf = math.log(0.6 / (m.nc - 0.999999)) if cf is None else torch.log(cf / cf.sum()) # nominal class frequency
# 循环遍历 cv2 和 cv3 模块列表以及步长列表 m.stride 。 a :表示 cv2 模块列表中的每个模块,用于边界框(box)的预测。 b :表示 cv3 模块列表中的每个模块,用于类别(class)的预测。 s :表示对应的步长。
for a, b, s in zip(m.cv2, m.cv3, m.stride): # from
# 将 cv2 模块中最后一个卷积层的偏置初始化为1.0。这通常是为了平衡边界框的预测,使其在开始时不偏向于任何特定的值。
a[-1].bias.data[:] = 1.0 # box
# 将 cv3 模块中最后一个卷积层的偏置初始化为一个计算值。这个值是基于类别数量 m.nc 、 输入图像的尺寸(这里假设为640) ,以及 步长 s 计算得出的。这个初始化策略有助于平衡类别的预测,特别是在类别不平衡的情况下。
b[-1].bias.data[: m.nc] = math.log(5 / m.nc / (640 / s) ** 2) # cls (.01 objects, 80 classes, 640 img)
# 检查是否处于端到端模式。
if self.end2end:
# 如果是端到端模式,则对 one2one_cv2 和 one2one_cv3 模块列表也进行相同的偏置初始化操作。
for a, b, s in zip(m.one2one_cv2, m.one2one_cv3, m.stride): # from
a[-1].bias.data[:] = 1.0 # box
b[-1].bias.data[: m.nc] = math.log(5 / m.nc / (640 / s) ** 2) # cls (.01 objects, 80 classes, 640 img)
# 这个方法的核心功能是在模型的检测头中初始化偏置,以便在训练开始时提供一个合理的起点。这种初始化策略可以帮助模型更快地收敛,并提高最终的检测性能。注意,这里的初始化策略假设了一个特定的输入图像尺寸(640),这可能需要根据实际的输入尺寸进行调整。
# 这段代码定义了 Detect 类中的 decode_bboxes 方法,它用于将模型输出的边界框预测(通常是相对于锚点的偏移量)解码成实际的边界框坐标。
# 定义了 Detect 类的 decode_bboxes 方法,接收两个参数 :
# bboxes :模型输出的边界框预测,通常是相对于锚点的偏移量。
# anchors :用于预测边界框的锚点坐标。
def decode_bboxes(self, bboxes, anchors):
# 解码边界框。
"""Decode bounding boxes."""
# 调用 dist2bbox 函数将偏移量解码成边界框坐标。
# bboxes :传入的边界框预测。
# anchors :传入的锚点坐标。
# xywh=not self.end2end :一个布尔值,指示输出的边界框格式。
# 如果 self.end2end 为 False ,则 xywh 为 True ,表示边界框以 (x, y, w, h) 的格式输出,其中 x 和 y 是边界框中心的坐标, w 和 h 是边界框的宽度和高度。
# 如果 self.end2end 为 True ,则 xywh 为 False ,表示边界框以 (x1, y1, x2, y2) 的格式输出,其中 x1 和 y1 是边界框左上角的坐标, x2 和 y2 是边界框右下角的坐标。
# dim=1 :指定在哪个维度上操作,这里选择在维度1上,即每个锚点对应的偏移量。
# def dist2bbox(distance, anchor_points, xywh=True, dim=-1):
# -> 它用于将距离(通常是左上角和右下角的偏移量)转换为边界框的坐标。这个函数可以输出两种格式的边界框:中心点加宽高(xywh)格式或者左上角和右下角(xyxy)格式。
# -> return torch.cat((c_xy, wh), dim) # xywh bbox / return torch.cat((x1y1, x2y2), dim) # xyxy bbox
return dist2bbox(bboxes, anchors, xywh=not self.end2end, dim=1)
# dist2bbox 函数是一个通用的函数,用于将距离(或偏移量)转换为边界框坐标。这个转换通常涉及到一些几何计算,例如将中心点坐标和宽高转换为角点坐标,或者反之。
# 在这个上下文中, decode_bboxes 方法的作用是将模型的输出转换为可以直接用于非极大值抑制(NMS)和后续处理的边界框坐标。
# 这段代码定义了一个名为 postprocess 的静态方法,它用于对YOLO模型的原始预测进行后处理。
# 装饰器,表示这个方法是一个静态方法,不依赖于类的实例。
@staticmethod
# 定义了 postprocess 方法,接收以下参数 :
# 1.preds :原始预测张量,形状为 (batch_size, num_anchors, 4 + nc) ,其中最后一个维度的格式为 [x, y, w, h, class_probs] 。
# 2.max_det :每张图片的最大检测数量。
# 3.nc :类别数量,默认为80。
def postprocess(preds: torch.Tensor, max_det: int, nc: int = 80):
# 对 YOLO 模型预测进行后处理。
"""
Post-processes YOLO model predictions.
Args:
preds (torch.Tensor): Raw predictions with shape (batch_size, num_anchors, 4 + nc) with last dimension
format [x, y, w, h, class_probs].
max_det (int): Maximum detections per image.
nc (int, optional): Number of classes. Default: 80.
Returns:
(torch.Tensor): Processed predictions with shape (batch_size, min(max_det, num_anchors), 6) and last
dimension format [x, y, w, h, max_class_prob, class_index].
"""
# 获取预测张量的形状,其中 batch_size 是批量大小, anchors 是锚点(或称为先验)的数量。
batch_size, anchors, _ = preds.shape # i.e. shape(16,8400,84)
# 将预测张量在最后一个维度上分割成两个部分, boxes 包含边界框坐标(x, y, w, h), scores 包含类别概率。
boxes, scores = preds.split([4, nc], dim=-1)
# numpy.amax(arr, axis=None, out=None, keepdims=False, initial=None, where=None)
# numpy.amax 函数计算数组元素的最大值。它接受一个数组对象作为参数,并返回数组中包含的最大值。如果指定了 axis 参数,它将沿着该轴计算最大值。如果不指定 axis ,则返回整个数组的最大值。
# 参数 :
# arr :要评估的输入数组。
# axis :沿此轴计算最大值。如果为 None ,则计算整个数组的最大值。
# out :用于存储结果的输出数组。
# keepdims :布尔值,表示在输出数组中是否保持输入数组的维度。如果为 True ,则缩减的轴将保留为尺寸1。
# initial :用于计算的初始值。如果提供,它将覆盖数组中的最大值。
# where :一个布尔数组,指示输入数组元素的有效性。
# 处理NaN值 :
# 当输入数组包含NaN(非数字)值时, amax 函数默认返回NaN。但是,用户可以使用 where 参数指定如何处理NaN值。 例如 : numpy.amax(arr, where=~np.isnan(arr)) 这将计算数组的最大值,忽略NaN值。
# amax 函数在数据分析和科学计算中非常有用,尤其是在需要识别极端值的任务中,如统计分析或图像处理任务。通过掌握这个函数在不同轴设置中的使用,你可以充分利用NumPy在数据分析、机器学习等领域的应用。
# 在类别概率中找到每个锚点的最大值,并获取这些最大值的索引。 topk 方法返回值和索引,这里只取索引,并增加一个维度。
# 这行代码用于从模型的预测分数中选择最可能的检测结果。
# scores.amax(dim=-1) :
# scores 是一个张量,包含模型预测的类别概率。 amax 函数计算 scores 张量在最后一个维度(即类别概率维度)上的最大值。 结果是一个张量,其中每个元素是对应锚点的最大类别概率。
# .topk(min(max_det, anchors)) :
# topk 函数从 amax 的结果中选择最大的 k 个值。 k 是 max_det (每张图片的最大检测数量)和 anchors (锚点数量)之间的最小值。 topk 函数返回两个张量:值和索引。这里我们只对索引感兴趣,即 topk 的第二个输出。
# [1] :
# 这选择了 topk 函数返回的第二个张量,即包含最大值索引的张量。
# .unsqueeze(-1) :
# unsqueeze 函数在索引张量的最后一个维度上增加一个维度。 这样做是为了确保索引张量的形状与后续操作所需的形状相匹配,例如使用 torch.gather 收集边界框。
# 综合起来,这行代码的作用是从每个锚点的最大类别概率中选择前 min(max_det, anchors) 个最大值的索引,并将这些索引调整为适当的形状,以便在后续步骤中使用。这些索引将用于从边界框预测和类别概率张量中选择相应的元素。
index = scores.amax(dim=-1).topk(min(max_det, anchors))[1].unsqueeze(-1)
# torch.gather(input, dim, index, *, out=None) → Tensor
# torch.gather 函数是 PyTorch 中的一个函数,它根据索引从输入张量中收集元素。这个函数可以看作是从输入张量中“挑选”特定元素的高级索引操作。
# 参数 :
# input (Tensor) :要从中收集元素的输入张量。
# dim (int) :沿着哪个维度进行收集。
# index (Tensor) :包含要收集的元素索引的张量。它的数据类型必须为长整型( torch.long ),并且形状与 input 相同,除了在 dim 维度上,它必须与 input 在 dim 维度上的大小相同。
# out (Tensor, 可选) :输出张量,用于存储结果。
# 描述 :
# torch.gather 函数沿着指定的维度 dim ,根据 index 张量提供的索引,从 input 张量中收集元素。结果张量的形状将与 index 张量相同。
# torch.gather 函数在处理分类问题、索引选择和数据重排等任务时非常有用。它允许你根据索引动态地从张量中选择元素,这在构建复杂的神经网络模型时尤其有用。
# 根据索引 index ,从 boxes 中选取对应的边界框坐标。
# 这行代码使用 gather 函数根据索引 index 从 boxes 张量中收集边界框。
# boxes :
# boxes 是一个张量,包含模型预测的边界框坐标。它的形状通常是 (batch_size, num_anchors, 4) ,其中最后一个维度包含每个锚点的 (x, y, w, h) 坐标。
# index :
# index 是一个张量,包含从 scores 张量中选择的最大类别概率的索引。它的形状是 (batch_size, num_max_detections) ,其中 num_max_detections 是 min(max_det, anchors) 。
# index.repeat(1, 1, 4) :
# repeat 函数将 index 张量沿着最后一个维度重复 4 次,以匹配 boxes 张量的形状。这使得 index 可以用于从每个边界框的 4 个坐标中选择元素。 结果 index.repeat(1, 1, 4) 的形状是 (batch_size, num_max_detections, 4) 。
# boxes.gather(dim=1, index=index.repeat(1, 1, 4)) :
# gather 函数沿着第二个维度( dim=1 )根据 index.repeat(1, 1, 4) 提供的索引从 boxes 张量中收集边界框。 结果 boxes 将包含选定的最大类别概率对应的边界框坐标。
# 综合起来,这行代码的作用是从 boxes 张量中收集与每个锚点的最大类别概率相对应的边界框坐标。结果张量 boxes 的形状将是 (batch_size, num_max_detections, 4) ,其中每个元素包含一个边界框的 (x, y, w, h) 坐标。
boxes = boxes.gather(dim=1, index=index.repeat(1, 1, 4))
# 根据索引 index ,从 scores 中选取对应的类别概率。
scores = scores.gather(dim=1, index=index.repeat(1, 1, nc))
# 在所有选取的类别概率中,再次使用 topk 方法找到前 max_det 个最大值及其索引。
scores, index = scores.flatten(1).topk(min(max_det, anchors))
# 创建一个包含批量索引的张量。
i = torch.arange(batch_size)[..., None] # batch indices
# 将选取的 边界框坐标 、 最大类别概率 和 类别索引 拼接起来,并返回。 index // nc 用于获取类别索引, index % nc 用于获取类别ID。 scores[..., None] 和 (index % nc)[..., None].float() 分别增加一个维度以便拼接。
# 这行代码使用 torch.cat 函数将 边界框 、 分数 和 类别索引 沿最后一个维度拼接起来。
# boxes[i, index // nc] :
# i 是一个张量,包含批量索引。 index // nc 计算每个最大类别概率索引对应的类别索引。 boxes[i, index // nc] 从 boxes 张量中选择每个类别索引对应的边界框。
# scores[..., None] :
# scores 张量包含每个选定锚点的最大类别概率。 None 在 PyTorch 中用于增加一个维度,这里在最后一个维度上增加一个维度。
# (index % nc)[..., None].float() :
# index % nc 计算每个最大类别概率索引在类别维度上的余数,得到类别索引。 float() 将类别索引转换为浮点数。 None 在最后一个维度上增加一个维度。
# torch.cat([boxes[i, index // nc], scores[..., None], (index % nc)[..., None].float()], dim=-1) :
# torch.cat 函数沿最后一个维度( dim=-1 )拼接三个张量 : 边界框 、 分数 和 类别索引 。
# 结果张量的形状将是 (batch_size, num_max_detections, 6) ,其中最后一个维度包含 (x, y, w, h, max_class_prob, class_index) 。
# 综合起来,这行代码的作用是将 边界框 、 最大类别概率 和 类别索引 合并成一个单一的张量,准备进行最终的输出。这个合并后的张量可以直接用于评估模型的性能或进一步的处理,如绘制边界框或计算精确度和召回率。
return torch.cat([boxes[i, index // nc], scores[..., None], (index % nc)[..., None].float()], dim=-1)
# 这个方法的核心功能是对YOLO模型的原始预测进行处理,包括选取每个锚点的最大类别概率、应用非极大值抑制(NMS)的逻辑(虽然这里没有显式实现NMS,但是 topk 方法可以看作是NMS的一个简化版本),并重新格式化预测结果,以便可以直接使用或进一步处理。
# 最终返回的张量形状为 (batch_size, min(max_det, num_anchors), 6) ,其中最后一个维度的格式为 [x, y, w, h, max_class_prob, class_index] 。
3.class Segment(Detect):
# 这段代码定义了一个名为 Segment 的类,它是 Detect 类的子类,用于在 YOLOv8 模型中处理分割任务。
# 定义了 Segment 类,它继承自 Detect 类。
class Segment(Detect):
# YOLOv8 分割模型的分割头。
"""YOLOv8 Segment head for segmentation models."""
# 是 Segment 类的构造函数,接收以下参数 :
# 1.nc :类别数量,默认为80。
# 2.nm :掩码数量,默认为32。
# 3.npr :原型数量,默认为256。
# 4.ch :一个元组,表示每个检测层的输入通道数。
def __init__(self, nc=80, nm=32, npr=256, ch=()):
# 初始化 YOLO 模型属性,例如掩码数量、原型数量和卷积层数量。
"""Initialize the YOLO model attributes such as the number of masks, prototypes, and the convolution layers."""
# 调用父类 Detect 的构造函数。
super().__init__(nc, ch)
# 存储掩码数量。
self.nm = nm # number of masks
# 存储原型数量。
self.npr = npr # number of protos
# 创建一个 Proto 实例,用于生成掩码原型。
self.proto = Proto(ch[0], self.npr, self.nm) # protos
# 计算中间通道数 c4 ,取 ch[0] 的四分之一和 nm 的最大值。
c4 = max(ch[0] // 4, self.nm)
# 创建一个模块列表 cv4 ,包含多个序列模块。每个序列模块包含两个3x3的卷积层,后跟一个1x1的卷积层,用于预测掩码系数。
self.cv4 = nn.ModuleList(nn.Sequential(Conv(x, c4, 3), Conv(c4, c4, 3), nn.Conv2d(c4, self.nm, 1)) for x in ch)
# 定义了 Segment 类的前向传播方法。
def forward(self, x):
# 如果训练则返回模型输出和掩码系数,否则返回输出和掩码系数。
"""Return model outputs and mask coefficients if training, otherwise return outputs and mask coefficients."""
# 使用 Proto 实例生成掩码原型。
p = self.proto(x[0]) # mask protos
# 获取批量大小。
bs = p.shape[0] # batch size
# 将所有特征图的掩码系数在第三个维度上拼接起来。
mc = torch.cat([self.cv4[i](x[i]).view(bs, self.nm, -1) for i in range(self.nl)], 2) # mask coefficients
# 调用父类 Detect 的 forward 方法,获取模型输出。
x = Detect.forward(self, x)
# 检查是否处于训练模式。
if self.training:
# 如果是训练模式,则返回 模型输出 x 、 掩码系数 mc 和 掩码原型 p 。
return x, mc, p
# 根据是否处于导出模式决定返回值。
# 如果是导出模式,则返回 模型输出 和 掩码系数 的拼接结果,以及 掩码原型 p 。
# 如果不是导出模式,则返回一个元组,包含 模型输出的第一个元素 和 掩码系数 的拼接结果,以及 模型输出的其余元素 、 掩码系数 和 掩码原型 。
return (torch.cat([x, mc], 1), p) if self.export else (torch.cat([x[0], mc], 1), (x[1], mc, p))
# 这个方法的核心功能是在目标检测的基础上添加了掩码分割的能力,通过 Proto 实例生成掩码原型,并使用 cv4 模块列表预测掩码系数。在训练模式下,它返回模型输出和掩码相关信息;在非训练模式下,它根据是否导出模式返回不同的结果。
4.class OBB(Detect):
# 这段代码定义了一个名为 OBB 的类,它是 Detect 类的子类,用于在 YOLOv8 模型中处理旋转边界框(Oriented Bounding Boxes, OBB)检测。
# 定义了 OBB 类,它继承自 Detect 类。
class OBB(Detect):
# YOLOv8 OBB 检测头,用于带旋转模型的检测。
"""YOLOv8 OBB detection head for detection with rotation models."""
# 是 OBB 类的构造函数,接收以下参数 :
# 1.nc :类别数量,默认为80。
# 2.ne :额外参数的数量,用于旋转边界框的角度,默认为1。
# 3.ch :一个元组,表示每个检测层的输入通道数。
def __init__(self, nc=80, ne=1, ch=()):
# 使用类别数“nc”和层通道数“ch”初始化 OBB。
"""Initialize OBB with number of classes `nc` and layer channels `ch`."""
# 调用父类 Detect 的构造函数。
super().__init__(nc, ch)
# 存储额外参数的数量。
self.ne = ne # number of extra parameters
# 计算中间通道数 c4 ,取 ch[0] 的四分之一和 ne 的最大值。
c4 = max(ch[0] // 4, self.ne)
# 创建一个模块列表 cv4 ,包含多个序列模块。每个序列模块包含两个3x3的卷积层,后跟一个1x1的卷积层,用于预测旋转角度。
self.cv4 = nn.ModuleList(nn.Sequential(Conv(x, c4, 3), Conv(c4, c4, 3), nn.Conv2d(c4, self.ne, 1)) for x in ch)
# 定义了 OBB 类的前向传播方法。
def forward(self, x):
# 连接并返回预测的边界框和类概率。
"""Concatenates and returns predicted bounding boxes and class probabilities."""
# 获取批量大小。
bs = x[0].shape[0] # batch size
# 将所有特征图的旋转角度在第三个维度上拼接起来。
angle = torch.cat([self.cv4[i](x[i]).view(bs, self.ne, -1) for i in range(self.nl)], 2) # OBB theta logits
# NOTE: set `angle` as an attribute so that `decode_bboxes` could use it. 将“angle”设置为属性,以便“decode_bboxes”可以使用它。
# 将角度的 logits 转换为实际的角度值,范围在 [-pi/4, 3pi/4] 之间。
angle = (angle.sigmoid() - 0.25) * math.pi # [-pi/4, 3pi/4]
# angle = angle.sigmoid() * math.pi / 2 # [0, pi/2]
# 检查是否处于非训练模式。
if not self.training:
# 如果不是训练模式,则将 angle 设置为类的属性,以便在 decode_bboxes 方法中使用。
self.angle = angle
# 调用父类 Detect 的 forward 方法,获取模型输出。
x = Detect.forward(self, x)
# 检查是否处于训练模式。
if self.training:
# 如果是训练模式,则返回模型输出 x 和旋转角度 angle 。
return x, angle
# 根据是否处于导出模式决定返回值。
# 如果是导出模式,则返回 模型输出 和 旋转角度 的拼接结果。
# 如果不是导出模式,则返回一个元组,包含模型输出的 第一个元素 和 旋转角度 的拼接结果,以及 模型输出的其余元素 和 旋转角度 。
return torch.cat([x, angle], 1) if self.export else (torch.cat([x[0], angle], 1), (x[1], angle))
# 这个方法的核心功能是处理旋转边界框的预测,包括预测旋转角度和解码旋转边界框坐标。在训练模式下,它返回模型输出和旋转角度;在非训练模式下,它根据是否导出模式返回不同的结果。
# 这段代码定义了 OBB 类中的 decode_bboxes 方法,它用于将模型输出的旋转边界框(Oriented Bounding Boxes, OBB)预测解码成实际的边界框坐标。
# 定义了 decode_bboxes 方法,接收两个参数 :
# 1.bboxes :模型输出的边界框预测,通常是相对于锚点的偏移量。
# 2.anchors :用于预测边界框的锚点坐标。
def decode_bboxes(self, bboxes, anchors):
# 解码旋转的边界框。
"""Decode rotated bounding boxes."""
# 调用 dist2rbox 函数将偏移量解码成旋转边界框坐标。 bboxes :传入的边界框预测。 self.angle :在 OBB 类的 forward 方法中计算并设置的旋转角度。 anchors :传入的锚点坐标。 dim=1 :指定在哪个维度上操作,这里选择在维度1上,即每个锚点对应的偏移量。
# def dist2rbox(pred_dist, pred_angle, anchor_points, dim=-1):
# -> 它用于将预测的旋转边界框的偏移量和角度从锚点和分布中解码出来。将 旋转后的中心点坐标 和 原始的左上角 与 右下角 偏移量 拼接,得到最终的旋转边界框坐标,并返回。
# -> return torch.cat([xy, lt + rb], dim=dim)
return dist2rbox(bboxes, self.angle, anchors, dim=1)
# dist2rbox 函数是一个通用的函数,用于将距离(或偏移量)和角度转换为旋转边界框坐标。这个转换通常涉及到一些几何计算,例如将中心点坐标、宽度、高度和角度转换为边界框的四个角点坐标,或者反之。
# 在这个上下文中, decode_bboxes 方法的作用是将模型的输出转换为可以直接用于非极大值抑制(NMS)和后续处理的旋转边界框坐标。
5.class Pose(Detect):
# 这段代码定义了一个名为 Pose 的类,它是 Detect 类的子类,专门用于处理关键点检测(pose estimation)任务。
# 定义了 Pose 类,它继承自 Detect 类。
class Pose(Detect):
# YOLOv8 关键点模型的姿势检测头。
"""YOLOv8 Pose head for keypoints models."""
# 是 Pose 类的构造函数,接收以下参数 :
# 1.nc :类别数量,默认为80。
# 2.kpt_shape :关键点的形状,是一个元组,第一个元素表示关键点的数量,第二个元素表示每个关键点的维度(2表示 x, y 坐标,3表示 x, y, visibility),默认为 (17, 3)。
# 3.ch :一个元组,表示每个检测层的输入通道数。
def __init__(self, nc=80, kpt_shape=(17, 3), ch=()):
# 使用默认参数和卷积层初始化 YOLO 网络。
"""Initialize YOLO network with default parameters and Convolutional Layers."""
# 调用父类 Detect 的构造函数。
super().__init__(nc, ch)
# 存储关键点的形状。
self.kpt_shape = kpt_shape # number of keypoints, number of dims (2 for x,y or 3 for x,y,visible)
# 计算总的关键点数量,即关键点数量乘以每个关键点的维度。
self.nk = kpt_shape[0] * kpt_shape[1] # number of keypoints total
# 计算中间通道数 c4 ,取 ch[0] 的四分之一和 nk 的最大值。
c4 = max(ch[0] // 4, self.nk)
# 创建一个模块列表 cv4 ,包含多个序列模块。每个序列模块包含两个3x3的卷积层,后跟一个1x1的卷积层,用于预测关键点坐标。
self.cv4 = nn.ModuleList(nn.Sequential(Conv(x, c4, 3), Conv(c4, c4, 3), nn.Conv2d(c4, self.nk, 1)) for x in ch)
# 这个 Pose 类的设计目的是为了在 YOLOv8 模型中添加关键点检测的功能。它通过继承 Detect 类并添加关键点预测相关的属性和方法来实现这一点。在构造函数中,它初始化了关键点预测所需的参数,并设置了卷积层来预测关键点坐标。
# 这段代码定义了 Pose 类的 forward 方法,它用于执行 YOLO 模型的前向传播,并返回预测结果,包括关键点检测。
# 定义了 Pose 类的前向传播方法,接收一个参数.
# x :它是一个包含多个特征层输出的列表。
def forward(self, x):
# 通过 YOLO 模型执行前向传递并返回预测。
"""Perform forward pass through YOLO model and return predictions."""
# 获取批量大小,即第一个特征层输出的批量维度大小。
bs = x[0].shape[0] # batch size
# 将所有特征层的关键点预测在最后一个维度上拼接起来。
# self.cv4[i](x[i]) :通过序列模块处理第 i 个特征层的输出,得到关键点预测的 logits。
# .view(bs, self.nk, -1) :将每个特征层的输出重新塑形为 (batch_size, self.nk, -1) 的形状,其中 self.nk 是总的关键点数量。
# torch.cat(..., -1) :沿最后一个维度(即所有特征层的关键点预测)拼接这些张量,得到形状为 (batch_size, self.nk, h*w) 的关键点预测张量。
kpt = torch.cat([self.cv4[i](x[i]).view(bs, self.nk, -1) for i in range(self.nl)], -1) # (bs, 17*3, h*w)
# 调用父类 Detect 的 forward 方法,获取模型的检测输出。
x = Detect.forward(self, x)
# 检查是否处于训练模式。
if self.training:
# 如果是训练模式,则返回模型输出 x 和关键点预测 kpt 。
return x, kpt
# 如果处于非训练模式,则调用 kpts_decode 方法对关键点预测进行解码,得到最终的关键点坐标。
pred_kpt = self.kpts_decode(bs, kpt)
# 根据是否处于导出模式决定返回值。
# 如果是导出模式,则返回 模型输出 和 解码后的关键点预测 的拼接结果。 如果不是导出模式,则返回一个元组,包含 模型输出的第一个元素 和 解码后的关键点预测 的拼接结果,以及 模型输出的其余元素 和 原始的关键点预测 。
return torch.cat([x, pred_kpt], 1) if self.export else (torch.cat([x[0], pred_kpt], 1), (x[1], kpt))
# 这个方法的核心功能是在 YOLO 模型的基础上添加关键点检测的能力,包括关键点预测和解码。在训练模式下,它返回模型输出和关键点预测;在非训练模式下,它根据是否导出模式返回不同的结果。
# 这段代码定义了 Pose 类中的 kpts_decode 方法,它用于将模型输出的关键点预测 logits 解码成实际的关键点坐标。
# 定义了 kpts_decode 方法,接收两个参数 :
# 1.bs :批量大小。
# 2.kpts :关键点预测的 logits,形状为 (batch_size, self.nk, h*w) 。
def kpts_decode(self, bs, kpts):
# 解码关键点。
"""Decodes keypoints."""
# 获取每个关键点的维度数。
ndim = self.kpt_shape[1]
# 检查是否处于导出模式。如果是导出模式,为了避免 TensorFlow Lite 导出时的特定 bug,使用不同的解码逻辑。
if self.export: # required for TFLite export to avoid 'PLACEHOLDER_FOR_GREATER_OP_CODES' bug
# 将 kpts 重新塑形为 (batch_size, num_keypoints, num_dims, h*w) 的形状。
y = kpts.view(bs, *self.kpt_shape, -1)
# 对关键点的 x 和 y 坐标进行解码。
# y[:, :, :2] :取出关键点的 x 和 y 坐标。 * 2.0 :将坐标缩放两倍。 + (self.anchors - 0.5) :将坐标偏移,使得范围从 [-0.5, 0.5] 变为 [0, 1] 。 * self.strides :将坐标乘以步长,转换为原始图像的尺度。
a = (y[:, :, :2] * 2.0 + (self.anchors - 0.5)) * self.strides
# 检查每个关键点的维度数是否为 3。
if ndim == 3:
# 如果是,则对关键点的可见性进行 sigmoid 激活,并与其他坐标拼接。
a = torch.cat((a, y[:, :, 2:3].sigmoid()), 2)
# 将解码后的关键点坐标重新塑形为 (batch_size, self.nk, h*w) 的形状并返回。
return a.view(bs, self.nk, -1)
# 如果不是导出模式,使用另一种解码逻辑。
else:
# 克隆关键点预测的 logits ,以避免修改原始数据。
y = kpts.clone()
# 检查每个关键点的维度数是否为 3。
if ndim == 3:
# 如果是,则对关键点的可见性进行 sigmoid 激活。
y[:, 2::3] = y[:, 2::3].sigmoid() # sigmoid (WARNING: inplace .sigmoid_() Apple MPS bug)
# 对关键点的 x 坐标进行解码。
y[:, 0::ndim] = (y[:, 0::ndim] * 2.0 + (self.anchors[0] - 0.5)) * self.strides
# 对关键点的 y 坐标进行解码。
y[:, 1::ndim] = (y[:, 1::ndim] * 2.0 + (self.anchors[1] - 0.5)) * self.strides
# 返回解码后的关键点坐标。
return y
# 这个方法的核心功能是将模型输出的关键点预测 logits 解码成实际的关键点坐标,包括 x、y 坐标和可见性(如果每个关键点有三个维度)。解码过程包括坐标的缩放、偏移和步长调整。
6.class Classify(nn.Module):
# 这段代码定义了一个名为 Classify 的类,它是 nn.Module 的子类,用于构建 YOLOv8 模型的分类头部。
# 定义了 Classify 类,它继承自 PyTorch 的 nn.Module 类。
class Classify(nn.Module):
# YOLOv8 分类头,即 x(b,c1,20,20) 到 x(b,c2)。
"""YOLOv8 classification head, i.e. x(b,c1,20,20) to x(b,c2)."""
# 是 Classify 类的构造函数,接收以下参数 :
# 1.c1 :输入通道数。
# 2.c2 :输出通道数(即分类类别数)。
# 3.k :卷积核大小,默认为1。
# 4.s :步长,默认为1。
# 5.p :填充,默认为None,表示没有填充。
# 6.g :分组卷积的组数,默认为1。
def __init__(self, c1, c2, k=1, s=1, p=None, g=1):
# 初始化 YOLOv8 分类头,将输入张量从 (b,c1,20,20) 转换为 (b,c2) 形状。
"""Initializes YOLOv8 classification head to transform input tensor from (b,c1,20,20) to (b,c2) shape."""
# 调用父类 nn.Module 的构造函数。
super().__init__()
# 定义一个中间通道数 c_ ,这里使用了 EfficientNet-B0 模型的输出通道数作为中间通道数。
c_ = 1280 # efficientnet_b0 size
# 创建一个卷积层,用于将输入通道数从 c1 转换为中间通道数 c_ 。
self.conv = Conv(c1, c_, k, s, p, g)
# 创建一个自适应平均池化层,将特征图的大小从 (b, c_, h, w) 转换为 (b, c_, 1, 1) 。
self.pool = nn.AdaptiveAvgPool2d(1) # to x(b,c_,1,1)
# 创建一个 dropout 层,用于正则化,防止过拟合。这里的 dropout 概率设置为0.0,表示不进行 dropout。
self.drop = nn.Dropout(p=0.0, inplace=True)
# 创建一个全连接层,用于将中间通道数 c_ 转换为输出通道数 c2 ,即分类类别数。
self.linear = nn.Linear(c_, c2) # to x(b,c2)
# 这个 Classify 类的设计目的是将 YOLOv8 模型的特征图转换为分类结果。它通过卷积层、自适应平均池化层、dropout 层和全连接层来实现这一转换。这个分类头部可以用于图像分类任务,将特征图映射到类别概率分布。
# 这段代码定义了 Classify 类的 forward 方法,它用于执行 YOLO 模型的前向传播,并返回分类结果。
# 定义了 Classify 类的前向传播方法,接收一个参数 x ,它是一个包含输入图像数据的张量或张量列表。
def forward(self, x):
# 对输入图像数据执行 YOLO 模型的前向传递。
"""Performs a forward pass of the YOLO model on input image data."""
# 检查输入 x 是否为列表类型。
if isinstance(x, list):
# 如果是列表,则使用 torch.cat(x, 1) 将列表中的所有张量沿第二个维度(通道维度)拼接起来。这通常用于处理多尺度输入或来自不同特征层的输出。
x = torch.cat(x, 1)
# 执行以下步骤对输入 x 进行处理 :
# self.conv(x) :将输入 x 通过卷积层 conv 。
# self.pool(...) :将卷积层的输出通过自适应平均池化层 pool ,将特征图的大小减少到 (b, c_, 1, 1) 。
# flatten(1) :将池化后的输出在第二个维度(通道维度)上展平,得到形状为 (b, c_) 的张量。
# self.drop(...) :将展平后的输出通过 dropout 层 drop 。
# self.linear(...) :将 dropout 层的输出通过全连接层 linear ,得到最终的分类结果。
x = self.linear(self.drop(self.pool(self.conv(x)).flatten(1)))
# 根据是否处于训练模式返回不同的结果。
# 如果处于训练模式,则直接返回全连接层的输出 x 。 如果不处于训练模式(即推理模式),则返回 softmax 激活后的输出 x.softmax(1) 。softmax 激活函数用于将输出转换为概率分布,这对于分类任务是必要的。
# x.softmax(1) 中的 (1) 表示 :dim=1,指定在张量的第二个维度(索引从0开始)上进行 softmax 操作。这意味着 softmax 会沿着这个维度计算,将该维度上的每个子向量转换为一个概率分布。
return x if self.training else x.softmax(1)
# 这个方法的核心功能是将输入图像数据通过 YOLO 模型的分类头部进行处理,并返回分类结果。在训练模式下,它返回原始的 logits 输出;在推理模式下,它返回 softmax 激活后的概率分布。
7.class WorldDetect(Detect):
# 这段代码定义了一个名为 WorldDetect 的类,它是 Detect 类的子类,用于将 YOLOv8 检测模型与从文本嵌入中获得的语义理解相结合。
# 定义了 WorldDetect 类,它继承自 Detect 类。
class WorldDetect(Detect):
# 致力于将 YOLOv8 检测模型与文本嵌入的语义理解相结合。
"""Head for integrating YOLOv8 detection models with semantic understanding from text embeddings."""
# 是 WorldDetect 类的构造函数,接收以下参数 :
# 1.nc :类别数量,默认为80。
# 2.embed :嵌入的维度,默认为512。
# 3.with_bn :一个布尔值,指示是否在对比头中使用批量归一化(Batch Normalization),默认为False。•
# 4.ch :一个元组,表示每个检测层的输入通道数。
def __init__(self, nc=80, embed=512, with_bn=False, ch=()):
# 使用 nc 个类和层通道 ch 初始化 YOLOv8 检测层。
"""Initialize YOLOv8 detection layer with nc classes and layer channels ch."""
# 调用父类 Detect 的构造函数。
super().__init__(nc, ch)
# 计算中间通道数 c3 ,取 ch[0] 和 self.nc (类别数量)与100的最小值之间的最大值。
c3 = max(ch[0], min(self.nc, 100))
# 创建一个模块列表 cv3 ,包含多个序列模块。每个序列模块包含两个3x3的卷积层,后跟一个1x1的卷积层,用于预测嵌入特征。
self.cv3 = nn.ModuleList(nn.Sequential(Conv(x, c3, 3), Conv(c3, c3, 3), nn.Conv2d(c3, embed, 1)) for x in ch)
# 创建另一个模块列表 cv4 ,包含多个对比头模块。如果 with_bn 为True,则使用 BNContrastiveHead (包含批量归一化的对比头),否则使用 ContrastiveHead (基本的对比头)。
# class BNContrastiveHead(nn.Module):
# -> 它是对比学习头部的一个变体,用于 YOLO-World 模型,特点是使用批量归一化(Batch Normalization)代替 L2 归一化。
# -> def __init__(self, embed_dims: int):
# -> def forward(self, x, w):
# -> 返回值 :返回调整后的对比学习输出,形状为 (B, K, H, W) ,其中每个元素表示对应区域特征和文本特征之间的相似度得分。
# -> return x * self.logit_scale.exp() + self.bias
# class ContrastiveHead(nn.Module):
# -> 它用于实现视觉-语言模型中的对比学习头部,用于计算区域-文本相似性。
# -> def __init__(self):
# -> def forward(self, x, w):
# -> 返回值 :返回调整后的对比学习输出,形状为 (B, K, H, W) ,其中每个元素表示对应区域特征和文本特征之间的相似度得分。
# -> return x * self.logit_scale.exp() + self.bias
self.cv4 = nn.ModuleList(BNContrastiveHead(embed) if with_bn else ContrastiveHead() for _ in ch)
# 这个 WorldDetect 类的设计目的是在 YOLOv8 检测模型的基础上添加对文本嵌入的处理能力,以便进行更丰富的语义理解。通过这种方式,模型不仅能够检测图像中的对象,还能够理解这些对象与给定文本之间的关系。这种集成可以帮助模型在复杂的场景中进行更准确的检测和分类。
# 这段代码定义了 WorldDetect 类的 forward 方法,它用于执行前向传播,并返回预测的边界框和类别概率,同时整合了文本嵌入信息。
# 定义了 WorldDetect 类的前向传播方法,接收两个参数 :
# x :包含多个特征层输出的列表。
# text :文本嵌入信息。
def forward(self, x, text):
# 连接并返回预测的边界框和类概率。
"""Concatenates and returns predicted bounding boxes and class probabilities."""
# 循环遍历每个检测层。
for i in range(self.nl):
# 对于每个检测层,首先通过 cv2 和 cv3 模块处理特征图 x[i] ,然后将 cv3 的输出与文本嵌入 text 结合,通过 cv4 模块处理,最后将这两个结果沿通道维度(维度1)拼接起来。
x[i] = torch.cat((self.cv2[i](x[i]), self.cv4[i](self.cv3[i](x[i]), text)), 1)
# 检查是否处于训练模式。
if self.training:
# 如果是训练模式,则直接返回拼接后的结果列表 x 。
return x
# Inference path
# 获取第一个特征图的形状,这里假设所有特征图的形状相同。
shape = x[0].shape # BCHW
# 将所有特征图的输出在第三个维度(宽度)上拼接起来,形成一个大的特征图。
x_cat = torch.cat([xi.view(shape[0], self.nc + self.reg_max * 4, -1) for xi in x], 2)
# 检查是否需要动态重建网格或当前特征图形状与之前的形状不同。
if self.dynamic or self.shape != shape:
# 如果需要,则调用 make_anchors 函数生成锚点和步长,并更新 self.shape 。
# def make_anchors(feats, strides, grid_cell_offset=0.5):
# -> 它用于从特征图生成锚点(anchors)。锚点是在目标检测中用于预测目标位置的参考框。将所有特征图的 锚点 和 步长张量 分别在第一个维度上拼接起来,并返回结果。
# -> return torch.cat(anchor_points), torch.cat(stride_tensor)
self.anchors, self.strides = (x.transpose(0, 1) for x in make_anchors(x, self.stride, 0.5))
self.shape = shape
# 检查是否需要导出模型,并根据导出格式决定如何处理边界框和类别概率的分割。
if self.export and self.format in {"saved_model", "pb", "tflite", "edgetpu", "tfjs"}: # avoid TF FlexSplitV ops
# 提取边界框预测。
box = x_cat[:, : self.reg_max * 4]
# 提取类别概率预测。
cls = x_cat[:, self.reg_max * 4 :]
# 如果不是导出模式或导出格式不需要特殊处理,则直接分割边界框和类别概率。
else:
# 在通道维度上分割特征图,分别得到边界框和类别概率。
box, cls = x_cat.split((self.reg_max * 4, self.nc), 1)
# 检查是否需要为特定的导出格式预计算归一化因子以增加数值稳定性。
if self.export and self.format in {"tflite", "edgetpu"}:
# Precompute normalization factor to increase numerical stability
# See https://github.com/ultralytics/ultralytics/issues/7371
grid_h = shape[2]
grid_w = shape[3]
# 计算网格大小。
grid_size = torch.tensor([grid_w, grid_h, grid_w, grid_h], device=box.device).reshape(1, 4, 1)
# 计算归一化因子。
norm = self.strides / (self.stride[0] * grid_size)
# 使用归一化的锚点和边界框预测解码边界框。
# def decode_bboxes(self, bboxes, anchors):
# -> 它用于将模型输出的边界框预测(通常是相对于锚点的偏移量)解码成实际的边界框坐标。作用是将模型的输出转换为可以直接用于非极大值抑制(NMS)和后续处理的边界框坐标。
# -> return dist2bbox(bboxes, anchors, xywh=not self.end2end, dim=1)
dbox = self.decode_bboxes(self.dfl(box) * norm, self.anchors.unsqueeze(0) * norm[:, :2])
# 如果不需要特殊处理,则直接解码边界框。
else:
# 使用DFL模块处理边界框预测,并使用锚点和步长解码边界框。
dbox = self.decode_bboxes(self.dfl(box), self.anchors.unsqueeze(0)) * self.strides
# 将解码后的边界框和sigmoid激活后的类别概率在通道维度上拼接,并得到最终的预测结果 y 。
y = torch.cat((dbox, cls.sigmoid()), 1)
# 根据是否处于导出模