【AI基础篇06】位置编码:为什么需要它?
前言:如果你学过Self-Attention,你可能会问:注意力机制把"我喜欢猫"和"猫喜欢我"中的每个词都两两相连——那模型怎么区分这两个句子?Attention本身只关心"谁和谁相关",不关心"谁在前谁在后"。位置编码就是给模型补充"顺序感"的关键组件。本文从绝对位置编码讲到2025年最新的傅里叶位置编码(FoPE),带你一次看懂位置编码的进化史。

📋 目录
一、为什么Attention需要位置信息?

二、绝对位置编码:Sinusoidal PE

三、可学习的位置编码

四、相对位置编码

五、RoPE:旋转位置编码(当前主流)

六、ALiBi:基于偏置的位置编码

七、上下文扩展:NTK-aware、YaRN、DynaTan

八、FoPE:傅里叶位置编码(ICML 2025)

九、各模型位置编码方案汇总

十、实战:用代码理解位置编码

一、为什么Attention需要位置信息?
1.1 注意力机制没有"顺序感"

Self-Attention的本质是"词袋"操作:

输入句子A:“我 打 你”
输入句子B:“你 打 我”

标准的Self-Attention对这两个句子的处理是完全一样的!

为什么?
注意力计算 = Q × K^T
“我"和"你"的相似度是一样的,不管它们在前还是在后
Softmax只关心"谁和谁相关”,不关心"谁先出现"

所以:
“我 爱 你” = “你 爱 我” = “Love is Love”
对Attention来说,这些序列是等价的!→ 需要位置编码
1.2 位置编码要解决的核心问题

理想的位置编码应该满足:

✅ 唯一性
每个位置有独一无二的编码
位置1 ≠ 位置2 ≠ 位置100

✅ 距离感知
位置1和位置2的差异 < 位置1和位置100的差异
距离越近,编码越相似

✅ 外推性
训练时只见过512长度的序列
推理时能处理2048长度的序列
编码方案需要支持"位置插值"

✅ 相对位置敏感
模型需要知道"谁在谁前面"
“我打你” vs “你打我"→ 关键在"谁在前面”
1.3 没有位置编码会怎样?

训练时:
“我今天很开心” → 每个词的位置被打乱也没关系
模型:反正Attention只看内容相似度
结果:模型学到的是"词袋",不是"语言"

推理时:
用户输入:“AI的发展历史”
模型:把"AI"、“发展”、“历史"变成词袋
输出:完全混乱(谁在前谁在后不知道)

结论:没有位置编码的Transformer ≈ 词袋模型
二、绝对位置编码:Sinusoidal PE
2.1 原始Transformer的方案
Google在"Attention Is All You Need”(2017)中提出了Sinusoidal Positional Encoding——用不同频率的正弦和余弦函数为每个位置生成编码。

核心公式:

PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

其中:
pos: 位置(0, 1, 2, …)
i: 维度索引(0, 1, 2, …, d_model/2)
d_model: 模型维度
2.2 直观理解

把位置编码想象成"调频广播":

每个维度i对应一个"频率频道":
低维度(i小):频率高,变化快
→ 编码相邻位置的细微差异
高维度(i大):频率低,变化慢
→ 编码远距离的大范围位置

具体例子(d_model=512):

i=0(第一个维度):
PE(0, 0) = sin(0 / 10000^(0/512)) = sin(0) = 0
PE(1, 0) = sin(1 / 10000^0) = sin(1) ≈ 0.84
PE(2, 0) = sin(2) ≈ 0.91
频率很高,相邻位置的值差异很大

i=256(中间维度):
PE(0, 256) = sin(0 / 10000^(256/512)) = sin(0) = 0
PE(10, 256) = sin(10 / 10000^0.5) = sin(0.1) ≈ 0.10
PE(100, 256) = sin(100 / 100) = sin(1) ≈ 0.84
频率很低,需要很多位置才有明显变化
2.3 可视化

每个位置的编码 ≈ 一个d_model维度的向量

位置0: [sin(0), cos(0), sin(0), cos(0), …]
位置1: [sin(1), cos(1), sin(1/10000^(1/512)), cos(1/10000^(1/512)), …]
位置2: [sin(2), cos(2), sin(2/10000^(1/512)), cos(2/10000^(1/512)), …]

可视化(简化版,只取前2维):

pos1: ↑↗ (角度小)
pos2: ↗→ (角度变大)
pos3: →↘
pos4: ↘↓


不同的位置 → 不同的方向向量
位置越近 → 向量越相似(余弦相似度高)
2.4 Sinusoidal PE的优缺点

✅ 优点:
不需要学习参数(固定编码)
可外推(训练没见过的位置也能生成编码)
有理论保证(三角函数性质)

❌ 缺点:
编码是固定的,无法根据任务调整
位置信息通过"加法"注入(可能干扰语义信息)
边界效应(长序列两端编码差异大)
在大模型中已被RoPE等相对位置编码替代
💡 面试加分点:Sinusoidal PE的精妙之处在于它允许模型通过线性变换从位置1的编码推导出位置1+k的编码,这是因为三角函数有加法定理:sin(a+b) = sin(a)cos(b) + cos(a)sin(b)。这意味着模型可以"学会"相对位置关系。

三、可学习的位置编码
3.1 核心思想

与其用固定的三角函数,不如让模型自己学!

可学习位置编码:
初始化一个位置编码矩阵 [max_seq_len, d_model]
每个位置有一个可训练的向量
通过反向传播优化这些向量
3.2 典型代表

BERT(2018):使用可学习位置编码

配置:
最大序列长度:512
编码维度:768
参数量:512 × 768 = 393,216(不到0.5M参数)

实现:
embeddings = token_embedding + position_embedding

注意:位置编码直接加在词向量上

训练时:
位置1学到"句首"的编码模式
位置2学到"第二个词"的编码模式

位置512学到"很后面的词"的编码模式
3.3 优缺点

✅ 优点:
灵活:根据任务自动调整位置编码
效果好:在固定长度内优于Sinusoidal

❌ 缺点:
不可外推:训练时最大512,推理时超过512就没法处理
需要额外参数:虽然不大,但也是参数
对训练数据有要求:需要足够数据才能学到好编码

解决方案(外推):
方法1:截断——只取前512个位置的编码(丢失信息)
方法2:插值——把更长的序列"压缩"到512个编码内(降低分辨率)
方法3:换成支持外推的位置编码(RoPE等)
四、相对位置编码
4.1 绝对位置 vs 相对位置

绝对位置编码:“我在第5个位置”
每个位置一个固定编码
位置5的编码总是相同的

相对位置编码:“我在第3个词后面第2个位置”
关注的是"两个词之间距离多少"
位置1和位置5的距离 = 位置100和位置104的距离

为什么相对位置更重要?
在"我今天在银行办理了业务"中:
“办理"和"业务"的距离是固定的(相邻)
不管这个句子出现在文章开头还是结尾,这个距离关系不变
所以模型应该学习"距离"而不是"绝对位置”
4.2 T5的相对位置编码

Google T5(2019)的方案:

不是为每个位置分配一个向量
而是为每对位置之间的距离分配一个"偏置"

实现:
注意力分数 = Q × K^T + bias(i, j)
其中 bias(i, j) 取决于位置i和j之间的距离

例子:
“我"和"爱”(距离=1):bias值大 → 鼓励关注
“我"和"猫”(距离=2):bias值中
“我"和"它”(距离=3):bias值小

每个距离范围有一个可学习的bias
超出训练时的最大距离,使用训练时最大距离的bias
→ 天然支持外推!

距离桶(T5的巧妙设计):
|i-j| = 0: 桶0 (自己)
|i-j| = 1: 桶1 (相邻)
|i-j| = 2: 桶2

|i-j| = 7: 桶7 (近距离精密)
|i-j| = 8-15: 桶8 (中距离粗粒度)
|i-j| = 16-31: 桶9

|i-j| ≥ 128: 桶15 (远距离共享)

核心思想:近处精细、远处粗糙
相邻位置的关系最重要 → 每个距离一个桶
远距离位置关系不重要 → 多个距离共享一个桶
五、RoPE:旋转位置编码(当前主流)
5.1 RoPE的核心思想
RoPE(Rotary Position Embedding)是苏剑林在2021年提出的位置编码方案,已成为当前大模型的事实标准。它的核心思想非常优雅:通过在向量空间中旋转来实现位置编码。

核心直觉:

把每个词向量想象成二维平面上的一个点
位置编码 = 把这个点旋转一定的角度

位置1:"我"的向量 → 旋转1度
位置2:"我"的向量 → 旋转2度
位置3:“我"的向量 → 旋转3度

好处:
✅ 模型知道"同一个词在不同位置"的差异(旋转角度不同)
✅ 模型知道"两个词之间的距离”(旋转角度差 = 距离 × 单位旋转角)
✅ 旋转在数学上是干净的(线性变换,不引入噪声)
5.2 数学原理

二维情况:

位置m处的向量x = [x₁, x₂]
经过RoPE编码:
RoPE(x, m) = [x₁·cos(mθ) - x₂·sin(mθ), x₁·sin(mθ) + x₂·cos(mθ)]
= x 旋转了 mθ 角度!

其中θ是基频(控制旋转速度)

任意两个位置m和n的查询q和键k的点积:

RoPE(q, m) · RoPE(k, n) = q · k · cos((m-n)θ)

注意!点积只依赖于(m-n),不依赖m和n的绝对值!
→ 实现了"相对位置编码"的效果!

扩展到d维:
把d维分成d/2个二维子空间
每个子空间有自己的旋转频率θ_i = 10000^(-2i/d)
每个子空间独立旋转
5.3 用矩阵形式理解

旋转矩阵(二维):

R(mθ) = [cos(mθ) -sin(mθ)]
[sin(mθ) cos(mθ)]

高维旋转矩阵(d维):
R = diag(R₁(mθ₁), R₂(mθ₂), …, R_{d/2}(mθ_{d/2}))

即分块对角矩阵,每个块是一个2×2的旋转矩阵

应用RoPE:
Q’ = Q × R ← 对Q做旋转
K’ = K × R ← 对K做旋转
Attention(Q’, K’, V) ← 用旋转后的Q、K算注意力
5.4 RoPE的优美性质

1️⃣ 相对位置
点积结果只依赖于相对距离m-n
→ 位置1和位置5的关系 = 位置100和位置104的关系

2️⃣ 远程衰减
距离越远,cos((m-n)θ)振荡越剧烈
平均下来,远距离的注意力权重自然衰减
→ 符合语言直觉:近的词关系更紧密

3️⃣ 零延迟
RoPE是直接在Q和K上做变换
不像Sinusoidal要加上额外的编码向量
→ 不干扰语义信息

4️⃣ 可外推
训练时没见过长度1000的位置
但旋转矩阵对于任何m都有定义
→ 理论上支持任意长度!
5.5 RoPE的局限性

虽然RoPE理论上支持外推,但实践中:

训练长度4096 → 推理长度8192:
RoPE直接使用 → 性能明显下降
需要辅助技术才能实现外推

根本原因(清华大学ICML 2025论文揭示):
RoPE假设"每一维只有单一频率的语义"
但经过线性层和激活函数后,每一维都混入了其他频率
→ 频谱被破坏,周期性延拓失效

这就是为什么需要在RoPE基础上做NTK-aware、YaRN等调整
六、ALiBi:基于偏置的位置编码
6.1 ALiBi的思路
ALiBi(Attention with Linear Biases,2022)提出了一个极其简洁的方法:不在词向量上加位置编码,而是在注意力分数上加一个偏置。

ALiBi的做法:

标准注意力分数 = Q × K^T / √d_k

ALiBi的注意力分数 = Q × K^T / √d_k - m × |i - j|

其中:
m: 斜率(每个头不同)
|i-j|: 位置i和j之间的距离

半个公式解释所有:
Q × K^T / √d_k ← 内容相似度(和位置无关)

  • m × |i-j| ← 位置惩罚(距离越远,惩罚越大)
    6.2 每个头的不同斜率

ALiBi对不同注意力头使用不同的偏置斜率:

头0: m = 1/2^1 → 偏置很小,关注范围宽
头1: m = 1/2^2 → 偏置稍大
头2: m = 1/2^3 → …

头7: m = 1/2^8 → 偏置很大,只关注很近的词

为什么这样设计?

  • 某些头可以关注长距离(偏置小)
  • 某些头只关注局部(偏置大)
  • 模型自动选择需要的"注意力范围"
    6.3 ALiBi的特点

✅ 极致简单:
不需要额外参数
不需要位置嵌入
不需要学习

✅ 完美外推:
位置偏置只依赖于距离|i-j|
训练4096 → 推理65536也没问题
LLAMA原计划用ALiBi,后来改用了RoPE

❌ 缺点:
表达能力不如RoPE
“线性偏置"过于简单
在复杂任务上不如RoPE灵活

现状:ALiBi在长度外推上有优势
但RoPE+扩展技术在综合任务上更强
目前主流模型都是RoPE路线
七、上下文扩展:NTK-aware、YaRN、DynaTan
RoPE虽然优秀,但直接外推效果不佳。为了解决这个问题,社区提出了一系列"RoPE升级技术”。

7.1 问题:RoPE的外推困境

训练长度:4096 tokens
希望推理时处理:32768 tokens(8倍)

直接使用RoPE的问题:
位置10000处的旋转角度是位置1处的10000倍
导致频率完全错位,模型无法理解

→ 需要"调整"RoPE的频率,让长序列也能正常工作
7.2 位置插值(Position Interpolation)

Meta在2023年提出的方法:

把长序列的位置"压缩"到训练范围内

训练时:[0, 1, 2, …, 4095]
推理32768时:
原来的位置索引除以缩放因子S=8
位置: [0, 1/8, 2/8, …, 32767/8]

相当于每个token只旋转了原来1/8的角度
但模型知道这些token的位置仍然是顺序的

问题:
压缩后相邻token的角度差太小
模型难以区分相邻位置
→ 需要微调才能恢复效果
7.3 NTK-aware Scaled RoPE

NTK-aware(Neural Tangent Kernel)的核心洞察:

不同维度对外推的敏感度不同:
高频维度(i小):负责区分相邻位置
低频维度(i大):负责远距离范围

不是所有维度都"等比例压缩":
高频维度:少压缩(保留局部分辨率)
低频维度:多压缩(扩展全局范围)

实现:
scale = S^(i / (d/2 - 1))
其中S是缩放因子,i是维度索引

效果:
训练4096 → 推理16384,无需微调!
困惑度下降远小于直接插值
7.4 YaRN(Yet another RoPE extensioN method)

YaRN(2023)进一步优化了NTK-aware:

改进1:波长分析
不同维度的"波长"不同
波长大于训练窗口的维度 → 不需要调整
波长小于训练窗口的维度 → 需要缩放
→ 更精确的维度选择策略

改进2:注意力温度调整
插值后注意力分布变"平"了
Softmax的"竞争性"下降
引入温度系数t > 1来恢复"锐利度"
softmax(score / t)

改进3:动态缩放
短序列:用小缩放因子
长序列:用大缩放因子
→ 自适应调整

效果:
LLaMA 7B 训练4096 → 推理65536
大海捞针测试准确率 > 95%
无需微调!
7.5 DynaTan:DeepSeek的RoPE扩展方案

DeepSeek在V4-Pro(2026年4月)中使用的扩展方案:

核心思想:
RoPE的旋转角度随位置线性增长
当位置极远时,角度太大导致"模糊"

DynaTan用tanh函数"饱和"旋转角度:
θ’(pos) = θ × tanh(pos / L)

其中L是饱和长度(超参数)

效果:
近处:≈ 标准RoPE(tanh(x) ≈ x,当x很小)
远处:趋于饱和(tanh(x) → 1,当x很大)

优点:

  1. 近距离精度高
  2. 远距离不爆炸
  3. 支持1M+上下文
  4. 无需微调
    八、FoPE:傅里叶位置编码(ICML 2025)
    8.1 RoPE的"频谱损坏"问题
    清华大学和上海AI Lab在ICML 2025发表的论文揭示了一个关键发现——RoPE的周期延拓性被"频谱损坏"限制了。

RoPE的假设(只在第一层成立):
每一维只有"单一频率"的语义
位置m处第i维的语义 = 按频率θ_i的波传递

实际(第一层之后):
经过线性层 → 每一维混入其他频率的分量
经过激活函数 → 非线性操作产生更多频率
时域截断 → 周期性被边界截断破坏

结果:
每一维都包含了多个频率的语义
但仍然按单一频率的波长估算 → 混乱!
8.2 FoPE的核心思想

"打不过就加入"策略:

既然无法避免频谱损坏,就设计对频谱损坏更鲁棒的位置编码

FoPE的两大创新:

1️⃣ 傅里叶级数建模
每一维不再假设单一频率
而是建模为傅里叶级数(多频率叠加)

这样即使频谱损坏发生
FoPE也能利用三角函数的正交性
从混杂的信号中"解码"出正确的频率信息

2️⃣ 直流分量裁剪
极低频分量 → 周期太长 → 无法学到周期特性
FoPE将其裁剪为频率=0的直流分量

直流分量的优势:
既可以看作周期无限短
又可以看作周期无限长
→ 每个词的信息可以传递给无限远的词
8.3 实验结果

FoPE vs RoPE(多个基准测试):

📊 困惑度(越低越好):
训练长度内:FoPE ≈ RoPE(几乎持平)
2倍外推长度:FoPE比RoPE低7-12%
4倍外推长度:FoPE比RoPE低15-21%

📊 大海捞针测试(准确率):
训练长度4096,测试16384:
RoPE: 62.3%
FoPE: 91.7%
FoPE + YaRN: 94.5%

📊 下游任务:
FoPE在长文档问答、代码生成等任务上稳定优于RoPE
8.4 潜在影响

FoPE的发现不仅适用于位置编码:

论文作者指出,傅里叶分析工具可能影响更广泛的领域:

  • 长视频生成中的时序建模

  • KV Cache压缩的频率分析

  • 多模态融合的位置对齐

  • 语义通信(信号处理)

  • 脑机接口(时序信号)

FoPE是2025年位置编码领域最具影响力的工作之一
有望成为下一代大模型位置编码的候选方案
九、各模型位置编码方案汇总
9.1 主流模型的位置编码
模型 位置编码方案 上下文窗口 外推能力 亮点
Transformer(2017) Sinusoidal PE 512 ✅固定编码 开山之作
BERT(2018) 可学习PE 512 ❌不可外推 简单有效
GPT-3(2020) 可学习PE 2048 ❌ 八大模型
T5(2019) 相对位置bias 512 ✅桶机制 距离桶设计
LLaMA(2023) RoPE 4096 ✅理论可外推 当前标杆
Mistral(2023) RoPE 8192 ✅ 滑动窗口
Gemini 1.5(2024) RoPE + 自定义 1M ✅ 超长上下文
DeepSeek-V3(2024) RoPE + MLA 128K ✅ 解耦RoPE
DeepSeek-V4(2026) RoPE + DynaTan 1M ✅ 动态饱和
Qwen3(2026) RoPE + NTK-aware 1M ✅ 混合缩放
FoPE(ICML 2025) 傅里叶级数 可扩展 ✅✅ 最新突破
9.2 位置编码路线图

2017: Sinusoidal PE —— 固定三角函数
└─ 优点:可外推
└─ 缺点:固定不可调

2018: 可学习PE —— BERT
└─ 优点:灵活适应任务
└─ 缺点:不可外推

2019: 相对位置 bias —— T5
└─ 优点:天然支持外推
└─ 缺点:表达能力有限

2021: RoPE —— 当前主流
└─ 优点:相对位置 + 可外推 + 零延迟
└─ 所有主流大模型标配

2022: ALiBi —— 简洁方案
└─ 线性偏置 + 完美外推
└─ 但被RoPE取代

2023: NTK-aware / YaRN —— RoPE增强
└─ 解决RoPE外推问题
└─ 训练4096 → 推理32K+

2024: 解耦RoPE —— DeepSeek MLA
└─ RoPE部分不压缩,保持位置精度
└─ 支持极致长上下文

2025: FoPE —— 傅里叶位置编码
└─ 解决频谱损坏问题
└─ ICML 2025,下一代候选

2026: DynaTan —— 动态饱和
└─ DeepSeek V4-Pro
└─ 1M上下文无微调
十、实战:用代码理解位置编码
10.1 Sinusoidal Positional Encoding

import numpy as np
import matplotlib.pyplot as plt

def sinusoidal_positional_encoding(seq_len, d_model):
“”"
生成Sinusoidal位置编码

参数:
    seq_len: 序列长度
    d_model: 模型维度
返回:
    PE: 位置编码矩阵 [seq_len, d_model]
"""
PE = np.zeros((seq_len, d_model))

for pos in range(seq_len):
    for i in range(0, d_model, 2):
        # 偶数维度使用sin
        PE[pos, i] = np.sin(pos / (10000 ** (i / d_model)))
        # 奇数维度使用cos
        PE[pos, i + 1] = np.cos(pos / (10000 ** (i / d_model)))

return PE

生成位置编码

seq_len, d_model = 100, 32
PE = sinusoidal_positional_encoding(seq_len, d_model)

print(f"位置编码形状: {PE.shape}“)
print(f"位置0的编码 (前8维): {PE[0, :8].round(3)}”)
print(f"位置1的编码 (前8维): {PE[1, :8].round(3)}“)
print(f"位置2的编码 (前8维): {PE[2, :8].round(3)}”)

验证不同位置的相似度

pos_0 = PE[0]
pos_5 = PE[5]
pos_50 = PE[50]
cos_sim_0_5 = np.dot(pos_0, pos_5) / (np.linalg.norm(pos_0) * np.linalg.norm(pos_5))
cos_sim_0_50 = np.dot(pos_0, pos_50) / (np.linalg.norm(pos_0) * np.linalg.norm(pos_50))
print(f"\n位置0和位置5的余弦相似度: {cos_sim_0_5:.3f}“)
print(f"位置0和位置50的余弦相似度: {cos_sim_0_50:.3f}”)

结论:距离越近,相似度越高

10.2 RoPE实现

import torch
import torch.nn as nn
import math

class RotaryPositionalEncoding(nn.Module):
“”“旋转位置编码(RoPE)”“”

def __init__(self, d_model, max_seq_len=4096, base=10000.0):
    super().__init__()
    self.d_model = d_model
    self.max_seq_len = max_seq_len
    
    # 预计算旋转频率
    # θ_i = base^(-2i/d_model)
    theta = 1.0 / (base ** (torch.arange(0, d_model, 2).float() / d_model))
    
    # 预计算所有位置的旋转角度
    positions = torch.arange(max_seq_len).float()
    # freqs: [max_seq_len, d_model/2]
    freqs = torch.outer(positions, theta)
    
    # 预计算cos和sin
    # [max_seq_len, d_model/2] → 用view扩展为[max_seq_len, d_model]
    self.register_buffer('cos_cached', freqs.cos().view(max_seq_len, 1, d_model // 2, 1))
    self.register_buffer('sin_cached', freqs.sin().view(max_seq_len, 1, d_model // 2, 1))

def forward(self, q, k, position_ids=None):
    """
    对q和k应用RoPE旋转
    
    参数:
        q: Query [batch, heads, seq_len, d_model]
        k: Key   [batch, heads, seq_len, d_model]
        position_ids: 位置索引(可选,默认从0开始)
    返回:
        q_rotated, k_rotated
    """
    batch, heads, seq_len, d_model = q.shape
    assert d_model == self.d_model
    
    if position_ids is None:
        position_ids = torch.arange(seq_len, device=q.device)
    
    # 获取对应位置的cos和sin
    cos = self.cos_cached[position_ids]  # [seq_len, 1, d_model/2, 1]
    sin = self.sin_cached[position_ids]  # [seq_len, 1, d_model/2, 1]
    
    # 将q和k分成两半,每半代表一个"二维子空间"
    def rotate_half(x):
        """将后一半维度旋转到前一半"""
        x1 = x[..., :x.shape[-1] // 2]  # 前一半
        x2 = x[..., x.shape[-1] // 2:]  # 后一半
        return torch.cat((-x2, x1), dim=-1)
    
    # RoPE: q' = q * cos + rotate_half(q) * sin
    # 注意:需要reshape来匹配cos/sin的维度
    q_reshaped = q.view(batch, heads, seq_len, -1, 2)
    q_rotated = torch.stack([
        q_reshaped[..., 0] * cos.squeeze(-1) - q_reshaped[..., 1] * sin.squeeze(-1),
        q_reshaped[..., 1] * cos.squeeze(-1) + q_reshaped[..., 0] * sin.squeeze(-1)
    ], dim=-1).view(batch, heads, seq_len, d_model)
    
    k_reshaped = k.view(batch, heads, seq_len, -1, 2)
    k_rotated = torch.stack([
        k_reshaped[..., 0] * cos.squeeze(-1) - k_reshaped[..., 1] * sin.squeeze(-1),
        k_reshaped[..., 1] * cos.squeeze(-1) + k_reshaped[..., 0] * sin.squeeze(-1)
    ], dim=-1).view(batch, heads, seq_len, d_model)
    
    return q_rotated, k_rotated

测试RoPE的相对位置特性

d_model = 64
rope = RotaryPositionalEncoding(d_model=d_model, max_seq_len=128)

构造两个查询:一个在位置5,一个在位置10

q = torch.randn(1, 1, 1, d_model) # 相同的查询内容
k = torch.randn(1, 1, 10, d_model) # 10个不同的key

把q复制到位置5和位置10

q_5 = q.clone()
q_10 = q.clone()

q_rotated_5, k_rotated = rope(q_5.expand(1, 1, 1, d_model), k, position_ids=torch.tensor([5]))
q_rotated_10, k_rotated = rope(q_10.expand(1, 1, 1, d_model), k, position_ids=torch.tensor([10]))

计算注意力分数

attn_5 = torch.matmul(q_rotated_5, k_rotated.transpose(-2, -1)) / math.sqrt(d_model)
attn_10 = torch.matmul(q_rotated_10, k_rotated.transpose(-2, -1)) / math.sqrt(d_model)

print(f"位置5的注意力分布: {torch.softmax(attn_5, dim=-1).squeeze().detach().numpy().round(3)}“)
print(f"位置10的注意力分布: {torch.softmax(attn_10, dim=-1).squeeze().detach().numpy().round(3)}”)

注意!两个分布不同 → 位置信息已注入

10.3 ALiBi实现

import torch
import torch.nn as nn

class AliBiAttention(nn.Module):
“”“ALiBi位置编码注意力”“”

def __init__(self, n_heads, d_model):
    super().__init__()
    self.n_heads = n_heads
    self.d_k = d_model // n_heads
    
    # Q、K、V投影
    self.W_Q = nn.Linear(d_model, d_model)
    self.W_K = nn.Linear(d_model, d_model)
    self.W_V = nn.Linear(d_model, d_model)
    
    # 预计算每个头的ALiBi斜率
    self.register_buffer('slopes', self._get_slopes(n_heads))

def _get_slopes(self, n_heads):
    """计算每个头的ALiBi斜率"""
    def get_slopes_power_of_2(n):
        start = 2 ** (-(2 ** -(math.log2(n) - 3)))
        return [start * 2 ** (-i) for i in range(n)]
    
    if math.log2(n_heads).is_integer():
        return torch.tensor(get_slopes_power_of_2(n_heads))
    else:
        # 处理不是2的幂的情况
        n = 2 ** math.floor(math.log2(n_heads))
        slopes = get_slopes_power_of_2(n)
        extra = [slopes[-1] / 2 for _ in range(n_heads - n)]
        return torch.tensor(slopes + extra)

def forward(self, Q, K, V, mask=None):
    batch_size = Q.size(0)
    seq_len = Q.size(1)
    
    # 线性投影 + 重塑
    Q = self.W_Q(Q).view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)
    K = self.W_K(K).view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)
    V = self.W_V(V).view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)
    
    # 标准注意力分数
    scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
    
    # ALiBi: 添加位置偏置
    # 构建距离矩阵
    positions = torch.arange(seq_len, device=Q.device)
    distance = positions.unsqueeze(1) - positions.unsqueeze(0)
    distance = distance.abs().unsqueeze(0).unsqueeze(0)  # [1, 1, seq, seq]
    
    # 每个头乘以对应的斜率
    # slopes: [n_heads] → [1, n_heads, 1, 1]
    alibi_bias = -self.slopes.view(1, self.n_heads, 1, 1) * distance
    
    # 应用ALiBi偏置
    scores = scores + alibi_bias
    
    if mask is not None:
        scores = scores.masked_fill(mask == 0, float('-inf'))
    
    attention_weights = torch.softmax(scores, dim=-1)
    output = torch.matmul(attention_weights, V)
    
    return output.transpose(1, 2).contiguous().view(batch_size, -1, self.n_heads * self.d_k)

测试ALiBi

alibi_attn = AliBiAttention(n_heads=8, d_model=512)
x = torch.randn(2, 10, 512)
output = alibi_attn(x, x, x)
print(f"ALiBi注意力输出形状: {output.shape}")
10.4 NTK-aware Scaled RoPE实现

def ntk_scale_rope_theta(d_model, seq_len, target_len, base=10000.0):
“”"
NTK-aware缩放RoPE的基频率

参数:
    d_model: 模型维度
    seq_len: 训练时的序列长度
    target_len: 目标推理长度
    base: 基频率(默认10000)
返回:
    scaled_theta: 缩放后的基频率
"""
scale = target_len / seq_len
print(f"缩放因子: {scale}")

# 标准RoPE的theta
theta = 1.0 / (base ** (torch.arange(0, d_model, 2).float() / d_model))

# NTK-aware缩放
# 高频(小维度)缩放大 → 保留局部精度
# 低频(大维度)缩放小 → 扩展全局范围
num_dim = d_model // 2
ntk_scale = scale ** (torch.arange(0, num_dim).float() / (num_dim - 1))

scaled_theta = theta / ntk_scale

return scaled_theta

示例

d_model = 64
seq_len = 4096
target_len = 32768 # 8倍外推

scaled_theta = ntk_scale_rope_theta(d_model, seq_len, target_len)
print(f"原始theta (前8个): {scaled_theta[:8].round(3)}“)
print(f"缩放后theta (前8个): {scaled_theta[:8].round(3)}”)
print(f"高频维度缩放后: {scaled_theta[0].item():.2e} (原始: {1/10000**(0/64):.2e})“)
print(f"低频维度缩放后: {scaled_theta[-1].item():.2e} (原始: {1/10000**(62/64):.2e})”)
📌 总结
位置编码核心要点:

1️⃣ 为什么需要位置编码
Attention本身是"词袋"
没有位置信息,“我打你”=“你打我”
位置编码是序列模型的关键组件

2️⃣ 主流方案演进
Sinusoidal(2017) → 可学习(2018) → 相对bias(2019)
→ RoPE(2021) ← 当前主流
→ ALiBi(2022) → NTK/YaRN(2023)
→ 解耦RoPE(2024) → FoPE(2025) ← 最新突破

3️⃣ RoPE为什么是主流
旋转编码 → 相对位置
零延迟 → 不干扰语义
可外推 → 理论支持任意长度

4️⃣ 长度外推技术
位置插值(2023):压缩位置索引
NTK-aware(2023):频率按维度缩放
YaRN(2024):波长分析+温度调整
DynaTan(2026):动态饱和,1M上下文

5️⃣ FoPE(2025最新)
解决RoPE的"频谱损坏"
傅里叶级数建模 + 直流分量
长文本外推全面超越RoPE
🔗 延伸阅读
【AI基础篇01】AI大模型基础概念全景图

【AI基础篇02】从Transformer到GPT:生成式AI的演进史

【AI基础篇03】大模型参数、算力、数据:Scaling Law的本质

【AI基础篇04】Tokenization:文本如何变成数字,为什么分词器这么重要

【AI基础篇05】注意力机制:Self-Attention详解

【AI基础篇07】预训练 vs 微调 vs 提示工程

觉得有帮助?点赞收藏!下一篇我们讲预训练 vs 微调 vs 提示工程——同一个模型,三种完全不同的使用方式,它们各自的原理和适用场景是什么? 🚀

标签:人工智能、位置编码、RoPE、ALiBi、FoPE、NTK-aware、YaRN、Transformer、大模型、长上下文

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐