textmachine/eval/exp14_rank.py

120 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""exp14 Ступень II — ранжирующая панель финалистов (P0/A-2pass/F-disc) на главах 19/17/18.
РАЗДЕЛЬНО стиль/верность, span-цитаты, ≥N повторов со свапом меток, leave-one-judge-out.
Судьи gpt-5-mini + kimi + gemini(точечно). Self-family (F-disc=openai) снимается leave-one-out.
Guard мемо eval-aggregation: НЕ собирать ложную сходимость.
Использование: exp14_rank.py [--repeats 3] [--dry]
"""
from __future__ import annotations
import argparse, json, random, re, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import exp14_common as C
ARMS = C.DIAG / "arms"
MAT = json.load(open(C.DIAG / "material.json"))
FINALISTS = {"Fin-A": "P0", "Fin-B": "A-2pass", "Fin-C": "F-disc"}
CHAPTERS = [19, 17, 18]
AXES = {"style": "русская художественность/абзацы/конвенции (естественность прозы, красная строка, тире-диалог)",
"fidelity": "верность смыслу (нет инверсий полярности/участника, нет потери атомов; можно ценой лёгкой шероховатости)"}
JUDGES = ["gpt-5-mini", "kimi-k2.6", "gemini-3.1-pro-preview"]
CAPS = {"gpt-5-mini": 0.10, "kimi-k2.6": 0.15, "gemini-3.1-pro-preview": 0.30}
def chapter_text(arm, ch):
chunks = sorted(u["chunk_idx"] for u in MAT["stage2"][str(ch)])
return "\n\n".join((ARMS / arm / f"{ch}.{ck}.txt").read_text(encoding="utf-8") for ck in chunks)
def run():
ap = argparse.ArgumentParser(); ap.add_argument("--repeats", type=int, default=3)
ap.add_argument("--dry", action="store_true"); a = ap.parse_args()
sp = C.Spender(C.DIAG / "rank_costs.jsonl", CAPS)
vpath = C.DIAG / "rank_verdicts.jsonl"; out = []; done = set()
if vpath.exists():
for l in vpath.read_text(encoding="utf-8").splitlines():
if l.strip():
v = json.loads(l); out.append(v); done.add((v["ch"], v["axis"], v["judge"], v["rep"]))
tasks = []
for ch in CHAPTERS:
for axis in AXES:
for rep in range(a.repeats):
order = list(FINALISTS); random.Random(f"exp14-rank-{ch}-{axis}-{rep}").shuffle(order)
labels = {f"V{i+1}": order[i] for i in range(len(order))}
for jm in JUDGES:
if jm == "gemini-3.1-pro-preview" and rep > 0:
continue # gemini точечно: 1 повтор (бюджет €2.6)
if (ch, axis, jm, rep) in done:
continue
tasks.append((ch, axis, rep, jm, labels))
print(f"rank tasks: {len(tasks)} new ({len(done)} done)")
if a.dry:
from collections import Counter
print(Counter(t[3] for t in tasks)); return
for ch, axis, rep, jm, labels in tasks:
blocks = "\n\n".join(f"=== {lab} ===\n{chapter_text(FINALISTS[fin], ch)}" for lab, fin in labels.items())
sysmsg = ("Ты — литературный судья zh→ru. Оцени ТРИ перевода одной главы по ОДНОЙ оси. "
"Ранжируй от лучшего к худшему. Приведи 2-4 span-цитаты-обоснования (≤15 слов каждая). "
'Ответь СТРОГО JSON без markdown: {"ranking":["V?","V?","V?"],'
'"evidence":[{"label":"V?","quote":"≤15 слов","note":"почему"}]}.')
user = f"ОСЬ ОЦЕНКИ: {AXES[axis]}\n\nТРИ ВАРИАНТА:\n\n{blocks}\n\nРанжируй по оси. JSON."
s = C.spec(jm, max_tokens=(16000 if jm == "kimi-k2.6" else 6000))
in_est = C.count_tokens(sysmsg + user, "glm")
ok, pred = sp.guard(jm, in_est, s["max_tokens"])
if not ok:
print(f" OVER-CAP {ch} {axis} {jm} pred=${pred:.3f}"); continue
text, err, usage = C.call(s, [{"role": "system", "content": sysmsg},
{"role": "user", "content": user}], timeout=360)
cst = sp.record({"model": jm, "judge": jm, "keyenv": s["keyenv"], "ch": ch,
"axis": axis, "rep": rep, "err": err, "usage": usage})
rk = parse_rank(text) if not err else None
v = {"ch": ch, "axis": axis, "rep": rep, "judge": jm, "labels": labels,
"ranking_labels": rk, "err": err}
# map labels→finalist
if rk:
v["ranking_fin"] = [labels.get(x) for x in rk]
out.append(v)
with open(vpath, "a", encoding="utf-8") as f:
f.write(json.dumps(v, ensure_ascii=False) + "\n")
print(f" ch{ch} {axis:<8} rep{rep} {jm:<18}{v.get('ranking_fin')} ${cst:.4f}")
summarize(out)
def parse_rank(text):
m = re.search(r'"ranking"\s*:\s*(\[[^\]]*\])', text)
if not m:
return None
try:
arr = json.loads(m.group(1))
return [str(x) for x in arr]
except Exception:
return re.findall(r'V\d', m.group(1))
def summarize(out):
from collections import defaultdict
import statistics
# Borda: 1st=2pts,2nd=1,3rd=0; per axis; per judge for leave-one-out
def borda(rows):
pts = defaultdict(list)
for v in rows:
rf = v.get("ranking_fin")
if not rf or len(rf) != 3 or None in rf:
continue
for i, fin in enumerate(rf):
pts[fin].append(2 - i)
return {f: round(statistics.mean(p), 2) for f, p in pts.items()}
print("\n=== РАНЖИРОВАНИЕ (Borda mean, 2=лучший) ===")
for axis in AXES:
rows = [v for v in out if v["axis"] == axis]
print(f"\n{axis}: overall {borda(rows)}")
judges = sorted(set(v["judge"] for v in rows))
for j in judges:
print(f" only {j}: {borda([v for v in rows if v['judge']==j])}")
for j in judges: # leave-one-out
print(f" leave-out {j}: {borda([v for v in rows if v['judge']!=j])}")
if __name__ == "__main__":
run()