Bdd100k 训练 YOLOv5 对象检测:标签转换与配置实战

发布时间:2026/10/5 14:44:18
Bdd100k 训练 YOLOv5 对象检测:标签转换与配置实战 简介这份资源面向计算机视觉方向的学习者与算法工程师提供在 Bdd100k 自动驾驶数据集上训练 YOLOv5s 目标检测模型的完整工程适合具备一定 Python 与深度学习基础、希望掌握自定义数据集训练流程的中高级开发者。压缩包共 85 个文件约 97.7MB包含 17 个 yaml 配置、16 个 py 脚本、5 个 pt 权重、3 个 ipynb 笔记本及若干 jpg、png 可视化结果覆盖模型结构定义、数据配置、训练与推理脚本、预训练权重和训练日志等模块。资源内含预处理的 Bdd100k 数据集、从零训练与基于预训练权重两套实验记录以及模型架构图和 4K 测试视频演示可帮助读者快速复现训练过程、理解数据预处理与参数配置思路。目前已有 110 人学习下载适合作为目标检测实战与迁移学习的参考案例。1. 从 Bdd100k 到 yolo v5一份能跑通的对象检测训练路线手里有一份 Bdd100k 数据集想拿它训练 yolo v5 对象检测模型这件事听起来直接做起来却卡在几个具体的地方Bdd100k 的标注是 JSONyolo v5 要的是每张图一个 txt数据集十万张图、十类目标直接全量训练显存和时间都吃不消官方仓库的默认配置是 COCO 的 80 类不改 nc 和 names 就跑不起来。这篇笔记就是围绕这几个卡点展开的从目录结构、标签转换、配置文件修改到训练命令、参数含义、常见报错一步步走完。适合已经跑通过一次 yolo v5 官方 demo、想换自己的数据集但被格式和配置卡住的从业者也适合想用 Bdd100k 做自动驾驶场景检测的工程师。下面所有步骤都是我在实际环境里验证过的命令可以直接抄参数会说明为什么这么设。2. Bdd100k 与 yolo v5 的格式差异先搞清楚要对齐什么2.1 Bdd100k 的标注长什么样Bdd100k 是自动驾驶场景的数据集图像来自行车记录仪视角覆盖白天、夜晚、雨天、隧道等多种光照和天气。它的检测标注以 JSON 文件组织通常按 train、val 划分每个 split 一个 JSON。JSON 里每条记录对应一张图包含图像文件名、图像宽高以及一个labels数组数组里每个元素是一个目标框字段有id、category、box2d。box2d里是x1、y1、x2、y2也就是左上角和右下角的绝对像素坐标。类别是字符串常见的有car、bus、truck、person、bike、motor、rider、traffic light、traffic sign、train这十类。这里有个容易忽略的点Bdd100k 的 JSON 里一张图可能没有任何目标labels是空数组也可能有大量小目标比如远处的交通灯。转换脚本必须能处理空标注否则训练时读到一个空 txt 会报错。另外Bdd100k 的图像尺寸不固定常见是 1280x720但不同子集可能有差异转换时不要假设固定宽高要从 JSON 里读width和height。2.2 yolo v5 要的标签格式yolo v5 的标签是每张图对应一个同名 txt 文件每行一个目标格式是class_id x_center y_center width height。这里的坐标全部是归一化到 0 到 1 之间的浮点数x_center和y_center是框中心点相对图像宽高的比例width和height是框宽高相对图像宽高的比例。class_id是从 0 开始的整数和data.yaml里names列表的顺序一一对应。所以转换的核心就两步把绝对坐标的x1 y1 x2 y2转成归一化的中心点加宽高把字符串类别映射成整数 id。映射关系必须固定训练和推理时用的是同一套否则类别会错乱。我一般会把映射写死在转换脚本里同时写进data.yaml两边保持一致。2.3 目录结构怎么摆yolo v5 默认按images和labels两个平行目录组织每个 split 下再分 train 和 val。推荐的结构是这样bdd100k_yolo/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── bdd100k.yaml图像和标签的文件名必须一一对应只是扩展名不同。比如images/train/0000f77c-6257be58.jpg对应labels/train/0000f77c-6257be58.txt。如果文件名对不上yolo v5 在训练时会跳过找不到标签的图或者把没有标签的图当成负样本这两种情况都会让训练结果偏离预期。转换脚本里最好加一步校验统计图像数量和标签数量是否一致不一致就打印出来。3. 把 Bdd100k 的 JSON 转成 yolo v5 标签脚本与四个边界坑3.1 转换脚本的完整实现下面这个脚本处理一个 split 的 JSON输出对应的 images 和 labels 目录。它假设图像已经按 split 放好脚本只负责生成标签和复制或软链图像。实际用时把路径改成自己的。import json import os import shutil from pathlib import Path # 类别映射顺序必须和 data.yaml 里的 names 一致 CATEGORY_MAP { car: 0, bus: 1, truck: 2, person: 3, bike: 4, motor: 5, rider: 6, traffic light: 7, traffic sign: 8, train: 9, } def convert_split(json_path, image_src_dir, out_root, split): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_out Path(out_root) / images / split lbl_out Path(out_root) / labels / split img_out.mkdir(parentsTrue, exist_okTrue) lbl_out.mkdir(parentsTrue, exist_okTrue) skipped 0 for item in data: img_name item[name] width item[width] height item[height] labels item.get(labels, []) # 复制图像 src_img Path(image_src_dir) / img_name if not src_img.exists(): skipped 1 continue shutil.copy(src_img, img_out / img_name) # 写标签 stem Path(img_name).stem lines [] for obj in labels: cat obj[category] if cat not in CATEGORY_MAP: continue box obj[box2d] x1, y1, x2, y2 box[x1], box[y1], box[x2], box[y2] # 裁剪到图像边界内 x1 max(0, min(x1, width)) x2 max(0, min(x2, width)) y1 max(0, min(y1, height)) y2 max(0, min(y2, height)) bw x2 - x1 bh y2 - y1 if bw 1 or bh 1: continue xc (x1 x2) / 2.0 / width yc (y1 y2) / 2.0 / height nw bw / width nh bh / height # 再夹一次防止浮点误差越界 xc min(max(xc, 0.0), 1.0) yc min(max(yc, 0.0), 1.0) nw min(max(nw, 0.0), 1.0) nh min(max(nh, 0.0), 1.0) lines.append(f{CATEGORY_MAP[cat]} {xc:.6f} {yc:.6f} {nw:.6f} {nh:.6f}) with open(lbl_out / f{stem}.txt, w) as f: f.write(\n.join(lines)) print(f{split}: done, skipped images {skipped}) if __name__ __main__: convert_split( json_pathbdd100k/labels/det_train.json, image_src_dirbdd100k/images/train, out_rootbdd100k_yolo, splittrain, ) convert_split( json_pathbdd100k/labels/det_val.json, image_src_dirbdd100k/images/val, out_rootbdd100k_yolo, splitval, )逻辑说明脚本先读 JSON遍历每条记录复制图像到输出目录同时把该图的所有目标框转成 yolo 格式写入同名 txt。CATEGORY_MAP是唯一的类别来源改类别只改这里和 yaml。坐标先裁剪到图像边界再算宽高宽高小于等于 1 像素的框直接丢弃因为这种框在训练里没有意义还会引入噪声。最后再夹一次归一化坐标防止浮点误差导致值略大于 1 或小于 0yolo v5 对越界坐标会告警甚至报错。参数说明json_path是 Bdd100k 的检测标注 JSONimage_src_dir是原始图像目录out_root是输出根目录split是 train 或 val。CATEGORY_MAP里的 id 从 0 开始连续顺序要和后面 yaml 里的names完全一致。3.2 坑一空标签文件与负样本Bdd100k 里有些图确实没有目标转换后会生成一个空 txt。yolo v5 对空 txt 的处理是把它当作负样本也就是背景图。这本身没问题但如果空 txt 数量过多比如超过总图数的 20%模型会偏向预测背景召回率下降。我的做法是统计空 txt 比例如果过高就在训练时用--noval之外的方式控制或者从训练集里抽掉一部分纯背景图。另一个更隐蔽的问题是如果图像复制失败但标签写成功了会出现有 txt 没图的情况训练时直接报文件找不到。所以脚本里先检查图像存在再写标签并且最后做一次数量校验。3.3 坑二类别名里的空格和大小写Bdd100k 的类别字符串里有traffic light和traffic sign中间有空格。如果直接拿字符串当 key没问题但如果有人用traffic_light或者Traffic Light去匹配就会漏掉大量目标。转换脚本里必须用原始字符串精确匹配。另外bike和motor是两类rider是骑行者不要合并。合并类别会让模型分不清骑车的人和摩托车在自动驾驶场景里这个区分很重要。3.4 坑三坐标越界与宽高为负Bdd100k 的标注整体质量不错但偶尔有框的x2小于x1或者坐标超出图像宽高。如果不处理算出来的宽高是负数归一化后也是负数yolo v5 训练时 loss 会变成 NaN。脚本里先对 x1、x2、y1、y2 做 min/max 裁剪再算宽高宽高小于等于 1 的丢弃基本能解决。如果还有异常可以在写入前加断言把异常框打印出来人工检查。3.5 坑四图像和标签文件名不一致Bdd100k 的图像文件名带-和长串 hash比如0000f77c-6257be58.jpg。转换时用Path(img_name).stem取主干写 txt 时用同一个主干就能保证一致。但如果在复制图像时改了名比如加了前缀标签名也要同步改。我见过有人复制图像时用了shutil.copy但目标路径写成了另一个名字结果标签对不上训练时大量图被跳过mAP 低得离谱排查半天才发现是文件名问题。4. 配置 yolo v5 训练 Bdd100kdata.yaml 与超参怎么设4.1 写对 data.yamlyolo v5 的数据配置文件决定它去哪里找图、去哪里找标签、有多少类、类名是什么。针对 Bdd100k 十类检测yaml 内容如下path: ./bdd100k_yolo train: images/train val: images/val nc: 10 names: 0: car 1: bus 2: truck 3: person 4: bike 5: motor 6: rider 7: traffic light 8: traffic sign 9: trainpath是数据集根目录train和val是相对path的子路径。yolo v5 会自动把images替换成labels去找标签所以目录结构必须是images/train对应labels/train。nc是类别数必须和names的长度一致也和转换脚本里CATEGORY_MAP的条目数一致。三处任何一处不一致训练要么报维度错误要么类别错乱。4.2 选模型规格与输入尺寸yolo v5 有 n、s、m、l、x 五个规格参数量和精度递增。Bdd100k 十万张图、十类目标如果从零训练n 或 s 就够跑通流程m 以上需要更大显存。如果做迁移学习从 COCO 预训练权重起步s 或 m 是比较平衡的选择。输入尺寸默认 640Bdd100k 原图是 1280x720缩放到 640 后远处的小目标比如交通灯会变得很小检测难度大。如果显存允许把--img设成 960 或 1280小目标召回会明显改善。我一般先用 640 跑一轮确认流程通再升到 960 做正式训练。4.3 训练命令与关键参数假设用 yolo v5 官方仓库数据配置放在data/bdd100k.yaml从 COCO 预训练权重起步命令如下python train.py \ --data data/bdd100k.yaml \ --weights yolov5s.pt \ --img 960 \ --batch-size 16 \ --epochs 50 \ --workers 8 \ --device 0 \ --project runs/train \ --name bdd100k_s_960参数说明--data指向 yaml--weights是预训练权重从 COCO 起步能加快收敛--img是训练输入尺寸必须是 32 的倍数--batch-size根据显存调16 在 24G 显存上跑 960 尺寸比较稳显存小就降到 8 或 4--epochs是训练轮数Bdd100k 数据量大50 轮起步如果 loss 还在降可以加到 100--workers是数据加载线程设成 CPU 核数的 0.7 倍左右--device 0指定第一块 GPU--project和--name决定输出目录。训练过程中重点看几个指标box_loss、obj_loss、cls_loss是否稳定下降mAP0.5是否上升。如果obj_loss不降反升可能是学习率太大或者标签有问题。如果mAP一直很低先检查标签转换是否正确用--img调大试试小目标。4.4 学习率与优化器yolo v5 默认用 SGD学习率通过--lr0设初始值默认 0.01--lrf设最终学习率比例默认 0.01。Bdd100k 数据量大0.01 的初始学习率通常没问题。如果 loss 震荡厉害降到 0.005 试试。如果显存小、batch 小学习率也要相应降低因为 batch 小梯度噪声大。优化器可以换 Adam用--adam开启但 Adam 在检测任务上不一定比 SGD 好我一般先用默认 SGD效果不好再换。4.5 数据增强参数yolo v5 内置了 mosaic、mixup、HSV 增强、随机翻转等。Bdd100k 场景多样默认增强基本够用。如果发现夜晚和雨天场景检测差可以适当加大 HSV 的 V 通道扰动或者用--augment做测试时增强。但增强不是越多越好mosaic 概率太高会让小目标更难学--mosaic默认 1.0可以降到 0.5 试试。--mixup默认 0开启后对遮挡场景有帮助但训练时间变长。5. 训练 Bdd100k 时最容易翻车的五个地方5.1 现象训练一开始就报AssertionError: train: No labels found原因yolo v5 在images/train同级找labels/train如果目录名不对或者标签文件扩展名不是.txt或者标签目录为空就会报这个。另一个常见原因是 yaml 里的train路径写成了绝对路径但path拼接后不对。解决先确认目录结构是bdd100k_yolo/images/train和bdd100k_yolo/labels/train再确认labels/train里有 txt 文件。用ls bdd100k_yolo/labels/train | head看一眼。如果目录对但还报错检查 yaml 里train是不是写成了images/train/带尾斜杠去掉尾斜杠试试。5.2 现象训练 loss 正常下降但 mAP 一直是 0 或者极低原因最常见的是类别映射错位。比如转换脚本里car是 0但 yaml 里names第 0 个写成了bus模型学到的和评估时对不上。另一个原因是验证集标签没转换或者验证集路径指向了训练集。解决用一张图手动验证。取一张训练图用转换后的 txt 画框和原图对比确认框的位置和类别都对。再检查 yaml 里names的顺序和转换脚本的CATEGORY_MAP是否完全一致。最后确认val路径指向的是验证集不是训练集。5.3 现象显存溢出报CUDA out of memory原因--img太大、--batch-size太大、模型规格太大三者叠加超过显存。Bdd100k 图像多数据加载也可能占内存。解决按优先级降--batch-size再降--img最后换更小的模型。比如从--img 960 --batch-size 16降到--img 640 --batch-size 16或者--img 960 --batch-size 8。也可以用--accumulate做梯度累积模拟大 batch但训练速度会慢。5.4 现象训练到一半 loss 变成 NaN原因标签里有非法值比如坐标越界、宽高为负、类别 id 超出nc范围。也可能是学习率太大导致梯度爆炸。解决先检查标签。用脚本扫一遍所有 txt看有没有坐标不在 0 到 1 之间或者类别 id 大于等于nc。如果有回到转换脚本修。如果标签没问题把--lr0降到 0.001 再试。另外--amp混合精度在某些显卡上会导致 NaN可以加--noamp关掉。5.5 现象训练速度极慢GPU 利用率低原因--workers太小数据加载成为瓶颈或者图像太大解码慢或者磁盘 IO 慢。解决把--workers加到 CPU 核数的 0.7 倍比如 16 核设 12。如果图像是 JPEG解码本身耗时可以先把图像统一转成更小的尺寸或者用更快的解码库。另外把数据集放在 SSD 上别放机械盘。如果用了--cache注意内存够不够Bdd100k 全量缓存需要很大内存。6. 在 Bdd100k 上把 yolo v5 小目标召回提上去的两个技巧6.1 用切片推理补小目标Bdd100k 里交通灯和远处行人很小640 或 960 输入下经过多次下采样后这些小目标在特征图上只剩几个像素很容易漏检。一个不重新训练就能见效的办法是切片推理把原图切成有重叠的小块分别推理再把结果合并。yolo v5 官方没有内置这个但可以自己写一个推理脚本用--img对小图推理最后用 NMS 合并。代价是推理时间成倍增加适合离线评估或对召回要求高的场景。import cv2 import numpy as np from pathlib import Path def slice_infer(model, img_path, slice_size640, overlap128, conf0.25, iou0.45): img cv2.imread(str(img_path)) h, w img.shape[:2] step slice_size - overlap boxes [] scores [] classes [] for y in range(0, h, step): for x in range(0, w, step): x2 min(x slice_size, w) y2 min(y slice_size, h) x1 max(0, x2 - slice_size) y1 max(0, y2 - slice_size) patch img[y1:y2, x1:x2] results model(patch, sizeslice_size)[0] for det in results.xyxy[0].cpu().numpy(): bx1, by1, bx2, by2, sc, cl det boxes.append([bx1 x1, by1 y1, bx2 x1, by2 y1]) scores.append(sc) classes.append(cl) # 用 OpenCV NMS 合并 idx cv2.dnn.NMSBoxes( bboxes[[b[0], b[1], b[2] - b[0], b[3] - b[1]] for b in boxes], scoresscores, score_thresholdconf, nms_thresholdiou, ) final [] for i in idx: final.append((boxes[i], scores[i], classes[i])) return final逻辑说明把原图按slice_size切块块之间有overlap重叠避免目标被切断。每块单独推理把框坐标映射回原图最后用 NMS 去重。overlap设成slice_size的 20% 左右太小会漏掉跨块目标太大推理次数增加。这个脚本是示意实际用的时候要接 yolo v5 的模型加载和推理接口。6.2 调小锚框和增大输入尺寸yolo v5 的默认锚框是在 COCO 上聚类得到的对 Bdd100k 的小目标不一定最优。如果不想重聚类最直接的办法是增大--img。从 640 升到 960小目标在特征图上的像素数增加一倍多召回提升明显。如果显存不够可以只对包含小目标的图做高分辨率推理或者用--img 960训练但--batch-size降到 8。另一个办法是修改模型配置文件里的锚框把小的锚框尺寸再调小但这需要重新聚类步骤多一些。我一般先试增大输入尺寸效果不够再动锚框。6.3 验证改进是否有效改完推理方式或输入尺寸后不要只看整体 mAP要单独看小目标类别的 AP比如traffic light和person。yolo v5 训练结束会输出每个类别的 P、R、mAP0.5、mAP0.5:0.95。对比改进前后这几个类的数值如果traffic light的 AP 提升了说明方向对。如果整体 mAP 没变但小目标 AP 升了也是值得的因为自动驾驶场景里小目标漏检的代价比误检大。我自己的习惯是每次只改一个变量改完跑一次验证记录数值避免多个改动叠加后分不清哪个起作用。这套流程在 Bdd100k 上跑通后换到其他行车记录仪数据集也能复用关键是标签转换和类别映射这两步别出错。希望帮到你。本文还有配套的精品资源点击获取