论文地址:https://arxiv.org/pdf/2504.18136
论文标题:MASF-YOLO: An Improved YOLOv11 Network for Small Object Detection on Drone View
通过论文已知,论文新增加了这三个模块,还有一个skip(这里没有实现),下面内容我们将讲解如何实现这个改进。
MASF-YOLO 模型构建与验证全流程实现
一、项目结构与模块划分
为优化无人机视角下的小目标检测性能,本文基于 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” 的缩写,是深度学习里最基础的特征提取单元,
模块命名规律总结:从名字猜功能
- 单模块:直接用操作 / 技术名
Conv、DWConv、Concat、Upsample、MaxPool 等,名字即功能。- 复合模块:用 “缩写 + 数字 + 功能描述”
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模块
核心逻辑
模块通过特征分组、并行分支和跨空间重加权增强小目标的空间特征感知:
- 水平 / 垂直池化:分别提取空间维度的全局上下文。
- 分组卷积:将特征图划分为groups个子空间,并行处理不同尺度(3x3、1x5、5x1 卷积)。
- 注意力重加权:通过 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:
- 标准输入尺寸(默认)
常用尺寸:在 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)