Spaces:
Running
Running
File size: 6,126 Bytes
c83ff60 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | """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()
|