NLP基础知识补全

本文档旨在汇集NLP中一些常见的基础知识。主要包含损失函数、优化器、微调方法、Normlization Layer、位置编码、激活函数、Attention实现、量化模型、常见大模型结构几部分。

损失函数

这部分主要参考知乎文章:损失函数(Loss Function)(https://zhuanlan.zhihu.com/p/261059231)

1. 什么是损失函数?

一言以蔽之,损失函数(loss function)就是用来度量模型的预测值f(x)与真实值Y的差异程度的运算函数,它是一个非负实值函数,通常使用L(Y, f(x))来表示,损失函数越小,模型的鲁棒性就越好。

2. 为什么使用损失函数?

损失函数使用主要是在模型的训练阶段,每个批次的训练数据送入模型后,通过前向传播输出预测值,然后损失函数会计算出预测值和真实值之间的差异值,也就是损失值。得到损失值之后,模型通过反向传播去更新各个参数,来降低真实值与预测值之间的损失,使得模型生成的预测值往真实值方向靠拢,从而达到学习的目的。

3. 有哪些损失函数?

3.1 基于距离度量的损失函数

基于距离度量的损失函数通常将输入数据映射到基于距离度量的特征空间上,如欧氏空间、汉明空间等,将映射后的样本看作空间上的点,采用合适的损失函数度量特征空间上样本真实值和模型预测值之间的距离。特征空间上两个点的距离越小,模型的预测性能越好。

3.1.1 均方误差损失函数(MSE)

公式: \(L(Y|f(x))=\frac{1}{n}\sum_{i=1}^{N}{(Y_{i}-f(x_{i}))^{2}}\)

在回归问题中,均方误差损失函数用于度量样本点到回归曲线的距离,通过最小化平方损失使样本点可以更好地拟合回归曲线。均方误差损失函数(MSE)的值越小,表示预测模型描述的样本数据具有越好的精确度。由于无参数、计算成本低和具有明确物理意义等优点,MSE已成为一种优秀的距离度量方法。尽管MSE在图像和语音处理方面表现较弱,但它仍是评价信号质量的标准,在回归问题中,MSE常被作为模型的经验损失或算法的性能指标。

代码实现:

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
import numpy as np
# 自定义实现
def MSELoss(x:list,y:list):
"""
x:list,代表模型预测的一组数据
y:list,代表真实样本对应的一组数据
"""
assert len(x)==len(y)
x=np.array(x)
y=np.array(y)
loss=np.sum(np.square(x - y)) / len(x)
return loss

#计算过程举例
x=[1,2]
y=[0,1]
loss=((1-0)**2 + (2-1)**2)÷2=(1+1)÷2=1

# Tensorflow2.0版
y_true=tf.convert_to_tensor(y)
y_pred=tf.convert_to_tensor(x)
mse_loss = tf.keras.losses.MSE(y_true, y_pred) # y_true, y_pred都是张量格式

# pytorch版本
y_true=torch.tensor(y)
y_pred=torch.tensor(x)
mse_fc = torch.nn.MSELoss(y_true, y_pred)
mse_loss = mse_fc(x,y)
3.1.2 L2损失函数

L2损失函数: \(L(Y|f(x))=\sqrt{\frac{1}{n}\sum_{i=1}^{N}{(Y_{i}-f(x_{i}))^{2}}}\)

L2损失又被称为欧氏距离,是一种常用的距离度量方法,通常用于度量数据点之间的相似度。由于L2损失具有凸性和可微性,且在独立、同分布的高斯噪声情况下,它能提供最大似然估计,使得它成为回归问题、模式识别、图像处理中最常使用的损失函数。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
import numpy as np
# 自定义实现
def L2Loss(x:list,y:list):
"""
x:list,代表模型预测的一组数据
y:list,代表真实样本对应的一组数据
"""
assert len(x)==len(y)
x=np.array(x)
y=np.array(y)
loss=np.sqrt(np.sum(np.square(x - y)) / len(x))
return loss
3.1.3 L1损失函数

L1损失函数: \(L(Y|f(x))=\sum_{i=1}^{N}{|Y_{i}-f(x_{i})|}\)

L1损失又称为曼哈顿距离,表示残差的绝对值之和。L1损失函数对离群点有很好的鲁棒性,但它在残差为零处却不可导。另一个缺点是更新的梯度始终相同,也就是说,即使很小的损失值,梯度也很大,这样不利于模型的收敛。针对它的收敛问题,一般的解决办法是在优化算法中使用变化的学习率,在损失接近最小值时降低学习率。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
import numpy as np
# 自定义实现
def L1Loss(x:list,y:list):
"""
x:list,代表模型预测的一组数据
y:list,代表真实样本对应的一组数据
"""
assert len(x)==len(y)
x=np.array(x)
y=np.array(y)
loss=np.sum(np.abs(x - y)) / len(x)
return loss

3.2 基于概率分布度量的损失函数

基于概率分布度量的损失函数是将样本间的相似性转化为随机事件出现的可能性,即通过度量样本的真实分布与它估计的分布之间的距离,判断两者的相似度,一般用于涉及概率分布或预测类别出现的概率的应用问题中,在分类问题中尤为常用。

3.2.1 KL散度函数(相对熵)

公式: \(L(Y|f(x))=\sum_{i=1}^{n}{Y_{i}\times log(\frac{Y_{i}}{f(x_{i})})}\)

公式中Y代表真实值,f(x)代表预测值。 KL散度( Kullback-Leibler divergence)也被称为相对熵,是一种非对称度量方法,常用于度量两个概率分布之间的距离。KL散度也可以衡量两个随机分布之间的距离,两个随机分布的相似度越高的,它们的KL散度越小,当两个随机分布的差别增大时,它们的KL散度也会增大,因此KL散度可以用于比较文本标签或图像的相似性。基于KL散度的演化损失函数有JS散度函数。JS散度也称JS距离,用于衡量两个概率分布之间的相似度,它是基于KL散度的一种变形,消除了KL散度非对称的问题,与KL散度相比,它使得相似度判别更加准确。

相对熵是恒大于等于0的。当且仅当两分布相同时,相对熵等于0。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
def kl_loss(y_true:list,y_pred:list):
"""
y_true,y_pred,分别是两个概率分布
比如:px=[0.1,0.2,0.8]
py=[0.3,0.3,0.4]
"""
assert len(y_true)==len(y_pred)
KL=0
for y,fx in zip(y_true,y_pred):
KL+=y*np.log(y/fx)
return KL
3.2.2 交叉熵损失

公式: \(L(Y|f(x))=-\sum_{i=1}^{N}{Y_{i}log f(x_{i})}\)

交叉熵是信息论中的一个概念,最初用于估算平均编码长度,引入机器学习后,用于评估当前训练得到的概率分布与真实分布的差异情况。为了使神经网络的每一层输出从线性组合转为非线性逼近,以提高模型的预测精度,在以交叉熵为损失函数的神经网络模型中一般选用tanh、sigmoid、softmax或ReLU作为激活函数。

交叉熵损失函数刻画了实际输出概率与期望输出概率之间的相似度,也就是交叉熵的值越小,两个概率分布就越接近,特别是在正负样本不均衡的分类问题中,常用交叉熵作为损失函数。目前,交叉熵损失函数是卷积神经网络中最常使用的分类损失函数,它可以有效避免梯度消散。在二分类情况下也叫做对数损失函数。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
def CrossEntropy_loss(y_true:list,y_pred:list):
"""
y_true,y_pred,分别是两个概率分布
比如:px=[0.1,0.2,0.8]
py=[0.3,0.3,0.4]
"""
assert len(y_true)==len(y_pred)
loss=0
for y,fx in zip(y_true,y_pred):
loss+=-y * np.log(fx)
return loss

当正负样本不均衡的时候,通常会在交叉熵损失函数类别前面加个参数α

\[CE = \begin{cases} -\alpha log(p) & \text{ y = 1} \\ -(1-\alpha )log(1-p) & \text{ y = 0} \end{cases}\\\]

3.2.3 softmax损失函数

公式: \(L(Y|f(x))=-\frac{1}{n}\sum_{i=1}^{n}{log\frac{e^{f_{Y_{i}}}}{\sum_{j=1}^{c}{e^{f_{j}}}}}\)

从标准形式上看,softmax损失函数应归到对数损失的范畴,在监督学习中,由于它被广泛使用,所以单独形成一个类别。softmax损失函数本质上是逻辑回归模型在多分类任务上的一种延伸,常作为CNN模型的损失函数。softmax损失函数的本质是将一个k维的任意实数向量x映射成另一个k维的实数向量,其中,输出向量中的每个元素的取值范围都是(0,1),即softmax损失函数输出每个类别的预测概率。由于softmax损失函数具有类间可分性,被广泛用于分类、分割、人脸识别、图像自动标注和人脸验证等问题中,其特点是类间距离的优化效果非常好,但类内距离的优化效果比较差。

softmax损失函数具有类间可分性,在多分类和图像标注问题中,常用它解决特征分离问题。在基于卷积神经网络的分类问题中,一般使用softmax损失函数作为损失函数,但是softmax损失函数学习到的特征不具有足够的区分性,因此它常与对比损失或中心损失组合使用,以增强区分能力。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
def softmax(x):
x_exp = np.exp(x)
x_sum = np.sum(x_exp, axis=1, keepdims=True)
s = x_exp / x_sum
return s

# Tensorflow2.0版
softmax_fc = tf.keras.activations.softmax(x)
# pytorch版
softmax_fc = torch.nn.Softmax()
output = softmax_fc(x)
3.2.4 Focal loss

focal loss的引入主要是为了解决难易样本不均衡的问题,注意有区别于正负样本不均衡的问题。难易样本分为四个类型:

正难 正易
负难 负易

易分样本虽然损失很低,但是数量太多,对模型的效果提升贡献很小,模型应该重点关注那些难分样本,因此需要把置信度高的损失再降低一些

\(FE = \begin{cases} -\alpha(1-p)^{\gamma} log(p) & \text{ y = 1} \\ -(1-\alpha )p^{\gamma} log(1-p) & \text{ y = 0} \end{cases}\\\)

4. 如何选择损失函数?

通常情况下,损失函数的选取应从以下方面考虑:

(1) 选择最能表达数据的主要特征来构建基于距离或基于概率分布度量的特征空间。

(2)选择合理的特征归一化方法,使特征向量转换后仍能保持原来数据的核心内容。

(3)选取合理的损失函数,在实验的基础上,依据损失不断调整模型的参数,使其尽可能实现类别区分。

(4)合理组合不同的损失函数,发挥每个损失函数的优点,使它们能更好地度量样本间的相似性。

(5)将数据的主要特征嵌入损失函数,提升基于特定任务的模型预测精确度。

优化器

这部分主要参考知乎文章:[深度学基础]优化器算法SGD,AdaGrad,RMSprop,Adam(https://zhuanlan.zhihu.com/p/618265040),以及知乎文章:Adam和AdamW(https://zhuanlan.zhihu.com/p/643452086)。主要包含SGD(随机梯度下降)、RMSProp、Adam和AdamW。

本文首先介绍基础梯度下降法,然后介绍对SGD的改进方法:动量法、AdaGrad、RMSprop以及Adam。本专栏的文章都是本人找工作时根据面试经历和网络资料整理,因此更偏向于要点罗列的形式。由于是为了应付面试,内容略显肤浅,且本人水平有限,若想在学术科研的层面有更深入的理解,还请参考相关论文以及大佬的文章。

梯度下降法

梯度是一个向量,表示某一函数在该点处的方向导数沿着该方向取得最大值,即函数在该点处沿着该方向变化最快。梯度下降的主要思想是用当前位置负梯度方向作为搜索方向,因为该方向为当前位置的最快下降[这个意思是使得待优化函数例如Loss减小最快的参数更新方向]方向,所以也被称为”最速下降法“。最速下降法越接近目标值,步长越小,前进越慢。当目标函数是凸函数时,梯度下降的解时全局最优。但一般情况下,其解不保证全局最优。梯度下降原理推导(这个链接里的马东什么:梯度下降法和一阶泰勒展开的关系,很清晰),主要是理解为什么负梯度时下降最快的方向,为什么会有学习率这个东西,本质是对损失函数进行泰勒展开得到的。

在机器学习种,基于基本的梯度下降法,发展出了3种具体的梯度下降方法,分别为 BGD(Batch Gradient Descent批量梯度下降法),SGD, mini-batch GD

批量梯度下降法(Batch Gradient Desceent, BGD):具体做法也就是在更新参数时使用所有的样本来进行更新。 这样一来每迭代一步,都要用到训练集所有的数据,如果数据量很大,那么可想而知这种方法的迭代速度会很慢。

随机梯度下降(Stochastic Gradient Descent, SGD):每次迭代只用到了一个样本,在样本量很大的情况下,常见的情况是只用到了其中一部分样本数据即可迭代到最优解。因此随机梯度下降比批量梯度下降在计算量上会大大减少。SGD有一个缺点是,其噪音较BGD要多,使得SGD并不是每次迭代都向着整体最优化方向。而且SGD因为每次都是使用一个样本进行迭代,因此最终求得的最优解往往不是全局最优解,而只是局部最优解。但是大的整体的方向是向全局最优解的,最终的结果往往是在全局最优解附近。

小批量梯度下降(Mini-batch Gradient Descent):小批量梯度下降法是批量梯度下降法和随机梯度下降法的折衷,也就是对于m个样本,我们采用 x个样子来迭代,1<x<m 。

指数移动平均

为了方便后续对SGD的改进方法的介绍,先介绍指数移动平均的概念。指数移动平均是以指数式递减加权的移动平均。 各数值的加权影响力随时间而指数式递减,越近期的数据加权影响力越重,但较旧的数据也给予一定的加权值。

计算公式为: $v_t=v_{t-1}+(1-)_t $

优点:当想要计算均值的时候,不用保留所有时刻的值。随着时间推移,遥远过去的历史的影响会越来越小

动量法(Momentum)

算法思想:参数更新方向不仅由当前的梯度决定,也与此前累积的梯度方向有关。将过去梯度的指数移动平均称为动量。当前参数的更新值由动量和当前梯度两部分确定。在当前梯度方向发生改变时(震荡通常发生在梯度方向改变的时候),动量能够降低参数更新的速度,从而减少震荡;当前梯度方向与之前的梯度方向相同时,动量能够加速参数更新,从而加速收敛。

参数更新:

\(m_{t+1}=\gamma m_t + (1-\gamma)\nabla_{\theta}J(\theta)\)

\(\theta_{t+1}=\theta_{t}-m_{t+1}\)

参数 \(\gamma\) 决定了之前的梯度的贡献衰减的速度。当 \(\gamma=0\) 时,动量法就是SGD。

算法流程:

AdaGrad

算法思想:之前的SGD、动量法对每个参数都使用相同的学习率,AdaGrad对不同的参数动态采取不同的学习率。对于每个参数,其学习率为全局学习率除以该参数历史梯度平方和的平方根。在参数空间更为平缓的方向,会取得更大的进步(因为平缓,所以历史梯度平方和较小,对应学习下降的幅度较小),并且能够使得陡峭的方向变得平缓,从而加快训练速度。缺点:由于累计梯度平方和,训练中后期,分母越来越大,导致学习率很快会接近0。

参数更新:

\(s_{t+1}=s_t+\nabla_{\theta}J(\theta)\odot \nabla_{\theta}J(\theta)\)

\(\theta_{t+1}=\theta_{t}-\frac{\alpha}{\sqrt{s_{t+1}+\varepsilon}}\odot\nabla_{\theta}J(\theta)\)

\(\odot\) 表示Hadamard乘积(向量对应位置的元素相乘), \(\alpha\) 是全局学习率。

算法流程:

RMSprop

基本思想:RMSprop也是一种自适应学习率的方法,是在AdaGrad上的改进。AdaGrad会累计之前所有的梯度平方,而RMSprop采用的是指数加权移动平均,能够丢弃掉遥远过去的历史梯度平方,从而缓解AdaGrad学习率随迭代次数下降过快的问题。

参数更新:

\(r_{t+1}=\gamma r_t+(1-\gamma)\nabla_{\theta}J(\theta)\odot\nabla_{\theta}J(\theta)\)

\(\theta_{t+1}=\theta_{t}-\frac{\alpha}{\sqrt{r_{t+1}+\epsilon}}\odot\nabla_{\theta}J(\theta)\)

算法流程:

Adam

基本思想:也是一种自适应学习率的方法,可以看作是结合了RMSProp和动量法。Adam同时具备Momentum和RMSprop的优点。一是记录了过去的梯度,使用过去的累积梯度(动量)和当前梯度共同确定当前参数的更新量,可以减小震荡,加速收敛。而是使用梯度平和的累积值来动态调整学习率。

\(m_{t+1}=\beta_1m_t+(1-\beta_1)\nabla_{\theta}J(\theta)\)

\(r_{t+1}=\beta_2r_t+(1-\beta_2)\nabla_{\theta}J(\theta)\odot\nabla_{\theta}J(\theta)\)

\(\hat{m}_{t+1}=\frac{m_{t+1}}{1-\beta_1^{t}}, \hat{r}_{t+1}=\frac{r_t}{1-\beta_2^t}\)

\(\theta_{t+1}=\theta_t-\frac{\alpha}{\sqrt{r_{t+1}}+\epsilon}m_{t+1}\)

算法流程:

Adam详细参数说明可以参见https://blog.csdn.net/sinat_36618660/article/details/100026261

AdamW

AdamW相对与Adam的改动十分简单,其将权重衰减项从梯度的计算中拿出来直接加在了最后的权重更新步骤上(图1,式12)。其提出的动机在于:原先Adam的实现中如果采用了L2权重衰减,则相应的权重衰减项会被直接加在loss里,从而导致动量的一阶与二阶滑动平均均考虑了该权重衰减项(图1. 式6),而这影响了Adam的优化效果,而将权重衰减与梯度的计算进行解耦能够显著提升Adam的效果。目前,AdamW现在已经成为transformer训练中的默认优化器了。

img

关于显存占用:Adam和AdamW在反向传播时需要维护的变量分别为原始参数\(\theta_t\),梯度\(g_t\),动量\(m_t\)与二阶动量\(v_t\)(或者\(r_t\)),因此其训练时的显存占用为参数量的4倍。

Adam-mini

Adam(W)什么都好,但是耗显存,每个参数都需要额外储存的一阶动量\(m\)和二阶动量\(v\)

是否有必要对每个参数使用单独的学习率?如果不需要,我们可以节省多少?

  • 目标:减少内存占用,同时保持优化性能。
  • 方法:通过减少Adam中的学习率资源来降低内存占用。
  • 原理:基于Hessian矩阵的结构,将参数分组,并为每个组分配单一但高质量的学习率。

贡献:

  1. 新优化器:提出了Adam-mini,它通过基于Hessian结构的原则对模型参数进行分组,并为每个块选择单一学习率。
  2. 轻量级:显著减少了Adam中使用的学习能力数量,节省了45%到50%的内存成本。
  3. 有效性:在各种规模的语言模型上,包括预训练、监督微调和强化学习,Adam-mini都显示出与AdamW相当或更好的性能。
  4. 高效率:在预训练Llama2-7B时,Adam-mini比AdamW提高了49.6%的吞吐量,节省了33%的墙钟时间。

微调方法

分为全参数微调和参数高效微调。

全参数微调

这个没啥好说的,就是直接全部参数堆上去微调就行了。优点是理论上限高,微调出来的模型效果好。缺点是显存占用高,容易灾难性遗忘和过拟合。

参数高效微调

参考HuggingFace的PEFT设计,介绍LoRA、Prefix Tuning、Prompt Tuning和P-Tuning。参考知乎文章:大模型参数高效微调(PEFT)(https://zhuanlan.zhihu.com/p/621700272)

PEFT方法概述

如下图所示,PEFT 方法可以分为三类,不同的方法对 PLM 的不同部分进行下游任务的适配:

  • Prefix/Prompt-Tuning:在模型的输入或隐层添加\(k\)个额外可训练的前缀 tokens(这些前缀是连续的伪 tokens,不对应真实的 tokens),只训练这些前缀参数;
  • Adapter-Tuning:将较小的神经网络层或模块插入预训练模型的每一层,这些新插入的神经模块称为 adapter(适配器),下游任务微调时也只训练这些适配器参数;
  • LoRA:通过学习小参数的低秩矩阵来近似模型权重矩阵\(W\)的参数更新,训练时只优化低秩矩阵参数。
img
Transformer 结构和最先进的 PEFT 方法

Prefix Tuning

Prefix-Tuning 在模型输入前添加一个连续的且任务特定的向量序列(continuous task-specific vectors),称之为前缀(prefix)。前缀被视为一系列“虚拟 tokens”,但是它由不对应于真实 tokens 的自由参数组成。与更新所有 PLM 参数的全量微调不同,Prefix-Tuning 固定 PLM 的所有参数,只更新优化特定任务的 prefix。因此,在生产部署时,只需要存储一个大型 PLM 的副本和一个学习到的特定任务的 prefix,每个下游任务只产生非常小的额外的计算和存储开销。

img

Fine-tuning 更新所有 PLM 参数,并且需要为每个任务存储完整的模型副本。Prefix-tuning 冻结了 PLM 参数并且只优化了 prefix。因此,只需要为每个任务存储特定 prefix,使 Prefix-tuning 模块化且节省存储空间。

如下图所示,以 GPT2 的自回归语言模型为例,将输入\(x\)和输出\(y\)拼接为\(z=[x:y]\),经过 LM 的某一层计算隐层表示\(h=[h_1,\cdots, h_i, \cdots, h_n]\)\(h_i=LM_{\phi}(z_i,h_{<i})\),其中, \(X_{idx}\)\(Y_{idx}\)分别为输入和输出序列的索引。

img

Prefix-Tuning 示例图

Prefix-Tuning 在输入前添加前缀,即\(z=[prefix, x, y]\)\(P_{idx}\)为前缀序列的索引, \(|P_{idx}|\) 为前缀的长度。前缀索引对应着由\(\theta\)参数化的向量矩阵\(P_\theta\),维度为\(|P_{idx}|\times dim(h_i)\)。隐层表示的计算如下式所示,若索引为前缀索引\(P_{idx}\) ,直接从\(P_\theta\)复制对应的向量作为\(h_i\)在模型每一层都添加前缀向量);否则直接通过 LM 计算得到,同时,经过 LM 计算的\(h_i\)也依赖于其左侧的前缀参数\(P_\theta\),即通过前缀来影响后续的序列隐层激化值\[ h_i = \begin{cases} P_\theta [i,:] & \text{if i} \in P_{idx} \\ LM_\phi(z_i, h_{<i}) & \text{ otherwise.} \end{cases}\\ \] 但是直接优化\(P_\theta\)会导致训练不稳定,通过一个更小的矩阵\(P_\theta '\)和一个更大的前馈神经网络\(MLP_\theta\)\(P_\theta\)进行重参数化: \(P_\theta[i,:]=MLP_\theta(P_\theta'[i,:])\) 。在训练时,LM 的参数\(\phi\)被固定,只有前缀参数\(\theta\)为可训练的参数。训练完成后,只有前缀\(P_\theta\)被保存。

P-Tuning

P-Tuning 的方法思路与 Prefix-Tuning 很相近,P-Tuning 利用少量连续的 embedding 参数作为 prompt 使 GPT 更好的应用于 NLU 任务,而 Prefix-Tuning 是针对 NLG 任务设计,同时,P-Tuning 只在 embedding 层增加参数,而 Prefix-Tuning 在每一层都添加可训练参数

如下图所示,具体的 NLU 任务以预测一个城市的首都为例,一个离散的 prompt 模板\(T\)可以写为:"The capital of Britain is [MASK].",其中"Britain"为输入的上下文\(x\),"[MASK]"位置为需要输出的目标\(y\)。而对于连续的 prompt 模板可以表示为:\(T=\{ [P_{0:i}],x,[P_{i+1:m}],y \}\),其中,\([P_i]\) 表示模板\(T\)\(i^{th}\)个 prompt token,且为伪 token。经过嵌入层将模板\(T\)映射为:\(h_0,\cdots,h_i,e(x),h_{i+1},\cdots,h_m,e(y)\),其中\(h_i\)为可训练的参数,而其它预训练的真实token向量以及模型权重参数都被固定。

img
P-Tuning 示例图

直接优化连续的 prompt 参数面临两个挑战:一是预训练模型原始的词向量已经高度离散,若随机初始化 prompt 向量并进行 SGD 优化,也只会在小范围内优化并陷入局部最小值;二是 prompt 向量之间是相互关联而不是独立的。论文中设计了一个 prompt 编码器,该编码器由一个 Bi-LSTM 和一个两层的前馈神经网络组成,对 prompt embedding 序列进行编码后再传入到语言模型中

img

论文的实验主要表明了:在 SuperGLUE 基准测试中,P-tuning 使得 GPT-style 的生成式模型与相同大小的 BERT 在 NLU 方面实现可比较,有时甚至更好的性能。

P-Tuning V2方法的思路其实和 Prefix-Tuning 相似,在模型的每一层都应用连续的 prompts 并对 prompts 参数进行更新优化。同时,该方法是针对 NLU 任务优化和适配的。

img
P-Tuning V2 示例图

Prompt Tuning

Prompt Tuning 方式可以看做是 Prefix Tuning 的简化,固定整个预训练模型参数,只允许将每个下游任务的额外\(k\)个可更新的 tokens 前置到输入文本中,也没有使用额外的编码层或任务特定的输出层。如下图所示,在模型大小增加到一定规模时,仅仅使用 Prompt Tuning 就足以达到 Fine Tuning 的性能。

img
T5 的 Model Tuning 实现了强大的性能,但需要为每个任务存储单独的模型副本。 随着模型规模的增加,对 T5 的 Prompt Tuning 与 Model Tuning 的性能相当,同时允许所有任务复用同一固定模型。Prompt Tuning 方法明显优于使用 GPT-3 的少样本 Prompt Design。

Prompt Tuning 以 T5 为基础,将所有任务转化成文本生成任务,表示为\(Pr_\theta(Y|X)\)。Prompt Tuning 在输入\(X\)前额外添加一系列特殊 tokens \(P\),输入语言模型生成\(Y\),即\(Pr_{\theta;\theta_P}(Y|[P;X])\)。其中,\(\theta\)为预训练模型参数,在训练过程被固定,\(\theta_P\)为 prompts 的专有参数,在训练过程被更新优化。通过将输入\(X\)的 embedding 矩阵\(X_e\)与 prompts 的 embedding 矩阵进行拼接\([P_e,X_e]\)输入 T5 模型,最大化 $Y的概率训练模型,但是只有 prompt 参数被更新。

img
模型微调需要制作整个预训练模型的任务特定副本,推理分批执行。Prompt tuning 只需为每个任务存储一个 Task Prompts,并使用原始预训练模型进行混合任务推理。

Prompt Tuning 提出了 Prompt Ensembling 方法来集成预训练语言模型的多种 prompts。通过在同一任务上训练\(N\)个 prompts,为一个任务创建了\(N\)个单独的模型,同时在整个过程中共享核心的预训练语言建模参数。 除了大幅降低存储成本外,提示集成还使推理更加高效。 处理一个样例时,可以执行批此大小为\(N\)的单个前向传递,而不是计算\(N\)次不同模型的前向传递,跨批次复制样例并改变 prompts。在推理时可以使用 major voting 方法从 prompt ensembling 中得到整体的预测。

Adapter Tuning

与 Prefix Tuning 和 Prompt Tuning 这类在输入前可训练添加 prompt embedding 参数来以少量参数适配下游任务,Adapter Tuning 则是在预训练模型内部的网络层之间添加新的网络层或模块来适配下游任务。假设预训练模型函数表示为\(\phi_w(x)\) ,对于 Adapter Tuning ,添加适配器之后模型函数更新为:\(\phi_{w,w_0}(x)\)\(w\)是预训练模型的参数, \(w_0\)是新添加的适配器的参数,在训练过程中, \(w\)被固定,只有 \(w_0\)被更新。\(|w_0| \ll |w|\) ,这使得不同下游任务只需要添加少量可训练的参数即可,节省计算和存储开销,同时共享大规模预训练模型。

Adapter 主要包括 Series Adapter(串行) 和 Parallel Adapter(并行):

  • Series Adapter的适配器结构和与 Transformer 的集成如下图(a)所示。适配器模块被添加到每个 Transformer 层两次:多头注意力映射之后和两层前馈神经网络之后。适配器是一个 bottleneck(瓶颈)结构的模块,由一个两层的前馈神经网络(由向下投影矩阵、非线性函数和向上投影矩阵构成)和一个输出输出之间的残差连接组成。
  • Parallel Adapter如下图(b)所示。将适配器模块与每层 Transformer 的多头注意力和前馈层并行计算集成
img

LoRA

现有的 PEFT 方法主要有两类:Adapter Tuning 和 Prefix Tuning。Adapter Tuning 在 PLM 基础上添加适配器层会引入额外的计算,带来推理延迟问题;而 Prefix Tuning 难以优化,其性能随可训练参数规模非单调变化,更根本的是,为前缀保留部分序列长度必然会减少用于处理下游任务的序列长度

1. 方法原理

给定一个由\(\Phi\)参数化的预训练的自回归语言模型\(P_\Phi(y|x)\),对于全量微调,模型参数由预训练权重\(\Phi_0\) 初始化,并反复跟随使得条件语言模型目标函数最大化的梯度更新至\(\Phi_0+\Delta \Phi\)\[ \max_{\Phi} \sum_{(x,y)} \sum_{t=1}^{|y|} \log(P_\Phi(y_t|x,y_{<t})) \] 全量微调的一个主要缺点就是针对每个下游任务都学习和预训练权重维度相同的全新参数集合\(\Delta \Phi\) ,即\(|\Delta \Phi|=|\Phi_0|\)。尤其是 GPT-3 175B 这类大模型,全量微调对计算和存储资源的消耗是非常大的,存储和部署不同微调模型实例也是不可能的。LoRA 论文提出了一种计算和存储高效的低秩(Low-Rank)表示方法,利用更小规模的参数集合\(\Theta\)来对任务特定的参数增量进行编码,\(\Delta \Phi = \Delta \Phi(\Theta)\),\(|\Theta| \ll |\Phi_0|\)。利用该方法对 175B GPT-3 微调,需要训练更新的参数数量\(|\Theta|\)以小到全量微调参数数量\(|\Theta_0|\)的 0.01%。

具体地,Transformer 等神经网络包含许多执行矩阵乘法的密集层,这些权重矩阵通常具有满秩。研究表明预训练的语言模型具有较低的"内在维度(Instrisic Dimension)",并且可以和完整参数空间一样进行有效学习。受此启发,假设权重的更新在微调适配过程中也具有较低的"内在秩(Instrisic Rank)"。对于预训练模型的权重矩阵\(W_0 \in R^{d\times k}\),通过低秩分解(Low-Rank Decomposition)来表示约束其更新。 \[ W_0 + \Delta W = W_0 + BA \] 其中,\(B \in R^{d\times k}, A \in R^{r\times k}, r \ll min(d,k)\) 。训练过程,\(W_0\)被固定不再进行梯度更新,只训练\(A\)\(B\),如下图所示。对于输入\(x\),模型的前向传播过程\(h=W_0 x\)被更新为: \[ h=W_0 x+ BAx \] img

LoRA重参数化示意图,预训练模型的参数W固定,只训练A和B参数

2. 方法优点

  • 全量微调的一般化:LoRA 不要求权重矩阵的累积梯度更新在适配过程中具有满秩。当对所有权重矩阵应用 LoRA 并训练所有偏差时,将 LoRA 的秩\(r\)设置为预训练权重矩阵的秩,就能大致恢复了全量微调的表现力。也就是说,随着增加可训练参数的数量,训练 LoRA 大致收敛于训练原始模型。
  • 没有额外的推理延时:在生产部署时,可以明确地计算和存储\(W=W_0 + BA\),并正常执行推理。当需要切换到另一个下游任务时,可以通过减去\(BA\)来恢复\(W_0\),然后增加一个不同的\(B'A'\),这是一个只需要很少内存开销的快速运算。最重要的是,与结构参数上微调的模型相比,LoRA 推理过程中没有引入任何额外的延迟。
  • 减少内存和存储资源消耗:对于用 Adam 训练的大型 Transformer,若\(r \ll d_{model}\) ,LoRA 减少 2/3 的VRAM 用量(训练模型时,模型参数往往都会存储在显存 VRAM 中),因为不需要存储已固定的预训练参数\(W_0\)的优化器状态,可以用更少的GPU进行大模型训练。在 GPT-3 175B 上,训练期间的 VRAM 消耗从 1.2TB 减少到 350GB。在\(r=4\)且只有query 和 value 矩阵被调整的情况下,checkpoint 的大小大约减少了 10,000 倍(从 350GB 到 35MB)。另一个好处是,可以在部署时以更低的成本切换任务,只需更换 LoRA 的权重,而不是所有的参数。可以创建许多定制的模型,这些模型可以在将预训练的权重存储在 VRAM 中的机器上进行实时切换。在 GPT-3 175B 上训练时,与完全微调相比,速度提高了25%,因为我们不需要为绝大多数的参数计算梯度。

3. 实验分析

实验将五种方法进行对比,包括:Fine-Tuning (全量微调)、Bias-only or BitFit(只训练偏置向量)、Prefix-embedding tuning (PreEmbed,上文介绍的 Prefix Tuning 方法,只优化 embedding 层的激活)、Prefix-layer tuning (PreLayer,Prefix Tuning 方法,优化模型所有层的激活)Adapter tuning(不同的 Adapter 方法:AdapterH、AdapterL、 AdapterP、 AdapterL 、AdapterD)

实验结果以 LoRA 在 GPT-3 175B 上的验证分析为例。如下表所示,LoRA 在三个数据集上都能匹配或超过微调基准,证明了 LoRA 方法的有效性

img
GPT-3 上不同适配方法性能。展示了 WikiSQL 上的逻辑形式验证精度,MultiNLI-matched 上的精度,以及SAMSum上 的 Rouge-1/2/L 值。LoRA 比之前的方法表现更好,包括全量微调。在 WikiSQL 上的结果有 ±0.5% 左右的波动,MNLI-m 有 ±0.1% 左右的波动,SAMSum 有 ±0.2/±0.2/±0.1 左右的三个指标

但是,并不是所有方法都能从拥有更多的可训练参数中获益,而 LoRA 表现出更好的可扩展性和任务性能。当使用超过256个特殊token进行 Prefix-embedding tuning 或使用超过32个特殊 tokens 进行 Prefix-layer tuning时,可以观察到性能明显下降。

img

GPT-3 175B 准确率与WikiSQL和MNLI匹配的几种适配方法的可训练参数数的关系

[ 应该对 Transformer 中的哪些权重矩阵应用 LoRA ?] 把所有的参数放在\(\Delta W_q\)\(\Delta W_k\)中会导致性能明显下降,同时适配\(W_q\)\(W_v\)会产生最好的结果。这表明,即使\(r=4\)的较小秩也能在\(\Delta W\)中捕捉到足够的信息,因此,适配更多的权重矩阵比适配具有较大秩的单一类型的权重矩阵更可取

img
在可训练参数量相同的情况下,将LoRA应用于GPT-3中不同类型的注意力权重后,WikiSQL和MultiNLI的验证准确率

[ LORA的最佳秩是什么? ] LoRA 在很小的\(r\)下已经有了很好的表现了(适配\(\{W_q, W_v\}\)比只适配\(W_q\)更有竞争力)。这表明更新矩阵 \(\Delta W\)可能有一个很小的 "intrinsic rank",增加秩\(r\)不一定能够覆盖一个更有意义的子空间,一个低秩的适配矩阵已经足够

img
在WikiSQL和MultiNLI上用不同的秩 r 进行验证的准确性

[适配矩阵\(\Delta W\)\(W\)关系如何?] 通过计算\(U^TWV^T\)\(W\)投射到\(\Delta W\)\(r\)维子空间,\(U/V\)\(\Delta W\)的左/右奇异向量矩阵。然后,比较\(||U^TWV^T||_F\)\(||W||_F\)之间的 Frobenius 范数。作为比较,还计算了将\(||U^TWV^T||_F\)\(U\)\(V\)替换为\(W\)的前\(r\)个奇异向量或一个随机矩阵。

  • 与随机矩阵相比,\(\Delta W\)\(W\)有更强的相关性,表明\(\Delta W\)放大了\(W\)中已有的一些特征;
  • \(\Delta W\)没有重复\(W\)的顶级奇异方向,而只是放大了\(W\)中没有强调的方向;
  • 低秩适配矩阵可能会放大特定下游任务的重要特征,而这些特征在一般的预训练模型中没有得到强调
img
不同秩下 Frobenius范数