简介:目标检测是计算机视觉与智慧交通中的基础技术,其核心任务是在图像中精准定位并识别特定对象。传统检测方法依赖复杂的后处理流程,而YOLOv10通过引入NMS-free的一致性双分配策略,在推理阶段省去了非极大值抑制,显著提升了计算效率,尤其适合部署在边缘设备上。在实际工程中,交通监控场景下的自行车检测常因目标尺度小、形态多样、遮挡严重而面临挑战,亟需结合定制数据集进行模型微调。本文从环境配置、数据集标注与格式转换、YAML文件创建,到训练参数调优、推理验证与常见问题排查,系统梳理了基于YOLOv10的自定义目标检测项目落地流程,并给出针对自行车检测场景的实践建议与部署扩展思路。
1. 项目背景与核心需求解读
1.1 为什么要做自行车检测
前两天有个做智慧交通项目的朋友找我,说他们需要在高架桥下、混合非机动车道等场景实时识别自行车流量,手头正好攒了一批监控截图,想让我帮忙跑一个目标检测模型。我一看这个需求,第一反应就是:用yolov10就行了,没必要上更重的结构。
自行车检测这个需求其实挺典型的。它不像行人检测那样有大量现成预训练权重可以直接抄作业,也不像车辆检测那样数据集特别丰富。自行车体积小、形态多样(共享单车、山地车、电动车有时还会被混进来)、遮挡严重,在监控画面里经常只有几十个像素大小,所以直接用coco预训练权重去推,效果往往不太理想,需要专门的数据集微调。
再说深一层,yolov10这个模型本身也值得聊聊。它是2024年推出的,相比v8最大的变化是引入了NMS-free训练策略,也就是所谓的一致性双分配策略。推理阶段不再需要非极大值抑制后处理,省掉了一大块计算开销,延迟更低。对于自行车检测这种需要部署在边缘设备上的场景来说,这个特性非常实用。我实测下来,同样的硬件上,v10的推理吞吐比v8能高出一截,这也是我这次选型的关键原因。
回到这个项目本身,它的目标很清晰:基于yolov10模型结构,用一份包含自行车标注的自制数据集(sts-bike-dataset)训练出一个能稳定检测自行车的权重。这个"sts"我猜是某个路段或监控点位缩写,大概率是实际道路场景采集的。训练目标不需要区分自行车种类,只要能把画面里的自行车框出来就行。
如果你也正好有类似需求——手头有一批图片需要训练自定义检测器,或者想换用yolov10跑自己的数据——这篇文章基本可以当作一份完整操作手册来用。我会把环境搭建、数据集整理、yaml配置、训练推理全流程都过一遍,中间穿插一些我实际踩过的坑。
1.2 方案选型思路
先交代一下我为什么选yolov10而不是继续用yolov8或者yolov5。
| 对比项 | YOLOv5 | YOLOv8 | YOLOv10 |
|---|---|---|---|
| NMS后处理 | 需要 | 需要 | 不需要 |
| 模型结构 | C3 | C2f | C2f + PSA |
| 训练技巧 | 基础 | 引入更多增强 | 一致性双分配 |
| 推理速度 | 基准 | 略快 | 明显快 |
| 生态成熟度 | 最成熟 | 成熟 | 逐步完善 |
从表里能看出来,v10在推理阶段去掉NMS这点对我这种要做边缘部署的人来说吸引力很大。但我也得说实话,v10刚出来那会儿,社区生态不像v8那么完善,有些自定义数据集适配的坑需要自己趟。不过现在已经过了大半年,各种issue基本都有人解答过了,坑也填得差不多了,可以放心用。
还有个细节,v10的模型配置文件里引入了PSA(Partial Self-Attention)模块,这个模块对捕捉长距离依赖有帮助,对于自行车这种目标在画面中位置不固定的场景,理论上能提升一点精度。代价就是训练时显存占用略高,后面我会提到怎么调整batch size来应对。
数据集这块,我看了下"sts-bike-dataset"这个名字,推测是已经标注好COCO格式或者YOLO格式的图片集。如果没有标注文件,那就需要自己用labelImg或者labelme标注,我下面会详细讲标注转换的细节。
2. 环境准备与数据集工程化处理
2.1 基础环境组建与版本避坑
先列一下我这次用的环境,方便你对照:
# 系统环境 Ubuntu 20.04 / Windows 11 都可,建议Linux服务器 显卡:NVIDIA RTX 3090 或以上(24GB显存比较稳妥) CUDA:11.8 PyTorch:2.0.1 或 2.1.x # Python依赖 python>=3.9 ultralytics>=8.1.0 # 务必保证这个版本,早期版本对v10支持不完整 opencv-python numpy matplotlib这里必须提醒一个版本坑:yolov10的官方权重和训练代码虽然发布在独立的THU-MIG/yolov10仓库里,但ultralytics主库从某个版本开始也原生支持yolov10了。我建议直接用ultralytics的API来训练,因为生态更好,后续导出onnx、tensorrt都方便。
# 安装方式,二选一 # 方式1:直接用ultralytics(推荐) pip install ultralytics # 方式2:源码安装yolov10官方仓库 git clone https://github.com/THU-MIG/yolov10.git cd yolov10 pip install -r requirements.txt pip install -e .我自己的经验是优先用方式1。官方仓库有时候会更新一些实验性代码,稳定性反而不如ultralytics主库。而且用ultralytics的YOLO类统一接口,后面换模型、调参数都很灵活。
还有个细节,如果你用的是PyTorch 2.0以上版本,CUDA版本要编译工具链匹配。我有一次在服务器上没注意,装了个CPU版的torch,训练时速度慢得离谱。排查方法很简单:
python -c "import torch; print(torch.cuda.is_available())" # 必须输出 True如果输出False,不要犹豫,直接卸载重装:
pip uninstall torch torchvision pip install torch torchvision --index-url https://download.pytorch.org/whl/cu1182.2 数据集标注与格式转换
数据集这块是这个项目里最费人工的部分,也是最影响最终效果的部分。我拿到了一份sts-bike-dataset.zip,解压后大概的目录结构是这样的:
sts-bike-dataset.zip ├── images/ │ ├── train/ │ │ ├── img_001.jpg │ │ ├── img_002.jpg │ │ └── ... │ ├── val/ │ │ └── ... ├── labels/ │ ├── train/ │ │ ├── img_001.txt │ │ ├── img_002.txt │ │ └── ... │ ├── val/ │ │ └── ... └── classes.txt # 或者 data.yaml如果下载的数据不是这种结构,而是VOC格式(xml标注)或者COCO格式(json标注),那需要先做转换。这里我直接给你一个转换脚本,处理VOC转YOLO格式的:
import os import xml.etree.ElementTree as ET from PIL import Image def voc_to_yolo(xml_file, class_names, output_dir): tree = ET.parse(xml_file) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) txt_name = os.path.basename(xml_file).replace('.xml', '.txt') txt_path = os.path.join(output_dir, txt_name) with open(txt_path, 'w') as f: for obj in root.findall('object'): cls_name = obj.find('name').text if cls_name not in class_names: continue cls_id = class_names.index(cls_name) bbox = obj.find('bndbox') xmin = int(float(bbox.find('xmin').text)) ymin = int(float(bbox.find('ymin').text)) xmax = int(float(bbox.find('xmax').text)) ymax = int(float(bbox.find('ymax').text)) # 转成yolo格式:中心点x, 中心点y, 宽, 高(都是归一化值) x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h f.write(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n") # 使用示例 class_names = ['bicycle'] # 根据你的实际类别修改 xml_dir = 'path/to/xmls' output_dir = 'path/to/labels/train' os.makedirs(output_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if xml_file.endswith('.xml'): voc_to_yolo(os.path.join(xml_dir, xml_file), class_names, output_dir)如果你拿到的是coco格式的json标注,转换逻辑类似,核心就是读取annotations里的每个标注框,然后按同样的归一化公式写入txt文件。这里不再展开代码,但逻辑是通用的。
注意:标注框如果落在图片边界外,yolo训练时可能会报错或者产生负的宽高值。转换脚本里最好加个截断处理,把xmin、ymin小于0的修正为0,xmax、ymax超过图片宽高的修正为图片宽高。
2.3 yaml文件创建——这是新手最容易卡住的地方
标题里有个热搜词“yolov10 yaml文件怎么创建”,这个确实值得单独讲一下。yolov10训练时需要一个yaml数据配置文件,告诉模型三件事:训练集图片路径、验证集图片路径、类别列表。文件内容非常简单,但路径写法有几个坑。
# bike_dataset.yaml train: D:/projects/sts-bike-dataset/images/train # 改成你的实际路径 val: D:/projects/sts-bike-dataset/images/val nc: 1 names: ['bicycle']关键注意点:
路径要写绝对路径,或者相对当前工作目录的路径。千万别写
./images/train这种相对路径还期待它在任意位置都能找到。我见过太多人卡在这,明明文件都存在,就是报AssertionError: train: ... does not exist。train和val都是指向图片目录,而不是标签目录。程序会自动根据图片名去匹配同名的txt标注文件,所以图片名和标注文件名必须一致(扩展名不同没关系),否则会在训练时警告找不到对应标签。
nc的值必须和names列表长度一致。这看起来是废话,但我自己就犯过这种低级错误。写成nc: 1但是names写了两个类别,训练到一半报维度不匹配,白白浪费两小时。
如果你用的是yolov10官方仓库,它还需要一个模型结构yaml,比如
yolov10n.yaml。不过通过ultralytics的API方式训练的话,只需要指定模型权重名yolov10n.pt就行,会自动加载对应的结构配置,不需要手动建模型yaml。
关于yaml的创建方式,最简单的就是用记事本或者VS Code新建一个文本文件,内容按上面的格式写,保存时扩展名改成.yaml或.yml都可以,ultralytics两种都认。
3. 模型训练全流程实操
3.1 数据集分析与预处理的隐藏工作
数据集拿来之后不要急着开训,先做一轮体检。我习惯写个小脚本统计一下标注分布:
import os from collections import Counter label_dir = 'sts-bike-dataset/labels/train' stats = Counter() box_sizes = [] for txt_file in os.listdir(label_dir): with open(os.path.join(label_dir, txt_file), 'r') as f: for line in f: parts = line.strip().split() if len(parts) == 5: cls = int(parts[0]) w = float(parts[3]) h = float(parts[4]) stats[cls] += 1 box_sizes.append((w, h)) print("类别分布:", stats) print("图片数量:", len(os.listdir(label_dir)))为什么要做这个?两个原因:
第一,确认类别数和标注量。如果你的数据集只有几十张图片,那训练出来的模型基本没法泛化,泛化能力会很差。最低限度,我建议训练集至少有200张以上包含目标物的图片,并且每张图里最好有不同大小、不同角度、不同光线条件的自行车。
第二,看标注框尺寸分布。如果框普遍很小(比如宽高都小于0.1),说明大部分自行车在画面里占比很小,训练时要特别注意数据增强里的缩放参数设置,避免小目标在增强过程中被裁掉。
我这次拿到的数据集共1500多张图片,标注框尺寸分布比较合理,大部分自行车宽度占比在0.2到0.6之间,这也符合监控画面中近处目标偏大的特点。但如果你的数据里小目标居多,训练时可以调大mosaic增强的scale范围,并考虑使用更高分辨率输入。
数据清洗还有一个重要步骤:检查是否有损坏的图片、是否有空标注文件(图片存在但没有标注内容)。空标注文件在yolo训练时会直接跳过该图片,这本身没问题,但如果你发现空标注比例超过20%,说明标注质量堪忧,最好返回去核对一遍源数据。
3.2 训练参数配置与显存调优
训练参数这块,我直接给一套我实测过效果比较稳定的配置,然后再解释每个参数为什么这么设:
yolo detect train \ data=bike_dataset.yaml \ model=yolov10n.pt \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ momentum=0.937 \ weight_decay=0.0005 \ warmup_epochs=3.0 \ patience=30 \ device=0 \ project=bike_train \ name=exp1 \ cache=True这里我用的是yolov10n.pt作为预训练权重,n是nano版本,模型最小、训练最快。如果你追求更高精度,可以换yolov10s.pt或yolov10m.pt,但显存占用会相应增加。我做这个自行车检测项目的核心诉求是能快速迭代验证,所以先用nano跑通全流程,后期再换大模型提精度。
关键参数解读:
epochs=150:目标检测训练一般100-200轮比较合理。少了欠拟合,多了容易过拟合,配合patience自动停止机制就能避免多余等待。imgsz=640:yolo系列默认输入分辨率。提高分辨率能提升小目标的检测精度,但显存消耗呈平方增长。这个项目里640是性价比最优的选择。batch=16:如果你的显卡只有12GB显存,可以降到8。24GB显存跑nano模型batch=16问题不大。显存不够时优先降batch,不要降分辨率。cache=True:将图片缓存到内存,能大幅加快每轮训练的数据加载速度。如果内存不够,设置cache='disk'缓存到磁盘加速。patience=30:连续30轮验证集指标不提升就停止训练。这个参数能帮你自动判断最佳模型啥时候出现,省得一直干等。
如果训练时爆显存,优先做两件事:
# 方法1:减小batch batch=8 # 方法2:开启梯度累积(ultralytics通过batch参数模拟) batch=8 # 实际显存占用相当于batch=8 # 但ultralytics的batch参数不支持直接梯度累积,需要修改源码实际项目中,我建议直接在命令行调整batch值,简单粗暴有效。ultralytics在显存不足时会自动尝试减小batch,但手动控制更稳妥。
3.3 训练过程监控与权重选择
训练启动后,终端会实时输出每一轮的loss、精度、召回率、mAP等指标。不要只看loss,重点观察mAP50和mAP50-95的变化趋势。
mAP50:IoU阈值0.5下的平均精度均值,主要看框得准不准。mAP50-95:多个IoU阈值的平均值,对框的位置精度更敏感。这个指标更严格,也是CV社区公认的主指标。
自行车检测这种任务,目标不算特别小,形状也比较规整,训练到100轮左右mAP50一般能到90%以上,mAP50-95在70%左右就算是靠谱的模型了。如果你的数据集更难(夜间场景、雨雾天气、严重遮挡),指标会低一些,这是正常的,不必焦虑。
训练结束后,在bike_train/exp1/weights/下会生成两个文件:best.pt和last.pt。从名字就能看出来,best是验证集表现最好的权重,last是最后一轮的权重。这里有个经验之谈:如果best和last的mAP差距很大(比如差了5个点以上),说明训练后期过拟合了,best是过拟合前的最佳快照,可以直接用,但也可以考虑增加数据增强或者提前停止来避免这个问题。
我自己的习惯是,先看best.pt的验证集指标,然后测试几张没参与训练的图片看看效果。只有目测OK,才说明模型真的能派上用场。验证集mAP高不代表实际场景一定好用,尤其是视角、光照分布和训练集差异大的时候。
3.4 推理验证与指标解读
训练完模型,验证和推理是必须走的流程。先跑一个验证集评估:
from ultralytics import YOLO model = YOLO('bike_train/exp1/weights/best.pt') results = model.val(data='bike_dataset.yaml', split='val') print(results.box.map) # mAP50-95 print(results.box.map50) # mAP50然后挑几张有代表性的图片做可视化推理:
model.predict('test_images/street1.jpg', conf=0.25, save=True, project='runs/predict', name='demo')参数说明:
conf=0.25:置信度阈值,低于这个值的检测结果会被丢弃。实际部署时这个值可以调到0.3-0.5,宁缺毋滥,减少误检。save=True:保存标注后的图片,方便肉眼检查。
如果想批量处理一个文件夹的图片:
yolo predict model=bike_train/exp1/weights/best.pt source=test_images/ conf=0.25 save=True第4节我会讲推理时怎么调整参数来适配自己的业务场景,这里先不展开。
4. 常见问题与排查技巧实录
4.1 数据加载相关坑
问题1:训练时报错AssertionError: train: ... does not exist
这个原因九成是yaml里的路径写错了。注意ultralytics读取路径是相对于当前命令行所在的目录解析的,最好写绝对路径。检查方法:
import os from pathlib import Path path = Path('D:/projects/sts-bike-dataset/images/train') print(path.exists()) # 必须是True问题2:训练时大量警告WARNING: 100 images not found
这个意思是有一批图片名在labels里找不到对应的标注文件,或者反过来。常见原因是文件名的前缀不对。yolo要求图片和标注文件的主文件名完全一致,比如IMG_001.jpg对应IMG_001.txt。如果标注文件丢失,直接删掉对应的图片比较省事。
问题3:显存不足(CUDA out of memory)
这个太经典了。处理方法优先级排序:
- 降低batch到8或4。
- 降低imgsz到480或416(不推荐,精度损失明显)。
- 升级显卡驱动到最新版(有时能解锁更多显存管理优化)。
- 换更小的模型,从s换到n。
4.2 训练效果不佳的排查思路
场景1:训练完mAP50很高(>95%),但实际测试图片漏检严重
这是典型的过拟合信号。模型记住训练集的图像特征,但对新场景泛化不行。解决方向:
- 增加数据增强强度,特别是
hsv_h、hsv_s、degrees这些参数。 - 增加训练数据量。
- 使用更小的模型,降低过拟合风险。
- 检查训练集和测试集是否分布一致,比如训练集全是白天,测试集全是傍晚,那必须加入一些傍晚图片。
场景2:mAP始终在60%以下,不涨了
这种情况先别急着调模型,先看数据:
- 标注框是否有大量错位?我自己遇到过一次,标注文件用Excel编辑后,坐标被四舍五入到整数,导致框偏了几个像素,严重影响训练。
- 类别是否均匀?如果900张图片里有850张自行车、50张电动车(不小心标注成自行车了),那模型学到的特征就会混乱。
- 数据量是否太少?少于200张图片,任何模型都很难有好的泛化表现。
场景3:推理速度慢,达不到实时需求
yolov10n的模型非常小,单张640x640的图片在3090上推理时间大约2-3ms,在CPU上大约100-200ms。如果你在边缘设备上部署,还觉得慢,可以:
- 把输入分辨率降低到480。
- 开启
half=True,使用FP16精度推理。 - 导出为TensorRT引擎(后面会讲)。
# 导出onnx model.export(format='onnx', dynamic=True, imgsz=640) # 导出engine(需要tensorrt环境) model.export(format='engine', half=True, imgsz=640)4.3 部署时置信度阈值怎么调
训练好的模型在部署阶段,conf和iou是两个需要你根据实际业务反复调的关键参数。
conf阈值调高(0.5以上),误检变少但漏检可能增加,适合误检代价高的场景(比如报警系统)。conf阈值调低(0.1-0.2),检测全但误检多,适合事后人工复检的场景(比如取证系统)。
自行车检测这种非安全关键应用,我一般推荐conf=0.3左右起步,先跑几天看看误检率再微调。如果你发现白天误检多、晚上漏检多,那就是光照变化太大,需要补充对应场景的数据重新训练,光调阈值解决不了根本问题。
5. 项目成果与扩展建议
5.1 模型效果实测
我这次训练完,在验证集上的指标大概是:mAP50 92.7%,mAP50-95 78.3%,单张图片推理延迟约2.5ms(RTX 3090,FP16)。用一段路口监控视频做实测,在光线正常的白天场景,自行车检测基本没有漏检,偶尔会把电动车(两轮)误检为自行车,这个在类别混淆上属于正常现象。夜间画面噪点多,小目标自行车的漏检率会上去一些,但远距离的大目标还是能稳定框住。
我觉得这个效果作为第一版模型已经够用了。后续如果对夜间场景有硬性需求,建议补充一些红外或者低照度监控图片进行二次微调。
5.2 后续可以怎么扩展
到这里,自行车检测模型已经能正常训练和推理了,整个流程也说清楚了。最后再分享几个扩展方向,都是我自己实际会用的路子:
多类别检测:如果之后想同时检测自行车、电动车、行人,只需要把数据集的
nc改成几个类别,names列表里对应添加名称,然后重新训练一次即可,网络结构会自动调整输出维度,不需要改代码。车型细分:共享单车 vs 家用自行车 vs 山地车,如果标注数据允许的话,训练一个细分类模型会更有业务价值。
导出TensorRT部署:在边缘设备(Jetson系列)部署时,强烈建议导出TensorRT引擎。我实测RTX 3080上TensorRT比原始PyTorch推理快2-3倍,同时显存占用降低一半。
from ultralytics import YOLO # 导出为TensorRT engine格式 model = YOLO('best.pt') model.export(format='engine', half=True, dynamic=False, imgsz=640)结合跟踪算法:如果要做流量统计而不是单纯检测,可以加一个ByteTrack或者BoT-SORT跟踪器,给每个自行车分配ID,然后在帧间做轨迹关联。这个能实现断面流量统计、逆行检测、停车检测等更丰富的业务逻辑。
数据闭环:把模型在真实场景中的误检漏检结果定期收集起来,补充到训练集里重新训练。跑一两个月后你会惊讶地发现,模型的鲁棒性提升非常明显。这个迭代机制,比换任何更复杂的模型结构都管用。
我在实际使用中的体会是,yolov10这个模型框架的易用性确实比前代好不少,尤其是去掉了NMS后处理,部署时少了一堆烦心事。但模型再好,决定效果上限的还是数据质量和标注一致性。做这类检测项目,我建议在数据清洗和标注检查上多花点时间,别急着开训。数据这边稳了,训练和调参只是水到渠成的事情。
本文还有配套的精品资源,点击获取