Session 1: Echo 之战
问一棵树:“你能把输入原封不动还给我吗?"——这是 SPR 的第一场战役。
1. 什么是 Echo Test
训练任何翻译模型之前,先问一个更简单的问题:你能不能表示"输入等于输出”?
如果连"我是我"都做不到,“我是你”(翻译)就无从谈起。
echo 就是自映射:f(x) = x。对 SPR 树而言,echo 的流程是:
句子 → 每个词过树路由 → 叶子存词的 ID → 重建时从叶子取回 → 输出原句子
BLEU 用来度量重建精度。BLEU=100 意味着原句和重建句完全一致——这就是"完美 echo"。
Phase 0 是 SameTime 专题的第一个实验(详见 WMT-003)。它用一个 DummyModel——nn.Embedding(vocab, 64) → nn.Linear(64, vocab)——7.3M 参数,在 IWSLT14 全部训练集上跑了 2 个 epoch,用 teacher forcing 做自重建,BLEU 达到 97.3。注意这是同一个 echo 任务:输入英语→输出英语,不是翻译。Session 1 的目标:让 SPR 树在没有任何训练、没有任何可学习参数的前提下,达到同等甚至更高的 echo 精度。
2. 树的哈希:循环位移 + 符号破缺
2.1 树的结构
SPR 树是一棵固定的二叉树。每个内部节点做一件事:把到达该节点的词分成左右两组,递归到叶子。
这次我们不用复杂的"训练路由权重"——我们用一个完全确定性的哈希函数来路由。同一个词每次都走同一条路,路径由词自身决定。
2.2 第一次尝试:纯循环位移
直觉:把词的 embedding 向右"滚"一格,和没滚的向量相加,就能编码"谁在左、谁在右"。
父节点 = 左子树 + torch.roll(右子树, shifts=1)
让我们用具体数字验证。假设三个词的 4 维嵌入:
"我" = [1, 2, 3, 4]
"打" = [0, 1, 0, 1]
"你" = [5, 6, 7, 8]
“我打你”(我在左,你在右):
左(我) = [1, 2, 3, 4]
右(你) = [5,6,7,8] → roll→ [8, 5, 6, 7]
父节点 = [1+8, 2+5, 3+6, 4+7] = [9, 7, 9, 11]
“你打我”(你在左,我在右):
左(你) = [5, 6, 7, 8]
右(我) = [1,2,3,4] → roll→ [4, 1, 2, 3]
父节点 = [5+4, 6+1, 7+2, 8+3] = [9, 7, 9, 11]
撞车! 两个完全相反的句子,产生了完全相同的哈希值。这是由联盟中另一位船长发现的致命漏洞。
2.3 漏洞分析
为什么碰撞?因为 roll 只是把数字换了位置——第一维里 1+8=9 和 5+4=9,第三维里 3+6=9 和 7+2=9。加法是可交换的,纯位移不能打破这个对称性。
另一位船长的原话:“仅仅在空间上做单一方向的滚动位移,信息还是太容易在加法中发生对冲和坍缩。”
2.4 修复:符号交替破缺
在"滚"之后乘以一个交替正负号 [1, -1, 1, -1, ...]。奇偶位符号不同,加法就不再可交换:
父节点 = 左子树 + sign_alt(torch.roll(右子树, shifts))
# sign_alt(x) = x * [1, -1, 1, -1, ...]
重新算一遍:
“我打你”(修正好):
左(我) = [1, 2, 3, 4]
右(你) = [5,6,7,8] → roll→ [8,5,6,7] → sign_alt→ [8, -5, 6, -7]
父节点 = [1+8, 2-5, 3+6, 4-7] = [9, -3, 9, -3]
“你打我”(修正好):
左(你) = [5, 6, 7, 8]
右(我) = [1,2,3,4] → roll→ [4,1,2,3] → sign_alt→ [4, -1, 2, -3]
父节点 = [5+4, 6-1, 7+2, 8-3] = [9, 5, 9, 5]
分离! 第二位一个是 -3,一个是 5。解码器只需要看这一位,就能区分主宾。
2.5 自路由:词自身的演进
上述逻辑不仅用于"合并两个词",也用于"路由单个词"。SPR 自路由的核心:
每深一层,词向量做一次 roll+sign_alt,然后和原始向量做内积。
内积 > 0 → 向右;内积 ≤ 0 → 向左。
def route_chunk(chunk, depth):
idx = 0
current = chunk.clone()
for dp in range(depth):
current = torch.roll(current, shifts=dp+1) * SIGN_MASK # 自我演进
score = (chunk * current).sum() # 和原始做内积
if score > 0: idx = 2*idx + 2 # 右
else: idx = 2*idx + 1 # 左
return idx - (2^depth - 1) # 叶子号
每一层里,current 都在自我演进(roll 的偏移量逐层递增),所以 7 层的内积彼此独立——每一层都在看词的不同"侧面"。
3. 第一次挫折:单树不够
depth=14 的单棵树有 16384 片叶子。WMT14 训练集约 42K 个不同词——平均每叶 2.6 个词。
这意味着什么?当多个词共享一片叶子时,我们只能输出其中最频繁的词。假设叶子里有 {“the”, “cat”, “strategy”}——不管输入是 “cat” 还是 “strategy”,输出都是 “the”。
结果是 BLEU=62.88,独叶率 42.6%。离 97 还很远。
4. 突破:分解路由
直觉:一个词的一整条 64 维向量,能不能用 4 个独立的 16 维子向量来替代?每个子向量各自走一遍自路由,产生各自的叶子号,然后组合起来?
一个词 [64 维]
├─ chunk0 [0:16] → 自路由 → leaf0 ∈ [0, 127]
├─ chunk1 [16:32] → 自路由 → leaf1 ∈ [0, 127]
├─ chunk2 [32:48] → 自路由 → leaf2 ∈ [0, 127]
└─ chunk3 [48:64] → 自路由 → leaf3 ∈ [0, 127]
组合键 = leaf0 × 128³ + leaf1 × 128² + leaf2 × 128 + leaf3
∈ [0, 128⁴-1] = [0, 268,435,455]
128⁴ = 2.68 亿 —— 而词表只有 42K。42K 个词散进 2.68 亿个槽里,每个词几乎独享一个槽。
4.1 物理直觉
这不是简单的"拆分向量以求更多叶子"。这是让词的每一个侧面独立表达。
一个 64 维向量可以理解为一句话:维 0-15 说的是语法功能,维 16-31 说的是语义类别,维 32-47 说的是情感色彩,维 48-63 说的是时态语态。每一组走独立的树——不是在追问"你是谁",而是在追问"你的每一个侧面各是什么"。
结果:独叶率从 42.6% 跃升到 99.7%。BLEU 从 62.88 跃升到 99.99。
src: a republican strategy to counter the
hyp: a republican strategy to counter the ← 完美重建
src: republican leaders justified their policy by
hyp: republican leaders justified their policy by ← 完美重建
src: however, the brennan centre considers this
hyp: however, the brennan centre considers this ← 完美重建
5. 代码解析
完整的 SPR Echo 证明只需 95 行代码。核心组件:
# 1. 词表构建
word2id = {"<pad>": 0, "<unk>": 1} # 每词赋ID
for s in train_sents:
for w in s:
if w not in word2id: word2id[w] = len(word2id)
# 2. 随机 embedding(无训练)
E = torch.randn(V, d) / E.norm(dim=1) # V×64,单位球面上
# 3. 自路由——每词通过自身的循环演进决定路径
def route_chunk(chunks, depth):
current = chunks.clone()
for dp in range(depth):
current = torch.roll(current, shifts=dp+1) * [1,-1,...]
score = (chunks * current).sum()
idx = idx*2 + 1 + (1 if score > 0 else 0)
return idx - (2^depth - 1)
# 4. 分解路由——K=4 片,各自独立走树,组合
for k in range(4):
leaf_chunks[:, k] = route_chunk(E[:, k*16:(k+1)*16], depth=7)
leaf_combined = leaf_0*128³ + leaf_1*128² + leaf_2*128 + leaf_3
# 5. Echo 查表——每个组合叶存最频繁词
for wid in range(V):
leaf_top[leaf_combined[wid]] = most_frequent_in_that_leaf
关键参数:
| 参数 | 值 | 含义 |
|---|---|---|
| V | 42,109 | 去重词数 |
| d | 64 | 嵌入维度 |
| K | 4 | 分解片数 |
| depth | 7 | 每片树的深度 |
| 叶子 | 128⁴ = 268M | 有效组合叶子数 |
| 独叶率 | 99.7% | 每个词几乎独占叶子 |
6. 三场败仗教会我们的事
回顾 Session 1 的全程,有三场关键败仗塑造了最终设计:
败仗 1:纯循环位移碰撞
torch.roll 加 sum() 在加法中产生对称性——“我打你"和"你打我"算出同一个哈希。修复是符号交替——[1, -1, 1, -1] 零成本打破对称。
败仗 2:训练 E 导致坍缩
尝试让 embedding 可训练——所有词被凝聚损失推到同一点,BLEU 归零。冻结嵌入(不训练)才是正路。
败仗 3:残差级联的直觉陷阱
直觉说"先去掉大类特征,再切分细节”——但对随机嵌入不成立。随机嵌入没有"大类特征"可减。分解路由(拆维并行)才是正确答案。
7. Session 1 的定位
echo test 不是终点——是结构的压力测试。如果一棵树连"保存自身信息"都做不到,还谈什么"转化信息"(翻译)?
Session 1 证明了:SPR 的自路由树 + 分解路由,能在零参数、零训练的条件下,在 42K 词表上达到 99.99% 的 echo BLEU。树的结构本身有足够的表达能力。
7.1 切片路由的真正意义:从 Echo 到 Translation
“Echo 是一场容量的阅兵。Translation 将是一场转换的围猎。” —— Gemini
Session 1 里用 K=4 切片达到 2.68 亿叶子——但切片的物理意义远不只是"增加桶数"。4 个切片形成 4 个独立的特征子空间:
"apple" → chunk0(词义)=42, chunk1(语法)=7, chunk2(语境)=15, chunk3(词形)=88
"apfel" → chunk0(词义)=42, chunk1(语法)=7, chunk2(语境)=15, chunk3(词形)=23
↑ 只有词形不同——局部一跳,全局稳健
如果单树,两个词在某深度差一分就永远走散(雪崩效应)。切片把变化锁在特定维度——局部敏感、全局免疫。
Session 2 的翻译路径由此清晰:不是学 “apple→Apfel” 的全局映射,而是学每个 chunk 的跨语言码本——4 个可训练码本、4×128 个连续向量,从叶子号到连续特征到最近邻召回。参数量微乎其微,局部对齐能力极强。
代码:spr_echo_proof.py
联盟审查版:houming818.reviewed/spr_echo_proof.py
License: GPLv3 本文《SPR》系列采用 GPLv3 协议开源发布。