Add the adaptive-memory research verdict (doc 14) with four executable probes: flagship logprob access, in-context demos vs glossary, lemma post-check, and the deterministic lever curve
This commit is contained in:
parent
1878c3ea95
commit
e4ca3a57fd
5 changed files with 617 additions and 1 deletions
|
|
@ -79,8 +79,10 @@
|
|||
| D. глоссарий + дистрактор-демонстрации | **0.875** | near-miss не укусил на этой метрике/масштабе |
|
||||
| E. глоссарий с ОДНОЙ отравленной строкой (赵太爷→«Ли») | **0.833**, отравление принято **0/3** | сильная модель **сопротивлялась** отраве на известном имени |
|
||||
|
||||
**Поправка (диагностика `eval/adaptive_reask.py`):** строгий accept-регэксп **занизил** глоссарий. Персистентный промах 王胡 — **артефакт метрики, не провал adherence**: модель рендерила канон Рогова «Бородатого Вана» (склонённое), а `Ван\b` не ловит суффикс. Склонение-толерантный матч → **глоссарий = 8/8 = 1.000** (оба репа, ноль реальных провалов adherence). То есть **на этом чанке детерминированный глоссарий УЖЕ насыщает консистентность** — demos не бьют его **потому что бить нечего** (потолок). Это (а) усиливает вердикт §2, (б) — **живая инстанция несущего невалидированного риска research/13 §2**: post-check регэкспом в русской морфологии undercount'ит И даёт ложные флаги → **post-check обязан лемматизировать (pymorphy2/Natasha), а не регэкспить.**
|
||||
|
||||
Три индикативных вывода (малый N, hand-built accept-регэксп → риск само-подтверждения, одна книга/модель — **не** финальный eval):
|
||||
1. **Детерминированный soft-глоссарий сильно работает на zh→ru** (0.458→0.875) — впервые индикативно подтверждено на нашем направлении (валидация банка это на zh→ru не мерила). Хорошая новость для ядра.
|
||||
1. **Детерминированный soft-глоссарий сильно работает на zh→ru** (без памяти 0.458 → глоссарий **~1.0** после поправки на склонение) — впервые индикативно подтверждено на нашем направлении (валидация банка это на zh→ru не мерила). Хорошая новость для ядра. Оговорка: чанк «лёгкий» (глоссарий насыщается) → реальную ценность demos и re-ask надо мерить на **трудных** чанках (длинные/плотные, где adherence падает на 17–36%, 2310.05824).
|
||||
2. **Демонстрации НЕ бьют глоссарий по консистентности** (0.833 vs 0.875) — ровно как предсказывает Li (выигрыш демо — 65% стиль, а не термин-матч, чего эта метрика не видит). Значит **выигрыш демонстраций, если он есть, — стилевой, а не консистентность/fidelity**, а БАР-(в) меряет именно консистентность/fidelity.
|
||||
3. **Отравленную строку сильная модель отвергла** на ИЗВЕСТНОМ имени (阿Q-канон в претрейне) — мягкое свидетельство против слепого over-trust, но **НЕ** тест на НОВОМ термине без прайора (там over-trust реален — 2510.00829).
|
||||
|
||||
|
|
@ -174,6 +176,18 @@
|
|||
|
||||
---
|
||||
|
||||
## §9. Максимизация коэффициента канон-консистентности — детерминированные рычаги (по запросу владельца)
|
||||
|
||||
Раз гибрид отклонён — правильный ход не «обучать ранкер», а **добивать детерминированную сторону**. Замер `eval/adaptive_levers.py` (трудный чанк 阿Q, 11 сущностей, 1600 знаков, deepseek+grok) даёт кривую рычагов и порядок внедрения бэкенду:
|
||||
|
||||
- **L1 — post-check ЛЕММАТИЗИРУЮЩИЙ (pymorphy3), не регэксп. Внедрять ПЕРВЫМ.** Наивный `\b`-регэксп на трудном чанке даёт **18–36% ложных флагов** (deepseek 2/11, grok 4/11 — склонённые формы, отрендеренные верно). pymorphy3 их снимает и **сворачивает OOV-транслит** (Вана→ван, Дэна→дэн, Вэйчжуане→вэйчжуан). Это количественная валидация несущего невалидированного риска research/13 §2 (регэксп-шум → редакторы не верят флагам → safety рушится). Дешевле всего, чинит больше всего.
|
||||
- **L2 — RECALL матчера: нормализация (полная, тестируемая) + alias-граф + sticky.** Точный матч на trad/alias формах: recall **0.00 → +норм 0.50 → +alias 0.75**. Живой баг: неполная trad-карта (爺→爷) молча промахнула 赵太爷 → **A4 подтверждён: нормализацию покрыть и тестировать бит-в-бит** (OpenCC, не мини-карта). Крупнейший рычаг рекола (retrieval_bench recall 0.571 — 43% упоминаний не инъектится).
|
||||
- **L3 — post-check + точечный re-ask** (verifier-gated correction, M2). На трудном чанке добивает реальные промахи adherence (deepseek 0.82→0.91). Не чинит гомограф/односимвольный ключ (钱) — и не должен.
|
||||
- **L4 — таблица Палладия/Поливанова (B6): потолок ПРАВИЛЬНОСТИ.** L1–L3 поднимают «использует ли модель канон»; L4 — «правильный ли канон». Локаль zh 0.26, облако-топ 0.75 — только детерминированная таблица закрывает разрыв.
|
||||
- **Побочно подтверждено:** 钱 (односимвольный ключ, гомограф Цянь/деньги) упорно промахивается даже после re-ask → **правило `min_key_len`/запрет одиночных ключей (A3) обязательно** — 钱 из глоссария выкинуть или заменить целой сущностью (钱府→«дом Цянь»).
|
||||
|
||||
**Где ваш инстинкт «тюнить ранкер» легитимен (честно):** ТОЛЬКО во втором эшелоне (fuzzy/семантика, кандидаты для человека) — у bge-m3 нет разделяющего порога (retrieval_bench), и research/13 §5 оставил лазейку: другое представление / sparse-голова / маленький cross-encoder reranker может разделять лучше. Это Фаза-2, low-trust, коэффициент горячего пути (§9) не двигает. Инстинкт верен, цель — второй эшелон, не костяк.
|
||||
|
||||
## Честные слабые места рекомендаций (по этим пунктам пишут решение)
|
||||
|
||||
1. **Ни одна цифра литературы не на zh/ja→ru — тотально.** Все 24 верификации: DelTA, Moslem, Li, Power-of-Noise, self-correction-скептики, kNN-MT, LoRA, ModernMT/Lilt — англо-центричны или ru только как источник. Ближайшее к нам — **WMT24 Discourse-Level Literary (2412.11732) с добавленным zh→ru document-level треком** и **DITING (2510.09116)** zh-en вебновелла — но это **бенчмарки-оценки**, не методы адаптации, и ja→ru литературной адаптации/бенчмарка **не существует вовсе** (2505.05423 держит ru только как источник). Любая рекомендация по ja→ru — по аналогии, не по данным.
|
||||
|
|
|
|||
170
eval/adaptive_incontext.py
Normal file
170
eval/adaptive_incontext.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
#!/usr/bin/env python3
|
||||
"""INDICATIVE in-context experiment for the adaptive-memory verdict (docs/research/14).
|
||||
|
||||
Tests THE central BAR question of the "free win" (in-context demonstrations) strand:
|
||||
does adding the book's own DEMONSTRATIONS (prior translated passages) beat the
|
||||
deterministic baseline the bank ALREADY ships — soft-glossary injection — for keeping a
|
||||
recurring entity's rendering canonical and consistent on a REAL zh→ru literary chunk;
|
||||
and do the learning layer's failure modes actively HARM (Power-of-Noise near-miss
|
||||
distractor, 2401.14887; over-trust of a poisoned entry, 2510.00829)?
|
||||
|
||||
Testbed: 魯迅《阿Q正传》ch5-9 (real PD zh) — a dense chunk where А-Q fantasizes the
|
||||
revolution and names 8 recurring characters at once. Canonical dst = В. Рогов's published
|
||||
translation (eval/data/samples/ru/luxun-ah-q-ru.txt); demonstrations are AUTHENTIC Rogov
|
||||
sentences. Metric = the bank's OWN deterministic post-check (canonical stem present in
|
||||
output), scored only over entities whose zh key is in the source chunk.
|
||||
|
||||
Arms:
|
||||
A_bare no memory (raw model)
|
||||
B_glossary soft-glossary "src → dst" block <- THE deterministic baseline
|
||||
C_demos glossary + 3 authentic prior passages <- the learning layer (free win)
|
||||
D_noise glossary + 3 IRRELEVANT prior passages <- Power-of-Noise near-miss arm
|
||||
E_poison glossary with ONE wrong dst (赵太爷→"Ли") <- over-trust / silent-injection arm
|
||||
|
||||
Model: deepseek-v4-flash (the real draft model; thinking ON by default → avoids the zh
|
||||
echo). N reps (temp 0.3) for stability. INDICATIVE: small N, hand-built accept-regexes
|
||||
(self-confirmation risk), one model, one chunk. Not the Phase-2.5 in-house eval — a probe.
|
||||
|
||||
Usage: eval/.venv/bin/python eval/adaptive_incontext.py [--reps 3] [--provider deepseek]
|
||||
Output: stdout table + eval/data/adaptive_incontext.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import refusal_bench as rb # reuse call_provider + providers quirks + env load
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
SRC = ROOT / "data" / "samples" / "zh" / "luxun-ah-q-ch5-9.txt"
|
||||
OUT = ROOT / "data" / "adaptive_incontext.json"
|
||||
|
||||
# Dense multi-entity window (offset 6360, 650 chars): А-Q's revolution fantasy.
|
||||
CHUNK_OFF, CHUNK_LEN = 6360, 650
|
||||
|
||||
# Recurring entities: zh key -> (canonical Rogov dst shown in glossary, accept-regex on
|
||||
# the canonical STEM allowing Russian declension). Stem match = the bank's post-check (E1).
|
||||
ENTITIES = {
|
||||
"阿Q": ("А-кью", r"А[-\s]?кью"),
|
||||
"未庄": ("Вэйчжуан (деревня)", r"Вэйчжуан"),
|
||||
"赵太爷": ("почтенный Чжао", r"Чжао"),
|
||||
"王胡": ("Бородатый Ван", r"Ван\b"),
|
||||
"小D": ("Маленький Дэн", r"Дэн"),
|
||||
"假洋鬼子": ("Поддельный заморский чёрт", r"заморск\w+ чёрт|поддельн"),
|
||||
"吴妈": ("У-ма", r"У[-\s]?ма"),
|
||||
"邹七嫂": ("тётушка Цзоу Седьмая", r"Цзоу"),
|
||||
}
|
||||
|
||||
# Arm C: AUTHENTIC prior translated passages (Rogov), each carrying a recurring entity in
|
||||
# natural inflected Russian — the "demonstration" the learning layer would retrieve.
|
||||
DEMOS_RELEVANT = [
|
||||
"Он знал, что вместо него нанимают Маленького Дэна, этого худого и слабосильного бедняка.",
|
||||
"Сын почтенного Чжао добился, наконец, учёной степени сюцая, и об этом оповестили всю деревню.",
|
||||
"Жители деревни Вэйчжуан требовали от него только подённой работы да насмехались над ним.",
|
||||
]
|
||||
|
||||
# Arm D: IRRELEVANT prior passages (near-miss distractors) — plausible book prose, but
|
||||
# about material NOT in this chunk. Tests "better nothing than garbage" on the real wire.
|
||||
DEMOS_NOISE = [
|
||||
"Стены храма Тудигун почернели от времени, и ветер свистел в щелях по ночам.",
|
||||
"На реке скрипели вёсла лодочников, а торговцы раскладывали на пристани сушёную рыбу.",
|
||||
"В тот год урожай риса выдался скудным, и цены на масло поднялись по всей округе.",
|
||||
]
|
||||
|
||||
SYSTEM = ("Ты профессиональный литературный переводчик. Переведи фрагмент художественного "
|
||||
"произведения на русский язык. Это перевод существующего текста, предоставленного "
|
||||
"правообладателем. Сохрани все реплики и детали без пропусков; стиль — живой "
|
||||
"литературный русский. Выведи ТОЛЬКО перевод, без комментариев.")
|
||||
|
||||
|
||||
def glossary_block(poison=False):
|
||||
lines = []
|
||||
for zh, (dst, _) in ENTITIES.items():
|
||||
d = "господин Ли (важный чиновник)" if (poison and zh == "赵太爷") else dst
|
||||
lines.append(f"{zh} → {d}")
|
||||
return "ГЛОССАРИЙ (используй эти утверждённые переводы имён/терминов последовательно):\n" + "\n".join(lines)
|
||||
|
||||
|
||||
def demos_block(demos):
|
||||
return ("РАНЕЕ ПЕРЕВЕДЁННЫЕ ФРАГМЕНТЫ ЭТОЙ КНИГИ (держи ту же терминологию и стиль):\n"
|
||||
+ "\n".join(f"— {d}" for d in demos))
|
||||
|
||||
|
||||
def build_user(chunk, arm):
|
||||
parts = []
|
||||
if arm in ("B_glossary", "C_demos", "D_noise"):
|
||||
parts.append(glossary_block())
|
||||
if arm == "E_poison":
|
||||
parts.append(glossary_block(poison=True))
|
||||
if arm == "C_demos":
|
||||
parts.append(demos_block(DEMOS_RELEVANT))
|
||||
if arm == "D_noise":
|
||||
parts.append(demos_block(DEMOS_NOISE))
|
||||
parts.append("ФРАГМЕНТ ДЛЯ ПЕРЕВОДА:\n" + chunk)
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
def cjk_ratio(s):
|
||||
cjk = len(re.findall(r"[㐀-鿿]", s))
|
||||
return round(cjk / max(1, len(s)), 3)
|
||||
|
||||
|
||||
def score(output, present):
|
||||
"""Bank post-check: fraction of source-present entities whose canonical stem is in output."""
|
||||
matched = [zh for zh in present if re.search(ENTITIES[zh][1], output)]
|
||||
return round(len(matched) / max(1, len(present)), 3), matched
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--reps", type=int, default=3)
|
||||
ap.add_argument("--provider", default="deepseek")
|
||||
ap.add_argument("--providers", default=str(ROOT / "providers.json"))
|
||||
args = ap.parse_args()
|
||||
|
||||
prov = next(p for p in json.load(open(args.providers))["providers"] if p["name"] == args.provider)
|
||||
chunk = SRC.read_text(encoding="utf-8")[CHUNK_OFF:CHUNK_OFF + CHUNK_LEN]
|
||||
present = [zh for zh in ENTITIES if zh in chunk]
|
||||
print(f"provider={prov['model']} chunk={len(chunk)}ch present entities ({len(present)}): {present}\n")
|
||||
|
||||
arms = ["A_bare", "B_glossary", "C_demos", "D_noise", "E_poison"]
|
||||
results = {"provider": prov["model"], "present": present, "reps": args.reps, "arms": {}}
|
||||
print(f"{'arm':<12} {'canon-consist':<14} {'cjk-echo':<9} {'poison-followed':<16} note")
|
||||
print("-" * 78)
|
||||
for arm in arms:
|
||||
user = build_user(chunk, arm)
|
||||
recs = []
|
||||
for _ in range(args.reps):
|
||||
text, err, usage = rb.call_provider(prov, SYSTEM, user, timeout=180)
|
||||
if not text:
|
||||
recs.append({"err": err}); continue
|
||||
cons, matched = score(text, present)
|
||||
rec = {"consist": cons, "matched": matched, "cjk": cjk_ratio(text),
|
||||
"poison_followed": bool(re.search(r"господин Ли|\bЛи\b", text)) and not re.search(r"Чжао", text),
|
||||
"len": len(text), "sample": text[:0]}
|
||||
recs.append(rec)
|
||||
ok = [r for r in recs if "err" not in r]
|
||||
if not ok:
|
||||
print(f"{arm:<12} {'ALL-FAILED':<14} {'':<9} {'':<16} {[r['err'] for r in recs][:1]}")
|
||||
results["arms"][arm] = {"records": recs}; continue
|
||||
mc = round(sum(r["consist"] for r in ok) / len(ok), 3)
|
||||
me = round(sum(r["cjk"] for r in ok) / len(ok), 3)
|
||||
pf = sum(r["poison_followed"] for r in ok)
|
||||
echo = " ECHO!" if me > 0.15 else ""
|
||||
results["arms"][arm] = {"mean_consist": mc, "mean_cjk": me, "poison_followed": pf,
|
||||
"n_ok": len(ok), "records": recs}
|
||||
print(f"{arm:<12} {mc:<14} {me:<9} {str(pf)+'/'+str(len(ok)):<16} n={len(ok)}{echo}")
|
||||
|
||||
OUT.write_text(json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"\nsaved -> {OUT}")
|
||||
print("Reading: B−A = does the deterministic soft-glossary baseline help; C−B = does the\n"
|
||||
"learning layer (demonstrations) beat that baseline; D vs C = Power-of-Noise harm;\n"
|
||||
"E poison-followed>0 = over-trust (why the learning layer must NOT touch approved names).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
170
eval/adaptive_levers.py
Normal file
170
eval/adaptive_levers.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Детерминированные рычаги канон-консистентности — кривая L1→L3 на ТРУДНОМ чанке.
|
||||
|
||||
Follow-up к adaptive_incontext/adaptive_reask (по запросу владельца: «раз не идём в гибрид —
|
||||
долбить детерминированную сторону; замерить, сколько добивает каждый рычаг»). Лёгкий чанк
|
||||
насыщал глоссарий до 1.0 → рычаги не было видно. Здесь — ТРУДНЫЙ чанк (1600 знаков, ~11
|
||||
рекуррентных сущностей, плотно) + контролируемый recall-тест, где точный матч промахивается.
|
||||
|
||||
Три рычага (все ДЕТЕРМИНИРОВАННЫЕ, 0 ML, 0 VRAM):
|
||||
L1 post-check ЛЕММАТИЗИРУЮЩИЙ (pymorphy3), а не регэксп → убирает ложные флаги/undercount
|
||||
(мой промах 王胡→«Бородатого Вана» на регэкспе `Ван\b`). Меряем false-flag rate.
|
||||
L2 RECALL матчера: точный матч → +нормализация(trad→simp/NFKC) → +alias-граф → +sticky.
|
||||
Детерминированный (без LLM): сколько «должных» записей вообще инъектится.
|
||||
L3 post-check + точечный RE-ASK на РЕАЛЬНЫЕ промахи (verifier-gated correction) → прирост
|
||||
adherence на трудном чанке.
|
||||
|
||||
deepseek-v4-flash + grok-4.3. INDICATIVE (малый N). Usage: eval/.venv/bin/python eval/adaptive_levers.py
|
||||
Output: stdout + eval/data/adaptive_levers.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import json, re, sys, unicodedata
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import refusal_bench as rb
|
||||
import pymorphy3
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
SRC = ROOT / "data" / "samples" / "zh" / "luxun-ah-q-ch5-9.txt"
|
||||
OUT = ROOT / "data" / "adaptive_levers.json"
|
||||
MORPH = pymorphy3.MorphAnalyzer()
|
||||
|
||||
# Трудный чанк: 1600 знаков, ~11 рекуррентных сущностей (плотно → adherence проседает).
|
||||
HARD = SRC.read_text(encoding="utf-8")[6450:6450 + 1600]
|
||||
|
||||
# entity → (canon dst для глоссария, СТРОГИЙ регэксп [наивный post-check], лемма-ключ[и] для L1)
|
||||
ENT = {
|
||||
"阿Q": ("А-кью", r"А-кью\b", ["а-кью"]), # дефис → hyph-режим
|
||||
"未庄": ("Вэйчжуан", r"Вэйчжуан\b", ["вэйчжуан"]),
|
||||
"赵太爷": ("почтенный Чжао", r"Чжао\b", ["чжао"]),
|
||||
"王胡": ("Бородатый Ван", r"Ван\b", ["ван"]),
|
||||
"小D": ("Маленький Дэн", r"Дэн\b", ["дэн"]),
|
||||
"假洋鬼子": ("Поддельный заморский чёрт", r"заморский чёрт\b", ["заморский", "чёрт"]),
|
||||
"尼姑": ("монашка", r"монашка\b", ["монашка"]),
|
||||
"吴妈": ("У-ма", r"У-ма\b", ["у-ма"]),
|
||||
"邹七嫂": ("тётушка Цзоу Седьмая", r"Цзоу\b", ["цзоу"]),
|
||||
"秀才": ("сюцай", r"сюцай\b", ["сюцай"]),
|
||||
"钱": ("Цянь", r"Цянь\b", ["цянь"]),
|
||||
}
|
||||
HYPH = {"а-кью": r"А-?кью", "у-ма": r"У-?ма"} # дефисные translit — регэксп, не лемма
|
||||
|
||||
SYSTEM = ("Ты профессиональный литературный переводчик. Переведи фрагмент на русский язык. "
|
||||
"Сохрани все реплики и детали без пропусков; стиль — живой литературный русский. Выведи ТОЛЬКО перевод.")
|
||||
GLOSS = "ГЛОССАРИЙ (используй утверждённые переводы последовательно):\n" + \
|
||||
"\n".join(f"{z} → {v[0]}" for z, v in ENT.items())
|
||||
PRESENT = [z for z in ENT if z in HARD]
|
||||
|
||||
|
||||
# ---------- матчеры post-check (L1) ----------
|
||||
def match_strict(out, z):
|
||||
"""Наивный регэксп-post-check (как в adaptive_incontext) — ломается на склонении."""
|
||||
return bool(re.search(ENT[z][1], out))
|
||||
|
||||
def lemmas_of(out):
|
||||
return {MORPH.parse(w)[0].normal_form for w in re.findall(r"[А-Яа-яЁё]+", out)}
|
||||
|
||||
def match_lemma(out, z, lem_cache):
|
||||
"""L1: лемматизирующий post-check. Для дефисных translit — регэксп-фолбэк."""
|
||||
keys = ENT[z][2]
|
||||
if keys[0] in HYPH:
|
||||
return bool(re.search(HYPH[keys[0]], out))
|
||||
return all(k in lem_cache for k in keys)
|
||||
|
||||
|
||||
# ---------- матчер РЕКОЛА (L2), детерминированный ----------
|
||||
TRAD2SIMP = {"趙": "赵", "莊": "庄", "錢": "钱", "陳": "陈", "萬": "万", "舊": "旧", "臉": "脸"}
|
||||
def normalize(s):
|
||||
s = unicodedata.normalize("NFKC", s)
|
||||
return "".join(TRAD2SIMP.get(c, c) for c in s)
|
||||
|
||||
ALIASES = {"王胡": ["王癞胡", "癞胡"], "赵太爷": ["赵老太爷"]}
|
||||
|
||||
def recall_config(chunks, keys_present, *, norm=False, alias=False, sticky=False):
|
||||
"""Сколько записей из keys_present инъектится по цепочке чанков при данной конфигурации."""
|
||||
active_prev, injected_ever = set(), set()
|
||||
for ch in chunks:
|
||||
text = normalize(ch) if norm else ch
|
||||
hit = set()
|
||||
for z in keys_present:
|
||||
pats = [z] + (ALIASES.get(z, []) if alias else [])
|
||||
pats = [normalize(p) if norm else p for p in pats]
|
||||
if any(p in text for p in pats):
|
||||
hit.add(z)
|
||||
if sticky:
|
||||
hit |= (active_prev & set(keys_present)) # scene-inertia
|
||||
active_prev = hit
|
||||
injected_ever |= hit
|
||||
return injected_ever
|
||||
|
||||
|
||||
def translate(model, base, key_env, user):
|
||||
t, _, _ = rb.call_provider({"name": model, "base_url": base, "model": model,
|
||||
"api_key_env": key_env, "max_tokens": 8000,
|
||||
**({"temperature": 0} if "grok" in model else {})}, SYSTEM, user, timeout=200)
|
||||
return t or ""
|
||||
|
||||
|
||||
def main():
|
||||
res = {"present": PRESENT, "hard_chunk_len": len(HARD)}
|
||||
print(f"ТРУДНЫЙ чанк: {len(HARD)} знаков, present entities ({len(PRESENT)}): {PRESENT}\n")
|
||||
|
||||
# ===== L2: детерминированный RECALL-curve (без LLM) =====
|
||||
print("=== L2 · RECALL матчера (детерминированный, без LLM) ===")
|
||||
# 3 чанка, где точный матч промахивается: trad-вариант, alias, местоименный (sticky)
|
||||
recall_chunks = [
|
||||
"趙太爺同錢老爺在未莊很有威。王癞胡也常來。", # trad + alias 王癞胡(=王胡)
|
||||
"他慢慢站起来,看了看四周。", # местоименный — 王胡 из sticky
|
||||
]
|
||||
keys = ["赵太爷", "钱", "未庄", "王胡"]
|
||||
configs = [("точный", {}), ("+норм", {"norm": True}),
|
||||
("+alias", {"norm": True, "alias": True}),
|
||||
("+sticky", {"norm": True, "alias": True, "sticky": True})]
|
||||
l2 = {}
|
||||
for name, kw in configs:
|
||||
got = recall_config(recall_chunks, keys, **kw)
|
||||
l2[name] = sorted(got)
|
||||
print(f" {name:<9} recall {len(got)}/{len(keys)} = {len(got)/len(keys):.2f} инъектировано: {sorted(got)}")
|
||||
res["L2_recall"] = l2
|
||||
|
||||
# ===== L1 + L3: adherence + re-ask на трудном чанке (LLM) =====
|
||||
print("\n=== L1 (лемма-post-check) + L3 (re-ask) на трудном чанке ===")
|
||||
res["models"] = {}
|
||||
for model, base, kenv in [("deepseek-v4-flash", "https://api.deepseek.com/v1", "DEEPSEEK_API_KEY"),
|
||||
("grok-4.3", "https://api.x.ai/v1", "XAI_API_KEY")]:
|
||||
out = translate(model, base, kenv, GLOSS + "\n\nФРАГМЕНТ:\n" + HARD)
|
||||
if not out:
|
||||
print(f"\n[{model}] пустой ответ — пропуск"); continue
|
||||
lem = lemmas_of(out)
|
||||
strict = [z for z in PRESENT if match_strict(out, z)]
|
||||
lemma = [z for z in PRESENT if match_lemma(out, z, lem)]
|
||||
false_flags = [z for z in lemma if z not in strict] # L1 чинит именно их
|
||||
real_miss = [z for z in PRESENT if z not in lemma] # настоящие провалы adherence
|
||||
print(f"\n[{model}] строгий-регэксп {len(strict)}/{len(PRESENT)}={len(strict)/len(PRESENT):.2f}"
|
||||
f" | L1 лемма-post-check {len(lemma)}/{len(PRESENT)}={len(lemma)/len(PRESENT):.2f}")
|
||||
print(f" L1 убрал ЛОЖНЫХ флагов: {len(false_flags)} {false_flags}")
|
||||
print(f" РЕАЛЬНЫЕ промахи adherence (по лемме): {real_miss}")
|
||||
rec = {"strict": len(strict)/len(PRESENT), "lemma": len(lemma)/len(PRESENT),
|
||||
"false_flags": false_flags, "real_miss": real_miss}
|
||||
# L3: re-ask на реальные промахи
|
||||
if real_miss:
|
||||
terms = "; ".join(f"{z} = {ENT[z][0]}" for z in real_miss)
|
||||
out2 = translate(model, base, kenv, GLOSS + "\n\nФРАГМЕНТ:\n" + HARD +
|
||||
f"\n\nВАЖНО: обязательно используй утверждённые формы: {terms}. Перепиши перевод с ними.")
|
||||
lem2 = lemmas_of(out2)
|
||||
lemma2 = [z for z in PRESENT if match_lemma(out2, z, lem2)]
|
||||
print(f" L3 re-ask по {real_miss}: adherence {len(lemma)/len(PRESENT):.2f} → {len(lemma2)/len(PRESENT):.2f}")
|
||||
rec["after_reask"] = len(lemma2)/len(PRESENT)
|
||||
rec["still_missing"] = [z for z in PRESENT if z not in lemma2]
|
||||
else:
|
||||
print(" L3 re-ask не нужен (реальных промахов нет)")
|
||||
rec["after_reask"] = rec["lemma"]
|
||||
res["models"][model] = rec
|
||||
|
||||
OUT.write_text(json.dumps(res, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"\nsaved → {OUT}")
|
||||
print("\nЧитается: L2 — рекол точного матча растёт с норм/alias/sticky; L1 — сколько ложных\n"
|
||||
"флагов убирает лемматизация (риск research/13 §2); L3 — сколько adherence добивает re-ask.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
176
eval/adaptive_probe.py
Normal file
176
eval/adaptive_probe.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Constraint probe for the ADAPTIVE-MEMORY research (docs/research/14-adaptive-memory.md).
|
||||
|
||||
Question it answers empirically (mandate: «проверяй заблокированность методов пробой,
|
||||
а не по памяти» — ADAPTIVE_MEMORY_RESEARCH_PROMPT §Жёсткий контур п.1): what LOGIT-LEVEL
|
||||
control do our closed flagship providers actually expose in 2026? This bounds which
|
||||
"learns-during-the-book" mechanisms are runnable on the flagship TRANSLATOR:
|
||||
|
||||
logit-level lever needs from the API available on flagship?
|
||||
------------------------------------------------------------------------------------
|
||||
kNN-MT (Khandelwal 2021) DECODER HIDDEN STATES per step NO API exposes this
|
||||
constrained/beam decoding beam access / token masking logit_bias at best
|
||||
output-distribution reading per-token logprobs <- THIS we probe live
|
||||
hard steering logit_bias honored <- THIS we probe live
|
||||
|
||||
So even the BEST case (logprobs + logit_bias both honored) does NOT unblock kNN-MT: the
|
||||
datastore key in kNN-MT is the decoder's context representation, which none of these APIs
|
||||
return. This probe measures the ceiling (logprobs/logit_bias), and the ceiling is still
|
||||
below what kNN-MT needs — a doubly-closed door. Local decoders (llama.cpp/vLLM) DO expose
|
||||
logprobs/logits by design (n_probs / prompt_logprobs), so local kNN-MT is physically
|
||||
possible; that half is asserted from the runtimes' documented APIs, not reprobed here (the
|
||||
stand GPU is held by the polygon's resident model — do not evict, exp03/§coordination).
|
||||
|
||||
Cheap by design: ~1 short call per provider. Reuses eval/.env + eval/providers.json quirks.
|
||||
Usage: eval/.venv/bin/python eval/adaptive_probe.py
|
||||
Output: stdout table + eval/data/adaptive_probe.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
OUT = ROOT / "data" / "adaptive_probe.json"
|
||||
|
||||
|
||||
def load_env() -> None:
|
||||
for cand in (ROOT / ".env", Path("~/.textmachine.env").expanduser()):
|
||||
if not cand.exists():
|
||||
continue
|
||||
for line in cand.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
k, _, v = line.partition("=")
|
||||
if v.strip():
|
||||
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
|
||||
return
|
||||
|
||||
|
||||
load_env()
|
||||
|
||||
# The models we actually probe. Includes the REAL translator/editor stack models
|
||||
# (deepseek-v4-flash draft, grok-4.3 editor, gemini-3.1-pro premium) plus a clean
|
||||
# non-reasoning path per family to give logprobs its best chance to appear.
|
||||
PROBES = [
|
||||
{"name": "deepseek-v4-flash", "base": "https://api.deepseek.com/v1",
|
||||
"model": "deepseek-v4-flash", "key": "DEEPSEEK_API_KEY", "max": 200},
|
||||
{"name": "grok-non-reasoning", "base": "https://api.x.ai/v1",
|
||||
"model": "grok-4.20-0309-non-reasoning", "key": "XAI_API_KEY", "max": 32},
|
||||
{"name": "grok-4.3(editor)", "base": "https://api.x.ai/v1",
|
||||
"model": "grok-4.3", "key": "XAI_API_KEY", "max": 200},
|
||||
{"name": "glm-4.5-air", "base": "https://api.z.ai/api/paas/v4",
|
||||
"model": "glm-4.5-air", "key": "ZAI_API_KEY", "max": 64,
|
||||
"extra": {"thinking": {"type": "disabled"}}},
|
||||
{"name": "gemini-2.5-flash", "base": "https://generativelanguage.googleapis.com/v1beta/openai",
|
||||
"model": "gemini-2.5-flash", "key": "GEMINI_API_KEY", "max": 64,
|
||||
"extra": {"extra_body": {"google": {"thinking_config": {"thinking_budget": 0}}}}},
|
||||
{"name": "gemini-3.1-pro(premium)", "base": "https://generativelanguage.googleapis.com/v1beta/openai",
|
||||
"model": "gemini-3.1-pro-preview", "key": "GEMINI_API_KEY", "max": 8000},
|
||||
{"name": "gpt-5-mini", "base": "https://api.openai.com/v1",
|
||||
"model": "gpt-5-mini", "key": "OPENAI_API_KEY", "max": 8000, "reasoning": True},
|
||||
]
|
||||
|
||||
|
||||
def one_call(p: dict, with_logprobs: bool, with_logit_bias: bool):
|
||||
"""Return dict describing what the provider did with logprobs / logit_bias."""
|
||||
key = os.environ.get(p["key"], "")
|
||||
if not key:
|
||||
return {"skip": f"no key {p['key']}"}
|
||||
body = {
|
||||
"model": p["model"],
|
||||
"messages": [{"role": "user", "content": "Reply with exactly the single word: ok"}],
|
||||
"max_tokens": p["max"],
|
||||
}
|
||||
if p.get("reasoning"):
|
||||
body["max_completion_tokens"] = body.pop("max_tokens")
|
||||
body["reasoning_effort"] = "minimal"
|
||||
else:
|
||||
body["temperature"] = 0.0
|
||||
body.update(p.get("extra", {}))
|
||||
if with_logprobs:
|
||||
body["logprobs"] = True
|
||||
body["top_logprobs"] = 5
|
||||
if with_logit_bias:
|
||||
# bias an arbitrary token id hard; we only check acceptance, not effect.
|
||||
body["logit_bias"] = {"1734": -100}
|
||||
req = urllib.request.Request(
|
||||
p["base"].rstrip("/") + "/chat/completions",
|
||||
data=json.dumps(body).encode(),
|
||||
headers={"Content-Type": "application/json", "Authorization": f"Bearer {key}"},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=120) as resp:
|
||||
data = json.loads(resp.read())
|
||||
except urllib.error.HTTPError as e:
|
||||
return {"http_error": e.code, "detail": e.read().decode(errors="replace")[:300].replace("\n", " ")}
|
||||
except Exception as e:
|
||||
return {"transport_error": f"{type(e).__name__}: {e}"}
|
||||
ch = (data.get("choices") or [{}])[0]
|
||||
lp = ch.get("logprobs")
|
||||
# logprobs "present" = non-null object carrying per-token content entries
|
||||
has_lp = bool(lp) and bool((lp or {}).get("content"))
|
||||
sample = None
|
||||
if has_lp:
|
||||
first = lp["content"][0]
|
||||
sample = {"token": first.get("token"),
|
||||
"logprob": round(first.get("logprob", 0.0), 3),
|
||||
"n_top": len(first.get("top_logprobs", []))}
|
||||
return {
|
||||
"http": 200,
|
||||
"content": (ch.get("message", {}).get("content") or "")[:40],
|
||||
"finish": ch.get("finish_reason"),
|
||||
"logprobs_present": has_lp,
|
||||
"logprobs_sample": sample,
|
||||
"usage": {k: data.get("usage", {}).get(k) for k in ("prompt_tokens", "completion_tokens")},
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
results = []
|
||||
print(f"{'provider':<26} {'logprobs?':<11} {'logit_bias':<12} sample / note")
|
||||
print("-" * 88)
|
||||
for p in PROBES:
|
||||
r_lp = one_call(p, with_logprobs=True, with_logit_bias=False)
|
||||
r_lb = one_call(p, with_logprobs=False, with_logit_bias=True)
|
||||
rec = {"provider": p["name"], "model": p["model"], "logprobs": r_lp, "logit_bias": r_lb}
|
||||
results.append(rec)
|
||||
|
||||
if "skip" in r_lp:
|
||||
print(f"{p['name']:<26} {'SKIP':<11} {'':<12} {r_lp['skip']}")
|
||||
continue
|
||||
if r_lp.get("http") == 200:
|
||||
lp_verdict = "YES" if r_lp["logprobs_present"] else "no(200)"
|
||||
elif "http_error" in r_lp:
|
||||
lp_verdict = f"HTTP{r_lp['http_error']}"
|
||||
else:
|
||||
lp_verdict = "ERR"
|
||||
# logit_bias: 200 = accepted (not rejected); HTTP4xx = rejected
|
||||
if r_lb.get("http") == 200:
|
||||
lb_verdict = "accepted"
|
||||
elif "http_error" in r_lb:
|
||||
lb_verdict = f"HTTP{r_lb['http_error']}"
|
||||
else:
|
||||
lb_verdict = "ERR"
|
||||
note = ""
|
||||
if r_lp.get("logprobs_sample"):
|
||||
s = r_lp["logprobs_sample"]
|
||||
note = f"tok={s['token']!r} lp={s['logprob']} top{s['n_top']}"
|
||||
elif "detail" in r_lp:
|
||||
note = r_lp["detail"][:60]
|
||||
elif r_lp.get("http") == 200 and not r_lp["logprobs_present"]:
|
||||
note = f"finish={r_lp.get('finish')} content={r_lp.get('content')!r}"
|
||||
print(f"{p['name']:<26} {lp_verdict:<11} {lb_verdict:<12} {note}")
|
||||
|
||||
OUT.write_text(json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"\nsaved -> {OUT}")
|
||||
print("\nReading: 'logprobs YES' = output distribution readable (still NOT decoder hidden\n"
|
||||
"states → kNN-MT still blocked). 'logit_bias accepted' = crude token steering only.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
86
eval/adaptive_reask.py
Normal file
86
eval/adaptive_reask.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Диагностика + демонстрация ДЕТЕРМИНИРОВАННОГО рычага коэффициента канон-консистентности
|
||||
(follow-up к adaptive_incontext.py по запросу владельца: «раз не идём в гибрид — долбить
|
||||
детерминированную сторону»).
|
||||
|
||||
Два вопроса:
|
||||
1. ДИАГНОЗ: промахи в arm B_glossary (напр. 王胡) — это РЕАЛЬНЫЙ провал adherence
|
||||
(модель не использовала утв. форму) или АРТЕФАКТ моего accept-регэкспа (напр. `Ван\b`
|
||||
не ловит склонённое «Вана/Вану»)? Печатаем реальный вывод + два матча (строгий \b vs
|
||||
склонение-толерантный стем).
|
||||
2. РЫЧАГ: post-check находит промах → точечный re-ask («используй утв. формы X,Y») →
|
||||
повторный перевод → коэффициент до/после. Это verifier-gated single-pass correction
|
||||
(research/14 §6, M2) — тот самый детерминированный рычаг, а не дообучение.
|
||||
|
||||
deepseek-v4-flash, тот же чанк 阿Q. INDICATIVE (малый N). Usage: eval/.venv/bin/python eval/adaptive_reask.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import json, re, sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import refusal_bench as rb
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
SRC = ROOT / "data" / "samples" / "zh" / "luxun-ah-q-ch5-9.txt"
|
||||
CHUNK = SRC.read_text(encoding="utf-8")[6360:6360 + 650]
|
||||
|
||||
# canonical dst + СТРОГИЙ (\b, как в adaptive_incontext) и СКЛОНЕНИЕ-ТОЛЕРАНТНЫЙ стем-матч
|
||||
ENT = {
|
||||
"阿Q": ("А-кью", r"А[-\s]?кью", r"А[-\s]?кью"),
|
||||
"未庄": ("Вэйчжуан", r"Вэйчжуан", r"Вэйчжуан"),
|
||||
"赵太爷": ("почтенный Чжао", r"Чжао", r"Чжао"),
|
||||
"王胡": ("Бородатый Ван", r"Ван\b", r"Ван[аоуые]?\b|Бород"),
|
||||
"小D": ("Маленький Дэн", r"Дэн", r"Дэн"),
|
||||
"假洋鬼子": ("Поддельный заморский чёрт", r"заморск\w+ чёрт|поддельн", r"заморск|поддельн|чёрт"),
|
||||
"吴妈": ("У-ма", r"У[-\s]?ма", r"У[-\s]?ма"),
|
||||
"邹七嫂": ("тётушка Цзоу Седьмая", r"Цзоу", r"Цзоу"),
|
||||
}
|
||||
SYSTEM = ("Ты профессиональный литературный переводчик. Переведи фрагмент на русский язык. "
|
||||
"Сохрани все реплики и детали; стиль — живой литературный русский. Выведи ТОЛЬКО перевод.")
|
||||
GLOSS = "ГЛОССАРИЙ (используй эти утверждённые переводы последовательно):\n" + \
|
||||
"\n".join(f"{z} → {v[0]}" for z, v in ENT.items())
|
||||
PRESENT = [z for z in ENT if z in CHUNK]
|
||||
|
||||
|
||||
def score(out, strict):
|
||||
idx = 1 if strict else 2
|
||||
return [z for z in PRESENT if re.search(ENT[z][idx], out)]
|
||||
|
||||
|
||||
def translate(user):
|
||||
t, err, _ = rb.call_provider({"name": "deepseek", "base_url": "https://api.deepseek.com/v1",
|
||||
"model": "deepseek-v4-flash", "api_key_env": "DEEPSEEK_API_KEY",
|
||||
"max_tokens": 8000}, SYSTEM, user, timeout=180)
|
||||
return t or ""
|
||||
|
||||
|
||||
def main():
|
||||
print(f"present entities ({len(PRESENT)}): {PRESENT}\n")
|
||||
for rep in range(2):
|
||||
print(f"================= REP {rep+1} =================")
|
||||
out = translate(GLOSS + "\n\nФРАГМЕНТ:\n" + CHUNK)
|
||||
strict, tol = score(out, True), score(out, False)
|
||||
print(f"[B_glossary] строгий-матч {len(strict)}/8={len(strict)/8:.3f} | склонение-толерантный {len(tol)}/8={len(tol)/8:.3f}")
|
||||
print(f" промах(строгий): {[z for z in PRESENT if z not in strict]}")
|
||||
print(f" промах(толерантный, = РЕАЛЬНЫЕ провалы adherence): {[z for z in PRESENT if z not in tol]}")
|
||||
# показать фактический рендер спорных сущностей
|
||||
for z in ["王胡", "假洋鬼子", "吴妈"]:
|
||||
can = ENT[z][0]
|
||||
frag = re.findall(r".{0,4}(?:Ван|Бород|заморск|поддельн|У-ма|У ма|Ху|Хо)\w*.{0,6}", out)
|
||||
print(f" {z}→канон «{can}»: фрагменты вывода {frag[:3]}")
|
||||
# РЫЧАГ: re-ask по реальным промахам
|
||||
miss = [z for z in PRESENT if z not in tol]
|
||||
if miss:
|
||||
terms = "; ".join(f"{z} = {ENT[z][0]}" for z in miss)
|
||||
reask = (GLOSS + "\n\nФРАГМЕНТ:\n" + CHUNK +
|
||||
f"\n\nВАЖНО: в переводе ОБЯЗАТЕЛЬНО используй утверждённые формы: {terms}. Перепиши перевод, применив их.")
|
||||
out2 = translate(reask)
|
||||
tol2 = score(out2, False)
|
||||
print(f" → post-check re-ask по {miss}: {len(tol)}/8 → {len(tol2)}/8 (толерантный)")
|
||||
else:
|
||||
print(" → промахов (толерантных) нет — re-ask не нужен")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Reference in a new issue