简介:面向生活垃圾分类智能识别任务的标注数据集,依据23年智能分类装置竞赛设定,覆盖有害垃圾、可回收垃圾、厨余垃圾与其他垃圾四类,样本包括电池、过期药品内包装、易拉罐、小号矿泉水瓶、小土豆、切块萝卜、瓷片与鹅卵石等赛题常见形态。压缩包内共2000个文件,核心为1998个XML标注文件,逐一记录目标类别与边界框位置;另含yolov5_train.txt和yolov5_val.txt两份清单,用于划分训练集与验证集,方便直接接入YOLOv5等目标检测流程。整包大小约411.59MB,目前已有1626人学习下载。使用该数据集可省去自行采集与标注的重复劳动,快速搭建四类垃圾检测基线,并可围绕赛题场景进行数据增强、类别均衡与模型调优,适用于竞赛复现、课程设计或小型智能分类装置原型验证,帮助开发者聚焦模型结构与部署优化。
1. 四类垃圾3000张数据集:规模不大,但足够你把检测流程完整跑通
拿到一份“四类垃圾、共3000张左右、已打完标签”的数据集,第一反应往往是兴奋,第二反应才是冷静。3000张图放在目标检测任务里属于典型的“小规模数据”:用来学习流程、验证方案、训练出一个看得过去的原型完全够用,但想直接部署到真实垃圾桶或清运车上,大概率会翻车。这个规模的数据集最有价值的定位,是让你用最小成本把“数据清洗 → 格式转换 → 数据划分 → 模型训练 → 指标评估 → 错误分析”整条链路走通,再带着这套经验去扩数据。本文就围绕这份数据集,把每一步怎么做、参数怎么设、坑在哪里讲透。适合正在做毕设、课堂项目或垃圾分类demo的开发者,也适合刚接触检测数据集、想搞明白标注和数据格式关系的从业者。
2. 先把标签体系吃透:四类垃圾数据集的“说明书”在哪里
数据集到手,第一步不是训练,而是搞清楚标签到底怎么存的。很多初学者拿到3000张图就急着开训,结果类别名对不上、框的格式读错,损失曲线一路飘,浪费两三天才发现是数据读取出问题。“打完标签”这个表述只能说明标注工作完成了,至于标注是用什么格式存储的、类别是怎么编号的、有没有漏标和错标,都需要先验证。这份数据集涉及四类垃圾,常见的分类口径是厨余垃圾、可回收物、有害垃圾、其他垃圾,但具体用的是中文名还是英文ID,框坐标是归一化还是像素值,XML里有没有包含难以处理的嵌套节点,都是要先摸清的底细。
2.1 四类垃圾的类别定义与标注规范:先定边界再谈训练
类别定义是整个数据集的灵魂。我一般会先做一件事:把数据集的类别清单读出来,统计每类的样本数和实例数。四类垃圾看起来边界清晰,实际上标注时很容易出现歧义:一个沾了油污的塑料瓶算可回收物还是其他垃圾?一块用过的餐巾纸是其他垃圾还是厨余垃圾?如果标注人员没有严格按同一份规范执行,模型学到的类别边界就是模糊的,推理时自然容易混淆。
操作上,先扫描所有标注文件,统计类别分布。假设标注是VOC XML格式,可以用下面的脚本快速摸底:
# inspect_labels.py import xml.etree.ElementTree as ET import glob from collections import Counter xml_files = glob.glob("labels/*.xml") cls_counter = Counter() inst_counter = 0 for xml_file in xml_files: tree = ET.parse(xml_file) root = tree.getroot() for obj in root.iter("object"): name = obj.find("name").text cls_counter[name] += 1 inst_counter += 1 print("类别统计:", dict(cls_counter)) print("总标注框数:", inst_counter) print("图像总数:", len(xml_files))这段脚本遍历所有XML标注文件,读取每个object节点里的类别名,统计每个类出现的次数以及总标注框数。运行之后,重点关注两件事:类别名的写法是否统一(比如“可回收”和“可回收物”同时出现就是大问题);各类别的实例数是否悬殊。四类各750张左右是理想状态,如果某一类只有200个框,而另一类有1800个框,训练时就要专门处理类别不平衡。此外还要留意有没有空的XML文件——没有object节点的文件对训练毫无贡献,还会干扰数据划分。
2.2 标注存储格式:VOC XML、COCO JSON、YOLO TXT怎么选
四类垃圾这个任务本身不复杂,但标注格式决定了你后面要接哪个训练框架。最常见的三种格式各有优劣。
| 格式 | 存储方式 | 坐标单位 | 优点 | 缺点 |
|---|---|---|---|---|
| VOC XML | 每个图像一个XML文件 | 像素绝对坐标 | 人眼可读、方便检查 | 文件多、解析慢 |
| COCO JSON | 整个数据集一个JSON文件 | 像素绝对坐标 | 生态完善、信息全 | 结构复杂、易写错 |
| YOLO TXT | 每个图像一个TXT文件 | 归一化相对坐标 | 训练读取快、框架直读 | 坐标是浮点数、不方便人检查 |
我拿到数据会先确认格式,再决定要不要转换。如果是VOC或COCO,而训练脚本希望读YOLO格式,就得转换。转换的核心逻辑是把像素坐标的xmin、ymin、xmax、ymax换算成归一化的中心点坐标和宽高:
# voc_to_yolo.py import xml.etree.ElementTree as ET import os class_map = {"厨余垃圾": 0, "可回收物": 1, "有害垃圾": 2, "其他垃圾": 3} def convert(xml_path, out_dir, img_w, img_h): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter("object"): name = obj.find("name").text cls_id = class_map.get(name) if cls_id is None: continue bndbox = obj.find("bndbox") xmin = float(bndbox.find("xmin").text) ymin = float(bndbox.find("ymin").text) xmax = float(bndbox.find("xmax").text) ymax = float(bndbox.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 lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") out_path = os.path.join(out_dir, os.path.basename(xml_path).replace(".xml", ".txt")) with open(out_path, "w") as f: f.write("\n".join(lines))这段脚本里有两个关键参数值得注意。第一个是class_map,它决定了类别名到数字ID的映射,训练时必须保证这个映射和模型输出层的顺序一致,否则模型输出的类别会整体错位,这是比较容易踩的坑。第二个是img_w和img_h,转换YOLO格式需要真实的图像宽高,如果图像是矩形而非正方形,宽高就不能用一个固定值。最稳妥的做法是从XML里的<size>节点读取原始宽高,或者用cv2.imread读取图像尺寸,避免从文件名或文件夹名猜测。
2.3 用一段脚本把标注可视化:抽检版本该看什么
格式转换完之后,建议花半小时把标注画到图上,肉眼检查一遍。这一步不能省,因为统计脚本只能发现问题,看不出质量。尤其是四类垃圾这种物体形状不规整的数据集,标注框是否贴合物体边缘、有没有把两个挨着的物体框在一个框里,直接决定模型学到的位置精度。
# visualize_boxes.py import cv2 import os label_dir = "labels" image_dir = "images" vis_dir = "visualized" os.makedirs(vis_dir, exist_ok=True) class_colors = {0: (0, 255, 0), 1: (0, 0, 255), 2: (255, 0, 0), 3: (0, 255, 255)} for txt_name in os.listdir(label_dir): if not txt_name.endswith(".txt"): continue img_path = os.path.join(image_dir, txt_name.replace(".txt", ".jpg")) img = cv2.imread(img_path) if img is None: print(f"图像不存在: {img_path}") continue h, w = img.shape[:2] with open(os.path.join(label_dir, txt_name)) as f: for line in f: parts = line.strip().split() cls_id = int(parts[0]) x_c, y_c, bw, bh = map(float, parts[1:]) x1 = int((x_c - bw / 2) * w) y1 = int((y_c - bh / 2) * h) x2 = int((x_c + bw / 2) * w) y2 = int((y_c + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), class_colors.get(cls_id, (255, 255, 255)), 2) cv2.putText(img, str(cls_id), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, class_colors.get(cls_id, (255, 255, 255)), 2) cv2.imwrite(os.path.join(vis_dir, txt_name.replace(".txt", ".jpg")), img) print("可视化完成,结果输出到", vis_dir)这段脚本读取YOLO格式的TXT标注,把归一化坐标换算回像素坐标,用OpenCV在原图上画框,并把类别ID标在框上方。抽检时我会按类分别抽20张左右,重点关注三类问题:框明显大于物体(把背景包进去了);框小于物体(只框住了物体中间一段);两个类别挨得很近的物体有没有框串位置。这些问题如果不能在训练前发现,训练完再回头补标注,成本就高多了。
3. 数据划分与预处理:3000张怎么分才不会自欺欺人
数据集规模一大,划分方式的影响会被稀释;但只有3000张图时,划分策略稍有不慎就会让评估结果失真。常见做法是按训练集、验证集、测试集7:2:1划分,但这只是在数量上达标。真正的关键点有三个:划分前要不要洗牌、划分时能不能保证四个类在每一份里比例接近、同一物体的多张相似图不能同时出现在训练集和测试集。
3.1 训练集/验证集/测试集的划分比例与脚本实现
3000张图按7:2:1划分,得到训练集约2100张、验证集约600张、测试集约300张。这个规模训练检测模型是可行的,但验证集和测试集的600与300都已经偏小,单张图对指标的影响比较大。所以划分脚本里必须加固定随机种子,保证每次复现结果一致,否则你调了几轮参数,回头对比时发现指标波动是数据划分不同造成的,就说不清了。
# split_dataset.py import os import random from shutil import copy2 random.seed(42) image_dir = "images" label_dir = "labels" split_ratio = (0.7, 0.2, 0.1) all_images = [f for f in os.listdir(image_dir) if f.endswith(".jpg")] all_images.sort() random.shuffle(all_images) n_train = int(len(all_images) * split_ratio[0]) n_val = int(len(all_images) * split_ratio[1]) splits = { "train": all_images[:n_train], "val": all_images[n_train:n_train + n_val], "test": all_images[n_train + n_val:] } for split_name, file_list in splits.items(): os.makedirs(os.path.join("dataset", split_name, "images"), exist_ok=True) os.makedirs(os.path.join("dataset", split_name, "labels"), exist_ok=True) for img_name in file_list: img_src = os.path.join(image_dir, img_name) lbl_src = os.path.join(label_dir, img_name.replace(".jpg", ".txt")) if not os.path.exists(lbl_src): continue copy2(img_src, os.path.join("dataset", split_name, "images", img_name)) copy2(lbl_src, os.path.join("dataset", split_name, "labels", img_name.replace(".jpg", ".txt")))脚本里有两个地方要特别注意。random.seed(42)这行如果不写,每次跑脚本划分结果都不同,后面做任何对比实验都会失去基准。lbl_src对应的TXT文件如果不存在就跳过,防止把漏标注的图混进训练集。划分完之后,我建议再跑一遍类别统计脚本,确认四个类在训练集和验证集里的比例接近总体比例。如果某类在验证集里只有十几个实例,评估出来的AP值波动会非常大,这种情况下要把该类部分训练图挪到验证集,或者改用分层划分。
3.2 图像尺寸、颜色空间与增强策略:小数据集更不能乱来
四类垃圾数据集的图像尺寸通常不统一,有些是手机拍的竖图,有些是从监控视频里截的横图。直接塞进模型训练会出问题,因为大多数检测框架会先把输入缩放到固定尺寸,比如640×640。缩放过程中,细长物体或被压缩变形的小物体很容易丢失特征。我的做法是先把所有图像统一调整到一个基准尺寸,再用padding补成正方形,避免直接拉伸变形。
增强策略对3000张的数据集很关键,但前提是增强操作不能破坏垃圾本身的语义。颜色抖动、亮度调整、随机裁剪、水平翻转都是安全操作;但随机旋转90度要谨慎——如果数据集中大多数垃圾桶都是正着拍的,旋转后会引入大量不真实的角度分布。更值得投入的是mosaic增强,把四张图拼成一张训练,相当于用3000张图生成了远超3000的组合样本,对小数据集提升明显。
# 用Python脚本统一尺寸并做基础增强 python -c " import cv2 import numpy as np import os src_dir = 'dataset/train/images' dst_dir = 'dataset/train/images_640' os.makedirs(dst_dir, exist_ok=True) for img_name in os.listdir(src_dir): img = cv2.imread(os.path.join(src_dir, img_name)) h, w = img.shape[:2] scale = 640 / max(h, w) new_h, new_w = int(h * scale), int(w * scale) resized = cv2.resize(img, (new_w, new_h)) canvas = np.zeros((640, 640, 3), dtype=np.uint8) canvas[:new_h, :new_w] = resized cv2.imwrite(os.path.join(dst_dir, img_name), canvas) "这段命令把训练集图像统一resize到640×640的画布上,短边不够的部分用黑色padding补齐。这里有一个容易被忽视的点:图像做了resize和padding之后,标注坐标也必须同步转换。YOLO格式的坐标是归一化的,理论上不依赖图像尺寸,但这个结论的前提是图像整体等比缩放。一旦引入了padding,左上角不再是(0,0)对应的像素位置,坐标就需要按实际显示区域重新计算。很多人在这一环节翻车,训练时loss降不下去,就是因为图变了但标注没跟着变。
4. 训练四类垃圾检测模型:选型、参数与损失曲线判断
数据集准备好之后,就到了核心的训练环节。四类垃圾检测的任务难度并不高,物体类别少、外形区分度大,所以不需要一上来就上重模型。先选一个训练速度和部署性价比都比较平衡的检测模型,把流程跑通,再根据验证集表现决定是否升级。
4.1 模型选型:为什么轻量检测模型比重模型更适合这份数据
3000张的规模决定了模型不能太“贪吃”。参数量很大的模型在这个量级的数据上很容易过拟合,训练集损失降到很低,验证集mAP却上不去。相比之下,轻量检测模型更稳。我比较常用的是YOLO系列中的轻量版本,输入640×640,主体网络用CSP结构,推理速度快,训练时对显存要求也不高,一般6GB以上的显卡就能跑。四类垃圾的检测框通常比较大且居中,这个任务对模型特征提取能力的要求并不苛刻,轻量模型完全够用。
选型时另一个重要原则是预训练权重。无论选哪个模型,都应该加载在大型数据集上预训练过的权重,而不是从零开始训练。预训练权重让模型已经具备基本的纹理、边缘、颜色特征提取能力,在这个基础上微调四类垃圾,3000张数据才能发挥出效果。从零训练的话,3000张图大概率连最基础的特征都学不扎实。
4.2 关键超参数:学习率、batch size、epochs怎么设定
训练超参数的设置直接决定收敛效果。我基于类似规模数据集总结了一套起始配置,可以先用它跑第一轮,再根据损失曲线微调。
| 参数 | 建议值 | 说明 |
|---|---|---|
| 输入尺寸 | 640×640 | 通用基线,四类垃圾无需更高分辨率 |
| batch size | 16 | 显存有限可降到8,但梯度噪声会变大 |
| 初始学习率 | 0.01 | batch较大时可适当提高 |
| 动量 | 0.937 | 保持默认即可 |
| 权重衰减 | 0.0005 | 防止过拟合的关键 |
| epochs | 100~150 | 3000张数据100轮基本够,多了会过拟合 |
| 类别数 | 4 | 对应四类垃圾 |
学习率是最需要盯的参数。初始学习率0.01对batch size 16是合理起点,但训练过程中要用余弦退火或按epoch衰减。如果训练到第20轮loss还在高位震荡,大概率是学习率偏大,可以砍到0.005再试;如果loss降得特别慢,则说明学习率偏小。另外,前3~5个epoch建议用warmup,让学习率从很小的值慢慢升到目标值,否则前期梯度方向不稳定,容易把预训练权重破坏掉。
4.3 训练过程监控:损失不降先查数据,而不是改网络
训练跑起来之后,要同时看两个指标:训练损失和验证集mAP。我见过太多人在loss不降时急着换模型结构、调网络层数,结果折腾一番毫无改善。实际上,对于3000张的数据集,loss不降的第一嫌疑永远是数据问题,而不是模型问题。
# 训练启动命令(以YOLO轻量版为例) yolo detect train data=dataset.yaml model=yolov8s.pt epochs=100 batch=16 imgsz=640 lr0=0.01 patience=20参数里的dataset.yaml是数据集配置文件,里面写清楚训练集和验证集的路径、类别数和类别名列表。model=yolov8s.pt指的是加载预训练权重,这里需要注意版本号要对上,否则会报结构不匹配。patience=20表示验证集指标连续20轮不提升就提前停止,这个参数对小数据集很有用,能在过拟合到来之前自动终止训练。
训练过程中我一般每隔10轮看一次验证集的PR曲线。如果precision和recall严重失衡,比如precision高但recall很低,说明模型只认出了它最有把握的那部分目标,漏检严重,这时候优先检查是不是某类训练样本太少;如果loss曲线是快速下降后突然反弹,说明学习率在后期偏大或数据增强过强,缩小学习率即可。训练完成后,模型会输出最终的权重文件,但这是整个流程的中间节点而不是终点——还要做测试集评估。
5. 3000张垃圾数据集的四类拦路虎:踩坑记录与排查方法
这部分挑四个我在类似数据集上反复遇到的真实问题,按“现象、原因、解决”的结构记录。这些问题单独看都不严重,但叠在一起会把最终指标拉低一大截。
5.1 类别分布严重失衡:模型只认得厨余垃圾
现象:训练结束后看每个类别的AP,厨余垃圾AP超过90%,可回收物只有40%,有害垃圾甚至不到30%。整体mAP看起来还行,但细看之下模型基本是个“偏科生”。
原因:这类问题十有八九是标注阶段留下的坑。四类垃圾在真实场景里的出现频率本身就不一样,厨余垃圾出现概率远高于有害垃圾,标注人员按真实分布采集数据时,四个类的框数量差距可能达到5倍以上。模型在训练时见过成千上万个厨余垃圾框,但有害垃圾框只有零星几个,自然学不好。
解决:先跑类别统计脚本,确认每个类的实例数。如果是5倍以上的差距,就按实例数对大类别做欠采样或对小类别做过采样。更稳妥的是用图像级重采样:小类别的图像复制两份或三份进训练集。如果统计下来发现某类只有100多张图,单纯复制可能引发过拟合,此时可以用crop增强专门针对该类物体裁切局部区域生成新样本。
5.2 标注框偏移:预测框总是往左上角偏
现象:模型训练完,可视化预测结果时发现,预测框和物体边缘总是差那么一小截,方向还比较统一,比如整体偏左上。
原因:这个现象指向标注环节的系统性误差,而不是模型问题。可能某个标注人员习惯性地把框往左上收紧,或者标注工具的光标偏移导致了固定偏差。由于训练时模型一直在学这种偏差,预测时也跟着学歪了。
解决:用第2章的可视化脚本,把训练集标注框画出来逐类检查。如果确认偏差方向一致,直接用脚本把某一侧的坐标批量修正,比如所有框统一向右下扩5个像素,再重新训练。如果偏差方向不一致,那就需要把有问题的图像挑出来重标。这个坑在数据量更大时往往会被淹没,但在3000张的规模里值得专门处理一轮。
5.3 重复和近似图像混入不同数据集:验证集mAP虚高
现象:训练过程正常,验证集mAP达到0.85,但把模型部署到新环境拍的照片上,表现明显变差,感觉之前的高指标是假的。
原因:3000张图像如果来源于同一个视频流或同一批连拍,相邻帧之间会存在大量高度相似的画面。划分数据时如果随机打乱,同一物体的两帧相似图很可能一张进了训练集、另一张进了验证集。验证时模型其实是在“记忆测试”,而不是“泛化测试”。
解决:划分之前先做去重,按图像的感知哈希计算相似度,把相似度超过阈值的图像归为一组,整组进同一份数据划分。更简单的方法是用文件名或采集时间分组——同一时间段的连续帧归入同一个集合,再按集合划分。测试集尤其要保证来源干净,最好是用不同时间、不同场景采集的图像单独组成一个外部测试集。
5.4 背景单一导致泛化失败:换了个环境就翻车
现象:训练集里垃圾全部出现在深色桌面上或灰色传送带上,模型表现很好;一到户外草地或室内地板上,检测精度骤降,部分类别直接漏检。
原因:模型把背景特征和物体特征一起学了。当背景高度单一时,模型不需要区分“垃圾本身的纹理”和“桌面颜色”,只要看到深色背景上有东西就能给高分。这种隐式的背景依赖在测试集同样来自相同背景时不会被发现,只有换环境才会暴露。
解决:训练时加大背景增强的随机性,随机改变图像的饱和度、色相、亮度,甚至对背景区域做模糊处理,让模型更依赖前景物体本身。同时适当加入一些背景替换增强手段,比如把训练集中的物体贴到随机背景图上。这也是为什么四类垃圾这种任务要特别注意数据采集的多样性——3000张图如果都在同一个背景里拍的,模型再调参也弥补不了背景偏置。
6. 从mAP到真实场景:验证结果时最值得做的两件事
模型训练完,常规做法是看整体的mAP,看每个类别的AP,然后结束。但实际上,对3000张数据训练出的模型,mAP只是一个粗略参考,真正验证它能不能用,需要做两件事:第一,按来源分组评估,把测试集按图像采集场景拆成若干小组,逐组看指标差异;第二,做错误分析,把测试集里所有预测框按“正确”“漏检”“误检”“类别混淆”分类统计,找出错误集中发生在哪里。
# error_analysis.py # 伪代码:比较预测结果和真实标注 tp = 0 fp = 0 fn = 0 confusion = {} for img_id in test_images: preds = model.predict(img_id) truths = load_gt(img_id) for pred in preds: matched = match_iou(pred, truths, iou_thresh=0.5) if matched and matched.cls == pred.cls: tp += 1 elif matched: key = f"真实{matched.cls}->预测{pred.cls}" confusion[key] = confusion.get(key, 0) + 1 else: fp += 1 for truth in truths: if not any(iou(pred, truth) > 0.5 for pred in preds): fn += 1 print(f"TP={tp} FP={fp} FN={fn}") print("类别混淆明细:", confusion)这个脚本的核心逻辑是把模型预测的每个框和真实标注框做IoU匹配。IoU大于0.5且类别一致算正确,大于0.5但类别不符记入混淆矩阵,没有匹配到任何真实框算误检,真实框没有被任何预测框匹配上算漏检。输出里最值得看的是混淆矩阵的明细——如果“可回收物→其他垃圾”这一类混淆特别多,说明模型没有学透这两个类的区分特征,需要回看训练集里这两个类的样本边界是否清晰,必要时补充标注规范。
我的习惯是,模型部署之前永远留一批“一次性测试集”,只在最终评估时用一次,绝不用它反复调参。3000张数据集本来就小,测试集只有300张左右,你用它在调参过程中看了十几次,它对模型的参考价值就被消耗得差不多了。把最后一次评估留到模型定版之后再跑,得出的指标才真正代表模型的泛化能力。这一步是我做过几个检测项目之后才养成的习惯,早期常常图方便,用同一个测试集反复验证,最后部署就吃了亏。希望这个习惯能帮你少走一段弯路,也希望这份3000张的四类垃圾数据集在你的流程里发挥出它应有的价值。
本文还有配套的精品资源,点击获取