3. YOLOv8基础:模型结构解析与自定义检测模型训练
好,咱们进入正题。YOLOv8是Ultralytics团队在2023年初推出的版本,说实话,它比之前的v5和v7都要“整饬”得多——代码结构清晰,训练流程标准化,导出ONNX几乎零门槛。我个人习惯把v8看作一个“工业化”的起点,而不是一个纯粹的学术创新。
这一章我们做两件事:第一,把v8的骨架拆开看看,搞清楚它到底长什么样;第二,用你自己的数据集训一个检测模型。这两步走完,你才算真正“拥有”了一个能用的模型,后面导出ONNX和TensorRT加速才有意义。
3.1 YOLOv8模型结构:从输入到输出的完整链路
YOLOv8的结构可以分成三块:Backbone(骨干网络)、Neck(颈部)和Head(检测头)。说白了,就是“看图片→提特征→做预测”这么个流程。
核心变化:v8相比v5最大的改动是Head部分——从Anchor-Based(锚框机制)换成了Anchor-Free(无锚框)。这意味着你不再需要手动调锚框尺寸了,模型自己学。我在项目里遇到过因为锚框设置不合理导致小目标漏检的情况,换成v8之后这个问题自然消失了。
下面这张图是我画的v8结构简图,你看一眼就能明白整体脉络:
3.2 关键模块详解:C2f 与 Detect Head
咱们挑两个最重要的模块细说一下。
3.2.1 C2f 模块
C2f 是 v8 对 v5 中 C3 模块的升级版。它把输入特征图分成两路:一路直接走 shortcut,另一路经过多个 Bottleneck 后再拼接。这样做的好处是——梯度在反向传播时有了“高速公路”,不会因为网络深就消失。
我的经验:如果你在训练时发现 loss 降不下去,或者模型收敛特别慢,可以检查一下 C2f 中的 Bottleneck 数量。v8n(nano版本)默认只有1个,v8l(large版本)有3个。我曾经在轻量级部署场景下把 v8n 的 Bottleneck 从1改成2,mAP 涨了1.2个点,推理速度只慢了3ms,很划算。
3.2.2 Detect Head(解耦头)
v8 的检测头是 Decoupled Head,也就是把分类和回归分开做。具体来说:
- 分类分支:输出每个 anchor point 对应每个类别的概率,形状为 [batch, num_classes, h, w]。
- 回归分支:输出每个 anchor point 到目标框四条边的距离,形状为 [batch, 4, h, w]。
为什么要分开?因为分类和回归关注的特征不一样——分类更关注“这是什么”,回归更关注“边界在哪”。共用参数反而会互相干扰。v5 用的是耦合头,v8 改成解耦头之后,收敛速度和精度都有提升。
注意:v8 的回归分支输出的是 dist(距离),而不是直接输出坐标。它用 Distribution Focal Loss(DFL)把坐标建模成离散分布,说白了就是把一个连续值拆成多个 bin 去预测。这样做对小目标的定位更精细,但也会增加一点计算量。导出 ONNX 时要注意 DFL 层的算子兼容性,我后面会专门讲。
3.3 训练一个自定义检测模型:完整流程
好,结构看完了,咱们动手训练一个。我假设你已经准备好了数据集——图片和对应的标注文件(YOLO格式的 .txt)。
3.3.1 数据集准备
YOLO格式的标注文件长这样:
class_id x_center y_center width height
注意,x_center、y_center、width、height 都是相对于图片宽高的归一化值(0~1)。举个例子,一张 640×640 的图片,目标框左上角在 (160, 160),右下角在 (480, 480),那么标注就是:
0 0.5 0.5 0.5 0.5
数据集目录结构建议这样组织:
datasets/
my_dataset/
images/
train/
img001.jpg
img002.jpg
...
val/
img100.jpg
...
labels/
train/
img001.txt
img002.txt
...
val/
img100.txt
...
3.3.2 编写配置文件
在 Ultralytics 框架下,你需要一个 YAML 文件来描述数据集和模型。我一般叫它 my_dataset.yaml:
# 数据集路径
path: ./datasets/my_dataset
train: images/train
val: images/val
# 类别信息
nc: 3 # 类别数量
names: ['person', 'car', 'bicycle'] # 类别名称
关键点:nc 和 names 的顺序必须和标注文件中的 class_id 一一对应。我见过有人把 names 写错了顺序,结果训练出来的模型把“人”识别成“车”,排查了半天才发现是配置问题。
3.3.3 开始训练
训练代码非常简洁,Ultralytics 把一切都封装好了:
from ultralytics import YOLO
# 加载预训练模型(推荐)
model = YOLO('yolov8n.pt')
# 开始训练
results = model.train(
data='my_dataset.yaml',
epochs=100,
imgsz=640,
batch=16,
device='cuda:0',
workers=4,
lr0=0.01,
augment=True,
project='runs/train',
name='my_exp'
)
几个参数我解释一下:
- epochs:我一般从100开始,观察 loss 曲线,如果50轮就收敛了就提前停止。
- imgsz:训练尺寸。640是默认值,如果你的目标很小(比如人脸),可以试试800或1024。
- batch:根据显存来调。RTX 3090 可以跑 batch=32,笔记本 3060 建议 8~16。
- lr0:初始学习率。0.01 是官方推荐值,但如果你用很小的 batch(比如4),建议降到 0.001,否则 loss 容易震荡。
3.3.4 训练过程中的关键指标
训练时终端会打印一堆指标,我挑几个重点说:
| 指标 | 含义 | 正常范围 |
|---|---|---|
| box_loss | 边界框回归损失 | 训练结束时 < 1.5 |
| cls_loss | 分类损失 | 训练结束时 < 1.0 |
| dfl_loss | 分布聚焦损失 | 训练结束时 < 1.2 |
| mAP50 | IoU阈值0.5时的平均精度 | 简单数据集 > 0.9,复杂数据集 > 0.7 |
| mAP50-95 | IoU从0.5到0.95的平均精度 | 通常比mAP50低0.1~0.2 |
避坑指南:我曾经在训练一个工业缺陷检测模型时,发现 box_loss 一直在降,但 mAP 纹丝不动。后来排查发现是标注文件里有些框的 width 或 height 写成了0(目标太小被误标)。YOLO 对 width=0 的框会直接忽略,但 loss 计算时又把它算进去了,导致 loss 虚低。所以训练前一定要做数据校验——检查标注文件里有没有0值。
3.3.5 训练完成后得到什么
训练结束后,在 runs/train/my_exp/ 目录下你会看到:
- weights/best.pt:验证集上 mAP 最高的权重,导出 ONNX 时用这个。
- weights/last.pt:最后一轮的权重,如果训练中断了可以接着训。
- results.csv:所有 epoch 的指标记录,可以用 Excel 或 pandas 画 loss 曲线。
- confusion_matrix.png:混淆矩阵,帮你分析哪些类别容易混淆。
3.4 验证模型效果
训练完了,跑个验证看看效果:
model = YOLO('runs/train/my_exp/weights/best.pt')
results = model.val(data='my_dataset.yaml')
print(results.box.map) # 打印 mAP50-95
如果想看单张图片的预测结果:
results = model('test.jpg', save=True, conf=0.5)
# 结果会保存在 runs/detect/predict/ 下
我的习惯:验证时我会把 conf 设成 0.25 而不是 0.5,这样能看到更多低置信度的预测框,方便判断模型是“漏检”还是“误检”。如果低置信度框里有很多正确的目标,说明模型其实学到了,只是置信度偏低,后面可以通过调整 NMS 阈值或增加训练轮数来改善。
3.5 本章小结
这一章我们做了三件事:
- 拆解了 YOLOv8 的 Backbone、Neck、Head 结构,重点讲了 C2f 和 Decoupled Head。
- 用 Ultralytics 框架训练了一个自定义检测模型,从数据准备到训练参数都过了一遍。
- 学会了验证模型效果,以及如何从训练日志中发现问题。