开源 AI 食谱文档
使用 PEFT 进行 Prompt Tuning。
并获得增强的文档体验
开始使用
使用 PEFT 进行 Prompt Tuning。
作者:Pere Martra
在本笔记本中,我们将介绍如何使用 PEFT 库将 prompt tuning 应用于预训练模型。
有关与 PEFT 兼容的模型的完整列表,请参阅其文档。
可以使用 PEFT 训练的模型的简短示例包括 Bloom、Llama、GPT-J、GPT-2、BERT 等。Hugging Face 正在努力向库中添加更多模型。
Prompt Tuning 简介。
它是一种用于模型的附加微调技术。这意味着我们不会修改原始模型的任何权重。您可能会想,那么我们将如何执行微调呢?好吧,我们将训练添加到模型的附加层。这就是为什么它被称为附加技术。
考虑到它是一种附加技术,并且它的名称是 Prompt-Tuning,似乎很清楚我们将要添加和训练的层与 prompt 相关。
我们正在创建一种超级 prompt,通过使模型能够使用其获得的知识来增强 prompt 的一部分。但是,prompt 的特定部分无法翻译成自然语言。 这就像我们已经掌握了用嵌入表达自己和生成高效 prompt 的能力。
在每个训练周期中,唯一可以修改以最小化损失函数的权重是那些集成到 prompt 中的权重。
这项技术的主要结果是,要训练的参数数量非常少。但是,我们遇到了第二个,也许更重要的结果,即, 由于我们不修改预训练模型的权重,因此它不会改变其行为或忘记它以前学到的任何信息。
训练更快,成本效益更高。此外,我们可以训练各种模型,并且在推理时,我们只需要加载一个基础模型以及新的较小的训练模型,因为原始模型的权重没有被更改
我们将在笔记本中做什么?
我们将使用两个不同的数据集训练两个不同的模型,每个模型都来自 Bloom 系列的一个预训练模型。一个模型将使用 prompt 数据集进行训练,而另一个模型将使用励志句子数据集。我们将比较训练前后来自两个模型的相同问题的结果。
此外,我们将探索如何仅使用基础模型的一个副本加载内存中的两个模型。
加载 PEFT 库
该库包含 Hugging Face 对各种微调技术的实现,包括 Prompt Tuning
!pip install -q peft==0.8.2
!pip install -q datasets==2.14.5
从 transformers 库中,我们导入必要的类来实例化模型和分词器。
from transformers import AutoModelForCausalLM, AutoTokenizer
加载模型和分词器。
Bloom 是可用于使用 Prompt Tuning 的 PEFT 库进行训练的最小且最智能的模型之一。您可以从 Bloom Family 中选择任何模型,我鼓励您尝试至少两个模型以观察差异。
我选择最小的一个是为了最大限度地减少训练时间并避免 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
由于我们想要有两个不同的训练模型,我将创建两个不同的 prompt。
第一个模型将使用包含 prompt 的数据集进行训练,第二个模型将使用励志句子数据集。
第一个模型将收到 prompt “我希望你充当励志教练。”,第二个模型将收到“有两件美好的事情应该对你很重要:”
但首先,我将收集一些没有微调的模型的结果。
>>> 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.
)
创建两个 Prompt Tuning 模型。
我们将使用相同的预训练模型和相同的配置创建两个相同的 prompt tuning 模型。
>>> 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 时间),我们使用相同的基本模型作为基础训练了 2 个不同的模型,具有两个不同的任务。
保存模型
我们将保存模型。这些模型已准备好使用,只要我们将从中创建它们的预训练模型保存在内存中即可。
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.']
对于第二个模型,我们得到了类似的结果。
- 预训练模型: 有两件美好的事情应该对你很重要:你产品的价格和质量。
- 微调模型: 有两件美好的事情应该对你很重要:天气和你的健康。
结论
Prompt Tuning 是一项了不起的技术,可以为我们节省数小时的训练时间和大量资金。在笔记本中,我们在短短几分钟内训练了两个模型,并且我们可以将这两个模型都保存在内存中,为不同的客户端提供服务。
如果您想尝试不同的组合和模型,该笔记本已准备好使用 Bloom 系列中的另一个模型。
您可以更改要训练的 epoch 数量、虚拟 token 的数量以及第三个单元格中的模型。但是,有很多配置可以更改。如果您正在寻找一个好的练习,您可以将虚拟 token 的随机初始化替换为固定值。
微调模型的响应可能每次我们训练它们时都会有所不同。我已经粘贴了我的一次训练的结果,但实际结果可能会有所不同。