
简介基于深度学习的垃圾分类完整项目资源定位给人工智能与计算机视觉方向的初中级学习者用于解决生活垃圾自动识别与分类问题。压缩包共十个文件包含五个脚本、示例图片、说明文档和训练脚本包体仅五点七三兆其中脚本覆盖数据抓取、模型训练、实时检测与串口通讯等环节结构清晰便于快速部署。已有169人学习。资源从数据预处理图像缩放、归一化与翻转增强、卷积神经网络模型搭建到优化器与损失函数配置均有代码呈现并针对过拟合给出验证集与早停策略可帮助读者完整理解图像分类全流程。代码可适配主流深度学习框架运行适合希望通过完整项目提升实战能力并关注环保应用的开发者。1. 收到“基于深度学习的垃圾分类.zip”后先从这三个角度拆项目如果在答辩前一周队友给你丢来一个“基于深度学习的垃圾分类.zip”最稳妥的拆解思路是别急着解压找权重先把它当作一个“单标签图像分类”工程来看。任务边界很明确——给定一张图片推它是厨余、可回收、有害还是其他垃圾但真正的复杂度分布在三处数据整理和标签质量、CNN 骨干网与训练参数的选择、训练完成后能否在摄像头或服务端完成稳定推理。多数人把时间花在反复换网络上最后演示时准确率反而“过山车”原因往往是样本分布没看过、训练和推理预处理不一致。这套链路可以用 PyTorch 完整跑通用 ImageFolder 组织数据基于 ResNet18 做迁移学习通过损失曲线和混淆矩阵调参再导成 ONNX 或量化模型给边缘设备用。新手可以直接照着第3章的脚本从头跑一遍有工程经验的同事可以看第2章的选型权衡和第4章的部署边界。2. 垃圾分类项目的模型选型与数据集闭环2.1 为什么垃圾分类必须用深度学习而不是传统图像处理垃圾分类的视觉难点在于“同类不同形、异类同形”。一张塑料袋和一张白纸在颜色直方图层面可以非常接近但语义不同一个透明玻璃瓶和一个矿泉水瓶形状和材质都接近却分别属于可回收物和其他垃圾。传统方法用颜色、纹理、手工设计的边缘算子配合 SVM在固定背景和固定灯光的实验室数据集上能到 90% 以上一旦换到自然光、倾斜角度和有遮挡的环境特征分布马上漂移。深度学习在这里的实际体量不是“从零发明网络”而是“预训练骨干网络微调”。垃圾分类公开数据集的规模通常在几千到几万张相比 ImageNet 小得多。从零训练 ResNet 会很快过拟合而使用在 ImageNet 上预训练的权重做迁移学习模型在低数据预算下也能学到通用视觉特征再用垃圾图片微调高层语义。这就是“基于深度学习”的项目里最常见、也最可靠的落地方式。2.2 骨干网络选型从 ResNet 到轻量化模型的权衡第一版实验用 ResNet18 最省事。它结构短、预训练权重好找、对四类垃圾图片的识别精度足够作为 baseline跑通以后想提点再换 ResNet50 或 EfficientNet-B0如果目标是部署在带 NPU 的嵌入式板卡上MobileNetV3-Small 通常比压一个 ResNet 更容易达到实时要求。一上来就用 EfficientNet 或 Vision Transformer 不一定划算参数量大、推理慢且需要更多数据支撑。我的建议是“以部署目标反推模型”做成网页 DemoResNet18 就行做成固定角度摄像头实时分类优先轻量化如果要求批量离线分类则追求精度优先。模型参数量级CPU 单图推理量级适用阶段ResNet18约11M20-50ms先跑通流程ResNet50约25M50-120ms追求准确率MobileNetV3-Small约2.5M5-15ms边缘设备EfficientNet-B0约5M20-40ms数据量充足且算力均衡推理耗时会根据 CPU 型号、线程数、是否使用 TensorRT 有明显浮动表里只是量级参考。这里的关键是换模型时训练输入尺寸和预处理方式不能跟着模型随意换否则平均精度下降不一定来自模型能力而来自输入分布变化。2.3 把散装图片整理成 ImageFolder一份可复制的目录脚本PyTorch 训练代码里十个有八个会用 ImageFolder 加载数据它的硬性要求是“类别文件夹”结构。而许多公开数据集的原始形态是 CSV 加图片文件第一步要做的是把 CSV 改造成目录结构。由于中文路径在某些深度学习框架和日志系统里会带来莫名报错我通常把目录建为英文 ID另写一个 JSON 做 ID 到中文名的映射。from pathlib import Path import os import shutil root Path(garbage_src) # 把 CSV: 相对路径, 类别id 转到 train/val 对应子目录 for split in [train, val]: src_file root / f{split}.csv with open(src_file, r, encodingutf-8) as f: for line in f.readlines(): rel_path, label_id line.strip().split(,) # label_id: 0-厨余 1-可回收 2-有害 3-其他 label_dir fclass_{int(label_id)} dst root / split / label_dir dst.mkdir(parentsTrue, exist_okTrue) shutil.copy2(root / rel_path, dst / os.path.basename(rel_path))对这段代码的解读split用来区分训练集和验证集把同一个文件放在 train 和 val 两个目录下label_dir用class_0而不是中文命名是为了避免后续 C 部署或日志打印时遇到编码问题shutil.copy2会保留元数据也能防止误改原始文件。做完这一步后模型训练里直接用datasets.ImageFolder就能跑不会再出现“图片读入后找不到标签”的报错。2.4 类别不均衡先于网络结构两种可落地的修正方式我拿到任何数据集都会先跑一行统计看每类图片数量相差多少。垃圾分类里“有害垃圾”的数量往往只有厨余的十分之一。如果直接训练模型会为了最小化总损失而牺牲少样本类验证准确率看起来不错但实际部署时对电池、灯管这类物品基本失灵。这里有两个常用手段且可以叠加。第一种是训练集类别加权采样让每个 batch 里少样本类出现的概率不低于一个阈值。第二种是在损失函数里加权重把低频类别的错误放大。PyTorch 的写法如下import torch import torch.nn as nn class_counts torch.tensor([1200, 800, 150, 900], dtypetorch.float) class_weights 1.0 / class_counts class_weights class_weights / class_weights.sum() criterion nn.CrossEntropyLoss(weightclass_weights)这里class_counts要按 ImageFolder 返回的类别顺序填也就是目录顺序不是按 CSV 里的出现顺序顺序反了权重就作用在错误的类别上。加了权重后训练 loss 的绝对值会变大这正常需要重点观察每个类别的召回率而不是总准确率。3. 用 PyTorch 训练垃圾分类模型最小实现与训练参数3.1 深度学习环境配置conda 与版本对齐在进入代码前先把环境配置步骤固定下来。深度学习环境配置的版本不匹配问题比手写 loss 问题更容易卡住人。常见做法是用 Anaconda 建一个独立环境项目结束可以直接删除环境不影响其他项目。conda create -n garbage python3.10 -y conda activate garbage pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install pillow matplotlib scikit-learn tqdm第一行创建虚拟环境第二行激活第三行安装带 CUDA 12.1 的 PyTorch。如果在没有 NVIDIA 显卡的机器上训练把 URL 中的cu121换成cpu否则 torch 能装上但启动时会提示 CUDA 不可用。第四行的tqdm用来打印训练进度scikit-learn是最后一章做混淆矩阵要用的可以顺手装上。装完执行python -c import torch; print(torch.cuda.is_available())输出 True 才说明 GPU 可用。3.2 用 torchvision 定义数据增强与归一化数据加载阶段需要把“垃圾图片”转成“网络可吃的张量”。ResNet18 预训练权重默认期望 224x224 的输入并期望输入按 ImageNet 的均值和方差归一化所以 transforms 不能乱写from torchvision import datasets, transforms train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_dataset datasets.ImageFolder(garbage_dataset/train, transformtrain_transform) val_dataset datasets.ImageFolder(garbage_dataset/val, transformval_transform)说明有几处容易被忽略Resize((224, 224))直接拉伸不做中心裁剪是为了减少推理时要记住“先 Resize 再 CenterCrop”的隐式流程虽然 CenterCrop 保留更多比例信息但演示时经常忘记写导致精度下降随机翻转和旋转只在训练集上用验证集必须和推理保持一致都用固定的val_transformNormalize的四个值是 ResNet 预训练模型的标准不能全部替换为 0 和 1。3.3 ResNet18 微调训练循环与配置说明训练脚本给一个可复现的最小版本。核心逻辑是加载预训练 ResNet18替换全连接层输出为 4 类然后跑若干 epoch。为了让代码在 CPU 机器上也能直接跑设备判断写成“有 GPU 用 GPU否则用 CPU”。import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import models from tqdm import tqdm num_classes 4 batch_size 32 epochs 20 learning_rate 1e-4 train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue, num_workers2) val_loader DataLoader(val_dataset, batch_sizebatch_size, shuffleFalse, num_workers2) model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, num_classes) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lrlearning_rate) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) for epoch in range(epochs): model.train() running_loss 0.0 correct 0 total 0 for images, labels in tqdm(train_loader, descfEpoch {epoch 1}/{epochs}): images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) correct (predicted labels).sum().item() total labels.size(0) epoch_loss running_loss / total epoch_acc correct / total print(fepoch {epoch 1}: train loss {epoch_loss:.4f}, acc {epoch_acc:.4f})代码拆开说models.ResNet18_Weights.IMAGENET1K_V1是 torchvision 新的权重参数写法旧写法pretrainedTrue会提示 deprecation但不影响训练model.fc nn.Linear(...)只替换最后一层前面的卷积层参数保留在模型里参与微调criterion可以使用 2.4 节中带 class weight 的损失函数optimizer Adam(lr1e-4)这个学习率在微调阶段是安全区间换 SGD 时通常要用 0.01 量级。tqdm只负责打印与模型精度无关。3.4 训练参数调节表epoch、学习率、batch size训练过程中真正要动的是下面几个旋钮初始值可以这样设参数初始值调整方向典型观察信号batch_size32显存不够降到 8/16类别不均衡时考虑增大loss 震荡频繁可能是 batch 过小learning_rate1e-4Adam 下 loss 不降可短时升到 1e-3训练 2-3 轮 loss 都不变化epochs20看验证集是否连续多轮不涨验证准确率不再提升时提前停optimizerAdam抖动严重可换 SGDmomentum验证准确率在某一区间来回跳weight_decay0过拟合时设为 1e-4训练 acc 高验证 acc 低这里的逻辑是先让模型在一个 batch 上过拟合再谈调参。如果模型连 32 张图片都背不下来大概率是学习率、预处理或代码链路有问题而不是需要更复杂的网络。把 batch_size 设为训练集的一个子集训练 50 轮看 train loss 能否降到接近 0这是最快的冒烟测试。3.5 loss 不降、验证抖动、显存不足的排查顺序出现 loss 不降时不要立即改模型按顺序排查先打印一两张 transform 后的图片张量确认像素范围、通道顺序是否是 RGB确认 label 范围在[0, num_classes)内把学习率从 1e-4 提到 1e-3 试跑 5 轮若还是不降就换一个预训练模型如 MobileNet跑通流程。验证准确率抖动通常由 batch_size 小或学习率偏大造成可以试试余弦退火学习率调度。显存不足时优先把num_workers调低、batch_size减半而不是换小模型因为识别流程里加载的是 224 尺寸显存压力并不直接来自模型结构。提示Windows 上DataLoader(num_workers2)需要把训练代码放到if __name__ __main__:里否则子进程会反复重启并报 RuntimeError。4. 垃圾分类模型推理、ONNX 导出与边缘部署4.1 预处理一致性与推理脚本训练完成不等于能演示。最容易出 bug 的地方是推理脚本里把RandomHorizontalFlip也复制了过去或者图像尺寸不一致。推理阶段只需要固定变换import torch from PIL import Image from torchvision import transforms def predict_one(model, image_path, class_names): model.eval() # 切换 BatchNorm 和 Dropout 行为 transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) image Image.open(image_path).convert(RGB) tensor transform(image).unsqueeze(0) with torch.no_grad(): logits model(tensor) probs torch.softmax(logits, dim1) confidence, index torch.max(probs, 1) return class_names[index.item()], float(confidence.item())函数里的细节集中在两处model.eval()必须写否则 BatchNorm 会使用当前 batch 的统计量而不是训练时的移动平均造成输出概率漂移with torch.no_grad()不构建计算图推理显存明显下降速度更快。输入图片.convert(RGB)是因为摄像头或 PNG 图片可能出现 RGBA 四通道ResNet 的卷积层只接受三通道输入。4.2 导出 ONNX 并在 ONNX Runtime 中验证如果项目要交付给非 Python 环境常见做法是先导出 ONNX。ONNX 是一份静态计算图解除了对 PyTorch 版本和训练代码的依赖。导出代码很短dummy_input torch.randn(1, 3, 224, 224).to(device) model.to(cpu).eval() torch.onnx.export(model, dummy_input, garbage.onnx, input_names[input], output_names[output], opset_version12)说明dummy_input只决定输入的形状和维度值本身不影响导出结果model.to(cpu)是为了避免导出图中残留 GPU 设备信息后续在只有 CPU 的服务器上加载更省事opset_version12覆盖面较广如果目标设备需要更低版本可降到 11。导出后要用 ONNX Runtime 跑一次确认输出和 PyTorch 结果在数值上相差不大import onnxruntime session onnxruntime.InferenceSession(garbage.onnx) ort_inputs {input: tensor.numpy()} ort_outputs session.run(None, ort_inputs)[0] print(ort_outputs.shape, ort_outputs[0].argmax())4.3 部署到 K230 这类边缘设备体积、量化与延迟垃圾分类比赛和项目中常见“摄像头加一块集成 NPU 的板卡”的方案比如工创赛里出现的 K230 场景。这类板卡和 PC GPU 完全不是一个量级部署时重点不再是训练准确率而是模型体积、算力匹配和内存占用。ResNet18 在 fp32 下权重约 45MB直接移植到部分边缘板卡会慢或放不下。常规处理是先用 MobileNetV3 这类轻量网络训练再导出 ONNX最后在板卡工具链上做 int8 量化把模型压到几 MB 到十几 MB。推理端还要把头尾的图片预处理同步到板卡语言环境在 C/C 里把 RGB 数据按相同 mean/std 归一化。只搬模型不搬预处理推理结果会和本地完全不同。4.4 用 FastAPI 包装分类服务如果要交付的是 Web Demo直接让浏览器调 PyTorch 进程不是好方案。常见做法是用 FastAPI 包一个 HTTP 接口前端传图片服务端返回类别和置信度from fastapi import FastAPI, UploadFile import torch from PIL import Image app FastAPI() model load_model(garbage_best.pth) model.eval() app.post(/predict) async def predict(file: UploadFile): image Image.open(file.file).convert(RGB) tensor transform(image).unsqueeze(0) with torch.no_grad(): probs torch.softmax(model(tensor), dim1) idx int(torch.argmax(probs, 1)) return {class: class_names[idx], confidence: float(probs[0, idx])}这段代码写给负责接入的同学看load_model只应该在服务启动时执行一次不要写进每个请求里UploadFile拿到的文件是临时类型Image.open后要立刻转为张量避免文件句柄不释放。生产环境里建议在 Nginx 后面挂 Gunicorn并且用共享内存缓存模型这部分按团队已有基建决定即可。5. 用混淆矩阵验证垃圾分类模型的真实水平5.1 混淆矩阵定位真正混掉的类别训练完只看“验证集准确率 95%”不够。四分类任务即使不做任何学习按多数类预测也能到 50% 上下。我拿到模型后的第一个动作就是生成混淆矩阵看哪两个类别互相被认错玻璃瓶被分到可回收物还是其他垃圾塑料袋是否大量落进厨余。直接调用sklearn就有现成实现from sklearn.metrics import confusion_matrix, classification_report import torch all_preds [] all_labels [] with torch.no_grad(): for images, labels in val_loader: images images.to(device) outputs model(images) _, predicted torch.max(outputs, 1) all_preds.extend(predicted.cpu().tolist()) all_labels.extend(labels.tolist()) cm confusion_matrix(all_labels, all_preds) print(cm) print(classification_report(all_labels, all_preds, target_namesclass_names))confusion_matrix的行是真实类别列是预测类别classification_report会给出每个类别的精确率、召回率和 F1。当“有害垃圾”召回率低于其他类别时问题往往不在网络结构而在类别不均衡或样本量太少。这时优先做的是回到 2.4 节把损失函数权重加回去而不是换更深网络。5.2 低置信度转人工复核一个能真正提升体验的部署技巧混淆矩阵看完就剩最后一个落地技巧不要求模型对所有图片都给出答案。垃圾分类的实际场景中软塑料和纸制品、陶瓷和玻璃的边界本身存在歧义强制返回一个类别会造成错误引导。在部署脚本里加一个置信度阈值低于阈值时返回“无法识别请人工复核”。阈值通过验证集确定统计所有正确样本置信度分布的 5% 分位作为默认值对高风险类别比如有害垃圾可以再单独调高。这样演示时模型报告“低置信度请人工复核”是特性而不是缺陷反而说明系统知道自己不知道什么。这个技巧在工业侧模型管理中很常用能直接拉高用户对系统的信任度。本文还有配套的精品资源点击获取