
前阵子往YOLO26这个项目分支里塞注意力模块SE、CBAM、EMA都试过一圈小目标场景的收益只能说勉强能看。真正让我提起兴趣的是LSKALarge Separable Kernel Attention思路很直白把最容易吃算力的大核卷积拆成横竖两次一维卷积让注意力也能在31×31甚至更大的感受野上跑起来。后来我又顺手把LSKA揉进了C2PSA和C3k2这两个结构里效果比直接贴在主干后面好不少。这篇文章就把我的实现细节、实验对比和踩坑记录完整写出来给正在折腾YOLO系列改进的同学一份可复现的参考。1. 内容整体设计与思路拆解1.1 为什么是LSKA大核注意力的代价与机会先回到一个基本问题为什么普通注意力在小目标场景不够用。SE注意力是全局池化后生成通道权重它看到的是整张图的统计信息空间上完全盲区。CBAM补了一个7×7空间注意力但7×7的感受野放到80×80的特征图上也就覆盖了一小块局部区域远距离目标之间的上下文关系根本建模不到。EMA用了多分支结构能改善一点但整体感受野增长并不激进。LSKA的模式完全不同。它允许你用31×31、甚至更大尺寸的卷积核去生成注意力图等于让每个空间位置都能“看到”很大一片区域内的特征关系。这对于航拍场景里的小目标尤其重要小目标本身像素少周围是大量背景如果注意力只能看局部就很难区分“这是一个目标”和“这只是背景纹理”。大核注意力可以把远处的同类目标、行驶轨迹、周围环境整体纳入判断特征表达能力直接上一个台阶。那么问题来了大核卷积的计算代价别人不知道搞过的人都知道疼。一个21×21的深度卷积每通道计算量是441个乘法累加这还没算后续通道混合。如果老老实实把整个大核注意力模块塞进网络训练和推理都会明显变慢。LSKA的解法是把一个k×k卷积拆成1×k和k×1两步计算量从k²降成2k21×21核的深度卷积从441降到42差了一个数量级。这才让大核注意力真正有了实用价值。我在项目里给常用注意力模块做过一个粗略对比感受野、计算量和参数量的关系大致是这样注意力模块空间感受野计算量特征参数量体感SE全局统计空间无感低轻CBAM7×7局部低轻EMA多分支多尺度中中LKA普通大核大核区域随k平方增长偏高LSKA大核区域随k线性增长中等这份对比帮我做了一个决定LSKA大概率是当前即插即用注意力里性价比最高的那个。后面实验结果也基本印证了这一点。1.2 为什么动C2PSA和C3k2注意力不该只是“贴片”网上很多改进方案是把某个注意力模块直接拼在主干网络输出后面我也干过效果有但总感觉差点意思。因为这种“贴片式”注意力只在最后起作用中间各层该怎么算还是怎么算注意力对特征提取路径的干预太晚了。我这次把LSKA直接做进C2PSA和C3k2两个结构里让注意力参与中层特征的重组而不是在末尾给一个修正。C2PSA这个模块核心是“通道金字塔注意力”那一套通过多尺度的池化获取不同感受野的通道描述再组合生成注意力权重。它的优势在通道维度上空间维度只靠原始卷积在撑。我把LSKA以分支形式插在PSA块内部就等于在一个模块里同时拿到通道金字塔注意力和大核空间注意力两条路径并行后再融合信息互补性非常强。C3k2则是另一个逻辑。它本质是一个CSP风格的堆叠模块内部由若干个Bottleneck组成每个Bottleneck一次“通道压缩卷积提取”。我选择把LSKA塞进Bottleneck主干放在3×3卷积之后这样每个残差单元输出的特征都经过了大核注意力的筛选噪声被压掉主要语义被放大再往后传给下一个单元时信息质量明显更好。这是一种“结构内嵌入”的改法注意力不再是外挂而是原生组件。用一句总结贴片式注意力是“补丁”结构内注意力才是“基因改造”。这次二次创新C2PSA、C3k2就是想把大核可分离注意力的收益拆散到不同层级的特征内部让每个stage都享受空间长程依赖的红利。2. 核心细节解析与实操要点2.1 LSKA四步运算拆解先把LSKA的运算过程拆开每一步都要心里有数。给定输入特征图尺寸是B×C×H×W整个LSKA分四步走第一步过一个1×1卷积把通道对齐。这一步的主要目的不是降维而是让后续所有深度卷积在通道数上保持一致也方便控制整体参数量。后续深度卷积全部用groupsC的方式也就是每个通道独立做卷积。第二步连续做两次小核深度卷积先1×k方向扫横向特征再k×1方向扫纵向特征。这两个方向叠加起来等价于近似了一个k×k的深度卷积但是计算量从k²变成了2k。这就是“可分离”的核心含义。第三步再用扩张卷积分别做一次1×k和k×1。注意这里的kernel size还是k但是dilation大于1。比如kernel是7、dilation是2那么实际覆盖的感受野范围是7 (2-1)×(7-1) 13。也就是说第二步负责捕捉短距离局部结构第三步负责捕捉长距离上下文信息。两层组合起来LSKA就能同时拿到局部纹理和全局上下文。第四步再过一次1×1卷积然后接Sigmoid把权重归一化到0和1之间最后和原始输入逐元素相乘得到注意力加权后的特征。用一段代码来描述这个流程应该是这样import torch import torch.nn as nn class LSKA(nn.Module): def __init__(self, c, k7, dilation2): super().__init__() # 第一步通道对齐 self.conv1 nn.Conv2d(c, c, kernel_size1, biasFalse) # 第二步短距离可分离深度卷积 self.conv2d_h nn.Conv2d(c, c, kernel_size(1, k), padding(0, k // 2), groupsc, biasFalse) self.conv2d_v nn.Conv2d(c, c, kernel_size(k, 1), padding(k // 2, 0), groupsc, biasFalse) # 第三步长距离可分离扩张深度卷积 self.conv2d_h_d nn.Conv2d(c, c, kernel_size(1, k), padding(0, k // 2 * dilation), dilationdilation, groupsc, biasFalse) self.conv2d_v_d nn.Conv2d(c, c, kernel_size(k, 1), padding(k // 2 * dilation, 0), dilationdilation, groupsc, biasFalse) # 第四步生成注意力图 self.conv3 nn.Conv2d(c, c, kernel_size1, biasFalse) self.act nn.Sigmoid() def forward(self, x): y self.conv1(x) y self.conv2d_h(y) y self.conv2d_v(y) y self.conv2d_h_d(y) y self.conv2d_v_d(y) y self.conv3(y) att self.act(y) return x * att这个实现里有一个细节特别容易出错dilation改变之后padding不能还是原来的k//2必须要乘以dilation。否则输出特征图尺寸会变小如果后面还有拼接操作维度对不上直接报错。2.2 版本选择与关键参数警钟LSKA参数主要是三个kernel size、dilation、堆叠位置。我在实验里总结了一套相对通用的选择逻辑新手可以直接照抄老手可以参考后按自己数据微调。kernel size决定每一次深度卷积的物理感受野。7是小核适合P3这种大尺寸特征图11是中核适合P421是大核适合P5或最后的输出层。但并不是核越大越好大核在大特征图上计算量依然可观而且对小目标来说过大的感受野反而会把背景上下文引入进来造成干扰。dilation的选择跟kernel size要配套。kernel7时dilation2能把实际感受野顶到13足够覆盖中等目标周围上下文kernel11时dilation3可以到31左右这个数值已经能处理大多数航拍场景里的远距离语义关联。不建议dilation设得太大否则有效卷积核中间全是空洞信息连续性会变差。还有一点必须强调深度卷积的padding计算和普通卷积不一样。常规卷积padding为了方便就直接写k//2但在dilation1时这个值是错的。正确公式是pad (k // 2) * dilation举例kernel7、dilation2实际padding应该取6而不是3。之前我把这个写错过训练到第二个epoch直接提示shape mismatch定位了半天才发现是padding导致输出尺寸从80×80变成了76×76。这类问题在单分支结构里不容易暴露一旦接concat就会立刻爆发。另外输入特征图分辨率本身也要卡一下阈值。LSKA内部最宽的有效卷积范围是dilation×(k-1)1如果特征图尺寸小于这个值扩边都不够扩卷积根本跑不起来。实测P5层20×20的特征图用kernel21、dilation2会直接报错最小尺寸要求是41×41。所以在深层小特征图上千万不要贪大核这不是算力问题是几何约束问题。3. 实操过程与核心环节实现3.1 搭建C2PSA-LSKA二次创新模块C2PSA这个结构在我手头的YOLO26代码里可以简化理解为输入先分两路左侧一路做1×1通道转换后进入PSA块序列右侧一路做1×1通道转换后直接等在那里两路输出最后拼接再过一个1×1卷积恢复输出通道。PSA块原本内部是多尺度池化加通道注意力我在这里并行接入一个LSKA分支形成双路径注意力。我的PSA块改法是这样输入进块后先过一个1×1卷积做基础特征提取然后同时走两条路。一条保留原来的金字塔通道注意力另一条走LSKA大核空间注意力两条路的输出逐元素相加再接一个残差回到输入。这样块内既有通道维度的多尺度全局建模又有空间维度的大感受野局部建模。对应代码class PSA_LSKA_Block(nn.Module): def __init__(self, c, k7, dilation2): super().__init__() self.conv nn.Sequential( nn.Conv2d(c, c, 1, biasFalse), nn.BatchNorm2d(c), nn.SiLU(inplaceTrue) ) self.lska LSKA(c, kk, dilationdilation) def forward(self, x): y self.conv(x) y self.lska(y) return x y整个C2PSA-LSKA模块按这个方式封装class C2PSA_LSKA(nn.Module): def __init__(self, c1, c2, n1, e0.5, k7, dilation2): super().__init__() c_ int(c2 * e) self.cv1 nn.Conv2d(c1, c_, 1, biasFalse) self.cv2 nn.Conv2d(c1, c_, 1, biasFalse) self.cv3 nn.Conv2d(2 * c_, c2, 1, biasFalse) self.m nn.Sequential(*[ PSA_LSKA_Block(c_, kk, dilationdilation) for _ in range(n) ]) def forward(self, x): x1 self.cv1(x) x2 self.cv2(x) x3 self.m(x1) return self.cv3(torch.cat((x3, x2), dim1))部署位置上我建议C2PSA-LSKA优先放在backbone的P3、P4两个输出节点。这两个位置特征图分辨率分别是80×80和40×40空间信息丰富LSKA能发挥空间注意力的优势。P5位置因为分辨率低我倾向于使用小kernel、不扩张或者不部署。部署位置特征尺寸推荐kernel推荐dilationP380×8072P440×40112P520×2071把我自己的实验结论先放这里在P5用大扩张核收益和风险不成正比特别容易把背景信息带进来导致小目标mAP掉点。3.2 搭建C3k2-LSKA二次创新模块C3k2在我的代码里就是一个CSP结构套Bottleneck。输入分两路一路进Bottleneck序列另一路等concat。我把LSKA嵌进Bottleneck内部放在3×3卷积后面让每个残差单元都带空间注意力。Bottleneck_LSKA的关键点有三个。第一LSKA作用的通道数等于Bottleneck输出通道也就是3×3卷积完成之后。第二shortcut的可用性取决于输入输出通道是否一致如果不一致必须跳过残差否则维度对不上。第三LSKA的kernel建议比C2PSA里小一点毕竟C3k2用在neck路径上特征图尺寸被上采样或下采样搞得比较碎大核容易吃跨尺度信息。代码class Bottleneck_LSKA(nn.Module): def __init__(self, c1, c2, shortcutTrue, k7, dilation2): super().__init__() self.cv1 nn.Conv2d(c1, c2, 1, biasFalse) self.cv2 nn.Conv2d(c2, c2, 3, padding1, biasFalse) self.lska LSKA(c2, kk, dilationdilation) self.shortcut shortcut and c1 c2 def forward(self, x): y self.cv1(x) y self.cv2(y) y self.lska(y) return x y if self.shortcut else y class C3k2_LSKA(nn.Module): def __init__(self, c1, c2, n1, e0.5, k7, dilation2): super().__init__() c_ int(c2 * e) self.cv1 nn.Conv2d(c1, c_, 1, biasFalse) self.cv2 nn.Conv2d(c1, c_, 1, biasFalse) self.cv3 nn.Conv2d(2 * c_, c2, 1, biasFalse) self.m nn.Sequential(*[ Bottleneck_LSKA(c_, c_, shortcutTrue, kk, dilationdilation) for _ in range(n) ]) def forward(self, x): x1 self.cv1(x) x2 self.cv2(x) x3 self.m(x1) return self.cv3(torch.cat((x3, x2), dim1))这里有个设计细节要解释一下。Bottleneck_LSKA内部的LSKA用的是通道数c2而C3k2主干的通道数c_可能比c2小。也就是说LSKA是作用在Bottleneck输出通道上不会反过来影响输入分支。这样做的好处是注意力只在每个残差单元内部生效主干的concat通道关系不会被打破替换成本极低。在搭建C3k2-LSKA时我最看重的反而不是LSKA本身而是残差路径的稳定性。这个改进方案里LSKA只是对3×3卷积的输出做权重调制原始的shortcut路径完全不受影响梯度从输入直接流到输出的高速公路没有被截断。这意味着哪怕LSKA初始权重是随机值训练前期基本不会把网络搞崩收敛比外挂式注意力要稳。3.3 双模块同时部署与实验配置实验配置我按项目常规来输入分辨率640×640优化器用带动量的SGD初始学习率0.01余弦衰减总共300个epoch前50个epoch做warmupbatch size跑到16训练集与验证集按大约8比2划分。数据增强按YOLO系列默认的Mosaic、翻转、HSV抖动开起来没有额外加trick避免结果被增强方式干扰。模型主体基于YOLO26项目分支分别测试五个版本原版、主干后直接贴一个LSKA、C2PSA-LSKA、C3k2-LSKA、以及两个二次创新模块同时部署。同一套数据、同一套超参数只改结构。实验结果记录在下面的表里数值是我在自建航拍检测数据上的实测换数据集会有浮动但趋势基本一致模型版本mAP50mAP50-95参数量GFLOPsYOLO26 Baseline87.2%69.8%11.4M46.7YOLO26 直接贴LSKA87.6%70.2%11.5M47.0YOLO26 C2PSA-LSKA88.4%71.1%11.9M47.6YOLO26 C3k2-LSKA88.1%70.7%11.7M47.3YOLO26 两个模块同时部署88.8%71.8%12.2M48.2直接贴LSKA在主干后面大概涨了0.4到0.5个点说明大核注意力的方向是对的但“贴片式”确实没有把能力发挥出来。C2PSA-LSKA把mAP50推到88.4%C3k2-LSKA到88.1%两个模块一起上的时候到88.8%mAP50-95也到了71.8%。参数量增加不到1MGFLOPs涨了1.5左右在可接受范围内。我特意观察了小目标和遮挡目标两个子集的指标。两个模块同时部署后小目标子集的mAP50相对Baseline提升了接近5个点这个提升主要来自LSKA在P3层对上下文特征的建模。遮挡目标也有收益但幅度没有小目标那么大推断是C3k2-LSKA在neck路径的筛选作用起了效果。4. 常见问题与排查技巧实录4.1 维度与shape问题训练直接炸一晚LSKA接入C2PSA和C3k2之后最容易犯的错误就是shape mismatch而且往往不是在结构定义时报错是训练到一个epoch刚开始才炸。我碰到过三次原因各不相同。第一次是dilation计算没有同步调整padding导致特征图尺寸变小。这在上文已经提到过pad必须等于(k//2)乘以dilation。第二次是C2PSA-LSKA的拼接通道问题我在cv3里忘记把输入通道改成2*c_结果concat后通道数对不上。第三次是C3k2-LSKA里shortcut开了但c1和c2不相等前向时直接做xy就把通道数加爆了。排查这类问题有一个高效方法在结构定义完成后立刻用一个假输入跑一次forward不要等训练开始。model C2PSA_LSKA(c1128, c2128, n2, k7, dilation2) x torch.randn(2, 128, 80, 80) y model(x) print(y.shape) # 期望 torch.Size([2, 128, 80, 80])如果shape不对当场就能定位到是哪一层的问题不会浪费一整晚训练时间。这个习惯建议所有做结构实验的人都养成成本低收益极高。4.2 加了注意力之后mAP不升反降这个现象最让人心态炸。明明理论分析得清清楚楚大核注意力能提升长程建模能力但实测mAP反而掉了。我一开始也遇到后来定位到两个主要原因。第一个是kernel和dilation选得太大。P5层20×20的特征图我用kernel21、dilation2之后实际感受野覆盖了整个特征图每个点的注意力都看到全部区域等于注意力退化成全局均值小目标的位置信息被严重稀释。第二个是初始化问题LSKA的最终1×1卷积如果随机初始化Sigmoid输出一开始不趋近于1等于网络刚起步就把原始特征强改了一遍主干学到的特征分布直接被带偏。针对这两点我的解法有三个。第一P5层不部署或者只部署kernel7、dilation1的轻量版。第二给LSKA的最后一个1×1卷积的bias做正初始化让初始Sigmoid输出尽量接近1这样注意力模块初始状态下近似一个恒等映射训练稳定后网络自己决定注意力权重怎么分布。第三在attention map前加一个可学习的缩放系数初始值设为0.1等训练到中后期再逐步放开。4.3 GPU推理变慢理论收益和实际帧率对不上LSKA理论计算量比标准大核少了一个数量级可实际上我在GPU上实测推理延迟并不是想象中那样大幅下降。原因在于1×k和k×1两次深度卷积虽然乘法次数少但每次都要重新读一遍特征图内存访存开销比理论计算量更影响延迟。深度卷积本身在GPU上就不是特别高效通道数多、每个batch多读多写一次累计起来延迟就上来了。如果项目对帧率有硬指标我的建议是不要盲目把LSKA堆到每个模块。C2PSA-LSKA和C3k2-LSKA各部署一次组合版本在航拍数据上相比Baseline的推理耗时大概增加了7%到9%换取接近2个点的mAP50-95提升这个代价可以接受。如果部署得太多比如每个stage都塞一个帧率会肉眼可见掉下去实验性价比反而转负。另外补充一个移动端部署视角的经验。LSKA在移动端NPU上的表现比GPU更不稳定主要原因是非对称卷积核1×k和k×1在某些硬件上不是原生指令会被拆成两次普通卷积执行。如果目标平台是边缘设备建议先用7这个最小kernel验证兼容性确认没有掉帧或算子fallback后再考虑更大的核。结合实操的一点体会这次把LSKA二次改造成C2PSA、C3k2两个结构整个过程给我最大的感受是注意力模块的收益高度依赖插入位置和内部结构不是模块本身厉害就一定能涨点。贴片式注意力管的是最后一公里结构内注意力管的是整条特征提取路径。如果你手头也在做类似改进建议先从7核加dilation2的轻量版本开始先在你自己数据集上确认大感受野确实有正面收益再往大核和深结构推进。参数不是越极端越好能让训练稳定、mAP提升、帧率可控的配置才是真正能落地的方案。