Vision Transformer (ViT) 代码实现详解:从 Patch Embedding 到 MLP Head 的 5 个关键模块

发布时间:2026/7/6 14:44:05

Vision Transformer (ViT) 代码实现详解:从 Patch Embedding 到 MLP Head 的 5 个关键模块 Vision Transformer (ViT) 代码实现详解从 Patch Embedding 到 MLP Head 的 5 个关键模块当计算机视觉领域还在被卷积神经网络CNN统治时Google Research 在 2020 年提出的 Vision Transformer (ViT) 彻底改变了游戏规则。这篇技术指南将带您深入 ViT 的 PyTorch 实现细节通过 5 个核心模块的代码级解析让您不仅能理解 ViT 的工作原理还能亲手实现这个革命性的架构。1. Patch Embedding图像到序列的魔法转换传统 Transformer 处理的是序列数据而图像本质上是二维的。ViT 的第一个关键创新就是将图像转换为适合 Transformer 处理的序列形式。让我们看看如何用 PyTorch 实现这一转换import torch import torch.nn as nn class PatchEmbedding(nn.Module): def __init__(self, img_size224, patch_size16, in_channels3, embed_dim768): super().__init__() self.img_size img_size self.patch_size patch_size self.n_patches (img_size // patch_size) ** 2 # 使用卷积层实现patch分割和线性投影 self.proj nn.Conv2d( in_channelsin_channels, out_channelsembed_dim, kernel_sizepatch_size, stridepatch_size ) def forward(self, x): # x形状: [batch_size, channels, height, width] x self.proj(x) # [batch_size, embed_dim, n_patches^0.5, n_patches^0.5] x x.flatten(2) # [batch_size, embed_dim, n_patches] x x.transpose(1, 2) # [batch_size, n_patches, embed_dim] return x这段代码有几个精妙之处卷积核与步长的设计kernel_sizepatch_size和stridepatch_size确保每个卷积操作正好处理一个 patch没有重叠维度变换的艺术通过flatten和transpose操作我们将 2D 特征图转换为适合 Transformer 处理的序列形式计算效率相比先分割图像再线性投影这种实现方式更加高效提示对于 224x224 的图像和 16x16 的 patch会产生 196 个 patch(224/16)²。每个 patch 被投影到 768 维空间ViT-Base 的默认设置。2. Position Embedding为序列注入空间信息Transformer 本身不具备处理序列顺序的能力而图像中的空间位置信息至关重要。ViT 使用可学习的位置编码来解决这个问题class PositionEmbedding(nn.Module): def __init__(self, n_patches196, embed_dim768): super().__init__() self.pos_embed nn.Parameter(torch.zeros(1, n_patches 1, embed_dim)) nn.init.trunc_normal_(self.pos_embed, std0.02) def forward(self, x): # x形状: [batch_size, n_patches, embed_dim] # 添加位置编码不包括class token的位置 return x self.pos_embed[:, 1:, :]值得注意的是可学习参数与原始 Transformer 的固定位置编码不同ViT 使用可学习的位置编码class token 处理位置编码的第一个位置保留给 class token初始化技巧使用截断正态分布初始化标准差为 0.023. Transformer Encoder Block自注意力的核心ViT 的编码器由多个相同的 Transformer Block 堆叠而成每个 Block 包含多头自注意力机制和前馈网络class TransformerBlock(nn.Module): def __init__(self, embed_dim768, num_heads12, mlp_ratio4.0, dropout0.1): super().__init__() self.norm1 nn.LayerNorm(embed_dim) self.attn nn.MultiheadAttention(embed_dim, num_heads, dropoutdropout) self.norm2 nn.LayerNorm(embed_dim) self.mlp nn.Sequential( nn.Linear(embed_dim, int(embed_dim * mlp_ratio)), nn.GELU(), nn.Dropout(dropout), nn.Linear(int(embed_dim * mlp_ratio), embed_dim), nn.Dropout(dropout) ) def forward(self, x): # LayerNorm前置Pre-LN结构 norm_x self.norm1(x) attn_output, _ self.attn(norm_x, norm_x, norm_x) x x attn_output norm_x self.norm2(x) mlp_output self.mlp(norm_x) x x mlp_output return x关键实现细节Pre-LN 结构LayerNorm 在自注意力和 MLP 之前应用相比原始 Transformer 的 Post-LN 更稳定残差连接每个子层都有残差连接缓解梯度消失问题GELU 激活使用高斯误差线性单元比 ReLU 表现更好4. MLP Head从特征到分类结果ViT 使用一个简单的 MLP 作为分类头处理来自 class token 的特征class MLPHead(nn.Module): def __init__(self, embed_dim768, num_classes1000): super().__init__() self.head nn.Sequential( nn.LayerNorm(embed_dim), nn.Linear(embed_dim, num_classes) ) def forward(self, x): # x形状: [batch_size, embed_dim] (class token) return self.head(x)这个看似简单的结构有几个设计考量仅使用 class token与 CNN 通常使用全局平均池化不同ViT 依赖 class token 的特征最后的 LayerNorm确保特征稳定有助于模型收敛线性分类器微调时可以使用更复杂的结构但预训练时保持简单5. 完整 ViT 模型集成现在我们将所有模块组合成完整的 ViT 模型class VisionTransformer(nn.Module): def __init__(self, img_size224, patch_size16, in_channels3, embed_dim768, depth12, num_heads12, mlp_ratio4.0, num_classes1000, dropout0.1): super().__init__() self.patch_embed PatchEmbedding(img_size, patch_size, in_channels, embed_dim) n_patches self.patch_embed.n_patches # Class token self.cls_token nn.Parameter(torch.zeros(1, 1, embed_dim)) # Position embedding self.pos_embed PositionEmbedding(n_patches, embed_dim) # Transformer encoder self.blocks nn.ModuleList([ TransformerBlock(embed_dim, num_heads, mlp_ratio, dropout) for _ in range(depth) ]) # Classification head self.head MLPHead(embed_dim, num_classes) # Initialize weights nn.init.trunc_normal_(self.cls_token, std0.02) self.apply(self._init_weights) def _init_weights(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): # Patch embedding x self.patch_embed(x) # [batch_size, n_patches, embed_dim] # Add class token cls_tokens self.cls_token.expand(x.shape[0], -1, -1) x torch.cat((cls_tokens, x), dim1) # [batch_size, n_patches1, embed_dim] # Add position embedding x x self.pos_embed(x) # Transformer blocks for block in self.blocks: x block(x) # Classification (use only class token) cls_token_final x[:, 0] return self.head(cls_token_final)完整模型的几个关键点模块化设计清晰分离各个组件便于理解和修改参数初始化使用截断正态分布初始化对 Transformer 模型很重要前向传播流程严格遵循 ViT 论文中的数据处理流程实战训练 ViT 的实用技巧理解了 ViT 的实现后让我们看看如何有效地训练它学习率调度使用带 warmup 的余弦退火调度optimizer torch.optim.AdamW(model.parameters(), lr1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_maxepochs, eta_min1e-6 ) warmup_scheduler torch.optim.lr_scheduler.LinearLR( optimizer, start_factor0.01, total_iterswarmup_epochs )数据增强对 ViT 特别重要from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])混合精度训练加速训练并减少内存使用scaler torch.cuda.amp.GradScaler() with torch.cuda.amp.autocast(): outputs model(inputs) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()ViT 变体与性能对比不同规模的 ViT 模型在性能和计算成本上有显著差异模型变体层数隐藏层维度MLP大小头数参数量ImageNet Top-1 AccViT-Tiny1219276835.7M75.5%ViT-Small123841536622M79.9%ViT-Base1276830721286M84.5%ViT-Large241024409616307M85.8%ViT-Huge321280512016632M86.5%注意这些结果是在 ImageNet-21k 上预训练并在 ImageNet-1k 上微调得到的。实际性能会因训练设置和数据增强策略而有所不同。常见问题与解决方案在实现和训练 ViT 时可能会遇到以下问题训练不稳定解决方案使用梯度裁剪nn.utils.clip_grad_norm_和学习率 warmup过拟合解决方案增加 dropout 率、使用更强的数据增强、添加权重衰减内存不足解决方案减小 batch size、使用梯度累积、尝试混合精度训练收敛速度慢解决方案检查初始化、使用适当的学习率调度、确保数据预处理正确进阶技巧与优化对于希望进一步提升 ViT 性能的开发者可以考虑以下优化知识蒸馏使用更大的 ViT 或 CNN 作为教师模型teacher_model torch.hub.load(facebookresearch/deit:main, deit_base_patch16_224, pretrainedTrue)渐进式训练从小分辨率开始逐步增加图像尺寸模型剪枝移除不重要的注意力头或 MLP 神经元量化感知训练为部署准备低精度模型model torch.quantization.quantize_dynamic( model, {nn.Linear}, dtypetorch.qint8 )从理论到实践ViT 的完整训练流程为了帮助您快速上手以下是训练 ViT 的完整 PyTorch Lightning 示例import pytorch_lightning as pl from torchmetrics import Accuracy class ViTLightningModule(pl.LightningModule): def __init__(self, num_classes1000, lr1e-4, warmup_epochs5): super().__init__() self.model VisionTransformer(num_classesnum_classes) self.criterion nn.CrossEntropyLoss() self.train_acc Accuracy(taskmulticlass, num_classesnum_classes) self.val_acc Accuracy(taskmulticlass, num_classesnum_classes) self.lr lr self.warmup_epochs warmup_epochs def forward(self, x): return self.model(x) def training_step(self, batch, batch_idx): x, y batch logits self(x) loss self.criterion(logits, y) self.train_acc(logits, y) self.log(train_loss, loss, prog_barTrue) self.log(train_acc, self.train_acc, prog_barTrue) return loss def validation_step(self, batch, batch_idx): x, y batch logits self(x) loss self.criterion(logits, y) self.val_acc(logits, y) self.log(val_loss, loss, prog_barTrue) self.log(val_acc, self.val_acc, prog_barTrue) def configure_optimizers(self): optimizer torch.optim.AdamW(self.parameters(), lrself.lr) scheduler { scheduler: torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_maxself.trainer.max_epochs, eta_min1e-6 ), interval: epoch } if self.warmup_epochs 0: warmup { scheduler: torch.optim.lr_scheduler.LinearLR( optimizer, start_factor0.01, total_itersself.warmup_epochs ), interval: epoch } return [optimizer], [warmup, scheduler] return [optimizer], [scheduler]这个实现包含了训练和验证逻辑清晰的步骤分离指标跟踪准确率和损失记录学习率调度带 warmup 的余弦退火日志记录实时监控训练进度ViT 在实际项目中的应用建议根据实际项目经验以下是在不同场景下使用 ViT 的建议数据量充足时直接使用 ViT 进行端到端训练数据有限时使用预训练 ViT 作为特征提取器在顶层添加任务特定层进行微调计算资源受限时考虑较小的 ViT 变体如 ViT-Tiny 或 ViT-Small使用知识蒸馏训练更小的学生模型实时性要求高时探索 ViT 的轻量级变体如 DeiT 或 MobileViT应用模型量化和剪枝性能优化与调试技巧当 ViT 表现不如预期时可以尝试以下调试方法可视化注意力图检查模型关注了图像的哪些部分attention_maps model.blocks[-1].attn.get_attention_map()检查梯度流动使用torchviz可视化计算图from torchviz import make_dot make_dot(loss, paramsdict(model.named_parameters()))监控激活统计确保没有异常大的激活值for name, param in model.named_parameters(): if param.requires_grad: self.logger.experiment.add_histogram(name, param, self.global_step)对比基线与 CNN 基准模型如 ResNet比较性能未来方向与扩展阅读ViT 开启了计算机视觉的新时代以下是有前景的研究方向高效注意力机制如 Linformer 或 Performer 中的线性注意力层次化 ViT结合 CNN 的层次化特征提取自监督学习如 MAEMasked Autoencoder方法多模态应用如 CLIP 中的图像-文本联合训练推荐阅读的进阶论文DeiT: Data-efficient Image TransformersSwin Transformer: Hierarchical Vision TransformerMAE: Masked Autoencoders Are Scalable Vision LearnersMobileViT: Light-weight, General-purpose, and Mobile-friendly Vision Transformer结语ViT 实现的工程实践实现 ViT 不仅仅是复制论文中的公式更需要理解其设计哲学和工程细节。在实际项目中我发现以下几点特别重要初始化策略正确的初始化对 Transformer 的稳定训练至关重要学习率调度warmup 阶段能显著提高训练稳定性正则化技术适当的 dropout 和权重衰减防止过拟合硬件利用充分利用混合精度训练和分布式训练加速实验迭代ViT 的成功证明了 Transformer 在计算机视觉中的巨大潜力。通过本指南中的代码实现和技巧希望您能快速掌握这一强大工具并在自己的项目中应用它。记住理解每个模块的设计动机比简单复制代码更重要这将帮助您灵活应对不同的应用场景和挑战。

相关新闻