Commit
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,250 @@
|
|||||||
|
"""
|
||||||
|
Lab P-10: RAG Part 1 - Embedding, Chunking, Vector DB
|
||||||
|
Cara pakai:
|
||||||
|
pip install pypdf chromadb sentence-transformers
|
||||||
|
mkdir dokumen # taruh 2-3 PDF di sini
|
||||||
|
|
||||||
|
python rag_lab.py lab1 # Lab 1: eksperimen chunking
|
||||||
|
python rag_lab.py lab2 # Lab 2: indeks + 5 pertanyaan + 2 parafrase
|
||||||
|
python rag_lab.py lab3 # Lab 3: hit@k pada 2 ukuran chunk
|
||||||
|
python rag_lab.py query "pertanyaan" # tanya bebas (opsi --size 500)
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
|
||||||
|
import chromadb
|
||||||
|
from chromadb.utils import embedding_functions
|
||||||
|
from pypdf import PdfReader
|
||||||
|
|
||||||
|
FOLDER = "dokumen"
|
||||||
|
DB_PATH = "chroma_db"
|
||||||
|
# Model multibahasa supaya bahasa Indonesia & parafrase dipahami dengan baik
|
||||||
|
EMBED_MODEL = "paraphrase-multilingual-MiniLM-L12-v2"
|
||||||
|
|
||||||
|
_ef = None
|
||||||
|
_client = None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- dasar
|
||||||
|
def baca_pdf(path):
|
||||||
|
"""Baca seluruh teks PDF, rapikan spasi/baris baru."""
|
||||||
|
reader = PdfReader(path)
|
||||||
|
teks = " ".join((hal.extract_text() or "") for hal in reader.pages)
|
||||||
|
return re.sub(r"\s+", " ", teks).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def potong(teks, ukuran=500, overlap_persen=0.15):
|
||||||
|
"""Potong teks per `ukuran` karakter dengan overlap (10-20%)."""
|
||||||
|
overlap = int(ukuran * overlap_persen)
|
||||||
|
langkah = ukuran - overlap
|
||||||
|
return [teks[i:i + ukuran] for i in range(0, len(teks), langkah)
|
||||||
|
if teks[i:i + ukuran].strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def get_client():
|
||||||
|
global _client, _ef
|
||||||
|
if _client is None:
|
||||||
|
_client = chromadb.PersistentClient(path=DB_PATH)
|
||||||
|
_ef = embedding_functions.SentenceTransformerEmbeddingFunction(
|
||||||
|
model_name=EMBED_MODEL)
|
||||||
|
return _client
|
||||||
|
|
||||||
|
|
||||||
|
def daftar_pdf():
|
||||||
|
files = sorted(glob.glob(os.path.join(FOLDER, "*.pdf")))
|
||||||
|
if not files:
|
||||||
|
raise SystemExit(f"Tidak ada PDF di folder '{FOLDER}/'. Letakkan 2-3 PDF dulu.")
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- indeks
|
||||||
|
def indeks(ukuran, overlap_persen=0.15, verbose=True):
|
||||||
|
"""Bangun koleksi `dok_<ukuran>` dari semua PDF di folder dokumen/."""
|
||||||
|
client = get_client()
|
||||||
|
nama = f"dok_{ukuran}"
|
||||||
|
try:
|
||||||
|
client.delete_collection(nama)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
col = client.create_collection(nama, embedding_function=_ef,
|
||||||
|
metadata={"hnsw:space": "cosine"})
|
||||||
|
for path in daftar_pdf():
|
||||||
|
chunks = potong(baca_pdf(path), ukuran, overlap_persen)
|
||||||
|
fname = os.path.basename(path)
|
||||||
|
col.add(
|
||||||
|
ids=[f"{fname}-{i}" for i in range(len(chunks))],
|
||||||
|
documents=chunks,
|
||||||
|
metadatas=[{"sumber": fname, "chunk": i} for i in range(len(chunks))],
|
||||||
|
)
|
||||||
|
if verbose:
|
||||||
|
print(f" [{nama}] {fname}: {len(chunks)} chunk")
|
||||||
|
return col
|
||||||
|
|
||||||
|
|
||||||
|
def get_collection(ukuran):
|
||||||
|
client = get_client()
|
||||||
|
try:
|
||||||
|
return client.get_collection(f"dok_{ukuran}", embedding_function=_ef)
|
||||||
|
except Exception:
|
||||||
|
return indeks(ukuran, verbose=False)
|
||||||
|
|
||||||
|
|
||||||
|
def query(pertanyaan, k=3, ukuran=500, tampil=True):
|
||||||
|
col = get_collection(ukuran)
|
||||||
|
hasil = col.query(query_texts=[pertanyaan], n_results=k)
|
||||||
|
docs = hasil["documents"][0]
|
||||||
|
metas = hasil["metadatas"][0]
|
||||||
|
jarak = hasil["distances"][0]
|
||||||
|
if tampil:
|
||||||
|
print(f"\nTanya: {pertanyaan}")
|
||||||
|
for rank, (d, m, j) in enumerate(zip(docs, metas, jarak), 1):
|
||||||
|
print(f" #{rank} [{m['sumber']} chunk {m['chunk']}] jarak={j:.3f}")
|
||||||
|
print(f" {d[:200]}...")
|
||||||
|
return docs
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- lab 1
|
||||||
|
def lab1():
|
||||||
|
path = daftar_pdf()[0]
|
||||||
|
teks = baca_pdf(path)
|
||||||
|
print(f"=== LAB 1 | file: {path} | total {len(teks)} karakter ===")
|
||||||
|
print("\n[1] 500 karakter pertama:\n")
|
||||||
|
print(teks[:500])
|
||||||
|
|
||||||
|
# --- Tabel 1: ringkasan jumlah chunk
|
||||||
|
print("\n[2] Jumlah chunk (overlap 15%)\n")
|
||||||
|
hasil = {}
|
||||||
|
print(f"{'Ukuran':>7} | {'Overlap':>7} | {'Jml chunk':>9} | {'Rata2 panjang':>13} | {'Berakhir titik':>14}")
|
||||||
|
print("-" * 64)
|
||||||
|
for ukuran in (500, 1000, 1500):
|
||||||
|
chunks = potong(teks, ukuran, 0.15)
|
||||||
|
hasil[ukuran] = chunks
|
||||||
|
rata = sum(len(c) for c in chunks) / len(chunks)
|
||||||
|
titik = sum(c.rstrip().endswith(".") for c in chunks)
|
||||||
|
print(f"{ukuran:>7} | {int(ukuran*0.15):>7} | {len(chunks):>9} | "
|
||||||
|
f"{rata:>13.0f} | {titik:>8}/{len(chunks):<5}")
|
||||||
|
|
||||||
|
# --- Tabel 2: 3 chunk acak per ukuran
|
||||||
|
print("\n[3] 3 chunk acak per ukuran (cek: terpotong di tengah kalimat/tabel?)\n")
|
||||||
|
print(f"{'Ukuran':>7} | {'Awal chunk (30 krk)':<32} | {'Akhir chunk (30 krk)':<32} | Titik?")
|
||||||
|
print("-" * 85)
|
||||||
|
for ukuran, chunks in hasil.items():
|
||||||
|
for c in random.sample(chunks, min(3, len(chunks))):
|
||||||
|
awal = c[:30].ljust(30)
|
||||||
|
akhir = c.rstrip()[-30:].rjust(30)
|
||||||
|
ya = "Ya" if c.rstrip().endswith(".") else "Tidak"
|
||||||
|
print(f"{ukuran:>7} | {awal:<32} | ..{akhir:<30} | {ya}")
|
||||||
|
print("-" * 85)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- lab 2
|
||||||
|
PERTANYAAN_LAB2 = [
|
||||||
|
"Wahana apa yang pertama kali mengirim gambar jarak dekat Mars?",
|
||||||
|
"Siapa yang mengusulkan Tes Turing?",
|
||||||
|
"Berapa target porsi energi terbarukan Indonesia tahun 2025?",
|
||||||
|
"Apa itu Narrow AI dan contohnya?",
|
||||||
|
"Apa saja tantangan pengembangan energi terbarukan?",
|
||||||
|
]
|
||||||
|
PERTANYAAN_PARAFRASE = [
|
||||||
|
# tidak memakai kata yang sama dengan dokumen
|
||||||
|
"Pesawat robotik pertama yang memotret planet merah dari dekat",
|
||||||
|
"Sumber listrik dari perut bumi yang jalan nonstop tanpa peduli cuaca",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def lab2():
|
||||||
|
print("=== LAB 2 | Bangun indeks (ukuran 500) ===")
|
||||||
|
indeks(500)
|
||||||
|
print("\n--- 5 pertanyaan: periksa manual apakah chunk teratas relevan ---")
|
||||||
|
for q in PERTANYAAN_LAB2:
|
||||||
|
query(q, k=3, ukuran=500)
|
||||||
|
print("\n--- 2 pertanyaan parafrase: apakah pencarian semantik tetap menemukan? ---")
|
||||||
|
for q in PERTANYAAN_PARAFRASE:
|
||||||
|
query(q, k=3, ukuran=500)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- lab 3
|
||||||
|
# (pertanyaan, [kata kunci jawaban]) -> hit jika SALAH SATU kata kunci ada di chunk
|
||||||
|
# Sesuaikan jika dokumen Anda berbeda.
|
||||||
|
SOAL_UJI = [
|
||||||
|
("Wahana apa yang pertama mengirim gambar jarak dekat Mars?", ["mariner 4"]),
|
||||||
|
("Kapan Viking 1 dan 2 mendarat di Mars?", ["1976"]),
|
||||||
|
("Apa nama rover pertama yang beroda di Mars?", ["sojourner"]),
|
||||||
|
("Apa temuan utama Curiosity?", ["bahan organik", "kawah gale"]),
|
||||||
|
("Apa program Mars Sample Return?", ["mars sample return", "msr"]),
|
||||||
|
("Siapa yang mengusulkan Tes Turing?", ["alan turing"]),
|
||||||
|
("Siapa yang mencetuskan istilah Artificial Intelligence pada 1956?", ["john mccarthy"]),
|
||||||
|
("Sebutkan contoh Narrow AI.", ["siri", "alexa"]),
|
||||||
|
("Siapa yang dikalahkan IBM Deep Blue?", ["kasparov"]),
|
||||||
|
("Siapa yang dikalahkan AlphaGo?", ["lee sedol"]),
|
||||||
|
("Berapa target bauran EBT nasional tahun 2025?", ["23%"]),
|
||||||
|
("Berapa persen cadangan panas bumi dunia yang dimiliki Indonesia?", ["40%"]),
|
||||||
|
("Berapa potensi tenaga surya Indonesia?", ["207,8"]),
|
||||||
|
("Berapa potensi tenaga air (hidro) Indonesia?", ["75,0"]),
|
||||||
|
("Apa tantangan pengembangan energi terbarukan di Indonesia?",
|
||||||
|
["investasi awal", "intermitensi", "transmisi"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def hit_at_k(ukuran, k):
|
||||||
|
col = get_collection(ukuran)
|
||||||
|
hit, gagal = 0, []
|
||||||
|
for q, kunci in SOAL_UJI:
|
||||||
|
docs = col.query(query_texts=[q], n_results=k)["documents"][0]
|
||||||
|
gabung = " ".join(docs).lower()
|
||||||
|
if any(kw in gabung for kw in kunci):
|
||||||
|
hit += 1
|
||||||
|
else:
|
||||||
|
gagal.append(q)
|
||||||
|
return hit / len(SOAL_UJI), gagal
|
||||||
|
|
||||||
|
|
||||||
|
def lab3():
|
||||||
|
ukuran_list = (500, 1000) # ubah ke (500, 1500) jika mau
|
||||||
|
print("=== LAB 3 | hit@k pada dua ukuran chunk ===")
|
||||||
|
for u in ukuran_list:
|
||||||
|
indeks(u, verbose=False)
|
||||||
|
|
||||||
|
print(f"\n{'ukuran':>8} | " + " | ".join(f"hit@{k}" for k in (1, 3, 5)))
|
||||||
|
print("-" * 36)
|
||||||
|
gagal_semua = {}
|
||||||
|
for u in ukuran_list:
|
||||||
|
baris = []
|
||||||
|
for k in (1, 3, 5):
|
||||||
|
skor, gagal = hit_at_k(u, k)
|
||||||
|
baris.append(f"{skor:.2f}")
|
||||||
|
if k == 1:
|
||||||
|
gagal_semua[u] = gagal
|
||||||
|
print(f"{u:>8} | " + " | ".join(f"{b:>5}" for b in baris))
|
||||||
|
|
||||||
|
print("\nPertanyaan yang GAGAL pada k=1:")
|
||||||
|
for u, g in gagal_semua.items():
|
||||||
|
print(f" ukuran {u}:")
|
||||||
|
for q in g or ["(tidak ada)"]:
|
||||||
|
print(f" - {q}")
|
||||||
|
print("\nTugas Anda: tulis kesimpulan 3-4 kalimat (ukuran mana lebih baik, "
|
||||||
|
"pertanyaan apa gagal, dugaan penyebab).")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- main
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("perintah", choices=["lab1", "lab2", "lab3", "query"])
|
||||||
|
ap.add_argument("teks", nargs="?", help="pertanyaan (untuk perintah query)")
|
||||||
|
ap.add_argument("--size", type=int, default=500)
|
||||||
|
ap.add_argument("-k", type=int, default=3)
|
||||||
|
a = ap.parse_args()
|
||||||
|
|
||||||
|
if a.perintah == "lab1":
|
||||||
|
lab1()
|
||||||
|
elif a.perintah == "lab2":
|
||||||
|
lab2()
|
||||||
|
elif a.perintah == "lab3":
|
||||||
|
lab3()
|
||||||
|
else:
|
||||||
|
if not a.teks:
|
||||||
|
raise SystemExit('Contoh: python rag_lab.py query "Siapa Alan Turing?"')
|
||||||
|
query(a.teks, k=a.k, ukuran=a.size)
|
||||||
Reference in New Issue
Block a user