前三篇做的都是准备工作:Transformer(1) - tokenizer 把文字拆成 token,Transformer(2) - embedding 把每个 token 变成一个 维的向量,Transformer(3) - 位置编码 给向量加上了位置信息。这一篇正式进入 Transformer 的核心:self-attention(自注意力)。
embedding 那篇说过,查表得到的只是每个 token 的“初始画像”,同一个 token 永远是同一个向量。“我爱吃苹果”和“苹果发布了新手机”里的两个“苹果”,查出来一模一样。
人是怎么分清这两个“苹果”的?看上下文。前一句里有“吃”,说明是水果;后一句里有“发布”“手机”,说明是公司。
self-attention 做的就是这件事:让每个 token 去看句子里的所有 token,判断哪些和自己关系大,然后按关系的大小把它们的信息吸收进来,更新自己的向量。输入是 个向量,输出还是 个向量,只不过每个向量都融入了上下文的信息。
可以把这个过程想象成去图书馆查资料。你带着一个问题去,书架上每本书的书脊上都有标签。你拿问题去和每本书的标签对一对,对得越上的书,就越仔细的读里面的内容,对不上的就略过。最后你得到的知识,是所有书的内容按“对得上的程度”混合起来的结果。
对应到 attention 里就是这样
| 图书馆查资料 | self-attention |
| 你带着的问题 | Query,查询向量 |
| 书脊上的标签 | Key,键向量 |
| 书里的内容 | Value,值向量 |
| 问题和标签对得上的程度 | 和 的点积 |
| 按对得上的程度决定每本书读多少 | softmax 算出的权重 |
| 最后得到的知识 | 对所有 加权求和 |
不同的是,在 self-attention 里,句子里的每个 token 既是来查资料的人,又是书架上的一本书。每个 token 都拿着自己的问题,去查所有 token(包括自己)。
假设一句话有 个 token,第 个 token 的向量是 (embedding 加上位置编码以后的结果)。
先介绍两个后面会一直用到的符号
这两个数和 一样,都是设计模型时定好的,可以和 不一样。如果只用一套 attention,最简单的做法就是都取成 。下一篇讲 multi-head 时会看到,实际上它们一般比 小得多,比如原版论文里 ,而 。
那这两个长度定多大合适?主要要考虑这么几点
所以 和 的设计,本质上是在“表达能力”和“计算、内存开销”之间做取舍。
每个 token 的向量分别乘三个矩阵,得到它的 、、:
其中 、 的大小是 , 的大小是 。这三个矩阵和 embedding 一样,都是训练出来的参数,所有 token 共用同一套。
为什么一定要乘一个矩阵
最简单的做法其实是什么都不乘,直接让 。为什么不这么做?主要有这么几个原因
把两个矩阵代进分数的公式里,能更清楚的看到第 2 点和第 4 点:
中间的 相当于一把可以学习的“尺子”,由它来规定两个 token 在哪些方面像才算相关。而且只要 ,这把尺子就不是对称的, 和 可以不一样。
至于为什么只是乘一个矩阵,而不是用更复杂的变换:一是便宜,矩阵乘法在 GPU 上非常快,而且所有 token 可以一次算完;二是够用,非线性的部分已经有 softmax,后面还有专门的前馈网络来做更复杂的处理。
把所有 token 的向量按行排成一个 的矩阵 ,就可以一次算出所有的 、、:
第 个 token 想知道第 个 token 和自己有多相关,就拿自己的 和对方的 做点积:
点积越大,说明两个向量方向越一致,也就是“问题和标签对得越上”。每个 token 都要和所有 token 算一遍,一共得到 个分数,写成矩阵就是
第 行第 列就是 。
分数可正可负,大小也没有范围,还不能直接当权重用。用 softmax 把第 个 token 对所有 token 的分数变成一组权重,每个都在 0 到 1 之间,加起来等于 1:
就是第 个 token 分给第 个 token 的“注意力”,也就是这本书要读多少。对 的每一行分别做 softmax,得到的权重矩阵大小不变:
最后用这组权重,把所有 token 的 加权加起来,就得到第 个 token 新的向量:
和自己关系越大的 token,它的信息在 里占的比重就越大。所有 token 一起算,就是一次矩阵乘法:
把上面四步写成矩阵的形式,就是 Transformer 论文里那个最有名的公式:
是第 (二) 步,softmax 是第 (三) 步(对每一行分别做),再乘 是第 (四) 步。唯一多出来的是除以 ,这个后面单独说。
输入是 个向量,输出是一个 的矩阵,也就是 个新向量,每个 token 一个。
attention 的本意其实就是一次“查询”,所以先看一个最接近这个本意的例子:查一个小小的通讯录。通讯录里有三条记录
| 记录 | key:这条记录是关于什么的 | value:记录的内容 |
| 1 | 张三的住址 | 西安 |
| 2 | 张三的电话 | 138xxxx |
| 3 | 李四的住址 | 北京 |
你问“张三住在哪”,这就是 query。你拿它去和每条记录的 key 比,第 1 条对上了,于是取出它的 value“西安”。
这里能很清楚的看到 key 和 value 为什么要分开:key 写的是“这条记录是关于什么的”,value 才是真正要拿走的内容。你永远是拿问题去匹配 key,而不是去匹配“西安”这两个字。
把 key 写成向量,4 个维度分别表示 [关于张三, 关于李四, 是住址, 是电话],所以 。value 为了简单,用 3 维向量分别表示三个答案 [西安, 138xxxx, 北京],所以
| 记录 | () | () |
| 张三的住址 | [1, 0, 1, 0] | [1, 0, 0]:西安 |
| 张三的电话 | [1, 0, 0, 1] | [0, 1, 0]:138xxxx |
| 李四的住址 | [0, 1, 1, 0] | [0, 0, 1]:北京 |
问题“张三住在哪”写成 (),表示“关于张三”并且“是住址”。
第一步,打分。用 和每条记录的 做点积:
第 1 条两个条件都满足,分数最高;另外两条各只满足一个。
第二步,除以 ,得到 3、1.5、1.5。
第三步,做 softmax。先分别取指数得到 20.086、4.482、4.482,总和是 29.049,再各自除以总和:
第四步,按权重把 value 加起来:
结果是 69% 的“西安”,加上一点点电话和北京。普通字典只会精确的给出“西安”,而 attention 给的是一个按相关程度混合的结果,大头是“西安”。如果某个 key 和问题对得特别好,它的权重会接近 1,效果就和普通字典差不多。所以可以把 attention 理解成一个“模糊版”的字典查询。
通讯录不变,只换问题:
| 问题 | 三条记录的权重 | 主要拿到 | |
| 张三住在哪 | [3, 0, 3, 0] | 0.691 / 0.154 / 0.154 | 西安 |
| 张三电话多少 | [3, 0, 0, 3] | 0.175 / 0.786 / 0.039 | 138xxxx |
| 李四住在哪 | [0, 3, 3, 0] | 0.175 / 0.039 / 0.786 | 北京 |
和 完全没变,只换了 ,拿到的东西就不一样了。
self-attention 里,一句话就是一本通讯录,每个 token 都是其中的一条记录,同时每个 token 又都在提问。和查通讯录相比,只有三点不同
还有一点要注意:在通讯录里,value 恰好就是 key 所描述的那个属性的值,“张三的住址”对应的就是“西安”。但在句子里不一定是这样。比如在“我 吃 苹果”里,“苹果”可能是靠“我是一个动词”这个标签找到了“吃”,但从“吃”那里拿回来的,却是“和食物有关”这样的含义。怎么被找到和交出什么可以完全是两回事,这也是 和 要用两个不同矩阵来算的原因。下面「一个完整的例子」会把这个过程完整的算一遍。
假设 和 的每一维都是均值为 0、方差为 1 的随机数,那么它们的点积是 个这样的乘积加起来,方差就是 :
也就是说,点积的典型大小会随着维度变大而变大,大约是 的量级。 时点积动不动就是 ±8 这种大小。
分数太大会让 softmax 变得极端。比如分数是 [1, 2, 3] 时,softmax 的结果是 [0.09, 0.24, 0.67],注意力还是分散的;把分数放大 10 倍变成 [10, 20, 30],结果就几乎是 [0, 0, 1],注意力全部挤到了一个 token 上。这时候其他 token 几乎拿不到权重,训练时传给它们的调整信号也几乎为 0,模型就很难学。
除以 以后,点积的方差又回到了 1,不管维度多大,softmax 都工作在一个比较平和的范围里。这也是论文把这个公式叫做缩放点积注意力(scaled dot-product attention)的原因。
因为 、、 都来自同一个序列 ,是一句话自己和自己做 attention。
与之相对的是交叉注意力(cross-attention), 来自一个序列, 和 来自另一个序列。Transformer(0) - 总览 里说的原版 Transformer,decoder “参考 encoder 的结果”,用的就是交叉注意力:查询来自已经翻译出来的译文,键和值来自 encoder 算出的原文向量。而 GPT 这类只有 decoder 的模型里,用的都是 self-attention。
看上面的计算过程,每一步用到的都只是向量的内容,没有任何地方用到“这是第几个 token”。如果把输入的顺序打乱(用一个 的置换矩阵 表示,左乘以后就是交换 的行),输出也只是跟着同样的方式打乱:
每个 token 得到的新向量本身完全不变。这就是 Transformer(3) - 位置编码 要解决的问题:正弦编码和可学习编码是提前把位置加进 里,而 RoPE 是在算 之前按位置把 和 转一个角度,让上面第 (二) 步的点积带上距离信息。
第 (二) 步里,每个 token 都要和所有 token 算一遍相关程度,一共 个分数。句子长度翻一倍,这部分的计算量和占用的内存就变成 4 倍。这是上下文窗口不能无限变长的主要原因之一,也是 tokenizer 那篇说“拆出来的 token 越少越好”的原因。
反过来说,这也是 attention 强大的地方:任意两个 token 之间都是直接算的,不管它们在句子里隔了多远,一步就能建立联系,而且所有 token 可以同时并行的算。
前面通讯录的例子里,、、 是直接给出来的。这里换回句子,从 开始完整走一遍,看看三个矩阵是怎么把一个普通的向量,变成“问题”“标签”和“内容”的。所有数字都是为了好理解而设计的,真实模型里的每一维不会有这么清楚的含义。
假设 ,向量的 4 个维度分别表示 [是动词, 是名词, 和食物有关, 和科技有关]。句子是“我 吃 苹果”
| token | () | 说明 |
| 我 | [0, 1, 0, 0] | 名词 |
| 吃 | [1, 0, 1, 0] | 动词,和食物有关 |
| 苹果 | [0, 1, 0.5, 0.5] | 名词,既可能是水果也可能是公司 |
“苹果”的初始向量里,食物和科技各占一半,它自己还不知道是哪个意思。
取 ,三个矩阵都是 的。矩阵的每一行对应 的一个维度,表示这个维度会被变换成什么。
负责贴标签。它把“是动词”“是名词”这两个维度原样保留下来,其他的丢掉。也就是说每个 token 对外宣称的是“我是动词”或“我是名词”:
负责提问。它把“我是名词”翻译成“我要找动词”,把“我是动词”翻译成“我要找名词”。注意它的两列和 是交叉的:
负责决定传递什么内容。它不关心词性,只把“食物”“科技”这两个表示含义的维度拿出来:
可以看到,同一个 ,经过三个矩阵以后各自只保留了对当前角色有用的那部分信息。这就是前面说的“一个 token 扮演三个角色”。
用 分别乘三个矩阵(都是 乘 ,得到 )
| token | |||
| 我 | [3, 0]:我要找动词 | [0, 1]:我是名词 | [0, 0] |
| 吃 | [0, 3]:我要找名词 | [1, 0]:我是动词 | [1, 0]:食物 |
| 苹果 | [3, 0]:我要找动词 | [0, 1]:我是名词 | [0.5, 0.5]:食物和科技各一半 |
“苹果”拿着自己的问题 (我要找动词),和每个 token 的标签对一对:
只有“吃”的标签是“我是动词”,和问题对上了。除以 再做 softmax,得到权重:
按权重把内容加起来:
原来“苹果”的食物和科技各占一半,现在食物占了 0.855,科技只剩 0.048。它从“吃”那里抄来了“和食物有关”的信息,确定了自己是水果。
再看“苹果 发布 手机”。假设“发布”是 [1, 0, 0, 1](动词,和科技有关),“手机”是 [0, 1, 0, 1](名词,和科技有关),三个矩阵完全不变。
“苹果”的问题还是“我要找动词”,这次对上的是“发布”,权重同样是 0.807。但“发布”的内容是 [0, 1](科技),所以:
这一次“苹果”变成了科技公司。
同一个“苹果”、同一套矩阵,在两句话里得到了完全不同的结果。分工很清楚: 和 决定“去看谁”(这里靠词性找到了动词), 决定“看到以后带回什么”(这里带回的是食物还是科技的含义)。用来匹配的信息和用来传递的信息可以完全不同,这正是要用三个不同矩阵的原因。
真实的模型里,这三个矩阵不是人设计的,而是训练出来的,它们学到的“问法”也远比“名词找动词”复杂。但机制就是这样。
self-attention 让每个 token 带着自己的问题 ,去和所有 token 的标签 算相关程度,用 softmax 变成权重,再按权重把所有 token 的内容 汇总起来,得到一个融入了上下文的新向量。写成公式就是
不过还有两点没讲。一是现在每个 token 都能看到整句话,包括排在它后面的 token。对 encoder 来说这正是想要的,原文是一次性完整给出的,前后都看才能理解得更好。但对 decoder 来说就是个问题了:总览里说过,decoder 是一个一个往后生成的,生成第 5 个 token 的时候第 6 个还不存在,训练时也不能让它偷看答案。二是一套 、、 只能学一种“提问方式”,而一个 token 往往需要同时关注好几种关系,比如语法上的、语义上的。第一点等讲到 decoder 的时候再说,下一篇先来解决第二点:multi-head attention。