Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0364638834 | ||
|
|
1e8e7e39dd | ||
|
|
8f8acbc24b |
@@ -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="<OOV>")
|
||||
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/")
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Reference in New Issue
Block a user