机器学习 | 语言模型对话模板
语言模型对话模板 (Chat Template): 语言模型的对话模板是 Tokenizer 的一部分,用来把问答的对话内容转换为模型的输入提示词,是模型对话能力至关重要的组件。
在我初次接触大语言模型的时候,我实际上完全不知道“对话模板”这个概念,在尝试模型微调时也只会在 LLaMA-Factory 中选择对应的模板,完全不理解为什么。所以我认为可能很多人在初次接触大语言模型时也不知道这个概念,本篇文章便是针对它的分享。
1 对话模型
我们都知道,大语言模型是自回归模型,输入历史 Token 序列,输出下一个 Token 的概率分布,(如果使用采样法)然后在该概率分布中随机抽取下一个 Token.
那么很容易产生的一个问题是,为什么这个自回归的概率预测模型能够和人类一来一回的对话,它难道不会无限生成下去吗?
确实是这样的,这种大语言模型一般称作基座模型 (Base) 或预训练模型 (Pretrain),如果你尝试与基座模型对话,输入以下问题:
为我介绍一下大语言模型
你会发现,模型完全没有跟随你的指令,而是单纯开始进行概率预测。
首先把你的问题补全成了“为我介绍一下大语言模型的优缺点。”,然后又往后预测生成了一堆和你问题无关的内容。
的优缺点。
大语言模型(Large Language Models, LLMs)是一种基于深度学习的自然语言处理技术,它们能够理解和生成人类语言。以下是一些大语言模型的主要优点和缺点:
### 优点:
1. **强大的文本生成能力**:LLMs可以生成连贯、流畅且具有上下文理解的文本,这在写作、翻译、对话系统等领域非常有用。
2. **多任务学习能力**:许多LLMs能够同时执行多种语言任务,如文本分类、情感分析、问答系统等。
3. **可扩展性**:随着计算资源的增加,LLMs的性能可以进一步提升,适用于更复杂和多样化的任务。
4. **创新应用**:LLMs为各种创新应用提供了基础,包括但不限于智能助手、聊天机器人、内容创作工具等。
5. **研究推动**:LLMs的开发促进了自然语言处理领域的研究,推动了相关技术的发展。
### 缺点:
1. **数据依赖性**:LLMs通常需要大量的标注数据来训练,这限制了它们在资源有限的环境中应用。
2. **解释性差**:由于其复杂的内部工作原理,LLMs的决策过程往往难以解释,这在某些领域(如医疗、法律)可能是一个问题。
3. **安全性风险**:LLMs可能会生成不准确或有害的内容,尤其是在没有适当监督的情况下。
4. **能耗问题**:大型模型的训练和运行需要大量的计算资源,这可能导致能源消耗增加。
5. **隐私问题**:处理大量用户数据的LLMs可能涉及隐私泄露的风险,尤其是在数据收集和使用方面。
总的来说,大语言模型在许多方面都展现出了巨大的潜力,但也面临着一些挑战和限制。随着技术的不断进步和对这些问题的解决,大语言模型有望在未来发挥更大的作用。
那么,我们平时使用的模型为什么能够对话呢?首先,可以对话的模型一般称作对话模型 (Chat) 或者指令模型 (Instruct),从基座模型到对话模型的这一步便需要通过对话模板进行训练。
2 对话模板
对话模板便是解决上面的问题。
首先需要让模型认识到每条消息的界限,不要在用户消息的后面预测新的内容。容易想到的是,可以引入一些特殊标记分隔开每条消息,比如可以仿照 XML,用标签分隔消息:
<message>
这里放用户提问
</message>
<message>
这里放模型输出
</message>
不过还有个问题,这样仅仅分隔了消息,但无法分辨出每条消息的来源是用户还是自己。可能你会说奇数标号消息是用户提问,偶数标号是模型,这当然没错,但太不明显,也徒增训练难度,不如直接在标记中注明:
<message>user
这里放用户提问
</message>
<message>assistant
这里放模型输出
</message>
以上,最基本的对话模板实际上就完成了。
3 模型推理
我们需要收集大量对话数据,套用对话模板后,使用带对话模板的对话数据继续训练基座模型,得到对话模型。假如我们已经通过以上数据训练得到了对话模型,接下来就得思考这个模型具体怎么运行了,模型如何开始,模型如何停止?
模型如何开始?
收到用户消息:
为我介绍一下大语言模型
首先,套用对话模板:
<message>user
为我介绍一下大语言模型
</message>
接下来,添加新消息后缀:
<message>user
为我介绍一下大语言模型
</message>
<message>assistant
这一步可能会有点令人疑惑,为什么要手动添加一个后缀,不能直接把用户信息交给模型吗。
实际上,直接交给模型也是可以正常输出的,这在 Transformers 库中也是一个开关参数。即使不加后缀,模型很大概率会自己预测出紧随其后的 2 个 Token 为 <message>
和 assistant
,然后开始正常的模型消息生成。
但需要知道的是,大语言模型是采样法完成的,如果运气很差,紧随其后的 2 个 Token 采样到了其他 Token,那整个 Token 序列就乱了套了,模型大概率就要输出混乱了。因此一般来说为了稳定,手动强制添加下一条消息的起始标签作为后缀。
模型如何结束?
模型收到了 Prompt,开始了预测生成:
<message>user
为我介绍一下大语言模型
</message>
<message>assistant
好的,我将会为你介绍...
自回归预测是无穷无尽的,那什么时候停呢?
很容易想到,如果训练得当,模型一定会在何时的时机输出 </message>
这个 Token,那么我们的代码如果捕获到模型输出 </message>
这个 Token 就结束循环就行了。
<message>user
为我介绍一下大语言模型
</message>
<message>assistant
好的,我将会为你介绍...
...
...
</message>
4 实际的模板
上面的模板是我在这篇文章举例瞎搓的一个,生产环境中的模型使用的模板通常更为复杂。
不妨来看看 Qwen2.5 系列模型使用的对话模板:
{%- if tools %}
{{- '<|im_start|>system\n' }}
{%- if messages[0]['role'] == 'system' %}
{{- messages[0]['content'] }}
{%- else %}
{{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}
{%- endif %}
{{- "\n\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
{%- for tool in tools %}
{{- "\n" }}
{{- tool | tojson }}
{%- endfor %}
{{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
{%- else %}
{%- if messages[0]['role'] == 'system' %}
{{- '<|im_start|>system\n' + messages[0]['content'] + '<|im_end|>\n' }}
{%- else %}
{{- '<|im_start|>system\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\n' }}
{%- endif %}
{%- endif %}
{%- for message in messages %}
{%- if (message.role == "user") or (message.role == "system" and not loop.first) or (message.role == "assistant" and not message.tool_calls) %}
{{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>' + '\n' }}
{%- elif message.role == "assistant" %}
{{- '<|im_start|>' + message.role }}
{%- if message.content %}
{{- '\n' + message.content }}
{%- endif %}
{%- for tool_call in message.tool_calls %}
{%- if tool_call.function is defined %}
{%- set tool_call = tool_call.function %}
{%- endif %}
{{- '\n<tool_call>\n{"name": "' }}
{{- tool_call.name }}
{{- '", "arguments": ' }}
{{- tool_call.arguments | tojson }}
{{- '}\n</tool_call>' }}
{%- endfor %}
{{- '<|im_end|>\n' }}
{%- elif message.role == "tool" %}
{%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != "tool") %}
{{- '<|im_start|>user' }}
{%- endif %}
{{- '\n<tool_response>\n' }}
{{- message.content }}
{{- '\n</tool_response>' }}
{%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
{{- '<|im_end|>\n' }}
{%- endif %}
{%- endif %}
{%- endfor %}
{%- if add_generation_prompt %}
{{- '<|im_start|>assistant\n' }}
{%- endif %}
这一看头皮发麻了,怎么这么复杂?因为该模板使用 Jinja2 语法,在模板里就内置了分支、循环逻辑,同时这个模板还支持函数调用功能,于是看着就复杂起来了。
那不妨看看由该模板生成的对话格式,这样直观多了。如果将上文的消息套用这个模板,则结果如下:
<|im_start|>system
You are Qwen, created by Alibaba Cloud. You are a helpful assistant.
<|im_end|>
<|im_start|>user
为我介绍一下大语言模型
<|im_end|>
<|im_start|>assistant
好的,我将会为你介绍...
...
...
<|im_end|>
可以看到,唯一的区别便是分隔符“长得”不一样,同时模板内置了默认的系统提示词,除此之外功能是完全一致的。
5 其他细节
特殊 Token 不可切分
我们知道,Tokenizer 在切分 Token 的时候不一定为按完整词切开,那如果分隔符被切了那不就乱套了:
因此,对话模板中的特殊标记被称为特殊 Token (Special Token),它们是加入特殊 Token 表中的。加入表后,Tokenizer 可以保证特殊 Token 不会被斩断。
训练和推理必须使用同套模板
这点应该比较好理解,模型训练时拟合的是训练时模板的格式,如果推理时传入一个完全不同的模板,那模型预测就会混乱了。
避免与用户输入冲突
可以发现,实际使用的特殊 Token 长得样子都很怪,包含多个少用符号,基本上不是现实生活中会使用的组合形式。这样做的用意是防止用户输入和特殊 Token 撞车,因为如果用户意外输出了特殊 Token,很可能导致模型行为异常。
例如本文示例的 <message>
就很不好,因为和 XML 语法太像了。假如用户传入了一个 XML 文本想要让模型编写代码,同时里面正好包含 <message>
这个标签,那模型就得混乱了。
本文采用 CC BY-SA 4.0 许可,本文 Markdown 源码:Haotian-BiJi