别只写注意力了,这个来自 GitHub 的 LSK Block 更适合讲“多尺度选择”
开场昨天那篇我写的是WTConv方向是“更大的感受野怎么用 wavelet 解决”。今天我继续沿着 GitHub 即插即用模块这条线往下挖又挑了一个更适合讲“多尺度选择”的模块LSK Block来自LSKNet官方 GitHub[zcablii/LSKNet(https://github.com/zcablii/LSKNet)论文Large Selective Kernel NetworkarXivhttps://arxiv.org/abs/2303.09030我这次选它不是因为它又是一个“注意力名字变体”而是因为它在工程上有个很直接的吸引力它不是推翻 CNN而是在 CNN 里增强大核建模能力它保留卷积结构但不再死用一个固定感受野它有一种很适合写文章的直觉不同位置需要的卷积尺度并不一样。换句话说LSK Block最适合拿来讲的点不是“它又涨了多少指标”而是为什么一个模块可以在同一张特征图里动态偏向不同大小的感受野。这篇我会直接按下面这个思路写它想解决什么问题官方 GitHub 代码怎么用模块内部到底是怎么分支和选择的关键几段代码怎么读你如果自己要插最自然该插到哪里1. LSK Block 想解决什么问题很多 CNN 模块都有一个默认假设同一层里的所有位置使用同一种卷积感受野就够了。但实际并不是这样。举个最简单的图像直觉边缘细节可能更适合相对小一点的局部感受野大目标、长条结构、宽区域语义可能更适合更大的感受野如果你始终只给它一个固定大小的卷积核那模型的表达其实是有点死的。LSK Block的核心思路就是先做两个不同感受野的 depthwise 分支再让模块自己根据当前空间位置去决定更偏向哪一个分支所以它和很多“直接做个大核卷积”不太一样。它不是简单把 kernel 一路做大而是保留多尺度分支再做选择。这也是它名字里Selective Kernel的来源。2. 官方 GitHub 上这类模块为什么值得写很多 GitHub 模块其实不适合写文章主要问题无非这几个仓库写法太乱代码一看就不直观模块虽然新但不好解释读者看完依然不知道能往哪里接LSK Block相对就好很多因为它的文章可读性很高来源明确官方 GitHub 原始论文结构直观两条卷积分支 选择门控用途明确增强 CNN 的多尺度建模图非常好画流程图、分支选择图、门控偏好图都能说明白它特别适合写成一篇“我看懂了 GitHub 上一个模块并且知道怎么往自己网络里塞”的文章而不只是停留在论文摘要复述。3. 先看图LSK Block 在模块内部做了什么你可以把这张图粗暴理解成四步输入特征图走两条不同感受野的 depthwise 分支做空间选择再把结果融合回输出这里最关键的不是“分了两支”而是不是所有位置都平均对待而是后面会有一个 gate 去决定该信哪一支。4. 为什么这个模块比“单纯大核卷积”更有意思如果只是为了扩大感受野最朴素的方法当然是把卷积核变大。但这么做有两个问题大核未必适合所有位置局部细节有时反而会被更大范围的模式冲淡LSK Block更像是在说小一些的感受野分支保留局部模式更大的感受野分支负责更宽的空间关系最后用一个空间门控去决定当前区域更适合哪种信息这就使它不只是“大核卷积增强版”而是带位置感知选择能力的大核建模模块。5. 再看一张图为什么多尺度选择是有必要的为了把这个问题讲直观我专门做了一张 toy 示例图。这张图可以这样看输入图里同时存在局部亮点和更大范围的扩散结构小核分支更容易保住局部轮廓大核分支更容易看到更宽的整体区域最终融合结果不是机械平均而是更像“按位置偏好选择”这张图特别适合放在“为什么要做 selective kernel”这一节后面因为它能把“固定卷积核不够灵活”这件事讲得很顺。6. 官方 GitHub 这类模块一般怎么接这类模块最自然的接法一般不是独立拿出来当一个完整 backbone而是作为一个 block 插在卷积主干中间。如果你原来某一层长这样self.dwconvnn.Conv2d(dim,dim,kernel_size5,padding2,groupsdim)self.pwconvnn.Conv2d(dim,dim,kernel_size1)那 LSK 风格的改法通常就会变成self.spatialLSKBlock(dim)也就是说它更像是一个spatial enhancement blockmulti-scale feature selection block而不是简单替换成一层普通卷积。7. 我写了一个教学版最小例子为了方便讲清楚这个模块我没有直接大段复制官方仓库而是自己写了一个教学版MiniLSKBlock。这个版本不追求和原始实现逐行一致但保留了最核心的思想两个不同感受野分支一套空间 gate根据 gate 权重做分支融合核心代码如下classMiniLSKBlock(nn.Module):def__init__(self,channels:int):super().__init__()self.branch_smallnn.Conv2d(channels,channels,kernel_size5,padding2,groupschannels)self.branch_largenn.Conv2d(channels,channels,kernel_size7,padding9,dilation3,groupschannels)self.reduce_smallnn.Conv2d(channels,channels//2,kernel_size1)self.reduce_largenn.Conv2d(channels,channels//2,kernel_size1)self.gatenn.Conv2d(2,2,kernel_size7,padding3)self.expandnn.Conv2d(channels,channels,kernel_size1)defforward(self,x:torch.Tensor):aself.branch_small(x)bself.branch_large(a)a_halfself.reduce_small(a)b_halfself.reduce_large(b)mergedtorch.cat([a_half,b_half],dim1)avgmerged.mean(dim1,keepdimTrue)mxmerged.max(dim1,keepdimTrue).values gate_logitsself.gate(torch.cat([avg,mx],dim1))gatetorch.softmax(gate_logits,dim1)fuseda*gate[:,0:1]b*gate[:,1:2]outself.expand(fused)*xreturnout,gate8. 这段代码怎么读第 1 段先做两条不同尺度分支self.branch_smallnn.Conv2d(channels,channels,kernel_size5,padding2,groupschannels)self.branch_largenn.Conv2d(channels,channels,kernel_size7,padding9,dilation3,groupschannels)这里的意思非常直接一条分支偏小尺度一条分支偏大尺度注意第二条用了dilation3这也是很多大感受野模块常见的技巧不只是靠更大的 kernel本身也借助 dilation 把感受野再撑开。第 2 段先把两条分支都压缩一下self.reduce_smallnn.Conv2d(channels,channels//2,kernel_size1)self.reduce_largenn.Conv2d(channels,channels//2,kernel_size1)这里可以理解成两个分支都先做一次压缩后面拼起来做 gate 时更省这一步本质上是在给后面的选择模块准备输入。第 3 段用统计特征生成 gateavgmerged.mean(dim1,keepdimTrue)mxmerged.max(dim1,keepdimTrue).values gate_logitsself.gate(torch.cat([avg,mx],dim1))gatetorch.softmax(gate_logits,dim1)这是最值得讲的一段。它做的事情不是直接把两个分支相加而是先提取mean和max用这两个统计视角去预测一个空间 gate再让 gate 决定当前位置更偏向哪个分支也就是说LSK 风格模块真正聪明的地方不是“我有两条分支”而是我会根据位置去选分支。第 4 段最后再做空间融合fuseda*gate[:,0:1]b*gate[:,1:2]outself.expand(fused)*x这里就是最终融合第一张 gate 图控制小尺度分支第二张 gate 图控制大尺度分支融合后再映射回原通道空间所以它不是“固定比例混合”而是逐位置的加权融合。9. 最小例子跑出来是什么样我把这个教学版模块实际跑了一次输出如下Input shape : (2, 16, 32, 32) Gate shape : (2, 2, 32, 32) - (B, 2, H, W) Output shape: (2, 16, 32, 32) Mean small-branch weight: 0.5002 Mean large-branch weight: 0.4998这至少说明两件事gate 不是一个全局标量而是一个空间图模块输出尺寸没有变可以继续往后接这也是为什么我会把它归到“可插进主干网络的结构模块”而不是一篇论文里自娱自乐的定制件。10. 再看一张图空间 gate 到底在偏向谁为了把“选择”这件事讲清楚我又做了一张门控偏好图。这张图虽然是示意性的但很适合用来讲直觉local edge、small blob这种更局部的结构更偏小核分支large blob、wide region这种更宽的结构更偏大核分支mixed区域则可能介于中间它不是为了证明精确数值而是为了说明LSK 这种模块存在的意义就是让同一层里的不同区域不必共享同一个卷积偏好。11. 给一个已知数据集上的效果图案例如果只讲模块原理读者很容易还是觉得抽象所以我又补了一张大家都认识的数据集案例图这次直接用的是Fashion-MNIST。这张图不是在严格复现官方论文实验而是用一个教学化可视化去说明同样一张输入图小尺度分支和大尺度分支看到的响应不一样最后融合结果会更偏向当前图像真正需要的空间尺度这张图里可以直观看到几件事对鞋子这种横向轮廓比较明显的形状大尺度分支会更强调整体外轮廓对上衣这类既有局部领口、又有整体服饰结构的样本两条分支的侧重点并不一样融合图不是简单平均而是更像“把更该亮的结构提出来”如果把这张图的意义再说得更直白一点它其实是在说明LSK Block的价值不只是“多做了一条卷积分支”而是它能让模型在面对不同形状目标时自动调整自己更依赖哪一种空间尺度。像鞋子这种长条、整体轮廓很重要的目标更大的感受野更容易把外形连起来而像上衣、裤子这类同时包含局部边界和整体结构的目标局部响应和大范围响应都不能丢。最终的融合结果之所以更稳定就是因为它不是死用一个固定卷积核而是在不同位置上动态选择更合适的特征来源。对于读者来说这张图最重要的意义就是把“selective kernel”这几个字从论文概念变成了一个可以肉眼理解的现象。这类图对 CSDN 很有用因为它能把“模块的结构直觉”从抽象原理拉到一个大家熟悉的数据集上。12. 如果你自己要接最适合放在哪如果你手头是 CNN 主干LSK Block 最自然的插法通常是下面几种放在 backbone 中后段放在本来就有 depthwise / spatial mixing 的位置放在对大目标、长结构、宽区域更敏感的阶段我不建议一上来就每一层全换每个 stage 全塞不做 ablation 就直接下结论更稳的做法是先选一个 stage 试保留原始 backbone 结构不大动看收益是否来自多尺度选择而不是训练偶然性附我这次实际参考的 GitHub / 论文官方 GitHubhttps://github.com/zcablii/LSKNetarXivhttps://arxiv.org/abs/2303.09030