From 0d282059304ea2a61cdc0c1cea7dc493c85b089e Mon Sep 17 00:00:00 2001 From: "Muhammad.haekal Rahmatullah" Date: Mon, 2 Nov 2026 01:41:24 -0500 Subject: [PATCH] Upload files to "/" --- Indeks_Gapeka.py | 128 ++++++++++++++++++++++++ LAPORAN GAPEKA 2026.docx | Bin 0 -> 23636 bytes cari_gapeka.py | 146 +++++++++++++++++++++++++++ evaluasi_gapeka.py | 206 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 480 insertions(+) create mode 100644 Indeks_Gapeka.py create mode 100644 LAPORAN GAPEKA 2026.docx create mode 100644 cari_gapeka.py create mode 100644 evaluasi_gapeka.py diff --git a/Indeks_Gapeka.py b/Indeks_Gapeka.py new file mode 100644 index 0000000..b93dad8 --- /dev/null +++ b/Indeks_Gapeka.py @@ -0,0 +1,128 @@ +import random +from pathlib import Path + +# FUNGSI MEMBACA PDF +def baca_pdf(path): + reader = PdfReader(path) + return "\n".join( + (p.extract_text() or "") + for p in reader.pages + ) + +# FUNGSI MEMOTONG TEKS (CHUNKING) +def potong(teks, ukuran=800, tumpang=100): + 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()] + +# MENCARI PDF DI FOLDER DOKUMEN +daftar_pdf = list(Path("dokumen").glob("*.pdf")) + +if not daftar_pdf: + print("❌ Tidak ada file PDF di folder 'dokumen'! Pastikan Anda membuat folder bernama 'dokumen' dan memasukkan file PDF terkait KAI di dalamnya.") +else: + # Mengambil PDF pertama + pdf = daftar_pdf[0] + + # Membaca isi PDF + teks = baca_pdf(pdf) + + print(f"\n📄 Nama PDF: {pdf.name}") + print(f"📊 Total karakter: {len(teks)}") + + # Menampilkan 500 karakter pertama + print("\n===== 500 KARAKTER PERTAMA =====") + print(teks[:500]) + + # Eksperimen tiga ukuran chunk + for ukuran in (500, 1000, 1500): + overlap = int(ukuran * 0.1) + chunks = potong( + teks, + ukuran=ukuran, + tumpang=overlap + ) + + print("\n" + "=" * 50) + print(f"UKURAN CHUNK: {ukuran}") + print(f"OVERLAP: {overlap}") + print(f"JUMLAH CHUNK: {len(chunks)}") + + # Mengambil maksimal 3 chunk secara acak + sampel = random.sample( + range(len(chunks)), + min(3, len(chunks)) + ) + + for nomor in sampel: + print(f"\n--- CHUNK {nomor} ---") + print(chunks[nomor]) + + +# LAB 2: EMBEDDING DAN CHROMADB +if daftar_pdf: + # Model embedding multilingual + emb = embedding_functions.SentenceTransformerEmbeddingFunction( + model_name="paraphrase-multilingual-MiniLM-L12-v2" + ) + + # Membuat database lokal + client = chromadb.PersistentClient(path="./chroma_db") + + def bangun_indeks(nama_koleksi, ukuran): + # Menghapus koleksi lama agar tidak tercampur + try: + client.delete_collection(name=nama_koleksi) + except Exception: + pass + + # Membuat koleksi baru + koleksi = client.create_collection( + name=nama_koleksi, + embedding_function=emb, + metadata={"hnsw:space": "cosine"} + ) + + for pdf in daftar_pdf: + teks = baca_pdf(pdf) + overlap = int(ukuran * 0.1) + chunks = potong( + teks, + ukuran=ukuran, + tumpang=overlap + ) + + if not chunks: + print(pdf.name, "tidak memiliki teks.") + continue + + koleksi.upsert( + ids=[ + f"{pdf.stem}-{ukuran}-{i}" + for i in range(len(chunks)) + ], + documents=chunks, + metadatas=[ + { + "sumber": pdf.name, + "chunk": i, + "ukuran": ukuran + } + for i in range(len(chunks)) + ] + ) + + print(pdf.name, "->", len(chunks), "chunk") + + print(f"\n✅ Koleksi '{nama_koleksi}' berhasil dibuat.") + print("Total data dalam koleksi:", koleksi.count()) + + # Membuat dua koleksi untuk eksperimen + bangun_indeks("pedoman_500", 500) + bangun_indeks("pedoman_1000", 1000) + +else: + print("⚠️ Indeks gagal dibuat karena tidak ada file PDF.") \ No newline at end of file diff --git a/LAPORAN GAPEKA 2026.docx b/LAPORAN GAPEKA 2026.docx new file mode 100644 index 0000000000000000000000000000000000000000..b97be7e16a92f7e57a561989ab92ba418fecd5a7 GIT binary patch literal 23636 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 mzl{H$bAQDnw0^;VW?)qiCMvuDsK4Rj1JF 0 else 0 + ) + + vector[word] = term_freq * idf + return vector + + def _cosine_similarity(self, vec1, vec2): + """Menghitung kemiripan kosinus antara dua vektor TF-IDF.""" + intersection = set(vec1.keys()).intersection(set(vec2.keys())) + numerator = sum(vec1[x] * vec2[x] for x in intersection) + + sum1 = sum(val ** 2 for val in vec1.values()) + sum2 = sum(val ** 2 for val in vec2.values()) + denominator = math.sqrt(sum1) * math.sqrt(sum2) + + if denominator == 0: + return 0.0 + return numerator / denominator + + def retrieve(self, query, top_k=1): + """Mencari dokumen paling relevan menggunakan Cosine Similarity.""" + query_vector = self._compute_tfidf(query, self.vocab, self.documents) + scores = [] + + for i, doc_vec in enumerate(self.doc_vectors): + score = self._cosine_similarity(query_vector, doc_vec) + scores.append((score, self.documents[i])) + + # Urutkan berdasarkan skor tertinggi + scores.sort(key=lambda x: x[0], reverse=True) + return [doc for score, doc in scores[:top_k]] + + def generate_answer(self, query): + """Tahap Generation: Menggabungkan hasil retrieval dengan format jawaban RAG.""" + retrieved_docs = self.retrieve(query, top_k=1) + context = ( + retrieved_docs[0] + if retrieved_docs + else "Maaf, informasi tidak ditemukan." + ) + + # Template Generator Extractive RAG + answer = ( + f"==================================================\n" + f"HASIL GENERASI RAG - PT KERETA API INDONESIA (GAPEKA 2026)\n" + f"==================================================\n" + f"Pertanyaan : {query}\n\n" + f"Konteks Relevan (Retrieved):\n-> {context}\n\n" + f"Jawaban Sistem:\n" + f"Berdasarkan informasi resmi kebijakan GAPEKA 2026 PT KAI, " + f"berikut adalah detail informasinya:\n" + f'"{context}"\n' + f"==================================================" + ) + return answer, retrieved_docs + + +# ========================================== +# EKSEKUSI PROGRAM UTAMA +# ========================================== +if __name__ == "__main__": + # 1. Dataset / Basis Pengetahuan Lokal KAI GAPEKA 2026 + kai_kb = [ + ( + "PT Kereta Api Indonesia (KAI) resmi memberlakukan Grafik Perjalanan " + "Kereta Api (GAPEKA) 2026 mulai tanggal 1 Desember 2026." + ), + ( + "Tiket perjalanan kereta api untuk jadwal GAPEKA 2026 sudah dapat " + "dipesan secara online melalui aplikasi Access by KAI." + ), + ( + "Terdapat penyesuaian serta penyederhanaan nama pada beberapa " + "perjalanan kereta api agar lebih simpel dan mudah diingat oleh " + "penumpang." + ), + ( + "KAI menambah titik pemberhentian di sejumlah stasiun untuk " + "mendukung mobilitas antarkota dan kawasan aglomerasi." + ), + ( + "KAI menghadirkan layanan rute baru seperti KA Borobudur " + "(Lempuyangan–Gambir PP, fakultatif) dan KA Pandawa " + "(Yogyakarta–Semarang Tawang PP) yang mulai beroperasi pada 1 " + "Desember 2026." + ), + ] + + # 2. Inisialisasi Sistem RAG + rag = KAIGapekaRAG(kai_kb) + + # 3. Contoh Daftar Pengujian Pertanyaan Tugas RAG + daftar_pertanyaan = [ + "Kapan GAPEKA 2026 mulai diberlakukan dan di mana bisa pesan tiketnya?", + "Apa saja rute kereta api baru yang dihadirkan KAI dalam GAPEKA 2026?", + "Bagaimana kebijakan terkait nama kereta api dalam GAPEKA 2026?" + ] + + # 4. Menjalankan Pipeline Pengujian + for idx, sample_query in enumerate(daftar_pertanyaan, 1): + print(f"\n[PENGUJIAN {idx}]") + final_answer, sources = rag.generate_answer(sample_query) + print(final_answer) \ No newline at end of file diff --git a/evaluasi_gapeka.py b/evaluasi_gapeka.py new file mode 100644 index 0000000..354dfc1 --- /dev/null +++ b/evaluasi_gapeka.py @@ -0,0 +1,206 @@ +import math +import re +from collections import Counter + + +class KAIGapekaRAG: + """Sistem RAG Sederhana berbasis Pure Python untuk topik GAPEKA 2026 PT KAI.""" + + def __init__(self, knowledge_base): + print("[INFO] Menginisialisasi Basis Pengetahuan KAI Gapeka 2026...") + self.documents = knowledge_base + self.vocab = self._build_vocab(self.documents) + # Pra-penghitungan vektor TF-IDF untuk dokumen lokal + self.doc_vectors = [ + self._compute_tfidf(doc, self.vocab, self.documents) + for doc in self.documents + ] + + def _tokenize(self, text): + """Membersihkan teks dan memecahnya menjadi kata-kata (token).""" + return re.findall(r"\b\w+\b", text.lower()) + + def _build_vocab(self, docs): + """Membangun kamus kata unik dari seluruh dokumen.""" + vocab = set() + for doc in docs: + vocab.update(self._tokenize(doc)) + return sorted(list(vocab)) + + def _compute_tfidf(self, text, vocab, all_docs): + """Menghitung bobot TF-IDF secara manual tanpa library eksternal.""" + tokens = self._tokenize(text) + tf = Counter(tokens) + total_tokens = len(tokens) if tokens else 1 + + vector = {} + for word in vocab: + # Term Frequency (TF) + term_freq = tf[word] / total_tokens + + # Inverse Document Frequency (IDF) + doc_count = sum(1 for d in all_docs if word in self._tokenize(d)) + idf = ( + math.log(len(all_docs) / (doc_count + 1)) + 1 if doc_count > 0 else 0 + ) + + vector[word] = term_freq * idf + return vector + + def _cosine_similarity(self, vec1, vec2): + """Menghitung kemiripan kosinus antara dua vektor TF-IDF.""" + intersection = set(vec1.keys()).intersection(set(vec2.keys())) + numerator = sum(vec1[x] * vec2[x] for x in intersection) + + sum1 = sum(val ** 2 for val in vec1.values()) + sum2 = sum(val ** 2 for val in vec2.values()) + denominator = math.sqrt(sum1) * math.sqrt(sum2) + + if denominator == 0: + return 0.0 + return numerator / denominator + + def retrieve_with_scores(self, query, top_k=5): + """Mengembalikan daftar dokumen dan skornya berdasarkan Cosine Similarity.""" + query_vector = self._compute_tfidf(query, self.vocab, self.documents) + scores = [] + + for i, doc_vec in enumerate(self.doc_vectors): + score = self._cosine_similarity(query_vector, doc_vec) + scores.append((score, self.documents[i])) + + # Urutkan berdasarkan skor tertinggi + scores.sort(key=lambda x: x[0], reverse=True) + return scores[:top_k] + + def generate_answer(self, query): + """Tahap Generation: Menggabungkan hasil retrieval dengan format jawaban RAG.""" + retrieved_scores = self.retrieve_with_scores(query, top_k=1) + context = ( + retrieved_scores[0][1] + if retrieved_scores and retrieved_scores[0][0] > 0 + else "Maaf, informasi tidak ditemukan." + ) + + answer = ( + f"==================================================\n" + f"HASIL GENERASI RAG - PT KERETA API INDONESIA (GAPEKA 2026)\n" + f"==================================================\n" + f"Pertanyaan : {query}\n\n" + f"Konteks Relevan (Retrieved):\n-> {context}\n\n" + f"Jawaban Sistem:\n" + f"Berdasarkan informasi resmi kebijakan GAPEKA 2026 PT KAI, " + f"berikut adalah detail informasinya:\n" + f'"{context}"\n' + f"==================================================" + ) + return answer, context + + +# ========================================== +# MODUL EVALUASI RETRIEVAL (Hit@K & MRR) +# ========================================== + +# Daftar Uji (Pertanyaan dan kata kunci yang harus ada di dokumen yang diretrubusi) +UJI = [ + ("Kapan GAPEKA 2026 mulai diberlakukan?", "1 desember 2026"), + ("Di mana bisa pesan tiket kereta GAPEKA 2026?", "access by kai"), + ("Apa ada perubahan nama kereta api?", "penyederhanaan nama"), + ("Apakah ada penambahan titik pemberhentian stasiun?", "titik pemberhentian"), + ("Sebutkan rute kereta api baru yang beroperasi!", "ka borobudur"), +] + + +def cari_peringkat(rag_system, pertanyaan, kunci, k=5): + """Mencari peringkat dokumen pertama yang mengandung kata kunci.""" + hasil = rag_system.retrieve_with_scores(pertanyaan, top_k=k) + kunci_bersih = " ".join(kunci.lower().split()) + + for peringkat, (score, doc) in enumerate(hasil, start=1): + doc_bersih = " ".join(doc.lower().split()) + if kunci_bersih in doc_bersih and score > 0: + return peringkat + return None + + +def hit_at_k(rag_system, uji_data, k=3): + """Menghitung metrik Hit@K.""" + benar = 0 + for pertanyaan, kunci in uji_data: + peringkat = cari_peringkat(rag_system, pertanyaan, kunci, k) + if peringkat is not None: + benar += 1 + return benar / len(uji_data) + + +def hitung_mrr(rag_system, uji_data): + """Menghitung metrik Mean Reciprocal Rank (MRR@5).""" + total = 0 + for pertanyaan, kunci in uji_data: + peringkat = cari_peringkat(rag_system, pertanyaan, kunci, 5) + if peringkat is not None: + total += 1 / peringkat + return total / len(uji_data) + + +# ========================================== +# EKSEKUSI PROGRAM UTAMA +# ========================================== +if __name__ == "__main__": + # 1. Dataset / Basis Pengetahuan Lokal KAI GAPEKA 2026 + kai_kb = [ + ( + "PT Kereta Api Indonesia (KAI) resmi memberlakukan Grafik Perjalanan " + "Kereta Api (GAPEKA) 2026 mulai tanggal 1 Desember 2026." + ), + ( + "Tiket perjalanan kereta api untuk jadwal GAPEKA 2026 sudah dapat " + "dipesan secara online melalui aplikasi Access by KAI." + ), + ( + "Terdapat penyesuaian serta penyederhanaan nama pada beberapa " + "perjalanan kereta api agar lebih simpel dan mudah diingat oleh penumpang." + ), + ( + "KAI menambah titik pemberhentian di sejumlah stasiun untuk " + "mendukung mobilitas antarkota dan kawasan aglomerasi." + ), + ( + "KAI menghadirkan layanan rute baru seperti KA Borobudur " + "(Lempuyangan–Gambir PP, fakultatif) dan KA Pandawa " + "(Yogyakarta–Semarang Tawang PP) yang mulai beroperasi pada 1 Desember 2026." + ), + ] + + # 2. Inisialisasi Sistem RAG + rag = KAIGapekaRAG(kai_kb) + + # 3. Contoh Uji Jalankan Pipeline RAG Tunggal + sample_query = "Kapan GAPEKA 2026 mulai diberlakukan dan di mana bisa pesan tiketnya?" + final_answer, sources = rag.generate_answer(sample_query) + print("\n" + final_answer) + + # 4. Evaluasi Performa Retrieval (Hit@K & MRR) + print("\n" + "=" * 50) + print("EVALUASI PERFORMA RETRIEVAL KAI GAPEKA 2026") + print("=" * 50) + print(f"Jumlah Pertanyaan Uji : {len(UJI)}") + + for k in (1, 3, 5): + nilai_hit = hit_at_k(rag, UJI, k) + print(f"Hit@{k} = {nilai_hit:.2f}") + + mrr_score = hitung_mrr(rag, UJI) + print(f"MRR@5 = {mrr_score:.2f}") + + print("\nPERTANYAAN YANG GAGAL DITEMUKAN (Jika ada):") + gagal = False + for pertanyaan, kunci in UJI: + peringkat = cari_peringkat(rag, pertanyaan, kunci, 5) + if peringkat is None: + gagal = True + print(f"- {pertanyaan}") + + if not gagal: + print("Semua pertanyaan berhasil ditemukan oleh sistem retrieval!") + print("=" * 50) \ No newline at end of file