neural: real-MNIST external-validity tier (collapse + grounding)

Confirms model collapse and its arrest by grounding on REAL images, not
just the synthetic sandbox. A conv VAE (the canonical generative-collapse
model) is retrained each generation on its own generated digits, with a
fraction g of fresh real MNIST mixed in. Modes = digit class x stroke-
thickness bin (K=30, Zipf, ~18 tail modes); the oracle is a frozen CNN +
deterministic thickness at 98.5% mode accuracy (30x30 confusion matrix
recorded in the manifest as the measurement-noise floor).

Result (4 reps): dry (g=0) collapses to a single mode -- forward-KL
0.5->18, support 30->1, tail 1.0->0.06, H->0 -- while 10% grounding holds
all 30 modes (KL~0.6, full tail, H~0.9). Signs, not magnitudes (blueprint
3.5); the exact synthetic oracle stays the quantitative anchor. The VAE
needs ~10% grounding vs the synthetic histogram's ~5%, consistent with the
grounding finding that trained nets need more than the exact operator.

Plugs into the existing data-agnostic contract (metrics/grounding/output
reused verbatim): mnist_data (thickness bins, class x thickness bijection,
MnistSampler), mnist_oracle (ClassifierOracle + confusion matrix),
mnist_vae (ConvVAEGenerator), mnist_loop (run_mnist_lineage), kind=
mnist_lineage dispatch, MnistCfg/OracleCfg. Figures: plot_mnist (parquet-
only) + mnist_montage (eyeball diagnostic showing digits degenerate to one
blurry mode). make mnist / make env-mnist, kept out of the make neural
loop. 99 tests green (+5 torchvision-gated).

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

View file

@ -28,6 +28,9 @@ from .generation_loop import run_generative_lineage
# Config groups that make up a single neural lineage (everything else is experiment-level).
_NEURAL_KEYS = ("synthetic", "model", "dynamics", "generations", "metrics", "n_eval")
# Config groups for one MNIST lineage (the real-image tier; `mnist` replaces `synthetic`).
_MNIST_KEYS = ("mnist", "model", "dynamics", "generations", "metrics")
# Libraries recorded in the manifest on top of the Layer-1 core set (skipped if absent).
_EXTRA_LIBS = ("torch", "torchvision")
@ -91,12 +94,90 @@ def run_experiment(cfg: dict) -> pd.DataFrame:
return out
def _expand_mnist(cfg: dict) -> list[tuple[dict, dict]]:
"""Expand the MNIST g-sweep into (label, resolved-lineage) pairs (reuses ``_apply_param``)."""
base = {k: copy.deepcopy(cfg[k]) for k in _MNIST_KEYS if k in cfg}
sweeps = cfg.get("sweep", [])
if isinstance(sweeps, dict):
sweeps = [sweeps]
if not sweeps:
return [({}, base)]
params = [s["param"] for s in sweeps]
value_lists = [list(s["values"]) for s in sweeps]
combos: list[tuple[dict, dict]] = []
for values in itertools.product(*value_lists):
lin = copy.deepcopy(base)
label: dict = {}
for param, val in zip(params, values):
label.update(_apply_param(lin, param, val))
combos.append((label, lin))
return combos
def run_mnist_experiment(cfg: dict) -> tuple[pd.DataFrame, dict]:
"""Run every MNIST grid point x replicate; return (results, oracle-provenance manifest).
The frozen classifier oracle, per-mode real-image pools, and ``p*`` are built **once** and
shared across all arms/replicates (training the CNN and indexing the pools is expensive).
The oracle's confusion matrix over the real test set is returned for the manifest as the
measurement-noise floor.
Args:
cfg (dict): Parsed MNIST experiment YAML (``mnist``/``model``/``dynamics``/``oracle``
blocks, a ``sweep`` over ``g``, ``seed``, ``n_replicates``).
Returns:
tuple[pd.DataFrame, dict]: Long-form results and the oracle-provenance manifest dict.
"""
from knowledge.config import _sub
from .config import MnistCfg, OracleCfg
from .mnist_data import assign_modes, load_mnist, make_mnist_truth, MnistSampler
from .mnist_loop import run_mnist_lineage
from .mnist_oracle import build_oracle, confusion_summary
mnist_cfg = _sub(cfg["mnist"], MnistCfg)
oracle_cfg = _sub(cfg.get("oracle", {}), OracleCfg)
master = int(cfg["seed"])
n_rep = int(cfg["n_replicates"])
data = load_mnist(mnist_cfg.data_root)
td = make_mnist_truth(mnist_cfg)
oracle, cuts, ckpt_hash = build_oracle(mnist_cfg, oracle_cfg, data, seed=master)
modes = assign_modes(data.train_x, data.train_y, cuts, mnist_cfg)
sampler = MnistSampler(data.train_x, modes, mnist_cfg.K)
conf = confusion_summary(oracle, data, cuts, mnist_cfg)
combos = _expand_mnist(cfg)
seeds = spawn_seeds(master, n_rep)
frames: list[pd.DataFrame] = []
for label, lineage_cfg in combos:
for rep, ss in enumerate(seeds):
df = run_mnist_lineage(lineage_cfg, int(ss.generate_state(1)[0]), oracle, sampler, td)
for col, val in label.items():
df[col] = val
df["replicate"] = rep
frames.append(df)
out = pd.concat(frames, ignore_index=True)
out.insert(0, "experiment", cfg["experiment"])
manifest = {
"layer": "1.5", "tier": "mnist", "model_kind": cfg.get("model", {}).get("kind"),
"oracle_ckpt_sha256": ckpt_hash,
"oracle_mode_accuracy": conf["mode_accuracy"],
"oracle_class_accuracy": conf["class_accuracy"],
"confusion_matrix": conf["confusion"],
}
return out, manifest
def run_and_save(config_path: str | Path) -> Path:
"""Load a neural experiment YAML, run it, and write artifacts. Returns the output dir."""
config_path = Path(config_path)
cfg = yaml.safe_load(config_path.read_text())
out_dir = Path(cfg.get("output", {}).get("dir", f"results/{cfg['experiment']}"))
kind = cfg.get("kind", "gen_lineage")
extra_manifest = {"layer": "1.5", "model_kind": cfg.get("model", {}).get("kind", "histogram")}
if kind == "recombination":
from .recombine import run_recombination # the `recombination` experiment; lazy import
df = run_recombination(cfg)
@ -104,11 +185,13 @@ def run_and_save(config_path: str | Path) -> Path:
elif kind == "gen_lineage":
df = run_experiment(cfg)
grid = [{"label": label, "neural_cfg": c} for label, c in expand_sweeps(cfg)]
elif kind == "mnist_lineage":
df, extra_manifest = run_mnist_experiment(cfg) # oracle provenance + confusion matrix
grid = [{"label": label, "lineage_cfg": c} for label, c in _expand_mnist(cfg)]
else:
raise ValueError(f"unknown neural experiment kind {kind!r}")
model_kind = cfg.get("model", {}).get("kind", "histogram")
save_artifacts(cfg, df, out_dir, extra_libs=_EXTRA_LIBS,
extra_manifest={"layer": "1.5", "model_kind": model_kind}, grid=grid)
extra_manifest=extra_manifest, grid=grid)
return out_dir