YOLO26改进 – 卷积Conv GCNet之金箍棒块GCBlock : 重参数化捕获全局依赖 CVPR 2025

# 前言

本文介绍了GCBlock在YOLO26中的结合应用。GCBlock是GCNet的核心组件,能高效捕获特征图中的全局依赖关系,融合了非局部网络和挤压 - 激励网络的优势,降低计算成本并保障模型性能。其遵循“上下文建模 - 特征变换 - 特征融合”框架,通过全局平均池化、全连接层等操作生成注意力权重。我们将GCBlock集成进YOLO26,替代原有的部分卷积模块,实现更高效的全局上下文建模。实验证明,YOLO26-GCBlock在目标检测任务中表现出色,展现了GCBlock在深度学习中的广泛应用前景。

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

专栏链接: YOLO26改进专栏

介绍

image-20251119230707964

摘要

近年来的实时语义分割模型,无论是单分支还是多分支结构,都在性能和速度上取得了不错的表现。然而,这些模型的速度常常受到多路径模块的限制,有些还依赖于高性能的教师模型进行训练。为了解决这些问题,我们提出了 金箍棒网络(GCNet)

具体来说,GCNet 在训练阶段结合了 纵向多卷积横向多路径结构,在推理阶段则将这些结构重新参数化为一个单一的卷积操作,从而同时优化性能与速度。这样的设计使得 GCNet 能够在训练时“自我膨胀”,而在推理时“自我收缩”,相当于无需外部教师模型就具备了“教师模型”的能力。

实验结果表明,GCNet 在 Cityscapes、CamVid 和 Pascal VOC 2012 数据集上,在性能和推理速度方面都优于现有的先进模型。

项目代码已开源,地址为:https://github.com/gyyang23/GCNet

文章链接

论文地址:论文地址

代码地址:代码地址

基本原理

金箍棒块(GCBlock)详细介绍

GCBlock(Golden Cudgel Block,金箍棒块)是GCNet(金箍棒网络)的核心组件,核心设计理念是“训练时扩张增强学习能力,推理时收缩提升运行速度”,既兼顾了多路径模块的训练优势,又保留了单路径模块的推理效率,完美解决了实时语义分割中“性能与速度难以平衡”的关键问题。

一、设计初衷

之前的实时语义分割模型存在两大痛点:

  1. 多路径模块(如ResBlock、Conv-Former Block)虽能提升训练效果,但结构复杂、内存访问频繁,严重拖慢推理速度;
  2. 单路径模块虽快,但学习能力有限,难以捕捉丰富的特征信息。

GCBlock的目标就是“鱼与熊掌兼得”:通过重参数化技术,让模块在训练时变成“多卷积+多路径”的复杂结构,在推理时自动压缩成单个简单卷积,全程不损失性能。

二、核心结构与工作原理

GCBlock的核心是“训练时的扩张结构”和“推理时的重参数化收缩”,具体分为两个阶段:

(一)训练阶段:多卷积+多路径的“扩张形态”

训练时,GCBlock采用“纵向多卷积+横向多路径”的设计,本质是通过多个并行路径增强特征学习能力,结构包含4类关键路径(基于瓶颈结构优化,移除了冗余的1×1卷积,保留核心计算):

路径类型 结构组成 核心作用
Path₃×3·1×1 1个3×3卷积 + 1个1×1卷积 捕捉局部空间特征与通道维度信息,是核心特征学习路径
Path₁×1·1×1 2个串联的1×1卷积 补充通道交互信息,增强模型对细节的敏感度(实验证明2个1×1卷积效果最优)
Path residual(残差路径) 等效为1个1×1卷积(权重按需设为1或0) 避免梯度消失,让浅层特征直接传递到深层
主路径 1个3×3卷积 + 1个1×1卷积 保证基础特征提取能力,与其他路径互补

注:Path₃×3·1×1的数量(N)可调整,轻量版(GCNet-S)设为4,中/重量级(GCNet-M/L)设为2,过多会导致训练成本飙升且性能饱和。

这些路径在训练时并行计算,各自学习不同维度的特征(空间细节、语义信息、通道交互等),最后汇总融合,相当于“多人协作完成复杂任务”,学习能力拉满。

(二)推理阶段:重参数化的“收缩形态”

训练完成后,GCBlock会通过数学等价转换,将所有并行路径“压缩”成一个单一的3×3卷积,全程无性能损失,推理时相当于“一个人高效完成任务”,速度大幅提升。

重参数化的核心是“卷积与BN融合”“多路径卷积合并”,具体分3步:

1. 第一步:卷积(Conv)与批归一化(BN)融合

训练时为了稳定训练,每个卷积后会加BN层,但推理时BN会增加计算开销。通过公式转换,可将BN的参数(均值、方差、缩放因子、偏置)融入卷积的权重(W)和偏置(B)中,得到新的卷积参数:

  • 新权重 W' = (γ / √(σ² + ε)) × W
  • 新偏置 B' = (γ×(B - μ) / √(σ² + ε)) + β (其中γ、β是BN的缩放因子和偏置,μ、σ是BN的均值和方差,ε是防止除零的常数,默认1e-5)

融合后,Conv+BN模块就变成了一个“等效卷积”,减少了推理时的计算步骤。

2. 第二步:单路径内的卷积合并

对于包含多个串联卷积的路径(如Path₃×3·1×1、Path₁×1·1×1),通过矩阵乘法等价转换,将多个卷积合并成一个3×3卷积:

  • 例:Path₃×3·1×1(3×3卷积 + 1×1卷积):将1×1卷积的权重与3×3卷积的权重矩阵相乘,偏置也相应累加,最终得到一个等效的3×3卷积;
  • 例:Path₁×1·1×1(2个1×1卷积):将1×1卷积视为“中心权重非零、周围为零”的特殊3×3卷积,再通过矩阵乘法合并,最终也转化为3×3卷积。

3. 第三步:多路径的卷积求和

所有路径都转化为3×3卷积后,由于它们的输入输出通道、卷积核尺寸完全一致,可直接将所有路径的权重(W)相加、偏置(B)相加,最终得到一个“融合了所有路径特征的单一3×3卷积”。

整个重参数化过程是数学等价的,意味着推理时的单一卷积和训练时的多路径结构,在特征提取能力上完全一致,但推理速度提升数倍,内存访问次数也大幅减少。

三、关键特性

  1. 训练与推理解耦:训练时“复杂”保证性能,推理时“简单”保证速度,无需额外操作,模型自动完成转换;
  2. 无性能损失:重参数化是严格的数学等价转换,不会丢失训练时学到的特征信息;
  3. 灵活性强:可通过调整Path₃×3·1×1的数量(N)、路径类型,适配轻量(移动端)、中量级(边缘设备)、重量级(服务器)等不同场景;
  4. 兼容性好:基于标准卷积和BN实现,无需特殊硬件支持,可无缝集成到现有语义分割框架中。

四、 ablation实验验证(关键设计的有效性)

研究者通过控制变量实验,验证了GCBlock各组件的必要性:

1. Path₁×1·1×1的卷积数量

  • 实验设置:固定其他路径,改变Path₁×1·1×1中的1×1卷积数量(0~4个);
  • 结果:数量为2时效果最优(mIoU=76.7%),少于2个则学习能力不足,多于2个则内存和训练时间飙升,且性能下降;
  • 结论:2个1×1卷积是平衡性能与成本的最优选择。

2. Path₃×3·1×1的数量(N)

  • 实验设置:固定其他路径,改变Path₃×3·1×1的数量(1~10个);
  • 结果:N=4~5时mIoU达到峰值,N=10时性能反而下降,且训练时间增加80%;
  • 结论:路径数量并非越多越好,过多会导致模型过拟合、计算成本激增,需根据模型规模适配(GCNet-S用N=4,M/L用N=2)。

五、与传统模块的对比

模块类型 训练时结构 推理时结构 推理速度 训练能力
ResBlock(残差块) 多路径+残差连接 多路径+残差连接
Conv-Former Block 类Transformer结构 类Transformer结构 很慢
GCBlock(金箍棒块) 多卷积+多路径 单一3×3卷积

可见,GCBlock既解决了传统多路径模块“慢”的问题,又弥补了单路径模块“训练能力弱”的缺陷,是实时语义分割中高效的模块设计。

总结

GCBlock的核心价值在于用重参数化技术打破了“训练能力”与“推理速度”的矛盾:训练时通过多路径扩张充分学习特征,推理时收缩为单卷积高效运行,为GCNet在Cityscapes、CamVid等数据集上实现“又快又准”的性能提供了关键支撑。其设计思路也可为其他实时计算机视觉任务(如目标检测、实例分割)提供参考。

核心代码


class GCBlock(nn.Module):
    """GCBlock.

    Args:
        in_channels (int): Number of channels in the input image
        out_channels (int): Number of channels produced by the convolution
        kernel_size (int or tuple): Size of the convolving kernel
        stride (int or tuple): Stride of the convolution. Default: 1
        padding (int, tuple): Padding added to all four sides of
            the input. Default: 1
        padding_mode (string, optional): Default: 'zeros'
        norm_cfg (dict): Config dict to build norm layer.
            Default: dict(type='BN', requires_grad=True)
        act (bool) : Whether to use activation function.
            Default: False
        deploy (bool): Whether in deploy mode. Default: False
    """

    def __init__(self,
                 in_channels: int,
                 out_channels: int,
                 kernel_size: Union[int, Tuple[int]] = 3,
                 stride: Union[int, Tuple[int]] = 1,
                 padding: Union[int, Tuple[int]] = 1,
                 padding_mode: Optional[str] = 'zeros',
                 norm_cfg: OptConfigType = dict(type='BN', requires_grad=True),
                 act_cfg: OptConfigType = dict(type='ReLU', inplace=True),
                 act: bool = True,
                 deploy: bool = False):
        super().__init__()

        self.in_channels = in_channels
        self.out_channels = out_channels
        self.kernel_size = kernel_size
        self.stride = stride
        self.padding = padding
        self.deploy = deploy

        assert kernel_size == 3
        assert padding == 1

        padding_11 = padding - kernel_size // 2

        if act:
            self.relu = build_activation_layer(act_cfg)
        else:
            self.relu = nn.Identity()

        if deploy:
            self.reparam_3x3 = nn.Conv2d(
                in_channels=in_channels,
                out_channels=out_channels,
                kernel_size=kernel_size,
                stride=stride,
                padding=padding,
                bias=True,
                padding_mode=padding_mode)

        else:
            if (out_channels == in_channels) and stride == 1:
                self.path_residual = build_norm_layer(norm_cfg, num_features=in_channels)[1]
            else:
                self.path_residual = None

            self.path_3x3_1 = Block3x3(
                in_channels=in_channels,
                out_channels=out_channels,
                stride=stride,
                padding=padding,
                bias=False,
                norm_cfg=norm_cfg,
            )
            self.path_3x3_2 = Block3x3(
                in_channels=in_channels,
                out_channels=out_channels,
                stride=stride,
                padding=padding,
                bias=False,
                norm_cfg=norm_cfg,
            )
            self.path_1x1 = Block1x1(
                in_channels=in_channels,
                out_channels=out_channels,
                stride=stride,
                padding=padding_11,
                bias=False,
                norm_cfg=norm_cfg,
            )

    def forward(self, inputs: Tensor) -> Tensor:

        if hasattr(self, 'reparam_3x3'):
            return self.relu(self.reparam_3x3(inputs))

        if self.path_residual is None:
            id_out = 0
        else:
            id_out = self.path_residual(inputs)

        return self.relu(self.path_3x3_1(inputs) + self.path_3x3_2(inputs) + self.path_1x1(inputs) + id_out)

    def get_equivalent_kernel_bias(self):
        """Derives the equivalent kernel and bias in a differentiable way.

        Returns:
            tuple: Equivalent kernel and bias
        """
        self.path_3x3_1.switch_to_deploy()
        kernel3x3_1, bias3x3_1 = self.path_3x3_1.conv.weight.data, self.path_3x3_1.conv.bias.data
        self.path_3x3_2.switch_to_deploy()
        kernel3x3_2, bias3x3_2 = self.path_3x3_2.conv.weight.data, self.path_3x3_2.conv.bias.data
        self.path_1x1.switch_to_deploy()
        kernel1x1, bias1x1 = self.path_1x1.conv.weight.data, self.path_1x1.conv.bias.data
        kernelid, biasid = self._fuse_bn_tensor(self.path_residual)

        return kernel3x3_1 + kernel3x3_2 + self._pad_1x1_to_3x3_tensor(kernel1x1) + kernelid, bias3x3_1 + bias3x3_2 + bias1x1 + biasid

    def _pad_1x1_to_3x3_tensor(self, kernel1x1):
        """Pad 1x1 tensor to 3x3.
        Args:
            kernel1x1 (Tensor): The input 1x1 kernel need to be padded.

        Returns:
            Tensor: 3x3 kernel after padded.
        """
        if kernel1x1 is None:
            return 0
        else:
            return torch.nn.functional.pad(kernel1x1, [1, 1, 1, 1])

    def _fuse_bn_tensor(self, conv: nn.Module) -> Tuple[np.ndarray, Tensor]:
        """Derives the equivalent kernel and bias of a specific conv layer.

        Args:
            conv (nn.Module): The layer that needs to be equivalently
                transformed, which can be nn.Sequential or nn.Batchnorm2d

        Returns:
            tuple: Equivalent kernel and bias
        """
        if conv is None:
            return 0, 0
        if isinstance(conv, ConvModule):
            kernel = conv.conv.weight
            running_mean = conv.bn.running_mean
            running_var = conv.bn.running_var
            gamma = conv.bn.weight
            beta = conv.bn.bias
            eps = conv.bn.eps
        else:
            assert isinstance(conv, (nn.SyncBatchNorm, nn.BatchNorm2d, _BatchNormXd))
            if not hasattr(self, 'id_tensor'):
                input_in_channels = self.in_channels
                kernel_value = np.zeros((self.in_channels, input_in_channels, 3, 3),
                                        dtype=np.float32)
                for i in range(self.in_channels):
                    kernel_value[i, i % input_in_channels, 1, 1] = 1
                self.id_tensor = torch.from_numpy(kernel_value).to(
                    conv.weight.device)
            kernel = self.id_tensor
            running_mean = conv.running_mean
            running_var = conv.running_var
            gamma = conv.weight
            beta = conv.bias
            eps = conv.eps
        std = (running_var + eps).sqrt()
        t = (gamma / std).reshape(-1, 1, 1, 1)
        return kernel * t, beta - running_mean * gamma / std

    def switch_to_deploy(self):
        """Switch to deploy mode."""
        if hasattr(self, 'reparam_3x3'):
            return
        kernel, bias = self.get_equivalent_kernel_bias()
        self.reparam_3x3 = nn.Conv2d(
            in_channels=self.in_channels,
            out_channels=self.out_channels,
            kernel_size=self.kernel_size,
            stride=self.stride,
            padding=self.padding,
            bias=True)
        self.reparam_3x3.weight.data = kernel
        self.reparam_3x3.bias.data = bias
        for para in self.parameters():
            para.detach_()
        self.__delattr__('path_3x3_1')
        self.__delattr__('path_3x3_2')
        self.__delattr__('path_1x1')
        if hasattr(self, 'path_residual'):
            self.__delattr__('path_residual')
        if hasattr(self, 'id_tensor'):
            self.__delattr__('id_tensor')
        self.deploy = True

实验

脚本

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

if __name__ == '__main__':
#     修改为自己的配置文件地址
    model = YOLO('./ultralytics/cfg/models/26/yolo26-GCBlock.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',
                amp=True,
                project='runs/train',
                name='yolo26-GCBlock',
                )

结果

image-20260117142208896

THE END