ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

BDD100K 上训 YOLOv5 实战:从数据转换到 mAP 掉点排查

BDD100K 上训 YOLOv5 实战:从数据转换到 mAP 掉点排查 简介这份资源面向计算机视觉初学者与目标检测实践者提供在 Bdd100k 自动驾驶数据集上训练 YOLOv5s 的完整工程解决从数据预处理到模型训练、推理验证的全流程问题。压缩包共 85 个文件约 97.7MB包含 16 个 Python 脚本、17 个 YAML 配置、3 个 Jupyter Notebook、5 个预训练权重文件及若干训练日志与可视化图片覆盖模型定义、数据配置、训练与检测脚本等模块。资源内含预训练权重训练与从头训练两套流程记录并附带 Bdd100k 预处理 Notebook、模型架构图以及 4K 测试视频展示链接便于读者对照复现实验、理解数据组织与训练参数设置。目前已有 110 人学习下载适合希望快速上手 YOLOv5 自定义数据集训练、掌握 Bdd100k 预处理与调参思路的读者参考。1. 在 BDD100K 上训 YOLOv5从零跑通到 mAP 掉点的真实路径BDD100K 是目前少有的把「城市道路 多天气 多时段」打包在一起的公开驾驶数据集10 万张图、10 类目标标注里还带天气、场景、时间这些属性字段。很多人第一次拿它训 YOLOv5跑完发现 mAP 比 COCO 上低一大截甚至怀疑自己代码写错了。问题往往不在模型而在数据本身类别极度不均衡、小目标密集、夜间和雨雾样本占比不低直接套默认配置必然翻车。这篇笔记面向已经会跑 YOLOv5 官方 demo、想把这套组合真正落到自己项目里的工程师从数据转换、配置改写、训练参数到掉点排查一步步走完。读完你应该能独立复现一版可用的 BDD100K 检测模型并且知道每个参数为什么这么设。2. BDD100K 与 YOLOv5 的匹配度先想清楚再动手2.1 BDD100K 的标注结构到底长什么样BDD100K 的图像是 1280×720标注分两类检测用的框和可行驶区域的分割掩码。检测标注以 JSON 组织一张图一条记录里面labels数组每个元素包含id、category、box2dx1,y1,x2,y2以及属性字段。类别一共 10 个pedestrian、rider、car、bus、truck、train、motorcycle、bicycle、traffic light、traffic sign。注意rider指的是骑车的人和bicycle/motorcycle是分开的两类标注时人和车各一个框这点和 COCO 的person逻辑不同转换时不能想当然合并。属性字段里weather、scene、timeofday对训练很有价值。比如你想做夜间专项优化可以直接按timeofday night筛子集想做雨天鲁棒性按weather rainy抽验证集。这些字段在 JSON 里是字符串转换脚本里顺手保留成额外索引文件后面做分层评估会省很多事。2.2 为什么 YOLOv5 默认配置在 BDD100K 上会掉点YOLOv5 官方权重是在 COCO 上训的COCO 的类别分布相对均匀小目标比例也没 BDD100K 这么极端。BDD100K 里traffic light和traffic sign的框普遍很小1280×720 缩到 640 后一个红绿灯可能只剩十几个像素。默认的 anchor 是按 COCO 聚类的对这类细长小目标匹配度差正样本命中率低训练时这些类的梯度信号很弱mAP 自然上不去。另一个坑是类别不均衡。car占了绝大多数框train和rider少得可怜。默认的 BCE 分类损失不做重加权模型会倾向于把不确定的框都预测成car导致稀有类召回极低。常见做法是在data.yaml里给稀有类调cls损失权重或者用--hyp覆盖默认超参。我一般先把fl_gamma打开focal loss再对train、rider这类做适度过采样比单纯改 anchor 见效快。2.3 转换前必须确认的三件事动手写转换脚本之前先确认目录结构和文件完整性。BDD100K 官方下载下来通常是bdd100k_images_100k.zip和bdd100k_labels_release.zip两个包解压后图像在bdd100k/images/100k/train和val标注在bdd100k/labels/bdd100k_labels_release/bdd100k/labels下检测标注文件名是bdd100k_labels_images_train.json和..._val.json。先ls确认这两个 JSON 存在再确认图像数量和 JSON 记录数一致差太多说明解压不完整后面转换会静默丢图。提示BDD100K 的验证集官方叫val但有些镜像包会写成validation转换脚本里路径最好做成可配置参数别写死。3. 把 BDD100K 转成 YOLO 格式脚本、参数与四个边界坑3.1 转换脚本的核心逻辑YOLO 格式要求每张图一个同名.txt每行class_id cx cy w h坐标全部归一化到 0~1。BDD100K 给的是绝对像素坐标x1,y1,x2,y2需要先算中心点和宽高再除以图像宽高。下面是我常用的转换脚本处理了类别映射、越界裁剪和空标注三个问题。import json import os from pathlib import Path # BDD100K 10 类到 YOLO 连续 id 的映射顺序一旦定下就不要改 CATEGORY_MAP { pedestrian: 0, rider: 1, car: 2, bus: 3, truck: 4, train: 5, motorcycle: 6, bicycle: 7, traffic light: 8, traffic sign: 9, } def convert(json_path, img_dir, out_dir, img_w1280, img_h720): os.makedirs(out_dir, exist_okTrue) with open(json_path, r) as f: data json.load(f) skipped 0 for record in data: name record[name] # 形如 0000f77c-6257be58.jpg stem Path(name).stem labels record.get(labels, []) lines [] for obj in labels: cat obj.get(category) if cat not in CATEGORY_MAP: continue # 忽略非检测类如 drivable area box obj.get(box2d) if box is None: continue # 有些记录只有多边形没有框 x1, y1, x2, y2 box[x1], box[y1], box[x2], box[y2] # 裁剪到图像范围内防止个别越界框把归一化坐标搞成负数 x1, x2 max(0, min(x1, img_w)), max(0, min(x2, img_w)) y1, y2 max(0, min(y1, img_h)), max(0, min(y2, img_h)) w, h x2 - x1, y2 - y1 if w 1 or h 1: continue # 过滤掉退化框 cx, cy (x1 x2) / 2 / img_w, (y1 y2) / 2 / img_h nw, nh w / img_w, h / img_h lines.append(f{CATEGORY_MAP[cat]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}) if not lines: skipped 1 continue # 没有有效框的图不生成 txt with open(os.path.join(out_dir, stem .txt), w) as f: f.write(\n.join(lines)) print(fdone, skipped {skipped} images without valid boxes) if __name__ __main__: convert(bdd100k_labels_images_train.json, bdd100k/images/100k/train, datasets/bdd100k/labels/train) convert(bdd100k_labels_images_val.json, bdd100k/images/100k/val, datasets/bdd100k/labels/val)逻辑上分四步读 JSON、逐框映射类别、裁剪并归一化、写 txt。CATEGORY_MAP的顺序就是最终模型输出的类别顺序训练和推理必须一致中途改顺序等于让模型重新学。skipped计数是给你一个 sanity check如果跳过的图超过总量 5%说明 JSON 和图像对不上得回去查路径。3.2 图像目录怎么摆才不踩 YOLOv5 的路径坑YOLOv5 的data.yaml里train和val指向的是图像目录不是标签目录。标签目录默认是图像路径里把images替换成labels。所以最省事的摆法是datasets/bdd100k/ images/train/ *.jpg images/val/ *.jpg labels/train/ *.txt labels/val/ *.txt如果你不想移动原始图像可以用软链接但要注意 YOLOv5 在部分环境下对软链接的缓存处理有差异第一次跑建议直接复制或硬链接确认流程通了再换软链。data.yaml内容如下path: ../datasets/bdd100k train: images/train val: images/val nc: 10 names: [pedestrian,rider,car,bus,truck,train,motorcycle,bicycle,traffic light,traffic sign]nc必须等于 10names顺序和脚本里的CATEGORY_MAP严格对应。写错一个名字不会报错但评估时你会看到某个类 mAP 恒为 0排查半天才发现是名字对不上。3.3 四个边界坑空标注、越界框、类别名空格、文件名大小写第一个坑是空标注图。BDD100K 里有些图只有可行驶区域没有检测框转换后不生成 txtYOLOv5 训练时读到没有对应标签的图会直接跳过不会报错但你的有效训练集就悄悄变小了。解决办法是在转换时统计并打印跳过数量心里有数。第二个坑是越界框。个别标注的x2会略大于 1280不裁剪的话归一化后cx可能超过 1YOLOv5 的 dataloader 会把这些框当异常值处理轻则警告重则训练不稳定。脚本里的max(0, min(...))就是干这个的。第三个坑是类别名里的空格。traffic light和traffic sign带空格如果你在别的地方用空格分隔的格式解析会直接错位。YOLO 格式用的是数字 id所以转换后没这个问题但你在写评估脚本读names时要注意别用空格切分。第四个坑是文件名大小写。BDD100K 的图像名是 UUID 加.jpg全小写但有些系统解压后可能保留原始大小写。Linux 下大小写敏感0000f77c.jpg和0000F77C.jpg是两个文件转换脚本用Path(name).stem拿到的 stem 必须和实际图像文件名完全一致否则训练时找不到图。转换完跑一遍ls images/train | wc -l和ls labels/train | wc -l数量应该接近差值就是空标注图数量。4. 训练配置与参数调优让 mAP 从 0.2 爬到可用4.1 从官方权重起步还是从零训BDD100K 有 7 万张训练图从零训 YOLOv5s 大概需要几百 epoch 才能收敛单卡时间成本很高。常见做法是加载yolov5s.pt官方权重做迁移学习冻结 backbone 先训几个 epoch 让 head 适应新类别再解冻全量微调。命令如下python train.py \ --img 640 \ --batch 16 \ --epochs 100 \ --data data/bdd100k.yaml \ --weights yolov5s.pt \ --cfg models/yolov5s.yaml \ --hyp data/hyp.scratch-low.yaml \ --freeze 10 \ --cache--freeze 10冻结前 10 层backbone 大部分先让检测头适应 10 类输出。跑 5~10 个 epoch 后可以停掉去掉--freeze重新开一次全量训练或者直接用--freeze 0从头微调。--cache把图像缓存到内存BDD100K 全量约 7 万张 720p 图内存小于 32G 的机器慎用容易 OOM可以改成--cache disk。4.2 关键超参anchor、学习率、focal lossanchor 建议重新聚类。YOLOv5 自带utils/autoanchor.py训练启动时会自动检查 anchor 和数据的匹配度如果best possible recall低于 0.98 会提示你重聚类。手动跑python utils/autoanchor.py --data data/bdd100k.yaml --weights yolov5s.pt它会输出一组新的 anchor你把这组值填进模型 cfg 里对应的anchors字段。对 BDD100K 这种小目标多的数据重聚类后traffic light的召回通常能涨几个点。学习率方面hyp.scratch-low.yaml的lr0是 0.01对微调偏大我一般降到 0.001~0.003配合 cosine 调度。lrf设 0.1 左右让学习率平滑衰减。focal loss 的fl_gamma默认 0.0改成 1.5 对稀有类有帮助但别开太大超过 2.0 容易让car这类主导类的召回下降。# 我常用的微调超参片段覆盖在 hyp 文件里 lr0: 0.002 lrf: 0.1 fl_gamma: 1.5 cls: 0.7 # 分类损失权重稀有类多时可提到 0.8 box: 0.05 obj: 0.7cls和obj的权重调整要一起看。cls调高会让模型更关注分类正确性但可能牺牲定位精度obj调高会让模型更保守地预测目标存在性漏检可能增加。建议一次只动一个跑 10 个 epoch 看验证集曲线再决定下一步。4.3 用 BDD100K 的属性字段做分层验证训练完不能只看一个总 mAPBDD100K 的价值在于它的属性标注。你可以按timeofday把验证集分成 day/night按weather分成 clear/rainy/snowy分别跑val.py看每层的 mAP。做法是先用 JSON 生成每个子集的图像名列表再写一个简单的筛选脚本把对应图复制到临时目录用--data指向一个只含该子集的 yaml。python val.py \ --data data/bdd100k_night.yaml \ --weights runs/train/exp/weights/best.pt \ --img 640 \ --task val如果夜间 mAP 比白天低 15 个点以上说明模型对低照度泛化差可以考虑在训练时对夜间样本过采样或者加亮度增强。这一步是很多人忽略的但恰恰是 BDD100K 相比 COCO 最有价值的地方——它逼你面对真实场景的分布偏移。5. 避坑与排查训练不收敛、mAP 异常、显存爆炸的现场记录5.1 现象loss 一直震荡不下降cls_loss 居高不下原因通常是学习率太大或者 anchor 不匹配。BDD100K 的框尺度分布和 COCO 差异大默认 anchor 下正样本匹配率低分类梯度噪声大。解决先跑 autoanchor 重聚类再把lr0降到 0.001 试 10 个 epoch观察cls_loss是否稳定下降。如果还震荡检查data.yaml的names顺序和转换脚本是否一致顺序错位会让模型学一个自相矛盾的分类目标。5.2 现象验证集 mAP 正常但推理时大量漏检小目标原因多半是输入分辨率。训练用--img 6401280×720 缩到 640 后小目标信息损失严重。解决训练和推理都提到--img 960或1280显存不够就降 batch。另一个可能是 NMS 的conf-thres设太高默认 0.25 对traffic light偏大可以降到 0.1 再试。注意降 conf 会引入更多误检要结合业务权衡。5.3 现象训练到一半显存爆炸进程被 kill--cache是头号嫌疑。BDD100K 全量图像缓存到 RAM 大约需要 20~30G加上模型和数据加载32G 机器很容易被 OOM killer 干掉。解决去掉--cache或改成--cache disk后者把缓存写到磁盘速度慢一点但稳。另外--workers设太大也会因为每个 worker 复制数据而吃内存8 核机器设 4 就够。5.4 现象某个类别 mAP 始终为 0先确认这个类在验证集里有没有样本。train类在 BDD100K 里非常少验证集可能只有几十个框mAP 波动极大甚至为 0 是正常的。如果car这种大类也为 0那基本是类别 id 映射错了回去对CATEGORY_MAP和data.yaml的names。还有一种情况是转换时该类框全被过滤比如尺寸小于 1 像素检查脚本里的w 1 or h 1阈值是否过严。5.5 现象训练日志里Labels数量远小于预期YOLOv5 启动时会打印每个类别的标签数量。如果总数比 BDD100K 官方统计少很多说明转换时丢了一批图。常见原因是 JSON 里box2d字段缺失只有多边形标注被跳过或者图像文件名和 JSON 里的name对不上。解决在转换脚本里加一个计数器分别统计「无 labels」「无 box2d」「框退化」三种跳过原因定位是哪一类问题。6. 进阶技巧用加权 NMS 和 TTA 把夜间 mAP 再抬一档训练跑通只是及格线真正上线前还有两个低成本提点手段可以试。第一个是类别加权 NMS。BDD100K 里car的框数量碾压其他类标准 NMS 在密集场景下容易把相邻的pedestrian框误抑制掉。YOLOv5 的val.py和detect.py支持--agnostic-nms但那是类别无关的更细的做法是改utils/general.py里的non_max_suppression对不同类别用不同的 IoU 阈值。比如给pedestrian和traffic light设 0.6给car设 0.45减少小目标被误杀。第二个是 TTA测试时增强。YOLOv5 自带--augment参数推理时对图像做翻转和多尺度把结果融合。对夜间和雨雾样本TTA 通常能涨 1~3 个点代价是推理时间翻几倍。如果业务对延迟不敏感值得开。命令很简单python detect.py \ --weights runs/train/exp/weights/best.pt \ --source datasets/bdd100k/images/val \ --img 960 \ --augment \ --conf-thres 0.15 \ --save-txt--augment会做水平翻转和三个尺度的推理再融合--conf-thres配合 TTA 可以适当降低因为融合后误检会被平均掉一部分。跑完对比一下开和关的 mAP如果涨幅不到 1 个点说明你的模型对这个数据集已经比较鲁棒没必要为这点收益牺牲推理速度。还有一个我踩过的坑TTA 和加权 NMS 同时开的时候如果conf-thres设得太低融合后的框数量会爆炸后处理时间可能超过推理本身。建议先单独开 TTA 调好阈值再叠加 NMS 改动每次只动一个变量。这套组合我在夜间子集上从 0.31 抬到 0.36代价是单帧推理从 18ms 涨到 55ms值不值得看你的场景。希望帮到你。本文还有配套的精品资源点击获取
返回列表