Skip to content

Transformer

鉴于Transformer架构已经逐步成为深度学习乃至机器学习的核心架构,我单独开这么一篇笔记来记录学习Transformer架构。在DNN架构介绍中我们已经简单了解了Transformer的一些特点,在本笔记中我们将进一步深入了解Transformer。

从 RNN/CNN 到 Transformer

在 RNN 笔记中我们已经看到,RNN 有两个致命问题:

问题 RNN 的困境 Transformer 的解决方案
无法并行 \(h_t\) 必须等 \(h_{t-1}\) 算完,顺序依赖 \(O(n)\) Self-Attention 一次性处理所有位置,\(O(1)\) 顺序操作
长距离依赖 梯度经过多步连乘后消失,记不住远处信息 任意两个位置直接相连,路径长度\(O(1)\)

LSTM/GRU 缓解了梯度消失但无法解决并行问题。Attention 机制最初是作为 Seq2Seq 的辅助(Bahdanau, 2014),但 2017 年 Vaswani 等人提出了一个激进想法:完全抛弃 RNN,仅用 Attention 构建整个模型

"Attention is All You Need" — Vaswani et al., 2017


架构总览

完整架构图

下图是 Transformer 的完整架构(图源:"Attention Is All You Need" 原论文):

Transformer完整架构

架构要点:

  • 左半边 = Encoder(编码器):处理源语言输入,输出每个词的上下文表示
  • 右半边 = Decoder(解码器):利用 Encoder 的输出,自回归生成目标语言
  • Nx 表示堆叠 \(N\) 层(原论文 \(N=6\)
  • 核心超参数:\(d_{\text{model}}=512\)\(h=8\) 头,\(d_{ff}=2048\)

数据流:一次翻译的完整过程

以翻译 "我 喜欢 猫" → "I like cats" 为例:

  1. 输入编码:源语言 "我 喜欢 猫" → Token Embedding + Positional Encoding
  2. Encoder处理:经过6层Encoder,每层都做Self-Attention + FFN,最终输出每个源词的上下文表示
  3. Decoder自回归生成
    • 输入 <BOS>(句首标记)→ Decoder通过Cross-Attention看Encoder输出 → 预测 "I"
    • 输入 <BOS> I → 预测 "like"
    • 输入 <BOS> I like → 预测 "cats"
    • 输入 <BOS> I like cats → 预测 <EOS>(结束标记)

输入处理

Token Embedding

将离散的词(token)映射为连续的向量。这与Word2Vec类似,但Transformer的嵌入矩阵是随模型一起训练的。

\[ \text{Embedding}(x) = E[x] \in \mathbb{R}^{d_{\text{model}}} \]

其中 \(E \in \mathbb{R}^{V \times d_{\text{model}}}\) 是嵌入矩阵,\(V\) 是词表大小。

注意:原论文中对嵌入向量乘以了 \(\sqrt{d_{\text{model}}}\) 进行缩放:

\[ \text{ScaledEmbedding}(x) = E[x] \times \sqrt{d_{\text{model}}} \]

这是为了在与Positional Encoding相加时,保持两者的量级相当。

Positional Encoding(位置编码)

为什么需要位置编码?

Self-Attention是一个集合操作——它对输入的处理与顺序无关。如果不加位置信息,"狗咬人" 和 "人咬狗" 对模型来说完全一样。Positional Encoding给每个位置注入顺序信息。

正弦余弦位置编码:

\[ 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) \]
  • \(pos\):词在序列中的位置(0, 1, 2, ...)
  • \(i\):维度索引(0, 1, ..., \(d_{\text{model}}/2 - 1\)
  • 偶数维用 \(\sin\),奇数维用 \(\cos\)

直觉理解:每个维度对应一个不同频率的"钟摆"。低维度变化快(高频),高维度变化慢(低频)。这就像二进制计数器——个位翻转最快,高位翻转最慢。不同位置的组合是唯一的,就像每个数字的二进制表示是唯一的。

为什么用正弦/余弦而不是简单的线性编码(如1, 2, 3...)?

  1. 有界性:值域在 \([-1, 1]\),不会随位置增长而爆炸
  2. 相对位置可学习:对于固定偏移 \(k\)\(PE_{pos+k}\) 可以表示为 \(PE_{pos}\) 的线性函数(利用三角函数的和差化积公式),这使模型能学习相对位置关系:
\[ \begin{bmatrix} \sin(pos \cdot \omega + k\omega) \\ \cos(pos \cdot \omega + k\omega) \end{bmatrix} = \begin{bmatrix} \cos(k\omega) & \sin(k\omega) \\ -\sin(k\omega) & \cos(k\omega) \end{bmatrix} \begin{bmatrix} \sin(pos \cdot \omega) \\ \cos(pos \cdot \omega) \end{bmatrix} \]

其中 \(\omega = 1/10000^{2i/d_{\text{model}}}\)。也就是说,位置 \(pos+k\) 的编码可以由位置 \(pos\) 的编码通过一个只依赖于 \(k\)(不依赖于 \(pos\)的线性变换得到。

  1. 可外推:理论上可以推广到训练中未见过的序列长度

与输入的结合方式:直接相加(不是拼接)

\[ \text{Input} = \text{TokenEmbedding}(x) + \text{PositionalEncoding}(pos) \]

Encoder 详解

下图高亮标注了 Encoder 部分(红色框内):

Encoder结构

每个Encoder层包含两个子层:Multi-Head Self-AttentionFeed-Forward Network,每个子层外面包裹 残差连接 + Layer Normalization

子层1:Multi-Head Self-Attention

编码器中的Self-Attention让每个源词都能关注到所有其他源词,构建全局上下文理解。

\[ \text{SelfAttn}(X) = \text{MultiHead}(Q=X, K=X, V=X) \]

下图展示了 Scaled Dot-Product Attention 和 Multi-Head Attention 的内部结构(图源:原论文):

Attention机制

Scaled Dot-Product Attention(左图)的计算流程:

\[ \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) V \]
  1. MatMul\(Q\)\(K^T\) 做矩阵乘法,得到注意力分数矩阵 \(\in \mathbb{R}^{n \times n}\)
  2. Scale:除以 \(\sqrt{d_k}\),防止点积过大导致 softmax 梯度消失
  3. Mask(可选):Decoder 中的因果掩码,将未来位置设为 \(-\infty\)
  4. Softmax:将分数归一化为概率分布(每行加和为1)
  5. MatMul:用注意力权重对 \(V\) 加权求和,得到输出

Multi-Head Attention(右图)的计算流程:

\[ \text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h) W^O \]
\[ \text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V) \]
  • \(h=8\) 个头,每个头的维度 \(d_k = d_v = d_{\text{model}} / h = 64\)
  • 每个头学习不同的注意力模式(如语法关系、语义关系、位置关系等)
  • 最后拼接并通过线性层 \(W^O\) 映射回 \(d_{\text{model}}\)

在编码 "我 吃 苹果" 时:

  • "苹果"可以关注到"吃",从而理解这里的"苹果"是水果(而非公司)
  • "吃"可以关注到"我",理解动作的主语

这回答了待思考的问题: "模型在翻译'苹果'时,是否真的关注到了前面的动词'吃'?" —— 是的。 Self-Attention让"苹果"可以直接(\(O(1)\) 路径长度)关注到"吃",并根据上下文动态调整自己的表示。

子层2:Position-wise Feed-Forward Network (FFN)

对每个位置独立地施加相同的两层全连接网络:

\[ \text{FFN}(x) = \max(0, xW_1 + b_1)W_2 + b_2 \]

或者写成更清晰的两步:

\[ \text{hidden} = \text{ReLU}(xW_1 + b_1) \quad \in \mathbb{R}^{d_{ff}} \]
\[ \text{output} = \text{hidden} \cdot W_2 + b_2 \quad \in \mathbb{R}^{d_{\text{model}}} \]
  • \(W_1 \in \mathbb{R}^{d_{\text{model}} \times d_{ff}}\):升维,\(512 \to 2048\)
  • \(W_2 \in \mathbb{R}^{d_{ff} \times d_{\text{model}}}\):降维,\(2048 \to 512\)

为什么需要FFN?

Self-Attention是一个线性加权求和操作(Softmax权重 × Value),即使堆叠多层,也只是在做加权组合。FFN引入了非线性(通过ReLU),赋予模型非线性变换能力。

"Position-wise"的含义:FFN对序列中每个位置独立施加,不同位置之间不交互。位置间的信息交换完全由Self-Attention负责。可以将FFN理解为对每个词的"独立思考",而Self-Attention是词与词之间的"交流讨论"。

残差连接与Layer Normalization

每个子层都配有残差连接(Residual Connection)层归一化(Layer Normalization)

\[ \text{SubLayerOutput} = \text{LayerNorm}(x + \text{SubLayer}(x)) \]

残差连接(He et al., 2015):让梯度可以"跳过"子层直接传播,解决深层网络的梯度消失问题。

\[ \text{output} = x + f(x) \]

如果 \(f(x)\) 学到的变换不好,梯度至少可以通过恒等映射 \(x\) 传回去。

Layer Normalization:对单个样本的特征维度做归一化:

\[ \text{LayerNorm}(x) = \gamma \odot \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta \]
  • \(\mu = \frac{1}{d}\sum_{i=1}^d x_i\):特征维度的均值
  • \(\sigma^2 = \frac{1}{d}\sum_{i=1}^d (x_i - \mu)^2\):特征维度的方差
  • \(\gamma, \beta\):可学习的缩放和偏移参数
  • \(\epsilon\):防止除零的小常数

为什么Layer Norm而非Batch Norm?

这回答了待思考问题之一。

Batch Norm:在batch维度上归一化(对同一个特征,跨batch中所有样本计算均值和方差)

Layer Norm:在特征维度上归一化(对同一个样本,跨所有特征计算均值和方差)

              Batch Norm              Layer Norm
              跨样本归一化             跨特征归一化

Batch:  ┌ Sample1: [f1 f2 f3 f4] ┐   Sample1: [f1 f2 f3 f4] ← 在这一行归一化
        │ Sample2: [f1 f2 f3 f4] │   Sample2: [f1 f2 f3 f4] ← 在这一行归一化
        │ Sample3: [f1 f2 f3 f4] │   Sample3: [f1 f2 f3 f4] ← 在这一行归一化
        └────────────────────────┘
              ↑  在每一列归一化

Layer Norm更适合NLP的三个原因:

  1. 变长序列:NLP中每个样本的序列长度不同。Batch Norm需要跨样本统计,但不同样本的第10个位置可能有完全不同的语义角色,统计没有意义。
  2. 小batch问题:Transformer训练常用较小的batch size(GPU显存限制),Batch Norm的统计量不稳定。
  3. 推理时不依赖batch:Layer Norm只看单个样本自身,推理时行为与训练时一致,不需要维护running mean/variance。

Decoder 详解

下图高亮标注了 Decoder 部分(红色框内):

Decoder结构

Decoder比Encoder多一个子层,共三个子层:

子层1:Masked Multi-Head Self-Attention

与Encoder的Self-Attention几乎相同,唯一区别是加了因果掩码(Causal Mask)

\[ \text{MaskedSelfAttn}(X) = \text{MultiHead}(Q=X, K=X, V=X, \text{mask}=\text{CausalMask}) \]

为什么需要掩码?

在训练时,Decoder看到的是完整的目标序列(如 "I like cats"),但为了模拟自回归生成的过程,生成第 \(t\) 个词时不能"偷看"第 \(t+1, t+2, \ldots\) 个词。

目标序列: <BOS> I like cats <EOS>

生成 "I"    时,只能看到 <BOS>
生成 "like" 时,只能看到 <BOS> I
生成 "cats" 时,只能看到 <BOS> I like
生成 <EOS>  时,只能看到 <BOS> I like cats

掩码矩阵(0=可见,-∞=遮蔽):
         <BOS>  I    like  cats  <EOS>
<BOS>  [  0    -∞    -∞    -∞    -∞  ]
I      [  0     0    -∞    -∞    -∞  ]
like   [  0     0     0    -∞    -∞  ]
cats   [  0     0     0     0    -∞  ]
<EOS>  [  0     0     0     0     0  ]

这个掩码加在Softmax之前的注意力分数上。\(-\infty\) 经Softmax后变为0,实现了信息屏蔽。

训练效率的关键:有了掩码,Decoder可以一次性并行处理整个目标序列(而不是像RNN那样逐步生成),因为掩码保证了每个位置只看到它该看到的历史信息。这就是所谓的 Teacher Forcing

子层2:Cross-Attention(编码器-解码器注意力)

这是连接Encoder和Decoder的桥梁:

\[ \text{CrossAttn} = \text{MultiHead}(Q=\text{Decoder}, K=\text{Encoder}, V=\text{Encoder}) \]
  • Query来自Decoder当前层的输出("我要翻译什么?")
  • Key和Value来自Encoder的最终输出("源语言说了什么?")

这让Decoder在生成每个目标词时,可以关注源语言中最相关的部分。例如生成 "cats" 时,Cross-Attention会高权重关注 "猫" 对应的Encoder输出。

子层3:Feed-Forward Network

与Encoder中的FFN完全相同。

输出层

Decoder最顶层的输出通过一个线性层 + Softmax,得到词表上的概率分布:

\[ P(y_t | y_{<t}, x) = \text{softmax}(W_{\text{vocab}} \cdot h_t^{\text{dec}} + b) \]

\(W_{\text{vocab}} \in \mathbb{R}^{V \times d_{\text{model}}}\),实践中通常与Token Embedding矩阵权重共享(Weight Tying)以减少参数。


前向传播:完整的数值示例

参考 RNN 和 CNN 笔记的风格,我们用一个具体的数值示例走完 Transformer 的整个前向传播过程。

为了简化,使用一个极小的 Transformer:\(d_{\text{model}}=4\)\(h=2\)(2个头),\(d_k = d_v = 2\)\(d_{ff}=8\)\(N=1\)(1层),词表大小 \(V=6\)

翻译任务:"我 猫" → "I cat"

第1步:输入 Embedding + Positional Encoding

Token Embedding(从嵌入矩阵 \(E\) 查表):

\[ \text{Embed}(\text{"我"}) = [0.2, -0.1, 0.4, 0.3], \quad \text{Embed}(\text{"猫"}) = [0.5, 0.3, -0.2, 0.1] \]

Positional Encoding(用正弦余弦公式计算):

\[ PE_0 = [\sin(0), \cos(0), \sin(0), \cos(0)] = [0, 1, 0, 1] \]
\[ PE_1 = [\sin(1/100), \cos(1/100), \sin(1/10000), \cos(1/10000)] \approx [0.01, 1.0, 0.0001, 1.0] \]

相加得到 Encoder 输入

\[ x_1 = [0.2, -0.1, 0.4, 0.3] + [0, 1, 0, 1] = [0.2, 0.9, 0.4, 1.3] \]
\[ x_2 = [0.5, 0.3, -0.2, 0.1] + [0.01, 1.0, 0.0001, 1.0] \approx [0.51, 1.3, -0.2, 1.1] \]

输入矩阵 \(X \in \mathbb{R}^{2 \times 4}\)(2个词,每个词4维):

\[ X = \begin{bmatrix} 0.2 & 0.9 & 0.4 & 1.3 \\ 0.51 & 1.3 & -0.2 & 1.1 \end{bmatrix} \]

第2步:Encoder Self-Attention

生成 Q, K, V(通过线性变换 \(W^Q, W^K, W^V \in \mathbb{R}^{4 \times 4}\)):

\[ Q = XW^Q, \quad K = XW^K, \quad V = XW^V \quad \in \mathbb{R}^{2 \times 4} \]

拆分为2个头(每个头 \(d_k = 2\)):

\[ Q = [Q_1 | Q_2], \quad K = [K_1 | K_2], \quad V = [V_1 | V_2] \]

其中 \(Q_1, K_1, V_1 \in \mathbb{R}^{2 \times 2}\)(取前2列),\(Q_2, K_2, V_2 \in \mathbb{R}^{2 \times 2}\)(取后2列)。

Head 1 的 Attention 计算

(a) 注意力分数:

\[ \text{score}_1 = \frac{Q_1 K_1^T}{\sqrt{d_k}} = \frac{Q_1 K_1^T}{\sqrt{2}} \in \mathbb{R}^{2 \times 2} \]

假设计算结果为:

\[ \text{score}_1 = \begin{bmatrix} 1.2 & 0.8 \\ 0.9 & 1.5 \end{bmatrix} \]

(b) Softmax(对每一行做归一化):

\[ \alpha_1 = \text{softmax}(\text{score}_1) = \begin{bmatrix} 0.60 & 0.40 \\ 0.35 & 0.65 \end{bmatrix} \]

如何理解注意力权重

第1行 \([0.60, 0.40]\) 表示"我"对自己关注60%,对"猫"关注40%。 第2行 \([0.35, 0.65]\) 表示"猫"对"我"关注35%,对自己关注65%。

(c) 加权求和:

\[ \text{head}_1 = \alpha_1 \cdot V_1 = \begin{bmatrix} 0.60 & 0.40 \\ 0.35 & 0.65 \end{bmatrix} \begin{bmatrix} v_{11} & v_{12} \\ v_{21} & v_{22} \end{bmatrix} \in \mathbb{R}^{2 \times 2} \]

Head 2 同理计算,得到 \(\text{head}_2 \in \mathbb{R}^{2 \times 2}\)

拼接 + 线性变换

\[ \text{MultiHead} = [\text{head}_1; \text{head}_2] \cdot W^O \in \mathbb{R}^{2 \times 4} \]

第3步:残差连接 + Layer Norm

\[ \text{out}_1 = \text{LayerNorm}(X + \text{MultiHead}) \]

对每个词向量(每一行):计算均值 \(\mu\) 和方差 \(\sigma^2\),然后归一化。

假设"我"经过 Attention 后的值为 \([0.3, 0.7, 0.5, 1.1]\),残差相加后:

\[ [0.2, 0.9, 0.4, 1.3] + [0.3, 0.7, 0.5, 1.1] = [0.5, 1.6, 0.9, 2.4] \]
\[ \mu = \frac{0.5 + 1.6 + 0.9 + 2.4}{4} = 1.35, \quad \sigma^2 = \frac{(0.5-1.35)^2 + \ldots + (2.4-1.35)^2}{4} = 0.5225 \]
\[ \text{LN}([0.5, 1.6, 0.9, 2.4]) = \gamma \odot \frac{[0.5, 1.6, 0.9, 2.4] - 1.35}{\sqrt{0.5225 + \epsilon}} + \beta \]

第4步:Feed-Forward Network

对每个词独立施加两层全连接:

\[ \text{hidden} = \text{ReLU}(x \cdot W_1 + b_1) \quad \in \mathbb{R}^{8} \quad \text{(升维 4→8)} \]
\[ \text{output} = \text{hidden} \cdot W_2 + b_2 \quad \in \mathbb{R}^{4} \quad \text{(降维 8→4)} \]

这就是两次普通的矩阵乘法,和 MLP 的前向传播完全一样。

第5步:再次残差 + Layer Norm

\[ \text{EncoderOutput} = \text{LayerNorm}(\text{out}_1 + \text{FFN}(\text{out}_1)) \in \mathbb{R}^{2 \times 4} \]

至此,Encoder 处理完毕。EncoderOutput 中的每个词向量都融合了全局上下文信息。

Encoder 的所有中间值都保存在内存中

与 RNN 一样(见 RNN 笔记),训练时前向传播的所有中间结果(\(Q, K, V\), attention 权重, FFN 中间层等)都需要保存,因为反向传播要用。这就是 Transformer 训练显存开销大的直接原因。

第6步:Decoder 输入

假设当前已生成 <BOS>,要预测下一个词 "I"。

Decoder 的输入同样经过 Embedding + Positional Encoding:

\[ y_1 = \text{Embed}(\text{<BOS>}) + PE_0 \]

第7步:Decoder Masked Self-Attention

与 Encoder 的 Self-Attention 相同,但加上因果掩码。由于当前只有一个词 <BOS>,掩码矩阵为 \(1 \times 1\),没有需要遮蔽的位置。

第8步:Decoder Cross-Attention

这一步是 Encoder 和 Decoder 的连接点

\[ Q = \text{Decoder当前输出}, \quad K = \text{EncoderOutput}, \quad V = \text{EncoderOutput} \]
  • \(Q \in \mathbb{R}^{1 \times 4}\)(Decoder 的1个词)
  • \(K, V \in \mathbb{R}^{2 \times 4}\)(Encoder 的2个词)

注意力分数 \(\in \mathbb{R}^{1 \times 2}\):模型决定生成当前目标词时应该"看"源语言的哪个词。

\[ \text{score} = \frac{QK^T}{\sqrt{d_k}} = \begin{bmatrix} 0.7 & 1.3 \end{bmatrix} \]
\[ \alpha = \text{softmax}([0.7, 1.3]) = [0.35, 0.65] \]

这表示生成 "I" 时,模型 35% 关注"我",65% 关注"猫"。(实际中应该更关注"我",这里只是示意数值。)

第9步:Decoder FFN + 输出

经过 FFN → 残差 + LayerNorm 后,Decoder 输出 \(h_1^{\text{dec}} \in \mathbb{R}^{4}\)

线性层 + Softmax

\[ \text{logits} = W_{\text{vocab}} \cdot h_1^{\text{dec}} + b \in \mathbb{R}^{V} \]
\[ P = \text{softmax}(\text{logits}) = [0.01, 0.02, \mathbf{0.85}, 0.05, 0.04, 0.03] \]

取概率最大的位置(index 2)→ 对应词表中的 "I"。

前向传播总结

Transformer 的前向传播本质上就是矩阵乘法的堆叠,和 MLP/CNN/RNN 没有本质区别:

组件 核心操作
Embedding 查表(矩阵行选取)
Self-Attention 三次线性变换 + 一次矩阵乘法(\(QK^T\))+ Softmax + 一次矩阵乘法(\(\alpha V\)
FFN 两次矩阵乘法 + ReLU
LayerNorm 逐元素归一化
Output 一次矩阵乘法 + Softmax

反向传播

损失函数

训练时使用交叉熵损失,对每个位置的预测计算 loss 后取平均:

\[ \mathcal{L} = -\frac{1}{T}\sum_{t=1}^{T}\log P(y_t | y_{<t}, x) \]

梯度如何流动

Transformer 的反向传播与普通深度网络的反向传播完全相同——都是链式法则。但由于 Transformer 的计算图比 RNN 复杂(有多头注意力、残差连接、Layer Norm 等),我们需要理解梯度经过每个模块时的行为。

梯度流动路径(从 Loss 往回看):

Loss
  ↓
Softmax + Linear (∂L/∂W_vocab)
  ↓
Decoder Layer N → ... → Decoder Layer 1
  ↓ (每层内部:FFN ← Cross-Attn ← Masked Self-Attn)
  ↓
Decoder Embedding
  ↓
同时,Cross-Attention 的 K,V 分支将梯度传回:
  ↓
Encoder Layer N → ... → Encoder Layer 1
  ↓ (每层内部:FFN ← Self-Attn)
  ↓
Encoder Embedding

各模块的梯度计算

1. Softmax + 交叉熵

\[ \frac{\partial \mathcal{L}}{\partial z_i} = P(y_i) - \mathbb{1}[y_i = y_{\text{true}}] \]

即预测概率减去 one-hot 标签,这是一个非常简洁的梯度。

2. 线性层

\[ \frac{\partial \mathcal{L}}{\partial W} = h^T \cdot \frac{\partial \mathcal{L}}{\partial z}, \quad \frac{\partial \mathcal{L}}{\partial h} = \frac{\partial \mathcal{L}}{\partial z} \cdot W^T \]

和 MLP 的全连接层反向传播完全一样。

3. Self-Attention 的梯度

这是 Transformer 反向传播中最复杂的部分。设 \(A = \text{softmax}(QK^T / \sqrt{d_k})\)

\[ \text{Output} = A \cdot V \]

梯度需要流过三条路径:

  • \(V\) 的梯度\(\frac{\partial \mathcal{L}}{\partial V} = A^T \cdot \frac{\partial \mathcal{L}}{\partial \text{Output}}\)
  • \(A\) 的梯度\(\frac{\partial \mathcal{L}}{\partial A} = \frac{\partial \mathcal{L}}{\partial \text{Output}} \cdot V^T\)
  • \(Q\)\(K\) 的梯度:需要经过 Softmax 的雅可比矩阵和缩放点积

最终梯度会传播到 \(W^Q, W^K, W^V\) 的参数上。

4. 残差连接的梯度——关键优势

\[ \text{output} = x + f(x) \implies \frac{\partial \text{output}}{\partial x} = 1 + \frac{\partial f}{\partial x} \]

残差连接为什么能解决梯度消失

梯度中始终有一个 +1 项。即使 \(\frac{\partial f}{\partial x}\) 趋近于 0,梯度仍然可以通过恒等映射(shortcut)传回去。这就是为什么 Transformer 可以堆叠几十甚至上百层而不会出现 RNN 那样的梯度消失。

对比 RNN 的 BPTT(见 RNN 笔记):RNN 的梯度是连乘 \(\prod W_{hh}\),一项小于1就指数衰减;而 Transformer 的梯度是连加 \(1 + \frac{\partial f}{\partial x}\),始终有通路。

5. Layer Normalization 的梯度

LayerNorm 的反向传播需要计算雅可比矩阵,涉及均值和方差对输入的依赖。核心公式:

\[ \frac{\partial \mathcal{L}}{\partial x_i} = \frac{\gamma}{\sqrt{\sigma^2 + \epsilon}} \left( \frac{\partial \mathcal{L}}{\partial \hat{x}_i} - \frac{1}{d}\sum_j \frac{\partial \mathcal{L}}{\partial \hat{x}_j} - \frac{\hat{x}_i}{d}\sum_j \frac{\partial \mathcal{L}}{\partial \hat{x}_j} \hat{x}_j \right) \]

这个公式看起来复杂,但本质上就是归一化操作的逆——把梯度从归一化后的空间映射回原始空间。

Transformer vs RNN 的反向传播对比

特性 RNN (BPTT) Transformer
梯度路径 沿时间轴顺序回传,\(O(n)\) 通过 Attention直接连接任意位置,\(O(1)\)
梯度消失 连乘\(\prod_{i} W_{hh} \cdot \text{diag}(\tanh')\) → 指数衰减 残差连接保证梯度通路,不会消失
参数更新 所有时间步共享\(U, V, W\),梯度累加 每层有独立参数,各层梯度独立计算
计算并行性 反向传播也必须顺序,\(O(n)\) 反向传播可并行,\(O(1)\) 顺序操作
显存占用 存储\(h_0, h_1, \ldots, h_T\)\(O(n \cdot d_h)\) 存储所有层的\(Q, K, V\), Attention 矩阵等,\(O(N \cdot n^2 + N \cdot n \cdot d)\)

Transformer 的显存瓶颈

虽然 Transformer 解决了梯度消失和并行性问题,但它引入了新的瓶颈:\(O(n^2)\) 的注意力矩阵。对于长度 \(n\) 的序列,每层每个头都要存储 \(n \times n\) 的注意力权重矩阵。这就是为什么处理超长文本非常吃显存——序列长度翻倍,显存翻4倍。


训练

Teacher Forcing

训练时,Decoder的输入是真实的目标序列(而非模型自己的预测),这叫Teacher Forcing:

  • 输入<BOS> I like cats(目标序列右移一位)
  • 标签I like cats <EOS>(目标序列)
  • 损失函数:对每个位置计算交叉熵损失,然后取平均
\[ \mathcal{L} = -\frac{1}{T}\sum_{t=1}^{T}\log P(y_t | y_{<t}, x) \]

Label Smoothing

原论文使用了 \(\epsilon_{\text{ls}} = 0.1\) 的标签平滑。不使用硬标签(one-hot),而是将正确类别的概率从1降为 \(1 - \epsilon\),剩余概率均分给其他类别:

\[ y_{\text{smooth}}(k) = \begin{cases} 1 - \epsilon + \epsilon/V & \text{if } k = y_{\text{true}} \\ \epsilon / V & \text{otherwise} \end{cases} \]

这防止模型过度自信,提升泛化能力。

学习率调度(Warm-up + Decay)

原论文使用了一个特殊的学习率策略:

\[ lr = d_{\text{model}}^{-0.5} \cdot \min\left(\text{step}^{-0.5},\ \text{step} \cdot \text{warmup\_steps}^{-1.5}\right) \]
  • \(\text{warmup\_steps}\)(论文中4000步):学习率线性增长
  • 之后:学习率按步数的平方根倒数衰减
lr ↑
   │      ╱╲
   │    ╱    ╲
   │  ╱        ╲
   │╱            ╲───────────────
   └──────────────────────────→ step
   0    4000
     warmup     decay

为什么需要warmup? 训练初期,模型参数是随机初始化的,梯度方向不可靠。如果一开始就用大学习率,模型可能"跑偏"到难以恢复的参数区域。先用小学习率"热身",让模型找到一个合理的参数区域后再加速。


参数量分析

以原论文的 Transformer Base 配置为例(\(d_{\text{model}}=512, h=8, d_{ff}=2048, N=6\)):

组件 参数量公式 数量
Token Embedding \(V \times d_{\text{model}}\) \(37000 \times 512 \approx 19M\)
每层Self-Attention \(4 \times d_{\text{model}}^2\) \(4 \times 512^2 = 1.05M\)
每层FFN \(2 \times d_{\text{model}} \times d_{ff}\) \(2 \times 512 \times 2048 = 2.10M\)
每层LayerNorm × 2 \(2 \times 2 \times d_{\text{model}}\) \(2048\)
Encoder(6层) \(6 \times (1.05M + 2.10M)\) \(\approx 18.9M\)
Decoder(6层,含Cross-Attn) \(6 \times (1.05M + 1.05M + 2.10M)\) \(\approx 25.2M\)
总计 \(\approx 63M\)

思考:现代大模型还是"预测下一个token"吗?

一个有意思的问题:当 ChatGPT 写出 "an" 的时候,它知道下一个词是 "apple" 吗?

短回答

是的,现代大模型(GPT-4、Claude 等)在架构层面依然是自回归的 next-token prediction。 写出 "an" 的时候,模型并不"知道"下一个一定是 "apple"——它只是在词表上算出一个概率分布,"apple" 的概率很高而已。模型没有一个"草稿本"提前把整句话写好再逐字输出。

但"只是预测下一个token"≠"没有规划能力"

虽然输出机制是逐 token 的,但模型的内部表示(隐藏层状态)可能已经隐式编码了对后续多个 token 的"预期"。研究表明,Transformer 中间层的隐状态中可以探测到未来 token 的信息。也就是说:

  • 输出层面:一次只产生一个 token
  • 内部层面:隐藏状态中可能已经"规划"了接下来好几步要说什么

这就像一个人说话时,嘴巴一次只能说一个字,但大脑里可能已经组织好了整句话。

超越 next-token prediction 的新技术

虽然底层架构没变,但训练方法和推理策略已经有了重大演进:

技术 原理 是否改变了架构
Chain-of-Thought (CoT) 让模型输出中间推理步骤,用"自言自语"辅助思考 否,仍是 next-token,但用输出 token 充当"工作记忆"
RLHF / RLAIF 用人类反馈训练模型偏好,优化目标从"预测准确"变为"回答有用" 否,改变了训练目标,但推理仍是 next-token
Reasoning Models(如 OpenAI o1/o3) 生成大量隐藏的"思考 token",对答案进行搜索和验证 否,本质还是 next-token,但推理时分配了大量计算用于"思考"——类似在脑子里打草稿
Multi-Token Prediction 训练时同时预测未来\(k\) 个 token(Meta, 2024) ,修改了训练目标。每一步预测 4-8 个未来 token,使模型学到更长远的规划
Speculative Decoding 小模型先"猜"多个 token,大模型一次性验证 否,但推理速度提升 2-3 倍
Diffusion LM 非自回归,像图像扩散模型一样同时生成所有 token ,完全不同的范式,但目前效果尚不及自回归模型

核心洞察

截至 2025 年,主流大模型的架构底座仍然是 Transformer + 自回归。真正改变的是:

1.训练方法:从单纯的语言建模(predict next token)→ RLHF/DPO → 推理强化学习 2. 推理策略:从贪心/采样生成 → Chain-of-Thought → 搜索 + 验证(test-time compute scaling) 3. 规模:从 63M(原论文)→ 数千亿参数

"predict next token" 看似简单,但当模型足够大、数据足够多、训练方法足够好时,这个简单的目标函数能涌现出令人惊讶的能力。


与 CNN、RNN 的全面对比

特性 CNN RNN Transformer
核心操作 卷积(局部窗口) 循环(逐步处理) 自注意力(全局交互)
归纳偏置 局部性 + 平移不变性 时间依赖性 + 时间平移不变性 几乎无(靠数据学习)
并行性 高(不同位置独立卷积) 低(\(h_t\) 依赖 \(h_{t-1}\) 高(所有位置同时计算注意力)
长距离依赖 需要多层堆叠,\(O(\log n)\)\(O(n/k)\) 理论上可以,但梯度消失 \(O(1)\),任意两个位置直接相连
计算复杂度(每层) \(O(k \cdot n \cdot d^2)\) \(O(n \cdot d^2)\) \(O(n^2 \cdot d)\)
适用场景 图像、局部模式 短序列、流式数据 长序列、需要全局理解的任务
参数量与序列长度 无关 无关 无关(但注意力矩阵占显存\(O(n^2)\)

计算复杂度的权衡

\(n < d\)(序列长度小于特征维度)时,Self-Attention 比 RNN 更快;当 \(n > d\) 时(超长序列),Self-Attention 的 \(O(n^2)\) 成为瓶颈。这催生了各种高效 Attention 变体(Linear Attention、Sparse Attention 等)。


大模型

Scaling Law

Scaling Laws 的发现(最早由 OpenAI 在 2020 年系统提出)指出:模型的最终性能(通常用 Loss 衡量)主要取决于三个要素: 参数量 (\(N\))训练数据量 (\(D\))计算量 (\(C\)) 。当这三个要素不受限制时,Loss 会随着这三个维度的增加而呈 幂律下降 :

\[ L(N, D) \approx \left( \frac{N_c}{N} \right)^{\alpha_N} + \left( \frac{D_c}{D} \right)^{\alpha_D} \]
  • \(L\) :模型在测试集上的交叉熵损失(可以理解为预测的"准确度")。
  • \(N\) :模型有效参数量(雕刻刀的精细度)。
  • \(D\) :训练数据集的大小(见识的广度)。
  • \(\alpha_N, \alpha_D\) :幂律指数。在 Transformer 架构下,这些系数表现出惊人的稳定性。
  • 计算量关系 :通常 \(C \approx 6ND\)(前向传播 + 反向传播的总浮点运算量)。

三大要素的底层原理:

维度 原理理解 对应你的"空间折叠"理论
参数量 (\(N\)) 空间分辨率 参数量越多,意味着在高维空间里用于"对折"的关节越多,能够雕刻出的分割面(Decision Boundary)越精细。
数据量 (\(D\)) 流形覆盖度 数据量决定了模型对高维知识空间(Manifold)的覆盖率。没有足够的数据,再细的雕刻刀也只能在局部区域反复折叠,导致过拟合。
计算量 (\(C\)) 折叠的动力 算力是执行梯度下降、寻找最优折叠方案的能源。算力保证了模型能从混乱状态(随机初始化)抵达那个万能逼近预言的"完美参数点"。

Scaling Laws带来的三大工程启示:

  1. 架构不重要,规模才重要。只要是基于 Transformer 的前馈结构,具体的微调(比如头的个数、残差的细节)对性能的影响远小于规模增加带来的收益。
  2. 算力分配的最佳比例 (Chinchilla Optimality):如果你的算力翻倍,你应该同比例地增加参数量和数据量。而不是只加参数(会导致模型"虚胖",见多识广但没记牢)。
  3. 性能的可预测性:你可以在模型还没训练出来之前,先在小模型上试一下,然后精准预测出投 1 亿美元训练出来的大模型 Loss 到底是多少。

Scaling Laws 描述的是拟合精度的提升,而非逻辑本质的突变。虽然增加 \(N\)\(D\) 可以无限逼近数据的分布(万能逼近定理),但如果数据的分布本身存在质量上限(比如互联网上充斥着垃圾信息),模型也只能学会'平庸的规律'。此外,Scaling Laws 目前主要是在语言建模(Next Token Prediction)上表现稳定,在复杂的推理链条(Reasoning)自我纠错上,我们可能还需要在架构上引入类似强化学习(RLHF/Search)的新维度,而不仅仅是堆规模。

自回归

KV-Cache

KV-cache 是专门为了服务于大语言模型的自回归(Auto-regressive)推理过程而诞生的技术,是大语言模型(如 GPT、LLaMA)在推理(Inference)阶段用来加速生成速度、减少重复计算的一种核心技术手段。

简单来说,它是模型为了"不把算过的东西重算一遍"而建立的一个临时存储区。

在 Transformer 的注意力机制中,每个词都会生成三个向量:

  • Q (Query) :当前的查询(我是谁?我要找什么?)。
  • K (Key) :索引标签(我的特征是什么?)。
  • V (Value) :实际内容(我的含义是什么?)。

比如模型要生成句子 "我 爱 学习"

  1. 第1步 :输入 "我",模型算出 "爱"。
  2. 第2步 :模型把 "我" 和 "爱" 放在一起看,算出 "学习"。
  3. 第3步 :模型把 "我"、"爱"、"学习" 放在一起看,算出下一个词(比如句号)。

如果没有 KV-cache,第3步会发生什么?

  1. 模型拿到 "我、爱、学习"。
  2. 模型重新计算 "我" 的 K 和 V。
  3. 模型重新计算 "爱" 的 K 和 V。
  4. 模型计算新词 "学习" 的 K、V 和 Q。
  5. 然后大家一起做注意力运算。

问题: "我" 和 "爱" 的 K 和 V,在第1步和第2步早就又算过一遍了!它们是固定的, 重复计算它们是在浪费时间(GPU 算力)

KV-cache 的逻辑是:算过的就存起来(Cache),下次直接用。带 KV-cache 的流程:

  • 第1步(生成"爱"):
    • 计算 "我" 的 \(K_{1}, V_{1}\)
    • 【存储】\(K_{1}, V_{1}\) 存入显存(这就是 KV-cache)。
    • 输出 "爱"。
  • 第2步(生成"学习"):
    • 输入新词 "爱"。
    • 只计算 "爱" 的 \(K_{2}, V_{2}\)\(Q_{2}\)
    • 【读取】 从显存里把 "我" 的 \(K_{1}, V_{1}\) 拿出来。
    • 【拼接】 把旧的 \(K, V\) 和新的 \(K, V\) 拼在一起。
    • 做注意力运算,输出 "学习"。
    • 【存储】\(K_{2}, V_{2}\) 也存入 KV-cache。
  • 第3步(生成"。"):
    • 输入新词 "学习"。
    • 只计算 "学习" 的 \(K_{3}, V_{3}\)\(Q_{3}\)
    • 【读取】 拿之前的 \(K_{1}, V_{1}, K_{2}, V_{2}\)
    • ...以此类推。

模型不需要重新"阅读"原文,只要拿着这些 KV 向量,就拥有了对过去所有对话的"记忆"。因此KV-Cache实际上是一种"工作时记忆"。在具身智能笔记中介绍世界模型的时候我们提到过,KV-Cache的局限性在于线性增长和显存爆炸:1000个词的对话,就要存1000组 KV;10万个词,就要存10万组。

ViT

Time Series Transformers


评论 #