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:
parent
79bbc45f41
commit
871bc39ec6
21 changed files with 660 additions and 3 deletions
|
|
@ -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
87
src/knowledge/kernel.py
Normal 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)
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue