ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Transformer位置编码:从正弦函数到RoPE,让模型理解序列顺序

Transformer位置编码:从正弦函数到RoPE,让模型理解序列顺序

1. 从“词袋”到“序列”:位置编码的缘起与核心挑战

如果你尝试过用早期的神经网络处理文本,比如用全连接网络或简单的词袋模型去做情感分析,可能会觉得效果还行。但当你把任务换成机器翻译、文本摘要或者代码生成时,问题立刻就暴露了:模型根本不知道词语的顺序。给它“猫追老鼠”和“老鼠追猫”,它可能认为这两句话表达的意思是一样的,因为模型看到的只是“猫”、“追”、“老鼠”这三个词的集合。这就是自然语言处理(NLP)早期面临的核心困境——模型缺乏对序列顺序的感知能力。

循环神经网络(RNN)及其变体LSTM、GRU的出现,一度被认为是解决序列建模的“银弹”。它们通过隐状态在时间步上的传递,理论上能够捕捉序列的先后关系。然而,在实际的大规模训练中,RNN的串行计算特性导致了严重的效率瓶颈,并且长距离依赖的捕捉能力依然有限,梯度消失或爆炸的问题始终如影随形。卷积神经网络(CNN)通过滑动窗口也能捕捉局部顺序,但其感受野受限于卷积核大小,对于长序列的全局依赖建模能力不足。

Transformer架构的横空出世,彻底改变了游戏规则。它摒弃了RNN的循环结构,完全依赖自注意力(Self-Attention)机制来建立序列中任意两个元素之间的关联。自注意力机制的计算是“并行”且“无序”的:给定一个句子,模型会同时计算所有词与所有词之间的注意力权重。这带来了前所未有的计算效率和强大的全局建模能力,但也引入了一个根本性问题:自注意力机制本身是置换等变的(Permutation Equivariant)。简单来说,如果你把输入序列的词序完全打乱,自注意力层输出的结果,其内部顺序也会相应打乱,但每个位置上的向量表示,其“内容”只与打乱后的输入集合有关,而与原始顺序无关。模型丢失了至关重要的位置信息。

这就好比在一个会议上,大家只根据彼此的身份(词向量)进行交流和形成共识,但完全忘记了谁先发言、谁后发言。会议的最终结论(模型输出)可能只取决于参与者的身份集合,而非他们发言的次序,这显然无法准确理解一段有序的叙述或指令。因此,我们必须人为地将“位置信息”注入到模型的输入中,这就是位置编码(Positional Encoding, PE)最根本的动机:在保持自注意力并行计算优势的同时,让模型能够利用序列的顺序信息。

那么,如何注入位置信息呢?最直观的想法可能是直接给每个位置分配一个独立的、可学习的向量,即“可学习的位置编码”。这确实是一种常用且有效的方法,尤其是在预训练数据充足、序列长度固定的场景下(如BERT)。然而,Transformer原论文的作者们选择了一条更优雅、更具理论深度的路径:使用正弦和余弦函数来构造位置编码。这个选择背后,蕴含着对模型泛化能力、计算效率和多维度关系编码的深刻考量。

2. 正弦与余弦:为什么是它们,而不是别的?

当我们决定要编码位置时,首先要确定编码的形式。我们需要一个函数 $f: \mathbb{N} \to \mathbb{R}^d$,将位置索引 $pos$(一个整数)映射到一个 $d$ 维的实数向量,这个向量最终会与词嵌入向量相加,作为Transformer的输入。

2.1 核心设计目标与正弦函数的契合度

作者的设计目标非常明确,他们希望位置编码能满足以下几个关键性质:

  1. 唯一性:每个位置必须有唯一的编码。
  2. 相对位置关系的可表达性:模型应能轻易地学习到位置之间的相对关系,例如“位置5”和“位置6”的关联,应该与“位置20”和“位置21”的关联具有某种相似的模式。这对于理解语言中的顺序至关重要。
  3. 长度外推性:模型在训练时可能只见过一定长度(如512)的序列,但我们希望它能够处理更长的序列。因此,位置编码应该能自然地泛化到训练时未见过的位置。
  4. 有界性且平滑:编码值应该有界,避免在叠加到词嵌入后造成数值不稳定。同时,相邻位置的编码变化应该是平滑的,这有助于模型的优化。

现在,让我们看看正弦和余弦函数是如何完美契合这些目标的。

唯一性与周期性:正弦函数 $sin(x)$ 本身是周期性的,单独使用确实会导致不同位置编码相同。但Transformer使用的是一组频率不同的正弦和余弦函数的组合。具体公式如下:

$$ PE_{(pos, 2i)} = sin(pos / 10000^{2i/d_{model}}) $$ $$ PE_{(pos, 2i+1)} = cos(pos / 10000^{2i/d_{model}}) $$

其中:

  • $pos$ 是位置索引(0, 1, 2, ...)。
  • $i$ 是维度索引(0, 1, ..., d_model/2 - 1),它决定了该维度所使用的函数的频率。
  • $d_{model}$ 是词嵌入的维度(也是位置编码的维度)。

这里的关键在于,对于每个维度 $i$,我们使用了一个不同的频率(由 $10000^{2i/d_{model}}$ 决定)。频率随着维度索引 $i$ 的增加而指数级下降。这意味着在低维度($i$ 小),正弦波变化非常快(高频),编码了细粒度的位置差异;在高维度($i$ 大),正弦波变化非常慢(低频),编码了粗粒度的、大范围的位置信息。这种从高频到低频的频谱覆盖,确保了即使某个频率在某个区间内重复,其他频率的波也处于不同相位,从而在高维空间中,每个位置的向量表示几乎是唯一的。

相对位置关系的线性可表达性:这是正弦位置编码最精妙之处。对于某个固定的偏移量 $k$,位置 $pos + k$ 的编码可以由位置 $pos$ 的编码通过一个线性变换得到。

我们可以利用三角函数的和角公式: $$ sin(\alpha + \beta) = sin\alpha cos\beta + cos\alpha sin\beta $$ $$ cos(\alpha + \beta) = cos\alpha cos\beta - sin\alpha sin\beta $$

令 $\alpha = pos \cdot \omega_i$, $\beta = k \cdot \omega_i$,其中 $\omega_i = 1 / 10000^{2i/d_{model}}$。那么: $$ PE_{(pos+k, 2i)} = sin((pos+k)\omega_i) = sin(pos\omega_i)cos(k\omega_i) + cos(pos\omega_i)sin(k\omega_i) $$ $$ PE_{(pos+k, 2i+1)} = cos((pos+k)\omega_i) = cos(pos\omega_i)cos(k\omega_i) - sin(pos\omega_i)sin(k\omega_i) $$

这可以写成一个矩阵乘法的形式: $$ \begin{bmatrix} PE_{(pos+k, 2i)} \ PE_{(pos+k, 2i+1)} \end{bmatrix} = \begin{bmatrix} cos(k\omega_i) & sin(k\omega_i) \ -sin(k\omega_i) & cos(k\omega_i) \end{bmatrix} \cdot \begin{bmatrix} PE_{(pos, 2i)} \ PE_{(pos, 2i+1)} \end{bmatrix} $$

这个变换矩阵是一个旋转矩阵!它只依赖于相对距离 $k$ 和频率 $\omega_i$,而与绝对位置 $pos$ 无关。这意味着,模型的自注意力机制理论上可以很容易地学会通过这种固定的线性变换,来关注“相对距离为k”的关系。例如,要判断两个词是否是相邻关系,模型可以学习一个权重模式,该模式对应于 $k=1$ 时的旋转矩阵所诱导的向量关系。这为模型理解“下一个词”、“前一个词”等相对位置概念提供了直接的数学基础。

长度外推性:由于正弦函数定义在整个实数域上,我们可以为任意大的 $pos$ 计算其编码。虽然训练时模型只见过有限范围内的位置,但正弦函数的连续性使得模型有机会将学到的位置关系模式泛化到更长的序列上(尽管实践中直接外推效果可能并不完美,但这提供了可能性)。相比之下,可学习的位置编码在遇到超出训练长度的位置时,完全无法给出有意义的表示。

有界性与平滑性:正弦和余弦函数的值域天然就是 $[-1, 1]$,这与经过归一化处理的词嵌入向量能够很好地相加,不会引起数值爆炸。同时,函数本身是无限可微的,相邻位置的编码变化极其平滑,这为基于梯度的优化算法提供了良好的条件。

2.2 与“可学习位置编码”的对比

理解了正弦编码的优势,我们再回头对比“可学习位置编码”(Learned Positional Embedding)。后者将每个位置视为一个独立的可学习参数,在训练中通过梯度下降来优化。

  • 优点:极度灵活,不引入任何先验假设,让数据自己决定最好的位置表示。在数据充足且序列长度固定的任务上(如BERT的512长度),它通常能取得非常好的效果。
  • 缺点
    1. 缺乏外推性:模型无法处理比训练时更长的序列。除非在训练时就用更长的序列,或者采用一些技巧(如截断、分段)。
    2. 可能缺乏对相对位置的显式归纳偏置:模型需要从零开始学习所有位置两两之间的关系,这可能比利用正弦编码固有的相对位置线性性质需要更多的数据和计算。
    3. 对训练数据中的位置分布敏感:如果训练数据中某些位置出现得很少,其编码可能学习不充分。

在实际应用中,两种方法都被广泛使用。Transformer原论文选择正弦编码,很大程度上是出于其对机器翻译这种序列到序列任务中长度可变性和相对位置重要性的考量。而在像BERT这样的编码器模型中,由于输入长度固定且数据量巨大,可学习位置编码成为了更主流和简便的选择。

3. 正弦位置编码的实践细节与可视化理解

理论很优美,但落到代码和实际理解上,我们还需要拆解几个关键细节。

3.1 维度交替与频率衰减:编码空间的构建

让我们仔细审视公式中的 $10000^{2i/d_{model}}$。这个项决定了频率。由于 $2i/d_{model}$ 在 $i$ 从0增长到 $d_{model}/2-1$ 时,从0增长到接近1,所以 $10000^{2i/d_{model}}$ 从 $10000^0=1$ 衰减到 $10000^{1}=10000$ 的倒数,即频率从 $1$ 衰减到 $1/10000$。

为什么是10000?这个数字是一个超参数,论文中称为“波长”的调节因子。较大的基数(如10000)意味着频率衰减得更慢,低频维度占据更多。这相当于为模型提供了更丰富的、变化缓慢的“全局位置”信号。你可以将其调小(如1000)或调大,但10000是一个经过实验验证、在多种任务上表现良好的默认值。

维度的交替:公式中,偶数维度($2i$)用 $sin$,奇数维度($2i+1$)用 $cos$。这种交替安排并非随意。回顾之前的线性变换推导,正是这种 $sin$ 和 $cos$ 的配对,使得同一频率 $\omega_i$ 下的两个维度可以组成一个二维空间中的点 $[sin(pos\cdot\omega_i), cos(pos\cdot\omega_i)]$。随着 $pos$ 增加,这个点在该二维平面上做匀速圆周运动。不同频率 $\omega_i$ 对应不同转速的圆。最终的位置编码向量,就是所有这些不同转速的圆周运动在 $d_{model}$ 维空间中的叠加。

我们可以通过一个简单的Python可视化来感受一下:

import numpy as np import matplotlib.pyplot as plt def get_positional_encoding(max_len, d_model): pe = np.zeros((max_len, d_model)) for pos in range(max_len): for i in range(0, d_model, 2): omega = 1 / (10000 ** ((i) / d_model)) pe[pos, i] = np.sin(pos * omega) pe[pos, i+1] = np.cos(pos * omega) return pe # 假设词向量维度是64,看前128个位置 d_model = 64 max_len = 128 pe = get_positional_encoding(max_len, d_model) # 可视化前几个维度的波形 plt.figure(figsize=(12, 8)) for i in range(6): # 看前3对(6个)维度 plt.subplot(3, 2, i+1) plt.plot(pe[:100, i]) plt.title(f'Dimension {i} ({"sin" if i%2==0 else "cos"})') plt.xlabel('Position') plt.ylabel('Encoding Value') plt.tight_layout() plt.show() # 热力图观察整个编码矩阵 plt.figure(figsize=(10, 8)) plt.imshow(pe.T, aspect='auto', cmap='RdBu') plt.xlabel('Position Index') plt.ylabel('Encoding Dimension') plt.colorbar(label='Encoding Value') plt.title('Positional Encoding Matrix (d_model=64)') plt.show()

运行这段代码,你会看到:

  1. 前几个维度(高频)的波形变化非常剧烈,相邻位置的值差异很大。
  2. 越往后的维度(低频),波形越来越平缓,像一条缓慢波动的曲线。
  3. 从热力图中,你能清晰地看到一种“条纹”模式,这是不同频率正弦波叠加的结果。每个位置(每一列)的向量模式都是独特的。

3.2 与词嵌入的相加:信息融合的方式

得到位置编码 $PE$ 和词嵌入 $E$ 后,Transformer的输入是 $X = E + PE$。为什么是相加(Add)而不是拼接(Concatenate)?

  • 相加保持了维度不变:$d_{model}$ 维的嵌入加上 $d_{model}$ 维的位置编码,输入维度仍然是 $d_{model}$,网络结构无需改变。
  • 相加迫使模型在同一个表示空间中同时处理语义和位置信息:这可以看作是一种“调制”。模型后续的线性变换和注意力机制会同时作用于融合后的信息。从信息论角度看,这要求模型学会在同一个向量通道中区分和利用两种不同来源的信号,虽然增加了学习难度,但也可能促使学习到更紧凑、高效的表示。
  • 实践有效性:在Transformer及其后续变体中,相加被证明是简单且有效的。拼接虽然能更清晰地区分两种信息,但会加倍输入维度,显著增加第一层线性变换的参数量和计算量,而收益并不明显。

注意:相加操作的一个隐含假设是,词嵌入和位置编码的数值范围大致在同一量级。通常词嵌入会进行缩放(例如乘以 $\sqrt{d_{model}}$)或标准化,位置编码值域为[-1,1],这保证了相加的稳定性。

4. 超越原始设计:位置编码的演进与变体

原始的Sinusoidal PE是开创性的,但它并非没有缺点,尤其是在长度外推和高效计算方面。近年来,研究者们提出了许多改进和替代方案。

4.1 相对位置编码(Relative Positional Encoding)

原始Transformer的绝对位置编码有一个问题:它只告诉模型“这是第几个词”,但模型在计算注意力时,更关心的是词与词之间的“相对距离”。虽然正弦编码本身支持相对位置的线性表示,但需要模型在注意力计算中自己去发现和利用这种关系。

相对位置编码将位置信息直接注入到注意力权重的计算过程中。典型代表如《Self-Attention with Relative Position Representations》和Transformer-XL中的方法。其核心思想是,在计算注意力分数 $Attention(Q, K) = softmax(\frac{QK^T}{\sqrt{d_k}})$ 时,额外加入一个偏置项 $B$,这个 $B$ 只与查询位置 $i$ 和键位置 $j$ 的相对距离 $(i-j)$ 有关。

例如,可以定义一组可学习的标量 $b_{i-j}$,然后让注意力分数变为 $softmax(\frac{QK^T}{\sqrt{d_k}} + B)$,其中 $B_{ij} = b_{i-j}$。这样,模型在计算“猫”对“追”的注意力时,如果“猫”在位置2,“追”在位置3,那么就会加上一个代表“距离为+1”的偏置 $b_{+1}$。这种方式更直接地建模了相对位置关系,并且在处理超长文本时,由于偏置只依赖于相对距离,其参数数量是固定的,与序列长度无关,外推性更好。

4.2 RoPE(Rotary Position Embedding)

RoPE可以看作是正弦位置编码理论优雅性的现代升级版。它同样利用了旋转的思想,但将其应用得更加彻底和统一。

RoPE的核心洞察是:不对词向量做加法,而是对查询(Q)和键(K)向量做旋转变换。对于位置 $m$ 的词向量 $x_m$,其对应的查询向量 $q_m$ 和键向量 $k_m$ 会经过一个依赖于位置 $m$ 的旋转矩阵 $R_{\Theta, m}$ 进行变换:$\tilde{q}m = R{\Theta, m} q_m$, $\tilde{k}n = R{\Theta, n} k_n$。

这个旋转矩阵 $R_{\Theta, m}$ 的设计非常巧妙,它作用于词向量的每一对维度上(类似于正弦编码中的配对维度),使其旋转角度与位置 $m$ 和频率 $\theta_i$ 成正比。计算变换后的注意力分数时: $$ \tilde{q}m^T \tilde{k}n = (R{\Theta, m} q_m)^T (R{\Theta, n} k_n) = q_m^T R_{\Theta, n-m} k_n $$

神奇的事情发生了:最终的注意力分数 $q_m^T R_{\Theta, n-m} k_n$只依赖于词向量本身和它们的相对位置 $(n-m)$。这完美地将相对位置信息编码进了注意力机制,且形式非常简洁。RoPE具有很好的外推性,并且被广泛应用于LLaMA、GPT-NeoX等当前主流的大语言模型中。

4.3 可学习位置编码的复兴与改进

尽管正弦编码有诸多理论优点,但在许多视觉Transformer(ViT)和经过海量数据预训练的语言模型中,简单的可学习位置编码(一个nn.Embedding(max_len, d_model))因其极致的简单和灵活性,依然是最主流的选择。

为了克服其外推性差的缺点,研究者们也提出了许多方法:

  • 层次化位置编码:将位置索引分解为不同粒度(如段落、句子、词)进行编码后再融合。
  • 相对位置偏置:在可学习绝对位置编码的基础上,再像相对位置编码那样,在注意力分数中加入可学习的相对位置偏置。
  • 外推法:在训练时采用某种策略,让模型接触到更长的位置索引,例如随机裁剪长序列、使用位置插值(Position Interpolation)等。Meta在发布Llama 2时,就通过微调时将位置索引线性缩放,成功将上下文长度从2k扩展到4k。

4.4 无位置编码的探索

更有趣的探索是:我们是否真的需要显式的位置编码?一些研究发现,在视觉任务中,当图像被分割成 patches 后,由于每个 patch 本身就带有空间结构信息(通过卷积或特殊初始化),模型有时能在没有显式位置编码的情况下学到令人满意的性能。但在语言任务中,由于文本的强顺序性,完全移除位置编码通常会导致性能显著下降。

5. 实战中的选择、调试与避坑指南

了解了这么多理论,在实际项目中该如何选择和运用位置编码呢?

5.1 如何根据任务选择位置编码

  1. 自然语言处理(尤其是生成式任务、长度可变)

    • 首选RoPE:对于自回归语言模型(如GPT系列)、机器翻译解码器,RoPE是目前事实上的标准。它的外推性和理论性质都非常优秀。许多开源模型(LLaMA, ChatGLM)都采用了RoPE。
    • 次选正弦编码:如果你在复现原始Transformer或进行相关研究,正弦编码仍然是理解原理的绝佳起点。对于长度相对固定的编码任务(如文本分类),它也能工作得很好。
    • 慎用可学习编码:除非你能确定你的训练和推理序列长度完全一致且不会变化。
  2. 计算机视觉(ViT及变体)

    • 可学习位置编码:这是最普遍、最稳定的选择。因为图像通常被分割成固定数量的 patches(如14x14=196),序列长度是固定的。可学习编码简单有效。
    • 相对位置偏置:在Swin Transformer等模型中,由于使用了窗口注意力,相对位置偏置(在一个局部窗口内定义)能更好地捕捉局部空间关系,效果通常优于绝对位置编码。
    • 条件位置编码(CPE):一种更灵活的方式,根据输入图像内容动态生成位置编码,能更好地处理可变分辨率的输入。
  3. 多模态任务

    • 需要统一处理文本和图像的位置信息。通常文本端使用RoPE或可学习编码,图像端使用可学习编码或相对偏置。关键在于将两种模态的位置编码映射到同一语义空间,或者设计跨模态的相对位置计算方式。

5.2 实现正弦位置编码的常见陷阱

即使你决定使用正弦编码,在实现时也容易踩坑:

陷阱一:频率计算错误错误的实现(在一些早期教程中常见):

# 错误!分母的指数计算不对 for pos in range(max_len): for i in range(d_model): pe[pos, i] = np.sin(pos / (10000 ** (i / d_model))) # i 应该按奇偶区分处理

正确的实现必须严格按照公式,对奇偶维度分别用sin和cos,并且频率项的计算要准确。使用前面提供的get_positional_encoding函数是安全的。

陷阱二:忘记停止梯度在PyTorch中,如果你像计算词嵌入那样将位置编码定义为一个nn.Parameter并赋值,那么它会被视为可学习参数。但原始的正弦编码是不可学习的确定性函数。你应该在forward函数中实时计算,或者预先计算好一个缓冲区(register_buffer)并设置requires_grad=False

# 正确做法:注册为buffer class TransformerModel(nn.Module): def __init__(self, d_model, max_len=5000): super().__init__() self.d_model = 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) * -(math.log(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('pe', pe) # 不参与梯度更新 def forward(self, x): # x: (batch, seq_len, d_model) x = x + self.pe[:, :x.size(1)] # 自动广播相加 return x

陷阱三:与词嵌入的缩放不匹配如前所述,如果词嵌入做了缩放(例如乘以 $\sqrt{d_{model}}$),那么位置编码的幅值([-1,1])可能显得过小。一种经验性做法是对位置编码也进行适当的缩放,或者确保词嵌入初始化在一个合理的范围内(如使用Xavier或Kaiming初始化),使得两者量级相近。

5.3 长度外推:当模型遇到更长的序列

这是所有位置编码方案面临的共同挑战。即使理论上正弦编码可以外推,但模型在短序列上训练出的注意力模式,可能无法直接迁移到长序列。

策略一:位置插值(Position Interpolation)这是目前最流行且有效的后处理方法。基本思想是将超出训练长度的位置索引“压缩”回模型见过的范围。例如,模型在长度为L的序列上训练,现在要处理长度为L’ > L的序列。我们不是直接使用位置索引pos,而是使用缩放后的索引pos * (L / L’)。这相当于将更长的位置序列线性地“挤”进训练过的位置区间。Meta的Code Llama和许多开源模型扩展上下文窗口都用了类似技巧。

策略二:随机化训练长度在训练时,不是固定使用最大长度,而是每次随机从某个范围内(如[256, 1024])抽取序列长度进行训练。这能强迫模型适应多种长度,提升鲁棒性。但这种方法可能牺牲在固定长度上的最优性能。

策略三:使用本身具有更好外推性的编码如RoPE,其相对位置的性质使其在外推上具有天然优势。ALiBi(Attention with Linear Biases)也是一种为外推而设计的相对位置编码,它直接给注意力分数加上一个与相对距离成负比例的线性偏置,被证明具有极强的外推能力。

位置编码,这个看似简单的组件,实则是Transformer模型理解有序世界的基石。从正弦波的优雅旋转,到RoPE的巧妙设计,再到各种工程上的权衡与技巧,它的演进史也反映了深度学习对先验知识、归纳偏置和计算效率的持续探索。理解它,不仅是为了用好Transformer,更是为了洞察如何将人类对世界的结构性认知(如顺序、距离),有效地赋予给强大的数据驱动模型。

返回列表