Template
144 lines
7.2 KiB
Python
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()
|