终于到重头戏了,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 计算注意力分数
对于每个注意力头执行:
-
计算Q与K的转置的点积: Q@K^T,形状[3, 3]
-
这个矩阵表示每个词对其他词的相关性
-
[0,0]位置表示"我"对"我"的注意力分数
-
[0,1]位置表示"我"对"是"的注意力分数
-
-
缩放:除以sqrt(d_k),即除以8
-
Scaled_QK^T = QK^T/√64
-
这一步是为了防止点积变得过大,导致softmax梯度消失
-
-
应用softmax使每一行的权重之和为1
- Attention_weights = softmax(Scaled_QK^T),形状[3, 3] <– 注意力矩阵就是在这一步得到的。
-
与V相乘得到加权值向量
- head_i = Attention_weights × V,形状[3, 64]
1.3.3 多头机制的合并
-
所有8个头的输出并列在一起
- Concat(head_1,…,head_8),形状[3, 512]
-
通过一个线性变换W^O(形状[512, 512])投影回原始维度
- MultiHead = Concat(head_1,…,head_8) × W^O,形状[3, 512]
-
添加与标准化(Add & Norm)
-
残差连接:将多头注意力的输出与输入相加
- X’ = X + MultiHead(X),形状[3, 512]
-
层标准化:对每个位置的特征向量进行标准化
- LayerNorm(X’),形状不变,仍为[3, 512]
-
前馈神经网络(Feed Forward Network)
-
两层线性变换,中间有ReLU激活函数
-
FFN(x) = max(0, x×W_1+b_1)×W_2+b_2
-
W_1形状[512, 2048],W_2形状[2048, 512]
-
-
残差连接:将FFN的输出与输入相加
- X’’ = X’ + FFN(X’),形状[3, 512]
-
层标准化
- LayerNorm(X’’),形状仍为[3, 512]
单独追踪"我"这个词在自注意力机制中的处理:
“我"的嵌入向量(512维)经过映射得到Q、K、V向量(各64维)
计算"我"对{“我”,“是”,“天才”}三个词的注意力分数
通过softmax得到注意力权重,如[0.6, 0.2, 0.2]
这些权重与对应的V值向量相乘并加权求和
最后得到"我"在当前注意力头的输出向量(64维)
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 并行的注意力计算流程
-
头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]
-
-
头i计算自己的注意力分数:
- Score_i = Q_i × K_i^T / √64 # [3, 3]
-
头i应用softmax得到注意力权重:
- Weights_i = softmax(Score_i) # [3, 3]
-
头i生成自己的输出:
- head_i = Weights_i × V_i # [3, 64]
多头融合机制
-
拼接8个头的输出:
- Concat(head_1,…,head_8) # [3, 512]
-
通过投影矩阵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能够关注输入序列的相关部分
假设我们想翻译"我是天才"为英文:
-
Encoder处理"我是天才",输出形状为[3, 512]的矩阵
-
Decoder在生成每个英文词时:
-
Q来自Decoder当前状态
-
K,V来自Encoder的[3, 512]输出
-
这样Decoder能决定生成每个英文词时应该关注中文序列的哪些部分
-
-
形状变化
假设我们已经生成了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之前,添加在注意力矩阵上面的:
-
正常计算QK^T:
-
和Encoder一样,计算Q×K^T/√d_k得到注意力分数矩阵
-
假设我们生成到第三个词,这个矩阵是3×3的
-
-
添加掩码操作:
-
创建一个掩码矩阵(下三角矩阵),形状与注意力分数矩阵相同
-
掩码矩阵的下三角(包括对角线)为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"之后想生成第三个词:
输入到Decoder的是[“I”, “am”]
计算自注意力时:
“I"只能关注"I”
“am"可以关注"I"和"am”
生成第三个词时(假设是"a"):
将[“I”, “am”, “a”]输入Decoder
“a"可以关注"I”、“am"和"a”
但根本不会有第四个词的信息,因为还没生成
训练与推理的区别
-
训练时:目标序列是已知的,整个序列一次性输入,但通过掩码确保每个位置只看到之前的词
-
推理时:真正的自回归过程,逐词生成,每生成一个词就添加到输入序列中
掩码机制+自回归生成确保了"只能看见已生成的内容":
-
掩码保证了位置i只能关注位置1到i
-
自回归生成确保每次只生成一个新词,然后将其加入输入序列
是掩码操作(而非参数矩阵)保证了Decoder的因果性,使其只能"看到"已经生成的内容。
6. Decoder与Encoder的主要区别
掩码机制:Decoder使用掩码自注意力,Encoder使用完全自注意力
交叉注意力:Decoder有额外的交叉注意力层,连接Encoder和Decoder
自回归特性:Decoder是自回归的,逐词生成输出
Decoder最后接一个线性层和softmax,预测下一个词。
自回归生成过程
-
输入起始标记(通常是<START>)
-
生成第一个词,添加到输入序列
-
输入[<START>, 词1],生成第二个词
-
输入[<START>, 词1, 词2],生成第三个词
-
重复直到生成结束标记<END>
7. Transformer中的训练与推理差异
看到这里,你有没有感到困惑?这里额外详细讲一下模型训练的时候和推理的时候的差别:
Decoder训练流程
训练时,我们一次性处理整个目标序列:
- 数据准备
-
输入序列:如"我是天才"(经过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推理流程
推理时,我们必须逐步生成每个词:
- 第一步
-
输入:仅[<START>]
-
Decoder前向传播(只有一个词,掩码不起作用)
-
预测概率最高的词:“I”
- 第二步
-
输入:[<START>, “I”]
-
完全重新运行Decoder前向传播
-
注意力矩阵现在是2×2的:
[[1, 0], # <START>只看自己
[?, 1]] # "I"看到<START>和自己
- 预测第二个词:“am”
- 第三步
-
输入:[<START>, “I”, “am”]
-
再次完全重新运行Decoder前向传播
-
注意力矩阵现在是3×3的
-
预测第三个词:“a”
依此类推…
- 训练时:
一次性输入整个序列
通过掩码确保因果关系
并行计算所有位置的预测
一次前向传播完成所有预测
- 推理时:
逐步构建输入序列
每预测一个词就重新运行一次完整的Decoder
序列逐渐变长,注意力矩阵也逐渐变大
需要多次前向传播
这篇属于 Transformer 的基础篇,希望能帮助你搞清楚 Transformer 的结构和流程,特别是在训练和推理的时候 Decoder 的不同的表现。这一篇哪怕是前端同学李正也能轻而易举的弄懂,但是下一篇 KV Cache 就不一定了,打好基础再继续前进。