☰
基于卷积神经网络的垃圾分类系统:从数据预处理到预测部署的完整实践
2026/9/28 20:12:42 网站建设 项目流程

简介:这是一份面向计算机专业毕业设计及项目实战学习的Python垃圾分类系统资源,基于卷积神经网络实现图像识别与分类,适合正在做毕设、课程设计或期末大作业的学生,也适合需要完整项目练手的学习者;内容经助教老师审定,难度适中。压缩包为zip格式、大小约5.1MB,主要包含可运行的Python源码和论文PDF;源码经过本地编译与严格调试,下载后可直接运行,论文部分可用于撰写设计说明、梳理模型结构与实验过程。资源目前已有156人学习下载。项目覆盖垃圾分类识别的完整流程,评审分为98分,属于导师认可的高分设计,可帮助读者对照完整方案理解CNN在图像分类任务中的实际应用,同时减少从零搭建模型和排错的时间。

1. 基于卷积神经网络的垃圾分类系统:毕设选题不踩雷的一套完整源码

每年毕业季都能看到一批人抱着计算机视觉方向冲进垃圾分类的选题,但真正能从头到尾跑通、又符合毕设要求深度的并不多。这套基于卷积神经网络的垃圾分类系统源码加论文PDF,是我拆过比较顺手的资源之一:PyTorch或者Keras的代码结构都给你理好了,数据预处理、模型训练、预测展示一条线走完,评审分98分的底子在哪,跑一遍就能有体感。如果你是计算机相关专业的应届生,或者想拿项目实战练手、做课程设计和期末大作业,这套资源的适合度比较高——难度中等偏上一点点,但代码全部本地编译可运行,几乎不用二次返工。这篇笔记就把我的复现过程和参数细节拆给你看,尤其是那些一跑就翻车的坑,我都会单独点名。

2. CNN为什么是垃圾分类的标配:先看懂卷积神经网络的设计逻辑

2.1 从全连接到卷积:图像分类为什么要换网络结构

刚接触图像分类的人最容易有个疑问:用普通的全连接神经网络直接吃像素不行吗?当然可以,但代价很大。一张 224×224 的三通道彩图,展平之后是 150528 个输入特征,如果第一层全连接用 1024 个神经元,光这一层就有超过 1.5 亿个参数。训练这样的网络,不仅算力吃紧,还非常容易过拟合——因为全连接层对图像的每个像素一视同仁,根本不会关注「垃圾图像里的塑料瓶轮廓」这种局部特征。

卷积神经网络解决的就是这个问题。它的核心思路来自对图像局部性的观察:一个物体在图像里往往由局部的边缘、纹理、色块组合而成,而这些局部特征在图像的不同位置可能重复出现。于是 CNN 做了两件关键的事:局部连接和权值共享。局部连接让每个神经元只感知一个小区域(比如 3×3 或 5×5 的卷积核),权值共享让同一个卷积核在整张图上滑动计算,这样参数数量大幅下降,同时网络能提取到位置无关的特征——这在垃圾分类里非常实用,因为同一个易拉罐可能出现在图像左上角,也可能出现在右下角。

实际的 CNN 结构通常在卷积层之后接池化层(MaxPooling 或 AveragePooling),用来下采样、压缩特征图尺寸,只保留最显著的特征;堆叠若干组卷积+池化之后,再接 Flatten 扁平化和全连接层,最后由 Softmax 输出每个类别的概率。这套组合拳,就是绝大多数图像分类模型的底层逻辑,也是这份源码里模型设计的出发点。

2.2 数据目录怎么组织:ImageDataGenerator 的拼图规则

拿到源码之后,第一步不是急着跑模型,而是先把数据目录对齐。这套项目里的数据组织形式,走的是 Keras 经典的flow_from_directory约定:根目录下分 train、validation、test 三个文件夹,每个文件夹里再按类别建子文件夹,子文件夹名就是类别名。

dataset/ train/ recyclable/ # 可回收:纸箱、塑料瓶、玻璃瓶 kitchen/ # 厨余:剩饭、果皮、菜叶 harmful/ # 有害:电池、过期药品 other/ # 其他:烟头、陶瓷碎片 validation/ recyclable/ kitchen/ harmful/ other/ test/ recyclable/ kitchen/ harmful/ other/

这个目录结构决定了后续的数据加载方式。flow_from_directory会自动扫描子文件夹名称并生成类别标签索引,所以子文件夹的命名必须和类别一一对应,不要用中文,也不要用带空格的文件夹名,否则在 Windows 上很容易出现编码问题。我一般会额外检查一遍每个类别下的图片数量,确保没有空文件夹——空的类别文件夹不会直接报错,但训练时会出现样本不足,导致那一类的准确率拉胯。

2.3 预处理与数据增强:训练之前让模型少走弯路

图像输入的预处理直接决定模型能不能收敛。这份源码里用的预处理方式可以总结为三件事:缩放、归一化、数据增强。缩放是把所有图片统一到target_size,源码里默认是 224×224,这个尺寸和很多预训练模型的输入尺寸一致,后续如果想换成 VGG16 或 ResNet50 做迁移学习,不需要改数据管道。归一化是把像素值从 0~255 压缩到 0~1,做法是 rescale=1.0/255。

数据增强是训练阶段最有用的技巧,它通过对已有图片做小幅度变换,让模型看到更多样的输入,从而降低过拟合风险。

from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen = ImageDataGenerator( rescale=1.0 / 255, rotation_range=20, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2, horizontal_flip=True, fill_mode='nearest' ) val_datagen = ImageDataGenerator(rescale=1.0 / 255) train_generator = train_datagen.flow_from_directory( 'dataset/train', target_size=(224, 224), batch_size=32, class_mode='categorical' ) val_generator = val_datagen.flow_from_directory( 'dataset/validation', target_size=(224, 224), batch_size=32, class_mode='categorical' )

这里的关键参数是rotation_range=20(最多旋转 20 度)、width_shift_range=0.2(水平偏移 20%)、zoom_range=0.2(缩放范围 20%)。需要注意的是,验证集和测试集不要做数据增强,只做归一化,否则验证集的指标会被增强后的图片干扰,不能真实反映模型泛化能力。训练集增大图片多样性,验证集保持原图,这是一个很容易被忽视、又直接影响答辩时评委问题的细节。

3. 模型搭建与训练调参:让卷积神经网络收敛且不欠拟合

3.1 网络结构怎么选:这个源码里的 CNN 分层设计

垃圾分类虽然属于图像分类任务,但各类别之间的视觉差异有时很小,比如「其他垃圾」里的陶瓷碎片和「可回收」里的玻璃瓶,颜色和纹理高度相似。因此模型不能做得太浅,源码里给出的网络结构是 3 组卷积块加 2 层全连接,每一组卷积块包含卷积层、BatchNormalization 和最大池化层。

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout, BatchNormalization model = Sequential([ Conv2D(32, (3, 3), activation='relu', input_shape=(224, 224, 3)), BatchNormalization(), MaxPooling2D((2, 2)), Conv2D(64, (3, 3), activation='relu'), BatchNormalization(), MaxPooling2D((2, 2)), Conv2D(128, (3, 3), activation='relu'), BatchNormalization(), MaxPooling2D((2, 2)), Flatten(), Dense(256, activation='relu'), Dropout(0.5), Dense(4, activation='softmax') ]) model.summary()

这个结构的选型逻辑值得展开说。第一层卷积核数量 32,第二层 64,第三层 128,采用翻倍策略,因为随着网络加深,特征图尺寸变小,需要更多通道数来承载抽象的语义特征。每个卷积块里都加了 BatchNormalization,它的作用是在每批数据进入激活函数之前做标准化,让梯度分布更稳定——训练时你会发现加了 BN 之后,模型对学习率的敏感度下降,不需要反复调学习率也能收敛。最后的 Dropout(0.5) 是防过拟合的主要手段,训练时随机丢弃一半神经元,推理时全部保留。

3.2 训练参数设定:学习率、batch size 与 epochs 的搭配关系

训练参数看起来是几个数字,实际上每一组都像在走钢丝。先说 batch size,源码里给的是 32,这个值在 224×224 输入下对显存的压力适中。如果显存不够,可以降到 16,但要明白 batch size 变小会导致梯度估计的噪声变大,训练震荡会更明显,所以学习率也要相应调低。如果显存充裕并且想加速收敛,可以尝试 64,但前提是数据集的类别分布够均衡。

学习率是所有参数里最玄学的一个,没有标准答案,只有经验区间。用 Adam 优化器时,我一般把初始学习率设在 1e-4 到 3e-3 之间:数据集小、类别少,可以激进一点用 3e-3;数据集大、类别多,保守一点用 1e-4。这份源码里直接用的 default 学习率(Adam 默认约 1e-3),在多数场景下能正常收敛,但如果训练过程中 loss 震荡剧烈,建议加一个 LearningRateScheduler,每 5 个 epochs 把学习率乘 0.6,你会发现 loss 曲线明显变得平滑。

epochs 的设定我习惯配合 EarlyStopping 一起用,而不是傻跑固定轮数。EarlyStopping 的参数设置如:

from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint early_stop = EarlyStopping( monitor='val_loss', patience=8, restore_best_weights=True ) checkpoint = ModelCheckpoint( 'best_model.h5', monitor='val_accuracy', save_best_only=True, mode='max' ) history = model.fit( train_generator, steps_per_epoch=train_generator.samples // 32, validation_data=val_generator, validation_steps=val_generator.samples // 32, epochs=50, callbacks=[early_stop, checkpoint] )

patience=8 表示验证集 loss 连续 8 个 epochs 不下降就停止训练,restore_best_weights=True 是真正的后悔药——训练结束后自动回滚到验证集表现最好的那一轮权重,而不是最后一轮的权重。ModelCheckpoint 则保证训练过程中任何一次的 val_accuracy 新高都会被保存。

3.3 训练输出怎么读:loss 曲线与准确率背后的信息量

训练跑起来之后,每过一个 epoch 都会打印一行训练 loss、训练 acc、验证 loss、验证 acc。很多人只看 acc,这是不够的。正确做法是同时盯住验证 loss 和训练 loss 的差值:如果训练 acc 一直在涨,验证 acc 涨到某个点之后开始掉头,同时验证 loss 开始回升,说明模型已经开始过拟合了——这时候 EarlyStopping 如果还没触发,就需要手动检查是不是 Dropout 率太低、数据增强不够,或者模型容量太大。

如果你的训练输出里 val_loss 始终不降,甚至越跑越高,而训练 loss 却能正常下降,先别急着改模型结构,检查一下训练集和验证集的数据分布是否一致。我见过最典型的情况是:训练集做了数据增强,验证集没有做归一化之外的任何操作,但这本身没问题;问题出在验证集图片里有大量训练集完全没见过的拍摄角度和光线条件,导致验证集难度远高于训练集。复现这套项目时,建议把验证集图片数量控制在每个类别 30~50 张之间,保证和训练集有重叠的场景分布,这样才能准确判断模型的学习进度。

4. 从训练到预测:把卷积神经网络权重跑通到实际图片

4.1 模型保存与加载:h5 格式和 SavedModel 选哪个

训练完成之后的第一件事就是把模型权重存下来。Keras 里模型保存有两种主流格式:.h5和 SavedModel 目录。这套源码里用的是.h5,单文件管理方便,尤其适合毕设答辩时拷贝演示。需要注意的一点是,如果你用model.save('model.h5')保存完整模型,那么加载时用的是load_model,它会反向构建整个网络结构,不需要你再手动重写一遍模型定义。但如果你的模型里有自定义层或者自定义损失函数,load_model在加载时会报错,这时候要么改成model.save_weights()只存权重,然后手动构建模型结构再load_weights,要么老老实实把自定义层定义好再保存。

我一般会把最终模型用两种方式各存一份,h5 用来演示和存档,SavedModel 用来部署。因为 SavedModel 格式对后续转 TensorFlow Serving 或者移动端部署更友好,如果你预计这个毕设项目后续要做成绩展示之外的拓展,多存一份没坏处。

4.2 单张图片预测:预处理必须和训练时保持一致性

训练结束、模型加载成功,接下来就是最关键的一步:拿一张没见过的垃圾图片来预测。这里最容易踩的坑是预处理不一致——训练时图片被 ImageDataGenerator 缩放到 224×224 并除以 255,预测时如果直接用原始尺寸原像素值喂给模型,结果会离谱到完全不可信。

from tensorflow.keras.models import load_model from tensorflow.keras.preprocessing.image import load_img, img_to_array import numpy as np model = load_model('best_model.h5') img = load_img( 'test/plastic_bottle.jpg', target_size=(224, 224) ) x = img_to_array(img) # 形状: (224, 224, 3), 取值 0~255 x = x / 255.0 # 与训练时保持一致, 归一化到 0~1 x = np.expand_dims(x, axis=0) # 加上 batch 维度, 形状: (1, 224, 224, 3) pred = model.predict(x) class_index = np.argmax(pred[0]) confidence = pred[0][class_index] class_names = ['recyclable', 'kitchen', 'harmful', 'other'] print(f'分类结果: {class_names[class_index]}, 置信度: {confidence:.2%}')

这段代码里最容易被忽略的是expand_dims这一步。模型训练时的输入是四维张量(batch_size, 224, 224, 3),单张图片只有三维,必须手动加一个 batch 维度。另外,x / 255.0的归一化操作看似简单,但如果你用的是 Float32 类型,需要注意内存中的数值精度,一般默认没问题,不需要额外处理。

4.3 做一个可交互的预测展示:Flask 接口让毕设答辩有底气

单个脚本里跑通预测之后,建议再花半小时套一个 Flask 接口,这样答辩演示时可以直接打开浏览器上传图片,比在黑框框里敲命令直观得多。这个项目的源码里已经带了 Web 展示部分,核心逻辑可以拆成三步:接收上传图片、调用模型预测、返回 JSON 结果。

from flask import Flask, request, jsonify from tensorflow.keras.models import load_model from tensorflow.keras.preprocessing.image import load_img, img_to_array import numpy as np app = Flask(__name__) model = load_model('best_model.h5') class_names = ['recyclable', 'kitchen', 'harmful', 'other'] @app.route('/predict', methods=['POST']) def predict(): file = request.files['image'] img = load_img(file, target_size=(224, 224)) x = img_to_array(img) / 255.0 x = np.expand_dims(x, axis=0) pred = model.predict(x) idx = int(np.argmax(pred[0])) conf = float(pred[0][idx]) return jsonify({ 'class': class_names[idx], 'confidence': conf }) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)

这里说明一点:load_img传入的文件对象可以直接处理 Flask 上传的 FileStorage 对象,不需要先保存到本地再加载,这能省掉一大段临时文件管理代码。接口返回的confidence建议保留几位小数再传给前端,避免 JSON 序列化时出现过长浮点数。另外,host='0.0.0.0'可以让局域网内其他设备访问到这个接口,答辩时如果评委想用自己的手机试一张图片,直接访问http://你的IP:5000就能演示,体验会好很多。

5. 避坑与排查:复现这套垃圾分类源码最容易翻车的五个场景

5.1 运行时报错:keras 和 tf.keras 混用导致的 ImportError

现象:按照 README 的说明运行脚本,启动阶段直接报ImportError: cannot import name 'to_categorical' from 'keras',或者是module 'keras' has no attribute 'preprocessing'。

原因:电脑上同时安装了独立的 Keras 包和 TensorFlow 自带的tf.keras,而代码里from keras...和from tensorflow.keras...混着写,解释器随机拉到的是旧版 Keras 的接口,兼容性直接炸掉。

解决:全部统一成from tensorflow.keras...,包括models、layers、preprocessing、callbacks等模块。更彻底的做法是卸载独立的 Keras 包,只保留 TensorFlow 全家桶。我复现任何 Keras 项目的第一步,永远是pip show keras看一眼版本,如果版本号里带2.x且不是以tf-keras开头,直接卸载重来。

5.2 OpenCV 与图片路径中文名导致的读图失败

现象:训练数据准备阶段,flow_from_directory能正常统计图片数量,但训练中途报cv2.error: ... filename is not recognized,损失直接停住,恢复不了。

原因:数据集的某个子文件夹或者图片文件名包含中文字符。在 Windows 下,OpenCV 的imread默认不处理非 ASCII 路径,文件读取失败后会返回空矩阵,Keras 在预处理时对空矩阵做 resize 就会抛异常。

解决:把数据集的所有路径改成纯英文,文件夹和文件名都不要出现中文、空格和特殊符号。如果你已经拍了很多中文命名的图片,用一个批量重命名循环跑一遍就搞定,不要在代码里写编码转换——那是给自己挖坑。

5.3 显存爆炸和 CUDA out of memory

现象:训练刚开始几个 batch 就报CUDA out of memory,或者运行到中途突然 OOM,整个进程被杀掉。

原因:224×224 的输入加上默认 32 的 batch size,对 4GB 显存的显卡已经逼近极限,如果再开几个 TensorBoard 或视频解码程序,显存很容易不够。

解决:把 batch size 降到 16 或 8,或者把图片输入尺寸降到 160×160,模型重新训练时的参数会急剧减少,但准确率下降通常可以接受。还有一个常用技巧是在代码开头加一行:

import tensorflow as tf gpus = tf.config.experimental.list_physical_devices('GPU') for gpu in gpus: tf.config.experimental.set_memory_growth(gpu, True)

set_memory_growth(True)的作用是让 TensorFlow 按需分配显存,而不是启动时把所有显存都占满。加上之后即使显存不充裕,也能在小显存的机器上完成训练,只是速度会稍微慢一点。

5.4 训练 loss 变成 NaN 的排查思路

现象:训练到第几个 epoch 之后,loss 突然变成 NaN,准确率也跟着变成 0,之后再也回不来。

原因:NaN 出现的常见源头有三个——学习率过高导致梯度爆炸、数据里有 NaN 像素值或全黑图片、模型权重初始化异常导致前向传播输出 Inf。检查顺序也是按这个优先级来。

解决:先在compile里给优化器加梯度裁剪约束,Adam 优化器支持clipnorm=1.0,这是最常见的止损手段;然后检查读取图片的管道,打印几张图片的像素最大值和最小值,确认没有异常值;最后把数据集的异常图片(比如损坏的 JPEG、0 字节文件)批量清理掉。我一般会在数据加载后加一个断言,检查np.isnan(images).sum()是否为 0,不满足就自动跳过该文件。

5.5 预测结果永远指向同一个类别

现象:模型训练完,准确率看着还行(比如 85%),但一到预测阶段,无论输入什么图片,输出永远是同一个类别,置信度还接近 100%。

原因:数据集类别严重不平衡。如果「其他垃圾」类别的图片数量是其他两类的三倍以上,模型学到的最优策略就是「一律输出其他垃圾」,因为这样总体的准确率也能刷到 60% 以上。训练时 model.fit 输出的 acc 会骗人,但验证集的单类别回传矩阵会暴露问题。

解决:重新统计每个类别的图片数量,把最多的类别降采样到接近最少类别的数量,或者用class_weight参数给样本少的类别加上权重,让模型在反向传播时更关注那些「稀有类别」。

6. 进阶用法:用 sklearn 的混淆矩阵定位模型短板,再决定是否换迁移学习

训练结束后,别急着写论文,先把测试集上的分类细节拉出来看一眼。classification_report能告诉你每个类别的精确率、召回率和 F1-score,而confusion_matrix能直观展示模型具体把哪两类垃圾搞混了。

from sklearn.metrics import classification_report, confusion_matrix y_true = [] y_pred = [] for filename, label in test_pairs: img = load_img(filename, target_size=(224, 224)) x = img_to_array(img) / 255.0 x = np.expand_dims(x, axis=0) pred = model.predict(x) y_true.append(label) y_pred.append(np.argmax(pred[0])) print(classification_report(y_true, y_pred, target_names=class_names)) print(confusion_matrix(y_true, y_pred))

如果发现「陶瓷碎片」经常被识别成「玻璃瓶」,说明这两个类别在纹理和颜色上高度重叠,人工肉眼也容易混淆。这时候与其盲目加网络层数,不如直接换迁移学习,把这个分类问题从头训练改成特征迁移:去掉你自己搭建的全连接头,换成 ImageNet 上预训练的 ResNet50 主干做特征提取器,只训练最后几个全连接层。这样做的好处是模型在训练集较小的情况下也能借到 ImageNet 学到的底层纹理和边缘特征,准确率往往比你从零训练的 CNN 高出一截。具体做法和参数讲起来又是一篇文章,但核心就一句话:当你的目标任务和自己的 CNN 结构在视觉上不匹配时,用轻量级微调往往更省力。

我后来每次做毕设类的图像识别项目,都会强制走一遍这个流程:先看分类报告里每一类的召回率,再对比混淆矩阵里的高混淆对,最后决定是调自己模型的深度,还是直接上迁移学习。这套垃圾分类源码也一样,它不是完美的银弹,但只要把数据和训练管线吃透,论文里的实验分析部分反而比算法本身更好写。希望这份复现笔记能帮到你,少走一段我当年走过的最坑的弯路。

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

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

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

立即咨询