05 / 2026 / 实验原型

DDefConv:面向不规则视觉目标的实验性可变形特征卷积

基于 PyTorch、注意力机制和可变形采样的实验性视觉算子,尝试接入 Ultralytics YOLO11,用于不规则目标特征增强。

DDefConv:面向不规则视觉目标的实验性可变形特征卷积

问题

如何针对边缘形状不规则、方向变化明显的视觉目标探索可变形特征增强,并保持实现边界清晰可验证?

角色

独立研究原型开发;负责模块实现、YOLO11 接入实验与基础验证。

方法

输入特征分为注意力分支和变形分支,利用学习到的采样偏移执行可变形卷积,再通过空间注意力加权并与旁路特征融合。

结果

README 记录了最小测试、YOLO11 配置接入与已知边界。当前是 Experimental / Research Prototype,不表述为完整 DCNv4、严格的 depthwise deformable convolution 或已全面优于标准卷积。

DDefConv:深度可分离可变形卷积

基于 PyTorch 的实验性视觉算子,以及接入 Ultralytics YOLO11 的完整说明。

DDefConv 面向边缘形状不规则、方向变化明显的视觉目标。模块先将输入特征拆成注意力分支和变形分支,再利用学习到的采样偏移执行可变形卷积,最后通过空间注意力对变形特征进行加权并与旁路特征融合。

项目最初用于水稻病斑特征提取,也可以作为通用特征增强模块放入检测、分类或分割网络。核心代码位于 DDefConv0/ddef_conv.py。

项目亮点

  • 将输入特征拆分为注意力分支与可变形采样分支,增强对不规则目标区域的特征表达。
  • 提供可独立运行的 PyTorch 模块与 DDefConvBlock 封装。
  • 给出接入 Ultralytics YOLO11、自定义 YAML、训练和验证的完整流程。
  • 明确记录当前实现边界、适用位置与后续改进方向,便于继续实验和消融对比。

快速体验:

cd DDefConv0
python -m pip install torch torchvision
python ddef_conv.py

目录

当前实现说明

在使用代码前,请先了解下面几个边界。

  1. 本模块不是 DCNv4。 DDefConv 调用的是 torchvision.ops.deform_conv2d,并且没有传入 modulation mask,因此当前使用的是 Torchvision 文档所述的 DCNv1 式 offset-only 算子;运行 DDefConv 本身不需要编译仓库中的 DCNv4_op。
  2. 当前代码不是严格的 depthwise deformable convolution。 deform_weight 的形状是 [C/2, C/2, k, k],因此变形分支仍会混合通道。严格逐通道卷积的权重第二维应为 1,通常还要再接一个 1 × 1 pointwise convolution。当前版本更准确的描述是“通道拆分、注意力引导的可变形特征卷积”。
  3. 推荐配置为 kernel_size=3, stride=1, padding=1, dilation=1。 偏移分支始终保持输入空间尺寸;若直接设置 stride=2 或不匹配的 padding/dilation,偏移、注意力和变形卷积输出的空间尺寸会不一致。
  4. 输入通道数必须为偶数。 模块会沿通道维将输入均分为两个分支。
  5. 代码并非设计文档公式的逐行复现。 设计文档描述了逐通道、离散取整并带边界 mask 的采样过程;当前实现把采样交给 Torchvision 的连续偏移可变形卷积,二者不能视为完全相同的算子。
  6. 该文件只实现特征提取算子。 它没有实现显式边缘算子、旋转框检测头、角度预测、旋转损失或旋转 NMS。“边缘感知”和“角度自适应”是原始设计动机,当前代码只是其中的实验性近似实现。需要旋转框检测时,应将它接入 YOLO OBB 模型,而不是把 DDefConv 本身当作角度检测头。
  7. DDefConvBlock 不是 Ultralytics Conv 的完全等价替换。 为了方便 YAML 调用,它保留了 g 和 act 参数,但当前代码没有使用这两个参数;它也不能替换输入为 RGB 的第一层,因为第一层输入通道数为 3,无法均分。

因此,不建议把 YOLO 中所有 Conv 都直接替换成 DDefConvBlock,尤其不要替换 stride=2 的下采样卷积。最稳妥的做法是先替换一个 stride=1 的特征块并完成消融实验。

模块原理

设输入特征为:

$$ X \in \mathbb{R}^{B \times C \times H \times W}, \qquad C \bmod 2 = 0 $$

模块的计算流程如下。

flowchart LR
    X[输入 X] --> O[3×3 Conv + BN<br/>生成偏移 O]
    X --> S{沿通道均分}
    S --> A[注意力分支 A]
    S --> P[变形分支 P]
    A --> ABN[BN + 空间统计]
    ABN --> W[注意力权重 ω]
    P --> DC[DeformConv]
    O --> DC
    DC --> M[逐元素相乘]
    W --> M
    X --> U[1×1 Conv 旁路 U]
    M --> F[相加 + BN + Sigmoid]
    U --> F
    F --> Pool[3×3 MaxPool, stride=1]
    Pool --> Proj[1×1 输出投影]
    Proj --> Y[输出 Y]

1. 生成采样偏移

对输入特征执行 3 × 3 Conv + BatchNorm,为每个卷积位置生成二维偏移:

$$ O = f_{offset}(X), \qquad O \in \mathbb{R}^{B \times 2k^2 \times H \times W} $$

当 k=3 时,偏移通道数为 2 × 3 × 3 = 18。

2. 拆分特征

输入沿通道维均分:

$$ A, P = \mathrm{Split}(X), \qquad A,P \in \mathbb{R}^{B \times C/2 \times H \times W} $$

其中,A 用于计算空间注意力,P 用于可变形采样。

3. 可变形卷积

变形分支按照偏移 O 改变规则卷积网格的采样位置:

$$ P' = \mathrm{DeformConv}(P, O; W_d) $$

与普通卷积的固定采样点相比,可变形卷积可以让感受野随目标形状发生变化,更适合描述弯曲边缘、细长结构和不规则区域。

4. 空间注意力

注意力分支先做批归一化,再按每个样本、每个通道统计空间均值和方差:

$$ \omega = \mathrm{Sigmoid}\left( \rho \cdot \frac{|A' - \mu|}{\sigma^2 + \xi} + \gamma \right) $$

其中 ρ 和 γ 是可学习参数,ξ 用于避免除零。偏离通道空间均值较明显的位置会得到不同的响应权重。

5. 融合与输出

注意力加权后的变形特征与输入的 1 × 1 卷积旁路相加:

$$ Y = \mathrm{Proj}\left( \mathrm{Pool}\left( \mathrm{Sigmoid}\left( \mathrm{BN}(\omega \odot P' + U) \right) \right) \right), \qquad U = \mathrm{Conv}_{1\times1}(X) $$

当前 MaxPool2d 的步长为 1,所以不会改变特征图尺寸;最后的 1 × 1 投影负责得到指定输出通道数。

张量形状

以下以 B=2, C=64, H=W=128, k=3, out_channels=128 为例。

张量 形状 含义
x [2, 64, 128, 128] 输入特征
offset [2, 18, 128, 128] 9 个采样点的二维偏移
at_feat [2, 32, 128, 128] 注意力分支
p_deep [2, 32, 128, 128] 变形分支
omega [2, 32, 128, 128] 空间注意力权重
p_deep_prime [2, 32, 128, 128] 可变形卷积输出
out [2, 128, 128, 128] 最终输出

项目结构

深度可分离可变形卷积/
├── README.md                         # 本说明
├── DDefConv_TODO.md                  # 原始设计/需求说明
├── DDefConv0/
│   ├── ddef_conv.py                  # DDefConv 与 YOLO 封装,核心文件
│   ├── benchmark_conv.py             # 探索性 benchmark 脚手架
│   ├── DDefConv_TODO.md              # 设计说明副本
│   ├── DCNv4_op/                     # DCNv4 CUDA 扩展,上游代码
│   ├── classification/               # DCNv4 上游分类工程
│   ├── detection/                    # DCNv4 上游检测工程
│   └── segmentation/                 # DCNv4 上游分割工程
└── DCNv4-main/DCNv4-main/            # 原始 DCNv4 工程副本

如果只使用本文介绍的 DDefConv,重点关注下面两个文件即可:

DDefConv0/README.md 和 DDefConv0/README_CN.md 主要介绍上游 DCNv4;本项目的 DDefConv 说明以当前顶层 README 为准。

环境与快速测试

依赖

  • Python 3.9+
  • PyTorch
  • Torchvision
  • 可选:CUDA,用于 GPU 训练和测速
  • 接入 YOLO 时:Ultralytics 源码仓库

PyTorch 与 Torchvision 必须是相互兼容的构建版本。建议根据自己的 CUDA 版本,从 PyTorch 官方安装页面 复制安装命令。

python -m pip install torch torchvision

运行最小测试

cd DDefConv0
python ddef_conv.py

预期输出形状为:

输入特征形状: torch.Size([2, 64, 128, 128])
DDefConv 输出形状: torch.Size([2, 128, 128, 128])

运行基准脚本

cd DDefConv0
python benchmark_conv.py

脚本会尝试比较普通 Conv2d、DDefConv 和可选的 DCNv4,但它目前只是探索性脚手架:反向计时会重复使用同一计算图,DCNv4 分支的输入布局也没有与其接口对齐。不要据此发布速度结论;正式比较前应先修正计时过程、完成预热与同步,并为每个算子使用正确输入格式。

独立使用

import torch

from ddef_conv import DDefConv, DDefConvBlock

x = torch.randn(2, 64, 128, 128)

# 原始模块接口
layer = DDefConv(
    in_channels=64,
    out_channels=128,
    kernel_size=3,
    stride=1,
    padding=1,
    dilation=1,
)
y = layer(x)
print(y.shape)  # torch.Size([2, 128, 128, 128])

# 与 Ultralytics 常用 c1/c2/k/s 参数风格一致的封装
block = DDefConvBlock(c1=64, c2=128, k=3, s=1)
y = block(x)

接入 Ultralytics

下面采用修改 Ultralytics 源码的方式。官方也推荐在需要从模型 YAML 解析自定义模块时,将模块放入源码、导出类名,并在 parse_model() 中处理通道参数。具体目录可能随版本变化,请同时参考 Ultralytics 自定义模块说明。

第 1 步:以可编辑方式安装 Ultralytics

git clone https://github.com/ultralytics/ultralytics.git
cd ultralytics
python -m pip install -e .

确认当前 Python 确实使用这份源码:

python -c "import ultralytics; print(ultralytics.__file__)"

输出路径应指向刚刚克隆的 ultralytics 目录,而不是另一份虚拟环境中的普通安装。

第 2 步:复制模块文件

将本仓库的:

DDefConv0/ddef_conv.py

复制到 Ultralytics 源码的:

ultralytics/nn/modules/ddef_conv.py

复制后无需编译 DCNv4_op,因为这个文件使用的是 torchvision.ops.deform_conv2d。

第 3 步:从 modules 包导出模块

编辑:

ultralytics/nn/modules/__init__.py

加入导入:

from .ddef_conv import DDefConv, DDefConvBlock

并在该文件的 __all__ 中加入:

"DDefConv",
"DDefConvBlock",

第 4 步:在 tasks.py 注册

编辑:

ultralytics/nn/tasks.py

先在 from ultralytics.nn.modules import (...) 中加入:

DDefConvBlock,

再找到 parse_model() 内的 base_modules 集合,加入:

DDefConvBlock,

示意如下:

base_modules = frozenset(
    {
        # ... Ultralytics 原有模块
        Conv,
        DWConv,
        DDefConvBlock,
        # ...
    }
)

这样 YAML 中的第一个参数会被当作输出通道 c2,parse_model() 会自动补入输入通道 c1,并继续应用 YOLO 的宽度缩放:

YAML:          DDefConvBlock, [c2, k, s]
实际构造参数:  DDefConvBlock(c1, c2, k, s)

DDefConvBlock 不接收内部重复次数,因此不要把它加入 repeat_modules。

较旧的 Ultralytics 版本可能没有单独命名的 base_modules。这种情况下,在 parse_model() 中找到对 Conv、C2f 等基础模块补入 c1/c2 的判断集合,把 DDefConvBlock 加入同一集合即可。

第 5 步:修改模型 YAML

先复制官方 YOLO11 检测配置:

ultralytics/cfg/models/11/yolo11.yaml

例如保存为:

ultralytics/cfg/models/11/yolo11-ddef.yaml

建议在配置中显式指定模型尺寸,便于后续加载对应预训练权重:

scale: n

当前 DDefConv 不支持 YOLO 下采样层常用的 stride=2,因此请选择一个 stride=1 的特征块进行替换。最简单的做法是保持层数和输出通道不变,原位替换 backbone 最后的 C2PSA。该位置分辨率较低,适合作为首次接入测试。

原始配置中的第 10 层:

- [-1, 2, C2PSA, [1024]]

替换为:

- [-1, 1, DDefConvBlock, [1024, 3, 1]]

替换后的骨干示例:

nc: 80
scale: n

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]]
  - [-1, 1, Conv, [128, 3, 2]]
  - [-1, 2, C3k2, [256, False, 0.25]]
  - [-1, 1, Conv, [256, 3, 2]]
  - [-1, 2, C3k2, [512, False, 0.25]]
  - [-1, 1, Conv, [512, 3, 2]]
  - [-1, 2, C3k2, [512, True]]
  - [-1, 1, Conv, [1024, 3, 2]]
  - [-1, 2, C3k2, [1024, True]]
  - [-1, 1, SPPF, [1024, 5]]
  - [-1, 1, DDefConvBlock, [1024, 3, 1]]

# head 与官方 yolo11.yaml 保持不变

这里是“替换”而不是“插入”,因此后面的 from 层索引无需整体调整,未被替换层的预训练权重键名也能尽量保持不变。若更关注浅层小目标特征,也可以另做实验,将前面的某个 C3k2 原位替换为同输出通道、stride=1 的 DDefConvBlock,但要重新测量显存和延迟。

不要做下面这种替换:

# 错误示例:当前实现的 offset 分支与 stride=2 输出尺寸不匹配
- [-1, 1, DDefConvBlock, [256, 3, 2]]

第 6 步:构建并检查模型

from ultralytics import YOLO
from ultralytics.nn.modules import DDefConvBlock

model = YOLO("ultralytics/cfg/models/11/yolo11-ddef.yaml")
model.info()

assert any(
    isinstance(module, DDefConvBlock)
    for module in model.model.modules()
), "模型中没有解析出 DDefConvBlock"

如果模型可以成功构建、model.info() 能完成一次计算,并且断言通过,说明 Python 导入、YAML 解析和基础前向链路已经接通。

训练与验证

从头训练

yolo detect train \
  model=ultralytics/cfg/models/11/yolo11-ddef.yaml \
  data=/path/to/data.yaml \
  epochs=100 \
  imgsz=640 \
  batch=16

Windows PowerShell 可以写成一行:

yolo detect train model=ultralytics/cfg/models/11/yolo11-ddef.yaml data=D:\data\data.yaml epochs=100 imgsz=640 batch=16

加载 YOLO11 预训练权重后微调

from ultralytics import YOLO

model = YOLO("ultralytics/cfg/models/11/yolo11-ddef.yaml")

# 只会迁移名称和形状匹配的参数;新加入的 DDefConvBlock 保持随机初始化
model.load("yolo11n.pt")

model.train(
    data="/path/to/data.yaml",
    epochs=100,
    imgsz=640,
    batch=16,
    device=0,
)

scale: n 应与 yolo11n.pt 对应;使用 s/m/l/x 尺寸时,请同步更改 scale 和预训练权重名称。

验证与推理

from ultralytics import YOLO

model = YOLO("runs/detect/train/weights/best.pt")

metrics = model.val(data="/path/to/data.yaml")
results = model.predict(source="/path/to/image.jpg", save=True)

加载 best.pt 的环境仍需包含注册后的 DDefConvBlock 源码,否则 Python 无法还原自定义模型结构。

用于旋转框检测

如果任务需要预测病斑方向或旋转框,请以官方 yolo11-obb.yaml 为基础,按同样方式替换一个 stride=1 特征块,并使用 OBB 数据格式和训练任务:

yolo obb train model=yolo11-ddef-obb.yaml data=/path/to/obb-data.yaml epochs=100 imgsz=640

DDefConv 负责增强特征;角度回归、OBB 损失和旋转框后处理由 YOLO OBB 检测头负责。 数据标签格式和完整训练参数请参考 Ultralytics OBB 官方文档。

推荐实验方式

为了判断模块是否真正有效,建议至少保留以下对照组,并确保数据划分、随机种子、输入尺寸、训练轮数和增强策略一致。

实验 改动 关注指标
Baseline 官方 YOLO 配置 mAP、参数量、FLOPs、延迟、显存
DDefConv-P2 替换较浅层 stride=1 特征块 小目标召回率、边缘细节、延迟
DDefConv-P3/P4 替换中后层特征块 mAP、显存、速度
多层 DDefConv 替换两个或更多特征块 精度收益是否抵消计算开销

不要只比较训练集损失。至少报告验证集 mAP50、mAP50-95、参数量、单张延迟和峰值显存,并在相同硬件上进行测速。

常见问题

KeyError: 'DDefConvBlock'

通常是 tasks.py 的全局命名空间中没有该类。检查:

  1. ddef_conv.py 是否放在当前实际导入的 Ultralytics 源码中;
  2. ultralytics/nn/modules/__init__.py 是否导入并导出了该类;
  3. ultralytics/nn/tasks.py 是否导入了 DDefConvBlock;
  4. 是否使用 python -m pip install -e . 安装了当前源码。

AssertionError: in_channels 必须为偶数

输入通道必须能均分为两个分支。YOLO 中尽量使用 8 的倍数作为通道数,并确认宽度缩放后的实际输入通道仍为偶数。

offset、omega 或输出尺寸不一致

先确认 YAML 中使用的是:

DDefConvBlock, [c2, 3, 1]

即 k=3, s=1,并保持默认 p=1, d=1。当前版本不能直接承担 stride=2 下采样。

Couldn't load custom C++ ops 或找不到 torchvision::deform_conv2d

这通常表示 PyTorch 与 Torchvision 版本、CUDA 构建或运行环境不匹配。使用 PyTorch 官方给出的同一组安装命令重新安装二者,并先单独验证:

from torchvision.ops import deform_conv2d

加载 best.pt 时提示找不到自定义模块

请使用训练时同一份修改后的 Ultralytics 源码。自定义类的模块路径必须保持可导入;仅复制权重文件到一个未注册 DDefConv 的环境中通常无法直接加载。

ONNX、TensorRT、OpenVINO 导出失败

torchvision.ops.deform_conv2d 并非所有导出器和推理后端都原生支持。训练前如果最终目标是边缘部署,应尽早测试目标格式:

model.export(format="onnx")

必要时需要自定义 ONNX symbolic、TensorRT plugin,或改用目标后端已经支持的可变形卷积实现。

为什么参数量可能并不“轻量”

当前变形权重是 [C/2, C/2, k, k],参数量随通道数近似二次增长,因此不能仅凭“拆分了一半通道”就认定它是轻量算子。请以实际参数量、FLOPs、延迟和显存结果为准。

后续改进建议

  • 将变形权重改为 [C/2, 1, k, k],完成真正的逐通道可变形卷积,再用 1 × 1 pointwise convolution 混合通道。
  • 让偏移分支使用与主分支一致的 stride、padding 和 dilation,从而支持下采样。
  • 将 offset 卷积初始化为零或增加可控的 offset 初始化策略,评估训练稳定性。
  • 为调制 mask 增加预测分支,评估 DCNv2 风格调制是否有效。
  • 增加自动化测试:输出形状、前向/反向、混合精度、CPU/GPU 一致性和梯度检查。
  • 增加标准消融结果,而不是只报告单次基准耗时。
  • 若用于 OBB,独立验证 DDefConv 对角度误差和旋转框 mAP 的影响。

参考资料

License

仓库内的 DCNv4 上游代码及许可见 DDefConv0/LICENSE。将自定义模块复制进 Ultralytics 源码或发布衍生项目时,还应分别确认并遵守 DCNv4、Torchvision、Ultralytics 及其他依赖的许可证要求。