从 Echo 到 Order

P6 的树能将输入原样输出(echo)。P7 回答:怎么让树感知"词序"——直到它能翻译。

1. 问题:为什么 echo 不够

P6 的树用 mean(E[tokens]) 做哈希。“猫吃鱼"和"鱼吃猫"打出同一个哈希——因为均值运算不在乎词序。

翻译必须区分语序。TF 靠 positional encoding,SPR 树靠合并规则。P7 给这个规则装上"顺序感知”。

2. 有序哈希:循环位移 + 符号交替

2.1 方案:用维度位移编码位置

编码规则很简单——左子树保持不动,右子树的向量往右滚一格,然后两个向量直接相加:

父节点 = 左子树 + torch.roll(右子树, shifts=1)

为什么这样能编码位置?因为"滚一格"和"不滚"是不同的——就像十进制里 21 和 12 不一样,不是数字不同,是数位不同。向量里没有"十位"和"个位",但有"第 0 维"和"第 1 维"。torch.roll 就是把向量的维度轮换一圈——零计算成本,纯内存重排。

2.2 漏洞推导:纯位移的碰撞

拿具体的数字走一遍。假设每个词的嵌入向量是 4 维,三个词:“我” = [1,2,3,4],“打” = [0,1,0,1],“你” = [5,6,7,8]

用位置二分拆分——前半句一个词,后半句一个词:

场景 A:“我打你”——“我"在左,“你"在右:

左子树(我) = [1, 2, 3, 4]            ← 不动
右子树(你) = [5, 6, 7, 8] → roll右移1位 → [8, 5, 6, 7]

父节点 = [1,2,3,4] + [8,5,6,7] = [9, 7, 9, 11]

场景 B:“你打我”——“你"在左,“我"在右:

左子树(你) = [5, 6, 7, 8]            ← 不动
右子树(我) = [1, 2, 3, 4] → roll右移1位 → [4, 1, 2, 3]

父节点 = [5,6,7,8] + [4,1,2,3] = [9, 7, 9, 11]

撞车了! 两个完全不同的句子,算出来的父节点一模一样——都是 [9, 7, 9, 11]

为什么?因为 roll 只是把数字换了位置,加法是可交换的。在每一维里,同一个数字对上了——比如第一维: 1+8=95+4=9,第三维: 3+6=97+2=9。纯循环位移在和操作下不能消除这个对称性。

另一位船长的原话:“因为普通的循环位移,在元素两两配对相加时,产生了数学上的’巧合’(1+8 = 5+4)。这说明:仅仅在空间上做单一方向的滚动位移,信息还是太容易在加法中发生对冲和坍缩。”

2.3 修正:符号交替破缺

另一位船长的方案:“为了彻底解决上面的碰撞,让推理绝对精确,我们需要在’位移’的基础上,加上一个极其廉价、同样不需要计算的操作:正负号交替(打破对称性)。”

问题出在加法对称。解法:在"滚"之后,乘以一个交替的正负号 [1, -1, 1, -1, ...],打破对称。奇偶位符号不同,加法就不再可交换了。

修正后的合并规则:

父节点 = 左子树 + sign_alt(torch.roll(右子树, shifts=1))
# 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]

结果: [9, -3, 9, -3][9, 5, 9, 5] ——永久分离。第二位一个是 -3,一个是 5。解码器只需要看这一位,就能确定语序。

2.4 硬件成本

torch.roll 是纯线轨操作——只是改了内存地址的读取偏移,不耗 ALU。sign_alt 就是乘 ±1——位翻转或一次廉价浮点乘。整个有序哈希方案的计算成本加起来 ≈ 零。

方案 序敏感 碰撞 硬件成本
均值 mean +
roll 0
roll + sign_alt 0

3. 实验一:有序 hash 真的有用吗?

WMT14 真实语料,取 20 句,co-occ 嵌入(d=32)。每句用两种 hash——有序(roll+sign_alt)和无序(mean)——算出根哈希向量,然后比较 20 个哈希之间的相似度。

有序 无序(mean)
同一句两次,hash 一样? ✅ 20/20 ✅ 20/20
正序≠逆序? ✅ 20/20 天然不敏感
句间差异(越小越好) 0.48 0.96

无序 hash 把 20 个句子几乎压到了同一个向量(sim=0.96)——不同句子的语义差异被平均操作抹平了。有序 hash 保留了句子结构信息,句间差异从 0.96 降到了 0.48——每个句子被清楚地区分开。

GPU 上也做了同样实验,规模更大——256 叶、42K 词汇、50 句。结果一致:有序区分度 0.69 vs 无序 0.96。

4. 双树翻译架构

有序 hash 告诉树"词的顺序是什么”。翻译还需要"这句德语对应那句英语”。需要三组件:

┌──────────┐     ┌───────┐     ┌──────────┐
│ Encoder  │ ──→ │Bridge │ ──→ │ Decoder  │
│ 源语言句  │     │跨语言  │     │ 目标语言  │
│ → 根哈希  │     │空间映射 │     │ → 叶子词  │
└──────────┘     └───────┘     └──────────┘
   0 参数          d×d 矩阵       逐层 W_split

Encoder(0 参数,纯规则)

把源语言句子从底向上合并成唯一根哈希——使用 §2 的规则。编码后:语义 + 语序 全部锁在根向量里。不需要训练。

Bridge(d×d 线性矩阵)

另一位船长的原话:“在映射阶段,那个轻量级矩阵不仅翻译了词义,由于不同语言在空间中的流形不同,它在对齐时会自动发生空间的几何偏转。”

因为德语的"Katze"和英语的"cat"不在同一个向量空间,需要一个转换层。H_en = H_de @ W_bridge——跨语言空间映射。

Decoder(逐层 W_split)

另一位船长的设计:“解码器必须引入可训练的分裂矩阵来解决数学上的不唯一反解问题。用统计学和梯度下降,引导树结构走向目标语言的语法流形。”

解码是从根向量反向拆成叶子。编码时用 H = HL + sign_alt(roll(HR)),解码时已知 H,要反解 HL 和 HR。

问题:H 只有一个,HL 和 HR 是两个未知量。数学上 5 = x + y 有无穷多组解(2+3、1+4、100-95…)。纯数学反解做不到。

所以引入可训练的 W_split:每层深度共享一个 d×d 矩阵,学出来"怎么从 H 预测 HL”:

HL = H @ W_split[level]      ← 预测左子树
HR = sign_alt(unroll(H - HL)) ← 右子树由剩余部分确定

不需要每节点独立参数——同一深度的所有节点共享同一个 W_split。深度 6 的树,总共只需要 6 个 d×d 矩阵。

验证

玩具数据(10 个 token,4 句对)

训练 300 步后,解码准确率 4/4 (100%)。loss 从 0.33 降到 0.0001。“我打你"对应源句 ID [0,1,2] → 解码出目标 ID [5,6,7] = “I hit you”。每句 3 个词全对。

WMT14 50K 句

训练中 loss 收敛(0.20 → 0.001),梯度流通正常,所有架构组件验证通过。BLEU 仍为 0 ——75K+45K 词汇 / 2K 句训练量不够学习跨语言映射。这是数据规模瓶颈,不是架构问题。

生产规模训练中

100K 句,30K+25K 词汇(频筛),depth=6(64 叶),d=128。Enc(0) + Bridge(16K) + Dec(6×16K) + E(7M) ≈ 18M 可训练参数。GPU 上全量训练中。

玩具 WMT14-2K WMT14-100K
4 2,000 100,000
10+10 75K+45K 37K+26K
深度 2 3 6
结果 4/4 ✅ loss ✅ GPU 中

5. 当前状态

模块 状态 数据
echo test BLEU=100%
ordered hash (cyclic + sign_alt) 0.48 vs 0.96
GPU routing training 42K 词/300 步/2 秒
Encoder + Bridge MSE=0
Decoder (W_split) 玩具 4/4, WMT14 loss 收敛
routing BLEU (depth=7, 128叶) 随机 1.62 → 训练 6.25 (+4.63)
Translation BLEU (twin tree) ⚠️ 架构通, 规模化训练中

7.1 另一位船长的代码审查与修复

训练脚本的初版存在两个关键 Bug,由另一位船长在审查中发现:

Bug 问题 修复
argmax 断流 loss = MSE(E, lc[assign.argmax()]) — argmax 离散化,梯度断流。只有平衡损失在驱动参数 软中心:token_to_center = assign @ lc,全可导
递归爆炸 soft_assign 递归 + 原地赋值,depth=8 时显存爆炸 逐层外积 probs × splits,非递归 GPU 并行
无效对照 unordered_hash 不涉及 nw_trained,随机/训练组无区别 训练后抽取叶子语义中心 E_semantic 做 ordered hash

修复后结果:

随机 训练 Δ
句间 sim 0.510 0.451 +0.059
训练时间 - 1.4 秒 GPU 并行加速

梯度全通后,路由训练真正让句子间区分度提升 0.059——> 更低 sim = 更强分离。

代码: spr_hash_cyclic.py · spr_008_twin_tree.py · spr_007_verify.py


License: GPLv3 本文《SPR》系列采用 GPLv3 协议开源发布。