YOLO26改进 – 卷积Conv SPConv:基于分割的卷积巧解特征冗余,实现高效特征提取

 # 前言

本文介绍了一种基于分割的卷积操作SPConv及其在YOLO26中的结合。传统卷积方法忽视了特征图中的模式冗余,SPConv将输入特征图分割为代表性部分和不确定冗余部分,分别采用 $k \times k$ 卷积和 $1 \times 1$ 卷积处理,并引入组卷积减少代表性部分冗余,还设计了无参数特征融合模块。SPConv可直接替代原始卷积。我们将其集成进YOLO26,实验表明配备SPConv的网络在准确性、推理时间上优于基准模型,同时减少了浮点运算和参数量。

文章目录: YOLO26改进大全:卷积层、轻量化、注意力机制、损失函数、Backbone、SPPF、Neck、检测头全方位优化汇总

专栏链接: YOLO26改进专栏

介绍

image-20240704151756307

摘要

许多有效的解决方案已被提出以减少推理加速中模型的冗余。然而,常见的方法大多集中在消除不重要的滤波器或构建高效的操作上,而忽视了特征图中的模式冗余。我们揭示了在一个层内,许多特征图分享相似但不完全相同的模式。然而,确定具有类似模式的特征是否冗余或包含重要细节是困难的。因此,我们提出了一种基于分割的卷积操作,即SPConv,来容忍具有相似模式但需要较少计算的特征。具体来说,我们将输入的特征图分割成代表性部分和不确定冗余部分,从代表性部分中通过相对复杂的计算提取内在信息,而在不确定冗余部分中处理微小的隐藏细节则采用轻量级操作。为了重新校准和融合这两组处理过的特征,我们提出了一个无需参数的特征融合模块。此外,我们的SPConv设计成可以直接替代原始卷积,在使用中非常便捷。实验结果表明,基准测试中配备SPConv的网络在GPU上在准确性和推理时间上始终优于最先进的基准模型,同时大幅减少了浮点运算和参数量。

文章链接

论文地址:论文地址

代码地址:代码地址

基本原理

在现有的滤波器中,如常规卷积、GhostConv、OctConv和HetConv,通常在所有输入通道上执行 ( k \times k ) 卷积操作。然而,这些方法未能解决同一层特征中存在的冗余问题,即使不存在完全相同的两个通道特征,也难以直接剔除冗余。

受此现象启发,研究者提出了一种新的方法,将所有输入特征按比例分为两部分:

  1. 代表性部分:执行 $k \times k$ 卷积以提取重要信息;
  2. 不确定部分:执行 $1 \times 1$ 卷积以补充隐含细节信息。

这种过程可以用以下公式描述:

$$ \text{SPConv} = \text{Representative部分} \oplus \text{Uncertain部分} $$

代表性部分的进一步减少

在将所有输入通道分成两个主要部分后,代表性部分之间可能存在冗余。为了进一步减少这种冗余,我们引入了组卷积的概念。组卷积可以视作具有稀疏块对角卷积核的普通卷积,其中每个块对应一个通道,并且分组之间没有连接。这一步骤旨在消除代表性部分内部的冗余,尽管可能会牺牲一些跨通道连接的有用信息。为了弥补信息损失,我们在所有代表性通道上应用逐点卷积,将组卷积和逐点卷积的结果通过直接求和融合,从而获得更丰富的特征表示。

image-20240704152340065

3.2 无参数特征融合模块

为了克服分组信息损失带来的影响,我们设计了一个无参数特征融合模块,用于控制不同输入通道特征的有效融合。这一模块与传统的直接求和融合不同,不需要额外的参数,却能提升模型的性能表现。这种设计使得我们的SPConv在性能和效率上都有显著的提升。

image-20240704152419869

通过这些优化,SPConv不仅解决了传统方法中存在的特征冗余和信息损失问题,还有效地提升了模型的整体性能和学习能力。

这些创新的特征提取和融合方法,使得SPConv在图像处理和计算机视觉任务中具有广泛的应用前景。

image-20240704152201788

核心代码

class SPConv_3x3(nn.Module):
    def __init__(self, inplanes, outplanes, stride=1, ratio=0.5):
        super(SPConv_3x3, self).__init__()

        # 计算3x3卷积和1x1卷积的输入输出通道数
        self.inplanes_3x3 = int(inplanes * ratio)
        self.inplanes_1x1 = inplanes - self.inplanes_3x3
        self.outplanes_3x3 = int(outplanes * ratio)
        self.outplanes_1x1 = outplanes - self.outplanes_3x3
        self.outplanes = outplanes
        self.stride = stride

        # 定义3x3组卷积和1x1卷积层
        self.gwc = nn.Conv2d(self.inplanes_3x3, self.outplanes, kernel_size=3, stride=self.stride,
                             padding=1, groups=2, bias=False)
        self.pwc = nn.Conv2d(self.inplanes_3x3, self.outplanes, kernel_size=1, bias=False)

        # 定义1x1卷积层
        self.conv1x1 = nn.Conv2d(self.inplanes_1x1, self.outplanes, kernel_size=1)

        # 定义平均池化层
        self.avgpool_s2_1 = nn.AvgPool2d(kernel_size=2, stride=2)
        self.avgpool_s2_3 = nn.AvgPool2d(kernel_size=2, stride=2)
        self.avgpool_add_1 = nn.AdaptiveAvgPool2d(1)
        self.avgpool_add_3 = nn.AdaptiveAvgPool2d(1)

        # 定义批归一化层
        self.bn1 = nn.BatchNorm2d(self.outplanes)
        self.bn2 = nn.BatchNorm2d(self.outplanes)

        self.ratio = ratio
        self.groups = int(1 / self.ratio)

    def forward(self, x):
        b, c, _, _ = x.size()

        # 分割输入特征为3x3卷积部分和1x1卷积部分
        x_3x3 = x[:, :int(c * self.ratio), :, :]
        x_1x1 = x[:, int(c * self.ratio):, :, :]

        # 3x3卷积部分的前向传播
        out_3x3_gwc = self.gwc(x_3x3)
        if self.stride == 2:
            x_3x3 = self.avgpool_s2_3(x_3x3)
        out_3x3_pwc = self.pwc(x_3x3)
        out_3x3 = out_3x3_gwc + out_3x3_pwc
        out_3x3 = self.bn1(out_3x3)
        out_3x3_ratio = self.avgpool_add_3(out_3x3).squeeze(dim=3).squeeze(dim=2)

        # 1x1卷积部分的前向传播
        if self.stride == 2:
            x_1x1 = self.avgpool_s2_1(x_1x1)
        out_1x1 = self.conv1x1(x_1x1)
        out_1x1 = self.bn2(out_1x1)
        out_1x1_ratio = self.avgpool_add_1(out_1x1).squeeze(dim=3).squeeze(dim=2)

        # 将3x3和1x1卷积部分的输出按通道进行堆叠,并应用Softmax进行权重融合
        out_31_ratio = torch.stack((out_3x3_ratio, out_1x1_ratio), 2)
        out_31_ratio = nn.Softmax(dim=2)(out_31_ratio)

        # 按融合的权重计算最终的输出
        out = out_1x1 * (out_31_ratio[:, :, 1].view(b, self.outplanes, 1, 1).expand_as(out_1x1)) \
              + out_3x3 * (out_31_ratio[:, :, 0].view(b, self.outplanes, 1, 1).expand_as(out_3x3))

        return out

实验

脚本

import warnings
warnings.filterwarnings('ignore')
from ultralytics import YOLO

if __name__ == '__main__':
#     修改为自己的配置文件地址
    model = YOLO('./ultralytics/cfg/models/26/yolo26-SPConv.yaml')
#     修改为自己的数据集地址
    model.train(data='./ultralytics/cfg/datasets/coco8.yaml',
                cache=False,
                imgsz=640,
                epochs=10,
                single_cls=False,  # 是否是单类别检测
                batch=8,
                close_mosaic=10,
                workers=0,
                optimizer='MuSGD',  
                # optimizer='SGD',
                amp=False,
                project='runs/train',
                name='yolo26-SPConv',
                )

结果

image-20260125173023920

THE END