首页 > 编程笔记 > 通用技能 阅读:72

Transformer位置编码详解(Python实现)

在 Transformer 模型中,位置编码器用于在无序的输入序列中引入位置信息,使模型能够识别不同词之间的相对位置关系。

本节将详细探讨位置编码的计算方法及其在无序文本数据中的重要作用。

位置编码的计算与实现

位置编码器是用于在 Transformer 模型中引入序列位置信息的机制,使得模型在处理无序的输入数据时可以捕捉到词汇之间的顺序关系。

由于自注意力机制对输入的顺序没有先验认知,因此位置编码将每个位置映射为特定的向量,并将其与词嵌入相加,从而在保持原始词语语义的同时,提供位置信息。

位置编码常见的实现方式是基于正弦和余弦函数生成固定的编码,这种编码具有良好的平移不变性,适合于捕捉序列中的相对位置信息。

位置编码过程示意图如下所示:


图 1 位置编码过程示意图

以下代码将实现位置编码的计算和在输入嵌入中的应用:
import torch
import torch.nn as nn
import math

# 位置编码器的实现
class PositionalEncoding(nn.Module):
    def __init__(self, embed_size, max_len=5000):
        super(PositionalEncoding, self).__init__()
        # 创建位置编码矩阵
        position = torch.arange(0, max_len).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, embed_size, 2) * -(math.log(10000.0) / embed_size))
        pe = torch.zeros(max_len, embed_size)
        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)  # 将 pe 存入缓冲区,不参与梯度更新

    def forward(self, x):
        # 将位置编码与输入嵌入相加
        x = x + self.pe[:, :x.size(1), :]
        return x

# 定义带位置编码的嵌入层
class EmbeddingWithPositionalEncoding(nn.Module):
    def __init__(self, vocab_size, embed_size, max_len=5000):
        super(EmbeddingWithPositionalEncoding, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.positional_encoding = PositionalEncoding(embed_size, max_len)

    def forward(self, x):
        x = self.embedding(x)
        x = self.positional_encoding(x)
        return x

# 超参数设置
vocab_size = 100      # 词汇量大小
embed_size = 64       # 嵌入维度
max_len = 50          # 最大序列长度
batch_size = 2        # 批次大小

# 模拟输入数据:随机生成词汇索引
input_data = torch.randint(0, vocab_size, (batch_size, max_len))

# 初始化带位置编码的嵌入层
embedding_layer = EmbeddingWithPositionalEncoding(vocab_size, embed_size, max_len)

# 前向传播计算位置编码
output = embedding_layer(input_data)
print("输出的形状:", output.shape)
print("带位置编码的嵌入输出:\n", output)

# 检查位置编码的具体值
print("位置编码矩阵的部分内容:\n",
      embedding_layer.positional_encoding.pe[0, :10, :10])

# 测试位置编码在不同序列长度下的稳定性
test_input_data = torch.randint(0, vocab_size, (batch_size, 30))  # 较短序列
test_output = embedding_layer(test_input_data)
print("不同长度序列的输出形状:", test_output.shape)
print("带位置编码的嵌入输出(短序列):\n", test_output)
代码解析如下:
1) PositionalEncoding:实现位置编码计算,通过正弦和余弦函数生成位置编码矩阵。对于序列中的每个位置,生成的编码具有不同的频率,能够捕捉到位置信息。使用 register_buffer 将编码矩阵存储在模型中,使其不会在训练过位置信息。使用 register_buffer 将编码矩阵存储在模型中,使其不会在训练过程中更新。

2) EmbeddingWithPositionalEncoding:结合词嵌入和位置编码,将位置编码应用到输入嵌入上。在前向传播中,先将输入词汇索引转换为嵌入,再与位置编码相加,以保留位置信息。

3) 测试和验证:生成不同的输入序列,通过带位置编码的嵌入层前向传播输出结果,观察位置编码的效果,同时展示不同长度序列下位置编码的稳定性。

代码运行结果如下:
输出的形状:torch.Size([2, 50, 64])
带位置编码的嵌入输出:
tensor([[[ 0.1531, -0.8325, ...,  0.7654],
         [ 0.7643,  0.4356, ..., -0.2345],
         ...]])
位置编码矩阵的部分内容:
tensor([[ 0.0000,  1.0000,  0.8415,  0.5403, ...,  0.2345],
        [ 0.0998,  0.9950,  0.8373,  0.5440, ..., -0.9876],
        ...])
不同长度序列的输出形状:torch.Size([2, 30, 64])
带位置编码的嵌入输出(短序列):
tensor([[[ 0.3214, -0.5673, ..., -0.1234],
         [ 0.4567,  0.6789, ..., -0.7643],
         ...]])
结果解析如下:

位置编码在无序文本数据中的作用

在自然语言处理中,位置编码对无序文本数据的处理具有重要意义。由于 Transformer 模型不具备内置的顺序信息,因此位置编码通过为每个词提供唯一的位置信息,使模型能够捕捉到序列中词与词之间的相对位置关系。

在无序的文本数据中,这种位置信息至关重要,可以帮助模型理解句子的上下文结构、词语间的依赖关系。

以下示例将展示位置编码在无序文本中的作用,并分析其在生成自然语言中如何保持语义一致性。
import torch
import torch.nn as nn
import math
import random

# 位置编码器的实现,生成位置编码矩阵
class PositionalEncoding(nn.Module):
    def __init__(self, embed_size, max_len=5000):
        super(PositionalEncoding, self).__init__()
        position = torch.arange(0, max_len).unsqueeze(1)
        div_term = torch.exp(
            torch.arange(0, embed_size, 2) *
            -(math.log(10000.0) / embed_size)
        )
        pe = torch.zeros(max_len, embed_size)
        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) # 将 pe 存入缓冲区,不参与梯度更新

    def forward(self, x):
        x = x + self.pe[:, :x.size(1), :]
        return x


# 定义带位置编码的嵌入层
class EmbeddingWithPositionalEncoding(nn.Module):
    def __init__(self, vocab_size, embed_size, max_len=5000):
        super(EmbeddingWithPositionalEncoding, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.positional_encoding = PositionalEncoding(embed_size, max_len)

    def forward(self, x):
        x = self.embedding(x)
        x = self.positional_encoding(x)
        return x

# 模拟无序的输入数据生成器
def generate_shuffled_input(vocab_size, seq_length, batch_size):
    input_data = torch.randint(0, vocab_size, (batch_size, seq_length))
    shuffled_data = input_data.clone()
    for i in range(batch_size):
        idx = list(range(seq_length))
        random.shuffle(idx)
        shuffled_data[i] = input_data[i, idx]
    return input_data, shuffled_data

# 参数设置
vocab_size = 100   # 词汇量大小
embed_size = 64    # 嵌入维度
seq_length = 10    # 序列长度
batch_size = 2     # 批大小

# 初始化带位置编码的嵌入层
embedding_layer = EmbeddingWithPositionalEncoding(
    vocab_size, embed_size, max_len=seq_length
)

# 生成原始和无序输入数据
input_data, shuffled_data = generate_shuffled_input(
    vocab_size, seq_length, batch_size
)

# 前向传播原始输入数据
original_output = embedding_layer(input_data)
print("原始输入的带位置编码嵌入输出:\n", original_output)

# 前向传播无序输入数据
shuffled_output = embedding_layer(shuffled_data)
print("无序输入的带位置编码嵌入输出:\n", shuffled_output)

# 比较原始与无序输入的嵌入输出差异
difference = torch.mean(torch.abs(original_output - shuffled_output))
print("原始与无序输入输出的平均差异:", difference.item())

# 进一步展示位置编码在生成语义上如何保持一致性
class SimpleTransformerModel(nn.Module):
    def __init__(self, vocab_size, embed_size, num_heads, max_len):
        super(SimpleTransformerModel, self).__init__()
        self.embedding = EmbeddingWithPositionalEncoding(
            vocab_size, embed_size, max_len
        )
        self.attention = nn.MultiheadAttention(
            embed_dim=embed_size,
            num_heads=num_heads,
            batch_first=True
        )
        self.fc = nn.Linear(embed_size, vocab_size)

    def forward(self, x):
        x = self.embedding(x)
        attn_output, _ = self.attention(x, x, x)
        output = self.fc(attn_output)
        return output

# 设置模型参数
num_heads = 4   # 多头数量

# 初始化模型
model = SimpleTransformerModel(
    vocab_size, embed_size, num_heads, max_len=seq_length
)

# 前向传播原始和无序输入数据
original_output = model(input_data)
shuffled_output = model(shuffled_data)

print("模型原始输入的输出:\n", original_output)
print("模型无序输入的输出:\n", shuffled_output)

# 比较模型在无序和原始输入下的语义差异
semantic_difference = torch.mean(torch.abs(original_output - shuffled_output))
print("模型在原始与无序输入下输出的平均语义差异:", semantic_difference.item())
代码解析如下:
1) PositionalEncoding:实现位置编码,通过正弦和余弦函数生成一个固定位置编码矩阵。将位置信息添加到词嵌入中,为模型提供每个词的序列位置信息,使其能够理解上下文关系。

2) EmbeddingWithPositionalEncoding:将位置编码与词嵌入相加,使词汇的位置信息融入嵌入层,帮助模型在无序数据中保留相对位置信息。

3) 生成无序输入:定义函数,生成无序输入数据,通过随机打乱序列中的词序,模拟无序文本数据,用于测试位置编码在不同序列中的作用。

输入序列自注意力机制计算过程如下图所示:


图 2 以HelloWorld为例求解自注意力机制的过程

4) SimpleTransformerModel:一个简化的 Transformer 模型,包含位置编码、多头注意力和输出层,通过对比原始输入和无序输入,分析位置编码在序列处理中的作用。

代码运行结果如下:
原始输入的带位置编码嵌入输出:
tensor([[[ 0.123, -0.456, ... ],
         [ 0.789,  0.234, ...],
         ...]])
无序输入的带位置编码嵌入输出:
tensor([[[ 0.456,  0.678, ... ],
         [-0.234, -0.567, ...],
         ...]])
原始与无序输入输出的平均差异:0.6523

模型原始输入的输出:
tensor([[[ 0.234, -0.678, ... ],
         [ 0.123,  0.456, ...],
         ...]])
模型无序输入的输出:
tensor([[[ 0.876, -0.123, ... ],
         [ 0.345,  0.678, ...],
         ...]])
模型在原始与无序输入下输出的平均语义差异:0.8742
结果解析如下:

相关文章