
简介这是一份面向计算机、人工智能等专业学生与从业者的毕业设计资源实现基于深度学习卷积神经网络的智能垃圾分类功能主体为Python源码与配套说明文档。项目经过完整调试已在答辩评审中取得98分可稳定运行适合期末课程设计、大作业或毕业设计借鉴。压缩包共含216个文件大小17.29MB其中以XML布局文件、Java与Gradle工程文件、Python脚本及JPG/PNG图片为主另含MobileNetV2等模型权重文件便于直接加载测试与二次开发。目录结构清晰覆盖Android端界面与模型推理模块能够帮助读者快速定位核心逻辑。已有182人学习使用尤其适合需要快速搭建垃圾分类项目或深入理解CNN应用流程的读者。借助模型权重和说明文档可在现有基础上调整功能、替换数据集适配不同应用场景。1. 智能垃圾分类系统真正的分水岭不是CNN结构是检查点恢复与数据对齐很多人拿到一套基于深度学习卷积神经网络的智能垃圾分类系统源码第一反应是打开模型文件看网络结构。真正动手跑通之后会发现卡住你的往往不是 MobileNetV2、ResNet 这些卷积结构本身而是 checkpoint 怎么加载、图片预处理是不是和训练时一致、softmax 输出怎么映射回中文标签。这套基于 Python 的毕业设计源码把核心模型、预测脚本和资源文件放在一起正好把这些工程问题摊开在桌面上。这个项目能解决的问题很直接给定一张垃圾图片输出它属于可回收、有害、厨余还是其他垃圾并给出具体类别名。适合两类人看一类是正在做课程设计或毕设的学生需要一套能复现、能答辩讲解的 CNN 分类流程另一类是想把图像分类模型从 Colab 搬到本地环境的技术从业者想知道检查点文件到底该怎么和模型结构对齐。本文不会只讲概念会从 MobileNetV2 的卷积设计开始再落到恢复检查点、预处理、推理封装和 Grad-CAM 可视化验证每一步都有可复现的写法。2. MobileNetV2 为什么是垃圾分类的合理选择从深度可分离卷积到线性瓶颈2.1 深度可分离卷积如何把算力压到十分之一普通的卷积神经网络在处理一张 224×224 的垃圾图片时标准卷积层会对输入的所有通道同时做空间滤波。假设输入特征图有 32 个通道输出也要 64 个通道卷积核尺寸为 3×3那么这一层需要 32×64×3×3 个参数也就是 18432 个参数。这个数字在浅层还能接受但在深层网络中会成倍膨胀训练和推理都会变得笨重。MobileNetV2 在这里做了一个关键替换把标准卷积拆成两步。第一步是 depthwise convolution每个输入通道单独用一个 3×3 卷积核做空间滤波得到特征图尺寸不变、通道数不变的中间结果第二步是 pointwise convolution用 1×1 卷积在通道维度上做线性组合把通道数从 32 升到 64。两步加起来参数是 32×3×3 32×64×1×1 288 2048 2336 个比标准卷积少了一个量级。这套源码里选择 MobileNetV2 作为主干而不是 VGG 这类结构很大程度上就是因为垃圾分类系统要能跑在普通 CPU 机器上甚至迁移到树莓派或安卓端。深度可分离卷积带来的不仅是参数减少更重要的是实际前向推理时内存访问次数降低这对毕业设计演示场景非常友好。2.2 ReLU 之后信息会丢线性瓶颈模块的设计逻辑MobileNetV2 和第一代 MobileNet 最大的区别是引入了线性瓶颈。在标准卷积块里卷积层后面接 BN 和 ReLU 几乎是固定写法但在 MobileNetV2 的 bottleneck 中最后一个 pointwise 卷积的输出不再接 ReLU而是直接输出。原因要从低维空间的信息保留说起。ReLU 会把负值全部置零当特征图的通道维度很窄时经过 ReLU 后很多通道直接变成全零矩阵信息彻底丢失。垃圾分类图像里的材质纹理、边缘朝向这些特征恰恰分布在高频细节里一旦被 ReLU 洗掉模型就很难区分塑料瓶和玻璃瓶。所以 MobileNetV2 的 block 顺序是先 1×1 卷积升维然后接 ReLU6再做 depthwise 卷积再接 ReLU6最后 1×1 卷积降维降维输出不加激活函数。升维是为了给 ReLU 提供充足的信息冗余降维不加激活是为了保住低维表示。2.3 checkpoint 文件里到底存了什么项目源码目录里可以看到resMobileNetV2.ckpt.data-00000-of-00001和pretrain_weights.ckpt.data-00000-of-00001这种命名是 TensorFlow 检查点的典型格式。.data文件保存的是所有变量的张量值也就是卷积核权重、BN 层的均值和方差、全连接层的偏置。真正让检查点能恢复的还需要.index文件记录变量名和偏移量以及可能的.meta文件保存图结构。我在恢复这类检查点时习惯先用下方代码查看变量名确认模型文件对应的是什么结构。import tensorflow as tf # 列出 checkpoint 中的变量名称和形状 ckpt_path resMobileNetV2.ckpt try: for var_name, shape in tf.train.list_variables(ckpt_path): print(var_name, shape) except Exception as e: print(读取失败, e)这段代码的关键在于tf.train.list_variables。它并不加载完整模型只是读取检查点的元信息用来判断训练时保存的是 Keras 权重格式还是tf.train.Checkpoint格式。如果输出里看到layer_with_weights-0/kernel这类名字说明检查点里只存了权重如果看到optimizer、global_step等变量说明还保存了优化器状态可以从中断处继续训练。常见的一个坑是下载的资源里只有一个.data文件而没有.index文件这种情况下 TensorFlow 会直接报DataLossError或NotFoundError。遇到时不要怀疑代码写错优先检查文件完整性。3. 把 checkpoint 变成能用的垃圾分类模型Python 加载与推理3.1 模型骨架与 checkpoint 对齐拿到一套源码后最忌讳的事情是直接把模型输出层改掉然后尝试加载别人的检查点。检查点里的变量名和模型结构是强绑定的全连接层节点数不一致恢复时就会提示 shape 不匹配。这里推荐的做法是先用 Keras 把 MobileNetV2 骨架搭出来再按项目训练时的类别数接上分类头。import tensorflow as tf def build_model(num_classes): base_model tf.keras.applications.MobileNetV2( input_shape(224, 224, 3), include_topFalse, weightsNone, poolingavg ) x tf.keras.layers.Dropout(0.2)(base_model.output) outputs tf.keras.layers.Dense(num_classes, activationsoftmax)(x) model tf.keras.Model(inputsbase_model.input, outputsoutputs) return model # 假设垃圾分类类别数为 40 model build_model(num_classes40)这份代码里include_topFalse表示不要原始 ImageNet 分类头改用poolingavg把最后的空间特征压缩成一维向量再接 Dropout 和 Dense 层。这样做的原因是垃圾分类类别数通常不是 1000而是依据具体数据集定义的几十类比如废纸、塑料、玻璃、金属、厨余等需要自定义顶层。如果项目里提供了pretrain_weights.ckpt这部分很可能是 MobileNetV2 在 ImageNet 上预训练的主干权重加载时需要确保骨架部分的变量名匹配。使用layer.set_weights()逐一赋值也可以但遇到 BN 层时顺序容易出错我一般更倾向于用tf.train.Checkpoint或model.load_weights()统一恢复。3.2 图像预处理管线的四个对齐项垃圾分类模型最大的坑集中在预处理上。训练时如果用的是 TensorFlow 官方的 MobileNetV2 预训练权重那输入归一化方式就和迁移学习到的权重强绑定推理时改掉任何一个参数输出的 softmax 分布都会明显变化。import numpy as np from PIL import Image def preprocess_image(image_path): img Image.open(image_path).convert(RGB) img img.resize((224, 224), Image.BILINEAR) img_array np.array(img, dtypenp.float32) / 127.5 - 1.0 img_array np.expand_dims(img_array, axis0) return img_array这里有几个必须对齐的参数第一是尺寸MobileNetV2 默认输入是224如果训练时改成256推理时也必须是256第二是通道顺序用 PIL 读出来是 HWC用 CV2 读出来也是 HWC但如果当初训练用的数据是 CHW就必须在加载前转置第三是归一化方式/127.5 - 1.0将像素映射到[-1,1]很多从其他地方复制的代码会用/255.0这两者不可混用。实践里我见到的另一种偏差异常隐蔽数据集在训练时用了随机裁剪和水平翻转增强但推理时没有中心裁剪。这会导致模型在训练时看到的目标位置变化很大但推理时垃圾物体在图像中的比例、位置和训练集分布不一致表现大幅下降。如果发现准确率明显低于预期先查看训练代码里的增强策略。3.3 从 Softmax 到可读标签的映射逻辑模型输出的是一组概率值比如[0.01, 0.85, 0.03, ...]如果直接打印数组用户完全看不懂是哪一类垃圾。这个映射关系需要在加载类别列表时保持和训练时一致的顺序。class_names [ 纸箱, 易拉罐, 塑料瓶, 玻璃瓶, 剩饭, 果皮, 废电池, 过期药品 ] def predict_garbage(model, image_path, top_k3): img_array preprocess_image(image_path) probs model.predict(img_array, verbose0)[0] top_indices np.argsort(probs)[-top_k:][::-1] for idx in top_indices: print(f{class_names[idx]:8s} {probs[idx]:.4f}) return top_indices[0]这里np.argsort把概率从小到大排列然后取最后top_k个[::-1]反转成从大到小。返回的top_indices[0]是概率最高的类别索引查询class_names得到标签名。要注意的是class_names的排列顺序必须和训练时数据生成器传入的class_indices一致否则模型权重完全没变输出的索引却会对应到错误的中文名上。更可靠的做法是在源码里找到class_indices.json或者训练时的flow_from_directory配置确认标签字典里每个类别对应的数字。有些复现项目会把classes列表写入data/classes.txt推理时逐行读取这样的顺序只有一个事实来源不容易出错。4. 从调试到部署恢复检查点、超参数与推理服务4.1 恢复检查点时最常见的三类错误从源码包里的 checkpoint 恢复模型是整套代码运行前最让人头疼的环节。我总结了三类高频错误第一类是变量名不匹配比如训练代码使用了tf.keras.Model保存而推理代码手动写了Variable恢复时报unexpected key第二类是文件缺失检查点散落在不同目录tf.train.latest_checkpoint找不到最后写入的 index第三类是 shape 不匹配多见于分类头类别数不一致。错误现象原因处理方式NotFoundError: Key ... not found检查点与模型变量名不一致用tf.train.list_variables对比变量名DataLossError: truncated.data文件不完整确认文件哈希值重新下载ValueError: Shapes ... incompatible分类层节点数不同检查num_classes和训练配置加载检查点更稳定的方式是先加载到构建好的模型里再逐层验证是否能完成一次前向而不是直接干掉加载异常硬编码成try-except。检查点恢复是强状态的只有一个正确的对齐路径容错处理可以放在外部不要吞掉异常。4.2 训练超参数速查表垃圾分类属于细粒度图像分类中相对常规的场景因为各类别差异大通常用迁移学习就能达到不错的效果。关键在于训练阶段的参数设计。超参数推荐值说明输入尺寸224×224匹配 MobileNetV2 输入主干冻结层数前 100 层冻结保留低频纹理特征分类头学习率1e-3加速收敛主干微调学习率1e-5避免破坏预训练权重Batch Size32显存不足时降到 16优化器Adamepsilon 设置为 1e-8如果自己重新训练我一般先在冻结主干的情况下把分类头训练 10 个 epoch等到验证集准确率不再上升再解冻部分主干把学习率降到1e-5继续训练。这个流程和项目里pretrain_weights.ckpt的设计思路一致先借用 ImageNet 学习到的通用形状和纹理特征再在垃圾数据上做小幅度适应。4.3 把模型封装成 HTTP 推理服务训练好的模型只有变成可以被外部调用的服务才算是一个完整的智能垃圾分类系统。最常见的做法是用 Flask 包一层 HTTP 接口接收图片文件返回类别名和置信度。import io from flask import Flask, request, jsonify from PIL import Image app Flask(__name__) model build_model(num_classes40) model.load_weights(best_model.h5) app.route(/classify, methods[POST]) def classify(): file request.files[image] image Image.open(io.BytesIO(file.read())).convert(RGB) image.save(/tmp/input.jpg) idx predict_garbage(model, /tmp/input.jpg) return jsonify({ category: class_names[idx], probability: float(probs[idx]) }) if __name__ __main__: app.run(host0.0.0.0, port8501)这里是完整的推理链路Flask 接收图片 - PIL 解码 - 预处理 - 模型预测 - JSON 返回。代码里的load_weights适合保存成 h5 或权重文件的系统如果资源中只有.ckpt则需要换成model.load_weights(resMobileNetV2.ckpt)TensorFlow 会自动读取同目录下的.index文件。响应中加入图片编号、识别耗时、各类别概率会比只给一个标签更有调试价值前端展示时也能显示 Top-3 候选结果。5. 用 Grad-CAM 把卷积神经网络的分类依据可视化出来5.1 构建梯度模型的核心写法垃圾分类模型判断一张塑料瓶图片到底靠的是瓶身纹理还是背景颜色这个问题用准确率回答不了。Grad-CAM 的思路很直接让类别得分对最后一个卷积层的特征图求梯度把梯度空间平均后作为每个通道的权重再和特征图加权求和得到空间上的热度分布。import numpy as np import tensorflow as tf def grad_cam(model, img_array, class_idx, layer_nameConv_1): grad_model tf.keras.models.Model( inputsmodel.input, outputs[model.get_layer(layer_name).output, model.output] ) with tf.GradientTape() as tape: conv_output, predictions grad_model(img_array) loss predictions[:, class_idx] grads tape.gradient(loss, conv_output) pooled_grads tf.reduce_mean(grads, axis(0, 1, 2)) conv_output conv_output[0].numpy() pooled_grads pooled_grads.numpy() for i in range(pooled_grads.shape[-1]): conv_output[:, :, i] * pooled_grads[i] heatmap np.mean(conv_output, axis-1) heatmap np.maximum(heatmap, 0) / np.max(heatmap 1e-8) return heatmap代码里的关键在于tf.GradientTape记录了从输入到输出的完整前向过程然后对目标类别的得分反向传播。layer_name要替换成实际视觉特征最丰富的层在 MobileNetV2 中我一般选择最后一个Conv_1点卷积层而不是全局平均池化之后的层因为池化会把空间位置信息压掉热力图会失去定位能力。5.2 热力图叠加与判断标准得到热力图后把它缩放到原图尺寸叠加到原图上就能直观看到模型关注区域。判断标准很简单如果模型正确识别出易拉罐热力图应该集中在罐身中央或拉环位置而不是背景桌面如果识别厨余垃圾时热力图偏高物体边缘说明模型可能在依赖训练集的颜色伪影。Grad-CAM 解码后是低分辨率特征图需要插值放大。OpenCV 中直接用cv2.resize做双线性插值即可。这一部分在答辩和项目演示中很有说服力因为卷积神经网络不再是黑箱老师能够直接看到模型学到了什么特征。对于自己也读源码的人来说这也是最快确认“迁移学习是否真的继承了通用特征”的办法远比盯着训练曲线更有诊断价值。本文还有配套的精品资源点击获取