Rules Made of Rules: A Composable Operator Library
This is a small, self-contained result out of a much longer continual-learning research program, so I’m writing it up on its own. The question: can a network summarize the rules behind its experience — rather than just fit them — so that a brand-new combination of known rules costs it zero new parameters to explain? The answer here is a clean, pre-registered yes, with a paired anti-cheat control and a robustness sweep to back it up. It also comes with an equally clean no: the moment the rules have to be learned from raw, per-trial-scrambled observations instead of a clean latent space, the whole thing collapses to chance. I’ll show both, with the numbers that killed and confirmed each claim, and six interactive demos so you can watch the mechanism instead of trusting my prose.
这是一个更大的持续学习研究项目里,一个足够独立、足够干净的小结果,所以单独写一篇。问题是:一个网络能不能总结出它的经验背后的规律——而不只是拟合它们——以至于一个从未见过的、已知规律的新组合,解释它不需要花一分新参数?这里的答案是一个干净的、预注册过的「是」,带一个配对的反作弊对照和一轮稳健性扫描撑腰。同时它也带着一个同样干净的「否」:一旦规律必须从原始的、逐次都被打乱过的观测里学出来,而不是从一个干净的潜空间里学,整件事就塌回随机水平。下面我会把两边都摆出来,附上杀死或确认每条主张的具体数字,外加六个可交互演示,让你能亲眼看机制运转,而不是只信我的文字。
1. Not “fit a function” — “summarize the rules”
Most of what deep nets do is approximate a function: give me enough $(x,y)$ pairs and I’ll fit $f$. That’s not what I wanted to test here. I wanted a setting where the right answer is a small set of reusable rules, and where the honest measure of success is: when a new situation is literally a known rule composed with another known rule, does the model need new capacity to explain it, or does it already have what it needs?
If a system has genuinely summarized “rule A” and “rule B” as distinct, reusable objects, then “first A, then B” should be free — it’s just running the two things you already have, in sequence. If instead the system has only learned to recognize twelve separate surface patterns, a thirteenth pattern (even one built cleanly from pieces it knows) is a cold start.
That’s the whole experiment. Everything below is in service of making that question answerable with a number instead of an intuition.
一、不是”拟合一个函数”——是”总结出规律”
深度网络大多数时候在做的事是逼近一个函数:给我足够多的 $(x,y)$ 对,我就能拟合出 $f$。这不是我想在这里检验的东西。我想要一个设定:正确答案是一小撮可复用的规律,而衡量成功的诚实标准是——当一个新情况本质上就是「已知规律 A 接已知规律 B」的时候,模型解释它需要新容量吗,还是它手里已经有了?
如果一个系统真的把”规律 A”和”规律 B”当成两个独立的、可复用的对象总结了出来,那”先 A 后 B”就应该是免费的——不过是把两个已经有的东西按顺序跑一遍。如果这个系统只是学会了识别十二种各自独立的表面模式,那第十三种模式(哪怕它是干净地由已知零件搭出来的)也是一次冷启动。
这就是整个实验。下面所有内容都是为了把这个问题变成一个数字,而不是一个直觉。
2. A world built from four moves
The world (neural_drift/comp_world.py, CompWorld) has 4 primitives — four basic transformations of a set of entities. It also has 12 compounds: every ordered pair of distinct primitives, $(a,b)$ with $a\neq b$, so $(0,2)$ and $(2,0)$ are different rules, applying $a$ then $b$ is not the same as $b$ then $a$.
One data point is a trajectory: 17 frames (16 steps plus the start), $N\in{3,4}$ entities, 16 dimensions each — shape (17, N, 16). Balancing is baked into the generator, not patched on afterward: every (rule × entity-count) cell gets an equal number of episodes, and episode order is shuffled before the model ever sees it — otherwise the index itself would leak the label, a mistake this project has made and re-caught more than once.
The critical design choice: training only ever shows depth-1 episodes — a single primitive, run for 16 steps. The 12 compounds never appear during training, not once. At test time the model gets a depth-2 trajectory and has to say which two primitives produced it, and in what order — a 12-way classification, chance $= 1/12 \approx 0.083$.
Play with the world below before you see any results — step through a primitive, then watch what a compound trajectory looks like next to it.
二、一个由四个动作搭出来的世界
这个世界(neural_drift/comp_world.py,CompWorld)有 4 条原语——对一组实体的四种基本变换。它还有 12 条复合体:所有不同原语的有序对 $(a,b)$,$a\neq b$,所以 $(0,2)$ 和 $(2,0)$ 是两条不同的规律,先 $a$ 后 $b$ 不等于先 $b$ 后 $a$。
一条数据是一段轨迹:17 帧(16 步加上初始帧),$N\in{3,4}$ 个实体,每个实体 16 维——形状 (17, N, 16)。配平是做进生成器里的,不是事后补的:每个(规律 × 实体数)格子拿到等量的 episode,而 episode 的顺序在模型看到之前就打乱了——不然下标本身就会泄漏标签,这个坑这个项目已经踩过、也已经抓出来过不止一次。
最关键的设计:训练自始至终只给深度 1 的 episode——单条原语,跑 16 步。这 12 条复合体在训练中一次都不出现。测试时模型拿到一段深度 2 的轨迹,要回答它是由哪两条原语、按什么顺序生成的——12 选 1,随机水平 $= 1/12 \approx 0.083$。
在看任何结果之前,先玩一下下面这个世界——单步播放一条原语,再看一条复合体轨迹在它旁边长什么样。
Four primitives, and the 12 ordered compounds built from them. Step through a trajectory and watch how a compound’s path through state space differs depending on which primitive runs first.
3. One operator, shared by every entity
The whole rule library is two matrices per primitive:
\[s' = \mathrm{normalize}\Big(\tanh\big(g\,(sW_p + \beta\,\overline{sV_p})\big)\Big)\]$s$ is the $(N,16)$ state of all entities, $p$ indexes the primitive, $g$ is a fixed gain, $\overline{sV_p}$ is the mean across entities of $sV_p$. That mean term is what makes the operator permutation-equivariant and entity-count-independent: the same $(W_p,V_p)$ works whether there are 3 entities or 300, because every entity only ever sees the average influence of the others, not a fixed-size list of them. normalize projects the state back onto the unit sphere after every step, which is what keeps 16 repeated applications of a nonlinear map from either blowing up or collapsing to a point.
Count the parameters: 4 primitives × 2 matrices × 16 × 16 = 2,048. That’s the entire rule library. There’s no separate “compound” module — a compound is just two primitive steps run back to back, with zero new parameters.
四条原语,以及由它们搭出的 12 条有序复合体。单步播放一条轨迹,看一条复合体的状态空间路径,会因为「哪条原语先跑」而长得不一样。
三、一个算子,所有实体共用
整个规律库就是每条原语两个矩阵:
\[s' = \mathrm{normalize}\Big(\tanh\big(g\,(sW_p + \beta\,\overline{sV_p})\big)\Big)\]$s$ 是所有实体的 $(N,16)$ 状态,$p$ 是原语下标,$g$ 是固定增益,$\overline{sV_p}$ 是 $sV_p$ 在实体维上取平均。正是这个均值项让算子置换等变且与实体数无关:同一套 $(W_p,V_p)$ 不管有 3 个实体还是 300 个实体都能跑,因为每个实体看到的永远只是其他实体的平均影响,而不是一份固定长度的列表。normalize 在每一步之后把状态重新投回单位球——正是这一步让一个非线性映射连续施加 16 次既不会炸也不会塌成一个点。
数一下参数:4 条原语 × 2 个矩阵 × 16 × 16 = 2048。这就是整个规律库。没有单独的”复合体”模块——一个复合体就是两次原语步骤前后接起来,不花一分新参数。
Watch a single learned operator step act on 3 entities, then on 8, with no change to the matrices. Permute the entity order and the output permutes with it — nothing else moves.
4. An algebra, not a lookup table
Two mathematical claims are doing the real work here, and both are checkable rather than aesthetic.
Closure. The set of primitives, closed under composition, forms an algebra: $F_{p_2}\circ F_{p_1}$ is itself a rule the system can run, using nothing but the primitives it already has. This is what licenses calling the 12 compounds “the same kind of object” as the 4 primitives rather than 12 unrelated new things to be memorized.
Non-commutativity. Because $(a,b)\neq(b,a)$ by construction, order has to matter for the model to succeed — this is a falsifiable prediction, not an assumption. A model that only identified which two primitives were present, with no signal about order, would already have narrowed the 12 candidates down to the correct unordered pair — but then has nothing to break the tie between the two orderings, so exact-match accuracy caps at $1/2$, not lower (checked by a 200,000-trial Monte Carlo: 0.5006). The results in §6 land well above that $1/2$ ceiling, which is what shows order is in fact being used, not just pair membership.
There’s a second, less obvious property worth naming: operators here can be executed and reordered independently of the training graph. Nothing in training ever ran $F_2\circ F_0$ — the two matrices for primitive 0 and primitive 2 were each updated only from their own single-primitive episodes. At test time those two independently-trained objects get composed for the first time, in an order neither ever saw during training, and the composition still means what it should. That’s a different thing from a function being factored into sub-computations inside one forward pass — I’ll come back to exactly why in §7.
看同一个学到的算子步骤先作用在 3 个实体上,再作用在 8 个实体上,矩阵本身没有任何改变。把实体顺序打乱,输出也跟着打乱——除此之外什么都不会变。
四、一个代数,不是一张查找表
这里真正起作用的是两条数学论断,而且都是可检验的,不是审美判断。
封闭性。 原语集合在复合运算下封闭,构成一个代数:$F_{p_2}\circ F_{p_1}$ 本身就是系统能跑的一条规律,用的完全是它已经有的原语。这正是”12 条复合体和 4 条原语属于同一类对象”这个说法的依据,而不是”12 个需要单独记住的新东西”。
不可交换。 因为构造上 $(a,b)\neq(b,a)$,模型要想成功,顺序就必须起作用——这是一个可证伪的预言,不是一个假设。一个只识别”出现了哪两条原语”、对顺序完全无感的模型,已经把 12 个候选缩小到了正确的那个无序对,但接下来没有任何信号能帮它在两种顺序之间做出选择,所以精确匹配准确率封顶在 $1/2$,不会更低(用 20 万次 Monte-Carlo 验证过:0.5006)。第六节的结果远高于这个 $1/2$ 的天花板,这才说明模型用到的确实是顺序,而不只是”这两条原语出现过”这个信息。
还有一条不那么显眼、但值得单独说的性质:这里的算子可以脱离训练图,独立地被执行、被重新排序。 训练中从来没有跑过 $F_2\circ F_0$——原语 0 和原语 2 各自的矩阵,只从各自的单原语 episode 里更新过。测试时,这两个各自独立训练出来的对象第一次被组合到一起,用的是训练中从未见过的顺序,而这个组合依然是对的。这和”一个函数在一次前向内部被拆成子计算”是不同的东西——第七节会精确说明区别在哪。
5. Inference is analysis-by-synthesis
There’s no classifier head that outputs “compound 7.” Recognition works by replaying every candidate and seeing which one matches:
Given a depth-2 trajectory, form all 12 candidate pairs $(a,b)$, $a\neq b$. For each one, take the trajectory’s starting state, roll it forward under $F_b\circ F_a$ (learned operators, unchanged from training), and measure the reconstruction error against the real trajectory. The candidate with the smallest error wins.
This is the concrete meaning of “recognizing a new rule costs zero new capacity.” The 12 candidates are not 12 things the model was trained to distinguish — they’re 12 replays of the same 2,048-parameter library, scored against reality. Adding a 13th, 100th, or 132nd compound (going to 12 primitives instead of 4) means adding more candidates to the search, never a new parameter to the model.
五、推理是分析-综合
这里没有一个输出”复合体 7”的分类头。识别的做法是重演每一个候选,看哪个对得上:
给定一段深度 2 的轨迹,构造全部 12 个候选对 $(a,b)$,$a\neq b$。对每一个候选,取轨迹的起始状态,在 $F_b\circ F_a$(训练时学到的算子,原封不动)下往前滚,把重演结果和真实轨迹比对误差。误差最小的候选获胜。
这就是”识别一条新规律不花新容量”的具体含义。这 12 个候选不是模型被训练去区分的 12 样东西——它们是同一个 2048 参数的规律库,被拿去和现实对了 12 次分。把候选从 12 个加到 100 个、132 个(原语从 4 条加到 12 条),加的是搜索里的候选数,从来不是模型里的新参数。
All 12 candidate compounds replayed against one held-out trajectory. Watch the reconstruction error bars settle — the true pair should pull ahead of the rest, including the one with the same two primitives in the wrong order.
6. The numbers
Exact decomposition of unseen compounds: 0.894 ± 0.051 (3 seeds), against chance 0.083 and a same-pipeline permutation null of 0.152 — the null matters here because Hungarian-style matching over 12 candidates isn’t automatically at chance, so the honest floor to beat isn’t $1/12$, it’s 0.152.
The number that actually earns the claim, though, is the paired anti-cheat control. COMPOSE_random runs the identical search — same 12 candidates, same replay-and-score procedure — but over a primitive library that was never trained, just randomly initialized:
| arm (3 seeds) | exact pair accuracy | permutation null | at-least-one-right |
|---|---|---|---|
| COMPOSE_learned | 0.894 ± 0.051 | 0.152 | 0.977 |
| COMPOSE_random (random primitives, same search) | 0.097 | 0.152 | 0.824 |
Paired difference +0.797 ± 0.066, win in 3/3 seeds; with only 3 seeds the 95% CI needs Student’s $t$ rather than the normal approximation ($t_{0.975,\,df=2}=4.303$), which gives a lower bound of +0.513 — still comfortably clear of zero. COMPOSE_random sits right at chance — meaning the win is entirely attributable to what the primitives learned, not to the fact that the model gets to search over 12 candidates. That distinction matters more than it might look: a system that could win this test purely by having a flexible enough search procedure would tell you nothing about whether it understood the primitives at all.
六、结果
未见复合体的精确分解:0.894 ± 0.051(3 seeds),对照随机水平 0.083 与同流水线的置换零分布 0.152——这里零分布很关键,因为对 12 个候选做匈牙利式匹配并不天然等于随机水平,所以要打赢的诚实地板不是 $1/12$,而是 0.152。
不过真正撑起这个主张的,是配对的反作弊对照。COMPOSE_random 跑的是完全相同的搜索——同样 12 个候选,同样的重演打分流程——但用的是一个从未训练过、只是随机初始化的原语库:
| 臂(3 seeds) | 精确分解准确率 | 置换零分布 | 至少猜对一个 |
|---|---|---|---|
| COMPOSE_learned | 0.894 ± 0.051 | 0.152 | 0.977 |
| COMPOSE_random(随机原语,同样的搜索) | 0.097 | 0.152 | 0.824 |
配对差 +0.797 ± 0.066,3/3 seeds 全赢;只有 3 个 seed 时,95%CI 该用 Student’s $t$ 而不是正态近似($t_{0.975,\,df=2}=4.303$),算出来的下界是 +0.513——依然远离零。COMPOSE_random 精确地落在随机水平——说明这个胜利完全归功于原语学到了什么,而不是”模型可以在 12 个候选里搜索”这件事本身。这个区分比看起来重要:一个仅靠”搜索足够灵活”就能通过这个测试的系统,完全不能说明它是否理解了原语。
Three aggregate bar views: the headline 0.894 result, COMPOSE_learned against COMPOSE_random (checkbox reveals the random-primitive bar), and the forgetting comparison against the no-library baseline. These are the same aggregate numbers as the tables in this section, not per-seed points — expand “show the numbers” in the demo for the raw figures.
One real held-out episode, ground truth $(0,2)$: replaying the correct candidate $(0,2)$ gives reconstruction error 0.06009; replaying the same two primitives in the wrong order, $(2,0)$, gives 0.11486 — almost double. The next-best wrong candidates aren’t close either: $(0,1)$ scores 0.11907, $(2,3)$ scores 0.12397, $(3,2)$ scores 0.12541. That’s the non-commutativity prediction from §4 landing on an actual example: getting the order wrong isn’t a rounding error, it’s a qualitatively worse fit, which is exactly what you’d expect from a model that learned dynamics rather than a model doing feature matching on which primitives are “present.” Script: experiments/exp_comp_single_episode.py, results: results/comp_single_episode_20260812_202703/results.json, reproduce with python experiments/exp_comp_single_episode.py — it’s a 12-line output, worth actually running.
Robustness. The learned operator’s functional form matches the world’s by construction, and it’s handed the world’s true nonlinearity gain $g=3.0$ — two gifts worth checking. Sweeping the model’s assumed gain away from the true value:
| model gain (world truth = 3.0) | seeds | exact decomposition | verdict |
|---|---|---|---|
| 1.5 (2× under-estimate) | 3 | 0.936 | better than matched |
| 3.0 (matched) | 3 | 0.894 | pass |
| 6.0 (2× over-estimate) | 8 | 0.361 | still degraded, but passes the gate (≥0.35) — paired diff vs random-primitive control +0.274, $t$-CI lower bound +0.186, 8/8 |
(The gain-6.0 row was rerun at 8 seeds after an independent audit caught that the original 3-seed run’s CI used the wrong distribution — see the correction below; the other two rows are still 3 seeds.) Under-estimating the nonlinearity is harmless, even slightly helpful. Over-estimating it is not: at gain 6.0, exact decomposition falls from 0.894 to 0.361, a drop of roughly 60%, even though it now clears the 0.35 gate rather than failing it as I originally reported. A too-powerful operator still eats a large share of the expressive burden that composition is supposed to carry — this shows up again as the central warning in §7, just not as a hard gate failure.
Forgetting. This next result comes from a sibling architecture in the same research line — a Lie-generator library (neural_drift/lgm.py, tested via experiments/exp_lgm_stream.py) rather than the tanh-operator library above — but it tests the same underlying structural claim: does having a shared, reusable rule library, on its own, resist catastrophic forgetting? Across a 4-stage stream, 3 seeds: a model with the shared library forgets 0.101 ± 0.016; an equivalent model fine-tuned with no shared library forgets 0.444 ± 0.010. Paired difference +0.343 ± 0.019, 3/3 seeds (0.379 / 0.317 / 0.333), $t$-CI lower bound (same $t_{0.975,\,df=2}=4.303$ correction as above) +0.263 — forgetting cut by 4–5×. Mechanistically this isn’t mysterious: new experience that lands on an existing generator costs zero new capacity and doesn’t overwrite old parameters, so interference stays inside the gate rather than spreading across the whole weight matrix.
Learning window. A parameter-free reason to prefer the compositional world over a plain linear-system world (OpWorld) I’d tried earlier: with 16 rules total (4 primitives + 12 compounds, chance 0.0625 for this separate probe), sweeping the nonlinearity gain and the multiplicative gate on/off, 3 seeds per row:
| gain | gate | closed-form escape hatch | analytic supervised | MLP ceiling | learning window |
|---|---|---|---|---|---|
| 1.0 | 0 | 0.478 | 0.522 | 0.610 | 0.133 |
| 3.0 | 0 | 0.428 | 0.494 | 0.558 | 0.130 |
| 3.0 | 1 | 0.414 | 0.488 | 0.568 | 0.155 |
| 6.0 | 1 | 0.343 | 0.407 | 0.497 | 0.154 |
Against the earlier plain-linear world (OpWorld: escape hatch 0.70, ceiling 0.77, window 0.07), the window here sits at 0.13–0.155 — roughly double, not the 3.5× I originally wrote (see the correction box below for why). That “closed-form escape hatch” column matters as its own discipline: this project has repeatedly found that a zero-training, closed-form spectral method matches or beats a trained model on synthetic dynamical-systems tasks, because pairwise second moments are an almost-sufficient statistic for identifying linear systems — no amount of observation-side nuisance breaks that. Composition is what finally makes the escape hatch lose its footing, because $F_b\circ F_a$ is not any single linear system.
These are 3-seed means, and the per-seed spread is large enough to matter — e.g. the gain=1.0 row’s escape hatch ranges 0.422–0.527 across its 3 seeds; see results/comp_learning_window_20260812_202810/results.json for the full per-seed breakdown. Don’t read a single seed’s window off this table and trust it.
Correction, caught while writing this up. The first version of this table had gain=3.0/gate=0 at window 0.251, and I’d written “widens the window 3.5×.” That number doesn’t reproduce. Two compounding mistakes: (a) partway through this work I added shared-nuisance-family support to
comp_world.py, and the newnuis_famdraw shifted the RNG consumption order — the same seed number silently stopped generating the same world; (b) the original sweep was single-seed, and the window turns out to be noisy enough across seeds that one draw isn’t a number, it’s a sample. Script:experiments/exp_comp_learning_window.py, results:results/comp_learning_window_20260812_202810/results.json. The table above is the 3-seed re-run; the 0.251 figure is retracted.
A methodological note this is worth stating plainly. A number computed in an ad-hoc exploration script isn’t data — it’s a scratch note. It only becomes citable once it has a checked-in script, a results.json, and a reproduce command someone else can run. This post has one number that failed exactly that test: the 0.251 above only got caught because I went back to attach a repro path for this write-up and the script wouldn’t reproduce it. If I hadn’t gone looking for the command, it would still be sitting in this post as fact. A second, independent audit caught a subtler version of the same discipline problem: every confidence interval in this post’s first draft used $\mathrm{mean}\pm1.96\cdot\mathrm{SEM}$, the normal-distribution formula — but $n=3$ isn’t large enough for that approximation, it needs Student’s $t$ ($t_{0.975,\,df=2}=4.303$, more than double the multiplier). $n=3$ is neither enough to compute a defensible CI nor enough to declare a gate passed or failed — which is exactly why the two figures in this post that moved under re-audit (the gain-6.0 robustness row just above, and the globally-fixed-nuisance tier in §8) got resolved by collecting more seeds rather than by arguing about which formula to use.
全部 12 个候选复合体对一段留出轨迹做重演。看重构误差条逐渐分开——真值那一对应该领先于其余的,也包括「同两条原语、顺序错了」的那一个。
三个聚合柱状图:主结果 0.894、COMPOSE_learned 对 COMPOSE_random(checkbox 用来显示随机原语那根柱子)、以及和无库基线的抗遗忘对比。这些都是和本节表格里一样的聚合数字,不是逐 seed 的散点——想看原始数字可以展开演示里的”展开原始数据”。
一个真实的留出 episode,真值 $(0,2)$:重演正确候选 $(0,2)$ 的重构误差是 0.06009;重演同样两条原语但顺序反过来 $(2,0)$,误差是 0.11486——几乎翻倍。次优的错误候选也不接近:$(0,1)$ 是 0.11907,$(2,3)$ 是 0.12397,$(3,2)$ 是 0.12541。这正是第四节”不可交换”那个预言落在一个具体样例上的样子:顺序错了不是舍入误差,是质的差别,这正是”学到了动力学”而不是”在比对哪些原语出现过”的模型该有的样子。脚本:experiments/exp_comp_single_episode.py,结果:results/comp_single_episode_20260812_202703/results.json,复现命令 python experiments/exp_comp_single_episode.py——输出只有十几行,值得亲自跑一遍。
稳健性。 学到的算子函数形式在构造上就和世界同型,而且拿到了世界真实的非线性增益 $g=3.0$——这是两份需要自查的礼物。把模型假设的增益从真值扫开:
| 模型增益(世界真值 = 3.0) | seeds | 精确分解 | 判定 |
|---|---|---|---|
| 1.5(低估 2×) | 3 | 0.936 | 比匹配时还好 |
| 3.0(匹配) | 3 | 0.894 | 通过 |
| 6.0(高估 2×) | 8 | 0.361 | 依然明显退化,但过了门槛(≥0.35)——对比随机原语对照配对差 +0.274,$t$-CI 下界 +0.186,8/8 |
(gain=6.0 这一行是在独立审计抓出原三 seed 结果的 CI 用错了口径之后,补到 8 seeds 重跑的——见下面的更正;另外两行仍是 3 seeds。)低估非线性强度是无害的,甚至略有帮助。高估则不然:增益 6.0 时精确分解从 0.894 掉到 0.361,跌了约六成,不过它现在过了 0.35 的门槛,不是我最初报告的”未过门槛”。一个太强的算子依然会吃掉相当一部分本该由组合承担的表达负担——第七节会再次遇到这条警告,只是不再是硬性的门槛失败。
抗遗忘。 下面这个结果来自同一条研究线里的姊妹架构——一个 Lie 生成元库(neural_drift/lgm.py,用 experiments/exp_lgm_stream.py 测的),而不是上面那个 tanh 算子库——但检验的是同一个结构性主张:拥有一个共享的、可复用的规律库,这件事本身,能不能抵抗灾难性遗忘?在一个 4 阶段流上,3 seeds:带共享库的模型遗忘 0.101 ± 0.016;一个等价的、没有共享库的微调模型遗忘 0.444 ± 0.010。配对差 +0.343 ± 0.019,3/3 seeds 全赢(0.379 / 0.317 / 0.333),$t$-CI 下界(用上面同一个 $t_{0.975,\,df=2}=4.303$ 校正)+0.263——遗忘量少了 4–5 倍。机制上这不难理解:新经验若落在已有的生成元上,占用的新容量是零,也不改写旧参数,所以干扰被限制在门控内部,不会散布到整个权重矩阵。
学习窗口。 有一个无自由参数的理由,说明为什么这个组合世界比我早先试过的纯线性系统世界(OpWorld)更值得用:一共 16 条规律(4 原语 + 12 复合体,这个独立探针的随机水平是 0.0625),扫非线性增益、以及乘性门控开/关,每行 3 seeds:
| 增益 | 门控 | 闭式逃生舱 | 解析有监督 | MLP 天花板 | 学习窗口 |
|---|---|---|---|---|---|
| 1.0 | 0 | 0.478 | 0.522 | 0.610 | 0.133 |
| 3.0 | 0 | 0.428 | 0.494 | 0.558 | 0.130 |
| 3.0 | 1 | 0.414 | 0.488 | 0.568 | 0.155 |
| 6.0 | 1 | 0.343 | 0.407 | 0.497 | 0.154 |
对比更早的、纯线性的世界(OpWorld:逃生舱 0.70、天花板 0.77、窗口 0.07),这里的窗口落在 0.13–0.155——约宽一倍,不是我最初写的 3.5 倍(原因见下面的更正框)。”闭式逃生舱”这一列本身是一条独立的纪律:这个项目反复发现,一个零训练的闭式谱方法在合成动力系统任务上会打平甚至打赢训练出来的模型,因为成对的二阶矩几乎是识别线性系统的充分统计量——观测端加多少 nuisance 都打不倒它。组合是第一个真正让这个逃生舱失去立足点的东西,因为 $F_b\circ F_a$ 不是任何单一的线性系统。
这些是 3 seeds 的均值,而单 seed 的窗口噪声大到不能忽略——比如 gain=1.0 那一行,逃生舱在 3 个 seed 上的取值范围是 0.422–0.527;完整的逐 seed 明细见 results/comp_learning_window_20260812_202810/results.json。不要从这张表里读出单个 seed 的窗口就当真。
写这篇文章时抓到的更正。 这张表最初的版本里,gain=3.0/gate=0 那一行窗口是 0.251,我原来写的是”把学习窗口拉宽 3.5 倍”。那个数字复现不了。两个叠加的失误:(a) 写这篇文章的过程中,我给
comp_world.py加了共享 nuisance 族的支持,新加的nuis_fam抽样改变了 RNG 的消耗顺序——同一个 seed 编号已经悄悄不再生成同一个世界;(b) 原来的扫描是单 seed 的,而这个窗口跨 seed 的噪声大到一次抽样根本不能当一个数字用。脚本:experiments/exp_comp_learning_window.py,结果:results/comp_learning_window_20260812_202810/results.json。上面这张表是 3 seeds 重跑后的结果;0.251 那个数字撤回。
一条值得写给读者的方法论。 一个在临时探索脚本里跑出来的数字不算数据——它是一张草稿纸。只有当它有一个入库的脚本、一份 results.json、以及一条别人能跑通的复现命令时,它才有资格被引用。这篇文章里恰好有一个数字没通过这个检验:上面那个 0.251,正是在我回头给这篇文章补复现路径时,脚本跑不出同样的数才被抓出来的。如果我没有回去找那条命令,它现在还会原样躺在这篇文章里当事实用。第二次、独立的审计抓到了同一种纪律问题的一个更隐蔽的版本:这篇文章初稿里所有的置信区间都用的是 $\mathrm{mean}\pm1.96\cdot\mathrm{SEM}$,这是正态分布的公式——但 $n=3$ 根本不够用这个近似,该用 Student’s $t$($t_{0.975,\,df=2}=4.303$,是前者乘数的两倍还多)。$n=3$ 既不够算出一个站得住的置信区间,也不够判定一个门槛过没过——这正是为什么本文里被重新审计翻过的两处数字(上面这行 gain=6.0 的稳健性结果、以及第八节里全局固定 nuisance 那一档)最终是靠补 seed 解决的,而不是靠争论该用哪个公式。
7. Where this sits next to KAN
Kolmogorov-Arnold Networks reparametrize a layer so every edge carries a learnable 1-D function instead of a scalar weight:
\[f(x_1,\dots,x_n)\approx\sum_q \Phi_q\Big(\sum_p \phi_{q,p}(x_p)\Big)\]The pitch is that composing simple, interpretable 1-D functions can approximate complex multivariate functions with fewer, more legible parameters than a dense layer. It’s a genuinely useful idea, and it’s the closest existing thing to what I’m doing here — close enough that the differences are worth being precise about.
| KAN | this work | |
|---|---|---|
| composes what | simple functions | simple operators (state transitions) |
| composed where | inside one forward pass’s computation graph | across time, as function composition |
| approximates | a static function $y=f(x)$ | a transition operator $F_p: s_t\to s_{t+1}$ |
| reorderable at test time | not natively | yes — train $F_0,F_2$ separately, at test time run $F_2\circ F_0$, or even $F_0\circ F_2\circ F_0\circ F_3$ |
| does the composition carry meaning | no — it’s just two layers | yes — $F_2\circ F_0$ means “rule 0 happened, then rule 2” |
Three things are genuinely shared, and I don’t want to erase them: both approaches break a model’s knowledge into small atomic pieces instead of one dense tensor; both let composition generate far more distinct functions than the parameter count would suggest; both carry the same inductive bias — that the underlying primitives are meant to be reused, over and over, rather than relearned per task.
The short version: KAN asks how a single function factors internally. This work asks how a set of functions in a rule-based system forms an algebra and gets composed, again and again, across time. The second question buys you one thing the first doesn’t have to answer for: composition that carries semantics — $F_2\circ F_0$ isn’t just “layer 2 applied after layer 0,” it’s a claim about what happened in the world.
七、这个工作和 KAN 的关系
Kolmogorov-Arnold Networks(KAN)把一层重参数化成:每条边携带一个可学习的一维函数,而不是一个标量权重:
\[f(x_1,\dots,x_n)\approx\sum_q \Phi_q\Big(\sum_p \phi_{q,p}(x_p)\Big)\]它的主张是:组合简单、可解释的一维函数,能用比 dense 层更少、更可读的参数逼近复杂的多元函数。这是一个真正有用的想法,也是现有工作里离我这里做的东西最近的一个——近到值得把差异说精确。
| KAN | 本工作 | |
|---|---|---|
| 组合什么 | 简单函数 | 简单算子(状态转移) |
| 在哪组合 | 一次前向内部的计算图 | 跨时间,作为函数复合 |
| 逼近对象 | 静态函数 $y=f(x)$ | 转移算子 $F_p: s_t\to s_{t+1}$ |
| 测试时能否重排 | 不天然支持 | 可以——单独训练 $F_0,F_2$,测试时直接拼 $F_2\circ F_0$,甚至 $F_0\circ F_2\circ F_0\circ F_3$ |
| 组合有语义吗 | 没有——就是两层而已 | 有——$F_2\circ F_0$ 的意思是”先发生了规律 0,再发生了规律 2” |
有三样东西是两边真正共享的,我不想把它们抹掉:都把模型的知识拆成小的原子零件,而不是一个巨大的 dense 张量;都让组合能产生远多于参数量所暗示的不同函数;都带着同一个归纳偏置——底层的基础模块本该被反复复用,而不是每个任务都重新学一遍。
一句话版本:KAN 问的是一个函数内部如何因子分解。本工作问的是一个规律系统里的函数集合如何形成一个代数,并且在时间上被反复组合。 第二个问题多买到一样东西,是第一个问题不需要回答的:带语义的组合——$F_2\circ F_0$ 不只是”第 2 层接在第 0 层后面”,它是一句关于世界里发生了什么的断言。
Play with the two side by side below — same underlying idea of atomic reusable pieces, different thing being composed and a different place the composition happens.
下面把两者摆在一起玩一下——同一个”原子可复用零件”的底层想法,但组合的对象不同,组合发生的地方也不同。
Left: a KAN edge, a learnable 1-D function composed inside one layer’s forward pass. Right: an operator, composed across time steps, reorderable after training. Toggle to see the same “atomic, reusable piece” framing applied to two different objects.
A future direction, with a warning that’s already backed by data. The natural next step is Operator-KAN: make each $F_p$ itself a KAN, $s_j’ = \sum_i \phi^{(p)}_{ij}(s_i)$, giving two nested layers of composition — micro-composition of 1-D functions inside one operator, macro-composition of operators across time. That’s an appealing architecture on paper. But §6’s robustness sweep is already a data point against doing this carelessly: when I gave the operator too much expressive power (model gain 6.0 against a true gain of 3.0), exact decomposition fell from 0.894 to 0.361 — a roughly 60% drop. What happens mechanistically is that $F_0$ becomes flexible enough to quietly represent some of what should only be representable by $F_2\circ F_0$ — it starts absorbing the compound’s behavior into a single primitive, which destroys the very identifiability that made the primitives distinct, reusable objects in the first place. A KAN-ified operator is, almost by definition, a more expressive operator. Build one, and check for exactly this failure mode before trusting the result.
左:一条 KAN 边,一个在单层前向内部被组合的可学习一维函数。右:一个算子,在时间步之间被组合,训练后还能重新排序。切换看同一个”原子可复用零件”的框架被套在两个不同的对象上。
一个未来方向,外加一条已经有数据背书的警告。 自然的下一步是 Operator-KAN:把每个 $F_p$ 本身也 KAN 化,$s_j’ = \sum_i \phi^{(p)}_{ij}(s_i)$,形成两层嵌套的组合——一个算子内部是一维函数的 micro composition,时间之间是算子的 macro composition。这在纸面上是个吸引人的架构。但第六节的稳健性扫描已经是一个反对”不加小心就这么做”的数据点:当我给算子太多表达力(模型增益 6.0,对真值增益 3.0)时,精确分解从 0.894 掉到 0.361——跌了约六成。机制上发生的事是:$F_0$ 变得足够灵活,足以偷偷表示一部分本该只能由 $F_2\circ F_0$ 表示的东西——它开始把复合体的行为吸收进单个原语里,而这恰恰破坏了让原语一开始就成为独立、可复用对象的那个可辨识性。一个 KAN 化的算子,几乎按定义就是一个更有表达力的算子。真做出来之后,先检查这个失效模式,再相信结果。
8. The wall, honestly
Everything in §6 has one precondition I haven’t flagged yet: the model sees a clean latent state directly. That’s a real gift, and I want to show exactly what happens when it’s taken away, rather than footnote it.
Switch to learning end-to-end from raw observations that have been scrambled, and exact decomposition drops to 0.077 — chance. Not “worse.” Chance. To see why, I ran a three-tier ladder, changing nothing but how the scrambling works:
| nuisance setting | seeds | exact decomposition of unseen compounds | vs random-primitive control (paired) |
|---|---|---|---|
| none (clean latent, §6’s result) | 3 | 0.894 ± 0.051 | +0.797 ± 0.066, $t$-CI low +0.513, 3/3 |
| globally fixed — one shared rotation for every episode | 8 | 0.233 ± 0.040 | +0.154 ± 0.037, $t$-CI low +0.067, 8/8 |
| per-episode random — a fresh rotation every episode | 3 | 0.077 ± 0.008 | +0.001 ± 0.016 (exactly chance) |
Monotone, and the middle tier is significantly above random — but still short of the gate. The paired win over the random-primitive control is real down to the $t$-corrected lower bound (8 seeds this time, rerun after an audit found the original 3-seed CI used the wrong distribution), and the per-seed spread is wide — 0.135 to 0.458 across the 8 seeds. But 0.233 is still below the 0.35 accuracy threshold GATE-R requires, so the honest read is “clearly better than nothing, not a pass”: composition compounds the encoder’s error across 2 primitives × 16 steps, so it demands far more representational precision than single-step prediction ever did, and this tier doesn’t reach it.
The math behind the collapse: the raw observation is $x=[s;d]Q_g^\top$, where $Q_g$ is a random rotation applied to the true state plus distractor dimensions — and in the “per-episode random” tier, $Q_g$ is different every single episode. A fixed encoder cannot undo a rotation that changes every time it’s asked to undo it. The only thing that can undo it is an invariant (something like a Gram matrix, which is blind to the rotation) — but the invariant throws away exactly the frame information composition needs to work with. This isn’t a failure of model capacity or training budget. Under this nuisance design, the end-to-end task is mathematically unsolvable for any fixed encoder. That’s a strong claim, so it’s worth being precise: it isn’t “the model didn’t manage to learn it,” it’s “there is no fixed function from these observations to a usable frame.”
八、诚实地说说那道墙
第六节的一切都有一个我还没标出来的前提:模型直接看到一个干净的潜状态。这是一份真实的礼物,我想把拿掉它之后发生的事精确地摆出来,而不是塞进一条脚注。
换成从被打乱过的原始观测端到端地学,精确分解掉到 0.077——随机水平。不是”变差”,是随机。为了搞清楚原因,我跑了一个三级阶梯,其余一切不变,只改打乱的方式:
| nuisance 设定 | seeds | 未见复合体的精确分解 | vs 随机原语对照(配对) |
|---|---|---|---|
| 无(干净潜态,第六节的结果) | 3 | 0.894 ± 0.051 | +0.797 ± 0.066,$t$-CI 下界 +0.513,3/3 |
| 全局固定——所有 episode 共用一个旋转 | 8 | 0.233 ± 0.040 | +0.154 ± 0.037,$t$-CI 下界 +0.067,8/8 |
| 逐 episode 随机——每个 episode 换一次旋转 | 3 | 0.077 ± 0.008 | +0.001 ± 0.016(精确等于随机) |
单调,而且中间档显著高于随机——但仍够不到门槛。它对随机原语对照的配对胜利是真实的,$t$ 校正后的下界依然为正(这次是 8 seeds,因为审计发现原来 3 seed 的 CI 用错了分布才补跑的),而且逐 seed 跨度很大——8 个 seed 里从 0.135 到 0.458 都有。但 0.233 依然低于 GATE-R 要求的 0.35 准确率门槛,所以诚实的说法是”明显好于什么都不做,但没有通过”:组合会把编码器的误差沿 2 条原语 × 16 步累乘放大,所以它对表示精度的要求远高于单步预测,而这一档没能达到。
塌缩背后的数学:原始观测是 $x=[s;d]Q_g^\top$,其中 $Q_g$ 是施加在真实状态加干扰维上的一个随机旋转——而在”逐 episode 随机”这一档,$Q_g$ 每一条 episode 都不一样。一个固定的编码器,不可能撤销一个每次被要求撤销时都在变的旋转。 唯一能撤销它的是一个不变量(类似 Gram 矩阵这种对旋转不敏感的量)——但不变量恰好丢掉了组合所需要的那个标架信息。这不是模型容量不够、也不是训练预算不够。在这种 nuisance 设计下,端到端任务对任何固定编码器而言,数学上就是无解的。 这是一个很强的断言,所以值得说精确:不是”模型没能学会”,而是”从这些观测到一个可用标架,根本不存在这样一个固定函数”。
The same operator library, the same composition search, three different nuisance regimes. Slide between them and watch exact decomposition fall off a cliff between “fixed” and “per-episode” — that’s the exact point where a fixed encoder stops being able to undo the rotation at all.
For what it’s worth, I tried the theoretically-motivated fix — a shared finite family of $M=4$ nuisance rotations, with $M$ encoder heads and per-episode hard selection of the best-fitting one — and it made things worse, not better (0.094 at 3 seeds, vs the single-fixed-rotation tier’s 0.233 at 8 seeds — that 0.094 hasn’t been re-audited at higher $n$, so take it as directionally right rather than precise). The joint discrete inference (which head, which primitive) collapsed early in training: hard selection dumped most episodes onto one head almost immediately and never recovered. Identifiability in theory (“the family can in principle be told apart”) is not the same thing as optimizability in practice. That’s this project’s cleanest recent lesson, and it belongs here rather than buried in a lab notebook.
同一个算子库,同一套组合搜索,三种不同的 nuisance 设定。在它们之间滑动,看精确分解在”固定”和”逐 episode”之间掉下悬崖——那正是固定编码器彻底丧失撤销旋转能力的那个点。
顺带一提,我试过一个理论上讲得通的修法——一个共享的、大小 $M=4$ 的 nuisance 有限族,配 $M$ 个编码头,逐 episode 硬选择拟合最好的那一个——结果更差,不是更好(3 seeds 下 0.094,对比单一固定旋转档 8 seeds 下的 0.233——这个 0.094 还没在更大的 $n$ 下重新审计过,姑且当方向性正确、不当精确数字看)。联合的离散推断(选哪个头、选哪条原语)在训练早期就崩了:硬选择几乎立刻把大部分 episode 塞给了同一个头,之后再也没能翻身。理论上的可辨识(”这个族原则上是能被分辨的”)不等于实践中的可优化。这是这个项目最近最干净的一条教训,它该摆在这里,而不是埋在实验笔记里。
9. Where this leaves things
The result I’d keep from this post, stated at the size it deserves: a small, shared library of composable operators can summarize a set of rules well enough that a brand-new, unseen composition of two known rules is recognized correctly 89% of the time, at zero new parameters — and this is not the search procedure winning by itself, since the identical search over a random library sits at chance. That’s a real answer to the question in §1.
The result I’d hold with more caution: the moment the composition has to be learned from observations rather than handed a clean latent space, the specific way nuisance enters the data decides whether the problem is hard or literally unsolvable. That’s not a soft “needs more data” caveat — the per-episode-random tier isn’t underfit, it’s mathematically closed off to any fixed encoder. The globally-fixed tier shows the door isn’t shut in general, just narrower than I’d like, and finding the right way to make nuisance learnably identifiable across episodes — without the joint-discrete-inference collapse from §8 — is the open problem this post leaves on the table.
Scope, so the two paragraphs above don’t outrun what was actually measured: 4 primitives, 16-dimensional state, entity counts of 3–4, 3 seeds per arm, one synthetic world. I have no evidence yet that any of this holds at the primitive counts or state dimensionalities where it would actually matter for a real system. The forgetting number in §6 is from a related but distinct architecture, not the exact operator library described in §3 — I’ve tried to be clear about that inline rather than letting the two blur together.
九、目前站在哪里
这篇文章里我愿意留下的结果,按它应得的分量说出来:一个小的、共享的可组合算子库,能把一组规律总结得足够好,以至于一个全新的、从未见过的、由两条已知规律组成的复合体,能以 89% 的准确率被正确识别,且不花一分新参数——而且这不是搜索程序自己赢的,因为对一个随机库做一模一样的搜索,结果精确落在随机水平。 这是对第一节那个问题的一个真实回答。
我愿意更谨慎地对待的结果:一旦组合必须从观测里学出来,而不是被直接喂进一个干净的潜空间,nuisance 进入数据的具体方式,决定了这个问题是「难」还是字面意义上的「无解」。 这不是一句软绵绵的”需要更多数据”的 caveat——逐 episode 随机那一档不是欠拟合,它对任何固定编码器而言,数学上就是被封死的。全局固定那一档说明这扇门在一般意义上并没有关死,只是比我希望的窄——而怎样让 nuisance 能跨 episode 可学习地被辨识出来、同时又不撞上第八节那种联合离散推断的崩溃,是这篇文章留在桌上的那个开放问题。
说清楚范围,免得上面两段说过了头:4 条原语,16 维状态,3–4 个实体,每个 arm 3 个 seed,一个合成世界。我目前没有任何证据说明,这一切在真正对一个实际系统有意义的原语数量或状态维度下依然成立。第六节的抗遗忘数字来自一个相关但不同的架构,不是第三节描述的那个算子库本身——我尽量在正文里就地说清楚这一点,而不是让两者混在一起。