上一课中,分词器把文本变成 Token,Embedding 层又把 Token 转成向量。接下来,模型怎样知道不同位置之间有什么关系?例如,一句话中的代词需要联系前面的人名,模型就需要从其他位置取得相关信息。
Transformer 使用注意力完成位置之间的信息汇总,再用前馈网络处理每个位置的表示。残差连接与归一化把这些计算组织成一层,我们理解一层后,就能继续分析多层模型。原始论文
Q、K、V 从输入投影出来
我们设输入矩阵 X 包含 T 个位置,每个位置有 d_model 维特征。模型通过三组可学习投影得到 Q、K、V:
Q = X W_Q T × d_k
K = X W_K T × d_k
V = X W_V T × d_v
读者可以把 Q 理解为当前位置发出的查询,把 K 理解为供查询比较的键,把 V 理解为匹配后准备汇总的内容。模型先比较 Q 与 K,再根据匹配结果汇总 V。自注意力的三者来自同一组输入,但通常使用不同投影参数;交叉注意力则可以从另一组表示产生 K 和 V。
一行权重对应一个查询位置
S = Q K^T / sqrt(d_k)
A = softmax(S + mask_bias)
Y = A V
计算得到的 S 是一个 T×T 矩阵。它的第 i 行表示位置 i 对各个键的匹配分数。Softmax 沿这一行归一化后,模型就得到一组加权系数,再用它们汇总 V。公式中的缩放项用于控制点积分数的尺度。
下面的程序使用固定投影矩阵实现单头注意力。我们可以观察权重和是否为 1,因果掩码是否屏蔽未来,以及输入重排后输出怎样变化。
import numpy as np
x = np.array([[1., 0., 1., 0.], [0., 1., 0., 1.], [1., 1., 0., 0.]])
wq = np.array([[1., 0.], [0., 1.], [0.5, 0.], [0., 0.5]])
wk = np.array([[1., 0.], [0., 1.], [0., 0.5], [0.5, 0.]])
wv = np.array([[1., 0.], [0., 1.], [1., 0.], [0., 1.]])
def attention(values, causal=False):
q, k, v = values @ wq, values @ wk, values @ wv
scores = q @ k.T / np.sqrt(q.shape[-1])
allowed = np.ones_like(scores, dtype=bool)
if causal:
allowed = np.tril(allowed)
if not allowed.any(axis=-1).all():
raise ValueError("a query has no allowed key")
scores = np.where(allowed, scores, -np.inf)
weights = np.exp(scores - scores.max(axis=-1, keepdims=True))
weights /= weights.sum(axis=-1, keepdims=True)
return weights @ v, weights
y, weights = attention(x)
causal_y, causal_weights = attention(x, causal=True)
assert y.shape == causal_y.shape == (3, 2)
np.testing.assert_allclose(weights.sum(axis=-1), 1)
np.testing.assert_allclose(causal_weights[np.triu_indices(3, k=1)], 0)
np.testing.assert_allclose(causal_y[0], (x @ wv)[0])
order = np.array([2, 0, 1])
reordered_y, _ = attention(x[order])
np.testing.assert_allclose(reordered_y, y[order])
print("attention shapes, mask and permutation checked")
最后一个断言说明,这个没有位置信息、没有因果掩码的计算,在输入重排后会相应重排输出。模型若要利用文本的先后关系,还需要让位置信息参与计算,这也是位置编码存在的原因。
多头、位置和掩码
多头注意力让模型在不同投影空间分别完成上述计算,再拼接各头结果,并通过输出投影恢复所需维度。我们读实现时,可以依次写出批次、头、位置与特征四个轴,再跟踪 reshape 和 transpose 怎样排列它们。
位置信息可以通过向量相加、RoPE 等方式进入模型。某种方法能够计算更长位置的编码,说明表示在数值上有了定义;模型是否能可靠理解这些长上下文,仍需要实际评估。
掩码负责限制信息流向。因果掩码屏蔽未来位置,Padding 掩码屏蔽补齐位置。如果某个查询的所有键都被屏蔽,程序需要单独处理,避免产生非法数值。框架接口对布尔值的含义可能不同,我们应核对 SDPA 与 MultiheadAttention 的具体定义。
从注意力接到完整模型
注意力完成跨位置的信息汇总后,逐位置前馈网络通常会执行两次线性映射和中间激活。残差支路把输入与子层结果相加,因此两者需要尺寸匹配。Pre-Norm 与 Post-Norm 将归一化放在不同位置,研究者复现模型时应保持相应结构。
完整 Transformer 还有几种常见组织方式。Encoder-only 常用于理解任务,Decoder-only 常用于自回归生成,Encoder–Decoder 则通过交叉注意力联系输入与输出。Decoder-only 没有独立 Encoder 时,也就不必包含从 Encoder 读取信息的交叉注意力。
语言模型最后通过输出头产生词表 Logits,再使用上一课介绍的后继 Token 目标训练。计算成本也随结构变化:密集注意力分数矩阵有 T² 个位置,整层还包含投影和前馈,运行时还有缓存。因此,我们估算长序列成本时需要一起考虑这些部分。
练习:读者可以手算一行注意力,解释 Q 与 K 的维度为什么要匹配。请再故意写反因果掩码,观察第一个位置能否读取未来信息,并用这个反例解释正确掩码的作用。
读者完成前两篇后,可以继续学习文本检索与 RAG,也可以直接进入SFT、LoRA 与 DPO。应用与训练是两条可分别阅读的路线。
资料:疏锦行大模型专题《Transformer 架构解读》,讲义第 7–22、29–34、72–82、92–94、102 页。单头练习已运行,MoE、KV Cache 实现等扩展尚未展开。
大模型 02:把 Transformer 的一次计算走通
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法