Transformers 4.37 中文文档(七)(2)https://developer.aliyun.com/article/1564149
聊天模型的模板
原始文本:
huggingface.co/docs/transformers/v4.37.2/en/chat_templating
介绍
越来越常见的 LLMs 的用例是聊天。在聊天环境中,模型不是继续单个文本字符串(这是标准语言模型的情况),而是继续由一个或多个消息组成的对话,每个消息包括一个角色,如“用户”或“助手”,以及消息文本。
与标记化类似,不同的模型对于聊天期望非常不同的输入格式。这就是我们将聊天模板作为一个特性添加的原因。聊天模板是分词器的一部分。它们指定如何将表示为消息列表的对话转换为模型期望的单个可标记化字符串的格式。
让我们通过使用 BlenderBot
模型的一个快速示例来具体化这一点。BlenderBot 有一个非常简单的默认模板,主要是在对话轮之间添加空格:
>>> from transformers import AutoTokenizer >>> tokenizer = AutoTokenizer.from_pretrained("facebook/blenderbot-400M-distill") >>> chat = [ ... {"role": "user", "content": "Hello, how are you?"}, ... {"role": "assistant", "content": "I'm doing great. How can I help you today?"}, ... {"role": "user", "content": "I'd like to show off how chat templating works!"}, ... ] >>> tokenizer.apply_chat_template(chat, tokenize=False) " Hello, how are you? I'm doing great. How can I help you today? I'd like to show off how chat templating works!</s>"
请注意整个聊天被压缩成一个字符串。如果我们使用 tokenize=True
,这是默认设置,那么该字符串也将被标记化。然而,为了看到一个更复杂的模板在操作中的效果,让我们使用 mistralai/Mistral-7B-Instruct-v0.1
模型。
>>> from transformers import AutoTokenizer >>> tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-Instruct-v0.1") >>> chat = [ ... {"role": "user", "content": "Hello, how are you?"}, ... {"role": "assistant", "content": "I'm doing great. How can I help you today?"}, ... {"role": "user", "content": "I'd like to show off how chat templating works!"}, ... ] >>> tokenizer.apply_chat_template(chat, tokenize=False) "<s>[INST] Hello, how are you? [/INST]I'm doing great. How can I help you today?</s> [INST] I'd like to show off how chat templating works! [/INST]"
请注意,这次分词器已经添加了控制标记 [INST] 和 [/INST] 来指示用户消息的开始和结束(但不包括助手消息!)。Mistral-instruct 是使用这些标记进行训练的,但 BlenderBot 没有。
如何使用聊天模板?
正如您在上面的示例中所看到的,聊天模板很容易使用。只需构建一个带有 role
和 content
键的消息列表,然后将其传递给 apply_chat_template() 方法。一旦您这样做了,您将得到准备好的输出!当将聊天模板用作模型生成的输入时,使用 add_generation_prompt=True
添加一个 生成提示 也是一个好主意。
这是准备输入给 model.generate()
的示例,使用 Zephyr
助手模型:
from transformers import AutoModelForCausalLM, AutoTokenizer checkpoint = "HuggingFaceH4/zephyr-7b-beta" tokenizer = AutoTokenizer.from_pretrained(checkpoint) model = AutoModelForCausalLM.from_pretrained(checkpoint) # You may want to use bfloat16 and/or move to GPU here messages = [ { "role": "system", "content": "You are a friendly chatbot who always responds in the style of a pirate", }, {"role": "user", "content": "How many helicopters can a human eat in one sitting?"}, ] tokenized_chat = tokenizer.apply_chat_template(messages, tokenize=True, add_generation_prompt=True, return_tensors="pt") print(tokenizer.decode(tokenized_chat[0]))
这将产生一个符合 Zephyr 期望的输入格式的字符串。
<|system|> You are a friendly chatbot who always responds in the style of a pirate</s> <|user|> How many helicopters can a human eat in one sitting?</s> <|assistant|>
现在我们的输入已经正确格式化为 Zephyr,我们可以使用模型为用户的问题生成响应。
outputs = model.generate(tokenized_chat, max_new_tokens=128) print(tokenizer.decode(outputs[0]))
这将产生:
<|system|> You are a friendly chatbot who always responds in the style of a pirate</s> <|user|> How many helicopters can a human eat in one sitting?</s> <|assistant|> Matey, I'm afraid I must inform ye that humans cannot eat helicopters. Helicopters are not food, they are flying machines. Food is meant to be eaten, like a hearty plate o' grog, a savory bowl o' stew, or a delicious loaf o' bread. But helicopters, they be for transportin' and movin' around, not for eatin'. So, I'd say none, me hearties. None at all.
啊,原来如此简单!
是否有用于聊天的自动化管道?
是的,有:ConversationalPipeline。这个管道旨在使使用聊天模型变得容易。让我们再次尝试 Zephyr
示例,但这次使用管道:
from transformers import pipeline pipe = pipeline("conversational", "HuggingFaceH4/zephyr-7b-beta") messages = [ { "role": "system", "content": "You are a friendly chatbot who always responds in the style of a pirate", }, {"role": "user", "content": "How many helicopters can a human eat in one sitting?"}, ] print(pipe(messages))
Conversation id: 76d886a0-74bd-454e-9804-0467041a63dc system: You are a friendly chatbot who always responds in the style of a pirate user: How many helicopters can a human eat in one sitting? assistant: Matey, I'm afraid I must inform ye that humans cannot eat helicopters. Helicopters are not food, they are flying machines. Food is meant to be eaten, like a hearty plate o' grog, a savory bowl o' stew, or a delicious loaf o' bread. But helicopters, they be for transportin' and movin' around, not for eatin'. So, I'd say none, me hearties. None at all.
ConversationalPipeline 将负责所有的标记化细节,并为您调用 apply_chat_template
- 一旦模型有了聊天模板,您所需要做的就是初始化管道并将消息列表传递给它!
“生成提示”是什么?
您可能已经注意到 apply_chat_template
方法有一个 add_generation_prompt
参数。这个参数告诉模板添加指示机器人响应开始的标记。例如,考虑以下聊天:
messages = [ {"role": "user", "content": "Hi there!"}, {"role": "assistant", "content": "Nice to meet you!"}, {"role": "user", "content": "Can I ask a question?"} ]
这是没有生成提示的样子,使用我们在 Zephyr 示例中看到的 ChatML 模板:
tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=False) """<|im_start|>user Hi there!<|im_end|> <|im_start|>assistant Nice to meet you!<|im_end|> <|im_start|>user Can I ask a question?<|im_end|> """
这是带有生成提示的样子:
tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) """<|im_start|>user Hi there!<|im_end|> <|im_start|>assistant Nice to meet you!<|im_end|> <|im_start|>user Can I ask a question?<|im_end|> <|im_start|>assistant """
请注意,这一次,我们添加了指示机器人响应开始的标记。这确保了当模型生成文本时,它将写入一个机器人响应,而不是做一些意外的事情,比如继续用户的消息。请记住,聊天模型仍然只是语言模型 - 它们被训练来继续文本,而聊天只是对它们来说的一种特殊文本!您需要使用适当的控制标记来指导它们知道应该做什么。
并非所有模型都需要生成提示。一些模型,如 BlenderBot 和 LLaMA,在机器人响应之前没有任何特殊标记。在这些情况下,add_generation_prompt
参数将不起作用。add_generation_prompt
的确切效果将取决于所使用的模板。
我可以在训练中使用聊天模板吗?
是的!我们建议您将聊天模板应用为数据集的预处理步骤。之后,您可以像处理任何其他语言模型训练任务一样继续。在训练时,通常应设置add_generation_prompt=False
,因为在训练过程中,添加的提示助手响应的标记将不会有帮助。让我们看一个例子:
from transformers import AutoTokenizer from datasets import Dataset tokenizer = AutoTokenizer.from_pretrained("HuggingFaceH4/zephyr-7b-beta") chat1 = [ {"role": "user", "content": "Which is bigger, the moon or the sun?"}, {"role": "assistant", "content": "The sun."} ] chat2 = [ {"role": "user", "content": "Which is bigger, a virus or a bacterium?"}, {"role": "assistant", "content": "A bacterium."} ] dataset = Dataset.from_dict({"chat": [chat1, chat2]}) dataset = dataset.map(lambda x: {"formatted_chat": tokenizer.apply_chat_template(x["chat"], tokenize=False, add_generation_prompt=False)}) print(dataset['formatted_chat'][0])
然后我们得到:
<|user|> Which is bigger, the moon or the sun?</s> <|assistant|> The sun.</s>
从这里开始,就像处理标准语言建模任务一样继续训练,使用formatted_chat
列。
高级:聊天模板如何工作?
模型的聊天模板存储在tokenizer.chat_template
属性中。如果没有设置聊天模板,则将使用该模型类的默认模板。让我们看一下BlenderBot
的模板:
>>> from transformers import AutoTokenizer >>> tokenizer = AutoTokenizer.from_pretrained("facebook/blenderbot-400M-distill") >>> tokenizer.default_chat_template "{% for message in messages %}{% if message['role'] == 'user' %}{{ ' ' }}{% endif %}{{ message['content'] }}{% if not loop.last %}{{ ' ' }}{% endif %}{% endfor %}{{ eos_token }}"
这有点令人生畏。让我们添加一些换行和缩进,使其更易读。请注意,每个块后的第一个换行以及块之前的任何前导空格默认情况下会被忽略,使用 Jinja 的trim_blocks
和lstrip_blocks
标志。但是,请谨慎 - 尽管每行的前导空格被剥离,但同一行上块之间的空格不会被剥离。我们强烈建议检查您的模板是否在不应该的地方打印额外的空格!
{% for message in messages %} {% if message['role'] == 'user' %} {{ ' ' }} {% endif %} {{ message['content'] }} {% if not loop.last %} {{ ' ' }} {% endif %} {% endfor %} {{ eos_token }}
如果您以前从未见过这种模板,这是一个Jinja 模板。Jinja 是一种模板语言,允许您编写生成文本的简单代码。在许多方面,代码和语法类似于 Python。在纯 Python 中,这个模板看起来会像这样:
for idx, message in enumerate(messages): if message['role'] == 'user': print(' ') print(message['content']) if not idx == len(messages) - 1: # Check for the last message in the conversation print(' ') print(eos_token)
实际上,模板执行三件事:
- 对于每条消息,如果消息是用户消息,则在其前添加一个空格,否则不打印任何内容。
- 添加消息内容
- 如果消息不是最后一条消息,请在其后添加两个空格。在最后一条消息之后,打印 EOS 标记。
这是一个非常简单的模板 - 它不添加任何控制标记,也不支持“系统”消息,这是一种常见的方式,用于向模型提供关于其在随后对话中应该如何行为的指令。但是 Jinja 为您提供了很大的灵活性来执行这些操作!让我们看一个 Jinja 模板,可以类似于 LLaMA 格式化输入(请注意,真正的 LLaMA 模板包括处理默认系统消息以及一般情况下稍有不同的系统消息处理 - 不要在实际代码中使用这个!)
{% for message in messages %} {% if message['role'] == 'user' %} {{ bos_token + '[INST] ' + message['content'] + ' [/INST]' }} {% elif message['role'] == 'system' %} {{ '<<SYS>>\\n' + message['content'] + '\\n<</SYS>>\\n\\n' }} {% elif message['role'] == 'assistant' %} {{ ' ' + message['content'] + ' ' + eos_token }} {% endif %} {% endfor %}
希望如果您仔细看一下,您就能看出这个模板在做什么 - 它根据每条消息的“角色”添加特定的标记,这些标记代表发送者是谁。用户、助手和系统消息因为它们被包裹在其中的标记而清晰可辨。
高级:添加和编辑聊天模板
如何创建聊天模板?
简单,只需编写一个 Jinja 模板并设置tokenizer.chat_template
。您可能会发现,从另一个模型的现有模板开始,并为您的需求简单编辑它会更容易!例如,我们可以采用上面的 LLaMA 模板,并为助手消息添加"[ASST]“和”[/ASST]":
{% for message in messages %} {% if message['role'] == 'user' %} {{ bos_token + '[INST] ' + message['content'].strip() + ' [/INST]' }} {% elif message['role'] == 'system' %} {{ '<<SYS>>\\n' + message['content'].strip() + '\\n<</SYS>>\\n\\n' }} {% elif message['role'] == 'assistant' %} {{ '[ASST] ' + message['content'] + ' [/ASST]' + eos_token }} {% endif %} {% endfor %}
现在,只需设置tokenizer.chat_template
属性。下次使用 apply_chat_template(),它将使用您的新模板!此属性将保存在tokenizer_config.json
文件中,因此您可以使用 push_to_hub()将您的新模板上传到 Hub,并确保每个人都在使用正确的模板来使用您的模型!
template = tokenizer.chat_template template = template.replace("SYS", "SYSTEM") # Change the system token tokenizer.chat_template = template # Set the new template tokenizer.push_to_hub("model_name") # Upload your new template to the Hub!
使用您的聊天模板的方法 apply_chat_template()由 ConversationalPipeline 类调用,因此一旦您设置了正确的聊天模板,您的模型将自动与 ConversationalPipeline 兼容。
“默认”模板是什么?
在引入聊天模板之前,聊天处理是在模型类级别上硬编码的。为了向后兼容,我们保留了这种特定类处理作为默认模板,也在类级别上设置了。如果一个模型没有设置聊天模板,但是它的模型类有一个默认模板,ConversationalPipeline
类和apply_chat_template
等方法将使用类模板。您可以通过检查tokenizer.default_chat_template
属性来查找您的分词器的默认模板。
这是我们纯粹为了向后兼容性的原因而做的事情,以避免破坏任何现有的工作流程。即使类模板适用于您的模型,我们强烈建议通过将chat_template
属性显式设置来覆盖默认模板,以便向用户明确表明您的模型已正确配置为聊天,并为将来防范默认模板被修改或弃用的情况做好准备。
我应该使用哪个模板?
当为已经训练过的聊天模型设置模板时,您应该确保模板与模型在训练过程中看到的消息格式完全匹配,否则您可能会遇到性能下降。即使您继续训练模型,也是如此 - 如果保持聊天标记不变,您可能会获得最佳性能。这与标记化非常类似 - 在推理或微调时,当您精确匹配训练过程中使用的标记化时,通常会获得最佳性能。
如果您从头开始训练模型,或者在另一方面微调基础语言模型以用于聊天,您有很大的自由选择适当的模板!LLMs 足够聪明,可以学会处理许多不同的输入格式。我们为没有特定类别模板的模型提供的默认模板遵循 ChatML 格式,对于许多用例来说,这是一个很好的、灵活的选择。它看起来像这样:
{% for message in messages %} {{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}} {% endfor %}
如果您喜欢这个,这里有一个一行代码形式的版本,可以直接复制到您的代码中。这个一行代码还包括对生成提示的方便支持,但请注意它不会添加 BOS 或 EOS 标记!如果您的模型需要这些标记,apply_chat_template
不会自动添加它们 - 换句话说,文本将被使用add_special_tokens=False
进行标记化。这是为了避免模板和add_special_tokens
逻辑之间的潜在冲突。如果您的模型需要特殊标记,请确保将它们添加到模板中!
tokenizer.chat_template = "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}"
此模板将每条消息封装在<|im_start|>
和<|im_end|>
令牌中,并简单地将角色写入字符串,这样可以灵活地使用训练的角色。输出如下所示:
<|im_start|>system You are a helpful chatbot that will do its best not to say anything so stupid that people tweet about it.<|im_end|> <|im_start|>user How are you?<|im_end|> <|im_start|>assistant I'm doing great!<|im_end|>
“用户”、“系统”和“助手”角色是聊天的标准角色,我们建议在有意义的情况下使用它们,特别是如果您希望您的模型在 ConversationalPipeline 中运行良好。但是,您不限于这些角色 - 模板非常灵活,任何字符串都可以是一个角色。
我想添加一些聊天模板!我应该如何开始?
如果您有任何聊天模型,您应该设置它们的tokenizer.chat_template
属性,并使用 apply_chat_template()进行测试,然后将更新后的 tokenizer 推送到 Hub。即使您不是模型所有者 - 如果您使用的模型具有空白聊天模板,或者仍在使用默认类模板,请打开一个拉取请求到模型存储库,以便正确设置此属性!
一旦设置了属性,就完成了!tokenizer.apply_chat_template
现在将正确地为该模型工作,这意味着它也会自动支持像ConversationalPipeline
这样的地方!
通过确保模型具有此属性,我们可以确保整个社区都能够使用开源模型的全部功能。格式不匹配已经困扰该领域并悄悄损害了性能太久了 - 是时候结束它们了!
高级:模板编写提示
如果您对 Jinja 不熟悉,我们通常发现编写聊天模板的最简单方法是首先编写一个格式化消息的 Python 脚本,然后将该脚本转换为模板。
记住模板处理程序将接收对话历史作为名为messages
的变量。每条消息都是一个带有两个键role
和content
的字典。您可以在模板中像在 Python 中一样访问messages
,这意味着您可以使用{% for message in messages %}
循环遍历它,或者例如使用{{ messages[0] }}
访问单个消息。
您还可以使用以下提示将您的代码转换为 Jinja:
对于循环
Jinja 中的 for 循环如下所示:
{% for message in messages %} {{ message['content'] }} {% endfor %}
请注意,无论{{表达式块}}中有什么都将打印到输出中。您可以在表达式块内使用+
等运算符来组合字符串。
if 语句
Jinja 中的 if 语句如下所示:
{% if message['role'] == 'user' %} {{ message['content'] }} {% endif %}
请注意,Python 使用空格来标记for
和if
块的开始和结束位置,而 Jinja 要求您使用{% endfor %}
和{% endif %}
显式结束它们。
特殊变量
在您的模板中,您将可以访问messages
列表,但也可以访问几个其他特殊变量。这些包括像bos_token
和eos_token
这样的特殊标记,以及我们上面讨论过的add_generation_prompt
变量。您还可以使用loop
变量来访问有关当前循环迭代的信息,例如使用{% if loop.last %}
来检查当前消息是否是对话中的最后一条消息。以下是一个将这些想法结合在一起,在对话结束时添加生成提示的示例,如果add_generation_prompt
为True
:
{% if loop.last and add_generation_prompt %} {{ bos_token + 'Assistant:\n' }} {% endif %}
空格注意事项
尽可能地,我们已经尝试让 Jinja 忽略{{表达式}}之外的空格。但是,请注意,Jinja 是一个通用的模板引擎,它可能会将同一行上块之间的空格视为重要并将其打印到输出中。我们强烈建议在上传模板之前检查您的模板是否在不应该的地方打印额外的空格!
Transformers 4.37 中文文档(七)(4)https://developer.aliyun.com/article/1564152