textmachine/eval/exp16/a6b_verify.py

129 lines
6.7 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
"""exp16 — A6b: local 9B as a Z1 VERIFIER of translation pairs (research/20 §D2 A6b, §B4 Z1). $0 (stand).
§B4 routes SPOT to code (A6 showed code>=local 9B) and reserves the local 9B for VERIFICATION of the
stably-wrong class Z1 (蛊→«гусеница»: high termhood, zero spread, dst is a common word — invisible to the
spread signal). This mini-bench asks: can a local 9B judge whether a ru rendering of a zh term IN CONTEXT
is faithful? Gold is AUTOMATIC: CORRECT pairs use the owner-signed seed dst; WRONG pairs inject the Z1
failure class (plausible common-word mistranslations + cross-term swaps). Accuracy + wrong-catch rate.
30 pairs (15 correct + 15 wrong). Deterministic (temp 0, think off). Health-checked.
"""
from __future__ import annotations
import json
import re
import urllib.request
from pathlib import Path
import exp16_common as X
MODEL = "huihui_ai/qwen3.5-abliterated:9b"
OLLAMA = "http://localhost:11434/api/chat"
OUT = Path("/home/ubuntu/books/gu-zhenren/exp16")
_opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
# Z1 verification bench: (src_term, correct_dst, wrong_dst[Z1 stably-wrong class]). Wrong = a plausible
# common Russian word or a cross-term swap that the spread signal would NOT catch.
BENCH = [
("", "гу", "гусеница"), # Z1 archetype: 蛊 as a common bug-word
("蛊师", "гу-мастер", "заклинатель насекомых"),
("蛊虫", "гу-червь", "личинка"),
("", "ранг", "оборот"), # 转 polysemy: rank vs turn/revolution
("元石", "первобытный камень", "метеорит"),
("真元", "истинная ци", "чистая энергия"),
("空窍", "апертура", "полость"),
("凡人", "смертный", "простолюдин"),
("修炼", "культивация", "тренировка"),
("长生", "бессмертие", "долголетие"),
("族长", "глава клана", "старейшина"), # the D38 c3 confound as a Z1 pair
("四代族长", "Четвёртый глава рода", "четвёртый старейшина"),
("人上之人", "человек над людьми", "человек среди людей"), # the c1 catastrophe as Z1
("方源", "Фан Юань", "Квадратный Источник"), # name mistranslated literally
("花酒行者", "Монах Цветочного Вина", "Странник цветов и вина"),
]
def call_local(prompt, timeout=90):
body = json.dumps({"model": MODEL, "think": False,
"messages": [{"role": "user", "content": prompt + "\n/no_think"}],
"stream": False, "options": {"temperature": 0.0, "num_predict": 200}}).encode()
req = urllib.request.Request(OLLAMA, data=body, headers={"Content-Type": "application/json"})
try:
with _opener.open(req, timeout=timeout) as r:
d = json.loads(r.read())
return (d.get("message", {}) or {}).get("content", ""), None
except Exception as e:
return "", f"{type(e).__name__}: {str(e)[:120]}"
def find_sentence(term, chunks):
tn = X.norm(term)
for c in chunks:
for sent in re.split(r"(?<=[。!?])", c.source):
if term in sent and 6 <= len(sent) <= 80:
return sent.strip()
return term
def verdict_yes_no(text):
text = re.sub(r"<think>.*?</think>", "", text, flags=re.S).lower()
# first explicit yes/да or no/нет
m = re.search(r"\b(да|верно|correct|yes|правильн)\b", text)
n = re.search(r"\b(нет|неверно|incorrect|no|ошибочн|неправильн)\b", text)
if n and (not m or n.start() < m.start()):
return "no"
if m:
return "yes"
return "?"
def main():
chunks = X.load_chunks()
pairs = []
for term, ok_dst, bad_dst in BENCH:
sent = find_sentence(term, chunks)
pairs.append((term, sent, ok_dst, "correct"))
pairs.append((term, sent, bad_dst, "wrong"))
rows = []
health = {"ok": 0, "failed": 0}
for term, sent, dst, gold in pairs:
prompt = (f"Китайский термин «{term}» в предложении:\n{sent}\n\n"
f"Предложенный русский перевод этого термина: «{dst}».\n\n"
f"Верен ли этот перевод термина по смыслу в данном контексте? "
f"Ответь одним словом: ДА или НЕТ, затем короткое обоснование.")
out, err = call_local(prompt)
if err:
health["failed"] += 1
rows.append(dict(term=term, dst=dst, gold=gold, pred="failed", err=err)); continue
health["ok"] += 1
pred = verdict_yes_no(out)
model_says = "correct" if pred == "yes" else ("wrong" if pred == "no" else "?")
rows.append(dict(term=term, dst=dst, gold=gold, pred=pred, model_says=model_says,
correct=(model_says == gold)))
mark = "" if model_says == gold else ("?" if pred == "?" else "")
print(f" {mark} {term:<6} «{dst[:24]:<24}» gold={gold:<7} model={model_says:<7}")
scored = [r for r in rows if r.get("pred") not in ("failed", "?")]
acc = sum(r["correct"] for r in scored) / len(scored) if scored else 0
# wrong-catch (recall on the Z1 wrong class) + correct-accept (specificity)
wrong = [r for r in scored if r["gold"] == "wrong"]
correct = [r for r in scored if r["gold"] == "correct"]
wrong_catch = sum(r["model_says"] == "wrong" for r in wrong) / len(wrong) if wrong else 0
correct_accept = sum(r["model_says"] == "correct" for r in correct) / len(correct) if correct else 0
print(f"\n=== A6b Z1 verifier ({MODEL}) — {len(pairs)} pairs ===")
print(f"health: {health}; scored: {len(scored)}")
print(f"accuracy: {acc:.3f}")
print(f"wrong-catch (recall on Z1 mistranslations): {sum(r['model_says']=='wrong' for r in wrong)}/{len(wrong)} = {wrong_catch:.3f}")
print(f"correct-accept (specificity): {sum(r['model_says']=='correct' for r in correct)}/{len(correct)} = {correct_accept:.3f}")
missed = [r["term"] for r in wrong if r["model_says"] != "wrong"]
print(f"Z1 mistranslations the local 9B FAILED to catch: {missed}")
json.dump({"model": MODEL, "health": health, "accuracy": acc, "wrong_catch": wrong_catch,
"correct_accept": correct_accept, "rows": rows},
open(OUT / "a6b_verify.json", "w"), ensure_ascii=False, indent=1)
print(f"[written] {OUT/'a6b_verify.json'}")
if __name__ == "__main__":
main()