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 ,加到预训练的模型参数上,微调后使用的模型参数为
微调训练时需要最小化的函数为
其中,
当 稀疏时,就有,从而使微调时需要训练更新的参数是比较少的。就是的L0-norm,使尽量稀疏。
实验结果:
Prefix-tuning
参考资料:《Prefix-Tuning:Optimizing Continuous Prompts for Generation》
初衷是一个合适的上下文能在不改变参数的情况下使得语言模型的表现更好。但是,不同于让专家优化单个词,单个词容易受具体的词的影响,不同的词有不同的向量表示。而是将引导的上下文作为连续的词向量来优化,这样能通过前向传播影响整个网络中的所有层。
Prefix-tuning给了自回归的语言模型一个prefix,,或者同时给编码和解码结构一个prefix,,其中上图中的表示序列中prefix的序号。
是初始化的一个可训练的prefix对应的参数矩阵,参数维度是。
微调过程中的损失函数不变,仍然是自回归语言模型的损失函数,为
微调训练过程中更新的参数仅,初始语言模型的参数不变。
微调过程中更新的参数:
直接更新会导致优化不稳当以及最终效果的略微下降,用一个MLP压缩到一个较小的矩阵,和矩阵行数相同,列维度不同。当训练完成后,被丢弃,保留。
实验结果:
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会占用一部分序列长度。
对于一个预训练的权重矩阵,将参数更新表示为,其中,, ,也就是将后面更新部分用一个低秩分解表示。将前向计算表示为:。A使用高斯分布随机初始化,B初始化为0,在训练开始时时0。然后将乘以, 其中是一个常量, 改变其大小可以认为相当于学习率的作用。
如何理解和?
以下代码摘自 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模块的参数,自注意力模块含四个参数矩阵
实验结果
论文作者还研究了以下问题:
1.应该对Transformer的哪个部分使用LoRA?
调整的效果不错,而且从表中可以看出调整多个矩阵会比调整单个矩阵的效果好。
2. LoRA最好的秩r是什么?
上表可以看出很小的r就能有不错的效果。
3. 和之间的关系,是否高度相关?【这部分暂时还没看】
如何在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可以用两步来概括,第一步是将一个预训练的参数矩阵分解成一个长度向量和一个方向矩阵。因为任何向量可以表示为长度和方向的乘积(如下图左)。在DoRA中,是对参数矩阵分解,而不是向量,矩阵的每一列参数连接了输入到输出,也就是将每一列看作向量做分解(如下图右)。
第二步是分别训练 和 将LoRA作用于V。
预训练参数, 其中是向量维度的norm。
DoRA后的参数
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值较小的层应该有较小的概率来对参数微调,但具体方法不是给每一层不同的学习率,而是对层采样。
其中, ,控制更新多少层。表示0到1之间的均匀分布。
因为embedding层和head层的概率值为1,的条件不会被满足,这两层的参数一定会更新。
部分实验结果:
平均得分来看比LoRA高较多。