AI 工程基础体系 · 第 77/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。

指令微调:样本格式、数据混合、损失、灾难遗忘和评测

指令微调(Instruction Fine-Tuning,通常属于监督微调,Supervised Fine-Tuning,SFT)是在已经完成预训练的语言模型上,使用“用户意图—输入—期望输出”样本继续训练,使模型不仅能预测语言,还能按照任务说明、输出格式和对话角色完成任务。

预训练主要学习:

P(xtx<t)P(x_t\mid x_{<t})

其中 xtx_t 是当前位置的 token,x<tx_{<t} 是此前的 token。它回答的是“什么文本在语言上可能出现”。

指令微调增加了任务条件:

P(yx)P(y\mid x)

其中 xx 可以是指令、上下文、工具结果和历史对话,yy 是希望模型生成的回答。它回答的是“给定这个任务和输入,什么回答符合要求”。

二者并不是完全不同的模型结构。通常仍然使用 Transformer 解码器和因果注意力。差别主要在于训练数据的组织方式、哪些 token 参与损失,以及样本所表达的行为目标。Transformer 使用自注意力计算序列中 token 的依赖关系,原始结构见 Attention Is All You Need;实际训练通常通过 Hugging Face Transformers 等框架完成。


一、先明确训练对象:一个样本到底表示什么

1. 单轮指令样本

最简单的指令微调样本可以表示为三元组:

{
  "instruction": "把下面的句子翻译成英文。",
  "input": "机器学习需要高质量数据。",
  "output": "Machine learning requires high-quality data."
}

其语义是:

  • instruction 描述要做什么;
  • input 提供要处理的内容;
  • output 是监督信号;
  • output 不只是参考答案,也决定模型被训练成什么行为。

可以将它序列化为:

### Instruction:
把下面的句子翻译成英文。

### Input:
机器学习需要高质量数据。

### Response:
Machine learning requires high-quality data.

实际训练时,模型并不理解这些字段名本身的特殊含义。它只看到经过 tokenizer 转换后的 token 序列。因此,字段名、分隔符、角色标记和结束标记共同构成了训练协议。

2. 对话样本

多轮对话通常表示为消息数组:

{
  "messages": [
    {
      "role": "system",
      "content": "你是一个严谨的中文技术助手。"
    },
    {
      "role": "user",
      "content": "什么是梯度下降?"
    },
    {
      "role": "assistant",
      "content": "梯度下降是一种通过沿损失函数负梯度方向更新参数来降低损失的方法。"
    }
  ]
}

常见角色包括:

  • system:全局行为或约束;
  • user:用户输入;
  • assistant:期望模型生成的内容;
  • tool:工具调用结果或外部系统返回值。

角色本身不会自动产生训练效果。只有当训练模板把角色编码成模型可见的 token,并且训练时正确构造标签,角色边界才真正参与学习。

例如,一个对话模板可能生成:

<|system|>
你是一个严谨的中文技术助手。
<|user|>
什么是梯度下降?
<|assistant|>
梯度下降是一种通过沿损失函数负梯度方向更新参数来降低损失的方法。
<|end|>

推理时也必须使用兼容的模板。训练使用一种角色格式、推理换成另一种格式,模型可能仍然生成文字,但角色遵循、停止位置和回答风格都可能明显退化。

3. 工具调用样本

工具调用不是普通文本的简单变体。一个完整样本可能是:

{
  "messages": [
    {
      "role": "user",
      "content": "北京今天的天气如何?"
    },
    {
      "role": "assistant",
      "tool_calls": [
        {
          "name": "get_weather",
          "arguments": {
            "city": "北京"
          }
        }
      ]
    },
    {
      "role": "tool",
      "name": "get_weather",
      "content": "{\"temperature\": 23, \"condition\": \"晴\"}"
    },
    {
      "role": "assistant",
      "content": "北京今天晴,气温约 23°C。"
    }
  ]
}

这里至少包含两类能力:

  1. 生成结构正确的工具调用;
  2. 读取工具返回结果并生成最终回答。

如果数据中只有第二步,模型可能学会“看到工具结果后回答”,却不会生成可靠的调用参数。如果只有第一步,没有真实工具结果,模型可能学习到调用格式,但不会正确使用返回值。


二、样本格式的核心:输入序列、标签序列和损失掩码

1. 因果语言模型的训练形式

假设一个样本经过 tokenizer 后得到:

x=(x1,x2,,xT)x=(x_1,x_2,\ldots,x_T)

因果语言模型在位置 tt 预测下一个 token:

Pθ(xtx<t)P_\theta(x_t\mid x_{<t})

训练目标通常是交叉熵:

L(θ)=1Tt=1TlogPθ(xtx<t)\mathcal{L}(\theta) = -\frac{1}{T}\sum_{t=1}^{T} \log P_\theta(x_t\mid x_{<t})

其中:

  • θ\theta 是模型参数;
  • TT 是序列长度;
  • x<tx_{<t} 表示位置 tt 之前的 token;
  • 训练目标是提高真实下一个 token 的概率。

在实现中,input_idslabels 通常错开一个位置:

input_ids: [你, 好, 世, 界]
labels:    [好, 世, 界, EOS]

模型读入“你”,预测“好”;读入“你好”,预测“世”,以此类推。框架通常在模型内部完成 shift,因此传入的 labels 常常与 input_ids 等长。

2. 指令微调不一定让所有 token 都产生损失

对样本:

用户:2 + 2 等于多少?
助手:4

可以有两种训练方式。

全序列损失

用户:2 + 2 等于多少?助手:4
损失:所有 token 都计算

此时模型既学习回答“4”,也学习复现用户问题和模板。

仅回答部分损失

用户:2 + 2 等于多少?助手:4
损失:只计算“4”和结束标记

设每个 token 有一个掩码 mtm_t

mt={1,token 属于目标回答0,token 属于提示、用户消息或填充m_t= \begin{cases} 1,& \text{token 属于目标回答}\\ 0,& \text{token 属于提示、用户消息或填充} \end{cases}

则掩码损失为:

L=t=1TmtlogPθ(xtx<t)t=1Tmt\mathcal{L} = -\frac{\sum_{t=1}^{T}m_t\log P_\theta(x_t\mid x_{<t})} {\sum_{t=1}^{T}m_t}

被忽略的位置通常在 labels 中设为 -100,PyTorch 的交叉熵会跳过这些位置。

两种方式没有绝对的正确答案:

  • 如果想训练完整对话模板、格式和角色边界,全序列损失可能有价值;
  • 如果用户输入很长、回答很短,或者不希望模型过度拟合提示文本,assistant-only loss 更直接;
  • 如果把所有非回答 token 都屏蔽,却忘记保留回答的结束标记,模型可能不学会何时停止;
  • 如果回答部分掩码错位一位,模型训练的可能是回答前一个 token 或后一个 token,损失仍然会下降,但生成质量会异常。

3. 一个完整的数值例子

假设目标回答有三个 token,模型在三个位置对真实 token 的预测概率分别为:

0.5,0.25,0.80.5,\quad 0.25,\quad 0.8

则平均负对数似然为:

L=log0.5+log0.25+log0.830.707\mathcal{L} = -\frac{\log 0.5+\log 0.25+\log 0.8}{3} \approx 0.707

如果序列前面还有四个提示 token,并采用 assistant-only loss,分母仍是有效回答 token 数 33,因此损失仍约为 0.7070.707

如果错误地把这四个提示 token 也放入分母,但它们被当作零损失位置处理,则损失会变成:

3×0.70770.303\frac{3\times0.707}{7}\approx0.303

这个数值看起来更低,却不代表模型变好了。它只是被无效位置稀释了。因此,比较不同数据集或不同 batch 的训练损失时,必须明确分母是:

  • 所有 token 数;
  • 非 padding token 数;
  • 仅 assistant token 数;
  • 还是经过样本权重后的有效 token 数。

4. padding、截断和结束标记

一个 batch 中的样本长度通常不同,需要补齐到相同长度:

样本 A: [问题, 答案, EOS, PAD, PAD]
样本 B: [问题, 答案, 更长, EOS, PAD]

attention_mask 负责告诉模型哪些位置是有效输入;labels=-100 负责告诉损失函数哪些位置不参与监督。两者职责不同:

  • attention_mask=0:通常表示注意力计算忽略该位置;
  • labels=-100:表示该位置不计算监督损失。

将两者混淆会产生不同错误。若 padding 没有从注意力中排除,模型可能读取无意义的填充;若 padding 没有从 labels 中排除,模型会被训练去生成 padding token。

截断也会改变监督目标。若从序列右侧截断,可能截掉答案末尾;若从左侧截断,可能丢失系统约束或用户问题。长上下文样本尤其需要记录:

  • 原始长度;
  • 截断方向;
  • 截断后剩余的回答 token 数;
  • 被完全截断、没有任何监督 token 的样本数。

“输入能放进上下文窗口”不等于“样本有效”。如果截断后 labels 全部是 -100,该样本对参数没有梯度贡献,却可能仍占用显存和训练时间。


三、使用模板和 tokenizer 构造可验证的样本

下面示例展示单轮 prompt-completion 的基本构造。它依赖一个已经安装并可访问的 Hugging Face tokenizer:

pip install torch transformers

代码:

import torch
from transformers import AutoTokenizer

model_name = "Qwen/Qwen2.5-0.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name)

prompt = "用户:2 + 2 等于多少?\n助手:"
answer = "4"

prompt_ids = tokenizer(prompt, add_special_tokens=False)["input_ids"]
answer_ids = tokenizer(
    answer + tokenizer.eos_token,
    add_special_tokens=False
)["input_ids"]

input_ids = prompt_ids + answer_ids

# 提示部分不计算损失,答案和 EOS 计算损失
labels = [-100] * len(prompt_ids) + answer_ids
attention_mask = [1] * len(input_ids)

batch = {
    "input_ids": torch.tensor([input_ids], dtype=torch.long),
    "attention_mask": torch.tensor([attention_mask], dtype=torch.long),
    "labels": torch.tensor([labels], dtype=torch.long),
}

print("input_ids 长度:", len(input_ids))
print("有效监督 token 数:", sum(x != -100 for x in labels))
print("解码结果:", tokenizer.decode(input_ids))

预期现象是:

  • 解码结果包含用户问题、助手前缀、答案和结束标记;
  • 有效监督 token 数只统计答案及 EOS;
  • 模型前向计算时,labelsinput_ids 等长;
  • 计算损失时,-100 位置被忽略。

这段代码只构造一个样本,还没有处理 batch padding。生产训练还需要确保:

  1. tokenizer 与模型来自兼容的 checkpoint;
  2. 使用模型原本约定的 chat template,而不是随意拼接字符串;
  3. pad_token_id 已设置;
  4. 每个样本至少有一个有效监督 token;
  5. 训练和推理使用相同的模板协议。

对于支持聊天模板的 tokenizer,通常可以使用:

messages = [
    {"role": "system", "content": "你是严谨的数学助手。"},
    {"role": "user", "content": "2 + 2 等于多少?"},
    {"role": "assistant", "content": "4"},
]

text = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=False,
)
print(text)

apply_chat_template 的具体参数和模板能力依赖 Transformers 版本以及 tokenizer 配置。它不是所有模型都以相同方式实现的 API。训练前应打印模板结果,确认:

  • system、user、assistant 是否按预期分隔;
  • 是否存在重复 BOS/EOS;
  • assistant 是否真的位于目标回答区域;
  • 推理时 add_generation_prompt=True 的输出是否与训练格式衔接。

仅仅把 JSON 字段传给 Trainer,不会自动得到正确的角色掩码。掩码必须由数据整理器或预处理逻辑明确生成。


四、数据混合:不是简单地把文件拼在一起

数据混合是指同时使用多个来源或多个任务的数据训练同一个模型,例如:

  • 通用问答;
  • 代码生成;
  • 数学推理;
  • 企业内部知识;
  • 安全拒答;
  • 工具调用;
  • 多语言数据。

设第 kk 个数据集的分布为 DkD_k,混合权重为 αk\alpha_k,满足:

αk0,k=1Kαk=1\alpha_k\ge 0,\qquad \sum_{k=1}^{K}\alpha_k=1

混合分布为:

Dmix=k=1KαkDkD_{\text{mix}}=\sum_{k=1}^{K}\alpha_kD_k

对应的总体目标是:

Lmix(θ)=k=1KαkE(x,y)Dk[θ(x,y)]\mathcal{L}_{\text{mix}}(\theta) = \sum_{k=1}^{K} \alpha_k \mathbb{E}_{(x,y)\sim D_k} [\ell_\theta(x,y)]

这一定义说明一个关键事实:数据集的样本数量和训练中的实际影响力不是一回事。

1. 直接拼接的隐含权重

假设:

  • 通用数据集有 90 万条;
  • 代码数据集有 10 万条。

如果完整拼接并各遍历一次,近似采样权重是:

αgeneral=0.9,αcode=0.1\alpha_{\text{general}}=0.9,\qquad \alpha_{\text{code}}=0.1

但如果代码样本每条平均 500 个 token,通用样本每条平均 50 个 token,那么按 token 计算的训练量约为:

90×50=450090万\times50=4500万

和:

10×500=500010万\times500=5000万

此时代码实际贡献的监督 token 约占:

50004500+500052.6%\frac{5000}{4500+5000}\approx52.6\%

因此至少要区分三种权重:

  1. 样本权重:某类样本被抽到的概率;
  2. token 权重:某类监督 token 在总有效 token 中的比例;
  3. 梯度权重:某类数据最终对参数更新大小的影响。

它们在样本长度不同、损失归一化不同或使用样本权重时并不相同。

2. 重采样和损失加权

有两种常见混合方式。

采样层混合

按指定概率选择数据集。例如:

P(k)={0.5,通用0.3,代码0.2,安全P(k)= \begin{cases} 0.5,& \text{通用}\\ 0.3,& \text{代码}\\ 0.2,& \text{安全} \end{cases}

然后从被选中的数据集采样一条样本。

优点是直观,能够控制每个 batch 的组成;风险是小数据集可能被反复采样,导致过拟合。

损失层混合

每个 batch 分别计算各数据集损失:

L=0.5Lgeneral+0.3Lcode+0.2Lsafety\mathcal{L} = 0.5\mathcal{L}_{\text{general}} + 0.3\mathcal{L}_{\text{code}} + 0.2\mathcal{L}_{\text{safety}}

这允许分别观察各项损失,但必须定义每个子损失的归一化方式。若代码样本平均回答长度远大于安全样本,直接对 token 平均损失加权,和按样本平均损失加权,结果可能不同。

3. 一个反例:大数据集吞没小任务

假设企业领域数据只有 1 万条,通用数据有 1000 万条。将它们直接拼接后训练一个 epoch,企业数据只占约 0.1%。即使企业任务对产品最重要,梯度更新也主要由通用数据决定。

反过来,把企业数据复制 1000 倍,也不一定正确。模型可能快速记住内部措辞和固定答案,却损害通用能力,还可能放大重复样本中的错误和敏感信息。

合理的混合策略必须同时观察:

  • 每个数据源的有效样本数和有效 token 数;
  • 训练过程中的实际抽样比例;
  • 各数据源独立损失;
  • 各任务验证集指标;
  • 重复采样次数;
  • 数据源的权限和时效性。

4. 混合不是数据质量的替代品

如果一个数据集包含错误标签、互相矛盾的指令或不一致的输出格式,降低它的采样比例只能降低伤害,不能修复问题。

例如,同一问题:

问题:退款需要几天?
答案 A:通常 3 个工作日。
答案 B:通常 7 个自然日。

若没有条件说明,这不是“需要多混一些数据”的问题,而是监督信号冲突。模型可能学到平均化、随机化或依赖表面词的回答。应先补充生效时间、业务渠道和适用条件,或者将问题改写为需要澄清的任务。


五、损失:下降的数字不一定代表行为变好

1. 交叉熵的含义

对目标 token yty_t,模型输出词表上的概率分布 pθ(x,y<t)p_\theta(\cdot\mid x,y_{<t})。交叉熵为:

t=logpθ(ytx,y<t)\ell_t=-\log p_\theta(y_t\mid x,y_{<t})

如果模型给正确 token 的概率从 0.10.1 提升到 0.50.5,该位置损失从:

log0.12.303-\log0.1\approx2.303

降到:

log0.50.693-\log0.5\approx0.693

损失降低代表模型更愿意生成训练标签,但不直接保证:

  • 答案事实正确;
  • 输出符合业务权限;
  • 长答案没有局部幻觉;
  • 拒答策略正确;
  • 结构化 JSON 可被解析。

交叉熵是 token 级代理目标,生产质量是任务级结果。

2. 为什么训练损失可能下降而评测变差

常见因果链包括:

flowchart LR
    A[训练样本格式] --> B[tokenizer与模板]
    B --> C[labels和loss mask]
    C --> D[梯度更新]
    D --> E[模型行为]
    E --> F[任务评测]
    E --> G[安全与权限检查]

其中任意一环都可能让损失与行为脱钩。

例如:

  • labels 错位:模型学习错误的下一个 token;
  • 只训练回答、不训练 EOS:回答后继续生成;
  • 训练模板和推理模板不一致:离线损失正常,实际输入分布不同;
  • 训练集存在重复:损失下降来自记忆,而不是泛化;
  • 评测要求精确 JSON,但训练答案带解释文字:文本损失正常,解析成功率下降;
  • 训练数据默认模型可以查看内部字段,部署时权限系统却不允许访问:模型学会了不应有的回答路径。

3. 长回答会改变损失贡献

如果每个样本采用 token 平均,长回答会产生更多梯度项。假设:

  • 样本 A 的回答长 20 token;
  • 样本 B 的回答长 200 token。

在样本数量相同的情况下,B 的回答 token 约贡献十倍的损失项。若 B 属于某个风格单一的任务,它可能主导模型输出风格。

若改为“先按样本求平均,再对样本平均”,每条样本权重更接近一致,但长回答中的每个 token 权重会降低。两者都合理,关键是必须明确目标:

Ltoken=itmi,ti,titmi,t\mathcal{L}_{\text{token}} = \frac{\sum_i\sum_t m_{i,t}\ell_{i,t}} {\sum_i\sum_t m_{i,t}}

或:

Lsample=1Nitmi,ti,ttmi,t\mathcal{L}_{\text{sample}} = \frac{1}{N} \sum_i \frac{\sum_t m_{i,t}\ell_{i,t}} {\sum_t m_{i,t}}

前者按有效 token 加权,后者按样本加权。长短样本混合时,二者会产生不同模型。


六、灾难遗忘:为什么新任务学会了,旧能力却消失

灾难遗忘(Catastrophic Forgetting)是指模型在学习新数据后,原先已经掌握的能力显著下降。它不是“模型完全忘记了所有知识”,而通常表现为某些分布、任务或行为能力被新训练目标覆盖。

设预训练或旧任务目标为:

Lold(θ)\mathcal{L}_{\text{old}}(\theta)

新指令数据目标为:

Lnew(θ)\mathcal{L}_{\text{new}}(\theta)

只优化新目标:

minθLnew(θ)\min_\theta \mathcal{L}_{\text{new}}(\theta)

得到的参数可能位于新任务的低损失区域,但不再位于旧任务的低损失区域。更具体地,某个参数更新为:

θ=θηθLnew\theta'=\theta-\eta\nabla_\theta\mathcal{L}_{\text{new}}

旧任务损失的一阶变化近似为:

Lold(θ)Lold(θ)ηLoldLnew\mathcal{L}_{\text{old}}(\theta') \approx \mathcal{L}_{\text{old}}(\theta) - \eta \nabla\mathcal{L}_{\text{old}}^\top \nabla\mathcal{L}_{\text{new}}

如果两个梯度方向相反:

LoldLnew<0\nabla\mathcal{L}_{\text{old}}^\top \nabla\mathcal{L}_{\text{new}}<0

则更新新任务会增加旧任务损失。这就是梯度冲突的一个形式化解释。

1. 典型触发因素

新数据过窄

只用客服问答训练,模型可能更倾向于模板化、礼貌化回答;代码、数学和开放式生成能力可能下降。

新数据量太小但重复次数太多

小规模领域数据被过采样后,模型会强化领域特有的词汇、格式和事实,即使这些规律不适用于通用输入。

学习率过高或训练过久

更新步长过大时,模型参数离原有能力所在区域更远。训练损失还会下降,但通用评测可能先升后降。

标签冲突

新数据要求“只输出 JSON”,旧数据常要求解释过程。若没有明确任务边界,模型可能在普通问题中也强制输出 JSON,或者在结构化接口中混入自然语言。

分布和模板变化

灾难遗忘不仅是知识遗忘,也可能是格式和行为遗忘。模型仍然知道某个事实,却不再遵守原有停止符、角色规则或工具调用协议。

2. 反例:领域微调并不必然导致遗忘

如果新数据覆盖面合理、学习率较小、训练步数受控,并且保留部分通用数据混合训练,旧能力可能基本稳定。因而不能仅凭“进行了 SFT”就断言发生灾难遗忘,必须比较微调前后的同一组回归评测。

3. 缓解方法及其机制

混合回放

在新数据中加入旧分布或通用数据:

L=λLnew+(1λ)Lreplay\mathcal{L} = \lambda\mathcal{L}_{\text{new}} + (1-\lambda)\mathcal{L}_{\text{replay}}

replay 不必等同于完整预训练数据,可以是经过授权的代表性样本、公开能力集和历史回归集。

降低更新自由度

LoRA 等参数高效微调方法只训练低秩适配器,而不是直接更新全部权重。它通常降低了修改基础模型的范围,但不是遗忘的数学保证:

  • rank 较高、学习率较大或训练时间较长时,仍可能明显改变行为;
  • 推理时合并 adapter 后,基础模型和 adapter 的版本必须绑定;
  • 多个 adapter 叠加可能产生新的冲突。

保留旧模型蒸馏约束

让新模型在旧样本或回放样本上的输出接近旧模型:

L=Lnew+βDKL(PoldPnew)\mathcal{L} = \mathcal{L}_{\text{new}} + \beta D_{\mathrm{KL}} \left( P_{\text{old}}\parallel P_{\text{new}} \right)

其中 DKLD_{\mathrm{KL}} 衡量两个概率分布的差异,β\beta 控制保持旧行为的强度。约束过强会限制新能力,过弱则保护效果不明显。

分离能力和路由

对不同领域使用不同 adapter,在请求进入系统时根据租户、任务或权限选择 adapter。这样可以减少一个全局模型承载互相冲突的行为,但代价是:

  • 路由逻辑更复杂;
  • 需要管理多个权重版本;
  • 跨领域任务可能需要组合能力;
  • adapter 本身不能绕过数据访问权限。

七、评测:必须测行为,而不是只看训练损失

评测是用独立数据和明确指标,判断模型在目标任务上的泛化、可靠性和系统约束是否满足。它至少包含四个层次。

1. 训练指标

训练集 loss 用于观察优化是否进行,但不能作为最终质量结论。

应至少记录:

  • train loss;
  • validation loss;
  • 每个数据源的独立 loss;
  • 有效监督 token 数;
  • 被截断样本比例;
  • 没有有效 label 的样本数;
  • 梯度范数和学习率;
  • checkpoint 之间的变化。

验证集损失持续上升而训练损失下降,通常提示过拟合或训练—验证格式差异。两个损失都下降,但任务指标下降,则说明 token 目标和任务目标不一致,或者评测管线存在问题。

2. 任务指标

不同任务必须使用不同的判定标准。

分类

使用 accuracy、precision、recall、F1 等,但要明确类别不平衡。例如拒答检测中,全部预测为“拒答”可能得到很高 accuracy,却没有实际价值。

抽取和结构化生成

不能只用字符串完全匹配。应分别评估:

  1. JSON 是否可解析;
  2. 字段是否完整;
  3. 字段类型是否正确;
  4. 字段值是否满足业务约束;
  5. 内容是否正确。

例如:

{"name": "张三", "age": "二十"}

虽然是合法 JSON,但若 schema 要求 age 为整数,则仍然失败。

翻译、摘要和生成

BLEU、ROUGE 等词面指标能反映与参考答案的重合,但不能完整表示事实正确性和可读性。摘要应额外检查:

  • 是否遗漏关键事实;
  • 是否引入原文没有的事实;
  • 是否违反长度或格式要求。

数学和代码

数学应执行答案归一化或符号验证;代码应在隔离环境中运行测试。仅比较字符串会把变量名不同但正确的代码误判为错误,也会把看似相同但无法运行的代码误判为正确。

3. 能力回归评测

为检测灾难遗忘,应在微调前后使用同一版本的回归集:

能力 微调前 微调后 变化
通用问答
数学
代码
领域任务
安全拒答
工具调用

不能只报告新增领域分数。若领域任务从 60 提升到 80,但通用任务从 75 降到 50,生产系统是否接受取决于业务权重,而不是单一“总分”。

4. 安全、权限和成本评测

模型输出正确,不代表系统允许输出。

例如用户询问内部订单信息,模型可能根据训练记忆生成一个看似合理的结果,但系统仍必须在检索、工具调用或数据库访问层做授权检查。权限应由外部系统强制执行,不能依赖 SFT 后模型“自觉拒绝”。

需要分别测量:

  • 未授权用户访问受限数据时是否被拦截;
  • 模型是否泄露训练样本中的敏感字段;
  • 工具调用参数是否越权;
  • 拒答是否过度,导致合法请求被阻断;
  • 单次请求的 token 数、推理延迟和 GPU 成本;
  • 重试、长输出和工具循环造成的成本增长。

成本可粗略拆为:

C=C训练+C推理+C存储+C评测C = C_{\text{训练}} + C_{\text{推理}} + C_{\text{存储}} + C_{\text{评测}}

其中训练成本受有效 token 数、序列长度、batch、训练步数和硬件影响;推理成本受输入长度、输出长度、并发和重试影响。一个 SFT 模型若倾向于输出冗长解释,可能在离线质量略升的同时显著提高线上 token 成本。


八、评测集设计:防止数据泄漏和虚假提升

1. 训练集、验证集和测试集的职责

  • 训练集用于更新参数;
  • 验证集用于选择超参数、checkpoint 和数据混合比例;
  • 测试集只在方案基本确定后使用,用于最终估计。

如果反复根据测试集结果修改训练数据,测试集实际上变成了验证集,最终分数会偏乐观。

2. 去重和污染

生成式模型容易记忆样本。若测试问题或其近似改写出现在训练集,模型可能通过记忆获得高分。

需要检查:

  • 完全重复;
  • 高相似问题;
  • 相同答案模板;
  • 同一文档不同切分;
  • 公开基准数据是否被训练语料收录;
  • 评测提示词是否进入过训练集。

污染检查不能保证绝对无泄漏,但能发现明显问题。对内部数据,还需要保留样本来源、授权范围、采集时间和版本哈希,避免无法解释“模型为什么知道这件事”。

3. 生成式评审的边界

使用另一个语言模型作为评审器可以降低人工评测成本,但评审器有系统偏差:

  • 偏爱更长的答案;
  • 偏爱自己的表达风格;
  • 对格式错误不敏感;
  • 可能被提示注入;
  • 在事实核验上不一定可靠。

因此,模型评审适合做排序、初筛或辅助分析,不应替代可执行的程序检查和人工抽检。对于 JSON、SQL、代码和权限结果,应优先使用解析器、测试执行器和策略引擎。


九、从数据到上线的完整验证路径

一个可追踪的 SFT 流程应保留以下状态:

flowchart TD
    A[原始数据] --> B[授权与脱敏]
    B --> C[格式规范化]
    C --> D[模板渲染与tokenize]
    D --> E[labels与mask校验]
    E --> F[训练/混合采样]
    F --> G[验证集选择checkpoint]
    G --> H[回归与安全评测]
    H --> I[灰度部署]
    I --> J[线上监控与回滚]

关键路径不是“文件上传后启动训练”,而是:

  1. 原始数据先经过权限确认和敏感信息处理;
  2. 格式规范化后,保存原始字段与规范化字段的对应关系;
  3. 模板渲染后抽样解码,确认模型实际看到的文本;
  4. 检查每条样本的有效 label、EOS、截断状态;
  5. 训练时记录数据源和实际采样比例;
  6. 选择 checkpoint 时同时看新增能力和旧能力;
  7. 上线前执行权限、安全、成本和回归测试;
  8. 线上发现异常时可以按模型、模板、adapter、数据版本回滚。

权限和成本必须进入同一生产系统,而不是训练完成后的附加检查。模型权重、训练数据、评测集、日志和推理服务都应有版本和访问控制。尤其不能因为“数据已经用于训练”,就认为所有部署环境都自动获得了访问其中信息的权限。


十、常见失败表现与诊断顺序

1. 模型回答像复读模板

可能原因:

  • 数据中模板占比过高;
  • instruction 和 output 的边界不清;
  • 全序列损失让模型过度学习固定前缀;
  • 训练集答案过短且高度重复。

诊断方法:

  • 解码检查不同来源的训练样本;
  • 统计模板 token 占有效 token 的比例;
  • 对比 assistant-only loss 和全序列 loss;
  • 使用全新措辞的验证问题测试。

2. 模型会回答,但不会停止

可能原因:

  • EOS 没有加入回答;
  • EOS 被错误设置为 -100
  • 推理停止 token 与训练结束 token 不一致;
  • 长答案截断导致模型没看到结束位置。

应检查最后一个有效 label、tokenizer 的 EOS 配置和生成参数中的停止条件。

3. 训练损失很低,实际答案很差

可能原因:

  • 数据泄漏;
  • 标签错位;
  • 训练和推理模板不同;
  • 评测答案被过度格式化;
  • 只测 token loss,没有测任务约束。

应先用一个极小样本集做过拟合测试:模型是否能准确复现训练样本。如果连极小集都无法拟合,优先检查 tokenization、labels、mask 和模型调用;如果能拟合但验证差,再检查数据质量、分布和过拟合。

4. 新领域效果提升,但通用能力下降

可能原因:

  • 新领域数据比例过高;
  • 小数据重复采样;
  • 学习率或训练步数过大;
  • 新旧任务梯度冲突;
  • 领域格式要求被泛化到所有任务。

可通过减少学习率、缩短训练、混入回放数据、使用独立 adapter,以及对不同任务分别评测来定位。不要只看领域验证集 loss。

5. 结构化输出偶尔无法解析

可能原因:

  • 训练样本中 JSON 外混有解释;
  • 训练时 schema 不固定;
  • 没有 EOS 或停止控制;
  • 评测只比较文本,没有记录解析失败类型;
  • 生成时采样参数过于发散。

诊断应将失败拆为“无法解析、缺字段、类型错误、值错误、权限错误”,而不是只统计一个总准确率。


十一、一个可执行的最小损失检查

在正式训练前,可以用 PyTorch 验证 mask 是否按预期工作:

import torch
import torch.nn.functional as F

# 假设 batch=1,序列长度=5,词表大小=4
# 模型输出 logits[:, t, :] 用于预测 labels[:, t+1]
logits = torch.tensor([[
    [2.0, 0.0, 0.0, 0.0],
    [0.0, 2.0, 0.0, 0.0],
    [0.0, 0.0, 2.0, 0.0],
    [0.0, 0.0, 0.0, 2.0],
    [1.0, 1.0, 1.0, 1.0],
]])

# 第一个位置是提示,不计损失;后面三个位置是回答目标
labels = torch.tensor([[-100, 1, 2, 3, -100]])

shift_logits = logits[:, :-1, :].contiguous()
shift_labels = labels[:, 1:].contiguous()

loss = F.cross_entropy(
    shift_logits.view(-1, shift_logits.size(-1)),
    shift_labels.view(-1),
    ignore_index=-100,
)

print("loss =", float(loss))
print("有效目标数 =", int((shift_labels != -100).sum()))

这里的关键是:

  • logits[:, :-1]labels[:, 1:] 对齐;
  • -100 位置不会进入交叉熵;
  • 最后一个 labels 位置不会被前面的 logits 预测,因此 shift 后自然不参与;
  • 如果把 labels 整体向左或向右错误移动,代码仍可能运行,但监督语义会改变。

这类小测试不能证明训练一定正确,却能在消耗 GPU 之前发现最常见的标签和掩码错误。


十二、如何判断一次指令微调是否真正成功

一次 SFT 不应只用“loss 下降了”来定义成功。至少需要同时满足:

  1. 样本协议正确:角色、模板、EOS、截断和 padding 均可解释;
  2. 监督目标明确:知道哪些 token 参与损失,以及损失如何归一化;
  3. 混合比例可追踪:能够区分样本、token 和梯度层面的数据影响;
  4. 新增能力可验证:领域任务在独立测试集上提升;
  5. 旧能力无不可接受回归:通用、代码、数学、安全等回归集有对比;
  6. 输出可被系统消费:结构化结果可解析,工具调用可执行;
  7. 权限边界仍然成立:模型不能代替授权系统;
  8. 成本可接受:训练和推理的 token、延迟、并发和重试成本可控;
  9. 版本可恢复:数据、模板、tokenizer、模型、adapter 和评测结果能够对应并回滚。

指令微调的本质不是把一批问答文本“喂给模型”,而是重新定义一部分条件概率分布。样本格式决定条件如何编码,数据混合决定哪些行为获得更多梯度,损失掩码决定哪些 token 被当作行为目标,灾难遗忘描述新目标对旧能力的覆盖,而评测负责验证这种改变是否符合系统目标。只有把这几层连起来,训练损失才有可解释性,模型能力变化才有可归因性,最终部署结果才可验证和恢复。


系列导航与关联阅读

官方资料

本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。