随着ChatGPT的快速崛起,大型模型的时代正在发生革命性变化。但对于很多人而言,进行大型模型的预训练或全面微调似乎是遥不可及的。
不过随着多种高效参数微调技术的涌现,科研人员和普通开发者都有机会尝试微调这些庞大的模型了。
本文我将分享了大模型微调技术的原理及代码案例 ,完整版代码,可在文末获取。
P-Tuning 简述
P-Tuning(论文:GPT Understands, Too),该方法将 Prompt 转换为可以学习的 Embedding 层,并用MLP+LSTM的方式来对Prompt Embedding进行一层处理。
相比Prefix Tuning,P-Tuning加入的可微的virtual token,但仅限于输入层,没有在每一层都加;另外,virtual token的位置也不一定是前缀,插入的位置是可选的。这里的出发点实际是把传统人工设计模版中的真实token替换成可微的virtual token。
经过预训练的LM的词嵌入已经变得高度离散,如果随机初始化virtual token,容易优化到局部最优值,而这些virtual token理论是应该有相关关联的。因此,作者通过实验发现用一个提示编码器(即用一个LSTM+MLP去编码这些virtual token以后,再输入到模型)来编码会收敛更快,效果更好。
P-Tuning 微调实战
为了不影响阅读体验,完整版代码在公众号:机器学习社区,回复:tuning,即可获取,这里仅列出关键步骤。
第一步,引进必要的库,如: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)
ifnot 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 技术进行实战讲解。
如果觉得我的文章能够能够给您带来帮助,期待您的点赞收藏加关注~~
技术交流群
完整版代码在公众号:机器学习社区,回复:tuning,即可获取。
建了实战技术交流群!想要进交流群的同学,可以直接加微信号:mlc2060。加的时候备注一下:研究方向 +学校/公司+知乎,即可。然后就可以拉你进群了。
前沿技术资讯、算法交流、求职内推、算法竞赛、面试交流(校招、社招、实习)等、与 10000+来自港科大、北大、清华、中科院、CMU、腾讯、百度等名校名企开发者互动交流~