"""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()