Skip to main content

Transformer架构 (LLM的基石)

整体概览:

这张图展示了一个典型的编码器-解码器 (Encoder-Decoder) 架构的 Transformer。

  • 左半部分 (蓝色框,标号2) 是 编码器 (Encoder),负责理解输入序列(比如源语言句子)。
  • 右半部分 (绿色框,标号3) 是 解码器 (Decoder),负责根据编码器的理解和已经生成的部分,生成输出序列(比如目标语言句子)。
  • 整个模型从下往上处理数据,最终在最顶端输出结果。
  • "Nx" 表示编码器和解码器内部的核心模块(带有 Attention 和 Feed Forward 的那一大块)会重复堆叠 N 次。论文中 N 通常是 6。

1. 输入 (Input) - 粉色框底部

这部分负责准备模型真正开始处理前的数据。

  • Encoder 端 (左侧):

    • Inputs: 这是原始的输入序列,比如一个英文句子 "I love NLP"。每个词是一个独立的 token。
    • Input Embedding:
      • 作用: 将输入的每个离散的词(token)转换为一个连续的、稠密的向量表示。计算机不直接理解文字,需要将其数字化。
      • 方式: 通常是一个可学习的查找表。每个词对应一个固定维度的向量(比如 512 维)。意思相近的词,其嵌入向量在空间中也应该比较接近。
    • Positional Encoding:
      • 作用: Transformer 的核心是自注意力机制,它本身并不包含序列中词的顺序信息(即它对词序不敏感,"我 爱 你" 和 "你 爱 我" 在它看来如果只看词本身可能一样)。为了解决这个问题,需要给模型的输入加入位置信息。
      • 方式: 生成一个与词嵌入维度相同的向量,这个向量根据词在序列中的绝对或相对位置计算得出(论文中使用 sin 和 cos 函数的不同频率组合)。然后,这个位置编码向量会加到 (Add) 对应的词嵌入向量上。
      • 结果: 最终进入编码器的,是每个词的 "词义信息" (来自 Embedding) 和 "位置信息" (来自 Positional Encoding) 的结合体。
  • Decoder 端 (右侧):

    • Outputs (shifted right): 这是在训练时解码器的输入。它通常是目标序列 (Ground Truth),但是会向右平移一位,并在开头加上一个特殊的起始符 (e.g., <SOS>)。
      • “Shifted right” 的原因 (Teacher Forcing): 在训练解码器预测第 t 个词时,我们希望它能利用到真实的前 t-1 个词。例如,要预测 "NLP",那么输入给解码器的就是 "<SOS> I love"。这样模型学习根据 "<SOS> I love" 来预测 "NLP"。如果没有 "shifted right",模型在预测第 t 个词时可能会看到第 t 个词本身,这就作弊了。
    • Output Embedding: 与 Encoder 端的 Input Embedding 类似,将解码器的输入词(目标语言的词)转换为向量。
    • Positional Encoding: 与 Encoder 端的 Positional Encoding 作用和方式相同,为解码器的输入序列提供位置信息。

2. Encoder block (编码器模块) - 蓝色框 (Nx)

编码器由 N 个相同的层堆叠而成。每一层包含两个主要的子层 (sub-layers):

  • 第一个子层: Multi-Head Attention (多头自注意力)

    • 输入: 来自上一层编码器(或输入嵌入+位置编码)的输出。
    • 作用: 核心是自注意力 (Self-Attention)。对于输入序列中的每一个词,自注意力机制会计算这个词与序列中所有其他词(包括它自己)的关联程度(注意力权重),然后根据这些权重对所有词的表示进行加权求和,得到这个词在当前上下文中的新表示。
    • “Multi-Head” (多头): 不是只进行一次注意力计算,而是将原始的词向量(Query, Key, Value 向量)线性投影到多个不同的子空间(“头”),在每个子空间独立计算注意力,然后将所有“头”的结果拼接起来再进行一次线性变换。这允许模型同时关注来自不同表征子空间的不同位置的信息,就像从不同角度审视同一个问题。
    • 输出: 经过自注意力加权后的序列表示,每个词的向量都融入了整个输入序列的上下文信息。
  • Add & Norm (残差连接与层归一化) - 第一个

    • Add (残差连接/Skip Connection): 将 Multi-Head Attention 子层的输入直接加到其输出上。即 Output = LayerNorm(Input_to_Sublayer + Sublayer_Output)
      • 作用: 帮助缓解深度网络中的梯度消失问题,使得信息能够更容易地在网络中传播,加速训练并提高性能。
    • Norm (Layer Normalization): 对相加后的结果进行层归一化。
      • 作用: 稳定每一层输入的分布,加速训练,提高模型的泛化能力。它对每个样本在特征维度上进行归一化。
  • 第二个子层: Feed Forward (前馈神经网络)

    • 输入: 经过第一个 Add & Norm 处理后的序列表示。
    • 作用: 这是一个简单的、全连接的前馈网络,但它会独立地应用于序列中的每一个位置(每一个词的表示)。它通常由两个线性变换和一个 ReLU 激活函数组成:FFN(x) = max(0, xW1 + b1)W2 + b2
    • 目的: 对自注意力层输出的表示进行进一步的非线性变换和特征提取,增加模型的表达能力。可以理解为对每个词融合了上下文信息后的表示进行更深层次的“思考”。
  • Add & Norm (残差连接与层归一化) - 第二个

    • 与第一个 Add & Norm 类似,将 Feed Forward 子层的输入加到其输出上,然后进行层归一化。

编码器整体流程: 输入序列的嵌入和位置编码 → (多头自注意力 → Add & Norm → 前馈网络 → Add & Norm) x N次 → 最终输出一系列编码后的向量,这些向量捕获了输入序列的深层语义和上下文信息。这个输出将作为解码器中一个关键注意力机制的输入。


3. Decoder block (解码器模块) - 绿色框 (Nx)

解码器也由 N 个相同的层堆叠而成。每一层包含三个主要的子层:

  • 第一个子层: Masked Multi-Head Attention (带掩码的多头自注意力)

    • 输入: 来自上一层解码器(或目标序列的嵌入+位置编码,即 "Outputs (shifted right)" 经过处理后)的输出。
    • 作用: 与编码器的自注意力类似,解码器也需要关注自身已经生成的部分。但是,在生成当前词时,它不能看到未来的词
    • “Masked” (掩码): 这是关键区别。在计算注意力权重时,会使用一个掩码 (mask) 将当前位置之后的所有位置的注意力权重设置为一个非常小的负数(这样经过 Softmax 后会接近于0)。这确保了预测第 i 个词时,只能依赖于第 1 到 i-1 个词的信息,符合自回归 (auto-regressive) 的生成方式。
    • Multi-Head: 与编码器中的多头机制相同。
    • 输出: 经过掩码自注意力加权后的解码器序列表示。
  • Add & Norm (残差连接与层归一化) - 第一个

    • 与编码器中的 Add & Norm 作用相同。
  • 第二个子层: Multi-Head Attention (编码器-解码器注意力 / Cross-Attention),不是自注意力

    • 输入:
      • Query (Q): 来自解码器前一个子层(Masked Multi-Head Attention + Add & Norm)的输出。
      • Key (K) 和 Value (V): 来自编码器栈的最终输出 (Encoder Output)
    • 作用: 这是连接编码器和解码器的桥梁。它允许解码器在生成每个目标词时,关注输入序列(源语言句子)的不同部分。例如,在翻译 "I love NLP" 时,当解码器要生成 "我" 这个词时,它可能会高度关注编码器输出中对应 "I" 的表示。
    • Multi-Head: 与编码器中的多头机制相同。
    • 输出: 结合了编码器信息和解码器自身状态的序列表示。
  • Add & Norm (残差连接与层归一化) - 第二个

    • 与编码器中的 Add & Norm 作用相同。
  • 第三个子层: Feed Forward (前馈神经网络)

    • 与编码器中的 Feed Forward 子层结构和作用相同,对第二个 Add & Norm 的输出进行进一步处理。
  • Add & Norm (残差连接与层归一化) - 第三个

    • 与编码器中的 Add & Norm 作用相同。

解码器整体流程: 目标序列的嵌入和位置编码 ("shifted right") → (掩码多头自注意力 → Add & Norm → 编码器-解码器注意力 → Add & Norm → 前馈网络 → Add & Norm) x N次 → 最终输出一系列解码后的向量。


4. 输出 (Output) - 粉色框顶部

这部分负责将解码器最后一层的输出转换为最终的词概率。

  • Linear (线性层):

    • 输入: 解码器栈最后一层的输出(一个向量序列,每个向量代表一个潜在的输出词)。
    • 作用: 一个全连接线性层,将解码器输出的 d_model 维向量投影到词汇表大小 (Vocabulary Size) 的维度。例如,如果你的目标语言词汇表有 30000 个词,那么这个线性层会将每个位置的向量转换为一个 30000 维的向量。这个向量的每个元素可以看作是对应词汇表中每个词的“分数”或“logit”。
  • Softmax:

    • 输入: 线性层输出的 logits 向量。
    • 作用: 将 logits 转换为概率分布。Softmax 函数会使得输出向量中所有元素之和为 1,并且每个元素都在 0 到 1 之间。每个元素的值代表了对应词汇表中的词是下一个词的概率。
    • 输出: "Output Probabilities" (输出概率)。在生成时,模型通常会选择概率最高的那个词作为当前时间步的输出,然后这个词会作为下一个时间步解码器的输入 (Outputs shifted right 的一部分),如此循环直到生成结束符 (e.g., <EOS>)。

总结一下关键点:

  • Attention is All You Need: 模型的核心是注意力机制,特别是多头自注意力 (用于理解自身序列上下文) 和编码器-解码器注意力 (用于对齐源序列和目标序列)。
  • 并行化: 相较于 RNN 必须顺序处理,Transformer 的大部分计算(尤其是注意力)可以高度并行化,训练效率高。
  • Positional Encoding: 弥补了纯注意力机制对顺序信息不敏感的缺陷。
  • Residual Connections & Layer Normalization: 对于训练深度 Transformer 至关重要,保证了训练的稳定性和有效性。
  • Encoder-Decoder 结构: 经典的处理序列到序列任务的框架。
  • Masking in Decoder: 保证了生成过程的自回归特性,防止模型“偷看”未来信息。

维度变化

假设:

  • seq_len_target (或简写为 T): 解码器正在处理的目标序列的当前长度 (比如正在生成第 T 个词)。
  • d_model: 模型内部的隐藏层维度 (例如 512)。
  • vocab_size_target: 目标语言的词汇表大小 (例如 30000)。

1. 解码器块 (Decoder block) 的输入与输出

  • 输入到第一个解码器块 (或任何一个解码器块):

    • 通常是一个形状为 (batch_size, seq_len_target, d_model) 的张量。
      • batch_size: 一次处理的样本数量。
      • seq_len_target: 目标序列的长度。对于第一个解码器块,这是目标词嵌入加上位置编码后的结果。对于后续的解码器块,这是前一个解码器块的输出。
      • d_model: 每个词/token 的表示向量维度。
  • 解码器块的输出 (经过 N 层堆叠后的最终输出):

    • 经过解码器栈 (N 个解码器块) 处理后,输出的张量形状仍然是 (batch_size, seq_len_target, d_model)
    • 这是因为解码器内部的子层(Masked Self-Attention, Encoder-Decoder Attention, Feed Forward Network)以及 Add & Norm 操作都设计为保持序列长度 seq_len_target 和特征维度 d_model 不变。每个解码器块只是对每个位置的 d_model 维向量进行更深层次的加工和信息融合。

2. 输出 (Output) 层的输入与输出 (即图中最顶部的 Linear + Softmax)

  • 输入到 Linear 层:

    • 这就是解码器栈的最终输出,其形状为 (batch_size, seq_len_target, d_model)
  • Linear 层 (也称为投影层或输出投影层):

    • 这个线性层的作用是将 d_model 维的向量投影到词汇表大小的维度。
    • 它有一个权重矩阵,形状大约是 (d_model, vocab_size_target) (忽略偏置项)。
    • 输出形状: (batch_size, seq_len_target, vocab_size_target)
      • 对于目标序列中的每一个位置 (从 1 到 seq_len_target),现在都有一个长度为 vocab_size_target 的向量。这个向量中的每个元素可以被看作是对应词汇表中该词成为下一个预测词的 "得分" 或 "logit"。
  • Softmax 层:

    • Softmax 函数会独立地应用于最后一个维度 (即 vocab_size_target 这个维度)。
    • 对于序列中的每一个位置,它会将 vocab_size_target 维的 logits 向量转换为一个概率分布。
    • 输出形状: 仍然是 (batch_size, seq_len_target, vocab_size_target)
      • 现在,对于目标序列中的每一个位置,你都有一个长度为 vocab_size_target 的概率分布向量。这个向量中的每个元素表示对应词汇表中的词是该位置输出词的概率,且该维度上所有概率之和为 1。

总结一下维度变化:

  1. 解码器输入 (第一个解码器块): (batch_size, seq_len_target, d_model)

    • (例如:目标序列 "I love NLP <EOS>",如果当前只生成到 "love",seq_len_target 可能为 3 (对应 <SOS> I love))
  2. 解码器栈输出 (最后一个解码器块的输出): (batch_size, seq_len_target, d_model)

    • (维度不变,但内容更丰富)
  3. Linear 层输出 (logits): (batch_size, seq_len_target, vocab_size_target)

    • (例如:如果 vocab_size_target 是 30000,那么对于 "love" 这个位置,会有一个 30000 维的向量,表示预测下一个词(应该是 "NLP")的得分)
  4. Softmax 层输出 (概率): (batch_size, seq_len_target, vocab_size_target)

    • (维度不变,但值变成了概率)

在实际生成 (Inference/Decoding) 过程中的应用:

在模型训练完成后进行文本生成时,通常是一步一步 (auto-regressively) 生成的:

  1. 解码器输入通常只包含一个起始符 <SOS> (所以 seq_len_target=1)。
  2. 经过解码器栈和输出层,得到一个形状为 (batch_size, 1, vocab_size_target) 的概率分布。
  3. 从这个分布中选择一个词作为第一个生成的词 (比如选择概率最高的词,或通过采样)。
  4. 将这个生成的词添加到解码器的输入序列中 (现在 seq_len_target=2),然后重复步骤 2 和 3,直到生成一个结束符 <EOS> 或者达到最大长度限制。

所以,在训练时,seq_len_target 是整个目标序列的长度 (因为我们有 "teacher forcing")。在推理时,seq_len_target 是动态增长的,从 1 开始。但无论何时,输出层的最终输出都是在 vocab_size_target 维度上的概率分布。

训练 (Training) 和 推理/生成 (Inference/Decoding) 过程中的行为是不同的。

你对 Transformer 在推理/生成时是一个 token 一个 token 输出的理解是完全正确的!

让我们来解释为什么输出层在训练时和推理时看起来行为不同,以及它们是如何统一的:

1. 训练 (Training) 阶段:Teacher Forcing

  • 在训练时,我们已知整个目标序列 (ground truth)。例如,我们要将 "我爱NLP" 翻译成 "I love NLP"。
  • 并行计算的优势: Transformer 的一个巨大优势就是可以并行处理整个序列。
  • Teacher Forcing: 为了高效训练,我们通常使用 "Teacher Forcing"。这意味着:
    • 解码器的输入: 不是模型上一步自己生成的词,而是真实的、向右平移一位的目标序列。例如,如果目标是 "I love NLP <EOS>",那么解码器的输入会是 "<SOS> I love NLP"。
    • 一次性预测所有位置: 因为解码器一次性看到了整个(或大部分)目标序列的 "提示"(shifted right),它可以并行地为目标序列中的每一个位置都计算出一个概率分布
    • 这就是为什么训练时输出层的输出维度是 (batch_size, seq_len_target, vocab_size_target)。它同时计算了:
      • 给定 <SOS>,预测第一个词的概率分布。
      • 给定 <SOS> I,预测第二个词的概率分布。
      • 给定 <SOS> I love,预测第三个词的概率分布。
      • ...
    • Masked Self-Attention 的作用: 即使是并行计算,解码器内部的 Masked Self-Attention 依然确保了在预测第 t 个位置的词时,只能依赖于 1 到 t-1 位置的信息,不会"偷看"第 t 个或之后位置的真实标签。
  • 计算损失: 然后,我们会将这个 (batch_size, seq_len_target, vocab_size_target) 的概率分布与真实的目标序列(例如 "I love NLP <EOS>" 的 one-hot 编码或标签索引)进行比较,计算损失函数(通常是交叉熵损失),然后反向传播更新模型参数。

所以,在训练阶段,输出 (batch_size, seq_len_target, vocab_size_target) 并不是说模型一次性“生成”了 seq_len_target 个词,而是说模型并行地对目标序列的每一个位置都进行了预测,并输出了相应的概率分布。 这是为了训练效率。

2. 推理/生成 (Inference/Decoding) 阶段:自回归 (Auto-regressive) 生成

  • 在推理时,我们没有目标序列,我们需要模型一个词一个词地生成。
  • 逐步生成:
    1. 初始输入: 解码器的初始输入通常只包含一个特殊的起始符,比如 <SOS>。此时,seq_len_target = 1
    2. 第一次预测: 模型运行一次,输出一个形状为 (batch_size, 1, vocab_size_target) 的概率分布。这个分布对应于给定 <SOS> 后,第一个目标词的概率。
    3. 选择词元: 我们从这个概率分布中选择一个词作为第一个生成的词。选择策略可以是:
      • Greedy search: 选择概率最高的词。
      • Beam search: 保留概率最高的 k 个候选序列,并在下一步扩展它们。
      • Sampling: 根据概率分布进行随机采样 (可以加入 temperature 等参数控制随机性)。
    4. 更新输入: 将上一步生成的词添加到解码器的输入序列中。例如,如果生成了 "I",那么新的解码器输入就是 <SOS> I。此时,seq_len_target = 2
    5. 下一次预测: 模型再次运行,输出一个形状为 (batch_size, 2, vocab_size_target) 的概率分布。然而,我们只关心最后一个时间步的预测结果,即 [:, -1, :],它对应于给定 <SOS> I 后,下一个词的概率分布。
    6. 重复: 重复步骤 3-5,直到模型生成一个特殊的结束符 <EOS>,或者达到预设的最大生成长度。

关键点:

  • 虽然在推理的每一步,模型内部的计算(如果从头开始计算所有已生成词的表示)可能会输出 (batch_size, current_seq_len, vocab_size_target),但我们只使用最后一个时间步 (current_seq_len - 1 或 [:, -1, :]) 的概率分布来决定下一个要生成的词。
  • 缓存 (KV Cache): 为了提高推理效率,实际应用中通常会使用 KV 缓存。在解码器生成第 t 个词时,对于自注意力和编码器-解码器注意力,之前 t-1 个词的 Key 和 Value 向量可以被缓存起来,这样在计算第 t 个词的注意力时,只需要计算当前词的 Query,并与缓存的 Key 和 Value 进行交互,而不需要重新计算整个序列的 K 和 V。这大大减少了计算量,但概念上仍然是一个词一个词地生成。

总结:

  • 训练时输出 (batch_size, seq_len_target, vocab_size_target) 是为了并行计算和高效训练,模型在每个位置上都进行预测,并与真实标签比较计算损失。
  • 推理时,Transformer 确实是一个 token 一个 token 地自回归生成。 尽管模型内部可能计算了到当前步为止所有位置的表示,但我们只利用最后一个位置的概率分布来选择下一个 token。