跳到内容
Link's Blog
返回

从 128 条到 7473 条:一次 SFT 实现与五组 GSM8K 实验复盘

· 12分钟阅读编辑页面

从 128 条到 7473 条:一次 SFT 实现与五组 GSM8K 实验复盘

实验日期:2026 年 10 月 20—21 日。本文基于实际训练日志和本地 W&B 结果整理。代码为整理时的当前快照;实验数值来自已完成运行,没有为写文章重新训练。本文为实验记录,不代表官方基准成绩。

这次练习围绕一个问题:使用相同的模型和训练步数,只改变监督微调的数据量,会发生什么?

五组实验的训练数据量分别为 128、256、512、1024 和 7473 条。最明显的现象是:小数据组很快达到较高准确率,但训练后期持续退化;全量数据组在更长训练过程中保持了效果,最终准确率为 74.91%。

1. 这次 SFT 在训练什么

SFT 使用成对的 prompt 和 response 作为监督数据。模型读取问题和回答的前缀,预测回答中的下一个 token。训练目标是提高示范回答的概率,评估阶段计算的 reward 不参与这段训练的梯度更新。

本实验的模型为 Qwen2.5-Math-1.5B,训练文件为 train_sft_reason_gsm8k_raw.jsonl。代码直接读取其中的 prompt 和 response。没有开启 --filter_correct,因此没有按 is_correct 字段筛选;这不等于已经核实训练数据中存在多少错误回答。

评估时,程序把 GSM8K 问题放入 r1_zero.prompt,让模型生成推理文本及答案。评分器检查输出格式,再比较提取出的最终答案。

2. 从代码理解训练流程

2.1 输入、标签和 response mask

tokenize_prompt_and_output 分别编码 prompt 和 response,再拼接并右侧补齐。假设 prompt token 为 [P1, P2],response token 为 [R1, R2],则对齐关系是:

拼接序列:      P1  P2  R1  R2
input_ids:     P1  P2  R1
labels:        P2  R1  R2
response_mask: 0   1   1

每个位置预测下一个 token,因此输入取 [:-1],标签与回答掩码取 [1:]。损失只保留回答位置,prompt 和补齐位置不直接作为监督目标。prompt 仍是生成回答的上下文,回答损失可以通过模型的计算路径向前传播梯度。

2.2 取出目标 token 的 log probability

模型 logits 的形状为 [B, L, V]:batch、序列长度、词表大小。训练代码计算:

lse = torch.logsumexp(logits, dim=-1)
target_logits = torch.gather(logits, -1, labels.unsqueeze(-1)).squeeze(-1)
log_probs = target_logits - lse

这对应目标 token 的 log-softmax 值,结果形状为 [B, L]。SFT 对其取负,得到负对数似然损失。

2.3 当前 loss 的归一化方式

当前实现先对每条回答的有效 token 损失求和,再按样本数平均:

L = (1 / B) × Σ样本 Σ回答位置 [-log p(目标 token | 前缀)]

这里的 normalize_constant=1.0,所以没有再按回答 token 数平均。这意味着回答越长,在其他条件相同的情况下,累计损失贡献越大。不能把日志中的 loss 直接与其他项目的 token 平均交叉熵比较,也不能直接对它取指数当作通常定义的 token 困惑度。

2.4 梯度累积与参数更新

每个 micro-batch 为 2 条,累积 32 次后更新一次参数,有效样本量为 64 条次。每次反向传播前把 loss 除以 32,日志记录时再还原这一缩放。然后将梯度范数裁剪到 1.0,执行 AdamW 更新并清空梯度。

实际循环由 micro_batch_size × gradient_accumulation_steps 决定;当前代码中的 batch_size 参数没有用于自动计算或校验累积次数。单独修改 --batch_size 并不会改变实际有效 batch。

每次抽取 micro-batch 内的两条数据互不重复,但不同 micro-batch 之间可以抽到相同样本。

2.5 评估与权重同步

训练使用 PyTorch 模型,生成评估使用 vLLM。每次评估前,将当前策略权重同步到 vLLM 并清空 prefix cache,避免用旧权重或旧缓存评估新模型。代码通过 CPU 中转权重,以适配当时的运行环境。

评估使用 temperature=0.0,最多生成 1024 个 token,遇到 </answer> 停止并保留停止字符串。这里的 1024 是生成 token 上限,不是训练输入截断长度。

3. 实验设置

配置五组共同设置
基础模型Qwen2.5-Math-1.5B
训练步数256
学习率2e-5,代码未配置学习率调度器
micro-batch / 梯度累积2 / 32
每步样本条次64
评估间隔每 8 步,另包含 step 0
评估数据GSM8K test.jsonl,1319 题
生成方式贪心,最多 1024 token
正确样本过滤未开启
设备单卡训练与评估,cuda:0
随机种子参数42;显式传入 vLLM

128~1024 条由批量脚本运行;7473 条来自单独完成的全量实验。其主要配置一致。更早的另一份 7473 条未完成日志没有混入本次比较。

当前训练程序没有独立显式调用 random.seed、numpy.random.seed 或 torch.manual_seed;vLLM 初始化是否影响全局随机状态依赖库实现。因此,只看 --seed 42 不足以证明训练抽样的完全可复现性,也不能假定不同规模的子集严格嵌套。

4. 指标含义

指标当前实现的定义阅读方式
eval/accuracy平均最终 reward,等于格式合格且答案正确的题目比例核心效果指标
eval/answer_score平均答案奖励与 accuracy 完全相同
eval/format_score格式合格的题目比例不代表数学答案正确
eval/avg_length生成文本 len(text) 的均值单位是字符,不是 token
eval/avg_length_correctreward 大于 0.5 的回答平均字符数当前即判对的回答
eval/avg_length_incorrect其他回答平均字符数包含格式错误
train/loss回答 token NLL 求和后按样本平均衡量训练拟合,受长度影响
train/response_entropy训练回答位置的平均词表分布熵低表示确定,不表示正确
train/global_entropyprompt 与回答非 padding 位置的平均熵混合了两类位置
train_step优化器更新次数不是 micro-batch 次数
eval/samples每轮前 100 题的生成表用于错误分析,不是全评估集

评分器要求出现精确子串 </think> <answer> 和 </answer>。格式不合格时,答案奖励也为 0。它不是对任意格式都提取数字的纯答案准确率,也没有严格校验完整 XML 结构。

熵是在训练文本前缀下计算的,不是评估集自由生成过程的平均熵。代码使用自然对数;当前直接在模型 logits 的数值精度下计算,小熵数值不宜作过细解读。全局熵和回答熵也不能脱离各位置的权重直接相减得到 prompt 熵。

5. 五组结果总览

训练条数最高准确率最高点 step最终准确率下降/百分点平均抽取次数
12872.10%849.81%22.29128.00
25671.42%855.50%15.9264.00
51273.46%3256.03%17.4432.00
102473.09%72、8061.33%11.7516.00
747374.91%25674.91%0.002.19

五组初始准确率均为 20.85%。最高点只指每 8 步采样评估所观测到的最高值,不是对所有训练时刻的穷尽搜索。

按最终模型比较,7473 条组最好,较 1024 条组高 13.57 个百分点,相当于多答对 179 道题。按各组峰值比较,全量组仍最高,但比 512 条组只高约 1.44 个百分点;单次运行不足以证明这一小幅峰值差距可以稳定复现。

准确率随训练进度变化

step12825651210247473
020.85%20.85%20.85%20.85%20.85%
872.10%71.42%70.96%70.89%70.96%
1670.66%70.81%70.58%70.05%69.37%
3266.19%70.58%73.46%70.96%71.34%
6455.12%64.22%70.20%72.25%72.40%
8058.38%62.09%68.84%73.09%73.01%
12853.98%59.29%62.09%70.89%73.84%
19252.16%55.65%56.48%66.79%74.68%
25649.81%55.50%56.03%61.33%74.91%

完整 33 个评估点见 results/eval_history_all.csv,总计五组 165 条记录。

6. 逐组分析

128 条:早期见效,后期退化最明显

step 8 达到 72.10%,step 64 降至 55.12%,最终只有 49.81%。此时最终训练 loss 为 0.1373,回答熵为 0.0358。低训练损失与下降的测试准确率同时出现,是明显的过拟合信号。不过,仅凭这些指标不能把所有下降都严格归因于同一个机制。

256 条:高分维持时间延长,但仍训练过久

step 8 为 71.42%,step 40 仍有 70.51%,到 step 128 为 59.29%,最终为 55.50%。最终 loss 为五组最低的 0.0562,却没有获得最高测试准确率。这说明训练拟合程度与泛化表现必须分开评估。

512 条:额外训练先有收益,再出现退化

从 step 8 的 70.96% 上升到 step 32 的 73.46%,step 64 仍有 70.20%,但最终降至 56.03%。更多不同样本使额外更新在早期仍有价值,长期反复训练后优势则大幅丢失。

1024 条:高分窗口进一步延长

step 72、80 达到 73.09%,到 step 128 仍有 70.89%,最终为 61.33%。相较小数据组,它更耐训练,但 256 步相对于当前数据量仍表现出后期退化。

7473 条:高位波动,没有出现同样的持续下滑

step 8 达到 70.96%,step 48 为 73.01%,step 128 为 73.84%,最终为 74.91%。中间也有回落,例如 step 160 为 72.25%,随后恢复,因此不能描述成单调增长。

这组最后一步恰好是观测最高点,并不证明 256 步是最佳停止时间,也不能直接断言继续训练一定会更好。能够确认的是:在已经观察到的训练范围内,全量数据组保留了早期收益。

7. 为什么数据量会改变训练曲线

五组都处理了 256 × 64 = 16384 条次训练样本,但每条样本平均被抽到的次数差异很大:128 条组约 128 次,256 条组 64 次,512 条组 32 次,1024 条组 16 次,全量组约 2.19 次。

这不是严格逐条遍历的 epoch,而是随机抽样的期望次数;全量组也不保证每一条样本都出现过。

相同更新次数下,小数据组反复拟合同一小批回答,全量组则接触更丰富的数据。这与前四组后期退化、全量组较稳定的结果相符。不过,实验同时改变了数据覆盖范围和平均重复次数,不能把两者的作用完全拆开,也不是严格的等 token 或等 FLOPs 对比。

8. 用其他指标交叉检查

以下是 step 256 的结果。训练指标是最后一个更新步所统计的值,不是全程均值,也不是对整个训练集重新计算的损失。

最终指标12825651210247473
准确率49.81%55.50%56.03%61.33%74.91%
格式合格率98.64%99.39%99.39%99.85%99.55%
训练 loss0.13730.05620.518811.375043.4434
回答熵0.03580.02500.07570.19550.5107
全局熵1.07670.79440.86080.78251.2410
平均长度/字符275.7258.0281.8273.5259.7
正确回答长度/字符239.5228.8238.9245.5232.2
错误回答长度/字符311.7294.4336.5317.9341.6

loss 更大,为什么效果更好

全量组最终 loss 为 43.4434,远高于小数据组,但测试准确率最高。它没有把训练回答拟合到小数据组那样的程度,却在未见题目上表现更好。loss 还受回答长度和当前抽样 batch 影响,不能直接按跨组 loss 大小评选最佳模型,更不能把“让 loss 变大”当成优化目标。

格式已经基本学会,剩下主要是答案错误

五组最终格式合格率都超过 98%。全量组的 1319 题中,1313 题格式合格,988 题答对,325 题格式合格但答案错误,另有 6 题格式不合格。此时继续单纯强化标签格式,不足以解决主要错误。

初始准确率也受格式门槛影响。保留的初始前 100 题样例中,55 题格式合格、16 题答对;这是局部样本统计,不能直接当作全量格式率。因此,从 20.85% 到约 71% 的初期跃升,不能全部解释成数学能力提高。

错误回答普遍更长,但不能据此推断因果

五组错误回答平均长度均高于正确回答。可能是难题同时导致长推理与错误,也可能存在重复或绕路。需要检查具体样例才能区分;直接限制回答长度未必提高准确率。

低回答熵不是高可靠性的保证

小数据组最终回答熵很低,但测试表现较差,符合对训练上下文高度确定而泛化不佳的现象。全量组熵较高,也不意味着“熵越高越好”;这里只能结合训练暴露次数、loss 和评估曲线共同解释。

9. 实验边界和下一步

  1. 当前评估文件实际是测试集。本文可以回顾其训练曲线,但下一轮应单独划分验证集选择停止点,再对测试集做最终评估。
  2. 当前只在训练结束保存模型,没有保存每次评估的最佳权重。日志中的早期峰值不能直接恢复成 checkpoint。
  3. 每个规模只有一次运行,没有多随机种子均值、误差区间或全量逐题配对检验。512 与 1024 的峰值差约 5 道题,不足以认定前者稳定更优。
  4. 后续显式设置训练随机种子,并保存抽样索引;如要研究纯粹的数据规模效应,可预先构造嵌套子集。
  5. 同时记录 token 平均 NLL、实际生成 token 数、截断比例,并按错误类型检查样例,提升指标解释性。
  6. 优先补齐最佳 checkpoint 保存和独立验证集,再决定是否扩展全量组训练时长。

本次实验表明,少量数据可以迅速改善模型在当前提示与评分规则下的表现,但反复训练同一小批数据容易使收益退化。全量 7473 条数据让高分维持到更晚阶段,并获得本次实验最高的最终准确率。选择 SFT 配置时,数据规模、训练时长和验证效果应放在一起看。

附件下载

附:代码和结果索引

评分器是带有上游署名和 Apache-2.0 声明的依赖代码,不应作为个人原创算法发布。本文中的实验结果来自运行产物,分析文字由本次整理辅助生成并供作者审阅。


编辑页面
分享这篇文章:

上一篇
博客搭建完成