简介:这份资源是面向计算机、通信、人工智能、自动化等专业学生与从业者的智能垃圾分类系统完整项目源码,基于深度学习卷积神经网络实现,可作为毕业设计、课程大作业或期末课程设计的参考方案。项目为个人毕设成果,答辩评审分达98分,代码经过调试测试,确保可运行,适合小白入门学习,也便于基础较好的读者在此基础上修改调整、扩展功能。压缩包共216个文件,约17.29MB,包含Python脚本、Java源码、XML配置、PNG与JPG图像样本、模型检查点与权重数据文件,以及Gradle、Maven构建脚本和说明文档,覆盖数据处理、模型训练与界面交互等模块。目前已有183人学习关注。读者可从中获取完整的卷积神经网络垃圾分类实现思路、可复现的工程目录结构、模型权重与训练检查点,以及项目说明文档,便于快速理解项目全貌并开展二次开发。
1. 从一张宿舍楼下的垃圾桶照片说起:这套智能垃圾分类系统到底解决什么问题
去年帮学弟看毕业设计,他选题是「基于深度学习卷积神经网络的智能垃圾分类系统」,代码跑通了,答辩前一周却慌了——老师问「你这模型在真实照片上准确率多少」,他答不上来,因为训练集里全是白底单物体的标准图,一换成宿舍楼下那种堆叠、遮挡、光线昏暗的实拍图,模型直接崩。这不是个例,是绝大多数 Python 毕业设计里 CNN 项目的通病:实验室指标漂亮,落地场景翻车。
这套系统的核心其实就三件事:用卷积神经网络做图像特征提取与分类、用 Python 把训练和推理串起来、最后包一个能演示的界面。它适合三类人:正在做计算机/电子信息方向毕业设计的学生、想入门深度学习但缺一个完整项目练手的初学者、以及需要快速搭一个图像分类 demo 的开发者。读完你能拿到一条从数据准备、模型搭建、训练调参到界面集成的完整路径,知道每一步参数为什么这么设、哪里最容易出问题。热词里「深度学习入门」「卷积神经网络原理」「python深度学习教程」这些搜索意图,本质都是想找一个能跑通、能改、能讲清楚的项目,这篇就按这个标准写。
2. 卷积神经网络凭什么能认垃圾:从一张 224×224 的图到四个类别
2.1 为什么垃圾分类天然适合 CNN,而不是全连接网络
垃圾图像分类的本质是「从像素里找可区分的纹理和形状模式」——塑料瓶有反光的高光带和圆柱轮廓,纸板有纤维纹理和折痕,金属罐有镜面反射和拉环结构,玻璃瓶有透明边缘和折射。这些特征有强烈的局部性和平移不变性:一个瓶盖出现在图片左上角还是右下角,不影响它是塑料这个判断。
全连接网络把图像拉成一维向量,等于把空间结构彻底打散,相邻像素的关系丢失,参数量还爆炸——一张 224×224×3 的图接一个 512 维隐层,光这一层就是 7700 万参数。CNN 用两个机制解决这个问题:局部感受野让每个神经元只看一小块区域,权值共享让同一个卷积核在整张图上滑动。一个 3×3 的卷积核只有 9 个权重,却能扫描全图,参数量直接降几个数量级。
卷积层之后接池化层做下采样,把特征图尺寸逐步缩小、通道数逐步增大,浅层学边缘和颜色,深层学部件和整体形状。最后接全连接层或全局平均池化做分类。这套结构对垃圾这种「类内差异大、类间差异有时小」的任务,比传统 HOG+SVM 方案鲁棒得多,尤其是光照和角度变化时。
2.2 用 PyTorch 搭一个能跑通的四分类 CNN
下面是一个最小可用的 CNN 定义,输入 224×224 三通道,输出四类(可回收、厨余、有害、其他)。我一般用 PyTorch,因为调试直观、报错清晰,对毕业设计来说改结构也方便。
import torch import torch.nn as nn class GarbageCNN(nn.Module): def __init__(self, num_classes=4): super().__init__() # 特征提取:3 个卷积块,通道 32->64->128 self.features = nn.Sequential( # 块1: 224x224 -> 112x112 nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), # 加速收敛,缓解内部协变量偏移 nn.ReLU(inplace=True), nn.MaxPool2d(2), # 块2: 112x112 -> 56x56 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 块3: 56x56 -> 28x28 nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2), ) # 分类头:全局平均池化替代大全连接,减少过拟合 self.classifier = nn.Sequential( nn.AdaptiveAvgPool2d(1), # 128x28x28 -> 128x1x1 nn.Flatten(), nn.Dropout(0.5), # 训练时随机丢弃,防过拟合 nn.Linear(128, num_classes) ) def forward(self, x): x = self.features(x) x = self.classifier(x) return x if __name__ == "__main__": model = GarbageCNN(num_classes=4) dummy = torch.randn(8, 3, 224, 224) # 模拟一个 batch out = model(dummy) print(out.shape) # 期望 torch.Size([8, 4])逻辑说明:features负责逐层提取空间特征,每个卷积块是「卷积 + BN + ReLU + 池化」的标准组合。BN 放在卷积后、激活前是常见做法,能让训练更稳、学习率可以设大一点。classifier用AdaptiveAvgPool2d(1)把每个通道压成一个数,再接 Dropout 和线性层,这样参数量小、对输入尺寸也不敏感。
参数说明:kernel_size=3, padding=1保证卷积后空间尺寸不变,尺寸缩小全靠池化,这样每层输出尺寸好推算。通道数 32→64→128 是逐层翻倍的经典节奏,显存不够可以整体减半。Dropout(0.5)是分类头常用的丢弃率,数据量小可以调到 0.3,数据量大可以不用。num_classes=4要和你数据集的实际类别数一致,改类别数时只改这一个参数。
2.3 数据准备:从原始图片到 DataLoader 的完整链路
模型定义只是骨架,真正决定成败的是数据。毕业设计常见的数据组织方式是每个类别一个文件夹,用ImageFolder直接读。
from torchvision import datasets, transforms from torch.utils.data import DataLoader, random_split # 训练集做增强,验证集只做缩放和归一化 train_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转 transforms.RandomRotation(15), # 随机旋转 ±15 度 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]) ]) # 假设数据目录结构: data/train/可回收/*.jpg 等 full_ds = datasets.ImageFolder("data/train", transform=train_tf) n_val = int(len(full_ds) * 0.2) n_train = len(full_ds) - n_val train_ds, val_ds = random_split(full_ds, [n_train, n_val]) val_ds.dataset.transform = val_tf # 验证集换用无增强的 transform 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) print(f"训练样本 {n_train},验证样本 {n_val},类别 {full_ds.classes}")逻辑说明:训练集用增强是为了让模型见到更多变化,缓解过拟合;验证集绝不能加随机增强,否则每次评估结果都在抖,指标不可信。Normalize用的均值方差是 ImageNet 的统计值,如果你从零训练可以用自己数据集的统计值,但用预训练权重时必须保持一致。
参数说明:batch_size=32是显存和训练稳定性的折中,显存小就降到 16 或 8,同时把学习率按比例调小。num_workers=4在 Windows 上如果报错就改成 0。RandomRotation(15)的 15 度对垃圾图像够用,转太多会让瓶罐倒置,反而不符合真实场景。
3. 训练、调参与评估:让模型在真实照片上也能打
3.1 训练循环与学习率调度
训练循环本身不复杂,关键是损失函数、优化器和调度器的搭配。分类任务用交叉熵,优化器用 AdamW,学习率用余弦退火。
import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = GarbageCNN(num_classes=4).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=30) # 30 个 epoch 内余弦下降 def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total = 0.0, 0, 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() * imgs.size(0) correct += (outputs.argmax(1) == labels).sum().item() total += imgs.size(0) return total_loss / total, correct / total @torch.no_grad() def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total = 0.0, 0, 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) outputs = model(imgs) loss = criterion(outputs, labels) total_loss += loss.item() * imgs.size(0) correct += (outputs.argmax(1) == labels).sum().item() total += imgs.size(0) return total_loss / total, correct / total best_acc = 0.0 for epoch in range(30): tr_loss, tr_acc = train_one_epoch(model, train_loader, optimizer, criterion, device) val_loss, val_acc = evaluate(model, val_loader, criterion, device) scheduler.step() if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), "best_garbage_cnn.pth") # 只存最优 print(f"Epoch {epoch+1:02d} | train {tr_loss:.4f}/{tr_acc:.3f} | val {val_loss:.4f}/{val_acc:.3f}")逻辑说明:每个 epoch 先训练后验证,验证用torch.no_grad()关闭梯度节省显存。保存策略是「只存验证集最优」,避免最后一个 epoch 过拟合反而变差。scheduler.step()放在 epoch 末尾,让学习率按余弦曲线从 1e-3 平滑降到接近 0。
参数说明:lr=1e-3是 AdamW 的常用起点,如果 loss 震荡就降到 3e-4。weight_decay=1e-4是 L2 正则,数据量小可以加到 1e-3。T_max=30要和总 epoch 数一致,否则学习率曲线对不上。如果验证准确率卡在某个值不动,先看是不是学习率太大,再看数据增强是不是过猛。
3.2 评估不能只看准确率:混淆矩阵和每类指标
准确率在类别不平衡时会骗人。如果「其他垃圾」占了 60%,模型全猜这一类也有 60% 准确率。必须看混淆矩阵和每类的精确率、召回率。
from sklearn.metrics import confusion_matrix, classification_report import numpy as np @torch.no_grad() def get_all_preds(model, loader, device): model.eval() all_preds, all_labels = [], [] for imgs, labels in loader: imgs = imgs.to(device) outputs = model(imgs) all_preds.extend(outputs.argmax(1).cpu().numpy()) all_labels.extend(labels.numpy()) return np.array(all_preds), np.array(all_labels) preds, labels = get_all_preds(model, val_loader, device) print(confusion_matrix(labels, preds)) print(classification_report(labels, preds, target_names=full_ds.classes, digits=3))逻辑说明:混淆矩阵的行是真实类别、列是预测类别,对角线是分对的,非对角线能看出哪两类容易混。classification_report给出每类的 precision、recall、f1-score,比一个总准确率信息量大得多。
参数说明:target_names要按ImageFolder的类别顺序传,顺序错了报告就张冠李戴。如果发现「玻璃」和「塑料」互相误判严重,说明这两类的视觉特征太接近,要么补数据,要么在增强里加更强的颜色扰动逼模型学形状而不是颜色。
3.3 迁移学习:小数据集上把准确率拉起来的最快手段
毕业设计的数据集通常每类只有几百张,从零训练很难到 90% 以上。用 ImageNet 预训练的 ResNet18 做迁移学习,通常能把验证准确率直接拉到 95% 左右。
import torchvision.models as 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 model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) return model model = build_resnet18(num_classes=4, freeze_backbone=True).to(device) # 冻结时只优化 fc 层,学习率可以大一点 optimizer = optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3)逻辑说明:weights=...IMAGENET1K_V1加载预训练权重,freeze_backbone=True时只训练新换的分类头,训练快、不容易过拟合。等分类头收敛后,可以解冻最后几个 block 做微调,学习率降到 1e-4。
参数说明:filter(lambda p: p.requires_grad, ...)只把需要梯度的参数交给优化器,冻结的参数不更新。如果显存够、数据量也还行,可以一开始就不冻结,用 1e-4 的学习率整体微调,效果通常更好但更慢。
4. 从模型到能演示的系统:推理封装、界面与部署
4.1 推理函数:单张图片到类别和置信度
训练完要能对任意一张图给出结果,这个函数是界面调用的核心。
from PIL import Image CLASS_NAMES = ["可回收", "厨余", "有害", "其他"] def predict_image(model, image_path, device, class_names=CLASS_NAMES): model.eval() 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]) ]) img = Image.open(image_path).convert("RGB") # 强制三通道,防灰度图报错 tensor = tf(img).unsqueeze(0).to(device) # 加 batch 维度 with torch.no_grad(): logits = model(tensor) probs = torch.softmax(logits, dim=1)[0] conf, idx = probs.max(0) return class_names[idx.item()], conf.item(), probs.cpu().numpy() label, conf, probs = predict_image(model, "test.jpg", device) print(f"预测: {label}, 置信度: {conf:.3f}")逻辑说明:convert("RGB")是关键一步,用户上传的图可能是灰度或带透明通道,不转会在ToTensor后通道数不对。unsqueeze(0)补上 batch 维度,因为模型期望输入是[N, C, H, W]。softmax把 logits 转成概率,方便展示置信度。
参数说明:class_names的顺序必须和训练时ImageFolder的类别顺序完全一致,否则标签全错。置信度低于某个阈值(比如 0.6)时,界面上最好提示「不确定」,而不是硬给一个答案。
4.2 用 Gradio 快速搭一个可交互界面
毕业设计答辩需要一个能现场演示的界面,Gradio 几行代码就能搞定,比 PyQt 省事得多。
import gradio as gr def classify(img): if img is None: return "请上传图片", {} 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]) ]) tensor = tf(img.convert("RGB")).unsqueeze(0).to(device) model.eval() with torch.no_grad(): probs = torch.softmax(model(tensor), dim=1)[0].cpu().numpy() return {CLASS_NAMES[i]: float(probs[i]) for i in range(len(CLASS_NAMES))} demo = gr.Interface( fn=classify, inputs=gr.Image(type="pil"), outputs=gr.Label(num_top_classes=4), title="智能垃圾分类系统", description="上传一张垃圾图片,模型会给出四个类别的概率" ) demo.launch(server_name="0.0.0.0", server_port=7860)逻辑说明:gr.Image(type="pil")直接把上传的图转成 PIL 对象,省去自己处理文件流。gr.Label输出概率条,答辩时视觉效果直观。server_name="0.0.0.0"让局域网内其他设备也能访问,方便老师用手机现场测试。
参数说明:num_top_classes=4显示全部四类概率。如果部署在服务器上,server_port要选一个没被占用的端口。模型加载要在demo.launch()之前完成,避免每次请求都重新加载权重。
4.3 模型导出与轻量化:让系统能在没有 GPU 的机器上跑
答辩教室的电脑不一定有 GPU,把模型导出成 ONNX 或做动态量化,CPU 上也能跑到实时。
# 方式一:导出 ONNX dummy = torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy, "garbage_cnn.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, opset_version=12 ) # 方式二:动态量化(仅 CPU) quantized = torch.quantization.quantize_dynamic( model.cpu(), {nn.Linear}, dtype=torch.qint8 ) torch.save(quantized.state_dict(), "garbage_cnn_quantized.pth")逻辑说明:ONNX 导出后可以用 onnxruntime 推理,跨平台、不依赖 PyTorch。动态量化把线性层权重从 float32 压到 int8,模型体积约缩到四分之一,CPU 推理速度提升明显,精度损失通常在一个百分点以内。
参数说明:opset_version=12兼容性较好,太新的版本某些推理引擎不支持。dynamic_axes让 batch 维度可变,方便一次处理多张图。量化只对nn.Linear做,卷积层量化需要静态量化流程,复杂一些,毕业设计用动态量化足够。
5. 避坑与排查:那些让答辩翻车的细节
5.1 训练准确率 99%,验证准确率 60%
现象:训练集上几乎全对,验证集惨不忍睹,loss 曲线一个往下走一个往上翘。原因:典型过拟合,数据量太小或增强太弱,模型把训练样本背下来了。解决:先加数据增强(翻转、旋转、颜色扰动),再加 Dropout 和 weight_decay,还不行就换迁移学习冻结主干,最后才考虑减模型容量。
5.2 换了台电脑就报「CUDA out of memory」
现象:在自己电脑上跑得好好的,换到答辩教室或同学机器上直接显存溢出。原因:batch_size是按自己显卡设的,别人显存更小;或者忘了model.to(device)导致部分张量还在 CPU。解决:把batch_size做成可配置,启动时根据torch.cuda.get_device_properties(0).total_memory自动降档;所有输入和模型统一.to(device),推理时用torch.no_grad()。
5.3 预测结果永远是同一类
现象:不管传什么图,输出都是「其他」,置信度还很高。原因:最常见的是类别顺序错位——训练时ImageFolder按文件夹名字母序排,推理时CLASS_NAMES手写的顺序对不上。其次是归一化参数不一致,训练用了 ImageNet 均值方差,推理忘了加。解决:打印full_ds.classes确认顺序,把它直接存成 json 供推理读取;归一化 transform 抽成一个函数,训练和推理共用。
5.4 中文路径导致图片读不进来
现象:Image.open报FileNotFoundError或乱码,路径里带中文。原因:某些库在 Windows 下对非 ASCII 路径处理有问题。解决:读文件用open(path, 'rb')再交给 PIL,或者统一把数据集路径改成英文。这个坑血泪经验,答辩前一天才发现数据集读不全,后悔药都没得吃。
5.5 界面卡死或每次预测都很慢
现象:点一次分类要等好几秒,连续点几次界面无响应。原因:每次请求都重新加载模型或重新构建 transform。解决:模型和 transform 在全局初始化一次,推理函数只做前向;如果还是慢,检查是不是跑在 CPU 上而模型没量化,或者图片分辨率被意外放大。
6. 把准确率再往上推一把:几个我实际用过的技巧
模型能跑通只是及格线,答辩想拿高分、或者你真想把这套东西用到实际场景,还得在细节上抠。下面几个技巧是我在多个图像分类项目里反复验证过的,按投入产出比排序。
第一,用 TTA(测试时增强)白捡一两个点。推理时对同一张图做原图、水平翻转、轻微缩放三个版本,分别预测后把概率平均。代码上就是把predict_image包一层循环,成本是推理时间翻三倍,但准确率通常能涨 1~2 个百分点,答辩演示时很稳。
def predict_with_tta(model, img, device, n=3): tf_list = [ transforms.Compose([transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])]), transforms.Compose([transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=1.0), transforms.ToTensor(), transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])]), transforms.Compose([transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])]), ] probs = [] model.eval() with torch.no_grad(): for tf in tf_list[:n]: t = tf(img.convert("RGB")).unsqueeze(0).to(device) probs.append(torch.softmax(model(t), dim=1)[0].cpu().numpy()) return np.mean(probs, axis=0)第二,处理类别不平衡用加权损失或重采样。如果「有害垃圾」样本特别少,模型会倾向忽略它。在CrossEntropyLoss里传weight参数,权重按类别频率的倒数设,或者用WeightedRandomSampler让每个 batch 里各类别比例均衡。前者改一行代码,后者要改 DataLoader,效果都不错。
第三,把验证集做成「真实场景集」。这是我最想强调的一点。训练和验证都从同一个分布里切,指标再高也不代表真实可用。单独收集一批手机实拍、带背景、有遮挡的图做测试集,哪怕只有几十张,也能暴露真问题。我见过太多项目在标准验证集上 98%,在实拍测试集上不到 70%,差距全在数据分布上。
第四,模型集成是最后的保险。训一个 ResNet18 和一个自己搭的 CNN,推理时把两者 softmax 概率平均。两个模型结构不同、犯错的地方往往不一样,集成后准确率通常比单模型高 2~3 个点。代价是训练和推理成本翻倍,但对毕业设计这种一次性投入的场景,完全值得。
最后说个习惯:每次改完超参数或数据增强,都把验证集准确率、混淆矩阵、几个典型错例截图存到一个experiments/目录里,文件名带上日期和改动点。答辩前翻一遍,你能清楚说出每一步为什么这么调,而不是「试出来的」。这套系统本身不难,难的是把每个环节的因果关系讲明白,希望帮到你。
本文还有配套的精品资源,点击获取