我们接手某个社区生活垃圾分类督导项目时,甲方提了一个让我印象很深的需求:投放点的监控画面不能只用来录像,还得能自动看出居民扔进去的到底是什么垃圾,实时判断分类是否准确,出了问题要能自动通知督导员。当时靠人盯着几十路视频去判断分类,不仅效率低,还很难做到全天候覆盖。几轮讨论之后,我们敲定做一套基于深度学习的垃圾分类检测系统,核心逻辑很简单:让摄像头把画面里的垃圾目标实时框出来,并归类到可回收物、有害垃圾、厨余垃圾、其他垃圾等预设类别,然后把识别结果对接给监管平台。
这套系统听起来只是常规目标检测的应用,但真正做起来之后,我发现它的难点不在模型本身,而在数据、场景和部署细节上。这篇文章不打算讲教科书式的算法原理,而是把我们从项目立项、数据集构造、模型选型、训练调优到边缘部署的完整过程,以及踩过的坑,全部摊开来说。如果你在做的方向是智慧城市、环卫信息化,或者想走一遍从数据到部署的AI落地流程,这篇文章应该能帮你少走不少弯路。
1. 项目概述:为什么需要一套垃圾识别系统
垃圾分类这个话题已经不是新鲜事,但落到技术手段上,传统方案的短板非常明显。我们最初走访了几个投放点,现场主要靠人工巡检翻袋,费时费力不说,居民和督导员之间还容易起争执。遇到高峰时段,投放量一大,督导员根本忙不过来。清运环节更麻烦,不同类别的垃圾需要不同的车辆和处置路径,前端的分类准确率上不来,后端的清运调度就只能跟着返工。
这套系统的定位就是替代一部分重复性高、时效性强的巡检工作。它的核心目标有两层,第一层是看得见,也就是目标检测,要把画面里出现的瓶子、袋子、纸箱、果皮等目标框出来;第二层是认得准,也就是分类,把框出来的目标归到正确的生活垃圾类别。这两个目标放在理论上都不复杂,但放在真实的摄像头画面里就变得很麻烦:垃圾的形态千奇百怪,同一类物品的颜色和材质差异极大,遮挡、反光、阴影又特别多。
所以从选型第一天起,我们就没怎么考虑传统图像处理方案。颜色阈值、边缘检测、形状匹配这些方法在固定的传送带分拣线上还能用,因为背景干净、光照稳定、物体摆放位置固定。但一旦换到开放环境的摄像头俯拍角度,背景复杂度和光照变化完全没法用传统规则覆盖。深度学习目标检测可以同时解决定位和分类两个问题,而且对复杂背景和形变有很强的鲁棒性。我们后来实测下来,主流检测框架在自建测试集上准确率可以达到95%上下,这个精度在社区督导、园区巡检场景里完全够用。
从项目角色来看,这套系统适合两类人参考。一类是做城市管理、环卫信息化、智慧社区的技术团队,需要快速了解这个场景下如何搭一套识别服务;另一类是刚接触AI落地、想完整走一遍数据标注到边缘部署全流程的开发者。文章里的方案、参数和代码都是我们在某园区实际跑过的,不同现场环境可能略有差异,但整体思路是可以直接搬走的。
1.1 场景差异决定了系统设计
有人可能会问,垃圾分类检测系统是不是同一个模型到处都能用?实际完全不是这么回事。我们接触下来,这类系统至少有两种落地形态,设计的侧重点完全不一样。
第一种是固定点位形态,比如垃圾房、垃圾亭顶部安装的摄像头。这类场景相对固定,摄像头视角和高度可以统一设计,通过调整安装角度,可以规避一部分遮挡问题。因为背景变化小、光照波动相对可控,模型不需要做得太大,更看重推理速度和边缘部署成本。第二种是移动巡检形态,比如督导员手持终端、车载巡查摄像头。这种场景下目标的距离、角度、遮挡程度都是动态的,对模型的泛化能力和推理速度要求都更高,往往需要轻量网络或剪枝量化来换取实时性。
我们这套系统先落地的是固定点位版本。当时选定方案的理由很直接:固定点位的数据可控、迭代速度快,可以先跑通业务闭环,拿到实际效果再往移动巡检上延伸。事实证明这个决策是对的,固定点位积累的数据和模型底座后来做手持端版本的时候直接复用了一部分,移植成本低了很多。
2. 数据集构建:精度问题的源头都在这
很多做目标检测的人有一个习惯,拿到项目先找开源模型跑一圈,发现效果不行就开始调参。但我们在垃圾分类这个场景上得到的教训是:模型精度的上限,在拿到数据那一刻就基本决定了。第一版测试集上mAP只有78%,当时以为是模型参数没用对,换了几个训练策略都没明显改善。后来把数据集打开一看,才发现问题全在数据上。
脏数据有三类典型问题。第一类是标注框偏移,有些标注框框住的不是整个垃圾目标,只框了一半,导致模型在回归边界时学到了错误的位置信息;第二类是类别错误,比如把卫生纸标成了厨余垃圾,把塑料瓶标成了可回收物之外的类别;第三类是样本分布不均,某个类别在数据集中占比只有2%,另一个占40%,模型训练完自然倾向多数类,少数类的召回率低得没法看。
处理数据问题没有捷径,只能靠扎实的数据工程。我们最终的数据集由三块拼起来:公开的开源垃圾类数据集作为预训练底座,自建采集的部分覆盖真实场景,人工补充的负样本用来消除误检。自建采集我们选了某园区和几个社区投放点,布了摄像头断断续续拍了三周,累计筛选出约2万张有效画面。这个数字听起来不小,但摊到细分类别上,每个类别平均也就两三千张,遇到类别多的场景还得继续补充。
这里有一个容易被忽视的点:类别体系设计。我们看到过一些方案直接把四分类当检测类别去训练,也就是模型直接输出“可回收物”“有害垃圾”“厨余垃圾”“其他垃圾”。这个做法看起来简单,但实际上模型学不好。因为“可回收物”下面包含了塑料瓶、纸箱、易拉罐、旧衣物等多种外观差异巨大的物体,跨类内差异太大,模型难以收敛。我们采用的策略是底层细分、上层映射:检测模型输出的物品级细分类别,比如塑料瓶、玻璃瓶、纸箱、果皮、剩饭、废电池、过期药品等,识别出来之后再通过后处理规则映射到四分类体系。这样做的好处是模型更容易训练,可解释性强,未来如果标准变更,只需要改映射规则,不必重新训练模型。
2.1 标注规范与质检流程
标注环节是纯体力活,但里面的坑比想象中多。垃圾目标的边界很不清晰,一个半透明的塑料袋里装了果皮和纸巾,框到底应该怎么画?我们的约定是:只标注最外层可见的容器或物体边界,不做内部穿透标注。如果目标被遮挡面积超过40%,这个目标就不标注。这看似简单的规则,能避免大量干扰样本。
另一个经验是标注结果一定要做交叉复检。我们让两组标注人员互相抽检,每组抽20%的图给另一组复核,不一致的地方返回重新标注。第一批标注完成的时候,互检发现的不一致率超过15%,主要集中在瓶子类和纸类这种外观相近的目标上。如果这批数据直接进训练集,后面模型大概率会在瓶子和纸盒之间犯迷糊。多花两天做质检,比花两星期调模型划算得多。
我们还在预处理阶段加入了一步批量清洗:把所有标注框的宽高比、面积分布导出成统计图,肉眼扫描异常值。比如某张图中标注框的面积特别大,占整幅图的90%以上,那大概率是误标;某个类别的标注框平均宽高比严重偏离常见值,也要拉出来检查。这套流程虽然土,但真的很管用。
3. 模型选型与训练配置:别一上来就堆算力
模型选型方面,我的建议是别盲目追新。新模型结构层出不穷,每年都有更强的backbone和neck结构发布,但垃圾分类检测这个场景的特点是目标尺寸跨度大、类别间外观差异复杂、部署环境算力有限。我们最终采用的是目标检测框架的思路,把整个流程拆成检测和分类两阶段来优化,这样比直接端到端硬学更可控。
我们第一版用的是通用目标检测框架,精度确实高,但在边缘盒子上只能跑到十二三帧的水平,现场画面稍微多一点就跟不上。后来换成轻量化主流检测模型,配合tuned输入尺寸,精度只下降了不到1个百分点,帧率却翻了一倍多。这个对比不是想说明哪个模型更好,而是想强调:部署环境的约束要前置到选型阶段。你选的新模型训练精度再高,跑不动等于零。
输入尺寸的调整也很关键。很多人习惯用默认的640×640输入,这在边缘设备上有时候是浪费。我们针对固定点位画面做了实验,把输入压到480×480,精度下降不到1.5%,速度上升明显。因为固定点位的垃圾目标在画面里占比往往比较大,不需要那么高的整图分辨率来检测小目标。
这里给一个我们调试下来的参考试验表,不同项目的具体数值可能会有差异,但趋势可以参考:
| 模型方案 | 输入尺寸 | GPU环境mAP | 边缘盒实测帧率 | 适用场景 |
|---|---|---|---|---|
| 通用检测框架基础版 | 640×640 | 0.92 | 12-15 | 离线分析、高精度要求 |
| 轻量优化版 | 640×640 | 0.91 | 25-30 | 固定点位实时识别 |
| 轻量优化+剪枝量化 | 480×480 | 0.87 | 35以上 | 多路并发、低算力设备 |
从表里能看出来的核心关系是:输入尺寸、模型规模、量化策略是三个可以互相置换的变量。做边缘部署的时候,这三者需要一起调,单独动一个很难达到预期。
3.1 训练流程与关键参数解读
训练策略上,我们用了两阶段方法:先在公开数据上做预训练,再在自建数据上微调。这么做背后的逻辑很简单,自建数据的采集环境相对单一,直接随机初始化训练的话,模型的泛化能力会很弱。实地部署时,经常有新的投放点加入,摄像头的角度和现场的采光环境都不一样,预训练过的模型底子更扎实,换场景时掉精度会小很多。
训练参数方面,我贴一份我们在某个版本里实际用过的配置,可以直接当作起点:
model: num_classes: 8 data: train_path: ./dataset/train val_path: ./dataset/val input_size: 640 batch_size: 16 train: epochs: 120 optimizer: SGD lr: 0.01 lr_scheduler: cosine augmentation: mosaic, mixup, hsv这份配置里的几个参数我分别说一下。batch_size设到16,是基于单卡显存来定的,如果显存更大可以适当往上加,收敛会更平滑;如果用的是入门级显卡,建议把batch_size调小一点,同时把epoch数量拉长。学习率初始值0.01配合余弦退火是我们试验里比较稳的组合,能实现在前期大步快跑、后期精细收敛的效果,尤其是对样本量少的类别比较友好。
数据增强这块,mosaic和mixup在目标检测里是标配了。之所以对垃圾分类特别有效,是因为垃圾场景里目标遮挡和堆叠非常严重。mosaic把四张图拼到一张里训练,让模型被迫学会在杂乱背景下区分前景和背景,这个能力在真实投放点画面里特别宝贵。如果你的场景里遮挡特别严重,我建议再加一些随机遮挡增强,比如把部分目标区域用矩形遮掉,逼模型学非遮挡部分的有效特征。
3.2 训练指标怎么看
训练启动之后,控制台会刷出一堆指标。我的经验是别只盯着loss看,要loss和mAP配合着一起看。loss下降但mAP不涨,通常意味着模型已经过拟合了,或者验证集的数据分布和训练集差异太大;loss不降,先别急着调模型,检查学习率和数据路径有没有配错,这个低级错误我们自己也犯过。
我们早期遇到过一个典型问题:loss降到比较低的位置,但纸箱这个类别的mAP只有0.6。排查后发现是样本不均衡,纸箱类占比只有5%,而且大部分是远景小目标,特征模糊。解决办法是给纸箱类提高采样权重,然后专门补充了近景纸箱的正样本。调整之后纸箱mAP拉到了0.9以上,整体mAP也跟着涨了。
另一个容易被忽略的点是,训练过程要多存几份checkpoint,不要只留下最后一版。因为最后一版未必是最优的,有时候在验证集上表现最好的反而是倒数几个epoch的中间版本。我们每次训练都会把每个epoch的验证精度记录到表格里,训练结束后把最高点对应的权重拿出来部署,而不是直接拿最后的权重用。
4. 实操过程:从训练到边缘部署的完整链路
前面讲了方案和数据,这一部分把完整实操链路串一遍。整体流程大概是:环境准备、数据整理、模型训练、模型转换、边缘部署、联调测试。如果你已经有目标检测基础,可以直接跳到后面的常见问题,那部分才是游玩过之后攒下的真正经验。
4.1 环境准备与依赖安装
训练阶段我们用的是Linux环境加Python生态,部署目标是一台带GPU的推理盒子。环境搭建部分没有花太多时间,因为整个项目把依赖都锁在了一个conda环境里,后面复制起来很方便。新建环境可以直接按下面的命令走:
# 创建虚拟环境 conda create -n waste_det_env python=3.10 -y conda activate waste_det_env # 安装深度学习框架,具体版本号以官方发布为准 pip install torch pip install torchvision # 安装目标检测相关依赖 pip install opencv-python pyyaml tqdm这里有个容易踩的坑是训练环境和推理环境版本不一致。我们第一次部署的时候就因为训练时用了某个自定义算子,导出到ONNX模型时提示算子不兼容,不得不再回训练环节把算子换掉。所以建议项目一开始就定好导出格式和推理引擎版本,训练时尽量少用自定义操作,能省掉很多导出时的麻烦。
4.2 数据标注与格式转换
采集完图片之后,标注数据会以XML或JSON格式保存。训练脚本读的是标准检测格式,所以需要写一个转换脚本。这个过程看起来机械,但它是训练能不能顺利跑起来的前提。我们当时写了一个简单的转换函数,把XML里的目标框信息转成训练所需的文本格式:
import os import xml.etree.ElementTree as ET def xml_to_txt(xml_path, out_txt_path, class_dict): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter('object'): name = obj.find('name').text bbox = obj.find('bndbox') xmin = float(bbox.find('xmin').text) ymin = float(bbox.find('ymin').text) xmax = float(bbox.find('xmax').text) ymax = float(bbox.find('ymax').text) class_id = class_dict[name] img_w = float(root.find('size/width').text) img_h = float(root.find('size/height').text) x_center = ((xmin + xmax) / 2) / img_w y_center = ((ymin + ymax) / 2) / img_h width = (xmax - xmin) / img_w height = (ymax - ymin) / img_h lines.append( f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}" ) with open(out_txt_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines))转换完成后,一定要做一次可视化检查:把标注框画回到原图上,人眼过一遍。很多数字上看不出来的问题,比如框偏了、类别贴错、文件路径没对上,画出来一眼就能看到。这一步能过滤掉相当比例的脏数据。我们项目里专门写了一个可视化脚本,批量生成带标注框的图片,然后按分类抽样检查,确保每个类别的标注质量都过关。
4.3 训练过程与模型选择
数据准备好了,模型训练其实是最省心的一环,因为前面工作扎实之后,后面就是交给算力的事。但训练过程中也要记录指标,尤其是验证集上的表现。我会在每轮训练结束时输出一张汇总表,记录当前epoch的loss、各类别mAP、整体mAP等。训练完成后,从记录里挑最好的权重,而不是直接把最后的checkpoint拿去部署。
有一次训练,我们跑到第80轮的时候整体mAP到了0.9,后面继续训练到第120轮,整体mAP反而降到了0.88。这就是典型的过拟合。好在我们把每个epoch的权重都存了一份,回溯后取了第80轮的权重,部署效果反而更好。所以训练完不要急着删中间文件,它们的价值很多时候是最后才体现出来。
4.4 模型导出与边缘设备部署
训练完的权重不能直接扔到边缘盒子上用,需要先做转换和优化。我们统一导出为ONNX格式,再用推理引擎来处理。导出命令大致是:
python export.py --weights best.pt --include onnx --img-size 640 640导出的ONNX文件在推送前还需要做一次简化,把一些冗余计算节点去掉。这个环节要特别注意ONNX输入输出的节点名称,后续的推理代码会引用这些名字,一旦没对上,运行时就报错。
部署侧的边缘盒子用的是精简过的系统环境。为了让模型跑得更快,我们对ONNX模型做了INT8量化。量化之后精度损失了大约两个点,在我们的可接受范围之内,但推理速度提升非常明显,多路并发能力也有实质性的改善。需要提醒的是,量化不是一个完全无感的操作,如果模型对精度很敏感,建议先跑一批校准数据,看看量化后的效果再决定是否启用。
部署时我还碰到过一次诡异的情况:模型量化之后在本地测试一切正常,到了现场却偶尔出现画面卡顿。排查了很久,最后发现现场接入的摄像头画面分辨率太高,多路视频流同时进来,解码环节成了瓶颈,和模型推理本身没关系。后来我们在接入侧做了统一缩放和帧率限制,最多保留25帧每秒,画面卡顿问题就解决了。
4.5 后处理规则与业务联动
模型输出的是一个个带置信度的检测框,但业务侧不能直接用裸的检测结果。我们在这中间加了一层后处理规则,把模型输出的细分类别映射到四分类体系。规则引擎的代码不复杂,但逻辑要梳理清楚。
比如模型输出"塑料瓶"类别,规则层就把它映射到"可回收物";输出"果皮"映射到"厨余垃圾";输出"废电池"映射到"有害垃圾"。当模型输出的置信度低于某个阈值时,规则引擎会把这条结果标记为"待复核",推送人工确认。这样一来,系统不会因为单帧检测的不确定性给监管方造成过多误报,又能在真正拿不准的时候把人工拉进来兜底,整体方案更贴合实际业务。
5. 常见问题与排查技巧实录
项目做下来大半年,有些问题几乎是每个新点位部署时都会撞上的。我把它们整理成几类,每一类都附上当时的排查思路和解决办法,希望对正在做类似系统的你有点帮助。
5.1 误检和漏检的典型原因
误检最常见的情况,是把和垃圾外观相似的物体识别成垃圾。比如地面上的树影,在某些光照条件下会被模型当成黑色塑料袋;墙面上的污渍,会被当成厨余垃圾。这种情况下模型其实没有错,它学到的确实是颜色纹理特征,问题是场景里存在天然相似的干扰项。
我们从两个方向解决。一个是扩充负样本,把含树影、污渍、反光的地面照片加入训练集,标注为背景类别,让模型知道这些“长得像垃圾但不是垃圾”的东西不该触发检测。另一个是做后处理过滤,对置信度低于0.5的检测结果直接丢弃,同时对检测框位置在相邻帧之间跳动过大的目标做抑制处理。两条路并行,误报率能压下去不少。
漏检则往往集中在很小的目标上。瓶盖、烟头这类东西在整幅画面里只占十几个像素,模型做了几次下采样之后,特征基本就消失了。常规解法是提高输入分辨率,或者引入多尺度检测。如果设备算力跟不上,可以用画面切块配合放大检测,或者单独针对小目标增加一个检测分支。我们在现场其实用的是折中方案:把画面分成四个区域分别检测,虽然增加了少量算力开销,但小目标召回率提升很明显。
5.2 光照与环境变化的应对
户外点位最折磨人的是光照变化。一批垃圾在中午、傍晚和夜间补光下,颜色表现完全是三种感觉。我们第一版模型在中午的表现不错,但到了傍晚,准确率明显往下掉。后来检查训练集才发现,里面傍晚时段拍摄的样本少得可怜。模型根本没学过那个光照条件下的特征,自然就认不出来。
解决办法说起来很直白:按时间段比例补充数据,同时增加亮度扰动、对比度扰动、色温扰动等数据增强操作。调整之后,训练集的光照分布和真实场景尽量保持了一致,傍晚时段的识别率很快就上来了。这次经历让我形成了一个习惯,凡是户外场景的项目,数据分析阶段就要把“条件分布”做出来看,不能只看总量。
5.3 相似类别混淆的处理
垃圾分类里有些类别之间的界限,人眼都不一定分得清。比如纸盒和塑料膜,都是薄薄的片状物,在俯拍画面里纹理特征接近,模型经常在两个类别之间摇摆,输出的置信度相差很微弱。
这类问题靠模型本身很难彻底解决,所以我们选择在后处理层加规则。如果“可回收物”和“其他垃圾”两个类别的置信度都大于0.4且差值小于0.3,就判定为“待复核”,推一个提示给人工。这个策略牺牲了一点点全自动化率,但换来的是更可靠的业务判断。毕竟垃圾分类错了,后续清运和处置环节的纠错成本远比人工多看一眼高得多。
5.4 现场部署问题速查表
日常运维中那些高频问题,我们整理成了一张速查表。以后遇到类似现象,先对一下表能节省大量排查时间。
| 现象 | 常见原因 | 处理建议 |
|---|---|---|
| 模型准确率偏低 | 数据类别不平衡、标注不准确 | 分类补充样本,做交叉复检 |
| 特定时段识别差 | 该时段训练样本不足 | 按时间段补充采集 |
| 检测框抖动严重 | 输入分辨率低或推理帧率不稳 | 提高推理帧率,加跟踪平滑 |
| 转换后精度下降 | 量化过度或算子不支持 | 减小量化范围,重新校准数据集 |
| 部署后画面卡顿 | 解码环节瓶颈、多路并发 | 统一缩放、限制帧率、优化IO |
5.5 部署后的持续迭代
系统上线不是项目的结束。垃圾场景的变化速度远比想象中快,包装换了材质、新的消费品类出现了、一个季度里不同投放点的垃圾构成也会改变。如果模型不动,准确率迟早会往下掉。
我们的做法是从上线第一天起就搭一套数据回流机制。每个月把现场截到的误检和漏检图片存下来,人工打标之后加入训练集,做一轮小规模增量训练。这套流程不复杂,但坚持下来效果非常明显。项目运行了几个月之后,模型在目标区域的稳定性明显比刚上线时好很多。可以说,系统真正的价值是通过持续迭代堆出来的。
6. 写在最后
这个项目让我感触最深的一点是,深度学习模型本身早就不是核心难点,真正的难点在于对场景的理解和数据耐心的打磨。垃圾分类检测系统恰好把这两点体现得淋漓尽致:每一处误检的背后,多半都藏着一条关于真实世界的规律,可能是光影条件,可能是垃圾摆放习惯,也可能是摄像头安装角度的差异。把这些规律摸清楚了,模型的提升就有了明确方向。
目前我们已经在做一个扩展方向,让系统不仅识别垃圾类别,还能评估垃圾的装满程度,联动清运调度。只要检测网络输出的信息足够结构化,上层业务能做的事情其实比想象中要多得多。希望这篇文章能给你提供一个可靠的切入点,少踩几个我们当年踩过的坑。