"""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? Versi ini memakai ketiga PDF tentang kecerdasan buatan: 1_Pengenalan_Kecerdasan_Buatan.pdf 2_Machine_Learning_dan_Deep_Learning.pdf 3_Etika_Risiko_dan_Masa_Depan_AI.pdf Jika belum ada di folder dokumen/, skrip menyalinnya dari folder skrip atau folder kerja. """ import io import os import re import shutil 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 FOLDER_DOKUMEN, bangun_indeks, query BASE = os.path.dirname(os.path.abspath(__file__)) UKURAN, OVERLAP = 1000, 0.15 DAFTAR_PDF = [ "1_Pengenalan_Kecerdasan_Buatan.pdf", "2_Machine_Learning_dan_Deep_Learning.pdf", "3_Etika_Risiko_dan_Masa_Depan_AI.pdf", ] # kw = kata kunci yang HARUS ada (semuanya) di sebuah chunk agar dianggap relevan # file = PDF tempat jawaban sebenarnya berada (dipakai untuk memeriksa kunci jawaban) PERTANYAAN = [ {"q": "Siapa yang memperkenalkan istilah kecerdasan buatan dan pada tahun berapa?", "kw": ["McCarthy", "1956"], "file": DAFTAR_PDF[0]}, {"q": "Siapa juara Go yang dikalahkan oleh AlphaGo?", "kw": ["AlphaGo", "Lee Sedol"], "file": DAFTAR_PDF[0]}, {"q": "Apa itu overfitting dan apa cirinya?", "kw": ["overfitting", "data latih"], "file": DAFTAR_PDF[1]}, {"q": "Arsitektur jaringan saraf apa yang dipakai untuk mengolah gambar?", "kw": ["CNN", "Convolutional"], "file": DAFTAR_PDF[1]}, {"q": "Apa saja isu etika utama dalam penggunaan AI?", "kw": ["Bias dan diskriminasi", "Privasi"], "file": DAFTAR_PDF[2]}, ] PARAFRASE = [ {"q": "Mengapa model bahasa kadang memberi jawaban yang terdengar meyakinkan padahal salah?", "kw": ["halusinasi"], "file": DAFTAR_PDF[2], "catatan": "Teks memakai kata 'halusinasi'; pertanyaan memakai 'terdengar meyakinkan padahal salah'."}, {"q": "Metode belajar mesin lewat coba-coba dengan imbalan dan sanksi itu namanya apa?", "kw": ["Reinforcement"], "file": DAFTAR_PDF[1], "catatan": "Teks memakai 'percobaan', 'hadiah', 'hukuman'; pertanyaan memakai 'coba-coba', 'imbalan', 'sanksi'."}, ] # --------------------------------------------------------------------------- utilitas def siapkan_dokumen(): """Pastikan ketiga PDF ada di FOLDER_DOKUMEN; salin dari folder skrip/kerja bila belum ada.""" os.makedirs(FOLDER_DOKUMEN, exist_ok=True) for nama in DAFTAR_PDF: tujuan = os.path.join(FOLDER_DOKUMEN, nama) if os.path.isfile(tujuan): continue for d in (BASE, os.getcwd()): asal = os.path.join(d, nama) if os.path.isfile(asal): shutil.copy(asal, tujuan) print(f" disalin: {nama} -> {FOLDER_DOKUMEN}") break else: print(f" [PERINGATAN] {nama} tidak ditemukan di {FOLDER_DOKUMEN}, {BASE}, maupun {os.getcwd()}") def rapikan(t): """Buang baris footer 'Halaman N' dan ratakan spasi/baris baru agar kata kunci lintas baris tetap cocok.""" t = re.sub(r"^[ \t]*Halaman \d+[ \t]*$", " ", t, flags=re.MULTILINE) return " ".join(t.split()).lower() def cocok_kata_kunci(teks, kw): """True bila SEMUA kata kunci ada di teks (tanpa peduli huruf besar/kecil dan pemisah baris).""" t = rapikan(teks) return all(k.lower() in t for k in kw) def ringkas(t, n=150): t = " ".join(t.split()) return t if len(t) <= n else t[:n] + "..." def ya(x): return "ya" if x else "tidak" def sumber(a): return f"{a['file']}#{a['no']}" if a else "-" def jalankan(indeks, daftar, k=3): hasil = [] for s in daftar: atas = query(indeks, s["q"], k) rel = [cocok_kata_kunci(a["teks"], s["kw"]) for a in atas] hasil.append((s, atas, rel)) return hasil def top1(rel): return bool(rel) and rel[0] def periksa_kunci(idx, daftar): """Validasi soal: berapa chunk di indeks yang memuat semua kata kunci, dan di file mana? Soal yang kuncinya tidak ada di chunk mana pun tidak mungkin dijawab, jadi harus ketahuan.""" hasil = [] for s in daftar: ada = [i for i, c in enumerate(idx.chunks) if cocok_kata_kunci(c, s["kw"])] file_ada = sorted({idx.meta[i]["file"] for i in ada}) hasil.append((s, len(ada), file_ada)) return hasil def peringkat_relevan(idx, s): """Peringkat chunk relevan pertama di SELURUH indeks (None bila tidak ada).""" semua = query(idx, s["q"], len(idx.chunks)) for r, a in enumerate(semua, 1): if cocok_kata_kunci(a["teks"], s["kw"]): return r return None # --------------------------------------------------------------------------- utama def main(): # ---- Langkah 1 print("=== Langkah 1: membangun indeks ===") siapkan_dokumen() 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 hilang = [f for f in DAFTAR_PDF if per_file.get(f, 0) == 0] for f, n in per_file.items(): print(f" {f}: {n} chunk") if hilang: print(" [PERINGATAN] tidak ada chunk dari: " + ", ".join(hilang)) if not idx.chunks: raise SystemExit("Indeks kosong: tidak ada teks yang terbaca dari folder dokumen/.") # Validasi soal: kunci jawaban harus benar-benar ada di korpus kunci = periksa_kunci(idx, PERTANYAAN + PARAFRASE) soal_cacat = [] for s, n, file_ada in kunci: status = f"{n} chunk memuat kunci, di {', '.join(file_ada) or '-'}" if n == 0 or s["file"] not in file_ada: soal_cacat.append(s["q"]) status += " <-- [PERINGATAN] kunci tidak ada di file yang diharapkan" print(f" soal: {s['q'][:60]}... | {status}") # ---- Langkah 2 print("\n=== 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} {sumber(a)} relevan={ok} | {ringkas(a['teks'])}") n_top1 = sum(top1(rel) for _, _, rel in h2) n_top3 = sum(any(rel) for _, _, rel in h2) n_soal = len(PERTANYAAN) # ---- 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={top1(r1)} top3 relevan={any(r1)} | {sumber(a1[0] if a1 else None)}") print(f" tfidf : top1 relevan={top1(r2)} top3 relevan={any(r2)} | {sumber(a2[0] if a2 else None)}") n_par = len(PARAFRASE) # ---- Kesimpulan: setiap klaim dihitung dari data sem_top3 = sum(any(r) for _, _, r in sem) lek_top3 = sum(any(r) for _, _, r in lek) sem_top1 = sum(top1(r) for _, _, r in sem) lek_top1 = sum(top1(r) for _, _, r in lek) kes = (f"Pada {n_soal} pertanyaan langsung, chunk peringkat 1 relevan pada {n_top1}/{n_soal} pertanyaan dan " f"salah satu dari 3 chunk teratas relevan pada {n_top3}/{n_soal}. Pada {n_par} pertanyaan parafrase, " f"backend '{idx.backend}' menemukan chunk relevan di peringkat 1 pada {sem_top1}/{n_par} dan di 3 " f"teratas pada {sem_top3}/{n_par}, sedangkan pembanding TF-IDF {lek_top1}/{n_par} dan " f"{lek_top3}/{n_par}. ") gagal = [(s, n) for (s, atas, rel), (_, n, _) in zip(h2, kunci[:n_soal]) if not top1(rel)] for s, n in gagal: r = peringkat_relevan(idx, s) if n == 0: kes += (f'Untuk "{s["q"]}", tidak ada satu chunk pun yang memuat semua kata kunci ' "(jawaban terbelah di batas chunk atau kata kunci keliru). ") elif r is None: kes += f'Untuk "{s["q"]}", chunk relevan tidak muncul sama sekali dalam peringkat indeks. ' else: kes += (f'Untuk "{s["q"]}", chunk relevan pertama baru muncul di peringkat {r} dari ' f"{len(idx.chunks)} chunk. ") if not gagal: kes += "Semua pertanyaan langsung menemukan chunk relevan di peringkat 1. " if sem_top1 == lek_top1 and sem_top3 == lek_top3: kes += ("Pada parafrase ini, hasil semantik dan TF-IDF sama, sehingga tes ini belum membedakan " "keduanya; gunakan parafrase yang tidak berbagi kata atau akar kata dengan teks untuk " "menguji kemampuan semantik. ") elif (sem_top1, sem_top3) > (lek_top1, lek_top3): kes += "Pada parafrase ini, pencarian semantik lebih unggul daripada TF-IDF. " else: kes += "Pada parafrase ini, TF-IDF justru sama baik atau lebih baik daripada pencarian semantik. " kes += (f"Korpus hanya {len(idx.chunks)} chunk dari {len(per_file)} file, sehingga satu pertanyaan " "mengubah persentase cukup besar. ") if soal_cacat: kes += "Perhatian: kunci jawaban tidak ditemukan di file yang diharapkan untuk soal: " + \ "; ".join(f'"{q}"' for q in soal_cacat) + ". " if idx.backend == "lsa": kes += ("Catatan: backend LSA hanya semantik ringan (hubungan antar kata dipelajari dari korpus yang " "kecil), sehingga parafrase yang tidak berbagi kata akan sulit. Untuk hasil lebih kuat, pasang " "sentence-transformers; kode ini otomatis memakainya.") kes = kes.strip() 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(f"{len(per_file)} PDF berada 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]) if hilang: L.p("Peringatan: tidak ada chunk dari " + ", ".join(hilang) + ".") 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 (huruf besar/kecil dan " "pemisah baris diabaikan). 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}", sumber(a), ya(ok), 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}/{n_soal} pertanyaan; ada chunk relevan di 3 teratas " f"pada {n_top3}/{n_soal}.") 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(top1(r1))} / {ya(any(r1))}", f"{ya(top1(r2))} / {ya(any(r2))}"]) L.tabel(baris, [5 * 28.35, 6 * 28.35, 3 * 28.35, 3 * 28.35]) for (s, a1, r1), _ in zip(sem, lek): if a1: L.p(f"Chunk teratas untuk \"{s['q']}\": {sumber(a1[0])} (skor {a1[0]['skor']:.3f})") L.kode(ringkas(a1[0]["teks"], 400)) L.h1("Kesimpulan") L.p(kes) L.halaman_baru() L.h1(f"Lampiran: kode {os.path.basename(__file__)}") L.kode(open(__file__, encoding="utf-8").read()) L.simpan() print("\nLaporan: Lab2_Indeks_dan_Pencarian.pdf") if __name__ == "__main__": main()