Pada Chapter Loss dan Optimizer kita sudah menjalankan model.fit() dengan tiga argumen dan mendapat model terlatih dalam sebaris kode. Bab ini membedah panggilan tersebut secara utuh: bagaimana data validasi disisipkan lewat validation_split atau validation_data, apa saja yang terekam di objek History, dan bagaimana callback mengubah fit() dari mesin buta jumlah epoch menjadi pelatihan yang bisa berhenti pada saat tepat serta menyimpan model terbaiknya sendiri. Kita akan membandingkan dua konfigurasi pada dataset yang sama — SGD dengan validation_split versus Adam dengan EarlyStopping dan ModelCheckpoint — lalu membaca hasilnya dari tabel history.

Anatomi model.fit #

model.compile() menyiapkan tiga bahan — loss, optimizer, dan metrik — sedangkan model.fit() memutar siklus epoch-batch yang selama ini kita susun manual: ambil batch, forward di dalam GradientTape, hitung loss, hitung gradien, perbarui bobot. Argumen yang paling sering kita pakai dirangkum pada tabel berikut:

ArgumenPeranNilai pada contoh
x, ydata latih — array, tuple, atau objek tf.data.DatasetX_latih, y_latih
epochsjumlah putaran penuh menelusuri data30 dan 120
batch_sizejumlah sampel per pembaruan bobot32
validation_splitpotong bagian akhir x sebagai data validasi0.2
validation_datapasangan data validasi eksplisit(X_val, y_val)
callbacksdaftar objek yang dipanggil tiap akhir epoch[early, checkpoint]
verbosetampilan progres — 0 senyap, 1 bar progres, 2 satu baris per epoch0

Objek Dataset dari Chapter Dataset dan tf.data dapat langsung menggantikan x dan y; fit() mengenali pipeline bertanda (fitur, label) dan memakai ukuran batch dari pipeline itu, sehingga argumen batch_size boleh ditinggalkan.

Dua Konfigurasi yang Dibandingkan #

Berkas fit_dua_konfigurasi.py memakai dataset make_moons 200 sampel yang dibagi 150 sampel latih dan 50 sampel validasi. Kedua model berarsitektur sama — Dense(64) ReLU dan keluaran logit dua kelas — sehingga satu-satunya perbedaan adalah cara pelatihannya dikendalikan:

python
"""model.fit() dengan dua konfigurasi: SGD + validation_split vs Adam + callbacks."""

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.model_selection import train_test_split

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

X, y = make_moons(n_samples=200, noise=0.20, random_state=42)
X_latih, X_val, y_latih, y_val = train_test_split(
    X, y, test_size=0.25, random_state=42
)


def buat_model():
    return tf.keras.Sequential(
        [
            tf.keras.Input(shape=(2,)),
            tf.keras.layers.Dense(64, activation="relu"),
            tf.keras.layers.Dense(2),
        ]
    )


def kompilasi(model, optimizer):
    model.compile(
        optimizer=optimizer,
        loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
        metrics=["accuracy"],
    )


# ---------- Konfigurasi 1: SGD + validation_split ----------
tf.keras.utils.set_random_seed(42)
model_sgd = buat_model()
kompilasi(model_sgd, tf.keras.optimizers.SGD(learning_rate=0.5))
h_sgd = model_sgd.fit(
    X_latih, y_latih,
    epochs=30,
    batch_size=32,
    validation_split=0.2,
    verbose=0,
)

# ---------- Konfigurasi 2: Adam + validation_data + callbacks ----------
tf.keras.utils.set_random_seed(42)
model_adam = buat_model()
kompilasi(model_adam, tf.keras.optimizers.Adam(learning_rate=0.01))

early = tf.keras.callbacks.EarlyStopping(
    monitor="val_loss", patience=4, restore_best_weights=True
)
checkpoint = tf.keras.callbacks.ModelCheckpoint(
    "model_terbaik.keras", monitor="val_loss", save_best_only=True
)

h_adam = model_adam.fit(
    X_latih, y_latih,
    epochs=120,
    batch_size=32,
    validation_data=(X_val, y_val),
    callbacks=[early, checkpoint],
    verbose=0,
)

# ---------- Ringkasan hasil dari history ----------
print("kunci history:", list(h_adam.history.keys()))
print()
val_loss = h_adam.history["val_loss"]
print("val_loss Adam tiap 10 epoch:",
      [round(val_loss[i], 4) for i in range(0, len(val_loss), 10)])
terbaik = int(np.argmin(val_loss)) + 1
print("val_loss terendah di epoch:", terbaik)
if early.stopped_epoch == 0:
    print("EarlyStopping : tidak berhenti dini")
else:
    print("EarlyStopping : berhenti di epoch", early.stopped_epoch + 1)
print()
print(f"{'model':5s} | {'loss':>8s} | {'akurasi':>8s} | {'val_loss':>8s} | {'val_akurasi':>11s} | epoch")
for nama, h in (("SGD", h_sgd), ("Adam", h_adam)):
    d = h.history
    print(f"{nama:5s} | {d['loss'][-1]:8.4f} | {d['accuracy'][-1]:8.4f} | "
          f"{d['val_loss'][-1]:8.4f} | {d['val_accuracy'][-1]:11.4f} | {len(d['loss']):5d}")

print()
for nama, model in (("SGD", model_sgd), ("Adam", model_adam)):
    loss, akurasi = model.evaluate(X_val, y_val, verbose=0)
    print(f"evaluate {nama:4s} pada validasi: loss {loss:.4f}, akurasi {akurasi:.4f}")

print()
print("berkas checkpoint dibuat:", os.path.exists("model_terbaik.keras"))
os.remove("model_terbaik.keras")
print("berkas demo dihapus agar bundle tetap bersih")

Output yang Dihasilkan:

text
kunci history: ['accuracy', 'loss', 'val_accuracy', 'val_loss']

val_loss Adam tiap 10 epoch: [0.4274, 0.2814, 0.2271, 0.1779, 0.1636, 0.1618]
val_loss terendah di epoch: 47
EarlyStopping : berhenti di epoch 51

model |     loss |  akurasi | val_loss | val_akurasi | epoch
SGD   |   0.2832 |   0.8583 |   0.3080 |      0.8667 |    30
Adam  |   0.0723 |   0.9733 |   0.1618 |      0.9400 |    51

evaluate SGD  pada validasi: loss 0.3006, akurasi 0.8200
evaluate Adam pada validasi: loss 0.1603, akurasi 0.9400

berkas checkpoint dibuat: True
berkas demo dihapus agar bundle tetap bersih

Mari kita telaah kode di atas baris per baris:

  1. Baris 12-13 (seed ganda): seed NumPy menjaga pembangkitan data dan seed Keras menjaga inisialisasi bobot; seed direset lagi pada baris 40 dan 52 agar kedua model berangkat dari bobot awal yang identik — syarat perbandingan yang adil.
  2. Baris 15-18 (data dan pembagian): 150 sampel latih, 50 sampel validasi; make_moons mengacak urutan sampel secara bawaan sehingga kedua subset berimbang kelasnya.
  3. Baris 21-36 (dua fungsi bantu): buat_model dan kompilasi dipakai bergilir; loss memakai from_logits=True karena layer terakhir linear — konsistensi yang dibahas tuntas pada Chapter Loss dan Optimizer .
  4. Baris 40-49 (konfigurasi SGD): validation_split=0.2 memotong 20% bagian akhir data latih — 30 dari 150 sampel — sebagai validasi; angka val_* pada history SGD berasal dari potongan 30 sampel itu.
  5. Baris 56-61 (dua callback): EarlyStopping mengawasi val_loss dan memberi kesempatan 4 epoch untuk membaik sebelum berhenti; restore_best_weights=True memutar bobot kembali ke epoch terbaik. ModelCheckpoint menyimpan seluruh model ke berkas .keras setiap kali val_loss memecahkan rekor terendah.
  6. Baris 63-70 (konfigurasi Adam): batas epochs=120 hanya langit-langit; validation_data memakai 50 sampel validasi eksplisit, dan kedua callback digantung pada argumen callbacks.
  7. Baris 73-83 (membaca history): h_adam.history berperilaku seperti dictionary dengan kunci loss, accuracy, val_loss, val_accuracy — masing-masing daftar sepanjang jumlah epoch yang benar-benar berjalan. early.stopped_epoch bernilai nol bila pelatihan selesai tanpa berhenti dini.
  8. Baris 85-89 (tabel akhir): SGD berhenti tepat di 30 epoch sesuai perintah; Adam berhenti sendiri di epoch ke-51 dari kuota 120 — akurasi validasinya 0.9400 mengungguli 0.8667 milik SGD.
  9. Baris 92-94 (evaluate silang): kedua model dinilai ulang pada 50 sampel validasi yang sama. Nilai Adam 0.1603 lebih rendah daripada val_loss epoch terakhirnya (0.1618) — inilah efek restore_best_weights: model yang dievaluasi adalah bobot epoch 47, bukan epoch 51.
  10. Baris 97-99 (pembersihan): berkas model_terbaik.keras benar-benar tercipta — bukti ModelCheckpoint bekerja — lalu dihapus agar bundle chapter tetap bersih.

validation_split atau validation_data #

Kedua argumen menghasilkan kolom val_* pada history, tetapi jalurnya berbeda. validation_split=0.2 memotong bagian akhir data latih sebelum epoch pertama dimulai — praktis untuk eksperimen cepat, tetapi potongannya tidak pernah ikut dilatih. validation_data=(X_val, y_val) menyerahkan kontrol penuh: kita yang membagi, menghitung standardisasi dari data latih saja, dan memakai subset yang sama lintas eksperimen sehingga hasilnya sebanding.

Callback: Mata dan Tangan fit() #

Callback adalah objek yang diberi kesempatan bertindak di titik-titik tertentu pelatihan — paling sering di akhir tiap epoch. EarlyStopping menghentikan pelatihan ketika metrik yang dipantau berhenti membaik selama patience epoch; mekanisme dan variasinya dibahas mendalam pada Chapter Early Stopping . ModelCheckpoint menyimpan model ke disk tepat ketika rekor baru tercapai, sehingga pelatihan panjang tidak kehilangan model terbaiknya bila kualitas justru menurun setelahnya — pola simpan-muatnya kita bedah pada Chapter Penyimpanan dan Pemuatan Model .

Padanan manual dari seluruh mekanisme ini — loop epoch-batch tulisan tangan yang memeriksa val_loss, menghitung kesabaran, dan memanggil model.save — sudah kita tulis pada Chapter Training Loop versi PyTorch . Daftar lengkap argumen fit() beserta perilakunya tersedia di keras.io/api/models/model_training_apis , dan panduan resmi pelatihan dengan API bawaan di tensorflow.org/guide/keras .

Ringkasan #

Kita telah mengendalikan model.fit() pada tiga level: argumen dasar (epochs, batch_size, verbose=0), sumber validasi (validation_split untuk cepat, validation_data untuk kontrol), dan callback yang mengubah siklus pelatihan — EarlyStopping menghentikan Adam di epoch 51 dengan bobot terbaik epoch 47, sementara ModelCheckpoint menyimpan rekor ke berkas .keras. Objek History merekam seluruh perjalanan metrik sebagai daftar Python yang siap diplot. Namun fit() tetap kotak hitam berpintu rapat: bila kita butuh aturan learning rate yang aneh, loss multi-komponen, atau logging kustom per batch, pintu itu harus dibuka — lewat custom training loop, topik Chapter berikutnya.