共计 3365 个字符,预计需要花费 9 分钟才能阅读完成。
背景痛点
在计算机视觉领域,传统的卷积神经网络(CNN)长期以来是主流架构。但随着任务复杂度提升,其固有局限性逐渐显现:

- 局部感受野限制 :标准卷积核(如 3 ×3)只能捕捉局部邻域信息,难以建模远距离像素间关系。对于需要全局理解的场景(如物体遮挡、长条形物体识别),需堆叠多层卷积才能扩大感受野,导致计算成本增加
- 平移不变性双刃剑 :CNN 的共享权重特性虽能处理平移不变性,但会忽略关键位置信息。例如在医疗图像中,肿瘤的绝对位置可能是重要诊断依据
与此同时,Transformer 在自然语言处理(NLP)领域取得突破后,研究者开始探索其在视觉任务中的应用。2020 年提出的 Vision Transformer(ViT)证明,纯 Transformer 架构在足够数据量下可超越 CNN 的性能,由此开启了视觉 Transformer 的新纪元。
核心原理对比
CNN 的局部特征提取
典型 CNN 通过卷积层渐进式组合特征:
- 低层卷积检测边缘、纹理等基础模式
- 中层卷积组合基础特征形成部件(如车轮、窗户)
- 高层卷积整合全局语义(如整个车辆)
数学上,单个卷积层的计算可表示为:
$$
\mathbf{Y}(i,j) = \sum_{m=0}^{k-1}\sum_{n=0}^{k-1} \mathbf{W}(m,n) \cdot \mathbf{X}(i+m, j+n) + \mathbf{b}
$$
其中 $k$ 为卷积核尺寸,这种局部连接特性是 CNN 处理图像的基础。
Transformer 的全局注意力
Transformer 的核心是 self-attention 机制,其计算过程分为三步:
- 将输入 $\mathbf{X}$(图像分块展开后的序列)通过线性变换得到 Query、Key、Value 矩阵:
$$
\mathbf{Q} = \mathbf{X}\mathbf{W}_Q, \quad \mathbf{K} = \mathbf{X}\mathbf{W}_K, \quad \mathbf{V} = \mathbf{X}\mathbf{W}_V
$$ - 计算注意力权重矩阵:
$$
\text{Attention}(\mathbf{Q},\mathbf{K},\mathbf{V}) = \text{softmax}(\frac{\mathbf{Q}\mathbf{K}^T}{\sqrt{d_k}})\mathbf{V}
$$
其中 $d_k$ 是 Key 向量的维度,缩放因子用于控制梯度稳定性 - 多头注意力(Multi-Head Attention)将上述过程并行执行多次后拼接结果,增强模型捕捉不同子空间信息的能力
与 CNN 的固定局部连接不同,self-attention 能动态建立任意两个图像块(patch)间的关联,如图 1 所示(可视化示例见附录)。
代码实战
CNN 图像分类器实现
import torch
import torch.nn as nn
class SimpleCNN(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.features = nn.Sequential(
# 卷积层 1: 3 通道输入, 64 通道输出, 3x3 卷积核
nn.Conv2d(3, 64, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
# 最大池化: 2x2 窗口, 步长 2
nn.MaxPool2d(kernel_size=2, stride=2),
# 卷积层 2: 64->128 通道
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2)
)
self.classifier = nn.Sequential(nn.Linear(128 * 8 * 8, 256), # 假设输入图像 32x32
nn.ReLU(),
nn.Linear(256, num_classes)
)
def forward(self, x):
x = self.features(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x
ViT 关键模块实现
class PatchEmbedding(nn.Module):
"""将图像分割为不重叠的 patch 并线性嵌入"""
def __init__(self, img_size=224, patch_size=16, in_chans=3, embed_dim=768):
super().__init__()
self.img_size = img_size
self.patch_size = patch_size
self.n_patches = (img_size // patch_size) ** 2
# 卷积实现分块与线性投影
self.proj = nn.Conv2d(
in_chans, embed_dim,
kernel_size=patch_size,
stride=patch_size
)
def forward(self, x):
x = self.proj(x) # (B, E, H/P, W/P)
x = x.flatten(2) # (B, E, N)
x = x.transpose(1, 2) # (B, N, E)
return x
class VisionTransformer(nn.Module):
def __init__(self, ...):
super().__init__()
self.patch_embed = PatchEmbedding()
# 添加 class 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]
# 获取 patch 嵌入
x = self.patch_embed(x) # (B, N, E)
# 扩展 class token 到 batch 中所有样本
cls_tokens = self.cls_token.expand(B, -1, -1)
# 拼接 class token
x = torch.cat((cls_tokens, x), dim=1)
# 添加位置编码
x = x + self.pos_embed
...
常见陷阱与解决方案
- 显存溢出问题
- 现象:训练时 GPU 显存不足
- 原因:未合理设置 patch 大小,导致序列长度($\frac{H \times W}{P^2}$)过大
-
解决:
- 增大 patch 尺寸(如从 16×16 调整为 32×32)
- 降低 batch size
- 使用梯度累积
-
位置编码失效
- 现象:模型无法区分相同内容在不同位置的情况
- 原因:未正确添加位置信息或使用固定尺寸的位置编码处理不同分辨率输入
-
解决:
- 采用可学习的位置编码(优于固定正弦编码)
- 对位置编码进行插值适应不同输入尺寸
-
训练不稳定
- 现象:损失值剧烈波动或出现 NaN
- 原因:学习率设置过高或未使用适当的归一化
- 解决:
- 使用 warmup 学习率调度
- 添加 Layer Normalization
- 梯度裁剪(如设置 max_norm=1.0)
性能对比实验
在 CIFAR-10 数据集上的对比结果(RTX 3090):
| 指标 | ResNet-18 | ViT-Tiny |
|---|---|---|
| 参数量 | 11.2M | 5.7M |
| 训练时间 /epoch | 45s | 68s |
| 显存占用 | 1.8GB | 3.2GB |
| 测试准确率 | 93.5% | 91.2% |
注:ViT 在小数据集上通常需要更强正则化或预训练
架构融合与未来方向
现代视觉架构正走向 CNN 与 Transformer 的融合:
- 局部 - 全局平衡 :Swin Transformer 通过移位窗口实现层次化注意力,在局部窗口内计算 self-attention 降低计算量
- 混合架构 :Conformer 等模型在浅层使用 CNN 提取局部特征,深层用 Transformer 建模全局关系
- 效率优化 :MobileViT 等轻量设计将卷积操作引入 ViT,提升移动端部署效率
对于初学者,建议:
1. 从小规模 CNN+Transformer 混合模型开始实践
2. 优先使用成熟实现(如 timm 库中的 swin_transformer)
3. 关注模型可解释性工具(如 Attention Rollout)分析特征学习过程
