简介
Prefix Tuning是2021.01提出来的,在它之前,我们使用prompt主要是人工设计模板或者自动化搜索模板,也就是prompt范式的第一阶段,就是在输入上加上prompt文本,再对输出进行映射。这种离散模板对模型的鲁棒性很差。所以后续的研究都将离散生成prompt方式转成连续的方式。
Prefix Tuning是在模型输入前添加一个连续的且是任务特定的向量序列,该序列称之为prefix,然后在训练的时候固定PLM的所有参数,只更新优化特定任务的prefix。
实现思路
1、与Full Fine Tuning对比
从下图上看,对于FFT,我们在微调的时候,需要更新所有的PLM的参数,训练时需要大量的数据,而且耗费的资源和时间比较多。
Prefix Tuning针对特定的任务,只更新前缀那部分的参数,训练时只需要很少的数据,训练速度也非常快。
2、自回归模型与Encoder-Decoder模型的实现区别:
对于类似于GPT-2的自回归模型,最终的结果 z = [PREFIX; x; y],参考下图的上半部分
对于类似于BART的Encoder-Decoder模型,最终的结果z = [PREFIX; x; PREFIX0 ; y],参考下图的下半部分
实验效果
从实验的效果看,还是非常不错的
在HF的PEFT中的实现
1、下面是代码实现
2、我们需要关注的仅仅是PrefixTuningConfig类的num_virtual_tokens
from peft import PromptTuningConfig, PromptTuningInit, get_peft_model
from transformers import get_linear_schedule_with_warmup
from tqdm import tqdm
peft_config = PrefixTuningConfig(task_type="CAUSAL_LM", num_virtual_tokens=20)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
#"trainable params: 983,040 || all params: 560,197,632 || trainable%: 0.1754809274167014"
lr = 3e-2
num_epochs = 50
optimizer = torch.optim.AdamW(model.parameters(), lr=lr)
lr_scheduler = get_linear_schedule_with_warmup(
optimizer=optimizer,
num_warmup_steps=0,
num_training_steps=(len(train_dataloader) * num_epochs),
)
device = "cuda"
model = model.to(device)
for epoch in range(num_epochs):
model.train()
total_loss = 0
for step, batch in enumerate(tqdm(train_dataloader)):
batch = {k: v.to(device) for k, v in batch.items()}
outputs = model(**batch)
loss = outputs.loss
total_loss += loss.detach().float()
loss.backward()
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
model.eval()
eval_loss = 0
eval_preds = []
for step, batch in enumerate(tqdm(eval_dataloader)):
batch = {k: v.to(device) for k, v in batch.items()}
with torch.no_grad():
outputs = model(**batch)
loss = outputs.loss
eval_loss += loss.detach().float()
eval_preds.extend(
tokenizer.batch_decode(torch.argmax(outputs.logits, -1).detach().cpu().numpy(), skip_special_tokens=True)
)
eval_epoch_loss = eval_loss / len(eval_dataloader)
eval_ppl = torch.exp(eval_epoch_loss)
train_epoch_loss = total_loss / len(train_dataloader)
train_ppl = torch.exp(train_epoch_loss)
print(f"{epoch=}: {train_ppl=} {train_epoch_loss=} {eval_ppl=} {eval_epoch_loss=}")
结论:
1、prefix部分到底使用多少个虚拟token,直接影响模型微调的参数量级,以及处理长文本的能力。默认的prefix长度为10
2、不同的前缀长度有不一样的性能表现,在一定程度上长度越长,prefix的效果越明显,但也可能出现降低的问题。
3、实验表明,prefix长度对推理速度影响不大,因为prefix上的attention计算是并行的。
4、前缀调整通过向输入序列插入特定于任务的前缀来修改模型的更多层,因此需要对更多参数进行微调
5、与Fine-tuning对比,Fine-tuning需要根据不同的任务训练所有的参数,所以所有的任务都需要copy一份model,但是prefix-tuning只需要优化prefix,所以只需要存储一个大的Transformer和一个学习得来的特定任务的prefix。
6、此外,prefix-tuning是模块化的:训练一个上游LM,引导下游的LM,而下游的LM保持不变。因此一个单一的LM可以一次性支持多个任务。所以基于prefix的体系结构可以使我们能再单个batch中处理多个用户的任务。