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

@ -11,6 +11,8 @@ from __future__ import annotations
from dataclasses import dataclass, field, replace
from typing import Any, Mapping, Optional
from .kernel import LearningKernelCfg
# Runtime-tunable knobs live here, defaults chosen to match the blueprint's illustrative
# schema. Nothing here is a magic number buried in algorithm code.
@ -61,6 +63,7 @@ class DynamicsCfg:
grounding: GroundingCfg = field(default_factory=GroundingCfg)
selection: SelectionCfg = field(default_factory=SelectionCfg)
remint: RemintCfg = field(default_factory=RemintCfg)
kernel: LearningKernelCfg = field(default_factory=LearningKernelCfg)
@dataclass(frozen=True)
@ -89,6 +92,7 @@ class LineageCfg:
grounding=_sub(dyn_raw.get("grounding", {}), GroundingCfg),
selection=_sub(dyn_raw.get("selection", {}), SelectionCfg),
remint=_sub(dyn_raw.get("remint", {}), RemintCfg),
kernel=_sub(dyn_raw.get("kernel", {}), LearningKernelCfg),
)
metrics = _sub(cfg.get("metrics", {}), MetricsCfg)
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,
selection_mode=cfg.dynamics.selection.mode,
novelty_alpha=cfg.dynamics.selection.novelty_alpha,
kernel=cfg.dynamics.kernel,
)
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 dataclasses import dataclass
from dataclasses import dataclass, field
import numpy as np
from .kernel import LearningKernelCfg, apply_kernel
@dataclass(frozen=True)
class StepCtx:
@ -24,6 +26,8 @@ class StepCtx:
regions (np.ndarray): Length-K region index per item.
selection_mode (str): ``none`` | ``greedy`` | ``qd``.
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
@ -32,6 +36,7 @@ class StepCtx:
regions: np.ndarray
selection_mode: str = "none"
novelty_alpha: float = 0.0
kernel: LearningKernelCfg = field(default_factory=LearningKernelCfg)
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)
counts = c_syn + c_real
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)
cfg.selection_mode, cfg.novelty_alpha)
return p_next