资讯动态

YOLO26小缺陷检测升级:C3k2-EIEM融合边缘与空间特征

发布时间:2026/10/19 9:21:03 来源:尧图企业网站定制
做工业质检目标检测的朋友八成都被“小缺陷漏检”折磨过。这期是我在YOLO26上持续优化迭代的第33个版本核心改动是把主干中的C3k2模块升级为“C3k2-EIEM”融合结构在保留原有检测效率的前提下把边缘特征和空间信息同时塞进特征提取主干。断断续续验证了三周最终在自建的某工件表面缺陷数据集上小目标缺陷类的平均精度均值涨了快4个百分点推理耗时几乎可以忽略。这条改进路线不是玄学调参核心逻辑很直接常规卷积的局部感受野对小缺陷不够敏感而小缺陷恰恰依赖边缘响应和位置信息来定位。EIEM模块就是奔着这两个痛点去的。下面我把整套方案的思路、代码落地、实验对比和踩过的坑一次讲清楚。1. 为什么要给小缺陷检测单独做特征融合1.1 小缺陷在大特征图上的困境目标检测发展到现在大目标的检测已经相当成熟难啃的骨头基本都在小目标和小缺陷上。工业场景里的划痕、凹坑、崩边动不动就占图像面积不到1%用常规检测网络去跑很容易出现三种情况。第一种是特征响应弱。小缺陷在浅层特征图里是有边缘响应的但经过几层下采样之后这些响应会被背景纹理稀释掉尤其当缺陷对比度不高时深层特征图里基本就找不到它的痕迹了。第二种是位置漂移。小缺陷的定位非常依赖空间位置信息但常规卷积核在空间维度上是共享权重的对“缺陷在哪”这件事不敏感。加上下采样的次数多了像素级别的空间误差会被放大导致预测框偏移。第三种是边界模糊。小缺陷与背景之间的边界往往不清晰网络却更倾向于把注意力集中在语义特征强的区域比如工件轮廓、大面积纹理起伏。也就是说网络会主动忽略那些“看起来不重要”的边缘细节——可工业检测要抓的偏偏就是这些细节。EIEM的出发点就是解决这三个问题在特征提取早期就保留边缘响应在融合过程中补足空间坐标信息才不至于让深层网络彻底“忘掉”小缺陷。1.2 为什么选择C3k2作为融合载体YOLO系列的后辈版本里C3、C2f、C3k2这类模块本质上都是CSP风格的变体做法是把输入特征分成主分支和残差分支主分支做重特征提取残差分支做恒等映射最后再拼接融合。C3k2的结构特点是使用了更轻量的Bottleneck设计每个瓶颈的卷积核尺寸可以配置。相比C2f那一套C3k2在同等算力下能堆更多层对小数据集更友好也更容易做自定义改造。把EIEM塞进C3k2比单独在FPN或检测头前面挂一个注意力模块更合理。原因有两条。其一C3k2出现在主干的不同深度EIEM融合模块放到这里可以同时作用于多个尺度的特征而不是只改造最后一层。其二C3k2本身是CSP结构主分支改造不会影响残差分支的梯度回传训练起来更稳也方便做模块级的消融实验。我试过在之后的检测头里单独加边缘注意力分支效果也有但涨幅不如在C3k2内部融合来得明显。检测头那边的特征图分辨率已经很低边缘信息早就不完整了所谓“亡羊补牢”不如从源头把特征喂好。2. EIEM模块的设计思路与核心原理2.1 边缘特征用梯度算子捕获缺陷轮廓EIEM里的“E”对应的就是边缘特征提取。边缘在图像处理里最经典的表达就是梯度像素值发生剧烈变化的地方就是边缘。对一个二维特征图而言水平梯度和垂直梯度分别反映了不同方向的灰度变化两者合起来就能勾勒出目标的轮廓信息。从2020年左右开始目标检测领域开始流行把高频信息显式引入网络结构。常见做法是在卷积之前先做一次Sobel滤波或Laplacian滤波把得到的梯度特征图作为额外的输入通道。但也有问题Sobel算子固定不变跨数据集泛化能力一般而且直接拼接会显著增加通道数拖慢训练和推理。EIEM模块里我用的不是固定算子而是用“可学习的深度可分离卷积”来模拟边缘提取的过程。深度可分离卷积的每个通道只处理一个输入通道的空间信息参数数量很少却能在训练中自动学会类似梯度算子的特征——既保留了边缘提取能力又避免了手工设计算子的局限性。具体实现上EIEM的边缘分支包含两层3x3深度可分离卷积中间接一次批量归一化和ReLU激活。第一层负责提取局部梯度响应第二层负责扩大感受野把相邻边缘连接起来。因为深度可分离卷积的参数少这个分支的额外计算量可以控制在很小的范围内。2.2 空间信息让网络知道缺陷“在哪里”EIEM里的“I”指的是空间信息。很多注意力模块会用全局平均池化来生成通道权重这样做能捕捉通道之间的关系却也丢掉了“缺陷出现在哪个位置”的信息。对图片分类来说位置不重要但对检测来说位置不对一切归零。我参考了坐标注意力Coordinate Attention的思路把水平和垂直两个方向上的特征编码成空间注意力。简单说对输入特征图做一次全局平均池化但是沿着高度方向、宽度方向分别做得到两个一维的描述子。一个描述子负责编码“每一行对缺陷有没有贡献”另一个负责编码“每一列有没有贡献”。把这两个描述子拼接起来过一个小网络再各自还原成与原始特征图宽度、高度匹配的注意力权重最后施加到输入特征上。这样做的好处是水平注意力和垂直注意力是分解的计算量很小同时网络能学习到“沿着第几行第几列附近更容易出现缺陷”这种空间先验。工业场景里缺陷分布往往有规律比如工件表面划痕大多是水平走向崩边多半出现在边缘区域这种分解式的空间注意力学起来非常快。2.3 融合策略加性融合还是门控融合EIEM模块里边缘分支的输出和空间分支出来的注意力融合有两种常见思路。一种是直接相加。把边缘增强后的特征图和空间注意力加权后的特征图逐元素相加实现简单两个分支的信息对等地进入后续卷积。另一种是门控融合。用一个小卷积层从两个分支的输出中生成权重图对两个分支做加权平均。这个做法更灵活网络可以自己学会“什么时候更依赖边缘信息什么时候更依赖空间信息”但也多了一组参数训练初期更容易过拟合。我实测下来在数据量不超过几千张的工业缺陷数据集上直接相加的效果更稳。原因也简单门控机制需要更多数据才能拟合出门控权重样本少的时候门控值容易走极端反而不利于训练。所以EIEM最终采用的是加权相加——初始权重固定为0.5和0.5随着训练过程自动学习分支权重。融合之后还要过一层1x1卷积把两个分支拼接或相加的结果投影回目标维度同时做一次跨通道交互。这一步很关键直接决定融合后的特征有没有被后续模块“消化”。如果不做投影融合特征和C3k2主分支的通道数对不齐拼接时还得额外处理容易出维度错误。3. C3k2融合EIEM的实现方案3.1 网络结构改动位置YOLO26的模型配置文件和早期版本大同小异核心模块都注册在某个模型构建文件里。改动集中在两块一是新定义一个EIEM类二是新定义一个C3k2_EIEM类把原来的C3k2里逐层Bottleneck替换成带EIEM融合的Bottleneck同时保留主分支的CSP思想。以YOLO26主干中某两个下采样阶段为例原先的配置大概是这样的backbone: - [-1, 1, Conv, [64, 3, 2]] - [-1, 1, Conv, [128, 3, 2]] - [-1, 1, C3k2, [128, True, 0.25]]改成融合版本后只需要把关键层的模块名替换backbone: - [-1, 1, Conv, [64, 3, 2]] - [-1, 1, Conv, [128, 3, 2]] - [-1, 1, C3k2_EIEM, [128, True, 0.25]]模块名改了但输入输出的通道数、步长都不变。这样配置文件里的后续结构完全不用动改动风险很小做消融实验也方便——想对比就直接把模块名换回去。3.2 EIEM核心代码实现先看EIEM模块的完整定义PyTorch实现结构是“边缘分支 坐标注意力分支 加权融合 投影”。import torch import torch.nn as nn import torch.nn.functional as F class EIEM(nn.Module): def __init__(self, in_channels, reduction16): super().__init__() # 边缘分支深度可分离卷积自动学习梯度特征 self.dconv1 nn.Conv2d(in_channels, in_channels, 3, padding1, groupsin_channels, biasFalse) self.bn1 nn.BatchNorm2d(in_channels) self.dconv2 nn.Conv2d(in_channels, in_channels, 3, padding1, groupsin_channels, biasFalse) self.bn2 nn.BatchNorm2d(in_channels) self.edge_proj nn.Conv2d(in_channels, in_channels, 1, biasFalse) # 坐标注意力分支分解式空间编码 self.pool_h nn.AdaptiveAvgPool2d((None, 1)) self.pool_w nn.AdaptiveAvgPool2d((1, None)) mid_ch max(in_channels // reduction, 8) self.fc1 nn.Conv2d(in_channels, mid_ch, 1, biasFalse) self.bn3 nn.BatchNorm2d(mid_ch) self.fc_h nn.Conv2d(mid_ch, in_channels, 1, biasFalse) self.fc_w nn.Conv2d(mid_ch, in_channels, 1, biasFalse) # 可学习融合权重 self.fusion_weight nn.Parameter(torch.ones(1)) def forward(self, x): # 边缘分支 edge self.dconv1(x) edge self.bn1(edge) edge F.relu(edge) edge self.dconv2(edge) edge self.bn2(edge) edge F.relu(edge) edge self.edge_proj(edge) # 坐标注意力分支 identity x b, c, h, w x.shape x_h self.pool_h(x) # (b, c, h, 1) x_w self.pool_w(x) # (b, c, 1, w) cat torch.cat([x_h, x_w], dim2) # (b, c, hw, 1) cat self.fc1(cat) cat self.bn3(cat) cat F.relu(cat) x_h, x_w cat.split([h, w], dim2) att_h torch.sigmoid(self.fc_h(x_h)) att_w torch.sigmoid(self.fc_w(x_w)) att identity * att_h * att_w # 加权融合 out x self.fusion_weight * edge att return out有几点值得说明。边缘分支用了两次深度可分离卷积第一次提取局部梯度第二次把边缘连成轮廓。这里没有用固定的Sobel算子为的是让网络根据数据集自适应调整边缘响应的强度。坐标注意力分支中池化后的特征图先拼在一起经过共享的1x1卷积压缩通道再split回两个方向这种方式比分别做两个独立分支更省参数也更容易训练。split的时候需要注意原始特征图的高度和宽度如果下采样后特征图不是正方形split参数要按实际h和w来写。融合部分没有直接把edge分支替换输入而是把边缘分量叠加到原特征上。我特意加了一个fusion_weight参数初始值设为1.0让训练初期边缘特征的影响温和一点网络可以自适应调整。如果一开始边缘分量权重太大训练很容易被梯度噪声带偏。3.3 C3k2_EIEM模块的组装有了EIEM模块改造C3k2就比较直接了。C3k2原本包含若干Bottleneck每个Bottleneck由两个卷积和一个残差连接构成。我做的改动是在Bottleneck内部把第二个卷积的输出接入EIEM再把EIEM的输出加回主分支。class Bottleneck_EIEM(nn.Module): def __init__(self, c1, c2, shortcutTrue, e0.5): super().__init__() c_ int(c2 * e) self.cv1 Conv(c1, c_, 1, 1) self.cv2 Conv(c_, c2, 3, 1) self.eiem EIEM(c2) self.add shortcut and c1 c2 def forward(self, x): y self.cv1(x) y self.cv2(y) y self.eiem(y) return x y if self.add else y class C3k2_EIEM(nn.Module): def __init__(self, c1, c2, n1, shortcutFalse, e0.5): super().__init__() self.c int(c2 * e) self.cv1 Conv(c1, 2 * self.c, 1, 1) self.cv2 Conv((2 n) * self.c, c2, 1, 1) self.m nn.ModuleList( Bottleneck_EIEM(self.c, self.c, shortcut, e1.0) for _ in range(n) ) def forward(self, x): y list(self.cv1(x).chunk(2, 1)) y.extend(m(y[-1]) for m in self.m) return self.cv2(torch.cat(y, 1))C3k2_EIEM的外部接口和原版C3k2保持一致只是内部用的瓶颈从普通Bottleneck换成了Bottleneck_EIEM。这样只改模块不碰其余结构。一个容易踩的坑是C3k2内部的Bottleneck的输入输出通道数相同EIEM的in_channels参数必须和瓶颈输出通道数对齐。如果某个Bottleneck因为e参数导致内部通道和c2对不上EIEM的通道数也会错运行时直接报维度不匹配。建议在写模型定义时打印每个模块的shape日志确认一遍。另外一个坑在训练稳定性上。EIEM里的深度可分离卷积虽然参数少但分组卷积在不同通道之间的特征交互很弱如果网络宽度不够边缘分支可能学不动。实际测试时当输入通道数低于128时EIEM的增益会明显下降。所以我把EIEM主要加在主干的中深层浅层只做了简单的1x1卷积投影避免过浅层算力浪费。4. 实验配置与效果对比4.1 数据集与训练设置我用来验证的是一套自建的某微型轴承表面缺陷数据集总共3460张图像包含六类缺陷划痕、崩边、麻点、压伤、脏污、水渍。其中小缺陷标注框面积占图像总面积小于1%占比达到64%整体难度偏向小目标检测。训练配置沿用YOLO26的默认参数批量大小16输入分辨率640x640优化器用带动量的随机梯度下降初始学习率0.01余弦退火衰减一共训练300轮提前停止机制打开。没做额外的预训练从零开始训练这样对比更公平也能看出模块本身带来的增益。对比版本一共四个基线YOLO26原版、YOLO26加通用坐标注意力、YOLO26加EIEM但不接入C3k2挂在主干之后、YOLO26融合C3k2_EIEM本次方案。所有版本都在同一份训练集上训练测试集固定保证可比性。4.2 关键指标对比模型版本平均精度均值0.5小目标平均精度0.5参数量帧率(640)基线YOLO26原版71.4%58.2%21.8M86YOLO26 坐标注意力72.9%61.0%22.4M82YOLO26 独立EIEM72.1%60.3%22.6M78YOLO26 C3k2_EIEM75.3%65.8%23.1M74最直观的结论是C3k2_EIEM版本让整体平均精度均值从71.4%涨到75.3%提升了3.9个百分点其中小目标的平均精度从58.2%涨到65.8%涨幅达到7.6个百分点。这个幅度在工业缺陷数据集上算是相当可观了。对比“独立EIEM”和“C3k2_EIEM”两个版本能看出融合位置的重要性。独立EIEM挂在主干输出之后只改了最后一层特征图仍然涨了点但远不如融合在C3k2内部的效果。这是因为主干中每一层C3k2都包含EIEM等于多个尺度的特征都获得了边缘和空间增强而不是只做一次“事后补救”。参数量方面C3k2_EIEM版本比基线多了1.3M参数换算下来约6%的涨幅。因为EIEM用了深度可分离卷积和坐标注意力的轻量设计参数量增加主要来自多套EIEM实例。帧率从86降到74降幅14%左右在工业端侧部署时还能接受考虑到小缺陷检测收益明显这个性价比是划算的。4.3 消融实验里看到的细节我做了一组分解消融单独保留边缘分支、单独保留坐标注意力分支、以及两个分支都保留结果很有意思。只保留边缘分支时平均精度均值提升约1.8个百分点但小目标类别的召回率提升明显尤其划痕类从68%涨到76%说明边缘分支确实在抓“轮廓型缺陷”上有效。只保留坐标注意力分支时平均精度均值提升约2.1个百分点掉头看效果更明显崩边和压伤类的定位误差明显减小。这两类缺陷的共同点是位置感强要么在边缘要么在特定区域空间注意力学到的位置先验帮了大忙。两个分支都保留时涨幅不是简单相加的3.9%说明特征互补性确实存在。边缘分支负责“找出可疑轮廓”空间分支负责“判断该区域是否常见缺陷位置”两者配合既减少了漏检也压住了一些背景纹理造成的误检。5. 训练与部署中的常见问题5.1 梯度震荡导致训练不稳定第一次在C3k2里接EIEM时训练到第40轮左右损失值出现明显的上下波动验证集上的平均精度均值徘徊不前偶尔还往下掉。排查后发现问题出在边缘分支的深度可分离卷积和批量归一化配合不当。深度可分离卷积每个通道独立做空间卷积通道间的响应差异很大如果批量归一化之前没有足够的样本统计量归一化尺度容易抖动导致梯度不稳定。解决办法是给EIEM内部的所有批量归一化层设置了较小的动量参数让运行统计量更新得更平滑。同时把学习率从0.01下调到0.008。调整后训练曲线平滑多了最后收敛到75.3%的精度均值。5.2 边缘特征过度强化导致误检增多融合权重参数虽然能自适应调节但训练初期默认权重1.0有时候还是偏激进。在数据集中背景纹理比较丰富的类别比如磨砂表面的脏污检测边缘分支会把磨砂纹理也当成边缘特征放大导致误检数量增加。建议在数据集中背景噪声大的场景下把EIEM的fusion_weight初始值改成0.5让边缘分支温和一点。也可以在损失函数权重上做一些平衡比如对背景类别增加难例挖掘的权重。我的经验是先跑一轮训练观察误检曲线如果误检随训练轮数攀升优先调小初始融合权重而不是改网络结构。5.3 部署阶段模型导出问题训练完做模型导出时EIEM模块里的split操作和坐标注意力分支用到的一些动态shape逻辑在导出ONNX或TensorRT时偶尔会报算子不支持。解决办法是导出前先跑一次固定shape的模拟输入确认所有中间张量形状与预设一致。坐标注意力分支里的AdaptiveAvgPool2d在导出时如果有动态shape需要关掉动态轴。我一般先用640x640的固定shape导出ONNX再转TensorRT的FP16版本这样算子兼容性最好。实际部署到某条产线工控机上时用的就是TensorRT FP16版本单张图像推理耗时大约13毫秒加上前后处理整条流水线能跑到每秒70帧以上满足实时检测要求。5.4 多尺度特征融合时的维度对齐C3k2_EIEM在配置文件中接入时需要确保后续PAN或FPN的输入通道数是预设值。我最初为了省显存把某层C3k2_EIEM的e参数从0.25改到0.2结果导致cv2输出的通道数变化下游拼接维度直接对不上报了维度错误。建议做通道改动时摸清YOLO26模型配置文件里模块传入通道的字段来源不要凭感觉改e参数改完一定要把模型结构可视化或者打印每层shape验证一遍。6. 当前限制与下一步改进方向C3k2_EIEM不是万灵药。在数据增强比较激进、图像分辨率波动较大的场景下边缘分支学到的东西可能不稳定。比如我用过一组包含大量旋转样本的数据集做测试准确率提升就不如在固定姿态的产线数据上明显因为旋转会改变缺陷边缘的方向分布需要更多样本来学出旋转不变性。另一个限制是EIEM模块本身没有引入任何跨尺度交互。小缺陷在多尺度特征图上表现不同目前的EIEM是对每个尺度的特征独立做增强没有显式把浅层高分辨率特征和深层语义特征关联起来。下一步我准备在EIEM里增加一个轻量的跨尺度交换分支把浅层的边缘响应引导到深层特征图上。还有就是目前EIEM只作用在主干网络检测头的回归分支没有参与增强。理论上如果检测头在回归时能直接参考边缘响应定位精度还能再提但改动会复杂不少涉及损失函数的改动我还在实验阶段。最后给想复现的朋友一个实操建议先在较小的数据集上做消融测试比如两三类的缺陷集确定EIEM分支在你的数据上确有增益再扩展到全类别训练。不要本末倒置用复杂模块去覆盖数据本身的问题工业检测场景里数据质量永远是最重要的底牌。这个改进版本我做出来的直接体会是边缘特征和空间信息其实一直是小目标检测的“隐藏杠杆”只是平时被深层语义特征掩盖了。EIEM做的事情很简单就是把这两根杠杆显式放到主干模块里让网络在提取特征时不至于忽略它们。花小成本换七八个点的小目标精度这笔账值得算。如果你手头也有小缺陷检测的难题可以照着这套思路在自己模型上试一试大概率能有意外收获。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑