4. YOLOv8模型架构(下)
上一讲我们把Backbone聊透了。这一讲咱们把剩下的三块拼图补上:Neck、Head和损失函数。这三块决定了模型能不能把特征用好、能不能精准预测、能不能快速收敛。
我个人习惯把Neck+Head比作“漏斗”——Neck负责把多尺度的特征揉在一起,Head负责从揉好的特征里“挤”出结果。而损失函数,就是告诉模型挤得对不对的裁判。
4.1 Neck结构:FPN + PAN的黄金组合
YOLOv8的Neck沿用了YOLOv5/v7的经典设计:FPN(特征金字塔) + PAN(路径聚合网络)。说白了,就是一条自上而下的通路加一条自下而上的通路。
为什么要这么设计?
你想想看,Backbone输出的特征图有多层:浅层分辨率高、语义弱;深层分辨率低、语义强。检测小目标需要浅层的细节,检测大目标需要深层的语义。FPN把深层的语义信息“向下传”,让浅层也能“看懂”自己在检测什么。PAN则反过来,把浅层的定位信息“向上传”,让深层也能“找准”目标在哪。
我在项目中遇到过一个问题:只用FPN时,小目标检测还行,但大目标的边框总有点偏。加上PAN之后,大目标的定位精度明显提升。嗯,这就是PAN带来的位置信息补偿。
具体结构上,YOLOv8的Neck做了几点优化:
- 去掉了C3模块,改用C2f模块——和Backbone保持一致,减少梯度消失风险
- 保留了上采样和下采样——FPN用上采样,PAN用下采样,尺度对齐
- 特征融合方式——直接相加(add),不是拼接(concat),减少参数量
下面这张图展示了Neck的整体流程:
4.2 Head结构:解耦头(Decoupled Head)
YOLOv8的Head和之前的版本有个重大区别:从耦合头变成了解耦头。
什么叫耦合头?就是分类和回归(边框预测)共享同一组卷积层,最后分出两个分支。YOLOv5就是这么干的。解耦头呢?分类和回归各自用独立的卷积层,互不干扰。
为什么要解耦?
我个人的理解是:分类任务关注的是“这是什么”,回归任务关注的是“它在哪里”。这两个任务的特征需求不一样。分类需要更强的语义信息,回归需要更精确的空间信息。硬把它们绑在一起,两边都受限制。
YOLOv8的解耦头结构如下:
| 组件 | 输入 | 操作 | 输出通道 |
|---|---|---|---|
| 共享卷积 | Neck输出特征 | 1×1 Conv + BN + SiLU | 128 |
| 分类分支 | 共享特征 | 2× 3×3 Conv + 1× 1×1 Conv | num_classes |
| 回归分支 | 共享特征 | 2× 3×3 Conv + 1× 1×1 Conv | 4 × reg_max |
这里有个细节:回归分支的输出是 4 × reg_max,不是直接输出4个坐标值。YOLOv8使用了Distribution Focal Loss(DFL)的思想,把每个坐标值建模成一个离散分布。reg_max默认是16,意味着每个坐标用16个bin来表示。这样做的好处是:边框预测更灵活,能适应不同形状的目标。
代码实现上,解耦头的核心逻辑大概是这样:
class Detect(nn.Module):
def __init__(self, nc=80, ch=()):
super().__init__()
self.nc = nc # 类别数
self.reg_max = 16 # DFL的bin数
# 分类分支
self.cv2 = nn.ModuleList(
nn.Sequential(Conv(x, 128, 3), Conv(128, 128, 3), nn.Conv2d(128, self.nc, 1))
for x in ch)
# 回归分支
self.cv3 = nn.ModuleList(
nn.Sequential(Conv(x, 128, 3), Conv(128, 128, 3), nn.Conv2d(128, 4 * self.reg_max, 1))
for x in ch)
def forward(self, x):
# x是Neck输出的多尺度特征列表
for i in range(len(x)):
# 分类和回归独立计算
cls_out = self.cv2[i](x[i])
reg_out = self.cv3[i](x[i])
# 后续处理...
4.3 损失函数设计
YOLOv8的损失函数由三部分组成:分类损失 + 回归损失 + DFL损失。嗯,比YOLOv5多了一个DFL损失。
具体来说:
- 分类损失:BCE Loss(二值交叉熵),和YOLOv5一样
- 回归损失:CIoU Loss(Complete IoU),比YOLOv5的GIoU更精细
- DFL损失:Distribution Focal Loss,用于优化边框的离散分布
总损失公式:
Loss = λ_cls × L_cls + λ_reg × L_ciou + λ_dfl × L_dfl
其中:
λ_cls = 0.5
λ_reg = 7.5
λ_dfl = 1.5
这三个权重是经验值。我个人习惯在训练自己的数据集时,如果目标尺寸差异很大,会把λ_reg调高一点,比如调到10.0。因为CIoU对大目标的惩罚更明显,调高回归权重能让大目标框得更准。
4.3.1 CIoU Loss
CIoU比普通的IoU多了两个维度:中心点距离和长宽比一致性。公式如下:
CIoU = IoU - (ρ²(b, b_gt) / c²) - αv
其中:
ρ²(b, b_gt) = 预测框和真实框中心点的欧氏距离平方
c = 最小外接矩形的对角线长度
v = 长宽比一致性度量
α = 平衡参数
为什么要用CIoU?
我举个例子:两个预测框和真实框的IoU相同,但一个中心点偏了,一个长宽比不对。普通IoU分不出好坏,CIoU能区分。它会让模型优先优化中心点偏移,再优化长宽比。这个优先级在实际检测中非常合理——位置对了,形状可以慢慢调;位置不对,形状再准也没用。
4.3.2 DFL Loss
DFL是YOLOv8新引入的损失。它的核心思想是:边框坐标不应该是一个确定值,而是一个分布。
举个例子:某个目标的x中心坐标,模型预测它在0.3到0.35之间,但真实值是0.33。传统方法直接回归0.33,DFL则让模型学习一个分布,比如在0.33附近的bin权重最高,两边逐渐降低。
DFL的公式:
DFL(S_i, S_{i+1}) = -((y_{i+1} - y) × log(S_i) + (y - y_i) × log(S_{i+1}))
其中:
y = 真实坐标值
y_i, y_{i+1} = 离y最近的两个离散bin
S_i, S_{i+1} = 这两个bin的预测概率
4.3.3 正负样本分配
YOLOv8的损失计算还有一个关键环节:Task-Aligned Assigner(任务对齐分配器)。这是YOLOv8在样本分配上的改进。
传统方法(比如YOLOv5)根据IoU阈值分配正负样本。YOLOv8则同时考虑分类得分和回归质量:
alignment_metric = cls_score^α × IoU^β
其中:
α = 1.0(分类权重)
β = 6.0(回归权重)
只有alignment_metric高的锚点才被选为正样本。这样做的好处是:分类好但框不准的锚点不会被选为正样本,框准但分类差的也不会。两者必须都好,才算正样本。
| 方法 | 分配依据 | 优点 | 缺点 |
|---|---|---|---|
| YOLOv5分配器 | IoU阈值 | 简单、速度快 | 忽略分类质量 |
| YOLOv8分配器 | 分类得分 × IoU | 任务对齐、精度高 | 计算稍复杂 |