TT Lab
开始
学习 学习路径 课程

MiniMind — 亲手从头到尾训练一个小型语言模型

用 MiniMind 的训练循环预训练 300 步并读懂损失曲线

在 TT Lab 中继续学习

目标

亲手写一个训练循环,把 MiniMind train_pretrain.py 的骨架(AdamW、MiniMind 余弦学习率、梯度裁剪 1.0)缩小到 CPU 的规模,固定随机种子确认可复现之后,训练 300 步。用数字读懂损失曲线,并看看学习率过大时会毁掉什么。

为什么重要

训练循环的核心只有两行,但决定结果的是它周围的决定——学习率及其安排、梯度裁剪、随机种子、测量验证损失的周期。这些决定一旦错了,要跑几个小时之后才会知道。所以需要养成习惯:先用几分钟的训练确认曲线的形状。 损失曲线也会显示模型先学到什么。最初的几十步学的是令牌频率,之后学的是依赖上下文的规则。与不看上下文、只知道频率的模型的损失(一元语法熵)对比,就能知道曲线的哪一部分是“开始使用上下文”的区间。

步骤

  1. 写 /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。
  2. 训练 300 步(批次 8、长度 128、lr 3e-3),把权重保存为 /root/mm/pre/ckpt.pth,把记录保存为 /root/mm/pre/log.csv。验证损失必须降到 1.6 以下。
  3. 从 log.csv 中提取曲线的形状,写入 /root/mm/pre/curve.json。
  4. 把训练数组的一元语法熵写入 /root/mm/pre/unigram.json。
  5. 用 lr 0.05 运行 50 步,留下 /root/mm/pre/log_high.csv。
  6. 用 ckpt.pth 对“Garam 村的”之后用 16 个令牌的贪心解码做续写,保存到 /root/mm/pre/sample.txt。
  7. 在 /root/mm/pre/report.md 中写 ## 손실 곡선、## 학습률、## 이어 쓰기 三节(三个标题为韩文,依次意为“损失曲线”“学习率”“续写”),并以数字放入最后的验证损失和一元语法熵。

参考

相同的随机种子,相同的损失

编写 /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 步里看到的东西。