refactor: restrukturisasi folder eksperimen ke tasks modular

This commit is contained in:
matthewflrnt
2026-09-27 18:50:08 +07:00
parent e827aa0e96
commit fe63a50997
9 changed files with 112 additions and 60 deletions
+148
View File
@@ -0,0 +1,148 @@
"""
Tugas No. 4 - Implementasi CNN sederhana untuk MNIST
dan perbandingannya dengan MLP dari Tugas No. 1.
"""
import os
import sys
import time
# Sembunyikan log informasi TensorFlow
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
import matplotlib
matplotlib.use("Agg") # simpan plot sebagai file
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf
# Tambahkan root directory ke path agar dapat import dari src
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
from src.data_loader import load_mnist_for_cnn
from src.models import create_cnn_mnist_model
from src.utils import EpochTimerCallback, ensure_dir
def main():
# Konfigurasi
SEED = 42
EPOCHS = 10
BATCH_SIZE = 128
VAL_SPLIT = 0.1
tf.random.set_seed(SEED)
np.random.seed(SEED)
# Hasil MLP terbaik dari Tugas No. 1 (Model C: 256-128-64)
MLP_ACCURACY = 0.9764
MLP_PARAMS = 242_762
MLP_EPOCH_TIME = 2.50
# 1. Load dan preprocessing data
print("Memuat dataset MNIST untuk CNN...")
(x_train, y_train), (x_test, y_test) = load_mnist_for_cnn()
print(f"Data training : {x_train.shape[0]:,} gambar")
print(f"Data testing : {x_test.shape[0]:,} gambar")
print(f"Ukuran gambar : {x_train.shape[1]} x {x_train.shape[2]} piksel (channel={x_train.shape[3]})\n")
# 2. Bangun model CNN
model = create_cnn_mnist_model()
model.summary()
# 3. Training dengan pencatatan waktu per epoch
timer_cb = EpochTimerCallback()
print("\nMemulai training CNN...")
print("Perkiraan waktu: 15-20 detik per epoch di CPU (total sekitar 3 menit).\n")
history = model.fit(
x_train,
y_train,
validation_split=VAL_SPLIT,
epochs=EPOCHS,
batch_size=BATCH_SIZE,
verbose=2,
callbacks=[timer_cb],
)
# 4. Evaluasi
test_loss, test_acc = model.evaluate(x_test, y_test, verbose=0)
n_params = model.count_params()
avg_epoch_time = float(np.mean(timer_cb.epoch_times)) if timer_cb.epoch_times else 0.0
predictions = np.argmax(model.predict(x_test, verbose=0), axis=1)
n_wrong = int(np.sum(predictions != y_test))
# 5. Perbandingan CNN vs MLP
error_mlp = (1 - MLP_ACCURACY) * 100
error_cnn = (1 - test_acc) * 100
gain_point = (test_acc - MLP_ACCURACY) * 100
error_reduction = (1 - error_cnn / error_mlp) * 100
print("\n" + "=" * 62)
print("HASIL EKSPERIMEN CNN")
print("=" * 62)
print(f"Test accuracy : {test_acc * 100:.2f}%")
print(f"Test loss : {test_loss:.4f}")
print(f"Jumlah parameter : {n_params:,}")
print(f"Waktu training/epoch : {avg_epoch_time:.2f} detik")
print(f"Salah klasifikasi : {n_wrong} dari {len(y_test):,} gambar")
print("\n" + "=" * 62)
print("PERBANDINGAN CNN vs MLP")
print("=" * 62)
print(f"{'Metrik':<24}{'MLP':>14}{'CNN':>14}")
print("-" * 62)
print(f"{'Test accuracy':<24}{MLP_ACCURACY * 100:>13.2f}%{test_acc * 100:>13.2f}%")
print(f"{'Error rate':<24}{error_mlp:>13.2f}%{error_cnn:>13.2f}%")
print(f"{'Jumlah parameter':<24}{MLP_PARAMS:>14,}{n_params:>14,}")
print(f"{'Waktu/epoch (detik)':<24}{MLP_EPOCH_TIME:>14.2f}{avg_epoch_time:>14.2f}")
print("-" * 62)
print(f"Improvement akurasi : +{gain_point:.2f} poin persentase")
print(f"Pengurangan error rate : {error_reduction:.1f}% (relatif)")
print(f"Selisih parameter : {n_params - MLP_PARAMS:+,}")
print("=" * 62)
# 6. Simpan grafik ke folder results/02_cnn/figures
results_fig_dir = os.path.join("results", "02_cnn", "figures")
ensure_dir(results_fig_dir)
fig_path = os.path.join(results_fig_dir, "hasil_cnn.png")
fig, axes = plt.subplots(1, 2, figsize=(12, 4.5))
epochs_range = range(1, EPOCHS + 1)
axes[0].plot(epochs_range, history.history["accuracy"], label="Train accuracy")
axes[0].plot(epochs_range, history.history["val_accuracy"], label="Validation accuracy")
axes[0].set_title("Learning Curve CNN")
axes[0].set_xlabel("Epoch")
axes[0].set_ylabel("Accuracy")
axes[0].legend()
axes[0].grid(alpha=0.3)
axes[1].bar(
["MLP terbaik", "CNN"],
[MLP_ACCURACY * 100, test_acc * 100],
color=["#55A868", "#C44E52"],
)
axes[1].set_ylim(96, 100)
axes[1].set_ylabel("Test Accuracy (%)")
axes[1].set_title("Perbandingan MLP vs CNN")
for i, value in enumerate([MLP_ACCURACY * 100, test_acc * 100]):
axes[1].text(i, value + 0.05, f"{value:.2f}%", ha="center")
plt.tight_layout()
plt.savefig(fig_path, dpi=130)
print(f"\n[INFO] Grafik disimpan di: {fig_path}")
# 7. Simpan model terlatih ke folder results/02_cnn/models
results_model_dir = os.path.join("results", "02_cnn", "models")
ensure_dir(results_model_dir)
model_path = os.path.join(results_model_dir, "model_cnn_mnist.keras")
model.save(model_path)
print(f"[INFO] Model disimpan di: {model_path}")
if __name__ == "__main__":
main()
+195
View File
@@ -0,0 +1,195 @@
"""
Tugas 1 (Week 2): Pipeline CNN MNIST Lengkap
Memenuhi seluruh poin instruksi:
(a) Model Summary
(b) Loss & Accuracy curve training vs validation
(c) Confusion Matrix pada test set
(d) Perbandingan accuracy MLP vs CNN dan analisis efisiensi parameter
"""
import os
import sys
# Sembunyikan log TensorFlow yang berlebihan
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
from sklearn.metrics import classification_report, confusion_matrix
import tensorflow as tf
# Tambahkan root directory ke path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
from src.data_loader import load_mnist_for_cnn
from src.models import create_cnn_mnist_model
from src.utils import EpochTimerCallback, ensure_dir
def main():
SEED = 42
EPOCHS = 10
BATCH_SIZE = 128
VAL_SPLIT = 0.1
tf.random.set_seed(SEED)
np.random.seed(SEED)
# Baseline MLP Terbaik (Model C dari Tugas 1 & 2)
MLP_ACCURACY = 0.9824
MLP_PARAMS = 244_554
MLP_EPOCH_TIME = 2.50
print("=" * 70)
print("TUGAS 1: PIPELINE CNN MNIST LENGKAP")
print("=" * 70)
# 1. Load Data
print("\n[1/5] Memuat dataset MNIST untuk CNN...")
(x_train, y_train), (x_test, y_test) = load_mnist_for_cnn()
print(f" -> Data training : {x_train.shape[0]:,} gambar ({x_train.shape[1]}x{x_train.shape[2]}x{x_train.shape[3]})")
print(f" -> Data testing : {x_test.shape[0]:,} gambar")
# 2. Model Summary (Poin A)
print("\n[2/5] Membangun Arsitektur CNN MNIST...")
model = create_cnn_mnist_model()
print("\n" + "-" * 70)
print("(a) MODEL SUMMARY:")
print("-" * 70)
model.summary()
# 3. Training Model
print("\n[3/5] Memulai proses training (10 Epochs)...")
timer_cb = EpochTimerCallback()
history = model.fit(
x_train,
y_train,
validation_split=VAL_SPLIT,
epochs=EPOCHS,
batch_size=BATCH_SIZE,
verbose=1,
callbacks=[timer_cb],
)
avg_epoch_time = float(np.mean(timer_cb.epoch_times)) if timer_cb.epoch_times else 0.0
# 4. Evaluasi & Prediksi pada Test Set
print("\n[4/5] Evaluasi pada Test Set (10,000 data)...")
test_loss, test_acc = model.evaluate(x_test, y_test, verbose=0)
y_pred_probs = model.predict(x_test, verbose=0)
y_pred = np.argmax(y_pred_probs, axis=1)
print(f" -> Test Accuracy : {test_acc * 100:.2f}%")
print(f" -> Test Loss : {test_loss:.4f}")
# 5. Visualisasi & Penyimpanan
figures_dir = os.path.join("results", "02_cnn", "figures")
ensure_dir(figures_dir)
# (b) Loss & Accuracy Curve
print("\n[5/5] Membuat visualisasi...")
curve_path = os.path.join(figures_dir, "loss_accuracy_cnn_lengkap.png")
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
epochs_range = range(1, EPOCHS + 1)
# Loss Curve
axes[0].plot(epochs_range, history.history["loss"], "b-o", label="Training Loss")
axes[0].plot(epochs_range, history.history["val_loss"], "r--s", label="Validation Loss")
axes[0].set_title("(b1) Loss Curve: Training vs Validation", fontsize=12, fontweight="bold")
axes[0].set_xlabel("Epoch", fontsize=10)
axes[0].set_ylabel("Cross-Entropy Loss", fontsize=10)
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Accuracy Curve
axes[1].plot(epochs_range, history.history["accuracy"], "b-o", label="Training Accuracy")
axes[1].plot(epochs_range, history.history["val_accuracy"], "r--s", label="Validation Accuracy")
axes[1].set_title("(b2) Accuracy Curve: Training vs Validation", fontsize=12, fontweight="bold")
axes[1].set_xlabel("Epoch", fontsize=10)
axes[1].set_ylabel("Accuracy", fontsize=10)
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig(curve_path, dpi=150)
plt.close()
print(f" -> (b) Kurva Loss & Accuracy tersimpan: {curve_path}")
# (c) Confusion Matrix pada Test Set
cm_path = os.path.join(figures_dir, "confusion_matrix_cnn.png")
cm = confusion_matrix(y_test, y_pred)
plt.figure(figsize=(9, 7.5))
sns.heatmap(
cm,
annot=True,
fmt="d",
cmap="Blues",
cbar=True,
xticklabels=range(10),
yticklabels=range(10),
annot_kws={"size": 10}
)
plt.title("(c) Confusion Matrix CNN pada MNIST Test Set (10.000 Gambar)", fontsize=13, fontweight="bold", pad=12)
plt.xlabel("Predicted Digit", fontsize=11, labelpad=8)
plt.ylabel("True Digit", fontsize=11, labelpad=8)
plt.tight_layout()
plt.savefig(cm_path, dpi=150)
plt.close()
print(f" -> (c) Confusion Matrix tersimpan: {cm_path}")
# (d) Perbandingan Kuantitatif MLP vs CNN
n_params_cnn = model.count_params()
diff_acc = (test_acc - MLP_ACCURACY) * 100
param_saving = (1 - n_params_cnn / MLP_PARAMS) * 100
error_mlp = (1 - MLP_ACCURACY) * 100
error_cnn = (1 - test_acc) * 100
rel_error_red = (1 - error_cnn / error_mlp) * 100
print("\n" + "=" * 70)
print("(d) PERBANDINGAN ACCURACY & EFISIENSI: MLP vs CNN")
print("=" * 70)
print(f"{'Metrik Evaluasi':<28}{'MLP Terbaik':>18}{'CNN Sederhana':>18}")
print("-" * 70)
print(f"{'Test Accuracy':<28}{MLP_ACCURACY * 100:>17.2f}%{test_acc * 100:>17.2f}%")
print(f"{'Error Rate':<28}{error_mlp:>17.2f}%{error_cnn:>17.2f}%")
print(f"{'Jumlah Parameter':<28}{MLP_PARAMS:>18,}{n_params_cnn:>18,}")
print(f"{'Waktu Training / Epoch':<28}{MLP_EPOCH_TIME:>16.2f}s{avg_epoch_time:>16.2f}s")
print("-" * 70)
print(f"-> Improvement Akurasi : +{diff_acc:.2f}% poin persentase")
print(f"-> Pengurangan Error : {rel_error_red:.1f}% penurunan error relatif")
print(f"-> Efisiensi Parameter : CNN hemat {param_saving:.1f}% bobot dibanding MLP")
print("=" * 70)
# Analisis Singkat Tertulis
print("\n" + "=" * 70)
print("ANALISIS: EFISIENSI PARAMETER & PERFORMA (MLP vs CNN)")
print("=" * 70)
print(
f"1. Jumlah Parameter:\n"
f" Model CNN memiliki {n_params_cnn:,} parameter, sedangkan MLP terbaik memiliki {MLP_PARAMS:,} parameter.\n"
f" CNN menggunakan {param_saving:.1f}% parameter lebih sedikit (sekitar separuh dari MLP).\n\n"
f"2. Mana yang Lebih Efisien?\n"
f" CNN jauh lebih efisien. Meskipun memiliki parameter yang jauh lebih sedikit, CNN menghasilkan\n"
f" akurasi {test_acc * 100:.2f}% (lebih tinggi dari MLP {MLP_ACCURACY * 100:.2f}%).\n\n"
f"3. Mengapa demikian?\n"
f" - Parameter Sharing: Filter konvolusi berukuran kecil (3x3) digunakan secara berulang di seluruh citra,\n"
f" sehingga tidak membutuhkan koneksi bobot penuh antar setiap piksel seperti Dense layer pada MLP.\n"
f" - Preservasi Spasial: CNN memproses data dalam representasi 2D asli (lebar x tinggi x channel),\n"
f" menjaga hubungan antar-piksel tetangga, tepi, dan kontur angka tulisan tangan."
)
print("=" * 70 + "\n")
# Simpan bobot terbaru
models_dir = os.path.join("results", "02_cnn", "models")
ensure_dir(models_dir)
model.save(os.path.join(models_dir, "model_cnn_mnist.keras"))
print(f"[INFO] Model terbaru disimpan di: {os.path.join(models_dir, 'model_cnn_mnist.keras')}")
if __name__ == "__main__":
main()
+197
View File
@@ -0,0 +1,197 @@
"""
Tugas 2 (Week 2): Eksplorasi Kedalaman Arsitektur CNN
Memodifikasi CNN dengan menambahkan satu blok Conv2D + MaxPooling ketiga.
Mengevaluasi:
1. Apakah model yang lebih dalam menghasilkan akurasi lebih tinggi?
2. Bagaimana perbandingan waktu training per epoch?
3. Catatan temuan dalam satu paragraf analitis.
"""
import os
import sys
import time
# Sembunyikan log TensorFlow yang berlebihan
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf
# Tambahkan root directory ke path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
from src.data_loader import load_mnist_for_cnn
from src.models import create_cnn_mnist_model, create_deep_cnn_mnist_model
from src.utils import EpochTimerCallback, ensure_dir
def main():
SEED = 42
EPOCHS = 10
BATCH_SIZE = 128
VAL_SPLIT = 0.1
tf.random.set_seed(SEED)
np.random.seed(SEED)
print("=" * 75)
print("TUGAS 2: EKSPLORASI KEDALAMAN ARSITEKTUR CNN (2 BLOK vs 3 BLOK)")
print("=" * 75)
# 1. Load Data
print("\nMemuat dataset MNIST...")
(x_train, y_train), (x_test, y_test) = load_mnist_for_cnn()
architectures = [
{
"name": "CNN 2 Blok (Baseline)",
"builder": create_cnn_mnist_model,
"desc": "Conv(32) + MaxPool -> Conv(64) + MaxPool -> Dense(64)"
},
{
"name": "CNN 3 Blok (Lebih Dalam)",
"builder": create_deep_cnn_mnist_model,
"desc": "Conv(32) + MaxPool -> Conv(64) + MaxPool -> Conv(128) + MaxPool -> Dense(64)"
}
]
results = []
histories = {}
for arch in architectures:
model_name = arch["name"]
print("\n" + "=" * 75)
print(f"PELATIHAN: {model_name}")
print(f"Konfigurasi: {arch['desc']}")
print("=" * 75)
model = arch["builder"]()
model.summary()
timer_cb = EpochTimerCallback()
start_train_time = time.time()
history = model.fit(
x_train,
y_train,
validation_split=VAL_SPLIT,
epochs=EPOCHS,
batch_size=BATCH_SIZE,
verbose=1,
callbacks=[timer_cb]
)
total_train_time = time.time() - start_train_time
avg_epoch_time = float(np.mean(timer_cb.epoch_times)) if timer_cb.epoch_times else 0.0
# Evaluasi pada test set
test_loss, test_acc = model.evaluate(x_test, y_test, verbose=0)
n_params = model.count_params()
results.append({
"name": model_name,
"params": n_params,
"test_acc": test_acc * 100,
"test_loss": test_loss,
"epoch_time": avg_epoch_time,
"total_time": total_train_time,
"model_obj": model
})
histories[model_name] = history
# Simpan model 3 blok
models_dir = os.path.join("results", "02_cnn", "models")
ensure_dir(models_dir)
results[1]["model_obj"].save(os.path.join(models_dir, "model_cnn_3blocks.keras"))
# Tabel Perbandingan Kuantitatif
print("\n\n" + "=" * 75)
print("HASIL PERBANDINGAN: KEDALAMAN CNN PADA MNIST")
print("=" * 75)
print(f"{'Arsitektur Model':<28}{'Parameter':>14}{'Test Acc':>14}{'Waktu/Epoch':>18}")
print("-" * 75)
for r in results:
print(f"{r['name']:<28}{r['params']:>14,}{r['test_acc']:>13.2f}%{r['epoch_time']:>16.2f}s")
print("-" * 75)
diff_acc = results[1]["test_acc"] - results[0]["test_acc"]
time_increase = ((results[1]["epoch_time"] - results[0]["epoch_time"]) / results[0]["epoch_time"]) * 100
param_diff = results[1]["params"] - results[0]["params"]
print(f"Selisih Akurasi (3 Blok vs 2 Blok) : {diff_acc:+.2f}% poin")
print(f"Perubahan Waktu Training / Epoch : {time_increase:+.1f}%")
print(f"Selisih Jumlah Parameter : {param_diff:+,} parameter")
print("=" * 75)
# Visualisasi
figures_dir = os.path.join("results", "02_cnn", "figures")
ensure_dir(figures_dir)
fig_path = os.path.join(figures_dir, "perbandingan_kedalaman_cnn.png")
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
epochs_range = range(1, EPOCHS + 1)
# Plot Accuracy Perbandingan
for name, hist in histories.items():
axes[0].plot(epochs_range, hist.history["val_accuracy"], label=f"{name} (Val Acc)", linewidth=2)
axes[0].set_title("Validation Accuracy per Epoch", fontsize=12, fontweight="bold")
axes[0].set_xlabel("Epoch", fontsize=10)
axes[0].set_ylabel("Accuracy", fontsize=10)
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Bar chart perbandingan Waktu vs Akurasi
labels = [r["name"] for r in results]
accs = [r["test_acc"] for r in results]
times = [r["epoch_time"] for r in results]
x = np.arange(len(labels))
width = 0.35
ax2 = axes[1].twinx()
p1 = axes[1].bar(x - width/2, accs, width, label="Test Accuracy (%)", color="#3498db")
p2 = ax2.bar(x + width/2, times, width, label="Waktu / Epoch (s)", color="#e67e22")
axes[1].set_ylabel("Accuracy (%)", color="#3498db", fontsize=10)
ax2.set_ylabel("Detik per Epoch", color="#e67e22", fontsize=10)
axes[1].set_xticks(x)
axes[1].set_xticklabels(labels, fontsize=10)
axes[1].set_ylim(97, 100)
axes[1].set_title("Trade-off: Test Accuracy vs Waktu Training", fontsize=12, fontweight="bold")
# Nilai di atas bar
for bar in p1:
axes[1].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.05, f"{bar.get_height():.2f}%", ha='center', va='bottom', fontsize=9)
for bar in p2:
ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.2, f"{bar.get_height():.2f}s", ha='center', va='bottom', fontsize=9)
plt.tight_layout()
plt.savefig(fig_path, dpi=150)
plt.close()
print(f"\n[INFO] Grafik perbandingan tersimpan di: {fig_path}")
# Catatan Analisis Satu Paragraf (Sesuai Instruksi Tugas 2)
print("\n" + "=" * 75)
print("CATATAN TEMUAN (SATU PARAGRAF ANALISIS):")
print("=" * 75)
paragraf = (
f"Penambahan blok ketiga (Conv2D 128 filter + MaxPooling2D) pada citra MNIST (28x28 piksel) "
f"menghasilkan akurasi test sebesar {results[1]['test_acc']:.2f}% dibandingkan dengan {results[0]['test_acc']:.2f}% "
f"pada baseline 2 blok (selisih {diff_acc:+.2f}% poin), namun meningkatkan waktu komputasi training "
f"sebesar {time_increase:+.1f}% per epoch ({results[1]['epoch_time']:.2f}s vs {results[0]['epoch_time']:.2f}s) "
f"serta menambah kompleksitas parameter. Pada dataset sederhana dengan resolusi spasial kecil seperti MNIST, "
f"tiga kali operasi pooling mereduksi resolusi spasial representasi secara drastis (hingga 3x3 piksel), "
f"sehingga arsitektur yang lebih dalam mengalami fenomena diminishing returns di mana penambahan layer "
f"tidak memberikan peningkatan performa yang sebanding dengan biaya komputasi yang dikeluarkan; "
f"oleh karena itu, arsitektur CNN 2 blok tetap menjadi pilihan paling optimal dan efisien untuk MNIST."
)
print(paragraf)
print("=" * 75 + "\n")
if __name__ == "__main__":
main()
+302
View File
@@ -0,0 +1,302 @@
"""
Tugas Anggota 2 (Week 2): CNN Evaluation + Visualization
=========================================================
PENTING - KONSISTENSI KONFIGURASI DENGAN ANGGOTA 1:
Skrip ini TIDAK melatih ulang model dari nol. Sebagai gantinya, skrip ini
memuat bobot model CNN yang sudah dilatih oleh Anggota 1
(results/models/model_cnn_mnist.keras) pada 05_cnn_full_pipeline.py.
Ini adalah cara paling aman untuk menjamin "tidak ada konfigurasi berbeda"
antara Anggota 1 dan Anggota 2, karena:
- Dataset yang sama -> load_mnist_for_cnn() (src/data_loader.py)
- Preprocessing yang sama -> normalisasi 0-1, reshape (28, 28, 1)
- Epoch yang sama -> 10 epoch (bobot hasil training Anggota 1)
- Batch size yang sama -> 128 (dipakai saat training oleh Anggota 1)
- Optimizer yang sama -> Adam (tersimpan di dalam file .keras)
Konstanta di bawah HANYA didokumentasikan ulang (bukan dipakai untuk
melatih ulang) agar terlihat eksplisit bahwa konfigurasi cocok dengan
skrip Anggota 1.
Cakupan tugas (sesuai skema kelompok):
(1) Evaluasi menyeluruh pada test set (classification report per kelas)
(2) Visualisasi confusion matrix (raw & ternormalisasi/persentase)
(3) Visualisasi akurasi per kelas (digit mana yang paling sulit)
(4) Visualisasi distribusi confidence prediksi (benar vs salah)
(5) Visualisasi contoh gambar yang salah diklasifikasikan
"""
import os
import sys
# Sembunyikan log TensorFlow yang berlebihan
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn.metrics import classification_report, confusion_matrix
import tensorflow as tf
from tensorflow import keras
# Tambahkan root directory ke path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
from src.data_loader import load_mnist_for_cnn
from src.utils import ensure_dir
# ---------------------------------------------------------------------------
# KONFIGURASI (harus identik dengan Anggota 1 / 05_cnn_full_pipeline.py)
# ---------------------------------------------------------------------------
SEED = 42
EPOCHS = 10 # epoch training model (didokumentasikan, tidak dipakai ulang)
BATCH_SIZE = 128 # batch size training model (didokumentasikan, tidak dipakai ulang)
OPTIMIZER = "adam" # optimizer training model (tersimpan di file .keras)
MODEL_PATH = os.path.join("results", "02_cnn", "models", "model_cnn_mnist.keras")
FIGURES_DIR = os.path.join("results", "02_cnn", "figures")
REPORTS_DIR = os.path.join("results", "02_cnn", "reports")
CLASS_NAMES = [str(i) for i in range(10)]
def main():
tf.random.set_seed(SEED)
np.random.seed(SEED)
ensure_dir(FIGURES_DIR)
ensure_dir(REPORTS_DIR)
print("=" * 75)
print("TUGAS ANGGOTA 2: CNN EVALUATION + VISUALIZATION")
print("=" * 75)
print(
f"[KONFIGURASI - selaras dengan Anggota 1] "
f"seed={SEED} | epochs={EPOCHS} | batch_size={BATCH_SIZE} | optimizer={OPTIMIZER}"
)
# 1. Load Data (dataset & preprocessing sama persis dengan Anggota 1)
print("\n[1/6] Memuat dataset MNIST (load_mnist_for_cnn - sama dengan Anggota 1)...")
(x_train, y_train), (x_test, y_test) = load_mnist_for_cnn()
print(f" -> Data testing : {x_test.shape[0]:,} gambar {x_test.shape[1:]}")
# 2. Load Model Terlatih dari Anggota 1
print(f"\n[2/6] Memuat model terlatih dari: {MODEL_PATH}")
if not os.path.exists(MODEL_PATH):
raise FileNotFoundError(
f"Model tidak ditemukan di '{MODEL_PATH}'. "
f"Jalankan dahulu tasks/cnn/05_cnn_full_pipeline.py (skrip Anggota 1)."
)
model = keras.models.load_model(MODEL_PATH)
print(f" -> Model '{model.name}' berhasil dimuat ({model.count_params():,} parameter).")
# 3. Evaluasi Umum pada Test Set
print("\n[3/6] Mengevaluasi model pada test set...")
test_loss, test_acc = model.evaluate(x_test, y_test, verbose=0)
y_pred_probs = model.predict(x_test, verbose=0)
y_pred = np.argmax(y_pred_probs, axis=1)
y_confidence = np.max(y_pred_probs, axis=1)
is_correct = (y_pred == y_test)
print(f" -> Test Accuracy : {test_acc * 100:.2f}%")
print(f" -> Test Loss : {test_loss:.4f}")
print(f" -> Total Salah : {np.sum(~is_correct):,} dari {len(y_test):,} gambar")
# -----------------------------------------------------------------
# (1) Classification Report per Kelas
# -----------------------------------------------------------------
print("\n[4/6] Membuat classification report per kelas...")
report_dict = classification_report(
y_test, y_pred, target_names=CLASS_NAMES, digits=4, output_dict=True
)
report_text = classification_report(y_test, y_pred, target_names=CLASS_NAMES, digits=4)
print("\n" + "-" * 75)
print("CLASSIFICATION REPORT (Precision / Recall / F1-Score per Digit):")
print("-" * 75)
print(report_text)
report_txt_path = os.path.join(REPORTS_DIR, "classification_report_cnn.txt")
with open(report_txt_path, "w") as f:
f.write("CLASSIFICATION REPORT - CNN MNIST (Anggota 2: CNN Evaluation)\n")
f.write(f"Test Accuracy: {test_acc * 100:.2f}% | Test Loss: {test_loss:.4f}\n")
f.write("=" * 65 + "\n")
f.write(report_text)
print(f" -> Laporan teks tersimpan: {report_txt_path}")
report_df = pd.DataFrame(report_dict).transpose()
report_csv_path = os.path.join(REPORTS_DIR, "classification_report_cnn.csv")
report_df.to_csv(report_csv_path)
print(f" -> Laporan CSV tersimpan : {report_csv_path}")
# Visualisasi precision/recall/f1 per kelas (bar chart berkelompok)
per_class_df = report_df.loc[CLASS_NAMES, ["precision", "recall", "f1-score"]]
fig, ax = plt.subplots(figsize=(11, 5))
per_class_df.plot(kind="bar", ax=ax, width=0.75,
color=["#3498db", "#2ecc71", "#e74c3c"])
ax.set_title("Precision, Recall, dan F1-Score per Digit (CNN)", fontsize=13, fontweight="bold")
ax.set_xlabel("Digit", fontsize=11)
ax.set_ylabel("Skor", fontsize=11)
ax.set_ylim(0.90, 1.005)
ax.set_xticklabels(CLASS_NAMES, rotation=0)
ax.legend(["Precision", "Recall", "F1-Score"], loc="lower right")
ax.grid(True, axis="y", alpha=0.3)
plt.tight_layout()
metrics_bar_path = os.path.join(FIGURES_DIR, "per_class_metrics_cnn.png")
plt.savefig(metrics_bar_path, dpi=150)
plt.close()
print(f" -> Grafik metrik per kelas tersimpan: {metrics_bar_path}")
# -----------------------------------------------------------------
# (2) Confusion Matrix: Raw Count & Ternormalisasi (%)
# -----------------------------------------------------------------
print("\n[5/6] Membuat visualisasi confusion matrix (raw & normalisasi)...")
cm = confusion_matrix(y_test, y_pred)
cm_norm = cm.astype("float") / cm.sum(axis=1, keepdims=True) * 100
fig, axes = plt.subplots(1, 2, figsize=(17, 7))
sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", cbar=True,
xticklabels=CLASS_NAMES, yticklabels=CLASS_NAMES,
annot_kws={"size": 9}, ax=axes[0])
axes[0].set_title("Confusion Matrix (Jumlah Gambar)", fontsize=12, fontweight="bold")
axes[0].set_xlabel("Predicted Digit")
axes[0].set_ylabel("True Digit")
sns.heatmap(cm_norm, annot=True, fmt=".1f", cmap="Oranges", cbar=True,
xticklabels=CLASS_NAMES, yticklabels=CLASS_NAMES,
annot_kws={"size": 9}, ax=axes[1])
axes[1].set_title("Confusion Matrix (Persentase per Baris / Recall %)", fontsize=12, fontweight="bold")
axes[1].set_xlabel("Predicted Digit")
axes[1].set_ylabel("True Digit")
plt.suptitle("Evaluasi CNN pada MNIST Test Set (10.000 Gambar)", fontsize=14, fontweight="bold", y=1.02)
plt.tight_layout()
cm_dual_path = os.path.join(FIGURES_DIR, "confusion_matrix_cnn_detail.png")
plt.savefig(cm_dual_path, dpi=150, bbox_inches="tight")
plt.close()
print(f" -> Confusion matrix detail tersimpan: {cm_dual_path}")
# -----------------------------------------------------------------
# (3) Akurasi per Kelas (diagonal confusion matrix ternormalisasi)
# -----------------------------------------------------------------
per_class_acc = np.diag(cm_norm)
fig, ax = plt.subplots(figsize=(10, 5))
colors = ["#e74c3c" if a < per_class_acc.mean() else "#2ecc71" for a in per_class_acc]
bars = ax.bar(CLASS_NAMES, per_class_acc, color=colors)
ax.axhline(per_class_acc.mean(), color="gray", linestyle="--", linewidth=1.5,
label=f"Rata-rata: {per_class_acc.mean():.2f}%")
for bar, acc in zip(bars, per_class_acc):
ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() - 1.2,
f"{acc:.1f}%", ha="center", va="top", fontsize=9, color="white", fontweight="bold")
ax.set_title("Akurasi per Digit pada Test Set (CNN)", fontsize=13, fontweight="bold")
ax.set_xlabel("Digit")
ax.set_ylabel("Akurasi (%)")
ax.set_ylim(min(per_class_acc) - 5, 100.5)
ax.legend()
ax.grid(True, axis="y", alpha=0.3)
plt.tight_layout()
per_class_acc_path = os.path.join(FIGURES_DIR, "per_class_accuracy_cnn.png")
plt.savefig(per_class_acc_path, dpi=150)
plt.close()
print(f" -> Grafik akurasi per kelas tersimpan: {per_class_acc_path}")
hardest_idx = np.argsort(per_class_acc)[:3]
print(" -> 3 digit paling sulit dikenali:")
for idx in hardest_idx:
print(f" Digit {idx}: akurasi {per_class_acc[idx]:.2f}%")
# -----------------------------------------------------------------
# (4) Distribusi Confidence Prediksi (Benar vs Salah)
# -----------------------------------------------------------------
print("\n[6/6] Membuat visualisasi distribusi confidence & sampel kesalahan...")
fig, ax = plt.subplots(figsize=(9, 5.5))
ax.hist(y_confidence[is_correct], bins=30, alpha=0.65, color="#2ecc71",
label=f"Prediksi Benar (n={np.sum(is_correct):,})", density=True)
ax.hist(y_confidence[~is_correct], bins=30, alpha=0.75, color="#e74c3c",
label=f"Prediksi Salah (n={np.sum(~is_correct):,})", density=True)
ax.set_title("Distribusi Confidence Prediksi Model CNN", fontsize=13, fontweight="bold")
ax.set_xlabel("Confidence (Probabilitas Softmax Maksimum)")
ax.set_ylabel("Kepadatan (Density)")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
confidence_path = os.path.join(FIGURES_DIR, "confidence_distribution_cnn.png")
plt.savefig(confidence_path, dpi=150)
plt.close()
print(f" -> Distribusi confidence tersimpan: {confidence_path}")
avg_conf_correct = y_confidence[is_correct].mean() * 100
avg_conf_wrong = y_confidence[~is_correct].mean() * 100
print(f" -> Rata-rata confidence saat BENAR : {avg_conf_correct:.2f}%")
print(f" -> Rata-rata confidence saat SALAH : {avg_conf_wrong:.2f}%")
# -----------------------------------------------------------------
# (5) Contoh Gambar yang Salah Diklasifikasikan
# -----------------------------------------------------------------
wrong_idx = np.where(~is_correct)[0]
rng = np.random.default_rng(SEED)
n_show = min(25, len(wrong_idx))
sample_idx = rng.choice(wrong_idx, size=n_show, replace=False)
fig, axes = plt.subplots(5, 5, figsize=(11, 12))
for ax, idx in zip(axes.flat, sample_idx):
ax.imshow(x_test[idx].reshape(28, 28), cmap="gray")
conf = y_confidence[idx] * 100
ax.set_title(f"True: {y_test[idx]} | Pred: {y_pred[idx]}\nConf: {conf:.1f}%",
fontsize=9, color="#c0392b")
ax.axis("off")
for ax in axes.flat[n_show:]:
ax.axis("off")
plt.suptitle(f"Contoh {n_show} Gambar Salah Klasifikasi dari {len(wrong_idx)} Total Kesalahan",
fontsize=13, fontweight="bold", y=1.0)
plt.tight_layout()
misclassified_path = os.path.join(FIGURES_DIR, "misclassified_samples_cnn.png")
plt.savefig(misclassified_path, dpi=150, bbox_inches="tight")
plt.close()
print(f" -> Contoh gambar salah klasifikasi tersimpan: {misclassified_path}")
# -----------------------------------------------------------------
# Ringkasan Akhir
# -----------------------------------------------------------------
summary_path = os.path.join(REPORTS_DIR, "evaluation_summary_cnn.csv")
summary_df = pd.DataFrame([{
"test_accuracy_pct": round(test_acc * 100, 4),
"test_loss": round(test_loss, 4),
"total_errors": int(np.sum(~is_correct)),
"total_samples": int(len(y_test)),
"avg_confidence_correct_pct": round(avg_conf_correct, 4),
"avg_confidence_wrong_pct": round(avg_conf_wrong, 4),
"hardest_digit_1": int(hardest_idx[0]),
"hardest_digit_1_acc_pct": round(per_class_acc[hardest_idx[0]], 4),
"hardest_digit_2": int(hardest_idx[1]),
"hardest_digit_2_acc_pct": round(per_class_acc[hardest_idx[1]], 4),
}])
summary_df.to_csv(summary_path, index=False)
print("\n" + "=" * 75)
print("RINGKASAN EVALUASI CNN (ANGGOTA 2)")
print("=" * 75)
print(f"Test Accuracy : {test_acc * 100:.2f}%")
print(f"Test Loss : {test_loss:.4f}")
print(f"Total Kesalahan : {np.sum(~is_correct):,} / {len(y_test):,}")
print(f"Confidence saat Benar : {avg_conf_correct:.2f}%")
print(f"Confidence saat Salah : {avg_conf_wrong:.2f}%")
print(f"Digit Paling Sulit : {hardest_idx[0]} ({per_class_acc[hardest_idx[0]]:.2f}%), "
f"{hardest_idx[1]} ({per_class_acc[hardest_idx[1]]:.2f}%)")
print("-" * 75)
print("File yang dihasilkan:")
print(f" 1. {metrics_bar_path}")
print(f" 2. {cm_dual_path}")
print(f" 3. {per_class_acc_path}")
print(f" 4. {confidence_path}")
print(f" 5. {misclassified_path}")
print(f" 6. {report_txt_path}")
print(f" 7. {report_csv_path}")
print(f" 8. {summary_path}")
print("=" * 75 + "\n")
if __name__ == "__main__":
main()