What happens to Set members never seen in training?

Vocabulary and OOV
Train a Set field on known members without ever teaching it how unavailable members should affect prediction.

Example

tags: [red, never-seen]
labels: [red, never-seen]
target: 1.0

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

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

Model tree with record, tags, labels, target. 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 empty, all-unknown, and partly known sets. Check which member names the output can actually emit.

Result

P058 · Latest full run · Limited

Seed 5801 on cpu: 11 of 11 test checks met. This experiment demonstrates a limitation. History: 9 of 10 full runs met every check, across 3 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
Prediction for empty input -0.00967
Prediction for all-unknown input -0.00967

The untrained fallback treats all-unknown input like empty input here. Unknown output members also cannot be named. The next fallback test shows how training can distinguish nonempty input.

Experiment details and code

Latest full run

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

Source fingerprint: ba81d540b3c99719bfd3172b15d25e3d26fef5fd14ae210298deaa7b36936ae1. Python 3.12.10; Torch 2.12.0.

Measurement Value
steps 400
vocabularies tags: blue, red; labels: blue, red
baseline_rmse 1.51524
nrmse 0.0108351
known_exact_set_accuracy 1
shuffled_nrmse 1.44736
unknown_and_duplicate_drift 0
boundary_predictions empty: -0.00967047; all_unknown: -0.00967047; null: 3.00935; partial_unknown: 0.992384
all_unknown_target_exact_match False
Behavioral checks
Behavioral check Outcome
Known membership regression nRMSE below 0.15 Met
Known valued sets reconstruct above 0.95 exact accuracy Met
Shuffled tags remove predictive accuracy Met
Unknown and duplicate members leave regression unchanged Met
Unknown and duplicate members leave reconstructed sets unchanged Met
All-unknown and empty inputs have identical known membership bits Met
All-unknown and empty targets have identical membership bits Met
Untrained fallback leaves all-unknown and empty predictions identical Met
Null remains distinct from valued empty content Met
Unknown target labels cannot be recovered Met
Evaluation does not grow either vocabulary Met

Full run history

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

Seed Outcome Recorded Code fingerprint
5801 met 2026-09-16T22:46:45.480204+00:00 d1cea628893655a71803333ff8371746fd9bd57f74ff5711e5586acc52f2a4b7
5801 met 2026-09-16T22:48:58.593402+00:00 d1cea628893655a71803333ff8371746fd9bd57f74ff5711e5586acc52f2a4b7
6501 met 2026-09-16T22:49:52.825178+00:00 d1cea628893655a71803333ff8371746fd9bd57f74ff5711e5586acc52f2a4b7
6502 met 2026-09-16T22:50:42.758112+00:00 d1cea628893655a71803333ff8371746fd9bd57f74ff5711e5586acc52f2a4b7
5801 met 2026-09-17T00:03:50.370932+00:00 d1cea628893655a71803333ff8371746fd9bd57f74ff5711e5586acc52f2a4b7
5801 met 2026-09-17T01:02:20.163458+00:00 a90f27d80b142cc6e63c826dce3acaebae2d7ae24b294319ea057a0751a98ae3
5801 not met 2026-09-18T16:29:07.439643+00:00 a90f27d80b142cc6e63c826dce3acaebae2d7ae24b294319ea057a0751a98ae3
5801 met 2026-09-18T16:41:57.581869+00:00 a90f27d80b142cc6e63c826dce3acaebae2d7ae24b294319ea057a0751a98ae3
5801 met 2026-09-20T02:47:33.536335+00:00 34e11fd2f3d4f9b96a1539b02a517669e546251f6254e5f4b81494eb4b6b162a
5801 met 2026-09-20T02:52:57.798350+00:00 34e11fd2f3d4f9b96a1539b02a517669e546251f6254e5f4b81494eb4b6b162a

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 P058

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

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

Data and model

Each record samples one of five equally likely patterns: empty, red, blue, both, or null. target = 1*red - 1.5*blue for valued sets and 3 for null. A separate masked Set head reconstructs the original membership. Inputs and targets have independent, automatically grown vocabularies. Disable simulated unavailability to isolate actual OOV behavior. Splits contain 4,096 training, 512 validation, and 2,048 test records.

Training and controls

Fit for 400 AdamW updates at learning rate 0.002. Known membership must give regression nRMSE below 0.15 and at least 0.95 exact-set accuracy on valued targets. Shuffling tags removes regression accuracy. Adding OOV members and duplicates must leave predictions unchanged. An all-OOV target and an empty target must have the same encoded membership bits, without learning labels from evaluation. Null versus empty predictions must remain well separated.

Complete experiment code

Code
"""P058: learn known membership while exposing empty/all-OOV and null boundaries."""

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

PROOF_ID = "P058"
BUDGET = 400
PATTERNS = ((), ("red",), ("blue",), ("red", "blue"), None)


def records(*, rows: int, seed: int) -> Iterator[dict]:
    rng = np.random.default_rng(seed)
    for index in rng.integers(len(PATTERNS), size=rows):
        pattern = PATTERNS[index]
        tags = None if pattern is None else list(pattern)
        target = 3.0 if pattern is None else float(("red" in pattern) - 1.5 * ("blue" in pattern))
        yield {"tags": tags, "labels": None if tags is None else list(tags), "target": target}


def build() -> rf.Model:
    return rf.Model(
        d_model=32,
        n_layers=1,
        n_heads=4,
        dropout=0.0,
        batch_size=64,
        tags=rf.Set(p_unavailable=0.0),
        labels=rf.Set(p_unavailable=0.0, threshold=0.5, mask=True),
        target=rf.Number(mask=True, objective="mse"),
    )


def prediction(model: rf.Model, rows: list[dict]) -> tuple[np.ndarray, list[set[str]]]:
    output = model.predict(pa.Table.from_pylist(rows))["predictions"].to_pylist()
    values = np.asarray([row["target"]["content"] for row in output], dtype=np.float64)
    labels = [{candidate["value"] for candidate in row["labels"]["content"]} for row in output]
    return values, labels


def run(seed: int, steps: int | None, accelerator: str) -> tuple[dict, dict]:
    lit.seed_everything(seed, workers=True)
    model = build()
    model.optimizer = rf.adamw(learning_rate=0.002)
    data = rf.SyntheticDataModule(
        model=model,
        train=partial(records, rows=4096, seed=seed + 1),
        validate=partial(records, rows=512, 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)
    model.eval()
    vocabularies = {name: rf.Set.vocabulary(model, f"/{name}") for name in ("tags", "labels")}
    train = list(records(rows=4096, seed=seed + 1))
    test = list(records(rows=2048, seed=seed + 3))
    actual = np.asarray([row["target"] for row in test])
    baseline = float(np.sqrt(np.mean((actual - np.mean([row["target"] for row in train])) ** 2)))
    values, labels = prediction(model, test)
    augmented = [
        {**row, "tags": None if row["tags"] is None else [*row["tags"], *row["tags"], "never-seen"]} for row in test
    ]
    repeated, repeated_labels = prediction(model, augmented)
    order = np.random.default_rng(seed + 4).permutation(len(test))
    shuffled = [{**row, "tags": test[index]["tags"]} for row, index in zip(test, order, strict=True)]
    broken, _ = prediction(model, shuffled)
    probes = [
        {"tags": [], "labels": [], "target": 0.0},
        {"tags": ["never-seen"], "labels": ["never-seen"], "target": 0.0},
        {"tags": None, "labels": None, "target": 3.0},
        {"tags": ["red", "never-seen"], "labels": ["red", "never-seen"], "target": 1.0},
    ]
    boundary, boundary_labels = prediction(model, probes)
    fields = model.encode(pa.Table.from_pylist(probes), strata="test")
    input_field = fields["/tags"]
    target_bits = fields["/labels"].targets[rf.TensorKey.content]["membership"]
    metrics = {
        "steps": trainer.global_step,
        "vocabularies": vocabularies,
        "baseline_rmse": baseline,
        "nrmse": float(np.sqrt(np.mean((values - actual) ** 2))) / baseline,
        "known_exact_set_accuracy": float(
            np.mean(
                [
                    predicted == set(row["labels"])
                    for row, predicted in zip(test, labels, strict=True)
                    if row["labels"] is not None
                ]
            )
        ),
        "shuffled_nrmse": float(np.sqrt(np.mean((broken - actual) ** 2))) / baseline,
        "unknown_and_duplicate_drift": float(np.max(np.abs(values - repeated))),
        "boundary_predictions": dict(
            zip(("empty", "all_unknown", "null", "partial_unknown"), boundary.tolist(), strict=True)
        ),
        "all_unknown_target_exact_match": boundary_labels[1] == {"never-seen"},
    }
    return metrics, {
        "Known membership regression nRMSE below 0.15": metrics["nrmse"] < 0.15,
        "Known valued sets reconstruct above 0.95 exact accuracy": metrics["known_exact_set_accuracy"] >= 0.95,
        "Shuffled tags remove predictive accuracy": metrics["shuffled_nrmse"] > 0.85,
        "Unknown and duplicate members leave regression unchanged": metrics["unknown_and_duplicate_drift"] < 1e-5,
        "Unknown and duplicate members leave reconstructed sets unchanged": labels == repeated_labels,
        "All-unknown and empty inputs have identical known membership bits": bool(
            input_field.state[:2].eq(rf.Tokens.valued).all()
        )
        and bool((input_field.content["membership"][0] == input_field.content["membership"][1]).all()),
        "All-unknown and empty targets have identical membership bits": bool((target_bits[0] == target_bits[1]).all()),
        "Untrained fallback leaves all-unknown and empty predictions identical": abs(boundary[0] - boundary[1]) < 1e-5
        and boundary_labels[0] == boundary_labels[1],
        "Null remains distinct from valued empty content": abs(boundary[2] - boundary[0]) > 2.0,
        "Unknown target labels cannot be recovered": not metrics["all_unknown_target_exact_match"]
        and all("never-seen" not in row for row in boundary_labels),
        "Evaluation does not grow either vocabulary": all(
            rf.Set.vocabulary(model, f"/{name}") == vocabulary for name, vocabulary in vocabularies.items()
        ),
    }


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