Numerical stability
On paper, math is exact. On a computer it runs in finite-precision floating point, where exp(1000) is infinity, log(0) is minus-infinity, and a thousand small probabilities multiply to zero. ML is full of these traps — and the fixes are a handful of standard tricks every framework uses.
Written and reviewed by Shreyash Prashu Last reviewed
What you'll learn
- The two floating-point failure modes — out of range and lost precision
- The overflow-in-softmax trap and the subtract-the-max fix
- Why ML works in log space — underflow of probability products
- The log-sum-exp trick and catastrophic cancellation
Before you start
The last lesson left a queasy feeling: arithmetic can be flawless and the conclusion still wrong. There the culprit was a hidden variable. Here it is a hidden limitation, and it sits one level deeper — inside the machine itself.
Every derivation in this course assumed real numbers with infinite precision. The math your code actually runs does not — it uses floating point, a finite approximation with a finite range and about seven honest digits. Most of the time the gap is invisible.
But ML lives exactly where it bites: exp of a large logit is infinity, log of a
probability that rounded to zero is minus-infinity, and the product of a thousand small
probabilities underflows to zero. A naive softmax or cross-entropy then returns NaN
and poisons the whole training run.
The reassuring news: the fixes are a small, standard toolkit. And once you have seen them,
you will spot the subtract-the-max, the log-space, and the +1e-9 everywhere in real code.
Two ways floating point fails
A float has finite range and finite precision:
- Out of range. Above roughly
3.4 × 10^38(for 32-bit float) a value overflows toinf; below about1.2 × 10^-38it underflows to0. Both are unrecoverable — once you haveinfor0, the information is gone. - Lost precision. A 32-bit float keeps only ~7 significant digits (16-bit far fewer). Subtracting two nearly-equal numbers can wipe out every meaningful digit — catastrophic cancellation.
ML’s favorite operations — exp, log, products of probabilities — live right at these
edges.
Trap 1: overflow in softmax — subtract the max
Softmax exponentiates its inputs. If the logits are large,
exp overflows and the whole thing collapses to NaN. The fix uses a free identity:
softmax is shift-invariant, softmax(x) = softmax(x − c) for any constant c. Pick
c = max(x) so the largest exponent is exp(0) = 1 and nothing overflows:
import numpy as np
x = np.array([1000.0, 1001.0, 1002.0])
naive = np.exp(x) / np.exp(x).sum() # exp(1000) overflows to inf
z = x - x.max() # shift so the largest is 0
stable = np.exp(z) / np.exp(z).sum()
print("naive softmax :", naive)
print("stable softmax:", np.round(stable, 4))
naive softmax : [nan nan nan]
stable softmax: [0.09 0.2447 0.6652]
Same math, but the naive version returns NaN (it computed inf / inf) while the
shifted version is exact. Every framework’s softmax does the subtraction for you.
Trap 2: underflow — why ML lives in log space
Now the opposite end. A sequence’s probability is a product of many per-token
probabilities, each less than 1. Multiply a thousand of them and the result
underflows to 0 — and log(0) = -inf, so your loss explodes. The fix is to never
form the product: work in log space, where a product becomes a sum.
log( p_1 · p_2 · ... · p_n ) = log p_1 + log p_2 + ... + log p_n
A sum of a thousand log-probabilities (each a modest negative number) is perfectly stable, while their product is not. This is the deep reason ML uses log-likelihood, cross-entropy, and log-probabilities everywhere — not mathematical taste, numerical survival.
Trap 3: the log-sum-exp trick
Sometimes you need log( Σ exp(x_i) ) — the normalizer of a softmax, the denominator of
a log-likelihood. Computing the exp directly overflows. The log-sum-exp identity
factors the max back out, exactly like the softmax shift:
log Σ exp(x_i) = m + log Σ exp(x_i − m) where m = max(x_i)
import numpy as np
x = np.array([1000.0, 1001.0, 1002.0])
m = x.max()
print("naive log-sum-exp :", np.log(np.exp(x).sum())) # log(inf) = inf
print("stable log-sum-exp:", round(float(m + np.log(np.exp(x - m).sum())), 4))
naive log-sum-exp : inf
stable log-sum-exp: 1002.4076
The naive form overflows to inf; the stable form gives the right answer (1002.41).
scipy.special.logsumexp and torch.logsumexp are exactly this.
In one breath
- Floating point has finite range (overflow →
inf, underflow →0) and finite precision (~7 digits in fp32); ML’sexp/log/probability-products sit right at these edges. - Softmax overflow: large logits make
expblow up; subtract the max first — softmax is shift-invariant, so the result is unchanged and nothing overflows. - Underflow: products of many small probabilities round to 0, so ML works in log space (a product becomes a sum) — the real reason log-likelihood/cross-entropy are everywhere.
- The log-sum-exp trick,
log Σ exp(x) = m + log Σ exp(x − m), computes sums-of-exps safely; watch for catastrophic cancellation when subtracting nearly-equal numbers. - Use the framework’s stable ops (
log_softmax,cross_entropy,logsumexp); never hand-roll the naive form — and it’s worse in fp16/bf16.
Practice
Quick check
A question to carry forward
Notice the move that rescued every trap in this lesson. Underflow vanished the moment we
stopped multiplying probabilities and started adding their logs; overflow vanished the
moment we factored the max out and back in. In each case we never touched the answer — we
changed the representation, did the work where it was easy, then changed back.
log → add → exp is that round trip in miniature.
Hold that shape, because the final stretch of this course is built on it at a far grander scale. There is a change of representation so powerful that it turns one of the most expensive operations in computing — convolution — into a plain multiplication. This change is the front door to all of audio ML.
- What is the Fourier transform, and why is any signal secretly a sum of sine waves?
- How does moving from the time domain to the frequency domain, doing the easy thing, and moving back become a tool hiding inside spectrograms, rotary positional embeddings, and fast long-convolution models?
Practice this in an interview
All questionsLog loss (cross-entropy loss) measures how well a model's predicted probabilities match the true labels: it is the negative log-likelihood of the correct class. It penalises confident wrong predictions severely because log(p) approaches negative infinity as p approaches zero — predicting 0.99 for the wrong class incurs roughly 100x the penalty of predicting 0.6 for the wrong class. A perfect model achieves 0; a random binary classifier achieves ln(2) ≈ 0.693.
Naive Bayes multiplies feature likelihoods, so one unseen feature-class combination with probability zero makes the entire class score zero. Laplace or Lidstone smoothing adds a positive pseudocount to every possible value; log probabilities prevent underflow but do not replace smoothing.
Mixed precision training uses float16 or bfloat16 for throughput-heavy forward and backward operations while retaining float32 where range, accumulation, or optimizer updates need it. It reduces activation memory and can speed tensor-core workloads, but the gain and accuracy depend on hardware, model, and numerical stability; loss scaling is usually needed for float16, not bfloat16.
The accuracy paradox occurs when a trivial model — one that always predicts the majority class — achieves high accuracy on an imbalanced dataset despite having zero predictive power for the minority class. A model that predicts 'not fraud' on every transaction achieves 99.9% accuracy if fraud is 0.1% of the data, but its recall for fraud is zero. Accuracy is only meaningful when classes are roughly balanced.