118 lines
7.7 KiB
Python
118 lines
7.7 KiB
Python
#!/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()
|