聊天模板:结束无声的性能杀手

聊天模板:说再见给无声的性能杀手

警告:错误的格式化闪现在聊天模型之中!

简明总结

聊天模型是使用非常不同的格式进行对话转换为单个可标记化字符串的训练。使用与模型训练时不同的格式通常会导致严重且悄无声息的性能下降,因此匹配训练过程中使用的格式非常重要!Hugging Face的分词器现在具有一个名为chat_template的属性,可用于保存模型训练时使用的聊天格式。此属性包含将对话历史转换为正确格式字符串的Jinja模板。请参阅技术文档了解如何在代码中编写和应用聊天模板的信息。

介绍

如果您熟悉🤗 Transformers库,可能会编写以下类似的代码:

tokenizer = AutoTokenizer.from_pretrained(checkpoint)model = AutoModel.from_pretrained(checkpoint)

通过从同一checkpoint加载标记器和模型,确保输入被令牌化为模型所期望的方式。如果选择来自另一个模型的标记器,输入的分词可能会完全不同,结果将导致模型的性能严重受损。这被称为分布偏移 – 模型一直在学习来自一个分布(它所训练的分词)的数据,突然间它已经转移到一个完全不同的分布。

无论是微调模型还是直接进行推理,尽量减少这些分布偏移并使输入尽可能保持与模型训练时的输入相似总是个好主意。对于常规语言模型,这相对容易 – 只需从同一个checkpoint加载标记器和模型,就可以开始使用。

然而,在聊天模型中,情况稍有不同。这是因为“聊天”不仅仅是一个可简单进行令牌化的文本字符串 – 它是一个消息序列,每个消息都包含role(角色)和content(实际消息文本)。最常见的角色是用户发送的消息是“user”,模型写的回复是“assistant”,在对话开始时还可以使用“system”用来给出高级指令。

如果这一切听起来有点抽象,下面是一个聊天示例,可以使其更具体:

[    {"role": "user", "content": "嗨!"},    {"role": "assistant", "content": "很高兴见到你!"}]

在将这些消息序列转换为文本字符串之前,需要将其进行转换。然后,可以对其进行令牌化并用作模型的输入。然而,问题在于有很多种方法可以进行此转换!例如,您可以将消息列表转换为“即时通讯”格式:

用户:嘿!机器人:很高兴见到你!

或者您可以添加特殊标记以指示角色:

[USER] 嘿! [/USER][ASST] 很高兴见到你! [/ASST]

或者您可以添加标记以指示消息之间的边界,但将角色信息作为字符串插入其中:

<|im_start|>user嘿!<|im_end|><|im_start|>assistant很高兴见到你!<|im_end|>

有许多方法可以实现这一点,没有明显最好或正确的方法。结果导致不同模型受到极其不同的格式化训练。我不是编造这些例子; 它们都是真实存在并且至少被一个活跃模型使用!但是,一旦使用特定格式对模型进行了训练,您确实希望将来的输入使用相同的格式,否则可能会发生破坏性的分布偏移。

模板:保存格式信息的一种方法

目前,如果你很幸运,你所需的格式在模型卡片的某个位置有正确的文档记录。如果你不幸的话,可能没有文档,在那种情况下,祝你好运!在极端情况下,我们甚至在博客文章中放置了整个提示格式,以确保用户不会错过它!即使在最理想的情况下,您还需找到模板信息并在微调或推理流程中手动编写它。我们认为这是一个尤其危险的问题,因为使用错误的聊天格式是一个悄无声息的错误 – 您不会收到响亮的失败或Python异常来告诉您出现问题,模型的性能只是比使用正确格式时差很多,很难调试错误的原因!

这是聊天模板旨在解决的问题。聊天模板是保存和加载到您的分词器中的Jinja模板字符串,包含将聊天消息列表转换为正确格式化的输入以供模型使用所需的所有信息。以下是三个聊天模板字符串,对应于上述三种消息格式:

{% for message in messages %}    {% if message['role'] == 'user' %}        {{ "用户: " }}    {% else %}        {{ "机器人: " }}    {{ message['content'] + '\n' }}{% endfor %}

{% for message in messages %}    {% if message['role'] == 'user' %}        {{ "[用户] " + message['content'] + " [/用户]" }}    {% else %}        {{ "[助手] " + message['content'] + " [/助手]" }}    {{ 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 %}"

还有一个不将标准格式硬编码的原因是,除了现有格式的广泛使用外,我们预计模板将在许多类型的模型预处理中广泛使用,包括那些可能在很多方面与标准聊天不同的模型。硬编码一个标准格式限制了模型开发者利用此功能进行我们甚至还没有考虑到的操作的能力,而模板化则为用户和开发者提供了最大的自由。甚至可以在模板中编码检查和逻辑,这是我们不在任何默认模板中广泛使用的功能,但是我们预计它在有冒险精神的用户手中具有巨大的威力。我们坚信,开源生态系统应该让您做您想做的事情,而不是告诉您可以做什么。

模板是如何工作的?

聊天模板是Tokenizer的一部分,因为它们起到与分词器相同的作用:它们存储有关数据如何进行预处理的信息,以确保您以与训练过程中模型所见到的相同格式提供数据。我们设计它非常容易向现有的分词器添加模板信息,并保存或上传到Hub上。

在聊天模板之前,聊天格式信息存储在类级别 – 这意味着,例如,所有LLaMA检查点将使用在LLaMA模型类的transformers中硬编码的代码使用相同的聊天格式。为了向后兼容,具有自定义聊天格式方法的模型类已被赋予默认聊天模板

默认聊天模板也在类级别设置,并且告诉ConversationPipeline等类在模型没有聊天模板时如何格式化输入。我们这样做纯粹是为了向后兼容 – 我们强烈建议您在任何聊天模型上明确设置聊天模板,即使默认聊天模板是适当的。这可以确保默认聊天模板的任何未来更改或弃用不会破坏您的模型。虽然在可预见的未来我们将保留默认聊天模板,但我们希望随着时间的推移将所有模型过渡到显式聊天模板,届时可能会完全删除默认聊天模板。

有关如何设置和应用聊天模板的信息,请参阅技术文档

我该如何开始使用模板?

很容易!如果分词器设置了chat_template属性,它就准备就绪了。您可以在ConversationPipeline中使用该模型和分词器,或者您可以调用tokenizer.apply_chat_template()来格式化用于推理或训练的聊天。请参阅我们的开发者指南apply_chat_template文档以获取更多信息!

如果分词器没有chat_template属性,它可能仍然可以工作,但它将使用为该模型类设置的默认聊天模板。正如我们上面提到的那样,这是脆弱的,并且在类模板与实际训练模型不匹配时也是潜在的错误来源。如果您想使用没有chat_template的检查点,则建议您检查类似于模型卡的文档以验证正确格式,并添加正确的chat_template。即使默认聊天模板是正确的,我们也建议这样做 – 这可以使模型具备未来性,并清楚地表明该模板存在且适用。

您甚至可以为不是您所有的检查点添加聊天模板,方法是打开一个pull request。您唯一需要做出的更改是将tokenizer.chat_template属性设置为Jinja模板字符串。完成后,推送更改并准备就绪!

如果您想使用用于聊天的检查点,但找不到有关所使用的聊天格式的任何文档,请您可能需要在检查点上打开一个问题或联系所有者!在弄清模型使用的格式后,请打开一个pull request以添加适当的chat_template。其他用户将非常感激!

结论:模板哲学

我们认为模板是一个非常令人兴奋的变化。除了解决大量潜在的、影响性能的错误之外,我们认为它们还开辟了全新的方法和数据模态。也许最重要的是,它们代表了一种哲学转变:将一个重要的函数从核心的transformers代码库中移出,并将其移入各个模型仓库,从而赋予用户去进行奇特、大胆和美妙的事情的自由。我们很期待看到您对其的用途发挥!