有监督指令微调 (SFT):让无监督基座听懂人话

彻底破解预训练基模“只会接龙、听不懂人话”的本质机密:解析 Prompt 掩码与 ignore_index=-100 隔离反传机制、工业级 Chat Template 与 special tokens 控制流、从 46 万首诗提纯 5 大维度 3 万条自然口语指令对,深入拆解 Meta LIMA 黄金法则(微调质量远重于数量),以及动态 Padding 与 Collate 算子手写实操。

本文目录20 个章节

本章导读: 恭喜你!如果你已经完整跑完了前面的预训练,你的电脑里已经诞生了一个货真价实的 0.04B 语言基座模型(Base Model)。 但当你兴冲冲地在终端里向它提问:“请帮我写一首关于春天的诗”,你可能会大失所望——它可能不仅没有回答你,反而接着你的话无脑续写:“请帮我写一首关于夏天的诗;请帮我写一首关于秋天的诗……”,或者像个复读机一样停不下来。 为什么模型明明记住了 46 万首精选诗词,却像个“听不懂人话的呆子”? 那些能像 ChatGPT 一样彬彬有礼、对答如流的商用大模型(如 Qwen、DeepSeek),到底比基座模型多经历了什么? 本章将为你彻底揭开大模型从“只会接龙的无监督基座”蜕变为“懂礼貌、听指挥的智能助手”的核心机密——SFTSupervised Fine-Tuning,有监督指令微调)与 Prompt 掩码技术

7.1 新手困惑:为什么预训练基座“听不懂人话”?

7.1.1 预训练模型的真正本质:“无情概率接龙机”

在深入代码之前,我们必须打破对大模型的第一个拟人化误区。

预训练阶段(第 04 章),我们教给模型的唯一目标函数是自回归交叉熵损失(Next-Token Prediction)P(wtw1,w2,,wt1)P(w_{t} \mid w_1, w_2, \dots, w_{t-1})

模型的大脑里根本没有“我是一个人工智能助手,屏幕对面是一个人类,他在向我提问,我需要给出解答”这个概念。它唯一掌握的物理规律,是全人类文本中的统计概率分布。

请看下面这个对比:

文本
===================================================================================
                     人类预期的对话模式 vs 基座模型的接龙视角
===================================================================================

人类的预期(问答模式):
  人类输入: "请问中国的首都是哪里?"
  期待模型: "中国的首都是北京。"

基座模型的视角(统计续写模式):
  输入上文: "请问中国的首都是哪里?"
  概率推演: 在海量互联网语料里,这段话最常出现在什么地方?
           ──> 它可能是一份中学地理考试试卷!
  模型续写: "A. 上海  B. 北京  C. 广州  D. 深圳。请考生将正确答案涂在答题卡上。"

看到问题的本质了吗?模型并没有回答错,它只是在完成它被赋予的天职——将概率最合理的下文补全出来。

7.1.2 从“条件预训练”到“自然语言指令对齐”的二段跳

在前面的第 03 章中,我们采用了前沿的 特殊 Token + Loss Mask 条件预训练: 通过 <|title|>登鹳雀楼<|author|>王之涣<|content|> 加上 Loss Mask (-100),模型已经掌握了结构化命题生成无条件自由续写两种底座能力。

但如果人类带着各种口语习惯向它提问:

“AI 助手,你能帮我写一首杜甫风格的思乡诗吗?谢谢你!” “背诵一下王之涣的名作《登鹳雀楼》。” “假如你是古代大诗人李白,请挥毫写下你的代表作!”

在预训练语料中,从来没有出现过现代口语打招呼、问候语气词和角色扮演要求! 对于只见过固定结构的前缀预训练基座而言,这些多变的人类意图属于陌生的分布外输入(Out-of-Distribution, OOD)

这就是为什么必须有 SFT 阶段!

  • 预训练阶段(第 03~04 章):模型掌握了语言学规律,并通过专用 Special Tokens 建立了题目、作者与诗歌韵律的初步条件纽带;
  • SFT 阶段(第 07 章):从 data/poetry_meta.jsonl 中读取 46 万首结构化元数据,通过动态槽位扰动(Slot-Filling)生成 5 大维度的数万条自然交互问答对。通过更高阶的 Prompt Masking(ignore_index=-100,让模型不仅“懂格律”,更学会“听懂复杂人类口语,礼貌工整按需交付”!

7.2 SFT 的核心机密:Prompt 掩码与 ignore_index=-100

要让基座模型听懂人话,业界通用的做法就是 SFTSupervised Fine-Tuning,有监督微调)

而在 SFT 的工程实现中,隐藏着整个大模型技术栈中最精妙、也最容易被初学者忽视的核心细节——Prompt 掩码(Loss Masking)

7.2.1 致命的错误做法:朴素全量微调

假设我们有一组整理好的问答对数据:

  • Prompt (人类提问)“写一首关于秋天的五言绝句。”
  • Response (模型回答)“空山新雨后,天气晚来秋。”

很多初学者直接把这两句话拼在一起:

“写一首关于秋天的五言绝句。空山新雨后,天气晚来秋。”

然后像预训练一样,直接计算整句话从头到尾的交叉熵损失。

这样做的灾难性后果是什么? 模型不仅在学如何回答,还在拼命背诵“人类是怎么提问的”! 每一次人类提问的句式、语气词、错别字,都会产生庞大的梯度去更新模型权重。模型会被带偏,甚至开始抢人类的话说,或者学会自己跟自己提问。

7.2.2 优雅的解法:只惩罚回答,绝不惩罚提问

大模型导师必须明确告诉模型:

“人类爱怎么问就怎么问,那是人类的自由,你不用去背诵人类的问题;你的任务只有一个:看到提问后,给出正确的回答!

在数学和代码上,这是如何实现的?答案就是 PyTorch 交叉熵损失函数中内置的 ignore_index = -100

文本
===================================================================================
                     SFT 训练中的 Prompt 掩码对齐示意图
===================================================================================

完整输入序列 (X):
  [BOS]  请  以  秋  天  写  首  诗  [SEP]  空  山  新  雨  后  [EOS]

目标标签 (Target Labels Y):
  原始标签:   请   以   秋   天   写   首   诗  [SEP]   空   山   新   雨   后  [EOS]
  SFT掩码:  -100 -100 -100 -100 -100 -100 -100 -100   空   山   新   雨   后  [EOS]
            \_______________________________/        \___________________________/
                     人类提问部分 (Prompt)                  模型回答部分 (Response)
                     全部填入 -100!                        保留真实 Token ID!
                     损失梯度 = 0!                         正常反向传播计算 Loss!

当 PyTorch 计算 torch.nn.functional.cross_entropy(logits, targets, ignore_index=-100) 时: 所有目标标签为 -100 的位置,其损失直接被硬编码跳过,产生的梯度绝对为 0

这样,反向传播的全部算力,100% 聚焦在教模型“如何根据给定的问题生成优美的回答”上!

7.3 工业级对话模板(Chat Template)与特殊标记

在真正的商业模型(如 Qwen、Llama)中,问答并不是简单粗暴地用冒号隔开,而是采用结构极其严密的对话模板(Chat Template)

7.3.1 认识控制模型起止的“特殊红绿灯”

为什么很多小模型生成时永远停不下来?因为它们不知道何时该结束回答

SFT 阶段,我们通常会引入特殊的控制标记(Special Tokens):

特殊标记扮演的角色直观含义
`<im_start>`
`<im_end>(或<eos>`)
system系统设定角色设定全局世界观(如“你是一个精通中国唐宋文学的诗词大师”)。
user人类用户角色人类的具体要求或提问。
assistantAI 助手角色模型必须学习生成的回答内容。

经典的 ChatML 格式样本长这样:

TEXT
<|im_start|>system
你是一位精通古典诗词创作的文学导师。<|im_end|>
<|im_start|>user
请以《春夜喜雨》为题,为我作一首诗。<|im_end|>
<|im_start|>assistant
好雨知时节,当春乃发生。随风潜入夜,润物细无声。<|im_end|>

在训练时,只有 好雨知时节...<|im_end|> 这段文字参与计算 Loss;一旦模型在生成时预测出了 <|im_end|>,自回归循环立刻 break 终止,彻底杜绝无脑复读!

7.4 动手实战:将诗词语料加工为 SFT 指令对

我们不必从零去手工标注几十万条问答。利用现有的 46 万首精选诗词,我们可以通过编写自动化流水线,生成极其多样化的指令数据集!

7.4.1 多样化 Prompt 提示词模板生成(拒绝古板书名号)

很多初学者在制作诗词微调数据时,习惯性地把所有提问都加上书名号《》:

“请以《静夜思》为题,写一首诗。”

这样做在大模型工程中是一个严重的设计缺陷!

  1. 人类日常输入习惯:在日常微信聊天、手机打字时,输入《》需要切换特殊符号键盘,极其繁琐。90% 的用户根本不会打书名号,只会直接输入:“请以春雪为题写一首诗”“写一首李白的登高”
  2. 过拟合灾难(Prompt Overfitting):如果训练数据里 100% 的题目都包在《》里,模型就会形成死板的模式识别偏置——一旦人类输入不带《》,模型就不知道哪个词才是题目!

因此,工业级最佳实践是混合分布(Mixed Distribution):绝大多数采用日常自然口语(不带任何括号),部分混合双引号、中括号、键值对,仅少量保留正式书名号:

PYTHON
PROMPT_TEMPLATES = [
    # 1. 现代自然口语(不带任何书名号,接地气、容错率高)
    "请以{title}为题,创作一首古风诗词。",
    "帮我写一首关于{title}的诗,要有{author}的意境风格。",
    "请模仿{author}的笔触,写一首{title}。",
    "我想读一首关于{title}的诗词,作者偏向{author}。",
    "请围绕{title}赋诗一首,作者风格:{author}。",
    "请为我创作一首古诗,题目叫{title}。",
    "写一首{author}的{title}。",

    # 2. 双引号 / 括号等日常标点
    "请以“{title}”为题作诗一首。",
    "请帮我以【{title}】为题,写一首古诗,风格仿照{author}。",

    # 3. 正式书名号(兼顾严谨公文体裁)
    "请以《{title}》为题,创作一首古典诗词。",
    "假若你是古代诗人{author},请为我挥毫作一首《{title}》。",

    # 4. 键值格式
    "题目:{title},作者:{author}",
]

这样训练出来的模型,无论用户打不打书名号、加不加引号,都能精准捕捉用户意图!

7.4.2 构造 SFT JSON 数据集实操代码

scripts/ 目录下,我们可以编写一个轻巧的数据制作工具(例如命名为 build_sft_data.py):

PYTHON
import json
import random

def build_sft_dataset(raw_poetry_list, output_path, max_samples=30000):
    """
    将原始诗词转换为标准的 Alpaca/ChatML 指令微调格式
    """
    sft_data = []
    
    # 随机采样 30,000 首精品诗词(做 SFT 质量远重于数量)
    sampled_poems = random.sample(raw_poetry_list, min(len(raw_poetry_list), max_samples))
    
    for poem in sampled_poems:
        title = poem.get("title", "无题")
        author = poem.get("author", "无名氏")
        dynasty = poem.get("dynasty", "古")
        content = poem.get("content", "")
        
        template = random.choice(PROMPT_TEMPLATES)
        instruction = template.format(title=title, author=author, dynasty=dynasty)
        
        sft_data.append({
            "instruction": instruction,
            "input": "",
            "output": content
        })
        
    with open(output_path, "w", encoding="utf-8") as f:
        json.dump(sft_data, f, ensure_ascii=False, indent=2)
        
    print(f"✅ 成功生成 {len(sft_data)} 条高质量指令微调样本!")

7.5 手写 SFT 数据加载器与张量对齐

这是 SFT 训练中最核心的工程实现——如何在 PyTorch Dataset 中优雅地拼接 Prompt 与 Response,并将 Prompt 区域的标签精准置为 -100

7.5.1 SFTDataset 类核心代码实现

PYTHON
import torch
from torch.utils.data import Dataset

class SFTDataset(Dataset):
    def __init__(self, data_list, tokenizer, max_seq_len=512):
        self.tokenizer = tokenizer
        self.max_seq_len = max_seq_len
        self.samples = []
        
        for item in data_list:
            # 1. 编码人类提示词与模型回答
            prompt = f"问:{item['instruction']}\n答:"
            response = item['output']
            
            prompt_ids = tokenizer.encode(prompt, add_bos=True, add_eos=False)
            response_ids = tokenizer.encode(response, add_bos=False, add_eos=True)
            
            input_ids = prompt_ids + response_ids
            # 2. 构造目标标签:Prompt 区域全置为 -100,Response 区域保留原词!
            labels = [-100] * len(prompt_ids) + list(response_ids)
            
            # 3. 截断至最大长度
            if len(input_ids) > max_seq_len:
                input_ids = input_ids[:max_seq_len]
                labels = labels[:max_seq_len]
                
            self.samples.append((input_ids, labels))

    def __len__(self):
        return len(self.samples)

    def __getitem__(self, idx):
        input_ids, labels = self.samples[idx]
        return torch.tensor(input_ids, dtype=torch.long), torch.tensor(labels, dtype=torch.long)

7.5.2 动态 Padding 与 Collate Function

由于每首诗的长短不一,在组装成同一个 Batch 时,短的样本需要补齐(Padding):

  • input_ids 的 Padding:补入 <pad><eos> 的 Token ID;
  • labels 的 Padding必须全部补入 -100!因为 Padding 的占位符也绝对不能产生任何训练损失!
PYTHON
def sft_collate_fn(batch, pad_token_id=0):
    input_ids_list, labels_list = zip(*batch)
    max_len = max(len(x) for x in input_ids_list)
    
    batch_input_ids = []
    batch_labels = []
    
    for input_ids, labels in zip(input_ids_list, labels_list):
        pad_len = max_len - len(input_ids)
        # 输入张量用 pad_token 补齐
        batch_input_ids.append(torch.cat([input_ids, torch.full((pad_len,), pad_token_id, dtype=torch.long)]))
        # 标签张量必须用 -100 补齐!
        batch_labels.append(torch.cat([labels, torch.full((pad_len,), -100, dtype=torch.long)]))
        
    return torch.stack(batch_input_ids), torch.stack(batch_labels)

7.6 SFT 微调黄金军规(超参数与避坑指南)

相比于动辄跑几千步的预训练,SFT 的微调过程有着截然不同的“物理法则”:

超参数 / 指标预训练阶段 (Pre-training)SFT 微调阶段 (Instruction Tuning)导师底层原理深度剖析
起始权重纯随机初始化(正态分布小随机数)必须加载预训练 Checkpoint绝不能从零训!SFT 只是给已经学会语言的基座模型(如 checkpoints_0.04b/model_step_3000.pt)“教规矩”。
基础学习率较大:3×1045×1043\times 10^{-4} \sim 5\times 10^{-4}极小:2×1055×1052\times 10^{-5} \sim 5\times 10^{-5}学习率如果过大,会发生**“灾难性遗忘Catastrophic Forgetting)”**,把预训练辛辛苦苦学到的语言常识一脚踢翻!
训练步数与耗时几千到数万步 (1~3 Epochs)几百步 (通常仅需 200 ~ 500 步)0.04B 模型仅需约 8 ~ 12 分钟 即可微调完毕!看几万条指令就能彻底参透问答范式,过多训练反而会导致过拟合复读。
损失函数下降幅度从 8.3+ 跌至 2.0~2.5通常从 2.5 下降到 1.5~1.8 左右此时 Loss 衡量的是在给定明确指令下,模型输出目标回答的精确度。

7.6.2 工业级避坑:合成指令的模板中毒与 LIMA 法则(拒绝假人客套词) ⭐

在大模型后训练(Post-Training)实践中,很多初学者为了快速扩充 SFT 样本,会写一个简单的随机插槽脚本:

PYTHON
# ❌ 反面教材:充满机械套话与分布漂移的随机插槽
GREETINGS = ["AI助手,", "劳驾,", "大师,", "诗仙,", "请问你能否"]
SUFFIXES = [",谢谢!", "?", "。"]
# 生成结果形如:“AI助手,吟诵一首以‘阌乡寓居木塔偶题’为主题的七言律诗,谢谢!”

🚨 这种数据会对小模型造成致命的“模板中毒”伤害:

  1. 测试分布严重漂移(OOD)
    • 现实中人类用户的提问往往极其精炼干脆:“写一首李白风格的五言”“写一首送别的七言”
    • 若训练集充斥着冗长的套话,当模型面对简短 Prompt 时,会因缺少“AI助手”、“谢谢”等表面激活词而出现意图识别迟钝或注意力发散。
  2. 跨域 Token 污染古风语义
    • 古诗专用的 4096 精简词表里根本没有现代英文 AI,它会被强制切碎为两个单字节字节 Token,污染了古典上下文的表征。
  3. 体裁标签精神分裂
    • 若随机给五言绝句贴上“七言律诗”的提问标签,模型会同时接收到相互冲突的长度信号,彻底丧失对格律的敏锐度。

💡 Meta LIMA 黄金法则(Less Is More for Alignment):

微调的质量远远重于数量!几千条贴近人类自然口语、去噪纯净的“黄金指令”,效果远胜于几万条充满假人客套的僵化模板。

scripts/build_sft_data.py 中,我们彻底重构了指令构造器:

  • 纯自然口语命令(35%)写一首五言《登鹳雀楼》以“春雪”为题作诗
  • 诗人风格摹写(25%)写一首李白风格的五言模仿杜甫的风骨作诗
  • 真实意象命题(20%):基于正文语义自适应提取 送别思乡春雪明月 等主题,精准贴合自然语言;
  • 格律与字数严格自洽:基于诗词正文动态提取“五言”或“七言”,确保提问与回答格律 100% 对应。

7.7 终极见证:预训练基模 vs SFT 模型的行为演化录

当你的 SFT 微调跑完 300 步后,你会见证大模型领域最激动人心的魔法时刻——机器第一次“听懂了人话”

文本
===================================================================================
                       同一提示词下:基座模型 vs SFT 模型的真实表现
===================================================================================

用户输入Prompt:
  "请帮我模仿李白的风格,写一首关于秋天喝酒的五言绝句。"

【阶段一:预训练 Base 模型输出】
  "请帮我模仿李白的风格,写一首关于秋天喝酒的五言绝句。李白字太白,号青莲居士,
  唐代伟大的浪漫主义诗人,被后人誉为诗仙。他的代表作有《望庐山瀑布》《行路难》
  《蜀道难》……(无休止百度百科式胡话)"

【阶段二:SFT 指令微调模型输出】
  "秋风吹落叶,把酒对青山。
  举盏邀明月,浮生尽醉欢。<eos>
  [模型成功击中结束符,优雅停机]"

看!模型不再胡乱揣测上文,而是:

  1. 识别出你在要求它写诗;
  2. 识别出风格是“李白”,意象是“秋天、喝酒”,体裁是“五言绝句”;
  3. 输出工整的 20 字诗句,并在最后输出 <eos> 自行断开连接!

7.8 本章总结与课后思考

走到这里,你已经完全攻克了大模型三级火箭中的前两级:

  • 第 01 ~ 06 章(第一级预训练火箭):从零造出发动机,吸收海量文本概率,打牢语言基座;
  • 第 07 章(第二级 SFT 火箭):使用 Prompt 掩码与指令对齐,让模型从“野生接龙作家”蜕变为“懂礼貌、听指挥的专属智能体”。

但此时一个现实的工程问题摆在我们面前:

如果我们要微调像 Qwen3.5 这样参数量更大(2B、4B)、文学水平更高、知识更渊博的现代顶尖基模,全量微调动辄需要几十 GB 显存,消费级单卡根本装不下,该怎么办?

在下一章,我们将踏入大模型工业界最耀眼的明珠——PEFT / LoRA 参数高效微调,带你用单张消费级显卡,轻松驯服数十亿参数的工业级大模型!🚀

REFERENCES

参考链接

  1. 01Training Language Models to Follow Instructions - InstructGPT (Ouyang et al.)
  2. 02LIMA: Less Is More for Alignment (Zhou et al.)
  3. 03Stanford Alpaca: An Instruction-following LLaMA Model

所属系列

从零开始手搓大模型

下一步

继续浏览相关主题

沿着同一主题继续阅读。

查看最新资讯