128 lines
9.3 KiB
Python
128 lines
9.3 KiB
Python
#!/usr/bin/env python3
|
||
"""Ф0.6 (часть) — КАЛИБРОВКА ВНЕШНЕГО ГЕЙТА АРМА G на реальных редакторских правках. $0.
|
||
|
||
Арм G принимает правку редактора, только если внешний гейт видит улучшение (guarded APE,
|
||
max(draft, edited)). Шаг 3 (`qe_power.py`) показал, что парный QE уверенно ловит ПОРЧУ: инверсия
|
||
полярности даёт медиану Δ +1.49 при направлении 66/72, p<1e-5. Но это половина вопроса. Вторая
|
||
половина — цена ошибки в другую сторону: сколько ХОРОШИХ правок гейт отвергнет, потому что не
|
||
увидел в них улучшения. Арм, который режет половину полезной редактуры, бесполезен независимо от
|
||
того, как хорошо он ловит порчу.
|
||
|
||
Норма, по которой это делается ДО прогона, а не после: «Сначала померить ИНСТРУМЕНТ, потом им
|
||
мерить» (эксп-20 §7 п.0). Гейт арма G — инструмент, и его поведение на реальных правках обязано
|
||
быть известно до того, как на нём построен вердикт о топологии.
|
||
|
||
Материал. `~/books/gu-zhenren/labels/raw/corpus.jsonl` несёт для одной единицы ВСЕ ТРИ текста:
|
||
`source` (zh), `draft` (черновик) и `final` (после редактора). То есть реальная редакторская правка
|
||
уже сделана и лежит — её не надо покупать. Сегменты выравниваются тем же кодом, что в шаге 2.
|
||
|
||
Что печатается и как читается:
|
||
Δ = QE(final) − QE(draft) при одном и том же источнике. Шкала перевёрнута ⇒ Δ<0 значит «редактор
|
||
улучшил по мнению гейта», Δ>0 — «ухудшил».
|
||
Гейт арма G принял бы правку при Δ < 0. Доля принятых и есть его пропускная способность.
|
||
|
||
⚠ Что этот замер НЕ говорит. Он не утверждает, что правки с Δ>0 плохи: QE — оценка адекватности
|
||
перевода источнику, а редактор по ратифицированному мандату D32.1(г) покупает СТИЛЬ, а не верность
|
||
(«ось решения для черновика — верность, не стиль; стиль редактор переписывает»). Стилевое улучшение
|
||
при неизменной верности даёт Δ≈0, и гейт его отвергнет — это не ошибка гейта, а его конструктивная
|
||
слепота, и именно её величину надо знать заранее.
|
||
|
||
Запуск (офлайн, $0): ~/.venvs/tm-qe/bin/python eval/role_topology/qe_guard.py [n_units]
|
||
"""
|
||
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"))
|
||
|
||
from align import align, split_ru, split_zh # noqa: E402
|
||
from qe_bench import QE, MODEL # noqa: E402
|
||
|
||
OUT = Path.home() / "books" / "role-topology"
|
||
CORPUS = Path.home() / "books" / "gu-zhenren" / "labels" / "raw" / "corpus.jsonl"
|
||
|
||
|
||
def main(n_units: int) -> None:
|
||
units = [json.loads(l) for l in CORPUS.read_text(encoding="utf-8").splitlines() if l.strip()]
|
||
units = [u for u in units
|
||
if (u.get("source") or "").strip() and (u.get("draft") or "").strip()
|
||
and (u.get("final") or "").strip()]
|
||
# Берётся ПРЕФИКС, а не случайная выборка: `Math.random` в этом харнессе сделал бы прогон
|
||
# невоспроизводимым, а вопрос замера не требует случайности — требуется объявленное n.
|
||
units = units[:n_units]
|
||
|
||
qe = QE()
|
||
rows: list[dict] = []
|
||
unit_rows: list[dict] = []
|
||
skipped = 0
|
||
for idx, u in enumerate(units):
|
||
zs = split_zh(u["source"])
|
||
ds, fs = split_ru(u["draft"]), split_ru(u["final"])
|
||
# Черновик и финал выравниваются с источником НЕЗАВИСИМО: редактор меняет число
|
||
# предложений (эксп-20 §4: 13→6 абзацев), и общее выравнивание было бы подгонкой.
|
||
pd = [(a, b) for a, b in align(zs, ds) if a and b]
|
||
pf = [(a, b) for a, b in align(zs, fs) if a and b]
|
||
# ⚠ СОПОСТАВЛЕНИЕ ПО ПЕРЕСЕЧЕНИЮ, а не по точному совпадению группы zh-индексов.
|
||
# Первая редакция требовала совпадения кортежа и теряла 596 сегментов из 1143 — причём
|
||
# СИСТЕМАТИЧЕСКИ: группировка расходится ровно там, где редактор слил или разбил
|
||
# предложения, то есть там, где он и работал. Клейм на такой выборке был бы смещён в
|
||
# сторону консервативных правок. Здесь берётся объединение пересекающихся групп.
|
||
for si, di in pd:
|
||
sset = set(si)
|
||
hit = [(a, b) for a, b in pf if sset & set(a)]
|
||
if not hit:
|
||
skipped += 1
|
||
continue
|
||
s = " ".join(zs[i] for i in sorted(sset | {i for a, _ in hit for i in a}))
|
||
d = " ".join(ds[i] for i in di)
|
||
f = " ".join(fs[i] for i in sorted({i for _, b in hit for i in b}))
|
||
if d == f:
|
||
continue # редактор сегмент не тронул — решать нечего
|
||
rows.append(dict(unit=idx, draft=qe.score(s, d), final=qe.score(s, f),
|
||
dlen=len(d), flen=len(f)))
|
||
|
||
# ЕДИНИЦА ЦЕЛИКОМ — замер без выравнивания вовсе, поэтому без потерь по построению.
|
||
# Арм G может гейтить и на этой гранулярности; какая из двух работает, решает замер.
|
||
unit_rows.append(dict(unit=idx,
|
||
draft=qe.score(u["source"], u["draft"]),
|
||
final=qe.score(u["source"], u["final"])))
|
||
|
||
if not rows:
|
||
print("сравнимых сегментов не набралось")
|
||
return
|
||
deltas = [r["final"] - r["draft"] for r in rows]
|
||
better = sum(1 for x in deltas if x < 0)
|
||
same = sum(1 for x in deltas if abs(x) < 0.25)
|
||
print(f"КАЛИБРОВКА ГЕЙТА АРМА G — модель {MODEL}")
|
||
print(f"единиц {len(units)} · изменённых редактором сегментов {len(rows)} · "
|
||
f"без пары после выравнивания {skipped}\n")
|
||
print(f"медиана Δ (final − draft) {statistics.median(deltas):+.3f}")
|
||
print(f"редактор УЛУЧШИЛ по гейту {better}/{len(rows)} = {better / len(rows):.0%}")
|
||
print(f"гейт видит паритет (|Δ|<0.25) {same}/{len(rows)} = {same / len(rows):.0%}")
|
||
print(f"\n{'решение гейта':>28s}{'сегментов':>11s}{'доля':>8s}")
|
||
print("-" * 47)
|
||
for label, cond in (("принять правку (Δ<0)", lambda x: x < 0),
|
||
("принять с запасом (Δ<−0.5)", lambda x: x < -0.5),
|
||
("отвергнуть (Δ>0)", lambda x: x > 0),
|
||
("отвергнуть уверенно (Δ>0.5)", lambda x: x > 0.5)):
|
||
k = sum(1 for x in deltas if cond(x))
|
||
print(f"{label:>28s}{k:11d}{k / len(rows):8.0%}")
|
||
ud = [r["final"] - r["draft"] for r in unit_rows]
|
||
if ud:
|
||
ub = sum(1 for x in ud if x < 0)
|
||
print(f"\nЕДИНИЦА ЦЕЛИКОМ (без выравнивания, потерь нет по построению), n={len(ud)}:")
|
||
print(f" медиана Δ {statistics.median(ud):+.3f} · редактор улучшил {ub}/{len(ud)} "
|
||
f"= {ub / len(ud):.0%}")
|
||
(OUT / f"qe-guard-{'xl' if 'XL' in MODEL else 'large'}.json").write_text(
|
||
json.dumps({"segments": rows, "units": unit_rows}, ensure_ascii=False), encoding="utf-8")
|
||
print("\nЧитать вместе с шагом 3: гейт ловит порчу (Δ +1.49 на инверсии, 66/72), но его\n"
|
||
"решение по РЕАЛЬНОЙ правке — это доля выше, и она определяет, что арм G вообще купит.")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
os.environ.setdefault("PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION", "python")
|
||
main(int(sys.argv[1]) if len(sys.argv) > 1 else 25)
|