qinghuan
← 监督微调与偏好对齐 / 全部笔记

单元 31 / 监督微调与偏好对齐

P0 · 必会主线L3 · 深入

监督微调 SFT:指令数据与损失掩码

先分清对话中每段文字属于谁,再移位标签、只对助手回答计算 SFT 损失,检查被屏蔽位置与上下文的梯度。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能打印每个输入词元的角色与目标,构造只监督助手回答的 assistant-only 损失,处理截断与空目标并验证实际梯度。

学习理由、准备要求与知识点

为什么学它

数据模板和掩码没有对齐时,损失下降也可能是在学复制用户问题,或预测错误位置。先检查每个输入实际对应哪个目标。

先读这些内容

知识点清单

  • 条件语言建模与teacher forcing
  • 聊天模板和角色边界
  • completion与assistant掩码
  • 标签所有权与一次移位
  • EOS和PAD区分
  • 提示词上下文梯度
  • 有效目标计数与截断
  • 训练评测模板一致

运行前先确认Java17、PyTorch2.9.1 CPU;源码基线TRL v0.23.1与Transformers v4.57.1。实验使用手写小词元与因果累计上下文模型,不下载预训练模型,不运行完整SFTTrainer或声称获得语言能力。

01先弄懂它是什么

如果已有“用户怎么问、理想答案怎么答”的样例,可以用它们继续训练语言模型,这叫监督微调 SFT。模型读取对话前缀,逐位置预测下一个词元;训练时给它真实的历史回答,这种方式叫 teacher forcing。训练目标是让参考回答在这些前缀下更可能出现。它并没有让模型先去环境里尝试再靠奖励学习,训练损失下降也不能证明模型会正确处理所有新指令。

只训练助手回答时,容易把“当前输入的位置”和“要预测的目标位置”弄混。设输入是 x0 到 xT-1,logits 形状为 T乘V,V 是词表大小;助手掩码 m 标出输入中哪些词元属于要学习的回答。位置 i 预测的是 x(i+1),是否计入损失应查看 m(i+1),不是 m(i)。回答的第一个词元由前一个位置预测,若直接按当前输入角色屏蔽 logits,就会漏掉它,并把监督边界移错。

只监督助手不代表从输入删除用户内容。用户、系统和先前工具输出仍是条件上下文,应在允许的因果注意力范围内可见。它们本身作为目标可以被屏蔽,但其输入Embedding可能通过后续回答损失得到梯度。必须区分某个logits行的直接损失梯度为零与某个输入词元完全不影响模型这两件事。

文中这几个词,先认一认

assistant-only loss
只把选定的助手内容词元算进训练损失。角色前缀和结束标记是否算进去,要由对话模板策略明确规定。
completion-only loss
只训练 prompt 之后的 completion 部分。如果这部分包含多轮对话,也可能有其他角色,因此不一定等同于只训练助手内容。
ignore_index
告诉交叉熵“这个目标位置跳过”的特殊标记,本例是 -100。它不是能送进 Embedding 查表的词元 ID。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

首先把结构化消息转换成模型约定的聊天模板,再分词并保留助手范围映射。不能假设单独分词prompt所得长度一定等于拼接后prompt的词元长度,子词分词可能跨拼接边界变化。优先使用模板提供的可靠角色掩码;若自行实现,必须逐词元检查边界。本文直接给定词元和所有权,专门隔离损失对齐问题,不伪装成任意真实模板的通用解析器。

常见整理器先生成未移位labels,非助手与填充位置设-100,然后模型内部移位一次。本文直接调用交叉熵,所以自行左移标签,最后一项保持忽略;不能把这份已移位结果再交给自动移位的模型。EOS若属于助手输出可监督,让模型学习停止;PAD应按实际填充位置屏蔽,即使它与EOS共享数值也要保留真实EOS目标。

截断长对话时,可能把要训练的回答全部删掉,最后一个有效目标也不剩。因此先统计有效目标数,再跳过无效样本或重新构造数据,不能直接对全部被忽略的标签取平均。多轮对话还要规定训练所有助手轮次,还是只训练最后一轮,并让训练、验证采用相同规则。实验用累计输入 Embedding 表示因果上下文,检查提示输入仍可收到梯度、被屏蔽的 logits 行没有直接梯度,并实际训练这个小模型降低参考回答损失。

先跑通一个最小例子

运行 experiment.py。使用累计Embedding的因果玩具模型进行真实CPU反向和更新;它只验证目标与梯度,不代表真实对话模型能力。

展开收起代码 · experiment.pypython · 53 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

experiment.py · python
import torch
import torch.nn.functional as F
torch.set_num_threads(1)
torch.manual_seed(31)
ids = torch.tensor([1,2,3,4,5,6,7,8])
assistant = torch.tensor([False,False,True,True,False,True,True,True])
# 先按目标词元所属角色屏蔽,再移位一次
raw_labels = ids.clone()
raw_labels[~assistant] = -100
target = torch.full_like(ids, -100)
target[:-1] = raw_labels[1:]
assert target.tolist() == [-100,3,4,-100,6,7,8,-100]
valid = target != -100
assert valid.sum().item() == 5
embedding = torch.nn.Embedding(10, 8)
head = torch.nn.Linear(8, 10)
params = list(embedding.parameters()) + list(head.parameters())
optimizer = torch.optim.Adam(params, lr=0.05)
def forward():
    # 累计前缀只读取当前和过去输入,是简化因果上下文
    prefix = embedding(ids).cumsum(dim=0)
    prefix = prefix / torch.arange(1, len(ids)+1)[:, None]
    return head(prefix)
def objective(logits, labels):
    if not (labels != -100).any():
        raise ValueError("no supervised targets")
    return F.cross_entropy(logits, labels, ignore_index=-100)
logits = forward()
logits.retain_grad()
loss = objective(logits, target)
initial = loss.detach()
loss.backward()
assert torch.count_nonzero(logits.grad[~valid]) == 0
# 提示词作为输入仍参与后续回答的条件上下文
assert embedding.weight.grad[1].abs().sum() > 0
manual = -F.log_softmax(logits.detach(), dim=-1)[valid, target[valid]].mean()
assert torch.allclose(loss.detach(), manual)
optimizer.zero_grad(set_to_none=True)
for _ in range(100):
    optimizer.zero_grad(set_to_none=True)
    current = objective(forward(), target)
    current.backward()
    optimizer.step()
final = objective(forward(), target).detach()
assert final < initial * 0.2
try:
    objective(forward(), torch.full_like(target, -100))
except ValueError:
    pass
else:
    raise AssertionError("all ignored must be rejected")
assert target[-2].item() == 8  # 本例最后助手词元代表结束标记
print(f"8 SFT masking checks passed; loss {initial.item():.4f} -> {final.item():.4f}")

核对运行结果八项检查通过;提示输入有梯度,被屏蔽预测行无直接梯度,有效目标5个,玩具模型损失下降。

正在加载流程图…

角色掩码先属于目标词元,再通过移位对齐预测位置;提示仍然是条件上下文。
查看流程图源码(Mermaid)
flowchart TD
 A["结构化消息与模板"] --> B["词元和角色所有权"]
 B --> C["截断与填充位置"]
 C --> D["未移位助手标签"]
 D --> E["仅移位一次"]
 B --> F["因果模型保留提示上下文"]
 F --> G["每位置词表logits"]
 E --> H["有效目标交叉熵"]
 G --> H
 H --> I["梯度和格式对照测试"]

按顺序看,每一步做了什么

  1. 固定模板、特殊词元和监督助手范围。
  2. 打印输入ID、角色掩码、填充位置与未移位标签。
  3. 只做一次移位,统计有效目标并处理全忽略样本。
  4. 检查logits与上下文梯度,验证训练及推理模板一致。

03对照实现代码

TRL SFT整理器如何屏蔽非助手标签 (新标签页)↗

TRL v0.23.1 · DataCollatorForLanguageModeling.torch_call;completion_mask;assistant_masks

下例为原创右填充标签模型,只演示未移位角色和位置屏蔽;没有运行TRL训练器,也不实现聊天模板解析。

核心机制代码 · python
import torch
def assistant_labels(ids, assistant, real_position):
    # 三个张量必须已经由同一次模板分词结果对齐
    if ids.shape != assistant.shape or ids.shape != real_position.shape:
        raise ValueError("mask shape")
    labels = ids.clone()
    # 按位置屏蔽填充,避免PAD与EOS同ID时误删真实EOS
    labels[~(assistant.bool() & real_position.bool())] = -100
    # 返回未移位标签;让下游模型损失负责唯一一次移位
    return labels

真实整理器读取input_ids和可选labels,分别处理填充、completion_mask与assistant_masks;输入数据必须提供可信的范围信息。

completion_only_loss启用且提供completion_mask时会屏蔽非完成区域;assistant_masks存在时进一步限制助手范围,两者可以共同生效。

整理器的labels仍按输入位置排列,移位由模型损失处理。阅读时要把数据整理和模型loss连起来,而不是只看一个布尔配置。

padding_free路径额外构造位置并屏蔽序列起点,但其注意力边界需要相应模型内核支持;不能把标签屏蔽当成完整文档隔离。

代码先调用谁,再调用谁

  1. 模板处理形成input_ids与助手范围
  2. 整理器填充并将非监督位置写为-100
  3. 因果模型损失移位后计算有效目标交叉熵

04动手写一个例子

把助手词元所有权对齐到预测位置

输入右填充词元数组、对应助手目标标记和真实前缀长度,返回已经移位的目标数组;无有效目标保留全-100,训练入口负责拒绝。

输入 1
IDs=[1,2,3,4,0],助手=[假,假,真,真,假],真实长度4
输出 1
[-100,3,4,-100,-100]

角色标记属于词元本身;要监督第j个词元,应把它写入第j-1个预测位置,前提是j有可见前驱。

真实长度限制可监督范围,右侧填充即使有错误角色标记也不能形成目标。真实EOS按其位置身份处理,不以ID是否等于PAD判断。

返回后统计有效目标,零目标批次不进入均值交叉熵。将掩码构造与训练入口检查分开,方便定位到底是模板、截断还是损失归约的问题。

为什么这样写是对的对每个预测位置i,标签有效当且仅当i+1位于真实前缀且被标为监督目标;标签值恰为ids[i+1]。

时间 O(T)额外空间 O(T)输出,O(1)额外状态

一次校验真实词元,一次扫描构造目标,T为含填充的数组长度;无需搜索文本中的角色字符串。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 33 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
    static int[] targets(int[] ids,boolean[] assistant,int realLength){
        if(ids.length!=assistant.length || realLength<0 || realLength>ids.length)
            throw new IllegalArgumentException("shape");
        for(int j=0;j<realLength;j++)if(ids[j]<0)throw new IllegalArgumentException("token");
        int[] labels=new int[ids.length];Arrays.fill(labels,-100);
        // 所有权在目标位置j,预测它的是位置j-1
        for(int j=1;j<realLength;j++){
            if(assistant[j])labels[j-1]=ids[j];
        }
        // 尾部填充和没有后继的最后真实位置保持忽略
        return labels;
    }
    static long count(int[] labels){
        // 空目标由训练入口显式处理,不拿它做零分母均值
        return Arrays.stream(labels).filter(x->x!=-100).count();
    }
    static void check(boolean ok){if(!ok)throw new AssertionError();}
    public static void main(String[] args){
        int[] ids={1,2,3,4,0};boolean[] mask={false,false,true,true,false};
        check(Arrays.equals(targets(ids,mask,4),new int[]{-100,3,4,-100,-100}));
        check(count(targets(ids,mask,2))==0);
        check(targets(new int[]{},new boolean[]{},0).length==0);
        check(Arrays.equals(targets(new int[]{1,0,0},new boolean[]{false,true,false},2),
                            new int[]{0,-100,-100}));
        check(Arrays.equals(targets(new int[]{7},new boolean[]{true},1),new int[]{-100}));
        check(count(targets(new int[]{1,2,3,4,5},new boolean[]{false,true,false,true,true},5))==3);
        try{targets(new int[]{1},new boolean[]{},1);throw new AssertionError();}
        catch(IllegalArgumentException expected){}
        System.out.println("7 assistant target alignment checks passed");
    }
}

这些特殊情况也要试一试

  • 空数组或回答全被截断时有效目标为零。
  • 序列第一个词元即使标助手,没有前驱也不能在本文构造中被预测;加入BOS可改变这一条件。
  • PAD与真实EOS同ID仍按实际位置区分,不按数值一刀切。
进阶选读 · 多轮、工具轨迹与截断策略

场景一条样本包含多个助手回答和工具结果,需要只监督指定助手动作并保留必要上下文。

做法用结构化消息生成词元所有权,记录轮次与工具边界;截断优先保留目标及足够前缀,对工具结果是否监督作显式配置,加入逐词元审计和模板版本测试。

复杂度已知对齐映射时掩码生成O(T);重新分词与长样本截断成本由分词器和策略决定。

好处与代价只监督最后一轮减少历史回答权重,但有效数据量降低;监督全部助手轮次则需要检查早期回答质量。

05项目里怎么用

SFT数据质量首先是问题、上下文和参考回答之间的对应关系。按来源或任务家族划分训练验证集,过滤近重复,保留错误答案和格式异常的审核记录。训练时复制参考答案只能证明拟合能力,验证时要用未见问题并按同一模板生成,检查事实、格式、停止条件与任务成功率。

样本审计应直接显示输入词元、角色、labels和解码后的监督文本。随机抽一批长样本、空回答、多轮与共享PAD/EOS样本,确认没有把问题加入目标或丢掉结束标记。截断统计应报告零有效目标比例、回答保留比例与来源分布,避免某类长问题系统性消失而没有人发现。

使用LoRA还是全参数更新不改变目标掩码的基本要求。梯度累积、分布式平均和Packing会让有效分母跨微批次或rank扩展,必须延续前文的全局计数约定。保存模型时也保存模板、分词器、特殊词元和监督策略;推理前缀若与训练不一致,单纯增加训练轮次通常无法修复协议错误。

容易踩的坑

  • 按当前输入角色屏蔽预测行,造成一位偏移。
  • 把-100传入Embedding,混淆标签标记与输入ID。
  • 认为提示labels被忽略就应删除提示或切断所有提示梯度。
  • 截断后全忽略仍取均值,生成NaN或无效更新。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
全序列监督实现简单且目标数量多也学习用户或系统文本分布明确需要该语言建模目标的任务
仅助手监督更直接对应回答生成需要可靠模板与角色映射常规对话指令微调
仅最后回答监督控制历史回答的权重丢弃部分可用监督信号历史质量混杂或指定最后轮目标

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01助手第一词元应由哪个位置预测?

展开参考答案与推导

由它前一个输入位置的logits预测;目标所有权必须随标签移位对齐。

02提示标签忽略后,提示Embedding能否有梯度?

展开参考答案与推导

能。提示仍作为上下文影响后续助手预测,梯度可以沿注意力或其他因果连接传回输入表示。

03completion-only和assistant-only是否相同?

展开参考答案与推导

只有完成范围恰好全部是要监督的助手内容时才相同;多轮或工具结果可能使二者不同。

换个情况,还会处理吗

01loss突然变NaN且样本很长怎么查?

展开参考答案与推导

先统计截断后的有效目标数,检查回答是否全被删掉;再检查数值精度与logits,而不是立即调小学习率。

02训练格式正常但生成不停怎么办?

展开参考答案与推导

检查EOS是否被误当填充屏蔽,模板的结束标记是否与推理停止配置一致,再看训练数据是否包含有效结束监督。

03训练时只保留回答输入可否提高效率?

展开参考答案与推导

那会改变条件分布,模型无法学习依据问题回答;应保留提示上下文,只按目标策略屏蔽损失。

写代码试一试

01生成移位后的助手标签。

输入 / 条件
IDs=[4,5,6,7],助手=[假,假,真,真]
输出 / 预期
[-100,6,7,-100]

提示 目标j写到j-1。

展开参考答案与推导

位置1预测第一助手词元6,位置2预测7,其余忽略。

02设计共享PAD/EOS测试。

输入 / 条件
IDs=[2,0,0],真实长度2,第二位置为助手EOS
输出 / 预期
[0,-100,-100]

提示 依据真实长度,而不是ID为零。

展开参考答案与推导

真实EOS仍受监督,末尾填充不产生目标。

03如何验证屏蔽位置没有直接损失梯度?

输入 / 条件
logits保留梯度,含-100标签
输出 / 预期
被屏蔽logits行的grad为零

提示 retain_grad后反向。

展开参考答案与推导

在logits上检查而非仅看Embedding;Embedding可能因为上下文贡献仍有梯度。

读完之后,独立完成

课后作业:自己做一遍

本篇共 12 道作业,先完成 9 道基础与综合任务,再按前置知识挑战进阶题。其中包含 2 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能打印每个输入词元的角色与目标,构造只监督助手回答的 assistant-only 损失,处理截断与空目标并验证实际梯度。

本篇重点加练

监督范围与截断后的有效目标数决定训练数据是否仍有学习信号,增加逐位置审计。

  • completion与assistant掩码
  • 标签所有权与一次移位
  • 有效目标计数与截断

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01代码推演

    为一段“用户—助手—用户—助手”对话自行分配 token ID,画出 assistant-only 与 completion-only 两套目标归属和移位关系。

    提交内容
    对话序列及标签图
    怎样算完成
    • 角色边界清楚
    • 移位由单一环节负责

    练到的知识点条件语言建模与teacher forcing · 聊天模板和角色边界 · completion与assistant掩码 · 标签所有权与一次移位

  2. 作业 02原理问答

    区分提示 token 的输入参与、直接监督和梯度来源;讨论 EOS 与 PAD 共用 ID 时为什么仍需保留位置语义。

    提交内容
    三种角色说明表
    怎样算完成
    • 输入和标签不混同
    • 语义身份不只依赖数值 ID

    练到的知识点EOS和PAD区分 · 提示词上下文梯度

  3. 作业 03原理问答

    同一会话含助手普通文本与工具调用参数。自行定义训练目标是二者都学还是仅学一种,写出标签归属及移位的统一规则。

    提交内容
    监督目标与位置归属表
    怎样算完成
    • 任务选择先于 mask 实现
    • 一次移位责任明确

    练到的知识点completion与assistant掩码 · 标签所有权与一次移位

综合应用

  1. 作业 04编程实践

    实现 CPU SFT 标签构造器,覆盖多轮对话、截断到助手边界、没有有效回答三类样本;输出每样本有效目标数。

    提交内容
    构造器和带注释测试数据
    怎样算完成
    • 角色与截断规则确定
    • 空目标有显式处理

    练到的知识点聊天模板和角色边界 · completion与assistant掩码 · 标签所有权与一次移位 · 有效目标计数与截断

  2. 作业 05排错实验

    在小嵌入模型上注入双重移位、误屏蔽 EOS、屏蔽提示输入三类变化,记录有效目标和梯度差异。

    提交内容
    受控故障实验
    怎样算完成
    • 标签变化可逐位置审计
    • 不同梯度来源分开

    练到的知识点标签所有权与一次移位 · EOS和PAD区分 · 提示词上下文梯度

  3. 作业 06面试场景

    训练使用一套聊天模板,评测时省略角色边界。制定模板一致性回归测试,包含长度分布与停止行为的检查。

    提交内容
    模板审计器与回归样例
    怎样算完成
    • 模板版本可追踪
    • 格式差异不误归因模型能力

    练到的知识点有效目标计数与截断 · 训练评测模板一致

  4. 作业 07编程实践

    编写 CPU 标签对账器,输入原始位置 ID、截断窗口与角色掩码,输出原目标到实际训练位置的映射;覆盖左截断、右截断和空目标。

    提交内容
    对账代码与三类样例
    怎样算完成
    • 位置身份跨变换保留
    • 实际有效计数可核验

    练到的知识点标签所有权与一次移位 · 有效目标计数与截断

  5. 作业 08排错实验

    两条同长度样本具有不同助手比例。比较按整序列长度归一化与按有效助手目标归一化的梯度贡献,固定模型与标签。

    提交内容
    CPU 归约故障实验
    怎样算完成
    • 唯一变化因素是分母
    • 各样本权重有记录

    练到的知识点completion与assistant掩码 · 有效目标计数与截断

  6. 作业 09设计评审

    某数据集把拒绝回答、正常回答和仅工具动作统一套一个模板。设计至少三种标签审核样例,避免仅检查 JSON 结构而忽略训练目标。

    提交内容
    样例库与审核规则
    怎样算完成
    • 标签契约逐类别明确
    • 内容形式不直接决定监督归属

    练到的知识点completion与assistant掩码 · 标签所有权与一次移位

进阶挑战 · 掌握前置后选做

  1. 作业 10设计评审

    设计小型对照研究,比较两种监督范围对特定任务的影响;固定数据、基础模型和有效目标预算,说明无法完全配平的因素。

    提交内容
    研究协议与预算表
    怎样算完成
    • 监督策略定义精确
    • 混杂因素公开

    练到的知识点条件语言建模与teacher forcing · completion与assistant掩码 · 训练评测模板一致

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 11思维拓展 · 反例构造

    如果为了节省算力只保留含助手目标的片段,可能改变哪些条件信息?构造有效目标数不变但学习任务已改变的会话反例。

    提交内容
    会话反例及条件信息图
    怎样算完成
    • 目标数相同可核验
    • 上下文差异确实影响任务定义

    练到的知识点completion与assistant掩码 · 有效目标计数与截断

  2. 作业 12思维拓展 · 跨域迁移

    把标签掩码审计迁移到语音帧或图像 patch 监督,存在不同采样率或切块尺度时如何保存目标身份?给出小型映射实例。

    提交内容
    跨模态目标映射与计数契约
    怎样算完成
    • 原始单位与训练单位明确
    • 映射遗漏和重复可检测

    练到的知识点标签所有权与一次移位 · 有效目标计数与截断

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗