用 MiniMind 的训练循环预训练 300 步并读懂损失曲线
目标
亲手写一个训练循环,把 MiniMind train_pretrain.py 的骨架(AdamW、MiniMind 余弦学习率、梯度裁剪 1.0)缩小到 CPU 的规模,固定随机种子确认可复现之后,训练 300 步。用数字读懂损失曲线,并看看学习率过大时会毁掉什么。
为什么重要
训练循环的核心只有两行,但决定结果的是它周围的决定——学习率及其安排、梯度裁剪、随机种子、测量验证损失的周期。这些决定一旦错了,要跑几个小时之后才会知道。所以需要养成习惯:先用几分钟的训练确认曲线的形状。 损失曲线也会显示模型先学到什么。最初的几十步学的是令牌频率,之后学的是依赖上下文的规则。与不看上下文、只知道频率的模型的损失(一元语法熵)对比,就能知道曲线的哪一部分是“开始使用上下文”的区间。
步骤
- 写 /root/mm/pre/train.py(参数
--steps、--lr、--seed、--eval-every、--out、--log),用相同的随机种子把 20 步运行两次,留下 /root/mm/pre/log_a.csv 和 /root/mm/pre/log_b.csv。 - 训练 300 步(批次 8、长度 128、lr 3e-3),把权重保存为 /root/mm/pre/ckpt.pth,把记录保存为 /root/mm/pre/log.csv。验证损失必须降到 1.6 以下。
- 从
log.csv中提取曲线的形状,写入 /root/mm/pre/curve.json。 - 把训练数组的一元语法熵写入 /root/mm/pre/unigram.json。
- 用 lr 0.05 运行 50 步,留下 /root/mm/pre/log_high.csv。
- 用
ckpt.pth对“Garam 村的”之后用 16 个令牌的贪心解码做续写,保存到 /root/mm/pre/sample.txt。 - 在 /root/mm/pre/report.md 中写
## 손실 곡선、## 학습률、## 이어 쓰기三节(三个标题为韩文,依次意为“损失曲线”“学习率”“续写”),并以数字放入最后的验证损失和一元语法熵。
参考
- 记录的列是
step,train_loss,val_loss,lr。没有测验证损失的行,val_loss留空。验证损失用mmkit.lm_loss(model, val, n_batches=4)来测,就与评分器的方法相同。 - 学习率由
mmkit.minimind_lr(step, 전체 걸음, lr)(占位符依次为当前步、总步数与学习率)返回与 MiniMindget_lr相同的公式。步数从 1 开始数。 - 300 步在节点上大约 20 秒(Pod 有 2 个 CPU)。如果 Pod 调度到繁忙的节点上,可能会慢好几倍。
- 常见错误:只对模型初始化设随机种子,而没有对抽取批次设;用
model.eval()做完验证后没有用model.train()切换回来;像torch.save(model)这样把整个模型对象保存下来(请保存state_dict())。 - 原文:train_pretrain.py · trainer_utils.py · PyTorch AdamW · clip_grad_norm_
相同的随机种子,相同的损失
编写 /root/mm/pre/train.py。用 /opt/mm/ref/config.json 创建模型,从 /opt/mm/ref/train.npy 中每次在随机位置抽取 128 个令牌、凑成 8 个一批来训练,每一步把 step,train_loss,val_loss,lr 记录到 --log 路径的 CSV 中。用 --steps 20 运行两次,生成 /root/mm/pre/log_a.csv 和 /root/mm/pre/log_b.csv。
要复现,两种随机数都得固定——权重初始化(在创建模型 之前 调用 mmkit.seed_all(시드),占位符为随机种子),以及抽取批次(把 torch.Generator().manual_seed(시드),占位符为随机种子,传给 randint)。第一步的损失应该在 ln 1024 ≈ 6.93 附近。
300 步预训练
用 train.py 训练 300 步(批次 8、长度 128、lr 3e-3、随机种子 42,每 50 步验证一次),把权重保存为 /root/mm/pre/ckpt.pth(model.state_dict()),把记录保存为 /root/mm/pre/log.csv。
学习率用 MiniMind 的余弦(mmkit.minimind_lr),第一步应为 3e-3,最后应为 3e-4。评分器会加载 ckpt.pth 重新测量验证损失,检查是否低于 1.6,以及是否与记录中最后一个 val_loss 相同。
用数字表示曲线的形状
从 log.csv 中取出第一个损失(first_loss)、训练损失第一次低于 3 的那一步(step_below_3)、最后的训练损失(final_train)、最后以及最低的验证损失(final_val、best_val),写入 /root/mm/pre/curve.json。
看看急速下降的区间在第几步结束。在那之后,下降同样的量要多花很多步。
只知道频率的模型的损失
用 /opt/mm/ref/train.npy 的令牌频率计算一元语法熵(−Σ p·ln p,单位为奈特),以 unigram_entropy 写入 /root/mm/pre/unigram.json。
完全不看上下文、始终给出同一个分布(令牌频率)的模型,它的交叉熵就是这个值。如果曲线降到这个值以下,就意味着模型开始根据前面的令牌缩小下一个令牌的范围。numpy.bincount 很快。
学习率太大会怎样
用 --lr 0.05 --steps 50 运行 train.py,留下 /root/mm/pre/log_high.csv。因为随机种子相同,可以与第 2 步记录的第 41–50 步对比。
学习率大,最初几步似乎下降得很快,但猛地跳起来一次之后,就贴在某个值上降不下来。梯度裁剪(1.0)也拦不住步子的大小——步子有多大由 lr 决定。
用学到的东西来续写
加载 ckpt.pth,对 [bos] + '가람 마을의'(韩文,意为“Garam 村的”)之后用 16 个令牌的贪心解码(do_sample=False, top_k=0, top_p=1.0, eos_token_id=2)做续写,把去掉 bos 的完整文本保存到 /root/mm/pre/sample.txt。
用 mmkit.load_model(경로)(占位符为文件路径)加载,再调用 MiniMind 的 model.generate 即可。贪心生成在权重相同时总是给出同样的文本,所以评分器会用你的 ckpt.pth 重新生成并对比。只做了预训练的模型不会回答问题,而是像语料一样续写。
读曲线的记录
在 /root/mm/pre/report.md 中写 ## 손실 곡선、## 학습률、## 이어 쓰기 三节(三个标题为韩文,依次意为“损失曲线”“学习率”“续写”),并以数字放入第 3 步的 final_val 和第 4 步的 unigram_entropy。
以一元语法熵为基准,用一行写出曲线的哪一段学的是频率、哪一段学的是上下文。学习率一节写第 5 步里看到的东西。