YOLOv8垃圾检测数据集实战:从标注清洗到训练调参指南

发布时间:2026/10/1 18:22:53
YOLOv8垃圾检测数据集实战:从标注清洗到训练调参指南
简介面向目标检测与垃圾分类应用开发者这是一份可用于YOLO系列模型训练的数据资源覆盖纸箱、纸张、塑料、铝、玻璃、硬纸板、瓶子、塑料瓶等常见可回收物类别。压缩包共2000个xml标注文件约348MBxml格式可保存目标框坐标与类别信息便于脚本批量解析为VOC或YOLO文本标签直接接入现有训练流程。目前已有346人学习下载适合需要扩充数据集、训练垃圾检测模型、验证算法效果的计算机视觉学习者与项目工程师。资源内标注文件命名规整按样本编号有序排列便于脚本批量读取、格式转换与训练集划分。对智慧环保、环卫自动分拣、回收分类等场景的开发者来说这份数据能显著降低手动标注成本帮助快速搭建垃圾识别原型并为进一步调优模型精度提供基础整体体量适中适合中小型项目入门与算法对比实验。1. 垃圾检测数据集13707张带标签图像到底解决了什么问题做垃圾检测的工程师应该都有体会YOLO算法本身早就不稀奇真正卡住项目进度的是没有可用的标注数据。这套压缩包里的13707张带标签图像覆盖纸箱、纸张、塑料、铝、玻璃、硬纸板、瓶子、塑料瓶八个常见类目解决的就是从“想训练一个垃圾分类模型”到“有一批能直接喂给YOLO的数据”之间的那段路。它适合两类人一是准备用yolov8训练自己的数据集、但手里只有零散照片没有标签的开发者二是在回收站、工厂或者市政场景做试点需要先验证这套类目方案精度是否达标的工程师。我的建议是拿到压缩包后先别急着解压训练花半小时把标签质量、类目边界和样本分布过一遍这半小时能帮你省下后面几天的调参时间。2. 拆开压缩包看数据8类垃圾的标注边界与真实分布2.1 纸箱、硬纸板、纸张三类的视觉边界怎么在标注里划清这个数据集的类目命名存在明显的语义交叠。纸箱和硬纸板在日常生活中经常是同一个物体的不同状态立体完整的快递盒叫纸箱被压扁堆叠之后就变成了硬纸板纸张则可能混入报纸、打印纸、纸巾甚至纸袋。如果标注人员在画框时对这些边界没有统一规则模型会在同一个物体上来回震荡训练loss看似收敛但推理时频繁跳变。我一般拿到这种多类目数据集第一件事是建立一张类目决策表直接用最简单、可执行的标准定义每类边界。纸箱定义为保持立体盒体结构、能看到瓦楞或折痕的纸容器硬纸板定义为已经压扁或失去盒体结构的大块纸板纸张则限定为薄片状纸制品纸袋、纸杯这类立体成型物按盒体处理。塑料瓶和瓶子按材质区分PET材质的瓶身归塑料瓶玻璃材质的归瓶子铝罐归铝。这条规则并不完美但能让标注数据维持一致性比“凭感觉画”可靠得多。数据类别标注判定标准容易混淆的对象纸箱立体盒体结构可见瓦楞层或折叠接缝压扁后看起来像硬纸板硬纸板无盒体结构的大块纸板可带折痕纸箱压扁状态纸张薄片状纸制品不含立体结构纸袋、纸盒展开状态瓶子玻璃材质瓶身透明PET瓶在特定光照下塑料瓶PET等塑料材质瓶身带瓶盖或标贴玻璃瓶身形状相似时这张决策表的价值不只是给标注员看也是训练后检查混淆矩阵的参照。如果纸箱和硬纸板的混淆数据依然很高说明标注规范没有落地需要回看原始样本重新洗标签而不是急着调模型参数。2.2 读Label文件txt格式、类别编号与归一化坐标YOLO数据集的标签文件通常是每张图像同名的一个txt每行描述一个目标框。格式为类别ID加四个归一化坐标具体如下0 0.5123 0.4782 0.1345 0.2891 1 0.2456 0.6821 0.1023 0.1544第一个数是类别ID后面依次是框中心点x、中心点y、框宽、框高全部除以图像宽高归一化到0到1。类别ID的对应关系由classes.txt或data.yaml里的names列表决定这行顺序不能乱改否则后续看PR曲线、混淆矩阵时会完全对不上号。打开标签文件之前还有一个容易忽略的检查点确认标签文件扩展名。有的标注工具默认导出xml或json比如LabelImg导出Pascal VOC格式如果压缩包里不是统一txt就需要先做一次格式转换。常见做法是写脚本把xml里的xmin、ymin、xmax、ymax读出来转成中心点归一化坐标。这个转换我踩过两次一是忘了除以图像宽高二是把左上右下坐标当成中心点坐标直接用两个错误都会让训练loss异常高、框完全偏到图像角落。2.3 写脚本体检数据集各类框数、框尺寸与异常值统计拿到数据集先体检再训练这是做yolo项目以来的固定动作。下面这段脚本统计每个类别的框数量、框面积并顺便检查标签文件里是否有格式异常的行。import os from collections import Counter label_dir labels # 换成解压后的标签目录 stats Counter() error_lines [] for f in os.listdir(label_dir): if not f.endswith(.txt): continue with open(os.path.join(label_dir, f), encodingutf-8) as fp: for line_no, line in enumerate(fp, 1): parts line.strip().split() if len(parts) ! 5: error_lines.append((f, line_no, line.strip())) continue cls int(parts[0]) w float(parts[3]) h float(parts[4]) stats[cls] 1 # 记录框面积用于判断目标大小分布 if w * h 0.01: print(f小目标: {f} 类别{cls} 面积{w*h:.3f}) for cls_id in sorted(stats): print(f类别{cls_id}: 框数 {stats[cls_id]}) print(异常行数:, len(error_lines))这段脚本做三件事统计框数判断各类是否均衡、打印小目标样本判断是否需要调分辨率、捕获标签行格式异常。框面积小于0.01意味着目标只占全图面积的1%这种小目标在imgsz640下可能只有十几个像素检测难度会明显上升。如果这类情况较多训练时的imgsz建议开到640以上评估指标要重点看mAP.5:.95而不是只看mAP.5。体检完框数和面积还要把坐标越界和空标签查出来。越界框表现为中心点坐标或宽高超出0到1def check_bound(parts): x, y, w, h map(float, parts[1:]) x1, y1 x - w / 2, y - h / 2 x2, y2 x w / 2, y h / 2 if x1 -0.01 or y1 -0.01 or x2 1.01 or y2 1.01: return False return True这里允许0.01的浮点误差超过就需要把框裁剪或丢弃。空标签文件的特征是文件大小为零直接删除对应的图片和标签即可。处理顺序必须是先清洗再划分train和val否则清洗时删了验证集的图训练集里还留着同名文件目录结构就全乱了。3. 处理数据集用于yolov8训练目录整理、train/val划分与data.yaml生成3.1 按YOLO规范整理images与labels目录YOLOv8对数据目录的约定是images和labels两个平级目录各自下面再分train和val图片和标签文件通过同名不含扩展名关联。压缩包里如果只是平铺的图片和txt需要先整理成下面的样子dataset/ images/ train/ 000001.jpg 000002.jpg val/ 000101.jpg labels/ train/ 000001.txt 000002.txt val/ 000101.txt整理目录用shutil移动文件即可但有一个细节值得注意图片扩展名可能同时存在.jpg、.jpeg或.png移动时按图片实际扩展名对应处理标签的txt后缀不要把扩展名写死。这个目录结构同样适用于mmrotate这类旋转框检测框架虽然这里的纸张、纸箱用水平框已经够用但保持标准结构后续要换检测框架时不用再改。3.2 train/val划分脚本按场景整体划分而不是随机洗牌训练集和验证集的划分方式是决定评测可信度的第一要素。全局随机shuffle在图像分类里问题不大但在目标检测里会带来严重的数据泄漏同一个垃圾堆连续拍摄的几帧一帧进train一帧进val模型在训练时已经把验证目标的纹理特征记住了验证mAP会虚高到不真实之后换到新场景立刻打回原形。熟悉这类数据集的人会注意到垃圾检测数据经常来自定点摄像头或连续抽帧文件名往往带有批次或时间前缀。按文件名前缀分组再把整个组划进train或val是比较稳妥的做法。下面这段脚本以文件名下划线前的部分作为分组单位按85%的比例整体划分import os, random, shutil from collections import defaultdict random.seed(42) base dataset imgs [f for f in os.listdir(os.path.join(base, images)) if f.endswith((.jpg, .jpeg, .png))] groups defaultdict(list) for f in imgs: group_key f.split(_)[0] # 按文件名前缀分组 groups[group_key].append(f) os.makedirs(dataset/images/train, exist_okTrue) os.makedirs(dataset/images/val, exist_okTrue) os.makedirs(dataset/labels/train, exist_okTrue) os.makedirs(dataset/labels/val, exist_okTrue) for key, files in groups.items(): random.shuffle(files) split int(len(files) * 0.85) for f in files[:split]: shutil.move(os.path.join(base, images, f), dataset/images/train/) shutil.move(os.path.join(base, labels, f.rsplit(., 1)[0] .txt), dataset/labels/train/) for f in files[split:]: shutil.move(os.path.join(base, images, f), dataset/images/val/) shutil.move(os.path.join(base, labels, f.rsplit(., 1)[0] .txt), dataset/labels/val/)这里需要注意几个参数。group_key按文件名前缀取如果压缩包里的文件名本来就是无规律编号那就改按拍摄时间或者目录名来分组。0.85是热身用的比例数据量少可以降到0.8。random.seed(42)固定随机种子保证两次执行结果一致这一点在复现实验时非常关键。划分完成后统计一下val中每一类的框数确保最少的类也至少有几十个框否则该类的评估指标会因样本太少而剧烈抖动。3.3 生成data.yaml并启动第一轮训练目录就绪后需要给YOLO一个数据描述文件data.yaml里面写明训练集、验证集路径、类别数量和类别名称列表。注意路径既可以用绝对路径也可以用相对YOLO运行目录的路径容器环境下绝对路径最不容易出错。path: /data/waste/dataset train: images/train val: images/val nc: 8 names: 0: cardboard_box 1: paper 2: plastic 3: aluminum 4: glass 5: cardboard 6: bottle 7: plastic_bottlenc必须和names列表长度一致顺序必须和标签txt里的类别ID一致。我平时把这个yaml当作项目的一部分放进git里管理类别名称的命名规范也以它为准。如果只想用相对路径也可以写成train: ../dataset/images/train但一旦换机器就特别容易断还是写绝对路径稳妥。提示如果你中途改了类别ID或names顺序旧标签txt和新data.yaml会对不上。改类目前先把原data.yaml备份一份这是给自己留的后悔药。启动第一轮训练我推荐用预训练权重而不是随机初始化yolo detect train \ data/data/waste/dataset/data.yaml \ modelyolov8n.pt \ imgsz640 \ batch16 \ epochs100 \ device0命令里的参数解释一下model指定预训练权重yolov8n.pt是nano版显存占用小第一轮先用它跑通流程batch16是单卡16G显存的经验值OOM就降到8imgsz640是YOLO系列的默认输入尺寸如果前面统计发现大量小目标可以升到768但训练时间会涨。epochs给100是为了先拿到完整的学习曲线而不是为了直接训出最终模型。第一轮的验收标准不是mAP高不高而是loss是否稳定下降、验证集是否有曲线、过程中有没有报错。跑通了再换s或m模型和增强参数做正式训练。4. 训练与推理的5个常见翻车现场从标签打架到背景误检这几条是垃圾检测项目里最常见的翻车现场每一条背后都是一个当时没意识到的错误假设按“现象、原因、解决”的顺序记下来方便你排查时对上号。4.1 标签语义重叠纸箱与硬纸板、瓶子与塑料瓶让模型左右横跳现象训练过程loss正常验证集mAP也不差但部署后同一个物体在连续几帧被识别成两个不同类别框在纸箱和硬纸板之间来回跳。原因标注规范没有落实到全部样本。纸箱压扁了看起来就是硬纸板透明塑料瓶在逆光下和玻璃瓶几乎无法区分不同标注员对边界理解不一致导致同一类别内部的视觉特征分裂成两簇模型被迫在两个簇之间折中。解决合并是成本最低的手段。把纸箱和硬纸板合并成cardboard瓶子与塑料瓶合并成bottlenc从8降到6重新生成标签和data.yaml重新训练。合并之后mAP.5通常会涨3到5个点因为模型不再需要区分一对本质相同的物体。如果业务上必须要保留细分那就必须回炉清洗把混淆严重的样本抽出来人工复核统一标签后再训练。合并标签时每类ID都要重新映射不只是替换被合并的那几个类。下面这段脚本演示了“合并重编号”一步到位import os # 新ID映射原0-4不变5并入06和7都并入新5 merge_map {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 0, 6: 5, 7: 5} for split in [train, val]: label_dir flabels/{split} for f in os.listdir(label_dir): if not f.endswith(.txt): continue lines [] with open(os.path.join(label_dir, f)) as fp: for line in fp: parts line.strip().split() if not parts: continue new_cls merge_map[int(parts[0])] lines.append(f{new_cls} .join(parts[1:])) with open(os.path.join(label_dir, f), w) as fp: fp.write(\n.join(lines))这段代码里的merge_map是提前定义好的字典注意它把原6和原7同时映射到新ID 5替换后0到5共六个类别刚好连续。重新编号之后data.yaml的names列表也要同步改成6项。train和val的标签必须在同一次脚本里改否则两边ID体系不一致训练时类别会错位。4.2 坐标越界与空标签文件训练进度卡了一半才发现脏数据现象训练到第40个epochval loss开始震荡不降某些batch的loss明显比其他batch高一截打开训练日志发现YOLO提示存在坐标异常的warnings。原因数据集的标签文件里存在两类脏数据。第一类是坐标越界的框比如x中心点加一半宽度大于1YOLO在数据加载时会按原尺寸计算损失越界框会给loss加入一个不该有的惩罚项让梯度被异常样本主导。第二类是空txt标签文件整张图没有任何目标框batch中正样本缺失模型在几个空batch上出现loss回弹。解决在数据体检阶段就处理掉用第2.3节的check_bound函数把越界框裁回边界内空txt直接删除。还有一个容易漏的地方txt里的类别ID越界比如nc8但标签里出现了8或者9读取时会直接索引到不存在的类别训练直接崩。写脚本时顺便验证一下0 cls_id nc这个条件。4.3 小类目AP挂零玻璃类为什么训了100轮还是0现象训练完查看results.csvglass类的mAP.5一直为0而其他类都正常翻看验证集图片玻璃瓶确实存在且被漏检。原因类别样本数量不均衡。13707张图不代表每类都均匀分布玻璃制品在生活垃圾中的占比本来就低如果压缩包里玻璃类只有两三百个框且拍摄视角单一模型只能记住训练集里那几种固定姿态验证集稍一变角度就认不出。解决先增加小类目在训练中的曝光率。常见做法有两种一是复制粘贴增强把玻璃框从原图抠出来粘贴到其他无玻璃的图片上合成时注意光照方向合理二是调大损失权重在yolov8里通过cls超参数提高分类损失的贡献。训练策略上给小类目多一些mosaic增强机会让模型在混合图里反复看到玻璃特征。不要只加epochs样本分布不变时加轮数只会让模型对验证集记得更牢。如果数据集是按来源划分train和val的还需要确认小类目在train和val里都有而不只是恰好全落在验证集。4.4 验证集mAP虚高随机划分导致的评估失真现象训练曲线显示mAP.5达到0.85打包给客户演示时换个摄像头精度直线跌到0.5以下场面一度尴尬。原因数据划分用了全局随机shuffle没有考虑同一场景的连续帧。垃圾检测数据大部分来自固定摄像头同一批垃圾堆的多个角度照片高度相似模型在训练时已经见过验证集的物体mAP虚高。这在多类别检测里尤其隐蔽因为P/R曲线看起来都很好很难直接判断是过拟合还是数据泄漏。解决按第3.2节的方式按批次或前缀分组划分。判断划分是否合格有一个笨办法在val中随机抽10张图去train里比对如果肉眼看得出是同一场景的连续帧说明泄漏存在。改完划分后mAP会下降这是回归真实水准不用慌用第5章的调参手段把泛化能力补回来。还要注意拍摄时间跨度如果train全是7月的晴天、val全是8月的阴天光照变化本身就会导致mAP下降这属于分布漂移问题需要在训练集里主动混入多种光照样本。4.5 背景误检地面反光、阴影和路面纹理被当垃圾识别现象现场测试时没有垃圾的地面被画了大量框框内是树叶阴影、地砖纹路或者塑料反光点训练时的PR曲线却一切正常。原因垃圾检测任务的特殊之处在于“非垃圾”的负样本没有在数据集中独立标注。模型学到的是“该区域纹理像垃圾”而阴影、反光在局部纹理上确实和塑料、纸张有重叠。验证集里全是正样本图模型没有机会在评估时暴露这个问题。解决收集负样本是根治手段。在现场拍一批完全没有垃圾的地面、桌面、传送带背景图放进训练集对应的标签txt置空。训练时YOLO会把这些图当作纯背景参与损失计算模型能逐渐学会抑制无目标区域的响应。如果没有额外负样本推理侧先顶住把置信度阈值从默认的0.25上调到0.4以上NMS的iou阈值从0.45调到0.3减少重叠误检框。这两种调法会牺牲一部分召回但误检率的下降在演示场合更重要。5. 调参与评估把垃圾检测模型调到敢上线的几个关键参数5.1 预训练权重与模型尺寸从nano起步还是直接上small13707张图说少不少但说多也就刚够把模型喂饱。我的建议是先用yolov8n.pt把整个流程跑通验证数据没问题后换yolov8s.pt或yolov8m.pt做正式训练。预训练权重在COCO上学习的边缘、纹理、形状特征可以直接迁移到垃圾检测任务收敛更快最终精度通常也比从零开始高两三个点。如果机器显存只有6到8GB就留在nano级用更长的训练轮数来弥补模型容量如果有24G以上单卡或多卡直接上s推理帧率仍然够用。这里有个容易被忽略的参数是冻结骨干层freeze。第一次训练时可以冻结前10层跑50个epoch让模型先专心学检测头再解冻全部层做第二轮微调。这样能避免预训练特征被垃圾数据集的小样本量破坏同时训练速度明显加快。yolov8命令里通过freeze10指定但解冻训练之后记得把学习率调低否则骨干层会出现loss反弹。5.2 数据增强参数mosaic、hsv、mixup怎么调YOLOv8的默认增强参数是通用预设针对垃圾检测需要做三处调整。第一处是mosaic默认开启且融合4张图对小目标训练很有帮助但会引入大量被裁剪的残缺目标。垃圾数据里纸张和纸箱本来就是大目标残缺框太多反而干扰学习建议在前1/3的轮次把mosaic关闭之后再打开微调。第二处是hsv色彩增强垃圾材质对颜色敏感度不算高但塑料和玻璃在颜色上有区分价值hsv_h保持默认0.015即可hsv_s可以略降到0.5避免颜色失真过度。第三处是mixup默认是0.0如果类别混淆不严重可以开到0.1让模型在混合样本中更关注形状而不是单一像素纹理。这些超参数在yolov8里可以通过命令行直接覆盖比如yolo detect train ... hsv_h0.015 hsv_s0.5 mosaic0.0 mixup0.1。改参数时只动一处跑一轮看结果再动下一处不要一次全改否则效果好坏你根本不知道是哪一项起作用。5.3 训练曲线怎么看P、R、mAP.5与mAP.5:.95各自代表什么只看总mAP垃圾检测很容易出现一种玄学感分数不低现场一测又是另一回事。要拆开看训练结束后优先看results文件夹里的results.csv用下面几行直接读出来import pandas as pd df pd.read_csv(runs/detect/train/results.csv) cols [epoch, metrics/precision(B), metrics/recall(B), metrics/mAP50(B), metrics/mAP50-95(B)] print(df[cols].tail(10))Pprecision表示检出的框中垃圾真正占的比例Rrecall表示真实垃圾被找出的比例。两者在默认阈值0.25下都有参考价值P低说明误检多R低说明漏检多。mAP.5是IoU阈值0.5下的平均精度它更宽容适合衡量“能不能检测到目标的大致位置”mAP.5:.95则计算从0.5到0.95十几个IoU阈值下的平均精度对框的定位精度更敏感。垃圾检测部署到摄像头场景时两类指标要分开看。如果只是做计数和粗定位比如“这个区域有没有垃圾”mAP.5就够如果下游要做机械臂抓取或自动分类定位误差直接决定抓取成功率必须盯mAP.5:.95。还有一种情况是验证集mAP.5很高但:.95很低说明框位置偏了但类别基本正确此时优先提升回归精度而不是分类能力手段是提高imgsz并且把box损失权重调大。5.4 用混淆矩阵决定是否合并类别训练完成后yolov8会输出confusion_matrix.png这是判断类目体系是否合理的最终依据。混淆矩阵中非对角线上的深色块表示两类之间的误检比如纸箱和硬纸板、瓶子和塑料瓶的对角线附近出现明显的暗色块就说明这两类在特征空间里太近标注规范没有把类间特征分开合并它们是合理的。另一种值得注意的模式是background那一行出现深色块对应第4.5节说的背景误检提示需要补充负样本。看混淆矩阵时还要结合每个类别的框数来读。框数最少的类别即使有轻微混淆也不一定值得处理因为样本量小评估噪声本来就大。更好的做法是输出几张典型误检图把同一帧里模型给两类打的置信度分数打出来人工判断模型是在哪个特征维度上摇摆。这个过程通常耗时一两个小时但能彻底决定标签体系要不要改、数据要不要补比反复盲调训练参数更有效。6. 出门测试用一段没见过的视频验证模型边界训练和调参完成后真正的验收在于一段没见过的视频。我通常的做法是找一段现场环境里、不在训练和验证集中的连续视频按每秒抽两帧的方式跑推理统计三类指标检出率有垃圾但有框、误检率无垃圾但有框、定位稳定性同一目标连续帧的框中心点抖动幅度。下面给一个不计帧率的脚本骨架import cv2 from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) cap cv2.VideoCapture(scene_new.mp4) fps cap.get(cv2.CAP_PROP_FPS) frame_id 0 det_info [] while True: ok, frame cap.read() if not ok: break if frame_id % int(fps / 2) ! 0: frame_id 1 continue results model(frame, conf0.35, iou0.45, verboseFalse) for r in results: for box in r.boxes: cls_id int(box.cls[0]) conf float(box.conf[0]) cx (box.xyxy[0][0] box.xyxy[0][2]) / 2 det_info.append((frame_id, cls_id, conf, float(cx))) frame_id 1脚本里的conf和iou是推理侧的两个旋钮conf调高滤误检但也会漏掉一部分低置信度的真目标iou调低减少重叠框适合同一目标被重复检测的情况。第一遍测试建议保持conf0.35、iou0.45跑一遍记录每一类的漏检和误检再用conf0.5跑一遍对比两条曲线相交的点就是适合现场环境的置信度。FPS验证要单独测用下面的命令去掉显示和画框逻辑测纯推理速度yolo detect predict \ modelruns/detect/train/weights/best.pt \ sourcescene_new.mp4 \ conf0.4 \ saveFalse这类部署级验证最容易被忽略的是帧率瓶颈。如果在测试机上达到30FPS换到现场边缘盒子可能只有8FPS需要把模型导出成ONNX或TensorRT再跑。导出之前务必在测试机上对比一次导出前后相同输入的输出差异超过一个置信度阈值就要检查预处理是否被简化。我在第一个垃圾检测版本上就吃过随机划分的亏验证集mAP漂亮一到现场全露馅。后来养成习惯训练前先问数据是怎么拍的划分时能不能按批次切开出门测试的录像永远单独留一份。技术上的坑大多能通过日志和曲线定位数据集本身的一致性反而是最花时间的部分。希望帮到你。本文还有配套的精品资源点击获取