简介:这是一份基于Python的垃圾识别分类系统源码压缩包,面向希望入门图像识别与垃圾分类项目的开发者。系统以卷积神经网络等模型为基础,覆盖数据预处理、模型训练、评估与推理等完整流程,可帮助读者理解从图片输入到垃圾类别输出的端到端实现思路。压缩包共28个文件,大小约1.79MB,以Python脚本为主,辅以XML配置、Markdown/Word/PDF文档及工程配置文件,既包含关键算法代码,也提供项目计划、详细设计、风险管理等文本资料,适合作为课程设计或毕业设计的参考素材。已有336人学习下载,源码目录结构清晰,便于按模块研读。通过研读其中的训练与推理脚本,读者还能进一步掌握常见深度学习库的实际用法和调参细节。
1. 一个 Python 写的垃圾识别分类系统:源码里藏着什么
我第一次拆这个garbage-classification-main源码包时,第一反应是“终于有人把毕设级项目打包得这么完整了”。它不是那种只有一个train.py完事的玩具,而是把数据组织、模型训练、推理测试、Tkinter 图形界面、项目计划书和风险管理文档全部塞进了同一个压缩包。换句话说,你拿到的是一套可以直接跑通的垃圾分类图像识别系统,而不是一段写了开头就断了的半成品代码。适合两类人:一类是正在做图像分类课程设计或毕设的学生,想找一个结构完整、有文档支撑、能答辩的项目;另一类是刚入门深度学习、想搞清楚 CNN 和迁移学习在真实图片分类任务上怎么落地的开发者。这套源码给了你两条训练路线——自己搭 CNN,以及用 MobileNet 做迁移学习,后面所有章节都围绕这两条路线展开。
2. 数据与模型选型:先搞清楚这六类垃圾怎么进网络
2.1 数据集目录约定:Dataset.md 告诉你的关键信息
打开压缩包,第一个值得阅读的文件其实是Dataset.md。绝大多数图像分类项目的数据集不会以单个文件形式存在,而是按“每个子文件夹代表一个类别”的方式组织。这个项目沿用了我见过的主流垃圾分类数据集结构:训练集根目录下有六个子文件夹,分别对应可回收垃圾中的纸板、玻璃、金属、纸张、塑料,以及不可回收的其他垃圾。你不需要手动写标签文件,因为 Keras 的flow_from_directory会自动扫描子文件夹名并生成类别索引。
这种组织方式有一个很实际的好处:如果你想换成自己的数据集,不用改训练代码,只需要保证新数据也按“类别名作为文件夹名”的结构摆放。我一般会先跑一个快速脚本确认目录层级没摆错,否则后面所有训练都是白跑。
# 查看数据集目录结构(前两层) find garbage-classification-main/dataset -maxdepth 2 -type d | head -20这段命令的作用是列出数据集目录的前两层文件夹结构,-maxdepth 2限制递归深度为两层,type d只显示目录。如果输出里出现类似dataset/train/cardboard、dataset/train/glass这样的路径,说明数据组织正确。常见的问题是有人把图片直接散落在dataset根目录下,或者多套了一层train/train,这两种情况都会让flow_from_directory在运行时直接报“找不到类别目录”的错误。
2.2 CNN 与 MobileNet:两套训练脚本的定位差异
源码里提供了train_cnn.py和train_mobilenet.py两个训练入口,对应两种完全不同的建模思路。train_cnn.py是从零搭一个小型卷积神经网络,输入层接Conv2D,经过若干轮卷积池化后接全连接层输出分类概率。这种网络规模小、参数少,在自己的笔记本 CPU 上也能跑,训练一轮大概几分钟到十几分钟,适合用来理解卷积、池化、Dropout 这些基础概念。
train_mobilenet.py走的是迁移学习路线:加载在 ImageNet 上预训练好的 MobileNetV2 作为特征提取器,冻结主干参数,只训练新加的分类头。这套方案网上讨论很多,核心优势是收敛快、精度高,因为底层特征已经由千万级图片训练好了。代价是模型文件体积更大,推理时需要加载更多参数。在垃圾识别这种类别间视觉差异明显的任务上,MobileNet 迁移学习的准确率通常会明显高于从零训练的 CNN,这也是我把它推荐为“最终部署版本”的原因。
下面通过一张表直观对比两条路线的差异,方便你决定先跑哪一个。
| 对比维度 | train_cnn.py | train_mobilenet.py |
|---|---|---|
| 模型结构 | 自定义小型CNN | MobileNetV2 + 自定义分类头 |
| 依赖预训练权重 | 否 | 是(需下载 ImageNet 权重) |
| 训练速度 | 快(CPU可跑) | 较慢(建议GPU) |
| 预期准确率 | 中等 | 更高 |
| 适用场景 | 学习原理、快速验证 | 实际部署、追求精度 |
2.3 train_cnn.py 的关键参数:batch_size、学习率与图像增强
打开train_cnn.py,整个训练流程可以被压缩成“读取数据增强器 → 构建模型 → 编译 → fit”四步。源码里最值得你动手调整的是ImageDataGenerator里的那一串增强参数,它们直接决定模型见过多少种“变形”的垃圾图片。
# train_cnn.py 核心片段 from tensorflow.keras.preprocessing.image import ImageDataGenerator datagen = ImageDataGenerator( rescale=1.0 / 255.0, rotation_range=20, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2, horizontal_flip=True, validation_split=0.2 ) train_generator = datagen.flow_from_directory( 'garbage-classification-main/dataset/train', target_size=(224, 224), batch_size=32, class_mode='categorical', subset='training' ) val_generator = datagen.flow_from_directory( 'garbage-classification-main/dataset/train', target_size=(224, 224), batch_size=32, class_mode='categorical', subset='validation' )rescale把像素从 0-255 缩放到 0-1,这是神经网络训练的标配操作,不缩放的后果是梯度更新不稳定。rotation_range、width_shift_range、height_shift_range分别是旋转、水平平移、垂直平移的范围,zoom_range是缩放范围,horizontal_flip是随机水平翻转。这些增强手段相当于在原始数据基础上制造“新样本”,能有效抑制过拟合。
validation_split=0.2表示从训练目录中抽出 20% 作为验证集,flow_from_directory不会重复读取图片,而是按照内部索引切分。target_size设为(224, 224)与 MobileNet 的输入尺寸保持一致,这样如果后面想切换两套训练脚本,数据集不用重新处理。batch_size=32是一个在显存和梯度稳定性之间平衡的常见取值,显存不够可以降到 16。
3. 训练与推理验证:模型能不能用,别只看准确率
3.1 train_mobilenet.py 的迁移学习流程
MobileNet 训练脚本和 CNN 脚本最大的区别在于主干网络的构建方式。为了让预训练权重发挥最大作用,源码把base_model的trainable属性设为False,让预训练参数在初期保持不动,只训练新增的GlobalAveragePooling2D和Dense分类层。这种做法可以避免训练初期梯度噪声破坏已经学好的底层特征。
# train_mobilenet.py 核心片段 from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras.models import Model from tensorflow.keras.layers import GlobalAveragePooling2D, Dense, Dropout base_model = MobileNetV2(weights='imagenet', include_top=False, input_shape=(224, 224, 3)) base_model.trainable = False x = base_model.output x = GlobalAveragePooling2D()(x) x = Dense(128, activation='relu')(x) x = Dropout(0.5)(x) predictions = Dense(6, activation='softmax')(x) model = Model(inputs=base_model.input, outputs=predictions) model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])include_top=False表示去掉 MobileNetV2 自带的 ImageNet 分类头,只保留特征提取部分,这样输出的特征图才能被替换成你自己的六类分类头。GlobalAveragePooling2D把二维特征图压缩成一维向量,相比直接Flatten,它能大幅减少参数数量并降低过拟合概率。中间的Dense(128, activation='relu')是特征变换层,Dropout(0.5)随机丢弃一半神经元,防止全连接层记住训练集中的噪声。
这里有一个值得注意的细节:迁移学习的最佳实践是分阶段训练。先冻结主干训练几轮,让分类头收敛;然后解冻主干的后几层,用很小的学习率做微调。如果你直接解冻全部层并保持默认学习率,预训练权重容易被破坏,精度反而下降。源码没有强制分阶段,但我在实际项目中几乎总是手动改成两段式训练。
3.2 test_model.py 的推理链路
训练完成后,test_model.py负责把训练好的模型用在新图片上。这个脚本虽然短,但它是从“训练”过渡到“使用”的关键桥梁。整个推理流程可以拆成加载模型、加载图片、预处理、预测、解析结果五个环节。
# test_model.py 核心片段 import numpy as np from tensorflow.keras.models import load_model from tensorflow.keras.preprocessing import image model = load_model('models/trash_mobilenet.h5') class_names = ['cardboard', 'glass', 'metal', 'paper', 'plastic', 'trash'] def predict_image(img_path): img = image.load_img(img_path, target_size=(224, 224)) x = image.img_to_array(img) x = np.expand_dims(x, axis=0) x = x / 255.0 preds = model.predict(x)[0] idx = int(np.argmax(preds)) return class_names[idx], float(preds[idx])load_img加载图片并统一缩放到(224, 224),这是模型在训练时看到的输入尺寸,推理时保持一致是硬性要求。img_to_array把 PIL 图片对象转成 NumPy 数组,expand_dims在数组最前面增加一维,把单张图片变成(1, 224, 224, 3)的张量,因为模型要求第一维是批大小。predict返回的是六类概率的数组,argmax取出最大概率对应的索引,再映射回类别名。
这个流程看着简单,但有一个很容易踩的细节:源码训练时用的是flow_from_directory,它内部会自动做rescale=1/255;推理时如果你忘了手动除以 255,模型的输入分布就和训练时不一致,预测结果会严重漂移。很多“模型明明训练准确率很高,但测试就是乱猜”的问题,根源就在这一个除法上。
3.3 准确率之外的三个判断维度
模型训练完,test_model.py会在测试集上算准确率,但这个数字不一定代表真实场景表现。我做模型验证时通常会额外看三个东西。
第一是逐类别的分类报告,六类垃圾里往往有一两类准确率特别低。比如“纸张”和“纸板”在外观上高度相似,模型很可能把纸板当成纸张,整体准确率看不出来,但混淆矩阵里这两类的交叉错误非常扎眼。第二是置信度阈值,垃圾分类场景中,如果模型对某张图片的预测概率只有 0.4,更好的做法是标记为“不确定”而不是强行归类。第三是测试集与真实场景的分布差异,公开数据集的图片大多是单物体白底图,手机随手拍的照片背景杂乱、光线复杂,模型表现会明显下滑。
# 查看每类垃圾的精确率与召回率 from sklearn.metrics import classification_report y_true = [...] # 真实类别 y_pred = [...] # 模型预测类别 print(classification_report(y_true, y_pred, target_names=class_names))这段代码用classification_report输出每个类别的精确率、召回率和 F1 值。精确率衡量“预测为某类的图中有多少是对的”,召回率衡量“该类所有图中被找回来多少”。如果某类召回率特别低,说明模型老是漏掉它,需要补充该类数据或调整类别权重。
4. 踩坑记录:垃圾识别最容易翻车的四个地方
4.1 中文路径导致图片全部加载失败
现象:把压缩包解压到D:\垃圾识别\目录下,运行train_cnn.py,训练直接报错,提示找不到图片文件或文件夹为空。
原因:flow_from_directory底层依赖的文件读取、路径编码在 Windows 中文路径下容易出现兼容问题。Python 的open函数对 Unicode 路径的默认处理方式在不同版本上行为不一致,而 TensorFlow 的 C++ 底层代码对中文路径支持更差,尤其是路径中包含中文文件夹名时,图片加载会静默失败。
解决:把整个项目和解压目录放到一个纯英文路径下,比如C:\projects\garbage-classification\,并且数据集目录名也不要出现空格或中文字符。我自己的习惯是压缩包解压后,第一件事就是把路径复制出来检查一遍,确保没有一个中文字符,再开始装依赖。
4.2 Keras 版本更新后 h5 模型加载报错
现象:按照 README 装好 TensorFlow 后,运行test_model.py加载models目录下的模型文件,报错提示Unknown layer或AttributeError: module 'tensorflow.keras' has no attribute 'get_uid'之类的问题。
原因:不同版本的 Keras 对模型结构的序列化格式不完全兼容。2.x 版本保存的模型权重在 3.x 版本下加载时,某些自定义层或者默认参数定义发生了变化,反序列化就会失败。
解决:查看项目文档中要求的 TensorFlow 版本,然后创建一个虚拟环境,安装与模型保存时一致的版本。如果确认版本匹配仍报错,另一个可行方案是加载模型时指定compile=False,先把模型结构加载出来,再用model.load_weights单独加载权重。从那以后我每次拿到别人的源码包,都会先看一眼requirements.txt里锁定的版本范围,再决定装哪个环境。
4.3 类别不平衡让“塑料”永远猜不对
现象:训练完成后,整体准确率显示 0.85,但实际试图片时发现,无论上传什么塑料瓶,模型都倾向于预测成“金属”或“其他”。
原因:数据集里plastic类的图片数量远少于paper或cardboard类。模型在训练过程中看到塑料样本的次数少,决策边界被其他类别的样本主导,导致塑料类召回率偏低。整体准确率被大类别的正确预测撑起来了,但小类别几乎没学会。
解决:先统计每个类别的样本数量,计算最大值与最小值的比值。如果超过 2 倍,就需要在ImageDataGenerator中设置class_weight参数,给样本少的类别更大的损失权重。也可以在flow_from_directory中设置classes参数,按需控制每类读取数量。我的做法是两手抓:先计算类别权重,再对样本少的类别额外做旋转和缩放增强。
4.4 window_trash.py 窗口点开图片就卡死
现象:运行window_trash.py弹出图形界面后,点击“选择图片”按钮,界面瞬间卡住,几秒后才恢复,有时甚至直接无响应。
原因:Tkinter 是单线程 GUI 框架,源码如果在按钮事件回调里直接执行模型加载和预测,那么推理的时间会被 UI 线程阻塞。加载 MobileNet 模型本身就需要几秒,再加上图片预处理和预测,界面自然就像死掉了一样。
解决:凡是耗时超过 100 毫秒的操作都不要写进按钮回调函数里。正确的方式是:程序启动时在后台把模型加载好,按钮只负责让用户选图片;选完图后,如果预测仍耗时较长,就用threading.Thread把预测逻辑丢到子线程,UI 主线程只负责显示结果。这个改动看着简单,但带来的用户体验提升是质的改变。
5. window_trash.py 图形界面:把模型接到按钮上
5.1 Tkinter 界面布局与文件选择
图形界面是让你训练的模型能被非技术人员实际使用的最后一步。源码里的window_trash.py采用 Tkinter 实现,这是 Python 自带的 GUI 库,不需要额外安装,适合做轻量级桌面工具。界面布局一般分三块:顶部是操作按钮,中间是图片预览区,底部显示预测结果。代码结构大致如下。
# window_trash.py 界面布局核心 import tkinter as tk from tkinter import filedialog, Label, Button from PIL import Image, ImageTk class TrashApp: def __init__(self, root): self.root = root self.root.title("垃圾识别分类系统") self.btn_open = Button(root, text="选择图片", command=self.open_file) self.btn_open.pack(pady=10) self.label_img = Label(root) self.label_img.pack() self.label_result = Label(root, text="尚未识别", font=("SimHei", 14)) self.label_result.pack(pady=10) def open_file(self): path = filedialog.askopenfilename(filetypes=[("Image", "*.jpg *.png *.jpeg")]) if not path: return # 后续在这里加载图片并进行预测filedialog.askopenfilename弹出系统文件选择对话框,filetypes限定了可选的文件类型。Label组件分别用于显示图片和展示预测文本。这里有一个中国用户特别容易遇到的问题:默认字体在 Windows 上不显示中文,显示成方框乱码,所以字体要显式指定为中文字体,如SimHei。
5.2 预测结果回填与置信度展示
选择图片后,界面需要完成三件事:在预览区显示这张图、把图片送入模型预测、把预测结果和置信度更新到结果标签上。这三件事的顺序很重要,尤其是“先显示图片再做预测”,因为用户需要即时反馈。
def open_file(self): path = filedialog.askopenfilename(filetypes=[("Image", "*.jpg *.png *.jpeg")]) if not path: return img = Image.open(path) img.thumbnail((300, 300)) self.photo = ImageTk.PhotoImage(img) self.label_img.config(image=self.photo) self.root.update() # 预测逻辑 class_name, confidence = predict_image(path) self.label_result.config( text=f"识别结果: {class_name} 置信度: {confidence:.2%}" )img.thumbnail((300, 300))按比例缩小图片,防止超大图片把界面撑爆。ImageTk.PhotoImage是把 PIL 图片对象转成 Tkinter 能显示的图片对象。这里有一个坑:PhotoImage对象必须保存在实例变量上,比如self.photo,如果只存在局部变量中,图片会被 Python 垃圾回收机制回收,界面显示空白。
predict_image函数加载训练好的模型并返回类别和置信度。:.2%是 Python 的格式化语法,把 0.86 转成 86.00%。置信度是加分项,它让用户对预测结果有一个直观预期,也方便在置信度低于某个阈值时提示“请重新拍摄”。
5.3 把界面脚本和训练脚本解耦的工程习惯
window_trash.py和test_model.py之间有一个工程上的耦合问题:两个文件都各自写了图片预处理、模型加载、类别映射。如果训练时把类别列表改了,或者换了模型输入尺寸,就必须同步修改两处代码,漏改一处就是灾难。
我一般会在项目里增加一个inference.py,把模型加载、预处理、预测封装成一个TrashClassifier类,test_model.py和window_trash.py都只调用这个类,不直接操作 Keras API。这样推理逻辑只有一份,界面和命令行测试共用同一套代码。
# inference.py —— 统一推理入口 class TrashClassifier: def __init__(self, model_path): self.model = load_model(model_path) self.class_names = ['cardboard', 'glass', 'metal', 'paper', 'plastic', 'trash'] def predict(self, img_path): img = image.load_img(img_path, target_size=(224, 224)) x = image.img_to_array(img) x = np.expand_dims(x, axis=0) / 255.0 preds = self.model.predict(x)[0] idx = int(np.argmax(preds)) return self.class_names[idx], float(preds[idx])这个类把模型路径和类别列表收进构造函数,调用方只需要classifier = TrashClassifier('models/trash_mobilenet.h5')然后classifier.predict(...)就行。后续如果你想把推理服务升级成 Flask 接口,也只需要基于这个类加路由,不需要再重写模型调用逻辑。做工程和做实验的差别,往往就在这些“不提前做就会返工”的小抽象上。
6. 最后一个建议:让数据集和模型目录可迁移
很多拿到源码的人训练完模型就结束了,把.h5文件丢在models文件夹里,数据集留在盘符深处。等到换机器或者做演示时,才发现在别处跑不起来,要么路径写死了,要么模型和数据集的相对位置发生了变化。我在实际部署这套垃圾识别系统时,固定使用一个约定:项目根目录下只保留代码和文档,数据集和模型都通过配置文件定位。
具体做法是:在项目根目录创建一个config.py,把数据集路径、模型路径、输入尺寸、类别列表全部集中在一个字典里;所有脚本从config.py读取路径,而不是在代码里写死字符串。这样整个项目打包给别人时,对方只需要修改config.py里的路径前缀,就能在自己机器上跑起来,不用逐个文件找硬编码路径。如果数据集实在太大不方便移动,我也会用符号链接把数据集目录链接到项目内,保证相对路径始终可用。从那以后,我每次解压源码包的第一件事,就是全局搜索.h5和/dataset这类硬编码路径,统一替换成配置读取。毕竟源码的价值在于能改、能跑、能换数据,而不是被路径锁死在一台机器上。希望这套拆解能帮你在跑通垃圾分类项目时少走几步弯路。
本文还有配套的精品资源,点击获取