首页 > 其他分享 >Transformer为什么需要“位置编码”?

Transformer为什么需要“位置编码”?

时间:2024-11-29 12:30:43浏览次数:6  
标签:编码 Transformer torch self 位置 NTK xq

Transformer为什么需要“位置编码”?

技术微佬 丁师兄大模型 2024年11月20日 21:06 湖北

 我是丁师兄,专注于智能驾驶大模型,持续分享LLM面试干货。

图片

offer捷报

图片

坐标苏州的学员,通过训练营的项目和面试辅导,拿下苏州某国企大模型offer,年薪30w+,虽然相比于互联网不高,但是在二线城市国企,也很稳定了,恭喜上岸!

01

Transformer的位置编码

对于序列处理模型,他的位置信息需要通过显式或者隐式的方式传入给模型。

transformer 系列模型的位置编码常用包括:包括绝对位置编码(包括静态位置编码和可学习位置编码)、相对位置编码、旋转位置编码,下面详细介绍这种位置编码的原理和优缺点。

02

位置编码

(1)静态位置编码

transformer 原始位置编码方式,通过正弦/余弦函数得到位置编码,为什么要使用正余弦交替出现的方式来得到位置编码呐?

图片

公式1

主要的考虑的点如下:

  • 每个 token 的向量是唯一值;

  • 利用三角函数做位置编码可以保证编码的值在 [-1,1] 之间,是有界函数;

  • 不同的位置向量是可以通过线性变化得到的,这样 token 的位置信息不仅包含了绝对位置还包含了相对位置信息;

三角函数静态编码的重要性质如下图:

图片

class SinPositionEncoding(nn.Module):    def __init__(self, max_sequence_length, d_model, base=10000):        super().__init__()        self.max_sequence_length = max_sequence_length        self.d_model = d_model        self.base = base

    def forward(self):        pe = torch.zeros(self.max_sequence_length, self.d_model, dtype=torch.float)  # size(max_sequence_length, d_model)        exp_1 = torch.arange(self.d_model // 2, dtype=torch.float)  # 初始化一半维度,sin位置编码的维度被分为了两部分        exp_value = exp_1 / (self.d_model / 2)

        alpha = 1 / (self.base ** exp_value)  # size(dmodel/2)        out = torch.arange(self.max_sequence_length, dtype=torch.float)[:, None] @ alpha[None, :]  # size(max_sequence_length, d_model/2)        embedding_sin = torch.sin(out)        embedding_cos = torch.cos(out)

        pe[:, 0::2] = embedding_sin  # 奇数位置设置为sin        pe[:, 1::2] = embedding_cos  # 偶数位置设置为cos        return pe

SinPositionEncoding(d_model=4, max_sequence_length=10, base=10000).forward()

(2)可学习的位置编码

主要存在于 BERT、VIT 等变种系列模型,直接将位置作为可学习的参数,让模型自己学习。

class TrainablePositionEncoding(nn.Module):    def __init__(self, max_sequence_length, d_model):        super().__init__()        self.max_sequence_length = max_sequence_length        self.d_model = d_model

    def forward(self):        pe = nn.Embedding(self.max_sequence_length, self.d_model)        nn.init.constant(pe.weight, 0.)        return pe

(3)相对位置编码

相对位置编码的实现可以参考之前的文章:鹅厂面试官:Transformer为何需要位置编码?

(4)旋转位置编码

旋转位置编码(Rotary Position Embedding,RoPE)是论文 Roformer: Enhanced Transformer With Rotray Position Embedding 提出的一种能够将相对位置信息依赖集成到 self-attention 中并提升 transformer 架构性能的位置编码方式。

而目前很火的 LLaMA、GLM 模型也是采用该位置编码方式。

和相对位置编码相比,RoPE 具有更好的外推性(在训练时和预测时的输入长度不一致,导致模型的泛化能力下降的问题),目前是大模型相对位置编码中应用最广的方式之一。

对于 self- attention 中的 KQV 通过如下方式得到:

图片

其中 qm 表示第 m 个 token 对应的词向量 xm 集成位置信息 m 之后的 query 向量。

而 kn 和 vn 则表示第 n 个 token 对应的词向量 xn 集成位置信息 n 之后的 key 和 value 向量。

对于 transformer 的位置信息是三角函数,所以函数 f 为 x_m+p_i,其中 p_i 是根据公式 1 计算得到的;

对于 BERT 等可学习位置编码,它的f函数是 x_m+p_i,其中 p_i 是模型学习得到的;

图片

图片

图片

对于图例如下:

图片

# 旋转位置编码计算def apply_rotary_emb(    xq: torch.Tensor,    xk: torch.Tensor,    freqs_cis: torch.Tensor,) -> Tuple[torch.Tensor, torch.Tensor]:    # xq.shape = [batch_size, seq_len, dim]    # xq_.shape = [batch_size, seq_len, dim // 2, 2]    xq_ = xq.float().reshape(*xq.shape[:-1], -1, 2)    xk_ = xk.float().reshape(*xk.shape[:-1], -1, 2)        # 转为复数域    xq_ = torch.view_as_complex(xq_)    xk_ = torch.view_as_complex(xk_)        # 应用旋转操作,然后将结果转回实数域    # xq_out.shape = [batch_size, seq_len, dim]    xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(2)    xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(2)    return xq_out.type_as(xq), xk_out.type_as(xk)

class Attention(nn.Module):    def __init__(self, args: ModelArgs):        super().__init__()

        self.wq = Linear(...)        self.wk = Linear(...)        self.wv = Linear(...)                self.freqs_cis = precompute_freqs_cis(dim, max_seq_len * 2)

    def forward(self, x: torch.Tensor):        bsz, seqlen, _ = x.shape        xq, xk, xv = self.wq(x), self.wk(x), self.wv(x)

        xq = xq.view(batch_size, seq_len, dim)        xk = xk.view(batch_size, seq_len, dim)        xv = xv.view(batch_size, seq_len, dim)

        # attention 操作之前,应用旋转位置编码        xq, xk = apply_rotary_emb(xq, xk, freqs_cis=freqs_cis)                # scores.shape = (bs, seqlen, seqlen)        scores = torch.matmul(xq, xk.transpose(1, 2)) / math.sqrt(dim)        scores = F.softmax(scores.float(), dim=-1)        output = torch.matmul(scores, xv)  # (batch_size, seq_len, dim)  # ......

上面介绍了 transformer 处理序列时的常用位置编码,包括包括绝对位置编码(包括静态位置编码和可学习位置编码)、相对位置编码、旋转位置编码。

随着大模型的兴起,需要模型有更好的外推性,传统的相对和绝对位置编码无法满足时,LLM 是如何处理的。接下来我们介绍 LLM 中常用的位置编码。

位置编码的作用:

  • 克服 transformer 的位置不变性,因为双向注意力是位置不敏感的,所以需要位置编码;

  • 注入位置相关的先验知识,大部分的位置编码都带有“位置越近,越重要”的先验知识。

传统位置编码的问题:

  • 可学习位置编码:不具备外推性

  • 正余弦位置编码:有一定的外推性,但是缺少相对位置关系,效果较差;

LLM 的位置编码:

  • Rope:旋转位置编码,将位置编码通过旋转矩阵的方式,添加到注意力机制中;

  • ALIBI:添加一个不可学习的偏置项,来代表位置编码,添加到注意力机制中;

  • NTK:神经正切核,他是一种 scaling 位置编码的算法,常用 NTK-Rope 和 NTK-Alibi;

  • YaRN:在 Rope 的基础上进行改进,使其有更好的外推性;

  • Cope:考虑上下文的编码,解决了以往位置编码与 token 无关,只与位置有关的问题;

03

新型编码介绍

(1)Rope

旋转位置编码不像原始的绝对位置编码(在输入中单独加入位置编码信息),而是在计算注意力时,考虑 token 之间的位置关系。

和原始的 transformer 的区别,是在 qkv 中增加位置信息,得到新的 qkv:

图片

在做注意力计算时,将位置信息通过旋转矩阵的方式引入,

图片

图片

图片

注意:截图来自于论文,矩阵中 m 实则为位置差 m-n。

图片

RoPE 形式上和 Sinusoidal 位置编码有点相似,只不过 Sinusoidal 位置编码是加性的,而 RoPE 可以视为乘性的。这么做的其实很大程度上依赖于三角函数的特性。

优点:去掉了量级笨重且难训练的位置向量矩阵,显著化了相对位置在注意力中的重要性,让每个变量更关注相对位置的变化,增加了模型的外推性。

图片

(2)ALiBi

alibi 的原理相对简单,也是在计算注意力时引入位置关系,但是他是直接通过添加一个不可学习的偏置项,来弱化远距离的注意力系数,强化近距离的注意力系数。

图片

图片

m 的取值与 attention head 有关,假设有 H 个头,那么 m 的取值是:

图片

其中 h 为第 h 个头。

(3)NTK

问题引入:无论是 Rope 还是 Alibi 位置编码,都存在训练数据长度有限,但是推理时长度要求更高的场景(现在很多大模型都支持上百 k 的输入长度)。

那么如何提升模型处理长文本的能力那,直接想法有两种:

线性内插:压缩输入长度到模型原本训练的长度,训练数据最大长度 l,需要支持的推理最大长度 L,那么需要将 L 压缩到 l 的长度。

具体做法是:

图片

线性外推:线性外推(直接外推)就是不做任何处理,直接扩充l到L的长度。

两者的缺点:

  • 线性内插:会将不同视野大小下统一缩减,导致相邻位置的差异变小,损失视野分辨率。尤其是原先就在训练范围内的相邻位置(高频),因此需要重新训练。

  • 线性外推:因为训练时没有处理过较长文本,所以效果比较差,不如线性内插。

神经正切核(NTK)方式(NTK-aware RoPE)

这里要引入位置编码进制转换,主要参考的是苏剑林:高频外推,低频内插的思想。

位置编码进制转换就是用 2/8/10/16 进制数来表示位置坐标,即同样表示 2048,直接表示是 2048,用 10 进制表示是 [2,0,4,8]。

这样做的好处是:既缩小了数字的跨度,又没有缩小相邻数字的差距(因为直接表示,0 和 2048 跨度太大,梯度优化器不容易优化,直接压缩相邻差距又会变小)。

NTK 的作用是基于 NTK 理论,证明了神经网络无法直接学习高频信号(低维信息),所以高频信号要采用外推的方式,低频信号内插。

NTK-Rope

图片

NTK-Alibi

ntk-alibi 是将 alibi 的 m 缩放成:

图片

动态 NTK(Dynamic NTK)

Dynamic NTK 主要提出了动态的缩放因子。对于自回归模型而言,其所处理的序列是不断变化的,每一步比前一步增加一个 token。

在这种情况下有两种插值的方法:

  • 在整个推理阶段,使用固定的缩放因子 a=L'/L, 其中 L' 是预测阶段文本长度

  • 在每轮前向传播中,位置编码更新缩放因子 a = max(1, l'/L), 其中 l' 是当前序列的长度

方案 1 的问题在于,当序列长度小于 L' 时,模型性能可能会有一点小折扣;而当序列长度大于 L' 时,模型性能则会突然下降,而使用方案 2 可以使模型性能下降的更少。

在推理阶段不断调整缩放因子的方式称为 Dynamic 缩放,即动态 NTK 插值。

(4)YaRN

NTK-by-parts

图片

图片

图片

图片

温度系数

YaRN 除了上面说的 NTK-by-parts,还引入了一个在计算 softmax 时的温度系数。

图片

(5)CoPE

上下文位置编码(Contextual Positional Encoding,简称 CoPE)。

对于传统位置的位置编码(可学习或不可学习)和相对位置编码(Rope、ALIBI)是独立于上下文编码的,而且相对位置编码在处理长文本时,位置注意力是个递减的趋势。

因此 Cope 提出了根据上下文的位置编码:

图片

Cope 主要的思想是引入一个“门控”机制来控制当前 token 是否参与注意力计算,门控值是根据公式:

图片

图片

04

总结

位置编码是 transformer 中非常重要的组成部分,随着 LLM 的发展,也要求位置编码有更好的泛化性,适配更多的不同长度的处理场景,随机产生了 NTK/YaRN 等编码,Cope 是考虑了上下文,把 token 信息也考虑到位置编码中。总之,位置编码还在不断改进,值得持续关注和研究。

链接:https://zhuanlan.zhihu.com/p/720755157

标签:编码,Transformer,torch,self,位置,NTK,xq
From: https://blog.csdn.net/sinat_37574187/article/details/144027704

相关文章