跳过正文

Transformer初探

·5434 字·11 分钟
AI 深度学习 大模型
作者
马斯陶
前TGUCSDN社团主席 现北邮深度学习开发者
目录

终于到重头戏了,Transformer 打算写三个文档:本篇是第一篇,讲 transformer 的原理和实现代码;第二个讲 工程上的优化方案,重点是 KV Cache 等;第三个是更前沿的原创思路,如何搞出一套(可能的)更合理的优化 Transformer 的方案。

原始的 Transformer 网上有很多详细的教学,建议首先去看看 CSDN 播客上对原始 Transformer 的讲解,弄清楚基本原理之后再看这一部分,此处不对原始 Transformer 做过分详细的讲解,而对更深入的细节进行讲解。

1. encoder 中的多头自注意力机制

本章节假设你已经看完了网上的基础讲解,基本理解了 Transformer 的运行过程。在此处为了加深你的理解,我直接给出一个实际的例子,展示一句话输入到 Transformer 网络之后的全流程,以及过程中各种参数的 shape 和信息流动情况。

假设你输入的话是“我是天才”。

1.1 输入嵌入(Input Embedding)

首先,将输入的词转换为向量表示:

  • 假设使用的嵌入维度是 d_model = 512

  • 输入序列:“我”、“是”、“天才”(3个token)

  • 每个词被转换为一个512维的向量

  • 得到一个输入矩阵X,形状为[3, 512]

1.2 位置编码(Positional Encoding)

为了让模型了解词的位置信息:

  • 生成位置编码向量(维度同样是512)

  • 将位置编码与词嵌入相加

  • 得到的仍是一个[3, 512]的矩阵

1.3 多头自注意力机制(Multi-Head Self-Attention)

1.3.1 生成查询(Q)、键(K)、值(V)矩阵

对于每个注意力头,有三个不同的权重矩阵:

  • W^Q:查询权重矩阵,形状[512, 64]

  • W^K:键权重矩阵,形状[512, 64]

  • W^V:值权重矩阵,形状[512, 64]

设有8个注意力头,那么每个头的维度是 d_k = d_v = 512/8 = 64。

Q = X × W^Q  # 形状: [3, 64]
K = X × W^K  # 形状: [3, 64]
V = X × W^V  # 形状: [3, 64]

1.3.2 计算注意力分数

对于每个注意力头执行:

  1. 计算Q与K的转置的点积: Q@K^T,形状[3, 3]

    • 这个矩阵表示每个词对其他词的相关性

    • [0,0]位置表示"我"对"我"的注意力分数

    • [0,1]位置表示"我"对"是"的注意力分数

  2. 缩放:除以sqrt(d_k),即除以8

    • Scaled_QK^T = QK^T/√64

    • 这一步是为了防止点积变得过大,导致softmax梯度消失

  3. 应用softmax使每一行的权重之和为1

    • Attention_weights = softmax(Scaled_QK^T),形状[3, 3] <– 注意力矩阵就是在这一步得到的。
  4. 与V相乘得到加权值向量

    • head_i = Attention_weights × V,形状[3, 64]

1.3.3 多头机制的合并

  1. 所有8个头的输出并列在一起

    • Concat(head_1,…,head_8),形状[3, 512]
  2. 通过一个线性变换W^O(形状[512, 512])投影回原始维度

    • MultiHead = Concat(head_1,…,head_8) × W^O,形状[3, 512]
  3. 添加与标准化(Add & Norm)

  4. 残差连接:将多头注意力的输出与输入相加

    • X’ = X + MultiHead(X),形状[3, 512]
  5. 层标准化:对每个位置的特征向量进行标准化

    • LayerNorm(X’),形状不变,仍为[3, 512]
  6. 前馈神经网络(Feed Forward Network)

  7. 两层线性变换,中间有ReLU激活函数

    • FFN(x) = max(0, x×W_1+b_1)×W_2+b_2

    • W_1形状[512, 2048],W_2形状[2048, 512]

  8. 残差连接:将FFN的输出与输入相加

    • X’’ = X’ + FFN(X’),形状[3, 512]
  9. 层标准化

    • LayerNorm(X’’),形状仍为[3, 512]

单独追踪"我"这个词在自注意力机制中的处理:

  1. “我"的嵌入向量(512维)经过映射得到Q、K、V向量(各64维)

  2. 计算"我"对{“我”,“是”,“天才”}三个词的注意力分数

  3. 通过softmax得到注意力权重,如[0.6, 0.2, 0.2]

  4. 这些权重与对应的V值向量相乘并加权求和

  5. 最后得到"我"在当前注意力头的输出向量(64维)

  6. 8个注意力头的结果合并后得到一个512维的向量

2. 为什么最后输出的时候需要 W^O 投影矩阵?

这其实是为了融合不同的头的信息。如果直接拼接8个头的输出,只是简单地把它们放在一起,但没有"混合"这些信息。另外W^O 矩阵(形状为[512, 512]),这也是一个可以学习的参数,它允许模型学习如何融合这8个头,可以捕获不同类型的关系和模式。

如果你之前学过图像处理,其实Transformer 分头的机制就类似于图像处理中的通道。每个头可能关注输入序列的不同方面,W^O帮助模型整合这些不同视角

3. 多头注意力机制的并行计算

3.1 每个头的独立权重矩阵

对于8个注意力头,有8组完全独立的权重矩阵:

  • 每个头有自己专属的W^Q, W^K, W^V三个权重矩阵

  • 对于头i,它的权重矩阵是W^Q_i, W^K_i, W^V_i

  • 总共有8×3=24个权重矩阵

  • 每个W^Q_i, W^K_i, W^V_i的形状都是[512, 64]

3.2 并行的注意力计算流程

  1. 头i使用自己的权重矩阵计算查询、键、值:

    • Q_i = X × W^Q_i # [3, 64]

    • K_i = X × W^K_i # [3, 64]

    • V_i = X × W^V_i # [3, 64]

  2. 头i计算自己的注意力分数:

    • Score_i = Q_i × K_i^T / √64 # [3, 3]
  3. 头i应用softmax得到注意力权重:

    • Weights_i = softmax(Score_i) # [3, 3]
  4. 头i生成自己的输出:

    • head_i = Weights_i × V_i # [3, 64]

多头融合机制

  1. 拼接8个头的输出:

    • Concat(head_1,…,head_8) # [3, 512]
  2. 通过投影矩阵W^O融合不同头捕获的信息:

    • MultiHead = Concat(head_1,…,head_8) × W^O # [3, 512]

也就是说:当一个 3*512 的向量矩阵输入时: 1. 每一个头中的QKV 的参数矩阵都是 512*64 大小,将输入向量变成从 3*512 变成 3*64; 2. 对于 3*64 的向量,计算注意力权重,完成 self attention; 3. 最后得到的是 8 个 364 的向量矩阵,然后用一个 dense 层将他们合并融合

4. Decoder的详细结构

4.1 Decoder的基本结构

Decoder也由多个相同的层堆叠而成,但每一层包含三个主要子层:

  • 掩码多头自注意力(Masked Multi-Head Self-Attention)

  • 交叉注意力(Cross-Attention)

  • 前馈神经网络(Feed-Forward Network)

每个子层后都有残差连接和层标准化,与Encoder类似。

4.2 掩码多头自注意力层

在训练时,为了防止"作弊”,添加了一个掩码操作。掩码确保位置i只能注意到位置0到i的信息,不能看到未来的信息。

在代码的具体实现中,一般是在注意力分数矩阵中,将右上三角部分设为-∞(或很大的负数)

比如:

如果生成到"我是",计算"是"的表示时:

  • 注意力矩阵:[ [1, 0], # “我"只能看到"我” [a, 1] # “是"可以看到"我"和"是” ]

  • 其中a表示"是"对"我"的注意力权重

4.3 交叉注意力层(Encoder-Decoder Attention)

这是Decoder独有的部分,学习的时候注意对比和 Self Attention 的区别:

  • Q: 来自Decoder第一层的输出

  • K, V: 来自Encoder最后一层的输出

  • 这使得Decoder能够关注输入序列的相关部分

假设我们想翻译"我是天才"为英文:

  1. Encoder处理"我是天才",输出形状为[3, 512]的矩阵

  2. Decoder在生成每个英文词时:

    • Q来自Decoder当前状态

    • K,V来自Encoder的[3, 512]输出

    • 这样Decoder能决定生成每个英文词时应该关注中文序列的哪些部分

  3. 形状变化

假设我们已经生成了2个词的输出序列:

  • Decoder输入:[2, 512] (已生成的2个词)

  • 掩码自注意力输出:仍为[2, 512]

  • 交叉注意力输入:

    • Q:来自Decoder,形状[2, 512]

    • K,V:来自Encoder,形状[3, 512]

  • 交叉注意力输出:形状[2, 512]

  • 最终输出:形状[2, 512]

5. Decoder中掩码机制详解

首先明确一点:W_Q, W_K, W_V参数矩阵的结构和处理方式与Encoder中完全相同。区别不在参数矩阵上

掩码是在注意力分数计算完成后、应用softmax之前,添加在注意力矩阵上面的:

  1. 正常计算QK^T

    • 和Encoder一样,计算Q×K^T/√d_k得到注意力分数矩阵

    • 假设我们生成到第三个词,这个矩阵是3×3的

  2. 添加掩码操作

  • 创建一个掩码矩阵(下三角矩阵),形状与注意力分数矩阵相同

  • 掩码矩阵的下三角(包括对角线)为0,上三角为-∞(实践中通常是-10000)

掩码矩阵 = [
  [0, -∞, -∞],
  [0,  0, -∞],
  [0,  0,  0]
]

应用掩码

  • 将掩码矩阵与注意力分数矩阵相加

  • 结果是上三角部分变为-∞

掩码后的分数 = [
  [score(1,1),       -∞,       -∞],
  [score(2,1), score(2,2),       -∞],
  [score(3,1), score(3,2), score(3,3)]
]

应用softmax

  • 对每一行应用softmax

  • 由于exp(-∞)≈0,上三角部分的权重几乎为0

  • 确保位置i只关注位置1到i的信息

假设我们在生成"I am"之后想生成第三个词:

  1. 输入到Decoder的是[“I”, “am”]

  2. 计算自注意力时:

    • “I"只能关注"I”

    • “am"可以关注"I"和"am”

  3. 生成第三个词时(假设是"a"):

    • 将[“I”, “am”, “a”]输入Decoder

    • “a"可以关注"I”、“am"和"a”

    • 但根本不会有第四个词的信息,因为还没生成

训练与推理的区别

  • 训练时:目标序列是已知的,整个序列一次性输入,但通过掩码确保每个位置只看到之前的词

  • 推理时:真正的自回归过程,逐词生成,每生成一个词就添加到输入序列中

掩码机制+自回归生成确保了"只能看见已生成的内容":

  • 掩码保证了位置i只能关注位置1到i

  • 自回归生成确保每次只生成一个新词,然后将其加入输入序列

是掩码操作(而非参数矩阵)保证了Decoder的因果性,使其只能"看到"已经生成的内容。

6. Decoder与Encoder的主要区别

掩码机制:Decoder使用掩码自注意力,Encoder使用完全自注意力

交叉注意力:Decoder有额外的交叉注意力层,连接Encoder和Decoder

自回归特性:Decoder是自回归的,逐词生成输出

Decoder最后接一个线性层和softmax,预测下一个词。

自回归生成过程

  1. 输入起始标记(通常是<START>)

  2. 生成第一个词,添加到输入序列

  3. 输入[<START>, 词1],生成第二个词

  4. 输入[<START>, 词1, 词2],生成第三个词

  5. 重复直到生成结束标记<END>

7. Transformer中的训练与推理差异

看到这里,你有没有感到困惑?这里额外详细讲一下模型训练的时候和推理的时候的差别:

Decoder训练流程

训练时,我们一次性处理整个目标序列:

  1. 数据准备
  • 输入序列:如"我是天才"(经过Encoder处理)

  • 目标序列:如"I am a genius"

  • 处理目标序列:

    • 输入版本:[<START>, “I”, “am”, “a”, “genius”]

    • 输出版本:[“I”, “am”, “a”, “genius”, <END>]

  • 一次性前向传播
  • Decoder接收整个输入版本[<START>, “I”, “am”, “a”, “genius”]

  • 重点:这是一次性输入,不是逐个词输入

  • 掩码自注意力机制
  • 计算完整的Q、K、V矩阵,形状[5, 512](5个词)

  • 计算原始注意力分数矩阵QK^T,形状[5, 5]

  • 应用掩码,得到下三角注意力矩阵

[[1, 0, 0, 0, 0],  # <START>只能看到自己 
[?, 1, 0, 0, 0],  # "I"看到<START>和自己 
[?, ?, 1, 0, 0],  # "am"看到<START>、"I"和自己 
[?, ?, ?, 1, 0],  # "a"看到前面所有和自己 
[?, ?, ?, ?, 1]]  # "genius"看到前面所有和自己
  • 其中?表示学习到的注意力权重
  • 并行预测下一个词
  • Decoder输出形状为[5, vocab_size]的矩阵

  • 第0行预测"I",第1行预测"am",第2行预测"a"…

  • 在这个过程中,对所有位置同时进行预测

  • 计算损失和更新参数
  • 计算所有位置预测与真实下一个词的交叉熵损失

  • 一次性反向传播更新所有参数

Decoder推理流程

推理时,我们必须逐步生成每个词:

  1. 第一步
  • 输入:仅[<START>]

  • Decoder前向传播(只有一个词,掩码不起作用)

  • 预测概率最高的词:“I”

  • 第二步
  • 输入:[<START>, “I”]

  • 完全重新运行Decoder前向传播

  • 注意力矩阵现在是2×2的:

[[1, 0],  # <START>只看自己 
[?, 1]]  # "I"看到<START>和自己
  • 预测第二个词:“am”
  • 第三步
  • 输入:[<START>, “I”, “am”]

  • 再次完全重新运行Decoder前向传播

  • 注意力矩阵现在是3×3的

  • 预测第三个词:“a”

依此类推…

  1. 训练时
  • 一次性输入整个序列

  • 通过掩码确保因果关系

  • 并行计算所有位置的预测

  • 一次前向传播完成所有预测

  • 推理时
  • 逐步构建输入序列

  • 每预测一个词就重新运行一次完整的Decoder

  • 序列逐渐变长,注意力矩阵也逐渐变大

  • 需要多次前向传播

这篇属于 Transformer 的基础篇,希望能帮助你搞清楚 Transformer 的结构和流程,特别是在训练和推理的时候 Decoder 的不同的表现。这一篇哪怕是前端同学李正也能轻而易举的弄懂,但是下一篇 KV Cache 就不一定了,打好基础再继续前进。