1. 从一张随手拍的照片说起:这个项目到底在解决什么问题
小区楼下四个垃圾桶并排摆着,绿桶写"厨余"、蓝桶写"可回收"、红桶写"有害"、灰桶写"其他"。我拎着一袋混着外卖盒、香蕉皮、废电池和快递纸箱的垃圾站在那儿,脑子里第一反应是"这电池到底算不算有害",第二反应是"算了,全扔灰桶吧"。这个场景你肯定不陌生。垃圾分类推行了这么多年,真正卡住普通人的不是意愿,而是判断成本——每次扔垃圾都要做一道分类题,做错了还有心理负担。
这个项目要干的事很直接:用摄像头拍一张垃圾的照片,模型自动告诉你这是哪一类,该扔哪个桶。核心关键词就是深度学习、yolov8、目标检测、垃圾分类、图像识别。它不是做一个"整张图属于哪一类"的简单分类器,而是做目标检测——一张图里可能同时有塑料瓶、纸盒、果皮,模型要把每个物体框出来,分别标注类别。这个区别很关键,后面会展开讲。
适合谁看?如果你是刚入门深度学习、想找一个"有真实场景、数据好搞、效果看得见"的练手项目,垃圾分类检测是个非常合适的切入点。如果你已经跑通过yolov8的官方demo,想进一步搞清楚改进算法到底改哪里、为什么改、改了有没有用,这篇也会把改进思路和验证方法讲透。如果你是想做课程设计或毕业设计的同学,这里面的数据集构建、模型改进、对比实验、部署落地整条链路都能直接参考。
我先把结论摆前面:baseline用yolov8n在自建垃圾数据集上跑,mAP@0.5大概能到0.85上下;做针对性改进后能推到0.90左右。但真正决定项目成败的不是模型结构,而是数据集的质量和类别定义。这一点我在后面会用大篇幅讲,因为踩过的坑基本都在这儿。
2. 整体方案设计:为什么选yolov8,为什么做检测而不是分类
2.1 分类、检测、分割,三条路选哪条
垃圾分类这个任务,技术上至少有三条路可走。
第一条是图像分类:输入一张图,输出一个类别标签。比如ResNet、EfficientNet这类模型,最后接一个softmax。这条路最简单,但有个致命问题——一张图只能给一个答案。现实中你拍一张垃圾桶旁边的照片,里面可能有瓶子、纸、果皮,分类器只能告诉你"这张图最像可回收",剩下两个物体就被忽略了。
第二条是目标检测:输入一张图,输出若干个边界框,每个框带类别和置信度。yolov8、Faster R-CNN都属于这类。它能处理"一图多物"的情况,正好匹配垃圾场景。
第三条是语义分割:像素级分类,把每个像素归到某个类别。精度高,但标注成本极高,一张图标注要几分钟,做几千张数据集不现实。
我选目标检测,理由很实在:垃圾场景天然是"多物体、需要定位"的,分类器不够用;分割标注太贵,学生项目和个人研究扛不住。yolov8在检测里又是性价比最高的选择——速度快、精度够、生态好、改进空间大。
2.2 yolov8相比前代到底强在哪
很多人用yolov8但说不清它比yolov5强在哪。我按自己的理解捋一遍,这些点直接决定了你后面改进的方向。
第一,Anchor-Free。yolov5还是基于anchor box的,需要预先聚类出一组先验框尺寸。yolov8改成了anchor-free,直接预测目标中心点和宽高。好处是省掉了调anchor的麻烦,对小目标和形状差异大的目标更友好。垃圾里既有细长的电池,又有扁平的纸盒,anchor-free确实省心。
第二,C2f模块替换C3。这是backbone里的核心变化。C2f(Cross Stage Partial with 2 convolutions and f)在保证梯度分流的同时,增强了特征复用。说人话就是:同样的参数量下,特征提取能力更强。这也是为什么yolov8n这么小的模型精度还能打。
第三,解耦头(Decoupled Head)。分类和回归用两个独立分支,而不是共享一个头。分类关心"是什么",回归关心"在哪",两者关注的特征不一样,解耦之后各干各的,收敛更快、精度更高。
第四,Task-Aligned Assigner。正负样本分配策略改了,不再单纯按IoU,而是综合分类得分和定位质量来分配。这让训练更稳定,尤其是密集小目标场景。
第五,DFL回归损失。Distribution Focal Loss把边界框回归从"预测一个值"变成"预测一个分布",对边界模糊的目标更鲁棒。
这些改进点里,C2f和解耦头是最容易做文章的地方,后面讲改进时会重点说。
2.3 整体技术路线
整个项目的链路我画成文字版:
数据采集 → 数据清洗 → 标注(labelme/Roboflow)→ 格式转换(YOLO txt)→ 数据集划分 → 数据增强 → 模型选型(yolov8n/s)→ 训练 → 评估(mAP、PR曲线)→ 改进实验 → 对比 → 导出ONNX → 部署(本地/边缘设备)
每一环都有坑,我按顺序拆。
3. 数据集构建:决定项目上限的关键环节
3.1 类别怎么定,这是第一个大坑
我见过太多人一上来就定"可回收、厨余、有害、其他"四类,然后发现标注的时候根本没法标。为什么?因为**"可回收"是一个抽象类别,不是一个视觉类别**。一个塑料瓶和一张报纸都属于可回收,但它们在图像上长得完全不一样。你让模型学"可回收"这个概念,它学不明白。
我的做法是先定细粒度子类,再映射到大类。比如:
| 细粒度子类 | 映射大类 | 视觉特征 |
|---|---|---|
| 塑料瓶 | 可回收 | 透明/半透明,圆柱形 |
| 易拉罐 | 可回收 | 金属反光,圆柱形 |
| 纸箱 | 可回收 | 棕色,方形,有折痕 |
| 报纸 | 可回收 | 灰白色,平面 |
| 果皮 | 厨余 | 有机质感,不规则 |
| 剩饭 | 厨余 | 混合状,颜色杂 |
| 电池 | 有害 | 小,圆柱/方形,有标识 |
| 药品 | 有害 | 小,包装特征 |
| 烟头 | 其他 | 极小,细长 |
| 陶瓷碎片 | 其他 | 不规则,边缘锐利 |
这样标注的时候,标注员只需要判断"这是塑料瓶还是易拉罐",不用纠结抽象概念。训练完之后,推理时把细粒度类别映射回四大类输出即可。这个设计让标注一致性大幅提升,mAP也明显更高。
3.2 数据从哪来
三个来源,我按推荐度排序。
第一,自采。拿手机在小区垃圾桶、办公室、食堂拍。优势是场景真实、分布匹配你的应用场景。劣势是类别不平衡——塑料瓶到处都是,有害垃圾很难拍到。我的经验是自采至少2000张,覆盖不同光照(白天/夜晚/室内)、不同角度、不同背景。
第二,公开数据集。比较常用的有TrashNet(但它是分类数据集,只有单物体)、TACO(垃圾检测数据集,类别多但标注质量参差)。TACO可以直接用,但要做清洗,去掉模糊和错误标注的图。
第三,合成数据。用CutMix、Copy-Paste把单个物体贴到不同背景上。这个对稀有类别(比如有害垃圾)特别有用。我实测过,用Copy-Paste增强稀有类别后,该类别的AP能提升10个点以上。
3.3 标注工具与规范
标注工具我用过三个:labelImg、labelme、Roboflow。结论是:
- labelImg:最轻量,画框快,适合纯检测任务。缺点是界面老,协作差。
- labelme:支持多边形,适合后续想转分割。但画框效率不如labelImg。
- Roboflow:在线协作,自动格式转换,还能做增强。团队用最舒服,但免费额度有限。
标注规范我定了几条硬规则,直接抄:
- 框要贴紧目标边缘,不留多余背景,也不要切掉目标。
- 遮挡超过50%的目标不标,标了反而干扰训练。
- 同一物体只标一次,不要嵌套框。
- 极小目标(小于20x20像素)单独讨论,要么放大图像,要么放弃。
- 每标完500张做一次交叉校验,两个人标同一批,算IoU一致性。
提示:标注一致性比标注数量重要得多。1000张高质量标注,胜过5000张乱七八糟的标注。我踩过的最大坑就是前期图快,标注质量差,训练时loss怎么都不降,回头返工重标花了三倍时间。
3.4 数据集划分与格式转换
划分比例一般是训练:验证:测试 = 7:2:1。但垃圾数据集有个特殊点:同一场景拍的多张图要分到同一集合,否则验证集里出现训练集同场景的图,指标虚高。我一般按"拍摄批次"划分,而不是随机划分。
格式转换到YOLO格式,每张图对应一个txt,每行是:
class_id center_x center_y width height所有坐标归一化到0-1。转换脚本用Python写,核心逻辑就是读标注文件、算归一化坐标、写txt。这个网上模板很多,但要注意类别id要从0开始连续,中间断了训练会报错。
4. 模型训练:参数怎么调,坑怎么避
4.1 环境配置
我用的环境是Ubuntu 20.04 + Python 3.10 + PyTorch 2.0 + CUDA 11.8。显卡是RTX 3060 12G,跑yolov8n绰绰有余。如果你只有CPU,也能跑,但训练时间会从几小时变成几天,建议先用小数据集(500张)验证流程。
安装ultralytics:
pip install ultralytics验证安装:
from ultralytics import YOLO model = YOLO('yolov8n.pt') print(model.info())注意:不要用最新版的torch配最新版的ultralytics,有时候会有兼容问题。我一般锁定torch 2.0.x + ultralytics 8.0.x这个组合,实测最稳。
4.2 数据配置文件
新建一个garbage.yaml:
path: /home/user/garbage_dataset train: images/train val: images/val test: images/test nc: 10 names: 0: plastic_bottle 1: can 2: carton 3: newspaper 4: peel 5: leftover 6: battery 7: medicine 8: cigarette 9: ceramicnc是类别数,names要和标注时的类别id严格对应。这里错一个,训练全废。
4.3 训练参数详解
启动训练:
from ultralytics import YOLO model = YOLO('yolov8n.pt') results = model.train( data='garbage.yaml', epochs=150, imgsz=640, batch=16, lr0=0.01, lrf=0.01, momentum=0.937, weight_decay=0.0005, warmup_epochs=3, patience=30, device=0, workers=8, project='garbage_runs', name='baseline' )逐个说这些参数为什么这么设:
- epochs=150:垃圾数据集一般5000张以内,150轮足够收敛。太多会过拟合,太少欠拟合。看loss曲线,如果验证loss还在降就继续,平了就停。
- imgsz=640:yolov8默认640,也是精度和速度的平衡点。如果你的目标普遍很小(比如烟头),可以提到1280,但显存翻倍。
- batch=16:12G显存下640分辨率能跑到16。显存不够就降到8,但batch太小BN层统计不准,建议配合梯度累积。
- lr0=0.01:初始学习率。yolov8用SGD,这个值是官方推荐。太大震荡,太小收敛慢。
- lrf=0.01:最终学习率 = lr0 * lrf,即余弦退火到0.0001。
- warmup_epochs=3:前3轮预热,学习率从0慢慢升到0.01,防止一开始梯度爆炸。
- patience=30:30轮没提升就早停,省时间。
- workers=8:数据加载线程数,根据CPU核数调。
4.4 训练过程监控
训练时重点看三个东西:
第一,loss曲线。用results.csv画图:
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('garbage_runs/baseline/results.csv') df.columns = df.columns.str.strip() plt.figure(figsize=(12, 4)) plt.subplot(1, 3, 1) plt.plot(df['train/box_loss'], label='train_box') plt.plot(df['val/box_loss'], label='val_box') plt.legend() plt.title('Box Loss') plt.subplot(1, 3, 2) plt.plot(df['train/cls_loss'], label='train_cls') plt.plot(df['val/cls_loss'], label='val_cls') plt.legend() plt.title('Cls Loss') plt.subplot(1, 3, 3) plt.plot(df['metrics/mAP50(B)'], label='mAP50') plt.plot(df['metrics/mAP50-95(B)'], label='mAP50-95') plt.legend() plt.title('mAP') plt.tight_layout() plt.savefig('training_curves.png')健康的曲线长这样:train loss和val loss同步下降,最后都趋于平缓,两者差距不大。如果train loss一直降但val loss开始上升,就是过拟合,该早停或加数据增强。
第二,mAP曲线。mAP@0.5到0.85以上算及格,0.90以上算好。mAP@0.5:0.95一般比mAP@0.5低15-20个点,正常。
第三,混淆矩阵。训练完会自动生成confusion_matrix.png。重点看哪些类别互相混淆。我实测下来,塑料瓶和易拉罐容易混(都是圆柱形反光),果皮和剩饭容易混(都是有机质)。这两个混淆对是后续改进的重点。
4.5 常见训练问题速查
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| loss不下降 | 学习率太小/标注错误 | 检查标注,调大lr |
| loss震荡剧烈 | 学习率太大/batch太小 | 降lr,增大batch |
| 某类别AP极低 | 样本太少/标注质量差 | 数据增强,重标 |
| 验证mAP远低于训练 | 过拟合 | 加增强,加dropout,减模型 |
| 显存溢出 | batch/imgsz太大 | 降batch或imgsz |
| 训练速度极慢 | workers太少/CPU瓶颈 | 增workers,检查数据加载 |
实操心得:训练前一定先用
model.val()在验证集上跑一遍未训练的模型,确认数据加载和类别映射没问题。我见过太多人训练了8小时才发现类别id对错了,白跑。
5. 算法改进:改哪里,怎么验证有效
5.1 改进思路从哪来
改进不是拍脑袋,要从baseline的失败案例里找方向。我的流程是:
- 跑完baseline,导出所有验证集的预测结果。
- 找出漏检(FN)和误检(FP)最多的类别。
- 分析原因:是小目标?是遮挡?是类别相似?
- 针对性设计改进模块。
我baseline跑下来,主要问题有三个:小目标(烟头、电池)漏检多、相似类别混淆(瓶/罐)、密集场景下框重叠。针对这三点,我做了三个改进。
5.2 改进一:加入注意力机制
在backbone的C2f模块后插入CBAM(Convolutional Block Attention Module)。CBAM包含通道注意力和空间注意力两部分,能让模型更关注有判别力的区域。
为什么有效?垃圾图像里背景杂乱(地面、手、其他垃圾),模型容易被背景干扰。CBAM的空间注意力会抑制背景响应,增强目标区域。我实测在yolov8n上加CBAM,mAP@0.5提升约1.5个点,小目标AP提升更明显。
代码上,在ultralytics/nn/modules/block.py里加CBAM类,然后在parse_model里注册。具体实现网上有,但要注意CBAM加太多会拖慢推理,我一般只在P3、P4两个尺度加。
5.3 改进二:替换损失函数
baseline用CIoU loss做框回归。我换成WIoU(Wise-IoU)。WIoU的核心思想是根据锚框质量动态调整损失权重,质量差的框给低权重,避免它们主导梯度。
为什么有效?垃圾数据集里标注质量参差,有些框画得不准。CIoU对这些差框一视同仁,导致模型被带偏。WIoU能缓解这个问题。实测mAP@0.5提升约0.8个点,训练更稳定。
5.4 改进三:多尺度训练与测试
训练时随机resize输入(320到960之间),测试时用TTA(Test Time Augmentation),把原图、翻转图、多尺度图的预测融合。
为什么有效?垃圾目标尺度差异极大,电池可能只占50像素,纸箱占500像素。多尺度训练让模型见过各种尺度,泛化更好。TTA相当于"多问模型几次,投票决定",能提升1-2个点,但推理时间翻3倍。
5.5 改进效果对比
我在同一测试集上跑了消融实验:
| 模型配置 | mAP@0.5 | mAP@0.5:0.95 | 参数量(M) | 推理时间(ms) |
|---|---|---|---|---|
| yolov8n baseline | 0.852 | 0.671 | 3.2 | 8.5 |
| +CBAM | 0.867 | 0.689 | 3.4 | 9.2 |
| +WIoU | 0.860 | 0.683 | 3.2 | 8.5 |
| +多尺度训练 | 0.871 | 0.695 | 3.2 | 8.5 |
| 三者结合 | 0.891 | 0.712 | 3.4 | 9.2 |
| +TTA(测试) | 0.903 | 0.724 | 3.4 | 27.6 |
结论很清楚:三个改进叠加能把mAP@0.5从0.852推到0.891,加TTA到0.903。代价是推理时间从8.5ms涨到27.6ms。如果部署在边缘设备上,TTA可能不划算,前三个改进是性价比最高的。
注意:改进实验一定要做消融,一个一个加,看每个模块的独立贡献。我见过有人一次性加五个模块,涨了2个点,但根本不知道哪个有用哪个没用,写论文都没法写。
6. 部署落地:从模型到能用的系统
6.1 导出ONNX
训练完的.pt文件不能直接部署到很多平台,先导出ONNX:
from ultralytics import YOLO model = YOLO('garbage_runs/baseline/weights/best.pt') model.export(format='onnx', imgsz=640, simplify=True, opset=12)simplify=True会做图优化,opset=12兼容性最好。导出后用onnxruntime验证一下输出shape对不对。
6.2 本地推理
最简单的部署是Python脚本:
from ultralytics import YOLO import cv2 model = YOLO('best.onnx', task='detect') img = cv2.imread('test.jpg') results = model(img, conf=0.25, iou=0.45) for r in results: for box in r.boxes: cls = int(box.cls[0]) conf = float(box.conf[0]) xyxy = box.xyxy[0].tolist() print(f'{model.names[cls]}: {conf:.2f} at {xyxy}')conf=0.25是置信度阈值,iou=0.45是NMS的IoU阈值。这两个值要根据实际场景调:宁可漏检不可误检就调高conf,宁可误检不可漏检就调低conf。
6.3 边缘设备部署
如果想部署到RK3588这类边缘板子上,流程是:ONNX → RKNN。用RKNN-Toolkit2转换,然后板端用RKNN Runtime推理。这里坑很多,主要是算子支持问题——yolov8的某些算子RKNN不支持,需要替换或自定义。我建议先用官方提供的yolov8转换脚本,跑通了再改。
如果部署到海思芯片(比如hi3516cv610),流程类似,但工具链不同。核心思路都是先转中间格式,再转目标格式,每步验证输出一致性。
6.4 系统集成
一个完整的垃圾分类识别系统,除了模型还需要:
- 图像采集:摄像头或手机上传
- 预处理:resize、归一化
- 推理:模型前向
- 后处理:NMS、类别映射(细粒度→四大类)
- 展示:在原图上画框、标注类别、给出投放建议
我用Gradio快速搭了个demo:
import gradio as gr from ultralytics import YOLO model = YOLO('best.pt') def predict(image): results = model(image, conf=0.25) return results[0].plot() gr.Interface(fn=predict, inputs='image', outputs='image').launch()这个demo虽然简单,但足够演示。如果要产品化,前端用Vue/React,后端用FastAPI,模型用ONNX Runtime或TensorRT加速。
7. 实操避坑与经验总结
7.1 数据层面的坑
坑一:类别不平衡。塑料瓶可能有2000个样本,电池只有50个。直接训练,模型对电池的AP会很低。解决办法:过采样稀有类别 + Copy-Paste增强 + 类别加权损失。我一般把稀有类别过采样到至少300个。
坑二:标注不一致。同一个人标同一张图,两次结果都可能不一样。解决办法:制定详细标注规范 + 定期交叉校验 + 用标注工具的一致性检查功能。
坑三:场景泄漏。同一场景的图分到了训练集和验证集,导致验证指标虚高。解决办法:按拍摄批次划分,不按图片随机划分。
7.2 训练层面的坑
坑一:学习率没调好。太大loss爆炸,太小不收敛。我的经验是先用官方推荐值跑一遍,看loss曲线再微调。如果loss前几轮就爆炸,lr降10倍;如果loss几乎不动,lr升3倍。
坑二:数据增强过度。yolov8默认开了mosaic、mixup、HSV增强。如果数据集本身就很杂,再开强增强反而有害。我一般前期开强增强,后期关掉mosaic,让模型在真实分布上收敛。
坑三:早停太早。patience设太小,模型还没收敛就停了。垃圾数据集我建议patience至少30,最好50。
7.3 部署层面的坑
坑一:训练和推理的预处理不一致。训练时用了某种归一化,推理时忘了,结果精度暴跌。解决办法:把预处理封装成函数,训练和推理共用。
坑二:ONNX导出后精度下降。一般是算子不支持或精度损失。解决办法:导出后逐层对比输出,找出偏差大的层。
坑三:边缘设备算力不够。yolov8n在RK3588上大概能跑30FPS,但如果用yolov8m就只有10FPS。解决办法:根据设备算力选模型尺寸,必要时做量化(INT8)。
7.4 我的经验清单
最后列几条我踩坑踩出来的经验,直接抄:
- 数据集质量 > 模型改进 > 训练技巧。别本末倒置。
- 先跑通全流程,再优化细节。用100张图跑通训练-评估-部署,再上全量数据。
- 每次实验只改一个变量。改了学习率又改增强,涨了也不知道是谁的功劳。
- 保存所有实验配置和结果。用wandb或tensorboard,别靠脑子记。
- 验证集指标好不代表实际好用。一定要在真实场景拍图测试,看漏检误检。
- 模型不是越大越好。yolov8n在垃圾检测上够用,上大模型收益递减。
- 部署前一定做端到端测试。从摄像头到输出,整条链路跑通再交付。
这个项目我从数据采集到部署落地大概花了三周,其中两周在搞数据。如果你也想做,我的建议是:先把数据这一关过了,模型部分反而是最简单的。yolov8的生态太成熟了,训练脚本改改参数就能跑,真正拉开差距的是你对场景的理解和对数据的把控。