N-gram Gap 机制指南
本文档解释 n-gram gap 实验中的核心概念:什么是 table、gate、shell、optimizer grouping,它们如何组合产生 train/val gap,以及关键实验数据。目标读者是刚接手项目的研究者。
术语表
以下是本文档中频繁出现的核心术语。每个术语首次在正文中出现时用 加粗 标记,在此处可快速查阅定义。
| 术语 | 定义 |
|---|---|
| n-gram table / VE | 一张巨大的查找表(lookup table),用几个连续 token 的 hash 值做 key 来查表。等价于 "value embedding"(VE)。包含 bigram(2-gram)和 trigram(3-gram)两种。 |
| gate(门控) | 控制 n-gram table 输出注入 attention 强度的机制:2×sigmoid(Linear(x)),输出范围 [0, 2]。gate 初始化为 1.0(中性),训练中模型自主调节。 |
| shell(外壳) | 指 transformer block 的架构实现。本文档涉及两种:nanoGPT shell(经典 nanoGPT 架构)和 current shell(从 current/GPT 架构移植,含 x0 residual、resid_lambda、split QKV)。gap 仅在 current shell 下出现。 |
| gap / gap onset | train loss 阶梯下降同时 val loss 上升的现象。"gap onset"(也称 "fork")指 gap 开始出现的具体训练步数。 |
| hit / hit count | 某个 n-gram context(如 trigram 的 2-token 前缀)在整个训练集中总共出现的次数。是数据的静态属性,不随训练变化。在 §9 中用于按频率分组分析 loss。 |
| novel / novel bucket | 指 train 中从未出现过的 context(hit count = 0)。在 §9 的 hit count 分析中,novel bucket 是 val 翘起的最大单一来源。 |
| baseline_current / B.1 | 本文档 §2 现象图、§7.9 norm 图、§9 频次分解统一使用 baseline_current(current shell + current-style grouping + bigram+trigram RMSProp,seed42,1000 step,vocab=8192,n_layer=8;epoch 边界 [338, 687],epoch3 gap 从 ~0.5 扩大到 1.33)。旧的 B.1(20260725e_nano_rmsprop_fixv3,nanogpt_original,gap 0.688)仍作为 §10 干预实验的 nanogpt_original 基线。 |
| fixv2 / fixv3 | 实验方法论的两个迭代版本。fixv2 使用 epoch 冻结的在线累积计数器,fixv3 替换为完全离线预计算的频率表。本文档当前所有数据均来自 fixv3(详见 agent-experiment-log.html §Gotchas G7)。 |
| obcurves | observable curves 的缩写。训练过程中记录的结构化指标数据(gate norm、table content RMS、hit bucket loss 等),存储在 observable_curves.obcurves.json 中。 |
| context / context_hash | n-gram 的条件前缀。如 trigram 的 context = (token[i-2], token[i-1]),bigram 的 context = (token[i-1])。context_hash 是 hash 后的整数标识。 |
| gated_value_rms / raw_value_rms | raw_value_rms = n-gram table 查出的 value 在 gate 之前的 RMS;gated_value_rms = gate × table value 的 RMS,即实际注入 attention value head 的量。 |
| optimizer grouping | 参数分组与 LR 分配策略。nanoGPT-style grouping 给 n-gram table LR=0.004,current-style grouping 给 LR=0.6(差 150 倍),这是 gap 出现的必要条件。 |
| epoch boundary | 训练数据遍历完一轮的分界步数。当前数据集:epoch 1→2 在 step 337,epoch 2→3 在 step 686,epoch 3→4(moredata)在 step 1384/2082。 |
| warmdown | LR schedule 的衰减阶段。当前实验在 step 750 附近 lrm_adam 从 0.33 降到 0.05,触发 train loss 的同步坍塌(memorize 突变)。 |
| RMSProp(table optimizer) | 用于 n-gram table 的优化器。无 first moment(momentum),β₂=0.999 使二阶矩 v 能跨 epoch 持久化(半衰期 ~693 步 ≈ 2 个 epoch)。与 AdamW 的关键区别在于写入方向 = 当前梯度,不被历史 momentum 稀释。 |
| Muon(matrix optimizer) | 用于 transformer 矩阵参数的优化器。在 nanogpt_original shell 下,table RMSProp + matrix Muon 的组合产生 delayed gap(onset 在 epoch2 而非 epoch3)。 |
| collision | hash 冲突:不同的 n-gram context 被映射到同一个 table row。实测 batch 内 bigram collision ~20%,trigram ~8%。每层用不同 hash prime 降低有效 collision。 |
| per-token loss / total contribution | per-token loss = 该 bucket 内每个 token 的平均 loss;total contribution = frac × loss(该 bucket 对总 loss 的贡献,考虑了 token 占比)。§9 的交互式图表可切换两种视角。 |
1.1 什么是 n-gram table
n-gram table 就是一张巨大的查表(lookup table),和 token embedding 没有本质区别。区别在于:token embedding 用 token id 做 key,n-gram table 用几个连续 token 组成的 context 做 key。
table 的规模
| table 类型 | context | 行数 | 每行维度 | hash 方式 |
|---|---|---|---|---|
| unigram | 单个 token | 32,768 | 768 | 直接用 token id |
| bigram | 前一个 + 当前 token | 2,097,152 (32K × 64) | 64 × 2 = 128 | 乘法 XOR hash |
| trigram | 前两个 + 当前 token | 2,097,152 | 64 × 2 = 128 | 乘法 XOR hash |
| fourgram | 前三个 + 当前 token | 2,097,152 | 64 × 2 = 128 | 乘法 XOR hash |
bigram/trigram/fourgram 的行数都是 vocab_size × 64 = 32768 × 64 = 2,097,152。hash 把 n 个 token 映射到这个范围内的一个整数。每层用不同的 hash prime,使不同层的同一 context 映射到不同行。
1.2 Hash 机制详解
hash 的作用是把 n 个 token id 压缩成一个 row 号。具体实现是乘法 + XOR + 取模,全部在 GPU 上以 tensor 整数运算完成。
每层使用不同的 prime 组(来自 MurmurHash/FNV/golden-ratio 家族),保证不同层的同一 context 映射到完全不同的行。K=2 multi-hash 是指每个 n-gram 查两次独立 hash,各取半个维度(64 维),拼成完整的 128 维。
collision 的实测数据
| 指标 | bigram | trigram | 含义 |
|---|---|---|---|
| batch 内 unique_frac | 0.80 | 0.92 | trigram 的 collision 更低 (92% unique vs 80%) |
| within_batch_repeat | 20% | 8% | 同一 batch 内有多少 token 共享 row |
| entropy_norm | 0.983 | 0.993 | trigram 的 row 分布更均匀 |
| rows_ever_seen (step 1000) | 97% | 99% | 到训练结束时大部分 row 都被访问过 |
| rows_epoch_lag | 10% | 1.5% | epoch 间同位置 row overlap 很低 |
关键发现:epoch 间同一文本映射到的 row 几乎完全不同(trigram 只有 1.5% overlap)。gap 不是来自"精确同 row 重复读",而是来自 row 内容的统计积累。
1.3 Gate 机制:table 如何注入到模型
n-gram table 查出的 value 不会直接加到模型输出上——它经过一个 input-dependent gate 控制注入强度。gate 本身是一个小 linear layer + sigmoid。
gate 初始化为全 0 权重,所以 2×sigmoid(0) = 1.0——训练初期 gate 全开(中性),之后模型自己学习该在什么时候、什么位置放大或抑制 n-gram value 的注入。
1.4 Shell 是什么:nanoGPT vs current shell
项目中有两类模型架构,称为 "shell"(外壳)。它们的区别在于 transformer block 的内部结构。
经典 nanoGPT 架构。标准 LayerNorm、标准 attention、标准 MLP。
即使加 n-gram table + RMSProp + 高 LR,也不产生 gap。
从 current/GPT 架构移植的 block。包含 x0 residual、resid_lambda、split QKV 等额外结构。
n-gram table 注入到 attention value stream 的方式和 nanoGPT shell 不同。在 current shell 下,gap 出现。
关键区别不在 table 本身(两者都可以有相同的 n-gram table),而在于 reader 的传输路径:current shell 的 attention split QKV + 残差路径使 n-gram value 在后层 residual 中被更有效地放大。这是为什么 nanoGPT shell 即使加上所有组件仍然无 gap,而切到 current shell 后 gap 出现。
1.5 Optimizer grouping:两个独立的概念
项目中 "optimizer" 相关的配置有两个独立维度,经常被混淆。这里明确区分。
维度一:n-gram table 用什么 optimizer
由 nanogpt_ngram_optimizer 控制,决定 table 参数的更新算法:
| 取值 | unigram table | bigram/trigram table | 含义 |
|---|---|---|---|
"adamw" | AdamW | AdamW | 全部用 AdamW(有 momentum) |
"rmsprop" | RMSProp | RMSProp | 全部用 RMSProp(无 momentum) |
"mixed" | AdamW | RMSProp | unigram 用 AdamW,high-order 用 RMSProp |
维度二:参数分组与 LR 分配
由 optimizer_grouping 控制,决定每类参数的学习率:
| 参数类型 | nanoGPT-style grouping | current-style grouping |
|---|---|---|
| embedding (wte + wpe) | 0.004 | 0.6 |
| lm_head (unembedding) | 0.004 | 0.004 |
| unigram VE | 0.004 | 0.6 |
| bigram/trigram VE | 0.004 | 0.6 |
| norm params | 0.004 | 0.004 |
| matrix (Muon) | 0.04 | 0.04 |
RMSProp 对比 AdamW:为什么对 sparse table 更合适
2. Gap 现象与因果链
以下为 baseline_current 的真实 loss 记录(current shell + current-style grouping + bigram+trigram RMSProp,seed42,1000 step,vocab=8192,n_layer=8)——与 §7.9 / §9 的 norm / 频次分解同一 run。
关键现象:epoch2 时 train 与 val 基本贴合(gap ~0.04);进入 epoch3 后 train 持续阶梯下降、val 停滞并回升,gap 从 ~0.5 扩大到 1.33。图中曲线直接来自真实训练记录,与下方 §7.9 / §9 同一 run。
| step | train loss | val loss | gap | 现象 |
|---|---|---|---|---|
| 338 | 4.854 | 5.038 | 0.184 | epoch1→2 边界,gap 极小 |
| 500 | 4.437 | 4.476 | 0.040 | epoch2 中段,train 与 val 贴合 |
| 687 | 3.911 | 4.424 | 0.513 | epoch2→3 边界,gap 开始扩大 |
| 800 | 3.718 | 4.377 | 0.659 | train 持续下降 |
| 900 | 3.596 | 4.362 | 0.766 | val 停滞,gap 扩大 |
| 1000 | 3.197 | 4.527 | 1.330 | epoch3 末,val 回升、gap 超过 1.3 |
因果链:gap 是如何产生的
以下因果链基于 baseline_current 的 observable 数据(与 §7.9 / §9 同一 run)。每个环节都有直接测量证据。
7. Table Norm × Gate:loss↔norm 时间关系
视图 A:loss↔norm。与 §9 的 loss↔频次分解同一实验(baseline_current,current shell)。
baseline_current
(current shell + current-style grouping + bigram+trigram RMSProp,seed42,1000 step,vocab=8192,n_layer=8,
epoch 边界 [338, 687],epoch3 gap 从 ~0.5 扩大到 1.33)——
与上方 §2 现象图同一 setting 系(current shell,gap 量级远大于 nanogpt_original),
且同时记录了逐 step 的 gate/table/gated norm与按频次分桶的 loss。
§7.9 Loss × n-gram Norm(双轴复合图)
左轴 train/val/gap loss,右轴 n-gram norm(gate_rms / table content RMS / gated value RMS)。 切 context(unigram/bigram/trigram)、切 loss 曲线、开关各 norm。判据:注入 norm (gated value RMS = gate × table)的增长时点是否与 gap 爆发对齐。
9. 频次分解分析
把 loss 按 n-gram context 的训练频次分解,是定位 gap 来源的核心观测量。
每个 n-gram context 的频次 = 它在单个 train epoch 内出现的精确次数,由训练前一次性离线扫描的
精确频率索引(GlobalNgramFrequencyIndex)给出,整个训练过程不变。
频次定义仅在 train 上;val 的 token 按"其 context 在 train 中的频次"归类到同一套桶,
因此 val 有 novel 桶(context 在 train 中从未出现),而 train 按定义没有 novel 桶。
桶的边界(精确频次,上下界闭区间):novel(0) / 1 / 2 / 3 / 4 / 5 / 6-10 / 11-20 / 21-50 / 51-100 / 101-200 / 201-500 / 501-1k / 1k-5k / 5k+。
低频段被细分为单值桶(1/2/3/4/5),便于直接观察最低频 context 的过拟合。
本章频次分解与 §7 的 loss↔norm 图用同一 run:baseline_current
(current shell + current-style grouping + bigram+trigram RMSProp,seed42,1000 step,vocab=8192,n_layer=8,
epoch3 gap 从 ~0.5 扩大到 1.33)——与 §2 现象图同一 setting 系,gap 量级远大于 nanogpt_original。
§7(视图 A)说"注入 norm 在增长、与 gap 时间对齐",本章(视图 B/C)补充"这些被放大的 pattern 主要让 novel / 低中频 context 过拟合"——同一现象的两个测量面。
9.1 频次分桶:train / val / gap 损失曲线
下面这张复合图把每个频次桶的 train loss、val loss、gap(val−train) 放进同一个面板,可切换:
- 曲线 (curve):train loss / val loss / train+val 同图(默认,train 蓝实线、val 红虚线)/ gap(val−train)。 按用户要求,所有 gap 图都自动带上 val 和 train 两条曲线,切换到 gap 视图时 0 线固定为坐标轴。
- 指标 (metric):per-token loss / 总贡献 = frac × loss(衡量该桶对总 loss/gap 的实际贡献量级)。
- context:bigram / trigram。每个桶可用复选框单独开关。
interactive/fig_gap_by_freq.html 单独查看/放大)。
bigram 与 trigram 均拥有完整频段(含 novel 与 1/2/3/4/5 单值低频桶);trigram 的 novel 桶在 val 上占 ~30%,是 val 翘起的主要来源。
- gap 是全局性的,低中频 bucket 最严重:所有非高频桶的 gap 在 epoch3 爆发。 切到 gap 视图可见低中频(1/2/3、6-10、11-20、21-50)bucket 的 gap 最大,高频(5k+)bucket 的 gap 最小。
- train 与 val 的分离在低频端最明显:切到 train+val 视图,低频桶的 train loss 显著低于 val(过拟合), 高频桶两者接近(QA pattern 天然可预测)。
- novel 桶是 val 翘起的最大单一来源:切到 总贡献 视图,novel 桶(仅 val,占 trigram val token 的 ~30%)贡献最大, 其次是 21-50/51-100 等中频桶;高频桶虽然 per-token gap 小,但若 token 占比高也会有一定的总贡献。
- trigram 的 novel 占比远高于 bigram:trigram 组合空间远大于 train 文本,~30% 的 val trigram context 从未在 train 出现; bigram 的 novel 占比仅 ~4%。这正是 trigram 对 gap 贡献更大的结构性原因。
9.2 命中频次分布(单 train epoch 内)
在解释 gap 之前,先看训练数据的频次结构本身。频次定义仅在 train:统计每个 context 在一个 train epoch 内出现的精确次数。 下面这张复合图分别统计 bigram / trigram,提供 token 占比视角与两个 split:
- 视图:token 占比(按出现次数加权)。
- split:train(按 train 频次)/ val(val token 按 train 频次归类,故 val 有 novel 桶)。
训练集的 context 频次呈典型幂律分布:train 侧大部分 token 来自少数高频 context, 但 val 侧有大量 context(尤其 trigram,~30%)在 train 中从未出现(novel)。gap 的成因正是这种分布与多 epoch 重复训练的交互:
- novel / 低频 context:模型在 train 中见过几次(低频)或没见过(novel), 到 epoch3 配合 gate 通道建立,模型把这些 context 的特定后续 token memorize 下来(train loss 坍塌), 但 val 中即使遇到相同 context,后续 token 也不同,模型"过度自信"反而出错(val loss 翘起)。trigram 的 ~30% val novel 是其 gap 尤为显著的结构性原因。
- 高频 context:多为 QA pattern(
? Answer:等),后续 token 天然可预测, train 和 val 的 loss 都相对低且接近,对 gap 贡献最小。 - 启示:用 BPE 把高频 token 组合 over-encode 成单个 token、避免对低频 context 建 hash 表, 可以从源头减少低频过拟合——这是后续干预实验的方向(见 agent-experiment-log.html)。
baseline_current)的 n-gram value tables 只启用 bigram 与 trigram
(ENABLE_UNIGRAM_VE=0),unigram 不参与训练,故无 unigram 频次分解。
如需 unigram 视角,需要一次 ENABLE_UNIGRAM_VE=1 且把 unigram 纳入频次分解的新 run;
服务器环境已就绪(见 实验日志),可在需要时补跑。
10. 扩展实验:干预验证(gap 是否能被消除)
§9 的频次分解定位了 gap 的来源(低中频 n-gram context 过拟合)。本章用两个干预实验直接验证: 若假设成立,干预应能消除/减弱 gap。
两个干预都在 20260725e_nano_rmsprop_fixv3 同款设置上跑(table RMSProp, nanogpt_original shell, vocab=8192, n_layer=8, seed42, 1000 step, 单 shard tr1),
并各加一个 current-shell 版本(同 rmsprop,对照 current-shell baseline baseline_current,即 §2/§7.9/§9 用的同款 setting)。
共 4 个干预 run,各自对照同 shell 的 baseline。所有 run 都到 step 1000、含 3 个 epoch(边界 step 337/686)。
注:§9 的频次分解现已统一到 baseline_current(current shell,gap 量级更大、现象更典型);本章干预在两个 shell 上都验证,nanogpt_original 用 B.1,current-shell 用 baseline_current 同款配置。
10.1 实验 4:per-epoch hash 错位
n-gram table 的 gap 根因是"同一 context 跨 epoch 重复查到同一行,table 在该行累积 train-specific pattern"。 若每个 epoch 用 epoch 号做 seed 重生成全部 hash primes,使同一 context 跨 epoch 落到完全不同的 row, 则 table 无法跨 epoch 累积 → 应该没有 gap。
实现:新增 env NGRAM_HASH_RESEED_PER_EPOCH=1;epoch 1 保留 canonical primes(与 baseline 一致),
epoch≥2 边界调用 _reseed_ngram_hash_primes(model, epoch_seed) 用 epoch 号重生成 bigram/trigram/fourgram 全部 primes。
reseed 只改 hash 映射、不改 table 行内容——正好实现"新 epoch 同一 context 查到另一行(未被该 context 训练污染)"。
10.2 实验 5:低频 context 强制 gate=0
§9 显示低中频 n-gram context 是 gap 的主要 driver。若用离线 train 频次表(ngram_global_frequency_v2,
一个"与训练完全解耦的独立统计者")判定低频区间(hit count 1-200),并在这些 context 上把 n-gram 注入 gate 强制置零,
则低频 context 不再注入 n-gram → gap 应减弱/消除。
实现:新增 env NGRAM_LOWFREQ_GATE_ZERO=1 + NGRAM_GLOBAL_FREQUENCY_CUSTOM_RANGES=bigram:1-200;trigram:1-200;
在模型主 forward 里用 GlobalNgramFrequencyIndex.mask_for_ranges 算出低频位置的 [B,T] 布尔 mask,
在 _add_value_residual 里把该位置的 value_gate 置零(gate=0 → n-gram value 不注入)。
mask 从 epoch 1 起全程生效(不是 step 338 后),目的是"杜绝"低频 context 的 n-gram 注入。
与已有 readout-mask(value 置零)的区别:本实验置零 gate(直接关闭注入通道,且 gate 仍收梯度但被强制为 0)。
10.3 结果:复合对比图
下面这张复合图把 4 个干预 run 与各自 baseline 的 gap/train/val 曲线放进同一面板,可切 shell 与曲线类型; 下方表格给出每 epoch 最大 gap 与 final gap 的数值对比。
interactive/fig_exp45_results.html)。
红虚线=baseline,青实线=exp4(hash reseed),绿实线=exp5(low-freq gate=0)。切 shell 看 nanogpt_original / current-shell。
10.4 结论
| run | epoch2 max gap | epoch3 max gap | final gap | vs baseline |
|---|---|---|---|---|
| B.1 baseline (nanogpt_original) | 0.383 | 0.688 | 0.688 | — |
| exp4 hash-reseed (nanogpt_original) | 0.257 | 0.371 | 0.047 | −93% |
| exp5 low-freq gate=0 (nanogpt_original) | 0.166 | 0.102 | 0.040 | −94% |
| baseline (current-shell) | 1.300 | 3.937 | 3.658 | — |
| exp4 hash-reseed (current-shell) | 0.307 | 0.351 | 0.252 | −93% |
| exp5 low-freq gate=0 (current-shell) | 0.543 | 1.523 | 1.523 | −58% |
- nanogpt_original:epoch3 max gap 从 0.688 → 0.371,final gap 从 0.688 → 0.047(−93%)。 epoch3 的 gap 爆发被完全压住——gap 不再随 epoch 增长。
- current-shell:epoch3 max gap 从 3.937 → 0.351(−91%),final gap 从 3.658 → 0.252(−93%)。 即使在 gap 大 5 倍的 current-shell 上,reseed 仍几乎完全消除 gap。
- 机理确认:gap 确实来自"同一 context 跨 epoch 重复查到同一行、table 在该行累积 train-specific pattern"。 一旦打断这个跨 epoch 的 row 对应关系,table 无法累积 → gap 消失。这是对 §7.9/§9 因果链的直接因果干预验证。
- nanogpt_original:epoch3 max gap 从 0.688 → 0.102,final gap → 0.040(−94%)。 与 exp4 几乎一样有效——说明在 nanogpt_original 上,低频 context 确实是 gap 的几乎全部来源。
- current-shell:epoch3 max gap 从 3.937 → 1.523(−61%),final gap → 1.523。 显著减弱但未消除——current-shell 的注入通道更强(split QKV + 残差路径),中高频 context 仍能过拟合, 仅挡低频不够。这与 §9"current-shell gap 量级大得多"一致。
- 启示:用 BPE 把高频 token 组合 over-encode、避免对低频 context 建 hash 表(等价于永久 gate=0), 是从源头消除低频过拟合的方向。exp5 用 gate=0 在 nanogpt_original 上已验证可行;current-shell 需配合更强的频率截断。
- exp4 的 reseed 只在 epoch≥2 边界触发(epoch1 保留 canonical primes 以与 baseline 对齐); reseed 不改 table 行内容,只改 hash→row 映射,所以是干净的因果干预(不改变模型容量、不冻结参数)。
- exp5 的频次表来自训练前一次离线扫描(
ngram_global_frequency_v2),与训练过程完全解耦—— 是"独立统计者"判定低频,不存在在线漂移。gate=0 是逐位置 mask(shape [B,T]),只对 bigram/trigram 分支生效,unigram 不受影响。
11. 精确重复次数 vs gap(exp6 细粒度分桶)
§9 的频次分桶用的是粗粒度区间(1, 2, 3, 4, 5, 6-10, 11-20, …)。本节在 同一 current-shell 设置(baseline_current 同款:current shell + bigram+trigram RMSProp, seed42,但延长到 1700 step / 5 epoch,让 gap 充分展开)下,把分桶细化到 精确到每个重复次数(0=novel, 1, 2, 3, …, 9, 10-14, 15-20, 21-30, 31-50, …, 5k+), 并在 epoch 2/3/4/5 各取一个横截面,观察 gap 如何随重复次数和训练进度演化。
频次定义与 §9 一致:每个 context 在单 train epoch 内的离线精确命中次数
(GlobalNgramFrequencyIndex,train-only 定义)。val token 按"其 context 在 train 中的频次"归类到同一套桶。
每个 epoch 的横截面取该 epoch 最后一个 probe step(epoch1=step337, epoch2=650, epoch3=1025, epoch4=1375, epoch5=1700)。
interactive/fig_exp6_freq_gap.html)。
切 epoch(2/3/4/5)· 切曲线(train / val / train+val / gap)· 切指标(per-token / 总贡献=frac×loss)· 切 context(bigram / trigram)。
11.1 关键发现
- epoch 2:gap 已经在低频桶(1-2 次重复)出现(bigram bucket "1" gap≈1.12,bucket "2" gap≈1.39), 但高频桶(5k+)gap≈0.02(几乎为 0)。此时 gap 几乎完全由低频驱动。
- epoch 3:低频桶 gap 急剧扩大(bigram bucket "1" gap≈3.57),中频(51-100)gap≈1.79, 高频(5k+)gap≈0.94——gap 开始向高频蔓延。
- epoch 4-5:所有频段都有大 gap(bigram e5: bucket "1" gap≈6.70,5k+ gap≈5.37)。 到 epoch 5,高频桶的 gap 也接近低频水平——说明多 epoch 重复训练后, table 在所有频段都累积了 train-specific pattern,不再只是低频问题。
- trigram novel bucket(val 中占 30%)始终是大 gap 来源: e5 trigram novel val loss=6.72(无 train 对照,因为 train 中按定义无 novel context), 总贡献 = 0.30 × 6.72 ≈ 2.0,是 trigram val 翘起的最大单一来源。
§9 的 1000-step run 到 epoch 3 末 final gap≈3.66;本 exp6 延长到 1700 step / 5 epoch, final gap(step 1700, train=1.38 / val=5.83)≈4.45——gap 仍在扩大。 精确分桶揭示了 "低频优先过拟合 → 多 epoch 后高频也沦陷" 的动态过程: epoch 2 时 gap 集中在 1-5 次重复的低频桶;到 epoch 5,1 次和 5k+ 次的 gap 差距缩小到 1.3(6.70 vs 5.37)。 这解释了为什么 exp5(仅挡低频 1-200)在 current-shell 上只能减 58%——多 epoch 后中高频也需要干预。
11.2 数值表(bigram,per-token loss)
| bucket | e2 train | e2 val | e2 gap | e3 gap | e4 gap | e5 gap |
|---|---|---|---|---|---|---|
| novel | train 无(按定义) | — | — | — | — | |
| 1 | 3.659 | 4.779 | 1.121 | 3.567 | 5.971 | 6.700 |
| 2 | 3.506 | 4.894 | 1.387 | 3.744 | 5.918 | 6.552 |
| 5 | 3.773 | 4.379 | 0.607 | 2.871 | 4.739 | 5.455 |
| 10-14 | 3.591 | 4.287 | 0.696 | 2.468 | 4.627 | 5.140 |
| 51-100 | 3.913 | 4.362 | 0.449 | 1.794 | 3.894 | 4.884 |
| 5k+ | 5.060 | 5.078 | 0.019 | 0.936 | 3.363 | 5.370 |
颜色:绿=gap<0.1(基本无 gap), 橙=gap 1-4(中等), 红=gap>4(严重过拟合)。 novel 桶无 train 对照(train 中按定义无 novel context),故 gap 列为空。
- run:
exp6_freqdecomp_current(服务器/data3/guoshaoyang/ngram-gap-exp/runs/,2026-07-31,GPU 6) - config:与
baseline_current同款 current-shell(ARCH_VARIANT=nanogpt_current_shell, table RMSProp, seed42, vocab=8192, n_layer=8),延长到 MAX_TRAINING_STEPS=1700(5 epoch) - 频次分桶:
NGRAM_GLOBAL_FREQUENCY_BUCKET_EDGES=0,1,2,3,4,5,6,7,8,9,10,15,21,31,51,101,201,501,1001,5001(精确到 1-9 次重复,10-14 合并) - epoch 边界:[338, 687, 1035, 1384](每 epoch ~349 step)
- final loss:train=1.382 / val=5.829(gap=4.447,仍在上升)