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.
|
||||
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
|
||||
# (+~1pp precizie la acelasi coverage, masurat LOO).
|
||||
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
|
||||
# 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
|
||||
@@ -841,18 +848,18 @@ def enrich_suggestions(
|
||||
# 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=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.
|
||||
scoruri: 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))
|
||||
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
|
||||
scoruri[lab] = scoruri.get(lab, 0.0) + EMB_VOTE_DECAY ** rang
|
||||
sim_max[lab] = max(sim_max.get(lab, 0.0), sim)
|
||||
if scoruri:
|
||||
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).
|
||||
|
||||
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)
|
||||
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)
|
||||
@@ -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:
|
||||
"""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] = {}
|
||||
for lab, sim in zip(nbr_labels, nbr_sims):
|
||||
for rang, (lab, sim) in enumerate(zip(nbr_labels, nbr_sims), 1):
|
||||
if sim < prag:
|
||||
continue
|
||||
scoruri[lab] = scoruri.get(lab, 0.0) + float(sim)
|
||||
scoruri[lab] = scoruri.get(lab, 0.0) + EMB_VOTE_DECAY ** rang
|
||||
if not scoruri:
|
||||
return None
|
||||
return max(scoruri, key=lambda k: scoruri[k])
|
||||
|
||||
Reference in New Issue
Block a user