Bootstrap

YOLOv10改进 | 注意力机制| 引入多尺度分支来增强特征表征的注意力机制 【CVPR2021】

秋招面试专栏推荐深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转


💡💡💡本专栏所有程序均经过测试,可成功执行💡💡💡


专栏目录 :《YOLOv8改进有效涨点》专栏介绍 & 专栏目录 | 目前已有50+篇内容,内含各种Head检测头、损失函数Loss、Backbone、Neck、NMS等创新点改进——点击即可跳转

DBB(多样化分支块)是一种用于提高卷积神经网络性能的通用建筑模块,能够在不增加推理时间成本的情况下增强单一卷积的表征能力。通过结合不同尺度和复杂度的多样化分支,DBB丰富了特征空间,包括卷积序列、多尺度卷积和平均池化。训练完成后,DBB可以转换为一个单一的卷积层以供部署。与新型ConvNet架构的进步不同,DBB虽然复杂化了训练时的微结构,但保持了宏观架构不变,因此它可以作为任何架构中常规卷积层的直接替代。通过这种方式,模型可以训练以达到更高的性能水平,然后在推理时转换回原始的推理时间结构。文章在介绍主要的原理后,将手把手教学如何进行模块的代码添加和修改将修改后的完整代码放在文章的最后方便大家一键运行小白也可轻松上手实践。以帮助您更好地学习深度学习目标检测YOLO系列的挑战。


专栏地址YOLOv10入门 + 涨点——持续更新各种涨点方法

目录

1. 原理

2. 将DBB添加到YOLOv10中

2.1 DBB代码实现

2.2 更改init.py文件

2.3 添加yaml文件

2.4 在task.py中进行注册

2.5 执行程序

3. 完整代码分享

4. GFLOPs

5. 进阶

6. 总结


1. 原理

论文地址:Diverse Branch Block: Building a Convolution as an Inception-like Unit——点击即可跳转

官方仓库:官方代码仓库——点击即可跳转

DBB (Diverse Branch Block) 的主要原理如下:

核心思想

DBB 是一种用于提升卷积神经网络(ConvNet)性能的通用构建模块,其特点是在训练时通过多种不同规模和复杂度的分支组合来丰富特征空间,包括卷积序列、多尺度卷积和平均池化。在推理时,这些复杂的训练结构可以等效转换为单一的卷积层,从而无需在推理阶段付出额外的计算成本。

主要特点

  1. 多分支拓扑结构:DBB 采用多分支拓扑结构,包含多尺度卷积、序列的 1×1 - K×K 卷积、平均池化和分支相加等操作。通过不同复杂度和感受野的路径组合,DBB 能够丰富特征空间,类似于 Inception 架构。

  2. 结构重参数化:DBB 可以等效地转换为单一卷积层,用于推理阶段。这种转换属于结构重参数化方法,即用一种结构的参数来参数化另一种结构。在训练后将模型转换为推理用的原始结构,只需保存和使用转换后的模型即可。

优势

  • 性能提升:DBB 通过在训练时插入复杂结构来提升模型的表现,例如在 ImageNet 上提升最高可达 1.9% 的 top-1 准确率,并且在 COCO 检测和 Cityscapes 语义分割任务中也表现出色。

  • 无需额外推理成本:由于 DBB 在推理时可以转换为单一卷积层,因此不会增加推理时间的成本。

  • 易于使用和通用性:DBB 可以作为基础卷积层的升级模块,适用于多种卷积神经网络架构,不影响原有的宏观架构设计。

应用场景

DBB 可以用于图像分类、目标检测和语义分割等任务,通过在训练时替换部分常规卷积层为 DBB,提升模型性能。然后在推理时将这些 DBB 转换回原始结构,从而在不增加推理成本的情况下享受更高的性能。

总之,DBB 通过在训练时复杂化模型微观结构,并在推理时保持宏观架构不变,实现了在不增加推理成本的前提下大幅提升卷积神经网络性能的目标。

2. 将DBB添加到YOLOv10中

2.1 DBB代码实现

关键步骤一: 将下面代码粘贴到在/ultralytics/ultralytics/nn/modules/block.py中,并在该文件的__all__中添加“BAMBlock”

*注代码过长,完整代码请查看第三部分【完整代码】的内容

class DiverseBranchBlock(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size,
                 stride=1, padding=None, dilation=1, groups=1,
                 internal_channels_1x1_3x3=None,
                 deploy=False, single_init=False):
        super(DiverseBranchBlock, self).__init__()
        self.deploy = deploy

        self.nonlinear = Conv.default_act

        self.kernel_size = kernel_size
        self.out_channels = out_channels
        self.groups = groups

        if padding is None:
            padding = autopad(kernel_size, padding, dilation)
        assert padding == kernel_size // 2

        if deploy:
            self.dbb_reparam = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=kernel_size,
                                         stride=stride,
                                         padding=padding, dilation=dilation, groups=groups, bias=True)

        else:

            self.dbb_origin = conv_bn(in_channels=in_channels, out_channels=out_channels, kernel_size=kernel_size,
                                      stride=stride, padding=padding, dilation=dilation, groups=groups)

            self.dbb_avg = nn.Sequential()
            if groups < out_channels:
                self.dbb_avg.add_module('conv',
                                        nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1,
                                                  stride=1, padding=0, groups=groups, bias=False))
                self.dbb_avg.add_module('bn', BNAndPadLayer(pad_pixels=padding, num_features=out_channels))
                self.dbb_avg.add_module('avg', nn.AvgPool2d(kernel_size=kernel_size, stride=stride, padding=0))
                self.dbb_1x1 = conv_bn(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=stride,
                                       padding=0, groups=groups)
            else:
                self.dbb_avg.add_module('avg', nn.AvgPool2d(kernel_size=kernel_size, stride=stride, padding=padding))

            self.dbb_avg.add_module('avgbn', nn.BatchNorm2d(out_channels))

            if internal_channels_1x1_3x3 is None:
                internal_channels_1x1_3x3 = in_channels if groups < out_channels else 2 * in_channels  # For mobilenet, it is better to have 2X internal channels

            self.dbb_1x1_kxk = nn.Sequential()
            if internal_channels_1x1_3x3 == in_channels:
                self.dbb_1x1_kxk.add_module('idconv1', IdentityBasedConv1x1(channels=in_channels, groups=groups))
            else:
                self.dbb_1x1_kxk.add_module('conv1',
                                            nn.Conv2d(in_channels=in_channels, out_channels=internal_channels_1x1_3x3,
                                                      kernel_size=1, stride=1, padding=0, groups=groups, bias=False))
            self.dbb_1x1_kxk.add_module('bn1', BNAndPadLayer(pad_pixels=padding, num_features=internal_channels_1x1_3x3,
                                                             affine=True))
            self.dbb_1x1_kxk.add_module('conv2',
                                        nn.Conv2d(in_channels=internal_channels_1x1_3x3, out_channels=out_channels,
                                                  kernel_size=kernel_size, stride=stride, padding=0, groups=groups,
                                                  bias=False))
            self.dbb_1x1_kxk.add_module('bn2', nn.BatchNorm2d(out_channels))

        #   The experiments reported in the paper used the default initialization of bn.weight (all as 1). But changing the initialization may be useful in some cases.
        if single_init:
            #   Initialize the bn.weight of dbb_origin as 1 and others as 0. This is not the default setting.
            self.single_init()

    def get_equivalent_kernel_bias(self):
        k_origin, b_origin = transI_fusebn(self.dbb_origin.conv.weight, self.dbb_origin.bn)

        if hasattr(self, 'dbb_1x1'):
            k_1x1, b_1x1 = transI_fusebn(self.dbb_1x1.conv.weight, self.dbb_1x1.bn)
            k_1x1 = transVI_multiscale(k_1x1, self.kernel_size)
        else:
            k_1x1, b_1x1 = 0, 0

        if hasattr(self.dbb_1x1_kxk, 'idconv1'):
            k_1x1_kxk_first = self.dbb_1x1_kxk.idconv1.get_actual_kernel()
        else:
            k_1x1_kxk_first = self.dbb_1x1_kxk.conv1.weight
        k_1x1_kxk_first, b_1x1_kxk_first = transI_fusebn(k_1x1_kxk_first, self.dbb_1x1_kxk.bn1)
        k_1x1_kxk_second, b_1x1_kxk_second = transI_fusebn(self.dbb_1x1_kxk.conv2.weight, self.dbb_1x1_kxk.bn2)
        k_1x1_kxk_merged, b_1x1_kxk_merged = transIII_1x1_kxk(k_1x1_kxk_first, b_1x1_kxk_first, k_1x1_kxk_second,
                                                              b_1x1_kxk_second, groups=self.groups)

        k_avg = transV_avg(self.out_channels, self.kernel_size, self.groups)
        k_1x1_avg_second, b_1x1_avg_second = transI_fusebn(k_avg.to(self.dbb_avg.avgbn.weight.device),
                                                           self.dbb_avg.avgbn)
        if hasattr(self.dbb_avg, 'conv'):
            k_1x1_avg_first, b_1x1_avg_first = transI_fusebn(self.dbb_avg.conv.weight, self.dbb_avg.bn)
            k_1x1_avg_merged, b_1x1_avg_merged = transIII_1x1_kxk(k_1x1_avg_first, b_1x1_avg_first, k_1x1_avg_second,
                                                                  b_1x1_avg_second, groups=self.groups)
        else:
            k_1x1_avg_merged, b_1x1_avg_merged = k_1x1_avg_second, b_1x1_avg_second

        return transII_addbranch((k_origin, k_1x1, k_1x1_kxk_merged, k_1x1_avg_merged),
                                 (b_origin, b_1x1, b_1x1_kxk_merged, b_1x1_avg_merged))

    def switch_to_deploy(self):
        if hasattr(self, 'dbb_reparam'):
            return
        kernel, bias = self.get_equivalent_kernel_bias()
        self.dbb_reparam = nn.Conv2d(in_channels=self.dbb_origin.conv.in_channels,
                                     out_channels=self.dbb_origin.conv.out_channels,
                                     kernel_size=self.dbb_origin.conv.kernel_size, stride=self.dbb_origin.conv.stride,
                                     padding=self.dbb_origin.conv.padding, dilation=self.dbb_origin.conv.dilation,
                                     groups=self.dbb_origin.conv.groups, bias=True)
        self.dbb_reparam.weight.data = kernel
        self.dbb_reparam.bias.data = bias
        for para in self.parameters():
            para.detach_()
        self.__delattr__('dbb_origin')
        self.__delattr__('dbb_avg')
        if hasattr(self, 'dbb_1x1'):
            self.__delattr__('dbb_1x1')
        self.__delattr__('dbb_1x1_kxk')

    def forward(self, inputs):
        if hasattr(self, 'dbb_reparam'):
            return self.nonlinear(self.dbb_reparam(inputs))

        out = self.dbb_origin(inputs)
        if hasattr(self, 'dbb_1x1'):
            out += self.dbb_1x1(inputs)
        out += self.dbb_avg(inputs)
        out += self.dbb_1x1_kxk(inputs)
        return self.nonlinear(out)

    def init_gamma(self, gamma_value):
        if hasattr(self, "dbb_origin"):
            torch.nn.init.constant_(self.dbb_origin.bn.weight, gamma_value)
        if hasattr(self, "dbb_1x1"):
            torch.nn.init.constant_(self.dbb_1x1.bn.weight, gamma_value)
        if hasattr(self, "dbb_avg"):
            torch.nn.init.constant_(self.dbb_avg.avgbn.weight, gamma_value)
        if hasattr(self, "dbb_1x1_kxk"):
            torch.nn.init.constant_(self.dbb_1x1_kxk.bn2.weight, gamma_value)

    def single_init(self):
        self.init_gamma(0.0)
        if hasattr(self, "dbb_origin"):
            torch.nn.init.constant_(self.dbb_origin.bn.weight, 1.0)

DBB (Diverse Branch Block) 处理图像的主要流程如下:

1. 训练阶段

在训练阶段,DBB 通过多种不同规模和复杂度的分支组合来处理输入图像。以下是具体流程:

(1) 输入图像

图像作为输入,经过预处理后进入 DBB。

(2) 多分支结构

DBB 包含多个不同的分支,每个分支有不同的操作,包括:

  • 多尺度卷积:在不同尺度上进行卷积操作(例如 1×1 和 K×K 的组合)。

  • 序列卷积:多个卷积操作的序列(例如 1×1 后跟 K×K)。

  • 平均池化:对特征图进行平均池化操作。

这些操作通过不同感受野和路径复杂度的组合,丰富了特征空间。

(3) 特征组合

所有分支的输出在最后会被合并,形成一个综合的特征图。

(4) 批归一化和非线性激活

合并后的特征图经过批归一化和非线性激活函数(如 ReLU)处理,提升模型的非线性表达能力。

2. 推理阶段

在推理阶段,DBB 经过转换,可以等效为一个单一的卷积层。以下是具体流程:

(1) 输入图像

图像作为输入,经过预处理后进入已经转换的 DBB。

(2) 单一卷积层

DBB 在推理阶段被转换为一个单一的卷积层,该卷积层在结构上与常规的卷积层无异。

(3) 特征提取

单一卷积层对输入图像进行卷积操作,提取特征图。

(4) 批归一化和非线性激活

提取的特征图同样会经过批归一化和非线性激活函数处理。

3. 等效转换

DBB 在训练阶段的复杂多分支结构通过以下几种等效转换方式被简化为单一卷积层:

  • 多尺度卷积转换:将不同尺度的卷积核合并为一个更大的卷积核。

  • 序列卷积转换:将序列卷积的权重合并为一个单一卷积层的权重。

  • 平均池化转换:将池化操作转换为卷积操作的等效权重。

4. 结构重参数化

DBB 的等效转换通过结构重参数化技术实现,即用训练阶段复杂结构的参数来生成推理阶段单一卷积层的参数。

总结

DBB 在训练阶段通过复杂的多分支结构提升特征表达能力,训练完成后通过结构重参数化转换为单一卷积层,从而在推理阶段不增加计算成本的前提下获得更高的性能。

这种方法既保证了模型的高性能,又避免了在实际应用中的高计算开销,非常适合需要高效推理的场景。

2.2 更改init.py文件

关键步骤二:修改modules文件夹下的__init__.py文件,先导入函数

然后在下面的__all__中声明函数

2.3 添加yaml文件

关键步骤三:在/ultralytics/ultralytics/cfg/models/v10下面新建文件yolov10_DBB.yaml文件,粘贴下面的内容

# Parameters
nc: 80 # number of classes
scales: # model compound scaling constants, i.e. 'model=yolov8n.yaml' will call yolov8.yaml with scale 'n'
  # [depth, width, max_channels]
  l: [1.00, 1.00, 512] # YOLOv8l summary: 365 layers, 43691520 parameters, 43691504 gradients, 165.7 GFLOPs

# YOLOv8.0n backbone
backbone:
  # [from, repeats, module, args]
  - [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
  - [-1, 1, DiverseBranchBlock, [128, 3, 2]] # 1-P2/4
  - [-1, 3, C2f, [128, True]]
  - [-1, 1, DiverseBranchBlock, [256, 3, 2]] # 3-P3/8
  - [-1, 6, C2f, [256, True]]
  - [-1, 1, SCDown, [512, 3, 2]] # 5-P4/16
  - [-1, 6, C2f, [512, True]]
  - [-1, 1, SCDown, [1024, 3, 2]] # 7-P5/32
  - [-1, 3, C2fCIB, [1024, True]]
  - [-1, 1, SPPF, [1024, 5]] # 9
  - [-1, 1, PSA, [1024]] # 10

# YOLOv8.0n head
head:
  - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
  - [[-1, 6], 1, Concat, [1]] # cat backbone P4
  - [-1, 3, C2fCIB, [512, True]] # 13

  - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
  - [[-1, 4], 1, Concat, [1]] # cat backbone P3
  - [-1, 3, C2f, [256]] # 16 (P3/8-small)

  - [-1, 1, Conv, [256, 3, 2]]
  - [[-1, 13], 1, Concat, [1]] # cat head P4
  - [-1, 3, C2fCIB, [512, True]] # 19 (P4/16-medium)

  - [-1, 1, SCDown, [512, 3, 2]]
  - [[-1, 10], 1, Concat, [1]] # cat head P5
  - [-1, 3, C2fCIB, [1024, True]] # 22 (P5/32-large)

  - [[16, 19, 22], 1, v10Detect, [nc]] # Detect(P3, P4, P5)

温馨提示:因为本文只是对yolov8基础上添加模块,如果要对yolov8n/l/m/x进行添加则只需要指定对应的depth_multiple 和 width_multiple。


# YOLOv10n
depth_multiple: 0.33  # model depth multiple
width_multiple: 0.25  # layer channel multiple
max_channels: 1024 # max_channels
 
# YOLOv10s
depth_multiple: 0.33  # model depth multiple
width_multiple: 0.50  # layer channel multiple
max_channels: 1024 # max_channels
 
# YOLOv10l 
depth_multiple: 1.0  # model depth multiple
width_multiple: 1.0  # layer channel multiple
max_channels: 512 # max_channels
 
# YOLOv10m
depth_multiple: 0.67  # model depth multiple
width_multiple: 0.75  # layer channel multiple
max_channels: 768 # max_channels
 
# YOLOv10x
depth_multiple: 1.33  # model depth multiple
width_multiple: 1.25  # layer channel multiple
max_channels: 512 # max_channels

2.4 在task.py中进行注册

关键步骤四:在task.py的parse_model函数中进行注册, DiverseBranchBlock

2.5 执行程序

关键步骤五:在ultralytics文件中新建train.py,将model的参数路径设置为yolov10_DBB.yaml的路径即可

from ultralytics import YOLOv10
 
# Load a model
# model = YOLO('yolov8n.yaml')  # build a new model from YAML
# model = YOLO('yolov8n.pt')  # load a pretrained model (recommended for training)
 
model = YOLOv10(r'/projects/ultralytics/ultralytics/cfg/models/v10/yolov10_DBB.yaml')  # build from YAML and transfer weights
 
# Train the model
model.train(batch=16)

🚀运行程序,如果出现下面的内容则说明添加成功🚀

3. 完整代码分享

https://pan.baidu.com/s/15BSgzi5i1tqvTnW4qJSwpQ?pwd=n7jm

提取码: n7jm 

4. GFLOPs

关于GFLOPs的计算方式可以查看百面算法工程师 | 卷积基础知识——Convolution

未改进的YOLOv10lGFLOPs

img

改进后的GFLOPs

5. 进阶

可以结合损失函数或者卷积模块进行多重改进

6. 总结

Diverse Branch Block (DBB) 是一种用于提升卷积神经网络性能的模块。它在训练阶段采用多分支结构,包括多尺度卷积、序列卷积和平均池化,通过这些多样化的路径丰富特征空间和感受野。训练完成后,DBB 使用结构重参数化技术将复杂的多分支结构转换为单一的卷积层。这种转换使得在推理阶段,无需额外的计算成本,即可利用训练时复杂结构带来的性能提升。因此,DBB 能够在不增加推理计算成本的前提下,大幅度提升卷积神经网络在图像分类、目标检测和语义分割等任务中的表现。

;