ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

真实废弃物图像分类:从数据集到部署的避坑指南

真实废弃物图像分类:从数据集到部署的避坑指南 简介面向图像分类与目标检测任务这份生活中真实废弃物图像分类数据集提供约4800张已标注图片覆盖纸板、食品有机物、玻璃、金属、杂项垃圾、纸张、塑料、纺织品垃圾和植被共9个类别。数据已完成预处理并划分好训练集与测试集每类图片单独存放可直接作为分类网络输入配套的JSON文件记录类别对应关系show脚本帮助快速可视化样本便于检查数据质量或开展模型迭代。资源包共2000个文件以1998张JPG图片为主另含1个Python脚本和1个JSON配置文件压缩包整体约156MB结构清晰适合用于垃圾分类模型的训练与验证也可作为计算机视觉课程设计或算法改进的实验数据。目前已有65人浏览学习配合配套专栏可进一步了解分类、分割网络改造思路。整体适合中高级学习者结合具体项目实践使用。1. 生活中真实废弃物图像分类数据集为什么验证集95%落地却只有70%真拿这份约4800张已标注的“生活中真实废弃物图像分类数据集”去训一个图像分类模型你会先惊喜后头大验证集top-1轻松超过95%可把模型拿到小区垃圾房实拍照片上一测准确率掉到70%出头。问题不在模型而在“真实”这两个字——真实废弃物是压扁的饮料瓶、沾着油污的餐盒、蜷成一团的塑料袋和纯色底商品图是两个物种。这个数据集的价值就是用真实场景里的杂乱背景、遮挡和光照变化把图像分类模型从“会做题”逼向“能干活”支撑智能回收箱、社区分类督导、再生资源分拣预筛这类落地场景。它适合三类人给智慧城市做视觉方案的算法工程师做图像分类方向毕业设计的学生以及要在边缘设备上做垃圾识别的一线开发者。2. 拆开这份数据集标注格式、类别分布与训练前的三个核对动作拿到手先别急着开训先花半小时做三件事看清标注怎么组织的、摸清类别分布、确认图像尺寸和完整性。这三件事做不踏实后面训练出的模型就是一个没法解释的黑匣子出了问题都不知道往哪查。2.1 盘点三样东西标注组织方式、类别分布、图像尺寸不管数据集是下载的还是团队标注的第一件事是看目录结构。真实废弃物数据集常见的标注形式有两种一类是按类别分文件夹的ImageFolder结构另一类是图片平铺加一份CSV或JSON映射表。这两种格式决定了后面数据加载代码怎么写先确认能省很多返工。数据形态典型结构打开方式ImageFolder目录式data/train/塑料瓶/xxx.jpgtorchvision.datasets.ImageFolder 直接读CSV映射式images/xxx.jpg labels.csvpandas 读按文件名关联标注工具导出式图片 labelme/cvat/labelstudio 导出的jsonjson 解析再统一转格式确认结构的命令很简单一个 find 就能把目录层级和每类图片数都摸清楚# 1. 看目录层数确认是按类别分文件夹还是平铺 find . -maxdepth 2 -type d | head -30 # 2. 统计每个类别文件夹下的图片数量 find . -type f \( -name *.jpg -o -name *.jpeg -o -name *.png \) \ | sed s|/[^/]*$|| | sort | uniq -c | sort -rn这两条命令跑完你能立刻知道两件事一是每个类别有多少张图二是图片文件是不是混了多种扩展名。head -30限制只输出前30个目录防止数据集目录多时刷屏sort -rn按数量倒序最长尾的类别一眼就能看到。如果数据是图片加CSV的形态用Python快速盘点一下类别分布和图像尺寸比眼睛看靠谱得多import pandas as pd from PIL import Image import os df pd.read_csv(labels.csv) print(df.head()) # 前5行看文件名是否带路径前缀 print(df[class].value_counts()) # 类别分布提前发现长尾类别 # 抽查20张图的尺寸确认是否需要统一resize for p in df[filepath].head(20): w, h Image.open(p).size print(os.path.basename(p), w, h)逻辑说明value_counts()输出每个类别的样本数能直接看出类别是不是长尾分布。遍历图片尺寸是为了防止出现超大图、单通道灰度图这类“意外”它们会在训练时造成显存溢出或通道数断言错误。真实数据集里偶尔混着不同尺寸、不同格式甚至打不开的损坏文件这一步排查能省掉训练中断的麻烦。抽样检查完顺手用 OpenCV 扫一遍有没有损坏图片这是从数据标注到训练之间最容易被跳过的动作import cv2 broken [] for p in df[filepath]: img cv2.imread(p) if img is None: broken.append(p) print(损坏图片数:, len(broken))cv2.imread读失败会返回None而不是抛异常所以只要收集到None就说明该换了。这类脏数据混进训练集轻则少几个样本重则让DataLoader直接崩掉整个训练进程。2.2 先重新划分数据集按拍摄来源分组切别全局随机切这是真实废弃物数据集的第一个大坑也是新人最容易踩的直接用random_split或train_test_split做八成两成随机划分。如果你真这么干了大概率会得到一个虚高的验证集指标而上线就露馅。原因在于真实废弃物图像的高度时空相关性。同一批数据往往来自几个固定拍摄点比如小区垃圾房、学校食堂、回收站分拣线。这些拍摄点会连环拍几十上百张同一场景的连续帧背景几乎一模一样垃圾对象也只是轻微挪动。全局随机划分时同一个场景的相似图片很容易一部分进训练集、一部分进验证集——模型在验证集上表现的“准确”其实是在“复读”训练集里见过的背景和物品形态。正确做法是按拍摄来源或批次分组划分。如果文件名里碰巧带了地点或批次前缀比如site1_20240101_001.jpg这种规则就可以用前缀作为分组ID。sklearn的GroupShuffleSplit专门干这个事from sklearn.model_selection import GroupShuffleSplit # 假设文件名是 地点前缀_日期_序号.jpg df[group] df[filepath].str.split(_).str[0] # 取地点或批次前缀 split GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(split.split(df, groupsdf[group])) train_df df.iloc[train_idx] val_df df.iloc[val_idx] # 核对验证集的类别比例防止某类完全没进验证集 print(val_df[class].value_counts(normalizeTrue))逻辑说明GroupShuffleSplit按groups参数保证同一个组的所有样本只会落在同一边——要么全在训练集要么全在验证集。这里用文件名的地点前缀做组ID切出来的验证集反映的是“新地点能不能认出来”而不是“同场景能不能复读”。test_size0.2是验证集比例对4800张的规模来说留960张左右验证比较合理太少则指标波动大。random_state42固定随机种子保证换机器、重跑脚本时划分结果可复现。做完这一步如果发现验证集里某个类别样本数少于10张我一般会把它合并到更粗的父类别或者干脆把该类全部留在训练集、单靠整体指标监控。样本太少硬留在验证集算出来的召回率毫无统计意义。2.3 处理类别不平衡过采样少数类别让模型只认识塑料瓶垃圾类别天然长尾。干净的塑料瓶、易拉罐这类常见垃圾可能上千张而纽扣电池、灯管、过期药品这类有害垃圾可能只有三四十张。如果直接把原始数据喂给模型梯度会被多数类主导模型会学到“见过瓶子就万事大吉”的偷懒策略。处理不平衡有两个常用手段建议配合使用而不是二选一。第一招是过采样少数类用PyTorch的WeightedRandomSampler控制每个样本被抽到的概率import torch from torch.utils.data import WeightedRandomSampler, DataLoader # class_counts 是每类的样本数train_df[class] 是每个样本的类别 counts train_df[class].value_counts() weights 1.0 / counts[train_df[class]] sampler WeightedRandomSampler( weightstorch.tensor(list(weights), dtypetorch.float), num_sampleslen(train_df), replacementTrue ) train_loader DataLoader(train_dataset, batch_size32, samplersampler, num_workers4)逻辑说明每个样本的权重是它所属类别样本数的倒数所以只有30张的“纽扣电池”类单张样本被抽到的概率是千张样本的“塑料瓶”类的三十多倍。replacementTrue允许重复采样相当于把少数类复制了几份进每个epoch。num_sampleslen(train_df)让每个epoch的采样本数跟原数据集一致保持训练步数稳定。第二招是在损失函数层面加权。交叉熵直接支持类别权重数值上取每类样本数的中位数除以该类样本数比单纯倒数更温和median_count counts.median() class_weights median_count / counts class_weights torch.tensor(class_weights.tolist(), dtypetorch.float) criterion torch.nn.CrossEntropyLoss(weightclass_weights)注意一个边界加权损失改变了loss的数值范围如果发现loss曲线的绝对值比不加权时大不少不必慌看验证集准确率和每类召回率即可。过采样和加权之间有个平衡两者同时用可能会导致少数类过拟合我一般先只用过采样如果少数类召回率还不够再叠加加权。3. 用EfficientNet-B0迁移学习训练从一个脚本到一组能上线的参数数据准备好了接下来就是选模型和定训练方案。这里容易犯的错是拿参数量最大的模型一上来就抄网上的超参结果在4800张数据上反复过拟合白白烧掉几天时间。我一般固定用一套“小模型迁移学习分阶段微调”的流程稳定且可复现。3.1 迁移学习而不是从零训练4800张的边界与两个模型选型从零训练ResNet50这种深网络通常需要几十万张同分布数据才能收敛到像样的效果。4800张扔进去模型只能在训练集上背答案在验证集上随机猜。唯一的正解是迁移学习用ImageNet上预训练好的权重做初始化只把最后的分类型头换成自己的类别数然后微调。选哪个底座我建议在两个里挑timm库的efficientnet_b0或者torchvision的resnet18。前者在ImageNet上精度更好参数只有约530万CPU都能凑合跑推理后者生态最全部署到OpenVINO、ONNX Runtime时资料最多。如果你更熟悉YOLO生态用Ultralytics YOLOv8的yolov8n-cls模型跑同一份数据也可以它对ImageFolder格式直接兼容训练命令一条就能启动。不同模型的容量差异在小数据上非常敏感我整理了一个简单的选型对照模型参数量4800张数据的过拟合风险部署友好度MobileNetV3-Small约250万低极高适合树莓派EfficientNet-B0约530万中高ResNet50约2500万高需强正则化中这里的原则是宁小勿大。模型越大4800张数据越不够喂与其上ResNet50然后跟过拟合搏斗不如先用小模型把pipeline跑通。3.2 训练脚本从ImageFolder加载到每个epoch保存最优权重下面给一套可直接照着跑的PyTorch训练脚本模型用EfficientNet-B0。关键逻辑都写在注释里import timm import torch import torch.nn as nn from torchvision import transforms, datasets from torch.utils.data import DataLoader # 数据增强训练集做强扰动验证集只做缩放和标准化 train_tf transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.3, 0.3, 0.2, 0.1), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) val_tf transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_ds datasets.ImageFolder(data/train, transformtrain_tf) val_ds datasets.ImageFolder(data/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) model timm.create_model(efficientnet_b0, pretrainedTrue, num_classeslen(train_ds.classes)) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) def evaluate(model, loader): model.eval() correct, total 0, 0 with torch.no_grad(): for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) pred model(imgs).argmax(dim1) correct (pred labels).sum().item() total labels.size(0) return correct / total best_acc 0.0 for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() loss nn.CrossEntropyLoss()(model(imgs), labels) loss.backward() optimizer.step() scheduler.step() val_acc evaluate(model, val_loader) print(fepoch {epoch1:02d}, val_acc{val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth)逻辑说明RandomResizedCrop(224, scale(0.6, 1.0))是废弃物场景的关键参数真实照片里目标经常只占画面中央一小块裁剪比例下限取0.6能让模型适应这种“目标不完整”的情况。验证集用Resize(256)CenterCrop(224)而不是直接Resize(224)原因是先放大再裁剪中心能保留更多中央区域的细节。CosineAnnealingLR配合T_max30让学习率在30个epoch内从初始值余弦下降到接近0前段快速收敛、后段精细调整。每个epoch验证一次只在验证准确率刷新时保存权重这样即使后面过拟合了也不会污染最优模型。这里有个小细节模型最后一层被替换成了自己数据集的类别数timm会自动用随机初始化的权重替代原ImageNet的1000分类头所以在第一个epoch时分类头的loss会比较大属于正常现象。3.3 必调参数学习率、训练轮数、冻结策略与早停同样一个数据集参数不同效果能差出十个点。我总结了四个对4800张规模最敏感的调整项学习率迁移学习常用范围是1e-3到1e-4。如果你选择全参数一起微调从1e-3起步最稳如果只微调分类头可以放宽到1e-2。我用1e-3跑前5个epoch如果验证loss震荡就降到3e-4重启后半程。训练轮数30到60轮足够4800张数据完全收敛。关键是盯着验证loss曲线当val loss开始回升而val acc不再上涨的那一刻就是过拟合信号别再硬跑。冻结策略小数据最怕一开始就全量微调把预训练权重冲乱我习惯先冻结backbone只训练分类头5轮让新分类头先适应预训练特征的分布再解冻全部层继续微调。冻结代码就三行for param in model.parameters(): param.requires_grad False for param in model.classifier.parameters(): param.requires_grad True # 先跑5轮再解冻全部参数继续微调解冻后记得把学习率调低否则预训练特征会被大步长破坏。早停逻辑很简单连续8个epoch验证准确率不上升就停并回滚到历史最佳的权重文件。这比固定跑满60轮省时间也天然防止过拟合。4. 避坑真实废弃物图像分类的五个翻车现场与排查路径这一章的每一条都是我从真实项目里踩过的坑按“现象—原因—解决”的方式写清楚你遇到类似情况可以直接对号入座。4.1 换拍摄点准确率崩盘模型在认背景而不是认垃圾现象训练时验证集top-1有94%把模型部署到另一个没参与训练的小区垃圾房准确率直接掉到65%左右。看错误样本发现模型对没见过的地砖、墙面特别敏感。原因真实废弃物数据往往来自少数几个固定拍摄点背景高度重复。模型在训练时偷了懒用背景特征来分类——比如某个拍摄点的垃圾桶是绿色的模型就把“绿色区域”当成“可回收物”的线索。这是背景过拟合也是这类数据集最普遍的翻车原因。解决训练阶段就按第2.2节的方式按拍摄点分组划分让验证集模拟“新地点”的分布。增强阶段把RandomResizedCrop的scale下限调到0.5强制模型看物体的局部而不是整张图的背景再加一个RandomRotation(15)打破地砖、墙面等背景的方向线索。排查时把预测错误的图片可视化如果错误图背景高度相似基本就坐实了这个问题。4.2 全局准确率90%有害垃圾召回率却不到40%现象训练日志打印全局准确率93%看起来一切正常。但把每类单独拉出来看有害垃圾这一类召回率只有38%大部分被误分成了可回收物。原因全局准确率被多数类掩盖了。如果塑料瓶占了40%且被正确识别光这一类的正确预测就能把全局准确率顶到很高少数类的错误被淹没在多数类的正确里。解决从第一个epoch起就打印每类的precision、recall、F1而不是只看全局准确率。用sklearn一行搞定from sklearn.metrics import classification_report print(classification_report(y_true, y_pred, target_namestrain_ds.classes))对着报告找那些召回率明显低于平均值的类别用第2.3节的重采样或加权损失重点修正。修正后先看目标类的召回率是否提升、其他类的召回率有没有被拉低再做全局指标决策。4.3 纸类类别互相混淆标注规范不一致酿成的黑匣子现象模型对纸箱、纸盒、纸屑、纸杯四个类别的预测概率接近0.3/0.3/0.2/0.2怎么调阈值都分不开验证集的混淆矩阵里这几类互相污染严重。原因垃圾品类边界本来就模糊一张被压扁的纸盒有人标“纸箱”有人标“纸盒”还有人标“其他纸类”。数据标注阶段如果没定好规范模型就会学到一套混乱的边界输出近乎均匀的分布。这是标注一致性问题不是模型容量问题。解决先停训练回去对着原始标注重理类别层级。常见做法是合并视觉上不可分的类别——把纸箱、纸盒、纸屑合并成“废纸类”宁可类别粗一点也不要在模糊边界上硬分。如果是第三方标注的数据拿预测置信度最低的500张图让标注员在LabelStudio这类工具里复核一遍把模棱两可的样本改到粗粒度类别。这类数据集的标注质量直接决定模型上限模型调参救不了标注噪声尤其在只有4800张图的情况下。4.4 训练集99%、验证集82%4800张撑不住大模型现象第5个epoch后训练准确率逼近100%验证准确率停在82%上不去训练loss持续下降、验证loss开始反弹。原因4800张对EfficientNet-B0来说属于小样本模型容量有余、数据不足开始把训练样本的细节当作通用规律背下来。这是典型的过拟合加深网络只会更严重。解决按优先级试三个手段。第一步把增强强度拉满加入MixUp混合样本增强让模型没法背原图def mixup_data(x, y, alpha0.2): lam np.random.beta(alpha, alpha) idx torch.randperm(x.size(0)) mixed_x lam * x (1 - lam) * x[idx] return mixed_x, y, y[idx], lam # 使用时交叉熵对两个标签分别计算再按lam加权求和第二步换更小的模型从EfficientNet-B0降到MobileNetV3-Small。第三步把weight_decay从1e-4加到1e-3给优化器更大的权重衰减惩罚。如果三步都做了还是有明显过拟合说明这个类别的视觉区分度本身就不够回到第4.3条重新审视类别定义。4.5 同一件垃圾同时进了训练集和验证集数据泄露比欠拟合更难发现现象验证集准确率一直很高但模型在线上推理的表现和验证指标完全对不上差了十几个点。排查了模型、环境、预处理都没找到原因。原因数据泄露。同一个垃圾对象被拍了多个角度比如一个矿泉水瓶拍了五个角度五张照片随机划分时其中三张进了训练集、两张进了验证集。验证集里那两张和训练集里那三张是同一个物体模型等于提前见过答案。真实废弃物数据的连续拍摄特性决定了这个问题特别隐蔽——你不能从单张图上看出它和训练集某张图是同一件垃圾但模型能。解决按拍摄对象或连续拍摄批次去重。如果文件名带了对象ID或时间批次前缀用第2.2节的GroupShuffleSplit按这批ID分组切分。如果没有ID至少保证同一拍摄点、同一分钟内拍的照片全部落在同侧。宁可用更严的划分方式损失一部分验证集样本量也不能要一个虚高的验证指标。排查时做一个简单实验把验证集里每张图去训练集里做最近邻检索如果大量验证图能在训练集里找到像素级接近的样本数据泄露基本实锤。5. 部署前的最后一公里用置信度阈值把低质量预测拦在生产线外模型训完了准确率看着也不错但直接上线还是危险。风险在于softmax输出的概率不是真正的置信度模型对没见过的废弃物形态也会给出一个0.8、0.9的高分。垃圾分类场景和别的图像分类不太一样分错一瓶矿泉水问题不大把有害垃圾分进可回收物后果就严重了。我的做法是给每个类别单独搜一个置信度阈值低于阈值统一进“待复核”队列。具体步骤用训练好的模型跑一遍验证集保存每张图和每个类别的预测概率然后在验证集上给每个类搜索F1最大的概率阈值import numpy as np from sklearn.metrics import f1_score # val_probs: 验证集每张图的预测概率形状 [N, num_classes] # val_labels: 验证集真实标签形状 [N] best_thr {} for cls_id, cls_name in enumerate(class_names): probs val_probs[:, cls_id] targets (val_labels cls_id).astype(int) best_f1, best_t 0, 0.5 for t in np.arange(0.5, 0.99, 0.01): pred (probs t).astype(int) f1 f1_score(targets, pred, pos_label1) if f1 best_f1: best_f1, best_t f1, t best_thr[cls_name] best_t逻辑说明对每个类单独搜阈值是因为不同类别的可区分度完全不同。外观特征明显的瓶罐类往往只需要0.85左右的阈值就能保住召回而像纸类和织物这类视觉重叠较大的类阈值设高了会让大量样本被丢进复核队列设低了又会误分。阈值搜索范围取0.5到0.99、步进0.01足够覆盖绝大多数情况。实际部署时推理结果如果低于对应类别的阈值就把图片和模型预测结果一起送入人工复核或二次检测而不是直接相信模型。生产环境的经验阈值可以参考下表来设定起点类别建议阈值判断依据塑料瓶0.85外观特征明显可以设高纸类0.65类别间特征重叠低阈值更稳妥有害垃圾0.95漏报代价大宁可多复核阈值定好后上线前还有两个我固定的检查动作。第一是导出ONNX时用小数据集验证数值一致性torch.onnx.export导出的模型在ONNX Runtime上跑输出跟PyTorch原模型应几乎一致第二是拿一张从来没见过的纯底白背景商品图喂给模型如果模型给出高置信度但明显错误的分类说明背景过拟合没清理干净回到第4.1条再查。另外要留一个预警指标在真实环境里统计低置信度样本占总预测数的比例。这个比例超过30%说明当前数据分布和训练分布偏离得厉害继续调模型没意义应该回头补拍数据、扩展数据集而不是硬调阈值曲线。我现在做这类项目有一个固定习惯不管数据集描述得多漂亮先做类别分布和分组盘点再谈训练。这个顺序帮我躲过了好几次因数据泄露和背景过拟合导致的整体返工。希望帮到你。本文还有配套的精品资源点击获取
返回列表