Spaces:
Sleeping
Sleeping
File size: 5,382 Bytes
b8930b3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | """Named subsets of a dataset's cache (tuning vs held-out).
Threshold selection and rule diagnosis have to happen on *some* documents, and
metrics reported on those same documents are contaminated: the operating point
was fitted to them. Splitting the cache keeps the two roles apart, so a held-out
number can be quoted without that caveat.
**Membership is by id, never by position.** A manifest file pins the exact ids
belonging to a split::
eval/splits/<dataset>_tuning.json {"ids": ["X00016469670", ...], ...}
Everything cached for the dataset that is *not* in the manifest is held out.
That definition is stable under any of the things that break a positional
"first N" rule: cache files are read in filesystem-glob order, which need not
match the order the dataset streamed them in; new documents can be added later;
a re-predict can rewrite a file's timestamp. Only the id set matters, so the
same documents land in the same split on every machine and every run.
The manifest is committed even though ``eval/cache/`` is git-ignored. The cache
can be regenerated from the dataset; the record of *which documents were used to
tune* cannot be, and losing it would silently turn a held-out claim into an
in-sample one.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Literal
# Committed alongside the code, unlike the cache itself.
DEFAULT_SPLITS_DIR = Path("eval/splits")
SplitName = Literal["all", "tuning", "heldout"]
SPLIT_NAMES: tuple[str, ...] = ("all", "tuning", "heldout")
class SplitError(RuntimeError):
"""Raised when a named split cannot be resolved for a dataset."""
def manifest_path(dataset: str, *, splits_dir: Path = DEFAULT_SPLITS_DIR) -> Path:
"""Return the tuning-manifest path for a dataset (not checked for existence)."""
return Path(splits_dir) / f"{dataset}_tuning.json"
def load_tuning_ids(dataset: str, *, splits_dir: Path = DEFAULT_SPLITS_DIR) -> frozenset[str]:
"""Load the set of tuning-slice ids for a dataset.
Args:
dataset: Dataset name.
splits_dir: Directory holding split manifests.
Returns:
The frozen set of example ids in the tuning slice.
Raises:
SplitError: If the manifest is missing, unreadable, or declares no ids.
"""
path = manifest_path(dataset, splits_dir=splits_dir)
if not path.exists():
raise SplitError(
f"No tuning manifest for dataset {dataset!r} at {path}. "
"A tuning/held-out split needs one; score the whole cache with "
"--split all, or write the manifest first."
)
try:
payload: dict[str, Any] = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
raise SplitError(f"Tuning manifest {path} is not valid JSON: {exc}") from exc
ids = payload.get("ids")
if not isinstance(ids, list) or not ids:
raise SplitError(f"Tuning manifest {path} declares no 'ids' list.")
return frozenset(str(value) for value in ids)
def select(
entries: list[dict[str, Any]],
split: str,
*,
dataset: str,
splits_dir: Path = DEFAULT_SPLITS_DIR,
) -> list[dict[str, Any]]:
"""Filter cached entries down to a named split.
Selection is set membership on the entry id, so the result depends only on
which documents are cached -- not on the order they were read in. Input order
is preserved among the surviving entries.
Args:
entries: Cached entries for the dataset.
split: One of ``"all"``, ``"tuning"``, or ``"heldout"``.
dataset: Dataset name, used to locate the manifest.
splits_dir: Directory holding split manifests.
Returns:
The subset of ``entries`` belonging to ``split``.
Raises:
SplitError: If ``split`` is unknown, the manifest is needed but missing,
or the split resolves to no documents at all.
"""
if split not in SPLIT_NAMES:
raise SplitError(f"Unknown split {split!r}; expected one of {', '.join(SPLIT_NAMES)}.")
if split == "all":
return list(entries)
tuning_ids = load_tuning_ids(dataset, splits_dir=splits_dir)
if split == "tuning":
selected = [e for e in entries if str(e.get("id")) in tuning_ids]
else:
selected = [e for e in entries if str(e.get("id")) not in tuning_ids]
if not selected:
raise SplitError(
f"Split {split!r} selected 0 of {len(entries)} cached entries for "
f"dataset {dataset!r}. The cache may not have been expanded beyond "
"the tuning slice yet."
)
return selected
def describe(split: str, n_selected: int, n_total: int) -> str:
"""Render a one-line description of the active split for a report header.
Args:
split: The split name.
n_selected: Documents in the split.
n_total: Documents cached for the dataset overall.
Returns:
A human-readable summary naming the split and its size.
"""
if split == "all":
note = "WHOLE CACHE -- mixes tuning and held-out documents"
elif split == "tuning":
note = "TUNING slice -- the operating point was fitted on these, not independent"
else:
note = "HELD OUT -- never used to select the threshold or diagnose rules"
return f"{split} ({n_selected} of {n_total} cached): {note}"
|