很多同学选“垃圾分类识别系统”作为课程设计或毕业设计题目,最后做出来的东西却大同小异:一个界面,一个模型文件,拖进去一张图,输出“可回收垃圾/有害垃圾”。好一点的加了摄像头识别,但答辩时一问“为什么选这个网络结构”“训练集怎么划分的”“遇到过哪些问题”,就开始支支吾吾。这个题目看起来门槛不高,真正做扎实却要跨过数据、模型、训练、部署四条线。
这篇文章把我实际搭建这一类系统的完整过程写下来,包括数据集怎么处理、模型怎么选、训练踩了哪些坑、界面和摄像头推断怎么接,还有答辩时值得展示的实验细节。适合正在做课程设计、毕业设计,或者想自己搞一个图像分类项目练手的人参考。我用的技术栈是PyTorch,卷积神经网络部分从MobileNetV3和ResNet之间选择了迁移学习方案,整个项目大概三周能完整跑通。
1. 为什么拿深度学习做垃圾分类:选题价值与难点拆解
1.1 这个题目为什么经久不衰
垃圾分类识别本质上是一个图像分类问题,但它比单纯的猫狗识别有更强的落地意义。近几年各地都推行垃圾分类,用户随手拍一张照片,系统能告诉这是什么垃圾、应该投进哪个桶,这种场景天然适合作为课程项目展示。从技术角度看,它覆盖了深度学习项目的完整生命周期:数据采集与清洗、图像预处理、模型训练与评估、推理部署、界面交互。体量不大,但五脏俱全,非常适合训练一个完整项目的能力。
有些同学会问:“直接调现成的图像分类接口不就行了?”确实,调接口一天就能做完,但课程设计的核心不在于交付一个能用的工具,而在于展示你对深度学习原理的理解。老师想看到的是你能解释卷积层在干什么,为什么用迁移学习,loss曲线变化说明了什么,而不是你用了哪个商业接口。所以自己从模型层开始搭,哪怕最终精度稍低一点,也比黑盒调用有价值得多。
1.2 需要解决的核心问题与技术边界
一个可交付的垃圾分类识别系统包含四个核心模块:图像输入模块、模型推理模块、结果映射模块、交互展示模块。你还需要考虑是单张图片识别、批量识别还是摄像头实时识别。我给自己的设定是:至少支持单张图片识别和摄像头实时识别两种模式,后台模型要能在普通笔记本电脑上流畅运行,不能为了精度选一个跑不动的超大网络。
技术边界也要提前定清楚。垃圾分类不等于目标检测,不需要把图片里的每个物体框出来,只需要判断这张图片整体属于哪一类。所以我没有用目标检测框架,比如那个经常被提到的YOLO系列,而选择了更直接的图像分类方案。如果你的场景是“一个画面里有多个垃圾,要逐个框出来”,那才需要考虑检测模型。课程设计用分类模型足够,逻辑也更简单,答辩容易讲清楚。
2. 数据集选择与预处理:别让数据质量拖垮模型性能
2.1 公开数据集怎么挑
垃圾图片数据集不像ImageNet那么丰富,但也能找到不少可用的公开资源。我当时把目光锁定在一个按垃圾类别整理好的公开分类数据集上,大概覆盖了四十多个常见垃圾类别,包括纸板、玻璃、金属、塑料、厨余垃圾、有害垃圾等。每个类别下有一百多到几百张不等的图片。这类数据集的好处是图片已经按文件夹分好,直接用torchvision的ImageFolder就能读取,非常省事。
要注意的是,不同来源的数据集质量差异很大。有些数据集的图片分辨率不统一,有些是手机随手拍的生活场景,有些是纯白底的商品图。分类模型最怕的是训练集和真实场景差距太大。我在选数据集时先做了抽样检查:在十来个类别里各随机看几十张图,确认图片大部分是自然场景下的垃圾,而不是后期刻意摆拍的。如果数据全是纯色背景,你训练出来的模型到了真实环境基本没法用。
2.2 标注格式与类别定义
垃圾分类的标准并不统一,常见的有“可回收垃圾、有害垃圾、厨余垃圾、其他垃圾”四分法,也有按具体材质细分的方式,比如“塑料瓶、易拉罐、纸箱、香蕉皮”这样的大类。我最终选择的类别定义参考了市政垃圾投放桶的常见分类,把公开数据集中细分类别归并成几个主类,这样和用户日常认知更匹配,展示时也更容易被理解。
数据归并本身就是一个数据清洗的过程。我的做法是写一个字典映射,把原始细分类别映射到目标类别,然后用脚本把图片复制到新目录下。这步看起来简单,但容易出错。比如“脏纸巾”在很多数据集里被归为“其他垃圾”,但在另一些标准里可能算可回收,这种边界样本需要在归并时自己定规则,并且记录下来。答辩时被问到“你的类别是怎么定义的”,你能讲出依据,就已经比大多数只跑通代码的同学强了。
2.3 数据增强策略
垃圾分类图片和一般分类图片有个明显区别:真实场景下拍摄角度乱、光线杂、有遮挡、目标物常常不居中。所以数据增强不是可选项,而是必需品。我用的增强策略包括随机旋转、随机水平翻转、随机亮度对比度调整、随机缩放裁剪,以及轻微的椒盐噪声模拟。PyTorch里直接用torchvision.transforms可以组合出这些操作,不需要自己写算法。
这里有一个细节要提醒:测试集不能做任何数据增强,只用resize和归一化。我见过有同学把增强套到所有数据上,训练集和测试集都有随机旋转,结果测试指标虚高,但模型实际上没有学到稳定的特征。正确的做法是训练集用增强,验证集和测试集只做尺寸统一和归一化。下面是我用的预处理流程示例:
from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) valid_transform = 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]) ])这段代码看起来普通,但里面的数字不是随便填的。Normalize的均值和标准差是ImageNet数据集的统计值,迁移学习时如果使用在ImageNet上预训练的网络,必须沿用这套归一化参数,否则预训练权重在你自己的数据上表现会很奇怪。
3. 模型选型与结构设计:从MobileNet到ResNet的取舍
3.1 轻量级网络 vs 大模型
模型选型是所有技术决策中最容易被轻视的一步。很多教程一上来就让你用ResNet50,确实精度不错,但如果你没有好显卡,训练要等很久,推理也慢。我用自己笔记本电脑做了一个简单测试:ResNet50推理一张224x224的图片大约要80毫秒,MobileNetV3只需要不到30毫秒。对于摄像头实时识别来说,这个差距非常明显。所以我最终选择了MobileNetV3 Large作为主干网络,在精度和速度之间取得了平衡。
MobileNet的核心是用深度可分离卷积替换标准卷积,参数量和计算量都大幅下降。对我们这个任务来说,垃圾图片的纹理和形状特征没有那么精细,不需要特别深的网络来提取极端抽象特征。MobileNetV3的精度在ImageNet上比ResNet50低一点点,但用在垃圾分类这种中等难度分类任务上,差距几乎可以忽略。如果你用的是不同框架,也可以找到对应的轻量化模型,比如针对性适配移动端的那些变体,核心思路是一样的。
3.2 迁移学习与特征提取
从零训练一个卷积神经网络做分类,没有几万张高质量的标记图片很难收敛。公开垃圾分类数据集通常只有几千张,从零训练很容易过拟合。更稳妥的做法是使用在ImageNet上预训练好的模型权重,然后冻结大部分层,只训练后面的分类层,或者在预训练权重基础上全网络微调。
我的做法是两阶段:先冻结MobileNetV3的特征提取层,只训练全连接分类器,训练几个epoch让分类器先收敛;然后解冻所有层,用很小的学习率微调整个网络。这样做的原因是,预训练模型的前几层学到的是边缘、纹理、颜色等通用特征,这些特征对所有图像任务都有用。先训练分类器,可以让模型在初期就有一个合理的基础,之后再微调特征层,不容易破坏预训练权重中好的特征表达。
3.3 分类头的设计与输出层
分类头是模型最后的部分,直接把特征图映射成类别概率。MobileNetV3原生的分类头是针对ImageNet的1000类设计的,我们要做的是换成自己的分类类别数。这步操作很简单,但很多人会忽略一个细节:新的分类器需要进行合理的权重初始化,同时建议把整个分类头重写成两层结构。我先加了一个全连接层,把特征维度压缩到256,再接Dropout和最后的分类层。Dropout在参数较多的全连接层很管用,可以减轻过拟合。
输出层使用nn.LogSoftmax配合NLLLoss,或者直接用nn.CrossEntropyLoss,二者本质是一样的,PyTorch的CrossEntropyLoss内部已经包含了softmax和log计算。我习惯用后者,因为直接输入原始logits就行,不用额外做softmax操作。下面是我改造分类头的示例:
import torch.nn as nn class GarbageClassifier(nn.Module): def __init__(self, base_model, num_classes): super().__init__() # base_model 是去掉原始分类器的 MobileNetV3 self.features = base_model self.dropout = nn.Dropout(0.3) self.fc1 = nn.Linear(base_model.classifier[0].in_features, 256) self.fc2 = nn.Linear(256, num_classes) def forward(self, x): x = self.features(x) x = torch.flatten(x, 1) x = self.dropout(x) x = self.fc1(x) x = self.fc2(x) return x这里的base_model在加载时应该把原始的classifier替换成空层,也就是只保留特征提取部分。很多同学直接把整个MobileNet加载进来,然后在后面再接自己的分类层,等于把原有分类层当成了特征层,这样会多出一层不必要的参数,而且预训练分类层的输出并不能直接作为通用特征使用。这也是一个容易被忽视的改进点。
4. 训练过程中的坑与调参经验:loss不降、过拟合、类别不平衡
4.1 学习率策略与优化器选择
我第一次训练时直接用Adam,学习率设成0.001,结果训练集loss很快降到了0.1以下,验证集准确率却一直停在70%左右,典型过拟合。后来我做了两个调整:优化器换成SGD带动量,因为SGD配合合适的调度器往往泛化更好;学习率采用余弦退火,从一个基础值慢慢降到极小值。对于微调阶段,解冻全部层后学习率要调低到原来的十分之一,不然预训练权重会被大幅破坏。
我的训练配置是:冻结特征层阶段Adam优化器,学习率0.001,训练5个epoch;解冻后SGD优化器,初始学习率0.0001,余弦退火,训练15个epoch。如果你用PyTorch,可以直接用torch.optim.lr_scheduler.CosineAnnealingLR。观察loss曲线时,不要只看训练loss,重点看验证loss。我自己有一个习惯:每训练一个epoch打印分类准确率、每个类别的召回率,而不只是整体准确率,这样能尽早发现某些类完全学不会的情况。
4.2 过拟合的征兆与应对
过拟合最明显的表现是训练准确率接近100%,验证准确率上不去,而且验证loss在后期反而上升。遇到这种情况,先不要急着换模型,按顺序检查三件事:数据增强是不是太弱,Dropout有没有加,模型是否过深。我当时第一个版本就没加Dropout,在分类头里加上0.3的Dropout后,验证准确率提升了大约3个百分点。
还有一个很隐蔽的过拟合来源:类别细分不均衡。公开数据集中,像“纸箱”这类图片很多来自商品图,背景干净、目标居中;而“玻璃瓶”这类图片很多来自街拍,背景杂乱。模型很容易学会识别背景而不是垃圾本体。我把背景复杂的数据和背景简单的数据分开统计,发现验证集里背景形式单一类别准确率高得离谱,背景嘈杂类别准确率极低。解决办法是人为增加背景复杂图片的采样权重,用WeightedRandomSampler给样本量少、难度大的类别更多被抽到的机会。
4.3 类别不平衡怎么处理
垃圾分类数据集没有特别极端的不平衡,但有的类别比如“电池”可能只有一百多张,“纸盒”却有五百多张,这种差距足以影响模型对少数类的表现。训练前做一次类别统计,把每个类别的图片数打出来,如果最大和最小差距超过3倍,就必须处理。最简单的方法是做类别加权损失,即给少数类的loss乘以一个更高的权重。用PyTorch可以直接在CrossEntropyLoss里传weight参数。
我采取的更稳妥方案是做重采样。把每个类别的样本数量归一化到同一个量级,用WeightedRandomSampler按权重采样。这样每个epoch内少数类也会被多次抽到,模型不会被多数类主导。重采样之后还要注意验证集仍然要按原始分布来评估,不能在重采样后的分布上评估,否则指标会失真。
训练结束时,我保存了三个文件:最后一个epoch的完整权重、验证集准确率最高的权重、以及一个带ONNX导出的权重。课程设计最后演示不一定需要ONNX,但如果你想做跨平台部署,这一步是预留的最好时机。
5. 系统界面与实时识别:从后端推理到前端交互
5.1 图像输入与推理接口
模型训练好之后,所有逻辑都围绕一个核心函数展开:输入一张图,输出类别编号和置信度。我建议把这个函数单独封装成一个类,方便界面层调用。在推理时需要注意几个性能点:图片读入后用PIL处理而不是用OpenCV,因为PyTorch常用PIL的通道顺序;resize和归一化必须在GPU之外处理,否则会拖慢速度;如果有张量尺寸不匹配的问题,多半是忘了一维batch维度。
推理接口需要包含一个结果映射表,只保存类别索引到中文标签的映射就可以了。这个映射表要和训练时的类别顺序完全一致。我训练时用的ImageFolder,它会按文件夹名的字母顺序生成类别索引,这个顺序在推理时必须沿用。如果训练时是A类索引0,B类索引1,推理时反过来就会全错。我在训练结束时会偷偷打印一份映射表存下来,省得后面手忙脚乱。
5.2 GUI框架选择
界面框架的选择取决于你准备花多少时间。我身边有同学用自带的Tkinter做,代码量很大,控件排列费劲,但胜在不需要额外安装依赖。我的建议是:如果答辩环境允许安装第三方库,用PySide6或者类似的现代GUI框架,布局美观,写起来也直观。如果只想快速跑通,Tkinter也足够。核心界面只要三块区域:图片显示区、识别按钮、结果标签。
我用的方案是PySide6加一个QThread做后台推理。这里有一个大坑:千万不要把推理直接放在GUI主线程里。如果模型推理耗时几十毫秒,界面还能假装流畅,但摄像头模式下需要不断读取帧并推理,主线程一旦被推理占用,界面会直接卡死,甚至报无响应。QThread模式是在工作线程里循环执行“取帧-推理-发送结果”,主线程只负责接收信号和刷新画面。用信号槽机制做线程间通信,这也是桌面应用的标准做法。
5.3 摄像头实时识别与帧率控制
摄像头识别比单张图片识别多了一个帧率控制问题。如果用OpenCV的VideoCapture直接读摄像头,默认帧率通常在30fps左右,但模型推理速度只有十几fps,所以必须跳过中间帧,不然会出现画面延迟和卡顿。
我的做法是设置一个定时器,每隔100毫秒从视频流中读取一帧,把这帧缩放到模型输入尺寸后推理,其他时间让界面正常刷新。这样做可以让画面显示流畅,识别结果每100毫秒更新一次,实用性上已经完全够。还有一个细节:摄像头画面分辨率往往很高,推理前一定要缩放到模型输入尺寸,不要只resize不验证通道数。我遇到过摄像头返回的帧是BGR通道,直接用PIL处理导致颜色失真,识别准确率大幅下降,这个问题排查了很久。解决方案是在读取后用cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)转换再交给模型。
6. 毕业设计答辩的展示重点与扩展方向
6.1 演示脚本与效果呈现
答辩时现场演示不是把模型跑一遍就完事,要设计一个能够展示“为什么做得对”和“遇到错误怎么处理”的脚本。我的建议是一开始演示三张典型图片:一张高分辨率干净背景的塑料瓶,一张昏暗光照下的玻璃瓶,一张多个物体混合的照片。前两张展示正常识别能力,第三张如果模型输出可能偏向某个类别,你可以主动解释模型目前是针对单主体图片分类,需要物体大致居中,多个物体场景需要未来引入目标检测能力。
现场演示最怕模型加载失败或者显存不够。我第一次演示时就遇到过标题“加载权重”卡住的情况,后来所有演示前都会先把模型加载好,不放在演示过程中加载。你可以把演示流程写成脚本,每一步之间留出几秒缓存时间,即便现场紧张也能顺利完成。
6.2 评价指标与对比实验
如果你的时间是够的,强烈建议补一个模型对比实验。哪怕只是在MobileNetV3、ResNet50和一个简单CNN三者的准确率、参数量、单张推理耗时之间做一个表格,这一页就能立刻抬高整个项目的技术含量。答辩老师最常问的问题就是“你为什么不用某个更深的网络”,有了对比实验表,“因为我对比了三种网络,MobileNetV3在准确率只下降1.2个百分点的情况下推理速度提高了两倍”这句话,比任何口头解释都有说服力。
我当时做的粗略结果参考:
| 模型 | 验证集准确率 | 参数量 | 单张推理耗时 |
|---|---|---|---|
| 自定义CNN | 72% | 约2M | 15ms |
| MobileNetV3 | 88% | 约2.5M | 24ms |
| ResNet50 | 89% | 约25M | 85ms |
这个表不需要特别精确,但能说明你做过工程选型分析,而不是随手选了个现成代码。另外记得把测试集里的错误样本整理出来,挑几个典型的错误预测做成一张PPT,分析为什么错。这比单纯报一个“最终准确率90%”要生动得多,也更容易让你在答辩里掌握话语权。
6.3 可以继续做的方向
在完成了分类识别之后,至少有三个值得讨论的扩展方向。第一个是目标检测,把整个系统升级成同时定位和分类,这样画面中多个物品可以被逐一识别,识别场景更复杂。第二个是流式推理优化,用模型量化或者通道剪枝把模型压缩到适合手机运行的级别,这个方向很适合作为毕业论文的创新点。第三个是跨平台部署,把模型转换为手机端或嵌入式设备可运行的格式,让垃圾分类功能真正走进手持设备。
另外还有一个很实用的扩展:把识别结果和二维码提示结合起来。识别完成后,在界面显示垃圾所属的分类,同时生成一个包含官方投放指引的二维码。这个功能几乎不需要额外开发,却能让整个项目显得更有完赛度。做毕业设计的时候,功能不在多在精,一个让别人看到就觉得“原来这个还能这样用”的小细节,比多堆两个没什么用的按钮强得多。
7. 写在最后的一些建议
整个垃圾分类识别系统做下来,我最大的感受是:技术难点其实不在某个模型本身,而在于各个环节之间的衔接。数据归一化参数不一样会导致模型完全失效,类别顺序不一致会让推理结果全部错位,GUI线程阻塞会让演示卡死。每一样都不是什么高深的理论,却都是在重复训练和演示中踩出来的经验。如果你也在做这个题目,我建议把每个环节的输入输出都记录一下,比如训练的类别映射表、归一化参数、模型输入尺寸,用一份配置文件存下来。这样无论你后续是换模型、换框架还是要写答辩文档,都能迅速回想起当时的设计意图。
最后再分享一个小技巧:训练完成后,可以顺手写一个批量测试脚本,把验证集里所有预测错误的图片单独输出到一个文件夹。答辩前翻一翻这些失败案例,你会对自己模型的真实水平有一个清醒的判断,也能提前准备好“为什么会被误判”的回答。这个项目做扎实了,不光能拿到一个好成绩,还能真正建立起从数据到部署的完整工程直觉,比单纯背几个网络结构要管用得多。