hitit-cuneiform-ocr / code /src /preprocessing /rebuild_stratified_folds.py
savastakan's picture
Initial upload: code + 5 record checkpoints + fuse
f211247 verified
Raw
History Blame Contribute Delete
3.37 kB
#!/usr/bin/env python3
"""Rebuild tablet_view_fold with class-stratification.
Problem: current folds are tablet-level; some classes end up in val
with 0 train samples. This caps val accuracy.
Solution: hash(tablet_id) still drives fold to avoid tablet leakage, but
we re-balance so every class appears in train (by promoting isolated val
samples to train). For classes with ≥5 samples we keep fold; for <5 we
force all samples to fold != 0 (i.e., into train).
"""
import json, argparse, shutil
from collections import defaultdict, Counter
from pathlib import Path
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--manifest', required=True)
ap.add_argument('--output', required=True)
ap.add_argument('--val-fold', type=int, default=0)
ap.add_argument('--min-per-class-train', type=int, default=3,
help='Force classes with < N train samples to have 0 val samples')
args = ap.parse_args()
records = []
with open(args.manifest) as f:
for line in f:
r = json.loads(line)
records.append(r)
# Classify
per_class_all = defaultdict(list)
for r in records:
if r.get('task') != 'classification': continue
if not r.get('unified_label'): continue
per_class_all[r['unified_label']].append(r)
# Current train/val distribution
cls_train_count = Counter()
cls_val_count = Counter()
for label, recs in per_class_all.items():
for r in recs:
if r.get('tablet_view_fold', 0) == args.val_fold:
cls_val_count[label] += 1
else:
cls_train_count[label] += 1
val_only = [l for l in per_class_all if cls_train_count[l] == 0]
thin_train = [l for l in per_class_all
if cls_train_count[l] < args.min_per_class_train]
print(f"Classes total: {len(per_class_all)}")
print(f"Val-only (0 train): {len(val_only)}")
print(f"Thin train (<{args.min_per_class_train}): {len(thin_train)}")
# Promote: for thin classes, flip all records to non-val-fold (fold=1)
flipped = 0
promote = set(thin_train + val_only)
for r in records:
if r.get('task') != 'classification': continue
if r.get('unified_label') in promote and r.get('tablet_view_fold', 0) == args.val_fold:
r['tablet_view_fold'] = 1
r['stratified_promoted'] = True
flipped += 1
print(f"Flipped val→train: {flipped}")
# Write
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
with open(args.output, 'w') as f:
for r in records:
f.write(json.dumps(r) + '\n')
# Report post
cls_train_post = Counter()
cls_val_post = Counter()
for r in records:
if r.get('task') != 'classification': continue
if not r.get('unified_label'): continue
if r.get('tablet_view_fold', 0) == args.val_fold:
cls_val_post[r['unified_label']] += 1
else:
cls_train_post[r['unified_label']] += 1
post_val_only = [l for l in per_class_all if cls_train_post[l] == 0]
print(f"\nAfter stratification:")
print(f" Val-only: {len(post_val_only)}")
print(f" Val size: {sum(cls_val_post.values())}")
print(f" Train size: {sum(cls_train_post.values())}")
print(f"Output: {args.output}")
if __name__ == '__main__':
main()