textmachine/eval/role_topology/qe_bench.py

118 lines
7.7 KiB
Python
Raw Permalink 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, ШАГ 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-токенайзер вешает </s>; 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()