做 CV 这几年,Swin Transformer 是少数几个让我觉得“Transformer 还能这么改”的工作。最早拿 ViT 改检测实验时,固定单尺度特征图和高昂的全局计算成本让我头疼得不行,后来看到 Swin Transformer 的思路——把 Transformer 做成金字塔结构,再用滑动窗口把复杂度从平方量级拉回到线性增长,我才觉得这条路真正走通了。这篇文章我用做实验的视角把 Swin Transformer 拆开讲:它到底在解决什么问题,四个关键技术点怎么起作用的,核心模块怎么复现,以及我在实际训练和落地中踩过的坑。不管你是刚开始接触 Transformer 结构,还是已经会用开源模型库但没细看内部实现,这篇文章应该都能给你一份可以直接动手参考的“说明书”。
1. Swin Transformer 在解决什么问题
1.1 从 ViT 说起:固定分辨率与平方级复杂度
要理解 Swin Transformer,得先回到 Vision Transformer(ViT)。ViT 的核心操作很直接:把一张图切成固定大小的 patch,比如 16×16,拉平成 token 序列,然后直接丢进标准的 Transformer Encoder 里做全局自注意力。这里的“全局”很关键,每个 token 都要跟序列里的所有 token 计算相似度,序列长度是 h×w 时,注意力部分的时间复杂度基本是 O(h²w²)。图像分辨率一大,这个计算量涨得很快,一张 224×224 的图切成 16×16 patch,得到 14×14=196 个 token,还算能接受;一旦输入变成 512×512,token 变成 1024 个,全局注意力就要处理一百多万对关系,训练直接吃力。
ViT 还有第二个硬伤:它输出的是固定 stride 的特征图。比如 patch 16 时,最终只有一个 14×14 分辨率的特征图,这对分类问题够用,但检测、分割这类密集预测任务需要多尺度特征。目标检测里的特征金字塔(FPN)要求不同层提供不同语义和分辨率的特征,语义分割也要恢复空间细节。单尺度特征让 ViT 作为骨干网络显得很别扭,除非额外加一堆上采样和特征融合模块,否则没法直接复用到检测分割的常规框架里。
当时我做目标检测实验时,把 ViT 塞进检测框架里,backbone 输出的特征图就一档,FPN 各层要么靠上采样硬凑,要么就只用某一层,性能总是差口气。要说 ViT 没有优点也不公平,它对长距离建模能力强,在大规模数据上预训练效果很好,但直接在中等规模数据集上从零训练,收敛速度也明显比 CNN 慢。Swin Transformer 的设计初衷就是把 ViT 的长处和 CNN 的层次化结构结合起来,解决这两个核心问题。
1.2 Swin 的破局思路:向 CNN 的层级化看齐
Swin Transformer 的名字来自 Shifted Window,也就是“移动窗口”。它最本质的改动是:把自注意力限制在局部窗口内进行计算,并通过窗口移动机制让不同窗口之间产生信息交互,然后像 CNN 一样逐阶段合并 token,形成从高分辨率到低分辨率的多层级特征。
这样做的好处可以直接对应前面提到的痛点。
计算复杂度上,窗口自注意力让复杂度从全局的 O(h²w²) 降到了 O(M²hw),这里的 M 是窗口边长,一个常数。我算过一笔账:输入 224×224 的图,patch size 为 4,token 数为 56×56=3136,embedding 维度取 96。标准全局注意力在 QK 矩阵相乘这一步的复杂度大约是 2×3136²×96,约 18.9 亿次运算量;而窗口注意力把注意力限制在 7×7=49 个 token 的窗口内,同一个阶段的复杂度大约是 2×49×3136×96,只有约 2950 万次,差了快两个数量级。这也是为什么 Swin Transformer 能用标准显存直接训 224 分辨率甚至更大输入。
层级化结构方面,Swin 最初的 patch 是 4×4,输出特征图是输入的 1/4。之后每个阶段通过 Patch Merging 把相邻的 2×2 patch 合并成一个,特征图分辨率依次变成 1/8、1/16、1/32。这样的输出结构和 ResNet 的 stage 对齐,直接能适配 FPN、UperNet 这些常见的检测分割框架。我在实际项目中甚至把 Swin 的四个 stage 特征直接喂给现成的 FPN,不需要改任何东西,这在 ViT 上是做不到的。
所以 Swin Transformer 本质上是在“Transformer 主干网络应该长什么样”这个问题上交了一份更贴近 CV 习惯的答卷:局部先粗后细,层级递进,计算可控,语义和空间兼得。
2. 四个关键技术点逐个拆
2.1 Patch 划分与 Embedding:先降分辨率再进 Transformer
Swin 的第一步和 ViT 类似,也是把图片切 patch,但切得更细。patch size 是 4×4,也就是说每个 patch 覆盖原图上 4×4=16 个像素。以 224×224 输入为例,切完后得到 56×56 个 patch,这比 ViT 的 14×14 要密得多,空间细节保留得也更好。
每个 patch 内的像素通道会被拉平,比如 RGB 三通道,每个 patch 是 4×4×3=48 维的向量,然后通过一个线性层映射到 embedding 维度 C。这一步在实现里通常是个 Conv2d(kernel_size=4, stride=4),它把输入从 [B, 3, 224, 224] 直接变成 [B, C, 56, 56]。用卷积实现不是为了花哨,而是因为 patch 切分和卷积操作完全等价,而且 GPU 上卷积高度优化,效率很高。之后拉平成 [B, 56×56, C] 就能进 Transformer 了。
需要注意,因为 patch size 是 4,模型要求输入图像的长宽都能被 4 整除;如果图像尺寸不是 4 的倍数,最后的这些像素会被直接丢弃,造成奇怪的边界效应。我试用 Swin 做自定义数据集时,第一件事就是把所有图 resize 成 4 的倍数再进网络,省掉很多 debug 时间。后续每个阶段还会继续降分辨率,所以更严格地说,输入尺寸最好能被 4×2×2×2×2=64 整除,虽然不能整除也能跑,但最后几个阶段的特征图会被截断,我在训练时观察到过这种问题:模型一样收敛,但验证指标会比预期低 0.5 个点左右。
2.2 W-MSA:窗口内自注意力如何降低计算量
W-MSA 就是窗口多头自注意力,全称 Window Multi-head Self-Attention。做法是把特征图均匀划分成若干不重叠的窗口,每个窗口大小固定为 M×M,比如 7×7,然后在每个窗口内部独立计算标准自注意力,窗口之间没有信息流动。
对于一张 56×56 的特征图,切分成 7×7 窗口后,总共得到 8×8=64 个窗口,每个窗口 49 个 token。每个窗口的注意力计算就是标准的 QKV 流程:Query 和 Key 做点乘得到 49×49 的注意力矩阵,再对 Value 做加权求和。由于每个窗口只有 49 个 token,矩阵很小,计算量相比全局注意力缩小了很多。
这里的 M=7 不是随便定的,和输入尺寸 56 正好整除,这样窗口划分干净利落。如果输入不是窗口大小的整数倍,就需要填充或者边界处理,Swin 在论文里用的是把特征图 pad 到能整除的尺寸,在实现上也就是多了几行索引操作。我在复现时试过 M=8,显存略有上升,精度变化不大,但和默认参数比也没有优势,就放弃了。
窗口自注意力还有一个容易被忽视的好处:它天然带有一定的局部归纳偏置,类似于卷积的感受野。这能让 Transformer 在小规模数据集上也能训得动。我做实验时从零训练一个 Swin-T 在几万张图的小数据集上,最终精度比同等参数量的 ViT 高出不少,原因就是窗口结构提供了更强的空间先验。
2.3 SW-MSA:移位窗口为什么能打通信息孤岛
如果所有层都用 W-MSA,每个窗口内部各玩各的,信息永远无法跨窗口传播,这就和卷积网络里各通道互不通信一样是个大问题。Swin 的解法是在相邻的 Transformer Block 里交替使用两种注意力:奇数层用 W-MSA,偶数层用 SW-MSA,即移位窗口多头自注意力。
SW-MSA 的做法很直白:把窗口划分整体向左上角平移 M/2 个 patch,这样一来,原本位于不同窗口边界的 token 在下一层就会落到同一个窗口里。例如第 1 层窗口边界处的 token,第 2 层移位窗口后会被窗口内部的其他 token 关注到,通过两层堆叠,信息就能跨窗口传播。我在讲解时经常用这个比喻:就像你坐在教室第一排,本来只能看自己小组的人,但下一次重新分组,把相邻两组的成员混进来,人际网络一下就打通了。
实现上有个效率问题:简单地把特征图整体移位再重新划分窗口,会导致窗口数量增加,因为移位后四周多出一些不完整区域。Swin 用了 cyclic shift 加 mask 的技巧:先把特征图按对角线方向循环滚动,把不完整的区域拼接到对面,让窗口数量保持和原来一致,再用注意力掩码把不应该相互看到的 token 挡住。这个掩码矩阵形状是 num_windows×M²×M²,在训练前按公式算好,运行时直接作为 bias 加到注意力分数上,不让 mask 区域参与 softmax。这个设计我第一次看的时候觉得绕,但真自己实现一遍就明白了,它是纯工程上的优化,让移位操作不增加任何计算上的额外开销。
我在代码里同时实现了 naive 的移位窗口和带 mask 的优化版本,两者输出完全一致,但 naive 因为要用 padding 凑出多窗口,速度慢约 20%,显存也高一些。在正式训练里我都是用优化版本,理解原理后调试也更容易。
2.4 相对位置偏置:Swin 的几何先验
窗口注意力解决计算量问题后,还有一个细节决定质量:如何编码位置信息。ViT 用的是绝对位置编码,每个 token 有一个绝对的位置向量,加到 embedding 上。Swin 没有照搬这套做法,而是用了相对位置偏置(Relative Position Bias):在计算注意力分数时,根据 query 和 key 之间的相对位置加上一个可学习的偏置项。
具体来说,对于窗口内任意两个 token,先算出它们在横纵方向的相对位置偏移,范围是 -(M-1) 到 M-1,然后映射到一个大小为 (2M-1)×(2M-1)×num_heads 的偏置表里,取出对应的值加到注意力分数上。这个偏置是可学习的参数,初始化为 0,训练时不断更新。为什么不用绝对位置编码?一个很重要的原因是泛化性。绝对位置编码在训练分辨率固定时往往和分辨率绑定得比较紧,换更大分辨率时需要插值;相对位置偏置只依赖于 token 之间的相对偏移,分辨率变化时窗口内部相对位置关系基本不变,迁移能力更强。平移不变性也更好,这更接近 CNN 的归纳偏置。
我在迁移到高分辨率数据时体会特别明显。模型在 224×224 上训练好,直接拿到 384×384 上做检测,Swin 几乎不需要重新训练太多轮就能稳住,因为窗口内的相对位置关系没变,只有特征图变大了而已。而用 ViT 时,绝对位置编码一旦插值不做好,loss 直接飙升。这也是我后来在项目中优先选择 Swin 做基座的一个原因。
3. 动手复现核心模块
3.1 从整体结构到 Stage 配置
动手写代码之前,先把整体结构画个大概。Swin Transformer 通常有四个 Stage,每个 Stage 包含一个 Patch Merging 层(第一个 Stage 前面是 Patch Partition)和若干个 Swin Transformer Block。每个 Block 由两个连续的自注意力层组成:一个 W-MSA,一个 SW-MSA,中间带 MLP 和 LayerNorm,整体是标准 Transformer 的外层残差结构,但 MLP 的扩展因子是 4。四个 Stage 输出特征图的分辨率依次是输入的 1/4、1/8、1/16、1/32,通道数依次增大,典型配置如下表所示。
| 模型配置 | 嵌入维度 C | 各 Stage Block 数 | 各 Stage 注意力头数 | 参数量 | 吞吐量参考 |
|---|---|---|---|---|---|
| Swin-T | 96 | [2, 2, 6, 2] | [3, 6, 12, 24] | 约 28M | 中 |
| Swin-S | 96 | [2, 2, 18, 2] | [3, 6, 12, 24] | 约 50M | 中低 |
| Swin-B | 128 | [2, 2, 18, 2] | [4, 8, 16, 32] | 约 88M | 低 |
| Swin-L | 192 | [2, 2, 18, 2] | [6, 12, 24, 48] | 约 197M | 极低 |
我做实验时一般从 Swin-T 起步,因为训练快、显存友好,验证思路用 T,最后做结果时再上 Swin-B 或 Swin-L。timm 这类开源模型库里有完整实现,但我建议至少自己手写一遍核心注意力模块,否则遇到分辨率变化、微调异常时完全不知道去哪查。
3.2 窗口注意力与移位窗口的简化实现
这里我贴两个核心模块的简化实现,代码风格我尽量贴近我日常调试用的版本,注释都写在关键位置。
import torch import torch.nn as nn class WindowAttention(nn.Module): """窗口内部的多头自注意力,带相对位置偏置""" def __init__(self, dim, num_heads, window_size): super().__init__() self.num_heads = num_heads self.window_size = window_size # (M, M) self.dim = dim # 可学习的相对位置偏置表,大小 (2M-1) x (2M-1) x num_heads self.relative_position_bias_table = nn.Parameter( torch.zeros((2 * window_size[0] - 1) * (2 * window_size[1] - 1), num_heads) ) # 预计算窗口内每个位置对的相对位置索引 coords_h = torch.arange(window_size[0]) coords_w = torch.arange(window_size[1]) coords = torch.stack(torch.meshgrid(coords_h, coords_w, indexing="ij")) # 2, M, M coords_flatten = coords.flatten(1) # 2, M*M relative_coords = coords_flatten[:, :, None] - coords_flatten[:, None, :] # 2, M*M, M*M relative_coords = relative_coords.permute(1, 2, 0).contiguous() # M*M, M*M, 2 # 把 [-M+1, M-1] 映射到 [0, 2M-2],再合并成单索引 relative_coords[:, :, 0] += window_size[0] - 1 relative_coords[:, :, 1] += window_size[1] - 1 relative_coords[:, :, 0] *= 2 * window_size[1] - 1 relative_position_index = relative_coords.sum(-1) # M*M, M*M self.register_buffer("relative_position_index", relative_position_index) self.qkv = nn.Linear(dim, dim * 3, bias=True) self.proj = nn.Linear(dim, dim) def forward(self, x): """ x 形状: (num_windows*B, M*M, dim) """ B_, N, C = x.shape qkv = self.qkv(x).reshape(B_, N, 3, self.num_heads, C // self.num_heads) qkv = qkv.permute(2, 0, 3, 1, 4) # 3, B_, heads, N, head_dim q, k, v = qkv[0], qkv[1], qkv[2] attn = (q @ k.transpose(-2, -1)) * (C // self.num_heads) ** -0.5 # 取出对应相对位置偏置并加到注意力分数上 relative_position_bias = self.relative_position_bias_table[self.relative_position_index.view(-1)].view( N, N, self.num_heads ).permute(2, 0, 1) attn = attn + relative_position_bias.unsqueeze(0) attn = attn.softmax(dim=-1) x = (attn @ v).transpose(1, 2).reshape(B_, N, C) x = self.proj(x) return x上面的 WindowAttention 是核心,但真正的 Shifted Window 需要在窗口划分之前对特征图做循环移位,再给注意力加 mask。Mask 矩阵我一般用一个单独函数算好存下来,避免每个 batch 反复计算:
def compute_shifted_mask(H, W, window_size, shift_size): img_mask = torch.zeros(1, H, W, 1) h_slices = (slice(0, -window_size[0]), slice(-window_size[0], -shift_size), slice(-shift_size, None)) w_slices = (slice(0, -window_size[1]), slice(-window_size[1], -shift_size), slice(-shift_size, None)) cnt = 0 for h in h_slices: for w in w_slices: img_mask[:, h, w, :] = cnt cnt += 1 mask_windows = window_partition(img_mask, window_size) # nW, M, M, 1 mask_windows = mask_windows.view(-1, window_size[0] * window_size[1]) attn_mask = mask_windows.unsqueeze(1) - mask_windows.unsqueeze(2) attn_mask = attn_mask.masked_fill(attn_mask != 0, float(-100.0)).masked_fill( attn_mask == 0, float(0.0) ) return attn_mask这段代码的意图是:对不同移位区域编号,落在不同区域的 token 对之间在注意力矩阵里被置为 -100,softmax 后权重基本归零。这样循环移位后虽然物理位置上不完整的窗口被拼到了一起,但注意力仍然只在应该看的区域之间计算,结果和朴素移位窗口完全一致,窗口数量却保持不变。
3.3 训练配置参考
模型写完之后,训练超参数和配置对最终效果的影响不亚于模型结构本身。我自己的默认配置参考如下,基于常用实验经验,适合中等规模图像分类数据集。
| 超参数 | 参考值 | 说明 |
|---|---|---|
| 优化器 | AdamW | 对 Transformer 类结构比 SGD 稳 |
| 基础学习率 | 1e-3(batch 1024 时) | 按 batch 缩放:目标 batch 除以 1024 再开根号 |
| warmup 轮数 | 20 epochs | Swin 对学习率敏感,不能省 |
| weight decay | 0.05 | 偏大一点有助于收敛 |
| label smoothing | 0.1 | 提升泛化,尤其数据不足时 |
| 数据增强 | RandomResizedCrop + Mixup + CutMix | 是训练大模型的基本盘 |
| 输入分辨率 | 224×224 | 后续微调阶段可上调到 384 |
我踩过一个特别实在的坑:Swin 的 LayerNorm 对学习率非常敏感,warmup 一旦省掉,前几十个 step loss 就往上飘,后面再拉回来要浪费很多时间。换 Swin-B 或更深模型时,warmup 轮数还得再加。
4. 落地应用与微调技巧
4.1 图像分类场景怎么用
分类是最直接的用法。Swin 四个阶段做完之后,最后一个 1/32 分辨率的特征图接一个全局平均池化,再经过一个全连接层输出类别 logits。我用开源预训练权重做下游分类任务微调时,通常会把最后的全连接层换成随机初始化,分类头学习率设为 backbone 的 10 倍,这样新类别的梯度更新速度更合理。
微调时有个参数值得单独提:drop path(随机深度)。Swin 里大量依靠 drop path 做正则,预训练权重默认带了 drop path rate,比如 Swin-T 是 0.1,Swin-B 是 0.3 左右。下游数据较少时我一般不改这个参数;但数据很多,比如几千万张时,我会把 drop path 往低调一点,防止欠拟合。这个调起来没有标准答案,只能看验证集表现。
如果是从零训练而不是微调,我的建议是不要上来就训 224 分辨率。先用小尺寸比如 128 跑通流程,确认 loss 在合理下降,再切到 224。因为 Swin 的窗口机制和分辨率强相关,小尺寸训练能很快暴露实现错误,比如 patch 分组不对、位置索引越界等,跑个几百 step 就能看出来。
4.2 检测与分割:把 backbone 接进现有框架
Swin 在检测分割任务里做骨干网络,最大优势是四个 stage 的特征图和 FPN、UperNet 天然匹配。我自己在检测任务里的接法是这样:
- stage 1 输出(1/4 分辨率)送给 FPN 的高分辨率层
- stage 2 输出(1/8)作为次高层输入
- stage 3 输出(1/16)是 FPN 的主要层
- stage 4 输出(1/32)作为顶层
FPN 需要 backbone 各层通道数固定,Swin 的通道数本来就是逐 stage 递增的,接一个 1×1 卷积对齐通道就行。检测头我用 Cascade Mask R-CNN 时,Swin-B 相比同量级的 CNN 骨干能稳定涨点,尤其是小物体和边缘目标的检出率。分割任务里我试过 UperNet 作为解码器,输入 512 分辨率时显存占用比 CNN 骨干高不少,但是 mIoU 的提升是实打实的。
迁移时有一个关键点:预训练分类模型不做任何改动去跑检测,后三个 stage 的相对位置偏置表会在检测的更高分辨率输入下被反复使用,注意不要重复加载预训练模型中的位置索引 buffer,那些是根据原分辨率预计算的,到新分辨率后需要重新算。我见过有人直接复用旧索引导致简直效果崩掉的情况,排查了很久才发现是这个细节。
4.3 分辨率迁移和预训练权重转换
Swin 对分辨率变化比较友好的原因,我在前面提到过:它用的是相对位置偏置而不是绝对位置编码。但也不是完全不用管。比如预训练在 224×224,你要在 384×384 上做检测,窗口大小还是 7,只是特征图从 56×56 变成 96×96,窗口数量从 64 变成 169 个左右。窗口内部的相对位置偏置表完全不用动,因为每个窗口内的相对位置关系没变。
Patch Embedding 也几乎不用动,它的步长是 4,对任意 4 的倍数分辨率都能工作。唯一要处理的是各个 stage 的分辨率奇偶问题:如果特征图边长不是两个 patch 合并的整数倍,Patch Merging 会丢掉最后一个 patch,导致 FPN 的对应层出现尺寸不一致,这种情况我通常会额外 padding 或调整输入尺寸。总之,我的经验是检测任务里输入分辨率直接翻倍基本能跑,但别忘了重新设置训练轮数和学习率,不要在预训练模型上强行微调太多轮。
还有一点,如果拿不到官方预训练权重,自己从零预训练再微调是可行的,但成本很高。我自己做过一次“先用分类数据集预训练 Swin-T,再拿去做检测”的路子,在中等数据量下和直接用开源权重微调相比,性能差了 1 到 2 个点。所以条件允许时,直接加载在更大数据集上训练好的权重是最省事也最稳的路线。
5. 常见问题与排查技巧实录
5.1 训练不收敛或收敛很慢
这个是最多人问的问题之一。Swin 和 CNN 不同,它对学习率、warmup、数据增强都比较敏感。我在一次项目中帮 A 同学排查过,他的 Swin-T 在小数据集上训练,loss 前 50 个 step 不降反升,最后发现是初始学习率设成了 5e-3,而 batch 只有 64,scale 后实际 lr 太高,warmup 又只设了 3 个 epoch。调成 1e-3 左右,warmup 拉到 20 epoch 之后,第 50 个 step loss 就正常下降了。
如果 loss 下降了但验证集精度一直上不去,优先检查数据增强。Swin 的参数量不小,中等数据集上不过强正则基本必过拟合。Mixup 和 CutMix 要一起开,drop path 也要按模型规模设置。我评估过一个特定数据集的对比:开启全套增强后验证精度提升了接近 3 个点,这个幅度很多时候比换模型还大。
还有一个隐蔽问题:LayerNorm 统计的是每个 token 自身的归一化统计量,和 batch 大小关系不像 BatchNorm 那么大,但如果混用了 BatchNorm 和 LayerNorm,会导致训练不稳定。Swin 内部全部是 LayerNorm,我建议不管接什么下游头,都尽量保持一致,避免引入额外归一化层。
5.2 显存 OOM 和推理速度慢
很多人以为窗口注意力已经足够省,但实际训练 Swin 还是会 OOM。原因在于注意力模块只是 Transformer Block 的一部分,MLP 的中间维度是 embedding 的 4 倍,这一层显存占用同样不小。我处理 OOM 的顺序一般是:
- 开梯度检查点(activation checkpointing),用一点计算换显存,通常能省 30% 以上;
- 减小 batch size,配合梯度累积保证等效 batch 不降;
- 混合精度训练,fp16 下显存和速度都有改善;
- 换更小的模型配置,Swm-T 换 Swin-S 这种只降一档。
推理速度慢的问题,在低分辨率输入上尤其常见。窗口自注意力的优势在高分辨率大 token 数时最明显,如果输入只有 128×128,全局注意力也没多大,窗口切换和 mask 生成反而成了额外开销。所以做实际项目时,我会先测一版纯 CNN 的小模型做 baseline,再上 Swin,如果任务本身是小图分类,Swin 未必是性价比最高的选择。
5.3 位置索引、尺寸整除这类低级错误
相对位置索引是手写实现最容易出 bug 的地方。我最初复现时,relative_position_index 算错了一个 step,注意力偏置就全乱了,但训练 loss 仍然在降,只是精度低了几个点,特别难察觉。排查方式很简单:把索引矩阵打印出来,检查是不是覆盖了 0 到 (2M-2)²-1 的全部整数,并且对角线位置的相对位置应该完全相同。
输入尺寸必须能被 patch size 和合并倍数整除这件事我也栽过跟头。某次分割任务里我直接把原始图 512×288 丢进去,最后 stage 特征图是 8×4.5 这种带小数的情况,框架直接报错。后来我把输入统一缩放到 512×320,问题立刻消失。经验是:先用能被 64 整除的尺寸,处理不了的图就用 padding,不要硬进网络。
5.4 一个能力很接近的替代方向:MLP-Mixer、ConvNeXt 等结构适合对比
排查问题期间我经常顺手对比同一任务下其他结构的表现。Swin 不是唯一的 Transformer 式 backbone,后来的全局注意力设计、CNN 改进设计在某些场景下也确实更快。但从我自己的使用体验看,Swin 最稳定的特点是层级结构和窗口机制的配合,上手简单、文档多、下游框架适配成熟。如果只是做实验验证想法,Swin-T 跑得快还好调;如果做产品级项目且希望兼顾效率和效果,我会在 Swin-B 和最新 CNN 改进结构之间做一轮对比再定。
以下把高频问题整理成速查表:
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| loss 上升或震荡 | lr 过高 / warmup 不足 | 检查 lr 缩放,warmup 至少 10 epoch |
| 验证精度低 | 数据增强不足 / drop path 过低 | 打开 Mixup、CutMix、label smoothing |
| 换分辨率后性能骤降 | 位置索引或 mask 未重算 | 打印相对位置索引和 attn_mask 形状 |
| 显存溢出 | MLP 占用大 / 输入太大 | 开 checkpointing,降 batch,用混合精度 |
| 训练速度慢 | 小图上窗口优势不明显 | 对比 CNN baseline,考虑模型容量 |
| 分割/检测结果少几个点 | 预训练权重没对齐 / 尺寸被截断 | 检查输入整除,确认预训练模型配置一致 |
写在最后的个人体会
我自己实际用下来的感受是,Swin Transformer 最好的定位不是“替代 CNN”,而是给那些需要高分辨率、多尺度、强表达能力骨干网络的任务提供了一条更顺的路。分类任务里它能打,但优势不是压倒性的;检测分割任务里,尤其是需要 FPN 多尺度特征和密集预测时,它才真正体现出结构设计上的价值。
后来我养成了一个固定的工作流:拿到新任务先不换模型,先用 Swin-T 在低分辨率上跑通整个流程,确认 loss 和验证指标都对,再切到 Swin-B 调分辨率调增强。另外一个小技巧是,微调时先固定 backbone 跑 5 到 10 轮,让分类头或者检测头稳定下来,再解冻 backbone 一起训。这个做法帮我躲过了好几次“头没学好、骨干也被带偏”的窘境。如果你现在正准备拿 Swin Transformer 做实验,希望这篇文章能让你少走几步弯路;遇到问题的时候,回头看看自己的窗口划分和位置索引,多半答案就在那里。