Compare commits
3
Commits
3701a40b34
...
0364638834
| 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/")
|
||||||
+2
-1
@@ -7,7 +7,8 @@ scikit-learn>=1.3.0
|
|||||||
# Scientific Computing & Data Manipulation
|
# Scientific Computing & Data Manipulation
|
||||||
numpy>=1.24.0
|
numpy>=1.24.0
|
||||||
pandas>=2.0.0
|
pandas>=2.0.0
|
||||||
|
datasets>=2.0.0
|
||||||
|
|
||||||
# Data Visualization
|
# Data Visualization
|
||||||
matplotlib>=3.7.0
|
matplotlib>=3.7.0
|
||||||
seaborn>=0.12.0
|
seaborn>=0.12.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