Upload files to "/"

This commit is contained in:
2026-11-01 23:33:36 -05:00
parent 269265f94a
commit 3d73a6cea2
5 changed files with 278 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

+278
View File
@@ -0,0 +1,278 @@
import hashlib
import random
import re
from pathlib import Path
import chromadb
from chromadb.utils import embedding_functions
from langchain_text_splitters import RecursiveCharacterTextSplitter
from pypdf import PdfReader
BASE_DIR = Path(__file__).resolve().parent
DOCUMENT_DIR = BASE_DIR / "dokumen"
DB_DIR = BASE_DIR / "chroma_db_data"
CHUNK_SIZES = (500, 1000, 1500)
OVERLAP_RATIO = 0.1
EMBEDDING_MODEL = "paraphrase-multilingual-MiniLM-L12-v2"
# Ubah pertanyaan dan kata kunci agar sesuai dengan dokumen yang diuji.
EVALUATION_QUESTIONS = [
("Berapa SKS maksimal jika IP semester sebelumnya di atas 3?", "24 SKS"),
("Berapa tingkat kehadiran minimum untuk mengikuti UAS?", "75%"),
("Di mana mahasiswa merencanakan studi setiap awal semester?", "SIAKAD"),
("Apa bukti yang harus diserahkan untuk ketidakhadiran karena sakit?", "surat keterangan resmi"),
("Kapan surat keterangan sakit atau izin harus diserahkan?", "tiga hari"),
("Berapa semester masa studi aktif yang harus ditempuh sebelum mengajukan cuti?", "minimal 2"),
("Apa yang diperlukan jika mahasiswa penerima beasiswa ingin mengajukan cuti?", "surat izin tertulis"),
("Melalui apa formulir cuti akademik diajukan?", "portal SIAKAD"),
("Seberapa awal cuti harus diajukan sebelum registrasi semester baru?", "2 minggu"),
("Kewajiban apa saja yang harus dilunasi sebelum cuti?", "uang kuliah"),
("Siapa yang harus menyetujui permohonan cuti akademik?", "Dosen Pembimbing Akademik"),
("Berapa lama cuti akademik boleh diambil selama masa studi?", "maksimal 2"),
("Apakah masa cuti dihitung dalam batas waktu evaluasi kelulusan?", "tidak akan diperhitungkan"),
("Berapa minimal SKS yang umumnya harus diselesaikan untuk lulus sarjana?", "144 SKS"),
("Berapa IPK minimum untuk dinyatakan lulus?", "2.00"),
]
# Lima pertanyaan langsung dan dua parafrasa untuk Lab 2.
RETRIEVAL_QUERIES = [
"Apa syarat untuk mengajukan cuti akademik?",
"Berapa batas kehadiran untuk bisa mengikuti ujian akhir?",
"Berapa jumlah SKS yang dapat diambil mahasiswa dengan IP tinggi?",
"Apa persyaratan untuk dinyatakan lulus sarjana?",
"Apa yang harus dilakukan mahasiswa pada awal semester?",
"Bagaimana cara berhenti kuliah sementara secara resmi?", # Parafrasa cuti akademik
"Berapa banyak kredit kuliah yang dapat diambil jika nilai semester bagus?", # Parafrasa SKS
]
def find_pdf_files():
files = []
handbook = BASE_DIR / "Buku Pedoman Akademik.pdf"
if handbook.is_file():
files.append(handbook)
if DOCUMENT_DIR.is_dir():
files.extend(DOCUMENT_DIR.glob("*.pdf"))
unique_files = sorted({path.resolve() for path in files})
if not unique_files:
raise FileNotFoundError(
"PDF tidak ditemukan. Letakkan 2-3 PDF berbasis teks di folder "
f"'{DOCUMENT_DIR.name}' atau sediakan 'Buku Pedoman Akademik.pdf'."
)
if len(unique_files) > 3:
raise ValueError(
f"Ditemukan {len(unique_files)} PDF. Lab menggunakan 2-3 PDF; "
f"pilih maksimal 3 PDF di '{DOCUMENT_DIR.name}'."
)
if len(unique_files) < 2:
print(
"PERHATIAN: baru ditemukan 1 PDF. Untuk memenuhi Lab 2, tambahkan "
"1-2 PDF lain ke folder 'dokumen'."
)
return unique_files
def read_pdf(path):
reader = PdfReader(str(path))
text = "\n".join((page.extract_text() or "") for page in reader.pages).strip()
if not text:
raise ValueError(
f"Tidak ada teks yang dapat diekstrak dari '{path.name}'. "
"Pastikan PDF bukan hasil scan, atau lakukan OCR terlebih dahulu."
)
return text
def make_chunks(text, size):
splitter = RecursiveCharacterTextSplitter(
chunk_size=size,
chunk_overlap=int(size * OVERLAP_RATIO),
length_function=len,
)
return splitter.split_text(text)
def show_chunking_experiment(documents):
print("\n" + "=" * 70)
print("LAB 1 - EKSPERIMEN CHUNKING")
print("=" * 70)
for path, text in documents.items():
print(f"\nPDF: {path.name}")
print("500 karakter pertama:")
print(text[:500] or "(tidak ada teks)")
for size in CHUNK_SIZES:
chunks = make_chunks(text, size)
print(f"\nUkuran {size}, overlap {int(size * OVERLAP_RATIO)}: "
f"{len(chunks)} chunk")
sample = random.Random(size).sample(chunks, min(3, len(chunks)))
for index, chunk in enumerate(sample, start=1):
print(f" Contoh {index} ({len(chunk)} karakter):")
print(f" {chunk[:350]}{'...' if len(chunk) > 350 else ''}")
print(
"\nPeriksa contoh di atas: catat apakah kalimat, tabel, atau daftar "
"terpotong pada batas chunk."
)
def create_indexes(client, embedding_function, documents):
collections = {}
chunks_by_size = {}
for size in CHUNK_SIZES:
collection_name = f"rag_koleksi_chunk_{size}"
existing_names = {collection.name for collection in client.list_collections()}
if collection_name in existing_names:
client.delete_collection(name=collection_name)
collection = client.create_collection(
name=collection_name,
embedding_function=embedding_function,
metadata={"hnsw:space": "cosine"},
)
all_chunks = []
all_ids = []
all_metadatas = []
for path, text in documents.items():
chunks = make_chunks(text, size)
source = path.relative_to(BASE_DIR).as_posix()
source_hash = hashlib.sha1(source.encode("utf-8")).hexdigest()[:10]
all_chunks.extend(chunks)
all_ids.extend(
f"{source_hash}-{index}" for index in range(len(chunks))
)
all_metadatas.extend(
{
"source": source,
"chunk_size": size,
"chunk_index": index,
}
for index in range(len(chunks))
)
chunks_by_size.setdefault(size, {})[source] = chunks
print(f" {source}: {len(chunks)} chunk (ukuran {size})")
if all_chunks:
collection.add(
documents=all_chunks,
metadatas=all_metadatas,
ids=all_ids,
)
collections[size] = collection
return collections, chunks_by_size
def show_retrieval_queries(collection):
print("\n" + "=" * 70)
print("LAB 2 - BANGUN INDEKS & UJI PENCARIAN")
print("=" * 70)
print("Hasil berikut memakai koleksi chunk 1000 dan menampilkan top-3.")
for query in RETRIEVAL_QUERIES:
results = collection.query(query_texts=[query], n_results=3)
print(f"\nPertanyaan: {query}")
for rank, (document, metadata, distance) in enumerate(
zip(
results["documents"][0],
results["metadatas"][0],
results["distances"][0],
),
start=1,
):
print(
f" Top {rank} | jarak={distance:.4f} | "
f"{metadata['source']} | chunk {metadata['chunk_index']}"
)
print(f" {document[:350]}{'...' if len(document) > 350 else ''}")
print(
"\nPeriksa relevansi hasil top-1. Dua pertanyaan terakhir adalah "
"parafrasa; catat apakah tetap menemukan potongan yang tepat."
)
def normalize(text):
return re.sub(r"\s+", " ", text.lower()).strip()
def hit_at_k(collection, questions, k):
hits = 0
misses = []
for question, keyword in questions:
results = collection.query(query_texts=[question], n_results=k)
documents = results["documents"][0] or []
if any(normalize(keyword) in normalize(document) for document in documents):
hits += 1
else:
misses.append(question)
return hits / len(questions), misses
def evaluate_indexes(collections):
print("\n" + "=" * 70)
print("LAB 3 - EVALUASI HIT@K")
print("=" * 70)
print(f"Jumlah pertanyaan uji: {len(EVALUATION_QUESTIONS)}")
scores = {}
misses_at_three = {}
for size, collection in collections.items():
scores[size] = {}
for k in (1, 3, 5):
score, misses = hit_at_k(collection, EVALUATION_QUESTIONS, k)
scores[size][k] = score
if k == 3:
misses_at_three[size] = misses
print(f"Chunk {size:>4} | Hit@{k} = {score:.2f}")
best_size = max(scores, key=lambda size: scores[size][3])
failed_questions = misses_at_three[best_size]
print("\nBahan kesimpulan:")
print(
f"- Hit@3 tertinggi diperoleh ukuran chunk {best_size} "
f"({scores[best_size][3]:.2f})."
)
if failed_questions:
print(
"- Pertanyaan yang belum menemukan kata kunci pada top-3: "
+ "; ".join(failed_questions)
)
else:
print("- Semua kata kunci jawaban ditemukan pada top-3 ukuran terbaik.")
print(
"- Hit@k berbasis kata kunci adalah evaluasi praktis; periksa hasil "
"secara manual karena kata kunci yang muncul belum tentu menjawab "
"pertanyaan dengan benar."
)
def main():
pdf_files = find_pdf_files()
documents = {path: read_pdf(path) for path in pdf_files}
print("PDF yang akan diproses:")
for path in pdf_files:
print(f"- {path.relative_to(BASE_DIR)}")
show_chunking_experiment(documents)
print("\nMenyiapkan model embedding multilingual...")
embedding_function = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name=EMBEDDING_MODEL
)
client = chromadb.PersistentClient(path=str(DB_DIR))
print("\nMembangun indeks ChromaDB dengan metrik cosine...")
collections, _ = create_indexes(client, embedding_function, documents)
show_retrieval_queries(collections[1000])
evaluate_indexes(collections)
if __name__ == "__main__":
main()