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>
This commit is contained in:
@@ -631,10 +631,17 @@ def delete_text_rule(conn, account_id: int | None, pattern: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
# Prag minim de similaritate cosine pentru sugestia din embeddings NN.
|
||||
# Sub acest scor, sugestia NN e prea incerta si nu o afisam (previne recomandari
|
||||
# irelevante cand corpus-ul e mic sau neindexat corect).
|
||||
EMB_MIN_SIMILARITATE = 0.5
|
||||
# Prag minim de similaritate cosine pentru un vecin k-NN luat in calcul la vot.
|
||||
# Calibrat prin leave-one-out pe corpusul SILVER (tools/mapare-llm/knn_calibrate.py):
|
||||
# modelul e anizotrop (perechi nelegate scoreaza 0.79-0.87), deci sub ~0.88 vecinul
|
||||
# e zgomot. La 0.88: coverage 83%, precizie 93%; la 0.5 (vechiul prag): coverage
|
||||
# 100% dar 7.4% cod gresit preselectat.
|
||||
EMB_MIN_SIMILARITATE = 0.88
|
||||
|
||||
# Cati vecini intra in votul ponderat cu similaritatea. Vot > top-1: corpusul SILVER
|
||||
# are etichete contradictorii pe denumiri aproape identice; votul e imun la ele
|
||||
# (+~1pp precizie la acelasi coverage, masurat LOO).
|
||||
EMB_VOTE_TOP_K = 5
|
||||
|
||||
# Protejeaza secventa hash->load->embed->save->purge->index (embedding_cache) de
|
||||
# executie concurenta intre warmup-ul de fundal (block=True) si calea de request
|
||||
@@ -769,7 +776,8 @@ def enrich_suggestions(
|
||||
Returneaza:
|
||||
{
|
||||
'sugestie_principala': {'cod_prestatie': str, 'sursa': str} | None,
|
||||
'surse': {'gold_partajat': str|None, 'silver': str|None, 'embedding': str|None, 'nul': bool}
|
||||
'surse': {'gold_partajat': str|None, 'silver': str|None, 'embedding': str|None,
|
||||
'embedding_similaritate': float|None, 'nul': bool}
|
||||
}
|
||||
|
||||
INVARIANTE:
|
||||
@@ -783,7 +791,10 @@ def enrich_suggestions(
|
||||
importa normalize_for_match din mapping).
|
||||
"""
|
||||
sugestie_principala: dict | None = None
|
||||
surse: dict = {"gold_partajat": None, "silver": None, "embedding": None, "nul": False}
|
||||
surse: dict = {
|
||||
"gold_partajat": None, "silver": None,
|
||||
"embedding": None, "embedding_similaritate": None, "nul": False,
|
||||
}
|
||||
|
||||
if not denumire:
|
||||
return {"sugestie_principala": sugestie_principala, "surse": surse}
|
||||
@@ -829,15 +840,28 @@ def enrich_suggestions(
|
||||
# Corpusul k-NN e text NORMALIZAT (denumire_normalizata),
|
||||
# deci query-ul TREBUIE normalizat la fel — altfel cosine degradeaza si
|
||||
# nu mai e configul sub care s-a masurat 94.3%.
|
||||
nn = _emb.suggest_nearest(normalize_for_match(denumire), top_k=1)
|
||||
# Prag minim: similaritate prea mica = sugestie inutila.
|
||||
# Evita recomandari irelevante cand corpus-ul e mic/partial.
|
||||
if nn and nn[0].get("similaritate", 0) >= EMB_MIN_SIMILARITATE:
|
||||
if nn[0].get("is_nul"):
|
||||
# Vecin NUL (non-operatie) = semnal de SUPRESIE, nu cod.
|
||||
nn = _emb.suggest_nearest(normalize_for_match(denumire), top_k=EMB_VOTE_TOP_K)
|
||||
# Vot ponderat cu similaritatea pe vecinii peste prag; NUL e eticheta
|
||||
# proprie (castiga -> supresie, nu cod). Vecinii sub prag nu voteaza.
|
||||
scoruri: dict[str, float] = {}
|
||||
sim_max: dict[str, float] = {}
|
||||
for v in nn:
|
||||
sim = float(v.get("similaritate", 0))
|
||||
if sim < EMB_MIN_SIMILARITATE:
|
||||
continue
|
||||
lab = "NUL" if v.get("is_nul") else (str(v["cod"]) if v.get("cod") else None)
|
||||
if lab is None:
|
||||
continue
|
||||
scoruri[lab] = scoruri.get(lab, 0.0) + sim
|
||||
sim_max[lab] = max(sim_max.get(lab, 0.0), sim)
|
||||
if scoruri:
|
||||
castigator = max(scoruri, key=lambda k: scoruri[k])
|
||||
if castigator == "NUL":
|
||||
# Vecinatate NUL (non-operatie) = semnal de SUPRESIE, nu cod.
|
||||
surse["nul"] = True
|
||||
elif nn[0].get("cod"):
|
||||
surse["embedding"] = str(nn[0]["cod"])
|
||||
else:
|
||||
surse["embedding"] = castigator
|
||||
surse["embedding_similaritate"] = sim_max[castigator]
|
||||
except Exception:
|
||||
pass # degradare gratioasa (#16b): motorul absent nu blocheaza
|
||||
|
||||
|
||||
Reference in New Issue
Block a user