Angka val_accuracy dari model.fit() hanya potongan cerita: angka tersebut dihitung dari data validasi selama pelatihan berlangsung, satu angka untuk seluruh kelas, dan tidak memberi tahu model salah dengan pola apa. Evaluasi yang layak dipercaya menuntut tiga hal terpisah — data uji yang tidak pernah disentuh pelatihan, metrik yang dipilih sesuai biaya kesalahan, dan pembacaan per kelas. Chapter ini menjalankan ketiganya pada sisi TensorFlow: model.evaluate() untuk metrik agregat, model.predict() untuk probabilitas per sampel, confusion matrix dan classification_report dari scikit-learn untuk bedah per kelas, lalu ambang keputusan (threshold) pada kasus biner. Seluruhnya diuji nyata pada model tiga kelas berbasis make_classification.

evaluate dan predict: Dua Pintu Keluar #

model.evaluate(X_uji, y_uji) menjalankan forward pass pada seluruh data uji sekali jalan dan mengembalikan loss beserta metrik yang terdaftar saat compile — tanpa pembaruan bobot apa pun. model.predict(X) hanya menghasilkan keluaran model, satu baris per sampel, sehingga kita bebas memrosesnya — mengambil kelas terbesar atau menerapkan ambang. Karena model contoh kita berakhir dengan layer linear, predict mengembalikan logit — softmax atau sigmoid diterapkan sendiri di luar model, sesuai konsistensi from_logits=True yang kita pegang sejak Chapter Loss dan Optimizer .

Laporan Lengkap Tiga Kelas #

Berkas evaluasi_keras.py melatih model Dense(32) pada 300 sampel latih selama 15 epoch, lalu menilai 100 sampel uji dengan seluruh perangkat evaluasi:

python
"""Evaluasi model Keras: evaluate, predict, classification_report, confusion matrix."""

import os

os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"

import numpy as np
import tensorflow as tf
from sklearn.datasets import make_classification
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.model_selection import train_test_split

np.random.seed(42)
tf.keras.utils.set_random_seed(42)

# ---------- 1. Data 3 kelas: 400 sampel, 8 fitur ----------
X, y = make_classification(
    n_samples=400,
    n_features=8,
    n_informative=5,
    n_redundant=0,
    n_classes=3,
    random_state=42,
)
X_latih, X_uji, y_latih, y_uji = train_test_split(
    X, y, test_size=0.25, random_state=42, stratify=y
)

rata2 = X_latih.mean(axis=0)
simpangan = X_latih.std(axis=0)
X_latih = ((X_latih - rata2) / simpangan).astype(np.float32)
X_uji = ((X_uji - rata2) / simpangan).astype(np.float32)

# ---------- 2. Latih mini model ----------
model = tf.keras.Sequential(
    [
        tf.keras.Input(shape=(8,)),
        tf.keras.layers.Dense(32, activation="relu"),
        tf.keras.layers.Dense(3),
    ]
)
model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.01),
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=["accuracy"],
)
model.fit(X_latih, y_latih, epochs=15, batch_size=32, verbose=0)

# ---------- 3. evaluate: loss + metrik sekali jalan ----------
hasil = model.evaluate(X_uji, y_uji, verbose=0, return_dict=True)
print("evaluate (return_dict=True):",
      {k: round(float(v), 4) for k, v in hasil.items()})

# ---------- 4. predict: probabilitas per sampel ----------
proba5 = tf.nn.softmax(model.predict(X_uji[:5], verbose=0)).numpy()
print()
print("predict 5 sampel pertama (probabilitas softmax):")
print(np.round(proba5, 3))
print("kelas prediksi   :", proba5.argmax(axis=1).tolist())
print("label sebenarnya :", y_uji[:5].tolist())

# ---------- 5. Laporan metrik + confusion matrix ----------
logit_uji = model.predict(X_uji, verbose=0)
y_pred = logit_uji.argmax(axis=1)

print()
print("confusion matrix (baris = kelas sebenarnya, kolom = prediksi):")
print(confusion_matrix(y_uji, y_pred))
print()
print(classification_report(y_uji, y_pred, digits=3))

Output yang Dihasilkan:

text
evaluate (return_dict=True): {'accuracy': 0.74, 'loss': 0.6505}

predict 5 sampel pertama (probabilitas softmax):
[[0.018 0.063 0.92 ]
 [0.014 0.951 0.035]
 [0.    0.579 0.421]
 [0.942 0.023 0.035]
 [0.202 0.764 0.034]]
kelas prediksi   : [2, 1, 1, 0, 1]
label sebenarnya : [2, 1, 2, 0, 2]

confusion matrix (baris = kelas sebenarnya, kolom = prediksi):
[[26  2  6]
 [ 1 26  6]
 [ 2  9 22]]

              precision    recall  f1-score   support

           0      0.897     0.765     0.825        34
           1      0.703     0.788     0.743        33
           2      0.647     0.667     0.657        33

    accuracy                          0.740       100
   macro avg      0.749     0.740     0.742       100
weighted avg      0.750     0.740     0.742       100

Mari kita telaah kode di atas baris per baris:

  1. Baris 13-14 (seed ganda): pembagian data, inisialisasi bobot, dan pelatihan terkunci pada seed 42 — laporan metrik identik di setiap eksekusi.
  2. Baris 17-27 (data dan pembagian): 400 sampel dibagi 300 latih dan 100 uji dengan stratify=y agar proporsi tiga kelas terjaga di kedua subset — tanpa stratify, satu kelas bisa kurang terwakili di data uji.
  3. Baris 29-32 (standardisasi): rata-rata dan simpangan dihitung dari data latih saja, lalu diterapkan ke kedua subset; data uji tidak boleh menyumbang statistik preprocessing.
  4. Baris 35-47 (latih mini): model dilatih dengan verbose=0; 15 epoch cukup menghasilkan model yang belajar namun tidak sempurna — kondisi ideal untuk membaca metrik.
  5. Baris 50-52 (evaluate): return_dict=True mengubah keluaran menjadi dictionary {accuracy, loss} alih-alih daftar — kebalikan dari fit, yang menyimpan history sebagai dictionary secara bawaan.
  6. Baris 55-60 (predict): lima sampel pertama menghasilkan matriks probabilitas — tf.nn.softmax menerjemahkan logit menjadi probabilitas. Sampel ketiga menarik: [0.0, 0.579, 0.421] nyaris seri, dan prediksi 1 ternyata salah (label sebenarnya 2) — probabilitas yang ragu adalah tempat kesalahan berkumpul.
  7. Baris 63-64 (prediksi seluruh data uji): argmax(axis=1) memilih kelas berprobabilitas terbesar untuk tiap baris; argmax pada logit dan pada softmax menghasilkan kelas yang sama.
  8. Baris 66-70 (laporan sklearn): confusion matrix dan classification_report menerima label prediksi dan label sebenarnya — keduanya array 1 dimensi, sudah di luar dunia tensor.

Membaca Confusion Matrix dan Laporan #

Angka akurasi 0.74 menyembunyikan cerita per kelas yang hanya terbaca dari confusion matrix. Baris ketiga — kelas 2 — memuat 22 prediksi benar tetapi juga 9 salah ke kelas 1 dan 2 salah ke kelas 0: recall kelas 2 hanya 22/33 = 0.667, terendah di antara ketiga kelas. Kolom kedua memuat 26 + 26 + 9 prediksi kelas 1, sehingga precision kelas 1 jatuh ke 26/61 = 0.703. Pola dua kelas yang saling tertukar ini menuntun perbaikan berikutnya: menambah sampel kelas yang lemah atau menaikkan bobotnya pada loss.

Tabel berikut merangkum metrik yang muncul pada laporan dan kapan masing-masing layak diutamakan:

MetrikRumusKapan diutamakan
Accuracybenar / totalkelas seimbang, semua kesalahan sama mahalnya
PrecisionTP / (TP + FP)salah positif mahal — misal filter spam
RecallTP / (TP + FN)salah negatif mahal — misal skrining penyakit
F1-scoreharmonik precision dan recallkeseimbangan keduanya saat data timpang
Macro avgrata-rata sederhana lintas kelaskelas minoritas sama pentingnya dengan mayoritas
Weighted avgrata-rata berbobot ukuran kelasringkasan satu angka yang mencerminkan komposisi data

Ambang Keputusan untuk Kasus Biner #

Klasifikasi biner dengan sigmoid punya kenop ekstra: prediksi positif tidak harus berprobabilitas di atas 0.5. Ambang diturunkan bila recall kelas positif lebih berharga, dinaikkan bila salah positif yang harus dihindari. Berkas threshold_biner.py melatih model sigmoid pada make_moons noise=0.25 lalu menggeser ambangnya:

python
"""Ambang keputusan biner: geser threshold, amati precision dan recall berubah."""

import os

os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"

import numpy as np
import tensorflow as tf
from sklearn.datasets import make_moons
from sklearn.metrics import precision_score, recall_score
from sklearn.model_selection import train_test_split

np.random.seed(42)
tf.keras.utils.set_random_seed(42)

X, y = make_moons(n_samples=300, noise=0.25, random_state=42)
X_latih, X_uji, y_latih, y_uji = train_test_split(
    X, y, test_size=0.3, random_state=42
)

model = tf.keras.Sequential(
    [
        tf.keras.Input(shape=(2,)),
        tf.keras.layers.Dense(16, activation="relu"),
        tf.keras.layers.Dense(1),
    ]
)
model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.01),
    loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
    metrics=["accuracy"],
)
model.fit(X_latih, y_latih, epochs=20, batch_size=32, verbose=0)

proba = tf.sigmoid(model.predict(X_uji, verbose=0)).numpy().ravel()
print("probabilitas 8 sampel pertama:", [round(float(p), 3) for p in proba[:8]])
print("label sebenarnya             :", y_uji[:8].tolist())
print()
print("threshold | akurasi | precision | recall")
for ambang in (0.3, 0.5, 0.7):
    pred = (proba >= ambang).astype(int)
    akurasi = float((pred == y_uji).mean())
    presisi = precision_score(y_uji, pred, zero_division=0)
    recall = recall_score(y_uji, pred, zero_division=0)
    print(f"{ambang:9.2f} | {akurasi:7.3f} | {presisi:9.3f} | {recall:6.3f}")

Output yang Dihasilkan:

text
probabilitas 8 sampel pertama: [0.938, 0.607, 0.979, 0.754, 0.995, 0.272, 0.394, 0.065]
label sebenarnya             : [1, 1, 1, 1, 1, 0, 1, 0]

threshold | akurasi | precision | recall
     0.30 |   0.911 |     0.879 |  0.981
     0.50 |   0.867 |     0.900 |  0.865
     0.70 |   0.844 |     0.932 |  0.788

Mari kita telaah kode di atas baris per baris:

  1. Baris 16-19 (data lebih berderau): noise=0.25 sengaja dipilih lebih tinggi daripada chapter pelatihan agar model tidak sempurna — perbedaan antar ambang baru terlihat pada model yang melakukan kesalahan.
  2. Baris 21-33 (model biner): keluaran satu unit linear dengan BinaryCrossentropy(from_logits=True) — pasangan yang konsisten; tf.sigmoid menerjemahkan logit menjadi probabilitas setelah pelatihan.
  3. Baris 35-37 (probabilitas): sampel keenam berprobabilitas 0.394 dengan label sebenarnya 1 — pada ambang 0.5 sampel ini salah, pada ambang 0.3 sampel ini benar.
  4. Baris 39-45 (sapuan ambang): pred = (proba >= ambang) menggantikan pembandingan tetap 0.5; tiga ambang menghasilkan tiga himpunan prediksi berbeda dari satu model yang sama — tanpa pelatihan ulang.

Hasilnya menampilkan tarik-menarik klasik: saat ambang naik dari 0.3 ke 0.7, precision naik dari 0.879 ke 0.932 (prediksi positif semakin bisa dipercaya) sementara recall turun dari 0.981 ke 0.788 (makin banyak positif yang terlewat). Tidak ada ambang yang menang mutlak — pilihan mengikuti biaya kesalahan pada domain masing-masing.

Ringkasan #

Kita kini menilai model secara berlapis: evaluate untuk loss dan metrik agregat, predict untuk probabilitas per sampel, confusion matrix untuk pola kesalahan antar kelas, dan laporan sklearn untuk precision–recall–F1 per kelas. Ambang keputusan menambah satu kenop terakhir pada kasus biner — cukup digeser tanpa melatih ulang. Model yang dinilai layak kemudian perlu dijaga. Chapter berikutnya menutup section ini dengan penyimpanan dan pemuatan model — format .keras, save_weights, hingga checkpoint otomatis.