☰
基于深度学习的Python垃圾分类系统:从数据集到Web部署全流程解析
2026/10/13 3:54:23 网站建设 项目流程

简介:本资源是一套面向人工智能初学者与计算机专业学生的深度学习实践项目,聚焦垃圾分类这一典型图像识别应用场景,提供从数据准备、模型训练到系统集成的完整Python实现方案。资源共2000个文件,含1986张JPG格式垃圾图片(覆盖可回收物、有害垃圾等多类别)、8个核心PY源码文件(涵盖CNN模型构建、数据集划分、前后端交互及五大功能模块逻辑)、4个DOCX文档(含系统设计报告、需求规格说明书、测试方案及可行性研究报告),压缩包大小为44.31MB。已有516人下载学习,适合开展课程设计、毕业设计或AI应用开发实训。读者可直接复现注册登录、智能识别、分类查询、数据划分与白盒测试全流程,获得结构清晰的工程化代码框架、详实的文档支撑及可扩展的模型训练基础,显著降低深度学习落地门槛。 我这两年帮人做过好几个毕业设计和技术项目,其中一个就是"基于深度学习的Python垃圾分类系统",这个方向的搜索量一直很大,说明很多人都在做。但说实话,市面上的源码和教程有个通病——给你一堆代码文件,注释也写了,结果一跑就报错,报错还不给原因。所以这篇文章我打算换个思路,不贴完整源码,而是把我从数据集处理、模型选型到Web展示的整个过程拆开讲,每个环节为什么这么做、踩了哪些坑,全部交代清楚。

1. 垃圾分类系统要解决的到底是什么问题

1.1 它不是"给图片分类"那么简单

很多同学第一次拿到这个题目,第一反应就是:不就是图像分类吗?我用CNN跑一遍Imagenet预训练模型,精度能到95%,完事了。这个理解方向没错,但实际操作起来会发现,垃圾分类和常规的图像分类有明显区别。

常规分类任务,比如猫狗识别、花鸟识别,类别之间的视觉差异是很明显的。猫就是猫,狗就是狗,特征边界相对清晰。而垃圾分类常见的几大类——可回收垃圾、厨余垃圾、有害垃圾、其他垃圾——这里面每类包含的物品五花八门。比如"可回收垃圾"里既有矿泉水瓶,也有纸箱,还有旧衣服,它们的颜色、纹理、形状差异非常大。而"其他垃圾"里的烟头、陶瓷碎片、脏纸巾,又和某些可回收垃圾在视觉上有重合。

所以这个系统真正的难点不在模型结构,而在类别定义和数据多样性。你得先想清楚是做成四分类(四类垃圾)还是几十分类(具体物品如纸板、玻璃、金属、塑料等),这两条路的技术方案差别很大。

1.2 我最终采用的方案架构

我选择的方案是:细粒度物品识别 + 高层归类映射。

  • 第一层:模型识别出具体物品,比如"玻璃瓶""报纸""香蕉皮""废电池"等;
  • 第二层:通过建立物品到四分类的映射表,自动归类到可回收、厨余、有害、其他。

这样做的好处有两个:一是用户看到"这是玻璃瓶"比"这是可回收垃圾"更有信任感,体验更好;二是细粒度的分类任务,模型能学到更有区分度的特征,后续如果投放区域更换了垃圾分类标准(比如某些城市分干湿垃圾),只需改映射表,不用重新训练模型。

整体技术链路就是:图片输入 -> PyTorch读取并预处理 -> 深度学习模型推理 -> 输出物品类别编码 -> 查映射表得到垃圾分类结果 -> 展示到前端界面。

这个链路简单清晰,非常适合做毕业设计或者个人项目展示。下面我把每一环的关键细节展开讲。

2. 数据集准备:这一环决定了你后面所有工作的上限

2.1 别自己手动收集图片,公开数据集完全够用

我第一次做这个项目的时候,天真地想自己从网上爬图片,搞了三天,图片量不到500张,质量还参差不齐。后来学乖了,直接上公开数据集。

目前垃圾分类领域比较常用的公开数据集有这几个:

数据集规模类别数特点
Garbage Classification约2.5万张6类(纸、玻璃、金属、塑料、纸板、垃圾)图片比较规整,适合入门
Huawei Garbage Classification约2万张40类细粒度类别,接近真实使用场景
TrashNet约2500张6类规模小,适合快速验证
Kaggle垃圾分类数据集约2万张4大类直接按四分类划分

如果你做的是四分类系统,建议优先选40类细粒度数据集,然后自己建映射。因为直接用4分类数据集训练,模型见过的东西太少,推广到新场景时容易误判。而用40个类别的数据训练,模型对"物品"本身的理解更深入,再映射到四分类时鲁棒性更强。

2.2 标注质量检查:最容易忽略的一步

下载完公开数据集,我强烈建议你务必做一次数据分布检查。怎么检查?写个Python脚本,统计每个文件夹下图片的数量、尺寸分布、是否有损坏文件。

我遇到过最坑的情况是:某个"纸板"子类目下混进来了几十张"塑料瓶"的图片,标注噪声直接带偏了模型。还有的图片分辨率是1920x1080,有的却是256x256,如果统一resize到固定尺寸,低分辨率图片的信息可能不够用。

动手之前先画个分布直方图,看每个类别的样本数是否均衡。垃圾分类数据集往往严重不均衡——"其他垃圾"可能占一半,"有害垃圾"只有几百张。这种数据扔进去训练,模型会偏向样本多的类别。

处理方法很简单,但效果显著:

  • 欠采样:对样本多的类别随机抽取一部分参与训练;
  • 重采样:对样本少的类别,在DataLoader里提高采样概率;
  • 损失函数加权:给样本少的类别更高的loss权重。

我实际用下来,最省事的是给损失函数加权重,就是按类别样本数的倒数归一化,不需要额外生成图片文件。这个思路代码量小,而且效果稳定。

2.3 数据增强策略:不要盲目堆一堆操作

垃圾分类图片和一般的自然图像不同,背景相对简单,很多是纯色背景下的物品特写。数据增强主要解决两个问题:一是防止过拟合,二是模拟真实场景中的光照和角度变化。

我的一组增强配置如下:

from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomResizedCrop(size=(224, 224), scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(degrees=15), transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, hue=0.1), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])

注意几个细节:

  • RandomResizedCrop的scale参数不要设得太小,比如不要低于0.5,否则会裁剪到物品的局部,反而变成难题,因为垃圾图片本身不是像文字OCR那样富含上下文信息的。
  • RandomRotation角度不要太大,15度以内够了。垃圾图片中有些物品(如玻璃瓶)旋转后类别不变,但旋转90度可能导致关键特征丢失(比如瓶身标签)。
  • ColorJitter的光照扰动很有用,因为实际场景中拍照光线差异很大,但这个参数也别过度,否则模型会忽略颜色这个重要特征。

验证集和测试集不要做任何随机增强,只用Resize(256) -> CenterCrop(224) -> Normalize,保证评估结果稳定可复现。

3. 模型选型与训练策略:别迷信"越深越好"

3.1 为什么我选择了迁移学习而不是从头训练

一个很现实的问题:垃圾分类系统属于"中小数据规模"的图像分类任务。哪怕是公开数据集,规模也就是2~3万张。用从头训练的深度CNN(比如ResNet50)去学,参数量好几千万,这点数据量完全不够用,训练只会严重过拟合——train_acc能到99%,val_acc却卡在70%。

所以我的建议是:用ImageNet预训练权重做迁移学习。这些预训练权重已经学到了泛化的视觉特征,比如边缘、纹理、形状、部分语义,我们只需要在它的基础上微调,让它适应垃圾分类这个特定任务。

在PyTorch里,这一步非常简单:

import torchvision.models as models # 以MobileNetV3-Large为例 model = models.mobilenet_v3_large(pretrained=True) # 替换分类头 model.classifier[3] = torch.nn.Linear(1280, num_classes)

注意:不同版本的预训练权重信息来源不同,PyTorch现在推荐使用weights='DEFAULT'显式声明,比如:

model = models.mobilenet_v3_large(weights=models.MobileNet_V3_Large_Weights.DEFAULT)

3.2 几类主流模型的实测对比

我在这套系统上分别跑过ResNet50、MobileNetV3-Large、EfficientNet-B3,说说实际感受:

模型参数量单张推理耗时(GPU)验证集准确率模型文件大小
ResNet50~25.6M约10ms92.3%~98MB
MobileNetV3-Large~5.4M约5ms91.8%~22MB
EfficientNet-B3~12M约8ms93.1%~45MB

从这个结果能明显看出来,模型性能并不是和深度严格成正比。ResNet50和MobileNetV3准确率几乎一样,但MobileNet体积只有ResNet的四分之一,推理速度更快。如果你的系统要部署到树莓派、嵌入式设备或者Web服务上,MobileNet系列是性价比之选。如果追求极致精度,EfficientNet-B3略高一筹,代价是训练和推理都更慢。

我的最终选择是MobileNetV3-Large,原因有三:体积小、速度快、准确率足够高,而且它内置了Squeeze-and-Excitation注意力模块,对区分"看起来很像"的垃圾品种有帮助。

3.3 训练超参数与优化器选择

训练这一个环节,我踩过不少坑,最重要的一条经验是:先冻结骨干网络只训练分类头,等loss降下来再解冻全模型微调。

第一次做的时候我直接把整个模型扔进去训,learning_rate设为0.001,结果损失的下降曲线乱七八糟,到后期甚至发散。后来按照下面这套参数,效果就稳定了:

阶段一(冻结骨干,只训分类头):

  • 优化器:AdamW
  • 学习率:0.001
  • 批大小(batch size):64
  • 训练轮数:5轮

阶段二(解冻全部参数,微调):

  • 优化器:AdamW
  • 学习率:0.0001,配合CosineAnnealingLR余弦退火
  • 批大小:64
  • 训练轮数:15~20轮

为什么用AdamW而不是SGD?AdamW自带权重衰减,正则化效果比Adam更好;配合余弦退火可以让学习率在后半段平缓降低,避免在loss最小值附近震荡。

训练过程中还建议加早停策略(early stopping):如果连续4~5个epoch验证集准确率没有提升,就停止训练并恢复最优模型。PyTorch实现不复杂,核心逻辑如下:

best_acc = 0.0 patience = 0 for epoch in range(num_epochs): train_one_epoch(...) val_acc = evaluate(...) if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), 'best_model.pth') patience = 0 else: patience += 1 if patience >= 5: break

这样既能省时间,又能确保保存的是验证集上表现最好的那版权重,而不是最后一个epoch的权重。

4. 核心代码模块拆解:从加载数据到训练出模型

4.1 自定义Dataset:读取文件夹即标签

数据集我按ImageFolder的目录结构组织:

data/ train/ glass/ paper/ metal/ plastic/ cardboard/ trash/ val/ glass/ paper/ ...

如果直接用torchvision.datasets.ImageFolder,标签会自动按ASCII排序生成,很方便。但我需要的是40类细粒度映射到4大类,所以通常会自己写一个GarbageDataset,继承torch.utils.data.Dataset。

import os from PIL import Image from torch.utils.data import Dataset import numpy as np class GarbageDataset(Dataset): def __init__(self, root_dir, class_to_idx, mapping, transform=None): self.root_dir = root_dir self.transform = transform self.mapping = mapping # 到四位分类的映射 self.samples = [] for cls_name in class_to_idx.keys(): cls_dir = os.path.join(root_dir, cls_name) for fname in os.listdir(cls_dir): if fname.lower().endswith(('.jpg', '.jpeg', '.png')): self.samples.append((os.path.join(cls_dir, fname), class_to_idx[cls_name])) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label = self.samples[idx] image = Image.open(path).convert('RGB') if self.transform: image = self.transform(image) return image, label

这里有个容易踩的坑:Image.open不会立即读取图片内容,它是惰性的,执行到image = Image.open(path)这一步时,文件可能还没真正加载到内存。如果后续直接对image做np.array(image)或者transforms操作,由于文件句柄已经关闭,可能会报OSError: image file is truncated。所以务必加一个.convert('RGB'),这会强制把图片完整加载进内存,也能统一通道数。

4.2 训练循环:一个"不完美但够用"的版本

核心训练循环的代码逻辑很简单,三句话就能说清:取数据、算loss、反向传播。真正体现功力的地方在于IO效率和信息记录。

DataLoader里的num_workers参数,我实测在Windows上设成0最稳定,设大了会报BrokenPipeError;在Linux服务器上设成4或8都能明显提速。如果训练数据很大,建议先把图片预处理成224分辨率的副本,训练时直接加载,否则每次epoch都做一次resize,白白吃掉大量CPU时间。

记录信息这块,我用的是tensorboard+ 本地日志双保险。在训练循环里把每一轮的loss、acc、lr都存下来:

with open('train_log.txt', 'a') as f: f.write(f"epoch {epoch+1}: loss={avg_loss:.4f}, acc={avg_acc:.4f}, val_acc={val_acc:.4f}, lr={current_lr:.6f}\n")

这行简单操作在后面调参时帮了大忙。当你同时跑了三组实验,用grep对比日志里的val_acc曲线,很快就能分辨哪组参数值得继续调,哪组直接放弃。

4.3 映射表:细粒度类别向四大类的转换

模型输出的40类编码,到四分类的映射我用一个Python字典搞定:

mapping = { 'glass_bottle': 0, 'glass_cup': 0, 'glass_jar': 0, 'newspaper': 1, 'cardboard': 1, 'magazine': 1, 'banana_peel': 2, 'apple_core': 2, 'food_waste': 2, 'battery': 3, 'medicine': 3, 'paint': 3, }

映射关系要单独维护成一个JSON文件,不要写在代码里。因为垃圾分类标准是各地政府定的,你今天的项目在A市部署,明天可能要在B市上线,只要改JSON文件,整个系统的分类规则就跟着变了,代码一行都不用动。这个设计在被老师验收时也是一个加分项。

5. 训练排障实录:我踩过的三个典型坑

5.1 GPU显存不足:不是你想的那样

我用的是笔记本上的GTX 1650(4GB显存),第一次训练ResNet50,batch_size设成64,一跑就CUDA out of memory。

我当时第一反应是减小batch size,从64减到32,还是爆;减到16,勉强能跑,但训练速度慢得离谱。后来查了资料才明白,问题出在我同时开着PyCharm、浏览器、微信等一堆程序,占用大量系统内存,而GPU在batch_size计算时会把中间激活值缓存在显存里,系统内存不够时就会出问题。

解决方案:

  • 关掉不必要的图形界面应用;
  • 把训练脚本改成python train.py命令行运行;
  • 使用torch.cuda.empty_cache()清理显存碎片(注意:这个函数只清空缓存,不释放已分配的张量,别指望它解决真正的OOM)。

如果还是不够,就把batch size降到8,同时把图像输入尺寸从224降到192,用scale动态调整。反正最终验证集准确率下降幅度在1%以内,总比跑不起来强。

5.2 验证集准确率上不去,但训练集准确率接近100%

这是非常典型的过拟合信号。出现这个情况,我一开始怀疑是模型太深了,后来一步步排查才发现根本不是。

第一,先检查数据划分。我用的是随机划分,但这个随机其实有个隐患:同一个类别的图片可能在划分前被全部排到了一起。解决方案是使用sklearn.model_selection.train_test_split在文件夹级别做划分,确保每种类别图片在训练集和验证集里都按比例出现。

第二,加Dropout。MobileNetV3的classifier层里自带Dropout,默认概率是0.2,我调到0.5后验证集准确率提升了大约2个百分点。别小看这一层,它直接决定模型有没有"死记硬背"训练样本的特征。

第三,最有效的还是数据增强里的RandomErasing或CutMix,随机擦除图片的一部分,强迫模型关注整体特征而不是某一小块区域。

transform = transforms.Compose([ # ... 之前的增强 transforms.RandomErasing(p=0.5, scale=(0.02, 0.15)), ])

注意RandomErasing必须在ToTensor之后用,因为它操作的是张量。

5.3 模型预测时总是把"塑料瓶"识别成"玻璃瓶"

这个问题有意思,验证集准确率很高,但实际测试时却翻了车。我的排查思路是:先看训练集里塑料瓶和玻璃瓶的样本数,发现玻璃瓶明显多于塑料瓶。再看了下样本图片,很多塑料瓶是透明的,和玻璃瓶在视觉上几乎没法区别,尤其是有光照反射的时候。

最终解决是用背景增强:让模型不再只靠"透明+圆柱形"来判断,因为这两个特征在两类中都存在。我在数据增强里加了RandomGrayscale(p=0.1),偶尔把彩色图片转灰度,模型被迫学除了颜色以外的形状、纹理特征。虽然准确率提升不算特别大,但误判率确实明显下降了。

这件事给我的启发是:模型表现差的时候,先看看数据里到底有哪些区分性线索,再决定是改模型还是改数据。

6. 推理部署与Web展示:从"能跑"到"能用"

6.1 Flask后端搭建:把模型封装成API

训练好的模型最终要通过一个接口暴露给前端使用。我用Flask写了一个轻量级的推理服务,代码量不大:

from flask import Flask, request, jsonify from PIL import Image import torch import torchvision.transforms as transforms import torchvision.models as models import io app = Flask(__name__) # 加载模型 model = models.mobilenet_v3_large(weights=None) model.classifier[3] = torch.nn.Linear(1280, 40) model.load_state_dict(torch.load('best_model_garbage.pth', map_location='cpu')) model.eval() preprocess = 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]) ]) @app.route('/predict', methods=['POST']) def predict(): file = request.files['image'] image = Image.open(file.stream).convert('RGB') image_tensor = preprocess(image).unsqueeze(0) with torch.no_grad(): outputs = model(image_tensor) probs = torch.softmax(outputs, dim=1) confidence, idx = torch.max(probs, dim=1) # idx -> 类别名 -> 映射成四分类 result = {'class_id': idx.item(), 'confidence': round(confidence.item(), 4)} return jsonify(result) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)

有几个关键点:

  • map_location='cpu'是为了在无GPU环境下也能加载模型,不然换到没有CUDA的服务器上会报错;
  • 每次推理时要加上torch.no_grad(),它告诉PyTorch不需要计算梯度,能大幅降低显存消耗和推理耗时;
  • 模型的eval()模式不能漏,漏了的话Dropout和BatchNorm的行为会和训练时一样,导致推理结果不稳定。

6.2 前端页面:一张图片、一次点击、一个结果

前端我写了一个单页面HTML,样式就用纯色背景加卡片式布局,通过fetch上传图片到Flask后端,然后显示返回的结果和置信度。

<input type="file" id="imageInput" accept="image/*"> <button onclick="uploadImage()">识别</button> <div id="result"></div>
async function uploadImage() { const fileInput = document.getElementById('imageInput'); const formData = new FormData(); formData.append('image', fileInput.files[0]); const response = await fetch('/predict', { method: 'POST', body: formData }); const data = await response.json(); document.getElementById('result').innerText = '类别ID:' + data.class_id + ',置信度:' + data.confidence; }

如果想让这个系统看起来更"高级"一点,可以在前端调用摄像头getUserMedia做实时识别,但要注意摄像头帧率很高,每帧都发到后端推理对服务器压力很大。我采用的方案是:前端每2秒取一帧,只有画面变化超过阈值才发请求,这样既保持实时性,又不会把服务器打垮。

6.3 置信度阈值:让系统会"说不知道"

很多人做完分类系统就完了,用户传一张不相关的图片(比如一只猫),系统也会强行说出一个垃圾类别,这就是"胡说八道"。

为了避免这个尴尬场景,我在后端的预测逻辑里加了一个置信度判断:

if confidence < 0.6: result = {'error': '无法识别,请拍摄更清晰的物体照片'} else: result = {'class_id': idx.item(), 'confidence': round(confidence.item(), 4)}

这个0.6的阈值不是拍脑袋定的,而是我在验证集上算出来的。具体做法是:把50张不属于任何类别的图片混进去测试,观察模型给出的置信度分布,取能让"误判率小于5%"的阈值。这样系统在遇到未知物体时,会诚实地告诉用户"我不认识",而不是瞎猜。

7. 总结一下我从这个项目里沉淀出的几条经验

做垃圾分类系统这件事,技术栈并不复杂,难的是把每个环节做扎实。数据集决定了天花板,模型和训练方法决定你能不能接近这个天花板,而工程化能力决定你的系统能不能被别人用起来。

我个人最大的体会是,千万不要跳步。很多同学下载源码第一件事就是跑训练,跑通就认为完事了,完全不看数据分布、不验证模型的泛化能力,最后答辩的时候面对老师的一个问题就懵了。老师随便问一句"你的模型为什么选择MobileNetV3而不是ResNet"或者"训练集和验证集怎么划分的",如果没提前做过对比实验,根本答不上来。

这个系统后续可以扩展的方向也不少,比如加入OCR识别包装袋上的文字、加入语音播报提示、部署到小程序端,甚至用目标检测算法(YOLO系列)实现多物品同时识别,每次看到一篮子垃圾可以一次性给出各自的分类。不过这些都是后话了,先把图像分类这条主线跑通,后面加功能自然水到渠成。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询