位置编码:绝对位置编码、相对位置编码、旋转位置编码

发布时间:2026/6/12 2:57:05

位置编码:绝对位置编码、相对位置编码、旋转位置编码 文章目录前言一、为什么需要位置编码二、绝对位置编码2.1可学习的绝对位置编码2.2固定的绝对位置编码三、相对位置编码四、旋转位置编码RoPE五、总结参考前言模型本身是“无状态”的它看不到句子里单词的顺序比如“我吃苹果”和“苹果吃我”在模型眼里如果不做处理输入的token是一样的那它怎么区分语序、理解语义呢答案就是「位置编码」——它就像给每个单词贴了一个“坐标标签”告诉模型“这个单词在句子里排第1位、那个排第3位”让模型能捕捉到语序带来的语义差异。关于位置编码主要分为三类绝对位置编码Absolute Positional Encoding、相对位置编码Relative Positional Encoding以及近年来大火的旋转位置编码Rotary Position Embedding, RoPE。一、为什么需要位置编码先举个简单的例子句子1我 吃 苹果句子2苹果 吃 我这两句话的单词完全一样但语义天差地别。如果没有位置编码Transformers的自注意力机制只会关注“哪些单词相关”却不知道“这些单词在什么位置”自然无法区分这两句话。位置编码的作用就是给每个单词添加“位置信息”让模型知道“谁在前、谁在后”从而理解语序带来的语义变化——使每个token都有自己的位置第1位、第2位…。二、绝对位置编码绝对位置编码的思路给句子里的每个位置分配一个唯一的“位置向量”这个向量和单词本身的词向量相加就能让模型知道“这个单词在第几个位置”。为什么要相加而不是相乘或拼接1.如果采用拼接后期经过线性变换回原来的维度增加模型的参数量如果采用相乘则会引入更多的计算量增加模型的训练时间。2.在高维空间中两个向量呈现出正交性即他们的点乘向量为0。3.相加操作提供了更优的梯度传播路径。在反向传播时损失函数的梯度可以无阻碍地同时反向传播到词嵌入和位置编码分支。如果拼接则会引入投影向量造成梯度消失或者梯度爆炸如果相乘则两者梯度会产生依赖使得计算过程变得复杂。在经典的BERT、GPT-1等模型中绝对位置编码主要有两种实现方式分为可学习的绝对位置编码和固定的绝对位置编码2.1可学习的绝对位置编码核心逻辑初始化一个形状为「max_seq_len × d_model」的位置矩阵max_seq_len是最大序列长度d_model是词向量维度该矩阵作为可训练参数与词向量逐元素相加。importtorchimporttorch.nnasnnclassLearnablePositionalEncoding(nn.Module):def__init__(self,d_model,max_seq_len512):super().__init__()self.pos_embnn.Parameter(torch.randn(max_seq_len,d_model))defforward(self,x):# x: [batch_size, seq_len, d_model]seq_lenx.shape[1]returnxself.pos_emb[:seq_len,:]2.2固定的绝对位置编码Transformer原论文使用的方式提前用正弦/余弦函数生成位置向量无需训练。对于位置为pos的单词其位置向量的第i维有以下编码规则PE(posⓜ,2i)sin(pos/10000^(2i/d_model ) ) i为偶数PE(posⓜ,2i1)cos(pos/10000^(2i/d_model ) ) i为奇数公式解读pos是单词在句子中的位置从0或1开始d_model是词向量维度通过不同频率的正弦/余弦函数让不同位置的向量具有唯一性且位置越近向量相似度越高。为什么这里要用正弦/余弦函数表示位置信息1.正弦/余弦函数频率递减设计保证了位置相近时变化范围小。2.正弦/余弦函数具有合角公式sin(ak)sin(a)×cos(k)sin(k)×cos(a)sin(a-k)sin(a)cos(k)-sin(k)cos(a)cos(a-k)cos(a)cos(k)sin(a)sin(k)cos(ak)cos(a)cos(k)-sin(a)sin(k)因此可以用绝对位置编码来表示相对位置信息。3.函数平滑、连续不会出现梯度消失/爆炸的情况。importtorchimportmathclassSinCosPositionalEncoding(nn.Module):def__init__(self,d_model,max_seq_len512):super().__init__()pos_enctorch.zeros(max_seq_len,d_model)postorch.arange(0,max_seq_len,dtypetorch.float).unsqueeze(1)div_termtorch.exp(torch.arange(0,d_model,2).float()*(-math.log(10000.0)/d_model))pos_enc[:,0::2]torch.sin(pos*div_term)pos_enc[:,1::2]torch.cos(pos*div_term)pos_encpos_enc.unsqueeze(0)self.register_buffer(pos_enc,pos_enc)defforward(self,x):# x: [batch_size, seq_len, d_model]returnxself.pos_enc[:,:x.shape[1],:]绝对位置编码实现简单、容易训练能清晰地告诉模型“每个单词的具体位置”在短序列任务比如句子分类、情感分析中表现很好。但是其灵活性差无法捕捉“相对位置关系”。比如“我 爱 西瓜”和“西瓜 爱 我”中“爱”分别在第2位和第3位绝对位置编码会认为这两个“爱”的位置完全不同但实际上它们的“相对位置”前后都是两个单词是相似的另外当序列很长时模型很难记住“第100位”和“第101位”的差异位置信息会逐渐弱化。三、相对位置编码绝对位置编码的思路不关注单词的“绝对位置”只关注单词之间的“相对距离”绝对位置编码关注“你是第5位”而相对位置编码关注“你在我前面1位”“他在我后面2位”只需要知道“你前面的人是谁、后面的人是谁”就能知道自己的相对位置——这就是相对位置编码的核心。相对位置编码的实现核心是在自注意力计算中融入“相对距离”信息公式如下其中R 是相对位置偏置矩阵形状为「seq_len × seq_len」R_(i,j) 代表第i个单词与第j个单词的相对距离对应的偏置值提前预定义或可训练。importtorchimporttorch.nnasnnclassRelativePositionalEncoding(nn.Module):def__init__(self,d_model,max_seq_len512):super().__init__()self.d_modeld_model# 预定义相对位置偏置范围-max_seq_len1 到 max_seq_len-1self.max_rel_distmax_seq_len-1self.rel_pos_biasnn.Parameter(torch.randn(2*self.max_rel_dist1,d_model))defget_rel_pos(self,seq_len):# 计算每个位置对的相对距离i - jpostorch.arange(seq_len,dtypetorch.long).unsqueeze(0)-torch.arange(seq_len,dtypetorch.long).unsqueeze(1)# 映射到0~2*max_rel_dist的范围避免负索引pospos.clamp(-self.max_rel_dist,self.max_rel_dist)self.max_rel_distreturnposdefforward(self,q,k):# q: [batch_size, num_heads, seq_len_q, d_k]# k: [batch_size, num_heads, seq_len_k, d_k]seq_len_q,seq_len_kq.shape[2],k.shape[2]# 获取相对位置矩阵rel_posself.get_rel_pos(max(seq_len_q,seq_len_k))# 获取相对位置偏置rel_biasself.rel_pos_bias[rel_pos[:seq_len_q,:seq_len_k]]# 计算注意力权重简化版忽略多头细节attntorch.matmul(q,k.transpose(-2,-1))/torch.sqrt(torch.tensor(self.d_model,dtypetorch.float))# 加入相对位置偏置attnrel_bias.unsqueeze(0).unsqueeze(0)returnattn举个例子句子“我 爱 吃 西瓜”中“爱”和“我”的相对距离是1“我”在“爱”前面1位“爱”和“吃”的相对距离是1“吃”在“爱”后面1位“爱”和“西瓜”的相对距离是2“西瓜”在“爱”后面2位——相对位置编码会用不同的向量表示“相对距离1”“相对距离2”从而让模型捕捉到这种关系。相对位置编码灵活性强能捕捉单词之间的相对关系更符合人类语言习惯在长序列任务比如文本生成、长文档理解中表现更好因为即使序列很长“相对距离”依然清晰比如“第100位”和“第101位”的相对距离是1和“第2位”与“第3位”的相对距离一样模型能轻松捕捉。但是其实现更复杂训练难度稍高在某些需要“绝对位置”的任务比如句子排序中表现不如绝对位置编码相对位置偏置矩阵的维度会随序列长度增加而增大占用更多内存。四、旋转位置编码RoPERoPERotary Position Embedding旋转位置编码是近年来最流行的位置编码方式被LLaMA、ChatGLM、Qwen等主流大模型广泛采用——它的核心优势是使用绝对位置信息能够表示相对位置关系同时解决了长序列位置信息弱化、内存占用高的问题。类比一下如果说绝对位置编码是“给每个单词贴固定学号”相对位置编码是“记录单词之间的距离”那么RoPE就是“给每个单词的词向量做‘旋转’”——位置越靠后旋转角度越大两个单词的相对位置就对应它们词向量的“旋转角度差”既知道“各自转了多少度绝对位置”也知道“彼此差多少度相对位置”。RoPE的核心原理通过旋转矩阵对词向量或注意力中的Q、K向量进行旋转旋转角度与单词的绝对位置正相关从而将位置信息融入词向量中。对于位置为m的单词其词向量的第2i维和第2i1维成对旋转旋转矩阵如下其中θi10000^(-2i/d_model)和正弦余弦编码的频率一致m是token在句子中的绝对位置。上述公式可能对于理解旋转位置编码还是有些困难下面是一个具体的案例对于嵌入维度 d4我们把维度分成 2 组两两配对的维度(0,1) 和 (2,3)每组对应一个独立的旋转角度。设位置为 t则第 i 组i0,1的旋转角度为第 0 组维度 0,1第 1 组维度 2,3位置 t 的旋转矩阵是块对角矩阵query向量与key向量之间的内积先计算两个旋转矩阵之间的乘积令Δm−n根据上述表达式可以看到整个内积结果只和原始词嵌入xm​,xn​语义信息和相对位置差 Δm−n位置信息相关完全不依赖绝对位置 m 或 n 本身完美验证了等式这个等式是怎么来的呢下面是其详细推导过程对于query向量的映射矩阵假设嵌入维度为2则有以下等式Wq是一个2×2的矩阵xm是一个2×1的矩阵根据复数的性质[m,n][min]则有以下等式继续展开同理那么key向量按照同样的操作根据自注意力机制计算公式query向量与key向量的转置相乘的结果根据三角函数的积化和差公式对上述公式进行合并至此融合m的query向量与融合n的key向量乘积可以准确表示为融合m、n以及m-n的相对位置信息。其旋转位置编码代码为importtorchimportmathclassRotaryPositionalEncoding(nn.Module):def__init__(self,d_model,max_seq_len512):super().__init__()self.d_modeld_model# 计算每个维度对的旋转频率 theta_itheta1.0/(10000.0**(torch.arange(0,d_model,2,dtypetorch.float32)/d_model))self.register_buffer(theta,theta)defforward(self,x):# x: [batch_size, seq_len, d_model]seq_lenx.shape[1]# 生成绝对位置 pos0到seq_len-1postorch.arange(seq_len,dtypetorch.float32,devicex.device).unsqueeze(1)# 计算旋转角度pos * theta形状为 [seq_len, d_model/2]anglespos*self.theta# 生成旋转矩阵的余弦和正弦重复一次适配所有维度cos_anglestorch.cos(angles).repeat_interleave(2,dim1)sin_anglestorch.sin(angles).repeat_interleave(2,dim1)# 对词向量进行旋转成对旋转x_rottorch.empty_like(x)x_rot[:,:,0::2]x[:,:,0::2]*cos_angles-x[:,:,1::2]*sin_angles x_rot[:,:,1::2]x[:,:,0::2]*sin_anglesx[:,:,1::2]*cos_anglesreturnx_rot五、总结三种位置编码它们没有“谁更好”只有“谁更适合”具体的任务和场景。①如果是短序列、不需要太多长距离依赖比如给句子打标签用绝对位置编码就足够了简单高效位置外推失效训练时最大序列长度是固定的比如 512/1024位置向量只学到了这个范围内的模式。当测试序列超过这个长度比如2048模型从未见过的新位置向量会破坏注意力分布性能急剧下降。 绝对位置干扰注意力分数 ⟨qm​,kn​⟩ 会混入 pm​ 和 pn​ 的绝对位置信息模型会混淆 “语义关联” 和 “绝对位置”无法泛化到不同长度的序列。②如果是长序列、需要捕捉单词之间的相对关系比如写文章、理解长文档且模型规模不大用相对位置编码即可长序列显存爆炸需要维护一个大小为 L×L 的相对位置偏置矩阵L 是序列长度当 L 达到 10k时显存占用呈平方级增长完全不可行。 长序列上限固定训练时会预设最大相对位置差比如±512超过这个范围的相对位置会被截断长序列中远距离 token 的位置信息丢失。③如果是大模型训练、长序列生成比如ChatGPT类应用追求性能和效率RoPE旋转位置编码是首选兼顾所有优势。天然支持无限外推无固定长度限制RoPE 用旋转角度 θi​10000−2i/d 定义位置信息位置 t 对应的旋转角度是tθi​不需要预定义最大序列长度。 数学上可无限延伸无论 t多大都能计算出对应的旋转矩阵测试时序列长度可以远超训练时的长度不会出现 “未知位置向量” 的问题。纯相对位置依赖内积只与相对位置有关如你之前推导的⟨fq​(xm​,m),fk​(xn​,n)⟩g(xm​,xn​,m−n)注意力分数完全由语义和相对位置差决定与绝对位置m,n 无关。 泛化性极强在短序列上学到的 “相对位置模式”比如“前两个词”“后三个词”可以直接迁移到长序列模型不需要重新学习长距离位置关系。显存友好无额外大矩阵RoPE 是对词嵌入的逐元素旋转操作只在计算 Q/K 时动态应用不需要维护额外的位置偏置矩阵显存占用和原始Transformer 几乎一致线性复杂度 O(Ld)完美支撑超长序列比如 100k另外现在很多主流模型比如LLaMA、GPT-4都会基于RoPE做优化进一步提升长序列处理能力比如LLaMA 2的RoPE扩展、ChatGLM的动态RoPE等本质上都是在保留RoPE核心优势的基础上解决更多场景的需求。参考一文通透位置编码从标准位置编码、复数、欧拉公式到旋转位置编码RoPE(含其推导与代码实现)

相关新闻