Numerical stability and special functions

SciPy · Expert · 13 min

Probabilistic and scientific code fails silently with NaN, inf or wrong digits when it ignores floating-point limits.

Floating point has about 16 significant digits and a range up to roughly 1e308. Algorithms that are correct on paper fail in three classic ways.

Overflow and underflow. np.exp(1000) is inf; a product of 500 small probabilities is 0.0. Work in log space: sum log-probabilities instead of multiplying probabilities, and use scipy.special.logsumexp(a) to compute log(sum(exp(a))) safely by factoring out the maximum. special.softmax and special.log_softmax do the same for normalisation, and distributions offer logpdf, logcdf, logsf.

Cancellation. Subtracting nearly equal numbers loses digits: 1 - cos(x) or log(1 + x) for tiny x. Use np.log1p, np.expm1, special.cosm1, special.xlogy (handles 0 * log 0), and special.expit / special.logit for the sigmoid.

Large factorials and binomials. Use special.gammaln, special.betaln and special.comb(n, k, exact=True) rather than computing factorials directly.

Ill-conditioning. A problem with a large condition number amplifies input errors whatever the algorithm. Signs include LinAlgWarning: Ill-conditioned matrix, and optimisers that stall. Rescale variables, centre data, use orthogonal factorisations (QR, SVD) instead of normal equations, and regularise.

Also, tolerances such as rtol, xtol and atol are relative to scale; set them consciously rather than trusting defaults.

Example

import numpy as np
from scipy import special

logp = np.array([-1000.0, -1001.0, -1002.0])      # log-likelihoods
print(np.log(np.sum(np.exp(logp))))  # -inf + RuntimeWarning (exp underflows to 0)
print(round(special.logsumexp(logp), 4))         # -999.5924

x = 1e-12
print(np.log(1 + x), np.log1p(x))                # 1.000088900581841e-12 9.999999999995e-13

print(special.expit(-800.0), special.log_expit(-800.0))  # 0.0 -800.0
print(special.xlogy(0, 0))                       # 0.0 (0 * log 0 treated as 0)

print(special.comb(1000, 500, exact=True) % 1000)  # 320 (exact big integer)
print(round(special.gammaln(1001), 2))             # 5912.13 (log of 1000!)

Remember

  • Work in log space; use logsumexp, log_softmax, logpdf
  • Use log1p, expm1, xlogy, expit to avoid cancellation and overflow
  • Rescale and use orthogonal factorisations for ill-conditioned problems

Watch out for

  • Multiplying many probabilities directly and getting 0.0 or NaN.

Check yourself

  1. Why does special.logsumexp(a) succeed where np.log(np.sum(np.exp(a))) fails for a = [-1000, -1001]?

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

Official docs: docs.scipy.org/doc/scipy/reference/special.html