YOLOv8工业目标检测实战:火车、轨道、手推车数据集训练与调优

YOLOv8工业目标检测实战:火车、轨道、手推车数据集训练与调优 简介面向YOLO系列算法目标检测任务提供火车、轨道、手推车三类目标的标注数据集适合计算机视觉学习者、算法工程师及科研人员直接用于模型训练、验证和测试。资源已将数据划分好并附带data.yaml配置文件可无缝适配YOLOv5、YOLOv7、YOLOv8、YOLOv9、YOLOv10及YOLO11等主流版本省去数据集格式转换与划分的繁琐工作。压缩包共2000个文件以VOC格式xml标注文件为主同时包含YOLO格式txt标签整体236.33MB。yolo格式每行记录类别索引、归一化中心坐标与宽高voc格式通过xml描述目标边界框两种标注分目录存放便于对照不同算法框架使用。文件名末尾带有部分类别名称可快速辨识样本资源已具备直接训练和验证测试的条件适合用于交通场景目标检测的模型训练、迁移学习与算法对比实验。目前已有100人学习是目标检测入门与进阶的实用数据资源。1. 为什么“火车、轨道、手推车”三个类别值得单独建数据集很多目标检测教程拿 COCO、VOC 练手类别动辄 20 个以上场景又是自然图像。真正到了轨交货场、检修库这类工业现场你会发现模型在通用数据集上表现好不等于在货运场景里能直接用火车车厢常常占据大半幅画面轨道是宽度只有十几个像素的细长条手推车在视野远端可能只占几十个像素三类目标尺度差了十倍不止。用 3793 张带标签图像跑 YOLO 训练本质不是在复现教程而是在解决一个典型的工业小样本加极端长宽比目标检测问题。这篇文章按我处理这类数据集的实际流程来写先解析 YOLO 标签格式和类别分布再做目录整理与训练然后针对火车、轨道、手推车的尺度差异调优最后用验证和可视化确认模型可用。新手照着能跑通老手可以在参数与坑点上对照自己的处理方式。2. 先拆数据集YOLO 标签格式、目录结构和类别分布2.1 解压后先确认三样文件拿到这类“数据集 zip”我不会急着把文件丢进训练脚本。先解压再用 tree 或 find 看一眼顶层结构。常见交付形式是 images 和 labels 两个并列目录images 里放原始 JPG/PNGlabels 里放同名 txt也可能是每个子目录内同时放图像和对应标签再看 train.txt / val.txt 列表提供划分。这两种结构不能直接套同一个流程后者需要先合并成统一目录。我一般会先跑一段命令确认unzip YOLO算法-火车-轨道-手推车数据集-3793张图像带标签-火车-轨道-手推车.zip -d freight cd freight find . -maxdepth 2 -type d | sort find . -name *.txt | wc -l find . -name *.jpg -o -name *.png | wc -l第一条命令解压到 freight 目录第二条列出两层以内的文件夹结构可以立刻看出 labels 目录是否独立存在第三条和第四条分别统计标签与图像数量。这里有个容易误判的点工业数据集经常把没有目标的纯背景图也放进 images导致 txt 数量少于图像数。只要差值在合理范围内不算交付缺漏不必删图后期在 data.yaml 里靠标签配对逻辑管理即可。2.2 YOLO 标签格式一行一个目标五个数字YOLO 的 txt 标签不用 XML 或 JSON每行就是五个空格分隔的数字对应一个检测框class_id x_center y_center width height后四列是相对图像宽高的归一化值范围落在 0 到 1 之间。类别编号从 0 开始所以这个数据集里比较常见的分配是 0 为火车 train、1 为轨道 track、2 为手推车 trolley但真实顺序要以标签文件里实际出现的 id 为准不能听 zip 文件名猜。归一化的好处是同一个框在 1920×1080 和 640×640 下对应同一行文本不需要按图像尺寸换算。我会顺手抽两个标签文件看真实内容sed -n 1,5p labels/train/000123.txt如果看到某一行 width 接近 1.0说明标注把整条轨道拉成了一个贯穿画面的长框。这种极端长宽比目标会直接影响 anchor 的效果也给后续 mosaic 增强带来麻烦先留意到这里后调参时会用到这个观察。2.3 先统计类别数量和尺度再决定要不要加权重在划分数据集之前我会先算清楚三件事每类目标有多少个、图像尺寸分布、标注框宽高比分布。这些数字能直接回答“类别是否不平衡”“轨道是不是极端细长”“手推车是不是以小目标为主”。用一个小脚本就能算from pathlib import Path label_dir Path(labels/train) class_count {0: 0, 1: 0, 2: 0} w_sum {0: 0.0, 1: 0.0, 2: 0.0} h_sum {0: 0.0, 1: 0.0, 2: 0.0} for txt in label_dir.glob(*.txt): for line in txt.read_text().strip().splitlines(): parts line.strip().split() if len(parts) ! 5: continue cls int(parts[0]) class_count[cls] 1 w_sum[cls] float(parts[3]) h_sum[cls] float(parts[4]) for cls in sorted(class_count): n max(class_count[cls], 1) print(fclass {cls}: count{class_count[cls]} avg_w{w_sum[cls]/n:.3f} avg_h{h_sum[cls]/n:.3f})脚本按行拆分标签忽略格式异常的行只统计类别数量、平均框宽和高。输出示例类别目标数量平均归一化宽平均归一化高判读0 train相对较少0.500.30大目标宽高比适中1 track可能最多0.750.03极端细长条2 trolley中等偏少0.050.05大量小目标这里的具体数值只是示例以你本机统计为准。判读的结论比数值本身更重要如果 track 数量远大于 train说明轨道被拆成小段标注训练时目标数量足够但每段都太短容易互相混淆如果 trolley 的平均框宽只有 0.05按 1920 像素的图算也才不到 100 像素属于小目标范畴。这组数据直接决定后面要不要做类别过采样、要不要加大输入分辨率。3. 把原始图像整理成 YOLOv8 能直接使用的目录结构3.1 用 Python 脚本划分 train/val按 8:2 起步统一目录结构是 YOLOv8 训练前的关键一步。常见做法是 images/train、images/val 和 labels/train、labels/val 四个目录两侧文件名一一对应。3793 张图像这个量级train/val 按 8:2 划分足够我一般不单独切 test而是在数据集外手动留 30 到 50 张完全没参与训练的图像等训练全部结束后再拿来做人工确认。下面这段脚本可以直接用import random import shutil from pathlib import Path random.seed(42) src_images Path(images) src_labels Path(labels) dst Path(freight_yolo) # 收集所有有标签的图像主文件名 all_ids [] for ext in (*.jpg, *.jpeg, *.png): all_ids [p.stem for p in src_images.glob(ext)] all_ids sorted(set(all_ids)) random.shuffle(all_ids) val_n int(len(all_ids) * 0.2) val_ids set(all_ids[:val_n]) train_ids [aid for aid in all_ids if aid not in val_ids] for split, ids in [(train, train_ids), (val, val_ids)]: for sub in (images, labels): (dst / sub / split).mkdir(parentsTrue, exist_okTrue) for aid in ids: img_candidate None for ext in (*.jpg, *.jpeg, *.png): p src_images / f{aid}{ext[1:]} if p.exists(): img_candidate p break lbl src_labels / f{aid}.txt if img_candidate is not None and lbl.exists(): shutil.copy(img_candidate, dst / images / split / img_candidate.name) shutil.copy(lbl, dst / labels / split / lbl.name)脚本逻辑分三步先以图像主文件名构建 id 列表随机打乱后取前 20% 作为 val再按 id 找到图像和标签要求两个文件都存在才复制。注意脚本里用 ext[1:] 拼文件名例如 ext*.jpg 时 ext[1:].jpg配合 f{aid}{ext[1:]} 生成完整文件名。复制而不是移动是为了保留原始数据方便后面出问题时回退。划分后目录结构是freight_yolo/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/随机种子固定为 42保证重复执行得到相同划分。工业数据集里偶尔会出现图像与标签不在同一层目录、后缀不一致的情况脚本里已经做了处理如果重新执行建议不要同时跑多份避免覆盖掉之前留出的人工确认图片。3.2 校验标签空文件、越界框和类别号错误数据统计只能摸底不能代替校验。实际训练中常见的三个问题标签是空文件、框的归一化坐标超出 0 到 1、类别 id 不在 0 到 2 之间。最后一个问题最隐蔽会让模型把两类目标当成同一类训练指标还一切正常。我的做法是先跑一个校验函数from pathlib import Path def check_labels(label_root: Path, num_classes: int 3): bad [] for txt in label_root.glob(*/*.txt): if txt.stat().st_size 0: bad.append((txt, empty)) continue for line in txt.read_text().strip().splitlines(): parts line.split() if len(parts) ! 5: bad.append((txt, not5cols)) continue cls int(parts[0]) if cls 0 or cls num_classes: bad.append((txt, badcls)) for v in map(float, parts[1:]): if not 0.0 v 1.0: bad.append((txt, outofrange)) break return bad for t, reason in check_labels(Path(freight_yolo/labels))[:20]: print(t, reason)这里 glob(/.txt) 是匹配 train 和 val 两层下的所有标签读每一行先检查列数再检查类别 id最后检查四个坐标是否都在 0 到 1 之内。遇到越界框我不会直接删行而是把坐标裁剪回 0 到 1 范围。框主体可能还在图像内删掉会损失一个真实目标裁剪则只是丢掉画面外的部分保留的框仍然有效。3.3 配置 data.yamlpath 字段用绝对路径YOLOv8 的 data.yaml 是训练入口最常踩的坑是 path 的相对路径写法。训练脚本经常在不同目录下被执行相对路径会直接导致数据集找不到。工业项目里我会一律写成绝对路径# freight_yolo/data.yaml path: /home/user/datasets/freight_yolo train: images/train val: images/val names: 0: train 1: track 2: trolleypath 是数据集根目录train 和 val 相对 path 拼接names 的 id 顺序必须和标签文件里的 class_id 完全一致顺序反了会出现“火车被标成手推车”而 mAP 完全正常的诡异现象。如果交付方附带了 classes.txt 或 labels.txt优先对照它写不要自己猜。3.4 先跑一条默认 baseline别急着调参数数据集整理完第一次训练我会用默认参数先跑 baseline。模型尺寸选 yolov8s对 3793 张图这个规模从 COCO 预训练权重起步已经是性价比最高的做法一上来用 yolov8x 只会又慢又容易过拟合。命令如下yolo detect train \ data/home/user/datasets/freight_yolo/data.yaml \ modelyolov8s.pt \ epochs100 imgsz640 batch16 workers8 cacheTruemodel 指定预训练权重yolov8s.pt 是 COCO 预训练模型不是从头训练epochs100 对当前数据规模足够imgsz640 是默认输入尺寸对轨道这种细长目标不太够但 baseline 阶段先跑通流程结果记录下来作为对比基线batch16 在 12GB 显存内可以承受cacheTrue 把图像预加载到内存训练时减少磁盘 IO。训练结束后 runs/detect/train/weights/ 下会出现 best.pt 和 last.pt后续评估和推理统一使用 best.pt。这个阶段不要提前清洗数据。觉得“质量差”的模糊图、遮挡图在工业场景里往往是最有价值的难例保留它们才会让模型在实拍环境中更稳。清洗动作放到第 5 章的 bad case 复盘阶段那时有依据再动手。4. 针对火车、轨道、手推车的尺度差异调参4.1 先加大 imgsz对小目标和细长轨道最直接第一次 baseline 跑完后验证集指标通常会呈现两个规律track 的 mAP50 不低但 mAP50-95 偏低trolley 在远处漏检偏多。前者说明模型能框出轨道的大致位置但 IoU 精度不够后者说明小目标的特征在默认分辨率下已经被压缩掉了。两个问题的共同解法是先加大输入尺寸而不是去调 anchor。把 imgsz 从 640 提升到 960对轨道这类长条目标和远端手推车的影响是实打实的特征图上目标占据的像素更多中心点定位误差相对变小。代价是显存占用和训练时间上升。12GB 显存配合 yolov8simgsz960、batch8 是可以跑的如果想上 1280batch 要降到 4梯度噪声会变大。我的经验是先 960 起步如果 mAP50-95 涨幅明显但还未收敛再考虑 1280。anchor 策略上YOLOv8 默认自动学习 anchor让模型自己拟合三类目标的尺度分布即可手动指定适配极端长宽比的静止 anchor通常会在另外两类目标上掉点得不偿失。4.2 mosaic 与 close_mosaic轨道容易被拼图截断mosaic 把四张图拼接成一张喂给模型是 Ultralytics 训练中增强效果最明显的策略。对火车和手推车这种封闭轮廓目标mosaic 有助于提升泛化但对贯穿画面的轨道拼接时很可能在中间被直接截断。训练集里出现大量半截轨道模型学到的就是“轨道可以被截断”验证时反而框不对完整轨道。针对这个问题YOLOv8 提供了 close_mosaic 参数含义是训练最后 N 个 epoch 关闭 mosaic 增强让模型在收尾阶段回到正常图像分布。yolo detect train \ data/home/user/datasets/freight_yolo/data.yaml \ modelyolov8s.pt \ epochs150 imgsz960 batch8 \ mosaic1.0 close_mosaic15 \ hsv_h0.015 hsv_s0.7 hsv_v0.4参数说明mosaic1.0 表示全程开启 mosaic但最后按 close_mosaic 控制关闭取 15 表示最后 15 个 epoch 不再做 mosaic 拼接hsv_h、hsv_s、hsv_v 控制色相、饱和度、明度抖动幅度。轨道灰褐色常与背景铁锈色相近工业场景下色相抖动太大会让模型无法依赖颜色信息所以 hsv_h 只给 0.015饱和度抖动可以稍大明度适中。这里容易踩的坑是 close_mosaic 占总 epoch 的比例。如果总 epoch 只有 30、close_mosaic 也是 15意味着后半段都在无增强状态下训练模型容易快速过拟合。经验上 close_mosaic 占总 epoch 的 10% 到 15% 比较合理150 个 epoch 对应 close_mosaic15 就是这个比例。4.3 类别不平衡过采样优先于调 loss 权重用 2.3 节的统计结果看类别分布这里要分清“数量不平衡”和“尺度不平衡”是两件事。轨道如果被标成很多小段数量看着很多但每段都细对小目标检测器不友好手推车数量中等偏少且集中在远端属于少样本加小目标叠加。YOLOv8 没有直接的 per-class loss 权重常用做法是给少样本类别做离线过采样把包含手推车标签的图像在 train 里多复制一份或多份。具体操作只需要在 3.1 的划分脚本后面加一步扫描标签文件凡是包含类别 2 的图像 id就把它在 train_ids 里再追加一次。复制后同一张图会在一个 epoch 里出现两次mosaic 和随机翻转会带来不同的裁剪组合相当于隐式扩充了难例比例。这个做法不改动任何标签结构比调 loss 权重直观也容易回退。如果跑完过采样后小目标漏检仍然集中在置信度 0.2 到 0.4 区间再考虑调整 fl_gamma。fl_gamma 是 focal loss 的难易样本权重参数默认 1.5降到 1.0 会让模型更多关注难样本但背景误检可能增多。相对稳妥的流程先过采样默认 gamma 训练最后用验证集 bad case 判断有没有必要降每一步有对比再进下一步。4.4 用 best.pt 继续精修比从头重训更快baseline 跑完后与其从头调参再训 150 个 epoch我通常会直接拿 best.pt 作为初始化做二次训练配合已经确定好的 imgsz、close_mosaic 和学习率。这个做法本质上是在用第一次模型已经学到的特征做起点再用更大分辨率精修边界框yolo detect train \ data/home/user/datasets/freight_yolo/data.yaml \ model/home/user/datasets/freight_yolo/runs/detect/train/weights/best.pt \ epochs50 imgsz960 batch8 close_mosaic15 lr00.005model 参数指向的是上次训练的 best.pt 而不是 COCO 预训练权重。第二次训练把 lr0 降到 0.005epochs 只跑 50重点是让模型在更高分辨率上适应已有的权值分布而不是重新拟合。这种做法通常能在 mAP50-95 上看到比 baseline 明显的一截提升同时训练时间只有完整训练的三分之一左右。连续多次训练时Ultralytics 会自动创建 train2、train3 这样的递增目录不会互相覆盖。5. 验证 mAP50-95跑可视化预测收尾三个坑5.1 用一条命令复现正式验证训练收尾后第一步是用 best.pt 在 val 集上做一次正式验证而不是直接看训练日志里的最后一轮 lossyolo detect val \ data/home/user/datasets/freight_yolo/data.yaml \ model/home/user/datasets/freight_yolo/runs/detect/train/weights/best.pt \ splitval输出里会给出三个类别的 Precision、Recall、mAP50 和 mAP50-95。对于这个数据集重点关注 track 的 mAP50-95 是否远低于 mAP50。差值大代表框的位置精度差模型能框住轨道但边界抖动明显trolley 的 mAP50 如果本身偏低则要回到标注质量去查小目标漏标是常见原因标注噪声和算法问题要分开处理否则调参没有方向。5.2 可视化预测图确认漏检发生在哪类画面指标只能说明程度不能说明位置。我习惯从 val 里抽几十张图直接保存预测结果逐张看漏检发生的画面背景yolo predict \ model/home/user/datasets/freight_yolo/runs/detect/train/weights/best.pt \ source/home/user/datasets/freight_yolo/images/val \ conf0.25 saveTrue save_txtTruesource 指到 val 图像目录conf0.25 是默认置信度阈值saveTrue 保存带框图片save_txtTrue 额外输出 YOLO 格式的预测标签方便和真实标签做 diff。看到手推车漏检集中在画面远端、轨道误检集中在视觉上像钢轨的整齐反光边缘时不要急着调 conf因为漏检通常不是置信度不够而是特征没学到位。正确的动作是回到数据侧给这些 bad case 附近的区域补充标注加入训练后再复跑一次。5.3 三个可执行的收尾技巧第一个技巧确认类别 id 对应关系。names 写成 0:train、1:track、2:trolley但标签里 track 是 0整个模型类别就全乱了。跑一次混淆矩阵看哪两类在互相打架再回查标签统计脚本的输出比肉眼抽图高效。第二个技巧轨道这类超长条目标如果轴对齐框总差一点角度YOLOv8 本身不支持旋转框这时候要么接受轴对齐框的精度上限要么换用 OBB 模型作为专项方案。数据集里轨道标注如果是两点式拉框本身就表达不了角度后续升级 OBB 时要重新标注这是成本问题要在方案选型阶段想清楚。第三个技巧把 val 中预测置信度在 0.25 到 0.5 之间的样本单独导出和人工标注逐张对比。这个区间是提升 mAP50-95 性价比最高的区域定位差一点还是分类错一眼就能分清。做完这一步数据集和参数状态都可以进入下一轮迭代后续的提升就从补齐遮挡样本和扩展夜间或阴天图像开始而不是继续盲调训练参数。本文还有配套的精品资源点击获取