diff --git a/eval/exp14b_arms.py b/eval/exp14b_arms.py new file mode 100644 index 0000000..0fefd14 --- /dev/null +++ b/eval/exp14b_arms.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""exp14b — армы батареи смысл-трапов + P1a↔F-disc. ПРОМПТ константен (дискурс), варьируем +editor-модель: C=glm-5 / F=gpt-5.4 / X=grok-4.3(reasoning-ON). Общий flash-черновик. + +Реюз exp14-выходов где есть (не пере-оплата). Per-call кап ОБЯЗАТЕЛЕН. ЧЕСТНЫЙ учёт ошибок +(finish=length/empty/http/retry — раздельно; НЕ «0 ошибок»). $ персист. + +Использование: exp14b_arms.py [--dry] +""" +from __future__ import annotations +import argparse, json, shutil, sys, time +from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import exp14_common as C +import exp14_prompts as P + +RERUN = Path("/home/ubuntu/books/gu-zhenren/rerun") +RECS = {(r["chapter"], r["chunk_idx"]): r for r in json.load(open(RERUN / "records.json"))} +INJ = json.load(open(Path("/home/ubuntu/books/gu-zhenren/exp14") / "injection_blocks.json")) +EX14 = Path("/home/ubuntu/books/gu-zhenren/exp14/arms") +OUT = Path("/home/ubuntu/books/gu-zhenren/exp14b"); OUT.mkdir(parents=True, exist_ok=True) +ARMS = OUT / "arms"; ARMS.mkdir(exist_ok=True) + +# базовые (пре-рег §1) + добавленные семьи-редакторы (расширение панели floor-теста, прозрачно) +MODEL = {"C": "glm-5", "F": "gpt-5.4", "X": "grok-4.3", + "D": "deepseek-v4-pro", "K": "kimi-k2.6", "M": "mistral-large-2512"} +EX14_ARM = {"C": "P1a", "F": "F-disc", "X": "X-disc"} # соответствие для реюза (только базовые) +CAPS = {"glm-5": 0.08, "gpt-5.4": 0.40, "grok-4.3": 0.40, + "deepseek-v4-pro": 0.06, "kimi-k2.6": 0.20, "mistral-large-2512": 0.10} + +# battery chunks + stage2 (для head-to-head C vs F) +BATTERY = ["7.0", "8.1", "11.1", "17.0", "19.0", "10.1", "6.0", "16.0", "2.1", "9.1"] +STAGE2 = ["17.0", "17.1", "18.0", "18.1", "18.2", "19.0", "19.1"] +# что каждому арму нужно покрыть +NEED = { + "C": sorted(set(BATTERY) | set(STAGE2)), + "F": sorted(set(BATTERY) | set(STAGE2)), # F на stage2 уже есть в exp14 → реюз + "X": sorted(set(BATTERY)), + "D": sorted(set(BATTERY)), # deepseek-v4-pro — только батарея (floor-тест) + "K": sorted(set(BATTERY)), # kimi-k2.6 + "M": sorted(set(BATTERY)), # mistral-large-2512 +} + + +def esys(): + return P.editor_system("discourse") + + +def main(): + ap = argparse.ArgumentParser(); ap.add_argument("--dry", action="store_true"); a = ap.parse_args() + sp = C.Spender(OUT / "costs.jsonl", CAPS) + errs = {"length": 0, "empty": 0, "http": 0, "content_filter": 0, "other": 0, "retried_ok": 0} + sysd = esys() + for arm, chunks in NEED.items(): + d = ARMS / arm; d.mkdir(exist_ok=True) + model = MODEL[arm] + for cid in chunks: + outp = d / f"{cid}.txt" + if outp.exists(): + continue + # РЕЮЗ exp14 (только базовые C/F/X — D/K/M новых семей в exp14 не было) + if arm in EX14_ARM: + ex = EX14 / EX14_ARM[arm] / f"{cid}.txt" + if ex.exists(): + shutil.copy(ex, outp) + print(f" [reuse exp14] {arm} {cid} ← {EX14_ARM[arm]}") + continue + ch, ck = cid.split("."); ch = int(ch); ck = int(ck) + r = RECS.get((ch, ck)) + if not r: + print(f" [MISS rec] {cid}"); continue + einj = INJ.get(f"{ch}/{ck}", {}).get("editor_constraint", "") + user = P.editor_user(r["source"], r["draft"]) + msgs = C.messages_with_injection(sysd, einj, user) + fam = "deepseek" if model.startswith("deepseek") else "glm" + in_est = C.count_tokens(sysd + einj + user, fam) + s = C.spec(model, temp=0.4) + ok, pred = sp.guard(model, in_est, s["max_tokens"]) + print(f" {arm} {cid}: in≈{in_est} predict=${pred:.4f} cap=${CAPS[model]} {'OK' if ok else 'OVER'}") + if a.dry or not ok: + continue + # до 2 ретраев на transient (honest error accounting) + text, err, usage, attempts = "", None, {}, 0 + for attempt in range(3): + attempts += 1 + text, err, usage = C.call(s, msgs, timeout=360) + sp.record({"arm": arm, "model": model, "keyenv": s["keyenv"], "id": cid, + "attempt": attempts, "err": err, "usage": usage}) + if not err: + if attempt > 0: errs["retried_ok"] += 1 + break + # классификация ошибки + if "length" in (err or ""): errs["length"] += 1 + elif "empty" in (err or ""): errs["empty"] += 1 + elif "http" in (err or ""): errs["http"] += 1 + elif "content_filter" in (err or ""): errs["content_filter"] += 1 + else: errs["other"] += 1 + if "http" in (err or "") or "transport" in (err or ""): + time.sleep(3); continue # transient → retry + break # length/empty/filter → не ретраим слепо + if err: + print(f" ERR {err[:60]} (attempts={attempts})"); continue + outp.write_text(text, encoding="utf-8") + print(f" ok {len(text)}ch") + print(f"\n=== ЧЕСТНЫЙ учёт ошибок: {errs} ===") + print(f"=== spend by key: {dict((k,round(v,4)) for k,v in sp.by_key.items())} ===") + + +if __name__ == "__main__": + main() diff --git a/eval/exp14b_judge.py b/eval/exp14b_judge.py new file mode 100644 index 0000000..510ffaf --- /dev/null +++ b/eval/exp14b_judge.py @@ -0,0 +1,105 @@ +#!/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)) +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() diff --git a/eval/exp14b_monitor.py b/eval/exp14b_monitor.py new file mode 100644 index 0000000..57a08fd --- /dev/null +++ b/eval/exp14b_monitor.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""exp14b §2 — слепой head-to-head пакет P1a(=C, glm-5 дискурс) ↔ F-disc(=F, gpt-5.4 дискурс) +на главах 17/18/19 (+ оригинал для верности). Метки U1/U2 перемешаны по главам, ключ отдельно. +Копирайт: ВНЕ git. +""" +from __future__ import annotations +import json, random +from pathlib import Path +import exp14_common as C + +ARMS = Path("/home/ubuntu/books/gu-zhenren/exp14b/arms") +RERUN = Path("/home/ubuntu/books/gu-zhenren/rerun") +RECS = {(r["chapter"], r["chunk_idx"]): r for r in json.load(open(RERUN / "records.json"))} +OUT = Path("/home/ubuntu/books/gu-zhenren/exp14b/monitor"); OUT.mkdir(parents=True, exist_ok=True) +FINALISTS = {"cheap-P1a": "C", "frontier-F-disc": "F"} +CHAPTERS = [19, 17, 18] +CHUNKS = {19: [0, 1], 17: [0, 1], 18: [0, 1, 2]} +SALT = "exp14b-h2h-blind-v1" + + +def ch_text(arm, ch): + return "\n\n".join((ARMS / arm / f"{ch}.{ck}.txt").read_text(encoding="utf-8") for ck in CHUNKS[ch]) +def ch_src(ch): + return "\n".join(RECS[(ch, ck)]["source"] for ck in CHUNKS[ch]) + + +def main(): + miss = [(f, ch, ck) for f, a in FINALISTS.items() for ch in CHAPTERS for ck in CHUNKS[ch] + if not (ARMS / a / f"{ch}.{ck}.txt").exists()] + if miss: + print("⚠ нет выходов:", miss[:8]); return + key = {} + md = ["# exp14b head-to-head — P1a(дешёвый дискурс) ↔ F-disc(фронтир). Слепо.\n", + "Две версии на главу (U1/U2, порядок в каждой главе СВОЙ). Сверху китайский оригинал. " + "Оцени РАЗДЕЛЬНО: (A) какая читается лучше как русская проза; (B) верность смыслу (сверяя с " + "оригиналом) — смысловые ошибки/инверсии со спанами. Насколько дешёвый близок к фронтиру? Ключ — `_КЛЮЧ.json`, ПОСЛЕ.\n"] + for ch in CHAPTERS: + order = list(FINALISTS); random.Random(f"{SALT}-{ch}").shuffle(order) + labels = {f"U{i+1}": order[i] for i in range(len(order))} + key[str(ch)] = {lab: {"finalist": fin, "arm": FINALISTS[fin]} for lab, fin in labels.items()} + md.append(f"\n\n{'='*60}\n## Глава {ch}\n{'='*60}\n\n### ОРИГИНАЛ (zh)\n\n```\n{ch_src(ch)}\n```\n") + for lab in ("U1", "U2"): + md.append(f"\n### {lab}\n\n{ch_text(FINALISTS[labels[lab]], ch)}\n") + (OUT / "h2h.md").write_text("\n".join(md), encoding="utf-8") + (OUT / "_КЛЮЧ.json").write_text(json.dumps({"salt": SALT, "finalists": FINALISTS, "per_chapter": key}, ensure_ascii=False, indent=1), encoding="utf-8") + print(f"→ {OUT/'h2h.md'} (2 версии × 3 главы + оригинал)") + for ch in CHAPTERS: + print(f" гл.{ch}: " + ", ".join(f"{lab}={key[str(ch)][lab]['finalist']}" for lab in ("U1","U2"))) + + +if __name__ == "__main__": + main() diff --git a/eval/exp14b_score.py b/eval/exp14b_score.py new file mode 100644 index 0000000..2ef39c1 --- /dev/null +++ b/eval/exp14b_score.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""exp14b — детерминированный скоринг DET-классов (a/b/c/d): извлекает span-рендер трапа в +каждом арме (C/F/X) + применяет правило (полярность/число/ранг/направление) → fix/fail/ambiguous ++ печатает span для верификации. $0. Реюз P0 для контраста где есть. + +SOFT-классы (e/f) — отдельным судейским проходом (exp14b_judge.py). +""" +from __future__ import annotations +import json, re +from pathlib import Path + +ARMS = Path("/home/ubuntu/books/gu-zhenren/exp14b/arms") +EX14 = Path("/home/ubuntu/books/gu-zhenren/exp14/arms") + +# DET-трапы: (id, chunk, anchor-regex для поиска строки, rule(line)->'fix'/'fail'/'?', описание) +def rule_polarity_all(line): + l = line.lower() + if re.search(r'ни один|никто|не знал ни|ни одного человека не', l): return 'fail' + if re.search(r'все |всем |каждый|мёртв|не было ни одного,? (кто|который)', l): return 'fix' + return '?' +def rule_polarity_envy(line): + l = line.lower() + if 'завист' not in l and 'зависти' not in l and 'зависть' not in l: return '?' + if re.search(r'никто не|не было зависти|без зависти', l): return 'fail' + return 'fix' # завист присутствует в утвердит. контексте +def rule_num_b1(line): + l = line.lower() + # 十二分之一 = 1/12 ; 三成 = 30% + has12 = bool(re.search(r'одну двенадцатую|двенадцатую часть|1/12|двенадцатой', l)) + has30 = bool(re.search(r'тридцать процент|30\s*%|три десят|целых три десят', l)) + bad = bool(re.search(r'\b3\s*%|\bтри процент', l)) # 三成→3% = 10× ошибка + if bad and not has30: return 'fail' + if has30 or has12: return 'fix' + return '?' +def rule_num_b2(line): + l = line.lower() + if re.search(r'сорок четыре процент|44\s*%|сорока четырёх процент', l): return 'fix' + if re.search(r'4[.,]4|\b4\s*%|четыре процент', l): return 'fail' + return '?' +def rule_rank_above(line): + l = line.lower() + if re.search(r'среди людей|одним из', l): return 'fail' + if re.search(r'над людьми|выше (других|прочих|обычных)|возвыс|человеком над', l): return 'fix' + return '?' +def rule_grade_bing(line): + l = line.lower() + # 丙 = 3-й = В/C ; ошибка = Б/второй/A/B + if re.search(r'разряд[а-я]* в|класс[а-я]* в|«в»|третьего разряда|третий разряд', l): return 'fix' + if re.search(r'разряд[а-я]* б|класс[а-я]* б|«б»|второго разряда', l): return 'fail' + return '?' +def rule_patriarch(line): + l = line.lower() + if re.search(r'старейшин', l): return 'fail' + if re.search(r'глав[аеуы].{0,20}поколени|четвёртого поколения|патриарх|четвёртый глава', l): return 'fix' + return '?' +def rule_counterfactual(line): + l = line.lower() + if re.search(r'иначе|в противном случае|не то|если бы не', l): return 'fix' + return '?' + +TRAPS = [ + ("a1", "7.0", r'Гуюэ.*(знал|извест|мёртв)|(знал|извест).*Гуюэ', rule_polarity_all, "все знали (двойн.отриц.)"), + ("a2", "7.0", r'завист|зависти|зависть', rule_polarity_envy, "все полны зависти (无不)"), + ("b1", "10.1", r'двенадцат|тридцать|десят|процент|祭', rule_num_b1, "1/12 & 30%"), + ("b2", "6.0", r'сорок|44|процент|апертур', rule_num_b2, "44%"), + ("c1", "17.0", r'людьми|людей|возвыс', rule_rank_above, "над людьми (人上之人)"), + ("c2", "9.1", r'Фан Юань.*(разряд|класс)|(разряд|класс).*Фан', rule_grade_bing, "丙=разряд В"), + ("c3", "16.0", r'族长|поколени|старейшин|глава клана|патриарх', rule_patriarch, "族长=глава, не старейшина"), + ("d1", "19.0", r'иначе|счастью|повезло|противном', rule_counterfactual, "幸亏…否则 контрфактив"), + ("d2", "7.0", r'иначе|противном|揭破|разоблач', rule_counterfactual, "否则 контрфактив"), +] +ARM_MODELS = {"C": "glm-5(P1a)", "F": "gpt-5.4(F-disc)", "X": "grok-4.3(X-disc)", "P0": "prod-baseline", + "D": "deepseek-v4-pro", "K": "kimi-k2.6", "M": "mistral-large"} +ALL_ARMS = ("C", "F", "X", "D", "K", "M") + + +def find_span(text, anchor): + for line in text.split("\n"): + if line.strip() and re.search(anchor, line): + return line.strip() + return "" + + +def main(): + print("=== exp14b DET-скоринг (a/b/c/d) — fix/fail/? + span ===\n") + rates = {arm: {"fix": 0, "fail": 0, "?": 0} for arm in ALL_ARMS} + for tid, cid, anchor, rule, desc in TRAPS: + print(f"── {tid} ch{cid} [{desc}] ──") + for arm in ("P0",) + ALL_ARMS: + base = ARMS if arm in ALL_ARMS else EX14 + fp = base / arm / f"{cid}.txt" if arm != "P0" else EX14 / "P0" / f"{cid}.txt" + if not fp.exists(): + print(f" {arm:<3} —"); continue + span = find_span(fp.read_text(encoding="utf-8"), anchor) + verd = rule(span) if span else '?' + if arm in rates: rates[arm][verd] += 1 + mark = {"fix": "✓", "fail": "✗КАТ" if tid in ("a1","a2","c1") else "✗", "?": "?"}[verd] + print(f" {arm:<3} {mark:<5} {span[:88] if span else '(span не найден)'}") + print() + print("=== FIX-RATE по армам (DET, 9 инстансов) ===") + for arm in ALL_ARMS: + r = rates[arm]; n = sum(r.values()) + print(f" {arm} {ARM_MODELS[arm]:<18}: fix {r['fix']}/{n}, fail {r['fail']}, ? {r['?']}") + json.dump(rates, open(Path("/home/ubuntu/books/gu-zhenren/exp14b")/"det_rates.json","w"), ensure_ascii=False, indent=1) + + +if __name__ == "__main__": + main()