← 返回首页
位置编码
发表时间:2025-04-17 14:28:01
位置编码

Transformer模型因为只有attention计算,没有类似RNN的时序信息,而文本中,序列信息是十分重要的信息,因此需要额外的位置编码信息。位置编码可以分为绝对位置编码和相对位置编码。

1.为什么需要位置编码

Transformer 的架构中没有内置处理序列顺序的机制,需要通过位置编码显式地为模型提供序列中单词的位置信息,以更好地学习序列关系。位置编码通常通过数学函数生成,目的是为每个位置生成一个独特的向量。这些向量在嵌入空间中具有特定的性质,比如周期性和连续性。Transformer的自注意力是一种集合运算,这意味着它是置换等变的。如果我们不利用位置信息来丰富自注意力,就无法确定许多重要的关系。

举例说明最能说明这一点。考虑一下这个句子,其中同一个单词出现在不同的位置:

「这只狗追赶另一只狗」

直观地看,「狗」指的是两个不同的实体。如果我们首先对它们进行 token 化,映射到 Llama 3.2 1B 的真实 token 嵌入,并将它们传递给 torch.nn.MultiheadAttention ,会发生这两个 token 是相同的,尽管这些 token 显然代表不同的实体。

所以 Transformer 中使用位置编码来保存单词在序列中的相对或绝对位置。

2.位置编码的计算

位置 Embedding 用 PE表示,PE 的维度与单词 Embedding 是一样的。PE 可以通过训练得到,也可以使用某种公式计算得到。在 Transformer 中采用了后者,计算公式如下:

其中,pos 表示单词在句子中的位置,d 表示 PE的维度 (与词 Embedding 一样),2i 表示偶数的维度,2i+1 表示奇数维度 (即 2i≤d, 2i+1≤d)。使用这种公式计算 PE 有以下的好处:

将单词的词 Embedding 和位置 Embedding 相加,就可以得到单词的表示向量 x,x 就是 Transformer 的输入。

3.Pytorch实现计算位置编码案例

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]])