Seed-ul Haiku eticheta sistematic "INLOCUIT ANVELOPE ..." cu OE-1 (223 vs 17
OE-8), desi RAR are cod dedicat OE-8 (inlocuire sezoniera anvelope), asa ca
votul k-NN propunea OE-1 pentru orice inlocuire de anvelope.
- tools/mapare-llm/fix_anvelope_oe8.py: re-etichetare deterministica (187
randuri DB + seed JSON -> OE-8; regula exclude singular cu pozitie, D/R,
janta, vulcanizare) + 13 intrari canonice curate manual pentru formularile
sezoniere absente din corpus ("MONTAT CAUCIUCURI VARA" etc.); idempotent
- enrich_suggestions: ponderea votului = EMB_VOTE_DECAY^rang (0.7) in loc de
similaritatea bruta — anizotropia modelului lasa zgomotul de coada (ex.
INLOCUIT BECURII la 0.934 de INLOCUIRE ANVELOPE) sa invinga vecinii corecti
de rang 1-2; LOO: precizie egala (93.1%), cod-gresit 4.67% -> 4.63%
- test_fix_anvelope_oe8: gardii pe regula + seed-ul din repo (regresie la
regenerare); auditul k-NN al dezacordurilor nu a gasit alte erori
sistematice (curatat=OE-2, reglat=OE-4, bujii/ulei=OE-3 raman apararabile)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
166 lines
6.3 KiB
Python
166 lines
6.3 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
|
|
vote<k> — vot ponderat pe rang (EMB_VOTE_DECAY^rang) pe top-k vecini peste
|
|
prag; NUL e eticheta proprie (castiga -> supresie). Politica
|
|
livrata in enrich_suggestions. Ponderarea cu similaritatea bruta
|
|
a fost respinsa: anizotropia modelului lasa zgomotul de coada
|
|
(sim ~0.93 pe vecini fara legatura) sa invinga rangurile 1-2.
|
|
|
|
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 pe rang (decay^rang) pe vecinii peste prag — politica din enrich."""
|
|
from app.mapping import EMB_VOTE_DECAY
|
|
scoruri: dict[str, float] = {}
|
|
for rang, (lab, sim) in enumerate(zip(nbr_labels, nbr_sims), 1):
|
|
if sim < prag:
|
|
continue
|
|
scoruri[lab] = scoruri.get(lab, 0.0) + EMB_VOTE_DECAY ** rang
|
|
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()
|