原理
在之前的编码器 - 解码器介绍中,我们发现模型并没有记录时序相关信息,即没有感知不同词汇的位置顺序。这会导致一个问题,针对'我喜欢你'这句话,经过 Embedding 处理后,再进入编码器 - 解码器处理,最后生成的内容,是和输入'你喜欢我'最后生成的内容是一样的,但我们知道,这两句是含义完全不一样的语句。
加入位置编码,可以解决这个问题。位置编码通过给每个位置添加一个向量,这个向量包含了位置信息,然后把这个向量加到词汇向量上。
例如:
位置 1 向量:[0.1,0.2,0.3,...]
位置 2 向量:[0.4,0.5,0.6,...]
位置 3 向量:[0.7,0.8,0.9,...]
'我喜欢你',添加位置编码后:
'我'在位置 1:'我'的词向量 + 位置 1 向量
'喜欢'在位置 2:'喜欢'的词向量 + 位置 2 向量
'你'在位置 3:'你'的词向量 + 位置 3 向量
经过这样处理,Transformer 就可以区分词的位置了。
实现
在 Transformer 中,使用的是正弦位置编码。
正弦位置编码详解
- 参数
d_model: 8max_len: 10
- 位置索引
position: [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
- 分母项
div_term: [1.0, 0.3162, 0.1, 0.0316]- 解释:
div_term = 10000^(-2i/d_model) i是维度索引(0, 2, 4, 6, ...),用于控制不同维度的频率
- 位置编码矩阵
- 形状:
torch.Size([10, 8]) - 数据:
位置 0: [0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0] 位置 1: [0.8415, 0.5403, 0.3129, 0.9499, 0.0998, 0.9950, 0.0316, 0.9995] 位置 2: [0.9093, -0.4161, 0.5946, 0.8040, 0.1987, 0.9801, 0.0632, 0.9980] 位置 3: [0.1411, -0.9900, 0.8120, 0.5835, 0.2955, 0.9553, 0.0948, 0.9955] 位置 4: [-0.7568, -0.6536, 0.9516, 0.3073, 0.3894, 0.9211, 0.1263, 0.9920] ...
- 形状:
- 公式与含义
- 公式:
PE(pos, 2i) = sin(pos / 10000^(2i/d_model)) PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model)) - 含义:
pos: 位置索引(0, 1, 2, ...)2i: 偶数维度(0, 2, 4, ...)2i+1: 奇数维度(1, 3, 5, ...)
- 特点:
- 偶数维度用 sin,奇数维度用 cos
- 不同维度有不同的频率
- 公式:
- 可视化与作用
- 位置编码的每个维度:
- 维度 0:
sin(pos * freq_0) - 维度 1:
cos(pos * freq_0) - 维度 2:
sin(pos * freq_1) - 维度 3:
cos(pos * freq_1) - ...
- 维度 0:
- 不同维度的频率:
- 低维度:高频率(快速变化),捕捉局部位置
- 高维度:低频率(慢速变化),捕捉全局位置
- 位置编码的每个维度:
位置编码添加过程
- 词向量
- 形状:
torch.Size([1, 3, 8]) - 数据:
位置 0: [0.1234, -0.5678, 0.9012, -0.3456, 0.7890, -0.1234, 0.5678, -0.9012] 位置 1: [0.2345, -0.6789, 0.0123, -0.4567, 0.8901, -0.2345, 0.6789, -0.0123] 位置 2: [0.3456, -0.7890, 0.1234, -0.5678, 0.9012, -0.3456, 0.7890, -0.1234]
- 形状:
- 位置编码
- 形状:
torch.Size([3, 8]) - 数据:
位置 0: [0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0] 位置 1: [0.8415, 0.5403, 0.3129, 0.9499, 0.0998, 0.9950, 0.0316, 0.9995] 位置 2: [0.9093, -0.4161, 0.5946, 0.8040, 0.1987, 0.9801, 0.0632, 0.9980]
- 形状:
- 添加位置编码后
- 形状:
torch.Size([1, 3, 8]) - 数据:
位置 0: [0.1234, 0.4322, 0.9012, 0.6544, 0.7890, 0.8766, 0.5678, 0.0988] 位置 1: [1.0760, -0.1386, 0.3252, 0.4932, 0.9899, 0.7605, 0.7105, 0.9872] 位置 2: [1.2549, -1.2051, 0.7180, 0.2362, 1.0999, 0.6345, 0.8532, 0.8746] - 计算:
输出 = 词向量 + 位置编码
- 形状:
- 总结
- 步骤:
- 获取词向量
- 获取位置编码
- 词向量 + 位置编码
- 结果:每个词的向量包含了位置信息,Transformer 可以区分不同位置的词
- 数据流动:
输入 (1, 3, 8) ↓ Embedding 词向量 (1, 3, 8) ↓ + 位置编码 (1, 3, 8) 输出 (1, 3, 8)
- 步骤:
class PositionalEncoding(nn.Module):
"""位置编码模块"""
def __init__(self, args):
super(PositionalEncoding, self).__init__()
# Dropout 层
# self.dropout = nn.Dropout(p=args.dropout)
# block size 是序列的最大长度
pe = torch.zeros(args.block_size, args.n_embd)
position = torch.arange(0, args.block_size).unsqueeze(1)
# 计算 theta
div_term = torch.exp(torch.arange(0, args.n_embd, 2) * -(math.log(10000.0) / args.n_embd))
# 分别计算 sin、cos 结果
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0)
self.register_buffer("pe", pe)
def forward(self, x):
# 将位置编码加到 Embedding 结果上
x = x + self.pe[:, :x.size(1)].requires_grad_(False)
return x
位置编码类型
| 类型 | 特点 | 优点 | 缺点 | 使用模型 |
|---|---|---|---|---|
| 正弦位置编码 | 固定公式 | 不需要参数,可外推 | 不能学习 | Transformer |
| 可学习的位置编码 | 可以学习 | 效果可能更好 | 需要参数,不能外推 | BERT、GPT |
| 旋转位置编码 | 相对位置 | 适合长序列 | 实现复杂 | LLaMA,GPT-NeoX |

