从下一个词预测到指令遵循:GPT模型的微调
预训练的GPT是如何学会遵循指令的:记录格式化、标签偏移、填充掩码、训练循环、评估限制以及向DPO偏好靠拢的过程。
来源: From Next-Token Prediction to ChatGPT <> Build an LLM From Scratch [6]
预训练已经让语言模型具备了写作能力,但它缺乏对用户需求的理解。指令微调则弥补了这一缺陷。将同样的请求交给两个模型,比如向十岁孩子解释递归,两者的差异十分明显:基础模型往往会重复请求内容,然后开始冗长地介绍计算机科学课程的相关知识,实际上并未真正回答问题,只是在延伸文本而已。而经过微调的模型则会用图示来解释:一个盒子里面装着另一个盒子,那个盒子里又装着更小的盒子,同时将递归描述为一种不断将问题简化为更小版本,直到最简单的问题易于解决的过程。
在底层,这两种模型都执行相同的功能——预测下一个标记,但它们的表现却大相径庭。聊天类产品让遵循指令显得很自然,从而掩盖了这种差异。问题能得到解答,代码请求能生成代码,摘要也能被撰写出来。而这些都不是仅通过阅读大量文本就能免费获得的。预训练则传授更基础的东西:基于目前的所有信息,下一个标记是什么?其中并未提及必须执行用户的请求。填补这一差距正是本文要探讨的主题。
第一幕:预训练教授的是内容生成能力,而非服从指令
预训练的任务仅仅是下一个标记的预测。仅此一项就足以让模型掌握语法、事实、代码语法、写作风格以及一些极为复杂的模式,但服从指令并不在其中。给基础模型输入这样的提示:
Explain why the sky appears blue.
接下来可能会继续如下内容:
Explain why the sky appears blue.
The following questions are commonly asked in introductory physics...
那不一定是模型出故障了。它正在按照预训练的要求做,也就是继续生成文本。理想中的表现应该更像这样:
The sky appears blue because Earth's atmosphere scatters
shorter wavelengths of sunlight more strongly than longer
wavelengths...
底层模型是相同的,但预期行为有所不同。问题变为如下:
What text probably comes next?
翻译为:
When the text looks like an instruction,
what kind of continuation should I produce?
让模型学会回答第二个问题,这就是指令微调的目的。
第二阶段:格式化指令数据
教授模型遵循指令的最直接方法,就是向它展示大量带有正确答案的指令对,其实原理就是这么简单。一条训练记录可能看起来像这样:
example = {
"instruction": "Convert the following sentence to passive voice.",
"input": "The developer fixed the bug.",
"output": "The bug was fixed by the developer."
}
Transformer无法直接处理Python字典,因此需要先将记录序列化为纯文本,格式大致如下:
Below is an instruction that describes a task.
### Instruction:
Convert the following sentence to passive voice.
### Input:
The developer fixed the bug.
### Response:
The bug was fixed by the developer.
格式很重要,因为模型会反复看到相同的模式:
instruction
↓
optional context
↓
response
简而言之,每条记录先被转换为格式化文本,再拆分为标记,随后组成填充后的批次,这些批次用于微调预训练的GPT模型。
标记视角
经过标记化处理后,示例会变成一组ID列表,大致如下所示:
[21106, 318, 281, 12064, ... 4435, 257, 3126, ...]
那些令牌ID构成了训练序列。对于自回归模型而言,标签其实就是将序列向右移动一个位置后的结果:
Tokens:
[A, B, C, D, E]
Input:
[A, B, C, D]
Labels:
[B, C, D, E]
因此,指令微调并不会引入全新的学习目标。模型依然负责预测下一个标记。变化的是它需要预测的文本分布——它不再主要面对普通文档,而是反复看到这种特定的映射关系:
instruction → useful response
这就是核心思想。
填充值需要特殊处理
训练样本的长度各不相同,但批次需要形状兼容的张量。举两个例子:
Example 1:
[10, 20, 30, 40]
Example 2:
[11, 21]
可以对它们进行填充:
Example 1:
[10, 20, 30, 40]
Example 2:
[11, 21, PAD, PAD]
填充标记并非有意义的目标。模型不应因学习这些内容而获得奖励:
PAD → PAD → PAD → PAD
因此,那些位置通常会被替换为忽略值:
ignore_index = -100
PyTorch的交叉熵损失函数会跳过它们。从概念上讲:
loss = cross_entropy(
logits.reshape(-1, vocab_size),
targets.reshape(-1),
ignore_index=-100,
)
细节至关重要。如果不进行掩码处理,训练信号中的一部分就会被批量处理产生的伪影所占用,而非用于语言学习。
第三阶段:微调行为
这里的训练循环本身并无特别之处,因为优化下一个词元模型已是常见操作。关键在于哪些示例会被输入到该循环中。简而言之:
for batch in train_loader:
optimizer.zero_grad()
input_ids = batch[:, :-1]
targets = batch[:, 1:]
logits = model(input_ids)
loss = cross_entropy(
logits.flatten(0, 1),
targets.flatten(),
ignore_index=-100,
)
loss.backward()
optimizer.step()
那段代码中没有任何内容类似这样:
model.learn_to_be_helpful()
行为源自数据。假设有成千上万的示例都是这样的:
### Instruction:
Summarize this paragraph.
### Response:
<clear concise summary>
梯度更新会不断促使模型在接收到类似结构的提示后,生成结构相同的回复。假设其他示例如下所示:
### Instruction:
Write Python code that reverses a string.
### Response:
def reverse_string(s):
return s[::-1]
它会学习指令模式与合适延续内容之间的另一种映射关系。从标记层面来看,指令微调并没有那么神奇:预测结果周围的上下文已被刻意修改。之前是:
The CPU cache is...
指令格式化后:
### Instruction:
Explain CPU caching to a beginner.
### Response:
A CPU cache is...
并未引入“理解用户”之类的目标。相反,训练文本的分布被调整,使得在接收到指令后,有用的回答会变成统计上更常见的后续内容。正是这种变化导致了新的行为表现。在示例中,预训练的GPT-2-medium模型经过指令微调后生成响应,并保存最终的模型。
第四幕:评估变得棘手
分类任务提供了简单的评估方式。以垃圾邮件检测为例:
Prediction: SPAM
Label: SPAM
Correct.
遵循指令时可没有这样的奢侈。试想要求用简单的方式解释递归,一个可能的答案是:
Recursion is when a function solves a problem by calling
itself on a smaller version of the same problem.
另一个:
Recursion is a technique where a problem is reduced into
smaller instances of itself until a stopping condition is reached.
两者都可能是正确的,且无需与参考文本逐字一致。正因如此,对于开放式回答而言,精确匹配的准确性几乎毫无用处。
与参考文本对比
一种方法是提供参考文本:
{
"instruction": "Explain recursion simply.",
"reference": "Recursion solves a problem by repeatedly reducing it...",
"model_response": "A recursive function calls itself..."
}
并将生成的回复与之进行比较。但相似度并不等同于质量,模型完全可以用不同的措辞给出出色的答案。这就引出了另一种方法。
请另一款大语言模型进行评判
评估者会收到指令、参考答案以及待评分的回复:
Instruction:
Explain recursion simply.
Reference answer:
...
Model answer:
...
Rate the model answer from 0 to 100 based on correctness,
relevance, and clarity.
本地法官可以运行 Ollama,这很方便,但也带来了新问题:法官本身也是一个模型。其评分可能受评估提示、评估者选择、响应顺序、详略程度、风格偏好以及法官自身的错误影响。一些案例显示,本地法官会给质量较差的基模型响应给出出人意料的高分,这提醒我们,以大语言模型作为法官的评分并非真实标准。
一个小型项目可以叠加三层结构:
1. Inspect representative outputs manually
2. Compare against reference answers where useful
3. Use an LLM judge for aggregate comparison
单独来看,这些方法都没有完美的表现。但结合起来,它们能比一个被当作真理的单一数字提供更清晰的图景。
第五章:更棘手的问题——偏好
指令微调能让模型了解合理的回应应是什么样子。现在来比较两个答案。答案A:
Recursion is when a function calls itself.
回应B:
Recursion is a technique where a function solves a problem
by calling itself on a smaller version of that problem.
The process stops when it reaches a base case.
两者都是有效的,但可能更倾向于选择B。训练信号不再是这样:
correct vs incorrect
就是这个:
chosen vs rejected
偏好设置记录可能如下所示:
{
"prompt": "Explain recursion simply.",
"chosen": """
Recursion solves a problem by repeatedly reducing it
until reaching a base case.
""",
"rejected": """
Recursion is when recursion happens recursively.
"""
}
这是一种不同的监督方式。它不是说“这是可供模仿的响应”,而是说“在这两个响应中,选择这个”。这时就需要用到直接偏好优化(DPO)之类的技术。简而言之:
Prompt
↓
Chosen response ─────┐
├── preference objective
Rejected response ───┘
↓
model update
DPO需要单独讨论,因此其运作机制不在本次讨论范围内。有用的参考是整体进展情况:
Pretraining
↓
learn language patterns
------------------------------
Instruction fine-tuning
↓
learn instruction → response behavior
------------------------------
Preference tuning
↓
learn which acceptable responses we prefer
使用 Llama 3.1 通过 Ollama 生成偏好数据集,以及从零实现 DPO,这些都是以后要探讨的主题。需要注意一点:虽然 DPO 作为附加材料提供,但并不代表它就是每个模型或数据集的合适下一步。一些实验显示了其结果的局限性,这正是我们在进行偏好调整时应当保持的怀疑态度。
实际发生了什么变化
起点是一个已经预训练、能够继续生成文本的模型。并没有用特殊的助手架构取代Transformer。关于下一个标记问题的示例发生了变化,从而形成了不同的行为先验。当模型现在看到以下内容时:
### Instruction:
已经遇到过很多情况,其后续内容并非来自互联网的另一个段落,而是一个答案。从这个角度理解,内容的进展就更容易把握了:
Pretraining:
What token should come next?
Instruction tuning:
What does a good answer look like after an instruction?
Preference tuning:
Of several reasonable answers, which behavior should we prefer?
架构、优化器以及学习率都很重要,但在指令微调中数据起着特殊作用,因为它明确了需要模仿的行为。当存在多种可行的答案时,生成有效输出就不再是唯一目标。问题转变为哪种可行的回应更值得重视,而这正是DPO发挥作用的地方。
预训练负责学习文本后续通常会出现的内容。指令微调则决定了哪些续写内容可被视为对指令的有效回应。偏好微调则进一步筛选,确定哪些有效回应更值得优先考虑。
相关阅读
- 利用印度民间故事训练小型GPT:分词器、掩码与损失函数 —— 从零构建的900万参数GPT:为何GPT-2的分词器无法处理民间故事中的词汇,BPE与因果掩码的运作原理,以及为何一个批次能产生4,096个下一个标记的损失值。
- 深入理解LLM:使用MLX通过纯Python实现Qwen3-0.6B的推理 —— 通过约200行Python代码,逐步展示如何在MLX上实现LLM推理:包括分词器、RMSNorm、RoPE、分组查询注意力机制、SwiGLU、KV缓存以及采样过程。