YOLOv8 目标检测完整学习笔记
------ 从整体结构、Anchor-Free、DFL、置信度到训练与推理流程
适合已经听说过 YOLO、但希望真正理解 YOLOv8 工作流程的读者。
本文主要讨论经典的 YOLOv8 Detect 目标检测模型 。实例分割、姿态估计和旋转框检测是在检测模型基础上的扩展,不是本文重点。
Ultralytics 的主分支会持续演进,因此源码中可能同时出现后续模型的兼容逻辑。学习 YOLOv8 时,先抓住本文整理的核心路径,再阅读源码会轻松很多。
目录
- 先建立整体认识
- 目标检测到底要解决什么问题
- [YOLOv8 的整体网络结构](#YOLOv8 的整体网络结构)
- [Backbone、Neck 和 Detect Head 分别做什么](#Backbone、Neck 和 Detect Head 分别做什么)
- [C2f 模块:YOLOv8 为什么替换 YOLOv5 的 C3](#C2f 模块:YOLOv8 为什么替换 YOLOv5 的 C3)
- [多尺度检测:P3、P4、P5 是什么](#多尺度检测:P3、P4、P5 是什么)
- [从 YOLOv5 的 Anchor-Based 讲起](#从 YOLOv5 的 Anchor-Based 讲起)
- [YOLOv8 的 Anchor-Free 到底是什么意思](#YOLOv8 的 Anchor-Free 到底是什么意思)
- [YOLOv8 的参考点为什么位于 Cell 中心](#YOLOv8 的参考点为什么位于 Cell 中心)
- [YOLOv8 如何表示边界框](#YOLOv8 如何表示边界框)
- [DFL:为什么每条边预测 16 个离散位置](#DFL:为什么每条边预测 16 个离散位置)
- [DFL 推理解码:如何从 16 个 Logit 得到连续距离](#DFL 推理解码:如何从 16 个 Logit 得到连续距离)
- [DFLoss 训练损失:如何监督离散分布](#DFLoss 训练损失:如何监督离散分布)
- [为什么默认使用 reg_max = 16](#为什么默认使用 reg_max = 16)
- [YOLOv8 的置信度怎么算](#YOLOv8 的置信度怎么算)
- [Task-Aligned Assigner:正样本如何动态分配](#Task-Aligned Assigner:正样本如何动态分配)
- [YOLOv8 的损失函数](#YOLOv8 的损失函数)
- 训练流程完整串联
- 推理流程完整串联
- [为什么 ONNX 输出经常是 1 × 84 × 8400](#为什么 ONNX 输出经常是 1 × 84 × 8400)
- [YOLOv5 与 YOLOv8 对比表](#YOLOv5 与 YOLOv8 对比表)
- 常见误区
- 源码阅读顺序
- 最终记忆卡片
- 参考资料
1. 先建立整体认识
YOLOv8 是一个单阶段目标检测器。输入一张图像后,它会一次前向传播产生大量候选框,再经过置信度过滤和 NMS,输出最终检测结果。
可以先把它看成下面这条流水线:
text
输入图像
↓
预处理:缩放、补边、归一化
↓
Backbone:提取不同层次的图像特征
↓
Neck:融合浅层细节与深层语义
↓
Detect Head:预测类别分数与边界框
↓
边界框解码
↓
置信度阈值过滤
↓
NMS 去除重复框
↓
输出最终检测框、类别和置信度
如果你学过 YOLOv5,可以先记住 YOLOv8 的四个关键变化:
text
1. C3 → C2f
2. Anchor-Based → Anchor-Free
3. 耦合检测头 → 分类与回归分离的检测头
4. Anchor 尺寸匹配 → Task-Aligned 动态正样本分配
此外,YOLOv8 的边界框回归使用了 DFL:
text
直接回归一个距离
↓
预测距离的离散概率分布
↓
通过期望值恢复连续距离
2. 目标检测到底要解决什么问题
目标检测需要同时回答两个问题:
text
问题 1:图像里有什么?
问题 2:目标在哪里?
例如,对一张街道图片进行检测:
text
类别:car
边界框:[x1, y1, x2, y2]
置信度:0.91
其中:
text
x1, y1:边界框左上角坐标
x2, y2:边界框右下角坐标
YOLOv8 的检测头也自然分成两条路径:
text
分类分支:
这个候选框属于哪个类别?
回归分支:
这个候选框的四条边在哪里?
3. YOLOv8 的整体网络结构
标准 YOLOv8 Detect 使用三个尺度的特征图:
text
P3:stride = 8
P4:stride = 16
P5:stride = 32
假设模型输入尺寸为:
text
640 × 640 × 3
整体结构可以简化为:
text
输入图像:640 × 640
│
▼
Conv,stride = 2
320 × 320
│
▼
Conv,stride = 2 + C2f
160 × 160
│
▼
Conv,stride = 2 + C2f
80 × 80 ← P3,stride = 8
│
▼
Conv,stride = 2 + C2f
40 × 40 ← P4,stride = 16
│
▼
Conv,stride = 2 + C2f + SPPF
20 × 20 ← P5,stride = 32
│
▼
上采样 + Concat + C2f
│
▼
继续上采样 + Concat + C2f
│
▼
再向下采样 + Concat + C2f
│
▼
Detect(P3, P4, P5)
官方 yolov8.yaml 中明确列出了 Backbone 中的 C2f、SPPF,Head 中的上采样、拼接和三尺度 Detect 输出。
4. Backbone、Neck 和 Detect Head 分别做什么
4.1 Backbone:提取特征
Backbone 可以理解为"视觉信息提取器"。
浅层特征更关注:
text
边缘
纹理
颜色
局部形状
深层特征更关注:
text
目标整体结构
语义信息
类别相关信息
随着网络逐渐下采样,特征图尺寸越来越小,但每个特征点看到的输入图像区域越来越大,也就是感受野逐渐增大。
4.2 Neck:融合不同层次的信息
小目标需要细节,大目标需要较强的语义信息。
因此,YOLOv8 不会只使用最深层特征,而是将浅层、中层和深层特征融合起来。
可以粗略理解为:
text
深层特征:语义强,细节少
↓ 上采样
与中层特征拼接
↓
继续上采样
与浅层特征拼接
↓
再向下聚合
↓
输出 P3、P4、P5 三个尺度
这种设计与 FPN 和 PAN 的思想相近:让信息在不同层级之间双向流动。
4.3 Detect Head:产生最终预测
对于每一个特征图位置,YOLOv8 的 Detect Head 分成两条主要分支:
text
输入特征
├── Box 分支:预测边界框的距离分布
└── Cls 分支:预测每个类别的 Logit
标准设置下:
text
reg_max = 16
Box 分支输出维度 = 4 × 16 = 64
Cls 分支输出维度 = nc
其中:
text
4:左、上、右、下四个方向
16:每个方向的离散距离档位数量
nc:类别数量
5. C2f 模块:YOLOv8 为什么替换 YOLOv5 的 C3
5.1 YOLOv5 的 C3
YOLOv5 中常见的 C3 模块大致可以理解为:
text
输入 x
├── 分支 1:Conv → Bottleneck × N
└── 分支 2:Conv
↓
Concat
↓
Conv
重点是:
text
一个较深的加工分支
+
一个较浅的旁路分支
最后将二者拼接。
5.2 YOLOv8 的 C2f
C2f 会保留更多中间结果:
text
输入 x
↓
Conv
↓
拆分为 y0、y1
│
└── y1 → Bottleneck → y2
↓
Bottleneck → y3
↓
...
↓
Concat(y0, y1, y2, y3, ...)
↓
Conv
↓
输出
伪代码:
python
y0, y1 = split(conv1(x))
y2 = bottleneck_1(y1)
y3 = bottleneck_2(y2)
y4 = bottleneck_3(y3)
out = conv2(concat(y0, y1, y2, y3, y4))
C2f 的直观优势是:
text
保留多个中间阶段的特征
增加梯度传播路径
增强特征复用
需要注意:模型 FLOPs 较低不代表在所有设备上都一定更快。边缘端部署时,Split、Concat、内存访问和算子融合情况也会影响真实速度。
6. 多尺度检测:P3、P4、P5 是什么
假设输入为:
text
640 × 640
那么三个检测尺度为:
| 特征层 | Stride | 特征图尺寸 | 参考点数量 | 整体倾向 |
|---|---|---|---|---|
| P3 | 8 | 80 × 80 |
6400 |
更适合细节丰富的小目标 |
| P4 | 16 | 40 × 40 |
1600 |
更适合中等目标 |
| P5 | 32 | 20 × 20 |
400 |
更适合较大目标 |
总参考点数量:
text
80 × 80 + 40 × 40 + 20 × 20
= 6400 + 1600 + 400
= 8400
这里的"小、中、大"只是倾向,不是强制规则。
YOLOv8 不会编写类似这样的硬编码:
python
if area < 32 * 32:
assign_to_P3()
elif area < 96 * 96:
assign_to_P4()
else:
assign_to_P5()
实际正样本由动态分配机制决定。
7. 从 YOLOv5 的 Anchor-Based 讲起
理解 YOLOv8 的 Anchor-Free 之前,先回顾经典 YOLOv5。
7.1 YOLOv5 使用预设 Anchor Box
YOLOv5 会为 P3、P4、P5 预先设置不同大小的 Anchor:
yaml
anchors:
- [10, 13, 16, 30, 33, 23] # P3
- [30, 61, 62, 45, 59, 119] # P4
- [116, 90, 156, 198, 373, 326] # P5
每层有 3 个宽高模板。
对于 640 × 640 输入:
text
P3:80 × 80 × 3 = 19200
P4:40 × 40 × 3 = 4800
P5:20 × 20 × 3 = 1200
--------------------------
总计:25200 个候选 Anchor
每一个 Anchor 预测:
text
tx, ty, tw, th, objectness, class_1, class_2, ...
7.2 YOLOv5 的 Anchor 是人为固定的吗
准确答案是:
text
有预设先验,但不是完全僵硬的人为分组。
默认 Anchor 会提前写在模型配置文件中。自定义数据集训练时,经典 YOLOv5 还可以使用 AutoAnchor 检查默认 Anchor 是否适合数据集,并在必要时重新聚类生成更匹配的 Anchor。
但是,训练正式开始后,Anchor 通常不会像卷积核权重那样随每个 Batch 反向传播更新。
7.3 YOLOv5 是否强制小目标只能交给 P3
不是。
YOLOv5 不会直接按面积将 GT 框硬切分到某一个层,而是比较:
text
GT 的宽高
与
Anchor 的宽高
如果比例差异在允许范围内,则对应 Anchor 可以成为正样本。
因此,一个 GT 可能同时匹配:
text
P3 的某些 Anchor
+
P4 的某些 Anchor
+
邻近 Cell
P3、P4、P5 形成的是一种由 Anchor 尺寸自然诱导出的分工,不是绝对规则。
7.4 YOLOv5 的解码方式
YOLOv5 中心点解码公式可以简化为:
text
x = [2 × sigmoid(tx) - 0.5 + grid_x] × stride
y = [2 × sigmoid(ty) - 0.5 + grid_y] × stride
宽高解码:
text
w = [2 × sigmoid(tw)]² × anchor_w
h = [2 × sigmoid(th)]² × anchor_h
这里:
text
grid_x, grid_y
可以理解为 Cell 左上角对应的整数网格索引。
但这不代表预测框中心固定在 Cell 左上角。网络会继续预测中心偏移量。
当:
text
tx = 0
ty = 0
由于:
text
sigmoid(0) = 0.5
预测中心恰好落在 Cell 中心。
8. YOLOv8 的 Anchor-Free 到底是什么意思
YOLOv8 取消了 YOLOv5 那种带宽高先验的 Anchor Box。
对于 640 × 640 输入:
text
P3:80 × 80 = 6400
P4:40 × 40 = 1600
P5:20 × 20 = 400
------------------------
总计:8400 个参考点
它不再为每个 Cell 绑定 3 个 Anchor 宽高模板。
对比:
text
YOLOv5:
每个 Cell × 3 个 Anchor Box
YOLOv8:
每个 Cell × 1 个中心参考点
8.1 Anchor-Free 是否等于完全没有 Anchor
不是。
阅读 YOLOv8 源码时,你仍然会看到:
python
make_anchors(...)
anchor_points
这里的 anchor_points 是参考点,不是 YOLOv5 中带宽高的 Anchor Box。
需要分清:
text
YOLOv5 Anchor Box:
带宽高先验,例如 (10, 13)、(16, 30)
YOLOv8 Anchor Point:
只有位置坐标,例如 (0.5, 0.5)、(1.5, 0.5)
因此,YOLOv8 的 Anchor-Free 更准确的含义是:
text
取消预设宽高模板
保留网格参考点
9. YOLOv8 的参考点为什么位于 Cell 中心
YOLOv8 生成参考点时,默认使用:
python
grid_cell_offset = 0.5
因此,特征图上的参考点坐标为:
text
(0.5, 0.5)
(1.5, 0.5)
(2.5, 0.5)
...
而不是:
text
(0, 0)
(1, 0)
(2, 0)
...
假设 P3 层:
text
stride = 8
第一个参考点映射到输入图像后:
text
(0.5 × 8, 0.5 × 8)
= (4, 4)
示意图:
text
输入图像上的第一个 8 × 8 区域
(0, 0) ┌──────────────┐
│ │
│ ● │
│ (4, 4) │
│ │
└──────────────┘ (8, 8)
●:YOLOv8 的固定参考点
YOLOv8 的参考点位于 Cell 中心,但最终预测框中心不一定与它重合。
10. YOLOv8 如何表示边界框
YOLOv8 不直接预测:
text
x_center, y_center, width, height
而是预测参考点到四条边的距离:
text
l:参考点到左边界的距离
t:参考点到上边界的距离
r:参考点到右边界的距离
b:参考点到下边界的距离
示意图:
text
t
↑
┌────────────────────┐
│ │
│ ● A │
l ← │ 参考点 │ → r
│ │
└────────────────────┘
↓
b
假设参考点为:
text
A = (ax, ay)
则边界框为:
text
x1 = ax - l
y1 = ay - t
x2 = ax + r
y2 = ay + b
例如:
text
参考点 = (10.5, 8.5)
l = 2
t = 1
r = 4
b = 3
得到:
text
x1 = 10.5 - 2 = 8.5
y1 = 8.5 - 1 = 7.5
x2 = 10.5 + 4 = 14.5
y2 = 8.5 + 3 = 11.5
注意:
text
预测框中心 ≠ 固定参考点
只有:
text
l = r
并且
t = b
时,两者才重合。
11. DFL:为什么每条边预测 16 个离散位置
如果直接回归,可以让网络输出:
text
l = 3.6
t = 1.8
r = 5.2
b = 4.1
但是 YOLOv8 不直接输出四个普通浮点数,而是为每一个方向预测一组离散分布。
标准设置:
text
reg_max = 16
每条边对应 16 个 Logit:
text
0, 1, 2, 3, ..., 15
四条边总共:
text
4 × 16 = 64 个回归 Logit
拆开来看:
text
l:16 个 Logit
t:16 个 Logit
r:16 个 Logit
b:16 个 Logit
11.1 为什么四个方向分别预测
不同边界的清晰程度可能不同。
例如,一个人站在桌子后面:
text
左边缘:清晰
右边缘:被另一个人遮挡
上边缘:清晰
下边缘:被桌子遮挡
模型对四条边的不确定性并不一致。
所以,更合理的方式是:
text
分别预测 l、t、r、b 的分布
11.2 16 个档位不是 16 个框
错误理解:
text
每个参考点预测 16 个边界框
正确理解:
text
每条边预测 16 个距离档位的相对可能性
对于一个参考点,完整输出仍然对应一个框,只是这个框的四条边使用概率分布表示。
11.3 不是在 Cell 内再划分 16 个小格子
错误理解:
text
将每个 Cell 再切成 16 份
正确理解:
text
16 个档位代表距离为:
0、1、2、...、15 个特征图单位
假设:
text
stride = 8
则:
text
档位 0 → 0 像素
档位 1 → 8 像素
档位 2 → 16 像素
...
档位 15 → 120 像素
但模型最后不是只能输出 24 或 32 像素。因为它会进行概率加权求和,所以仍然可以得到:
text
3.62 × 8 = 28.96 像素
12. DFL 推理解码:如何从 16 个 Logit 得到连续距离
以左侧距离 l 为例。
模型先输出 16 个 Logit:
text
z0, z1, z2, ..., z15
这些 Logit 经过 Softmax:
text
p0, p1, p2, ..., p15
满足:
text
p0 + p1 + p2 + ... + p15 = 1
最终距离通过期望值计算:
text
l = Σ(k × pk), k = 0 ... 15
数学形式:
text
l = 0 × p0 + 1 × p1 + 2 × p2 + ... + 15 × p15
例如:
| 档位 | 概率 |
|---|---|
| 2 | 0.02 |
| 3 | 0.38 |
| 4 | 0.56 |
| 5 | 0.04 |
| 其余 | 接近 0 |
则:
text
l ≈ 2 × 0.02
+ 3 × 0.38
+ 4 × 0.56
+ 5 × 0.04
≈ 3.62
所以最终距离仍然是连续数。
12.1 推理阶段的 DFL 模块
源码中有一个类叫:
python
DFL
虽然名字里有 Loss,但这个模块在前向过程中主要负责积分解码:
text
64 个回归 Logit
↓
拆成 l、t、r、b 四组
↓
每组执行 Softmax
↓
使用 [0, 1, 2, ..., 15] 加权求和
↓
得到 4 个连续距离
这一步之后,再结合参考点恢复边界框。
13. DFLoss 训练损失:如何监督离散分布
推理时使用 DFL 模块解码。
训练时还需要一个真正的损失函数:
python
DFLoss
二者不是同一个东西。
13.1 真实距离是浮点数怎么办
假设 GT 框相对于参考点的左侧距离为:
text
l_gt = 3.6
离散档位只有整数:
text
0, 1, 2, 3, 4, ..., 15
不能粗暴地四舍五入:
text
3.6 ≈ 4
否则会丢失细节。
DFLoss 会将监督信号分配给相邻两个整数。
13.2 将 3.6 拆分到 3 和 4
text
左侧档位:
floor(3.6) = 3
右侧档位:
3 + 1 = 4
权重:
text
档位 3 的权重:
4 - 3.6 = 0.4
档位 4 的权重:
3.6 - 3 = 0.6
理想监督:
| 档位 | 目标权重 |
|---|---|
| 3 | 0.4 |
| 4 | 0.6 |
| 其余 | 0 |
期望值:
text
3 × 0.4 + 4 × 0.6
= 3.6
DFLoss 可以简化写成:
text
Loss_DFL =
- 0.4 × log(p3)
- 0.6 × log(p4)
这样,模型会被鼓励将概率集中到真实值左右两个邻近档位。
14. 为什么默认使用 reg_max = 16
先给出结论:
text
16 不是数学上唯一正确的答案。
它是表达能力、距离范围与计算成本之间的工程折中。
标准 YOLOv8 Detect 中:
text
reg_max = 16
每条边有 16 个档位:
text
0 ~ 15
14.1 reg_max 太小的问题
假设:
text
reg_max = 4
那么档位只有:
text
0, 1, 2, 3
在 P3 层:
text
stride = 8
单条边能表达的距离范围非常有限:
text
大约 3 × 8 = 24 像素
很容易不足以覆盖较大的目标。
14.2 reg_max 太大的问题
假设:
text
reg_max = 256
边界框分支的输出通道数:
text
4 × 256 = 1024
标准设置只需要:
text
4 × 16 = 64
增大 reg_max 会提高:
text
输出通道数
计算量
显存占用
内存带宽压力
部署成本
而且,由于期望值已经能够恢复连续坐标,继续无限增大档位数量未必能带来明显收益。
14.3 reg_max 主要影响范围,不是简单的定位精度
当档位间隔固定为 1 个特征图单位时,增大 reg_max 首先扩大的是:
text
单条边能够表达的最大距离范围
标准 YOLOv8 中:
text
档位:0 ~ 15
大致对应:
| 检测层 | Stride | 单边可表达距离量级 |
|---|---|---|
| P3 | 8 | 约 15 × 8 = 120 像素 |
| P4 | 16 | 约 15 × 16 = 240 像素 |
| P5 | 32 | 约 15 × 32 = 480 像素 |
如果参考点位于目标内部较中心的位置,那么框宽或框高的覆盖量级可以达到单边距离的约两倍。
这些是帮助理解的近似值,不是人为设置的严格目标尺寸边界。
14.4 为什么源码会限制到 reg_max - 0.01
训练时,需要为真实距离找到左右两个相邻档位:
text
tl = floor(target)
tr = tl + 1
假设:
text
target = 15
则:
text
tl = 15
tr = 16
但档位只有:
text
0 ~ 15
索引 16 越界。
因此,训练时会将目标距离限制为:
text
小于 reg_max
源码中常见形式:
python
dist.clamp_(0, reg_max - 0.01)
15. YOLOv8 的置信度怎么算
这是 YOLOv8 与经典 YOLOv5 的另一处重要差异。
15.1 回顾 YOLOv5
经典 YOLOv5 每个 Anchor 会输出:
text
边界框参数
+
objectness
+
类别分数
推理时,某个类别的最终分数通常为:
text
confidence =
objectness × class_probability
例如:
text
objectness = 0.8
person_probability = 0.9
最终 person 置信度:
0.8 × 0.9 = 0.72
15.2 YOLOv8 没有独立 Objectness 通道
标准 YOLOv8 Detect 的检测头输出维度为:
text
nc + 4 × reg_max
而不是:
text
nc + 1 + 4 × reg_max
没有额外的 +1,因此没有经典 YOLOv5 式独立 Objectness 分支。
YOLOv8 的两条主要输出路径为:
text
Box 分支:4 × reg_max
Cls 分支:nc
15.3 分类分支先输出 Logit
假设有 3 个类别:
text
person
car
dog
某个参考点的分类分支输出:
text
person logit = 2.0
car logit = -1.0
dog logit = -2.2
这些不是最终置信度,需要分别经过 Sigmoid:
text
sigmoid(z) = 1 / (1 + exp(-z))
得到:
text
person score ≈ 0.881
car score ≈ 0.269
dog score ≈ 0.100
默认单标签路径中,通常取最大类别分数:
text
confidence = max(class_scores)
class_id = argmax(class_scores)
因此:
text
最终类别 = person
最终置信度 = 0.881
15.4 为什么使用 Sigmoid 而不是 Softmax
Softmax 会强制:
text
所有类别概率之和 = 1
Sigmoid 则是独立判断:
text
像不像 person?
像不像 car?
像不像 dog?
每个类别单独得到一个分数。
15.5 DFL 是否直接参与置信度计算
不会。
标准 YOLOv8 推理路径中:
text
DFL 分布
↓
解码边界框坐标
分类 Logit
↓
Sigmoid
↓
类别置信度
不会计算:
text
confidence = class_score × DFL_peak
也不会计算:
text
confidence = class_score × DFL_entropy
所以:
text
DFL 负责框画在哪里
Cls 分支负责分数有多高
但是在训练阶段,由于正样本分配同时考虑分类质量和定位质量,二者会间接关联。
16. Task-Aligned Assigner:正样本如何动态分配
训练目标检测器时,需要回答:
text
对于一个 GT 框,哪些预测位置应该负责学习它?
YOLOv8 使用 Task-Aligned Assigner,简称 TAL。
这一步是 YOLOv8 和 YOLOv5 的重要差异。
在训练阶段,模型必须回答:
对于一个真实目标,哪些预测位置应该负责学习它?
YOLOv8 使用 TaskAlignedAssigner,简称 TAL。
官方文档将其描述为:结合分类信息和定位信息,为真实目标分配正样本。 (Ultralytics Docs)
它的思路可以简化为四步。
16.1 第一步:筛选落在真实框内部的参考点
如果某个 Anchor Point 位于 Ground Truth 框外部,它通常不会成为该目标的候选正样本。
text
GT 框
┌───────────────────┐
│ × × │
│ × │
│ × │
└───────────────────┘
框内的参考点进入候选集合
16.2 第二步:计算分类和定位对齐程度
对于候选参考点,计算一个对齐指标:
text
alignment_metric = score^α × IoU^β
其中:
score:预测为真实类别的分数;IoU:预测框与真实框的重叠程度;α:分类分数的权重;β:定位质量的权重。
当前官方 v8DetectionLoss 中,常见设置为:
text
alpha = 0.5
beta = 6.0
topk = 10
(GitHub)
由于 β 较大,IoU 会产生较强影响。模型倾向于选择:
text
分类判断较可靠
并且
边界框位置较准确
的预测点作为正样本。
16.3 第三步:为每个 GT 选择 Top-K 候选点
每一个真实框会从候选点中挑选对齐程度较高的若干个点。
16.4 第四步:解决冲突
某个参考点可能同时落入两个真实框内。此时,分配器会根据重叠情况选择更合适的 GT。
官方实现中包含 select_highest_overlaps() 逻辑。 (GitHub)
16.5 与 YOLOv5 的区别
text
YOLOv5:
GT 与 Anchor 宽高模板是否匹配?
YOLOv8:
当前哪些参考点的分类与定位表现更适合负责该 GT?
YOLOv8 的正样本分配会随着模型状态变化,更加动态。
17. YOLOv8 的损失函数
YOLOv8 Detect 的损失主要包括三部分:
text
Loss =
λbox × Loss_box
+
λcls × Loss_cls
+
λdfl × Loss_dfl
17.1 Box Loss
Box Loss 关注:
text
预测框与 GT 框整体是否接近
通常使用 IoU 类损失,例如 CIoU。
可以粗略理解为:
text
预测框与 GT 框重叠越好
Loss 越小
17.2 Cls Loss
分类损失关注:
text
预测类别是否正确
YOLOv8 使用基于 Logit 的二分类交叉熵形式:
text
BCEWithLogitsLoss
注意:正样本的分类监督不是只包含简单硬标签。TAL 还会让定位质量影响目标分数,使类别分数在训练后包含一定的定位质量信息。
因此,YOLOv8 推理时直接使用类别分数,但它不应被机械理解成严格校准后的统计学概率。
17.3 DFL Loss
DFL Loss 关注:
text
l、t、r、b 四条边的距离分布
是否集中在真实距离附近
它与 IoU 类 Box Loss 互补:
text
IoU Loss:
从整体上约束完整边界框
DFL Loss:
细化四条边的位置分布
18. 训练流程完整串联
下面将 YOLOv8 的训练流程完整走一遍。
18.1 输入图像与标签
输入图像经过缩放、补边等预处理。
YOLO 常见标签格式:
text
class_id center_x center_y width height
这些坐标通常会归一化到:
text
[0, 1]
训练时再转换到模型需要的坐标系。
18.2 Backbone 与 Neck 提取多尺度特征
对于 640 × 640 输入:
text
P3:80 × 80
P4:40 × 40
P5:20 × 20
总参考点:
text
8400
18.3 检测头输出两类结果
对于每个参考点:
text
Box 分支:
64 个 Logit = 4 × 16
Cls 分支:
nc 个类别 Logit
假设 COCO 数据集:
text
nc = 80
则每个参考点训练阶段的主要原始输出维度:
text
64 + 80 = 144
18.4 生成 Cell 中心参考点
参考点:
text
(0.5, 0.5)
(1.5, 0.5)
...
不同检测层还有对应 Stride:
text
P3:8
P4:16
P5:32
18.5 使用 DFL 将分布解码为连续距离
对于每个方向:
text
16 个 Logit
↓ Softmax
16 个概率
↓ 加权求和
连续距离
得到:
text
l, t, r, b
18.6 恢复预测框
text
x1 = ax - l
y1 = ay - t
x2 = ax + r
y2 = ay + b
18.7 TAL 分配正样本
根据:
text
参考点是否在 GT 内
分类质量
预测框与 GT 的 IoU
Top-K 筛选
冲突处理
确定哪些参考点为正样本。
18.8 计算损失并反向传播
text
Box Loss
+
Cls Loss
+
DFL Loss
然后进行反向传播,更新卷积层等可学习参数。
19. 推理流程完整串联
下面以一张图像推理为例。
19.1 图像预处理
常见步骤:
text
读取图像
↓
Letterbox 缩放与补边
↓
BGR → RGB
↓
HWC → CHW
↓
归一化到 [0, 1]
↓
增加 Batch 维度
19.2 网络前向传播
得到三尺度特征:
text
P3、P4、P5
检测头产生:
text
Box 分支:距离分布
Cls 分支:类别 Logit
19.3 DFL 解码边界框
每个方向:
text
16 个 Logit
↓
Softmax
↓
期望值
↓
连续距离
然后结合参考点与 Stride 恢复边界框。
19.4 分类分支计算置信度
text
class_scores = sigmoid(class_logits)
默认单标签情况下:
text
confidence = max(class_scores)
class_id = argmax(class_scores)
19.5 阈值过滤
过滤掉:
text
confidence < conf_threshold
的候选框。
19.6 NMS 去除重复框
标准 YOLOv8 Detect 仍然需要 NMS。
NMS 逻辑:
text
选择置信度最高的框
↓
计算它与其他框的 IoU
↓
删除高度重叠的重复框
↓
在剩余框中继续重复
最终输出:
text
边界框
类别
置信度
20. 为什么 ONNX 输出经常是 1 × 84 × 8400
对于:
text
输入尺寸:640 × 640
类别数量:80
Batch Size:1
参考点数量:
text
80 × 80 + 40 × 40 + 20 × 20
= 8400
DFL 在模型图中完成解码后,每个参考点通常包含:
text
4 个边界框参数
+
80 个类别分数
=
84
因此,常见 ONNX 输出:
text
[1, 84, 8400]
不同导出工具或后端也可能输出:
text
[1, 8400, 84]
二者只是维度顺序不同。
20.1 为什么不是 144
训练阶段原始预测:
text
64 个回归 Logit
+
80 个分类 Logit
=
144
但是常见导出图通常已经包含:
text
64 个回归 Logit
↓
DFL 解码
↓
4 个边界框参数
所以最终变成:
text
4 + 80 = 84
21. YOLOv5 与 YOLOv8 对比表
| 对比项 | 经典 YOLOv5 | YOLOv8 Detect |
|---|---|---|
| 检测范式 | Anchor-Based | Anchor-Free |
| 参考对象 | 带宽高先验的 Anchor Box | Cell 中心 Anchor Point |
| 每个 Cell 的候选数量 | 通常 3 个 Anchor | 1 个参考点 |
640 × 640 输入候选数量 |
25200 |
8400 |
| Backbone 核心模块 | C3 | C2f |
| SPPF | 有 | 有 |
| 多尺度输出 | P3、P4、P5 | P3、P4、P5 |
| 检测头 | 耦合头 | 分类与回归分离 |
| 独立 Objectness | 有 | 无经典 YOLOv5 式独立通道 |
| 边界框参数化 | 中心点偏移 + 宽高缩放 | 点到四条边距离 ltrb |
| 是否依赖 Anchor 宽高 | 是 | 否 |
| 正样本分配 | GT 与 Anchor 尺寸匹配 | TAL 动态分配 |
| 框损失 | IoU 类 Loss | IoU 类 Loss + DFL |
| 常见置信度 | objectness × class_score |
sigmoid(class_logit) |
| 常见 ONNX 输出 | [1, 25200, 85] |
[1, 84, 8400] 或 [1, 8400, 84] |
| 后处理 | NMS | NMS |
22. 常见误区
误区 1:Anchor-Free 就是完全没有 Anchor
错误。
YOLOv8 没有 Anchor Box,但仍然有 Anchor Point。
text
没有宽高先验
≠
没有参考位置
误区 2:YOLOv8 的参考点就是预测框中心
错误。
参考点只是计算 l、t、r、b 的起点。
只有:
text
l = r
t = b
时,参考点才与预测框中心重合。
误区 3:reg_max = 16 表示预测 16 个框
错误。
text
每条边预测 16 个距离档位
四条边共 64 个 Logit
最终仍然恢复一个框
误区 4:16 个档位表示只能输出整数距离
错误。
通过概率期望:
text
distance = Σ(k × pk)
模型仍然可以输出:
text
3.62
7.18
11.47
这样的连续距离。
误区 5:DFL 是在 Cell 内继续划分 16 个小格子
错误。
档位代表到边界的距离:
text
0 ~ 15 个特征图单位
不是 Cell 内部二次划分。
误区 6:YOLOv8 置信度仍然是 obj × cls
错误。
经典 YOLOv5 才使用:
text
objectness × class_probability
标准 YOLOv8 Detect 没有独立 Objectness 通道。
误区 7:DFL 分布越尖锐,最终置信度一定越高
不一定。
推理时:
text
DFL → 解码框
Cls → 计算置信度
两者不会直接相乘。
误区 8:P3 只能检测小目标,P5 只能检测大目标
错误。
P3、P4、P5 具有不同的倾向,但正样本分配不是面积硬切分。
误区 9:YOLOv5 的 Anchor 在训练过程中不断更新
通常不是。
AutoAnchor 可以在训练开始前检查或重新生成 Anchor,但正式训练中 Anchor 一般保持固定。
23. 源码阅读顺序
学习 YOLOv8 时,不建议一开始遍历整个仓库。
推荐顺序:
| 顺序 | 文件 | 重点 |
|---|---|---|
| 1 | ultralytics/cfg/models/v8/yolov8.yaml |
Backbone、Neck、P3/P4/P5、Detect 输入 |
| 2 | ultralytics/nn/modules/block.py |
C2f、SPPF、DFL 积分解码模块 |
| 3 | ultralytics/nn/modules/head.py |
Detect Head、Box 分支、Cls 分支、reg_max |
| 4 | ultralytics/utils/tal.py |
make_anchors()、dist2bbox()、TAL |
| 5 | ultralytics/utils/loss.py |
DFLoss、BboxLoss、v8DetectionLoss |
| 6 | 导出的 ONNX 图 | [1, 84, 8400] 的来源 |
对照 YOLOv5 时,可阅读:
| 文件 | 重点 |
|---|---|
models/yolov5s.yaml |
Anchor Box 配置 |
models/yolo.py |
YOLOv5 Detect Head 与解码公式 |
utils/loss.py |
Anchor 匹配、build_targets() |
utils/autoanchor.py |
AutoAnchor |
24. 最终记忆卡片
24.1 用一句话描述 YOLOv8
text
YOLOv8 使用 C2f Backbone、类似 FPN/PAN 的多尺度融合结构和 Anchor-Free 解耦检测头;
它以 Cell 中心为参考点,为每个位置预测类别分数与 l、t、r、b 四条边的离散距离分布,
通过 DFL 解码连续边界框,并通过 TAL 动态分配正样本。
24.2 用一张流程图记住推理
text
输入图像
↓
Backbone + Neck
↓
P3、P4、P5 三尺度特征
↓
8400 个 Cell 中心参考点
↓
┌──────────────────────────────┐
│ Box 分支:每点 4 × 16 个 Logit │
│ Cls 分支:每点 nc 个 Logit │
└──────────────────────────────┘
↓ ↓
DFL:Softmax + 期望值 Sigmoid
↓ ↓
l、t、r、b 类别分数
↓ ↓
结合参考点与 Stride 置信度过滤
↓ ↓
边界框坐标 ───────────────→ NMS
↓
最终检测结果
24.3 三条公式
边界框解码:
text
x1 = ax - l
y1 = ay - t
x2 = ax + r
y2 = ay + b
DFL 距离期望:
text
distance = Σ(k × softmax(logits)k)
YOLOv8 类别置信度:
text
confidence_c = sigmoid(class_logit_c)
24.4 三个关键词
text
Anchor-Free
DFL
Task-Aligned Assigner
真正吃透这三部分后,YOLOv8 的训练、推理、ONNX 输出与后处理逻辑就能自然串起来。
25. 参考资料
以下资料适合配合本文阅读:
-
Ultralytics YOLOv8 官方说明:
-
YOLOv8 网络 YAML:
https://github.com/ultralytics/ultralytics/blob/main/ultralytics/cfg/models/v8/yolov8.yaml
-
Detect Head 源码文档:
-
C2f 与 DFL 模块源码文档:
-
TAL、
make_anchors()、dist2bbox()源码文档: -
DFLoss、BboxLoss、v8DetectionLoss 源码文档:
-
Generalized Focal Loss 论文:
-
经典 YOLOv5 仓库: