132 lines
8.4 KiB
Python
132 lines
8.4 KiB
Python
#!/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()
|