做铁路轨道部件识别与分类这个项目之前我一直觉得目标检测是件喂数据调参数就能出结果的事直到真正面对巡检车采集的画面才发现自己太天真。钢轨、轨枕、弹条扣件、垫板、道砟、夹板这些部件在画面里又小、又暗、又容易粘连夜间天窗期拍摄的低照度图像里一个弹条扣件可能只占几十个像素肉眼都得凑近屏幕才能看清。所以这篇博文来完整复盘一个基于改进YOLO13的轨道部件识别项目从业务需求拆解、数据集构造和标注规范到YOLO13的选型理由、BAMConv模块的设计与插入位置再到训练实验记录、踩坑排查链路和最终的部署优化。适合正在做工业视觉、巡检自动化、缺陷检测的工程师也适合那些在折腾注意力模块和目标检测器组合、想搞明白模块到底该加在哪、数据到底怎么喂的读者。1. 轨道部件识别的业务难点与需求拆解1.1 检测目标不是看起来像什么而是安全状态里缺什么先交代这个项目到底要解决什么。轨道巡检不是做目标存在性检测那么简单的视觉任务它的产出要直接服务于检修计划。弹条扣件松动或者缺失会导致钢轨轨距变化严重时威胁行车安全夹板即鱼尾板如果出现螺栓缺失或裂纹接头区域就有断轨风险。所以检测模型不光要把部件框出来还要能支撑哪些位置异常的下游判断。我们的检测类别定义一共六类钢轨、轨枕、弹条扣件、垫板、道砟、夹板。看起来类不多但每一类的尺度差异非常大。钢轨可能在整幅画面里横跨上千像素弹条扣件却只有20乘40像素左右的小目标。这种尺度跨度直接决定了对模型下采样倍率和输入分辨率的要求也决定了后面一系列改造的方向。为什么用目标检测而不是语义分割核心原因是业务只需要定位到哪个部件在哪个位置并不需要把道砟的每一块石头都分割出来。检测框输出加上类别配合巡检车的里程计信息就能换算成某公里标处存在异常扣件。分割的信息冗余大、标注成本高对检修来说反而是负担。这也是我在项目初期就坚持检测范式的原因。1.2 现场图像为什么总让检测模型翻车现场图像的问题比想象中严峻。巡检车在行驶中采集速度通常到60到80公里每小时低照度场景还需要补光。运动模糊是第一个大敌模糊让弹条扣件的边缘轮廓糊在一起模型打框时会出现大量低置信度候选阈值调高就漏检调低就误检。第二个问题是金属反光。扣件、钢轨表面都是金属有水膜或抛光后反光很强高光区域在图像上就是一片白色高亮斑块。模型很容易学到大片高亮区域等于金属部件这种捷径把道砟上的反光石子、积水映光都误判成扣件或钢轨。第三个是恶劣天气和光照变化。白天太阳角度变化、夜间灯光的光晕、雨雪在镜头前的遮挡都会改变图像的统计分布。一个在白天样本上训练得很好的模型放到夜间数据上mAP往往直接掉十几个点。这个现象的本质是域偏移后面会讲怎么用数据和增强去缓解而不是靠调超参数硬扛。我整理了一张现场挑战和对应影响的表方便快速对照现场挑战对检测的具体影响影响最大的类别运动模糊边缘糊化低置信度候选增多弹条扣件、螺栓金属反光/积水反光高亮斑块触发误检弹条扣件、钢轨夜间光晕与低照度对比度下降小目标漏检弹条扣件、垫板雨雪遮挡镜头图像整体退化域偏移全类别部件锈蚀与道砟同色语义混淆错误分类扣件与道砟这张表本质上决定了数据采集和增强策略而不仅仅是模型结构。很多项目一上来就改网络结构结果发现数据里根本覆盖不了这些现场情况改结构的效果自然有限。2. 数据集的坑类别定义和标注规范决定模型上限2.1 六类目标的标注规范怎么定才不打架数据这块单独拎出来讲因为在这个项目里数据工程占了大概六成的时间但换来的收益是模型上限的两成提升。目标检测里有一句话标注规范错了后面所有训练和调参都是在错误的地基上盖楼。先看扣件的标注边界。一个弹条扣件总成包括弹条、轨距挡板、螺栓和尼龙垫这些零件在图像上一部分被钢轨遮挡一部分相互叠压。如果标注得特别细碎把弹条和螺栓各自单独框出来标注员之间的一致性会很差模型反而学不到稳定的语义。我们最终采用的规范是对整个扣件总成标注一个外接矩形框框要完整包含弹条轮廓但不刻意去包住分散的螺栓光斑。这个决策让标注 IoU 的一致性从 0.68 提升到 0.82 左右。遮挡和截断的处理规则同样要提前定死。巡检图像是连续拍摄的扣件经常被钢轨部分遮挡。我们规定可见部分超过整体面积的80%按完整目标标注介于50%到80%标注可见部分的外接矩形低于50%跳过不标。这个规则的出发点是如果训练样本里目标极度不完整的情况太多模型会学出一堆边界含糊的候选框推理时反而更容易误检。宁可漏掉一部分难样本也不能让标注噪声放大。轨枕和道砟是背景型类别很多人容易忽略它们的标注价值。轨枕和道砟的出现其实给模型提供了极强的上下文扣件永远出现在轨枕两侧的钢轨底部附近垫板托着钢轨、压在轨枕上。训练时把轨枕和道砟标注出来等于让检测头学会了部件之间的空间先验对提升小目标召回率帮助非常大。2.2 数据来源、扩样策略和切分方式数据来源覆盖了三个维度白天和夜间、晴天和雨雪、直线段和道岔区段。道岔区的夹板、尖轨、转辙结构形态和一般正线差异很大。如果训练集里完全没有道岔样本模型在那几个区段会变成半盲。增强策略上Mosaic、MixUp、HSV 抖动这些常规操作不展开重点说两个容易踩坑的点。第一是翻转增强要谨慎。铁路图像存在明显的左右不对称性接触网支柱一般集中在一侧随意水平翻转会让网络学到支柱可以在任意一侧的错误先验。我们只在部分类别上允许水平翻转而不是整图无脑翻转。第二是亮度扰动范围要专门针对夜间场景加大。扣件在夜间图像里本身就偏暗对着白天训练集的亮度分布去扰动夜间推理时仍然会崩。把 HSV 的 V 通道扰动范围从正负30扩大到正负50并在训练集中混入15%左右的纯夜间样本后夜间 mAP 的差距才明显缩小。数据切分方式这里划个重点按帧随机切分是最省事但最危险的做法。相邻帧之间内容高度相似随机切分会导致验证集里塞满训练集的近亲指标虚高得离谱。我们按巡检区段切分比如一条线路的前80%区段做训练后20%区段做验证。这样做出来的 mAP 才真正能反映未知区段的泛化能力。实测下来按帧切分时验证集 mAP 能到0.93按区段切分就只有0.88差距就是这么真实。2.3 标注质量的验收花一小时做的事省三天的事这一节不长但特别值得说。我们项目有多个标注员参与如果没有验收机制返工成本会让人崩溃。验收方法从每批标注结果里随机抽5%的图片由经验最丰富的标注员重新标注计算两组标注之间的 IoU 一致性和类别一致性。IoU 低于0.75或者类别不一致率超过3%整批退回重标。这个机制看似保守实际上堵住了最大的风险标注员之间的尺度偏好差异。有人喜欢把扣件框得紧贴轮廓有人习惯留一圈边距这种差异如果不控制模型回归分支会无所适从具体表现就是训练 loss 早期就异常偏高或者框总是偏大偏小。另外模型第一版训练完之后专门做了一轮难样本补充标注把推理结果里所有高置信度但标注为空的位置以及所有模型漏掉但人眼能确认是目标的位置全部拉出来重新确认一遍确实漏标的补上确认不是目标的收进负样本。这一轮操作对后期精度的提升甚至超过了模型结构的改动。3. YOLO13基线如何选、BAMConv模块加在哪3.1 为什么选 YOLO13 而不是其他检测框架先说选型逻辑。这个项目要求最终能部署到巡检车上甚至要考虑边缘设备所以速度和精度的平衡是第一优先级。两阶段检测器比如 Faster R-CNN 系列精度底子确实厚但推理速度和工程复杂度在巡检视频流的场景下并不占优。DETR 这类基于 Transformer 的检测器在小数据集上的收敛速度慢且对小目标的支持需要大量额外调参对工期紧张的工业项目来说风险偏高。YOLO 系列里面的 YOLO13 在这个节点上有几个实际优势。首先是主干网络效率高计算量和显存占用控制得比前代更均衡。其次是解耦检测头和 DFL 损失在小尺度目标的回归上表现稳定。最后是社区生态成熟无论导出 ONNX 做 TensorRT 加速还是改配置做注意力模块实验资料都很多踩坑成本低。另外补充一点YOLO13 的默认数据增强管线已经很完善Mosaic、MixUp、随机仿射都内置了这能省掉不少数据代码的工作量。对工业项目来说能用成熟基线解决问题就不要自己从零搭一个框架。3.2 BAMConv 到底是什么它在模型里解决什么问题BAMConv 是这次项目里做的核心改进。先讲来源BAM 是 Bottleneck Attention Module 的缩写是一种经典的注意力模块核心思路是在瓶颈结构中同时使用通道注意力和空间注意力让网络知道哪些通道重要以及哪些空间位置重要。在这套方案里BAMConv 并不是简单地把一个注意力模块塞进网络而是把普通卷积改写成一个带注意力增强的卷积单元。结构可以概括为输入特征先经过1x1卷积降维进入瓶颈在瓶颈中间一路并行做通道注意力另一路做空间注意力两路注意力加权后的特征与原始特征做残差相加最后再经过1x1卷积恢复到原始通道数。这样的设计有两个好处注意力带来的额外计算量被瓶颈结构压得很低残差连接保证了模块在初始化时接近恒等映射训练更稳定。为什么这个项目需要注意力看两个具体痛点。第一是小目标问题弹条扣件只有几十像素经过骨干网络多次下采样之后有用的纹理信息在特征图上被稀释得很厉害。空间注意力能帮助网络把有限的表达能力集中在扣件所在的局部区域而不是平均分配到整个轨道平面上。第二是反光误检问题金属高亮区域在语义上不能只靠纹理判断模型需要结合颜色、形状和上下文综合判断通道注意力会在特征通道维度上抑制那些只对亮斑敏感的通道放大那些真正描述结构形状的通道。3.3 模块插入位置和代码级实现BAMConv 在哪个位置插入是个很讲究的问题。我在实验里对比过三种做法替换骨干网络最后阶段的普通卷积精度提升有限但训练成本高替换 C2f 内部的部分 Bottleneck收敛速度变慢只加在 Neck 输出的 P3、P4、P5 三个尺度层之后成本最低对精度提升最直接。最终采用的是第三种。理由是检测头的输入特征经过注意力增强之后分类和回归分支能直接受益信息损失最小。而且 P3 层负责小目标正是扣件漏检最严重的尺度这一层加注意力收益最明显。核心代码不长大致是这个结构import torch import torch.nn as nn import torch.nn.functional as F class ChannelAttention(nn.Module): def __init__(self, c, reduction16): super().__init__() self.fc nn.Sequential( nn.Linear(c, c // reduction), nn.ReLU(inplaceTrue), nn.Linear(c // reduction, c), nn.Sigmoid() ) def forward(self, x): b, c, _, _ x.size() y F.adaptive_avg_pool2d(x, 1).view(b, c) y self.fc(y).view(b, c, 1, 1) return x * y class SpatialAttention(nn.Module): def __init__(self, kernel7): super().__init__() self.conv nn.Conv2d(2, 1, kernel, paddingkernel // 2, biasFalse) def forward(self, x): avg_out torch.mean(x, dim1, keepdimTrue) max_out, _ torch.max(x, dim1, keepdimTrue) y torch.cat([avg_out, max_out], dim1) y self.conv(y).sigmoid() return x * y class BAMConv(nn.Module): def __init__(self, in_c, out_c, reduction16): super().__init__() self.conv1 nn.Conv2d(in_c, in_c // reduction, 1) self.ca ChannelAttention(in_c, reduction) self.sa SpatialAttention() self.conv2 nn.Conv2d(in_c // reduction, out_c, 1) self.shortcut nn.Conv2d(in_c, out_c, 1) if in_c ! out_c else nn.Identity() def forward(self, x): identity self.shortcut(x) y self.conv1(x) y self.ca(y) y self.sa(y) y self.conv2(y) return identity y接进 YOLO13 检测头之前只需要在特征拼接之后加一行前向调用比如x_p3 BAMConv(x_p3.shape[1], x_p3.shape[1])(x_p3)。为了验证模块确实生效我会先打印三张特征图的 shape 和内存占用确认通道数匹配再做一个20个epoch的小规模 smoke test看 loss 是不是能正常下降。记住BAMConv 初始化时因为残差接近恒等前几个 epoch 的 loss 应该和基线几乎一样如果明显偏高多半是通道数或者 shortcut 写错了。4. 训练实验全记录从头到尾的参数和成绩单4.1 训练环境和超参数设置训练设备是单张24G显存的GPU数据量不算大没有上分布式梯度累积也没开。输入分辨率从公开预训练的640提高到1280。这个决定很关键它直接让扣件类小目标的平均像素数量翻了一倍小目标召回率有了肉眼可见的提升。代价是单次训练时间拉长到40小时左右但换来的是总任务周期的缩短这笔账很划算。完整的超参表如下超参数数值备注输入分辨率1280 x 1280高于默认640小目标收益明显Batch size16单卡不爆显存的上限附近Epochs300使用早停在260左右停止优化器SGD momentum 0.937比 AdamW 在这类任务上更稳初始学习率0.01cosine 衰减Weight decay0.0005过大会抑制注意力模块收敛Label smoothing0.01对类别不平衡有一点缓解EMA0.999启用推理权重用 EMA4.2 基线到改进模型的指标变化做了两组完整对比一组是纯 YOLO13另一组是 YOLO13 加 BAMConv。统一用同一份数据集、同样的增强和训练参数。最终结果如下类别基线 mAP50BAMConv mAP50差值钢轨0.9520.9580.006轨枕0.9140.9210.007弹条扣件0.8320.8740.042垫板0.8460.8670.021道砟0.9080.9120.004夹板0.8790.8960.017全部0.8880.9050.017从表格里能清楚看到BAMConv 的收益主要集中在扣件、垫板这类小目标和中等尺度目标上钢轨、道砟这种本体大、结构简单的类别几乎没有变化。这也印证了前面的设计逻辑注意力模块不是万能膏药它只对局部细节信息容易被稀释的目标有明显帮助。还做了一组消融实验把通道注意力和空间注意力分别去掉只保留另一路。去掉空间注意力后扣件 mAP 掉到0.851去掉通道注意力后扣件 mAP 掉到0.858。两路同时保留才能到0.874。这说明两个注意力分支各有各的作用通道注意力帮助区分扣件与背景的语义空间注意力帮助框定位更精确。4.3 loss 曲线里值得关注的三个拐点训练过程不是一条直线往下走的有三个拐点对类似项目很有参考价值。第一个拐点是前20个epoch。如果用 COCO 预训练权重直接换掉最后的检测头训练前几个 epoch 的 cls loss 会有一个轻微上升再下降的过程这是检测头重新适配的正常现象。但如果改了骨干结构又不去下载匹配的预训练权重整体收敛会慢很多。我的建议是网络结构改得再深也尽量保留骨干部分的原结构这样能直接复用公开预训练权重相当于省了一半训练时间。第二个拐点在epoch 120附近。观察到验证集 mAP 开始停滞但训练集的 box loss 还在下降典型的过拟合信号。对策不是停训练而是逐步关闭部分增强Mosaic 在后期继续开着反而会让小目标被拼贴得支离破碎于是在epoch 120之后把 Mosaic 概率从1.0降到0.5MixUp 直接关闭。这个操作让验证集 mAP 又往上爬了大概1到1.5个点。第三个拐点出现在约epoch 220夜间样本的 recall 提升明显变慢。这时候翻了难样本日志发现瓶颈不是模型而是数据夜间图像里部分扣件被车灯光晕完全吞没人眼都难以辨认。再训练也没有用必须回到数据层解决比如调整补光角度或者增加多角度拍摄。这算是一个典型的指标碰到数据天花板的判断方法模型在困难子集上的 loss 不降而简单子集的 loss 已经趋平就该怀疑数据而不是怀疑结构。5. 最花时间的排查小目标漏检和金属反光误判5.1 扣件漏检的完整排查链路这个项目里最折腾人的一段时间就是夜间小目标扣件反复漏检。记录一下完整的排查过程可以照这个思路复现。第一步先确认漏检是否集中在某个尺度区间。按目标框面积把验证集分成大、中、小三档分别统计 recall。小目标档的 recall 只有0.71中等目标0.88大目标0.93问题确认在小目标。第二步检查输入分辨率。640输入时小目标扣件大概只有12到18个像素的等效框宽。把输入提到1280后小目标 recall 提升到0.82。这一步说明分辨率是硬约束必须先解决。第三步检查夜间亮度和对比度分布。把夜间图像的平均亮度画出来发现大量图像的直方图集中在低灰度区扣件和道砟的对比度非常低。做法是给训练集增加 Gamma 抖动和局部对比度增强让模型见过更黑的扣件。这一步后夜间小目标 recall 到了0.86。第四步上 BAMConv。前面所有数据层面的操作都做完了模型能力还是到顶这时候投入结构改进才花得值。加上 BAMConv 后小目标 recall 最终稳定到0.89左右。这个排查顺序是有讲究的先数据后结构。很多团队一上来就改网络结果数据压根没覆盖到困难样本改完结构指标纹丝不动。反过来先把数据喂到天花板附近结构的每一分改进都能体现在指标上。5.2 金属反光和锈迹导致的误判误检方面的头号问题是金属反光。最初版本的模型在验证集上出现了一批相当顽固的误检道砟上有一块亮斑模型就输出一个扣件框。分析激活图之后发现检测头的高响应区域集中在图像亮度极高的像素上说明模型学到的是高亮等于金属部件这种捷径而不是扣件形状加与钢轨轨枕的相对位置。针对这个做了三件事。第一在增强管线里加入高光模拟随机在图像中叠加圆形或线形的高亮区域逼模型不要依赖纯亮度特征。第二把 HSV 增强里 S 和 V 的扰动范围缩小避免训练样本里出现太多颜色失真的假反光。第三收集一批纯负样本也就是包含大量反光道砟但没有扣件的图像作为硬负样本混入训练。三个措施叠加之后反光引起的误检率下降了大半。锈蚀导致的错误分类比较隐蔽生锈的扣件和生锈的道砟颜色非常接近模型容易把扣件框错成背景或者把道砟上的锈色石子误判成扣件。这类问题靠模型结构很难解决最后是靠增加锈蚀扣件的正样本数量同时给标注规范补充了一条只要锈蚀扣件轮廓可辨认就必须标注不能因为颜色脏就跳过。数据层面纠错往往比模型改进更直接。5.3 漏标注的连锁反应与补救方法上面提到过漏标注问题这里展开讲一下连锁反应。夜间图像里如果标注员漏标了某个扣件模型在训练时会把那个位置的高 IoU 预测当成误检来惩罚。久而久之模型学到的是那个视觉模式的扣件不应该被检出体现在结果上就是无故漏检。最麻烦的是这种坏样本不会出现在 loss 的前几位很难被直观发现。补救流程是先让模型在验证集上做推理筛出所有模型高置信度但标注为空的框然后把这些框连同图像交给标注员确认确认是目标的连夜补标并加入训练集确认不是目标的加入负样本库。这个流程每个迭代周期做一次相当于给模型和数据互相做体检。它带来的效果是验证集上无中生有的误判明显减少同时模型对边缘情况的鲁棒性更强。6. 部署阶段的数学账帧率、量化与延迟6.1 从 PyTorch 到 TensorRT 的加速路线训练和实验做完之后真正的考验在部署。目标平台是一台工控机或者边缘盒子要在视频流上每帧都推理。加速路线是标准的 PyTorch 导出 ONNX再用 TensorRT 做 FP16 量化中间校准集选用了约500张覆盖白天夜间的代表图像。需要注意的一个坑是 ONNX 导出时BAMConv 里的自适应平均池化和动态 shape 在部分版本里会出兼容问题。我的建议是导出时固定输入尺寸不要动态维度省掉一堆麻烦。固定1280输入后模型推理时间稳定下来后续做 FP16 也没有精度抖动问题。记录了几个关键数字单位是毫秒每帧平台是边缘 GPU 盒子阶段推理耗时说明PyTorch fp32约35ms未优化参考基线ONNX Runtime fp32约28ms精度无损TensorRT FP16约15ms精度损失可忽略加上 BAMConv 模块后整体推理耗时只增加了约2ms扣件类的 mAP 提升却接近4个点。这种性价比对工业项目来说很难拒绝。6.2 端侧推理的几个细节端侧部署的细节往往决定现场效果。第一个是图像预处理必须和训练时保持一致特别是 letterbox 的填充颜色和 pad 值。很多项目导出模型后在端侧掉精度一查全是预处理没对齐比如 RGB 通道顺序反了或者归一化系数写错。第二个是置信度阈值不要照搬验证时的0.25。现场数据分布比验证集更野低阈值会带来大量噪声框。我在现场测试后把阈值调到0.35并用精准率和召回率的曲线确认这个点没有牺牲太多召回。更稳的做法是叠加时间维度的平滑连续三帧里只有至少两帧都检出的目标才保留单帧闪断的框直接丢掉。这种做法对视频流非常有效基本消灭了闪烁误检。第三个是模型更新和日志。巡检车每天产生的图像量很大全部回传不现实。我在端侧做了按里程抽帧保存的机制每10米留一帧原始图和检测结果。这些数据既用于当天的快速问题排查也累积成下一轮训练的数据池。对算法团队来说这批来自现场的难样本价值远高于任何人为构造的增强数据。6.3 上线后的稳定性故障定义和回退策略最后补一个工业项目容易忽略的点上线后的稳定性机制。我定义了几个人工可检查的运行时指标GPU 显存占用、推理帧率、单帧最大延迟、检测框平均置信度。任何一个指标偏离正常区间系统就自动降级为只保存原图、不输出判断结果等人工复核。这个回退策略很重要因为巡检车的检测结果如果错误地进入检修系统会造成无效作业甚至漏报真实风险宁可少输出也不能乱输出。实际运行中还发现一个意想不到的问题晴天上午太阳角度低时钢轨反光会让检测框在连续几十帧里轻微偏移。虽然置信度没下降但框的位置抖动明显。后来在端侧加了一个轻量的卡尔曼滤波做框平滑位置抖动就基本消失了。这些小问题在实验室里完全看不出来只有在真实车次、真实光线运行一周后才会暴露。最后分享一点这轮项目下来最深的体会。目标检测项目的天花板通常不在模型而在数据和工程验证。BAMConv 给了几个点的提升但真正让夜间指标稳定的还是那15%的夜间样本、严格的标注验收以及按区段切分的验证集。如果你也在做类似的巡检识别项目建议先花时间把数据规范定死再动手改模型。另外一个小技巧是每次训练都保留一份固定随机种子下的验证集推理日志这样前后对比才有说服力不然两次训练之间的随机性就会干扰你的判断。后续如果再往下走我打算把检测结果和工务台账对齐做扣件缺失等级的自动研判那会是这个项目更深一层、也更有业务价值的方向。
阅读完成 · 觉得有帮助?