TensorFlow
EarlyStopping
machine learning
deep learning
model training

How to monitor a filtered version of a metric in EarlyStopping callback in tensorflow?

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

Introduction

EarlyStopping can only monitor values that appear in the logs dictionary at the end of an epoch. If you want to stop based on a smoothed or otherwise filtered version of a metric, the practical solution is to compute that derived metric in a custom callback, add it to logs, and then tell EarlyStopping to monitor the derived name.

How EarlyStopping Sees Metrics

At on_epoch_end, Keras passes a logs dictionary to each callback. That dictionary contains values such as:

  • 'loss'
  • 'accuracy'
  • 'val_loss'
  • 'val_accuracy'

EarlyStopping does not know how you got those values. It just looks up the metric named in monitor.

That means you can create your own filtered metric as long as you insert it into logs before EarlyStopping runs.

Example: Exponential Moving Average of val_loss

Suppose raw val_loss is noisy and you want an exponentially smoothed version called val_loss_ema.

python
1import tensorflow as tf
2
3
4class FilterMetric(tf.keras.callbacks.Callback):
5    def __init__(self, source_name, target_name, alpha=0.2):
6        super().__init__()
7        self.source_name = source_name
8        self.target_name = target_name
9        self.alpha = alpha
10        self.state = None
11
12    def on_epoch_end(self, epoch, logs=None):
13        logs = logs or {}
14        value = logs.get(self.source_name)
15        if value is None:
16            return
17
18        value = float(value)
19        if self.state is None:
20            self.state = value
21        else:
22            self.state = self.alpha * value + (1.0 - self.alpha) * self.state
23
24        logs[self.target_name] = self.state
25        print(f" - {self.target_name}: {self.state:.5f}", end="")
26
27
28model = tf.keras.Sequential([
29    tf.keras.layers.Dense(16, activation="relu", input_shape=(8,)),
30    tf.keras.layers.Dense(1)
31])
32
33model.compile(optimizer="adam", loss="mse")
34
35x = tf.random.normal((256, 8))
36y = tf.random.normal((256, 1))
37
38filtered_metric = FilterMetric("val_loss", "val_loss_ema", alpha=0.3)
39early_stop = tf.keras.callbacks.EarlyStopping(
40    monitor="val_loss_ema",
41    patience=5,
42    mode="min",
43    restore_best_weights=True,
44)
45
46model.fit(
47    x, y,
48    validation_split=0.2,
49    epochs=50,
50    callbacks=[filtered_metric, early_stop],
51    verbose=1,
52)

The important detail is callback order: FilterMetric must come before EarlyStopping, otherwise the filtered value will not be present when EarlyStopping checks logs.

Why Filtering Can Help

Validation curves can bounce around from epoch to epoch because of:

  • small validation sets
  • stochastic training noise
  • data augmentation effects
  • unstable objectives

A filtered metric can reduce false early stops caused by one unlucky noisy epoch.

But smoothing also delays the signal. If the filter is too aggressive, training may continue long after the model has clearly stopped improving.

Alternatives to Injecting into logs

Another valid design is to subclass EarlyStopping itself and override on_epoch_end so it transforms the metric before applying the stopping logic. That works, but the injected-logs approach is often simpler because:

  • the filtered metric becomes visible in the history and logs
  • you can reuse standard EarlyStopping
  • you can monitor different filtered metrics without reimplementing stop logic

For many teams, that makes debugging much easier.

Window Average Versus Exponential Smoothing

An exponential moving average is not the only option. You could also maintain:

  • a fixed-size moving average
  • a median filter
  • a clipped or winsorized metric

The mechanism is the same: compute the derived value inside a callback and write it into logs under a custom key.

What History Will Contain

Because you add the filtered metric to logs, Keras can include it in the training history. That is useful for plotting and for checking whether your filter is actually improving stop behavior rather than just hiding problems.

If the filtered metric does not appear, the usual cause is callback ordering or a mismatch in the metric name.

Common Pitfalls

The biggest mistake is monitoring a filtered metric name that never gets added to logs. EarlyStopping can only monitor names it actually sees.

Another mistake is placing the filtering callback after EarlyStopping in the callback list. In that order, the derived metric arrives too late.

People also make the smoothing factor too aggressive. A filter that is too slow can mask real degradation and delay stopping too much.

Finally, make sure the mode of EarlyStopping matches the metric. For loss-like metrics, use mode="min". For accuracy-like metrics, use mode="max".

Summary

  • 'EarlyStopping can monitor only metrics present in logs at epoch end.'
  • To stop on a filtered metric, compute the filtered value in a custom callback and add it to logs.
  • Put the filtering callback before EarlyStopping in the callback list.
  • Exponential moving averages are a common and simple filter choice.
  • Tune the filter strength carefully so you smooth noise without hiding real regressions.

Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Basis Lab
No course covers this one yet

Basis Lab writes one for you from a sentence about what you want to be able to do, then teaches it and checks you understood. Your first course is free.

Build my course
ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design