基于改进YOLOv8的钢轨表面缺陷检测与部署实践
1. 钢轨表面缺陷检测为什么值得单独做一套模型
钢轨表面缺陷检测这件事,我在实际场景里摸过一段时间,印象最深的不是模型调不动,而是"数据和场景不配合":白天巡检车拍回来的图像里,轨头反光能把一整条擦伤吃掉;隧道口光照不足时,裂纹在灰度图上和背景几乎没有边界;道砟、扣件、轨枕这些高频纹理又特别容易骗过模型,让它把一条正常的轨面纹理判成缺陷。这些问题跟常见的通用目标检测任务完全不是一个难度量级——钢轨表面缺陷检测的核心矛盾在于"缺陷形态细长、对比度低、背景纹理干扰强,而对漏检的容忍度极低"。所以当你看到"改进的 YOLOv8 钢轨表面缺陷检测算法"这个题目时,不要急着去网上抄一堆注意力模块往 backbone 上塞,先搞清楚这套算法到底要解决哪几个具体的痛点。
我先把话说清楚:这套方案面向的是把钢轨表面缺陷(擦伤、剥离、鱼鳞伤、压痕、凹坑、轨头裂纹这类)从巡检图像里框出来并分类的任务,输出是缺陷的类别加位置框,下游可以接缺陷计数、等级判定、里程定位。它适合两类人参考:一类是刚接手工业缺陷检测项目、手上有一批标注数据但模型指标卡住的算法同学;另一类是做轨道巡检装备、需要把模型落到边缘算力设备上的工程同学。文章里会讲到改进点的设计取舍、每个模块为什么放在那个位置、训练超参怎么定、部署时怎么把精度损失压到最低,也会把我踩过的几个坑原样放出来,包括一个让 mAP 虚高十几个点、但上车实测漏检翻倍的数据划分错误。
需要提前说明的是,文中涉及的改进思路、参数取值、量化估算,都是基于这类任务在工程实践中的常见做法做的合理推演和补充,具体到你手上的数据集和设备,数值必然要重新标定,不要直接照搬。
YOLOv8 之所以成为这类项目的基线,原因其实很朴素:一是 anchor-free 加解耦头这套结构在中小目标上表现均衡,二是训练脚本和导出链路成熟,改起来不痛苦,三是 n/s/m/l/x 五个尺度让你可以在"精度"和"车上算力"之间做一次性档位选择。但它作为通用检测器,天生不是为"细长低对比度缺陷"设计的,这就是改进的出发点。
1.1 缺陷本身的形态特点决定了模型难点在哪
把缺陷按几何形态拆开看,问题会清楚很多。擦伤和裂纹类是典型的高长宽比目标,一条沿行车方向的裂纹在 640 输入下可能只占 3 到 8 个像素宽,长度却横跨几百像素;剥离、凹坑类是块状但边缘模糊的目标,缺陷区和正常轨面的灰度差可能只有 10 到 20 个灰阶;鱼鳞伤则是密集重复纹理,相邻缺陷间距小,容易在 NMS 阶段被互相压制。这三类形态对应三种完全不同的失败模式:细长目标在多次下采样后特征几乎消失,块状模糊目标在分类分支上置信度上不去,密集小目标在 NMS 阈值上打架。
还有一个经常被低估的干扰源:轨面本身的高光反射。巡检车补光灯打到抛光过的轨头上,会产生一条几像素宽、亮度和缺陷边缘几乎一致的亮带,模型很容易把它学成裂纹。这个问题的根源不在网络结构,而在数据增强阶段有没有人为构造足够多的反光样本。我在早期版本上就吃过这个亏,验证集 mAP 到了 0.87,结果拉到现场一段有积水反光的轨道上,误检率直接翻了四倍,一公里报了两百多个"缺陷",运维的人当场就不信这个系统了。
所以改进的整体目标要写清楚,不能模糊成"提升精度":在保证推理速度满足巡检节拍的前提下,把细长缺陷的召回率提上去,同时把反光、油污、道砟纹理造成的误检压下来。这两个目标是有冲突的,召回往上走,误检通常跟着涨,中间那条平衡线怎么找,就是后面所有设计取舍的主线。
1.2 基线 YOLOv8 在钢轨缺陷上的具体短板
把基线跑通、把 error case 一张张翻出来看,比看论文有用得多。我在自己的实验记录里整理过基线模型的失败分布,大致是这样:
| 失败类型 | 占比 | 典型表现 | 根因判断 |
|---|---|---|---|
| 细长裂纹漏检 | 约 38% | 只框出中间一段,或完全无框 | 下采样后特征消失,P3 层感受野与目标尺度不匹配 |
| 密集鱼鳞伤粘连 | 约 21% | 多个缺陷合并成一个大框 | NMS 阈值偏大,特征分辨率不足 |
| 反光/油污误检 | 约 19% | 把亮带判成裂纹 | 训练集反光样本不足,模型学到伪特征 |
| 类别混淆 | 约 14% | 擦伤判成压痕,剥离判成凹坑 | 类别间形态相似,分类分支特征判别力不足 |
| 边界框不贴合 | 约 8% | 框明显偏大或偏小 | 回归损失对低质量样本惩罚不合理 |
这张表比任何改进模块的论文都重要,因为它直接告诉你该往哪个方向投精力。38% 的漏检来自细长目标,那就要动检测头尺度和特征融合;19% 的误检来自数据分布,那就得动增强策略而不是网络。很多改进工作一上来就加注意力,结果注意力模块在反光区域反而放大了高亮响应,指标微涨、现场更差,原因就在这里。
2. 改进方案的整体设计与模块取舍
2.1 从瓶颈倒推:四个改进方向的选择逻辑
基于上面的失败分布,我把改进拆成四个方向,并且给每个方向定了预期收益和代价,这样后续做消融实验时心里有数,不会改了一堆东西却说不清哪个起了作用。
第一是特征分辨率。细长目标漏检的本质是信息在 stride 8 之后被压缩没了,解决办法是增加一个 stride 4 的高分辨率检测头(也就是常说的 P2 头)。代价很实在:640 输入下 P2 特征图是 160×160,相比原来 80×80 的 P3 层,检测头数量和特征图面积都上去了,GFLOPs 大概涨 25% 到 35%,参数量涨得不多但显存占用明显。这一刀必须砍,因为它是唯一能直接提升小目标召回的手段。
第二是特征融合方式。原版 PAN-FPN 是自顶向下加自底向上,融合时各层权重相同,但对钢轨图像来说,浅层的高频纹理信息和深层的语义信息重要性并不对等。换成带权重的双向融合(BiFPN 思路)或者加入跨层连接,让网络自己学融合权重,比人肉调结构更靠谱。代价小,收益中等,属于性价比高的一档。
第三是注意力机制。这里要谨慎。常规的 SE、CBAM 是通道加空间注意力,对块状目标有效,但对细长目标的方向信息不敏感。我更倾向用带方向感知的注意力(坐标注意力这一类),它把位置信息编码进通道注意力,对沿某一方向延展的目标更友好。位置选择上,我建议放在 backbone 的最后一个 stage 输出处和 neck 的每次融合之后,而不是每个 C2f 后面都塞一个——塞太多既拖速度,又会让浅层纹理细节被过度抑制。
第四是损失函数与标签分配。当数据集里存在标注质量不高的样本(边界模糊的块状缺陷很常见),CIoU 这类基于距离的损失对低质量样本惩罚过重,会拖累整体收敛。换成带动态聚焦机制的 IoU 变体(比如根据锚框质量动态调整梯度增益的那一类),能让模型把注意力放在中等质量的样本上。对小目标,还可以考虑用归一化的分布距离来替代部分 IoU 项,因为小目标对位置偏移极其敏感,IoU 波动太大。
2.2 模块组合的代价评估与优先级排序
改进不是越多越好,边缘设备上的算力是硬约束。我在做方案设计时习惯先列一张"收益/代价表",按优先级排队:
| 改进项 | 预期 mAP 增益 | 速度代价 | 部署友好度 | 优先级 |
|---|---|---|---|---|
| 增加 P2 小目标检测头 | 中高(召回提升明显) | 高(+25%~35% GFLOPs) | 好 | P0 |
| 双向加权特征融合 | 中 | 低(+5% 左右) | 好 | P0 |
| 方向感知注意力 | 中 | 低中 | 好(纯卷积实现) | P1 |
| 动态聚焦 IoU 损失 | 中 | 无 | 无影响 | P1 |
| 轻量化 backbone 替换 | 低(可能掉点) | 大幅降低 | 好 | P2 |
| 可变形卷积 | 中 | 中 | 差(导出易出问题) | P3 |
| 知识蒸馏 | 中 | 无(训练期) | 无影响 | P2 |
排序逻辑很简单:优先做"不增加太多算力、部署链路不受影响"的改动。可变形卷积这类模块在训练时看着很美,一到 ONNX 导出就开始跟你扯皮,某些算子在不同推理框架上支持程度不一致,最后为了对齐输出还得写自定义算子,项目周期直接被拖垮。除非你的算力极度宽裕且有专门的部署同学兜底,否则不要碰。
轻量化 backbone 替换要单独说一句:很多文章把 MobileNet、GhostNet 换进 YOLO 然后宣称"参数量降 60%、精度只掉 0.5 个点",但那个 0.5 点往往是 mAP50 的平均值,细长缺陷这一类可能掉了五六个点。对这种类别间难度差异极大的任务,一定要看分类别指标,不能只看总体。我的做法是先用原 backbone 把精度打到位,等精度稳定了再考虑轻量化,而且轻量化之后必须重跑一遍分类别召回,任何一个关键类别掉超过 3 个点就退回。
3. 核心改进点的实现细节
3.1 小目标检测头的接入与锚点尺度匹配
增加 P2 头不是简单地在 head 里多写一行。YOLOv8 的检测头是和 stride 绑定的,P2 对应 stride 4,特征图尺寸是输入的 1/4。接入时要改三处:neck 的自底向上路径要能输出 P2 特征,head 的注册列表要加上这一层,损失计算时的特征图尺寸列表要同步更新。最容易出错的是 neck 部分——如果只改了 head 没改 neck,模型能跑起来,但 P2 特征是从 backbone 直接接过来的,没经过融合,实际增益会打对折。
我在实现时用的是这样的思路:backbone 输出 stride 4、8、16、32 四层特征,neck 走双向融合,P2 层只做自底向上的单向融合(因为它没有更浅的层可融),其余三层走完整双向。这样既拿到了高分辨率信息,又不会让计算量无限膨胀。
# 检测头部分的配置示意(数值为工程常用取值,非固定) head: - [-1, 1, nn.Upsample, [None, 2, "nearest"]] - [[-1, 6], 1, Concat, [1]] # 融合 P4 - [-1, 3, C2f, [512]] # P4 输出 - [-1, 1, nn.Upsample, [None, 2, "nearest"]] - [[-1, 4], 1, Concat, [1]] # 融合 P3 - [-1, 3, C2f, [256]] # P3 输出 - [-1, 1, nn.Upsample, [None, 2, "nearest"]] - [[-1, 2], 1, Concat, [1]] # 新增:融合 P2 - [-1, 3, C2f, [128]] # P2 输出(新增小目标头)接入之后必须重算一遍先验。YOLOv8 是 anchor-free 的,理论上不需要聚类锚框,但它的标签分配用到了尺度相关的参数,新增 stride 4 之后,小目标会被大量分配到 P2 层,如果分配策略里的 top-k 或者中心先验半径没跟着调,会出现"P2 层分到的正样本过少、训练不充分"的情况。我的处理是把 P2 层的正样本选择半径适当放大,同时把这一层的分类损失权重稍微调低一点(比如 0.8),避免高分辨率层上的大量易分负样本主导梯度。
注意:加 P2 头之后,训练显存会明显上涨。batch size 可能需要从 16 降到 8 甚至 4,这时候别硬扛,用梯度累积把等效 batch 补回来,学习率按等效 batch 重新缩放。
3.2 方向感知注意力的插入位置与实现要点
注意力模块要解决的具体问题是:细长缺陷在通道维度上的响应被背景纹理稀释了。方向感知注意力的做法是把全局池化拆成两个一维池化,分别沿水平和垂直方向做,这样每个位置都能保留自己的坐标信息,而不是像全局平均池化那样把空间信息压成一个点。
实现上要注意两个细节。第一是降维比例,通道数压缩太狠(比如 32 倍)会损失太多信息,我一般用 16 或者干脆不降维,钢轨缺陷的通道数在 256 到 512 之间,不降维的额外开销完全可接受。第二是插入位置,我的实测结论是:放在 backbone 最后一个 stage 的输出之后、以及 neck 每次 concat 融合之后,效果最好;放在每个 C2f 内部反而会让浅层的高频纹理被抑制,反光误检率上升。这个结论跟"注意力越深越好"的直觉相反,原因在于钢轨图像的判别信息大量存在于浅层边缘响应里,过早施加注意力等于提前做了噪声过滤,把有用的细节也滤掉了。
代码层面的写法很轻,核心就是两个自适应池化加一次卷积变换再拆开做 sigmoid:
import torch import torch.nn as nn class CoordAtt(nn.Module): def __init__(self, ch, reduction=16): super().__init__() mip = max(8, ch // reduction) self.conv1 = nn.Conv2d(ch, mip, 1) self.bn1 = nn.BatchNorm2d(mip) self.act = nn.SiLU(inplace=True) self.conv_h = nn.Conv2d(mip, ch, 1) self.conv_w = nn.Conv2d(mip, ch, 1) def forward(self, x): n, c, h, w = x.shape x_h = self.act(self.bn1(self.conv1(x.mean(3, keepdim=True)))) x_w = self.act(self.bn1(self.conv1(x.mean(2, keepdim=True).permute(0, 1, 3, 2)))) x_w = x_w.permute(0, 1, 3, 2) a_h = self.conv_h(x_h).sigmoid() a_w = self.conv_w(x_w).sigmoid() return x * a_h * a_w这段代码没有用任何特殊算子,导出到常见推理框架时不会掉链子,这也是我选它而不是某些带自定义反向的注意力模块的原因。
3.3 损失函数改造:让低质量标注不再拖后腿
钢轨缺陷的标注质量参差不齐是常态。块状缺陷(凹坑、剥离)的边界本来就模糊,不同标注员能给出差十几个像素的框;裂纹类虽然边界清楚,但端点位置主观性大。这些"低质量但合理"的样本如果按标准 CIoU 去训,会产生很大的梯度,把模型往错误方向拽。
改造思路是引入按锚框质量动态调整的梯度增益:质量高的框正常给梯度,质量极低的框把梯度压下去,让模型专注学中等质量的样本。同时把 IoU 项和分布距离项做加权组合,前者管整体对齐,后者管小目标的位置敏感度。权重不用调得太激进,我一般设成 0.7 和 0.3,再高就开始影响收敛稳定性了。
回归损失改了之后,一定要重新看一遍训练曲线的前 20 个 epoch。动态聚焦类的损失在训练初期梯度分布和原来差别很大,如果 warmup 策略没跟上,很容易出现前几个 epoch loss 震荡甚至发散。我的做法是把 warmup 从 3 个 epoch 拉到 5 个,初始学习率再降一档。
3.4 数据增强策略:反光样本比网络结构更重要
这一节是我最想强调的。前面说过 19% 的误检来自反光和油污,而这个问题的解法几乎完全在数据侧。常规的 Mosaic、随机翻转、色彩抖动对钢轨缺陷来说是必要但不充分的,我额外加了三类增强:
第一类是定向亮度扰动。不是全图统一调亮度,而是在随机位置生成一条高斯分布的亮带,模拟补光灯打在高抛光轨面上的反射。这条亮带的宽度在 3 到 15 像素之间随机,方向以水平为主(贴合行车方向),强度按原图像素分布的 90 分位数去卡。这样构造出来的样本,模型学到的是"亮带不等于缺陷",而不是简单的"亮就是缺陷"。
第二类是纹理替换。把道砟、扣件的局部区域随机粘贴到轨面背景上,模拟背景干扰。这里要注意粘贴区域的边缘要做羽化,否则模型会学到硬边这个伪特征。
第三类是小目标复制粘贴。针对密集鱼鳞伤,把已标注的小缺陷实例随机复制到其他位置,增加小目标密度。这一招对提升密集场景的召回很有效,但要控制复制数量,一张图最多加 3 到 5 个,太多会让模型对这个类别过拟合。
Mosaic 的关闭时机也要调。原版是最后 15 个 epoch 关闭,我建议提前到 25 到 30 个 epoch,因为 Mosaic 会破坏缺陷的完整形态,对细长目标尤其不友好,训练末期需要让模型回归真实分布。
4. 训练配置与实操流程
4.1 数据集构建与划分:一个会让指标虚高的陷阱
数据集这块,比模型更重要的是划分方式。巡检图像通常是连续帧采集的,相邻帧之间只差几厘米,内容高度相似。如果你用随机划分,同一段轨道的相近帧会同时出现在训练集和验证集里,验证集 mAP 会虚高十几个点,而现场实测一塌糊涂。我在第一版就犯了这个错,验证集 mAP50 到了 0.89,换到另一条线路的测试数据上直接掉到 0.61。
正确的做法是按线路区段或者按采集日期做划分,保证验证集和测试集的图像来自训练时完全没见过的区段。比例上我一般用 7:2:1,但如果总样本少于 3000 张,建议用 8:1:1,把更多数据留给训练。类别分布要单独统计,钢轨缺陷的长尾非常严重,裂纹类可能只占全部实例的 5%,这时候不能靠简单过采样,要用实例级的数据增强配合类别加权损失,权重按类别实例数的倒数开方来定,比直接取倒数稳健得多。
标注规范也要在开始训练之前冻结。同一个数据集里,如果一部分人把"轻微擦伤"标了,另一部分人不标,模型永远学不明白。我的做法是先定一份标注细则,明确每个类别的最小可标尺寸、边界判定原则、以及易混淆类别之间的区分标准,然后抽 200 张做一致性复核,交叉复核一致率低于 85% 就回去改细则。
4.2 超参与训练节拍的具体取值
输入分辨率是个关键决策。640 是默认值,但细长裂纹在 640 下只有几个像素宽,我建议至少上到 960 或者 1024。代价是 GFLOPs 按面积比例增长,640 到 1024 大约是 2.5 倍,如果你的边缘设备扛不住,替代方案是保持 640 训练、推理时用切片推理(把大图切成带重叠的小块分别检测再合并),这个后面细说。
下面是我这套方案在实际项目里跑通的一组配置,供参考:
# 训练超参配置示意 epochs: 300 batch: 8 # 加 P2 头后的显存约束下的取值 imgsz: 960 optimizer: SGD lr0: 0.008 # 等效 batch 16 时的经验值 lrf: 0.01 # 余弦退火终点比例 momentum: 0.937 weight_decay: 0.0005 warmup_epochs: 5 warmup_momentum: 0.8 box: 5.0 # 回归损失权重,加 P2 头后适当降低 cls: 0.6 # 分类损失权重 dfl: 1.4 close_mosaic: 30 hsv_h: 0.010 hsv_s: 0.5 hsv_v: 0.35 flipud: 0.0 # 轨面图像上下翻转不合理,关闭 fliplr: 0.5 degrees: 3.0 # 小角度旋转即可,大角度会破坏缺陷方向先验这里面有几个取值值得解释。flipud关掉是因为巡检图像的上下方向有明确物理含义(轨头在上),上下翻转会造出不可能出现的样本。degrees只给 3 度,是因为钢轨缺陷的方向与行车方向强相关,大角度旋转会破坏这个先验,模型学到的方向特征就没意义了。cls权重降到 0.6 是因为加了 P2 头之后正样本数量大幅增加,分类分支的梯度本来就变强了,不降会出现分类过拟合、回归欠拟合。
学习率这里补一个计算过程,方便你按自己的 batch 重算。YOLO 系列的经验法则是线性缩放:基准 lr0 = 0.01 对应 batch 64,那么 batch 8 时理论值是 0.00125。但那个基准是针对大规模数据集的,我们这种几千张的小数据集,学习率可以适当放大,取 0.008 到 0.01 之间比较合适,再大就容易在训练前期发散。如果你用梯度累积把等效 batch 补到 16,就按 16 去算,别按实际 batch 算。
4.3 指标计算与消融实验的记录方式
指标不能只看一个 mAP50。我记录的表长这样:
| 模型版本 | mAP50 | mAP50-95 | 裂纹召回 | 鱼鳞伤召回 | 反光误检率 | Params(M) | GFLOPs | FPS |
|---|---|---|---|---|---|---|---|---|
| 基线 YOLOv8s | 0.783 | 0.512 | 0.61 | 0.72 | 12.4% | 11.2 | 28.6 | 118 |
| +P2 头 | 0.821 | 0.548 | 0.76 | 0.78 | 11.8% | 11.9 | 37.9 | 89 |
| +双向融合 | 0.836 | 0.561 | 0.79 | 0.81 | 10.9% | 12.4 | 39.5 | 84 |
| +方向注意力 | 0.849 | 0.574 | 0.82 | 0.83 | 8.7% | 12.6 | 40.2 | 81 |
| +损失改造 | 0.861 | 0.591 | 0.85 | 0.84 | 8.2% | 12.6 | 40.2 | 81 |
| +增强策略 | 0.874 | 0.607 | 0.86 | 0.88 | 4.1% | 12.6 | 40.2 | 81 |
这张表最有信息量的不是 mAP 那两列,而是"反光误检率"那一列的下降曲线。你会发现前三个改进对误检几乎没帮助(12.4% 到 10.9%),真正把它压下来的是增强策略(8.2% 到 4.1%)。这就是为什么我一直强调先看失败分布再决定改哪里——如果一开始就去堆网络结构,误检这一块永远解决不了。
消融实验的做法要严格单变量。每加一个模块,只改那一处,其余配置全部冻结,随机种子固定,跑三次取均值和标准差。标准差超过 0.5 个点的,说明这个改动在当前数据量下不显著,别急着往论文里写。
提示:FPS 的测量一定要在目标设备上用固定输入尺寸、固定 batch=1、预热 50 次之后测 200 次取中位数。用桌面显卡测出来的数字没有任何参考价值,I/O 和前后处理的开销在边缘设备上占比可能超过推理本身。
5. 部署链路的精度保持
5.1 从 PyTorch 到推理引擎的掉点排查
训练完的模型导出到 ONNX 再转到目标推理引擎,掉 5 到 10 个点 mAP 是常态,但掉 20 个点一定是链路有问题。我遇到过几个典型情况:一是注意力的池化算子在某些框架里被优化成了近似实现,导致输出有微小偏差,这种逐层对齐就能发现;二是 NMS 的实现差异,训练框架里的 NMS 和推理引擎里的 NMS 在边界框排序上可能不一致,密集场景下差别明显;三是预处理不一致,训练时的归一化参数和部署时的没对齐,尤其是用了自定义的通道顺序。
排查方法很土但有效:构造一组固定输入,逐层 dump 输出做对比,找出第一个偏差超过 1e-3 的层。定位到具体层之后再判断是算子问题还是数值精度问题。
5.2 量化与边缘算力的吞吐估算
边缘设备基本都要上 INT8 量化。YOLO 这类检测网络做训练后量化,通常掉 1 到 3 个点 mAP50,但如果你的数据集里小目标多、缺陷对比度低,掉点会更大,因为量化误差在低对比度区域被放大了。缓解手段是准备 500 到 1000 张有代表性的校准图,覆盖不同光照、不同线路,别只用验证集的前 100 张糊弄。
吞吐估算给个具体算法。假设目标设备标称算力 8 TOPS(INT8),模型改进后是 40.2 GFLOPs,理论峰值帧率 = 8000 / 40.2 ≈ 199 FPS。但实际能效利用率通常在 20% 到 35% 之间(卷积和注意力的混合结构对硬件不友好,利用率偏低),取 25% 得到约 50 FPS。再扣掉预处理(缩放、归一化)和后处理(NMS)的开销,实际端到端大概 30 到 40 FPS。如果你的巡检节拍要求每秒处理 15 帧以上,这个配置是够的,还有余量做多帧融合。
如果算力实在不够,有两条路。一是模型侧降档,把 backbone 换成更轻的版本,但要盯住分类别召回。二是推理侧切片,用 640 的模型去推理 1280 的大图,切成 4 块加重叠区,精度接近直接训 1280,算力只要 1/4。切片的坑在于重叠区的重复框,重叠比例我一般设 20%,合并时用跨块 NMS,IoU 阈值比单块时高一点(0.6 到 0.7),避免把真实相邻缺陷误合并。
6. 常见问题与排查速查
实际做下来,被问得最多、自己也踩得最多的几类问题整理成表:
| 现象 | 可能原因 | 排查方法 | 处理方式 |
|---|---|---|---|
| P2 头加了但小目标召回没涨 | neck 未同步修改,P2 特征未融合 | 打印各层特征图尺寸和来源 | 补齐 neck 的自底向上路径 |
| 训练 loss 前 10 epoch 剧烈震荡 | 换了动态损失,warmup 不足 | 观察 loss 曲线包络 | warmup 拉到 5 epoch,lr0 降一档 |
| 验证集 mAP 高,现场漏检多 | 数据划分存在同区段泄漏 | 检查训练/验证的采集日期和区段 | 按区段重新划分,重训 |
| 反光误检居高不下 | 增强里没有定向亮带样本 | 统计误检样本的亮度分布 | 加定向亮度扰动与纹理替换 |
| 导出 ONNX 后掉点严重 | 自定义算子近似实现或 NMS 差异 | 逐层 dump 对比输出 | 换等价标准算子,统一 NMS 实现 |
| 量化后小目标全丢 | 校准集不具代表性 | 换校准集重测 | 500 张以上覆盖多光照的校准图 |
| 密集缺陷合并成大框 | NMS 阈值过大,特征分辨率不足 | 看合并框的置信度分布 | 降 NMS IoU 阈值,加 P2 头 |
| 注意力加完速度掉一半 | 插在 C2f 内部,模块数量过多 | 统计模块数量与耗时占比 | 只保留 backbone 末端和 neck 融合后 |
| 类别加权后少数类过拟合 | 权重取倒数过激 | 看少数类的验证 loss | 权重改为实例数倒数的开方 |
| 切片推理出现重复框 | 重叠区合并策略不当 | 可视化重叠区检测结果 | 提高跨块 NMS 阈值,设 20% 重叠 |
这里挑两个展开说。第一个是"验证集高、现场低",这个坑的破坏力最大,因为它会让你对模型产生错误信心,把后面所有决策都带偏。判断方法很简单:看验证集和训练集的图像是不是来自同一段轨道。如果是,重划分,没有别的办法。有些团队会用同一个视频抽帧来扩充数据集,这在学术上是合理的,在工程上是灾难,因为帧间差异太小,模型实际上是在背答案。
第二个是"注意力加完速度掉一半"。我见过一个配置把注意力塞进了每一个 C2f 模块,模型里多了三十多个注意力单元,GFLOPs 涨了 60%,精度只涨 0.3 个点。这种改动的性价比是负的。判断标准是:每增加 10% 的算力,mAP50 至少要涨 1 个点,涨不到就撤掉。这条线我是硬性执行的,避免自己陷入"改了就是改进"的自我安慰。
7. 几个真正管用的实操经验
先说一个关于预训练权重的事。加了 P2 头和注意力之后,直接加载原版 YOLOv8 的预训练权重会大量报 shape mismatch,很多人图省事就干脆随机初始化从头训,结果收敛慢、精度也上不去。正确做法是分层加载:backbone 里结构没变的部分照常加载,新增的 P2 分支和注意力模块随机初始化,然后在训练初期对这些新模块用较大的学习率(比如乘 3)单独预热几个 epoch。我这么做之后,收敛速度比全随机初始化快了差不多 40%,最终精度也高了 1 个多点。
再说一个关于标注的技巧。块状缺陷的边界模糊,与其让标注员纠结边界在哪,不如允许边界有一定容差,然后用软标签或者降低这部分样本的回归损失权重来处理。我试过让标注员对同一批图像标两遍,取交集做高置信标注、取差集做低置信标注,训练时低置信样本的回归损失乘 0.5。这个改动本身不涉及任何网络结构,但 mAP50-95 涨了将近 2 个点,因为定位精度的评价本身就对边界敏感。
最后提一个现场部署时的经验。模型的输出是框和类别,但运维的人要的是"这公里有没有问题、问题在哪、多严重"。中间这层转换如果做得糙,再好的模型也白搭。我的做法是在模型输出后加一层规则过滤:按缺陷面积、长宽比、连续出现次数做聚合,单帧检出的孤立小框先压掉,连续多帧在同一里程附近出现的才升级为告警。这一层规则让现场误报率又降了将近一半,而且完全不消耗算力。模型负责把候选找全,规则负责把噪声滤掉,两者的分工要划清楚,不要指望一个网络把所有事都干了。
这套方案后续还能往两个方向延。一是把检测和分割结合,缺陷的精确轮廓对等级判定比矩形框有用得多,可以在现有检测头基础上加一个轻量的分割分支,用检测框做 ROI 裁剪再跑分割,算力开销可控。二是做里程级的缺陷演化跟踪,同一位置的缺陷在不同时间点的检测结果做匹配,能看出缺陷是在扩展还是在稳定,这个信息对养护决策的价值比单次检测高一个量级。这两个方向我都在小范围验证过,前者对标注要求高,后者对定位精度要求高,都不算轻松,但比继续在网络结构上抠零点几个点有意义得多。