Files
Rag_Kecerdasan_Buatan/lab3_hit_at_k.py
T
2026-11-02 10:51:23 +07:00

172 lines
9.1 KiB
Python

"""Lab 3 - Ukur Hit@k (25 menit).
1. 15 pertanyaan uji beserta kata kunci jawaban dari dokumen.
2. hit_at_k untuk k = 1, 3, 5 pada dua koleksi dengan ukuran chunk berbeda.
3. Kesimpulan 3-4 kalimat: ukuran terbaik, pertanyaan yang gagal, dugaan penyebab.
"""
import os
import sys
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from laporan_utils import Laporan
from rag_utils import FOLDER_DOKUMEN, baca_pdf, bangun_indeks, cocok, hit_at_k, norm, peringkat_hit
BASE = os.path.dirname(os.path.abspath(__file__))
OVERLAP = 0.15
UKURAN = (500, 1000) # dua koleksi yang dibandingkan
KS = (1, 3, 5)
SOAL = [ # q = pertanyaan, kw = kata kunci yang harus ada di chunk jawaban, jenis = langsung / parafrase
{"q": "Siapa yang memperkenalkan RAG?", "kw": ["Lewis"], "jenis": "langsung"},
{"q": "Berapa dimensi embedding yang dihasilkan model MiniLM?", "kw": ["384"], "jenis": "langsung"},
{"q": "Seberapa besar tumpang tindih antar potongan teks yang disarankan?", "kw": ["10 sampai 20 persen"], "jenis": "parafrase"},
{"q": "Pustaka apa buatan Meta untuk pencarian tetangga terdekat?", "kw": ["FAISS"], "jenis": "langsung"},
{"q": "Jika 12 dari 15 soal terjawab pada lima hasil teratas, berapa skornya?", "kw": ["0,8"], "jenis": "parafrase"},
{"q": "Berapa rentang nilai cosine similarity?", "kw": ["-1 dan 1"], "jenis": "langsung"},
{"q": "Bagaimana menyatukan ketepatan dan kelengkapan menjadi satu angka?", "kw": ["rata-rata harmonik"], "jenis": "parafrase"},
{"q": "Mana yang diutamakan untuk skrining pasien: tidak melewatkan kasus atau menghindari alarm palsu?", "kw": ["deteksi penyakit"], "jenis": "parafrase"},
{"q": "Berapa banyak lipatan yang biasa dipakai pada validasi silang?", "kw": ["5 atau 10"], "jenis": "parafrase"},
{"q": "Teknik apa yang membuat sebagian bobot menjadi tepat nol?", "kw": ["Lasso"], "jenis": "parafrase"},
{"q": "Undang-undang nomor berapa yang mengatur pelindungan data pribadi?", "kw": ["27 Tahun 2022"], "jenis": "langsung"},
{"q": "Bagaimana melindungi sistem dari instruksi jahat yang disusupkan ke dokumen?", "kw": ["memisahkan instruksi"], "jenis": "parafrase"},
{"q": "Kasus perekrutan yang merendahkan pelamar perempuan dialami perusahaan apa?", "kw": ["Amazon"], "jenis": "parafrase"},
{"q": "Siapa yang memperkenalkan model card?", "kw": ["Mitchell"], "jenis": "langsung"},
{"q": "Apa kekurangan chunk berukuran 1500 karakter?", "kw": ["boros"], "jenis": "langsung"}, # jawaban ada di tabel
]
def diagnosis(idx, soal, k):
"""Tebak penyebab gagal secara otomatis dari data (bukan menebak sembarang)."""
r = peringkat_hit(idx, soal)
if r is None:
return "kata kunci terbelah di batas chunk (tidak ada satu chunk pun yang memuatnya utuh)"
return f"chunk jawaban ada tetapi baru muncul di peringkat {r} (> {k})"
def main():
# sanity check: setiap kata kunci memang ada di dokumen asli
korpus = norm(" ".join(baca_pdf(os.path.join(FOLDER_DOKUMEN, f))
for f in sorted(os.listdir(FOLDER_DOKUMEN)) if f.endswith(".pdf")))
for s in SOAL:
assert all(norm(k) in korpus for k in s["kw"]), f"kata kunci tidak ada di dokumen: {s}"
koleksi, hasil = {}, {}
for u in UKURAN:
print(f"\n=== Koleksi ukuran {u} ===")
koleksi[u] = bangun_indeks(ukuran=u, overlap=OVERLAP, backend="auto")
hasil[u] = {k: hit_at_k(koleksi[u], SOAL, k) for k in KS}
print({f"Hit@{k}": round(v, 3) for k, v in hasil[u].items()})
backend = koleksi[UKURAN[0]].backend
ranks = {u: [peringkat_hit(koleksi[u], s) for s in SOAL] for u in UKURAN}
# ---- kesimpulan otomatis
kunci = lambda u: (hasil[u][5], hasil[u][3], hasil[u][1])
a, b = UKURAN
terbaik = a if kunci(a) > kunci(b) else b if kunci(b) > kunci(a) else None
fmt = lambda u: "/".join(f"{hasil[u][k]:.2f}" for k in KS)
if terbaik:
lain = b if terbaik == a else a
s1 = (f"Ukuran {terbaik} karakter lebih baik: Hit@1/3/5 = {fmt(terbaik)} dibanding {fmt(lain)} pada "
f"ukuran {lain}.")
else:
s1 = f"Kedua ukuran seimbang: Hit@1/3/5 = {fmt(a)} (ukuran {a}) dan {fmt(b)} (ukuran {b})."
gagal = {u: [i for i, r in enumerate(ranks[u], 1) if r is None or r > 5] for u in UKURAN}
s2 = "Pada k=5, pertanyaan yang gagal: " + "; ".join(
f"ukuran {u} -> " + (", ".join(f"no. {i}" for i in gagal[u]) if gagal[u] else "tidak ada") for u in UKURAN) + "."
penyebab = {}
for u in UKURAN:
for i in gagal[u]:
penyebab[(u, i)] = diagnosis(koleksi[u], SOAL[i - 1], 5)
print("\nDiagnosis gagal (k=5):")
for (u, i), c in penyebab.items():
print(f" ukuran {u}, no. {i} ({SOAL[i-1]['q']}): {c}")
# hit@k per jenis soal (langsung vs parafrase)
per_jenis = {}
for jenis in ("langsung", "parafrase"):
sub = [s for s in SOAL if s["jenis"] == jenis]
per_jenis[jenis] = (len(sub), {u: hit_at_k(koleksi[u], sub, 5) for u in UKURAN})
# kalimat 3: dugaan penyebab berdasarkan jenis diagnosis
if penyebab:
terbelah = [x for x in penyebab.values() if x.startswith("kata kunci terbelah")]
peringkat = [x for x in penyebab.values() if x.startswith("chunk jawaban ada")]
dugaan = []
if peringkat:
dugaan.append("kosakata pertanyaan berbeda dari kosakata dokumen (parafrase, mis. istilah Indonesia "
"vs istilah Inggris di teks), dan chunk kecil memuat lebih sedikit kata konteks sehingga "
"skor kemiripannya kalah dari chunk lain yang topiknya lebih umum")
if terbelah:
dugaan.append("kata kunci jawaban terbelah di batas chunk dan overlap belum cukup menyelamatkannya")
s3 = "Dugaan penyebab: " + "; serta ".join(dugaan) + "."
else:
s3 = "Tidak ada kegagalan pada k=5, sehingga tidak ada penyebab yang perlu dianalisis."
s4 = ("Catatan: skor mendekati sempurna karena korpus hanya 3 dokumen kecil (13-25 chunk) dengan topik yang "
"jelas berbeda; pada PDF yang lebih besar dan lebih mirip antar dokumen, Hit@k diperkirakan turun dan "
"perbedaan antar ukuran chunk akan lebih terlihat.")
kesimpulan = " ".join([s1, s2, s3, s4])
print("\nKesimpulan:", kesimpulan)
# grafik
fig, ax = plt.subplots(figsize=(6.4, 3.2), dpi=150)
lebar = 0.35
for j, u in enumerate(UKURAN):
xs = [i + (j - 0.5) * lebar for i in range(len(KS))]
bars = ax.bar(xs, [hasil[u][k] for k in KS], lebar, label=f"ukuran {u}")
ax.bar_label(bars, fmt="%.2f", fontsize=8)
ax.set_xticks(range(len(KS)), [f"Hit@{k}" for k in KS])
ax.set_ylim(0, 1.12)
ax.set_ylabel("proporsi soal")
ax.set_title(f"Hit@k pada 15 soal (overlap {int(OVERLAP * 100)}%, backend {backend})", fontsize=10)
ax.legend(loc="lower right", fontsize=8)
fig.tight_layout()
gambar = os.path.join(BASE, "_grafik_hit_at_k.png")
fig.savefig(gambar)
plt.close(fig)
# laporan PDF
W = 28.35
L = Laporan(os.path.join(BASE, "Lab3_Hit_at_k.pdf"), "Lab 3 - Ukur Hit@k",
f"backend: {backend} | overlap {int(OVERLAP * 100)}% | koleksi ukuran {UKURAN[0]} dan {UKURAN[1]}")
L.h1("Langkah 1. Lima belas pertanyaan uji")
L.p("Soal dibuat dari tiga dokumen di folder dokumen/. Sebuah chunk dianggap benar bila memuat semua kata kunci "
"jawaban. Sebanyak 7 soal bernada langsung dan 8 soal diparafrase (kata-katanya sengaja berbeda dari teks).")
L.tabel([["No", "Jenis", "Pertanyaan", "Kata kunci"]] +
[[i, s["jenis"], s["q"], ", ".join(s["kw"])] for i, s in enumerate(SOAL, 1)],
[1 * W, 2 * W, 10 * W, 4 * W])
L.h1("Langkah 2. Hasil Hit@k")
L.tabel([["Koleksi"] + [f"Hit@{k}" for k in KS] + ["Jumlah chunk"]] +
[[f"ukuran {u}"] + [f"{hasil[u][k]:.3f}" for k in KS] + [len(koleksi[u].chunks)] for u in UKURAN],
[4 * W, 2.5 * W, 2.5 * W, 2.5 * W, 3 * W])
L.gambar(gambar, 13)
L.h2("Hit@5 menurut jenis soal")
L.tabel([["Jenis soal", "Jumlah"] + [f"ukuran {u}" for u in UKURAN]] +
[[j, n] + [f"{v[u]:.3f}" for u in UKURAN] for j, (n, v) in per_jenis.items()],
[4 * W, 2.5 * W, 3 * W, 3 * W])
L.h2("Peringkat chunk pertama yang memuat jawaban, per soal")
L.p("Angka 1 berarti chunk teratas sudah benar. Angka > 5 berarti gagal pada Hit@5.")
L.tabel([["No", "Jenis", f"Peringkat (ukuran {UKURAN[0]})", f"Peringkat (ukuran {UKURAN[1]})"]] +
[[i, s["jenis"], ranks[UKURAN[0]][i - 1], ranks[UKURAN[1]][i - 1]] for i, s in enumerate(SOAL, 1)],
[1.2 * W, 3 * W, 5 * W, 5 * W])
if penyebab:
L.h2("Diagnosis soal yang gagal (k=5)")
L.tabel([["Ukuran", "No", "Pertanyaan", "Diagnosis"]] +
[[u, i, SOAL[i - 1]["q"], c] for (u, i), c in penyebab.items()], [1.5 * W, 1 * W, 7 * W, 8 * W])
L.h1("Langkah 3. Kesimpulan")
L.p(kesimpulan)
L.halaman_baru()
L.h1("Lampiran: kode lab3_hit_at_k.py")
L.kode(open(__file__, encoding="utf-8").read())
L.simpan()
os.remove(gambar)
print("\nLaporan: Lab3_Hit_at_k.pdf")
if __name__ == "__main__":
main()