Swin Transformer核心机制与实践指南:窗口注意力与层次化设计解析
做CV视觉Transformer这块要是没听说过Swin Transformer那确实有点说不过去了。2021年ICCV最佳论文微软亚洲研究院出品当年一出就把ViT在视觉任务上“只能分类、不好下采样”的尴尬局面给打破了。直到今天Swin作为骨干网络在很多检测、分割任务里依然是强基线而且“层次化局部窗口”这套设计思路后来几乎成了视觉Transformer的标配范式。这篇是“ICCV 2021论文精读系列”的第八篇我就以实践者的视角把Swin Transformer拆开揉碎讲一遍。重点讲三件事它解决了ViT的哪些实际问题、核心机制是怎么一步步设计出来的、以及你如果要在自己的项目里用它有哪些工程细节必须留意。适合刚接触视觉Transformer的初学者也适合那些用Swin调过模型但没细看源码的人。看完之后你至少能回答三个问题为什么窗口要移动为什么窗口移动之后还能高效计算为什么Swin能在检测和分割任务上全面超过同体量CNN1. 视觉Transformer的瓶颈ViT到底卡在哪先说一个背景。ViTVision Transformer2020年提出的做法很简单把图像切成固定大小的patch然后当成一串token送进标准的Transformer Encoder。这个思路在图像分类上效果很好尤其是大数据集预训练之后ImageNet-1K上能打到84%以上当时的CNN要堆很大的模型才追得上。但ViT有两个问题在使用中非常明显。第一个问题是计算复杂度。标准Transformer里每个token都要和所有其他token做注意力交互复杂度是O(N²)N是token数量。ViT在224x224输入下patch size如果是16x16那么token数是14x14196这个量级还好但如果你是做目标检测输入通常是1024x1024甚至更大patch size还是16的话token数就变成了64x644096自注意力的计算量和显存占用是平方级上涨根本吃不消。你总不能为了省算力把patch切得很大切大了又丢失细节小目标直接就没法玩了。第二个问题是缺乏多尺度特征。ViT的结构是所有层都保持相同分辨率最后一层直接接一个分类头。这对分类来说没问题但对检测、分割这类任务几乎所有经典方法比如FPN、U-Net都依赖多尺度特征金字塔浅层特征保留细节、深层特征提供语义多层融合才能做好不同大小的目标。ViT没有这个天然设计直接拿来做检测就得各种魔改效果还很折腾。Swin Transformer的出发点就是同时解决这两个问题。它的名字Swin全称是Shifted Window Transformer核心思路借鉴了CNN的两个传统艺能局部感受野和层次化下采样。局部感受野对应窗口注意力把全局交互限制在固定大小的窗口内计算量从O(N²)降成O(N)层次化下采样对应patch merging类似CNN里的stride2卷积每过一个stage特征图分辨率减半、通道数翻倍。这样一来Swin的骨干网络长得很像ResNet可以无缝嵌入FPN、Mask R-CNN这类检测框架中。我当时第一次看这个设计时第一反应是“这不是把CNN的归纳偏置拿回来了吗”。但仔细想不是简单的回归。Swin保留了Transformer强大的全局建模能力只不过用“窗口移动窗口”的方式在局部和全局之间做折算局部窗口内做精细的注意力交互窗口移动后让不同区域的信息逐步联通经层层堆叠感受野一样能覆盖全图。这个折中方案既保住精度又换来实用性和效率这是它能成为通用骨干的关键。2. Swin Transformer的核心机制拆解2.1 从Patch到Stage层次化特征的构建Swin的输入处理跟ViT类似但细节有差异。输入图像先被切成patchpatch size是4x4每个patch的原始维度是4x4x348经过一个线性嵌入通常就是卷积核为4x4、步长为4的卷积把通道数映射到C。论文默认C96。对于224x224的输入patch嵌入之后得到的是56x56x96的特征图token数是56x563136比ViT的196多了一个数量级。从这里开始Swin进入四个stage每个stage的结构类似先做若干层Swin Transformer Block再做patch merging进行下采样。Patch Merging很有意思它是把2x2相邻的patch在通道维上拼起来然后通过一个全连接层把通道数压缩到一半。以56x56x96为例2x2的四个patch拼接后变成28x28x384全连接层映射到28x28x192分辨率减半、通道翻倍。这个操作本质上就是“通道换空间”和CNN里的pooling加通道扩张思路一脉相承只是换了一种方式实现。经过四个stage之后特征图的分辨率变化是Stage 1: 56x56Stage 2: 28x28Stage 3: 14x14Stage 4: 7x7通道数则是96、192、384、768依次翻倍。这个结构和ResNet的stage设计非常接近所以它的输出可以像ResNet一样直接喂给FPN这类检测头的多尺度分支里。你在timm里加载Swin模型后看它的stage输出会发现四个特征层分别对应上述四个分辨率接检测头非常顺手。每个stage里的Transformer Block数量不同变体不一样。以Swin-TTiny为例四个stage的block数分别是2、2、6、2和ResNet-50的层数分布3、4、6、3相似重心放在中后段。这个比例看起来是调过的stage 3的深度对性能影响最大。2.2 移动窗口注意力Swin的灵魂理解了整体结构再看每个stage里的Transformer Block。Swin的Block和标准Transformer Block长得像但注意力部分做了大改动不再是对所有token做全局注意力而是在局部窗口内算。具体做法是把特征图划分成固定大小、不重叠的窗口论文默认窗口尺寸M7x7。对于56x56的特征图就能划分成8x864个窗口。每个窗口内只有7x749个token注意力计算只在49个token之间进行。自注意力复杂度从O(N²)降成了O(M²N)其中M49是常数所以整体变成线性复杂度。这是窗口注意力的第一个好处。但问题来了如果所有Transformer Block都用固定窗口那么每个窗口内的token永远只能和同一个窗口内的其他token交互跨窗口的信息就断了。这相当于把注意力限制在了局部感受野内和多层CNN的卷积核堆叠有点像但没有跨区域的信息交换。Swin的解法就是移动窗口注意力Shifted Window Attention。思路很朴素把层分成两组交替使用。奇数层用规则的窗口划分W-MSA偶数层把窗口整体向右下偏移(M/2, M/2)个像素重新划分窗口SW-MSA。这样原本处于不同窗口边缘的token在偏移后的窗口中就会相聚从而实现跨窗口信息交流。一层的窗口注意力加下一层的移动窗口注意力合起来就是一个完整的Swin Transformer Block对。但移动窗口有一个效率陷阱窗口整体偏移之后特征图上多出来一些边界区域导致窗口个数不再是规则的整数网格。论文报告原始实现会产生9种大小不同的窗口导致计算效率很低。Swin的解决办法非常巧妙先把特征图在左上角方向做cyclic shift也即把左上那部分搬到右下补位使得窗口划分又恢复成规则的四等分同时配合一个mask矩阵把那些在cyclic shift中“绕回来”的不相邻区域的注意力分数屏蔽掉。这样一来窗口数量保持4个计算量不额外增加而mask保证了正确的注意力范围。我第一次读到这里时觉得很妙它把“不规则窗口”这个工程难题转成了“规则窗口掩码”的实现在不牺牲语义正确性的前提下保证了矩阵运算的整体性和GPU利用率。实际跑起来Swin的前向速度和同尺寸ViT差距不大但能处理更大分辨率输入。2.3 相对位置偏置比绝对位置编码更合适Swin的另一个重要细节是相对位置偏置Relative Position Bias。标准ViT用的是一维绝对位置嵌入把每个token的位置信息加到token向量里。Swin发现对于它的窗口注意力直接用相对位置偏移作为注意力分数的偏置项效果更好、也更灵活。具体公式是Attention(Q,K,V) SoftMax(QK^T/√d B)V。其中QK^T是窗口内token两两之间的注意力分数B就是相对位置偏置。B不是直接计算出来的而是从一个可学习的偏置表中查出来的。对于窗口大小M7相对位置可能的偏移范围是[-6, 6]于是每个维度有13种可能二维组合起来偏置表的大小是13x13169个可学习参数实际上论文用(2M-1)x(2M-1)的矩阵即13x13169再加上一个展平操作。计算注意力分数时根据两个token的相对坐标查表得到一个标量偏置加到对应的注意力分数上。这个设计的优势有两个。第一相对位置信息对平移更加鲁棒。一张图里的猫不管出现在左上角还是右下角它内部的相对位置关系是不变的偏置项也就一样绝对位置编码则不同换个位置就要重新学习。第二偏置表只在窗口大小M内定义模型训练好之后如果要在更大分辨率上微调可以把偏置表做双线性插值适配到更大的窗口范围比绝对位置编码更平滑。当然直接插值会带来一些精度损失这个后面讲避坑会再提。从整体设计看Swin把CNN的两大优点局部性和层次化移植到了Transformer上同时保留了Transformer的注意力机制。它不是简单地把两层结构拼在一起而是在效率和表达能力之间做了一套完整的工程化设计这是它在多个任务上全面占优的根本原因。3. 在项目中使用Swin选型、代码和核心参数3.1 模型变体怎么选Swin官方给出了四个主要变体Tiny、Small、Base、Large。它们的区别主要在于Embedding维度C、每个stage的block数量和MLP隐藏层倍数。变体CStage Block数参数量ImageNet-1K Top-1Swin-T962,2,6,228M81.3%Swin-S962,2,18,250M83.0%Swin-B1282,2,18,288M83.5%Swin-L1922,2,18,2197M86.4%这里的Top-1是224x224输入、ImageNet-1K训练集上报告的结果。实际使用中选Swin-T还是Swin-B主要看你的硬件和任务。如果你的数据集不大几万张以内数据增强常规那Swin-B可能比Swin-T更容易过拟合反而Swin-TSAM优化器的组合可能效果更好。如果是检测分割这类需要下采样特征的任务SWin-T/Swin-S往往性价比更高FPN加进来之后整个模型参数量约为35M到60M在单卡V100甚至消费级卡上都能跑得动。还有一个很常见的误区Swin-L在ImageNet-22K上预训练后再微调到1K能得到86.4%的成绩但那是用了额外的22K大数据预训练。你如果直接在1K上从零训L效果不会比B好多少而且训练成本高很多。所以除非你有大规模预训练条件否则不要盲目上大模型。3.2 timm里的一行代码调用如果你用PyTorch最容易上手的方式是通过timm库。timm里已经集成了Swin的多种预训练权重一行代码就能加载import timm model timm.create_model(swin_tiny_patch4_window7_224, pretrainedTrue) model.eval() # 输入格式是 B, C, H, W import torch x torch.randn(1, 3, 224, 224) out model(x) # 分类输出如果想拿中间层特征做检测或分割可以用forward_features方法返回四个stage的输出特征元组features model.forward_features(x) # features 是长度为4的tuple对应 stage1~stage4 for f in features: print(f.shape)timm的实现已经包含了完整的Patch Embedding、Patch Merging、Shifted Window Attention和相对位置偏置逻辑细节上和官方代码几乎一致适合直接用。唯一需要注意的是timm里Swin的命名规则是swin_variant_patch出patch大小_window窗口大小_输入分辨率比如swin_tiny_patch4_window7_224就对应Swin-Tpatch大小为4窗口大小7训练输入224。如果要做迁移学习可以加载预训练权重后替换分类头model timm.create_model(swin_tiny_patch4_window7_224, pretrainedTrue, num_classes0) # 删除分类头只保留backbone然后再接自己的分类层或检测头这样避免预训练权重的分类层与自己的类别数不匹配导致加载报错。3.3 调整输入尺寸和窗口大小时要注意什么Swin的参数里有两个数很容易让人觉得可以随便改patch size和window size。在Swin里patch size是4是定死的改小会直接改变初始嵌入和计算路径一般不推荐动。window size虽然是可配置的但预训练时的相对位置偏置表和窗口的网格划分都是基于7这个值训练的。如果要在更高分辨率下使用比如检测任务输入尺1280x1280你有两种选择第一种保持window size7不变只把输入分辨率调高。因为窗口划分是按特征图的尺寸整除的只要输入尺寸是32的倍数每个stage下采样2倍共4个stage也就是2^416倍所以实际上是16的倍数稳妥点取32窗口数量会变多但每个窗口内的token数不变相对位置偏置表依然有效不需要插值。这种情况模型可以直接适配更高分辨率精度影响很小。第二种如果因为某些原因想增大window size比如从7改成14那预训练的偏置表尺寸是13x132x7-1而新窗口需要27x27的偏置表必须通过双线性插值进行初始化。这个操作PyTorch里可以用torch.nn.functional.interpolate实现但插值后精度的衰减会比较明显通常需要在新任务上重新微调很久才能恢复。实践下来我不建议轻易增大window size除非你有充分的算力和时间做重训练。另外分类任务里常用的做法是训练时用224x224测试时用384x384做更大尺度的评估。Swin在384分辨率下窗口数量从8x8变成约13x13如果输入384的话384/496stage1是96x96窗口数(96/7)²约13x13因为96/713.7实践上需要padding或重新划分所以你需要确认输入能被7的倍数整除否则就得做padding。这个细节在timm的实现里已经处理了但如果自己写迁移代码就需要特别小心。3.4 从Swin接检测和分割头特征金字塔的对接如果你不是只用Swin做分类而是要把它接入Mask R-CNN、Cascade R-CNN或者语义分割模型过程中最核心的对接点就是backbone输出的特征层。Swin输出的四个stage特征图形状分别是stage1: (B, H/4, W/4, C)stage2: (B, H/8, W/8, 2C)stage3: (B, H/16, W/16, 4C)stage4: (B, H/32, W/32, 8C)这和ResNet的C2到C5阶段直接对应。所以在MMDetection或Detectron2里配置Swin时你只需要把backbone的out_indices设为[0,1,2,3]然后接一个FPN输出256通道的多尺度特征剩下的检测头和标准流程完全一样。这里FPN主要是对stage3和stage4的高层特征做上采样和stage1、stage2的低层特征融合形成金字塔。Swin本身已经有多尺度但FPN能把它们融合得更好特别是在小目标场景里作用很明显。配置时有一个常见参数叫pretrain_img_size用来控制相对位置偏置的初始化方式。比如你预训练用的224后来在检测数据上训练时输入尺寸是800x1333那么模型加载预训练权重时可以设置pretrain_img_size224让代码自动调整偏置表。MMDetection里Swin的配置项就有一项叫pretrain_img_size直接填224。如果你忘了设部分实现默认值是224问题不大但如果你自己写代码加载权重就要记得对位置偏置表做插值。分割任务里的对接也类似通常取stage3和stage4的特征对应1/16和1/32下采样作为decoder的多尺度输入配合UPerHead或者SegFormer Head使用。这里要注意的是stage4的特征分辨率较低如7x7上采样回原尺寸时插值质量影响较大很多实现会选择丢弃stage4只用前三层。实测下来对于一般语义分割只用stage1到stage3的效果和四层都用差距不大但显存能省不少。4. 效果与影响为什么Swin能在多个任务上称王4.1 分类、检测、分割三个维度的表现Swin论文里报告了一组很扎实的实验数据。图像分类方面Swin-B在ImageNet-1K上以88M参数达到83.5%的Top-1准确率如果先在ImageNet-22K上预训练再微调Swin-L可以达到86.4%。作为对比当年最强CNN之一的EfficientNet-B7参数量66M准确率84.3%ViT-B/16在ImageNet-21K上预训练、1K微调是84.0%。Swin-B在参数量和计算量接近的情况下比ViT-B略低0.5个点但EfficientNet/CNN的差距已经很小而且Swin在分类任务上的优势本来就不那么大。真正的差距体现在密集预测任务上。目标检测方面用Cascade Mask R-CNN作为检测头Swin-S在COCO test-dev上达到51.8 box APSwin-L搭配HTC可以达到58.7 box AP这个数字当时远超所有CNN backbone。语义分割方面Swin-L在ADE20K上达到53.5 mIoU比同期的ResNet-101FCN高出将近15个点。这个提升幅度在语义分割领域是很惊人的。为什么会有这么大的差距核心原因还是多尺度特征和局部建模在密集预测里的价值。分类只需要全局语义信息CNN的池化已经能提取得很好但检测和分割要求同时保留精确的空间位置和丰富的语义信息Transformer的全局注意力擅长后者Swin的层次化结构弥补了前者。再加上FPN把浅层细节和深层语义多尺度融合整体性能自然显著超过纯CNN方案。4.2 Swin对后续模型的影响Swin的成功不只是它自己在榜单上的成绩更在于它引领了一个设计范式把窗口注意力和层次化结构作为视觉Transformer的标准配置。后来的很多模型都沿着这个思路做了改进VOLO2021在Swin的基础上引入Outlook Attention专注细粒度特征。Focal Transformer2021用focal注意力机制让每个token关注附近的细粒度信息远距离粗粒度信息。CSWin2022把Swin的方形窗口改成横向和纵向的条形窗口降低窗口划分的复杂度。Swin v22022解决Swin在大规模训练和高分辨率下的稳定性问题优化了位置编码和Window Attention的数值稳定性。可以说Swin v1之后视觉Transformer的研究从“要不要用Transformer”全面转向了“怎么设计注意力机制来适配视觉任务”。即使到了2024年ViTDet、DINOv2这些模型做检测时很多还是沿用Swin的层次化特征输出方式。所以理解了Swin的机制你看后续的视觉Transformer论文都会容易很多。4.3 训练Swin的训练策略相关经验Swin本身的训练技巧和常规ViT类似但在实际复现时有几点值得注意。第一优化器建议用AdamW初始学习率设置不同变体不同。Swin-T大约在1e-3Swin-B可以是5e-4Swin-L建议3e-4。Batch Size对应512或1024如果显存不够可以梯度累积但要保持有效batch size不变。学习率调度用余弦退火配合5到20个epoch的warmup。我实测下来batch size从256加到1024学习率最好按比例线性放大否则收敛会很慢。第二数据增强方面Swin训练时用的增强比CNN多一些包括RandAugment、Mixup、CutMix、Random Erasing等。特别是CutMix对Swin很有帮助因为窗口注意力的局部性使得模型对遮挡和裁剪更敏感CutMix通过混合两个图像的patch相当于强制模型利用局部特征。Swin-T如果不开CutMixImageNet-1K上掉0.5个点左右看起来不多但差距在迁移到检测时会放大。第三正则化上要注意DropPathStochastic Depth的使用。Swin官方实现里drop_path_rate按stage递增建议Swin-T设置0.1到0.2Swin-B可以到0.3Swin-L甚至可以0.5。这个参数非常影响大模型的训练稳定性如果训练过程中loss震荡可以优先调低学习率再调整drop_path_rate。第四大规模输入下的显存优化。窗口注意力虽然计算复杂度线性但它涉及窗口划分和mask操作实际显存开销还是比CNN高不少。训练大模型时可以用混合精度AMP显存能省一半如果还不够可以把窗口内的batch size打小利用gradient checkpointing只保存少量中间激活前向时重新计算这是目前训练视觉Transformer的标准做法。5. 实际踩坑与常见问题汇总5.1 我踩过的几个高频坑第一个坑是加载预训练权重时报错。Swin在timm和官方实现里的state_dict的key命名并不完全一致如果你从官方GitHub下载权重想加载到timm模型里需要对key做映射尤其是attn.w_msa.relative_position_bias_table这类相对位置偏置表名称很长容易拼错。用timm自带的timm.create_model(pretrainedTrue)最省心但如果你需要加载自己训练的checkpoint建议保存时直接存模型的state_dict()并以原模型类加载不要手动逐层复制。第二个坑是检测任务中设置out_indices不对导致特征缺失。Swin的四个stage默认输出最后一个block的结果但如果你在MMDet中配置了out_indices[1,2,3]只有三个特征FPN的输入维度就不匹配会直接报错。同理你在timm里取中间层特征时要确认对应索引因为stage1到stage4的索引是0到3别搞混。第三个坑是相对位置偏置表在输入分辨率变化时的数值差异。高分辨率微调时如果不做偏置表插值直接把预训练权重加载进去模型会在初始阶段产生较大的loss spike因为注意力分数初始化不对。解决办法是在加载权重时检查偏置表的shape如果和当前窗口大小不匹配就用双线性插值初始化比如用torch.nn.functional.interpolate把(a, b, 169, 169)的偏置表插值到(2new_window_size-1, 2new_window_size-1)然后重新映射。实测中插值后再微调几个epoch精度就能恢复。第四个坑是padding对chunk边界的影响。如果你自己实现窗口划分原图切patch时如果是奇数尺寸在最后一个patch处padding了0那这个位置的注意力计算就会引入明显的边界偏移。Swin官方代码通过F.pad在输入右、下侧padding然后切patch、做窗口划分这个padding值最好不是0而是边缘像素的复制replicate不然边界位置的token都会变成0特征网络很难学到有效信息。5.2 常见问题速查表问题表现排查步骤解法建议高分辨率下显存暴涨训练从224调到384后OOM检查窗口数量变化、是否忘记gradient checkpointing开启AMP混合精度、设置gradient checkpointing、减小Batch Size微调效果反而不如线性探针分类精度大幅下降检查初始学习率是否过大、drop_path_rate是否偏低学习率降到标准ViT微调的1/10增加warmup检测时特征层维度对不上FPNA输入通道报错检查out_indices配置和Swin的C参数确认C96时stage1是96通道后续stage按2倍递增上采样图有网格伪影语义分割结果出现方块状边界检查是否用了旧版转置卷积或过大上采样倍数用双线性上采样或PixelShuffle避免一步上采样过大多卡训练不收敛DDP训练时loss异常检查窗口划分batch维度是否被distributed sampler打乱确保每个进程都执行窗口划分前对特征图重新reshape5.3 改为线性复杂度后哪些场景不适用虽然Swin把计算复杂度从O(N²)降到了O(N)但有一个前提它仍然需要在每个窗口内部计算自注意力。窗口内的token数M²通常是49或更大数量的平方依然是一个常数因子。当你把M从7调到14或者28时窗口内计算量是成倍增加的显存也会涨。所以Swin并不是“任意分辨率都能随便跑”它更适合分辨率不是特别夸张的场景比如检测、分割的常见输入尺寸512到1333。如果真的要做超高分辨率比如4K视频上的密集预测通常还得配合稀疏注意力或者轴向注意力来做二次优化。另外Swin的窗口注意力天然假设了局部相关信息比长距离信息更重要。如果你的任务非常依赖长距离、全局建模比如全景图分割或者需要推理全局上下文的场景纯Swin的窗口限制可能会变成瓶颈。这也是后来Focal Transformer、DAT这类模型会补充长距离注意力分支的原因。你不是只能盯着Swin用理解这个限制反而能帮你判断什么时候该换模型。5.4 我对Swin后续版本的一个理解Swin在2022年出了V2版本主要解决两个问题一是在更大数据集比如ImageNet-22K、甚至3B图文对上训练时数值不稳定二是高分辨率输入下的位置编码泛化问题。V2的改动包括把LayerNorm前移、用相对位置偏置的对数间隔替换线性间隔、注意力分数缩放与窗口大小解耦、更深的block里降低激活精度等。这些都是工程性细节如果你只是用Swin v1做常规任务不必强行升级到V2但如果你要在大规模数据集上长时间训练建议看看V2的改进点很多思路比如用连续位置编码替代离散偏置表值得借鉴。6. 一点额外的心得最后分享一个我在实际项目里的体会。很多人第一次接触Swin时容易被它复杂的窗口移动和掩码细节劝退总想着把每个公式手推一遍才开始用。其实完全不用这样。你大可以先在timm里把预训练模型加载起来跑通一个小任务的分类或检测流程感受它的训练曲线、推理速度和精度然后再回头读源码、理解窗口逻辑。代码跑通了再回头看论文很多卡住的概念会一下子顺畅起来。我最初用Swin的时候就是把timm.create_model(swin_tiny_patch4_window7_224)跑起来了但并不知道相对位置偏置表是怎么初始化的。后来有一次在高分辨率数据集上微调发现加载预训练权重后loss不降反升排查了好久才定位到是偏置表插值的问题。那次之后我才认认真真把源码读了一遍才发现Swin真正精巧的地方不只在于窗口注意力还包括它在工程上做的那些不起眼的细节比如cyclic shift配合mask来保持效率比如相对位置偏置为了偶数和奇数窗口尺寸统一而做的小处理。如果你想深入理解Swin我建议你自己尝试用PyTorch从零实现一个简化版不用mask、不用cyclic shift的那种把它跑通后再加复杂版本。哪怕实现得不太规范但你亲手写完一遍之后再看Swin官方代码就会觉得非常顺畅。这个过程中你会发现Swin并没有特别玄乎的数学理论它更多的是把一个优秀的设计用工程手段高效地落地了这恰恰是它最值得学习的地方。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →