尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

真实废弃物图像分类数据集实战:用ResNet18训练垃圾分类模型

真实废弃物图像分类数据集实战:用ResNet18训练垃圾分类模型 简介面向废弃物识别与图像分类研究的一套真实场景物品图像数据集共约四千八百张已标注图片覆盖纸板、食品有机物、玻璃、金属、杂项垃圾、纸张、塑料、纺织品垃圾与植被九个类别。资源围绕分类任务数据经过预处理可直接作为分类网络输入按训练集、测试集划分并各自存放同类图片便于对比与调优。压缩包内含两千个文件以一千九百九十八张 jpg 图像为主体同时提供一个 Python 可视化脚本与一个 json 类别配置文件整体大小约一百五十六 MB目录结构一目了然。运行 show 脚本即可快速预览各目录样本json 文件可查看类别与文件对应关系。目前已有六十七人学习适合正在做垃圾分类、目标检测或图像分类项目的研究者与学生也适用于图像分类、分割网络改进等实践场景可直接用于模型训练和结果验证。1. 一张真实废弃物照片数据集的含金量不是每个垃圾数据集都能直接用做图像分类的人最怕两件事数据集干净得不像话模型在玩具场景里跑得欢一上真实环境就翻车以及数据本身没问题但没整理、没标注光清洗就得耗掉两周工期。这份“生活中真实废弃物图像分类数据集”约4800张已标注照片覆盖纸板、食品有机物、玻璃、金属、纸张、塑料、纺织品、植被和杂项垃圾9类常见废弃物训练集和测试集已经划分好类别信息写在JSON里还附带可视化脚本属于解压后就能直接喂给分类网络的那类资源。它的价值集中在“真实”两个字上——照片来自日常场景光照、角度、遮挡、混放都不规整模型在这种数据上练过再上摄像头部署会稳得多。适合刚入门图像分类、想做垃圾分类落地但不想从零折腾数据的人。2. 数据集结构9类近4800张样本是怎么组织起来的拿到数据集第一件事不是开训而是把目录结构、JSON、文件名规律摸一遍。这个数据集不算大但真实场景样本的坑往往就藏在文件夹层级和标注格式里。下面几个信息点是我每次拿到同类资源都会先确认的。2.1 先看JSON9类垃圾的类别体系与标注格式摘要里明确写了“分类个数9”具体类别以JSON为准。打开JSON核心就两件事类别名以及每张图片和类别之间的映射关系。文件里通常存的是英文键对应的中文含义可以整理成下面这张表。JSON类别键中文对应典型样本cardboard纸板快递箱、瓦楞纸板、牛奶盒food organics食品有机物菜叶、果皮、剩饭glass玻璃啤酒瓶、玻璃罐、碎玻璃metal金属易拉罐、金属瓶盖miscellaneous trash杂项垃圾口罩、烟蒂、混合垃圾袋paper纸张报纸、打印纸、纸袋plastic塑料塑料瓶、塑料袋、外卖餐具textiles纺织品垃圾旧衣物、毛巾、碎布vegetation植被落叶、树枝、修剪下的草叶我见过的JSON结构大致有三种第一种是{类名: [图片文件名列表]}第二种是[{file: ..., label: plastic}]第三种是{images: [...], annotations: [...]}的检测格式。对分类任务来说不管哪一种最终都要能从JSON反查出“某张图属于哪一类”然后转成两个对齐的数组paths和labels。拿到文件后先用命令行验证一下类别分布python -c import json; djson.load(open(labels.json, encodingutf-8)); print({k: len(v) for k, v in d.items()})逻辑说明这段命令假设labels.json是最外层字典键是类别名值是该类别下的图片文件名列表一次性打印每类样本数能快速看出类别是否均衡。参数说明encodingutf-8在 Windows 下必须加否则中文注释或扩展字符可能触发UnicodeDecodeError。如果JSON实际结构是列表这段命令会报错那就先print(d[:2])看前两条长什么样再调整解析方式。另外从图片文件名也能看出一些规律。资源里的图片大量采用Vegetation_110.jpg、Vegetation_75.jpg这类命名前缀是类别英文名后面是序号。这意味着即使不打开JSON用一条find命令也能按文件名前缀统计各类数量。不过文件名有时会和标注不一致我一般还是以JSON为准文件名只做参考。2.2 训练集测试集已划分别再自己随意抽样这个数据集最省事的地方是把train和test两个集合提前切好了而且各自存同一类数据图片。做分类实验时最忌讳的就是训练测试从同一个大目录里随机抽因为同一场景、同一物品的照片很可能同时进训练和测试网络记住了物品本身而不是类别特征测试准确率看起来很高一出实验室就露馅。既然资源已经划分好基本原则是先用现成的划分跑通baseline之后做交叉验证时也以原划分为准不要轻易合并重抽。拿到手先数一下两边样本量find dataset/train -name *.jpg | wc -l find dataset/test -name *.jpg | wc -l逻辑说明find递归找出所有jpg文件wc -l统计行数得到训练和测试各自的图片总数。参数说明如果图片后缀有.jpeg或.png把*.jpg换成对应后缀即可。这里有一个需要留意的点随机划分只能保证类别比例大致一致不能保证“同一物品不出现在两边”。真实废弃物数据集里同一个饮料瓶可能既在训练集又在测试集只是文件名不同很难靠肉眼发现。后面第5章会专门讲这个问题。2.3 预处理到什么程度不是每张图都统一尺寸摘要写“数据经过预处理可以直接作为分类网络输入使用”。这句话容易被高估。它通常不意味着每张图已经是224×224且做好了归一化而是指图片文件完整、路径里没有中文和非法字符、标签与图片一一对应、没有损坏到打不开的图。真实废弃物照片来自不同手机和相机尺寸大概率不齐通道数偶尔有意外EXIF旋转也常见。先用一段探路脚本把图片尺寸和通道模式统计出来import os from collections import Counter from PIL import Image root dataset/train size_counter Counter() mode_counter Counter() for cls_name in os.listdir(root): cls_path os.path.join(root, cls_name) if not os.path.isdir(cls_path): continue for img_name in os.listdir(cls_path): img_path os.path.join(cls_path, img_name) with Image.open(img_path) as img: size_counter[img.size] 1 mode_counter[img.mode] 1 print(尺寸分布, size_counter.most_common(8)) print(通道模式, mode_counter)逻辑说明遍历train目录下的所有类别子文件夹用PIL打开每张图统计(宽, 高)和颜色模式结果用Counter汇总。参数说明root按实际解压路径改mode为RGB表示正常三通道图RGBA需要转成RGBL是灰度图分类网络不认必须在读取时转换。这个统计直接决定后面transform里Resize怎么写如果尺寸集中在3:4或4:3可以统一Resize((256, 256))如果很乱就直接用RandomResizedCrop(224)让网络自己去适应。3. 可视化先走一遍看懂show脚本的用途与自己写一个下载资源里附带show脚本目的就一个让你在训练之前先把4800张图实际看一遍。这一步看着简单却能躲掉后面大半标注错位的坑。3.1 show脚本的常规行为按类抽样画大图常见做法是把每一类随机抽几张图用matplotlib拼成一个大网格每行放一个类别左侧或顶部注明类名。运行完会生成一张类似data_preview.png的图9类样本一眼扫完。如果脚本支持命令行参数参数一般长这样python show.py --data_dir dataset --json labels.json --num_samples 8 --output grid.png参数说明--data_dir指向包含train和test的上级目录--num_samples是每类抽几张图建议3到8张--output是输出文件名。数值太小时看不到类内多样性太大时每张缩略图太小看不清细节。具体脚本内部怎么实现取决于原作者习惯但核心行为基本一致。3.2 自己写一个show脚本最小可用的数据预览如果自带脚本跑不起来或者想加自己的过滤逻辑自己写一个也就几十行。下面这段是我常用的最小实现import os import json import math import matplotlib.pyplot as plt from PIL import Image data_dir dataset/train label_file labels.json with open(label_file, r, encodingutf-8) as f: labels json.load(f) class_names list(labels.keys()) num_classes len(class_names) samples_per_class 3 cols 5 rows math.ceil(num_classes * samples_per_class / cols) fig, axes plt.subplots(rows, cols, figsize(cols * 2.5, rows * 2.5)) axes axes.flatten() for i, cls in enumerate(class_names): file_list labels[cls] picked file_list[:samples_per_class] for j, fname in enumerate(picked): img_path os.path.join(data_dir, cls, fname) img Image.open(img_path).convert(RGB) ax axes[i * samples_per_class j] ax.imshow(img) ax.set_title(cls, fontsize8) ax.axis(off) plt.tight_layout() plt.savefig(data_preview.png, dpi150) print(saved to data_preview.png)逻辑说明先把JSON读成字典类名作为class_names每个类取前三张图按每行5列排布生成一个大网格读取图片时统一.convert(RGB)避免灰度图或RGBA图导致绘图报错。参数说明samples_per_class建议1到4之间太多则单张面积过小cols取3或5都行但要让总列数能整除num_classes * samples_per_class除不尽时就多出空白子图不影响使用。3.3 可视化时要看三个问题类间相似、类内多样、错误标注看预览图不能只看个热闹要带着问题看。第一是类间相似度食品有机物和植被在颜色上高度接近纸张和纸板在纹理上也容易混如果预览图里这两类彼此长得很像训练时就要特别注意。第二是类内多样性如果某一类抽出来的照片全是同一角度、同一背景说明数据来源单一模型上线后遇到新场景容易崩。第三是错误标注真实数据集人工标注时确实存在把落叶标到食品有机物、把纸盒标到纸张的情况。看到可疑样本先记下来训练时作为噪声样本处理。4. 直接开练用PyTorch把ResNet18训成垃圾识别模型数据结构和可视化都确认过之后就可以进入训练环节。这里选PyTorch和ResNet18的组合理由是小数据集、9分类、真实场景噪声多ResNet18容量适中不容易一上来就过拟合训练速度也快几分钟能出一个baseline。4.1 从目录到DatasetImageFolder与transform设置既然训练集和测试集都是“一个类别一个子文件夹”的布局PyTorch自带的ImageFolder是最省事的加载方式。它会把子文件夹名自动映射成类别索引import torch from torchvision import datasets, transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) test_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset datasets.ImageFolder(dataset/train, transformtrain_transform) test_dataset datasets.ImageFolder(dataset/test, transformtest_transform) train_loader torch.utils.data.DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) test_loader torch.utils.data.DataLoader(test_dataset, batch_size32, shuffleFalse, num_workers4)逻辑说明ImageFolder扫描每个子目录把目录名当作类别自动生成class_to_idx。训练集用RandomResizedCrop(224)做随机裁剪增强网络对不同比例、不同位置的适应性测试集只用CenterCrop保证评估时每张图都被确定性地处理。参数说明mean和std直接用ImageNet预训练模型的标准值微调时不建议改num_workers在Windows上设0或2即可设太大会报DataLoader worker相关错误batch_size按显存来6GB以下建议16。4.2 加载预训练ResNet18并替换分类头9类输出9分类任务不需要从零初始化网络直接加载ImageNet预训练权重只把最后一层全连接换成9维输出import torchvision.models as models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) in_features model.fc.in_features model.fc torch.nn.Linear(in_features, 9)逻辑说明model.fc原本输出1000类先取出它的输入维度in_features再替换成输出9的全连接层。前面的卷积层参数全部保留这些层学到的边缘、纹理、形状特征对废弃物识别同样有效比自己从头训练省几十倍时间。参数说明IMAGENET1K_V1是PyTorch里官方预训练权重的版本标识如果显存够也可以换resnet50但4800张图用ResNet50容易过拟合ResNet18更适合作为baseline。4.3 训练循环与超参数lr怎么设、epoch怎么定训练代码直接套标准流程criterion torch.nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr3e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) epochs 30 for epoch in range(epochs): model.train() running_loss 0.0 for images, targets in train_loader: optimizer.zero_grad() outputs model(images) loss criterion(outputs, targets) loss.backward() optimizer.step() running_loss loss.item() scheduler.step() print(fepoch {epoch 1}/{epochs}, loss {running_loss / len(train_loader):.4f}) torch.save(model.state_dict(), waste_resnet18.pth)逻辑说明每个epoch把训练集完整过一遍累计loss反向传播更新参数一个epoch结束后更新学习率30轮结束后把模型权重存成本地文件。参数说明lr3e-4在小数据集微调时比默认1e-3稳不容易一开始就震荡momentum对Adam可省weight_decay1e-4能压一点过拟合但不是越大越好T_max30要和epochs一致余弦退火才会把学习率刚好降到最低点。训练时重点看loss曲线如果loss在前5轮明显下降说明预训练权重起了作用如果loss不但没降反而升高优先检查transform里有没有对图像做错归一化或者batch_size是否太小导致梯度噪声过大。下面是我在类似小数据集上常用的超参数组合超参数建议值说明lr3e-4预训练微调稳batch_size32显存小改16epochs30小数据不易过久optimizerAdam / AdamWAdamW防过拟合更好schedulerCosineAnnealingLR收尾阶段更平滑weight_decay1e-4轻量正则4.4 评估脚本测试集准确率与混淆矩阵训练完必须到测试集上评估才看得见真实水平model.load_state_dict(torch.load(waste_resnet18.pth)) model.eval() correct 0 total 0 all_preds [] all_targets [] with torch.no_grad(): for images, targets in test_loader: outputs model(images) preds outputs.argmax(dim1) correct (preds targets).sum().item() total targets.size(0) all_preds.extend(preds.tolist()) all_targets.extend(targets.tolist()) print(ftest accuracy {correct / total:.4f})逻辑说明加载保存好的权重把模型切到评估模式关闭梯度计算整批预测后统计预测正确数占总样本数的比例。参数说明model.eval()必须在测试前调用它会关闭Dropout和BatchNorm的训练行为否则结果不稳定argmax(dim1)沿类别维度取最大值索引得到每个样本的预测类别。只看整体准确率还不够。真实废弃物数据类别不平衡准确率可能虚高。想要细看每类的precision和recall可以用混淆矩阵from sklearn.metrics import confusion_matrix, classification_report cm confusion_matrix(all_targets, all_preds) print(classification_report(all_targets, all_preds, target_namestest_dataset.classes))逻辑说明confusion_matrix输出9×9矩阵行是真实类别列是预测类别对角线越亮说明越准classification_report按类别列出precision、recall、f1-score。参数说明target_names使用test_dataset.classes它就是ImageFolder扫描到的子目录名列表保证输出顺序一致。看到某两类互相混优先回到预览图确认这两类是否在视觉上确实难以区分。5. 避坑与常见问题真实废弃物数据的五个翻车点真实场景数据集跟实验室数据集最大的区别就是到处是意外。这一章把最常见的问题集中写出来每个都按现象、原因、解决的顺序拆方便直接对照处理。5.1 类别不平衡准确率虚高小类直接躺平现象训练完测试集准确率有七成多但看混淆矩阵发现玻璃和纺织品几乎全是错的模型把所有样本都偏向纸板、植被这类数量多的类别。原因9个类别的样本量并不均匀某个类可能只有另一类的三分之一CrossEntropyLoss对所有类一视同仁模型自然优先学多数类。解决给CrossEntropyLoss传class_weight或使用WeightedRandomSampler让每类每个epoch被抽到的次数接近。先说方案一class_counts torch.bincount(torch.tensor(train_dataset.targets)) class_weights 1.0 / class_counts.float() class_weights class_weights / class_weights.sum() criterion torch.nn.CrossEntropyLoss(weightclass_weights)逻辑说明统计训练集每个类别的样本数取倒数后归一化样本越少的类权重越大这样少数类的loss在反向传播时被抬高。参数说明train_dataset.targets是ImageFolder自动生成的类别索引列表如果某个类别样本数太少权重会很大训练时会轻微震荡这是正常现象。5.2 同一物品同时出现在训练集和测试集泄漏现象测试集准确率到了90%但把模型拿到现场拍几张新照片立刻掉到六成。原因数据集虽然按文件做了随机划分但真实废弃物照片中同一个瓶子、同一张纸板可能在多个角度出现随机划分时它们的一部分进了训练集、一部分进了测试集。这类泄漏在数据量小时特别隐蔽。解决先做一个快速检查把训练集和测试集的文件名按数字后缀排序找是否有同前缀、相邻序号的图片同时出现在两边。另一个办法是不要迷信原测试集自己额外收集一批现场照片做验证那才是真实部署水平的体现。5.3 食品有机物和植被互相混难分样本拖低指标现象混淆矩阵里food_organics和vegetation互相错边界上的样本谁来了都分不清。原因落叶和菜叶在颜色、纹理上高度相似人工标注时本身就存在主观判断误差真实场景里落叶掉进餐盒的情况也常有。解决把模型预测置信度在0.4到0.7之间的样本导出单独看一遍人工复核比对着整批数据找错误快得多。如果确认是标注噪声可以先从训练集里剔除或修正而不是一味加数据增强。5.4 中文路径和JSON编码问题Windows下最常见现象代码在本地跑着跑着突然FileNotFoundError或者json.load报UnicodeDecodeError。原因数据集解压到了含中文的路径或者JSON文件编码不是默认的UTF-8。Windows下这个坑尤其频繁。解决第一整个项目放在纯英文路径比如D:/waste_dataset不要放在桌面/垃圾分类/数据集第二所有open()都显式加encodingutf-8第三图片路径统一用pathlib.Path拼接避免手拼字符串时出现斜杠和转义问题。这些小改动看着琐碎能省掉大量调试时间。5.5 数据增强过猛训练集被“毁容”测试时崩盘现象训练loss下降很漂亮测试集准确率反而很差。原因数据增强不是越多越好。RandomResizedCrop尺度调太小时训练图可能只截到垃圾的一小块比如一个瓶盖颜色抖动太强时白平衡完全失真网络学到的是人造的颜色偏移。解决真实废弃物场景里先用基础增强组合RandomResizedCrop(scale(0.5, 1.0)) RandomHorizontalFlip(p0.5)等确认过拟合再逐步加ColorJitter。加一步看一步不要一次全开。6. 把baseline从72%提到85%的三种调法增强、换骨干、调阈值ResNet18跑出的baseline通常能用但要拿到实际项目里还有三种很实在的提点手段按投入产出比排序。6.1 按真实场景设计数据增强基础增强之外真实废弃物场景最缺的是光照变化。便利店冷白灯、傍晚暖黄灯、阴天偏暗都可能让模型在现场失手。我一般会加ColorJitter(brightness0.3, contrast0.3, saturation0.2)但会控制强度太强会把塑料瓶的白色高光直接抹掉。训练时只在对训练集增强测试集保持CenterCrop这一点不要图省事混用。6.2 换骨干网络从ResNet18到EfficientNet-B0ResNet18是baselineEfficientNet-B0在同样量级下准确率通常高1到3个点推理速度也没慢多少。切换代码量很小from torchvision.models import efficientnet_b0, EfficientNet_B0_Weights model efficientnet_b0(weightsEfficientNet_B0_Weights.IMAGENET1K_V1) in_features model.classifier[1].in_features model.classifier[1] torch.nn.Linear(in_features, 9)逻辑说明EfficientNet-B0最后一层叫classifier序列里第二个是全连接层把它替换成9维输出。参数说明如果显存允许EfficientNet-B1、B2也可以试但这套数据只有4800张再大的模型就开始吃样本量了未必划算。6.3 置信度阈值低置信度预测直接拦截现场应用和跑实验不同分错一袋垃圾的影响远大于“不识别”。推理时加一个阈值判断probs, preds torch.softmax(outputs, dim1).max(dim1) mask probs 0.7 final_preds preds.clone() final_preds[mask] -1 # -1 表示无法判断逻辑说明先取每个样本的最大softmax概率和对应类别概率低于0.7的样本把预测结果置为-1交给人工或让摄像头重新拍照。参数说明阈值0.7不是固定的可以通过在验证集上扫[0.5, 0.6, 0.7, 0.8, 0.9]选一个不牺牲过多准确率的点。这个做法在垃圾分类项目里很常见宁可说“不知道”也不要硬猜。从那以后我每次拿到新数据集都强制自己先跑一遍可视化脚本和类别平衡统计再谈训练。这几个步骤加在一起用不了十分钟却避开了绝大多数白费功夫的调参。真实废弃物数据本来就乱先把数据和标注摸透模型才不会被垃圾输入带偏。希望这份梳理能帮你在垃圾分类项目上少走几步弯路。本文还有配套的精品资源点击获取
返回列表