YOLO26改进 – 卷积Conv GCNet之金箍棒块GCBlock : 重参数化捕获全局依赖 CVPR 2025
# 前言
本文介绍了GCBlock在YOLO26中的结合应用。GCBlock是GCNet的核心组件,能高效捕获特征图中的全局依赖关系,融合了非局部网络和挤压 - 激励网络的优势,降低计算成本并保障模型性能。其遵循“上下文建模 - 特征变换 - 特征融合”框架,通过全局平均池化、全连接层等操作生成注意力权重。我们将GCBlock集成进YOLO26,替代原有的部分卷积模块,实现更高效的全局上下文建模。实验证明,YOLO26-GCBlock在目标检测任务中表现出色,展现了GCBlock在深度学习中的广泛应用前景。
文章目录: YOLO26改进大全:卷积层、轻量化、注意力机制、损失函数、Backbone、SPPF、Neck、检测头全方位优化汇总
专栏链接: YOLO26改进专栏
介绍

摘要
近年来的实时语义分割模型,无论是单分支还是多分支结构,都在性能和速度上取得了不错的表现。然而,这些模型的速度常常受到多路径模块的限制,有些还依赖于高性能的教师模型进行训练。为了解决这些问题,我们提出了 金箍棒网络(GCNet)。
具体来说,GCNet 在训练阶段结合了 纵向多卷积 和 横向多路径结构,在推理阶段则将这些结构重新参数化为一个单一的卷积操作,从而同时优化性能与速度。这样的设计使得 GCNet 能够在训练时“自我膨胀”,而在推理时“自我收缩”,相当于无需外部教师模型就具备了“教师模型”的能力。
实验结果表明,GCNet 在 Cityscapes、CamVid 和 Pascal VOC 2012 数据集上,在性能和推理速度方面都优于现有的先进模型。
项目代码已开源,地址为:https://github.com/gyyang23/GCNet
文章链接
论文地址:论文地址
代码地址:代码地址
基本原理
金箍棒块(GCBlock)详细介绍
GCBlock(Golden Cudgel Block,金箍棒块)是GCNet(金箍棒网络)的核心组件,核心设计理念是“训练时扩张增强学习能力,推理时收缩提升运行速度”,既兼顾了多路径模块的训练优势,又保留了单路径模块的推理效率,完美解决了实时语义分割中“性能与速度难以平衡”的关键问题。
一、设计初衷
之前的实时语义分割模型存在两大痛点:
- 多路径模块(如ResBlock、Conv-Former Block)虽能提升训练效果,但结构复杂、内存访问频繁,严重拖慢推理速度;
- 单路径模块虽快,但学习能力有限,难以捕捉丰富的特征信息。
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卷积”。
整个重参数化过程是数学等价的,意味着推理时的单一卷积和训练时的多路径结构,在特征提取能力上完全一致,但推理速度提升数倍,内存访问次数也大幅减少。
三、关键特性
- 训练与推理解耦:训练时“复杂”保证性能,推理时“简单”保证速度,无需额外操作,模型自动完成转换;
- 无性能损失:重参数化是严格的数学等价转换,不会丢失训练时学到的特征信息;
- 灵活性强:可通过调整Path₃×3·1×1的数量(N)、路径类型,适配轻量(移动端)、中量级(边缘设备)、重量级(服务器)等不同场景;
- 兼容性好:基于标准卷积和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',
)
结果
