☰
Python垃圾识别分类系统实战:从源码复现到部署避坑指南
2026/10/1 18:53:07 网站建设 项目流程

简介:一个基于Python实现的垃圾识别分类系统源码包,面向机器学习初学者和图像识别项目开发者,聚焦环保领域的垃圾分类场景。包内共28个文件,体积约1.79MB,以12个.py脚本为核心,具体包含基于CNN和MobileNet的模型定义、训练、测试以及在Windows环境下的图形界面推理代码;文档方面提供docx格式的项目计划书、详细设计文档、风险管理报告,pdf版最终报告,以及md格式的README和数据集说明,另有xml、pptx、gitignore、iml等配置文件,覆盖项目规划、数据管理、模型开发到部署演示全流程。目前已吸引336人学习浏览。这份源码完整呈现了从图像预处理、模型训练调参、准确率评估到实际分类应用的工程思路,并附有依赖清单和环境配置说明,适合对照阅读,快速上手并迁移到其他图像分类任务中,也可作为课程设计或竞赛项目的起点。

1. 一个 Python 写的垃圾识别分类系统源码:解压前先想清楚三个问题

很多朋友拿到“一个Python写的垃圾识别分类系统源码.zip”,第一反应是解压、跑 train.py、看准确率,结果两三个下午耗在版本冲突、数据集路径写死、类别顺序错乱上。这类源码技术栈并不神秘:一套以垃圾图像为对象的图像分类工程,把常见生活垃圾分类识别出来,可落地形态包括四分类、几十类细粒度分类,以及摄像头或拍照图片的实时推理。真正容易翻车的点不在模型,而在环境复现、数据落位和部署时的类别映射。适合谁?想学图像分类工程落地的,或者需要快速交付识别 demo 的。这篇直接按能复现的方式拆,先看结构和原理,再谈训练参数,最后把坑挨个踩一遍。

2. 识别链路与技术选型:为什么这种系统多半长着一张 ResNet 的脸

2.1 分类粒度决定模型复杂度:先搞清你的输出层要做几分类

垃圾识别分类系统和“目标检测”是两回事。大多数这类源码做的是图像分类:输入一张图片,输出一个类别标签,比如“可回收垃圾/厨余垃圾/有害垃圾/其他垃圾”,或者更细粒度的“塑料瓶、易拉罐、玻璃、纸箱、电池”。四分类和四十分类,对模型输出头的要求差别很大:输出通道数从 4 变成 40,最后的全连接层宽度、训练数据量、能否直接用预训练权重,都要跟着变。

拿到源码后,我第一件事是去模型定义文件里数num_classes。如果写死成 4,但你手上数据是几十类,要么改最后一层并重新训练,要么重新整理数据集。相反,如果你的落地场景只需要分“可回收/不可回收”两类,那原模型最后一层也要改。总之,分类粒度是这套系统的地基,它决定你要不要动网络结构。

在这类源码里最常见的默认配置是 ResNet 系列。原因很朴素:ResNet 在 ImageNet 上预训练权重多、PyTorch 里一句torchvision.models.resnet18(pretrained=True)就能拿到,特征提取器可以直接复用;而 ResNet 的残差结构对中小数据集特别友好,训练不容易陷入退化。多数垃圾识别数据集也就几千到几万张,用 ResNet-18 或 ResNet-50 做主干,已经是业界最稳的起点。

2.2 骨架选型:ResNet 与 MobileNet 怎么选才不后悔

骨架参数量单张 224x224 CPU 推理适合场景常见取舍
ResNet-18约 11.7M较快中小数据集、快速验证、微调准确率和速度最均衡
ResNet-50约 25.6M中等数据量大、对精度要求高显存占用高,小显存容易爆
MobileNetV3-Small约 2.5M很快嵌入式、移动端、Web 端部署精度略低,但模型体积小一个量级
EfficientNet-B0约 5.3M中等想用更小的模型拿更好的精度依赖较新的 torchvision 版本

我一般建议:如果源码默认是 ResNet-18,先不要动;如果你的最终目标是部署到树莓派或手机,再换成 MobileNetV3 并重新微调。追求“模型尽量小”而换骨架,ROI 不一定高,因为垃圾识别的难点通常不在模型容量,而在数据噪声和类别不平衡。

另外要提醒:很多新手会拿 YOLO 的思维来套“垃圾识别”,但这类标题下给的源码,绝大多数是分类网络而不是检测网络。分类网络输出的是全局类别,检测网络还要给坐标框。如果你的业务需求是“在画面里框出垃圾物体并分类”,那才需要往 YOLO 方向靠;如果只是给一张裁好的图打标签,ResNet 这条路的性价比远高于上检测模型。

2.3 数据管线:从 ImageFolder 到 Batch 的必备认知

垃圾识别源码的数据加载,十有八九是torchvision.datasets.ImageFolder。它要求数据目录按“类别文件夹”组织:

data/ train/ 可回收/ img01.jpg img02.jpg 厨余/ img03.jpg val/ 可回收/ img04.jpg 厨余/ img05.jpg

ImageFolder会按文件夹名字典序生成class_to_idx映射,比如“可回收”为 0,“厨余”为 1。这个映射在训练时看不出问题,但部署时如果不保存它,顺序错乱几乎是一定的。后续章节会专门讲这个坑。

数据加载的另外两个要点:训练集和验证集必须完全独立,不能有同一张图既在 train 又在 val;类别分布要尽量均匀,否则训练出来的模型对多数类有偏向。很多源码包里的数据本身就是散装的,需要先写脚本整理成ImageFolder结构。这一点在下一章给具体代码。

3. 把 zip 里的源码跑起来:Python 版本、虚拟环境与数据集落位

3.1 解压与初始目录检查:先别急着跑 train.py

拿到“一个Python写的垃圾识别分类系统源码.zip”后,我习惯先建一个干净的目录,再解压,绝不直接在当前目录散开。Linux 或 macOS 下用:

mkdir ~/garbage_project && cd ~/garbage_project unzip ~/Downloads/一个Python写的垃圾识别分类系统源码.zip -d ./src cd src find . -maxdepth 2 -type f | head -50

逻辑说明:-d ./src指定解压到 src 子目录,避免 zip 里的散装文件污染当前目录;find ... | head -50是看源码结构的第一道工具。先确认有没有README.md、requirements.txt、train.py、test.py、data/之类的常见文件,再决定下一步。

如果发现文件名全是乱码,比如鍨嬪彿、锟斤拷,那是 Windows 下用 GBK/CP936 压缩,解压工具默认按 UTF-8 解码导致的。先安装unzip的编码支持,再指定编码重解:

sudo apt install unzip p7zip-full cd ~/garbage_project 7z x ~/Downloads/一个Python写的垃圾识别分类系统源码.zip -o./src

如果 7z 解出来依然乱码,最省事的办法是用 Python 的zipfile做编码修正,具体代码放到第 5 章避坑部分。

3.2 虚拟环境与依赖安装:锁定 Python 和 torch 版本

给这类源码配环境时,最忌讳的是“先装最新版 Python 再 pip install”,因为 torch 官方预编译包对版本很敏感。如果你还在按网上 python 安装教程装最新版,先停一下,这不是入门手写代码,跑老源码要的是可复现环境。我的固定做法是:

conda create -n garbage python=3.9 -y conda activate garbage pip install torch==1.13.1 torchvision==0.14.1 --index-url https://download.pytorch.org/whl/cu117 pip install -r requirements.txt

参数说明:Python 3.9 是目前兼容性最稳的选择,torch 1.13.1 对应的 torchvision 是 0.14.1,能覆盖 2018 到 2023 年绝大多数开源源码写法;--index-url指定 CUDA 11.7 的预编译 wheel,避免从默认源拉到一个和你的显卡驱动不匹配的版本。如果你没有 NVIDIA 显卡,就把最后的参数去掉,直接装 CPU 版 torch。这里不建议硬上 torch 2.x,很多老源码里的torchvision.transforms接口在 2.x 下有细微行为变化,新手排错成本很高。

没有 conda 的话,用python -m venv garbage_env也是可以的,只是隔离效果略弱。装完依赖后,第一件事是跑一句python -c "import torch, torchvision; print(torch.__version__, torchvision.__version__)"验证,再跑python -c "from torchvision import models; print(models.resnet18(pretrained=True))",后者会联网下载预训练权重,能提前暴露网络代理问题,免得训练到一半才发现权重没拉下来。

3.3 数据集整理:用脚本按住类别落到 train/val 目录

很多源码包里的数据并不是规范的ImageFolder结构,而是几百张图堆在一个目录里,配一个 CSV 标注。如果你遇到的是这种情况,先别手动画文件夹,用脚本一次性搞定:

import os import shutil import pandas as pd from sklearn.model_selection import train_test_split df = pd.read_csv("labels.csv") # 列: img_path, category train_df, val_df = train_test_split(df, test_size=0.2, stratify=df["category"], random_state=42) for split, split_df in [("train", train_df), ("val", val_df)]: for _, row in split_df.iterrows(): src = row["img_path"] dst_dir = os.path.join("data", split, row["category"]) os.makedirs(dst_dir, exist_ok=True) shutil.copy(src, os.path.join(dst_dir, os.path.basename(src))) print("整理完成") print("train 类别分布:\n", train_df["category"].value_counts())

逻辑说明:train_test_split里stratify=df["category"]是按类别比例切分,保证每个类别在训练集和验证集都保持原有分布,这对垃圾这种严重不平衡的数据集非常重要;random_state=42固定随机种子,之后重跑不会变。shutil.copy是复制而不是os.rename,原图保留一份,将来改划分策略有后悔药,不会拷一次就丢源文件。

做完这步,用tree data或find data -maxdepth 2 -type d确认目录层级,再进下一步训练。这里最怕的坑是:源码里训练脚本写死了--data ./dataset/garbage,你的目录名却叫data,运行时报FileNotFoundError。第一轮建议先看参数解析器里--data的默认值,再决定是改参数还是改目录名。

4. 训练与调参:让验证准确率从 0.92 涨到 0.97 的六个旋钮

4.1 训练脚本里最该读懂的三段代码

打开源码里的train.py,不用逐行读,先定位三段:数据加载、训练循环、模型保存。数据加载决定你能不能跑通,训练循环决定改什么参数,模型保存决定你的成果会不会一夜清零。

# 1. 数据集与加载器 transform_train = transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.2, 0.2, 0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) train_dataset = datasets.ImageFolder(root="data/train", transform=transform_train) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4) # 2. 训练循环 model = models.resnet18(pretrained=True) model.fc = nn.Linear(model.fc.in_features, len(train_dataset.classes)) optimizer = torch.optim.AdamW(model.parameters(), lr=0.001, weight_decay=1e-4) criterion = nn.CrossEntropyLoss() 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() print(f"epoch {epoch+1}, loss {total_loss / len(train_loader):.4f}") # 3. 保存最优模型 torch.save({ "model_state_dict": model.state_dict(), "class_to_idx": train_dataset.class_to_idx, }, "best_model.pt")

逻辑说明:RandomResizedCrop是垃圾识别里最关键的数据增强,模拟不同拍摄距离和构图;ColorJitter模拟光线变化,因为这个场景大量图片来自手机随手拍。model.fc = nn.Linear(...)是把预训练模型的最后一层换成自己类别数的输出层,这是迁移学习的标准姿势。优化器用AdamW而不是老源码常写的SGD,能更快收敛且对大学习率不敏感,省去调 momentum 的麻烦。

这段代码里唯一需要随源码结构调整的,是模型保存方式。有些源码只存model.state_dict(),不存class_to_idx。如果你不改它,部署阶段必然要补类别映射,否则预测结果会像抽签。我在改这类源码时,第一件事就是把class_to_idx写进 checkpoint,这是血泪经验。

4.2 六个必调参数与推荐起始值

参数推荐起始值调参方向与影响
batch_size32显存不够就降到 16;准确率抖动大可以升到 64,但要注意收敛速度变慢
learning_rate0.001(AdamW)验证 loss 震荡就降一半;连续多轮不降就乘 3 再试,但别超过 0.01
weight_decay1e-4过拟合时升到 5e-4;欠拟合时降到 1e-5
epochs30配合早停,不设死;以验证集不再提升为准
schedulerCosineAnnealingLR比固定学习率稳;也可以用 StepLR 每 10 轮降一半
label_smoothing0.1类别噪声大时开启,能抑制模型过度自信

这里特别讲一下scheduler:源码里如果写的是lr = lr * 0.1 every 20 epochs,实际训练往往在最后几轮还有明显收益。换成CosineAnnealingLR(optimizer, T_max=30)后,学习率会平滑下降,后期微调更充分。改动只有三行,收益却肉眼可见,是我每次拿到这类源码必调的项之一。

关于“玄学调参”的说法:有时把学习率从 0.0001 改成 0.001,准确率反而涨 3 个点,因为 AdamW 对默认学习率的适配区间本就偏高。记住一个原则:先跑通,再调参;每轮只改一个变量,否则你根本不知道是哪个改动让准确率涨的。

4.3 类别不均衡与过拟合:两个最常见的准确率杀手

垃圾识别数据集的天然毛病:塑料瓶可能有五千张,玻璃碎片只有三百张。如果不处理,模型会把所有含糊样本都推给多数类,整体准确率看着不低,但每个类别单独看惨不忍睹。最简单的处理是给损失函数加权重:

from collections import Counter class_counts = Counter([path.split("/")[-2] for path, _ in train_dataset.samples]) total = sum(class_counts.values()) class_weights = {cls: total / count for cls, count in class_counts.items()} weights = torch.tensor([class_weights[cls] for cls in train_dataset.classes], dtype=torch.float).to(device) criterion = nn.CrossEntropyLoss(weight=weights)

逻辑说明:class_weights是“总样本数除各类别样本数”,少数类获得更高权重,损失函数里对少数类的错判会放大,梯度更新更偏向它们。train_dataset.classes的顺序和ImageFolder的class_to_idx严格一致,所以按它取权重不会错位。如果你的数据集类别极度不均衡,比如最大类是最小类的 20 倍以上,建议再叠加 Focal Loss,但那是下一步的优化,先用权重法跑一轮看效果。

过拟合的典型现象:训练 loss 一直在降,验证准确率却卡住甚至下跌。优先检查数据增强够不够强,然后在模型里加 dropout 或降低骨架复杂度。很多源码默认用 ResNet-50 且不加任何正则,几千张图很容易过拟合;换成 ResNet-18、打开RandomHorizontalFlip,验证集反而上涨两三个点。

4.4 验证不止看准确率:混淆矩阵与置信度阈值

训练结束后不要只看val_accuracy。垃圾识别场景里,类别越细粒度,混淆越集中在相近类别,比如“可乐罐”和“易拉罐”在模型看来几乎长一样。我习惯保留一段验证脚本,输出混淆矩阵:

import torch, numpy as np from sklearn.metrics import confusion_matrix model.eval() y_true, y_pred = [], [] with torch.no_grad(): for images, labels in val_loader: outputs = model(images.to(device)) preds = outputs.argmax(dim=1).cpu().numpy() y_pred.extend(preds) y_true.extend(labels.numpy()) cm = confusion_matrix(y_true, y_pred) np.set_printoptions(linewidth=150) print(cm)

逻辑说明:混淆矩阵能看出哪些类互相打架,比如“纸箱”总被预测成“书本”,那说明这两个类别的样本外形太接近,或者训练数据里两个类风格差异过大。解决方向不是调参,而是去检查数据质量和标注一致性。只看一个总准确率,很容易在这种局部问题上自我感觉良好,部署到真实场景才知道差距。另外,如果模型对某些类别的置信度普遍偏低,可以在部署阶段把置信度阈值设低一点,比如 0.4 以下直接返回“不确定”,这种保守策略在垃圾投放场景里比硬猜强得多。

5. 五个高频坑:从 zip 伪加密到 CUDA out of memory 的排障记录

5.1 zip 解压乱码与伪加密:文件名全是“锟斤拷”

现象:解压后目录结构正常,但所有中文文件名和文件夹名变成乱码,甚至有些文件提示需要密码但密码栏一空就能解出来。

原因:Windows 的压缩工具默认用 CP936(GBK)记录文件名,而 Linux 的 unzip 按 UTF-8 解码,就会产生“锟斤拷”;“伪加密”则是 zip 格式里通用位标记被置为 1,实际数据没有加密,但解压器误以为需要密码。

解决:先试unzip -O CP936,如果 unzip 版本不支持-O参数,用下面的 Python 脚本解压并强制修正编码:

import zipfile def fix_bad_filename(name: str) -> str: try: return name.encode("cp437").decode("gbk") except (UnicodeDecodeError, UnicodeEncodeError): return name with zipfile.ZipFile("一个Python写的垃圾识别分类系统源码.zip") as zf: for info in zf.infolist(): info.filename = fix_bad_filename(info.filename) # 清除伪加密标记 info.flag_bits &= ~0x1 zf.extract(info, "src_fixed")

逻辑说明:zip 规范里用 CP437 编码存储非 UTF-8 文件名,所以先把原始字节还原成 CP437,再转成 GBK,就能得到正确中文名;info.flag_bits &= ~0x1是强行把“需要密码”位清零,如果文件数据本身没加密,这样操作后直接解出。

5.2 AttributeError: 'NoneType' object has no attribute 'shape'

现象:训练脚本跑第一个 epoch,到某张图时报错,说返回了 None,无法取 shape。

原因:多数是图片路径里有中文或空格,PIL 在部分版本下打不开这种路径,返回 None;另一可能是这张图本身损坏,是 0 字节文件。

解决:先在数据整理阶段做一次完整校验,过滤坏图:

from PIL import Image import os for root, _, files in os.walk("data"): for name in files: if not name.lower().endswith((".jpg", ".jpeg", ".png")): continue path = os.path.join(root, name) try: img = Image.open(path) img.verify() except Exception: print("bad image:", path) os.remove(path) # 或移到 corrupted/ 目录

逻辑说明:Image.verify()只读文件头不做完整解码,速度快,能发现大部分损坏;跑完后还要检查路径里是否有中文,有的话统一改成拼音或英文目录,这是最省心的办法而不是去改 PIL 的编码行为。

5.3 CUDA out of memory:小显存选手的求生指南

现象:训练脚本一启动就报CUDA out of memory,或跑了几个 epoch 后在随机位置崩掉;显存小的用户甚至会在加载 torchvision 预训练模型时就报错。

原因:默认batch_size和num_workers是按大显存机器写的;另外DataLoader的pin_memory=True会把内存锁页,显存压力更大。

解决:优先把batch_size调到 8 或 16,再看模型是不是 ResNet-50 换成 ResNet-18;同时给训练循环包上显存清理逻辑:

try: loss.backward() optimizer.step() except RuntimeError as e: if "out of memory" in str(e): torch.cuda.empty_cache() optimizer.zero_grad() print("CUDA OOM skipped this batch") else: raise e

逻辑说明:OOM 时loss.backward()留下的计算图会占显存,torch.cuda.empty_cache()能主动释放缓存块。跳过当前 batch 而不是整体退出,保住了已经跑完的训练进度。但这种做法只是应急,长期还是要把batch_size降下来并调小num_workers。

5.4 训练 loss 降不下去:0.69 卡住的背后

现象:loss 卡在 0.69 附近,几十轮不动,验证准确率在随机水平附近晃。

原因:0.69 约等于-ln(0.5),常见于二分类任务里模型只会输出 50% 概率,也就是完全没学到有效特征。通常有两个来源:一是学习率设置过大导致梯度震荡,二是类别严重不均衡时模型学会了“永远猜多数类”。

解决:先把学习率降到 1e-4,用AdamW替代源码里的SGD;再把CrossEntropyLoss的weight参数设成类别权重;最后检查DataLoader是否加了shuffle=True,没加的话每个 epoch 的样本顺序固定,模型更容易陷入局部最优。这三步走完,绝大多数 loss 卡住的情况都会松绑。

5.5 验证集准确率 95%,一部署就类别错乱

现象:本地验证准确率很高,拿到新环境部署后,预测结果和真实类别对不上,比如“电池”总是被识别成“果皮”。

原因:训练时ImageFolder按字典序生成class_to_idx,比如“可回收”是 0,“果皮”是 1;而部署脚本里如果自己重新加载了一遍数据集或按不同顺序硬编码类别列表,索引和类名的对应关系就错位了。

解决:训练结束后立即把映射落盘,部署端只认这份映射:

import json with open("class_to_idx.json", "w", encoding="utf-8") as f: json.dump(train_dataset.class_to_idx, f, ensure_ascii=False, indent=2) print("类别映射:", train_dataset.class_to_idx)

部署时读取模型输出索引,再反向查这个json得到类别名。要特别提醒:不要在部署端用os.listdir("data/train")重新生成类别列表,排序方式一变,错位就发生了。模型权重本身是对的,类别名映射错了,预测结果自然风马牛不相及。

6. 落地收尾:模型做小、导成 ONNX、再包一个 Web 服务

6.1 把模型导出为 ONNX:部署的第一步

训练完的.pt文件依赖 PyTorch 环境,不适合直接交给做后端或嵌入式的人。我一般把模型转成 ONNX,推理时用onnxruntime,不仅摆脱了 torch 运行时,还能小幅提速。

import torch import torchvision.models as models model = models.resnet18(pretrained=False) model.fc = torch.nn.Linear(model.fc.in_features, 4) checkpoint = torch.load("best_model.pt", map_location="cpu") model.load_state_dict(checkpoint["model_state_dict"]) model.eval() dummy = torch.randn(1, 3, 224, 224) torch.onnx.export(model, dummy, "garbage_model.onnx", input_names=["input"], output_names=["output"], opset_version=11, dynamic_axes={"input": {0: "batch"}})

参数说明:opset_version=11是兼容性最好的版本,部署端不管用 ONNX Runtime 还是 OpenVINO 都能解析;dynamic_axes声明 batch 维度可变,这样单张图片或多张图片都能推理,不用重新导出。

6.2 用 Flask 包一个最小推理接口

导出完成后写一个极简的app.py,用onnxruntime加载模型,读class_to_idx.json做索引反转,上传图片返回类别和置信度:

import json import onnxruntime as ort import numpy as np from PIL import Image from flask import Flask, request, jsonify app = Flask(__name__) session = ort.InferenceSession("garbage_model.onnx", providers=["CPUExecutionProvider"]) with open("class_to_idx.json", encoding="utf-8") as f: idx_to_class = {v: k for k, v in json.load(f).items()} @app.route("/predict", methods=["POST"]) def predict(): img = Image.open(request.files["image"]).convert("RGB").resize((224, 224)) arr = np.asarray(img, dtype=np.float32) / 255.0 arr = (arr - np.array([0.485, 0.456, 0.406])) / np.array([0.229, 0.224, 0.225]) arr = arr.transpose(2, 0, 1)[None].astype(np.float32) probs = session.run(None, {"input": arr})[0][0] idx = int(np.argmax(probs)) return jsonify({"class": idx_to_class[idx], "confidence": float(probs[idx])})

逻辑说明:这个接口只做最小能跑版本;providers=["CPUExecutionProvider"]明确用 CPU,避免服务器上没有 CUDA 时报错。部署时不要用 Flask 自带的开发服务器扛压力,换 gunicorn 或多进程即可。我现在拿到这类源码的习惯是:先看类别映射,再看数据集落位,最后才碰训练参数;把这套 ONNX+json 的链路提前搭好,交付出去才不用隔三差五救火。希望这个拆解能帮到你少走弯路。

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

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

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

立即咨询