【深度学习进阶】Transformer 架构深度解析:从零手写 Self-Attention 与完整实现(2026 最新版)
【深度学习进阶】Transformer 架构深度解析:从零手写 Self-Attention 与完整实现(2026 最新版)
Transformer 是现代深度学习的基石,从 GPT-4、LLaMA 3 到 Vision Transformer,几乎所有 SOTA 模型都基于它。本文将从数学原理出发,从零手写 Self-Attention、Multi-Head Attention、位置编码,并实现完整的 Transformer,最后对比 2026 年最新的 Flash Attention、RoPE、GQA 等优化技术。适合有一定 PyTorch 基础、想深入理解 Transformer 的读者。
一、为什么 Transformer 颠覆了深度学习?
1.1 RNN/LSTM 的致命缺陷
在 Transformer 之前,序列建模主要靠 RNN/LSTM。但它们有两个核心问题:
| 问题 | RNN/LSTM | Transformer | |------|----------|-------------| | 长距离依赖 | 信息通过隐藏状态逐步传递,长序列容易梯度消失 | 直接计算任意两个位置的注意力,无距离限制 | | 并行计算 | 必须串行处理(t 时刻依赖 t-1) | 所有位置可并行计算 | | 训练速度 | 慢(无法充分利用 GPU) | 快(矩阵运算高度并行) |
数学本质: RNN 的隐藏状态更新是 h_t = f(h_{t-1}, x_t),这是一个递推关系,无法并行。而 Transformer 的 Self-Attention 是 Attention(Q, K, V),这是一个全局操作,可以一次性计算所有位置。
1.2 Transformer 的核心思想
Transformer 的核心是 Self-Attention(自注意力):让序列中的每个位置都能"看到"其他所有位置,并根据相关性动态分配权重。
直觉理解: 想象你在读一句话"The cat sat on the mat because it was tired"。当处理"it"时,你需要知道"it"指的是"cat"还是"mat"。Self-Attention 会让"it"对句子中所有词计算注意力权重,发现"cat"的权重最高,从而理解"it"指的是"cat"。
二、Self-Attention 的数学原理
2.1 从 Query-Key-Value 到注意力权重
Self-Attention 的核心是将输入映射为三个向量:Query(查询)、Key(键)、Value(值)。
数学公式:
Attention(Q, K, V) = softmax(QK^T / √d_k) V
其中:
Q:Query 矩阵,形状[seq_len, d_k]K:Key 矩阵,形状[seq_len, d_k]V:Value 矩阵,形状[seq_len, d_v]d_k:Key 的维度(用于缩放)
逐步推导:
- 计算相似度:
QK^T得到[seq_len, seq_len]的相似度矩阵 - 缩放:除以
√d_k防止点积过大导致 softmax 梯度消失 - 归一化:softmax 将相似度转为概率分布(每行和为 1)
- 加权求和:用注意力权重对 V 加权,得到输出
为什么要缩放? 假设 Q 和 K 的元素独立同分布,均值为 0,方差为 1。则 QK^T 的方差为 d_k。当 d_k 很大时,点积的绝对值会很大,softmax 会输出接近 one-hot 的分布,梯度接近 0。除以 √d_k 可以将方差归一化为 1。
2.2 代码实现:从零手写 Self-Attention
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
class SelfAttention(nn.Module):
def __init__(self, d_model: int, d_k: int, d_v: int):
"""
Args:
d_model: 输入维度
d_k: Query/Key 的维度
d_v: Value 的维度
"""
super().__init__()
self.d_k = d_k
# 线性变换:将输入映射到 Q, K, V
self.W_q = nn.Linear(d_model, d_k, bias=False)
self.W_k = nn.Linear(d_model, d_k, bias=False)
self.W_v = nn.Linear(d_model, d_v, bias=False)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Args:
x: 输入张量,形状 [batch_size, seq_len, d_model]
Returns:
输出张量,形状 [batch_size, seq_len, d_v]
"""
# 步骤 1:计算 Q, K, V
Q = self.W_q(x) # [batch, seq_len, d_k]
K = self.W_k(x) # [batch, seq_len, d_k]
V = self.W_v(x) # [batch, seq_len, d_v]
# 步骤 2:计算注意力分数 QK^T / √d_k
# Q: [batch, seq_len, d_k] → Q^T: [batch, d_k, seq_len]
# scores: [batch, seq_len, seq_len]
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
# 步骤 3:softmax 归一化
attn_weights = F.softmax(scores, dim=-1) # [batch, seq_len, seq_len]
# 步骤 4:加权求和
output = torch.matmul(attn_weights, V) # [batch, seq_len, d_v]
return output, attn_weights
# 测试
batch_size, seq_len, d_model, d_k, d_v = 2, 10, 512, 64, 64
x = torch.randn(batch_size, seq_len, d_model)
attention = SelfAttention(d_model, d_k, d_v)
output, attn_weights = attention(x)
print(f"输入形状: {x.shape}")
print(f"输出形状: {output.shape}")
print(f"注意力权重形状: {attn_weights.shape}")
print(f"注意力权重之和(应为 1): {attn_weights[0, 0].sum().item():.4f}")
2.3 可视化注意力权重
import matplotlib.pyplot as plt
import seaborn as sns
# 可视化注意力权重
plt.figure(figsize=(8, 6))
sns.heatmap(
attn_weights[0].detach().numpy(),
cmap='Blues',
xticklabels=[f'pos_{i}' for i in range(seq_len)],
yticklabels=[f'pos_{i}' for i in range(seq_len)]
)
plt.title('Self-Attention Weights')
plt.xlabel('Key Position')
plt.ylabel('Query Position')
plt.tight_layout()
plt.savefig('attention_weights.png', dpi=150)
plt.show()
三、Multi-Head Attention:多头注意力
3.1 为什么需要多头?
单头 Self-Attention 只能学习一种"关注模式"。但语言中有多重关系:
- 语法关系:主谓一致、修饰关系
- 语义关系:指代消解、同义词
- 位置关系:相邻词、远距离依赖
Multi-Head Attention 通过并行运行多个注意力头,让模型同时关注不同类型的关系。
数学公式:
MultiHead(Q, K, V) = Concat(head_1, ..., head_h) W^O
where head_i = Attention(Q W_i^Q, K W_i^K, V W_i^V)
其中 h 是头的数量,W_i^Q, W_i^K, W_i^V 是第 i 个头的投影矩阵,W^O 是输出投影矩阵。
3.2 代码实现:Multi-Head Attention
class MultiHeadAttention(nn.Module):
def __init__(self, d_model: int, num_heads: int):
"""
Args:
d_model: 模型维度(输入输出维度)
num_heads: 注意力头数量
"""
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, bias=False)
self.W_k = nn.Linear(d_model, d_model, bias=False)
self.W_v = nn.Linear(d_model, d_model, bias=False)
self.W_o = nn.Linear(d_model, d_model, bias=False)
def scaled_dot_product_attention(self, Q, K, V, mask=None):
"""
计算缩放点积注意力
Args:
Q, K, V: 形状 [batch, num_heads, seq_len, d_k]
mask: 可选的掩码,形状 [batch, 1, 1, seq_len] 或 [batch, 1, seq_len, seq_len]
"""
# 计算注意力分数
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
# 应用掩码(用于 Decoder 的因果掩码)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
# softmax 归一化
attn_weights = F.softmax(scores, dim=-1)
# 加权求和
output = torch.matmul(attn_weights, V)
return output, attn_weights
def forward(self, query, key, value, mask=None):
"""
Args:
query, key, value: 形状 [batch_size, seq_len, d_model]
mask: 可选的掩码
Returns:
output: 形状 [batch_size, seq_len, d_model]
attn_weights: 形状 [batch_size, num_heads, seq_len, seq_len]
"""
batch_size = query.size(0)
# 步骤 1:线性变换
Q = self.W_q(query) # [batch, seq_len, d_model]
K = self.W_k(key)
V = self.W_v(value)
# 步骤 2:拆分为多个头
# [batch, seq_len, d_model] → [batch, seq_len, num_heads, d_k]
# → [batch, num_heads, seq_len, d_k]
Q = Q.view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
K = K.view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
V = V.view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
# 步骤 3:计算注意力
attn_output, attn_weights = self.scaled_dot_product_attention(Q, K, V, mask)
# 步骤 4:拼接多个头
# [batch, num_heads, seq_len, d_k] → [batch, seq_len, d_model]
attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
# 步骤 5:输出投影
output = self.W_o(attn_output)
return output, attn_weights
# 测试
batch_size, seq_len, d_model, num_heads = 2, 10, 512, 8
x = torch.randn(batch_size, seq_len, d_model)
mha = MultiHeadAttention(d_model, num_heads)
output, attn_weights = mha(x, x, x)
print(f"输入形状: {x.shape}")
print(f"输出形状: {output.shape}")
print(f"注意力权重形状: {attn_weights.shape}")
3.3 因果掩码(Causal Mask)
在 Decoder 中,为了防止"看到未来"的信息,需要使用因果掩码:
def create_causal_mask(seq_len, device):
"""
创建因果掩码,确保位置 i 只能看到位置 0 到 i
返回布尔掩码:True 表示可见,False 表示屏蔽
形状:[1, 1, seq_len, seq_len]
"""
mask = torch.tril(torch.ones(seq_len, seq_len, device=device), diagonal=0).bool()
return mask.unsqueeze(0).unsqueeze(0) # [1, 1, seq_len, seq_len]
# 测试
mask = create_causal_mask(5, device='cpu')
print(mask.squeeze())
# 输出:
# tensor([[ True, False, False, False, False],
# [ True, True, False, False, False],
# [ True, True, True, False, False],
# [ True, True, True, True, False],
# [ True, True, True, True, True]])
四、位置编码:让 Transformer 理解顺序
4.1 为什么需要位置编码?
Self-Attention 是排列不变的:它不关心输入的顺序。如果打乱输入序列的顺序,输出也会相应打乱,但每个位置的输出值不变。
但语言是有顺序的:"I love you" 和 "You love I" 意思完全不同。因此需要位置编码来注入位置信息。
4.2 正弦位置编码(Sinusoidal Positional Encoding)
原始 Transformer 使用正弦和余弦函数生成位置编码:
PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
其中 pos 是位置索引,i 是维度索引。
直觉理解: 不同维度使用不同频率的正弦波,低频维度捕捉大范围的位置关系,高频维度捕捉局部位置关系。
class PositionalEncoding(nn.Module):
def __init__(self, d_model: int, max_len: int = 5000):
super().__init__()
# 创建位置编码矩阵
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(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]
# 注册为 buffer(不参与训练,但会随模型保存/迁移)
self.register_buffer('pe', pe)
def forward(self, x):
"""
Args:
x: 形状 [batch_size, seq_len, d_model]
"""
# 加上位置编码
x = x + self.pe[:, :x.size(1), :]
return x
# 可视化位置编码
pe = PositionalEncoding(d_model=512, max_len=100)
pe_matrix = pe.pe[0, :100, :].numpy()
plt.figure(figsize=(12, 6))
plt.imshow(pe_matrix, cmap='viridis', aspect='auto')
plt.xlabel('d_model')
plt.ylabel('Position')
plt.title('Sinusoidal Positional Encoding')
plt.colorbar()
plt.tight_layout()
plt.savefig('positional_encoding.png', dpi=150)
plt.show()
4.3 旋转位置编码(RoPE):2026 年主流方案
RoPE(Rotary Position Embedding) 是 LLaMA、Qwen 等现代大模型的主流选择。相比正弦编码,RoPE 有以下优势:
- 相对位置感知:直接编码相对位置关系
- 外推能力:能处理训练时未见过的序列长度
- 计算高效:通过旋转矩阵实现,无需额外参数
数学原理: 将 Query 和 Key 视为复数,通过旋转矩阵注入位置信息:
q_m = W_q * x_m * e^(i*m*θ)
k_n = W_k * x_n * e^(i*n*θ)
其中 θ 是频率参数,m, n 是位置索引。
class RotaryPositionalEncoding(nn.Module):
"""
RoPE:旋转位置编码(LLaMA、Qwen 使用)
"""
def __init__(self, d_model: int, max_seq_len: int = 2048, base: float = 10000.0):
super().__init__()
self.d_model = d_model
self.max_seq_len = max_seq_len
# 计算频率
inv_freq = 1.0 / (base ** (torch.arange(0, d_model, 2).float() / d_model))
self.register_buffer('inv_freq', inv_freq)
# 预计算 cos 和 sin
self._build_cache(max_seq_len)
def _build_cache(self, seq_len: int):
t = torch.arange(seq_len, device=self.inv_freq.device)
freqs = torch.einsum('i,j->ij', t, self.inv_freq)
emb = torch.cat((freqs, freqs), dim=-1)
self.register_buffer('cos_cached', emb.cos().unsqueeze(0).unsqueeze(0))
self.register_buffer('sin_cached', emb.sin().unsqueeze(0).unsqueeze(0))
def rotate_half(self, x):
"""将张量后半部分旋转到前半部分"""
x1, x2 = x.chunk(2, dim=-1)
return torch.cat((-x2, x1), dim=-1)
def forward(self, q, k):
"""
Args:
q, k: 形状 [batch, num_heads, seq_len, d_k]
"""
seq_len = q.shape[2]
if seq_len > self.max_seq_len:
self._build_cache(seq_len)
cos = self.cos_cached[:, :, :seq_len, :]
sin = self.sin_cached[:, :, :seq_len, :]
# 应用旋转
q_rot = q * cos + self.rotate_half(q) * sin
k_rot = k * cos + self.rotate_half(k) * sin
return q_rot, k_rot
# 测试
rope = RotaryPositionalEncoding(d_model=64)
q = torch.randn(2, 8, 10, 64) # [batch, heads, seq_len, d_k]
k = torch.randn(2, 8, 10, 64)
q_rot, k_rot = rope(q, k)
print(f"RoPE 后 Q 形状: {q_rot.shape}")
五、完整 Transformer 实现
5.1 Transformer 架构概览
原始 Transformer 由 Encoder 和 Decoder 组成:
Encoder:
Input → Embedding + Positional Encoding → [Encoder Layer × N] → Encoder Output
Decoder:
Output (shifted right) → Embedding + Positional Encoding → [Decoder Layer × N] → Linear → Softmax
Encoder Layer:
Multi-Head Self-Attention → Add & Norm → Feed Forward → Add & Norm
Decoder Layer:
Masked Multi-Head Self-Attention → Add & Norm →
Multi-Head Cross-Attention (with Encoder Output) → Add & Norm →
Feed Forward → Add & Norm
5.2 前馈神经网络(Feed-Forward Network)
class FeedForward(nn.Module):
"""
位置级前馈网络:两层全连接 + ReLU + Dropout
"""
def __init__(self, d_model: int, d_ff: int, dropout: float = 0.1):
super().__init__()
self.linear1 = nn.Linear(d_model, d_ff)
self.linear2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
return self.linear2(self.dropout(F.relu(self.linear1(x))))
5.3 Encoder Layer
class EncoderLayer(nn.Module):
def __init__(self, d_model: int, num_heads: int, d_ff: int, dropout: float = 0.1):
super().__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.feed_forward = FeedForward(d_model, d_ff, dropout)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
def forward(self, x, src_mask=None):
# Multi-Head Self-Attention + Add & Norm
attn_output, _ = self.self_attn(x, x, x, src_mask)
x = self.norm1(x + self.dropout1(attn_output))
# Feed Forward + Add & Norm
ff_output = self.feed_forward(x)
x = self.norm2(x + self.dropout2(ff_output))
return x
5.4 Decoder Layer
class DecoderLayer(nn.Module):
def __init__(self, d_model: int, num_heads: int, d_ff: int, dropout: float = 0.1):
super().__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.cross_attn = MultiHeadAttention(d_model, num_heads)
self.feed_forward = 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.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
self.dropout3 = nn.Dropout(dropout)
def forward(self, x, encoder_output, src_mask=None, tgt_mask=None):
# Masked Multi-Head Self-Attention + Add & Norm
attn_output, _ = self.self_attn(x, x, x, tgt_mask)
x = self.norm1(x + self.dropout1(attn_output))
# Multi-Head Cross-Attention + Add & Norm
attn_output, _ = self.cross_attn(x, encoder_output, encoder_output, src_mask)
x = self.norm2(x + self.dropout2(attn_output))
# Feed Forward + Add & Norm
ff_output = self.feed_forward(x)
x = self.norm3(x + self.dropout3(ff_output))
return x
5.5 完整 Transformer
class Transformer(nn.Module):
def __init__(
self,
src_vocab_size: int,
tgt_vocab_size: int,
d_model: int = 512,
num_heads: int = 8,
num_layers: int = 6,
d_ff: int = 2048,
max_len: int = 5000,
dropout: float = 0.1
):
super().__init__()
# Embedding 层
self.src_embedding = nn.Embedding(src_vocab_size, d_model)
self.tgt_embedding = nn.Embedding(tgt_vocab_size, d_model)
self.positional_encoding = PositionalEncoding(d_model, max_len)
# Encoder
self.encoder_layers = nn.ModuleList([
EncoderLayer(d_model, num_heads, d_ff, dropout)
for _ in range(num_layers)
])
# Decoder
self.decoder_layers = nn.ModuleList([
DecoderLayer(d_model, num_heads, d_ff, dropout)
for _ in range(num_layers)
])
# 输出层
self.output_projection = nn.Linear(d_model, tgt_vocab_size)
self.dropout = nn.Dropout(dropout)
self.d_model = d_model
def encode(self, src, src_mask=None):
# Embedding + Positional Encoding
x = self.src_embedding(src) * math.sqrt(self.d_model)
x = self.positional_encoding(x)
x = self.dropout(x)
# Encoder Layers
for layer in self.encoder_layers:
x = layer(x, src_mask)
return x
def decode(self, tgt, encoder_output, src_mask=None, tgt_mask=None):
# Embedding + Positional Encoding
x = self.tgt_embedding(tgt) * math.sqrt(self.d_model)
x = self.positional_encoding(x)
x = self.dropout(x)
# Decoder Layers
for layer in self.decoder_layers:
x = layer(x, encoder_output, src_mask, tgt_mask)
return x
def forward(self, src, tgt, src_mask=None, tgt_mask=None):
# Encode
encoder_output = self.encode(src, src_mask)
# Decode
decoder_output = self.decode(tgt, encoder_output, src_mask, tgt_mask)
# Output projection
output = self.output_projection(decoder_output)
return output
5.6 使用示例:机器翻译
# 创建模型
src_vocab_size = 10000 # 源语言词汇表大小
tgt_vocab_size = 10000 # 目标语言词汇表大小
model = Transformer(
src_vocab_size=src_vocab_size,
tgt_vocab_size=tgt_vocab_size,
d_model=512,
num_heads=8,
num_layers=6,
d_ff=2048
)
# 模拟输入
batch_size = 32
src_seq_len = 20
tgt_seq_len = 25
src = torch.randint(0, src_vocab_size, (batch_size, src_seq_len))
tgt = torch.randint(0, tgt_vocab_size, (batch_size, tgt_seq_len))
# 创建掩码
src_mask = None # Encoder 不需要掩码
tgt_mask = create_causal_mask(tgt_seq_len, device='cpu')
# 前向传播
output = model(src, tgt, src_mask, tgt_mask)
print(f"输出形状: {output.shape}") # [batch, tgt_seq_len, tgt_vocab_size]
六、2026 年 Transformer 最新优化技术
6.1 Flash Attention:IO 感知的注意力计算
问题: 标准 Attention 的中间结果(QK^T)需要大量显存,成为长序列训练的瓶颈。
解决方案: Flash Attention 通过分块计算(tiling)和重计算(recomputation)技术,将显存占用从 O(N^2) 降低到 O(N),同时保持数学等价。
# 使用 PyTorch 2.x 内置的 Flash Attention(需要 GPU)
if torch.cuda.is_available():
# PyTorch 2.0+ 内置 scaled_dot_product_attention,自动使用 Flash Attention
with torch.backends.cuda.sdp_kernel(enable_flash=True, enable_math=False, enable_mem_efficient=False):
output = F.scaled_dot_product_attention(Q, K, V, attn_mask=None, dropout_p=0.0)
性能对比(A100 GPU):
| 序列长度 | 标准 Attention | Flash Attention | 加速比 | |---------|---------------|----------------|--------| | 1K | 1.2 ms | 0.8 ms | 1.5x | | 4K | 19 ms | 6 ms | 3.2x | | 16K | OOM | 95 ms | - | | 64K | OOM | 1.5 s | - |
6.2 Grouped-Query Attention(GQA)
问题: Multi-Head Attention 的 KV Cache 占用大量显存,推理成本高。
解决方案: GQA 让多个 Query 头共享同一组 Key/Value 头,在性能和显存之间取得平衡。
class GroupedQueryAttention(nn.Module):
"""
GQA:分组查询注意力(LLaMA 2、Qwen 2 使用)
num_kv_heads < num_heads,多个 Q 头共享 KV 头
"""
def __init__(self, d_model: int, num_heads: int, num_kv_heads: int):
super().__init__()
self.num_heads = num_heads
self.num_kv_heads = num_kv_heads
self.num_groups = num_heads // num_kv_heads
self.d_k = d_model // num_heads
self.W_q = nn.Linear(d_model, d_model, bias=False)
self.W_k = nn.Linear(d_model, num_kv_heads * self.d_k, bias=False)
self.W_v = nn.Linear(d_model, num_kv_heads * self.d_k, bias=False)
self.W_o = nn.Linear(d_model, d_model, bias=False)
def forward(self, query, key, value, mask=None):
batch_size, seq_len, _ = query.shape
Q = self.W_q(query).view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2)
K = self.W_k(key).view(batch_size, seq_len, self.num_kv_heads, self.d_k).transpose(1, 2)
V = self.W_v(value).view(batch_size, seq_len, self.num_kv_heads, self.d_k).transpose(1, 2)
# 重复 KV 以匹配 Q 的头数
K = K.repeat_interleave(self.num_groups, dim=1)
V = V.repeat_interleave(self.num_groups, dim=1)
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
attn_weights = F.softmax(scores, dim=-1)
output = torch.matmul(attn_weights, V)
output = output.transpose(1, 2).contiguous().view(batch_size, seq_len, -1)
return self.W_o(output)
# 对比
# MHA: num_heads=32, num_kv_heads=32 → KV Cache 大
# GQA: num_heads=32, num_kv_heads=8 → KV Cache 缩小 4 倍
# MQA: num_heads=32, num_kv_heads=1 → KV Cache 最小,但性能下降
6.3 SwiGLU 激活函数
现代大模型(LLaMA、PaLM)使用 SwiGLU 替代 ReLU,性能更好:
class SwiGLU(nn.Module):
"""
SwiGLU 激活函数(LLaMA、PaLM 使用)
SwiGLU(x, W, V) = Swish(xW) ⊙ (xV)
"""
def __init__(self, d_model: int, d_ff: int):
super().__init__()
self.w1 = nn.Linear(d_model, d_ff, bias=False)
self.w2 = nn.Linear(d_model, d_ff, bias=False)
self.w3 = nn.Linear(d_ff, d_model, bias=False)
def forward(self, x):
return self.w3(F.silu(self.w1(x)) * self.w2(x))
七、实战:用 Transformer 做序列预测
7.1 任务定义
用 Transformer 预测正弦波的未来值:输入过去 50 个点,预测未来 10 个点。
import numpy as np
from torch.utils.data import Dataset, DataLoader
class SineWaveDataset(Dataset):
def __init__(self, num_samples=10000, seq_len=50, pred_len=10):
self.num_samples = num_samples
self.seq_len = seq_len
self.pred_len = pred_len
# 生成正弦波数据
t = np.linspace(0, 100, num_samples * (seq_len + pred_len))
self.data = np.sin(t)
def __len__(self):
return self.num_samples
def __getitem__(self, idx):
start = idx * (self.seq_len + self.pred_len)
x = self.data[start:start + self.seq_len]
y = self.data[start + self.seq_len:start + self.seq_len + self.pred_len]
return torch.tensor(x, dtype=torch.float32).unsqueeze(-1), \
torch.tensor(y, dtype=torch.float32).unsqueeze(-1)
# 创建数据集
train_dataset = SineWaveDataset(num_samples=10000, seq_len=50, pred_len=10)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
7.2 定义预测模型
class TransformerPredictor(nn.Module):
def __init__(self, d_model=64, num_heads=4, num_layers=2, d_ff=256, pred_len=10):
super().__init__()
self.pred_len = pred_len
self.input_projection = nn.Linear(1, d_model)
self.positional_encoding = PositionalEncoding(d_model, max_len=100)
encoder_layer = nn.TransformerEncoderLayer(
d_model=d_model,
nhead=num_heads,
dim_feedforward=d_ff,
batch_first=True
)
self.transformer_encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
# 输出 pred_len 个时间步
self.output_projection = nn.Linear(d_model, pred_len)
def forward(self, x):
# x: [batch, seq_len, 1]
x = self.input_projection(x) # [batch, seq_len, d_model]
x = self.positional_encoding(x)
x = self.transformer_encoder(x) # [batch, seq_len, d_model]
x = self.output_projection(x[:, -1, :]) # [batch, pred_len]
return x.unsqueeze(-1) # [batch, pred_len, 1]
7.3 训练与评估
# 创建模型
model = TransformerPredictor(d_model=64, num_heads=4, num_layers=2)
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# 训练
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
for epoch in range(10):
model.train()
total_loss = 0
for batch_x, batch_y in train_loader:
batch_x = batch_x.to(device)
batch_y = batch_y.to(device)
optimizer.zero_grad()
output = model(batch_x)
loss = criterion(output, batch_y)
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f"Epoch {epoch+1}/10, Loss: {total_loss/len(train_loader):.6f}")
# 可视化预测结果
model.eval()
with torch.no_grad():
test_x, test_y = train_dataset[0]
test_x = test_x.unsqueeze(0).to(device)
pred = model(test_x).cpu().numpy()
plt.figure(figsize=(12, 4))
plt.plot(range(50), test_x[0, :, 0].numpy(), label='Input (50 steps)', marker='o', markersize=3)
plt.plot(range(50, 60), test_y[:, 0].numpy(), label='Ground Truth (10 steps)', marker='s', markersize=4)
plt.plot(range(50, 60), pred[0, :, 0], label='Prediction (10 steps)', marker='^', markersize=4)
plt.axvline(x=49.5, color='gray', linestyle='--', alpha=0.5, label='Prediction start')
plt.legend()
plt.title('Transformer Sequence Prediction')
plt.xlabel('Time Step')
plt.ylabel('Value')
plt.tight_layout()
plt.savefig('transformer_prediction.png', dpi=150)
plt.show()
八、总结与最佳实践
8.1 Transformer 核心要点
- Self-Attention:让每个位置都能关注所有其他位置,解决长距离依赖问题
- Multi-Head Attention:并行学习多种关注模式
- 位置编码:注入序列顺序信息(正弦编码 / RoPE)
- 残差连接 + LayerNorm:稳定训练,加速收敛
- 因果掩码:Decoder 中防止看到未来信息
8.2 2026 年最佳实践
| 组件 | 推荐方案 | 适用场景 | |------|---------|---------| | 位置编码 | RoPE | 大语言模型(LLaMA、Qwen) | | 注意力机制 | Flash Attention | 长序列训练/推理 | | KV Cache 优化 | GQA | 推理加速,显存受限 | | 激活函数 | SwiGLU | 大语言模型 | | 归一化 | RMSNorm | 替代 LayerNorm,更高效 |
8.3 下一步学习建议
- 阅读源码:Hugging Face Transformers 库的
modeling_llama.py - 实战微调:用 LoRA 微调预训练 Transformer(下一篇博客主题)
- 多模态扩展:了解 Vision Transformer(ViT)和 CLIP
- 性能优化:学习 Tensor Parallelism 和 Pipeline Parallelism
九、KV Cache:自回归推理加速
9.1 为什么需要 KV Cache?
问题: 自回归生成时,每生成一个 token 都要重新计算所有历史 token 的 K 和 V,计算量随序列长度线性增长。
解决方案: 缓存已计算的 K 和 V,每次只计算新 token 的 K 和 V。
9.2 KV Cache 实现
class KVCacheAttention(nn.Module):
"""带 KV Cache 的注意力层(用于推理加速)"""
def __init__(self, d_model: int, num_heads: int):
super().__init__()
self.num_heads = num_heads
self.d_k = d_model // num_heads
self.W_q = nn.Linear(d_model, d_model, bias=False)
self.W_k = nn.Linear(d_model, d_model, bias=False)
self.W_v = nn.Linear(d_model, d_model, bias=False)
self.W_o = nn.Linear(d_model, d_model, bias=False)
self.cache_k = None
self.cache_v = None
def forward(self, x, use_cache=False):
"""
Args:
x: [batch, seq_len, d_model]
use_cache: 是否使用 KV Cache
"""
batch_size, seq_len, _ = x.shape
Q = self.W_q(x).view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2)
K = self.W_k(x).view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2)
V = self.W_v(x).view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2)
# 使用 KV Cache
if use_cache:
if self.cache_k is not None:
K = torch.cat([self.cache_k, K], dim=2)
V = torch.cat([self.cache_v, V], dim=2)
self.cache_k = K
self.cache_v = V
# 计算注意力
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
attn_weights = F.softmax(scores, dim=-1)
output = torch.matmul(attn_weights, V)
output = output.transpose(1, 2).contiguous().view(batch_size, -1, self.num_heads * self.d_k)
return self.W_o(output)
def reset_cache(self):
"""重置缓存"""
self.cache_k = None
self.cache_v = None
# 使用示例
model = KVCacheAttention(d_model=512, num_heads=8)
model.eval()
# 第一次推理(prefill)
x = torch.randn(1, 10, 512)
output1 = model(x, use_cache=True)
# 后续推理(decode,每次只处理 1 个 token)
x_new = torch.randn(1, 1, 512)
output2 = model(x_new, use_cache=True) # 只计算新 token,复用缓存
# 重置缓存(新序列)
model.reset_cache()
KV Cache 性能对比(LLaMA-2 7B,A100):
| 方法 | 首 token 延迟 | 后续 token 延迟 | 显存占用 | |------|-------------|---------------|---------| | 无缓存 | 100ms | 100ms | 14 GB | | KV Cache | 100ms | 15ms | 18 GB |
注意: KV Cache 会增加显存占用(需要存储所有历史 token 的 K 和 V),但大幅降低计算延迟。
十、实战:机器翻译完整示例
10.1 数据准备
from torchtext.datasets import Multi30k
from torchtext.data.utils import get_tokenizer
from torchtext.vocab import build_vocab_from_iterator
# 加载数据集
train_iter = Multi30k(split='train', language_pair=('de', 'en'))
# 分词器
de_tokenizer = get_tokenizer('spacy', language='de_core_news_sm')
en_tokenizer = get_tokenizer('spacy', language='en_core_web_sm')
# 构建词汇表
def yield_tokens(data_iter, tokenizer, language):
for _, text in data_iter:
yield tokenizer(text if language == 'de' else text)
de_vocab = build_vocab_from_iterator(yield_tokens(train_iter, de_tokenizer, 'de'), specials=['<unk>', '<pad>', '<bos>', '<eos>'])
en_vocab = build_vocab_from_iterator(yield_tokens(train_iter, en_tokenizer, 'en'), specials=['<unk>', '<pad>', '<bos>', '<eos>'])
de_vocab.set_default_index(de_vocab['<unk>'])
en_vocab.set_default_index(en_vocab['<unk>'])
print(f"德语词汇量: {len(de_vocab)}")
print(f"英语词汇量: {len(en_vocab)}")
10.2 训练机器翻译模型
# 创建模型
src_vocab_size = len(de_vocab)
tgt_vocab_size = len(en_vocab)
model = Transformer(
src_vocab_size=src_vocab_size,
tgt_vocab_size=tgt_vocab_size,
d_model=512,
num_heads=8,
num_layers=6,
d_ff=2048
).to(device)
criterion = nn.CrossEntropyLoss(ignore_index=en_vocab['<pad>'])
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4, betas=(0.9, 0.98), eps=1e-9)
# 训练循环
def train_translation(model, iterator, optimizer, criterion, clip):
model.train()
epoch_loss = 0
for src, tgt in iterator:
src = src.to(device)
tgt = tgt.to(device)
# 准备输入和目标
src = src.permute(1, 0) # [batch, src_len]
tgt_input = tgt[:, :-1] # [batch, tgt_len-1]
tgt_output = tgt[:, 1:] # [batch, tgt_len-1]
# 创建掩码
tgt_mask = create_causal_mask(tgt_input.size(1), device)
optimizer.zero_grad()
# 前向传播
output = model(src, tgt_input, tgt_mask=tgt_mask)
# 计算损失
output = output.reshape(-1, tgt_vocab_size)
tgt_output = tgt_output.reshape(-1)
loss = criterion(output, tgt_output)
loss.backward()
# 梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), clip)
optimizer.step()
epoch_loss += loss.item()
return epoch_loss / len(iterator)
# 训练
for epoch in range(10):
loss = train_translation(model, train_iter, optimizer, criterion, clip=1.0)
print(f"Epoch {epoch+1}, Loss: {loss:.4f}")
10.3 推理:贪婪解码
def greedy_decode(model, src, src_vocab, tgt_vocab, max_len=50):
"""贪婪解码生成翻译"""
model.eval()
# 编码源序列
src = src.unsqueeze(0).to(device)
encoder_output = model.encode(src)
# 初始化目标序列(只包含 <bos>)
tgt_tokens = torch.tensor([[tgt_vocab['<bos>']]], device=device)
for _ in range(max_len):
# 创建掩码
tgt_mask = create_causal_mask(tgt_tokens.size(1), device)
# 解码
decoder_output = model.decode(tgt_tokens, encoder_output, tgt_mask=tgt_mask)
# 取最后一个时间步的输出
output = model.output_projection(decoder_output[:, -1, :])
# 贪婪选择
next_token = output.argmax(dim=-1, keepdim=True)
tgt_tokens = torch.cat([tgt_tokens, next_token], dim=1)
# 如果生成 <eos>,停止
if next_token.item() == tgt_vocab['<eos>']:
break
# 转换为文本
tokens = tgt_tokens[0].cpu().tolist()
tokens = [tgt_vocab.lookup_token(t) for t in tokens]
tokens = [t for t in tokens if t not in ['<bos>', '<eos>', '<pad>']]
return ' '.join(tokens)
# 测试
test_sentence = "Eine Katze sitzt auf der Matte."
test_tokens = de_tokenizer(test_sentence)
test_indices = [de_vocab[t] for t in test_tokens]
test_tensor = torch.tensor(test_indices)
translation = greedy_decode(model, test_tensor, de_vocab, en_vocab)
print(f"德语: {test_sentence}")
print(f"英语: {translation}")
本文代码已完整实现,可直接运行。如有问题欢迎评论区交流。
下一篇预告:《大模型微调实战:用 LoRA/QLoRA 在消费级显卡上微调 LLaMA/Qwen》,教你用 8GB 显存的显卡微调 7B 参数的大模型!