聊天模板
一个幽灵困扰着聊天模型——不正确格式化的幽灵!
总结
聊天模型在将对话转换为单个可标记字符串方面,使用了非常不同的格式进行训练。使用与模型训练时使用的格式不同的格式,通常会导致严重、无声的性能下降,因此匹配训练期间使用的格式至关重要!Hugging Face 分词器现在具有一个 chat_template
属性,可用于保存模型训练时使用的聊天格式。此属性包含一个 Jinja 模板,可将对话历史转换为正确格式的字符串。有关如何在代码中编写和应用聊天模板的信息,请参阅技术文档。
引言
如果你熟悉 🤗 Transformers 库,你可能写过这样的代码:
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModel.from_pretrained(checkpoint)
通过从相同的检查点加载分词器和模型,可以确保输入按照模型预期的方式进行分词。如果从不同的模型中选择分词器,输入分词可能会完全不同,结果是模型的性能会受到严重损害。这被称为分布偏移——模型一直在学习来自一个分布(其训练时的分词)的数据,然后突然转移到完全不同的分布。
无论你是对模型进行微调还是直接用于推理,最好都尽量减少这些分布偏移,并使你提供给模型的输入尽可能地与它训练时的输入相似。对于常规的语言模型,这样做相对容易——只需从相同的检查点加载分词器和模型即可。
然而,对于聊天模型来说,情况有点不同。这是因为“聊天”不仅仅是一个可以简单分词的文本字符串——它是一系列消息,每条消息都包含一个 role
以及 content
,也就是消息的实际文本。最常见的角色是用户发送消息的“user”,模型编写回复的“assistant”,以及可选的在对话开始时给出高层指令的“system”。
如果这听起来有点抽象,这里有一个聊天示例,让它更具体:
[
{"role": "user", "content": "Hi there!"},
{"role": "assistant", "content": "Nice to meet you!"}
]
在将这一系列消息分词并用作模型输入之前,需要将其转换为文本字符串。然而,问题在于有很多种方法可以进行这种转换!例如,你可以将消息列表转换为“即时通讯”格式:
User: Hey there!
Bot: Nice to meet you!
或者你可以添加特殊令牌来指示角色:
[USER] Hey there! [/USER]
[ASST] Nice to meet you! [/ASST]
或者你可以添加令牌来指示消息之间的边界,但将角色信息作为字符串插入:
<|im_start|>user
Hey there!<|im_end|>
<|im_start|>assistant
Nice to meet you!<|im_end|>
有很多方法可以做到这一点,而且没有一种方法是显而易见的最佳或正确方法。因此,不同的模型已经用截然不同的格式进行了训练。我没有编造这些例子;它们都是真实存在的,并且至少被一个活跃的模型使用!但是,一旦模型用某种格式进行了训练,你就真的需要确保将来的输入使用相同的格式,否则可能会导致性能下降的分布偏移。
模板:一种保存格式信息的方法
目前,如果你运气好,所需的格式会正确地记录在模型卡中。如果你运气不好,它就不在那里,所以如果你想使用该模型,那就祝你好运了。在极端情况下,我们甚至将整个提示格式放在一篇博客文章中,以确保用户不会错过它!即使在最好的情况下,你也必须找到模板信息,并在你的微调或推理管道中手动编写代码。我们认为这是一个特别危险的问题,因为使用错误的聊天格式是一种静默错误——你不会收到响亮的失败或 Python 异常来告诉你出了问题,模型只会比使用正确格式时表现得差很多,而且很难调试出原因!
这就是聊天模板旨在解决的问题。聊天模板是Jinja 模板字符串,它们与你的分词器一起保存和加载,并包含将聊天消息列表转换为模型正确格式输入所需的所有信息。这里有三个聊天模板字符串,对应于上面提到的三种消息格式:
{% for message in messages %}
{% if message['role'] == 'user' %}
{{ "User : " }}
{% else %}
{{ "Bot : " }}
{{ message['content'] + '\n' }}
{% endfor %}
{% for message in messages %}
{% if message['role'] == 'user' %}
{{ "[USER] " + message['content'] + " [/USER]" }}
{% else %}
{{ "[ASST] " + message['content'] + " [/ASST]" }}
{{ message['content'] + '\n' }}
{% endfor %}
"{% for message in messages %}"
"{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}"
"{% endfor %}"
如果您不熟悉 Jinja,我强烈建议您花一点时间查看这些模板字符串及其对应的模板输出,看看您是否能理解模板如何将消息列表转换为格式化字符串!语法在很多方面与 Python 非常相似。
为什么使用模板?
虽然 Jinja 最初可能让人困惑,如果您不熟悉它,但在实践中我们发现 Python 程序员可以很快掌握它。在开发此功能期间,我们考虑了其他方法,例如一个有限的系统,允许用户为消息指定每个角色的前缀和后缀。我们发现这可能会变得令人困惑和笨拙,而且非常不灵活,以至于需要针对多个模型进行临时变通。而模板则足够强大,可以清晰地支持我们所知道的所有消息格式。
为什么要这么麻烦?为什么不直接选择一种标准格式?
这是一个很好的主意!不幸的是,为时已晚,因为多个重要模型已经用非常不同的聊天格式进行了训练。
然而,我们仍然可以稍微缓解这个问题。我们认为最接近“标准”格式的是 OpenAI 创建的 ChatML 格式。如果你正在训练一个新的聊天模型,并且这种格式适合你,我们建议你使用它并向你的分词器添加特殊的 <|im_start|>
和 <|im_end|>
标记。它的优点是对角色非常灵活,因为角色只是作为字符串插入,而不是有特定的角色标记。如果你想使用这个模板,它是上面第三个模板,你可以用这个简单的一行代码设置它:
tokenizer.chat_template = "{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}"
然而,除了现有格式的泛滥之外,还有第二个不应该硬编码标准格式的原因——我们预计模板将在许多类型模型的预处理中广泛有用,包括那些可能与标准聊天有很大不同的模型。硬编码标准格式限制了模型开发者使用此功能执行我们甚至尚未想到的事情的能力,而模板则为用户和开发者提供了最大的自由。甚至可以在模板中编码检查和逻辑,这是我们在任何默认模板中都没有广泛使用的功能,但我们预计它在冒险用户手中将具有巨大的力量。我们坚信开源生态系统应该让你做你想做的事,而不是规定你被允许做什么。
模板如何工作?
聊天模板是分词器的一部分,因为它们扮演着与分词器相同的角色:它们存储有关数据预处理方式的信息,以确保您以与模型训练期间看到的相同格式向模型提供数据。我们设计它使得向现有分词器添加模板信息并保存或上传到 Hub 变得非常容易。
在聊天模板出现之前,聊天格式信息存储在类级别——这意味着,例如,所有 LLaMA 检查点都将获得相同的聊天格式,使用硬编码在 transformers
中用于 LLaMA 模型类的代码。为了向后兼容,具有自定义聊天格式方法的模型类已改为提供默认聊天模板。
默认聊天模板也设置在类级别,并告诉 ConversationPipeline
等类如何在模型没有聊天模板时格式化输入。我们这样做纯粹是为了向后兼容——我们强烈建议您在任何聊天模型上明确设置聊天模板,即使默认聊天模板是合适的。这确保了默认聊天模板中任何未来的更改或弃用都不会破坏您的模型。尽管我们将在可预见的未来保留默认聊天模板,但我们希望随着时间的推移将所有模型过渡到明确的聊天模板,届时默认聊天模板可能会被完全删除。
有关如何设置和应用聊天模板的信息,请参阅技术文档。
如何开始使用模板?
很简单!如果分词器设置了 chat_template
属性,它就可以使用了。您可以在 ConversationPipeline
中使用该模型和分词器,或者调用 tokenizer.apply_chat_template()
来格式化聊天以便进行推理或训练。有关更多信息,请参阅我们的开发者指南或 apply_chat_template 文档!
如果分词器没有 chat_template
属性,它可能仍然有效,但它会使用该模型类的默认聊天模板。正如我们上面提到的,这很不稳定,而且当类模板与模型实际训练的模板不匹配时,这也是导致无声错误的来源。如果您想使用一个没有 chat_template
的检查点,我们建议您查看模型卡等文档,以验证正确的格式,然后为该格式添加一个正确的 chat_template
。我们建议这样做,即使默认聊天模板是正确的——它使模型面向未来,并且还明确了模板是存在且合适的。
您甚至可以为不属于您的检查点添加 chat_template
,方法是发起拉取请求。您唯一需要做的更改是将 tokenizer.chat_template
属性设置为 Jinja 模板字符串。完成后,推送您的更改即可!
如果你想将检查点用于聊天,但找不到任何关于其使用的聊天格式的文档,你可能应该在检查点上提出问题或联系所有者!一旦你弄清楚模型使用的格式,请发起一个拉取请求来添加一个合适的 chat_template
。其他用户会非常感激的!
结论:模板哲学
我们认为模板是一项非常激动人心的改变。除了解决大量无声的、降低性能的 bug 之外,我们认为它们还开启了全新的方法和数据模态。也许最重要的是,它们也代表了一种哲学转变:它们将核心 transformers
代码库中的一个重要功能移到了各个模型仓库中,在那里用户可以自由地做一些奇怪、狂野和美妙的事情。我们很高兴看到您能发现它们的新用途!