参考内容

​​​​​​【YOLO】深入理解 CSP 瓶颈模块的变种:Bottleneck、C3、C3k、C2f 和 C3k2_csp模块-CSDN博客

YOLOv12论文模型解析 | 手把手带你零基础看懂yolov12的网络结构(附手绘超详细网络结构图)-CSDN博客

YOLOv8 - Ultralytics YOLO Docs

YOLO12论文: https://arxiv.org/pdf/2502.12524

YOLOv8 Github:https://github.com/open-mmlab/mmyolo/tree/main/configs/yolov8 

YOLOv8

YOLOv8-P5 model structure

其中 C2f 对应的就是这一部分:

DarknetBottleneck 等结构如下图所示:

1.1 BottleNeck

包含两个卷积层,用于减少计算量和提取特征。可以选择是否使用 shortcut 连接,用于增强梯度传播。

c1:输入特征图通道数(输入张量纬度)

c2:输出特征图通道数(目标纬度)

shortcut:残差连接

g:group,分组卷积的组数

k:两个卷积层核尺寸,k[0]第一层,k[1]第二层

e:通道扩展系数,控制中间层通道数

class Bottleneck(nn.Module):
    """Standard bottleneck."""

    def __init__(self, c1, c2, shortcut=True, g=1, k=(3, 3), e=0.5):
        """Initializes a standard bottleneck module with optional shortcut connection and configurable parameters."""
        super().__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, k[0], 1)
        self.cv2 = Conv(c_, c2, k[1], 1, g=g)
        self.add = shortcut and c1 == c2

    def forward(self, x):
        """Applies the YOLO FPN to input data."""
        return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
class Conv(nn.Module):
    """Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation)."""

    default_act = nn.SiLU()  # default activation

    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
        """Initialize Conv layer with given arguments including activation."""
        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):
        """Apply convolution, batch normalization and activation to input tensor."""
        return self.act(self.bn(self.conv(x)))

    def forward_fuse(self, x):
        """Apply convolution and activation without batch normalization."""
        return self.act(self.conv(x))

1.2 C3

特征被分为两个路径,一个是通过瓶颈来获取复杂特征, 另一个路径直接传递输入特征,最后拼接。

class C3(nn.Module):
    """CSP Bottleneck with 3 convolutions."""

    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
        """Initialize the CSP Bottleneck with given channels, number, shortcut, groups, and expansion values."""
        super().__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c1, c_, 1, 1)
        self.cv3 = Conv(2 * c_, c2, 1)  # optional act=FReLU(c2)
        self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, k=((1, 1), (3, 3)), e=1.0) for _ in range(n)))

    def forward(self, x):
        """Forward pass through the CSP bottleneck with 2 convolutions."""
        return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), 1))

1.3 C3k

在 C3 的基础上,自定义卷积核大小。

class C3k(C3):
    """C3k is a CSP bottleneck module with customizable kernel sizes for feature extraction in neural networks."""

    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5, k=3):
        """Initializes the C3k module with specified channels, number of layers, and configurations."""
        super().__init__(c1, c2, n, shortcut, g, e)
        c_ = int(c2 * e)  # hidden channels
        # self.m = nn.Sequential(*(RepBottleneck(c_, c_, shortcut, g, k=(k, k), e=1.0) for _ in range(n)))
        self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, k=(k, k), e=1.0) for _ in range(n)))

1.4 C2f

class C2f(nn.Module):
    """Faster Implementation of CSP Bottleneck with 2 convolutions."""

    def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5):
        """Initialize CSP bottleneck layer with two convolutions with arguments ch_in, ch_out, number, shortcut, groups,
        expansion.
        """
        super().__init__()
        self.c = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, 2 * self.c, 1, 1)
        self.cv2 = Conv((2 + n) * self.c, c2, 1)  # optional act=FReLU(c2)
        self.m = nn.ModuleList(Bottleneck(self.c, self.c, shortcut, g, k=((3, 3), (3, 3)), e=1.0) for _ in range(n))

    def forward(self, x):
        """Forward pass through C2f layer."""
        y = list(self.cv1(x).chunk(2, 1))
        y.extend(m(y[-1]) for m in self.m)
        return self.cv2(torch.cat(y, 1))

    def forward_split(self, x):
        """Forward pass using split() instead of chunk()."""
        y = list(self.cv1(x).split((self.c, self.c), 1))
        y.extend(m(y[-1]) for m in self.m)
        return self.cv2(torch.cat(y, 1))

YOLO11

YOLOv8 和 YOLO11 主要修改点:

  1. 修改了模型深度和宽度

  2. 修改了 backbone 的内部结构(C2f 更换为 C3k2)

  3. YOLO11 在 SPPF 后增加了一层 C2PSA

  4. Head 检测头内部 cv3,分类头变为并行的 DWConv 处理

可以看出,两者之间的区别是:

1. YOLO11 用 C3k2 来代替 YOLOv8 的 C2f 模块

2. 在 SPPF 后又添加了一层 C2PSA

2.1 C3k2

c3k = True 时将使用 c3k 层,允许使用不同大小的卷积核。否则使用和 cf2 类似的瓶颈层。

class C3k2(C2f):
    """Faster Implementation of CSP Bottleneck with 2 convolutions."""

    def __init__(self, c1, c2, n=1, c3k=False, e=0.5, g=1, shortcut=True):
        """Initializes the C3k2 module, a faster CSP Bottleneck with 2 convolutions and optional C3k blocks."""
        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)
        )


class C3k(C3):
    """C3k is a CSP bottleneck module with customizable kernel sizes for feature extraction in neural networks."""

    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5, k=3):
        """Initializes the C3k module with specified channels, number of layers, and configurations."""
        super().__init__(c1, c2, n, shortcut, g, e)
        c_ = int(c2 * e)  # hidden channels
        # self.m = nn.Sequential(*(RepBottleneck(c_, c_, shortcut, g, k=(k, k), e=1.0) for _ in range(n)))
        self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, k=(k, k), e=1.0) for _ in range(n)))

2.2 SPPF

SPPF 可以理解为:SPP-Fast。SPP 的主要目的是解决卷积神经网络在处理不同输入尺寸图像时,如何生成固定大小的输出特征图的问题。

通过多个不同大小的池化核(比如5x5, 9x9, 13x13)进行最大池化,然后将结果拼接。通过这种方式,可以在不同尺度上捕获特征。

SPP结构: 并行多尺度池化

输入 ->并行池化(5x5, 9x9, 13x13) -> 特征拼接 -> 输出

SPPF结构: 串行重复池化

输入 -> 5x5池化 -> 5x5池化 -> 5x5池化 -> 特征拼接 -> 输出

class SPP(nn.Module):
    """Spatial Pyramid Pooling (SPP) layer https://arxiv.org/abs/1406.4729."""

    def __init__(self, c1, c2, k=(5, 9, 13)):
        """Initialize the SPP layer with input/output channels and pooling kernel sizes."""
        super().__init__()
        c_ = c1 // 2  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c_ * (len(k) + 1), c2, 1, 1)
        self.m = nn.ModuleList([nn.MaxPool2d(kernel_size=x, stride=1, padding=x // 2) for x in k])

    def forward(self, x):
        """Forward pass of the SPP layer, performing spatial pyramid pooling."""
        x = self.cv1(x)
        return self.cv2(torch.cat([x] + [m(x) for m in self.m], 1))
class SPPF(nn.Module):
    """Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher."""

    def __init__(self, c1, c2, k=5):
        """
        Initializes the SPPF layer with given input/output channels and kernel size.

        This module is equivalent to SPP(k=(5, 9, 13)).
        """
        super().__init__()
        c_ = c1 // 2  # hidden channels
        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):
        """Forward pass through Ghost Convolution block."""
        y = [self.cv1(x)]
        y.extend(self.m(y[-1]) for _ in range(3))
        return self.cv2(torch.cat(y, 1))

2.3 C2PSA

C2PSA 是在 C2(C2f 的前身)内部嵌入一个注意力机制(PSA)

 

class C2PSA(nn.Module):

    def __init__(self, c1, c2, n=1, e=0.5):
        """Initializes the C2PSA module with specified input/output channels, number of layers, and expansion ratio."""
        super().__init__()
        assert c1 == c2
        self.c = int(c1 * e)
        self.cv1 = Conv(c1, 2 * self.c, 1, 1)
        self.cv2 = Conv(2 * self.c, c1, 1)

        self.m = nn.Sequential(*(PSABlock(self.c, attn_ratio=0.5, num_heads=self.c // 64) for _ in range(n)))

    def forward(self, x):
        """Processes the input tensor 'x' through a series of PSA blocks and returns the transformed tensor."""
        a, b = self.cv1(x).split((self.c, self.c), dim=1)
        b = self.m(b)
        return self.cv2(torch.cat((a, b), 1))

YOLO12

论文:[2502.12524] YOLOv12: Attention-Centric Real-Time Object Detectors

代码:YOLOv12论文模型解析 | 手把手带你零基础看懂yolov12的网络结构(附手绘超详细网络结构图)-CSDN博客

在论文中主要有三项关键改进点:

  1. 提出区域注意力模块(A2, Area Attention):在保持大接收域的同时,降低了注意力计算的复杂性,从而提高了速度。

  2. 引入了残差高效层聚合网络(R-ELAN):基于原始的 ELAN 进行了两处改进:

    1. 采用块级残差设计和缩放技术

    2. 重新设计特征聚合方法

  3. 架构优化:

    1. 引入 FlashAttention 以解决注意力机制的内存访问问题

    2. 移除位置编码等设计,使模型更快且更简洁

    3. 将多层感知器(MLP)比例从 4 调整为 1.2,以平衡注意力和前馈网络之间的计算,提升性能

    4. 减少堆叠块的深度,以便于优化

    5. 尽可能多地使用卷积运算符,增加计算效率

3.1 注意力机制

人在观察事物时会有选择性的关注较为重要的信息,称其为注意力。通过持续关注这一关键位置以获得更多的信息,而忽略其他的无用信息,这种视觉注意力机制大大提高了我们处理信息的效率和准确性。深度学习中的注意力机制和人类视觉的注意力机制类似,就是在更多信息中把注意力集中放在重要的点上,选出关键信息,而忽略其他不重要的信息。

注意力机制的灵感来源可以归结到人对环境的生理感知,当人类看东西时,一般会将注意力注视着某个地方,而不会关注全部所有信息。比如,视觉系统更倾向于挑选影像中的部分信息进行集中分析,忽略图像中无关的信息。这样使人类能够利用有限的注意力资源从大量信息中快速获取高价值的信息,极大地提升了大脑处理信息的效率。

注意力机制(Attention Mechanism)是机器学习中的一种数据处理方法,广泛应用在自然语言处理、图像识别以及语音识别等各种不同类型的机器学习任务中。注意力机制对不同信息的关注程度(重要程度)由权值来体现,注意力机制可以视为查询矩阵(Query)、键(key)以及加权平均值构成了多层感知机(Multilayer Perceptron, MLP)。

注意力的思想,类似于寻址。给定Target中的某个元素Query,通过计算Query和各个Key的相似性或相关性,得到每个Key对应Value的权重系数,然后对Value进行加权求和,即得到最终的Attention数值。所以,本质上Attention机制是Source中元素的Value值进行加权求和,而Query和Key用来计算对应Value的权重系数。

计算公式如下,其中 Lx=||Source||

主要计算过程:1.计算 Value 权重系数;2.对得到的权重进行归一化处理;3.求和

注意力机制从计算本质上讲要比卷积神经网络 CNN 要慢。

1.对于一个长度为 𝐿 且特征维度为 𝑑 的输入序列,注意力矩阵的计算需要 𝑂(𝐿^2 𝑑) 次操作,因为每个标记都会关注其他所有标记。卷积操作在 CNN 中的复杂性与空间或时间维度呈线性关系,即 𝑂(𝑘𝐿𝑑),其中 𝑘 是卷积核的大小,通常远小于 𝐿。尤其对于高分辨率图像或长序列大型输入图像,注意力机制非常慢。

2.注意力的视觉变换器设计复杂,逐渐累积,导致其整体速度相比于 CNN 架构更慢。

3.内存访问模式。注意力内存访问模式相比于 CNN 的效率要低。具体来说,在注意力计算过程中,注意力图(QK^T)和 softmax 图(L × L)这样的中间图需要从高速 GPU SRAM(计算的实际位置)存储到高带宽 GPU 内存(HBM),并在计算过程中再进行检索,而前者的读写速度超过后者的 10 倍以上,这导致了显著的内存访问开销和增加的实际时间。此外,注意力中的不规则内存访问模式相比于 CNN 引入了更高的延迟,后者利用结构化和局部化的内存访问。CNN 受益于空间约束的卷积核,使得内存缓存高效,并因其固定的接收域和滑动窗口操作而降低延迟。这两个因素,即二次计算复杂性和低效的内存访问,使得注意力机制在实时或资源受限的场景中比 CNN 更慢。解决这些限制已成为一个关键的研究领域,采用稀疏注意力机制和内存高效近似(例如,Linformer 或 Performer )的方法旨在缓解二次扩展的问题。

3.2 区域注意力(2A)

传统的全局注意力机制需要计算每个位置之间的相关性,导致计算量随着输入尺寸的增加而急剧上升。Area Attention 通过将特征图划分为多个区域,减少了注意力机制的计算复杂度。

这种划分方式避免了复杂的窗口划分和反转操作,简化了计算过程。每个区域的计算复杂度显著降低,从而提高了模型的推理速度。


class AAttn(nn.Module):
    """
    Area-attention module for YOLO models, providing efficient attention mechanisms.

    This module implements an area-based attention mechanism that processes input features in a spatially-aware manner,
    making it particularly effective for object detection tasks.

    Attributes:
        area (int): Number of areas the feature map is divided.
        num_heads (int): Number of heads into which the attention mechanism is divided.
        head_dim (int): Dimension of each attention head.
        qkv (Conv): Convolution layer for computing query, key and value tensors.
        proj (Conv): Projection convolution layer.
        pe (Conv): Position encoding convolution layer.

    Methods:
        forward: Applies area-attention to input tensor.

    Examples:
        >>> attn = AAttn(dim=256, num_heads=8, area=4)
        >>> x = torch.randn(1, 256, 32, 32)
        >>> output = attn(x)
        >>> print(output.shape)
        torch.Size([1, 256, 32, 32])
    """

    def __init__(self, dim, num_heads, area=1):
        """
        Initializes an Area-attention module for YOLO models.

        Args:
            dim (int): Number of hidden channels.
            num_heads (int): Number of heads into which the attention mechanism is divided.
            area (int): Number of areas the feature map is divided, default is 1.
        """
        super().__init__()
        self.area = area

        self.num_heads = num_heads
        self.head_dim = head_dim = dim // num_heads
        all_head_dim = head_dim * self.num_heads

        self.qkv = Conv(dim, all_head_dim * 3, 1, act=False)
        self.proj = Conv(all_head_dim, dim, 1, act=False)
        self.pe = Conv(all_head_dim, dim, 7, 1, 3, g=dim, act=False)

    def forward(self, x):
        """Processes the input tensor 'x' through the area-attention."""
        B, C, H, W = x.shape
        N = H * W

        qkv = self.qkv(x).flatten(2).transpose(1, 2)
        if self.area > 1:
            qkv = qkv.reshape(B * self.area, N // self.area, C * 3)
            B, N, _ = qkv.shape
        q, k, v = (
            qkv.view(B, N, self.num_heads, self.head_dim * 3)
            .permute(0, 2, 3, 1)
            .split([self.head_dim, self.head_dim, self.head_dim], dim=2)
        )
        attn = (q.transpose(-2, -1) @ k) * (self.head_dim**-0.5)
        attn = attn.softmax(dim=-1)
        x = v @ attn.transpose(-2, -1)
        x = x.permute(0, 3, 1, 2)
        v = v.permute(0, 3, 1, 2)

        if self.area > 1:
            x = x.reshape(B // self.area, N * self.area, C)
            v = v.reshape(B // self.area, N * self.area, C)
            B, N, _ = x.shape

        x = x.reshape(B, H, W, C).permute(0, 3, 1, 2).contiguous()
        v = v.reshape(B, H, W, C).permute(0, 3, 1, 2).contiguous()

        x = x + self.pe(v)
        return self.proj(x)

3.3 R-ELAN

为了解决上述问题,YOLO12 提出残差高效层聚合网络(R-ELAN, Residual Efficient Layer Aggregation Networks)

通过在输入和输出之间添加直接的梯度路径,增强了模型的优化能力。

3.4 与 YOLO11 架构对比

可以通过 yaml 文件对比发现,YOLO12 用 A2C2f 替换 C3k2,同时 YOLO11 中的 SPPF 和 C2PSA 被移除。

 

3.5 与其它模型对比

 

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐