如何更好地理解位置编码的本质
位置编码 P的本质 其实就是给输入加上一个偏置量Q,使其能够在表示自己原本特征信息的同时,体现自己的位置信息
我们可以把输入X,当做一个 1 * max_len * num_hiddens ,
下面用一个例子来理解 假设 max_len = 3,num_hiddens = 4
现在用一个三个词的句子来表示 : 我爱你
用矩阵表示如下:
#我们所需要的位置编码形状 应该与 我们的输入句子一直,(应该最后是P + X)
#可以这样理解,这里的 max_len表示每个词
#而这里的num_hiddens则表示每个词的特征值,而我们的需求就是得到带上位置权重的特征值
[
[
[0, 0, 0, 0] #对应 我 + 位置编码 (假设) [1.0,2.0,1.2,2.3] = 带上位置信息的编码
],
[
[0, 0, 0, 0]#对应 爱
],
[
[0, 0, 0, 0]#对应 你
],
]
这里有一个重要的概念 div_term 衰减因子
这里对 torch.arange(0, num_hiddens, 2, dtype=torch.float32)有个关键的处理,很多人会问,这个2是哪里来的呢?有什么作用?
import torch
print(torch.arange(0,4,2))
#可以看到这个输出形状是tensor([0, 2])
#这个函数的作用是是 PyTorch 中生成整数序列的函数,三个参数分别代表「起始值」「结束值」「步长]
"""
意思是把num_hidden获取其所有偶数的值(就是偶数下标)
这里把hum_hidden分一半之后,这里的位置下标就只有[0,2,4,6,...]
如下:
torch.arange(0, 4, 2) 生成 [0, 2](两个偶数索引)→ 对应两个维度组
第 0 组:k=0 → 偶数维度 0(2×0)和奇数维度 1(2×0+1),共享 div_term[0]。
第 1 组:k=1 → 偶数维度 2(2×1)和奇数维度 3(2×1+1),共享 div_term[1]。
即 div_term[0]可以用来表示 实际上 偶数位置(0)的衰减因子,也可以用来表示奇数位置(1)的衰减因子,
同理:
div_term[1]可以用来表示 实际上 偶数位置(2)的衰减因子,也可以用来表示奇数位置(3)的衰减因子,
所以这里的num_hidden都会是偶数个
然后就是 除以 num_hiddens
这是对其归一化操作(和softmax同理)
"""
``
#TODO:这一步 衰减因子选择 10000 作为底数(即 10000^指数),核心是为了让不同维度的位置编码产生明显的 “频率差异”
# div_term = torch.pow(10000, torch.arange(0, num_hiddens, 2, dtype=torch.float32) / num_hiddens)
#即 [0,2] -> [0,0.5] -> [10000 * 0 = 1,10000 * 0.5 = 100]
#TODO:下一步 X = X / div_term
"""
X = torch.arange(max_len, dtype=torch.float32).reshape(-1, 1)
这里的X生成的形状是[max_len,1]
"""
import torch
div_term = torch.tensor([1,100])
X = torch.arange(3, dtype=torch.float32).reshape(-1, 1)
X.shape
X = X / div_term
X.shape
这里很关键的一点:为什么X是这样的,并且 X要除以 div_term
我们先来看X = X / div_term的结果的形状是:torch.Size([3, 2])
这整个计算过程是这样的:
X = [
[0],
[1],
[2]
]
X / [1,100]
0 / 1 = 0; 0 / 100 = 0 -> [0,0]
1 / 1 = 1;1 / 100 = 0.01 -> [1,0.01]
2 / 1 = 2;2 / 100 = 0.02 -> [2,0.02]
利用广播机制实现位置索引与衰减因子的逐元素运算,生成维度匹配的中间结果;
通过衰减因子的差异,让不同维度组的位置值呈现 “高频 - 低频” 的梯度,从而让后续的正弦 / 余弦函数能编码多尺度的位置关系。
这个X有什么实际意义呢?
这里的shape[3,2]
这里的3,可以理解为3个词,用最开始的话就是分别为 我,爱,你
这里的2有什么意义呢?这个2 是怎么来的呢,就是所以num_hidden 除以2来的,把所有的隐藏维度按照偶数来划分,并且奇偶公用一个缩放因子
# self.P[:, :, 0::2] = torch.sin(X) # 0::2 表示从0开始,步长为2(偶数索引)
# self.P[:, :, 1::2] = torch.cos(X) # 1::2 表示从1开始,步长为2(奇数索引)
#TODO:这段代码有什么意义呢?
import torch
X = torch.tensor([
[0.0,0.00],
[1.0,0.01],
[2.0,0.02]
])
P = torch.zeros(1,3,4)
P[:, :, 0::2] = torch.sin(X)#这里表示偶数位置
P[:, :, 1::2] = torch.cos(X)#这里表示奇数位置
#因为P的形状 是 1 * 3 * 4
"""
我们从一层一层的看
第一层:
注意下标的奇偶性
[sin(0.0),cos(0.0),sin(0.0),cos(0.0)]
第二层
[sin(1.0),cos(1.0),sin(0.01),cos(0.01)]
第三层
[sin(2.0),cos(2.0),sin(0.02),cos(0.02)]
"""
print(P.data)
#TODO;最后生成的P矩阵,就是对应的每层的(就是每个词)内部的位置信息了
显而易见,我们得到位置矩阵之后,只需要将原来的特征矩阵 加上 位置矩阵,就得到我们需要的结果了。
import torch
P = torch.tensor([[[ 0.0000, 1.0000, 0.0000, 1.0000],
[ 0.8415, 0.5403, 0.0100, 0.9999],
[ 0.9093, -0.4161, 0.0200, 0.9998]]])
#最后相加,我们只要保证两个矩阵的形状是一致的就好啦
#我们现在随机生成一个特征矩阵
input = torch.rand((1,3,4))
print(input.data,end='\n')
result = P + input
print(result.data)
我们现在就得到了带上位置编码的特征矩阵啦
现在我再提出个问题去思考
为什么要采用三角函数呢?
下面是实现的整体代码
import torch
from torch import nn
#TODO:实现位置编码(基于正弦函数与余弦函数)
class PositionEcoding(nn.Module):
def __init__(self, num_hiddens, dropout=0.1, max_len=5000):
super(PositionEcoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
self.P = torch.zeros(1, max_len,num_hiddens)
X = torch.arange(max_len, dtype=torch.float32).reshape(-1, 1)
"""
torch.arange(0, 4, 2) 生成 [0, 2](两个偶数索引)→ 对应两个维度组
第 0 组:k=0 → 偶数维度 0(2×0)和奇数维度 1(2×0+1),共享 div_term[0]。
第 1 组:k=1 → 偶数维度 2(2×1)和奇数维度 3(2×1+1),共享 div_term[1]。
"""
div_term = torch.pow(10000, torch.arange(0, num_hiddens, 2, dtype=torch.float32) / num_hiddens)
X = X / div_term # 形状为 (max_len, num_hiddens//2)
# 偶数维度用正弦函数,奇数维度用余弦函数
self.P[:, :, 0::2] = torch.sin(X) # 0::2 表示从0开始,步长为2(偶数索引)
self.P[:, :, 1::2] = torch.cos(X) # 1::2 表示从1开始,步长为2(奇数索引)
def forward(self, inputs):
inputs = inputs + self.P[:, :inputs.shape[1], :]
return self.dropout(inputs)
PositionEcoding(num_hiddens=4, dropout=0.1, max_len=3)