← 返回主线结果页 · 查看历史完整版本

背景与 debug:从 current shell 回到最小 n-gram setting

这是一份可选阅读。主页面只依赖一个干净结论:vanilla nanoGPT 加 bigram / trigram value memory,在固定顺序 replay 下即可出现 gap。本页解释术语、实现路径和曾经排除的错误。

1. 术语表:每个东西到底是什么

术语直观含义在最小实验中的角色
n-gram value table / VE用连续 token context 查出一个可训练向量。真正产生 train-specific memory 的参数。
bigram / trigram分别使用 2 个 / 3 个连续 token 作为 context。当前实验的两条 memory 分支。
gate把 table value 乘上一个可学习强度,典型形式是 2×sigmoid(Linear(x))只在 v / y 消融中使用;input baseline 不需要 gate。
input / over-encoding先查表,再把 n-gram value 加到 token embedding。最朴素、最适合作为 baseline 的注入点。
v 注入在 attention 前把 value residual 加进 V。信号会被 attention matrix 混合。
y 注入attention 完成后,把 residual 加到 y。信号直接进入 block 输出,gap 最强。
current shellOPHIS 自己的一整套 GPT forward / optimizer 外壳。历史分支,不是 n-gram gap 的必要条件。

2. 三种注入点的伪代码

这里的 ngram_value(idx) 是根据当前 token 的 context 查表得到的向量。它不是一个 scalar,而是与 hidden state 同维度的 trainable vector。

# input / over-encoding:一次注入,后面是普通 Transformer
def forward_input(idx):
    x = wte(idx) + wpe(pos)
    x = x + ngram_value(idx)       # bigram value + trigram value
    for block in transformer:
        x = block(x)
    return lm_head(ln_f(x))

# v residual:先加到 V,再经过 attention 的加权混合
def forward_v(idx):
    x = wte(idx) + wpe(pos)
    for block in transformer:
        q, k, v = block.c_attn(block.ln_1(x)).split(3 * dim)
        v = v + gate(x) * ngram_value(idx)
        y = attention(q, k, v)
        x = x + block.c_proj(y)
        x = x + block.mlp(block.ln_2(x))
    return lm_head(ln_f(x))

# y residual:attention 完成后直接加到输出
def forward_y(idx):
    x = wte(idx) + wpe(pos)
    for block in transformer:
        q, k, v = block.c_attn(block.ln_1(x)).split(3 * dim)
        y = attention(q, k, v)
        y = y + gate(x) * ngram_value(idx)
        x = x + block.c_proj(y)
        x = x + block.mlp(block.ln_2(x))
    return lm_head(ln_f(x))

3. 为什么 V 与 y/input 的结果不同

attention 对 V 的计算是线性的。若 n-gram residual 为 G,attention matrix 为 A,则:

A · (V + G) = A · V + A · G。只有当 G 对所有被读取位置完全相同,且后续投影也不改变位置关系时,A · G 才可能近似等于 G;一般情况下它会被不同 query 的 attention 权重重新混合。

因此,v 注入并不是“没有加进去”,而是信号路径更弱。诊断结果显示 gate 初始化正常、n-gram residual 确实非零,但其 norm 约为 V norm 的 6.5%,容易被主干 value 淹没。y / input 则绕开了这一步。

4. current shell 到底是什么

早期实验从 vanilla nanoGPT 往上逐步加入机制。后来出现的 current shell 不是某一个独立变量,而是 OPHIS 的完整模型实现分支,包含若干成组差异:

实验解释:current shell 可以改变 gap 的量级和训练动力学,但不能作为“只有 n-gram 才能产生 gap”的最小证据。现在的 ngram-gap-lab 把它全部移除,直接在 vanilla 路径上做注入点消融。

5. 频率统计 debug:为什么必须用真实 context key

模型内部为了节省表大小,会把完整 context hash 到有限的 table row。这个 hash row 适合查参数,但不适合判断“这个 context 是否在训练集出现过”。不同真实 trigram 可能碰撞到同一 row,导致 novel context 被误报为 seen。

# 用于频率统计的真实 key(示意)
bigram_key  = prev_token * vocab_size + current_token
trigram_key = prev2 * vocab_size**2 + prev * vocab_size + current_token

hit_count = count_train_contexts(real_key)
bucket = "novel" if hit_count == 0 else frequency_bucket(hit_count)

修复后得到 bigram novel 约 4.3%、trigram novel 约 31.2%,与历史离线统计一致。模型仍然可以继续使用 hash table;只有分析模块改用真实 token tuple 编码。

6. 优化器为什么 table 用 RMSProp

n-gram table 很大而且访问稀疏。当前 setting 对 table 使用 RMSProp(β₁=0, β₂=0.999),对 backbone 使用 AdamW。这样每次访问 table row 时,更新方向更接近当前梯度,不会被 momentum 把多个 context 的历史更新混在一起。

这是一项让现象稳定、便于复现的训练 setting;gap 的来源仍然是 train-specific n-gram memory 与固定顺序 replay 的组合,而不是 optimizer 名字本身。

7. 相关记录

svbird 的 regime-bridge 报告记录了从 vanilla 负例逐步接近 current core 的历史路径。它对理解项目演化有用,但本页的最小复现结论以 ngram-gap-lab 的 clean setting 为准。