
简介面向YOLOv3/v4/v5及Darknet框架训练需求这份VOC格式垃圾分类数据集提供了完整的图片、标注与标签体系。数据集中包含14963张垃圾图片每张均对应同名xml标注文件和yolo格式txt文件合计44类全英文标签并额外提供中英文标签对照表无需担心中文乱码。标注由labelImg工具绘制矩形框完成符合Pascal VOC规范类别文件单独存放便于训练前读取和筛选44个英文类别名称。整个压缩包共44891个文件包括14963个jpg、14963个xml和14965个txt资源包约791.41MB其中图片、xml与txt三者一一对应目录层级分明可方便地接入常见目标检测训练流程。目前已有1900人浏览学习说明该数据在垃圾分类项目中有较高的参考价值。对于需要快速搭建垃圾识别原型的学生或开发者而言这套数据免去了自行采集与标注的繁琐过程可直接作为Darknet/YOLO训练的数据基础也可用于迁移学习、模型调优和毕业设计扩展实际模型精度需结合训练配置进一步验证。1. 垃圾检测VOC-14963张数据集为什么值得拿来喂darknet垃圾分类识别是视觉落地里一个典型的“数据比模型更卡脖子”的场景市面开源数据散、标注格式乱改格式的时间比调模型还长。这个VOC-14963张数据集说白了就是把14963张带Pascal VOC标注的垃圾图集中打包图片和xml配好对能直接给yolov3、yolov4、yolov5乃至整个darknet系训练管线用。它的价值不只在“有图”而在省掉自己写抓取脚本、清洗错标、统一格式这一串脏活。适合刚想跑通垃圾检测demo的新手也适合要快速验证算法基线、评估垃圾识别能否落地的工程团队。前提是你愿意花半天时间把数据格式和训练配置一次做对。2. 读懂VOC垃圾数据集目录结构、类别体系与样本质量2.1 三个目录一个清单JPEGImages / Annotations / ImageSetsPascal VOC被当作基准格式不是因为它最花哨而是因为它的组织方式“一眼能看懂”。拿到这份垃圾数据集后你打开根目录先看到的就是三个固定目录JPEGImages存放全部原始jpg图片Annotations存放与图片一一对应的xml标注文件ImageSets/Main则放着按用途划分的图片清单txt比如train.txt和val.txt。xml文件名和图片文件名保持同名这个约定是整个格式的地基。darknet训练并不直接消费VOC的xml它要的是yolo格式的txt。但先别急着抱怨“多了一道转换”VOC这套结构真正的价值在于清单与图像分离训练集、验证集怎么切只改ImageSets里的txt就行不用复制一份图片。对于14963张这种量级的垃圾数据每张图按1MB算也有14GB以上的原始空间能在清单层面切分就避免了磁盘翻倍。我拿到数据集后习惯先把ImageSets/Main打开看一眼内容。如果里面是类似“000001\n000002”这种不带后缀的编号行说明它是标准的VOC风格清单后续切分脚本要自己拼jpg后缀如果自带.jpg后缀则要注意转换时别拼重。很多人在这个细节上翻车清单里已经写了后缀脚本里又补了一个.jpg结果文件列表全部指向不存在的路径训练时直接报0 images loaded。2.2 14963张图什么概念单类样本量和分布比总量更关键14963张听起来不少但落地时真正决定模型上限的不是总张数而是“最少的那个类别有多少张”。垃圾检测一般会涉及塑料瓶、纸箱、易拉罐、玻璃瓶、果皮、电池、织物等几大类我接触过的VOC格式垃圾集大多会把类别控制在5到12个之间。所以你从标题看到“VOC-14963张”时真正该问的是这14963张里纸箱占多少电池占多少有没有哪一类只有一两百张那这一类注定难学到能用的特征。这里必须提一个容易被忽视的陷阱VOC标注里的类别名大小写和拼写通常不统一有的xml里写“Bottle”有的写“bottle”一旦混用转换后类别索引会错位训练时模型会把这个类当另一个类学。因此拿到数据后的第一个动作不是转格式而是先从全部xml里把类别清单提出来去重、归并。我习惯的做法是先跑一个几分钟的扫描把所有name标签的值打成一个集合人工确认最终类别数再写映射表。以下是一份垃圾检测常见的类别映射参考实际以你手里数据集的xml为准类别索引xml原始name常见写法建议统一写法0Paper, paper, waste paperpaper1Plastic, plastic bottle, bottleplastic2Glass, glass bottle, bottleglass3Metal, can, aluminum canmetal4Cardboard, cartoncardboard5Food waste, organicorganic6Fabric, textilefabric7Battery, cellbattery8Other, mixed wasteother写这个表格时我要提示像“bottle”同时出现在plastic和glass下时说明这个数据集的标注哲学是按“材质”而非“形状”分类映射表要坚决地把同名标签归并到同一个类否则两个类共享一个名称darknet里类别索引会乱后面一切评估都建立在错误的标签之上。2.3 先跑一个探针脚本把数据集家底摸清转格式之前我建议先写一个统计脚本把三类信息打印出来xml与图片的数量、类别分布、每个类别对应的图片数。这个脚本几十行花不了几分钟但它能把上面的几个风险一次性暴露。import os import xml.etree.ElementTree as ET from collections import Counter data_root /path/to/VOC_dataset # 改成你的数据集根目录 annotations os.path.join(data_root, Annotations) images os.path.join(data_root, JPEGImages) xml_names set(os.path.splitext(f)[0] for f in os.listdir(annotations) if f.endswith(.xml)) img_names set(os.path.splitext(f)[0] for f in os.listdir(images) if f.lower().endswith((.jpg, .jpeg, .png))) print(fxml数: {len(xml_names)}, 图片数: {len(img_names)}) print(f有xml但无图: {len(xml_names - img_names)} 个) print(f有图但无xml: {len(img_names - xml_names)} 个) cls_counter Counter() img_with_cls set() for f in os.listdir(annotations): if not f.endswith(.xml): continue tree ET.parse(os.path.join(annotations, f)) for obj in tree.getroot().findall(object): name obj.findtext(name).strip() cls_counter[name] 1 img_with_cls.add((os.path.splitext(f)[0], name)) print(\n类别分布(top 20):) for cls, cnt in cls_counter.most_common(20): print(f{cls}: {cnt} 个目标, 涉及 {sum(1 for _, c in img_with_cls if c cls)} 张图)逻辑说明脚本前半部分统计xml和jpg的编号是否对得上后半部分遍历每个xml用ElementTree标准库提取object里的name标签用Counter统计类别名出现次数。img_with_cls这个集合我额外做了归一统计因为同一个类别可能在一张图里出现多个目标只报目标数会高估这个类对训练样本的贡献一张图里密集出现的塑料瓶并不比分散在不同图上的纸箱提供更多“场景多样性”。“有xml但无图”和“有图但无xml”两个数字是数据完整性的第一道底线只要任意一个不为0后面的训练必然丢样本。参数说明data_root按自己解压后的实际路径改xml文件名不匹配jpg的情况在这个数据集里极少但脚本留着以后往里面加入自己的标注时还能继续当质检工具用。我把这段脚本命名为voc_probe.py每次拿到新数据集都先跑一遍。3. 把VOC标签转成YOLO训练格式转换脚本与参数设置3.1 为什么darknet训练前必须“转格式”darknet的检测模型不读xml它约定的标签格式是每张图片对应一个同名txttxt里每一行占一个目标五个数值加一个类别索引分别是class x_center y_center width height其中坐标全部做归一化除以图片宽高后落在0到1之间。这套设计的出发点与VOC完全不同VOC把坐标存在xml里适合人类阅读和编辑darknet的txt则被设计成加载器可以无脑读的定长浮点数组解析快也方便在内存里批量预取。“为什么要归一化”是新手最爱问的问题。我一般这么解释不归一化的话同一张图被resize成416x416训练而标签还停留在原始分辨率网络输出层的感受野覆盖范围会对不上归一化之后无论原图是1万像素长边还是480像素长边标签都收缩到一个0到1的相对坐标系里训练时只需按输入尺寸乘回去坐标天然对齐。转换时最容易被忽略的是width和height的归一化不能拿xml里的sizewidth直接算而要以JPEG图片实际解码出来的尺寸为准。VOC的xml里确实有width和height字段但有些数据集在标注后做过压缩或重采样xml的尺寸和实际图片不一致一旦以xml为准框的位置就会按比例偏移小目标直接漂出边界。我转换脚本里统一用PIL打开图片读尺寸虽然慢一点但不会踩这个洞。3.2 一次性转完的转换脚本VOC xml转YOLO txt下面这段脚本是转换核心我拆成两步解析xml得到目标框再和图片实际尺寸做归一化并写txt。import os import xml.etree.ElementTree as ET from PIL import Image voc_root /path/to/VOC_dataset cls_map {paper: 0, plastic: 1, glass: 2, metal: 3, cardboard: 4, organic: 5, fabric: 6, battery: 7, other: 8} # 按实际扫描结果自行调整缺失类别先补 cls_map 再转换 def convert_one(xml_path, img_path, out_dir): tree ET.parse(xml_path) root tree.getroot() with Image.open(img_path) as im: w_img, h_img im.size lines [] for obj in root.iter(object): name obj.findtext(name).strip() if name not in cls_map: continue 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) # 裁剪到整图范围内防止越界框带偏训练 xmin max(0, min(xmin, w_img - 1)) xmax max(0, min(xmax, w_img - 1)) ymin max(0, min(ymin, h_img - 1)) ymax max(0, min(ymax, h_img - 1)) if xmax - xmin 0 or ymax - ymin 0: continue bw (xmax - xmin) / w_img bh (ymax - ymin) / h_img cx (xmin xmax) / 2 / w_img cy (ymin ymax) / 2 / h_img lines.append(f{cls_map[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) if lines: out_name os.path.splitext(os.path.basename(xml_path))[0] .txt with open(os.path.join(out_dir, out_name), w) as f: f.write(\n.join(lines) \n) xml_dir os.path.join(voc_root, Annotations) img_dir os.path.join(voc_root, JPEGImages) label_dir os.path.join(voc_root, labels) os.makedirs(label_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue stem os.path.splitext(xml_file)[0] img_path os.path.join(img_dir, stem .jpg) if not os.path.exists(img_path): print(f跳过缺失图片: {stem}) continue convert_one(os.path.join(xml_dir, xml_file), img_path, label_dir) print(转换完成, labels 目录: , label_dir)逻辑说明cls_map是类别映射表它的键必须和xml里的name完全一致。解析xml时我用了root.iter(object)而不仅是findall因为有些标注工具会在object外面再包一层iter能容错抓出所有层级的对象标签。每个bndbox里读出的四个坐标我先统一裁剪到[0, w_img - 1]范围内再做归一化这一步是数据洁净度的兜底标注框里哪怕只越过边界一个像素归一化后也会出现超过1.0的宽高值darknet加载时不会报错但loss会在那个batch上剧烈震荡。裁剪后如果宽或高被压成0说明这个框本来就是脏框直接跳过比强行保留合理。参数说明w_img和h_img来自PIL实际解码结果不信任xml的size字段原因见3.1。坐标的归一化顺序是先算宽高再算中心点方向不要搞反。所有数值用.6f固定6位小数darknet对小数位数并不敏感但保留足够精度可以避免小目标比如远处的易拉罐在归一化后因四舍五入偏移太多。3.3 按9:1切分训练集和验证集生成darknet的train.txt / valid.txtlabels转好之后还要生成两个清单文件train.txt和valid.txt每一行是一个jpg的绝对路径darknet就用这两个文件决定哪些样本参与训练、哪些样本参与验证。import os import random voc_root /path/to/VOC_dataset img_dir os.path.join(voc_root, JPEGImages) images [os.path.join(img_dir, f) for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .jpeg, .png))] random.seed(42) random.shuffle(images) split_idx int(len(images) * 0.9) train_imgs images[:split_idx] valid_imgs images[split_idx:] with open(os.path.join(voc_root, train.txt), w) as f: f.write(\n.join(train_imgs) \n) with open(os.path.join(voc_root, valid.txt), w) as f: f.write(\n.join(valid_imgs) \n) print(f训练集 {len(train_imgs)} 张, 验证集 {len(valid_imgs)} 张)逻辑说明切分在图片层面做不在一张图的目标层面做。把同一张图同时放进训练集和验证集会让验证集的指标虚高因为模型相当于“见过”了这些目标反过来同一张图的两个区域各分一边同样会污染评估。所以切分前一定先保证图片没有重复文件名。random.seed(42)是复现的关键换一个seed得到不同划分报告mAP时要和生产环境保持一致。参数说明9:1是darknet训练目标检测的常见默认比例14963张图按9:1切验证集约1500张足够稳定地评估mAP。如果你的类别特别不均衡建议在切分时用分层抽样保证每个类在训练集和验证集里的占比一致而不是全局shuffle否则稀疏类别有可能被全部塞进训练集或者全部塞进验证集后者会导致验证mAP剧烈抖动。分层抽样脚本也不复杂按类别把图片分桶后在每个桶内做9:1切分再合并即可。3.4 obj.names与obj.data两份配置文件的写法清单文件准备好之后去darknet的cfg目录下建两个文件。obj.names的内容极简每行一个类名顺序和3.2里cls_map的索引保持一致是铁律。obj.data则更像一份索引配置字段含义示例classes类别总数必须与cfg中filters匹配9train训练图片清单绝对路径/data/VOC/train.txtvalid验证图片清单绝对路径/data/VOC/valid.txtnames类别名文件路径/data/VOC/obj.namesbackup权重保存目录/data/VOC/backup写完这两个文件数据侧的工作才算闭环。注意obj.data里classes和obj.names行数不一致时darknet可能不报错却静默错位比如names只有8行但classes9训练时类别索引为8的标签会被当成无效类别丢弃mAP就是莫名其妙少一类。每次修改数据集后都要把classes、names行数、cfg的filters三者一起核对。4. darknet训练YOLOv3/v4/v5cfg修改与超参数调优4.1 cfg里三个数字改不对训练一切白搭classes / filters / max_batches拿到yolov3.cfg或yolov4.cfg、darknet下的yolov5 cfg之后不是直接开训就完事。第一步是把文件里所有[convolutional]层中filters值为255的地方统一改掉用公式计算filters 3 * (classes 5)。这里的3是每个网格预测3个anchor5是x、y、w、h、confidence这5个输出class数量就是你的obj.data里classes的值。以9类垃圾为例filters 3 * (9 5) 42原来的255是按COCO的80类算的不改的话网络输出的通道数和标签格式不匹配训练大概率loss下降异常。然后是max_batchesdarknet的官方建议是classes * 2000且不低于6000。9类垃圾就按18000步来设但这不是死数我会根据训练曲线微调如果avg loss在12000步时已经降到0.1以下且验证mAP开始平台期就可以提前截断如果一直降不下来加到24000也不丢人。关键是steps要和max_batches配套通常设为max_batches的80%和90%两个节点学习率在那里各衰减一次到0.1倍。batch和subdivisions这对参数直接影响显存。batch64意味着每个iteration用64张图subdivisions16则把这64张拆成16个小块依次前向、最后统一反传。显存不够时优先调subdivisions而不是batch如果8GB显存跑不动先保持batch64把subdivisions从8调到16或32通常就能挤进去。很多人一上来把batch降成16结果BN层的统计量变差训练出来的模型泛化能力明显下降这是血泪经验。4.2 用k-means重聚类anchor别拿COCO的anchor硬套垃圾检测的目标尺寸和COCO完全不同一个远景里的矿泉水瓶可能只有几十像素而近景的纸箱能占满半张图。yolov3默认anchor是COCO数据集上聚出来的直接硬套会让每个网格的预测框初始尺度和真实目标差距太大网络要额外花大量iteration去修正。常见做法是写个k-means脚本对训练集中所有标注框的宽高做聚类。注意距离度量不能用欧氏距离因为大框和小框的像素差天然不在一个量级用IoU距离更合理公式是d 1 - IoU。import numpy as np def wh_iou(wh, clusters): # wh: [N, 2], clusters: [k, 2] w np.minimum(wh[:, 0:1], clusters[:, 0]) h np.minimum(wh[:, 1:2], clusters[:, 1]) inter w * h area_wh wh[:, 0:1] * wh[:, 1:2] area_cl clusters[:, 0] * clusters[:, 1] return inter / (area_wh area_cl - inter) def kmeans(wh, k, max_iter50): n wh.shape[0] idx np.random.choice(n, k, replaceFalse) clusters wh[idx] for _ in range(max_iter): iou wh_iou(wh, clusters) assign iou.argmax(axis1) for j in range(k): if np.sum(assign j) 0: continue clusters[j] np.median(wh[assign j], axis0) return clusters逻辑说明wh_iou里用np.minimum把每个真实框和每个cluster的宽高取交集面积计算则是高维广播最终得到一个N行k列的IoU矩阵。k-means迭代时我选的是每个簇的中位数而不是均值因为标注框里偶尔有几个异常大的长条形均值会被拉偏中位数稳健得多。anchor数量k一般选9对应yolov3每个尺度3个anchor如果目标尺寸分布特别集中k选6也够用反而能减少回归压力。参数说明聚类前要把所有归一化宽高乘回输入尺寸比如416或者直接对归一化后的值聚类然后再乘回像素尺寸两种都行但不要混用。算出9个anchor后按面积从小到大排序依次填到cfg里三个yolo层对应的anchors行。我每次聚完都会在终端打印每个anchor被分配的真实框数量如果一个anchor几乎没人匹配说明k选大了或数据里确实没有这个尺度的目标删掉它、把k减1再重跑。4.3 启动训练命令行参数与avg loss怎么读数据、配置、anchor都就绪后按下面的命令启动训练。用yolov3训练时预训练权重下载darknet53.conv.74yolov4对应yolov4.conv.137yolov5在darknet下的权重文件名称略有不同但用法一致。./darknet detector train /data/VOC/obj.data /data/VOC/yolov3.cfg ./darknet53.conv.74 -map # 常用附加参数: # -dont_show 不弹训练窗口适合服务器后台跑 # -map 每个epoch后跑验证集mAP记录到chart # -gpus 0,1 多卡训练时指定GPU编号训练日志里最值得盯的指标是avg loss和每个Region层打印的avg IoU。avg loss每100个iteration打印一次它下降的速度比绝对数值更有意义。垃圾检测这种单类目标不太多的数据loss在2000步内从几十降到个位数是正常节奏。真正要警惕的不是loss高而是loss在某个数值附近震荡完全不再下降这通常提示学习率过大或anchor尺寸与目标分布不匹配。avg IoU则直观反映“预测框和真实框的重合度”一般稳定在0.6以上算合格。如果IoU一直在0.4以下徘徊第一步先检查是不是用小尺寸输入如320训练而目标又多是几十像素的小垃圾建议把训练尺寸调到608或保持416但对小目标做数据增强。4.4 yolov3到v4/v5同一份VOC数据怎么切换同一个VOC数据集换模型除了cfg不同数据侧的东西几乎不用动。yolov4在darknet下的训练流程和v3高度相似区别主要是cfg里加入了CSPDarknet结构、Mish激活和SPP模块需要的显存略高。yolov5在darknet也有原生cfg实现训练命令完全一致但如果你用的是ultralytics的pytorch版本那属于另一条技术栈数据格式仍然可以直接复用上面生成的labels和train.txt只需把路径写入yaml文件即可。我在实际项目里通常先在yolov3上跑通基线确认数据干净后再切v4或v5追求精度。不要在数据集还没检查干净时就直接上最重的模型那样一旦出问题你会分不清是数据问题还是模型问题这几乎是黑匣子里最容易翻车的情况。5. 训练垃圾检测数据集常见问题与排查从翻车现场定位根因5.1 现象loss一路不降甚至变成nan原因训练前几轮loss上下乱跳并快速冲到nan最常见的是标签数据里有脏框宽高为0、坐标出现负值或归一化后的中心点超出[0,1]范围。darknet加载这些标签时不校验合法性反传梯度时就会计算出无穷大。解决用第3章的探针脚本重新扫描所有labels目录下的txt过滤掉任何一行中出现非数字、坐标小于0或大于1、宽高小于等于0的记录。我习惯在转换脚本里直接加校验宁可丢弃脏框也不要带病训练。5.2 现象avg loss下降正常但验证集mAP始终为0原因这类问题的根因往往不在训练而在于类别索引错位。比如obj.data里classes9names文件却只有8行或者转换脚本里cls_map的索引和names顺序不一致模型输出层认为的第5类在数据集里实际上是第4类验证时全部错判。解决写一个几行的小脚本随机挑几张验证图片把标签里的类别索引映射回names之后和xml里原始name打印对照。凡是对不上的就回去改cls_map然后重跑转换重训。5.3 现象训练中途CUDA out of memory原因目标检测训练显存峰值出现在反传时刻而不是前向。batch64、输入608时activation会占满显存。很多人把subdivisions丢在8就把锅甩给显卡其实还可以牺牲一点速度换空间。解决先保持batch不变把subdivisions从8改到16、再改到32。如果还是OOM再把输入尺寸从608降到416。这两个招都用完还不行才考虑降batch且降batch后要观察BN表现必要时增加iteration数补偿。5.4 现象训练日志显示0 images loaded模型一直在空跑原因obj.data里的train或valid指向的txt内容为空或者路径写错。还有一个隐蔽情况txt里每行图片路径带空格、中文或特殊字符darknet的文件读取器截断路径后找不到文件。解决在终端用wc -l /data/VOC/train.txt先确认行数是否等于14963的九成。再用head -3 /data/VOC/train.txt检查路径格式。不要用中文路径darknet对非ASCII路径支持并不好老老实实放到纯英文目录。5.5 现象验证集mAP比训练集差十几个点泛化明显不足原因垃圾数据集里同类目标的背景高度重复比如塑料瓶样本大量来自同一批视频帧数据之间存在近重复帧另一个原因是每类样本数不均衡稀疏类只贡献了少量梯度。解决用第2章探针脚本统计类别-图片数分布对低于平均数的类做横向数据补充或者引入更强数据增强。darknet里flip1、mosaic1、mixup1这些参数在cfg的[net]段都能开不用改代码。如果是近重复帧造成的过拟合用视频抽帧方式补数据时记得按场景聚类去重而不是简单跳帧。6. 验证模型好坏的一个硬核技巧用mAP和PR曲线反推数据短板有了训练好的weights下一步是评估。darknet训练时加-map参数已经每个epoch都跑了验证集最终在backup目录里会同时存下训练曲线chart。但很多团队只看mAPmAP达到80%就觉得模型可以上线其实这个数字救不了你分辨“哪种垃圾容易漏检”。我实际的做法是拿出验证集预测结果画PR曲线重点是找到曲线上precision和recall的平衡点。如果模型在低recall时precision就开始跳水说明很多垃圾目标被漏检这时候补样本比调anchor更能救命如果precision一直上不去、大量误检则需要补充负样本或把置信度阈值调高。具体验证命令./darknet detector map /data/VOC/obj.data /data/VOC/yolov3.cfg backup/yolov3_final.weights -points 11 ./darknet detector recall /data/VOC/obj.data /data/VOC/yolov3.cfg backup/yolov3_final.weights-points 11是计算11点插值mAP的标准方式这样得到的AP才是可比较的。recall命令会额外打印出每类目标的recall我习惯把recall最低的三类挑出来回到数据集里看它们的标注质量是目标太小、被遮挡、还是和背景颜色太接近。这个方法比瞎调超参数高效得多。我最早跑垃圾检测数据集时也犯过蠢看到mAP 0.87就准备交差结果部署到现场瓶盖和瓶身混在一起全部漏检才发现验证集里这类样本本来就没几张。后来养成的习惯是每次训练结束都先跑一次detector recall按类看短板再看总mAP。这个习惯帮我保住了不少交付项目的效果。这个方案的真正价值在于14963张VOC数据是一次性可复用的资产格式统一后yolov3、v4、v5乃至pytorch版本都能无缝复用同一套labels和清单。数据侧的脏活一次做干净模型侧的迭代就是纯增量。希望帮到你。本文还有配套的精品资源点击获取