垃圾识别分类系统实战:CNN构建、训练与部署全解析
2026/9/13 2:44:25 网站建设 项目流程

简介:面向课程设计和期末大作业场景,这套基于深度学习卷积神经网络的垃圾分类识别系统源码,提供了完整的数据集、预训练模型和详细文档,适合需要完成图像分类任务的学生参考使用。压缩包共四十三个文件,涵盖十二个脚本源码、权重模型、数据库、设计说明及多种格式图片,整体大小约三百一十五兆,目录结构清晰,代码注释完整,下载后即可直接运行。功能模块包括登录验证、界面展示、垃圾分类查询、模型测试和再训练等,另附可执行程序和界面设计文件,便于快速体验和二次开发。借助这些内容,可以系统学习卷积神经网络的数据处理、模型训练与评估流程,也能参考高分设计思路完成自己的课程作业。目前已有二百三十五人学习下载,适合用作课程设计或期末大作业的实用参考资料。

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识别图片,混入.gifwebp会导致读取失败。文件名里尽量不要带中文和空格,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(p=0.5), transforms.RandomRotation(degrees=15), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.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 均值/方差使用预训练模型时务必沿用标准值
RandomHorizontalFlipp=0.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_classes=6): super().__init__() self.features = nn.Sequential( # 输入 3x224x224 nn.Conv2d(3, 32, kernel_size=3, padding=1), # 32x224x224 nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 32x112x112 nn.Conv2d(32, 64, kernel_size=3, padding=1), # 64x112x112 nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 64x56x56 nn.Conv2d(64, 128, kernel_size=3, padding=1), # 128x56x56 nn.BatchNorm2d(128), nn.ReLU(inplace=True), 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))

这个结构的核心在于padding=1保证卷积不改变空间尺寸,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_classes=6, freeze_backbone=False): model = models.resnet18(weights=models.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 model

freeze_backbone=True时只训练最后的全连接层,适合数据量特别小的情况,训练速度快但精度上限低。数据量在每类 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_classes=6).to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-4) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.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", transform=train_transform), batch_size=32, shuffle=True, num_workers=2 ) val_loader = DataLoader( datasets.ImageFolder("dataset/val", transform=val_transform), batch_size=32, shuffle=False, num_workers=2 ) 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(f"epoch {epoch+1}: 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_size=32是 Adam 在分类任务上的通用起手值,显存不够时降到 16,但不要小于 8,否则 BatchNorm 的统计量不稳定。num_workers=2表示用 2 个子进程预加载图片,在 Windows 上如果报BrokenPipeError,优先排查这里。shuffle=True只在训练集使用,验证集必须保持顺序,这样评估结果才确定。

关键是 checkpoint 的保存策略:不是每个 epoch 都存,而是只在验证集准确率提升时覆盖保存。这样即使后面过拟合,也不会把答辩现场要用的权重覆盖掉。

4.3 epoch、batch size、学习率的可执行参数表

参数建议起点调参方向失败时的表现
epoch30看训练与验证 loss 是否还有明显下降趋势训练 loss 降、验证 loss 升 → 过拟合,减少 epoch
batch_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 API

5.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_names=class_names, digits=3) print(report) return cm

classification_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, device="cpu", top_k=3): 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, dim=1) top_probs, top_indices = torch.topk(probs, k=top_k) results = [ {"class": class_names[idx], "confidence": prob.item()} for idx, prob in zip(top_indices[0], top_probs[0]) ] return results

top_k=3这个参数值得单独说说。很多场景里模型对某一类只有 51% 的置信度,直接取最大类别做判断容易造成“强行分类”。返回前三个候选类及其置信度,后续逻辑可以决定:置信度高于阈值就用第一候选,否则标记为“待人工确认”。这个设计在答辩现场演示时非常加分,它说明你不只是把模型跑通,还考虑了真实使用中的不确定性。class_names必须来自训练时导出的 JSON 映射,否则索引和类别名就会错位。

预测用的 transform 里绝对不要加RandomHorizontalFlip这类随机增强,否则同一张图两次预测结果可能不同。

5.3 导出成 ONNX 模型:脱离 PyTorch 也能跑

如果要把识别能力开放成 HTTP 接口,通常不需要把整个训练环境搬到服务端。把模型权重转成 ONNX,就能只依赖onnxruntime做 CPU 推理,依赖链短得多。

import torch model = build_resnet18(num_classes=6) model.load_state_dict(torch.load("best_model.pt", map_location="cpu")) 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, dim=1) 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), keepdim=True) cam = torch.sum(weights * activations, dim=0).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, threshold=0.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。

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

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

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

立即咨询