iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Callbacks

Callbacks hook into the training loop — checkpointing, early stopping, learning-rate schedules, custom logging, TensorBoard, and Slack pings. Used well they catch overfitting before it happens and save you when a run dies at 3am.

EarlyStopping, Checkpoint, LR schedule

EXAMPLE
import tensorflow as tf
import tensorflow.keras as keras
from tensorflow.keras.callbacks import (
    EarlyStopping, ModelCheckpoint, ReduceLROnPlateau, TensorBoard, CSVLogger,
    LearningRateScheduler, TerminateOnNaN, BackupAndRestore,
)
import datetime, math, os

# 1) Build a model
model = keras.Sequential([
    keras.layers.Input(shape=(28, 28)),
    keras.layers.Flatten(),
    keras.layers.Dense(128, activation='relu'),
    keras.layers.Dropout(0.2),
    keras.layers.Dense(10),
])
model.compile(
    optimizer='adam',
    loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['sparse_categorical_accuracy'],
)

# 2) EarlyStopping — stop when val loss stops improving
es = EarlyStopping(
    monitor='val_loss',
    patience=5,                  # wait 5 epochs without improvement
    min_delta=1e-4,              # 'improvement' threshold
    restore_best_weights=True,    # roll back to the best checkpoint
    verbose=1,
)

# 3) ModelCheckpoint — save best (and/or periodically)
ckpt_dir = './ckpt'
os.makedirs(ckpt_dir, exist_ok=True)
mc_best = ModelCheckpoint(
    filepath=f'{ckpt_dir}/best.keras',
    monitor='val_loss',
    save_best_only=True,
    verbose=1,
)
mc_every = ModelCheckpoint(
    filepath=f'{ckpt_dir}/epoch-{{epoch:03d}}.keras',
    save_freq='epoch',
)

# 4) ReduceLROnPlateau — back off LR when stuck
rlr = ReduceLROnPlateau(
    monitor='val_loss',
    factor=0.5,                  # halve the LR
    patience=3,                  # ... after 3 epochs of no improvement
    min_lr=1e-6,
    verbose=1,
)

# 5) Learning-rate schedules — explicit cosine
def cosine_lr(epoch, lr, *, base_lr=1e-3, warmup=5, total=50):
    if epoch < warmup:
        return base_lr * (epoch + 1) / max(warmup, 1)
    progress = (epoch - warmup) / max(total - warmup, 1)
    return base_lr * 0.5 * (1 + math.cos(math.pi * progress))

ls = LearningRateScheduler(cosine_lr, verbose=1)

# 6) TensorBoard — metrics + graphs + per-batch histograms
logdir = './tb/' + datetime.datetime.now().strftime('%Y%m%d-%H%M%S')
tb = TensorBoard(
    log_dir=logdir,
    histogram_freq=1,            # log weight histograms each epoch
    profile_batch=(2, 4),         # profiler — slow, use sparingly
    update_freq='epoch',
    write_graph=True,
    write_images=False,
)

# Launch:  tensorboard --logdir ./tb

# 7) CSV log + NaN guard + crash recovery
csv = CSVLogger('./train.csv', append=True)
nan = TerminateOnNaN()
backup = BackupAndRestore(backup_dir='./backup')   # resume from last completed epoch

# 8) Fit with the lot
history = model.fit(
    x_train, y_train,
    validation_data=(x_val, y_val),
    epochs=50,
    batch_size=64,
    callbacks=[es, mc_best, mc_every, rlr, ls, tb, csv, nan, backup],
    verbose=2,
)

# 9) Custom callback — Slack/Discord ping on val loss improvement
import urllib.request, json

class NotifyOnBest(keras.callbacks.Callback):
    def __init__(self, webhook_url, monitor='val_loss', mode='min'):
        super().__init__()
        self.webhook = webhook_url
        self.monitor = monitor
        self.mode = mode
        self.best = math.inf if mode == 'min' else -math.inf

    def on_epoch_end(self, epoch, logs=None):
        v = logs.get(self.monitor)
        if v is None: return
        improved = v < self.best if self.mode == 'min' else v > self.best
        if improved:
            self.best = v
            self._notify(f'epoch {epoch+1}: {self.monitor}={v:.4f} ✅')

    def on_train_end(self, logs=None):
        self._notify(f'Training complete. Best {self.monitor}: {self.best:.4f}')

    def _notify(self, text):
        body = json.dumps({'text': text}).encode()
        req = urllib.request.Request(self.webhook, data=body, headers={'Content-Type': 'application/json'})
        try: urllib.request.urlopen(req, timeout=5).read()
        except Exception as e: print('notify failed:', e)

notify = NotifyOnBest(os.environ['SLACK_WEBHOOK'])

# 10) Custom callback — gradient norm logger (debugging)
class GradNormLogger(keras.callbacks.Callback):
    def __init__(self, sample_x, sample_y): super().__init__(); self.x, self.y = sample_x, sample_y
    def on_epoch_end(self, epoch, logs=None):
        with tf.GradientTape() as tape:
            preds = self.model(self.x, training=True)
            loss  = self.model.compiled_loss(self.y, preds)
        grads = tape.gradient(loss, self.model.trainable_weights)
        gnorm = tf.linalg.global_norm(grads)
        print(f'epoch {epoch+1} grad_norm={float(gnorm):.4f}')

# 11) Callback ordering matters
# Keras processes callbacks in the order you pass them.
# Logging-style callbacks last (after model state is finalized).
# EarlyStopping before BackupAndRestore so resume picks the latest non-stopped state.

# 12) Class-based custom callback — full lifecycle
class Demo(keras.callbacks.Callback):
    def on_train_begin(self, logs=None): ...
    def on_train_end  (self, logs=None): ...
    def on_epoch_begin(self, epoch, logs=None): ...
    def on_epoch_end  (self, epoch, logs=None): ...
    def on_train_batch_begin(self, batch, logs=None): ...
    def on_train_batch_end  (self, batch, logs=None): ...
    def on_test_begin (self, logs=None): ...
    def on_test_end   (self, logs=None): ...
    def on_predict_begin(self, logs=None): ...
    def on_predict_end  (self, logs=None): ...

# 13) Production checklist
#   ✓ ModelCheckpoint(save_best_only=True) somewhere durable
#   ✓ EarlyStopping with restore_best_weights=True
#   ✓ ReduceLROnPlateau OR an explicit schedule
#   ✓ BackupAndRestore for long runs (resume after preemption)
#   ✓ CSVLogger or TensorBoard for post-hoc analysis
#   ✓ TerminateOnNaN — catch divergence early
#   ✓ Notification callback so you don't refresh logs at midnight

# 14) Common bugs
#   • EarlyStopping fires before model has converged → patience too small
#   • restore_best_weights=False (default) → final weights aren't the best
#   • ModelCheckpoint saving on training loss, not val → overfits to train
#   • ReduceLROnPlateau + LearningRateScheduler conflict → pick one
#   • TensorBoard profile_batch on every batch → massive logs, slow training
#   • Custom callback accessing self.model.optimizer.lr — set via assign for new TF versions

Why it matters

A production training loop needs at minimum EarlyStopping with restore_best_weights=True, ModelCheckpoint(save_best_only=True), and either ReduceLROnPlateau or an explicit schedule — everything else (TensorBoard, CSV logs, Slack pings) is gravy. Don’t mix ReduceLROnPlateau and a schedule on the same metric, and never trust the last-epoch weights without restoring the best.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
from tensorflow.keras import callbacks
cbs = [
    callbacks.EarlyStopping(patience=3, restore_best_weights=True),
    callbacks.ModelCheckpoint('best.keras', save_best_only=True),
    callbacks.TensorBoard('logs/'),
]
Try it Yourself »

Exercise

Stop when validation stops improving.

callbacks. (patience=3)

Test yourself

Q1. EarlyStopping fires when…
Q2. TensorBoard callback writes…
Q3. ModelCheckpoint saves…

Discussion

Loading…