位置编码
做个实验:把句子打乱,注意力的输出竟然不变,顺序信息去哪了?
先做实验:把句子打乱
「多头注意力」结尾留了一个思想实验:把输入句子的词序打乱,每个词的注意力输出会变吗? 不做猜测,直接用代码验证。把「我 打 你」喂给多头注意力,再换成「你 打 我」,逐词对比输出:
结果出来了: 每个词得到的输出向量完全相同 ,只是排列位置跟着输入变了。 「我打你」和「你打我」,对注意力机制来说没有任何区别。
诊断:注意力对顺序「无感」
刚才的实验现象是: 打乱输入,只会同样地打乱输出,每个词得到的向量不变 。 这一性质称为 置换等变(permutation equivariant) 。
原因是注意力分数只取决于 Query 和 Key 的 内容 ,加权求和也只是按分数混合,整个计算把句子当成了一个 「词的集合」 ,谁排第几位,从头到尾没有参与任何运算。
思路是:既然注意力只看向量内容,那就 把位置信息写进向量内容里 。 给每个位置生成一个「位置向量」,直接加到该位置的词嵌入上。 这样「第 1 个位置的我」和「第 3 个位置的我」向量不同,注意力自然能区分。
两个朴素方案,各有毛病
位置怎么变成向量?先试两个最容易想到的方案:
理想的位置编码应该:数值 有界 、每个位置 唯一 、 且与句长 无关 。Transformer 给出的方案是用三角函数。
正弦位置编码
前面两个方案都想用 单个数值 表示位置。正弦位置编码(sinusoidal positional encoding)换了个思路:给位置 pos 生成整整一个 维向量。这个向量的每一维,都是一条 频率不同的正弦 / 余弦波 在位置 pos 处的取值,偶数维取自 sin、奇数维取自 cos,不同维度对应不同的「波长」:
其中 i 是维度对的序号。i 越大,分母越大;分母越大,位置每增加一步带来的角度变化就越小, 正弦波也就振荡得越慢。靠前的维度是高频维(波长短、振荡快),靠后的维度是低频维(波长长、变化缓)。 一个位置的编码,就是这串快慢不一的振荡在该处的取值组合。
把几条不同维度的波画在一起就能看清这件事。下图横轴是位置,纵轴是编码值,每条曲线对应 中的一个维度:横向看,靠前的维度随位置快速来回振荡,靠后的维度变化缓慢; 纵向看任意一个位置,它在各维度上落到的是一串各不相同的取值,这正是「每个位置都拿到独一无二的编码」的来源。
维度 (最高频对)到维度 (最低频)的若干条正弦 / 余弦波, 取值始终落在 内。悬停某个位置可读出各维度在该处的取值组合。
这套设计恰好满足我们列出的全部要求(讲清直觉即可,不作严格证明):
- sin / cos 的取值始终在 [−1, 1], 有界 ,数值始终可控,不会像方案一那样越加越大
- 快慢维度组合在一起,正常句长范围内每个位置都得到 各不相同 的取值
- 编码只依赖 pos 本身、 与句长无关 ;训练与推理可复用同一张编码表
- 相邻位置的编码连续变化,且固定间隔的两个位置之间存在稳定的变换关系, 便于模型捕捉 相对位置 (点到为止)
- 无需训练,任意位置的编码都能直接算出,编码表可按需延长
实现 PositionalEncoding
编码表不含任何可学习参数,可以一次性预计算好。注意它要注册为 buffer ,跟着模型走(保存、搬到 GPU),但不参与梯度更新:
使用方式就是最后一行: 位置编码与词嵌入逐元素相加 , 相加后的向量既带语义、又带位置,再送入注意力。
把这张预计算好的编码表整体铺开,就能一眼看到它的全貌。下图把表的每一格按编码值着色, 横轴是维度( 到 )、纵轴是位置, 编码值在 之间,用双向色谱表示正负(一端为负、中间近零、另一端为正):
- 横着读一行 :这个位置的完整编码向量。每一行各不相同,体现每个位置的编码唯一。
- 竖着读一列 :这个维度的值随位置变化的波,偶数维是 波、奇数维是 波。
- 颜色 :取值大小,范围 。偏蓝为负、接近中性色表示靠近 、偏红为正。
- 左密右纯 :左侧列条纹细密 = 高频、随位置变化快;右侧列近乎纯色 = 低频,在这段位置范围内变化很慢(这正是右侧几列看起来「一整列一个色」的原因)。
位置编码表( )。左侧维度沿位置方向条纹细密(高频、变化快), 越往右条纹越宽(低频、变化缓),呈现频率从快到慢的整体规律;横向逐行扫过, 每个位置都对应一串独一无二的取值组合。悬停任意格子可读出该位置、该维度的具体编码值。
重做打乱实验
加上位置编码,再跑一遍开头的实验:
同一个「我」,位于第 1 个位置和第 3 个位置,输出向量不再相同, 顺序信息正式进入了模型 。 「我打你」和「你打我」,终于是两句不同的话了。
位置信息的注入方式不止这一种,还有可学习的位置嵌入、旋转位置编码(RoPE)等方案, 现代大模型多用后者。原始 Transformer 采用的是本课的正弦编码,其他方案留待后续课程。
部件够了,能堆深吗?
到这里,我们已经有两个核心部件:多头注意力 + 位置编码。但一层注意力只让每个词混合了 一轮 上下文,复杂的语义(指代、嵌套修饰)需要在混合结果上 再混合 、反复加工, 这与深层 CNN 靠堆叠卷积层提取越来越抽象的特征是同一个道理。
这些内容会在《残差与层归一化》中展开。