SCNet自校准卷积:原理、实现与工程实践详解
做视觉的同学应该都有体会骨干网络设计到后期单纯把网络加深加宽收益越来越小。前几年大家普遍喜欢在主干网络的卷积后面挂通道注意力模块后来又出现了核选择、全局上下文建模等思路各自解决了一部分问题却总在“涨点”和“耗时”之间反复纠结。2020年的计算机视觉顶会上有一篇题为《Improving Convolutional Networks with Self-calibrated Convolutions》的工作把目光落回卷积本身提出了自校准卷积网络SCNet核心是设计了一个可以直接替换普通3×3卷积的算子——自校准卷积SCConv。我最初是在一个50层残差骨干网络的分类任务上尝试把中间部分的3×3卷积替换成SCConv验证集精度确实涨了一点计算量也没有明显增加。后来又把它接到检测和分割模型里发现这类算子对多任务适配性比想象中好。这篇文章不打算复述论文原文而是把SCNet的原理、SCConv的结构、代码实现和我在实际改网络时总结的注意事项一起写出来方便想用在真实项目里的朋友直接参考。1. 为什么需要“自校准”普通卷积和注意力机制各自卡在哪儿1.1 普通卷积的视野局限普通3×3卷积每次计算只看到周围8个邻居。虽然通过层层堆叠理论感受野可以覆盖很大区域但有效感受野比理论值小很多。因为高层看到的信息是低层一系列卷积反复“稀释”后的结果真正对输出影响大的往往是中心附近区域。对于需要长距离依赖的任务比如检测时判断一个物体需不需要参考全局背景、分割时判断一个像素属于哪个大区域普通卷积要依靠几十层网络才能把有效感受野撑大代价高且效率低。另一个容易被忽略的问题是普通卷积在通道维度上把所有输入通道做加权和权重一经训练就固定不会根据当前输入内容动态调整。网络只能靠堆深堆宽来增加表达能力但每个位置的特征之间缺少显式的上下文沟通。换句话说一张特征图上不同位置的激活值虽然在数学上有交集但网络并不会主动让它们“讨论”该加强哪里、该抑制哪里所有信息都靠权重隐式传递。1.2 注意力机制带来了什么还缺什么SE通道注意力模块通过全局平均池化把每个通道压缩成一个标量再用两个全连接层生成通道权重最后乘回原特征。它解决了“哪些通道值得加强”的问题却没有解决“在某个空间位置周围发生了什么”的问题。全局平均池化会把空间细节全部抹掉SE本质上是在通道维度做一次重标定对空间关系并不敏感。Non-Local这类机制能建模任意两点之间的依赖显然后者更强但它需要构建一个HW×HW的相似度矩阵显存和计算量随分辨率平方级增长。在50层以上的骨干网络里使用部署成本往往压不住。SKNet通过多分支核选择来动态调整感受野思路很好但要同时维护多个中间特征图分支数和计算量之间的平衡并不容易把握。这些方法本质上都属于“外部纠偏”在卷积之后再加一个模块去修改特征。它们并不改变卷积算子内部的建模能力。SE没有扩大感受野Non-Local扩大了但代价高SK扩大了部分感受野但结构变重。1.3 自校准的思路SCConv把关注点从“加模块”变成“改造卷积本身”核心是让特征自己和自己对话。做法上它把输入通道分成两组一组保留局部细节另一组先通过下采样获得一个低分辨率版本用一次3×3卷积提炼上下文再上采样回去通过sigmoid变成一个0到1之间的“软门控”乘回原始特征。整个过程没有额外的全局池化没有大矩阵注意力也没有多分支叠加。输入输出张量形状和普通卷积一致直接替换即可。这个设计的妙处在于校准信号来自输入特征自身而不是外部模块。所以作者把它叫“自校准”。它既扩大了单个卷积层的有效视野又不需要显式计算位置关系。这也是SCNet命名时的核心直觉——让卷积网络内部多一层“参照自己”的能力。2. 自校准卷积的结构拆解一股分流、两次视野扩张、一次软门控2.1 通道切分X1与X2各自的分工假设输入X的shape是(C, H, W)SCConv先把输入沿通道平均切成两份X1和X2各占C/2。这两个分支的分工完全不同。X1所在的分支可以叫“自校准分支”原始X1仍然保留同时它自己会派生出一条“校准信号生成通路”。生成通路负责从X1中提取一个粗略的上下文图再决定原图上哪些位置、哪些通道需要被加强或抑制。X2所在的分支则是一个普通3×3卷积用来保留不经过任何门控调制的高频细节。之所以不能两个分支都做门控是因为如果全部特征都被重新缩放网络可能丢掉一些尖峰信息比如目标的边缘、小物体的细节。保留一路“原样走普通卷积”的特征相当于给主干留了一条未经过多干预的快速通道。用一个生活化的类比X1既拥有高清原图又先快速生成了一张“缩略图”由缩略图判断哪里值得关注再映射回高清原图去放大处理X2相当于原图原封不动送进另一条流水线保证基本细节不丢失。最后两条流水线的结果拼在一起形成输出。2.2 下采样、上采样与sigmoid校准信号是怎么生成并作用的校准信号要分三步走。第一步是平均池化。X1经过一个步长为s的平均池化尺寸变成(H/s, W/s)。在这个缩小后的特征图上一个3×3卷积能看到的范围大约相当于原图的3s×3s。比如s4时卷积核的有效视野从3×3扩大到大约12×12。这一步是“第一次视野扩张”它没有使用大卷积核也没有增加参数量纯粹靠降低分辨率来放大感知范围。第二步缩小的特征图经过一个3×3卷积再被双线性插值恢复成原始分辨率。上采样不是简单放大而是把一个低分辨率空间里提炼出的“上下文评价”映射回原图坐标。第三步把上采样结果送入sigmoid压到0到1之间生成与X1同尺寸的软门控。sigmoid之后的值接近1表示“这个位置的上下文值得看重”接近0表示“抑制”。然后这个软门控与X1逐元素相乘相当于对X1做了一次基于自身上下文的校准。校准后的X1再过一次普通3×3卷积得到Y1X2直接过普通3×3卷积得到Y2最后把Y1和Y2在通道上拼起来得到输出。整个模块不需要额外分支串联输出的通道数也和输入一致。2.3 计算复杂度估算为什么说SCConv算力开销不大以输入输出通道数为C、特征图为H×W的3×3卷积做基准。普通卷积的FLOPs大约是2×C×C×9×H×W。SCConv里X1路径的第二个卷积和X2路径的普通卷积各自只处理一半通道所以各自的计算量只有普通卷积的1/4左右。X1路径的第一个卷积在低分辨率特征图上计算s4时它的FLOPs大约是普通卷积的1/(16×4)量级几乎可以忽略。池化、上采样和sigmoid主要是访存开销乘加计算很少。所以总卷积计算量大致只有普通3×3卷积的一半到三分之一。这点在替换深层骨干网络时非常关键不是“为了涨点额外加模块”而是在近似甚至更低的预算内重构卷积。当然FLOPs低不代表内存占用一定低SCConv多出了低分辨率特征图、上采样特征图和门控特征图对访存有额外压力。这个问题后文会专门展开。3. 和SE、SK、Non-Local放一起看SCConv到底强在哪3.1 不同模块的分工对比很多同学一看到“自校准”就以为又是注意力机制。实际上SCConv和SE、SK、Non-Local并不在一个层面。下面这张表可以快速看出差异维度SESKNon-LocalSCConv处理粒度全局通道统计多核自适应选择全图位置关系通道组内空间上下文感受野影响不改变通过核选择改变全局感受野通过下采样放大局部视野额外开销很小中等大小到中等是否替代主卷积否串联在外部是多分支替代否并行外部路径是直接替代3×3卷积空间细节敏感度低中中高部署友好度高中低中这里最容易混淆的是SK。SK也是一个分支选择模块替换主卷积但它生成的是“选哪个核”的离散倾向SCConv生成的是“每个位置在该通道上放大多大程度”的连续门控。SK的空间制导能力不如SCConv细致因为SK的决策对象是卷积核不是特征图上的具体位置。Non-Local有能力建立任意点对的依赖但注意力图本身的存储和计算成本让它在真实项目里很难用在每个stage。3.2 什么场景适合用SCConv从我的实践来看SCConv最适合当骨干网络的基础算子而不是当特效药。如果你手里有一个分类网络、检测网络的backbone希望在基本不增加FLOPs的前提下提升精度SCConv是一个很顺手的替换选项。尤其是特征图分辨率处于中等尺度比如28×28到14×14的阶段s4池化后还有7×7或4×4左右的分辨率校准信号仍然保留足够空间结构效果最好。反过来如果输入特征图本身已经很小比如7×7甚至3×3再往下采样就只剩1×1或更小校准信号完全失去空间含义这时SCConv带来的不是上下文而是模糊。另外如果你的部署约束主要来自内存带宽SCConv多出的中间特征图数量会让它在某些芯片上比普通3×3卷积更慢。FLOPs只是参考指标访存和并行度也要一起评估。3.3 为什么SCConv的“涨点”通常比较稳我在多个任务上观察到的共同点是只要替换范围控制在网络的中后段SCConv很少让指标明显变差大多数时候是稳定提升幅度不大但可靠。原因在于它不依赖特定数据集的先验。下采样、上采样和sigmoid都是平滑且可导的操作训练过程中梯度能够顺利流回校准通路。它也没有引入像注意力图那样的高方差路径训练初期的损失波动比SE小。另一个原因是SCConv直接替代卷积后网络每个stage内部都多了一个“参照自身上下文”的机会。这种能力不需要额外的损失函数监督也不需要专家知识设计完全靠端到端训练自己学会。所以不管是图像分类、目标检测还是语义分割它都能以几乎相同的方式工作这也是它作为通用模块能脱颖而出的关键。4. 从公式到代码一个可运行的SCConv实现与维度核对4.1 基础实现把自校准分支写清楚下面我用常见的深度学习框架接口风格写一个简洁实现。注意我没有写与具体框架强绑定的导入核心方法在任何支持卷积、池化、插值的框架里都一样。class SelfCalibratedConv(nn.Module): def __init__(self, channels, s4, dilation1): super().__init__() half channels // 2 self.s s # 校准信号生成通路先做低分辨率卷积 self.down nn.AvgPool2d(kernel_sizes, strides, ceil_modeTrue) self.conv1 nn.Conv2d( half, half, kernel_size3, paddingdilation, dilationdilation, biasFalse ) # 两条输出卷积都是普通3x3 self.conv2 nn.Conv2d(half, half, kernel_size3, padding1, biasFalse) self.conv3 nn.Conv2d(half, half, kernel_size3, padding1, biasFalse) self.act nn.Sigmoid() def forward(self, x): x1, x2 x.split(x.size(1) // 2, dim1) # 第一次视野扩张池化降低分辨率 gap self.down(x1) # 低分辨率卷积 gap self.conv1(gap) # 第二次视野扩张上采样回原分辨率 gap interpolate( gap, sizex1.size()[2:], modebilinear, align_cornersFalse ) # 软门控 gate self.act(gap) # 校准后的X1再过常规卷积 y1 self.conv2(x1 * gate) # X2走普通卷积保留细节 y2 self.conv3(x2) # 拼接输出 return concat([y1, y2], dim1)这个实现有几个地方需要讨论。第一s4是论文里的常用下采样倍率。如果你的特征图分辨率特别小建议把s改成2。第二conv1的dilation参数可以调整我默认给1想要更大视野可以加大。第三内部卷积都不加bias因为整体模块后面通常会接BN。如果框架要求每个卷积都独立也可以加bias但效果差别不大。4.2 前向传播的每一步张量形状核对写代码最怕维度对不上这里把每一步的形状变化整理成一张表。假设输入是(B, 64, 32, 32)通道数C64下采样倍率s4步骤对应操作输出形状输入forward输入(B, 64, 32, 32)通道切分split成x1和x2(B, 32, 32, 32) × 2平均池化down(x1)(B, 32, 8, 8)低分辨率卷积conv1(B, 32, 8, 8)上采样interpolate(B, 32, 32, 32)sigmoidgate(B, 32, 32, 32)逐元素相乘x1 * gate(B, 32, 32, 32)分支一卷积conv2(B, 32, 32, 32)分支二卷积conv3(x2)(B, 32, 32, 32)拼接输出concat(B, 64, 32, 32)维度匹配的核心是interpolate必须指定sizex1.shape[2:]而不是固定使用scale_factors。如果你使用scale_factors当前向输入分辨率不是s的整数倍时上采样后的尺寸会和原图不一致直接导致后面的x1 * gate报错。我见过不少复现bug都出在这一行。4.3 嵌入残差骨干网络的三个注意点第一一个关键问题是替换哪些卷积。普通残差块里的3×3卷积通常步长为1输入输出通道一致。SCConv适合替换这类卷积。如果某个3×3卷积本身承担着空间下采样任务内部还带stride2直接换成SCConv会出问题SCConv内部的平均池化又对X1降了一次分辨率会让自校准分支的细节丢失更多。我的建议是只替换stride1的3×3卷积或者把SCConv改造成支持外部stride的版本在最后的cat输出处做下采样。第二通道数必须为偶数且输入输出通道相同。因为SCConv要把输入切成两个半通道如果C是奇数代码会直接崩。一般常用骨干网络都满足但如果你自己设计的网络在某层使用奇数通道需要先补一个1×1投影把通道调整成偶数或者在SCConv外面包一层通道对齐模块。第三BN层的位置。我推荐把BN放在SCConv整个模块之后而不是塞进内部每个卷积之后。原因是SCConv内部已经有池化、上采样和sigmoid如果在内部加BN会让优化路径变深训练稳定性反而下降。把它看成一个普通卷积层输出再接BNReLU加载预训练权重时也更容易对齐。5. 实际使用中的调参顺序和踩坑记录5.1 三个超参的调节方向SCConv最主要的超参是下采样倍率s、通道切分比例、以及conv1的膨胀率dilation。下采样倍率s控制校准信号的“全局程度”。s越大池化后卷积看到的范围越大但空间细节丢失越多。默认s4是我用得最多的在28×28、14×14的特征图上表现稳定。如果特征图已经变成7×7建议把s给到2否则校准通路只剩1×1sigmoid门控会对全图给出单一缩放系数相当于退化成通道级缩放失去了空间意义。通道切分比例我建议保持一半一半。我试过把X1调到总通道的1/4计算量更低但校准分支的表达能力变弱精度提升也变小试过X1占3/4自校准能力更强但X2分支保留的原始细节太少训练后期容易过拟合。一半一半是性价比最平衡的点。膨胀率dilation是可选优化项。加大conv1的dilation可以进一步扩大低分辨率分支的视野但扩得太大容易出现网格伪影特别是检测小目标的时候。我实际验证下来dilation2偶尔比1好dilation4基本只会增加风险。5.2 我踩过的三个典型坑第一个坑是输入尺寸不可整除。有一版实现我直接用了sizeH/4的池化结果网络在某个阶段输入是13×13池化后变成3×3再上采样回13×13丢失的两行像素永远补不回来验证精度掉了不少。解决方法是把AvgPool2d的ceil_mode打开或者干脆用AdaptiveAvgPool统一输出为目标尺寸。上采样使用size参数而不是scale_factor是我后来养成的习惯。第二个坑是和SE模块的叠加顺序。我开始图省事在骨干里先SE后SCConv结果SE的全局池化把SCConv校准后的局部特征又压平了一次两个模块的收益几乎抵消。后来把顺序反过来变成SCConv接SESCConv负责空间上下文校准SE负责通道重标定各管一段提升才回来。如果你不是非要把两个模块放一起我更推荐只留SCConv它在多数任务上的综合收益已经足够。第三个坑是混合精度训练。网上很多训练脚本默认开启FP16SCConv内部的sigmoid在低精度下会更容易饱和门控值贴上0或1后梯度消失早期训练loss下降明显变慢。我不建议把整个模块都算FP16在工程上我会把池化和上采样放在FP32路径上卷积部分再走低精度。这个细节在单卡训练时看不出来一上多卡同步就会暴露。最后一个跟迁移学习有关的经验用普通预训练权重初始化SCConv时不能指望权重直接映射。普通3×3卷积的权重是一个(C, C, 3, 3)的矩阵而SCConv的conv1、conv2、conv3各自是(C/2, C/2, 3, 3)。一个比较稳的初始化策略是把预训练权重按通道前一半、后一半切分分别复制给conv2和conv3conv1用随机初始化同时把gate分支的初始输出控制得靠近0.5。这样第一轮前向时门控不会直接改变特征分布损失曲线不会暴涨。5.3 替换前的自查清单每次要在新项目里用SCConv前我都会先过一遍下面的清单能省下不少调试时间目标卷积的stride是否为1如果不是先改造SCConv或放弃替换。输入输出通道是否相同、是否为偶数不满足就先用1×1投影对齐。当前特征图的最小边能不能被s整除不能就调小s或开ceil_mode。显存预算能否承受内部多出的几张中间特征图预训练权重有没有提前准备通道切分映射是否确定从网络的中后段开始替换而不是一上来全部改把这几条都确认清楚SCConv基本不会在工程层面给你“惊喜”。我这段时间用下来的最大体会是SCConv不是玄学涨点它最适合当骨干网络重构的第一块积木。它不保证每个任务都提分但只要你把替换范围控制好、把预训练映射做对通常能换来稳定的小幅提升。先把自查清单过一遍比调任何参数都管用。