开篇先交代一个背景:隧道裂缝检测这个场景,和一般工业质检不太一样。隧道内光照差、墙面大面积是混凝土纹理,裂缝本身又细又长,往往只占图像的千分之几像素,很多时候人眼都要凑近了才能确认,想靠传统图像处理的边缘检测或阈值分割去做,泛化能力基本撑不住。所以这两年大家基本上都转向深度学习,尤其是目标检测路线。我最初用的方案是YOLOv5s加原版PANet,跑下来发现在一些低对比度、远距离拍摄的样本上,细裂缝的漏检率偏高;后来把Neck换成了BiFPN,同样一个训练集,mAP明显涨了一截。这篇文章我把这套YOLOv5s+BiFPN的方案完整拆开讲,包括为什么选YOLOv5s而不是更大的版本、BiFPN在结构上到底改了什么、数据集从哪里拿、训练参数怎么调、以及我踩过的一些坑,最后附上数据集获取的几个渠道,方便想复现的同学直接上手。
1. 项目整体设计与技术选型分析
1.1 隧道裂缝检测的三个核心难点
做隧道裂缝检测和做普通的目标检测不完全是一回事。先说清楚这个场景的特殊性,你才能理解后面为什么每一步都要做针对性设计。
第一是目标尺度极端不均衡。隧道裂缝普遍是细长的条状,宽度可能只有两三个像素,长度却可能是几十甚至上百像素。在YOLO这类锚框检测里,这种极端长宽比的目标天然不友好——锚框的长宽比是预先设置的一组离散值,遇到长宽比远超出预设范围的裂缝,回归头要花更多迭代次数才能把框拉到位,甚至直接漏检。
第二是背景干扰太强。隧道内壁不像实验室的混凝土试块那么干净,上面有渗水痕迹、模板接缝、划痕、苔藓和灰尘,这些杂质的纹理特征和裂缝非常接近。如果模型学到的只是“图像里有一条暗色细线”,那它会把大量误检目标输出出来;如果模型学到的是“裂缝边缘的灰度突变模式”,那才具备真正的判别力。这一点直接决定了特征提取网络的选择和数据集的构造方式。
第三是光照和尺度变化。巡检车在不同时间进隧道,灯光角度、强度都不一样,而且隧道内几乎没有自然光,图像亮度分布极其不均匀。同一个裂缝,在近景和远景拍摄下,占图像的尺度能差一个数量级以上。这就要求Neck部分的多尺度融合能力一定要强,不能只在单一特征层上做预测。
这三点叠加下来,结论很明确:不能靠一个普通的检测网络直接套上去,必须针对多尺度、小目标、强干扰这几个关键词做结构上的调整。这也是我会重点聊BiFPN的原因。
1.2 为什么是YOLOv5s,而不是YOLOv5l或YOLOv8
很多朋友问过我一个问题:现在都有YOLOv8甚至新出的版本了,为什么还在用YOLOv5s?我的回答一直是:选模型不是看新不新,而是看你的部署场景和数据条件。
YOLOv5s最大的优势是轻量和易部署。它的基础通道宽度是32,depth multiple是0.33,参数量大概只有7.2M左右,FP16推理在单张消费级显卡上能做到几十毫秒一级,非常符合隧道巡检车/无人机端侧部署的需求。而YOLOv5l参数量直接到了46M以上,精度确实更高,但推理速度下降一个量级,在嵌入式设备上跑实时检测会很吃力——除非你做的不是实时巡检而是离线批处理。
YOLOv8在结构上改动比较大,C2f模块、Anchor-free头等都值得关注,但它对部署工具链有一些额外要求,尤其是在老设备上适配TensorRT和NCNN时,算子兼容性反而可能没有YOLOv5成熟。对于“快速上线、稳定运行”的项目诉求来说,YOLOv5s的社区生态、导出工具链和踩坑文档都要丰富得多。
另外还有一点很关键:YOLOv5s的模型结构足够简单清晰,改起来也方便。我们这次要把PANet替换成BiFPN,本质上就是改Neck那几行代码和yaml配置。这种程度的改动,在YOLOv8里也能做,但YOLOv5的代码结构更直白,对新手更友好。所以最终方案定为YOLOv5s+BIFPN,是在“精度够用、速度够快、改动可控”三者的平衡下选出来的。
1.3 BIFPN让特征融合从“一视同仁”变成“按需分配”
讲到BiFPN,先要从PANet说开去。
YOLOv5原版的Neck用的是PANet,也就是自顶向下传递语义信息,再自底向上传递位置信息,两条路径交叉融合,结构上像个“倒过来的金字塔”。PANet的优点是实现简单、效果稳定,但有一个隐藏问题:在特征融合的时候,它默认不同层级的特征图是同等重要的,直接用Concat或者Add把它们拼起来。
实际上呢?浅层特征分辨率高、细节多,但对裂缝这种目标来说,噪声也最多;深层特征语义丰富,但空间分辨率低,细小裂缝的响应已经接近消失了。如果二者权重一样,融合出来的特征既不够干净,也不够敏感。BiFPN干的第一件事就是给每一条融合路径加上可学习的权重,让网络自己学会“深层特征应该占多大比重、浅层细节应该保留多少”。
BiFPN的全称是Bidirectional Feature Pyramid Network,来自EfficientDet。它的核心机制有三个:
- 加权特征融合:对每条输入特征乘一个可学习权重w,再通过归一化(softmax或快速归一化)合成。训练的时候w会随着损失函数反向传播逐步调整,最终得到每个层级的最优融合权重。
- 双向跨尺度连接:在原来自顶向下和自底向上的两条路径之外,增加同层级的跳跃连接,并在没有经过任何处理的原始特征与经过融合处理的特征之间建立直接连接。换句话说,信息流动路径变多了,深层特征不用绕好几层才能影响浅层输出。
- 用深度可分离卷积做特征变换:Neck部分的卷积运算改成深度可分离卷积,参数量和计算量都降低,这恰好弥补了多路径融合带来的额外开销,让整体推理速度不会明显变慢。
这对隧道裂缝检测有什么具体帮助?裂缝是多尺度目标,在深层特征图(比如P5)上,细小裂缝的响应已经几乎消失,如果Neck只是简单把P5和P4、P3拼接,深层特征对浅层预测的帮助其实有限。而BiFPN的加权和跳跃连接,让P3层(负责小目标检测)能直接从多个层级拿到经过筛选的特征,让细裂缝的响应在融合后仍然保留下来。实测下来,这一改动对mAP@0.5的提升通常在2到5个点,主要就体现在小目标那一类上。
1.4 整体网络结构:CSPDarknet做骨干,BiFPN做脖子,YOLO Head做输出
我把这套方案的整体结构简单梳理一下,方便你对照后续的操作步骤。
输入图像一般是640×640,先进入Backbone。YOLOv5s的Backbone是CSPDarknet,分成5个Stage,逐级下采样,得到不同分辨率的特征图。我们这里沿用原版Backbone,不做改动,因为它已经能把基础的纹理、边缘、语义信息提取到位。
从Backbone出来的特征,按层级分成三条,分别是P3、P4、P5,对应输入图像的1/8、1/16、1/32分辨率。这三条特征进入Neck,也就是我们替换后的BiFPN。BiFPN先用自顶向下的路径融合语义,再用自底向上的路径融合空间细节,并引入跨层跳跃连接。最终输出融合后的P3_out、P4_out、P5_out,送到YOLO Head。
Head部分保持不变,仍然是三个尺度的预测头,每个尺度负责不同大小的目标。因为裂缝通常比较细,小目标基本集中在P3_out这个高分辨率支路上,所以P3_out的质量对最终效果影响最大——这也正是BiFPN能发挥价值的地方。
如果你对代码改动不熟悉,可以先从理解这个整体结构入手,后面第三部分我会给出具体的修改方式。
2. 数据集获取与预处理策略
2.1 公开数据集从哪里找,怎么判断能不能用
训练裂缝检测模型,最理想的情况是现场采集隧道巡检图像,但对于很多学习者和刚起步的项目来说,公开数据集是更现实的起点。以下是我用过且效果还不错的几类:
- 道路/桥梁裂缝数据集:这类数据相对多,包括水泥路面裂缝、沥青路面裂缝、桥面裂缝等。虽然场景不完全等于隧道,但裂缝的形态和纹理与隧道内壁比较接近,作为预训练或数据补充是可行的。常用的一些公开数据集合集,比如开源社区的裂缝检测数据包,搜索“concrete crack detection dataset”就能找到不少,注意筛选图像分辨率不要太低、标注格式是否统一。
- 土木工程领域学术数据集:有些高校和机构发布过结构表面病害数据集,标注格式一般是Pascal VOC或COCO,包含裂缝、渗水、露筋等类别。这类数据集质量较高,但往往需要提交申请或遵守一定的非商业使用条款,注意看授权说明。
- 自己采集小批量现场数据:公开数据集再丰富,和你的真实隧道场景总有偏差。最理想的做法是先拿公开数据让模型跑通,然后再到当地采集几百张隧道图像,人工标注后加入训练集,做一次微调。这一招能显著提升模型在现场的表现。
拿到数据集后,第一件事不是直接开训,而是做一次快速体检。我的判断标准有三个:图像的尺寸分布是否方差过大、标注框的长宽比分布是否严重偏向某一类、目标在整图中的平均占比是否过低。尤其是长宽比分布——隧道裂缝的标注框如果长宽比普遍超过5:1甚至10:1,会对锚框设置提出更高要求,这个数据统计你要留好,后面调锚框参数时要用。
2.2 数据标注的细节把模型上限卡在了哪里
很多人觉得标注就是把目标框出来,其实裂缝标注的细节直接决定了模型上线的上限,我这点要展开讲。
首先,尽量遵守“切段标注”原则。一条几米长的连贯裂缝,不要用一个大的长条框把它整体框进来。因为完整的裂缝框长宽比可能达到几十比一,这种极端比例会让锚框回归非常困难。正确做法是把裂缝按走势切成多段,每段一个框,框的长宽比控制在5:1以内。这样虽然标注工作量会大一些,但训练时每个框的目标形态都更接近常规检测任务的形状,模型收敛速度和精度都会有明显改善。
其次,框的边界要紧贴裂缝边缘。不要留太多的背景余量,否则框内的背景纹理占比太高,模型很容易把背景特征学进目标特征里去。尤其是细裂缝,框稍微松一点,同一个目标在图像上的有效区域可能就不到一半了,训练信号被严重稀释。
最后,标注类别要克制。如果项目只关心裂缝,就只标裂缝一个类,不要把渗水、污渍、接缝等一起标进去。类别越多,类别间的特征混淆越严重,模型在复杂背景下的误检率会上升。如果你确实需要区分,建议等单类模型跑稳之后再逐步扩展。
2.3 数据增强:让模型见过隧道里的“千奇百怪”
隧道内图像的多样性,靠采集几百张原图是不够的,必须在数据增强上下足功夫。
我用的增强策略里,重点放在这几个方面:
- Mosaic增强:YOLOv5自带的Mosaic把四张图随机裁剪拼接成一张新图,相当于一次训练看到四个不同的场景上下文。这对增加样本多样性非常有效,也能让模型在一个batch里接触到更多不同曝光和光照条件的局部区域。但注意,Mosaic的比例不要设得太高,全Mosaic容易让模型对拼接边界产生依赖,我会把Mosaic的概率控制在0.8左右。
- 亮度对比度扰动:这个是隧道场景的专项增强。隧道图像亮度分布不均,阴影区和强光区经常同时出现,所以HSV增强中H、S、V的扰动幅度我都调得比较大,尤其是V(亮度)方向,基本在±30%幅度内随机变化。这样模型不会因为亮度过暗或过亮就丢失裂缝响应。
- 随机旋转与翻转:裂缝的走向在隧道里是不固定的,水平、竖直、斜向都有,旋转增强必须开到180度全域,不能只做小角度旋转。翻面增强在隧道场景下也可以用,因为隧道左右壁的裂缝形态不会因为水平翻转而失效。
- 少量尺度变换:针对远近距离拍摄差异,随机缩放训练图像,让模型在同一个epoch里看到不同尺度的裂缝。
我一般用YOLOv5自带的hyp.scratch-low.yaml做基准,在这个基础上微调增强参数。具体怎么改,第三部分讲训练配置时会细化。
2.4 想做得更严谨,MODIS气象遥感数据怎么辅助裂缝分析
这部分算是一个延伸思路。隧道裂缝虽然是结构病害,但它和发展环境有很大关系——渗水、温度变化、湿度等都会加速裂缝的扩展和劣化。如果你做的项目不只停留在“检测到裂缝”,还想分析“裂缝产生的原因”或“哪些区域更容易出现裂缝”,那环境数据就是有价值的辅助分析维度。
MODIS(中分辨率成像光谱仪)是搭载在Terra和Aqua卫星上的一个关键传感器,观测数据经过处理后会形成一系列包含气象与环境信息的卫星产品,这些产品可以用来获取地表温度和夜间热红外等信息。对隧道相关研究来说,比较常用的是地表温度产品(如MOD11A1)和植被/地表覆盖类产品,可以辅助分析隧道周围环境的热应力变化或地表状态。此外,气象数据还常涉及降水、云量和大气水汽等观测要素,这些信息可以通过相关对地观测平台的公开数据服务获取,按区域和时间范围批量下载。
但这些遥感数据研究的是宏观环境,并不直接参与YOLO的目标检测训练。通常的用法是在检测结果之上做叠加分析,比如某段隧道附近地表温度变化剧烈或降水频繁,结合裂缝检测结果,可以推断哪些区域是裂缝高发区,从而给巡检计划提供优先级依据。对初学者来说,先不用把精力放在这条线上,把检测模型本身做好,后续再考虑数据融合即可。
3. 训练环境与关键参数配置
3.1 硬件和软件环境,最低和推荐配置
先说结论:这套模型并不需要顶配显卡,但显存和CPU内存都有个底线。
我训练的配置是两张RTX 3080 10G,batch size开到32,训练大概300个epoch。如果你只有一张显卡,建议把batch size降到16,图像分辨率从640降到512,也是能跑的,只是收敛速度会慢一些。显存低于6G的话,建议先用原版小分辨率和轻量增强跑通,再逐步加负担。
软件环境方面:
- Python 3.8以上,PyTorch 1.10到2.x均可。
- CUDA和cuDNN用PyTorch官方推荐的对应版本。这里有个经验:不用追求最新CUDA,稳定性优先,我记得自己踩过一个坑,装了CUDA 12.1然后某个算子不兼容,来回折腾了很久,后来退回CUDA 11.8就好。
- YOLOv5代码库直接clone官方仓库即可。BiFPN的修改是基于官方代码结构做小范围改动,不依赖额外第三方库,深度可分离卷积用PyTorch自带的torch.nn就可以实现。
3.2 把YOLOv5s的PANet换成BiFPN,具体代码怎么改
这是整个项目的核心改动,我按官方YOLOv5的代码结构来讲,建议你也对照源码一起看。
第一步:在models/common.py里添加BiFPN相关模块。
官方YOLOv5的Concat层做的是通道维拼接。我们的做法是写一个BiFPN专用的加权融合层,核心代码如下(这个是我在项目中实际用过的简化版本):
import torch import torch.nn as nn import torch.nn.functional as F class BiFPN_Concat(nn.Module): def __init__(self, channels, num_inputs=2, epsilon=1e-4): super().__init__() self.epsilon = epsilon self.num_inputs = num_inputs self.channels = channels # 可学习的融合权重,初始化为等权重 self.weights = nn.Parameter(torch.ones(num_inputs, dtype=torch.float), requires_grad=True) # 融合之后接一个1x1卷积,做通道适配 self.conv = Conv(channels, channels, 1) def forward(self, x): # 快速归一化加权 w = F.relu(self.weights) w = w / (torch.sum(w) + self.epsilon) x = torch.stack(x, dim=0) # [num_inputs, B, C, H, W] x = torch.sum(x * w.view(-1, 1, 1, 1, 1), dim=0) # 加权求和 return self.conv(x)当然,这个只是最简版本,实际正式的BiFPN还会把自顶向下路径和自底向上路径分别用不同通路组合。如果要更贴近原版EfficientDet的做法,可以在上面的基础上把融合后的结果再接一个深度可分离卷积模块,进一步提取特征。但官方YOLOv5里的Neck本身还会跟很多其他模块衔接,改动范围太大容易出问题,所以我建议按上面的最小方案接入,对多数项目来说收益已经足够明显。
第二步:在models/yolo.py里注册新模块。
官方YOLOv5在解析yaml配置时会按名称去ParseModel里查找模块类。你需要在models/yolo.py里找到类似下面的代码段:
if m in (Conv, GhostConv, Bottleneck, GhostBottleneck, SPPF, DWConv, MixConv2d, Focus, CrossConv, BottleneckCSP, C3, C3TR, C3SPP, C3Ghost, C2f): c1, c2 = ch[f], args[0] if c2 != no: c2 = make_divisible(c2, 8) args = [c1, c2, *args[1:]]在判断条件里加上BiFPN_Concat,并给它写一个分支,因为它的构造参数里既要通道数又要输入路径数。
elif m is BiFPN_Concat: c2 = ch[f] # args中第一个参数是通道数,第二个是输入分支数 args = [c2, *args]这里要注意,如果你用了上面的最简版本,它的第一参数是channels,第二参数是输入的路径数。yaml里写的时候要把这两个参数配上。
第三步:新建模型配置文件models/yolov5s_bifpn.yaml。
官方yolov5s.yaml里的Neck部分是PANet结构,长这样:
neck: - [-1, 1, Conv, [512, 1, 1]] - [-1, 1, nn.Upsample, [None, 2, 'nearest']] - [[-1, 6], 1, Concat, [1]] - [-1, 3, C3, [512, False]] ...我们要做的,是把原来的Concat替换成BiFPN_Concat,同时把原来PANet一条路到底的结构改成BiFPN的双向多路径结构。这份yaml里关键的改动点有两处:一个是自顶向下路径的Concat换成加权融合,一个是自底向上路径重复用不同的特征层输出。我建议你在跑通之前,先在草稿纸上画出各层的输入输出连接关系再改yaml,因为不同特征层的通道数如果不匹配,会直接报维度错误。
第四步:验证模型能正常前向传播。
改完之后,先不要急着训练,做一个短路测试:
python models/yolo.py --cfg models/yolov5s_bifpn.yaml这一步能检查yaml解析和模型构建是否成功。我第一次改的时候就把一个特征的index写错了,结果维度对不上,报错信息提示UpSample的输入通道不匹配,花了一点时间才对照着原始结构的张量shape定位到问题。所以这个步骤一定不要跳过。
3.3 训练参数:批次、学习率、epochs、锚框
训练超参数我是这样设置的,附上选择理由,方便你根据自己数据调整:
- 图像分辨率:640×640。如果你的裂缝目标普遍很小,可以尝试768甚至更高分辨率,但代价是显存翻倍。我记得在768下,单卡3080只能开到batch size 8,训练效率低很多。实际效果上,对细裂缝确实有提升,但不如调整BiFPN融合权重的收益明显。
- Batch size:32(双卡)、16(单卡)。batch size直接影响BatchNorm的统计稳定性,太小(比如4或8)会导致BN的running mean抖动,模型在验证集上会忽高忽低。如果显存不够,优先降低分辨率而不是无脑减小batch。
- 初始学习率:0.01。YOLOv5官方默认值就是0.01,配合warmup机制,前3个epoch线性从0涨到0.01,用来预热。建议不要动这个基准,除非你发现loss长期不降,再考虑调到0.005。
- Epochs:300。裂缝检测不是特别复杂的任务,但数据集规模通常不够大,300个epoch足够模型充分收敛。我跑下来到250个epoch之后训练集loss基本稳定,验证集mAP也进入平台期。
- 锚框重计算:YOLOv5默认会在训练前基于你的标注框重新计算k-means锚框。我们前面提到裂缝框长宽比极端,这个步骤一定要开,否则默认锚框和你的目标分布差太远。train.py里加
--noautoanchor可以关闭,默认是自动重算,别去关它。
训练命令示例:
python train.py --data tunnel.yaml --cfg models/yolov5s_bifpn.yaml --weights yolov5s.pt --batch-size 32 --epochs 300 --imgsz 640 --device 0,1 --hyp data/hyps/hyp.scratch-low.yaml --name tunnel_bifpn3.4 训练过程中的日志怎么看,什么时候该停
训练时YOLOv5会实时打印box_loss、obj_loss、cls_loss和验证集的mAP。我判断训练是否正常的标准很简单:前20个epoch里,box_loss和obj_loss应该平稳下降,验证集mAP哪怕是缓慢爬升都算正常。如果前20个epoch内mAP毫无变化甚至掉到0附近,说明数据或者配置出了问题,别盲目等100个epoch,浪费时间。
一个常见误区是追求训练集loss无限降低。裂缝检测里,训练集loss降到0.02以下时,通常已经开始过拟合,验证集mAP可能反而往下走。我建议在训练脚本里加上--save-period定期保存checkpoint,或者直接用YOLOv5自带的--patience早停机制,验证集连续N个epoch不涨就自动停。
4. 训练效果对比与结果分析
4.1 YOLOv5s原版和YOLOv5s+BIFPN在验证集上的直接对比
我在同一份隧道裂缝数据集上分别训练了原版YOLOv5s和YOLOv5s+BIFPN,严格控制其他变量一致,结果如下:
| 模型 | 参数量 | mAP@0.5 | mAP@0.5:0.95 | 单帧推理耗时(ms) |
|---|---|---|---|---|
| YOLOv5s (PANet) | 7.2M | 0.782 | 0.453 | 6.8 |
| YOLOv5s+BIFPN | 8.1M | 0.826 | 0.491 | 7.5 |
BIFPN版参数量只增加约12%,推理耗时慢了不到1ms,但mAP@0.5提升4.4个百分点,mAP@0.5:0.95提升3.8个百分点。这个提升幅度在目标检测的实验里算是非常划算的。从类别层面看,提升主要来自小目标裂缝——原版对小裂缝的召回率大概在0.71,BIFPN版提升到0.78。
需要说明的是,这个数据是在隧道现场数据上跑出来的效果。如果你用的是纯公开数据集,数值会有浮动,但BIFPN相对PANet的提升趋势大概率是一致的,因为这是结构上的优势,不是某个数据集特有的偶然。
4.2 为什么提升“看起来不大”,但实际很有意义
看到这个结果,有些朋友可能会想:才4个点,至于吗?我的看法是,在裂缝检测这个场景里,几个点的收益实际价值很大。
裂缝检测最怕的不是误检,而是漏检。一条细小裂缝如果被漏掉,后期可能发展成结构性病害,修复成本是早期的几倍甚至几十倍。mAP的提升意味着在同一个召回率下,模型能少漏检不少目标。拿我那个测试集来说,总共500多张图像,里面的裂缝目标大概2000多个,4个点的mAP提升换算下来大约少漏检七八十个目标,这对巡检任务来说是实打实的价值。
另外,mAP@0.5:0.95的提升比mAP@0.5更能说明框的回归质量。BIFPN版的框更贴合裂缝段本身,这在后续做裂缝长度估算、宽度量化时非常关键。如果框的位置偏移大,量化结果就不准,下游业务可能还要重新处理。
4.3 可视化结果里最容易暴露问题的地方
训练完之后,我习惯把验证集的预测结果全部可视化出来,一张一张翻。这比看任何指标都更直接。
最容易暴露问题的往往是两类图:一类是低对比度裂缝的样本,模型要么没检出来,要么在边缘位置检出一个置信度特别低的框;另一类是墙面有大量水渍和划痕的样本,模型容易出现误检框,框里是水渍边界的暗色细线。这两类问题,在PR曲线上也能看出来——置信度阈值从0.25提高到0.5时,precision曲线会快速上抬,说明低置信度的框很大比例是误检。
针对误检,我的处理办法通常有两个方向。第一,增加包含这类干扰背景的负样本,让模型见过更多“看起来像裂缝但实际不是”的纹理,这会直接提升模型判别力。第二,在后处理阶段适当提高置信度阈值,比如从默认的0.25提到0.35或0.4,牺牲一点召回率来换更干净的检测结果。具体阈值怎么选,要看你的业务更偏重查全还是查准。
5. 常见问题与排查技巧实录
5.1 显存溢出,通常是这三步来解
训练过程中最让人烦的就是OOM(Out of Memory)。我按优先级排了三个步骤供你排查:
第一步,降低batch size。从32改成16,OOM大概率能解决。但不要一次性降到4,BN层会不稳定。降batch之后建议同步把初始学习率也降一点,比如0.01降到0.008,因为batch变小等价于单次更新的噪声变大,学习率不变容易震荡。
第二步,关闭一些占用显存大的训练开关。YOLOv5训练时默认开了一些特征可视化、日志记录的模块,还有EMA(指数移动平均)在训练结束阶段会保存额外的模型副本。eval期间如果也OOM,可以检查是否缓存了太多中间张量。
第三步,实在不行才降分辨率。640降到512,显存占用会下降约36%,但小目标检测精度也会损失。所以这一步放在最后,优先动batch size。
5.2 模型不收敛,先别调结构,检查这三个地方
训练三个小时后发现loss还在高位不动,这种事我遇到不止一次。经验是出现不收敛时,先不要怀疑BiFPN改错了,优先检查这三个地方:
- 数据集路径和标签是否配对:YOLO的标签文件是txt格式,每行是“类别 x_center y_center width height”,都是归一化坐标。标签里如果有某个目标宽度或高度是0,或者坐标明显超出图像范围,训练时就会产生nan loss或mAP异常。这个排查要放在第一位,用一个脚本检查所有标签是否存在数值越界。
- 学习率是不是被改过头了:很多人一上来就把初始学习率调到0.001甚至更低,结果模型收敛极慢。YOLOv5的官方默认0.01配合warmup是经过充分验证的,除非你很清楚自己在做什么,否则别动它。
- 类别文件是否和标注一致:data yaml里的nc(类别数)和names必须和标注标签里的类别索引对应。如果names只有“crack”一个类别,但标签里出现了索引1或者2,模型训练时就会自动当作其他类处理,行为会很奇怪。
这三个地方都确认没问题,再回头检查BiFPN的权重是不是没有正常更新。可以在训练日志里打印BiFPN_Concat的weights参数,如果经过几十个epoch之后所有权重值仍然完全一样,说明这个模块没有参与到反向传播里,大概率是yaml解析时模块实例化方式有问题。
5.3 BIFPN接入后报维度不匹配,怎么快速定位
BIFPN接入最常见的报错是维度不匹配。这种问题通常集中在yaml里特征层index的指向错位,比如自底向上融合时引用了还没有被更新过的特征层索引。
我的定位方法是这样的:在yaml配置里给每个Conv操作加一个注释,标注当前输出的张量shape,然后逐个层核对。如果yaml里写的是[[-1, 6], 1, BiFPN_Concat, [256, 2]],含义是“取当前层和索引为6的层,做通道数为256、输入数为2的加权融合”。这里第6层到底是什么层,要看它在整个网络里的位置,从0开始数。最容易犯的错误是拿原始PANet的yaml硬改,没有重新数索引,导致融合的两个分支一个是P3一个是P5,通道数对不上。
还有一个经验:BiFPN_Concat的输入特征通道数不同时,不能直接做加权求和,必须先用1x1卷积把通道统一。正规做法是在yaml里先对每个输入分支加一个Conv转换通道,再做融合。这也是很多新手忽略的一点。
5.4 裂缝漏检严重,用这几招针对性优化
如果你的模型训练出来后,对细小裂缝的漏检率还是高,不要急着往大了换模型,先按下面的顺序优化:
第一招,提高输入分辨率。这招简单直接,640改到768或896,小目标在特征图上的像素数变多,响应更强。代价是训练和推理都变慢,你需要在速度和精度之间做个取舍。
第二招,针对小目标调整预测层权重或增加一个更高分辨率的检测层。YOLOv5默认在P3、P4、P5上做预测,P3对应8倍下采样,理论上可以检测到4×4像素以上的目标。如果裂缝宽度在2像素左右,P3特征其实已经很吃力了,这时可以再加一个P2层(4倍下采样)的预测头。但P2层计算量很大,显存开销明显上升,而且容易把背景噪声也放大,需要充分实验。
第三招,做裂缝本体分割。如果漏检问题始终无法通过检测框架解决,大概率说明这个任务的定位精度需求已经超出了检测模型的表达能力。这时候可以换成带分割头的模型(比如YOLOv5-seg或YOLOv8-seg),把每个裂缝像素分割出来。分割模型在细长目标的定位上天然优于矩形框,代价是标注成本大幅上升。我在实际项目里,如果裂缝宽度小于5个像素,就会优先考虑分割方案。
6. 部署与后续扩展思路
6.1 从PyTorch到TensorRT,推理提速多少
模型训练完只是第一步,部署上如果还是用PyTorch做推理,效率肯定不够。我在项目里一般会走这样的部署链路:PyTorch -> ONNX -> TensorRT(NVIDIA GPU设备),或者PyTorch -> ONNX -> NCNN/OpenVINO(CPU或移动端设备)。
YOLOv5官方提供export.py,可以直接导出ONNX和TensorRT引擎。导出TensorRT时需要根据你的GPU架构选择精度模式:FP16通常能满足需求,推理速度相比PyTorch能提升2到4倍;INT8量化能再快一截,但是量化后小目标检测精度可能会掉,裂缝这种细线目标对量化误差又特别敏感,所以我不建议在裂缝检测任务上一开始就用INT8,先FP16跑起来,精度有问题再考虑混合精度方案。
有一点要提醒:BiFPN里用到的深度可分离卷积在一些老版本TensorRT上支持不是很好,导出时可能报不支持的op。解决办法是升级TensorRT版本,或者把深度可分离卷积改写成标准的卷积组合。这个坑我在TensorRT 8.0时代踩过,后来升到8.5基本就没问题了。
6.2 从检测到量化分析,裂缝宽度和长度怎么估算
检测结果框定位到裂缝之后,很多项目还关心裂缝有多长、多宽,用来做严重程度分级。
最简单的方式:在检测框的基础上,对框内区域跑一个裂缝像素分割或边缘提取,统计连通域的像素数和骨架线长度,再根据相机标定结果把像素尺寸换算成物理尺寸。这个方案精度不算高,因为检测框多多少少包含背景,但用于巡检报告的粗评估足够。
更正规的方案是直接在分割模型上做。YOLOv5-seg输出的是裂缝目标的掩膜,可以在掩膜上直接计算面积、骨架、最大宽度等参数,误差比检测框小得多。如果业务确实需要对裂缝做定量评估,建议从一开始就上分割模型,省得后期从检测结果里再想办法提取几何信息。
6.3 后续迭代数据闭环怎么建
模型上线不是终点,而是数据循环的起点。隧道巡检每天产生的图像量很大,其中很多是“没裂缝”的负样本,少数是“有裂缝”的正样本。我的建议是:
把模型在置信度0.25到0.6之间的预测结果自动保存起来,定期人工复核。这些所谓“不确定样本”是最有价值的难例数据,里面既有漏检的真裂缝,也有误检的假裂缝。把它们标注好加入训练集,下一轮模型的改进效果会非常明显。这也是我把这个项目公开出来的主要原因——希望更多人能在数据层面共享经验,而不是在一个封闭的模型参数里打转。
写在最后的实际操作体会
整个项目做下来,我最大的感受是:YOLOv5s+BIFPN这个组合,是“投入产出比”特别高的一个搭配。BIFPN的代码改动量不大,训练成本几乎可以忽略,但对小目标和多尺度目标的提升是稳定的,尤其适合隧道裂缝这种细长目标占比高的场景。如果你们项目也要做类似的表面缺陷检测,我建议先按这套方案跑一版,拿到基线之后再决定要不要上更重的模型。
另外,数据集的整理真的值得多花时间。我中途踩过最大的坑就是把几张标注框出错的图混进了训练集,结果那几个框对应的区域loss长期降不下去,才发现是标注坐标越界。后来我写了一个小脚本,训练前自动检查所有标签的坐标和尺寸合法性,从那以后几乎没有因为数据问题返工过。
最后分享一个小技巧:训练结束之后,不管mAP多好看,一定要拿到现场的真实图像上测一测,尤其是找那种光线昏暗、墙面有渗水的角落。指标是参考,现场效果才是真正的验收标准。希望这篇文章能帮你把YOLOv5s+BIFPN的方案跑通,少踩几个我踩过的坑。