从 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=9 和 5+4=9,第三维: 3+6=9 和 7+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 协议开源发布。