拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

VOC垃圾数据集YOLO训练实战:格式转换到避坑指南

VOC垃圾数据集YOLO训练实战:格式转换到避坑指南 简介这是一套面向目标检测与垃圾分类任务的Pascal VOC格式数据集包含一万四千九百六十三张真实场景图片以及一一对应的xml标注和YOLO格式txt标签对应关系完整便于按常规流程开展训练。数据标注使用labelImg工具完成共涵盖44个全英文类别并附带中英文标签对照文件可避免中文乱码问题每张图的矩形框均按统一规则绘制类别划分清楚适合YOLOv3、YOLOv4、YOLOv5或Darknet框架直接读取。资源整体共44891个文件压缩包约791MB目录内jpg、xml、txt分类型存放结构规整、类别可查用户可快速拆分训练集与验证集。目前已有1900人学习下载覆盖入门学习者、毕业设计及垃圾分类检测实践项目。数据集不承诺训练精度仅提供准确合理的标注帮助使用者省去收集、清洗和标注图片的时间专注模型调参与迭代。1. VOC 垃圾数据集 14963 张图先搞清楚它是什么再决定要不要投入做垃圾分类检测最头疼的从来不是模型调参而是数据本身。VOC 格式的垃圾数据集14963 张 JPEG 图配上等量的 XML 标注文件解压之后就能直接喂给 yolov3、yolov4、yolov5 以及 darknet 训练流程省掉了整整一个标注周期。对个人开发者和小团队来说这是从零起步最现实的一条路不用自己拉标注外包不用从公开数据集里挑挑拣拣拿到手先做一遍标注质量体检转换脚本一跑就能进入训练环节。这篇文章面向的是准备用 darknet 训练 YOLO 系列模型做垃圾识别的从业者不管你是做智能垃圾桶、分拣机械臂还是巡检应用路径是同一套。我会把拿到数据集之后的完整操作讲清楚VOC 目录结构怎么检查、XML 标注怎么转成 YOLO 的 txt、obj.data 和 obj.names 怎么配、训练时哪些参数最值得调、哪些坑最容易翻车。你按着走一遍能少踩几个我已经替你踩过的坑。2. 解压之后先体检目录结构与标注质量决定训练上限2.1 标准 VOC 目录结构JPEGImages、Annotations、ImageSets 各管什么VOC 格式的打包方式通常是 VOCdevkit/VOC2007 或 VOC2012 这种层级。拿到手先别急着转格式第一步用 tree 命令把结构看一遍。tree VOCdevkit -L 3正常情况下你会看到这样的布局VOCdevkit/ └── VOC2007/ ├── Annotations/ ├── ImageSets/ │ └── Main/ └── JPEGImages/三个核心目录各管一件事。JPEGImages 放原始图片14963 张 jpg 应该全在这里Annotations 放同名 XML 文件一张图对应一个标注ImageSets/Main 里放官方划分好的 train.txt、val.txt但很多网上的数据集打包时会漏掉这一层不影响使用——darknet 训练根本不读 ImageSets它只认你传给它的图片路径列表文件。先做数量核对防止数据集被二次压缩时丢文件。ls VOCdevkit/VOC2007/JPEGImages | wc -l ls VOCdevkit/VOC2007/Annotations | wc -l两个数字应该一致都是 14963。如果不一致说明图片和标注对不上这种数据集先做一轮文件级比对再谈训练。常见做法是把两边文件名取交集找出哪些图缺标注、哪些标注没对应图直接剔除掉别指望训练时 darknet 会自动跳过坏样本。2.2 标注体检脚本类别分布、空标注、越界框一次查清转换之前必须做体检这一步决定了后面所有工作的地基。我用一个 Python 脚本扫完全部 XML把类别分布、空标注、越界框一次列出来。import xml.etree.ElementTree as ET from pathlib import Path from collections import Counter xml_dir Path(VOCdevkit/VOC2007/Annotations) xmls sorted(xml_dir.glob(*.xml)) cls_counter Counter() # 每个类别的目标总数 empty_anns [] # 没有任何 object 的 xml out_of_bounds [] # 框超出图片边界的样本 bad_boxes [] # xmaxxmin 或 ymaxymin 的非法框 for xml_path in xmls: root ET.parse(xml_path).getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) objs root.findall(object) if not objs: empty_anns.append(xml_path.name) continue for obj in objs: cls obj.find(name).text cls_counter[cls] 1 box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) if xmin 0 or ymin 0 or xmax img_w or ymax img_h: out_of_bounds.append((xml_path.name, cls)) if xmax xmin or ymax ymin: bad_boxes.append((xml_path.name, cls)) print(f标注文件总数: {len(xmls)}) print(f类别分布:\n{cls_counter.most_common()}) print(f空标注文件: {len(empty_anns)}) print(f越界框数量: {len(out_of_bounds)}) print(f非法宽高框: {len(bad_boxes)})这段脚本的逻辑很直接逐个解析 XML取 size 节点拿到图片宽高再遍历 object 节点统计类别名和坐标。空标注文件必须引起重视YOLO 训练时读到一张完全没有目标的图反向传播会出现奇怪的梯度轻则 loss 抖动重则直接 nan。越界框在转成归一化坐标时会变成负数或大于 1 的值同样会引发训练异常。体检结果怎么看类别分布决定你是不是需要做类别平衡越界框和空标注的数量决定你要不要先写一轮清洗脚本。我一般会把 out_of_bounds 的样本量控制在全量的 1% 以内超过这个比例就说明标注质量有问题需要回到源头修 XML而不是依赖后面的转换脚本兜底。3. 把 VOC XML 转成 Darknet 认识的 YOLO txt转换脚本与训练集划分3.1 XML 和 YOLO txt 的差异同样是标注框格式完全不同VOC 格式和 YOLO 格式的核心差异在于坐标表达方式。VOC 的 bndbox 是像素级的 xmin、ymin、xmax、ymax而 darknet 训练时读的 txt 是归一化后的中心点坐标加宽高。我用一个表把差异列清楚对比项VOC XMLYOLO txt坐标形式xmin,ymin,xmax,ymax像素x_center,y_center,w,h0~1 归一化类别表示字符串比如 plastic类别表索引比如 4文件粒度一个 XML 里多个 object每行一个目标行数等于目标数存储形式XML 标签树纯文本空格分隔为什么要归一化因为 darknet 训练时会把输入图 resize 成 416×416 或 608×608归一化后的相对坐标不用跟着输入尺寸变化模型学的是一套比例关系。这也是转换脚本里所有除法操作的来源。3.2 转换脚本把 14963 个 XML 批量转成 YOLO txt这一步是整个流程里最机械也最不能出错的部分。我习惯写一个独立函数处理单文件再用循环批量跑每次跑完打印统计信息方便定位问题。import xml.etree.ElementTree as ET from pathlib import Path # 类别表顺序就是最终的 class_id必须和后面 obj.names 完全一致 CLASSES [cardboard, glass, metal, paper, plastic, trash] def voc_xml_to_yolo(xml_path, out_dir): 单个 VOC XML 转 YOLO txt 输出out_dir/同名.txt每行 class_id x_center y_center w h root ET.parse(xml_path).getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) if img_w 0 or img_h 0: print(f[跳过] {xml_path.name} 图片尺寸为 0) return False lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASSES: print(f[警告] {xml_path.name} 含未知类别 {name}跳过该目标) continue cls_id CLASSES.index(name) b obj.find(bndbox) xmin float(b.find(xmin).text) ymin float(b.find(ymin).text) xmax float(b.find(xmax).text) ymax float(b.find(ymax).text) # 像素坐标 - 归一化中心点 宽高 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 剪到 [0,1]防止越界框在训练时产生 nan x_center max(0.0, min(1.0, x_center)) y_center max(0.0, min(1.0, y_center)) w max(0.0, min(1.0, w)) h max(0.0, min(1.0, h)) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) if not lines: return False # 没有有效目标不生成 txt out_path Path(out_dir) / (Path(xml_path).stem .txt) out_path.write_text(\n.join(lines) \n) return True然后批量执行xml_dir Path(VOCdevkit/VOC2007/Annotations) labels_dir Path(labels) labels_dir.mkdir(exist_okTrue) converted skipped 0 for xml_path in sorted(xml_dir.glob(*.xml)): if voc_xml_to_yolo(xml_path, labels_dir): converted 1 else: skipped 1 print(f转换成功 {converted} 个跳过 {skipped} 个)几个关键点说透。CLASSES 列表的顺序就是 class_id这个顺序后面要原封不动写进 obj.names一旦两者对不上训练时类别就全乱了。坐标保留 6 位小数足够darknet 读的是 float精度再高也没有实际意义。clamp 操作是兜底手段体检阶段已经筛过一遍越界框这里再夹一次是为了防止漏网的样本把训练搞挂。不挑数据集的转换逻辑是这套脚本最大的价值。只要标注是 VOC XML 结构不管是垃圾检测还是电力红外设备检测firc-dataset 那种 VOC 格式的红外数据集脚本一样能跑改 CLASSES 列表就行。3.3 训练集验证集划分固定种子洗牌生成 Darknet 路径列表转换完的 txt 要和图片路径对应上。darknet 不直接读 txt 标注它通过 train.txt 和 val.txt 里的图片路径去查找同名标注文件。划分的标准做法是固定随机种子保证每次跑出来的划分结果一致。import random from pathlib import Path random.seed(42) # 固定种子复现划分结果 jpg_dir Path(VOCdevkit/VOC2007/JPEGImages) all_imgs sorted(jpg_dir.glob(*.jpg)) random.shuffle(all_imgs) val_ratio 0.1 n_val int(len(all_imgs) * val_ratio) train_imgs all_imgs[n_val:] val_imgs all_imgs[:n_val] with open(train.txt, w) as f: for p in train_imgs: f.write(str(p.resolve()) \n) with open(val.txt, w) as f: for p in val_imgs: f.write(str(p.resolve()) \n) print(ftrain: {len(train_imgs)} val: {len(val_imgs)})这里有两个细节值得注意。第一sorted 之后再 shuffle是为了避免文件系统返回顺序带来的系统偏差第二写入的是绝对路径而不是相对路径——darknet 启动时的工作目录不一定在数据集目录下相对路径会直接挂掉这是新手最容易遇到的路径暗坑。14963 张图按 9:1 划分大约是 13467 张训练、1496 张验证这个比例对垃圾检测足够用。验证集占比不用太多毕竟训练数据才是模型能力的上限来源。4. YOLOv3/v4/v5 在 Darknet 上训练的配置类别表、数据文件和关键超参数4.1 obj.names 和 obj.data训练前必须写对的两个文件转换脚本跑完接下来要写两个 darknet 必须的配置文件。obj.names 是类别名清单每一行的顺序就是 class_idobj.data 是数据文件的索引告诉 darknet 训练集、验证集、类别表各在哪里。# obj.names每行一个类别顺序和转换脚本的 CLASSES 完全一致 cardboard glass metal paper plastic trash# obj.data classes 6 train /data/garbage/train.txt valid /data/garbage/val.txt names /data/garbage/obj.names backup /data/garbage/backup/obj.names 的坑在于顺序敏感。比如 CLASSES 里 plastic 排在第 4 位转换出来的 txt 里 plastic 的 class_id 就是 4obj.names 第 4 行必须写 plastic。一旦顺序错位模型学出来的类别和真实类别完全是错位的测试时看着分类正确实际是张冠李戴。backup 目录要先建好darknet 每 1000 次迭代会把权重存进去目录不存在训练会在第一次保存时直接报错退出。train 和 valid 路径写绝对路径这个和之前划分脚本里写绝对路径的原因一样darknet 运行时不会帮你解析相对路径尤其是从其他目录启动训练进程时。4.2 改网络配置文件classes、filters、max_batches 三处必改yolov3 和 yolov4 的 cfg 文件结构一样有三个 [yolo] 检测层每层对应一个特征尺度。改配置时三个位置必须联动修改。第一处是 [yolo] 层的 classes把 80 改成你的实际类别数这里是 6。第二处是每个 [yolo] 层上一个 [convolutional] 层的 filters计算公式是 (classes 5) × 3。5 代表每个 anchor 预测的 5 个值x、y、w、h、confidence3 是每个尺度下的 anchor 数量。6 个类别算出来就是 (65)×333原始值 255 对应的是 COCO 的 80 类。第三处是 max_batches。darknet 官方建议训练自己的数据集时按类别数调整常见经验值是 max_batches classes × 2000最低不要少于 6000。6 个类别就是 12000但考虑 14963 张图片的数据量我一般会开到 20000 到 30000让模型有足够的迭代次数收敛。预训练权重也要对应版本。yolov3 用 darknet53.conv.74yolov4 用 yolov4.conv.137。这两个文件在 darknet 官方仓库都有下载后放在 darknet 根目录。有个很多人踩过的坑不要拿 yolov4 的预训练权重去跑 yolov3 的 cfg卷积层结构对不上加载时直接报错。4.3 启动训练的命令与三个必调超参数配置改完就可以启动训练了。命令如下./darknet detector train obj.data cfg/yolov3.cfg darknet53.conv.74 -map -dont_show-Map 参数让 darknet 每个 epoch 后在验证集上计算 mAP 并打印这是监控训练是否有效的关键输出。-dont_show 用于没有图形界面的服务器环境避免训练过程尝试打开窗口卡住。训练过程中最值得调的三个超参数是 batch、subdivisions 和 learning_rate它们之间的关系直接决定你的显卡能不能跑起来以及收敛速度快慢参数常见做法作用调试建议batch64每次迭代参与计算的图片数显存不够优先降 subdivisionssubdivisions16 或 32把 batch 拆成小批次前向传播调大省显存训练速度会变慢learning_rate0.001初始学习率收敛慢就小范围调别超过 0.01burn_in1000前 1000 次迭代把 lr 从 0 升到设定值默认即可帮助训练前期稳定stepsmax_batches 的 80% 和 90%学习率衰减点到点后 lr 乘 0.1加速收敛momentum0.9梯度动量一般不动decay0.0005权重衰减防过拟合一般不动subdivisions 是最实用的显存调节旋钮。batch64、subdivisions16 意味着实际每次前向传播只处理 64/164 张图累积够 64 张的梯度再做一次反向传播。显存不够就调大 subdivisions训练会慢但不会爆显存。learning_rate 的改动要克制垃圾检测这类目标差异明显的任务0.001 起步基本是安全区间盲目调到 0.01 以上容易看到 loss 飞上天的翻车现场。yolov5 的情况略有不同。v5 主流是用 PyTorch 训练而不是 darknet但它吃的标注格式同样是 YOLO txt所以第 3 章转出来的文件可以直接复用。目录结构换成 images/labels 两棵树配一个 data.yaml 就行。# garbage.yaml train: /data/garbage/images/train val: /data/garbage/images/val nc: 6 names: [cardboard, glass, metal, paper, plastic, trash]格式转换脚本能跨框架复用这是先把 VOC 转成 YOLO txt 的额外收益。v3、v4 用 darknet 跑v5 用 PyTorch 跑底层数据是同一份后面想切换框架也不需要重新标注或重新写转换逻辑。5. 垃圾检测训练避坑指南5 个让 mAP 掉点的真实案例5.1 类别名大小写和拼写不一致训练完一个类都识别不出来现象训练流程完全正常loss 正常下降mAP 也有数字但把模型拿去测真实图片时一个目标都框不出来或者只有某几个类别能识别。翻看转换日志发现大量「未知类别跳过」的警告。原因XML 里的 name 是精确字符串匹配Python 里 Glass 和 glass 是两个完全不同的值。很多数据集不是机器标注的不同批次的标注员写类别名时大小写、拼写并不统一比如 plastic 和 Plastic 混用。解决体检脚本打印 cls_counter 时肉眼检查一遍看到大小写混杂就先统一成小写转换脚本里加 name.strip().lower()并在 CLASSES 里全部用小写从源头杜绝这类问题。5.2 越界标注框导致 loss 变成 nan归一化坐标超出合法范围现象训练跑到几百次迭代后 avg loss 突然变成 nan或者一开始就 nan没有任何先兆。原因某个标注框的 xmax 大于图片宽度转换时归一化计算出大于 1 的坐标值。darknet 在计算 IoU 或置信度损失时遇到非法输入梯度传播直接崩掉。解决第 3 章脚本里的 clamp 是兜底但更可靠的做法是在体检阶段就把越界框找出来修正把 xmax 改成图片宽度。只依赖 clamp 的问题在于它会掩盖原始标注的错误框的尺寸被硬压缩物体的真实范围信息已经丢了。5.3 训练集验证集划分泄漏mAP 虚高得离谱现象训练 loss 正常收敛-map 打印的 mAP 高到 0.95 以上换到真实场景测试立刻现原形精度掉到没法用的程度。原因划分训练集验证集时没有先洗牌就按顺序切分同一场景的连续帧图片同时进了 train.txt 和 val.txt。垃圾检测数据集经常包含同一堆垃圾从不同角度的多张连拍这种样本几乎重复验证集等于在考练习题答案测试结果当然虚高。解决划分脚本里先 sorted 再 shuffle固定种子跑完后把两个 txt 文件做一次交集检查确认没有任何路径重复。5.4 类别极端不平衡小类别 AP 一直是 0现象训练结束后打印 per-class AP瓶子、纸板这些大类能到 0.8 以上烟头、电池这类目标少的类别 AP 是 0。原因数据集中瓶子有几千个目标烟头只有几十个模型在 anchor 匹配阶段就把大部分计算资源分配给了高频类别低频类别很难被正样本激活。解决先按目标数而不是图片数看类别分布再做针对性处理。常见做法是对稀有小目标做复制粘贴增强把烟头这类目标随机贴到大图上或者调低大类的类别权重给小类更高的损失贡献。如果数据集本身允许重新标注补数据补小类的收益远大于调参。5.5 loss 卡在 1 到 2 不动不是训练失败了是你在看总 loss现象loss 从 8 降到 1.5 之后就不再下降每 100 次迭代打印的 avg loss 几乎是一条直线误以为模型没有在学了。原因darknet 打印的 avg loss 是坐标损失、置信度损失、类别损失三项合起来的总和对于检测任务loss 到 1 到 2 的平台期是正常现象不代表模型没有进步。14963 张图在 416 输入下大约两三万次迭代后进入平台期后面 mAP 可能还在缓慢爬升。解决不要只看 loss要看 -map 输出的 mAP 曲线。平台期后如果 mAP 还在涨就继续训练真正的判断标准是验证集上的 mAP 和 per-class AP而不是单看 avg loss 数字。6. 验收模型别只盯着 mAP单图测试、混淆矩阵和部署前检查训练结束拿到 final.weights 后先做一轮单图测试确认最基本的检测能力没问题。./darknet detector test obj.data cfg/yolov3.cfg backup/yolov3_final.weights test.jpg -thresh 0.25带一张验证集之外的图片跑一下看框的位置对不对、置信度分数分布是否合理。如果一张图上所有框的置信度都贴着 0.25 阈值说明模型很犹豫部署时要把阈值上调到 0.4 或 0.5如果置信度普遍在 0.8 以上阈值可以放松一些减少漏检。单图没问题之后用 -map 重新统计 per-class AP重点看小类别的表现。这一步会暴露训练阶段没注意的问题比如金属和玻璃互相混淆严重说明这两个类别的外观特征在训练数据里区分度不够需要补充更难负样本。再看混淆矩阵垃圾检测里塑料瓶和玻璃瓶形状相近混淆是常见事关键看混淆比例是否在可接受范围内。部署前我有个固定动作在项目目录预留一个 test_imgs 文件夹塞 20 到 30 张完全不在数据集里的实拍图覆盖不同光照、不同距离、不同垃圾堆叠程度。每版模型训练完先过一遍这批图再谈上线。这个习惯帮我避免了至少两次翻车——有一次模型在数据集上 mAP 有 0.87但实拍图里漏掉了 40% 的垃圾袋原因是数据集的拍摄场景太干净和真实环境差距太大。最后再用视频或者摄像头验证实时推理的稳定性。darknet 自带 demo 模式可以跑视频流观察连续帧之间框的抖动程度和漏检频率。./darknet detector demo obj.data cfg/yolov3.cfg backup/yolov3_final.weights video.mp4 -thresh 0.25实时场景下的检测稳定性和单帧精度同样重要连续几帧里同一个瓶子一会儿检出一会儿消失这种模型上线会被现场直接打回。我会优先保证 mAP 不掉太多的情况下让置信度更稳定实际收益往往比硬追那一个点的 mAP 大得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表