首页 / 文章

从下一个词预测到指令遵循:GPT模型的微调

预训练的GPT是如何学会遵循指令的:记录格式化、标签偏移、填充掩码、训练循环、评估限制以及向DPO偏好靠拢的过程。

发布者:REACTAPP.TOP阅读约 9 分钟2112 词

来源: 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发挥作用的地方。

预训练负责学习文本后续通常会出现的内容。指令微调则决定了哪些续写内容可被视为对指令的有效回应。偏好微调则进一步筛选,确定哪些有效回应更值得优先考虑。

相关阅读

  • Transformer如何预测下一个标记:注意力机制、残差连接与Softmax函数——用通俗语言讲解GPT风格的Transformer,涵盖标记与位置嵌入、因果注意力机制、前馈层、对数几率值以及Softmax函数等内容。
  • 从下一个标记的预测到可运行的代码:LLM如何生成Python代码——按顺序解释标记、Softmax函数、温度参数、自注意力机制以及前馈层,同时给出逐步排序示例,并分析预测过程中存在的限制条件。
  • 实用笔记:LLM的实际工作原理——标记、Transformer与下一个标记 —— 对《实用笔记:LLM的实际工作原理——标记、Transformer与下一个标记》的详细操作指南,包含适用于采用该模式的团队的代码模板、检查项及可直接插入的代码片段。
  • 生产环境RAG的分块策略:固定大小法、语义分块法及父子节点分块法 —— 对RAG分块技术的实用介绍,涵盖从固定大小分割、递归分割到父子节点分块、命题分块、延迟分块、RAPTOR算法、表格处理、代码处理以及基准测试等内容。
  • 递归语言模型沙箱:哪些问题阻碍了发展,哪些需要重新构建 —— 对一个递归语言模型平台的回顾:隔离层、代理、CDR、用户空间追踪限制、单主机限制,以及为何限制措施最为重要。
  • LLM的工作原理:标记、注意力机制与下一个标记预测 —— 对LLM架构的通俗讲解,涵盖从标记和嵌入开始,经过注意力机制、因果掩码处理到生成过程,以及训练与推理为何存在差异。