YOLOv11-seg改进 | C3k2_LEGM局部增强全局建模替换C3k2全流程指南
-
- 一、本文简介
-
- [原始 C3k2 的局限性](#原始 C3k2 的局限性)
- 本文改进核心
- [改进前后参数量 / GFLOPs 对比](#改进前后参数量 / GFLOPs 对比)
- 二、模块原理详解
-
- [2.1 层级结构总览](#2.1 层级结构总览)
- [2.2 基础组件](#2.2 基础组件)
-
- [`window_partition` / `window_reverse`](#
window_partition/window_reverse) - `get_relative_positions`
- `WATT`
- [`window_partition` / `window_reverse`](#
- [2.3 Att:窗口注意力局部全局融合算子(核心)](#2.3 Att:窗口注意力局部全局融合算子(核心))
- [2.4 LEGM:动态归一化 + 注意力 + MLP 残差块](#2.4 LEGM:动态归一化 + 注意力 + MLP 残差块)
- [2.5 C3k2_LEGM:完整 Bottleneck 替换逻辑](#2.5 C3k2_LEGM:完整 Bottleneck 替换逻辑)
- 三、改进思想与创新点
-
- [3.1 背景与动机](#3.1 背景与动机)
- [3.2 核心创新点](#3.2 核心创新点)
- [3.3 与现有方案的对比](#3.3 与现有方案的对比)
- [3.4 在 YOLOv11 框架中的适配设计](#3.4 在 YOLOv11 框架中的适配设计)
- 四、完整代码
-
- [4.1 来自 `ultralytics/nn/extra_modules/DCMPNet.py`](#4.1 来自
ultralytics/nn/extra_modules/DCMPNet.py) - [4.2 来自 `ultralytics/nn/extra_modules/block.py`](#4.2 来自
ultralytics/nn/extra_modules/block.py)
- [4.1 来自 `ultralytics/nn/extra_modules/DCMPNet.py`](#4.1 来自
- 五、手把手配置步骤(三步法)
-
- [Step 1:确认 `extra_modules/init.py` 导入(已自动导入,跳过)](#Step 1:确认
extra_modules/__init__.py导入(已自动导入,跳过)) - [Step 2:确认 `tasks.py` 注册(已注册,跳过)](#Step 2:确认
tasks.py注册(已注册,跳过)) - [Step 3:训练代码](#Step 3:训练代码)
- [Step 1:确认 `extra_modules/init.py` 导入(已自动导入,跳过)](#Step 1:确认
- [六、YAML 配置文件](#六、YAML 配置文件)
-
- [变体一:全面替换(Backbone + Head 全部替换,推荐作为首选配置)](#变体一:全面替换(Backbone + Head 全部替换,推荐作为首选配置))
- [变体二:仅替换 Backbone(Head 保留原版 C3k2)](#变体二:仅替换 Backbone(Head 保留原版 C3k2))
- [变体三:精度优先模式(深层全部使用 c3k=True 强力配置)](#变体三:精度优先模式(深层全部使用 c3k=True 强力配置))
- [变体四:混合模式(浅层保持原版 C3k2,深层引入 LEGM)](#变体四:混合模式(浅层保持原版 C3k2,深层引入 LEGM))
- [变体五:P2 四尺度版(增加 P2 输出层,适合小目标密集场景)](#变体五:P2 四尺度版(增加 P2 输出层,适合小目标密集场景))
- 七、常见问题(FAQ)
-
- [7.1 `NameError: name 'C3k2_LEGM' is not defined`](#7.1
NameError: name 'C3k2_LEGM' 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_LEGM' is not defined`](#7.1
- 八、总结
专栏系列 :YOLOv11 注意力/精度改进实战
改进点 :将 YOLOv11-seg 中的C3k2替换为C3k2_LEGM,引入 DCMPNet 中的LEGM模块,以窗口注意力、动态重标定归一化和残差 MLP 联合增强局部与全局特征建模能力,在参数量与 GFLOPs 小幅增加的前提下,提升复杂纹理、目标边界与多尺度场景下的实例分割表达能力。
一、本文简介
本文引入 CVPR 2024 DCMPNet 中的 LEGM 模块,将其嵌入 YOLOv11 的 C3k2 框架,形成 C3k2_LEGM。根据仓库文档 YOLOV11配置文件.md 第 303 项说明,当前配置的来源为 CVPR2024 DCMPNet 中的 LEGM 。从实现上看,LEGM 不是单纯卷积替换,而是将窗口注意力、动态归一化重标定与卷积式 MLP 组合到一个残差块中,用更强的局部增强全局建模能力替换原始 C3k2 的基础 Bottleneck。
原始 C3k2 的局限性
YOLOv11 默认 C3k2 模块在效率上很稳,但在实例分割任务里仍有几个典型局限:
- 感受野有限 :内部
Bottleneck主要依赖固定深度卷积堆叠,对复杂背景和长程依赖捕获不够充分。 - 全局建模不足:标准卷积更擅长局部邻域聚合,在多目标、遮挡和大尺度目标场景中容易缺少全局上下文。
- 特征自适应能力偏弱:缺少动态重标定和显式注意力机制,对细粒度结构和纹理差异建模能力有限。
本文改进核心
C3k2_LEGM 的核心思路是在不破坏 C2f/C3k2 外层结构的前提下,把原始内部单元替换成 LEGM,主要引入三点增强:
- 窗口注意力建模 :通过
WATT在局部窗口内做多头注意力,补足卷积对全局上下文建模的不足。 - 动态归一化重标定 :
LayNormal不仅做归一化,还额外预测rescale与rebias,增强特征动态调节能力。 - 残差 MLP 增强 :在注意力分支后接入卷积式
Mlp,进一步提升通道交互和特征表达能力。
改进前后参数量 / GFLOPs 对比
下表结果来自项目环境下的 model.info() 实测:
python
from ultralytics import YOLO
model = YOLO("ultralytics/cfg/models/11-seg/light_impro/yolo11-seg-C3k2-LEGM.yaml")
model.info()
| 模型 | 参数量 | GFLOPs | 说明 |
|---|---|---|---|
yolo11n-seg |
2.46M(2,460,112) |
9.6 |
基线 |
yolo11n-seg-C3k2-LEGM |
2.71M(2,713,360) |
10.3 |
2026-04-21 实测 |
| 变化 | +253,248(+10.29%) |
+0.7(+7.29%) |
精度改进方向 |
当前环境下,该模型的
model.info()实测结果为10.3 GFLOPs。
从机制和指标上看,C3k2_LEGM 更适合作为精度增强型替换块,而不是轻量化模块。
二、模块原理详解
2.1 层级结构总览
text
C3k2_LEGM
├─ 继承自 C3k2
│ ├─ 继承自 C2f
│ │ ├─ cv1: Conv(c1, 2c, 1)
│ │ ├─ cv2: Conv((2+n)c, c2, 1)
│ │ └─ m: ModuleList(...)
│ └─ 将原始 Bottleneck 替换为 LEGM
├─ C3k_LEGM
│ └─ 在 C3k 内部堆叠 LEGM
└─ LEGM
├─ norm1: LayNormal
├─ attn: Att
│ ├─ QK: 1x1 Conv
│ ├─ V: 1x1 Conv
│ ├─ WATT: 窗口多头注意力
│ └─ proj: 1x1 Conv
├─ 残差连接 1
├─ norm2: Identity(默认 mlp_norm=False)
├─ mlp: Conv1x1 -> ReLU -> Conv1x1
└─ 残差连接 2
2.2 基础组件
window_partition / window_reverse
这两个函数负责把输入特征图切分成固定大小的窗口,再在注意力完成后恢复回原始特征图布局。
设输入特征为:
X ∈ R B × H × W × C X \in \mathbb{R}^{B \times H \times W \times C} X∈RB×H×W×C
窗口大小为 M M M,则窗口切分后得到:
X w ∈ R ( B ⋅ H M ⋅ W M ) × M 2 × C X_w \in \mathbb{R}^{(B \cdot \frac{H}{M} \cdot \frac{W}{M}) \times M^2 \times C} Xw∈R(B⋅MH⋅MW)×M2×C
这使得注意力复杂度从全局的:
O ( ( H W ) 2 ) O((HW)^2) O((HW)2)
变为窗口内的:
O ( H W M 2 ⋅ M 4 ) = O ( H W M 2 ) O\left(\frac{HW}{M^2} \cdot M^4\right)=O(HWM^2) O(M2HW⋅M4)=O(HWM2)
get_relative_positions
该函数提前构建窗口内 token 之间的相对位置编码,用于注意力偏置项:
B i j = f ( Δ x i j , Δ y i j ) B_{ij} = f(\Delta x_{ij}, \Delta y_{ij}) Bij=f(Δxij,Δyij)
其中 f ( ⋅ ) f(\cdot) f(⋅) 由后续 meta 网络拟合。
WATT
WATT 是窗口多头注意力模块。它先将 qkv 重新组织成多头形式,再计算:
Attn ( Q , K , V ) = Softmax ( Q K T d + B ) V \text{Attn}(Q, K, V)=\text{Softmax}\left(\frac{QK^T}{\sqrt{d}} + B\right)V Attn(Q,K,V)=Softmax(d QKT+B)V
其中:
- d d d 是每个 head 的维度;
- B B B 是相对位置偏置;
- softmax 在窗口内部完成。
2.3 Att:窗口注意力局部全局融合算子(核心)
Att 是 LEGM 的关键算子。当前 LEGM 默认参数里:
use_attn=Trueconv_type=Nonewindow_size=8shift_size=0
因此它的实际流程是:
- 通过
QK得到查询和键; - 通过
V得到值分支; - 切分窗口;
- 在每个窗口内部执行
WATT; - 通过
proj做输出投影。
其核心公式可写成:
Q K = C o n v 1 × 1 ( X ) , V = C o n v 1 × 1 ( X ) QK = \mathrm{Conv}{1\times1}(X), \quad V = \mathrm{Conv}{1\times1}(X) QK=Conv1×1(X),V=Conv1×1(X)
X ′ = W A T T ( P a r t i t i o n ( Q , K , V ) ) X' = \mathrm{WATT}(\mathrm{Partition}(Q,K,V)) X′=WATT(Partition(Q,K,V))
Y = P r o j ( R e v e r s e ( X ′ ) ) Y = \mathrm{Proj}(\mathrm{Reverse}(X')) Y=Proj(Reverse(X′))
因为这里采用固定窗口注意力,所以它比全局自注意力更适合高分辨率检测/分割特征图。
2.4 LEGM:动态归一化 + 注意力 + MLP 残差块
LEGM 的前向传播非常清楚:
python
identity = x
if self.use_attn: x, rescale, rebias = self.norm1(x)
x = self.attn(x)
if self.use_attn: x = x * rescale + rebias
x = identity + x
identity = x
if self.use_attn and self.mlp_norm: x, rescale, rebias = self.norm2(x)
x = self.mlp(x)
if self.use_attn and self.mlp_norm: x = x * rescale + rebias
x = identity + x
如果把它抽象成公式,可以写成:
X ^ , α , β = L a y N o r m a l ( X ) \hat{X}, \alpha, \beta = \mathrm{LayNormal}(X) X^,α,β=LayNormal(X)
Y = A t t ( X ^ ) Y = \mathrm{Att}(\hat{X}) Y=Att(X^)
X 1 = X + ( α ⊙ Y + β ) X_1 = X + (\alpha \odot Y + \beta) X1=X+(α⊙Y+β)
X 2 = X 1 + M l p ( X 1 ) X_2 = X_1 + \mathrm{Mlp}(X_1) X2=X1+Mlp(X1)
与普通 Transformer block 相比,这里的区别在于 LayNormal 不只是归一化,还引入了可学习的动态缩放与偏置重构。
2.5 C3k2_LEGM:完整 Bottleneck 替换逻辑
C3k2_LEGM 保留 C3k2 的 CSP 聚合方式,只把内部单元从 Bottleneck 替换为 LEGM。因此它整体仍然符合 C2f 系列的前向逻辑:
y 0 , y 1 = S p l i t ( c v 1 ( x ) ) y_0, y_1 = \mathrm{Split}(\mathrm{cv1}(x)) y0,y1=Split(cv1(x))
y i + 1 = L E G M ( y i ) y_{i+1} = \mathrm{LEGM}(y_i) yi+1=LEGM(yi)
o u t = c v 2 ( C o n c a t ( y 0 , y 1 , ... ) ) \mathrm{out} = \mathrm{cv2}(\mathrm{Concat}(y_0, y_1, \ldots)) out=cv2(Concat(y0,y1,...))
这种设计的价值在于:
- 兼容原始
C3k2的输入输出格式; - 替换成本低;
- YAML 层面即可切换。
三、改进思想与创新点
3.1 背景与动机
实例分割任务对特征表达的要求比普通检测更高。一方面,需要对局部边界、纹理与小目标轮廓保持敏感;另一方面,又要求对复杂背景、遮挡关系和多尺度目标具备更强的全局上下文建模能力。原始 C3k2 的优势是高效,但它内部仍以卷积堆叠为主,在跨区域依赖建模上相对保守。
LEGM 的价值在于,它不是简单把注意力直接塞进 YOLO,而是采用了一种更平衡的做法:
- 用窗口注意力引入局部范围内的全局关系建模;
- 用
LayNormal的动态重标定增强特征适应性; - 用轻量 MLP 做通道补充建模。
3.2 核心创新点
创新点一:窗口注意力替代纯卷积式局部建模
Att 在固定大小窗口中做多头注意力:
A t t n ( Q , K , V ) = S o f t m a x ( Q K T d + B ) V \mathrm{Attn}(Q,K,V)=\mathrm{Softmax}\left(\frac{QK^T}{\sqrt{d}}+B\right)V Attn(Q,K,V)=Softmax(d QKT+B)V
相比原始卷积,这种机制能在局部区域内直接建模更长距离的 token 关系。
创新点二:动态重标定归一化
LayNormal 的输出不是单纯归一化结果,而是:
( X ^ , α , β ) (\hat{X}, \alpha, \beta) (X^,α,β)
然后在注意力后执行:
Y ′ = α ⊙ Y + β Y' = \alpha \odot Y + \beta Y′=α⊙Y+β
这种做法等价于给注意力输出提供了一层动态的特征校准,更适合复杂场景下的特征恢复与增强。
创新点三:注意力后接卷积式 MLP
Mlp 不是标准全连接,而是:
C o n v 1 × 1 → R e L U → C o n v 1 × 1 \mathrm{Conv}{1\times1} \rightarrow \mathrm{ReLU} \rightarrow \mathrm{Conv}{1\times1} Conv1×1→ReLU→Conv1×1
它在保留空间结构的前提下增强通道交互能力,相比直接只做注意力更完整。
创新点四:以最小侵入方式融入 YOLOv11
当前仓库的适配代码非常克制:
python
self.m = nn.ModuleList(C3k_LEGM(self.c, self.c, n, shortcut, g) if c3k else LEGM(self.c) for _ in range(n))
这意味着:
- 输入输出维度不变;
- Neck 和 Head 不需要大改;
- 更适合做可控消融实验。
3.3 与现有方案的对比
| 方法 | 局部建模 | 全局上下文 | 动态重标定 | 通道增强 | 复杂度方向 | 适用场景 |
|---|---|---|---|---|---|---|
| C3k2 原版 | 常规卷积 | 弱 | 无 | 弱 | 效率优先 | 通用检测 / 分割 |
| C3k2_LFE | Shift 位移卷积 | 弱 | 无 | 中 | 轻度精度增强 | 边缘和纹理细节 |
| C3k2_MambaOut | 门控卷积 | 中 | 有 | 中 | 精度增强 | 多尺度上下文建模 |
| C3k2_LEGM | 窗口注意力 + 残差 MLP | 强 | 有 | 强 | 精度增强 | 多尺度、遮挡、复杂背景实例分割 |
| ViT 式全局注意力块 | 强 | 很强 | 可选 | 强 | 高复杂度 | 高算力场景 |
3.4 在 YOLOv11 框架中的适配设计
当前仓库里的导入和注册已经完成:
ultralytics/nn/extra_modules/__init__.py已包含from .block import *和from .DCMPNet import *。ultralytics/nn/tasks.py的C3K2_CLASS已经注册C3k2_LEGM。- 目标 YAML 可直接被
YOLO()正常构建。
因此在本文的配置步骤里,这两步都应明确写成"已存在,跳过即可"。
四、完整代码
为保持和仓库源码逐字一致,下面代码块不在内部加入人工中文注释,解释放在代码块外。
4.1 来自 ultralytics/nn/extra_modules/DCMPNet.py
python
import torch, math
import torch.nn as nn
import torch.nn.functional as F
from timm.layers import to_2tuple, trunc_normal_
from ..modules.conv import Conv
__all__ = ['MFM', 'LEGM']
def window_partition(x, window_size):
B, H, W, C = x.shape
x = x.view(B, H // window_size, window_size, W // window_size, window_size, C)
windows = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size**2, C)
return windows
def window_reverse(windows, window_size, H, W):
B = int(windows.shape[0] / (H * W / window_size / window_size))
x = windows.view(B, H // window_size, W // window_size, window_size, window_size, -1)
x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, H, W, -1)
return x
def get_relative_positions(window_size):
coords_h = torch.arange(window_size)
coords_w = torch.arange(window_size)
# coords = torch.stack(torch.meshgrid(coords_h, coords_w, indexing='xy'))
coords = torch.stack(torch.meshgrid([coords_h, coords_w]))
coords_flatten = torch.flatten(coords, 1)
relative_positions = coords_flatten[:, :, None] - coords_flatten[:, None, :]
relative_positions = relative_positions.permute(1, 2, 0).contiguous()
relative_positions_log = torch.sign(relative_positions) * torch.log(1. + relative_positions.abs())
return relative_positions_log
class WATT(nn.Module):
def __init__(self, dim, window_size, num_heads):
super().__init__()
self.dim = dim
self.window_size = window_size
self.num_heads = num_heads
head_dim = dim // num_heads
self.scale = head_dim ** -0.5
relative_positions = get_relative_positions(self.window_size)
self.register_buffer("relative_positions", relative_positions)
self.meta = nn.Sequential(
nn.Linear(2, 256, bias=True),
nn.ReLU(True),
nn.Linear(256, num_heads, bias=True)
)
self.softmax = nn.Softmax(dim=-1)
def forward(self, qkv):
B_, N, _ = qkv.shape
qkv = qkv.reshape(B_, N, 3, self.num_heads, self.dim // self.num_heads).permute(2, 0, 3, 1, 4)
q, k, v = qkv[0], qkv[1], qkv[2]
q = q * self.scale
attn = (q @ k.transpose(-2, -1))
relative_position_bias = self.meta(self.relative_positions)
relative_position_bias = relative_position_bias.permute(2, 0, 1).contiguous()
attn = attn + relative_position_bias.unsqueeze(0)
attn = self.softmax(attn)
x = (attn @ v).transpose(1, 2).reshape(B_, N, self.dim)
return x
class Att(nn.Module):
def __init__(self, dim, num_heads, window_size, shift_size, use_attn=False, conv_type=None):
super().__init__()
self.dim = dim
self.head_dim = int(dim // num_heads)
self.num_heads = num_heads
self.window_size = window_size
self.shift_size = shift_size
self.use_attn = use_attn
self.conv_type = conv_type
if self.conv_type == 'Conv':
self.conv = nn.Sequential(
nn.Conv2d(dim, dim, kernel_size=3, padding=1, padding_mode='reflect'),
nn.ReLU(True),
nn.Conv2d(dim, dim, kernel_size=3, padding=1, padding_mode='reflect')
)
if self.conv_type == 'DWConv':
self.conv = nn.Conv2d(dim, dim, kernel_size=5, padding=2, groups=dim, padding_mode='reflect')
if self.conv_type == 'DWConv' or self.use_attn:
self.V = nn.Conv2d(dim, dim, 1)
self.proj = nn.Conv2d(dim, dim, 1)
if self.use_attn:
self.QK = nn.Conv2d(dim, dim * 2, 1)
self.attn = WATT(dim, window_size, num_heads)
def check_size(self, x, shift=False):
_, _, h, w = x.size()
mod_pad_h = (self.window_size - h % self.window_size) % self.window_size
mod_pad_w = (self.window_size - w % self.window_size) % self.window_size
if shift:
x = F.pad(x, (self.shift_size, (self.window_size-self.shift_size+mod_pad_w) % self.window_size,
self.shift_size, (self.window_size-self.shift_size+mod_pad_h) % self.window_size), mode='reflect')
else:
x = F.pad(x, (0, mod_pad_w, 0, mod_pad_h), 'reflect')
return x
def forward(self, X):
B, C, H, W = X.shape
if self.conv_type == 'DWConv' or self.use_attn:
V = self.V(X)
if self.use_attn:
QK = self.QK(X)
QKV = torch.cat([QK, V], dim=1)
# shift
shifted_QKV = self.check_size(QKV, self.shift_size > 0)
Ht, Wt = shifted_QKV.shape[2:]
# partition windows
shifted_QKV = shifted_QKV.permute(0, 2, 3, 1)
qkv = window_partition(shifted_QKV, self.window_size) # nW*B, window_size**2, C
attn_windows = self.attn(qkv)
# merge windows
shifted_out = window_reverse(attn_windows, self.window_size, Ht, Wt) # B H' W' C
# reverse cyclic shift
out = shifted_out[:, self.shift_size:(self.shift_size+H), self.shift_size:(self.shift_size+W), :]
attn_out = out.permute(0, 3, 1, 2)
if self.conv_type in ['Conv', 'DWConv']:
conv_out = self.conv(V)
out = self.proj(conv_out + attn_out)
else:
out = self.proj(attn_out)
else:
if self.conv_type == 'Conv':
out = self.conv(X)
elif self.conv_type == 'DWConv':
out = self.proj(self.conv(V))
return out
class Mlp(nn.Module):
def __init__(self, in_features, hidden_features=None, out_features=None):
super().__init__()
out_features = out_features or in_features
hidden_features = hidden_features or in_features
self.mlp = nn.Sequential(
nn.Conv2d(in_features, hidden_features, 1),
nn.ReLU(True),
nn.Conv2d(hidden_features, out_features, 1)
)
def forward(self, x):
return self.mlp(x)
class LayNormal(nn.Module):
def __init__(self, dim, eps=1e-5, detach_grad=False):
super(LayNormal, self).__init__()
self.eps = eps
self.detach_grad = detach_grad
self.weight = nn.Parameter(torch.ones((1, dim, 1, 1)))
self.bias = nn.Parameter(torch.zeros((1, dim, 1, 1)))
self.meta1 = nn.Conv2d(1, dim, 1)
self.meta2 = nn.Conv2d(1, dim, 1)
trunc_normal_(self.meta1.weight, std=.02)
nn.init.constant_(self.meta1.bias, 1)
trunc_normal_(self.meta2.weight, std=.02)
nn.init.constant_(self.meta2.bias, 0)
def forward(self, input):
mean = torch.mean(input, dim=(1, 2, 3), keepdim=True)
std = torch.sqrt((input - mean).pow(2).mean(dim=(1, 2, 3), keepdim=True) + self.eps)
normalized_input = (input - mean) / std
if self.detach_grad:
rescale, rebias = self.meta1(std.detach()), self.meta2(mean.detach())
else:
rescale, rebias = self.meta1(std), self.meta2(mean)
out = normalized_input * self.weight + self.bias
return out, rescale, rebias
class LEGM(nn.Module):
def __init__(self, dim, num_heads=8, mlp_ratio=4.,
norm_layer=LayNormal, mlp_norm=False,
window_size=8, shift_size=0, use_attn=True, conv_type=None):
super().__init__()
self.use_attn = use_attn
self.mlp_norm = mlp_norm
self.norm1 = norm_layer(dim) if use_attn else nn.Identity()
self.attn = Att(dim, num_heads=num_heads, window_size=window_size,
shift_size=shift_size, use_attn=use_attn, conv_type=conv_type)
self.norm2 = norm_layer(dim) if use_attn and mlp_norm else nn.Identity()
self.mlp = Mlp(dim, hidden_features=int(dim * mlp_ratio))
def forward(self, x):
identity = x
if self.use_attn: x, rescale, rebias = self.norm1(x)
x = self.attn(x)
if self.use_attn: x = x * rescale + rebias
x = identity + x
identity = x
if self.use_attn and self.mlp_norm: x, rescale, rebias = self.norm2(x)
x = self.mlp(x)
if self.use_attn and self.mlp_norm: x = x * rescale + rebias
x = identity + x
return x
4.2 来自 ultralytics/nn/extra_modules/block.py
python
######################################## CVPR2024 DCMPNet start ########################################
class C3k_LEGM(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(*(LEGM(c_) for _ in range(n)))
class C3k2_LEGM(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_LEGM(self.c, self.c, n, shortcut, g) if c3k else LEGM(self.c) for _ in range(n))
######################################## CVPR2024 DCMPNet end ########################################
五、手把手配置步骤(三步法)
Step 1:确认 extra_modules/__init__.py 导入(已自动导入,跳过)
当前仓库已经在 ultralytics/nn/extra_modules/__init__.py 中包含:
python
from .DCMPNet import *
from .block import *
因此 LEGM 与 C3k2_LEGM 所需导入已存在,这一步直接跳过即可。
Step 2:确认 tasks.py 注册(已注册,跳过)
当前仓库在 ultralytics/nn/tasks.py 的 C3K2_CLASS 中已经包含:
python
C3k2_LEGM
因此无需再手动补注册。
Step 3:训练代码
python
from ultralytics import YOLO
# 加载改进模型
model = YOLO("ultralytics/cfg/models/11-seg/light_impro/yolo11-seg-C3k2-LEGM.yaml")
# 查看参数量和 GFLOPs
model.info()
# 开始训练
model.train(
data="coco8-seg.yaml",
epochs=100,
imgsz=640,
batch=16,
device=0
)
六、YAML 配置文件
变体一:全面替换(Backbone + Head 全部替换,推荐作为首选配置)
适用场景:追求最完整的 LEGM 表达能力,Backbone 与 Head 全部使用 C3k2_LEGM。
实测参数量:2.71M / 10.3 GFLOPs(n scale)
yaml
# YOLO11-seg C3k2_LEGM 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_LEGM, [256, False, 0.25]] # 2
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 2, C3k2_LEGM, [512, False, 0.25]] # 4
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 2, C3k2_LEGM, [512, True]] # 6
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 2, C3k2_LEGM, [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_LEGM, [512, False]] # 13
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 14
- [[-1, 4], 1, Concat, [1]] # 15 cat backbone P3
- [-1, 2, C3k2_LEGM, [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_LEGM, [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_LEGM, [1024, True]] # 22 (P5/32-large)
- [[16, 19, 22], 1, Segment, [nc, 32, 256]] # 23 Segment(P3, P4, P5)
变体二:仅替换 Backbone(Head 保留原版 C3k2)
适用场景:仅在 Backbone 引入 LEGM,优先观察主干侧的上下文增强收益。
yaml
# YOLO11-seg C3k2_LEGM 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_LEGM, [256, False, 0.25]] # 2
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 2, C3k2_LEGM, [512, False, 0.25]] # 4
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 2, C3k2_LEGM, [512, True]] # 6
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 2, C3k2_LEGM, [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)
变体三:精度优先模式(深层全部使用 c3k=True 强力配置)
适用场景:进一步增强内部堆叠深度,追求更强表达能力。
yaml
# YOLO11-seg C3k2_LEGM 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_LEGM, [256, True, 0.25]] # 2
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 2, C3k2_LEGM, [512, True, 0.25]] # 4
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 2, C3k2_LEGM, [512, True]] # 6
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 2, C3k2_LEGM, [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_LEGM, [512, True]] # 13
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 14
- [[-1, 4], 1, Concat, [1]] # 15 cat backbone P3
- [-1, 2, C3k2_LEGM, [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_LEGM, [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_LEGM, [1024, True]] # 22 (P5/32-large)
- [[16, 19, 22], 1, Segment, [nc, 32, 256]] # 23 Segment(P3, P4, P5)
变体四:混合模式(浅层保持原版 C3k2,深层引入 LEGM)
适用场景:兼顾速度与精度,只在中深层加入 LEGM。
yaml
# YOLO11-seg C3k2_LEGM 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_LEGM, [512, False, 0.25]] # 4
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 2, C3k2_LEGM, [512, True]] # 6
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 2, C3k2_LEGM, [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_LEGM, [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_LEGM, [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_LEGM, [1024, True]] # 22 (P5/32-large)
- [[16, 19, 22], 1, Segment, [nc, 32, 256]] # 23 Segment(P3, P4, P5)
变体五:P2 四尺度版(增加 P2 输出层,适合小目标密集场景)
适用场景:小目标较多,希望引入四尺度分割输出。
注意:P2 版层索引重新计算,Segment 输出使用 [19, 22, 25, 28]。
yaml
# YOLO11-seg C3k2_LEGM 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_LEGM, [256, False, 0.25]] # 2
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 2, C3k2_LEGM, [512, False, 0.25]] # 4
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 2, C3k2_LEGM, [512, True]] # 6
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 2, C3k2_LEGM, [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_LEGM, [512, False]] # 13
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 14
- [[-1, 4], 1, Concat, [1]] # 15 cat backbone P3
- [-1, 2, C3k2_LEGM, [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_LEGM, [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_LEGM, [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_LEGM, [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_LEGM, [1024, True]] # 28 (P5/32-large)
- [[19, 22, 25, 28], 1, Segment, [nc, 32, 256]] # 29 Segment(P2, P3, P4, P5)
七、常见问题(FAQ)
7.1 NameError: name 'C3k2_LEGM' is not defined
优先检查以下三项:
ultralytics/nn/extra_modules/block.py中是否存在C3k2_LEGM。ultralytics/nn/extra_modules/__init__.py是否保留from .DCMPNet import *和from .block import *。ultralytics/nn/tasks.py的C3K2_CLASS是否仍包含C3k2_LEGM。
7.2 需要安装哪些第三方依赖
LEGM 这条链路主要依赖:
bash
pip install timm
因为 ultralytics/nn/extra_modules/DCMPNet.py 里使用了 timm.layers 中的 to_2tuple 和 trunc_normal_。
7.3 Segment 改成 Detect 怎么写
只需要替换最后一层:
- 三尺度版改为
[[16, 19, 22], 1, Detect, [nc]] - P2 四尺度版改为
[[19, 22, 25, 28], 1, Detect, [nc]]
7.4 YAML 参数怎么理解
C3k2_LEGM 的参数与普通 C3k2 保持一致:
| 写法 | 含义 | 示例 |
|---|---|---|
[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_LEGM堆叠;e是隐藏通道扩展比。
八、总结
| 对比维度 | 原始 C3k2 | C3k2_LEGM |
|---|---|---|
| 参数量(n scale) | 2.46M | 2.71M(+10.29%) |
| GFLOPs(n scale) | 9.6 | 10.3(+7.29%) |
| 局部建模方式 | 常规卷积堆叠 | 窗口划分后的局部多头注意力 |
| 全局上下文能力 | 较弱 | 通过窗口注意力与相对位置偏置增强 |
| 特征重标定机制 | 无 | LayNormal 动态生成 rescale + rebias |
| 通道增强方式 | 基础卷积映射 | 残差 Mlp(Conv1x1 -> ReLU -> Conv1x1) |
| 归一化格式 | BN(BCHW) | 动态 LayNormal(BCHW) |
| 论文来源 | YOLOv11 原版 | DCMPNet(CVPR 2024) |
| 适用场景 | 通用目标检测 / 分割 | 复杂背景、多尺度目标、遮挡场景与精细实例分割 |
总结 :C3k2_LEGM 将 CVPR 2024 DCMPNet 中的 LEGM 引入 YOLOv11,核心是用窗口注意力补足卷积对长程依赖建模的不足,再通过 LayNormal 的动态重标定与残差 Mlp 进一步增强特征表达。在 n scale 下参数量增加 10.29%(+0.25M),GFLOPs 增加 7.29%(+0.7),换来更强的局部增强全局建模能力。对于复杂背景、遮挡、多尺度目标以及边界要求更高的实例分割场景,这种替换通常比单纯卷积 Bottleneck 更有潜力。
如果这篇文章对你有帮助,欢迎点赞、收藏、在评论区交流你实际训练后的涨点情况,也欢迎关注我的 YOLOv11 改进专栏,后面还会继续更新更多能直接落地的检测/分割结构改法。