Custom metric based on tensorflow's streaming metrics returns NaN
ML System Design practice on Codemia
Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.
Introduction
When a custom TensorFlow metric built from streaming metrics returns NaN, the problem is usually not the formula itself. The usual causes are reading the metric value before its update op has executed, dividing by zero during early batches, or forgetting to initialize the local variables that store streaming state.
Why streaming metrics behave differently
The old TensorFlow streaming metric API, now mostly seen through tf.compat.v1.metrics.*, returns two things:
- A value tensor that reads the current accumulated state.
- An update op that changes that state using the latest batch.
That means the value tensor is not automatically refreshed just because you built it. If your custom metric combines value tensors without controlling update order, you can easily compute a ratio from stale zeros, which leads to NaN.
For example, an F1 score derived from precision and recall is vulnerable because the denominator is precision + recall. Early in evaluation, both may still be zero.
A correct TensorFlow 1 style pattern
The safe approach is to run the update ops first and to use divide_no_nan for the final ratio:
There are three important details in that example:
precision_updateandrecall_updaterun beforef1is read.tf.math.divide_no_nanprevents a0 / 0result from becomingNaN.local_variables_initializer()is required because streaming metrics store internal counts in local variables.
If you skip any of those steps, the metric may remain invalid even though the graph builds successfully.
What goes wrong in custom formulas
Suppose you build a custom metric like this:
This looks reasonable, but it has two hidden problems. First, the formula reads the value tensors directly without forcing the update ops to execute. Second, ordinary division will return NaN when both precision and recall are zero, which is common at startup or on edge-case batches.
A lot of confusion comes from the fact that the metric variables are stateful while the formula looks like ordinary stateless tensor math. Streaming metrics are accumulators, so execution order matters.
Modern TensorFlow 2 alternative
If you are in TensorFlow 2 or Keras, the cleaner approach is often to write a tf.keras.metrics.Metric subclass and keep the state yourself:
This avoids the awkward separation between value tensors and update ops and is usually easier to reason about in modern projects.
Common Pitfalls
The most common mistake is forgetting tf.compat.v1.local_variables_initializer(). Streaming metrics depend on local variables, so initializing only global variables is not enough.
Another issue is reading precision or recall before running their update ops. If the counts are still zero, the formula can produce invalid results or misleading zeros.
Division by zero is also common on highly imbalanced batches or empty positive classes. Use tf.math.divide_no_nan anywhere the denominator might legitimately be zero.
Finally, avoid mixing TensorFlow 1 streaming metrics into TensorFlow 2 Keras code unless you have a compatibility reason. A native Metric subclass is usually clearer and less error-prone.
Summary
- Streaming metrics return both a value tensor and an update op, and the update order matters.
- '
NaNusually comes from stale zero state, missing local variable initialization, or ordinary division by zero.' - In TensorFlow 1 style code, use control dependencies and
tf.math.divide_no_nan. - In TensorFlow 2, prefer a custom
tf.keras.metrics.Metricsubclass. - Treat metric code as stateful logic, not just a pure formula.
Related reading
- Custom Neural Network Implementation on MNIST using Tensorflow 2.0?
- Custom padding for convolutions in TensorFlow
- Custom TensorFlow Keras optimizer
- Custom weight initialization tensorflow tf.layers.dense
- Custom Performance Function in caret Package using predicted Probability
- Custom transformer for sklearn Pipeline that alters both X and y
- Custom weighted loss function in Keras for weighing each element
- ''CXXABI_1.3.8'' not found in tensorflow-gpu - install from source
.png&w=3840&q=75)
Tackling System Design Interview Problems
A short course that equips you with the skills to approach system design interviews methodically.
Start the free courseNo 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 courseML System Design practice on Codemia
Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.