Add decision core: autojev/data.py
Browse files- autojev/data.py +669 -0
autojev/data.py
ADDED
|
@@ -0,0 +1,669 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build fixed, family-separated decisions from pinned public sources.
|
| 2 |
+
|
| 3 |
+
The benchmark's converters are an external, revision-pinned dependency. This
|
| 4 |
+
module selects records, checks separation, and records their provenance.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import argparse
|
| 8 |
+
from collections import Counter, defaultdict
|
| 9 |
+
from collections.abc import Iterator
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
import hashlib
|
| 12 |
+
import importlib
|
| 13 |
+
import json
|
| 14 |
+
import math
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
import random
|
| 17 |
+
import re
|
| 18 |
+
import string
|
| 19 |
+
import sys
|
| 20 |
+
import tarfile
|
| 21 |
+
from typing import Protocol, cast
|
| 22 |
+
import zipfile
|
| 23 |
+
|
| 24 |
+
import httpx
|
| 25 |
+
from PIL import Image, ImageDraw
|
| 26 |
+
|
| 27 |
+
from autojev.events import record
|
| 28 |
+
from autojev.types import Content, Example, JSONValue, Label, Question, Target
|
| 29 |
+
|
| 30 |
+
NIMBLE_REVISION = "d2387fc0b32d1173bfc995395c076a25a2a107c9"
|
| 31 |
+
SEED = 20260920
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@dataclass(frozen=True)
|
| 35 |
+
class Source:
|
| 36 |
+
repository: str
|
| 37 |
+
revision: str
|
| 38 |
+
config: str | None = None
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
SOURCES = {
|
| 42 |
+
"boolq": Source("google/boolq", "35b264d03638db9f4ce671b711558bf7ff0f80d5"),
|
| 43 |
+
"squad2": Source("rajpurkar/squad_v2", "3ffb306f725f7d2ce8394bc1873b24868140c412"),
|
| 44 |
+
"paws": Source("google-research-datasets/paws", "161ece9501cf0a11f3e48bd356eaa82de46d6a09", "labeled_final"),
|
| 45 |
+
"civil_comments": Source("google/civil_comments", "f2970eb3a55777454c94069077cc8d9b5866312d"),
|
| 46 |
+
"aegis2": Source("nvidia/Aegis-AI-Content-Safety-Dataset-2.0", "d86bb8bedff51d25ac834ab7838f1cc61acb7a2c"),
|
| 47 |
+
"pubmedqa": Source("qiaojin/PubMedQA", "9001f2853fb87cab8d220904e0de81ac6973b318", "pqa_labeled"),
|
| 48 |
+
}
|
| 49 |
+
ARCHIVES = {
|
| 50 |
+
"vitaminc": ("https://github.com/TalSchuster/talschuster.github.io/raw/master/static/vitaminc.zip", "vitaminc.zip"),
|
| 51 |
+
"multinli": ("https://cims.nyu.edu/~sbowman/multinli/multinli_1.0.zip", "multinli_1.0.zip"),
|
| 52 |
+
"massive": ("https://amazon-massive-nlu-dataset.s3.amazonaws.com/amazon-massive-dataset-1.1.tar.gz", "massive-1.1.tar.gz"),
|
| 53 |
+
}
|
| 54 |
+
ARCHIVE_SHA256 = {
|
| 55 |
+
"vitaminc": "49d82dc1690cbee420d18e2c26f687a7937710bb211845d2571430dfd4dc0337",
|
| 56 |
+
"multinli": "049f507b9e36b1fcb756cfd5aeb3b7a0cfcb84bf023793652987f7e7e0957822",
|
| 57 |
+
"massive": "4cba5faa11c71437928e17cb1b9b3d8b8e727e7ea363a3a9a8045e19c0491577",
|
| 58 |
+
}
|
| 59 |
+
TASKS = ("vitaminc-dev", "massive-en-US", "massive-de-DE", "boolq", "squad2",
|
| 60 |
+
"paws", "multinli", "civil_comments", "aegis2", "pubmedqa")
|
| 61 |
+
TRAIN_QUOTAS = dict(zip(TASKS, (16000, 10000, 10000, 7000, 14000, 12000, 12000, 8000, 6000, 0)))
|
| 62 |
+
TEST_QUOTAS = dict(zip(TASKS, (920, 540, 540, 460, 460, 385, 460, 460, 385, 385)))
|
| 63 |
+
DEV_QUOTAS = dict(zip(TASKS, (275, 160, 160, 140, 140, 115, 140, 140, 115, 115)))
|
| 64 |
+
TEMPERATURE_QUOTAS = dict(zip(TASKS, (185, 110, 110, 90, 90, 75, 90, 90, 75, 75)))
|
| 65 |
+
KNOWN_SPLITS = {"vitaminc": "dev", "massive": "test", "boolq": "validation",
|
| 66 |
+
"squad2": "validation", "paws": "test", "multinli": "dev_matched",
|
| 67 |
+
"civil_comments": "test", "aegis2": "test", "pubmedqa": "train"}
|
| 68 |
+
DEVELOPMENT_SPLITS = {**KNOWN_SPLITS, "massive": "dev", "paws": "validation",
|
| 69 |
+
"civil_comments": "validation", "aegis2": "validation"}
|
| 70 |
+
FINAL_SPLITS = {**KNOWN_SPLITS, "vitaminc": "test"}
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class Converter(Protocol):
|
| 74 |
+
SOURCE_URL: str
|
| 75 |
+
LICENSE: str
|
| 76 |
+
|
| 77 |
+
def record(self, raw: dict[str, JSONValue], subset: str = "") -> dict[str, JSONValue] | None: ...
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class Serializer(Protocol):
|
| 81 |
+
def jsonl_line(self, value: dict[str, JSONValue]) -> str: ...
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class Arguments(argparse.Namespace):
|
| 85 |
+
cache_root: Path
|
| 86 |
+
source_cache: Path | None
|
| 87 |
+
nimble_source: Path | None
|
| 88 |
+
output: Path
|
| 89 |
+
manifest: Path
|
| 90 |
+
seed: int
|
| 91 |
+
max_length: int
|
| 92 |
+
train_scale: float
|
| 93 |
+
image_train: int
|
| 94 |
+
image_eval: int
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def checksum(path: Path) -> str:
|
| 98 |
+
with path.open("rb") as stream:
|
| 99 |
+
return hashlib.file_digest(stream, "sha256").hexdigest()
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def normalized(value: str) -> str:
|
| 103 |
+
return " ".join(value.casefold().split())
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def digest(value: str) -> str:
|
| 107 |
+
return hashlib.sha256(value.encode()).hexdigest()
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def ranking(value: str, seed: int) -> str:
|
| 111 |
+
return digest(f"{seed}:{value}")
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def download(url: str, path: Path) -> None:
|
| 115 |
+
if path.exists():
|
| 116 |
+
return
|
| 117 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 118 |
+
temporary = path.with_suffix(path.suffix + ".part")
|
| 119 |
+
with httpx.stream("GET", url, follow_redirects=True, timeout=180) as response:
|
| 120 |
+
response.raise_for_status()
|
| 121 |
+
with temporary.open("wb") as stream:
|
| 122 |
+
for chunk in response.iter_bytes(1024 * 1024):
|
| 123 |
+
stream.write(chunk)
|
| 124 |
+
temporary.replace(path)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def checkout(path: Path | None, raw: Path) -> Path:
|
| 128 |
+
if path is not None:
|
| 129 |
+
if (path / ".revision").read_text().strip() != NIMBLE_REVISION:
|
| 130 |
+
raise ValueError("The supplied Nimble checkout is not the pinned revision")
|
| 131 |
+
return path.resolve()
|
| 132 |
+
path = raw / "nimble-source"
|
| 133 |
+
if not path.exists():
|
| 134 |
+
archive = raw / f"nimble-{NIMBLE_REVISION}.tar.gz"
|
| 135 |
+
download(f"https://codeload.github.com/bespokelabsai/nimble/tar.gz/{NIMBLE_REVISION}", archive)
|
| 136 |
+
with tarfile.open(archive) as bundle:
|
| 137 |
+
bundle.extractall(raw, filter="data")
|
| 138 |
+
(raw / f"nimble-{NIMBLE_REVISION}").rename(path)
|
| 139 |
+
(path / ".revision").write_text(NIMBLE_REVISION + "\n")
|
| 140 |
+
return path.resolve()
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
class RawData:
|
| 144 |
+
def __init__(self, args: Arguments) -> None:
|
| 145 |
+
self.cache = args.cache_root
|
| 146 |
+
self.supplied = args.source_cache
|
| 147 |
+
self.folder = args.output / "raw"
|
| 148 |
+
self.files: dict[str, dict[str, str]] = {}
|
| 149 |
+
|
| 150 |
+
def remember(self, path: Path, source: str) -> None:
|
| 151 |
+
key = f"{source}/{path.name}"
|
| 152 |
+
if key not in self.files:
|
| 153 |
+
self.files[key] = {"name": path.name, "sha256": checksum(path)}
|
| 154 |
+
|
| 155 |
+
def archive(self, source: str) -> Path:
|
| 156 |
+
url, name = ARCHIVES[source]
|
| 157 |
+
cached = self.supplied / name if self.supplied else self.folder / name
|
| 158 |
+
if not cached.exists():
|
| 159 |
+
cached = self.folder / name
|
| 160 |
+
download(url, cached)
|
| 161 |
+
self.remember(cached, source)
|
| 162 |
+
if self.files[f"{source}/{cached.name}"]["sha256"] != ARCHIVE_SHA256[source]:
|
| 163 |
+
raise ValueError(f"The {source} archive differs from the pinned checksum")
|
| 164 |
+
return cached
|
| 165 |
+
|
| 166 |
+
def rows(self, source: str, split: str, locale: str = "") -> Iterator[dict[str, JSONValue]]:
|
| 167 |
+
if source == "massive":
|
| 168 |
+
with tarfile.open(self.archive(source)) as archive:
|
| 169 |
+
member = next(m for m in archive.getmembers() if m.name.endswith(f"/data/{locale}.jsonl"))
|
| 170 |
+
stream = archive.extractfile(member)
|
| 171 |
+
assert stream is not None
|
| 172 |
+
for line in stream:
|
| 173 |
+
row = cast(dict[str, JSONValue], json.loads(line))
|
| 174 |
+
if row["partition"] == split:
|
| 175 |
+
yield row
|
| 176 |
+
return
|
| 177 |
+
if source in ("vitaminc", "multinli"):
|
| 178 |
+
member_name = f"vitaminc/{split}.jsonl" if source == "vitaminc" else f"multinli_1.0/multinli_1.0_{split}.jsonl"
|
| 179 |
+
with zipfile.ZipFile(self.archive(source)) as zip_archive, zip_archive.open(member_name) as zip_stream:
|
| 180 |
+
for line in zip_stream:
|
| 181 |
+
yield cast(dict[str, JSONValue], json.loads(line))
|
| 182 |
+
return
|
| 183 |
+
cached = self.supplied / source / f"{split}.jsonl" if self.supplied else self.folder / source / f"{split}.jsonl"
|
| 184 |
+
if cached.exists():
|
| 185 |
+
self.remember(cached, source)
|
| 186 |
+
with cached.open() as text_stream:
|
| 187 |
+
for index, text_line in enumerate(text_stream):
|
| 188 |
+
row = cast(dict[str, JSONValue], json.loads(text_line))
|
| 189 |
+
if source == "civil_comments":
|
| 190 |
+
row["row_index"] = index
|
| 191 |
+
yield row
|
| 192 |
+
return
|
| 193 |
+
from datasets import Dataset, load_dataset
|
| 194 |
+
|
| 195 |
+
spec = SOURCES[source]
|
| 196 |
+
paths = sorted((self.cache / "datasets").glob(f"**/{spec.revision}/*-{split}*.arrow"))
|
| 197 |
+
datasets: list[Dataset]
|
| 198 |
+
if paths:
|
| 199 |
+
datasets = [Dataset.from_file(str(path)) for path in paths]
|
| 200 |
+
for path in paths:
|
| 201 |
+
self.remember(path, source)
|
| 202 |
+
else:
|
| 203 |
+
datasets = [cast(Dataset, load_dataset(spec.repository, spec.config, split=split,
|
| 204 |
+
revision=spec.revision, cache_dir=str(self.folder / "hf")))]
|
| 205 |
+
index = 0
|
| 206 |
+
for dataset in datasets:
|
| 207 |
+
for value in dataset:
|
| 208 |
+
row = cast(dict[str, JSONValue], value)
|
| 209 |
+
if source == "civil_comments":
|
| 210 |
+
row["row_index"] = index
|
| 211 |
+
index += 1
|
| 212 |
+
yield row
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
class Families:
|
| 216 |
+
"""Conservative PAWS groups: shared sentences or identical token bags connect."""
|
| 217 |
+
|
| 218 |
+
def __init__(self, raw: RawData) -> None:
|
| 219 |
+
self.parents: dict[str, str] = {}
|
| 220 |
+
self.paws: dict[tuple[str, str], str] = {}
|
| 221 |
+
for split in ("train", "validation", "test"):
|
| 222 |
+
for row in raw.rows("paws", split):
|
| 223 |
+
values: list[str] = []
|
| 224 |
+
for key in ("sentence1", "sentence2"):
|
| 225 |
+
text = normalized(str(row[key]))
|
| 226 |
+
values.extend((digest(text), digest(" ".join(sorted(re.findall(r"\w+", text))))))
|
| 227 |
+
roots = [self.find(value) for value in values]
|
| 228 |
+
root = min(roots)
|
| 229 |
+
for other in roots:
|
| 230 |
+
self.parents[other] = root
|
| 231 |
+
self.paws[split, str(row["id"])] = root
|
| 232 |
+
|
| 233 |
+
def find(self, value: str) -> str:
|
| 234 |
+
self.parents.setdefault(value, value)
|
| 235 |
+
while value != self.parents[value]:
|
| 236 |
+
self.parents[value] = self.parents[self.parents[value]]
|
| 237 |
+
value = self.parents[value]
|
| 238 |
+
return value
|
| 239 |
+
|
| 240 |
+
def key(self, source: str, split: str, raw: dict[str, JSONValue]) -> str:
|
| 241 |
+
if source == "paws":
|
| 242 |
+
return self.find(self.paws[split, str(raw["id"])])
|
| 243 |
+
field = {"massive": "id", "vitaminc": "case_id", "multinli": "promptID", "pubmedqa": "pubid"}.get(source)
|
| 244 |
+
if field:
|
| 245 |
+
return str(raw[field])
|
| 246 |
+
field = {"boolq": "passage", "squad2": "context", "aegis2": "prompt", "civil_comments": "text"}[source]
|
| 247 |
+
return digest(normalized(str(raw[field])))
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def task_source(task: str) -> tuple[str, str]:
|
| 251 |
+
if task.startswith("massive-"):
|
| 252 |
+
return "massive", task.removeprefix("massive-")
|
| 253 |
+
return ("vitaminc", "") if task == "vitaminc-dev" else (task, "")
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def convert(raw: dict[str, JSONValue], task: str, split: str, converter: Converter,
|
| 257 |
+
families: Families) -> tuple[Example, dict[str, JSONValue]] | None:
|
| 258 |
+
source, locale = task_source(task)
|
| 259 |
+
value = converter.record(raw | {"partition": "test"} if source == "massive" else raw, locale)
|
| 260 |
+
if value is None:
|
| 261 |
+
return None
|
| 262 |
+
model_input = cast(dict[str, JSONValue], value["input"])
|
| 263 |
+
questions = cast(dict[str, Question], model_input["questions"])
|
| 264 |
+
question = questions["decision"]
|
| 265 |
+
reference = cast(dict[str, JSONValue], value["reference"])
|
| 266 |
+
label = cast(Label, reference["target"])
|
| 267 |
+
target: Target = label
|
| 268 |
+
distribution = reference.get("distribution")
|
| 269 |
+
if split == "train" and source != "pubmedqa" and isinstance(distribution, dict):
|
| 270 |
+
if question["type"] == "noul":
|
| 271 |
+
target = float(cast(float, distribution["true"]))
|
| 272 |
+
elif question["type"] == "choice":
|
| 273 |
+
target = [float(cast(float, distribution[key])) for key in question["criteria"]]
|
| 274 |
+
provenance: dict[str, JSONValue] = {
|
| 275 |
+
"dataset": source, "split": split, "upstream_id": value["id"], "url": converter.SOURCE_URL,
|
| 276 |
+
"license": converter.LICENSE, "nimble_revision": NIMBLE_REVISION,
|
| 277 |
+
}
|
| 278 |
+
if source in SOURCES:
|
| 279 |
+
provenance["revision"] = SOURCES[source].revision
|
| 280 |
+
if distribution is not None:
|
| 281 |
+
provenance["human_distribution"] = distribution
|
| 282 |
+
identifier = f"{task}:{split}:{value['id']}"
|
| 283 |
+
if split == "train" and source != "pubmedqa":
|
| 284 |
+
# Some upstream training IDs name several distinct sentence pairs.
|
| 285 |
+
identifier += ":" + digest(json.dumps(model_input["state"], sort_keys=True, ensure_ascii=False))[:16]
|
| 286 |
+
result: Example = {
|
| 287 |
+
"id": identifier, "suite": task,
|
| 288 |
+
"family": families.key(source, split, raw), "state": cast(Content, model_input["state"]),
|
| 289 |
+
"question": question, "target": target, "label": label, "source": provenance,
|
| 290 |
+
}
|
| 291 |
+
return result, value
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
def choose(rows: list[Example], quota: int, seed: int, excluded: set[str] | None = None) -> list[Example]:
|
| 295 |
+
groups: dict[str, list[Example]] = defaultdict(list)
|
| 296 |
+
for row in rows:
|
| 297 |
+
if excluded is None or row["family"] not in excluded:
|
| 298 |
+
groups[row["family"]].append(row)
|
| 299 |
+
selected: list[Example] = []
|
| 300 |
+
for family in sorted(groups, key=lambda key: ranking(key, seed)):
|
| 301 |
+
group = groups[family]
|
| 302 |
+
if len(selected) + len(group) <= quota:
|
| 303 |
+
selected.extend(sorted(group, key=lambda row: row["id"]))
|
| 304 |
+
return selected
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def text_parts(row: Example) -> list[str]:
|
| 308 |
+
state = row["state"]
|
| 309 |
+
if isinstance(state, str):
|
| 310 |
+
return [state]
|
| 311 |
+
if isinstance(state, dict):
|
| 312 |
+
return [value for value in state.values() if isinstance(value, str)]
|
| 313 |
+
return [json.dumps(state, sort_keys=True)]
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
def state_key(row: Example) -> str:
|
| 317 |
+
return digest(normalized(json.dumps(row["state"], sort_keys=True, ensure_ascii=False)))
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
class NearDuplicates:
|
| 321 |
+
"""Candidate shingle index followed by exact Jaccard checks; no label access."""
|
| 322 |
+
|
| 323 |
+
def __init__(self) -> None:
|
| 324 |
+
self.parts: list[set[str]] = []
|
| 325 |
+
self.index: dict[str, list[int]] = defaultdict(list)
|
| 326 |
+
|
| 327 |
+
@staticmethod
|
| 328 |
+
def shingles(text: str) -> set[str]:
|
| 329 |
+
words = re.findall(r"\w+", normalized(text))
|
| 330 |
+
return {" ".join(words[index:index + 5]) for index in range(len(words) - 4)} if len(words) >= 20 else set()
|
| 331 |
+
|
| 332 |
+
def add(self, row: Example) -> None:
|
| 333 |
+
for text in text_parts(row):
|
| 334 |
+
values = self.shingles(text)
|
| 335 |
+
if values:
|
| 336 |
+
index = len(self.parts)
|
| 337 |
+
self.parts.append(values)
|
| 338 |
+
for anchor in sorted(values, key=digest)[:8]:
|
| 339 |
+
self.index[anchor].append(index)
|
| 340 |
+
|
| 341 |
+
def matches(self, row: Example) -> bool:
|
| 342 |
+
for text in text_parts(row):
|
| 343 |
+
values = self.shingles(text)
|
| 344 |
+
if not values:
|
| 345 |
+
continue
|
| 346 |
+
candidates = {index for anchor in sorted(values, key=digest)[:8] for index in self.index.get(anchor, [])}
|
| 347 |
+
for index in candidates:
|
| 348 |
+
other = self.parts[index]
|
| 349 |
+
if len(values & other) / len(values | other) >= 0.85:
|
| 350 |
+
return True
|
| 351 |
+
return False
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
def write_rows(path: Path, rows: list[Example]) -> str:
|
| 355 |
+
payload = "".join(json.dumps(row, ensure_ascii=True, allow_nan=False) + "\n" for row in rows)
|
| 356 |
+
if path.exists() and path.read_text() != payload:
|
| 357 |
+
raise FileExistsError(f"Refusing to replace a different frozen dataset: {path}")
|
| 358 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 359 |
+
path.write_text(payload)
|
| 360 |
+
return checksum(path)
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
def images(count: int, split: str, seed: int, output: Path) -> list[Example]:
|
| 364 |
+
rng = random.Random(seed)
|
| 365 |
+
colors = {"red": "#df3030", "blue": "#3068da", "green": "#259951", "orange": "#ed911d"}
|
| 366 |
+
shapes = ("circle", "square", "triangle")
|
| 367 |
+
rows: list[Example] = []
|
| 368 |
+
directory = output / "images" / split
|
| 369 |
+
directory.mkdir(parents=True, exist_ok=True)
|
| 370 |
+
for scene in range(math.ceil(count / 4)):
|
| 371 |
+
picture = Image.new("RGB", (224, 224), "#f9f6ee")
|
| 372 |
+
drawing = ImageDraw.Draw(picture)
|
| 373 |
+
objects: list[tuple[str, str]] = []
|
| 374 |
+
for x in (55, 169):
|
| 375 |
+
color, shape = rng.choice(list(colors)), rng.choice(shapes)
|
| 376 |
+
y, radius = rng.randint(85, 135), rng.randint(25, 40)
|
| 377 |
+
box = (x - radius, y - radius, x + radius, y + radius)
|
| 378 |
+
if shape == "circle":
|
| 379 |
+
drawing.ellipse(box, fill=colors[color])
|
| 380 |
+
elif shape == "square":
|
| 381 |
+
drawing.rectangle(box, fill=colors[color])
|
| 382 |
+
else:
|
| 383 |
+
drawing.polygon(((x, y - radius), (x + radius, y + radius), (x - radius, y + radius)), fill=colors[color])
|
| 384 |
+
objects.append((color, shape))
|
| 385 |
+
image_path = directory / f"{scene:04}.png"
|
| 386 |
+
picture.save(image_path)
|
| 387 |
+
for side, (color, shape) in zip(("left", "right"), objects):
|
| 388 |
+
for attribute, label, options in (("color", color, list(colors)), ("shape", shape, list(shapes))):
|
| 389 |
+
rows.append({"id": f"images:{split}:{scene}:{side}:{attribute}", "suite": "image_retention",
|
| 390 |
+
"family": f"{split}:{scene}", "state": "Use the picture to answer the question.",
|
| 391 |
+
"images": [str(image_path)], "label": label, "target": label,
|
| 392 |
+
"question": {"type": "choice", "instructions": f"What is the {attribute} of the {side} object?",
|
| 393 |
+
"criteria": dict.fromkeys(options)},
|
| 394 |
+
"source": {"dataset": "generated_geometry", "split": split,
|
| 395 |
+
"seed": seed, "image_sha256": checksum(image_path), "license": "Apache-2.0"}})
|
| 396 |
+
return rows[:count]
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
def validate(rows: list[Example]) -> None:
|
| 400 |
+
identifiers: set[str] = set()
|
| 401 |
+
for row in rows:
|
| 402 |
+
if row["id"] in identifiers:
|
| 403 |
+
raise ValueError(f"Duplicate example ID: {row['id']}")
|
| 404 |
+
identifiers.add(row["id"])
|
| 405 |
+
question, target, label = row["question"], row["target"], row["label"]
|
| 406 |
+
if not row["family"]:
|
| 407 |
+
raise ValueError("Missing family")
|
| 408 |
+
if question["type"] == "choice":
|
| 409 |
+
options = list(question["criteria"])
|
| 410 |
+
if not isinstance(label, str) or label not in options:
|
| 411 |
+
raise ValueError("Invalid Choice hard label")
|
| 412 |
+
if isinstance(target, list):
|
| 413 |
+
if len(target) != len(options) or abs(sum(target) - 1) > 1e-6 or any(not math.isfinite(p) or p < 0 for p in target):
|
| 414 |
+
raise ValueError("Invalid soft Choice target")
|
| 415 |
+
elif target not in options:
|
| 416 |
+
raise ValueError("Invalid Choice target")
|
| 417 |
+
elif question["type"] == "noul":
|
| 418 |
+
if type(label) is not bool or not isinstance(target, (bool, float)) or not 0 <= target <= 1:
|
| 419 |
+
raise ValueError("Invalid Noul label/target")
|
| 420 |
+
elif question["type"] == "score": # team-1: ordered rubric, label = level index as a string ("0".."n-1")
|
| 421 |
+
levels = [str(i) for i in range(len(question["criteria"]))]
|
| 422 |
+
if not isinstance(label, str) or label not in levels:
|
| 423 |
+
raise ValueError("Invalid Score hard label")
|
| 424 |
+
if isinstance(target, list):
|
| 425 |
+
if len(target) != len(levels) or abs(sum(target) - 1) > 1e-6 or any(not math.isfinite(p) or p < 0 for p in target):
|
| 426 |
+
raise ValueError("Invalid soft Score target")
|
| 427 |
+
elif target not in levels:
|
| 428 |
+
raise ValueError("Invalid Score target")
|
| 429 |
+
else:
|
| 430 |
+
raise ValueError("The accuracy experiment admits only Choice, Noul and Score examples")
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
def token_lengths(rows: list[Example], cache: Path) -> None:
|
| 434 |
+
"""Measure the exact shared prompt with the processor, without model weights."""
|
| 435 |
+
from transformers import AutoProcessor
|
| 436 |
+
from transformers.models.qwen3_vl.processing_qwen3_vl import Qwen3VLProcessor
|
| 437 |
+
from autojev.model import BASE_MODEL, BASE_REVISION, decision_messages, open_image
|
| 438 |
+
|
| 439 |
+
processor = cast(Qwen3VLProcessor, AutoProcessor.from_pretrained(
|
| 440 |
+
BASE_MODEL, revision=BASE_REVISION, cache_dir=str(cache / "hub")))
|
| 441 |
+
processor.image_processor.size = {"shortest_edge": 65536, "longest_edge": 262144}
|
| 442 |
+
codes = [code for code in string.ascii_uppercase if len(processor.tokenizer.encode(code, add_special_tokens=False)) == 1]
|
| 443 |
+
if len(codes) != 26:
|
| 444 |
+
raise ValueError("The benchmark requires the model's first 26 answer codes")
|
| 445 |
+
text_rows = [row for row in rows if not row.get("images")]
|
| 446 |
+
for start in range(0, len(text_rows), 512):
|
| 447 |
+
batch = text_rows[start:start + 512]
|
| 448 |
+
texts = [processor.apply_chat_template(decision_messages(row, codes), tokenize=False, # type: ignore[arg-type]
|
| 449 |
+
add_generation_prompt=True, enable_thinking=False) for row in batch]
|
| 450 |
+
encoded = cast(list[list[int]], processor.tokenizer(texts, padding=False, truncation=False)["input_ids"])
|
| 451 |
+
for row, tokens in zip(batch, encoded):
|
| 452 |
+
row["source"]["input_tokens"] = len(tokens)
|
| 453 |
+
for row in rows:
|
| 454 |
+
if not row.get("images"):
|
| 455 |
+
continue
|
| 456 |
+
text = processor.apply_chat_template(decision_messages(row, codes), tokenize=False, # type: ignore[arg-type]
|
| 457 |
+
add_generation_prompt=True, enable_thinking=False)
|
| 458 |
+
encoded_images = processor(text=[text], images=[open_image(image) for image in row.get("images", [])])
|
| 459 |
+
row["source"]["input_tokens"] = len(cast(list[list[int]], encoded_images["input_ids"])[0])
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
def build(args: Arguments) -> None:
|
| 463 |
+
args.output.mkdir(parents=True, exist_ok=True)
|
| 464 |
+
raw = RawData(args)
|
| 465 |
+
dependency = checkout(args.nimble_source, raw.folder)
|
| 466 |
+
sys.path.insert(0, str(dependency))
|
| 467 |
+
serializer = cast(Serializer, importlib.import_module("nimble.datasets.public_benchmarks"))
|
| 468 |
+
families = Families(raw)
|
| 469 |
+
folds: dict[str, list[Example]] = {name: [] for name in ("known", "test", "dev", "temperature", "train")}
|
| 470 |
+
report: dict[str, object] = {"seed": args.seed, "nimble_revision": NIMBLE_REVISION, "tasks": {}, "sources": {}}
|
| 471 |
+
task_reports = cast(dict[str, object], report["tasks"])
|
| 472 |
+
source_reports = cast(dict[str, object], report["sources"])
|
| 473 |
+
protected: dict[str, set[str]] = defaultdict(set)
|
| 474 |
+
record("data_started", seed=args.seed, tasks=list(TASKS))
|
| 475 |
+
for task in TASKS:
|
| 476 |
+
source, locale = task_source(task)
|
| 477 |
+
converter = cast(Converter, importlib.import_module(f"nimble.datasets.public_sources.{source}"))
|
| 478 |
+
published = json.loads((dependency / f"docs/assets/public-benchmarks/subsets/{task}-manifest.json").read_text())
|
| 479 |
+
known_ids = set(published["ids"])
|
| 480 |
+
pools: dict[str, list[Example]] = defaultdict(list)
|
| 481 |
+
exact_known: list[dict[str, JSONValue]] = []
|
| 482 |
+
stats: Counter[str] = Counter()
|
| 483 |
+
for split in sorted({KNOWN_SPLITS[source], FINAL_SPLITS[source], DEVELOPMENT_SPLITS[source]}):
|
| 484 |
+
for value in raw.rows(source, split, locale):
|
| 485 |
+
result = convert(value, task, split, converter, families)
|
| 486 |
+
if result is None:
|
| 487 |
+
stats["invalid_heldout_rows"] += 1
|
| 488 |
+
continue
|
| 489 |
+
row, upstream = result
|
| 490 |
+
protected[source].add(row["family"])
|
| 491 |
+
if split == KNOWN_SPLITS[source] and upstream["id"] in known_ids:
|
| 492 |
+
folds["known"].append(row)
|
| 493 |
+
exact_known.append(upstream)
|
| 494 |
+
pools[split].append(row)
|
| 495 |
+
unique_ids: dict[str, Example] = {}
|
| 496 |
+
for candidate in pools[split]:
|
| 497 |
+
previous_row = unique_ids.get(candidate["id"])
|
| 498 |
+
if previous_row is not None:
|
| 499 |
+
if previous_row["state"] != candidate["state"] or previous_row["label"] != candidate["label"]:
|
| 500 |
+
raise ValueError("Conflicting rows share an upstream example ID")
|
| 501 |
+
stats["duplicate_heldout_ids_removed"] += 1
|
| 502 |
+
unique_ids.setdefault(candidate["id"], candidate)
|
| 503 |
+
pools[split] = list(unique_ids.values())
|
| 504 |
+
known_payload = "".join(serializer.jsonl_line(row) for row in sorted(exact_known, key=lambda row: str(row["id"])))
|
| 505 |
+
if digest(known_payload) != published["dataset_sha256"]:
|
| 506 |
+
raise ValueError(f"Known benchmark content changed for {task}")
|
| 507 |
+
# Stronger grouping can connect known rows to extra sibling examples.
|
| 508 |
+
excluded = {row["family"] for row in folds["known"] if row["source"]["dataset"] == source}
|
| 509 |
+
for fold, quota, split in (("test", TEST_QUOTAS[task], FINAL_SPLITS[source]),
|
| 510 |
+
("dev", DEV_QUOTAS[task], DEVELOPMENT_SPLITS[source]),
|
| 511 |
+
("temperature", TEMPERATURE_QUOTAS[task], DEVELOPMENT_SPLITS[source])):
|
| 512 |
+
# MASSIVE locales must receive exactly the same family IDs.
|
| 513 |
+
if locale == "de-DE":
|
| 514 |
+
wanted = {row["family"] for row in folds[fold] if row["suite"] == "massive-en-US"}
|
| 515 |
+
selected = [row for row in pools[split] if row["family"] in wanted]
|
| 516 |
+
if len(selected) != len(wanted):
|
| 517 |
+
raise ValueError("A MASSIVE translation is missing")
|
| 518 |
+
else:
|
| 519 |
+
selected = choose(pools[split], quota, args.seed + {"test": 1, "dev": 2, "temperature": 3}[fold], excluded)
|
| 520 |
+
folds[fold].extend(selected)
|
| 521 |
+
excluded.update(row["family"] for row in selected)
|
| 522 |
+
stats[fold] = len(selected)
|
| 523 |
+
stats["known"] = len(exact_known)
|
| 524 |
+
source_reports[source] = {"url": converter.SOURCE_URL, "license": converter.LICENSE,
|
| 525 |
+
"revision": SOURCES[source].revision if source in SOURCES else None,
|
| 526 |
+
"converter_sha256": checksum(dependency / f"nimble/datasets/public_sources/{source}.py")}
|
| 527 |
+
task_reports[task] = dict(stats)
|
| 528 |
+
record("data_task_partitioned", task=task, counts=dict(stats))
|
| 529 |
+
|
| 530 |
+
# Keep folds disjoint even when repeated text carries different upstream IDs.
|
| 531 |
+
protected_states: set[str] = set()
|
| 532 |
+
protected_near = NearDuplicates()
|
| 533 |
+
for fold in ("known", "test", "dev", "temperature"):
|
| 534 |
+
if fold != "known":
|
| 535 |
+
bad_families = {(str(row["source"]["dataset"]), row["family"]) for row in folds[fold]
|
| 536 |
+
if state_key(row) in protected_states or protected_near.matches(row)}
|
| 537 |
+
folds[fold] = [row for row in folds[fold] if (str(row["source"]["dataset"]), row["family"]) not in bad_families]
|
| 538 |
+
report[f"{fold}_duplicate_families_removed"] = len(bad_families)
|
| 539 |
+
for row in folds[fold]:
|
| 540 |
+
protected_states.add(state_key(row))
|
| 541 |
+
protected_near.add(row)
|
| 542 |
+
|
| 543 |
+
token_lengths([row for name, rows in folds.items() if name != "train" for row in rows], args.cache_root)
|
| 544 |
+
early_partitions: dict[str, object] = {}
|
| 545 |
+
for name in ("known", "test", "dev", "temperature"):
|
| 546 |
+
rows = folds[name]
|
| 547 |
+
overlong = {(str(row["source"]["dataset"]), row["family"]) for row in rows
|
| 548 |
+
if cast(int, row["source"]["input_tokens"]) > args.max_length}
|
| 549 |
+
if name == "known" and overlong:
|
| 550 |
+
raise ValueError("The known benchmark exceeds the token limit; never silently change its records")
|
| 551 |
+
folds[name] = [row for row in rows if (str(row["source"]["dataset"]), row["family"]) not in overlong]
|
| 552 |
+
report[f"{name}_overlong_families_excluded"] = len(overlong)
|
| 553 |
+
validate(folds[name])
|
| 554 |
+
early_partitions[name] = {"rows": len(folds[name]), "sha256": write_rows(args.output / f"{name}.jsonl", folds[name]),
|
| 555 |
+
"tasks": dict(Counter(row["suite"] for row in folds[name]))}
|
| 556 |
+
args.manifest.parent.mkdir(parents=True, exist_ok=True)
|
| 557 |
+
(args.manifest.parent / "evaluation-partitions.json").write_text(json.dumps(early_partitions, indent=2) + "\n")
|
| 558 |
+
record("evaluation_data_frozen", partitions=early_partitions)
|
| 559 |
+
|
| 560 |
+
# Training-family sampling is label-blind. Soft human annotations are targets
|
| 561 |
+
# only for sources that actually supply them; teacher confidence is not used.
|
| 562 |
+
for task in TASKS:
|
| 563 |
+
quota = int(TRAIN_QUOTAS[task] * args.train_scale)
|
| 564 |
+
if not quota:
|
| 565 |
+
continue
|
| 566 |
+
source, locale = task_source(task)
|
| 567 |
+
converter = cast(Converter, importlib.import_module(f"nimble.datasets.public_sources.{source}"))
|
| 568 |
+
candidates: list[Example] = []
|
| 569 |
+
stats = Counter[str]()
|
| 570 |
+
pool_fraction = min(1.0, quota * 5 / {"vitaminc": 370653, "multinli": 392702, "civil_comments": 1804874}.get(source, 1))
|
| 571 |
+
for value in raw.rows(source, "train", locale):
|
| 572 |
+
family = families.key(source, "train", value)
|
| 573 |
+
if family in protected[source]:
|
| 574 |
+
stats["heldout_family_rows_excluded"] += 1
|
| 575 |
+
continue
|
| 576 |
+
if int(ranking(family, args.seed), 16) / 2**256 >= pool_fraction:
|
| 577 |
+
continue
|
| 578 |
+
result = convert(value, task, "train", converter, families)
|
| 579 |
+
if result is None:
|
| 580 |
+
stats["invalid_training_rows"] += 1
|
| 581 |
+
continue
|
| 582 |
+
row, _ = result
|
| 583 |
+
candidates.append(row)
|
| 584 |
+
bad = {row["family"] for row in candidates
|
| 585 |
+
if state_key(row) in protected_states or protected_near.matches(row)}
|
| 586 |
+
candidates = [row for row in candidates if row["family"] not in bad]
|
| 587 |
+
unique: dict[str, Example] = {}
|
| 588 |
+
conflicts: set[str] = set()
|
| 589 |
+
for row in candidates:
|
| 590 |
+
key = state_key(row)
|
| 591 |
+
if key in unique and unique[key]["label"] != row["label"]:
|
| 592 |
+
conflicts.add(key)
|
| 593 |
+
unique.setdefault(key, row)
|
| 594 |
+
candidates = [row for key, row in unique.items() if key not in conflicts]
|
| 595 |
+
selected = choose(candidates, quota, args.seed + 4)
|
| 596 |
+
if len(selected) < 0.8 * quota:
|
| 597 |
+
raise ValueError(f"Too few training rows for {task}: {len(selected)} / {quota}")
|
| 598 |
+
folds["train"].extend(selected)
|
| 599 |
+
stats.update({"train": len(selected), "near_duplicate_families_excluded": len(bad),
|
| 600 |
+
"conflicting_state_labels_excluded": len(conflicts)})
|
| 601 |
+
cast(dict[str, object], task_reports[task]).update(dict(stats))
|
| 602 |
+
record("data_training_task", task=task, counts=dict(stats))
|
| 603 |
+
|
| 604 |
+
folds["train"].extend(images(args.image_train, "train", args.seed + 100, args.output))
|
| 605 |
+
folds["image_test"] = images(args.image_eval, "test", args.seed + 101, args.output)
|
| 606 |
+
token_lengths(folds["train"] + folds["image_test"], args.cache_root)
|
| 607 |
+
for name in ("train", "image_test"):
|
| 608 |
+
rows = folds[name]
|
| 609 |
+
overlong = {(str(row["source"]["dataset"]), row["family"]) for row in rows
|
| 610 |
+
if cast(int, row["source"]["input_tokens"]) > args.max_length}
|
| 611 |
+
if name == "known" and overlong:
|
| 612 |
+
raise ValueError("The known benchmark exceeds the model token limit; never silently change its records")
|
| 613 |
+
folds[name] = [row for row in rows if (str(row["source"]["dataset"]), row["family"]) not in overlong]
|
| 614 |
+
report[f"{name}_overlong_families_excluded"] = len(overlong)
|
| 615 |
+
random.Random(args.seed).shuffle(folds["train"])
|
| 616 |
+
partitions: dict[str, object] = {}
|
| 617 |
+
previous: dict[tuple[str, str], str] = {}
|
| 618 |
+
for name, rows in folds.items():
|
| 619 |
+
validate(rows)
|
| 620 |
+
# Known contains fixed upstream rows; sibling locales are one family.
|
| 621 |
+
keys = {(str(row["source"]["dataset"]), row["family"]) for row in rows}
|
| 622 |
+
for family_key in keys:
|
| 623 |
+
if family_key in previous:
|
| 624 |
+
raise ValueError(f"Family crossed {previous[family_key]} and {name}")
|
| 625 |
+
previous[family_key] = name
|
| 626 |
+
path = args.output / f"{name}.jsonl"
|
| 627 |
+
partitions[name] = {"rows": len(rows), "families": len(keys), "sha256": write_rows(path, rows),
|
| 628 |
+
"tasks": dict(Counter(row["suite"] for row in rows)),
|
| 629 |
+
"tokens": sum(cast(int, row["source"]["input_tokens"]) for row in rows),
|
| 630 |
+
"maximum_input_tokens": max(cast(int, row["source"]["input_tokens"]) for row in rows),
|
| 631 |
+
"question_types": dict(Counter(row["question"]["type"] for row in rows))}
|
| 632 |
+
identifiers = [{"id": row["id"], "dataset": row["source"]["dataset"], "family": row["family"]} for row in rows]
|
| 633 |
+
identity_path = args.manifest.parent / "splits" / f"{name}.json"
|
| 634 |
+
identity_path.parent.mkdir(parents=True, exist_ok=True)
|
| 635 |
+
identity_path.write_text(json.dumps(identifiers, separators=(",", ":")) + "\n")
|
| 636 |
+
report.update({"partitions": partitions, "raw_files": raw.files, "generator_sha256": checksum(Path(__file__)),
|
| 637 |
+
"quotas": {"train": TRAIN_QUOTAS, "test": TEST_QUOTAS, "dev": DEV_QUOTAS, "temperature": TEMPERATURE_QUOTAS},
|
| 638 |
+
"max_input_tokens": args.max_length, "train_scale": args.train_scale,
|
| 639 |
+
"family_policy": "Whole source families; shared MASSIVE IDs; normalized passages/prompts; PAWS connected sentence/token-bag components.",
|
| 640 |
+
"near_duplicate_policy": "Five-word shingle Jaccard >= 0.85, candidates retrieved by eight minimum SHA256 anchors; text parts below 20 words use exact checks only.",
|
| 641 |
+
"known_suite": "The 3247 original Choice/Noul records are verified against pinned Nimble checksums before adding provenance.",
|
| 642 |
+
"limitations": ["PAWS original generation mapping is unavailable (official URL returned 403); sentence/token-bag components and shingle checks are heuristic.",
|
| 643 |
+
"Near-duplicate retrieval is approximate and does not prove absence of every paraphrase or pretraining overlap.",
|
| 644 |
+
"PubMedQA labeled data are reserved for evaluation; no in-domain PubMedQA training in the first corpus.",
|
| 645 |
+
"Image retention is controlled geometry, not a general vision benchmark.",
|
| 646 |
+
"Fresh means unused in this project's tuning, not necessarily unseen by the base model or Jev."]})
|
| 647 |
+
args.manifest.parent.mkdir(parents=True, exist_ok=True)
|
| 648 |
+
args.manifest.write_text(json.dumps(report, indent=2) + "\n")
|
| 649 |
+
record("data_frozen", manifest_sha256=checksum(args.manifest), partitions=partitions)
|
| 650 |
+
print(json.dumps(partitions, indent=2), flush=True)
|
| 651 |
+
|
| 652 |
+
|
| 653 |
+
def main() -> None:
|
| 654 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 655 |
+
parser.add_argument("--cache-root", type=Path, default=Path.home() / ".cache/huggingface")
|
| 656 |
+
parser.add_argument("--source-cache", type=Path)
|
| 657 |
+
parser.add_argument("--nimble-source", type=Path)
|
| 658 |
+
parser.add_argument("--output", type=Path, default=Path("data"))
|
| 659 |
+
parser.add_argument("--manifest", type=Path, default=Path("progress/data-manifest.json"))
|
| 660 |
+
parser.add_argument("--seed", type=int, default=SEED)
|
| 661 |
+
parser.add_argument("--max-length", type=int, default=8192)
|
| 662 |
+
parser.add_argument("--train-scale", type=float, default=1.0)
|
| 663 |
+
parser.add_argument("--image-train", type=int, default=1000)
|
| 664 |
+
parser.add_argument("--image-eval", type=int, default=200)
|
| 665 |
+
build(parser.parse_args(namespace=Arguments()))
|
| 666 |
+
|
| 667 |
+
|
| 668 |
+
if __name__ == "__main__":
|
| 669 |
+
main()
|