垃圾识别分类系统实战:CNN构建、训练与部署全解析
简介面向课程设计和期末大作业场景这套基于深度学习卷积神经网络的垃圾分类识别系统源码提供了完整的数据集、预训练模型和详细文档适合需要完成图像分类任务的学生参考使用。压缩包共四十三个文件涵盖十二个脚本源码、权重模型、数据库、设计说明及多种格式图片整体大小约三百一十五兆目录结构清晰代码注释完整下载后即可直接运行。功能模块包括登录验证、界面展示、垃圾分类查询、模型测试和再训练等另附可执行程序和界面设计文件便于快速体验和二次开发。借助这些内容可以系统学习卷积神经网络的数据处理、模型训练与评估流程也能参考高分设计思路完成自己的课程作业。目前已有二百三十五人学习下载适合用作课程设计或期末大作业的实用参考资料。1. 垃圾识别分类系统深度学习卷积神经网络的适用边界“垃圾识别分类系统”这个标题里最容易被看漏的是后半句它不只给你一个模型还自带了数据集与 Python 源码。换句话说这是一条已经闭合的技术链路——目录按类别摆好图片、用卷积神经网络训练权重、再写脚本对单张图片做推理。很多课程设计和入门项目的真实需求不是“发明新网络”而是在这条链路上把每一环跑通并且能讲清楚参数为什么要这样设。这类任务之所以默认选卷积神经网络而不是传统图像处理是因为垃圾图片在拍摄角度、光线、遮挡、形状畸变上的差异远远大于颜色阈值能覆盖的范围。CNN 通过卷积核自动学习局部纹理特征再通过池化逐层抽象天然适合“瓶身反光但仍是塑料瓶”这类场景。而到了具体落地最值得花时间的是数据集的组织、数据增强策略以及训练时对 loss 曲线的判断——这些恰好是项目标题里看不到、但使用者真正会卡住的地方。2. 垃圾图像数据集准备目录结构、标签映射与数据增强参数2.1 目录结构怎么定先按 ImageFolder 的约定排布PyTorch 的torchvision.datasets.ImageFolder是加载分类数据最省事的方式它要求图片按“根目录/类别名/图片文件”存放。对垃圾识别来说公开数据集多按 6 大类组织cardboard、glass、metal、paper、plastic、trash也有按 40 类细分的版本。拿到源码包后第一步不是急着训练而是确认data_root下是不是这种结构。dataset/ ├── train/ │ ├── cardboard/ │ ├── glass/ │ ├── metal/ │ ├── paper/ │ ├── plastic/ │ └── trash/ ├── val/ └── test/我会先写一段脚本统计每个目录下的图片数量并顺带检查损坏文件。ImageFolder 依赖扩展名.jpg、.jpeg、.png识别图片混入.gif或webp会导致读取失败。文件名里尽量不要带中文和空格Linux 下解析没有问题一旦把源码拷贝到 Windows 上路径分隔符和中文编码会让训练中断在你意想不到的地方。from pathlib import Path from PIL import Image def inspect_dataset(root: str): root Path(root) for split in [train, val, test]: split_dir root / split if not split_dir.exists(): continue for cls_dir in sorted(split_dir.iterdir()): images list(cls_dir.glob(*.jpg)) list(cls_dir.glob(*.jpeg)) list(cls_dir.glob(*.png)) broken 0 for img_path in images: try: Image.open(img_path).verify() except Exception: broken 1 print(f{split}/{cls_dir.name}: {len(images)} 张, 损坏 {broken} 张) inspect_dataset(dataset)这段逻辑的意义在于把“看不见的问题”暴露出来。Image.open().verify()不会完整解码图片但能识别截断的 JPEG 或伪造后缀的文件跑一遍通常只需要几秒。若发现某个类别只有个位数图片就要考虑做类别的合并或者针对该类额外采集否则训练时这个类别的 loss 会波动得非常厉害。2.2 标签映射保持训练与推理端一致ImageFolder 会按目录名字母顺序生成class_to_idx例如{cardboard: 0, glass: 1, ...}。训练阶段没问题但推理阶段如果直接用网络输出的索引反查类别很容易和标签订单不一致。常见做法是在训练前把映射关系写成 JSON 存档推理时直接读取同一份文件。{ 0: cardboard, 1: glass, 2: metal, 3: paper, 4: plastic, 5: trash }这个映射文件虽然只有 6 行但它决定了整条链路的正确性。模型输出的是 logits你拿到的argmax是索引必须通过映射文件翻译成可读的类别名。训练和推理使用同一份映射是最廉价的一致性保障。2.3 数据增强参数torchvision.transforms 的具体配置数据增强不是为了炫技而是让模型别只记住训练集里的特定角度。垃圾识别有个典型特点同一类垃圾在不同光照下的颜色差异极大瓶身的高光区域容易让模型学到“反光塑料”这种错误规律。以下几种变换是常用组合from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])参数建议范围作用与误用提醒Resize((224, 224))与预训练模型输入对齐直接拉伸会改变长宽比但对分类任务影响小于检测任务RandomRotation(degrees)10~20超过 30 度会引入过多黑色背景反而干扰ColorJitterbrightness/contrast 0.1~0.3垃圾识别要模拟室内外不同光照但不要过度NormalizeImageNet 均值/方差使用预训练模型时务必沿用标准值RandomHorizontalFlipp0.5等价于数据量翻倍成本最低这里最容易被忽略的是Normalize。如果用了 ResNet 的 ImageNet 预训练权重输入就必须减去 ImageNet 的 mean 再除以 std否则权重和输入分布不匹配前几层卷积的激活值会漂移训练初期的 loss 降不下去。数据增强只应该用在训练集验证集只用Resize ToTensor Normalize。2.4 脏数据的影响比模型结构更值得排查垃圾数据集中常见的“脏样本”是拍糊的、带手指遮挡的以及多个物体叠放在一起的图片。单标签模型对这类图片天然无解。我会在做训练前先按类别抽查 20 张图确认标签和内容基本对应再决定是否清洗。很多调了半天模型不收敛的问题最后都出在训练集和验证集里有重复图片或者某个类别被另一个类别的图片污染了。3. 用 Python 定义卷积神经网络从零搭 CNN 与迁移学习两条路3.1 为什么图像分类默认选 CNN图像可以看作由 R、G、B 三个通道组成的矩阵直接把像素值拉平输入全连接网络不是不行而是参数爆炸。一张 224×224 的图拉平后是 15 万个维度第一层全连接如果映射到 1024 维参数量就超过 1.5 亿在垃圾识别这种千级样本任务上必然过拟合。卷积神经网络用卷积核在二维平面上滑动同一卷积核在所有位置复用权重参数量被压缩到极低。更重要的是卷积操作天然保留空间局部关系瓶盖的边缘、瓶身的纹理、纸张的褶皱这些特征都由底层的卷积核组合出来。再通过池化逐步降低分辨率网络就能从“局部边缘”过渡到“全局类别”。垃圾识别的目标物体形状差异很大但纹理和材质特征相对稳定这正是 CNN 最擅长的输入分布。3.2 从零搭一个最小 CNN结构、输出与参数含义课程设计里常会遇到“不让用预训练模型”或“必须自己定义网络结构”的要求这时一个三层卷积加全连接的结构足够跑通又不会让代码看起来单薄。import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes6): super().__init__() self.features nn.Sequential( # 输入 3x224x224 nn.Conv2d(3, 32, kernel_size3, padding1), # 32x224x224 nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 32x112x112 nn.Conv2d(32, 64, kernel_size3, padding1), # 64x112x112 nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 64x56x56 nn.Conv2d(64, 128, kernel_size3, padding1), # 128x56x56 nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 128x28x28 ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), # 128x1x1 nn.Flatten(), nn.Linear(128, num_classes), ) def forward(self, x): return self.classifier(self.features(x))这个结构的核心在于padding1保证卷积不改变空间尺寸MaxPool2d(2)每层将宽高减半最终从 224 缩小到 28。AdaptiveAvgPool2d(1)把特征图压缩成一个 128 维向量再经过全连接输出 6 个类别的 logits。如果换用更大的输入如 256网络结构不需要改AdaptiveAvgPool2d会自动适配最后的分辨率。3.3 用 ResNet18 迁移学习更快收敛的常规操作当训练集只有几千张图时从零训练 CNN 很容易过拟合。更稳的做法是在 ImageNet 预训练权重的基础上做迁移学习把最后的全连接层换成自己的类别数。import torchvision.models as models import torch.nn as nn def build_resnet18(num_classes6, freeze_backboneFalse): model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) if freeze_backbone: for param in model.parameters(): param.requires_grad False model.fc nn.Linear(model.fc.in_features, num_classes) return modelfreeze_backboneTrue时只训练最后的全连接层适合数据量特别小的情况训练速度快但精度上限低。数据量在每类 500 张以上时通常不冻结而是用一个较低的学习率微调全部参数。要注意的是既然替换了model.fc输入预处理就必须和 ImageNet 保持一致否则预训练权重的统计信息会被破坏。ResNet18 的参数总量在 1100 万左右CPU 上单张推理约几十毫秒课程设计答辩现场用 CPU 跑演示完全来得及。3.4 两种方案怎么选一张表说清边界对比维度简版 CNNResNet18 迁移学习参数量约百万级约千万级训练集要求每类 500 张以上才稳每类 100 张可启动达到精度的速度慢需要较多 epoch快10 个 epoch 左右开始收敛代码复杂度需要自己写网络定义依赖 torchvision 封装答辩讲解空间卷积、池化、全连接全覆盖重点在迁移学习与微调策略如果这是课程设计我通常建议主体用 ResNet18 保证结果美观再在答辩 PPT 里附带一份简版 CNN 的对比实验讲清楚“为什么简单结构不够、残差连接解决了什么”。这比只跑通一个方案在深度上有优势。4. 训练脚本与调参epoch、batch size、学习率与收敛判断4.1 损失函数与优化器的搭配逻辑垃圾识别是单标签多分类问题标准的损失函数是交叉熵CrossEntropyLoss。它内部会先做 softmax 再把输出和目标标签做损失计算所以网络最后一层通常不加 softmax。优化器方面两种主流选择是 Adam 和带动量的 SGD。Adam 收敛快、对学习率不敏感适合快速跑通SGD Momentum 最终精度通常更高但需要把学习率调低从 0.01 或 0.001 起步慢慢调。我个人在迁移学习场景更常用 Adam初始学习率1e-4配合 StepLR 每 10 个 epoch 把学习率乘 0.1。这样前几个 epoch 快速下降后期用小学习率细调。4.2 训练循环DataLoader、checkpoint、日志一个不能少import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms device torch.device(cuda if torch.cuda.is_available() else cpu) model build_resnet18(num_classes6).to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.1) val_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]) ]) train_loader DataLoader( datasets.ImageFolder(dataset/train, transformtrain_transform), batch_size32, shuffleTrue, num_workers2 ) val_loader DataLoader( datasets.ImageFolder(dataset/val, transformval_transform), batch_size32, shuffleFalse, num_workers2 ) best_acc 0.0 for epoch in range(30): model.train() total_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() # 验证阶段 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() val_acc correct / total print(fepoch {epoch1}: train_loss{total_loss/len(train_loader):.4f}, val_acc{val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pt) print(f保存最佳模型: {val_acc:.4f}) scheduler.step()这段代码有几个值得说明的参数设计。batch_size32是 Adam 在分类任务上的通用起手值显存不够时降到 16但不要小于 8否则 BatchNorm 的统计量不稳定。num_workers2表示用 2 个子进程预加载图片在 Windows 上如果报BrokenPipeError优先排查这里。shuffleTrue只在训练集使用验证集必须保持顺序这样评估结果才确定。关键是 checkpoint 的保存策略不是每个 epoch 都存而是只在验证集准确率提升时覆盖保存。这样即使后面过拟合也不会把答辩现场要用的权重覆盖掉。4.3 epoch、batch size、学习率的可执行参数表参数建议起点调参方向失败时的表现epoch30看训练与验证 loss 是否还有明显下降趋势训练 loss 降、验证 loss 升 → 过拟合减少 epochbatch_size32显存不足降到 16收敛慢可以试 64loss 震荡剧烈 → 批次太小或学习率太大learning_rate1e-4 (Adam)前 5 个 epoch 没明显下降就降到 3e-5loss 直接 NaN → 学习率太大step_size10数据量小时可缩短到 7验证准确率曲线平台期无法突破gamma0.1后期收敛慢可不衰减或改为 0.3学习率过小导致 loss 卡住关于 epoch 的理解我一般建议学员盯着 loss 曲线而不是单纯跑满固定轮数。理想曲线是训练 loss 单调下降验证准确率逐步上升直到平台期。如果训练 loss 持续下降但验证准确率停滞可以先怀疑过拟合回看数据增强强度够不够如果训练 loss 都降不下去问题通常不在模型而在数据比如标签错乱、预处理不一致或学习率过高。4.4 训练 Loss 不降的排查顺序按经验最频繁的失败直接原因依次是归一化参数和预训练权重不匹配、标签生成错误导致随机梯度扰动、学习率过大导致梯度爆炸。拿到训练日志后第一步看 loss 初始值ResNet18 迁移学习在交叉熵下初始 loss 应该接近ln(6) ≈ 1.79。如果初始 loss 远高于这个值说明输入预处理或标签映射有问题如果 loss 是 NaN直接降低学习率。5. 验证、推理与接口化评估指标、单图预测与 Python API5.1 不止看准确率用混淆矩阵看类别混淆垃圾识别的 6 个类别里玻璃和塑料常常互相误判因为透明材质在普通光照下本身就难用肉眼区分。光看整体准确率容易掩盖这类问题所以训练完模型后要做一份混淆矩阵。from sklearn.metrics import confusion_matrix, classification_report import numpy as np def evaluate_model(model, val_loader, class_names): model.eval() all_labels [] all_preds [] with torch.no_grad(): for images, labels in val_loader: images images.to(device) labels labels.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_labels.extend(labels.cpu().numpy()) all_preds.extend(preds.cpu().numpy()) cm confusion_matrix(all_labels, all_preds) report classification_report(all_labels, all_preds, target_namesclass_names, digits3) print(report) return cmclassification_report会输出每个类别自己的 precision、recall、F1-score这几项指标比准确率更能说明哪些类别是短板。比如 metal 的 recall 是 0.83但 plastic 对 metal 的误判占了不少比例结合混淆矩阵的可用数据去调整数据增强策略比继续堆 epoch 更有效。评估时要注意torch.no_grad()必不可少它关闭了自动求导记录既能省内存又避免意外修改模型参数。脚本跑完后若发现某两类的混淆比例过高可以考虑增加这两类的训练样本量或者在数据增强中加入更强的颜色扰动让模型对材质差异更敏感。5.2 写一个单图预测函数从 Tensor 到可读结果训练完成后的核心工作是把权重加载回来封装成可复用的推理函数。import torch from PIL import Image from torchvision import transforms def predict_image(model, image_path, class_names, devicecpu, top_k3): model.to(device) model.eval() 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]) ]) image Image.open(image_path).convert(RGB) tensor transform(image).unsqueeze(0).to(device) with torch.no_grad(): logits model(tensor) probs torch.softmax(logits, dim1) top_probs, top_indices torch.topk(probs, ktop_k) results [ {class: class_names[idx], confidence: prob.item()} for idx, prob in zip(top_indices[0], top_probs[0]) ] return resultstop_k3这个参数值得单独说说。很多场景里模型对某一类只有 51% 的置信度直接取最大类别做判断容易造成“强行分类”。返回前三个候选类及其置信度后续逻辑可以决定置信度高于阈值就用第一候选否则标记为“待人工确认”。这个设计在答辩现场演示时非常加分它说明你不只是把模型跑通还考虑了真实使用中的不确定性。class_names必须来自训练时导出的 JSON 映射否则索引和类别名就会错位。预测用的 transform 里绝对不要加RandomHorizontalFlip这类随机增强否则同一张图两次预测结果可能不同。5.3 导出成 ONNX 模型脱离 PyTorch 也能跑如果要把识别能力开放成 HTTP 接口通常不需要把整个训练环境搬到服务端。把模型权重转成 ONNX就能只依赖onnxruntime做 CPU 推理依赖链短得多。import torch model build_resnet18(num_classes6) model.load_state_dict(torch.load(best_model.pt, map_locationcpu)) model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, garbage_classifier.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}} ) print(导出完成)dynamic_axes让 ONNX 模型支持动态 batch接口被并发调用时不需要为每个请求单独创建固定尺寸的输入。导出后先用onnxruntime加载跑一次同一张图和 PyTorch 的输出对一下确认偏差在1e-5以内再上线。常见的坑是 BatchNorm 层在eval模式下的统计量使用方式不同所以导出前务必保证模型已切换为eval。5.4 推理性能的目标参考部署方式硬件环境单张推理耗时适用场景PyTorch CPU普通台式机30~80ms课程设计、离线批量识别PyTorch GPU入门级显卡5~15ms模型训练、高吞吐实验ONNX CPU普通台式机20~50msHTTP 接口、轻量部署ONNX GPU显卡 CUDA3~10ms实时视频流识别垃圾识别系统对时延并不极端敏感核心指标仍然以准确率和误判的类别为主。演示时与其秀推理速度不如验证对不同光照、不同拍摄角度图片的稳定性。6. 最后调一拳置信度阈值与可解释性检查6.1 用 Grad-CAM 看模型关注的是“垃圾”还是“背景”模型在验证集上达到 90% 准确率不代表它学到了“塑料瓶”这个概念。它很可能学到了图中背景里的桌面颜色或者数据集拍摄角度里固定的阴影位置。要验证这一点常用的手段是 Grad-CAM 可视化把模型做决策时最关注的区域以热力图形式叠加回原图上。import torch from torchvision.transforms import functional as F def grad_cam_visualize(model, image_path, class_idx, target_layer): model.eval() image Image.open(image_path).convert(RGB) input_tensor F.normalize(F.resize(F.to_tensor(image), (224, 224)).unsqueeze(0), mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) features [] def hook_fn(module, input, output): features.append(output) hook target_layer.register_forward_hook(hook_fn) output model(input_tensor) hook.remove() prob torch.softmax(output, dim1) target prob[0, class_idx] model.zero_grad() target.backward() gradients features[0].grad.squeeze(0) activations features[0].squeeze(0) weights torch.mean(gradients, dim(1, 2), keepdimTrue) cam torch.sum(weights * activations, dim0).relu() cam (cam - cam.min()) / (cam.max() - cam.min()) return image, cam.detach().numpy()把热力图叠加回原图后理想结果是高亮区域集中在瓶身、纸张褶皱等物体本体。如果高亮区域分布在图片角落或桌面纹理上就要回看数据集的拍摄背景是不是过度统一。这种可解释性检查在答辩现场极其有用直接说明模型鲁棒性的改进方向而不是只给出一个孤立的准确率数字。6.2 置信度阈值让系统学会“承认不知道”现实里的垃圾图片可能同时包含多个物品甚至出现训练集中从未出现的材质。合适的设计不是把所有输入都硬分到某个类别而是设置置信度下限。当最大类别得分低于阈值时系统返回“需要人工确认”而不是一个自信心不足的强制分类。def classify_with_threshold(model, image_path, class_names, threshold0.6): results predict_image(model, image_path, class_names) top results[0] if top[confidence] threshold: return {label: 待人工确认, confidence: top[confidence], candidates: results} return {label: top[class], confidence: top[confidence]}阈值取 0.6 还是 0.7取决于你对漏判和误判的容忍度。阈值越高系统越保守垃圾袋里有多个物体时更容易触发“人工确认”但正常单物体的图片也会被误拒。我一般会用验证集里每个类别的精度/召回曲线来选阈值而不是拍脑袋定。真正有价值的是这样一个推理结论模型在置信度 0.45 时给出的候选类别几乎都是错的那 0.5 就不如 0.6。本文还有配套的精品资源点击获取