小白也能看懂的SFT微调:让大模型从“续写机器“变成“贴心助手“
系列文章:AI大模型知识体系 | 第二周·第二篇
一、引言:预训练模型的"尴尬"
上一篇我们聊完了预训练(Pre-training)。经过预训练的模型,本质上是一个超级"文字接龙选手"——你给它一段文字,它会拼命往后续写。
这就带来一个很尴尬的问题。
假设你问模型:"中国的首都是哪里?"
预训练模型不会回答"北京",它大概率会续写成这样:
中国的首都是哪里?美国的首都是华盛顿,日本的首都是东京,韩国的首都是首尔……
看到了吗?它把你的问题当成了"填空题的题干",顺着往下写了。因为在预训练阶段,它读了几万亿个token的文本,学到的本领就是"给定前面的文字,预测后面的文字"。
它从来没有学过"回答问题"这件事。
这就好比你招了一个读过几百万本书的天才,但你从来没教过他怎么跟人对话。他知识很丰富,但你问他一个问题,他可能会把你的问题抄一遍然后续写下去,而不是回答你。
那怎么让模型从"续写机器"变成"有问必答的助手"呢?答案就是今天的主角——SFT(Supervised Fine-Tuning,监督微调)。
二、SFT是什么:给模型来一场"入职培训"
SFT,全称 Supervised Fine-Tuning,直译就是"有监督的微调"。
用一个生活类比来理解:
预训练 = 大学四年读了无数课本,积累了海量知识 SFT = 入职培训,老员工手把手教你怎么干活
一个新员工(预训练后的模型)虽然知识储备很丰富,但他不知道怎么跟客户沟通、怎么写报告、怎么按公司的规范做事。入职培训(SFT)就是拿一批"标准示范"给他看:客户这样问,你应该这样答;领导这样要求,你应该这样写。
在技术层面,SFT做的事情很简单:
-
准备一批"标准问答对"——告诉模型"看到这样的输入,你应该输出这样的内容"
-
让模型在这些数据上继续训练——模型慢慢学会"回答问题"而不只是"续写文本"
-
微调,不是从头训练——在预训练权重的基础上做小幅调整,而不是重新训练一个新模型
这里的"监督"(Supervised),意思就是我们有"标准答案"。每一条训练数据都包含"正确的输出",模型可以对照着学。
三、指令数据集长什么样
SFT用到的数据叫做指令数据集(Instruction Dataset),核心格式是一个三元组:
|
字段 |
含义 |
类比 |
|---|---|---|
|
|
任务指令,告诉模型要做什么 |
老板的需求 |
|
|
可选的额外输入/上下文 |
老板给的参考材料 |
|
|
期望的标准回答 |
老板心目中的满分答案 |
我们来看几条不同场景的真实样本:
样本1:问答
{
"instruction": "简要解释什么是黑洞。",
"input": "",
"output": "黑洞是一种天体,其引力强大到连光都无法逃逸。它通常由大质量恒星在生命末期坍缩形成,中心是一个密度无限大的奇点,周围有一个被称为'事件视界'的边界。"
}
样本2:文本摘要
{
"instruction": "请对以下文章进行摘要总结。",
"input": "人工智能(Artificial Intelligence,简称AI)是计算机科学的一个分支,它试图理解智能的本质,并生产出一种新的能以人类智能相似的方式做出反应的智能机器。该领域的研究包括机器人、语言识别、图像识别、自然语言处理和专家系统等。",
"output": "人工智能是计算机科学的一个分支,旨在创造具有类人智能的机器,研究范围涵盖机器人、语音/图像识别、自然语言处理和专家系统等领域。"
}
样本3:代码生成
{
"instruction": "用Python写一个函数,判断一个数是否为素数。",
"input": "",
"output": "def is_prime(n):\n if n < 2:\n return False\n for i in range(2, int(n**0.5) + 1):\n if n % i == 0:\n return False\n return True"
}
样本4:角色扮演
{
"instruction": "你是一位经验丰富的Python面试官,请对以下代码进行Code Review,指出潜在问题并给出改进建议。",
"input": "def process(data):\n result = []\n for item in data:\n if item != None:\n result.append(item)\n return result",
"output": "这段代码有以下几个可以改进的地方:\n1. `item != None` 应该改为 `item is not None`,这是PEP 8推荐的写法。\n2. 函数名 `process` 过于模糊,建议改为 `filter_none_values` 等更具语义的名称。\n3. 可以用列表推导式简化:`return [item for item in data if item is not None]`。"
}
看到了吧?这些数据的共同特点是——每条数据都清楚地告诉模型:遇到这类指令,标准回答是什么。 这就是"监督"的含义。
四、SFT的训练过程:跟预训练有什么不同?
SFT和预训练在底层用的都是同一个技术——自回归语言建模(预测下一个token)。但有一个关键区别:
预训练:所有token都算loss
预训练时,模型对文本中的每一个token都计算损失(loss)。比如输入"今天天气真好",模型需要预测"天""气""真""好"每一个位置,所有位置的预测误差都会被累加到loss里。
SFT:只在output部分算loss
SFT时,我们把一条数据拼成这样的格式:
### 指令:用Python写一个判断素数的函数
### 回答:def is_prime(n): ...
训练时,指令部分("### 指令:...")的loss被设为 -100(即忽略不计),只有回答部分("### 回答:...")才计算loss。
为什么这很重要?
用一个类比:
想象一个老师给学生布置作业。老师出了一道题,学生在答题区写答案。老师批改的时候,只看答案区域对不对,不会去批改题目本身。
如果把题目的loss也算上,就相当于老师把题目也"批改"了一遍——"这个题目的'计'字你写得不够标准,扣两分"——这就离谱了。
在技术上,忽略instruction部分的loss,是为了让模型专注于学习"如何生成回答",而不是去学习"如何复述问题"。如果不做这个区分,模型会把精力浪费在记住指令的措辞上,回答质量就会下降。
五、SFT数据的质量远比数量重要
这可能是SFT最重要的一条经验。
很多人的直觉是:数据越多越好,10万条肯定比1000条强吧?
事实恰恰相反。
2023年Meta发表的 LIMA 论文(LIMA: Less Is More for Alignment)给出了一个震撼业界的发现:
仅仅用 1000条 精心筛选的高质量数据做SFT,训练出的模型在人类评估中的表现,就能与使用大量数据训练的模型相媲美甚至更好。
LIMA的1000条数据是这样来的:
-
从StackExchange上精选高质量的问答
-
从Reddit上筛选高赞回复
-
由人类专家手工撰写复杂推理样本
-
每一条都经过严格审查,确保回答准确、完整、有帮助
反过来,如果你用10万条质量参差不齐的数据(比如从网上随便爬的问答),模型会学到:
-
错误的事实信息
-
不完整的回答
-
低质量的表达
-
有害或偏见内容
模型就像一个新员工,你给他看1000个优秀案例,他学到的都是精华;你给他看10万个良莠不齐的案例,他可能学到一堆坏习惯。
这也是为什么现在很多团队在SFT阶段会投入大量人力做数据标注和质量审核——因为这是投入产出比最高的环节。
六、实操环节:用Transformers + TRL做SFT
理论讲完了,我们来看怎么用代码实现。这里使用HuggingFace的 transformers 和 trl(Transformer Reinforcement Learning)库。
1. 准备数据
from datasets import Dataset
# 准备指令数据(实际项目中从JSON文件加载)
data = {
"instruction": [
"用一句话解释什么是机器学习。",
"将以下英文翻译成中文。",
],
"input": [
"",
"The quick brown fox jumps over the lazy dog.",
],
"output": [
"机器学习是人工智能的一个分支,让计算机通过数据自动改进性能,而无需显式编程。",
"敏捷的棕色狐狸跳过了懒狗。",
]
}
dataset = Dataset.from_dict(data)
2. 构建训练用的prompt格式
def formatting_prompt(example):
"""将instruction/input/output拼接成SFT训练格式"""
if example["input"]:
text = f"### 指令:{example['instruction']}\n### 输入:{example['input']}\n### 回答:{example['output']}"
else:
text = f"### 指令:{example['instruction']}\n### 回答:{example['output']}"
return {"text": text}
dataset = dataset.map(formatting_prompt)
3. 配置模型和训练
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments
from trl import SFTConfig, SFTTrainer
# 加载预训练模型和分词器
model_name = "Qwen/Qwen2-0.5B"
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype="auto")
tokenizer = AutoTokenizer.from_pretrained(model_name)
# 训练配置
training_args = SFTConfig(
output_dir="./sft_output",
num_train_epochs=3,
per_device_train_batch_size=4,
learning_rate=2e-5,
warmup_ratio=0.1,
logging_steps=10,
save_strategy="epoch",
max_seq_length=512,
)
# 启动训练
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
processing_class=tokenizer,
)
trainer.train()
就这么十几行代码,一个SFT训练流程就跑起来了。trl 库的 SFTTrainer 已经帮我们处理好了"只在回答部分计算loss"的逻辑,不需要手动去mask掉指令部分的token。
提示:实际项目中,建议先用小数据集(100-200条)跑通流程,确认没有bug后,再上完整数据集。GPU显存不够的话,可以加上LoRA(下下周会专门讲)。
七、SFT之后,还有什么问题?
经过SFT,模型已经学会了"回答问题"。你问它"中国的首都是哪里",它会老老实实回答"北京",而不会再续写下去了。
但这里有一个关键问题:SFT只教了模型"怎么回答",没教它"怎么回答得更好"。
举个例子。你问模型:"给我推荐一部好看的电影。"
模型可能的回答:
-
A:"推荐《肖申克的救赎》,它是一部关于希望和自由的经典影片……"(高质量回答)
-
B:"推荐《逐梦演艺圈》,特别好看。"(低质量回答)
在SFT阶段,只要训练数据里写了B这种回答,模型一样会学到。SFT没有"好坏"的概念,它只知道"模仿训练数据中的回答"。
更麻烦的是,模型可能学会了一些"坏毛病":
-
看起来在回答,实际上在胡说八道(幻觉问题)
-
回答冗长啰嗦,抓不住重点
-
对有害问题也照答不误(安全问题)
模型学会了"回答",但没有人告诉它哪些回答好、哪些回答差。
怎么让模型学会区分"好回答"和"坏回答",并主动选择更好的回答?这就需要下一阶段的技术——RLHF(Reinforcement Learning from Human Feedback,基于人类反馈的强化学习)。
这也是我们下一篇文章的主题。
八、总结
来回顾一下今天的核心知识点:
|
要点 |
内容 |
|---|---|
|
SFT解决什么问题 |
让预训练模型从"文字续写"变成"指令回答" |
|
核心思路 |
用带标准答案的指令数据做有监督微调 |
|
训练关键 |
只在output部分计算loss,不关注instruction部分 |
|
数据策略 |
质量远重于数量,1000条精选数据 > 10万条粗糙数据 |
|
SFT的局限 |
只教了模型"回答",没教它"回答得好"——需要RLHF |
用一张图概括大模型训练的完整流程:
预训练(学知识)→ SFT(学回答)→ RLHF(学好回答)→ 对齐(Alignment)
↑ ↑ ↑
读遍互联网 入职培训 高级研修班
下一篇预告:《RLHF:教大模型区分"好回答"和"坏回答"——大模型对齐的关键一步》
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、关注三连支持!你的支持是我持续更新的最大动力。有问题可以在评论区交流,我会逐一回复。我们下一篇见!
更多推荐
所有评论(0)