textmachine/eval/pkg7/score_probe2.py

133 lines
6.8 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
"""Полигон, пакет-8 (замер 2): доля выдуманных dst на мусоре — счёт ИЗ СЫРЬЯ.
Разбор ответов делает БОЕВОЙ `terminology.ParseReply` через `termharness -mode parse`, а не реплика:
именно парсер решает, что считается ответом, что отказом ⟦TM-NO-DST⟧, а что «плохой строкой»
(эхо исходника, кривая лемма). Реплика тут расходилась бы с продом ровно там, где это важнее всего.
Пороги названы в §0.3 отчёта ДО прогона: >50% выдумок на МУСОРЕ = провал; <20% = годно.
"""
from __future__ import annotations
import argparse
import glob
import json
import subprocess
import sys
import tempfile
from collections import Counter
from pathlib import Path
def parse_with_engine(harness: Path, reply: str, keys: list[str]) -> dict:
with tempfile.TemporaryDirectory() as td:
rp, kp, op = Path(td) / "r.txt", Path(td) / "k.json", Path(td) / "o.json"
rp.write_text(reply, encoding="utf-8")
kp.write_text(json.dumps(keys, ensure_ascii=False), encoding="utf-8")
subprocess.run([str(harness), "-mode", "parse", "-reply", str(rp), "-keys", str(kp),
"-out", str(op)], check=True, capture_output=True)
return json.loads(op.read_text(encoding="utf-8"))
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--raw-dir", required=True, type=Path)
ap.add_argument("--labels", required=True, type=Path)
ap.add_argument("--control", required=True, type=Path, help="JSON кандидатов; хвост + контроль")
ap.add_argument("--harness", required=True, type=Path)
ap.add_argument("--out", type=Path)
a = ap.parse_args()
labels = json.loads(a.labels.read_text(encoding="utf-8"))
cands = json.loads(a.control.read_text(encoding="utf-8"))
ctrl_srcs = [c["src"] for c in cands if c["src"] not in labels]
# Повторы разбираются РАЗДЕЛЬНО: слить их в одну карту значило бы выдать разброс прогона за
# согласие (урок фазы B пакета-7). Основной вывод считается по прогону 0, размах — по всем.
per_rep: dict[int, dict] = {}
usd, cin, cout, cached = 0.0, 0, 0, 0
files = sorted(glob.glob(str(a.raw_dir / "*.json")))
for f in files:
d = json.loads(Path(f).read_text(encoding="utf-8"))
if "error" in d:
print(f"ВЫЗОВ С ОШИБКОЙ: {f}: {d['error']}", file=sys.stderr)
continue
rep = d.get("rep", 0)
st = per_rep.setdefault(rep, {"answered": {}, "declined": set(), "unanswered": set(), "bad": 0})
got = parse_with_engine(a.harness, d["response"], d["keys"])
st["answered"].update(got["answered"])
st["declined"].update(got["declined"])
st["unanswered"].update(got["unanswered"])
st["bad"] += got["bad_lines"]
usd += d.get("usd", 0.0)
u = d.get("usage") or {}
cin += u.get("in", 0); cout += u.get("out", 0); cached += u.get("cached", 0)
base = per_rep[min(per_rep)]
answered, declined, unanswered, bad = base["answered"], base["declined"], base["unanswered"], base["bad"]
def bucket(srcs: list[str]) -> Counter:
c = Counter()
for s in srcs:
if s in answered:
c["выдумала dst"] += 1
elif s in declined:
c["⟦TM-NO-DST⟧"] += 1
elif s in unanswered:
c["промолчала"] += 1
else:
c["не в ответе"] += 1
return c
junk = [s for s, v in labels.items() if v == "МУСОР"]
grey = [s for s, v in labels.items() if v == "СЕРЫЙ"]
print(f"вызовов: {len(files)} ${usd:.5f} in={cin} (cached {cached}) out={cout} плохих строк парсера: {bad}")
rows = []
for name, srcs in (("МУСОР", junk), ("СЕРЫЙ", grey), ("КОНТРОЛЬ (подписанные)", ctrl_srcs)):
c = bucket(srcs)
n = len(srcs)
inv = c["выдумала dst"]
print(f"\n{name}: {n} кандидатов")
for k in ("выдумала dst", "⟦TM-NO-DST⟧", "промолчала", "не в ответе"):
if c[k]:
print(f" {k:16s} {c[k]:3d} = {c[k]/n:.3f}")
rows.append({"class": name, "n": n, "invented": inv, "rate": inv / n if n else None})
jr = rows[0]["rate"]
verdict = ("ПРОВАЛ (>0.50)" if jr > 0.50 else "ГОДНО (<0.20)" if jr < 0.20 else "СЕРАЯ ЗОНА (0.200.50)")
print(f"\nВЕРДИКТ по §0.3 на классе МУСОР (прогон {min(per_rep)}): доля выдуманных {jr:.3f}{verdict}")
# Размах по повторам: вывод «провал» держится, только если он больше разброса прогона.
if len(per_rep) > 1:
print("\nразброс по повторам (доля выдуманных dst):")
spans = {}
for name, srcs in (("МУСОР", junk), ("СЕРЫЙ", grey), ("КОНТРОЛЬ", ctrl_srcs)):
vals = []
for rep in sorted(per_rep):
st = per_rep[rep]
vals.append(sum(1 for s in srcs if s in st["answered"]) / len(srcs))
spans[name] = (min(vals), max(vals))
print(f" {name:10s} " + " ".join(f"r{r}={v:.3f}" for r, v in zip(sorted(per_rep), vals))
+ f" размах {max(vals)-min(vals):.3f}")
rows.append({"spans": {k: list(v) for k, v in spans.items()}})
print("\nчто именно роль ответила на МУСОР (все строки):")
for s in sorted(junk):
v = answered.get(s)
print(f" {s}\t{v if v is not None else ('⟦TM-NO-DST⟧' if s in declined else 'промолчала')}")
print("\nКОНТРОЛЬ — ответ против подписи владельца:")
ctrl_dst = {c["src"]: c for c in cands}
for s in ctrl_srcs:
print(f" {s}\t{answered.get(s, '⟦TM-NO-DST⟧' if s in declined else 'промолчала')}")
if a.out:
a.out.write_text(json.dumps({"rows": rows, "answered": answered,
"declined": sorted(declined), "unanswered": sorted(unanswered),
"bad_lines": bad, "usd": usd,
"usage": {"in": cin, "out": cout, "cached": cached}},
ensure_ascii=False, indent=1), encoding="utf-8")
return 0
if __name__ == "__main__":
sys.exit(main())