fev-4b / fev4b /data.py
murabcd's picture
feat: add compiled serving shared training and benchmarks
5adf069 verified
Raw History Blame Contribute Delete
5.49 kB
"""Labelled typed requests and leakage-aware deterministic data splitting."""
import argparse
import hashlib
import json
import random
from pathlib import Path
from .decisions import validate_request
def canonical(value):
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(',', ':'), allow_nan=False)
def state_hash(row):
return hashlib.sha256(canonical(row['state']).encode()).hexdigest()
def request_of(row):
return validate_request({'state': row['state'], 'questions': {
key: {k: v for k, v in q.items() if k != 'label'} for key, q in row['questions'].items()}})
def option_keys(question):
if question['type'] == 'choice':
return list(question['criteria'])
if question['type'] == 'noul':
return ['false', 'true']
return [str(i) for i in range(len(question['criteria']))]
def target_index(question):
label = question['label']
if question['type'] == 'noul':
if type(label) is not bool:
raise ValueError('Noul label must be boolean')
return int(label)
if question['type'] == 'score':
if type(label) is not int or not 0 <= label < len(question['criteria']):
raise ValueError('Score label must be a zero-based level index')
return label
if not isinstance(label, str) or label not in question['criteria']:
raise ValueError('Choice label must name an option')
return list(question['criteria']).index(label)
def load_records(path):
records = []
seen = set()
for line in Path(path).read_text().splitlines():
if not line.strip():
continue
row = json.loads(line)
if (not isinstance(row, dict) or set(row) - {'id', 'group', 'domain', 'source', 'state', 'questions'}
or not isinstance(row.get('id'), str) or not row['id'] or row['id'] in seen):
raise ValueError('Records require unique nonempty IDs and supported fields')
for name in ('group', 'domain', 'source'):
if name in row and (not isinstance(row[name], str) or not row[name]):
raise ValueError('Record metadata must be nonempty strings')
request_of(row)
for question in row['questions'].values():
if set(question) - {'type', 'instructions', 'criteria', 'label'} or 'label' not in question:
raise ValueError('Every question needs a label')
target_index(question)
seen.add(row['id'])
records.append(row)
if not records:
raise ValueError('Dataset is empty')
return records
def require_disjoint(*partitions):
prior_ids, prior_states, prior_groups = set(), set(), set()
for records in partitions:
ids = {r['id'] for r in records}
states = {state_hash(r) for r in records}
groups = {r['group'] for r in records if 'group' in r}
if ids & prior_ids or states & prior_states or groups & prior_groups:
raise ValueError('Partitions overlap by ID, exact state, or supplied group')
prior_ids |= ids
prior_states |= states
prior_groups |= groups
def split_records(records, seed=42):
# Connected components prevent a repeated state from bridging two groups.
parent = list(range(len(records)))
def find(i):
while parent[i] != i:
parent[i] = parent[parent[i]]
i = parent[i]
return i
owners = {}
for i, row in enumerate(records):
keys = [('state', state_hash(row))]
if 'group' in row:
keys.append(('group', row['group']))
for key in keys:
if key in owners:
parent[find(i)] = find(owners[key])
owners[key] = i
groups = {}
for i, row in enumerate(records):
groups.setdefault(find(i), []).append(row)
values = list(groups.values())
if len(values) < 4:
raise ValueError('Need at least four independent groups for train/dev/calibration/test')
random.Random(seed).shuffle(values)
n = max(1, len(values) // 10)
boundaries = (len(values) - 3*n, len(values) - 2*n, len(values) - n, len(values))
result = {}
start = 0
for name, stop in zip(('train', 'development', 'calibration', 'test'), boundaries):
result[name] = [row for group in values[start:stop] for row in group]
start = stop
require_disjoint(*result.values())
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--data', type=Path, required=True)
parser.add_argument('--out', type=Path, required=True)
parser.add_argument('--seed', type=int, default=42)
args = parser.parse_args()
records = load_records(args.data)
partitions = split_records(records, args.seed)
args.out.mkdir(parents=True, exist_ok=False)
manifest = {'seed': args.seed, 'sourceSha256': hashlib.sha256(args.data.read_bytes()).hexdigest(),
'grouping': 'connected exact states and supplied family groups; not fuzzy decontamination', 'files': {}}
for name, rows in partitions.items():
path = args.out / (name + '.jsonl')
path.write_text(''.join(canonical(row) + '\n' for row in rows))
manifest['files'][path.name] = {'records': len(rows), 'sha256': hashlib.sha256(path.read_bytes()).hexdigest()}
(args.out / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n')
print(json.dumps(manifest))
if __name__ == '__main__':
main()