航行问题:TreeHeap route 点燃了,但 compact state 还没过海

这篇不是庆功,也不是翻车记录。

更准确地说,它是一张航海图。

SPR-045 解决了一个很严重的审计问题:

以前有些 echo / mirror 实验用的是 L x L route matrix。
那能恢复句子,但不是 TreeHeap route。

所以 SPR-045 重新要求:

从 arr[1] 出发;
每一步只看当前 node;
输出 stop / left / right;
沿着 TreeHeap 地址递归走下去。

这才叫 TreeHeap route。

但 SPR-045 之后,DS 又指出一个新问题:

route kernel 虽然递归走树,
但它读的是预计算几何区间 feature,
不是堆里真实装的 token / subheap state。

也就是说,它确实走树了,但像拿着答案地图走树。

所以 SPR-046 的问题变成:

kernel 能不能真的读 arr[i]arr[left]arr[right] 的内容,然后决定 stop / left / right?

这一步很关键。

如果能,就说明 S1 route 第一次真正进入了 TreeHeap 的数据结构内部。

如果不能,说明我们仍然只是在做外部几何导航。

先把粗糙算法讲清楚

这里先用一个粗糙类比入门。

注意,这不是最终理论定义。

它只是帮本科读者先看见:

TreeHeap route 至少不是 L x L 矩阵。
它必须在树地址上一步一步发生。

先看普通数组。

一句话可以写成:

["I", "like", "small", "cats"]

如果只用数组,我们会说:

第 0 个词是 I
第 1 个词是 like
第 2 个词是 small
第 3 个词是 cats

TreeHeap 会先把这句话放进一棵二叉堆。

为了简单,假设最多 4 个 leaf:

              arr[1]
             /      \
        arr[2]      arr[3]
        /   \       /   \
    arr[4] arr[5] arr[6] arr[7]
      I    like   small  cats

这里有一个很重要的规则:

left child  = arr[2i]
right child = arr[2i + 1]

例如:

arr[1] 的 left  是 arr[2]
arr[1] 的 right 是 arr[3]
arr[2] 的 left  是 arr[4]
arr[2] 的 right 是 arr[5]

leaf 里面放具体 token。

internal node 里面放这个子树的摘要。

比如:

arr[2] 表示 ["I", "like"] 这个子堆
arr[3] 表示 ["small", "cats"] 这个子堆
arr[1] 表示整句话 ["I", "like", "small", "cats"]

现在问一个问题:

query = “cats”,怎么从 root 找到它?

TreeHeap route 的走法是:

从 arr[1] 开始。

看当前 node、left child、right child。

如果 query 更像在 left 里,就走 left。
如果 query 更像在 right 里,就走 right。
如果当前 node 已经是答案,就 stop。

"cats" 来说,路线应该是:

arr[1] -> right -> arr[3] -> right -> arr[7] -> stop

用动作表示:

right, right, stop

如果 query 是 "like",路线就是:

arr[1] -> left -> arr[2] -> right -> arr[5] -> stop

动作是:

left, right, stop

所以 TreeHeap route 的本质是:

用一串 stop / left / right 动作,
在堆地址里递归查找目标。

这和一个 L x L 矩阵完全不同。

L x L 矩阵像这样:

我要第 3 个输出,所以直接从第 0/1/2/3 个输入里加权拷贝。

它处理的是“序列位置表”。

TreeHeap route 处理的是“树上的递归路径”。

这就是为什么我们一直强调:

矩阵可以有用,但不能冒充 TreeHeap route。

但更准确的类比不是查找树,而是信息编码树

上面的例子说:

query = cats
从 root 一路找 cats

这个说法容易误导。

它会让 TreeHeap route 听起来像二叉搜索树:

比较 key
选择 left/right
找到目标

这不是我们真正想要的 TreeHeap。

更准确的类比应该是霍夫曼编码树一类的信息编码树。

在霍夫曼树里,重点不是“比较大小”。

重点是:

root      表示一整团信息
internal  表示还能继续细分的信息集合
left/right 表示不同的信息分支
leaf      表示更具体的信息状态
path      就是编码

TreeHeap route 也应该这样理解。

当前 node 不是一个普通容器。

它应该是一个信息状态:

H_i = 当前 node 所承载的信息分布

例如:

H_1 表示整句话的信息
H_2 表示左半句的信息
H_3 表示右半句的信息

route 不是问:

cats 在左边还是右边?

更深一层,它是在问:

在当前问题 Q 下,
当前 node 的信息是否已经足够?

如果不够,
应该坍缩到哪个信息分支,
才能获得更多有效信息?

所以 stop / left / right 的含义应该改成:

动作 信息论含义
stop 当前 node 的信息已经足够回答 Q
left 左分支的信息增益更高
right 右分支的信息增益更高

这样看,TreeHeap route 就不是:

SearchTreeRoute

而是:

InformationCollapseRoute

也就是:

在一棵信息编码树上,
根据 Q 对当前信息状态做概率坍缩。

这个修正很重要。

因为 echo 任务里我们可以假装自己在“找 token”。

但翻译任务里,模型通常不是在找一个确定 token。

它可能是在问:

当前中文生成位置需要英文哪一块信息?
这个时间状语是否应该前置?
这个修饰语应该归到哪个短语里?
当前 node 是否已经是一个可读短语?

这些都不是普通查找树问题。

它们更像:

在当前信息分布中,
选择一个最有用的信息粒度。

route kernel 到底做什么

上面说“当前信息是否足够、哪个分支信息增益更高”,这句话要变成代码,就需要一个函数。

这个函数就叫 route kernel。

它的输入是:

q          = 要找的东西,比如 "cats" 的向量
arr[i]     = 当前子堆摘要
arr[2i]    = left 子堆摘要
arr[2i+1]  = right 子堆摘要

它的输出是三个分数:

score_stop
score_left
score_right

然后取最大值:

如果 score_left 最大,就走 left。
如果 score_right 最大,就走 right。
如果 score_stop 最大,就停下。

伪代码就是:

i = 1

while True:
    q_vec = encode(query)

    current = arr[i]
    left    = arr[2 * i]
    right   = arr[2 * i + 1]

    score_stop, score_left, score_right = K(q_vec, current, left, right)

    action = argmax([score_stop, score_left, score_right])

    if action == "stop":
        return arr[i]

    if action == "left":
        i = 2 * i

    if action == "right":
        i = 2 * i + 1

本科数据结构课里,这很像二叉树查找。

不同点在于:

普通二叉搜索树靠 key 的大小比较。
TreeHeap route 靠 kernel 从向量状态里判断方向。

也就是说,在更准确的口径里,TreeHeap 不是问:

cats > like 吗?

而是问:

在当前 Q 下:
  当前 node 是否已经足够?
  left subheap 是否提供更多有效信息?
  right subheap 是否提供更多有效信息?

这就是为什么我们关心 arr[i] 到底怎么表示。

如果 arr[i] 表示得好,kernel 就容易判断。

如果 arr[i] 表示得差,kernel 就会迷路。

这里必须停一下:H、Q、Theta 到底是什么

上面的二叉树查找只是工程近似。

如果我们把它上升到 TreeHeap 理论层,不能只说:

有个 kernel,输出 left/right/stop。

这样太像普通神经网络包装出来的说法。

必须把三个对象说清楚:

H      = 当前样本的 TreeHeap state
Q      = 当前输入问题 / 读取意图 / 求解变量
Theta  = 可学习的参数 TreeHeap

然后模型做的事情应该是:

Route_Theta(H, Q) -> ProbabilityBucket(stop, left, right)

也就是说,它不是先天直接给一个硬答案。

它先给一个概率桶:

stop  = 0.05
left  = 0.10
right = 0.85

如果需要硬执行,就坍缩成:

right

如果还不想坍缩,就把这个概率桶传给下一层。

这才更接近我们一直说的:

概率容器
延迟坍缩
结构搜索

H:当前句子的全程状态

假设输入句子是:

I like small cats

写入 TreeHeap 后,得到一个 H

H.arr[4] = state("I")
H.arr[5] = state("like")
H.arr[6] = state("small")
H.arr[7] = state("cats")

H.arr[2] = compose(H.arr[4], H.arr[5])
H.arr[3] = compose(H.arr[6], H.arr[7])
H.arr[1] = compose(H.arr[2], H.arr[3])

这里的 H 不是参数。

它更像一次前向计算里的运行时内存。

同一句话每次输入,都会生成自己的 H

不同句子有不同的 H

所以:

H = data state

不是:

H = learned parameter

Q:这次想解决的问题

Q 不是整棵树。

Q 是“我现在要问什么”。

在 echo 任务里,Q 可以很简单:

我要读回第 k 个 token。

或者:

我要找 token cats。

在翻译任务里,Q 会复杂很多,可能是:

我要生成中文第 t 个位置;
我要读英文里和当前中文位置最相关的 subheap;
我要判断某个时间状语是否应该前置。

所以:

Q = query / task intent

它决定这次 route 的目标。

同一个 H,不同的 Q,应该得到不同的概率桶。

比如同一句话:

H = TreeHeap("I like small cats")

如果:

Q = read("I")

路线应该偏 left-left。

如果:

Q = read("cats")

路线应该偏 right-right。

Theta:参数也应该是 TreeHeap

这里是最容易讲糊的地方。

如果我们写:

score = MLP([Q, H.arr[i], H.arr[2i], H.arr[2i+1]])

这当然能跑。

但它不一定是 TreeHeap 的理论形态。

更严格地说,Theta 应该也是一个参数 TreeHeap。

比如 route kernel 可以有自己的参数堆:

Theta_route.arr[1] = root slot parameter
Theta_route.arr[2] = left slot parameter
Theta_route.arr[3] = right slot parameter
Theta_route.arr[4...] = deeper/local pattern parameters

它不是一句话的运行时状态。

它是模型长期学习出来的规则。

类比线性回归:

y = w x + b

这里:

w, b = 参数
x    = 输入
y    = 输出

放到 TreeHeap 里,就是:

H      = 输入句子形成的 TreeHeap state
Theta  = 模型学到的参数 TreeHeap
Q      = 当前求解意图
P      = 输出概率桶

所以更像:

P = Route(H, Q; Theta)

而不是:

P = Route(H, Q)

Theta 怎么参与当前 node 的计算

在某个 node i,我们有当前局部数据:

local_H(i) = [H.arr[i], H.arr[2i], H.arr[2i+1]]

参数 TreeHeap 也提供一个局部算子:

local_Theta = [Theta.arr[root], Theta.arr[left], Theta.arr[right], ...]

一个最小形式可以写成:

z_stop  = f_stop(Q, H.arr[i],     local_Theta)
z_left  = f_left(Q, H.arr[2i],    local_Theta)
z_right = f_right(Q, H.arr[2i+1], local_Theta)

然后:

P(stop, left, right)
  = softmax([z_stop, z_left, z_right])

这里的 f_stop / f_left / f_right 可以先用 MLP 近似。

但理论上它应该逐步替换成 TreeHeap kernel:

root slot 怎么比较
left slot 怎么比较
right slot 怎么比较
路径前缀怎么参与
subheap compose 怎么参与
mirror / fold 等算子怎么参与

这就是当前理论还没完全闭合的地方。

Theta 怎么训练

训练过程和普通机器学习一样,需要 loss。

在 echo route 任务里,我们知道正确动作。

比如:

当前 node = arr[1]
query = cats
正确动作 = right

模型输出:

P(stop)=0.05
P(left)=0.30
P(right)=0.65

那么 loss 可以用交叉熵:

Loss = -log P(right)

如果模型把 right 概率调高,loss 下降。

梯度下降更新的是:

Theta_route

也就是参数 TreeHeap。

不是直接把这句话的 H 当作长期参数。

更完整一点:

for each training sample:
    H = Encode(sentence)
    Q = BuildQuery(task)

    for each route step:
        P = Route_Theta(H, Q, node=i)
        Loss += CE(P, correct_action)

    update Theta by gradient descent

如果 encoder 也是可学习的,那么还会更新:

Theta_encode
Theta_compose
Theta_route
Theta_read

但这几个最好分开训练或分阶段训练。

不然梯度会混在一起,最后我们不知道到底是谁学会了东西。

当前 046 实验实际做到了哪一步

必须诚实说,当前实验没有完全做到上面的理论形态。

当前 dense content-aware proof 做到的是:

H.arr[i] 使用 dense vocab-count subheap state;
Q 使用 supervised query token;
Theta 使用一个普通神经网络参数;
输出 stop/left/right 概率;
用交叉熵训练;
最后硬坍缩成路径。

也就是说,它证明了:

如果 H.arr[i] 真的包含子堆内容,
一个可学习 route function 可以读 H 和 Q,
并学出 stop/left/right 概率桶。

但它还没有证明:

Theta 本身已经是一个严格的参数 TreeHeap;
compose/read/route 都已经在 TreeHeap 代数内部闭合;
模型已经能无监督发现 Q;
模型已经能翻译。

所以 046 的正确定位应该是:

第一次 content-aware route 工程 proof。
不是完整 TreeHeap route 理论闭包。

这个区分必须保留。

如果不保留,我们就会再次把“能跑的近似实现”误写成“理论已经完成”。

现在的结论先说清楚

这次结论分两层。

第一层:

content-aware dense route 跑通了。

第二层:

compact 压缩版没有跑通严格门槛。

这不是 GPU 没点燃。

也不是 io 基础设施问题。

实验在 io 上正常执行,日志、summary、evidence 都保存了。

真正的问题在这里:

dense vocab-count subheap state 能支持 route;
naive random token sum compact state 会丢精度。

换句话说,船已经出港了,但现在卡在“船舱怎么压缩物资还不丢关键物品”。

content-aware route 的实验版算法

上面是算法概念。

这次实验里的 content-aware route,就是把它具体落成下面这个形式:

当前 node = i

kernel 输入:
  q              当前要找的 token/query
  arr[i]         当前子堆状态
  arr[2i]        left child 子堆状态
  arr[2i + 1]    right child 子堆状态

kernel 输出:
  stop / left / right

如果输出 left:

i = 2i

如果输出 right:

i = 2i + 1

如果输出 stop:

return arr[i]

这里的核心不是“能不能走到正确叶子”。

核心是:

kernel 的判断依据来自堆内容,而不是外部答案。

这和之前被审计的问题不同。

之前的几何版本里,feature 里有类似:

target in left?
target in right?

这等于把答案方向直接塞给 kernel。

content-aware 版本不允许这样做。

它只允许读:

query
当前子堆内容
左子堆内容
右子堆内容

这就比较像真正的 TreeHeap 局部卷积了。

用刚才的例子说:

query = cats

在 arr[1]:
  left  = ["I", "like"]
  right = ["small", "cats"]

kernel 应该判断 cats 在 right 里,所以输出 right。

到 arr[3]:
  left  = ["small"]
  right = ["cats"]

kernel 应该输出 right。

到 arr[7]:
  当前就是 cats

kernel 应该输出 stop。

所以这不是“一次分类”。

它是一串局部判断组成的查找过程。

这也是为什么整条 route exact 很苛刻:

只要某一步 left/right/stop 错了,
整条路径就错。

dense 版本怎么表示 subheap state

dense 版本用的是一个很朴素、但很清楚的表示。

假设词表大小是:

vocab = 1024

那么每个 arr[i] 是一个 1024 维向量。

它表示:

这个子堆里出现过哪些 token。

比如子堆里有:

cat, is, eating

那么对应 token 的位置会被计数。

这不是语义向量。

它更像一个子堆内容清单。

优点:

信息非常明确。
query token 在不在 left/right 子堆中,理论上可以从内容清单里学出来。

缺点也很明显:

太大。

这次 dense proof 的复杂度估计是:

dense_feature_memory_mb = 6191.25

大约 6.2GB。

这已经不是优雅方案了。

但它适合作为第一把尺子:

如果 dense 内容清单都跑不通,
那就说明 route 机制本身有问题。

结果它跑通了。

dense proof 的结果

实验数据:

data = /mnt/nas/datasets/wmt_massive/train.massive.zh-en.tsv
samples = 20,000
vocab = 1024
heap max_len = 32
train lengths = 3..24
OOD lengths = 25..32

这里的 OOD 是长度外推:

训练只看长度 3 到 24;
测试看长度 25 到 32。

这可以检查模型是不是只背了长度表。

结果:

指标 数值
train route steps 352110
OOD route steps 44130
dense feature memory 6191.25 MB
OOD step acc 0.9983
OOD route exact 0.9902
flat length-matrix OOD token acc 0.0029
flat length-matrix OOD exact 0.0000
pilot pass true

这说明:

content-aware dense TreeHeap route 第一次真正跑通了。

这里的“第一次真正”是有边界的。

它不是说:

TreeHeap 会翻译。
TreeHeap 理解语义。
TreeHeap 击败 Transformer。

它只说:

在 controlled S1 route 任务里,
kernel 可以读 subheap 内容,
递归执行 stop/left/right,
并在未见过的更长句子上保持接近 99% 的整条路径正确率。

这已经够重要。

因为它把 TreeHeap route 从“几何答案导航”推进到了“内容感知导航”。

为什么 compact 版本必须做

dense 版本有 6.2GB 的 feature memory。

如果我们每次都用 1024D 甚至更大的词表清单,后面没法往真实 S1/S2 走。

所以自然要问:

能不能把每个 subheap 压成一个小向量?

比如:

64D
128D

这就更像我们最终想要的 TreeHeap state。

每个 token 不是 one-hot 清单,而是一个向量。

每个子堆状态是:

arr[i] = 子堆里 token vectors 的组合

这也是直觉上更接近未来方案的地方:

token embedding
subheap embedding
learned compose
learned read

但是这次先做了一个最简单的 compact 版本:

token id -> 固定随机向量
arr[i]   -> 子堆 token 向量求和

注意,这里还不是 learned embedding。

它只是测试:

随机向量求和,能不能保住 enough information?

compact 64D 的结果

64D compact run:

指标 数值
compact memory 324.84 MB
dense prior memory 6191.25 MB
memory reduction 19.06x
OOD step acc 0.9973
OOD route exact 0.9838
flat length-matrix OOD exact 0.0000
pilot pass false

这个结果非常有意思。

它不是全失败。

它做到了:

内存从 6.2GB 降到 325MB;
单步 route accuracy 仍然有 0.9973。

但是它没有做到:

整条 route exact >= 0.99。

为什么 step acc 很高,route exact 却不够?

因为一条 route 是多步决策。

比如从 root 走到 leaf,可能要:

right -> right -> left -> right -> stop

只要中间一步错,整条路径就错。

所以:

单步 0.997

听起来很好。

但多步相乘以后,整条路径 exact 会下降。

这就是 route 任务比普通分类更苛刻的地方。

compact 128D 为什么也没救回来

为了检查是不是维度太小,又跑了 128D:

指标 数值
compact memory 637.59 MB
memory reduction 9.71x
OOD step acc 0.9973
OOD route exact 0.9837
pilot pass false

128D 没有明显变好。

这说明问题大概率不是:

64D 太小,换 128D 就好。

更像是:

随机 token vector 求和这个 state 设计本身不够好。

随机向量求和会有几个问题:

  1. 碰撞
    不同 token 集合的向量和可能靠得太近。

  2. 噪声积累
    子堆越大,里面 token 越多,sum 里的无关噪声越多。

  3. 重复 token 不稳
    如果一句话里同一个 token 出现多次,query 在哪个位置会更难区分。

  4. 没有学习参考系
    随机向量没有被训练成适合 route 的坐标系。

所以 compact 失败不是坏消息。

它告诉我们下一步不能再用“随机向量求和”假装世界模型。

当前航行问题

现在的位置可以画成这样:

flat LxL route matrix
  -> 被降级:能恢复,但不是 TreeHeap

recursive geometry route
  -> 跑通:真的走树
  -> 被限制:读了几何答案 feature

content-aware dense route
  -> 跑通:真的读 arr[i] / left / right 内容
  -> 问题:6.2GB dense feature,不可持续

compact random-sum route
  -> 内存跑通:325MB
  -> 精度没过:OOD route_exact 0.9838 < 0.99

所以现在的航行问题不是:

GPU 没点燃。
io 不稳定。
TreeHeap route 不存在。

而是:

我们需要一种更好的 compact subheap state。

这句话很重要。

它把问题从基础设施和哲学争论,收束成一个工程/数学对象:

怎样让 arr[i] 在低维空间里保留足够的子堆可读信息?

为什么我认为下一步是 learned token/subheap embeddings

dense content state 像一本清单:

这个子堆里有 token A、B、C。

它很大,但清楚。

random-sum compact state 像把清单撕碎压成一团纸:

体积小了,但有些字看不清。

learned embedding 的目标是:

不是随机压缩,而是为了 route 任务学习一种压缩方式。

也就是让模型自己学:

哪些 token 维度对 left/right/stop 有用;
哪些组合容易混淆;
哪些子堆状态需要拉开;
哪些状态可以靠近。

这和 Transformer 里的 embedding / attention 参数学习有相似之处。

不是把随机向量硬塞进模型,而是让 loss 反过来调整坐标系。

对应到 TreeHeap,就是:

token embedding table      可学习
subheap compose kernel     可学习
route kernel               可学习
read/collapse kernel       可学习

但要注意,不能大锅炖。

至少下一步应该把任务拆开:

  1. 学 token embedding,让 query 和 subheap state 可比较。
  2. 学 subheap compose,让大子堆不只是 token sum。
  3. 学 route kernel,让它读 q, arr[i], arr[left], arr[right]
  4. 加 repeated-token stress test,防止模型只靠 token 唯一性过关。
  5. 加 pointer baseline,防止 TreeHeap claim 被普通指针模型追平。

当前 claim 应该怎么写

我建议现在只保留两个很克制的 claim。

第一个:

S1-CONTENT-ROUTE-C01 supported pilot

意思是:

TreeHeap route 可以读真实 subheap 内容,
而不是读几何答案 feature。

证据是 dense proof:

OOD route_exact = 0.9902
flat length-matrix OOD exact = 0.0000

第二个:

S1-COMPACT-CONTENT-ROUTE-C01 open / mixed pilot

意思是:

compact state 方向必要,
但 naive random token sum 还不够。

证据是 compact proof:

64D memory reduction = 19.06x
OOD route_exact = 0.9838
pilot_pass = false

这个 mixed 结果很宝贵。

因为它没有否定 TreeHeap route。

它否定的是一个过于简单的 state 设计。

这对 S1 / S2 意味着什么

S1 现在不应该急着冲翻译。

它下一步应该先回答:

TreeHeap 能否把 token 序列写成可读、可压缩、可学习的 subheap state?

如果这个问题不解决,S2 会遇到同样的问题:

graph builder / fold / translation 读到的 arr[i] 不是稳定语义结构,
而是一团压缩噪声。

所以 SPR-046 的航行方向是:

不要再证明“能不能走树”。
已经能走树了。

现在要证明“树上的 state 怎么学”。

这是从 route proof 进入 encoder/state proof。

下一步实验建议

我建议下一批不要开太大,先做四个对照:

A. learned token embedding

把固定随机 token vector 改成可学习表:

E[token_id] = learnable vector

目标:

OOD route_exact >= 0.99
memory <= 512MB

如果它过了,说明 loss 可以把 token 坐标系调成 route-friendly。

B. learned subheap compose

不要再用:

arr[i] = sum(children)

改成:

arr[i] = Compose(arr[left], arr[right])

Compose 可以先很小:

MLP([left, right])

或者更 TreeHeap 一点:

root/left/right slot kernel

目标是减少大子堆里的噪声积累。

C. repeated-token stress test

现在实验只用 unique-token query positions。

这太温柔了。

真实语言里会有:

the ... the ...
of ... of ...
that ... that ...

如果 query token 重复出现,模型必须知道:

我要找的是哪一个 token 实例,
而不只是这个 token 类型在哪个子堆里。

这会迫使 TreeHeap state 引入位置、路径、实例信息。

D. pointer baseline

必须加普通 pointer model。

否则我们不能说 TreeHeap route 有结构优势。

对照应该包括:

flat shared route
pointer network
small Transformer pointer
TreeHeap content route

如果 TreeHeap 赢,才有资格继续往 S2 接。

如果 TreeHeap 只是持平,也不是坏事。

那说明这个任务本身不够区分,需要设计更能暴露 address/path/subheap 归纳偏置的任务。

最后的小结

SPR-046 的结论是:

content-aware dense TreeHeap route 跑通了。
这是第一次真正读堆内容的 TreeHeap route proof。

但同时:

compact random-sum state 没过严格门槛。

所以当前航行问题不是“有没有路”。

而是:

路已经出现了;
船上的状态压缩系统还不够好。

下一步要做的不是更大的口号。

而是把 arr[i] 这个 TreeHeap state 从:

随机 token sum

升级到:

learned token/subheap embedding

这是 S1 接 S2 前必须过的一道门。

ARA: claims / experiments / dense evidence / compact evidence