拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于Python深度学习的垃圾分类识别系统实战指南

简介这是一份基于Python与深度学习的垃圾分类系统毕业设计源码适用于本科毕业设计、期末大作业或课程设计场景。项目已通过老师指导与验收代码结构完整从模型训练到Web端识别展示均有覆盖适合希望快速搭建完整项目的小白开发者参考学习。压缩包共6319个文件包含约1457个Python源码文件、1454个编译缓存文件以及前端页面所需的html、js、css等资源另有po/mo多语言翻译文件与少量可执行程序整体仅18.98MB轻量易部署。目前已有338人学习下载。内容包含数据预处理、模型构建、训练与推理、Web交互界面等模块可帮助读者理解图片分类任务从数据集处理到系统上线的完整流程同时附带的目录结构清晰便于按功能检索和二次开发是入门深度学习实战的优质范例。1. 垃圾分类系统为什么值得用Python和深度学习重做一遍一张照片丢进垃圾桶前先在手机里停一秒系统告诉你它是可回收物还是厨余垃圾。垃圾分类是Python深度学习课程设计里出现频率最高的完整项目因为从数据、模型到可视化接口每个环节都能考核到动手能力。这套方案的本质是把图像分类任务拆成数据准备、模型训练、服务部署三段让不懂代码的人也能通过上传照片得到分类结果。它最适合三类人做毕设的学生、想把CNN和迁移学习真实跑一遍的开发者、需要给社区或学校做识别演示的工程师。这套系统里最容易被低估的不是模型本身而是数据质量和服务化封装。后面我会按实际落地的顺序把每一步讲透。2. 数据与标签把垃圾分类数据集整理成模型能直接学的样子2.1 六分类还是四十类别先定标签体系再谈模型国内的垃圾分类标准一般归成四类可回收物、有害垃圾、厨余垃圾、其他垃圾。但公开的垃圾分类图像数据集常见的是按材质和物品细分的类别例如纸板、玻璃、金属、塑料、废纸、厨余等。我建议在项目里做一个中间层训练时用英文类别命名的6类例如cardboard、glass、metal、paper、plastic、trash在系统展示的时候再把这6类映射回四分类。这么做的原因有三个。第一公开数据集里这6类的图片数量相对均衡不会出现一个类别两千张、另一个类别三十张的情况第二6类的模型决策边界比四十几种清晰同样的训练轮数下准确率更高第三答辩演示时“细分类结果置信度”比只给一个四分类结果显得更有说服力评委认可度高。对新手来说最容易犯的错是直接把几十个类别的原始文件夹拿去做标签。有些细分类图片只有几十张甚至十几张网络还没学会这个类别的特征准确率就被这类少样本类别拉下去了。正确做法是先做一个类别归并脚本把相近类别合并再进入训练流程。2.2 用Python脚本划分训练集与验证集防止数据泄漏的拷贝数据划分这一步很多教程是用train_test_split直接切一个DataFrame但实际项目里图片是按文件夹组织的模型读取也要按文件夹来。所以我习惯先写一个独立的划分脚本只做一件事把原始数据目录拷贝成dataset/train和dataset/val两个目录每个类别一个子文件夹。import os import shutil from sklearn.model_selection import train_test_split random_state 42 raw_root raw_data # 原始数据每个子文件夹一个类别 dataset_root dataset # 输出目录 for category in os.listdir(raw_root): src_dir os.path.join(raw_root, category) if not os.path.isdir(src_dir): continue images [f for f in os.listdir(src_dir) if f.lower().endswith((.jpg, .jpeg, .png))] # 固定随机种子同一份数据每次划分结果一致 train_imgs, val_imgs train_test_split( images, test_size0.2, random_staterandom_state ) train_dir os.path.join(dataset_root, train, category) val_dir os.path.join(dataset_root, val, category) os.makedirs(train_dir, exist_okTrue) os.makedirs(val_dir, exist_okTrue) for img in train_imgs: shutil.copy(os.path.join(src_dir, img), os.path.join(train_dir, img)) for img in val_imgs: shutil.copy(os.path.join(src_dir, img), os.path.join(val_dir, img)) print(f{category}: train {len(train_imgs)}, val {len(val_imgs)})这段脚本的逻辑很清楚遍历原始数据根目录下的每个类别文件夹过滤出图片文件按比例分成训练集和验证集再逐张拷贝到新的目录结构里。random_state42是必须写的否则每次运行划分结果都不同后面对比实验就失去了基准。注意test_size0.2是验证集比例。垃圾分类数据集的单个类别图片量不算大20%是合理的如果你的数据总量太少比如每个类别不到100张可以把验证集降到15%但不要低于10%否则验证结果波动会很大训练时看着验证准确率上蹿下跳根本没法判断模型好坏。每个类别图片数量相差悬殊时可以用stratifylabels让划分保持类别比例但这里按文件夹划分每个类别独立切分天然就做到了按类别分层。提示图片文件名不要用中文。有些公开数据集解压后文件名是中文或者带空格训练脚本跑一半会突然报编码错误排查起来很费时间。做任何项目之前先把文件名统一重命名成纯英文和下划线能省掉一堆玄学问题。2.3 数据加载与在线增强把图片变成模型眼里的几千种变化目录结构准备好之后用tf.keras.utils.image_dataset_from_directory加载数据。这是TensorFlow较新版本推荐的API从文件夹自动推断类别标签省去手写数据生成器。相比老的ImageDataGenerator.flow_from_directory它对错误图片的容错更好且和tf.data管道无缝衔接。import tensorflow as tf IMG_SIZE 224 BATCH_SIZE 64 NUM_CLASSES 6 # 在线数据增强层只在训练集上使用 data_augmentation tf.keras.Sequential([ tf.keras.layers.RandomFlip(horizontal), tf.keras.layers.RandomRotation(0.1), tf.keras.layers.RandomZoom(0.1), ]) train_ds tf.keras.utils.image_dataset_from_directory( dataset/train, labelsinferred, label_modecategorical, image_size(IMG_SIZE, IMG_SIZE), batch_sizeBATCH_SIZE, shuffleTrue, seed42 ) val_ds tf.keras.utils.image_dataset_from_directory( dataset/val, labelsinferred, label_modecategorical, image_size(IMG_SIZE, IMG_SIZE), batch_sizeBATCH_SIZE, shuffleFalse ) # 增强层只作用于训练集 train_ds train_ds.map(lambda x, y: (data_augmentation(x), y)) train_ds train_ds.prefetch(tf.data.AUTOTUNE) val_ds val_ds.prefetch(tf.data.AUTOTUNE)image_size(224, 224)是所有模型的输入尺寸224是ResNet系列最常用的输入宽度label_modecategorical让标签变成one-hot编码对应模型输出层的softmax。prefetch(tf.data.AUTOTUNE)是性能优化让数据加载和模型训练并行GPU利用率能明显提升。增强层里的水平翻转、旋转和缩放相当于在不增加磁盘图片的情况下给模型提供更多形态的样本对防止过拟合很有效。一个常见误用是把增强层也套在验证集上。验证集的作用是模拟真实分布应该用原始图片去做评估加增强反而会导致验证指标失真。3. 模型选型与训练用ResNet50迁移学习把准确率拉到可用水平3.1 传统机器学习模型为什么做不动图像分类在动手训练之前先把模型选型的逻辑说清楚。有的同学试过用颜色直方图或HOG特征喂给SVM结果在真实图片上表现很不稳定。原因是垃圾分类的特征不在颜色。玻璃瓶和塑料瓶颜色几乎一样但材质不同纸箱和牛皮纸袋颜色接近但形态纹理不同。传统手工特征很难把这些差异同时表达出来模型容量也不够。深度学习模型尤其是CNN可以通过卷积核自动学习从边缘、纹理到物体部件的层次化特征。同样是样本量不大的分类任务卷积神经网络通常比SVM加手工特征高10到20个百分点这是实践里反复验证过的差距。所以在垃圾分类这个项目上直接用CNN是正确的选择问题只在于用自己搭的浅层网络还是用预训练模型做迁移学习。自己从零搭一个几层的CNN不是不能做但小数据集上训练效率低最后的精度往往卡在瓶颈期上不去。更常见也更可靠的方案是迁移学习把在ImageNet上预训练好的ResNet50作为特征提取器去掉它的分类头换上适合自己任务的全连接层。这样模型在前几层已经具备通用视觉能力剩下的任务只是把“图像特征”映射到“垃圾类别”需要的训练数据和训练时间都少得多。3.2 用ResNet50训练分类模型完整训练脚本与参数解读下面这个脚本是项目里最核心的部分。它把预训练ResNet50的卷积层全部冻结只训练新增的全连接头。import tensorflow as tf from tensorflow.keras.applications import ResNet50 from tensorflow.keras.layers import GlobalAveragePooling2D, Dense, Dropout from tensorflow.keras.models import Model from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau IMG_SIZE 224 NUM_CLASSES 6 LEARNING_RATE 1e-3 EPOCHS 30 base_model ResNet50( weightsimagenet, include_topFalse, input_shape(IMG_SIZE, IMG_SIZE, 3) ) base_model.trainable False # 冻结卷积基 x base_model.output x GlobalAveragePooling2D()(x) x Dropout(0.3)(x) x Dense(128, activationrelu)(x) x Dropout(0.3)(x) outputs Dense(NUM_CLASSES, activationsoftmax)(x) model Model(inputsbase_model.input, outputsoutputs) model.compile( optimizertf.keras.optimizers.Adam(learning_rateLEARNING_RATE), losscategorical_crossentropy, metrics[accuracy] ) callbacks [ ModelCheckpoint(best_model.h5, monitorval_accuracy, save_best_onlyTrue, verbose1), EarlyStopping(monitorval_loss, patience5, restore_best_weightsTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience3) ] history model.fit( train_ds, validation_dataval_ds, epochsEPOCHS, callbackscallbacks )weightsimagenet表示加载在ImageNet上预训练好的权重第一次运行会从网上自动下载约90MB的权重文件。网络不通的话需要提前手动下载h5文件放置在指定缓存目录否则会在这里卡住报下载超时。include_topFalse去掉原模型的分类头只保留卷积特征提取部分。base_model.trainable False是冻结冻结后的卷积层参数不参与梯度更新这一步是为了防止一开始就用大学习率把预训练权重破坏掉。全连接头里用了两层Dropout(0.3)。垃圾分类图片背景复杂模型很容易记住训练集里的背景特征而不是垃圾本身Dropout是成本最低的缓解方式。GlobalAveragePooling2D把最后一个卷积层的特征图直接压成一个向量相比Flatten能减少大量参数也更不容易过拟合。注意ModelCheckpoint的monitor可以改成val_loss但如果类别不均衡val_accuracy更直观。save_best_onlyTrue保证磁盘里始终保存验证集表现最好的那一份权重这就是训练过程唯一的后悔药。3.3 冻结、微调与早停训练策略怎么配合第一阶段的训练只更新新增的Dense层学习率用1e-3。经过大约15到20个epoch验证准确率通常能到90%以上。想继续往上提就需要解冻部分卷积层做微调。微调阶段把ResNet50后段的部分层解冻用极小的学习率更新。代码如下# 第二阶段微调 base_model.trainable True for layer in base_model.layers[:120]: layer.trainable False # 前120层继续冻结 model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-5), losscategorical_crossentropy, metrics[accuracy] ) model.fit( train_ds, validation_dataval_ds, epochs10, callbackscallbacks )这里有两个关键参数。一是解冻范围layers[:120]保留前120层冻结只让最后50层左右的卷积层参与微调。解冻太多容易在小数据集上过拟合解冻太少微调效果不明显120这个位置在ResNet50里是一个常见经验值。二是学习率从1e-3降到1e-5因为经过第一阶段后模型已经接近收敛学习率太大会让损失函数剧烈震荡训练曲线像过山车验证准确率反而不升反降。配合这两个阶段的是早停与学习率衰减回调。EarlyStopping(patience5)会在验证损失连续5个epoch不下降时终止训练并回滚到最佳权重避免浪费算力ReduceLROnPlateau在验证损失平台期自动把学习率减半给模型多几次挣扎的机会。两者搭配基本可以做到丢进数据就不管训练结束直接取best_model.h5使用。4. 从模型到系统用Flask把识别能力变成可调用的垃圾分类接口4.1 系统结构模型、后端、页面各司其职模型训练完成只是项目的一半。毕设和真实落地的重点都在后半段怎么让使用者上传一张图返回一个可读的垃圾分类结果。这一步我一般用Flask实现因为它是Python生态里最轻量的Web框架写一个图片识别接口只要几十行代码不用像Django那样引入大量项目模板。系统目录结构如下garbage_classifier/ ├── app.py # Flask主程序所有接口都在这里 ├── best_model.h5 # 训练好的权重文件 ├── requirements.txt # 依赖清单 ├── templates/ │ └── index.html # 前端页面 ├── static/ │ └── uploads/ # 临时保存上传图片 └── dataset/ ├── train/ # 训练集 └── val/ # 验证集文件职责说明app.py后端接口模型加载、图片预处理、推理、返回JSONindex.html前端图片上传表单、结果展示best_model.h5模型权重由训练脚本生成requirements.txt依赖flask、tensorflow、pillow、numpy前端页面放在templates目录是Flask的约定render_template会自动从这里找文件。静态图片、CSS文件放static目录上传的临时文件也写到这个目录下。4.2 图片上传与推理接口完整app.py核心代码是/predict接口。它接收前端上传的图片文件预处理成模型需要的格式推理后把类别和置信度作为JSON返回。import io import numpy as np from PIL import Image import tensorflow as tf from flask import Flask, request, jsonify, render_template app Flask(__name__) # 类别顺序必须和训练时的文件夹顺序一致 class_names [cardboard, glass, metal, paper, plastic, trash] # 中文映射 class_map_cn { cardboard: 纸板, glass: 玻璃, metal: 金属, paper: 废纸, plastic: 塑料, trash: 其他垃圾 } model tf.keras.models.load_model(best_model.h5) def preprocess_image(image_bytes): 把上传的二进制图片转成模型输入张量 img Image.open(io.BytesIO(image_bytes)).convert(RGB) img img.resize((224, 224)) arr np.array(img, dtypenp.float32) / 255.0 # 从 (224, 224, 3) 增加 batch 维度变成 (1, 224, 224, 3) return np.expand_dims(arr, axis0) app.route(/, methods[GET]) def index(): return render_template(index.html) app.route(/predict, methods[POST]) def predict(): if file not in request.files: return jsonify({error: 未找到文件字段}), 400 file request.files[file] if file.filename : return jsonify({error: 未选择文件}), 400 input_tensor preprocess_image(file.read()) preds model.predict(input_tensor, verbose0)[0] idx int(np.argmax(preds)) confidence float(preds[idx]) return jsonify({ category_en: class_names[idx], category_cn: class_map_cn[class_names[idx]], confidence: round(confidence, 4) }) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)preprocess_image里做了三件事统一转成RGB格式、缩放到224x224、归一化到0到1。convert(RGB)这一步不能省有些手机拍摄的图片带透明通道或灰度模式不做转换模型输入维度会报错。np.expand_dims补batch维度因为模型训练时输入是(batch, 224, 224, 3)单张推理也要保持四维张量。model.predict(input_tensor, verbose0)的verbose0用于关闭进度条避免每次接口调用都在控制台刷一行日志。类名列表class_names的顺序必须和训练时文件夹的字母序一致因为image_dataset_from_directory默认按字母序生成标签这一步对不上输出的分类就会全错。4.3 前端联调让识别结果能看、能用后端的接口已经能返回结构化数据前端需要做一个最简的图片上传表单和结果展示区域。下面的模板足够应付演示场景。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title垃圾分类识别系统/title /head body h1垃圾分类识别系统/h1 form iduploadForm enctypemultipart/form-data input typefile namefile acceptimage/* required button typesubmit开始识别/button /form h2识别结果/h2 p类别span idcategory/span/p p置信度span idconfidence/span/p script const form document.getElementById(uploadForm); form.addEventListener(submit, async (e) { e.preventDefault(); const formData new FormData(form); const resp await fetch(/predict, { method: POST, body: formData }); const data await resp.json(); document.getElementById(category).textContent data.category_cn; document.getElementById(confidence).textContent data.confidence; }); /script /body /html表单的enctypemultipart/form-data是文件上传的前提。前端用fetch把FormData直接POST到/predict不需要手动设置Content-Type浏览器会自动加上带boundary的multipart头。data.category_cn和data.confidence对应后端JSON的字段名联调时前后端字段名必须严格一致这是新手最多翻车的地方。调试阶段可以用一行命令验证接口是否工作curl -X POST -F filetest_glass.jpg http://127.0.0.1:5000/predict返回{category_cn:玻璃,confidence:0.96}之类的JSON说明全链路已经打通。到这一步一个能上传图片、能识别、能显示结果的垃圾分类系统已经完整可运行。5. 垃圾识别翻车排查从训练到部署的五个高频问题5.1 训练时loss在下降验证准确率却原地踏步现象训练集loss一路走低准确率接近100%但每个epoch结束后的验证准确率一直在60%到70%徘徊。原因模型过拟合了训练集。垃圾分类数据集图片背景多变模型很可能记住了训练图片里的环境特征比如拍摄台面的颜色、光照角度而没有学到垃圾本身。解决先检查验证集是否和训练集有数据泄漏最常见的是同一个物品的多角度照片被切分到了两个集合。解决办法是先按“物品”分组再切分而不是按单张图片切分。如果数据泄漏排除确认数据增强层已开启并作用于训练集同时加大Dropout比例到0.5并提前使用ReduceLROnPlateau降低学习率。5.2 预测一张图片卡了好几秒现象接口能返回结果但耗时严重几乎每张图要等五六秒演示时非常尴尬。原因推理跑在CPU上ResNet50的参数量和计算量在这里完全暴露了。解决先确认推理机器有没有可用GPU。没有GPU时优先换用MobileNetV2这类轻量网络做迁移学习准确率只低两三个点但单张推理耗时能降到一两百毫秒。其次把输入尺寸从224降到160或128能显著减少计算量对垃圾分类这种粗粒度分类来说精度损失可接受。最后模型加载后不要重复调用load_model全局加载一次就够了这个低级错误最容易在Flask多线程请求场景里被放大。5.3 中文标签在页面上显示乱码现象接口返回的JSON在命令行里看是正常中文前端页面显示却变成一堆或者问号。原因页面响应没有声明UTF-8编码或者上传图片的文件名带中文导致Flask路由解析异常。解决后端统一用jsonify返回JSON它默认按UTF-8输出。前端index.html的head里必须有meta charsetUTF-8。还要检查上传文件名建议在前端或后端把所有上传文件重命名为英文加时间戳既避免中文文件名引发的编码问题也避免同名文件互相覆盖。5.4 某个类别怎么训练都分不对现象六分类模型总准确率有85%但其中“金属”这一类召回率特别低大部分金属物品都被识别成了“塑料”。原因类别不均衡。如果数据集中金属图片明显少于塑料模型在训练时天然偏向样本多的类别损失函数对金属的惩罚被稀释了。解决统计每个类别的样本数量用class_weight给少数类别加权。在model.fit里传入class_weight参数让模型对少样本类别更加敏感另一个办法是对金属类别做更强的在线增强等价于增加它的样本量。确认混淆矩阵里具体是哪些类别互相混淆再针对性处理不要盲目改全网络结构。5.5 训练好的模型换一台机器就加载失败现象在自己电脑上load_model一切正常把best_model.h5拷到实验室电脑或者答辩电脑上报错说无法加载或提示缺少自定义层。原因最常见的是TensorFlow版本不一致。新版本保存的模型Keras格式老版本TensorFlow读不出来另一种情况是训练时用了含有自定义层的模型加载时没有传入对应的custom_objects。解决在加载脚本开头先打印tf.__version__确认运行版本一致。如果模型用了自定义层用model.save(best_model.keras, save_formatkeras)保存为纯Keras格式并记录自定义层类名加载时通过tf.keras.models.load_model(best_model.keras, custom_objects{LayerName: LayerName})传入。跨环境部署前建议把best_model.h5再转成.tflite兼容性会好很多。6. 让项目真正落地模型量化、批量验证与实拍测试6.1 导出TFLite模型把部署体积和延迟降下来Web接口跑通后项目已经能评为高分但距离“真的有人用”还差两步模型瘦身和效果验证。先把训练好的H5模型转成TFLite格式。import tensorflow as tf model tf.keras.models.load_model(best_model.h5) converter tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations [tf.lite.Optimize.DEFAULT] tflite_model converter.convert() with open(best_model.tflite, wb) as f: f.write(tflite_model)开启Optimize.DEFAULT后模型权重会被量化到8位整数文件体积通常能压缩到原来的四分之一左右CPU推理速度提升明显精度损失一般控制在1到2个百分点以内。量化后的模型还可以进一步部署到移动端或边缘设备。推理端加载TFLite模型时把model.predict换成interpreter.invoke()输入输出张量规格不变。6.2 用批量验证脚本统计真实准确率训练脚本里的验证集准确率是模型在所有验证图片上的平均值它掩盖了类别之间的差异。落地前我会再加一道关卡逐类统计准确率和平均置信度确认每一类都达到可用水平。import numpy as np import tensorflow as tf from pathlib import Path model tf.keras.models.load_model(best_model.h5) val_root Path(dataset/val) class_names [cardboard, glass, metal, paper, plastic, trash] correct {c: 0 for c in class_names} total {c: 0 for c in class_names} for cls in class_names: for img_path in (val_root / cls).glob(*.jpg): img tf.keras.utils.load_img(img_path, target_size(224, 224)) arr tf.keras.utils.img_to_array(img) / 255.0 pred model.predict(np.expand_dims(arr, axis0), verbose0)[0] if np.argmax(pred) class_names.index(cls): correct[cls] 1 total[cls] 1 for cls in class_names: print(f{cls}: {correct[cls]}/{total[cls]} {correct[cls] / total[cls]:.2%})这段脚本直接对验证集目录逐张做预测按类别统计命中数。如果某一类的准确率显著低于其他类说明需要回看该类的图片质量、样本量和相似类别冲突。把这份输出截图放进毕设文档里比单贴一行准确率更有说服力。6.3 实拍测试与置信度阈值识别系统敢不敢用就看这里批量验证全部通过后还有最后一道坎用手机实拍测试。训练集里的图片大多来自公开数据集拍摄环境干净、物体居中、背景简单而真实场景里一张桌面可能同时有杯子、纸巾和果核光照有阴影物体有遮挡。这些都会让模型的输出概率变得平滑原本0.9的置信度可能掉到0.5。我习惯在系统里加一个置信度阈值低于0.6时返回“无法确定请重新拍摄或靠近物体”。这个设计能有效避免系统出现明显误判因为垃圾分类最让人反感的不是识别慢而是理直气壮地给一个错误答案。阈值配合前端显示实际使用体验会好一个档次。做这类项目多年我最大的感受是训练脚本人人能跑通但真正决定项目价值的永远是数据划分是否严谨、部署接口是否稳定、失败时有没有兜底。希望这份从数据到量化的完整路径能帮你在自己的垃圾分类系统上少走几段弯路。希望帮到你。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门