参考书《大语言模型》赵鑫 李军毅 周昆 唐天一 文继荣 著

一、初识大模型

1.1 语言模型的发展历程

语言模型:通常指能够建模自然语言文本生成概率的模型。

语言模型的发展历程:统计语言模型->神经语言模型->预训练模型->大语言模型

1.2 大模型技术基础

大语言模型:通常是指具有超大规模参数的预训练语言模型,主要以transformer解码器架构。训练阶段包括预训练(base model)和后训练(instruct model)。

预训练是使用与下游任务无关的大规模数据进行模型参数的初始训练。数据的数量和质量都很关键,基于transformer解码器架构对下一个词进行预测。

为了预训练大语言模型,需要准备大规模的文本数据,并且进行严格的清洗, 去除掉可能包含有毒有害的内容,最后将清洗后的数据进行词元化(Tokenization) 流,并且切分成批次(Batch),用于大语言模型的预训练。

后训练是使用输入与输出配对的指令数据对模型进行微调,提升模型通过问答模式进行任务求解的能力。

扩展定律:通过扩展参数规模、数据规模和计算算力,大语言模型的能力会出现显著提升。但随着模型参数、数据数量的扩展,模型性能增益将逐渐减小。


涌现能力:模型扩展到一定规模时,特定任务性能突然出现显著跃升趋势,高于随机水平。

三种典型的涌现能力:

上下文学习(In-context Learning, ICL). 具体方式为,在提示中为语言模型提供自然语言指令和多个任务示例(Demonstration),无需显式的训练或梯度更新,仅输入文本的单词序列就能为测试样本生成预期的输出。

指令遵循(Instruction Following). 是指大语言模型能够按照自然语言指令来执行对应的任务。为了获得这一能力,通常需要使用自然语言描述的多任务示例数据集进行微调,称为指令微调(Instruction Tuning)或 监督微调(Supervised Fine-tuning)。通过指令微调,大语言模型可以在没有使用显式示例的情况下按照任务指令完成新任务,有效提升了模型的泛化能力。相比于上下文学习能力,指令遵循能力整体上更容易获得,但是最终的任务执行效果还取决于模型性能和任务难度决定。

逐步推理(Step-by-step Reasoning). 大语言模型可以在提示中引入任务相关的中间推理步骤来加强复杂任务的求解,从而获得更为可靠的答案。


扩展定律和涌现能力是两种描述规模效应的度量方法。

1.3 GPT和DeepSeek模型介绍

看视频简单了解

《大语言模型》1.3 GPT+DeepSeek模型介绍_哔哩哔哩_bilibili

GTP系列模型发展过程:

DeepSeek系列模型发展过程:

五、模型架构

5.1 Transformer模型介绍

Transformer 是由多层的多头自注意力(Multi-head Self-attention)模块堆叠而成的神经网络模型。Transformer 模型的基本组成,包括基础的输入、多头自注意力模块和前置网络层;以及编码器和解码器模块。

5.1.1 输入编码

在 Transformer 模型中,为了捕获词汇本身的语义信息,输入的词元序列(\textbf{u}=[u_1,u_2,...,u_T])中的每个词元在输入嵌入模块中被映射成为一个可学习的、具有固定维度的词向量 v_t\in\mathbb{R} ^H。由于 Transformer 的编码器结构本身无法识别序列中元素的顺序,所以引入位置编码(Position Embedding, PE)表示序列中的位置信息。

给定一个词元u_t,位置编码根据其在输入中的绝对位置分配一个固定长度的嵌入向量\textbf{p}_t\in \mathbb{R}^H。然后,每个词元对应的词向量和位置向量将直接相加,生成了最终的输入嵌入序列\textbf{X}=[x_1,x_2,...,x_T],并且被传入到后续层中:

\textbf{x}_t=\textbf{v}_t+\textbf{p}_t

由于不同词元的位置编码仅由其位置唯一决定,因此这种位置建模 方式被称为绝对位置编码。

不足:尽管绝对位置编码能够一定程度上建模位置信息,但它只能局限于建模训练样本中出现的位置,无法建模训练数据中未出现过的位置,因此极大地限制了它们处理长文本的能力。这个问题会在后续解决。

5.1.2 多头自注意力机制(MHA)

多头自注意力机(Multi-Head Attention)制通常由多个自注意力模块组成。在每个自注意力模块中,对于输入的词元序列,将其映射为相应的查询(Query, \textbf{Q})、键(Key, \textbf{K})和值(Value, \textbf{V})三个矩阵。然后,对于每个查询,将和所有没有被掩盖的键之间计算点积。这些点积值进一步除以\sqrt{D} 进行缩放(D是键对应的向量维度),被传入到 softmax 函数中用于权重的计算。进一步,这些权重将作用于与键相关联的值,通过加权和的形式计算得到最终的输出。在数学上,上述过程可以表示为:

其中,\textbf{X}是处理后的词元序列,\textbf{W}^Q\textbf{W}^K\textbf{W}^V是权重矩阵(可学习参数,可能是可以通过训练得到?具体的的我还不清楚。疑问点1)看着下面这个图(来自深入剖析Transformer架构中的多头注意力机制-阿里云开发者社区)可能更容易理解原理。

输入序列首先通过不同的权重矩阵被映射为一组查询、键和值。每组查询、键和值的映射构成一个“头”,并独立地计算自注意 力的输出。最后,不同头的输出被拼接在一起,并通过一个权重矩阵 \textbf{W}^O\in\mathbb{R}^{H\times H} 进行映射,产生最终的输出。如下面的公式所示:

优点:

  • 自注意力机制能够直接建模序列中任意两个位置之间的关 系,进而有效捕获长程依赖关系,具有更强的序列建模能力。

  • 自注意力的计算过程对于基于硬件的并行优化(如 GPU、TPU 等)非常友好, 因此能够支持大规模参数的高效优化。

5.1.3 前馈网络层(FNN)

Transformer 模型引入了一个前馈网络层 (Feed Forward Netwok, FFN)是为了学习复杂的函数关系和特征,对于每个位置的隐藏状态进行非线性变换和特征提取。具体来说,给定输入 \textbf{x},Transformer 中的前馈神经网络由两个线性变换和 一个非线性激活函数组成:

其中 \textbf{W}^U\in\mathbb{R}^{H\times H'}\textbf{W}^D\in\mathbb{R}^{H'\times H} 分别是第一层和第二层的线性变换权重矩阵, \textbf{b}_1\in\mathbb{R}^{H'}\textbf{b}_2\in\mathbb{R}^{H}是偏置项,\sigma是激活函数(在原始的 Transformer 中,采用 ReLU 作为激活函数)。前馈网络层通过激活函数引入了非线性映射变换,提升了模型的表达能力,从而更好地捕获复杂的交互关系。

疑问点2:H是个实数还是向量/矩阵?H'是什么意思?

5.1.4 编码器

Transformer 模型中,编码器(Encoder)(图 5.1左)的作用是将每个输入词元都编码成一个上下文语义相关的表示向量。编码器结构由多个相同的层堆叠而成,其中每一层都包含多头自注意力模块和前馈网络模块。

在注意力和前馈网络后,模型使用层归一化(LayerNorm)和残差连接 (Residual Connection)来加强模型的训练稳定度。其中,残差连接将输入与该层的输出相加,实现了信息在不同层的跳跃传递,从而缓解梯度爆炸和消失的问题。而层归一化 则对数据进行重新放缩,提升模型的训练稳定性(详细介绍可见第 5.2.1 归一化方法

编码器接受经过位置编码层的词嵌入序列 \textbf{X} 作为输入,通过多个堆叠的编码器层来建模上下文信息,进而对于整个输入序列进行编码表示。由于输入数据是完全可见的,编码器中的自注意力模块通常采用双向注意力,每个位置的词元表示能够有效融合上下文的语义关系。 在编码器-解码器架构中,编码器的输出将作为解码器(Decoder)的输入,进行后续计算。形式化来说,第 l 层(l ∈ {1, . . . , L})的编码器的数据处理过程如下所示:

其中,\textbf{X}_{l-1}\textbf{X}_{l} 分别是该 Transformer 层的输入和输出,\textbf{X}'_{l}是该层中输入经过多 头注意力模块后的中间表示,LayerNorm 表示层归一化。

5.1.5 解码器 

Transformer 架构中的解码器(图 5.1 右)基于来自编码器编码后的最后一层的输出表示以及已经由模型生成的词元序列,执行后续的序列生成任务。

与编码器不同,解码器需要引入掩码自注意力(Masked Self-attention)模块,用来在计算注意力分数的时候掩盖当前位置之后的词,以保证生成目标序列时不依赖于未来的信息。除了建模目标序列的内部关系,解码器还引入了与编码器相关联的多头注意力层,从而关注编码器输出的上下文信息 \textbf{X}_{L}

同编码器类似,在每个模块之 后,Transformer 解码器也采用了层归一化和残差连接。在经过解码器之后,模型会通过一个全连接层将输出映射到大小为 V 的目标词汇表的概率分布,并基于某种解码策略生成对应的词元。在训练过程中,解码器可以通过一次前向传播,让每个词元的输出用于预测下一个词元。而在解码过程,解码器需要经过一个逐步的生成过程,将自回归地生成完整的目标序列(具体可以参考第 9 章)。解码器的数据流程如下所示:

 其中,\textbf{Y}_{l-1}\textbf{Y}_{l} 分别是该 Transformer 层的输入和输出,\textbf{Y}'_{l}\textbf{Y}''_{l} 是该层中输入经过掩码多头注意力 MaskedMHA 和交叉多头注意力 CrossMHA 模块后的中间表 示,LayerNorm 表示层归一化。然后将最后一层的输入 \textbf{Y}_L映射到词表的维度上:

其中,\textbf{O}\in\mathbb{R}^{H\times V} 是模型最终的输出,代表下一个词在词表上的概率分布\textbf{W}^L\in\mathbb{R}^{H\times V} 是将输入表示映射到词汇表维度的参数矩阵,而\textbf{W}^L\textbf{Y}_L是概率化前的中间值,通常被称为 logits。

5.2 详细配置

5.2.1 归一化方法

使用归一化方法是为了加强神经网络训练过程的稳定性。

主要有三种方法:LayerNorm、RMSNorm、DeepNorm。

LayerNorm:计算每一层中所有激活值的均值 \mu 和方差 \sigma,从而重新调整激活值的中心和缩放比例:

缺点:该方法难以处理可变长度的序列数据和小批次数据。

RMSNorm:为了提高层归一化的训练速度,RMSNorm仅利用激活值总和的均方根 RMS(\textbf{x}) 对激活值进行重新缩放。

DeepNorm:DeepNorm 在 LayerNorm 的基础上,在残差连接中对 之前的激活值 \textbf{x} 按照一定比例 \alpha 进行放缩。

其中,Sublayer 表示Transformer 层中的前馈神经网络或自注意力模块。

5.2.2 归一化模块位置

归一化模块的位置:层后归一化(Post-Layer Normalization, Post-Norm)、层前归一化(Pre-Layer Normalization, Pre-Norm)和夹心归一化(Sandwich-Layer Normalization, Sandwich-Norm)。

Post-Norm:归一化模块被放置于残差计算之后。

其中,Norm 表示任意一种归一化方法。

优点:1.有助于加快神经网络的训练收敛速度,使模型可以更有效地传播梯度,从而减少训练时间。2.后向归一化可以降低神经网络对于超参数(如学习率、初始 化参数等)的敏感性,使得网络更容易调优,并减少了超参数调整的难度。

不足:由于在输出层附近存在梯度较大的问题,采用 Post-Norm 的 Transformer 模型在训练过程中通常会出现不稳定的现象。

Pre-Norm:将归一化模块应用在每个子层之前。

其中,Norm 表示任意一种归一化方法。此外,Pre-Norm 在最后一个 Transformer 层后还额外添加了一个 LayerNorm。

优点:稳定,可以防止模型的梯度爆炸或者梯度消失现象。

不足:性能逊色于采用了 Post-Norm 的模型。

Sandwich-Norm:在 Pre-Norm 的基础上,Sandwich-Norm 在残差连接之前增加了额外的 LayerNorm,旨在避免 Transformer 层的输出出现数值爆炸的情况。 具体的实现方式如下所示:

优点:Sandwich-Norm 可以看作是 Pre-Norm 和 Post-Norm 两种方法的组合,理论上具有更加灵活的表达能力。

不足:有时无法保证大语言模型的稳定训练,甚至会引发训练崩溃的问题 。

5.2.3 激活函数

激活函数主要是为神经网络中引入非线性变化,从而提升神经网络的模型能力。

ReLU激活函数:该激活函数计算较为简单,仅仅是将对输入中每个神经元和“零值”进行比较,并将小于零的神经元的值设置为 0。

Swish 激活函数:将神经元和该神经元的 sigmoid 激活的乘积作为新的激活函数。

其中,sigmoid函数也叫Logistic函数,用于隐层神经元输出,取值范围为(0,1),它可以将一个实数映射到(0,1)的区间,可以用来做二分类。在特征相差比较复杂或是相差不是特别大时效果比较好。Sigmoid函数为神经网络中的激励函数,是一种光滑且严格单调的饱和函数,其表达式为:

Sig(x)=(1+e^{-x})^{-1}

GELU激活函数:利用标准高斯累积分布函数作为激活函数。

GLU激活函数 :它的变种有 SwiGLU 和 GeGLU等。GLU 激活函数引入了两个不同的线性层。其中一个线性层的输出将被输入 到一个激活函数(例如,GeGLU 采用 GELU 激活函数)中,其结果将和另一个线 性层的输出进行逐元素相乘作为最终的输出。SwiGLU 和 GeGLU 激活函数 的计算公式如下所示:

5.2.4 位置编码

因为仅使用注意力机制无法捕捉序列中的顺序关系,所以需要引入位置编码对于序列信息进行精确建模,从而将绝对或相对位置信息整合到模型中。结合视频更好理解 【11】Sinusoidal、RoPE、ALiBi等各类位置信息编码】

  • 绝对位置编码

在编码器和解码器的输入端,根据输入的词元在序列中的绝对位置生成唯一的位置嵌入,并与词元的嵌入表示进行相加来注入位置信息。绝对位置编码的公式如下所示:

其中,\textbf{p}_t表示位置t的位置嵌入, \textbf{v}_t是该位置词元对应的词向量。

原始的Transformer 采用了正余弦位置编码。该位置编码在不同维度上预先定义了特定的正弦或余弦函数,通过将词元的绝对位置作为输入代入这些函数,从而为这些维度赋予相应的值。对于维度大小为𝐻的位置嵌入,其第𝑖∈{1,...,𝐻}维的值按照如下方法进行设置:

  • 相对位置编码

相对位置编码是根据键和查询之间的偏移量计算得来的。计算得到的相对位置编码通常应用于注意力矩阵的计算中, 而不是直接与词元本身的位置编码进行相加。

目前还没有完全理解)Transformer-XL提出的一种编码方法:对于使用绝对位置编码的模型,其注意力值可以进行进一步的分解:

Transformer-XL 对上述注意力值进行了改写,使用相对位置信息代替绝对位置 信息。其公式表示如下所示(这里使用不同颜色与原始项进行对应):

 其中, \textbf{x}_i是每个词元对应的词向量(对应没有显式加入位置编码的词向量\textbf{v}_i),而 \textbf{r}_{i-j} 表示相对位置编码,𝒖和𝒗是两个可学习的表示全局信息的参数。相比于绝对 位置编码,注意力值的第二项中和第四项键对应的绝对位置编码\textbf{W}^{k\top }\textbf{p}_{j}被替换为相对位置编码\textbf{r}_{j},以引入相对位置信息;而第三和第四项中则使用全局参数𝒖和 𝒗 替换查询对应的绝对位置编码\textbf{p}_{i}\textbf{W}^{Q},用于衡量键的语义信息和相对位置信息本身的重要程度。

T5提出的一种编码方法:它在注意力分数中引入了可学习的标量,这些标量是基于查询和键的位 置之间的距离计算的。T5 相对位置编码的计算可以表达为:

 其中\textbf{r}_{i-j} 表示基于查询和键之间偏移的可学习标量。

  • 旋转位置编码(RoPE)

位置索引为𝑡对应的旋转矩阵定义如下所示:

利用旋转矩阵中三角函数的特性,位置索引为𝑖的旋转矩阵和位置索引为𝑗的旋转矩阵的转置的乘积等同于位置索引为它们相对距离𝑖−𝑗的旋转矩阵,即\mathbf{R}_{\theta ,i}\mathbf{R}_{\theta ,j}^{\top}=\mathbf{R}_{\theta ,i-j}。根据旋转矩阵的定义,RoPE在处理查询和键向量的时候,将每对连续出现的两个元素视为一个子空间 。对于一个长度为𝐻的向量来说,将会形成𝐻/2个这样的子空间。在这些子空间中,每一个子空间i\in{\{1,...,H/2\}}所对应的两个元素都会根据一个特定的旋转角度t\cdot \theta_i进行旋转,其中𝑡代表位置索引,而\theta_i表示该子空间中的基。其中将基\theta_i定义为底数𝑏(默认值是 10000)的指数:

通过这种方式,键和查询之间的注意力分数能够有效融入相对位置信息。 注意力矩阵的公式可以进一步变为如下形式:

RoPE具有良好的性能以及长期衰减的特性。

这个视频解释了为什么在代码中那样定义rotate_half(x)函数以及q_embed、k_embed:【通俗易懂-大模型的关键技术之一:旋转位置编码rope (3)】

RoPE实现代码:

def rotate_half(x):
  x1 =x[...,:x.shape[-1] //2]
  x2 =x[...,x.shape[-1] //2 :]
  #将向量每两个元素视为一个子空间
  return torch.cat((-x2,x1),dim=-1)
 
def apply_rotary_pos_emb(q,k,cos,sin,position_ids):
  cos = cos[position_ids].unsqueeze(1)
  sin = sin[position_ids].unsqueeze(1)
  #获得各个子空间旋转的正余弦值
  q_embed = (q* cos)+ (rotate_half(q) *sin)
  k_embed = (k* cos)+ (rotate_half(k) *sin)
  #将每个子空间按照特定角度进行旋转
  return q_embed,k_embed
  • ALiBi位置编码

ALiBi通过在键和查询之间的距离上施加相对距离相关的惩罚来调整注意力分数。其计算公式如下:

 其中,𝑖−𝑗是查询和键之间的位置偏移量,𝑚是每个注意力头独有的惩罚系数。

ALiBi中的惩罚分数是预先设定的,不需要引入任何可训练的参数。下面给出Transformers库中BLOOM 中的ALiBi代码实现:

def build_alibi_tensor(attention_mask:torch.Tensor,num_heads: int,dtype:
    torch.dtype)->torch.Tensor:
    batch_size,seq_length = attention_mask.shape
    closest_power_of_2 = 2 **math.floor(math.log2(num_heads))
    base = torch.tensor(
        2 ** (-(2**-(math.log2(closest_power_of_2)-3))),
            device=attention_mask.device,dtype=torch.float32
    )
  powers = torch.arange(1,1 + closest_power_of_2,
      device=attention_mask.device,dtype=torch.int32) 
  slopes = torch.pow(base,powers)
  #计算各个头的惩罚系数

  if closest_power_of_2!=num_heads:
      #如果头数不是2的幂次方,修改惩罚系数
      extra_base =torch.tensor(
          2 **(-(2 **-(math.log2(2 *closest_power_of_2)-3))),
             device=attention_mask.device,dtype=torch.float32 
      )
      num_remaining_heads = min(closest_power_of_2,num_heads-closest_power_of_2) 
      extra_powers = torch.arange(1, 1+ 2 * num_remaining_heads,2,
           device=attention_mask.device,dtype=torch.int32) 

       slopes = torch.cat([slopes,torch.pow(extra_base,extra_powers)],
            dim=0)
 
  arange_tensor = ((attention_mask.cumsum(dim=-1)-1) * attention_mask)[:,None,:] 
  #计算相对距离
  alibi = slopes[...,None] * arange_tensor
  #计算ALiBi施加的注意力偏置
  return alibi.reshape(batch_size *num_heads, 1,seq_length).to(dtype)

5.2.5 注意力机制 

常见的四种注意力机制:

完整自注意力机制:每个词元在注意力计算中都需要对于其前序的所有词元的键值对予以访问,对于序列长度为𝑇的序列需要O(T^2)的计算复杂度。

稀疏注意力机制:滑动窗口注意力机制(SlidingWindowAttention,SWA)是大语言模型中使用最多的一种稀疏注意力机制。不同于完整的注意力机制,滑动窗口注意力根据词元位置,仅仅将位置索引上距离该词元一定范围内的词元考虑到注意力的计算中。

具体来说,滑动窗口注意力设置了一个大小为𝑤的窗口,对每个词元u_t,只对窗口内的词元[u_{t-w+1},...,u_t]进行注意力计算,从而将复杂度降低到O(wT)。关于滑动窗口注意力详细机制如图5.3展示。

多查询/分组查询注意力:多查询注意力(Multi QueryAttention,MQA)提出针对不同的头共享相同的键和值变换矩阵。分组查询注意力机制(Grouped-QueryAttention,GQA)将全部的头划分为若干组,并且针对同一组内的头共享相同的变换矩阵。

硬件优化的注意力机制:FlashAttention通过矩阵分块计算以及减少内存读写次数的方式,提高注意力分数的计算效率;PagedAttention则针对增量解码阶段,对于KV缓存进行分块存储,并优化了计算方式,增大了并行计算度,从而提高了计算效率。对于这些技术的细节将在第9.2.2节进行介绍。

5.2.6 混合专家模型

在大语言模型中引入了基于稀疏激活的混合专家架构(Mixture-of-Experts,MoE),旨在不显著提升计算成本的同时实现对于模型参数的拓展。

在混合专家架构中,每个混合专家层包含𝐾个专家组件,记为[E_1,E_2,...,E_k], 其中每个专家组件E_i都是一个前馈神经网络。对于输入的每个词元表示x_t,模型通过一个路由网络(或称为门控函数)𝐺来计算该词元对应于各个专家的权重。 在路由函数中,首先通过线性层W^G\in\mathbb{R}^{H\times K} 映射为𝐾个专家的得分,并基于此选择出概率最高的𝑘个专家进行激活。随后,这𝑘个专家的得分将被送入softmax 函数计算出它们的权重G(x_t)=[G(x_t)_1,...,G(x_t)_k],没有被选择的专家权重将 被置为0。上述路由网络的计算过程如下式所示:

之后,每个被选择的词元的输出的加权和将作为该混合专家网络层的最终输出\mathbf{o}_t: 

 下面给出了Mixtral混合专家层的一个PyTorch示例代码:

class MoeLayer(nn.Module):
  def __init__(self,experts:List[nn.Module],gate:nn.Module,
      num_experts_per_tok:int):
      super().__init__()
      assertlen(experts) > 0
      self.experts = nn.ModuleList(experts)#所有专家的列表
      self.gate = gate #路由网络
      self.num_experts_per_tok = num_experts_per_tok#每个词元选择的专家数目
 
  def forward(self,inputs:torch.Tensor):
      gate_logits = self.gate(inputs)
      weights,selected_experts = torch.topk(gate_logits,self.num_experts_per_tok)
      #使用路由网络选择出top-k个专家
      weights = F.softmax(weights,dim=1,dtype=torch.float).to(inputs.dtype) 
      #计算出选择的专家的权重
      results = torch.zeros_like(inputs)
      for i,expert inenumerate(self.experts):
          batch_idx,nth_expert = torch.where(selected_experts==i)
          results[batch_idx] += weights[batch_idx,nth_expert,None] *
                expert( inputs[batch_idx])
      #将每个专家的输出加权相加作为最终的输出
      return results

5.2.7 LLaMA的详细配置

综合本节讨论的内容,下面给出了关于模型详细配置的推荐建议。

首先,为了增强模型的训练稳定性,建议采用前置的RMSNorm作为层归一化方法。其次,在选择激活函数时,为了获得更优的模型性能,可以优先考虑使用SwiGLUGeGLU。最后,对于位置编码,可以优先选择RoPE或者ALiBi,这两种位置编码方法在建模长序列数据时通常能够具有较好的性能。

接下来,我们以LLaMA模型的代码实现,来介绍Transformer解码器模型是如何进行模型搭建并且实现前向计算的过程。 对于一个LLaMA模型,其首先将输入的词元序列通过词嵌入矩阵转化为词向量序列。之后,词向量序列作为隐状态因此通过𝐿个解码器层,并在最后使用RMSNorm进行归一化。归一化后的最后一层隐状态将作为输出。

LLaMA在Transformers库中的整体实现如下所示:

class LlamaModel(LlamaPreTrainedModel):
    def __init__(self,config:LlamaConfig):
        super().__init__(config)
        self.vocab_size = config.vocab_size
        #LLaMA的词表大小
        self.embed_tokens = nn.Embedding(config.vocab_size,
             config.hidden_size,self.padding_idx) 
         #LLaMA的词嵌入矩阵,将输入的id序列转化为词向量序列
        self.layers = nn.ModuleList(
             [LlamaDecoderLayer(config,layer_idx) for layer_idx in
                 range(config.num_hidden_layers)] 
        )
        #所有的Transformer解码器层
        self.norm = LlamaRMSNorm(config.hidden_size, eps = config.rms_norm_eps) 
        causal_mask = torch.full(
               (config.max_position_embeddings,config.max_position_embeddings),
                fill_value = True,
                dtype = torch.bool
        )
 
    @add_start_docstrings_to_model_forward(Llama_INPUTS_DOCSTRING)
    def forward(
        self,
        input_ids:torch.LongTensor = None,
        attention_mask:Optional[torch.Tensor] = None,
        position_ids:Optional[torch.LongTensor] = None,
        **kwargs
 
     )-> Union[Tuple,BaseModelOutputWithPast]:
        if inputs_embeds is None:
            inputs_embeds = self.embed_tokens(input_ids)
            #将输入的inputid序列转化为词向量序列
        causal_mask = self._update_causal_mask(attention_mask,inputs_embeds) 
        #创建单向注意力的注意力掩盖矩阵

        hidden_states = inputs_embeds
 
        for decoder_layer in self.layers:
           hidden_states = decoder_layer(
              hidden_states,
              attention_mask = causal_mask,
              position_ids = position_ids
           )[0]
           #用每个LLaMA解码器层对词元的隐状态进行映射
         hidden_states = self.norm(hidden_states)
         #对每个词元的隐状态使用RMSNorm归一化
         return BaseModelOutputWithPast(last_hidden_state=hidden_states)

在每个解码器层中,隐状态首先通过层前的RMSNorm归一化并被送入注意力模块。注意力模块的输出将和归一化前的隐状态做残差连接。之后,新的隐状态进行RMSNorm归一化,并送入前馈网络层。和上面一样,前馈网络层的输出同样做残差连接,并作为解码器层的输出。

原始输入 → [归一化] → 注意力加工 → [融合1] → [归一化] → 思维加工 → [融合2] → 最终输出
                                ↑残差连接↑                     ↑残差连接↑

Transformers库中LLaMA每一层的代码实现如下所示:

class LlamaDecoderLayer(nn.Module):
    def __init__(self, config: LlamaConfig, layer_idx: int):
        super().__init__()
 
        self.hidden_size = config.hidden_size
        self.self_attn = LlamaAttention(config=config, layer_idx = layer_idx)
        #注意力层
        self.mlp = LlamaMLP(config)#前馈网络层

        self.input_layernorm = LlamaRMSNorm(config.hidden_size, eps=config.rms_norm_eps) 
        self.post_attention_layernorm = LlamaRMSNorm(config.hidden_size,
             eps=config.rms_norm_eps) 
        #注意力层和前馈网络层前的RMSNorm
 
     def forward(
         self,
         hidden_states:torch.Tensor,
         attention_mask:Optional[torch.Tensor] = None,
         position_ids:Optional[torch.LongTensor] = None,
         **kwargs
     )->Tuple[torch.FloatTensor,Optional[Tuple[torch.FloatTensor,
         torch.FloatTensor]]]: 

         residual = hidden_states
 
         hidden_states=self.input_layernorm(hidden_states)
         #注意力层前使用RMSNorm进行归一化
         hidden_states,self_attn_weights,present_key_value =
             self.self_attn( 
             hidden_states = hidden_states,
             attention_mask = attention_mask,
             position_ids = position_ids,
             **kwargs
          )
          #进行注意力模块的计算
          hidden_states = residual + hidden_states
          #残差连接

          residual = hidden_states
          hidden_states = self.post_attention_layernorm(hidden_states)
          #前馈网络层前使用RMSNorm进行归一化
          hidden_states = self.mlp(hidden_states)
          #进行前馈网络层的计算
          hidden_states = residual + hidden_states
          #残差连接
          outputs = (hidden_states)
          return outputs

5.3 主流架构

目前大语言模型的主流架构是:编码器-解码器(Encoder-decoder)架构、因果解码器(Causal Decoder)架构和前缀解码器(PrefixDecoder)架构。

下图对三种架构进行了对比(但是我也没看太明白这个图说明了什么)

5.3.1 编码器-解码器架构

编码器-解码器架构是自然语言处理领域里一种经典的模型结构,广泛应用于如机器翻译等多项任务。原始的Transformer模型也使用了这一架构,组合了两个分别担任编码器和解码器的Transformer模块(详细阐述参见第5.1节)。如图5.6 所示,此架构在编码器端采用了双向自注意力机制对输入信息进行编码处理,而在 解码器端则使用了交叉注意力与掩码自注意力机制,进而通过自回归的方式对输出进行生成。

5.3.2 因果解码器架构

当前绝大部分主流的大语言模型采用了因果解码器架构。因果解码器采用了Transformer 中的解码器组件,同时做出了几点重要改动。首先,因果解码器没有显式地区分输入和输出部分。如图5.6所示,该架构采用了单向的掩码注意力机制,使得每个输入的词元只关注序列中位于它前面的词元和它本身,进而自回归地预测输出的词元。此外,由于不含有编码器部分,因果解码器删除了关注编码器表示的交叉注意力模块。经过自注意力模块后的词元表示将直接送入到前馈神经网络中。

5.3.3 前缀解码器架构

前缀解码器架构也被称为非因果解码器架构,对于因果解码器的掩码机制进行了修改。该架构和因果解码器一样,仅仅使用了解码器组件。与之不同的是,该架构参考了编码器-解码器的设计,对于输入和输出部分进行了特定处理。如图5.6 所示,前缀解码器对于输入(前缀)部分使用双向注意力进行编码,而对于输出部分利用单向的掩码注意力利用该词元本身和前面的词元进行自回归地预测。与编码器-解码器不同的是,前缀解码器在编码和解码过程中是共享参数的,并没有划分为独立的解码器和编码器。对于前缀解码器,也可以由现有的因果解码器继续预训练转换而来,进而加速该模型的训练。

5.4 长上下文模型

在实际应用中,大语言模型对于长文本数据的处理需求日益凸显,尤其在长文档分析、多轮对话、故事创作等场景下。在这些情况下,模型需要处理的文本的长度常常超出预定义上下文窗口大小。目前,增强大语言模型长文本建模能力的研究主要集中在两个方向:一是扩展位置编码(详见第5.4.1节),二是调整上下文窗口(详见第5.4.2节)。除了探讨拓展上下文窗口的方法外,本部分将在最后探讨训练长上下文模型所需的长文本数据(详见第5.4.3节)。

5.4.1 扩展位置编码

某些特定的位置编码在超出原始上下文窗口的文本上,也能够表现 出较好的建模能力,这种能力通常被称为外推(Extrapolation)能力。

目前比较主流的位置编码方法RoPE在未经特殊修改的情况下并不具备良好的外推能力。很多研究工作在RoPE的基础上进行了重要改进,接下来将为这些改进方法给出一个统 一的形式化定义:

对于一个原始上下文窗口为T_{max}的模型,目标是将其上下文窗口扩展到T'_{max}(其中T'_{max} > T_{max})。在RoPE的每个子空间𝑖 上,对于相对位置𝑡,旋转角度 f(t,i)=t\cdot \theta_i的修改可以分解为对距离𝑡 的修改𝑔(𝑡)和对基\theta_i 的修改ℎ(𝑖)。因此,新的旋转角度可以表示为如下形式:

直接微调

一种直接的策略是使用相应的长文本数据对于模型进行微调。在这种情况下,模型可以直接根据相对位置计算出对应的位置编码,而无需对RoPE本身进行任何修改。旋转角度的计算方式依旧和之前相同:

f(t,i)=t\cdot \theta_i

在更长的文本上进行训练会导致出现比原始上下文窗口内更大的最大旋转 角度T'_{max}\cdot\theta_i。在模型进行微调前,这些超出原始窗口的位置对应的注意力值会远大于窗口内的值。因此这种方式通常 会导致收敛缓慢,并需要大量数据进行继续预训练。

位置索引修改

通过修改位置索引𝑔(𝑡)来调整所有子空间的旋转角度,从 而保证其不超过原始上下文窗口所允许的最大值。具体来说,位置索引的修改可 采用以下两种方法:

  • 位置内插

该策略将所有位置索引乘以一个小于1的系数T_{max}/T'_{max}(其中T_{max}< T'_{max})对位置索引进行特定比例的缩放,T_{max}T'_{max} 分别表示原始上下文窗口和拓展后的上下文窗口的长度。通过进行这样的缩放,新的旋转角度 计算公式变为:

通常来说,使用位置内插方法进行微调的训练代价较小。 然而在处理较短的文本时,由于位置索引的缩放,可能会对模型的性 能产生一定的负面影响。

  • 位置截断

位置截断针对不同距离采用了不同的处理方式。该方法依据语言建模的局部性原理,对模型中近距离敏感的位置索引进行保留,同时截断或插值处理远距离的位置索引,确保其不超出预设的最大旋转角度。

采用位置截断的ReRoPE和LeakyReRoPE方法,首先设定一个不大于原始上下文窗口长度的窗口大小𝑤(𝑤 ≤T_{max})。在此窗口范围内的部分,仍使 用原始相对位置索引;对于超出此窗口的部分,位置索引则会被截断至窗口大小,即𝑔(𝑡) = 𝑤;或通过线性插值方式,将目标上下文窗口长度的位置索引映射回原始上下文窗口长度,即g(t)=w+\frac{T_{max}-w}{T'_{max}-w}\cdot(t-w)。上述位置截断方法可通过以下公式表达:

通过这种方法对RoPE进行修改后,模型能够直接应用于更长的上下文而无需重新训练,并且依然保持对短文本的建模能力。然而,这种方法需要对注意力矩阵进行二次计算,进而增加了额外的计算开销。

基修改

根据第5.2.4节中对RoPE的介绍,每个子空间𝑖都有一个对应的波长\lambda _i=\frac{2\pi}{\theta_i},表示在该子空间上旋转一周所需要的距离。

某些子空间的波长可能会超过上 下文窗口的长度(\lambda_i>T_{max}),导致模型在这些子空间上无法对完整的旋转周期进行训练。这些子空间通常被称为关键子空间

如果想要调整这些子空间的旋转角度分布,另一种 方法是针对这些子空间的基ℎ(𝑖)进行缩放:

对基的修改可以通过对基的底数修改以及对基的截断实现,下面介绍这些修改方法。

  • 底数调整

依据公式\theta_i=b^{-2(i-1)/H},通过调整底数可以改变旋转的角度。具体来说,按照一定比例增大底数可以对基进行缩小,从而缩小旋转的角度,使得模型在不经过额外训练的情况下能够处理更长的上下文窗口。在这种情况下,每个子空间的旋转角度由下式给出:

其中,𝛼是一个大于等于放缩比例的数,通过对底数进行增大,实现缩小基来处理更长文本的能力。在实践中,不同方法通常会采用不同的𝛼值。 如果要进一步提升模型的长文本建模能力,还可以在长文本数据上进行微调。此时,使用较大的底数(例如,𝑏=108)通常能够获得更好的性能。

  • 基截断

与底数调整相似,基截断方法通过修改关键子空间来避免产生过大的旋转角度。

这种方法首先设定两个阈值𝑎和𝑐。根据每个子空间上的基\theta_i 与这两个阈值的比较结果,可以选择相应的调整策略来对基进行调整:当\theta_i ≥ 𝑐 时,基的值会被保持不变;当\theta_i ≤𝑎时,基会被设置为零;当𝑎<\theta_i <𝑐时,基会被截断为一个较小的固定数值。该方法的数学表达式如下:

这种方法在一定程度上削弱了某些子空间对不同位置索引的区分能力,进而可能对模型的性能产生不利影响。

5.4.2 调整上下文窗口

通过采用受限的注意力机制来调整原始的上下文窗口,从而实现对更长文本的有效建模。下面将详细介绍三种调整上下 文窗口的方法。

并行上下文窗口

该方法将输入文本划分为若干个片段,每个片段都进行独立的编码处理, 并共享相同的位置编码信息。在生成阶段,通过调整注意力掩码,使得后续生成的词元能够访问到前序的所有词元。然而,该方法无法有效地区分不同段落之间的顺序关系,在某些特定任务上可能会限制模型的表现能力。

Λ形上下文窗口

在处理长文本时,大语言模型有时会表现出一种不均匀关注的现象:它们 倾向于对序列起始位置以及邻近的词元赋予更高的注意力权重。基于这一观察, StreamingLLM等工作引入了“Λ形”注意力掩码方法,能够有选择性地关注每个查询的邻近词元以及序列起始的词元,同时忽略超出这一范围的其他词元。在给定的有限内存资源下,这种方法能够生成几乎无限长的流畅文本。然而,由于 无法有效利用被忽略的词元信息,这种方法无法充分利用所有的上下文信息。

词元选择

旨在挑选出最重要的𝑘个词元,以实现对于完整注意力的有效拟合。词元选择方法可以通过查询与词元相似度查询与词元所在分块的相似度实现。

  • 查询与词元相似度

根据位置索引和上下文窗口,词元被划分为窗口内的近距离词元和窗口外的远距离词元。对于窗口外的远距离词元,通常利用外部存储保存它们的键值对,并采用𝑘近邻搜索方法来获取当前生成所需的T_{max}个最相关词元。具体来说,在Transformer模型中,可以首先选定若干层,针对这些层从外部存储中检索到最相关词元的键值对,进一步将其送入注意力计算模块中,为模型补充远程语义信息;而在其他层中,模型仍然针对上下文窗口内的词元进行注意力计算。

  • 词元所在分块的相似度

分块级别的词元选择将序列划分为不同的长度固定的分块,并从分块序列中选择出最相关的部分分块。具体来说,模型首先将每个分块中所有的隐状态压缩为一个键向量表示,然后利用𝑘近邻方法选出与查询最相关的𝑘个分块,并保证这些块中的总词元数目至多为T_{max}。这些分块中所有的词元将按照它们在整个输入序列中的出现的顺序进行排序,并按照排序后的位置赋予位置编码。随后,这些词元被送入注意力模块中处理。

与词元级别的方法不同,分块级别的选择通常不需要外部存储,而是将所有数据存储在内存中。此外, 不同的层和头可以根据自身的特性选择不同的词元集合,从而更为灵活地利用整个长序列的信息。因此,分块级别的词元选择能够在保证性能的同时降低计算复杂度和内存需求。

5.4.3 长文本数据

本节将详细介绍如何确定所需的长文本数据量,以及如何合 理分布长文本数据的领域,以确保模型的长文本建模能力。

  • 长文本数据量

对于面向长文本建模的继续预训练来说,可以采用少量长文本数据进行轻量化的继续预训练。这一方法需要模型在初始预训练阶段已经学会了利用远程词元信息的能力,仅需使模型适应更长的上下文窗口。一般而来说,只需在约1B级别的词元上执行 数百步的训练,就可以将7B或者13B大小的LLaMA系列模型的上下文窗口至100K 词元以上的长度,并具有较好的长上下文利用能力。然而,值得注意的是,模型在处理短文本时的性能可能会受到一定程度的影响。

  • 长文本数据混合

为了提升模型的泛化能力,长文本数据的领域应尽可能多样化,并且与预训练数据集的分布保持相似。通过去除杂乱型(杂乱无章的文本),并在保留整体型(完整的有意义的长文)的同时对聚合型(多篇相关文本的聚合)进行上采样构建的训练集,可以更好地提升模型的长文本建模能力。

5.5 新型模型架构

Transformer的自注意力机制在计算每个词元时都需要利用到序列中所有词元的信息,这导致计算和存储复杂度随输入序列长度的平方级别增长。在处理长序列时,这种复杂性会消耗大量的计算资源与存储空间。为了解决这个问题,研究人员致力于新型模型架构的设计。这些新型模型大多基于参数化状态空间模型(StateSpaceModel,SSM)进行设计,在长文本建模效率方面相比Transformer有了大幅改进,同时也保持了较好的序列建模能力。在本节中,我们将首先对于参数化状态空间模型展开讨论,然后针对状态空间模型的各种变种模型进行介绍。为了帮助读者更好地理解这些模型之间的区别,我们在表5.2中对于它们进行了比较:

 5.5.1 参数化状态空间模型

参数化状态空间模型可以看作是循环神经网络和卷积神经网络的“结合体”。一方面,该模型可以利用卷积计算对输入进行并行化编码。另一方面,该模型在计算中不需要访问前序的所有词元,仅仅利用前一个词元就可以自回归地进行预测。由于自然语言文本本质上是离散型序列数据,本书将着重探讨离散型状态空间模型。

为了同时实现并行化计算和循环解码,状态空间模型在输入和输出之间引入了额外的状态变量。在循环计算中,状态空间模型首先循环地利用当前时刻的输 入\textbf{x}_t 和前一个时刻的状态\textbf{s}_{t-1}对当前时刻的状态\textbf{s}_{t}进行计算。然后,该模型将 当前时刻的状态\textbf{s}_{t}进一步映射为输出\textbf{y}_{t}。该模型的数学表示如下所示(公式5.44):

其中,\textbf{A}\in\mathbb{R}^{H\times N\times N}\textbf{B}\in\mathbb{R}^{H\times N\times 1}\textbf{C}\in\mathbb{R}^{H\times 1\times N} 是可学习参数,而⊗表示批量矩阵乘法。针对上述公式,当前时刻的输出可以通过循环的方式进行分解,进而 表示为如下的数学形式:

根据卷积计算的定义,上式对输出\textbf{y}_{t} 的计算可以看作是对输入的卷积, 其中卷积核为𝑲。这一计算可以表示为:

其中,“∗”表示卷积计算。在使用卷积计算时,状态空间模型可以利用快速傅里叶变 换加速计算效率,从而通过O(THlogT+THN^2+TH^2)的复杂度建模整个序列。在循环计算的时候,状态空间模型不需要和Transformer一样对前面所有时刻的状态进行访问,而是仅仅需要前一个时刻的状态。因此,该模型仅仅需要 O(N^2H+H^2)的复杂度就可以完成对整个序列的建模。由于具有更优的计算效率,状态空间模型常常被用来对长序列数据进行建模。

5.5.2 状态空间模型变种

为了保证计算效率的同时提高其语言建模的能力,研究人员对于状态空间模型进行了性能改进。代表性模型包括Mamba、 RWKV(Receptance Weighted Key Value)、RetNet(Retentive Network) 和Hyena 等。接下来,我们将对这些模型进行简要介绍。

  • Mamba

Mamba是一种状态空间模型的变种,主要思想是在状态空间模型的状态更新(公式5.44)中引入了基于当前输入的信息选择(Selection)机制, 来确定当前时刻状态如何从前一时刻状态以及当前输入中提取信息,从而提升其在语言建模上的性能。

对于文本建模而言,模型需要能够自适应地基于输入和之前状态来实现更好的上下文表示效果。因此,Mamba提出将更新状态和输出的方程中(公式5.44)的参数矩阵(𝑨,𝑩,𝑪) 表示成输入\textbf{x}_t 的非线性函数。进而,模型能够基于当前时刻的输入\textbf{x}_t对上一时刻的状态\textbf{S}_{t-1}和当前时刻输入\textbf{x}_t 中的信息进行选择性过滤,从而实现更为有效的上下文表示。

相比于标准状态空间模型,Mamba展现出了更好的文本建模性能,但是由于引入了关于\textbf{x}_t的非线性关系,Mamba无法利用快速傅里叶变换实现高效卷积计算。

  • RWKV

RWKV 尝试将 RNN 和 Transformer 的优点进行结合,继承了 Transformer 的建模优势和 RNN 的计算效率。

RWKV在每层的计算中使用词元偏移(TokenShift)来代替词元表示。在每一步的状态计算中,它显示地引入了上一个词元\textbf{x}_{t-1},通过两个相邻的词元\textbf{x}_t\textbf{x}_{t-1}进行线 性插值来代替\textbf{x}_t作为后续模块的输入。进一步,RWKV将Transformer中的多头注意力模块和前馈网络模块分别替换为时间混合(Time-mixing)模块和频道混合 (Channel-mixing)模块。其中,时间混合模块是一个类似于门控的RNN的网络, 并使用词元偏移对状态进行更新;频道混合模块是在前馈网络的基础上引入了词元偏移进行映射。

RWKV在解码过程中可以像RNN一样只参考前一时刻的状态,但是在训练过程中缺乏并行计算的能力。

  • RetNet

RetNet提出使用多尺度保留(Multi-scaleRetention, MSR)机制来代替多头注意力模块,从而提升计算效率。

多尺度保留机制是在标准状态空间模 型的基础上,在状态更新的线性映射中引入了输入相关信息来提升序列建模能力。 每个保留模块中,输入词元被映射为查询向量\textbf{q}_{t}、键向量\textbf{k}_{t}和值向量\textbf{v}_{t},并通过 \textbf{k}^T_t\textbf{v}_{t} 和前一个时刻的状态\textbf{S}_{t-1}进行线性相加,得到当前的状态:\textbf{S}_t=\textbf{A}\textbf{S}_{t-1}+\textbf{k}^T_t\textbf{v}_t。 最后,RetNet 使用查询\textbf{q}_{t} 将当前状态\textbf{S}_{t} 映射为输出\textbf{o}_t=\textbf{q}_t\textbf{S}_t。此外,RetNet还可以通过类似注意力操作的矩阵乘法,对所有词元的状态进行并行化计算。因此 类似于标准状态空间模型,RetNet同时保留了循环计算和并行计算的优点。

  • Hyena

Hyena 提出使用长卷积模块(LongConvolution)来替换 Transformer 架构中的注意力模块,从而借助卷积的快速傅里叶变换来提高计算效率。

Hyena 在每层的长卷积模块中包含了𝑀个滤波器,即每个相对位置𝑡有一个相应 的滤波器𝒉(𝑡),然后将这些滤波器组合成卷积核𝑲=(𝒉(1),...,𝒉(𝑇))。利用该卷 积核与输入序列[𝒙1,...,𝒙𝑡] 进行卷积,可以对序列中不同位置的信息进行聚合, 得到每个位置的中间表示𝒛𝑡。最后,再使用门控函数𝒈(𝑡)(基于输入\textbf{x}_t)对中间 表示\textbf{z}_t 进行加权,得到该模块的最终输出。

由于使用了卷积计算可以使用快速傅 里叶变换进行加速,在训练中,Hyena可以实现O(TMHlogT+TMH^2)的计算复杂度。但是在解码时,每次计算必须对前面所有的词元进行卷积,因此解码复杂度为O(TMH+MH^2)

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐