MiniMind 那次训练,我先把处理后的样本完整打印了一遍。这个动作很笨,却比盯着 Loss 有用。
终端里的 Loss 会下降,数据链路却可能接错。回复起点偏了、结束标记被截掉,或者训练和推理用了两套模板,曲线照样可以很平滑。那次练习后来保留的重点,便是从原文一路查到模型输出。
文本 → Tokenizer → Token ID → 预训练 → 基础模型 → 指令数据 → SFT → 固定问题测试
先看送进模型的东西
训练以前先随机抽几十条原文,直接看内容,不先看统计图。乱码和控制字符、网页残片、重复模板与大段重复、断在半句的截断、丢掉上下文的短样本,都比词频图更值得先看。训练集和测试集里是否混进同一段,也要趁这时查掉。模型不会嫌教材脏,只会照着学;数据重复得越整齐,它记得越牢,格式长期不一致,它也只会把不一致当成规律。
Tokenizer 也要做一次往返检查。中文、英文、数字、标点、换行和代码分别 encode,再 decode 回来。长度突然变得离谱、空格改变、特殊符号丢失,应该在训练开跑前就处理掉。模型配置、特殊 Token ID 和推理时加载的 Tokenizer 必须来自同一套文件。
Loss 到底算在哪儿
预训练做的是下一个 Token 预测:
- 输入:x₁ … xₙ₋₁
- 标签:x₂ … xₙ
SFT 还要处理对话边界。用户问题需要留在上下文里,但通常不参与回答部分的 Loss;不参与的位置要在标签里正确忽略。这里最省时间的做法,是把完整文本、Token 和标签并排打印出来:哪些位置被忽略,回复从哪里开始计算,结束标记还在不在,截断究竟切掉了问题还是答案。
训练模板和推理模板如果长得不一样,模型可能已经学过答案,却一直等不到熟悉的开场。
别急着开长任务
正式训练前,先用最小规模把读取、分词、前向、反向和参数更新走通。随后拿很少的数据故意过拟合;连眼前这点内容都记不住,就继续查标签、学习率和模型连接,不必急着扩大数据量。
Checkpoint 也要真的恢复一次。模型参数、优化器、Step 和学习率调度都应该接着走,固定输入的输出不能出现无法解释的跳变。最后再拿已知对话跑推理,核对角色分隔符、回复起点和停止条件。
这些短测试不负责证明模型好,只负责避免长任务跑完以后才发现线路接错。
参数和环境写在一起
有效 Batch = 单卡微批量 × 梯度累积步数 × GPU 数量
显存不够时,先减单卡微批量,再考虑用梯度累积补回有效 Batch。序列长度也要一起看,它拉高的不只是输入长度,激活和注意力开销都会跟着上来。微批量、累积步数和序列长度必须一起记录。显存里除了权重,还有梯度、优化器状态、前向激活和临时张量。只算参数量乘精度,只能判断权重能不能放下,不能估出整场训练。
学习率太大,Loss 会乱跳甚至出现 NaN;太小,又可能长时间没有明显变化。小数据反复跑太久,最后看到的也可能只是背答案。
曲线不会替你诊断
- 一开始就是 NaN,先查异常样本、精度设置和学习率。
- 长期不降,先确认标签有没有错位、参数是否真的在更新。
- 训练集继续下降而验证集变差,多半已经过拟合,或两边分布不同。
- 曲线正常但输出乱码,回查 Tokenizer、权重和模板。
- 回答开始复读,检查重复数据、训练轮次与解码设置。
- 回答看着很顺却总答非所问,训练目标可能没有覆盖指令理解。
若交叉熵使用自然对数,PPL 可以写成 exp(Loss)。它只适合在同一 Tokenizer、同一数据处理和同一测试集下比较,跨实验随意排座次没有意义。
固定问题要保留原始输出
调参以前就留出一组不参与训练的问题,覆盖句子完整性、指令跟随、基础常识、目标任务、长短输入、容易复读的问法,以及模型本来就不知道的内容。每轮沿用相同模板和生成参数,原样保存输出。只留下最好看的回答,下一轮就没法判断到底哪里变好了。
训练集用来更新参数,验证集用来选 Checkpoint,测试集留到最后。测试题一旦被反复看着改数据,就已经不再是测试集。
后来怎么记
实验日志里会同时留下数据版本、Tokenizer、随机种子、有效 Batch、序列长度、学习率、总 Step、Checkpoint、显存峰值和耗时,并写清这一轮究竟改了什么。
内容停在 2025 年 12 月那次 MiniMind 练习。后来再开训练任务,我会先保存样本打印和固定问题输出;曲线出了问题,至少知道该从哪一段往回查。