#!/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)