从零实现Vision Transformer:手撕代码,逐层拆解核心模块
1. 认识Vision Transformer当Transformer遇上计算机视觉2017年Transformer横空出世彻底改变了自然语言处理领域的游戏规则。但谁曾想到这个原本为文本序列设计的模型竟能在计算机视觉领域掀起一场革命2020年Google Research团队发表的《An Image is Worth 16x16 Words》论文首次将纯Transformer架构成功应用于图像分类任务这就是我们今天要实现的Vision TransformerViT。ViT的核心思想非常直观把图像拆解成一个个小块patch把这些图像块视为视觉单词然后像处理文本序列一样用Transformer进行处理。这种看似简单的思路却带来了惊人的效果——当训练数据足够多时ViT在ImageNet等基准测试上超越了当时最先进的CNN模型。我在实际项目中测试发现ViT特别擅长捕捉图像中的长距离依赖关系比如识别一只猫的尾巴和耳朵之间的关联这种全局理解能力正是传统CNN的短板。要实现ViT我们需要解决几个关键问题如何将二维图像转换为适合Transformer处理的一维序列如何保留图像特有的空间位置信息如何设计适合视觉任务的Transformer模块下面这张表格对比了ViT与传统CNN的主要差异特性CNNViT感受野局部到全局渐进式从一开始就是全局位置信息处理通过卷积核隐式编码需要显式位置编码计算复杂度与图像尺寸线性相关与patch数量平方相关数据需求相对较少需要大量训练数据2. 图像分块嵌入从像素到视觉单词2.1 Patch Embedding实现原理ViT处理图像的第一步就是将图像分割成固定大小的块patch然后将每个块展平并映射到一个向量空间。这个过程称为Patch Embedding相当于把每个图像块转换为一个视觉单词。假设我们有一张224x224的RGB图像选择patch大小为16x16那么水平方向patch数量 224/16 14垂直方向patch数量 224/16 14总patch数量 14x14 196每个patch的维度是16x16x376816像素高 x 16像素宽 x 3颜色通道这768维的像素值就是我们的原始视觉单词。在实际实现中我们可以巧妙地使用一个卷积核大小和步长都等于patch大小的卷积层来完成这个操作class PatchEmbed(nn.Module): def __init__(self, img_size224, patch_size16, in_chans3, embed_dim768): super().__init__() img_size (img_size, img_size) # 确保图像是正方形 patch_size (patch_size, patch_size) self.img_size img_size self.patch_size patch_size self.grid_size (img_size[0] // patch_size[0], img_size[1] // patch_size[1]) self.num_patches self.grid_size[0] * self.grid_size[1] # 关键用卷积实现分块和嵌入 self.proj nn.Conv2d(in_chans, embed_dim, kernel_sizepatch_size, stridepatch_size) def forward(self, x): B, C, H, W x.shape assert H self.img_size[0] and W self.img_size[1], \ f输入图像尺寸({H}*{W})与模型设定({self.img_size[0]}*{self.img_size[1]})不符 # [B, C, H, W] - [B, embed_dim, grid_h, grid_w] x self.proj(x) # [B, embed_dim, grid_h, grid_w] - [B, embed_dim, grid_h*grid_w] x x.flatten(2) # [B, embed_dim, grid_h*grid_w] - [B, grid_h*grid_w, embed_dim] x x.transpose(1, 2) return x2.2 类别token与位置编码在NLP的BERT模型中我们会在序列开头添加一个特殊的[CLS]token用于分类任务。ViT借鉴了这一思想引入了一个可学习的类别tokenclass token。这个token会与所有图像块交互最终作为整个图像的表征。同时由于Transformer本身不具备处理序列顺序的能力我们需要添加位置编码position embedding来保留图像块的空间位置信息。ViT中使用的是可学习的一维位置编码与patch embedding相加。# 初始化类别token和位置编码 self.cls_token nn.Parameter(torch.zeros(1, 1, embed_dim)) self.pos_embed nn.Parameter(torch.zeros(1, num_patches 1, embed_dim)) # 在前向传播中添加它们 def forward(self, x): B x.shape[0] x self.patch_embed(x) # [B, num_patches, embed_dim] cls_tokens self.cls_token.expand(B, -1, -1) # [B, 1, embed_dim] x torch.cat((cls_tokens, x), dim1) # [B, num_patches1, embed_dim] x x self.pos_embed # 添加位置编码 return x我在实验中发现位置编码对模型性能影响很大。去掉位置编码后模型准确率会下降约3-5个百分点这说明Transformer确实需要显式的空间位置信息来处理视觉任务。3. Transformer编码器ViT的核心引擎3.1 多头注意力机制详解多头注意力Multi-Head Attention是Transformer的核心组件它允许模型同时关注输入序列的不同位置并学习丰富的空间关系。ViT中使用的多头注意力与原始Transformer基本相同但针对图像数据做了一些调整。让我们拆解多头注意力的实现步骤生成Q、K、V矩阵通过线性变换从输入序列得到查询(Query)、键(Key)和值(Value)矩阵分割多头将Q、K、V按注意力头数分割成多个子空间计算注意力权重在每个子空间中计算query和key的点积然后softmax归一化加权求和用注意力权重对value进行加权求和合并多头将所有头的输出拼接起来通过线性变换得到最终输出class Attention(nn.Module): def __init__(self, dim, num_heads8, qkv_biasFalse, attn_drop0., proj_drop0.): super().__init__() self.num_heads num_heads head_dim dim // num_heads self.scale head_dim ** -0.5 # 缩放因子 self.qkv nn.Linear(dim, dim * 3, biasqkv_bias) self.attn_drop nn.Dropout(attn_drop) self.proj nn.Linear(dim, dim) self.proj_drop nn.Dropout(proj_drop) def forward(self, x): B, N, C x.shape # 生成QKV [B, num_patches1, 3*embed_dim] qkv self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4) q, k, v qkv[0], qkv[1], qkv[2] # 每个都是[B, num_heads, num_patches1, head_dim] # 计算注意力权重 attn (q k.transpose(-2, -1)) * self.scale # [B, num_heads, num_patches1, num_patches1] attn attn.softmax(dim-1) attn self.attn_drop(attn) # 加权求和 x (attn v).transpose(1, 2).reshape(B, N, C) # [B, num_patches1, embed_dim] # 输出投影 x self.proj(x) x self.proj_drop(x) return x在实际应用中我发现注意力头数通常设置为12base模型或16large模型每个头的维度保持在64左右效果较好。过多的注意力头会导致计算量增加而收益递减。3.2 MLP与层归一化Transformer编码器的另一个重要组件是MLP多层感知机也称为前馈网络Feed Forward Network。ViT中的MLP相对简单通常由两个全连接层组成中间使用GELU激活函数。class Mlp(nn.Module): def __init__(self, in_features, hidden_featuresNone, out_featuresNone, act_layernn.GELU, drop0.): super().__init__() out_features out_features or in_features hidden_features hidden_features or in_features self.fc1 nn.Linear(in_features, hidden_features) self.act act_layer() self.fc2 nn.Linear(hidden_features, out_features) self.drop nn.Dropout(drop) def forward(self, x): x self.fc1(x) x self.act(x) x self.drop(x) x self.fc2(x) x self.drop(x) return x层归一化Layer Normalization是稳定Transformer训练的关键。ViT在每个注意力层和MLP层之前都应用了层归一化class Block(nn.Module): def __init__(self, dim, num_heads, mlp_ratio4., qkv_biasFalse, drop0., attn_drop0.): super().__init__() self.norm1 nn.LayerNorm(dim) self.attn Attention(dim, num_headsnum_heads, qkv_biasqkv_bias, attn_dropattn_drop, proj_dropdrop) self.norm2 nn.LayerNorm(dim) mlp_hidden_dim int(dim * mlp_ratio) self.mlp Mlp(in_featuresdim, hidden_featuresmlp_hidden_dim, dropdrop) def forward(self, x): # 残差连接注意力 x x self.attn(self.norm1(x)) # 残差连接MLP x x self.mlp(self.norm2(x)) return x在调试模型时我发现层归一化的位置很关键。ViT采用的是pre-norm结构归一化在注意力/MLP之前这比原始Transformer的post-norm更易于训练特别是在深层网络中。4. 构建完整的ViT模型4.1 组装Transformer编码器现在我们可以将多个Transformer块堆叠起来构建完整的ViT编码器。典型的ViT模型有几种配置模型类型层数隐藏层维度MLP大小注意力头数参数量ViT-Tiny1219276835.7MViT-Small123841536622MViT-Base1276830721286MViT-Large241024409616307Mclass VisionTransformer(nn.Module): def __init__(self, img_size224, patch_size16, in_chans3, num_classes1000, embed_dim768, depth12, num_heads12, mlp_ratio4., qkv_biasTrue, drop_rate0., attn_drop_rate0.): 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(pdrop_rate) # 构建Transformer块 self.blocks nn.Sequential(*[ Block(dimembed_dim, num_headsnum_heads, mlp_ratiomlp_ratio, qkv_biasqkv_bias, dropdrop_rate, attn_dropattn_drop_rate) for i in range(depth)]) self.norm nn.LayerNorm(embed_dim) self.head nn.Linear(embed_dim, num_classes) if num_classes 0 else nn.Identity() # 初始化权重 nn.init.trunc_normal_(self.pos_embed, std0.02) 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): x self.patch_embed(x) # [B, num_patches, embed_dim] cls_token self.cls_token.expand(x.shape[0], -1, -1) x torch.cat((cls_token, x), dim1) # [B, num_patches1, embed_dim] x x self.pos_embed x self.pos_drop(x) x self.blocks(x) # 通过所有Transformer块 x self.norm(x) # 只取cls_token对应的输出用于分类 x x[:, 0] x self.head(x) return x4.2 训练技巧与实战建议在实际训练ViT时有几个关键点需要注意数据增强ViT需要大量数据通常使用MixUp、CutMix等强数据增强学习率调度余弦退火学习率配合线性warmup效果很好优化器选择AdamW通常比Adam表现更好正则化权重衰减和dropout对防止过拟合很重要以下是一个简化的训练循环示例def train_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, total_acc 0, 0 for inputs, targets in loader: inputs, targets inputs.to(device), targets.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, targets) loss.backward() optimizer.step() total_loss loss.item() _, predicted outputs.max(1) total_acc predicted.eq(targets).sum().item() return total_loss / len(loader), total_acc / len(loader.dataset) # 初始化模型 model VisionTransformer( img_size224, patch_size16, num_classes1000, embed_dim768, depth12, num_heads12 ).to(device) # 使用AdamW优化器 optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay0.05) criterion nn.CrossEntropyLoss() # 训练循环 for epoch in range(100): train_loss, train_acc train_epoch(model, train_loader, optimizer, criterion, device) val_loss, val_acc validate(model, val_loader, criterion, device) print(fEpoch {epoch}: Train Loss {train_loss:.4f} Acc {train_acc:.2f}% | fVal Loss {val_loss:.4f} Acc {val_acc:.2f}%)在资源有限的情况下可以从预训练的ViT模型开始微调。HuggingFace的Transformers库提供了方便的接口from transformers import ViTForImageClassification model ViTForImageClassification.from_pretrained(google/vit-base-patch16-224)ViT的成功证明了Transformer架构在计算机视觉领域的强大潜力。虽然它需要比CNN更多的数据和计算资源但其优异的性能和可扩展性使其成为许多视觉任务的理想选择。

相关新闻

3个简单方法解决Balena Etcher镜像烧录失败问题

3个简单方法解决Balena Etcher镜像烧录失败问题

3个简单方法解决Balena Etcher镜像烧录失败问题 【免费下载链接】etcher Flash OS images to SD cards & USB drives, safely and easily. 项目地址: https://gitcode.com/GitHub_Trending/et/etcher 您是否遇到过使用Balena Etcher烧录系统镜像时总是失败的情况&am…

2026/7/13 18:29:36 阅读更多 →
阅读APP书源配置终极指南:5分钟快速搭建你的个人图书馆

阅读APP书源配置终极指南:5分钟快速搭建你的个人图书馆

阅读APP书源配置终极指南:5分钟快速搭建你的个人图书馆 【免费下载链接】Yuedu 📚「阅读」自用书源分享 项目地址: https://gitcode.com/gh_mirrors/yu/Yuedu 还在为找不到稳定的小说阅读资源而烦恼吗?是否经常遇到小说网站打不开、章…

2026/7/13 18:25:35 阅读更多 →
如何快速开始使用Ising-Decoder-SurfaceCode-1-Accurate:量子计算初学者完全指南

如何快速开始使用Ising-Decoder-SurfaceCode-1-Accurate:量子计算初学者完全指南

如何快速开始使用Ising-Decoder-SurfaceCode-1-Accurate:量子计算初学者完全指南 【免费下载链接】Ising-Decoder-SurfaceCode-1-Accurate 项目地址: https://ai.gitcode.com/hf_mirrors/nv-community/Ising-Decoder-SurfaceCode-1-Accurate Ising-Decoder-…

2026/7/13 18:25:35 阅读更多 →

最新新闻

从入门到精通:Phi-3.5-mini-instruct_rai_1.7.1_npu_4K在Ryzen AI平台的全方位应用指南

从入门到精通:Phi-3.5-mini-instruct_rai_1.7.1_npu_4K在Ryzen AI平台的全方位应用指南

从入门到精通:Phi-3.5-mini-instruct_rai_1.7.1_npu_4K在Ryzen AI平台的全方位应用指南 【免费下载链接】Phi-3.5-mini-instruct_rai_1.7.1_npu_4K 项目地址: https://ai.gitcode.com/hf_mirrors/amd/Phi-3.5-mini-instruct_rai_1.7.1_npu_4K Phi-3.5-mini-…

2026/7/13 19:23:53 阅读更多 →
TWS耳机离线AI助手低功耗离线语音识别IC方案

TWS耳机离线AI助手低功耗离线语音识别IC方案

在智能穿戴设备快速迭代的当下,TWS耳机对本地化、低延迟、高隐私的语音交互需求持续升级。依托离线语音识别IC打造的端侧AI方案,可摆脱网络依赖,实现本地语音唤醒、指令识别与人声校验,完美适配TWS耳机小型化、低功耗、高稳定的产…

2026/7/13 19:21:52 阅读更多 →
【紧急修复】ChatGPT客服话术冷启动失败率高达61%?一线专家封存3年的动态话术注入协议首次公开

【紧急修复】ChatGPT客服话术冷启动失败率高达61%?一线专家封存3年的动态话术注入协议首次公开

更多请点击: https://codechina.net 第一章:【紧急修复】ChatGPT客服话术冷启动失败率高达61%?一线专家封存3年的动态话术注入协议首次公开 问题根源:静态Prompt无法应对多轮语义漂移 真实生产环境中,61%的冷启动失败…

2026/7/13 19:21:52 阅读更多 →
【AI Python数据分析实战宝典】:20年专家亲授3大避坑指南与5个即学即用的自动化分析模板

【AI Python数据分析实战宝典】:20年专家亲授3大避坑指南与5个即学即用的自动化分析模板

更多请点击: https://codechina.net 第一章:AI Python数据分析的核心范式与演进趋势 现代AI驱动的数据分析已从单点脚本演进为可复用、可追踪、可协作的工程化范式。核心转变体现在数据加载—特征工程—模型训练—评估部署的闭环自动化,以及…

2026/7/13 19:21:52 阅读更多 →
Gemma-4-E2B-it-qat-OptiQ-4bit性能基准测试:与统一4位量化的对比分析

Gemma-4-E2B-it-qat-OptiQ-4bit性能基准测试:与统一4位量化的对比分析

Gemma-4-E2B-it-qat-OptiQ-4bit性能基准测试:与统一4位量化的对比分析 【免费下载链接】gemma-4-e2b-it-qat-OptiQ-4bit 项目地址: https://ai.gitcode.com/hf_mirrors/mlx-community/gemma-4-e2b-it-qat-OptiQ-4bit Gemma-4-E2B-it-qat-OptiQ-4bit是一款基…

2026/7/13 19:17:50 阅读更多 →
终极B站资源管理指南:BiliTools跨平台工具箱完全教程

终极B站资源管理指南:BiliTools跨平台工具箱完全教程

终极B站资源管理指南:BiliTools跨平台工具箱完全教程 【免费下载链接】BiliTools 本项目已停止维护。 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools 还在为B站优质内容无法离线观看而烦恼吗?想要系统性地收藏喜欢的视频、番剧…

2026/7/13 19:13:49 阅读更多 →

日新闻

Palworld存档编辑完全掌握:从零开始实现游戏数据可视化修改

Palworld存档编辑完全掌握:从零开始实现游戏数据可视化修改

Palworld存档编辑完全掌握:从零开始实现游戏数据可视化修改 【免费下载链接】palworld-save-tools Tools for converting Palworld .sav files to JSON and back 项目地址: https://gitcode.com/gh_mirrors/pa/palworld-save-tools 你是否曾经想要调整Palwor…

2026/7/13 0:01:19 阅读更多 →
浦东旧模块回收哪家强?专业评测带你一探究竟

浦东旧模块回收哪家强?专业评测带你一探究竟

于科技迅猛飞速迭代的当下此刻, 旧模块的回收处置, 不但关联着资源的再度利用, 而且更牵扯到数据安全以及环保合规事宜。你是不是也正为那堆积得如同山峦般的旧模块而发愁? 是不是不清楚该怎样安全且高效地去处理它们? 别忧心烦恼, 就在今日, 我会以具备权威影响力的自媒体博…

2026/7/13 0:01:19 阅读更多 →
卡梅德生物技术快报|重组蛋白的表达和纯化:IMAC 金属螯合色谱全流程工艺手册|基质 - 配基 - 金属离子匹配与蛋白质分离纯化参数优化

卡梅德生物技术快报|重组蛋白的表达和纯化:IMAC 金属螯合色谱全流程工艺手册|基质 - 配基 - 金属离子匹配与蛋白质分离纯化参数优化

1 研究背景与现存技术痛点(提出问题)基因工程、蛋白质组学、生物制药研发流程中,蛋白质分离纯化是决定下游实验成败的关键环节。当前实验室常规蛋白质分离纯化工艺存在三类难以标准化的技术瓶颈:传统离子交换、分子筛层析无特异性…

2026/7/13 0:05:20 阅读更多 →

周新闻

互联网大厂 Java 求职面试:燕双非的搞笑回答与技术探讨

互联网大厂 Java 求职面试:燕双非的搞笑回答与技术探讨

互联网大厂 Java 求职面试:燕双非的搞笑回答与技术探讨 在一个阳光明媚的上午,互联网大厂的面试官坐在桌前,准备迎接他的面试候选人——燕双非,一个以搞笑和幽默著称的程序员。第一轮提问 面试官:燕双非,作…

2026/7/13 4:38:36 阅读更多 →
车载以太网PMA测试设备选型:示波器、VNA、信号源3类仪器关键参数与预算评估

车载以太网PMA测试设备选型:示波器、VNA、信号源3类仪器关键参数与预算评估

车载以太网PMA测试设备选型:示波器、VNA、信号源3类仪器关键参数与预算评估在智能驾驶和车联网技术快速发展的今天,车载以太网作为新一代车载网络的核心传输技术,其物理层性能直接决定了数据传输的可靠性和稳定性。1000BASE-T1作为当前主流的…

2026/7/13 4:38:38 阅读更多 →
VSCode EIDE 插件 2.0:APM32/STM32 项目迁移实战,5步完成Keil工程转换

VSCode EIDE 插件 2.0:APM32/STM32 项目迁移实战,5步完成Keil工程转换

VSCode EIDE 插件 2.0:APM32/STM32 项目迁移实战指南嵌入式开发领域正经历一场工具链的静默革命。当传统Keil用户首次打开VSCode的扩展市场搜索EIDE时,往往会惊讶于这个看似简单的插件竟能重构十余年的开发习惯。本文将揭示如何用五个精准步骤&#xff0…

2026/7/13 4:38:40 阅读更多 →

月新闻