YOLOv11改进 – 卷积Conv GCBlock 聚焦于高效捕获特征图中的全局依赖关系

# 前言

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

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

专栏链接: YOLOv11改进专栏

介绍

image-20251119230707964

摘要

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

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

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

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

文章链接

论文地址:论文地址

代码地址:代码地址

基本原理

GCBlock全称为Global Context Block(全局上下文块),是ICCV 2019提出的Global Context Network(GCNet)的核心组件。它聚焦于高效捕获特征图中的全局依赖关系,巧妙融合了非局部网络(NLNet)建模全局依赖的优势与挤压 - 激励网络(SENet)轻量化的特点,在降低计算成本的同时保障了模型性能,以下是详细介绍:

  1. 核心设计背景
    • 传统CNN依靠堆叠卷积层扩大感受野来获取全局信息,不仅参数量和计算量激增,还存在优化难、信息传递不充分的问题。
    • NLNet虽能通过自注意力机制建模全局依赖,但需为每个查询位置计算专属注意力图,计算复杂度极高。而研究发现,不同查询位置的注意力图几乎一致,这意味着可简化为与查询位置无关的通用结构。
    • SENet虽轻量化,但仅聚焦通道间依赖,对全局空间信息的建模能力不足。GCBlock正是针对这些问题,整合两类网络优势,形成高效的全局上下文建模方案。
  2. 核心结构与工作流程 GCBlock遵循“上下文建模 - 特征变换 - 特征融合”的通用框架,具体流程如下:
    1. 上下文建模模块:该模块负责聚合所有位置的特征以形成全局上下文特征。先输入特征图(X ∈ R^{C×H×W})(其中C为通道数,H、W分别为特征图的高和宽);再通过全局平均池化操作压缩空间维度,得到维度为(R^C)的全局上下文特征(z);最后经全连接层或1×1卷积层,结合softmax激活函数生成注意力权重,以此体现全局特征对各位置的重要性。
    2. 特征变换模块:此模块用于捕获通道间的依赖关系,同时控制计算量。它借鉴SENet的瓶颈结构,通过两层1×1卷积与ReLU激活函数完成特征变换。设置瓶颈比率(r)(默认值为16)缩减通道数,将原本(C×C)的参数量降至(2×C×C/r),大幅降低计算开销,同时有效提取通道维度的关键依赖信息。
    3. 特征融合模块:该模块的作用是将全局上下文特征与原始特征图融合。采用加法操作,把经过变换的全局上下文特征融入到输入特征图的每个位置中,让局部特征获得全局信息的补充,最终输出增强后的特征图(Y ∈ R^{C×H×W}),实现全局与局部信息的有机结合。
  3. 核心优势
    • 轻量化易部署:通过简化注意力计算和瓶颈变换,GCBlock参数量和计算量远低于NLNet。例如在ResNet深层中应用时,其参数量仅为简化NLNet的1/8左右,可灵活嵌入骨干网络的多个层次。
    • 性能全面领先:在图像分类、目标检测、语义分割等多个视觉任务的基准测试中,基于GCBlock构建的GCNet,性能普遍优于NLNet和SENet,既能保证全局依赖建模效果,又避免了冗余计算。
    • 结构通用性强:其框架可兼容NLNet和SENet的核心逻辑,便于与ResNet、ResNeXt等主流骨干网络结合,适配不同场景的视觉任务优化需求。
  4. 典型应用场景
    • 目标检测与实例分割:将GCBlock嵌入Mask R - CNN等模型的骨干网络,能提升对小目标、遮挡目标的特征提取能力,在COCO数据集上可在参数量相近的情况下,优于含NLNet的模型性能。
    • 语义分割:在Camvid等数据集上,结合ResNet50作为骨干网络,GCBlock可增强全局语义信息的传递,助力模型区分相似类别区域。
    • 图像分类:嵌入ResNet等分类网络的多个层次,可提升模型对全局场景和细节特征的综合理解,提升分类准确率。
  5. 代码实现特点 其PyTorch实现通常以轻量化为核心,通过全局平均池化、1×1卷积实现核心逻辑,同时设置通道缩减比率控制计算量。例如通过定义ratio参数缩减通道数,结合层归一化提升模型稳定性,可直接嵌入卷积神经网络的残差块中使用。

核心代码


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('/root/ultralytics-main/ultralytics/cfg/models/11/yolov11-GCBlock.yaml')
#     修改为自己的数据集地址
    model.train(data='/root/ultralytics-main/ultralytics/cfg/datasets/coco8.yaml',
                cache=False,
                imgsz=640,
                epochs=10,
                single_cls=False,  # 是否是单类别检测
                batch=8,
                close_mosaic=10,
                workers=0,
                optimizer='SGD',
                amp=True,
                project='runs/train',
                name='GCBlock',
                )

结果

image-20251119232303001

THE END