【深度学习】DeepL|LLM基础知识

Last updated on July 18, 2024 pm

这里将分享深度学习和大语言模型的基础知识和内容

深度学习基础

训练细节

1.逻辑回归和线性回归

线性回归解决的是回归问题,逻辑回归相当于是线性回归的基础上,来解决分类问题

线性回归(Linear Regression) \[ \begin{aligned} &f_{w, b}(x)=\sum_i w_i x_i+b\\ \end{aligned} \] 逻辑回归(Logistic Regression) $$ \[\begin{aligned} &f_{w, b}(x)=\sigma\left(\sum_i w_i x_i+b\right) \end{aligned}\]

$$ 逻辑回归可以理解为在线性回归后加了一个 sigmoid 函数。将线性回归变成一个0~1输出的分类问题。逻辑回归本质上是一个线性回归模型,因为除去sigmoid映射函数关系,其他的步骤,算法都是线性回归的。可以说,逻辑回归都是以线性回归为理论支持的,只不过逻辑回归可以轻松解决 0/1 分类问题。

2. 深度学习模型的参数范围

因为参数越小代表模型越简单,越是复杂的模型,越是尝试对所有样本进行拟合,包括异常点。这就会造成在较小的区间中产生较大的波动,这个较大的波动也会反映在这个区间的导数比较大。只有越大的参数才可能产生较大的导数。因此参数越小,模型就越简单。

3. 实现参数稀疏

参数的稀疏,在一定程度上实现了特征的选择。一般而言,大部分特征对模型是没有贡献的。这些没有用的特征虽然可以减少训练集上的误差,但是对测试集的样本,反而会产生干扰。稀疏参数的引入,可以将那些无用的特征的权重置为0

4. Batch size的大小对学习率的影响

  • batch-size大,学习率也可以取得大一点,而且,batch-size大通常更新次数少,所以需要更多的epoch才能让loss收敛。
  • batch-size小,学习率应该取得小一点,取的大会发生nan(梯度爆炸了),batch-size小通常更新次数多,较少的epoch就课可以让loss收敛,但是缺点是训练过程慢。

为什么batch-size小,学习率取的大会发生nan? 学习率较高的情况下,直接影响到每次更新值的程度比较大,走的步伐因此也会大起来。过大的学习率会导致无法顺利地到达最低点,稍有不慎就会跳出可控制区域,此时我们将要面对的就是损失成倍增大(跨量级)

5. 优化器于损失函数

优化器optimizer和损失函数loss function的区别:

  1. 优化器定义了哪些参数是要用来更新的,并且设置了更新的方式(学习率、动量、SGD等),还有一些权重衰减的设置。
  2. 损失函数是用来计算损失的,也可以说损失函数是负责反向传播求导用的

6. 训练中loss快速增大

训练过程中发现loss快速增大应该从哪些方面考虑?

  1. 学习率过大,导致学习的过程非常不平稳导致的损失值快速增大
  2. 训练的样本中存在坏数据,造成了数据的污染

7. 梯度消失和梯度爆炸

梯度消失的原因和解决

  1. 隐藏层的层数过多

    反向传播求梯度时的链式求导法则,某部分梯度小于1,则多层连乘后出现梯度消失

  2. 采用了不合适的激活函数

    如sigmoid函数的最大梯度为1/4,这意味着隐藏层每一层的梯度均小于1(权值小于1时),出现梯度消失

  3. 解决方法:1、relu激活函数,使导数衡为1 2、batch norm 3、残差结构

梯度爆炸的原因和解决办法

  1. 隐藏层的层数过多,某部分梯度大于1,则多层连乘后,梯度呈指数增长,产生梯度爆炸。
  2. 权重初始值太大,求导时会乘上权重
  3. 解决方法:1、梯度裁剪 2、权重L1/L2正则化 3、残差结构 4、batch norm

8. 如何取消张量梯度

可以选择使用torch.no_grad来将某个张量的值取消计算梯度,model.eval vs和torch.no_grad区别

  • model.eval(): 依然计算梯度,但是不反传;dropout层保留概率为1;batchnorm层使用全局的mean和var
  • with torch.no_grad: 不计算梯度

9.Dropout和BatchNorm

Dropout和Batch norm能否一起使用?

可以,但是只能将Dropout放在Batch norm之后使用。因为Dropout训练时会改变输入X的方差,从而影响Batch norm训练过程中统计的滑动方差值;而测试时没有Dropout,输入X的方差和训练时不一致,这就导致Batch norm测试时期望的方差和训练时统计的有偏差。

其余参考

神经网络权重初始化 https://blog.csdn.net/kebu12345678/article/details/103084851

https://zhuanlan.zhihu.com/p/667048896

https://zhuanlan.zhihu.com/p/643560888

https://zhuanlan.zhihu.com/p/599016986

LLM基础

1.Attention

1.1 讲讲对Attention的理解

Attention机制是一种在处理时序相关问题的时候常用的技术,主要用于处理序列数据。

核心思想是在处理序列数据时,网络应该更关注输入中的重要部分,而忽略不重要的部分,它通过学习不同部分的权重,将输入的序列中的重要部分显式地加权,从而使得模型可以更好地关注与输出有关的信息。

在序列建模任务中,比如机器翻译、文本摘要、语言理解等,输入序列的不同部分可能具有不同的重要性。传统的循环神经网络(RNN)或卷积神经网络(CNN)在处理整个序列时,难以捕捉到序列中不同位置的重要程度,可能导致信息传递不够高效,特别是在处理长序列时表现更明显。

Attention机制的关键是引入一种机制来动态地计算输入序列中各个位置的权重,从而在每个时间步上,对输入序列的不同部分进行加权求和,得到当前时间步的输出。这样就实现了模型对输入中不同部分的关注度的自适应调整。

1.2 Attention的计算步骤

具体的计算步骤如下:

  • 计算查询(Query):查询是当前时间步的输入,用于和序列中其他位置的信息进行比较。
  • 计算键(Key)和值(Value):键表示序列中其他位置的信息,值是对应位置的表示。键和值用来和查询进行比较。
  • 计算注意力权重:通过将查询和键进行内积运算,然后应用softmax函数,得到注意力权重。这些权重表示了在当前时间步,模型应该关注序列中其他位置的重要程度。
  • 加权求和:根据注意力权重将值进行加权求和,得到当前时间步的输出。

在Transformer中,Self-Attention 被称为"Scaled Dot-Product Attention",其计算过程如下:

  1. 对于输入序列中的每个位置,通过计算其与所有其他位置之间的相似度得分(通常通过点积计算)。
  2. 对得分进行缩放处理,以防止梯度爆炸。
  3. 将得分用softmax函数转换为注意力权重,以便计算每个位置的加权和。
  4. 使用注意力权重对输入序列中的所有位置进行加权求和,得到每个位置的自注意输出。

\[ Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})V \]

1.3 Attention机制和传统的Seq2Seq模型的区别

Seq2Seq模型是一种基于编码器-解码器结构的模型,主要用于处理序列到序列的任务,例如机器翻译、语音识别等。

传统的Seq2Seq模型只使用编码器来捕捉输入序列的信息,而解码器只从编码器的最后状态中获取信息,并将其用于生成输出序列。

而Attention机制则允许解码器在生成每个输出时,根据输入序列的不同部分给予不同的注意力,从而使得模型更好地关注到输入序列中的重要信息。

1.4 self-attention 和 target-attention的区别

self-attention是指在序列数据中,将当前位置与其他位置之间的关系建模。它通过计算每个位置与其他所有位置之间的相关性得分,从而为每个位置分配一个权重。这使得模型能够根据输入序列的不同部分的重要性,自适应地选择要关注的信息。

target-attention则是指将注意力机制应用于目标(或查询)和一组相关对象之间的关系。它用于将目标与其他相关对象进行比较,并将注意力分配给与目标最相关的对象。这种类型的注意力通常用于任务如机器翻译中的编码-解码模型,其中需要将源语言的信息对齐到目标语言。

因此,自注意力主要关注序列内部的关系,而目标注意力则关注目标与其他对象之间的关系。这两种注意力机制在不同的上下文中起着重要的作用,帮助模型有效地处理序列数据和相关任务。

1.5 在常规attention中,一般有k=v,那self-attention 可以吗

self-attention实际只是attention中的一种特殊情况,因此k=v是没有问题的,也即K,V参数矩阵相同。实际上,在Transformer模型中,Self-Attention的典型实现就是k等于v的情况。Transformer中的Self-Attention被称为"Scaled Dot-Product Attention",其中通过将词向量进行线性变换来得到Q、K、V,并且这三者是相等的。

1.6 目前主流的attention方法有哪些?

讲自己熟悉的就可:

  • Scaled Dot-Product Attention: 这是Transformer模型中最常用的Attention机制,用于计算查询向量(Q)与键向量(K)之间的相似度得分,然后使用注意力权重对值向量(V)进行加权求和。
  • Multi-Head Attention: 这是Transformer中的一个改进,通过同时使用多组独立的注意力头(多个QKV三元组),并在输出时将它们拼接在一起。这样的做法允许模型在不同的表示空间上学习不同类型的注意力模式。
  • Relative Positional Encoding: 传统的Self-Attention机制在处理序列时并未直接考虑位置信息,而相对位置编码引入了位置信息,使得模型能够更好地处理序列中不同位置之间的关系。
  • Transformer-XL: 一种改进的Transformer模型,通过使用循环机制来扩展Self-Attention的上下文窗口,从而处理更长的序列依赖性。

1.7 self-attention 在计算的过程中,如何对padding位做mask?

在 Attention 机制中,同样需要忽略 padding 部分的影响,这里以transformer encoder中的self-attention为例:self-attention中,Q和K在点积之后,需要先经过mask再进行softmax,因此,对于要屏蔽的部分,mask之后的输出需要为负无穷,这样softmax之后输出才为0。

1.8 深度学习中attention与全连接层的区别何在?

这是个非常有意思的问题,要回答这个问题,我们必须重新定义一下Attention。

Transformer Paper里重新用QKV定义了Attention。所谓的QKV就是Query,Key,Value。如果我们用这个机制来研究传统的RNN attention,就会发现这个过程其实是这样的:RNN最后一步的output是Q,这个Q query了每一个中间步骤的K。Q和K共同产生了Attention Score,最后Attention Score乘以V加权求和得到context。那如果我们不用Attention,单纯用全连接层呢?很简单,全链接层可没有什么Query和Key的概念,只有一个Value,也就是说给每个V加一个权重再加到一起(如果是Self Attention,加权这个过程都免了,因为V就直接是从raw input加权得到的。)

可见Attention和全连接最大的区别就是Query和Key,而这两者也恰好产生了Attention Score这个Attention中最核心的机制。而在Query和Key中,我认为Query又相对更重要,因为Query是一个锚点,Attention Score便是从过计算与这个锚点的距离算出来的。任何Attention based algorithm里都会有Query这个概念,但全连接显然没有。

最后来一个比较形象的比喻吧。如果一个神经网络的任务是从一堆白色小球中找到一个略微发灰的,那么全连接就是在里面随便乱抓然后凭记忆和感觉找,而attention则是左手拿一个白色小球,右手从袋子里一个一个抓出来,两两对比颜色,你左手抓的那个白色小球就是Query。

1.9 MHA

从多头注意力的结构图中,貌似这个所谓的多个头就是指多组线性变换层,其实并不是,只有使用了一组线性变化层,即三个变换张量对Q,K,V分别进行线性变换,这些变换不会改变原有张量的尺寸,因此每个变换矩阵都是方阵,得到输出结果后,多头的作用才开始显现,每个头开始从词义层面分割输出的张量,也就是每个头都想获得一组Q,K,V进行注意力机制的计算,但是句子中的每个词的表示只获得一部分,也就是只分割了最后一维的词嵌入向量。这就是所谓的多头,将每个头的获得的输入送到注意力机制中, 就形成多头注意力机制.

Multi-head attention允许模型共同关注来自不同位置的不同表示子空间的信息,如果只有一个attention head,它的平均值会削弱这个信息。

\[ MultiHead(Q,K,V)=Concat(head_1,...,head_h)W^O \\ where ~ head_i = Attention(QW_i^Q, KW_i^K, VW_i^V) \]

其中映射由权重矩阵完成:$ W^Q_i ^{d_ d_k} $, \(W^K_i \in \mathbb{R}^{d_{\text{model}} \times d_k}\), \(W^V_i \in \mathbb{R}^{d_{\text{model}} \times d_v}\)\(W^O_i \in \mathbb{R}^{hd_v \times d_{\text{model}} }\)

MHA示意图

多头注意力作用

这种结构设计能让每个注意力机制去优化每个词汇的不同特征部分,从而均衡同一种注意力机制可能产生的偏差,让词义拥有来自更多元的表达,实验表明可以从而提升模型效果.

为什么要做多头注意力机制呢

  • 一个 dot product 的注意力里面,没有什么可以学的参数。具体函数就是内积,为了识别不一样的模式,希望有不一样的计算相似度的办法。加性 attention 有一个权重可学,也许能学到一些内容。
  • multi-head attention 给 h 次机会去学习 不一样的投影的方法,使得在投影进去的度量空间里面能够去匹配不同模式需要的一些相似函数,然后把 h 个 heads 拼接起来,最后再做一次投影。
  • 每一个头 hi 是把 Q,K,V 通过 可以学习的 Wq, Wk, Wv 投影到 dv 上,再通过注意力函数,得到 headi。

1.10 MQA

MQA(Multi Query Attention)最早是出现在2019年谷歌的一篇论文 《Fast Transformer Decoding: One Write-Head is All You Need》。

MQA的思想其实比较简单,MQA 与 MHA 不同的是,MQA 让所有的头之间共享同一份 Key 和 Value 矩阵,每个头正常的只单独保留了一份 Query 参数,从而大大减少 Key 和 Value 矩阵的参数量

MQA示意图

在 Multi-Query Attention 方法中只会保留一个单独的key-value头,这样虽然可以提升推理的速度,但是会带来精度上的损失。《Multi-Head Attention:Collaborate Instead of Concatenate 》这篇论文的第一个思路是基于多个 MQA 的 checkpoint 进行 finetuning,来得到了一个质量更高的 MQA 模型。这个过程也被称为 Uptraining。

具体分为两步:

  1. 对多个 MQA 的 checkpoint 文件进行融合,融合的方法是: 通过对 key 和 value 的 head 头进行 mean pooling 操作,如下图。
  2. 对融合后的模型使用少量数据进行 finetune 训练,重训后的模型大小跟之前一样,但是效果会更好

多个MQA的finetune

1.11 GQA

Google 在 2023 年发表的一篇 《GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints》的论文

如下图所示

  • MHA(Multi Head Attention) 中,每个头有自己单独的 key-value 对;
  • MQA(Multi Query Attention) 中只会有一组 key-value 对;
  • GQA(Grouped Query Attention) 中,会对 attention 进行分组操作,query 被分为 N 组,每个组共享一个 Key 和 Value 矩阵。
MHA,MQA,GQA对比

GQA-N 是指具有 N 组的 Grouped Query Attention。GQA-1具有单个组,因此具有单个Key 和 Value,等效于MQA。而GQA-H具有与头数相等的组,等效于MHA。

在基于 Multi-head 多头结构变为 Grouped-query 分组结构的时候,也是采用跟上图一样的方法,对每一组的 key-value 对进行 mean pool 的操作进行参数融合。融合后的模型能力更综合,精度比 Multi-query 好,同时速度比 Multi-head 快

1.12 MHA&MQA&GQA对比总结

MHA(Multi-head Attention)是标准的多头注意力机制,h个Query、Key 和 Value 矩阵。

MQA(Multi-Query Attention)是多查询注意力的一种变体,也是用于自回归解码的一种注意力机制。与MHA不同的是,MQA 让所有的头之间共享同一份 Key 和 Value 矩阵,每个头只单独保留了一份 Query 参数,从而大大减少 Key 和 Value 矩阵的参数量

GQA(Grouped-Query Attention)是分组查询注意力,GQA将查询头分成G组,每个组共享一个Key 和 Value 矩阵。GQA-G是指具有G组的grouped-query attention。GQA-1具有单个组,因此具有单个Key 和 Value,等效于MQA。而GQA-H具有与头数相等的组,等效于MHA。

GQA介于MHA和MQA之间。GQA 综合 MHA 和 MQA ,既不损失太多性能,又能利用 MQA 的推理加速。不是所有 Q 头共享一组 KV,而是分组一定头数 Q 共享一组 KV,比如上图中就是两组 Q 共享一组 KV。

2.Transformer

2.1 transformer中multi-head attention中每个head为什么要进行降维?

在Transformer的Multi-Head Attention中,对每个head进行降维是为了增加模型的表达能力和效率。

每个head是独立的注意力机制,它们可以学习不同类型的特征和关系。通过使用多个注意力头,Transformer可以并行地学习多种不同的特征表示,从而增强了模型的表示能力。

然而,在使用多个注意力头的同时,注意力机制的计算复杂度也会增加。原始的Scaled Dot-Product Attention的计算复杂度为\(O(d^2)\),其中d是输入向量的维度。如果使用h个注意力头,计算复杂度将增加到\(O(hd^2)\)。这可能会导致Transformer在处理大规模输入时变得非常耗时。

为了缓解计算复杂度的问题,Transformer中在每个head上进行降维。在每个注意力头中,输入向量通过线性变换被映射到一个较低维度的空间。这个降维过程使用两个矩阵:一个是查询(Q)和键(K)的降维矩阵\(W_q\)\(W_k\),另一个是值(V)的降维矩阵\(W_v\)

通过降低每个head的维度,Transformer可以在保持较高的表达能力的同时,大大减少计算复杂度。降维后的计算复杂度为\((h\hat d ^ 2)\),其中\(\hat d\)是降维后的维度。通常情况下,\(\hat d\)会远小于原始维度d,这样就可以显著提高模型的计算效率。

2.2 transformer在哪里做了权重共享,为什么可以做权重共享?

Transformer在Encoder和Decoder中都进行了权重共享。

在Transformer中,Encoder和Decoder是由多层的Self-Attention Layer和前馈神经网络层交叉堆叠而成。权重共享是指在这些堆叠的层中,相同位置的层共用相同的参数

在Encoder中,所有的自注意力层和前馈神经网络层都共享相同的参数。这意味着每一层的自注意力机制和前馈神经网络都使用相同的权重矩阵来进行计算。这种共享保证了每一层都执行相同的计算过程,使得模型能够更好地捕捉输入序列的不同位置之间的关联性。

在Decoder中,除了和Encoder相同的权重共享方式外,还存在另一种特殊的权重共享:Decoder的自注意力层和Encoder的自注意力层之间也进行了共享。这种共享方式被称为"masked self-attention",因为在解码过程中,当前位置的注意力不能关注到未来的位置(后续位置),以避免信息泄漏。通过这种共享方式,Decoder可以利用Encoder的表示来理解输入序列并生成输出序列。权重共享的好处是大大减少了模型的参数数量,使得Transformer可以更有效地训练,并且更容易进行推理。此外,共享参数还有助于加快训练速度和提高模型的泛化能力,因为模型可以在不同位置共享并学习通用的特征表示。

2.3 Transformer的点积模型做缩放的原因是什么?

使用缩放的原因是为了控制注意力权重的尺度,以避免在计算过程中出现梯度爆炸的问题。

Attention的计算是在内积之后进行softmax,主要涉及的运算是\(e^{q \cdot k}\),可以大致认为内积之后、softmax之前的数值在\(-3\sqrt{d}\)\(3\sqrt{d}\)这个范围内,由于d通常都至少是64,所以\(e^{3\sqrt{d}}\)比较大而 \(e^{-3\sqrt{d}}\)比较小,因此经过softmax之后,Attention的分布非常接近一个one hot分布了,这带来严重的梯度消失问题,导致训练效果差。(例如y=softmax(x)在|x|较大时进入了饱和区,x继续变化y值也几乎不变,即饱和区梯度消失)

相应地,解决方法就有两个:

  1. 像NTK参数化那样,在内积之后除以 \(\sqrt{d}\),使q⋅k的方差变为1,对应\(e^3,e^{−3}\)都不至于过大过小,这样softmax之后也不至于变成one hot而梯度消失了,这也是常规的Transformer如BERT里边的Self Attention的做法
  2. 另外就是不除以 \(\sqrt{d}\),但是初始化q,k的全连接层的时候,其初始化方差要多除以一个d,这同样能使得使q⋅k的初始方差变为1,T5采用了这样的做法。

2.4 Transformer和RNN对比

最简单情况:没有残差连接、没有 layernorm、 attention 单头、没有投影。看和 RNN 区别

  • attention 对输入做一个加权和,加权和 进入 point-wise MLP。(画了多个红色方块 MLP, 是一个权重相同的 MLP)
  • point-wise MLP 对 每个输入的点 做计算,得到输出。
  • attention 作用:把整个序列里面的信息抓取出来,做一次汇聚 aggregation

RNN 跟 transformer 异:如何传递序列的信

RNN 是把上一个时刻的信息输出传入下一个时候做输入。Transformer 通过一个 attention 层,去全局的拿到整个序列里面信息,再用 MLP 做语义的转换。

RNN 跟 transformer 同:语义空间的转换 + 关注点

用一个线性层 or 一个 MLP 来做语义空间的转换。

关注点:怎么有效的去使用序列的信息。

2.5 Transformer结构细节

Transformer为何使用多头注意力机制?(为什么不使用一个头)

  • 多头保证了transformer可以注意到不同子空间的信息,捕捉到更加丰富的特征信息。可以类比CNN中同时使用多个滤波器的作用,直观上讲,多头的注意力有助于网络捕捉到更丰富的特征/信息。

Transformer为什么Q和K使用不同的权重矩阵生成,为何不能使用同一个值进行自身的点乘? (注意和第一个问题的区别)

  • 使用Q/K/V不相同可以保证在不同空间进行投影,增强了表达能力,提高了泛化能力。
  • 同时,由softmax函数的性质决定,实质做的是一个soft版本的arg max操作,得到的向量接近一个one-hot向量(接近程度根据这组数的数量级有所不同)。如果令Q=K,那么得到的模型大概率会得到一个类似单位矩阵的attention矩阵,这样self-attention就退化成一个point-wise线性映射。这样至少是违反了设计的初衷。

Transformer计算attention的时候为何选择点乘而不是加法?两者计算复杂度和效果上有什么区别?

  • K和Q的点乘是为了得到一个attention score 矩阵,用来对V进行提纯。K和Q使用了不同的W_k, W_Q来计算,可以理解为是在不同空间上的投影。正因为有了这种不同空间的投影,增加了表达能力,这样计算得到的attention score矩阵的泛化能力更高。
  • 为了计算更快。矩阵加法在加法这一块的计算量确实简单,但是作为一个整体计算attention的时候相当于一个隐层,整体计算量和点积相似。在效果上来说,从实验分析,两者的效果和dk相关,dk越大,加法的效果越显著。

为什么在进行softmax之前需要对attention进行scaled(为什么除以dk的平方根),并使用公式推导进行讲解

  • 这取决于softmax函数的特性,如果softmax内计算的数数量级太大,会输出近似one-hot编码的形式,导致梯度消失的问题,所以需要scale
  • 那么至于为什么需要用维度开根号,假设向量q,k满足各分量独立同分布,均值为0,方差为1,那么qk点积均值为0,方差为dk,从统计学计算,若果让qk点积的方差控制在1,需要将其除以dk的平方根,是的softmax更加平滑

在计算attention score的时候如何对padding做mask操作?

为何在获取输入词向量之后需要对矩阵乘以embedding size的开方?意义是什么?

  • embedding matrix的初始化方式是xavier init,这种方式的方差是1/embedding size,因此乘以embedding size的开方使得embedding matrix的方差是1,在这个scale下可能更有利于embedding matrix的收敛。

简单介绍一下Transformer的位置编码?有什么意义和优缺点?

  • 因为self-attention是位置无关的,无论句子的顺序是什么样的,通过self-attention计算的token的hidden embedding都是一样的,这显然不符合人类的思维。因此要有一个办法能够在模型中表达出一个token的位置信息,transformer使用了固定的positional encoding来表示token在句子中的绝对位置信息。

你还了解哪些关于位置编码的技术,各自的优缺点是什么?(参考上一题)

  • 相对位置编码(RPE)1.在计算attention score和weighted value时各加入一个可训练的表示相对位置的参数。2.在生成多头注意力时,把对key来说将绝对位置转换为相对query的位置3.复数域函数,已知一个词在某个位置的词向量表示,可以计算出它在任何位置的词向量表示。前两个方法是词向量+位置编码,属于亡羊补牢,复数域是生成词向量的时候即生成对应的位置信息。

为什么transformer块使用LayerNorm而不是BatchNorm?LayerNorm 在Transformer的位置是哪里?

  • LN:针对每个样本序列进行Norm,没有样本间的依赖。对一个序列的不同特征维度进行Norm
  • CV使用BN是认为channel维度的信息对cv方面有重要意义,如果对channel维度也归一化会造成不同通道信息一定的损失。而同理nlp领域认为句子长度不一致,并且各个batch的信息没什么关系,因此只考虑句子内信息的归一化,也就是LN。

简答讲一下BatchNorm技术,以及它的优缺点。

  • 优点:
    • 第一个就是可以解决内部协变量偏移,简单来说训练过程中,各层分布不同,增大了学习难度,BN缓解了这个问题。当然后来也有论文证明BN有作用和这个没关系,而是可以使损失平面更加的平滑,从而加快的收敛速度。
    • 第二个优点就是缓解了梯度饱和问题(如果使用sigmoid激活函数的话),加快收敛。
  • 缺点:
    • 第一个,batch_size较小的时候,效果差。这一点很容易理解。BN的过程,使用 整个batch中样本的均值和方差来模拟全部数据的均值和方差,在batch_size 较小的时候,效果肯定不好。
    • 第二个缺点就是 BN 在RNN中效果比较差。

Encoder端和Decoder端是如何进行交互的?(在这里可以问一下关于seq2seq的attention知识)

  • Cross Self-Attention,Decoder提供Q,Encoder提供K,V

Decoder阶段的多头自注意力和encoder的多头自注意力有什么区别?(为什么需要decoder自注意力需要进行 sequence mask)

  • 让输入序列只看到过去的信息,不能让他看到未来的信息

引申一个关于bert问题,bert的mask为何不学习transformer在attention处进行屏蔽score的技巧?

  • BERT和transformer的目标不一致,bert是语言的预训练模型,需要充分考虑上下文的关系,而transformer主要考虑句子中第i个元素与前i-1个元素的关系。

3 Normalization

3.1 Batch Norm

为什么要进行BN呢?

  1. 在深度神经网络训练的过程中,通常以输入网络的每一个mini-batch进行训练,这样每个batch具有不同的分布,使模型训练起来特别困难。
  2. Internal Covariate Shift (ICS) 问题:在训练的过程中,激活函数会改变各层数据的分布,随着网络的加深,这种改变(差异)会越来越大,使模型训练起来特别困难,收敛速度很慢,会出现梯度消失的问题。

BN的主要思想: 针对每个神经元,使数据在进入激活函数之前,沿着通道计算每个batch的均值、方差,‘强迫’数据保持均值为0,方差为1的正态分布, 避免发生梯度消失。具体来说,就是把第1个样本的第1个通道,加上第2个样本第1个通道 ...... 加上第 N 个样本第1个通道,求平均,得到通道 1 的均值(注意是除以 N×H×W 而不是单纯除以 N,最后得到的是一个代表这个 batch 第1个通道平均值的数字,而不是一个 H×W 的矩阵)。求通道 1 的方差也是同理。对所有通道都施加一遍这个操作,就得到了所有通道的均值和方差。

BN的使用位置: 全连接层或卷积操作之后,激活函数之前。

BN算法过程:

  • 沿着通道计算每个batch的均值
  • 沿着通道计算每个batch的方差
  • 做归一化
  • 加入缩放和平移变量\(\gamma\)\(\beta\)

加入缩放和平移变量的原因是:保证每一次数据经过归一化后还保留原有学习来的特征,同时又能完成归一化操作,加速训练。这两个参数是用来学习的参数。

encoder-decoder分类https://zhuanlan.zhihu.com/p/642923989

BN的作用:

  1. 允许较大的学习率;
  2. 减弱对初始化的强依赖性
  3. 保持隐藏层中数值的均值、方差不变,让数值更稳定,为后面网络提供坚实的基础;
  4. 有轻微的正则化作用(相当于给隐藏层加入噪声,类似Dropout)

BN存在的问题:

  1. 每次是在一个batch上计算均值、方差,如果batch size太小,则计算的均值、方差不足以代表整个数据分布。
  2. batch size太大: 会超过内存容量;需要跑更多的epoch,导致总训练时间变长;会直接固定梯度下降的方向,导致很难更新。

3.2 Layer Norm

LayerNorm是大模型也是transformer结构中最常用的归一化操作,简而言之,它的作用是 对特征张量按照某一维度或某几个维度进行0均值,1方差的归一化 操作,计算公式为:

\[ \mathrm{y}=\frac{\mathrm{x}-\mathrm{E}(\mathrm{x})}{\sqrt{\mathrm{V} \operatorname{ar}(\mathrm{x})+\epsilon}} * \gamma+\beta \]

这里的 \(x\) 可以理解为** 张量中具体某一维度的所有元素**,比如对于 shape 为 (2,2,4) 的张量 input,若指定归一化的操作为第三个维度,则会对第三个维度中的四个张量(2,2,1),各进行上述的一次计算.

详细形式:

\[ a_{i}=\sum_{j=1}^{m} w_{i j} x_{j}, \quad y_{i}=f\left(a_{i}+b_{i}\right) \]

\[ \bar{a}_{i}=\frac{a_{i}-\mu}{\sigma} g_{i}, \quad y_{i}=f\left(\bar{a}_{i}+b_{i}\right), \]

\[ \mu=\frac{1}{n} \sum_{i=1}^{n} a_{i}, \quad \sigma=\sqrt{\frac{1}{n} \sum_{i=1}^{n}\left(a_{i}-\mu\right)^{2}}. \]

这里结合PyTorch的nn.LayerNorm算子来看比较明白

1
2
nn.LayerNorm(normalized_shape, eps=1e-05, elementwise_affine=True, device=None, dtype=None)

  • normalized_shape:归一化的维度,int(最后一维)list(list里面的维度),还是以(2,2,4)为例,如果输入是int,则必须是4,如果是list,则可以是[4], [2,4], [2,2,4],即最后一维,倒数两维,和所有维度
  • eps:加在分母方差上的偏置项,防止分母为0
  • elementwise_affine:是否使用可学习的参数 \(\gamma\)\(\beta\) ,前者开始为1,后者为0,设置该变量为True,则二者均可学习随着训练过程而变化

Layer Normalization (LN) 的一个优势是不需要批训练,在单条数据内部就能归一化。LN不依赖于batch size和输入sequence的长度,因此可以用于batch size为1和RNN中。LN用于RNN效果比较明显,但是在CNN上,效果不如BN

3.3 Instance Norm

IN针对图像像素做normalization,最初用于图像的风格化迁移。在图像风格化中,生成结果主要依赖于某个图像实例,feature map 的各个 channel 的均值和方差会影响到最终生成图像的风格。所以对整个batch归一化不适合图像风格化中,因而对H、W做归一化。可以加速模型收敛,并且保持每个图像实例之间的独立。

对于,IN 对每个样本的 H、W 维度的数据求均值和标准差,保留 N 、C 维度,也就是说,它只在 channel 内部求均值和标准差,其公式如下:

\[ y_{t i j k}=\frac{x_{t i j k}-\mu_{t i}}{\sqrt{\sigma_{t i}^{2}+\epsilon}} \quad \mu_{t i}=\frac{1}{H W} \sum_{l=1}^{W} \sum_{m=1}^{H} x_{t i l m} \quad \sigma_{t i}^{2}=\frac{1}{H W} \sum_{l=1}^{W} \sum_{m=1}^{H}\left(x_{t i l m}-m u_{t i}\right)^{2} \]

3.4 Group Norm

GN是为了解决BN对较小的mini-batch size效果差的问题。

GN适用于占用显存比较大的任务,例如图像分割。对这类任务,可能 batch size 只能是个位数,再大显存就不够用了。而当 batch size 是个位数时,BN 的表现很差,因为没办法通过几个样本的数据量,来近似总体的均值和标准差。GN 也是独立于 batch 的,它是 LN 和 IN 的折中。

具体方法: GN 计算均值和标准差时,把每一个样本 feature map 的 channel 分成 G 组,每组将有 C/G 个 channel,然后将这些 channel 中的元素求均值和标准差。各组 channel 用其对应的归一化参数独立地归一化。 \[ \mu_{n g}(x)=\frac{1}{(C / G) H W} \sum_{c=g C / G}^{(g+1) C / G} \sum_{h=1}^{H} \sum_{w=1}^{W} x_{n c h w} \]

\[ \sigma_{n g}(x)=\sqrt{\frac{1}{(C / G) H W} \sum_{c=g C / G}^{(g+1) C / G} \sum_{h=1}^{H} \sum_{w=1}^{W}\left(x_{n c h w}-\mu_{n g}(x)\right)^{2}+\epsilon} \]

3.5 几类归一化区别与联系

Batch Normalization(Batch Norm)缺点:在处理序列数据(如文本)时,Batch Norm可能不会表现得很好,因为序列数据通常长度不一,并且一次训练的Batch中的句子的长度可能会有很大的差异;此外,Batch Norm对于Batch大小也非常敏感。对于较小的Batch大小,Batch Norm可能会表现得不好,因为每个Batch的统计特性可能会有较大的波动。

Layer Normalization(Layer Norm)优点:Layer Norm是对每个样本进行归一化,因此它对Batch大小不敏感,这使得它在处理序列数据时表现得更好;另外,Layer Norm在处理不同长度的序列时也更为灵活。

Instance Normalization(Instance Norm)优点:Instance Norm是对每个样本的每个特征进行归一化,因此它可以捕捉到更多的细节信息。Instance Norm在某些任务,如风格迁移,中表现得很好,因为在这些任务中,细节信息很重要。 缺点:Instance Norm可能会过度强调细节信息,忽视了更宏观的信息。此外,Instance Norm的计算成本相比Batch Norm和Layer Norm更高。

Group Normalization(Group Norm)优点:Group Norm是Batch Norm和Instance Norm的折中方案,它在Batch的一个子集(即组)上进行归一化。这使得Group Norm既可以捕捉到Batch的统计特性,又可以捕捉到样本的细节信息。此外,Group Norm对Batch大小也不敏感。 缺点:Group Norm的性能取决于组的大小,需要通过实验来确定最优的组大小。此外,Group Norm的计算成本也比Batch Norm和Layer Norm更高。

将输入的 feature map shape 记为**[N, C, H, W]**,其中N表示batch size,即N个样本;C表示通道数;H、W分别表示特征图的高度、宽度。这几个方法主要的区别就是在:

  1. BN是在batch上,对N、H、W做归一化,而保留通道 C 的维度。BN对较小的batch size效果不好。BN适用于固定深度的前向神经网络,如CNN,不适用于RNN;
  2. LN在通道方向上,对C、H、W归一化,主要对RNN效果明显;
  3. IN在图像像素上,对H、W做归一化,用在风格化迁移;
  4. GN将channel分组,然后再做归一化。
几类归一化方式的对比图

3.6 RMS Norm

与layerNorm相比,RMS Norm的主要区别在于去掉了减去均值的部分,计算公式为:

\[ \bar{a}_{i}=\frac{a_{i}}{\operatorname{RMS}(\mathbf{a})} g_{i}, \quad where~ \operatorname{RMS}(\mathbf{a})=\sqrt{\frac{1}{n} \sum_{i=1}^{n} a_{i}^{2}}. \]

RMS中去除了mean的统计值的使用,只使用root mean square(RMS)进行归一化。

3.7 pRMSNorm介绍

RMS具有线性特征,所以提出可以用部分数据的RMSNorm来代替全部的计算,pRMSNorm表示使用前p%的数据计算RMS值。k=n*p表示用于RMS计算的元素个数。实测中,使用6.25%的数据量可以收敛

\[ \overline{\operatorname{RMS}}(\mathbf{a})=\sqrt{\frac{1}{k} \sum_{i=1}^{k} a_{i}^{2}} \]

3.8 Post-LN 和 Pre-LN

Post-LN和Pre-LN的对比图

左边是原版Transformer的Post-LN,即将LN放在addition之后;右边是改进之后的Pre-LN,即把LN放在FFN和MHA之前。

一般认为,Post-Norm在残差之后做归一化,对参数正则化的效果更强,进而模型的收敛性也会更好;而Pre-Norm有一部分参数直接加在了后面,没有对这部分参数进行正则化,可以在反向时防止梯度爆炸或者梯度消失,大模型的训练难度大,因而使用Pre-Norm较多。

目前比较明确的结论是:同一设置之下,Pre Norm结构往往更容易训练,但最终效果通常不如Post Norm。Pre Norm更容易训练好理解,因为它的恒等路径更突出,但为什么它效果反而没那么好呢?这个是解释的链接:https://kexue.fm/archives/9009

4 PyTorch几类优化器

4.1 梯度下降 (Gradient Descent, GD):

  • 公式表达: \(\theta_{t+1}=\theta_t-\alpha \nabla J\left(\theta_t\right)\)

  • 其中, \(\theta_t\) 是第 \(t\) 步的模型参数, \(\alpha\) 是学习率, \(J\left(\theta_t\right)\) 是损失函数, \(\nabla J\left(\theta_t\right)\) 是损失函数关于参数的梯度。 ## 4.2 随机梯度下降 (SGD):

  • 公式表达: \(\theta_{t+1}=\theta_t-\alpha \nabla J\left(\theta_t ; x^{(i)}, y^{(i)}\right)\)

  • 与梯度下降不同的是, 每次更新时只随机选取一个样本 \(\left(x^{(i)}, y^{(i)}\right)\) 来计算梯度。

  • 优点:

        虽然SGD需要走很多步的样子,但是对梯度的要求很低(计算梯度快)。而对于引入噪声,大量的理论和实践工作证明,只要噪声不是特别大,SGD都能很好地收敛。应用大型数据集时,训练速度很快。比如每次从百万数据样本中,取几百个数据点,算一个SGD梯度,更新一下模型参数。相比于标准梯度下降法的遍历全部样本,每输入一个样本更新一次参数,要快得多。

    缺点:

        SGD在随机选择梯度的同时会引入噪声,使得权值更新的方向不一定正确。此外,SGD也没能单独克服局部最优解的问题。 ## 4.3 带动量的梯度下降 (Momentum):

  • 公式表达: \(v_{t+1}=\beta v_t+(1-\beta) \nabla J\left(\theta_t\right), \theta_{t+1}=\theta_t-\alpha v_{t+1}\)

  • \(v_t\) 是动量, \(\beta\) 是动量系数。

  • 动量主要解决SGD的两个问题:一是随机梯度的方法(引入的噪声);二是Hessian矩阵病态问题(可以理解为SGD在收敛过程中和正确梯度相比来回摆动比较大的问题)。

        简单理解:由于当前权值的改变会受到上一次权值改变的影响,类似于小球向下滚动的时候带上了惯性。这样可以加快小球向下滚动的速度。 ## 4.4 自适应学习率优化器 :

RMSprop (Root Mean Square Propagation):

  • 公式表达: \(v_{t+1}=\beta v_t+(1-\beta)\left(\nabla J\left(\theta_t\right)\right)^2, \theta_{t+1}=\theta_t-\frac{\alpha}{\sqrt{v_{t+1}}+\epsilon} \nabla J\left(\theta_t\right)\)

  • 更新权重的时候,使用除根号的方法,可以使较大的梯度大幅度变小,而较小的梯度小幅度变小,这样就可以使较大梯度方向上的波动小下来,那么整个梯度下降的过程中摆动就会比较小,就能设置较大的learning-rate,使得学习步子变大,达到加快学习的目的。

        在实际的应用中,权重W或者b往往是很多维度权重集合,就是多维的,在进行除根号操作中,会将其中大的维度的梯度大幅降低,不是说权重W变化趋势一样。

Adam (Adaptive Moment Estimation):

  • 公式表达: \(m_{t+1}=\beta_1 m_t+\left(1-\beta_1\right) \nabla J\left(\theta_t\right), v_{t+1}=\beta_2 v_t+\left(1-\beta_2\right)\left(\nabla J\left(\theta_t\right)\right)^2\), \(\theta_{t+1}=\theta_t-\frac{\alpha}{\sqrt{v_{t+1}}+\epsilon} \frac{m_{t+1}}{1-\beta_1^{t+1}}\)

  • 在这两种算法中, \(v_t\)\(m_t\) 分别是平方梯度的指数移动平均和梯度的指数移动平均。 \(\beta_1\)\(\beta_2\)是动量和梯度平方的衰减率。

  • Adam的优点主要在于:

    • 考虑历史步中的梯度更新信息,能够降低梯度更新噪声。
    • 此外经过偏差校正后,每一次迭代学习率都有个确定范围,使得参数比较平稳。

    但是Adam也有其自身问题:可能会对前期出现的特征过拟合,后期才出现的特征很难纠正前期的拟合效果。二者似乎都没法很好避免局部最优问题。

这些优化器之间的区别主要体现在对梯度的处理方式上, 以及对学习率的自适应调整。例如, 动量优化器可以加速收玫并帮助逃离局部最优点, 而自适应学习率优化器则可以根据梯度情况动态调整学习率, 适应不同参数的更新速度。

5.位置编码

5.1 绝对位置编码

不同于RNN、CNN等模型,对于Transformer模型来说,位置编码的加入是必不可少的,因为纯粹的Attention模块是无法捕捉输入顺序的,即无法区分不同位置的Token。为此我们大体有两个选择:

  1. 想办法将位置信息融入到输入中,这构成了绝对位置编码的一般做法;
  2. 想办法微调一下Attention结构,使得它有能力分辨不同位置的Token,这构成了相对位置编码的一般做法。

形式上来看,绝对位置编码是相对简单的一种方案,但即便如此,也不妨碍各路研究人员的奇思妙想,也有不少的变种。一般来说,绝对位置编码会加到输入中:在输入的第\(k\)个向量\(x_k\)中加入位置向量\(p_k\)变为\(x_k+p_k\),其中\(p_k\)只依赖于位置编号\(k\)

5.1.1 训练式

直接将位置编码当作可训练参数,比如最大长度为512,编码维度为768,那么就初始化一个512×768的矩阵作为位置向量,让它随着训练过程更新。

对于这种训练式的绝对位置编码,一般的认为它的缺点是没有外推性,即如果预训练最大长度为512的话,那么最多就只能处理长度为512的句子,再长就处理不了了。当然,也可以将超过512的位置向量随机初始化,然后继续微调。但笔者最近的研究表明,通过层次分解的方式,可以使得绝对位置编码能外推到足够长的范围,同时保持还不错的效果,细节请参考笔者之前的博文《层次分解位置编码,让BERT可以处理超长文本》。因此,其实外推性也不是绝对位置编码的明显缺点

5.1.2 三角式

三角函数式位置编码,一般也称为Sinusoidal位置编码,是Google的论文《Attention is All You Need》所提出来的一个显式解:

\[ \left\{\begin{array}{l}\boldsymbol{p}_{k, 2 i}=\sin \left(k / 10000^{2 i / d}\right) \\ \boldsymbol{p}_{k, 2 i+1}=\cos \left(k / 10000^{2 i / d}\right)\end{array}\right. \]

其中\(p_{k,2i}\),\(p_{k,2i+1}\)分别是位置\(k\)的编码向量的第\(2i\),\(2i+1\)个分量,\(d\)是位置向量的维度。

很明显,三角函数式位置编码的特点是有显式的生成规律,因此可以期望于它有一定的外推性。另外一个使用它的理由是:由于\(\sin (\alpha+\beta)=\sin \alpha \cos \beta+\cos \alpha \sin \beta\)以及\(\cos (\alpha+\beta)=\cos \alpha \cos \beta-\sin \alpha \sin \beta\),这表明位置\(\alpha+\beta\)的向量可以表示成位置\(\alpha\)和位置\(\beta\)的向量组合,这提供了表达相对位置信息的可能性。但很奇怪的是,现在我们很少能看到直接使用这种形式的绝对位置编码的工作,原因不详。

5.1.3 递归式

原则上来说,RNN模型不需要位置编码,它在结构上就自带了学习到位置信息的可能性(因为递归就意味着我们可以训练一个“数数”模型),因此,如果在输入后面先接一层RNN,然后再接Transformer,那么理论上就不需要加位置编码了。同理,我们也可以用RNN模型来学习一种绝对位置编码,比如从一个向量\(p_0\)出发,通过递归格式\(p_{k+1}=f(p_k)\)来得到各个位置的编码向量。

ICML 2020的论文《Learning to Encode Position for Transformer with Continuous Dynamical Model》把这个思想推到了极致,它提出了用微分方程(ODE)\(dp_t/dt=h(p_t,t)\)的方式来建模位置编码,该方案称之为FLOATER。显然,FLOATER也属于递归模型,函数\(h(p_t,t)\)可以通过神经网络来建模,因此这种微分方程也称为神经微分方程,关于它的工作最近也逐渐多了起来。

理论上来说,基于递归模型的位置编码也具有比较好的外推性,同时它也比三角函数式的位置编码有更好的灵活性(比如容易证明三角函数式的位置编码就是FLOATER的某个特解)。但是很明显,递归形式的位置编码牺牲了一定的并行性,可能会带速度瓶颈。

5.2 相对位置编码

相对位置并没有完整建模每个输入的位置信息,而是在算Attention的时候考虑当前位置与被Attention的位置的相对距离,由于自然语言一般更依赖于相对位置,所以相对位置编码通常也有着优秀的表现。对于相对位置编码来说,它的灵活性更大,更加体现出了研究人员的“天马行空”。

5.2.1经典式

相对位置编码起源于Google的论文《Self-Attention with Relative Position Representations》,华为开源的NEZHA模型也用到了这种位置编码,后面各种相对位置编码变体基本也是依葫芦画瓢的简单修改。

一般认为,相对位置编码是由绝对位置编码启发而来,考虑一般的带绝对位置编码的Attention:

\[ \left\{\begin{aligned} \boldsymbol{q}_{i} & =\left(\boldsymbol{x}_{i}+\boldsymbol{p}_{i}\right) \boldsymbol{W}_{Q} \\ \boldsymbol{k}_{j} & =\left(\boldsymbol{x}_{j}+\boldsymbol{p}_{j}\right) \boldsymbol{W}_{K} \\ \boldsymbol{v}_{j} & =\left(\boldsymbol{x}_{j}+\boldsymbol{p}_{j}\right) \boldsymbol{W}_{V} \\ a_{i, j} & =\operatorname{softmax}\left(\boldsymbol{q}_{i} \boldsymbol{k}_{j}^{\top}\right) \\ \boldsymbol{o}_{i} & =\sum_{j} a_{i, j} \boldsymbol{v}_{j}\end{aligned}\right. \]

其中softmax对j那一维归一化,这里的向量都是指行向量。我们初步展开\(q_ik^T_j\)

\[ \boldsymbol{q}_{i} \boldsymbol{k}_{j}^{\top}=\left(\boldsymbol{x}_{i}+\boldsymbol{p}_{i}\right) \boldsymbol{W}_{Q} \boldsymbol{W}_{K}^{\top}\left(\boldsymbol{x}_{j}+\boldsymbol{p}_{j}\right)^{\top}=\left(\boldsymbol{x}_{i} \boldsymbol{W}_{Q}+\boldsymbol{p}_{i} \boldsymbol{W}_{Q}\right)\left(\boldsymbol{W}_{K}^{\top} \boldsymbol{x}_{j}^{\top}+\boldsymbol{W}_{K}^{\top} \boldsymbol{p}_{j}^{\top}\right) \]

为了引入相对位置信息,Google把第一项位置去掉,第二项\(p_jW_K\)改为二元位置向量\(R^K_{i,j}\),变成

\[ a_{i, j}=\operatorname{softmax}\left(\boldsymbol{x}_{i} \boldsymbol{W}_{Q}\left(\boldsymbol{x}_{j} \boldsymbol{W}_{K}+\boldsymbol{R}_{i, j}^{K}\right)^{\top}\right) \]

以及\(\boldsymbol{o}_{i}=\sum_{j} a_{i, j} \boldsymbol{v}_{j}=\sum_{j} a_{i, j}\left(\boldsymbol{x}_{j} \boldsymbol{W}_{V}+\boldsymbol{p}_{j} \boldsymbol{W}_{V}\right)\)中的中的\(p_jW_V\)换成\(R^V_{i,j}\)

\[ \boldsymbol{o}_{i}=\sum_{j} a_{i, j}\left(\boldsymbol{x}_{j} \boldsymbol{W}_{V}+\boldsymbol{R}_{i, j}^{V}\right) \]

所谓相对位置,是将本来依赖于二元坐标\((i,j)\)的向量\(R^K_{i,j}\),\(R^V_{i,j}\),改为只依赖于相对距离\(i−j\),并且通常来说会进行截断,以适应不同任意的距离:

\[ \begin{array}{l}\boldsymbol{R}_{i, j}^{K}=\boldsymbol{p}_{K}\left[\operatorname{clip}\left(i-j, p_{\min }, p_{\max }\right)\right] \\ \boldsymbol{R}_{i, j}^{V}=\boldsymbol{p}_{V}\left[\operatorname{clip}\left(i-j, p_{\min }, p_{\max }\right)\right]\end{array} \]

这样一来,只需要有限个位置编码,就可以表达出任意长度的相对位置(因为进行了截断),不管\(p_K\),\(p_V\)是选择可训练式的还是三角函数式的,都可以达到处理任意长度文本的需求。

5.2.2 XLNET式

XLNET式位置编码其实源自Transformer-XL的论文《Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context》,只不过因为使用了Transformer-XL架构的XLNET模型并在一定程度上超过了BERT后,Transformer-XL才算广为人知,因此这种位置编码通常也被冠以XLNET之名。

XLNET式位置编码源于对上述\(q_ik^T_j\)的完全展开:

\[ \boldsymbol{q}_{i} \boldsymbol{k}_{j}^{\top}=\boldsymbol{x}_{i} \boldsymbol{W}_{Q} \boldsymbol{W}_{K}^{\top} \boldsymbol{x}_{j}^{\top}+\boldsymbol{x}_{i} \boldsymbol{W}_{Q} \boldsymbol{W}_{K}^{\top} \boldsymbol{p}_{j}^{\top}+\boldsymbol{p}_{i} \boldsymbol{W}_{Q} \boldsymbol{W}_{K}^{\top} \boldsymbol{x}_{j}^{\top}+\boldsymbol{p}_{i} \boldsymbol{W}_{Q} \boldsymbol{W}_{K}^{\top} \boldsymbol{p}_{j}^{\top} \]

Transformer-XL的做法很简单,直接将\(p_j\)替换为相对位置向量\(R_{i−j}\),至于两个\(p_i\),则干脆替换为两个可训练的向量\(u,v\)

\[ \boldsymbol{x}_{i} \boldsymbol{W}_{Q} \boldsymbol{W}_{K}^{\top} \boldsymbol{x}_{j}^{\top}+\boldsymbol{x}_{i} \boldsymbol{W}_{Q} \boldsymbol{W}_{K}^{\top} \boldsymbol{R}_{i-j}^{\top}+u \boldsymbol{W}_{Q} \boldsymbol{W}_{K}^{\top} \boldsymbol{x}_{j}^{\top}+\boldsymbol{v} \boldsymbol{W}_{Q} \boldsymbol{W}_{K}^{\top} \boldsymbol{R}_{i-j}^{\top} \]

该编码方式中的\(R_{i−j}\)没有像经典模型那样进行截断,而是直接用了Sinusoidal式的生成方案,由于\(R_{i−j}\)的编码空间与\(x_j\)不一定相同,所以\(R_{i−j}\)前面的\(W^T_K\)换了另一个独立的矩阵\(W^T_{K,R}\),还有\(uW_Q\)\(vW_Q\)可以直接合并为单个\(u\)\(v\),所以最终使用的式子是:

\[ \boldsymbol{x}_{i} \boldsymbol{W}_{Q} \boldsymbol{W}_{K}^{\top} \boldsymbol{x}_{j}^{\top}+\boldsymbol{x}_{i} \boldsymbol{W}_{Q} \boldsymbol{W}_{K, R}^{\top} \boldsymbol{R}_{i-j}^{\top}+\boldsymbol{u} \boldsymbol{W}_{K}^{\top} \boldsymbol{x}_{j}^{\top}+\boldsymbol{v} \boldsymbol{W}_{K, R}^{\top} \boldsymbol{R}_{i-j}^{\top} \]

此外,\(v_j\)上的位置偏置就直接去掉了,即直接令\(\boldsymbol{o}_{i}=\sum_{j} a_{i, j} \boldsymbol{x}_{j} \boldsymbol{W}_{V}\)。似乎从这个工作开始,后面的相对位置编码都只加到Attention矩阵上去,而不加到\(v_j\)上去了。

5.2.3 T5式

T5模型出自文章《Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer》,里边用到了一种更简单的相对位置编码。思路依然源自\(q_ik^T_j\)展开式,如果非要分析每一项的含义,那么可以分别理解为“输入-输入”、“输入-位置”、“位置-输入”、“位置-位置”四项注意力的组合。如果我们认为输入信息与位置信息应该是独立(解耦)的,那么它们就不应该有过多的交互,所以“输入-位置”、“位置-输入”两项Attention可以删掉,而\(\boldsymbol{p}_{i} \boldsymbol{W}_{Q} \boldsymbol{W}_{K}^{\top} \boldsymbol{p}_{j}^{\top}\)实际上只是一个只依赖于\((i,j)\)的标量,我们可以直接将它作为参数训练出来,即简化为:

\[ \boldsymbol{x}_{i} \boldsymbol{W}_{Q} \boldsymbol{W}_{K}^{\top} \boldsymbol{x}_{j}^{\top}+\boldsymbol{\beta}_{i, j} \]

说白了,它仅仅是在Attention矩阵的基础上加一个可训练的偏置项而已,而跟XLNET式一样,在\(v_j\)上的位置偏置则直接被去掉了。包含同样的思想的还有微软在ICLR 2021的论文《Rethinking Positional Encoding in Language Pre-training》中提出的TUPE位置编码。

比较“别致”的是,不同于常规位置编码对将\(\beta_{i, j}\)视为\(i−j\)的函数并进行截断的做法,T5对相对位置进行了一个“分桶”处理,即相对位置是\(i−j\)的位置实际上对应的是\(f(i−j)\)位置,映射关系如下:

\(i-j\) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
\(f(i-j)\) 0 1 2 3 4 5 6 7 8 8 8 8 9 9 9 9
\(i-j\) 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ...
\(f(i-j)\) 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 ...

这个设计的思路其实也很直观,就是比较邻近的位置(0~7),需要比较得精细一些,所以给它们都分配一个独立的位置编码,至于稍远的位置(比如8~11),我们不用区分得太清楚,所以它们可以共用一个位置编码,距离越远,共用的范围就可以越大,直到达到指定范围再clip。

5.2.4 DeBERTa式

DeBERTa也是微软搞的,去年6月就发出来了,论文为《DeBERTa: Decoding-enhanced BERT with Disentangled Attention》,最近又小小地火了一把,一是因为它正式中了ICLR 2021,二则是它登上SuperGLUE的榜首,成绩稍微超过了T5。

其实DeBERTa的主要改进也是在位置编码上,同样还是从\(q_ik^T_j\)展开式出发,T5是干脆去掉了第2、3项,只保留第4项并替换为相对位置编码,而DeBERTa则刚刚相反,它扔掉了第4项,保留第2、3项并且替换为相对位置编码(果然,科研就是枚举所有的排列组合看哪个最优):

\[ \boldsymbol{q}_{i} \boldsymbol{k}_{j}^{\top}=\boldsymbol{x}_{i} \boldsymbol{W}_{Q} \boldsymbol{W}_{K}^{\top} \boldsymbol{x}_{j}^{\top}+\boldsymbol{x}_{i} \boldsymbol{W}_{Q} \boldsymbol{W}_{K}^{\top} \boldsymbol{R}_{i, j}^{\top}+\boldsymbol{R}_{j, i} \boldsymbol{W}_{Q} \boldsymbol{W}_{K}^{\top} \boldsymbol{x}_{j}^{\top} \]

不过,DeBERTa比较有意思的地方,是提供了使用相对位置和绝对位置编码的一个新视角,它指出NLP的大多数任务可能都只需要相对位置信息,但确实有些场景下绝对位置信息更有帮助,于是它将整个模型分为两部分来理解。以Base版的MLM预训练模型为例,它一共有13层,前11层只是用相对位置编码,这部分称为Encoder,后面2层加入绝对位置信息,这部分它称之为Decoder,还弄了个简称EMD(Enhanced Mask Decoder);至于下游任务的微调截断,则是使用前11层的Encoder加上1层的Decoder来进行。

5.3 旋转位置编码 RoPE篇

RoPE旋转位置编码是苏神提出来的一种相对位置编码,之前主要用在自研的语言模型roformer上,后续谷歌Palm和meta的LLaMA等都是采用此位置编码,通过复数形式来对于三角式绝对位置编码的改进。有一些同学可能没看懂苏神的公式推导,我这里来帮助大家推理理解下公式。

通过线性attention演算,现在q和k向量中引入绝对位置信息:

\[ \tilde{\boldsymbol{q}}_{m}=\boldsymbol{f}(\boldsymbol{q}, m), \quad \tilde{\boldsymbol{k}}_{n}=\boldsymbol{f}(\boldsymbol{k}, n) \]

但是需要实现相对位置编码的话,需要显式融入相对。attention运算中q和k会进行内积,所以考虑在进行向量内积时考虑融入相对位置。所以假设成立恒等式:

\[ \langle\boldsymbol{f}(\boldsymbol{q}, m), \boldsymbol{f}(\boldsymbol{k}, n)\rangle=g(\boldsymbol{q}, \boldsymbol{k}, m-n) \]

其中m-n包含着token之间的相对位置信息。

给上述恒等式计算设置初始条件,例如\(f(q,0)=q\)\(f(k,0)=k\)

求解过程使用复数方式求解

将内积使用复数形式表示:

\[ \langle\boldsymbol{q}, \boldsymbol{k}\rangle=\operatorname{Re}\left[\boldsymbol{q} \boldsymbol{k}^{*}\right] \]

转化上面内积公式可得:

\[ \operatorname{Re}\left[\boldsymbol{f}(\boldsymbol{q}, m) \boldsymbol{f}^{*}(\boldsymbol{k}, n)\right]=g(\boldsymbol{q}, \boldsymbol{k}, m-n) \]

假设等式两边都存在复数形式,则有下式:

\[ \boldsymbol{f}(\boldsymbol{q}, m) \boldsymbol{f}^{*}(\boldsymbol{k}, n)=\boldsymbol{g}(\boldsymbol{q}, \boldsymbol{k}, m-n) \]

将两边公式皆用复数指数形式表示:

存在\(r e^{\theta \mathrm{j}}=r \cos \theta+r \sin \theta \mathrm{j}\),即任意复数\(z\)可以表示为\(\boldsymbol{z}=r e^{\theta \mathrm{j}}\),其中\(r\)为复数的模,\(\theta\)为幅角。

\[ \begin{aligned} \boldsymbol{f}(\boldsymbol{q}, m) & =R_{f}(\boldsymbol{q}, m) e^{\mathrm{i} \Theta_{f}(\boldsymbol{q}, m)} \\ \boldsymbol{f}(\boldsymbol{k}, n) & =R_{f}(\boldsymbol{k}, n) e^{\mathrm{i} \Theta_{f}(\boldsymbol{k}, n)} \\ \boldsymbol{g}(\boldsymbol{q}, \boldsymbol{k}, m-n) & =R_{g}(\boldsymbol{q}, \boldsymbol{k}, m-n) e^{\mathrm{i} \Theta_{g}(\boldsymbol{q}, \boldsymbol{k}, m-n)}\end{aligned} \]

由于带入上面方程中\(f(k,n)\)带*是共轭复数,所以指数形式应该是\(e^{-x}\)形式,带入上式公式可得方程组:

\[ \begin{aligned} R_{f}(\boldsymbol{q}, m) R_{f}(\boldsymbol{k}, n) & =R_{g}(\boldsymbol{q}, \boldsymbol{k}, m-n) \\ \Theta_{f}(\boldsymbol{q}, m)-\Theta_{f}(\boldsymbol{k}, n) & =\Theta_{g}(\boldsymbol{q}, \boldsymbol{k}, m-n)\end{aligned} \]

第一个方程带入条件\(m=n\)化简可得:

\[ R_{f}(\boldsymbol{q}, m) R_{f}(\boldsymbol{k}, m)=R_{g}(\boldsymbol{q}, \boldsymbol{k}, 0)=R_{f}(\boldsymbol{q}, 0) R_{f}(\boldsymbol{k}, 0)=\|\boldsymbol{q}\|\|\boldsymbol{k}\| \]

\[ R_{f}(\boldsymbol{q}, m)=\|\boldsymbol{q}\|, R_{f}(\boldsymbol{k}, m)=\|\boldsymbol{k}\| \]

从上式可以看出来复数\(f(q,m)\)\(f(k,m)\)\(m\)取值关系不大。

第二个方程带入\(m=n\)化简可得:

\[ \Theta_{f}(\boldsymbol{q}, m)-\Theta_{f}(\boldsymbol{k}, m)=\Theta_{g}(\boldsymbol{q}, \boldsymbol{k}, 0)=\Theta_{f}(\boldsymbol{q}, 0)-\Theta_{f}(\boldsymbol{k}, 0)=\Theta(\boldsymbol{q})-\Theta(\boldsymbol{k}) \]

上式公式变量两边挪动下得到:

\[ \Theta_{f}(\boldsymbol{q}, m)-\Theta_{f}(\boldsymbol{k}, m)=\Theta_{g}(\boldsymbol{q}, \boldsymbol{k}, 0)=\Theta_{f}(\boldsymbol{q}, 0)-\Theta_{f}(\boldsymbol{k}, 0)=\Theta(\boldsymbol{q})-\Theta(\boldsymbol{k}) \]

其中上式结果相当于m是自变量,结果是与m相关的值,假设为 \(\varphi(m)\),即\(\Theta_{f}(\boldsymbol{q}, m)=\Theta(\boldsymbol{q})+\varphi(m)\)

n假设为m的前一个token,则可得n=m-1,带入上上个式子可得:

\[ \varphi(m)-\varphi(m-1)=\Theta_{g}(\boldsymbol{q}, \boldsymbol{k}, 1)+\Theta(\boldsymbol{k})-\Theta(\boldsymbol{q}) \]

\(\varphi(m)\)是等差数列,假设等式右边为 \(\theta\) ,则mm-1位置的公差就是为\(\theta\),可推得 \(\varphi(m)=m \theta\)

得到二维情况下用复数表示的RoPE:

\[ \boldsymbol{f}(\boldsymbol{q}, m)=R_{f}(\boldsymbol{q}, m) e^{\mathrm{i} \Theta_{f}(\boldsymbol{q}, m)}=\|q\| e^{\mathrm{i}(\Theta(\boldsymbol{q})+m \theta)}=\boldsymbol{q} e^{\mathrm{i} m \theta} \]

矩阵形式是:

\[ \boldsymbol{f}(\boldsymbol{q}, m)=\left(\begin{array}{cc}\cos m \theta & -\sin m \theta \\ \sin m \theta & \cos m \theta\end{array}\right)\left(\begin{array}{l}q_{0} \\ q_{1}\end{array}\right) \]

公式最后还会采用三角式一样的远程衰减,来增加周期性函数外推位置差异性。

\[ \left(\boldsymbol{W}_{m} \boldsymbol{q}\right)^{\top}\left(\boldsymbol{W}_{n} \boldsymbol{k}\right)=\operatorname{Re}\left[\sum_{i=0}^{d / 2-1} \boldsymbol{q}_{[2 i: 2 i+1]} \boldsymbol{k}_{[2 i: 2 i+1]}^{*} e^{\mathrm{i}(m-n) \theta_{i}}\right] \]

5.4 几种位置编码方式总结

5.4.1 绝对位置编码

  • 最原始的正余弦位置编码(即sinusoidal位置编码)是一种绝对位置编码,但从其原理中的正余弦的和差化积公式来看,引入的其实也是相对位置编码。
  • 优势: 实现简单,可预先计算好,不用参与训练,速度快。
  • 劣势: 没有外推性,即如果预训练最大长度为512的话,那么最多就只能处理长度为512的句子,再长就处理不了了。当然,也可以将超过512的位置向量随机初始化,然后继续微调。

5.4.2 相对位置编码

  • 经典相对位置编码RPR式的讲解可看我的博客:相对位置编码之RPR式:《Self-Attention with Relative Position Representations》论文笔记 【在k, v中注入相对位置信息】
  • 优势: 直接地体现了相对位置信号,效果更好。具有外推性,处理长文本能力更强。

5.4.3 RoPE

  • RoPE通过绝对位置编码的方式实现相对位置编码,综合了绝对位置编码和相对位置编码的优点。
  • 主要就是对attention中的q, k向量注入了绝对位置信息,然后用更新的q,k向量做attention中的内积就会引入相对位置信息了

6 激活函数

6.1 介绍一下 FFN 块 计算公式?

FFN(Feed-Forward Network)块是Transformer模型中的一个重要组成部分,接受自注意力子层的输出作为输入,并通过一个带有 Relu 激活函数的两层全连接网络对输入进行更加复杂的非线性变换。实验证明,这一非线性变换会对模型最终的性能产生十分 重要的影响。

FFN由两个全连接层(即前馈神经网络)和一个激活函数组成。下面是FFN块的计算公式:

\[ \operatorname{FFN}(\boldsymbol{x})=\operatorname{Relu}\left(\boldsymbol{x} \boldsymbol{W}_{1}+\boldsymbol{b}_{1}\right) \boldsymbol{W}_{2}+\boldsymbol{b}_{2} \]

假设输入是一个向量 \(x\),FFN块的计算过程如下:

  1. 第一层全连接层(线性变换):\(z = xW1 + b1\) 其中,W1 是第一层全连接层的权重矩阵,b1 是偏置向量。
  2. 激活函数:\(a = g(z)\) 其中,g() 是激活函数,常用的激活函数有ReLU(Rectified Linear Unit)等。
  3. 第二层全连接层(线性变换):\(y = aW2 + b2\) 其中,W2 是第二层全连接层的权重矩阵,b2 是偏置向量。

增大前馈子层隐状态的维度有利于提 升最终翻译结果的质量,因此,前馈子层隐状态的维度一般比自注意力子层要大。

需要注意的是,上述公式中的 W1、b1、W2、b2 是FFN块的可学习参数,它们会通过训练过程进行学习和更新。

6.2 介绍一下 GeLU 计算公式?

GeLU(Gaussian Error Linear Unit)是一种激活函数,常用于神经网络中的非线性变换。它在Transformer模型中广泛应用于FFN(Feed-Forward Network)块。下面是GeLU的计算公式:

假设输入是一个标量 x,GeLU的计算公式如下:

\[ GeLU(x) = 0.5 \times x \times (1 + tanh(\sqrt{\frac{2}{\pi}} \times (x + 0.044715 \times x^3))) \]

其中,tanh()是双曲正切函数,sqrt() 是平方根函数,$ $是圆周率。

1
2
3
4
import numpy as np

def GELU(x):
return 0.5 * x * (1 + np.tanh(np.sqrt(2 / np.pi) * (x + 0.044715 * np.power(x, 3))))
几类激活函数的表示

相对于 Sigmoid 和 Tanh 激活函数,ReLU 和 GeLU 更为准确和高效,因为它们在神经网络中的梯度消失问题上表现更好。而 ReLU 和 GeLU 几乎没有梯度消失的现象,可以更好地支持深层神经网络的训练和优化。

ReLU 和 GeLU 的区别在于形状和计算效率。ReLU 是一个非常简单的函数,仅仅是输入为负数时返回0,而输入为正数时返回自身,从而仅包含了一次分段线性变换。但是,ReLU 函数存在一个问题,就是在输入为负数时,输出恒为0,这个问题可能会导致神经元死亡,从而降低模型的表达能力。GeLU 函数则是一个连续的 S 形曲线,介于 Sigmoid 和 ReLU 之间,形状比 ReLU 更为平滑,可以在一定程度上缓解神经元死亡的问题。不过,由于 GeLU 函数中包含了指数运算等复杂计算,所以在实际应用中通常比 ReLU 慢。

总之,ReLU 和 GeLU 都是常用的激活函数,它们各有优缺点,并适用于不同类型的神经网络和机器学习问题。一般来说,ReLU 更适合使用在卷积神经网络(CNN)中,而 GeLU 更适用于全连接网络(FNN)。

6.3 介绍一下 Swish 计算公式?

Swish是一种激活函数,它在深度学习中常用于神经网

络的非线性变换。Swish函数的计算公式如下: \[ Swish(x) = x \times sigmoid(\beta * x) \]

其中,\(sigmoid()\) 是Sigmoid函数,\(x\) 是输入,\(\beta\) 是一个可调节的超参数。

Swish图像随参数变化的趋势

Swish函数的特点是在接近零的区域表现得类似于线性函数,而在远离零的区域则表现出非线性的特性。相比于其他常用的激活函数(如ReLU、tanh等),Swish函数在某些情况下能够提供更好的性能和更快的收敛速度。

Swish函数的设计灵感来自于自动搜索算法,它通过引入一个可调节的超参数来增加非线性程度。当beta为0时,Swish函数退化为线性函数;当beta趋近于无穷大时,Swish函数趋近于ReLU函数。

需要注意的是,Swish函数相对于其他激活函数来说计算开销较大,因为它需要进行Sigmoid运算。因此,在实际应用中,也可以根据具体情况选择其他的激活函数来代替Swish函数。

6.4 介绍一下使用 GLU 线性门控单元的 FFN 块 计算公式?

使用GLU(Gated Linear Unit)线性门控单元的FFN(Feed-Forward Network)块是Transformer模型中常用的结构之一。它通过引入门控机制来增强模型的非线性能力。下面是使用GLU线性门控单元的FFN块的计算公式:

假设输入是一个向量 x,GLU线性门控单元的计算公式如下:

\[ GLU(x) = x * sigmoid(W_1 * x) \]

其中,\(sigmoid()\) 是Sigmoid函数,\(W_1\) 是一个可学习的权重矩阵。

在公式中,首先将输入向量 x 通过一个全连接层(线性变换)得到一个与 x 维度相同的向量,然后将该向量通过Sigmoid函数进行激活。这个Sigmoid函数的输出称为门控向量,用来控制输入向量 x 的元素是否被激活。最后,将门控向量与输入向量 x 逐元素相乘,得到最终的输出向量。

GLU线性门控单元的特点是能够对输入向量进行选择性地激活,从而增强模型的表达能力。它在Transformer模型的编码器和解码器中广泛应用,用于对输入向量进行非线性变换和特征提取。

需要注意的是,GLU线性门控单元的计算复杂度较高,可能会增加模型的计算开销。因此,在实际应用中,也可以根据具体情况选择其他的非线性变换方式来代替GLU线性门控单元。

6.5 介绍一下 使用 GeLU 的 GLU 块 计算公式?

使用GeLU作为激活函数的GLU块的计算公式如下:

\[ GLU(x) = x * GeLU(W_1 * x) \]

其中,GeLU()是Gaussian Error Linear Unit的激活函数,W_1是一个可学习的权重矩阵。

在公式中,首先将输入向量 x 通过一个全连接层(线性变换)得到一个与 x 维度相同的向量,然后将该向量作为输入传递给GeLU激活函数进行非线性变换。最后,将GeLU激活函数的输出与输入向量 x 逐元素相乘,得到最终的输出向量。

GeLU激活函数的计算公式如下:

\[ GeLU(x) = 0.5 \times x \times (1 + tanh(\sqrt{\frac{2}{\pi}} \times (x + 0.044715 \times x^3))) \]

其中,tanh()是双曲正切函数,sqrt() 是平方根函数,$ $是圆周率。

在公式中,GeLU函数首先对输入向量 x 进行一个非线性变换,然后通过一系列的数学运算得到最终的输出值。

使用GeLU作为GLU块的激活函数可以增强模型的非线性能力,并在某些情况下提供更好的性能和更快的收敛速度。这种结构常用于Transformer模型中的编码器和解码器,用于对输入向量进行非线性变换和特征提取。

需要注意的是,GLU块和GeLU激活函数是两个不同的概念,它们在计算公式和应用场景上有所区别。在实际应用中,可以根据具体情况选择合适的激活函数来代替GeLU或GLU。

6.6 介绍一下 使用 Swish 的 GLU 块 计算公式?

使用Swish作为激活函数的GLU块的计算公式如下:

\[ GLU(x) = x * sigmoid(W_1 * x) \]

其中,\(sigmoid()\) 是Sigmoid函数,\(W_1\) 是一个可学习的权重矩阵。

在公式中,首先将输入向量 x 通过一个全连接层(线性变换)得到一个与 x 维度相同的向量,然后将该向量通过Sigmoid函数进行激活。这个Sigmoid函数的输出称为门控向量,用来控制输入向量 x 的元素是否被激活。最后,将门控向量与输入向量 x 逐元素相乘,得到最终的输出向量。

Swish激活函数的计算公式如下:

\[ Swish(x) = x \times sigmoid(\beta * x) \]

其中,\(sigmoid()\) 是Sigmoid函数,\(x\) 是输入,\(\beta\) 是一个可调节的超参数。

在公式中,Swish函数首先对输入向量 x 进行一个非线性变换,然后通过Sigmoid函数进行激活,并将该激活结果与输入向量 x 逐元素相乘,得到最终的输出值。

使用Swish作为GLU块的激活函数可以增强模型的非线性能力,并在某些情况下提供更好的性能和更快的收敛速度。GLU块常用于Transformer模型中的编码器和解码器,用于对输入向量进行非线性变换和特征提取。

需要注意的是,GLU块和Swish激活函数是两个不同的概念,它们在计算公式和应用场景上有所区别。在实际应用中,可以根据具体情况选择合适的激活函数来代替Swish或GLU。

7 Tokenize相关

7.1 总览

分词方法 特点 被提出的时间 典型模型
BPE 采用合并规则,可以适应未知词 2016年 GPT-2、RoBERTa
WordPiece 采用逐步拆分的方法,可以适应未知词 2016年 BERT
Unigram LM 采用无序语言模型,训练速度快 2018年 XLM
SentencePiece 采用汉字、字符和子词三种分词方式,支持多语言 2018年 T5、ALBERT

背景与基础

在使用GPT BERT模型输入词语常常会先进行tokenize ,tokenize的目标是把输入的文本流,切分成一个个子串,每个子串相对有完整的语义,便于学习embedding表达和后续模型的使用。

tokenize有三种粒度:word/subword/char

  • word/词,词,是最自然的语言单元。对于英文等自然语言来说,存在着天然的分隔符,如空格或一些标点符号等,对词的切分相对容易。但是对于一些东亚文字包括中文来说,就需要某种分词算法才行。顺便说一下,Tokenizers库中,基于规则切分部分,采用了spaCy和Moses两个库。如果基于词来做词汇表,由于长尾现象的存在,这个词汇表可能会超大。像Transformer XL库就用到了一个26.7万个单词的词汇表。这需要极大的embedding matrix才能存得下。embedding matrix是用于查找取用token的embedding vector的。这对于内存或者显存都是极大的挑战。常规的词汇表,一般大小不超过5万
  • char/字符,即最基本的字符,如英语中的'a','b','c'或中文中的'你','我','他'等。而一般来讲,字符的数量是少量有限的。这样做的问题是,由于字符数量太小,我们在为每个字符学习嵌入向量的时候,每个向量就容纳了太多的语义在内,学习起来非常困难。
  • subword/子词级,它介于字符和单词之间。比如说'Transformers'可能会被分成'Transform'和'ers'两个部分。这个方案平衡了词汇量和语义独立性,是相对较优的方案。它的处理原则是,常用词应该保持原状,生僻词应该拆分成子词以共享token压缩空间

7.2 常用的tokenize算法

最常用的三种tokenize算法:BPE(Byte-Pair Encoding),WordPiece和SentencePiece

7.2.1 BPE(Byte-Pair Encoding)

BPE,即字节对编码。其核心思想在于将最常出现的子词对合并,直到词汇表达到预定的大小时停止

BPE是一种基于数据压缩算法的分词方法。它通过不断地合并出现频率最高的字符或者字符组合,来构建一个词表。具体来说,BPE的运算过程如下:

  1. 将所有单词按照字符分解为字母序列。例如:“hello”会被分解为["h","e","l","l","o"]。
  2. 统计每个字母序列出现的频率,将频率最高的序列合并为一个新序列。
  3. 重复第二步,直到达到预定的词表大小或者无法再合并。

词表大小通常先增加后减小

每次合并后词表可能出现3种变化:

  • +1,表明加入合并后的新字词,同时原来的2个子词还保留(2个字词不是完全同时连续出现)
  • +0,表明加入合并后的新字词,同时原来的2个子词中一个保留,一个被消解(一个字词完全随着另一个字词的出现而紧跟着出现)
  • -1,表明加入合并后的新字词,同时原来的2个子词都被消解(2个字词同时连续出现)

7.2.2 WordPiece

WordPiece,从名字好理解,它是一种子词粒度的tokenize算法subword tokenization algorithm,很多著名的Transformers模型,比如BERT/DistilBERT/Electra都使用了它。

wordpiece算法可以看作是BPE的变种。不同的是,WordPiece基于概率生成新的subword而不是下一最高频字节对。WordPiece算法也是每次从词表中选出两个子词合并成新的子词。BPE选择频数最高的相邻子词合并,而WordPiece选择使得语言模型概率最大的相邻子词加入词表。即它每次合并的两个字符串A和B,应该具有最大的\(\frac{P(A B)}{P(A) P(B)}\)值。合并AB之后,所有原来切成A+B两个tokens的就只保留AB一个token,整个训练集上最大似然变化量与\(\frac{P(A B)}{P(A) P(B)}\)成正比。

\[ \log P(S)=\sum_{i=1}^{n} \log P\left(t_{i}\right) \]

\[ S=\left[t_{1}, t_{2}, t_{3}, \ldots, t_{n}\right] \]

比如说 $ P(ed) \(的概率比\)P(e) + P(d)$ 单独出现的概率更大,可能比他们具有最大的互信息值,也就是两子词在语言模型上具有较强的关联性。

那wordPiece和BPE的区别:

  • BPE: apple 当词表有appl 和 e的时候,apple优先编码为 appl和e(即使原始预料中 app 和 le 的可能性更大)
  • wordPiece:根据原始语料, app和le的概率更大

7.2.3 Unigram

与BPE或者WordPiece不同,Unigram的算法思想是从一个巨大的词汇表出发,再逐渐删除trim down其中的词汇,直到size满足预定义。

初始的词汇表可以采用所有预分词器分出来的词,再加上所有高频的子串

每次从词汇表中删除词汇的原则是使预定义的损失最小。训练时,计算loss的公式为:

\[ Loss =-\sum_{i=1}^{N} \log \left(\sum_{x \in S\left(x_{i}\right)} p(x)\right) \]

假设训练文档中的所有词分别为\(x_{1} ; x_{2}, \ldots, x_{N}\),而每个词tokenize的方法是一个集合\(S\left(x_{i}\right)\)

当一个词汇表确定时,每个词tokenize的方法集合\(S\left(x_{i}\right)\)就是确定的,而每种方法对应着一个概率\(P(x)\).

如果从词汇表中删除部分词,则某些词的tokenize的种类集合就会变少,log( *)中的求和项就会减少,从而增加整体loss。

Unigram算法每次会从词汇表中挑出使得loss增长最小的10%~20%的词汇来删除。

一般Unigram算法会与SentencePiece算法连用。

7.2.4 SentencePiece

SentencePiece,顾名思义,它是把一个句子看作一个整体,再拆成片段,而没有保留天然的词语的概念。一般地,它把空格space也当作一种特殊字符来处理,再用BPE或者Unigram算法来构造词汇表

比如,XLNetTokenizer就采用了_来代替空格,解码的时候会再用空格替换回来。

目前,Tokenizers库中,所有使用了SentencePiece的都是与Unigram算法联合使用的,比如ALBERT、XLNet、Marian和T5.

8 长度外推问题篇

8.1 长度外推问题

大模型的外推性问题是指大模型在训练时和预测时的输入长度不一致,导致模型的泛化能力下降的问题。在目前的大模型中,一般指的是超出预训练设置的上下文长度时,依旧保持良好推理效果的能力。

长度外推性=train short, test long

train short:1)受限于训练成本;2)大部分文本的长度不会特别长,训练时的max_length特别特别大其实意义不大(长尾)。

test long:这里long是指比训练时的max_length长,希望不用微调就能在长文本上也有不错的效果。

8.2 长度外推问题的解决方法有哪些?

(1)进制表示

我们将整数n以一个三维向量[a,b,c]来输入,a,b,c分别是n的百位、十位、个位。这样,我们既缩小了数字的跨度,又没有缩小相邻数字的差距,代价了增加了输入的维度——刚好,神经网络擅长处理高维数据。

如果想要进一步缩小数字的跨度,我们还可以进一步缩小进制的基数,如使用8进制、6进制甚至2进制,代价是进一步增加输入的维度。

(2)直接外推

简单来说,假如原来位置编码用三维向量表示,那外插就是直接增加一维。

可以提前预留多几维,训练阶段设为0,推理阶段直接改为其他数字,这就是外推(Extrapolation)。

长度外推问题的示意图

然而,训练阶段预留的维度一直是0,如果推理阶段改为其他数字,效果不见得会好,因为模型对没被训练过的情况不一定具有适应能力。也就是说,由于某些维度的训练数据不充分,所以直接进行外推通常会导致模型的性能严重下降

(3)线性插值

就是将2000以内压缩到1000以内,比如通过除以2,1749就变成了874.5,然后转为三维向量[8,7,4.5]输入到原来的模型中。从绝对数值来看,新的[7,4,9]实际上对应的是1498,是原本对应的2倍,映射方式不一致;从相对数值来看,原本相邻数字的差距为1,现在是0.5,最后一个维度更加“拥挤”。所以,做了内插修改后,通常都需要微调训练,以便模型重新适应拥挤的映射关系。

线性插值的表示

(4)进制转换

有没有不用新增维度,又能保持相邻差距的方案呢?进制转换!三个数字的10进制编码可以表示0~999,如果是16进制呢?它最大可以表示163−1=4095>1999。所以,只需要转到16进制,如1749变为[6,13,5],那么三维向量就可以覆盖目标范围,代价是每个维度的数字从0~9变为0~15。

进制转换的示意图

其他文章参考

深度学习八股文,这里将会收集深度学习中的基本概念和常见的问题,以下是主要的参考文章

LLAMA2结构https://blog.csdn.net/sikh_0529/article/details/134375318

旋转位置嵌入https://www.zhihu.com/tardis/zm/art/647109286?source_id=1005

bert模型细节 https://www.zhihu.com/question/534763354

为什么Bert三个embedding可以相加 https://www.zhihu.com/question/374835153/answer/1080315948

Qlora https://zhuanlan.zhihu.com/p/618894919

RLHF https://zhuanlan.zhihu.com/p/631238431

LLAMA2 colabhttps://zhuanlan.zhihu.com/p/652588148

LLAMA2+QLora微调大模型https://www.bilibili.com/video/BV1594y1y76m/?spm_id_from=333.337.search-card.all.click&vd_source=9710fe8f4dbfeb6bd0b7202815b341c2

fine-tuning llama2https://ukey.co/blog/finetune-llama-2-peft-qlora-huggingface/

fine-tuning Llama 2 with PEFT's QLoRa https://ukey.co/blog/finetune-llama-2-peft-qlora-huggingface/

基础代码

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import torch
import torch.nn as nn
import torch.optim as optim

class BasicNet(nn.Module):
def __init__(self, input_dim=784, hidden_dim1=256, hidden_dim2=128, output_dim=10):
super(BasicNet, self).__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim1)
self.fc2 = nn.Linear(hidden_dim1, hidden_dim2)
self.fc3 = nn.Linear(hidden_dim2, output_dim)
self.relu = nn.ReLU()
self.batchnorm1 = nn.BatchNorm1d(hidden_dim1)
self.batchnorm2 = nn.BatchNorm1d(hidden_dim2)

def forward(self, x):
x = self.relu(self.batchnorm1(self.fc1(x)))
x = self.relu(self.batchnorm2(self.fc2(x)))
x = self.fc3(x)
return x

# 创建模型
model = BasicNet()

# 定义损失函数
criterion = nn.CrossEntropyLoss()

# 定义优化器
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 训练模型
def train(model, train_loader, criterion, optimizer, num_epochs=5):
model.train()
for epoch in range(num_epochs):
running_loss = 0.0
for i, (inputs, labels) in enumerate(train_loader):
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if (i+1) % 100 == 0:
print(f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{len(train_loader)}], Loss: {running_loss/100:.4f}')
running_loss = 0.0

# 保存模型
def save_model(model, filepath):
torch.save(model.state_dict(), filepath)

# 加载模型
def load_model(model, filepath):
model.load_state_dict(torch.load(filepath))
model.eval() # 设置为评估模式

# 示例数据加载
# train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=64, shuffle=True)

# 示例训练过程
# train(model, train_loader, criterion, optimizer)

# 示例模型保存
# save_model(model, 'model.pth')

# 示例模型加载
# loaded_model = BasicNet()
# load_model(loaded_model, 'model.pth')

交叉熵的实现

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import torch
import torch.nn.functional as F

# 创建一个简单的神经网络模型
class SimpleNN(torch.nn.Module):
def __init__(self, input_size, num_classes):
super(SimpleNN, self).__init__()
self.fc = torch.nn.Linear(input_size, num_classes)

def forward(self, x):
out = self.fc(x)
return out

# 定义手动计算交叉熵损失的函数
def cross_entropy_manual(predictions, targets):
# 计算 softmax
softmax = torch.exp(predictions) / torch.sum(torch.exp(predictions), dim=1, keepdim=True)

# 获取每个目标类别的概率
log_softmax = torch.log(softmax)
targets_one_hot = F.one_hot(targets, num_classes=predictions.shape[1])

# 计算交叉熵
loss = -torch.sum(targets_one_hot * log_softmax) / predictions.shape[0]

return loss

# 假设我们有一些数据
input_size = 10
num_classes = 3
batch_size = 5

# 随机生成输入数据和标签
inputs = torch.randn(batch_size, input_size)
labels = torch.randint(0, num_classes, (batch_size,))

# 初始化模型
model = SimpleNN(input_size, num_classes)

# 前向传播
outputs = model(inputs)

# 计算手动实现的交叉熵损失
loss = cross_entropy_manual(outputs, labels)

print("Manual Cross-Entropy Loss:", loss.item())

Batch Norm代码实现

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
class MyBN:
def __init__(self, momentum=0.01, eps=1e-5, feat_dim=2):
self._running_mean = np.zeros(shape = (feat_dim,))
self._running_var = np.ones(shape = (fear_dim,))
self._momentum = momentum
#防止分母计算为0
self._eps = eps

#对应batch norm中需要更新beta 和 gamma, 采用pytorch文档中的初始化
self._beta = np.zeros(shape=(feat_dim,))
self._gamma = np.ones(shape=(feat_dim,))


def batch_norm(self, x):
if self.training:
x_mean = x.mean(axis=0)
x_var = x.var(axis=0)
#对应running_mean的更新公式
self._running_mean = (1-self._momentum)*x_mean +self._momentum*self._running_mean
self._running_var = (1-self._momentum)*x_var + self._momentum*self._running_var
#对应论文中计算BN公式
x_hat = (x-x_mean)/np.sqrt(x_var+self._eps)
else:
x_hat = (x-self._running_mean)/np.sqrt(self._running_var+self._eps)
return self._gamma*x_hat + self._beta

PyTorch实现Attention

自注意力机制的实现

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
28
29
30
from math import sqrt
import torch
import torch.nn as nn

class SelfAttention(nn.Module):
def __init__(self, dim_in, dim_k, dim_v):
super(SelfAttention, self).__init__()
self.dim_in = dim_in
self.dim_k = dim_k
self.dim_v = dim_v
self.linear_q = nn.Linear(dim_in, dim_k, bias=False)
self.linear_k = nn.Linear(dim_in, dim_k, bias=False)
self.linear_v = nn.Linear(dim_in, dim_v, bias=False)
self._norm_fact = 1/sqrt(dim_k)


def forward(self, x):
batch, n, dim_in = x.shape
assert dim_in == self.dim_in

q = self.linear_q(x) #batch, n, dim_k
k = self.linear_k(x)
v = self.linear_v(x)

dist = torch.bmm(q, k.transpose(1,2))* self._norm_fact #batch, n, n
dist = torch.softmax(dist, dim=-1)

att = torch.bmm(dist, v)
return att

多头注意力机制的实现

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from math import sqrt
import torch
import torch.nn as nn

class MultiHeadAttention(nn.Module):
#dim_in input dimention
#dim_k kq dimention
#dim_v value dimention
#num_heads number of heads

def __init__(self, dim_in, dim_k, dim_v, num_heads=8):
super(MultiHeadAttention, self).__init__()
assert dim_k% num_heads ==0 and dim_v% num_heads ==0

self.dim_in = dim_in
self.dim_k = dim_k
self.dim_v = dim_v
self.num_heads = num_heads
self.linear_q = nn.Linear(dim_in, dim_k, bias==False)
self.linear_k = nn.Linear(dim_in, dim_k, bias==False)
self.linear_v = nn.Linear(dim_in, dim_v, bias==False)
self._norm_fact = 1/sqrt(dim_k//num_heads)

def forwards(self, x):
# x: tensor of shape(batch, n, dim_in)
batch, n, dim_in = x.shape
assert dim_in = self.dim_in

nh = self.num_heads
dk = self.dim_k // nh
dv = self.dim_v // nh

q = self.linear_q(x).reshape(batch, n, nh, dk).transpose(1, 2)
k = self.linear_k(x).reshape(batch, n, nh, dk).transpose(1, 2)
v = self.linear_v(x).reshape(batch, n, nk, dk).transpose(1, 2)

dist = torch.matmul(q, k.transpose(2,3))*self._norm_fact
dist = torch.softmax(dist, dim=-1)

att = torch.matmul(dist, v)
att = att.transpose(1,2).reshape(batch, n, self.dim_v)

Transformer位置编码

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import torch
import torch.nn as nn
import math

class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
super(PositionalEncoding, self).__init__()

# Create a matrix of shape (max_len, d_model) for positional encodings
pe = torch.zeros(max_len, d_model)

# Position indices (0 to max_len-1)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)

# Compute the div_term which is a part of the sinusoidal function
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))

# Compute the positional encodings for even and odd indices
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)

# Add a batch dimension and register as a buffer
pe = pe.unsqueeze(0).transpose(0, 1)
self.register_buffer('pe', pe)

def forward(self, x):
# Add positional encoding to the input tensor
return x + self.pe[:x.size(0), :]

# Example usage
d_model = 512
seq_len = 100
batch_size = 32

# Create the positional encoding module
pos_encoder = PositionalEncoding(d_model)

# Example input tensor with shape (seq_len, batch_size, d_model)
x = torch.zeros(seq_len, batch_size, d_model)

# Apply positional encoding
x = pos_encoder(x)

print(x.shape) # Should be (seq_len, batch_size, d_model)

Layer Nrom代码实现

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import torch
import torch.nn as nn

class LayerNorm(nn.Module):
def __init__(self, normalized_shape, eps=1e-5, elementwise_affine=True):
super(LayerNorm, self).__init__()
self.normalized_shape = normalized_shape
self.eps = eps
self.elementwise_affine = elementwise_affine

if self.elementwise_affine:
self.weight = nn.Parameter(torch.ones(normalized_shape))
self.bias = nn.Parameter(torch.zeros(normalized_shape))
else:
self.register_parameter('weight', None)
self.register_parameter('bias', None)

def forward(self, x):
# Compute the mean and variance along the last dimension
mean = x.mean(dim=-1, keepdim=True)
var = x.var(dim=-1, keepdim=True, unbiased=False)

# Normalize the input
x_normalized = (x - mean) / torch.sqrt(var + self.eps)

if self.elementwise_affine:
# Scale and shift
x_normalized = x_normalized * self.weight + self.bias

return x_normalized

# Example usage
input_dim = 10
batch_size = 2
seq_len = 5

# Create the LayerNorm module
layer_norm = LayerNorm(input_dim)

# Example input tensor with shape (batch_size, seq_len, input_dim)
x = torch.randn(batch_size, seq_len, input_dim)

# Apply LayerNorm
x_normalized = layer_norm(x)

print(x_normalized.shape) # Should be (batch_size, seq_len, input_dim)

RMS Norm

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import torch
import torch.nn as nn

class RMSNorm(nn.Module):
def __init__(self, normalized_shape, eps=1e-8, elementwise_affine=True):
super(RMSNorm, self).__init__()
self.normalized_shape = normalized_shape
self.eps = eps
self.elementwise_affine = elementwise_affine

if self.elementwise_affine:
self.weight = nn.Parameter(torch.ones(normalized_shape))
else:
self.register_parameter('weight', None)

def forward(self, x):
# Compute the root mean square along the last dimension
rms = torch.sqrt(torch.mean(x ** 2, dim=-1, keepdim=True) + self.eps)

# Normalize the input
x_normalized = x / rms

if self.elementwise_affine:
# Scale the normalized input
x_normalized = x_normalized * self.weight

return x_normalized

# Example usage
input_dim = 10
batch_size = 2
seq_len = 5

# Create the RMSNorm module
rms_norm = RMSNorm(input_dim)

# Example input tensor with shape (batch_size, seq_len, input_dim)
x = torch.randn(batch_size, seq_len, input_dim)

# Apply RMSNorm
x_normalized = rms_norm(x)

print(x_normalized.shape) # Should be (batch_size, seq_len, input_dim)

Transformer-Encoder

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
作者:天天向上o
链接:https://zhuanlan.zhihu.com/p/686980075
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

class MultiHeadSelfAttention(nn.Module):
def __init__(self, embed_size, heads):
super(MultiHeadSelfAttention, self).__init__()
self.embed_size = embed_size
self.heads = heads
self.head_dim = embed_size // heads

assert (
self.head_dim * heads == embed_size
), "Embedding size needs to be divisible by heads"

self.values = nn.Linear(self.head_dim, self.head_dim, bias=False)
self.keys = nn.Linear(self.head_dim, self.head_dim, bias=False)
self.queries = nn.Linear(self.head_dim, self.head_dim, bias=False)
self.fc_out = nn.Linear(heads * self.head_dim, embed_size)

def forward(self, values, keys, queries):
N = queries.shape[0]
value_len, key_len, query_len = values.shape[1], keys.shape[1], queries.shape[1]

# Split the embedding into self.heads different pieces
values = values.reshape(N, value_len, self.heads, self.head_dim)
keys = keys.reshape(N, key_len, self.heads, self.head_dim)
queries = queries.reshape(N, query_len, self.heads, self.head_dim)

values = self.values(values)
keys = self.keys(keys)
queries = self.queries(queries)

# Attention mechanism
#attention = torch.einsum("nqhd,nkhd->nhqk", [queries, keys])
attention = torch.matmul(queries, keys.transpose(-2, -1)) / torch.sqrt(torch.tensor(self.head_dim, dtype=torch.float32))

attention = F.softmax(attention / (self.embed_size ** (1 / 2)), dim=3)

out = torch.matmul(attention, values).reshape(N, query_len, self.heads * self.head_dim)
# out = torch.einsum("nhql,nlhd->nqhd", [attention, values]).reshape(
N, query_len, self.heads * self.head_dim
)

out = self.fc_out(out)

return out

class TransformerBlock(nn.Module):
def __init__(self, embed_size, heads, dropout, forward_expansion):
super(TransformerBlock, self).__init__()
self.attention = MultiHeadSelfAttention(embed_size, heads)
self.norm1 = nn.LayerNorm(embed_size)
self.norm2 = nn.LayerNorm(embed_size)

self.feed_forward = nn.Sequential(
nn.Linear(embed_size, forward_expansion * embed_size),
nn.ReLU(),
nn.Linear(forward_expansion * embed_size, embed_size),
)

self.dropout = nn.Dropout(dropout)

def forward(self, value, key, query):
attention = self.attention(value, key, query)
x = self.dropout(self.norm1(attention + query))
forward = self.feed_forward(x)
out = self.dropout(self.norm2(forward + x))
return out

class Encoder(nn.Module):
def __init__(
self,
embed_size,
num_layers,
heads,
device,
forward_expansion,
dropout,
):

super(Encoder, self).__init__()
self.embed_size = embed_size
self.device = device
self.layers = nn.ModuleList(
[
TransformerBlock(
embed_size,
heads,
dropout=dropout,
forward_expansion=forward_expansion,
)
for _ in range(num_layers)
]
)

self.dropout = nn.Dropout(dropout)

def forward(self, x):
out = self.dropout(x)

for layer in self.layers:
out = layer(out, out, out)

return out

# Hyperparameters
embed_size = 512
num_layers = 6
heads = 8
device = "cuda" if torch.cuda.is_available() else "cpu"
forward_expansion = 4
dropout = 0.1

# Example
encoder = Encoder(embed_size, num_layers, heads, device, forward_expansion, dropout).to(device)

PyTorch实现DQN

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np

class DQN(nn.Module):
def __init__(self, input_dim, output_dim):
super(DQN, self).__init__()
self.fc1 = nn.Linear(input_dim, 128)
self.fc2 = nn.Linear(128, 128)
self.fc3 = nn.Linear(128, output_dim)

def forward(self, x):
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
x = self.fc3(x)
return x

class DQNAgent:
def __init__(self, input_dim, output_dim, gamma=0.99, lr=0.001):
self.input_dim = input_dim
self.output_dim = output_dim
self.gamma = gamma
self.lr = lr
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.policy_net = DQN(input_dim, output_dim).to(self.device)
self.target_net = DQN(input_dim, output_dim).to(self.device)
self.target_net.load_state_dict(self.policy_net.state_dict())
self.target_net.eval()
self.optimizer = optim.Adam(self.policy_net.parameters(), lr=lr)
self.loss_fn = nn.MSELoss()

def select_action(self, state, epsilon):
if np.random.rand() < epsilon:
return np.random.randint(self.output_dim)
state = torch.FloatTensor(state).unsqueeze(0).to(self.device)
with torch.no_grad():
q_values = self.policy_net(state)
return q_values.max(1)[1].item()

def train(self, replay_buffer, batch_size):
if len(replay_buffer) < batch_size:
return
transitions = replay_buffer.sample(batch_size)
batch = Transition(*zip(*transitions))
state_batch = torch.FloatTensor(batch.state).to(self.device)
next_state_batch = torch.FloatTensor(batch.next_state).to(self.device)
action_batch = torch.LongTensor(batch.action).unsqueeze(1).to(self.device)
reward_batch = torch.FloatTensor(batch.reward).unsqueeze(1).to(self.device)
done_batch = torch.FloatTensor(batch.done).unsqueeze(1).to(self.device)

current_q_values = self.policy_net(state_batch).gather(1, action_batch)
next_q_values = self.target_net(next_state_batch).max(1)[0].unsqueeze(1)
target_q_values = reward_batch + (1 - done_batch) * self.gamma * next_q_values

loss = self.loss_fn(current_q_values, target_q_values.detach())
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()

def update_target_net(self):
self.target_net.load_state_dict(self.policy_net.state_dict())

class ReplayBuffer:
def __init__(self, capacity):
self.capacity = capacity
self.buffer = []
self.position = 0

def push(self, state, action, reward, next_state, done):
if len(self.buffer) < self.capacity:
self.buffer.append(None)
self.buffer[self.position] = (state, action, reward, next_state, done)
self.position = (self.position + 1) % self.capacity

def sample(self, batch_size):
return random.sample(self.buffer, batch_size)

def __len__(self):
return len(self.buffer)

Transition = namedtuple('Transition', ('state', 'action', 'reward', 'next_state', 'done'))

PyTorch实现PPO

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np

class ActorCritic(nn.Module):
def __init__(self, input_dim, output_dim):
super(ActorCritic, self).__init__()
self.fc1 = nn.Linear(input_dim, 128)
self.fc2 = nn.Linear(128, 128)
self.fc_actor = nn.Linear(128, output_dim)
self.fc_critic = nn.Linear(128, 1)

def forward(self, x):
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
logits = self.fc_actor(x)
value = self.fc_critic(x)
return logits, value

class PPOAgent:
def __init__(self, input_dim, output_dim, gamma=0.99, lr=0.001):
self.input_dim = input_dim
self.output_dim = output_dim
self.gamma = gamma
self.lr = lr
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.policy_net = ActorCritic(input_dim, output_dim).to(self.device)
self.optimizer = optim.Adam(self.policy_net.parameters(), lr=lr)
self.loss_fn = nn.MSELoss()

def select_action(self, state):
state = torch.FloatTensor(state).unsqueeze(0).to(self.device)
with torch.no_grad():
logits, _ = self.policy_net(state)
action_probs = torch.softmax(logits, dim=-1)
action = np.random.choice(np.arange(self.output_dim), p=action_probs.cpu().numpy().ravel())
return action

def train(self, states, actions, rewards, next_states, dones, old_log_probs, epsilon_clip=0.2, num_epochs=10):
states = torch.FloatTensor(states).to(self.device)
actions = torch.LongTensor(actions).unsqueeze(-1).to(self.device)
rewards = torch.FloatTensor(rewards).unsqueeze(-1).to(self.device)
next_states = torch.FloatTensor(next_states).to(self.device)
dones = torch.FloatTensor(dones).unsqueeze(-1).to(self.device)
old_log_probs = torch.FloatTensor(old_log_probs).unsqueeze(-1).to(self.device)

for _ in range(num_epochs):
logits, values = self.policy_net(states)
new_log_probs = torch.log_softmax(logits, dim=-1).gather(1, actions)
ratio = (new_log_probs - old_log_probs).exp()
advantages = rewards - values.detach()

surr1 = ratio * advantages
surr2 = torch.clamp(ratio, 1.0 - epsilon_clip, 1.0 + epsilon_clip) * advantages
actor_loss = -torch.min(surr1, surr2).mean()

critic_loss = 0.5 * (rewards - values).pow(2).mean()

loss = actor_loss + 0.5 * critic_loss

self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()


【深度学习】DeepL|LLM基础知识
https://lihaibineric.github.io/2024/03/05/dl_llm_basic/
Author
Haibin Li
Posted on
March 5, 2024
Updated on
July 18, 2024
Licensed under