Transformer模型因为只有attention计算,没有类似RNN的时序信息,而文本中,序列信息是十分重要的信息,因此需要额外的位置编码信息。位置编码可以分为绝对位置编码和相对位置编码。
Transformer 的架构中没有内置处理序列顺序的机制,需要通过位置编码显式地为模型提供序列中单词的位置信息,以更好地学习序列关系。位置编码通常通过数学函数生成,目的是为每个位置生成一个独特的向量。这些向量在嵌入空间中具有特定的性质,比如周期性和连续性。Transformer的自注意力是一种集合运算,这意味着它是置换等变的。如果我们不利用位置信息来丰富自注意力,就无法确定许多重要的关系。
举例说明最能说明这一点。考虑一下这个句子,其中同一个单词出现在不同的位置:
「这只狗追赶另一只狗」
直观地看,「狗」指的是两个不同的实体。如果我们首先对它们进行 token 化,映射到 Llama 3.2 1B 的真实 token 嵌入,并将它们传递给 torch.nn.MultiheadAttention ,会发生这两个 token 是相同的,尽管这些 token 显然代表不同的实体。
所以 Transformer 中使用位置编码来保存单词在序列中的相对或绝对位置。
位置 Embedding 用 PE表示,PE 的维度与单词 Embedding 是一样的。PE 可以通过训练得到,也可以使用某种公式计算得到。在 Transformer 中采用了后者,计算公式如下:

其中,pos 表示单词在句子中的位置,d 表示 PE的维度 (与词 Embedding 一样),2i 表示偶数的维度,2i+1 表示奇数维度 (即 2i≤d, 2i+1≤d)。使用这种公式计算 PE 有以下的好处:
将单词的词 Embedding 和位置 Embedding 相加,就可以得到单词的表示向量 x,x 就是 Transformer 的输入。
import torch
import torch.nn as nn
import math
torch.manual_seed(0)
class PositionalEncoding(nn.Module):
"""
Transformer位置编码实现
"""
def __init__(self, d_model, max_len=5000):
"""
初始化位置编码层
:param d_model: 词嵌入的维度
:param max_len: 序列的最大长度
"""
super(PositionalEncoding, self).__init__()
self.d_model = d_model
# 创建位置编码矩阵
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
# 计算位置编码的正弦和余弦部分
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
# 将位置编码添加到模块中,使其可以被GPU加速
self.register_buffer('pe', pe)
def forward(self, x):
"""
前向传播,将位置编码添加到输入张量中
:param x: 输入张量,形状为 (batch_size, seq_len, d_model)
:return: 添加了位置编码的张量
"""
# 获取序列长度
seq_len = x.size(1)
# 将位置编码添加到输入张量中
x = x + self.pe[:seq_len, :]
return x
# 测试位置编码 - 以"I love you"为例
if __name__ == "__main__":
# 参数设置
batch_size = 1
seq_len = 3 # "I love you"有3个单词
d_model = 64 # 词嵌入维度
# 创建位置编码层
pos_encoder = PositionalEncoding(d_model, max_len=seq_len)
# 创建一个随机输入张量 (模拟词嵌入)
# 在实际应用中,这里应该是经过词嵌入层处理后的张量
x = torch.randn(batch_size, seq_len, d_model)
print("------------添加位置编码之前 (前10个维度):----------")
print(x[0, :, :10])
# 添加位置编码
x_encoded = pos_encoder(x)
print("输入张量形状:", x.shape)
print("添加编码位置后的张量形状:", x_encoded.shape)
# 打印每个单词的位置编码
print("\n每个单词的位置编码:")
for i in range(seq_len):
print(f"位置 {i} 的编码值 (前10个维度):")
print(pos_encoder.pe[i, :10]) # 打印每个位置的前10个维度
# 打印添加位置编码后的结果
print("\n添加位置编码后的结果 (前10个维度):")
print(x_encoded[0, :, :10]) # 打印第一个样本的前10个维度
运行效果:
------------添加位置编码之前 (前10个维度):----------
tensor([[-1.1258, -1.1524, -0.2506, -0.4339, 0.8487, 0.6920, -0.3160, -2.1152,
0.3223, -1.2633],
[-0.5692, 0.9200, 1.1108, 1.2899, -1.4782, 2.5672, -0.4731, 0.3356,
-1.6293, -0.5497],
[-0.8834, -0.4189, -0.8048, 0.5656, 0.6104, 0.4669, 1.9507, -1.0631,
-0.0773, 0.1164]])
输入张量形状: torch.Size([1, 3, 64])
添加编码位置后的张量形状: torch.Size([1, 3, 64])
每个单词的位置编码:
位置 0 的编码值 (前10个维度):
tensor([0., 1., 0., 1., 0., 1., 0., 1., 0., 1.])
位置 1 的编码值 (前10个维度):
tensor([0.8415, 0.5403, 0.6816, 0.7318, 0.5332, 0.8460, 0.4093, 0.9124, 0.3110,
0.9504])
位置 2 的编码值 (前10个维度):
tensor([ 0.9093, -0.4161, 0.9975, 0.0709, 0.9021, 0.4315, 0.7469, 0.6649,
0.5911, 0.8066])
添加位置编码后的结果 (前10个维度):
tensor([[-1.1258, -0.1524, -0.2506, 0.5661, 0.8487, 1.6920, -0.3160, -1.1152,
0.3223, -0.2633],
[ 0.2722, 1.4603, 1.7924, 2.0216, -0.9450, 3.4132, -0.0638, 1.2479,
-1.3183, 0.4007],
[ 0.0259, -0.8351, 0.1927, 0.6366, 1.5125, 0.8983, 2.6976, -0.3982,
0.5138, 0.9230]])