简介:这份资源是面向计算机、通信、人工智能、自动化等专业学生与从业者的深度学习实战项目,以卷积神经网络为核心实现智能垃圾分类,可作为毕业设计、课程大作业或期末课程设计的完整参考方案。项目代码经过调试测试,答辩评审分达到98分,基础较好的学习者还能在此基础上修改调整,扩展出不同功能。压缩包共216个文件,约17.29MB,包含Python源码、TensorFlow检查点与预训练权重文件、Java与XML配置、PNG与JPG图像素材、Gradle与Maven构建脚本以及说明文档等,覆盖模型训练、界面实现与工程配置多个环节,目录结构清晰,便于按模块检索学习。目前已有182人学习下载。对于希望掌握卷积神经网络垃圾分类完整流程的读者,可从中获取模型搭建、权重加载、数据组织与项目部署的实践思路,适合小白入门与进阶提升。
1. 从一张垃圾桶照片说起:这套 CNN 垃圾分类系统到底能跑出什么效果
实验室楼下四个垃圾桶,可回收、有害、厨余、其他,每天中午都有人站在前面犹豫三秒然后随手一扔。我拿手机拍了 200 张这样的照片,想验证一件事:一个不依赖云端、能在本地跑起来的卷积神经网络,到底能不能把「塑料瓶」和「一次性纸杯」分开。结论是能,但前提是你得把数据、输入尺寸和最后一层改对,否则准确率会卡在 60% 上下反复横跳,这就是很多人做深度学习毕业设计时最常翻车的地方。
这套「基于深度学习卷积神经网络的智能垃圾分类系统」本质上是三件事的组合:一个图像分类模型(常见做法是 ResNet18 或 MobileNetV3 做迁移学习)、一套数据管道(把拍照得到的原始图整理成 ImageFolder 能读的目录结构)、一个推理入口(命令行或一个轻量 Web 界面)。它解决的不是「垃圾识别」这个宏大命题,而是「给定一张 224×224 的 RGB 图,输出它属于哪一类」这个可复现的工程问题。适合谁?适合正在做计算机毕业设计、需要一份能跑通、能讲清楚、能写进论文的 Python 深度学习项目的同学,也适合想用 CNN 练手但不想从零造轮子的入门者。下面我按「先跑通再优化」的顺序,把每一步的参数和坑都摊开讲。
2. 数据准备与目录结构:把散落照片变成 ImageFolder 能吃的格式
2.1 为什么不用现成数据集直接开跑
公开的垃圾分类数据集(比如 TrashNet、Kaggle 上的垃圾分类竞赛数据)质量参差不齐,最常见的问题是类别不均衡:纸类几千张,有害垃圾只有几十张。直接拿来训练,模型会把所有输入都预测成样本最多的那一类,准确率看着有 70%,实际毫无用处。我一般会先做一次类别统计,再决定是过采样、欠采样还是加类别权重。另一个坑是图片尺寸和通道不统一,有的带 alpha 通道,有的是灰度图,直接喂给 CNN 会在第一个卷积层报维度错误。
所以第一步不是写模型,而是把数据整理成torchvision.datasets.ImageFolder要求的格式:每个类别一个子文件夹,文件夹名就是类别标签。这个约定能省掉你自己写 Dataset 类的大部分工作。
2.2 目录结构与数据清洗脚本
假设你拍的照片按来源散在raw/下,先跑一段脚本做清洗和划分。下面这段代码做三件事:过滤掉损坏图片、统一转成 RGB、按 8:1:1 划分训练/验证/测试集。
import os import shutil import random from PIL import Image RAW_DIR = "raw" # 原始照片,按类别分子目录 OUT_DIR = "dataset" # 输出根目录 SPLIT = (0.8, 0.1, 0.1) # 训练/验证/测试比例 SEED = 42 random.seed(SEED) def is_valid_image(path): try: with Image.open(path) as im: im.verify() # 校验文件完整性 with Image.open(path) as im: im.convert("RGB") # 统一转 RGB,丢弃 alpha return True except Exception: return False for cls in os.listdir(RAW_DIR): cls_dir = os.path.join(RAW_DIR, cls) if not os.path.isdir(cls_dir): continue files = [f for f in os.listdir(cls_dir) if f.lower().endswith((".jpg", ".jpeg", ".png", ".bmp"))] files = [f for f in files if is_valid_image(os.path.join(cls_dir, f))] random.shuffle(files) n = len(files) n_train = int(n * SPLIT[0]) n_val = int(n * SPLIT[1]) splits = { "train": files[:n_train], "val": files[n_train:n_train + n_val], "test": files[n_train + n_val:], } for split, names in splits.items(): dst = os.path.join(OUT_DIR, split, cls) os.makedirs(dst, exist_ok=True) for name in names: src = os.path.join(cls_dir, name) img = Image.open(src).convert("RGB") img.save(os.path.join(dst, name)) # 重新保存,顺带修正格式 print(f"{cls}: total={n}, train={len(splits['train'])}, " f"val={len(splits['val'])}, test={len(splits['test'])}")逻辑说明:is_valid_image用两次Image.open,第一次verify()只检查文件头,第二次才真正解码并转 RGB,避免「文件能打开但解码失败」的漏网之鱼。random.seed(42)保证每次划分结果一致,论文里写「随机划分」时别人能复现。参数SPLIT我一般设 8:1:1,如果某类样本少于 200 张,会把验证集比例降到 0.05,把更多数据留给训练。
跑完后目录长这样:
dataset/ train/ 可回收/ 有害/ 厨余/ 其他/ val/ 可回收/ 有害/ 厨余/ 其他/ test/ 可回收/ 有害/ 厨余/ 其他/提示:类别文件夹名不要用中文以外的特殊字符,Windows 和 Linux 下编码不一致会导致 ImageFolder 读不到。如果必须用中文,确认 Python 文件系统编码是 UTF-8。
2.3 数据增强的边界:哪些变换会帮倒忙
训练时加数据增强能缓解过拟合,但垃圾分类这个场景有几个反直觉的点。随机水平翻转基本安全,因为瓶子左右翻转还是瓶子。但随机垂直翻转要慎用,倒置的「有害垃圾」标志可能被模型学成另一个类别。颜色抖动(ColorJitter)幅度别开太大,否则「厨余垃圾」的褐色被抖成灰色,模型会混淆。我一般用这套组合:
from torchvision import transforms train_tf = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), # 随机裁剪 transforms.RandomHorizontalFlip(p=0.5), # 只做水平翻转 transforms.ColorJitter(brightness=0.2, contrast=0.2), # 幅度克制 transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), # ImageNet 统计量 ]) val_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])参数说明:RandomResizedCrop的scale=(0.7, 1.0)表示裁剪面积占原图 70% 到 100%,太小会把垃圾主体裁掉。Normalize用的均值方差是 ImageNet 的统计量,因为后面要用 ImageNet 预训练权重做迁移学习,必须对齐。如果你从零训练,可以改成自己数据集的统计量,但收益不大,直接用这套就行。
3. 模型选型与迁移学习:ResNet18 和 MobileNetV3 怎么选
3.1 为什么毕业设计不建议从零搭 CNN
热词里经常出现「卷积神经网络结构图」「lenet5 卷积神经网络」,很多人第一反应是手写一个 LeNet 或自己堆几层 Conv+Pool。从学习角度这没错,但从项目落地角度,从零训练的 CNN 在几千张图上很难超过 75% 准确率,而且训练慢、调参玄学。常见做法是用预训练模型做迁移学习:把 ImageNet 上学到的特征拿过来,只替换最后的全连接层,用你的垃圾分类数据微调。这样即使每类只有几百张图,也能跑到 90% 以上。
选型上,ResNet18 参数量约 1100 万,MobileNetV3-Small 约 250 万。如果你要在树莓派或手机上部署,选 MobileNetV3;如果只在 PC 上跑、追求准确率,ResNet18 更稳。两者在垃圾分类这种 4 到 6 类的任务上差距不大,我一般先用 ResNet18 跑基线,再换 MobileNetV3 看能不能在准确率掉 2% 以内的情况下把模型缩小。
3.2 替换分类头的两种写法和参数含义
import torch.nn as nn from torchvision import models def build_resnet18(num_classes=4, freeze_backbone=True): model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) if freeze_backbone: for p in model.parameters(): p.requires_grad = False # 冻结主干 in_features = model.fc.in_features # ResNet18 是 512 model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) # 替换成你的类别数 ) return model def build_mobilenet_v3(num_classes=4, freeze_backbone=True): model = models.mobilenet_v3_small( weights=models.MobileNet_V3_Small_Weights.IMAGENET1K_V1) if freeze_backbone: for p in model.parameters(): p.requires_grad = False in_features = model.classifier[0].in_features # 576 model.classifier = nn.Sequential( nn.Linear(in_features, 256), nn.Hardswish(), nn.Dropout(0.3), nn.Linear(256, num_classes) ) return model逻辑说明:freeze_backbone=True时只训练新加的分类头,适合数据量小于 5000 张的情况,训练快且不容易过拟合。如果数据超过 1 万张,可以把freeze_backbone设为 False,用较小的学习率(比如 1e-4)整体微调。Dropout(0.3)放在全连接前,是防止小数据集过拟合的常规手段,别设太大,0.5 以上会欠拟合。
参数说明:num_classes必须和你的类别文件夹数量一致,多一个少一个都会在算 loss 时报维度错误。ResNet18 的fc.in_features是 512,MobileNetV3-Small 的classifier[0].in_features是 576,这两个数字写死也能跑,但用属性读取更保险,换模型时不用改。
3.3 训练循环里必须盯住的三个量
训练脚本本身不复杂,但有三个量决定你能不能收敛:学习率、batch size、以及验证集准确率的波动。我一般用 Adam,学习率 1e-3(冻结主干时)或 1e-4(整体微调时),batch size 设 32 或 64,取决于显存。下面是一个最小训练循环的骨架:
import torch from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder device = torch.device("cuda" if torch.cuda.is_available() else "cpu") train_ds = ImageFolder("dataset/train", transform=train_tf) val_ds = ImageFolder("dataset/val", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4) model = build_resnet18(num_classes=len(train_ds.classes)).to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam( filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3) for epoch in range(20): model.train() for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() loss = criterion(model(imgs), labels) loss.backward() optimizer.step() # 验证 model.eval() correct = total = 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) pred = model(imgs).argmax(dim=1) correct += (pred == labels).sum().item() total += labels.size(0) print(f"epoch {epoch}: val_acc={correct/total:.4f}")逻辑说明:filter(lambda p: p.requires_grad, ...)只把需要更新的参数交给优化器,冻结的主干不会被误更新。验证阶段用model.eval()和torch.no_grad(),前者关掉 Dropout 和 BatchNorm 的训练行为,后者省显存。如果验证准确率连续 5 个 epoch 不涨,就该降学习率或早停,别硬跑 100 轮。
参数说明:num_workers=4在 Windows 上有时会卡死,改成 0 用主进程加载,慢但稳。batch_size如果显存不够就减半,同时把学习率也减半,这是经验规律。
4. 推理、部署与界面:把模型变成能演示的系统
4.1 单张图片推理的最小命令
训练完保存权重后,推理入口要能接受一张任意尺寸的图,输出类别和置信度。下面这段可以直接当命令行工具用:
import sys import torch from PIL import Image from torchvision import transforms def predict(img_path, ckpt_path="best.pth"): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") ckpt = torch.load(ckpt_path, map_location=device) classes = ckpt["classes"] # 保存时把类别名一起存 model = build_resnet18(num_classes=len(classes), freeze_backbone=False) model.load_state_dict(ckpt["state_dict"]) model.to(device).eval() tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) img = Image.open(img_path).convert("RGB") x = tf(img).unsqueeze(0).to(device) # 加 batch 维度 with torch.no_grad(): prob = torch.softmax(model(x), dim=1)[0] idx = prob.argmax().item() return classes[idx], prob[idx].item() if __name__ == "__main__": cls, conf = predict(sys.argv[1]) print(f"预测类别: {cls}, 置信度: {conf:.4f}")逻辑说明:保存 checkpoint 时把classes列表一起存进去,推理时才能把索引映射回类别名,否则你只知道「第 2 类」却不知道第 2 类是什么。unsqueeze(0)是给单张图补上 batch 维度,因为模型期望输入是[N, C, H, W]。softmax把 logits 转成概率,方便展示置信度。
参数说明:Resize((224, 224))必须和验证集一致,训练时用了RandomResizedCrop但验证和推理都用直接 Resize,这是标准做法。如果推理时用了不同的尺寸,准确率会明显下降,这个坑很隐蔽。
4.2 用 Gradio 搭一个能演示的界面
毕业设计答辩时,命令行输出不够直观,常见做法是用 Gradio 或 Streamlit 套一个上传图片的界面。Gradio 代码量最少:
import gradio as gr def classify(img): if img is None: return {} cls, conf = predict_from_array(img) # 把 PIL 图转成模型输入 return {cls: conf} demo = gr.Interface( fn=classify, inputs=gr.Image(type="pil"), outputs=gr.Label(num_top_classes=4), title="智能垃圾分类", ) demo.launch(server_name="0.0.0.0", server_port=7860)逻辑说明:gr.Image(type="pil")让 Gradio 把上传的图直接转成 PIL 对象,省去自己处理文件流。gr.Label(num_top_classes=4)会显示所有类别的概率条,答辩时比只显示一个结果更有说服力。server_name="0.0.0.0"让局域网内其他设备也能访问,方便演示。
参数说明:server_port默认 7860,如果被占用就换一个。num_top_classes设成你的类别总数,这样能看到模型在哪些类上犹豫。
4.3 模型导出与轻量化:什么时候该做,什么时候别碰
如果只是 PC 演示,导出 ONNX 或 TorchScript 意义不大,反而多一层转换可能引入精度损失。只有当你要部署到边缘设备(比如 Jetson Nano、树莓派)时,才考虑导出 ONNX 再用 ONNX Runtime 推理,或者用 Torch 的量化工具把 FP32 转成 INT8。我一般建议毕业设计阶段先把 PyTorch 版本跑稳,量化作为「未来工作」写进论文即可,别在答辩前一周折腾量化,翻车概率很高。
5. 避坑与排查:训练不收敛、准确率虚高、推理对不上
5.1 训练 loss 不降,验证准确率卡在 25%
现象:四个类别,验证准确率一直在 25% 左右,等于随机猜。原因通常是标签和图片没对上,或者ImageFolder读到的类别顺序和你以为的不一致。解决:打印train_ds.classes和train_ds.class_to_idx,确认类别名和索引的映射;再抽几张图连同标签可视化一遍,看是不是文件名排序导致的错位。
5.2 训练准确率 99%,测试准确率 60%
现象:训练集上几乎全对,测试集惨不忍睹。原因是过拟合,常见于数据量小又没加增强、或者把测试集混进了训练集。解决:检查划分脚本有没有把同一张图分到两个集合;加数据增强;把freeze_backbone设为 True 只训练分类头;加 Dropout 和权重衰减(weight_decay=1e-4)。
5.3 推理时置信度全是 0.25 左右
现象:单张图推理,四个类别概率几乎相等。原因是推理时的预处理和训练时不一致,最常见的是忘了Normalize,或者用了不同的输入尺寸。解决:把推理的 transform 和验证集的 transform 写成同一个对象,别手抄一遍,抄错一个参数就前功尽弃。
5.4 CUDA out of memory
现象:训练几个 batch 后报显存不足。原因是 batch size 太大或图片尺寸太大。解决:把 batch size 从 64 降到 32 或 16,同时学习率按比例降;或者把输入从 256 降到 224。如果还不行,检查num_workers是不是设太高导致内存泄漏,改成 2 或 0。
5.5 中文类别名在 Linux 下乱码
现象:Windows 上跑得好好的,放到 Linux 服务器上ImageFolder报找不到类别。原因是文件系统编码不一致。解决:把类别文件夹名改成英文(recyclable、hazardous、kitchen、other),在代码里用字典映射回中文显示。这个改动越早做越好,后期改要动数据目录和所有引用。
6. 把准确率从 88% 推到 94% 的三个具体技巧
第一个技巧是类别权重。垃圾分类数据几乎不可能完全均衡,CrossEntropyLoss支持传weight参数,把样本少的类别权重调高。计算方式是weight = 1 / count,再归一化。我试过在一个 5:1 不均衡的数据集上,加权重后少数类召回率从 0.6 提到 0.85,整体准确率涨了 3 个点。代码就一行:
counts = [len(os.listdir(f"dataset/train/{c}")) for c in train_ds.classes] weights = torch.tensor([1.0 / c for c in counts], dtype=torch.float).to(device) weights = weights / weights.sum() * len(counts) criterion = nn.CrossEntropyLoss(weight=weights)第二个技巧是测试时增强(TTA)。推理时把同一张图做几次轻微变换(原图、水平翻转、轻微裁剪),分别预测后取平均概率。这个技巧不训练、不改模型,纯推理端改动,通常能涨 1 到 2 个点。代价是推理时间翻几倍,演示场景可以接受。
第三个技巧是分层学习率。整体微调时,主干用 1e-4,新加的分类头用 1e-3,因为分类头是随机初始化的,需要更快收敛。用 PyTorch 的param_groups实现:
head_params = list(model.fc.parameters()) backbone_params = [p for n, p in model.named_parameters() if not n.startswith("fc.") and p.requires_grad] optimizer = torch.optim.Adam([ {"params": backbone_params, "lr": 1e-4}, {"params": head_params, "lr": 1e-3}, ], weight_decay=1e-4)这三个技巧我一般按顺序上:先加类别权重,再看要不要 TTA,最后调分层学习率。每加一个都重新跑一次验证集,确认涨了再保留,别一次性全加上,否则出了问题不知道是哪个引起的。
最后说个我自己的习惯:每次改完参数,把验证集准确率和对应的配置写进一个experiments.md,格式就是「日期 + 改动 + 准确率」。做了十几版之后回头看,能清楚知道哪个改动真正有用,哪个只是玄学波动。毕业设计写「实验对比」那一章时,这份记录直接就是素材。希望帮到你。
本文还有配套的精品资源,点击获取