监督微调

6,858 字 · 预计阅读 20 分钟

预训练之后的训练都可以叫后训练,监督微调是其中的第一步。

预训练出来的模型,已经学到了一些中文的样子。但你要是问它一个问题,它多半不会回答,而是顺着你的问题继续往下写,像在续写一篇文章。

这很正常。预训练只教会了它"接话":看到前文,写出下文。它从来没见过"一个人问、另一个人答"的格式,也不知道答完了该停下来。

这一节用对话数据再训练一轮,教它一问一答。这一步叫监督微调,也有人叫它指令微调。

这两个名字拆开看就懂了。"监督"是说每道题都配好了标准答案,模型照着答案学;"指令"就是人给模型的问题或要求;"微调"是说不从头训练,而是在已经训练好的模型上,再轻轻调一调。

"微调"有点像收音机:预训练已经把旋钮拧到了电台附近,能听见声音了。监督微调只是再轻轻拧一点,让它播出我们想听的节目,用不着把旋钮拧回零重新找台。

SFT 是英文 Supervised Fine-Tuning 的缩写,就是"监督微调"。

学完你能做到:说清楚对话数据怎么变成训练样本,为什么只让回答部分计算损失,并且训练出一个能简单对话的模型。


一、从"接话"到"听话"

先用还没微调过的模型问两句。在 llm-course 目录下新建一个 ask.py,把问题按对话格式发给它:

python
# 按对话格式问几个问题,看模型怎么接话
# 用法:uv run python ask.py 你好 介绍一下长城
import sys
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

path = "out/pretrain"  # 前面训练出来的模型
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
tok = AutoTokenizer.from_pretrained(path)
model = AutoModelForCausalLM.from_pretrained(path).to(device).eval()

for question in sys.argv[1:] or ["你好"]:
    # 按模板拼提示词,末尾是"轮到我说话了",模型接着这段往下写
    prompt = tok.apply_chat_template([{"role": "user", "content": question}], tokenize=False, add_generation_prompt=True)
    ids = tok(prompt, return_tensors="pt", add_special_tokens=False).input_ids.to(device)
    with torch.no_grad():
        out = model.generate(ids, max_new_tokens=200, do_sample=True, temperature=0.7, top_p=0.8,
                             eos_token_id=tok.eos_token_id, pad_token_id=tok.pad_token_id)
    print(f"\n你:{question}")
    print("模型:" + tok.decode(out[0, ids.shape[1]:], skip_special_tokens=True).strip())

运行:

bash
uv run python ask.py 你好 介绍一下长城

在我们的 CPU 缩小版测试里,它的反应是这样的:

你:你好
模型:我是谁

你为什么想我?
我是谁
你是谁
你在哪里
……

你:介绍一下长城
模型:解释:
一、这个村子在江西的东南边,是长城宙的起点,后来变成了现在的样子。
二、这个村子是长城宙的起点,长城宙的起点在北部的一个地方,后来变成了现在的样子。
……
九、这个村子在长城宙的起

问它问题,它不接茬:问"你好",它回一串"我是谁""你在哪里",来回打转;问长城,它一本正经地分条列项,一直到第九条,讲的全是"长城宙的起点"这一句的变体,最后被程序掐断在"九、"的半截——不是它自己说完的。

我们用 16 个问题各问了 3 次,这 48 次里,它一次都没有自己停下来。

200 个 token 的上限,是 ask.py 里设的(max_new_tokens=200)。模型自己不会累,也不知道什么时候该收尾,所以程序替它数着,数到第 200 个就强行掐断。

这里说的"自己停下来",是指模型在 200 个 token 以内,自己写出了表示"说完了"的结束标记。被程序掐断的不算。

监督微调要教给它三件事:

  1. 认识对话格式:知道哪段是用户说的,哪段该自己说。
  2. 针对问题回答:回答的内容要和问题有关。
  3. 说完就停:回答结束时输出 <|im_end|>,而不是无休止地写下去。

上面说的 <|im_end|>,就是那个结束标记。它看着像一串字符,其实是分词器里预留的特殊 token,整个只算一个 token,有自己的编号。

为什么输出一个 token,就算"停"了?因为模型其实不会真的停,它只会一个接一个地往外吐 token。程序每拿到一个就看一眼:是 <|im_end|>,就不再让它往下写。

所以"说完就停",对模型来说其实是"说完就写一个 <|im_end|>"。这件事和写别的字一样,是要靠训练学会的。

方法和预训练一样,还是"猜下一个 token"。区别只在于数据:这次用的是一条条对话,并且按对话模板拼好。


二、对话怎么变成训练样本


1. 按模板拼起来

分词器里已经存了一个对话模板。一条两轮的对话,拼起来是这样的:

<|im_start|>user
你好<|im_end|>
<|im_start|>assistant
你好!有什么可以帮你的吗?<|im_end|>
<|im_start|>user
介绍一下长城<|im_end|>
<|im_start|>assistant
长城是……<|im_end|>

上面这个模板,可以当成剧本来读。剧本里每句台词前面都写着谁在说,这里也一样:<|im_start|> 表示"有人开口了",紧跟着的是角色名;<|im_end|> 表示"这句说完了"。

角色只有两个:user 是用户,也就是提问的人;assistant 是助手,也就是模型自己。

推理时,我们把用户的问题按同样的格式拼好,末尾加上 <|im_start|>assistant 和一个换行,然后让模型接着往下写。

它写到 <|im_end|> 就停下来,中间写出的就是回答。训练和推理用同一个模板,模型才知道什么时候轮到它说话。

"推理"就是训练好以后拿模型来用,比如和它聊天。推理时一步步发生的事,画出来是这样:

推理时程序搭好开头,模型一个接一个往后写,写出结束标记程序就停下

注意,模型还是在"接话",这一点没有变。变的是,训练时它见过成千上万次:<|im_start|>assistant 后面接的总是一段回答,回答完是 <|im_end|>。

所以我们把开头搭好,它"接话"接出来的,恰好就是回答。


2. 只让回答计算损失

拼好的整段文字里,用户的问题也是 token。要不要让模型学着"预测用户的问题"?不要。我们希望它把能力都用在学怎么回答上。

做法是给每个位置一个标签。助手回答的部分,标签就是正确的 token;其余部分,标签填 -100,这个特殊值表示"这个位置不计算损失":

token:  <|im_start|> user \n 你好 <|im_end|> \n <|im_start|> assistant \n 你好! <|im_end|> \n
标签:      -100      -100 -100 -100  -100   -100   -100       -100    -100  你好! <|im_end|> -100

注意回答末尾的 <|im_end|> 是计算损失的,模型靠它学会说完就停。

表里的 \n 表示换行。

为什么偏偏用 -100?token 的编号都是从 0 开始的整数,不会是负数,所以 -100 不会和任何真实的 token 撞车。PyTorch 计算损失时,默认就会跳过标签是 -100 的位置。

你可能会问:标签和 token 一模一样,模型照抄不就行了?不行。计算损失时,模型会自动把标签错开一位:写在"你好!"下面的标签,意思是"只看它前面的所有 token,猜出'你好!'"。模型只能看前面,看不到自己要猜的那个。

标签是 -100 的位置只看不判分,助手的回答和结束标记才判分

整段对话就像一张试卷。用户的问题是印好的题目,模型要读,但不用它写,也不判分;助手的回答是答题区,每个 token 都要判分。损失就是判卷扣的分。

所以"不计算损失"不等于"看不见"。用户的问题照样是输入,模型猜回答时一直看着它,只是不用去猜它。

多轮对话也是同样的规则:不管有几轮,每一轮助手的回答都计算损失,每一轮用户的话都不计算。

模型在学第二轮回答时,能看到第一轮的问答,所以它也能学到怎么接着前面的话说。

拿前面那条两轮的对话来看,哪些判分、学第二轮回答时能看到什么,画出来是这样:

两轮对话里两段回答都判分;学第二轮回答时,前面的问答全都看得见

本文是会员内容

全文 6,858 字,已试读约 29%。开通专业版可阅读全文,并解锁全部官方课程与后续更新。

开通专业版

支持 7 天无理由退款 · 早鸟价 99 元/年(售完恢复365/年)

课程中涉及的第三方项目,其著作权与商标归各自权利人所有;引用处的署名与许可见《开源与来源说明》。