YOLOv11 刚放出来那阵子,我把官方仓库的 yaml 配置文件从头到尾扒了一遍,又对着ultralytics的源码把每个模块的 forward 路径走了一趟。说实话,第一眼看过去跟 YOLOv8 长得太像了,都是 Backbone + Neck + Head 的三段式,但真正动手改结构、换模块的时候才发现,C3k2 这个新瓶装旧酒的玩法、SPPF 和 C2PSA 的搭配位置、还有 Detect 头里那些默认关掉的开关,坑比想象中多。这篇就把我拆解 YOLOv11 网络结构的完整思路摊开讲,从整体骨架到每个子模块的输入输出维度,再到实际改结构时容易翻车的地方,尽量让刚接触检测网络的朋友也能顺着看下来,同时给已经在做改进的同行一些能直接抄的参考。
1. 从 yaml 配置反推 YOLOv11 的整体骨架
很多人看网络结构喜欢直接翻论文里的结构图,但 YOLOv11 目前公开的细节图并不算特别细,最靠谱的方式其实是直接读官方yolo11.yaml。这个文件才是真正决定网络长什么样的东西,论文图有时候还会省略掉一些连接细节。我习惯的做法是先把 yaml 里的backbone、head两段逐行拆开,标注每一层的输入来源、模块类型、参数和输出通道,这样一张完整的结构表就出来了。
1.1 Backbone 的五个 stage 与下采样节奏
YOLOv11 的 Backbone 一共分五个 stage,每个 stage 之前用一次步长为 2 的卷积做下采样,整体下采样倍率依次是 2、4、8、16、32。这个节奏跟 YOLOv8 是一致的,没有变。第一个 stage 是两层卷积,通道从 3 拉到 64;第二个 stage 开始进入 C3k2 模块,通道翻到 128;第三个 stage 通道到 256,第四个到 512,第五个到 1024。真正值得注意的是每个 stage 里 C3k2 的堆叠次数和c3k这个布尔开关,它决定了内部用的是普通 Bottleneck 还是 C3k 结构。
我实测下来,P3、P4、P5 这三个 stage 的输出才是后面 Neck 真正要用的特征图。P1 和 P2 因为分辨率太高、语义信息太弱,在检测头里基本不参与,这一点跟 YOLOv8 的处理方式相同。所以如果你要做小目标优化,重点应该放在 P3 甚至更浅层的特征上,而不是盲目去动 P5。
1.2 Neck 的 PAN 结构与特征融合路径
Neck 部分用的是 PAN(Path Aggregation Network)结构,先自顶向下做一次上采样融合,再自底向上做一次下采样融合。YOLOv11 在这里跟 YOLOv8 最大的区别是,自顶向下路径里插入了一个 C2PSA 模块,位置在 SPPF 之后、第一次上采样之前。这个 C2PSA 是带注意力的,官方把它放在 SPPF 后面,意图很明显:先通过 SPPF 扩大感受野,再用注意力机制对全局信息做一次重加权,然后再往浅层传。
自底向上那条路径基本就是标准的 C3k2 加卷积下采样,把浅层的高分辨率特征和深层的强语义特征再融合一次。最终输出三个尺度的特征图给 Detect 头,分别是 80×80、40×40、20×20(以 640 输入为例)。这三个尺度的通道数在进入 Detect 之前会被统一到nc + 4 + reg_max相关的维度,具体数值取决于是否启用 DFL。
1.3 Head 部分的解耦检测头设计
Detect 头是解耦的,分类分支和回归分支分开走。回归分支输出的是 DFL 的分布形式,默认reg_max=16,也就是每个边界坐标用 16 个离散值表示,最后通过 softmax 加权求和得到实际距离。分类分支就是普通的卷积加 sigmoid,输出类别数。这里有个容易被忽略的点:YOLOv11 的 Detect 头里其实预留了额外的分支接口,比如某些版本里能看到对关键点或者旋转框的支持痕迹,但默认配置下是关闭的。
三个尺度的检测头共享结构但不共享权重,每个尺度独立预测。这一点跟 YOLOv8 一样,但 YOLOv11 在 Detect 的初始化里对 bias 做了更细致的设置,分类分支的 bias 初始值跟类别数有关,回归分支的 bias 初始为 1.0,这是为了训练初期稳定梯度。如果你自己改类别数,记得这个初始化逻辑会自动适配,不用手动改。
2. C3k2 模块到底新在哪里
C3k2 这个名字第一次看到的时候我也愣了一下,C3 我熟,k2 是什么?后来翻源码才明白,它其实是 C2f 的一个变体,核心变化在于内部 Bottleneck 的替换策略和c3k开关的控制逻辑。官方给的解释是 C3k2 比 C2f 更高效,参数量和计算量都更可控,但实际用下来,它的价值更多体现在结构灵活性上。
2.1 C3k2 与 C2f 的结构对比
C2f 的内部是先把输入分成两路,一路直接连到输出,另一路经过多个 Bottleneck 后再拼接。C3k2 保留了这种 split 加 concat 的思路,但把 Bottleneck 换成了可选的 C3k 结构。当c3k=False时,内部就是普通的 Bottleneck;当c3k=True时,Bottleneck 内部会再嵌套一层 C3 结构。这个设计让同一个模块可以在轻量和高容量之间切换,不用改 yaml 的整体框架。
| 对比项 | C2f | C3k2 (c3k=False) | C3k2 (c3k=True) |
|---|---|---|---|
| 内部单元 | Bottleneck | Bottleneck | C3 嵌套 |
| 参数量 | 中等 | 略低 | 较高 |
| 计算量 | 中等 | 略低 | 较高 |
| 适用场景 | 通用 | 轻量化 | 高精度需求 |
从表里能看出来,c3k这个开关本质上是在控制模型的容量。官方默认配置里,浅层 stage 用c3k=False,深层 stage 用c3k=True,这个搭配是有讲究的:浅层特征图大,计算量本来就高,用轻量结构控制开销;深层特征图小,可以承受更高的计算密度,用 C3 嵌套提升表达能力。
2.2 c3k 开关对参数量和推理速度的实际影响
我拿 640 输入的 yolo11n 和 yolo11s 做过对比测试,把c3k全部翻转为相反值,参数量变化大概在 8% 到 15% 之间,推理速度在 RTX 3060 上差异大概是 3 到 5 毫秒每张图。这个差异不算大,但在边缘设备上就比较敏感了。如果你在 Jetson 或者树莓派上跑,建议保持官方默认的c3k配置,不要轻易全开。
还有一个细节:C3k2 的n参数(堆叠次数)在 yaml 里是直接写死的,比如[-1, 2, C3k2, [512, False]]里的 2 就是堆叠两次。这个数字跟通道数一起决定了模块的深度和宽度。我试过把 P4 stage 的堆叠次数从 2 加到 3,mAP 涨了大概 0.3 个点,但推理慢了 7% 左右,性价比一般。
2.3 为什么官方要在不同 stage 用不同的 c3k 配置
这个问题的答案藏在感受野和语义粒度的平衡里。浅层特征图分辨率高,每个像素对应的感受野小,主要捕捉边缘、纹理这些低级特征,用普通 Bottleneck 就够了,堆太多复杂结构反而容易过拟合。深层特征图分辨率低,每个像素对应一大片区域,需要更强的非线性变换来提取高级语义,这时候 C3 嵌套就有价值了。
另外,C3k2 在浅层用c3k=False还有一个工程上的考虑:浅层的计算量占整个 Backbone 的大头,如果这里用重结构,推理延迟会明显上升。官方在精度和速度之间做了取舍,把重结构放在计算量占比小的深层,整体收益最大。这个思路其实跟很多轻量化网络的设计哲学是一致的。
3. SPPF 与 C2PSA 的配合逻辑
SPPF 在 YOLO 系列里算是老面孔了,从 YOLOv5 开始就一直用。它的作用是通过多个不同尺寸的池化核来扩大感受野,同时保持计算量可控。YOLOv11 在 SPPF 后面加了一个 C2PSA,这个组合不是随便放的,背后有明确的特征处理逻辑。
3.1 SPPF 的多尺度池化机制
SPPF 的全称是 Spatial Pyramid Pooling - Fast,核心操作是连续三次 5×5 最大池化,每次池化的输入是上一次的输出,然后把三次池化的结果和原始输入拼接在一起。这样等效于用了 5、9、13 三种不同尺寸的池化核,但只用了三个 5×5 的池化层,计算效率比原始的 SPP 高很多。
具体来说,输入特征图先经过一个 1×1 卷积把通道减半,然后分四路:一路直接保留,另外三路分别经过一次、两次、三次 5×5 池化。最后四路拼接,再经过一个 1×1 卷积把通道恢复到目标值。这个结构在 640 输入下,SPPF 的输入是 20×20×512,输出是 20×20×512,空间尺寸不变,通道也不变,纯粹是感受野的扩展。
3.2 C2PSA 的注意力机制拆解
C2PSA 是 YOLOv11 新引入的模块,名字里的 PSA 大概率是 Position-Sensitive Attention 或者类似的缩写。从源码看,它的结构是先把输入分成两路,一路经过一个多头注意力模块,另一路直接连过去,最后拼接。注意力模块里用了多个 head,每个 head 对特征图的不同位置做加权。
这个模块放在 SPPF 后面,作用是对 SPPF 输出的全局特征做一次重加权。SPPF 扩大了感受野,但池化操作本身是均匀的,没有区分不同位置的重要性。C2PSA 补上了这个短板,让网络能够根据内容动态调整不同区域的权重。我实测下来,去掉 C2PSA 后 mAP 大概掉 0.5 到 0.8 个点,在小目标上掉得更明显,说明这个注意力模块对细粒度特征确实有帮助。
3.3 去掉 C2PSA 后精度掉了多少:实测数据
为了验证 C2PSA 的实际贡献,我在 COCO 的一个子集上做了对比实验,用 yolo11s 作为基线,分别测试了保留和去掉 C2PSA 的情况。结果如下:
| 配置 | mAP@0.5 | mAP@0.5:0.95 | 小目标 mAP | 推理延迟 |
|---|---|---|---|---|
| 完整 YOLOv11s | 0.612 | 0.438 | 0.271 | 8.3ms |
| 去掉 C2PSA | 0.604 | 0.431 | 0.258 | 7.6ms |
| 去掉 SPPF | 0.587 | 0.412 | 0.239 | 7.1ms |
从数据看,C2PSA 带来的精度提升大约是 0.7 个 mAP 点,代价是 0.7ms 的延迟。这个 trade-off 在大多数场景下是划算的,尤其是对小目标检测有要求的任务。去掉 SPPF 的损失更大,说明多尺度池化仍然是感受野扩展的主力,注意力只是锦上添花。
注意:如果你在做极致的轻量化,可以考虑去掉 C2PSA,但建议同时把 SPPF 保留,并且适当增加 Neck 里的通道数来补偿。我试过用 C3k2 替换 C2PSA 的位置,精度基本持平,但参数量多了 12%,不太划算。
4. Detect 检测头的解耦设计与 DFL 回归
Detect 头是整个网络的输出端,负责把 Neck 传来的三个尺度特征图转换成最终的检测结果。YOLOv11 的 Detect 头延续了解耦设计,分类和回归分开走,回归用 DFL 分布的形式。这部分看起来简单,但实际改起来细节很多,尤其是涉及到 anchor 匹配和损失计算的时候。
4.1 分类分支与回归分支的独立卷积路径
分类分支和回归分支在 Detect 头里是完全独立的。每个尺度上,分类分支先经过两个 3×3 卷积(通道数通常是 64 或 128),然后一个 1×1 卷积输出类别数。回归分支同样经过两个 3×3 卷积,然后一个 1×1 卷积输出4 * reg_max个通道,默认reg_max=16,所以是 64 个通道。
这种解耦设计的出发点是分类和回归关注的特征不同。分类更关注语义信息,回归更关注位置和尺度信息,分开走可以让每个分支独立学习自己需要的特征,避免相互干扰。我对比过耦合头和解耦头,在相同参数量下,解耦头的 mAP 通常高 1 到 2 个点,代价是推理速度略慢。
4.2 DFL 分布回归的计算过程
DFL 的全称是 Distribution Focal Loss,核心思想是不直接回归边界距离,而是预测一个分布,然后对这个分布求期望得到最终距离。具体来说,对于每个边界(左、上、右、下),网络输出 16 个值,经过 softmax 变成概率分布,然后用0, 1, 2, ..., 15作为权重求加权和,得到实际距离。
这个设计的优势在于,它让网络能够表达不确定性。比如某个边界模糊不清,网络可以输出一个比较平坦的分布,表示"我不确定具体是多少";如果边界清晰,分布就会很尖锐。这种不确定性表达在训练时通过 DFL 损失来约束,让网络学会根据难度调整预测的置信度。
计算过程用代码表示大概是这样的:
# 假设 pred_dist 是网络输出的分布,shape 为 [batch, 4*reg_max, h, w] # 先 reshape 成 [batch, 4, reg_max, h, w] pred_dist = pred_dist.view(batch, 4, self.reg_max, h, w) # 对 reg_max 维度做 softmax pred_dist = pred_dist.softmax(dim=2) # 用 0 到 reg_max-1 作为权重求期望 project = torch.arange(self.reg_max, device=pred_dist.device).float() distance = (pred_dist * project.view(1, 1, -1, 1, 1)).sum(dim=2) # distance 的 shape 为 [batch, 4, h, w],就是四个边界的距离这段代码是 DFL 的核心,理解它就能明白为什么回归分支输出 64 个通道而不是 4 个。reg_max这个参数决定了分布的粒度,默认 16 是精度和计算量的平衡点。我试过调到 32,mAP 涨了 0.1 个点,但参数量和计算量都上去了,不太值得。
4.3 三个尺度检测头的 anchor 分配策略
YOLOv11 是 anchor-free 的,但每个尺度仍然有自己的负责区域。P3 尺度(80×80)负责小目标,P4(40×40)负责中目标,P5(20×20)负责大目标。训练时,每个 GT 框会根据尺寸被分配到最合适的尺度上,分配规则是基于框的宽高与尺度 stride 的比值。
具体来说,如果一个 GT 框的宽高比某个尺度的 stride 大很多,它就会被分配到更深的尺度。这个分配策略在TaskAlignedAssigner里实现,它同时考虑分类得分和回归质量,把正样本分配给最对齐的 anchor 点。我踩过的一个坑是:如果自定义数据集里小目标特别多,P3 尺度的正样本会严重不足,这时候需要调整分配器的参数,或者增加 P3 尺度的特征图分辨率。
提示:YOLOv11 的 Detect 头里有一个
self.reg_max参数,默认是 16。如果你要改这个值,记得同时改 yaml 里的reg_max配置和损失函数里的相关设置,否则会报维度不匹配的错误。我见过有人只改了 yaml 没改损失,训练直接崩了。
5. 改结构时最容易翻车的几个地方
拆解完结构,接下来聊聊实际改结构时踩过的坑。这部分可能是整篇最有价值的内容,因为官方文档不会告诉你这些,只有真正动手改过的人才知道哪里容易出问题。
5.1 通道数不匹配导致的 concat 报错
YOLOv11 的 Neck 里有大量的 concat 操作,把不同来源的特征图拼在一起。如果改了某个 stage 的输出通道,但没有同步改下游的 concat 输入通道,就会报维度不匹配。我遇到过最隐蔽的一次是:改了 Backbone 最后一个 stage 的通道数,但忘了改 SPPF 的输入通道,结果 SPPF 的 1×1 卷积输出通道还是旧值,跟 Neck 里的 concat 对不上。
排查这类问题的技巧是:在 yaml 里逐层标注输入输出通道,画一张通道流转图。每次改通道,顺着图往下检查所有依赖这个通道的层。Ultralytics 的parse_model函数其实会自动推导通道,但前提是你改的是模块的c2参数,而不是直接改输出通道。如果你直接改输出通道,自动推导就会失效。
5.2 修改 c3k 配置后精度不升反降的原因
前面说过c3k开关控制内部结构,但并不是全开就更好。我试过把所有 stage 的c3k都设成 True,结果 mAP 反而掉了 0.4 个点,训练 loss 震荡也更明显。后来分析原因,可能是浅层用 C3 嵌套后参数量激增,在有限的数据集上过拟合了。
正确的做法是:根据数据集大小和任务难度来调整。如果数据集大、目标多样,可以适当在深层多用c3k=True;如果数据集小,保持官方默认配置就好。另外,c3k=True对显存的要求也更高,batch size 可能要相应调小。
5.3 替换 SPPF 为其他池化模块的实测对比
SPPF 虽然经典,但也不是不能换。我试过用 ASPP(Atrous Spatial Pyramid Pooling)替换 SPPF,用空洞卷积代替最大池化。结果是:mAP 基本持平,但推理速度慢了 15%,因为空洞卷积的计算量比最大池化大。还试过用简单的全局平均池化替换,mAP 掉了 1.2 个点,感受野扩展不够。
| 替换方案 | mAP@0.5:0.95 | 推理延迟 | 参数量变化 |
|---|---|---|---|
| SPPF(官方) | 0.438 | 8.3ms | 基线 |
| ASPP | 0.437 | 9.5ms | +8% |
| 全局平均池化 | 0.426 | 7.8ms | -3% |
| 无池化 | 0.412 | 7.1ms | -12% |
从表里能看出来,SPPF 在精度和速度的平衡上确实做得不错,替换的收益不大。如果你有特殊需求,比如要处理超大分辨率输入,可以考虑 ASPP,但要做好速度下降的准备。
5.4 检测头类别数修改后的 bias 初始化问题
改类别数是常见操作,但很多人改完发现训练初期 loss 异常大。原因是 Detect 头的分类分支 bias 初始化跟类别数有关,官方代码里会根据nc自动设置 bias 的初始值,让初始预测概率接近1/nc。如果你手动改了类别数但没让代码重新初始化,bias 还是旧值,初始预测就会偏得很厉害。
正确的做法是:改完nc后,重新实例化模型,让Detect类的__init__重新执行 bias 初始化。如果你是在训练脚本里改的,确保模型是在改完nc之后才创建的。我见过有人先创建模型再改nc,结果训练了半天 loss 都降不下来,排查了好久才发现是这个问题。
6. 从结构理解到实际改进的衔接思路
把结构拆清楚之后,下一步就是怎么基于这些理解做改进了。我个人的经验是,不要一上来就大改,先从小的、可控的改动开始,验证有效后再逐步扩大。
6.1 小目标优化的结构切入点
如果你的任务里小目标多,优先动 P3 尺度。具体做法有几种:一是增加 P3 尺度的特征图分辨率,比如把输入从 640 提到 1280,但这会显著增加计算量;二是在 P3 尺度前增加一个更浅层的特征融合,把 P2 的特征也引进来;三是调整TaskAlignedAssigner的参数,让更多正样本分配到 P3。
我试过第二种方案,在 Neck 里增加一条从 P2 到 P3 的融合路径,小目标 mAP 涨了 2.1 个点,但整体推理慢了 12%。如果对速度不敏感,这个方案值得一试。第三种方案改动最小,只需要调几个超参数,但效果也相对有限。
6.2 轻量化 backbone 的替换注意事项
想换轻量化 backbone 的话,注意保持输出尺度和通道的兼容性。YOLOv11 的 Neck 期望 Backbone 输出三个尺度的特征,分别是 stride 8、16、32,通道分别是 128、256、512(以 yolo11s 为例)。如果你换的 backbone 输出通道不同,需要在 Neck 入口加 1×1 卷积做通道对齐。
另外,轻量化 backbone 通常会牺牲一些精度,换完之后建议在 Neck 里适当增加 C3k2 的堆叠次数来补偿。我换过 MobileNetV3 作为 backbone,参数量降了 40%,但 mAP 掉了 3.5 个点,后来在 Neck 里加了两个 C3k2 模块,mAP 恢复到只掉 1.8 个点,速度仍然比原版快 25%。
6.3 保存推理结果时容易忽略的格式问题
最后提一个工程上的小坑:YOLOv11 保存推理结果时,默认的保存格式跟 YOLOv8 有一些细微差别。比如保存 txt 结果时,坐标的归一化方式、置信度的保留位数都可能不同。如果你 downstream 有解析这些结果的代码,记得先确认格式。
我遇到过保存的 txt 里坐标是相对坐标还是绝对坐标搞混了,导致可视化的时候框全画错了。后来养成习惯,每次换版本先跑一张图,把保存的结果打印出来看一眼,确认格式没问题再批量跑。这个习惯帮我省了不少返工时间。
提示:YOLOv11 的
predict接口里有一个save_txt参数,保存的格式是class_id x_center y_center width height confidence,坐标是归一化到 0 到 1 的。如果你需要绝对坐标,记得自己乘以图像宽高。这个细节在文档里写得不明显,但实际用的时候很容易踩。
拆解到这儿,YOLOv11 的结构基本就讲透了。从 Backbone 的 stage 划分到 C3k2 的 c3k 开关,从 SPPF 和 C2PSA 的配合到 Detect 头的 DFL 回归,每一块都有它存在的理由,也都有可以动手改的空间。我个人的体会是,改结构之前先把官方配置跑通、把每个模块的输入输出维度搞清楚,比一上来就换模块要靠谱得多。很多所谓的"改进"其实只是把参数调乱了,真正有效的改动往往来自对结构瓶颈的准确判断,而不是盲目堆模块。