第 4 课算完了注意力分数表的形状:一句话 1,024 个 token, 表就是 1,024 × 1,024。可那张表从头到尾只回答一个问题—— 「谁和谁像」。「谁先出现、谁后出现」,它一个字都没说。这件事听起来小,代价却很大:把「十五」和「五十」喂进去, 模型看到的是一模一样的 8 × 2 个数。这一课我们补上 Transformer 拼装前的最后一块零件——让矩阵第一次知道顺序。
先把这件事看成一个具体的问题。第 3 课训出来的那张嵌入矩阵 E 是 [20, 8]。 一句话进去,模型拿到的是一个 [字數, 8] 的小矩阵—— 每一行是一个字在第 3 课学到的向量。就这些,没有别的了。
「十五」和「五十」用的是同一批字,只是摆的顺序不一样。 点下面的按钮来回切这两句话,盯住两个地方:袋子里的东西变没变,两两打分的那张表变没变。
a·b 和 b·a 本来就是同一个算式。 模型接下来要做的每一件事,都只认这 8 个数和这些分数。这不是「有点不准」的问题,是彻底看不见。你喂进去的是 15 还是 50, 模型给出的东西完全一样。所以第 4 课末尾那句「矩阵不知道谁先谁后」, 现在有了证据。
最笨的塞法就是字面意思:把座位号当成一个数,加到那个字向量的每一维上。 第 12 个座位的「加」就变成 E(加) + 12, 8 个数一起加 12。两个位置的字要量相似度时,就是(E + p) · (E′ + q)。
看起来挺合理。但把它展开,就会看见一件麻烦事。下面这台机器把这一整条算式摊成四项,每一长条都按同一个刻度尺画——拖两根滑块试试:
p × q:注意这件事的荒谬之处:④ 这一项跟字没关系。 把「加」换成任何一个字,它都一样大。 换句话说,模型量出来的「相似度」里,绝大部分是座位号自己在跟座位号聊天, 字的内容反而成了零头。
现在的做法是这样的:每个座位不发一个号,发一张「指纹」。 这张指纹也是 16 个数,但每个数都由一个快慢不同的指针转出来。
想象 8 根指针,都从正右方出发,但转速天差地别:最快的那根每走一个座位就转一整圈; 最慢的那根走完 32 个座位连一圈的百分之一都没转完。 每个座位停下来的位置,8 根指针的横坐标和纵坐标, 就是这张指纹的 16 个数。下面这张图就是它:
[32, 16] 的「指纹表」; 颜色刻度固定在 ±1,不按表里最大值归一这就是「位置编码」。第 2 站那个「座位号」是一个数、处处一样; 这里是一整串有快有慢的数,每一维的节奏都不同。 而且它是算出来的——不需要训练、不占任何参数, 座位号给多大就现算多大。
最要紧的是:32 个座位,32 张指纹,没有两张是一样的。 最接近的两个座位(3 和 4)也差着 1.0147;离得最远的(0 和 28) 差着 3.5090—— 是前者的 3 倍多。 所以模型只要看一眼指纹,就知道这是第几个座位。
位置有了,接下来是纯粹的工程问题:这张指纹,怎么和那个字的 8 个数合起来?三条路摆在眼前——逐位相加、拼接座位号、拼接整张指纹。下面把它们并排摆出来。 (演示里指纹缩到 4 对、8 个数,好跟 8 维的词向量一格对一格地摆; 真实模型里两张表都是 3,584 个数,道理完全一样。)
拼接看起来更干净——位置和内容分开放着,互不干扰,多好。 可真实模型(GPT、Qwen、Llama……)用的全是加法。为什么?
[T, 3,584] 加完还是 [T, 3,584],后面每一层的形状都不用改。 拼接会变成 [T, 7,168],全网络跟着加宽。到目前为止,指纹满足的要求只是「32 个座位不重样」。这件事随机数也能做到—— 随便生成 16 个数当作指纹,32 个座位几乎不可能撞上。那为什么全世界的模型都用 sin 和 cos?
因为 sin/cos 多给了一样随机数给不了的东西。回头看那些「快慢不同的指针」: 座位往后挪几步,每根指针就整体转几度。 而这个转过的角度,跟起点在第几个座位毫无关系—— 只跟「隔了几步」有关。
下面这张表把上面那件事一格一格量出来:三个不同的起点、三根快慢不同的指针, 每一格都是从两个座位的 (sin, cos) 反推出来的夹角。
(sin, cos) 各取出来,反推「从前者转到后者一共转了多少度」 (就是这两个二维指针的夹角)。同一行三个格子的数完全一样 —— 换起点、换距离,角度只跟着「隔了几步」和「是第几对」走。(sin, cos) 和座位 3 的 (sin, cos) 之间, 夹角就是 57.2958°; 换成座位 28 和座位 29,夹角还是 57.2958°。(sin, cos) 取出来、反推出来的夹角。同一行三个格子的数完全一样——座位 2 到 5、座位 11 到 14、座位 28 到 31, 隔的都是 3 步,转过的角度就都是同一个数。这件事的意义比它看起来大。语言里最要紧的位置信息从来不是「这是第 7 个字」, 而是「这两个字离得多远」。而这套指纹把「隔了几步」 直接编码成了一个可以算出来的角度:模型只要学会「看夹角」, 就等于学会了「看距离」,跟它们从哪儿开始没有关系。
位置编码听上去是件小事——16 个数而已。可它牵着两笔大账:一句话有多长,模型就要付多大的代价。 第一笔是第 4 课算过的注意力分数表,第二笔是位置本身怎么存。
两个数一起看,就能明白为什么现在的模型都在「怎么把位置做得更省、更长」上下功夫:表是平方长大的,位置表是按长度线性长大的,而用户永远想要更长的上下文。
自注意力只认「谁像谁」,天生看不见顺序——同一批字换个摆法, 它拿到的东西一个数都不差。补的办法是给每个座位发一张指纹:8 根快慢不同的指针,每一根提供 sin / cos 两个数, 合起来 16 个数。快的那几对分辨「挨着的差别」,慢的那几对分辨「离得远的差别」, 从最快到最慢跨了 3162 倍。 这张指纹是算出来的,不占参数;它最大的好处是天生带相对位置——座位往后挪几步,指针就整体转几度, 转角只跟「隔了几步」有关。用的时候逐位加到词向量上(不是拼接:拼接会把宽度从 3,584 撑到 7,168, 后面每一层都得跟着加宽)。代价也要认清:一句话的长度 T 一涨, 注意力分数表就按 T² 涨 —— 1,024 到 32,768 就是 1,024 倍。