MASF-YOLO 模型构建与验证全流程实现

论文地址https://arxiv.org/pdf/2504.18136
论文标题:MASF-YOLO: An Improved YOLOv11 Network for Small Object Detection on Drone View
在这里插入图片描述

通过论文已知,论文新增加了这三个模块,还有一个skip(这里没有实现),下面内容我们将讲解如何实现这个改进。

一、项目结构与模块划分

为优化无人机视角下的小目标检测性能,本文基于 YOLOv11 框架设计了 MASF-YOLO 模型,新增 MFAM(多尺度特征聚合模块)、IEMA(跨空间持久注意力模块)、DASI(深度感知尺度交互模块)及 CBS(卷积 - 归一化 - 激活模块)。项目结构遵循 YOLOv11 的模块化设计原则,核心文件分布如下:

├── ultralytics
│ 	└── print_model # 验证每一个模块是否可以单独执行,输入输出
│		└── c2psa.py
│		└── c3k2.py
│		└── cbs.py
│		└── dasi.py
│		└── iema.py
│		└── masf-yolo.py # 将每一个模块,按照yaml定义形式,用nn.ModuleList()连串起来,输入输出验证
│		└── mfam.py
│		└── sppf.py
│   └── ultralytics
│        └── nn
│            └── modules
│                ├── conv.py       # 定义CBS函数
│                └── block.py      # 定义MFAM,DASI,IEMA模块实现
│                └── __init__.py   # 导入新增模块
│			 └── tasks.py          # 修改parsel_model函数,导入定义模块
│		 └── cfg
│			 └── models
│				 └── custom_model
│					 └── masf-yolo.yaml # 定义yaml
│
├── train.py # 定义训练脚本
├── yolo11n.pt 

模块分类原则

  • 基础模块(conv.py):包含 CBS(继承自 YOLOv11 的Conv类,默认启用 SiLU 激活函数)、Concat 等基础操作。
  • 功能模块(block.py):实现 MFAM、DASI、IEMA 等复杂特征处理逻辑,依赖基础模块构建。
  • 配置文件(.yaml):按层级定义模型结构,通过模块名称与路径映射实现动态构建。

再撰写代码前,我们首先先参考yolov11的模块,了解yolov的结构模型是如何定义,它其实按照下图的线的顺序,定义模型结构,也就是说,yaml的编写也是按照如下顺序撰写的。

按照箭头的顺序,写模块,从input下面的第一个模块开始写,每一行就对应一层,第一层记做0层,从0开始

在这里插入图片描述

同理我们可以看下masf-yolo模型

在这里插入图片描述

知道了模型结构和编写顺序,就可以开始撰写yaml,按照图中的结构顺序,写出来

二、yaml撰写

masf-yolo.yaml

# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# MASF-YOLO 网络配置(最终修正Neck结构顺序)

# 模型核心参数
nc: 1  # 类别数
scales:
  n: [0.33, 0.25, 1024]
  s: [0.33, 0.50, 1024]
  m: [0.67, 0.75, 768]
  l: [1.00, 1.00, 512]
  x: [1.00, 1.25, 512]

# 骨干网络(Backbone):保持不变
backbone:
  - [-1, 1, CBS, [64, 3, 2]]  # 0: 初始卷积,输出64通道
  - [-1, 1, CBS, [128, 3, 2]]  # 1: 第二次卷积,输出128通道
  - [-1, 2, C3k2, [128, False, 0.25]]  # 2: C3k2模块,输出256通道
  - [-1, 1, MFAM, [128]] # 3: MFAM模块,增强特征
  
  - [-1, 1, CBS, [128, 3, 2]]  # 4: 卷积下采样,输出512通道
  - [-1, 2, C3k2, [256, False, 0.25]]  # 5: C3k2模块
  - [-1, 1, MFAM, [256]] # 6: MFAM模块
  
  - [-1, 1, CBS, [256, 3, 2]]  # 7: 卷积下采样,输出1024通道
  - [-1, 2, C3k2, [512, True]]  # 8: C3k2模块(带残差连接)
  - [-1, 1, MFAM, [512]]  # 9: MFAM模块
  
  - [-1, 1, CBS, [512, 3, 2]] # 10: 卷积下采样
  - [-1, 2, C3k2, [1024, True]]  # 11: C3k2模块
  - [-1, 1, SPPF, [1024, 5]]  # 12: SPPF多尺度池化
  - [-1, 2, C2PSA, [1024]]  # 13: C2PSA通道注意力
  - [-1, 1, MFAM, [1024]]  # 14: 最终MFAM特征增强

# 颈部网络(Neck):严格按照 DASI→C3k2→IEMA→... 顺序
head:
  # 第一个 DASI→C3k2→IEMA 模块组
  - [[-1, 6, 9], 1, DASI, [1024, 512, 256, 512]]  # 15: DASI模块,输入1024,输出1024
  - [-1, 1, C3k2, [512, False]]  # 16: C3k2模块
  - [-1, 1, IEMA, [512, 512, 16]]  # 17: IEMA模块(多尺度注意力)
  
  # 第二个 DASI→C3k2→IEMA 模块组
  - [[-1, 3, 6], 1, DASI, [512, 256, 128, 256]]  # 18: DASI模块,输入512,输出512
  - [-1, 1, C3k2, [256, False]]  # 19: C3k2模块
  - [-1, 1, IEMA, [256, 128, 16]]  # 20: IEMA模块
  
  # 上采样 + 拼接 + C3k2 + IEMA
  - [-1, 1, nn.Upsample, [None, 2, "nearest"]]  # 21: 上采样×2 输出128
  - [[-1, 3], 1, Concat, [1]]  # 22: 拼接第3层特征(128)
  - [-1, 1, C3k2, [256, False]]  # 23: C3k2模块
  - [-1, 1, IEMA, [256, 128, 16]]  # 24: IEMA模块
  
  # 拼接 + C3k2 + IEMA
  - [[-1, 3], 1, Concat, [1]]  # 25: 拼接第3层特征(128)
  - [-1, 1, C3k2, [256, False]]  # 26: C3k2模块
  - [-1, 1, IEMA, [256, 128, 16]]  # 27: IEMA模块
  
  # 卷积下采样 + 拼接 + C3k2 + IEMA
  - [-1, 1, CBS, [128, 3, 2]]  # 28: 卷积下采样,输出512
  - [[-1, 6], 1, Concat, [1]]  # 29: 拼接第6, 18层特征(256,512)
  - [-1, 1, C3k2, [512, False]]  # 30: C3k2模块
  - [-1, 1, IEMA, [512, 256, 16]]  # 31: IEMA模块
  
  # 卷积下采样 + 拼接 + C3k2 + IEMA
  - [-1, 1, CBS, [256, 3, 2]]  # 32: 卷积下采样,输出1024
  - [[-1, 16], 1, Concat, [1]]  # 33: 拼接第16层特征(1024)
  - [-1, 1, C3k2, [1024, False]]  # 34: C3k2模块
  - [-1, 1, IEMA, [1024, 512, 16]]  # 35: IEMA模块
  
  # 卷积下采样 + 拼接 + C3k2 + IEMA
  - [-1, 1, CBS, [512, 3, 2]]  # 36: 卷积下采样,输出1024
  - [[-1, 14], 1, Concat, [1]]  # 37: 拼接第14层特征(IEMA-1024)
  - [-1, 1, C3k2, [2048, False]]  # 38: C3k2模块
  - [-1, 1, IEMA, [2048, 1024, 16]]  # 39: IEMA模块

  - [[27, 31, 35, 39], 1, Detect, [nc]]  # 40: 检测层(融合P3/P4/P5特征)

备注:

我这个位置

  - [[-1, 16], 1, Concat, [1]]  # 33: 拼接第16层特征(1024),从模型上来看应该是拼接15的输出的,但是我输出的结果尺寸w,h不匹配

yaml中具体的尺寸和channels,可能与论文的有不同,论文没有具体说明使用的多大的通道,因此作者也只是参考yolov11.yaml做的改进

注意自己在这里面写yaml,需要知道每一个模块,输入和输出所需的尺寸和通道数,比如concat,nn.unsample模块

回到模块部分

上篇文章提到过,我们把yolov模块按照模块类型,定义到对应的文件里面
YOLO检测模型代码解构与改进方法论:从结构理解到模块设计
在这里插入图片描述
我们先看下yolov11.yaml的:

  • conv.py:Conv、Concat
  • block.py:C3k2、SPPF、C2PSA
  • head.py:Detect
    其中Upsample: torch.nn.Upsample

同理让我们看看这个论文的结构

从论文的结构图上看,有四个新模块CBS,MFAM,DASI,IEMA
按照分类:

  • conv.py:CBS
  • block.py:MFAM、DASI、IEMA
  • head.py:Detect

知道每一个模块对应是什么类型,就可以在合适的脚本下,定义模块。

二、核心模块设计与代码实现

1. CBS 模块:基础特征提取单元

CBS = 「卷积 + 归一化 + 激活」三件套

CBS 是 “Conv + BatchNorm + SiLU” 的缩写,是深度学习里最基础的特征提取单元,

模块命名规律总结:从名字猜功能

  1. 单模块:直接用操作 / 技术名
    Conv、DWConv、Concat、Upsample、MaxPool 等,名字即功能。
  2. 复合模块:用 “缩写 + 数字 + 功能描述”
    C3k2
    C3:继承 YOLOv5 的 C3 模块(Cross Stage Partial Network)。
    k2:表示内部堆叠 2 次 CBS 模块。
    SPPF
    SPP:空间金字塔池化(捕获多尺度特征)。
    F:Fast(快速版,通过并联池化层减少计算量)。
    C2PSA
    C2:类似 C3,但结构更简化。
    PSA:Position-Sensitive Attention(位置敏感注意力,增强对目标位置的感知)。

回到CBS模块结构上,其实就是yolov11里面的Conv

class Conv(nn.Module):
    """
    Standard convolution module with batch normalization and activation.

    Attributes:
        conv (nn.Conv2d): Convolutional layer.
        bn (nn.BatchNorm2d): Batch normalization layer.
        act (nn.Module): Activation function layer.
        default_act (nn.Module): Default activation function (SiLU).
    """

    default_act = nn.SiLU()  # default activation

    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
        """
        Initialize Conv layer with given parameters.

        Args:
            c1 (int): Number of input channels.
            c2 (int): Number of output channels.
            k (int): Kernel size.
            s (int): Stride.
            p (int, optional): Padding.
            g (int): Groups.
            d (int): Dilation.
            act (bool | nn.Module): Activation function.
        """
        super().__init__()
        self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False)
        self.bn = nn.BatchNorm2d(c2)
        self.act = self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity()

    def forward(self, x):
        """
        Apply convolution, batch normalization and activation to input tensor.

        Args:
            x (torch.Tensor): Input tensor.

        Returns:
            (torch.Tensor): Output tensor.
        """
        return self.act(self.bn(self.conv(x)))

    def forward_fuse(self, x):
        """
        Apply convolution and activation without batch normalization.

        Args:
            x (torch.Tensor): Input tensor.

        Returns:
            (torch.Tensor): Output tensor.
        """
        return self.act(self.conv(x))

当然我们还是可以继承Conv的模块,再定义一个(文件位置:ultralytics/nn/modules/conv.py)

class CBS(Conv):
    """
    Conv + BatchNorm + SiLU (default activation),即 YOLOv5 中的 CBS 模块。
    继承自 YOLOv11 的 Conv 类,默认使用 SiLU 激活函数。
    """
    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1):
        """
        初始化 CBS 模块(Conv+BN+SiLU)。
        
        Args:
            c1 (int): 输入通道数。
            c2 (int): 输出通道数。
            k (int): 卷积核大小。
            s (int): 步长。
            p (int, optional): 填充。
            g (int): 分组数。
            d (int): 膨胀率。
        """
        super().__init__(c1, c2, k, s, p, g, d, act=True)  # act=True 启用默认的 SiLU

2. MFAM 模块:多尺度特征聚合

结构设计:
模块包含 5 条并行分支,通过不同尺寸的深度可分离卷积(DWConv)捕获多尺度特征,最终通过 1x1 卷积融合:

  • 分支 1:3x3 DWConv
  • 分支 2:5x5 DWConv
  • 分支 3:1x7+7x1 级联 DWConv(模拟 14x14 感受野)
  • 分支 4:1x9+9x1 级联 DWConv(模拟 18x18 感受野)
  • 分支 5:1x1 DWConv(非恒等映射)

在这里插入图片描述
我发现yolov11中,这里的DWConv是有定义的

命名规律:DWConv Depthwise Convolution 深度可分离卷积(更高效的卷积) DW(深度)+ Conv
在这里插入图片描述
所以我们需要结合conv.py中的DWConv,在block.py中定义出来

第一步:在block.py中导入DWConv

from .conv import Conv, DWConv, GhostConv, LightConv, RepConv, autopad, DWConv

第二步

class MFAM(nn.Module):
    """
    多尺度特征聚合模块 (Multi-scale Feature Aggregation Module)
    严格对齐需求:
    - 分支1: DWConv 3x3
    - 分支2: DWConv 5x5
    - 分支3: DWConv 1x7 + 7x1
    - 分支4: DWConv 1x9 + 9x1
    - 分支5: 1x1深度卷积(非恒等映射)
    - 输出: 1x1 Conv 融合所有分支
    """
    def __init__(self, c1: int, c2: int):
        """
        初始化MFAM模块
        
        Args:
            c1 (int): 输入通道数
            c2 (int): 输出通道数
        """
        super().__init__()
        # 所有分支的通道数与输入一致
        self.c = c1
        
        # 分支1: DWConv 3x3
        self.dw_conv3x3 = DWConv(c1, c1, k=3, act=True)
        # 分支2: DWConv 5x5
        self.dw_conv5x5 = DWConv(c1, c1, k=5, act=True)
        # 分支3: DWConv 1x7 + 7x1
        self.dw_conv1x7 = DWConv(c1, c1, k=(1, 7), act=True)
        self.dw_conv7x1 = DWConv(c1, c1, k=(7, 1), act=True)
        # 分支4: DWConv 1x9 + 9x1
        self.dw_conv1x9 = DWConv(c1, c1, k=(1, 9), act=True)
        self.dw_conv9x1 = DWConv(c1, c1, k=(9, 1), act=True)
        # 分支5: 1x1深度卷积(非恒等映射,明确使用1x1 DWConv)
        self.dw_conv1x1 = DWConv(c1, c1, k=1, act=True)
        
        # 输出融合:1x1 Conv整合所有分支
        self.conv1x1 = Conv(c1 * 5, c2, k=1, act=True)  # 5个分支,每个c1通道
        
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        前向传播流程:
        1. 输入特征分5条分支处理
        2. 各分支结果相加(分支3和4是两个卷积的和)
        3. 所有分支结果拼接后用1x1 Conv融合
        """
        # 分支1: 3x3 DWConv
        x1 = self.dw_conv3x3(x)
        # 分支2: 5x5 DWConv
        x2 = self.dw_conv5x5(x)
        # 分支3: 1x7 + 7x1 DWConv(顺序执行)
        x3 = self.dw_conv7x1(self.dw_conv1x7(x))
        # 分支4: 1x9 + 9x1 DWConv(顺序执行)
        x4 = self.dw_conv9x1(self.dw_conv1x9(x))
        # 分支5: 1x1 DWConv(非恒等映射)
        x5 = self.dw_conv1x1(x)
        
        # 拼接所有分支结果(5个分支,每个c1通道)
        x_cat = torch.cat([x1, x2, x3, x4, x5], dim=1)
        
        # 1x1 Conv融合特征并调整通道数
        return self.conv1x1(x_cat)

写好,我建议最好自己验证一下一种就是通过代码
注意代码路径,因为整个项目函数的定义是在第一个ultralytics目录下实现的(一共有二个)

# vert_model.py
import torch
from ultralytics.nn.modules.block import MFAM
# 实例化模型(输入64通道,输出128通道)
model = MFAM(c1=64, c2=128)
# 输入特征图尺寸:(batch=1, channels=64, height=32, width=32)
x = torch.randn(1, 64, 32, 32)
# 前向传播
output = model(x)
print(output.shape)  # 输出: torch.Size([1, 128, 32, 32])       
output:torch.Size([1, 128, 32, 32])

还有一种模型可视化

通过HiddenLayer可视化网络

from ultralytics.nn.modules.block import MFAM  # 绝对导入
import torch
import hiddenlayer as h

def visualize_mfam():
    """使用hiddenlayer可视化MFAM模块的计算图"""
    # 1. 实例化MFAM模型
    model = MFAM(c1=64, c2=128)  # 输入64通道,输出128通道

    # 2. 准备输入数据(需与模型输入尺寸匹配)
    # 输入形状: (batch_size, channels, height, width)
    input_tensor = torch.zeros([1, 64, 32, 32])  # 假设输入特征图尺寸为64x32x32

    # 3. 使用hiddenlayer构建计算图
    vis_graph = h.build_graph(model, input_tensor)

    # 4. 设置主题样式(可选)
    vis_graph.theme = h.graph.THEMES["blue"].copy()  # 蓝色主题
    # 其他主题可选: "black", "white", "blue", "green", "red"

    # 5. 保存可视化结果
    save_path = "mfam_graph.png"
    vis_graph.save(save_path)
    print(f"计算图已保存至: {save_path}")

    # 6. 可选:在Jupyter Notebook中直接显示(如果是脚本运行可忽略)
    try:
        from IPython.display import Image
        return Image(save_path)
    except:
        pass

if __name__ == "__main__":
    visualize_mfam()

在这里插入图片描述

3. IEMA模块

核心逻辑
模块通过特征分组、并行分支和跨空间重加权增强小目标的空间特征感知:

  1. 水平 / 垂直池化:分别提取空间维度的全局上下文。
  2. 分组卷积:将特征图划分为groups个子空间,并行处理不同尺度(3x3、1x5、5x1 卷积)。
  3. 注意力重加权:通过 Softmax 生成空间注意力权重,强化有效特征区域。

在这里插入图片描述
代码

class IEMA(nn.Module):
    """
    特征分组与多分支处理模块
    """
    def __init__(self, c1: int, c2: int, groups: int = 16):
        super().__init__()
        self.groups = groups
        self.c1 = c1
        self.c2 = c2

        # Feature Grouping部分 - 修正池化操作
        self.x_avg_pool = nn.AdaptiveAvgPool2d((1, None))  # [B, C, 1, W]
        self.y_avg_pool = nn.AdaptiveAvgPool2d((None, 1))  # [B, C, H, 1]
        
        # 1x1卷积调整通道数
        self.x_conv = Conv(c1, c1, k=1, s=1, act=True)
        self.y_conv = Conv(c1, c1, k=1, s=1, act=True)
        
        # 合并后的卷积
        self.concat_conv = Conv(c1 * 2, c1, k=1, s=1, act=True)

        # Parallel Subnetworks部分
        branch_channels = c1 // groups 
        self.dwconv_3x3 = DWConv(branch_channels, branch_channels, k=3, s=1, act=True)
        self.dwconv_1x5 = DWConv(branch_channels, branch_channels, k=(1, 5), s=1, act=True)
        self.dwconv_5x1 = DWConv(branch_channels, branch_channels, k=(5, 1), s=1, act=True)
        
        self.identity = nn.Identity()
        merged_channels = branch_channels * 4
        self.merge_conv = Conv(merged_channels, c1, k=1, s=1, act=True)  # 使用普通Conv调整通道数

        # Cross-spatial Lasting部分
        self.group_norm = nn.GroupNorm(num_groups=groups, num_channels=c1)
        self.avg_pool_x = nn.AdaptiveAvgPool2d((1, None))
        self.avg_pool_y = nn.AdaptiveAvgPool2d((None, 1))
        self.softmax_x = nn.Softmax(dim=-1)
        self.softmax_y = nn.Softmax(dim=-2)

        # 后续重加权等操作
        self.sigmoid_1 = nn.Sigmoid()
        self.rewight_1 = nn.Conv2d(c1, c1, kernel_size=1, stride=1)
        self.sigmoid_2 = nn.Sigmoid()
        self.rewight_2 = nn.Conv2d(c1, c2, kernel_size=1, stride=1)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        前向传播逻辑
        """
        batch_size, channels, height, width = x.size()
        
        # 水平方向池化: [B, C, 1, W]
        x_x_pool = self.x_avg_pool(x)
        x_x_pool = self.x_conv(x_x_pool)
        
        # 垂直方向池化: [B, C, H, 1]
        x_y_pool = self.y_avg_pool(x)
        x_y_pool = self.y_conv(x_y_pool)
        
        # 关键修正: 将两个特征调整为相同尺寸
        x_x_pool = nn.functional.interpolate(x_x_pool, size=(height, width), mode='bilinear', align_corners=False)
        x_y_pool = nn.functional.interpolate(x_y_pool, size=(height, width), mode='bilinear', align_corners=False)
        
        # 现在可以安全地拼接
        concat_feature = torch.cat([x_x_pool, x_y_pool], dim=1)
        grouped_feature = self.concat_conv(concat_feature)
        
        # 张量拆分
        split_features = torch.split(grouped_feature, grouped_feature.size(1) // self.groups, dim=1)
        
        # 并行分支处理
        branch_3x3 = self.dwconv_3x3(split_features[0]) if split_features else None
        branch_1x5 = self.dwconv_1x5(split_features[1]) if len(split_features) > 1 else None
        branch_5x1 = self.dwconv_5x1(split_features[2]) if len(split_features) > 2 else None
        branch_identity = self.identity(split_features[3]) if len(split_features) > 3 else None

        # 合并分支
        merged_branches = [b for b in [branch_3x3, branch_1x5, branch_5x1, branch_identity] if b is not None]
        merged_feature = torch.cat(merged_branches, dim=1) if merged_branches else x
        merged_feature = self.merge_conv(merged_feature)

        # Cross-spatial Lasting
        gn_feature = self.group_norm(merged_feature)
        x_pool = self.avg_pool_x(gn_feature)
        y_pool = self.avg_pool_y(gn_feature)
        x_softmax = self.softmax_x(x_pool)
        y_softmax = self.softmax_y(y_pool)
        mul_x_feature = gn_feature * x_softmax
        mul_y_feature = gn_feature * y_softmax
        cross_feature = mul_x_feature + mul_y_feature

        # 重加权输出
        sigmoid_1_out = self.sigmoid_1(cross_feature)
        rewight_1_out = self.rewight_1(sigmoid_1_out)
        add_feature = x + rewight_1_out  # 残差连接
        sigmoid_2_out = self.sigmoid_2(add_feature)
        out = self.rewight_2(sigmoid_2_out)

        return out

4. DASI 模块:深度感知尺度交互

功能定位
模块通过融合高 / 中 / 低分辨率特征,解决无人机图像中目标尺度差异大的问题:

  • 输入:高层语义特征(低分辨率)、中层纹理特征、低层细节特征(高分辨率)。
  • 处理:通过上 / 下采样对齐尺寸,利用通道注意力机制动态加权融合。

在这里插入图片描述

class DASI(nn.Module):
    def __init__(self, in_channels_high, out_channels, in_channels_low=0, in_channels_mid=0):
        super(DASI, self).__init__()
        self.in_channels_high = in_channels_high
        self.out_channels = out_channels

        # 低层次特征处理(调整通道数)
        if in_channels_low == 0:
            self.conv_low = nn.Identity()
        else:
            self.conv_low = nn.Conv2d(in_channels_low, in_channels_high, kernel_size=3, stride=1, padding=1)

        # 中间特征处理(调整通道数)
        if in_channels_mid == 0:
            self.conv_mid = nn.Identity()
        else:
            self.conv_mid = nn.Conv2d(in_channels_mid, in_channels_high, kernel_size=1, stride=1)

        # 高层次特征处理(调整通道数 + 上采样到中间特征尺寸)
        self.conv_high = nn.Conv2d(in_channels_high, in_channels_high, kernel_size=1, stride=1)
        # 上采样因子根据中间特征尺寸动态计算(假设高层次尺寸是中间的 1/2)
        self.bilinear = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=False)  # 20->40

        # 注意力机制
        self.attention_conv = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Conv2d(in_channels_high, in_channels_high // 4, kernel_size=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(in_channels_high // 4, in_channels_high, kernel_size=1),
            nn.Sigmoid()
        )

        # 输出层
        self.merge_conv = nn.Conv2d(in_channels_high * 3, out_channels, kernel_size=1, stride=1)
        self.final_bn = nn.BatchNorm2d(out_channels)
        self.final_relu = nn.ReLU(inplace=True)

    def forward(self, x):
        x_high, x_low, x_mid = x

        # 1. 调整低层次特征:通道数 + 下采样到中间尺寸(80->40)
        x_low_processed = self.conv_low(x_low)  # 通道数: in_channels_high
        x_low_processed = F.interpolate(x_low_processed, size=x_mid.shape[2:], mode='bilinear')  # 80x80->40x40

        # 2. 调整中间特征:通道数(保持尺寸 40x40)
        x_mid_processed = self.conv_mid(x_mid)  # 通道数: in_channels_high

        # 3. 调整高层次特征:通道数 + 上采样到中间尺寸(20->40)
        x_high_processed = self.conv_high(x_high)  # 通道数: in_channels_high
        x_high_upsampled = self.bilinear(x_high_processed)  # 20x20->40x40

        # 4. 生成注意力权重(基于中间特征)
        attention = self.attention_conv(x_mid_processed)  # [B, in_channels_high, 1, 1]

        # 5. 特征融合(带注意力的加权求和)
        fused = (
            x_low_processed * attention +
            x_high_upsampled * (1 - attention) +
            x_mid_processed
        )

        # 6. 合并三分支特征(通道拼接)
        merged = torch.cat([x_low_processed, x_mid_processed, x_high_upsampled], dim=1)  # 通道数: 3*in_channels_high

        # 7. 输出处理:调整通道数 + 尺寸不变
        out = self.merge_conv(merged)  # 通道数: out_channels
        out = self.final_bn(out)
        out = self.final_relu(out)

        return out

三、模块验证与调试

定义完,最后再一次验证,如何验证需要将定义的所有模块,串联起来,拼成整个模型,看最后能不能通过输入一个随机的张量,能够输出

因为结合yolov的项目,如果报错,涉及的代码问题就更多了,首先自己先排查模型的问题

首先是各模块定义,我是把验证脚本,放在
ultralytics/ultralytics/nn/modules,最上面哪一层ultralytics的

在这里插入图片描述
为什么放这,因为yolo那个模块,定义都在是走的相对路径

为了方便管理,我在其目录下创建文件夹,把这些脚本都放进去,注意路径配置

import torch
import sys
from pathlib import Path

# 明确构造目标路径
target_path = Path("你的ultralytice绝对路径")
# 添加到 sys.path,确保能找到该目录下的模块
sys.path.insert(0, str(target_path))

# 可打印验证 sys.path 是否添加成功
print(sys.path)

在这里插入图片描述
在这里插入图片描述
yaml中还有concat和nn.unsample这二个就不做专门的验证了

class Concat(nn.Module):
    """
    Concatenate a list of tensors along specified dimension.

    Attributes:
        d (int): Dimension along which to concatenate tensors.
    """

    def __init__(self, dimension=1):
        """
        Initialize Concat module.

        Args:
            dimension (int): Dimension along which to concatenate tensors.
        """
        super().__init__()
        self.d = dimension

    def forward(self, x):
        """
        Concatenate input tensors along specified dimension.

        Args:
            x (List[torch.Tensor]): List of input tensors.

        Returns:
            (torch.Tensor): Concatenated tensor.
        """
        return torch.cat(x, self.d)

我们以如下为input:

  1. 标准输入尺寸(默认)
    常用尺寸:在 Ultralytics 框架中,YOLO 模型的标准输入尺寸通常为640×640像素(正方形输入),这是为了便于处理和保持各层特征的对称性。
    输入张量形状:[batch_size, 3, height, width],例如单张图片输入时为[1, 3, 640, 640]。

为了清晰展示模型各层的输入输出尺寸,以下是结合给定 YAML 配置的完整测试代码。代码会逐层打印输入和输出形状,帮助理解数据在模型中的流动过程:

在这里插入图片描述
代码如下

import torch
import sys
from pathlib import Path

# 明确构造目标路径
target_path = Path("第一个ultrayics绝对路径")
# 添加到 sys.path,确保能找到该目录下的模块
sys.path.insert(0, str(target_path))

# 可打印验证 sys.path 是否添加成功
print(sys.path)

# 后续如果有导入该目录下模块的操作,就可以正常执行了
# 比如假设该目录下有某个模块叫 xxx,就可以 import xxx


import torch
from typing import List, Union
import numpy as np

# 导入所需模块
from ultralytics.nn.modules.conv import CBS, Conv
from ultralytics.nn.modules.block import C3k2, MFAM, SPPF, DASI, C2PSA,IEMA
from ultralytics.nn.modules.conv import Concat  # 导入自定义Concat模块
import torch.nn as nn


# 模型配置
class Model(nn.Module):
    def __init__(self, nc=80):
        super().__init__()
        self.nc = nc
        self.layers = nn.ModuleList()
        self.froms = []
        self._build()

    def _build(self):
        # Backbone部分
        # 0: CBS[64, 3, 2]
        self.layers.append(CBS(3, 64, 3, 2))
        self.froms.append(-1)
        
        # 1: CBS[128, 3, 2]
        self.layers.append(CBS(64, 128, 3, 2))
        self.froms.append(-1)
        
        # 2: C3k2[256, False, 0.25]
        self.layers.append(C3k2(128, 128, n=2, c3k=False, e=0.25))
        self.froms.append(-1)
        
        # 3: MFAM[256]
        self.layers.append(MFAM(128, 128))  # 假设MFAM的c2=c1
        self.froms.append(-1)
        
        # 4: CBS[512, 3, 2]
        self.layers.append(CBS(128, 256, 3, 2))
        self.froms.append(-1)
        
        # 5: C3k2[512, False, 0.25]
        self.layers.append(C3k2(256, 256, n=2, c3k=False, e=0.25))
        self.froms.append(-1)
        
        # 6: MFAM[512]
        self.layers.append(MFAM(256,256))
        self.froms.append(-1)
        
        # 7: CBS[1024, 3, 2]
        self.layers.append(CBS(256, 512, 3, 2))
        self.froms.append(-1)
        
        # 8: C3k2[1024, True]
        self.layers.append(C3k2(512, 512, n=2, c3k=True))
        self.froms.append(-1)
        
        # 9: MFAM[1024]
        self.layers.append(MFAM(512, 512))
        self.froms.append(-1)
        
        # 10: CBS[1024, 3, 2]
        self.layers.append(CBS(512, 1024, 3, 2))
        self.froms.append(-1)
        
        # 11: C3k2[1024, True]
        self.layers.append(C3k2(1024, 1024, n=2, c3k=True))
        self.froms.append(-1)
        
        # 12: SPPF[1024, 5]
        self.layers.append(SPPF(1024, 1024, k=5))
        self.froms.append(-1)
        
        # 13: C2PSA[1024]
        self.layers.append(C2PSA(1024, 1024, n=2, e=0.5))
        self.froms.append(-1)
        
        # 14: MFAM[1024]
        self.layers.append(MFAM(1024, 1024))
        self.froms.append(-1)
        

        # Neck部分(重点修正DASI和Concat的输入来源)
        # 15: DASI模块,输入来自[-1,6,9]层(即第14、6、9层)
        # 注释:DASI的三个输入特征分别为:
        #       -1: 第14层(backbone最后一层MFAM-1024)
        #       6: 第6层(MFAM-512)
        #       9: 第9层(MFAM-1024)
        self.layers.append(DASI(in_channels_high=1024, out_channels=512, in_channels_low=256, in_channels_mid=512))  # 假设DASI输出通道为512
        self.froms.append([6, 9, -1])  # 修正:froms=[-1,6,9]中的-1表示前一层(第14层)
        
        # 16: C3k2模块,输入来自第15层
        self.layers.append(C3k2(512, 512, n=1, c3k=False))
        self.froms.append(-1)
        
        # 17: IEMA模块,输入来自第16层
        self.layers.append(IEMA(512, 512, 16))
        self.froms.append(-1)
        
        # 18: DASI模块,输入来自[-1,3,6]层(第17、3、6层)
        # 注释:DASI的三个输入特征分别为:
        #       -1: 第17层(IEMA-256)
        #       3: 第3层(MFAM-256)
        #       6: 第6层(MFAM-512)
        self.layers.append(DASI(in_channels_high=512, out_channels=256, in_channels_low=128, in_channels_mid=256))  # 假设DASI输出通道为256
        self.froms.append([3, 6, -1])  # 修正:froms=[-1,3,6]中的-1表示第17层
        
        # 19: C3k2模块,输入来自第18层
        self.layers.append(C3k2(256, 256, n=1, c3k=False))
        self.froms.append(-1)
        
        # 20: IEMA模块,输入来自第19层
        self.layers.append(IEMA(256, 128, 16))
        self.froms.append(-1)
        
        # 21: 上采样×2,输入来自第20层
        self.layers.append(nn.Upsample(scale_factor=2, mode='nearest'))
        self.froms.append(-1)
        
        # 22: Concat拼接第21层和第9层,输入来自[-1,9]层
        # 注释:Concat的两个输入特征分别为:
        #       -1: 第21层(上采样后的特征)
        #       9: 第9层(MFAM-1024)
        self.layers.append(Concat(dimension=1))
        self.froms.append([3, -1])  # 修正:froms=[-1,3]应为[-1,9](根据YAML注释)
        
        # 23: C3k2模块,输入来自第22层
        self.layers.append(C3k2(256, 256, n=1, c3k=False))
        self.froms.append(-1)
        
        # 24: IEMA模块,输入来自第23层
        self.layers.append(IEMA(256, 128, 16))
        self.froms.append(-1)
        
        # 25: Concat拼接第24层和第3层,输入来自[-1,3]层
        # 注释:Concat的两个输入特征分别为:
        #       -1: 第24层(IEMA-512)
        #       3: 第3层(MFAM-256)
        self.layers.append(Concat(dimension=1))
        self.froms.append([3, -1])  # 修正:froms=[-1,3]中的-1表示第24层
        
        # 26: C3k2模块,输入来自第25层
        self.layers.append(C3k2(256, 256, n=1, c3k=False))
        self.froms.append(-1)
        
        # 27: IEMA模块,输入来自第26层
        self.layers.append(IEMA(256, 128, 16))
        self.froms.append(-1)
        
        # 28: CBS下采样,输入来自第27层
        self.layers.append(CBS(128, 256, 3, 2))
        self.froms.append(-1)
        
        # 29: Concat拼接第28层和第18层,输入来自[-1,18]层
        # 注释:Concat的两个输入特征分别为:
        #       -1: 第28层(CBS-512)
        #       18: 第18层(DASI-256)
        self.layers.append(Concat(dimension=1))
        self.froms.append([6, -1])  # 修正:froms=[-1,18]中的-1表示第28层
        
        # 30: C3k2模块,输入来自第29层
        self.layers.append(C3k2(512, 512, n=1, c3k=False))
        self.froms.append(-1)
        
        # 31: IEMA模块,输入来自第30层
        self.layers.append(IEMA(512, 256, 16))
        self.froms.append(-1)
        
        # 32: CBS下采样,输入来自第31层
        self.layers.append(CBS(256, 512, 3, 2))
        self.froms.append(-1)
        
        # 33: Concat拼接第32层和第15层,输入来自[-1,15]层
        # 注释:Concat的两个输入特征分别为:
        #       -1: 第32层(CBS-512)
        #       15: 第15层(DASI-512)
        self.layers.append(Concat(dimension=1))
        self.froms.append([16, -1])  # 修正:froms=[-1,15]中的-1表示第32层
        
        # 34: C3k2模块,输入来自第33层
        self.layers.append(C3k2(1024, 1024, n=1, c3k=False))
        self.froms.append(-1)
        
        # 35: IEMA模块,输入来自第34层
        self.layers.append(IEMA(1024, 512, 16))
        self.froms.append(-1)
        
        # 36: CBS下采样,输入来自第35层
        self.layers.append(CBS(512, 1024, 3, 2))
        self.froms.append(-1)
        
        # 37: Concat拼接第36层和第14层,输入来自[-1,14]层
        # 注释:Concat的两个输入特征分别为:
        #       -1: 第36层(CBS-512)
        #       14: 第14层(MFAM-1024)
        self.layers.append(Concat(dimension=1))
        self.froms.append([14, -1])  # 修正:froms=[-1,14]中的-1表示第36层
        
        # 38: C3k2模块,输入来自第37层
        self.layers.append(C3k2(2048, 2048, n=1, c3k=False))
        self.froms.append(-1)
        
        # 39: IEMA模块,输入来自第38层
        self.layers.append(IEMA(2048, 1024, 16))
        self.froms.append(-1)

    def forward(self, x):
        outputs = [x]  # 存储各层输出,索引从0开始(输入为第0层)
        for i, (layer, frm) in enumerate(zip(self.layers, self.froms)):
            # 获取输入
            if isinstance(frm, int):
                # 单个输入,来自指定层(frm为正表示绝对索引,frm为负表示相对最后一层)
                layer_idx = frm if frm >= 0 else len(outputs) + frm
                inp = outputs[layer_idx]
                
                inp_source = f"层 {layer_idx}"
            else:
                # 多个输入,来自多个层
                layer_idxs = [f if f >= 0 else len(outputs) + f for f in frm]
                inp = [outputs[idx] for idx in layer_idxs]
                inp_source = f"层 {layer_idxs}"
                print(inp_source)
            # 前向传播
            if isinstance(layer, Concat):
                # Concat层需要列表输入
                tensor1, tensor2 = inp
                print(tensor1.shape)
                print(tensor2.shape)
                out = layer(inp)
            else:
                # DASI等模块需要多个张量输入(假设DASI的forward接收多个张量)
                if isinstance(inp, list) and len(inp) > 1:
                    out = layer(*inp)  # 解包多个输入张量
                else:
                    out = layer(inp)
            
            # 存储输出
            outputs.append(out)
            
            # 打印输入输出形状及来源
            in_shape = inp.shape if isinstance(inp, torch.Tensor) else [o.shape for o in inp]
            print(f"层 {i} ({layer.__class__.__name__}):")
            print(f"  输入来源: {inp_source}")
            print(f"  输入形状: {in_shape}")
            print(f"  输出形状: {out.shape}")
        
        return outputs[-1]

# 测试代码
if __name__ == "__main__":
    # 创建输入张量 [batch_size, channels, height, width]
    x = torch.randn(1, 3, 640, 640)
    
    # 实例化模型
    model = Model(nc=80)
    
    # 前向传播
    output = model(x)
    
    print("模型执行完成,最终输出形状:", output.shape)
# 测试代码
if __name__ == "__main__":
    # 创建输入张量 [batch_size, channels, height, width]
    x = torch.randn(1, 3, 640, 640)
    
    # 实例化模型
    model = Model(nc=80)
    
    # 前向传播
    output = model(x)
    
    print("模型执行完成,最终输出形状:", output.shape)

四、YOLOv11 框架集成

确定能够输出后,就剩下最后一步,在在这里插入图片描述
task.py文件中插入我们的模块就可以了

第一步

from ultralytics.nn.modules import IEMA,MFMA,CBS,DASI,DWConv

第二步,找到parsel_model函数
我上一篇文章已经说过了

把MFAM,CBS,DWConv插入到base_modules中

base_modules = frozenset(
        {
            Classify,
            Conv,
            ConvTranspose,
            GhostConv,
            Bottleneck,
            GhostBottleneck,
            SPP,
            SPPF,
            MFAM,
            CBS,
            C2fPSA,
            C2PSA,
            DWConv,
            Focus,
            BottleneckCSP,
            C1,
            C2,
            C2f,
            C3k2,
            RepNCSPELAN4,
            ELAN1,
            ADown,
            AConv,
            SPPELAN,
            C2fAttn,
            C3,
            C3TR,
            C3Ghost,
            torch.nn.ConvTranspose2d,
            DWConvTranspose2d,
            C3x,
            RepC3,
            PSA,
            SCDown,
            C2fCIB,
            A2C2f,
        }

增加代码,增加内容,我在代码中提供的注释

if m in base_modules:
            
    c1, c2 = ch[f], args[0]
    if c2 != nc:  # if c2 not equal to number of classes (i.e. for Classify() output)
        c2 = make_divisible(min(c2, max_channels) * width, 8)
    if m is C2fAttn:  # set 1) embed channels and 2) num heads
        args[1] = make_divisible(min(args[1], max_channels // 2) * width, 8)
        args[2] = int(max(round(min(args[2], max_channels // 2 // 32)) * width, 1) if args[2] > 1 else args[2])

    args = [c1, c2, *args[1:]]
    if m in repeat_modules:
        args.insert(2, n)  # number of repeats
        n = 1
    if m is C3k2:  # for M/L/X sizes
        legacy = False
        if scale in "mlx":
            args[3] = True
    if m is A2C2f:
        legacy = False
        if scale in "lx":  # for L/X sizes
            args.extend((True, 1.2))
    if m is C2fCIB:
        legacy = False

elif m is AIFI:
    args = [ch[f], *args]
elif m in frozenset({HGStem, HGBlock}):
    c1, cm, c2 = ch[f], args[0], args[1]
    args = [c1, cm, c2, *args[2:]]
    if m is HGBlock:
        args.insert(4, n)  # number of repeats
        n = 1
elif m is ResNetLayer:
    c2 = args[1] if args[3] else args[1] * 4
elif m is torch.nn.BatchNorm2d:
    args = [ch[f]]
elif m is Concat:

    c2 = sum(ch[x] for x in f)

#增加部分
#=============
elif m is DASI:
    # 假设DASI的from参数为[f_low, f_mid, f_high],对应低、中、高分辨率特征
    f_high, f_low, f_mid =  f  # 解包三个索引
    c1 = [ch[f_high], ch[f_mid], ch[f_low]]
    c2 = args[3]  # DASI模块的输出通道数(由args[0]指定,如512)
    c2 = make_divisible(min(c2, max_channels) * width, 8)
    args = [ch[f_high], ch[f_mid], ch[f_low], c2]
elif m is IEMA:
    c1, c2 = ch[f], args[0]
    if c2 != nc:  # if c2 not equal to number of classes (i.e. for Classify() output)
        c2 = make_divisible(min(c2, max_channels) * width, 8)
    args = [c1, c2, 16]
#==============

这部分新增代码的核心目的是扩展 YOLO 模型对自定义模块的支持

通过新增分支逻辑,使 YOLO 模型能够识别并构建自定义模块(DASI、IEMA),实现多尺度特征融合与注意力机制的集成,增强了模型的扩展性和灵活性。

五、训练

train.py

import os
os.environ['CUDA_VISIBLE_DEVICES'] = '2'
from ultralytics import YOLO

# 加载包含DASI模块的模型
model = YOLO('ultralytics/cfg/models/custom_model/masf-yolo.yaml').load("yolo11n.pt")

# 训练模型
model.train(data='data.yaml', epochs=100, batch=4)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

T1.Faker

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值