人工智能-位置编码
1 绝对位置编码
1.1 公式
绝对位置编码(Absolute Positional Encoding)是在每个输入序列的元素上加上位置信息,以表示该元素在序列中的具体位置。这个位置向量通常通过固定的函数生成,与输入数据无关。通常使用的是正弦和余弦函数,这样生成的编码具有很强的周期性,能够捕捉序列中的相对位置信息。位置编码就行给一堆人贴上一个位置序号,以后就能识别出来位置信息。看了很多文章要么只讲逻辑,要么只贴代码,即使同时贴了代码和逻辑的,也对代码不做解读。这里就记录下来给大家分享一下,让AI不再那么神秘,有兴趣都朋友多关注,加强交流分享。绝对位置编码的公式如下:


1 d-model是嵌入维度
2 其中 i 为维度索引,取值范围为 0 ≤ i < d_model/2
3 PE(pos,2i) 表示词位置2i表示词位置嵌入的偶数位置
4 PE(pos,2i+1) 表示词位置2i表示词位置嵌入的偶数位置
5 pos 其实对应每次词的行,一行的偶数列用sin,奇数列用cos计算
1.2 原理
一、为什么能标识先后顺序
- 每个位置pos的向量,是由多组不同频率的(sin(pos), cos(pos))二维正交基拼接而成,每一组都对应一个唯一的旋转角度。
- 位置每往后移动1步,所有组的向量都会按各自的固定角速度同步旋转一个固定角度,位置越靠后,累计旋转的总角度就越大。
- 不同位置的向量旋转状态完全不同,模型可以直接通过向量的角度差异,判断两个token谁在前、谁在后,不会出现顺序混淆。
二、为什么能直接计算相对距离
- 利用三角函数的和差化积公式,位置pos的向量旋转k步后,就能精准得到位置pos+k的向量,变换过程只和间隔步数k有关,和绝对位置pos完全无关。
- 两个不同位置的向量做内积运算,最终得到的结果只由它们的相对距离差决定,绝对位置信息会被自动抵消。
- 比如pos=2和pos=5的向量内积,和pos=10和pos=13的向量内积结果完全相等,模型不需要额外训练,就能直接通过内积数值得到两个token之间相隔多少步。
1.3 缺陷
一、单维度三角函数的周期性本质
- 正余弦函数的周期固定为2π(360°),当两个位置的相位差刚好接近2π的整数倍时,该维度上的sin/cos输出值几乎完全相等。
- 比如相位差为2π+0.1和相位差为0.1的两个点,单维度的函数输出值几乎一致,单维度相似度接近1,完全无法区分长距离和短距离。
二、多频率维度的组合失效
- Transformer位置编码使用的频率序列是
1/10000^(2i/d_model),低频维度的周期极长,高频维度的周期极短。 - 当序列长度足够大时,大量高频维度的相位差会同时落在2π整数倍附近,多个维度的输出值同步趋同,最终导致两个距离极远的位置,整体向量的余弦相似度异常升高。
三、位置编码的设计固有缺陷
- 原始正余弦位置编码没有对长距离的相位差做约束,随着pos不断增大,不同位置的向量空间分布会逐渐出现周期性重叠。
- 远距离位置的向量在高维空间中会意外靠近,模型无法通过向量内积准确区分长距离的间隔,最终导致长序列场景下的位置感知能力失效。
- 所以延伸了其他的编码方案。RoPE(Rotary Position Embedding,旋转位置编码)是目前大模型的标配位置编码方案,核心逻辑是通过向量旋转操作,把绝对位置信息注入到Q、K向量中,最终让注意力得分天然只依赖token之间的相对位置,完美解决了原始正余弦位置编码的长距离相似度异常、外推能力差的问题
下面上一个教科书上的代码
class PositionalEncoding(torch.nn.Module):
def __init__(self, d_model = 312, dropout = 0.05, max_len=80):
"""
:param d_model: pe编码维度,一般与word embedding相同,方便相加
:param dropout: dorp out
:param max_len: 语料库中最长句子的长度,即word embedding中的L
"""
super(PositionalEncoding, self).__init__()
# 定义drop out
self.dropout = torch.nn.Dropout(p=dropout)
# 计算pe编码
pe = torch.zeros(max_len, d_model) # 建立空表,每行代表一个词的位置,每列代表一个编码位
position = torch.arange(0, max_len).unsqueeze(1) # 建个arrange表示词的位置以便公式计算,size=(max_len,1)
#计算公式中10000**(2i/d_model)
#torch.arange(0, d_model, 2) = 2i
div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term) # 计算偶数维度的pe值
pe[:, 1::2] = torch.cos(position * div_term) # 计算奇数维度的pe值
pe = pe.unsqueeze(0) # size=(1, L, d_model),为了后续与word_embedding相加,意为batch维度下的操作相同
self.register_buffer('pe', pe) # pe值是不参加训练的
def forward(self, x):
# 输入的最终编码 = word_embedding + positional_embedding
x = x + self.pe[:, :x.size(1)].clone().detach().requires_grad_(False)
print('x shape = {}'.format(x.shape))
return self.dropout(x) # size = [batch, L, d_model]
div_term这个地方代码估计不容易看懂,反正我这种数学思维不太敏捷的看了几眼都没有明白,现在的教科书没有发现一本能写的简单透彻的,不是这个东西有多难,而是讲的人没讲好,一笔带过,甚至都不提一下,代码一贴就完事。很多人不是说数学不好,而是太久了记不到。因为代码计算逻辑和公式看起来长得并不一样,这里给一下推导过程。
首先给简单的公式 10 = e^ln10。这个其实很简单 设in10 = b,那么 10 = e^b,就得出了结论成立,PE(pos,2i)公式就变成了:
![]()
于是除了多了一个负号div_term计算公式就是右边的样子
公式中的负号,本质是对指数运算的符号约定。
若不加负号,exp(i⋅(log(10000.0)/d_model)) 会导致频率随维度随i增大而指数增长(频率越高,周期越短,相邻维度的正弦/余弦函数差异过大,无法有效区分位置信息)。
加上负号后exp(−i⋅(log(10000.0)/d_model)) 使频率随维度i增大而指数衰减(频率越低,周期越长,相邻维度的正弦/余弦函数差异适中,能平衡“区分位置”与“维度间差异”的需求)
公式中的 10000是“波长缩放因子”,其对数 log(10000) 决定了频率衰减的“速度”:
10000 越大,频率衰减越平缓(相邻维度的正弦/余弦函数更相似)
10000 越小,频率衰减越陡峭(相邻维度的正弦/余弦函数差异越大)
在Transformer的实验中,10000 是经验值,能平衡不同维度的频率差异,使位置编码在序列中有效传递位置信息
讲一下代码中的主要逻辑,以一个句子 “我很牛逼”为例(词嵌入可以参考我其他文章):
分词后成为 [我,很,牛逼], 句子长度就是3,因为可能有很多句子,那么我们可以设置一个最大长度,比如80,不足部分就使用0代表空填充,这样我很牛逼就变成了[我,很,牛逼,0,0,....0] 然后这个句子列表的词是使用312长度来编码一个词,就变成了 80 x 312 [ [],[],[]......[]]二维数组。
代码中的pe就初始成 80 x 312 与输入句子成形状,这样才能相加.
div_term就是每个2i的乘积因子数组,当i=0时,pos=0 ,PE(0,0)-sin,PE(0,1)-cos,即为第一行数据312列中第一列和第二列。所以pe[:, 0::2],pe[:, 1::2] 刚好把pe矩阵所有行的位置参数都算出来了。
最后将pe与输入矩阵相加就将位置信息标记到了输入矩阵上。
2 相对位置编码
更多推荐
所有评论(0)