Files
2026-11-02 10:51:23 +07:00

144 lines
7.2 KiB
Python

"""Lab 2 - Bangun indeks & cari (25 menit).
1. Letakkan 2-3 PDF di folder dokumen/, jalankan indeks, pastikan tiap file melaporkan jumlah chunk.
2. Ajukan 5 pertanyaan dengan query(); periksa apakah chunk teratas benar-benar relevan.
3. Ajukan 2 pertanyaan berparafrase; apakah pencarian semantik tetap menemukannya?
"""
import io
import os
import sys
from contextlib import redirect_stdout
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from laporan_utils import Laporan
from rag_utils import bangun_indeks, cocok, query
BASE = os.path.dirname(os.path.abspath(__file__))
UKURAN, OVERLAP = 1000, 0.15
# kw = kata kunci yang HARUS ada di chunk teratas agar dianggap relevan
PERTANYAAN = [
{"q": "Siapa yang memperkenalkan RAG dan pada tahun berapa?", "kw": ["Lewis", "2020"]},
{"q": "Berapa dimensi embedding yang dihasilkan model MiniLM?", "kw": ["384"]},
{"q": "Apa itu overfitting dan apa cirinya?", "kw": ["overfitting", "data latih"]},
{"q": "Undang-undang apa yang mengatur pelindungan data pribadi di Indonesia?", "kw": ["27 Tahun 2022"]},
{"q": "Bagaimana cara memitigasi prompt injection?", "kw": ["memisahkan instruksi"]},
]
PARAFRASE = [
{"q": "Bagaimana mencegah model bahasa mengarang jawaban yang keliru?", "kw": ["halusinasi"],
"catatan": "Teks memakai kata 'halusinasi'; pertanyaan memakai 'mengarang jawaban yang keliru'."},
{"q": "Kapan sebuah model dianggap terlalu menghafal data latihan?", "kw": ["overfitting"],
"catatan": "Teks memakai 'overfitting' dan 'data latih'; pertanyaan memakai 'menghafal' dan 'data latihan'."},
]
def ringkas(t, n=150):
t = " ".join(t.split())
return t if len(t) <= n else t[:n] + "..."
def jalankan(indeks, daftar, k=3):
hasil = []
for s in daftar:
atas = query(indeks, s["q"], k)
rel = [cocok(a["teks"], s["kw"]) for a in atas]
hasil.append((s, atas, rel))
return hasil
def main():
# ---- Langkah 1
print("=== Langkah 1: membangun indeks ===")
buf = io.StringIO()
with redirect_stdout(buf):
idx = bangun_indeks(ukuran=UKURAN, overlap=OVERLAP, backend="auto")
log_indeks = buf.getvalue()
print(log_indeks)
idx_leksikal = bangun_indeks(ukuran=UKURAN, overlap=OVERLAP, backend="tfidf", verbose=False)
per_file = {}
for m in idx.meta:
per_file[m["file"]] = per_file.get(m["file"], 0) + 1
# ---- Langkah 2
print("=== Langkah 2: 5 pertanyaan ===")
h2 = jalankan(idx, PERTANYAAN)
for s, atas, rel in h2:
print(f"\nQ: {s['q']} (kata kunci: {s['kw']})")
for r, (a, ok) in enumerate(zip(atas, rel), 1):
print(f" #{r} skor={a['skor']:.3f} {a['file']}#{a['no']} relevan={ok} | {ringkas(a['teks'])}")
n_top1 = sum(rel[0] for _, _, rel in h2)
n_top3 = sum(any(rel) for _, _, rel in h2)
# ---- Langkah 3
print("\n=== Langkah 3: parafrase ===")
sem = jalankan(idx, PARAFRASE)
lek = jalankan(idx_leksikal, PARAFRASE)
for (s, a1, r1), (_, a2, r2) in zip(sem, lek):
print(f"\nQ: {s['q']}")
print(f" {idx.backend}: top1 relevan={r1[0]} top3 relevan={any(r1)} | {a1[0]['file']}#{a1[0]['no']}")
print(f" tfidf : top1 relevan={r2[0]} top3 relevan={any(r2)} | {a2[0]['file']}#{a2[0]['no']}")
# ---- Kesimpulan dari angka
sem_top3 = sum(any(r) for _, _, r in sem)
lek_top3 = sum(any(r) for _, _, r in lek)
sem_top1 = sum(r[0] for _, _, r in sem)
lek_top1 = sum(r[0] for _, _, r in lek)
kes = (f"Pada 5 pertanyaan langsung, chunk peringkat 1 relevan pada {n_top1}/5 pertanyaan dan salah satu dari "
f"3 chunk teratas relevan pada {n_top3}/5 pertanyaan. Pada 2 pertanyaan parafrase, backend "
f"'{idx.backend}' menemukan chunk relevan di peringkat 1 pada {sem_top1}/2 dan di 3 teratas pada "
f"{sem_top3}/2, sedangkan pembanding TF-IDF (kecocokan bentuk kata) {lek_top1}/2 dan {lek_top3}/2. ")
gagal_top1 = [s["q"] for s, atas, rel in h2 if not rel[0]]
if gagal_top1:
kes += ("Pertanyaan yang chunk peringkat 1-nya belum relevan: " + "; ".join(f'"{q}"' for q in gagal_top1) +
". Penyebab yang paling mungkin: kalimat jawaban terbelah di batas chunk sehingga kata kuncinya "
"terpisah, sementara chunk di sebelahnya memuat topik yang mirip. ")
if sem_top1 == lek_top1 and sem_top3 == lek_top3:
kes += ("Pada dua parafrase ini, semantik dan TF-IDF memberi hasil yang sama, jadi tes ini belum bisa "
"membedakan keduanya: pertanyaan masih berbagi akar kata dengan teks (model, jawaban, data latih) "
"dan TF-IDF n-gram karakter menangkap variasi bentuk kata. ")
if idx.backend == "lsa":
kes += ("Catatan: backend LSA hanya semantik ringan (hubungan antar kata dipelajari dari korpus yang kecil), "
"sehingga parafrase yang benar-benar tidak berbagi kata akan sulit. Untuk hasil lebih kuat, pasang "
"sentence-transformers; kode ini otomatis memakainya.")
print("\nKesimpulan:", kes)
# ---- Laporan PDF
L = Laporan(os.path.join(BASE, "Lab2_Indeks_dan_Pencarian.pdf"), "Lab 2 - Bangun Indeks & Cari",
f"ukuran chunk {UKURAN}, overlap {int(OVERLAP * 100)}%, backend: {idx.backend}")
L.h1("Langkah 1. Membangun indeks")
L.p("Tiga PDF diletakkan di folder dokumen/. Setiap file melaporkan jumlah chunk-nya:")
L.tabel([["File", "Jumlah chunk"]] + [[f, n] for f, n in per_file.items()] + [["Total", len(idx.chunks)]],
[9 * 28.35, 4 * 28.35])
L.kode(log_indeks.rstrip())
L.h1("Langkah 2. Lima pertanyaan dan pemeriksaan chunk teratas")
L.p("Sebuah chunk dianggap relevan bila memuat semua kata kunci jawaban. Tabel menunjukkan 3 chunk teratas.")
for s, atas, rel in h2:
L.h2(s["q"])
L.tabel([["Peringkat", "Skor", "Sumber", "Relevan?", "Cuplikan"]] +
[[r, f"{a['skor']:.3f}", f"{a['file']} #{a['no']}", "ya" if ok else "tidak", ringkas(a["teks"], 140)]
for r, (a, ok) in enumerate(zip(atas, rel), 1)],
[1.6 * 28.35, 1.3 * 28.35, 3.8 * 28.35, 1.6 * 28.35, 8.7 * 28.35])
L.p(f"Ringkasan: chunk peringkat 1 relevan pada {n_top1}/5 pertanyaan; ada chunk relevan di 3 teratas pada {n_top3}/5.")
L.h1("Langkah 3. Pertanyaan berparafrase")
L.p("Dua pertanyaan memakai kata yang berbeda dari teks dokumen. Dibandingkan: pencarian semantik "
f"({idx.backend}) vs TF-IDF.")
baris = [["Pertanyaan", "Catatan", f"{idx.backend}: top1 / top3", "TF-IDF: top1 / top3"]]
for (s, a1, r1), (_, a2, r2) in zip(sem, lek):
baris.append([s["q"], s["catatan"], f"{'ya' if r1[0] else 'tidak'} / {'ya' if any(r1) else 'tidak'}",
f"{'ya' if r2[0] else 'tidak'} / {'ya' if any(r2) else 'tidak'}"])
L.tabel(baris, [5 * 28.35, 6 * 28.35, 3 * 28.35, 3 * 28.35])
for (s, a1, r1), _ in zip(sem, lek):
L.p(f"Chunk teratas untuk \"{s['q']}\": {a1[0]['file']} #{a1[0]['no']} (skor {a1[0]['skor']:.3f})")
L.kode(ringkas(a1[0]["teks"], 400))
L.h1("Kesimpulan")
L.p(kes)
L.halaman_baru()
L.h1("Lampiran: kode lab2_indeks_cari.py")
L.kode(open(__file__, encoding="utf-8").read())
L.simpan()
print("\nLaporan: Lab2_Indeks_dan_Pencarian.pdf")
if __name__ == "__main__":
main()