三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

从NLP到CV:Transformer跨界计算机视觉的核心原理与实战指南

从NLP到CV:Transformer跨界计算机视觉的核心原理与实战指南

1. 从NLP到CV:Transformer的跨界之旅

如果你在2020年之前问我,Transformer是什么,我会毫不犹豫地告诉你,它是一种在自然语言处理领域大放异彩的模型架构,其核心的自注意力机制让机器理解长距离文本依赖关系的能力得到了质的飞跃。但今天,当“Transformer”这个词再次被提及时,语境已经发生了翻天覆地的变化。它不再仅仅是BERT、GPT这些NLP巨头的专属引擎,而是成为了计算机视觉领域一股无法忽视的、甚至可以说是颠覆性的力量。从图像分类到目标检测,再到图像生成,Transformer架构正在重新定义我们处理视觉信息的方式。这篇指南,就是为你拆解这场跨界革命的核心逻辑、关键技术细节以及在实际视觉任务中落地时,那些文档里不会写的实战心得。

为什么视觉领域需要Transformer?传统的卷积神经网络(CNN)统治了计算机视觉近十年,它通过局部感受野和权值共享,高效地捕捉图像的局部特征,如图像的边缘、纹理。这套范式非常成功,ResNet、EfficientNet等经典网络至今仍是许多任务的基石。然而,CNN也存在其固有的局限性:卷积核的局部性决定了它难以直接建模图像中远距离像素间的全局依赖关系。虽然可以通过堆叠更深的层或使用更大的卷积核来扩大感受野,但这在计算效率和模型表达能力上并非最优解。而视觉任务中,这种全局关系恰恰至关重要——识别一只猫,需要同时看到它的耳朵、胡须和尾巴;理解一张街景图片,需要将分散的行人、车辆、交通标志关联起来。

Transformer的自注意力机制,天生就是为了建模这种全局依赖而生的。它允许序列中的任何一个元素(在视觉任务中,就是图像的一个个“块”或“像素”)直接与序列中的所有其他元素进行交互,计算它们之间的相关性权重。这种“全局视野”是CNN通过层层卷积间接获得的,而Transformer可以更直接、更灵活地实现。正是这一根本优势,催生了Vision Transformer等一系列模型的诞生,开启了视觉Transformer的时代。接下来,我们将深入这个新时代的核心。

2. Vision Transformer:如何将图像“喂”给Transformer

将原本为序列设计的Transformer应用于图像,第一个要解决的、也是最关键的问题就是:如何把二维的图像数据转换成Transformer能理解的序列?Vision Transformer的解决方案既巧妙又直接,可以概括为“分块、展平、嵌入”三步。

2.1 图像分块与线性投影

假设我们有一张分辨率是 H x W 像素,通道数为 C 的输入图像。ViT的做法是,将这张图像均匀地分割成 N 个固定大小的图像块。每个图像块的大小为 P x P 像素。那么,图像块的总数 N 就等于 (H / P) * (W / P)。例如,一张224x224的图片,如果使用16x16的分块大小,那么就会得到 (224/16) * (224/16) = 14 * 14 = 196 个图像块。

每个图像块本身是一个三维张量,形状为 (P, P, C)。ViT接下来将这个三维块沿着空间维度展平,变成一个长度为 (P * P * C) 的一维向量。对于16x16的块和3通道的RGB图像,这个向量的长度就是 16163 = 768。

现在,我们有了196个长度为768的向量。但这只是原始的像素值,还需要将它们映射到Transformer模型隐藏层的大小 D 上。这一步通过一个可训练的线性投影层(一个全连接层)来完成。这个投影层,通常也被称为“Patch Embedding”层。经过投影后,我们得到了一个形状为 (N, D) 的序列,其中 N 是图像块的数量,D 是模型的隐藏维度(例如768)。至此,二维图像被成功地转换成了一个长度为 N 的序列,序列中的每个元素都是一个 D 维的向量,代表了一个图像块的抽象特征。

注意:分块大小 P 是一个超参数,需要在模型容量和计算效率之间权衡。更小的 P(如8或14)会产生更多的图像块(N更大),序列更长,模型能捕捉更细粒度的信息,但计算自注意力的成本会以 O(N²) 的复杂度急剧上升。更大的 P(如32)则相反。ViT-Base/16(Patch Size=16)是一个常用的平衡点。

2.2 可学习的分类令牌与位置编码

在NLP的BERT中,有一个特殊的[CLS]令牌,用于汇聚整个句子的信息以进行分类。ViT借鉴了这一思想,在输入序列的开头,额外添加了一个可学习的嵌入向量,称为“分类令牌”。这个令牌本身不来自任何图像块,它是一个模型需要学习的参数,形状为 (1, D)。我们将它拼接到图像块序列之前,于是输入序列的形状变成了 (N+1, D)。

为什么需要这个额外的令牌?因为Transformer的输出是对应于每个输入位置的向量。对于分类任务,我们需要一个单一的、能够代表整张图片的向量。让模型自己学习一个专门的令牌来聚合全局信息,比从所有图像块输出中做平均池化或选择某一个,在理论上更灵活,实践中也通常更有效。

另一个至关重要的组件是位置编码。自注意力机制本身是排列不变的,它处理序列[A, B, C][C, B, A]在理论上没有区别。但对于图像而言,像素或图像块的空间位置信息是至关重要的。左上角的一块天空和右下角的一块天空含义可能完全不同。因此,我们必须向模型注入位置信息。

ViT采用的方式是标准的、可学习的一维位置编码。我们创建一个形状为 (N+1, D) 的可学习参数矩阵,其中每一行对应一个位置(包括分类令牌的位置)。在输入Transformer编码器之前,我们将这个位置编码矩阵直接加到图像块嵌入(含分类令牌)矩阵上:输入 = 嵌入 + 位置编码。这样,模型就能在学习过程中,区分不同位置的图像块了。

实操心得:关于位置编码,后来的一些研究(如Swin Transformer)表明,对于视觉任务,相对位置编码或二维感知的位置编码可能比绝对一维位置编码更合适,因为图像的本质是二维的。但在标准的ViT中,可学习的一维绝对位置编码已经能取得非常好的效果,且实现简单。在实际复现时,务必确保位置编码只加一次,并且在推理时,如果输入图像大小与训练时不同(导致N变化),需要如何处理位置编码是一个需要小心处理的问题(通常需要对预训练的位置编码进行插值)。

3. Transformer编码器在视觉中的核心:自注意力与MLP

经过嵌入和位置编码后,得到的 (N+1, D) 序列就被送入由 L 个完全相同的层堆叠而成的Transformer编码器。每一层都包含两个核心子层:多头自注意力层和前馈网络层,每个子层前后都应用了残差连接和层归一化。

3.1 多头自注意力机制详解

自注意力是Transformer的灵魂。对于视觉任务,它的工作方式如下:

  1. 生成Q, K, V:对于输入序列 X(形状 (N+1, D)),我们通过三个不同的线性变换矩阵 W_q, W_k, W_v(形状均为 (D, D_k),其中 D_k 是每个头的维度),将其分别投影为查询、键和值向量:Q = XW_q, K = XW_k, V = XW_v。这里,D_k 通常设置为 D / h,h 是注意力头的数量。

  2. 计算注意力分数:注意力分数的核心是衡量序列中每个元素(查询)与所有元素(键)的相关性。计算公式为:注意力分数 = softmax( (Q * K^T) / sqrt(D_k) )。这里,Q * K^T 得到一个 (N+1, N+1) 的矩阵,其中第 i 行第 j 列的值,就代表了第 i 个位置对第 j 个位置的“关注程度”。除以 sqrt(D_k) 是为了防止点积结果过大导致softmax梯度消失。

  3. 加权求和:将上一步得到的注意力权重矩阵与值矩阵 V 相乘:输出 = 注意力分数 * V。这就意味着,每个位置的输出,都是所有位置值的加权和,权重由该位置与其它位置的相关性决定。

“多头”的意义在于,模型可以并行地学习多种不同的关注模式。例如,一个头可能专注于捕捉颜色相似性,另一个头专注于纹理连续性,第三个头可能关注空间相邻关系。最后,将所有头的输出拼接起来,再经过一个线性投影,得到最终的自注意力层输出。

在视觉中,这意味着一个图像块可以同时关注到颜色相近的块、纹理连续的块以及空间上可能很远但语义相关的块(如猫的头部和尾巴)。这种能力是CNN通过多层卷积才能间接、隐式地学习到的。

3.2 前馈网络与层设计

自注意力层的输出,会经过一个前馈网络。这个FFN通常是一个简单的两层MLP,中间有一个非线性激活函数(如GELU):FFN(x) = GELU(xW1 + b1)W2 + b2。它的作用是对每个位置的特征进行独立的、非线性的变换和增强。

残差连接和层归一化是稳定深层模型训练的关键。每个子层(MSA和FFN)的输出都是LayerNorm(子层输入 + 子层函数(子层输入))。这种设计极大地缓解了梯度消失问题,使得堆叠数十甚至上百层Transformer成为可能。

经过 L 层这样的处理,序列中的每个令牌都融合了全局信息。最终,我们取分类令牌对应的输出向量(即序列的第一个位置),通过一个小的分类头(通常是单个线性层),映射到目标类别数,用于图像分类任务。

踩坑实录:训练视觉Transformer,尤其是大型ViT,对优化器和超参数非常敏感。AdamW优化器几乎是标配,并且需要一个精心设计的学习率warmup和cosine衰减策略。直接使用训练CNN的套路(如SGD with momentum)往往难以收敛或效果很差。这是因为Transformer的初始化方式和训练动态与CNN不同。一个常见的技巧是使用梯度裁剪来防止训练初期的不稳定。

4. 超越ViT:视觉Transformer的演进与变体

标准的ViT证明了Transformer在视觉上的可行性,但它也有明显的缺点:计算复杂度高(O(N²)),且缺乏像CNN那样的层次化归纳偏置(即局部性、平移不变性)。后续的研究主要围绕这两个方向展开。

4.1 层次化架构:Swin Transformer

Swin Transformer的提出,是视觉Transformer发展中的一个里程碑。它引入了两个关键思想:层级特征图滑动窗口注意力

层级特征图:Swin Transformer像CNN一样,构建了特征金字塔。它通过“Patch Merging”操作,在多个阶段逐渐合并相邻的图像块,从而在深层减少序列长度、增加每个令牌的感受野和特征维度。这产生了类似于CNN中“高分辨率-低语义”到“低分辨率-高语义”的特征图,非常适合于需要多尺度特征的任务,如目标检测和语义分割。

滑动窗口注意力:为了降低全局自注意力的计算复杂度,Swin Transformer将注意力计算限制在不重叠的局部窗口内。例如,将特征图划分为多个7x7的窗口,只在每个窗口内部计算自注意力。这样,计算复杂度就从与图像块数量的平方相关,变为与窗口大小(固定)的平方相关,是线性的复杂度。

但仅仅这样会失去窗口间的信息交互。因此,Swin Transformer采用了移位窗口机制。在下一层,窗口的划分网格会进行偏移(例如向右下角偏移半个窗口),使得新的窗口能够覆盖上一层中不同窗口的部分,从而实现了跨窗口的信息传递。这种设计在效率和建模能力之间取得了极佳的平衡。

4.2 高效注意力机制探索

除了滑动窗口,还有许多其他工作致力于降低自注意力的计算负担:

  • 轴向注意力:将二维的全局注意力分解为两个一维操作,先在行方向做注意力,再在列方向做注意力。复杂度从O(N²)降为O(N√N)。
  • 稀疏注意力:只让每个位置关注特定的、预先定义好的一组位置(如局部邻域、随机位置、或按步长采样的位置)。
  • 线性注意力:通过核函数近似,将softmax注意力中的QK^T计算顺序改写,实现理论上的线性复杂度。
  • 蒸馏令牌:在DeiT模型中,除了分类令牌,还引入了一个“蒸馏令牌”,它通过与一个CNN教师模型的输出进行交互,从教师那里学习知识,从而让ViT能在相对较小的数据集(如ImageNet-1K)上有效训练,而无需依赖超大规模数据集。

这些变体各有优劣,选择哪一种取决于具体的任务、对精度的要求以及对计算资源的限制。Swin Transformer因其在通用视觉任务上的优异表现和良好的效率,成为了目前应用最广泛的视觉Transformer架构之一。

5. 实战:在自定义数据集上微调Vision Transformer

理解了原理,我们来看看如何动手实践。这里以使用Hugging Facetransformers库,在自定义图像分类数据集上微调一个预训练的ViT模型为例。

5.1 环境准备与数据预处理

首先,确保安装必要的库:

pip install transformers datasets torch torchvision pillow

假设你的自定义数据集是一个文件夹结构,如下所示:

my_dataset/ ├── train/ │ ├── class_0/ │ │ ├── img1.jpg │ │ └── ... │ ├── class_1/ │ └── ... └── val/ ├── class_0/ ├── class_1/ └── ...

我们需要使用torchvisiontransformers的处理器来准备数据。ViT模型有特定的图像预处理要求(如分辨率调整、归一化等)。

from transformers import ViTImageProcessor from torchvision import transforms from datasets import load_dataset import torch # 加载预训练模型对应的处理器 # 以 `google/vit-base-patch16-224-in21k` 为例 model_name = "google/vit-base-patch16-224-in21k" processor = ViTImageProcessor.from_pretrained(model_name) # 定义转换函数 def transform(examples): # 假设数据集的图像列名为 'image' images = examples['image'] # 使用处理器进行标准化预处理 inputs = processor(images=images, return_tensors="pt") # 添加标签 inputs['labels'] = examples['label'] return inputs # 使用 `datasets` 库加载自定义数据 # 这里需要你先将文件夹数据集转换为 `datasets.Dataset` 格式 # 可以使用 `ImageFolder` 方式或自定义脚本 dataset = load_dataset("imagefolder", data_dir="./my_dataset") # 应用预处理 processed_dataset = dataset.with_transform(transform)

5.2 模型加载与微调配置

接下来,加载预训练模型,并修改其分类头以适应你的类别数。

from transformers import ViTForImageClassification # 获取数据集的类别数 num_labels = len(processed_dataset['train'].features['label'].names) # 加载模型,并指定新的类别数 model = ViTForImageClassification.from_pretrained( model_name, num_labels=num_labels, # 关键:覆盖预训练模型的分类头 ignore_mismatched_sizes=True # 忽略分类头大小不匹配的警告 ) # 配置训练参数 from transformers import TrainingArguments, Trainer training_args = TrainingArguments( output_dir="./vit-finetuned", per_device_train_batch_size=16, per_device_eval_batch_size=16, evaluation_strategy="epoch", save_strategy="epoch", num_train_epochs=10, fp16=True, # 如果GPU支持混合精度训练,可以加速 logging_dir='./logs', logging_steps=10, learning_rate=2e-5, # 微调学习率通常较小 weight_decay=0.01, load_best_model_at_end=True, metric_for_best_model="accuracy", )

5.3 训练循环与评估

定义评估指标,并创建Trainer开始训练。

import numpy as np from datasets import load_metric metric = load_metric("accuracy") def compute_metrics(eval_pred): logits, labels = eval_pred predictions = np.argmax(logits, axis=-1) return metric.compute(predictions=predictions, references=labels) trainer = Trainer( model=model, args=training_args, train_dataset=processed_dataset["train"], eval_dataset=processed_dataset["validation"], compute_metrics=compute_metrics, ) trainer.train()

训练完成后,你可以保存模型并进行推理。

# 保存模型 trainer.save_model("./my_finetuned_vit") processor.save_pretrained("./my_finetuned_vit") # 推理示例 from PIL import Image image = Image.open("path_to_test_image.jpg").convert("RGB") inputs = processor(images=image, return_tensors="pt") with torch.no_grad(): logits = model(**inputs).logits predicted_label = logits.argmax(-1).item() label_name = processed_dataset['train'].features['label'].names[predicted_label] print(f"Predicted class: {label_name}")

重要提示:视觉Transformer,尤其是像ViT这样的模型,对数据增强非常敏感。在微调时,强烈建议使用比CNN更激进的数据增强策略,如RandAugment、MixUp、CutMix等。这能有效防止模型在小数据集上过拟合,并提升泛化能力。transformersViTImageProcessor通常只包含基础的归一化和缩放,你可能需要在数据加载管道中额外添加这些增强。

6. 视觉Transformer的适用场景与当前局限

视觉Transformer并非万能,理解其优势和局限,有助于你在项目中做出正确的技术选型。

优势场景:

  1. 数据充足的任务:当拥有海量数据(如JFT-300M)时,ViT能展现出超越CNN的潜力,因为它能从数据中学习到更强大的、不受局部性先验限制的特征表示。
  2. 需要长距离依赖建模的任务:例如,图像生成(如Diffusion模型大量使用Transformer)、全景分割、视频理解(时空注意力)等,其中全局上下文至关重要。
  3. 多模态任务:Transformer是统一多种模态(图像、文本、音频)的天然架构。像CLIP、DALL-E等模型,其核心就是Transformer,它能无缝处理来自不同模态的嵌入序列。

当前局限与挑战:

  1. 计算与内存开销:尽管有Swin等优化,大型视觉Transformer的推理速度和在边缘设备上的部署难度,仍然高于经过高度优化的轻量级CNN(如MobileNet、EfficientNet-Lite)。
  2. 小样本学习:在数据量有限的情况下,缺乏CNN固有的归纳偏置(局部性、平移等变性)可能是一个劣势,导致模型更容易过拟合,需要更强的数据增强和正则化。
  3. 对空间细节的敏感性:将图像分割成块,可能会破坏最细粒度的空间信息。对于需要像素级精度的任务(如超分辨率、图像修复),纯Transformer架构有时需要与CNN或其他结构结合。
  4. 解释性:虽然注意力图可以可视化模型关注了图像的哪些区域,但其解释性仍然比CNN的滤波器可视化更抽象和难以理解。

在实际项目中,我的经验是“混合使用,各取所长”。对于需要快速部署、对计算资源敏感的应用,轻量级CNN仍是首选。对于追求极致精度、且有充足算力和数据的研发项目,或者涉及多模态、长序列理解的任务,视觉Transformer是必须深入探索的方向。而像ConvNeXt这样的模型,则展示了将Transformer的设计理念(如更大的感受野、更少的激活函数)重新注入CNN后,也能取得惊人效果,这提示我们,未来的架构很可能是融合了双方优点的混合体。

← 返回列表