在我们自己的模型上,把块里的零件一个个拆掉、换掉,会怎样?
本课一问:在我们自己的模型上,把块里的零件一个个拆掉、换掉,会怎样?
这一课先不碰:为什么 Post-LN 离不开 warmup(在我们这里表现为卡在字频,C8 解释);更大的模型上结论是否不变(C6 规模、C14 评测)。
② 从已有出发
前四课每个零件都有一个「朴素方案 → 失败 → 改进」,但那些失败大多是论文里的数字或随机初始化网络上的现象。这一课换成我们自己从 0 训练的模型:4 层、d = 256、8 头、词表 2,048、上下文 256,在《三國志演義》上训练 400 步,参数 3,688,704;基线 val loss 从 7.6852 降到 4.6263(README 记录的真实日志)。
这一课的探针不用贯穿例句,原因是:「小明把苹果给了小王」是简体现代汉语,项目语料是繁体古白话,它在项目分词器下切成 13 个 token,「苹」「给」两个字不在词表里、被拆成字节(README 的已知局限)。拿它去看神经元,看到的主要是「生僻字节」而不是模型学到的东西,所以探针用验证集里的真实原文。
我们的基线是演进图里「Llama 式」那一代(紫框那一列):Pre-RMSNorm、SwiGLU、普通残差;对照组把某一个零件退回更早的一代,或者干脆拿掉。
③ 先自己设计:怎么比才算数?
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
| 配置 | 最终 val loss(跑一次) |
|---|---|
| 基线 | 4.6263 |
| 某个改动 | 比基线低 0.005 → 「更好」? |
同一台机器上吞吐受其他进程影响:同一个模型 400 步,训练时间 22 / 29 / 38 秒。
项目 MILESTONES.md 的四条约定:
- 只改一项:数据、种子、步数、评估窗口都与基线相同,
exports/index.json自动列出改了什么。 - 同一把尺子:val loss 在验证集固定的 228 个 256-token 窗口上算。
- 噪声底线:MILESTONES 原写差值小于 0.01 不下结论;C14 用同配置 5 次重跑把它更新为 3σ ≈ 0.020(极差 0.016)。小于它就不下结论,要换 3 个种子取均值。
- 比吞吐的实验单独跑:tokens/s 取 3 次独立运行的中位数。
再加本章特有的一条:比较 FFN 时参数必须对齐(SwiGLU 688 对 GELU 1024,单层差 0.78%;纯注意力用 12 层对齐 FFN 的参数,差 0.42%)。
④ 回放真实实验
每一张小图都画「基线 + 这一组的改动」,所有小图共用同一横纵坐标;表里 Δ 的绝对值小于 0.02(C14 的 3σ)会标「在噪声内」。把指标切到 grad_norm,再看 Post-LN 那一组的开头。
⑤ 读图:把数字变成决策
读完上面的图表,把下面这张表填完——每一格都要写数字,而且要写这个数字来自哪张图、是否超出噪声底线:
| 要拍板的决策 | 读哪里 | 你要写下的数字 | 本章哪一课讲原理 |
|---|---|---|---|
| 要不要残差 | 「残差有 / 无」组;逐块梯度图 | 4 层与 12 层各自的 Δ val loss;初始化时底层块与顶层块的梯度之比 | L2 |
| FFN 能不能省 | 「FFN 有 / 无」组 | 同深度去 FFN 的 Δ;参数对齐的 12 层纯注意力的 Δ | L1 |
| Pre 还是 Post | 「归一化放在哪」组,指标切 grad_norm | 首 100 步 grad_norm 最大值;不 warmup、lr 1e-2 时谁发散、在第几步 | L3 → C8 |
| LayerNorm 还是 RMSNorm | 「LayerNorm / RMSNorm」组;算子微基准 | Δ 是否在噪声内;tokens/s 中位数之差;算子耗时 | L3 |
| GELU 还是 SwiGLU | 「GELU / SwiGLU」组 | 参数差(应 < 1%)与 Δ | L4 |
🔍 填完再对答案(2026-09-26 导出的数据)
残差:去掉后 4 层 6.627(+2.000),12 层 6.627(对照 12 层有残差 4.732,+1.895),都贴着「只按字频猜」的 6.625;训练结束时 4 层无残差模型底层块梯度只有顶层的 3.7 × 10⁻⁷。FFN:同深度去掉 5.205(+0.578),参数对齐的 12 层纯注意力 5.330(+0.704)。Pre / Post:Post-LN 首 100 步 grad_norm 最大 12.8(基线 3.2),停在 6.627;不 warmup、lr 1e-2 时 Post-LN 6.626、Pre-Norm 5.680,两者都没有 NaN。LayerNorm 对 RMSNorm:+0.0003,在噪声内;单独运行 3 次的吞吐中位数 198,300 对 200,900 tokens/s,算子微基准 LayerNorm 0.667 ms、RMSNorm 0.792 ms。GELU 对 SwiGLU:单种子 +0.015,在重跑噪声之内(C14:5 次重跑极差 0.016);补到 5 个种子后 +0.044(4.6657 ± 0.0206 对 4.6218 ± 0.0060),超过 3σ 阈值 0.020,可以下结论。数据更新后以上面组件显示的为准。试三件事:① 先找出所有标着「在噪声内」的对照——这些不能写成「A 比 B 好」;② 看「FFN 有 / 无」组:同深度去掉 FFN,参数少了一半多,变差是意料之中;真正回答 L1 问题的是参数对齐的那一条;③ 在「归一化放在哪」组把指标切到 grad_norm,只看前 100 步,再对照 C8 的解释。
⑥ ⚠ 常见误解
「val loss 低 0.005,所以 A 更好」✗ —— 同配置同种子重跑 5 次,极差就有 0.016(C14 实验课);差值小于 3σ ≈ 0.020 时只能说「这个规模下看不出区别」。
「tokens/s 跑一次就能比」✗ —— 同一个模型三次训练时间 22 / 29 / 38 秒。要比吞吐,单独运行、多次取中位数,最好再看算子级的微基准。
「在 3.7M 参数的模型上成立,在 27B 上也成立」✗ —— 这些实验只能提供「实验验证」这一层证据,而且只在「3.7M 参数 + 0.5M token 语料」这个规模上成立;Pre-LN 的优势随深度变大才更明显,残差、归一化的差距在 4 层上可能很小。
⑦ 证据三问
- 它解决什么问题? 用我们自己的真实日志检验前四课的结论,而不是只相信论文。
- 数学上为什么可能有效? 前四课已经给了各自的推导(残差的恒等路径、归一化的缩放不变性、门控的乘法交互)。
- 实验上为什么最后选了它? 这一课本身就是实验 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条04-实践项目 的对照实验:每组只改一项,数据、种子、步数、评估窗口相同;噪声底线按 C14:同配置 5 次重跑极差 0.016,3σ ≈ 0.020;结论只在 3.7M 参数、0.5M token 的规模上成立。 出处本章项目推进 ↗ 。和论文结论对不上的地方最值得记下来——它们往往说明规模、数据或训练步数不够,正是 C6「规模」要回答的问题。
⑧ 一句话
只改一项、对齐参数、看清噪声底线,再下结论——这三条比任何一个零件都重要。
⑨ 下一个缺口
块的结构拍板了。可开着同样的块,学习率取 3e-3 还是 1e-2、要不要 warmup、用 AdamW 还是 Muon,结果会差很多——Post-LN 不带 warmup 的那一组已经预告了这一点。下一章:C5 优化;Post-LN 为什么需要 warmup,C8 兑现。
⑩ 你可能会问
- 「想自己重跑一遍?」 —— 可以。在
04-实践项目/下按MILESTONES.mdC4 一节:python3.12 -m minillm.train --run-name c4_postln --set model.norm_pos=post,其余对照组照此只改一项,跑完用python3.12 -m minillm.export导出;本页读取的汇总文件格式见本章设计卡的「项目实验数据约定」。 - 「为什么基线只训 400 步?」 —— 语料只有 50 万 token,400 步已是 6.5 个 epoch;再训下去 val loss 反而上升(配套 run
c6_overfit_1500steps:第 300 步最低 4.7713,之后升到 6.9065)。这是 C6、C7「数据」的话题。