这次我们来看一个 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开始计数)和维度i(i为偶数或奇数),编码向量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, 2i+1))的正弦和余弦分量。设ω_i = 1 / 10000^(2i/d_model)。
那么:PE(pos, 2i) = sin(pos * ω_i)PE(pos, 2i+1) = cos(pos * ω_i)
对于位置pos + k,我们有:PE(pos+k, 2i) = sin((pos+k) * ω_i) = sin(pos*ω_i)cos(k*ω_i) + cos(pos*ω_i)sin(k*ω_i)PE(pos+k, 2i+1) = cos((pos+k) * ω_i) = cos(pos*ω_i)cos(k*ω_i) - sin(pos*ω_i)sin(k*ω_i)
这可以写成一个矩阵乘法的形式:
[PE(pos+k, 2i) ] [cos(k*ω_i) sin(k*ω_i)] [PE(pos, 2i) ] [PE(pos+k, 2i+1)] = [-sin(k*ω_i) cos(k*ω_i)] * [PE(pos, 2i+1)]这个变换矩阵M(k, i)是一个旋转矩阵!它只依赖于相对距离k和频率ω_i,而与绝对位置pos无关。
这意味着什么?意味着在模型的自注意力计算中,当它计算Query(在位置pos)和Key(在位置pos+k)的点积时,这个点积会天然地蕴含相对位置k的信息。模型可以很容易地学会根据相对距离来调整注意力权重。例如,它可能学会给邻近的词分配更高的注意力,或者识别出固定的句法模式(如动词通常跟在主语后第k个位置)。
import numpy as np import matplotlib.pyplot as plt def get_positional_encoding(pos, d_model=512): """计算单个位置的位置编码向量""" 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[i+1] = 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((pos+k) * omega_i), np.cos((pos+k) * omega_i)]) # 构造旋转矩阵 theta = k * omega_i M_k = np.array([[np.cos(theta), np.sin(theta)], [-np.sin(theta), np.cos(theta)]]) # 验证 PE(pos+k) ≈ M(k) * PE(pos) result = M_k @ PE_pos print(f"PE(pos={pos}) : {PE_pos}") print(f"PE(pos={pos+k}): {PE_pos_k}") print(f"M({k}) * PE(pos): {result}") print(f"两者是否接近: {np.allclose(PE_pos_k, result, atol=1e-7)}")运行上述代码,你会看到通过旋转矩阵计算出的PE(pos+k)与直接按公式计算的结果几乎完全一致。这就是相对位置感知的数学基础。
4. 数学特性二:绝对位置外推——导航仪的“地图缩放”
一个好的导航仪,不仅要在熟悉的城市里指路,最好还能在陌生的区域提供大致的方向。对于位置编码,我们希望模型在训练时只见过较短序列(如512个词),但在推理时能够处理更长的序列(如1024或2048个词)。这就是外推(Extrapolation)能力。
三角函数是连续且平滑的周期函数。sin(pos / 10000^(i/d_model))这个函数对于任意实数pos都有定义。这意味着,即使我们输入一个远大于训练时见过的pos(例如pos=1000,而训练时最大pos=511),我们仍然可以计算出一个合法的位置编码向量。
但是,外推的效果好吗?这取决于频率的分布。高频(i较小,ω_i较大)的正弦波变化很快,在训练范围外可能进入一个完全未见的相位,模型难以适应。低频(i较大,ω_i较小)的正弦波变化缓慢,外推时变化相对可预测。因此,正弦位置编码具有一定的外推能力,但并非完美。这也是后来 RoPE、ALiBi 等位置编码方法致力于改进的方向。
我们可以可视化一下不同维度的位置编码,看看它们的“波动”情况:
def get_positional_encoding_matrix(max_len=100, d_model=64): """生成位置编码矩阵""" 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, i+1] = 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], label=f'dim {i} (sin)') plt.plot(range(max_len), pe_matrix[:, i+1], '--', label=f'dim {i+1} (cos)', alpha=0.7) plt.xlabel('Position (pos)') plt.ylabel('Encoding Value') plt.title('Positional Encoding Values for Different Dimensions (First Few)') plt.legend() plt.grid(True, alpha=0.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_len=5000): """ 初始化位置编码矩阵 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_len=50): """可视化位置编码矩阵的热力图""" encoding = self.pe[:seq_len].T # 转置以便观察维度变化 plt.figure(figsize=(10, 6)) plt.imshow(encoding, aspect='auto', cmap=cm.RdBu) plt.xlabel('Position Index') plt.ylabel('Encoding Dimension') plt.colorbar(label='Encoding Value') plt.title(f'Sinusoidal Positional Encoding Heatmap (Seq Len={seq_len}, d_model={self.d_model})') plt.show() def verify_relative_property(self, pos1, pos2, dim_pair=0): """ 验证相对位置特性:PE(pos2) 是否可由 PE(pos1) 通过旋转得到 dim_pair: 要验证的维度对索引(0表示第0和1维) """ i = dim_pair * 2 if i + 1 >= self.d_model: raise ValueError(f"dim_pair {dim_pair} 超出范围,模型维度为 {self.d_model}") pe1 = self.pe[pos1, [i, i+1]] pe2 = self.pe[pos2, [i, i+1]] 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},{i+1}) 上的相对位置特性:") 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_len=50) # 3. 验证相对位置特性 print("\n" + "="*60) pos_encoder.verify_relative_property(pos1=5, pos2=10, dim_pair=0) # 验证第0个维度对 print("\n" + "-"*40) pos_encoder.verify_relative_property(pos1=5, pos2=10, dim_pair=15) # 验证第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_len=512): 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之间的相对距离信息。这使得模型在注意力层面就能直接利用相对位置。
更高级的“接口”设计,如RoPE(Rotary 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(pos+k)的表达式。 | 回顾本文第3节的数学推导,理解旋转矩阵的性质。点积PE(pos)^T * PE(pos+k)的结果是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)的钥匙。