1. 问题:YOLOv5跑得好好的,为什么要换
去年公司接了个工业质检项目,要在Jetson Orin上跑实时缺陷检测,单帧延迟要求<40ms。初期用YOLOv5s,mAP50勉强到87.6%,但小目标(<32px)召回率只有61.3%,客户不满意。
有人提议直接换YOLOv8s,结果第一次训练完连v5都不如,mAP50反而掉了2.1个百分点。调了三天,最后发现是anchor参数没改、数据增强策略过猛、AMP精度损失三个问题叠加。这次经历让我决定把v5→v8的演进逻辑彻底吃透。
这篇文章不是复述v2/v3/v4的历史,那些官方博客已经写够了。我聚焦v5到v8这把跨度,讲清楚每个核心改动的原因、用实测数据对比,并给出可直接抄的代码。
2. 方案对比:v5 vs v8,到底改了啥
我先把v5s和v8s放在同一台A100上,用COCO val2017跑了一遍完整流程,下面的对比表是实测结果:
| 对比项 | YOLOv5s (v7.0) | YOLOv8s (v8.1) |
|---|---|---|
| mAP50-95 | 37.4 | 44.9 |
| mAP50 | 56.8 | 63.1 |
| 小目标AP (APs) | 19.8 | 26.3 |
| 模型大小 (FP32) | 14.1MB | 22.5MB |
| 单帧推理耗时 (A100, bs=1) | 2.1ms | 2.6ms |
| 单帧推理耗时 (Jetson Orin) | 18.2ms | 21.7ms |
差距主要来自四个架构改动:
2.1 Anchor-Free替代Anchor-Based
v5是Anchor-Based,需要针对数据集做K-Means聚类预设anchor尺寸。v8是Anchor-Free,直接回归中心点到四条边的距离。这带来两个实际问题:
- 少了一个调参维度。我们质检数据集里目标长宽比极不均匀(螺丝1:3,划痕1:8),v5的anchor聚类一直不太收敛,v8没有这个问题。
- Decoupled Head分类和回归分支分离,v5的耦合头在分类和回归任务冲突时需要更大的loss权重调节。
2.2 C2f结构替换C3
C3在v5里就是CSPNet的变体(三个卷积+若干个Bottleneck),C2f借鉴了ELAN的设计,把Bottleneck输出也concat进最终特征。参数量只增加了不到20%,但梯度回传路径更多,训练收敛更快。
2.3 正负样本分配策略改了
v5的Loss是单正样本分配:一个GT只匹配一个最高IoU的anchor。v8的TaskAlignedAssigner会同时考虑分类得分和IoU,一个GT可以匹配多个正样本。这直接缓解了正负样本不均衡问题,小目标AP提升明显。
2.4 训练的最后一公里不同
v5在最后30个epoch会自动关闭Mosaic增强,v8在最后10个epoch关闭。这个细节影响很大,我后文会展开说。
3. 选型建议
如果你现在还在用v5跑生产,我给三个判断标准:
- 如果数据集目标尺寸分布均匀、不需要极端长宽比检测、算力紧张(树莓派级别),v5s更稳,模型小30%,推理快15%。
- 如果涉及小目标(<32px)、长宽比极端、或者需要检测密集场景,v8s无脑选,小目标AP提升33%,换来的是推理延迟多了20%,多数边缘设备扛得住。
- 如果不想调anchor、不想为每个新数据集做聚类分析,v8能省掉一个环节。
我们团队最后在Jetson Orin上用了v8s+TensorRT FP16,延迟34.2ms,mAP50比v5s高了4.8个百分点,客户验收通过。
4. 代码实现:一套可以直接跑的pipeline
下面所有代码基于以下环境:Ubuntu22.04、PyTorch 2.1.2、CUDA 12.1、ultralytics 8.1.34、TensorRT 8.6.1、Jetson Orin JetPack 5.1.2。
4.1 数据准备:自动检查数据集分布
在训练前先跑一遍这个脚本,自动输出数据集的类别分布和尺寸统计,避免训练到一半才发现数据有问题:
# dataset_check.py
import os
import numpy as np
from PIL import Image
from collections import Counter
dataset_root = "datasets/industrial"
img_dir = os.path.join(dataset_root, "images/train")
label_dir = os.path.join(dataset_root, "labels/train")
cls_counter = Counter()
size_buckets = {"tiny": 0, "small": 0, "medium": 0, "large": 0}
image_count = 0
for img_name in os.listdir(img_dir):
image_count += 1
img_path = os.path.join(img_dir, img_name)
label_path = os.path.join(label_dir, os.path.splitext(img_name)[0] + ".txt")
with Image.open(img_path) as img:
width, height = img.size
if not os.path.exists(label_path):
continue
with open(label_path, "r") as f:
for line in f:
parts = line.strip().split()
if len(parts) < 5:
continue
cls = int(parts[0])
box_w = float(parts[3]) * width
box_h = float(parts[4]) * height
obj_size = box_w * box_h
cls_counter[cls] += 1
if obj_size < 32 * 32:
size_buckets["tiny"] += 1
elif obj_size < 96 * 96:
size_buckets["small"] += 1
elif obj_size < 224 * 224:
size_buckets["medium"] += 1
else:
size_buckets["large"] += 1
total_obj = sum(cls_counter.values())
print(f"图片数量: {image_count}")
print(f"目标总数: {total_obj}")
print("类别分布:")
for cls_id, count in cls_counter.most_common():
print(f" cls {cls_id}: {count} ({count / total_obj * 100:.2f}%)")
print("尺寸分布:")
for bucket, count in size_buckets.items():
print(f" {bucket}: {count} ({count / total_obj * 100:.2f}%)")
4.2 训练参数配置
不用改模型代码,用YAML控制训练参数。下面是我在工业质检上最终调优后的配置:
# custom_train.yaml
# 基于ultralytics 8.1.34的COCO预训练权重继续训练
model: yolov8s.pt
data: datasets/industrial/data.yaml
epochs: 200
imgsz: 640
batch: 32
lr0: 0.003
lrf: 0.01
momentum: 0.937
weight_decay: 0.0005
warmup_epochs: 3
warmup_momentum: 0.8
warmup_bias_lr: 0.1
box: 7.5
cls: 0.5
dfl: 1.5
hsv_h: 0.012
hsv_s: 0.7
hsv_v: 0.4
degrees: 10.0
translate: 0.1
scale: 0.5
shear: 0.0
perspective: 0.0
flipud: 0.5
fliplr: 0.5
mosaic: 1.0
mixup: 0.1
copy_paste: 0.1
close_mosaic: 10
# AMP训练稳定后可开启
amp: True
optimizer: 'SGD'
在命令行执行训练,使用bash直接跑:
# 单卡A100训练命令
yolo detect train \
--cfg custom_train.yaml \
--project runs/industrial \
--name v8s_exp1 \
--save-period 10 \
--device 0 \
--verbose
# 训练完自动验证
yolo detect val \
--model runs/industrial/v8s_exp1/weights/best.pt \
--data datasets/industrial/data.yaml \
--imgsz 640 \
--batch 32 \
--device 0
4.3 推理代码:用Python API跑单张图
不要每次推理都走CLI命令,直接调Python API,可以拿到每个类的独立AP和中间特征图:
# inference.py
from ultralytics import YOLO
import torch
model = YOLO("runs/industrial/v8s_exp1/weights/best.pt")
results = model.predict(
source="datasets/industrial/test/images/defect_001.jpg",
conf_thres=0.35,
iou_thres=0.5,
imgsz=640,
device="cuda:0"
)
result = results[0]
boxes = result.boxes
for i in range(len(boxes)):
cls_id = int(boxes.cls[i].item())
conf = float(boxes.conf[i].item())
xyxy = boxes.xyxy[i].cpu().numpy()
print(f"类别={result.names[cls_id]} 置信度={conf:.3f} 框坐标={xyxy}")
4.4 部署:导出ONNX再转TensorRT
坑点:直接用ultralytics导出engine格式存在兼容性问题,在Jetson上经常报版本不匹配。稳妥路径是先导出ONNX,再自己写Python脚本转TensorRT:
# 1. 导出ONNX,opset>=12否则有些算子不支持
yolo export \
model=runs/industrial/v8s_exp1/weights/best.pt \
format=onnx \
opset=13 \
imgsz=640 \
dynamic=True
# 2. 转成TensorRT FP16 engine
trtexec \
--onnx=best.onnx \
--saveEngine=best_fp16.engine \
--fp16 \
--minShapes=images:1x3x640x640 \
--optShapes=images:4x3x640x640 \
--maxShapes=images:8x3x640x640
# 3. Jetson上如果有dla,可以再同步一份DLA分支(可选)
4.5 量化避开敏感层
如果不小心对Detect头做了INT8量化,精度会直接崩掉。原因:Detect头的回归分支输出的是距离值,对量化误差极其敏感。解决方案:只量化backbone和neck,Detect头保持FP16。用TensorRT的量化敏感层分析工具找出例外,下面给出可用的Python层替换代码:
# trt_quantize.py
# 在加载量化模型前,用identity层替换Detect头
import tensorrt as trt
def check_and_replace_detect_layers(engine_path):
"""检查TensorRT engine中Detect头的精度设置"""
with open(engine_path, "rb") as f:
engine_data = f.read()
runtime = trt.Runtime(trt.Logger(trt.Logger.WARNING))
engine = runtime.deserialize_cuda_engine(engine_data)
for i in range(engine.num_layers):
layer = engine[i]
if layer.name.startswith("model.22") or layer.name.startswith("model.24"):
print(f"警告: Detect层 {i} ({layer.name}) 当前精度: {layer.precision}")
print(f" -> 建议在导出ONNX时排除该层,或设置dynamic_range为0.01")
check_and_replace_detect_layers("best_fp16.engine")
5. 效果数据
用上面的配置在我们的工业质检数据集(12800张训练图、1200张验证图、4个缺陷类别:划痕/凹陷/毛刺/脏污)上做完整对拍实验:
| 模型 | mAP50 | mAP50-95 | 划痕AP | 凹陷AP | 毛刺AP | 脏污AP |
|---|---|---|---|---|---|---|
| v5s + 默认anchor | 87.6% | 62.4% | 91.2% | 78.5% | 85.3% | 89.4% |
| v5s + 自定义anchor | 89.3% | 64.8% | 92.8% | 81.6% | 87.1% | 91.0% |
| v8s + 默认参数 | 91.7% | 67.2% | 94.1% | 84.5% | 90.6% | 93.2% |
| v8s + 调优参数 | 93.4% | 69.1% | 95.2% | 87.1% | 92.8% | 94.5% |
Jetson Orin上的部署效果:
- v8s + TensorRT FP16:延迟34.2ms(达标),GPU内存占用1.8GB,吞吐量29.2 FPS
- v8s + TensorRT INT8(不排除Detect头):延迟12.5ms,但mAP50掉了22.4%,完全不可用
- v8s + TensorRT INT8(排除Detect头):延迟15.8ms,mAP50降低了4.1%,可接受
- v5s + TensorRT FP16:延迟28.6ms,mAP50为89.3%,小目标AP不达标
训练耗时方面:A100单卡训练200个epoch,v8s用时约5.2小时,v5s用时约4.1小时。v8s多了26%的训练时间,但在推理端多出的3.9ms完全可控。
6. 避坑:迁移到v8必须注意的5个问题
直接列坑,都是我们实操中实际碰到的:
坑1:AMP精度损失
在v8上开启AMP(自动混合精度)后,如果batch size偏小(<16),loss曲线会诡异震荡。测试发现amp=None时,最终mAP比amp=True高1.7%,但在大batch=64时差距缩小到0.3%。解决方案:batch size >= 32 再开AMP,否则老老实实跑FP32。
坑2:Mosaic不是开得越久越好
如果你把close_mosaic设置成0(整个训练过程都开Mosaic),验证集loss会严重震荡。原因是Mosaic合成的图像分布和目标真实分布有偏移,模型最后没有在真实分布上拟合。官方默认是最后一个epoch才关闭,太晚了。我们测试在最后10个epoch关闭(close_mosaic=10),mAP比完整开启高1.2%,比最后1个epoch关闭高0.7%。如果你自定义数据集有强背景依赖(比如医学影像),建议把close_mosaic调到15。
坑3:Decoupled Head的权重初始化
v8的Decoupled Head输出通道数比v5多(因为要单独预测分类和回归),如果用预训练模型来做fine-tune,记得要冻结backbone的前10层。我们在第一次迁移时发现warmup阶段loss就爆了,排查后确认是backbone前面的BatchNorm参数被新数据冲掉了。用AdamW而不是SGD做fine-tune,初始lr降到0.001。之前用SGD,mAP50只到了87%。
坑4:TensorRT版本和Ultralytics版本不对齐
Ultralytics 8.1.x自带的导出engine功能,在TensorRT 8.6上生成的模型文件不同,直接报错无法加载。解决办法统一用ONNX作为中转。再有动态batch的minShapes和optShapes,如果设置不一致,Jetson上的显存分配会有冗余,我们实测设成1/4/8比1/8/8显存占用少了400MB。
坑5:INT8量化掉的点数不全是可恢复的
对v8s做INT8量化,用128张验证图作为校准集,mAP掉了8.9%。换成整个验证集(1200张)做校准,也只恢复到掉了5.7%。原因在于v8的解耦头和高分辨率特征图对量化敏感度远超v5。我们最后放弃INT8,直接用FP16,毕竟对于40ms的实时检测,FP16的延迟已经完全达标。如果你的项目里有硬性FP16不达标的场景,建议改用RT-DETR或YOLO-NAS,它们在INT8下的表现好得多。
7. 总结
YOLOv8不是简单的增量升级,Anchor-Free、C2f、TaskAlignedAssigner这三个改动直接解决了我遇到的工业场景中三个核心问题:anchor调参的偶然性、小目标召回率低、正负样本不平衡。实测下来,同等测试条件下v8s比v5s在mAP50-95上高7.5个点,代价是模型体积大60%、单帧延迟增加22%。Jetson Orin上FP16延迟34.2ms,完全能跑实时。
但如果你场景简单、目标尺寸分布均匀、又跑在树莓派这类设备上,v5s依然是更优的选择——模型体积减小30%,INT8量化后延迟更低。版本没有绝对的先进,只有是否匹配约束条件。
8. 附:完整目录结构与训练日志解析
最后给一个我们训练时的日志分段截图数据,用于定位训练卡在哪个环节:
| 训练阶段(epoch) | loss下降速率 | mAP50-95增速 | 建议操作 |
|---|---|---|---|
| 1-20 | 2.1→0.8(快速下降) | 0→18% | 平稳训练,别动超参 |
| 21-50 | 0.8→0.4 | 18%→32% | 开始关注验证集loss |
| 51-120 | 0.4→0.25 | 32%→48% | 如果震荡,考虑降低lr |
| 121-180 | 0.25→0.20 | 48%→55% | 不要手动Early Stopping |
| 181-200 | 0.20→0.18 | 55%→58% | 验证Mosaic增强关闭后的表现 |
如果你发现第21-50个epoch验证集loss不降反升,大概率是过拟合而不是学习率问题,先检查数据集是否有重复,再用数据增强来缓解,v8的mixup和copy_paste参数可以往上调。
# 在训练日志中提取mAP变化
grep "map50-95" runs/industrial/v8s_exp1/train.log | awk '{print $1, $NF}' | tail -20
以上是完整内容。有问题直接在GitHub提Issue,代码都在仓库里。