随着,ChatGPT 迅速爆火,引发了大模型的时代变革。然而对于普通大众来说,进行大模型的预训练或者全量微调遥不可及。由此,催生了各种参数高效微调技术,让科研人员或者普通开发者有机会尝试微调大模型。
因此,该技术值得我们进行深入分析其背后的机理,之前分享了大模型参数高效微调技术原理综述的文章。下面给大家分享大模型参数高效微调技术实战系列文章,该系列共六篇文章,相关代码均放置在GitHub:llm-action。
- 大模型参数高效微调技术实战(一)-PEFT概述及环境搭建
- 大模型参数高效微调技术实战(二)-Prompt Tuning
- 大模型参数高效微调技术实战(三)-P-Tuning
- 大模型参数高效微调技术实战(四)-Prefix Tuning
- 大模型参数高效微调技术实战(五)-LoRA
- 大模型参数高效微调技术实战(六)-IA3
本文为大模型参数高效微调技术实战的第三篇。
P-Tuning 简述
P-Tuning(论文:GPT Understands, Too),该方法将 Prompt 转换为可以学习的 Embedding 层,并用MLP+LSTM的方式来对Prompt Embedding进行一层处理。
image.png
相比Prefix Tuning,P-Tuning加入的可微的virtual token,但仅限于输入层,没有在每一层都加;另外,virtual token的位置也不一定是前缀,插入的位置是可选的。这里的出发点实际是把传统人工设计模版中的真实token替换成可微的virtual token。
image.png
经过预训练的LM的词嵌入已经变得高度离散,如果随机初始化virtual token,容易优化到局部最优值,而这些virtual token理论是应该有相关关联的。因此,作者通过实验发现用一个提示编码器(即用一个LSTM+MLP去编码这些virtual token以后,再输入到模型)来编码会收敛更快,效果更好。
P-Tuning 微调实战
为了不影响阅读体验,详细的代码放置在GitHub:llm-action 项目中 peft_p_tuning_clm.ipynb 文件,这里仅列出关键步骤。
第一步,引进必要的库,如:P-Tuning 配置类 PromptEncoderConfig
。
from peft import (
get_peft_config,
get_peft_model,
get_peft_model_state_dict,
set_peft_model_state_dict,
PeftType,
TaskType,
PromptEncoderConfig,
)
第二步,创建 P-Tuning 微调方法对应的配置。
peft_config = PromptEncoderConfig(task_type=TaskType.CAUSAL_LM, num_virtual_tokens=20, encoder_hidden_size=128)
P-tuning 使用提示编码器(PromptEncoder)来优化提示参数,因此,您需要使用如下几个参数初始化 PromptEncoderConfig:
- task_type:训练的任务类型,如:序列分类(SEQ_CLS),因果语言建模(CAUSAL_LM)等。
- num_virtual_tokens:虚拟token的数量,换句话说就是提示(prompt)。
- encoder_hidden_size:编码器的隐藏大小,用于优化提示参数。
- encoder_reparameterization_type:指定如何重新参数化提示编码器,可选项有:MLP 或 LSTM,默认值为 MLP。
当使用 LSTM 时, 提示编码器结构如下:
(prompt_encoder): ModuleDict(
(default): PromptEncoder(
(embedding): Embedding(20, 1024)
(lstm_head): LSTM(1024, 128, num_layers=2, batch_first=True, bidirectional=True)
(mlp_head): Sequential(
(0): Linear(in_features=256, out_features=256, bias=True)
(1): ReLU()
(2): Linear(in_features=256, out_features=1024, bias=True)
)
)
)
当使用 MLP 时, 提示编码器结构如下:
(prompt_encoder): ModuleDict(
(default): PromptEncoder(
(embedding): Embedding(20, 1024)
(mlp_head): Sequential(
(0): Linear(in_features=1024, out_features=128, bias=True)
(1): ReLU()
(2): Linear(in_features=128, out_features=128, bias=True)
(3): ReLU()
(4): Linear(in_features=128, out_features=1024, bias=True)
)
)
)
PEFT 中的 P-tuning 的提示编码器是基于英伟达的NeMo库中 prompt_encoder.py 进行的重构,源码如下所示。
class PromptEncoder(torch.nn.Module):
def __init__(self, config):
super().__init__()
self.token_dim = config.token_dim
self.input_size = self.token_dim
self.output_size = self.token_dim
self.hidden_size = config.encoder_hidden_size
self.total_virtual_tokens = config.num_virtual_tokens * config.num_transformer_submodules
self.encoder_type = config.encoder_reparameterization_type
# 初始化 embedding 层
self.embedding = torch.nn.Embedding(self.total_virtual_tokens, self.token_dim)
if not config.inference_mode:
# 根据PromptEncoder重参数化类型初始化相应的lstm和mlp
if self.encoder_type == PromptEncoderReparameterizationType.LSTM:
lstm_dropout = config.encoder_dropout
num_layers = config.encoder_num_layers
# LSTM
self.lstm_head = torch.nn.LSTM(
input_size=self.input_size,
hidden_size=self.hidden_size,
num_layers=num_layers,
dropout=lstm_dropout,
bidirectional=True,
batch_first=True,
)
self.mlp_head = torch.nn.Sequential(
torch.nn.Linear(self.hidden_size * 2, self.hidden_size * 2),
torch.nn.ReLU(),
torch.nn.Linear(self.hidden_size * 2, self.output_size),
)
elif self.encoder_type == PromptEncoderReparameterizationType.MLP:
warnings.warn(
f"for {self.encoder_type}, the `encoder_num_layers` is ignored. Exactly 2 MLP layers are used."
)
layers = [
torch.nn.Linear(self.input_size, self.hidden_size),
torch.nn.ReLU(),
torch.nn.Linear(self.hidden_size, self.hidden_size),
torch.nn.ReLU(),
torch.nn.Linear(self.hidden_size, self.output_size),
]
self.mlp_head = torch.nn.Sequential(*layers)
else:
raise ValueError("Prompt encoder type not recognized. Please use one of MLP (recommended) or LSTM.")
def forward(self, indices):
input_embeds = self.embedding(indices)
if self.encoder_type == PromptEncoderReparameterizationType.LSTM:
output_embeds = self.mlp_head(self.lstm_head(input_embeds)[0])
elif self.encoder_type == PromptEncoderReparameterizationType.MLP:
output_embeds = self.mlp_head(input_embeds)
else:
raise ValueError("Prompt encoder type not recognized. Please use one of MLP (recommended) or LSTM.")
return output_embeds
第三步,通过调用 get_peft_model
方法包装基础的 Transformer 模型。
model = AutoModelForCausalLM.from_pretrained(model_name_or_path)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
通过 print_trainable_parameters 方法可以查看可训练参数的数量(仅为300,288)以及占比(仅为0.05366%)。
trainable params: 300,288 || all params: 559,514,880 || trainable%: 0.05366935013417338
第四步,模型训练的其余部分均无需更改,当模型训练完成之后,保存高效微调部分的模型权重以供模型推理即可。
peft_model_id = f"{model_name_or_path}_{peft_config.peft_type}_{peft_config.task_type}"
model.save_pretrained(peft_model_id)
输出的模型权重文件如下所示:
/data/nfs/llm/model/bloomz-560m_P_TUNING_CAUSAL_LM
├── [ 451] adapter_config.json
├── [ 81K] adapter_model.bin
└── [ 129] README.md
0 directories, 3 files
注意:这里只会保存经过训练的增量 PEFT 权重。其中,adapter_config.json
为 P-Tuning 配置文件;adapter_model.bin
为 P-Tuning 权重文件。
第五步,加载微调后的权重文件进行推理。
from peft import PeftModel, PeftConfig
peft_model_id = f"{model_name_or_path}_{peft_config.peft_type}_{peft_config.task_type}"
config = PeftConfig.from_pretrained(peft_model_id)
# 加载基础模型
model = AutoModelForCausalLM.from_pretrained(config.base_model_name_or_path)
# 加载PEFT模型
model = PeftModel.from_pretrained(model, peft_model_id)
# 编码
inputs = tokenizer(f'{text_column} : {dataset["test"][i]["Tweet text"]} Label : ', return_tensors="pt")
# 模型推理
outputs = model.generate(
input_ids=inputs["input_ids"],
attention_mask=inputs["attention_mask"],
max_new_tokens=10,
eos_token_id=3
)
# 解码
print(tokenizer.batch_decode(outputs.detach().cpu().numpy(), skip_special_tokens=True))
至此,我们完成了 P-Tuning 的训练及推理。
结语
本文对 P-Tuning 的基本原理进行了简述;同时,讲解了使用 P-Tuning 微调技术进行模型训练及推理。下文将对 Prefix Tuning / P-Tuning v2 技术进行实战讲解。