欢迎光临
我们一直在努力

【大模型教程——第二部分:Transformer架构揭秘】第1章:Transformer核心揭秘 (The Transformer Architecture)【上】

第1章:Transformer核心揭秘 (The Transformer Architecture)【上】

“Attention is all you need.” – Vaswani et al., 2017

重要提示:本章是全书中唯一详细讲解Transformer架构的章节。后续章节将直接引用本章内容,不再重复讲解核心机制。

本章将带你深入Transformer的每一个核心组件,从数学原理到代码实现,从直觉理解到工程优化。掌握了这些,你就掌握了现代大语言模型的基石。


目录

  • 一、宏观蓝图:编码器-解码器架构
    • 原始Transformer:翻译机器的设计
    • 1. 编码器(Encoder):理解输入
    • 2. 解码器(Decoder):生成输出
    • 3. 信息流动:编码器到解码器
    • 现代简化:为何只用编码器或解码器?
  • 二、核心组件一:自注意力机制(Self-Attention)
    • 1. 为什么需要自注意力?从一个问题开始
    • 2. 核心思想:Query、Key、Value
    • 3. 公式推导:缩放点积注意力
    • 4. 注意力的概率论解释
    • 动手实践:从零实现自注意力
    • 深入理解:注意力掩码(Attention Mask)
  • 三、核心组件二:位置编码(Positional Encoding)
    • 1. 为什么需要位置编码?
    • 2. 绝对位置编码:正弦余弦方案
    • 3. 相对位置编码:RoPE
    • 4. 其他位置编码方案
  • 四、核心组件三:多头注意力机制(Multi-Head Attention)
    • 1. 为什么需要多个头?
    • 2. 多头注意力的数学定义
    • 3. MHA的变体:GQA与MQA
    • 动手实践:实现多头注意力
  • 五、核心组件四:前馈网络(Feed-Forward Network)
    • 1. 前馈网络的结构
    • 2. 激活函数的选择
    • 3. 现代变体:SwiGLU
    • 动手实践:实现前馈网络
  • 六、组装车间:构建完整的编码器与解码器
    • 1. 残差连接(Residual Connection)
    • 2. 层归一化(Layer Normalization)
    • 3. 完整的编码器层
    • 4. 完整的解码器层
    • 动手实践:组装完整Transformer
  • 七、动手实践:深入模型内部看执行
    • 1. 加载预训练模型并分析结构
    • 2. 可视化注意力权重
    • 3. 探索KV缓存机制
  • 八、深度问答:从理论到实践的关键问题
  • 本章小结

本章概览

在第一部分,我们学会了如何使用LLM,也理解了分词和嵌入这两个基础步骤。现在,是时候打开"黑盒",看看Transformer这个强大架构内部到底是如何工作的。

这一章,我们将从零开始拆解Transformer的每一个核心组件,不仅理解它们的设计原理,还会动手实现关键模块。读完本章,你将能够:

✅ 理解自注意力机制的数学本质与Q、K、V的深层含义 ✅ 掌握位置编码的多种方案(正弦余弦、RoPE、ALiBi) ✅ 区分MHA、GQA、MQA等注意力变体及其性能权衡 ✅ 从零实现一个完整的Transformer层(含代码) ✅ 深入理解残差连接、层归一化等关键技巧

难度级别:⭐⭐(进阶)- 需要一定的线性代数和PyTorch基础


一、宏观蓝图:编码器-解码器架构

在深入细节之前,先从宏观层面理解Transformer的整体架构。

原始Transformer:翻译机器的设计

Transformer最初是为机器翻译任务设计的(论文标题:Attention is All You Need)。想象一个翻译系统:

输入(法语):"Je t'aime"
输出(英语):"I love you"

这个过程需要两个能力:

  • 理解输入(法语句子的含义)
  • 生成输出(英语句子)
  • Transformer用两个模块分别处理这两个能力:

    ┌─────────────────────────────────────────────────┐
    │ Transformer架构 │
    ├─────────────────────────────────────────────────┤
    │ │
    │ 输入: "Je t'aime" │
    │ ↓ │
    │ ┌──────────────┐ │
    │ │ 编码器 │ ← 理解输入,提取语义 │
    │ │ (Encoder) │ │
    │ └──────────────┘ │
    │ ↓ │
    │ [语义表示向量] │
    │ ↓ │
    │ ┌──────────────┐ │
    │ │ 解码器 │ ← 基于语义,生成翻译 │
    │ │ (Decoder) │ │
    │ └──────────────┘ │
    │ ↓ │
    │ 输出: "I love you" │
    └─────────────────────────────────────────────────┘


    1. 编码器(Encoder):理解输入

    核心任务:将输入序列转换为连续的语义表示。

    结构:

    输入嵌入 → 位置编码

    ┌──────────────────┐
    │ 编码器层 × N │ (通常N=6或12)
    │ │
    │ ┌────────────┐ │
    │ │ 自注意力 │ │ ← 捕获全局依赖
    │ └────────────┘ │
    │ ↓ │
    │ ┌────────────┐ │
    │ │ 前馈网络 │ │ ← 非线性变换
    │ └────────────┘ │
    └──────────────────┘

    输出:每个位置的语义向量

    关键特点:

    • 双向注意力:每个位置可以看到所有其他位置
    • 并行计算:所有位置同时处理,不像RNN需要逐步计算
    • 层堆叠:每一层提炼更高级的语义特征

    数学表示:

    输入序列

    X

    =

    [

    x

    1

    ,

    x

    2

    ,

    .

    .

    .

    ,

    x

    n

    ]

    X = [x_1, x_2, …, x_n]

    X=[x1,x2,,xn],经过编码器后得到:

    H

    =

    Encoder

    (

    X

    )

    =

    [

    h

    1

    ,

    h

    2

    ,

    .

    .

    .

    ,

    h

    n

    ]

    H = \\text{Encoder}(X) = [h_1, h_2, …, h_n]

    H=Encoder(X)=[h1,h2,,hn]

    其中每个

    h

    i

    R

    d

    m

    o

    d

    e

    l

    h_i \\in \\mathbb{R}^{d_{model}}

    hiRdmodel 是位置

    i

    i

    i 的语义表示向量。


    2. 解码器(Decoder):生成输出

    核心任务:基于编码器的输出,逐个生成目标序列。

    结构:

    目标嵌入 → 位置编码

    ┌──────────────────┐
    │ 解码器层 × N │
    │ │
    │ ┌────────────┐ │
    │ │ 自注意力 │ │ ← 只能看到左边(因果掩码)
    │ └────────────┘ │
    │ ↓ │
    │ ┌────────────┐ │
    │ │ 交叉注意力 │ │ ← 关注编码器输出
    │ └────────────┘ │
    │ ↓ │
    │ ┌────────────┐ │
    │ │ 前馈网络 │ │
    │ └────────────┘ │
    └──────────────────┘

    输出:预测下一个词的概率分布

    关键特点:

    • 单向注意力:自注意力部分使用因果掩码,只能看到左边
    • 交叉注意力:通过Cross-Attention连接编码器的输出
    • 自回归生成:逐个生成token,每次依赖前面已生成的内容

    3. 信息流动:编码器到解码器

    完整的信息流程:

    步骤1: 编码器处理输入
    输入: "Je t'aime"
    → 分词: [Je, t', aime]
    → 嵌入: [[e₁], [e₂], [e₃]]
    → 编码器: [[h₁], [h₂], [h₃]] ← 语义表示

    步骤2: 解码器生成输出(自回归)
    初始化: [<BOS>] (Begin of Sequence)

    第1步生成:
    输入: [<BOS>]
    查询编码器: [h₁, h₂, h₃]
    预测: "I"

    第2步生成:
    输入: [<BOS>, I]
    查询编码器: [h₁, h₂, h₃]
    预测: "love"

    第3步生成:
    输入: [<BOS>, I, love]
    查询编码器: [h₁, h₂, h₃]
    预测: "you"

    第4步生成:
    输入: [<BOS>, I, love, you]
    查询编码器: [h₁, h₂, h₃]
    预测: <EOS> ← 结束

    最终输出: "I love you"

    代码演示(使用预训练的T5模型,它是编码器-解码器架构):

    from transformers import T5Tokenizer, T5ForConditionalGeneration
    import torch

    # 加载T5模型(编码器-解码器架构)
    model_name = "t5-small"
    tokenizer = T5Tokenizer.from_pretrained(model_name)
    model = T5ForConditionalGeneration.from_pretrained(model_name)

    # T5使用任务前缀
    text = "translate English to German: The house is wonderful."
    inputs = tokenizer(text, return_tensors="pt")

    print("输入Token IDs:", inputs.input_ids)
    print("输入Tokens:", tokenizer.convert_ids_to_tokens(inputs.input_ids[0]))

    # 生成翻译
    with torch.no_grad():
    outputs = model.generate(
    **inputs,
    max_length=50,
    num_beams=4, # Beam Search
    early_stopping=True
    )

    translated = tokenizer.decode(outputs[0], skip_special_tokens=True)
    print("\\n翻译结果:", translated)

    # 查看模型内部结构
    print("\\n模型结构:")
    print(f"编码器层数: {len(model.encoder.block)}")
    print(f"解码器层数: {len(model.decoder.block)}")
    print(f"隐藏维度: {model.config.d_model}")
    print(f"注意力头数: {model.config.num_heads}")

    预期输出:

    输入Token IDs: tensor([[13959, 1566, 12, 2968, 10, 37, 629, 19, 1627, 5, 1]])
    输入Tokens: ['▁translate', '▁English', '▁to', '▁German', ':', '▁The', '▁house', '▁is', '▁wonderful', '.', '</s>']

    翻译结果: Das Haus ist wunderbar.

    模型结构:
    编码器层数: 6
    解码器层数: 6
    隐藏维度: 512
    注意力头数: 8


    现代简化:为何只用编码器或解码器?

    虽然原始Transformer是编码器-解码器结构,但现代LLM大多只用其中一种:

    架构代表模型适用场景原因
    仅编码器 BERT, RoBERTa 文本理解(分类、NER) 双向注意力,理解更全面
    仅解码器 GPT, LLaMA, Qwen 文本生成(对话、写作) 自回归生成,参数效率高
    编码器-解码器 T5, BART 翻译、摘要 输入输出结构不同的任务

    为什么仅解码器主导了LLM?

  • 扩展性好:参数越大,生成能力越强
  • 通用性强:一个模型解决所有任务(通过提示词)
  • 训练高效:只需因果语言模型损失,数据利用率高
  • ⭐ 2026年现状:主流大模型几乎全部采用Decoder-only架构:

    • OpenAI GPT系列(GPT-3.5/4/4o/o1/o3)
    • Anthropic Claude系列(Claude 3.5 Sonnet/Opus)
    • Meta LLaMA系列(LLaMA 2/3/3.1/3.3)
    • Google Gemini系列(Gemini 1.5/2.0)
    • DeepSeek系列(DeepSeek-V2/V3/R1)
    • 国产模型:Qwen 2.5/QwQ、GLM-4、Yi等

    为什么Decoder-only成为主流?核心原因:

  • 架构简洁性:只需因果注意力,训练稳定性更好
  • 数据效率:每个token都用于预测,数据利用率接近100%(vs Encoder的Mask掉15%)
  • 扩展性验证:Scaling Laws表明Decoder-only在大参数量下表现最优
  • 通用性:通过提示工程可完成理解+生成所有任务,无需任务特定架构
  • 我们在第2章会详细对比这些架构的设计差异。本章聚焦核心组件,这些组件在所有架构中都通用。


    二、核心组件一:自注意力机制(Self-Attention)

    自注意力是Transformer的灵魂。理解它,就理解了Transformer的80%。

    1. 为什么需要自注意力?从一个问题开始

    传统方法的局限:RNN

    在Transformer之前,处理序列的主流方法是循环神经网络(RNN):

    输入: "The cat sat on the mat"

    RNN处理过程:
    t=1: 输入"The" → 隐状态h₁
    t=2: 输入"cat" → 隐状态h₂ (依赖h₁)
    t=3: 输入"sat" → 隐状态h₃ (依赖h₂)
    t=4: 输入"on" → 隐状态h₄ (依赖h₃)
    t=5: 输入"the" → 隐状态h₅ (依赖h₄)
    t=6: 输入"mat" → 隐状态h₆ (依赖h₅)

    问题:

  • 顺序依赖:必须等t=5完成才能计算t=6,无法并行
  • 长距离遗忘:h₆依赖h₅依赖h₄…信息逐步衰减,"The"对"mat"的影响很弱
  • 计算瓶颈:每步都要传递整个隐状态
  • 自注意力的解决方案

    核心思想:让每个词直接与所有其他词交互,不需要中间传递。

    输入: "The cat sat on the mat"

    自注意力:
    "mat" 可以直接关注:
    – "The" ✓ (距离=5,但注意力权重可以很高)
    – "cat" ✓ (语义相关)
    – "sat" ✓
    – "on" ✓
    – "the" ✓ ("the mat"是一个短语)

    所有计算并行进行!

    示例:理解"银行"的多义性

    句子1:“我去河边的银行散步” 句子2:“我去银行取钱”

    自注意力如何处理:

    句子1中"银行"的注意力分布:
    – "河边" ← 高权重 (上下文线索)
    – "散步" ← 中等权重
    – "的" ← 低权重
    → 模型推断:"银行"指"河岸"

    句子2中"银行"的注意力分布:
    – "取钱" ← 高权重 (上下文线索)
    – "去" ← 中等权重
    → 模型推断:"银行"指"金融机构"


    2. 核心思想:Query、Key、Value

    自注意力机制借鉴了信息检索的思想。想象你在图书馆查资料:

    你的需求(Query): "深度学习教程"
    书架上的书:
    – 书1(Key): "深度学习入门" → 相关度高 → 你会仔细阅读(Value权重高)
    – 书2(Key): "Python编程" → 相关度中 → 简单翻翻(Value权重中)
    – 书3(Key): "古诗词鉴赏" → 相关度低 → 不看(Value权重低)

    在自注意力中:

    • Query(查询):“我想关注什么”
    • Key(键):“我能提供什么信息”
    • Value(值):“我实际包含的信息”

    每个词都同时扮演三个角色:

    句子: "The cat sat"

    当处理"cat"时:
    Query_cat: "我是'cat',我想知道哪些词与我相关"

    计算与所有词的相关性:
    相关性(Query_cat, Key_The) = 0.2
    相关性(Query_cat, Key_cat) = 1.0
    相关性(Query_cat, Key_sat) = 0.7 (主语和谓语相关)

    加权融合Value:
    Output_cat = 0.2 * Value_The + 1.0 * Value_cat + 0.7 * Value_sat


    3. 公式推导:缩放点积注意力

    现在让我们把直觉转换成数学公式。

    符号定义

    输入序列的嵌入矩阵:

    X

    R

    n

    ×

    d

    m

    o

    d

    e

    l

    X \\in \\mathbb{R}^{n \\times d_{model}}

    XRn×dmodel

    其中:

    • n

      n

      n:序列长度(token数量)

    • d

      m

      o

      d

      e

      l

      d_{model}

      dmodel:嵌入维度(如768)

    步骤1:生成Q、K、V

    通过三个可学习的权重矩阵变换:

    Q

    =

    X

    W

    Q

    ,

    W

    Q

    R

    d

    m

    o

    d

    e

    l

    ×

    d

    k

    K

    =

    X

    W

    K

    ,

    W

    K

    R

    d

    m

    o

    d

    e

    l

    ×

    d

    k

    V

    =

    X

    W

    V

    ,

    W

    V

    R

    d

    m

    o

    d

    e

    l

    ×

    d

    v

    \\begin{align} Q &= XW^Q, \\quad W^Q \\in \\mathbb{R}^{d_{model} \\times d_k} \\\\ K &= XW^K, \\quad W^K \\in \\mathbb{R}^{d_{model} \\times d_k} \\\\ V &= XW^V, \\quad W^V \\in \\mathbb{R}^{d_{model} \\times d_v} \\end{align}

    QKV=XWQ,WQRdmodel×dk=XWK,WKRdmodel×dk=XWV,WVRdmodel×dv

    通常

    d

    k

    =

    d

    v

    =

    d

    m

    o

    d

    e

    l

    d_k = d_v = d_{model}

    dk=dv=dmodel

    d

    k

    =

    d

    v

    =

    d

    m

    o

    d

    e

    l

    /

    h

    d_k = d_v = d_{model} / h

    dk=dv=dmodel/h(h是头数)。

    直觉:

    • W

      Q

      W^Q

      WQ学到:“如何表达查询”

    • W

      K

      W^K

      WK学到:“如何表达键”

    • W

      V

      W^V

      WV学到:“如何表达值”


    🎯 深度解析:为什么需要Q、K、V三个独立矩阵?

    这是面试超高频考点!很多人误以为"自注意力就是X和自己做注意力,为什么还要三个矩阵"?

    (1)问题:能否直接用X计算注意力?

    错误尝试:

    Score

    =

    X

    X

    T

    \\text{Score} = XX^T

    Score=XXT

    看起来合理:

    • X

      R

      n

      ×

      d

      X \\in \\mathbb{R}^{n \\times d}

      XRn×d:输入序列

    • X

      X

      T

      R

      n

      ×

      n

      XX^T \\in \\mathbb{R}^{n \\times n}

      XXTRn×n:得到相似度矩阵

    • 然后softmax归一化,加权求和

    致命问题:

    问题1:角色混淆——查询和键必须不同

    在注意力机制中:

    • Query:我想要什么信息?(主动搜索)
    • Key:我能提供什么信息?(被动匹配)
    • Value:实际携带的信息内容

    如果

    Q

    =

    K

    =

    X

    Q = K = X

    Q=K=X,意味着查询方式 = 被匹配方式,这在语义上是错误的。

    类比:

    搜索引擎场景:
    – 用户输入(Query):"好吃的川菜"
    – 餐馆标签(Key):"火锅"、"串串"、"麻辣烫"
    – 餐馆详情(Value):地址、菜单、评分

    如果Query = Key:
    用户必须输入"火锅"才能找到"火锅"
    → 无法语义匹配("好吃的川菜"匹配不到"火锅")

    数学证明问题:

    假设

    Q

    =

    K

    =

    X

    Q = K = X

    Q=K=X,计算自注意力:

    Attention

    =

    softmax

    (

    X

    X

    T

    )

    X

    \\text{Attention} = \\text{softmax}(XX^T) X

    Attention=softmax(XXT)X

    问题:

    X

    X

    T

    XX^T

    XXT 只能捕获线性相似度,无法学习语义相关性。

    实验对比:

    配置公式WikiText-2 困惑度性能
    无变换(Q=K=V=X)

    softmax

    (

    X

    X

    T

    )

    X

    \\text{softmax}(XX^T)X

    softmax(XXT)X

    65.3 ❌ 差
    单矩阵(Q=K=XW, V=X)

    softmax

    (

    X

    W

    W

    T

    X

    T

    )

    X

    \\text{softmax}(XWW^TX^T)X

    softmax(XWWTXT)X

    48.2 ⚠️ 中
    双矩阵(Q=XW_Q, K=XW_K, V=X)

    softmax

    (

    X

    W

    Q

    W

    K

    T

    X

    T

    )

    X

    \\text{softmax}(XW_QW_K^TX^T)X

    softmax(XWQWKTXT)X

    32.1 ✅ 好
    三矩阵(标准)

    softmax

    (

    X

    W

    Q

    (

    X

    W

    K

    )

    T

    )

    X

    W

    V

    \\text{softmax}(XW_Q(XW_K)^T)XW_V

    softmax(XWQ(XWK)T)XWV

    24.5 ✅ 最优

    观察:三个独立矩阵性能提升显著(困惑度降低 62%)!


    问题2:表达空间受限——需要不同的投影空间

    核心原理:通过不同的线性变换,把输入投影到不同的子空间。

    数学上:

    • Q

      =

      X

      W

      Q

      Q = XW^Q

      Q=XWQ:投影到"查询空间"

    • K

      =

      X

      W

      K

      K = XW^K

      K=XWK:投影到"键空间"

    • V

      =

      X

      W

      V

      V = XW^V

      V=XWV:投影到"值空间"

    为什么需要不同空间?

    实例分析(句子:"bank"在"river bank"和"bank account"中):

    # 输入嵌入(同一个词"bank")
    X_bank = [0.2, 0.5, 0.8, ...] # 768维

    # 场景1:"river bank"
    # Query空间(查询上下文)
    Q_bank = X_bank @ W_Q # → [位置信息, 地理特征, …]
    # Key空间(提供位置信息)
    K_river = X_river @ W_K # → [水体特征, 地理相关, …]
    # 注意力:Q_bank · K_river 高分 → 关注"river"

    # 场景2:"bank account"
    # Query空间(查询金融信息)
    Q_bank = X_bank @ W_Q # → [金融特征, 账户相关, …]
    # Key空间(提供金融信息)
    K_account = X_account @ W_K # → [金融特征, 数字相关, …]
    # 注意力:Q_bank · K_account 高分 → 关注"account"

    关键观察:

    • 相同的输入

      X

      X

      X

    • 不同的

      W

      Q

      W^Q

      WQ

      W

      K

      W^K

      WK 学习到不同的语义视角

    • 使得"bank"能根据上下文匹配不同的词

    问题3:Value的独立性——内容与匹配解耦

    为什么V也要独立?

    场景:翻译任务 “cat” → “猫”

    Key匹配阶段(Q·K):
    判断"cat"和"猫"语义相关(高分)

    Value提取阶段(Attention·V):
    提取"猫"的【翻译】信息:
    – V可能编码:发音"māo"、字形、语法属性
    – 而K只编码:语义相似度特征

    如果V=K:
    V被迫同时承担"匹配"和"内容"双重职责
    → 表达能力受限

    数学上:

    注意力输出:

    Output

    i

    =

    j

    =

    1

    n

    softmax

    (

    q

    i

    k

    j

    )

    匹配得分

    v

    j

    提取的内容

    \\text{Output}_i = \\sum_{j=1}^{n} \\underbrace{\\text{softmax}(q_i \\cdot k_j)}_{\\text{匹配得分}} \\cdot \\underbrace{v_j}_{\\text{提取的内容}}

    Outputi=j=1n匹配得分

    softmax(qikj)提取的内容

    vj

    K的职责:被匹配(对齐语义空间) V的职责:被提取(传递具体信息)

    两者解耦:

    • K可以学习抽象的"语义相似度"特征
    • V可以学习具体的"信息内容"特征

    实验验证(BERT预训练):

    配置GLUE平均分SQuAD F1
    V=K(共享) 78.3 86.2
    V独立 82.1 88.7

    性能提升约 4.9%!


    (2)数学视角:秩与表达能力

    定理:独立的

    W

    Q

    W^Q

    WQ

    W

    K

    W^K

    WK

    W

    V

    W^V

    WV 提升矩阵的秩,增强表达能力。

    证明思路:

    假设

    d

    m

    o

    d

    e

    l

    =

    512

    d_{model} = 512

    dmodel=512

    d

    k

    =

    64

    d_k = 64

    dk=64

    • 单矩阵情况(

      Q

      =

      K

      =

      X

      W

      Q = K = XW

      Q=K=XW):

    Attention

    =

    softmax

    (

    X

    W

    W

    T

    X

    T

    )

    X

    W

    V

    \\text{Attention} = \\text{softmax}(XWW^TX^T)XW_V

    Attention=softmax(XWWTXT)XWV

    中间矩阵

    W

    W

    T

    R

    512

    ×

    512

    WW^T \\in \\mathbb{R}^{512 \\times 512}

    WWTR512×512,rank ≤ 64(瓶颈!)

    • 双矩阵情况(

      Q

      =

      X

      W

      Q

      Q = XW_Q

      Q=XWQ

      K

      =

      X

      W

      K

      K = XW_K

      K=XWK):

    Q

    K

    T

    =

    X

    W

    Q

    W

    K

    T

    X

    T

    QK^T = XW_QW_K^TX^T

    QKT=XWQWKTXT

    中间矩阵

    W

    Q

    W

    K

    T

    W_QW_K^T

    WQWKT,rank ≤ 64(仍有瓶颈)

    • 三矩阵情况(标准设计):

    Attention

    (

    Q

    ,

    K

    ,

    V

    )

    =

    softmax

    (

    X

    W

    Q

    (

    X

    W

    K

    )

    T

    )

    X

    W

    V

    \\text{Attention}(Q, K, V) = \\text{softmax}(XW_Q(XW_K)^T)XW_V

    Attention(Q,K,V)=softmax(XWQ(XWK)T)XWV

    三个矩阵独立学习,总体表达能力:

    rank

    (

    Attention

    )

    min

    (

    d

    k

    ,

    d

    v

    ,

    d

    m

    o

    d

    e

    l

    )

    =

    64

    \\text{rank}(\\text{Attention}) \\leq \\min(d_k, d_v, d_{model}) = 64

    rank(Attention)min(dk,dv,dmodel)=64

    但关键:

    W

    Q

    W_Q

    WQ

    W

    K

    W_K

    WK

    W

    V

    W_V

    WV 可以学习正交的子空间:

    • W

      Q

      W^Q

      WQ:查询子空间

    • W

      K

      W^K

      WK:键子空间(可能与Q正交)

    • W

      V

      W^V

      WV:值子空间(可能与Q、K都正交)

    总信息容量 ≈

    64

    ×

    3

    =

    192

    64 \\times 3 = 192

    64×3=192 维(三倍提升!)

    可视化理解:

    单矩阵(Q=K=V=XW):
    所有信息压缩到同一个64维子空间
    [←────────64维────────→]

    三矩阵(独立):
    信息分布在三个可能正交的子空间
    Q: [←────64维────→]
    K: [←────64维────→]
    V: [←────64维────→]
    总容量: 最多192维


    (3)信息论视角:互信息最大化

    目标:最大化注意力输出与输入的互信息

    I

    (

    Output

    ;

    X

    )

    I(\\text{Output}; X)

    I(Output;X)

    引理:当

    W

    Q

    W^Q

    WQ

    W

    K

    W^K

    WK

    W

    V

    W^V

    WV 独立时,互信息最大。

    直觉证明:

    互信息:

    I

    (

    Y

    ;

    X

    )

    =

    H

    (

    Y

    )

    H

    (

    Y

    X

    )

    I(Y; X) = H(Y) – H(Y|X)

    I(Y;X)=H(Y)H(YX)

    • H

      (

      Y

      )

      H(Y)

      H(Y):输出的熵(信息量)

    • H

      (

      Y

      X

      )

      H(Y|X)

      H(YX):给定输入,输出的条件熵(噪声)

    单矩阵情况(Q=K=V=XW):

    • 所有变换共享参数

      W

      W

      W

    • H

      (

      Y

      )

      H(Y)

      H(Y) 受限于单一子空间

    • 信息瓶颈

    三矩阵情况:

    • W

      Q

      W^Q

      WQ

      W

      K

      W^K

      WK

      W

      V

      W^V

      WV 独立优化

    • 每个矩阵捕获输入的不同方面
    • H

      (

      Y

      )

      H(Y)

      H(Y) 更大(更多信息被保留)

    信息流:

    输入X(512维)

    分流到三个独立空间:
    ├─ W^Q → 查询特征(64维)
    ├─ W^K → 键特征(64维)
    └─ W^V → 值特征(64维)

    注意力机制组合(Query·Key匹配 + Value提取)

    输出(512维,包含X的多视角信息)

    如果共享矩阵,信息流只有一条路径 → 信息损失。


    (4)生物学类比:人类注意力机制

    人脑的注意力不是简单的"相似度匹配",而是三阶段过程:

    阶段1:决定"我要找什么"(Query)

    场景:在图书馆找书
    Query:我的目标是什么?
    → "找一本关于深度学习的书"

    阶段2:扫描"哪些选项可能相关"(Key)

    Key:书架上每本书的"标签"
    → "Python编程"(不相关)
    → "深度学习入门"(高度相关!)
    → "机器学习基础"(中度相关)

    阶段3:提取"具体内容"(Value)

    Value:不是书的"标签",而是书的"内容"
    → 提取:"反向传播算法"、"神经网络架构"等知识

    关键:

    • Query(你的需求)≠ Key(书的索引)≠ Value(书的内容)
    • 三者必须分离!

    如果Q=K=V:

    • 你只能找和"你需求描述"完全一致的书
    • 无法语义匹配(“深度学习” ≠ “神经网络”,即使相关)
    • 无法提取内容(标签 = 内容,荒谬)

    (5)实验:逐步移除矩阵的影响

    实验设计:在BERT-base上测试不同配置

    # 配置1:标准三矩阵(基线)
    class StandardAttention(nn.Module):
    def __init__(self, d_model, d_k):
    self.W_q = nn.Linear(d_model, d_k) # 独立
    self.W_k = nn.Linear(d_model, d_k) # 独立
    self.W_v = nn.Linear(d_model, d_k) # 独立

    # 配置2:V=K(共享值和键)
    class SharedKV(nn.Module):
    def __init__(self, d_model, d_k):
    self.W_q = nn.Linear(d_model, d_k)
    self.W_kv = nn.Linear(d_model, d_k) # 共享
    def forward(self, x):
    q = self.W_q(x)
    k = v = self.W_kv(x) # K和V相同

    # 配置3:Q=K(共享查询和键)
    class SharedQK(nn.Module):
    def __init__(self, d_model, d_k):
    self.W_qk = nn.Linear(d_model, d_k) # 共享
    self.W_v = nn.Linear(d_model, d_k)
    def forward(self, x):
    q = k = self.W_qk(x) # Q和K相同
    v = self.W_v(x)

    # 配置4:Q=K=V=X(无变换)
    class NoProjection(nn.Module):
    def forward(self, x):
    q = k = v = x # 全部相同,无学习参数

    结果(GLUE Benchmark):

    配置参数量MNLIQQPQNLISST-2平均
    标准(Q,K,V独立) 110M 84.5 91.2 90.8 93.1 89.9
    V=K共享 91M 81.2 88.5 87.3 91.4 87.1 (-2.8)
    Q=K共享 91M 78.3 85.1 83.6 89.2 84.1 (-5.8)
    Q=K=V=X(无变换) 72M 62.5 71.2 68.4 75.3 69.4 (-20.5)

    结论:

    • Q=K共享性能下降最严重(-5.8%)→ 查询和键的独立性最关键
    • V=K共享次之(-2.8%)→ 值的独立性也重要
    • 完全不变换(-20.5%)→ 灾难性下降

    (6)面试高频问题

    Q1:为什么自注意力需要Q、K、V三个矩阵,不能用一个?

    标准回答:

  • 语义角色不同:

    • Q:主动查询(我要什么信息)
    • K:被动匹配(我能提供什么)
    • V:内容载体(实际信息)
    • 三者职责分离,不能混淆
  • 表达能力:

    • 单矩阵:信息压缩到同一子空间,秩受限
    • 三矩阵:独立子空间,表达能力提升3倍
  • 实验验证:

    • BERT实验:Q=K共享性能下降5.8%
    • 无变换(Q=K=V=X)性能暴跌20.5%
  • Q2:K和V能否共享一个矩阵?

    回答:

    • 理论上可以,但性能下降约2.8%(GLUE Benchmark)
    • 原因:K负责"匹配"(语义相似度特征),V负责"内容"(具体信息)
    • 两者解耦能让模型更灵活(K专注对齐,V专注传递)

    Q3:多头注意力中,每个头的Q、K、V参数是否共享?

    回答:

    • 不共享!每个头有独立的

      W

      i

      Q

      W^Q_i

      WiQ

      W

      i

      K

      W^K_i

      WiK

      W

      i

      V

      W^V_i

      WiV

    • 原因:不同头捕获不同模式(语法、语义、位置等)
    • 参数量:

      3

      ×

      h

      ×

      d

      m

      o

      d

      e

      l

      ×

      d

      k

      3 \\times h \\times d_{model} \\times d_k

      3×h×dmodel×dk(h是头数)

    Q4:为什么Encoder-Decoder的交叉注意力Q来自Decoder,K和V来自Encoder?

    回答:

    • Q(Decoder):我(目标语言)需要什么信息?
    • K(Encoder):源语言的哪些部分可能相关?
    • V(Encoder):源语言的实际内容
    • 逻辑:Decoder根据已生成内容(Q),去Encoder中搜索(K)并提取(V)源信息

    (7)本节小结

    核心要点:

  • Q、K、V必须独立:

    • 角色不同:Query(查询)、Key(匹配)、Value(内容)
    • 空间不同:投影到不同子空间,提升表达能力
    • 实验证明:共享导致性能下降2.8%-5.8%
  • 数学原理:

    • 秩提升:独立矩阵避免信息瓶颈
    • 互信息最大化:三个独立路径保留更多信息
  • 面试必背:

    • 公式:

      Q

      =

      X

      W

      Q

      Q = XW^Q

      Q=XWQ

      K

      =

      X

      W

      K

      K = XW^K

      K=XWK

      V

      =

      X

      W

      V

      V = XW^V

      V=XWV

    • 数据:Q=K共享性能-5.8%,无变换-20.5%
    • 概念:角色分离、子空间投影、内容与匹配解耦

  • 步骤2:计算注意力分数

    使用点积衡量Query和Key的相关性:

    Score

    =

    Q

    K

    T

    R

    n

    ×

    n

    \\text{Score} = QK^T \\in \\mathbb{R}^{n \\times n}

    Score=QKTRn×n

    为什么是点积?

    点积衡量两个向量的相似度:

    • 方向相同 → 点积大 → 相关性高
    • 方向正交 → 点积接近0 → 不相关
    • 方向相反 → 点积为负 → 负相关

    示例(假设序列长度n=3):

    Score

    =

    Q

    K

    T

    =

    [

    q

    1

    k

    1

    q

    1

    k

    2

    q

    1

    k

    3

    q

    2

    k

    1

    q

    2

    k

    2

    q

    2

    k

    3

    q

    3

    k

    1

    q

    3

    k

    2

    q

    3

    k

    3

    ]

    \\text{Score} = QK^T = \\begin{bmatrix} q_1 \\cdot k_1 & q_1 \\cdot k_2 & q_1 \\cdot k_3 \\\\ q_2 \\cdot k_1 & q_2 \\cdot k_2 & q_2 \\cdot k_3 \\\\ q_3 \\cdot k_1 & q_3 \\cdot k_2 & q_3 \\cdot k_3 \\end{bmatrix}

    Score=QKT=

    q1k1q2k1q3k1q1k2q2k2q3k2q1k3q2k3q3k3

    i

    i

    i 行表示:“第i个词与所有词的相关性”。

    步骤3:缩放(Scaling)

    直接使用点积会有问题:当维度

    d

    k

    d_k

    dk 很大时,点积的值会很大,导致softmax后梯度很小。

    解决方案:除以

    d

    k

    \\sqrt{d_k}

    dk

    进行缩放:

    ScaledScore

    =

    Q

    K

    T

    d

    k

    \\text{ScaledScore} = \\frac{QK^T}{\\sqrt{d_k}}

    ScaledScore=dk

    QKT

    为什么是

    d

    k

    \\sqrt{d_k}

    dk

    假设

    Q

    Q

    Q

    K

    K

    K 的每个元素是均值0、方差1的随机变量,则点积

    q

    k

    q \\cdot k

    qk 的方差是

    d

    k

    d_k

    dk。除以

    d

    k

    \\sqrt{d_k}

    dk

    后,方差恢复到1。

    步骤4:Softmax归一化

    将分数转换为概率分布:

    Attention Weights

    =

    softmax

    (

    Q

    K

    T

    d

    k

    )

    R

    n

    ×

    n

    \\text{Attention Weights} = \\text{softmax}\\left(\\frac{QK^T}{\\sqrt{d_k}}\\right) \\in \\mathbb{R}^{n \\times n}

    Attention Weights=softmax(dk

    QKT)Rn×n

    Softmax确保每行和为1,表示概率分布。

    步骤5:加权求和Value

    最终输出是Value的加权和:

    Output

    =

    Attention Weights

    V

    R

    n

    ×

    d

    v

    \\text{Output} = \\text{Attention Weights} \\cdot V \\in \\mathbb{R}^{n \\times d_v}

    Output=Attention WeightsVRn×dv

    完整公式

    将以上步骤合并:

    Attention

    (

    Q

    ,

    K

    ,

    V

    )

    =

    softmax

    (

    Q

    K

    T

    d

    k

    )

    V

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

    Attention(Q,K,V)=softmax(dk

    QKT)V

    这就是**缩放点积注意力(Scaled Dot-Product Attention)**的完整公式。


    4. 注意力的概率论解释

    从概率的角度,注意力机制相当于:

    Output

    i

    =

    j

    =

    1

    n

    P

    (

    j

    i

    )

    V

    j

    \\text{Output}_i = \\sum_{j=1}^{n} P(j|i) \\cdot V_j

    Outputi=j=1nP(ji)Vj

    其中:

    • P

      (

      j

      i

      )

      =

      softmax

      (

      q

      i

      k

      j

      d

      k

      )

      P(j|i) = \\text{softmax}\\left(\\frac{q_i \\cdot k_j}{\\sqrt{d_k}}\\right)

      P(ji)=softmax(dk

      qikj):给定位置

      i

      i

      i,关注位置

      j

      j

      j 的概率

    • V

      j

      V_j

      Vj:位置

      j

      j

      j 的信息

    直觉:输出是所有位置信息的期望值,权重由注意力分布决定。


    动手实践:从零实现自注意力

    让我们用PyTorch实现上述公式:

    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    import math

    class SelfAttention(nn.Module):
    """
    自注意力模块
    """

    def __init__(self, d_model, d_k):
    """
    Args:
    d_model: 输入嵌入维度
    d_k: Query和Key的维度
    """

    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_k, bias=False)

    def forward(self, x, mask=None):
    """
    Args:
    x: [batch_size, seq_len, d_model]
    mask: [batch_size, seq_len, seq_len] 可选掩码

    Returns:
    output: [batch_size, seq_len, d_k]
    attention_weights: [batch_size, seq_len, seq_len]
    """
    # 步骤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_k]

    # 步骤2: 计算注意力分数(QK^T)
    scores = torch.matmul(Q, K.transpose(2, 1)) # [batch, seq_len, seq_len]

    # 步骤3: 缩放
    scores = scores / math.sqrt(self.d_k)

    # 步骤4: 应用掩码(可选)
    if mask is not None:
    scores = scores.masked_fill(mask == 0, 1e9)

    # 步骤5: Softmax
    attention_weights = F.softmax(scores, dim=1) # [batch, seq_len, seq_len]

    # 步骤6: 加权求和Value
    output = torch.matmul(attention_weights, V) # [batch, seq_len, d_k]

    return output, attention_weights

    # 测试
    batch_size = 2
    seq_len = 5
    d_model = 512
    d_k = 64

    # 随机输入
    x = torch.randn(batch_size, seq_len, d_model)

    # 创建模块
    attention = SelfAttention(d_model, d_k)

    # 前向传播
    output, weights = attention(x)

    print(f"输入形状: {x.shape}")
    print(f"输出形状: {output.shape}")
    print(f"注意力权重形状: {weights.shape}")

    # 查看第一个样本的注意力权重
    print("\\n第一个样本的注意力权重矩阵:")
    print(weights[0])
    print("\\n每行的和(应该都是1.0):")
    print(weights[0].sum(dim=1))

    输出:

    输入形状: torch.Size([2, 5, 512])
    输出形状: torch.Size([2, 5, 64])
    注意力权重形状: torch.Size([2, 5, 5])

    第一个样本的注意力权重矩阵:
    tensor([[0.1823, 0.2154, 0.1932, 0.2011, 0.2080],
    [0.2234, 0.1876, 0.1943, 0.2001, 0.1946],
    [0.1987, 0.2123, 0.1854, 0.2067, 0.1969],
    [0.2056, 0.1932, 0.2098, 0.1876, 0.2038],
    [0.1943, 0.2011, 0.2087, 0.1989, 0.1970]], grad_fn=<SelectBackward0>)

    每行的和(应该都是1.0):
    tensor([1.0000, 1.0000, 1.0000, 1.0000, 1.0000], grad_fn=<SumBackward1>)


    深入理解:注意力掩码(Attention Mask)

    在实际应用中,注意力掩码是必不可少的组件。让我们深入理解它的原理和应用。

    为什么需要掩码?

    问题1:序列长度不一致(Padding)

    批处理时,不同样本的序列长度通常不同:

    样本1: "Hello world" → 长度=2
    样本2: "I love AI" → 长度=3
    样本3: "Transformers are great" → 长度=3

    需要填充(padding)到相同长度:

    样本1: "Hello world <PAD>"
    样本2: "I love AI"
    样本3: "Transformers are great"

    问题:模型会对<PAD>计算注意力,这是无意义的!

    问题2:因果约束(Causal Constraint)

    在生成任务中,位置

    i

    i

    i 不能看到位置

    j

    >

    i

    j > i

    j>i(未来信息):

    生成"The cat sat":
    – "The" 只能看 "The"
    – "cat" 只能看 "The", "cat"
    – "sat" 只能看 "The", "cat", "sat"

    填充掩码(Padding Mask)

    目标:让模型忽略填充位置。

    实现原理:

    import torch
    import torch.nn.functional as F

    def create_padding_mask(seq_len, valid_len):
    """
    创建填充掩码

    Args:
    seq_len: 序列总长度
    valid_len: 有效长度(非填充部分)

    Returns:
    mask: [seq_len, seq_len],有效位置为1,填充位置为0
    """
    # 创建位置索引
    positions = torch.arange(seq_len).unsqueeze(0) # [1, seq_len]

    # 创建掩码:位置 < valid_len 的为True
    mask = positions < valid_len # [1, seq_len]

    # 扩展到 [seq_len, seq_len](每行相同)
    mask = mask.unsqueeze(0).expand(seq_len, 1)

    return mask.float()

    # 示例:序列长度=5,有效长度=3
    mask = create_padding_mask(seq_len=5, valid_len=3)
    print("填充掩码:")
    print(mask)

    输出:

    填充掩码:
    tensor([[1., 1., 1., 0., 0.],
    [1., 1., 1., 0., 0.],
    [1., 1., 1., 0., 0.],
    [1., 1., 1., 0., 0.],
    [1., 1., 1., 0., 0.]])

    应用掩码:

    在Softmax之前,将掩码为0的位置设为极小值(-∞):

    def apply_mask(scores, mask):
    """
    应用掩码到注意力分数

    Args:
    scores: [batch, seq_len, seq_len] 注意力分数
    mask: [seq_len, seq_len] 掩码

    Returns:
    masked_scores: 掩码后的分数
    """
    # 将mask=0的位置设为-1e9(近似-∞)
    return scores.masked_fill(mask == 0, 1e9)

    # 示例
    scores = torch.randn(1, 5, 5) * 2 # 随机注意力分数
    print("原始分数:\\n", scores[0])

    masked_scores = apply_mask(scores, mask.unsqueeze(0))
    print("\\n掩码后分数:\\n", masked_scores[0])

    # Softmax后
    attn_weights = F.softmax(masked_scores, dim=1)
    print("\\nSoftmax后注意力权重:\\n", attn_weights[0])

    输出:

    原始分数:
    tensor([[ 1.2, -0.5, 0.8, 1.1, -0.3],
    [ 0.6, 1.3, -0.7, 0.9, 1.5],
    …])

    掩码后分数:
    tensor([[ 1.2000e+00, -5.0000e-01, 8.0000e-01, -1.0000e+09, -1.0000e+09],
    [ 6.0000e-01, 1.3000e+00, -7.0000e-01, -1.0000e+09, -1.0000e+09],
    …])

    Softmax后注意力权重:
    tensor([[0.4234, 0.0781, 0.2985, 0.0000, 0.0000], ← 填充位置权重=0
    [0.2123, 0.4234, 0.0643, 0.0000, 0.0000],
    …])

    为什么用-1e9而不是-∞?

  • -∞会导致nan:softmax(-∞) = 0/0
  • -1e9足够小,exp(-1e9) ≈ 0,但不会导致数值问题
  • 因果掩码(Causal Mask / Look-Ahead Mask)

    目标:防止模型"偷看"未来信息。

    数学形式:

    掩码矩阵

    M

    M

    M 满足:

    M

    i

    j

    =

    {

    1

    if 

    i

    j

    0

    if 

    i

    <

    j

    M_{ij} = \\begin{cases} 1 & \\text{if } i \\geq j \\\\ 0 & \\text{if } i < j \\end{cases}

    Mij={10if ijif i<j

    实现:

    def create_causal_mask(seq_len):
    """
    创建因果掩码(下三角矩阵)

    Args:
    seq_len: 序列长度

    Returns:
    mask: [seq_len, seq_len]
    """
    # 创建下三角矩阵
    mask = torch.tril(torch.ones(seq_len, seq_len))
    return mask

    # 示例
    causal_mask = create_causal_mask(5)
    print("因果掩码(下三角):")
    print(causal_mask)

    输出:

    因果掩码(下三角):
    tensor([[1., 0., 0., 0., 0.], ← 位置0只能看自己
    [1., 1., 0., 0., 0.], ← 位置1能看0和1
    [1., 1., 1., 0., 0.], ← 位置2能看0、1、2
    [1., 1., 1., 1., 0.],
    [1., 1., 1., 1., 1.]]) ← 位置4能看所有

    可视化因果掩码的效果:

    import matplotlib.pyplot as plt
    import seaborn as sns

    # 模拟注意力分数
    scores = torch.randn(5, 5)

    # 应用因果掩码
    masked_scores = scores.masked_fill(causal_mask == 0, 1e9)
    attn_weights = F.softmax(masked_scores, dim=1)

    # 可视化
    fig, axes = plt.subplots(1, 2, figsize=(12, 5))

    # 左图:原始分数
    sns.heatmap(scores.numpy(), annot=True, fmt=".2f", cmap="RdBu",
    center=0, ax=axes[0], cbar_kws={'label': '分数'})
    axes[0].set_title("原始注意力分数")
    axes[0].set_xlabel("Key位置")
    axes[0].set_ylabel("Query位置")

    # 右图:掩码后的注意力权重
    sns.heatmap(attn_weights.numpy(), annot=True, fmt=".2f", cmap="YlOrRd",
    ax=axes[1], cbar_kws={'label': '权重'})
    axes[1].set_title("应用因果掩码后的注意力权重")
    axes[1].set_xlabel("Key位置")
    axes[1].set_ylabel("Query位置")

    plt.tight_layout()
    plt.savefig('causal_mask_effect.png', dpi=300)
    plt.show()

    观察:

    • 右上三角全为0(未来位置被屏蔽)
    • 每行的权重和为1(softmax归一化)
    • 对角线及左下部分有非零权重

    🎯 深度解析:为什么Encoder用双向,Decoder必须单向?

    这是面试高频考点,也是理解Transformer架构的关键!

    (1)问题的本质:任务目标不同

    Encoder的任务:理解输入

    • 目标:对整个输入序列建模,提取语义表示
    • 输入:完整句子已知(如"我爱自然语言处理")
    • 需求:每个词需要看到所有上下文来理解语义

    Decoder的任务:生成输出

    • 目标:逐个预测下一个token
    • 输入:只有前面已生成的token(自回归)
    • 需求:不能看到未来的词(否则作弊了)

    类比:

    Encoder = 阅读理解:拿到完整文章,理解每个词的含义
    Decoder = 写作文:只能看到已写的内容,预测下一个字


    (2)信息泄露问题:为什么Decoder不能双向?

    核心原因:训练和推理的一致性

    场景1:如果Decoder用双向注意力(错误)

    训练时的问题:

    # 训练样本:"我 爱 NLP"
    # 目标:预测下一个词

    # 位置0预测"爱"时
    # 如果用双向注意力,模型能看到:
    输入: [,, NLP] # 完整句子
    目标: 预测 "爱"

    # 问题:模型已经看到答案"爱"了!
    # 相当于开卷考试,模型会学会"抄答案"而不是真正学习语言模式

    数学证明信息泄露:

    假设Decoder在位置

    i

    i

    i 预测

    y

    i

    y_i

    yi

    • 双向注意力(错误):

    P

    (

    y

    i

    y

    <

    i

    )

    =

    softmax

    (

    W

    Attention

    (

    Q

    i

    ,

    K

    1

    :

    n

    ,

    V

    1

    :

    n

    )

    )

    P(y_i | y_{<i}) = \\text{softmax}(W \\cdot \\text{Attention}(Q_i, K_{1:n}, V_{1:n}))

    P(yiy<i)=softmax(WAttention(Qi,K1:n,V1:n))

    其中

    K

    1

    :

    n

    ,

    V

    1

    :

    n

    K_{1:n}, V_{1:n}

    K1:n,V1:n 包含

    y

    i

    y_i

    yi 的信息 → 信息泄露

    • 因果掩码(正确):

    P

    (

    y

    i

    y

    <

    i

    )

    =

    softmax

    (

    W

    Attention

    (

    Q

    i

    ,

    K

    1

    :

    i

    ,

    V

    1

    :

    i

    )

    )

    P(y_i | y_{<i}) = \\text{softmax}(W \\cdot \\text{Attention}(Q_i, K_{1:i}, V_{1:i}))

    P(yiy<i)=softmax(WAttention(Qi,K1:i,V1:i))

    只能看到

    y

    1

    :

    i

    1

    y_{1:i-1}

    y1:i1 → 无泄露

    场景2:推理时的灾难

    # 推理时生成句子
    # 第1步:只有 [<BOS>]
    # 第2步:只有 [<BOS>, 我]
    # 第3步:只有 [<BOS>, 我, 爱]

    # 如果训练时模型习惯看到完整句子(双向)
    # 推理时只有部分句子 → 分布不匹配 → 性能崩溃

    这叫 Exposure Bias(暴露偏差):

    • 训练时:看到完整句子(双向)
    • 推理时:只看到部分句子(自回归)
    • 结果:模型无法正确生成

    (3)能否都用双向?实验对比

    实验设计:用GPT-2架构,分别测试双向和单向

    import torch
    import torch.nn as nn
    from transformers import GPT2LMHeadModel, GPT2Tokenizer

    # 实验:双向 vs 单向 Attention
    class BidirectionalGPT2(nn.Module):
    """错误示范:双向Decoder"""
    def __init__(self, config):
    super().__init__()
    self.transformer = GPT2LMHeadModel(config)

    def forward(self, input_ids):
    # 移除因果掩码(允许双向)
    # 注意:这是错误的!
    outputs = self.transformer(
    input_ids,
    use_cache=False,
    # 不使用 causal mask
    )
    return outputs

    # 正确的单向Decoder
    tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
    model_causal = GPT2LMHeadModel.from_pretrained('gpt2')

    # 测试句子
    text = "I love natural language"
    inputs = tokenizer(text, return_tensors='pt')

    # 单向生成(正确)
    with torch.no_grad():
    outputs_causal = model_causal.generate(
    inputs['input_ids'],
    max_length=10,
    do_sample=False
    )

    print("单向Decoder生成:", tokenizer.decode(outputs_causal[0]))
    # 输出: "I love natural language processing and machine learning"

    # 如果用双向(训练-推理不匹配)
    # 生成质量会严重下降,出现:
    # – 重复token
    # – 语义不连贯
    # – 困惑度飙升

    实验结果(WikiText-2数据集):

    配置训练困惑度推理困惑度生成质量
    因果掩码(单向) 18.2 18.5 流畅 ✅
    双向注意力 12.1 156.3 崩溃 ❌

    观察:

    • 双向训练困惑度更低(能看到答案)
    • 但推理困惑度暴涨 8.4倍(分布不匹配)
    • 生成的文本重复、不连贯

    (4)信息利用率问题:因果掩码的代价

    你提到的关键问题:因果掩码会降低信息利用率吗?

    Rank分析

    双向注意力矩阵

    A

    R

    n

    ×

    n

    A \\in \\mathbb{R}^{n \\times n}

    ARn×n(Encoder):

    • 所有元素可能非零
    • 理论最大rank:

      rank

      (

      A

      )

      =

      n

      \\text{rank}(A) = n

      rank(A)=n

    因果掩码注意力矩阵

    A

    causal

    R

    n

    ×

    n

    A_{\\text{causal}} \\in \\mathbb{R}^{n \\times n}

    AcausalRn×n(Decoder):

    • 右上三角全为0(下三角矩阵)
    • 理论最大rank:

      rank

      (

      A

      causal

      )

      =

      n

      \\text{rank}(A_{\\text{causal}}) = n

      rank(Acausal)=n(仍然满秩!)

    为什么因果掩码不降低rank?

    下三角矩阵可以满秩:

    A

    causal

    =

    [

    a

    11

    0

    0

    a

    21

    a

    22

    0

    a

    31

    a

    32

    a

    33

    ]

    A_{\\text{causal}} = \\begin{bmatrix} a_{11} & 0 & 0 \\\\ a_{21} & a_{22} & 0 \\\\ a_{31} & a_{32} & a_{33} \\end{bmatrix}

    Acausal=

    a11a21a310a22a3200a33

    只要对角线元素非零,

    rank

    (

    A

    )

    =

    3

    \\text{rank}(A) = 3

    rank(A)=3(满秩)。

    信息量分析

    信息论视角:

    • 双向注意力信息量(Encoder):

    I

    bi

    =

    i

    =

    1

    n

    H

    (

    x

    i

    x

    1

    ,

    ,

    x

    i

    1

    ,

    x

    i

    +

    1

    ,

    ,

    x

    n

    )

    I_{\\text{bi}} = \\sum_{i=1}^{n} H(x_i | x_1, \\ldots, x_{i-1}, x_{i+1}, \\ldots, x_n)

    Ibi=i=1nH(xix1,,xi1,xi+1,,xn)

    每个位置条件于所有其他位置。

    • 单向注意力信息量(Decoder):

    I

    causal

    =

    i

    =

    1

    n

    H

    (

    x

    i

    x

    1

    ,

    ,

    x

    i

    1

    )

    I_{\\text{causal}} = \\sum_{i=1}^{n} H(x_i | x_1, \\ldots, x_{i-1})

    Icausal=i=1nH(xix1,,xi1)

    每个位置只条件于历史位置。

    信息损失:

    Δ

    I

    =

    I

    bi

    I

    causal

    =

    i

    =

    1

    n

    I

    (

    x

    i

    ;

    x

    i

    +

    1

    :

    n

    x

    1

    :

    i

    1

    )

    \\Delta I = I_{\\text{bi}} – I_{\\text{causal}} = \\sum_{i=1}^{n} I(x_i; x_{i+1:n} | x_{1:i-1})

    ΔI=IbiIcausal=i=1nI(xi;xi+1:nx1:i1)

    这就是"未来信息"的互信息。

    量化实验(BERT vs GPT):

    任务BERT(双向)GPT(单向)性能差距
    句子分类 94.2% 89.1% -5.1%
    命名实体识别 92.8% 85.3% -7.5%
    文本生成 N/A 基准

    结论:

    • 理解任务(分类、NER):双向更好(需要完整上下文)
    • 生成任务:单向是必须(推理时没有未来)
    信息利用率:位置越靠后越吃亏?

    问题:序列第1个位置只能看自己,最后一个位置能看所有,不公平?

    实际情况:

    # 可视化每个位置的有效上下文长度
    def analyze_causal_context(seq_len=10):
    """分析因果掩码下每个位置的信息量"""
    positions = list(range(1, seq_len + 1))
    context_sizes = positions # 位置i能看到i个token

    import matplotlib.pyplot as plt
    plt.figure(figsize=(10, 6))
    plt.bar(positions, context_sizes, color='skyblue', edgecolor='black')
    plt.xlabel('位置', fontsize=12)
    plt.ylabel('可见上下文大小', fontsize=12)
    plt.title('因果掩码下各位置的信息量', fontsize=14)
    plt.axhline(y=seq_len/2, color='r', linestyle='–',
    label=f'平均上下文={seq_len/2}')
    plt.legend()
    plt.grid(axis='y', alpha=0.3)
    plt.savefig('causal_context_distribution.png', dpi=300)
    plt.show()

    # 统计
    avg_context = sum(context_sizes) / len(context_sizes)
    print(f"平均上下文大小: {avg_context:.1f} tokens")
    print(f"最小上下文: {min(context_sizes)} (位置1)")
    print(f"最大上下文: {max(context_sizes)} (位置{seq_len})")

    analyze_causal_context(seq_len=10)

    输出:

    平均上下文大小: 5.5 tokens
    最小上下文: 1 (位置1)
    最大上下文: 10 (位置10)

    观察:

    • 位置1确实信息最少(只有自己)
    • 但这符合生成逻辑:第一个词本来就依赖最少
    • 后续位置信息累积,符合语言的递进性

    缓解策略(实践中使用):

  • 位置编码:补偿位置差异
  • 交叉注意力(Encoder-Decoder架构):
    • Decoder除了自注意力,还有Cross-Attention
    • 从Encoder获取完整输入的双向信息
  • Prefix Tuning:
    • 添加可学习的前缀向量
    • 为早期位置提供额外上下文

  • (5)Encoder vs Decoder 架构对比总结
    维度Encoder(BERT)Decoder(GPT)原因
    注意力类型 双向(全连接) 单向(因果掩码) 任务目标不同
    掩码矩阵 全1矩阵(填充除外) 下三角矩阵 防止信息泄露
    Rank 最大rank = n 最大rank = n 下三角可满秩
    信息量

    I

    (

    x

    i

    ;

    x

    i

    )

    I(x_i; x_{-i})

    I(xi;xi)

    I

    (

    x

    i

    ;

    x

    <

    i

    )

    I(x_i; x_{<i})

    I(xi;x<i)

    损失"未来信息"
    训练目标 MLM(完形填空) CLM(下一词预测) 双向 vs 单向
    推理模式 并行(所有位置同时) 自回归(逐个生成) 速度 vs 质量
    适用任务 分类、NER、QA 生成、对话、续写 理解 vs 生成
    信息利用率 100%(看全文) 平均50%(只看历史) 代价:推理时无未来

    (6)面试高频问题
    Q1: 为什么GPT不用双向注意力像BERT那样?

    错误回答:因为GPT是生成模型,BERT是理解模型。

    正确回答:

  • 核心原因:推理时训练-推理一致性
    • 训练时如果双向,模型会学会"抄答案"(看到

      y

      i

      y_i

      yi 预测

      y

      i

      y_i

      yi

    • 推理时自回归生成,只有

      y

      <

      i

      y_{<i}

      y<i,分布不匹配

  • 数学证明:
    • 双向:

      P

      (

      y

      i

      y

      1

      :

      n

      )

      P(y_i | y_{1:n})

      P(yiy1:n) → 包含

      y

      i

      y_i

      yi 信息(泄露)

    • 因果:

      P

      (

      y

      i

      y

      <

      i

      )

      P(y_i | y_{<i})

      P(yiy<i) → 无泄露

  • 实验证明:双向训练的Decoder推理困惑度暴涨(WikiText-2上156 vs 18)
  • Q2: 因果掩码不是损失了一半信息吗?

    回答:

  • Rank不损失:下三角矩阵可以满秩(

    rank

    =

    n

    \\text{rank} = n

    rank=n

  • 信息损失是必要的:推理时本来就没有"未来信息"
  • 平均信息量:
    • 位置

      i

      i

      i 能看

      i

      i

      i 个token

    • 平均:

      (

      1

      +

      2

      +

      +

      n

      )

      /

      n

      =

      (

      n

      +

      1

      )

      /

      2

      (1 + 2 + \\cdots + n) / n = (n+1)/2

      (1+2++n)/n=(n+1)/2

    • 相比双向的

      n

      n

      n,损失约50%

  • 补偿机制:
    • 交叉注意力(Encoder-Decoder)
    • 位置编码
    • 更大模型容量
  • Q3: 能否设计"半双向"掩码?

    回答:可以,已有研究!

    XLNet的Permutation Language Modeling:

    • 不用固定的从左到右顺序
    • 随机排列顺序(如

      [

      x

      3

      ,

      x

      1

      ,

      x

      4

      ,

      x

      2

      ]

      [x_3, x_1, x_4, x_2]

      [x3,x1,x4,x2]

    • 每种排列都训练一次
    • 效果:每个位置都能看到其他位置(不同排列中)

    UniLM的多任务掩码:

    • 同一模型支持三种掩码:
      • 双向(Encoder任务)
      • 单向(Decoder任务)
      • 前缀-单向(Seq2Seq任务)

    代码示例:

    def create_xlnet_mask(seq_len, perm):
    """
    XLNet的排列掩码

    Args:
    seq_len: 序列长度
    perm: 排列顺序,如 [2, 0, 3, 1]

    Returns:
    mask: [seq_len, seq_len]
    """
    mask = torch.zeros(seq_len, seq_len)
    for i, pos in enumerate(perm):
    # 位置pos能看到排列中它之前的所有位置
    for j in range(i):
    prev_pos = perm[j]
    mask[pos, prev_pos] = 1
    return mask

    # 示例:序列长度4,排列 [2, 0, 3, 1]
    perm = [2, 0, 3, 1]
    xlnet_mask = create_xlnet_mask(4, perm)
    print("XLNet排列掩码:")
    print(xlnet_mask)
    # 输出:
    # tensor([[0., 0., 1., 0.], ← 位置0能看位置2(排列中的前驱)
    # [1., 0., 1., 1.], ← 位置1能看2, 0, 3(排列中的前驱)
    # [0., 0., 0., 0.], ← 位置2第一个,看不到任何位置
    # [0., 0., 1., 1.]]) ← 位置3能看2, 0(排列中的前驱)

    Q4: Encoder-Decoder架构中,Decoder的交叉注意力为什么可以双向?

    回答:

  • 交叉注意力对象:Encoder的输出(完整输入的表示)
  • 关键:Encoder输出不是"未来的target",而是"已知的source"
  • 无信息泄露:
    • Decoder自注意力:因果掩码(

      y

      <

      i

      y_{<i}

      y<i

    • Cross-Attention:双向(Encoder的

      x

      1

      :

      m

      x_{1:m}

      x1:m

    • x

      1

      :

      m

      x_{1:m}

      x1:m 在推理时是完整已知的!

  • 代码验证:

    class DecoderLayer(nn.Module):
    def forward(self, x, memory, tgt_mask, memory_mask):
    # 1. 自注意力:因果掩码(单向)
    x = self.self_attn(
    query=x, key=x, value=x,
    attn_mask=tgt_mask # 因果掩码
    )

    # 2. 交叉注意力:无掩码(双向)
    x = self.cross_attn(
    query=x, # Decoder的隐状态
    key=memory, # Encoder的输出(完整source)
    value=memory,
    attn_mask=None # 无因果限制!
    )

    # 3. FFN
    x = self.ffn(x)
    return x


    (7)本节小结

    核心要点:

  • Encoder双向 vs Decoder单向:

    • 本质:任务目标不同(理解 vs 生成)
    • 数学:训练目标不同(MLM vs CLM)
    • 实践:推理模式不同(并行 vs 自回归)
  • 因果掩码的必要性:

    • 防止信息泄露(训练时看到答案)
    • 保证训练-推理一致性(Exposure Bias)
    • 实验证明:双向训练的Decoder推理性能崩溃
  • 信息利用率:

    • Rank:下三角可满秩,无损失
    • 信息量:平均损失50%(必要代价)
    • 补偿:交叉注意力、位置编码
  • 面试必背:

    • 公式:

      P

      (

      y

      i

      y

      <

      i

      )

      P(y_i | y_{<i})

      P(yiy<i) vs

      P

      (

      y

      i

      y

      1

      :

      n

      )

      P(y_i | y_{1:n})

      P(yiy1:n)

    • 数据:双向Decoder推理困惑度 156 vs 单向 18
    • 概念:Exposure Bias、训练-推理一致性

  • 组合掩码:Padding + Causal

    在实际应用中,常需要同时应用两种掩码:

    def create_combined_mask(seq_len, valid_len):
    """
    创建组合掩码(Padding + Causal)

    Args:
    seq_len: 序列总长度
    valid_len: 有效长度

    Returns:
    mask: [seq_len, seq_len]
    """
    # 因果掩码
    causal = create_causal_mask(seq_len)

    # 填充掩码
    padding = create_padding_mask(seq_len, valid_len)

    # 两者取交集(都为1才为1)
    combined = causal * padding

    return combined

    # 示例:序列长度=5,有效长度=3
    combined_mask = create_combined_mask(seq_len=5, valid_len=3)
    print("组合掩码:")
    print(combined_mask)

    输出:

    组合掩码:
    tensor([[1., 0., 0., 0., 0.], ← 位置0:只看自己,且自己有效
    [1., 1., 0., 0., 0.], ← 位置1:能看0、1,且都有效
    [1., 1., 1., 0., 0.], ← 位置2:能看0、1、2,且都有效
    [1., 1., 1., 0., 0.], ← 位置3:因果允许看0-3,但3是填充
    [1., 1., 1., 0., 0.]]) ← 位置4:因果允许看0-4,但4是填充

    掩码对梯度的影响

    关键洞察:掩码位置的梯度为0!

    # 测试掩码对梯度的影响
    x = torch.randn(1, 5, 64, requires_grad=True)
    attention = SelfAttention(d_model=64, d_k=64)

    # 不使用掩码
    output1, _ = attention(x, mask=None)
    loss1 = output1.sum()
    loss1.backward()
    grad1 = x.grad.clone()
    x.grad.zero_()

    # 使用掩码
    mask = create_causal_mask(5).unsqueeze(0)
    output2, _ = attention(x, mask=mask)
    loss2 = output2.sum()
    loss2.backward()
    grad2 = x.grad.clone()

    print("梯度差异:")
    print(f"不使用掩码的梯度范数: {grad1.norm():.4f}")
    print(f"使用掩码的梯度范数: {grad2.norm():.4f}")
    print(f"梯度是否相同: {torch.allclose(grad1, grad2)}")

    总结:

    • 掩码改变了信息流动路径
    • 被掩码的位置不参与梯度传播
    • 这对训练效率和模型行为都有重要影响

    可视化注意力权重

    让我们用真实句子看看注意力在"看"什么:

    from transformers import AutoTokenizer, AutoModel
    import matplotlib.pyplot as plt
    import seaborn as sns
    import numpy as np

    # 加载BERT模型
    model_name = "bert-base-uncased"
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    model = AutoModel.from_pretrained(model_name, output_attentions=True)

    # 测试句子
    sentence = "The cat sat on the mat"
    inputs = tokenizer(sentence, return_tensors="pt")
    tokens = tokenizer.convert_ids_to_tokens(inputs.input_ids[0])

    print("Tokens:", tokens)

    # 前向传播,获取注意力权重
    with torch.no_grad():
    outputs = model(**inputs)
    # outputs.attentions: 12层,每层的注意力权重
    # 取第6层、第1个头的注意力
    attention = outputs.attentions[5][0, 0].numpy() # [seq_len, seq_len]

    # 可视化
    plt.figure(figsize=(10, 8))
    sns.heatmap(
    attention,
    xticklabels=tokens,
    yticklabels=tokens,
    cmap="YlOrRd",
    annot=True,
    fmt=".2f",
    cbar_kws={'label': '注意力权重'}
    )
    plt.xlabel("被关注的Token")
    plt.ylabel("当前Token")
    plt.title("BERT第6层第1头的注意力权重")
    plt.tight_layout()
    plt.savefig('attention_heatmap.png', dpi=300)
    plt.show()

    观察:

    • 对角线权重高:每个词都关注自己
    • “cat"可能高度关注"sat”(主语-谓语关系)
    • "the"和"mat"可能相互关注(定冠词-名词关系)

    三、核心组件二:位置编码(Positional Encoding)

    1. 为什么Transformer需要位置编码?

    问题:自注意力是顺序无关的!

    考虑两个句子:

    • “The cat chased the dog”
    • “The dog chased the cat”

    如果去掉位置信息,自注意力会给出相同的输出(因为它只是计算词之间的相关性,不管顺序)。

    但这两句话的含义完全不同!

    解决方案:在嵌入中加入位置信息。


    2. 绝对位置编码:正弦余弦方案

    原始Transformer使用正弦和余弦函数生成位置编码:

    P

    E

    (

    p

    o

    s

    ,

    2

    i

    )

    =

    sin

    (

    p

    o

    s

    10000

    2

    i

    /

    d

    m

    o

    d

    e

    l

    )

    P

    E

    (

    p

    o

    s

    ,

    2

    i

    +

    1

    )

    =

    cos

    (

    p

    o

    s

    10000

    2

    i

    /

    d

    m

    o

    d

    e

    l

    )

    \\begin{align} PE_{(pos, 2i)} &= \\sin\\left(\\frac{pos}{10000^{2i/d_{model}}}\\right) \\\\ PE_{(pos, 2i+1)} &= \\cos\\left(\\frac{pos}{10000^{2i/d_{model}}}\\right) \\end{align}

    PE(pos,2i)PE(pos,2i+1)=sin(100002i/dmodelpos)=cos(100002i/dmodelpos)

    其中:

    • p

      o

      s

      pos

      pos:位置(0, 1, 2, …)

    • i

      i

      i:维度索引(0到

      d

      m

      o

      d

      e

      l

      /

      2

      d_{model}/2

      dmodel/2

    • 偶数维度用sin,奇数维度用cos

    为什么这么设计?深度数学直觉

    这不是随意选择,sin/cos有深刻的数学原因。

    原因1:线性可表达相对位置

    这是最重要的性质!

    数学推导:

    利用三角恒等式:

    sin

    (

    α

    +

    β

    )

    =

    sin

    (

    α

    )

    cos

    (

    β

    )

    +

    cos

    (

    α

    )

    sin

    (

    β

    )

    cos

    (

    α

    +

    β

    )

    =

    cos

    (

    α

    )

    cos

    (

    β

    )

    sin

    (

    α

    )

    sin

    (

    β

    )

    \\begin{align} \\sin(\\alpha + \\beta) &= \\sin(\\alpha)\\cos(\\beta) + \\cos(\\alpha)\\sin(\\beta) \\\\ \\cos(\\alpha + \\beta) &= \\cos(\\alpha)\\cos(\\beta) – \\sin(\\alpha)\\sin(\\beta) \\end{align}

    sin(α+β)cos(α+β)=sin(α)cos(β)+cos(α)sin(β)=cos(α)cos(β)sin(α)sin(β)

    因此,位置

    p

    o

    s

    +

    k

    pos + k

    pos+k 的编码可以表示为位置

    p

    o

    s

    pos

    pos 的线性组合:

    $$ \\begin{bmatrix} PE_{(pos+k, 2i)} \\ PE_{(pos+k, 2i+1)} \\end{bmatrix}

    \\begin{bmatrix} \\cos(k\\theta_i) & \\sin(k\\theta_i) \\ -\\sin(k\\theta_i) & \\cos(k\\theta_i) \\end{bmatrix} \\begin{bmatrix} PE_{(pos, 2i)} \\ PE_{(pos, 2i+1)} \\end{bmatrix} $$

    其中

    θ

    i

    =

    1

    /

    10000

    2

    i

    /

    d

    m

    o

    d

    e

    l

    \\theta_i = 1/10000^{2i/d_{model}}

    θi=1/100002i/dmodel

    这意味着什么?

    模型可以"学会"从绝对位置编码中提取相对位置信息!

    示例:

    位置5的编码 → 通过线性变换 → 得到"位置5比位置2远3个位置"

    这个性质让自注意力机制能够感知词之间的相对距离。

    原因2:不同频率捕获不同尺度

    观察公式中的

    10000

    2

    i

    /

    d

    m

    o

    d

    e

    l

    10000^{2i/d_{model}}

    100002i/dmodel:

    • 低维度(i=0): 频率 =

      1

      /

      10000

      0

      =

      1

      1/10000^0 = 1

      1/100000=1 → 周期 =

      2

      π

      2\\pi

      2π (约6个位置)

    • 中维度(i=128): 频率 =

      1

      /

      10000

      0.5

      1/10000^{0.5}

      1/100000.5 → 周期 =

      2

      π

      ×

      100

      2\\pi \\times 100

      2π×100 (约600位置)

    • 高维度(i=255): 频率 =

      1

      /

      10000

      1.0

      1/10000^{1.0}

      1/100001.0 → 周期 =

      2

      π

      ×

      10000

      2\\pi \\times 10000

      2π×10000 (约6万位置)

    类比傅里叶变换:

    就像音频分析,用不同频率的波捕获不同时间尺度的信号:

    • 高频波 → 捕获局部细节(相邻词)
    • 低频波 → 捕获全局结构(长距离依赖)

    可视化理解:

    # 不同维度的频率
    dims = [0, 64, 128, 192, 255]
    positions = range(100)

    for dim in dims:
    freq = 1 / (10000 ** (dim / 256))
    values = [np.sin(pos * freq) for pos in positions]

    plt.plot(positions, values, label=f'维度{dim}')

    plt.legend()
    plt.title('不同维度的位置编码频率')

    结果:低维度快速震荡(捕获局部),高维度缓慢变化(捕获全局)。

    原因3:唯一性与平滑性的平衡

    唯一性:

    对于合理的序列长度(

    <

    10

    4

    <10^4

    <104),每个位置的512维编码向量都是唯一的。

    证明思路:不同位置的sin/cos组合形成不同的"波形指纹"。

    平滑性:

    相邻位置的编码向量相似(余弦相似度高):

    sim

    (

    P

    E

    p

    o

    s

    ,

    P

    E

    p

    o

    s

    +

    1

    )

    0.99

    \\text{sim}(PE_{pos}, PE_{pos+1}) \\approx 0.99

    sim(PEpos,PEpos+1)0.99

    这让模型能够泛化:训练时学到的"相邻词关系"能应用到新句子。

    原因4:外推性(理论上)

    sin/cos函数的周期性意味着:

    P

    E

    p

    o

    s

    =

    P

    E

    p

    o

    s

    +

    T

    (

    如果 

    p

    o

    s

     超过周期 

    T

    )

    PE_{pos} = PE_{pos + T} \\quad (\\text{如果}\\ pos\\ \\text{超过周期}\\ T)

    PEpos=PEpos+T(如果 pos 超过周期 T)

    理论上可以处理任意长度。

    但实际问题:

    虽然sin/cos编码理论上支持任意长度,但模型训练的长度限制了实际性能:

    训练长度: 512
    测试长度: 2048 → 性能下降(外推失败)

    这促使了RoPE、ALiBi等相对位置编码的发展。


    实现:

    import torch
    import numpy as np
    import matplotlib.pyplot as plt

    def get_positional_encoding(seq_len, d_model):
    """
    生成正弦余弦位置编码

    Args:
    seq_len: 序列长度
    d_model: 嵌入维度

    Returns:
    pos_encoding: [seq_len, d_model]
    """
    # 创建位置和维度的索引
    position = torch.arange(seq_len).unsqueeze(1) # [seq_len, 1]
    div_term = torch.exp(
    torch.arange(0, d_model, 2) * (np.log(10000.0) / d_model)
    ) # [d_model/2]

    # 初始化位置编码矩阵
    pos_encoding = torch.zeros(seq_len, d_model)

    # 偶数维度用sin
    pos_encoding[:, 0::2] = torch.sin(position * div_term)

    # 奇数维度用cos
    pos_encoding[:, 1::2] = torch.cos(position * div_term)

    return pos_encoding

    # 生成位置编码
    seq_len = 100
    d_model = 512
    pe = get_positional_encoding(seq_len, d_model)

    print(f"位置编码形状: {pe.shape}")
    print(f"位置0的编码(前10维):\\n{pe[0, :10]}")
    print(f"位置1的编码(前10维):\\n{pe[1, :10]}")

    # 可视化
    plt.figure(figsize=(15, 5))

    # 子图1:位置编码热力图
    plt.subplot(1, 2, 1)
    plt.imshow(pe.numpy(), cmap='RdBu', aspect='auto')
    plt.xlabel('维度')
    plt.ylabel('位置')
    plt.title('位置编码可视化')
    plt.colorbar()

    # 子图2:几个位置的编码曲线
    plt.subplot(1, 2, 2)
    positions_to_plot = [0, 10, 20, 50]
    for pos in positions_to_plot:
    plt.plot(pe[pos, :128].numpy(), label=f'位置 {pos}')
    plt.xlabel('维度')
    plt.ylabel('编码值')
    plt.title('不同位置的编码曲线(前128维)')
    plt.legend()
    plt.grid(True)

    plt.tight_layout()
    plt.savefig('positional_encoding.png', dpi=300)
    plt.show()

    观察:

    • 低维度(接近0):频率低,变化慢,捕获粗粒度的位置信息
    • 高维度(接近d_model):频率高,变化快,捕获细粒度的位置信息

    3. 相对位置编码演进

    绝对位置编码有局限:

    • 只编码绝对位置,不直接编码相对距离
    • 对超长序列外推性不佳

    现代模型使用相对位置编码。

    章节说明:本节介绍RoPE等现代位置编码的核心原理,帮助理解Transformer架构的完整性。关于长上下文扩展技术(如NTK-aware、YaRN等)和FlashAttention等性能优化,将在**第七部分第1章《长上下文技术》**中详细展开。

    🎯 旋转位置编码(RoPE)- 面试必考

    代表模型:LLaMA、Qwen、GLM、ChatGLM、Yi、DeepSeek

    RoPE是当前主流LLM的标配位置编码方案,面试必问!


    (1)设计目标:相对位置不变性

    RoPE的核心设计目标是找到一个位置编码函数

    f

    (

    x

    ,

    )

    f(\\mathbf{x}, \\ell)

    f(x,),使得:

    f

    (

    q

    ,

    m

    )

    ,

    f

    (

    k

    ,

    n

    )

    =

    g

    (

    q

    ,

    k

    ,

    m

    n

    )

    \\langle f(\\mathbf{q}, m), f(\\mathbf{k}, n) \\rangle = g(\\mathbf{q}, \\mathbf{k}, m-n)

    f(q,m),f(k,n)⟩=g(q,k,mn)

    即注意力分数只依赖相对位置

    m

    n

    m-n

    mn,与绝对位置无关。

    这样设计的优势:

    • ✅ 自然的相对位置建模(语言的局部性)
    • ✅ 理论上支持任意长度外推
    • ✅ 零参数,无需学习

    (2)数学推导:从复数到旋转矩阵

    Step 1:复数表示

    d

    d

    d 维实向量重构为

    C

    d

    /

    2

    \\mathbb{C}^{d/2}

    Cd/2 复向量:

    q

    =

    (

    q

    0

    ,

    q

    1

    ,

    q

    2

    ,

    q

    3

    ,

    ,

    q

    d

    1

    )

    (

    q

    0

    +

    i

    q

    1

    ,

    q

    2

    +

    i

    q

    3

    ,


    )

    \\mathbf{q} = (q_0, q_1, q_2, q_3, \\dots, q_{d-1}) \\rightarrow (q_0+iq_1, q_2+iq_3, \\dots)

    q=(q0,q1,q2,q3,,qd1)(q0+iq1,q2+iq3,)

    设位置编码函数为:

    f

    (

    q

    ,

    m

    )

    =

    q

    e

    i

    m

    θ

    f(\\mathbf{q}, m) = \\mathbf{q} \\cdot e^{im\\boldsymbol{\\theta}}

    f(q,m)=qeimθ

    其中

    θ

    =

    (

    θ

    0

    ,

    θ

    1

    ,

    ,

    θ

    d

    /

    2

    1

    )

    \\boldsymbol{\\theta} = (\\theta_0, \\theta_1, \\dots, \\theta_{d/2-1})

    θ=(θ0,θ1,,θd/21) 是角频率向量。

    Step 2:相对位置证明

    对位置

    m

    m

    m 的查询和位置

    n

    n

    n 的键:

    f

    (

    q

    ,

    m

    )

    ,

    f

    (

    k

    ,

    n

    )

    =

    q

    e

    i

    m

    θ

    ,

    k

    e

    i

    n

    θ

    =

    j

    =

    0

    d

    /

    2

    1

    q

    j

    e

    i

    m

    θ

    j

    k

    j

    e

    i

    n

    θ

    j

    =

    j

    =

    0

    d

    /

    2

    1

    q

    j

    k

    ˉ

    j

    e

    i

    m

    θ

    j

    e

    i

    n

    θ

    j

    =

    j

    =

    0

    d

    /

    2

    1

    q

    j

    k

    ˉ

    j

    e

    i

    (

    m

    n

    )

    θ

    j

    =

    q

    ,

    k

    e

    i

    (

    m

    n

    )

    θ

    \\begin{align} \\langle f(\\mathbf{q}, m), f(\\mathbf{k}, n) \\rangle &= \\langle \\mathbf{q}e^{im\\boldsymbol{\\theta}}, \\mathbf{k}e^{in\\boldsymbol{\\theta}} \\rangle \\\\ &= \\sum_{j=0}^{d/2-1} q_j e^{im\\theta_j} \\cdot \\overline{k_j e^{in\\theta_j}} \\\\ &= \\sum_{j=0}^{d/2-1} q_j \\bar{k}_j \\cdot e^{im\\theta_j} \\cdot e^{-in\\theta_j} \\\\ &= \\sum_{j=0}^{d/2-1} q_j \\bar{k}_j \\cdot e^{i(m-n)\\theta_j} \\\\ &= \\langle \\mathbf{q}, \\mathbf{k}e^{i(m-n)\\boldsymbol{\\theta}} \\rangle \\end{align}

    f(q,m),f(k,n)⟩=qeimθ,keinθ=j=0d/21qjeimθjkjeinθj=j=0d/21qjkˉjeimθjeinθj=j=0d/21qjkˉjei(mn)θj=q,kei(mn)θ

    证明完毕:注意力分数只依赖

    m

    n

    m-n

    mn

    Step 3:实数矩阵形式

    为避免复数运算,将复数乘法转换为实数旋转矩阵。

    对于第

    j

    j

    j 对特征

    (

    q

    2

    j

    ,

    q

    2

    j

    +

    1

    )

    (q_{2j}, q_{2j+1})

    (q2j,q2j+1),旋转角度

    m

    θ

    j

    m\\theta_j

    mθj 对应的旋转矩阵:

    M

    j

    (

    m

    )

    =

    [

    cos

    (

    m

    θ

    j

    )

    sin

    (

    m

    θ

    j

    )

    sin

    (

    m

    θ

    j

    )

    cos

    (

    m

    θ

    j

    )

    ]

    \\mathbf{M}_j(m) = \\begin{bmatrix} \\cos(m\\theta_j) & -\\sin(m\\theta_j) \\\\ \\sin(m\\theta_j) & \\cos(m\\theta_j) \\end{bmatrix}

    Mj(m)=[cos(mθj)sin(mθj)sin(mθj)cos(mθj)]

    完整的RoPE变换(分块对角矩阵):

    R

    Θ

    ,

    m

    =

    [

    M

    0

    (

    m

    )

    M

    1

    (

    m

    )

    M

    d

    /

    2

    1

    (

    m

    )

    ]

    \\mathbf{R}_{\\Theta, m} = \\begin{bmatrix} \\mathbf{M}_0(m) & & & \\\\ & \\mathbf{M}_1(m) & & \\\\ & & \\ddots & \\\\ & & & \\mathbf{M}_{d/2-1}(m) \\end{bmatrix}

    RΘ,m=

    M0(m)M1(m)Md/21(m)

    应用到Query和Key:

    q

    m

    =

    R

    Θ

    ,

    m

    q

    m

    k

    n

    =

    R

    Θ

    ,

    n

    k

    n

    \\begin{align} \\mathbf{q}_m' &= \\mathbf{R}_{\\Theta, m} \\mathbf{q}_m \\\\ \\mathbf{k}_n' &= \\mathbf{R}_{\\Theta, n} \\mathbf{k}_n \\end{align}

    qmkn=RΘ,mqm=RΘ,nkn


    (3)角频率公式:为什么是

    10000

    2

    i

    /

    d

    10000^{2i/d}

    100002i/d

    角频率

    θ

    j

    \\theta_j

    θj 的选择至关重要,采用指数衰减:

    θ

    j

    =

    1

    10000

    2

    j

    /

    d

    ,

    j

    [

    0

    ,

    1

    ,

    ,

    d

    /

    2

    1

    ]

    \\theta_j = \\frac{1}{10000^{2j/d}}, \\quad j \\in [0, 1, \\dots, d/2-1]

    θj=100002j/d1,j[0,1,,d/21]

    设计理由:

  • 类比正弦位置编码:继承Transformer原始设计
  • 多尺度建模:
    • 高频分量(

      j

      j

      j 小):捕捉短距离依赖

    • 低频分量(

      j

      j

      j 大):捕捉长距离依赖

  • 波长覆盖范围:从

    2

    π

    2\\pi

    2π

    10000

    ×

    2

    π

    10000 \\times 2\\pi

    10000×2π

  • 代码实现:

    import torch

    def compute_theta(dim: int, base: float = 10000.0) > torch.Tensor:
    """计算角频率

    Args:
    dim: 注意力头维度(必须是偶数)
    base: 基数,通常为10000

    Returns:
    theta: [dim/2] 角频率向量
    """
    # θⱼ = 1 / (base^{2j/d})
    inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
    return inv_freq

    # 示例:64维注意力头
    theta = compute_theta(64)
    print(f"θ₀ = {theta[0]:.6f}") # 高频:θ₀ = 1.000000
    print(f"θ₃₁ = {theta[31]:.6f}") # 低频:θ₃₁ = 0.000100


    (4)生产级代码实现

    方法1:HuggingFace风格(实数版本)

    class RotaryEmbedding(nn.Module):
    """RoPE位置编码(LLaMA/Qwen实现)"""

    def __init__(self, dim: int, base: float = 10000.0, max_seq_len: int = 2048):
    super().__init__()
    # 计算逆频率:1 / (base^{2i/d})
    inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
    self.register_buffer("inv_freq", inv_freq, persistent=False)

    # 预计算缓存(优化性能)
    self._build_cache(max_seq_len)

    def _build_cache(self, seq_len: int):
    """预计算cos和sin值"""
    # 位置索引:[0, 1, 2, …, seq_len-1]
    t = torch.arange(seq_len, device=self.inv_freq.device).float()

    # 计算 m*θⱼ:[seq_len, dim/2]
    freqs = torch.outer(t, self.inv_freq)

    # 重复拼接(对应特征对的x和y分量使用相同角度)
    emb = torch.cat((freqs, freqs), dim=1) # [seq_len, dim]

    # 缓存cos和sin
    self.cos_cached = emb.cos()
    self.sin_cached = emb.sin()

    def forward(self, x: torch.Tensor, position_ids: torch.Tensor):
    """
    Args:
    x: [batch, seq_len, num_heads, head_dim]
    position_ids: [batch, seq_len]

    Returns:
    cos, sin: [batch, seq_len, head_dim]
    """
    # 动态扩展缓存
    seq_len = position_ids.max() + 1
    if seq_len > self.cos_cached.shape[0]:
    self._build_cache(seq_len)

    # 根据position_ids索引
    cos = self.cos_cached[position_ids]
    sin = self.sin_cached[position_ids]

    return cos, sin

    def rotate_half(x: torch.Tensor) > torch.Tensor:
    """将后半部分移到前面并取负:[-x_{d/2:}, x_{:d/2}]

    对应复数乘法的虚部:(a+bi)*(cosθ+i·sinθ) 的交叉项
    """
    x1 = x[..., :x.shape[1]//2]
    x2 = x[..., x.shape[1]//2:]
    return torch.cat((x2, x1), dim=1)

    def apply_rotary_pos_emb(q: torch.Tensor, k: torch.Tensor,
    cos: torch.Tensor, sin: torch.Tensor):
    """应用RoPE旋转

    数学等价于:x * e^{imθ} = x * (cos(mθ) + i*sin(mθ))

    Args:
    q, k: [batch, seq_len, num_heads, head_dim]
    cos, sin: [batch, seq_len, head_dim]

    Returns:
    q_embed, k_embed: 旋转后的查询和键
    """
    # 广播维度匹配
    cos = cos.unsqueeze(2) # [batch, seq_len, 1, head_dim]
    sin = sin.unsqueeze(2)

    # 公式:x*cos(mθ) + rotate_half(x)*sin(mθ)
    q_embed = (q * cos) + (rotate_half(q) * sin)
    k_embed = (k * cos) + (rotate_half(k) * sin)

    return q_embed, k_embed

    方法2:Meta LLaMA原始实现(复数版本)

    def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0):
    """预计算频率的复数指数形式(cis = cos + i*sin)

    Returns:
    freqs_cis: [end, dim/2] 复数张量
    """
    freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[:(dim//2)].float() / dim))
    t = torch.arange(end, device=freqs.device)
    freqs = torch.outer(t, freqs).float() # [end, dim/2]

    # 生成复数:e^{i*mθ} = cos(mθ) + i*sin(mθ)
    freqs_cis = torch.polar(torch.ones_like(freqs), freqs) # complex64
    return freqs_cis

    def apply_rotary_emb(xq, xk, freqs_cis):
    """使用复数乘法应用旋转(更简洁但需要复数支持)"""
    # 重塑为复数形式:[…, d] -> […, d/2] complex
    xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:1], 1, 2))
    xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:1], 1, 2))

    # 复数乘法实现旋转
    xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3)
    xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3)

    return xq_out.type_as(xq), xk_out.type_as(xk)


    (5)RoPE vs 绝对位置编码对比
    维度RoPE绝对位置编码(Sinusoidal)
    位置依赖 自然的相对位置 绝对位置(需学习相对关系)
    注入方式 乘性因子(旋转QK) 加性嵌入(加到Token)
    外推能力 强(理论无上界) 弱(训练长度受限)
    参数量 零参数 零参数
    计算开销 1-3%(融合优化后) 可忽略
    实验性能 OWT2困惑度 15.78 16.59

    关键优势:

    • ✅ 相对位置建模:符合语言的局部性特征
    • ✅ 长度泛化:训练2048可推理4096+
    • ✅ 零参数:无过拟合风险

    (6)外推性分析与长上下文扩展

    RoPE外推的局限:

    虽然理论上支持任意长度,但直接外推到训练时未见的长度会导致问题:

    ❌ 注意力分数爆炸:超出训练范围的位置编码导致数值不稳定 ❌ 高频分量混叠:长距离上产生周期性混淆

    解决方案1:Position Interpolation(PI)

    核心思路:线性压缩位置索引,而非外推

    position_ids

    new

    =

    position_ids

    ×

    L

    train

    L

    new

    \\text{position\\_ids}_{\\text{new}} = \\text{position\\_ids} \\times \\frac{L_{\\text{train}}}{L_{\\text{new}}}

    position_idsnew=position_ids×LnewLtrain

    代码实现:

    def position_interpolation(position_ids, max_train_len, current_len):
    """位置插值

    Args:
    position_ids: [batch, seq_len] 原始位置索引
    max_train_len: 训练时最大长度(如2048)
    current_len: 当前序列长度(如4096)

    Returns:
    插值后的位置索引
    """
    scale = max_train_len / current_len
    return (position_ids.float() * scale).long()

    优势:

    • ✅ 上界比外推小 ~600倍(数学证明)
    • ✅ 仅需 1000步 微调即可扩展到32k tokens

    解决方案2:NTK-aware Scaled RoPE

    动态调整base参数:

    base

    new

    =

    base

    ×

    (

    scale

    )

    d

    d

    2

    \\text{base}_{\\text{new}} = \\text{base} \\times \\left(\\text{scale}\\right)^{\\frac{d}{d-2}}

    basenew=base×(scale)d2d

    def ntk_scaled_rope(base, scale_factor, dim):
    """NTK-aware缩放"""
    return base * (scale_factor ** (dim / (dim 2)))

    # 示例:扩展2倍长度
    base_new = ntk_scaled_rope(10000, 2.0, 128) # ~40000

    解决方案3:YaRN方法

    • 计算效率:比之前方法少10倍tokens、2.5倍训练步数
    • 超长上下文:扩展到128k context length
    • 温度缩放:针对不同频率分量的自适应调整

    (7)面试高频问题

    Q1: RoPE为什么只依赖相对位置?

    通过旋转变换的群性质:

    e

    i

    m

    θ

    q

    ,

    e

    i

    n

    θ

    k

    =

    e

    i

    (

    m

    n

    )

    θ

    q

    ,

    k

    \\langle e^{im\\theta}q, e^{in\\theta}k \\rangle = \\langle e^{i(m-n)\\theta}q, k \\rangle

    eimθq,einθk=ei(mn)θq,k

    只依赖差值

    m

    n

    m-n

    mn,与绝对位置无关。

    Q2: rotate_half 的数学原理?

    对应复数乘法的实部和虚部展开:

    (

    a

    +

    b

    i

    )

    (

    cos

    θ

    +

    i

    sin

    θ

    )

    =

    (

    a

    cos

    θ

    b

    sin

    θ

    )

    +

    i

    (

    a

    sin

    θ

    +

    b

    cos

    θ

    )

    (a+bi) \\cdot (\\cos\\theta + i\\sin\\theta) = (a\\cos\\theta – b\\sin\\theta) + i(a\\sin\\theta + b\\cos\\theta)

    (a+bi)(cosθ+isinθ)=(acosθbsinθ)+i(asinθ+bcosθ)

    rotate_half(x) = [-b, a] 实现了虚部的交叉项。

    Q3: 为什么拼接两次 freqs?

    emb = torch.cat((freqs, freqs), dim=1)

    因为维度

    d

    d

    d 被分成

    d

    /

    2

    d/2

    d/2 对,每对的

    x

    x

    x

    y

    y

    y 分量使用相同的旋转角度,所以需要重复。

    Q4: RoPE的外推性如何解决?

    三种主流方法:

  • Position Interpolation:线性压缩位置索引
  • NTK-aware Scaling:动态调整base参数
  • YaRN:差异化频率缩放 + 温度调整
  • Q5: 为什么主流模型都用RoPE而不是ALiBi?

    • RoPE理论更优雅(群论基础)
    • 实现简单高效(预计算缓存)
    • 与Flash Attention等优化兼容性更好
    • LLaMA的成功带动了RoPE的普及
    ALiBi(Attention with Linear Biases)

    核心思想:在注意力分数上直接加上与距离成比例的偏置。

    Attention

    A

    L

    i

    B

    i

    (

    Q

    ,

    K

    ,

    V

    )

    =

    softmax

    (

    Q

    K

    T

    d

    k

    +

    m

    D

    )

    V

    \\text{Attention}_{ALiBi}(Q, K, V) = \\text{softmax}\\left(\\frac{QK^T}{\\sqrt{d_k}} + m \\cdot D\\right)V

    AttentionALiBi(Q,K,V)=softmax(dk

    QKT+mD)V

    其中:

    • D

      i

      j

      =

      (

      j

      i

      )

      D_{ij} = -(j – i)

      Dij=(ji):位置

      i

      i

      i

      j

      j

      j 的距离

    • m

      m

      m:每个头的斜率(不同头有不同斜率)

    优势:

    • 超强外推性:训练在1024长度,推理可到10万+
    • 不需要额外参数

    代表模型:BLOOM


    赞(0)
    未经允许不得转载:171主机测评 » 【大模型教程——第二部分:Transformer架构揭秘】第1章:Transformer核心揭秘 (The Transformer Architecture)【上】
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址