为什么ViT需要Patch Embedding?从卷积到Transformer的视觉特征提取演变
如果你在2020年之前问任何一位计算机视觉研究者,处理图像最核心的架构是什么,答案几乎毫无悬念会是卷积神经网络。从AlexNet到ResNet,卷积层通过其固有的局部连接和权重共享特性,成为了理解图像世界的标准语言。然而,当Transformer架构在自然语言处理领域掀起革命后,一个大胆的问题被提了出来:我们能否用同样的方式“阅读”图像?Vision Transformer的诞生给出了肯定的答案,而这场变革的起点,正是Patch Embedding这个看似简单的操作。
Patch Embedding远不止是将图像切成小块然后线性映射。它是连接像素世界与序列化理解之间的桥梁,是让Transformer这种为序列设计的架构能够“看见”图像的关键设计。理解它,不仅是为了看懂ViT的代码,更是为了洞察计算机视觉模型设计思想的一次根本性转向——从基于局部归纳偏好的卷积,转向基于全局关系建模的注意力机制。这篇文章将带你穿越这段思想演变的历史,剖析Patch Embedding背后的设计动机,对比它与传统卷积特征提取的异同,并探讨这一设计如何重塑了我们处理视觉数据的方式。
1. 卷积的黄金时代:局部感知与层次化特征提取
在Transformer进入视觉领域之前,卷积神经网络统治了近十年。要理解Patch Embedding的必要性,我们必须先回到卷积的设计哲学中。
卷积操作的核心思想基于两个关键的归纳偏好:局部性和平移等变性。局部性意味着每个神经元只感受输入图像的一小片区域(感受野),这模拟了生物视觉系统的工作原理。平移等变性则保证了一个特征检测器(比如边缘检测器)在图像的不同位置都能以相同的方式工作。这些偏好并非凭空而来,它们极大地减少了模型的参数数量,让网络能够从有限的数据中高效学习。
一个典型的CNN特征提取流程是这样的:输入图像经过多层卷积、池化操作,特征图的空间尺寸逐渐减小,而通道数(即特征的丰富程度)逐渐增加。早期层捕捉边缘、颜色等低级特征,后期层则组合这些低级特征形成更复杂的概念,如物体部件乃至整个物体。
# 一个简化的CNN特征提取过程示意
import torch.nn as nn
class SimpleCNN(nn.Module):
def __init__(self):
super().__init__()
# 第一层卷积:从原始像素中提取边缘等基础特征
self.conv1 = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, stride=1, padding=1)
# 池化层:降低空间分辨率,增加感受野
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
# 更深层的卷积:组合基础特征形成更复杂的模式
self.conv2 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1)
self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
def forward(self, x):
# 输入x形状: [batch, 3, 224, 224]
x = self.pool1(torch.relu(self.conv1(x))) # -> [batch, 64, 112, 112]
x = self.pool2(torch.relu(self.conv2(x))) # -> [batch, 128, 56, 56]
return x
这种层次化、局部化的处理方式取得了巨大成功,但也存在一些固有的限制。卷积核的大小限制了感受野的范围,要获取全局信息必须依赖深层网络的堆叠。更重要的是,卷积的权重在图像的不同位置是共享的,这虽然带来了平移等变性,但也意味着网络对图像内容的空间结构假设较强——它默认图像的不同区域应该用相同的方式处理。
注意:卷积的这些特性在小到中等规模数据集上表现优异,因为它们提供了强烈的先验,减少了过拟合的风险。但在数据量爆炸式增长的今天,这些先验是否反而成为了限制模型表达能力的瓶颈?这是ViT设计者们思考的起点。
2. Transformer的序列化世界观:从文本到图像的挑战
当我们将目光转向Transformer时,会发现它处理信息的方式与CNN截然不同。Transformer最初为序列数据(如句子)设计,其核心是自注意力机制,允许序列中的每个元素直接与所有其他元素交互,无论它们之间的距离有多远。
在自然语言处理中,输入通常已经是离散的token序列(单词或子词)。但对于图像,我们面对的是连续的、高维的像素网格。直接将每个像素视为一个token会带来灾难性的计算复杂度——对于一张224×224的图像,序列长度将达到50176,自注意力的计算成本与序列长度的平方成正比,这完全不可行。
这就是Vision Transformer面临的根本挑战:如何将二维的、连续的图像数据转化为适合Transformer处理的序列形式,同时保持计算可行性?
早期的尝试大致分为两类:
- 在CNN中引入注意力模块作为补充(如SENet、CBAM)
- 使用局部注意力或稀疏注意力来近似全局交互
但这些方法都没有完全摆脱CNN的骨架。直到ViT论文提出,研究者们才意识到,也许我们可以采取更激进的方式:完全抛弃卷积,但首先需要对图像进行一种“序列化预处理”。这个预处理就是Patch Embedding。
下表对比了CNN、早期混合方法以及ViT在处理图像时的核心差异:
| 特性 | 传统CNN | CNN+注意力混合模型 | Vision Transformer (ViT) |
|---|---|---|---|
| 核心操作 | 卷积、池化 | 卷积+注意力 | 自注意力 |
| 感受野 | 局部,随深度增大 | 局部+受限全局 | 全局(从第一层开始) |
| 归纳偏好 | 强(局部性、平移等变) | 中等 | 弱(最小化先验) |
| 输入形式 | 原始像素网格 | 原始像素网格 | 图像块序列 |
| 计算复杂度 | O(H×W×C×K²) | O(H×W×C×K²) + O(N²×D) | O(N²×D) |
| 数据需求 | 相对较少 | 中等 | 大量 |
注:H,W为图像高宽,C为通道数,K为卷积核尺寸,N为patch数量,D为嵌入维度
3. Patch Embedding的设计哲学:平衡信息保留与计算效率
Patch Embedding的精妙之处在于,它在信息保留和计算效率之间找到了一个优雅的平衡点。让我们深入分析这个设计的每个方面。
3.1 图像分块:从像素网格到视觉“单词”
将图像分割成固定大小的非重叠块,这看似简单的操作背后有深刻的考量。每个patch(例如16×16像素)可以看作是一个“视觉单词”,就像NLP中的token一样。这个大小的选择不是随意的:
- 16×16像素:足够包含有意义的局部结构(如眼睛的一部分、车轮的一段),但又不会太大以至于失去局部性
- 非重叠分割:确保每个像素只属于一个patch,避免冗余计算
- 固定大小:简化了后续的线性投影操作,所有patch被映射到相同维度的向量空间
对于一张224×224的图像,使用16×16的patch会得到196个视觉单词。与50176个像素相比,序列长度减少了256倍,这使得全局自注意力变得可行。
# Patch Embedding的直观理解:将图像视为视觉单词的集合
import torch
import torch.nn as nn
def visualize_patching(image_tensor, patch_size=16):
"""
展示图像如何被分割成patch
image_tensor: [1, 3, 224, 224]
"""
B, C, H, W = image_tensor.shape
# 计算patch数量
num_patches_h = H // patch_size # 14
num_patches_w = W // patch_size # 14
num_patches = num_patches_h * num_patches_w # 196
# 将图像分割成patch网格
patches = image_tensor.unfold(2, patch_size, patch_size).unfold(3, patch_size, patch_size)
# patches形状: [1, 3, 14, 14, 16, 16]
# 重新排列维度,将每个patch展平
patches = patches.permute(0, 2, 3, 1, 4, 5).contiguous()
patches = patches.view(B, num_patches, C * patch_size * patch_size)
# 现在patches形状: [1, 196, 768],每个patch是768维向量
return patche



被折叠的 条评论
为什么被折叠?



