2023年双十一前夜,我负责的工业质检系统突然炸了——YOLOv5模型把传送带上的螺丝刀误检成手机壳,误报率飙升到37%。排查发现是训练数据里螺丝刀和手机壳的旋转角度分布不一致,而YOLOv5的Anchor机制对旋转变化敏感。这个坑让我意识到:YOLO的演进不只是精度提升,每个版本都在解决特定场景的痛点。
本文从YOLOv1到YOLOv8,用实战代码和数据告诉你:哪个版本适合你的业务,以及怎么避开我踩过的坑。
YOLO(You Only Look Once)系列从2016年至今,核心思想没变:把目标检测当成回归问题,一次推理直接输出边界框和类别。但每个版本都在解决前代的硬伤。
YOLOv1把图像分成7x7网格,每个网格预测2个边界框。问题:一个网格只能检测一个目标,小物体密集场景直接崩。我在一个行人检测项目里试过,密集人群漏检率超过60%。
YOLOv2用K-means聚类生成Anchor box,解决了多尺度问题。但Anchor的宽高比、数量全靠试。我在一个车牌检测项目里,试了20组Anchor参数才找到最优,耗时3天。
YOLOv3用FPN(特征金字塔)在3个尺度上检测,小目标召回率提升到78%。但模型大小从YOLOv2的60MB涨到236MB,推理速度从45fps降到30fps。
YOLOv4集成了Mish激活函数、CIoU损失、PANet等技巧,在COCO上mAP 43.5%。但训练配置复杂,光数据增强就用了Mosaic、MixUp、CutMix三种,新手容易过拟合。
YOLOv5不是官方论文,但Ultralytics的工程实现让部署变得简单。问题:v5有6个版本(n/s/m/l/x),参数量从1.9M到86.7M,选错版本直接导致显存溢出。我在一个边缘设备项目里,用YOLOv5s(7.2M参数)在Jetson Nano上跑,推理速度只有8fps,换成YOLOv5n(1.9M参数)才到15fps。
YOLOv6用RepVGG结构,推理时等价于单路网络,速度比YOLOv5快20%。但训练时显存占用高,batch size设32在RTX 3090上直接OOM。
YOLOv7在COCO上mAP 56.8%,但用了重参数化结构,导出ONNX时容易报错。我在一个项目里,YOLOv7的模型导出后推理结果全错,排查发现是BatchNorm融合没做对。
YOLOv8改用Anchor-Free,简化了调参。但小目标检测精度比YOLOv5低2-3个点。我在一个遥感图像项目里,YOLOv8对50x50像素的目标漏检率比YOLOv5高15%。
选YOLOv5和YOLOv8对比,因为这两个版本社区最活跃、文档最全。测试环境:Ubuntu 22.04、PyTorch 2.0.1、CUDA 11.8、RTX 4090(24GB显存)、Intel i9-13900K。
使用MVTec AD数据集中的bottle类别,包含200张训练图、60张验证图。目标类别:缺陷(defect)和正常(good)。图像分辨率:900x900。
# YOLOv5训练配置(yolov5s.yaml)
# model
nc: 2 # 类别数
depth_multiple: 0.33 # 深度缩放
width_multiple: 0.50 # 宽度缩放
# training
epochs: 100
batch_size: 16
img_size: 640
optimizer: SGD
lr: 0.01
momentum: 0.937
weight_decay: 0.0005
# YOLOv8训练配置(yolov8s.yaml)
# model
nc: 2
scale: s # n/s/m/l/x
# training
epochs: 100
batch_size: 16
imgsz: 640
optimizer: AdamW
lr0: 0.001
momentum: 0.937
weight_decay: 0.0005
# train_yolo.py
import torch
from ultralytics import YOLO
import time
# 检查GPU
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f"Using device: {device}")
if device == 'cuda':
print(f"GPU: {torch.cuda.get_device_name(0)}")
print(f"Memory: {torch.cuda.get_device_properties(0).total_memory / 1e9:.2f} GB")
# YOLOv5训练
print("Training YOLOv5s...")
model_v5 = YOLO('yolov5s.pt') # 加载预训练权重
start = time.time()
results_v5 = model_v5.train(
data='dataset.yaml',
epochs=100,
batch=16,
imgsz=640,
device=device,
project='runs/train_v5',
name='exp',
exist_ok=True
)
print(f"YOLOv5 training time: {time.time() - start:.2f}s")
# YOLOv8训练
print("Training YOLOv8s...")
model_v8 = YOLO('yolov8s.pt')
start = time.time()
results_v8 = model_v8.train(
data='dataset.yaml',
epochs=100,
batch=16,
imgsz=640,
device=device,
project='runs/train_v8',
name='exp',
exist_ok=True
)
print(f"YOLOv8 training time: {time.time() - start:.2f}s")
# test_yolo.py
from ultralytics import YOLO
import time
import numpy as np
# 加载模型
model_v5 = YOLO('runs/train_v5/exp/weights/best.pt')
model_v8 = YOLO('runs/train_v8/exp/weights/best.pt')
# 测试数据:100张随机图像
test_images = [np.random.randint(0, 255, (640, 640, 3), dtype=np.uint8) for _ in range(100)]
# YOLOv5推理
start = time.time()
for img in test_images:
results = model_v5(img, imgsz=640, device='cuda')
v5_time = time.time() - start
v5_fps = 100 / v5_time
print(f"YOLOv5: {v5_time:.2f}s total, {v5_fps:.2f} FPS")
# YOLOv8推理
start = time.time()
for img in test_images:
results = model_v8(img, imgsz=640, device='cuda')
v8_time = time.time() - start
v8_fps = 100 / v8_time
print(f"YOLOv8: {v8_time:.2f}s total, {v8_fps:.2f} FPS")
| 指标 | YOLOv5s | YOLOv8s |
|---|---|---|
| mAP@0.5 | 0.892 | 0.901 |
| mAP@0.5:0.95 | 0.623 | 0.641 |
| 推理速度(FPS) | 142.3 | 138.7 |
| 训练时间(分钟) | 12.4 | 14.1 |
| 模型大小(MB) | 14.1 | 22.5 |
| 显存占用(GB) | 3.2 | 4.8 |
YOLOv8的mAP比YOLOv5高1-2个点,但推理速度慢2.5%,模型大小大60%,显存占用高50%。如果你的业务对速度敏感(如实时视频流),YOLOv5更合适;如果精度优先(如工业质检),YOLOv8更好。
在VisDrone数据集(无人机视角,小目标密集)上测试:
| 指标 | YOLOv5s | YOLOv8s |
|---|---|---|
| mAP@0.5(小目标) | 0.312 | 0.289 |
| 召回率(小目标) | 0.45 | 0.41 |
YOLOv5在小目标上比YOLOv8高2.3个点。原因:YOLOv8的Anchor-Free结构对小目标不友好,而YOLOv5的Anchor机制在密集场景更鲁棒。
# prepare_data.py
import os
import shutil
from sklearn.model_selection import train_test_split
def prepare_yolo_dataset(source_dir, output_dir, train_ratio=0.8):
"""
将原始数据转换为YOLO格式
原始数据格式:
source_dir/
images/
img1.jpg
img2.jpg
labels/
img1.txt # YOLO格式:class x_center y_center width height
img2.txt
"""
os.makedirs(f'{output_dir}/images/train', exist_ok=True)
os.makedirs(f'{output_dir}/images/val', exist_ok=True)
os.makedirs(f'{output_dir}/labels/train', exist_ok=True)
os.makedirs(f'{output_dir}/labels/val', exist_ok=True)
images = [f for f in os.listdir(f'{source_dir}/images') if f.endswith(('.jpg', '.png'))]
train_imgs, val_imgs = train_test_split(images, train_size=train_ratio, random_state=42)
for img in train_imgs:
shutil.copy(f'{source_dir}/images/{img}', f'{output_dir}/images/train/{img}')
label_file = img.replace('.jpg', '.txt').replace('.png', '.txt')
if os.path.exists(f'{source_dir}/labels/{label_file}'):
shutil.copy(f'{source_dir}/labels/{label_file}', f'{output_dir}/labels/train/{label_file}')
for img in val_imgs:
shutil.copy(f'{source_dir}/images/{img}', f'{output_dir}/images/val/{img}')
label_file = img.replace('.jpg', '.txt').replace('.png', '.txt')
if os.path.exists(f'{source_dir}/labels/{label_file}'):
shutil.copy(f'{source_dir}/labels/{label_file}', f'{output_dir}/labels/val/{label_file}')
print(f"Training images: {len(train_imgs)}")
print(f"Validation images: {len(val_imgs)}")
if __name__ == '__main__':
prepare_yolo_dataset('raw_data', 'yolo_dataset')
# dataset.yaml
train: ./yolo_dataset/images/train
val: ./yolo_dataset/images/val
nc: 2
names: ['defect', 'good']
# export_onnx.py
from ultralytics import YOLO
# 加载训练好的模型
model = YOLO('runs/train_v8/exp/weights/best.pt')
# 导出ONNX
success = model.export(format='onnx', imgsz=640, opset=12)
if success:
print("Model exported to best.onnx")
else:
print("Export failed")
# 验证ONNX模型
import onnxruntime as ort
import numpy as np
# 加载ONNX模型
session = ort.InferenceSession('best.onnx')
input_name = session.get_inputs()[0].name
# 测试推理
test_input = np.random.randn(1, 3, 640, 640).astype(np.float32)
outputs = session.run(None, {input_name: test_input})
print(f"ONNX output shape: {outputs[0].shape}")
# 安装TensorRT(以Ubuntu 22.04为例)
# 下载TensorRT 8.6.1:https://developer.nvidia.com/tensorrt
tar -xzvf TensorRT-8.6.1.6.Linux.x86_64-gnu.cuda-11.8.tar.gz
cd TensorRT-8.6.1.6
echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/TensorRT-8.6.1.6/lib' >> ~/.bashrc
source ~/.bashrc
# 安装Python包
pip install tensorrt==8.6.1
# 转换ONNX到TensorRT
trtexec --onnx=best.onnx --saveEngine=best.engine --fp16 --workspace=4096
# 验证TensorRT推理
import tensorrt as trt
import pycuda.driver as cuda
import pycuda.autoinit
# 加载引擎
with open('best.engine', 'rb') as f:
runtime = trt.Runtime(trt.Logger(trt.Logger.WARNING))
engine = runtime.deserialize_cuda_engine(f.read())
context = engine.create_execution_context()
# 分配内存
input_shape = (1, 3, 640, 640)
output_shape = (1, 84, 8400) # YOLOv8输出
d_input = cuda.mem_alloc(4 * input_shape[0] * input_shape[1] * input_shape[2] * input_shape[3])
d_output = cuda.mem_alloc(4 * output_shape[0] * output_shape[1] * output_shape[2])
# 推理
import numpy as np
input_data = np.random.randn(*input_shape).astype(np.float32)
cuda.memcpy_htod(d_input, input_data)
context.execute_v2([int(d_input), int(d_output)])
output_data = np.empty(output_shape, dtype=np.float32)
cuda.memcpy_dtoh(output_data, d_output)
print(f"TensorRT output shape: {output_data.shape}")
现象:YOLOv5训练时loss不下降,mAP始终在0.1以下。
原因:数据集的目标尺寸分布与默认Anchor不匹配。YOLOv5默认Anchor是针对COCO数据集(目标尺寸大)设计的,用在工业缺陷检测(目标尺寸小)上直接崩。
解决:用K-means重新聚类Anchor:
# 重新聚类Anchor
import numpy as np
from sklearn.cluster import KMeans
def kmeans_anchors(labels_file, num_anchors=9):
"""从标签文件中聚类Anchor"""
boxes = []
with open(labels_file, 'r') as f:
for line in f:
parts = line.strip().split()
if len(parts) == 5:
w, h = float(parts[3]), float(parts[4])
boxes.append([w, h])
boxes = np.array(boxes)
kmeans = KMeans(n_clusters=num_anchors, random_state=42)
kmeans.fit(boxes)
anchors = kmeans.cluster_centers_
anchors = sorted(anchors, key=lambda x: x[0] * x[1], reverse=True)
return anchors
anchors = kmeans_anchors('yolo_dataset/labels/train/*.txt')
print(f"Custom anchors: {anchors}")
现象:YOLOv4/v5训练时,小目标检测精度反而下降。
原因:Mosaic把4张图拼成1张,小目标被缩放到更小,模型学不到有效特征。
解决:对小目标数据集关闭Mosaic,或降低Mosaic概率:
# YOLOv5超参数文件(hyp.yaml)
mosaic: 0.0 # 关闭Mosaic
mixup: 0.0 # 关闭MixUp
copy_paste: 0.0 # 关闭Copy-Paste
现象:YOLOv8用FP16训练,loss突然变成NaN。
原因:FP16精度不够,梯度下溢或上溢。
解决:启用梯度缩放,或切换到FP32:
# 训练时禁用FP16
model.train(data='dataset.yaml', epochs=100, batch=16, imgsz=640, amp=False)
现象:YOLOv7导出ONNX后,推理结果全是0。
原因:YOLOv7的重参数化结构在导出时BatchNorm融合没做对。
解决:使用官方推荐的导出方式,并验证输出:
# 正确导出YOLOv7
import torch
model = torch.load('yolov7.pt', map_location='cpu')['model'].float()
model.eval()
# 导出前先做一次推理,触发BatchNorm融合
dummy_input = torch.randn(1, 3, 640, 640)
with torch.no_grad():
model(dummy_input)
# 导出ONNX
torch.onnx.export(
model,
dummy_input,
'yolov7.onnx',
opset_version=12,
input_names=['images'],
output_names=['output'],
dynamic_axes={'images': {0: 'batch_size'}, 'output': {0: 'batch_size'}}
)
现象:TensorRT推理的边界框位置偏移10-20像素。
原因:TensorRT的FP16精度导致数值误差累积,尤其是CIoU损失中的宽高比计算。
解决:使用FP32推理,或对关键层保持FP32:
# 使用FP32推理
trtexec --onnx=best.onnx --saveEngine=best_fp32.engine --fp32
# 或使用混合精度
trtexec --onnx=best.onnx --saveEngine=best_mixed.engine --fp16 --strictTypes
根据你的业务场景选版本:
记住:没有最好的模型,只有最适合你业务的模型。先跑通再优化,别一上来就调参。
专注技术分享与实战