YOLOv11-seg改进 | C3k2_MambaOut门控大核卷积替换C3k2全流程指南
-
- 一、本文简介
-
- [1.1 原始 C3k2 的局限](#1.1 原始 C3k2 的局限)
- [1.2 C3k2_MambaOut 做了什么](#1.2 C3k2_MambaOut 做了什么)
- [1.3 实测参数量与 GFLOPs 对比](#1.3 实测参数量与 GFLOPs 对比)
- 二、模块原理详解
-
- [2.1 从底层到顶层的结构树](#2.1 从底层到顶层的结构树)
- [2.2 原始 C3k2 与 C3k2_MambaOut 的本质差异](#2.2 原始 C3k2 与 C3k2_MambaOut 的本质差异)
- [2.3 C3k2_MambaOut 的前向流程](#2.3 C3k2_MambaOut 的前向流程)
- [2.4 为什么它更适合实例分割](#2.4 为什么它更适合实例分割)
- 三、改进思想与创新点
-
- [3.1 背景与动机](#3.1 背景与动机)
- [3.2 核心创新点](#3.2 核心创新点)
- [3.3 与原始 C3k2 的对比](#3.3 与原始 C3k2 的对比)
- [3.4 在 YOLOv11 中的适配设计](#3.4 在 YOLOv11 中的适配设计)
- 四、完整代码
-
- [4.1 底层卷积封装](#4.1 底层卷积封装)
- [4.2 CSP 外壳与原始 C3k2 结构](#4.2 CSP 外壳与原始 C3k2 结构)
- [4.3 MambaOut 的底层门控块](#4.3 MambaOut 的底层门控块)
- [4.4 最终接入 YOLO 的封装类](#4.4 最终接入 YOLO 的封装类)
- 五、手把手配置步骤
-
- [5.1 第一步:检查 `extra_modules/init.py` 是否需要额外导入](#5.1 第一步:检查
extra_modules/__init__.py是否需要额外导入) - [5.2 第二步:检查 `tasks.py` 是否需要注册](#5.2 第二步:检查
tasks.py是否需要注册) - [5.3 第三步:训练命令示例](#5.3 第三步:训练命令示例)
- [5.1 第一步:检查 `extra_modules/init.py` 是否需要额外导入](#5.1 第一步:检查
- [六、YAML 配置文件](#六、YAML 配置文件)
-
- [6.1 变体一:全面替换](#6.1 变体一:全面替换)
- [6.2 变体二:仅替换 Backbone](#6.2 变体二:仅替换 Backbone)
- [6.3 变体三:精度优先模式](#6.3 变体三:精度优先模式)
- [6.4 变体四:混合模式](#6.4 变体四:混合模式)
- [6.5 变体五:P2 四尺度版](#6.5 变体五:P2 四尺度版)
- 七、常见问题
-
- [7.1 `NameError: name 'C3k2_MambaOut' is not defined`](#7.1
NameError: name 'C3k2_MambaOut' is not defined) - [7.2 需要安装哪些第三方依赖](#7.2 需要安装哪些第三方依赖)
- [7.3 如何把 `Segment` 版改成 `Detect` 版](#7.3 如何把
Segment版改成Detect版) - [7.4 YAML 参数怎么理解](#7.4 YAML 参数怎么理解)
- [7.1 `NameError: name 'C3k2_MambaOut' is not defined`](#7.1
- 八、总结
专栏系列 :YOLOv11 注意力/精度改进实战
改进点 :将 YOLOv11-seg 中的C3k2替换为基于 CVPR2025 MambaOut 的C3k2_MambaOut,核心是引入GatedCNNBlock_BCHW的门控通道混合、7x7深度卷积局部建模和LayerNormGeneral归一化增强,在参数量与 GFLOPs 小幅增加的前提下,提供更强的特征表达能力,更适合精度优先实验。
一、本文简介
本文引入 CVPR 2025 MambaOut《Do We Really Need Mamba for Vision?》 中的门控卷积思想,在当前仓库里对应的落地模块为 ultralytics/nn/backbone/MambaOut.py 中的 GatedCNNBlock_BCHW,并由 ultralytics/nn/extra_modules/block.py 封装成 C3k2_MambaOut。需要先说明一个关键事实:这个改进块虽然名字叫 MambaOut,但当前仓库接入的并不是状态空间扫描模块,而是 MambaOut 里的 BCHW 版门控卷积块 ,因此它本质上是一个更强的卷积混合器替换方案,而不是额外引入 mamba_ssm 的 SSM 结构。
1.1 原始 C3k2 的局限
YOLOv11-seg 默认的 C3k2 结构足够高效,但在实例分割任务里也有几个比较明显的瓶颈:
Bottleneck内部仍以固定深度卷积堆叠为主,局部感受野相对保守。- 通道混合主要依赖前后两次卷积,缺少显式的门控选择机制。
- 对复杂纹理、边界细节和中大目标区域的一致性建模能力有限。
1.2 C3k2_MambaOut 做了什么
当前仓库里的 C3k2_MambaOut 没有改动 C2f/C3k2 的外层拼接范式,而是在 self.m 内把原先的 Bottleneck 替换成了 GatedCNNBlock_BCHW。它带来三点核心变化:
- 门控混合 :先做通道扩展,再拆分出门控分支
g、直连分支i和卷积分支c。 - 大核局部建模 :卷积分支通过
7x7深度卷积增强局部空间感受野。 - BCHW 原生归一化 :通过
LayerNormGeneral直接在BCHW张量上完成归一化,避免多余张量布局转换。
1.3 实测参数量与 GFLOPs 对比
本文没有臆造精度结果,只对当前仓库环境下的模型结构进行了真实测量。测试命令如下:
python
from ultralytics import YOLO
YOLO("ultralytics/cfg/models/11-seg/yolo11-seg.yaml").info()
YOLO("ultralytics/cfg/models/11-seg/light_impro/yolo11-seg-C3k2-MambaOut.yaml").info()
实测结果如下:
| 模型 | 参数量 | GFLOPs | 测量说明 |
|---|---|---|---|
yolo11n-seg |
2.46M(2,460,112) |
9.6 |
2026-04-21 复测,与 2026-04-18 既有基线一致 |
yolo11n-seg-C3k2-MambaOut |
2.63M(2,627,218) |
10.3 |
2026-04-21 实测 |
| 变化 | +167,106(+6.79%) |
+0.7(+7.29%) |
典型精度优先型替换块 |
从结构与复杂度看,
C3k2_MambaOut明显不是轻量化方向,而是用少量额外计算换更强表征 的精度向改进。至于是否真正涨点,需要在你的实例分割数据集上训练后再看mAP和mask mAP。
二、模块原理详解
2.1 从底层到顶层的结构树
text
C3k2_MambaOut
├─ 继承自 C3k2
│ ├─ 继承自 C2f
│ │ ├─ cv1: Conv(c1, 2c, 1)
│ │ ├─ cv2: Conv((2+n)c, c2, 1)
│ │ └─ m: ModuleList(...)
│ └─ 将原始 Bottleneck 替换为 GatedCNNBlock_BCHW
├─ C3k_MambaOut
│ └─ 在 C3k 的内部堆叠 GatedCNNBlock_BCHW
└─ GatedCNNBlock_BCHW
├─ norm: LayerNormGeneral((C,1,1))
├─ fc1: 1x1 Conv,通道扩展到 2h
├─ split: [g, i, c]
├─ conv: 7x7 Depthwise Conv(c)
├─ act: GELU(g)
├─ fc2: 1x1 Conv,投影回 C
└─ residual: x + DropPath(...)
2.2 原始 C3k2 与 C3k2_MambaOut 的本质差异
原始 C3k2 的核心单元还是 Bottleneck:
B o t t l e n e c k ( x ) = x + D W C o n v 2 ( D W C o n v 1 ( x ) ) \mathrm{Bottleneck}(x)=x+\mathrm{DWConv}_2(\mathrm{DWConv}_1(x)) Bottleneck(x)=x+DWConv2(DWConv1(x))
而 C3k2_MambaOut 的核心单元换成了 GatedCNNBlock_BCHW:
h = ⌊ 8 3 C ⌋ , C conv = r C h=\left\lfloor \frac{8}{3}C \right\rfloor,\quad C_{\text{conv}}=rC h=⌊38C⌋,Cconv=rC
g , i , c = S p l i t ( C o n v 1 × 1 ( L N ( x ) ) , h , h − C conv , C conv ) g,i,c=\mathrm{Split}\left(\mathrm{Conv}_{1\times1}(\mathrm{LN}(x)),\ h,\\ h-C_{\\text{conv}},\\ C_{\\text{conv}}\right) g,i,c=Split(Conv1×1(LN(x)), h, h−Cconv, Cconv)
c ~ = D W C o n v 7 × 7 ( c ) \tilde{c}=\mathrm{DWConv}_{7\times7}(c) c~=DWConv7×7(c)
y = C o n v 1 × 1 ( G E L U ( g ) ⊙ C a t ( i , c ~ ) ) y=\mathrm{Conv}_{1\times1}\left(\mathrm{GELU}(g)\odot \mathrm{Cat}(i,\tilde{c})\right) y=Conv1×1(GELU(g)⊙Cat(i,c~))
G a t e d C N N B l o c k _ B C H W ( x ) = x + D r o p P a t h ( y ) \mathrm{GatedCNNBlock\_BCHW}(x)=x+\mathrm{DropPath}(y) GatedCNNBlock_BCHW(x)=x+DropPath(y)
这里有两个非常值得注意的点:
- 当前仓库默认
conv_ratio=1.0,也就是卷积分支会使用完整的卷积分支通道,而不是只卷积少量通道。 GatedCNNBlock_BCHW不依赖mamba_ssm,而是一个标准卷积体系内可运行的门控块。
2.3 C3k2_MambaOut 的前向流程
外层 C3k2_MambaOut 仍沿用 C2f 思路,流程可以写成:
python
def forward(x):
y = list(cv1(x).chunk(2, 1))
y.extend(block(y[-1]) for block in m)
return cv2(cat(y, 1))
把它展开成更直观的形式就是:
y 0 , y 1 = C h u n k ( C o n v 1 × 1 ( x ) , 2 ) y_0,y_1=\mathrm{Chunk}(\mathrm{Conv}_{1\times1}(x),2) y0,y1=Chunk(Conv1×1(x),2)
y k + 2 = G ( y k + 1 ) , G = G a t e d C N N B l o c k _ B C H W y_{k+2}=\mathcal{G}(y_{k+1}),\quad \mathcal{G}=\mathrm{GatedCNNBlock\_BCHW} yk+2=G(yk+1),G=GatedCNNBlock_BCHW
o u t = C o n v 1 × 1 ( C a t ( y 0 , y 1 , ... , y n + 1 ) ) \mathrm{out}=\mathrm{Conv}{1\times1}\left(\mathrm{Cat}(y_0,y_1,\ldots,y{n+1})\right) out=Conv1×1(Cat(y0,y1,...,yn+1))
也就是说,外层的跨层聚合不变,变化发生在内部特征混合器。这也是它最适合在 YOLOv11 里做"最小侵入替换"的原因。
2.4 为什么它更适合实例分割
实例分割比纯检测更依赖边缘和区域一致性。GatedCNNBlock_BCHW 的优势恰好对应这类需求:
7x7深度卷积比原来的固定小核更有利于整合局部区域结构。- 门控分支让哪些通道应该被强化、哪些通道应该更直接保留,拥有更细的选择能力。
C2f外层拼接保留了 YOLO 系列一贯的梯度流优势,不会像大改主干那样破坏整体训练稳定性。
三、改进思想与创新点
3.1 背景与动机
MambaOut 这篇工作的核心观点非常有代表性:很多视觉任务里,真正有效的并不一定是"必须上状态空间模型",而是找到一种更合适的 token mixer 。因此,当前仓库没有把完整 MambaOut 分类骨干直接搬进 C3k2,而是抽取了里面最关键的 GatedCNNBlock_BCHW 作为增强块,这个选择非常工程化:
- 保留 YOLOv11 原有的
C3k2/C2f外壳,兼容现有 YAML 写法。 - 只替换内部混合单元,训练与部署成本更低。
- 不额外引入
mamba_ssm这类编译型依赖,更适合 Windows/通用 PyTorch 环境。
3.2 核心创新点
创新点一:门控式通道重组,而不是无条件卷积堆叠
传统 Bottleneck 会对所有隐藏通道做相近形式的卷积处理;GatedCNNBlock_BCHW 则先把特征拆为 g / i / c 三部分,再通过门控进行选择性交互:
m i x = G E L U ( g ) ⊙ C a t ( i , c ~ ) \mathrm{mix}=\mathrm{GELU}(g)\odot \mathrm{Cat}(i,\tilde{c}) mix=GELU(g)⊙Cat(i,c~)
这种设计意味着模型不是"把所有通道一视同仁地卷一遍",而是显式区分门控控制分支、保留分支和空间混合分支。
创新点二:大核深度卷积增强局部空间建模
卷积分支使用:
c ~ = D W C o n v 7 × 7 ( c ) \tilde{c}=\mathrm{DWConv}_{7\times7}(c) c~=DWConv7×7(c)
相比原始 Bottleneck 中常见的小核深度卷积,7x7 更容易覆盖边缘、纹理和局部形状上下文,这对实例分割中的 mask 边界质量更友好。
创新点三:BCHW 原生 LayerNorm 适配检测/分割骨干
当前实现没有使用 BHWC 版本的块,而是采用 GatedCNNBlock_BCHW,核心归一化形式为:
L N ( x ) = x − μ σ 2 + ϵ ⋅ γ + β \mathrm{LN}(x)=\frac{x-\mu}{\sqrt{\sigma^2+\epsilon}}\cdot \gamma+\beta LN(x)=σ2+ϵ x−μ⋅γ+β
其中均值与方差在 (1,2,3) 维上统计,更符合 YOLO 主体在 BCHW 布局下的工程使用习惯。
创新点四:以最小改动融入 YOLOv11
仓库里的适配代码非常克制:
python
self.m = nn.ModuleList(C3k_MambaOut(self.c, self.c, n, shortcut, g) if c3k else GatedCNNBlock_BCHW(self.c) for _ in range(n))
这意味着:
- 输入输出张量形状不变。
- 原有层索引体系保持稳定。
- 可以只改 YAML,不需要重写 head。
3.3 与原始 C3k2 的对比
| 对比维度 | 原始 C3k2 |
C3k2_MambaOut |
实际影响 |
|---|---|---|---|
| 核心单元 | Bottleneck |
GatedCNNBlock_BCHW |
内部混合器更强 |
| 归一化方式 | BN 为主 |
LayerNormGeneral + BN 外壳 |
通道统计方式更灵活 |
| 局部感受野 | 常规小核深度卷积 | 7x7 深度卷积 |
边缘/区域建模更充分 |
| 通道交互 | 固定卷积映射 | 门控 + 分支重组 | 选择性更强 |
| 复杂度方向 | 偏效率 | 偏表征 | 更适合精度优先实验 |
| 参数与 GFLOPs | 更低 | 实测 +6.79% params、+7.29% GFLOPs |
需要接受小幅算力上涨 |
| 适用场景 | 通用检测/分割 | 纹理复杂、边界要求高的分割任务 | 更建议做精度尝试 |
3.4 在 YOLOv11 中的适配设计
当前仓库的接入状态已经是完整可用的:
ultralytics/nn/extra_modules/block.py已经定义C3k_MambaOut和C3k2_MambaOut。ultralytics/nn/extra_modules/__init__.py已经通过from .block import *暴露该模块。ultralytics/nn/tasks.py已经导入from ultralytics.nn.backbone.MambaOut import *,并把C3k2_MambaOut加入C3K2_CLASS。
因此这篇文章的重点不是"如何手动补注册",而是如何正确理解、正确替换、正确写 YAML。
四、完整代码
下面代码块按"底层卷积封装 -> CSP 外壳 -> MambaOut 门控块 -> YOLO 封装块"的顺序给出,代码内容与仓库源码保持一致,只在代码块外补充中文说明,便于你快速定位。
4.1 底层卷积封装
文件路径:ultralytics/nn/modules/conv.py
python
import math
import numpy as np
import torch
import torch.nn as nn
def autopad(k, p=None, d=1): # kernel, padding, dilation
"""Pad to 'same' shape outputs."""
if d > 1:
k = d * (k - 1) + 1 if isinstance(k, int) else [d * (x - 1) + 1 for x in k] # actual kernel-size
if p is None:
p = k // 2 if isinstance(k, int) else [x // 2 for x in k] # auto-pad
return p
class Conv(nn.Module):
"""Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation)."""
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 arguments including activation."""
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."""
return self.act(self.bn(self.conv(x)))
def forward_fuse(self, x):
"""Perform transposed convolution of 2D data."""
return self.act(self.conv(x))
class DWConv(Conv):
"""Depth-wise convolution."""
def __init__(self, c1, c2, k=1, s=1, d=1, act=True): # ch_in, ch_out, kernel, stride, dilation, activation
"""Initialize Depth-wise convolution with given parameters."""
super().__init__(c1, c2, k, s, g=math.gcd(c1, c2), d=d, act=act)
4.2 CSP 外壳与原始 C3k2 结构
文件路径:ultralytics/nn/modules/block.py
python
class C2f(nn.Module):
"""Faster Implementation of CSP Bottleneck with 2 convolutions."""
def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5):
"""Initializes a CSP bottleneck with 2 convolutions and n Bottleneck blocks for faster processing."""
super().__init__()
self.c = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, 2 * self.c, 1, 1)
self.cv2 = Conv((2 + n) * self.c, c2, 1) # optional act=FReLU(c2)
self.m = nn.ModuleList(Bottleneck(self.c, self.c, shortcut, g, k=((3, 3), (3, 3)), e=1.0) for _ in range(n))
def forward(self, x):
"""Forward pass through C2f layer."""
y = list(self.cv1(x).chunk(2, 1))
y.extend(m(y[-1]) for m in self.m)
return self.cv2(torch.cat(y, 1))
def forward_split(self, x):
"""Forward pass using split() instead of chunk()."""
y = list(self.cv1(x).split((self.c, self.c), 1))
y.extend(m(y[-1]) for m in self.m)
return self.cv2(torch.cat(y, 1))
class C3(nn.Module):
"""CSP Bottleneck with 3 convolutions."""
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
"""Initialize the CSP Bottleneck with given channels, number, shortcut, groups, and expansion values."""
super().__init__()
c_ = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, c_, 1, 1)
self.cv2 = Conv(c1, c_, 1, 1)
self.cv3 = Conv(2 * c_, c2, 1) # optional act=FReLU(c2)
self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, k=((1, 1), (3, 3)), e=1.0) for _ in range(n)))
def forward(self, x):
"""Forward pass through the CSP bottleneck with 2 convolutions."""
return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), 1))
class Bottleneck(nn.Module):
"""Standard bottleneck."""
def __init__(self, c1, c2, shortcut=True, g=1, k=(3, 3), e=0.5):
"""Initializes a standard bottleneck module with optional shortcut connection and configurable parameters."""
super().__init__()
c_ = int(c2 * e) # hidden channels
self.cv1 = DWConv(c1, c_, k[0], 1)
self.cv2 = DWConv(c_, c2, k[1], 1)
self.add = shortcut and c1 == c2
def forward(self, x):
"""Applies the YOLO FPN to input data."""
return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
class C3k2(C2f):
"""Faster Implementation of CSP Bottleneck with 2 convolutions."""
def __init__(self, c1, c2, n=1, c3k=False, e=0.5, g=1, shortcut=True):
"""Initializes the C3k2 module, a faster CSP Bottleneck with 2 convolutions and optional C3k blocks."""
super().__init__(c1, c2, n, shortcut, g, e)
self.m = nn.ModuleList(
C3k(self.c, self.c, 2, shortcut, g) if c3k else Bottleneck(self.c, self.c, shortcut, g) for _ in range(n)
)
class C3k(C3):
"""C3k is a CSP bottleneck module with customizable kernel sizes for feature extraction in neural networks."""
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5, k=3):
"""Initializes the C3k module with specified channels, number of layers, and configurations."""
super().__init__(c1, c2, n, shortcut, g, e)
c_ = int(c2 * e) # hidden channels
# self.m = nn.Sequential(*(RepBottleneck(c_, c_, shortcut, g, k=(k, k), e=1.0) for _ in range(n)))
self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, k=(k, k), e=1.0) for _ in range(n)))
4.3 MambaOut 的底层门控块
文件路径:ultralytics/nn/backbone/MambaOut.py
python
from functools import partial
import torch
import torch.nn as nn
import torch.nn.functional as F
from timm.layers import trunc_normal_, DropPath
from timm.data import IMAGENET_DEFAULT_MEAN, IMAGENET_DEFAULT_STD
class LayerNormGeneral(nn.Module):
r""" General LayerNorm for different situations.
Args:
affine_shape (int, list or tuple): The shape of affine weight and bias.
Usually the affine_shape=C, but in some implementation, like torch.nn.LayerNorm,
the affine_shape is the same as normalized_dim by default.
To adapt to different situations, we offer this argument here.
normalized_dim (tuple or list): Which dims to compute mean and variance.
scale (bool): Flag indicates whether to use scale or not.
bias (bool): Flag indicates whether to use scale or not.
We give several examples to show how to specify the arguments.
LayerNorm (https://arxiv.org/abs/1607.06450):
For input shape of (B, *, C) like (B, N, C) or (B, H, W, C),
affine_shape=C, normalized_dim=(-1, ), scale=True, bias=True;
For input shape of (B, C, H, W),
affine_shape=(C, 1, 1), normalized_dim=(1, ), scale=True, bias=True.
Modified LayerNorm (https://arxiv.org/abs/2111.11418)
that is idental to partial(torch.nn.GroupNorm, num_groups=1):
For input shape of (B, N, C),
affine_shape=C, normalized_dim=(1, 2), scale=True, bias=True;
For input shape of (B, H, W, C),
affine_shape=C, normalized_dim=(1, 2, 3), scale=True, bias=True;
For input shape of (B, C, H, W),
affine_shape=(C, 1, 1), normalized_dim=(1, 2, 3), scale=True, bias=True.
For the several metaformer baslines,
IdentityFormer, RandFormer and PoolFormerV2 utilize Modified LayerNorm without bias (bias=False);
ConvFormer and CAFormer utilizes LayerNorm without bias (bias=False).
"""
def __init__(self, affine_shape=None, normalized_dim=(-1, ), scale=True,
bias=True, eps=1e-5):
super().__init__()
self.normalized_dim = normalized_dim
self.use_scale = scale
self.use_bias = bias
self.weight = nn.Parameter(torch.ones(affine_shape)) if scale else None
self.bias = nn.Parameter(torch.zeros(affine_shape)) if bias else None
self.eps = eps
def forward(self, x):
c = x - x.mean(self.normalized_dim, keepdim=True)
s = c.pow(2).mean(self.normalized_dim, keepdim=True)
x = c / torch.sqrt(s + self.eps)
if self.use_scale:
x = x * self.weight
if self.use_bias:
x = x + self.bias
return x
class GatedCNNBlock_BCHW(nn.Module):
r""" Our implementation of Gated CNN Block: https://arxiv.org/pdf/1612.08083
Args:
conv_ratio: control the number of channels to conduct depthwise convolution.
Conduct convolution on partial channels can improve practical efficiency.
The idea of partial channels is from ShuffleNet V2 (https://arxiv.org/abs/1807.11164) and
also used by InceptionNeXt (https://arxiv.org/abs/2303.16900) and FasterNet (https://arxiv.org/abs/2303.03667)
"""
def __init__(self, dim, expansion_ratio=8/3, kernel_size=7, conv_ratio=1.0,
norm_layer=partial(LayerNormGeneral,eps=1e-6,normalized_dim=(1, 2, 3)),
act_layer=nn.GELU,
drop_path=0.,
**kwargs):
super().__init__()
self.norm = norm_layer((dim, 1, 1))
hidden = int(expansion_ratio * dim)
self.fc1 = nn.Conv2d(dim, hidden * 2, 1)
self.act = act_layer()
conv_channels = int(conv_ratio * dim)
self.split_indices = (hidden, hidden - conv_channels, conv_channels)
self.conv = nn.Conv2d(conv_channels, conv_channels, kernel_size=kernel_size, padding=kernel_size//2, groups=conv_channels)
self.fc2 = nn.Conv2d(hidden, dim, 1)
self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
def forward(self, x):
shortcut = x # [B, H, W, C]
x = self.norm(x)
g, i, c = torch.split(self.fc1(x), self.split_indices, dim=1)
# c = c.permute(0, 3, 1, 2) # [B, H, W, C] -> [B, C, H, W]
c = self.conv(c)
# c = c.permute(0, 2, 3, 1) # [B, C, H, W] -> [B, H, W, C]
x = self.fc2(self.act(g) * torch.cat((i, c), dim=1))
x = self.drop_path(x)
return x + shortcut
4.4 最终接入 YOLO 的封装类
文件路径:ultralytics/nn/extra_modules/block.py
python
class C3k_MambaOut(C3k):
def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5, k=3):
super().__init__(c1, c2, n, shortcut, g, e, k)
c_ = int(c2 * e) # hidden channels
self.m = nn.Sequential(*(GatedCNNBlock_BCHW(c_) for _ in range(n)))
class C3k2_MambaOut(C3k2):
def __init__(self, c1, c2, n=1, c3k=False, e=0.5, g=1, shortcut=True):
super().__init__(c1, c2, n, c3k, e, g, shortcut)
self.m = nn.ModuleList(C3k_MambaOut(self.c, self.c, n, shortcut, g) if c3k else GatedCNNBlock_BCHW(self.c) for _ in range(n))
五、手把手配置步骤
5.1 第一步:检查 extra_modules/__init__.py 是否需要额外导入
当前仓库不需要修改。
原因如下:
ultralytics/nn/extra_modules/__init__.py已经包含from .block import *。ultralytics/nn/extra_modules/block.py的__all__已经包含C3k2_MambaOut。
所以如果你直接使用当前仓库,这一步跳过即可。
5.2 第二步:检查 tasks.py 是否需要注册
当前仓库也不需要修改。
你已经具备以下条件:
ultralytics/nn/tasks.py已经导入from ultralytics.nn.backbone.MambaOut import *。ultralytics/nn/tasks.py的C3K2_CLASS已经包含C3k2_MambaOut。
也就是说,YOLO("...yaml") 已经能够正确解析这个模块名。
5.3 第三步:训练命令示例
python
from ultralytics import YOLO
model = YOLO("ultralytics/cfg/models/11-seg/light_impro/yolo11-seg-C3k2-MambaOut.yaml")
model.train(
data="coco8-seg.yaml",
epochs=100,
imgsz=640,
batch=16,
device=0
)
如果你是自定义分割数据集,只需要把 data 换成自己的 xxx-seg.yaml 即可。
六、YAML 配置文件
下面给出 5 套可直接参考的配置方式。为了避免层索引错误,我把每个层号都重新核对了一遍,
Concat与Segment的输入索引也都按当前结构重新计算。
6.1 变体一:全面替换
适用场景:你想直接复现当前仓库的 light_impro 版本,让 Backbone 和 Head 都统一换成 C3k2_MambaOut。
yaml
# YOLO11-seg C3k2_MambaOut full replacement
nc: 80
scales:
n: [0.50, 0.25, 1024]
s: [0.50, 0.50, 1024]
m: [0.50, 1.00, 512]
l: [1.00, 1.00, 512]
x: [1.00, 1.50, 512]
backbone:
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
- [-1, 2, C3k2_MambaOut, [256, False, 0.25]] # 2
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 2, C3k2_MambaOut, [512, False, 0.25]] # 4
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 2, C3k2_MambaOut, [512, True]] # 6
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 2, C3k2_MambaOut, [1024, True]] # 8
- [-1, 1, SPPF, [1024, 5]] # 9
- [-1, 2, C2PSA, [1024]] # 10
head:
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 11
- [[-1, 6], 1, Concat, [1]] # 12 cat backbone P4
- [-1, 2, C3k2_MambaOut, [512, False]] # 13
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 14
- [[-1, 4], 1, Concat, [1]] # 15 cat backbone P3
- [-1, 2, C3k2_MambaOut, [256, False]] # 16 (P3/8-small)
- [-1, 1, Conv, [256, 3, 2]] # 17
- [[-1, 13], 1, Concat, [1]] # 18 cat head P4
- [-1, 2, C3k2_MambaOut, [512, False]] # 19 (P4/16-medium)
- [-1, 1, Conv, [512, 3, 2]] # 20
- [[-1, 10], 1, Concat, [1]] # 21 cat head P5
- [-1, 2, C3k2_MambaOut, [1024, True]] # 22 (P5/32-large)
- [[16, 19, 22], 1, Segment, [nc, 32, 256]] # 23 Segment(P3, P4, P5)
6.2 变体二:仅替换 Backbone
适用场景:你想先验证主干侧的收益,而不动分割 Head,便于做消融实验。
yaml
# YOLO11-seg C3k2_MambaOut backbone only
nc: 80
scales:
n: [0.50, 0.25, 1024]
s: [0.50, 0.50, 1024]
m: [0.50, 1.00, 512]
l: [1.00, 1.00, 512]
x: [1.00, 1.50, 512]
backbone:
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
- [-1, 2, C3k2_MambaOut, [256, False, 0.25]] # 2
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 2, C3k2_MambaOut, [512, False, 0.25]] # 4
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 2, C3k2_MambaOut, [512, True]] # 6
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 2, C3k2_MambaOut, [1024, True]] # 8
- [-1, 1, SPPF, [1024, 5]] # 9
- [-1, 2, C2PSA, [1024]] # 10
head:
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 11
- [[-1, 6], 1, Concat, [1]] # 12 cat backbone P4
- [-1, 2, C3k2, [512, False]] # 13
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 14
- [[-1, 4], 1, Concat, [1]] # 15 cat backbone P3
- [-1, 2, C3k2, [256, False]] # 16 (P3/8-small)
- [-1, 1, Conv, [256, 3, 2]] # 17
- [[-1, 13], 1, Concat, [1]] # 18 cat head P4
- [-1, 2, C3k2, [512, False]] # 19 (P4/16-medium)
- [-1, 1, Conv, [512, 3, 2]] # 20
- [[-1, 10], 1, Concat, [1]] # 21 cat head P5
- [-1, 2, C3k2, [1024, True]] # 22 (P5/32-large)
- [[16, 19, 22], 1, Segment, [nc, 32, 256]] # 23 Segment(P3, P4, P5)
6.3 变体三:精度优先模式
适用场景:显存和算力比较充足,想把 c3k=True 也打开,让内部走 C3k_MambaOut 分支。
yaml
# YOLO11-seg C3k2_MambaOut accuracy-first
nc: 80
scales:
n: [0.50, 0.25, 1024]
s: [0.50, 0.50, 1024]
m: [0.50, 1.00, 512]
l: [1.00, 1.00, 512]
x: [1.00, 1.50, 512]
backbone:
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
- [-1, 2, C3k2_MambaOut, [256, True, 0.25]] # 2
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 2, C3k2_MambaOut, [512, True, 0.25]] # 4
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 2, C3k2_MambaOut, [512, True]] # 6
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 2, C3k2_MambaOut, [1024, True]] # 8
- [-1, 1, SPPF, [1024, 5]] # 9
- [-1, 2, C2PSA, [1024]] # 10
head:
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 11
- [[-1, 6], 1, Concat, [1]] # 12 cat backbone P4
- [-1, 2, C3k2_MambaOut, [512, True]] # 13
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 14
- [[-1, 4], 1, Concat, [1]] # 15 cat backbone P3
- [-1, 2, C3k2_MambaOut, [256, True]] # 16 (P3/8-small)
- [-1, 1, Conv, [256, 3, 2]] # 17
- [[-1, 13], 1, Concat, [1]] # 18 cat head P4
- [-1, 2, C3k2_MambaOut, [512, True]] # 19 (P4/16-medium)
- [-1, 1, Conv, [512, 3, 2]] # 20
- [[-1, 10], 1, Concat, [1]] # 21 cat head P5
- [-1, 2, C3k2_MambaOut, [1024, True]] # 22 (P5/32-large)
- [[16, 19, 22], 1, Segment, [nc, 32, 256]] # 23 Segment(P3, P4, P5)
6.4 变体四:混合模式
适用场景:想重点增强中深层语义与大目标区域,但又不希望浅层小目标分支全部换掉。
yaml
# YOLO11-seg C3k2_MambaOut hybrid
nc: 80
scales:
n: [0.50, 0.25, 1024]
s: [0.50, 0.50, 1024]
m: [0.50, 1.00, 512]
l: [1.00, 1.00, 512]
x: [1.00, 1.50, 512]
backbone:
- [-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]] # 2
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 2, C3k2_MambaOut, [512, False, 0.25]] # 4
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 2, C3k2_MambaOut, [512, True]] # 6
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 2, C3k2_MambaOut, [1024, True]] # 8
- [-1, 1, SPPF, [1024, 5]] # 9
- [-1, 2, C2PSA, [1024]] # 10
head:
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 11
- [[-1, 6], 1, Concat, [1]] # 12 cat backbone P4
- [-1, 2, C3k2_MambaOut, [512, False]] # 13
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 14
- [[-1, 4], 1, Concat, [1]] # 15 cat backbone P3
- [-1, 2, C3k2, [256, False]] # 16 (P3/8-small)
- [-1, 1, Conv, [256, 3, 2]] # 17
- [[-1, 13], 1, Concat, [1]] # 18 cat head P4
- [-1, 2, C3k2_MambaOut, [512, False]] # 19 (P4/16-medium)
- [-1, 1, Conv, [512, 3, 2]] # 20
- [[-1, 10], 1, Concat, [1]] # 21 cat head P5
- [-1, 2, C3k2_MambaOut, [1024, True]] # 22 (P5/32-large)
- [[16, 19, 22], 1, Segment, [nc, 32, 256]] # 23 Segment(P3, P4, P5)
6.5 变体五:P2 四尺度版
适用场景:你的小目标很多,想把 P2/4 也纳入分割输出。
yaml
# YOLO11-seg C3k2_MambaOut P2 4-scale
nc: 80
scales:
n: [0.50, 0.25, 1024]
s: [0.50, 0.50, 1024]
m: [0.50, 1.00, 512]
l: [1.00, 1.00, 512]
x: [1.00, 1.50, 512]
backbone:
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
- [-1, 2, C3k2_MambaOut, [256, False, 0.25]] # 2
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 2, C3k2_MambaOut, [512, False, 0.25]] # 4
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 2, C3k2_MambaOut, [512, True]] # 6
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 2, C3k2_MambaOut, [1024, True]] # 8
- [-1, 1, SPPF, [1024, 5]] # 9
- [-1, 2, C2PSA, [1024]] # 10
head:
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 11
- [[-1, 6], 1, Concat, [1]] # 12 cat backbone P4
- [-1, 2, C3k2_MambaOut, [512, False]] # 13
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 14
- [[-1, 4], 1, Concat, [1]] # 15 cat backbone P3
- [-1, 2, C3k2_MambaOut, [256, False]] # 16 (P3/8-small)
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 17
- [[-1, 2], 1, Concat, [1]] # 18 cat backbone P2
- [-1, 2, C3k2_MambaOut, [128, False]] # 19 (P2/4-xsmall)
- [-1, 1, Conv, [256, 3, 2]] # 20
- [[-1, 16], 1, Concat, [1]] # 21 cat head P3
- [-1, 2, C3k2_MambaOut, [256, False]] # 22 (P3/8-medium)
- [-1, 1, Conv, [256, 3, 2]] # 23
- [[-1, 13], 1, Concat, [1]] # 24 cat head P4
- [-1, 2, C3k2_MambaOut, [512, False]] # 25 (P4/16-medium)
- [-1, 1, Conv, [512, 3, 2]] # 26
- [[-1, 10], 1, Concat, [1]] # 27 cat head P5
- [-1, 2, C3k2_MambaOut, [1024, True]] # 28 (P5/32-large)
- [[19, 22, 25, 28], 1, Segment, [nc, 32, 256]] # 29 Segment(P2, P3, P4, P5)
七、常见问题
7.1 NameError: name 'C3k2_MambaOut' is not defined
优先检查三件事:
ultralytics/nn/extra_modules/block.py中是否真的存在C3k2_MambaOut。ultralytics/nn/extra_modules/__init__.py是否仍保留from .block import *。ultralytics/nn/tasks.py中的C3K2_CLASS是否仍包含C3k2_MambaOut。
如果你直接使用当前仓库,这三项都已经满足。
7.2 需要安装哪些第三方依赖
就当前 C3k2_MambaOut 这条链路而言,重点是:
bash
pip install timm
因为 ultralytics/nn/backbone/MambaOut.py 里依赖了 timm.layers 的 DropPath。
不需要额外安装 mamba_ssm ,因为这里调用的是 GatedCNNBlock_BCHW,不是 SSM 扫描模块。
7.3 如何把 Segment 版改成 Detect 版
思路非常简单:
- 保留 Backbone 和 Neck 不变。
- 把最后一层
Segment改成Detect。 - 三尺度版输出索引保持
[[16, 19, 22], 1, Detect, [nc]]。 - P2 四尺度版则改成
[[19, 22, 25, 28], 1, Detect, [nc]]。
本质区别只有最后的任务头,前面的 C3k2_MambaOut 替换逻辑完全通用。
7.4 YAML 参数怎么理解
C3k2_MambaOut 的 YAML 参数对应关系如下:
| 写法 | 含义 | 示例 |
|---|---|---|
[256, False, 0.25] |
c2=256, c3k=False, e=0.25 |
Backbone 浅层常用 |
[512, False] |
c2=512, c3k=False, e=0.5 |
Head 常用 |
[512, True] |
c2=512, c3k=True, e=0.5 |
精度优先模式 |
其中:
c2是输出通道数。c3k为True时,内部走C3k_MambaOut分支。e是隐藏通道扩展比,默认0.5。
八、总结
C3k2_MambaOut 这类模块很适合想在 YOLOv11-seg 保持原有框架不大动 的前提下,尝试更强特征混合器的同学。它的优势不在于"超轻量",而在于用比较克制的改动换更强的局部建模和门控表达能力。同时,当前仓库已经把导入、注册和 YAML 示例都准备得比较完整,真正落地时门槛并不高。
最后用一张总表收尾:
| 总结项 | 结论 |
|---|---|
| 来源 | CVPR2025 MambaOut《Do We Really Need Mamba for Vision?》 |
| 当前仓库实际接入块 | GatedCNNBlock_BCHW |
| 是否属于轻量化 | 否,更偏精度优先型替换 |
| 基线实测 | 2,460,112 params / 9.6 GFLOPs |
| 改进版实测 | 2,627,218 params / 10.3 GFLOPs |
| 结构变化 | +6.79% params,+7.29% GFLOPs |
| 部署依赖 | 需要 timm,不需要 mamba_ssm |
| 适用场景 | 实例分割、边界细节较多、精度优先实验 |
| 改动成本 | 低,当前仓库已完成导入与注册 |
| 使用建议 | 先从"仅替换 Backbone"或"混合模式"开始做消融,再决定是否全面替换 |
如果这篇文章对你有帮助,欢迎点赞、收藏、在评论区交流你实际训练出来的涨点情况,也欢迎关注我的 YOLOv11 改进专栏,后面还会继续更新更多能直接落地的检测/分割结构改法。