
1. 背景为什么 Transformer 成了计算机视觉的主流架构近年来计算机视觉领域的核心架构发生了一次明显迁移。早期做图像分类、目标检测大家默认使用卷积神经网络CNN从 AlexNet、VGG 到 ResNet、EfficientNet卷积核的堆叠和感受野设计几乎是视觉任务的标配。但从 2020 年开始Transformer 以极快的速度进入计算机视觉并逐渐从“能用”变成“好用”再到今天成为与 CNN 并列甚至在很多任务上更强的主流方案。要理解这件事得先回答一个问题Transformer 解决的是 CNN 的什么痛点CNN 的核心机制是局部感受野。每个卷积核只能看到输入图像的一小片区域要获得全局信息必须靠堆叠层数、扩大卷积核或使用空洞卷积。这种设计带来两个问题一是长距离依赖建模成本高二是对全局上下文的理解不够直接。而 Transformer 的 Self-Attention 机制在计算时每个位置可以直接和所有其他位置交互从第一层开始就能建立全局依赖关系。另一个推动力来自 NLP 领域的成功。Transformer 架构在机器翻译、文本分类、预训练语言模型上表现优异这让研究者开始思考如果图像也能像文本一样被“切”成一个个 token那么 Transformer 是不是也能直接在图像上工作答案就是 Vision TransformerViT。ViT 把图像划分成固定大小的 patch每个 patch 线性映射为向量再加上位置编码送入标准 Transformer Encoder在 ImageNet 上取得了超过同期 CNN 的成绩。随后 Swin Transformer 进一步引入层次化结构和移动窗口注意力让 Transformer 在检测、分割等密集预测任务上全面铺开。从应用场景来看Transformer 在计算机视觉中已经覆盖了图像分类、目标检测、语义分割、图像生成、视频理解、多模态感知等几乎所有视觉方向。对于准备做深度学习项目、写毕业论文、做竞赛或者想转算法岗的同学来说理解 Transformer 已经不仅仅是了解一个新模型而是理解当前视觉技术演进的主线。本文将以“论文精讲 代码实现”的方式系统梳理注意力机制、Transformer 核心原理、ViT 和 Swin Transformer 的关键设计与实现细节。全文包含可运行的 PyTorch 代码片段、维度推导和常见踩坑总结。无论你是刚入门计算机视觉还是已有 CNN 基础想迁移到 Transformer本文都值得收藏反复看。2. 注意力机制与 Transformer 核心原理解读2.1 什么是注意力机制“注意力”这个词源于人类视觉认知在看一张图片时我们不会均匀地关注所有区域而是会把更多注意力放在关键对象上。深度学习中的注意力机制在早期主要是指空间注意力或通道注意力例如 SE 模块Squeeze-and-Excitation就是典型的通道注意力通过全局池化获得通道描述向量再用两个全连接层学习通道权重。CBAM 则是在通道注意力的基础上增加了空间注意力分支。这些早期注意力模块的特点是它们是作为 CNN 的插件存在的作用是在特征图上重新加权并没有改变网络的基本计算范式。而 Transformer 中的 Self-Attention自注意力则完全不同它不是“对特征做加权”而是通过 Query、Key、Value 三个向量的交互动态地计算任意两个位置之间的关联强度并据此聚合信息。用一个通俗的例子来理解 Self-Attention假设你在一间会议室里听报告你Query会环顾四周看哪些人的发言内容Key和你的关注点最相关 然后你会重点听那些发言最相关的人Value最后根据听到的内容更新自己对这个话题的理解输出。在这个过程中所有位置之间都发生了交互没有任何位置的优先级是在计算前预设好的所有权重都由输入内容动态决定。这就是“自”注意力的含义注意力的来源和目标都是输入自身。2.2 Self-Attention 的计算过程Self-Attention 的输入是一个序列通常记为 ( X \in \mathbb{R}^{N \times D} )其中 ( N ) 是序列长度例如图片分块后的 token 数量( D ) 是特征维度。为了计算注意力我们需要对输入做三次线性变换得到三个矩阵Q X W_Q # Query形状 [N, D_q] K X W_K # Key形状 [N, D_k] V X W_V # Value形状 [N, D_v]注意这里的是矩阵乘法W_Q、W_K、W_V都是可学习的权重矩阵。在实际代码中通常用一个nn.Linear(D, D)来实现。接下来计算注意力分数。对于序列中的第 ( i ) 个位置它和其他所有位置 ( j ) 的关联分数为[ \text{score}_{i,j} Q_i \cdot K_j^T ]把整个序列放到一起就是[ \text{Attention}(Q, K, V) \text{softmax}\left(\frac{Q K^T}{\sqrt{d_k}}\right) V ]其中 ( d_k ) 是 Key 的维度除以 ( \sqrt{d_k} ) 是为了防止点积结果过大导致 softmax 梯度消失。用 PyTorch 实现一个最简单的 Self-Attention 模块import torch import torch.nn as nn import torch.nn.functional as F class SelfAttention(nn.Module): def __init__(self, dim): super().__init__() self.q nn.Linear(dim, dim) self.k nn.Linear(dim, dim) self.v nn.Linear(dim, dim) def forward(self, x): # x: [batch_size, seq_len, dim] batch_size, seq_len, dim x.shape Q self.q(x) # [batch_size, seq_len, dim] K self.k(x) V self.v(x) # 计算注意力分数 scores torch.matmul(Q, K.transpose(-2, -1)) / (dim ** 0.5) # scores: [batch_size, seq_len, seq_len] attn_weights F.softmax(scores, dim-1) out torch.matmul(attn_weights, V) return out这段代码虽然简洁但已经包含了 Self-Attention 的全部核心逻辑。实际使用中我们很少直接使用这样精简的实现而是会用多头注意力机制让模型从多个子空间中学习不同的关联模式。2.3 Multi-Head Attention 与 Transformer Encoder多头注意力Multi-Head Attention简称 MHA是 Transformer 的关键组件。它的思路很简单不把整个 ( D ) 维空间一次性做注意力而是把特征维度切成 ( h ) 个头每个头独立执行 Attention最后把结果拼接起来再经过一个线性层。这样做的好处是每个头可以关注不同的关系模式有的头关注局部纹理有的头关注全局结构。在计算复杂度不变的情况下增强了模型的表达能力。PyTorch 中可以直接调用官方实现multi_head_attn nn.MultiheadAttention(embed_dim256, num_heads8, batch_firstTrue)Transformer Encoder 的完整结构由以下几个部分组成多头自注意力子层。残差连接与层归一化LayerNorm。前馈网络Feed-Forward NetworkFFN通常是两个全连接层加 GELU 激活函数。第二个残差连接与层归一化。用公式表达[ \text{Attn_Out} \text{MultiHead}(X) X ][ \text{Attn_Out} \text{LayerNorm}(\text{Attn_Out}) ][ \text{FFN_Out} \text{FFN}(\text{Attn_Out}) \text{Attn_Out} ][ \text{Out} \text{LayerNorm}(\text{FFN_Out}) ]关于 LayerNorm 和 BatchNorm 的区别这里多说一句。BatchNorm 是在 batch 维度上做归一化依赖 batch 内其他样本的统计量在 batch size 较小时效果不稳定。LayerNorm 是对每个样本的特征维度做归一化不依赖 batch 大小因此更适合 Transformer 这种训练方式。这也是为什么 Transformer 结构不使用 BatchNorm 的重要原因。2.4 位置编码Transformer 怎么感知顺序Self-Attention 本身是不带顺序信息的。你可以把输入 token 的排列顺序任意打乱Attention 计算出的结果不受影响因为 Q 和 K 的点积是对称的。但在图像和文本中位置信息非常重要图像左上角的 patch 和右下角的 patch 语义显然不同。为了让 Transformer 感知位置需要把位置信息以某种方式注入到输入向量中。常见方案有两种绝对位置编码和相对位置编码。ViT 使用绝对位置编码直接学习一组可训练的位置向量与 patch embedding 相加self.pos_embed nn.Parameter(torch.zeros(1, num_patches 1, dim))Swin Transformer 使用相对位置编码它建模的是“两个 token 之间的相对偏移”这种编码方式在窗口注意力和层次化结构中表现更好。关于相对位置表的实现细节我们会在 Swin Transformer 章节详细展开。2.5 从 RNN 到 Transformer 的演进逻辑在 Transformer 出现之前序列建模主要依赖 RNN、LSTM、GRU 这类循环神经网络。RNN 的核心特征是按时间步递归计算当前时刻的隐状态依赖上一时刻的输出这就导致两个问题无法并行计算。因为每一步都依赖前一步GPU 的并行能力得不到充分利用。长距离依赖问题。虽然 LSTM 通过门控机制缓解了梯度消失但信息在长序列传递中仍会衰减。Transformer 从根本上改变了这种计算模式。它抛弃了递归结构所有位置的 Attention 可以同时计算从而大幅提升了训练效率。这也是为什么近几年的预训练大模型普遍采用 Transformer 架构而不是 LSTM。从 RNN 到 Transformer 的演进本质上是从“串行压缩”到“并行交互”的演进。理解了这一层你就能理解为什么 Transformer 能成为深度学习的基础设施。3. 环境准备与项目结构说明在进入 ViT 和 Swin Transformer 的代码实现之前先把开发环境准备好。本文的代码基于 PyTorch因为 PyTorch 的自动求导和动态图机制非常适合快速验证模型结构。3.1 开发环境以下是我使用的环境你可以根据自己的情况调整版本软件版本建议操作系统Ubuntu 20.04 / Windows 10 / macOSPython3.8 及以上PyTorch1.10 及以上推荐 2.xtorchvision与 PyTorch 对应版本CUDA11.3 及以上如果没有 GPU 可以用 CPU版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。3.2 安装依赖创建虚拟环境并安装 PyTorchconda create -n vit python3.9 conda activate vit pip install torch torchvision还可以安装一些辅助库pip install numpy matplotlib tqdm tensorboard3.3 项目结构本文的代码将按以下结构组织transformer-cv-tutorial/ ├── models/ │ ├── __init__.py │ ├── attention.py # 自注意力与多头注意力实现 │ ├── vit.py # ViT 模型实现 │ └── swin.py # Swin Transformer 核心模块实现 ├── train.py # 训练脚本 ├── inference.py # 推理脚本 └── README.md如果你只是想在本地快速跑通某个模块也可以直接用 Jupyter Notebook 一段一段执行不需要把项目结构搭得很完整。但如果你打算做完整实验建议按照上面的结构组织代码方便后续扩展和维护。4. ViTVision Transformer论文精讲与代码实现ViT 的论文全称是An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale由 Google 团队于 2020 年提出。这篇论文的核心贡献是证明了标准 Transformer Encoder 可以直接应用于图像分类不需要卷积操作只要在足够大的数据集上预训练效果就能超越 CNN。4.1 ViT 的核心思想ViT 的流程可以概括为四步将输入图像 ( x \in \mathbb{R}^{H \times W \times C} ) 划分成大小为 ( P \times P ) 的 patch。假设图像尺寸为 224×224patch 大小为 16×16那么一共得到 ( 14 \times 14 196 ) 个 patch。将每个 patch 展平并线性映射为维度为 ( D ) 的向量这个过程称为 Patch Embedding。在序列的最前面加一个可学习的 class token用于最终分类。同时为每个 token 加上位置编码。将整个 token 序列送入标准 Transformer Encoder取 class token 对应的输出做分类。用一张伪流程图来表示输入图像 224x224x3 │ ▼ 划分 Patch14x14 196 个 16x16 patch │ ▼ Patch Embedding每个 patch 映射为 1x768 向量 │ ▼ 拼接 Class Token序列变为 197x768 │ ▼ 添加位置编码序列仍为 197x768 │ ▼ Transformer EncoderL 层 │ ▼ 取 Class Token 对应输出 → 分类头为什么需要 class token因为在图像分类任务中我们需要一个表示“整张图像”的特征向量。Transformer 的每个 token 输出对应的是该 token 位置的编码结果并没有天然的全局表示。ViT 选择在输入序列最前面拼接一个可学习的向量 class token让它通过 Self-Attention 与其他所有 patch token 交互从而在输出端携带全局图像信息。4.2 Patch Embedding 与 Position EmbeddingPatch Embedding 的代码实现有两种常见方式方式一先把图像切分成 patch然后每个 patch 通过一个线性层。方式二直接用卷积核大小为 patch_size、步长为 patch_size 的 Conv2d 实现一步到位。方式二更高效因为卷积操作天然支持批量处理。下面是基于 Conv2d 的 ViT Embedding 层实现import torch import torch.nn as nn class PatchEmbed(nn.Module): 将图像转换为 Patch Embedding 序列。 使用 Conv2d 实现相当于对每个 patch 做线性映射。 def __init__(self, img_size224, patch_size16, in_chans3, embed_dim768): super().__init__() self.img_size img_size self.patch_size patch_size self.num_patches (img_size // patch_size) ** 2 # 核心kernel_size 和 stride 都等于 patch_size self.proj nn.Conv2d(in_chans, embed_dim, kernel_sizepatch_size, stridepatch_size) def forward(self, x): # x: [B, C, H, W] x self.proj(x) # [B, embed_dim, H/P, W/P] x x.flatten(2) # [B, embed_dim, num_patches] x x.transpose(1, 2) # [B, num_patches, embed_dim] return x位置编码的维度是[1, num_patches 1, embed_dim]加 1 是因为 class token。ViT 使用的是可学习的位置编码初始化方式可以全零、随机正态分布或者用 sine-cosine 初始化。实践中直接随机初始化然后训练即可效果差异不大。4.3 ViT Encoder Block 实现一个完整的 ViT Encoder Block 包含多头自注意力、前馈网络、残差连接和 LayerNorm。下面是完整实现class ViTBlock(nn.Module): def __init__(self, dim, num_heads, mlp_ratio4.0, dropout0.1): super().__init__() self.norm1 nn.LayerNorm(dim) self.attn nn.MultiheadAttention(dim, num_heads, dropoutdropout, batch_firstTrue) self.norm2 nn.LayerNorm(dim) mlp_hidden_dim int(dim * mlp_ratio) self.mlp nn.Sequential( nn.Linear(dim, mlp_hidden_dim), nn.GELU(), nn.Dropout(dropout), nn.Linear(mlp_hidden_dim, dim), nn.Dropout(dropout) ) def forward(self, x): # 多头自注意力 残差 x x self.attn(self.norm1(x), self.norm1(x), self.norm1(x))[0] # 前馈网络 残差 x x self.mlp(self.norm2(x)) return x注意到这里self.attn(...)返回的是一个元组第一个元素是注意力输出第二个是注意力权重。因此取[0]。虽然重复对norm1(x)调用了三次但它们的值是一样的实际操作中更推荐先计算一次再传入。4.4 完整 ViT 模型代码下面给出完整可运行的 ViT 模型class VisionTransformer(nn.Module): def __init__(self, img_size224, patch_size16, in_chans3, num_classes1000, embed_dim768, depth12, num_heads12, mlp_ratio4.0, dropout0.1): super().__init__() self.patch_embed PatchEmbed(img_size, patch_size, in_chans, embed_dim) num_patches self.patch_embed.num_patches # Class Token self.cls_token nn.Parameter(torch.zeros(1, 1, embed_dim)) # 位置编码num_patches 1 是因为加了 class token self.pos_embed nn.Parameter(torch.zeros(1, num_patches 1, embed_dim)) self.pos_drop nn.Dropout(dropout) # 堆叠 Transformer Encoder Block self.blocks nn.Sequential(*[ ViTBlock(embed_dim, num_heads, mlp_ratio, dropout) for _ in range(depth) ]) self.norm nn.LayerNorm(embed_dim) self.head nn.Linear(embed_dim, num_classes) # 权重初始化 self._init_weights() def _init_weights(self): nn.init.trunc_normal_(self.pos_embed, std0.02) nn.init.trunc_normal_(self.cls_token, std0.02) self.apply(self._init_module) def _init_module(self, m): if isinstance(m, nn.Linear): nn.init.trunc_normal_(m.weight, std0.02) if m.bias is not None: nn.init.zeros_(m.bias) def forward(self, x): B x.shape[0] # 1. Patch Embedding x self.patch_embed(x) # [B, num_patches, embed_dim] # 2. 拼接 Class Token cls_tokens self.cls_token.expand(B, -1, -1) x torch.cat((cls_tokens, x), dim1) # [B, num_patches1, embed_dim] # 3. 添加位置编码 x x self.pos_embed x self.pos_drop(x) # 4. Transformer Encoder x self.blocks(x) # 5. 取 class token 输出 x self.norm(x) cls_out x[:, 0] return self.head(cls_out) # 快速测试 if __name__ __main__: model VisionTransformer(img_size224, patch_size16, num_classes10) dummy torch.randn(2, 3, 224, 224) out model(dummy) print(ViT 输出形状:, out.shape) # 期望 [2, 10]你可以直接复制代码块运行。如果显存足够可以尝试增大 batch size。如果是在 CPU 上测试可以把num_classes改小、depth改小来加速。4.5 ViT 的关键结论与局限ViT 论文还有一个重要发现在中等规模数据集如 ImageNet-1k上直接训练ViT 的效果略低于同规模的 ResNet但在大规模数据集如 ImageNet-21k、JFT-300M上预训练后ViT 的效果可以反超 CNN。这个结论告诉我们要理性看待 ViTViT 具有很强的扩展能力数据量越大优势越明显。在中小规模数据集上ViT 需要更强的正则化手段如数据增强、DropPath才能收敛。ViT 的归纳偏置较弱。CNN 天生具备局部性和平移等变性而 ViT 需要从数据中学习这些特性因此在数据不足时容易过拟合。5. Swin Transformer 论文精讲与代码实现Swin Transformer 的论文全称是Swin Transformer: Hierarchical Vision Transformer using Shifted Windows由微软亚洲研究院提出。它解决了 ViT 在实际视觉任务中的两个痛点ViT 输出的是单一分辨率的特征图不利于目标检测、语义分割这类需要多尺度特征的密集预测任务。ViT 的全局自注意力计算复杂度是 ( O(N^2) )N 是 patch 数量当图像分辨率较高时计算量非常大。Swin Transformer 的思路是回到层次化结构同时引入窗口注意力把自注意力的计算范围限制在局部窗口内。5.1 层次化设计从低分辨率到高分辨率熟悉 CNN 的同学都知道ResNet 通过 Stage 逐级降低特征图分辨率、增加通道数形成从细到粗的特征金字塔。Swin Transformer 延续了这个设计思路。Swin Transformer 的输入通常是 224×224 或 384×384 的图像。网络分为 4 个 StageStage 1先通过 Patch Partition 将图像划分为 4×4 大小的 patchEmbedding 维度为 C。此时 feature map 大小为 ( \frac{H}{4} \times \frac{W}{4} )。Stage 2通过 Patch Merging 将 2×2 邻域的 patch 合并成一个分辨率减半通道数翻倍得到 ( \frac{H}{8} \times \frac{W}{8} ) 的特征图。Stage 3再做一次 Patch Merging得到 ( \frac{H}{16} \times \frac{W}{16} )。Stage 4再做一次 Patch Merging得到 ( \frac{H}{32} \times \frac{W}{32} )。每个 Stage 内部由若干个 Swin Transformer Block 组成。这样的设计使得 Swin Transformer 可以直接作为 Backbone 接入 FPN、Mask R-CNN 等检测分割框架。5.2 Patch Merging 实现Patch Merging 的操作类似于 CNN 中的下采样但它不是简单的池化而是把 2×2 邻域内的四个 patch 在通道维度上拼接起来然后通过一个线性层把通道数压缩为原来的两倍。class PatchMerging(nn.Module): def __init__(self, dim): super().__init__() self.norm nn.LayerNorm(4 * dim) self.reduction nn.Linear(4 * dim, 2 * dim, biasFalse) def forward(self, x, H, W): x: [B, H*W, C] H, W: 当前特征图的高和宽 B, L, C x.shape x x.view(B, H, W, C) # 把 2x2 邻域的 patch 切出来 x0 x[:, 0::2, 0::2, :] # 左上 x1 x[:, 1::2, 0::2, :] # 左下 x2 x[:, 0::2, 1::2, :] # 右上 x3 x[:, 1::2, 1::2, :] # 右下 # 在通道维拼接 x torch.cat([x0, x1, x2, x3], dim-1) # [B, H/2, W/2, 4C] x x.view(B, -1, 4 * C) x self.norm(x) x self.reduction(x) # [B, H/2 * W/2, 2C] return x5.3 窗口注意力与移动窗口机制Swin Transformer 最核心的创新是移动窗口注意力。先看普通窗口注意力将特征图划分为多个 4×4 或 7×7 的窗口每个窗口内部独立计算 Self-Attention。这样计算复杂度从全局 ( O(N^2) ) 降为 ( O(N_{window}^2 \times M) )其中 ( M ) 是窗口数量。由于窗口尺寸固定复杂度与图像分辨率近似线性关系。但是如果窗口完全固定不同窗口之间就没有信息交互模型只能建模局部关系无法捕获跨窗口的全局依赖。为了解决这个问题Swin Transformer 引入了一个非常巧妙的机制在相邻层之间移动窗口。具体来说第 ( l ) 层使用规则窗口划分第 ( l1 ) 层将窗口向右下各偏移 ( \lfloor \frac{M}{2} \rfloor ) 个 patch再重新划分窗口。这样一来原本两个窗口之间的边界发生了变化信息就可以通过层间传递跨窗口流动了。移动窗口的代码实现有一个细节如果用普通方式做移位后划分窗口会产生更多不规则的窗口例如左上角会多出多个小块不利于并行计算。Swin Transformer 的处理方式是使用torch.roll做循环移位把不完整的窗口填到完整位置然后计算时通过 mask 将不相关的区域屏蔽掉。这里给出一个简化的窗口分割代码示例def window_partition(x, window_size): 将特征图划分为窗口 x: [B, H, W, C] B, H, W, C x.shape x x.view(B, H // window_size, window_size, W // window_size, window_size, C) x x.permute(0, 1, 3, 2, 4, 5).contiguous() x x.view(-1, window_size, window_size, C) return x def window_reverse(windows, window_size, H, W): 将窗口还原为特征图 windows: [num_windows*B, window_size, window_size, C] B int(windows.shape[0] / (H * W / window_size / window_size)) x windows.view(B, H // window_size, W // window_size, window_size, window_size, -1) x x.permute(0, 1, 3, 2, 4, 5).contiguous() x x.view(B, H, W, -1) return x完整的 Window Attention 实现还需要加入相对位置编码的 bias 表和移动窗口的 attention mask代码相对复杂。下面我们重点讲解相对位置编码的设计细节。5.4 相对位置编码详解Swin Transformer 使用的不是绝对位置编码而是相对位置编码。它的基本思想是两个 patch 之间的注意力权重不仅取决于内容相似度还取决于它们之间的相对偏移。比如在一个 ( 2 \times 2 ) 的窗口内任意两个位置的相对偏移只有几种可能如下表所示相对偏移值域行偏移{-1, 0, 1}列偏移{-1, 0, 1}将行偏移和列偏移组合得到一个相对位置表表中每个元素都是一个可学习的标量。在计算注意力分数时将(Q K^T)的结果与相对位置 bias 相加再做 softmax。PyTorch 中实现相对位置编码的代码片段如下import torch import torch.nn as nn def get_relative_position_table(window_size, num_heads): # 相对位置偏移的范围 coords_h torch.arange(window_size) coords_w torch.arange(window_size) coords torch.stack(torch.meshgrid(coords_h, coords_w)) # [2, window_size, window_size] coords_flatten coords.flatten(1) # [2, window_size*window_size] # 计算相对偏移 relative_coords coords_flatten[:, :, None] - coords_flatten[:, None, :] # [2, window_size*window_size, window_size*window_size] relative_coords relative_coords.permute(1, 2, 0).contiguous() # [window_size*window_size, window_size*window_size, 2] # 将偏移转为非负索引 relative_coords[:, :, 0] window_size - 1 relative_coords[:, :, 1] window_size - 1 relative_coords[:, :, 0] * 2 * window_size - 1 relative_position_index relative_coords.sum(-1) # [window_size*window_size, window_size*window_size] # 可学习的相对位置偏置表 relative_position_bias_table nn.Parameter( torch.zeros((2 * window_size - 1) * (2 * window_size - 1), num_heads) ) return relative_position_index, relative_position_bias_table这里做一个简单解释为什么相对位置表的尺寸是 ( (2W - 1) \times (2W - 1) )因为在一个大小为 ( W ) 的窗口内两个位置的行偏移量取值范围是 ( -(W-1) ) 到 ( W-1 )一共 ( 2W-1 ) 种列偏移同理。两者组合一共 ( (2W-1)^2 ) 种相对位置组合。所以偏置表就是这么大。在注意力计算时通过relative_position_index从表中取出对应位置的 bias加到注意力分数上。5.5 Swin Transformer Block 完整实现一个完整的 Swin Transformer Block 由两个连续的子层组成基于窗口的自注意力W-MSA。基于移动窗口的自注意力SW-MSA。下面是核心代码框架class SwinTransformerBlock(nn.Module): def __init__(self, dim, num_heads, window_size7, shift_size0): super().__init__() self.dim dim self.num_heads num_heads self.window_size window_size self.shift_size shift_size self.norm1 nn.LayerNorm(dim) self.attn WindowAttention(dim, num_heads, window_size, shift_size) self.norm2 nn.LayerNorm(dim) self.mlp nn.Sequential( nn.Linear(dim, 4 * dim), nn.GELU(), nn.Linear(4 * dim, dim) ) def forward(self, x, H, W): # x: [B, H*W, C] # 第一层窗口注意力 shortcut x x self.norm1(x) x x.view(x.shape[0], H, W, -1) x self.attn(x, H, W) # 内部完成窗口划分、注意力、还原 x x.view(x.shape[0], H * W, -1) x shortcut x # 第二层前馈网络 shortcut x x self.norm2(x) x self.mlp(x) x shortcut x return x由于 SW-MSA 的实现涉及torch.roll和 attention mask 的生成代码量较大。完整实现可以参考官方仓库这里不再贴出所有代码但核心原理已覆盖移动窗口的目的是让信息跨窗口流动而 mask 的作用是保证移位后不相关的区域不会互相计算注意力。5.6 Swin Transformer 的模型变体Swin Transformer 官方提供了多个不同规模的模型常见的有模型通道数 C每个 Stage 的 Block 数量参数量Swin-T96[2, 2, 6, 2]约 28MSwin-S96[2, 2, 18, 2]约 50MSwin-B128[2, 2, 18, 2]约 88MSwin-L192[2, 2, 18, 2]约 197M以 Swin-T 为例它的结构参数为Patch size 为 4×4。窗口大小为 7×7。每个 Stage 的输出分辨率依次为 ( 56 \times 56 )、( 28 \times 28 )、( 14 \times 14 )、( 7 \times 7 )。6. 手写一个小型分类实验CIFAR-10 上的 ViT/Swin 对比理解了模型结构之后我们来做一个小型实验在 CIFAR-10 数据集上分别用 ViT 和 Swin Transformer 训练分类模型感受两者在中小规模数据上的表现差异。6.1 训练脚本代码下面以 ViT 为例给出完整的训练脚本。Swin 的改动只需替换模型实例即可。import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms from models.vit import VisionTransformer # 数据增强与加载 transform_train transforms.Compose([ transforms.RandomCrop(32, padding4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), ]) transform_test transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), ]) train_dataset datasets.CIFAR10(root./data, trainTrue, downloadTrue, transformtransform_train) test_dataset datasets.CIFAR10(root./data, trainFalse, downloadTrue, transformtransform_test) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers2) test_loader DataLoader(test_dataset, batch_size64, shuffleFalse, num_workers2) # 初始化模型 model VisionTransformer( img_size32, patch_size4, in_chans3, num_classes10, embed_dim192, depth6, num_heads4, mlp_ratio4.0, dropout0.1 ) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) # 训练循环 for epoch in range(30): model.train() total_loss 0 correct 0 total 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() scheduler.step() train_acc 100.0 * correct / total # 验证 model.eval() val_correct 0 val_total 0 with torch.no_grad(): for images, labels in test_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted outputs.max(1) val_total labels.size(0) val_correct predicted.eq(labels).sum().item() val_acc 100.0 * val_correct / val_total print(fEpoch [{epoch1}/30] Loss: {total_loss/len(train_loader):.4f} fTrain Acc: {train_acc:.2f}% Val Acc: {val_acc:.2f}%)6.2 运行结果预期在你本机运行这个脚本时30 个 epoch 的 ViT 实验结果大致在 65% 到 75% 的准确率区间。Swin Transformer 由于窗口注意力的局部归纳偏置更强同样的数据增强条件下通常能比 ViT 高出 2 到 5 个点。这也是两者在中小数据集上的真实差距Swin 的窗口机制带来了更强的局部先验所以对数据量的要求低于 ViT。如果你在自己的电脑上运行结果偏低或者偏高不要担心因为随机种子、数据增强细节、学习率策略都会影响结果关键是观察相对趋势。6.3 实验观察要点通过这个实验你应该能感受到几个问题Transformer 类模型对训练策略比较敏感学习率、weight decay、dropout 需要仔细调节。数据增强策略非常关键。ViT 在没有大量数据时容易过拟合RandAugment、MixUp、CutMix 等增强手段能让 Training 过程稳定很多。在 CIFAR-10 这种小尺寸图像上patch_size 的选取会影响性能。本文实验里 patch_size4 是可行的但 ViT 原论文中使用的 patch_size16 是直接针对 224×224 图像设计的如果直接搬到 32×32 图上效果会大打折扣。GPU 显存占用可以通过一个小实验快速验证。假设输入为 2 张 224×224 的图像depth12embed_dim768num_heads12 的 ViT 模型大概需要 800MB 左右显存。如果显存不足优先减小 depth、embed_dim 或 batch size。7. 常见问题与排查思路在学习和实际使用 Transformer 视觉模型的过程中会遇到很多高频报错。下面整理一份常见的排查清单按错误现象分类。问题现象常见原因解决思路运行时报错size mismatch for pos_embed预训练模型与当前输入分辨率不匹配位置编码维度不同修改输入尺寸或对预训练位置编码做插值显存溢出OOM全局自注意力计算量过大或 batch size 太大减小 batch size、减小图像分辨率、使用窗口注意力训练集准确率很高但验证集很低模型过拟合常见于 ViT 在中小数据集上增加数据增强、dropout、DropPath、减少模型深度损失不下降学习率过大或过小权重初始化问题使用 AdamW 小学习率1e-4 到 1e-3检查数据归一化Transformer 训练到一半报 NaN学习率过高导致的梯度爆炸或数据中存在异常值降低学习率增加梯度裁剪分割/检测任务使用 ViT 效果差ViT 输出单一分辨率特征图不适合密集预测使用 Swin Transformer 或加入 FPN推理时间过长全局注意力计算量大模型参数量大使用窗口注意力、模型蒸馏、ONNX 导出加速不同框架实现结果不一致LayerNorm 位置、激活函数、初始化细节有差异对照官方实现逐层检查输出维度与数值分布针对最常见的“预训练权重加载失败”问题下面给一个通用处理思路。ViT 预训练模型是在 224×224 图像上训练得到的如果你要迁移到 384×384 或 512×512 的高分辨率输入位置编码的序列长度会变。最简单的做法是把预训练的位置编码用双线性插值放大到新的尺寸def interpolate_pos_embed(model, new_num_patches): old_pos_embed model.pos_embed.data # [1, old_num, dim] old_num old_pos_embed.shape[1] if new_num_patches old_num: return # 去掉 class token 对应位置 old_cls old_pos_embed[:, :1, :] old_patch old_pos_embed[:, 1:, :].permute(0, 2, 1) # [1, dim, old_patch] # 计算需要插值的网格尺寸 old_h old_w int((old_num - 1) ** 0.5) new_h new_w int((new_num_patches - 1) ** 0.5) old_patch old_patch.view(1, -1, old_h, old_w) new_patch F.interpolate(old_patch, size(new_h, new_w), modebilinear, align_cornersFalse) new_patch new_patch.flatten(2).permute(0, 2, 1) # [1, new_patch, dim] model.pos_embed.data torch.cat([old_cls, new_patch], dim1)这个代码段体现了迁移学习中的常见操作你可以把它封装成一个通用函数在加载预训练模型时调用。8. 最佳实践与工程建议8.1 如何选择模型ViT 还是 Swin Transformer在实际项目中模型选型应该基于数据量和任务类型来做判断。如果你的任务是图像分类且数据集规模较大百万级或更高ViT 是一个值得尝试的方案因为它结构更纯粹在大规模数据上的扩展性非常优秀。如果你的数据集只有几万张甚至几千张直接用 ViT 基本会过拟合这时候 Swin Transformer 的窗口归纳偏置会更友好或者退一步直接用 ResNet 也是合理选择。如果你的任务是目标检测、语义分割、实例分割等需要多尺度特征的任务优先选 Swin Transformer。它的层次化结构和四个 Stage 输出可以直接替换 ResNet 作为 Backbone与 FPN、Mask R-CNN 的兼容性非常好。如果你的任务涉及高分辨率输入例如医学影像、卫星遥感图像窗口注意力的计算优势会更加明显。全局注意力在高分辨率下显存消耗极大而 Swin 的窗口大小是固定的计算量近似线性增长。8.2 训练 Transformer 视觉模型的调参经验基于实际项目经验可以给出以下几条具体建议。关于优化器推荐使用 AdamW 而不是普通 SGD。Transformer 类模型用 AdamW 收敛更稳定weight decay 默认设在 0.05 左右即可。学习率需要配合 warmup 策略使用先从一个较小的学习率线性上升到目标学习率再用余弦退火下降这种组合在视觉 Transformer 的训练中几乎是标配。关于数据增强不要只使用简单的 RandomCrop 和 RandomFlip。建议增加 RandAugment、MixUp、CutMix 中的一种或多种。这些增强手段对提升 ViT 的泛化能力非常显著在中小数据集上的效果甚至比调模型结构更明显。关于正则化除了 dropout 之外DropPath 是 Transformer 视觉模型中很常用的正则化手段。DropPath 会以一定概率随机丢弃整个残差分支让模型在训练时更难依赖单一路径从而提升泛化能力。关于模型初始化可以使用trunc_normal_初始化权重标准差通常取 0.02。这个细节在很多开源实现中保持一致但往往被忽略如果初始化不当模型训练早期可能出现不稳定的情况。8.3 工程部署与推理优化模型训练完后部署到生产环境时需要考虑推理速度和显存占用。常见优化手段包括将模型转换为 ONNX 格式使用 ONNX Runtime 推理引擎。使用 TensorRT 做 FP16 或 INT8 量化速度提升明显。在医疗、遥感等不需要严格实时性的场景可以考虑模型蒸馏用大模型蒸馏出小模型降低推理成本。调整窗口大小。Swin Transformer 的窗口大小是超参数如果显存不足可以尝试把窗口从 7 改为 5 或 4性能会有小幅下降但显存占用降低很多。8.4 安全与合规提醒在实际工程中使用预训练模型时要注意数据合规问题尤其是从第三方仓库下载权重时要确认模型的使用许可和训练数据来源。如果在医疗、金融等敏感领域使用视觉模型还需要关注数据隐私保护和模型可解释性不能把模型当黑盒直接上线。另外任何涉及用户数据、生产环境变更的操作都应该先在测试环境充分验证保持最小权限原则。9. 总结与后续学习路线本文围绕 Transformer 在计算机视觉领域的演进系统梳理了从注意力机制、Self-Attention 原理到 ViT 和 Swin Transformer 的完整知识链路。你可以跟着文中代码逐步手写每个模块理解维度变化和计算流程最终在 CIFAR-10 上跑通一个 Transformer 分类实验。从学习路线的角度建议按照以下顺序继续深入先弄懂标准 Transformer Encoder 的每一层结构和维度推导这是基础。手写 ViT 的 Patch Embedding、位置编码、分类头运行一个 mini 实验。理解 Swin Transformer 的窗口注意力和移动窗口机制重点搞清楚相对位置编码表是怎么构建的。尝试把 Swin Transformer 接到检测或分割框架中例如 Mask R-CNN、FPN。阅读 newer 的改进工作例如 Swin Transformer V2、DeiT数据高效 Transformer等。在具体任务中实践比如高光谱图像分类、遥感目标检测、多模态感知体会 Transformer 在不同输入模态下的适配方式。Transformer 在计算机视觉领域还在持续演进学习过程中不必追求读完所有论文而是要抓住两条主线一是对注意力机制本身的理解二是对模型如何适配图像特性的理解。前者解释了模型为什么有效后者解释了为什么不同任务要选不同架构。最后给你一个非常实用的建议不要只停留在看懂公式和代码一定要自己动手跑一次训练。你可以用本文的 ViT 代码在 CIFAR-10 上训练观察损失下降曲线和注意力可视化结果。你也能尝试修改 patch_size、depth、num_heads 这些超参数对比不同配置下的性能和训练速度。只有手写过 forward 中的每一步维度变化才能把 Transformer 从“看着懂”变成“真的懂”。