This commit is contained in:
2026-11-02 11:13:19 +07:00
commit 6a4c7fa4c6
31 changed files with 3036 additions and 0 deletions
+276
View File
@@ -0,0 +1,276 @@
"""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()