微调 Llama3 使用聊天数据¶
Llama3 Instruct 引入了一种新的提示模板,用于使用聊天数据进行微调。在本教程中,我们将介绍你需要了解的内容,以便快速开始准备自己的自定义聊天数据集,用于微调 Llama3 Instruct。
Llama3 指令格式与 Llama2 的区别
关于提示模板和特殊标记的全部内容
如何使用您自己的聊天数据集微调 Llama3 Instruct
熟悉 配置数据集
注意
本教程需要 torchtune 版本高于 0.1.1。
模板从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.",
},
]
现在,让我们用 Llama2ChatFormat 类来格式化它,并查看它是如何被标记化的。Llama2ChatFormat 是提示模板的一个示例,它仅通过带有任务指示的修饰文本来构建提示结构。
from torchtune.data import Llama2ChatFormat, Message
messages = [Message.from_dict(msg) for msg in sample]
formatted_messages = Llama2ChatFormat.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还使用了一些特殊的标记,这些标记不在提示模板中。
如果你查看我们的 Llama2ChatFormat 类,你会发现
我们没有包含 <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].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/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|>'
注意
我们使用了 tokenize_messages API 来处理 Llama3,这与 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"
即使模型最初是使用 Llama2ChatFormat 进行预训练的,您也可以使用此模板对 Llama2 进行微调,只要这是模型在推理过程中看到的内容。该模型应该足够健壮,能够适应新的模板。
在自定义聊天数据集上进行微调¶
让我们通过尝试使用自定义聊天数据集来微调 Llama3-8B 指令模型来测试我们的理解。我们将逐步讲解如何设置数据,以便它可以被正确地分词并输入到我们的模型中。
假设我们有一个本地数据集,以 CSV 文件格式保存,其中包含某个在线论坛中的问题和答案。那么,我们该如何将这类数据转换为 Llama3 能够理解并正确分词的格式呢?
import pandas as pd
df = pd.read_csv('your_file.csv', nrows=1)
print("Header:", df.columns.tolist())
# ['input', 'output']
print("First row:", df.iloc[0].tolist())
# [
# "How do GPS receivers communicate with satellites?",
# "The first thing to know is the communication is one-way...",
# ]
Llama3 分词器类 Llama3Tokenizer
期望输入采用 Message 格式。让我们
快速编写一个函数,将 CSV 文件中的单行解析为
Message 数据类。该函数还需要包含一个 train_on_input 参数。
def message_converter(sample: Mapping[str, Any], train_on_input: bool) -> List[Message]:
input_msg = sample["input"]
output_msg = sample["output"]
user_message = Message(
role="user",
content=input_msg,
masked=not train_on_input, # Mask if not training on prompt
)
assistant_message = Message(
role="assistant",
content=output_msg,
masked=False,
)
# A single turn conversation
messages = [user_message, assistant_message]
return messages
由于我们正在微调 Llama3,分词器将为我们处理提示的格式。但是,如果我们正在微调需要模板的模型,例如使用 MistralTokenizer 的 Mistral-7B 模型,我们需要使用像 MistralChatFormat 这样的聊天格式,根据其 建议 对所有消息进行格式化。
现在让我们为我们的数据集创建一个构建器函数,该函数加载我们的本地文件,使用我们的函数转换为消息列表,并创建一个 ChatDataset 对象。
def custom_dataset(
*,
tokenizer: ModelTokenizer,
max_seq_len: int = 2048, # You can expose this if you want to experiment
) -> ChatDataset:
return ChatDataset(
tokenizer=tokenizer,
# For local csv files, we specify "csv" as the source, just like in
# load_dataset
source="csv",
# Default split of "train" is required for local files
split="train",
convert_to_messages=message_converter,
# Llama3 does not need a chat format
chat_format=None,
max_seq_len=max_seq_len,
# To load a local file we specify it as data_files just like in
# load_dataset
data_files="your_file.csv",
)
注意
您可以将任何关键字参数传递给所有我们的load_dataset Dataset 类,它们将会遵循这些参数。这对于常见参数非常有用,例如使用split指定数据划分或使用name进行配置
现在我们准备开始微调!我们将使用内置的 LoRA 单设备配方。
使用 tune cp 命令获取 8B_lora_single_device.yaml 配置的副本,并将其更新以使用您的新数据集。为您的项目创建一个新文件夹,并确保数据集构建器和消息转换器保存在该目录中,然后在配置中指定它。
dataset:
_component_: path.to.my.custom_dataset
max_seq_len: 2048
启动微调!
$ tune run lora_finetune_single_device --config custom_8B_lora_single_device.yaml epochs=15