简介:这份资源是基于Python与深度学习的垃圾分类系统设计与实现项目,定位为高分毕业设计、期末大作业或课程设计,适合具有一定Python基础、希望做图像分类实战项目的计算机专业学生。压缩包共12个文件,包含6个Python脚本,覆盖数据生成、模型训练、推理演示等环节;2个docx文档提供理论介绍与训练记录;另有json配置文件、jpg示意图等辅助材料,整体大小为4.21MB。代码结构清晰,主模型采用ResNet50,可帮助读者掌握深度学习图像分类项目的完整开发流程。已有373人学习,项目可直接运行,适合通过实际代码理解数据处理、模型搭建与训练调优的关键步骤,也可作为毕业设计答辩的参考资料。
1. 为什么一个“垃圾分类系统”会让编程新手卡在90%的进度上
先说结论:拿到“基于python与深度学习的垃圾分类系统设计与实现”这类高分项目,最大的难点不是模型怎么训练,而是别人看不见的那几步——数据集怎么组织、路径怎么处理、模型文件在解压zip之后还认不认、以及Web界面到底怎么跟推理代码串起来。很多照着教程瞎忙一周的人,最后不是倒在CNN结构上,而是倒在“能跑起来”四个字上。
这个题目的本质是用深度学习做一个图像分类系统,输入一张垃圾图片,模型输出它是可回收、有害、厨余还是其他垃圾。听起来简单,但它把python安装、深度学习环境配置、图像预处理、模型训练、后端接口、前端页面全部串了一遍,恰好覆盖了课设和毕设评分里爱看的内容。适合谁做?适合要把简历写成本科项目、或者需要交一份完整可演示系统的人。下面这套路线我自己带项目时反复用,照着一个模块一个模块过,三天能跑到Web界面出结果。
2. 系统拆解与技术选型:CNN不是唯一选择,但迁移学习是最优解
2.1 先想清楚这个系统由哪几块组成
一个能演示的垃圾分类系统,至少包含四个模块:数据集、训练脚本、推理服务、Web前端。很多教程只讲训练,把Web端一笔带过,导致项目根本没法对外演示。实际评分和答辩不会只看模型精度,他们更在意“完整链路能不能转起来”。
我一般把整个项目拆成三条流水线:数据流水线负责把图片变成模型能吃的张量;训练流水线负责加载预训练模型、替换分类头、在GPU或CPU上跑若干epoch;推理流水线负责接收前端上传的图片,走一次前向传播,返回分类结果和置信度。三者之间用模型文件(.pth或.pt)衔接,Web端只依赖推理服务,不碰训练代码。
这样拆的好处是每个模块都能单独调试。数据流水线出问题时,你不至于跑去改模型结构。
2.2 为什么选迁移学习而不是从零训练CNN
“基于深度学习”不等于要自己搭一个VGG或ResNet。垃圾分类公开数据集一般在几千到几万张之间,从零训练一个深层网络,在CPU上跑一个epoch可能就四五十分钟,而且精度大概率不如微调后的预训练模型。这里用迁移学习是标准做法:加载ImageNet上训好的权重,冻住前面若干层,只训练后面的分类层。
选型上我最常用ResNet18或MobileNetV3。选ResNet18是因为它对新手友好,结构直观,参数量不大,CPU也能勉强推理;选MobileNetV3是因为如果后续要换个安卓端或者边缘设备做展示,这个主干能直接搬。两个模型在百度网盘、Model Zoo之类的地方都能下载到预训练权重,zip包里一般也会内置一份,但要注意版本匹配,gpytorch和torchvision的模型列表是在不同版本里才能对上。
注意:不要拿ImageNet的1000类分类头直接跑垃圾图片分类,必须把最后一层全连接替换成“你的类别数”,否则训练时loss会一直降不下去。
2.3 zip包解压后先确认这几样东西
拿到基于python与深度学习的垃圾分类系统设计与实现(高分项目).zip之后,别急着看代码,先解压并确认目录结构里是否有这几类资源:
- 模型权重文件:通常是以
.pth、.pt、.ckpt结尾的文件,大小在几十到两百MB之间。 - 训练脚本:
train.py、data_loader.py这类文件,缺少时要能自己写得出来。 - Web相关文件:
app.py、templates文件夹、static文件夹。如果没有,只能用Flask或FastAPI自己补。 - 数据集文件夹或数据准备说明:不是所有zip都会附带全量数据,很多项目只给样例和下载链接,这时候需要自己准备。
发现缺文件不必慌。上面这就是个典型的高分课设结构,缺Model就补Model,缺Web就补Web。最怕的是解压时出现zip伪加密报错,Windows自带的解压工具会直接提示“文件损坏”。这种zip包在Linux下用7z或unar能正常解开,后面避坑章节会专门讲。
3. 数据准备与预处理:把图片喂进模型前,先解决“目录结构”和“中文路径”两个坑
3.1 数据集目录结构:torchvision.ImageFolder能直接吃什么样的文件夹
训练一个图像分类器,最省事的方式是用torchvision.datasets.ImageFolder,它对目录格式有约定:根目录下按分类建子文件夹,每个子文件夹放同一类别的图片。例如:
dataset/ train/ recyclable/ img1.jpg img2.jpg kitchen/ img3.jpg img4.jpg val/ recyclable/ kitchen/对应的加载代码是:
# data_loader.py from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练数据增强与归一化 train_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(degrees=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(root='dataset/train', transform=train_transform) val_dataset = datasets.ImageFolder(root='dataset/val', transform=val_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=0, pin_memory=True) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=0, pin_memory=True)这里有三个需要注意的参数。num_workers在Windows上建议设为0,否则经常报BrokenPipeError,这是因为Windows下多进程DataLoader的兼容性比较玄学;pin_memory=True在CPU训练时没有明显收益,但在GPU训练时可以加快host到device的传输;shuffle在训练集必须打开,否则模型会学到样本顺序里的虚假模式。归一化的mean和std是三通道RGB的标准ImageNet统计值,不是你想当然的[0.5, 0.5, 0.5],写错的话模型收敛会很慢。
3.2 样本不均衡:有的垃圾类别图片多,有的少,怎么办
垃圾分类数据集里,可回收物和有害垃圾的数量往往差得很远。一个极端不平衡的训练集,会让模型偏向样本多的类别,导致有害垃圾的recall低得可怜。处理的办法有三个:对少数类做过采样、对多数类做欠采样、在loss里加类别权重。
一个直接可用的方案是在交叉熵loss中传递class_weight参数,权重按类别样本数的倒数计算:
# train_utils.py import torch from torch.nn import CrossEntropyLoss def make_class_weight(labels): # labels: 长度为训练集样本总数的类别序号List cnt = {} for y in labels: cnt[y] = cnt.get(y, 0) + 1 total = len(labels) # 权重 = 总样本数 / 类别数量 / 该类样本数 weight = [total / (len(cnt) * cnt[i]) for i in range(len(cnt))] return torch.tensor(weight, dtype=torch.float32) criterion = CrossEntropyLoss(weight=make_class_weight(all_train_labels))这个weight列表会放进PyTorch的CrossEntropyLoss中,损失函数会把少数类的梯度放大。这是性价比最高的处理方式,因为不需要改动数据读入代码,也不需要对图片做额外变换。
数据增强方面,我的建议是:翻转、旋转、随机裁剪这三个操作是安全牌,不需要上CutMix、MixUp这类高级技法。垃圾分类图片背景相对干净,旋转角度不要超过20度,否则瓶子倒了、盒子歪了,语义信息反而被破坏。
3.3 公开数据集的下载与目录整理脚本
网上能找到的垃圾分类公开数据集有华为云垃圾分类数据集、垃圾图片分类数据集(Garbage Classification)等,zip包项目里一般会给下载链接。如果你拿到的数据是分散的、没有按ImageFolder格式组织,写一个脚本自动归位:
# organize_data.py import os import shutil import random def split_dataset(src_root, train_ratio=0.8): # src_root: 原始数据集根目录,子文件夹名为类别名 # 目标结构:dataset/train/某个类/xxx.jpg, dataset/val/某个类/xxx.jpg dst_root = 'dataset' os.makedirs(dst_root, exist_ok=True) for class_name in os.listdir(src_root): class_dir = os.path.join(src_root, class_name) if not os.path.isdir(class_dir): continue files = os.listdir(class_dir) random.shuffle(files) split_idx = int(len(files) * train_ratio) train_files = files[:split_idx] val_files = files[split_idx:] # 拷贝到新目录,目录名保留中文 for split_name, split_files in [('train', train_files), ('val', val_files)]: dst_dir = os.path.join(dst_root, split_name, class_name) os.makedirs(dst_dir, exist_ok=True) for f in split_files: src = os.path.join(class_dir, f) dst = os.path.join(dst_dir, f) if not os.path.exists(dst): shutil.copy(src, dst) if __name__ == '__main__': split_dataset('raw_data')运行前先在src_root下放一个test.jpg做全流程验证,防止写路径时大意导致全盘复制失败。这脚本里唯一需要你改的是train_ratio,如果数据量少于2000张,建议设成0.85,保留多一点训练样本。
4. 模型训练与评估:迁移学习的三行关键代码和一套监控指标
4.1 用torchvision加载ResNet18并替换分类头
既然前面选了迁移学习,这里直接给出最小训练配置。核心动作是三个:加载预训练模型、冻结特征层、替换全连接层。
# train.py import torch import torch.nn as nn from torchvision import models # 1. 加载预训练权重,最好指定weights参数而不是从旧版写法里扣 model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 2. 冻结全部参数 for param in model.parameters(): param.requires_grad = False # 3. 替换最后一层全连接,num_classes要换成你自己的类别数 num_classes = 4 # 比如:可回收、有害、厨余、其他 model.fc = nn.Linear(model.fc.in_features, num_classes) # 4. 只把分类头参数交给优化器 optimizer = torch.optim.Adam(model.fc.parameters(), lr=1e-3)逻辑说明:requires_grad=False意味着反向传播跳过这些层,训练时只更新最后一层,这既能大幅减少计算量,也能防止数据量小时把预训练特征破坏掉。Adam的lr从1e-3起步是保守选择,如果loss下降太慢,可以提到5e-3,但超过1e-2容易震荡。
训练主循环我不放完整代码,太长。你只需要知道:每个epoch遍历train_loader,把图片和标签放到同一个device上,前向传播计算loss,backward之后optimizer.step()把梯度更新到模型参数。验证集每轮结束后跑一次准确率。
4.2 如果精度不够,这两行参数比换模型更有效
一组值得试的调参顺序:先确认分类头训练到收敛,再解冻最后一两个block微调。
# 解冻layer4,配合更低的学习率 for name, param in model.named_parameters(): if 'layer4' in name or 'fc' in name: param.requires_grad = True # 分组学习率:分类头1e-3,解冻部分1e-4 optimizer = torch.optim.Adam([ {'params': model.fc.parameters(), 'lr': 1e-3}, {'params': model.layer4.parameters(), 'lr': 1e-4} ], weight_decay=1e-4)这个操作在业界叫“微调最后一层”,是精度的主要来源。原因很简单:预训练模型已经在ImageNet上见过大量纹理、轮廓、色彩特征,垃圾图片的语义信息集中在局部纹理上,因此只需微调最高层特征,不必动底层。若解冻全部层,在几千张图片上训练,过拟合几乎是必然的。
4.3 训练循环里的三个监控指标
训练时不要只盯着loss打印,应该额外保存三个东西:每个epoch的平均loss、验证集准确率、每个类别的准确率。
# evaluate.py def evaluate(model, val_loader, device, class_names): model.eval() correct = 0 total = 0 class_correct = [0] * len(class_names) class_total = [0] * len(class_names) with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) preds = outputs.argmax(dim=1) for i in range(len(labels)): cls = labels[i].item() class_total[cls] += 1 if preds[i].item() == cls: correct += 1 class_correct[cls] += 1 total += labels.size(0) overall_acc = correct / total print(f'Overall Acc: {overall_acc:.4f}') for i, name in enumerate(class_names): cls_acc = class_correct[i] / max(class_total[i], 1) print(f'{name}: {cls_acc:.4f}') return overall_acc类别准确率比总准确率重要得多。总准确率会被样本多的类别拉高,而有害垃圾分错,在答辩时是硬伤。如果某个类别的准确率低于70%,优先怀疑样本量不够,或该类图片背景和另一个类相似,而不是急着换网络结构。
训练中途可以用early stopping:连续5个epoch验证集准确率不再提升就终止并保存当前最优模型。模型权重用torch.save(model.state_dict(), 'best_model.pth')保存,不要保存整个模型对象,前者的文件更小,且在不同机器上加载兼容性更好。
5. 常见问题排查与避坑指南:我被这些坑各卡过一次
5.1 解压zip一直提示文件损坏
现象:Windows 11自带的资源管理器解压项目zip时,报“压缩文件已损坏”,重下两遍都一样。
原因:这类项目zip为了压缩效率或防盗,加了伪加密标志位。文件本身完整,但Windows内置解压工具不认这一位,Linux下的7z和macOS下的The Unarchiver能正常识别。还有一个原因是文件下载时网络中断导致zip包不完整,但这种情况校验大小就能发现。
解决:先试试用7-Zip打开,能打开就直接解压;如果7-Zip也提示头部损坏,在Linux下执行:
7z x 基于python与深度学习的垃圾分类系统设计与实现(高分项目).zip若7z无法处理,直接unzip命令加-O参数指定编码,处理中文文件名乱码:
unzip -O gbk 基于python与深度学习的垃圾分类系统设计与实现(高分项目).zip提示:Windows下解压后的中文文件名乱码,多半是zip内部文件名编码用了GBK而不是UTF-8,这种情况不要在Windows里改名,否则模型训练脚本里写死的路径会失效。
5.2 num_workers报BrokenPipeError
现象:Windows环境下一跑DataLoader就崩,错误日志一大堆BrokenPipeError。
原因:Windows没有Linux的fork机制,PyTorch DataLoader的多进程在主程序被if __name__ == '__main__'保护不当时,经常出这个问题。此外,num_workers不为0时,Windows下每次启动训练都要重新拉起子进程。
解决:训练脚本的入口必须写成if __name__ == '__main__':包裹,把DataLoader的num_workers设为0,或者减小batch_size。如果一定要用多进程,把DataLoader放到if块内部创建。
5.3 模型训练loss不降,准确率一直在20%左右
现象:分类问题有4个类,训练了10个epoch,loss从2.0降到1.8之后几乎不动,验证集准确率25%上下。
原因:分类头的输出维度不对,或者标签从0开始编号但数据集类别文件夹排序和期望不一致。20%这个数值几乎就是“模型在瞎猜”的信号。
解决:先检查num_classes是否等于数据集里的类别文件夹数量。再打印train_dataset.class_to_idx,确认类别名到索引的映射是否符合预期。常见坑是ImageFolder按文件夹名字典序分配索引,如果你的文件夹叫“可回收物、有害垃圾”这种中文名,排序不一定和你的预期对应。
5.4 Linux服务器上提示python环境不对,torch导入失败
现象:把项目传到Linux服务器上,一运行import torch就报ModuleNotFoundError或者Illegal instruction。
原因:服务器上可能装的是系统自带python3,而项目是在Windows的Anaconda里写的。torch不同版本对应不同CUDA版本,CPU版和GPU版的whl包不能混用。在Linux离线环境下,很多人用pip install在线失败,转而下zip包离线安装,反而容易装错版本。
解决:在项目根目录建一个requirements.txt,固定版本号:
torch==2.1.2 torchvision==0.16.2 flask==3.0.2 pillow==10.2.0 numpy==1.26.4然后用以下命令创建独立环境:
conda create -n waste python=3.10 conda activate waste pip install -r requirements.txt如果目标机器完全离线,copy一个torch的whl或zip包过去离线安装,但要先确认python版本兼容。
5.5 Web端上传图片后一直转圈,后端没有任何反应
现象:前端选了图片,点击识别按钮,页面一直loading,后端控制台什么都没打印。
原因:最常见是前端请求的URL和后端Flask路由不一致,比如前端请求/predict,后端实际注册的是/predict/,多了个结尾斜杠。另一个可能是后端代码里图片读取时用cv2.imread()读的是中文字段名路径,OpenCV不支持中文路径,静默失败。
解决:先在后端加一行日志打印:
@app.route('/predict', methods=['POST']) def predict(): file = request.files.get('image') print('received file:', file.filename) # 用PIL代替cv2读取,PIL对中文路径的支持好得多 from PIL import Image img = Image.open(file.stream).convert('RGB')关键点是图片传进来后不要走临时文件再读取,直接用file.stream喂给PIL,能避开文件名字符编码问题。如果还不行,打开浏览器开发者工具看Network面板,确认请求有没有发出去、返回的状态码是什么。
6. 进阶玩法:画一张混淆矩阵热力图,让答辩老师一眼看懂你的模型
前面所有步骤跑通后,模型在验证集上准确率可能到了85%以上,但答辩时老师问“哪些类别容易混”,你如果只报一个总准确率就太浪费了。我习惯在每个项目收尾时加一个混淆矩阵可视化脚本,既验证模型短板,又是答辩加分项。
混淆矩阵的行是真实类别,列是预测类别。对角线越亮,说明该类别识别越好;非对角线亮点则是指向“易混淆对”。
# confusion_matrix.py import torch import matplotlib.pyplot as plt import numpy as np from sklearn.metrics import confusion_matrix def plot_confusion_matrix(model, val_loader, device, class_names, save_path='confusion.png'): model.eval() y_true, y_pred = [], [] with torch.no_grad(): for images, labels in val_loader: images = images.to(device) outputs = model(images) preds = outputs.argmax(dim=1).cpu().numpy() y_pred.extend(preds) y_true.extend(labels.numpy()) cm = confusion_matrix(y_true, y_pred) # 归一化,方便比较类别间的混淆程度 cm_norm = cm.astype('float') / cm.sum(axis=1, keepdims=True).clip(min=1e-9) fig, ax = plt.subplots(figsize=(8, 6)) im = ax.imshow(cm_norm, cmap='Blues') ax.set_xticks(range(len(class_names))) ax.set_yticks(range(len(class_names))) ax.set_xticklabels(class_names, rotation=45, ha='right') ax.set_yticklabels(class_names) for i in range(cm.shape[0]): for j in range(cm.shape[1]): text = f'{cm_norm[i, j]:.2f}' ax.text(j, i, text, ha='center', va='center', color='white' if cm_norm[i, j] > 0.5 else 'black') ax.set_xlabel('Predicted') ax.set_ylabel('True') fig.colorbar(im, ax=ax) fig.tight_layout() fig.savefig(save_path, dpi=150) print('Confusion matrix saved to', save_path)这段代码的三个要点:normalize按行做,得到的是recall视角的混淆比例,比看原始计数直观;cm.sum(axis=1, keepdims=True).clip(min=1e-9)防止某类在验证集上为0时除零报错;color切换条件用的0.5阈值,让色块文字始终清晰可读。
跑这个脚本的时机很有讲究。我通常是训练结束后先跑一次,如果发现“可回收物”和“有害垃圾”混淆严重,就去翻几张三张典型的错误样本,看看是背景颜色接近,还是某些图片根本就是近义词标签下错了。这种错误分析,比再调一轮learning rate更能说明你理解了模型。看到混淆矩阵里某两类交叉高亮,最稳妥的补救办法是检查数据集里这两类的图片质量——模糊图、缩略图、带水印的图,它们才是准确率上不去的“隐藏杀手”,而不是网络不够深。
从我自己的经验看,一次完整的垃圾分类项目,数据处理占一半时间,模型训练只占两成,剩下的时间全花在“让系统稳定跑完一个演示流程”上。做这个项目前,我也总以为调参是最难的部分,后来翻了五次车才明白:把数据整理干净、处理好中文路径和zip伪加密这类边角料问题,整个系统其实已经成功了大半。希望这篇踩坑记录能帮你跳过那些不值得再踩的坑。
本文还有配套的精品资源,点击获取