1. 为什么Transformer非要位置编码不可一切的起点做Transformer相关的工作绕不开位置编码。很多人第一次接触这个概念时都会有同样的困惑输入序列明明是挨个喂进去的网络难道不知道谁在谁后面吗答案是真的不知道。Transformer的核心模块自注意力机制在处理序列时会同时计算任意两个token之间的关联权重这个过程本身完全不感知顺序。我举个直白的例子如果不加位置编码序列“猫追老鼠”和“老鼠追猫”在模型眼里几乎是同一句话。因为自注意力算的是词与词之间的相似度“猫”和“老鼠”这两个词不管谁先谁后它们的向量不变注意力分数不变最后输出的表达也没区别。这对语言理解来说是致命的。人类的语言里词序承担了很大一部分语义谁在施动、谁在受动、修饰关系、指代关系全都依赖顺序。所以Transformer在架构层面必须做一件事把“顺序”这种结构信息显式地注入到输入里去。这就是位置编码的来历。它不是给模型增加什么新能力而是在补一块天生的短板。理解位置编码还要区分两个层面第一是“怎么把位置信息表示成向量”第二是“怎么把这个向量传进模型”。绝大多数方案都采用加法也就是在词嵌入的维度上直接加上一个位置向量。为什么要用加法而不是拼接我实测过拼接方式维度变大以后参数量膨胀训练速度下降但精度并没有明显提升不划算。加法保持输入维度不变让语义信息和位置信息在每个维度上直接叠加后面的注意力层和全连接层可以自由地混合、提取这两类信息灵活性反而更高。这篇文章是这个系列的第二篇我会把位置编码从最初的sinusoidal方案讲到现在的旋转位置编码和线性衰减偏置配合代码、公式和踩坑记录完整梳理这块内容。1.1 自注意力机制为什么天生不关心顺序拆开看自注意力的计算过程就更清楚了。一个token的query会与序列里所有key做点积除以根号维度后过softmax得到权重再对value做加权求和。整个过程里决定一个token关注哪些位置的仅仅是向量之间的点积相似度。如果把“猫追老鼠”的词向量重排成“老鼠追猫”每一对词的向量乘积不变softmax的结果也不变最终每个token拿到的上下文表示一模一样。数学上这叫排列不变性听起来很学术实际上就是说模型根本没有“第一个”“第二个”这样的概念。RNN和LSTM不存在这个问题因为它们是逐步递归的隐藏状态天然带有“之前发生了什么”的信息。Transformer为了并行计算把序列一次全部送进去这一步省了时间但把顺序问题转移给了位置编码去解决。还有一个容易忽略的点位置编码不仅要告诉模型“我现在是第几个”还要帮助模型捕捉“两个位置之间差多远”。后面会讲到sinusoidal编码之所以设计得那么精巧目的之一就是为了让模型能够从编码里提取出相对距离。1.2 顺序信息对语言模型的三种价值第一种价值是语法角色区分。主语和宾语的位置不同含义完全不同这个靠词向量本身学不来因为“猫”向量并不包含“我在做主语”这个动态信息。第二种价值是局部依赖模式。语言里有很强的近邻效应形容词修饰名词、量词搭配名词往往集中在小窗口内。模型要知道“谁和谁挨得近”才能高效地建立局部组合。位置编码给了模型一个几何空间它可以通过注意力权重学习到“邻近位置的token应该倾向于互相注意”这种模式。第三种价值是长距离依赖的消解。指代关系经常跨越很长的距离例如一段话前面提到一个主体隔了几十个词又出现一个代词。模型需要感知“距离远”这一事实从而选择不同的注意力策略。位置编码为这种距离感知提供了基础信号。这三种需求叠加起来设计目标就很明确了位置编码提供的信号既要能区分绝对次序又要能表达相对距离还要在不同尺度上都有分辨率。sinusoidal编码恰好同时做到了这几点。2. 绝对位置编码Transformer原版方案长什么样回到最经典的那张公式表。对一个长度为max_len、嵌入维度为d_model的序列位置pos处的第i个维度的编码值为PE(pos, 2i) sin(pos / 10000^(2i / d_model)) PE(pos, 2i1) cos(pos / 10000^(2i / d_model))光看公式容易一头雾水我拆开讲清楚每个部分的来历。2.1 公式逐项拆解与代码实现d_model是嵌入总维度比如常见的512、768或者1024。位置编码的维度必须和词嵌入维度一致才能直接相加。i是维度下标从0到d_model/2。2i和2i1分别代表偶数维度和奇数维度所以同一对维度共享同一个频率值只是一个是正弦一个是余弦。分母里的10000^(2i/d_model)是整个公式最核心的设计。当i增大时指数2i/d_model变大10000的幂次变大作为分母就使得这个维度的频率变低。换句话说前半段的维度对应高频波长短能区分相邻位置的微小差异后半段的维度对应低频波长长能在整个序列长度范围内保持较强的区分度。举个例子d_model512时i0这一对维度对应的波长是2π位置每变化1正弦值就产生明显变化适合编码很细的位置差异。越往后波长越长最后一对维度的波长接近10000×2π在常规序列长度内几乎不会震荡适合表达位置的整体分布。这种多尺度特性和小波变换的思路有些相通也和人眼先看轮廓后看细节的机制类似。代码实现有个小技巧。通常的写法是import numpy as np def sinusoidal_positional_encoding(max_len, d_model): pe np.zeros((max_len, d_model)) position np.arange(max_len).reshape(-1, 1) i np.arange(0, d_model, 2) div_term np.exp(i * (-np.log(10000.0) / d_model)) pe[:, 0::2] np.sin(position * div_term) pe[:, 1::2] np.cos(position * div_term) return pe encoding sinusoidal_positional_encoding(512, 512) print(encoding.shape) # (512, 512)这里用exp换成对数运算避免了大数乘方的浮点溢出效果和原公式完全等价。position的形状是(max_len, 1)i的形状是(d_model/2,)两者广播相乘得到(max_len, d_model/2)的矩阵赋值给偶数维和奇数维。很多初学者会在这段代码上卡住核心就是理解numpy广播机制。2.2 为什么三角函数能承载相对位置关系在继续之前先解决一个关键疑问绝对位置编码怎么表达相对距离所有sinusoidal编码的奥妙都在三角函数恒等式里。对于任意偏移k有sin(pos k) sin(pos)·cos(k) cos(pos)·sin(k) cos(pos k) cos(pos)·cos(k) - sin(pos)·sin(k)这意味着位置posk的编码值是位置pos的编码值的线性组合。只要模型在训练中学到了合适的线性变换它就能从任意两个位置的编码里直接算出差值对应的信号。工程上我更倾向于把这个性质理解为绝对位置编码提供了一组基模型可以在其上做线性变换来提取相对信息。这是sinusoidal方案比简单把位置归一化到0到1之间的做法高明的地方——归一化方案在训练长度固定时可能有效一旦序列长度变化位置间隔会被压缩模型学到的距离概念全部失效。还有一个补充视角把每个维度的值当作二进制位来理解。低频维度变化慢、高频维度变化快多个维度组合起来实际上形成了一个类似格雷码的位置标记。这样设计的好处是鲁棒性单个维度的噪声不会整体破坏位置信息。2.3 为什么是加法而不是拼接刚才提到过加法比拼接省维度但还有更深一层的考虑。加法把位置信息分散到每个维度上和词向量融合后注意力层需要自己去“学”怎么把混合信号拆开。听起来是增加了模型的学习负担实际上给了模型更大的自由度有的头可以关注语义相似度有的头可以关注位置邻近性有的头可以两者结合这些策略是动态调整的。而拼接方案虽然保留了语义和位置的独立通道让初期的注意力计算更直接但也把这种独立性焊死了。后续层如果想做复杂的交叉组合还得先经过一次线性变换才有机会信息的灵活性反而差一些。在实际项目中我遇到过用拼接方案做的旧版本位置信息确实能更快地被利用模型前期收敛更快但最终精度上限不如加法方案而且参数量和显存占用都更高。所以除非有特殊理由直接抄sinusoidal加法方案是最稳妥的选择。3. 绝对位置编码的局限与相对位置编码的崛起sinusoidal方案从提出到现在依然有不少模型在用但它的局限也很明显。3.1 绝对编码“不管远”的问题sinusoidal编码是预先定义好的、不随训练更新的。注意力层确实可以从中提取相对距离但没有受过“距离越远相关性应该怎么衰减”的引导。模型从头开始训练时位置信号是固定的能不能把相对位置用好完全取决于attention层是否学出了合适的变换。我做过一个序列标注任务输入长度达到几百时sinusoidal编码在高频维度上的变化非常剧烈模型很容易把注意力集中到固定的小邻域里远处的相关信息捕捉得很弱。这就是所谓的“注意力局域化”倾向高频分量的存在让模型天然倾向于近距离token。对于不同长度的输入这个倾向会产生不同的影响长序列任务里尤其棘手。另外序列长度超过训练时见过的长度后sinusoidal编码本身依然能生成新位置的向量这点比可学习位置编码要强但注意力权重的泛化仍然没有保障。模型没有学过“长距离时应该怎么处理”换了长度后行为不可预测。3.2 显式建模相对偏移更直观的思路与其给模型一份“第几个”的表格不如直接告诉它“第i个token和第j个token之间的距离是多少”。这就是相对位置编码的核心思想。具体实现上常见的做法是给attention score加一个偏差项bias这个bias的取值取决于i-j这个偏移量。例如在某个经典编码器-解码器模型里相对位置被分成几个bucket每个bucket对应一个可学习的标量query在计算第i个位置对第j个位置的注意力权重时直接把这个标量加到点积结果上。用bucket而不是精确的每一种偏移一个参数是为了控制参数总量。如果输入长度很长每种偏移都单独学一个参数参数量会随序列长度平方增长训练数据一少就容易过拟合。把偏移量分桶后远处的偏移都映射到同一个“远处”类别既能表达“距离远”这个事实又能极大减少参数。从实际效果看相对位置编码在需要精确判断两个实体之间相对位置的场景下优势明显命名实体识别、关系抽取、句法分析这类任务都是典型例子。因为模型不再需要通过线性变换间接推理相对位置而是直接收到了相对距离的信号学习路径短得多。3.3 各种位置编码怎么选一张对比表我把常见的位置编码方案放在一起做过系统对比维度有四个是否可学习、能否外推、相对距离建模方式、实现复杂度。方案参数形式外推能力相对距离表达工程复杂度sinusoid绝对编码固定无参中等隐含可提取极低可学习绝对编码可训练差隐含需训练低相对bias编码可训练较好显式中旋转位置编码RoPE固定无参好显式且高效中高线性衰减偏置ALiBi超参数很好显式线性低表格里的“外推能力”指的是模型训练时序列长度较短、推理时输入更长序列的能力。这块现在是位置编码研究的核心热点后面单独展开。从我的经验看如果做的是中小规模的文本分类任务输入长度稳定用可学习绝对位置编码是最省事的收敛也快。如果做的是序列生成、长文本理解、或者以后想平滑升级到长上下文场景直接从RoPE入手更明智。如果预算有限、工程时间紧sinusoidal永远是兜底的可靠选择。4. 新一代方案旋转位置编码与线性衰减偏置近两年大模型普遍采用长上下文训练位置编码的外推性能被推到了聚光灯下。两个方案在这个背景下成为主流旋转位置编码和线性衰减偏置。4.1 RoPE把位置信息转成向量旋转旋转位置编码的英文全称是Rotary Position Embedding核心思路非常优雅把词向量按二维一组切分每组向量的角度加上一个与位置成正比的旋转角。具体来说对于位置pos处的第i组二维向量(x1, x2)做如下变换x1 x1·cos(pos·θ_i) - x2·sin(pos·θ_i) x2 x1·sin(pos·θ_i) x2·cos(pos·θ_i)其中θ_i 10000^(-2i/d)和sinusoidal方案里的频率选择一脉相承。这样变换后的向量在做内积时位置pos1和pos2的两个向量的点积结果只依赖于它们的相对夹角(pos1-pos2)·θ_i。这个性质保证了注意力权重天然是相对位置的函数而不是绝对位置的函数。这一点对泛化非常关键因为模型在训练时看到的相对距离分布如果足够均匀推理时遇到更大的绝对位置也可以合理外推。从数值上看RoPE实现时有几种写法。最朴素的做法是把token数量个位置逐一旋转每个位置对d_model/2组二维向量做旋转。矩阵运算时可以用预计算的cos和sin表节省重复计算import numpy as np def apply_rope(x, cos_table, sin_table): # x: (batch, seq, heads, head_dim) # cos_table, sin_table: (seq, head_dim/2) d x.shape[-1] x1 x[..., : d // 2] x2 x[..., d // 2 :] cos cos_table[: x.shape[1], None, :] sin sin_table[: x.shape[1], None, :] output np.concatenate( [x1 * cos - x2 * sin, x1 * sin x2 * cos], axis-1 ) return output这段代码在工程上被称作“旋转拼接”实现比逐组旋转再拼接更高效。我在生产环境里测过这个操作的开销相比注意力矩阵乘法可以忽略不计。RoPE在长文本场景的表现我在几个项目里都验证过训练长度4096的模型直接喂8000长度的输入困惑度变化很小。这个效果是sinusoidal和可学习位置编码很难做到的。4.2 ALiBi最简单也最反直觉的绝对位置偏置ALiBi的方案听起来几乎简单到不像是学术成果给注意力分数加上一个和距离成正比的负偏置。第i个query对第j个key的注意力logits计算公式变为score_ij q_i·k_j - m·|i - j|其中m是一个随注意力头变化的超参数通常是2^(-8/num_heads)这样一个等比序列。头越多衰减越慢远处的token在这些头里能保留更多注意力权重。这个方案的巧妙之处在于它根本不修改词向量不做任何变换纯粹在注意力分数层面做手脚。模型从第一步开始就知道“距离越远我越不应该关注”而且这个偏置是线性的、无参数的、可无限外推的。实操上ALiBi的缺点也很明显注意力分数偏置是固定的线性衰减缺少可学习的灵活性。有些任务里远处的token恰恰是关键的比如一篇文章的论点通常出现在开头和结尾固定的衰减策略可能压制这种需求。我做长文档摘要时对比过RoPE的方案在引入可学习成分后明显优于ALiBi但ALiBi在初始阶段训练稳定性更好、收敛更快。4.3 外推能力为什么成为选择位置编码的第一考虑因素现在的大模型动辄支持几十万token的上下文但训练时GPU显存有限真正用这么长的序列做完整训练成本极高。最好的方法是用较短序列训练推理时靠位置编码的外推能力撑起长输入。所以任何一种位置编码方案如果不能解决“训练短、推理长”的矛盾就难进主流。除了RoPE和ALiBi工程上还见过位置插值方案把超出训练范围的位置坐标按比例压缩回已知区间。这种方法在fine-tune阶段效果不错但需要额外做一步调整而且对超低频率分量多的模型效果不稳定。我的判断是新项目直接选RoPE除非你的输入长度非常固定且没有外推需求。RoPE在表达能力、外推能力和部署生态上都处于目前的最优平衡点。5. 实操中的坑与排查经验理论说再多不落地都是空谈。这部分完全是实战经验记录几个我在项目里真实遇到过的位置编码相关硬坑。5.1 可学习位置编码的长度限制与初始化问题可学习位置编码的最大问题就是训练时的max_len限制。有一个项目里我把max_len设为512后来业务方要求支持1024的输入直接推理时超出索引范围报错。应急方案是取前面512个位置的编码重复填充后面的位置结果模型输出质量明显下降——模型看到的是同一批位置编码反复出现注意力模式完全乱套。正规做法有两种一是模型训练前就把max_len设得足够大宁可前期浪费一点参数二是切成不等长分段训练让模型在训练时就见过各种长度分布。第二种做法的前提是位置编码本身有外推能力所以本质上还是要选RoPE这类方案。初始化也藏着一个坑。可学习位置编码通常是随机初始化标准差和词嵌入保持一致。如果初始标准差过大早期训练阶段位置信号会盖过词向量模型需要很长时间才能把两者权重调平衡。我习惯把位置编码初始化的标准差设为词嵌入的0.02倍左右再用和词嵌入相同的学习率微调收敛稳定很多。5.2 推理长度超过训练长度的外推实战当年用sinusoidal编码做长文本推理时我踩过一次典型的坑。一个对话模型训练时最长上下文是512上线后用户输入拼接历史达到700多个token。虽然sinusoidal能生成第700位的编码但注意力分布完全失控生成的文本开始重复和漂移。排查过程让我明白一件事sinusoidal编码的数学定义虽然可以无限外推但模型内部的注意力权重是在固定长度分布下学出来的长度一变位置向量之间的相对距离数值也随之变化联动效应直接击穿模型稳定性。后来在这类固定长度模型上做长文本支持时我采用了“位置编码插值”的方案把超出长度的位置坐标乘以一个缩放因子映射回训练区间。做一步轻量的微调后效果提升明显。这个方案的关键是lambda值不能太小否则相邻位置编码几乎相同模型无法分辨邻近token。5.3 数值精度与实现细节里的暗坑第一个坑是最容易忽略的整数溢出。实现RoPE时如果直接用pos乘以频率后用int型存储中间结果pos一旦超过几万就会溢出成负数旋转角度完全错误。强烈建议全部用float64计算位置表再截断成float32使用。第二个坑是d_model奇数。很多位置编码实现默认d_model是偶数写代码时用0::2和1::2切片。如果某天改成奇数维度最后一个维度没有配对切片长度不一致直接崩。做一个维度检查再根据奇偶调整旋转逻辑能省去很多线上事故。第三个坑是导出模型时的输入问题。推理框架导出时很多人习惯把位置编码表当作模型的一部分冻在权重里曲线没问题。但如果输入长度动态变化最好把位置编码表也作为输入传入推理接口避免每个长度都要重新生成并转换一遍。我之前在自研推理引擎上优化时发现预生成的cos、sin表可以离线缓存推理时按长度切片性能比每次重新算快不少。5.4 不同任务对位置编码的敏感度差别文本分类任务里我对比过取消位置编码和保留的位置编码效果差距可能只有不到一个点因为分类往往只需要抓住几个关键词顺序信息的需求很低。但序列标注、关系抽取、机器翻译这类任务位置编码几乎是决定性的去掉位置编码模型基本无法收敛。更强的对比发生在语义相似度任务里。有一次做句向量模型用均值池化把所有token的向量平均位置编码被池化操作冲淡了和词向量相比权重占比极低。这时候两种位置编码方案的差异几乎没有意义。后来改成用CLS位置的向量做分类位置信息才重新发挥作用。给新人的建议是上来先别纠结选哪种位置编码先用sinusoidal或可学习编码跑通baseline确认任务确实对顺序敏感后再引入RoPE这类高级方案做对比一步步来。5.5 从入门到工程化的选择路径站在2025年回看位置编码的发展脉络非常清晰从无参数的sinusoidal到可学习的绝对编码再到显式相对编码然后是旋转和线性衰减的复兴。背后驱动力是模型输入长度的持续扩张和对泛化性的更高要求。我的最终选择思路可以概括成三句话任务短且长度固定怎么方便怎么来任务要做长上下文预计算RoPE的cos/sin表并按需切片任务对相对位置敏感但不想改模型结构ALiBi是最小的侵入方案。用RoPE的时候还有一个小细节值得分享注意力头的维度越小每个头的旋转频率覆盖范围越窄模型对精确偏移的敏感度越低。在某些任务上适当加大单头维度反而能提升相对位置的分辨能力。这个参数值得在调参时专门测一下。最后给一个我反复验证过的结论位置编码不是越复杂越好。它是给模型补一个“顺序感”的开关设计精巧但使用起来不必神化。选一个和任务匹配的方案在长度控制和训练策略上做好准备比盲目追逐最新论文里的方法更实际。如果你正在做Transformer相关的项目建议把这个部分当成调试隐私层一样去对待——平时感觉不到它的存在一旦出问题排查代价就是整整一个周末。