Skip to content

章节5:Transformer架构


学习目标

  • 理解自注意力机制的计算流程(Q/K/V 缩放点积)
  • 掌握多头注意力的并行计算与拼接融合
  • 理解位置编码的作用与实现方式
  • 掌握 Transformer Encoder-Decoder 完整架构
  • 了解 Transformer 核心变体(ViT / BERT / GPT)

5.1 自注意力机制(Self-Attention)

5.1.1 从 RNN 到 Attention

RNN 存在两个根本问题:

  1. 顺序计算:无法并行,训练速度慢
  2. 长距离遗忘:距离越远,信息丢失越严重

自注意力(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)

计算步骤:

  1. 计算每个 Query 与所有 Key 的点积相似度
  2. 缩放(除以 $\sqrt{d_k}$)防止梯度消失
  3. Softmax 归一化为注意力权重
  4. 加权求和 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 计算流程

  1. 将 Q/K/V 线性投影到 $h$ 个不同的低维子空间
  2. 在每个子空间独立计算缩放点积注意力
  3. 拼接所有头的输出
  4. 通过线性层融合

公式:

$$ \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 变体对比

模型架构方向预训练任务典型应用
TransformerEncoder-Decoder双向+自回归翻译机器翻译
BERTEncoder only双向MLM + NSP自然语言理解
GPTDecoder only单向(自回归)下一词预测文本生成
ViTEncoder only双向图像分类视觉理解

小结

概念关键要点
自注意力Q/K/V 缩放点积,全局依赖,可并行
多头注意力多子空间并行学习,拼接融合
位置编码正弦/可学习,注入位置信息
Encoder-DecoderEncoder 编码输入,Decoder 自回归生成
FFN两层线性 + ReLU,增加表达能力
残差 + LayerNorm稳定深层训练
ViT图像分 Patch 输入 Transformer
BERT双向 Encoder,MLM 预训练
GPT单向 Decoder,自回归生成

练习

  1. 手写 Attention:不用 PyTorch 内置函数,手动实现 Q/K/V 投影 → 缩放点积 → Softmax → 加权求和。
  2. 注意力可视化:训练一个小型 Transformer 后,可视化注意力权重矩阵,分析模型关注了哪些位置。
  3. 实现 Transformer 翻译:使用小规模英德或英法平行语料,训练一个翻译 Transformer。
  4. 对比 ViT vs CNN:在 CIFAR-10 上分别训练 ViT 和 CNN,对比精度和训练速度。
  5. BERT 微调:使用 HuggingFace 加载 bert-base-chinese,在中文情感分类任务上微调。

Python 学习资料