简介:这份资源是专门面向生活垃圾智能分类赛项与目标检测训练整理的标注数据集,包含约三千张四类实拍生活垃圾图像,覆盖有害垃圾(电池、过期药品)、可回收垃圾(易拉罐、小号矿泉水瓶)、厨余垃圾(小土豆、切块白萝卜、胡萝卜)以及其他垃圾(瓷片、鹅卵石、砖块),可用于23年赛题初决赛阶段的模型训练与调参。压缩包内共2000个文件,其中以1998个XML标注文件和2个TXT路径划分文件为核心,XML记录每张图片中目标的类别与边界框,两个TXT分别对应YOLOv5训练集和验证集列表,整体包体约411.59MB。该数据集贴近真实赛题的尺寸与重量约束,并预留多目标同时识别的场景,适合准备算法验证、作品演示或论文实验。目前已有1626人学习下载,能够帮助开发者免去繁琐的人工标定流程,快速获得一份规范、可直接投入训练的数据底稿。
1. 四类垃圾 3000 张数据集:别急着训练,先想清楚这份数据能做什么
手里拿着一份「垃圾分类打完标签的数据集,四类垃圾共计 3000 张左右」,大多数人的第一反应是丢进训练脚本里跑一版。我的建议刚好相反:先花一个晚上做体检,再决定怎么用。因为“打完标签”只代表有人碰过这些图片,不代表标注格式统一、类别名规范、坐标没出界、训练能收敛。我第一次拿到类似数据时跳过体检直接训练,mAP 一塌糊涂,后来才发现光是类别别名混乱就分了五种写法,换名重训才勉强救回来。这篇笔记会从一线的角度,把这套四类垃圾数据集的检查思路、格式转换、训练验证和踩坑记录一次讲完,适合正在评估这份数据能不能用、以及想用它快速跑通模型的人。
2. 拿到数据集先体检:目录结构、类别分布与标注格式识别
2.1 先看目录,判断标注是分类还是检测格式
拿到压缩包不要急着解压训练,先解压到一个没有空格和中文的路径下,然后列目录。这一眼能决定后面所有处理走哪条路。我见过不少所谓“打完标签”的垃圾分类数据集,实际分两种形态:一种是整图分类,文件夹名就是类别;另一种是物体检测,图片旁边有同名 xml 或 txt。两种形态的数据处理方式完全不同,搞混了后面会白做一晚上。
# 进入数据根目录,只列一层目录,避免刷屏 cd ~/dataset/garbage4 && ls -la # 如果 images 和 labels 是分开的,说明是检测格式 # 如果看到 4 个类别文件夹,说明是整图分类格式常见的目录结构大概长这样:检测格式下图片在 images 里,标注在 labels 里,xml、json、txt 都有可能;分类格式则按「厨余 / 可回收 / 有害 / 其他」建文件夹,每类下面堆着几百张图。判断依据很简单:看到同名标注文件就是检测,只看到类别目录就是分类。这一步决定了后续是写转换脚本还是直接读图训练,所以我会先列目录,再找一张图打开看标注内部长什么样。看清形态再动手,是这行省时间的核心习惯。
2.2 一个脚本看清类别分布和图片尺寸
不管哪种格式,下一步都是统计类别分布和图片尺寸。四类垃圾共 3000 张,理论上每类 750 张,但实际分布往往参差不齐。有的类别占了 1200 张,有的类别只有 400 张,这种不均衡会直接拉低少数类别的召回率。图片尺寸也很重要,如果主要图片是 1080p,训练时下采样到 640 没问题;如果混着一批 320p 的缩略图,模型会越发越糊涂。
import os from collections import Counter, defaultdict from PIL import Image root = "/Users/me/dataset/garbage4/images" exts = {".jpg", ".jpeg", ".png", ".bmp"} size_counter = Counter() count_per_class = defaultdict(int) for fname in os.listdir(root): if os.path.splitext(fname)[1].lower() not in exts: continue # 假设检测标注是同名 txt,第一列是类别索引 label_path = os.path.join(root.replace("images", "labels"), os.path.splitext(fname)[0] + ".txt") if os.path.exists(label_path): for line in open(label_path, "r"): cls = int(line.strip().split()[0]) count_per_class[cls] += 1 # 图片尺寸直接读头信息,不需要完整解码 with Image.open(os.path.join(root, fname)) as im: size_counter[(im.width, im.height)] += 1 print("类别分布:", dict(count_per_class)) print("尺寸分布:", size_counter.most_common(10))这段脚本会输出两个最关键的统计:每类标注框出现的次数,以及图片尺寸的分布。注意这里统计的是标注框数量而不是图片数量,因为一张图里可能有两个瓶子,按图片数会低估类别占比。如果发现某个类别的标注数明显偏低,训练时要考虑给它加权重,或者用复制粘贴增强补样本。尺寸分布则决定了训练时的输入分辨率,如果 3000 张里混着大量不足 500px 的小图,imgsz 设成 640 就会造成大量上采样,模型学到的纹理全是马赛克。先跑完这两个统计,再决定增强策略和训练参数,心里才有底。
2.3 标注质量的三项快速检查
统计完数量,接下来要抽查标注本身的质量。四类垃圾数据集的标注质量普遍不稳定,我踩过的比较典型的问题包括:txt 里混入空行导致解析失败、坐标归一化后超出 0~1 范围、类别索引写错导致某个类实际一个样本都没有。这些坑不会报错,只会让训练 loss 看起来很收敛但验证集 recall 上不去。所以我会固定跑一套三项检查:标注文件能否全部解析、坐标是否越界、是否有图片找不到对应标注。
import os img_root = "/path/images" lab_root = "/path/labels" errors = [] for fname in os.listdir(lab_root): if not fname.endswith(".txt"): continue img_name = os.path.splitext(fname)[0] img_path = os.path.join(img_root, img_name + ".jpg") # 也可能是 .png if not os.path.exists(img_path): errors.append(f"{fname}: 图片缺失") continue for lineno, line in enumerate(open(os.path.join(lab_root, fname), "r")): parts = line.strip().split() if len(parts) != 5: errors.append(f"{fname} 第{lineno + 1}行: 列数不是5") continue cls_id, cx, cy, w, h = map(float, parts) if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 <= w <= 1 and 0 <= h <= 1): errors.append(f"{fname} 第{lineno + 1}行: 坐标越界") if cls_id < 0 or cls_id > 3: # 四类垃圾,索引0~3 errors.append(f"{fname} 第{lineno + 1}行: 类别索引超出范围") print("发现问题:", len(errors)) for e in errors[:20]: print(e)这套检查脚本应该在你准备转换格式前跑一遍。坐标越界和列数不对这两类问题最常见的来源是有人在 Excel 里手改过标注文件,另存时把 NaN 写进了文本;类别索引超出范围则多半是合并多个来源的标注时漏了类别映射。发现任何一条,记录下文件名清单,后面统一修。需要注意这份数据集的标注格式可能是 xml 或 json,检查脚本只要把解析部分替换一下即可,统计和判断逻辑完全通用。
3. 把数据集整理成训练可用的格式:YOLO 转换脚本与四个边界坑
3.1 统一类别名:先做映射,再做转换
四类垃圾数据集往往不是一个人标注的,自然会出现类别名不统一的局面。我见过最典型的例子:同一种“厨余垃圾”,在标注文件里同时出现 leftover、kitchen_waste、china_food 三种写法;把循环播放的视频抽帧后,不同批次的命名习惯又不一样。如果你拿到的数据集是 xml 格式,里面name字段千奇百怪,第一步不是写坐标转换,而是把类别名映射到统一的 0~3 索引。
# 定义别名到标准类别的映射 alias_map = { "recycle": 0, "recyclable": 0, "kehuishou": 0, "kitchen": 1, "kitchen_waste": 1, "chuyu": 1, "harmful": 2, "hazardous": 2, "youhai": 2, "other": 3, "residual": 3, "qita": 3, } def normalize_cls(name: str) -> int: key = name.strip().lower() if key not in alias_map: raise ValueError(f"未识别的类别名: {name}") return alias_map[key]映射之后,我建议立刻对全量标注跑一遍,输出所有没匹配上的类别名。这一步能帮你发现那些藏得比较深的拼写错误,比如 harmfu l、kithen 这种。写映射字典时保留英文别名的同时也要保留中文拼音,因为不少标注工具默认输出拼音。只有类别名全部能落进 0~3,后面转 YOLO 格式才不会出现类别索引错位。
3.2 VOC/通用格式转 YOLO txt 的脚本
常见做法是把 xml 标注转成 YOLO 格式的 txt,因为绝大多数检测框架原生吃这份格式。转换的核心只有一件事:把绝对值坐标xmin, ymin, xmax, ymax归一化到 0~1,然后换算成中心点和宽高。具体公式不复杂,但注意力要放在防止除零和宽高为负这两个边界问题上。
import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_dir, alias_map): tree = ET.parse(xml_path) root_xml = tree.getroot() img_w = int(root_xml.find("size/width").text) img_h = int(root_xml.find("size/height").text) lines = [] for obj in root_xml.iter("object"): name = obj.find("name").text cls_id = normalize_cls(name) 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, xmin); ymin = max(0, ymin) xmax = min(img_w, xmax); ymax = min(img_h, ymax) if xmax <= xmin or ymax <= ymin: continue # 无效框直接丢弃 cx = (xmin + xmax) / 2 / img_w cy = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") 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))这段脚本把每张图的 xml 读取、坐标归一化、类别映射全串起来了。注意我把坐标先裁剪到图像边界再参与计算,这是防止后续在增强时出现越界框的关键一步。另外丢弃 xmax <= xmin 或 ymax <= ymin 的框,这类数据多半是标注时误操作产生的竖线或横线,留着只会让模型学会预测无意义的细长框。转换完成后,用第一节的检查脚本重新跑一遍输出,确保每行都是 5 列且坐标在 0~1 之间。
3.3 切片与数据集划分:顺序打散,避免同场景连续帧进入同一个验证集
转换完成后还有一道重要手续:划分训练集和验证集。很多人直接取前 2400 张做训练、后 600 张做验证,这在垃圾分类数据上是典型的错误姿势。因为许多数据集是从视频抽帧得到的,前几百张可能全是同一个垃圾桶里的同一个苹果核;如果它们同时落进验证集,模型相当于提前见过了答案。
import os import random import shutil random.seed(42) labels = sorted([f for f in os.listdir("labels") if f.endswith(".txt")]) random.shuffle(labels) train_ratio = 0.8 split_idx = int(len(labels) * train_ratio) train_files, val_files = labels[:split_idx], labels[split_idx:] for split, files in [("train", train_files), ("val", val_files)]: os.makedirs(f"images/{split}", exist_ok=True) os.makedirs(f"labels/{split}", exist_ok=True) for lab in files: img = lab.replace(".txt", ".jpg") shutil.copy(f"images/{img}", f"images/{split}/{img}") shutil.copy(f"labels/{lab}", f"labels/{split}/{lab}")随机种子设成固定值能保证每次划分结果一致,便于复现实验。更重要的是,如果确认数据里有连续帧,稳妥做法是先对场景做去重再划分,但通常我们拿不到拍摄时间信息,那么随机洗牌加上短路径已经能规避大部分泄漏问题。这 3000 张四类垃圾里,80% 进训练、20% 进验证,基本能支撑后续判断。
3.4 四个边界坑
第一坑,路径带空格或中文。有些标注工具导出时保留了原始文件名,比如“厨余垃圾 001.jpg”,到了 OpenCV 和部分训练框架里直接读不进来,报错还是隐性的,表现为某张图一直不参与训练但没任何 warning。解决方式是先统一重命名,全部换成garbage_0001.jpg这种纯 ASCII 短名。第二坑,类别索引写错但脚本不报错,比如把可回收和厨余的索引对调,训练时 loss 照常下降,推理时才暴露。解决方式是在转换脚本里打印每个类别的样本数,和原始统计对账。第三坑,一张图对应多个同名标注文件,比如001.jpg的标签有001.txt和001.xml同时存在,框架只会按优先级读其中一个,造成实际参与训练的标注量少于你看到的数量。解决方式是在划分前删掉旧的 xml。第四坑是空标注文件,有些标注员对“没有垃圾的图”也生成了空 txt,这会让模型学到“看到这张图就不要输出任何框”,验证集的 recall 被瞬间拉低。解决方式是把空文件对应的图片直接移出数据集,或者单独建一个负样本集。
4. 用 3000 张四类数据先跑一个小模型:训练命令与参数怎么调
4.1 数据划分与 class 文件
确认标注格式正确、数据已切分之后,下一步是让模型先跑起来。无论你最终打算用分类网络还是检测网络,都要先写一份数据描述文件。以检测为例,用当前主流的检测框架跑时,需要提供一份 yaml 文件,里面包含训练集、验证集路径和类别名列表。这里有个最容易踩的细节:yaml 里的类别顺序必须和你转换脚本里的类别索引完全一致,第三行写了厨余、第四行写了有害,那么索引 1 就是厨余、索引 2 就是有害,一旦写反,模型训练时不会报错,但推理结果会张冠李戴。
4.2 一次能验证数据质量的最小训练流程
数据文件准备好后,先做一个最小训练,目的是验证数据链路,而不是刷精度。这个训练集只有 3000 张左右,四类垃圾,用小模型、小分辨率、短 epoch 先跑通。我一般会选轻量级网络,batch 视显存而定,先确保能在一个小时内跑完。
# 训练四类垃圾检测模型,imgsz=640 是通用值,batch 按显存调整 yolo train data=garbage4.yaml model=yolov8n.pt epochs=50 imgsz=640 batch=16 device=0这条命令在普通单卡环境就能跑。imgsz 设 640 是因为检测任务中垃圾框通常只占画面的 5%~30%,分辨率太低会丢掉小目标信息;如果你手里的图片本身只有 480p,那就改成 512 而不是 640。epochs 设 50 是刻意的,这个规模的数据集不需要 300 个 epoch,跑到 50 轮已经能看出数据有没有问题,一次性跑太久只会在错误数据上浪费算力。batch 设 16 是 8GB 显存左右的安全值,显存不够就往下调,但别低于 8。
4.3 参数解释与期望值
| 参数 | 建议值 | 说明 |
|---|---|---|
| imgsz | 640 或原图缩放至 640 | 分辨率过高会显著加长训练时长,过低则小目标容易丢 |
| batch | 8~32 | 显存 8G 建议 16,12G 以上可以 32,再大会让 BN 不稳定 |
| epochs | 50~120 | 先跑 50 轮验证数据,模型稳定后再加 |
| workers | 4~8 | 3000 张图吞吐量不大,workers 太高反而加大 IO 争抢 |
| optimizer | auto 或 SGD | 小数据集用 SGD 收敛更稳,AdamW 容易早停不动 |
这几项里最关键的是 imgsz 和 epochs 的配合。很多新手把 epochs 拉到 300,但 3000 张图的数据在小模型上 80 轮左右就会过拟合,用验证 loss 早停即可。优化器选 auto 省心,但如果发现训练集 loss 降得很快、验证集纹丝不动,换成 SGD 会有改善。
4.4 训练结果怎么看:判断“能用的数据”还是“白折腾的数据”
跑完 50 轮,不要只看 mAP 这一个数字。我会先看三个指标:训练集 loss 是否连续下降、验证集 mAP50 是否稳步爬升、类别维度上有没有某个类明显拉胯。四类垃圾数据,如果 mAP50 在 0.5 附近说明标注质量不错,如果只有 0.2 左右,不要急着调模型,先用测试图片可视化看几个预测结果。我习惯把验证集里置信度最高的 20 张和置信度最低的 20 张打印出来对比,观察模型对哪个类别的预测框歪了。多数情况是某个类别的样本不够或标注框画得太随意,这时回到数据和标注去修,比加模型复杂度有效得多。记住:在小规模数据集上,模型只是测量标注质量的工具,训练阶段玄学最少的部分是看曲线趋势,而不是盯着一张 loss 图瞎猜。
5. 人工排查 5 类常见标注坑:从现象到原因再到解决
5.1 坑一:训练正常但某个类别 recall 始终为 0
现象:四类垃圾里其他三类 mAP 都还行,只有“有害垃圾”的 recall 一直是零。曲线从头到尾没抬过头。原因:我遇到过两次,第一次是类别映射漏了该类的别名,导致所有该类别的标注在转换时被归到“其他”,模型从头到尾没见过这个类的正样本;第二次是转换脚本里该类别的索引和 yaml 文件里排列顺序错位。排查时先打印每个类别的训练样本数,如果“有害垃圾”显示 0,就回去查映射字典和读取逻辑,而不是调模型。解决:修复映射关系,重新转换,再训练一版短的看曲线是否抬升。
5.2 坑二:训练 loss 出现周期性尖峰
现象:loss 曲线整体下降,但每隔几十个 step 会突兀地跳出一个尖峰,随后又恢复正常。原因:这通常是训练集里混入了损坏图片或超大分辨率图片,某个 batch 加载失败导致 loss 计算异常。我遇到的具体场景是数据里有一批 6000×4000 的航拍大图,被缩略后放进训练集,加载时内存占用突增,训练进程卡顿后 loss 激增。解决:检查数据里的极端尺寸和损坏文件,把超过某个阈值的超清图统一降采样到 1280 以内,然后用imghdr或 PIL 扫描一遍所有图片是否能正常解码。
5.3 坑三:验证集 loss 下降但 mAP 忽高忽低
现象:验证集 mAP50 在 0.3 到 0.6 之间剧烈波动,每个 epoch 变化很大。原因:验证集太小,且验证集里混着大量相似图片。3000 张数据切出 20% 只有 600 张,如果再有三五个相似场景各占几十张,评估结果就会异常不平稳。解决:先确认切分时有没有打散,如果确认已打散但波动仍在,就把验证集占比从 20% 降到 15%,同时保证每类都有足够样本,或者改用 K 折交叉验证观察稳定性。
5.4 坑四:推理结果框到了背景上
现象:训练完成,模型在测试图上稳定地把垃圾桶边缘或桌子角识别成“可回收垃圾”。原因:标注时框的范围太随意,有些框把垃圾之外的背景也包进去了,模型学到的是背景纹理而不是垃圾本体的特征,尤其是塑料瓶和纸箱这类带明显边缘的物品更容易触发这种误检。解决:对验证集中置信度在 0.3~0.5 之间的预测框做可视化,看看模型喜欢在哪些区域给错答案。如果是背景误检,回到标注文件把框收紧,而不是单纯调低置信度阈值。
5.5 坑五:数据增强后训练时间翻倍但精度不涨
现象:加了 mosaic、旋转、色彩抖动之后,训练时间明显变长,mAP 几乎没变,甚至轻微下降。原因:3000 张的小数据集本身多样性不足,增强如果过于激进,会让模型学到扭曲后的伪特征;另一个常见问题是训练图分辨率太低,旋转后大量边缘区域被填充黑边,模型被迫学这些黑边。解决:在分辨率本来就有限的数据上,优先使用亮度扰动和轻微缩放,mosaic 可以开但把拼图比例调低,旋转角度限制在 ±15 度以内。增强的作用是补足样本分布,不是替代人工数据补充。
6. 让这 3000 张数据发挥更大价值:置信度清洗与二次标注的实用技巧
6.1 用置信度做半自动清洗
数据量只有 3000 张时,与其花大量时间逐张翻图,不如让模型先帮你筛一遍“可疑标注”。思路很简单:用一个已经训练好的模型对训练集做一次推理,输出每张图中置信度最低的框。置信度低有两种可能,一是模型没学到位,二是标注本身有问题。无论哪种,都值得优先人工复审。
from ultralytics import YOLO model = YOLO("best.pt") results = model.predict(source="images/train", conf=0.1, save_txt=True, save_conf=True) # 找出平均置信度最低的 200 张图,优先人工检查 import os from collections import defaultdict conf_map = defaultdict(list) for r in results: img_name = os.path.basename(r.path) if len(r.boxes) == 0: conf_map[img_name] = [0.0] continue conf_map[img_name] = r.boxes.conf.tolist() ranked = sorted(conf_map.items(), key=lambda kv: sum(kv[1]) / len(kv[1])) for img, confs in ranked[:200]: print(f"{img}: {confs}")这段脚本把低置信度图片按平均分排序输出。人工复审时只看前 200 张,而不是 3000 张,时间成本从一整天压缩到一两个小时。要注意的是,置信度设 0.1 是为了让模型尽量把可疑目标都框出来,宁可多召回也别漏检。复审时重点看两类:框明显错位的,和类别判断与肉眼不符的。
6.2 少量人工复审:把“回报率”最高的图挑出来
二次标注不必追求全量重标,那样跟重新做一遍数据集没区别。我会按类别做分层抽样,四类各挑 50 张低置信度图片,共 200 张,由一个人用标注工具过一遍。修正后再把这 200 张重新丢进训练集,模型通常会有肉眼可见的提升。这个效果的来源是:错标和漏标对梯度的伤害远大于正常标注对模型的贡献,纠偏 200 张的收益可能比增加 500 张重复样本更大。做完之后保留清洗清单,记录哪些图被改了什么,方便后面排查问题。我一直保持的习惯是:任何数据集改动都会留一个训练前后对比表,改动类别名、修坐标、删无效图都记下来,这样模型一旦变差,我能从前一天的改动里快速定位,而不是把时间花在反复试参上。这份四类垃圾 3000 张的数据集,把格式、分布、边界坑和清洗流程都走一遍之后,后续的训练就能稳定复现,希望帮到你。
本文还有配套的精品资源,点击获取