简介:生活中真实废弃物图像分类数据集,面向图像分类、目标检测与分割领域的开发者和学生,提供约四千八百张已标注的原始生活场景垃圾图像。压缩包内含两千个文件,以一千九百九十八张JPG图像为主体,另附一个Python可视化脚本和一个JSON类别配置文件,整体约一百五十六兆。数据涵盖纸板、食品有机物、玻璃、金属、杂项垃圾、纸张、塑料、纺织品垃圾和植被九类,并已划分训练集与测试集,各类图像单独存放;图像经过预处理,可直接作为分类网络输入,也可运行show脚本快速查看样本分布。已有六十七人学习下载。这份数据集既可直接用于垃圾分类模型训练与算法对比,也可为数据增强、类别不平衡等研究提供贴近真实场景的实验素材。
1. 拿到 4,800 张已标注废弃物照片时,先别急着写网络
处理过几轮真实项目之后,我越来越确认一件事:一份整理干净的生活中真实废弃物图像分类数据集,哪怕只有约 4,800 张已标注图片,也比网上爬来的几万张杂乱图片耐用得多。真实废弃物场景长得“脏”——同一个矿泉水瓶,在办公室灯光、户外阴影、塑料袋褶皱里的颜色和纹理完全不同,合成图或精修图骗得过人眼,骗不过模型。这类数据集恰好解决从图像分类算法学到能放进小程序或边缘设备那一段最难的转化:让网络在真实噪声里学习废弃物本身的特征,而不是去背背景。适合正在做毕业设计、想快速验证垃圾分类或废品识别原型的人,也适合刚接触图像分类、想用一份靠谱数据把迁移学习全流程走通的人。后面我把数据清洗、训练参数、评估口径和踩坑细节一次讲完。
2. 数据管线:目录规范化、标注校验与去重这一关,决定后面所有指标
拿到任何标注好的图像分类数据集,最不该做的第一件事就是直接开训练。尤其是真实废弃物这类照片——拍摄器材不一、光照忽亮忽暗、背景从厨房台面到垃圾袋都有,标注和原始文件的“脏”程度往往超出想象。我之前接过一份号称 3,000 张的回收物数据,清洗完只剩约 2,700 张能用,这还是在只去除重复和损坏图的前提下。所以先把数据管线走扎实,模型后面才不玄学。
2.1 先盘点标注形式:文件夹命名是标签,先统一成 ImageFolder 布局
已经标注的数据集,标注形式通常是两种:要么图片按类别文件夹存放,要么是一张 CSV 或 JSON 记录每张图的路径和标签。生活中真实废弃物图像分类,绝大多数采用前者。第一步不是改代码,而是先盘点根目录下到底有几个类、每类多少张、文件名是否带中文或空格。后者会影响跨平台脚本和 DataLoader 路径解析,常见做法是先把这批文件统一重命名成 img_0001.jpg 这样的安全格式,再转入 ImageFolder 标准布局。
用一句 bash 就能完成初步统计。先确认文件总数和标注说明对得上:
find raw_images -type f \( -name "*.jpg" -o -name "*.jpeg" -o -name "*.png" \) | wc -l再按子目录列出每个类别的图片数量:
for d in raw_images/*/; do echo "$(basename "$d") $(find "$d" -type f | wc -l)"; done这个循环会把每个类别的图片数列出来,用来快速发现某个类别只有十几张的极端情况。一个分类项目如果存在只有个位数样本的类,不管后面做多么漂亮的增强,这个类都只能靠运气,优先要决定是合并进相近大类还是补数据。这两条命令本身很简单,真正的价值在于把“通读数据”变成强制步骤。任何语言任何框架,在写 DataLoader 之前先打印一遍类别分布,后面所有决策才有依据。
2.2 哈希去重与训练/验证切分:防数据泄露的第一道闸
真实废弃物数据集的“重复”有两种:第一种是完全相同的文件,可能是别人打包时重复放的;第二种是同一个瓶子在不同角度、不同背景下的连拍,内容相似但像素不同。第一种用 MD5 去重就好处理,第二种在图像分类里更阴险——它会让同一物体同时出现在训练集和验证集,造成验证指标虚高,这就是常见的数据泄露。
用 Python 做 MD5 去重的逻辑很直接:
import hashlib from pathlib import Path def dedupe_by_md5(root: Path): seen: dict[str, Path] = {} duplicates = [] for p in root.rglob("*.[jJ][pP][gG]"): digest = hashlib.md5(p.read_bytes()).hexdigest() if digest in seen: duplicates.append((p, seen[digest])) else: seen[digest] = p return duplicates逻辑说明:遍历目录下所有 jpg 文件,逐个计算文件字节的 MD5 值;第一个遇到某个值的文件被保留,后续相同文件会被记录到 duplicates 列表里。实际使用不要直接删除,先把 duplicate 列表存成 CSV,人工扫一眼是不是真的重复,避免脚本误判不同尺寸但同一内容的图片。
MD5 只解决完全复制。针对连拍造成的“近似重复”,更实际的办法是抽样看一遍:每类随机抽 20 张拼成一张网格图,用肉眼看是不是同一件物品在多个角度反复出现。很多血泪经验表明,这一步比任何算法都省钱。如果发现一堆同一个易拉罐不同角度的照片,做切分时要把这批连拍归到同一个集合——按拍摄时间或文件名前缀分组再切,而不是按单张图片随机切。
切分用 stratify 才能保证小类别不消失:
import random from pathlib import Path from sklearn.model_selection import train_test_split paths = list(Path("data/waste_clean").rglob("*.jpg")) labels = [p.parent.name for p in paths] train_paths, tmp_paths, train_labels, tmp_labels = train_test_split( paths, labels, test_size=0.2, stratify=labels, random_state=42 ) val_paths, test_paths, val_labels, test_labels = train_test_split( tmp_paths, tmp_labels, test_size=0.5, stratify=tmp_labels, random_state=42 )参数说明:test_size=0.2 先切出 20% 作暂存,再把这 20% 对半切成验证集和测试集,最终得到 80/10/10 的划分;stratify 保证每个类别在所有集合里的占比与原始分布一致;random_state 固定,让同数据在同脚本下可复现,不会每次跑都是随机结果。按 4,800 张计算,训练集约 3,840 张,验证和测试各约 480 张,足够支撑一个图像分类算法的迁移学习实验。
2.3 增强策略别照搬通用库:真实废弃物要的是“弱增强”
网络上大量图像分类教程给的增强配方是随机旋转 90 度、裁剪 scale 到 0.08、大范围色差抖动。这套配方用在 CIFAR 或者通用物体上没问题,但用在真实废弃物分类上会让模型学歪——废弃物是有“方向性”的:矿泉水瓶上的标签文字、罐体的拉环、纸盒的印刷面向,都是分类时的强判别特征,把它旋转 90 度或裁掉一半,模型就只能去记纹理背景。
我一般给真实废弃物场景用这组轻增强,既能泛化又不毁特征:
from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomResizedCrop(size=224, scale=(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.1), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])参数说明:scale 下限设为 0.6 而不是常见的 0.08,是为了避免把瓶子的一小块局部裁剪成大特写,那样学到的不是“废弃物类别”而是“纹理碎片”;水平翻转对瓶子、纸盒是合理的,不会改变类别语义;亮度和对比度抖动控制在 0.2,因为真实废弃物在不同光照下颜色差异极大,抖动太小模拟不了白天黑夜,抖动太大会把金属和玻璃的反光变成噪声。
提示:验证集和测试集只用 ToTensor 和 Normalize,绝不能用随机增强,否则验证指标每次跑都不一样。
这类数据的增强本质上是在“让模型见更多光照”和“别毁掉关键特征”之间找平衡。数据量只有 4,800 张时,多试几组 scale 和旋转角度的组合,比盲目堆增强操作更值得。
3. 用迁移学习在 4800 张上跑图像分类算法:模型、损失与关键超参
3.1 模型选型:EfficientNet-B0 为什么比大模型更划算
图像分类算法的选型在 4,800 张规模下不是“越大越好”。ResNet50 大约 2,500 万参数,EfficientNet-B0 只有约 530 万参数,但在用 ImageNet1k 预训练权重做迁移学习时,B0 的 top-1 准确率并不输 ResNet50,推理速度快出不少;MobileNetV3-Large 再轻一档,适合最终要跑在手机端的场景,但训练时容量偏小,在瓶罐和纸类这种细粒度区分上会吃力。实践经验是:主力用 EfficientNet-B0,拿 ResNet50 跑一次对照,如果两者验证集 top-1 差距在 1% 以内,就选快的那个。
在 4,800 张上下,从零训练任何现代 CNN 都会严重过拟合。正确做法是用 ImageNet1k 的预训练权重做迁移学习,即把除最后一层分类器之外的所有层都当作特征提取器,只在废弃物类别上微调。因为 ImageNet 本身就包含大量瓶、罐、包装盒照片,这些低中层的边缘、纹理、颜色特征可以直接复用。
3.2 训练脚本与参数:AdamW、余弦退火、类别权重的正确配置
训练脚本用 PyTorch 写,核心配置如下:
import torch from torch import nn from torch.utils.data import DataLoader, WeightedRandomSampler from torchvision import datasets, transforms from torchvision.models import efficientnet_b0, EfficientNet_B0_Weights from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR train_ds = datasets.ImageFolder("data/waste_clean/train", transform=train_transform) val_ds = datasets.ImageFolder("data/waste_clean/val", transform=val_transform) # 按训练集标签统计每类样本数,换算出反比类别权重 counts = torch.bincount(torch.tensor(train_ds.targets)) class_weight = counts.float().reciprocal() class_weight = class_weight / class_weight.sum() * len(counts) # 替换最后一层分类器,输出维度等于废弃物类别数 model = efficientnet_b0(weights=EfficientNet_B0_Weights.IMAGENET1K_V1) model.classifier[1] = nn.Linear(model.classifier[1].in_features, len(counts)) loss_fn = nn.CrossEntropyLoss(weight=class_weight, label_smoothing=0.1) optimizer = AdamW(model.parameters(), lr=1e-3, weight_decay=0.01) scheduler = CosineAnnealingLR(optimizer, T_max=30) for epoch in range(30): model.train() for x, y in train_dl: x, y = x.to(device), y.to(device) out = model(x) loss = loss_fn(out, y) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() # 每轮结束在 val_dl 上算一次 top-1,保存最优权重逻辑说明:从 ImageFolder 构造数据集后,用训练集的标签统计每类样本数,换算成反比权重传给交叉熵;model.classifier[1] 是 B0 最后的全连接层,替换成自定义类别数;训练循环里按标准流程做前向、算损失、反向、更新,每轮推进一次学习率调度。这个脚本只保留核心骨架,实际工程要补上断点续训和最优权重保存。
参数说明:lr=1e-3 配合 AdamW 是微调阶段的稳妥起点,如果验证集 top-1 在头 5 轮不涨,把 lr 降到 3e-4 再重跑;weight_decay=0.01 控制权重衰减,防止 4,800 张这种小数据上过拟合;label_smoothing=0.1 让模型不要对训练标签过于自信,废弃物图片里本身有大量标注噪声,平滑系数能换回一点泛化;T_max=30 要和总 epoch 数一致,余弦退火结束时学习率正好落到最低点。
一个值得注意的细节是,类别权重是加在损失函数上还是采样器上,效果不一样:损失权重适合样本量差距在 3 倍以内的不平衡,差距超过 10 倍时,要换 WeightedRandomSampler 让每个 batch 都见到小类别样本。
batch_size 在 4,800 张规模下不用太大,32 足够收敛,显存吃紧就降到 16;num_workers 按 CPU 核数一半设置,Windows 下要放到 ifname== "main" 里,否则多进程加载会报错。
3.3 训练时怎么观察:loss 曲线和验证集的两条线
第一次跑这类小数据集,不要盯每一步的 loss 值,要盯两条曲线的关系:训练 loss 和验证 top-1。如果训练 loss 稳步下降而验证 top-1 在 5 轮之后开始震荡甚至走低,说明过拟合已经在 4,800 张规模上提前出现,优先提高增强强度或加 dropout,而不是换模型。反之,如果两个集合的指标一起停在原地,说明学习率太低或预训练权重没被解锁,先把特征提取层解冻到后一半层。
还有一种常见怪象:验证 top-1 在 0.9 左右震荡但训练 loss 不降。这通常不是优化器的问题,而是标注本身有脏数据——某些类别被标错,模型在训练集里学的“正确答案”和验证集冲突。这时候去看损失最高的那批训练样本,把疑似标注错误的图挑出来复核。这是数据标注质量问题和模型问题最容易混淆的地方,能用可视化解决就不要盲目调参。
4. 评估不只看准确率:混淆矩阵、宏平均与 ONNX 部署
4.1 混淆矩阵比 accuracy 诚实:真实废弃物里谁和谁在混淆
一个垃圾分类模型在 4,800 张数据上训练出来的总体 accuracy 很容易做到 0.92 以上,但这个数字会骗人。真实废弃物类别分布极不均衡:塑料瓶可能有 1,200 张,电池只有 60 张;模型只要把所有不确定样本都预测成塑料瓶,accuracy 就不会太低,但这个模型在真实场景里根本没有可用性。所以评估的第一张表不是 accuracy,而是混淆矩阵。
import matplotlib.pyplot as plt import seaborn as sns from sklearn.metrics import confusion_matrix, classification_report cm = confusion_matrix(all_labels, all_preds, labels=class_names) plt.figure(figsize=(10, 8)) sns.heatmap(cm, annot=True, fmt="d", xticklabels=class_names, yticklabels=class_names) plt.xlabel("Predict") plt.ylabel("True") plt.tight_layout() plt.savefig("confusion_matrix.png") print(classification_report(all_labels, all_preds, target_names=class_names, digits=3))逻辑说明:把所有测试图片的真实标签和预测标签收集成两个列表,confusion_matrix 按类别名生成方阵;heatmap 把数字画成热力图,一眼就能看出哪两类之间颜色最深。classification_report 会同时输出每个类的 precision、recall、f1-score,以及 macro avg 和 weighted avg。这张图的价值在于,它能直接告诉你“这个模型到底哪里不行”。
真实废弃物场景里最常见的混淆对是:玻璃瓶和塑料瓶、易拉罐和金属罐头、纸箱和纸板、陶瓷和玻璃。它们视觉差异本来就小,加上拍摄角度和反光,人能看出区别,模型却很难。如果你在混淆矩阵里看到某两类交叉项特别高,下一步不是调参,而是先确认类别定义是否合理:业务上能不能合并?不能合并的话,要不要给这两类单独做二阶段细粒度分类器?
4.2 宏平均与加权平均的 gap,暴露多数类偏好
只看 classification_report 里的 weighted avg 也会被多数类带偏。重点看 macro avg 和 weighted avg 之间的差值:两个数字差距越大,说明模型对小类别越不友好。比如 weighted avg F1 是 0.90,macro avg F1 只有 0.78,那 0.90 基本是塑料瓶这类大类撑起来的,电池、陶瓷这类小类别的真实表现远没有报表好看。
修这个 gap 有三种常用手段。第一种是之前提到的类别权重和重采样,让训练时小类别的梯度贡献更大。第二种是给大类做负例抑制:小类别 recall 过低时,去训练集里把被误分为大类的样本找出来,当成难例重新训练。第三种最务实——增加小类别的真实照片。数据增强在小样本类上只能延缓过拟合,不能无中生有地创造真实光照下的特征。一个类别低于 100 张时,先别折腾模型,补数据才是正路。
注意:模型在测试集上的指标只代表“同源数据”下的表现,真实场景泛化能力必须用全新采集的样本来验证。
4.3 导出 ONNX 到边缘设备前,先过一遍真实场景测试集
模型在测试集上表现不错,不代表可以在真实场景里用。原因是测试集和训练集来自同一批原始采集,背景、拍摄器材、标注口径都一致,模型可能偷偷学会了这套采集环境特有的纹理。所以在部署之前,我习惯用手边真实拍摄的照片做一次冒烟测试:办公室废纸篓、厨房台面、阳台角落,各拍十来张,用一个独立的脚本跑推理,这个测试集不进训练、不进验证、不参与任何超参选择。
确认能过冒烟测试后,导出 ONNX:
import torch.onnx model.eval() dummy = torch.randn(1, 3, 224, 224, device="cuda") torch.onnx.export( model, dummy, "waste_cls.onnx", input_names=["input"], output_names=["logits"], dynamic_axes={"input": {0: "batch"}, "logits": {0: "batch"}}, opset_version=17 )参数说明:dynamic_axes 让导出的模型支持动态 batch,边缘设备上既能单张推理又能批量处理;opset_version=17 对应新版 ONNX Runtime 和大部分推理框架,太低会缺算子,太高在老旧设备上可能不受支持。导出后用 onnxruntime 加载模型,对刚才的真实场景照片跑一次,对比 PyTorch 结果有没有差异。浮点版本验证无误后,再考虑 int8 量化,这一步能省一半内存和推理时间,但会让精度掉 1% 到 3%,必须在真实场景测试集上重新量化验证。
5. 避坑记录:脏标注、数据泄露、增强过猛,这些坑我全踩过
真实项目里最花时间的往往不是模型,是这些绕不开的坑。我列了五条自己的翻车记录,每条都是现象、原因、解决三连,给正要入手的你提前做个心理准备。
5.1 验证集 0.93,真实场景 0.71:数据泄露的黑匣子
现象:训练结束,验证集 top-1 到 0.93,我拿手机拍了十几张办公室垃圾照片一测,掉到 0.71,完全不可用。原因:原始数据集里同一个饮料瓶在不同角度连拍了十几张,随机切分时这些连拍被拆进了训练集和验证集。模型在验证集里见到的“新瓶子”,其实是训练时见过的同一个瓶子,指标自然虚高。解决:先做 MD5 去重去掉完全重复文件,再把连拍按文件名前缀或拍摄时间分组,整体切进同一边。最保险的办法是最后用独立采集的全新场景照片做验收。数据泄露不靠模型调参能救回来,只能在数据切分这层建好闸。
5.2 电池只有 60 张,权重调出幻觉
现象:某次实验里电池类在训练集只有 60 张,我在损失函数上把电池的权重调得很高。结果训练 loss 一路走低,验证时电池 recall 从 0.1 涨到 0.3,precision 却掉到 0.2——模型把大量塑料瓶预测成电池。原因:权重太高让模型“宁可错杀也不放过”,代价是牺牲多数类的准确率。解决:类别权重只适合样本量差距在 3 倍以内的不平衡,60 张对大几百甚至上千张的类,先补数据比调权重重要。如果短期补不了,把相近类别合并成“电池/蓄电池”,增加每类样本数再训,指标反而更真实。现在我知道,类别不平衡是数据问题,不是损失函数问题,把精力花在补数据和合并类别上。
5.3 旋转 90 度和过强裁剪,把关键特征裁掉了
现象:训练 loss 降到 1.2 之后怎么都降不下去,验证准确率在 0.85 上下震荡。看预测错误的样本,发现矿泉水瓶的标签被随机裁剪裁掉了,模型只能看到一块半透明的塑料纹理。原因:增强设得太猛。Rotate 90 度和 scale 0.08 是通用数据集的配方,但废弃物图片的判定极度依赖瓶身文字、拉环、密封圈这类局部细节,把它们旋转或裁掉,模型等于在学“反光噪声”。解决:把仿射变换改成 RandomResizedCrop scale=(0.6, 1.0),旋转角度限制在 ±15 度以内,或者干脆不旋转。损失函数和模型体系都不用动,增强强度回到合理区间后,验证 top-1 能升回 0.9 以上。现在每换一个数据集,我都会先打印增强后的样例图,肉眼确认关键特征还在。
5.4 瓶罐类高反光让 loss 卡在平台期
现象:玻璃和金属类别的样本在训练时 loss 一直稳定但不下降,验证时这两类的 recall 停留在 0.7。原因:真实废弃物里玻璃瓶、铝罐在灯光下会产生高光区域,高光把商标和液位线这些特征直接“曝光”了。模型在训练时每次见到的反光位置不同,学不到稳定的判别信息。解决:加入 RandomErasing,以 0.3 的概率把图片随机区域的像素抹成灰色,强迫模型不能只依赖某一小块高光特征;同时把输入分辨率从 224 提到 256 或 288,让更多的瓶身细节进入视野。这一步之后,玻璃和金属的 recall 各涨了 4% 到 6%。反光这类光照噪声,靠增强模拟“遮挡”比调 loss 更直接。
5.5 纸箱和纸板怎么都分不开:合并类别反而更好
现象:混淆矩阵里纸箱和纸板的交叉项最高,两类各自的 recall 都只有 0.6。原因:这批数据的原始标注本来就不一致——一批标注员把瓦楞纸盒算“纸箱”,另一批把同一类材质算“纸板”,模型学到的是两套互相矛盾的“正确答案”。解决:先抽 100 张两类图片人工复核,确认没有稳定可分的视觉边界后,把“纸箱”“纸板”合并为“纸类”,与业务方对齐后再训练。合并后纸类 recall 直接到 0.88,产品里的假阳性也少了一半。这次翻车的教训是:类别定义不是建模时定的,是拿到标注时就要定的。两个类在视觉上分不开时,坚持保留只会让模型和标注一起摆动。如果业务真的需要区分,先重新标注出清晰的定义,给标注团队一套参考图,而不是让模型自己猜。
6. 让 CLIP 当第二标注员:低置信度样本的复核与真实场景验收
到这一步,标准流程已经完整。还值得推荐一个进阶玩法:把 CLIP 当成“第二标注员”,用零样本语义打分复核低置信度样本。CLIP 见过海量互联网图片,它的语义空间比我们训练出来的分类器更宽泛,对“易拉罐 vs 金属罐头”这类细粒度概念,它给出的文字匹配分数可以作为参考。做法是把测试集里模型置信度低于 0.6 的样本挑出来,让 CLIP 在类别名称上打分,如果 CLIP 的排序和模型预测不一致,通常意味着标注有误或类别定义模糊。
import clip import torch from PIL import Image model_c, preprocess_c = clip.load("ViT-B/32", device=device) texts = clip.tokenize([f"a photo of {c}" for c in class_names]).to(device) with torch.no_grad(): image = preprocess_c(Image.open(img_path)).unsqueeze(0).to(device) logits = model_c(image, texts)[0].softmax(dim=-1)逻辑说明:clip.load 加载 ViT-B/32 的预训练权重,texts 把类别名转成模板句子,模型为图片对每个类别输出匹配概率;softmax 后取 top-2,与当前分类器的预测对比。这段代码只用于二次复核和脏样本挑选,不参与主分类器的训练和推理,避免让 CLIP 的偏差污染主模型。
最后一个习惯,我会在项目一开始就留出一个独立照片目录,名字叫 real_world_smoke_test,里面放着真实场景里拍的 20 到 30 张废弃物照片。这些照片不进入训练集、验证集和测试集,只在每个版本模型训完后跑一次,用它代替任何美化过的验证指标。第一次跑出 0.71 时我就知道,指标再漂亮也算不得数,模型能不能在真实世界里站稳,是要看真实场景测试集怎么说的。做数据类项目,我现在第一步永远是全库哈希去重加脏样本抽检,而不是急着写模型。希望这个流程能帮到你,少走我走过的弯路。
本文还有配套的精品资源,点击获取