Can it learn separate probabilities for Set members?

Vocabulary
With no useful context, red appears in 90% of answer sets and blue in 20%. Predict each member’s probability.

Example

x: 0.0
labels: [red, blue]

The answer is shown here for explanation; it is hidden from the model when scored.

Model tree with record, x, labels. Amber cards are hidden prediction targets. Other fields provide the input.

Model tree with record, x, labels. Amber cards are hidden prediction targets. Other fields provide the input.

Figure 1: Amber cards are hidden prediction targets. Other fields provide the input.

Comparison

Compare internal output allocations and test sets that also contain unfamiliar members. Known positive and negative annotations remain usable.

Result

P063 · Latest full run · Passing

Seed 7631 on cpu: 12 of 12 test checks met. History: 10 of 11 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
Predicted red membership 90.5%
Predicted blue membership 20.3%

The member probabilities recover their population rates. Unknown members still reduce coverage and cannot be emitted; annotations here are complete sets, not partially supplied labels.

Experiment details and code

Latest full run

Seed 7631, cpu, recorded 2026-09-20T02:52:20.743327+00:00. Outcome: met.

Source fingerprint: b01ac2fd736809ab32c7cd83ccf2732cacd9b9eba4d93bca355c735605d5e593. Python 3.12.10; Torch 2.12.0.

Measurement Value
oracle_nll 0.412743
capacity_2 probabilities: red: 0.905335; blue: 0.202591; excess_nll: 9.21014e-05; steps: 400
coverage_2 loss.state: 2.66524; accuracy.state: 0.8; vocabulary.size: 2; targets.known: 256; targets.unavailable: 256; coverage.content: 0.5; sets.valued: 512; sets.complete: 256; coverage.set: 0.5; accuracy.content: 0.5; accuracy.set: 0; precision.content: 0.25; recall.content: 0.5; recall.all: 0.25; f1.content: 0.333333; f1.all: 0.25; loss.content: 1.18093
capacity_512 probabilities: red: 0.908177; blue: 0.195002; excess_nll: 0.000234878; steps: 400
coverage_512 loss.state: 2.61309; accuracy.state: 0.8; vocabulary.size: 2; targets.known: 256; targets.unavailable: 256; coverage.content: 0.5; sets.valued: 512; sets.complete: 256; coverage.set: 0.5; accuracy.content: 0.5; accuracy.set: 0; precision.content: 0.25; recall.content: 0.5; recall.all: 0.25; f1.content: 0.333333; f1.all: 0.25; loss.content: 1.19319
Behavioral checks
Behavioral check Outcome
2: probabilities recover prevalence within 0.06 Met
2: excess expected NLL below 0.02 Met
2: member counts include partial and all-OOV targets Met
2: member and complete-set coverage are one half Met
2: rebatching preserves epoch scores Met
2: evaluation never admits unknown labels Met
512: probabilities recover prevalence within 0.06 Met
512: excess expected NLL below 0.02 Met
512: member counts include partial and all-OOV targets Met
512: member and complete-set coverage are one half Met
512: rebatching preserves epoch scores Met
512: evaluation never admits unknown labels 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 met 2026-09-17T00:43:58.477589+00:00 ba7d023d4a62b2df48855a892bfe2917f5e99e32c20598f0c4d0028b8df1c6f5
7631 met 2026-09-17T00:50:35.052145+00:00 ba7d023d4a62b2df48855a892bfe2917f5e99e32c20598f0c4d0028b8df1c6f5
7631 not met 2026-09-17T00:54:46.136464+00:00 ba7d023d4a62b2df48855a892bfe2917f5e99e32c20598f0c4d0028b8df1c6f5
7631 met 2026-09-17T00:56:46.394742+00:00 ba7d023d4a62b2df48855a892bfe2917f5e99e32c20598f0c4d0028b8df1c6f5
7701 met 2026-09-17T00:57:39.079826+00:00 ba7d023d4a62b2df48855a892bfe2917f5e99e32c20598f0c4d0028b8df1c6f5
7702 met 2026-09-17T00:58:31.886937+00:00 ba7d023d4a62b2df48855a892bfe2917f5e99e32c20598f0c4d0028b8df1c6f5
7631 met 2026-09-18T16:29:24.799102+00:00 ba7d023d4a62b2df48855a892bfe2917f5e99e32c20598f0c4d0028b8df1c6f5
7631 met 2026-09-18T16:43:43.241331+00:00 ba7d023d4a62b2df48855a892bfe2917f5e99e32c20598f0c4d0028b8df1c6f5
7631 met 2026-09-18T16:54:48.183363+00:00 ba7d023d4a62b2df48855a892bfe2917f5e99e32c20598f0c4d0028b8df1c6f5
7631 met 2026-09-20T02:45:36.278499+00:00 27e78b913e911747eacc2511b731daf8fd7963ed3f588a1571216dae9513c4c9
7631 met 2026-09-20T02:52:20.743327+00:00 27e78b913e911747eacc2511b731daf8fd7963ed3f588a1571216dae9513c4c9

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 P063

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

PYTHONPATH=proofs python proofs/vocabulary/set_reconstruction_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

Each arm sees 32,768 training rows and 1,024 independent validation rows. Evaluate written probabilities and expected Bernoulli log loss against the oracle. Then replace validation with equal numbers of partial-OOV, all-OOV, empty, known-only and null sets. Rebatch them in a different order. Member coverage must be 1/2, complete-set coverage 1/2, and all epochs must count exactly 256 known and 256 unknown positive memberships. Unused capacity must not change the meaning of the reported membership loss.

Complete experiment code

Code
"""P063: membership probability and coverage have explicit support."""

from collections.abc import Iterator
from functools import partial

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

import relflow as rf
from relflow.helpers.resize import Resize

PROOF_ID = "P063"
BUDGET = 400


def records(*, rows: int, seed: int) -> Iterator[dict]:
    rng = np.random.default_rng(seed)
    for red, blue in rng.random((rows, 2)) < np.asarray([0.9, 0.2]):
        yield {"x": 0.0, "labels": [name for name, present in (("red", red), ("blue", blue)) if present]}


def build(size: int) -> rf.Model:
    model = rf.Model(
        d_model=16,
        n_layers=1,
        n_heads=2,
        batch_size=128,
        dropout=0.0,
        x=rf.Number,
        labels=rf.Set(p_unavailable=1.0, mask=True),
    )
    # Experimental storage control only: allocation is not a schema option.
    node = model.nodes["/labels"]
    resize = Resize()
    resize.embedding(node.embedder.embeddings["content"], size)
    resize.linear(node.decoder.linears["content"], size)
    node.embedder.counters["content"].resize(size, resize)
    resize.attribute(node.embedder.vocab, "size", size)
    resize.commit()
    model.optimizer = rf.adamw(learning_rate=0.002)
    return model


def run(seed: int, steps: int | None, accelerator: str) -> tuple[dict, dict]:
    metrics, checks = {}, {}
    prior = np.asarray([0.9, 0.2])
    oracle = float(np.mean(-prior * np.log(prior) - (1 - prior) * np.log1p(-prior)))
    metrics["oracle_nll"] = oracle
    for size in (2, 512):
        lit.seed_everything(seed, workers=True)
        model = build(size)
        data = rf.SyntheticDataModule(
            model=model,
            train=partial(records, rows=32768, 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),
            deterministic=True,
            logger=False,
            enable_checkpointing=False,
            enable_progress_bar=False,
            enable_model_summary=False,
            num_sanity_val_steps=0,
        )
        trainer.fit(model, datamodule=data)
        vocabulary = rf.Set.vocabulary(model, "/labels")
        written = model.predict(pa.table({"x": [0.0]}))["predictions"].to_pylist()[0]["labels"]["content"]
        probabilities = {item["value"]: item["probability"] for item in written}
        p = np.asarray([probabilities[name] for name in ("red", "blue")]).clip(1e-7, 1 - 1e-7)
        excess = float(np.mean(-prior * np.log(p) - (1 - prior) * np.log1p(-p)) - oracle)
        metrics[f"capacity_{size}"] = {
            "probabilities": probabilities,
            "excess_nll": excess,
            "steps": trainer.global_step,
        }
        checks[f"{size}: probabilities recover prevalence within 0.06"] = bool(np.max(np.abs(p - prior)) < 0.06)
        checks[f"{size}: excess expected NLL below 0.02"] = excess < 0.02
        rows = [
            {"x": 0.0, "labels": labels}
            for labels in (["red", "novel"], ["novel"], [], ["blue"], None)
            for _ in range(128)
        ]
        results = []
        for order in (np.arange(len(rows)), np.random.default_rng(seed + 3).permutation(len(rows))):
            table = pa.Table.from_pylist([rows[index] for index in order])
            measured = trainer.validate(
                model, datamodule=rf.ArrowDataModule(model=model, validate=table, num_workers=0), verbose=False
            )[0]
            prefix = ".labels/validate."
            results.append(
                {key.removeprefix(prefix): float(value) for key, value in measured.items() if key.startswith(prefix)}
            )
        first, second = results
        metrics[f"coverage_{size}"] = {key: value if np.isfinite(value) else None for key, value in first.items()}
        checks[f"{size}: member counts include partial and all-OOV targets"] = (
            first.get("targets.known") == 256 and first.get("targets.unavailable") == 256
        )
        checks[f"{size}: member and complete-set coverage are one half"] = (
            first.get("coverage.content") == 0.5 and first.get("coverage.set") == 0.5
        )
        names = (
            "targets.known",
            "targets.unavailable",
            "coverage.content",
            "coverage.set",
            "loss.content",
            "accuracy.set",
            "f1.all",
        )
        checks[f"{size}: rebatching preserves epoch scores"] = all(
            name in first and name in second and abs(first[name] - second[name]) < 1e-5 for name in names
        )
        checks[f"{size}: evaluation never admits unknown labels"] = rf.Set.vocabulary(model, "/labels") == vocabulary
    return metrics, checks


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