initial commit

This commit is contained in:
matthewflrnt
2026-11-02 11:32:33 +07:00
commit 4f2a26e696
21 changed files with 254 additions and 0 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+78
View File
@@ -0,0 +1,78 @@
from main import koleksi_kecil, koleksi_besar
# Lab 3 — 15 pasangan (pertanyaan, kata/frasa kunci yang HARUS ada di chunk jawaban yang benar)
UJI = [
("Apa syarat cuti akademik?", "cuti akademik"),
("Berapa batas kehadiran untuk ujian?", "75 persen"),
("Berapa maksimal cuti akademik selama masa studi?", "2 semester"),
("Kapan batas pengajuan cuti akademik dilakukan?", "2 minggu"),
("Berapa SKS maksimal jika IP semester sebelumnya 3.20?", "24 SKS"),
("Berapa SKS minimal untuk kelulusan program sarjana?", "144 SKS"),
("Apa syarat IPK minimal untuk lulus program sarjana?", "2.00"),
("Berapa lama masa studi maksimal program sarjana?", "14 semester"),
("Berapa SKS minimal yang harus ditempuh untuk mengambil skripsi?", "110 SKS"),
("Berapa syarat nilai minimal mata kuliah Metodologi Penelitian?", "nilai C"),
("Berapa kali bimbingan minimal sebelum sidang skripsi?", "8 kali"),
("Apa sanksi bagi mahasiswa yang melakukan kecurangan ujian?", "nilai E"),
("Berapa lama waktu ujian susulan dapat diajukan setelah jadwal?", "3 hari"),
("Berapa nilai minimal untuk mendapatkan predikat Cum Laude?", "3.51"),
("Berapa persentase maksimal kemiripan turnitin untuk naskah skripsi?", "20 persen"),
]
def hit_at_k(uji, k=3, target_koleksi=koleksi_kecil):
if not uji:
return 0.0
benar = 0
for pertanyaan, kunci in uji:
docs = target_koleksi.query(query_texts=[pertanyaan], n_results=k)["documents"][0]
if any(kunci.lower() in d.lower() for d in docs):
benar += 1
return benar / len(uji)
def tampilkan_detail_gagal(uji, k, target_koleksi, nama_koleksi):
gagal = []
for pertanyaan, kunci in uji:
docs = target_koleksi.query(query_texts=[pertanyaan], n_results=k)["documents"][0]
if not any(kunci.lower() in d.lower() for d in docs):
gagal.append((pertanyaan, kunci))
if gagal:
print(f"\nPertanyaan yang gagal pada {nama_koleksi} (Hit@{k}):")
for p, kw in gagal:
print(f" - {p} (kunci: '{kw}')")
else:
print(f"\nSemua pertanyaan berhasil ditemukan pada {nama_koleksi} untuk Hit@{k}!")
KESIMPULAN = """
KESIMPULAN EVALUASI (LAB 3):
1. Ukuran chunk kecil (500 karakter) umumnya menghasilkan Hit@1 yang lebih presisi untuk pertanyaan spesifik (seperti angka SKS atau batas persentase), karena vektor embedding tidak terdilusi oleh topik lain di dalam satu chunk yang terlalu panjang.
2. Sebaliknya, ukuran chunk besar (1500 karakter) unggul pada Hit@3 dan Hit@5 ketika kata kunci jawaban berada agak jauh dari kalimat topik utamanya, namun rentan menurunkan akurasi Hit@1 karena satu chunk menggabungkan beberapa sub-pasal sekaligus.
3. Kegagalan retrieval biasanya terjadi pada pertanyaan yang jawabannya terpotong tepat di batas chunk (boundary split) atau pertanyaan yang menggunakan angka/istilah singkat yang mirip antar-pasal (misalnya angka semester atau SKS).
4. Solusi untuk meningkatkan performa di proyek RAG adalah menggunakan chunking berbasis struktur kalimat/paragraf dengan ukuran moderat (500–800 karakter) dan overlap 10–20% serta mengambil k=3 hingga k=5 chunk teratas.
""".strip()
if __name__ == "__main__":
if koleksi_kecil.count() == 0 or koleksi_besar.count() == 0:
print("Koleksi evaluasi masih kosong. Jalankan `python main.py` terlebih dahulu setelah mengisi folder `dokumen/`.")
else:
print("=" * 70)
print("LAB 3 — EVALUASI HIT@K PADA DUA UKURAN CHUNK BERBEDA")
print("=" * 70)
print(f"Jumlah pertanyaan uji: {len(UJI)}\n")
print(f"{'Metrik':<10} | {'Chunk 500 (Overlap 75)':<25} | {'Chunk 1500 (Overlap 225)':<25}")
print("-" * 66)
for k in (1, 3, 5):
skor_500 = hit_at_k(UJI, k=k, target_koleksi=koleksi_kecil)
skor_1500 = hit_at_k(UJI, k=k, target_koleksi=koleksi_besar)
print(f"Hit@{k:<6} | {skor_500:<25.2f} | {skor_1500:<25.2f}")
tampilkan_detail_gagal(UJI, k=1, target_koleksi=koleksi_kecil, nama_koleksi="Chunk 500")
tampilkan_detail_gagal(UJI, k=1, target_koleksi=koleksi_besar, nama_koleksi="Chunk 1500")
print("\n" + "=" * 70)
print(KESIMPULAN)
print("=" * 70)
+131
View File
@@ -0,0 +1,131 @@
import os
import random
# Gunakan cache model lokal jika sudah terunduh agar tidak lambat saat offline
os.environ.setdefault("HF_HUB_OFFLINE", "1")
os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
import chromadb
from chromadb.utils import embedding_functions
from pathlib import Path
from pypdf import PdfReader
def baca_pdf(path):
reader = PdfReader(path)
return "\n".join((p.extract_text() or "") for p in reader.pages)
def potong(teks, ukuran=800, tumpang=100):
"""Pemotongan ukuran tetap dengan overlap (versi paling sederhana)."""
hasil, mulai = [], 0
while mulai < len(teks):
hasil.append(teks[mulai:mulai + ukuran])
mulai += ukuran - tumpang
return [h for h in hasil if h.strip()]
# model embedding multilingual (jalan lokal, tanpa API key)
emb = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="paraphrase-multilingual-MiniLM-L12-v2")
client = chromadb.PersistentClient(path="./chroma_db") # tersimpan di folder
# Koleksi utama (ukuran chunk default 800, overlap 100)
koleksi = client.get_or_create_collection(
name="pedoman", embedding_function=emb,
metadata={"hnsw:space": "cosine"}) # ukuran jarak: cosine
# Dua koleksi tambahan untuk Lab 3 (perbandingan dua ukuran chunk berbeda)
koleksi_kecil = client.get_or_create_collection(
name="pedoman_chunk_500", embedding_function=emb,
metadata={"hnsw:space": "cosine"})
koleksi_besar = client.get_or_create_collection(
name="pedoman_chunk_1500", embedding_function=emb,
metadata={"hnsw:space": "cosine"})
def eksperimen_chunking(folder="dokumen"):
"""Lab 1 — Eksperimen chunking (ukuran 500, 1000, 1500 dengan overlap 15%)."""
daftar_pdf = sorted(Path(folder).glob("*.pdf"))
if not daftar_pdf:
print(f"Tidak ada file PDF di '{folder}/'. Masukkan minimal 1 PDF terlebih dahulu.")
return
# Pilih PDF dengan teks terpanjang agar ukuran 1500 pun memiliki >= 3 chunk
pdf_pertama = max(daftar_pdf, key=lambda p: len(baca_pdf(p)))
teks = baca_pdf(pdf_pertama)
print("=" * 70)
print(f"LAB 1 — EKSPERIMEN CHUNKING (Sampel: {pdf_pertama.name})")
print("=" * 70)
print("\n[1] 500 Karakter Pertama:")
print("-" * 70)
print(teks[:500])
print("-" * 70)
# Tiga pengaturan ukuran chunk (500, 1000, 1500) dengan overlap 15% (antara 10-20%)
pengaturan = [
(500, 75), # overlap 15%
(1000, 150), # overlap 15%
(1500, 225), # overlap 15%
]
random.seed(42)
print("\n[2] Perbandingan Jumlah Chunk & 3 Sampel Acak Tiap Ukuran:")
for ukuran, tumpang in pengaturan:
chunks = potong(teks, ukuran=ukuran, tumpang=tumpang)
print(f"\n>>> Ukuran = {ukuran}, Overlap = {tumpang} ({tumpang*100//ukuran}%) → Total: {len(chunks)} chunk")
sampel = random.sample(chunks, k=min(3, len(chunks)))
for idx, c in enumerate(sampel, start=1):
cuplikan = c.replace("\n", " ")
print(f" Sampel {idx}: \"{cuplikan[:120]} ... {cuplikan[-80:]}\"")
def indeks_ke_koleksi(target_koleksi, daftar_pdf, ukuran=800, tumpang=100, tampilkan_log=True):
"""Memotong semua PDF dan melakukan upsert ke koleksi ChromaDB tertentu."""
for pdf in daftar_pdf:
chunks = potong(baca_pdf(pdf), ukuran=ukuran, tumpang=tumpang)
if not chunks:
if tampilkan_log:
print(f"{pdf.name} → dilewati (tidak ada teks)")
continue
target_koleksi.upsert(
ids=[f"{pdf.stem}-{i}" for i in range(len(chunks))],
documents=chunks,
metadatas=[{"sumber": pdf.name, "chunk": i} for i in range(len(chunks))],
)
if tampilkan_log:
print(f" {pdf.name} → {len(chunks)} chunk (ukuran={ukuran}, overlap={tumpang})")
def indeks_dokumen(folder="dokumen"):
"""Lab 2 (Bagian 1) & Persiapan Lab 3 — Bangun indeks dari folder dokumen/."""
folder_path = Path(folder)
if not folder_path.exists():
folder_path.mkdir(parents=True, exist_ok=True)
print(f"Folder '{folder}/' telah dibuat. Silakan letakkan 2–3 file PDF di dalamnya.")
return
daftar_pdf = sorted(folder_path.glob("*.pdf"))
if not daftar_pdf:
print(f"Tidak ada file PDF di '{folder}/'. Silakan letakkan 2–3 file PDF di dalamnya.")
return
print("\n" + "=" * 70)
print("LAB 2 — BANGUN INDEKS CHROMADB")
print("=" * 70)
print("[Koleksi Utama: 'pedoman' (ukuran=800, overlap=100)]")
indeks_ke_koleksi(koleksi, daftar_pdf, ukuran=800, tumpang=100, tampilkan_log=True)
print("\n[Koleksi Lab 3: 'pedoman_chunk_500' (ukuran=500, overlap=75)]")
indeks_ke_koleksi(koleksi_kecil, daftar_pdf, ukuran=500, tumpang=75, tampilkan_log=True)
print("\n[Koleksi Lab 3: 'pedoman_chunk_1500' (ukuran=1500, overlap=225)]")
indeks_ke_koleksi(koleksi_besar, daftar_pdf, ukuran=1500, tumpang=225, tampilkan_log=True)
if __name__ == "__main__":
eksperimen_chunking()
indeks_dokumen()
+45
View File
@@ -0,0 +1,45 @@
from main import koleksi
# Lab 2 — 5 Pertanyaan langsung sesuai isi dokumen
PERTANYAAN_UTAMA = [
"Apa syarat cuti akademik?",
"Berapa batas kehadiran minimal untuk mengikuti ujian akhir semester?",
"Berapa beban maksimal SKS yang dapat diambil dalam satu semester?",
"Apa syarat IPK minimal untuk mengambil mata kuliah Skripsi?",
"Berapa lama batas waktu maksimal masa studi program sarjana?",
]
# Lab 2 — 2 Pertanyaan dengan kata berbeda dari teks dokumen (parafrase)
PERTANYAAN_PARAFRASE = [
# Parafrase dari "cuti akademik" -> "berhenti kuliah sementara waktu"
"Bagaimana prosedur jika mahasiswa ingin berhenti kuliah sementara waktu?",
# Parafrase dari "batas kehadiran 75 persen untuk ujian" -> "jarang masuk kelas apakah boleh ikut UAS"
"Jika mahasiswa sering absen kelas, berapa minimal presensi supaya tetap diizinkan ikut UAS?",
]
def cari_dan_tampilkan(daftar_pertanyaan, judul, n_results=3):
print("=" * 70)
print(judul)
print("=" * 70)
for idx, pertanyaan in enumerate(daftar_pertanyaan, start=1):
print(f"\nQ{idx}: {pertanyaan}")
hasil = koleksi.query(query_texts=[pertanyaan], n_results=n_results)
for rank, (doc, meta, jarak) in enumerate(
zip(hasil["documents"][0], hasil["metadatas"][0], hasil["distances"][0]),
start=1,
):
cuplikan = doc[:200].replace("\n", " ")
print(f" #{rank} [{meta['sumber']} · chunk {meta['chunk']}] jarak={jarak:.3f}")
print(f" {cuplikan} ...")
if __name__ == "__main__":
if koleksi.count() == 0:
print("Koleksi masih kosong. Jalankan `python main.py` terlebih dahulu setelah mengisi folder `dokumen/`.")
else:
cari_dan_tampilkan(PERTANYAAN_UTAMA, "LAB 2 (BAGIAN 2) — 5 PERTANYAAN PENCARIAN")
print()
cari_dan_tampilkan(PERTANYAAN_PARAFRASE, "LAB 2 (BAGIAN 3) — 2 PERTANYAAN PARAFRASE (SEMANTIC SEARCH)")