#!/usr/bin/env python3 """Ф0.4 детектор №2, ШАГ 1 — покрытие QE-ранкера на паре zh→ru. МИНИ-БЕНЧ ДО ОПОРЫ. Промт эксп-21 предписывает дословно: «QE-ранкер локальными весами CometKiwi/xCOMET — покрытие zh→ru проверить мини-бенчем ДО опоры». Это не формальность: WMT-метрики обучены на языковых парах шэрда, и zh→ru в них представлена слабо. Опереть на ранкер арм C и арм G, не проверив, что он вообще РАЗЛИЧАЕТ порчу на этой паре, — значит купить дорогой прогон вслепую. Что за модель и почему не CometKiwi. `Unbabel/wmt22-cometkiwi-da`, `wmt23-cometkiwi-da-xl` и `XCOMET-XL` на HuggingFace имеют `gated: auto` — нужен принятый лицензионный клик и токен, которого у сессии нет. Пакет `unbabel-comet` 2.2.7 вдобавок не импортируется на Python 3.14 (`functools. _HashedSeq` удалён в 3.14). Взято НЕзагейченное: **`google/metricx-24-hybrid-large-v2p6`** — та же задача (обученный предсказывать MQM-ошибку), есть штатный reference-free (QE) режим, mT5-large 1.23B, поднимается на CPU за ~7 с. Отклонение от буквы промта объявлено здесь, а не в отчёте задним числом. ⚠ ШКАЛА ПЕРЕВЁРНУТА: MetricX предсказывает ВЕЛИЧИНУ ОШИБКИ (0 = идеально, 25 = мусор). Больше — хуже. Все сравнения ниже написаны в этой ориентации; спутать её значит получить детектор, который флагает ровно здоровые сегменты. Земля бенча. Проба 18 посадила в шесть окон по одной СМЫСЛОВОЙ ИНВЕРСИИ (класс k2: сказано обратное исходнику — «не выдержат»→«легко выдержат», «поднимался»→«опускался»). Эталон известен посимвольно, посадки валидированы ревью посылки экспа-19 (12/12 регексов корректны). Это ровно тот класс, который детектор обязан ловить, и он построен ЧУЖОЙ сессией. Читается так: если испорченное окно НЕ получает систематически худший балл, чем чистое, — ранкер на zh→ru слеп, и арм C с реальными детекторами надо либо строить иначе, либо объявить неизмеримым. Запуск (офлайн, $0): ~/.venvs/tm-qe/bin/python eval/role_topology/qe_bench.py """ from __future__ import annotations import os import statistics import sys from pathlib import Path os.environ.setdefault("PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION", "python") REPO = Path("/home/ubuntu/projects/textmachine") sys.path.insert(0, "/home/ubuntu/.venvs/metricx") sys.path.insert(0, str(REPO / "eval" / "bank_arbitration")) sys.path.insert(0, str(REPO / "eval" / "editor_contract")) import torch # noqa: E402 from transformers import AutoTokenizer # noqa: E402 from metricx24 import models # noqa: E402 import os as _os # Модель переключается переменной окружения, чтобы отрицательный результат можно было # перепроверить на СТАРШЕЙ версии тем же кодом, а не второй копией скрипта. MODEL = _os.environ.get("METRICX_MODEL", "google/metricx-24-hybrid-large-v2p6") TOKENIZER = _os.environ.get("METRICX_TOKENIZER", "google/mt5-large") MAX_LEN = 1536 class QE: """Reference-free обёртка MetricX-24. Балл = предсказанная величина ошибки, БОЛЬШЕ — ХУЖЕ.""" def __init__(self) -> None: self.tok = AutoTokenizer.from_pretrained(TOKENIZER, legacy=False) self.model = models.MT5ForRegression.from_pretrained(MODEL, dtype=torch.float32) self.model.eval() def score(self, source: str, candidate: str) -> float: # Формат входа QE-режима задан авторами метрики (metricx24/predict.py): без reference. text = f"source: {source} candidate: {candidate}" enc = self.tok(text, max_length=MAX_LEN, truncation=True, padding=False, return_tensors="pt") ids = enc["input_ids"] # mT5-токенайзер вешает ; MetricX обучен БЕЗ него — авторский препроцессинг снимает. ids = ids[:, :-1] with torch.no_grad(): # ⚠ `use_cache=False` ОБЯЗАТЕЛЕН и найден исполнением: с кэшем по умолчанию # transformers 4.57 строит маску кросс-внимания на один токен короче выхода энкодера # и падает («size of tensor a (N) must match b (N-1)») на ЛЮБОЙ длине входа. Веса # MetricX старше этой версии transformers; апстрим её не пинит. out = self.model(input_ids=ids, attention_mask=torch.ones_like(ids), use_cache=False) return float(out.predictions[0]) def main() -> None: import editor_wire_probe as P # noqa: PLC0415 from inject_probe import pick_windows # noqa: PLC0415 qe = QE() wins = pick_windows() print("МИНИ-БЕНЧ ПОКРЫТИЯ zh→ru: MetricX-24-hybrid-large, reference-free режим") print("шкала перевёрнута — БОЛЬШЕ значит ХУЖЕ\n") print(f"{'окно':6s}{'чистый':>9s}{'k1 слово':>10s}{'k2 инверсия':>13s}" f"{'Δk1':>8s}{'Δk2':>8s}") print("-" * 54) d1, d2 = [], [] for k, (_, src, _) in enumerate(wins): clean = P.draft_of(k) s_clean = qe.score(src, clean) row = [s_clean] for cls in ("k1", "k2"): one = [d for d in P.DEFECTS[k] if d[0] == cls] bad = clean for _c, old, new, _b, _f in one: bad = bad.replace(old, new, 1) row.append(qe.score(src, bad)) d1.append(row[1] - row[0]) d2.append(row[2] - row[0]) print(f"w{k:<5d}{row[0]:9.3f}{row[1]:10.3f}{row[2]:13.3f}" f"{row[1] - row[0]:+8.3f}{row[2] - row[0]:+8.3f}") print("-" * 54) print(f"{'медиана Δ':6s}{'':9s}{'':10s}{'':13s}{statistics.median(d1):+8.3f}" f"{statistics.median(d2):+8.3f}") for name, d in (("k1 выдуманное слово", d1), ("k2 смысловая инверсия", d2)): worse = sum(1 for x in d if x > 0) print(f" {name:24s} испорченное хуже чистого в {worse}/{len(d)} окнах") print("\nЧитать: положительная Δ = ранкер увидел порчу. Знак, а не величина, решает вопрос " "покрытия;\nвеличина скажет, хватит ли разрешения на СЕГМЕНТНЫЙ флаг (шаг 2).") if __name__ == "__main__": main()