Custom losses and metrics

Keras · Advanced · 12 min

Business objectives and research ideas rarely match the built-in losses and metrics exactly.

Custom losses: any function loss(y_true, y_pred) built from keras.ops that returns one value per sample works. Keras applies sample weights and reduces to a scalar for you. If the loss has hyperparameters that must be saved, subclass keras.losses.Loss, implement call(y_true, y_pred) and get_config.

Custom metrics must accumulate across batches, so they are classes:

  • Subclass keras.metrics.Metric.
  • In __init__, create state with self.add_variable(shape=(), initializer="zeros", name=...).
  • update_state(y_true, y_pred, sample_weight=None): update the variables with assign_add.
  • result(): compute the metric from the state.
  • reset_state(): the default resets every variable to its initializer, which is usually enough.

Why not compute a metric as a mean of per-batch values? Because many metrics (precision, AUC, percentages of totals) are not averages of per-batch results. Keeping sums and counts gives the exact epoch-level value.

For quantities that are not functions of (y_true, y_pred), such as a regularisation term computed inside a layer, use self.add_loss(...) in the layer, or override compute_loss in the model.

Example

import numpy as np
import keras
from keras import ops
def huber_like(y_true, y_pred, delta=1.0):         # a plain function is a valid loss
    err = ops.abs(y_true - y_pred)
    sq = 0.5 * err ** 2
    return ops.mean(ops.where(err < delta, sq, delta * (err - 0.5 * delta)), axis=-1)

class MeanAbsPct(keras.metrics.Metric):
    def __init__(self, name="mape", **kwargs):
        super().__init__(name=name, **kwargs)
        self.total = self.add_variable(shape=(), initializer="zeros", name="total")
        self.count = self.add_variable(shape=(), initializer="zeros", name="count")
    def update_state(self, y_true, y_pred, sample_weight=None):
        pct = ops.abs((y_true - y_pred) / y_true) * 100.0
        self.total.assign_add(ops.sum(pct))
        self.count.assign_add(ops.sum(ops.ones_like(pct)))
    def result(self):
        return self.total / self.count

m = MeanAbsPct()
m.update_state(np.array([100.0, 200.0], "float32"), np.array([110.0, 190.0], "float32"))
print(float(m.result()))                           # 7.5  (10% and 5% errors)
model = keras.Sequential([keras.Input((3,)), keras.layers.Dense(1)])
model.compile(optimizer="adam", loss=huber_like, metrics=[MeanAbsPct()])

Remember

  • A loss can be any fn(y_true, y_pred) built from keras.ops
  • Metrics keep sums/counts in variables: update_state, result, reset_state
  • Use add_loss for terms that do not depend on targets

Watch out for

  • Averaging per-batch metric values for metrics that are not batch-decomposable, like precision.

Check yourself

  1. Why does a custom Metric keep total and count variables instead of averaging per-batch results?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: keras.io/api/metrics/

Builds on

Unlocks

Nothing builds on this yet.