diff --git a/experiments/08_sentimen_gap.py b/experiments/08_sentimen_gap.py new file mode 100644 index 0000000..859de54 --- /dev/null +++ b/experiments/08_sentimen_gap.py @@ -0,0 +1,290 @@ +import os +os.environ["TF_USE_LEGACY_KERAS"] = "1" # Bypass Keras 3 & optree + +import re +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt + +from datasets import load_dataset + +from sklearn.model_selection import train_test_split +from sklearn.metrics import ( + classification_report, + confusion_matrix, + ConfusionMatrixDisplay +) + +import tensorflow as tf +import tf_keras as keras +from tf_keras import layers +from tf_keras.callbacks import EarlyStopping +from tf_keras.preprocessing.text import Tokenizer # type: ignore +from tf_keras.preprocessing.sequence import pad_sequences + +# ============================================================ +# KONFIGURASI DIREKTORI PENYIMPANAN +# ============================================================ + +VOCAB_SIZE = 10000 +MAX_LEN = 60 +EMBED_DIM = 64 + +EPOCHS = 20 +BATCH_SIZE = 64 +RANDOM_STATE = 42 + +FIGURES_DIR = "results/figures" +REPORTS_DIR = "results/reports" +MODELS_DIR = "results/models" + +os.makedirs(FIGURES_DIR, exist_ok=True) +os.makedirs(REPORTS_DIR, exist_ok=True) +os.makedirs(MODELS_DIR, exist_ok=True) + + +# ============================================================ +# SEL 7 — LOAD DATASET TOKOPEDIA +# ============================================================ + +print("=" * 60) +print("SEL 7 — LOAD DATASET") +print("=" * 60) + +dataset = load_dataset( + "farhamu/tokopedia-product-reviews-2019" +) + +df = dataset["train"].to_pandas() + +# Buang rating 3 +df = df[df["rating"] != 3].copy() + +# Rating 4-5 = positif (1), Rating 1-2 = negatif (0) +df["label"] = (df["rating"] >= 4).astype(int) + +# Rename text menjadi teks +df = df.rename( + columns={"text": "teks"} +)[["teks", "label"]].reset_index(drop=True) + +print(df.head(5)) +print(f"\nShape : {df.shape}") +print(f"Kolom : {df.columns.tolist()}") +print(f"\nDistribusi label:\n{df['label'].value_counts().sort_index()}") +print(f"\nNull : {df.isnull().sum().sum()}") + + +# ============================================================ +# DISTRIBUSI LABEL +# ============================================================ + +label_counts = df["label"].value_counts().sort_index() + +plt.figure(figsize=(7, 5)) +plt.bar( + ["Negatif", "Positif"], + [label_counts.get(0, 0), label_counts.get(1, 0)] +) +plt.title("Distribusi Label Sentimen Tokopedia") +plt.xlabel("Sentimen") +plt.ylabel("Jumlah Review") +plt.tight_layout() + +plt.savefig(f"{FIGURES_DIR}/distribusi_label_sentimen.png") +plt.close() + + +# ============================================================ +# CEK PANJANG TEKS +# ============================================================ + +df["panjang"] = df["teks"].apply(lambda x: len(str(x).split())) +print(f"\nPanjang teks:\n{df['panjang'].describe()}") + + +# ============================================================ +# SEL 8 — TEXT PREPROCESSING +# ============================================================ + +print("\n" + "=" * 60) +print("SEL 8 — TEXT PREPROCESSING") +print("=" * 60) + +def clean_text(teks): + teks = str(teks).lower() + teks = re.sub(r"[^a-z\s]", "", teks) + teks = re.sub(r"\s+", " ", teks).strip() + return teks + +df["teks_bersih"] = df["teks"].apply(clean_text) + +# Train/Test Split +X = np.array(df["teks_bersih"].tolist()) +y = np.array(df["label"].tolist(), dtype=int) + +X_tr, X_te, y_tr, y_te = train_test_split( + X, y, + test_size=0.2, + random_state=RANDOM_STATE, + stratify=y +) + +print(f"Training data : {len(X_tr)}") +print(f"Testing data : {len(X_te)}") + +# Tokenizer +tokenizer = Tokenizer(num_words=VOCAB_SIZE, oov_token="") +tokenizer.fit_on_texts(X_tr) + +# Text -> Integer Sequence +X_tr_seq = tokenizer.texts_to_sequences(X_tr) +X_te_seq = tokenizer.texts_to_sequences(X_te) + +# Padding +X_tr_pad = pad_sequences(X_tr_seq, maxlen=MAX_LEN, padding="post") +X_te_pad = pad_sequences(X_te_seq, maxlen=MAX_LEN, padding="post") + +print(f"\nX_tr_pad shape : {X_tr_pad.shape}") +print(f"X_te_pad shape : {X_te_pad.shape}") +print(f"Vocab size : {len(tokenizer.word_index)}") + + +# ============================================================ +# SEL 9 — MODEL GAP +# ============================================================ + +print("\n" + "=" * 60) +print("SEL 9 — MODEL GAP") +print("=" * 60) + +sent_model = keras.Sequential([ + layers.Embedding(input_dim=VOCAB_SIZE, output_dim=EMBED_DIM, input_length=MAX_LEN), + layers.GlobalAveragePooling1D(), + layers.Dense(64, activation="relu"), + layers.Dropout(0.4), + layers.Dense(1, activation="sigmoid") +], name="Sentimen_GAP") + +sent_model.compile( + optimizer="adam", + loss="binary_crossentropy", + metrics=["accuracy"] +) + +sent_model.summary() + + +# ============================================================ +# TRAINING +# ============================================================ + +print("\n" + "=" * 60) +print("TRAINING MODEL GAP") +print("=" * 60) + +early_stop = EarlyStopping( + monitor="val_loss", + patience=4, + restore_best_weights=True +) + +history_sent = sent_model.fit( + X_tr_pad, y_tr, + epochs=EPOCHS, + batch_size=BATCH_SIZE, + validation_split=0.15, + callbacks=[early_stop], + verbose=1 +) + + +# ============================================================ +# TEST ACCURACY & HISTORY PLOTS +# ============================================================ + +loss_s, acc_s = sent_model.evaluate(X_te_pad, y_te, verbose=0) +print(f"\nSentimen GAP → Test accuracy: {acc_s:.4f}") + +# Plot Accuracy & Loss +plt.figure(figsize=(8, 5)) +plt.plot(history_sent.history["accuracy"], label="Training Accuracy") +plt.plot(history_sent.history["val_accuracy"], label="Validation Accuracy") +plt.title("Training vs Validation Accuracy — GAP") +plt.xlabel("Epoch") +plt.ylabel("Accuracy") +plt.legend() +plt.tight_layout() +plt.savefig(f"{FIGURES_DIR}/histori_gap_sentimen.png") +plt.close() + + +# ============================================================ +# SEL 11 — EVALUATION +# ============================================================ + +print("\n" + "=" * 60) +print("EVALUATION") +print("=" * 60) + +y_pred_prob = sent_model.predict(X_te_pad, verbose=0) +y_pred = (y_pred_prob > 0.5).astype("int").flatten() + +report = classification_report( + y_te, y_pred, + target_names=["Negatif (0)", "Positif (1)"] +) + +print("\nClassification Report:") +print(report) + +# Simpan Classification Report Txt +with open(f"{REPORTS_DIR}/classification_report_gap_sentimen.txt", "w", encoding="utf-8") as f: + f.write(report) + +# Confusion Matrix Plot +cm = confusion_matrix(y_te, y_pred) +disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=["Negatif", "Positif"]) +fig, ax = plt.subplots(figsize=(6, 6)) +disp.plot(ax=ax, cmap="Blues") +plt.title("Confusion Matrix — Model Sentimen GAP") +plt.tight_layout() +plt.savefig(f"{FIGURES_DIR}/confusion_matrix_gap_sentimen.png") +plt.close() + + +# ============================================================ +# SEL 12 — INFERENCE (PREDIKSI 5 ULASAN) +# ============================================================ + +def prediksi_sentimen(teks, model, tokenizer, maxlen): + teks_bersih = clean_text(teks) + seq = tokenizer.texts_to_sequences([teks_bersih]) + padded = pad_sequences(seq, maxlen=maxlen, padding="post") + prob = model.predict(padded, verbose=0)[0][0] + label = "Positif" if prob > 0.5 else "Negatif" + return label, prob + +print("\n" + "=" * 60) +print("PREDIKSI 5 ULASAN") +print("=" * 60) + +ulasan_uji = [ + "Produk berkualitas tinggi, pengiriman cepat, packing aman!", + "Kecewa banget, barang datang rusak dan customer service tidak responsif", + "Lumayan sih, tapi harganya kemahalan untuk kualitas segitu", + "Tidak sesuai deskripsi sama sekali, mau return tapi ribet", + "Barang bagus, sesuai dengan foto dan penjual sangat ramah" +] + +for i, ulasan in enumerate(ulasan_uji, start=1): + label, prob = prediksi_sentimen(ulasan, sent_model, tokenizer, MAX_LEN) + print(f"\n{i}. {ulasan}\n Prediksi : {label}\n Confidence: {prob:.3f}") + + +# ============================================================ +# SIMPAN MODEL +# ============================================================ + +sent_model.save(f"{MODELS_DIR}/model_gap_sentimen.keras") +print("\nModel GAP berhasil disimpan di folder results/models/") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a8b4eed..88745dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,8 @@ scikit-learn>=1.3.0 # Scientific Computing & Data Manipulation numpy>=1.24.0 pandas>=2.0.0 +datasets>=2.0.0 # Data Visualization matplotlib>=3.7.0 -seaborn>=0.12.0 +seaborn>=0.12.0 \ No newline at end of file diff --git a/results/figures/confusion_matrix_gap_sentimen.png b/results/figures/confusion_matrix_gap_sentimen.png new file mode 100644 index 0000000..af8f2ba Binary files /dev/null and b/results/figures/confusion_matrix_gap_sentimen.png differ diff --git a/results/figures/distribusi_label_sentimen.png b/results/figures/distribusi_label_sentimen.png new file mode 100644 index 0000000..1db4212 Binary files /dev/null and b/results/figures/distribusi_label_sentimen.png differ diff --git a/results/figures/histori_gap_sentimen.png b/results/figures/histori_gap_sentimen.png new file mode 100644 index 0000000..c97fbd4 Binary files /dev/null and b/results/figures/histori_gap_sentimen.png differ diff --git a/results/models/model_gap_sentimen.keras b/results/models/model_gap_sentimen.keras new file mode 100644 index 0000000..9213c15 Binary files /dev/null and b/results/models/model_gap_sentimen.keras differ diff --git a/results/reports/classification_report_gap_sentimen.txt b/results/reports/classification_report_gap_sentimen.txt new file mode 100644 index 0000000..3361e6f --- /dev/null +++ b/results/reports/classification_report_gap_sentimen.txt @@ -0,0 +1,8 @@ + precision recall f1-score support + + Negatif (0) 0.58 0.34 0.42 185 + Positif (1) 0.98 0.99 0.99 7572 + + accuracy 0.98 7757 + macro avg 0.78 0.66 0.71 7757 +weighted avg 0.97 0.98 0.98 7757 diff --git a/results/sentiment/distribusi_label_sentimen.png b/results/sentiment/distribusi_label_sentimen.png new file mode 100644 index 0000000..1db4212 Binary files /dev/null and b/results/sentiment/distribusi_label_sentimen.png differ diff --git a/results/sentiment/history_accuracy_gap.png b/results/sentiment/history_accuracy_gap.png new file mode 100644 index 0000000..b6348a2 Binary files /dev/null and b/results/sentiment/history_accuracy_gap.png differ