自注意力机制是Transformer的核心,但它有一个与生俱来的缺陷:计算过程本身对输入序列的顺序完全无感。换句话说,如果不额外注入位置信息,模型看到“猫追狗”和“狗追猫”时,得到的表示是完全相同的。位置编码就是为了解决这一问题而生的技术,它把每个词在句子中的位置以数学方式叠加进词向量,让模型能够区分词序差异。理解位置编码的必要性,是理解现代大模型输入处理逻辑的第一步。
为什么自注意力机制天然忽略词序
自注意力的计算过程是这样的:每个词的向量会分别乘以三个矩阵,得到查询向量Q、键向量K和值向量V,然后每个位置的Q与所有位置的K做点积,再经过softmax得到注意力权重,最后对V加权求和。仔细观察这个过程就会发现,每一步运算都只依赖向量的数值内容,而不依赖词在序列中的下标。也就是说,如果把输入序列的词随机打乱,只要每个词的向量跟着移动,注意力权重的分配结果只是位置上的重排,模型提取到的语义信息没有任何变化。
这种性质在数学上称为置换不变性。置换不变性意味着模型等价于一个词袋模型,只能知道句子里有哪些词,却不知道词与词之间的先后关系。然而自然语言的语义高度依赖顺序,“我打他”和“他打我”包含的词完全一样,语义却截然相反。因此,必须在词向量进入注意力层之前,把位置信息明确地编码进去。
另外需要说明的是,卷积神经网络和循环神经网络天然带有局部位置感知:CNN的卷积核在固定窗口上滑动,RNN按时间步逐词处理,位置信息隐含在计算顺序中。Transformer彻底抛弃了循环结构换取并行计算能力,代价就是必须显式补充位置信号。这就是位置编码在Transformer中被单独拿出来设计的根本原因。
绝对位置编码与可学习位置编码的实现思路
最经典的做法来自原始Transformer论文中的正弦位置编码。它为每个位置pos生成一组不同频率的正弦和余弦值,直接加到词向量上。其公式为:偶数维度使用sin(pos / 10000^(2i/d)),奇数维度使用cos(pos / 00^(2i/d))。这种编码的优点是不需要训练,理论上可以泛化到训练时未见过的长度,并且不同频率的组合让模型能够通过线性变换近似表达相对位置关系。缺点是加法融合的方式比较生硬,位置信息与语义信息纠缠在一起。
import numpy as np
def sinusoidal_encoding(pos, d_model):
# pos: 词的位置,d_model: 向量维度
encoding = np.zeros(d_model)
for i in range(0, d_model, 2):
div_term = np.power(10000, 2 * i / d_model)
encoding[i] = np.sin(pos / div_term)
if i + 1 < d_model:
encoding[i + 1] = np.cos(pos / div_term)
return encoding
# 对比位置0和位置5的编码向量
e0 = sinusoidal_encoding(0, 64)
e5 = sinusoidal_encoding(5, 64)
print(np.dot(e0, e5)) # 观察不同位置编码的内积差异
另一类方案是可学习位置编码,代表是BERT和GPT的早期版本。做法非常直接:定义一个形状为最大长度乘以隐藏维度的可训练参数矩阵,位置i的向量就是这个矩阵的第i行,与词向量相加。这种方式的优点是位置表示完全由数据驱动,模型可以学出最适合任务的位置表达。缺点是最大长度在训练时就固定死了,推理时序列一旦超过训练长度就没有对应的位置向量可用,这也是早期BERT最多只能处理512个token的根源。
两种方案的共同局限在于,它们表达的都是绝对位置,即“这个词在第几个位置”,而语言中很多现象更依赖相对位置,比如“前一个词是名词”这类局部依赖。于是研究者提出了相对位置编码的思路,把位置信息从输入层移到注意力计算内部,直接对Q和K的点积结果按相对距离进行调整。
RoPE旋转位置编码为何成为主流
旋转位置编码RoPE是目前大模型领域应用最广的方案,LLaMA、Qwen、ChatGLM等主流开源模型都采用它。RoPE的核心思想非常优雅:把词向量的维度两两一组,每一组看作二维平面上的一个点,然后将第m个位置的向量旋转m乘以基准角度。根据复数乘法的性质,两个位置旋转后做点积,结果只依赖它们的相对距离m减n,绝对位置项在运算中自然抵消了。
这种设计的巧妙之处在于,它不是把位置信息加到输入上,而是融入注意力分数的计算过程,天然表达了相对位置关系;同时由于每个位置独立旋转,序列中任意两个token的计算互不依赖,完全保留了自注意力的并行性。此外,配合NTK-aware缩放、YaRN等插值技巧,RoPE可以在不做继续训练或少量微调的情况下,把模型的上下文窗口从4K扩展到32K甚至更长,这是绝对位置编码难以企及的优势。
import torch
def rope_encode(x, base=10000):
# x: (batch, seq_len, dim),dim必须为偶数
seq_len, dim = x.shape[-2], x.shape[-1]
# 计算每个维度组的旋转角频率
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
pos = torch.arange(seq_len).float()
# 外积得到每个位置、每个维度组的角度
angles = torch.outer(pos, inv_freq) # (seq_len, dim/2)
cos = torch.cos(angles).unsqueeze(0)
sin = torch.sin(angles).unsqueeze(0)
x1, x2 = x[..., 0::2], x[..., 1::2] # 奇偶维度分组
# 旋转操作:二维旋转矩阵展开
rotated = torch.cat([x1 * cos - x2 * sin,
x1 * sin + x2 * cos], dim=-1)
return rotated
需要注意RoPE的一个实现细节:旋转是作用在Q和K上的,V不做旋转,因为相对位置信息只需要体现在注意力权重的打分环节。另外,不同框架对奇偶维度的分组方式有差异,有的是前一半维度与后一半维度配对,有的是相邻两维配对,复现模型时必须与预训练权重保持一致,否则位置信号会被彻底打乱,模型输出会明显崩坏。
如何为自己的项目选择位置编码方案
选择方案时可以从三个维度考量。第一是序列长度需求:如果任务长度固定且较短,比如分类、命名实体识别,可学习位置编码简单直接,效果足够;如果需要处理超长文本或希望灵活扩展上下文,RoPE是首选。第二是训练数据规模:数据充足时可学习编码表达能力更强,数据稀缺时正弦编码这种免训练方案更稳妥。第三是部署兼容性:目前主流推理框架如vLLM、TensorRT-LLM都对RoPE做了深度优化,选择RoPE意味着整个工具链开箱即用。
对于自研模型的情况,一个务实的建议是直接对齐主流开源架构采用RoPE,而不是自行设计新的位置编码。位置编码看似只是输入层的小模块,但它与注意力实现、长度外推、注意力偏置等机制深度耦合,贸然改动很容易引入难以排查的问题。当确实需要扩展上下文时,优先尝试基于RoPE的插值方法,例如线性插值会把旋转角按比例缩小,让长序列的位置角落在模型见过的范围内,通常配合少量长文本微调就能取得不错的效果。
总结来看,位置编码的存在弥补了自注意力机制置换不变性的天然缺陷,从正弦编码到可学习编码再到RoPE,方案的演进方向始终是把位置信息更自然地融入注意力计算,同时支持更长的上下文。理解这条技术脉络,不仅能回答“为什么词序需要被标记”这个问题,也为阅读和修改大模型源码打下了扎实的基础。
位置编码RoPETransformer修改时间:2026-08-31 05:40:57