textmachine/eval/role_topology/selfcheck.py

132 lines
8.4 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
"""РЕВЬЮ ИСПОЛНЕНИЕМ для харнесса эксп-21 (мандат самопроверки, D39.46/промт §Мандат).
Зачем. Полигон регулярно ошибается, и ошибка рига неотличима от находки, пока её не поймали
отдельным кодом (D37: заявленные «0 ошибок» = 36 ошибок; эксп-20: FEWSHOT утёк в боевой арм и был
пойман только регрессией на длину промпта). Здесь пинится всё, на чём стоят выводы Ф0.4, включая
дефекты, найденные в САМОЙ ЗЕМЛЕ, — иначе следующий ре-ран молча получит другие числа.
Проверки, которые НЕ требуют весов QE, идут всегда. Проверки MetricX идут только если модель уже
скачана (`--qe`), потому что 4.6 ГБ весов не должны быть условием прогона детектора слов.
Запуск: eval/.venv/bin/python eval/role_topology/selfcheck.py
~/.venvs/tm-qe/bin/python eval/role_topology/selfcheck.py --qe
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
sys.path.insert(0, str(REPO / "eval" / "role_topology"))
sys.path.insert(0, str(REPO / "eval" / "bank_arbitration"))
sys.path.insert(0, str(REPO / "eval" / "editor_contract"))
FAILS = 0
def ck(name: str, ok: bool, got: str = "") -> None:
global FAILS
if not ok:
FAILS += 1
print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {name}" + ("" if ok else f"{got}"))
def check_word_detector() -> None:
import detect_word as D # noqa: PLC0415
bank = D.load_bank()
ck("С1: вайтлист банка непуст и несёт канон", len(bank) > 100 and "юаня" in bank,
f"{len(bank)} форм")
# Сигналы поодиночке. Ловушка этого детектора — слой, который «улучшает» precision, гася
# настоящие дефекты; поэтому каждый слой проверяется И на пропуск, И на срабатывание.
ck("С0: обычное русское слово знакомо словарю", D.known("сокровище"))
ck("С0: выдуманное слово словарю незнакомо", not D.known("дерзновяжной"))
ck("С2: реальная транскрипция опознана как транскрипция", D.translit_shape("фанчжэнь"))
ck("С2: склонённая транскрипция опознана", D.translit_shape("цунях"))
ck("С2: выдуманное слово НЕ опознано как транскрипция",
not D.translit_shape("дерзновяжной"))
ck("С3: продуктивное сложение разбирается", D.decomposes("высокорангового"))
ck("С3: суффиксальная деривация разбирается", D.decomposes("женьшеневого"))
ck("С3 (регрессия снятой редакции): «силённый» НЕ разбирается",
not D.decomposes("силённый"),
"вернулся жадный цикл «известное начало + любой хвост», гасивший 6 дефектов из 10")
# Выделенная валидация. Пинится ЗНАЧЕНИЕ, а не факт запуска: если посадки перестанут ловиться,
# это регрессия детектора, и она обязана быть громкой.
import editor_wire_probe as WP # noqa: PLC0415
plants = [w for rs in WP.DEFECTS.values() for cls, old, new, _, _ in rs if cls == "k1"
for w in new.split() if w not in old.split()]
hit = sum(1 for w in plants if D.verdict(w, bank)[0])
ck(f"holdout: посадки k1 пробы 18 ловятся {hit}/{len(plants)}",
(hit, len(plants)) == (6, 6), f"{hit}/{len(plants)}")
rows = [json.loads(l) for l in D.LABELS.read_text(encoding="utf-8").splitlines() if l.strip()]
s = D.score(rows, 3, bank)
ck("k3: полный детектор даёт tp=7 fp=2 fn=3",
(s["tp"], s["fp"], s["fn"]) == (7, 2, 3),
f"tp={s['tp']} fp={s['fp']} fn={s['fn']}")
ck("k3: оба выживших ложных срабатывания — известная пара U+0301",
sorted(s["fp_words"]) == ["льшим", "льшую"], str(sorted(s["fp_words"])))
# ⚠ ДЕФЕКТЫ ЗЕМЛИ, найденные при проверке посылки. Пинятся здесь, чтобы отчёт не пересчитал
# recall по номинальным 10 позитивам, когда часть из них не того класса и не находится.
corp = D.LABELS.parent.parent / "raw" / "corpus.jsonl"
big = "\n".join((json.loads(l).get("draft") or "") + "\n" + (json.loads(l).get("final") or "")
for l in corp.read_text(encoding="utf-8").splitlines() if l.strip())
ck("земля: «вперди» и «силённый» ОТСУТСТВУЮТ в corpus.jsonl (дефект набора пакета-6)",
"вперди" not in big and "силённый" not in big,
"они появились — набор пере-собран, числа recall пере-считать")
ck("земля: «ной» — валидное русское слово, а не выдуманное (класс метки неверен)",
D.known("ной"))
def check_alignment() -> None:
import align as Q # noqa: PLC0415
zh = ["天很黑。", "他走了。", "风停了,雨也停了。"]
ru = ["Было темно.", "Он ушёл.", "Ветер стих.", "Дождь тоже прекратился."]
pairs = Q.align(zh, ru)
ck("выравнивание: монотонно и без потерь по zh",
[i for a, _ in pairs for i in a] == list(range(len(zh))),
str([a for a, _ in pairs]))
ck("выравнивание: монотонно и без потерь по ru",
[i for _, b in pairs for i in b] == list(range(len(ru))),
str([b for _, b in pairs]))
ck("выравнивание: 1→2 распознано на последней паре",
any(len(a) == 1 and len(b) == 2 for a, b in pairs), str(pairs))
ck("разбиение zh: терминаторы не съедены", len(Q.split_zh("甲。乙!丙?")) == 3,
str(Q.split_zh("甲。乙!丙?")))
ck("разбиение ru: аббревиатуры не рвут строку по строчной букве",
len(Q.split_ru("Он сказал. она молчала.")) == 1,
str(Q.split_ru("Он сказал. она молчала.")))
def check_qe() -> None:
from qe_bench import QE # noqa: PLC0415
qe = QE()
good = qe.score("他没有来。", "Он не пришёл.")
bad = qe.score("他没有来。", "Он пришёл.")
# САМАЯ ОПАСНАЯ ОШИБКА этого детектора — перепутанная ориентация шкалы: он тогда флагает
# ровно здоровые сегменты, и это выглядит как работающий детектор.
ck(f"QE: шкала перевёрнута (хуже = больше); {good:.2f} < {bad:.2f}", bad > good,
f"верный {good:.3f}, инверсия {bad:.3f}")
ck("QE: детерминизм повтора", abs(qe.score("他没有来。", "Он не пришёл.") - good) < 1e-4)
ck("QE: пустой кандидат штрафуется", qe.score("他没有来。", "") > good)
def main() -> None:
check_word_detector()
check_alignment()
if "--qe" in sys.argv:
check_qe()
else:
print("[ПРОП ] проверки QE пропущены (запустить ~/.venvs/tm-qe/bin/python … --qe)")
print(f"\n{'ХАРНЕСС ЧИСТ' if not FAILS else f'ПРОВАЛОВ: {FAILS}'}")
sys.exit(1 if FAILS else 0)
if __name__ == "__main__":
main()