📚 本文是《Transformer 由浅入深》系列第 5 篇 · 阶段二「核心机制」
前两篇我们把注意力的数学讲透了。但它藏着一个你可能已经隐约察觉的致命盲区:注意力根本不在乎词的顺序。
这一篇,我们先把这个盲区讲清楚,再看 Transformer 用什么办法——位置编码(Positional Encoding)——把"语序"重新交还给模型。
本篇目标
读完你应该能:
- 说清为什么纯注意力"看不见"顺序;
- 看懂正弦位置编码的设计巧思;
- 知道现代大模型(如 RoPE)在往哪个方向演进。
阅读前提
第 3、4 篇。记住注意力的核心:每个词对全句做"加权汇总"。
1. 问题:注意力是「置换不变」的
回想第 3 篇的输出公式:第 \( i \) 个词的新表示是
$$ \text{output}_i = \sum_{j=1}^{n} A_{ij}\, v_j $$这里是个求和。而求和有个性质:加数换个顺序,结果不变。也就是说,如果你把输入句子里的词打乱顺序,注意力的计算里没有任何一个环节"知道"词原来排第几——每个词的 Q、K、V 只取决于它自己的内容,跟它站在哪个位置毫无关系。
结果就是:“猫 追 狗"和"狗 追 猫”,在纯注意力眼里几乎是一回事。 打乱输入,输出也只是跟着打乱,词与词之间的关系完全没变。这个性质术语叫置换等变 / 置换不变(permutation equivariant/invariant)。
可"猫追狗"和"狗追猫"意思完全相反!顺序是语言的命脉,而注意力恰恰把它丢了。我们必须想办法,把"每个词排第几"这个信息,显式地喂给模型。
换个比喻你会更有感觉。 想象你把一句话里的每个词,分别写在一张张卡片上,然后把这叠卡片洗牌,扔进一个袋子里。现在让你只看袋子里这堆"无序卡片",去判断这句话到底是"猫追狗"还是"狗追猫"——你做不到,因为袋子里只有"猫"“追"“狗"这几张卡片,没有任何"谁在前谁在后"的记号。纯注意力面对的就是这样一个袋子:它能看清每张卡片上写了什么、能计算任意两张卡片有多相关,但它永远不知道卡片原来的排列顺序。
再打个比方:注意力像是在算一桌人之间"两两的关系网”——谁和谁亲近、谁和谁疏远。这张关系网本身不带"座位号”,所以无论这桌人怎么换座位,网的结构都一模一样。可对语言来说,“主语在动词前还是动词后"恰恰是决定意思的关键。我们要做的,就是给每个座位贴一个独一无二的"座位号牌”,让模型一眼就能看出谁坐在第几号位子上。
为什么这一点如此要命?因为它不是"准确率掉几个点"的小毛病,而是模型在结构上根本无法区分两类完全相反的句子。不补上位置信息,后面再强的注意力、再多的层数都是白搭——它处理的始终是一个"洗过牌的词袋",而不是一句有头有尾的话。
2. 朴素方案及其问题
最朴素的想法:给每个词加一个位置序号 0, 1, 2, 3…,拼到它的向量里不就行了?
但这有几个问题:
- 数值范围不可控:句子越长,序号越大。第 1000 个词带着"1000"这个大数,会严重干扰原本归一化好的词向量。
- 泛化差:如果训练时只见过长度 50 的句子,模型从没见过"位置 500 长什么样",一遇到长句就懵。
把序号除以句子长度归一化?那又会导致"同一个绝对位置在不同长度的句子里数值不同",自相矛盾。
为什么"大数会捣乱"? 打个比方:词向量本身好比一杯调好味道的饮料,各种成分(语义维度)都被仔细调到了相近的浓度。这时你往里倒进一个数值是 1000 的"位置浓汤",原来那点精心调好的味道立刻就被淹没了——模型几乎只尝得到"位置",尝不到"词义"。数值有界的好处,就是这勺调料的分量始终温和,不会喧宾夺主。
为什么"归一化序号"也不行? 假设我们规定"位置 = 序号 / 句长"。那么在一句 10 个词的话里,第 5 个词的位置是 0.5;可在一句 100 个词的话里,第 5 个词的位置却变成了 0.05。同一个"第 5 个词",换了句长就换了身份证号——模型没法把"句首附近"这个概念稳定地学下来。这就像用"我在队伍的百分之多少处"来报位置:队伍一长一短,同一个人报出的数就变了,完全不可靠。
我们需要一种更聪明的编码:数值有界、每个位置有独一无二的"指纹"、而且最好能让模型方便地推断"相对距离"。
3. 正弦位置编码(原论文方案)
原版 Transformer 给出的方案很优雅:用一组不同频率的正弦/余弦函数,为每个位置生成一个 \( d \) 维的"指纹"向量。对位置 \( pos \)、维度下标 \( i \):
$$ PE_{(pos,\,2i)} = \sin\!\left(\frac{pos}{10000^{2i/d}}\right), \qquad PE_{(pos,\,2i+1)} = \cos\!\left(\frac{pos}{10000^{2i/d}}\right) $$听起来玄乎,但直觉很简单。看维度下标 \( i \):
- \( i \) 小的维度,分母小,频率高——数值随位置变化快;
- \( i \) 大的维度,分母大(直到 10000),频率低——数值随位置变化慢。
于是每个位置就有了一个由"快慢不同的波"组合成的独特指纹。这特别像时钟:秒针转得快、分针中等、时针慢——三根指针的组合,就能唯一地标定一个时刻。也像二进制计数:最低位翻转最快,最高位最慢,各位组合出唯一的数。
你还可以把它想成给每个位置发一张独一无二的"声纹身份证":有人嗓音里高频成分多、有人低频成分多,把高中低各种频率的强弱组合起来,就能唯一地认出一个人。正弦编码给每个位置配的,就是这样一张由几十上百种频率拼出来的"声纹"——位置不同,声纹的组合就不同,模型据此就能分辨"这是第几个词"。
把"时钟"这个比喻再讲细一点。 单看秒针,它每分钟就转回原点,所以光凭秒针你分不清"现在是 1 分 30 秒还是 5 分 30 秒"——它有周期,会重复。但把秒针、分针、时针凑在一起看,情况就完全不同了:这三根指针要再次回到完全相同的组合姿态,得等上整整 12 个小时。也就是说,虽然每根针自己都在循环,多根不同快慢的针组合起来,就能在很长一段时间内不重样地标定每一个时刻。正弦编码就是这个道理:单个维度(单根针)会周期性重复,但几十上百个不同频率的维度叠在一起,就能给每个位置一个几乎不会撞车的独特指纹。
二进制计数的比喻同样贴切。 想想 0、1、2、3、4… 用二进制写出来:000、001、010、011、100…。你会发现最右边那一位(最低位)每加 1 就翻转一次,翻得最勤;往左每一位翻转的频率都减半,最左边那位翻得最慢。正弦编码里"高频维度变化快、低频维度变化慢"几乎是同一种设计——只不过把二进制里非黑即白的"0/1 跳变",换成了平滑连续的正弦波而已。平滑这一点很重要:它让相邻位置的编码也彼此相近,模型更容易体会到"位置 7 和位置 8 离得很近"。
这套编码的好处正好补上了朴素方案的短板:数值恒在 \( [-1, 1] \) 之间(有界);每个位置指纹唯一;而且因为是确定的函数,理论上能外推到训练时没见过的更长位置。
为什么说它"能外推"? 关键在于:这套编码是一个写死的数学公式,不是从训练数据里"背"出来的。你想知道位置 5000 的指纹,直接把 \( pos = 5000 \) 代进 sin/cos 公式就能算出来,根本不需要模型在训练时见过这么长的句子。这和朴素的可学习序号形成鲜明对比:后者就像一本只印到第 50 页的词典,查第 500 页直接翻不到;而正弦编码像一个能算任意位置的公式,你报多大的位置它都能现场给你算出一个合理的指纹。当然,“能算出数值"不完全等于"模型能用好这个数值”,真实的外推效果还有讲究,但在设计动机上,这种确定性的公式天然比死记硬背更有外推的底气。
4. 为什么用 sin/cos:相对位置可线性表示
正弦编码还有一个更深的妙处,这也是当初选它的关键原因:任意两个位置的相对距离,可以用一个简单(线性)的变换来表示。
回忆三角恒等式:
$$ \sin(\alpha + \beta) = \sin\alpha\cos\beta + \cos\alpha\sin\beta $$这意味着,位置 \( pos + k \) 的编码,可以由位置 \( pos \) 的编码经过一个只与偏移量 \( k \) 有关、与 \( pos \) 无关的线性变换(旋转)得到。
用大白话翻译一下这句话。 “线性变换"在这里你可以直接理解成"把指针转一个固定的角度”。假设你想知道"往后数 3 个位置"长什么样:不管你现在站在位置 10 还是位置 100,只要把当前位置的编码"转动一个固定的角度"(这个角度只由偏移量 3 决定),就能得到后面第 3 个位置的编码。“往后 3 步"对应一个固定动作,在哪儿做都一样。 这就好比拧螺丝:不管螺丝现在拧到哪个深度,“再拧一圈"这个动作永远是同一个动作,效果可预测。
对模型来说这是天大的好消息:它不必死记每个绝对位置,而可以方便地学习”相隔 \( k \) 个位置“这种相对关系——而相对位置(谁挨着谁、隔多远)往往才是语言里真正重要的东西。
为什么"相对距离"比"绝对位置"更重要? 想想"the cat sat on the mat"这句话,“cat"和"sat"紧挨着,这个"紧挨着"的关系,无论这句话出现在文章的开头还是第 300 个词的位置,都同样成立。语言里大量的语法和搭配关系,本质上都是"这个词和它前面/后面第几个词"的相对关系,而不是"这个词正好是全文第几个”。正弦编码"相对位置可线性表示"的这个性质,等于把模型最需要的那种信息,用一种它最容易上手的形式(固定的线性变换)递到了它手边。
5. 绝对 vs 相对位置编码
由此引出两大流派:
- 绝对位置编码:给"第 5 个位置"一个固定向量。正弦编码属于此类;早期 BERT、GPT 则用可学习的绝对位置向量(干脆把每个位置的编码当参数训练)。简单有效,但对"没见过的更长位置"外推能力弱。
- 相对位置编码:不关心"第几个”,只关心"两个词隔多远”。更贴合语言的本质,外推性通常更好,成为后来的主流方向。
一个生活里的类比帮你记住两者的区别。 绝对位置编码像是给电影院每个座位钉死一个号牌:“3 排 12 座”。它精确,但如果换了一个更大、座位排数更多的影厅(更长的句子),那些超出原影厅范围的新座位号(“第 80 排”)模型压根没见过,就抓瞎了。相对位置编码则像是只关心"你和你朋友隔着几个座位":隔 2 个就是隔 2 个,换到多大的影厅都成立。正因为它编码的是"距离"这种放之四海皆准的量,所以换了更长的句子也不容易失效——这就是它外推性更好的直觉来源。
6. 现代主流:RoPE 旋转位置编码(浅介绍)
今天的大模型(LLaMA、Qwen 等)多采用 RoPE(Rotary Position Embedding,旋转位置编码)。它的核心想法非常漂亮:
不再把位置信息"加"到词向量上,而是按位置把 Q、K 向量旋转一个对应的角度——位置越靠后,转得越多。
为什么这样好?因为注意力打分靠的是 Q 和 K 的点积,而两个向量分别旋转后,它们点积的结果只取决于二者的旋转角度差,也就是只取决于它们的相对位置。于是 RoPE 用一种极自然的方式,把"相对位置"直接编织进了注意力的打分里,既优雅又有很好的长度外推性。
“旋转"到底在转什么?给个最小的画面。 把每个向量想象成钟面上的一根指针。RoPE 做的事是:位置 1 的词,它的 Q/K 指针转一点点;位置 2 的词,转多一点;位置 10 的词,转得更多——转的角度和位置成正比。现在关键来了:当模型计算"位置 3 的词"和"位置 7 的词"有多相关时,它实际算的是两根指针的"夹角”。而位置 3 转了 3 份角度、位置 7 转了 7 份角度,两者的夹角恰好对应 7 − 3 = 4 份——也就是它们相隔 4 个位置这个事实。两个词各自的绝对位置在做点积时"抵消"掉了,只留下纯粹的相对距离。
这正是 RoPE 的精妙之处:它没有像正弦编码那样在入口处"加"一个东西进去,而是把位置信息藏在了"姿态/角度"里。两根指针一起转,它们的相对夹角不变;只有当它们转的角度不同(即位置不同)时,夹角才变化。于是"相对位置"就自动、干净地落到了注意力分数里。
细节(怎么分组旋转、角度怎么设)我们留给扩展阅读,这里你只要记住一句:RoPE = 用"旋转"把相对位置注入注意力。
7. 加在哪、怎么加
最后说说位置编码在模型里的"接入点":
- 正弦 / 可学习绝对编码:在最开始,把位置编码加到输入的词向量上,然后一路往上传。位置信息一次注入,全程携带。
- RoPE:不在输入处加,而是在每一层的注意力计算中,对 Q、K 当场施加旋转。位置信息在每层都"新鲜"地参与打分。
绝对编码: 词向量 ⊕ 位置编码 → 进入第1层 → 第2层 → …
RoPE: 词向量 → 第1层(算注意力时旋转Q,K)→ 第2层(再旋转)→ …
这两种接入点的差别,可以这样体会。 绝对编码像是在出发前给每个旅客发一张写好座位号的车票,之后一路上这张票一直揣在兜里,但越往后传、经过的处理越多,这张票上的字迹可能会被各种变换"蹭花"一点。RoPE 则像是在每一站的检票口都重新核对一次位置——它不依赖入口处那一次注入,而是在每一层算注意力时当场把相对位置用上,所以位置信息始终"新鲜",不会在深层被稀释。这也是 RoPE 在很深的模型里依然稳定好用的一个直觉原因。
常见疑问与易错点
问:位置编码到底是"加"到词向量上,还是"拼接"在后面? 答:经典的正弦编码和可学习绝对编码,都是逐元素相加(词向量和位置编码维度相同,直接对应相加),而不是拼成更长的向量。很多人初学会以为是拼接,其实是相加。这样做既不增加维度、省参数,实践效果也好。而 RoPE 是另一回事——它既不加也不拼,而是在算注意力时旋转 Q、K,根本不动词向量本身的数值大小。
问:为什么不能干脆用最简单的序号 0、1、2、3 当位置? 答:两个硬伤。一是数值不可控,序号会随句子变长而无限增大,大数会盖过精心归一化的词向量(就像往饮料里倒一勺"位置浓汤")。二是外推差,训练时没见过的大序号(比如位置 5000),模型完全没概念。正弦编码用有界的 sin/cos、二进制式的多频率组合,恰好同时解决了这两点。
问:RoPE 和正弦编码到底有什么区别?都用了三角函数啊。 答:确实都和正弦/旋转有关,但作用方式根本不同。正弦编码是把一个位置向量在入口处"加"到词向量上,属于绝对位置编码;RoPE 是在每一层注意力计算时,按位置把 Q、K 向量"旋转"一个角度,让点积结果天然只反映相对位置,属于相对位置编码。一句话:正弦编码是"加在输入上的标签",RoPE 是"转进打分里的姿态"。
问:有了位置编码,模型就能处理任意超长文本了吗? 答:不一定。位置编码解决的是"如何表示位置"的问题,但超长文本还有别的瓶颈:一是注意力的计算量随序列长度平方增长,太长会很慢很费显存;二是即便公式上能外推到没见过的长度,模型在那些长度上的实际表现也常常下降。所以工程上还要配合各种长度外推技巧(比如对 RoPE 的频率做插值/缩放)和高效注意力方案,才能真正把上下文撑长。
问:既然相对位置更重要,绝对位置编码是不是就被淘汰了? 答:不能这么绝对。绝对位置编码(尤其可学习的那种)简单直接,在很多固定长度、不需要强外推的场景里依旧好用,早期的 BERT、GPT 就靠它。只是当大家越来越追求"长上下文 + 强外推"时,以 RoPE 为代表的相对位置方案因为外推性更好,成了大模型的主流选择。两者是"各有适用场景",而非简单的新旧替代。
问:位置编码是模型自己学出来的,还是人为设定的? 答:看是哪一种。正弦编码和 RoPE 都是写死的数学公式,不含可训练参数,模型直接拿来用;而可学习的绝对位置编码则是把每个位置的向量当成参数,在训练中和其他权重一起学出来。写死的公式胜在外推有底气,可学习的胜在灵活贴合数据,各有取舍。
小结 & 下一篇预告
这一篇补上了注意力的最大盲区:
- 纯注意力置换不变,天生看不见词序;
- 位置编码显式注入"第几个"的信息;朴素序号行不通(数值不可控、不外推);
- 正弦编码用多频率 sin/cos 给每个位置唯一指纹,数值有界,且让相对位置可线性表示;
- 现代主流 RoPE 用"旋转 Q/K"的方式,把相对位置直接编进注意力打分,外推更好。
至此,注意力子层(多头 + 位置)已经完整。但一个真正的 Transformer 还不止注意力——它还需要残差连接、层归一化、前馈网络这些"配件"才能稳定地堆叠很深。下一篇,我们就把这些拼起来,看清楚一个完整的 Transformer Block 长什么样。
📖 系列目录
见 第 1 篇 文末完整目录。