Download fev4b/data.py from murabcd/fev-4b: direct link, hf CLI and curl.
- Browser
- Download file 5.49 kB
-
https://huggingface.co/murabcd/fev-4b/resolve/main/fev4b/data.py
- Command line
-
hf download hf://murabcd/fev-4b/fev4b/data.py
-
curl -L -o data.py https://huggingface.co/murabcd/fev-4b/resolve/main/fev4b/data.py
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() | |