Transformer 架构详解:从 Attention 到 BERT/GPT

关键词:Transformer、Self-Attention、多头注意力、位置编码、BERT、GPT


目录

  1. [为什么需要 Transformer?](#为什么需要 Transformer?)
  2. 整体架构一览
  3. [Self-Attention 自注意力机制](#Self-Attention 自注意力机制)
  4. [多头注意力(Multi-Head Attention)](#多头注意力(Multi-Head Attention))
  5. [位置编码(Positional Encoding)](#位置编码(Positional Encoding))
  6. 前馈网络与残差连接
  7. [Encoder 与 Decoder 的区别](#Encoder 与 Decoder 的区别)
  8. [从 Transformer 到 BERT](#从 Transformer 到 BERT)
  9. [从 Transformer 到 GPT](#从 Transformer 到 GPT)
  10. [代码实现:手写 Self-Attention](#代码实现:手写 Self-Attention)
  11. 总结与面试要点

1. 为什么需要 Transformer?

在 Transformer 出现之前,NLP 领域主要依赖 RNN(循环神经网络)和 LSTM。它们存在两个致命缺陷:

  • 无法并行计算:RNN 必须一个时间步一个时间步地顺序处理,训练效率极低。
  • 长距离依赖问题:句子很长时,梯度消失导致模型"记不住"前面的信息。

2017 年,Google 的论文《Attention is All You Need》横空出世,提出了完全基于注意力机制的 Transformer,彻底解决了上述问题,并成为后续 BERT、GPT、T5 等一切大模型的基石。


2. 整体架构一览

Transformer 由 Encoder(编码器)Decoder(解码器) 两部分组成,原始论文中各堆叠 6 层。

复制代码
输入序列
    ↓
[Embedding + Positional Encoding]
    ↓
┌─────────────────────┐
│   Encoder × N       │
│  ┌───────────────┐  │
│  │ Multi-Head    │  │
│  │ Self-Attention│  │
│  └───────┬───────┘  │
│      Add & Norm     │
│  ┌───────────────┐  │
│  │  Feed Forward │  │
│  └───────┬───────┘  │
│      Add & Norm     │
└─────────────────────┘
    ↓
┌─────────────────────┐
│   Decoder × N       │
│  Masked Self-Attn   │
│  Cross-Attention    │
│  Feed Forward       │
└─────────────────────┘
    ↓
Linear + Softmax
    ↓
输出序列

3. Self-Attention 自注意力机制

Self-Attention 是 Transformer 的核心,它让序列中每个位置都能"关注"到其他所有位置。

3.1 Q、K、V 是什么?

对于输入序列的每个词向量 x i x_i xi,通过三个可学习的权重矩阵分别映射出:

  • Q(Query,查询):当前词"想要查找什么"
  • K(Key,键):每个词"能提供什么信息"
  • V(Value,值):每个词"实际携带的内容"

Q = X W Q , K = X W K , V = X W V Q = XW^Q, \quad K = XW^K, \quad V = XW^V Q=XWQ,K=XWK,V=XWV

3.2 注意力分数计算

Attention ( Q , K , V ) = softmax ( Q K T d k ) V \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V Attention(Q,K,V)=softmax(dk QKT)V

  • Q K T QK^T QKT:计算每对词之间的相关性分数(点积)
  • d k \sqrt{d_k} dk :缩放因子,防止点积值过大导致 softmax 梯度消失
  • softmax:将分数归一化为概率分布
  • 最终乘以 V V V:按概率加权求和,得到每个词的上下文表示

直觉理解:处理句子"我爱北京天安门"时,"天安门"这个词在计算自己的表示时,会给"北京"分配更高的注意力权重,因为它们语义相关。


4. 多头注意力(Multi-Head Attention)

单头注意力只能从一个"角度"看词与词之间的关系。多头注意力把 Q、K、V 分别投影到 h h h 个不同的子空间,并行运行 h h h 次注意力计算:

MultiHead ( Q , K , V ) = Concat ( head 1 , ... , head h ) W O \text{MultiHead}(Q,K,V) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h)W^O MultiHead(Q,K,V)=Concat(head1,...,headh)WO

head i = Attention ( Q W i Q , K W i K , V W i V ) \text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V) headi=Attention(QWiQ,KWiK,VWiV)

好处:不同的头可以捕捉不同类型的语言关系,例如:

  • 第 1 个头关注句法结构(主谓关系)
  • 第 2 个头关注语义关联(近义词)
  • 第 3 个头关注指代关系("它"指代"猫")

原始论文使用 h = 8 h=8 h=8 个头, d m o d e l = 512 d_{model}=512 dmodel=512,每个头维度为 64。


5. 位置编码(Positional Encoding)

Attention 本身没有位置信息(对词序无感知)。Transformer 通过在词嵌入中加入位置编码来引入位置信息:

P E ( p o s , 2 i ) = sin ⁡ ( p o s 10000 2 i / d m o d e l ) PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right) PE(pos,2i)=sin(100002i/dmodelpos)

P E ( p o s , 2 i + 1 ) = cos ⁡ ( p o s 10000 2 i / d m o d e l ) PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right) PE(pos,2i+1)=cos(100002i/dmodelpos)

  • 使用正余弦函数是因为:任意位置的编码可以表示为另一位置编码的线性组合,便于模型学习相对位置。
  • 现代模型(如 LLaMA)改用 RoPE(旋转位置编码),性能更好且可外推到更长序列。

6. 前馈网络与残差连接

每个 Transformer 层还包含:

前馈网络(FFN):对每个位置独立进行两层线性变换(中间激活函数通常用 ReLU 或 GeLU):

FFN ( x ) = max ⁡ ( 0 , x W 1 + b 1 ) W 2 + b 2 \text{FFN}(x) = \max(0, xW_1 + b_1)W_2 + b_2 FFN(x)=max(0,xW1+b1)W2+b2

FFN 的维度通常是 d m o d e l d_{model} dmodel 的 4 倍(如 512 → 2048 → 512),负责存储模型的"知识"。

残差连接 + Layer Norm:每个子层的输出加上输入(残差),再做归一化:

output = LayerNorm ( x + Sublayer ( x ) ) \text{output} = \text{LayerNorm}(x + \text{Sublayer}(x)) output=LayerNorm(x+Sublayer(x))

这样做的好处是防止梯度消失,使深层网络更容易训练。


7. Encoder 与 Decoder 的区别

组件 Encoder Decoder
Self-Attention 双向(可看全文) Masked(只能看已生成部分)
Cross-Attention 有(关注 Encoder 输出)
典型应用 BERT(理解任务) GPT(生成任务)

Masked Self-Attention 是 Decoder 的关键设计:在训练时,通过遮住未来位置防止"作弊",保证自回归生成的合理性。


8. 从 Transformer 到 BERT

BERT(Bidirectional Encoder Representations from Transformers)只使用 Transformer 的 Encoder 部分。

核心思想:双向预训练

  • MLM(掩码语言模型):随机遮住 15% 的词,让模型预测被遮住的词

  • NSP(下一句预测):判断两个句子是否相邻

    输入:我 [MASK] 北京 → 预测:爱
    输入:[CLS] 句子A [SEP] 句子B [SEP]

BERT 的双向性使它在理解任务(分类、NER、问答)上表现优异,但不擅长文本生成。


9. 从 Transformer 到 GPT

GPT 系列只使用 Transformer 的 Decoder 部分(去掉 Cross-Attention)。

核心思想:自回归语言模型,预测下一个词

复制代码
输入:今天天气  →  输出:真
输入:今天天气真  →  输出:好
输入:今天天气真好  →  输出:啊

GPT 的单向性(只能看左边)使其天然适合文本生成任务,通过 Instruction Fine-tuning 和 RLHF 变成了 ChatGPT。


10. 代码实现:手写 Self-Attention

python 复制代码
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):
        super().__init__()
        self.d_model = d_model
        # Q、K、V 的线性投影
        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, x, mask=None):
        """
        x: (batch_size, seq_len, d_model)
        mask: (batch_size, seq_len, seq_len) 可选,用于 Decoder 的因果掩码
        """
        batch_size, seq_len, _ = x.shape
        
        # 计算 Q、K、V
        Q = self.W_q(x)  # (B, L, d_model)
        K = self.W_k(x)
        V = self.W_v(x)
        
        # 计算注意力分数并缩放
        scale = math.sqrt(self.d_model)
        scores = torch.bmm(Q, K.transpose(1, 2)) / scale  # (B, L, L)
        
        # 应用掩码(Decoder 中防止看到未来位置)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, float('-inf'))
        
        # Softmax 归一化
        attn_weights = F.softmax(scores, dim=-1)  # (B, L, L)
        
        # 加权求和
        output = torch.bmm(attn_weights, V)  # (B, L, d_model)
        output = self.W_o(output)
        
        return output, attn_weights


class MultiHeadAttention(nn.Module):
    """多头注意力实现"""
    
    def __init__(self, d_model: int, num_heads: int):
        super().__init__()
        assert d_model % num_heads == 0
        
        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 split_heads(self, x):
        """将最后一维拆分成 (num_heads, d_k)"""
        B, L, _ = x.shape
        x = x.view(B, L, self.num_heads, self.d_k)
        return x.transpose(1, 2)  # (B, num_heads, L, d_k)
    
    def forward(self, x, mask=None):
        B, L, _ = x.shape
        
        # 线性投影
        Q = self.split_heads(self.W_q(x))  # (B, h, L, d_k)
        K = self.split_heads(self.W_k(x))
        V = self.split_heads(self.W_v(x))
        
        # 缩放点积注意力
        scale = math.sqrt(self.d_k)
        scores = torch.matmul(Q, K.transpose(-2, -1)) / scale  # (B, h, L, L)
        
        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)  # (B, h, L, d_k)
        
        # 拼接所有头
        output = output.transpose(1, 2).contiguous().view(B, L, self.d_model)
        output = self.W_o(output)
        
        return output


# ========== 测试 ==========
if __name__ == "__main__":
    batch_size, seq_len, d_model = 2, 10, 512
    num_heads = 8
    
    x = torch.randn(batch_size, seq_len, d_model)
    
    # 单头测试
    single_attn = SelfAttention(d_model)
    out, weights = single_attn(x)
    print(f"单头注意力输出: {out.shape}")         # (2, 10, 512)
    print(f"注意力权重矩阵: {weights.shape}")      # (2, 10, 10)
    
    # 多头测试
    multi_attn = MultiHeadAttention(d_model, num_heads)
    out = multi_attn(x)
    print(f"多头注意力输出: {out.shape}")          # (2, 10, 512)
    
    # 创建因果掩码(用于 Decoder)
    causal_mask = torch.tril(torch.ones(seq_len, seq_len)).unsqueeze(0).unsqueeze(0)
    out_masked, _ = single_attn(x, mask=causal_mask)
    print(f"带掩码的输出: {out_masked.shape}")     # (2, 10, 512)

11. 总结与面试要点

核心知识点速查

问题 答案
为什么要除以 d k \sqrt{d_k} dk ? 防止点积过大,导致 softmax 进入饱和区,梯度近似为 0
Transformer 复杂度是多少? 时间和空间复杂度均为 O ( n 2 ⋅ d ) O(n^2 \cdot d) O(n2⋅d), n n n 为序列长度
BERT 和 GPT 的本质区别? BERT 用 Encoder + 双向注意力,适合理解;GPT 用 Decoder + 单向注意力,适合生成
LayerNorm 的作用? 稳定训练过程,加速收敛,防止梯度爆炸/消失
位置编码为什么用 sin/cos? 可以用线性变换表示相对位置,且对任意长度序列有定义

学习路线建议

  1. 精读原论文《Attention is All You Need》
  2. 手写实现 Self-Attention(本文代码)
  3. 用 HuggingFace 跑一个 BERT/GPT-2 的 Fine-tuning
  4. 阅读 LLaMA 代码,理解 RoPE、RMSNorm 等现代改进