Download training/ensemble_v9.py from SinhalaMMLU-SharedTask/team-riswan: direct link, hf CLI and curl.
- Browser
- Download file 5.72 kB
-
https://huggingface.co/SinhalaMMLU-SharedTask/team-riswan/resolve/main/training/ensemble_v9.py
- Command line
-
hf download hf://SinhalaMMLU-SharedTask/team-riswan/training/ensemble_v9.py
-
curl -L -o ensemble_v9.py https://huggingface.co/SinhalaMMLU-SharedTask/team-riswan/resolve/main/training/ensemble_v9.py
5.72 kB
| #!/usr/bin/env python3 | |
| """ | |
| Final merge ("v9") that produced the leaderboard submission (Codabench ID #138, 64.2482). | |
| It is a deterministic weighted vote over the saved raw passes in training/passes/. | |
| No model is called and no labels are used. This is the exact logic that was run, cleaned only | |
| to read from the repo layout. | |
| python training/ensemble_v9.py # -> predictions_v9_rebuilt.jsonl | |
| cmp predictions_v9_rebuilt.jsonl predictions.jsonl # identical | |
| Each source is a dict {(q_no, subject, category, level): "1".."4"} (answers in ORIGINAL option order): | |
| p1 : Pass 1 think, 2048 tok, original order, all 2238 questions | |
| p2 : Pass 2 think, 4096 tok, options rotated by 1 (hard + questions where CoT != Direct) | |
| p3 : Pass 3 think, 4096 tok, system prompt with Sanskrit/Pali/technical-term hint (uncertain set) | |
| p4 : Pass 4 think, 4096 tok, options rotated by 2 (uncertain set) | |
| p5 : Pass 5 think, 4096 tok, options rotated by 3 (uncertain set) | |
| mac : repeat of the Pass 1 configuration on a second machine (Apple Metal), disputed + hard questions | |
| cot : 4-shot English CoT baseline (think=False) | |
| dr : 4-shot direct-answer baseline (think=False) | |
| Rotated passes (p2, p4, p5) are ignored on questions whose options refer to option positions | |
| ("ඉහත", "above", "1 සහ 2", ...) because rotation changes the meaning of such options. | |
| """ | |
| import argparse | |
| import json | |
| import re | |
| from collections import Counter | |
| from pathlib import Path | |
| POS_RE = re.compile(r"ඉහත|above|\b[1-4]\s*(?:හා|සහ|හෝ|,|and|or)\s*[1-4]\b", re.I) | |
| PASS_FILES = { | |
| "p1": "pass1_think_2048_orig.jsonl", | |
| "p2": "pass2_think_4096_rot1.jsonl", | |
| "p3": "pass3_think_4096_hint.jsonl", | |
| "p4": "pass4_think_4096_rot2.jsonl", | |
| "p5": "pass5_think_4096_rot3.jsonl", | |
| "mac": "mac_think_2048_orig.jsonl", | |
| "cot": "baseline_cot_4shot.jsonl", | |
| "dr": "baseline_direct_4shot.jsonl", | |
| } | |
| def key_of(r): | |
| return (r["q_no"], r["subject"], r["category"], r["level"]) | |
| def load_map(path): | |
| m = {} | |
| with Path(path).open(encoding="utf-8") as f: | |
| for line_no, line in enumerate(f, 1): | |
| if line.strip(): | |
| r = json.loads(line) | |
| k = key_of(r) | |
| answer = str(r["answer"]) | |
| if k in m or answer not in {"1", "2", "3", "4"}: | |
| raise ValueError(f"Duplicate key or invalid answer: {path}:{line_no}") | |
| m[k] = answer | |
| return m | |
| def merge_one(k, pos, p1, p2, p3, p4, p5, mac, cot, dr): | |
| """Weighted vote for one question. k = (q_no, subject, category, level).""" | |
| lvl = k[3] | |
| v = Counter() | |
| if lvl == "hard": | |
| v[p1[k]] += 1.15 | |
| if k in mac: v[mac[k]] += 1.10 | |
| if k in p3: v[p3[k]] += 1.10 | |
| for m in (p2, p4, p5): | |
| if k in m and k not in pos: v[m[k]] += 1.10 | |
| if k in cot: v[cot[k]] += 0.35 | |
| if k in dr: v[dr[k]] += 0.35 | |
| elif lvl == "medium": | |
| v[p1[k]] += 1.25 | |
| if k in mac: v[mac[k]] += 1.05 | |
| if k in p3: v[p3[k]] += 1.10 | |
| for m in (p2, p4, p5): | |
| if k in m and k not in pos: v[m[k]] += 1.05 | |
| if k in cot: v[cot[k]] += 0.35 | |
| if k in dr: v[dr[k]] += 0.35 | |
| else: # easy: Pass 1 anchored at 2.40, so only a margin >= 2 supermajority can overturn it | |
| v[p1[k]] += 2.40 | |
| if k in mac: v[mac[k]] += 1.0 | |
| if k in p3: v[p3[k]] += 1.0 | |
| for m in (p2, p4, p5): | |
| if k in m and k not in pos: v[m[k]] += 0.95 | |
| if k in cot: v[cot[k]] += 0.30 | |
| if k in dr: v[dr[k]] += 0.30 | |
| return v.most_common(1)[0][0] | |
| def positional_keys(questions): | |
| """Keys of questions whose option text refers to option positions (input-only regex).""" | |
| pos = set() | |
| for q in questions: | |
| if POS_RE.search(" ".join(map(str, q["choices"]))): | |
| pos.add((q["q_no"], q["subject"], q["category"], q["level"])) | |
| return pos | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--passes", default=str(Path(__file__).parent / "passes")) | |
| ap.add_argument("--data", default=None, | |
| help="dir with easy/medium/hard/*.json (needed only for the positional-option filter)") | |
| ap.add_argument("--out", default="predictions_v9_rebuilt.jsonl") | |
| a = ap.parse_args() | |
| if Path(a.out).resolve() == (Path(__file__).resolve().parents[1] / "predictions.jsonl"): | |
| raise ValueError("Refusing to overwrite the submitted predictions.jsonl; choose a different --out.") | |
| maps = {n: load_map(Path(a.passes) / f) for n, f in PASS_FILES.items()} | |
| ref = [json.loads(l) for l in open(Path(a.passes) / PASS_FILES["p1"], encoding="utf-8") if l.strip()] | |
| pos = set() | |
| if a.data: | |
| qs = [] | |
| for lvl in ("easy", "medium", "hard"): | |
| for f in sorted((Path(a.data) / lvl).glob("*.json")): | |
| for q in json.load(open(f, encoding="utf-8")): | |
| q["level"] = lvl | |
| qs.append(q) | |
| pos = positional_keys(qs) | |
| else: | |
| pos_file = Path(a.passes) / "positional_option_keys.json" | |
| pos = {tuple(x) for x in json.load(open(pos_file, encoding="utf-8"))} | |
| with open(a.out, "w", encoding="utf-8") as f: | |
| for q in ref: | |
| k = key_of(q) | |
| ans = merge_one(k, pos, maps["p1"], maps["p2"], maps["p3"], maps["p4"], maps["p5"], | |
| maps["mac"], maps["cot"], maps["dr"]) | |
| f.write(json.dumps({"q_no": q["q_no"], "subject": q["subject"], "category": q["category"], | |
| "level": q["level"], "answer": ans}, ensure_ascii=False) + "\n") | |
| print("wrote", a.out) | |
| if __name__ == "__main__": | |
| main() | |