
Transformer 系列在计算机视觉里已经不是什么新技术但很多同学学到这里容易卡住注意力机制公式看得懂代码跑不通ViT 论文读完了不知道 Patch Embedding 为什么那样设计Swin Transformer 的移位窗口更是一看到图就发懵。这篇文章把 Transformer 计算机视觉这条线重新捋一遍从自注意力机制到 ViT再到 Swin Transformer讲清楚每一层设计要解决什么问题最后给出可落地的实现思路和测试建议。全文按“核心概念 - 公式拆解 - 架构演进 - ViT - Swin - 代码实现 - 对比分析 - 工程建议”展开适合正在入门视觉 Transformer、或者准备课程作业、期末项目、毕业设计的同学直接收藏。1. 核心概念速览概念一句话理解典型应用位置自注意力机制让序列中每个元素根据其他元素动态计算权重Transformer Encoder / ViT 编码器多头注意力多组注意力并行计算捕获不同子空间关系Transformer 编码器、ViT Block位置编码给无顺序的注意力机制补充位置信息ViT 位置嵌入、Transformer PEPatch Embedding把图像切块并映射为 token 向量ViT 输入阶段CLS Token额外加入的类别标记用于分类输出ViT 分类头分层特征多尺度特征金字塔适应目标检测、分割Swin Transformer移位窗口注意力在窗口内计算注意力并跨窗口交互Swin Transformer Block相对位置编码只用 token 间相对偏移量编码位置Swin Transformer、部分 ViT 变体从材料看这套知识体系适合三类读者正在做计算机视觉课程作业或期末项目的同学需要快速掌握 Transformer 核心概念并落地实验。想入门 ViT、Swin Transformer 并复现论文结果的研究生。准备从事视觉算法工程师岗位的人需要补上 Transformer 基础认知。需要注意Transformer 在视觉里不是万能的。ViT 在小规模数据集上不如 ResNetSwin 结构更复杂但换来了通用性。下面按主线逐步拆解。2. 注意力机制从 RNN 到自注意力的演进2.1 为什么视觉任务需要注意力机制传统 CNN 靠卷积核做局部感知。Conv 的感受野是有限的想看到更大区域只能堆层数或加大卷积核。RNN 在序列建模时按时间步逐步推理无法并行长距离依赖容易被稀释。注意力机制解决的是“选择性聚焦”问题。在计算某个位置的特征时不是只看相邻区域而是让模型在所有位置之间动态分配权重。对于图像来说某个像素/区域的重要程度由它与其他区域的关系共同决定。2.2 自注意力核心公式自注意力Self-Attention的标准公式来自《Attention Is All You Need》Attention(Q, K, V) softmax(Q * K^T / sqrt(d_k)) * V其中QQuery表示“当前 token 想问什么”。KKey表示“其他 token 能提供什么”。VValue表示“其他 token 携带的内容”。d_k 是 Q/K 的维度除以 sqrt(d_k) 是为了防止点积过大导致 softmax 梯度消失。可以按三步理解每个 token 生成 Q、K、V 三个向量。计算所有 token 两两之间的相关性权重。用权重对所有 Value 做加权求和得到新的特征表示。对应 PyTorch 实现思路import torch import torch.nn as nn import torch.nn.functional as F class SelfAttention(nn.Module): def __init__(self, dim, d_kNone): super().__init__() self.dim dim self.d_k d_k if d_k is not None else dim self.w_q nn.Linear(dim, self.d_k) self.w_k nn.Linear(dim, self.d_k) self.w_v nn.Linear(dim, self.d_k) def forward(self, x): # x: [batch, seq_len, dim] Q self.w_q(x) K self.w_k(x) V self.w_v(x) scores torch.matmul(Q, K.transpose(-2, -1)) / (self.d_k ** 0.5) attn F.softmax(scores, dim-1) out torch.matmul(attn, V) return out, attn这段代码是理解 ViT 的基础。后面所有视觉 Transformer 的 Block几乎都在这个结构上做改动。2.3 多头注意力机制单头注意力只能关注一种关系多头注意力就是把 Q、K、V 拆成多组每组独立计算注意力最后拼接。公式MultiHead(Q, K, V) Concat(head_1, ..., head_h) * W_O 其中 head_i Attention(Q * W_Q^i, K * W_K^i, V * W_V^i)多头的好处不同头可以关注不同语义比如有的头关注颜色有的头关注边缘。增加模型表达能力并不显著增加计算复杂度因为每个头维度是原来的 1/h。class MultiHeadAttention(nn.Module): def __init__(self, dim, num_heads8): super().__init__() assert dim % num_heads 0 self.num_heads num_heads self.head_dim dim // num_heads self.w_q nn.Linear(dim, dim) self.w_k nn.Linear(dim, dim) self.w_v nn.Linear(dim, dim) self.fc_out nn.Linear(dim, dim) def forward(self, x): batch, seq_len, dim x.shape Q self.w_q(x).view(batch, seq_len, self.num_heads, self.head_dim).transpose(1, 2) K self.w_k(x).view(batch, seq_len, self.num_heads, self.head_dim).transpose(1, 2) V self.w_v(x).view(batch, seq_len, self.num_heads, self.head_dim).transpose(1, 2) scores torch.matmul(Q, K.transpose(-2, -1)) / (self.head_dim ** 0.5) attn F.softmax(scores, dim-1) out torch.matmul(attn, V) # [batch, heads, seq_len, head_dim] out out.transpose(1, 2).contiguous().view(batch, seq_len, dim) out self.fc_out(out) return out实际工程中直接使用nn.MultiheadAttention或F.scaled_dot_product_attention也可以但手写一遍能加深对维度变化的理解。面试、考试、课程答辩时能说清楚 head 维度如何拆分是过关关键。3. 位置编码让 Transformer 感知顺序注意力机制本身没有位置概念。交换任意两个 token 的输入顺序输出结果在数学上相同。这对序列任务不可接受对图像任务同样如此。常见位置编码方式绝对位置编码给每个位置一个独立的嵌入向量ViT 采用这一种。相对位置编码只编码两个 token 之间的距离Swin Transformer 采用这一种。旋转位置编码 RoPE通过旋转矩阵注入位置信息目前主流大模型常用。ViT 的做法是在 Patch Embedding 之后直接加一个可学习的 Position Embeddingself.pos_embed nn.Parameter(torch.zeros(1, num_patches 1, dim))其中num_patches 1里的 1 是 CLS Token 的位置。为什么num_patches 1而不是num_patches因为在 ViT 中输入序列前面额外拼接了一个 CLS Token它也需要对应一个位置嵌入。这部分在实现时经常出错初学最容易漏掉。4. ViT把图像当成句子来学ViTVision Transformer论文标题是《An Image is Worth 16x16 Words》。核心思想把图像切分成固定大小的 Patch每个 Patch 展平后线性映射成向量然后送进标准 Transformer Encoder。4.1 Patch Embedding 详解输入图像尺寸H x W x CPatch 大小P x P那么 Patch 数量N (H / P) * (W / P)每个 Patch 展平后维度是P^2 * C通过一个线性层映射到D维import torch.nn as nn class PatchEmbed(nn.Module): def __init__(self, img_size224, patch_size16, in_chans3, embed_dim768): super().__init__() self.num_patches (img_size // patch_size) ** 2 self.proj nn.Conv2d(in_chans, embed_dim, kernel_sizepatch_size, stridepatch_size) def forward(self, x): # x: [batch, 3, 224, 224] x self.proj(x) # [batch, embed_dim, 14, 14] x x.flatten(2).transpose(1, 2) # [batch, 196, embed_dim] return x这里用Conv2d实现 Patch Embedding 是工程上的常见写法。卷积核大小和步长都等于 Patch 大小效果与手动切 Patch 再线性映射完全等价但实现更高效。4.2 CLS Token 的作用ViT 在序列最前面追加一个可学习的class_token维度也是D。经过 Transformer Encoder 后CLS Token 对应位置的输出向量作为整张图片的表征接一个分类层得到类别概率。为什么用 CLS Token 而不是直接对所有 Patch 的特征做池化论文原文说明CLS Token 在训练中逐步聚合了全局信息相当于模型自己学会“读”整张图。后续也有很多工作尝试用平均池化替代 CLS Token效果各有千秋但 ViT 原始设计是 CLS Token。ViT 整体结构图像切块并展平。线性映射得到 Patch Embedding。拼接 CLS Token。加上可学习位置编码。送入 L 层 Transformer Encoder。取 CLS Token 输出接分类头。4.3 ViT 训练技巧与数据需求ViT 最出名的问题是“需要大数据量”。在 ImageNet-1k 上直接训练ViT 打不过 ResNet。原因是 Transformer 没有 CNN 的内置归纳偏置比如局部性和平移不变性必须靠大量数据学出来。两个常用策略在更大数据集上预训练如 ImageNet-21k、JFT-300M再迁移到下游任务。使用数据增强Mixup、CutMix、RandAugment、Random Erasing 等。这对只有小型数据集的同学很重要。如果实验室只有几千张图片直接训练 ViT 很可能掉点。补救方式通常是加载预训练权重做微调而不是随机初始化训练。4.4 ViT 代码实现骨架import torch import torch.nn as nn class ViT(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 self.cls_token nn.Parameter(torch.zeros(1, 1, embed_dim)) self.pos_embed nn.Parameter(torch.zeros(1, num_patches 1, embed_dim)) self.pos_drop nn.Dropout(pdropout) self.blocks nn.ModuleList([ TransformerBlock(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) nn.init.trunc_normal_(self.pos_embed, std0.02) nn.init.trunc_normal_(self.cls_token, std0.02) def forward(self, x): B x.shape[0] x self.patch_embed(x) cls_tokens self.cls_token.expand(B, -1, -1) x torch.cat([cls_tokens, x], dim1) x x self.pos_embed x self.pos_drop(x) for block in self.blocks: x block(x) x self.norm(x) cls_out x[:, 0] return self.head(cls_out)TransformerBlock 里包含 LayerNorm、多头注意力、MLP 和残差连接顺序采用 Pre-LN 结构。Pre-LN 是实践中训练的稳定性优于 Post-LN 的常见选择。这里只给骨架实际训练还需要 attention mask、学习率 warmup、AdamW 优化器、余弦退火调度器。这些都是 ViT 训练成功的关键因素。5. Swin Transformer分层窗口与跨窗口交互ViT 有两个明显问题特征分辨率全程不变没有像 CNN 那样的多尺度金字塔结构。这对目标检测、实例分割等密集预测任务非常不利。全局注意力计算复杂度是 token 数量的平方。输入分辨率增大时显存和耗时快速膨胀。Swin Transformer 针对这两点做了改进。论文标题是《Swin Transformer: Hierarchical Vision Transformer using Shifted Windows》。5.1 分层结构设计Swin 通过 Patch Merging 层逐步合并相邻 Patch使 token 数量逐层减少特征维度逐层增加。以输入 224x224 为例Stage下采样倍数特征尺寸通道数Stage 14x56x56CStage 28x28x282CStage 316x14x144CStage 432x7x78C这种分层设计与 ResNet 类似方便直接替换原有骨干网络用于检测、分割任务。5.2 窗口自注意力机制Swin 的核心是限制注意力在固定大小的窗口内计算。比如把 56x56 的特征图划分成 7x7 的窗口每个窗口大小 8x8那么 token 数量是 64。全局注意力的复杂度是O(N^2)窗口注意力的复杂度是O(N_window^2 * M)其中 M 是窗口个数。当窗口大小固定时整体复杂度与图像分辨率呈线性关系计算效率大幅提升。代价是窗口之间没有信息交互。一个窗口内的 token 看不到其他窗口的内容。如果只是简单地做窗口注意力模型就成了“局部 Transformer”感受野受限于窗口大小。5.3 移位窗口注意力Swin 的解决方案是在相邻两层之间移动窗口划分方式。第一层使用常规窗口划分第二层将窗口偏移半个窗口大小再划分。通过这种交替机制原本处于不同窗口的 token 在下一层有机会互相建模。虽然单层看是局部注意力但堆叠两层后等效感受野能够跨窗口扩散。数学上并不复杂但工程实现时移位窗口会导致窗口数量不固定处理边缘 token 比较麻烦。Swin 原始实现使用了torch.roll循环移位和 attention mask 来保证正确性。简化理解方式普通窗口注意力图像按固定网格切块。移位窗口注意力网格整体平移半个窗口。平移后每个 token 的邻居集合发生变化跨窗口信息开始流通。5.4 相对位置编码Swin 没有使用 ViT 那种绝对位置编码而是采用相对位置编码。对于每个窗口内的一对 token只关心它们之间的相对偏移量而不是绝对位置。相对位置编码的好处泛化性更好输入分辨率变化时不需要重新插值位置编码。符合视觉任务的平移不变性特征。实现时Swin 预先构造一个相对位置索引表通过查询表得到对应位置偏置加到注意力分数上。初次接触最好实际打印一下索引形状理解(2 * window_size - 1) * (2 * window_size - 1)这个二维表是怎么来的。5.5 Swin 的瓶颈与升级方向Swin 虽然解决了计算效率和分层特征问题但并非没有短板窗口内注意力仍然限制了远程依赖建模能力。移位窗口带来工程复杂度注意力 mask 的设计容易出错。SwinV2 针对训练稳定性、分辨率迁移、大模型微调做了进一步改进。后续还有 SwinV2、SwinUNet、CvT、PVT、MogaNet 等一系列工作。理解 Swin 的窗口设计后再看这些变体就容易得多。6. 视觉 Transformer 实验环境与硬件要求虽然这篇文章重点是原理但到课程设计或论文复现阶段环境要求还是要说清楚。资源项建议配置显卡显存 8GB 以上可训练小型模型24GB 可训练 ViT-Base 级别CPU只做推理可用 CPU训练强烈建议 GPUPython3.8 到 3.11 均可PyTorch2.0 以上支持scaled_dot_product_attention更佳CUDA11.8 或 12.1 根据 PyTorch 版本选择数据集CIFAR-10 学习用完整复现 ViT 建议 ImageNet 或使用预训练权重纯学习场景可以先在 CIFAR-10 上用简化版 ViT 做实验不用一次上 ImageNet。训练时把 patch size 调大、depth 调小能显著降低显存占用。7. 功能测试与效果验证7.1 验证自注意力实现用随机输入验证前向传播和输出形状x torch.randn(2, 16, 64) # batch2, seq_len16, dim64 attn SelfAttention(dim64) out, weights attn(x) print(out.shape) # [2, 16, 64] print(weights.shape) # [2, 16, 16]判断标准输出形状与输入保持一致。attention 权重每一行和为 1。不出现 NaN。7.2 验证 Patch Embeddingimg torch.randn(2, 3, 224, 224) patch_embed PatchEmbed(img_size224, patch_size16, in_chans3, embed_dim768) out patch_embed(img) print(out.shape) # [2, 196, 768]如果输出 token 数量不是 196检查分辨率是否能被 patch_size 整除。ViT 要求输入尺寸必须能整除 patch size。7.3 验证 ViT 分类输出候选做法使用 timm 库加载预训练模型快速验证推理逻辑。import timm model timm.create_model(vit_base_patch16_224, pretrainedTrue) model.eval() test_input torch.randn(1, 3, 224, 224) with torch.no_grad(): logits model(test_input) print(logits.shape) # [1, 1000]timm 库内置了大量视觉 Transformer 实现包括 ViT、Swin、DeiT 等。初学阶段用 timm 跑通流程再逐步替换成手写实现效率更高。7.4 验证 Swin TransformerSwin 的实现比 ViT 复杂初次验证建议直接使用官方源码或 timm 预训练模型model timm.create_model(swin_base_patch4_window7_224, pretrainedTrue) model.eval() out model(torch.randn(1, 3, 224, 224)) print(out.shape) # [1, 1000]如果项目要求从零实现务必要把 attention mask 的生成单独写成测试用例用可视化方式确认窗口移动后哪些位置被 mask。这里是最容易出错也最难调试的地方。8. ViT 与 Swin Transformer 对比对比维度ViTSwin Transformer输入处理全局 Patch EmbeddingPatch Embedding Patch Merging注意力范围全局窗口内 移位窗口计算复杂度O(N^2)O(N * window^2)特征层级单尺度多尺度金字塔位置编码绝对位置编码相对位置编码归纳偏置弱中接近 CNN 层级特性适合任务分类为主分类、检测、分割通用训练难度需要大数据集或预训练相对稳定工程复杂度低中高从竞赛和论文使用情况来看Swin Transformer 在检测和分割任务中更常用。ViT 适合作为理解视觉 Transformer 的入门模型也适合直接做大模型预训练底座。两者不是替代关系而是设计思路的延续和改进。9. 常见问题与排查方法问题现象可能原因排查方式解决方案ViT 训练 loss 不下降学习率过大或过小打印梯度统计换成 AdamW使用 warmup显存不足token 数太多、batch 太大观察显存占用曲线增大 patch size、减小 batch、使用梯度累积位置编码维度不匹配忘记加 CLS Token 的位置检查 pos_embed 第二维修改为 num_patches 1Swin 移位窗口结果异常attention mask 错误可视化 mask 矩阵对照官方源码重写 mask 生成高分辨率输入报错尺寸不能被 patch_size 整除打印输入形状中心裁剪或 resize注意力图全一样训练崩溃或梯度消失检查 loss 和权重范数降低学习率检查 LayerNorm 位置从零训练准确率低数据量不足或增强不够对比预训练模型效果使用预训练权重微调建议第一次实验时先固定随机种子保证结果可复现。视觉 Transformer 对超参数敏感同一套代码在不同学习率下差异可能非常大。10. 工程实践与实验管理建议10.1 实验配置管理用 YAML 或 dataclass 管理配置避免在代码里写死超参数model: name: vit_base_patch16_224 img_size: 224 patch_size: 16 embed_dim: 768 depth: 12 num_heads: 12 dropout: 0.1 train: batch_size: 64 lr: 1e-4 warmup_epochs: 5 epochs: 100 weight_decay: 0.05 amp: true10.2 性能观察训练时用nvidia-smi或者 PyTorch Profiler 观察显存占用是否稳定。是否存在显存碎片。每个 step 的耗时是否波动。梯度是否出现 NaN 或异常增大。如果显存不够优先级排序减小 batch size。打开混合精度 AMP。增大 patch size。减少 transformer block 数量。使用梯度累积。10.3 实验记录每个实验至少记录以下内容参数配置。训练 loss 曲线。验证准确率。显存占用和单 step 耗时。模型参数量和 FLOPs。推荐使用tensorboard或wandb记录。课程答辩或论文写作时这些数据比代码本身更有说服力。11. 总结与下一步这条学习路线最值得验证的三个点手写 Self-Attention 并确认输出形状和注意力权重分布是否正确。完整跑通 ViT 的 Patch Embedding CLS Token 位置编码流程。理解 Swin Transformer 的窗口注意力和移位机制最好对比可视化普通窗口和移位窗口的注意力分布差异。最容易踩的坑集中在三处CLS Token 导致的位置编码维度变化、Swin 的 attention mask 构造、ViT 小数据集训练不收敛。这三个问题在面试和答辩中也经常被追问建议重点掌握。后续扩展方向可以是 DeiT 的知识蒸馏训练方式、SwinV2 的改进细节、CvT 的卷积 Token 嵌入以及 Masked Autoencoder 自监督预训练。把一个模型的原理吃透再横向对比同类工作Transformer 在视觉里的全貌会越来越清晰。建议收藏这篇文章写代码时对照核心公式和实现骨架能少走不少弯路。免责声明本文所有内容仅用于技术学习、算法复现与学术研究。使用公开数据集和预训练模型时请遵守相应许可证和版权规定。不得将相关技术用于未经授权的图像识别、人脸分析、隐私侵犯或其他违反法律法规的场景。