2、YOLOv8模型结构解析:Backbone、Neck、Head详解
YOLOv8的结构,说白了就是三个大模块串起来:Backbone(骨干网络)负责提特征,Neck(颈部)负责融合特征,Head(头部)负责输出预测。我刚开始接触时也觉得这名字挺唬人,拆开看其实没那么复杂。
一句话总结:Backbone是眼睛,Neck是大脑的整合区,Head是嘴巴——把看到的东西说出来。
2.1 整体架构概览
先看一张我画的整体流程图,你跟着箭头走一遍就明白了。
2.2 Backbone:特征提取的基石
Backbone 的任务很简单——把一张 640×640 的输入图,逐步下采样成不同尺度的特征图。YOLOv8 用的是 修改版的 CSPDarknet,核心是两个模块:C2f 和 SPPF。
C2f 模块:跨阶段部分连接
C2f 是 YOLOv8 对比 v5 最大的改动之一。我刚开始看代码时也觉得它跟 C3 差不多,仔细一对比才发现门道。
| 对比项 | YOLOv5 的 C3 | YOLOv8 的 C2f |
|---|---|---|
| 结构 | 3 个卷积 + 1 个 Bottleneck | 2 个卷积 + n 个 Bottleneck |
| 梯度流 | 单路分流 | 多路分流 + 拼接 |
| 参数量 | 相对较大 | 更轻量 |
| 特征复用 | 一般 | 更强(梯度丰富) |
我的经验:C2f 的「f」代表「feature fusion」。它把输入分成多路,每路经过 Bottleneck 后再拼回来。这样做的好处是——梯度可以沿着多条路径回传,训练时不容易梯度消失。我在做小目标检测时,换成 C2f 后召回率明显提升了。
看一段简化版的 C2f 实现逻辑:
class C2f(nn.Module):
def __init__(self, c1, c2, n=1, shortcut=True):
super().__init__()
self.cv1 = Conv(c1, c2 * 2, 1, 1) # 先升维
self.cv2 = Conv(c2 * (n + 2), c2, 1) # 再降维
self.m = nn.ModuleList(
[Bottleneck(c2, c2, shortcut) for _ in range(n)]
)
def forward(self, x):
y = list(self.cv1(x).chunk(2, 1)) # 分成两路
y.extend(m(y[-1]) for m in self.m) # 每路过 Bottleneck
return self.cv2(torch.cat(y, 1)) # 拼接后输出
SPPF 模块:快速空间金字塔池化
SPPF 是 SPP(空间金字塔池化)的加速版。为什么要做池化?因为网络最后需要固定尺寸的特征图,但输入图像大小不一。SPPF 用多个不同大小的池化核并行处理,然后把结果拼起来。
YOLOv8 用的 SPPF 比原版 SPP 快了 2 倍以上,效果还差不多。怎么做到的?
- 原版 SPP:分别用 5×5、9×9、13×13 三个池化核并行算
- SPPF:只用 5×5 的核,串行连三次,等效于 5、9、13 的感受野
避坑指南:我曾经在自定义数据集上尝试把 SPPF 换回原版 SPP,结果训练速度慢了 30%,mAP 只涨了 0.1%。得不偿失。除非你有特殊需求,否则别动 SPPF。
2.3 Neck:特征融合的桥梁
Neck 的作用是让不同尺度的特征图「对话」。YOLOv8 沿用了 FPN + PAN 的结构,我习惯叫它「双向特征金字塔」。
- FPN(自顶向下):把高层语义信息传递到低层,让大特征图也有「全局视野」
- PAN(自底向上):把低层位置信息传递到高层,让小特征图也有「细节感知」
你想想看,检测小目标时,低层的特征图分辨率高但语义弱;检测大目标时,高层的特征图语义强但分辨率低。Neck 就是来解决这个矛盾的。
YOLOv8 的 Neck 里也用了 C2f 模块做特征融合,而不是简单的卷积。这样融合后的特征更「干净」,噪声更少。
2.4 Head:解耦检测头
这是 YOLOv8 另一个大改动——从耦合头变成了解耦头。
| 类型 | YOLOv5 Head | YOLOv8 Head |
|---|---|---|
| 结构 | 一个卷积同时输出分类+回归 | 两个独立分支 |
| 分类分支 | 共用特征 | 独立卷积 + 分类损失 |
| 回归分支 | 共用特征 | 独立卷积 + 回归损失 |
| 训练稳定性 | 一般 | 更稳定(任务冲突少) |
为什么要解耦?因为分类和回归其实是两个不同的任务。分类关注「这是什么」,回归关注「它在哪里」。共用特征时,两个任务会互相干扰。解耦后,每个分支可以专注学自己的东西。
关键点:YOLOv8 的 Head 输出三个尺度的预测结果,分别对应 P3(小目标)、P4(中目标)、P5(大目标)。每个尺度上,特征图每个格子预测 4 + 1 + num_classes 个值:4 个坐标、1 个置信度、C 个类别概率。
2.5 模型配置文件解读(yaml)
YOLOv8 的模型结构定义在 .yaml 文件里。我刚开始看这文件时也觉得像天书,后来发现它其实就是个「积木清单」。
以 yolov8n.yaml 为例:
# Ultralytics YOLOv8n
nc: 80 # 类别数
scales: # 模型缩放系数
n: [0.33, 0.25, 1024] # depth, width, max_channels
backbone:
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
- [-1, 3, C2f, [128, True]] # 2
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 6, C2f, [256, True]] # 4
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 6, C2f, [512, True]] # 6
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 3, C2f, [1024, True]] # 8
- [-1, 1, SPPF, [1024, 5]] # 9
head:
- [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 10
- [[-1, 6], 1, Concat, [1]] # 11
- [-1, 3, C2f, [512]] # 12
- [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 13
- [[-1, 4], 1, Concat, [1]] # 14
- [-1, 3, C2f, [256]] # 15 (P3/8-small)
- [-1, 1, Conv, [256, 3, 2]] # 16
- [[-1, 12], 1, Concat, [1]] # 17
- [-1, 3, C2f, [512]] # 18 (P4/16-medium)
- [-1, 1, Conv, [512, 3, 2]] # 19
- [[-1, 9], 1, Concat, [1]] # 20
- [-1, 3, C2f, [1024]] # 21 (P5/32-large)
- [[15, 18, 21], 1, Detect, [nc]] # 22
解读这个文件,记住三个字段:
- 第一个数字:输入来自哪一层(-1 表示上一层)
- 第二个数字:该模块重复几次
- 第三个字段:模块类型(Conv / C2f / SPPF / Detect)
我的习惯:想改模型结构时,直接改 yaml 文件比改 Python 代码快得多。比如我想让 Backbone 更深,就在 C2f 那行把重复次数从 3 改成 4。想换检测头,直接替换 Detect 那行就行。
2.6 不同规模的模型对比
YOLOv8 提供了 n / s / m / l / x 五种规模,区别就在 scales 里的三个系数:
| 模型 | depth(深度系数) | width(宽度系数) | 参数量(M) | mAP@0.5:0.95 | 推理速度(T4) |
|---|---|---|---|---|---|
| YOLOv8n | 0.33 | 0.25 | 3.2 | 37.3 | 1.2ms |
| YOLOv8s | 0.33 | 0.50 | 11.2 | 44.9 | 1.6ms |
| YOLOv8m | 0.67 | 0.75 | 25.9 | 50.2 | 2.5ms |
| YOLOv8l | 1.00 | 1.00 | 43.7 | 52.9 | 3.8ms |
| YOLOv8x | 1.00 | 1.25 | 68.2 | 53.9 | 5.5ms |
避坑指南:别一上来就用 x 模型。我见过很多新手直接上最大模型,结果显存爆了、推理跑不动。先拿 n 或 s 跑通流程,确认数据没问题了,再换大模型提精度。
depth 控制 C2f 里 Bottleneck 的重复次数,width 控制每层卷积的通道数。这两个系数乘到基础值上,就得到了不同规模的模型。
嗯,到这里 YOLOv8 的结构你应该心里有数了。Backbone 提特征,Neck 做融合,Head 出结果——三个模块各司其职。下回你看到 yaml 文件,就知道每一行在干什么了。