
简介基于 Python 与深度学习 CNN 的垃圾分类识别系统设计与实现源码是一份面向计算机相关专业学生的高分毕业设计项目评审分为 98 分。资源适用于正在完成毕业设计、课程设计或期末大作业的学生也适合需要项目实战练习的开发者。压缩包共包含 2000 个文件以 1196 个 jpg 和 789 个 jpeg 图片为主涵盖电池、塑料瓶等典型垃圾类别可作为 CNN 模型的训练与验证数据另有 13 个 Python 源码文件实现数据加载、模型构建、训练评估与识别输出等完整流程配合 1 个 json 配置文件和 1 个 md 说明文档便于快速梳理项目结构与运行方式。压缩包整体大小 564.36MB已有 254 人学习下载。通过这份源码读者可以获得从数据准备、模型训练到结果评估的端到端实现思路对完成同类深度学习项目、课程答辩或毕业设计二次开发都有实用参考价值。1. 垃圾分类识别CNN 图像分类项目到底难在哪做毕业设计或者期末大作业选“垃圾分类识别”这个方向的人很多但真正能跑通、能讲清原理、能在答辩时站得住脚的没几个。这个项目用的是 Python 深度学习 CNN 做图像分类原始数据集是两类实物图片——电池和塑料瓶每张图片都是真实拍摄的 JPEG 文件文件名带类别标签正好免去了自己爬数据、清洗数据的脏活。整套代码覆盖了数据加载、CNN 模型构建、训练、验证到推理的全流程适合正在做课程设计、期末大作业的在校学生也适合想练手图像分类实战的开发者。我的建议是别把它当成“调包跑通一次”就完事你真正要带走的是“怎么把一套图片数据变成能用的分类模型”的完整方法论。2. 数据准备与预处理从一堆 JPEG 到标准数据集2.1 图片文件结构文件名本身就是标签打开资源包你会看到一堆类似img_电池_452.jpeg、img_塑料瓶_244.jpeg这样的文件。这个命名规则很直观类别名直接嵌在文件名里。要做有监督分类训练第一步就是把文件名里的类别解析出来然后整理成深度学习框架能吃的目录结构。拿到原始图片后我一般先做一次完整性检查确认图片能正常读取、尺寸分布是什么情况。常见做法是写一个脚本统一处理import os import glob from PIL import Image # 假设原始图片都在 images_raw 目录下 image_paths glob.glob(images_raw/*.jpeg) print(f总共找到 {len(image_paths)} 张图片) # 检查每张图片是否能正常打开并统计尺寸 size_stats {} broken [] for path in image_paths: try: img Image.open(path) img.verify() # 只校验文件完整性不加载像素数据 size_stats[img.size] size_stats.get(img.size, 0) 1 except Exception as e: broken.append((path, str(e))) print(尺寸分布, size_stats) print(损坏图片, broken)这个脚本的作用有两层。img.verify()是 PIL 里校验文件头、数据块完整性的方法不会把整张图读进内存速度快且省资源。size_stats字典统计了所有图片的尺寸分布如果发现尺寸跨度很大后面就需要统一 resize。损坏图片单独列出来宁可在这一步删掉也不要让脏数据混进训练集。参数方面glob.glob的模式匹配用的是通配符*.jpeg会命中所有 jpeg 结尾的文件如果你手里还有.jpg和.png格式的图片要把匹配模式改成*.jp*g或直接遍历目录后缀判断。2.2 目录划分train / val / test 的比例与代码实现深度学习项目里数据划分是后续所有工作的地基。这里我用的是 8:1:1 的比例——80% 训练、10% 验证、10% 测试。划分时要保证每个类别在各个集合中的比例基本一致避免出现训练集里全是塑料瓶、测试集里全是电池这种极端情况。import os import shutil import random random.seed(42) # 先按类别建目录 for split in [train, val, test]: for cls in [电池, 塑料瓶]: os.makedirs(fdataset/{split}/{cls}, exist_okTrue) # 收集所有图片按类别分组 samples {电池: [], 塑料瓶: []} for path in image_paths: filename os.path.basename(path) if 电池 in filename: samples[电池].append(path) elif 塑料瓶 in filename: samples[塑料瓶].append(path) # 每个类别内部打乱后按比例切分 for cls, paths in samples.items(): random.shuffle(paths) n len(paths) n_train int(n * 0.8) n_val int(n * 0.1) for i, path in enumerate(paths): if i n_train: split_dir train elif i n_train n_val: split_dir val else: split_dir test dst fdataset/{split_dir}/{cls}/{os.path.basename(path)} shutil.copy(path, dst) print(划分完成各集合样本数) for split in [train, val, test]: for cls in [电池, 塑料瓶]: cnt len(os.listdir(fdataset/{split}/{cls})) print(f{split}/{cls}: {cnt})这段代码有几个关键点。random.seed(42)是固定随机种子保证每次运行划分结果一致这个习惯在需要复现实验时非常重要源码里如果没写推理逻辑通常也是这个套路。n_val是先用整数乘出数量再切Python 里int(0.1 * n)对小数截断样本量小的时候可能让 val 和 test 差一张问题不大但要知道有这个细节。切分用的是shutil.copy而不是move保留原始数据一份后面如果划分逻辑有问题还能重新来相当于给自己留了后悔药。2.3 数据增强与归一化为什么不能直接喂原图CNN 模型对输入尺寸有固定要求原图直接进网络会报维度错误。另外样本量本身不大直接用原始数量训练很容易过拟合。所以数据加载时我习惯用 PyTorch 的torchvision.transforms做两件事统一尺寸加数据增强再归一化。from torchvision import transforms train_transforms transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(10), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transforms transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这里有一个区分训练集用了RandomHorizontalFlip、RandomRotation、ColorJitter三个增强操作验证集和测试集只做 resize 和归一化。原因很朴素——验证集要看模型的真实表现加增强会让指标失真。Resize((224, 224))直接把图拉成正方形如果原图不是正方形会变形但在这个资源场景下图片基本接近正方形问题不大。如果遇到严重变形的场景可以考虑Resize(256)加CenterCrop(224)的组合先等比缩放再中心裁剪保留更多有效信息。均值方差用的是 ImageNet 的标准值[0.485, 0.456, 0.406]。如果是从零训练小网络也可以自己统计数据集的均值和方差但 ImageNet 的先验值在大多数情况下效果都不差直接用是省事且稳妥的选择。3. CNN 模型构建与训练从零训练还是微调3.1 两种路线自建小网络 vs 预训练模型迁移垃圾分类二分类任务电池、塑料瓶在计算资源有限的情况下完全可以从零训练一个小型 CNN。但如果你手里有 GPU或者项目要求里暗示了使用经典结构我建议用预训练模型做迁移学习效果往往好一大截。两者的选择逻辑是这样自建网络灵活、可控、代码量少答辩时能把每一层的作用讲得很清楚适合强调“自己实现”的课程设计迁移学习走的是 ResNet、VGG 这类成熟架构用 ImageNet 上训好的权重做初始化模型收敛快、准确率高适合追求结果分数的项目。这个项目的源码给我的感觉是偏自建方向代码结构适合快速读懂和修改。3.2 ResNet 迁移学习修改最后一层全连接的细节如果走迁移学习路线PyTorch 下用官方预训练模型只需要改一行输出层。这里以 ResNet18 为例它对 224×224 输入友好计算量适中。import torchvision.models as models import torch.nn as nn model models.resnet18(pretrainedTrue) # 替换最后一层全连接改成二分类输出 num_features model.fc.in_features model.fc nn.Linear(num_features, 2) print(model.fc)pretrainedTrue会从网络下载在 ImageNet 上训练好的权重如果环境没网这个参数要改成False然后自己从零训但效果会差不少。model.fc.in_features是读取原全连接层的输入维度ResNet18 是 512不同网络不一样用这行代码自适应取比写死更稳。把这个改成nn.Linear(512, 2)可能更直观但动态读取能让代码在换用 ResNet34、ResNet50 时不用改第二处。迁移学习的另一个关键点是冻结参数。如果要冻结前面所有层只训练最后的分类层可以这样for param in model.parameters(): param.requires_grad False # 只让最后一层参与梯度更新 for param in model.fc.parameters(): param.requires_grad True # 优化器只传 fc 层的参数 optimizer torch.optim.Adam(model.fc.parameters(), lr0.001)冻结前面层的思路是ImageNet 预训练模型的前几层已经学到了通用的边缘、纹理、形状特征这些特征对电池和塑料瓶同样适用不需要重新学。只微调最后分类层可以大幅减少训练时间和显存占用。但要注意如果图片的拍摄角度、背景风格和 ImageNet 差异很大冻结太多层反而会限制模型表达能力。3.3 训练循环从 loss 曲线判断模型学没学会不管用哪种模型训练循环的骨架是通用的。下面这段代码是核心我加了几个在 debug 时非常有用的输出。import torch import torch.nn as nn device torch.device(cuda if torch.cuda.isavailable() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) num_epochs 30 best_val_acc 0.0 for epoch in range(num_epochs): model.train() running_loss 0.0 correct 0 total 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() epoch_loss running_loss / total epoch_acc correct / total * 100 # 验证集评估 val_acc evaluate(model, val_loader, device) print(fEpoch {epoch1}/{num_epochs}, Loss: {epoch_loss:.4f}, fTrain Acc: {epoch_acc:.2f}%, Val Acc: {val_acc:.2f}%) if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_model.pth) print(f 保存最佳模型Val Acc: {val_acc:.2f}%) print(训练完成最佳验证准确率, best_val_acc)训练循环里有几个习惯值得学。optimizer.zero_grad()必须在每次反向传播前清空旧梯度漏了这行梯度会累加loss 直接飞掉。loss.item()取的是标量值用于记录日志loss 本身是张量直接用于打印会带着tensor(..., grad_fn...)的附加信息比较乱。torch.max(outputs, 1)返回每行最大值和对应的索引predicted就是每个样本的预测类别。关于保存模型torch.save(model.state_dict(), best_model.pth)只保存权重参数不保存整个模型结构。加载时需要先构建好相同的模型结构再load_state_dict。如果你想把结构也一起存可以用torch.save(model, model_complete.pth)但换环境时版本兼容性差我一般只推荐前一种方式。learning rate 的选择也很关键0.001 对 Adam 来说是常用的初始值。如果 loss 震荡很厉害可以先降到 0.0001 试试。如果 loss 下降太慢考虑是不是数据没归一化或者学习率太小别一上来就调网络结构。4. 推理与评估模型训练完怎么用、怎么测4.1 模型加载与单张图片预测训练出模型后最终要落到实际使用场景——给一张图片判断它是电池还是塑料瓶。推理代码和训练代码有个重要区别不需要梯度计算也不走 loss 和反向传播。import torch from PIL import Image from torchvision import transforms # 加载模型结构 model models.resnet18(pretrainedFalse) num_features model.fc.in_features model.fc nn.Linear(num_features, 2) # 加载训练好的权重 state_dict torch.load(best_model.pth, map_locationcpu) model.load_state_dict(state_dict) model.eval() # 单张图片预处理 def predict_image(image_path): image Image.open(image_path).convert(RGB) transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) input_tensor transform(image).unsqueeze(0) with torch.no_grad(): outputs model(input_tensor) probabilities torch.softmax(outputs, dim1) confidence, predicted torch.max(probabilities, 1) class_names [电池, 塑料瓶] print(f图片 {image_path} 预测结果{class_names[predicted.item()]}, f置信度{confidence.item():.4f}) predict_image(test_image/塑料瓶_001.jpeg)推理代码里有几个必须注意的细节。model.eval()必须调用它会关闭 dropout 和 batch normalization 的训练状态不调用的话同一个输入每次输出会不一样结果不稳定——这是新手最容易翻车的地方。torch.no_grad()上下文管理器关闭梯度计算显著减少显存占用和推理耗时。torch.softmax(outputs, dim1)把原始 logits 转成概率分布torch.max返回最大概率值和对应索引confidence能告诉你模型对这个预测有多确定。如果预测置信度低于 0.6我会把结果标成“不确定”而不是硬给一个类别。实际场景里测试图片可能是不在训练集里的对象比如快递纸箱模型没有见过这个类别硬分类没意义返回概率值让上层逻辑做判断才是工程上更稳妥的姿势。4.2 测试集评估准确率、混淆矩阵与单类性能训练过程中监控的是验证集指标但验证集被用来选模型了会存在轻微过拟合风险。所以最终要用没参与训练和验证的测试集评估模型真实性能。from sklearn.metrics import confusion_matrix, classification_report import numpy as np def evaluate_model(model, test_loader): model.eval() all_preds [] all_labels [] with torch.no_grad(): for images, labels in test_loader: images images.to(device) labels labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) all_preds.extend(predicted.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) acc np.mean(np.array(all_preds) np.array(all_labels)) * 100 cm confusion_matrix(all_labels, all_preds) report classification_report(all_labels, all_preds, target_names[电池, 塑料瓶], digits3) print(f测试集准确率{acc:.2f}%) print(混淆矩阵) print(cm) print(分类报告) print(report) return acc混淆矩阵在二分类问题中能暴露很多细节。比如总准确率 90% 但电池类别的召回率只有 70%说明模型对电池的识别偏保守很多电池被误判成了塑料瓶。这种信息在答辩时比一句“准确率 95%”更有说服力它说明你做过系统性评估知道模型短板在哪。classification_report里的 precision、recall、f1-score 三个指标每个都有自己的含义。精确率看的是“模型说它是电池有多大概率真的是电池”召回率看的是“所有电池里模型找出了多少”f1-score 是两者的调和平均。样本不均衡时准确率会骗人而这三个指标不会。5. 避坑与常见问题五个让我修复到半夜的坑5.1 图片通道数不一致现象训练时报错提示Expected 3 input channels, but got 1或got 4。原因数据里有灰度图单通道或者带透明通道的 PNG四通道。JPEG 本身没有透明通道但灰度 JPEG 是单通道读进来就是(H, W)而不是(3, H, W)。CNN 第一层卷积核是按三通道设计的输入维度不匹配直接报错。解决Image.open(path).convert(RGB)在读取阶段强制三通道。训练和推理的预处理都要用这个保持一致。我吃过一次亏是训练数据全处理好了但推理脚本里忘了写convert(RGB)灰度测试图直接崩。5.2 验证集准确率很高但测试集很差现象训练时 Val Acc 到了 95%测试集只有 70% 左右。原因最常见的是划分时没有打乱。如果数据集本身是按类别顺序排列的前 80% 全是电池后 20% 全是塑料瓶那验证集和测试集类别分布严重失衡。另外就是数据增强只在训练时使用如果验证和测试也加了增强会让指标失真。解决重写数据划分逻辑按类别分组后random.shuffle再切分。测试集永远只用基础 transformsresize normalize任何随机增强都不加。5.3 训练 loss 不降或降得很慢现象训练了十几个 epochloss 一直在 0.7 附近徘徊准确率停在 50% 上下。原因最常见的是学习率不合适或权重没初始化。0.01 对 ResNet 迁移学习来说偏高0.001 对大多数场景够用。另外如果冻结了太多层模型表达能力受限loss 也会降不到理想水平。解决先用 0.001 跑 5 个 epoch 看趋势如果 loss 完全不动可以试试 0.01如果 loss 剧烈震荡降到 0.0001。换优化器时注意SGD 需要调 momentum 和 weight_decayAdam 对超参数不敏感默认参数基本能用。5.4 推理时同一个输入两次预测结果不一样现象同一张图片跑两次 predict_image输出的类别或置信度不同。原因忘了调用model.eval()。训练模式下 dropout 随机失活神经元BatchNorm 使用当前 batch 的统计量每次前向传播都是一个“训练态”的随机结果。解决推理前加model.eval()记住这条就够了。一个完整的推理流程里eval()和no_grad()几乎总是成对出现一个是改模式一个是关梯度。5.5 GPU 和 CPU 环境切换导致加载模型报错现象训练时用了 GPU换到 CPU 环境加载模型时提示RuntimeError: Attempting to deserialize object on a CUDA device。原因torch.load默认按保存时的设备映射来GPU 保存的模型在 CPU 环境加载就会报设备不匹配。解决加载权重时加map_locationcpu。训练时的模型在 GPU 上device要指定“cuda”推理时如果机器没有 GPU模型结构构建后也用map_locationcpu加载权重再把模型to(cpu)。6. 把模型用起来写一个可复用的预测函数与边界测试这个项目如果只是训练完打个准确率就交差说实话有点可惜。我习惯把推理封装成可复用的模块方便在命令行或者小的 Web 接口里调用。下面是完整的封装代码包含了加载权重、预测、置信度判断和边界测试四件事。import torch import torch.nn as nn import torchvision.models as models from PIL import Image from torchvision import transforms class GarbageClassifier: def __init__(self, model_path, deviceNone): self.device device if device else torch.device(cuda if torch.cuda.is_available() else cpu) # 构建模型并加载权重 self.model models.resnet18(pretrainedFalse) self.model.fc nn.Linear(self.model.fc.in_features, 2) state_dict torch.load(model_path, map_locationself.device) self.model.load_state_dict(state_dict) self.model.to(self.device) self.model.eval() self.class_names [电池, 塑料瓶] self.transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def predict(self, image_path, confidence_threshold0.6): image Image.open(image_path).convert(RGB) input_tensor self.transform(image).unsqueeze(0).to(self.device) with torch.no_grad(): outputs self.model(input_tensor) probabilities torch.softmax(outputs, dim1) confidence, predicted torch.max(probabilities, 1) # 低于阈值返回 uncertain if confidence.item() confidence_threshold: return uncertain, confidence.item() return self.class_names[predicted.item()], confidence.item() if __name__ __main__: classifier GarbageClassifier(best_model.pth) test_images [ test_image/电池_001.jpeg, test_image/塑料瓶_002.jpeg, test_image/某种陌生物品.jpeg ] for img_path in test_images: label, conf classifier.predict(img_path) print(f{img_path}: {label} (置信度 {conf:.4f}))这个封装有几个值得借鉴的设计。__init__里完成模型构建和权重加载predict方法只做推理职责分离清晰。置信度阈值默认 0.6可以把“模型也没把握”的结果单独分流而不是强行输出一个类别。map_locationself.device保证在 CPU 或 GPU 环境下都能正常加载。构建模型时pretrainedFalse因为权重来自我们自己训练的best_model.pth不需要下载 ImageNet 预训练参数。边界测试我用三张图片两张正常类别的测试图外加一张既不像电池也不像塑料瓶的陌生物品图。前两张应该输出对应类别且置信度较高第三张最好输出uncertain。如果第三张也被硬分类到某个类别说明置信度阈值设得太低或者模型过拟合到靠背景特征做判断了。如果想把模型部署成对外服务可以在predict外面再套一层 Flask 接口接收 POST 上传的图片文件返回 JSON 格式的预测结果。流程是前端接收文件 → 保存临时文件 → 调用predict→ 返回{label: 电池, confidence: 0.97}。这个扩展不难但很加分毕业设计答辩时能现场演示 Web 端识别比在 Jupyter Notebook 里跑几行代码有说服力得多。关于模型固化我建议把best_model.pth和最终使用的模型结构描述写在一个 README 里说明输入的图片尺寸、通道数、类别顺序方便几个月后回来看代码时不用重新猜。这个习惯救过我很多次每次改了网络结构都会同步更新 README希望帮到你。本文还有配套的精品资源点击获取