knowledge: learning kernel — model the estimator bias, not just sampling

Revisiting Layer 1 against Layer 1.5 (and Riis 2026, arXiv:2604.08554):
neutral Wright-Fisher is a null that BOTH neural architectures deviate
from, in opposite directions. Add a learning kernel to the refit step,
p_{t+1} = T_theta(counts/n), with two population-genetics knobs -- reset u
(mutation toward a prior = smoothing) and temperature tau (sharpening =
mode-competition) -- both identity by default, so the histogram bridge and
all 68 scientific-validation/correctness tests are unchanged.

Result: neutral drift fails both neural models, oppositely.
- VAE regime (n=6000, K=30): neutral drift is inert (no collapse), yet the
  real VAE collapsed to one mode. Sharpening tau=0.8 reproduces it -- the
  estimator ADDS collapse pressure.
- RNN regime (n=200, K=256): neutral drives H->0, but the real RNN only
  partially collapses. Mutation u=0.006 reproduces the H-floor -- the
  estimator REMOVES collapse pressure. Honest caveat: uniform-mutation
  overshoots the RNN's forward-KL, evidence its smoothing prior is
  truth-like, not uniform (future refinement).

This mechanistically explains the architecture-generality result and the
softened neural g*, and develops the estimator axis Riis names as future
work. New: knowledge/kernel.py, configs/layer1/kernel_{sharpen,smooth}.yaml,
figures/plot_kernel.py (overlays analytic arms vs committed neural
endpoints), READMEs, tests/test_kernel.py (+6, 105 total green). Strategic
Riis positioning recorded in CLAUDE.md: concede "collapse=drift" as prior
art; lead with recombination, the kernel axis, and the Lamarckian society.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Giorgio Gilestro 2026-07-05 10:23:33 +01:00
parent 79bbc45f41
commit 871bc39ec6
21 changed files with 660 additions and 3 deletions

View file

@ -76,6 +76,10 @@ E4's whole purpose is to isolate the effect of teacher **decorrelation ρ**, so
**Finding (2026-07-05, real-MNIST `mnist_collapse`) — collapse and grounding-rescue reproduce on real images.** External-validity tier: a small **convolutional VAE** (the canonical generative-collapse model) is retrained each generation on its own generated digits. Modes = digit class × stroke-thickness bin (K=30, Zipf, ~18 tail modes); the oracle is a **frozen CNN + deterministic thickness** at **98.5% mode accuracy** (its 30×30 confusion matrix is recorded in the manifest as the measurement floor). Result (4 reps): the **dry (g=0) lineage collapses to a single mode** — forward-KL 0.5→18, support 30→1, tail truth-mass 1.0→0.06, H→0 — while **10% grounding holds all 30 modes** (KL≈0.6, full tail, H≈0.9). The VAE needs ~10% grounding here vs the synthetic histogram's ~5%, consistent with the `grounding` finding that trained neural models need more grounding than the exact operator. **Confirmation-only (signs, not magnitudes; blueprint §3.5)** — the exact synthetic oracle stays the quantitative anchor. `figures/mnist_montage.py` is an eyeball diagnostic (re-runs a short dry lineage; NOT a parquet figure). Build gates passed: CNN mode accuracy 98.5%; VAE gen-0 recovers full 30/30 support (over-smooths frequencies, KL≈0.5, no prior hole — unlike the *synthetic*-codeword VAE, which is why the MNIST VAE works where that one didn't). The MNIST tier is heavy (torchvision `--extra mnist`, downloads MNIST, ~5 min): `make mnist`, kept out of the `make neural` loop. **Finding (2026-07-05, real-MNIST `mnist_collapse`) — collapse and grounding-rescue reproduce on real images.** External-validity tier: a small **convolutional VAE** (the canonical generative-collapse model) is retrained each generation on its own generated digits. Modes = digit class × stroke-thickness bin (K=30, Zipf, ~18 tail modes); the oracle is a **frozen CNN + deterministic thickness** at **98.5% mode accuracy** (its 30×30 confusion matrix is recorded in the manifest as the measurement floor). Result (4 reps): the **dry (g=0) lineage collapses to a single mode** — forward-KL 0.5→18, support 30→1, tail truth-mass 1.0→0.06, H→0 — while **10% grounding holds all 30 modes** (KL≈0.6, full tail, H≈0.9). The VAE needs ~10% grounding here vs the synthetic histogram's ~5%, consistent with the `grounding` finding that trained neural models need more grounding than the exact operator. **Confirmation-only (signs, not magnitudes; blueprint §3.5)** — the exact synthetic oracle stays the quantitative anchor. `figures/mnist_montage.py` is an eyeball diagnostic (re-runs a short dry lineage; NOT a parquet figure). Build gates passed: CNN mode accuracy 98.5%; VAE gen-0 recovers full 30/30 support (over-smooths frequencies, KL≈0.5, no prior hole — unlike the *synthetic*-codeword VAE, which is why the MNIST VAE works where that one didn't). The MNIST tier is heavy (torchvision `--extra mnist`, downloads MNIST, ~5 min): `make mnist`, kept out of the `make neural` loop.
**Finding (2026-07-05, learning kernel) — neutral drift is a null both real models fail, oppositely; the estimator bias is a signed operator.** Layer-1 extension (`knowledge/kernel.py`, `LearningKernelCfg`): the refit becomes `p_{t+1} = T_θ(counts/n)` with two pop-gen knobs — **reset `u`** (mutation toward a prior = smoothing) and **temperature `τ`** (sharpening = mode-competition) — both identity at their defaults, so the histogram bridge and every scientific-validation test are unchanged (68 core tests still green). Result: **neutral WrightFisher fails both neural architectures, in opposite directions.** VAE regime (`n=6000, K=30`): neutral drift is *inert* (no collapse), yet the real VAE collapsed to one mode — **sharpening `τ=0.8` reproduces it** (the estimator ADDS collapse). RNN regime (`n=200, K=256`): neutral drives `H→0`, but the real RNN only partially collapses — **mutation `u=0.006` reproduces the `H`-floor** (the estimator REMOVES collapse). Honest caveat: uniform-mutation matches the RNN `H`-floor but overshoots its forward-KL (~5 vs ~2), evidence the RNN's smoothing prior is *truth-like, not uniform* (future refinement). Configs `configs/layer1/kernel_{sharpen,smooth}.yaml`, figure `plot_kernel.py`. This mechanistically explains the architecture-generality result and the softened neural `g*`.
**Strategic positioning vs Riis 2026 (arXiv:2604.08554, "Drift and selection in LLM text ecosystems").** Riis independently formalizes **collapse = WrightFisher drift** (his Thm 1) with n-gram agents: minority-mass martingale, rare-first extinction, single-token dropout ≈ αe^{α}, de Bruijn-polytope fixed points, plus descriptive-vs-normative *selection* (Thm 2). **Concede as prior art:** "collapse is literally WrightFisher", the martingale, rare-first loss, the WF/effective-population formalism — cite him; do **not** frame these as our contribution. **Crucial distinction that protects us:** his "mixed environment" *retains the lineage's own old synthetic tokens* — there is **no injection of fresh real data from a fixed `p*`**, so his headline is *pessimistic* (Thm 1c: extinction is independent of α — retention only changes speed). Our **grounding is immigration from a non-drifting external truth**, giving a stationary `H_eq>0` and a critical `g*≪1` that *prevents* collapse — the mechanism his closed loop lacks. **Our defensible novelty, ranked:** (1) **recombination + "merge, don't average" conservation law** (E4) — he has no model-merging operator; flagship; (2) **the learning-kernel / estimator-bias axis** — he *explicitly names it as future work*; we now build+measure it; (3) grounding threshold (solid anchor, but immigrationdrift balance is classic — not a flagship); (4) architecture-generality in real weights + MNIST; (5) **the Lamarckian society + the vertical/cumulative C3 claim — wholly ours, not yet run.** Reposition the paper from *"collapse is drift"* (now contested) to **a population-genetic *control theory* for sustaining open-ended knowledge**: drift is the diagnosed disease (cite Riis), our contribution is the engineered remedies and their integration.
## Build order (blueprint §7) — respect the gate ## Build order (blueprint §7) — respect the gate
1. Scaffold: repo layout (§5), container, pytest skeleton, config system, seeding utils. `make test` green. 1. Scaffold: repo layout (§5), container, pytest skeleton, config system, seeding utils. `make test` green.

View file

@ -15,8 +15,8 @@ env-mnist: ## add torchvision for the real-MNIST confirmation tier
test: ## correctness tests + scientific-validation tests (the spine of trust) test: ## correctness tests + scientific-validation tests (the spine of trust)
uv run pytest uv run pytest
layer1: ## run experiments E1-E6 layer1: ## run experiments E1-E6 + the learning-kernel bridge (analytic)
for e in E1 E2 E3 E4 E5 E6; do uv run python -m knowledge.experiment configs/layer1/$$e.yaml; done for e in E1 E2 E3 E4 E5 E6 kernel_sharpen kernel_smooth; do uv run python -m knowledge.experiment configs/layer1/$$e.yaml; done
neural: ## run Layer 1.5 synthetic neural experiments (excludes the heavy MNIST tier) neural: ## run Layer 1.5 synthetic neural experiments (excludes the heavy MNIST tier)
for c in configs/neural/*.yaml; do case "$$c" in *mnist*) ;; \ for c in configs/neural/*.yaml; do case "$$c" in *mnist*) ;; \

View file

@ -0,0 +1,29 @@
experiment: kernel_sharpen
kind: lineage
seed: 20260705
n_replicates: 24
# (Learning-kernel bridge, pro-collapse arm): does neutral Wright-Fisher explain the VAE's
# collapse on MNIST? NO -- and that is the point. This matches the MNIST regime (K=30, n=6000,
# Zipf) where drift is nearly inert: neutral (temperature=1.0) barely moves (H stays ~H*, ~all
# modes alive), yet the real VAE collapsed to a SINGLE mode (results/mnist_collapse). Adding the
# estimator's sharpening / mode-competition (temperature<1: p ~ p^(1/tau)) reproduces the
# catastrophic collapse. tau=0.8 is calibrated to reproduce collapse-to-one-mode. This is the
# axis Riis (2026) names as future work: the estimator, not the sampling, drives VAE collapse.
truth: {K: 30, R: 1, tail: zipf, zipf_s: 1.5, tail_threshold: 0.01, init: truth}
dynamics:
n: 6000 # huge vs K=30 -> neutral drift is essentially inert
grounding: {m: 0, policy: proportional}
kernel: {reset: 0.0, temperature: 1.0, floor: 0.0}
generations: 15
metrics: {kl_floor: 1.0e-9, support_eps: 1.0e-9}
sweep:
- param: dynamics.kernel.temperature
values: [1.0, 0.8] # neutral (no collapse) vs sharpened (catastrophic collapse)
output: {dir: results/kernel_sharpen}

View file

@ -0,0 +1,32 @@
experiment: kernel_smooth
kind: lineage
seed: 20260705
n_replicates: 24
# (Learning-kernel bridge, anti-collapse arm): neutral Wright-Fisher OVER-predicts the RNN's
# collapse. This matches the RNN grounding regime (K=256, n=200, Zipf): neutral (reset=0) drives
# H all the way to 0, but the real RNN only PARTIALLY collapses -- H plateaus ~0.68 of a possible
# 0.88, forward-KL plateaus ~2 (does not diverge), ~half the tail stays alive (results/grounding).
# The estimator's smoothing / regularisation supplies a diversity FLOOR. A mutation-toward-prior
# knob (reset=u: p <- (1-u)p + u*uniform) reproduces the H-floor. reset=0.006 is calibrated to the
# RNN's stationary dry H. Honest caveat carried in the write-up: uniform-mutation matches the
# H-floor but overshoots forward-KL (analytic ~6 vs RNN ~2), evidence the RNN's smoothing target
# is TRUTH-LIKE, not uniform -- a refinement for future work. The sign, though, is unambiguous:
# the estimator here REMOVES collapse pressure (opposite to the VAE's sharpening).
truth: {K: 256, R: 1, tail: zipf, zipf_s: 1.3, tail_threshold: 0.001, init: truth}
dynamics:
n: 200
grounding: {m: 0, policy: proportional}
kernel: {reset: 0.0, temperature: 1.0, floor: 0.0}
generations: 100 # long enough to show neutral -> 0 vs smoothed -> floor clearly
metrics: {kl_floor: 1.0e-9, support_eps: 1.0e-9}
sweep:
- param: dynamics.kernel.reset
values: [0.0, 0.006] # neutral (H -> 0) vs smoothed (H floors, like the RNN)
output: {dir: results/kernel_smooth}

121
figures/plot_kernel.py Normal file
View file

@ -0,0 +1,121 @@
"""`kernel` figure — the learning kernel: neutral drift fails both neural models, oppositely.
The Layer-1.5 architecture-generality result gets a mechanistic explanation. Neutral Wright-Fisher
(the histogram / Riis baseline) is the null; a real learner adds an estimator bias that can point
either way. Two regimes, each falsifying neutral drift in the OPPOSITE direction, each repaired by
one knob of the learning kernel:
* **VAE regime (n=6000, K=30):** drift is nearly inert neutral holds ~all modes yet the real
VAE collapsed to a single mode. Sharpening (temperature<1) reproduces it. The estimator ADDS
collapse pressure.
* **RNN regime (n=200, K=256):** neutral drives H to 0, but the real RNN only partially collapses
(H floors, forward-KL plateaus). Mutation-toward-prior (reset>0) reproduces the floor. The
estimator REMOVES collapse pressure.
Analytic arms are read from results/kernel_{sharpen,smooth}; the neural reference endpoints
(dashed) are read from the committed results/mnist_collapse and results/grounding parquets so the
figure is a pure function of committed artifacts.
Usage: python figures/plot_kernel.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, savefig # noqa: E402
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from knowledge.metrics import heterozygosity # noqa: E402
from knowledge.truth import make_true_distribution # noqa: E402
def _mean_traj(df, knob, val, col):
sub = df[df[knob] == val]
s = sub.groupby("generation")[col].mean()
return s.index.to_numpy(), s.to_numpy()
def _neural_dry(results_dir, col, stationary_frac=0.0):
"""Mean of ``col`` on the dry (g=0) neural arm — endpoint, or stationary tail if frac>0."""
df, _ = load_bundle(results_dir)
dry = df[df["g"] == 0.0]
if stationary_frac > 0:
dry = dry[dry["generation"] >= int(dry["generation"].max() * (1 - stationary_frac))]
return float(dry[col].mean())
return float(dry[dry["generation"] == dry["generation"].max()][col].mean())
def main() -> None:
sh, sh_cfg = load_bundle("results/kernel_sharpen")
sm, sm_cfg = load_bundle("results/kernel_smooth")
Hstar_sh = heterozygosity(make_true_distribution(
sh_cfg["truth"]["K"], 1, "zipf", 0.5, sh_cfg["truth"]["zipf_s"], 0,
tail_threshold=sh_cfg["truth"]["tail_threshold"]).p_star)
Hstar_sm = heterozygosity(make_true_distribution(
sm_cfg["truth"]["K"], 1, "zipf", 0.5, sm_cfg["truth"]["zipf_s"], 0,
tail_threshold=sm_cfg["truth"]["tail_threshold"]).p_star)
# Neural reference endpoints (dashed) from the committed neural runs.
vae_H = _neural_dry("results/mnist_collapse", "heterozygosity")
vae_sup = _neural_dry("results/mnist_collapse", "support_size")
rnn_H = _neural_dry("results/grounding", "heterozygosity", stationary_frac=0.4)
rnn_KL = _neural_dry("results/grounding", "forward_kl", stationary_frac=0.4)
fig, axes = plt.subplots(2, 2, figsize=(13, 9))
NEU, KER = "#1f77b4", "#d62728"
# --- VAE regime: sharpening ---
ax = axes[0, 0]
for val, c, lab in [(1.0, NEU, "neutral (τ=1)"), (0.8, KER, "sharpened (τ=0.8)")]:
g, y = _mean_traj(sh, "temperature", val, "heterozygosity")
ax.plot(g, y, "-o", color=c, ms=3, label=lab)
ax.axhline(Hstar_sh, ls=":", color="gray", lw=1, label="$H^*$")
ax.axhline(vae_H, ls="--", color="#2ca02c", lw=1.3, label=f"real VAE (dry): {vae_H:.2f}")
ax.set(xlabel="generation", ylabel="heterozygosity $H$",
title="VAE regime ($n$=6000, $K$=30): neutral drift is inert;\nsharpening collapses (like the VAE)")
ax.legend(frameon=False, fontsize=8)
ax = axes[0, 1]
for val, c, lab in [(1.0, NEU, "neutral (τ=1)"), (0.8, KER, "sharpened (τ=0.8)")]:
g, y = _mean_traj(sh, "temperature", val, "support_size")
ax.plot(g, y, "-o", color=c, ms=3, label=lab)
ax.axhline(vae_sup, ls="--", color="#2ca02c", lw=1.3, label=f"real VAE (dry): {vae_sup:.0f}")
ax.set(xlabel="generation", ylabel="distinct modes alive",
title="Support: neutral holds ~all; sharpening → 1 mode")
ax.legend(frameon=False, fontsize=8)
# --- RNN regime: smoothing ---
ax = axes[1, 0]
for val, c, lab in [(0.0, NEU, "neutral (u=0)"), (0.006, KER, "smoothed (u=0.006)")]:
g, y = _mean_traj(sm, "reset", val, "heterozygosity")
ax.plot(g, y, "-", color=c, lw=1.8, label=lab)
ax.axhline(Hstar_sm, ls=":", color="gray", lw=1, label="$H^*$")
ax.axhline(rnn_H, ls="--", color="#2ca02c", lw=1.3, label=f"real RNN (dry): {rnn_H:.2f}")
ax.set(xlabel="generation", ylabel="heterozygosity $H$",
title="RNN regime ($n$=200, $K$=256): neutral → 0;\nsmoothing floors $H$ (like the RNN)")
ax.legend(frameon=False, fontsize=8)
ax = axes[1, 1]
for val, c, lab in [(0.0, NEU, "neutral (u=0)"), (0.006, KER, "smoothed (u=0.006)")]:
g, y = _mean_traj(sm, "reset", val, "forward_kl")
ax.plot(g, y, "-", color=c, lw=1.8, label=lab)
ax.axhline(rnn_KL, ls="--", color="#2ca02c", lw=1.3, label=f"real RNN (dry): {rnn_KL:.1f}")
ax.set(xlabel="generation", ylabel=r"forward-KL $D(p^*\Vert p)$",
title="Forward-KL: neutral diverges; smoothing plateaus\n(overshoots RNN → prior is truth-like, not uniform)")
ax.legend(frameon=False, fontsize=8)
fig.suptitle("learning kernel — neutral WrightFisher fails both neural models, oppositely: "
"the estimator sharpens (VAE) or smooths (RNN)", y=1.0, fontsize=12)
fig.tight_layout()
for d in ("results/kernel_sharpen", "results/kernel_smooth"):
savefig(fig, d, "kernel")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,42 @@
# kernel — the learning kernel: why real learners deviate from neutral drift
*(This legend covers both `results/kernel_sharpen/` and `results/kernel_smooth/`; the figure
`kernel.png` is written into both.)*
**Claim tested.** Neutral WrightFisher drift (the histogram bridge, and the baseline of Riis
2026) is the *null* model of collapse. But Layer 1.5 showed real trained models deviate from it —
and in **opposite directions**. Can a single extension of Layer 1 — a parameterized *learning
kernel* on the refit step, `p_{t+1} = T_θ(counts/n)` — reproduce both deviations, and does neutral
drift genuinely fail without it?
**Setup.** The kernel (`knowledge/kernel.py`) has two population-genetics knobs, both reducing to
neutral drift at their defaults (so the histogram and every scientific-validation test are
unchanged): **reset `u`** — mutation toward a prior (`p ← (1u)p + u·π`), i.e. smoothing; and
**temperature `τ`** — sharpening (`p ∝ p^{1/τ}`, `τ<1` concentrates), i.e. mode-competition. Two
matched-to-neural regimes, 24 replicates each.
### The four panels (`kernel.png`; blue = neutral, red = kernel-on, green dashed = the real neural model)
**Top row — VAE regime (`n=6000`, `K=30`), pro-collapse:**
1. **Heterozygosity.** Neutral drift is **inert** — at `n=6000` it barely moves (`H` stays at
`H*`). Yet the real VAE (green) collapsed to `H≈0`. **Sharpening (`τ=0.8`) reproduces the
collapse.** Neutral drift is *falsified*; the estimator's mode-competition is required.
2. **Support.** Neutral holds ~all 30 modes; sharpening → 1 mode, matching the VAE.
**Bottom row — RNN regime (`n=200`, `K=256`), anti-collapse:**
3. **Heterozygosity.** Neutral drift drives `H → 0`, but the real RNN (green) only *partially*
collapses (`H` floors at ~0.68). **Mutation `u=0.006` reproduces the floor.** The estimator here
*removes* collapse pressure.
4. **Forward-KL.** Neutral diverges; smoothing plateaus. Honest caveat: uniform-mutation plateaus
*above* the RNN's KL (~5 vs ~2) — evidence the RNN's smoothing target is **truth-like, not
uniform** (a refinement for future work). The *sign* is unambiguous.
### Takeaway
Model collapse in real learners = **neutral drift ⊕ an architecture-specific estimator-bias
operator that can point either way.** The histogram sits at the neutral null (`u=0, τ=1`); the VAE
**sharpens** (adds collapse); the RNN/MLP **smooth** (add a diversity floor). This mechanistically
explains the Layer-1.5 architecture-generality result and the softened neural `g*`, and develops
the exact axis Riis (2026) names as future work ("different smoothing schemes… each induce their
own fixed-point geometry… a natural direction for further work"). `u`/`τ` are calibrated from a
single neural diagnostic and pinned in the configs. **Falsifier (not triggered):** if neutral drift
had already reproduced the neural curves, the estimator axis would be superfluous — instead it fails
in *both* regimes, oppositely.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

View file

@ -0,0 +1,14 @@
{
"experiment": "kernel_sharpen",
"master_seed": 20260705,
"git_commit": "79bbc45f41822757e60d7f1994a82c5171a9e554",
"python": "3.14.5",
"libraries": {
"numpy": "2.5.0",
"scipy": "1.18.0",
"pandas": "3.0.3",
"pyarrow": "24.0.0"
},
"rows": 768,
"results_sha256": "d5651a841d58f12abed1339476b1839fb03fe02329e38d675d0602ccb5691e52"
}

View file

@ -0,0 +1,82 @@
experiment: kernel_sharpen
seed: 20260705
n_replicates: 24
source_config:
experiment: kernel_sharpen
kind: lineage
seed: 20260705
n_replicates: 24
truth:
K: 30
R: 1
tail: zipf
zipf_s: 1.5
tail_threshold: 0.01
init: truth
dynamics:
n: 6000
grounding:
m: 0
policy: proportional
kernel:
reset: 0.0
temperature: 1.0
floor: 0.0
generations: 15
metrics:
kl_floor: 1.0e-09
support_eps: 1.0e-09
sweep:
- param: dynamics.kernel.temperature
values:
- 1.0
- 0.8
output:
dir: results/kernel_sharpen
grid:
- label:
temperature: 1.0
lineage_cfg:
truth:
K: 30
R: 1
tail: zipf
zipf_s: 1.5
tail_threshold: 0.01
init: truth
dynamics:
n: 6000
grounding:
m: 0
policy: proportional
kernel:
reset: 0.0
temperature: 1.0
floor: 0.0
generations: 15
metrics:
kl_floor: 1.0e-09
support_eps: 1.0e-09
- label:
temperature: 0.8
lineage_cfg:
truth:
K: 30
R: 1
tail: zipf
zipf_s: 1.5
tail_threshold: 0.01
init: truth
dynamics:
n: 6000
grounding:
m: 0
policy: proportional
kernel:
reset: 0.0
temperature: 0.8
floor: 0.0
generations: 15
metrics:
kl_floor: 1.0e-09
support_eps: 1.0e-09

View file

@ -0,0 +1,42 @@
# kernel — the learning kernel: why real learners deviate from neutral drift
*(This legend covers both `results/kernel_sharpen/` and `results/kernel_smooth/`; the figure
`kernel.png` is written into both.)*
**Claim tested.** Neutral WrightFisher drift (the histogram bridge, and the baseline of Riis
2026) is the *null* model of collapse. But Layer 1.5 showed real trained models deviate from it —
and in **opposite directions**. Can a single extension of Layer 1 — a parameterized *learning
kernel* on the refit step, `p_{t+1} = T_θ(counts/n)` — reproduce both deviations, and does neutral
drift genuinely fail without it?
**Setup.** The kernel (`knowledge/kernel.py`) has two population-genetics knobs, both reducing to
neutral drift at their defaults (so the histogram and every scientific-validation test are
unchanged): **reset `u`** — mutation toward a prior (`p ← (1u)p + u·π`), i.e. smoothing; and
**temperature `τ`** — sharpening (`p ∝ p^{1/τ}`, `τ<1` concentrates), i.e. mode-competition. Two
matched-to-neural regimes, 24 replicates each.
### The four panels (`kernel.png`; blue = neutral, red = kernel-on, green dashed = the real neural model)
**Top row — VAE regime (`n=6000`, `K=30`), pro-collapse:**
1. **Heterozygosity.** Neutral drift is **inert** — at `n=6000` it barely moves (`H` stays at
`H*`). Yet the real VAE (green) collapsed to `H≈0`. **Sharpening (`τ=0.8`) reproduces the
collapse.** Neutral drift is *falsified*; the estimator's mode-competition is required.
2. **Support.** Neutral holds ~all 30 modes; sharpening → 1 mode, matching the VAE.
**Bottom row — RNN regime (`n=200`, `K=256`), anti-collapse:**
3. **Heterozygosity.** Neutral drift drives `H → 0`, but the real RNN (green) only *partially*
collapses (`H` floors at ~0.68). **Mutation `u=0.006` reproduces the floor.** The estimator here
*removes* collapse pressure.
4. **Forward-KL.** Neutral diverges; smoothing plateaus. Honest caveat: uniform-mutation plateaus
*above* the RNN's KL (~5 vs ~2) — evidence the RNN's smoothing target is **truth-like, not
uniform** (a refinement for future work). The *sign* is unambiguous.
### Takeaway
Model collapse in real learners = **neutral drift ⊕ an architecture-specific estimator-bias
operator that can point either way.** The histogram sits at the neutral null (`u=0, τ=1`); the VAE
**sharpens** (adds collapse); the RNN/MLP **smooth** (add a diversity floor). This mechanistically
explains the Layer-1.5 architecture-generality result and the softened neural `g*`, and develops
the exact axis Riis (2026) names as future work ("different smoothing schemes… each induce their
own fixed-point geometry… a natural direction for further work"). `u`/`τ` are calibrated from a
single neural diagnostic and pinned in the configs. **Falsifier (not triggered):** if neutral drift
had already reproduced the neural curves, the estimator axis would be superfluous — instead it fails
in *both* regimes, oppositely.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

View file

@ -0,0 +1,14 @@
{
"experiment": "kernel_smooth",
"master_seed": 20260705,
"git_commit": "79bbc45f41822757e60d7f1994a82c5171a9e554",
"python": "3.14.5",
"libraries": {
"numpy": "2.5.0",
"scipy": "1.18.0",
"pandas": "3.0.3",
"pyarrow": "24.0.0"
},
"rows": 4848,
"results_sha256": "8e50c084f8b3ca707375e677a977c00ac36aa96b3363ebecefad2d96a9912f3f"
}

View file

@ -0,0 +1,82 @@
experiment: kernel_smooth
seed: 20260705
n_replicates: 24
source_config:
experiment: kernel_smooth
kind: lineage
seed: 20260705
n_replicates: 24
truth:
K: 256
R: 1
tail: zipf
zipf_s: 1.3
tail_threshold: 0.001
init: truth
dynamics:
n: 200
grounding:
m: 0
policy: proportional
kernel:
reset: 0.0
temperature: 1.0
floor: 0.0
generations: 100
metrics:
kl_floor: 1.0e-09
support_eps: 1.0e-09
sweep:
- param: dynamics.kernel.reset
values:
- 0.0
- 0.006
output:
dir: results/kernel_smooth
grid:
- label:
reset: 0.0
lineage_cfg:
truth:
K: 256
R: 1
tail: zipf
zipf_s: 1.3
tail_threshold: 0.001
init: truth
dynamics:
n: 200
grounding:
m: 0
policy: proportional
kernel:
reset: 0.0
temperature: 1.0
floor: 0.0
generations: 100
metrics:
kl_floor: 1.0e-09
support_eps: 1.0e-09
- label:
reset: 0.006
lineage_cfg:
truth:
K: 256
R: 1
tail: zipf
zipf_s: 1.3
tail_threshold: 0.001
init: truth
dynamics:
n: 200
grounding:
m: 0
policy: proportional
kernel:
reset: 0.006
temperature: 1.0
floor: 0.0
generations: 100
metrics:
kl_floor: 1.0e-09
support_eps: 1.0e-09

View file

@ -11,6 +11,8 @@ from __future__ import annotations
from dataclasses import dataclass, field, replace from dataclasses import dataclass, field, replace
from typing import Any, Mapping, Optional from typing import Any, Mapping, Optional
from .kernel import LearningKernelCfg
# Runtime-tunable knobs live here, defaults chosen to match the blueprint's illustrative # Runtime-tunable knobs live here, defaults chosen to match the blueprint's illustrative
# schema. Nothing here is a magic number buried in algorithm code. # schema. Nothing here is a magic number buried in algorithm code.
@ -61,6 +63,7 @@ class DynamicsCfg:
grounding: GroundingCfg = field(default_factory=GroundingCfg) grounding: GroundingCfg = field(default_factory=GroundingCfg)
selection: SelectionCfg = field(default_factory=SelectionCfg) selection: SelectionCfg = field(default_factory=SelectionCfg)
remint: RemintCfg = field(default_factory=RemintCfg) remint: RemintCfg = field(default_factory=RemintCfg)
kernel: LearningKernelCfg = field(default_factory=LearningKernelCfg)
@dataclass(frozen=True) @dataclass(frozen=True)
@ -89,6 +92,7 @@ class LineageCfg:
grounding=_sub(dyn_raw.get("grounding", {}), GroundingCfg), grounding=_sub(dyn_raw.get("grounding", {}), GroundingCfg),
selection=_sub(dyn_raw.get("selection", {}), SelectionCfg), selection=_sub(dyn_raw.get("selection", {}), SelectionCfg),
remint=_sub(dyn_raw.get("remint", {}), RemintCfg), remint=_sub(dyn_raw.get("remint", {}), RemintCfg),
kernel=_sub(dyn_raw.get("kernel", {}), LearningKernelCfg),
) )
metrics = _sub(cfg.get("metrics", {}), MetricsCfg) metrics = _sub(cfg.get("metrics", {}), MetricsCfg)
return LineageCfg( return LineageCfg(

87
src/knowledge/kernel.py Normal file
View file

@ -0,0 +1,87 @@
"""The learning kernel — the estimator/inductive-bias operator (Layer-1 extension).
Neutral Wright-Fisher models the generational step as *resample and refit the raw empirical
distribution* (``p_{t+1} = counts/n``). A real learner does not refit the raw histogram: it
applies a **biased estimator** it smooths (regularises toward a simpler distribution) and it
can sharpen (concentrate mass, drop weakly-supported modes). Layer 1.5 measured exactly these
biases: the RNN/MLP *over-smooth* (H resists collapse, spurious tail support stays alive), the
VAE *self-reinforces* (accelerating collapse to a single mode). This module turns the refit into
a parameterised kernel ``p_{t+1} = T_θ(counts/n)`` so the analytic core can reproduce those
deviations the axis Riis (2026) names as future work.
Two population-genetics knobs, both reducing to the neutral null at their defaults:
* **reset ``u``** mutation toward a prior: ``p <- (1-u)·p + u·π``. Models smoothing /
regularisation. Gives a diversity floor (H stops collapsing to 0), keeps rare modes alive, and
softens the grounding threshold the RNN/MLP signature.
* **temperature ``τ`` + floor ``ε``** sharpening / support pruning: ``p p^{1/τ}`` then drop
mass below ``ε``. Models the winner-take-all mode competition of a mode-dropping generator
the VAE signature. ``τ<1`` sharpens (pro-collapse).
Identity (``u=0, τ=1, ε=0``) recovers Layer 1 exactly, so the histogram bridge and every
scientific-validation test are unchanged.
"""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True)
class LearningKernelCfg:
"""Estimator-bias knobs for the generational refit (all defaults = neutral Wright-Fisher).
Attributes:
reset (float): Mutation rate ``u`` toward the prior (smoothing). 0 = off.
temperature (float): Sharpening temperature ``τ`` (``p p^{1/τ}``); <1 sharpens
(mode competition), >1 flattens, 1 = off.
floor (float): Hard support threshold ``ε``: modes below it are dropped. 0 = off.
prior (str): Smoothing target for ``reset``: currently ``uniform``.
"""
reset: float = 0.0
temperature: float = 1.0
floor: float = 0.0
prior: str = "uniform"
@property
def is_identity(self) -> bool:
"""True when the kernel is the neutral refit (recovers Layer 1 exactly)."""
return self.reset == 0.0 and self.temperature == 1.0 and self.floor == 0.0
def _prior_vector(kind: str, K: int) -> np.ndarray:
"""Return the smoothing-target distribution of length ``K``."""
if kind == "uniform":
return np.full(K, 1.0 / K)
raise ValueError(f"unknown kernel prior {kind!r} (expected uniform)")
def apply_kernel(p: np.ndarray, cfg: LearningKernelCfg) -> np.ndarray:
"""Apply the estimator-bias kernel to a refit distribution.
Composition order: mutation toward the prior (keeps modes alive) -> sharpening (concentrates
mass) -> support floor (drops weak modes). Each step is a no-op at its default.
Args:
p (np.ndarray): The raw refit distribution ``counts/n`` (length ``K``, sums to 1).
cfg (LearningKernelCfg): The kernel knobs.
Returns:
np.ndarray: The estimator's distribution ``p_{t+1}`` (length ``K``, sums to 1).
"""
p = np.asarray(p, dtype=float)
if cfg.is_identity:
return p
K = p.size
if cfg.reset > 0.0: # mutation toward the prior (smoothing)
p = (1.0 - cfg.reset) * p + cfg.reset * _prior_vector(cfg.prior, K)
if cfg.temperature != 1.0: # sharpening / flattening
with np.errstate(divide="ignore"):
p = np.power(p, 1.0 / cfg.temperature)
if cfg.floor > 0.0: # hard support pruning
p = np.where(p < cfg.floor, 0.0, p)
total = p.sum()
return p / total if total > 0 else np.full(K, 1.0 / K)

View file

@ -66,6 +66,7 @@ def run_lineage(cfg: Mapping[str, Any] | LineageCfg, seed: int) -> pd.DataFrame:
regions=regions, regions=regions,
selection_mode=cfg.dynamics.selection.mode, selection_mode=cfg.dynamics.selection.mode,
novelty_alpha=cfg.dynamics.selection.novelty_alpha, novelty_alpha=cfg.dynamics.selection.novelty_alpha,
kernel=cfg.dynamics.kernel,
) )
remint = cfg.dynamics.remint remint = cfg.dynamics.remint

View file

@ -7,10 +7,12 @@ in the perspective paper is one operator here; they compose in the order below.
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass, field
import numpy as np import numpy as np
from .kernel import LearningKernelCfg, apply_kernel
@dataclass(frozen=True) @dataclass(frozen=True)
class StepCtx: class StepCtx:
@ -24,6 +26,8 @@ class StepCtx:
regions (np.ndarray): Length-K region index per item. regions (np.ndarray): Length-K region index per item.
selection_mode (str): ``none`` | ``greedy`` | ``qd``. selection_mode (str): ``none`` | ``greedy`` | ``qd``.
novelty_alpha (float): QD novelty exponent (0 recovers greedy). novelty_alpha (float): QD novelty exponent (0 recovers greedy).
kernel (LearningKernelCfg): Estimator-bias kernel applied to the refit (default
identity = neutral Wright-Fisher).
""" """
n: int n: int
@ -32,6 +36,7 @@ class StepCtx:
regions: np.ndarray regions: np.ndarray
selection_mode: str = "none" selection_mode: str = "none"
novelty_alpha: float = 0.0 novelty_alpha: float = 0.0
kernel: LearningKernelCfg = field(default_factory=LearningKernelCfg)
def _normed(p: np.ndarray) -> np.ndarray: def _normed(p: np.ndarray) -> np.ndarray:
@ -166,6 +171,7 @@ def generation_step(teachers: list[np.ndarray], p_star_eff: np.ndarray,
cfg.regions, cfg.policy, rng) cfg.regions, cfg.policy, rng)
counts = c_syn + c_real counts = c_syn + c_real
p_next = counts / counts.sum() p_next = counts / counts.sum()
p_next = apply_kernel(p_next, cfg.kernel) # (estimator bias)
p_next = apply_selection(p_next, p_star_eff, # (selection) p_next = apply_selection(p_next, p_star_eff, # (selection)
cfg.selection_mode, cfg.novelty_alpha) cfg.selection_mode, cfg.novelty_alpha)
return p_next return p_next

View file

@ -245,8 +245,33 @@ C3 vertical claim deferred.*
confirmed on real images. VAE needs ~10% grounding vs synthetic ~5% (cf. the `grounding` finding). confirmed on real images. VAE needs ~10% grounding vs synthetic ~5% (cf. the `grounding` finding).
**99 tests green** (+5 torchvision-gated). `make mnist` / `make env-mnist` (kept out of `make neural`). **99 tests green** (+5 torchvision-gated). `make mnist` / `make env-mnist` (kept out of `make neural`).
**2026-07-05 — learning kernel (Layer-1 extension) + Riis positioning.**
- Prompted by revisiting Layer 1 vs 1.5 and the Riis 2026 paper (arXiv:2604.08554). Added
`knowledge/kernel.py` (`LearningKernelCfg`: reset `u` = smoothing, temperature `τ` = sharpening,
floor `ε`), wired into `step.generation_step` (post-refit) / `StepCtx` / `DynamicsCfg` — **identity
by default, so the 68 Layer-1 scientific-validation + correctness tests are unchanged.**
- **Result:** neutral WrightFisher fails BOTH neural models, oppositely. VAE regime (n=6000,K=30):
neutral is inert, sharpening `τ=0.8` reproduces the collapse-to-one-mode. RNN regime (n=200,K=256):
neutral → H=0, mutation `u=0.006` reproduces the H-floor (~0.68). Uniform-mutation overshoots the
RNN's KL → its prior is truth-like, not uniform (honest caveat, future refinement).
- `configs/layer1/kernel_{sharpen,smooth}.yaml`, `figures/plot_kernel.py` (overlays analytic arms vs
the committed neural endpoints), READMEs, `tests/test_kernel.py` (+6). Wired into `make layer1`.
- **Strategic (see CLAUDE.md finding):** concede "collapse=drift" to Riis (prior art; cite); his
mixed environment retains OLD SYNTHETIC (no real-data injection) → pessimistic, no g* that prevents
collapse. Our defensible novelty: recombination "merge-don't-average" (flagship), the learning-kernel
axis (he flags as future work), grounding-as-immigration, real-weights+MNIST breadth, and the
Lamarckian society + vertical/cumulative C3 claim (not yet run). Reposition: from "collapse is drift"
to a population-genetic CONTROL THEORY for sustaining open-ended knowledge.
## Remaining (all optional) ## Remaining (all optional)
- [ ] **Learning-kernel refinement:** truth-like smoothing prior (`prior="truth"`) + measurement floor
for a quantitative RNN match; **multi-locus / linkage** modes (class×style) as the rigorous home for
recombination. Both enrich predictive power and separate us further from Riis's single-locus n-grams.
- [ ] **The Lamarckian society experiments** (multi-agent grounding + decorrelated specialists +
recombination + QD-selection + re-mint) and the **vertical/cumulative C3 claim** — the highest-ceiling,
wholly-novel frame; not yet entered.
- [ ] **`region_matched`** grounding (R>1), **`remint`** re-mint gate (optional). - [ ] **`region_matched`** grounding (R>1), **`remint`** re-mint gate (optional).
- [ ] **VAE fidelity:** fix the prior-hole mismatch (KL-annealing / free-bits / larger latent) so it - [ ] **VAE fidelity:** fix the prior-hole mismatch (KL-annealing / free-bits / larger latent) so it
clears the gen-0 gate, then add to `architectures`. Or document as a known limitation. clears the gen-0 gate, then add to `architectures`. Or document as a known limitation.

72
tests/test_kernel.py Normal file
View file

@ -0,0 +1,72 @@
"""Learning-kernel tests (pure NumPy).
The kernel is an additive Layer-1 extension: identity by default (so the neutral Wright-Fisher
core and every scientific-validation test are unchanged), a sharpening knob that ADDS collapse
where neutral drift is inert, and a smoothing knob that supplies a diversity FLOOR where neutral
drift would collapse to zero. These assert exactly those three behaviours.
"""
from __future__ import annotations
import numpy as np
from knowledge.kernel import LearningKernelCfg, apply_kernel
from knowledge.lineage import run_lineage
from knowledge.metrics import heterozygosity
def test_apply_kernel_identity_is_noop():
p = np.array([0.5, 0.3, 0.15, 0.05])
assert np.array_equal(apply_kernel(p, LearningKernelCfg()), p)
def test_apply_kernel_reset_mixes_toward_uniform():
p = np.array([1.0, 0.0, 0.0, 0.0])
out = apply_kernel(p, LearningKernelCfg(reset=0.2))
assert np.isclose(out.sum(), 1.0)
assert np.allclose(out, [0.8 + 0.2 / 4, 0.05, 0.05, 0.05]) # keeps dead modes alive
assert (out > 0).all()
def test_apply_kernel_sharpen_and_floor_prune():
p = np.array([0.6, 0.3, 0.09, 0.01])
sharp = apply_kernel(p, LearningKernelCfg(temperature=0.5)) # p^2, renormalised
assert sharp[0] > p[0] and sharp[-1] < p[-1] # mass concentrates
floored = apply_kernel(p, LearningKernelCfg(floor=0.05))
assert floored[-1] == 0.0 and np.isclose(floored.sum(), 1.0) # weak mode dropped
def test_kernel_identity_leaves_lineage_unchanged():
base = {"truth": {"K": 64, "init": "truth"}, "dynamics": {"n": 200},
"generations": 20, "metrics": {"kl_floor": 1e-9, "support_eps": 1e-9}}
withk = {**base, "dynamics": {"n": 200, "kernel": {"reset": 0.0, "temperature": 1.0}}}
a = run_lineage(base, seed=3)["heterozygosity"].to_numpy()
b = run_lineage(withk, seed=3)["heterozygosity"].to_numpy()
assert np.array_equal(a, b) # identity kernel == neutral Wright-Fisher, bitwise
def test_sharpening_adds_collapse_where_neutral_is_inert():
# Large n vs small K: neutral drift barely collapses; sharpening drives it to ~1 mode.
cfg = {"truth": {"K": 30, "tail": "zipf", "zipf_s": 1.5, "tail_threshold": 1e-2,
"init": "truth"},
"dynamics": {"n": 6000, "grounding": {"m": 0}},
"generations": 15, "metrics": {"kl_floor": 1e-9, "support_eps": 1e-9}}
neutral = run_lineage(cfg, seed=0)
sharp = run_lineage({**cfg, "dynamics": {**cfg["dynamics"],
"kernel": {"temperature": 0.8}}}, seed=0)
assert neutral["support_size"].iloc[-1] > 20 # neutral: ~all modes survive
assert sharp["support_size"].iloc[-1] <= 3 # sharpening: collapse to a point
assert sharp["heterozygosity"].iloc[-1] < 0.1
def test_smoothing_floors_diversity_where_neutral_collapses():
# Small n vs large K: neutral drift drives H toward 0; smoothing holds a positive floor.
cfg = {"truth": {"K": 256, "tail": "zipf", "zipf_s": 1.3, "tail_threshold": 1e-3,
"init": "truth"},
"dynamics": {"n": 200, "grounding": {"m": 0}},
"generations": 120, "metrics": {"kl_floor": 1e-9, "support_eps": 1e-9}}
neutral = run_lineage(cfg, seed=0)
smooth = run_lineage({**cfg, "dynamics": {**cfg["dynamics"],
"kernel": {"reset": 0.006}}}, seed=0)
assert neutral["heterozygosity"].iloc[-1] < 0.4 # neutral collapses
assert smooth["heterozygosity"].iloc[-1] > 0.55 # smoothing floors H well above neutral