kozynear / backend /scripts /explore_lexical_vs_semantic.py
DYmazeh's picture
deploy: 6f884effe461
4a452f8 verified
Raw
History Blame Contribute Delete
5.4 kB
"""Eksplorasi 1: kapan semantic (neural) menang atas lexical (BM25)?
Temuan sebelumnya: setelah fielded indexing, MiniLM tidak menambah nilai
(alpha optimal -> 1.0). Hipotesis: itu karena query eval BERBAGI kosakata
dengan dokumen, jadi BM25 sudah cukup. Uji: query PARAPHRASE yang sengaja
menghindari kata di listing, dengan relevansi didefinisikan via ORACLE
METADATA (gender/geo/fasilitas/harga) yang tidak bergantung kata.
Kalau neural unggul di paraphrase tapi BM25 unggul di literal -> terbukti
batas lexical vs semantic, dengan bukti dari data sendiri.
Usage: cd backend && python -m scripts.explore_lexical_vs_semantic
"""
from __future__ import annotations
import json, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from app.indexing.loader import load_all_indexes # noqa: E402
from app.preprocessing import PreprocessingPipeline # noqa: E402
from app.search.gazetteer import haversine_km # noqa: E402
from scripts.eval_smart import load_listings # noqa: E402
ROOT = Path(__file__).resolve().parents[2]
# Tiap kasus: query LITERAL (berbagi kata corpus) vs SEMANTIC (paraphrase
# menghindari kata corpus), dengan oracle relevansi yang SAMA.
# oracle: ("gender", "putri") | ("geo", (lat,lng,km)) | ("fac","ac") | ("price",max)
CASES = [
{"intent": "kos perempuan", "literal": "kos putri",
"semantic": "hunian khusus muslimah", "oracle": ("gender", "putri")},
{"intent": "kos laki-laki", "literal": "kos putra",
"semantic": "indekos untuk pria lajang", "oracle": ("gender", "putra")},
{"intent": "dekat ITERA", "literal": "kos dekat itera",
"semantic": "tinggal dekat institut teknologi sumatera",
"oracle": ("geo", (-5.3668, 105.3149, 3.0))},
{"intent": "dekat UNILA", "literal": "kos dekat unila",
"semantic": "akomodasi sekitar universitas negeri lampung",
"oracle": ("geo", (-5.3645, 105.2434, 3.0))},
{"intent": "ada AC", "literal": "kos ac",
"semantic": "kamar dengan pendingin ruangan sejuk", "oracle": ("fac", "ac")},
{"intent": "ada wifi", "literal": "kos wifi",
"semantic": "tersedia koneksi internet nirkabel", "oracle": ("fac", "wifi")},
{"intent": "murah", "literal": "kos murah",
"semantic": "sewa kamar ramah kantong ekonomis", "oracle": ("price", 600000)},
{"intent": "kamar mandi dalam", "literal": "kos kamar mandi dalam",
"semantic": "toilet pribadi di dalam kamar", "oracle": ("fac", "mandi")},
]
def relevant(row, oracle) -> bool:
kind, val = oracle
if kind == "gender":
return row.tipe == val
if kind == "geo":
if row.koordinat_lat is None:
return False
lat, lng, km = val
return haversine_km(float(row.koordinat_lat), float(row.koordinat_lng), lat, lng) <= km
if kind == "fac":
return any(val in str(f).lower() for f in (row.fasilitas or []))
if kind == "price":
return (row.harga_per_bulan or 10**9) <= val
return False
def p_at_k(ranked_ids, listings, oracle, k=5) -> float:
top = ranked_ids[:k]
if not top:
return 0.0
return sum(1 for d in top if d in listings and relevant(listings[d], oracle)) / len(top)
def main() -> int:
idx = load_all_indexes(ROOT / "data" / "indexes", include_neural=True)
bm25, tfidf, neural = idx["bm25"], idx["tfidf"], idx["indobert"]
pipe = PreprocessingPipeline()
pre = lambda s: pipe.process(s).processed # noqa: E731
listings = load_listings()
def bm25_ids(q): return [h.doc_id for h in bm25.query(pre(q), top_k=5)]
def tfidf_ids(q): return [h.doc_id for h in tfidf.query(pre(q), top_k=5)]
def neural_ids(q): return [h.doc_id for h in neural.query(q, top_k=5)]
agg = {"literal": {"bm25": [], "neural": [], "tfidf": []},
"semantic": {"bm25": [], "neural": [], "tfidf": []}}
print(f"{'intent':<22} {'mode':<9} {'BM25':>5} {'TFIDF':>6} {'Neural':>7}")
for c in CASES:
for mode in ("literal", "semantic"):
q = c[mode]
pb = p_at_k(bm25_ids(q), listings, c["oracle"])
pt = p_at_k(tfidf_ids(q), listings, c["oracle"])
pn = p_at_k(neural_ids(q), listings, c["oracle"])
agg[mode]["bm25"].append(pb); agg[mode]["tfidf"].append(pt); agg[mode]["neural"].append(pn)
print(f"{c['intent']:<22} {mode:<9} {pb:>5.2f} {pt:>6.2f} {pn:>7.2f}")
print("\n=== RATA-RATA P@5 ===")
print(f"{'mode':<10} {'BM25':>6} {'TFIDF':>6} {'Neural':>7}")
out = {}
for mode in ("literal", "semantic"):
n = len(agg[mode]["bm25"])
mb = sum(agg[mode]["bm25"]) / n
mt = sum(agg[mode]["tfidf"]) / n
mn = sum(agg[mode]["neural"]) / n
out[mode] = {"bm25": mb, "tfidf": mt, "neural": mn}
print(f"{mode:<10} {mb:>6.3f} {mt:>6.3f} {mn:>7.3f}")
gap_lit = out["literal"]["neural"] - out["literal"]["bm25"]
gap_sem = out["semantic"]["neural"] - out["semantic"]["bm25"]
print(f"\nGap neural-BM25 @ literal : {gap_lit:+.3f}")
print(f"Gap neural-BM25 @ semantic: {gap_sem:+.3f}")
print("INTERPRETASI:", "neural unggul saat kosakata MISMATCH (semantic)"
if gap_sem > gap_lit else "neural tidak menolong walau paraphrase")
(ROOT / "eval" / "explore_lexical_vs_semantic.json").write_text(
json.dumps(out, indent=2), encoding="utf-8")
return 0
if __name__ == "__main__":
sys.exit(main())