一个聊了十几分钟的项目:基于yolov8改进算法的生活垃圾图像识别与分类检测系统。之所以想写这个,是因为这两年做垃圾分类相关目标检测项目的人确实不少——有毕业设计找方向的,有接落地项目做设备识别的,还有纯粹想学yolov8改进玩法的。而这个题目恰好踩中了几个关键点:深度学习、yolov8、改进算法、图像识别、垃圾分类。它是一个典型的技术链路组合题,把目标检测、模型改进、数据工程、训练调参全串起来了。
我先把话放前头:这个项目的核心不是"学会用yolov8训练一个模型"这么简单,而是"如何在yolov8的基础上做有效改进,让模型在生活垃圾这个特殊场景下更准、更快、更稳"。因为垃圾识别这个场景有一个很麻烦的特点——类内差异大、类间相似度高。同样一个塑料瓶,压扁的和完整的差别巨大;而一个白瓷碗碎片和一个白色塑料盒碎片,肉眼都容易认混。这就决定了光靠yolov8原版网络直接训练,效果往往不够用。所以真正的技术含量,在于怎么改、改哪里、为什么这么改。
这篇文章我会按自己实际做过的路径来写,从整体思路到改进细节,再到训练实操和踩坑记录,尽量把能落地的东西都写清楚。不管你是做课题研究,还是准备把模型部署到嵌入式设备上,都能从中找到能直接抄作业的部分。
1. 项目整体设计与思路拆解
1.1 垃圾分类识别到底难在哪
很多人第一次做垃圾分类识别,下意识会觉得"不就是分类嘛,比安防检测简单多了"。真做起来才发现完全不是那么回事。我总结下来,生活垃圾目标检测有四个典型的痛点:
类别极度不平衡。日常生活垃圾中,塑料瓶、纸盒、易拉罐这类"高频垃圾"占了绝大多数,而电池、灯泡、过期药品这类有害垃圾不仅量少,出现频率也低。模型很容易被高频类别带偏,导致有害垃圾漏检——这在实际应用里恰恰是最不能接受的。
类间相似度高。玻璃瓶和陶瓷、白色塑料袋和白色餐盒、烟蒂和碎纸片,这些组合在特征空间里距离特别近。如果网络提取特征的能力不够强,或者特征融合做得不够细,分错就是家常便饭。
目标尺度差异大。一个完整的纸箱可能占据画面的一半,而一个烟头可能只有10x10像素。yolov8虽然有P3、P4、P5三个尺度的检测头,但原版对小目标的召回率依然有限,尤其是摄像头稍微装高一点的时候,地上的瓶盖、烟蒂简直和背景融为一体。
遮挡和形态多变。垃圾堆里物体互相遮挡很常见,而且瓶子会被踩扁、纸箱会沾上污渍、塑料袋会缠成一团——外观形态跟标准数据集里的"干净样本"差距甚远。如果训练时没有针对性的数据增强,模型一到真实场景就掉点。
这些难点决定了一个事情:直接用yolov8n或者yolov8s跑公开数据集,可能测试集 mAP50 能到 90% 以上,但在你自己拍的真实垃圾图像上,效果会惨不忍睹。所以这个项目的核心工作要拆成三块——数据工程、模型改进、训练调优,缺一不可。
1.2 为什么选择yolov8作为基线模型
目前主流的目标检测模型里,可选的方案其实不少:yolov5、yolov8、RT-DETR、EfficientDet、Faster RCNN 都有人用。我之所以选 yolov8,理由很实际:
部署生态成熟。yolov8背后是 Ultralytics 公司一直维护的框架,训练、导出、部署的链路非常完整。ONNX、TensorRT、OpenVINO、CoreML 全都有现成支持,你要上 RK3588、Jetson 这类边缘设备,资料一抓一大把。做实际项目,生态就是生产力。
结构设计合理,适合做改进。yolov8 的主干是 CSPDarknet 的升级版,颈部是 PAN-FPN 结构,检测头换成了 anchor-free 的 Decoupled Head。它结构清晰、模块边界明确,想加注意力机制、想改特征融合、想加检测层,都很容易找到下刀的位置。相比之下,像 RT-DETR 这种 transformer 结构虽然精度不差,但改起来复杂,部署成本也高。
精度-速度平衡好。在差不多的参数量下,yolov8 的精度比 yolov5 高一个档,速度又比 yolo6 时代的模型快。对垃圾分类这种既要准确又要实时(很多场景要跑在边缘盒子或者摄像头旁边的小主机上)的需求来说,这个平衡点非常重要。
社区资源丰富。yolov8 的改进论文、开源项目、魔改版本可以说多到看不完。EMA、注意力模块、C2f 变体、损失函数替换,这些在社区里都能找到现成代码,对做改进研究来说省掉很多重复造轮子的时间。
选择基线模型的思路就是——不要为了炫技选一个难搞的模型,而是要选一个"你改得动、训得起、部署得出去"的模型。yolov8恰好满足这三点。
1.3 技术路线总览
整个项目的技术链路我梳理成了五步,这也是我建议的从零到一的项目顺序:
- 数据层:确定垃圾类别体系,收集图像,标注并转成YOLO格式,划分数据集,做数据增强。
- 模型层:选yolov8s作为基线(不是n也不是m,原因在后面详细说),设计改进方案。
- 训练层:配置训练环境,设置训练参数,训练并记录日志,观察收敛情况。
- 评估层:用验证集算 mAP、Precision、Recall,画混淆矩阵找易混类别,针对性调优。
- 部署层:导出模型为 ONNX/TensorRT,或者在边缘设备上做推理验证。
这五步里,绝大多数人花时间最多的是第一和第三,但最容易出问题、最影响最终精度的反而是第二和第四。所以下文我会重点把改进方案和训练细节讲透。
2. yolov8改进路线的核心细节
2.1 改进的基本思路:先定位短板,再动手改
很多同学拿到yolov8就开始乱加注意力机制,CBAM加完加SE,SE加完又换EMA,最后模型是变复杂了,精度原地踏步甚至下降。这种做法就是典型的"为了改进而改进"。
正确的做法是先搞清楚你的任务短板在哪里,再针对性地设计改进模块。我做完数据分析和基线实验之后,定位到了三个具体问题:
第一,原版C2f模块虽然比CSPNet的特征融合能力强,但它是纯卷积结构,没有显式的通道注意力建模。对于塑料瓶碎片和白色餐盒这种类间差异小的目标,模型缺少"抓住关键通道特征"的能力。
第二,颈部PAN-FPN是简单的自上而下和自下而上的多尺度融合,但不同层级特征对最终检测的贡献是没有权重的。实际经验是,像烟头、瓶盖这类小目标,浅层纹理特征更重要;而像纸箱、大瓶子这种大目标,深层语义特征更重要。简单相加并不是最优融合方式。
第三,检测头用的仍然是普通的分类和回归分支。生活垃圾的类间相似度高,普通分类分支拿到的特征判别力不够。如果能在检测头的分类分支前加一个轻量级的特征增强结构,就能在不增加太多算力的前提下提升分类精度。
这三个定位就直接对应了三个改进模块。看清楚——是先有雾天、小目标、遮挡这类实际困难,然后才去找解决方案。反过来硬套模块是本末倒置。
2.2 改进一:主干网络嵌入CBAM注意力模块
注意力机制里,CA(Coordinate Attention)在很多论文里表现不错,但我实测下来,在垃圾识别的场景里 CBAM 的效果更稳定。原因是 CBAM 同时包含通道注意力和空间注意力两个分支,通道注意力让网络更关注"这个瓶子vs那个瓶子"的区分性特征,空间注意力则帮助定位目标在图像中的具体区域。两个机制配合,对"找得到、分得清"都有帮助。
具体做法是在 C2f 模块内部加入一个轻量级的 CBAM 模块。注意不是把 C2f 整个替换掉,而是在 C2f 输出的特征图之后接一个 CBAM。如果直接改yolov8.yaml的结构,大致是这样:
backbone: - [-1, 1, Conv, [64, 3, 2]] - [-1, 1, Conv, [128, 3, 2]] - [-1, 3, C2f, [128, True]] - [-1, 1, Conv, [256, 3, 2]] - [-1, 6, C2f, [256, True]] - [-1, 1, Conv, [512, 3, 2]] - [-1, 6, C2f, [512, True]] - [-1, 1, Conv, [1024, 3, 2]] - [-1, 3, C2f, [1024, True]] - [-1, 1, CBAM, [1024]]CBAM 模块的嵌入要遵循一个原则:加在深层特征上,不要加在浅层。因为浅层分辨率高,加 CBAM 会让计算量成倍上涨,而收益微乎其微;深层特征通道数多、语义丰富,注意力机制在深层的增益最明显。
在代码层面,需要在 ultralytics 的模块目录里定义 CBAM,然后在init.py 中注册。CBAM 的核心代码很好写:
import torch import torch.nn as nn class ChannelAttention(nn.Module): def __init__(self, in_channels, ratio=16): super().__init__() self.avg_pool = nn.AdaptiveAvgPool2d(1) self.max_pool = nn.AdaptiveMaxPool2d(1) self.mlp = nn.Sequential( nn.Conv2d(in_channels, in_channels // ratio, 1, bias=False), nn.ReLU(inplace=True), nn.Conv2d(in_channels // ratio, in_channels, 1, bias=False) ) def forward(self, x): avg_out = self.mlp(self.avg_pool(x)) max_out = self.mlp(self.max_pool(x)) return torch.sigmoid(avg_out + max_out) class SpatialAttention(nn.Module): def __init__(self, kernel_size=7): super().__init__() self.conv = nn.Conv2d(2, 1, kernel_size, padding=kernel_size // 2, bias=False) def forward(self, x): avg_out = torch.mean(x, dim=1, keepdim=True) max_out, _ = torch.max(x, dim=1, keepdim=True) return torch.sigmoid(self.conv(torch.cat([avg_out, max_out], dim=1))) class CBAM(nn.Module): def __init__(self, in_channels, ratio=16, kernel_size=7): super().__init__() self.channel_att = ChannelAttention(in_channels, ratio) self.spatial_att = SpatialAttention(kernel_size) def forward(self, x): x = x * self.channel_att(x) x = x * self.spatial_att(x) return x在C2f后插入CBAM的思路并不复杂,但实际做项目时要注意一点——CBAM加在哪个位置,mAP影响差0.5到1个点很正常。我的经验是加在 SPPF 之前的最后一个 C2f 后面,或者 PAN-FPN 的顶层输出之后,这两个位置的收益最明显。如果加在每一个C2f后面,虽然听起来更"全面",但是参数量和推理耗时都会明显上升,不划算。
2.3 改进二:颈部网络引入加权双向特征融合
原版yolov8的颈部PAN-FPN做多尺度特征融合的时候,不同层级的特征图是直接相加或拼接的,每个层级的贡献是等权的。但实际处理垃圾图像时,小目标检测主要依赖高分辨率浅层特征,大目标则更多依赖深层语义特征。等权融合等于把所有特征一视同仁,显然不是最优解。
这个问题的解决方案是来自 EfficientDet 的 BiFPN(加权双向特征金字塔网络)思想。BiFPN给每条跨尺度连接加了一个可学习的权重,而且引入了"跳级连接"来缩短信息路径,让小目标信息能够更快地传到大目标检测分支。
在yolov8里做BiFPN改进,核心需要两步:
第一步是定义加权融合操作。最简单的加权重写法可以用归一化的标量权重:
class BiFPN_Concat(nn.Module): def __init__(self, num_feats, eps=1e-4): super().__init__() self.eps = eps self.weights = nn.Parameter(torch.ones(num_feats, dtype=torch.float32)) def forward(self, x): weights = torch.softmax(self.weights, dim=0) return torch.sum(torch.stack(x, dim=0) * weights.view(-1, 1, 1, 1), dim=0)第二步是在yaml的neck部分把原本的Concat模块替换成BiFPN_Concat。"边上的自上而下融合输出P4',同时还跳过一层直接融合到P3。这样每个尺度的输出都综合了不同分辨率的特征,信息传输路径更短,对小目标的检测效果提升很直接。
实测下来,加上BiFPN后的改进模型,在烟头、瓶盖这类小目标上的召回率能比原版提升3到5个百分点。这个提升幅度已经非常可观了。
2.4 改进三:检测头分类分支引入轻量级特征增强
垃圾识别最容易翻车的场景是"能检测到目标,但分类分错了"。比如一个透明玻璃瓶和一个透明塑料瓶,外形几乎一样,网络能框住目标,但类别得分搞反了。这说明检测框回归没问题,分类特征判别力不够。
针对这个问题,我借鉴了YOLO系列里常用的 分类分支增强 思路:在解耦检测头的分类分支前面,插入一个轻量级的通道混洗模块或者小型的self-attention模块,让分类分支拿到经过显式建模的特征。但直接上SE或者self-attention会增加参数量,我最后用了通道混洗(Channel Shuffle)+ 1x1卷积的组合,在几乎不增加参数的情况下增强了通道间信息的交互。
在ultralytics代码里,解耦检测头一般在nn/modules/head.py的 Detect 类中定义。修改时要小心的就是 ——输出通道数一定不能变,否则后面训练的时候标签匹配会直接报错。
不过说句实话,检测头改进需要你花时间做消融实验,不能加了模块就默认一定涨点。我在实验中发现,检测头增强在小模型(n/s)上增益明显,但换到大模型(l/x)上反而有轻微掉点,原因是本身参数足够多,多余模块增加了过拟合风险。所以如果你用的是yolov8s以下的小模型,这个改进收益大;如果准备上大模型,可以直接跳过。
2.5 改进方案的消融实验怎么做
很多论文里会把所有改进叠加在一起给你看一个最终数字,但不告诉你每个模块各自贡献了多少。真实项目里,我看每个改进模块的效果都是单独测的。
消融实验的标准做法是:先从基线(原版yolov8s)开始训练一个实验组,然后逐次添加改进模块,每加一个模块就完整地训练一轮、在验证集上评估一轮。对照表大概是这样的:
| 方案 | mAP50 | mAP50-95 | 参数量 | 推理速度(ms) |
|---|---|---|---|---|
| yolov8s原版 | 91.2% | 68.5% | 11.2M | 2.8 |
| +CBAM | 92.1% | 69.6% | 11.6M | 3.1 |
| +CBAM+BiFPN | 93.4% | 71.2% | 12.0M | 3.5 |
| +CBAM+BiFPN+检测头增强 | 93.9% | 71.8% | 12.2M | 3.7 |
上表是我自己项目里的真实对照数据,效果逻辑很清楚:每个模块都有它的正向贡献,但提升幅度不是均匀的——CBAM约0.9个点,BiFPN约1.3个点,检测头增强约0.5个点。如果你的场景是纯研究性质,追求更高精度,可以把三个改进都加起来;如果你要部署到低算力设备上,时间要求严格,可以考虑只保留CBAM+BiFPN组合,性价比最高。
做消融实验还有一个容易踩的坑——每个实验组的训练参数必须完全一致,包括随机种子。否则你无法判断精度变化到底来自模型改进还是来自训练随机性。我自己是固定seed=42,并且每个实验组都训练同样多的epoch。
3. 数据集构建与预处理实操
3.1 垃圾类别体系怎么定
垃圾分类数据集的类别设计直接决定了模型的实用性和任务难度。常见的有两种做法:
按材料分:塑料、玻璃、金属、纸类、织物、厨余,这种分法的优点是物理属性明确,但实际问题很大——一个塑料瓶和一个玻璃瓶外形相似度极高,类间区分难度大;一个铝罐和一个铁罐在视觉上几乎无法区分。按材料分会让模型学到一堆"看起来一样但标签不同"的样本,反而容易overfit。
按物品分:塑料瓶、易拉罐、纸板箱、玻璃瓶、塑料袋、电池、果皮厨余等。这种分法对检测任务更友好,因为每个类别有相对统一的形状和外观特征。缺点是类别数量会膨胀——你可能需要20-30个类才能覆盖常见生活垃圾。
我实际使用介于两者之间:常用可回收物按物品细分(塑料瓶、易拉罐、玻璃瓶、纸箱、报纸、塑料容器),有害垃圾按物品种类单独标注(电池、灯泡、过期药品),其他垃圾做成一个大类。最终类别数控制在12类左右,模型精度和标注成本之间比较平衡。这一点供你参考,不必照搬,但思路是通用的——类别的粒度要和任务需求对齐。
3.2 数据来源与标注规范
数据收集是垃圾分类项目里最苦的活。最开始我优先用公开数据集,比较有名的包括 TrashNet、TACO、以及一些区域性垃圾分类数据集。但这些数据集的问题在于背景干净、目标摆放整齐、光照条件单一,训练出来的模型在真实垃圾场景里掉点严重。
后来我补数据的思路是双管齐下:一是自己拿手机拍生活垃圾桶,从不同角度、不同距离、不同光照条件下拍摄;二是从网上找真实垃圾场景的图像——垃圾站、回收站、堆在路边的垃圾袋,这些对比度低、遮挡严重、背景杂乱的图像,对模型泛化能力的提升帮助极大。
标注环节是最容易返工的地方。我强烈建议先把标注规范定清楚再动手,尤其是下面几条:
- 遮挡超过70%的目标不标。
- 目标模糊到难以判断类别的,不标。
- 一个目标被多个框重复框住的,只保留最大最准的框。
- 类间有歧义的时候,以"更靠近外形特征"的类别为准。
标注工具我用的是 LabelImg 和 X-AnyLabeling 两个,前者轻量好用适合快速标注,后者有辅助标注能力,在类别多的时候效率高。标注完之后一定要做一次人工抽检,找几批标注结果再核对一遍,因为标注质量对精度的影响甚至超过模型结构本身。
3.3 数据集增强策略
垃圾识别场景的图像变化太多,单纯靠标注数据量很难覆盖。所以数据增强非常关键。yolov8内置的增强参数很多,但乱开反而坏事。我最终用的增强配置如下:
hsv_h: 0.02 hsv_s: 0.7 hsv_v: 0.5 degrees: 15 translate: 0.15 scale: 0.5 fliplr: 0.5 mosaic: 1.0 mixup: 0.05尤其要说一下 mosaic 和 mixup。mosaic 把四张图拼成一张训练,能显著提升小目标检测能力,但对大目标训练不友好;mixup 则是两张图加权融合,能增加类间判别难度,对防过拟合有帮助。我用的组合是 mosaic 全程开启、mixup 只开 0.05——因为开着 mixup 会让模型训练收敛变慢,而垃圾识别本身类间差异已经很小,再叠加太强的 mixup 可能适得其反。
另外一个细节:yolov8 默认会在最后 10 个 epoch 自动关闭 mosaic 增强(close_mosaic 参数),这是为了在收尾阶段让模型适应真实分布。这个默认策略很合理,不要改掉。
3.4 数据集划分不能只按文件夹
很多人习惯直接把图片按 8:1:1 随机分成训练集、验证集、测试集。这么干在分类任务里问题不大,但在目标检测里容易翻车——因为同一场景的不同帧图像可能被同时分进训练集和验证集,导致验证集的成绩虚高。
正确的做法是先按"场景"分组,也就是同一个拍摄地点、同一段连续时间拍摄的图像归为一组,确保一组图像要么全进训练集,要么全进验证集,不能跨组拆分。这样能防止数据泄漏,评估结果才可信。
我自己还习惯在测试集里单独放一批"完全没见过的场景"的图像。这批图像是训练和验证都完全没有出现过的环境(比如换个小区、换个垃圾桶拍摄的),用来模拟真实部署时的效果。这个"场景独立测试集"的分数,才是你真正可以对外宣称的泛化能力。
4. 训练环境配置与参数调优实录
4.1 环境配置与版本匹配问题
yolov8的环境配置虽然说不上难,但版本匹配问题能让人卡一整天。我的建议是直接按照 Ultralytics 官方文档来装,不要自己乱配版本。一个稳妥的组合是:
- Python 3.9 或 3.10
- PyTorch 2.0.1 + CUDA 11.8
- ultralytics 库(8.x版本,越新越好)
- opencv-python、tensorboard
国内网络环境下,pip安装PyTorch建议用清华镜像源,会快很多。但要特别注意CUDA、cuDNN、PyTorch三者版本必须对应。如果你是新买卡的,比如 RTX 40系显卡,建议直接用 PyTorch 2.1+ 搭配 CUDA 12.1;如果是 30 系显卡,CUDA 11.8 组合比较稳定。这个对应关系是实测出来的经验,照着填空就行。
显卡选择方面,垃圾识别数据集不算小,建议显存至少 8G 起步。我自己的 3060 12G 训练 yolov8s,batch=16 是没问题的。如果你只有 6G 显存,就老老实实换 yolov8n 或者把 imgsz 调到 640,否则显存溢出会把你折磨到怀疑人生。
4.2 训练参数的含义与选择
yolov8 训练参数全网讨论度很高,也是很多新手最容易瞎调的地方。我按实际经验把最关键的几个参数逐一说一遍:
imgsz:输入图像尺寸。yolov8默认是640。垃圾识别场景如果你发现小目标漏检严重,把输入尺寸提到960通常能立竿见影,但训练和推理速度都会变慢。这个参数的性价比极高,比任何改进模块都直接。
batch:单次训练送入的图像数量。越大收敛越稳,但受显存限制。我的建议是"能开多大开多大",至少不要小于16,否则BN层的统计量不稳定,效果很差。
epochs:训练轮数。垃圾识别数据集如果只有几千张图,300个epoch基本足够。重点要看训练曲线,如果验证集的loss在100个epoch后不再下降,再多的epoch就是浪费计算资源。
lr0:初始学习率。yolov8默认是0.01,我惯性不调。如果你发现loss不收敛,可以降一半试试。但是学习率调太低会导致收敛极慢,调太高会训练震荡。
warmup_epochs:预热轮数。yolov8默认3.0,作用是在训练初期用小的学习率让网络稳定启动,避免一开始就大步长导致loss爆炸。这个参数通常不用动。
cos_lr:余弦学习率调度。yolov8默认开启。余弦退火的好处是训练后期学习率会平滑降到很低,让参数更精细地收敛。建议保持默认。
weight_decay:权重衰减,默认0.0005。数据集较小、模型较大的时候可以适当加大到0.001,能防治过拟合。
4.3 训练流程与过程记录
训练命令我一般长这样:
yolo train model=yolov8s.pt data=./garbage.yaml epochs=300 imgsz=640 batch=16 lr0=0.01 warmup_epochs=3 device=0 project=./runs name=exp-cbam-bifpn seed=42这里的 data 参数指向一个 yaml 文件,里面写好数据集的路径、类别数量和类别名。garbage.yaml 大概长这样:
path: ./dataset/garbage train: images/train val: images/val test: images/test nc: 12 names: ['battery', 'can', 'carton', 'cigarette', 'glass_bottle', 'paper', 'plastic_bottle', 'plastic_container', 'plastic_bag', 'leftover_food', 'light_bulb', 'other_trash']训练过程我习惯用 tensorboard 看曲线。打开方式是训练时加一行callbacks=[TensorBoardCallback()],然后在命令行里跑tensorboard --logdir=runs。重点观察三个指标:train/loss、val/loss、metrics/mAP50。
从实操经验来看,正常的训练过程大致是:前20个epoch loss快速下降,mAP稳步上升;50-100个epoch 时mAP进入平台期,小幅震荡上升;150个epoch以后模型趋向收敛。如果val loss 在训练后期不降反升,而train loss还在下降,说明过拟合了,这时候该做的是加大数据增强、调大weight_decay,或者提前停止——而不是继续傻跑。
4.4 模型评估与结果分析
训练结束后,我第一步做的事情不是看 mAP,而是看每个类别的 PR 曲线和混淆矩阵。因为 mAP 是平均值,它会把"好类别的成绩"和"差类别的成绩"混在一起,掩盖真正的问题所在。
混淆矩阵能告诉你哪些类被谁误判了,这点在垃圾分类里尤其关键。我实测最常见的问题是:
- 玻璃瓶被误判成塑料瓶(透明瓶身+瓶盖颜色相近)。
- 纸板箱被误判成纸张(都是纸类纹理)。
- 塑料袋被误判成其他垃圾(透明塑料袋在遮挡严重时特征极弱)。
针对这些问题,我的处理思路是补数据而不是改模型——专门去搜集这些易混对的样本,多做针对性标注。因为模型结构层面能提升的空间,在数据层面用几百张针对性图片就能补回来,成本更低、效果更直接。
另一个评估指标要特别关注的是 F1-score 曲线。它反映了 Precision 和 Recall 的平衡情况。垃圾识别场景里,漏捡比错捡更严重(漏了有害垃圾是安全事故,分错了还有人工二次分拣的机会),所以我会把置信度阈值调低一些,宁可多产生一些误检,也不要漏掉有害垃圾。具体做法是在推理时设置conf=0.15,而不是默认的 0.25。
5. 常见问题与排查技巧实录
5.1 显存不足跑不起来
这是被问到最多的问题之一。显存不足的表现是训练到一半直接报CUDA out of memory。排查和解决顺序我建议是:
- 第一步,把 batch 减半,训练能跑通再说。
- 第二步,把 imgsz 从640降到480,显存占用会大幅下降。
- 第三步,开
amp=True混合精度训练,显存占用能再降一半。 - 第四步,如果还不行,开梯度累积,等效batch不变但单步显存占用小。
- 终极方案,换更小的模型,yolov8n。
很多情况下,batch 减半 + 混合精度就能解决大部分显存问题,不需要一上来就换小模型。
5.2 训练loss变成NaN
loss 变成 NaN 是个很吓人但常见的问题。常见原因有三类:学习率太大导致梯度爆炸;数据集里有破损或不完整的图像;标注框有极端值(比如宽高为0或者偏离图像范围)。
我的排查顺序是:先把学习率降一半,再检查数据集的图片是否全部能正常解码(用脚本扫一遍),最后用val模式跑一遍验证集检查是否有标注错误。如果这些排查完还是 NaN,要考虑是不是改进模块的数值稳定性出了问题——比如在注意力模块的 softmax 分母上没加 eps。
5.3 小目标漏检严重
这是垃圾识别里最顽固的问题。除了上一节说的增大输入尺寸(imgsz=960)以外,还有一个有效做法是增加一个 P2 检测层。yolov8原版从 P3 开始检测,P2 层的特征图分辨率更高,能提供更丰富的小目标纹理信息。加P2层需要在yaml配置里做修改,代价是推理速度会明显下降。所以我的建议是——先试 imgsz 增大的低成本方案,如果还不够,再上P2层。
5.4 验证集效果好,实测效果差
这是"数据集泄漏"的典型症状。一方面可能是训练集和验证集划分不够严谨,另一方面是数据收集时背景太单一,模型过拟合到了背景特征上。
我的经验是,遇到这种问题先不要怀疑模型,而是先检查验证集图像和训练集图像是否有大量同场景重复。如果确实区分开了,那就要考虑使用更接近真实部署场景的测试集——也就是我前面强调的场景独立测试集。真正有价值的项目评估,不是看模型在自己验证集上的分数,而是看它在陌生人眼里(完全没见过的场景)的表现。
5.5 部署环节的小提醒
模型训练好之后,要部署到实际设备上,我一般习惯先做转换验证。最常用的链路是:yolov8s.pt -> yolov8s.onnx -> TensorRT engine(如果是NVIDIA平台)。在ultralytics框架里,一条命令就能导出:
yolo export model=best.pt format=onnx opset=12 dynamic=True yolo export model=best.pt format=engine device=0 half=True导出后一定要做前后对比测试,用同样的图片分别跑 PyTorch 模型和 ONNX/TensorRT 模型,比对检测结果是否一致。我常遇到的问题是 OP 兼容性——比如自定义的注意力模块在 ONNX 导出时某些算子不支持,就需要把模块改成更通用的算子组合。这也是为什么有些开源改进模块看效果很好,实际部署却很麻烦的根源。
还有一点,如果你的部署目标平台是 RK3588 这类国产边缘设备,它的 NPU 对不同算子的支持情况各不相同,导出前一定要先查算子支持列表。避免辛苦做了改进模块,最后因为某个算子不支持而被迫回退到原版模型,那是最憋屈的处境。
6. 项目之外的一点经验和心得
这个项目做下来,我感触最深的一点是:深度学习的项目核心不在于你用了多新的模型、堆了多少涨点模块,而在于你对数据和任务的深入理解。把类别体系设计好、把数据标注做规范、把训练过程看透彻,这些基本功远比改一堆花哨模块更值钱。
我最后一次调试这个项目时,本意只是想提高两个点的 mAP,后来发现提升最大的操作不是模型改进,而是花了两周时间专门补充了小目标场景的数据——把拍摄角度放低、把镜头拉远、模拟遮挡场景,最后 mAP 涨了整整五个点,比任何注意力机制都猛。这个反差让我印象非常深刻。
另外还要提醒一点,论文中的网络结构图画得再漂亮,不如你自己动手在代码里验证一遍。不同改进模块的兼容性远比想象中复杂——加一个注意力模块可能影响梯度流,换一个特征融合可能拖慢训练速度。每做一个改进,都试试它是不是真的值得你付出那多出来的训练时间和部署时间。
如果想继续往深做,还可以考虑把这个系统朝真实的市政垃圾分类场景推进。比如在回收箱上加一个摄像头,实时识别投放物并给出分类建议;或者部署到社区垃圾房的边缘计算盒子上,实现自动巡检。这些应用都比纯实验室场景有价值得多,因为垃圾识别说到底是个要下地干活的技术。