File size: 5,715 Bytes
682bee1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/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()