team-riswan / training /ensemble_v9.py
riswanmsm's picture
Submit final system code, manifest, and predictions for team-riswan
682bee1
Raw History Blame Contribute Delete
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()