textmachine/eval/exp14b/exp14b_judge.py

105 lines
6.3 KiB
Python
Raw Permalink 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
"""exp14b — судейский проход по SOFT-классам (e чэнъюй, f кореференция) × армы C/F/X.
≥2 КРОСС-семейных span-цитирующих судьи, self-family exclusion (урок D37: exp14 фронтир
судил ОДИН судья). leave-one-out. Катастроф-скрин к победителю. $ персист, честный учёт.
Судьи: gpt-5-mini(openai) + kimi(moonshot) + mistral(mistral). Для F(gpt) gpt-5-mini исключён.
"""
from __future__ import annotations
import argparse, json, re, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "exp14")) # exp14b reuses exp14's harness (exp14_common lives in eval/exp14/)
import exp14_common as C
ARMS = Path("/home/ubuntu/books/gu-zhenren/exp14b/arms")
OUT = Path("/home/ubuntu/books/gu-zhenren/exp14b")
ARM_FAMILY = {"C": "zai", "F": "openai", "X": "xai",
"D": "deepseek", "K": "moonshot", "M": "mistral"} # K→kimi-судья excl; M→mistral-судья excl
JUDGE_FAMILY = {"gpt-5-mini": "openai", "kimi-k2.6": "moonshot", "mistral-large-2512": "mistral"}
ALL_ARMS = ("C", "F", "X", "D", "K", "M")
CAPS = {"gpt-5-mini": 0.05, "kimi-k2.6": 0.08, "mistral-large-2512": 0.05}
SOFT = [
("e1", "8.1", "物是人非", "контраст «вещи/места ПРЕЖНИЕ — люди уже НЕ ТЕ»; плоское «всё изменилось» теряет половину (物是)."),
("e2", "2.1", "韬光养晦", "«скрывать блеск, выжидать» — таиться/копить силы, не выпячиваться; не «развиваться/трудиться»."),
("f1", "16.0", "这促使它不得不外出,寻找野生的酒囊花", "субъект «它» = ВИННЫЙ ЧЕРВЬ (酒虫); это ОН вынужден выйти искать дикие酒囊花. Не спутать субъект."),
("f2", "11.1", "他们的身形隐没在阴影中…分不清男女", "двое (пол НЕЯСЕН) в тени; НЕ навязывать род/пол, НЕ выдумывать кто это."),
]
SYS = ("Ты — билингвальный эксперт zh→ru. Оцени, верно ли русский перевод передаёт смысл ОДНОГО "
"указанного китайского фрагмента. Ответь СТРОГО JSON без markdown: "
'{"verdict":"correct|partial|wrong","catastrophe":true|false,'
'"evidence":"дословная цитата из перевода ≤15 слов","note":"кратко"}. '
"catastrophe=true при подмене субъекта/участника или перевёрнутом смысле.")
def run():
ap = argparse.ArgumentParser(); ap.add_argument("--dry", action="store_true"); a = ap.parse_args()
sp = C.Spender(OUT / "judge_costs.jsonl", CAPS)
vpath = OUT / "soft_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["trap"], v["arm"], v["judge"]))
tasks = []
for tid, cid, span, meaning in SOFT:
for arm in ALL_ARMS:
fp = ARMS / arm / f"{cid}.txt"
if not fp.exists(): continue
for jm in JUDGE_FAMILY:
if JUDGE_FAMILY[jm] == ARM_FAMILY[arm]: continue # self-family exclusion
if (tid, arm, jm) in done: continue
tasks.append((tid, cid, span, meaning, arm, jm, fp.read_text(encoding="utf-8")))
print(f"soft-judge tasks: {len(tasks)} new ({len(done)} done)")
if a.dry:
from collections import Counter; print(Counter(t[5] for t in tasks)); return
errs = {"empty": 0, "http": 0, "other": 0}
for tid, cid, span, meaning, arm, jm, text in tasks:
user = (f"КИТАЙСКИЙ ФРАГМЕНТ: {span}\nДОПУСТИМЫЙ СМЫСЛ: {meaning}\n\n"
f"РУССКИЙ ПЕРЕВОД ОТРЫВКА:\n{text}\n\nОцени передачу ТОЛЬКО этого фрагмента. JSON.")
s = C.spec(jm, max_tokens=(16000 if jm == "kimi-k2.6" else 6000))
in_est = C.count_tokens(SYS + user, "glm")
ok, pred = sp.guard(jm, in_est, s["max_tokens"])
if not ok:
print(f" OVER {tid} {arm} {jm}"); continue
t, err, usage = C.call(s, [{"role": "system", "content": SYS}, {"role": "user", "content": user}], timeout=300)
cst = sp.record({"model": jm, "judge": jm, "keyenv": s["keyenv"], "trap": tid, "arm": arm, "err": err, "usage": usage})
if err:
errs["empty" if "empty" in err else ("http" if "http" in err else "other")] += 1
v = parse(t) if not err else {"verdict": "ERR"}
v.update(trap=tid, arm=arm, judge=jm)
out.append(v)
with open(vpath, "a", encoding="utf-8") as f: f.write(json.dumps(v, ensure_ascii=False) + "\n")
print(f" {tid} {arm} {jm:<18}{v.get('verdict'):<8} cat={v.get('catastrophe')} ${cst:.4f}")
print(f"\n=== ошибки судей (честно): {errs} ===")
summarize(out)
def parse(text):
m = re.search(r"\{.*\}", text, re.S)
if not m: return {"verdict": "PARSE"}
try:
d = json.loads(m.group(0))
return {"verdict": str(d.get("verdict", "?")).lower(), "catastrophe": bool(d.get("catastrophe")),
"evidence": (d.get("evidence") or "")[:100], "note": (d.get("note") or "")[:100]}
except Exception:
vv = re.search(r'"verdict"\s*:\s*"(\w+)"', m.group(0))
return {"verdict": vv.group(1).lower() if vv else "PARSE", "catastrophe": "true" in m.group(0).lower()}
def summarize(out):
from collections import defaultdict
agg = defaultdict(lambda: defaultdict(list))
for v in out: agg[v["trap"]][v["arm"]].append(v)
print("\n=== SOFT-классы: вердикты (арм: судьи) ===")
for tid, cid, span, meaning in SOFT:
if tid not in agg: continue
print(f"\n{tid} ch{cid} [{span}]:")
for arm in ALL_ARMS:
vs = agg[tid].get(arm)
if not vs: continue
zv = [(v["judge"][:4], v["verdict"], "CAT" if v.get("catastrophe") else "") for v in vs]
print(f" {arm}: {zv}")
if __name__ == "__main__":
run()