
1. 从一堆垃圾照片说起这个项目到底在做什么去年夏天我接手了一个社区垃圾分类督导的数字化改造小项目。说白了就是小区里那排智能垃圾桶旁边装了个摄像头居民扔垃圾的时候系统得自动判断手里拿的是不是可回收物、有没有混投。一开始我们用的是最朴素的办法——人工看监控回放结果三天下来督导员眼睛都快瞎了误判率还高得离谱。后来我决定上目标检测模型选来选去最终落在了YOLOv8上并且针对生活垃圾这个特定场景做了一轮改进。这个项目的核心就是基于深度学习的垃圾分类识别目标检测系统。它要解决的问题很具体给定一张生活垃圾的图像系统需要同时完成两件事——定位垃圾在画面中的哪个位置用边界框标出来和分类这个垃圾属于可回收物、厨余垃圾、有害垃圾还是其他垃圾。这跟普通的图像分类不一样分类只告诉你“这张图里有塑料瓶”而目标检测要告诉你“画面左下角那个塑料瓶是可回收物右上角那团纸巾是其他垃圾”。适合谁来参考这篇内容如果你正在做YOLOv8训练自己的数据集、想了解目标检测算法在真实场景中的落地细节、或者单纯对深度学习图像识别感兴趣那这篇东西应该能帮你省下不少查文档和踩坑的时间。我会把从数据准备、模型改进、训练调参到部署上线的完整链路拆开讲重点放在那些官方文档里不会写、但实际做项目时一定会遇到的细节上。2. 为什么选YOLOv8以及我为什么还要改它2.1 目标检测算法的选型逻辑做目标检测市面上主流的路子就那么几条两阶段的Faster R-CNN系列、单阶段的SSD和YOLO系列、还有基于Transformer的DETR家族。我选YOLOv8理由很直接速度与精度的平衡垃圾分类是个实时性要求不低的场景垃圾桶旁边的摄像头得在居民扔完垃圾之前给出反馈。YOLOv8在COCO数据集上的mAP和推理速度在同等参数量下基本是第一梯队。工程化成熟度Ultralytics这个库的封装做得太舒服了从训练到导出ONNX、TensorRT一条命令的事。相比之下DETR系列虽然精度不错但训练收敛慢小目标检测也容易翻车。社区生态遇到问题能搜到答案。你搜“yolov8训练自己的数据集”能出来几百篇教程虽然质量参差不齐但至少说明用的人多。但直接用官方预训练的YOLOv8n或者YOLOv8s在垃圾分类场景下有几个明显的问题。第一生活垃圾的类别间差异有时候非常细微——比如“用过的纸巾”和“干净的纸板”在像素层面可能都是灰白色的块状物模型很容易混淆。第二垃圾图像里小目标特别多比如烟头、瓶盖、药片板这些在640×640的输入分辨率下经过多次下采样后特征几乎消失。第三实际场景的光照条件恶劣垃圾桶旁边经常是背光或者夜间补光图像质量不稳定。2.2 我做的几处关键改进针对上面这些问题我在YOLOv8的基础上做了三处改动实测下来对垃圾分类场景的提升比较明显。第一处是注意力机制的引入。我在Backbone的C2f模块后面加了协调注意力机制Coordinate Attention。普通的SE注意力只关注通道维度但垃圾图像里很多关键信息是空间相关的——比如一个瓶子它的瓶身和瓶盖在空间上是分离的但语义上是一个整体。CoordAttention把通道注意力分解成两个一维的特征编码分别沿水平和垂直方向聚合信息这样既能捕获通道间的依赖又能保留空间位置信息。代码上就是在ultralytics/nn/modules/block.py里加了一个CoordAtt模块然后在yaml配置文件里把对应的C2f替换掉。第二处是Head部分的改进。官方YOLOv8的检测头是解耦的分类和回归分支分开这个设计本身没问题。但我发现对于垃圾这种类内差异大、类间差异小的数据分类分支的感受野需要更大一些。我把分类分支的卷积核从3×3换成了5×5同时增加了一个额外的下采样特征层专门用来检测那些特别小的目标。这个改动会增加一些参数量但换来的是小目标召回率提升了大概7个百分点。第三处是数据增强策略的调整。官方默认的Mosaic增强对垃圾分类其实不太友好——它把四张图拼在一起容易让模型学到一些不存在的上下文关系。比如一张图里同时出现香蕉皮和电池模型可能会误以为它们经常一起出现。我改成了Mosaic和MixUp交替使用并且在最后10个epoch关掉Mosaic让模型在真实分布上做微调。另外针对光照问题我加了随机Gamma校正和CLAHE限制对比度自适应直方图均衡化模拟不同光照条件下的垃圾外观。注意改进不是越多越好。我试过同时加注意力、换Head、改损失函数结果训练直接不收敛。后来一个一个加每次只改一个地方观察验证集指标的变化才找到真正有效的组合。3. 数据准备垃圾分类数据集的构建与清洗3.1 数据来源与类别定义垃圾分类的数据集公开的其实不少但质量参差不齐。我主要用了两个来源一个是某环保组织公开的垃圾图像库大概有8000张另一个是自己用手机在小区垃圾桶旁边拍的大概3000张。公开数据集的好处是标注相对规范坏处是场景单一很多都是在白色背景下的摆拍跟实际垃圾桶旁边的环境差距很大。自己拍的数据更真实但标注得从头来。类别定义上我按照通用的四分类法可回收物、厨余垃圾、有害垃圾、其他垃圾。但实际标注的时候发现有些东西的归属很模糊。比如“一次性餐盒”如果干净的是可回收物如果沾了油污就是其他垃圾。这种边界情况我最后统一按“其他垃圾”处理因为在实际督导场景里宁可让居民把可回收物扔进其他垃圾也不能让污染物混进可回收物。最终的数据集规模是11000张图像类别分布如下类别训练集验证集测试集总计可回收物38505505504950厨余垃圾24503503503150有害垃圾9801401401260其他垃圾12801801801640合计85601220122011000这个分布是不均衡的可回收物最多有害垃圾最少。不均衡带来的问题是模型会偏向多数类对有害垃圾的识别率很低。我的处理方式是在损失函数里给少数类更高的权重具体来说在YOLOv8的BCE分类损失里给每个类别的正样本损失乘以一个权重系数权重跟类别频率成反比。这个系数不是拍脑袋定的我用了sklearn.utils.class_weight.compute_class_weight算了一个初始值然后根据验证集上的混淆矩阵微调。3.2 标注规范与质量控制标注用的是LabelImg格式是YOLO的txt格式每行是class_id x_center y_center width height坐标都归一化到0到1之间。标注的时候有几个坑我踩过边界框不要贴太紧有些标注员喜欢把框画得刚刚好包住物体但这样在数据增强的时候一旦做随机裁剪或者缩放物体边缘容易被切掉。我要求框比物体实际边界外扩5%左右。遮挡处理垃圾堆叠是常态一个瓶子被另一个瓶子挡住一半这种情况我要求只要可见部分超过30%就标否则不标。标的时候框只框可见部分不要脑补被遮挡的部分。小目标单独检查烟头、瓶盖、药片这些小于32×32像素的目标我专门抽了500张出来人工复核确保没有漏标。漏标比错标更致命因为模型会把漏标的目标当成背景学到错误的特征。标注完成后我写了一个脚本做一致性检查遍历所有标注文件检查坐标是否越界、宽高是否为零、类别ID是否在有效范围内。这个脚本帮我抓出了大概200个有问题的标注文件主要是坐标越界和宽高为负。3.3 数据增强的实操配置YOLOv8的数据增强配置在data.yaml和训练参数里。我的配置是这样的# data.yaml path: ./dataset train: images/train val: images/val test: images/test names: 0: recyclable 1: kitchen_waste 2: hazardous_waste 3: other_waste训练时的增强参数from ultralytics import YOLO model YOLO(yolov8s.yaml) model.train( datadata.yaml, epochs200, imgsz640, batch16, mosaic1.0, # 前190个epoch开启Mosaic mixup0.15, # MixUp概率 copy_paste0.1, # 复制粘贴增强 degrees10.0, # 随机旋转角度 translate0.1, # 随机平移 scale0.5, # 随机缩放 shear2.0, # 随机剪切 perspective0.0005,# 透视变换 flipud0.0, # 上下翻转关闭垃圾图像上下翻转不合理 fliplr0.5, # 左右翻转 hsv_h0.015, # 色调抖动 hsv_s0.7, # 饱和度抖动 hsv_v0.4, # 明度抖动 close_mosaic10, # 最后10个epoch关闭Mosaic )这里重点说几个参数的选择理由。mosaic1.0表示100%的概率做Mosaic增强但我在最后10个epoch用close_mosaic10关掉了原因是Mosaic会让图像的真实分布发生偏移最后阶段需要让模型在真实分布上收敛。mixup0.15是一个比较保守的值MixUp太强会导致图像语义模糊垃圾本来就难分再模糊就更分不清了。flipud0.0是因为垃圾图像上下翻转后不符合物理常识比如一个倒置的瓶子看起来很奇怪模型学到的特征没有意义。4. 模型训练参数调优与损失曲线解读4.1 环境配置与训练硬件训练环境这块我试过两种配置本地一台带GTX 1660 Ti的机器和云上的A100。GTX 1660 Ti跑YOLOv8sbatch size只能开到8一个epoch大概要4分钟200个epoch下来十几个小时。A100上batch size开到64一个epoch不到1分钟但成本高。对于这个项目我建议如果是学习目的GTX 1660 Ti完全够用只是慢一点如果是赶项目进度云上租一张A100或者3090会舒服很多。环境配置的步骤# 创建虚拟环境 conda create -n yolov8 python3.9 conda activate yolov8 # 安装PyTorch根据CUDA版本选择 pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cu118 # 安装Ultralytics pip install ultralytics # 验证安装 yolo checksyolo checks会输出当前环境的信息包括PyTorch版本、CUDA是否可用、GPU型号等。如果CUDA不可用检查一下驱动版本和PyTorch的CUDA版本是否匹配。4.2 训练参数的含义与调优YOLOv8的训练参数很多我挑几个对垃圾分类场景影响最大的说。学习率lr0初始学习率默认是0.01我用的是0.001。原因是垃圾分类数据集不大11000张图学习率太大会导致损失震荡。我用了一个简单的策略先跑50个epoch观察损失曲线如果震荡厉害就减半如果下降太慢就加倍。最终定在0.001配合余弦退火调度训练很稳。权重衰减weight_decay默认0.0005我调到了0.001。垃圾分类的类别间差异小模型容易过拟合到训练集的特定纹理上增大权重衰减相当于加了一个L2正则有助于泛化。** warmup_epochs**默认3我改成了5。warmup阶段学习率从0线性增加到初始学习率让模型在训练初期不要更新太猛。垃圾分类的预训练权重是在COCO上学的跟垃圾图像分布差异大warmup长一点更稳。box损失权重和cls损失权重YOLOv8默认box7.5cls0.5。我调成了box5.0cls1.5。原因是垃圾分类更看重分类准确率定位稍微偏一点问题不大但类别分错了就是大问题。增大cls权重让模型更关注分类分支的优化。训练轮数epochs我设了200但实际在150左右就收敛了。判断收敛的方法是看验证集的mAP50如果连续20个epoch没有提升就可以停了。YOLOv8自带早停机制patience50我设成了30。4.3 损失曲线与指标解读训练过程中YOLOv8会输出几个关键指标box_loss、cls_loss、dfl_loss、precision、recall、mAP50、mAP50-95。我一般会画损失函数曲线图来观察训练状态。import pandas as pd import matplotlib.pyplot as plt # 读取训练结果 results pd.read_csv(runs/detect/train/results.csv) fig, axes plt.subplots(2, 2, figsize(12, 10)) # 损失曲线 axes[0, 0].plot(results[epoch], results[train/box_loss], labeltrain_box) axes[0, 0].plot(results[epoch], results[val/box_loss], labelval_box) axes[0, 0].set_title(Box Loss) axes[0, 0].legend() axes[0, 1].plot(results[epoch], results[train/cls_loss], labeltrain_cls) axes[0, 1].plot(results[epoch], results[val/cls_loss], labelval_cls) axes[0, 1].set_title(Classification Loss) axes[0, 1].legend() # mAP曲线 axes[1, 0].plot(results[epoch], results[metrics/mAP50(B)], labelmAP50) axes[1, 0].plot(results[epoch], results[metrics/mAP50-95(B)], labelmAP50-95) axes[1, 0].set_title(mAP) axes[1, 0].legend() # 精确率和召回率 axes[1, 1].plot(results[epoch], results[metrics/precision(B)], labelprecision) axes[1, 1].plot(results[epoch], results[metrics/recall(B)], labelrecall) axes[1, 1].set_title(Precision Recall) axes[1, 1].legend() plt.tight_layout() plt.savefig(training_curves.png)看损失曲线有几个经验训练损失和验证损失同步下降说明没有过拟合如果训练损失继续降但验证损失开始升就是过拟合了需要加正则或者减模型复杂度。分类损失如果下降很慢说明类别区分度不够可能需要检查标注质量或者增加分类分支的容量。我最终的模型在测试集上的指标是mAP500.892mAP50-950.673精确率0.901召回率0.856。其中有害垃圾的召回率最低只有0.78主要原因是样本太少而且有害垃圾电池、药品、灯管的外观差异很大模型很难学到统一的特征。5. 部署落地从PyTorch到RK3588的完整链路5.1 模型导出与格式转换训练完的模型是PyTorch的.pt格式要部署到边缘设备上需要转成ONNX或者TensorRT。我用的边缘设备是RK3588它支持RKNN格式所以链路是PyTorch - ONNX - RKNN。from ultralytics import YOLO # 加载训练好的模型 model YOLO(runs/detect/train/weights/best.pt) # 导出ONNX model.export(formatonnx, imgsz640, simplifyTrue, opset12)导出ONNX的时候有几个坑。simplifyTrue会调用onnx-simplifier做图优化去掉一些冗余节点但有时候会引入一些不支持的算子。opset12是比较稳的版本太高了RKNN可能不支持。导出后最好用onnxruntime跑一下推理确认输出跟PyTorch一致。import onnxruntime as ort import numpy as np session ort.InferenceSession(best.onnx) input_name session.get_inputs()[0].name dummy np.random.randn(1, 3, 640, 640).astype(np.float32) output session.run(None, {input_name: dummy}) print(output[0].shape) # 应该是 (1, 84, 8400) 对于4类5.2 RK3588上的部署实操RK3588的部署需要用到RKNN-Toolkit2。首先在PC上把ONNX转成RKNNfrom rknn.api import RKNN rknn RKNN(verboseTrue) # 配置 rknn.config( mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3588, quantized_dtypeasymmetric_quantized-8, ) # 加载ONNX ret rknn.load_onnx(modelbest.onnx) if ret ! 0: print(Load ONNX failed) exit(ret) # 构建 ret rknn.build(do_quantizationTrue, dataset./quant_dataset.txt) if ret ! 0: print(Build failed) exit(ret) # 导出 ret rknn.export_rknn(best.rknn)量化数据集quant_dataset.txt里放的是校准图像路径一般准备100到200张有代表性的垃圾图像就行。量化后的模型大小会缩小到原来的四分之一左右推理速度提升明显但精度会掉一点。我实测量化后mAP50掉了大概2个百分点从0.892降到0.871可以接受。在RK3588上跑推理用的是RKNN的C API或者Python API。Python API适合快速验证from rknnlite.api import RKNNLite rknn_lite RKNNLite() rknn_lite.load_rknn(best.rknn) rknn_lite.init_runtime(core_maskRKNNLite.NPU_CORE_0_1_2) # 推理 img cv2.imread(test.jpg) img cv2.resize(img, (640, 640)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) outputs rknn_lite.inference(inputs[img])RK3588有三个NPU核心core_mask可以指定用哪几个。对于实时视频流我建议用NPU_CORE_0_1_2三个核心一起跑帧率能到30fps以上。如果只是单张图片推理用一个核心就够了省电。5.3 后处理与业务逻辑对接模型输出的原始结果是8400个预测框每个框有84个值4个坐标4个类别分数对于4类。后处理需要做三件事置信度过滤、NMS去重、坐标还原。def postprocess(outputs, conf_thres0.25, iou_thres0.45): # outputs shape: (1, 84, 8400) predictions outputs[0].transpose(1, 0) # (8400, 84) # 置信度过滤 scores predictions[:, 4:] max_scores np.max(scores, axis1) mask max_scores conf_thres predictions predictions[mask] max_scores max_scores[mask] # 坐标转换 boxes predictions[:, :4] boxes_xyxy xywh2xyxy(boxes) # NMS indices nms(boxes_xyxy, max_scores, iou_thres) return boxes_xyxy[indices], max_scores[indices], np.argmax(scores[mask][indices], axis1)业务逻辑上我把检测结果映射到四个垃圾桶的类别然后通过串口发给控制板控制对应的桶盖打开。这里有个细节如果一张图里检测到多个类别的垃圾我取置信度最高的那个类别而不是让多个桶同时打开。因为实际场景里居民一次通常只扔一种垃圾。6. 踩坑记录与常见问题速查6.1 训练阶段的典型问题问题一损失不收敛box_loss一直在0.5以上震荡。排查下来是学习率太大而且warmup不够。把lr0从0.01降到0.001warmup_epochs从3加到5问题解决。另外检查一下标注文件里有没有坐标越界的越界的标注会导致损失计算异常。问题二mAP50很高但mAP50-95很低。这说明模型能检测到物体但边界框不够准。原因是标注框的质量不高或者box损失权重太低。我把box权重从7.5调到5.0反而更差了后来调回7.5同时用了一个标注复核脚本重新检查了一遍边界框mAP50-95从0.58提升到0.67。问题三验证集损失比训练集损失还低。这通常是因为验证集的数据分布比训练集简单或者验证集的增强比训练集弱。检查一下验证集是不是用了跟训练集不同的预处理确保两者一致。6.2 部署阶段的典型问题问题一ONNX导出后推理结果跟PyTorch不一致。最常见的原因是输入归一化方式不同。YOLOv8的PyTorch模型内部做了0-1归一化但导出ONNX后这个操作可能被融合或者丢失。检查ONNX的输入是否需要手动除以255。问题二RKNN量化后精度掉太多。量化校准集的选择很关键。如果校准集里全是可回收物的图像模型对厨余垃圾的量化误差就会很大。校准集要覆盖所有类别而且最好包含一些困难样本比如光照差的、遮挡的。问题三RK3588上推理速度慢。检查一下是不是用了CPU推理而不是NPU。init_runtime的时候指定core_mask确保用的是NPU核心。另外输入图像的预处理resize、归一化如果在CPU上做也会拖慢速度可以考虑用RK3588的RGA硬件加速。6.3 常见问题速查表问题现象可能原因排查方法解决方案损失震荡不收敛学习率过大观察loss曲线降低lr0增加warmupmAP50高但mAP50-95低边界框不准可视化预测框检查标注质量调整box权重某类别召回率极低样本不均衡看混淆矩阵增加该类样本调整类别权重ONNX推理结果异常预处理不一致对比PyTorch输出检查归一化手动对齐RKNN量化精度掉太多校准集不具代表性对比量化前后mAP扩充校准集覆盖所有类别边缘设备推理慢未用NPU查看运行时日志指定NPU核心用RGA加速预处理最后再分享一个小技巧训练的时候开plotsTrueYOLOv8会自动生成混淆矩阵、PR曲线、F1曲线等图表。这些图比数字更能说明问题。比如混淆矩阵里如果“可回收物”和“其他垃圾”之间的误判很多说明这两个类别的特征区分度不够可能需要重新审视类别定义或者增加更多区分性强的样本。这个项目从数据采集到部署上线前前后后折腾了大概两个月。最大的体会是目标检测在真实场景里的难点从来不是模型结构本身而是数据质量和场景理解。YOLOv8已经足够强大但如果你喂给它的数据是脏的、标注是乱的、类别定义是模糊的再好的模型也救不回来。反过来把数据做干净把场景想清楚哪怕用YOLOv8n这种小模型也能跑出不错的效果。