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:
"""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:
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 100Mari kita telaah kode di atas baris per baris:
- Baris 13-14 (seed ganda): pembagian data, inisialisasi bobot, dan pelatihan terkunci pada seed 42 — laporan metrik identik di setiap eksekusi.
- Baris 17-27 (data dan pembagian): 400 sampel dibagi 300 latih dan 100 uji dengan
stratify=yagar proporsi tiga kelas terjaga di kedua subset — tanpa stratify, satu kelas bisa kurang terwakili di data uji. - 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.
- 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. - Baris 50-52 (evaluate):
return_dict=Truemengubah keluaran menjadi dictionary{accuracy, loss}alih-alih daftar — kebalikan darifit, yang menyimpan history sebagai dictionary secara bawaan. - Baris 55-60 (predict): lima sampel pertama menghasilkan matriks probabilitas —
tf.nn.softmaxmenerjemahkan logit menjadi probabilitas. Sampel ketiga menarik:[0.0, 0.579, 0.421]nyaris seri, dan prediksi1ternyata salah (label sebenarnya2) — probabilitas yang ragu adalah tempat kesalahan berkumpul. - 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. - 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:
| Metrik | Rumus | Kapan diutamakan |
|---|---|---|
| Accuracy | benar / total | kelas seimbang, semua kesalahan sama mahalnya |
| Precision | TP / (TP + FP) | salah positif mahal — misal filter spam |
| Recall | TP / (TP + FN) | salah negatif mahal — misal skrining penyakit |
| F1-score | harmonik precision dan recall | keseimbangan keduanya saat data timpang |
| Macro avg | rata-rata sederhana lintas kelas | kelas minoritas sama pentingnya dengan mayoritas |
| Weighted avg | rata-rata berbobot ukuran kelas | ringkasan 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:
"""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:
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.788Mari kita telaah kode di atas baris per baris:
- Baris 16-19 (data lebih berderau):
noise=0.25sengaja dipilih lebih tinggi daripada chapter pelatihan agar model tidak sempurna — perbedaan antar ambang baru terlihat pada model yang melakukan kesalahan. - Baris 21-33 (model biner): keluaran satu unit linear dengan
BinaryCrossentropy(from_logits=True)— pasangan yang konsisten;tf.sigmoidmenerjemahkan logit menjadi probabilitas setelah pelatihan. - Baris 35-37 (probabilitas): sampel keenam berprobabilitas
0.394dengan label sebenarnya1— pada ambang0.5sampel ini salah, pada ambang0.3sampel ini benar. - Baris 39-45 (sapuan ambang):
pred = (proba >= ambang)menggantikan pembandingan tetap0.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.