从NLP到CV:手把手教你用Transformers实现图像分类(附ViT实战代码)
如果你是一位熟悉BERT、GPT等模型的NLP开发者,第一次听说Vision Transformer(ViT)时,可能会和我当初一样,心里冒出几个问号:那个处理单词序列的Transformer,怎么就能看懂图片了?把图片切成块,当成“单词”喂进去,真的能行吗?这听起来有点像用螺丝刀去拧螺母,工具虽然强大,但真的顺手吗?
事实上,这种“跨界”不仅可行,而且在足够数据量的加持下,其表现甚至开始超越统治计算机视觉领域多年的卷积神经网络(CNN)。对于希望将NLP领域经验快速复用到CV任务的开发者而言,理解并上手ViT,正成为一项极具价值的前沿技能。本文不会止步于理论探讨,我们将直接切入实战,从零开始,一步步构建一个能够识别图像的ViT模型。我会分享代码实现中的关键细节、调试经验,以及如何避免那些我踩过的“坑”。无论你是想拓展技术栈,还是为下一个项目寻找更优的视觉解决方案,这里都有你需要的干货。
1. 跨界思维:为什么Transformer能“看见”图像?
要理解ViT,首先要打破一个思维定式:我们并非要让Transformer去“理解”像素。相反,我们做的是形式转换——将图像这种结构化的二维数据,转化为Transformer天生擅长的处理对象:一维序列。
在NLP中,Transformer的输入是一个个单词(或子词)的嵌入向量序列。每个单词是一个语义单元。ViT的灵感正在于此:它将一张图片分割成多个固定大小的图像块(例如16x16像素),每个图像块经过线性投影后,就变成了一个“视觉单词”的嵌入向量。这些“视觉单词”按顺序排列,就构成了Transformer编码器的输入序列。
这里有一个至关重要的设计:位置嵌入。 在NLP中,“我 爱 你”和“你 爱 我”含义截然不同,位置信息是关键。对于图像,空间位置关系同样蕴含重要信息(天空通常在顶部,草地通常在底部)。ViT为每个图像块附加一个可学习的位置嵌入向量,这样模型就能知道“天空”这个块在上面,“草地”这个块在下面。
与CNN的“归纳偏置”(即先验假设,如局部性、平移不变性)不同,ViT的“归纳偏置”极少。它没有内置的卷积核来强制关注局部邻域,自注意力机制允许模型在网络的第一层就建立任意两个图像块之间的全局关联。这意味着,模型需要从海量数据中自己学习到“相邻的块通常更相关”、“物体的部分应该聚集在一起”这些视觉规律。这也解释了为什么ViT在大规模数据集(如JFT-300M)上预训练后,性能会爆发式增长——数据量足够大时,模型自己学到的规律,可能比我们人为设计的卷积归纳偏置更优、更灵活。
注意:对于中小型数据集,CNN因其强大的归纳偏置,通常比同等规模的ViT更容易训练、表现更好。ViT的优势需要大数据来激发。
下表直观对比了CNN与ViT在处理图像时的核心思想差异:
| 特性维度 | 卷积神经网络 (CNN) | 视觉Transformer (ViT) |
|---|---|---|
| 基本处理单元 | 局部感受野内的像素 | 整张图像切分后的图像块 |
| 核心操作 | 卷积、池化 | 多头自注意力、前馈网络 |
| 归纳偏置 | 强(局部性、平移不变性) | 极弱(仅存在于图像块切割和位置嵌入) |
| 信息融合范围 | 由浅到深,逐步扩大(感受野) | 任意层均可实现全局交互 |
| 数据依赖性 | 中小规模数据即可有效学习 | 严重依赖大规模预训练数据 |
| 可解释性 | 特征图可视化,相对直观 | 注意力图可显示模型关注区域 |
理解了这套“化图为文”的跨界逻辑,我们就可以开始动手,将理论转化为代码了。
2. 环境搭建与数据准备
工欲善其事,必先利其器。我们选择PyTorch作为深度学习框架,并利用Hugging Face的 transformers 库,它提供了高质量、易用的ViT模型实现。当然,为了更透彻地理解,我们也会从更底层的地方开始构建一些关键组件。
首先,创建一个干净的Python环境并安装依赖:
# 创建并激活虚拟环境(可选但推荐)
conda create -n vit-tutorial python=3.8
conda activate vit-tutorial
# 安装核心依赖
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 根据你的CUDA版本调整
pip install transformers datasets timm pillow matplotlib
pip install accelerate -U # 用于混合精度训练等加速
接下来,我们准备数据。这里以经典的CIFAR-10数据集为例,它包含10个类别的6万张32x32小图片。虽然ViT论文中强调大数据,但我们可以通过一些技巧(如更强的数据增强、更小的图像块)在小数据集上进行有益的探索。
import torch
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
# 定义训练和测试的数据增强与转换
# 对于小数据集,强数据增强至关重要
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224), # ViT通常输入224x224
transforms.RandomHorizontalFlip(),
transforms.RandomRotation(15),
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), # ImageNet统计值
])
test_transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
# 下载并加载CIFAR-10数据集
# 注意:CIFAR-10原图32x32,上采样到224x224会模糊,这里仅为演示流程。
# 实际项目中,应使用ImageNet或更大尺寸数据集。
train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=train_transform)
test_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=test_transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=4, pin_memory=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False, num_workers=4, pin_memory=True)
print(f'训练集大小: {len(train_dataset)}')
print(f'测试集大小: {len(test_dataset)}')
print(f'类别: {train_dataset.classes}')
数据管道搭建好后,一个常见的疑惑是:图像块到底怎么切? 在代码层面,这通常由一个名为 PatchEmbedding 的模块完成。它本质上是一个卷积操作,但有着特殊的参数设置。
3. 核心模块拆解:从图像块嵌入到Transformer编码器
现在,让我们深入ViT的核心,亲手实现几个关键模块。这能帮你彻底理解数据在模型中的流动与变换。
3.1 图像块嵌入层
这是将图像转化为序列的第一步。我们使用一个卷积层来实现,其核大小和步长等于图像块大小(如16),输出通道数等于Transformer的隐藏维度(如768)。这个卷积层相当于同时做了两件事:将图像切分成块,并对每个块进行线性投影。
import torch.nn as nn
class PatchEmbedding(nn.Module):
"""将2D图像转换为1D序列嵌入"""
def __init__(self, img_size=224, patch_size=16, in_channels=3, embed_dim=768):
super().__init__()
self.img_size = img_size
self.patch_size = patch_size
self.num_patches = (img_size // patch_size) ** 2
# 使用卷积层实现投影:核大小=步长=patch_size
self.projection = nn.Conv2d(in_channels, embed_dim,
kernel_size=patch_size, stride=patch_size)
def forward(self, x):
# x形状: (B, C, H, W)
B, C, H, W = x.shape
# 投影后形状: (B, embed_dim, H/patch_size, W/patch_size)
x = self.projection(x)
# 展平空间维度: (B, embed_dim, num_patches)
x = x.flatten(2)
# 调整维度为Transformer输入序列格式: (B, num_patches, embed_dim)
x = x.transpose(1, 2)
return x
3.2 位置嵌入与类别标记
得到图像块嵌入后,我们需要添加位置信息和一个特殊的 [CLS] 标记(借鉴自BERT)。[CLS] 标记的最终输出将用于图像分类。
class ViTEmbeddings(nn.Module):
"""组合图像块嵌入、位置嵌入和类别标记"""
def __init__(self, img_size=224, patch_size=16, in_channels=3, embed_dim=768, dropout_rate=0.1):
super().__init__()
self.patch_embed = PatchEmbedding(img_size, patch_size, in_channels, embed_dim)
self.num_patches = self.patch_embed.num_patches
# 可学习的类别标记
self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
# 可学习的位置嵌入
self.position_embeddings = nn.Parameter(torch.zeros(1, self.num_patches + 1, embed_dim))
self.dropout = nn.Dropout(dropout_rate)
# 初始化参数
nn.init.trunc_normal_(self.cls_token, std=0.02)
nn.init.trunc_normal_(self.position_embeddings, std=0.02)
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), dim=1) # (B, num_patches+1, embed_dim)
# 添加位置嵌入
x = x + self.position_embeddings
x = self.dropout(x)
return x
3.3 Transformer编码器层
这是模型的核心,由多头自注意力(MSA)和前馈网络(MLP)交替组成,并应用了层归一化(LayerNorm)和残差连接。
class MultiHeadSelfAttention(nn.Module):
"""简化版多头自注意力"""
def __init__(self, embed_dim=768, num_heads=12, dropout_rate=0.0):
super().__init__()
assert embed_dim % num_heads == 0, "embed_dim必须能被num_heads整除"
self.num_heads = num_heads
self.head_dim = embed_dim // num_heads
self.scale = self.head_dim ** -0.5
self.qkv = nn.Linear(embed_dim, embed_dim * 3) # 同时计算Q, K, V
self.attn_dropout = nn.Dropout(dropout_rate)
self.proj = nn.Linear(embed_dim, embed_dim)
self.proj_dropout = nn.Dropout(dropout_rate)
def forward(self, x):
B, N, C = x.shape # N: 序列长度 (num_patches + 1)
# 计算Q, K, V并重塑为多头
qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
q, k, v = qkv[0], qkv[1], qkv[2] # 每个形状: (B, num_heads, N, head_dim)
# 计算注意力分数
attn = (q @ k.transpose(-2, -1)) * self.scale
attn = attn.softmax(dim=-1)
attn = self.attn_dropout(attn)
# 应用注意力权重到V
x = (attn @ v).transpose(1, 2).reshape(B, N, C)
x = self.proj(x)
x = self.proj_dropout(x)
return x
class TransformerEncoderLayer(nn.Module):
"""单个Transformer编码器层"""
def __init__(self, embed_dim=768, num_heads=12, mlp_ratio=4.0, dropout_rate=0.1):
super().__init__()
self.norm1 = nn.LayerNorm(embed_dim, eps=1e-6)
self.attn = MultiHeadSelfAttention(embed_dim, num_heads, dropout_rate)
self.norm2 = nn.LayerNorm(embed_dim, eps=1e-6)
mlp_hidden_dim = int(embed_dim * mlp_ratio)
self.mlp = nn.Sequential(
nn.Linear(embed_dim, mlp_hidden_dim),
nn.GELU(), # ViT使用GELU激活函数
nn.Dropout(dropout_rate),
nn.Linear(mlp_hidden_dim, embed_dim),
nn.Dropout(dropout_rate)
)
def forward(self, x):
# 残差连接和层归一化 (Pre-Norm结构,ViT论文中使用)
x = x + self.attn(self.norm1(x))
x = x + self.mlp(self.norm2(x))
return x
4. 组装与训练:构建完整的ViT模型
将上述模块组合起来,并添加分类头,就得到了完整的Vision Transformer模型。
class VisionTransformer(nn.Module):
"""完整的Vision Transformer模型"""
def __init__(self, img_size=224, patch_size=16, in_channels=3, num_classes=10,
embed_dim=768, depth=12, num_heads=12, mlp_ratio=4.0,
dropout_rate=0.1, representation_size=None):
super().__init__()
self.embeddings = ViTEmbeddings(img_size, patch_size, in_channels, embed_dim, dropout_rate)
self.encoder = nn.Sequential(*[
TransformerEncoderLayer(embed_dim, num_heads, mlp_ratio, dropout_rate)
for _ in range(depth)
])
self.norm = nn.LayerNorm(embed_dim, eps=1e-6)
# 分类头
self.head = nn.Linear(embed_dim, num_classes) if representation_size is None else nn.Sequential(
nn.Linear(embed_dim, representation_size),
nn.Tanh(),
nn.Linear(representation_size, num_classes)
)
# 初始化权重
self.apply(self._init_weights)
def _init_weights(self, m):
if isinstance(m, nn.Linear):
nn.init.trunc_normal_(m.weight, std=0.02)
if m.bias is not None:
nn.init.zeros_(m.bias)
elif isinstance(m, nn.LayerNorm):
nn.init.zeros_(m.bias)
nn.init.ones_(m.weight)
def forward(self, x):
# 嵌入
x = self.embeddings(x)
# 通过Transformer编码器
x = self.encoder(x)
# 对序列进行层归一化
x = self.norm(x)
# 取类别标记[CLS]的输出用于分类
cls_output = x[:, 0]
# 分类头
logits = self.head(cls_output)
return logits
模型定义好后,训练环节同样充满技巧。ViT的训练对超参数比较敏感,尤其是学习率和优化器。
import torch.optim as optim
from torch.cuda.amp import GradScaler, autocast # 混合精度训练
def train_one_epoch(model, dataloader, criterion, optimizer, device, scaler=None):
model.train()
running_loss = 0.0
correct = 0
total = 0
for batch_idx, (images, labels) in enumerate(dataloader):
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
# 混合精度训练前向传播
with autocast():
outputs = model(images)
loss = criterion(outputs, labels)
# 混合精度训练反向传播
if scaler is not None:
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
else:
loss.backward()
optimizer.step()
running_loss += loss.item()
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
epoch_loss = running_loss / len(dataloader)
epoch_acc = 100. * correct / total
return epoch_loss, epoch_acc
# 初始化模型、损失函数和优化器
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = VisionTransformer(img_size=224, patch_size=16, num_classes=10, embed_dim=384, depth=6, num_heads=6).to(device) # 小型配置,便于实验
criterion = nn.CrossEntropyLoss()
# 使用AdamW优化器,并设置权重衰减,这对Transformer训练很重要
optimizer = optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.05)
# 学习率预热调度器
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50, eta_min=1e-6)
# 混合精度训练缩放器
scaler = GradScaler()
print(f'模型参数量: {sum(p.numel() for p in model.parameters())/1e6:.2f}M')
print(f'使用设备: {device}')
训练循环和验证代码与常规PyTorch训练无异,但有几个关键点需要特别留意:
- 学习率预热:ViT训练初期不稳定,通常需要几十到几百个step的线性学习率预热。
- 权重衰减:使用AdamW优化器并设置适当的权重衰减(如0.05)是防止过拟合的有效手段。
- 梯度裁剪:对于较深的ViT模型,梯度裁剪有助于稳定训练。
- 数据增强:如前所述,对于小数据集,RandAugment、MixUp、CutMix等强增强策略几乎是必需品。
5. 实战技巧与性能优化
纸上得来终觉浅,绝知此事要躬行。在实际项目中应用ViT,你会遇到一些在理论论文中不会详述的挑战。下面分享几个我实践中总结的技巧。
5.1 处理高分辨率图像与微调
预训练的ViT模型(如在ImageNet-21k上训练)通常使用224x224分辨率。当你的下游任务图像分辨率不同(如384x384或512x512)时,直接调整输入尺寸会导致图像块数量变化,从而与预训练位置嵌入的序列长度不匹配。
解决方案是:对预训练的位置嵌入进行二维插值。 Hugging Face的 transformers 库已经内置了这个功能。基本原理是,将学习到的1D位置嵌入,根据其对应的原始2D坐标,插值到新的2D网格坐标上。
from transformers import ViTForImageClassification
import torch.nn.functional as F
# 加载预训练模型 (例如 google/vit-base-patch16-224)
model = ViTForImageClassification.from_pretrained('google/vit-base-patch16-224')
# 假设我们需要将输入分辨率从224调整为384
new_image_size = 384
patch_size = 16
original_num_patches = (224 // patch_size) ** 2
new_num_patches = (new_image_size // patch_size) ** 2
# 获取原始位置嵌入 (1, num_patches+1, hidden_dim)
original_pos_embed = model.vit.embeddings.position_embeddings
# 分离出类别标记的位置嵌入和图像块的位置嵌入
cls_pos_embed = original_pos_embed[:, 0:1, :]
img_pos_embed = original_pos_embed[:, 1:, :]
# 将1D序列位置嵌入重塑为2D网格 (假设原始为14x14网格)
original_grid_size = int(original_num_patches ** 0.5)
img_pos_embed_2d = img_pos_embed.reshape(1, original_grid_size, original_grid_size, -1).permute(0, 3, 1, 2)
# 计算新的网格大小并插值
new_grid_size = int(new_num_patches ** 0.5)
# 使用双线性插值,因为位置嵌入在2D空间是连续的
img_pos_embed_2d_resized = F.interpolate(img_pos_embed_2d, size=(new_grid_size, new_grid_size), mode='bilinear', align_corners=False)
img_pos_embed_resized = img_pos_embed_2d_resized.permute(0, 2, 3, 1).reshape(1, new_num_patches, -1)
# 重新组合类别标记和插值后的图像块位置嵌入
new_pos_embed = torch.cat([cls_pos_embed, img_pos_embed_resized], dim=1)
# 替换模型中的位置嵌入
model.vit.embeddings.position_embeddings = nn.Parameter(new_pos_embed)
# 同时需要更新模型配置中的image_size信息(如果模型配置中有)
model.config.image_size = new_image_size
print(f'位置嵌入已从 {original_grid_size}x{original_grid_size} 插值到 {new_grid_size}x{new_grid_size}')
5.2 注意力可视化:模型在看哪里?
Transformer的可解释性是其一大优点。我们可以通过提取注意力权重,来观察模型在做出分类决策时,更关注图像的哪些区域。
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
def visualize_attention(model, image_path, device, patch_size=16):
"""可视化最后一层自注意力头对[CLS]标记的注意力图"""
model.eval()
# 预处理图像
image = Image.open(image_path).convert('RGB')
transform = test_transform # 使用之前的测试转换
input_tensor = transform(image).unsqueeze(0).to(device)
# 使用钩子获取注意力权重
attention_maps = []
def hook_fn(module, input, output):
# output 通常是 (attention_output, attention_weights)
# 对于某些实现,attention_weights在output的元组中
if isinstance(output, tuple):
attention_weights = output[1] # 假设第二个元素是注意力权重
else:
# 可能需要根据具体模型结构调整
attention_weights = module.attention_map # 如果模型存储了它
attention_maps.append(attention_weights.detach().cpu())
# 注册钩子到最后一个编码器层的自注意力模块
# 注意:这里需要根据你的模型结构找到正确的模块路径
last_attn_layer = model.vit.encoder.layer[-1].attention.attention
handle = last_attn_layer.register_forward_hook(hook_fn)
with torch.no_grad():
outputs = model(input_tensor)
handle.remove() # 移除钩子
if attention_maps:
attn = attention_maps[0] # (batch_size, num_heads, seq_len, seq_len)
# 取第一个样本,所有注意力头对[CLS]标记(索引0)的注意力
# 平均所有头的注意力
attn_to_cls = attn[0, :, 0, 1:].mean(dim=0) # 形状: (num_patches,)
num_patches = attn_to_cls.shape[0]
grid_size = int(num_patches ** 0.5)
attn_map = attn_to_cls.reshape(grid_size, grid_size).numpy()
# 将注意力图上采样到原图尺寸进行叠加显示
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 5))
ax1.imshow(image)
ax1.set_title('Original Image')
ax1.axis('off')
im = ax2.imshow(attn_map, cmap='hot', interpolation='nearest')
ax2.set_title('Attention Map (averaged over heads)')
ax2.axis('off')
plt.colorbar(im, ax=ax2)
plt.tight_layout()
plt.show()
运行这段代码,你可能会发现一个有趣的现象:在分类任务中,模型最后几层的注意力往往高度集中在与目标物体相关的语义区域上,这直观地展示了模型“看”的过程。
5.3 使用预训练模型与迁移学习
从头开始训练一个ViT,尤其是在CIFAR-10这样的小数据集上,很难达到理想效果。最实用的方法是迁移学习。Hugging Face Model Hub提供了大量预训练的ViT模型,我们可以轻松加载并针对自己的任务进行微调。
from transformers import ViTForImageClassification, ViTImageProcessor
from datasets import load_dataset
# 1. 加载预训练模型和处理器
model_name = 'google/vit-base-patch16-224-in21k' # 在ImageNet-21k上预训练
processor = ViTImageProcessor.from_pretrained(model_name)
model = ViTForImageClassification.from_pretrained(model_name, num_labels=10, ignore_mismatched_sizes=True) # 忽略分类头尺寸不匹配
# 2. 准备自定义数据集 (以CIFAR-10为例,但需处理为ImageFolder格式或使用datasets库)
# 假设我们有一个图像文件夹结构: data/train/class1/, data/train/class2/...
from torchvision.datasets import ImageFolder
train_dataset = ImageFolder(root='./data/train', transform=processor)
# 注意:processor会负责将图像转换为模型所需的像素值、尺寸和归一化。
# 3. 微调模型(仅训练分类头或全部参数)
# 通常先冻结主干,只训练分类头
for name, param in model.named_parameters():
if 'classifier' not in name: # 假设分类层名称为'classifier'
param.requires_grad = False
# 然后使用较小的学习率训练所有参数
optimizer = optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=5e-5)
# 训练循环...
在实际微调时,经验告诉我,渐进式解冻(gradual unfreezing)策略有时效果更好:先解冻最后几层Transformer块进行训练,然后逐步解冻更前面的层。这有助于在保留预训练知识的同时,让模型更好地适应新任务。
6. 超越分类:ViT的扩展应用与未来展望
图像分类只是ViT的起点。其强大的序列建模能力,正在计算机视觉的各个子领域催生革命性的变化。了解这些扩展方向,能帮助你更好地定位ViT在项目中的潜力。
- 目标检测:DETR系列模型将目标检测视为一个集合预测问题,使用Transformer编码器-解码器架构,直接输出一组目标框和类别,省去了传统方法中复杂的锚框设计和后处理(如NMS)。
- 图像分割:SETR和Segmenter等模型将语义分割任务视为对每个图像块进行分类。通过Transformer编码器捕获全局上下文,再通过解码器上采样恢复细节,在多个数据集上取得了领先结果。
- 图像生成:Vision Transformer同样被用于生成模型。例如,ViT-VQGAN结合了VQ-VAE和Transformer,能够生成高质量、高分辨率的图像,展示了其在理解和生成视觉内容方面的双重能力。
- 多模态学习:CLIP和ALIGN等模型将ViT与文本Transformer结合,在海量“图像-文本”对上进行对比学习,实现了强大的零样本图像分类和跨模态检索能力。
这些应用的核心,依然是Transformer处理序列数据的灵活性和强大的全局建模能力。当你掌握了ViT的基本原理和实现后,探索这些前沿方向将是一个自然的过程。我最近在一个细粒度图像检索项目中尝试了基于ViT的特征提取器,发现其全局注意力机制对于捕捉物体间细微的上下文差异非常有帮助,这是传统CNN池化层难以做到的。
技术的迭代从未停止,从最初的ViT到Swin Transformer(引入了局部窗口和位移窗口注意力以提升效率),再到最近层出不穷的变体,这个领域充满活力。作为开发者,最重要的不是追逐每一个新模型,而是深刻理解其核心思想——将数据转化为序列,并用注意力机制建立全局依赖。无论未来架构如何变化,这一思想很可能继续闪耀。当你下次面对一个复杂的视觉问题时,不妨想一想:它能被转化为一个序列问题吗?也许,Transformer就是那把钥匙。
1万+



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



