1
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user