一.SENet V2模块的理论原理:

代码地址:官方代码地址
论文地址:官方论文地址
请添加图片描述
上述图中:
a)是ResNeXt模块, b)是SENet模块, c)是SENetV2模块.
其中c)是SENetV2模块是结合了ResNeXt模块和SENet模块的特点, 在SENet模块上多加了2个1x1的全连接层和Sigmoid, 即多分支全连接层, 在原论文中叫做挤压激励操作, 能进一步提升特征表达的精细度和全局信息的能力.
在这里插入图片描述

请添加图片描述

上图中是SENet V2提出的SaE(Squeeze-and-Excitation)模块的内部工作机制, 上部分(红框1部分)是ResNeXt模块, 下部分(蓝框2部分)是SENet V2模块, 这一部分压缩后的输出被送入多分支全连接层进行激励处理,在最后被传递以恢复其原始形状。这种设计能够让网络更有效地学习到输入数据的不同特征,并且在进行特征变换时考虑到不同通道之间的相互依赖性。


二.SENet V2实践结论:(针对自己项目数据的训练后的测试指标)

1.总结论: 添加了SENet V2模块(其他变量都统一)的yolov11精度指标比官方的yolov11指标的大部分类别要略微高一些, 但有一些少的品类精度略低一些.
2.测试统计指标:
1.下图是原始yolov11训练的每个类别的精度指标:
请添加图片描述

2.下图是添加了SENet V2模块后的yolov11训练的每个类别的精度指标:
在这里插入图片描述


三.将SENet V2模块添加到yolov11算法中:

1. SENetV2模块代码:

https://github.com/github-eliviate/SENetV2_torch_tf/blob/main/SaELayer_torch.py

上面github上的代码, 修改了SaELayer类名为SELayerV2, 并将 reduction改为16

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.model_zoo import load_url

__all__ = [ 'SELayerV2']

# 定义SE模块
class SELayer(nn.Module):
    def __init__(self, channel, reduction=16):
        super(SELayer, self).__init__()
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.fc = nn.Sequential(
            nn.Linear(channel, channel // reduction, bias=False),
            nn.ReLU(inplace=True),
            nn.Linear(channel // reduction, channel, bias=False),
            nn.Sigmoid()
        )

    def forward(self, x):
        b, c, _, _ = x.size()
        y = self.avg_pool(x).view(b, c)
        y = self.fc(y).view(b, c, 1, 1)
        return x * y.expand_as(x)

# 定义SaE模块
class SELayerV2(nn.Module):
    def __init__(self, in_channel, reduction=16):
        super(SaELayer, self).__init__()
        assert in_channel>=reduction and in_channel%reduction==0,'invalid in_channel in SaElayer'
        self.reduction = reduction
        self.cardinality=4
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        #cardinality 1
        self.fc1 = nn.Sequential(
            nn.Linear(in_channel,in_channel//self.reduction, bias=False),
            nn.ReLU(inplace=True)
        )
        # cardinality 2
        self.fc2 = nn.Sequential(
            nn.Linear(in_channel, in_channel // self.reduction, bias=False),
            nn.ReLU(inplace=True)
        )
        # cardinality 3
        self.fc3 = nn.Sequential(
            nn.Linear(in_channel, in_channel // self.reduction, bias=False),
            nn.ReLU(inplace=True)
        )
        # cardinality 4
        self.fc4 = nn.Sequential(
            nn.Linear(in_channel, in_channel // self.reduction, bias=False),
            nn.ReLU(inplace=True)
        )

        self.fc = nn.Sequential(
            nn.Linear(in_channel//self.reduction*self.cardinality, in_channel, bias=False),
            nn.Sigmoid()
        )

    def forward(self, x):
        b, c, _, _ = x.size()
        y = self.avg_pool(x).view(b, c)
        y1 = self.fc1(y)
        y2 = self.fc2(y)
        y3 = self.fc3(y)
        y4 = self.fc4(y)
        y_concate = torch.cat([y1,y2,y3,y4],dim=1)
        y_ex_dim = self.fc(y_concate).view(b,c,1,1)

        return x * y_ex_dim.expand_as(x)

# import ipdb
# ipdb.set_trace()
se_v2 = SaELayer(64)
# 示例输入
input = torch.randn(3, 64, 224, 224)
output = se_v2(input)

print(output.shape)#torch.Size([3, 64, 224, 224])

参数说明:
(1) 参数reduction:
表示的压缩维度, 先对通道做压缩后再还原, 将in_channel // reduction 作为每个分支的压缩维度, 这个参数主要用于调整速度和精度, 我的项目主要以精度为主, 所以我设置成了16(必须整除).

以通道数为256为例:
reduction=16 ⇒ 中间维度 = 256//16 = 16
reduction=32 ⇒ 中间维度 = 256//32 = 8(更窄)

reduction 越小(比如 16):中间层更宽 → 参数更多/算力更多 → 注意力更“有表达力”,可能效果更好。
reduction 越大(比如 32):中间层更窄 → 更轻量更快 → 可能略损失一点效果,但更省资源、更不容易过拟合(取决于任务和数据量)。
非常粗略地说:reduction 大约相当于在 速度/参数 vs 效果 之间调节的参数。

2. 添加代码

(1) 在ultralytics-main/ultralytics/nn路径中新建文件Addmodules, 在Addmodules中添加SENetV2.py(上面代码粘帖进来)和__init__.py代码:

ultralytics-main/ultralytics/nn/Addmodules/SENetV2.py
ultralytics-main/ultralytics/nn/Addmodules/__init__.py

在__init__.py代码:

from .SENetV2 import *

(2) 修改ultralytics-main/ultralytics/nn/task.py代码:
A. task中导入和注册新增的SENetV2模块

from .Addmodules import *

B.添加SELayerV2类名:
请添加图片描述


四.添加SENetV2的yaml文件

在下面的路径下添加yolo11m-obb_SENetV2.yaml
(注: 是训练n,s,m,l,x模型, 由这里yolo11m-obb_SENetV2.yaml命名决定, yolo11m是训练m模型, yolo11x则是训练x模型)

ultralytics-main/ultralytics/cfg/models/11/yolo11m-obb_SENetV2.yaml

yolo11m-obb_SENetV2.yaml内容:
(注意: 添加的注意力机制层, 要知道是哪一层, 体现在 - [[17, 21, 25], 1, OBB, [nc, 1]] # Detect(P3, P4, P5)

# Ultralytics YOLO 🚀, AGPL-3.0 license
# YOLO11 object detection model with P3-P5 outputs. For Usage examples see https://docs.ultralytics.com/tasks/detect
 
# Parameters
nc: 80 # number of classes
scales: # model compound scaling constants, i.e. 'model=yolo11n.yaml' will call yolo11.yaml with scale 'n'
  # [depth, width, max_channels]
  n: [0.50, 0.25, 1024] # summary: 319 layers, 2624080 parameters, 2624064 gradients, 6.6 GFLOPs
  s: [0.50, 0.50, 1024] # summary: 319 layers, 9458752 parameters, 9458736 gradients, 21.7 GFLOPs
  m: [0.50, 1.00, 512] # summary: 409 layers, 20114688 parameters, 20114672 gradients, 68.5 GFLOPs
  l: [1.00, 1.00, 512] # summary: 631 layers, 25372160 parameters, 25372144 gradients, 87.6 GFLOPs
  x: [1.00, 1.50, 512] # summary: 631 layers, 56966176 parameters, 56966160 gradients, 196.0 GFLOPs
 
# YOLO11n backbone
backbone:
  # [from, repeats, module, args]
  - [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
  - [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
  - [-1, 2, C3k2, [256, False, 0.25]]
  - [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
  - [-1, 2, C3k2, [512, False, 0.25]]
  - [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
  - [-1, 2, C3k2, [512, True]]
  - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
  - [-1, 2, C3k2, [1024, True]]
  - [-1, 1, SPPF, [1024, 5]] # 9
  - [-1, 2, C2PSA, [1024]] # 10
 
# YOLO11n head
head:
  - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
  - [[-1, 6], 1, Concat, [1]] # cat backbone P4
  - [-1, 2, C3k2, [512, False]] # 13
 
  - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
  - [[-1, 4], 1, Concat, [1]] # cat backbone P3
  - [-1, 2, C3k2, [256, False]] # 16 (P3/8-small)
  - [-1, 1, SELayerV2, []] # 17 (P3/8-small)  小目标检测层输出位置
 
  - [-1, 1, Conv, [256, 3, 2]]
  - [[-1, 13], 1, Concat, [1]] # cat head P4
  - [-1, 2, C3k2, [512, False]] # 20 (P4/16-medium)
  - [-1, 1, SELayerV2, []] # 21 (P4/16-medium) 中目标检测层输出位置
 
  - [-1, 1, Conv, [512, 3, 2]]
  - [[-1, 10], 1, Concat, [1]] # cat head P5
  - [-1, 2, C3k2, [1024, True]] # 24 (P5/32-large)
  - [-1, 1, SELayerV2, []] # 25 (P5/32-large) 大目标检测层输出位置
 
# 重要重要!!!  添加的注意力位置是[17, 21, 25]位置对应的检测层!
  - [[17, 21, 25], 1, OBB, [nc, 1]] # Detect(P3, P4, P5)

五.train训练

train.py

from ultralytics.models.yolo.obb import OBBTrainer
from ultralytics import YOLO

if __name__ == "__main__":
    device_list = [1]    # device=[2,3,4,5,6,7]对应的batch是96
    my_project="runs/SENetV2_model_project"

    # 微调模型
    #pre_train_model = "yolo11m-obb.pt"
    model_cfg = "../../../ultralytics/cfg/models/11/yolo11m-obb_SENetV2.yaml"
    data_cfg_yaml = "person_93_labels.yaml"

    # batch --》 256
    args = dict(
                # detect, segment, classify, pose, obb
                task="obb",
                # 使用预训练权重进行训练
                #pretrained=pre_train_model,  
                #  冻结层, 跟模型结构model_cfg相关, 冻结11层, 冻结后12层开始训练
                freeze=None ,  
                model=model_cfg,
                data=data_cfg_yaml,

                epochs=500,
                imgsz=1024,
                device=device_list,      
                batch=16*len(device_list), 
                workers=16,
                patience=200,
                #  指定在训练的最后若干轮次(epoch)中禁用mosaic
                close_mosaic=20,
                # name 
                project=my_project,
                cache = False,
                # [SGD, Adam, Adamax, AdamW, NAdam, RAdam, RMSProp, auto]
                optimizer = "SGD",
                # 单类别训练
                single_cls=False,
                rect=False,
                multi_scale=False,
                label_smoothing=0.0,
                resume =False,
                
                hsv_h=0.015, 
                hsv_s=0.01,  
                hsv_v=0.01, 
                degrees =180,
                scale=0.2,
                flipud=0.5,
                fliplr=0.5,
                # 建议开启
                mosaic=1,

                translate = 0.0, #0.0,
                shear=0.0,      #0.0,
                perspective =0.0,
                bgr=0.0,
                # 范围 0.0 - 1.0 将两张图像及其标签混合,创建复合图像。通过引入标签噪声和视觉变化,提高模型的泛化能力。
                mixup=0.1,   #0.0,
                # 范围 0.0 - 1.0 将一个图像中的物体复制并粘贴到另一个图像中,有助于增加物体实例并学习物体遮挡。
                copy_paste = 0.2, #0.0,
                #  范围 0.1 - 1.0  将分类图像裁剪为其大小的一部分,以强调中心特征并适应物体尺度,减少背景干扰。1.0表示不裁剪,小于1.0表示裁剪。
                crop_fraction = 0.0,

                )
    trainer = OBBTrainer(overrides=args)
    trainer.train()

(1) 如果要从头训练, 就不继承模型finetune, 就不设置pre_train_mode参数和pretrained=pre_train_model。
(2) 如果不从头训练, 要继承模型finetune, 需要设置设置pre_train_mode参数和pretrained=pre_train_model。
但模型finetune时有个细节超参数需要注意, freeze=None 和 freeze=17(0-16层没有加上注意力机制), 是从头训练(freeze=None)还是冻结前17层(freeze=17), 由继承的模型和训练数据集决定。

我用我的数据训练了基座模型A, 用基座模型A微调小批量数据:
(1) 由于我的小批量的数据集改动了, 一些类别做了合并, 一些类别进行了拆分, 这样基座模型A的权重语义已经不再完美匹配小批量数据, 所以需要将freeze设置为None, 让模型继承基座模型A权重以后从头训练, 精度上限能更高。
(2) 但如果模型B对应的数据集没有改动, 那么冻结前17层(freeze=17)更合适, 只需要微调后面几层, 训练时间更快, 收敛更快。

无论上述小批量数据是属于哪种数据集方式, 精度都有可能出现上限瓶颈, 用下面的方式, 能有效提升整个模型精度。

第一阶段:freeze=17,直到验证集指标明显上升并趋稳。
目的:让随机初始化的 SENetV2 和检测头先进入合理区间。

第二阶段:freeze=None, 继承第一阶段权重继续训练到收敛。
目的:让 backbone 真正适配我们的数据,把精度上限拉上去。

请添加图片描述请添加图片描述

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐