Hugging Face 为 tokenizer 引入 chat_template 属性
Chat Templates: An End to the Silent Performance Killer
Hugging Face 的 tokenizer 新增 chat_template 属性,用 Jinja 模板保存模型训练时使用的对话格式,并把对话历史转换为正确格式的字符串,以避免因格式不匹配导致的静默性能下降。
原文说明格式错配会静默拖垮模型表现,并给出把格式信息随 tokenizer 保存和加载的具体做法。
一个幽灵在对话模型上空游荡——格式不正确的幽灵!
简而言之
对话模型在训练时使用了非常不同的格式,来将对话转换成单个可分词字符串。使用与模型训练时不同的格式通常会导致严重且无声的性能下降,因此匹配训练时使用的格式极为重要!HF Mirror 分词器现在拥有一个 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 检查点都会获得相同的聊天格式,使用在 LLaMA 模型类的 transformers 中硬编码的代码。为了向后兼容,具有自定义聊天格式方法的模型类已被赋予 默认聊天模板 代替。
默认聊天模板也设置在类级别,并告诉像 ConversationPipeline 这样的类在模型没有聊天模板时如何格式化输入。我们这样做 纯粹是为了向后兼容 —— 我们强烈建议你在任何聊天模型上显式设置聊天模板,即使默认聊天模板是合适的。这确保了默认聊天模板中任何未来的更改或弃用不会破坏你的模型。尽管我们将在可预见的未来保留默认聊天模板,但我们希望随着时间的推移将所有模型过渡到显式聊天模板,届时默认聊天模板可能会被完全移除。
关于如何设置和应用聊天模板的信息,请参阅 技术文档。
我如何开始使用模板?
简单!如果 tokenizer 设置了 chat_template 属性,就可以直接使用了。你可以在 ConversationPipeline 中使用该模型和 tokenizer,或者你可以调用 tokenizer.apply_chat_template() 来为推理或训练格式化聊天。请参阅我们的 开发者指南 或 apply_chat_template 文档 了解更多!
如果 tokenizer 没有 chat_template 属性,它可能仍然有效,但它将使用为该模型类设置的默认聊天模板。正如我们上面提到的,这是脆弱的,并且当类模板与模型实际训练时所用的不匹配时,这也是静默错误的来源。如果你想使用没有 chat_template 的检查点,我们建议查看模型卡等文档以验证正确的格式是什么,然后为该格式添加正确的 chat_template。我们建议即使默认聊天模板是正确的,也要这样做——它使模型具有前瞻性,并且也清楚地表明模板存在且合适。
你甚至可以为那些你不拥有的检查点添加 chat_template,只需打开一个 拉取请求。你唯一需要做的更改是将 tokenizer.chat_template 属性设置为一个 Jinja 模板字符串。完成之后,推送你的更改就可以开始了!
如果你想将某个检查点用于聊天,但找不到关于它使用的聊天格式的任何文档,你应该在检查点上打开一个 issue 或者联系所有者!一旦你弄清楚了模型使用的格式,请打开一个拉取请求来添加一个合适的 chat_template。其他用户会非常感激的!
结论:模板哲学
我们认为模板是一个非常令人兴奋的变化。除了解决一个巨大的、悄无声息且严重影响性能的 bug 来源之外,我们认为它们开启了全新的方法和数据模态。也许最重要的是,它们还代表了一种哲学上的转变:它们将一个大函数从核心 transformers 代码库中取出,移入各个模型仓库,在那里用户可以自由地做奇怪、狂野和奇妙的事情。我们很期待看到你为它们找到的用途!
来源:Hugging Face Blog · hf-mirror.com