Back

【深度学习进阶】Transformer 架构深度解析:从零手写 Self-Attention 与完整实现(2026 最新版)

June 29, 202617 min read
PyTorchTransformerVision Transformer

【深度学习进阶】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 的维度(用于缩放)

逐步推导:

  1. 计算相似度QK^T 得到 [seq_len, seq_len] 的相似度矩阵
  2. 缩放:除以 √d_k 防止点积过大导致 softmax 梯度消失
  3. 归一化:softmax 将相似度转为概率分布(每行和为 1)
  4. 加权求和:用注意力权重对 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 有以下优势:

  1. 相对位置感知:直接编码相对位置关系
  2. 外推能力:能处理训练时未见过的序列长度
  3. 计算高效:通过旋转矩阵实现,无需额外参数

数学原理: 将 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 由 EncoderDecoder 组成:

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 核心要点

  1. Self-Attention:让每个位置都能关注所有其他位置,解决长距离依赖问题
  2. Multi-Head Attention:并行学习多种关注模式
  3. 位置编码:注入序列顺序信息(正弦编码 / RoPE)
  4. 残差连接 + LayerNorm:稳定训练,加速收敛
  5. 因果掩码:Decoder 中防止看到未来信息

8.2 2026 年最佳实践

| 组件 | 推荐方案 | 适用场景 | |------|---------|---------| | 位置编码 | RoPE | 大语言模型(LLaMA、Qwen) | | 注意力机制 | Flash Attention | 长序列训练/推理 | | KV Cache 优化 | GQA | 推理加速,显存受限 | | 激活函数 | SwiGLU | 大语言模型 | | 归一化 | RMSNorm | 替代 LayerNorm,更高效 |

8.3 下一步学习建议

  1. 阅读源码:Hugging Face Transformers 库的 modeling_llama.py
  2. 实战微调:用 LoRA 微调预训练 Transformer(下一篇博客主题)
  3. 多模态扩展:了解 Vision Transformer(ViT)和 CLIP
  4. 性能优化:学习 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 参数的大模型!