ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

垃圾识别分类数据集四分类实战:清洗、训练与避坑

垃圾识别分类数据集四分类实战:清洗、训练与避坑 简介面向垃圾分类识别类课程作业与毕设项目这份资源提供了一套完整的垃圾图像采集方案覆盖厨余、有害、可回收、其他四类垃圾场景包含类别名称清单、爬虫脚本和数据分布可视化工具帮助学习者在缺少现成数据集时快速构建3万多张规模的自建图像库。压缩包共14个文件其中7个txt用于存放各类垃圾的具体名称、源关键词和爬取清单6个Python脚本按四类垃圾分别执行爬取任务另有1张png图展示类别占比整体仅35KB轻量易用。txt与py相互配合既可直接运行获取图片也可修改关键词扩展采集范围适合课设、期末大作业或入门图像分类项目。目前已有176人学习下载。通过这份资料使用者能快速掌握从类别定义到数据爬取、从目录组织到可视化分析的全流程方法为后续模型训练、评估与报告撰写节省时间尤其适合需要独立完成数据准备环节的初学者参考。1. 垃圾识别分类数据集 3 万多张怎么用四分类从 zip 到模型的第一关接手一个垃圾识别分类数据集3 万多张图按厨余、有害、可回收、其他四类分好很多人解压后的第一反应是“够了直接训练”。实际往前跑两步就会发现这个量级正卡在“够跑通流程”和“能支撑上线”之间目录干不干净、类别分布是否均衡、验证集切得科不科学都会决定后面几周是顺推还是反复返工。这篇文章写给两类人一是手里刚拿到这个 zip、想快速出一个四分类基线模型的开发者二是被智能垃圾桶、环卫拍照识别这类项目缠住需要在有限数据和算力下把准确率稳稳推到可用线以上的工程师。我会按解压、清洗、划分、训练、排错、复盘六步讲每一步都给能直接复制的操作和参数最后告诉你哪些坑值得绕路。2. 解压后先查这三项四类垃圾数据集的目录结构、坏图清洗与类别分布2.1 两种常见 zip 布局目录即标签还是 CSV 配平铺图片拿到 zip 之后我一般不急着解压先用unzip -l只列压缩包内容而不落盘先确认顶层结构。常见做法有两种一种是根目录下四个文件夹分别叫“厨余”“有害”“可回收”“其他”每个文件夹里面是图片标签就是文件夹名另一种是图片全部平铺在一个目录配一个 CSV 文件CSV 里写了文件名和类别名。这两种布局决定了后面用torchvision.datasets.ImageFolder直接读还是要自己写一个Dataset。unzip -l 垃圾识别分类数据集3万多张_厨余_有害_可回收_其他4大类.zip | head -40head -40只截前 40 行目的是快速看顶层目录有没有__MACOSX、隐藏文件这类干扰项。如果发现文件名显示成乱码一般是从 Windows 压缩包里带出来的 GBK 编码中文名解压时可以用unzip -O gbk指定编码或者解压后用convmv做转码。这个细节不处理后面所有读图脚本都会因为路径找不到而报错而且报错信息是“No such file or directory”特别容易让人误以为是图片缺失。确认完布局我一般会顺手把压缩包里的目录树导出来存成一份structure.txt。这一步看起来多余但当你跑完模型想回查某一张图到底属于哪个批次、哪个桶时这份文件就是后悔药。等全部清洗做完再回头找目录结构往往已经记不清原始状态了。2.2 清洗脚本坏图过滤、通道统一、类别计数解压之后不要直接开训先跑一个清洗扫描脚本。3 万张图里混进去几十张坏图、灰度图、小尺寸图太常见了尤其从网上爬取或从多个渠道拼起来的数据集坏图比例在 0.5% 到 2% 都属于正常。这些图会在训练中途突然炸掉或者静默拉低验证指标属于典型的黑匣子问题。import os import numpy as np import cv2 from pathlib import Path from collections import Counter def cv_imread(path): # cv2.imread 对中文路径支持差用 np.fromfile imdecode 替代 data np.fromfile(str(path), dtypenp.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR) root Path(garbage_dataset/train) ext_cnt Counter() cls_cnt Counter() bad_images [] gray_images [] small_images [] for img_path in root.rglob(*): if img_path.suffix.lower() not in {.jpg, .jpeg, .png, .bmp, .webp}: continue cls_name img_path.parent.name ext_cnt[img_path.suffix.lower()] 1 cls_cnt[cls_name] 1 img cv_imread(img_path) if img is None: bad_images.append(str(img_path)) continue if img.ndim 2: gray_images.append(str(img_path)) if img.shape[0] 32 or img.shape[1] 32: small_images.append(str(img_path)) print(类别分布:, dict(cls_cnt)) print(扩展名分布:, dict(ext_cnt)) print(坏图数量:, len(bad_images)) print(灰度图数量:, len(gray_images)) print(小于32像素的图:, len(small_images))这段脚本做了三件事统计每类样本数、统计图片扩展名分布、筛出三种问题图。坏图判定标准是imdecode返回None这类文件多半是下载中断产生的截断 JPEG 或伪装的非图片文件灰度图要单独统计因为后续训练如果统一走三通道灰度图会被复制通道数量少没事数量大了会影响颜色相关的特征小于 32 像素的图插值之后已经失去可辨识特征单独列出来人工判断是删是留。cv_imread这个封装是必须的不是锦上添花。在 Windows 上路径含中文时cv2.imread会静默返回None导致坏图误报清洗报告就会失真。我自己第一次跑清洗脚本时就踩过这个坑统计出 400 多张“坏图”结果全是因为路径里带了“厨余”两个字。清洗时的原则是“先报告后动手”。把坏图、小图的路径导出成一个clean_report.csv人工扫一眼再删。不要写个循环直接删除因为有些全黑或全白的图能正常解码但内容无意义你单靠代码识别不出来。另外清洗完建议把删掉的文件清单保留这能让你在后面复现结果时知道训练集经过了什么处理。3. 数据划分与增强3 万张图怎么切才不骗自己3.1 用分层抽样切分 train/val/test比例、随机种子与同源重复检测数据集划分是决定模型评估可信度的第一道关。四类垃圾里有害垃圾这类样本往往远少于其他类如果直接随机划分验证集里有害垃圾可能只有几十张准确率波动极大你会以为模型在收敛实际只是噪声。我一般用两层train_test_split先把整个数据集按类别比例留出 20%再从这 20% 里各分一半作为验证集和测试集最终比例是训练集 80%、验证集 10%、测试集 10%。import pandas as pd from sklearn.model_selection import train_test_split # 假设 labels.csv 至少含 image_path, label 两列 df pd.read_csv(garbage_labels.csv) train_df, temp_df train_test_split( df, test_size0.2, stratifydf[label], # 按类别比例分层 random_state42 ) val_df, test_df train_test_split( temp_df, test_size0.5, # 从 temp 里再各取一半 stratifytemp_df[label], random_state42 ) train_df.to_csv(split_train.csv, indexFalse) val_df.to_csv(split_val.csv, indexFalse) test_df.to_csv(split_test.csv, indexFalse)stratify参数让每次切分都保持类别比例不变random_state固定成 42 是为了可复现。这里有个容易忽略的点不要每跑一个实验就换一个随机种子否则模型精度的上升到底是来自你的改动还是数据切分的运气你根本分不清。固定同一个 seedbaseline 和实验组之间才有可比性。切分之前还要做一道同源重复检测。所谓同源重复是同一张图在压缩包里出现了两次或者同一场景的连拍帧被同时分进训练集和验证集。这会造成数据泄漏模型在验证集上表现好不是因为它学会了“识别垃圾”而是因为它记住了那张图本身。3 万张图里出现几十组重复很常见尤其数据集来自多个采集批次时。我一般会对全量图片算一遍感知哈希用汉明距离小于阈值的判定为重复把重复项归到同一个组整个组只放进训练集或只放进验证集。运行完上面的划分脚本后一定要打印一次验证集的类别分布确认四类的比例和全量数据一致。这一步不用写复杂代码df[label].value_counts()一行就够但它能拦住最傻的数据泄漏。验证集切得不科学后面所有调参都是在给自己制造幻觉。3.2 对垃圾图片真正有效的增强翻转裁剪颜色抖动别乱用擦除垃圾分类识别的拍摄场景其实很固定垃圾桶内、垃圾袋旁、传送带上、手持拍摄。相比 ImageNet这个任务的域内变化小得多增强策略要克制。我常用的训练增强和验证增强分别如下。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.05), transforms.RandomRotation(degrees10), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])每个增强项都有对应的硬件条件。RandomResizedCrop模拟垃圾在镜头前的尺度变化——厨余垃圾经常被凑近拍导致物体占比忽大忽小ColorJitter模拟不同灯光和垃圾桶内阴影黄色灯光下塑料瓶和纸质包装的颜色偏移很大HorizontalFlip对垃圾完全安全因为垃圾没有左右语义。RandomRotation我只给 10 度因为易拉罐、纸板箱有明显竖直方向特征转太多会把方向变成噪声让模型花参数去学一个不存在的变换。验证集和测试集只用Resize CenterCrop Normalize不做任何随机变换。原因很简单验证集要稳定你今天跑和明天跑结果应该一样一旦验证集带了随机翻转同一个模型两次评估差 0.5 个百分点你就很难判断是改动生效还是噪声波动。禁用项方面我尤其不建议一上来就上RandomErasing或CutMix。这个数据集的图片主体是单一目标随机擦除很可能直接抹掉关键判别部位——比如去掉电池上的标识、抹掉玻璃瓶的反光特征模型只能靠剩余背景去蒙。等你训练收敛、确定小类过拟合了再考虑这些强增强不迟。另一个不要做的是旋转 90 度或 180 度的大角度增强除非你的采集数据里确实有各种朝向的瓶子否则它只会拉大类内方差。4. 用 ResNet18 / EfficientNet-B0 跑通四分类训练脚本与参数调优4.1 最小训练脚本torchvision 加载自定义数据集的四分类训练循环这个数据集是 4 大类不是检测框所以直接用图像分类的套路就好。常见做法是选 ResNet18 或 EfficientNet-B0 起步ResNet18 训练快、显存省适合第一次跑通全流程EfficientNet-B0 精度高一点、参数不算多适合做最终模型。数据集规模 3 万张时两个模型最后都能收敛到可用的水平差别主要在训练时间和显存占用。import torch import torch.nn as nn import torchvision from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder from torchvision import models train_ds ImageFolder(garbage_dataset/train, transformtrain_transform) val_ds ImageFolder(garbage_dataset/val, transformval_transform) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size64, shuffleFalse, num_workers4, pin_memoryTrue) model models.efficientnet_b0(weightsmodels.EfficientNet_B0_Weights.IMAGENET1K_V1) model.classifier[1] nn.Linear(model.classifier[1].in_features, 4) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) model model.cuda() for epoch in range(30): model.train() for images, labels in train_loader: images, labels images.cuda(), labels.cuda() optimizer.zero_grad() logits model(images) loss criterion(logits, labels) loss.backward() optimizer.step() scheduler.step() torch.save(model.state_dict(), fgarbage4cls_epoch{epoch}.pth)这里的关键是ImageFolder要求目录结构严格按train/类别名/图片组织第 2 章清洗完的数据如果原本是 CSV 格式需要先按标签建目录并移动图片否则换用自定义Dataset。加载预训练权重是必须的垃圾图片虽然和 ImageNet 域差异比较大但边缘、纹理、颜色这些底层特征还是能迁过来的。classifier[1]是 EfficientNet-B0 最后一层线性层只替换它前面的 backbone 参数保留预训练值。30 个 epoch 对这个任务规模是一个比较稳的默认值配合CosineAnnealingLR学习率从初始值平滑降到接近 0比固定学习率省掉“到底哪个 epoch 收敛”的纠结。如果用的是 ResNet18替换分类头的代码改成model.fc nn.Linear(model.fc.in_features, 4)即可。两个模型的参数量和显存占用不同选型参考下面的表。模型参数量级别单卡 224 输入典型显存训练速度适用阶段ResNet18约 11M2GB 左右可跑 batch32快第一版 baseline快速验证流程EfficientNet-B0约 5M3GB 左右可跑 batch32中等主力模型精度和速度均衡EfficientNet-B2约 9M5GB 左右可跑 batch32较慢追求精度且显存充足时EfficientNet-B0 虽然参数量比 ResNet18 小但计算量并不少因为它在 depth 和 resolution 上做了折中实际训练速度不比 ResNet18 快。所以别只看参数量选模型要看你的显卡能承受多大 batch 和多少训练时间。4.2 四个必调参数学习率、batch size、weight decay 与类别权重训练脚本能跑只是第一步真正决定四分类效果的是接下来几个参数。我在垃圾分类这个任务上反复调过几次稳定有效的组合基本固定在这组值上。参数推荐值调整方向与副作用学习率AdamW 下 1e-3SGD 下 3e-2调大 loss 震荡不收敛调小前 10 个 epoch 几乎不降建议前 5 个 epoch 从 1e-4 warmup 到 1e-3batch size32 起步显存不够时不要直接砍半先上梯度累积小于 16 时 BN 统计不稳定精度会明显掉weight decay1e-4过拟合严重时提到 5e-4但 3 万张图这个规模太大 weight decay 会让模型欠拟合类别权重按样本数反比能抬升有害垃圾这类小类的召回但代价是大类精度下降属于拿 A 换 B这里单独说类别权重。四类分布如果极不均衡比如厨余占了一半、有害垃圾只有一两千张直接训出来的模型会把所有不确定样本都猜成厨余整体准确率看起来不低但有害垃圾的召回率可能只有 20%。这种模型上不了真实场景因为漏掉一个有害垃圾比多分错一个厨余后果严重得多。常见做法是用compute_class_weight算每类权重把权重传给损失函数。from sklearn.utils.class_weight import compute_class_weight import torch classes train_ds.classes y [train_ds.classes.index(p.parent.name) for p in train_ds.samples] weights compute_class_weight(balanced, classesclasses, yy) class_weights torch.tensor(weights, dtypetorch.float32).cuda() criterion nn.CrossEntropyLoss(weightclass_weights)balanced模式的计算公式是n_samples / (n_classes * n_class_samples)样本数少的类拿到的权重自动更大。但注意副作用如果有害垃圾原本只有 1500 张权重会被拉到 4 倍以上模型会变得对有害垃圾过度敏感把厨余里的骨头、鱼刺频繁误判成有害。所以加了类别权重之后必须配合混淆矩阵检查是不是过矫正了。这个我放到第 6 章讲先记住“加权不是免费午餐”。学习率 warmup 对 EfficientNet 尤其重要。没有 warmup 直接上 1e-3经常前两个 epoch 的 loss 不降反升然后再慢慢降回来白白浪费迭代次数。我一般在前 5 个 epoch 让学习率从 1e-4 线性涨到目标值你可以用 torch.optim.lr_scheduler 里现成的LinearLR接CosineAnnealingLR实现。这属于花小钱办大事的调整值得加。5. 避坑手册垃圾识别分类模型训练最常见的 5 个翻车现场5.1 验证集涨到 90% 但新场景全崩先查同源重复图片现象训练时验证集准确率一路涨到 90% 以上你满心欢喜把模型部署到一台真实设备上拍几张垃圾桶照片测试结果一塌糊涂连厨余和可回收都分不清。原因多半是数据泄漏训练集和验证集之间存在同源图片或相似帧模型在验证集上的高精度是“背题”背出来的。解决在切分数据集之前做一遍去重。下面这段用感知哈希去重的逻辑我每次都会跑阈值 8 可以兜住压缩、缩放、轻微调色后的重复图。from PIL import Image import imagehash def dedup_by_hash(paths, threshold8): seen [] dup [] for p in paths: h imagehash.phash(Image.open(p)) if any(h - old_h threshold for _, old_h in seen): dup.append(p) else: seen.append((p, h)) return dupphash感知哈希的汉明距离越小代表图片越相似。这里用 8 作为阈值是靠经验试出来的太小漏掉缩放后的重复图太大把不同垃圾但背景一样的正常图误杀。去重跑完后把同一组重复图整体划到训练集或者整体划到验证集绝不能拆开。5.2 训练中途崩出 broken image / truncated JPEG现象训练跑到第 7 个 epoch程序突然抛OSError: image file is truncated中断退出。有些读者会把这当成“随机崩溃”重新跑一次结果第 9 个 epoch 又崩。原因就是数据清洗没做干净解压过程或原始采集阶段产生了截断 JPEG图片文件本身不完整前面几个 epoch 没加载到一加载就炸。解决两个方案。最省事的是一行代码from PIL import ImageFile; ImageFile.LOAD_TRUNCATED_IMAGES True让 PIL 用已有数据把图拼出来但我不建议直接这么干因为这等于让坏图静默进入训练集模型看到半张图学到的特征就是错的。正确做法是回到第 2 章的清洗脚本用cv2.imdecode把传输中损坏的图全部检测出来手动确认后剔除或替换。说实话3 万张图里坏图重采也就个把小时的事不比排查崩溃省时间。5.3 厨余和其他类互相认错调参压不住现象训练结束看分类报告厨余垃圾的召回率 94%其他垃圾的召回率只有 78%而错误的去向里一大半是厨余反过来把厨余错分到其他类的情况也不少。你加类别权重、调学习率、加数据增强改了三轮两个类别的准确率像跷跷板一样此消彼长整体就是不涨。原因要从数据本身找这两类的视觉差异本来就小。鱼骨放在厨余里牙签放在其他里二者都是细长条沾了油的纸巾是厨余干纸巾是其他颜色和纹理几乎一样。模型在特征空间里找不到一个干净的分类边界调什么参数都白搭。解决先统计混淆矩阵确认矛盾集中在“厨余 vs 其他”这一对而不是其他配对。如果是这两类纠缠我不会继续硬调而是考虑改任务结构先训一个“干垃圾/湿垃圾”二分类再在湿垃圾内部细分厨余这样每一层的分类难度都降低了。这个方案改动大一点但比你对着混淆矩阵调一周学习率有效得多。5.4 准确率高但混淆矩阵显示在“抄背景答案”现象验证集准确率 92%你觉得可以收工了但一眼扫过混淆矩阵发现一个诡异规律——几乎所有被正确识别的“有害”样本都是红桶拍的其他颜色的桶几乎没有有害样本。这说明模型学的不是垃圾本身而是背景有害垃圾的图片背景高度一致模型找到了一条捷径直接根据背景色投票。解决按采集批次划分数据集而不是按图片随机划分。也就是说把同一个批次、同一个场景拍出来的图片尽量放进同一个集合训练集和验证集拿到的背景分布才有差异模型被迫去学物体本身。如果你手里这批数据没有记录拍摄批次退一步用 5.1 的同源去重把相似图片聚一下类按聚类结果分组划分能减轻背景泄露。CutMix 也能打破背景与类别的强关联但对小目标垃圾效果不稳定我通常最后才试。5.5 显存不足 OOM梯度累积和混合精度现象CUDA out of memory不是每次都在同一个 batch 崩而是看运气跑着跑着就崩了。原因很简单EfficientNet-B2 加 batch 64 加 512 分辨率8GB 显卡根本装不下。最常见的错误做法是把 batch 从 32 砍到 16再砍到 8结果 BN 统计量扭曲模型精度跟着掉。正确思路是保 batch降显存单次开销。解决梯度累积加混合精度双管齐下。下面这段是梯度累积的标准写法相当于把 4 个 batch 的梯度攒齐后统一更新一步等效 batch 从 32 变成 128但单次显存占用不变。scaler torch.cuda.amp.GradScaler() accum_steps 4 optimizer.zero_grad() for i, (images, labels) in enumerate(train_loader): images, labels images.cuda(), labels.cuda() with torch.cuda.amp.autocast(): logits model(images) loss criterion(logits, labels) / accum_steps scaler.scale(loss).backward() if (i 1) % accum_steps 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad()注意两个细节。第一loss 除以accum_steps再 backward这一步不做的话梯度会放大accum_steps倍学习率等于白调。第二混合精度下的 BN 统计仍然按单卡 batch 更新所以单卡 batch 不要小于 16梯度累积解决的是显存问题不是 BN 统计问题。6. 不算完用混淆矩阵与 Bad Case 复盘把四分类模型推到可上线6.1 混淆矩阵之外的第二份报告哪两类在互相认错训练结束拿到 90% 以上的准确率工作只完成了一半。另一半在测试集上用没参与过训练和验证的测试集跑一次完整预测打印混淆矩阵把每一类的命中率和最大混淆对象列出来。from sklearn.metrics import confusion_matrix import torch all_pred, all_true [], [] model.eval() with torch.no_grad(): for images, labels in test_loader: pred model(images.cuda()).argmax(dim1).cpu().numpy() all_pred.extend(pred) all_true.extend(labels.numpy()) cm confusion_matrix(all_true, all_pred) for i, cls in enumerate(test_loader.dataset.classes): total cm[i].sum() wrong cm[i].copy() wrong[i] 0 worst wrong.argmax() print(f{cls}: 准确率 {cm[i][i]/total:.2%}, 最大混淆 - {test_loader.dataset.classes[worst]})混淆矩阵会告诉你模型在哪儿犯糊涂但不会告诉你为什么犯糊涂所以第二步是导出 bad case。做法很简单把所有pred ! true的样本路径、真实标签、预测标签写进一个 CSV然后按错误类型分组把同一组错图的缩略图拼成网格一张一张看。这里才是整个流程里最花时间但也最出效果的一步。我第一次认真做这个复盘时发现模型总把透明塑料盒误判成玻璃原因不是材质分不清而是训练集里透明塑料盒的样本基本都出现在“可回收”类的深色背景下模型学到了“深背景 透明物体 可回收”。后来补了一批浅色背景的透明塑料盒样本误判率直接降了一半。看完 bad case 之后你手里会有两张清单一类是数据问题标注错误、遮挡严重、目标太模糊需要回过去修数据另一类是类间本质重叠比如 5.3 说的厨余和其他需要改任务结构。这两张清单比又一个 0.5 个百分点的准确率提升值钱得多。我现在的习惯是每训完一版模型在测试集上重跑一次混淆矩阵和 bad case 导出再做小结哪怕只改了一个学习率参数也看。不看清楚这两样就调参等于白加班。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进