CODE04:实现 Sinusoidal 编码(DONE)#
Author by: lwh & ZOMI
1. Sinusoidal 绝对位置编码原理#
在 Transformer 模型中,由于缺乏循环结构或卷积结构,模型本身不具备处理序列中位置信息的能力。为了解决这一问题,论文《Attention is All You Need》中引入了 Sinusoidal(正弦函数)位置编码,它将序列中每个位置编码成一个具有特定模式的向量,并加到输入的词向量中,从而注入位置信息。
Sinusoidal 绝对位置编码具有以下几个特点:
编码方式不依赖训练(即是固定的,不需要学习的);
每个位置使用一组不同频率的正余弦函数;
不同位置之间的编码具有良好的相对位置可区分性;
编码具有一定的平移不变性,便于模型学习相对位置信息。
给定一个位置 pos
和一个维度索引 i
,位置编码向量中第 i
个维度的值由如下公式给出:
其中:
\(pos\):表示序列中当前的位置;
\(i\):表示当前维度索引(从 0 开始);
\(d_{\text{model}}\):表示位置编码的维度大小;
\(PE_{(pos, k)}\):表示位置 \(pos\) 在第 \(k\) 维的编码值。
2. Sinusoidal 绝对位置编码实现#
下面我们来实现一种非常经典的位置编码方式:正余弦位置编码(Sinusoidal Positional Encoding),它被广泛应用于 Transformer 模型中,为没有时序感知能力的结构显式引入位置信息。
首先,我们导入 torch 和 math 库,用于张量操作和数学计算:
import torch
import math
然后我们定义一个函数 sinusoidal_pos_encoding()
,这个函数接收两个参数:
max_len:表示序列的最大长度(也就是你输入句子的最大长度)
d_model:编码维度(也就是每个位置要映射成多长的向量)
sinusoidal_pos_encoding()
输出是一个形状为 (max_len, d_model) 的张量,表示从位置 0 到位置 max_len - 1 的所有位置的编码值。
运行过程中,首先函数内部先检查 d_model 是否为偶数,然后构造位置索引和维度索引,计算每个位置在不同频率下的角度值,分别用正弦函数填入偶数维度、余弦函数填入奇数维度。最终返回一个形状为 (max_len, d_model) 的张量,表示每个位置的唯一编码,使模型在没有循环结构的情况下能够捕捉到位置信息。
下面是具体实现代码:
def sinusoidal_pos_encoding(seq_len: int, d_model: int) -> torch.Tensor:
if d_model % 2 != 0:
raise ValueError("d_model 必须为偶数")
# 生成位置向量和维度向量
pos = torch.arange(0, seq_len).unsqueeze(1).float() # shape: (seq_len, 1)
i = torch.arange(0, d_model // 2).float() # shape: (d_model/2,)
# 计算频率除数项
denom = torch.pow(10000, 2 * i / d_model) # shape: (d_model/2,)
angle = pos / denom # shape: (seq_len, d_model/2)
# 初始化编码矩阵
pe = torch.zeros(seq_len, d_model)
# 填入 sin 和 cos
pe[:, 0::2] = torch.sin(angle) # 偶数维度
pe[:, 1::2] = torch.cos(angle) # 奇数维度
return pe
在代码的主程序部分,调用 sinusoidal_pos_encoding(seq_len=3, d_model=4)
来生成一个长度为 3,每个位置编码为 4 维的正弦位置编码矩阵,并将其保存在变量 pe 中。
3. Sinusoidal 结果输出#
接着通过 print()
语句输出编码结果,方便观察每个位置对应的编码值。打印信息前后分别加上“开始输出”和“结束”标记,用于清晰地划分输出区域,下面是代码部分:
pe = sinusoidal_pos_encoding(seq_len=3, d_model=4)
print("=== 开始位置编码输出 ===")
print(pe)
print("=== 结束 ===")
=== 开始位置编码输出 ===
tensor([[ 0.0000, 1.0000, 0.0000, 1.0000],
[ 0.8415, 0.5403, 0.0100, 0.9999],
[ 0.9093, -0.4161, 0.0200, 0.9998]])
=== 结束 ===
运行该脚本后,程序首先输出提示信息,表明 sinusoidal_test.py
已开始执行。
随后,函数 sinusoidal_pos_encoding(seq_len=3, d_model=4)
返回了一个形状为 (3, 4)
的张量,其中每一行表示一个位置的编码,每一列对应一个特定频率下的正弦或余弦值。
4 总结与思考#
本文围绕 Transformer 的 Sinusoidal 位置编码,其设计巧思在于相对位置可通过绝对编码推导、频率分层适配不同位置区分需求,虽无需训练且泛化性强,但灵活性不足、长序列高频维度易衰减,更适合中小数据集与适中序列长度任务,而在长序列或个性化需求场景下,需与可学习位置编码等方案权衡选择。