垃圾分类检测算是我经手过的最“实在”的视觉项目之一。为什么这么说?因为从数据整理到模型部署,每一个环节都会踩到坑,而且每个坑都能直接影响到最终识别效果。很多朋友拿到这个题目第一反应就是“用卷积神经网络跑一下”,但实际做下来你会发现,真正决定项目上限的往往是数据质量、类别平衡、以及部署时的推理效率这些细节。这篇博文我就把整个完整流程拆开揉碎,从方案选型到训练调试再到落地部署,把核心步骤和容易翻车的地方都列出来,希望给正在做类似项目的同学一个能直接参考的路线图。
1. 整体设计思路与方案选型
1.1 先搞清楚识别对象是什么
垃圾分类检测系统这个名字听着简单,但里面其实有两个完全不同的技术方向。一个方向是“分类”——你给它一张垃圾的特写照片,它告诉你这是塑料瓶还是易拉罐;另一个方向是“检测”——你给它一张街边垃圾桶或者一堆垃圾的场景图,它得先从画面里把每个垃圾目标框出来,再判断每个框里是什么类别。
这两个方向对应的技术路线完全不一样。分类任务用图像分类网络就行,比如ResNet、MobileNet这类;检测任务则需要用目标检测框架,比如YOLO、SSD或者Faster R-CNN。大多数教程默认讲分类,但实际场景里“一堆垃圾混在一起”才是常态,所以检测才是更贴近真实需求的方向。不过,检测模型的数据标注成本比分类高得多,很多人做到一半就卡在标注上了。
1.2 我的技术选型逻辑
结合成本和效果,我当时给某智能硬件团队做的模拟项目X采用的是“分类为主、检测为辅”的折中方案:先用一个轻量级分类模型对单类垃圾图片做快速判断,再在需要定位的场景中使用检测模型做辅助。这个策略的优势在于——分类模型对设备要求低,可以跑在树莓派、Jetson Nano这类边缘设备上;而检测模型只在小范围触发,不会因为持续运行导致算力吃紧。
模型方面,主力分类网络我选了MobileNetV3-Large,一个关键原因是它支持输入尺寸缩放。垃圾分类的图片来源很多样,有手机拍摄的、有监控摄像头截帧的,分辨率参差不齐,MobileNetV3的输入尺寸可以灵活调整(如224×224或192×192),在识别精度和推理速度之间能找到一个比较舒服的平衡点。检测部分用的则是轻量化的YOLOv5s,训练成本和部署难度都比较友好。
2. 数据准备与预处理:打好地基
2.1 数据集从哪来、怎么整理
垃圾分类项目最大的门槛就是数据。公开数据集确实有,但直接用效果往往不理想。原因很简单——公开数据集的图片大多很“干净”,背景单一、光照均匀、物品完整;而真实场景里的垃圾是皱巴巴的、脏兮兮的、各种角度都有,还可能被遮挡。模型在公开集上跑得很漂亮,一换场景就原形毕露,这个现象我做项目第一次测试时就踩中了。
我的做法是建立一个多层次数据池:
- 第一层:公开数据集的常见类别图片,作为基础训练集
- 第二层:自己用手机拍摄的真实垃圾照片,覆盖日常可见的各种形态
- 第三层:从视频监控截帧和网上找的街拍场景图,补充复杂背景
整理数据时按可回收物、有害垃圾、厨余垃圾、其他垃圾四大类归拢。这里有一个容易忽略的细节:每个大类下还要细分具体物品,比如可回收物下面有塑料瓶、玻璃瓶、易拉罐、纸箱等子类。先训练子类模型,再映射到根类别,比直接训四分类要准确得多,因为子类之间的特征区分度更高。
2.2 数据清洗和增强的关键细节
数据整理阶段最容易翻车的是脏数据。图片模糊、曝光过度、物品占比过小、标签贴错,这四类是重灾区。我当时写了一个简单的预处理脚本,自动剔除分辨率过低的图片,再人肉抽检一遍标签是否对得上。
数据增强方面,除了常规的随机旋转、翻转、色彩抖动之外,我额外用了两个针对性策略:
一是随机遮挡。给训练图片随机加上一些黑色方块,模拟垃圾被部分遮挡的情况。这个操作能显著提高模型在真实场景下的鲁棒性,尤其是当多个垃圾堆叠时。
二是mixup增强。按一定比例混合两张不同类别的图片。这种方法能减少模型对特定背景的过拟合,让模型更关注物体本身的特征。实测下来,加入mixup之后模型在验证集上的表现不一定会提升太多,但在真实场景图上的泛化能力会明显变好。
提示:数据和标签的质量直接决定模型上限。哪怕模型结构再先进,喂进去一堆错误标签,训出来的结果也只能是错的。
3. 模型搭建与训练实操:从零到能用的全流程
3.1 环境配置与代码骨架
训练环境我用的是一台带有单张GPU的机器,软件栈是PyTorch框架。PyTorch在快速迭代方面确实比TensorFlow顺手不少,尤其是调试阶段报错信息更直观。
先给出一个最简训练骨架,分类模型部分:
import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms, models # 数据预处理 data_transforms = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.3, contrast=0.3), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) # 加载数据集 train_dataset = datasets.ImageFolder('data/train', transform=data_transforms) val_dataset = datasets.ImageFolder('data/val', transform=data_transforms) train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=64, shuffle=True) val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=64, shuffle=False) # 加载预训练模型 model = models.mobilenet_v3_large(pretrained=True) # 替换最后的分类层为4分类 model.classifier[3] = nn.Linear(model.classifier[3].in_features, 4) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) # 定义损失函数和优化器 criterion = nn.CrossEntropyLoss() optimizer = optim.AdamW(model.parameters(), lr=1e-4)很多人会问为什么要用预训练权重,我的回答是:除非你的数据集有几万张,否则不要从零开始训。ImageNet上预训练出来的模型已经学会了纹理、边缘、形状这些底层特征,你只是在它的基础上做迁移学习,收敛速度和最终精度都会好很多。
3.2 训练策略与参数调优
垃圾分类这个任务有一个特殊性:同一个物品,不同状态下的视觉差异极大。一个完整的塑料瓶和一个被踩扁的塑料瓶,在模型看来几乎就是两个不同的物体。所以训练参数设置上,学习率不能太大,否则模型容易在局部震荡而学不到细微特征。
我的经验是分两阶段训练:
- 第一阶段:冻结backbone,只训练分类头,学习率设在1e-3,跑5-8个epoch
- 第二阶段:解冻整个网络,将学习率降到1e-4到5e-5之间,对整个网络进行微调
epoch数量的判断标准不是固定值,而是看验证集loss是否还在下降。我一般是每3个epoch保存一次权重,等训完之后挑验证集上表现最好的那个模型,而不是用最后一个epoch的结果。
batch size的选择上,如果显存足够,尽量用64以上。小的batch size带来的梯度噪声大,在细粒度分类任务上会让收敛变慢。当然了,如果你用的是CPU训练,那就另说了——建议直接上云GPU或者用Colab,不然一个4分类模型训十几个小时确实熬人。
3.3 检测模型的训练要点
YOLOv5s的训练相对复杂一些,因为需要处理标注框。标注工具我用的是LabelImg,导出为YOLO格式。这里有一个容易踩的坑:标注框的坐标需要归一化到0到1之间,并且是相对于图片宽高的比例。
YOLO训练过程中的关键参数:
# YOLOv5训练指令示例 python train.py --data garbage.yaml --weights yolov5s.pt --img 640 --epochs 100 --batch-size 16img参数决定了训练时缩放到的分辨率。640是默认推荐值,太大(比如1280)会显著增加训练时间,但精度提升有限;太小(比如320)则对小目标很不友好。batch size 16在显存有限的机器上是一个安全选择。训练时如果发现loss前期不下降,优先检查标注文件是否正确,而不是急着调参。
4. 模型评估与优化:纸上指标和真实场景要分开看
4.1 评估指标解读
模型训完之后,不能只看准确率一个指标。垃圾分类场景存在明显的类别不平衡问题——比如“其他垃圾”样本数量远远多于“有害垃圾”,如果只看准确率,模型只要把所有东西都判成其他垃圾就能获得很高的分数,但是这个模型毫无实用性。
所以必须看每个类别的精确率、召回率和F1分数。我当时用sklearn的classification_report直接输出各类别指标,重点盯着两个容易混淆的类别看:可回收物和厨余垃圾。这两个大类在视觉上经常有交集,比如一个沾了食物的塑料盒,你说它属于可回收还是厨余?人都会犹豫,模型更会出错。
4.2 混淆分析和错误样本复盘
训练完之后,我建议不要急着部署。把验证集里预测错误的图片全部打出来,人肉看一下模型到底错在什么地方。这一步很多人嫌麻烦直接跳过,但恰恰是这一步最能发现数据的短板。
我当时做这个复盘时发现了一个规律:模型对完好的、干净的单品垃圾识别得很准,但对多件垃圾叠在一起的图片识别率骤降。这个发现直接改变了我的数据策略——从公开集里挑了一百多张多目标场景图,单独做了一轮标注和微调。后面再测,压叠场景的识别率提升明显。
4.3 模型轻量化和推理加速
部署阶段要考虑模型体积和推理速度。MobileNetV3-Large本身已经挺轻了,但还可以压缩。PyTorch提供了量化接口,可以把模型从FP32压缩到INT8,体积缩小近4倍,推理速度翻倍,代价是精度略微下降——在垃圾分类这种任务上,1%-2%的精度损失完全能接受。
# PyTorch量化示例 import torch.quantization as quant model.qconfig = quant.get_default_qconfig('fbgemm') quant.prepare(model, inplace=True) # 在验证集上跑一轮以统计激活值范围 calibrate(model, calib_loader) quant.convert(model, inplace=True) # 导出为ONNX格式 dummy_input = torch.randn(1, 3, 224, 224).cpu() torch.onnx.export(model, dummy_input, "garbage_model.onnx", opset_version=11)经验之谈是:如果模型在边缘设备上跑,建议导出ONNX再转到TensorRT或者OpenVINO,这比纯PyTorch推理快得多。我之前在Jetson Nano上直接跑PyTorch模型,一帧要300多毫秒;转成TensorRT之后降到80多毫秒,完全够实时了。
5. 实际部署与演示:把模型变成能用的东西
5.1 命令行推理工具的实现
模型做得再好,没有落地界面就只能算个实验品。我最早做的是一个Python命令行工具,从摄像头或者图片文件读入,实时输出识别结果。推理部分的核心逻辑很简单:
import torch from PIL import Image from torchvision import transforms def predict_image(model, image_path, class_names, device): img = Image.open(image_path).convert('RGB') transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) img_tensor = transform(img).unsqueeze(0).to(device) with torch.no_grad(): outputs = model(img_tensor) _, predicted = torch.max(outputs, 1) probs = torch.nn.functional.softmax(outputs[0], dim=0) confidence, _ = torch.max(probs, dim=0) return class_names[predicted.item()], confidence.item()注意最后一个细节:softmax后取的概率是置信度,一般在垃圾图片比较标准的时候能到0.9以上,但遇到模糊图片就会掉到0.5、0.6左右。这个置信度信息非常有用,应该作为输出的一部分呈现,方便用户判断结果是否可靠。
5.2 Web端快速展示选项
如果要给非技术背景的人演示,命令行就不够直观了。我后来用Flask写了一个极简Web服务,允许用户上传图片,前端显示识别结果和置信度。整个服务代码量不大,核心就是一个上传接口加一个模型推理封装:
from flask import Flask, request, jsonify import torch from PIL import Image import io app = Flask(__name__) @app.route('/predict', methods=['POST']) def predict(): if 'file' not in request.files: return jsonify({'error': 'no file uploaded'}) file = request.files['file'] img_bytes = file.read() # 转换为模型输入格式 image = Image.open(io.BytesIO(img_bytes)).convert('RGB') result, confidence = predict_image(model, image, class_names, device) return jsonify({'category': result, 'confidence': confidence}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)这种方式在局域网内演示的效果很直观,手机拍照上传到电脑端,几秒就能看到识别结果。而且因为核心是一个HTTP接口,后续如果要接硬件设备,或者把它集成到更大的平台,也只需要在这个接口上做文章。
5.3 边缘设备部署的经验
最后聊下在树莓派或者开发板上部署的注意事项。这类设备的系统环境不像PC上那么完整,装上PyTorch可能就会遇到依赖缺失的问题。有几个坑一定要说:
首先,建议在设备上用一个独立的Python虚拟环境,不要直接装到系统里,不然依赖一冲突,整个设备的环境就乱了。其次,由于这些设备的ARM架构问题,有些预编译轮子找不到,需要自己编译,比较费时间。最省事的替代方案是使用英伟达系开发板自带的镜像系统,很多常见的库都是预装好的,省去不少麻烦。
还有一个个人习惯:在边缘设备上部署时,我会把整个推理流程封装成服务,而不是一个单独脚本。因为脚本一旦断掉,重启很麻烦;但如果是服务,可以有进程守护机制,自动恢复,稳定很多。
6. 常见问题与排查技巧实录
6.1 模型训练loss不下降怎么办
这个问题我看到被问得最多。首先确认数据预处理环节有没有问题,特别是Normalize的均值方差是否和训练时一致。如果数据增强太过激进,模型也会学不到东西,尤其是mixup比例过大时,模型看到的全是“四不像”图片,会严重影响收敛。
排除这些之后,再考虑学习率设置。如果还没效果,就调低学习率重跑几个epoch看看。我当时遇到过一次loss完全不下降的情况,排查下来发现是数据增强中的随机遮挡概率设得太高,导致大部分图片的主体都看不到了,模型无从学起。
6.2 模型对真实场景图片识别率低怎么办
这种情况九成是训练集和真实场景分布不一致造成的。解决思路就一个:收集更多贴近真实场景的数据来微调。我当时从网上找了大量关于垃圾桶、垃圾站的特征素材,也自己拍了一堆,专门补充了一轮训练集,识别率提升非常明显。
还有一种情况是检测模型能框出目标但分类错误,那大概率是训练时正负样本比例失衡。解决办法是增加困难负样本的挖掘比例,换句话说,把容易混淆的类别图片多放一些进去训练。
6.3 不同设备上推理速度差距为什么这么大
训练机上的GPU推理和部署机上的CPU推理完全是两个量级。如果发现CPU推理速度慢得离谱,先别急着优化代码,检查一下模型是否绑定了GPU和CUDA的特定算子。我用ONNX导出再转换运行引擎后,速度和兼容性都好了很多。
6.4 常见问题速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 训练loss不降 | 数据增强过度、学习率过大/过小 | 检查数据管线,调整学习率 |
| 训练收敛但验证集准确率低 | 过拟合、训练集与验证集分布不一致 | 增加数据量、加正则化、调整验证集 |
| 真实场景识别率差 | 训练集过于“干净” | 补充真实场景样本做微调 |
| 部署端推理慢 | 模型未量化/未转引擎 | 做INT8量化,转入推理引擎 |
| 边缘设备上无法加载模型 | 内存不足、模型过大 | 换更轻量模型如MobileNetV3-Small或做剪枝 |
6.5 几个容易忽略的细节
分类类别名称别用中文直接作为文件夹名,容易在某些环境上出编码问题,用数字编号或者拼音最省心。
每次训练前固定随机种子,保证结果可复现。这个在调试阶段特别重要,不然排bug的时候你根本不知道是这个改动起了效果还是随机性导致的。
备份每一个版本的权重文件。尤其是做数据增强、微调实验的时候,旧权重的效果可能比新权重还好。每次要覆盖前先确认新模型真的更好,不然想回退都没文件用。这一点我是吃过亏的,后来就养成了备份的习惯。
最后再分享几个心得
做垃圾分类检测这个项目,最大的体会其实不是模型架构多复杂,而是数据工程的投入占比远比想象中高。我在这个项目上大概七成的时间花在了数据清洗、标注和增强上,真正调模型的时间不到三成。很多人一开始冲劲十足地跑代码,跑到中期发现效果不好,回头一看是数据没跟上,然后又重新回来整理数据——我一开始也是这么走了弯路。
另外就是部署意识要提前。很多人在训练阶段完全不考虑推理环境和性能,等到模型训好了才发现部署端跑不动,只能从头选轻量化模型,前面的训练全白费了。正确的节奏应该是一开始就想好最终要部署在哪里,然后反推模型的选型和优化方案。
如果后续要把这个系统做得更完善,我觉得方向上可以考虑视频流实时检测,就是对接摄像头的RTSP流,在帧级别连续识别,那才算真正达到一个可商用的状态。不过那就是另外一个工作量了,先把单张图片的流程跑通,打牢地基,后面再多的事情也不怕。