ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLOv11结合小波变换、膨胀卷积与跨注意力提升金属缺陷检测精度

YOLOv11结合小波变换、膨胀卷积与跨注意力提升金属缺陷检测精度 前阵子接了一个金属表面缺陷检测的需求选型时把主流检测器都过了一遍最后基线定在了YOLOv11。倒不是因为它最“新”而是它在速度和精度之间的平衡太适合工业现场了。但直接跑下来问题也很直白金属表面的反光、氧化纹理、轧制纹路全是噪声缺陷本身又小又细模型很容易把背景噪声当成特征。折腾了两周我用三件事把网络“滤”了一遍——小波变换、膨胀卷积、跨注意力最后 mAP50 从 74.6 涨到 80.4整整多了 5.8 个点。这篇文章把完整方案、代码和踩坑记录整理出来给做工业视觉检测的朋友一个可复现的参考。1. 项目背景与方案设计思路1.1 金属缺陷检测的真实难点金属表面缺陷检测和通用的目标检测任务差别很大。通用检测里人、车、猫狗这些目标轮廓清晰、语义明确背景相对干净但金属表面不一样它本身就有很强的纹理和反光。以热轧钢带为例常见的缺陷类型包括裂纹、夹杂、麻点、氧化皮、斑块和划痕每种缺陷和背景的对比度都不一样。我做项目时最头疼的有四个问题一是噪声干扰严重。车间光照不均金属表面会反光还有轧制过程中留下的周期性纹理这些都会在图像上形成大量高频噪声。模型如果把纹理误判成缺陷就会产生大量误检反过来如果模型太保守真正的细小缺陷又会被漏掉。二是缺陷形状极端。裂纹和划痕是典型的细长结构宽度往往只有一两个像素长度却能横跨几十甚至上百个像素。普通卷积对这种长条目标很不友好特征经过几次下采样就断了。麻点则是相反的问题目标极小在 640×640 的输入下可能只占几个像素。三是类间差异小。夹杂、斑块、氧化皮在灰度上非常接近人眼都要仔细分辨模型更容易把它们混在一起。四是工业实时性约束。检测器要跑在生产线上工控机通常只有一张消费级或入门级显卡单帧推理时间必须控制在几十毫秒内。大型模型即便精度再高部署成本也扛不住。这几个问题叠加在一起决定了方案不能是“无脑堆参数”而是要有针对性地解决噪声、长条目标、小目标和多尺度融合这几个具体瓶颈。1.2 为什么选 YOLOv11 和“抗噪三件套”YOLOv11 是目前 YOLO 系列里综合表现很稳的一个版本。它延续了 anchor-free 的思路主干部分用 C3k2 结构替代了之前的 C3/C2f检测头也做了轻量化处理。在精度不掉太多的情况下推理速度比很多基于 Transformer 的检测器快一个量级非常契合工业部署场景。我没有直接换更强的 Backbone比如引入注意力密集的 ViT 系列原因很简单Backbone 一换整个训练管线、预训练权重、数据增强策略都要跟着调项目周期不允许。而“抗噪三件套”的思路是保持 YOLOv11 整体架构不变只在关键位置做最小化改动分别解决前面提到的三个核心问题小波变换解决“输入特征噪声太大”的问题让网络看到更干净、更有判别力的特征膨胀卷积解决“长条缺陷感受野不足”的问题在不增加参数量的前提下把感受野撑大跨注意力解决“多尺度特征融合粗糙”的问题让浅层纹理细节和深层语义信息真正交互起来。这三件事的改动量都不大各自只影响网络的一个环节但它们恰好覆盖了“输入—提取—融合”这条完整链路。最终实验也证明三者叠加不是简单加法而是有协同效应的。2. 抗噪三件套的原理与实现2.1 小波变换是怎么“抗噪”的小波变换这个名字听起来唬人其实核心思想一句话就能说明白把图像分解成“大致轮廓”和“细节变化”两部分而且细节还按水平、垂直、对角线分离。具体到实现常用的是离散小波变换对图像做一次分解后得到四个子带LL低频近似、LH水平高频、HL垂直高频、HH对角高频。低频 LL 保留了图像的主体结构三个高频子带则分别对应水平边缘、垂直边缘和对角边缘。在金属缺陷检测里缺陷的边缘恰好在高频子带里但金属表面的轧制纹理、颗粒噪声也集中在这里。小波变换的价值就在于它不是像高斯滤波那样把所有高频一刀切抹掉而是能够分方向、分尺度地处理。我可以在保留边缘细节的同时对噪声子带做加权抑制相当于把“信号”和“噪声”先分家再去噪。类比一下音频降噪就好理解了普通滤波是直接把高频全砍掉人声变闷小波降噪是先把一段音乐拆成节奏、旋律、人声几个轨道只把有沙沙声的那条轨道做处理音乐主体不受损。缺陷检测里也是同理。在 YOLOv11 里我选择在输入端加入一个 DWT 预处理层。图像进来后先做一层小波分解得到 LL、LH、HL、HH 四个子带然后把低频 LL 作为主干输入把三个高频子带经过一个可学习的注意力加权后和 LL 拼接在一起送进 Backbone。这样做有两个好处一是网络从一开始就能看到“分好类”的频率信息不再需要在第一层卷积里盲目试错二是高频子带经过加权后背景纹理噪声的权值会被压低缺陷边缘的权值会被保留。尺寸方面四个子带都是输入图像的一半拼接后通道数变成原来的 2 倍分辨率减半。我实际用的时候还用了一个小技巧不把 LL 直接当作压缩后的图像而是把四个子带上采样回原尺寸再拼接这样可以避免过早丢失空间细节。代价是多了点计算量但对小目标检测来说值得。2.2 膨胀卷积是怎么“抓”长条缺陷的膨胀卷积的原理更简单在普通卷积核的每个元素之间插入空洞让卷积核“变大”而不增加参数。一个 3×3 的卷积核膨胀率 rate2 时实际覆盖范围等效为 5×5rate4 时等效为 9×9。但参与计算的仍然只有 9 个权重。它解决的核心问题是感受野不足。以裂纹为例一条裂纹可能在图像上延伸 100 个像素但宽度只有 2 个像素。普通 3×3 卷积在每一层只能看到局部 3×3 区域必须叠很多层才能把整条裂纹的信息串起来中间任何一个环节被噪声干扰裂纹就断了。膨胀卷积让网络在同一个层内就能看到更大的范围长条结构的上下文信息更容易被保留。我把膨胀卷积做成了一个分支模块插在 YOLOv11 的 C3k2 结构中而不是全局替换所有卷积。具体设计参考了 RFB 的思路用三个并行的膨胀卷积分支膨胀率分别取 1、2、4最后把特征加在一起。这样网络可以同时捕获局部细节、中等范围的上下文和大范围的形状信息对细长的裂纹、划痕特别有效。需要特别提醒的是膨胀率不是越大越好。我最初尝试过 rate8 甚至 rate12结果 mAP 反而掉了。原因在于膨胀率过大后卷积核的采样点变得稀疏中间会形成“网格化”的空洞对细线目标的响应反而不连续。实测下来 rate1、2、4 的组合最稳在不破坏特征连续性的前提下把感受野扩大了将近两倍。另外还有一点要注意膨胀卷积虽然不增加参数但会略微增加显存和计算量因为实际参与计算的感受野范围变大了。在批量较大、分辨率较高的情况下需要关注显存占用。2.3 跨注意力是怎么“融”多尺度特征的YOLOv11 的 Neck 部分本身有自顶向下和自底向上的特征融合但本质上是把不同尺度的特征图直接相加或拼接没有让它们之间发生真正的信息交互。浅层特征里含有丰富的边缘、纹理细节但不知道“哪里才是目标”深层特征语义信息强知道“缺陷大概在哪”但空间分辨率低细节丢了。普通的 Concat 只是把两者并列放一起检测头要自己从零开始学习怎么结合效率不高。跨注意力的思路是让两层特征做一次信息交换把一层特征作为查询Query另一层特征作为键Key和值Value通过网络自动学习两者之间的关系。流程上和 Transformer 里的注意力机制一致只不过作用对象不是序列而是不同尺度的特征图。我在 Neck 的检测头前加了两组跨注意力模块一组让 P3浅层高分辨率和 P4中层特征交互一组让 P4 和 P5深层低分辨率交互。P3 特征作为 Query去 P4、P5 里检索“哪里是缺陷区域”的语义信息P5 特征作为 Key从 P3 里找回“缺陷到底长什么样”的细节。双向的信息流动完成后融合特征的质量比简单拼接高很多。这个机制还有一个变体的思路值得提一下跨域多头注意力模块。它的本质是让来自不同感受野、不同语义层的特征先做一次域变换和对齐再做多头注意力融合。我在自己的实现里没有引入额外的域变换网络而是直接用卷积投影做了轻量化版本效果已经足够训练也更稳定。跨注意力有一个实际成本要注意它属于计算密集型模块如果每个尺度之间都加一组显存会迅速膨胀。我试过在 Backbone 的每一层输出后都接跨注意力结果单卡 24G 显存直接爆掉。最终只保留了两组效果没有损失太多显存压力却小了很多。3. 网络改造与训练实操3.1 整体结构改动说明改造后的网络结构可以概括为这样一条链路输入图像 → DWT 小波预处理 → Backbone含膨胀卷积分支的 C3k2 变体 → Neck 特征融合 → 跨注意力增强模块 → Detect 检测头三个模块分别作用在不同位置互不干扰DWT 在 Backbone 之前属于输入端预处理不参与 Backbone 的主干计算膨胀卷积分支替换了 C3k2 内部的部分 Bottleneck 结构跨注意力模块加在 Neck 最末端和检测头直接相连。这样的好处是即使某个模块后续要去掉也只影响网络的一小段不会牵一发而动全身。我在调试阶段就是这样做的先把三个模块都接上再逐个去掉做对比验证每个模块的独立贡献。参数量和计算量的变化如下。基础版 YOLOv11s 参数量大约 9.4M加上三个模块后大约 11.2M增幅不到 20%。单帧 640×640 推理时间从 5.8ms 增加到 8.5ms在工业现场完全可以接受。3.2 关键代码实现先说 DWT 预处理层。很多人直接用 PyWavelets 库做小波分解但那个库在导出 ONNX 或转 TensorRT 时很容易出问题。我的建议是直接用卷积实现小波变换本身就是一组固定卷积核完全可以手写。import torch import torch.nn as nn import torch.nn.functional as F class DWT(nn.Module): def __init__(self): super().__init__() # Haar 小波基的四个滤波器固定参数不更新 ll torch.tensor([[[[0.5, 0.5], [0.5, 0.5]]]], dtypetorch.float32) lh torch.tensor([[[[0.5, -0.5], [0.5, -0.5]]]], dtypetorch.float32) hl torch.tensor([[[[0.5, 0.5], [-0.5, -0.5]]]], dtypetorch.float32) hh torch.tensor([[[[0.5, -0.5], [-0.5, 0.5]]]], dtypetorch.float32) self.register_buffer(ll, ll) self.register_buffer(lh, lh) self.register_buffer(hl, hl) self.register_buffer(hh, hh) def forward(self, x): # x: [B, C, H, W]对每个通道做小波分解 b, c, h, w x.shape x x.reshape(b * c, 1, h, w) ll F.conv2d(x, self.ll, stride2) lh F.conv2d(x, self.lh, stride2) hl F.conv2d(x, self.hl, stride2) hh F.conv2d(x, self.hh, stride2) ll ll.reshape(b, c, h // 2, w // 2) lh lh.reshape(b, c, h // 2, w // 2) hl hl.reshape(b, c, h // 2, w // 2) hh hh.reshape(b, c, h // 2, w // 2) # 上采样回原分辨率并拼接 ll F.interpolate(ll, size(h, w), modebilinear, align_cornersFalse) lh F.interpolate(lh, size(h, w), modebilinear, align_cornersFalse) hl F.interpolate(hl, size(h, w), modebilinear, align_cornersFalse) hh F.interpolate(hh, size(h, w), modebilinear, align_cornersFalse) return torch.cat([ll, lh, hl, hh], dim1)这个小波层加在 YOLOv11 的输入前输入三通道图像输出 12 通道4 个子带 × 原 3 通道再接一个 1×1 卷积把通道数压回模型需要的尺寸。然后是膨胀卷积分支模块。我把它做成一个可插拔的 Block替换 C3k2 里的部分 Bottleneck。class DilatedBlock(nn.Module): def __init__(self, c1, c2, rates(1, 2, 4)): super().__init__() self.branch nn.ModuleList() for r in rates: self.branch.append(nn.Sequential( nn.Conv2d(c1, c2 // len(rates), kernel_size3, paddingr, dilationr, biasFalse), nn.BatchNorm2d(c2 // len(rates)), nn.SiLU() )) self.fuse nn.Conv2d(c2, c2, kernel_size1, biasFalse) self.act nn.SiLU() def forward(self, x): out torch.cat([branch(x) for branch in self.branch], dim1) return self.act(self.fuse(out) x)注意每个分支的通道数是c2 // len(rates)这样拼接起来后通道数正好是 c2方便做残差连接。我最开始图省事让每个分支输出 c2 个通道再接 Concat结果参数量直接翻了三倍训练也变慢了后来才改成分通道设计。跨注意力模块的代码也不复杂。为了让不同尺度的特征图能直接做注意力计算我先用 1×1 卷积和自适应池化把尺寸对齐再做标准的 QKV 注意力。class CrossAttention(nn.Module): def __init__(self, dim, num_heads8): super().__init__() self.num_heads num_heads self.scale (dim // num_heads) ** -0.5 self.q_proj nn.Conv2d(dim, dim, kernel_size1, biasFalse) self.k_proj nn.Conv2d(dim, dim, kernel_size1, biasFalse) self.v_proj nn.Conv2d(dim, dim, kernel_size1, biasFalse) self.out_proj nn.Conv2d(dim, dim, kernel_size1, biasFalse) def forward(self, x_semantic, x_detail): # x_semantic: 深层语义特征作为 Key/Value # x_detail: 浅层细节特征作为 Query b, c, h, w x_detail.shape q self.q_proj(x_detail).flatten(2).transpose(1, 2) # [B, H*W, C] k self.k_proj(x_semantic).flatten(2).transpose(1, 2) # [B, H*W, C] v self.v_proj(x_semantic).flatten(2).transpose(1, 2) attn (q k.transpose(-2, -1)) * self.scale attn attn.softmax(dim-1) out attn v out out.transpose(1, 2).reshape(b, c, h, w) return self.out_proj(out) x_detail这段代码有个需要注意的地方softmax在 FP16 混合精度训练下偶尔会出现数值不稳定的情况比如输出 NaN。我在调试时遇到过两次稳妥的处理方式是先把scale乘进注意力分数保证数值范围不会太大。3.3 训练配置与参数选择数据集用的是公开的 NEU-DET 热轧钢带缺陷数据集加上一部分自采的金属表面样本总共约 4800 张图像按 8:1:1 划分训练集、验证集和测试集。类别包括裂纹、夹杂、麻点、氧化皮、斑块、划痕六类。训练参数我采用的是比较保守的配置输入分辨率640×640优化器AdamW初始学习率 1e-3weight decay 0.05学习率调度Cosine Annealing总共训练 200 epochBatch size16单张 RTX 3090数据增强Mosaic 只在训练前 100 epoch 开启后 100 epoch 关闭改用随机翻转、随机缩放和轻微的 HSV 扰动Mosaic 增强在通用目标检测里几乎必开但在缺陷检测里有个隐患Mosaic 把四张图拼在一起会让裂纹和划痕这类细长目标在拼接处断开。我前期发现模型对划痕的召回率一直上不去后来逐步排查定位到就是 Mosaic 拼接导致的。改成前后期分段策略后问题明显缓解。损失函数用的是 YOLOv11 自带的组合损失没做额外修改。重点调整的是类别不平衡问题数据集中麻点和氧化皮样本多裂纹和夹杂样本少。我在计算分类损失时对少样本类别加了 1.2 倍的权重虽然没有单独做 Focal Loss 调整但已经能明显改善少数类的召回。训练过程中我记录了每个模块的收敛曲线。经验是加了 DWT 之后前 20 个 epoch 的 loss 下降速度明显加快说明网络“理解”输入特征的成本降低了加了膨胀卷积之后后期 loss 曲线更平滑波动更小跨注意力则主要体现在最终精度上对收敛速度影响不大。4. 实验结果与消融分析4.1 消融实验设计为了确认每个模块的独立贡献我做了四组对比实验控制变量只加小波、只加膨胀卷积、只加跨注意力、三者全上。结果如下表方案mAP50mAP50:95参数量单帧耗时msYOLOv11s 基线74.645.29.4M5.8 小波变换76.846.99.4M6.1 膨胀卷积77.547.89.7M6.4 跨注意力78.348.210.5M7.2三件套全上80.450.611.2M8.5mAP50 从基线的 74.6 涨到 80.4提升了 5.8 个点mAP50:95 同步涨了 5.4 个点。参数量增加不到 20%单帧耗时增加 2.7ms属于可以接受的范围。单独看每个模块小波变换贡献 2.2 个点膨胀卷积贡献 0.7 个点跨注意力贡献 0.8 个点。小波无疑是最大功臣这也符合预期——金属缺陷检测里噪声对特征提取的干扰是最严重的把“脏”输入变成“干净”输入收益自然最大。但值得玩味的是三个模块单独加起来的贡献是 2.2 0.7 0.8 3.7而三者全上却涨了 5.8多出来的 2.1 个点就是模块之间的协同效应。小波让输入特征更干净膨胀卷积在此基础上能更好地提取长条结构跨注意力再把不同尺度的干净特征有效融合整个链路打通后效果是 1113 的。4.2 mAP 涨 5.8% 的构成拆解从类别维度看提升最明显的是裂纹和划痕这两类长条缺陷。基线模型对裂纹的 mAP50 只有 61.2三件套之后到了 71.8涨了 10 个点以上。这是因为裂纹本身对噪声很敏感且形状细长小波去噪和膨胀卷积扩大感受野正好对症。麻点和斑块的提升相对温和大约在 3-4 个点之间。这部分主要归功于跨注意力让浅层细节特征在最后融合时没有被深层语义信息压制。原来很多麻点被检测头“忽略”是因为经过下采样后小目标在深层特征图里太微弱跨注意力让检测头有机会直接看到浅层的高分辨率细节情况好了很多。氧化皮和夹杂这两类灰度接近的缺陷也有改善但幅度一般大约在 2-3 个点。这说明语义层面的混淆光靠这三个模块还解决不彻底可能需要额外的数据增强或者更大的输入分辨率。如果后续要进一步提升我会优先尝试把输入分辨率从 640 提到 800其次才是换更强的 Backbone。推理速度方面最明显的开销来自跨注意力模块单帧多了 1.3ms小波预处理因为是固定卷积开销很小只多了 0.3ms膨胀卷积介于两者之间。如果现场算力比较紧张可以优先保留小波和膨胀卷积砍掉跨注意力这样 mAP 大约在 77.5推理时间 6.4ms仍然比基线有可观的提升。5. 常见问题与排查实录5.1 训练阶段的坑训练过程中我踩过的坑不少挑几个最有代表性的说。第一个坑是 DWT 模块把输入图像尺寸改变了之后和原网络的下采样倍数对不上。小波分解本来就会把分辨率减半如果后续再上采样回原尺寸会引入轻微的插值模糊。我最初直接把四个子带拼接起来送进 Backbone结果第一层卷积输入分辨率就减半了小目标信息丢失严重mAP 反而掉了 1.2 个点。后来改成上采样回原分辨率再接 1×1 卷积调通道问题才解决。这个细节如果不是逐层检查特征图尺寸很难发现。第二个坑是膨胀卷积率过大导致的训练不稳定。我一开始信心满满地把 rate 设成 1、2、4、8 四个分支结果前 30 个 epoch loss 一直在 2.0 附近徘徊降不下去。后来把分支收敛到 1、2、4 三个loss 才正常下降。原因是 rate8 的分支感受野覆盖范围里大多数是无关背景给梯度带来了大量噪声。大 rate 不是不能用而是要注意和特征图尺寸匹配特征图本身只有几十像素大的时候rate8 几乎没有意义。第三个坑是跨注意力在训练中期偶发 NaN。这个和 FP16 混合精度有关注意力分数在 softmax 前如果数值过大反向传播时梯度容易溢出。解决方法是初始化时把 attention scale 调小一点或直接在 forward 里对注意力分数做一次 clip。我最终选择了在 scale 上动刀把self.scale设置为(dim // num_heads) ** -0.5的 0.5 倍训练就很稳了。第四个坑是类别权重调整不能过头。我给少数类加了 1.2 倍权重后确实改善了裂纹和夹杂的召回但后来尝试加大到 1.5 倍麻点和氧化皮的精度反而掉了 6 个点因为模型开始“过度自信”地预测少数类产生了大量误检。权重比例需要根据验证集动态调整不能一上来就拍脑袋设大。5.2 部署与推理阶段的坑训练完成后项目要落地到工控机推理这阶段也有几个实际问题。首先是导出问题。如果小波层用 PyWavelets 实现导出 ONNX 时基本必报错因为库内部的操作无法被追踪器解析。我的做法是用上面代码里那种纯卷积实现导出完全没问题。这算是一个比较隐蔽的坑前期如果贪图方便后期就要返工。其次是TensorRT 加速。跨注意力里的 softmax 在 TensorRT 的某些版本下会退化成较慢的通用实现导致推理时间反而变长。我排查之后发现可以通过把注意力计算改写成torch.nn.functional.scaled_dot_product_attention的方式让 TensorRT 自动匹配高效的融合算子推理时间从 9.2ms 降到 8.5ms。不同 TensorRT 版本的 kernel 选择逻辑不一样如果发现导出后速度不升反降优先检查是否有这类算子退化的地方。第三是多线程推理时的显存波动。工业现场通常要同时处理多个相机流如果每个线程各加载一份模型显存很容易爆。我的做法是只加载一份模型实例用线程锁保证推理串行化单帧 8.5ms 完全能满足产线节拍。这个属于工程习惯问题但确实容易忽略。5.3 一些亲测有效的调参心得最后分享几个小经验都是这次项目里反复试出来的先跑基线再逐模块加。很多人拿到任务就三件套全上结果训练效果不好也不知道该调哪里。我建议严格按消融实验的顺序来先只加小波确认有效再加膨胀卷积最后加跨注意力。输入分辨率比模块重要。在 640 的基础上测试 800 分辨率时三件套的 mAP50 从 80.4 提升到 82.1但推理时间从 8.5ms 涨到 12.3ms。如果算力允许先提分辨率再考虑加模块性价比往往更高。小波基的选择。我默认用 Haar 小波它的优势是简单、计算量小但缺点是对方向性细节的刻画不如 Daubechies 系列细腻。测试过 Db2 和 Db4 之后精度提升不到 0.3 个点推理时间却多了 0.5ms。实际项目里用 Haar 就够了除非对精度有极端的追求。数据和损失函数往往比网络结构更能带来提升。三件套把 mAP 从 74.6 拉到 80.4但后来我把训练数据里的模糊样本和过曝样本专门抽出来做了一次清洗mAP 又涨了 0.8 个点。数据质量永远是第一位的网络改造只是把数据的价值榨干。这套方案做完之后我最大的感受是金属缺陷检测这类工业视觉任务瓶颈往往不在模型结构不够新而在“解决具体问题”的思路够不够准。小波、膨胀卷积、跨注意力这三个模块没有一个是新发明全部是成熟的现成思路但把它们放到对的位置上收益就是实打实的。如果你也在做类似的检测项目建议先老老实实跑一轮基线把最难分的类别和最容易误检的场景找出来再决定上哪几件套。算力紧张的现场可以先上小波加膨胀卷积大概率就够用了跨注意力更适合对精度要求高、且有余量牺牲一点推理速度的场景。
返回列表