自学内容网 自学内容网

手撕正弦-余弦位置编码(Sinusoidal Positional Encoding)

请添加图片描述

请添加图片描述

改写后的代码:

import torch
import math
import torch.nn as nn

class PositionalEncoder(nn.Module):
    def __init__(self, d_model, max_seq_len=80):
        super().__init__()
        self.d_model = d_model
        
        # 根据 pos 和 i 创建一个常量 PE 矩阵
        pe = torch.zeros(max_seq_len, d_model)
        for pos in range(max_seq_len):
            for i in range(0, d_model, 2):
                pe[pos, i] = math.sin(pos / (10000 ** ((2 * i) / d_model)))
                pe[pos, i + 1] = math.cos(pos / (10000 ** ((2 * (i + 1)) / d_model)))

        # 增加 batch 维度
        pe = pe.unsqueeze(0)
        
        # 将 pe 注册为 buffer,不作为模型的参数,但会在模型中使用
        self.register_buffer('pe', pe)

    def forward(self, x):
        # 使得词嵌入表示相对大一些
        x = x * math.sqrt(self.d_model)
        
        # 获取输入序列的长度
        seq_len = x.size(1)
        
        # 增加位置编码到词嵌入表示中,不需要梯度
        x = x + self.pe[:, :seq_len].cuda()
        
        return x

代码解读:

  1. 初始化位置编码矩阵

    • 使用 torch.zeros(max_seq_len, d_model) 初始化了一个大小为 (max_seq_len, d_model) 的零矩阵,之后通过两个循环来填充位置编码的值:
      • 偶数索引维度上用正弦函数生成值。
      • 奇数索引维度上用余弦函数生成值。
  2. unsqueeze(0)

    • pe 增加了一个 batch 维度,以便在输入的 batch 中应用相同的编码矩阵。
  3. register_buffer

    • self.register_buffer('pe', pe):将 pe 注册为模型的 buffer,它不会在训练过程中更新(即不参与梯度计算),但会作为模型的一部分保存在模型的状态字典中。
  4. 前向传播函数 forward

    • 首先通过 x * math.sqrt(self.d_model) 放大输入的词嵌入,这与 Transformer 中的设计一致,用于在数值上平衡词嵌入和位置编码的量级。
    • 然后将生成的 pe(位置编码)添加到词嵌入 x 上,self.pe[:, :seq_len].cuda() 取出与输入序列长度相同的部分,并传到 GPU 上(如果在 GPU 上训练)。

    注意:这里移除了 Variable,因为现代 PyTorch 不再需要用 Variable 包装张量,直接使用张量即可。由于 pe 不需要计算梯度,在 register_buffer 注册时就默认不参与梯度计算。

如何使用:

你可以将这个类实例化并在模型中使用。例如:

pos_encoder = PositionalEncoder(d_model=512, max_seq_len=100)
x = torch.randn(32, 50, 512)  # 一个 batch 的输入,大小为 (batch_size, seq_len, d_model)
output = pos_encoder(x)  # 加入位置编码

这样就可以为输入序列的每个 token 添加位置编码,并准备好用于 Transformer 模型的输入了。如果你有其他问题,欢迎继续提问!

位置编码的作用:

通过使用正弦和余弦函数,Transformer 的位置编码可以在不同维度上引入不同的周期变化,这样模型能够感知序列中每个 token 的位置以及相对距离。这种编码具有两个优点:

  1. 周期性:正弦和余弦的周期性可以很好地表示序列中不同位置之间的关系。
  2. 连续性:相邻位置的编码向量非常接近,能够捕捉到相邻 token 之间的顺序信息。

可视化

import numpy as np
import matplotlib.pyplot as plt

# 设置参数
pos = 10  # 位置
d = 512   # 隐藏层维度
i_vals = np.arange(0, d // 2)  # 取一半的维度进行计算

# 计算PE(pos, 2i) 和 PE(pos, 2i+1)
pe_sin = np.sin(pos / (10000 ** (2 * i_vals / d)))
pe_cos = np.cos(pos / (10000 ** (2 * i_vals / d)))

# 画图
plt.figure(figsize=(10, 6))
plt.plot(2 * i_vals, pe_sin, label='sin(pos / 10000^(2i/d))', color='blue')
plt.plot(2 * i_vals + 1, pe_cos, label='cos(pos / 10000^(2i/d))', color='orange')

plt.title(f"Positional Encoding for pos = {pos} and d = {d}")
plt.xlabel('Dimension Index')
plt.ylabel('Value')
plt.legend()
plt.grid(True)
plt.show()

在这里插入图片描述

from mpl_toolkits.mplot3d import Axes3D

# 设置参数
positions = [0, 5, 10, 15, 20]  # 多个位置
d = 512   # 隐藏层维度
i_vals = np.arange(0, d // 2)  # 取一半的维度进行计算

# 创建三维数组保存不同位置的编码
pe_values = np.zeros((len(positions), d))

for idx, pos in enumerate(positions):
    pe_sin = np.sin(pos / (10000 ** (2 * i_vals / d)))
    pe_cos = np.cos(pos / (10000 ** (2 * i_vals / d)))
    pe_values[idx, 2 * i_vals] = pe_sin
    pe_values[idx, 2 * i_vals + 1] = pe_cos

# 画三维图
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')

# 为每个位置画出曲线
for idx, pos in enumerate(positions):
    ax.plot(np.arange(d), [pos]*d, pe_values[idx], label=f'pos={pos}')

# 设置标签
ax.set_xlabel('Dimension Index')
ax.set_ylabel('Position')
ax.set_zlabel('PE Value')
ax.set_title('Positional Encoding for Different Positions')
ax.legend()

plt.show()

在这里插入图片描述

这张三维图展示了不同位置(pos = 0, 5, 10, 15, 20)下的位置编码值(Positional Encoding)随维度变化的情况。每条曲线代表一个位置对应的编码向量,横轴是维度索引,纵轴是编码值,颜色区分不同的位置。

从图中可以看到,不同的位置编码在不同维度上变化的模式不同,但都有一定的周期性。随着位置的增加,编码值的形状会有所变化,这种编码允许 Transformer 模型捕捉序列中 token 的相对位置和顺序。


原文地址:https://blog.csdn.net/weixin_46460463/article/details/142729471

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!