fix(sugestii): inlocuirea de anvelope -> OE-8 + vot k-NN ponderat pe rang
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>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -638,11 +638,18 @@ def delete_text_rule(conn, account_id: int | None, pattern: str) -> None:
|
|||||||
# 100% dar 7.4% cod gresit preselectat.
|
# 100% dar 7.4% cod gresit preselectat.
|
||||||
EMB_MIN_SIMILARITATE = 0.88
|
EMB_MIN_SIMILARITATE = 0.88
|
||||||
|
|
||||||
# Cati vecini intra in votul ponderat cu similaritatea. Vot > top-1: corpusul SILVER
|
# Cati vecini intra in votul ponderat pe rang. Vot > top-1: corpusul SILVER
|
||||||
# are etichete contradictorii pe denumiri aproape identice; votul e imun la ele
|
# are etichete contradictorii pe denumiri aproape identice; votul e imun la ele
|
||||||
# (+~1pp precizie la acelasi coverage, masurat LOO).
|
# (+~1pp precizie la acelasi coverage, masurat LOO).
|
||||||
EMB_VOTE_TOP_K = 5
|
EMB_VOTE_TOP_K = 5
|
||||||
|
|
||||||
|
# Ponderea unui vecin = EMB_VOTE_DECAY^rang (1-indexat). Ponderarea cu similaritatea
|
||||||
|
# bruta e pacalita de anizotropia modelului: vecini fara legatura scoreaza aproape
|
||||||
|
# cat cei buni (ex. INLOCUIT BECURII la 0.934 de INLOCUIRE ANVELOPE), deci coada
|
||||||
|
# de zgomot poate invinge 2 vecini corecti de rang 1-2. Ordinea rangurilor ramane
|
||||||
|
# informativa; decay 0.7 = cel mai mic cod-gresit la precizie egala (LOO).
|
||||||
|
EMB_VOTE_DECAY = 0.7
|
||||||
|
|
||||||
# Protejeaza secventa hash->load->embed->save->purge->index (embedding_cache) de
|
# Protejeaza secventa hash->load->embed->save->purge->index (embedding_cache) de
|
||||||
# executie concurenta intre warmup-ul de fundal (block=True) si calea de request
|
# executie concurenta intre warmup-ul de fundal (block=True) si calea de request
|
||||||
# (block=False, dupa ce modelul e deja incarcat) -- altfel purjarea uneia ar sterge
|
# (block=False, dupa ce modelul e deja incarcat) -- altfel purjarea uneia ar sterge
|
||||||
@@ -841,18 +848,18 @@ def enrich_suggestions(
|
|||||||
# deci query-ul TREBUIE normalizat la fel — altfel cosine degradeaza si
|
# deci query-ul TREBUIE normalizat la fel — altfel cosine degradeaza si
|
||||||
# nu mai e configul sub care s-a masurat 94.3%.
|
# nu mai e configul sub care s-a masurat 94.3%.
|
||||||
nn = _emb.suggest_nearest(normalize_for_match(denumire), top_k=EMB_VOTE_TOP_K)
|
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
|
# Vot ponderat pe rang (decay^rang) pe vecinii peste prag; NUL e eticheta
|
||||||
# proprie (castiga -> supresie, nu cod). Vecinii sub prag nu voteaza.
|
# proprie (castiga -> supresie, nu cod). Vecinii sub prag nu voteaza.
|
||||||
scoruri: dict[str, float] = {}
|
scoruri: dict[str, float] = {}
|
||||||
sim_max: dict[str, float] = {}
|
sim_max: dict[str, float] = {}
|
||||||
for v in nn:
|
for rang, v in enumerate(nn, 1):
|
||||||
sim = float(v.get("similaritate", 0))
|
sim = float(v.get("similaritate", 0))
|
||||||
if sim < EMB_MIN_SIMILARITATE:
|
if sim < EMB_MIN_SIMILARITATE:
|
||||||
continue
|
continue
|
||||||
lab = "NUL" if v.get("is_nul") else (str(v["cod"]) if v.get("cod") else None)
|
lab = "NUL" if v.get("is_nul") else (str(v["cod"]) if v.get("cod") else None)
|
||||||
if lab is None:
|
if lab is None:
|
||||||
continue
|
continue
|
||||||
scoruri[lab] = scoruri.get(lab, 0.0) + sim
|
scoruri[lab] = scoruri.get(lab, 0.0) + EMB_VOTE_DECAY ** rang
|
||||||
sim_max[lab] = max(sim_max.get(lab, 0.0), sim)
|
sim_max[lab] = max(sim_max.get(lab, 0.0), sim)
|
||||||
if scoruri:
|
if scoruri:
|
||||||
castigator = max(scoruri, key=lambda k: scoruri[k])
|
castigator = max(scoruri, key=lambda k: scoruri[k])
|
||||||
|
|||||||
58
tests/test_fix_anvelope_oe8.py
Normal file
58
tests/test_fix_anvelope_oe8.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
"""Gardii pentru corectia anvelope->OE-8 (tools/mapare-llm/fix_anvelope_oe8.py).
|
||||||
|
|
||||||
|
Doua niveluri:
|
||||||
|
1. Regula `este_inlocuire_anvelope` pe cazuri pozitive/negative.
|
||||||
|
2. Seed-ul real din repo NU mai contine inlocuiri de anvelope etichetate
|
||||||
|
altfel decat OE-8 (regresie la o viitoare regenerare a seed-ului).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
TOOLS_DIR = os.path.abspath(os.path.join(HERE, "..", "tools", "mapare-llm"))
|
||||||
|
if TOOLS_DIR not in sys.path:
|
||||||
|
sys.path.insert(0, TOOLS_DIR)
|
||||||
|
|
||||||
|
from fix_anvelope_oe8 import SEED_PATH, este_inlocuire_anvelope # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def test_regula_pozitive():
|
||||||
|
for t in [
|
||||||
|
"INLOCUIT ANVELOPE B 55 XMH",
|
||||||
|
"INLOCUIT ANVELOPE 2 BUC",
|
||||||
|
"INLOCUIT + ECHILIBRAT ANVELOPE",
|
||||||
|
"SCHIMBAT ANVELOPE VARA",
|
||||||
|
"MONTAT ANVELOPE (2)",
|
||||||
|
"INL ANVELOPE + ECHILIBR",
|
||||||
|
]:
|
||||||
|
assert este_inlocuire_anvelope(t), t
|
||||||
|
|
||||||
|
|
||||||
|
def test_regula_negative():
|
||||||
|
for t in [
|
||||||
|
"INLOCUIT ANVELOPA DR FATA", # singular cu pozitie = context reparatie
|
||||||
|
"D/R ANVELOPE(4 BUC)", # demontat/remontat, nu inlocuire
|
||||||
|
"INLOCUIT ANVELOPA+JANTA", # janta = reparatie
|
||||||
|
"REPARAT ANVELOPE", # vulcanizare/reparatie
|
||||||
|
"CHIRIE ANVELOPE AUGUST", # non-operatie (fara verb de inlocuire)
|
||||||
|
"INLOCUIT PLACUTE FRANA", # alta piesa
|
||||||
|
"VERIFICAT PRESIUNE ANVELOPE", # verificare, nu inlocuire
|
||||||
|
]:
|
||||||
|
assert not este_inlocuire_anvelope(t), t
|
||||||
|
|
||||||
|
|
||||||
|
def test_seed_fara_inlocuiri_anvelope_gresite():
|
||||||
|
"""Orice intrare de inlocuire anvelope din seed trebuie sa fie OE-8."""
|
||||||
|
with open(SEED_PATH, encoding="utf-8") as f:
|
||||||
|
items = json.load(f)
|
||||||
|
gresite = [
|
||||||
|
it["denumire_normalizata"]
|
||||||
|
for it in items
|
||||||
|
if not it.get("is_nul")
|
||||||
|
and este_inlocuire_anvelope(it.get("denumire_normalizata") or "")
|
||||||
|
and it.get("cod") != "OE-8"
|
||||||
|
]
|
||||||
|
assert gresite == [], f"{len(gresite)} inlocuiri de anvelope ne-OE-8 in seed: {gresite[:5]}"
|
||||||
165
tools/mapare-llm/fix_anvelope_oe8.py
Normal file
165
tools/mapare-llm/fix_anvelope_oe8.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
"""Corectie etichete SILVER: inlocuirea de anvelope -> OE-8.
|
||||||
|
|
||||||
|
Seed-ul Haiku a etichetat sistematic "INLOCUIT ANVELOPE ..." cu OE-1 (REPARATIE),
|
||||||
|
desi nomenclatorul RAR are un cod dedicat: OE-8 (INLOCUIRE SEZONIERA A ANVELOPELOR).
|
||||||
|
Eroarea domina vecinatatea k-NN (223 OE-1 vs 17 OE-8), asa ca votul sugera OE-1
|
||||||
|
pentru orice "Inlocuire anvelope".
|
||||||
|
|
||||||
|
Regula (deterministica, aceeasi in DB si in seed):
|
||||||
|
text NORMALIZAT care contine un verb de inlocuire/montare
|
||||||
|
(INLOCUIT/INLOCUIRE/INL/SCHIMB*/MONTAT/MONTARE) SI anvelope la plural
|
||||||
|
(ANVELOPE/CAUCIUCURI), FARA context de reparatie (JANTA, D/R, D/M, REPARAT,
|
||||||
|
VULCANIZ, PRESIUNE, DEPRESAT) -> OE-8.
|
||||||
|
Singularul cu pozitie ("INLOCUIT ANVELOPA DR FATA") ramane OE-1: e tipic
|
||||||
|
inlocuire dupa pana/uzura in cadrul unei reparatii, nu schimb sezonier.
|
||||||
|
|
||||||
|
In plus, adauga intrari canonice OE-8 curate manual (`CURATE_OE8`) pentru
|
||||||
|
formularile uzuale de schimb sezonier absente din corpus ("MONTAT CAUCIUCURI
|
||||||
|
VARA" etc.) — fara ele, k-NN nu are niciun vecin relevant si votul cade pe
|
||||||
|
zgomot de coada.
|
||||||
|
|
||||||
|
Aplica idempotent pe:
|
||||||
|
- tabela `mapping_suggestions` (sursa corpusului k-NN la runtime)
|
||||||
|
- `app/data/operatii-etichetate.json` (seed-ul pentru instalari noi)
|
||||||
|
`source` devine "haiku_seed+fix_oe8" pe randurile corectate, "curat_manual"
|
||||||
|
pe cele adaugate (audit).
|
||||||
|
|
||||||
|
Rulare:
|
||||||
|
python3 tools/mapare-llm/fix_anvelope_oe8.py --db data/autopass.db [--dry-run]
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
_ROOT = os.path.abspath(os.path.join(_HERE, "..", ".."))
|
||||||
|
SEED_PATH = os.path.join(_ROOT, "app", "data", "operatii-etichetate.json")
|
||||||
|
|
||||||
|
VERB = re.compile(r"\b(INLOCUIT|INLOCUIRE|INL|SCHIMB|SCHIMBAT|SCHIMBARE|MONTAT|MONTARE)\b")
|
||||||
|
PLURAL = re.compile(r"ANVELOPE|CAUCIUCURI")
|
||||||
|
EXCLUDERE = re.compile(r"JANTA|D/R|D/M|REPARAT|VULCANIZ|PRESIUNE|DEPRESAT")
|
||||||
|
|
||||||
|
# Formulari canonice de schimb sezonier, absente din corpusul Haiku.
|
||||||
|
# Texte deja in forma normalizata (majuscule ASCII, fara diacritice).
|
||||||
|
CURATE_OE8 = [
|
||||||
|
"INLOCUIRE ANVELOPE",
|
||||||
|
"SCHIMBARE ANVELOPE",
|
||||||
|
"SCHIMB ANVELOPE",
|
||||||
|
"SCHIMBAT ANVELOPE IARNA",
|
||||||
|
"SCHIMBAT ANVELOPE VARA",
|
||||||
|
"MONTAT ANVELOPE IARNA",
|
||||||
|
"MONTAT ANVELOPE VARA",
|
||||||
|
"MONTAT CAUCIUCURI IARNA",
|
||||||
|
"MONTAT CAUCIUCURI VARA",
|
||||||
|
"SCHIMB CAUCIUCURI",
|
||||||
|
"SCHIMBAT ROTI IARNA",
|
||||||
|
"SCHIMBAT ROTI VARA",
|
||||||
|
"INLOCUIRE SEZONIERA ANVELOPE",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def este_inlocuire_anvelope(text_normalizat: str) -> bool:
|
||||||
|
"""True daca textul descrie o inlocuire/montare de anvelope (plural)."""
|
||||||
|
t = text_normalizat or ""
|
||||||
|
return bool(VERB.search(t) and PLURAL.search(t) and not EXCLUDERE.search(t))
|
||||||
|
|
||||||
|
|
||||||
|
def fix_db(conn: sqlite3.Connection, dry_run: bool) -> int:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT id, denumire_normalizata FROM mapping_suggestions "
|
||||||
|
"WHERE is_nul=0 AND cod_prestatie != 'OE-8' "
|
||||||
|
"AND (denumire_normalizata LIKE '%ANVELOP%' OR denumire_normalizata LIKE '%CAUCIUC%')"
|
||||||
|
).fetchall()
|
||||||
|
ids = [r[0] for r in rows if este_inlocuire_anvelope(r[1])]
|
||||||
|
if not dry_run and ids:
|
||||||
|
conn.executemany(
|
||||||
|
"UPDATE mapping_suggestions SET cod_prestatie='OE-8', "
|
||||||
|
"source='haiku_seed+fix_oe8', updated_at=datetime('now') WHERE id=?",
|
||||||
|
[(i,) for i in ids],
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return len(ids)
|
||||||
|
|
||||||
|
|
||||||
|
def adauga_curate_db(conn: sqlite3.Connection, dry_run: bool) -> int:
|
||||||
|
n = 0
|
||||||
|
for t in CURATE_OE8:
|
||||||
|
exists = conn.execute(
|
||||||
|
"SELECT 1 FROM mapping_suggestions WHERE denumire_normalizata=?", (t,)
|
||||||
|
).fetchone()
|
||||||
|
if exists:
|
||||||
|
continue
|
||||||
|
n += 1
|
||||||
|
if not dry_run:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO mapping_suggestions "
|
||||||
|
"(denumire_normalizata, cod_prestatie, is_nul, source, confidence) "
|
||||||
|
"VALUES (?, 'OE-8', 0, 'curat_manual', 1.0)",
|
||||||
|
(t,),
|
||||||
|
)
|
||||||
|
if not dry_run:
|
||||||
|
conn.commit()
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def adauga_curate_seed(items: list[dict], dry_run: bool) -> int:
|
||||||
|
existente = {it.get("denumire_normalizata") for it in items}
|
||||||
|
n = 0
|
||||||
|
for t in CURATE_OE8:
|
||||||
|
if t in existente:
|
||||||
|
continue
|
||||||
|
n += 1
|
||||||
|
if not dry_run:
|
||||||
|
items.append({
|
||||||
|
"denumire": t,
|
||||||
|
"denumire_normalizata": t,
|
||||||
|
"cod": "OE-8",
|
||||||
|
"is_nul": False,
|
||||||
|
"source": "curat_manual",
|
||||||
|
"confidence": 1.0,
|
||||||
|
})
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def fix_seed(dry_run: bool) -> tuple[int, int]:
|
||||||
|
with open(SEED_PATH, encoding="utf-8") as f:
|
||||||
|
items = json.load(f)
|
||||||
|
n = 0
|
||||||
|
for it in items:
|
||||||
|
if it.get("is_nul") or it.get("cod") == "OE-8":
|
||||||
|
continue
|
||||||
|
if este_inlocuire_anvelope(it.get("denumire_normalizata") or ""):
|
||||||
|
it["cod"] = "OE-8"
|
||||||
|
it["source"] = "haiku_seed+fix_oe8"
|
||||||
|
n += 1
|
||||||
|
n_curate = adauga_curate_seed(items, dry_run)
|
||||||
|
if not dry_run and (n or n_curate):
|
||||||
|
with open(SEED_PATH, "w", encoding="utf-8") as f:
|
||||||
|
# acelasi format ca genereaza_seed.py (diff git minimal la corectii)
|
||||||
|
json.dump(items, f, ensure_ascii=False, indent=2)
|
||||||
|
return n, n_curate
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("--db", default=os.path.join(_ROOT, "data", "autopass.db"))
|
||||||
|
ap.add_argument("--dry-run", action="store_true")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
conn = sqlite3.connect(args.db)
|
||||||
|
n_db = fix_db(conn, args.dry_run)
|
||||||
|
n_db_curate = adauga_curate_db(conn, args.dry_run)
|
||||||
|
n_seed, n_seed_curate = fix_seed(args.dry_run)
|
||||||
|
eticheta = "(dry-run) " if args.dry_run else ""
|
||||||
|
print(
|
||||||
|
f"{eticheta}DB: {n_db} relabel + {n_db_curate} curate; "
|
||||||
|
f"seed JSON: {n_seed} relabel + {n_seed_curate} curate"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -10,9 +10,12 @@ NU ground-truth uman — valorile absolute sunt optimiste; comparatia RELATIVA
|
|||||||
intre politici si calibrarea pragului raman valide (aceeasi tinta pentru toate).
|
intre politici si calibrarea pragului raman valide (aceeasi tinta pentru toate).
|
||||||
|
|
||||||
Politici evaluate:
|
Politici evaluate:
|
||||||
top1 — eticheta primului vecin (comportamentul curent din enrich_suggestions)
|
top1 — eticheta primului vecin
|
||||||
vote<k> — vot ponderat cu similaritatea pe top-k vecini peste prag;
|
vote<k> — vot ponderat pe rang (EMB_VOTE_DECAY^rang) pe top-k vecini peste
|
||||||
NUL e eticheta proprie (castiga -> supresie)
|
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):
|
Metrice (per politica x prag):
|
||||||
coverage — % din exemple pentru care se emite o predictie (cod sau NUL)
|
coverage — % din exemple pentru care se emite o predictie (cod sau NUL)
|
||||||
@@ -85,12 +88,13 @@ def predict_top1(nbr_labels: list[str], nbr_sims: np.ndarray, prag: float) -> st
|
|||||||
|
|
||||||
|
|
||||||
def predict_vote(nbr_labels: list[str], nbr_sims: np.ndarray, prag: float) -> str | None:
|
def predict_vote(nbr_labels: list[str], nbr_sims: np.ndarray, prag: float) -> str | None:
|
||||||
"""Vot ponderat cu similaritatea pe vecinii peste prag."""
|
"""Vot ponderat pe rang (decay^rang) pe vecinii peste prag — politica din enrich."""
|
||||||
|
from app.mapping import EMB_VOTE_DECAY
|
||||||
scoruri: dict[str, float] = {}
|
scoruri: dict[str, float] = {}
|
||||||
for lab, sim in zip(nbr_labels, nbr_sims):
|
for rang, (lab, sim) in enumerate(zip(nbr_labels, nbr_sims), 1):
|
||||||
if sim < prag:
|
if sim < prag:
|
||||||
continue
|
continue
|
||||||
scoruri[lab] = scoruri.get(lab, 0.0) + float(sim)
|
scoruri[lab] = scoruri.get(lab, 0.0) + EMB_VOTE_DECAY ** rang
|
||||||
if not scoruri:
|
if not scoruri:
|
||||||
return None
|
return None
|
||||||
return max(scoruri, key=lambda k: scoruri[k])
|
return max(scoruri, key=lambda k: scoruri[k])
|
||||||
|
|||||||
Reference in New Issue
Block a user