PEFT 技术简介

转载自 https://mp.weixin.qq.com/s/E_0-skD3__w5jLGEJlDpoA

本文介绍了参数高效微调 Parameter-Efficient Fine-Tuning(PEFT)技术,可以仅微调少量或者额外的模型参数,并固定住大部分预训练参数,从而大大降低 LLM 的训练成本。

下图总结了一些最广泛使用的 PEFT 技术。PEFT 技术的主要做法包括 Adapter Tuning、Prefix Tuning 和 Prompt Tuning。Adapter Tuning 通过设计 Adapter 结构,只对新增的 Adapter 结构进行微调,保证训练的高效性。Prefix Tuning 是在输入 token 之前构造一段任务相关的 virtual tokens Prefix,训练的只更新 Prefix 部分的参数,而 Transformer 中的部分参数固定。Prompt Tuning 则是在输入层加入 prompt tokens 进行微调,主要在 T5 预训练模型上做实验。实验结果表明,PEFT 技术能够在提高模型效果的同时,缩短模型训练时间和计算成本,让更多人能够参与到深度学习研究中来。

大型预训练模型的训练成本估算,以 LLaMA 7B 训练为例:
估算一下: 以 bf16 半精度,每个参数用 2 个字节 (以 fp32 精度四字节的标准),训练时需要 8 个字节 (例如 Adam 优化器,参见 Tramsformers 的 性能文档)。可见 7B 参数量的模型将用 (2+8)* 7B = 70 GB 的内存,并且还可能需要更多用于计算诸如注意力分数的中间值。所以很难在一张 80GB 显存的 A100 上训练。或许你可以使用一些技巧,比如用更高效的半精度训练的优化器来压缩内存,但溢出是迟早的。

Adapter Tuning

谷歌的研究人员首次在论文《Parameter-Efficient Transfer Learning for NLP》提出针对 BERT 的 PEFT 微调方式,拉开了 PEFT 研究的序幕。他们指出,在面对特定的下游任务时,如果进行 Full-fintuning(即预训练模型中的所有参数都进行微调),太过低效;而如果采用固定预训练模型的某些层,只微调接近下游任务的那几层参数,又难以达到较好的效果。

于是他们设计了如下图所示的 Adapter 结构,将其嵌入 Transformer 的结构里面,在训练时,固定住原来预训练模型的参数不变,只对新增的 Adapter 结构进行微调。

同时为了保证训练的高效性(也就是尽可能少的引入更多参数),他们将 Adapter 设计为这样的结构:首先是一个 down-project 层将高维度特征映射到低维特征,然后过一个非线形层之后,再用一个 up-project 结构将低维特征映射回原来的高维特征;同时也设计了 skip-connection 结构,确保了在最差的情况下能够退化为 identity。

Python 伪代码如下:

从实验结果来看,该方法能够在只额外对增加的 3.6% 参数规模(相比原来预训练模型的参数量)的情况下取得和 Full-finetuning 接近的效果(GLUE 指标在 0.4% 以内)。

Prefix Tuning

Prefix Tuning 方法由斯坦福的研究人员提出,与 Full-finetuning 更新所有参数的方式不同,该方法是在输入 token 之前构造一段任务相关的 virtual tokens 作为 Prefix,然后训练的时候只更新 Prefix 部分的参数,而 Transformer 中的其他部分参数固定。该方法其实和构造 Prompt 类似,只是 Prompt 是人为构造的“显式”的提示,并且无法更新参数,而 Prefix 则是可以学习的“隐式”的提示。

同时,为了防止直接更新 Prefix 的参数导致训练不稳定的情况,他们在 Prefix 层前面加了 MLP 结构(相当于将 Prefix 分解为更小维度的 Input 与 MLP 的组合后输出的结果),训练完成后,只保留 Prefix 的参数。

实验结果也说明了 Prefix Tuning 的方式可以取得不错的效果。

Prompt Tuning

论文《The Power of Scale for Parameter-Efficient Prompt Tuning》
https://mp.weixin.qq.com/s/Wgj1ATMAkL1Gx4dsAlkJZw

我给这篇文章取了个新名字:Scale is All You Need,总的来说就是,只要模型规模够大,简单加入 Prompt tokens 进行微调,就能取得很好的效果。Prompt Tuning主要在 T5 预训练模型上做实验。

该方法可以看作是 Prefix Tuning 的简化版本,只对输入层加入的 prompt tokens embedding 进行微调,而Prefix是对虚拟Token对应的上游layer全部进行微调。因此Prompt-Tunning的微调参数量级要更小,且不需要修改原始模型结构,这是“简化”的来源。为什么上面 prefix-tuning 只微调embedding层效果就不好,放在prompt-tuning这里效果就好了呢?原因大概率跟模型 Scale 有关,似乎只要预训练模型足够强大,其他的一切都不是问题。作者也做实验说明随着预训练模型参数量的增加,Prompt Tuning 的方法会逼近 Fine-tune 的结果。

Fine-tune模型的每个副本都需要110亿个参数。而Prompt每个任务只需要20480个参数——减少超过5个数量级。

实验效果:随着预训练模型参数的增加,一切的问题都不是问题,最简单的设置也能达到极好的效果。

P-Tuning

P-Tuning 方法的提出主要是为了解决这样一个问题:大模型的 Prompt 构造方式严重影响下游任务的效果。

  • P-Tuning V1 提出将 Prompt 转换为可以学习的 Embedding 层
  • P-tuning v2 方法在多层加入了 Prompts tokens 作为输入

LoRA

梯度视角下的LoRA:简介、分析、猜测及推广
论文阅读:LORA-大型语言模型的低秩适应

微软和 CMU 的研究者指出,现有的一些 PEFT 的方法还存在这样一些问题:

  • 由于增加了模型的深度从而额外增加了模型推理的延时,如 Adapter 方法
  • Prompt 较难训练,同时减少了模型的可用序列长度,如 Prompt Tuning、Prefix Tuning、P-Tuning 方法
  • 往往效率和质量不可兼得,效果差于 full-finetuning

有研究者对语言模型的参数进行研究发现:语言模型虽然参数众多,但是起到关键作用的还是其中低秩的本质维度(low instrisic dimension)。受此观点的启发,Microsoft 于 2021 年发表的论文《LoRA: Low-Rank Adaptation of Large Language Models》提出了低秩适配 Low-Rank Adaption(LoRA)技术微调大型语言模型 (LLM)。设计了如下所示的结构,冻结预训练模型的权重参数,然后在每个 Transformer Block 引入 A、B 这样两个低秩矩阵模块,去模拟 Full-finetune 的过程。

LoRA与Transformer的结合也很简单,仅在QKV attention的计算中增加一个旁路,而不动MLP模块。基于大模型的内在低秩特性,增加旁路矩阵来模拟全模型参数微调,LoRA通过简单有效的方案来达成轻量微调的目的,可以将现在的各种大模型通过轻量微调变成各个不同领域的专业模型。

例如,假设 A=100 且 B=500 ,则$W$的大小为 100 × 500 = 50,000。现在,如果我们将其分解为两个较小的矩阵,一个 100×5 维矩阵$W_A$和一个 5×500 维矩阵 $W_B$。这两个矩阵总共只有 5×100+5×500=3000 个参数。需要优化的参数量大大减少!

方法具体介绍
对于预训练的参数矩阵$W_0∈ℝ^{m×n}$,我们不去直接微调$W_0$,而是对增量做低秩分解假设:

  • 其中$U,V$之一用全零初始化,另一个由随机高斯初始化,使得$∆W=UV$的初始值在训练开始时为零;
  • 训练时,$W_0$固定不变,优化器只优化$U,V$
  • 推理时,可将$UV$加到原参数上,不引入额外的推理延迟;
  • 可插拔式的切换任务,当前任务$W_0+U_1V_1$,将lora部分减掉,换成$W_0+U_2V_2$,即可实现任务切换;
  • 秩的选取:对于一般的任务,rank=1,2,4,8 足矣,而对于一些领域差距比较大的任务可能需要更大的rank。

这么做就能完美解决以上存在的 3 个问题:

  • 相比于原始的 Adapter 方法“额外”增加网络深度,必然会带来推理过程额外的延迟,该方法可以在推理阶段直接用训练好的 A、B 矩阵参数与原预训练模型的参数相加去替换原有预训练模型的参数,这样的话推理过程就相当于和 Full-finetune 一样,没有额外的计算量,从而不会带来性能的损失。
  • 由于没有使用 Prompt 方式,自然不会存在 Prompt 方法带来的一系列问题。
  • 该方法由于实际上相当于是用 LoRA 去模拟 Full-finetune 的过程,几乎不会带来任何训练效果的损失,后续的实验结果也证明了这一点。

注意:存储模型的所有权重$W’ = W_0 + W_A W_B$对于 LLM 来说可能非常大,因为 LLM 通常有数十亿到数万亿个权重参数。因此,我们可以保留原始模型$W$,只需要存储新的轻量级矩阵$W_A$和$W_B$。为了用具体数字说明这一点,一个完整的 7B LLaMA 检查点需要 23GB 的存储容量,而如果我们选择r=8的等级,LoRA 权重可以小到 8MB 。

在实验中,研究人员将这一 LoRA 模块与 Transformer 的 attention 模块相结合,在 RoBERTa 、DeBERTa、GPT-2 和 GPT-3 175B 这几个大模型上都做了实验,实验结果也充分证明了该方法的有效性。

PEFT对LORA的实现

使用 LoRA 和 Hugging Face 高效训练大语言模型

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
# 设置超参数及配置
LORA_R = 8
LORA_ALPHA = 16
LORA_DROPOUT = 0.05
TARGET_MODULES = [
"q_proj",
"v_proj",
]

config = LoraConfig(
r=LORA_R,
lora_alpha=LORA_ALPHA,
target_modules=TARGET_MODULES,
lora_dropout=LORA_DROPOUT,
bias="none",
task_type="CAUSAL_LM",
)

# 创建基础transformer模型
model = AutoModelForSeq2SeqLM.from_pretrained(model_name_or_path)
# 加入PEFT策略
model = get_peft_model(model, config)
model.print_trainable_parameters()

# trainable params: 18874368 || all params: 11154206720 || trainable%: 0.16921300163961817

LORA 伪代码

Parameter-Efficient LLM Finetuning With Low-Rank Adaptation (LoRA)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

input_dim = 768 # e.g., the hidden size of the pre-trained model
output_dim = 768 # e.g., the output size of the layer
rank = 8 # The rank 'r' for the low-rank adaptation

W = ... # from pretrained network with shape input_dim x output_dim

W_A = nn.Parameter(torch.empty(input_dim, rank)) # LoRA weight A
W_B = nn.Parameter(torch.empty(rank, output_dim)) # LoRA weight B

# Initialization of LoRA weights
nn.init.kaiming_uniform_(W_A, a=math.sqrt(5))
nn.init.zeros_(W_B)

def regular_forward_matmul(x, W):
h = x @ W
return h

def lora_forward_matmul(x, W, W_A, W_B):
h = x @ W # regular matrix multiplication
h += x @ W_A @ W_B # use LoRA weights
return alpha * h # alpha通常设置为 1

6 Towards a Unified View of PETL

这篇 ICLR2022 的文章研究了典型的 PEFT 方法,试图将 PEFT 统一到一个框架下,找出它们起作用的具体原因,并进行改进。主要研究了三个问题:

  • 典型的PEFT方法有什么联系?
  • 典型的PEFT方法中是哪些关键模块在起作用?
  • 能否对这些关键模块进行排列组合,找出更有用的 PEFT 方法?

通用形式

通过对 Prefix Tuning 的推导,得出了和 Adapter Tuning 以及 LoRA 形式一致的形式。

更近一步地,可以将这些 Tuning 的方法统一在同一套框架下,

PS: PEFT实现

  1. https://github.com/huggingface/peft huggingface PEFT
  2. https://github.com/jxhe/unify-parameter-efficient-tuning

PS: Fine-tuning、Instruction-tuning、Prompt-Tuning的区别?

  1. 之前 Fine-tuning 是在某个下游任务 + 少量的 labels → 去微调/适配 PTM,比如针对猫狗分类任务进行微调。Fine Tuning需要有大量的任务相关数据,且可能会导致预训练的通用性能力下降。
  2. Prompt-tuning 为具体的任务生成一个prompt模板以适应大模型进行微调,例如给定“请分类这张图片是否是猫”作为Prompt,让模型输出是或不是。这种方法可以让模型更好地遵循任务的要求,但是需要手动设计Prompt,且精调后的模型只能应用于特定的单一任务!
  3. Instruction-tuning 则是在多个已知任务上进行微调(通过自然语言的形式),通过大量的任务指令+大量的 labels → 去适配不同的任务,最后在某个新任务上进行 zero-shot 推理。Instruction Tuning可以减少手动设计Prompt的工作量,同时也能够保持预训练模型的通用性能力。

Instruction Tuning 和 Prompt-tuning 的核心一样,就是去发掘语言模型本身具备的知识。而他们的不同点就在于,Prompt是去激发语言模型的补全能力,比如给出上半句生成下半句、或者做完形填空,都还是像在做language model任务,它的模版是这样的:

而Instruction Tuning则是激发语言模型的理解能力,通过给出更明显的指令/指示,让模型去理解并做出正确的action。比如NLI/分类任务:

参考


PEFT 技术简介
http://example.com/2023/05/18/2023-05-18-PEFT技术简介/
作者
Ning Shixian
发布于
2023年5月18日
许可协议