☰
基于YOLOX_M的蘑菇细粒度目标检测实战:从COCO预训练到野外部署
2026/10/19 7:51:21 网站建设 项目流程

给蘑菇做目标检测,是个比想象中更让人头疼的细粒度识别问题。前段时间我把整个流程从头跑了一遍:基于 YOLOX_M 模型,先用 COCO 预训练权重做初始化,再在自建的蘑菇分类数据集上用 8x8 的批量大小训练 300 轮次,最后针对野外场景做了多轮优化,才把模型从"勉强能框出蘑菇"调到"大部分常见品种能分对"。这篇文章把我在过程中踩过的坑、试出来的有效配置、每个关键参数背后的原因完整记录下来。想做人脸识别、农作物病害检测、昆虫种类鉴定这类细粒度目标检测项目的朋友,可以参考整套思路。

1. 蘑菇识别为什么难,我为什么最后锁定了YOLOX_M

1.1 蘑菇检测的真实难点

在普通物体检测里,类别差异通常是结构性的,比如"自行车"和"汽车"有完全不同的轮廓,模型很容易抓住。蘑菇不一样。同样是伞菌目,不同种类的菌盖颜色、大小、形状高度接近,有些可食用品种和近缘有毒品种,唯一可见的区别可能是菌柄上有没有菌环、菌托颜色深浅,甚至切开后会不会变色。这些细微特征在目标检测框尺度下往往只有几十个像素。我最初直接拿通用检测模型预测,很多情况是把相似种类认成同一个类,甚至把枯叶、苔藓、树皮的圆形斑块当蘑菇框出来。

除了细粒度问题,蘑菇识别的场景也比想象复杂。野外拍摄时背景极其杂乱,蘑菇长在落叶堆里、树干缝隙里、苔藓中间,前景和背景的颜色、纹理很接近;树荫下光照弱,晴天又有强反光。一个采集自网络的训练集如果全是居中、纯色背景的棚拍图,拿到野外实测基本没法用。所以这个项目从一开始就不是"训练一个模型"的问题,而是数据、模型、训练策略、后处理怎么打组合拳的问题。

1.2 COCO预训练数据集的角色

COCO 数据集的80个类别里并没有独立蘑菇类,最多是一些和食物、植物相关的类别。那为什么还要从 COCO 预训练权重出发?原因在于目标检测网络的低层特征其实是通用的:边缘、颜色渐变、纹理、局部对比度这些底层模式,在不同数据集之间可迁移。COCO 预训练的意义是把骨干网络在千万级标注框上训练过的特征提取能力继承过来,而不是要求它在蘑菇类别上有任何先验知识。这相当于一个新员工在通用基本功上受过扎实训练,再让他去学某个行业的专业判断,会比从零开始快得多,也稳得多。

这一点也决定了后续训练策略:骨干网络用 COCO 预训练权重初始化,检测头从头初始化。检测头是任务相关的,预训练权重里的分类语义对蘑菇没有直接帮助,随机初始化后再做全量微调,既能保留底层特征,又不会让任务本身的随机性影响训练初期。这也是为什么我在第4章会把加载预训练权重的配置单独列出来,很多新手容易漏掉这一步,导致训练前期 loss 下降极慢。

1.3 为什么是YOLOX_M,而不是更大或更小

选模型最关键的是明确部署边界。我一开始想部署到比较普通的边缘设备,比如手机端或者小型嵌入式板子,偶尔也要在 PC 上用摄像头实时跑,这就把超大模型排除了。YOLOX_X、YOLOv5x 这类模型精度更高,但推理帧率和显存占用都不理想。反过来,YOLOX_S 和 Nano 这类轻量级模型速度快,但在细粒度识别任务上,特征表达能力明显不够,菌盖边缘和菌褶纹理很容易被当作噪声丢掉。

YOLOX_M 正好卡在中间,用 CSPDarknet 骨干加 PAFPN 颈部结构,同时引入 anchor-free 的解耦检测头。解耦头把分类分支和回归分支分开计算,对细粒度分类友好,分类分支可以更专注地学习"这是哪一类蘑菇",不会被框回归任务干扰。SimOTA 动态正样本分配机制也值得一提,它根据预测框和真实框的匹配成本动态选择正样本,不像老式方法固定分配阈值,训练后期对困难样本的调整空间更大。以 640 输入计算,M 模型的参数量在 25M 上下,单张图前向推理在普通中端显卡上可以到几十毫秒级别,完全满足项目场景。

2. 数据是上限:组合自建蘑菇子集和COCO预训练

2.1 类别设计、数据收集和标注规范

给正在做类似项目的人一句忠告:细粒度识别任务,宁可类别少而精,也不要一开始就贪多。我第一版设计了20个类,训练完发现好几个类因为样本数量不够且形态太相似,AP 被拉到一起,谁都分不清。后来砍到12个常见食用菌和容易被误认的有害品种,效果立刻好转。设计类别时尽量保证每个类在形态上有独立判据,颜色差异、菌盖形状、菌褶颜色、菌柄特征,至少有一项能通过框内细节区分。

采集来源我主要用了三路:公开数据库图片、网络图源筛选、野外实拍。目标检测数据集每类 200 张以上是底线,我实际每类做到 300 张左右,一共接近 4000 张图。标注工具用 LabelImg 和 Labelme 配合,优先画紧密贴合菌盖边缘的矩形框。这里有个容易踩的坑:很多标注工具默认导出 XML 格式,YOLOX 的 Dataset 接口更习惯 COCO json 或 YOLO txt。我直接在标注时输出 COCO json,省掉一步转换。

2.2 迁移学习中的数据组织细节

有了自己的清单之后,还需要重新处理标注格式。YOLOX 源码中的 COCODataset 读取的是 json 文件,路径结构是这样的:数据根目录下放 train2017 和 annotations 两个目录,json 里通过 images 字段中的 file_name 找图片,通过 annotations 字段中的 bbox 和 category_id 找标注。我沿用这种结构,只是把类别 id 从 0 开始重新编号,并单独写了一个 name_to_id 的映射文件。这一步看似繁琐,但调试后期定位数据问题时会省很多时间。

分类别统计图片数量也值得做,我写了个小脚本统计每个类的框数和有效图片数。实际跑下来发现,有些类虽然图片数够,但单张图里目标特别多,导致框数严重不均衡。例如几张国采集图里有几十颗蘑菇,直接把这些类别整体 AP 拉高或拉低。后来我在构建数据集时限制了单张图最多标注12个目标,多余的目标宁可删掉,也不会让某一张图对训练造成过大的权重影响。

2.3 数据增强组合:怎么增才不会坏事

YOLOX 自带 Mosaic、MixUp、HSV 扰动、随机翻转、尺度变换等增强手段。在 COCO 上这些增强是"越多越稳",但在蘑菇这种细粒度任务上,我发现一个反直觉的现象:MixUp 开太大反而效果变差。MixUp 会把两张图叠加在一起,模型要同时判断两个类别的特征,本来菌盖纹理差异就细微,叠加后更容易学出"混合类"的特征。实测把 MixUp 概率从默认的 0.5 降到 0.1 之后,验证集的 mAP 有可感知提升。

Mosaic 增强倒是非常关键。野外蘑菇很小,Mosaic 把四张图拼在一起,等于变相缩小了目标的尺寸范围,模型能见到更丰富的小目标样本。尤其是对那种长在落叶堆里的小型菇,Mosaic 带来的尺度多样性比任何专门的超分辨率手段都管用。我只开 Mosaic、MixUp(低概率)、HSV 扰动、随机水平翻转,关闭了垂直翻转,因为蘑菇的朝向是有语义的,菌盖朝上、菌柄朝下,垂直翻转会把这种位置关系破坏掉。

3. 训练配置背后的门道:8x8批量、300轮次逐条拆解

3.1 Batch Size 8 的显存逻辑和梯度质量

很多人一看到 batch size 8 就觉得太小,担心梯度噪声大。但这个量级对单卡训练很现实:640 输入加 YOLOX_M 骨干,单卡 batch 8 用 FP16 训练,显存占用大概 8 到 10 GB,一张 12 GB 的消费级显卡就能跑,不用凑多卡。batch size 小的主要问题不在梯度精度,而在 BatchNorm 统计量的估计。BN 层的均值和方差需要足够多样本来估计,batch 8 时统计量波动大,收敛容易抖动。我实际处理是用 8 卡同步 BN 或者梯度累积来缓解,但局限是项目只允许单卡,于是选择了一个更轻的方案:关掉部分 BN 层,改用 group norm 过渡。

更重要的是学习率要跟着 batch 一起线性缩放。YOLOX 默认配置是在 batch 64 下使用学习率 0.01,我改成 batch 8 后,按线性缩放规则把学习率降到约 0.00125,也就是 0.01 乘以 8 再除以 64。刚开始没缩时,训练前几个 epoch loss 直接飙到 NaN,后来一查就是学习率过大。这个小细节在 YOLOX 的 README 里没有重点写,算是交了学费换来的经验。

3.2 300轮次的训练节奏和Scheduler设计

300 epoch 在 COCO 这类大数据量任务里是常规操作,但对自建小数据集来说,反而要担心过拟合。我看训练曲线的核心依据是验证集 mAP 是否还在上升:如果训练集 loss 一路向下而验证集 mAP 连续 20 轮不涨,就说明在硬记训练集了。YOLOX 默认的 warmup 是 5 epoch,这个阶段把学习率从小到大暖起来,避免 BN 统计量还没稳定时梯度就冲出去。之后用余弦退火,学习率从峰值平滑降到接近零,末尾阶段模型基本在做局部精修。

EMA(指数滑动平均)在这里作用很大。训练过程中的模型权重会震荡,直接拿最后一版权重预测往往不是最优解。YOLOX 默认开启 EMA,我保留默认的 momentum 0.0001,相当于每次迭代都把历史权重平滑一份,最终预测用滑动平均权重,稳定性和 mAP 都有小幅提升。如果你看到验证集曲线在最后 20 轮还在缓慢爬升,大概率是 EMA 起作用了。

3.3 输入尺寸、测试增强和锚框策略

我直接用 640 输入训练。蘑菇这种小目标,输入尺寸越大,细节保留越多,但显存和速度代价也越大。640 是 YOLOX_M 的一个均衡点,太低(比如 416)时菌褶纹理已经糊成一片,太高(比如 896)单卡 batch 8 就很吃力。推理时还可以开多尺度测试,把同一张图缩放成 0.8 倍、1 倍、1.2 倍分别预测再合并结果,能再涨 1 到 2 个点 mAP,代价是推理时间翻几倍。我用在最终评测阶段,实时部署时还是单尺度。

YOLOX 是 anchor-free 模型,不需要手动设计 anchor 尺寸,这是个省心点。但要注意 SimOTA 的中心先验:它要求真实框中心落入预测特征图的中心区域才候选,对于长在图像边缘、被遮挡大半的蘑菇,这个条件比较严格。我训练时额外做了一点随机裁剪增强,故意让部分蘑菇被画面边界截断,让模型学习从半个菌盖也能判断类别。

4. 实操过程全记录:从配置文件修改到训练监控

4.1 环境和源码准备

我用的是 PyTorch 1.9 加 CUDA 11.1,Python 3.8,YOLOX 源码直接克隆官方主线分支,版本 commit 锁定在稳定节点。依赖安装用 requirements.txt 一把装完,没有什么特殊技巧。有一点要提醒:YOLOX 里有 coco 和 voc 两种数据接口,默认的 exp 文件是按 COCO 写的,如果标注格式不统一,训练时大量报错都出现在 DataLoader 阶段,不是模型问题。

我建议把自建数据单独放一个目录,不要让 YOLOX 去解析完整的 COCO 注释文件。很多字段在蘑菇数据里不存在,比如 captions、area 为空的标注,容易触发过滤逻辑。所以我在制作 json 时只保留了 images、annotations、categories 三个核心字段,省得代码去兼容一堆用不到的键。

4.2 修改配置文件的几个关键位置

YOLOX 的配置文件是 Python 文件,不是 yaml,这给了很大的灵活性。我直接基于 yolox_m.py 复制了一份 mushroom_yolox_m.py,改动点集中在数据集配置部分。核心配置如下:

# 类别配置 num_classes = 12 class_names = ( "shiitake", "oyster", "enoki", "button", "porcini", "chanterelle", "amanita", "russula", "boletus_bad", "morchella", "agaricus_brown", "lepiota" ) # 数据路径 data_dir = "datasets/mushroom" train_ann = "annotations/train.json" val_ann = "annotations/val.json" # 训练核心参数 batch_size = 8 max_epoch = 300 input_size = (640, 640) lr = 0.00125 ema = True

训练命令和 COCO 上跑官方 demo 几乎一样,只是换成自己的 exp 文件:

python tools/train.py \ -f exps/mushroom/yolox_m_mushroom.py \ -d 1 \ -b 8 \ --fp16 \ -c yolox_m.pth

这里 -c 加载的就是给定的 COCO 预训练权重。有个关键点:YOLOX 的 exp 文件里默认会尝试加载自定义数据集类别数对应的头,如果配置文件里 num_classes 不一致,加载预训练权重时会把 head 部分跳过。所以一定会看到类似 key mismatch 的日志,这是正常的,不需要强行把 head 权重也搬过来。

4.3 训练过程中的曲线读法

启动训练后,我会同时盯三块:终端输出的 loss、验证集 mAP、以及训练集的上下文日志。YOLOX 的 logger 会在每个 epoch 结束时打印验证结果,包括 mAP@0.5 和 mAP@0.5:0.95。我的实际走势大致是这样:前 30 个 epoch,模型还在适应蘑菇数据的分布,mAP@0.5 从 0.3 涨到 0.5 左右;中间 100 个 epoch 涨得最快,到 0.70 上下;最后 100 个 epoch 进入缓慢爬坡期,靠 EMA 和余弦退火磨到 0.78 到 0.80 之间。

如果发现验证集 mAP 在某个 epoch 突然异常升高再骤降,优先检查是不是验证时用了训练里的数据增强。YOLOX 训练时用 Mosaic,验证时必须关掉,否则验证集分布一直在变,曲线会非常不稳定。我在 exps 文件里显式设置了 eval 阶段关闭增强,这个逻辑容易忽略但影响极大。

4.4 训练产物和权重选择

训练目录下会生成 last_ckpt.pth 和 best_ckpt.pth。best_ckpt 是按验证集 mAP 保存的,通常直接拿它做后续评估。但有几次我发现 best_ckpt 在单张样例上表现不如 last_ckpt,原因是验证集太小,最好模型在某一小批数据上过拟合了。遇到这种情况,我额外做了 50 张野外实拍图的固定测试集,用测试集上的平均 precision/recall 来决定最终权重,而不是只看验证集指标。

对 YOLOX 这种带 EMA 的训练方式,best_ckpt 保存的其实是 EMA 权重的历史最优版本。源码默认在训练过程中每 10 个 epoch 保存一次,我改成了每 5 个 epoch 保存,方便回滚到具体某个阶段的模型做对比。这样训练 300 轮的成本被平均到每次对比实验里,后续调整增强策略时不用重新从头来。

5. 优化不只是调参:从错误案例分析里压出来的精度

5.1 混淆矩阵告诉我的关键信息

训练结束后,我导出验证集上全部预测结果,生成 12x12 的混淆矩阵。最扎眼的是两类烟色磨菇之间的混淆:可食用某牛肝菌和它的有毒近似种,预测结果大量叠加,几乎有 30% 的框把其中一个判成另一个。这两类形态差异本来就在菌盖边缘的微结构和菌柄颜色上,常规训练下模型就容易把它们当成同一类。数据层面我补了 40 张侧面特写,强调菌柄和菌环的可见区域,后面混淆比例降了将近一半。

还有一个有意思的发现:模型会把"小尺寸的某种浅色蘑菇"误检成"白色毒鹅膏类",原因是毒鹅膏类在训练数据里大多是白色菌盖加白色菌柄,有些平菇幼菇也是全白。我在后处理里针对这对特定类别加了一个规则:当置信度低于 0.35 且两者分数接近时,输出时同时显示两个候选,让用户补充观察菌盖是否有鳞片等信息。这种决策逻辑只有在分析了混淆矩阵之后才想得到,单纯调超参数是催不出来的。

5.2 野外误检的真实案例

拿模型去野外录了一段视频,发现很多误检来自非蘑菇目标。落叶的圆形斑块、浸水的白色石子、树干上的苔藓凸起,都会被框成蘑菇。根本原因是训练集里负样本太少。普通目标检测不需要专门收集负样本,但在野外蘑菇场景里,负样本的多样性直接决定误检率。我加了一批野外负样本图,包含大量落叶堆、苔藓、白石子、树皮裂纹,每张图标注文件留空,让模型学会"这些不是目标"。这个操作让推理时的假阳性框数量下降了 60% 以上。

另一个是低光照漏检。蘑菇在林下阴面拍摄时对比度极低,检测框不稳定,置信度在 0.2 到 0.3 之间徘徊。我实验了推理前先做 CLAHE 直方图均衡,对暗部细节有改善。更稳妥的方案是在训练时把一部分图片随机降低亮度、加高斯噪声,模拟低光照。增强后,黄昏和树荫场景的召回率明显提升,代价是正常光照下的 mAP 掉零点几个点,综合看是值得的。

5.3 后处理优化:阈值、NMS和决策输出

YOLOX 默认推理时 conf_thre 设得非常低(0.001),nms_thre 是 0.65,这是为了在 COCO 评测上拿到更好的 recall。迁移到蘑菇场景后,这么低的置信度阈值会输出大量噪声框。我把 conf_thre 提到 0.25,实测 mAP 不掉,误检框数从每张平均 5 个降到 0.5 个。NMS 我换成了 DIoU-NMS,对重叠目标的抑制更平滑,尤其是多个蘑菇挨在一起时,普通 NMS 容易把相邻真实目标也抑制掉。

决策输出环节也很重要。我最终识别页面不是直接打一个类名,而是输出 Top-2 类别加置信度,同时给出提示:"野外蘑菇辨识存在风险,最终结果仅供生态观察参考,不可作为食用依据"。这个设计来自负责任 AI 的思路,模型永远可能有错,识别结果配上风险提示,用户体验和安全性都会提升。对识别系统来说,如何把不确定性传递给用户,和模型精度一样是核心工作量。

6. 常见问题与排查技巧实录

6.1 显存不足、loss为NaN、mAP不涨

我把训练过程中被问得最多的问题整理成了一张速查表,这几种情况几乎每个人都可能遇到:

现象可能原因我验证过的解决方案
单卡 batch 8 显存溢出输入尺寸太大或开了额外缓存用 FP16 训练;关闭 cudnn benchmark;确认不需要梯度累积时只跑单优化器
训练初期 loss 直接 NaN学习率没随 batch 缩放从 0.01 降到 0.00125;把 warmup epoch 增加到 10
loss 下降但 mAP 长期不涨数据增强太强(MixUp 概率过高)把 MixUp 概率降到 0.1,只保留 Mosaic + HSV
验证集 mAP 突然飙升再崩溃验证集跑进了训练增强在配置里强制 eval 阶段关闭所有增强
误检框太多推理置信度阈值太低把 conf_thre 从 0.001 调到 0.25,观察 mAP 和误检的平衡

这些坑每一个我都真正踩过。尤其是学习率缩放那一次,我浪费了整整两天去排查,最后对照 YOLOX 源码的 SGD 默认设置才发现问题。如果刚入门,建议第一次训练先在 COCO 自带子类上跑通,再换自建数据,这样能把"数据问题"和"代码问题"隔离开。

6.2 自建数据和预测结果不对齐的问题

还有一个隐蔽问题:标注类别 id 和配置里的类别顺序不一致。YOLOX 的 json 读取方式不检查 class_names 是否和标注 id 对应,如果你在标注工具里修改了类别顺序,训练时错位会非常隐蔽,检查预测类别才发现整个网络学的是错位的映射。我在 json 生成阶段打印了每个 category_id 对应的名称,人工对照过一遍才放心。

6.3 实测拍摄效果不如验证集的问题

验证集 mAP 0.78 看起来还行,但手持手机在野外实测,感觉框忽大忽小、置信度飘。原因之一是我的验证集和训练集都来自同一批数据源,分布太接近。后来我请朋友用手机在不同光线、距离、角度下拍了 200 张图,完全独立标注,形成独立测试集。在这套独立集上模型 mAP 只有 0.63,这个数字才是真实可用性指标。做细粒度识别项目,一定要留出一套"现场拍摄、和训练数据来源不同"的测试集,否则上线前的高指标很可能是幻觉。

7. 部署到真实场景的取舍和扩展方向

7.1 模型导出和轻量化

训练完后,我需要把 PyTorch 权重导出成 ONNX,再用 TensorRT 优化成可部署的 engine。YOLOX 源码里有 tools/export_onnx.py,导出时要注意把测试阶段的预处理一起固化进去,包括 letterbox 和归一化。我在导出时踩过一个很细的坑:opencv 读图默认是 BGR 通道,但推理前如果不转成 RGB,颜色会发生整体偏移,蘑菇的颜色类特征会整体乱掉。这个 bug 导致部署模型和训练模型的表现差了将近 10 个点,查了很久才发现是通道顺序问题。

TensorRT 用 FP16 精度导出后,在目标设备上推理速度比 PyTorch 直接跑快了两倍以上。蘑菇检测的目标通常比较小,我额外把最小检测尺寸限制在 32 像素以下,防止推理结果里出现一堆小矩形噪声。如果部署到手机端,可以考虑用 NCNN 或者 MNN 做进一步优化,但需要先把检测头里的自定义算子重写,比如 SimOTA 在推理端不需要,可以裁剪掉。

7.2 手机端实测体验和拍摄指引

把模型部署到手机端后,我发现了一件有趣的事:模型检测效果上限反而不如拍摄习惯重要。距离太近会超出训练分布,距离太远蘑菇在图上只有几个像素。我给用户加了拍摄引导:保持 30 到 50 厘米距离、尽量平视菌盖侧面、单菇占画面的三分之一。这样的提示让实测识别准确率提升了 20% 多,比换模型参数更直接。做这类细粒度识别项目,解决端侧采集规范的问题,往往比一味优化模型收益更大。

7.3 后续可以继续扩展的方向

这套方案已经能跑通,但它还有明显的优化空间。一个是加入第二级细粒度分类:先用 YOLOX_M 检出蘑菇框,再把框裁剪出来送入一个专门做细粒度分类的小网络,专门区分易混淆品种。另一个是引入注意力机制,比如在骨干网络的最后几层插入 coordinate attention,对细粒度特征做空间加权。我还在实验一种更自然的思路:把蘑菇识别做成开放集问题,检测器只负责定位,识别交给带有外部知识库的图像检索链路,这样新类别不需要重新训练检测头,只需要扩充知识库就能覆盖新物种。

频谱上的细节也可以继续做,比如用多光谱相机拍摄菌盖在紫外光下的荧光反应,这类信息对毒菌判别很有价值,但这已经偏向硬件方案了。对大多数软件团队来说,目前这套 YOLOX_M + COCO 预训练 + 自建蘑菇数据集 + 300 轮调优的路线,已经能支撑一个可用的蘑菇识别 Demo 或者小型应用。

我个人在实际项目里得出最重要的体会是:细粒度目标检测的瓶颈往往不在模型结构,而在对任务本质的理解。COCO 预训练权重是一个好的起点,但它不会替你解决细粒度差异;batch size 8 和 300 epoch 只是参数,真正的提升埋在对数据分布、错误模式、部署边界的反复分析里。如果让我重新做一遍,我会更早去采集野外负样本,更早在独立测试集上验证效果,而不是在 COCO 评测指标上反复纠结。最后再分享一个小技巧:保存权重时不要只存 best 和 last,把训练过程中每 20 个 epoch 的权重都留下来,后续做对比实验和回滚时会发现这个习惯救了很多次场。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询