Kurva pada chapter Overfitting dan Underfitting menyimpan fakta yang nyaris terbuang: loss validasi terbaik model besar tercapai di epoch 4, sementara pelatihan tetap berjalan sampai epoch 1500 tanpa membaik sedikit pun. Early stopping menjadikan observasi itu sebagai strategi: pantau metrik validasi selama pelatihan, hentikan ketika metrik tersebut berhenti membaik selama sejumlah epoch (patience), lalu pulihkan bobot dari epoch terbaik. Regularisasi ini tidak menambah parameter, tidak mengubah arsitektur, dan justru menghemat biaya komputasi. Chapter ini membangun kerangkanya secara manual di PyTorch, lalu menunjukkan padanan resminya di Keras — keduanya diuji nyata pada CPU.

Tiga Bahan: Monitor, Patience, Restore #

Setiap implementasi early stopping berdiri di atas tiga keputusan. Pertama, metrik yang dipantau (monitor) — lazimnya loss validasi, karena sinyalnya halus dan langsung mewakili generalisasi. Kedua, patience — berapa epoch metrik boleh memburuk sebelum pelatihan dihentikan; nilai kecil berhenti terlalu cepat, nilai besar memboroskan epoch. Ketiga, pemulihan bobot terbaik — tanpa pemulihan ini, model yang kita terima adalah bobot epoch terakhir yang justru sudah memburuk. Alur keputusannya per epoch:

Kerangka Manual di PyTorch #

PyTorch tidak menyediakan callback bawaan, sehingga pola manualnya menjadi cetakan yang muncul di hampir setiap proyek. Berkas early_stopping_manual.py menjalankan dua pelatihan dari bobot awal identik — satu penuh 1000 epoch, satu lagi dengan early stopping patience 50 — pada model overkapasitas [2, 128, 128, 2]:

python
"""Early stopping manual di PyTorch: monitor val loss + patience.

Dua pelatihan dijalankan dari bobot awal identik pada make_moons 150
sampel dengan model overkapasitas [2, 128, 128, 2]:
- pelatihan penuh 1000 epoch tanpa early stopping;
- pelatihan dengan early stopping manual (patience 50, bobot terbaik
  disimpan sebagai salinan state_dict lalu dipulihkan di akhir).
"""

import copy

import torch
import torch.nn as nn
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split

torch.manual_seed(42)
criterion = nn.CrossEntropyLoss()

# ---------- 1. Data: make_moons 150 sampel, 90 latih / 60 validasi ----------
X, y = make_moons(n_samples=150, noise=0.30, random_state=42)
X_latih, X_val, y_latih, y_val = train_test_split(
    X, y, test_size=0.40, random_state=42
)
rata2 = X_latih.mean(axis=0)
simpangan = X_latih.std(axis=0)
X_latih = (X_latih - rata2) / simpangan
X_val = (X_val - rata2) / simpangan

X_latih_t = torch.tensor(X_latih, dtype=torch.float32)
y_latih_t = torch.tensor(y_latih, dtype=torch.long)
X_val_t = torch.tensor(X_val, dtype=torch.float32)
y_val_t = torch.tensor(y_val, dtype=torch.long)

def buat_model():
    torch.manual_seed(42)
    return nn.Sequential(
        nn.Linear(2, 128), nn.ReLU(),
        nn.Linear(128, 128), nn.ReLU(),
        nn.Linear(128, 2),
    )

MAKS_EPOCH = 1000
PATIENCE = 50

# ---------- 2. Pelatihan penuh tanpa early stopping ----------
model_penuh = buat_model()
optimizer = torch.optim.Adam(model_penuh.parameters(), lr=0.05)
riwayat_val = []

for epoch in range(1, MAKS_EPOCH + 1):
    model_penuh.train()
    optimizer.zero_grad()
    loss = criterion(model_penuh(X_latih_t), y_latih_t)
    loss.backward()
    optimizer.step()
    model_penuh.eval()
    with torch.no_grad():
        loss_val = criterion(model_penuh(X_val_t), y_val_t).item()
    riwayat_val.append(loss_val)

epoch_terbaik_penuh = min(range(MAKS_EPOCH), key=riwayat_val.__getitem__) + 1

# ---------- 3. Pelatihan dengan early stopping manual ----------
model_es = buat_model()
optimizer = torch.optim.Adam(model_es.parameters(), lr=0.05)

loss_terbaik = float("inf")
bobot_terbaik = None
sabar = 0
epoch_berhenti = MAKS_EPOCH

for epoch in range(1, MAKS_EPOCH + 1):
    model_es.train()
    optimizer.zero_grad()
    loss = criterion(model_es(X_latih_t), y_latih_t)
    loss.backward()
    optimizer.step()

    model_es.eval()
    with torch.no_grad():
        loss_val = criterion(model_es(X_val_t), y_val_t).item()

    if loss_val < loss_terbaik:
        loss_terbaik = loss_val
        bobot_terbaik = copy.deepcopy(model_es.state_dict())
        sabar = 0
    else:
        sabar += 1
        if sabar >= PATIENCE:
            epoch_berhenti = epoch
            break

model_es.load_state_dict(bobot_terbaik)  # pulihkan bobot terbaik

# ---------- 4. Bandingkan hasil kedua pelatihan ----------
def akurasi(model, X, y_):
    model.eval()
    with torch.no_grad():
        return (model(X).argmax(dim=1) == y_).float().mean().item()

print("data    : make_moons 150 sampel | latih 90 | validasi 60 | noise 0.30")
print("model   : MLP [2, 128, 128, 2] | Adam lr=0.05 | full-batch")
print("kebijakan early stopping: monitor loss validasi | patience",
      PATIENCE, "| restore bobot terbaik\n")

print(f"pelatihan penuh   : {MAKS_EPOCH} epoch | loss val terbaik "
      f"{min(riwayat_val):.4f} di epoch {epoch_terbaik_penuh} | "
      f"loss val akhir {riwayat_val[-1]:.4f}")
print(f"early stopping    : berhenti di epoch {epoch_berhenti} | "
      f"loss val terbaik {loss_terbaik:.4f} | bobot dipulihkan\n")

print(f"{'pelatihan':<15s} | {'epoch jalan':>11s} | {'akurasi latih':>13s} | "
      f"{'akurasi val':>11s}")
print(f"{'penuh 1000':<15s} | {MAKS_EPOCH:11d} | "
      f"{akurasi(model_penuh, X_latih_t, y_latih_t):13.3f} | "
      f"{akurasi(model_penuh, X_val_t, y_val_t):11.3f}")
print(f"{'early stopping':<15s} | {epoch_berhenti:11d} | "
      f"{akurasi(model_es, X_latih_t, y_latih_t):13.3f} | "
      f"{akurasi(model_es, X_val_t, y_val_t):11.3f}")
print(f"\nhemat epoch: {MAKS_EPOCH - epoch_berhenti} epoch latih dilewati")

Output yang Dihasilkan:

text
data    : make_moons 150 sampel | latih 90 | validasi 60 | noise 0.30
model   : MLP [2, 128, 128, 2] | Adam lr=0.05 | full-batch
kebijakan early stopping: monitor loss validasi | patience 50 | restore bobot terbaik

pelatihan penuh   : 1000 epoch | loss val terbaik 0.3138 di epoch 23 | loss val akhir 2.4352
early stopping    : berhenti di epoch 73 | loss val terbaik 0.3138 | bobot dipulihkan

pelatihan       | epoch jalan | akurasi latih | akurasi val
penuh 1000      |        1000 |         1.000 |       0.800
early stopping  |          73 |         0.867 |       0.850

hemat epoch: 927 epoch latih dilewati

Mari kita telaah kode di atas baris per baris:

  1. Baris 10 (import copy): bobot terbaik harus disimpan sebagai salinan lepas — copy.deepcopy pada state_dict — karena tanpa salinan, objek yang disimpan akan ikut berubah ketika optimizer memperbarui bobot model.
  2. Baris 21-33 (data): make_moons 150 sampel, 90 latih / 60 validasi, standardisasi dari data latih saja.
  3. Baris 35-41 (buat_model): seed di-reset ke 42 sehingga kedua pelatihan berangkat dari titik bobot yang sama — syarat perbandingan yang adil.
  4. Baris 43-44 (kebijakan): MAKS_EPOCH = 1000 adalah kuota langit-langit; PATIENCE = 50 memberi ruang 50 epoch untuk membaik sebelum berhenti.
  5. Baris 47-62 (pelatihan penuh): training loop standar yang mencatat loss validasi tiap epoch; baris 62 lalu mencari epoch dengan nilai terendah — ini yang akan dilewatkan oleh pelatihan penuh.
  6. Baris 68-71 (state pola): empat variabel penjaga — loss_terbaik diinisialisasi tak hingga, bobot_terbaik kosong, sabar sebagai penghitung patience, epoch_berhenti default ke kuota penuh.
  7. Baris 84-92 (jantung pola): bila loss validasi memecahkan rekor, salinan bobot disimpan dan penghitung reset; bila tidak, penghitung bertambah dan pelatihan berhenti begitu sabar >= PATIENCE lewat break.
  8. Baris 94 (pemulihan): load_state_dict memutar bobot model kembali ke salinan terbaik — langkah yang membedakan “berhenti dini” dari “early stopping yang benar”.
  9. Baris 97-121 (perbandingan): kedua model dievaluasi dalam mode eval dengan torch.no_grad() pada data yang sama.

Hasilnya lugas: pelatihan penuh mencapai loss validasi terbaik 0,3138 di epoch 23 lalu terus memburuk sampai 2,4352 di epoch 1000 — akurasi validasinya 0,800. Early stopping berhenti di epoch 73, memulihkan bobot epoch 23 yang sama (loss 0,3138), dan mencatat akurasi validasi 0,850 — lebih baik dengan hanya 73 dari 1000 epoch, hemat 927 epoch.

Versi Keras: callbacks.EarlyStopping #

TensorFlow/Keras membungkus seluruh pola di atas menjadi satu callback. Berkas early_stopping_keras.py mengulang eksperimen yang sama — model Dense overkapasitas, kuota 1000 epoch, patience 50, restore_best_weights=True:

python
"""EarlyStopping Keras: callback bawaan dengan restore_best_weights.

Model Dense overkapasitas dilatih pada make_moons 150 sampel dengan
kuota 1000 epoch, tetapi callbacks.EarlyStopping mengawasi val_loss
dengan patience 50 dan memulihkan bobot epoch terbaik secara otomatis.
"""

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)

# ---------- 1. Data: make_moons 150 sampel, 90 latih / 60 validasi ----------
X, y = make_moons(n_samples=150, noise=0.30, random_state=42)
X_latih, X_val, y_latih, y_val = train_test_split(
    X, y, test_size=0.40, random_state=42
)
rata2 = X_latih.mean(axis=0)
simpangan = X_latih.std(axis=0)
X_latih = (X_latih - rata2) / simpangan
X_val = (X_val - rata2) / simpangan

# ---------- 2. Model overkapasitas + callback EarlyStopping ----------
model = tf.keras.Sequential(
    [
        tf.keras.Input(shape=(2,)),
        tf.keras.layers.Dense(128, activation="relu"),
        tf.keras.layers.Dense(128, activation="relu"),
        tf.keras.layers.Dense(2),
    ]
)
model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.01),
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=["accuracy"],
)

early = tf.keras.callbacks.EarlyStopping(
    monitor="val_loss",
    patience=50,
    restore_best_weights=True,
)

# ---------- 3. Latih dengan kuota 1000 epoch ----------
h = model.fit(
    X_latih, y_latih,
    validation_data=(X_val, y_val),
    epochs=1000,
    batch_size=32,
    callbacks=[early],
    verbose=0,
)

epoch_jalan = len(h.history["loss"])
epoch_terbaik = int(np.argmin(h.history["val_loss"])) + 1
loss, akurasi = model.evaluate(X_val, y_val, verbose=0)

print("data    : make_moons 150 sampel | latih 90 | validasi 60 | noise 0.30")
print("model   : Dense(128)-Dense(128)-Dense(2) | Adam lr=0.01 | batch 32")
print("callback: EarlyStopping monitor val_loss | patience 50 | "
      "restore_best_weights=True\n")

print("kuota epoch      :", 1000)
print("epoch dijalankan :", epoch_jalan)
print("epoch terbaik    :", epoch_terbaik, "(val_loss terendah)")
if early.stopped_epoch == 0:
    print("EarlyStopping    : tidak berhenti dini")
else:
    print("EarlyStopping    : berhenti di epoch", early.stopped_epoch + 1)
print(f"hasil evaluasi validasi (bobot terbaik): loss {loss:.4f} | "
      f"akurasi {akurasi:.4f}")

Output yang Dihasilkan:

text
data    : make_moons 150 sampel | latih 90 | validasi 60 | noise 0.30
model   : Dense(128)-Dense(128)-Dense(2) | Adam lr=0.01 | batch 32
callback: EarlyStopping monitor val_loss | patience 50 | restore_best_weights=True

kuota epoch      : 1000
epoch dijalankan : 57
epoch terbaik    : 7 (val_loss terendah)
EarlyStopping    : berhenti di epoch 57
hasil evaluasi validasi (bobot terbaik): loss 0.4155 | akurasi 0.8167

Bedah singkatnya: baris 8-10 menyetel TF_CPP_MIN_LOG_LEVEL sebelum impor TensorFlow agar keluaran bersih; baris 45-49 membangun EarlyStopping dengan tiga bahan pembuka chapter sebagai argumen; baris 52-59 menggantungkan callback pada model.fit() — mekanisme fit() dan callback lainnya dibahas tuntas di chapter Training dengan model.fit() ; baris 61-63 membaca h.history untuk jumlah epoch yang benar-benar berjalan dan posisi epoch terbaik; baris 73-76 membaca atribut early.stopped_epoch (nol berarti tidak pernah berhenti dini). Keluaran menunjukkan callback bekerja: dari kuota 1000, pelatihan berhenti di epoch 57 dengan bobot terbaik epoch 7 — evaluate pada baris 63 menilai model terpulihkan itu, loss 0,4155 dan akurasi 0,8167. Referensi resmi seluruh argumennya (termasuk min_delta dan mode) ada di keras.io/api/callbacks/early_stopping .

ParameterPeranNilai pada contohEfek jika salah pilih
monitor / metrik pantauansinyal kesehatan modelval_lossmemantau loss latih tidak pernah memicu berhenti
patienceruang membaik sebelum berhenti50terlalu kecil: berhenti sebelum konvergen; terlalu besar: boros epoch
restore_best_weights / salinan state_dictbobot yang diterima di akhirTrue / copy.deepcopytanpa pemulihan, model akhir justru bobot epoch terakhir yang sudah memburuk
MAKS_EPOCH / epochslangit-langit kuota1000kuota rendah memotong pelatihan yang belum selesai

Ketiga teknik pada section ini — dropout, weight decay, dan early stopping — mengubah perilaku pelatihan tanpa menambah satu sampel data pun. Teknik berikutnya bekerja dari sisi sebaliknya: memperkaya data latih itu sendiri. Chapter Data Augmentation menunjukkan bagaimana rotasi, penggeseran, dan transformasi lain memperluas cakupan pola yang dilihat model.