https://arxiv.org/pdf/2104.09864.pdf
Transformer升级之路:2、博采众长的旋转式位置编码 - 科学空间|Scientific Spaces
参考(此文中有证明):
一文通透位置编码:从标准位置编码、旋转位置编码RoPE到ALiBi、LLaMA 2 Long-CSDN博客
Transformer升级之路:2、博采众长的旋转式位置编码 - 科学空间|Scientific Spaces
正弦余弦位置编码、ROPE位置编码、可学习的位置编码的区别:
1、绝对 or 相对
正弦余弦位置编码(Sine-Cosine Positional Encoding)和ROPE(Recurrent Ordered Permutation Equivariant)编码都属于绝对位置编码。
可学习的位置编码(Learnable Positional Encoding)则属于相对位置编码。它是通过模型的权重参数来学习位置信息的表示,这意味着位置编码是相对于输入序列中其他位置的内容和顺序而确定的。这种编码方式可以允许模型在学习不同长度的序列时更好地适应位置信息。
2、添加的地方:word embedding层 or qkv中的q里面
正余弦位置编码,是加在了word embedding层。
训练式的位置编码以及ROPE,是加在了qkv中的q里面。
3、输入长度是否有限制
正余弦位置编码和ROPE位置编码没有输入长度的限制,但仍有推理资源和计算资源的限制。
训练式位置编码有输入长度的限制,在推理时不能推理更长的文本。
4、ROPE的有限性
ROPE(Recurrent Ordered Permutation Equivariant)确实是一种能够处理任意长度序列的编码方法,并且不会受到输入序列长度限制的影响。
ROPE或其他采用递归结构来处理长序列的模型,通常会出现数值稳定性的问题。这通常表现为在处理非常长的序列时,模型的训练稳定性或推理精度可能会受到影响。
旋转位置编码(Rotary Position Embedding,简称RoPE)是一种在众多大型神经网络模型中普遍应用的位置编码技术,被诸如Llama、Baichuan、ChatGLM、Qwen等模型所采用。大型模型由于受限于计算资源,往往在 有限的上下文窗口 内进行训练,这导致当 输入长度超出训练时的上下文范围时,模型性能可能会大幅下降。为了解决这个问题,出现了众多研究致力于基于RoPE的模型,使之能在超出预训练长度的情况下仍保持较好的性能。因此,深入理解RoPE的基本原理对于扩展基于RoPE的模型至超出训练长度的场景至关重要。
一般来说,绝对位置编码具有实现简单、计算速度快等优点,而相对位置编码则直接地体现了相对位置信号,跟我们的直观理解吻合,实际性能往往也更好。由此可见,如果可以通过绝对位置编码的方式实现相对位置编码,那么就是“集各家之所长”、“鱼与熊掌兼得”了。Sinusoidal位置编码隐约做到了这一点,但并不够好。
在RoPE中,我们的出发点就是“通过绝对位置编码的方式实现相对位置编码”,这样做既有理论上的优雅之处,也有实践上的实用之处,比如它可以拓展到线性Attention中就是主要因为这一点。
RoPE位置编码通过将一个向量旋转某个角度,为其赋予位置信息。
正余弦位置编码,是加在了word embedding层。而训练式的位置编码以及rope,是加在了qkv中的q里面。
一、为什么需要位置编码?
Attention机制的本质是计算输入序列中的每个token与整个序列的注意力权重。假设 qm 和 kn 分别表示词向量 q 位于位置 m 和词向量 k 位于位置 n ,在未添加位置信息的时候,qm=q,kn=k ,则两者的注意力权重计算如下:
在未加入位置信息的情况下,无论 q 和 k 所处的位置如何变化,它们之间的注意力权重 a 均不会发生变化,也就是位置无关(比如相同的一组token在不同的位置,attention score是一样的),这显然与我们的直觉不符。对于两个词向量,如果它们之间的距离较近,我们希望它们之间的的注意力权重更大,当距离较远时,注意力权重更小。
二、绝对位置编码
2.1、训练式位置编码 (可学习的)
GPT2的位置编码是可学习的。但是维度固定,不方便扩展。
训练式位置编码,顾名思义就是每个位置的位置向量会随着模型一起训练。假设模型最大输入长度为512,向量维度为768,我们可初始化一个512*768的位置编码矩阵,该矩阵将参与模型的训练,从而学习得到每个位置所对应的向量表示。
如何为每个位置的词向量注入位置信息呢?答案是相加,如以下公式所示,其中 pm 表示第 m 个位置的位置向量:
训练式位置编码广泛应用于早期的transformer类型的模型,如BERT、GPT、ALBERT等。但其缺点是模型不具有长度外推性,因为位置编码矩阵的大小是预设的,若对其进行扩展,将会破坏模型在预训练阶段学习到的位置信息。例如将512*768扩展为1024*768,新拓展的512个位置向量缺乏训练,无法正确表示512~1023的位置信息。但早期大家对长文本输入的需求并不如现在迫切。
2.2、Sinusoidal位置编码 (不可学习的)
Sinusoidal位置编码是谷歌在Transformer模型中提出的一种绝对位置编码,它的形式如下,其中 d 表示词向量的维度, k 表示位置索引,2i 和 2i+1 表示位置向量的分量索引,例如 p(k,2i) 和 p(k,2i+1) 分别表示位置 k 的位置向量的第 2i 和第 2i+1 个分量:
Sinusoidal位置编码的 每个分量都是正弦或余弦函数,所有每个分量的数值都具有周期性。如下图所示,每个分量都具有周期性,并且越靠后的分量,波长越长,频率越低。这是一个非常重要的性质,基于RoPE的大模型的长度外推工作,与该性质有着千丝万缕的关联,后续我们会进行分享。
Sinusoidal位置编码还具有远程衰减的性质,具体表现为:对于两个相同的词向量,如果它们之间的距离越近,则他们的内积分数越高,反之则越低。如下图所示,我们随机初始化两个向量q和k,将q固定在位置0上,k的位置从0开始逐步变大,依次计算q和k之间的内积。我们发现随着q和k的相对距离的增加,它们之间的内积分数震荡衰减。
因为Sinusoidal位置编码中的正弦余弦函数具备周期性,并且具备远程衰减的特性,所以理论上也具备一定长度外推的能力。
三、相对位置编码
3.1、相对位置编码为什么比绝对位置编码更好?
相对位置编码(Relative Position Encoding)与绝对位置编码(Absolute Position Encoding)是两种不同的方法,用于赋予序列模型识别元素在序列中位置的能力。相对位置编码的优势主要体现在以下几个方面:
-
泛化能力: 相对位置编码允许模型学习元素间的相对距离,而不是其在序列中的绝对位置。这种表示方法使得模型能够更好地泛化到不同长度的序列上,因为相对关系不会随着序列长度的变化而改变。
-
序列长度灵活性: 绝对位置编码通常是针对特定长度的序列预先定义的。如果序列长度超过了训练时的长度,模型可能无法处理新的位置编码。相对位置编码则可以适应任意长度的序列,因为它关注的是元素之间的距离而非固定的位置标识。
-
对齐: 在一些任务中,如机器翻译或文本摘要,输入和输出之间存在对齐关系。相对位置编码更容易捕捉这种对齐信息,因为它直接编码元素间的相对距离。
-
模型理解: 相对位置编码可能更符合人类的语言理解,我们通常会根据词语之间的关系来理解句子意义,而不是词语在句子中的绝对位置。因此,相对位置编码可能更贴近自然语言处理的实际需求。
-
可变上下文处理: 在处理像对话或文档这样的上下文时,关键信息可能根据上下文的变化而变化位置。相对位置编码可以帮助模型更好地理解上下文变化中的关键信息位置关系。
3.2、RoPE
RoPE位置编码通过将一个向量旋转某个角度,为其赋予位置信息。用绝对位置编码的方式,实现相对位置编码。
和绝对位置编码的任务一样,把位置编码的信息加在q里面。这样q k在计算attention score的时候,就带有了位置信息。
比如相同的一组token在不同的位置,attention score是一样的。
欧拉公式e^(ix)=cosx+isinx。e^(i*m*theta)=cos(m*theta)+isin(m*theta)
f(q, m) = (cos m*theta -sin m*theta , sin m*theta cos m*theta)(q0 q1) =
import torch
class LlamaRotaryEmbedding(torch.nn.Module):
def __init__(self, dim, max_position_embeddings=2048, base=10000, device=None):
super().__init__()
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float().to(device) / dim))
self.register_buffer("inv_freq", inv_freq)
# Build here to make `torch.jit.trace` work.
self.max_seq_len_cached = max_position_embeddings #2048
t = torch.arange(self.max_seq_len_cached, device=self.inv_freq.device, dtype=self.inv_freq.dtype)
freqs = torch.einsum("i,j->ij", t, self.inv_freq)
# Different from paper, but it uses a different permutation in order to obtain the same calculation
emb = torch.cat((freqs, freqs), dim=-1)
self.register_buffer("cos_cached", emb.cos()[None, None, :, :], persistent=False)
self.register_buffer("sin_cached", emb.sin()[None, None, :, :], persistent=False)
def forward(self, x, seq_len=None):
# x: [bs, num_attention_heads, seq_len, head_size]
# This `if` block is unlikely to be run after we build sin/cos in `__init__`. Keep the logic here just in case.
if seq_len > self.max_seq_len_cached:
self.max_seq_len_cached = seq_len
t = torch.arange(self.max_seq_len_cached, device=x.device, dtype=self.inv_freq.dtype)
freqs = torch.einsum("i,j->ij", t, self.inv_freq)
# Different from paper, but it uses a different permutation in order to obtain the same calculation
emb = torch.cat((freqs, freqs), dim=-1).to(x.device)
self.register_buffer("cos_cached", emb.cos()[None, None, :, :], persistent=False)
self.register_buffer("sin_cached", emb.sin()[None, None, :, :], persistent=False)
return (
self.cos_cached[:, :, :seq_len, ...].to(dtype=x.dtype),
self.sin_cached[:, :, :seq_len, ...].to(dtype=x.dtype),
)
rope = LlamaRotaryEmbedding(200, 2048)