使用聊天数据微调 Llama3¶
Llama3 Instruct 引入了一种新的提示模板,用于使用聊天数据进行微调。在本教程中,我们将介绍快速开始准备自己的自定义聊天数据集以微调 Llama3 Instruct 所需的知识。
Llama3 Instruct 格式与 Llama2 的区别
关于提示模板和特殊标记的一切
如何使用自己的聊天数据集微调 Llama3 Instruct
熟悉 配置数据集
从 Llama2 到 Llama3 的模板更改¶
Llama2 聊天模型在提示预训练模型时需要特定的模板。由于聊天模型是用这种提示模板预训练的,如果你想对模型进行推理,你需要使用相同的模板,才能在聊天数据上获得最佳性能。否则,模型将只执行标准的文本完成,这可能与你的预期用例一致,也可能不一致。
从 Llama2 聊天模型的官方 Llama2 提示模板指南 中,我们可以看到添加了特殊标签
<s>[INST] <<SYS>>
You are a helpful, respectful, and honest assistant.
<</SYS>>
Hi! I am a human. [/INST] Hello there! Nice to meet you! I'm Meta AI, your friendly AI assistant </s>
Llama3 Instruct 彻底修改 了 Llama2 的模板,以更好地支持多轮对话。Llama3 Instruct 格式中的相同文本将如下所示
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
You are a helpful, respectful, and honest assistant.<|eot_id|><|start_header_id|>user<|end_header_id|>
Hi! I am a human.<|eot_id|><|start_header_id|>assistant<|end_header_id|>
Hello there! Nice to meet you! I'm Meta AI, your friendly AI assistant<|eot_id|>
这些标签完全不同,实际上它们与 Llama2 中的编码方式也不同。让我们逐步介绍用 Llama2 模板和 Llama3 模板对示例进行分词,以了解其中的原因。
注意
Llama3 基础模型使用的是 与 Llama3 Instruct 不同的提示模板,因为它尚未进行指令微调,额外的特殊标记未经训练。如果你对 Llama3 基础模型进行推理而不进行微调,我们建议使用基础模板以获得最佳性能。通常,对于指令和聊天数据,我们建议使用 Llama3 Instruct 及其提示模板。本教程的其余部分假定你使用的是 Llama3 Instruct。
分词提示模板和特殊标记¶
假设我有一个用户-助手轮次的示例,以及一个系统提示
sample = [
{
"role": "system",
"content": "You are a helpful, respectful, and honest assistant.",
},
{
"role": "user",
"content": "Who are the most influential hip-hop artists of all time?",
},
{
"role": "assistant",
"content": "Here is a list of some of the most influential hip-hop "
"artists of all time: 2Pac, Rakim, N.W.A., Run-D.M.C., and Nas.",
},
]
现在,让我们使用 Llama2ChatTemplate
类对它进行格式化,并看看它是如何被分词的。Llama2ChatTemplate 是提示模板的一个示例,它只用味道文本构建一个提示,以指示特定任务。
from torchtune.data import Llama2ChatTemplate, Message
messages = [Message.from_dict(msg) for msg in sample]
formatted_messages = Llama2ChatTemplate.format(messages)
print(formatted_messages)
# [
# Message(
# role='user',
# content='[INST] <<SYS>>\nYou are a helpful, respectful, and honest assistant.\n<</SYS>>\n\nWho are the most influential hip-hop artists of all time? [/INST] ',
# ...,
# ),
# Message(
# role='assistant',
# content='Here is a list of some of the most influential hip-hop artists of all time: 2Pac, Rakim, N.W.A., Run-D.M.C., and Nas.',
# ...,
# ),
# ]
Llama2 中还使用了一些特殊标记,它们不在提示模板中。如果你查看我们的 Llama2ChatTemplate
类,你会注意到我们没有包含 <s>
和 </s>
标记。它们是序列开始 (BOS) 和序列结束 (EOS) 标记,在分词器中的表示方式与提示模板中的其他部分不同。让我们用 Llama2 使用的 llama2_tokenizer()
对此示例进行分词,看看为什么。
from torchtune.models.llama2 import llama2_tokenizer
tokenizer = llama2_tokenizer("/tmp/Llama-2-7b-hf/tokenizer.model")
user_message = formatted_messages[0].text_content
tokens = tokenizer.encode(user_message, add_bos=True, add_eos=True)
print(tokens)
# [1, 518, 25580, 29962, 3532, 14816, 29903, 6778, ..., 2]
我们在对示例文本进行编码时添加了 BOS 和 EOS 标记。它显示为 ID 1 和 2。我们可以验证它们是否是我们的 BOS 和 EOS 标记。
print(tokenizer._spm_model.spm_model.piece_to_id("<s>"))
# 1
print(tokenizer._spm_model.spm_model.piece_to_id("</s>"))
# 2
BOS 和 EOS 标记是我们所说的特殊标记,因为它们有自己保留的标记 ID。这意味着它们将在模型学习的嵌入表中索引到它们自己的单独向量。提示模板中的其余标签,[INST]
和 <<SYS>>
则作为普通文本分词,而不是它们自己的 ID。
print(tokenizer.decode(518))
# '['
print(tokenizer.decode(25580))
# 'INST'
print(tokenizer.decode(29962))
# ']'
print(tokenizer.decode([3532, 14816, 29903, 6778]))
# '<<SYS>>'
需要注意的是,你不应该手动将特殊保留标记放在输入提示中,因为它将被视为普通文本,而不是特殊标记。
print(tokenizer.encode("<s>", add_bos=False, add_eos=False))
# [529, 29879, 29958]
现在让我们看看 Llama3 的格式是如何与 Llama2 不同地分词的。
from torchtune.models.llama3 import llama3_tokenizer
tokenizer = llama3_tokenizer("/tmp/Meta-Llama-3-8B-Instruct/original/tokenizer.model")
messages = [Message.from_dict(msg) for msg in sample]
tokens, mask = tokenizer.tokenize_messages(messages)
print(tokenizer.decode(tokens))
# '<|start_header_id|>system<|end_header_id|>\n\nYou are a helpful, respectful,
# and honest assistant.<|eot_id|><|start_header_id|>user<|end_header_id|>\n\nWho
# are the most influential hip-hop artists of all time?<|eot_id|><|start_header_id|>
# assistant<|end_header_id|>\n\nHere is a list of some of the most influential hip-hop
# artists of all time: 2Pac, Rakim, N.W.A., Run-D.M.C., and Nas.<|eot_id|>'
注意
我们使用了 Llama3 的 tokenize_messages
API,它与 encode 不同。它只是在对各个消息进行编码后管理在正确位置添加所有特殊标记。
我们可以看到分词器处理了所有格式,而无需我们指定提示模板。事实证明,所有附加标签都是特殊标记,我们不需要单独的提示模板。我们可以通过检查标签是否被编码为它们自己的标记 ID 来验证这一点。
print(tokenizer.special_tokens["<|begin_of_text|>"])
# 128000
print(tokenizer.special_tokens["<|eot_id|>"])
# 128009
最棒的是——所有这些特殊标记都是由分词器完全处理的。这意味着你不必担心弄乱任何必需的提示模板!
我什么时候应该使用提示模板?¶
是否使用提示模板取决于你想要的推理行为是什么。如果你对基础模型进行推理,并且它是在预训练时使用提示模板进行训练的,或者你希望让微调后的模型在推理时针对特定任务期望特定的提示结构,那么你应该使用提示模板。
对微调后的模型使用提示模板并不是严格必须的,但通常特定任务需要特定的模板。例如,SummarizeTemplate
提供了一个轻量级的结构来让微调后的模型针对要求总结文本的提示进行预训练。这将围绕用户消息进行包装,助手消息保持不变。
f"Summarize this dialogue:\n{dialogue}\n---\nSummary:\n"
你可以使用此模板对 Llama2 进行微调,即使模型最初是使用 Llama2ChatTemplate
进行预训练的,只要模型在推理过程中看到的是这个模板。模型应该足够健壮,可以适应新模板。
在自定义聊天数据集上微调¶
让我们通过尝试使用自定义聊天数据集微调 Llama3-8B 指令模型来测试我们的理解。我们将逐步介绍如何设置数据,以便它能够被正确地标记并馈送到我们的模型中。
假设我们有一个本地数据集,保存在一个 JSON 文件中,该文件包含与 AI 模型的对话。我们如何将类似的内容转换成 Llama3 能够理解并正确标记的格式呢?
# data/my_data.json
[
{
"dialogue": [
{
"from": "human",
"value": "What is your name?"
},
{
"from": "gpt",
"value": "I am an AI assistant, I don't have a name."
},
{
"from": "human",
"value": "Pretend you have a name."
},
{
"from": "gpt",
"value": "My name is Mark Zuckerberg."
}
]
},
]
首先,让我们看一下 通用数据集构建器,看看哪一个适合我们的用例。由于我们有对话数据,chat_dataset()
似乎是一个不错的选择。对于任何自定义的本地数据集,我们始终需要为 torchtune 中的任何数据集构建器指定 source
、data_files
和 split
。对于 chat_dataset()
,我们还需要额外指定 conversation_column
和 conversation_style
。我们的数据遵循 "sharegpt"
格式,因此我们可以在此处指定它。总而言之,我们的 chat_dataset()
调用应该如下所示
from torchtune.datasets import chat_dataset
from torchtune.models.llama3 import llama3_tokenizer
tokenizer = llama3_tokenizer("/tmp/Meta-Llama-3-8B-Instruct/original/tokenizer.model")
ds = chat_dataset(
tokenizer=tokenizer,
source="json",
data_files="data/my_data.json",
split="train",
conversation_column="dialogue",
conversation_style="sharegpt",
)
# In config
tokenizer:
_component_: torchtune.models.llama3.llama3_tokenizer
path: /tmp/Meta-Llama-3-8B-Instruct/original/tokenizer.model
dataset:
_component_: torchtune.datasets.chat_dataset
source: json
data_files: data/my_data.json
split: train
conversation_column: dialogue
conversation_style: sharegpt
注意
您可以将任何关键字参数传递给所有数据集类中的 load_dataset,它们将遵守这些参数。这对于常用的参数非常有用,例如使用 split
指定数据拆分,或使用 name
指定配置。
如果您需要添加提示模板,只需将其传递给标记器即可。由于我们正在微调 Llama3,标记器将为我们处理所有格式,并且提示模板是可选的。其他模型,例如 Mistral 的 MistralTokenizer
,默认情况下使用聊天模板 (MistralChatTemplate
) 来根据他们的 建议 格式化所有消息。
现在,我们已准备好开始微调了!我们将使用内置的 LoRA 单设备配方。使用 tune cp 命令获取 8B_lora_single_device.yaml
配置的副本,并使用您的数据集配置对其进行更新。
启动微调!
$ tune run lora_finetune_single_device --config custom_8B_lora_single_device.yaml epochs=15