首页 / 第二部分 · 注意力和 Transformer / 第 05 篇

第 05 篇 · 第二部分 · 注意力和 Transformer

自注意力拆开看:带着问题去找答案

上一篇讲清了注意力机制为何出现。这一篇换个方向,把自注意力按零件拆开:Q、K、V 分别代表什么,那个 softmax 在计算什么,为什么公式里要乘一个看似突兀的 1/√d。拆开之后会发现,这套数学味很浓的公式背后,其实只是一个很朴素的流程——拿着问题,去所有地方找答案。

一、先看一个具体的问题

句子:小明把书放在了桌子上,然后他走了。

这里的"他"是谁?人看一眼就知道是小明。机器要回答这个问题,必须同时看见"他"和"小明",还要判断这两个词之间的关系比"他"和"桌子"更近。

早年的循环神经网络(RNN)是逐个词往前读的。读到"他"的时候,关于"小明"的信息早就被压缩进了一个固定长度的隐藏状态里,其中还夹杂着"书""桌子"这些内容,层层传递下来已经模糊不清。序列越长,前面的信息越容易被冲淡。

注意力机制的做法就直接得多:读到"他"这个位置时,让模型回过头把整句话每个位置都重新看一遍,然后按相关程度分配注意力。相关性高的位置给予更多关注,不相关的给予较少甚至不给予关注。"他"自然会把大部分注意力放在"小明"上。

二、先借用图书馆检索来理解

你在图书馆找资料,流程大致是三步:

  • 你手里有一个查询(query)——你想知道的事;
  • 每本书有一个标签(key)——它大概讲什么;
  • 每本书有具体的内容(value)——真正有价值的部分。

拿查询和每本书的标签比一比相似度。相似度最高的那几本,其内容会被更多地引入答案;相似度越高,引入的内容越多。

自注意力就是把这一整套动作搬到了同一句话内部,而且每个词都同时发起一次检索。"他"发起一次,"小明"发起一次,"桌子"也发起一次,所有词一起查询、一起作答。

这里有个容易被忽略的关键点:查询、标签、内容这三样东西,没有一样是从外部输入的,全都是从句子自身计算出来的。所以它叫"自注意力"——自己对自己做注意力。

三、同一个词,三副面孔

输入是一个矩阵 X,每一行是一个词(严格说是 token)的向量表示。接下来用三个不同的权重矩阵去乘它:

Q = X · WQ    K = X · WK    V = X · WV

同一个 X,乘上三套不同的参数,就得到了三套向量。可以这样理解它们的角色:

  • Q(Query,查询):这个词在问什么、它想找什么;
  • K(Key,键):这个词能对外提供什么样的"特征标签";
  • V(Value,值):这个词实际携带的内容。

三个权重矩阵 W_Q、W_K、W_V 都是可训练参数。它们一开始是随机的,训练完之后,"小明"的 K 和"他"的 Q 之间会自然地对应上——这是模型自己学出来的,没有人手工指定过指代关系。

四、打分、归一化、加权求和

有了 Q、K、V,剩下的就是三步流水线。完整公式只有一行:

Attention(Q, K, V) = softmax( Q·KT / √dk ) · V

拆开来看:

第一步:打分

Q·KT 得到的是一个 n×n 的矩阵(n 是序列长度)。第 i 行第 j 列的数字,表示第 i 个词对第 j 个词的关注程度。行是"谁在看",列是"看谁"。

第二步:归一化

分数是原始实数,可能很大也可能为负。softmax 把它按行转成一排概率:每行加起来正好等于 1,越相关的列分到的比例越大。这就是"注意力权重"。

第三步:加权求和

拿这排权重去乘 V。某个位置的权重是 0.8,它对应的内容就有八成被纳入结果;权重是 0.01,基本可以忽略。所以每个词的新表示,是全句所有词内容的一次加权平均,权重由它自己发起的这次检索决定。

整件事的可贵之处在于:从打分到加权,全程都是可微运算。意思是梯度能顺畅地传回去,模型能靠训练自动学会这套注意力分配。

五、那个 1/√d 究竟在防止什么

这是最令人费解的地方:为什么要除以一个维度的平方根?

原因与 softmax 的性质有关。Q·KT 里每个数是一对向量的点积,而点积是把 dk 个乘积加在一起。如果每个维度的值彼此独立、均值为 0、方差为 1,那么点积的方差大约是 dk。维度越高,点积的数值就越往两端发散,绝对值越来越大。

而 softmax 对大数值非常敏感:当输入里某个数特别大,它输出的分布就会极度尖锐——几乎全部集中到一个位置,其余全接近 0。这种分布的梯度很小,这部分参数几乎收不到有效的训练信号,难以学习。

除以 √dk,恰好把点积的方差调回到 1 附近,让分布保持在一个"有区分度但不极端"的区间。一句话:它是为了稳定 softmax 的数值表现,不是装饰性的数学技巧。

六、多头注意力:为什么需要多个视角

单头注意力在一次计算里只能产出一套权重分布。但一个词同时需要考虑的关系往往不止一种:

  • "他"需要找它的指代对象;
  • "走了"需要绑定它的主语;
  • 修饰语需要找它修饰的中心词。

把这些需求集中在一套权重里,只能得到一种折中的平均结果。多头注意力的思路很直接:把 dmodel 维的空间切成 h 份,每一份独立做一次完整的注意力,最后把 h 个结果拼回去,再过一次线性层 W_O。

等于给了模型 h 个并行的观察窗口。每个头各自学习自己关心的那类关系,有的头专门关注指代关系,有的头关注语法结构,有的头关注固定搭配。这种分工不是人工设计的,是在训练中自然分化出来的。

注意总计算量并没有涨——每个头的维度变成了 dmodel/h,h 个加起来正好抵消。多头换来的是"视角的多样性",并不产生额外的算力开销。

七、代价:n 的平方

要算两两之间的关系,就必须完整填满一个 n×n 的矩阵。序列长度翻一倍,计算量和这个矩阵的显存占用大约变成原来的四倍。这是注意力机制无法回避的代价。

也正是这一代价,决定了后面许多工程做法的走向:为什么长上下文那么昂贵,为什么推理时要缓存 K 和 V(KV Cache),为什么 FlashAttention 这类实现值得反复优化。这些都留到后面的文章再展开。

八、一个必须记住的性质:它自己不知道顺序

注意力还有一个很反直觉的性质:如果把输入的词打乱顺序,输出也会随之被打乱,但每个位置拿到的数值集合不变。也就是说,纯粹的注意力对词序无感。

把"我打你"和"你打我"输入进去,仅靠这一层算术,模型无法区分两者。这显然不可取。所以必须在输入上额外加入位置信息——这就是下一篇文章要专门解决的问题。

九、骨架代码

把数学换成代码,核心其实只有几行。为了看清结构,这里用 numpy 手写一遍:

import numpy as np

def softmax(x, axis=-1):
    x = x - x.max(axis=axis, keepdims=True)   # 先减最大值,防指数溢出
    e = np.exp(x)
    return e / e.sum(axis=axis, keepdims=True)

def self_attention(X, W_q, W_k, W_v):
    Q = X @ W_q          # n × d_k
    K = X @ W_k          # n × d_k
    V = X @ W_v          # n × d_v
    d_k = Q.shape[-1]

    scores  = Q @ K.T / np.sqrt(d_k)   # n × n,两两打分
    weights = softmax(scores, axis=-1) # 每行和为 1
    out     = weights @ V              # 加权求和

    return out, weights

真实实现里还要处理 batch 维度、加因果掩码(让模型看不到未来的词)等等,但骨架就是这几行。换个模型、换个任务,这套结构反复出现,只是参数量不同。

十、小结

自注意力不神秘。它把"这句话里谁该参考谁"这么一个模糊的问题,翻译成了一次可微、可并行的加权平均。

可微意味着它能被训练;可并行意味着它能在 GPU 上把整句一次性铺开算,不像 RNN 必须一步一步串着走。这两点,才是它取代老架构、成为主流架构的真正原因——不是因为它更"像大脑"。

参考资料

  1. Ashish Vaswani 等, Attention Is All You Need, NeurIPS 2017. arXiv:1706.03762(自注意力与多头注意力的原始出处)
  2. Dzmitry Bahdanau, Kyunghyun Cho, Yoshua Bengio, Neural Machine Translation by Jointly Learning to Align and Translate, 2014. arXiv:1409.0473(注意力机制的早期形态)
  3. Jay Alammar, The Illustrated Transformer. jalammar.github.io/illustrated-transformer(图解入门,适合对照阅读)
  4. Andrej Karpathy, Let's build GPT: from scratch, in code, spelled out. YouTube(跟着从零手写一遍注意力,理解最快)
  5. Tri Dao 等, FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness, 2022. arXiv:2205.14135(后文会提到的工程优化)