☰
改进YOLOv10n实现蝴蝶鱼识别:SPDConv小目标检测与细粒度分类实践
2026/10/19 3:00:04 网站建设 项目流程

蝴蝶鱼(Chaetodontidae)这类珊瑚礁鱼,颜色花纹极其丰富,同一物种在不同地理种群、不同生长阶段外观差异都很大,加上水下拍摄角度、光照、水质散射的影响,传统图像检索和人眼识别的方式在批量图像数据处理时效率非常低。我最近做了一个蝴蝶鱼种类识别与分类项目,使用 YOLOv10n 作为基础检测框架,针对小目标与密集纹理识别困难的问题,引入了 SPDConv(Space-to-Depth Convolution)替换部分标准卷积层,最终在自建数据集上把 mAP50 从 81.6% 提升到了 88.3%,整体参数量只增加了约 0.3M。这篇文章把我从数据准备、模型改造、训练调参到坑点排查的完整过程做个记录,适合正在做细粒度鱼类识别、小目标检测,或者想给轻量化 YOLO 模型做改进的开发者参考。

1. 蝴蝶鱼识别为什么难,以及项目整体思路

1.1 蝴蝶鱼识别的真实痛点

先说结论:蝴蝶鱼分类不是一个普通的通用目标检测任务,它的难度集中在“细粒度差异”和“环境干扰”两件事上。

蝴蝶鱼全球约有 120 多个物种,国内常见于南海珊瑚礁海域。很多种类的外形轮廓几乎一样,比如常见的镰鳍蝴蝶鱼和珍鲨蝴蝶鱼,身体都是圆扁的碟形,区分点只在背鳍后部的黑色斑纹形状、尾鳍的黄色带宽度以及体侧纵带是否穿过眼部。普通检测模型在 Imagenet 上见过的“鱼”大多是轮廓清晰的鱼类照片,遇到这种只有细微纹理差异的类别,特征提取能力明显不够用。

水下环境带来的干扰更为棘手。珊瑚礁背景的纹理极其复杂,蝴蝶鱼本身又是高纹理目标,鱼体上的网格纹、斑点、条纹与背景珊瑚的纹理容易产生混淆。更麻烦的是水下图像的色偏非常严重,5 米以浅和 15 米以深拍摄的同一物种,整体色调完全不同。光线衰减导致蓝绿色通道占比过高,红黄色通道信号弱,这会影响模型对黄色条纹、橙色斑纹这些关键判别特征的提取。

因此,这个项目从一开始就不是简单“跑一个 YOLO”,而是要在算法层面解决小目标与纹理细节丢失的问题。

1.2 选型逻辑:YOLOv10n 为什么够用

YOLOv10 系列是在 YOLOv8 基础上做的优化升级,核心改进点是取消了 NMS 后处理,通过双标签分配策略和一致性匹配机制让模型在训练和推理阶段的行为对齐。对于部署在边缘设备上的水下监测系统来说,去掉 NMS 意味着推理延迟更稳定,更便于移植到嵌入式平台。

我选择 n 版本(nano)的核心理由是算力约束。项目最终要部署到水下机器人的机载计算单元,这类设备通常只有 4GB 到 8GB 的显存,功耗和发热也受到严格限制。yolov10n 的参数量大约在 2.3M 左右,计算量约 6.7 GFLOPs,在移动端设备上可以跑到 30 FPS 以上。相比之下,yolov10s 的参数量翻倍到 4.7M,计算量到了 21.6 GFLOPs,在嵌入式平台上推理延迟会明显上升。考虑到蝴蝶鱼检测属于单目标类别较多但单张图像中个体数量并不极端密集的场景,nano 级别的模型容量是够用的。

但是,nano 模型的容量小也意味着特征提取能力有限,直接拿来训练蝴蝶鱼这种细粒度分类任务,很容易出现特征区分度不足的问题。这正是我引入 SPDConv 的初衷——在不显著增加参数量的前提下,提升模型对细节纹理和中小目标的感知能力。

2. SPDConv 核心原理与改进方案设计

2.1 SPDConv 到底解决了什么问题

SPDConv(Space-to-Depth Convolution)最早是 2022 年提出的一种替代标准下采样方式的模块,它的出发点非常朴素:标准卷积层中的 stride 卷积会导致空间信息丢失,尤其对于小目标或包含丰富纹理细节的图像,这种信息丢失是致命的。

标准的 stride=2 卷积在每次滑动时,只从输入特征图的对应位置取一个像素点,生成输出特征图。这意味着输出特征图中每个像素对应的感受野内部,有 75% 的空间信息被直接丢弃了。对于大目标来说,丢掉的这 75% 信息可能不关键;但对于蝴蝶鱼体表那些只有几个像素宽的条纹和斑点来说,这些被丢弃的空间信息恰恰是分类的关键证据。

SPDConv 的做法是先把输入特征图按空间位置做重排,把一个空间维度上的像素重新排列到通道维度上。比如输入是 [B, C, H, W],经过 space-to-depth 变换后变成 [B, 4C, H/2, W/2],然后接一个 1x1 卷积对通道进行融合。这样做的好处是:下采样的过程中没有丢弃任何像素信息,所有空间位置都被保留下来了,只是重新组织了一下排列方式。

用个不太严谨但容易理解的类比:标准 stride 卷积相当于从一幅大图里每隔一个像素抽一个点拼成小图,中间的像素直接扔掉;SPDConv 相当于把大图均匀切成了四块小图叠在一起,然后用一个融合层把它们合并。前者丢信息,后者信息无损。

具体到蝴蝶鱼检测这个任务,SPDConv 的价值体现在两个方面:一是浅层特征图中的小目标信号不会因为下采样而衰减太多,二是深层特征图在多次下采样之后仍能保留足够的纹理细节,供分类分支判断物种类别。

2.2 网络结构改造:替换哪些模块

我参考了 yolov8 改进方案中的常见做法,在 yolov10n 的基础上做了两处关键替换。

第一处是主干网络中的第一个下采样层。yolov10n 的 backbone 使用一个 stride=2 的标准卷积作为 stem 层。我把这个 stem 层替换为 SPDConv 模块,具体做法是:将原本的 Conv(k=3, s=2, p=1) 拆成 space-to-depth 变换 + 1x1 卷积。这样输入图像长宽减半后,通道数从 3 变为 12,再通过 1x1 卷积将通道统一到目标维度。

第二处是 C2f 模块内部的下采样分支。原始 C2f 模块中的 bottleneck 分支包含一个 stride=2 的卷积,我把这里的标准卷积替换为 SPDConv 结构。这样做能让每次特征图尺度减半时,保留更多的空间细节信息。

需要特别说明的是,并不是所有下采样层都适合替换。我试过把 backbone 中所有 stride=2 的卷积全部替换为 SPDConv,结果训练时 loss 收敛明显变慢,最终指标反而下降了 1.2 个百分点。原因在于 SPDConv 的 space-to-depth 变换会使得后续 1x1 卷积的计算量增加,过多的替换会导致特征图的通道维度膨胀过快,模型需要更多参数来融合冗余信息。对于 nano 级别的小模型,这种改动带来的收益是负的。最终保留了两处关键替换,即在保证信息保留的同时,控制住了计算量。

# ----- 配置文件节选 ----- # yolov10n_spd.yaml backbone: stem: type: SPDConv in_ch: 3 out_ch: 16 stage2: type: C2f_SPD in_ch: 16 out_ch: 32 stage3: type: C2f in_ch: 32 out_ch: 64 # ...

实际上手改代码的时候,我并没有直接去改官方 YOLOv10 仓库里的模型文件,而是在已有的改进版本基础上做二次开发。社区里已经有人把 SPDConv 集成进了 ultralytics 框架,可以直接从 models 目录下引用。如果你们从头自己写,核心就是space_to_depth函数加一个Conv2d,大概 20 行代码就能搞定。

3. 数据集构建:从采集到标注的完整流程

3.1 图像采集与清洗

数据集质量直接决定模型上限。

我最初从公开的珊瑚礁鱼类图像库、自然摄影图集和潜水爱好者社区收集了大约 3000 张蝴蝶鱼图像,涉及 15 个常见物种,包括美丽蝴蝶鱼、铜带蝴蝶鱼、黑背蝴蝶鱼、黄高鳍蝴蝶鱼等。但这些原始图像存在两个问题:一是正样本中很多图片是由单反相机在水下微距模式下拍摄的高清特写,干净背景下目标占比很大,这类图像公开数据集里一大堆,但实际部署时水下机器人拍到的往往是目标占比不足 10% 的中远景画面;二是不同来源图像的色彩空间差异极大,有些经过后期处理,色温已经被调过,直接用这些图像训练会让模型学到颜色修正后的分布,而真实部署时的图像完全没有经过后期处理。

针对第一类问题,我采取了一个比较笨但有效的方法:用小目标模拟器把高清特写图贴到真实珊瑚礁背景图上做数据合成。把目标鱼类从原始图像中抠出来,回贴到不同形态的礁石、砂底、海扇背景中,同时随机缩放目标到原图的 5% 到 15% 尺寸区间。这样生成的 2000 张合成图能有效模拟水下机器人在真实作业时看到的画面比例。

针对第二类问题,我在预处理管线里写了统一的色彩校正函数。对每一张图像做灰度世界假设的简单白平衡处理,然后统一调整亮度到 80 到 140 的区间(0 到 255 灰度值范围),色温统一向 5500K 左右拉齐。这样处理后,不同来源图像的亮度分布和色彩偏差会收敛到一个相对一致的范围内,模型不用浪费参数去适应无关的色偏变化。

3.2 标注规范与格式转换

标注是另一个容易掉坑的环节。蝴蝶鱼种类识别中,很多物种之间的差异在图像上只有几个像素级别的细微差距,标注框的边界会直接影响模型提取到的特征内容。

我使用的标注工具是 LabelImg,标注规范为:

  • 标注框必须紧贴鱼体轮廓,不能把鱼鳍完全展开后的透明区域算进框内。蝴蝶鱼的背鳍和臀鳍张开时面积会比闭鳍时大 30% 左右,如果把透明鳍膜也标进去,会引入大量背景信息
  • 两条鱼交叉重叠时,分别标注两个框,不截断
  • 鱼体被遮挡超过 50% 时不标注
  • 幼鱼与成鱼形态差异明显的物种,按物种统一标注为同一类别,依靠模型自行学习不同生长阶段的外观特征

原始标注格式是 PASCAL VOC(XML 格式),项目训练使用的是 YOLO 格式,我写了一个转换脚本把 XML 转为 txt 标注文件。这里有一个容易出错的地方:YOLO 格式的坐标是归一化的中心点坐标和宽高,除以图像尺寸前要注意宽高比变化,如果做过 resize 预处理,标注坐标也必须同步换算。

经过清洗后,最终数据集大约 4600 张,标注框总量 6800 多个。按照 8:1:1 的比例划分训练集、验证集和测试集,划分时按类别分层抽样,确保每个物种在三个集合中的比例一致。

3.3 数据增强策略

蝴蝶鱼识别任务的数据增强策略比通用检测任务需要更精细的调校。

我用到了以下几类增强操作,参数如下:

增强操作参数设置理由
Mosaic关闭蝴蝶鱼是细粒度分类目标,拼接图的边界裁剪会切掉关键纹理
HSV 扰动H ± 20, S ± 30, V ± 30模拟不同水深下的色偏变化
随机旋转± 15°真实水下图像中鱼体姿态非水平
随机缩放0.5 到 1.5 倍模拟不同拍摄距离
轻度高斯模糊有 20% 概率应用模拟水下浑浊环境中图像退化

一个重要发现是:Mosaic 增强在通用目标检测中几乎是标配,但对这个细粒度任务反而是负优化。Mosaic 会把四张图的拼接边直接切过鱼体部位,导致训练时模型看到的“鱼”经常是半截的。蝴蝶鱼分类依赖的关键判别区域(如体侧条纹、尾鳍形态)一旦在拼接过程中被切掉,模型就会学到残缺形状与类别的错误关联。实测关闭 Mosaic 后,val 集的 mAP50 从 84.8% 提升到 86.2%。

4. 训练实战:配置、参数与完整过程

4.1 环境准备与依赖安装

本次训练使用的硬件环境是单张 RTX 4090(24GB 显存)。软件环境方面,Python 版本 3.9,PyTorch 2.0.1,CUDA 11.8。yolov10 的官方仓库基于 ultralytics 框架,安装依赖时需要注意版本兼容性:ultralytics 8.1.0 及以上版本才对 yolov10 的非 NMS 推理模式支持得比较完整,低于这个版本会出现前向推理报错。

安装命令很简单:

git clone https://github.com/THU-MIG/yolov10.git cd yolov10 pip install -r requirements.txt pip install -e .

这里有一个实操细节:yolov10 官方仓库的 requirements.txt 里没有显式锁定 ultralytics 的版本号,在部分环境里 pip 会拉到最新版本,而最新版本与 yolov10 代码中的部分接口不兼容。建议手动指定 ultralytics 版本为 8.1.2:

pip install ultralytics==8.1.2

4.2 配置文件修改

模型结构配置文件的修改是整个改进中最关键的代码操作。官方仓库中ultralytics/cfg/models/v10/yolov10n.yaml定义了模型结构,我在此基础上创建了一个新配置文件yolov10n_spd.yaml。

核心改动逻辑如下:

  • 将backbone中第一层Conv的 stride 从 2 改为 1,并把该层替换为 SPDConv 模块。具体实现中,使用SPDConv类替代原来的Conv类,SPDConv 内部完成 space-to-depth 和 1x1 卷积的级联,输出的是 stride=1 的特征图
  • 将backbone中 stage2 的 C2f 模块替换为C2f_SPD模块。该模块内部在第一个 1x1 卷积后插入了一层 SPDConv,用于进一步下采样特征图

在实际代码操作中,我直接修改了ultralytics/nn/modules/conv.py文件,在原有 Conv 类旁边新增了 SPDConv 类。随后在ultralytics/nn/tasks.py中注册了新的模块名,调用parse_model时就能正确解析配置文件中的SPDConv与C2f_SPD字段。

训练时加载模型结构的命令如下:

yolo detect train data=bfs.yaml model=yolov10n_spd.yaml pretrained=yolov10n.pt epochs=200 batch=32 imgsz=640 device=0

数据集的配置文件bfs.yaml格式如下:

path: /home/user/datasets/butterflyfish train: images/train val: images/val test: images/test nc: 15 names: ['beautiful', 'copperband', 'blackback', 'yellowhighfin', 'orange', ' ... ]

4.3 训练参数选择与调优

训练参数的选择直接影响最终效果。我第一轮训练用了默认参数,epochs=100,batch=16,结果是 mAP50 只有 79.8%,远低于预期。逐步调整后,发现几个关键参数对结果的影响非常大。

batch size 对训练稳定性的影响最明显。在 nano 模型 + 4600 张图像的情况下,batch size 从 16 增大到 32 之后,训练 loss 曲线明显更平滑,val mAP 提升了约 1.4 个点。原因在于更大的 batch 提供了更准确的梯度估计,尤其在类别不平衡的情况下(如美丽蝴蝶鱼的样本量可能是黄高鳍蝴蝶鱼的 3 倍),梯度噪声的降低有助于模型更稳定地学习稀有类别的特征。

epochs 的选择需要结合早停机制。我将 epochs 设置为 300,同时开启 patience=30,即连续 30 个 epoch 在验证集上没有改进时就提前停止。实测最终训练在 198 个 epoch 时触发了早停,这比硬性设定 300 个 epoch 节省了大约三分之一的时间。

optimizer 选择上,我对比了 SGD 和 AdamW。在细粒度检测任务上,AdamW 在前 50 个 epoch 收敛更快,但最终的 mAP 不如 SGD 微调后的结果。我的做法是先用 AdamW 训练 100 个 epoch 作为预训练,再切换到 SGD(momentum=0.937, weight_decay=0.0005)微调剩余 epoch。这个两阶段策略最后比单一优化器高出了约 1.8 个点。

以下是我的最终训练配置:

参数数值备注
imgsz640输入分辨率
batch32单卡
epochs300早停 patience=30
optimizerAdamW 100ep + SGD 200ep两阶段
lr00.01初始学习率
lrf0.01最终学习率衰减系数
warmup_epochs3预热轮数
weight_decay0.0005仅 SGD 阶段
hsv_h0.2色彩增强幅度
mosaic0关闭

5. 效果对比与结果分析

5.1 改进前后指标对比

训练完成后,我在同一个测试集上对原版 yolov10n 和改进版 yolov10n-SPDConv 做了对比评估。测试集包含 460 张图像,涵盖 15 个类别,其中小目标(像素面积小于 32x32)占比约 42%,中等目标占比 38%,大目标占比 20%。

模型mAP50mAP50-95参数量推理耗时(ms)
yolov10n 原版81.6%57.4%2.3M4.2
yolov10n + SPDConv88.3%64.1%2.6M5.1
yolov10n + 全部SPDConv替换82.4%58.9%2.9M6.3

改进版在 mAP50 上提升了 6.7 个百分点,mAP50-95 提升了 6.7 个百分点,推理耗时增加了 0.9ms,参数量增加了 0.3M。

值得深入看一下的是不同目标尺度下的表现差异。按照目标像素面积分组统计 AP:

目标尺寸yolov10nyolov10n-SPDConv提升幅度
小目标 (<32x32)64.8%76.2%+11.4%
中目标 (32x32到96x96)84.3%89.9%+5.6%
大目标 (>96x96)92.1%93.6%+1.5%

这个数据验证了 SPDConv 的核心价值:目标越小,信息保留带来的收益越大。大目标由于本身包含的像素多,每次下采样丢掉的 75% 信息中可能仍然有足够多的关键特征留下来,因此提升不明显。

5.2 误检与漏检案例分析

指标提升之外,我还系统性地分析了改进前后模型的错误模式差异。这部分的发现对后续继续优化非常有价值。

原版 yolov10n 最突出的问题是将背景中的珊瑚纹理误检为蝴蝶鱼,尤其在鹿角珊瑚密集的区域,B 类误报率很高。检查具体误检样本发现,误检区域的纹理密度和频率分布确实与某些蝴蝶鱼的体表网格纹高度相似。SPDConv 改进版在这个问题上有明显缓解,因为 1x1 卷积层在融合 space-to-depth 特征时,能够结合通道维度的上下文重新校准纹理特征,相当于对“像鱼又不像鱼”的区域做了一次更严格的甄别。

漏检方面,改进版仍然存在两个明显短板。一是黄色系蝴蝶鱼在黄色珊瑚背景中的漏检率偏高达 18%,原版模型中这个数字是 26%,改进版虽然改善了但还不够。说明这类目标的区分信息依赖的主要是形态轮廓而不是颜色纹理,SPDConv 对颜色信息没有直接增益。二是快速游动状态下被拍成拖影的个体,两类模型都几乎没有检出能力,这个问题的根源不在网络结构,而在数据集中缺少运动模糊样本,后续计划用 ESRGAN 做模糊增强后补充训练。

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

6.1 训练崩溃与梯度异常

SPDConv 改造过程中最容易遇到的坑是训练 loss 直接发散为 NaN。这个现象通常出现在训练的前 10 个 epoch 内,原因是空间到深度变换后通道数增大了 4 倍,BN 层的统计量计算出现了数值不稳定。

我在第一次实验中也踩了这个坑,排查过程如下:

  • 第一步检查数据输入,确认没有脏数据后排除
  • 第二步检查学习率,lr0 从 0.01 降到 0.001 后问题依旧,说明不是学习率过高
  • 第三步检查 BN 层的 eps 参数,将 eps 从默认的 1e-3 调整到 1e-5 后问题解决

原因在于 SPDConv 输出的特征图通道数增加后,BN 层计算方差时的最小值被 eps 钳制住了,一旦某条通道数值分布范围较窄,就会因为方差除了一个过大的 eps 导致梯度爆炸。这个坑在各类基于 SPDConv 的改进中都很常见,遇到 NaN 时优先排查这个位置。

6.2 推理输出框偏移

第二个常见坑是修改模型结构后,推理输出的目标框位置整体向图像左上角偏移。这个问题在训练时不容易暴露(因为训练 loss 会正常下降),但部署后非常致命。

排查后确认原因是我在修改 backbone 的 stem 层时,space-to-depth 变换改变了特征图的空间排列顺序,但后续用于解码目标框坐标的 anchor 网格没有同步调整。具体来说,space-to-depth 把原始图像的 4 个相邻像素(左上、右上、左下、右下)重新排列到通道维,相当于把特征图的感受野中心移动了半个像素,这个偏移量在多层下采样中积累后,最终输出的框坐标就会出现系统性偏移。

解决办法是在解码或后处理阶段,对输出坐标加上一个补偿项half_step * stride。这不是常规操作,需要根据你实际安排的 SPDConv 模块数量和位置推算偏移量。我的经验是每使用一个 SPDConv 替换 stride=2 卷积,就在最终输出中累加一次半个像素的偏移。

6.3 小目标检测不到的调试方法

如果你在训练后发现自己项目里的小目标仍然大量漏检,可以尝试以下几个排查方向:

先检查验证集上模型是否有检测结果但没有正确分类。如果是分类错误,说明模型的语义特征提取能力足够,但细粒度区分能力不足,优先考虑增加分类损失权重或者引入注意力机制。如果根本没有检出框,说明目标信号在特征提取阶段就已经丢失了。这时候需要做特征图可视化,确认小目标物体在 backbone 的第几层输出中消失了。我之前在调试时用热力图可视化发现,部分暗色蝴蝶鱼在深层特征图中的信号几乎为 0,原因是多次下采样后它们的像素强度衰减到了背景水平。这个现象的解决方法是在对应层增加辅助监督,或者把检测头特征图的融合深度加深。

6.4 常见问题速查表

现象可能原因解决方案
训练 Loss 出现 NaNSPDConv 后 BN 的 eps 过小或学习率过高将 BN eps 调到 1e-5,降低初始学习率
推理框偏移space-to-depth 引入了像素偏移在解码层补偿 half_step * stride
小目标完全不检多次下采样后信号衰减可视化特征图确定衰减层,增加跨层连接
同类不同色误检数据集色彩空间不一致统一白平衡预处理,扩大 HSV 扰动范围
模型收敛慢SPDConv 替换过多,通道膨胀减少替换层数,优先替换浅层
部署帧率低推理链路未做 TensorRT 转换导出为 TensorRT engine 并开启 FP16

7. 部署与后续扩展方向

训练完成后的一步是模型导出。由于项目最终目标是用在边缘设备上,我在导出的时候做了两件关键操作:一是把模型转成 ONNX 格式,然后转为 TensorRT engine,期间开启了 FP16 精度,精度损失只有 0.8 个点左右,但推理速度提升了一倍;二是在转 ONNX 阶段把--simplify参数打开,移除掉部分冗余节点后大小从 6.4MB 降到了 4.9MB。

部署过程中还有一个小发现:yolov10n 的 NMS-free 特性对 TensorRT 非常友好,不需要额外编写自定义 NMS plugin,整个导出过程没有遇到兼容性的坑。相比以往使用 yolov5 或 yolov8 在 TensorRT 上还要处理 NMS 插件兼容性来说,这确实省了不少事。

这个项目后续我计划在三个方向继续扩展。一是收集更多包含幼鱼和不同地理种群个体的图像,把类别从 15 种扩展到 30 种左右,解决当前部分物种种内差异过大导致的混淆问题。二是引入水下测距信息,结合双目立体视觉得到的目标深度,利用距离信息辅助判断目标实际大小,从而排除一些因拍摄距离过近而放大外观差异的误判。三是尝试将分类分支与检测分支解耦,先做检测再做细粒度分类,使用更大的图像分辨率作为分类网络输入,这个方案在理论上会更适合这种细粒度识别场景,目前正在实验阶段。

做这个项目时我最深刻的体会是,模型改进不是无脑往上堆模块,每一项结构改动都要回到任务本身去寻找依据。蝴蝶鱼识别需要保留纹理信息,SPDConv 的价值在理论上说得通,实测也验证了小目标场景下收益最大。但同样的技巧如果直接用在大目标场景,收益是边际递减的。做改进实验之前,建议大家先统计自己数据集中的目标尺寸分布,用数字说话来决定是否值得引入这类结构优化。

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

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

立即咨询