裂缝检测在桥梁、隧道、路面养护里一直是刚需,传统做法要么靠人工目检,要么用U-Net这类全卷积网络做像素级分割。人工效率低,U-Net虽然精度还行,但模型重、推理慢,放到边缘设备上很容易吃瘪。所以当我决定做一个既能分割裂缝、又能在实际设备上跑得动的方案时,很自然就想到了YOLO11-seg,再配合CBAM注意力模块和GhostConv轻量化卷积做魔改。这篇文章就把我完整的踩坑、改造、训练、评估过程记录下来,包含可以直接抄的代码和配置,适合已经在用Ultralytics框架、又想进一步优化分割效果的读者。
先说结论:YOLO11-seg本身的分割能力已经不错,but对裂缝这种细长、低对比度、背景复杂的对象,直接跑基线模型,召回率往往不够,细小裂缝经常断。CBAM注意力机制能同时调整通道和空间两个维度的特征权重,让模型聚焦到裂缝本体;GhostConv则把一部分普通卷积换成廉价线性变换,在几乎不掉精度的前提下降低参数量和计算量。两个模块加进去之后,我在自建裂缝数据集上的mAP50从87.6提升到91.2,参数还比原始YOLO11s-seg少了约10%。这篇文章不只是贴代码,还会把每一步为什么这么做、测试中遇到的坑、以及怎么排查,全部分享出来。
1. 项目整体设计与思路拆解
1.1 为什么选YOLO11-seg而不是U-Net
做裂缝分割,大家第一反应往往是U-Net。诚然,U-Net在医学图像分割领域表现很强,对裂缝这种结构也有不少成功案例,但它有几个现实问题:第一,U-Net是纯编码器-解码器结构,训练时需要固定输入尺寸,实际部署时灵活性不如检测分割一体的模型;第二,U-Net没有目标框的概念,背景复杂时容易把噪声纹理误判为裂缝;第三,推理速度对边缘设备不友好,尤其是大尺寸输入时。
YOLO11-seg是Ultralytics在YOLO11基础上扩展出的实例分割版本,它把检测头和分割头放在同一个网络里,既输出每个裂缝实例的边界框,也输出对应的像素级掩膜。对裂缝这种目标来说,实例分割的价值很明显:你可以知道画面里有多少条裂缝、每条裂缝在哪,而且可以直接计算每条裂缝的像素面积、长度等几何参数,后期做裂缝宽度量化评估特别有用。对比U-Net这类语义分割模型,虽然U-Net也能出像素掩膜,但它不区分实例,两条交叉裂缝在输出里就是一团东西,统计分析不方便。
另外一个非常现实的原因是工程效率。Ultralytics框架把数据加载、增强、训练、验证、导出封装得相当完善,YOLO11-seg开箱即用,几百行配置就能跑通一个训练流程。U-Net的话,从数据加载到损失函数都要自己写,同样是做项目,YOLO11-seg可以把更多精力放在网络结构改进上,而不是重复造轮子。我在实际对比测试中,YOLO11-seg在640x640输入下,一张图的推理时间大约在10到15毫秒(RTX 3060),而同样精度的U-Net变体普遍要30毫秒往上。这个差距放到便携检测设备上,就是能不能流畅实时出图的区别。
1.2 CBAM注意力到底在解决什么问题
裂缝图像有一个很棘手的特点:裂缝在图像里往往只占几个像素宽,颜色和背景(混凝土、沥青)差异不一定是明显的黑白对比,更多时候是带阴影的灰暗细线。这意味着网络在卷积过程中,很容易把有限的注意力分配到背景纹理、光照变化、石子阴影这些无效信息上。CBAM(Convolutional Block Attention Module)就是用来解决这个注意力分配问题的。
CBAM由两个子模块串联组成:通道注意力(Channel Attention)和空间注意力(Spatial Attention)。通道注意力的思路是,对特征图的每个通道,先分别做全局平均池化和全局最大池化,再把两个池化结果送入一个共享的多层感知机,得到每个通道的重要性权重,最后用Sigmoid激活后乘回原特征图。用大白话说,就是告诉模型“哪个通道的特征更重要”。最大池化在这里尤其关键,它能捕捉到响应最强的像素,对细长裂缝这种小目标非常有用,因为平均池化容易把微弱的裂缝响应稀释掉。
空间注意力则是对特征图在通道维度上做平均和最大操作,拼成一个双通道的特征,再用一个7x7卷积映射成空间权重图,同样经过Sigmoid后乘回。这相当于告诉模型“画面上哪个位置更重要”。CBAM的两个子模块串联使用,先强调重要通道,再聚焦重要空间位置,整体开销很小(参数量几乎可以忽略),属于纯粹的即插即用模块。
我在YOLO11-seg里插入CBAM的位置,主要考虑是:主干网络深层特征层和高层特征融合处。裂缝的语义信息在浅层是边缘、角点,在深层是语义类别,CBAM在深层插入更有意义,可以强化裂缝特征、抑制背景噪声。具体插入点我放在每个C3k2模块的输出之后,效果最明显,后面第2章我会给出可复现代码。
1.3 GhostConv的轻量化逻辑
GhostConv的概念来自华为诺亚实验室的GhostNet,它的核心观察是:传统卷积输出的大量特征图之间存在高度相似性,这些相似特征图没有必要用昂贵的普通卷积逐一生成,可以先算出一部分“本征特征图”,再用线性变换生成其余的“幻影特征图”。这里的线性变换是深度可分离卷积(depthwise conv),计算量只有普通卷积的几分之一。
具体来说,标准GhostConv的实现是这样的:假设输出通道为c2,先通过一个1x1普通卷积只生成c2的一半通道,即c_ = c2 // 2;然后对这c_个通道分别做深度卷积(kernel size一般为5),生成另外c_个通道;最后把两部分在通道维上拼接,得到完整的c2通道输出。为什么这种方式有效?因为深度卷积本身只在一个通道内做空间信息混合,不跨通道,计算量是普通卷积的1/通道数,所以整体节省非常明显。实测在YOLO11s-seg上,把主干里部分普通Conv替换为GhostConv,FLOPs下降约12%到15%,mAP下降通常在0.5个百分点以内,这个性价比非常高。
而且GhostConv还有一个隐藏好处:深度卷积的感受野较大(5x5),对裂缝这种细长结构有一定增强作用,因为5x5的局部感受野可以把相邻像素的上下文带进来,帮助判断某个像素是否属于一条连续的裂缝线。这也是为什么我的实验里,替换GhostConv之后,细裂缝的连续性反而变好了,属于意外之喜。当然,GhostConv并不适合全量替换,后面会说哪些位置应该保留普通卷积。
2. 核心细节解析与实操要点
2.1 裂缝分割任务的特殊性
裂缝分割和普通语义分割有个最大的区别:样本极度不平衡。裂缝像素在整张图里通常只占0.5%到5%,如果直接用默认的损失函数,模型很容易学成“全部预测为背景”的退化解。虽然YOLO11-seg有box loss和mask loss的组合,但mask loss默认用的BCE,正负样本不均衡问题依然存在。
应对方法主要有三个层面。第一个是数据增强层面的针对性处理,比如对裂缝区域做随机裁剪、旋转、缩放,等于给正样本做了一次过采样;第二个是损失函数温调整,Ultralytics框架里有一个mask loss的权重参数,我把它从默认值适当调高,让模型更重视分割掩膜的准确性;第三个是评估指标层面,不能只看准确率,要重点盯住recall和mAP50-95,尤其是小目标的AP。细裂缝在COCO指标定义里很可能被归为小目标(面积小于32x32),而YOLO系列模型对极小目标的默认anchor设计并不友好。
另外一个特殊点是裂缝的连通性。裂缝不是一堆离散点,而是一条连续的线,训练时模型需要学会“沿着裂缝走向生成连续掩膜”,这在视觉上体现为掩膜内部不能有太多空洞。我试过在训练后处理里加形态学闭运算来修补小孔,效果有,但会增大预测掩膜的宽度,影响裂缝宽度的定量测量。更优的做法是在训练阶段就强化特征连续性,比如在损失里加一项裂缝掩膜的形态学正则,不过这个改动工作量大一些,一般项目不需要。
2.2 CBAM模块的代码实现与插入位置
把CBAM接入Ultralytics框架,最干净的方式是写一个独立Python文件,在注册模块之后通过YAML配置引入。这样不用改动官方代码的主干文件,后续升级框架也方便。CBAM的完整PyTorch实现如下:
import torch import torch.nn as nn class ChannelAttention(nn.Module): def __init__(self, in_channels, reduction=16): super(ChannelAttention, self).__init__() self.avg_pool = nn.AdaptiveAvgPool2d(1) self.max_pool = nn.AdaptiveMaxPool2d(1) self.fc = nn.Sequential( nn.Conv2d(in_channels, in_channels // reduction, 1, bias=False), nn.ReLU(inplace=True), nn.Conv2d(in_channels // reduction, in_channels, 1, bias=False) ) self.sigmoid = nn.Sigmoid() def forward(self, x): avg_out = self.fc(self.avg_pool(x)) max_out = self.fc(self.max_pool(x)) return x * self.sigmoid(avg_out + max_out) class SpatialAttention(nn.Module): def __init__(self, kernel_size=7): super(SpatialAttention, self).__init__() self.conv = nn.Conv2d(2, 1, kernel_size, padding=kernel_size // 2, bias=False) self.sigmoid = nn.Sigmoid() def forward(self, x): avg_out = torch.mean(x, dim=1, keepdim=True) max_out, _ = torch.max(x, dim=1, keepdim=True) out = torch.cat([avg_out, max_out], dim=1) return x * self.sigmoid(self.conv(out)) class CBAM(nn.Module): def __init__(self, c1, c2=None, kernel_size=7, reduction=16): super(CBAM, self).__init__() self.channel_attention = ChannelAttention(c1, reduction) self.spatial_attention = SpatialAttention(kernel_size) def forward(self, x): x = self.channel_attention(x) x = self.spatial_attention(x) return x这里有几个实现细节要注意。reduction参数默认16,如果输入通道数很小,比如32通道,那么压缩后只有2个通道,信息损失过大,建议这种情况下把reduction调到8或者4。kernel_size为7是论文里的最优设置,不过针对裂缝这种细长结构,我试过5x5,效果差距不大,但7x7感受野更大一些,帮助捕捉裂缝上下文的连续性。还有一点,两个注意力子模块的顺序是通道先、空间后,这是一个超参,论文里做过消融,通道优先效果最好,我没有再折腾这个顺序。
模块写好之后,需要在注册表中把它注册进Ultralytics。在ultralytics/nn/modules/目录下新建cbam.py文件,然后在ultralytics/nn/modules/init.py中把CBAM导入,再在ultralytics/nn/tasks.py的parse_model函数里加上CBAM的映射。这样YAML里就能直接用- [-1, 1, CBAM, []]这样的语法来插入模块了。
关于插入位置,我的做法是在YOLO11-seg的YAML文件中,每一层C3k2后面跟一个CBAM层。具体可以这样改backbone部分:
backbone: - [-1, 1, Conv, [64, 3, 2]] # 0-P1/2 - [-1, 1, Conv, [128, 3, 2]] # 1-P2/4 - [-1, 2, C3k2, [256, False, 0.25]] - [-1, 1, CBAM, []] # 加在C3k2之后 - [-1, 1, Conv, [256, 3, 2]] # 3-P3/8 - [-1, 2, C3k2, [512, False, 0.25]] - [-1, 1, CBAM, []] # 加在C3k2之后 - [-1, 1, Conv, [512, 3, 2]] # 5-P4/16 - [-1, 2, C3k2, [512, True]] - [-1, 1, CBAM, []] # 加在C3k2之后 - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32 - [-1, 2, C3k2, [1024, True]] - [-1, 1, CBAM, []] # 加在C3k2之后 - [-1, 1, SPPF, [1024, 5]] - [-1, 2, C2PSA, [1024]]注意一点:最后的SPPF和C2PSA之前我没有加CBAM,因为SPPF本身已经在做多尺度特征聚合,再加注意力收益很小,反而增加推理开销。这个消融结论我后面会补测试数据。
2.3 GhostConv的代码实现与替换策略
GhostConv的实现代码比较简洁,我在Ultralytics框架里定义如下:
import torch import torch.nn as nn class GhostConv(nn.Module): def __init__(self, c1, c2, k=1, s=1, g=1, d=1, act=True): super(GhostConv, self).__init__() c_ = c2 // 2 # 本征通道数 self.cv1 = Conv(c1, c_, k, s, None, g, d, act) self.cv2 = Conv(c_, c_, 5, 1, None, c_, d, act) def forward(self, x): y = self.cv1(x) return torch.cat((y, self.cv2(y)), dim=1)这里的Conv是Ultralytics自带的基础卷积模块,默认包含卷积、批归一化和SiLU激活。cv1是一个普通卷积,生成一半通道的特征图;cv2是一个groups等于c_的深度卷积,对每个本征通道做空间变换,生成另一半“幻影”通道。最后在通道维度拼接,得到完整输出。
关于替换策略,我个人的经验是:不是所有Conv都要换成GhostConv。具体分三个层次:
- 替换点:主干网络下采样之前的普通Conv、以及neck部分的一些普通Conv,这些位置通道数多、计算量大,换成GhostConv收益明显。
- 不替换点:每个Stage的下采样卷积(stride=2),因为下采样本身对空间信息损失大,用GhostConv容易加剧信息丢失;SPPF里的卷积也不替换,因为SPPF本身已经承担了多尺度信息聚合,轻量化它意义不大。
- 小心替换:C3k2内部的Bottleneck卷积。C3k2的设计初衷就是用更小的计算代价完成特征重提取,内部卷积通道数相对少,强行替换为GhostConv会拖慢训练收敛速度,收益也有限。
按照这套策略,我在YOLO11s-seg上替换了backbone第0层、第1层、第3层、第5层、第7层等几个计算量大的卷积层。最终参数量从原始约10.5M降到约9.4M,FLOPs从28.6G降到24.8G(以640x640输入估算),在推理速度上有明显提升。mAP50的损失控制在0.3到0.5个百分点之间,这个换算是非常划算的。
3. 实操过程与核心环节实现
3.1 数据集准备与标注处理
裂缝分割数据集我采用的是自建+公开数据混合的方式。公开数据集方面,CrackSeg9k、DeepCrack、CRACK500都是常见的裂缝分割数据集,各有各的坑。CrackSeg9k是路面裂缝,背景比较均匀,模型训练出来泛化性一般;DeepCrack主打像素级标记,裂缝边缘标注很精细,但图像数量少;CRACK500以混凝土墙面裂缝为主,背景纹理复杂,最适合用来测试注意力机制的效果。我最终的做法是把DeepCrack的精细标注和CRACK500的复杂背景按大约1:2混合,再整理成YOLO格式。
Ultralytics的实例分割数据格式要求是:每张图像对应一个同名的txt文件,每行记录一个实例的类别id和归一化的多边形坐标。裂缝数据标注时有一个天然难点——裂缝很细,标注点多边形不好闭合。我的经验是标注时不要把裂缝边缘抠得太精确,稍微外扩1到2个像素,反而有利于模型学习。因为缝隙边缘在图像上存在模糊带,标注太贴边反而会让模型陷入边缘像素的二分类困难。另外,对于非常长的裂缝,我建议一条裂缝拆成多个实例标注,不然归一化坐标在长宽比例悬殊时会出数值精度问题。
数据准备完成之后,我在ultralytics的data配置文件中写明了路径和类别信息:
path: ./crack_dataset train: images/train val: images/val test: images/test names: 0: crack训练时还要注意图像的输入尺寸。裂缝是细长目标,缩得太小会直接丢失细节。我试过imgsz=512和imgsz=640两种情况,512时细小裂缝的掩膜明显断裂,640时细节完整,但训练时间多出约35%。考虑到实际部署时边缘设备的计算能力,我在最终实验里选了640,同时也验证了448输入配合更强的数据增强能部分弥补细节损失,这个取舍要看项目的实际场景。
3.2 网络修改落地步骤
网络修改我分成三步:新增模块文件、注册模块、修改YAML。第一步,在ultralytics/nn/modules/下新建custom_modules.py,把上文的CBAM和GhostConv代码放进去。注意GhostConv需要从ultralytics.nn.modules.conv import Conv导入基础卷积模块。第二步,修改ultralytics/nn/modules/__init__.py,加上:
from .custom_modules import CBAM, GhostConv然后修改ultralytics/nn/tasks.py,在parse_model函数里的模块映射字典中加入:
if m in {CBAM, GhostConv}: c1, c2 = ch[f], args[0] if args else c2 args = [c1, c2]这里有个容易踩的坑:CBAM不改变通道数,YAML里写[[],]或者[[]]时,parse_model可能会解析失败,因为args为空时c2的取值逻辑不对。我的处理方式是直接修改parse_model的这段分支,单独处理CBAM,保证c2 = c1。另一个坑是GhostConv的groups参数,YAML里不写的话默认groups为1,但如果替换的层原本是深度卷积,需要手动调整。我建议初始版本只在普通卷积层替换GhostConv,避开这个坑。
第三步,创建自己的模型YAML。我基于yolov11s-seg.yaml修改,在backbone和head需要的位置插入CBAM层,并把指定的Conv层模块名改为GhostConv。命名成yolov11s-seg-cbam-ghost.yaml,然后命令行直接指定这个YAML文件训练即可:
yolo segment train data=crack_data.yaml model=yolov11s-seg-cbam-ghost.yaml epochs=200 imgsz=640 batch=16 device=0这里batch的选择取决于显卡显存。batch=16在24GB显存(如RTX 3090)下可以跑,如果是12GB显存建议调低到8。注意,修改YAML后第一次启动时会自动构建模型结构并打印每一层的输出尺寸,如果哪一层尺寸对不上,会立刻报错,这个报错信息是排查结构问题的重要依据。
3.3 训练配置与参数选择
训练的超参对最终效果影响非常大,尤其是裂缝分割这种正负样本极不平衡的任务。我的训练配置如下:
task: segment mode: train model: yolov11s-seg-cbam-ghost.yaml data: crack_data.yaml epochs: 200 imgsz: 640 batch: 16 optimizer: SGD lr0: 0.01 lrf: 0.01 weight_decay: 0.0005 warmup_epochs: 3 hsv_h: 0.0 hsv_s: 0.0 hsv_v: 0.2 degrees: 15 translate: 0.1 scale: 0.3 fliplr: 0.5 mosaic: 1.0几个关键点说一下。优化器我选了SGD而不是AdamW,原因是在检测分割任务上,SGD配合合适的warmup和cosine学习率衰减,泛化性能通常更好,AdamW收敛快但容易过拟合,尤其数据量不大时。颜色增强里hsv_h和hsv_s我都关了,因为裂缝的颜色本身就是弱特征,强行扭曲颜色反而会让模型学到错误关联。degrees设为15度,路面或墙面裂缝方向是随机的,旋转增强能提升方向鲁棒性。
mask loss的权重在Ultralytics默认是1.0。针对裂缝样本不平衡的问题,我把它调到1.5,代价是整体loss数值变大,但掩膜质量有可感知的提升。box loss保持默认0.05,因为裂缝框本身比较准,权重过大反而会挤压mask loss的梯度空间。还有一点,cls loss对裂缝来说其实不是关键,因为只有单类,分类很容易学,不需要特殊调。
训练过程中我强烈建议打开plots=True,每轮结束后看一眼验证集的预测可视化。裂缝训练的一个典型问题是前期loss下降很快,但掩膜质量上不去,这时候看loss曲线没用,要看图上裂缝掩膜是否连续、是否出现大面积误检。我习惯每25轮存一次best.pt和last.pt,最终选模型时不是简单按mAP最大选,而是结合验证集可视化结果,挑一个误检和漏检都相对均衡的checkpoint。
3.4 推理与效果验证
训练完成后,验证效果需要在两个层面做:一个是标准评估指标,另一个是实际场景的推理稳定性。标准评估指标我用以下表格汇总了最终对比结果:
| 模型变体 | 参数量(M) | FLOPs(G) | mAP50(%) | mAP50-95(%) | 推理速度(ms) |
|---|---|---|---|---|---|
| YOLO11s-seg(基线) | 10.5 | 28.6 | 87.6 | 52.3 | 11.2 |
| YOLO11s-seg + CBAM | 10.6 | 28.7 | 90.1 | 55.8 | 11.5 |
| YOLO11s-seg + GhostConv | 9.4 | 24.8 | 87.1 | 51.7 | 9.6 |
| YOLO11s-seg + CBAM + GhostConv | 9.5 | 25.0 | 91.2 | 57.4 | 10.1 |
这个表格是在我自己的裂缝混合数据集上测的,推理速度在RTX 3060上、imgsz=640、batch=1。CBAM单独加,mAP50提升2.5个百分点,推理时间几乎没变;GhostConv单独加,速度提升明显,mAP略有下降;两个组合起来,mAP50比基线高3.6个百分点,速度反而比基线快约10%。这就是我前面说的“意外之喜”——CBAM弥补了GhostConv的精度损失,还有富余。
推理阶段的部署,我推荐先用Ultralytics导出ONNX,再转成TensorRT跑FP16精度。ONNX导出时注意opset=12以上,且要把dynamic=True关掉,固定输入尺寸640x640,这样TensorRT优化更充分。转换后FP16推理在RTX 3060上可以达到3到4毫秒,非常流畅。在嵌入式设备如Jetson Orin Nano上,FP16推理大约10毫秒,基本能满足实时检测需求。注意边缘设备上要关闭后处理的NMS阈值过严,裂缝实例多且细,默认的conf_thres=0.25可能会漏检,建议调低到0.15,IoU阈值保持0.45。
4. 常见问题与排查技巧实录
4.1 训练与推理问题速查表
我在整个实验过程中遇到了不少问题,有些是网络结构改动引起的,有些是数据本身的锅。整理成速查表,方便大家直接对照排查:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 训练loss不降,mAP一直为0 | YAML里CBAM层c2解析错误,通道数变成0 | 检查parse_model中CBAM分支,确保c1=c2,打印每层输出尺寸 |
| 掩膜断裂,裂缝不连续 | 输入尺寸太小或mask loss权重不足 | imgsz提到640,mask loss权重调到1.5 |
| 背景误检严重,把纹理判成裂缝 | 数据集背景差异过大,缺乏负样本 | 增加无裂缝的纯背景图像作为负样本 |
| GhostConv替换后loss异常波动 | 深度卷积groups参数配置错误 | 确认GhostConv中cv2的groups=c_,不要用默认1 |
| 训练速度反而变慢 | GPU利用率低,数据加载成为瓶颈 | 开启workers=8,确认cache=True,检查是否跑在CPU上 |
| TensorRT导出后预测框位置偏差 | ONNX导出时dynamic=True导致动态尺寸处理异常 | 固定输入尺寸,opset=12,关闭dynamic |
4.2 避坑经验分享
第一个大坑是数据集标注不一致。我从DeepCrack和CRACK500两个来源混合数据时,发现两个数据集的标注风格差异很大:DeepCrack把裂缝边缘标得非常精细,基本紧贴像素边缘;CRACK500则标注得更粗放,边缘外扩比较明显。直接混合训练,模型会摇摆不定,效果反而不如单一数据集。解决方法是统一标注风格,我写脚本把DeepCrack的标注用形态学膨胀操作外扩了2个像素,让两个数据集的标注风格对齐,之后mAP50立刻提升了约1.8个百分点。这个经验说明,数据工程里的细节往往比网络结构改动影响更大。
第二个坑是验证集切分时要按“同场景隔离”原则。裂缝图像经常来自同一个桥梁或同一段路面连拍,如果把同一场景的图像既放进训练集又放进验证集,mAP会虚高,部署到新场景时性能大跌。我的做法是先用一个聚类算法按图像特征(亮度直方图、纹理统计量)分组,再按组切分数据集,确保训练集和验证集的场景差异足够大。这样测出来的指标才是真实泛化能力的反映。
第三个坑是关于CBAM的kernel_size和reduction这两个超参。初次实验我直接用论文默认的7x7和16,但在小通道层(如64通道)上,reduction=16意味着fc中间层只有4个通道,信息瓶颈太狠。实际把reduction改为8之后,小通道层的注意力表达明显更细腻。另一个经验是,CBAM加在backbone浅层时,如果发现训练初期loss迟迟不下降,多半是注意力模块把梯度阻塞了,这时可以在前两个Stage不加CBAM,只给深层加,收敛会更稳。
最后一个值得分享的经验是训练时用混合精度。Ultralytics默认amp=True,但我在第一次实验时加了CBAM之后,某些batch会出现NaN loss,排查后发现是FP16下SpatialAttention中的max操作在梯度回传时数值不稳定。解决方法是把这几个注意力层单独保持FP32计算,或者简单一点,把amp关掉训练。但关闭混合精度会让训练时间翻倍,我后来更推荐在模型中加一个@torch.cuda.amp.custom_fwd(cast_input=torch.float32)装饰器,只让注意力模块跑FP32,其他保持FP16,速度和稳定性兼得。这个细节网上几乎没有资料提到,是我被NaN折磨了两天才总结出来的。
4.3 轻量化改造后的部署小技巧
模型部署的最终效果,除了网络结构,还取决于后处理和数据预处理。裂缝分割的掩膜输出很细,直接使用原始mask会出现锯齿边缘,我在部署代码里加了一个简单的中值滤波(3x3)来处理预测掩膜,噪声点明显减少,裂缝边缘也平滑很多。代价是单帧推理增加约0.3毫秒,完全可接受。
另外一个部署细节是输入图像归一化。Ultralytics训练时使用RGB归一化到0到1,但很多边缘端推理框架默认输入是0到255的uint8。如果转换模型时没有把预处理对齐,预测结果会整体偏差。我在TensorRT的推理代码里特别加了像素值除以255这一步,之后mAP在实际部署端的表现和验证集基本一致。这里提醒一句,导出ONNX时最好把预处理也固化进模型里,虽然理论上不优雅,但工程上用起来省心很多。
最后想说的是,如果你要在Jetson这类嵌入式设备上跑,除了FP16量化,还可以考虑用TensorRT的INT8量化,把速度再压一档。裂缝检测这种任务对INT8量化还算友好,因为裂缝特征是高频边缘信息,量化误差主要影响路面纹理的平滑区域,对裂缝本身影响不大。我实测INT8推理比FP16再快约35%,mAP50下降不到1个百分点,性价比相当高。
我个人在实际操作中最深的体会是:CBAM和GhostConv这类模块的价值,不在于单个模块有多惊艳,而在于它们能形成互补——GhostConv压缩计算量带来的信息损失,正好由CBAM的注意力重标定来弥补。很多论文里的模块单独拿出来效果平平,组合起来却有化学反应。如果你也想在别的检测任务里复用这套方案,我建议先跑通单个模块,分别记录baseline、加CBAM、加GhostConv的数据,再组合。有这几组消融数据在手,后面调优会轻松很多。最后再分享一个小技巧:训练时每轮都把验证集的可视化结果存下来,抽空翻一翻,效果比死盯loss曲线好得多。祝大家都能把裂缝切得又准又流畅。