第二节:SFT基础回顾——监督微调的核心原理

聊RLHF之前,咱们得先把SFT这块地基打牢。说实话,我见过不少同学一上来就冲RLHF,结果连SFT的loss怎么算的都说不清楚——这就像没学会走路就想跑马拉松。

监督微调(Supervised Fine-Tuning,SFT),说白了就是让预训练模型学会“说人话”。预训练阶段模型学的是语言规律,但不知道该怎么回答用户的问题。SFT就是给它喂一堆高质量的“问答对”,让它学会指令遵循。

2.1 指令微调的数据构建

数据是SFT的灵魂。我踩过最大的坑,就是以为数据越多越好。结果模型学了一堆垃圾,反而变笨了。

指令微调的数据格式,通常长这样:

{
  "instruction": "请解释什么是反向传播?",
  "input": "",
  "output": "反向传播是一种计算梯度的算法..."
}

有些数据集还会加一个system prompt字段,用来设定角色。比如:

{
  "system": "你是一个资深AI算法工程师",
  "instruction": "解释一下Transformer的注意力机制",
  "output": "注意力机制的核心是Query、Key、Value..."
}

数据构建的几个关键点:

  • 多样性:指令类型要覆盖问答、写作、翻译、代码生成等。我有个项目只用了单一类型的数据,结果模型在其他任务上直接崩了。
  • 质量优先:宁缺毋滥。一条高质量数据胜过一百条垃圾数据。我曾经用GPT-4生成了一批数据,但没做人工审核,结果模型学会了胡编乱造。
  • 难度递进:先简单后复杂。就像教小孩,先教1+1,再教微积分。
我的小技巧: 数据构建时,我会刻意加入一些“边界案例”。比如让模型回答“我不知道”的场景,或者处理多轮对话中的指代消解。这些数据虽然少,但能显著提升模型的鲁棒性。

2.2 损失函数与训练流程

SFT用的损失函数,其实就是标准的交叉熵损失(Cross-Entropy Loss)。但这里有个细节——我们只计算输出部分的loss,不计算输入部分的。

为什么?你想想看,输入是用户给的指令,模型不需要去预测指令本身。它只需要学会怎么生成正确的回答。

具体来说,对于每个样本,我们只对output部分的token计算loss:

import torch.nn.functional as F

def compute_sft_loss(logits, labels, attention_mask):
    # logits: [batch, seq_len, vocab_size]
    # labels: [batch, seq_len]
    # attention_mask: [batch, seq_len]
    
    shift_logits = logits[..., :-1, :].contiguous()
    shift_labels = labels[..., 1:].contiguous()
    shift_mask = attention_mask[..., 1:].contiguous()
    
    loss = F.cross_entropy(
        shift_logits.view(-1, shift_logits.size(-1)),
        shift_labels.view(-1),
        reduction='none'
    )
    
    # 只保留output部分的loss
    loss = loss * shift_mask.view(-1)
    return loss.sum() / shift_mask.sum()

这段代码里,我用了shift操作,把logits和labels对齐。这是做自回归语言模型的标准做法。

重点提醒: 千万别忘了mask掉padding部分!我刚开始做SFT时,就因为没处理好padding,导致loss计算错误,模型训练了三天才发现问题。

2.3 训练流程详解

SFT的训练流程,其实就三步:

  1. 数据准备:把指令-回答对拼接成模型能理解的格式,加上特殊token(如<|im_start|><|im_end|>)。
  2. 前向传播:模型根据输入生成logits,计算loss。
  3. 反向传播:更新模型参数。

听起来简单吧?但实际做的时候,有几个坑要注意:

  • 学习率:SFT的学习率通常比预训练小很多,一般在1e-5到5e-5之间。我习惯用cosine衰减,效果比较稳定。
  • Batch size:能大就大。但受限于显存,我一般用梯度累积来模拟大batch。
  • Epoch数:2-3个epoch就够了。多了容易过拟合,模型会死记硬背训练数据。
避坑指南: 我曾经在一个项目里,把SFT跑了10个epoch。结果模型在训练集上表现完美,但一到新问题上就胡言乱语。这就是典型的过拟合。后来我加了early stopping,效果好了很多。

2.4 SFT的核心逻辑图

下面这张图,是我自己总结的SFT核心流程。每次做项目前,我都会看一眼,确保没漏掉什么:

SFT监督微调核心流程 数据准备 指令-回答对 + 特殊token 前向传播 计算logits → 交叉熵loss 反向传播 更新模型参数 迭代训练 关键细节 • 只计算output部分的loss,不计算input部分 • 学习率:1e-5 ~ 5e-5,使用cosine衰减 • Epoch数:2-3个,避免过拟合

这张图里,我特意把“只计算output部分的loss”标红了。这是SFT和预训练最大的区别,也是新手最容易忽略的地方。

2.5 实战中的一些经验

最后,分享几个我在实际项目中积累的经验:

  • 数据清洗比模型调参更重要:我花在数据清洗上的时间,通常是模型训练时间的3倍。一条有问题的数据,可能让模型学歪。
  • 先小规模验证:别一上来就全量数据训练。我会先用1000条数据跑一个epoch,看看loss曲线是否正常,生成效果是否合理。
  • 混合数据策略:如果任务类型多样,我会按比例混合数据。比如60%的通用问答,20%的代码生成,20%的翻译。这样模型不会偏科。
一个小技巧: 训练过程中,我会定期保存checkpoint,并手动测试几个典型case。如果发现模型开始胡言乱语,就回滚到上一个checkpoint。这比等训练完再发现问题要高效得多。

嗯,SFT的基础就这些。说白了,它就是让模型学会“模仿”高质量的回答。但光靠模仿还不够,RLHF才是让模型真正“理解”用户意图的关键。不过那是后面章节的内容了,咱们先把SFT吃透。


专注资料整理