Spaces:
Running
Running
| """Reshuffle the multiple-choice option ordering for every EgoMemReason question. | |
| Rationale: the private answer key was leaked on 2026-06-17 (a submission | |
| scored 100% overall). Rotating tokens and gating the public leaderboard | |
| prevents new leakage, but does *not* invalidate the copy already exfiltrated. | |
| This script permutes, per question, which option-string each letter (A-J) | |
| points to. Any cached copy of the previous answer key becomes worse than | |
| random — the letters no longer identify the same options. | |
| Design constraints: | |
| - The *set* of letters used per question is unchanged (a 4-option question | |
| still uses A-D, a 10-option one still uses A-J). Only the mapping from | |
| letter to option-string changes. | |
| - The *content* of each option (the strings themselves) is unchanged. | |
| - Each per-question permutation is guaranteed to differ from the identity | |
| (so no question happens to end up unshuffled). Requires n >= 2 options, | |
| which every EgoMemReason question satisfies. | |
| - Per-question RNG is seeded so runs are reproducible. | |
| Outputs: | |
| --out-private fresh annotations_private.json → Ted412/EgoMemReason-Private | |
| --out-public fresh annotations_public.jsonl → Ted412/EgoMemReason | |
| Usage: | |
| python reshuffle_options.py \\ | |
| --src /nas-ssd2/ziyang/Memory_project/COLM/final_benchmark/final_benchmark_500_final_release.json \\ | |
| --out-private /tmp/annotations_private.json \\ | |
| --out-public /tmp/annotations_public.jsonl \\ | |
| --seed 20260713 | |
| Upload: | |
| hf upload Ted412/EgoMemReason-Private annotations_private.json --repo-type=dataset | |
| hf upload Ted412/EgoMemReason annotations_public.jsonl --repo-type=dataset | |
| """ | |
| import argparse | |
| import json | |
| import random | |
| from pathlib import Path | |
| PUBLIC_DROP_FIELDS = ("correct_answer",) | |
| def reshuffle_one(sample, master_seed): | |
| """Return (new_sample, new_correct_letter).""" | |
| letters = sorted(sample["options"].keys()) | |
| strings = [sample["options"][L] for L in letters] | |
| n = len(letters) | |
| if n < 2: | |
| raise ValueError(f"example_id {sample['example_id']}: <2 options") | |
| rng = random.Random(master_seed * 1_000_003 + int(sample["example_id"])) | |
| order = list(range(n)) | |
| # Reroll until the permutation is not the identity — kills the edge case | |
| # where the reshuffled key would still equal the leaked one for this Q. | |
| for _ in range(64): | |
| rng.shuffle(order) | |
| if order != list(range(n)): | |
| break | |
| else: | |
| # Fallback: swap first two. Cannot be identity because n >= 2. | |
| order = list(range(n)) | |
| order[0], order[1] = order[1], order[0] | |
| permuted_strings = [strings[i] for i in order] | |
| new_options = {letters[i]: permuted_strings[i] for i in range(n)} | |
| old_correct_letter = sample["correct_answer"].strip().upper() | |
| if old_correct_letter not in sample["options"]: | |
| raise ValueError( | |
| f"example_id {sample['example_id']}: correct_answer " | |
| f"{old_correct_letter!r} not in options {list(sample['options'].keys())}" | |
| ) | |
| old_correct_string = sample["options"][old_correct_letter] | |
| new_correct_letter = letters[permuted_strings.index(old_correct_string)] | |
| new = dict(sample) | |
| new["options"] = new_options | |
| new["correct_answer"] = new_correct_letter | |
| return new, new_correct_letter | |
| def summarize_diff(before, after): | |
| """Sanity stats: fraction of questions whose correct letter changed.""" | |
| same = sum( | |
| 1 for b, a in zip(before, after) | |
| if b["correct_answer"] == a["correct_answer"] | |
| ) | |
| return { | |
| "n": len(before), | |
| "correct_letter_changed": len(before) - same, | |
| "correct_letter_unchanged": same, | |
| } | |
| def main(): | |
| p = argparse.ArgumentParser() | |
| p.add_argument("--src", required=True, | |
| help="Source-of-truth annotations JSON with correct_answer per question.") | |
| p.add_argument("--out-private", required=True, | |
| help="Destination for the new private annotations JSON (wrapped dict).") | |
| p.add_argument("--out-public", required=True, | |
| help="Destination for the new public annotations JSONL (no correct_answer).") | |
| p.add_argument("--seed", type=int, required=True, | |
| help="Master RNG seed (per-question seed = seed*1_000_003 + example_id).") | |
| args = p.parse_args() | |
| src = json.loads(Path(args.src).read_text()) | |
| if isinstance(src, dict) and "samples" in src: | |
| header = {k: v for k, v in src.items() if k != "samples"} | |
| samples = src["samples"] | |
| else: | |
| header = None | |
| samples = src | |
| new_samples = [] | |
| for s in samples: | |
| ns, _ = reshuffle_one(s, args.seed) | |
| new_samples.append(ns) | |
| # Private: wrapped dict, preserves the source-of-truth metadata. | |
| if header is not None: | |
| private_out = dict(header) | |
| private_out["samples"] = new_samples | |
| else: | |
| private_out = {"samples": new_samples} | |
| Path(args.out_private).write_text(json.dumps(private_out, indent=2, ensure_ascii=False)) | |
| # Public: flat JSONL, no correct_answer. Matches the current HF dataset schema. | |
| with open(args.out_public, "w") as f: | |
| for s in new_samples: | |
| row = {k: v for k, v in s.items() if k not in PUBLIC_DROP_FIELDS} | |
| f.write(json.dumps(row, ensure_ascii=False) + "\n") | |
| stats = summarize_diff(samples, new_samples) | |
| print(f"[reshuffle] wrote {args.out_private}") | |
| print(f"[reshuffle] wrote {args.out_public}") | |
| print(f"[reshuffle] n={stats['n']} " | |
| f"correct_letter_changed={stats['correct_letter_changed']} " | |
| f"unchanged={stats['correct_letter_unchanged']}") | |
| if stats["correct_letter_unchanged"]: | |
| # Not a bug — a permutation can leave a letter in place as long as it's | |
| # not the identity permutation. We just report it for transparency. | |
| print(f"[reshuffle] note: {stats['correct_letter_unchanged']} questions " | |
| f"happen to keep the same correct letter under this permutation " | |
| f"(their option content is still shuffled).") | |
| if __name__ == "__main__": | |
| main() | |