117 lines
7.9 KiB
Python
117 lines
7.9 KiB
Python
#!/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 re
|
||
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()
|