textmachine/eval/role_topology/qe_power.py

116 lines
7.9 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
"""Ф0.4 детектор №2, ШАГ 3 — МОЩНОСТЬ парного QE на классе «инверсия полярности».
Что осталось неотвеченным после шага 2. На XL парная Δ вышла положительной по медиане в обоих
классах (+0.981 и +1.147) с направлением 4/6 и 5/6. Соблазн прочитать это как «на сравнении версий
ранкер работает» — но n=6, и знаковый тест при 5/6 даёт односторонний p≈0.11. Клейм НЕ УСТАНОВЛЕН,
и объявлять его установленным на шести точках — ровно тот антипаттерн, который эксп-19 уже допустил
однажды (правило «диапазоны не пересекаются» при n=3, отозвано адверсариальным ревью).
Почему вопрос стоит того, чтобы добрать мощность. Он разводит два РАЗНЫХ применения ранкера:
арм C ищет дефект в ОДНОМ тексте, образца для сравнения нет ⇒ нужен АБСОЛЮТНЫЙ порог.
Шаг 2 показал, что порога нет: на обеих моделях лучшая precision ≤0.17.
арм G сравнивает ДВЕ версии одного сегмента (guarded APE, «принимать правку, только если
внешний гейт видит улучшение») ⇒ нужна ПАРНАЯ дельта, и только она.
Если парная дельта состоятельна, арм G питаем, а арм C — нет. Это не оттенок, а разная судьба двух
армов фазы 2б, поэтому вопрос решается замером, а не осторожной формулировкой.
Как добирается n. Ручных посадок больше не станет — их делала другая сессия под другую задачу.
Зато ИНВЕРСИЯ ПОЛЯРНОСТИ порождается механически и остаётся тем же классом дефекта: снятие или
вставка отрицания при глаголе меняет смысл на противоположный, сохраняя грамматичность. По 81
здоровому сегменту строится столько пар, сколько найдётся глаголов, и каждая судится парно.
⚠ ГРАНИЦА, объявленная заранее: механическая инверсия ЛЕГЧЕ ручной посадки. Голое «не» — сильный
поверхностный сигнал, а посадки пробы 18 меняли смысл переписыванием оборота («не выдержат» →
«легко выдержат»). Поэтому результат читается как ВЕРХНЯЯ ГРАНИЦА парного режима: если ранкер
не берёт даже это, вопрос закрыт отрицательно; если берёт — это ещё не значит, что возьмёт тонкую
инверсию, и ручные 6 точек остаются единственной оценкой на реальном классе.
Запуск (офлайн, $0):
~/.venvs/tm-qe/bin/python eval/role_topology/qe_power.py # large
METRICX_MODEL=google/metricx-24-hybrid-XL-v2p6 METRICX_TOKENIZER=google/mt5-xl \
~/.venvs/tm-qe/bin/python eval/role_topology/qe_power.py # XL
"""
from __future__ import annotations
import json
import os
import statistics
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"))
sys.path.insert(0, str(REPO / "eval" / "exp16"))
import pymorphy3 # noqa: E402
from align import align, flip_polarity, split_ru, split_zh # noqa: E402
from qe_bench import QE, MODEL # noqa: E402
_MORPH = pymorphy3.MorphAnalyzer()
OUT = Path.home() / "books" / "role-topology"
def sign_test_p(successes: int, n: int) -> float:
"""Односторонний p знакового теста при H0: направление случайно (q=0.5).
Считается точно через биномиальные коэффициенты — scipy тут не нужен, а зависимость лишняя.
"""
from math import comb # noqa: PLC0415
return sum(comb(n, k) for k in range(successes, n + 1)) / (2 ** n)
def main() -> None:
import editor_wire_probe as P # noqa: PLC0415
from inject_probe import pick_windows # noqa: PLC0415
qe = QE()
pairs_scored: list[dict] = []
for k, (_, src, _) in enumerate(pick_windows()):
clean = P.draft_of(k)
zs, rs = split_zh(src), split_ru(clean)
for si, di in align(zs, rs):
if not si or not di:
continue
s = " ".join(zs[i] for i in si)
d = " ".join(rs[i] for i in di)
bad = flip_polarity(d)
if not bad or bad == d:
continue
base = qe.score(s, d)
pairs_scored.append(dict(window=k, base=base, flipped=qe.score(s, bad),
seg_len=len(d)))
n = len(pairs_scored)
if not n:
print("инверсий не построено — проверить flip_polarity")
return
deltas = [r["flipped"] - r["base"] for r in pairs_scored]
pos = sum(1 for x in deltas if x > 0)
print(f"МОЩНОСТЬ ПАРНОГО QE на инверсии полярности — модель {MODEL}")
print(f"пар построено: {n} (из 81 здорового сегмента шести окон)\n")
print(f"медиана Δ {statistics.median(deltas):+.3f}")
print(f"среднее Δ {statistics.mean(deltas):+.3f}")
print(f"направление {pos}/{n} = {pos / n:.0%}")
print(f"знаковый тест p = {sign_test_p(pos, n):.5f} (односторонний)")
print(f"Δ > 1.0 {sum(1 for x in deltas if x > 1.0)}/{n}")
print(f"Δ < 0 (ранкер счёл инверсию ЛУЧШЕ) {sum(1 for x in deltas if x < 0)}/{n}")
# Операционный вопрос арма G: с каким порогом на ПАРНУЮ дельту он принимал бы решение и
# сколько здоровых правок при этом отверг бы. Ложная тревога здесь = отказ от хорошей правки.
print(f"\n{'порог Δ':>9s}{'поймано инверсий':>19s}{'доля':>8s}")
print("-" * 36)
for thr in (0.0, 0.25, 0.5, 1.0, 2.0):
got = sum(1 for x in deltas if x > thr)
print(f"{thr:9.2f}{got:19d}{got / n:8.0%}")
(OUT / f"qe-power-{'xl' if 'XL' in MODEL else 'large'}.json").write_text(
json.dumps(pairs_scored, ensure_ascii=False), encoding="utf-8")
print("\nЧитать: это ВЕРХНЯЯ граница парного режима (механическая инверсия проще ручной).\n"
"Отрицательный исход закрывает вопрос; положительный — открывает только арм G, не арм C.")
if __name__ == "__main__":
os.environ.setdefault("PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION", "python")
main()