Appearance
章节5:Transformer架构
学习目标
- 理解自注意力机制的计算流程(Q/K/V 缩放点积)
- 掌握多头注意力的并行计算与拼接融合
- 理解位置编码的作用与实现方式
- 掌握 Transformer Encoder-Decoder 完整架构
- 了解 Transformer 核心变体(ViT / BERT / GPT)
5.1 自注意力机制(Self-Attention)
5.1.1 从 RNN 到 Attention
RNN 存在两个根本问题:
- 顺序计算:无法并行,训练速度慢
- 长距离遗忘:距离越远,信息丢失越严重
自注意力(Self-Attention) 让每个位置直接与所有位置交互,一次计算即可捕捉全局依赖关系。
5.1.2 Q/K/V 计算
每个输入向量 $x_i$ 通过三个不同的线性变换得到:
- Query(查询):$Q_i = x_i W^Q$
- Key(键):$K_i = x_i W^K$
- Value(值):$V_i = x_i W^V$
5.1.3 缩放点积注意力(Scaled Dot-Product Attention)
计算步骤:
- 计算每个 Query 与所有 Key 的点积相似度
- 缩放(除以 $\sqrt{d_k}$)防止梯度消失
- Softmax 归一化为注意力权重
- 加权求和 Value
公式:
$$ \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) V $$
python
import torch
import torch.nn as nn
import torch.nn.functional as F
def scaled_dot_product_attention(Q, K, V, mask=None):
"""
Q: [batch, seq_len, d_k]
K: [batch, seq_len, d_k]
V: [batch, seq_len, d_v]
mask: [batch, seq_len, seq_len] 可选,padding mask 或 causal mask
"""
d_k = Q.size(-1)
scores = torch.matmul(Q, K.transpose(-2, -1)) / torch.sqrt(torch.tensor(d_k, dtype=torch.float32))
# scores: [batch, seq_len, seq_len]
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
attention_weights = F.softmax(scores, dim=-1)
output = torch.matmul(attention_weights, V)
return output, attention_weights
# 示例
batch, seq_len, d_k = 2, 4, 8
Q = torch.randn(batch, seq_len, d_k)
K = torch.randn(batch, seq_len, d_k)
V = torch.randn(batch, seq_len, d_k)
output, weights = scaled_dot_product_attention(Q, K, V)
print(f"输出形状: {output.shape}") # [2, 4, 8]
print(f"注意力权重形状: {weights.shape}") # [2, 4, 4]5.2 多头注意力(Multi-Head Attention)
5.2.1 为什么要多头?
单一注意力只能关注一种表示空间。多头注意力让模型同时从不同子空间学习不同类型的特征关系。
5.2.2 计算流程
- 将 Q/K/V 线性投影到 $h$ 个不同的低维子空间
- 在每个子空间独立计算缩放点积注意力
- 拼接所有头的输出
- 通过线性层融合
公式:
$$ \text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, \dots, \text{head}_h) W^O $$
$$ \text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V) $$
python
class MultiHeadAttention(nn.Module):
def __init__(self, d_model=512, num_heads=8):
super().__init__()
assert d_model % num_heads == 0, "d_model 必须能被 num_heads 整除"
self.d_model = d_model
self.num_heads = num_heads
self.d_k = d_model // num_heads
self.W_q = nn.Linear(d_model, d_model)
self.W_k = nn.Linear(d_model, d_model)
self.W_v = nn.Linear(d_model, d_model)
self.W_o = nn.Linear(d_model, d_model)
def forward(self, Q, K, V, mask=None):
batch_size = Q.size(0)
# 1. 线性投影并拆分为多头
Q = self.W_q(Q).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
K = self.W_k(K).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
V = self.W_v(V).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
# 形状: [batch, num_heads, seq_len, d_k]
# 2. 缩放点积注意力
scores = torch.matmul(Q, K.transpose(-2, -1)) / torch.sqrt(torch.tensor(self.d_k, dtype=torch.float32))
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
attn = F.softmax(scores, dim=-1)
output = torch.matmul(attn, V)
# 形状: [batch, num_heads, seq_len, d_k]
# 3. 拼接所有头
output = output.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
# 形状: [batch, seq_len, d_model]
# 4. 线性输出
return self.W_o(output)
# 测试
mha = MultiHeadAttention(d_model=512, num_heads=8)
x = torch.randn(2, 10, 512)
output = mha(x, x, x)
print(f"多头注意力输出: {output.shape}") # [2, 10, 512]5.3 位置编码(Positional Encoding)
自注意力没有时序概念,打乱输入顺序会得到相同结果。因此需要位置编码注入位置信息。
5.3.1 正弦位置编码(原版 Transformer)
使用不同频率的正弦/余弦函数:
$$ \begin{aligned} PE_{(pos, 2i)} &= \sin\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right) \ PE_{(pos, 2i+1)} &= \cos\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right) \end{aligned} $$
其中 $pos$ 是位置索引,$i$ 是维度索引。
优点: 无需学习,可外推到任意长度。
python
class SinusoidalPositionalEncoding(nn.Module):
def __init__(self, d_model=512, max_len=5000):
super().__init__()
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float32).unsqueeze(1)
div_term = torch.exp(
torch.arange(0, d_model, 2, dtype=torch.float32) *
-(torch.log(torch.tensor(10000.0)) / d_model)
)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0) # [1, max_len, d_model]
self.register_buffer('pe', pe)
def forward(self, x):
# x: [batch, seq_len, d_model]
return x + self.pe[:, :x.size(1), :]5.3.2 可学习位置编码
让位置编码作为可训练参数学习,常用于 BERT 等预训练模型。
python
class LearnablePositionalEncoding(nn.Module):
def __init__(self, d_model=512, max_len=512):
super().__init__()
self.pe = nn.Embedding(max_len, d_model)
def forward(self, x):
seq_len = x.size(1)
positions = torch.arange(seq_len, device=x.device)
return x + self.pe(positions)5.4 Transformer Encoder-Decoder 完整架构
5.4.1 整体结构
Transformer(Vaswani et al., 2017)由 Encoder 和 Decoder 堆叠而成:
输入序列 → [N× Encoder层] → 编码表示 → [N× Decoder层] → 输出序列Encoder 层(每层): 多头自注意力 → 前馈网络(FFN)→ 均以残差连接 + 层归一化
Decoder 层(每层): Masked 自注意力 → 交叉注意力(Encoder输出)→ FFN → 残差 + 层归一化
python
class FeedForward(nn.Module):
"""前馈网络(FFN)"""
def __init__(self, d_model=512, d_ff=2048, dropout=0.1):
super().__init__()
self.net = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(d_ff, d_model),
nn.Dropout(dropout)
)
def forward(self, x):
return self.net(x)
class EncoderLayer(nn.Module):
def __init__(self, d_model=512, num_heads=8, d_ff=2048, dropout=0.1):
super().__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.ffn = FeedForward(d_model, d_ff, dropout)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x, mask=None):
# 子层1:多头自注意力 + 残差
attn_output = self.self_attn(x, x, x, mask)
x = self.norm1(x + self.dropout(attn_output))
# 子层2:前馈网络 + 残差
ffn_output = self.ffn(x)
x = self.norm2(x + self.dropout(ffn_output))
return x
class DecoderLayer(nn.Module):
def __init__(self, d_model=512, num_heads=8, d_ff=2048, dropout=0.1):
super().__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.cross_attn = MultiHeadAttention(d_model, num_heads)
self.ffn = FeedForward(d_model, d_ff, dropout)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.norm3 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x, encoder_output, src_mask=None, tgt_mask=None):
# 子层1:Masked 自注意力
attn_output = self.self_attn(x, x, x, tgt_mask)
x = self.norm1(x + self.dropout(attn_output))
# 子层2:交叉注意力(Q来自Decoder,K/V来自Encoder)
attn_output = self.cross_attn(x, encoder_output, encoder_output, src_mask)
x = self.norm2(x + self.dropout(attn_output))
# 子层3:前馈网络
ffn_output = self.ffn(x)
x = self.norm3(x + self.dropout(ffn_output))
return x
class Transformer(nn.Module):
def __init__(self, src_vocab_size, tgt_vocab_size,
d_model=512, num_heads=8, num_layers=6,
d_ff=2048, max_len=512, dropout=0.1):
super().__init__()
self.encoder_embedding = nn.Embedding(src_vocab_size, d_model)
self.decoder_embedding = nn.Embedding(tgt_vocab_size, d_model)
self.positional_encoding = SinusoidalPositionalEncoding(d_model, max_len)
self.encoder_layers = nn.ModuleList([
EncoderLayer(d_model, num_heads, d_ff, dropout)
for _ in range(num_layers)
])
self.decoder_layers = nn.ModuleList([
DecoderLayer(d_model, num_heads, d_ff, dropout)
for _ in range(num_layers)
])
self.fc_out = nn.Linear(d_model, tgt_vocab_size)
self.dropout = nn.Dropout(dropout)
self.d_model = d_model
def forward(self, src, tgt, src_mask=None, tgt_mask=None):
# Encoder
src_emb = self.dropout(self.positional_encoding(
self.encoder_embedding(src) * torch.sqrt(torch.tensor(self.d_model, dtype=torch.float32))
))
encoder_output = src_emb
for layer in self.encoder_layers:
encoder_output = layer(encoder_output, src_mask)
# Decoder
tgt_emb = self.dropout(self.positional_encoding(
self.decoder_embedding(tgt) * torch.sqrt(torch.tensor(self.d_model, dtype=torch.float32))
))
decoder_output = tgt_emb
for layer in self.decoder_layers:
decoder_output = layer(decoder_output, encoder_output, src_mask, tgt_mask)
return self.fc_out(decoder_output)
# 测试 Transformer
src_vocab_size, tgt_vocab_size = 1000, 1000
d_model = 512
num_heads = 8
num_layers = 2 # 演示用,原版为6
model = Transformer(src_vocab_size, tgt_vocab_size,
d_model=d_model, num_heads=num_heads, num_layers=num_layers)
src = torch.randint(0, src_vocab_size, (2, 20)) # 源序列
tgt = torch.randint(0, tgt_vocab_size, (2, 15)) # 目标序列
output = model(src, tgt)
print(f"Transformer 输出: {output.shape}") # [2, 15, 1000]5.5 Transformer 重要变体
5.5.1 ViT(Vision Transformer, 2020)
将 Transformer 应用于图像:将图像分割为固定大小的 Patch,展平后作为 token 序列输入 Transformer。
python
class ViT(nn.Module):
"""简化版 Vision Transformer"""
def __init__(self, image_size=224, patch_size=16, in_channels=3,
num_classes=1000, d_model=768, num_heads=12, num_layers=12, dropout=0.1):
super().__init__()
num_patches = (image_size // patch_size) ** 2
patch_dim = in_channels * patch_size * patch_size
# Patch Embedding
self.patch_embed = nn.Conv2d(in_channels, d_model,
kernel_size=patch_size, stride=patch_size)
# [CLS] Token + 位置编码
self.cls_token = nn.Parameter(torch.randn(1, 1, d_model))
self.pos_embed = nn.Parameter(torch.randn(1, num_patches + 1, d_model))
self.dropout = nn.Dropout(dropout)
# Transformer Encoder
encoder_layer = nn.TransformerEncoderLayer(d_model, num_heads,
dim_feedforward=d_model*4,
dropout=dropout, batch_first=True)
self.transformer = nn.TransformerEncoder(encoder_layer, num_layers)
self.mlp_head = nn.Sequential(
nn.LayerNorm(d_model),
nn.Linear(d_model, num_classes)
)
def forward(self, x):
# Patch Embedding
x = self.patch_embed(x).flatten(2).transpose(1, 2) # [B, num_patches, d_model]
# 拼接 [CLS] token
cls_tokens = self.cls_token.expand(x.size(0), -1, -1)
x = torch.cat([cls_tokens, x], dim=1)
x = self.dropout(x + self.pos_embed)
# Transformer
x = self.transformer(x)
# 取 [CLS] token 分类
return self.mlp_head(x[:, 0])5.5.2 BERT(Bidirectional Encoder Representations from Transformers, 2018)
- 架构:仅使用 Transformer Encoder(双向)
- 预训练方式:Masked Language Model(MLM)+ Next Sentence Prediction(NSP)
- 应用:文本分类、命名实体识别、问答等
5.5.3 GPT(Generative Pre-trained Transformer, 2018~)
- 架构:仅使用 Transformer Decoder(自回归、单向)
- 预训练方式:自回归语言建模(预测下一个 token)
- 应用:文本生成、对话、代码生成(GPT-3/4、ChatGPT)
5.5.4 变体对比
| 模型 | 架构 | 方向 | 预训练任务 | 典型应用 |
|---|---|---|---|---|
| Transformer | Encoder-Decoder | 双向+自回归 | 翻译 | 机器翻译 |
| BERT | Encoder only | 双向 | MLM + NSP | 自然语言理解 |
| GPT | Decoder only | 单向(自回归) | 下一词预测 | 文本生成 |
| ViT | Encoder only | 双向 | 图像分类 | 视觉理解 |
小结
| 概念 | 关键要点 |
|---|---|
| 自注意力 | Q/K/V 缩放点积,全局依赖,可并行 |
| 多头注意力 | 多子空间并行学习,拼接融合 |
| 位置编码 | 正弦/可学习,注入位置信息 |
| Encoder-Decoder | Encoder 编码输入,Decoder 自回归生成 |
| FFN | 两层线性 + ReLU,增加表达能力 |
| 残差 + LayerNorm | 稳定深层训练 |
| ViT | 图像分 Patch 输入 Transformer |
| BERT | 双向 Encoder,MLM 预训练 |
| GPT | 单向 Decoder,自回归生成 |
练习
- 手写 Attention:不用 PyTorch 内置函数,手动实现 Q/K/V 投影 → 缩放点积 → Softmax → 加权求和。
- 注意力可视化:训练一个小型 Transformer 后,可视化注意力权重矩阵,分析模型关注了哪些位置。
- 实现 Transformer 翻译:使用小规模英德或英法平行语料,训练一个翻译 Transformer。
- 对比 ViT vs CNN:在 CIFAR-10 上分别训练 ViT 和 CNN,对比精度和训练速度。
- BERT 微调:使用 HuggingFace 加载
bert-base-chinese,在中文情感分类任务上微调。