核心算子锻造(二)
上一篇 核心算子锻造(一),我们已经亲手完成了 YOLO 中最基础的 Conv 模块,也知道了Conv模块在稳定梯度方面的威力,接下来要干什么呢?
是持续堆叠Conv形成一个目标检测模型?还是,要“锻造”一些新的模块(算子)?
因为毕竟,这一卷的题目就叫“核心算子锻造”,总不能就只有Conv一个吧。
胡乱猜测总不是办法,挨个踩坑试错效率太低,显然也不合适。
第二卷中我们说过:我们不再完全闭门造车,而是会选择’YOLO11 OBB‘作为后续持续升级的参考蓝图。不知道下一步怎么走的时候,不妨看一看”蓝图“吧。
3.2 "偷师"藏经阁:直面 Ultralytics 源码
3.2.1 顺藤摸瓜:Ultralytics YOLO 总体结构
我们使用的是 Ultralytics YOLO v11的源码,可以从github上下载,这里要注意,不同版本的源码可能略有差别。
在直接打开某一个 Python 文件之前,我们先看一下 Ultralytics YOLO 源码的大体结构。
把与我们后续学习最相关的部分抽出来,大致是这样:
ultralytics/
│
├── cfg/ # 各类配置文件
│ └── models/
│ └── 11/
│ └── yolo11-obb.yaml # YOLO11 OBB 的模型结构蓝图
│
├── nn/ # 神经网络核心代码
│ ├── modules/
│ │ ├── conv.py # Conv、autopad 等基础卷积模块
│ │ ├── block.py # Bottleneck、C2f、C3k2、SPPF 等网络基础模块
│ │ └── head.py # Detect、OBB 等检测头
│ │
│ └── tasks.py # 根据 YAML 组装模型等核心逻辑
│
├── data/ # 数据读取、增强等
├── engine/ # 训练、验证、预测等流程
└── utils/ # 各类通用工具
看起来文件很多,但对我们现在而言,真正需要先关注的其实只有两块:
cfg/models/11/ → “模型准备怎么搭”
nn/modules/ → “这些模块具体是怎么造的”
一个负责画蓝图,一个负责造零件。
3.2.2 按图索骥:寻找核心算子
刚刚在对 Ultralytics YOLO 源码结构的注释中,我们写了:
cfg/models/11/ → 模型准备怎么搭
yolo11-obb.yaml → YOLO11 OBB 的模型结构蓝图
对于已经写完Conv不知道下一步怎么走的我们而言,这不正是我们需要的吗? yolo11-obb.yaml,就我们要找的蓝图。
第一次看到 .yaml 文件的读者不用紧张。YAML 本质上就是一种用文本保存配置信息的格式。
你可以把它理解成一张比较容易让人阅读的“配置表”。
例如: yolo11-obb.yaml文件里会写:
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]]
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 2, C3k2, [512, False, 0.25]]
...
head:
...
- [-1, 1, Conv, [256, 3, 2]]
- [[-1, 13], 1, Concat, [1]] # cat head P4
- [-1, 2, C3k2, [512, False]] # 19 (P4/16-medium)
...
意思很直白:
backbone 下面描述 Backbone 怎么搭
head 下面描述检测部分怎么搭
它不是网络模型本身,也不在这里完成卷积计算。它更像一张 模型装配清单:
第一个位置装什么模块,第二个位置装什么模块,某个模块重复几次,需要传入哪些参数……
真正负责读取这张清单、找到对应网络模块并组装网络的工作,后面会由 tasks.py 中的模型解析逻辑完成。
不过现在这些还都不是的重点,我们现在只做一件事:找模块名字
打开 yolo11-obb.yaml,我们先只看 Backbone 中的内容,因为这是模型的主体结构。
backbone:
# [from, repeats, module, args]
- [-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]]
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 2, C3k2, [512, False, 0.25]]
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 2, C3k2, [512, True]]
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 2, C3k2, [1024, True]]
- [-1, 1, SPPF, [1024, 5]] # 9
- [-1, 2, C2PSA, [1024]] # 10
现在我们只盯着第三个位置,也就是模块名字:
Conv
Conv
C3k2
...
SPPF
C2PSA
这时候,出现了一个我们认识的老朋友:
Conv
刚才在 main.py 里,我们把Conv模块一行一行写出来,现在它真正出现在了 YOLO11 OBB 的模型蓝图里。
而且它并不是只出现一次。YOLO 会反复使用 Conv 完成基础特征提取、通道调整和下采样。
继续往下看,新的模块名字出现了。
C3k2
SPPF
C2PSA
其中 C3k2 在 Backbone 和后续特征融合部分反复出现,显然是一个重要角色;SPPF 则出现在 Backbone 靠后的地方, C2PSA 出现在 Backbone 的最后位置。
对于锻造核心算子这个任务而言,下一步的任务,已经被 YAML 自己“点名”出来了:
Conv ← 已经完成
C3k2 ← 接下来重点
SPPF ← 接下来重点
C2PSA ← 稍后完成
这就是蓝图的作用了。
YOLO11 OBB 的结构蓝图告诉我们:想继续往下搭,这几个模块绕不开。
有一点要特别说明一下,C2PSA模块开始引入 Attention,也就是注意力机制,相对于卷积而言,这是另一条不一样的路径,为了行文的连续,我们把这个模块放到后面再讲,本卷除了Conv之外,我们主要关注C3k2和SPPF两个模块。
3.2.3 抽丝剥茧:梳理核心算子依赖
知道了要写这几个模块,但是这几个模块是干什么的?该怎么写呢?先写哪个后写哪个?
比如,YAML 可以告诉我们:
这里需要一个
C3k2。
可是它并不会告诉我们:
C3k2内部到底长什么样;- 它还依赖哪些更基础的模块;
- 如果要自己手搓,应该怎么写。
这就像一张汽车装配图上写着:
这里安装发动机
装配图告诉你“要发动机”,但它不会告诉你发动机里面的活塞、曲轴和气缸是怎么工作的。
想知道内部结构,还得继续顺着名字去看这些模块的实现源码。
我们再回到刚才的源码目录,block.py文件,顾名思义,应该就各类基础模块实现的地方:
block.py # Bottleneck、C2f、C3k2、SPPF 等网络基础模块
我们在block.py文件中搜索 C3k2,定位到C3k2模块的实现代码:
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)
)
发现 C3k2 继承自 C2f ,而 C2f 内部又使用 Bottleneck,再往下追 Bottleneck,原来还是Conv你小子。
特别说明:Ultralytics 官方源码中的 C3k2 其实还提供了另一条分支:当 c3k=True 时,内部的 Bottleneck 会换成 C3k。而 C3k 本身又建立在 C3 模块之上。为了不在这里同时引入过多新模块,我们这一阶段先实现 c3k=False 的基础版本,也就是只使用 Bottleneck。等后面需要完整对齐 YOLO11 官方结构时,再补上 C3/C3k 分支。
画成关系图就是这样:
┌─────────────────────┐
│ │
Conv → Bottleneck → C2f → C3k2
│
└── 内部暂时使用 Bottleneck
看到这里,虽然还未必知道各个模块该怎么写,但模块之间的实现顺序就已经非常清晰了。因为源码中的依赖关系已经给了我们最好的指引,我们要做的就是从底层开始一块块的构建:
Conv ← 已完成
↓
Bottleneck ← 下一步
↓
C2f
↓
C3k2
SPPF模块的梳理思路也是一样的,源码给了我们最好的指引。
注意,上述探索“藏经阁”的过程,我们并没有把 Ultralytics 源码从头读到尾。
我们实际上只做了三件事:
第一步:看整体目录
↓
知道模型蓝图和核心模块大概分别放在哪里
第二步:打开 yolo11-obb.yaml
↓
看看完整 OBB 网络到底使用了哪些模块
第三步:在block.py或其他实现文件中检索对应模块,梳理依赖关系
↓
发现 C3k2 → C2f → Bottleneck → Conv 的依赖关系
而这三步已经足够回答开头的问题:
Conv 造完以后,下一步到底造什么?
💡 关键洞察
第一次读一个大型深度学习项目的源码,最忌讳的就是“从第一行开始往后读”。
更高效的方法是:先从模型结构中找到自己想研究的模块,再顺着模块名定位源码,然后沿着依赖关系一层层往下追。
这样,庞大的源码仓库就不再是一片森林,而会慢慢变成一张可以导航的地图。
下一节 核心算子锻造(三),我们就沿着刚刚找到的这条依赖链逐步实现C3k2和SPPF两个模块。
这其中Bottleneck是一个非常重要的基础模块。它会第一次把一个极其重要的思想正式带进我们的网络:
残差连接。
而这个看似简单的“捷径”,正是深层网络能够继续往更深处走的重要武器之一。