跳到正文
原文
Hugging Face:Blog(RSS)·· 2023-10-03精选AI 评分79

Hugging Face 推出 chat_template 属性解决聊天格式静默性能问题

Chat Templates: An End to the Silent Performance Killer

AI 导读

Hugging Face 在 tokenizers 中新增 chat_template 属性,用 Jinja 模板保存模型训练时的聊天格式,避免因格式不匹配导致的静默性能下降。

推荐理由

原文解释了错误聊天格式如何造成静默性能下降,并给出用 Jinja 模板随 tokenizer 保存格式的具体做法。

正文 · AI 翻译
一个幽灵在聊天模型间游荡——格式错误的幽灵!

简而言之

聊天模型在训练时使用了非常不同的格式来将对话转换为单个可标记化的字符串。使用与模型训练时不同的格式通常会导致严重的、无声的性能下降,因此匹配训练时使用的格式极其重要!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 %}"

不过,除了现有格式繁多之外,还有一个不硬编码标准格式的理由——我们预期模板在许多类型模型的预处理中都会广泛有用,包括那些可能与标准聊天做非常不同事情的模型。硬编码标准格式会限制模型开发者使用此功能去做我们甚至还没想到的事情,而模板化则给用户和开发者最大的自由。甚至可以在模板中编码检查和逻辑,这是我们在任何默认模板中都没有广泛使用的功能,但我们预期它在敢于冒险的用户手中会有巨大的威力。我们坚信,开源生态系统应该让你能够做你想做的事,而不是规定你被允许做什么。

模板如何工作?

聊天模板是 tokenizer 的一部分,因为它们与 tokenizer 扮演相同的角色:它们存储关于数据如何被预处理的信息,以确保你以模型在训练期间所见过的相同格式向模型提供数据。我们将其设计得非常容易向现有 tokenizer 添加模板信息,并将其保存或上传到 Hub。

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

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

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

我如何开始使用模板?

很简单!如果 tokenizer 设置了 chat_template 属性,它就可以直接使用。你可以在 ConversationPipeline 中使用该模型和 tokenizer,或者你可以调用 tokenizer.apply_chat_template() 来为推理或训练格式化聊天。请参阅我们的 开发者指南 或 apply_chat_template 文档 了解更多!

如果 tokenizer 没有 chat_template 属性,它可能仍然可以工作,但它将使用为该模型类设置的默认聊天模板。正如我们上面提到的,这很脆弱,而且当类模板与模型实际训练时所用的模板不匹配时,它也是静默错误的来源。如果你想使用没有 chat_template 的检查点,我们建议查看模型卡等文档,以验证正确的格式是什么,然后为该格式添加正确的 chat_template。我们建议即使默认聊天模板是正确的也这样做——它使模型面向未来,并且也清楚地表明模板存在且合适。

你可以通过提交 pull request,为并非你所有的 checkpoint 添加 chat_template。你唯一需要做的改动就是把 tokenizer.chat_template 属性设置为一个 Jinja 模板字符串。完成之后,推送你的改动,就可以使用了!

如果你想用某个 checkpoint 进行对话,却找不到关于它所用对话格式的任何文档,你大概应该在对应 checkpoint 上开一个 issue,或者直接联系所有者!一旦你弄清楚模型使用的格式,请提交一个 pull request 来添加合适的 chat_template。其他用户会非常感激你!

结论:模板哲学

我们认为模板是一项非常令人兴奋的改动。除了解决一个巨大的、悄无声息地拖慢性能的 bug 来源之外,我们认为它们还开启了全新的方法和数据模态。也许最重要的是,它们还代表了一种理念上的转变:它们把一大块功能从核心 transformers 代码库中拿出来,移入各个模型仓库,让用户可以自由地做各种古怪、狂野而奇妙的事情。我们很期待看到你会为它们找到哪些用途!

来源:Hugging Face:Blog(RSS) · huggingface.co