Bootstrap

【YOLOv11改进- 原理解析】 YOLO11 架构解析 以及代码库关键代码逐行解析

YOLOv11目标检测创新改进与实战案例专栏

文章目录: YOLOv11创新改进系列及项目实战目录 包含卷积,主干 注意力,检测头等创新机制 以及 各种目标检测分割项目实战案例

专栏链接: YOLOv11目标检测创新改进与实战案例

在这里插入图片描述

文章目录

冷笑话

🤦‍♂️🤦‍♂️🤦‍♂️:: YOLO没有YOLOv11,只有YOLO11

从YAML了解YOLO11结构

官方配置

# Ultralytics YOLO 🚀, AGPL-3.0 license
# YOLO11 object detection model with P3-P5 outputs. For Usage examples see https://docs.ultralytics.com/tasks/detect

# Parameters
nc: 8 # number of classes
scales: # model compound scaling constants, i.e. 'model=yolo11n.yaml' will call yolo11.yaml with scale 'n'
  # [depth, width, max_channels]
  n: [0.50, 0.25, 1024] # summary: 319 layers, 2624080 parameters, 2624064 gradients, 6.6 GFLOPs
  s: [0.50, 0.50, 1024] # summary: 319 layers, 9458752 parameters, 9458736 gradients, 21.7 GFLOPs
  m: [0.50, 1.00, 512] # summary: 409 layers, 20114688 parameters, 20114672 gradients, 68.5 GFLOPs
  l: [1.00, 1.00, 512] # summary: 631 layers, 25372160 parameters, 25372144 gradients, 87.6 GFLOPs
  x: [1.00, 1.50, 512] # summary: 631 layers, 56966176 parameters, 56966160 gradients, 196.0 GFLOPs

# YOLO11n backbone
backbone:
  # [from, repeats, module, args]
  - [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
  - [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
  - [-1, 2, C3k2, [256, False, 0.25]]
  - [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
  - [-1, 2, C3k2, [512, False, 0.25]]
  - [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
  - [-1, 2, C3k2, [512, True]]
  - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
  - [-1, 2, C3k2, [1024, True]]
  - [-1, 1, SPPF, [1024, 5]] # 9
  - [-1, 2, C2PSA, [1024]] # 10

# YOLO11n head
head:
  - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
  - [[-1, 6], 1, Concat, [1]] # cat backbone P4
  - [-1, 2, C3k2, [512, False]] # 13

  - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
  - [[-1, 4], 1, Concat, [1]] # cat backbone P3
  - [-1, 2, C3k2, [256, False]] # 16 (P3/8-small)

  - [-1, 1, Conv, [256, 3, 2]]
  - [[-1, 13], 1, Concat, [1]] # cat head P4
  - [-1, 2, C3k2, [512, False]] # 19 (P4/16-medium)

  - [-1, 1, Conv, [512, 3, 2]]
  - [[-1, 10], 1, Concat, [1]] # cat head P5
  - [-1, 2, C3k2, [1024, True]] # 22 (P5/32-large)

  - [[16, 19, 22], 1, Detect, [nc]] # Detect(P3, P4, P5)

架构的变化:

Backbone(骨干)

骨干网络是模型的一部分,负责从输入图像中提取多尺度特征。通常通过堆叠卷积层和模块来生成不同分辨率的特征图。

backbone:
  # [from, repeats, module, args]
  - [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
  - [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
  - [-1, 2, C3k2, [256, False, 0.25]]
  - [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
  - [-1, 2, C3k2, [512, False, 0.25]]
  - [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
  - [-1, 2, C3k2, [512, True]]
  - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
  - [-1, 2, C3k2, [1024, True]]
  - [-1, 1, SPPF, [1024, 5]] # 9
  - [-1, 2, C2PSA, [1024]] # 10
骨干中的卷积层:

YOLO11具有类似V8的结构,最初的卷积层用于对图像进行下采样。

- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
骨干中的C3k2

YOLO11引入了C3k2模块来代替C2f模块,该模块在计算方面更加高效。这个模块是CSP瓶颈层的自定义实现,它使用了两个卷积层,而不是像YOLOv8那样使用一个大的卷积层。

CSP (Cross Stage Partial): CSP网络将特征图分成两部分,一部分通过瓶颈层进行处理,另一部分与瓶颈层的输出进行合并。这样可以减少计算负载,并改进特征表示。

C3k2模块还使用了较小的卷积核尺寸(由k2表示),这使得它在保持性能的同时处理速度更快。

- [-1, 2, C3k2, [256, False, 0.25]]
骨干中的SPPF and C2PSA

YOLO11保留了V8中的SPPF模块,但在SPPF之后新增了一个C2PSA模块。

- [-1, 1, SPPF, [1024, 5]]
- [-1, 2, C2PSA, [1024]

C2PSA(Cross Stage Partial with Spatial Attention:交叉阶段部分与空间注意力)模块增强了特征图中的空间注意力,提升了模型对图像中重要部分的关注。通过空间池化特征,该模块使模型能够更有效地关注特定的兴趣区域。

Head(头部)

头部结构负责聚合来自不同分辨率的特征,并将它们传递给头部进行预测。它通常包括对特征图的上采样以及来自不同层级特征图的拼接。

# YOLO11n head
head:
  - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
  - [[-1, 6], 1, Concat, [1]] # cat backbone P4
  - [-1, 2, C3k2, [512, False]] # 13

  - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
  - [[-1, 4], 1, Concat, [1]] # cat backbone P3
  - [-1, 2, C3k2, [256, False]] # 16 (P3/8-small)

  - [-1, 1, Conv, [256, 3, 2]]
  - [[-1, 13], 1, Concat, [1]] # cat head P4
  - [-1, 2, C3k2, [512, False]] # 19 (P4/16-medium)

  - [-1, 1, Conv, [512, 3, 2]]
  - [[-1, 10], 1, Concat, [1]] # cat head P5
  - [-1, 2, C3k2, [1024, True]] # 22 (P5/32-large)
头部中的C3k2

YOLO11在颈部结构中将C2f模块替换为C3k2模块。如前所述,C3k2模块更快且效率更高。例如,在上采样和拼接之后,YOLO11中的颈部结构如下所示:

此更改提高了要素聚合过程的速度和性能。

  - [-1, 2, C3k2, [512, False]] # 13

Detect(检测层)

最终的 Detect 层与 YOLOv8 中的层相同:

  - [[16, 19, 22], 1, Detect, [nc]] # Detect(P3, P4, P5)

新层或者新模块的代码

C3k2 (blocks.py )

C3k2是一种更快且更高效的CSP瓶颈变体。它使用两个卷积代替一个大的卷积,从而加快了特征提取的速度。

class C3k2(C2f):
    def __init__(self, c1, c2, n=1, c3k=False, e=0.5, g=1, shortcut=True):
        super().__init__(c1, c2, n, shortcut, g, e)
        self.m = nn.ModuleList(
            C3k(self.c, self.c, 2, shortcut, g) if c3k else Bottleneck(self.c, self.c, shortcut, g) for _ in range(n)
        )

C3k (blocks.py )

C3k是一个更灵活的瓶颈模块,允许定制卷积核的大小。这对于提取图像中的更详细特征非常有用。

class C3k2(C2f):
    def __init__(self, c1, c2, n=1, c3k=False, e=0.5, g=1, shortcut=True):
        # 调用父类 C2f 的构造函数,初始化基础参数
        # c1: 输入通道数
        # c2: 输出通道数
        # n: 堆叠 Bottleneck 或 C3k 层的数量,默认值为1
        # c3k: 如果为 True,则使用 C3k 结构;如果为 False,则使用 Bottleneck 结构
        # e: 膨胀系数,默认为 0.5,控制 Bottleneck 内部卷积通道数
        # g: 分组卷积的组数,默认为 1
        # shortcut: 是否使用残差连接,默认为 True
        super().__init__(c1, c2, n, shortcut, g, e)

        # self.m: 使用 nn.ModuleList 来存储多层模块的列表
        # 根据传入的 c3k 参数决定使用 C3k 模块还是 Bottleneck 模块
        # 如果 c3k 为 True,则使用 C3k 模块;否则使用 Bottleneck 模块
        # self.c 是从父类 C2f 中继承的计算出的通道数
        # 这里会根据 n 的数量创建多个 Bottleneck 或 C3k 模块,组成一个列表
        self.m = nn.ModuleList(
            # 如果 c3k 为 True,则创建 C3k 模块,输入输出通道都为 self.c,shortcut 和 g 保持一致,卷积核大小为 2
            C3k(self.c, self.c, 2, shortcut, g) if c3k else 
            # 否则,创建 Bottleneck 模块,输入输出通道同样为 self.c,shortcut 和 g 也保持一致
            Bottleneck(self.c, self.c, shortcut, g) 
            for _ in range(n)  # 这里根据 n 来创建 n 个模块
        )

C2PSA (blocks.py )

C2PSA(Cross Stage Partial with Spatial Attention)增强了模型的空间注意力能力。该模块通过在特征图中添加注意力机制,帮助模型更好地关注图像中的重要区域。

class C2PSA(nn.Module):
    def __init__(self, c1, c2, e=0.5):
        # 初始化父类 nn.Module
        # c1: 输入通道数
        # c2: 输出通道数
        # e: 通道压缩系数,默认为 0.5,控制中间层的通道数
        super().__init__()
        
        # 计算中间层通道数 c_,将输出通道数 c2 按照系数 e 进行缩放
        c_ = int(c2 * e)
        
        # 定义第一个卷积层 cv1
        # 该层将输入从 c1 个通道压缩到 c_ 个通道,使用 1x1 卷积核,步长为 1
        self.cv1 = Conv(c1, c_, 1, 1)
        
        # 定义第二个卷积层 cv2
        # 与 cv1 类似,也是将输入从 c1 个通道压缩到 c_ 个通道,使用 1x1 卷积核,步长为 1
        self.cv2 = Conv(c1, c_, 1, 1)
        
        # 定义第三个卷积层 cv3
        # 将两个中间卷积层输出的结果(2 * c_ 通道)合并后,恢复到 c2 个通道,使用 1x1 卷积核
        self.cv3 = Conv(2 * c_, c2, 1)
    
    def forward(self, x):
        # 前向传播函数,将输入 x 通过 cv1 和 cv2 分别处理
        # torch.cat 将两个处理后的结果沿着通道维度(dim=1)拼接在一起
        # 拼接后的张量通过 cv3 进行卷积运算,最终得到输出
        return self.cv3(torch.cat((self.cv1(x), self.cv2(x)), 1))

YOLO11核心文件及代码

核心文件

ultralytics/nn/modules
	- block.py
	- conv.py
	- head.py
	- transformer.py
	- utils.py
ultralytics/nn/
	tasks.p

作用

  1. block.py:定义模型中使用的各种构建块(模块),例如瓶颈、CSP 模块和注意力机制。
  2. conv.py:包含卷积模块,包括标准卷积、深度卷积和其他变体。
  3. head.py:实现负责生成最终预测(例如,边界框、类概率)的模型头。
  4. transformer.py:包括基于 transformer 的模块,用于注意力机制和高级特征提取。
  5. utils.py:提供跨模块使用的实用程序函数和帮助程序类。

block.py

1. Bottleneck Modules 瓶颈

Bottleneck类实现了Bottleneck: 一个标准的瓶颈模块,带有可选的捷径连接。Res: 一个残差块,使用一系列卷积和一个恒等捷径连接。

组件:

  • self.cv1:1×1卷积,用于减少通道数量。
  • self.cv2:3×3卷积,用于将通道数量恢复到原始值。
  • self.add:一个布尔值,指示是否添加快捷连接(shortcut connection)。

前向传播(Forward Pass):

输入x先通过cv1和cv2卷积操作。如果self.add为True,则将原始输入x与输出相加(即残差连接,residual connection)。

class Bottleneck(nn.Module):
    def __init__(self, c1, c2, shortcut=True, g=1, e=0.5):
        # 初始化父类 nn.Module
        # c1: 输入通道数
        # c2: 输出通道数
        # shortcut: 是否使用残差连接,默认为 True
        # g: 分组卷积的组数,默认为 1,即标准卷积
        # e: 通道压缩系数,默认为 0.5,控制中间层的通道数
        super().__init__()

        # 计算中间层的通道数 c_,将输出通道数 c2 按照系数 e 进行缩放
        c_ = int(c2 * e)
        
        # 定义第一个卷积层 cv1
        # 该层将输入从 c1 个通道压缩到 c_ 个通道,使用 1x1 卷积核,步长为 1
        self.cv1 = Conv(c1, c_, 1, 1)

        # 定义第二个卷积层 cv2
        # 该层将中间层的通道数从 c_ 恢复到 c2,使用 3x3 卷积核,步长为 1
        # g 参数控制分组卷积的组数,如果 g=1 则为普通卷积
        self.cv2 = Conv(c_, c2, 3, 1, g=g)

        # 判断是否需要使用残差连接
        # 如果 shortcut=True 且输入通道数 c1 等于输出通道数 c2,则可以使用残差连接
        self.add = shortcut and c1 == c2

    def forward(self, x):
        # 前向传播函数,定义数据流过网络的过程
        
        # 如果 self.add 为 True,则进行残差连接:输入 x 加上通过两个卷积层 cv1 和 cv2 处理后的输出
        # 如果 self.add 为 False,则仅返回通过两个卷积层处理后的结果
        return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))

2. CSP (Cross Stage Partial) Modules

CSPBottleneck 模块将特征图分成两部分,一部分通过一系列瓶颈层,另一部分直接连接到输出,从而降低计算成本并增强梯度流动。

组件:

  • self.cv1:1×1卷积,用于减少通道数量。
  • self.cv2:一系列瓶颈层,用于对特征进行处理。
  • self.cv3:组合特征并调整通道数量。
  • self.add:一个布尔值,确定是否添加快捷连接(shortcut connection)。

前向传播(Forward Pass):
输入特征图被分成两部分:一部分通过 self.cv2 的瓶颈层处理,另一部分则直接保留原样。最后,这两部分在 self.cv3 中进行组合并调整通道数量。如果 self.add 为 True,则加入残差连接。

class BottleneckCSP(nn.Module):
    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
        # 初始化父类 nn.Module
        # c1: 输入通道数
        # c2: 输出通道数
        # n: Bottleneck 模块的重复次数,默认为 1
        # shortcut: 是否使用残差连接,默认为 True
        # g: 分组卷积的组数,默认为 1,即标准卷积
        # e: 通道压缩系数,默认为 0.5,控制中间层的通道数
        super().__init__()
        
        # 计算中间层的通道数 c_,将输出通道数 c2 按照系数 e 进行缩放
        c_ = int(c2 * e)
        
        # 定义第一个卷积层 cv1
        # 该层将输入从 c1 个通道压缩到 c_ 个通道,使用 1x1 卷积核,步长为 1
        self.cv1 = Conv(c1, c_, 1, 1)
        
        # 定义一个由 n 个 Bottleneck 组成的顺序容器 cv2
        # 每个 Bottleneck 都以 c_ 作为输入和输出通道,shortcut 和 g 参数与外层传入一致
        # e=1.0 意味着 Bottleneck 模块内部不会进一步压缩通道数
        self.cv2 = nn.Sequential(
            *[Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)]
        )
        
        # 定义第三个卷积层 cv3
        # 输入为拼接后的 2 * c_ 个通道,输出为 c2 个通道,使用 1x1 卷积核
        self.cv3 = Conv(2 * c_, c2, 1)
        
        # 判断是否需要使用残差连接
        # 如果输入通道数 c1 等于输出通道数 c2,则可以进行残差连接
        self.add = c1 == c2

    def forward(self, x):
        # 前向传播函数
        
        # 首先通过 cv1 进行卷积,将通道数从 c1 压缩到 c_
        # 然后通过包含 n 个 Bottleneck 的 cv2 进行处理
        y1 = self.cv2(self.cv1(x))
        
        # y2 负责保存残差连接部分
        # 如果 self.add 为 True,则 y2 为输入 x,否则 y2 为 None
        y2 = x if self.add else None
        
        # 如果 y2 不为 None(即残差连接成立),则将 y1 和 y2 在通道维度上进行拼接
        # 最后通过 cv3 进行卷积,返回输出
        # 如果 y2 为 None,则仅返回通过 cv3 处理后的 y1
        return self.cv3(torch.cat((y1, y2), 1)) if y2 is not None else self.cv3(y1)

SPPF

SPPF 模块在不同的尺度上执行最大池化,并将结果连接起来,以捕捉多个空间尺度的特征。

组件:

  • self.cv1:减少通道数量。
  • self.cv2:在连接后调整通道数量。
  • self.m:最大池化层。

前向传播(Forward Pass):

输入 x 先通过 cv1,然后依次通过三个连续的最大池化层(y1、y2、y3)。结果会被连接起来并通过 cv2。

class SPPF(nn.Module):
    def __init__(self, c1, c2, k=5):
        super().__init__()
        c_ = c1 // 2
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c_ * 4, c2, 1, 1)
        self.m = nn.MaxPool2d(kernel_size=k, stride=1, padding=k // 2)

    def forward(self, x):
        x = self.cv1(x)
        y1 = self.m(x)
        y2 = self.m(y1)
        y3 = self.m(y2)
        return self.cv2(torch.cat([x, y1, y2, y3], 1))

conv.py

此文件包含各种卷积模块,包括标准卷积和专用卷积。

标准卷积模块 (Conv)

class Conv(nn.Module):
    default_act = nn.SiLU()  # default activation

    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
        super().__init__()
        self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False)
        self.bn = nn.BatchNorm2d(c2)
        self.act = self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity()

    def forward(self, x):
        return self.act(self.bn(self.conv(x)))

实现一个带有批量归一化和激活函数的标准卷积层。

组件:

  • self.conv:卷积层。
  • self.bn:批量归一化层。
  • self.act:激活函数(默认是 nn.SiLU())。

前向传递过程:先进行卷积操作,然后是批量归一化,最后是激活函数。

深度卷积 (DWConv)

class DWConv(Conv):
    def __init__(self, c1, c2, k=1, s=1, d=1, act=True):
        super().__init__(c1, c2, k, s, g=math.gcd(c1, c2), d=d, act=act)

执行深度卷积,其中每个输入通道单独进行卷积。

组件:

  • 继承自 Conv。
  • 将 groups 参数设置为 c1 和 c2 的最大公约数,从而实现按通道分组的卷积。

其他卷积模块:

  • Conv2:RepConv 的简化版本,用于模型压缩和加速。
  • GhostConv:实现了 GhostNet 的 ghost 模块,减少特征图中的冗余。
  • RepConv:可重新参数化的卷积层,可以从训练模式转换为推理模式。

head.py

此文件实现了负责生成模型最终预测的 head 模块

检测头 (Detect):

class Detect(nn.Module):
    def __init__(self, nc=80, ch=()):
        super().__init__()
        self.nc = nc  # number of classes
        self.nl = len(ch)  # number of detection layers
        self.reg_max = 16  # DFL channels
        self.no = nc + self.reg_max * 4  # number of outputs per anchor
        self.stride = torch.zeros(self.nl)  # strides computed during build

        # Define layers
        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
        )
        self.cv3 = nn.ModuleList(
            nn.Sequential(
                nn.Sequential(DWConv(x, x, 3), Conv(x, c3, 1)),
                nn.Sequential(DWConv(c3, c3, 3), Conv(c3, c3, 1)),
                nn.Conv2d(c3, self.nc, 1),
            )
            for x in ch
        )
        self.dfl = DFL(self.reg_max) if self.reg_max > 1 else nn.Identity()

Detect 类定义了检测头,其输出包括边界框坐标和类别概率。

组件:

  • self.cv2:用于边界框回归的卷积层,负责预测边界框的坐标。
  • self.cv3:用于分类的卷积层,负责预测类别概率。
  • self.dfl:用于边界框精细化的 Distribution Focal Loss 模块,帮助提高边界框的精度。

前向传播过程:

  • 处理输入的特征图。
  • self.cv2 进行边界框回归,输出边界框的坐标预测。
  • self.cv3 进行分类预测,输出类别概率。
  • self.dfl 对边界框的回归结果进行优化和精细化。

这个类的主要功能是利用检测头从特征图中生成边界框的坐标和相应的类别概率,并通过损失模块(如 dfl)对边界框预测进行精细调整。

分割头(Segment

class Segment(Detect):
    def __init__(self, nc=80, nm=32, npr=256, ch=()):
        super().__init__(nc, ch)
        self.nm = nm  # number of masks
        self.npr = npr  # number of prototypes
        self.proto = Proto(ch[0], self.npr, self.nm)  # protos

        c4 = max(ch[0] // 4, self.nm)
        self.cv4 = nn.ModuleList(nn.Sequential(Conv(x, c4, 3), Conv(c4, c4, 3), nn.Conv2d(c4, self.nm, 1)) for x in ch)

组件:

  • self.proto:生成用于分割的 mask prototypes(原型掩码),这些原型掩码是潜在的掩码基础,用于构建具体目标的掩码。
  • self.cv4:用于计算 mask coefficients(掩码系数)的卷积层。每个检测到的对象都有其相应的系数,系数与原型掩码组合生成最终的实例掩码。

前向传播过程:

  1. Bounding Box Regression and Classification:

    • 和基础的 Detect 类一样,输入特征图经过 self.cv2 进行边界框回归,生成边界框坐标。
    • 特征图经过 self.cv3 进行分类,生成类别概率。
  2. Mask Generation:

    • 特征图通过 self.proto,生成若干 mask prototypes。这些原型是一个尺寸较小的特征图,包含多个潜在的掩码。
    • 然后,特征图通过 self.cv4 生成 mask coefficients,这些系数与每个检测到的对象一一对应。
    • 最终,通过将每个检测对象的 mask coefficientsmask prototypes 进行组合,可以生成对应的对象掩码。

输出:

  • Bounding boxes:对象的边界框坐标。
  • Class probabilities:对象的分类概率。
  • Mask coefficients:用于生成对象掩码的系数。
  • Masks(可选的输出)通过 mask coefficients 和 mask prototypes 的组合计算出具体的实例掩码。

这种扩展实现了检测和分割的一体化网络设计,通常用于实例分割任务。模型不仅可以预测目标的边界框,还可以生成该目标的精确掩码。

姿势估计头部 (Pose):

class Pose(Detect):
    def __init__(self, nc=80, kpt_shape=(17, 3), ch=()):
        super().__init__(nc, ch)
        self.kpt_shape = kpt_shape  # number of keypoints, number of dimensions
        self.nk = kpt_shape[0] * kpt_shape[1]  # total number of keypoint outputs

        c4 = max(ch[0] // 4, self.nk)
        self.cv4 = nn.ModuleList(nn.Sequential(Conv(x, c4, 3), Conv(c4, c4, 3), nn.Conv2d(c4, self.nk, 1)) for x in ch)

组件:

  1. self.kpt_shape

    • 定义关键点的形状,表示每个目标有多少个关键点以及每个关键点的维度(通常为 2D 坐标 (x, y) 或 3D 坐标 (x, y, z))。
    • 例如,如果模型预测 17 个 2D 关键点,那么 self.kpt_shape 可能是 (17, 2)。
  2. self.cv4

    • 用于关键点回归的卷积层。类似于边界框回归的设计,这些卷积层专门学习每个检测对象的关键点位置。
    • 这些卷积层会输出与 self.kpt_shape 相匹配的预测,即每个目标对应的关键点坐标。

前向传播过程:

  1. Bounding Box Regression and Classification

    • 和标准的 Detect 类一样,输入特征图首先通过 self.cv2 生成边界框坐标(bounding boxes),用于定位检测对象。
    • 然后,特征图经过 self.cv3,输出每个对象的类别概率(class probabilities),用于目标分类。
  2. Keypoint Regression

    • 特征图经过 self.cv4 生成关键点坐标。这里的卷积层直接回归出每个检测对象的关键点坐标。
    • 输出的形状与 self.kpt_shape 相匹配,例如 (17, 2) 的关键点坐标形式。
    • 每个目标(检测到的边界框)都会有一组关键点与之对应。

输出:

  • Bounding boxes:预测的对象边界框坐标。
  • Class probabilities:对象的分类概率。
  • Keypoint coordinates:人体关键点的坐标,这些关键点表示人体关节的具体位置。

utils.py

# Ultralytics YOLO 🚀, AGPL-3.0 license
"""Module utils."""

import copy
import math

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.init import uniform_

__all__ = "multi_scale_deformable_attn_pytorch", "inverse_sigmoid"


def _get_clones(module, n):
    """Create a list of cloned modules from the given module."""
    return nn.ModuleList([copy.deepcopy(module) for _ in range(n)])


def bias_init_with_prob(prior_prob=0.01):
    """Initialize conv/fc bias value according to a given probability value."""
    return float(-np.log((1 - prior_prob) / prior_prob))  # return bias_init


def linear_init(module):
    """Initialize the weights and biases of a linear module."""
    bound = 1 / math.sqrt(module.weight.shape[0])
    uniform_(module.weight, -bound, bound)
    if hasattr(module, "bias") and module.bias is not None:
        uniform_(module.bias, -bound, bound)


def inverse_sigmoid(x, eps=1e-5):
    """Calculate the inverse sigmoid function for a tensor."""
    x = x.clamp(min=0, max=1)
    x1 = x.clamp(min=eps)
    x2 = (1 - x).clamp(min=eps)
    return torch.log(x1 / x2)


def multi_scale_deformable_attn_pytorch(
    value: torch.Tensor,
    value_spatial_shapes: torch.Tensor,
    sampling_locations: torch.Tensor,
    attention_weights: torch.Tensor,
) -> torch.Tensor:
    """
    Multiscale deformable attention.

    https://github.com/IDEA-Research/detrex/blob/main/detrex/layers/multi_scale_deform_attn.py
    """
    bs, _, num_heads, embed_dims = value.shape
    _, num_queries, num_heads, num_levels, num_points, _ = sampling_locations.shape
    value_list = value.split([H_ * W_ for H_, W_ in value_spatial_shapes], dim=1)
    sampling_grids = 2 * sampling_locations - 1
    sampling_value_list = []
    for level, (H_, W_) in enumerate(value_spatial_shapes):
        # bs, H_*W_, num_heads, embed_dims ->
        # bs, H_*W_, num_heads*embed_dims ->
        # bs, num_heads*embed_dims, H_*W_ ->
        # bs*num_heads, embed_dims, H_, W_
        value_l_ = value_list[level].flatten(2).transpose(1, 2).reshape(bs * num_heads, embed_dims, H_, W_)
        # bs, num_queries, num_heads, num_points, 2 ->
        # bs, num_heads, num_queries, num_points, 2 ->
        # bs*num_heads, num_queries, num_points, 2
        sampling_grid_l_ = sampling_grids[:, :, :, level].transpose(1, 2).flatten(0, 1)
        # bs*num_heads, embed_dims, num_queries, num_points
        sampling_value_l_ = F.grid_sample(
            value_l_, sampling_grid_l_, mode="bilinear", padding_mode="zeros", align_corners=False
        )
        sampling_value_list.append(sampling_value_l_)
    # (bs, num_queries, num_heads, num_levels, num_points) ->
    # (bs, num_heads, num_queries, num_levels, num_points) ->
    # (bs, num_heads, 1, num_queries, num_levels*num_points)
    attention_weights = attention_weights.transpose(1, 2).reshape(
        bs * num_heads, 1, num_queries, num_levels * num_points
    )
    output = (
        (torch.stack(sampling_value_list, dim=-2).flatten(-2) * attention_weights)
        .sum(-1)
        .view(bs, num_heads * embed_dims, num_queries)
    )
    return output.transpose(1, 2).contiguous()

1. 模块导入

  • 导入的模块
    • copy: 用于深拷贝模块,用于生成模型的多个副本。
    • math: 提供数学运算,如平方根和对数。
    • numpy: 提供数值计算功能,这里用于初始化操作。
    • torch, torch.nn, torch.nn.functional: PyTorch 的核心模块,用于构建神经网络层、初始化权重以及执行张量操作。

2. 工具函数分析

2.1 _get_clones(module, n)
  • 功能:创建一个包含 n 个相同模块副本的列表,通常用于需要共享结构但独立训练的网络层。
  • 分析:通过 copy.deepcopy 来深度复制模块,生成一系列的 nn.ModuleList,这一点在 Transformer 模型中的层复用中经常用到。
2.2 bias_init_with_prob(prior_prob=0.01)
  • 功能:根据给定的概率值初始化偏置(bias)。
  • 分析:通过对数运算计算偏置的初始值,常用于分类任务中的输出层,确保初始的预测概率为指定的值(如 1%)。
2.3 linear_init(module)
  • 功能:初始化线性层的权重和偏置。
  • 分析:使用均匀分布对权重和偏置进行初始化,初始化边界通过输入特征的大小进行计算。该方法可确保线性层在训练开始时的稳定性。
2.4 inverse_sigmoid(x, eps=1e-5)
  • 功能:计算逆 sigmoid 函数。
  • 分析:该函数用于将 sigmoid 激活的输出值(概率)逆转为对应的 logit 值,主要用于某些计算需要恢复到概率之前的 logit 空间。它对极小或极大值加上 eps 来避免数值不稳定性。

3. 核心功能:multi_scale_deformable_attn_pytorch

这是代码的核心部分,专注于实现 多尺度可变形注意力机制。它是一种在多个尺度下执行注意力操作的机制,常用于增强神经网络的空间感知能力,尤其在 DETR(检测器网络)等模型中。

函数参数
  • value:输入的特征张量,形状为 (batch_size, num_values, num_heads, embed_dims)
  • value_spatial_shapes:输入特征图的空间形状,即不同尺度的高宽信息。
  • sampling_locations:注意力机制的采样位置,表示在特征图中的不同位置采样的点。
  • attention_weights:用于控制注意力分配的权重,定义每个采样点的权重。
步骤解析
  1. 特征分割

    • 将输入的 value 根据空间形状 value_spatial_shapes 切分成多个尺度的特征图,每个尺度代表不同的分辨率。
    • 使用 value.splitvalue 按照每个尺度的特征图大小进行切分,得到 value_list
  2. 生成采样网格

    • 采样位置被转换为二维坐标,范围在 [-1, 1] 之间,表示可以在图像中进行插值采样。
    • 使用双线性插值(grid_sample)在每个尺度的特征图上按照这些坐标采样,得到每个采样位置的特征值。
  3. 加权求和

    • 对不同尺度的采样结果进行加权,权重由 attention_weights 提供。
    • 最终的输出是将不同尺度的特征加权求和,输出每个采样点的最终特征。
输出
  • 返回多尺度的采样特征,形状为 (batch_size, num_queries, num_heads * embed_dims)。这代表对每个查询在不同尺度上聚合后的特征,准备传递给下游任务(例如分类或检测)。

nn/tasks.py

# Ultralytics YOLO 🚀, AGPL-3.0 license

import contextlib
import pickle
import types
from copy import deepcopy
from pathlib import Path

import torch
import torch.nn as nn

# Other imports...

class BaseModel(nn.Module):
    """The BaseModel class serves as a base class for all the models in the Ultralytics YOLO family."""

    def forward(self, x, *args, **kwargs):
        """Handles both training and inference, returns predictions or loss."""
        if isinstance(x, dict):
            return self.loss(x, *args, **kwargs)  # Training: return loss
        return self.predict(x, *args, **kwargs)  # Inference: return predictions

    def predict(self, x, profile=False, visualize=False, augment=False, embed=None):
        """Run a forward pass through the network for inference."""
        if augment:
            return self._predict_augment(x)
        return self._predict_once(x, profile, visualize, embed)
    
    def fuse(self, verbose=True):
        """Fuses Conv and BatchNorm layers for efficiency during inference."""
        for m in self.model.modules():
            if isinstance(m, (Conv, Conv2, DWConv)) and hasattr(m, "bn"):
                m.conv = fuse_conv_and_bn(m.conv, m.bn)
                delattr(m, "bn")
                m.forward = m.forward_fuse  # Use the fused forward
        return self
    
    # More BaseModel methods...

class DetectionModel(BaseModel):
    """YOLOv8 detection model."""

    def __init__(self, cfg="yolov8n.yaml", ch=3, nc=None, verbose=True):
        """Initialize the YOLOv8 detection model with config and parameters."""
        super().__init__()
        self.yaml = cfg if isinstance(cfg, dict) else yaml_model_load(cfg)
        self.model, self.save = parse_model(deepcopy(self.yaml), ch=ch, verbose=verbose)
        self.names = {i: f"{i}" for i in range(self.yaml["nc"])}  # Class names
        self.inplace = self.yaml.get("inplace", True)

        # Initialize strides
        m = self.model[-1]  # Detect() layer
        if isinstance(m, Detect):
            s = 256  # Max stride
            m.stride = torch.tensor([s / x.shape[-2] for x in self._predict_once(torch.zeros(1, ch, s, s))])
            self.stride = m.stride
            m.bias_init()  # Initialize biases

    # More DetectionModel methods...

class SegmentationModel(DetectionModel):
    """YOLOv8 segmentation model."""

    def __init__(self, cfg="yolov8n-seg.yaml", ch=3, nc=None, verbose=True):
        """Initialize YOLOv8 segmentation model with given config and parameters."""
        super().__init__(cfg=cfg, ch=ch, nc=nc, verbose=verbose)

    def init_criterion(self):
        """Initialize the loss criterion for the SegmentationModel."""
        return v8SegmentationLoss(self)

class PoseModel(DetectionModel):
    """YOLOv8 pose model."""

    def __init__(self, cfg="yolov8n-pose.yaml", ch=3, nc=None, data_kpt_shape=(None, None), verbose=True):
        """Initialize YOLOv8 Pose model."""
        if not isinstance(cfg, dict):
            cfg = yaml_model_load(cfg)
        if list(data_kpt_shape) != list(cfg["kpt_shape"]):
            cfg["kpt_shape"] = data_kpt_shape
        super().__init__(cfg=cfg, ch=ch, nc=nc, verbose=verbose)

    def init_criterion(self):
        """Initialize the loss criterion for the PoseModel."""
        return v8PoseLoss(self)

class ClassificationModel(BaseModel):
    """YOLOv8 classification model."""

    def __init__(self, cfg="yolov8n-cls.yaml", ch=3, nc=None, verbose=True):
        """Initialize the YOLOv8 classification model."""
        super().__init__()
        self._from_yaml(cfg, ch, nc, verbose)

    def _from_yaml(self, cfg, ch, nc, verbose):
        """Set YOLOv8 model configurations and define the model architecture."""
        self.yaml = cfg if isinstance(cfg, dict) else yaml_model_load(cfg)
        self.model, self.save = parse_model(deepcopy(self.yaml), ch=ch, verbose=verbose)
        self.names = {i: f"{i}" for i in range(self.yaml["nc"])}
        self.info()

    def reshape_outputs(model, nc):
        """Update a classification model to match the class count (nc)."""
        name, m = list((model.model if hasattr(model, "model") else model).named_children())[-1]
        if isinstance(m, nn.Linear):
            if m.out_features != nc:
                setattr(model, name, nn.Linear(m.in_features, nc))

    def init_criterion(self):
        """Initialize the loss criterion for the ClassificationModel."""
        return v8ClassificationLoss()

class Ensemble(nn.ModuleList):
    """Ensemble of models."""

    def __init__(self):
        """Initialize an ensemble of models."""
        super().__init__()

    def forward(self, x, augment=False, profile=False, visualize=False):
        """Generate the ensemble’s final layer by combining outputs from each model."""
        y = [module(x, augment, profile, visualize)[0] for module in self]
        return torch.cat(y, 2), None  # Concatenate outputs along the third dimension

# Functions ------------------------------------------------------------------------------------------------------------

def parse_model(d, ch, verbose=True):
    """Parse a YOLO model.yaml dictionary into a PyTorch model."""
    import ast

    max_channels = float("inf")
    nc, act, scales = (d.get(x) for x in ("nc", "activation", "scales"))
    depth, width, kpt_shape = (d.get(x, 1.0) for x in ("depth_multiple", "width_multiple", "kpt_shape"))

    # Model scaling
    if scales:
        scale = d.get("scale")
        if not scale:networ
            scale = tuple(scales.keys())[0]
            LOGGER.warning(f"WARNING ⚠️ no model scale passed. Assuming scale='{scale}'.")
        depth, width, max_channels = scales[scale]

    if act:
        Conv.default_act = eval(act)  # redefine default activation
        if verbose:
            LOGGER.info(f"Activation: {act}")

    # Logging and parsing layers
    if verbose:
        LOGGER.info(f"\n{'':>3}{'from':>20}{'n':>3}{'params':>10}  {'module':<45}{'arguments':<30}")
    ch = [ch]
    layers, save, c2 = [], [], ch[-1]

    for i, (f, n, m, args) in enumerate(d["backbone"] + d["head"]):  # from, number, module, args
        m = globals()[m] if m in globals() else getattr(nn, m[3:], m)  # get module
        for j, a in enumerate(args):
            if isinstance(a, str):
                with contextlib.suppress(ValueError):
                    args[j] = ast.literal_eval(a) if a in locals() else a

        n = max(round(n * depth), 1) if n > 1 else n  # depth gain
        if m in {Conv, Bottleneck, C2f, C3k2, ...}:  # Module list
            c1, c2 = ch[f], args[0]
            c2 = make_divisible(min(c2, max_channels) * width, 8)
            args = [c1, c2, *args[1:]]
            if m in {C2f, C3k2, ...}:  # Repeated layers
                args.insert(2, n)
                n = 1
        elif m in {Concat, Detect, ...}:  # Head layers
            args.append([ch[x] for x in f])
        # Append layers
        m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args)
        layers.append(m_)

        ch.append(c2)
        save.extend([x % i for x in ([f] if isinstance(f, int) else f) if x != -1])

        if verbose:
            LOGGER.info(f"{i:>3}{str(f):>20}{n:>3}{sum(x.numel() for x in m_.parameters()):10.0f}  {str(m):<45}{str(args):<30}")

    return nn.Sequential(*layers), sorted(save)

def yaml_model_load(path):
    """Load a YOLO model from a YAML file."""
    path = Path(path)
    unified_path = path.with_name(path.stem.replace("yolov8", "yolov"))
    yaml_file = check_yaml(str(unified_path), hard=False) or check_yaml(path)
    d = yaml_load(yaml_file)
    d["scale"] = guess_model_scale(path)
    d["yaml_file"] = str(path)
    return d

# More utility functions...

def guess_model_scale(model_path):
    """Extract the scale from the YAML file."""
    import re
    return re.search(r"yolov\d+([nslmx])", Path(model_path).stem).group(1)

def attempt_load_weights(weights, device=None, inplace=True, fuse=False):
    """Loads weights for a model or an ensemble of models."""
    ensemble = Ensemble()
    for w in weights if isinstance(weights, list) else [weights]:
        ckpt, _ = torch_safe_load(w)
        model = (ckpt.get("ema") or ckpt["model"]).to(device).float()
        model = model.fuse().eval() if fuse and hasattr(model, "fuse") else model.eval()
        ensemble.append(model)

    return ensemble if len(ensemble) > 1 else ensemble[-1]

tasks.py 是代码流水线中的核心部分;它仍然使用 YOLOv8 的方法和逻辑,只需将 YOLO11 模型解析到其中即可。此脚本设计用于多种计算机视觉任务,如目标检测、分割、分类、姿态估计、OBB(定向边界框)等。它定义了基础模型、任务特定模型以及用于训练、推理和模型管理的工具函数。

关键组件:

导入模块:脚本导入了重要的模块,如 PyTorch(torch)、神经网络层(torch.nn)以及来自 Ultralytics 的实用函数。主要的导入包括:

  • 模型架构模块,如 C3k2、C2PSA、C3、SPPF、Concat 等。
  • 损失函数,如 v8DetectionLoss、v8SegmentationLoss、v8ClassificationLoss、v8OBBLoss。
  • 各种工具函数,如 model_infofuse_conv_and_bnscale_imgtime_sync,用于辅助模型处理、性能分析和评估。

模型基类:

BaseModel 类
  • BaseModel 是 Ultralytics YOLO 系列中所有模型的基类。
  • 实现了基础方法,如:
    • forward():根据输入数据处理训练或推理。
    • predict():执行推理过程。
    • fuse():融合 Conv2d 和 BatchNorm2d 层以提高效率。
    • info():提供详细的模型信息。
  • 此类旨在被任务特定的模型继承,例如检测、分割和分类模型。
DetectionModel 类
  • 扩展自 BaseModel,专门用于目标检测任务。
  • 加载模型配置,初始化检测头(如 Detect 模块),并设置模型步幅(strides)。
  • 支持使用 YOLOv8 架构的检测任务,并通过 _predict_augment() 进行增强推理。

任务特定模型:

SegmentationModel
  • DetectionModel 的子类,专用于分割任务(如 YOLOv8 分割)。
  • 初始化分割特定的损失函数(v8SegmentationLoss)。
PoseModel
  • 处理姿态估计任务,通过特定的配置初始化模型,用于关键点检测(kpt_shape)。
  • 使用 v8PoseLoss 计算姿态特定的损失。
ClassificationModel
  • 用于图像分类任务,基于 YOLOv8 分类架构。
  • 初始化并管理分类特定的损失(v8ClassificationLoss)。
  • 还支持重塑预训练的 TorchVision 模型以用于分类任务。
OBBModel
  • 用于定向边界框(OBB)检测任务。
  • 实现了处理旋转边界框的特定损失函数(v8OBBLoss)。
WorldModel
  • 处理图像描述生成和基于文本的识别任务。
  • 利用来自 CLIP 模型的文本特征进行基于文本的视觉识别任务。
  • 包含对文本嵌入(txt_feats)的特殊处理,用于图像描述和与世界相关的任务。
Ensemble Model

Ensemble

  • 一个简单的集成类,将多个模型组合为一个。
  • 允许对不同模型的输出进行平均或拼接,以提高整体性能。
  • 在需要结合多个模型输出来提供更好预测的任务中很有用。

实用函数:

模型加载与管理
  • attempt_load_weights()attempt_load_one_weight():用于加载模型、管理集成模型,以及在加载预训练权重时处理兼容性问题。
  • 这些函数确保模型以正确的步幅、层和配置加载。
临时模块重定向
  • temporary_modules():一个上下文管理器,用于临时重定向模块路径,确保当模块位置发生变化时的向后兼容性。
  • 在维护旧版本模型兼容性时非常有用。
Pickle 安全处理
  • SafeUnpickler:一个自定义的 Unpickler,用于安全加载模型检查点,确保将未知类替换为安全占位符(SafeClass),以避免加载时崩溃。
模型解析
  • parse_model()
    • 此函数将 YAML 文件中的模型配置解析为 PyTorch 模型。
    • 解析骨干网络和头部架构,解释每种层类型(如 Conv、SPPF、Detect),并构建最终模型。
    • 支持包括 YOLO11 组件(如 C3k2、C2PSA 等)在内的各种架构。
YAML 模型加载
  • yaml_model_load():从 YAML 文件中加载模型配置,检测模型尺度(如 n、s、m、l、x)并相应地调整参数。
  • guess_model_scale()guess_model_task():帮助函数,用于根据 YAML 文件结构推断模型的尺度和任务。

YOLO11 的亮点

  1. 增强的特征提取:在复杂环境下更好地进行检测
    YOLO11 的设计使其能够捕捉图像中的复杂模式,从而在光线不佳或场景杂乱等复杂环境中更好地识别物体。

  2. 更高的 mAP,参数更少
    YOLO11 在检测物体时的平均精度(mAP)更高,同时使用的参数比 YOLOv8 少 22%。简单来说,它在保持精度的同时更快、更高效。

  3. 更快的处理速度
    YOLO11 比 YOLOv10 的处理速度快 2%,这使其非常适合实时应用,如自动驾驶、机器人技术或视频实时分析。

  4. 资源高效:以更少的资源完成更多工作
    即便应对更复杂的任务,YOLO11 仍然设计得更加节省计算资源,适用于大规模项目和计算能力有限的系统。

  5. 改进的训练流程
    YOLO11 的训练流程更加简化,能够更有效地适应各种任务。无论是小数据集还是大规模项目,YOLO11 都能根据问题的规模进行调整。

  6. 跨部署的灵活性
    YOLO11 被设计为在云服务器和边缘设备(如智能手机或物联网设备)上都能高效运行。这种灵活性使其适用于需要在不同环境中运行的应用。

  7. 多样化应用的通用性
    无论是自动驾驶、医疗影像、智能零售还是工业自动化,YOLO11 都能适用于各种领域,是解决计算机视觉挑战的首选方案。

;