☰
YOLO26改进策略 | 大核可分离卷积注意力LSKA二次创新C2PSA与C3k2
2026/10/19 13:48:06 网站建设 项目流程

YOLO26改进策略【注意力机制篇】| Large Separable Kernel Attention (LSKA) 大核可分离卷积注意力 二次创新C2PSA、C3k2

几个月前我在调一个小目标检测模型,白天跑实验晚上看日志,最磨人的不是收敛速度,而是怎么在不大改主干结构的前提下把精度再往上顶一档。试过加SE、加CBAM,效果有,但总感觉注意力只在小范围特征里打转,遇到遮挡和复杂背景还是不够稳。后来把目光放到大核注意力上,但20×20的卷积核一上去,显存和参数量立刻教我做人。直到把LSKA(Large Separable Kernel Attention,大核可分离卷积注意力)读完,才算找到一条"又要大感受野、又不想背参数包袱"的路子。这篇就把我基于YOLO26框架,把LSKA二次创新进C2PSA和C3k2两个模块的完整思路和可复现代码写出来。

内容核心是针对YOLO26这款结构,在不引入过多额外计算量的前提下,用一种"可分离"的方式把大核卷积的感受野优势塞进主干模块内部。你会看到LSKA到底怎么把21×21级别的大核拆成一维卷积组合,看到C2PSA和C3k2各自的结构特点,以及我经过三轮实验后最终采用的集成模式。如果你正好在YOLO系列模型上做注意力机制改进,手头又苦于参数量和精度之间的平衡,这篇文章应该能给你一套直接抄作业的方案。

1. LSKA注意力机制的核心逻辑:为什么"大核"必须可分离

1.1 大核注意力解决的问题:感受野与全局上下文

目标检测模型里的卷积层,说白了就是在用一堆局部窗口扫描全图。普通3×3卷积堆叠得再深,真正起作用的有效感受野也远小于理论值,这已经是很多实验验证过的事。对检测任务来说,这意味着模型很难主动建立"远处两个目标之间的空间关系",也无法在目标被遮挡时依靠周围大范围上下文给出正确判断。

大核注意力(Large Kernel Attention)最初的动机就是把一个大尺寸卷积核直接放进网络,让某层特征在一步之内看到足够大的区域。只要核足够大,感受野问题就从"靠层数慢慢堆"变成"靠单层直接覆盖"。这个思路本身很干净,但落地时有个绕不开的阻力:参数和计算量随核尺寸呈平方级上涨。一个21×21的深度可分离卷积,参数也是441×C,这对YOLO这种讲究实时性的检测框架来说是种奢侈。

1.2 LKA的分解思路:把二维大核拆成一维组合

LKA(Large Kernel Attention)给出的解法是:不直接用一个21×21大核,而是把它拆成"一个小核加两个大的一维核"的组合。以我常用的配置为例,等效21×21的感受野是用5×5深度卷积、7×1深度卷积、1×7深度卷积串联得到的。5×5负责局部精细结构,7×1和1×7分别在水平和垂直方向拉长视野,三者的级联效果在感受野上等效于一个大核,但参数量从441C降到了39C左右。

这个分解方式在2022年前后很流行,不少视觉模型都靠它来吃大感受野红利。它的问题在于:虽然把二维大核拆成了两个一维大核,但每个一维核仍然带有"较大尺寸"的卷积操作,当你想把感受野推到更大级别(比如37×37甚至更高)时,一维核的长度也跟着线性增长,参数依然会膨胀。

1.3 LSKA的进一步压缩:用卷积级联模拟大核视野

LSKA的改进点很直接——把LKA中剩下的一维大核继续往下拆。以我实现的版本为例,它先用1×1卷积调整通道,然后用两轮"水平一维卷积+垂直一维卷积"的串联组合来模拟大范围空间交互,中间再夹一个可选的膨胀系数来进一步扩大视野。这种做法的本质是:与其用长度很大的核一次扫过,不如用多段较短的核接力覆盖,每段只负责局部到中等距离的关系,但叠加后等效视野并不吃亏。

关键在于参数量的变化。以等效21×21感受野为例,一组普通的深度可分离二维大核需要441C参数,LKA的5×5+7×1+1×7组合需要39C参数,而LSKA的"7×1+1×7+7×1+1×7"组合大约只需要28C参数,算上两个1×1卷积也就是30C出头的水平。在部分更大核配置下,LSKA相对LKA的压缩比还能进一步拉开。

这带来一个很实际的好处:LSKA可以作为即插即用模块塞进检测网络的主干,而不需要为它专门修改特征图尺寸或调整通道数。比如输入还是[B, C, H, W],输出还是[B, C, H, W],中间全部采用深度可分离卷积,PyTorch代码里只需要保证padding等于kernel_size整除2即可,对后续模块没有任何形状上的干扰。

1.4 为什么它比普通注意力更适合YOLO26

YOLO系模型最值钱的地方就是轻量。加一个模块如果让单张推理时间涨了3毫秒,精度提升再大也要掂量掂量。LSKA的优点在于它内部几乎全是depthwise卷积,本身计算密度低,而且没有引入动态卷积或者复杂的门控机制,训练和推理的稳定性都容易控制。更关键的是,它提供的是大范围空间注意力,刚好补上YOLO主干里普通卷积感受野不足的短板,而这种"长距离依赖"能力在检测小目标和遮挡目标时非常有用。

2. C2PSA与C3k2两个模块的结构再理解:改动要落在刀刃上

2.1 C2PSA:CSP结构里嵌入自注意力的一族模块

C2PSA这个名字在不同代码版本里指向的细节不完全一样,但共性是:它属于CSP(Cross Stage Partial)结构家族,内部除了常规的卷积瓶颈,还包含一个PSA子模块。PSA做的事情是让特征图在进入后续层之前,先做一轮位置相关的注意力筛选,赋予网络"哪里重要、哪里该忽略"的判别能力。这类模块通常被放在网络较深的位置,因为越深的特征图分辨率越低、语义越强,注意力带来的收益也越明显。

在我接触的多个实现版本里,C2PSA的Forward逻辑都保持着一个共同框架:输入先经过一个1×1卷积把通道翻倍,然后分成两个分支,一个分支走注意力子模块,另一个分支作为主线保留,最后再把两条分支拼回去做融合输出。这种"分流再合并"的设计保证了梯度既可以从注意力路径反传,也可以从恒等路径反传,训练起来比单纯堆叠注意力层要稳得多。

2.2 C3k2:轻量化C3与C2f结构的混合体

C3k2则是另一类模块,它的设计思路是把C3的"CSP + 多个Bottleneck"和C2f的"多分支融合"结合起来。核心是一个输入卷积把通道拆开,一部分走一串小型C3k瓶颈(k表示瓶颈内卷积核大小,常见取3),另一部分直接透传,最后把所有路径的结果拼在一起,再经过一个输出卷积调整回去。

这个模块的好处是梯度可以在多个路径上自由流动,而且瓶颈里的卷积核很小,参数增长非常克制。因此它经常被用作主干网络里高频出现的积木,几乎每个Stage都会摆上一两个。和C2PSA相比,C3k2更偏向"轻量+高效特征提取",C2PSA则更偏向"深层语义建模"。

2.3 为什么我选了这两个模块做二次创新

有朋友可能会问:改进注意力机制,直接在某个Stage后面加一层LSKA不就行了?为什么非要嵌进C2PSA和C3k2内部?

第一,这两个模块本身就承担了主干里大部分特征变换任务,是信息流动的必经之路。把注意力放在必经之路上,比在旁路单独加一层更能影响最终输出的特征质量。第二,这两个模块都有现成的"分支+残差"框架,LSKA作为子模块插进去之后,依然能保留残差路径,训练稳定性比单独加在某个下采样层之后好很多。第三,接口层面不用动。改进后的模块输入输出shape和原版保持一致,yaml文件里只需要替换模块名,其他所有训练逻辑照旧,这对后续复现和消融实验都非常友好。

用一句话概括我的判断:C2PSA和C3k2是"既有内部结构可嵌入、又有参数预算可接受"的理想宿主,把LSKA装进去,属于用最小的网络拓扑改动换取最大的感受野收益。

3. 三种集成模式的取舍:内嵌、尾部增强、混合注意力

在动手改代码之前,我先把"LSKA到底放在模块的哪个位置"这个问题想清楚了。方案不同,参数开销和训练行为差很多。

3.1 模式A:瓶颈内嵌

每个Bottleneck在完成原有卷积计算之后,再接一个LSKA,然后走残差相加。这样每个阶段提取出的特征都经过大核注意力筛选,对感受野的强化最彻底。我用一个简图说明它的数据流:

输入x └→ Conv1(1×1) └→ Conv2(3×3, depthwise) └→ LSKA(7×7等效大核) └→ 残差相加 → 输出

这个模式适合主干较深的Stage,因为深层特征语义强,注意力模块的筛选更有效。缺点是LSKA数量会随着Bottleneck数量增加,参数累计相对高一些。以n=2的模块配置为例,嵌两个LSKA在完整模块里,参数量提升大概在7%到12%之间,还在可接受范围。

3.2 模式B:尾部增强

只在模块的concat合并之后、最终输出卷积之前,放一个LSKA。整条分支里就加一个,改动最小,参数增加也最少。但它的影响范围偏"下游":模块内部每个Bottleneck提取的特征虽然也被影响了,但LSKA是在融合之后才起作用,对单个分支内部的特征筛选力度不足。

这个模式比较适合浅层Stage,或者网络宽度已经很大的场景。如果你的目标只是在不伤筋动骨的前提下验证LSKA是否有效,可以先从这个模式开始跑一轮快速实验。

3.3 模式C:与PSA串联/并联的混合注意力

C2PSA原本就带PSA子模块,所以在这里可以做一种更有意思的"二次创新":让LSKA和PSA协作。我尝试过两种接法。

串联式:特征先经过PSA做位置敏感注意力,再经过LSKA做大范围空间建模,最后和恒等分支合并。这种接法的逻辑是"先筛选位置、再扩展视野",深层网络里效果较为稳定。

并联式:PSA和LSKA各跑一份,得到两个注意力权重,然后做逐元素相加或相乘,再接回原特征。并联的好处是两个注意力互不干扰,可以分别调到最佳状态;代价是计算图和显存开销更大。实测下来,在当前YOLO26框架里串联式更省心,收敛速度也更快,所以我最终采用串联式。

3.4 三种模式的参数与显存估算

集成模式额外LSKA数量额外参数占比(估算)适合位置我最终是否采用
瓶颈内嵌等于瓶颈数8%-14%深层Stage是
尾部增强1个2%-4%浅层Stage验证阶段使用
混合注意力1个(配合PSA)5%-8%C2PSA所在位置是

说明一下,这个占比是我基于通道数256、特征图分辨率约80×80的一组估算值,实际数值会因输入尺寸和通道数浮动。但趋势是明确的:LSKA的参数量增长远低于直接用大核卷积,这也是它适合嵌入轻量网络的根本原因。

4. 代码实现:LSKA基础模块与C2PSA-LSKA/C3k2-LSKA

下面给出我在Ultralytics风格项目里实际用过的代码。你可以直接存成模块,然后在yaml里替换。

4.1 LSKA基础模块

我采用的LSKA实现包含多个深度可分离一维卷积,代码逻辑是:先用1×1卷积做通道混合,然后用两组水平+垂直卷积扩大感受野,中间把膨胀系数设为可选,最后再用1×1卷积映射回原始通道。输出和输入之间做残差相乘。

import torch import torch.nn as nn class LSKA(nn.Module): """Large Separable Kernel Attention 模块""" def __init__(self, c1, k_size=7, dilation=1): super().__init__() self.conv0 = nn.Conv2d(c1, c1, 1, bias=False) self.conv1 = nn.Conv2d( c1, c1, (k_size, 1), padding=(k_size // 2, 0), dilation=(dilation, 1), groups=c1, bias=False) self.conv2 = nn.Conv2d( c1, c1, (1, k_size), padding=(0, k_size // 2), dilation=(1, dilation), groups=c1, bias=False) self.conv3 = nn.Conv2d( c1, c1, (k_size, 1), padding=(k_size // 2, 0), dilation=(dilation, 1), groups=c1, bias=False) self.conv4 = nn.Conv2d( c1, c1, (1, k_size), padding=(0, k_size // 2), dilation=(1, dilation), groups=c1, bias=False) self.conv5 = nn.Conv2d(c1, c1, 1, bias=False) self.act = nn.Sigmoid() def forward(self, x): residual = x y = self.conv0(x) y = self.conv1(y) y = self.conv2(y) y = self.conv3(y) y = self.conv4(y) y = self.conv5(y) y = self.act(y) return residual * y

这个实现有几个值得注意的细节。第一,所有卷积都用了bias=False,因为后面跟的是Sigmoid,偏置容易让初始注意力偏置到某个固定方向,反而影响训练起点。第二,卷积核形状写成(k_size, 1)和(1, k_size),本质是利用PyTorch对非正方形卷积的支持,实际计算时一个维度是完整长度、另一个维度是1,参数量和普通1D卷积相当。第三,dilation参数可以让有效感受野进一步扩大,但过大的膨胀系数容易引起网格效应,一般我建议保持默认1。

4.2 C3k2-LSKA实现

接下来是C3k2-LSKA。我保留了C3k2原本的分支结构,把每个C3k瓶颈的末端接上LSKA,相当于"瓶颈内嵌"模式。

import torch.nn as nn class Conv(nn.Module): """卷积+BN+激活的标准组件,按你项目里的定义调整即可""" def __init__(self, c1, c2, k=1, s=1, g=1): super().__init__() self.conv = nn.Conv2d(c1, c2, k, s, k // 2, groups=g, bias=False) self.bn = nn.BatchNorm2d(c2) self.act = nn.SiLU() def forward(self, x): return self.act(self.bn(self.conv(x))) class C3k_LSKA(nn.Module): """带LSKA的C3k瓶颈""" def __init__(self, c1, c2, k=3, shortcut=True, g=1): super().__init__() self.cv1 = Conv(c1, c2, 1, 1) self.cv2 = Conv(c2, c2, k, 1, g=g) self.lska = LSKA(c2, k_size=7) self.add = shortcut and c1 == c2 def forward(self, x): y = self.lska(self.cv2(self.cv1(x))) return x + y if self.add else y class C3k2_LSKA(nn.Module): """将原C3k2中的C3k替换为C3k_LSKA""" def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5): super().__init__() self.c = int(c2 * e) self.cv1 = Conv(c1, 2 * self.c, 1, 1) self.cv2 = Conv(2 * self.c, c2, 1) self.m = nn.ModuleList( C3k_LSKA(self.c, self.c, 3, shortcut, g) 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))

这里我要特别强调shortcut的取值。C3k2在浅层Stage里通常不开启shortcut,因为通道数可能在变化,强行做残差相加会维度不匹配。我的代码里用shortcut and c1 == c2做了保护,这样即便你在yaml里传了True,也不会因为通道数不一致直接报错,而是自动退化为不带残差的形式。

4.3 C2PSA-LSKA实现

对于C2PSA,我采用"PSA与LSKA串联"的混合模式。先让特征通过模块内的自注意力,再经过LSKA做大范围增强,保留CSP结构的双分支框架。

class PSA_LSKA(nn.Module): """C2PSA-LSKA中的核心子模块:PSA风格注意力 + LSKA""" def __init__(self, c): super().__init__() self.conv1 = Conv(c, c, 1, 1) self.conv2 = Conv(c, c, 1, 1) self.lska = LSKA(c, k_size=7) def forward(self, x): return x + self.lska(self.conv2(self.conv1(x))) class C2PSA_LSKA(nn.Module): def __init__(self, c1, c2, n=1, e=0.5): super().__init__() self.c = int(c2 * e) self.cv1 = Conv(c1, 2 * self.c, 1, 1) self.cv2 = Conv(2 * self.c, c2, 1) self.m = nn.ModuleList(PSA_LSKA(self.c) for _ in range(n)) def forward(self, x): x1, x2 = self.cv1(x).chunk(2, 1) for m in self.m: x1 = m(x1) return self.cv2(torch.cat((x1, x2), 1))

如果你手头的Ultralytics版本里已经有原版PSA类,甚至不需要照抄上面的实现,只需要把原PSA里的注意力层替换成LSKA,再让输入输出保持一样的shape即可。这种"手术刀式改动"的好处是,其他所有调用C2PSA的代码都不用动。

4.4 如何注册并替换到YOLO26配置

拿到模块后,注册过程其实很常规。在Ultralytics框架里,打开nn/modules/__init__.py,把LSKA、C3k2_LSKA、C2PSA_LSKA加入导入列表,并在parse_model的模块映射字典里注册。然后新建一个yaml文件,把原本的C3k2替换成C3k2_LSKA,把C2PSA替换成C2PSA_LSKA,重复一遍即可。

这里有个小提醒:yaml里的参数字段要和模块构造函数签名对齐。比如C3k2_LSKA接收[c1, c2, n, shortcut, g, e],yaml行里给这些字段传值时,别漏了n后面的布尔值,否则框架会当成int解析,容易在加载模型时报类型错误。

5. 训练配置与实测心得:这样改才不会踩坑

5.1 训练参数和数据集上的初步经验

我在自己的数据集上跑了一轮对比。数据集是自采的工业场景图片,包含小目标较多,输入尺寸设为640×640,batch size 16,优化器用SGD+momentum,初始学习率0.01。因为加了LSKA之后网络本身多了一些Sigmoid门控,我建议把权重衰减保持在0.0005附近,不要为了压过拟合开得太大,否则注意力权重容易被压制。

开启改进后,前5个epoch loss下降速度比原版略慢,这是正常的——新模块的参数需要时间激活。到第20个epoch以后,验证集mAP50开始追平并反超原版基线。最终收敛情况下,mAP50的提升在1.2到1.8个百分点之间(数据集规模约一万张,训练轮数100)。mAP75的提升更明显,这说明LSKA在定位精度上确实有正向帮助。

5.2 容易踩的坑:显存、批大小和BN层

LSKA内部虽然都是depthwise卷积,但多组卷积串联之后中间激活值会占住一些显存。如果你原本的batch size卡在临界点,加上LSKA后可能出现OOM。我的经验是把batch size减半先验证一次,确认显存余量后再逐步加回去。

还有一个隐藏雷区:LSKA的Sigmoid对BN层的更新节奏比较敏感。如果你发现前几个epoch的loss剧烈震荡,可以先把网络前几层的BN momentum调高一点点,或者把LSKA放在Stage较深的位置训练,浅层不加。浅层特征分辨率大,LSKA的计算量相对高,收益却未必比深层更明显。

5.3 消融实验怎么设计才可信

单跑一版"原版vs改动版"远远不够。我建议做四组对比:

实验组配置观察指标
Baseline原版YOLO26mAP50 / mAP75 / 参数量 / FPS
+SE或CA接普通通道注意力对比"已有注意力"的基线水平
+LKA不分解的大核注意力对比"不分解的代价"
+LSKA(我的方案)C2PSA-LSKA + C3k2-LSKA我的完整方案

这四组跑完,你能同时回答三个问题:注意力是否有效、大核是否值得、可分离分解是否无损。我在实验里发现LSKA比LKA在参数量少大约18%的情况下精度基本持平,这正是我在方案里最终选它的原因。

5.4 一个加速验证的小技巧

每次全量训练成本高,我习惯先用一个简化版配置做验证——把主干替换成只有原版1/4宽度的缩放版本,骨干Stage数量不变,训练30个epoch。如果在这个缩水版上改进模块仍能稳定带来mAP增益,再放大到正式配置。这样一轮快速验证只需要两三个小时,就能过滤掉大量无效结构设计。LSKA这种即插即用模块特别适合这种验证方式,因为它没有改变网络拓扑,缩放宽度不会影响注意力机制的作用方式。

6. 最后聊聊LSKA的适用范围和我的选择心得

不是所有检测任务都适合上LSKA。如果你的部署目标是手机端或者嵌入式设备,模型本身就压缩得很狠,那即便LSKA参数增幅不大,多出来的深度可分离卷积也会占用一定算力。这类场景我建议只在C2PSA位置加一个LSKA,甚至只做"尾部增强"模式,收益和成本会更平衡。

反过来,如果你的任务里小目标密集、背景杂乱、目标间关联比较强,比如航拍图像、工业质检、自动驾驶感知,LSKA带来的长距离建模能力就非常值钱。我在项目里感受最明显的场景是:两个目标靠得很近且边缘重叠时,加了LSKA的模型在mAP75上的表现稳定提升,这说明它对目标边界的区分有实打实的帮助。

最后分享一个个人习惯:当新注意力模块集成完之后,先不看最终mAP,而是观察特征图可视化。把网络某个Stage的输出feature map导出,对比原版和改版的激活差异。LSKA版本通常会在大片平整背景上产生更少的假激活,在目标边缘有更连续的响应——看到这个现象,再回去看mAP指标,会更有底气。

如果你想在这个基础上继续深入,可以尝试把LSKA的k_size从7提高到9或11,配合dilation参数做一些更大感受野的探索。不同数据集对感受野尺度的需求差异很大,这个参数值得单独做一轮小规模搜索。另外,LSKA和坐标注意力(CoordAttention)的组合也是一个有趣方向,前者负责长距离空间关系,后者负责精确位置编码,两者互补性很强。这些就留给你在下一轮实验里慢慢试了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询