news 2026/10/24 15:30:22

基于Python的智能垃圾分类系统:从数据清洗到Docker部署全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Python的智能垃圾分类系统:从数据清洗到Docker部署全流程

简介:这份资源是一套基于Python开发的智能垃圾分类系统完整源码与部署指南,源自本科毕业设计课题,适合计算机视觉入门者、毕业设计选题学生及需要期末大作业参考的学习者。系统通过卷积神经网络与迁移学习实现可回收物、厨余垃圾、有害垃圾及其他垃圾四大类别的识别分类,涵盖图像采集、数据预处理、模型推理与可视化界面等模块。压缩包共24个文件,约55.76MB,包含7个py源码文件、7个zip数据集与备份包、3个ui界面文件、3个zbak备份文件,以及cpp、jpg、md、png等辅助资料,代码采用模块化设计并配有详尽注释。资源内附环境配置、依赖库清单、模型训练与部署流程说明,便于快速复现与二次开发。目前已有57人学习,可为教学研究和技术开发提供完整参考。

1. 从一堆垃圾到一条数据:智能垃圾分类系统到底在做什么

小区楼下四个桶,厨余、可回收、有害、其他,督导员站在旁边盯着你手里的塑料袋。这个场景催生了一个很实际的需求:能不能让摄像头或一张照片自动判断垃圾类别,把结果推给投放口或者小程序?基于 Python 的智能垃圾分类系统,本质就是一套「图像输入 → 类别输出 → 部署上线」的完整链路,核心是图像分类模型,外围是数据采集、推理服务、前端交互和部署运维。它适合两类人:一类是想拿一个完整项目练手 Python 工程化的开发者,另一类是想在社区、园区、学校做小规模试点的实施者。源码和部署指南这两个词之所以被反复搜,是因为大多数人卡在「模型跑通了但服务起不来」这一步。下面按数据、模型、服务、部署、排错的顺序,把这条链路拆开讲清楚。

2. 数据从哪来:垃圾分类数据集的采集与清洗

2.1 公开数据集与自采数据的取舍

做垃圾分类,第一道坎不是模型,是数据。公开数据集里常见的是 TrashNet 这类,大约两千多张,分六类:纸板、玻璃、金属、纸、塑料、其他。它的优点是干净、标注规范,缺点是背景单一,全是白底摆拍,直接拿去识别真实场景里的垃圾袋会翻车。我的做法是公开数据集打底,再自采补充。自采渠道有三个:手机在小区投放点拍、从监控视频抽帧、网上爬取带标注的图片。自采数据一定要覆盖真实光照和遮挡,否则模型上线后准确率会掉得很难看。

数据量上,每类至少准备 500 张起步,四分类(厨余、可回收、有害、其他)建议每类 800 到 1500 张。类别不均衡是常态,厨余和其他往往最多,有害垃圾最少。处理不均衡有两个办法:一是对少数类做数据增强,二是训练时用类别权重。我一般两个一起上。

2.2 用 Python 做数据清洗和增强的实操

清洗的核心是去重、去模糊、修正错标。下面这段脚本做三件事:用感知哈希找重复图、用拉普拉斯方差筛模糊图、按类别统计数量。

import os import cv2 import imagehash from PIL import Image from collections import defaultdict DATA_DIR = "dataset" # 结构:dataset/类别名/图片 def find_duplicates(root, hash_size=8): """用感知哈希找出重复或高度相似的图片""" hashes = defaultdict(list) for cls in os.listdir(root): cls_dir = os.path.join(root, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): path = os.path.join(cls_dir, fname) try: h = imagehash.phash(Image.open(path), hash_size=hash_size) hashes[str(h)].append(path) except Exception as e: print(f"读取失败 {path}: {e}") # 同一哈希下多于一张,视为重复 return {k: v for k, v in hashes.items() if len(v) > 1} def find_blurry(root, threshold=100.0): """拉普拉斯方差低于阈值判为模糊""" blurry = [] for cls in os.listdir(root): cls_dir = os.path.join(root, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): path = os.path.join(cls_dir, fname) img = cv2.imread(path) if img is None: continue gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) score = cv2.Laplacian(gray, cv2.CV_64F).var() if score < threshold: blurry.append((path, round(score, 2))) return blurry def count_by_class(root): stat = {} for cls in os.listdir(root): cls_dir = os.path.join(root, cls) if os.path.isdir(cls_dir): stat[cls] = len(os.listdir(cls_dir)) return stat if __name__ == "__main__": print("重复图:", find_duplicates(DATA_DIR)) print("模糊图:", find_blurry(DATA_DIR)) print("类别分布:", count_by_class(DATA_DIR))

逻辑说明:imagehash.phash把图片压成一个 64 位指纹,内容相近的图指纹接近,适合找翻拍和重复。cv2.Laplacian的方差反映图像边缘强度,方差越低越模糊,阈值 100 是经验值,手机拍摄的垃圾图可以放宽到 60。参数说明:hash_size越大越敏感,8 是通用值;threshold要根据你的拍摄设备调,先用一批人工确认的清晰图跑一遍,取最低分的 80% 作为阈值。

增强用torchvision.transforms或albumentations都行。垃圾分类场景我推荐albumentations,因为它对随机裁剪、旋转、亮度调整的组合更灵活。注意增强只加在训练集,验证集和测试集保持原样,否则评估结果会虚高。

提示:清洗完一定要人工抽检 50 张,机器筛出来的模糊图里往往混着正常图,直接删会丢样本。

3. 模型怎么选:从 MobileNet 到迁移学习的落地路径

3.1 为什么垃圾分类不适合从零训练

垃圾分类的类别少、类间差异大(玻璃瓶和纸板差别明显),但类内差异也大(不同形状的塑料瓶)。从零训练一个 CNN 需要几十万张图才能收敛,个人和小团队没有这个数据量。迁移学习是标准答案:拿 ImageNet 上预训练好的骨干网络,换掉最后的全连接层,用你的数据微调。骨干网络选型看部署环境:服务器端可以用 ResNet50 或 EfficientNet-B3,边缘设备或手机端用 MobileNetV3 或 ShuffleNetV2。我一般先用 MobileNetV3-Small 跑通全流程,准确率不够再换大的。

3.2 用 PyTorch 微调 MobileNetV3 的完整代码

下面这段是训练脚本的核心部分,包含数据加载、模型改造、训练循环和验证。

import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms, models DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") BATCH_SIZE = 32 EPOCHS = 20 LR = 1e-3 NUM_CLASSES = 4 # 厨余、可回收、有害、其他 # 训练集增强,验证集只做缩放和归一化 train_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) val_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) train_ds = datasets.ImageFolder("dataset/train", transform=train_tf) val_ds = datasets.ImageFolder("dataset/val", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=4) # 加载预训练模型,替换分类头 model = models.mobilenet_v3_small(weights=models.MobileNet_V3_Small_Weights.DEFAULT) model.classifier[3] = nn.Linear(model.classifier[3].in_features, NUM_CLASSES) model = model.to(DEVICE) # 类别不均衡时给少数类更高权重 class_weights = torch.tensor([1.0, 1.0, 2.0, 1.0]).to(DEVICE) criterion = nn.CrossEntropyLoss(weight=class_weights) optimizer = torch.optim.AdamW(model.parameters(), lr=LR, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS) for epoch in range(EPOCHS): model.train() running_loss = 0.0 for imgs, labels in train_loader: imgs, labels = imgs.to(DEVICE), labels.to(DEVICE) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() scheduler.step() # 验证 model.eval() correct, total = 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(DEVICE), labels.to(DEVICE) outputs = model(imgs) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) acc = correct / total print(f"Epoch {epoch+1}/{EPOCHS} loss={running_loss/len(train_loader):.4f} val_acc={acc:.4f}") torch.save(model.state_dict(), "garbage_mobilenetv3.pth")

逻辑说明:models.mobilenet_v3_small的classifier是一个 Sequential,索引 3 是最后的 Linear 层,替换成你的类别数。class_weights里给有害垃圾(假设索引 2)加了 2 倍权重,缓解样本少的问题。CosineAnnealingLR让学习率按余弦曲线下降,比固定学习率更容易收敛到好的局部最优。参数说明:BATCH_SIZE在显存够的情况下越大越稳,8G 显存跑 224 分辨率可以到 64;LR用 1e-3 是微调的常用起点,如果 loss 震荡就降到 3e-4;EPOCHS看验证准确率,一般 15 到 30 轮就饱和。

训练完看混淆矩阵,如果厨余和其他经常混,说明这两类在视觉上确实接近,需要补充更多区分度高的样本,或者考虑加一个二级分类器。

4. 服务怎么起:用 FastAPI 把模型包成接口

4.1 推理服务的接口设计

模型训练完只是半成品,要能用起来必须包成 HTTP 接口。选 FastAPI 的理由是异步支持好、自带文档、部署简单。接口设计上,一个/predict接收图片返回类别和置信度,一个/health做健康检查。图片传输用 multipart 表单,返回 JSON。注意推理时要加torch.no_grad(),并且把模型设为 eval 模式,否则 BatchNorm 和 Dropout 会导致结果不稳定。

4.2 FastAPI 推理服务代码与启动命令

import io import torch import torch.nn as nn from fastapi import FastAPI, File, UploadFile from PIL import Image from torchvision import transforms, models app = FastAPI(title="垃圾分类推理服务") DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") CLASSES = ["厨余垃圾", "可回收物", "有害垃圾", "其他垃圾"] # 与训练时保持一致的预处理 tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) def load_model(): model = models.mobilenet_v3_small(weights=None) model.classifier[3] = nn.Linear(model.classifier[3].in_features, len(CLASSES)) model.load_state_dict(torch.load("garbage_mobilenetv3.pth", map_location=DEVICE)) model.to(DEVICE) model.eval() return model model = load_model() @app.get("/health") def health(): return {"status": "ok"} @app.post("/predict") async def predict(file: UploadFile = File(...)): raw = await file.read() img = Image.open(io.BytesIO(raw)).convert("RGB") tensor = tf(img).unsqueeze(0).to(DEVICE) with torch.no_grad(): logits = model(tensor) probs = torch.softmax(logits, dim=1)[0] idx = int(probs.argmax()) return { "class": CLASSES[idx], "confidence": round(float(probs[idx]), 4), "all_probs": {CLASSES[i]: round(float(probs[i]), 4) for i in range(len(CLASSES))} }

逻辑说明:load_model里weights=None是因为我们要加载自己训练好的权重,不需要再下载预训练参数。map_location=DEVICE保证在 CPU 和 GPU 之间切换时不会报错。unsqueeze(0)把单张图变成 batch 维度为 1 的张量。返回all_probs是为了前端能做置信度展示,也方便排查误判。

启动命令:

uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2

参数说明:--workers 2表示起两个进程,适合多核 CPU;如果模型在 GPU 上,多 worker 会各自加载一份模型,显存要够。生产环境建议用gunicorn配合uvicorn.workers.UvicornWorker,并加--timeout 60防止大图推理超时。

注意:预处理必须和训练时完全一致,包括归一化的均值和方差。我见过有人推理时忘了 Normalize,准确率直接掉 30 个点,排查了半天。

5. 部署怎么落地:Docker 打包与边缘设备适配

5.1 用 Docker 把服务打包成镜像

部署最省心的方式是 Docker。基础镜像选python:3.10-slim,把依赖、模型文件、代码一起打进去。注意 PyTorch 的 CPU 版本和 GPU 版本镜像不同,边缘设备用 CPU 版,服务器用 GPU 版。下面是一个可用的 Dockerfile。

FROM python:3.10-slim WORKDIR /app # 先装系统依赖,opencv 需要 libgl RUN apt-get update && apt-get install -y --no-install-recommends \ libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY main.py garbage_mobilenetv3.pth ./ EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

requirements.txt里写:

fastapi==0.110.0 uvicorn[standard]==0.29.0 torch==2.2.0 torchvision==0.17.0 pillow==10.2.0 python-multipart==0.0.9

构建和运行:

docker build -t garbage-cls:1.0 . docker run -d --name garbage -p 8000:8000 garbage-cls:1.0

参数说明:--no-install-recommends减少镜像体积;python-multipart是 FastAPI 接收文件上传的必需依赖,漏了会报 422。镜像大小 CPU 版大约 1.5G,GPU 版 5G 以上,边缘设备建议用 ONNX Runtime 替代 PyTorch,能把体积压到 200M 以内。

5.2 边缘设备上的模型转换与性能取舍

如果部署在树莓派、Jetson 或安卓设备上,PyTorch 太重,转 ONNX 或 NCNN 是常见做法。转 ONNX 的代码:

import torch import torch.nn as nn from torchvision import models model = models.mobilenet_v3_small(weights=None) model.classifier[3] = nn.Linear(model.classifier[3].in_features, 4) model.load_state_dict(torch.load("garbage_mobilenetv3.pth", map_location="cpu")) model.eval() dummy = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy, "garbage.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, opset_version=12 )

逻辑说明:dynamic_axes让 batch 维度可变,方便批量推理。opset_version=12兼容性较好,Jetson 上的 TensorRT 支持这个版本。转完后用onnxruntime验证输出和 PyTorch 一致,误差在 1e-4 以内算正常。

性能取舍上,MobileNetV3-Small 在树莓派 4B 上单张推理约 80 到 120 毫秒,够用;如果要更快,可以量化成 INT8,速度提升 2 到 3 倍,准确率掉 1 到 2 个点。量化用onnxruntime.quantization的quantize_dynamic就行。

6. 避坑与排查:垃圾分类系统上线前后的 5 个血泪教训

现象一:本地准确率 95%,部署后用户反馈一半识别错。原因:训练数据是白底摆拍,真实场景是复杂背景和遮挡,域偏移导致模型失效。 解决:自采至少 30% 的真实场景图加入训练集,推理前加一个简单的目标检测或裁剪,把垃圾主体从背景里抠出来再分类。

现象二:服务跑一段时间后内存持续上涨,最后 OOM。原因:FastAPI 默认不限制上传大小,有人传了 10M 的大图,PIL 解码后占大量内存;另外模型没有做单例,每次请求都重新加载。 解决:在load_model外用模块级变量保证只加载一次;加UploadFile大小校验,超过 5M 直接返回 413;用--limit-max-requests让 worker 定期重启。

现象三:GPU 显存够但推理报 CUDA out of memory。原因:多个 uvicorn worker 各自加载一份模型,显存翻倍。 解决:GPU 部署时--workers 1,用批处理提高吞吐;或者用torch.cuda.empty_cache()在请求间隙释放缓存,但更推荐单 worker 加异步队列。

现象四:同一张图两次请求返回不同类别。原因:模型忘了eval(),Dropout 和 BatchNorm 在训练模式下随机性导致输出波动。 解决:加载模型后立刻model.eval(),推理包在torch.no_grad()里。这个坑很隐蔽,本地测试时如果只跑一次不会发现。

现象五:Docker 镜像构建成功但容器启动报找不到模型文件。原因:.dockerignore里把.pth排除了,或者COPY路径写错。 解决:检查.dockerignore,模型文件单独COPY并确认路径;用docker run --rm -it garbage-cls:1.0 ls /app进容器看文件在不在。

提示:上线前一定要做一轮「脏数据测试」,故意传纯色图、文字截图、超大图,看服务会不会崩。这些边界情况在真实使用中一定会遇到。

7. 把准确率再往上推:置信度阈值与二级分类的实战技巧

模型上线后,你会发现大部分误判集中在置信度 0.5 到 0.7 这个区间。我的做法是设一个阈值,低于 0.7 的结果不直接返回,而是走「人工确认」或「二级分类」。二级分类针对容易混的类别对,比如厨余和其他,单独训一个小模型,只在这两类之间做区分。这个小模型可以用更细粒度的特征,比如颜色直方图和纹理,甚至不需要深度学习,用 SVM 加 HOG 特征就能到 85% 以上。

另一个技巧是测试时增强(TTA)。对同一张图做水平翻转和轻微裁剪,各推理一次,把 softmax 概率平均。这个操作能让准确率涨 1 到 2 个点,代价是推理时间翻倍。边缘设备上不划算,服务器端可以开。

def predict_with_tta(model, img_tensor, n_aug=3): """TTA:原图 + 水平翻转 + 轻微裁剪,概率平均""" model.eval() probs = torch.zeros(1, len(CLASSES)).to(DEVICE) with torch.no_grad(): probs += torch.softmax(model(img_tensor), dim=1) probs += torch.softmax(model(torch.flip(img_tensor, dims=[3])), dim=1) # 中心裁剪再缩放 crop = transforms.CenterCrop(200)(img_tensor) crop = transforms.Resize((224, 224))(crop) probs += torch.softmax(model(crop), dim=1) return (probs / n_aug).argmax(dim=1)

参数说明:n_aug是增强次数,3 次是精度和耗时的平衡点;CenterCrop(200)的 200 要根据你的主体占比调,主体小就裁大一点。TTA 对旋转不变的类别(比如玻璃瓶)提升明显,对方向敏感的类别要慎用。

最后说一个我自己的习惯:每次改完模型或预处理,一定用同一批 100 张固定测试图跑一遍,记录准确率和混淆矩阵,和上一版对比。没有这个基线,你根本不知道改动是变好还是变坏。垃圾分类这个方向,数据质量比模型结构重要得多,把清洗和自采做扎实,比换十个骨干网络都管用。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 4:07:10

给Claude加上长期记忆:三层记忆架构与本地化实现指南

1. 从"聊完就忘"说起&#xff1a;claude-mem 到底想解决什么 如果你用 Claude 这类对话式 AI 做过稍微长一点的项目&#xff0c;大概率遇到过这种尴尬&#xff1a;昨天花了两个小时跟它把一套数据清洗逻辑捋清楚了&#xff0c;今天开个新会话&#xff0c;它一脸无辜地…

作者头像 李华
网站建设 2026/10/20 7:41:13

caveman 本地代理:AI coding agent 的 token 管理与请求转发实战

1. 从“caveman”说起&#xff1a;这个项目到底在解决什么问题 第一次看到“caveman”这个词&#xff0c;很多人会以为是某个复古风格的小工具&#xff0c;或者是个玩笑性质的项目名。但如果你最近在折腾 AI coding agent 相关的工具链&#xff0c;尤其是围绕 codex 这类命令行…

作者头像 李华
网站建设 2026/10/17 4:09:31

Agent Skills 实战指南:从零开发到多 Agent 编排的完整笔记

最近在调试自己的 Agent 项目时&#xff0c;我花了不少时间折腾 Agent Skills。以前实现一个 AI Agent 的能力&#xff0c;基本是把工具函数一股脑塞给模型&#xff0c;然后在 prompt 里写清楚每个函数是干嘛的&#xff0c;祈祷它能在正确的时候调用正确的那一个。可项目一复杂…

作者头像 李华
网站建设 2026/10/19 16:26:55

caveman范式:极简单文件AI Agent开发实践

1. 项目概述&#xff1a;这不是一个“原始人”玩笑&#xff0c;而是一套轻量级AI Agent开发范式“caveman”这个标题乍看像在调侃——回到石器时代&#xff1f;但如果你最近刷过技术社区、GitHub Trending 或 Discord 开发频道&#xff0c;就会发现这个词正悄然成为一批务实开发…

作者头像 李华
网站建设 2026/10/20 20:31:50

给命令行Claude装上持久记忆:claude-mem实现跨会话上下文延续

如果你每天在命令行里跟 Claude 打交道&#xff0c;应该早就意识到一个问题&#xff1a;它什么都好&#xff0c;就是记性太差。昨天刚聊明白的接口设计&#xff0c;今天新开一个会话&#xff0c;它又当成了全新需求&#xff0c;连字段名都要重新确认。Claude 官方确实有记忆&am…

作者头像 李华
网站建设 2026/10/24 4:01:29

claude-mem:给Claude补上跨会话记忆的本地外置记忆层

我最近半年多一直在用一个叫 claude-mem 的小工具。它不是某个大厂官方出的功能&#xff0c;而是一个给 Claude 这类对话模型补“记忆”的本地辅助工具。用过它之后&#xff0c;我最大的感受是&#xff1a;以前每次新开会话&#xff0c;我都得在提示词里重新交代项目背景、技术…

作者头像 李华