(ViT) AN IMAGE IS WORTH 16X16 WORDS: TRANSFORMERS FOR IMAGE RECOGNITION AT SCALE(含实现代码)
论文阅读笔记:[AN IMAGE IS WORTH 16X16 WORDS: TRANSFORMERS FOR IMAGE RECOGNITION AT SCALE]
状态: 🟢 已精读
1. 元数据 (Metadata)
| 条目 | 内容 |
|---|---|
| 年份 | 2021 |
| 论文链接 | https://arxiv.org/abs/2010.11929 |
| 代码链接 | https://github.com/wbzuo/paper-impl/tree/main/papers/vit |
| 我的标签 | 计算机视觉 |
2. 摘要与核心问题 (Abstract & Core Problem)
虽然Transformer架构已经成为自然语言处理任务的标准,但它在计算机视觉中的应用仍然有限。
在视觉中,注意力要么与卷积网络一起应用,要么用于替换卷积网络的某些组件,同时保持其整体结构不变。本文表明,这种对CNN的依赖是不必要的,并且直接应用于图像Patch(块序列)的仅有的Transformer可以很好地执行图像分类任务。当对大量数据进行预训练并传输到多个中型或小型图像识别基准(ImageNet、CIFAR-100、VTAB等)时,与最先进的卷积网络相比,ViT获得了优异的结果,同时训练所需的计算资源少得多。
- 论文要解决什么问题?
- 摆脱对卷积神经网络(CNN)的依赖,探索一个“纯粹”的Transformer架构是否能够成为视觉任务的主流骨干网络。
- 解决Transformer在视觉中的直接应用难题:将原本为序列设计的Transformer直接应用于像素会面临计算复杂度高(与像素数的平方成正比)和缺乏视觉归纳偏置(如局部性、平移不变性)两大挑战。本文需要提出一种方法,将2D图像有效地转化为Transformer能够处理的1D序列,并克服数据效率低下的问题。
- 核心主张或贡献是什么?
-
提出Vision Transformer架构:
将图像分割成固定大小的图像块,将每个图像块线性嵌入成一个向量(视为一个“词”),并加上位置编码,形成一个序列。这个序列可以直接输入标准Transformer编码器进行处理。 -
证明“大规模预训练”胜过“归纳偏置”:
在中小型数据集(如ImageNet)上,由于缺乏CNN固有的归纳偏置,ViT的性能会略低于同等规模的ResNet。当在超大规模数据集上进行预训练时,ViT的性能产生质的飞跃。模型能够直接从数据中学习到所需的视觉结构,从而在迁移到多个下游任务时,达到或超越了当时的SOTA性能。这证明了数据的规模可以弥补甚至超越精心设计的结构先验。
-
好的,这是根据论文内容整理的“背景知识”部分。
3. 背景知识 (Background)
前置知识/术语
-
Transformer: 完全基于自注意力机制,摒弃了循环和卷积结构。它已成为自然语言处理领域的标准模型。其核心组件包括多头自注意力 和前馈神经网络,通常使用编码器架构。
-
自注意力 / 多头自注意力:
- 自注意力:一种机制,允许序列中的每个元素(如一个词)与序列中的所有其他元素进行交互,通过计算“注意力权重”来决定在处理当前元素时应该“关注”其他元素的多少信息。
- 多头自注意力:将自注意力机制并行执行多次(即多个“头”),每个头可以学习关注序列中不同方面的信息,最后将各头的输出拼接并线性变换,从而增强模型的表示能力。
-
归纳偏置: 指模型为学习问题而内置的假设或先验知识。对于CNN,其归纳偏置包括局部性(相邻像素关联性强)和平移等变性(物体在图像中移动,其表征也应相应移动)。ViT则缺乏这种强图像归纳偏置。
-
位置编码: 由于Transformer本身不包含序列顺序信息,需要额外向输入中添加表示其位置信息的编码。在ViT中,这是通过可学习的一维位置嵌入实现的,被加到每个图像块嵌入上,以让模型理解图像块的空间关系。
相关工作简要回顾
论文中提到的相关研究工作主要分为以下几类:
-
CNN与注意力的结合:
- 方法:在CNN架构中引入自注意力模块,例如通过注意力机制来增强特征图,或使用注意力对CNN的输出进行后处理。应用于图像分类、目标检测、视频处理等。
- 代表工作:Non-local Neural Networks, Attention Augmented Convolutional Networks, DETR。
-
用自注意力完全替代卷积:
- 方法:尝试用局部自注意力或稀疏全局注意力等机制来构建不包含卷积的“纯”注意力模型。
- 代表工作:Stand-alone Self-Attention, Local Relation Networks, Sparse Transformers。
- 局限性:这些模型虽然理论计算效率高,但由于使用了特殊的注意力模式,在当时难以在现代硬件加速器上进行有效扩展,导致在大型图像识别任务中,经典的类ResNet架构仍然是主流。
-
基于Transformer的生成式图像模型:
- 方法:将Transformer直接应用于图像像素,但通常需要降低图像分辨率或颜色空间,并以无监督方式(如自回归建模)进行训练。
- 代表工作:Image GPT。
- 局限性:在ImageNet上的最高准确率仅为72%,性能未能超越有监督的CNN。
-
大规模监督学习:
- 方法:研究CNN性能如何随数据集规模扩大而提升,并探索从超大数据集进行迁移学习。
- 代表工作:Big Transfer。
- 局限性:这些工作都局限于使用CNN作为核心架构。
局限性总结与本文动机
综上所述,在ViT之前,相关工作的局限性在于:
- 对CNN的依赖:主流方法要么是CNN的变体,要么与CNN强耦合。
- “纯”注意力模型难以有效扩展:已有的完全基于注意力的模型,或因硬件支持问题,或因性能问题,未能撼动CNN的地位。
- 缺乏对标准Transformer在视觉中潜力的探索:未能系统性地证明,一个尽可能不做修改的标准Transformer,在足够大规模数据的支持下,能够在视觉任务上达到顶尖水平。
因此,本文的动机是: 直接、最小修改地将标准Transformer架构应用于图像序列,并通过大规模预训练来验证其能否克服归纳偏置的不足,最终在性能和效率上超越传统的CNN。
4. 方法/模型 (Methodology/Model)
这里我将以ViT分为不同模块来讲解,下图为总体架构

4.1 Patch Embedding + Position Embedding + CLS
如下图所示,Vision Transformer 的核心创新在于将 图像处理重新定义为序列建模问题。这一过程包含四个关键步骤:
4.1.1 图像分块
- 输入:一张
224 × 224 × 3的 RGB 图像 - 分块策略:将图像均匀分割成
16 × 16像素的小块 - 数学关系:
- 每行块数:
224 ÷ 16 = 14 - 每列块数:
224 ÷ 16 = 14 - 总块数:
14 × 14 = 196个 patches
- 每行块数:
- 每个 Patch:包含
16 × 16 × 3 = 768个像素值
4.1.2 块嵌入
- 每个
16×16×3的图像块被展平为长度为768的向量 - 通过一个线性投影层(在代码中常实现为卷积层)映射到固定的嵌入维度(如 768 维)
- 此时图像被转换为:
196 个嵌入向量,每个向量维度为 768(形状:[batch_size, 196, 768])
注:这里有一个重要的细节。投影后的维度(例如768)是独立选择的,并不必须等于展平后的像素数(16x16x3=768这只是一种巧合)。我们可以选择投影到512维、768维或1024维。
4.1.3 添加特殊标记
- [CLS] Token:在序列开头添加一个特殊的、可学习的分类标记
- 这个标记在整个 Transformer 处理过程中会聚合全局信息
- 序列变为:
[CLS] + 196 个图像块 = 197 个嵌入向量(形状:[batch_size, 197, 768])
4.1.4 位置编码
- 由于 Transformer 本身不包含位置信息,需要显式添加位置编码
- 为序列中的每个位置(包括 [CLS])学习一个独特的可学习的位置向量
- 位置向量与嵌入向量逐元素相加,为模型提供空间关系信息
- 最终输出:一个包含了图像内容信息和空间位置信息的序列,可以直接输入 Transformer 编码器。
NLP 与 CV 的完美类比
| 组件 | NLP (文本处理) | CV (图像处理) | 功能说明 |
|---|---|---|---|
| 基本单元 | 一句话/段落 | 一张图片 | 处理的完整对象 |
| 处理单元 | 词元/单词 | 图像块 | 模型处理的基本元素 |
| 序列长度 | 词元数量 | 图像块数量 | 决定计算复杂度 |
| 嵌入层 | 词嵌入 | 块嵌入 | 将离散单元映射到连续向量空间 |
| 位置信息 | 位置编码 | 位置编码 | 提供序列中的顺序/空间关系 |
| 分类标记 | [CLS] Token | [CLS] Token | 聚合全局信息用于分类 |
最终结果
通过这一系列变换:
- 输入:
224 × 224 × 3的二维图像 - 输出:
50 × 768的一维序列
这个序列可以直接输入标准 Transformer 编码器进行处理,使得原本为 NLP 设计的 Transformer 架构能够"理解"图像内容,实现了计算机视觉任务的序列化处理范式。

class PatchEmbedding(nn.Module):
def __init__(self, in_channels: int = 3, patch_size: int = 16, emb_size: int = 768, img_size: int = 224):
super().__init__()
self.patch_size = patch_size
# 224 = 14 * 16
# 做法1.使用卷积层(b, 14 * 14, 3 * 16 * 16) 主要做法
self.projection = nn.Sequential(
# 使用一个卷积层而不是一个线性层 -> 性能增加
# b c h w -> b e h // p w // p
nn.Conv2d(in_channels, emb_size, kernel_size=patch_size, stride=patch_size),
# 将卷积操作后的patch铺平
# b e h // p w // p ->
Rearrange('b e h w -> b (h w) e'),
)
# 做法2.使用线性层实现
# self.projection = nn.Sequential(
# # 将原始图像切分为16*16的patch并把它们拉平
# Rearrange('b c (h s1) (w s2) -> b (h w) (s1 s2 c)', s1=patch_size, s2=patch_size),
# # 注意这里的隐层大小设置的也是768,可以配置
# nn.Linear(patch_size * patch_size * in_channels, emb_size)
# )
# 生成cls_token的emb_size 添加在序列最前面的
self.cls_token = nn.Parameter(torch.randn(1, 1, emb_size))
num_patches = (img_size // patch_size) ** 2
self.positions = nn.Parameter(torch.randn(num_patches+ 1, emb_size))
# 添加dropout
self.dropout = nn.Dropout(0.2)
def forward(self, x):
b, _, _, _ = x.shape
# patch操作
x = self.projection(x)
# cls_token拓展b次 (b, 1, emb_size)
cls_tokens = repeat(self.cls_token, '() n e -> b n e', b = b)
# cls_tokens和patch进行拼接
x = torch.cat([cls_tokens, x], dim = 1)
# 添加位置编码
x += self.positions
return x
4.2 Transformer Encoder
Transformer Encoder 是 ViT 的核心组成部分,负责对序列化的图像块进行深度特征提取和关系建模。
4.2.1 整体架构
输入: [batch_size, num_patches + 1, hidden_dim]
↓
Transformer Encoder (L个相同的层)
↓
输出: [batch_size, num_patches + 1, hidden_dim]
4.2.2 单个Encoder层详细结构
class TransformerEncoderLayer(nn.Module):
"""
Single layer of Transformer encoder
Input: (B, N, D)
Output: (B, N, D)
"""
def __init__(self, embed_dim=256, num_heads=8, hidden_dim=1024, dropout=0.1):
super().__init__()
# Layer normalization before MHSA
self.ln1 = nn.LayerNorm(embed_dim)
# Multi-Head Self-Attention
self.mha = MultiHeadAttention(embed_dim, num_heads, dropout)
# Layer normalization before MLP
self.ln2 = nn.LayerNorm(embed_dim)
# MLP block
self.mlp = MLP(embed_dim, hidden_dim, dropout)
def forward(self, x):
# Residual connection + MHSA
x = x + self.mha(self.ln1(x)) # (B, 65, 256)
# Residual connection + MLP
x = x + self.mlp(self.ln2(x)) # (B, 65, 256)
return x
输入预处理
输入 → LayerNorm → 进入Multi-Head Attention
Multi-Head Attention Block
- 功能: 建立全局依赖关系,让每个位置都能关注所有其他位置
- 流程:
- 生成Q、K、V矩阵(每个头独立)
- 计算注意力权重:
Softmax(Q·K^T/√d_k) - 加权求和:
Attention = Weights · V - 多头输出拼接并线性投影
class MultiHeadAttention(nn.Module):
def __init__(self, emb_size = 512, num_heads = 8, dropout = 0):
super().__init__()
self.emb_size = emb_size
self.num_heads = num_heads
self.keys = nn.Linear(emb_size, emb_size)
self.querys = nn.Linear(emb_size, emb_size)
self.values = nn.Linear(emb_size, emb_size)
self.att_drop = nn.Dropout(dropout)
self.projection = nn.Linear(emb_size, emb_size)
def forward(self, x, mask = None):
queries = rearrange(self.queries(x), "b n (h d) -> b h n d", h=self.num_heads)
keys = rearrange(self.keys(x), "b n (h d) -> b h n d", h=self.num_heads)
values = rearrange(self.values(x), "b n (h d) -> b h n d", h=self.num_heads)
energy = torch.einsum('bhqd, bhkd -> bhqk', queries, keys) # batch, num_heads, query_len, key_len
if mask is not None:
fill_value = torch.finfo(torch.float32).min
energy.mask_fill(~mask, fill_value)
scaling = self.emb_size ** (1/2)
att = F.softmax(energy, dim=-1) / scaling
att = self.att_drop(att)
# sum up over the third axis
out = torch.einsum('bhal, bhlv -> bhav ', att, values)
out = rearrange(out, "b h n d -> b n (h d)")
out = self.projection(out)
return out
残差连接: Attention_Output = Attention(LayerNorm(Input)) + Input
MLP Block
- 结构: 两层的全连接网络
- 扩展层:
hidden_dim → 4×hidden_dim - 压缩层:
4×hidden_dim → hidden_dim
- 扩展层:
- 激活函数: GELU(高斯误差线性单元)
- 作用: 对每个位置的特征进行非线性变换和增强
class MLP(nn.Module):
"""
Multi-Layer Perceptron for Transformer encoder
Input: (B, N, D)
Output: (B, N, D)
"""
expansion = 4
def __init__(self, embed_dim=256, hidden_dim=1024, dropout=0.1):
super().__init__()
self.fc1 = nn.Linear(embed_dim, hidden_dim) # Expand to 4x dimension
self.fc2 = nn.Linear(hidden_dim, embed_dim) # Project back
self.dropout = nn.Dropout(dropout)
self.gelu = nn.GELU() # Activation function
def forward(self, x):
x = self.fc1(x) # (B, 65, 256) -> (B, 65, 1024)
x = self.gelu(x)
x = self.dropout(x)
x = self.fc2(x) # (B, 65, 1024) -> (B, 65, 256)
x = self.dropout(x)
return x
残差连接: MLP_Output = MLP(LayerNorm(Attention_Output)) + Attention_Output
在ViT中的具体配置
模型变体参数
| 模型 | 层数(L) | 隐藏维度 | MLP大小 | 头数 | 参数量 |
|---|---|---|---|---|---|
| ViT-Base | 12 | 768 | 3072 | 12 | 86M |
| ViT-Large | 24 | 1024 | 4096 | 16 | 307M |
| ViT-Huge | 32 | 1280 | 5120 | 16 | 632M |
[CLS] Token的最终状态
- 经过所有L层Encoder后,[CLS] token包含了整个图像的压缩表示
- 用于最终的分类任务:
y = Classifier(Encoder_Output[:, 0])
Patch Embeddings的输出
- 其他位置的输出可用于密集预测任务(如分割、检测)
- 保留了空间结构和局部信息
这种设计使得Transformer Encoder能够:
- 捕捉长距离依赖:不受卷积核大小的限制
- 保持平移不变性:通过位置编码而非卷积
- 高效并行计算:自注意力的矩阵运算高度并行化
- 可扩展性强:通过增加层数和维度提升性能

5. 结论 (Conclusion)
主要贡献:
-
提出了第一个完全基于标准Transformer架构的纯视觉模型(ViT),摆脱了对CNN的依赖,将图像处理重新定义为序列建模问题。
-
仅通过将图像分割成块、线性投影、添加位置编码,即可直接应用NLP中的标准Transformer,无需复杂的视觉专用修改。
-
首次系统证明大规模预训练可以超越归纳偏置——当数据量足够大时(JFT-300M),ViT的性能显著优于基于强归纳偏置的CNN模型。
局限性:
-
数据依赖性:在中小型数据集(如ImageNet)上,ViT的性能不如同等规模的ResNet,表明其需要大量数据来弥补缺乏的视觉归纳偏置。
-
计算复杂度:自注意力机制在序列长度上的二次复杂度,限制了其处理超高分辨率图像的能力。
6. 个人总结
ViT的核心贡献是完成了视觉领域的范式转变:它用“视觉即序列”的新思路打破卷积垄断,证明了大尺度数据能够超越人工设计的归纳偏置;虽然依赖大数据,但其开创性架构催生了DeiT、Swin Transformer等后续工作,重塑了现代计算机视觉的发展路径。
这篇论文开辟了视觉Transformer这一全新研究方向,启发了后续大量工作,堪称视觉领域的"Attention Is All You Need"时刻。
更多推荐

所有评论(0)