自然语言处理(NLP)随着基于Transformer的模型取得了显著的进步。这些模型中的一项关键创新是位置编码,它有助于捕捉语言的序列特性。在这篇文章中,您将了解:
- Transformer模型中位置编码的必要性
- 不同类型的位置编码及其特性
- 如何实现各种位置编码方案
- 现代语言模型中如何使用位置编码
让我们开始吧!

语言模型中的位置编码
照片作者:Svetlana Gumerova。部分权利保留。
概述
本文分为五个部分,它们是:
- 理解位置编码
- 正弦位置编码
- 学习式位置编码
- 旋转位置编码 (RoPE)
- 相对位置编码
理解位置编码
考虑这两个句子:“狐狸跳过狗”和“狗跳过狐狸”。它们包含相同的单词,但顺序不同。在循环神经网络中,模型按顺序处理单词,自然地捕捉到这种差异。然而,Transformer模型并行处理所有单词,使其在没有额外信息的情况下无法区分这些句子。
位置编码通过提供每个token在序列中的位置信息来解决这个问题。每个token通过模型的嵌入层转换为一个向量,向量的大小称为“隐藏维度”。位置编码通过创建一个具有相同隐藏维度的向量来添加位置信息。
位置编码被添加到注意力模块的输入中。在点积操作期间,这些编码会强调相邻token之间的关系,帮助模型理解上下文。这使得模型能够区分具有相同单词但顺序不同的句子。
最常见的几种位置编码是:
- 正弦位置编码(原始Transformer中使用):使用由正弦和余弦函数构建的常数向量
- 学习式位置编码(BERT和GPT中使用):在训练期间学习向量
- 旋转位置编码(RoPE,Llama模型中使用):使用由旋转矩阵构建的常数向量
- 相对位置编码(T5和MPT中使用):基于token之间的距离而不是绝对位置
- 带有线性偏差的注意力(ALiBi,Falcon模型中使用):一种基于token距离添加到注意力得分的偏差项
每种类型都有其独特的优点和局限性,我们将在下面详细探讨。
正弦位置编码
原始Transformer论文引入了正弦位置编码。使用确定性函数为每个位置生成唯一的模式,如下面的公式所示:
$$
\begin{aligned}
PE(p, 2i) &= \sin\left(\frac{p}{10000^{2i/d}}\right) \\
PE(p, 2i+1) &= \cos\left(\frac{p}{10000^{2i/d}}\right)
\end{aligned}
$$
其中 $d$ 是隐藏维度(必须是偶数),$i$ 的取值范围是从0到 $d/2$。位置编码 $PE(p, k)$ 代表位置 $p$ 的向量中的第 $k$ 个元素。常数10000是原始Transformer论文建议的。它应该大于最大序列长度。
以下是PyTorch实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import torch import numpy as np def create_sinusoidal_encodings(seq_len, dim): N = 10000 i = torch.arange(0, dim//2) div_term = torch.exp(-np.log(N) * (2*i / dim)) position = torch.arange(seq_len).unsqueeze(1) pe = torch.zeros(seq_len, dim) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) return pe # 示例用法 seq_len = 512 dim = 768 positional_encodings = create_sinusoidal_encodings(seq_len, dim) sequence = sequence + positional_encodings |
在此实现中,div_term
计算 $1/N^{2i/d}$($i$ 从 0 到 $d/2-1$)。position
矩阵的形状为 (seq_len,1)
。在正弦和余弦函数中它们的乘积会产生一个形状为 (seq_len, dim//2)
的矩阵。结果会被交错放入形状为 (seq_len, dim)
的输出矩阵 pe
中。
正弦编码有两个关键优势:它们是确定性的,并且可以外插到比训练期间看到的更长的序列。由于正弦函数的特性,可以通过其位置编码向量的点积轻松计算token之间的相对位置。
然而,这些编码无法适应数据特征,对于非常长的序列可能效果不佳。
学习式位置编码
像GPT-2这样的模型使用学习式位置编码。以下是PyTorch实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import torch.nn as nn class LearnedPositionalEncoding(nn.Module): def __init__(self, max_seq_len, dim): super().__init__() self.position_embeddings = nn.Embedding(max_seq_len, dim) def forward(self, x): positions = torch.arange(x.size(1), device=x.device).expand(x.size(0), -1) position_embeddings = self.position_embeddings(positions) return x + position_embeddings # 示例用法 model = LearnedPositionalEncoding(max_seq_len=512, dim=768) |
nn.Embedding
层充当一个查找表,将整数索引映射到维度为 dim
的向量。在 forward()
函数中,positions
张量具有 (batch_size, seq_len, dim)
的形状,与输入 x
匹配。位置编码在注意力操作之前被加到 x
上。
学习式位置编码通过训练适应数据特征,在正确训练的情况下可能提供更好的性能。但是,它们无法外插到更长的序列,并且可能过拟合。它们还增加了模型的大小,因为它们是模型参数的一部分。
旋转位置编码 (RoPE)
大多数现代大型语言模型使用旋转位置编码 (RoPE)。它们通过旋转矩阵编码相对位置,每个位置代表角度的几何级数。公式为:
$$
\begin{aligned}
\hat{x}_m^{(i)} &= x_m^{(i)} \cos(m\theta_i) + x_m^{(d/2+i)} \sin(m\theta_i) \\
\hat{x}_m^{(d/2+i)} &= x_m^{(d/2+i)} \cos(m\theta_i) – x_m^{(i)} \sin(m\theta_i) \\
\end{aligned}
$$
其中 $\theta_i = 10000^{-2i/d}$,$d$ 是嵌入维度,$m$ 是位置索引,$i$ 的取值范围是从 0 到 $d/2-1$。以矩阵形式表示为:
$$
\mathbf{\hat{x}}_m = \mathbf{R}_m\mathbf{x}_m = \begin{bmatrix}
\cos(m\theta_i) & -\sin(m\theta_i) \\
\sin(m\theta_i) & \cos(m\theta_i)
\end{bmatrix} \mathbf{x}_m
$$
其中 $\mathbf{x}_m$ 代表位置 $m$ 处向量中的一对 $(i, d/2+i)$ 元素。
以下是PyTorch实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import torch import numpy as np def rotate_half(x): x1, x2 = x.chunk(2, dim=-1) return torch.cat((-x2, x1), dim=-1) def apply_rotary_pos_emb(x, cos, sin): return (x * cos) + (rotate_half(x) * sin) class RotaryPositionalEncoding(nn.Module): def __init__(self, dim, max_seq_len=512): super().__init__() N = 10000 inv_freq = 1. / (N ** (torch.arange(0, dim, 2).float() / dim)) inv_freq = torch.cat((inv_freq, inv_freq), dim=-1) position = torch.arange(max_seq_len).float() sinusoid_inp = torch.outer(position, inv_freq) self.register_buffer("cos", sinusoid_inp.cos()) self.register_buffer("sin", sinusoid_inp.sin()) def forward(self, x, seq_len=None): if seq_len is None: seq_len = x.size(1) cos = self.cos[:seq_len].view(1, seq_len, 1, -1) sin = self.sin[:seq_len].view(1, seq_len, 1, -1) return apply_rotary_pos_emb(x, cos, sin) |
register_buffer()
调用缓存了正弦和余弦计算,以提高效率。inv_freq
变量计算所有 $i$ 的 $\theta_i$,position
代表 $m$(索引从 0 到 max_seq_len-1
),而 sinusoid_inp
包含 $m\theta_i$,其矩阵形状为 (max_seq_len, dim//2)
。rotate_half()
函数将向量 $(x_1, x_2, \cdots, x_{d-1}, x_{d})$ 转换为 $(-x_{d/2+1}, -x_{d/2+2}, \dots, x_{d/2-1}, x_{d/2})$。然后,apply_rotary_pos_emb()
将旋转矩阵应用于输入。
RoPE 提供了几个优势:
- 旋转矩阵 $\mathbf{R}_m$ 将 2D 输入向量按角度 $m\theta_i$ 进行几何旋转。
- 转置 $\mathbf{R}_m^\top = \mathbf{R}_m^{-1}$ 代表反向旋转。因此,相对位置可以很容易地计算为 $\mathbf{R}_{m-n} = \mathbf{R}_m\mathbf{R}_n^\top$。
- 由于角度的几何级数,它可以外插到更长的序列。
- 由于 $\cos^2t+\sin^2t=1$,RoPE 保留了 $\mathbf{x}_m$ 的向量范数,有助于训练稳定性。
相对位置编码
虽然以前的实现使用绝对 token 位置,但通常重要的是 token 之间的相对位置。以下是使用相对位置编码的简化实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import torch import torch.nn as nn class RelativePositionalEncoding(nn.Module): def __init__(self, max_relative_position, d_model): super().__init__() self.max_relative_position = max_relative_position self.relative_attention_bias = nn.Parameter( torch.randn(2 * max_relative_position + 1, d_model) ) def forward(self, length): context_position = torch.arange(length, dtype=torch.long)[:, None] memory_position = torch.arange(length, dtype=torch.long)[None, :] relative_position = memory_position - context_position relative_position_bucket = relative_position + self.max_relative_position return self.relative_attention_bias[relative_position_bucket] |
relative_position
矩阵的形状为 (length, length)
,每个元素表示 token $i$ 和 $j$ 之间的相对位置。这是通过将 $N\times 1$ 矩阵 context_position
从 $1\times N$ 矩阵 memory_position
中减去来计算的。
relative_position_bucket
将值移位为非负数,并从 relative_attention_bias
张量中查找位置编码向量。
相对位置编码自然地处理可变长度序列,并且对于翻译等任务效果很好,因此成为T5等模型的选择。
带有线性偏差的注意力(ALiBi)是一种相关的方法,它向注意力得分添加一个偏差矩阵,而不是操作输入序列。在上面的代码中,您可以看到 relative_positon_bucket
用于查找一系列向量作为位置编码,然后将其添加到注意力模块的输入序列中。在ALiBi中,输入序列直接用于计算注意力得分。但在之后,relative_positon_bucket
的矩阵会被缩放并添加到注意力得分矩阵中,然后才进行 softmax 操作。ALiBi 中的缩放因子计算为 $m_h=1/2^{8h/H}$,其中 $h$ 是头索引,$H$ 是总注意力头数。
进一步阅读
以下是一些关于该主题的进一步阅读材料:
- Attention Is All You Need(原始Transformer论文)
- RoFormer: Enhanced Transformer with Rotary Position Embedding(RoPE论文)
- Self-Attention with Relative Position Representations
- Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation(ALiBi论文)
- BERT:用于语言理解的深度双向 Transformer 预训练
- Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer(T5论文)
总结
在这篇文章中,您了解了位置编码及其在Transformer模型中的重要性。特别是,您了解到:
- 位置编码是必需的,因为Transformer会并行处理token。
- 不同类型的位置编码具有不同的优缺点。
- 正弦编码是确定性的,并且可以外插到更长的序列。
- 学习式编码很简单,但不能外插。
- RoPE在长序列上提供了更好的性能。
- 相对位置编码关注token之间的距离。
暂无评论。