尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Transformer位置编码:三角函数如何让模型理解序列顺序

Transformer位置编码:三角函数如何让模型理解序列顺序 这次我们来看一个 Transformer 模型中的核心设计问题位置编码。Transformer 模型本身没有循环或卷积结构无法感知序列中元素的顺序。位置编码就是给模型装上“导航仪”让它知道每个词在句子中的“坐标”。而三角函数正是这个导航仪最精妙、最不可或缺的“刻度盘”。这篇文章不讲复杂的数学推导而是聚焦于三个关键的数学特性它们共同决定了为什么正弦和余弦函数是位置编码的“唯一解”。理解了这三个特性你就能明白 Transformer 是如何从一个对顺序“视而不见”的“瞎子”变成一个能精准处理长序列的“导航仪”的。我们将从最直观的需求出发逐步拆解这三个特性相对位置感知、绝对位置外推和维度间解耦。本文会结合简单的代码示例和几何直观让你不仅知道“是什么”更能理解“为什么”。无论你是刚入门 Transformer还是想深入理解其设计哲学这篇文章都能给你带来清晰的答案。1. 核心能力速览三角函数位置编码的“三板斧”在深入细节前我们先通过一个表格快速把握三角函数位置编码以经典的 Transformer 论文中的正弦编码为例最核心的几个能力点。这能帮你快速判断它的价值所在。能力项说明与影响核心功能为 Transformer 模型注入序列顺序信息使其能理解“先来后到”。数学形式使用正弦(sin)和余弦(cos)函数的组合频率随维度变化。关键特性1相对位置感知模型能轻松学会计算两个位置之间的相对距离这是处理语言逻辑如主谓一致的基础。关键特性2绝对位置外推编码公式是连续的允许模型处理训练时未见过的更长序列长度尽管效果有衰减。关键特性3维度间解耦与规律性不同维度编码不同频率的信息形成有规律的“波”便于模型学习和泛化。计算与存储确定性计算无需训练原始 Transformer或作为可训练参数的初始化如 BERT。预计算后存储为矩阵推理时查表或计算开销极小。“硬件”门槛纯数学计算对硬件无特殊要求在任何支持 Transformer 的平台上均可实现。“启动”方式在模型嵌入层Embedding Layer之后与词向量直接相加。“接口”能力是模型内部机制不直接对外提供 API但其思想衍生出 RoPE旋转位置编码等可直接用于注意力计算的变体。“批量”任务天然支持批量处理位置编码矩阵可广播broadcast到批次中所有样本。适合场景所有基于原始 Transformer 架构的模型如 BERT、GPT、T5 等的基础位置感知理解更高级位置编码如 RoPE、ALiBi的基石。2. 为什么需要位置编码Transformer 的“先天缺陷”在循环神经网络RNN中模型按顺序处理输入词的顺序信息自然地被隐含在隐藏状态中。然而Transformer 采用了自注意力机制Self-Attention它允许序列中的任意两个位置直接交互。这种设计带来了强大的并行计算能力和长距离依赖捕捉能力但也付出了代价自注意力本身是置换等变的Permutation Equivariant。这是什么意思呢简单来说如果你把输入句子的词序完全打乱自注意力层输出的结果也仅仅是相应地被重新排列而不会因为词序变化而产生本质不同的“理解”。对于一个打乱的句子“吃 我 苹果”和一个正常的句子“我 吃 苹果”原始的 Transformer不加位置编码可能会给出相似的语义表示这显然是错误的。因此我们必须显式地告诉模型每个词的位置。这就是位置编码的使命将离散的、绝对的位置索引如第1个词第2个词映射到一个连续的、高维的向量空间中然后把这个位置向量加到词嵌入向量上。这样输入到 Transformer 的每个词向量就同时包含了“你是谁”语义和“你在哪”位置的信息。接下来的问题是我们该用什么函数来做这个映射为什么偏偏是三角函数3. 数学特性一相对位置感知——让模型学会“距离”这是三角函数位置编码最精妙、也最重要的特性。我们希望模型不仅能知道每个词的绝对位置如第5个词更能轻松地捕捉任意两个词之间的相对位置关系。例如在判断动词和主语是否一致时模型需要知道它们相隔多远。Transformer 原论文中提出的正弦位置编码公式如下对于位置pos从0开始计数和维度ii为偶数或奇数编码向量PE的第i个分量是当i为偶数时PE(pos, i) sin(pos / 10000^(i/d_model))当i为奇数时PE(pos, i) cos(pos / 10000^(i/d_model))其中d_model是模型的隐藏层维度。这个设计的核心在于对于某个固定的偏移量k位置pos k的位置编码可以由位置pos的位置编码通过一个线性变换来表示。让我们来推导一下。考虑同一频率即同一维度对(2i, 2i1)的正弦和余弦分量。设ω_i 1 / 10000^(2i/d_model)。那么PE(pos, 2i) sin(pos * ω_i)PE(pos, 2i1) cos(pos * ω_i)对于位置pos k我们有PE(posk, 2i) sin((posk) * ω_i) sin(pos*ω_i)cos(k*ω_i) cos(pos*ω_i)sin(k*ω_i)PE(posk, 2i1) cos((posk) * ω_i) cos(pos*ω_i)cos(k*ω_i) - sin(pos*ω_i)sin(k*ω_i)这可以写成一个矩阵乘法的形式[PE(posk, 2i) ] [cos(k*ω_i) sin(k*ω_i)] [PE(pos, 2i) ] [PE(posk, 2i1)] [-sin(k*ω_i) cos(k*ω_i)] * [PE(pos, 2i1)]这个变换矩阵M(k, i)是一个旋转矩阵它只依赖于相对距离k和频率ω_i而与绝对位置pos无关。这意味着什么意味着在模型的自注意力计算中当它计算Query在位置pos和Key在位置posk的点积时这个点积会天然地蕴含相对位置k的信息。模型可以很容易地学会根据相对距离来调整注意力权重。例如它可能学会给邻近的词分配更高的注意力或者识别出固定的句法模式如动词通常跟在主语后第k个位置。import numpy as np import matplotlib.pyplot as plt def get_positional_encoding(pos, d_model512): 计算单个位置的位置编码向量 pe np.zeros(d_model) for i in range(0, d_model, 2): div_term np.power(10000, (2 * i) / d_model) pe[i] np.sin(pos / div_term) if i 1 d_model: pe[i1] np.cos(pos / div_term) return pe # 验证相对位置特性 pos 10 k 3 d_model 64 omega_i 1 / np.power(10000, (2 * 10) / d_model) # 取第10个维度对 PE_pos np.array([np.sin(pos * omega_i), np.cos(pos * omega_i)]) PE_pos_k np.array([np.sin((posk) * omega_i), np.cos((posk) * omega_i)]) # 构造旋转矩阵 theta k * omega_i M_k np.array([[np.cos(theta), np.sin(theta)], [-np.sin(theta), np.cos(theta)]]) # 验证 PE(posk) ≈ M(k) * PE(pos) result M_k PE_pos print(fPE(pos{pos}) : {PE_pos}) print(fPE(pos{posk}): {PE_pos_k}) print(fM({k}) * PE(pos): {result}) print(f两者是否接近: {np.allclose(PE_pos_k, result, atol1e-7)})运行上述代码你会看到通过旋转矩阵计算出的PE(posk)与直接按公式计算的结果几乎完全一致。这就是相对位置感知的数学基础。4. 数学特性二绝对位置外推——导航仪的“地图缩放”一个好的导航仪不仅要在熟悉的城市里指路最好还能在陌生的区域提供大致的方向。对于位置编码我们希望模型在训练时只见过较短序列如512个词但在推理时能够处理更长的序列如1024或2048个词。这就是外推Extrapolation能力。三角函数是连续且平滑的周期函数。sin(pos / 10000^(i/d_model))这个函数对于任意实数pos都有定义。这意味着即使我们输入一个远大于训练时见过的pos例如pos1000而训练时最大pos511我们仍然可以计算出一个合法的位置编码向量。但是外推的效果好吗这取决于频率的分布。高频i较小ω_i较大的正弦波变化很快在训练范围外可能进入一个完全未见的相位模型难以适应。低频i较大ω_i较小的正弦波变化缓慢外推时变化相对可预测。因此正弦位置编码具有一定的外推能力但并非完美。这也是后来 RoPE、ALiBi 等位置编码方法致力于改进的方向。我们可以可视化一下不同维度的位置编码看看它们的“波动”情况def get_positional_encoding_matrix(max_len100, d_model64): 生成位置编码矩阵 pe np.zeros((max_len, d_model)) for pos in range(max_len): for i in range(0, d_model, 2): div_term np.power(10000, (2 * i) / d_model) pe[pos, i] np.sin(pos / div_term) if i 1 d_model: pe[pos, i1] np.cos(pos / div_term) return pe max_len 100 d_model 64 pe_matrix get_positional_encoding_matrix(max_len, d_model) # 绘制前几个维度的位置编码随位置的变化 plt.figure(figsize(12, 6)) for i in range(0, 6, 2): # 看前3个维度对 plt.plot(range(max_len), pe_matrix[:, i], labelfdim {i} (sin)) plt.plot(range(max_len), pe_matrix[:, i1], --, labelfdim {i1} (cos), alpha0.7) plt.xlabel(Position (pos)) plt.ylabel(Encoding Value) plt.title(Positional Encoding Values for Different Dimensions (First Few)) plt.legend() plt.grid(True, alpha0.3) plt.show()从图像中可以看到低维度如 dim 0, 1的波形频率很高在100个位置内就振荡了多次。而高维度的波形频率很低几乎呈线性变化。这种多频率组合使得编码既能捕捉细粒度的局部位置信息也能编码大范围的全局位置信息。5. 数学特性三维度间解耦与规律性——有组织的“信息仓库”如果我们简单地将位置索引pos直接通过一个可训练的线性层映射到高维空间即学习一个位置嵌入表会发生什么每个维度学到的值将是独立且无明确规律的。虽然模型最终可能也能学会一些位置信息但学习效率可能较低并且缺乏可解释性。三角函数位置编码则不同它在不同维度上建立了清晰、规律的数学结构维度成对出现每个频率ω_i对应两个维度正弦和余弦它们共同决定了一个“旋转”分量。频率按几何级数递减频率ω_i随着维度索引i的增加以几何级数底数为1/10000减小。这确保了不同维度捕获不同尺度的时间位置信息从快速变化到缓慢变化。值域有界正弦和余弦函数的值域被限制在[-1, 1]之间这有助于训练的稳定性防止位置编码的数值范围与词嵌入的数值范围差异过大。这种规律性使得位置编码向量本身就是一个高度结构化、信息丰富的信号。模型可以更容易地从中提取出有用的模式例如通过线性变换来组合不同频率的分量以表示复杂的相对位置关系。6. 环境准备与代码验证亲手“启动”位置编码理解理论最好的方式就是实践。下面我们搭建一个简单的环境来生成和可视化位置编码并验证其关键特性。环境准备你只需要一个能运行 Python 和 NumPy 的环境即可。推荐使用 Jupyter Notebook 或任何 Python IDE。# 基础环境使用 pip 安装必要库 pip install numpy matplotlib完整的位置编码生成与验证代码import numpy as np import matplotlib.pyplot as plt from matplotlib import cm class SinusoidalPositionalEncoding: 实现 Transformer 原始论文中的正弦位置编码 def __init__(self, d_model, max_len5000): 初始化位置编码矩阵 Args: d_model: 模型隐藏层维度必须是偶数 max_len: 预计算的最大序列长度 self.d_model d_model self.max_len max_len self.pe self._create_positional_encoding() def _create_positional_encoding(self): 计算位置编码矩阵形状为 (max_len, d_model) pe np.zeros((self.max_len, self.d_model)) position np.arange(0, self.max_len).reshape(-1, 1) # (max_len, 1) # 计算除数项公式中的 10000^(2i/d_model) div_term np.exp(np.arange(0, self.d_model, 2) * -(np.log(10000.0) / self.d_model)) # (d_model/2,) # 向量化计算提高效率 pe[:, 0::2] np.sin(position * div_term) # 偶数维度sin pe[:, 1::2] np.cos(position * div_term) # 奇数维度cos return pe def __call__(self, seq_len): 获取指定长度的位置编码 return self.pe[:seq_len] def visualize_heatmap(self, seq_len50): 可视化位置编码矩阵的热力图 encoding self.pe[:seq_len].T # 转置以便观察维度变化 plt.figure(figsize(10, 6)) plt.imshow(encoding, aspectauto, cmapcm.RdBu) plt.xlabel(Position Index) plt.ylabel(Encoding Dimension) plt.colorbar(labelEncoding Value) plt.title(fSinusoidal Positional Encoding Heatmap (Seq Len{seq_len}, d_model{self.d_model})) plt.show() def verify_relative_property(self, pos1, pos2, dim_pair0): 验证相对位置特性PE(pos2) 是否可由 PE(pos1) 通过旋转得到 dim_pair: 要验证的维度对索引0表示第0和1维 i dim_pair * 2 if i 1 self.d_model: raise ValueError(fdim_pair {dim_pair} 超出范围模型维度为 {self.d_model}) pe1 self.pe[pos1, [i, i1]] pe2 self.pe[pos2, [i, i1]] k pos2 - pos1 # 计算旋转角度 theta k * omega_i # 注意div_term 10000^(-2i/d_model) exp(-(2i/d_model)*log(10000)) # 而 omega_i 1 / 10000^(2i/d_model) div_term omega_i np.exp((2 * dim_pair) * -(np.log(10000.0) / self.d_model)) theta k * omega_i # 构造旋转矩阵 M np.array([[np.cos(theta), np.sin(theta)], [-np.sin(theta), np.cos(theta)]]) pe1_transformed M pe1 error np.linalg.norm(pe2 - pe1_transformed) print(f验证位置 {pos1} 和 {pos2} (k{k}) 在维度对 ({i},{i1}) 上的相对位置特性:) print(f PE({pos1}) {pe1}) print(f PE({pos2}) {pe2}) print(f M(k) * PE({pos1}) {pe1_transformed}) print(f 误差 (L2范数): {error:.10f}) print(f 是否近似相等: {error 1e-10}) return error 1e-10 # 实例化并测试 d_model 128 max_len 100 pos_encoder SinusoidalPositionalEncoding(d_model, max_len) # 1. 获取前20个位置前10个维度的编码看看 pe_matrix pos_encoder(20) print(位置编码矩阵形状:, pe_matrix.shape) print(\n前5个位置前8个维度的编码值:) print(pe_matrix[:5, :8]) # 2. 可视化热力图 pos_encoder.visualize_heatmap(seq_len50) # 3. 验证相对位置特性 print(\n *60) pos_encoder.verify_relative_property(pos15, pos210, dim_pair0) # 验证第0个维度对 print(\n -*40) pos_encoder.verify_relative_property(pos15, pos210, dim_pair15) # 验证第15个维度对低频运行这段代码你将得到数值输出看到位置编码矩阵的具体数值感受其结构。热力图直观看到位置编码矩阵的整体模式。你会观察到明显的带状结构这是不同频率的正余弦波叠加的结果。图像下半部分高维度条纹更粗代表低频变化上半部分低维度条纹更细密代表高频变化。验证结果程序会计算并显示对于任意两个位置其编码在同一个频率维度对上确实满足旋转关系误差在数值精度范围内几乎为零。7. 在 Transformer 中的“部署”与“接口”理解了位置编码的生成我们来看看它在 Transformer 模型中是如何“集成”和“调用”的。“部署”方式与词嵌入相加位置编码不参与训练在原始 Transformer 中它是一个固定的、预计算的查找表。在模型的前向传播开始时它被加到词嵌入Word Embedding上输入 词嵌入(词序列) 位置编码(位置序列)这个相加操作是逐元素element-wise的意味着位置信息被直接注入到每个词的表示中。代码示例模拟前向传播import torch import torch.nn as nn class TransformerInputLayer(nn.Module): 模拟 Transformer 的输入层词嵌入 位置编码 def __init__(self, vocab_size, d_model, max_len512): super().__init__() self.token_embedding nn.Embedding(vocab_size, d_model) # 注册一个不参与梯度更新的缓冲区存储位置编码 pe torch.zeros(max_len, d_model) position torch.arange(0, max_len).unsqueeze(1) div_term torch.exp(torch.arange(0, d_model, 2) * -(torch.log(torch.tensor(10000.0)) / d_model)) pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) pe pe.unsqueeze(0) # 增加批次维度 (1, max_len, d_model) self.register_buffer(positional_encoding, pe) def forward(self, token_ids): token_ids: 形状为 (batch_size, seq_len) 的词ID张量 返回形状为 (batch_size, seq_len, d_model) 的嵌入张量 batch_size, seq_len token_ids.shape # 获取词嵌入 token_embeds self.token_embedding(token_ids) # (batch, seq, d_model) # 获取位置编码自动广播到批次大小 position_embeds self.positional_encoding[:, :seq_len, :] # 相加 return token_embeds position_embeds # 模拟一个批次的数据 vocab_size 10000 d_model 512 batch_size 4 seq_len 32 model_input_layer TransformerInputLayer(vocab_size, d_model) # 随机生成一批词ID dummy_token_ids torch.randint(0, vocab_size, (batch_size, seq_len)) # 前向传播 combined_embeddings model_input_layer(dummy_token_ids) print(f词嵌入位置编码后的张量形状: {combined_embeddings.shape}) print(f位置编码张量形状: {model_input_layer.positional_encoding.shape})“接口”思想从加法到注意力计算的内积位置编码更深层的“接口”思想体现在自注意力机制中。当我们计算 Query 和 Key 的点积时由于位置编码的加入点积结果可以分解为(词嵌入_A 位置编码_A) · (词嵌入_B 位置编码_B) 词嵌入_A·词嵌入_B 词嵌入_A·位置编码_B 位置编码_A·词嵌入_B 位置编码_A·位置编码_B其中位置编码_A·位置编码_B这一项由于三角函数的性质主要包含了位置A和B之间的相对距离信息。这使得模型在注意力层面就能直接利用相对位置。更高级的“接口”设计如RoPERotary Position Embedding旋转位置编码将这一思想发挥到极致。RoPE 不再将位置编码加到词嵌入上而是将位置信息以旋转矩阵的形式直接作用于 Query 和 Key 向量本身使得它们的点积只依赖于相对位置。这成为了当前众多大模型如 LLaMA、GPT Neo的标准配置。# RoPE 核心思想示意简化版 def apply_rope(q, k, pos): q, k: 对应位置的 query 和 key 向量复数形式或二维向量表示 pos: 位置索引 返回旋转后的 q 和 k # 假设每个维度对对应一个旋转角度 theta theta pos * frequency_base # 旋转操作二维情况 rotation_matrix [[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]] q_rotated rotation_matrix q k_rotated rotation_matrix k return q_rotated, k_rotated # 这样q_rotated_i · k_rotated_j 的结果将只依赖于 (i-j)即相对位置。8. 常见问题与排查思路在实际研究或实现 Transformer 时关于位置编码可能会遇到以下问题问题现象可能原因排查方式解决方案模型无法学习长距离依赖位置编码的维度不足或频率分布不合理高频部分过多导致外推失败。检查位置编码的热力图观察高频维度是否在训练长度内已振荡过多周期。调整频率基数如将10000改为更大的数降低高频维度频率或改用 RoPE、ALiBi 等外推性更好的编码。训练不稳定损失震荡位置编码的值域-1,1与词嵌入的值域差异过大或初始化不当。检查词嵌入层的初始化标准差对比位置编码的幅度。对词嵌入进行适当的缩放如乘以 sqrt(d_model)或使用 LayerNorm 来稳定输入分布。推理时序列长度超过训练长度使用正弦编码外推模型性能显著下降。对比模型在训练长度内和外的任务表现如困惑度。1. 在训练时使用更长的序列。2. 采用支持更长上下文的位置编码如 NTK-aware Scaled RoPE、YaRN 等。3. 在推理时进行位置插值Position Interpolation。自己实现的位置编码效果差公式实现错误如维度索引i计算错误或 sin/cos 应用错维度。使用上文提供的验证代码检查相对位置特性是否成立。可视化编码矩阵看是否呈现规律的带状结构。严格对照原始论文公式使用向量化实现并编写单元测试进行验证。可训练的位置嵌入与正弦编码孰优孰劣可训练嵌入更灵活但可能过拟合正弦编码有强归纳偏置但外推有限。在小数据集和大道具上分别实验两种方法。通常正弦编码作为起点更可靠。许多现代模型如 BERT使用可训练的位置嵌入但会以正弦编码进行初始化结合了两者优点。如何理解“相对位置”被编码在注意力分数中对理论推导不清晰。手动计算两个不同位置编码的点积观察其是否主要依赖于位置差。推导PE(pos)^T * PE(posk)的表达式。回顾本文第3节的数学推导理解旋转矩阵的性质。点积PE(pos)^T * PE(posk)的结果是cos(k*ω_i)它只与相对距离k有关。9. 最佳实践与使用建议基于三角函数的特性与工程经验在使用位置编码时建议遵循以下实践优先使用标准实现在大多数深度学习框架如 PyTorch、TensorFlow和主流 Transformer 库如 Hugging Face Transformers中位置编码已有成熟、优化的实现。除非有特殊研究目的否则不建议自己从头实现。理解外推局限性对于需要处理远超训练长度文本的任务如长文档摘要、代码生成原始的三角函数位置编码可能不是最佳选择。应优先考虑 RoPE 及其变种如 LLaMA 所用或专门为长上下文设计的编码如 ALiBi。可视化是利器在调试或研究时务必像本文所做的那样将位置编码矩阵绘制成热力图。异常的图案如无规律噪声、条纹断裂往往意味着实现错误。与 LayerNorm 配合使用Transformer 块中的 LayerNorm 层能有效平滑词嵌入和位置编码相加后可能存在的分布偏移是稳定训练的关键。对于可训练位置嵌入如果决定使用可训练的位置嵌入一个(max_len, d_model)的矩阵考虑用正弦编码矩阵来初始化它。这相当于给模型一个关于位置规律的强先验可以加速收敛并可能提升泛化能力。注意版本差异不同 Transformer 变体可能对位置编码有微小修改。例如原始的 Transformer 在编码器和解码器中都使用相同的位置编码。而有些模型只在解码器中使用或在嵌入层之后额外添加。查阅你所使用模型的官方文档或代码至关重要。10. 总结从“瞎子”到“导航仪”的关键一跃回到最初的问题为什么位置编码必须用三角函数通过三个数学特性的剖析我们可以给出清晰的回答相对位置感知旋转不变性三角函数使得任意两个位置编码之间的关系可以表示为一个只与它们相对距离有关的线性变换旋转矩阵。这为模型理解词序逻辑提供了最直接、最优雅的数学基础。绝对位置外推函数连续性作为连续函数三角函数允许模型对训练时未见过的位置进行“猜测”尽管这种外推能力有限但为处理可变长度序列提供了可能性。维度间解耦与规律性结构化表示通过在不同维度上设置几何级数衰减的频率三角函数创造了一个从高频到低频、信息丰富且规律性强的多维表示空间极大地方便了模型的学习与泛化。正是这三个特性的结合让一组简单的sin和cos函数成为了点亮 Transformer “视觉”使其从对序列顺序“视而不见”的“瞎子”蜕变为能在语言序列中精准“导航”的智能体的关键。当你下次阅读 Transformer 相关论文或代码时再看到那些sin和cos希望你能会心一笑明白这不仅仅是两个普通的函数而是整个模型能够理解“顺序”这一核心概念的数学基石。理解了这个基石你也就掌握了通往更高级位置编码技术如 RoPE、ALiBi的钥匙。
返回列表