大语言模型peft微调技术

本文探讨了Adapter、DiffPruning、Prefix-tuning(包括P-Tuningv2)和LoRA等在自然语言处理中减少参数量、提高效率的方法。这些技术通过低秩适应、插件式适应或优化上下文提示,实现了在保持性能的同时显著减少微调所需的参数。实验结果显示,这些方法在GLUE数据集上表现出与全参数微调相近的效果,但参数量显著减少。
摘要由CSDN通过智能技术生成

Adapter

参考资料:《Parameter-efficient transfer learning for nlp》

adpater首先将原始的d维特征映射到较小的维度m,应用非线性函数,然后再重新映射回d维。总的参数量(包含biases)为 2md+d+m, 当m远小于d时,就能限制每个任务被增加的参数量。

adapter内部也有残差连接,也就是当adapter内的参数初始化为接近0时,相当于是一个增加adpater层前相同的模型。

在微调过程中更新以下参数:

1. adapter内部参数

2. layer norm层参数(实验表明单独训练layer norm层无法达到好结果)

3. 根据任务在最上层添加的层(例如分类任务的分类层)的参数

实验结果:

在GLUE参数集上,总体效果和所有参数重新训练的效果差不多,但训练的参数量减少很多。

Diff Pruning

参考资料:《Parameter-efficient transfer learning with diff pruning》

主要思路是学习一个diff vector \delta _{\tau },加到预训练的模型参数\theta _{pretrained}上,微调后使用的模型参数为\theta _{\tau }

微调训练时需要最小化的函数为

其中,

当 \delta _{\tau }稀疏时,就有,从而使微调时需要训练更新的参数是比较少的。就是\delta _{\tau }的L0-norm,使\delta _{\tau }尽量稀疏。

实验结果:

Prefix-tuning

参考资料:《Prefix-Tuning:Optimizing Continuous Prompts for Generation》

初衷是一个合适的上下文能在不改变参数的情况下使得语言模型的表现更好。但是,不同于让专家优化单个词,单个词容易受具体的词的影响,不同的词有不同的向量表示。而是将引导的上下文作为连续的词向量来优化,这样能通过前向传播影响整个网络中的所有层。

Prefix-tuning给了自回归的语言模型一个prefix,,或者同时给编码和解码结构一个prefix,,其中上图中的P_{idx}表示序列中prefix的序号。

P_{\theta }是初始化的一个可训练的prefix对应的参数矩阵,参数维度是\left | P_{idx} \right |\times dim(h_{i})

微调过程中的损失函数不变,仍然是自回归语言模型的损失函数,为

微调训练过程中更新的参数仅P_{\theta },初始语言模型的参数\phi不变。

微调过程中更新的参数:

直接更新P_{\theta }会导致优化不稳当以及最终效果的略微下降,P_{\theta }用一个MLP压缩到一个较小的矩阵{P_{\theta}}'{P_{\theta}}'P_{\theta }矩阵行数相同,列维度不同。当训练完成后,{P_{\theta}}'被丢弃,保留P_{\theta }

实验结果:

P-Tuning v2

参考资料:《P-Tuning v2:Prompt Tuning can be comparable to fine-tuning universally across scales and tasks》

【Prefix-tuning在后面都简称为P-tuning】

P-tuning的缺点:1. 并不能做到在所有规模参数的模型上表现都好,超过10 billion参数的模型表现好,100 million到1 billion规模参数的模型上相比全参数微调差很多。 2.不同任务上的表现不统一,例如在序列标注任务上表现差。

图左(a)是P-tuning的示意图,可以看出可训练的参数量(橘黄色部分)较少,而且并不直接作用于输出层。图右(b)在不同层都添加prompts当作prefix tokens,使得可训练参数量增加,且对预测输出的作用更直接。

实验结果:

相同参数量的prompt添加到不同层的结果如下图,可以看出在从深层添加会比从开头的浅层添加最终效果好。

LoRA

参考资料:《LoRA: Low-Rank adaption of large language models》

之前方法的不足:Adapter因为顺序计算会导致推断速度慢(如下图),P-tuning会占用一部分序列长度。

对于一个预训练的权重矩阵W_{0}\in R^{d\times k},将参数更新表示为W_{0}+\Delta W=W_{0} +BA,其中B\in R^{d\times r}A\in R^{r\times k}, r\ll min(d,k),也就是将后面更新部分用一个低秩分解表示。将前向计算表示为:。A使用高斯分布随机初始化,B初始化为0,\Delta W在训练开始时时0。然后将\Delta Wx乘以\frac{\alpha }{r}, 其中\alpha是一个常量, 改变其大小可以认为相当于学习率的作用。

如何理解r\alpha

以下代码摘自 Code LoRA from Scratch - a Lightning Studio by sebastiacode

【Code LoRA from Scratch这个对理解LoRA非常用帮助】

class LoRALayer(torch.nn.Module):
    def __init__(self, in_dim, out_dim, rank, alpha):
        super().__init__()
        std_dev = 1/torch.sqrt(torch.tensor(rank)).float()
        # rank是低秩矩阵的秩,较小的r会有一个较简单的低秩矩阵,使得微调过程中的参数较少,但所能捕捉到的信息也较少
        # A的维度 (in_dim, rank), B的维度 (rank, out_dim)
        self.A = torch.nn.Parameter(torch.randn(in_dim, rank)*std_dev)
        self.B = torch.nn.Parameter(torch.zeros(rank, out_dim))
        self.alpha = alpha

    def forward(self,x):
        # alpha决定了LoRA层的变化有多少作用给原始参数,值越大,对原参数的调整越大
        x = self.alpha* (x @ self.A @ self.B)
        return x

# 可以将原来的线性层用LinearWithLoRA来替换
class LinearWithLoRA(torch.nn.Module):
    def __init__(self, linear, rank, alpha):
        super().__init__()
        self.linear = linear
        self.lora = LoRALayer(linear.in_features, linear.out_features, rank, alpha)

    def forward(self,x):
        return self.linear(x) + self.lora(x)

论文认为这种形式。当r等于预训练参数矩阵的秩时,类似于一个通用的全部参数的fine-tune,。而且这种形式没有推断延迟。

将LoRA应用到Transformer中时,论文只调整attention参数,冻结MLP模块的参数,自注意力模块含四个参数矩阵(W_{q}, W_{k}, W_{v}, W_{o})

实验结果

论文作者还研究了以下问题:

1.应该对Transformer的哪个部分使用LoRA?

调整W_{q},W_{v}的效果不错,而且从表中可以看出调整多个矩阵会比调整单个矩阵的效果好。

2. LoRA最好的秩r是什么?

上表可以看出很小的r就能有不错的效果。

3. \Delta WW之间的关系,是否高度相关?【这部分暂时还没看】

如何在python中使用LoRA?

GitHub - microsoft/LoRA: Code for loralib, an implementation of "LoRA: Low-Rank Adaptation of Large Language Modelscan

参考quickstart部分,或者也可以使用 PEFT, 已经集成仅PEFT里面了GitHub - huggingface/peft: 🤗 PEFT: State-of-the-art Parameter-Efficient Fine-Tuning.

 LoRA的变种:

1. DoRA--Weight-Decomposed Low-Rank Adaptation

以下内容参考 Improving LoRA: Implementing Weight-Decomposed Low-Rank Adaptation (DoRA) from Scratch

DoRA可以用两步来概括,第一步是将一个预训练的参数矩阵分解成一个长度向量m和一个方向矩阵V。因为任何向量可以表示为长度和方向的乘积(如下图左)。在DoRA中,是对参数矩阵分解,而不是向量,矩阵的每一列参数连接了输入到输出,也就是将每一列看作向量做分解(如下图右)。

第二步是分别训练m 和 将LoRA作用于V。

预训练参数W_{0}=m\frac{V}{\left \| V \right \|_{c}}, 其中\left \| V \right \|_{c}是向量维度的norm。

DoRA后的参数{W}'=m\frac{V+\Delta V}{\left \| V+\Delta V \right \|}_{c}=m\frac{V+BA}{\left \| V+BA \right \|}_{c}

class LoRALayer(torch.nn.Module):
    def __init__(self, in_dim, out_dim, rank, alpha):
        super().__init__()
        std_dev = 1/torch.sqrt(torch.tensor(rank)).float()
        # rank是低秩矩阵的秩,较小的r会有一个较简单的低秩矩阵,使得微调过程中的参数较少,但所能捕捉到的信息也较少
        # A的维度 (in_dim, rank), B的维度 (rank, out_dim)
        self.A = torch.nn.Parameter(torch.randn(in_dim, rank)*std_dev)
        self.B = torch.nn.Parameter(torch.zeros(rank, out_dim))
        self.alpha = alpha

    def forward(self,x):
        # alpha决定了LoRA层的变化有多少作用给原始参数,值越大,对原参数的调整越大
        x = self.alpha* (x @ self.A @ self.B)
        return x

class LinearWithDoRAMerged(torch.nn.Module):
    def __init__(self, linear, rank, alpha):
        super().__init__()
        self.linear = linear
        self.lora = LoRALayer(linear.in_feature,linear.out_features, rank, alpha)
        # self.linear.weight.norm(p=2) 计算公式 sum(abs(x)**2)**(1./2), 即平方求和开根号
        self.m = torch.nn.Parameter(self.linear.weight.norm(p=2, dim=0, keepdim=True))

    def forward(self,x):
        lora = self.lora.A @ self.lora.B
        numerator =self.linear.weight + self.lora.alpha*lora.T
        denomator =numerator.norm(p=2, dim=0, keepdim=True)
        # norm之后可以使训练过程更稳定
        directional_component = numerator/denomator
        # self.m能动态调整在训练过程中结合参数向量到参数矩阵过程中每个参数向量的大小,类似参数向量的重要性
        new_weight = self.m * directional_component
        return F.linear(x, new_weight, self.linear.bias)

DoRA相比于LoRA的结果

当参数量仅为LoRA的一半时,DoRA的效果也比LoRA的效果好。

当超参rank变化时,DoRA的鲁棒性比LoRA更好。

DoRA方法也已经被集成到PEFT, GitHub - huggingface/peft: 🤗 PEFT: State-of-the-art Parameter-Efficient Fine-Tuning.

2. LISA

参考资料:《LISA: Layerwise Importance Sampling for Memory-Efficient Large Language Model Fine-Tuning》

为什么会想到LISA这个方法?

对模型每一层的参数计算mean_weight_norm, ,上图x轴表示的是层id,即哪一层,从embedding层到最后输出层,y轴表示的是norm值。可以看出,在LoRA中embedding层和head层的norm值明显大于中间层,而在全参数训练中这种现象不明显。LoRA对每层的重要性判断和全参数微调不同。

LISA认为norm值较小的层应该有较小的概率来对参数微调,但具体方法不是给每一层不同的学习率,而是对层采样。

其中, \left \{ p_{l} \right \}_{l=1}^{N_{L}}=\left \{ 1.0, \gamma /N_{L},\gamma /N_{L},\cdots ,\gamma /N_{L},1.0 \right \}\gamma控制更新多少层。表示0到1之间的均匀分布。

因为embedding层和head层的概率值为1,的条件不会被满足,这两层的参数一定会更新。

部分实验结果:

平均得分来看比LoRA高较多。

  • 24
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值