使用 PEFT 进行提示微调。
作者:Pere Martra
在本笔记本中,我们将介绍如何将 PEFT 库中的提示微调应用于预训练模型。
有关与 PEFT 兼容的完整模型列表,请参阅其文档。
可以使用 PEFT 进行训练的模型的简短示例包括 Bloom、Llama、GPT-J、GPT-2、BERT 等。Hugging Face 正在努力将更多模型添加到库中。
提示微调简介。
这是一种模型的增量式微调技术。这意味着我们**不会修改原始模型的任何权重**。您可能想知道,那么我们将如何执行微调呢?好吧,我们将训练添加到模型中的额外层。这就是它被称为增量式技术的原因。
考虑到它是一种增量式技术,并且它的名称是提示微调,因此很明显我们将要添加和训练的层与提示相关。
我们通过使模型能够利用其获得的知识增强提示的一部分来创建一种超级提示。但是,提示的特定部分无法翻译成自然语言。**就好像我们掌握了用嵌入表达自己并生成高效提示的方法。**
在每个训练周期中,唯一可以修改以最小化损失函数的权重是集成到提示中的权重。
这种技术的主要结果是,要训练的参数数量确实很少。但是,我们遇到了第二个可能更重要的结果,即,**由于我们没有修改预训练模型的权重,因此它不会改变其行为或忘记之前学习的任何信息。**
训练速度更快且成本效益更高。此外,我们可以训练各种模型,并且在推理期间,我们只需要加载一个基础模型以及新的较小的训练模型,因为原始模型的权重没有被改变。
我们在笔记本中将做什么?
我们将使用两个数据集训练两个不同的模型,每个数据集都只使用 Bloom 家族的一个预训练模型。一个模型将使用提示数据集进行训练,而另一个模型将使用励志句子数据集进行训练。在训练前后,我们将比较两个模型对相同问题的答案。
此外,我们还将探索如何只使用一个基础模型副本加载这两个模型到内存中。
加载 PEFT 库
该库包含 Hugging Face 对各种微调技术的实现,包括提示微调。
!pip install -q peft==0.8.2
!pip install -q datasets==2.14.5
从 transformers 库中,我们导入实例化模型和分词器所需的类。
from transformers import AutoModelForCausalLM, AutoTokenizer
加载模型和分词器。
Bloom 是可用于使用 PEFT 库和提示微调进行训练的最小巧妙的模型之一。您可以从 Bloom 家族中选择任何模型,我建议您至少尝试两个模型以观察差异。
我选择使用最小的模型以最大限度地减少训练时间并避免 Colab 中的内存问题。
model_name = "bigscience/bloomz-560m"
# model_name="bigscience/bloom-1b1"
NUM_VIRTUAL_TOKENS = 4
NUM_EPOCHS = 6
tokenizer = AutoTokenizer.from_pretrained(model_name)
foundational_model = AutoModelForCausalLM.from_pretrained(model_name, trust_remote_code=True)
使用预训练的 Bloom 模型进行推理
如果您希望获得更多样化和原创的生成结果,请取消以下注释:model.generate 中的 temperature、top_p 和 do_sample 参数。
使用默认配置,模型的响应在每次调用中保持一致。
# this function returns the outputs from the model received, and inputs.
def get_outputs(model, inputs, max_new_tokens=100):
outputs = model.generate(
input_ids=inputs["input_ids"],
attention_mask=inputs["attention_mask"],
max_new_tokens=max_new_tokens,
# temperature=0.2,
# top_p=0.95,
# do_sample=True,
repetition_penalty=1.5, # Avoid repetition.
early_stopping=True, # The model can stop before reach the max_length
eos_token_id=tokenizer.eos_token_id,
)
return outputs
由于我们希望有两个不同的训练模型,我将创建两个不同的提示。
第一个模型将使用包含提示的数据集进行训练,第二个模型将使用励志句子的数据集进行训练。
第一个模型将接收提示“我希望你扮演一名励志教练的角色。”,第二个模型将接收“有两件美好的事情应该对你很重要:”。
但首先,我将收集一些来自未进行微调的模型的结果。
>>> input_prompt = tokenizer("I want you to act as a motivational coach. ", return_tensors="pt")
>>> foundational_outputs_prompt = get_outputs(foundational_model, input_prompt, max_new_tokens=50)
>>> print(tokenizer.batch_decode(foundational_outputs_prompt, skip_special_tokens=True))
["I want you to act as a motivational coach. Don't be afraid of being challenged."]
>>> input_sentences = tokenizer("There are two nice things that should matter to you:", return_tensors="pt")
>>> foundational_outputs_sentence = get_outputs(foundational_model, input_sentences, max_new_tokens=50)
>>> print(tokenizer.batch_decode(foundational_outputs_sentence, skip_special_tokens=True))
['There are two nice things that should matter to you: the price and quality of your product.']
两个答案或多或少都是正确的。任何 Bloom 模型都是预训练的,可以准确而合理地生成句子。让我们看看,在训练之后,响应是否相等或生成得更准确。
准备数据集
使用的数据集为
- https://huggingface.co/datasets/fka/awesome-chatgpt-prompts
- https://huggingface.co/datasets/Abirate/english_quotes
import os
# os.environ["TOKENIZERS_PARALLELISM"] = "false"
from datasets import load_dataset
dataset_prompt = "fka/awesome-chatgpt-prompts"
# Create the Dataset to create prompts.
data_prompt = load_dataset(dataset_prompt)
data_prompt = data_prompt.map(lambda samples: tokenizer(samples["prompt"]), batched=True)
train_sample_prompt = data_prompt["train"].select(range(50))
display(train_sample_prompt)
>>> print(train_sample_prompt[:1])
{'act': ['Linux Terminal'], 'prompt': ['I want you to act as a linux terminal. I will type commands and you will reply with what the terminal should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. my first command is pwd'], 'input_ids': [[44, 4026, 1152, 427, 1769, 661, 267, 104105, 28434, 17, 473, 2152, 4105, 49123, 530, 1152, 2152, 57502, 1002, 3595, 368, 28434, 3403, 6460, 17, 473, 4026, 1152, 427, 3804, 57502, 1002, 368, 28434, 10014, 14652, 2592, 19826, 4400, 10973, 15, 530, 16915, 4384, 17, 727, 1130, 11602, 184637, 17, 727, 1130, 4105, 49123, 35262, 473, 32247, 1152, 427, 727, 1427, 17, 3262, 707, 3423, 427, 13485, 1152, 7747, 361, 170205, 15, 707, 2152, 727, 1427, 1331, 55385, 5484, 14652, 6291, 999, 117805, 731, 29726, 1119, 96, 17, 2670, 3968, 9361, 632, 269, 42512]], 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]}
dataset_sentences = load_dataset("Abirate/english_quotes")
data_sentences = dataset_sentences.map(lambda samples: tokenizer(samples["quote"]), batched=True)
train_sample_sentences = data_sentences["train"].select(range(25))
train_sample_sentences = train_sample_sentences.remove_columns(["author", "tags"])
display(train_sample_sentences)
微调。
PEFT 配置
API 文档: https://huggingface.co/docs/peft/main/en/package_reference/tuners#peft.PromptTuningConfig
我们可以对要训练的两个模型使用相同的配置。
from peft import get_peft_model, PromptTuningConfig, TaskType, PromptTuningInit
generation_config = PromptTuningConfig(
task_type=TaskType.CAUSAL_LM, # This type indicates the model will generate text.
prompt_tuning_init=PromptTuningInit.RANDOM, # The added virtual tokens are initializad with random numbers
num_virtual_tokens=NUM_VIRTUAL_TOKENS, # Number of virtual tokens to be added and trained.
tokenizer_name_or_path=model_name, # The pre-trained model.
)
创建两个提示微调模型。
我们将使用相同的预训练模型和相同的配置创建两个相同的提示微调模型。
>>> peft_model_prompt = get_peft_model(foundational_model, generation_config)
>>> print(peft_model_prompt.print_trainable_parameters())
trainable params: 4,096 || all params: 559,218,688 || trainable%: 0.0007324504863471229 None
>>> peft_model_sentences = get_peft_model(foundational_model, generation_config)
>>> print(peft_model_sentences.print_trainable_parameters())
trainable params: 4,096 || all params: 559,218,688 || trainable%: 0.0007324504863471229 None
太棒了:您是否看到了可训练参数的减少?我们将训练可用参数的 0.001%。
现在我们将创建训练参数,并且在两次训练中都将使用相同的配置。
from transformers import TrainingArguments
def create_training_arguments(path, learning_rate=0.0035, epochs=6):
training_args = TrainingArguments(
output_dir=path, # Where the model predictions and checkpoints will be written
use_cpu=True, # This is necessary for CPU clusters.
auto_find_batch_size=True, # Find a suitable batch size that will fit into memory automatically
learning_rate=learning_rate, # Higher learning rate than full Fine-Tuning
num_train_epochs=epochs,
)
return training_args
import os
working_dir = "./"
# Is best to store the models in separate folders.
# Create the name of the directories where to store the models.
output_directory_prompt = os.path.join(working_dir, "peft_outputs_prompt")
output_directory_sentences = os.path.join(working_dir, "peft_outputs_sentences")
# Just creating the directoris if not exist.
if not os.path.exists(working_dir):
os.mkdir(working_dir)
if not os.path.exists(output_directory_prompt):
os.mkdir(output_directory_prompt)
if not os.path.exists(output_directory_sentences):
os.mkdir(output_directory_sentences)
在创建 TrainingArguments 时,我们需要指示包含模型的目录。
training_args_prompt = create_training_arguments(output_directory_prompt, 0.003, NUM_EPOCHS)
training_args_sentences = create_training_arguments(output_directory_sentences, 0.003, NUM_EPOCHS)
训练
我们将为每个要训练的模型创建一个 Trainer 对象。
from transformers import Trainer, DataCollatorForLanguageModeling
def create_trainer(model, training_args, train_dataset):
trainer = Trainer(
model=model, # We pass in the PEFT version of the foundation model, bloomz-560M
args=training_args, # The args for the training.
train_dataset=train_dataset, # The dataset used to tyrain the model.
data_collator=DataCollatorForLanguageModeling(
tokenizer, mlm=False
), # mlm=False indicates not to use masked language modeling
)
return trainer
# Training first model.
trainer_prompt = create_trainer(peft_model_prompt, training_args_prompt, train_sample_prompt)
trainer_prompt.train()
# Training second model.
trainer_sentences = create_trainer(peft_model_sentences, training_args_sentences, train_sample_sentences)
trainer_sentences.train()
在不到 10 分钟的时间内(M1 Pro 上的 CPU 时间),我们训练了两个不同的模型,它们具有两个不同的任务,并以同一个基础模型为基础。
保存模型
我们将保存模型。只要我们拥有创建这些模型的基础模型副本,这些模型就可以随时使用。
trainer_prompt.model.save_pretrained(output_directory_prompt) trainer_sentences.model.save_pretrained(output_directory_sentences)
推理
您可以加载之前保存的模型路径,并要求模型根据我们的输入生成文本!
from peft import PeftModel
loaded_model_prompt = PeftModel.from_pretrained(
foundational_model,
output_directory_prompt,
# device_map='auto',
is_trainable=False,
)
>>> loaded_model_prompt_outputs = get_outputs(loaded_model_prompt, input_prompt)
>>> print(tokenizer.batch_decode(loaded_model_prompt_outputs, skip_special_tokens=True))
['I want you to act as a motivational coach. You will be helping students learn how they can improve their performance in the classroom and at school.']
如果我们比较这两个答案,某些内容发生了变化。
- 预训练模型: 我希望你扮演一名激励教练的角色。不要害怕接受挑战。
- 微调模型: 我希望你扮演一名激励教练的角色。如果你感到焦虑,可以使用这种方法。
我们必须记住,我们只训练了模型几分钟,但这些时间已经足够获得更接近我们期望的响应。
loaded_model_prompt.load_adapter(output_directory_sentences, adapter_name="quotes")
loaded_model_prompt.set_adapter("quotes")
>>> loaded_model_sentences_outputs = get_outputs(loaded_model_prompt, input_sentences)
>>> print(tokenizer.batch_decode(loaded_model_sentences_outputs, skip_special_tokens=True))
['There are two nice things that should matter to you: the weather and your health.']
第二个模型也获得了类似的结果。
- 预训练模型: 有两件不错的事情应该对你很重要:产品的价格和质量。
- 微调模型: 有两件不错的事情应该对你很重要:天气和你的健康。
结论
提示微调是一种非常棒的技术,可以为我们节省数小时的训练时间和大量的资金。在本笔记本中,我们仅用几分钟就训练了两个模型,并且可以将这两个模型都保存在内存中,为不同的客户提供服务。
如果您想尝试不同的组合和模型,该笔记本已准备好使用 Bloom 系列中的另一个模型。
您可以在第三个单元格中更改训练的轮次数量、虚拟标记的数量和模型。但是,有很多配置可以更改。如果您正在寻找一个好的练习,您可以用固定值替换虚拟标记的随机初始化。
每次我们训练微调模型时,其响应可能会有所不同。我粘贴了我其中一次训练的结果,但实际结果可能会有所差异。