Can Cluster reconstruction hide missing coverage too?

Vocabulary
Reconstruct a Cluster answer from a categorical cue while reducing the share of test identities seen in training.

Example

x: cue-2
code: class-2
hide: true

code is hidden wherever hide is true. Unfamiliar answer identities are counted in coverage separately.

Model tree with record, x, code. Selector notes identify hidden values. hide chooses which code answers are hidden.

Model tree with record, x, code. Selector notes identify hidden values. hide chooses which code answers are hidden.

Figure 1: Selector notes identify hidden values. hide chooses which code answers are hidden.

Comparison

Measure known-answer accuracy and all-answer accuracy separately from vocabulary coverage.

Result

P065 · Latest full run · Failing

Seed 7651 on gpu: 15 of 18 test checks met. The experiment did not meet every criterion. History: 1 of 12 full runs met every check, across 4 distinct seeds. History can include earlier source versions. The experiment code has changed since this run; these measurements describe its earlier version.

Comparison Latest measurement
Known-answer accuracy, low coverage 88.2%
Answers covered 10.0%
All-answer accuracy 8.8%

Coverage and recovery are different questions. The latest run also misses the required known-answer accuracy, so its reconstruction result remains incomplete.

Experiment details and code

Latest full run

Seed 7651, gpu, recorded 2026-09-18T16:55:22.868859+00:00. Outcome: not met.

Source fingerprint: 49674d7075f8814bb628c5a2d7718d157cbb78c535cda7f5c18280403fffa6b0. Python 3.12.6; Torch 2.12.0.

Measurement Value
known_1024 loss.state: 3.51714e-05; accuracy.state: 1; vocabulary.size: 8; targets.known: 1024; targets.unavailable: 0; coverage.content: 1; accuracy.content: 0.851562; accuracy.all: 0.851562; loss.content: 0.37868; nll.content: 0.37868
known_512 loss.state: 3.51714e-05; accuracy.state: 1; vocabulary.size: 8; targets.known: 512; targets.unavailable: 512; coverage.content: 0.5; accuracy.content: 0.851562; accuracy.all: 0.425781; loss.content: 0.383708; nll.content: 0.383708
known_102 loss.state: 3.51714e-05; accuracy.state: 1; vocabulary.size: 8; targets.known: 102; targets.unavailable: 922; coverage.content: 0.0996094; accuracy.content: 0.882353; accuracy.all: 0.0878906; loss.content: 0.336963; nll.content: 0.336963
batch_partition_drift 2.98023e-08
reported_content_loss_drift 2.98023e-08
known_0 loss.state: 3.51714e-05; accuracy.state: 1; vocabulary.size: 8; targets.known: 0; targets.unavailable: 1024; coverage.content: 0; accuracy.content: None; accuracy.all: 0; loss.content: None; nll.content: None
Behavioral checks
Behavioral check Outcome
1024: exact known and unavailable target counts Met
1024: coverage matches selected targets Met
1024: known accuracy exceeds 0.95 Not met
1024: all-target accuracy includes unavailable errors Met
512: exact known and unavailable target counts Met
512: coverage matches selected targets Met
512: known accuracy exceeds 0.95 Not met
512: all-target accuracy includes unavailable errors Met
102: exact known and unavailable target counts Met
102: coverage matches selected targets Met
102: known accuracy exceeds 0.95 Not met
102: all-target accuracy includes unavailable errors Met
Rebatching preserves counts and scores Met
0: exact known and unavailable target counts Met
0: coverage matches selected targets Met
All-OOV conditional scores are undefined Met
All-OOV all-target accuracy is zero Met
Validation leaves mapping and exposures frozen Met

Full run history

Runs may include earlier source versions. Repeating a seed is not an independent seed check.

Seed Outcome Recorded Code fingerprint
7699 not met 2026-09-17T00:44:32.412858+00:00 5a9491c285c7452c1289d2589ed2ff0c8adcd61a7c85f18e89fa127d47b4c6d3
7699 not met 2026-09-17T00:47:12.002503+00:00 d425018b5725c9c17a218369522b63f19974e14dd946c4f8c39b41a6008e3a01
7699 not met 2026-09-17T00:50:07.221774+00:00 257c6f6a170430fb3ff85b0ea9d9caa09c1228e4eef740cd1990f1269b415049
7699 not met 2026-09-17T00:53:05.922433+00:00 d425018b5725c9c17a218369522b63f19974e14dd946c4f8c39b41a6008e3a01
7651 not met 2026-09-17T00:54:46.943954+00:00 d425018b5725c9c17a218369522b63f19974e14dd946c4f8c39b41a6008e3a01
7651 not met 2026-09-17T00:55:20.080230+00:00 d425018b5725c9c17a218369522b63f19974e14dd946c4f8c39b41a6008e3a01
7651 not met 2026-09-17T00:57:20.442192+00:00 d425018b5725c9c17a218369522b63f19974e14dd946c4f8c39b41a6008e3a01
7701 not met 2026-09-17T00:58:13.155776+00:00 d425018b5725c9c17a218369522b63f19974e14dd946c4f8c39b41a6008e3a01
7702 met 2026-09-17T00:59:06.078249+00:00 d425018b5725c9c17a218369522b63f19974e14dd946c4f8c39b41a6008e3a01
7651 not met 2026-09-18T16:30:04.457290+00:00 d425018b5725c9c17a218369522b63f19974e14dd946c4f8c39b41a6008e3a01
7651 not met 2026-09-18T16:44:18.364286+00:00 d425018b5725c9c17a218369522b63f19974e14dd946c4f8c39b41a6008e3a01
7651 not met 2026-09-18T16:55:22.868859+00:00 d425018b5725c9c17a218369522b63f19974e14dd946c4f8c39b41a6008e3a01

Recorded results.

Run this experiment

Proof execution is optional and separate from building this site. Use a RelFlow build matching the recorded provenance; replace the wheel path below with that artifact. Run from this documentation repository’s root:

uv run --with /path/to/matching-relflow.whl python proofs/run.py P065

Or, from a prepared proof environment, run the self-contained script directly:

PYTHONPATH=proofs python proofs/vocabulary/cluster_pretraining_coverage.py

Add --accelerator gpu for CUDA or --seed 42 for another seeded experiment. --steps 2 checks execution with a short training budget; it is recorded as a smoke run.

Download the complete proof.

Training and controls

Train for 800 updates on 8,192 rows; storage grows to admit all eight labels. Evaluation selects exactly 1,024 of 2,048 independent records. Compare the original set with copies retaining 50%, 10%, or 0% of selected target names. All copies preserve the exact visible inputs and query-selected masks.

Repeat the 10%-coverage case with unknown targets grouped into separate batches. Count-weighted epoch metrics must agree despite all-OOV batches. Scores with no known targets must be undefined, not zero reconstruction error. Naive accuracy over all selected valued targets counts OOV as wrong.

Complete experiment code

Code
"""P065: report stable reconstruction quality alongside pretraining coverage."""

from collections.abc import Iterator
from functools import partial

import lightning.pytorch as lit
import numpy as np
from reporting import report

import relflow as rf

PROOF_ID = "P065"
BUDGET = 800
LABELS = tuple(f"class-{index}" for index in range(8))


def records(*, rows: int, seed: int) -> Iterator[dict]:
    rng = np.random.default_rng(seed)
    for index, label in enumerate(rng.integers(0, len(LABELS), rows)):
        yield {"x": f"cue-{label}", "code": LABELS[label], "hide": index % 2 == 0}


def build() -> rf.Model:
    model = rf.Model(
        d_model=32,
        n_layers=1,
        n_heads=4,
        batch_size=64,
        dropout=0.0,
        x=rf.Category(p_unavailable=0.0),
        code=rf.Cluster(n_clusters=8, p_unavailable=1.0, mask=rf.Mask(query="hide", reconstruct=True)),
    )
    model.optimizer = rf.adamw(learning_rate=0.002)
    return model


def evaluate(trainer: lit.Trainer, model: rf.Model, rows: list[dict]) -> dict[str, float]:
    data = rf.SyntheticDataModule(model=model, validate=lambda: iter(rows), seed=0)
    result = trainer.validate(model, datamodule=data, verbose=False)[0]
    prefix = ".code/validate."
    return {name.removeprefix(prefix): float(value) for name, value in result.items() if name.startswith(prefix)}


def run(seed: int, steps: int | None, accelerator: str) -> tuple[dict, dict]:
    lit.seed_everything(seed, workers=True)
    model = build()
    data = rf.SyntheticDataModule(
        model=model,
        train=partial(records, rows=8192, seed=seed + 1),
        validate=partial(records, rows=1024, seed=seed + 2),
        seed=seed,
    )
    trainer = lit.Trainer(
        accelerator=accelerator,
        devices=1,
        max_epochs=-1,
        max_steps=BUDGET if steps is None else min(steps, BUDGET),
        logger=False,
        enable_checkpointing=False,
        enable_progress_bar=False,
        enable_model_summary=False,
        deterministic=True,
        num_sanity_val_steps=0,
    )
    trainer.fit(model, datamodule=data)
    original = list(records(rows=2048, seed=seed + 3))
    vocabulary = rf.Cluster.vocabulary(model, "/code")
    counts = tuple(model.nodes["/code"].embedder.counters["content"].counts.tolist())
    metrics, checks = {}, {}
    for keep in (1024, 512, 102, 0):
        selected = 0
        rows = []
        for row in original:
            unavailable = row["hide"] and selected >= keep
            rows.append({**row, "code": f"novel-{row['code']}" if unavailable else row["code"]})
            selected += int(row["hide"])
        measured = evaluate(trainer, model, rows)
        # Keep undefined quantities explicit without nonstandard JSON NaNs.
        metrics[f"known_{keep}"] = {name: value if np.isfinite(value) else None for name, value in measured.items()}
        checks[f"{keep}: exact known and unavailable target counts"] = (
            measured.get("targets.known") == keep and measured.get("targets.unavailable") == 1024 - keep
        )
        checks[f"{keep}: coverage matches selected targets"] = (
            abs(measured.get("coverage.content", -1) - keep / 1024) < 1e-7
        )
        if keep:
            checks[f"{keep}: known accuracy exceeds 0.95"] = measured.get("accuracy.content", -1) > 0.95
            checks[f"{keep}: all-target accuracy includes unavailable errors"] = (
                abs(measured.get("accuracy.all", -1) - measured.get("accuracy.content", 0) * keep / 1024) < 1e-7
            )
        else:
            checks["All-OOV conditional scores are undefined"] = all(
                name in measured and np.isnan(measured[name])
                for name in ("accuracy.content", "nll.content", "loss.content")
            )
            checks["All-OOV all-target accuracy is zero"] = measured.get("accuracy.all") == 0
        if keep == 102:
            rng = np.random.default_rng(seed + 4)
            permuted = [rows[index] for index in rng.permutation(len(rows))]
            regrouped = evaluate(trainer, model, permuted)
            names = (
                "targets.known",
                "targets.unavailable",
                "coverage.content",
                "accuracy.content",
                "accuracy.all",
                "nll.content",
                "loss.content",
            )
            drift = (
                max(abs(measured[name] - regrouped[name]) for name in names)
                if all(name in measured and name in regrouped for name in names)
                else None
            )
            metrics["batch_partition_drift"] = drift
            metrics["reported_content_loss_drift"] = abs(measured["loss.content"] - regrouped["loss.content"])
            checks["Rebatching preserves counts and scores"] = drift is not None and drift < 1e-5
    checks["Validation leaves mapping and exposures frozen"] = (
        rf.Cluster.vocabulary(model, "/code") == vocabulary
        and tuple(model.nodes["/code"].embedder.counters["content"].counts.tolist()) == counts
    )
    return metrics, checks


if __name__ == "__main__":
    report(PROOF_ID, run, seed=7651)