Files
rar-autopass/tools/mapare-llm/knn_calibrate.py
Claude Agent 809823a8ae perf+calitate sugestii k-NN: matvec numpy, vot top-5 cu prag calibrat, indicatori import
- embeddings: corpus ca matrice numpy cu norme precalculate; suggest_nearest
  = un matvec (~0.6ms/query fata de ~500ms cosine pur-Python la 17k vectori)
- enrich_suggestions: vot ponderat cu similaritatea pe top-5 vecini (NUL =
  eticheta proprie); prag 0.5 -> 0.88, calibrat LOO pe corpusul SILVER
  (tools/mapare-llm/knn_calibrate.py): precizie 90.5% -> 93.1%, cod gresit
  preselectat 7.2% -> 4.7%; sub prag abtinere -> preselectie fuzzy
- UI: codul sugerat de sistem afisat explicit cu sursa si scorul, separat de
  lista fuzzy ("potrivire text"); indicator de progres reparat pe upload
  (display:inline anula .htmx-indicator) si adaugat pe pasii 2->3 si
  "Salveaza maparile"

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 10:10:58 +00:00

162 lines
6.0 KiB
Python

"""Calibrare politica k-NN pentru sugestia embeddings (top-1 vs vot ponderat top-k).
Metodologie: leave-one-out pe corpusul SILVER (`mapping_suggestions`) folosind
vectorii persistati in `embedding_cache` (zero re-embedding). Pentru fiecare
exemplu: se exclude din corpus, se prezice din vecinii ramasi, se compara cu
eticheta lui SILVER (cod sau NUL).
ATENTIE la interpretare: SILVER e etichetat de LLM (~95% calitate la scara),
NU ground-truth uman — valorile absolute sunt optimiste; comparatia RELATIVA
intre politici si calibrarea pragului raman valide (aceeasi tinta pentru toate).
Politici evaluate:
top1 — eticheta primului vecin (comportamentul curent din enrich_suggestions)
vote<k> — vot ponderat cu similaritatea pe top-k vecini peste prag;
NUL e eticheta proprie (castiga -> supresie)
Metrice (per politica x prag):
coverage — % din exemple pentru care se emite o predictie (cod sau NUL)
precizie — % predictii corecte din cele emise
cod-gresit — % din TOATE exemplele unde s-a emis un COD diferit de gold
(critic: cod gresit preselectat = risc FINALIZATA eronata)
Rulare:
python3 tools/mapare-llm/knn_calibrate.py --db data/autopass.db
python3 tools/mapare-llm/knn_calibrate.py --db data/autopass.db --sample 5000 --seed 42
"""
from __future__ import annotations
import argparse
import os
import random
import sqlite3
import sys
import numpy as np
_HERE = os.path.dirname(os.path.abspath(__file__))
_ROOT = os.path.abspath(os.path.join(_HERE, "..", ".."))
if _ROOT not in sys.path:
sys.path.insert(0, _ROOT)
from app import embedding_cache as cache # noqa: E402
from app.embeddings import FASTEMBED_MODEL # noqa: E402
NUL = "NUL"
def load_corpus(db_path: str):
"""Incarca (etichete, matrice vectori) aliniate pozitional din SQLite."""
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
rows = conn.execute(
"SELECT denumire_normalizata, cod_prestatie, is_nul FROM mapping_suggestions "
"WHERE denumire_normalizata IS NOT NULL"
).fetchall()
vecmap = {
h: np.frombuffer(b, dtype=np.float32)
for h, b in conn.execute(
"SELECT text_hash, vector FROM embedding_cache WHERE model=?", (FASTEMBED_MODEL,)
)
}
labels: list[str] = []
vecs: list[np.ndarray] = []
lipsa = 0
for r in rows:
v = vecmap.get(cache.text_hash(r["denumire_normalizata"]))
if v is None:
lipsa += 1
continue
labels.append(NUL if r["is_nul"] else str(r["cod_prestatie"]))
vecs.append(v)
if lipsa:
print(f"AVERTISMENT: {lipsa} exemple fara vector in cache (excluse)")
matrix = np.vstack(vecs)
norms = np.linalg.norm(matrix, axis=1)
norms[norms == 0.0] = 1.0
return labels, matrix / norms[:, None]
def predict_top1(nbr_labels: list[str], nbr_sims: np.ndarray, prag: float) -> str | None:
"""Politica curenta: eticheta celui mai apropiat vecin, daca trece pragul."""
if nbr_sims[0] < prag:
return None
return nbr_labels[0]
def predict_vote(nbr_labels: list[str], nbr_sims: np.ndarray, prag: float) -> str | None:
"""Vot ponderat cu similaritatea pe vecinii peste prag."""
scoruri: dict[str, float] = {}
for lab, sim in zip(nbr_labels, nbr_sims):
if sim < prag:
continue
scoruri[lab] = scoruri.get(lab, 0.0) + float(sim)
if not scoruri:
return None
return max(scoruri, key=lambda k: scoruri[k])
def evalueaza(labels, unit, indices, k_max, politici, praguri):
"""LOO: pentru fiecare index, top-k_max vecini (fara el insusi), apoi
aplica fiecare (politica, prag) pe aceiasi vecini. Intoarce metrice agregate."""
stats = {(p, prag): {"emis": 0, "corect": 0, "cod_gresit": 0} for p in politici for prag in praguri}
n = len(indices)
for count, i in enumerate(indices, 1):
sims = unit @ unit[i]
sims[i] = -1.0 # exclude exemplul insusi (leave-one-out)
top = np.argpartition(-sims, k_max)[:k_max]
top = top[np.argsort(-sims[top])]
nbr_labels = [labels[j] for j in top]
nbr_sims = sims[top]
gold = labels[i]
for (pname, prag), st in stats.items():
pred = politici[pname](nbr_labels, nbr_sims, prag)
if pred is None:
continue
st["emis"] += 1
if pred == gold:
st["corect"] += 1
elif pred != NUL:
st["cod_gresit"] += 1 # a emis un COD gresit (sau cod in loc de NUL)
if count % 2000 == 0:
print(f" ...{count}/{n}", file=sys.stderr)
return stats, n
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--db", default=os.path.join(_ROOT, "data", "autopass.db"))
ap.add_argument("--sample", type=int, default=0, help="LOO doar pe un esantion (0 = tot corpusul)")
ap.add_argument("--seed", type=int, default=42)
ap.add_argument("--k", type=int, default=5, help="k pentru politica de vot")
args = ap.parse_args()
labels, unit = load_corpus(args.db)
print(f"corpus: {len(labels)} exemple ({sum(1 for l in labels if l == NUL)} NUL)")
indices = list(range(len(labels)))
if args.sample and args.sample < len(indices):
random.Random(args.seed).shuffle(indices)
indices = indices[: args.sample]
print(f"evaluare LOO pe {len(indices)} exemple, k={args.k}")
politici = {
"top1": predict_top1,
f"vote{args.k}": predict_vote,
}
praguri = [0.5, 0.7, 0.8, 0.85, 0.88, 0.9, 0.92, 0.95]
stats, n = evalueaza(labels, unit, indices, args.k, politici, praguri)
print(f"\n{'politica':<8} {'prag':>5} {'coverage':>9} {'precizie':>9} {'cod-gresit':>11}")
for pname in politici:
for prag in praguri:
st = stats[(pname, prag)]
cov = st["emis"] / n
prec = (st["corect"] / st["emis"]) if st["emis"] else 0.0
wc = st["cod_gresit"] / n
print(f"{pname:<8} {prag:>5.2f} {cov:>8.1%} {prec:>8.1%} {wc:>10.2%}")
if __name__ == "__main__":
main()