diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 10c3e5a2ec..6e1f0b46a4 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -32,6 +32,7 @@ It also brings some dreadfully awaited fixes, so be sure to go through the chang - Fix issue in `logp` method of `HyperGeometric`. It now returns `-inf` for invalid parameters (see [4367](https://github.com/pymc-devs/pymc3/pull/4367)) - Fixed `MatrixNormal` random method to work with parameters as random variables. (see [#4368](https://github.com/pymc-devs/pymc3/pull/4368)) - Update the `logcdf` method of several continuous distributions to return -inf for invalid parameters and values, and raise an informative error when multiple values cannot be evaluated in a single call. (see [4393](https://github.com/pymc-devs/pymc3/pull/4393)) +- Improve numerical stability in `logp` and `logcdf` methods of `ExGaussian` (see [#4407](https://github.com/pymc-devs/pymc3/pull/4407)) ## PyMC3 3.10.0 (7 December 2020) diff --git a/pymc3/distributions/continuous.py b/pymc3/distributions/continuous.py index c60898ab16..985f90bcba 100644 --- a/pymc3/distributions/continuous.py +++ b/pymc3/distributions/continuous.py @@ -20,7 +20,6 @@ import warnings import numpy as np -import theano import theano.tensor as tt from scipy import stats @@ -37,10 +36,10 @@ gammaln, i0e, incomplete_beta, + log_normal, logpow, normal_lccdf, normal_lcdf, - std_cdf, zvalue, ) from pymc3.distributions.distribution import Continuous, draw_values, generate_samples @@ -3214,22 +3213,22 @@ def logp(self, value): sigma = self.sigma nu = self.nu - standardized_val = (value - mu) / sigma - cdf_val = std_cdf(standardized_val - sigma / nu) - cdf_val_safe = tt.switch(tt.eq(cdf_val, 0), np.finfo(theano.config.floatX).eps, cdf_val) - - # This condition is suggested by exGAUS.R from gamlss - lp = tt.switch( - tt.gt(nu, 0.05 * sigma), - -tt.log(nu) + (mu - value) / nu + 0.5 * (sigma / nu) ** 2 + logpow(cdf_val_safe, 1.0), - -tt.log(sigma * tt.sqrt(2 * np.pi)) - 0.5 * standardized_val ** 2, + # Alogithm is adapted from dexGAUS.R from gamlss + return bound( + tt.switch( + tt.gt(nu, 0.05 * sigma), + ( + -tt.log(nu) + + (mu - value) / nu + + 0.5 * (sigma / nu) ** 2 + + normal_lcdf(mu + (sigma ** 2) / nu, sigma, value) + ), + log_normal(value, mean=mu, sigma=sigma), + ), + 0 < sigma, + 0 < nu, ) - return bound(lp, sigma > 0.0, nu > 0.0) - - def _distr_parameters_for_repr(self): - return ["mu", "sigma", "nu"] - def logcdf(self, value): """ Compute the log of the cumulative distribution function for ExGaussian distribution @@ -3253,22 +3252,25 @@ def logcdf(self, value): """ mu = self.mu sigma = self.sigma - sigma_2 = sigma ** 2 nu = self.nu - z = value - mu - sigma_2 / nu + + # Alogithm is adapted from pexGAUS.R from gamlss return tt.switch( tt.gt(nu, 0.05 * sigma), - tt.log( - std_cdf((value - mu) / sigma) - - std_cdf(z / sigma) - * tt.exp( - ((mu + (sigma_2 / nu)) ** 2 - (mu ** 2) - 2 * value * ((sigma_2) / nu)) - / (2 * sigma_2) - ) + logdiffexp( + normal_lcdf(mu, sigma, value), + ( + (mu - value) / nu + + 0.5 * (sigma / nu) ** 2 + + normal_lcdf(mu + (sigma ** 2) / nu, sigma, value) + ), ), normal_lcdf(mu, sigma, value), ) + def _distr_parameters_for_repr(self): + return ["mu", "sigma", "nu"] + class VonMises(Continuous): r""" diff --git a/pymc3/tests/test_distributions.py b/pymc3/tests/test_distributions.py index a103008035..dcfceee441 100644 --- a/pymc3/tests/test_distributions.py +++ b/pymc3/tests/test_distributions.py @@ -1825,6 +1825,9 @@ def test_get_tau_sigma(self): (15.0, 5.000, 7.500, 7.500, -3.3093854), (50.0, 50.000, 10.000, 10.000, -3.6436067), (1000.0, 500.000, 10.000, 20.000, -27.8707323), + (-1.0, 1.0, 20.0, 0.9, -3.91967108), # Fails in scipy version + (0.01, 0.01, 100.0, 0.01, -5.5241087), # Fails in scipy version + (-1.0, 0.0, 0.1, 0.1, -51.022349), # Fails in previous pymc3 version ], ) def test_ex_gaussian(self, value, mu, sigma, nu, logp): @@ -1851,6 +1854,9 @@ def test_ex_gaussian(self, value, mu, sigma, nu, logp): (15.0, 5.000, 7.500, 7.500, -0.4545255), (50.0, 50.000, 10.000, 10.000, -1.433714), (1000.0, 500.000, 10.000, 20.000, -1.573708e-11), + (0.01, 0.01, 100.0, 0.01, -0.69314718), # Fails in scipy version + (-0.43402407, 0.0, 0.1, 0.1, -13.59615423), # Previous 32-bit version failed here + (-0.72402009, 0.0, 0.1, 0.1, -31.26571842), # Previous 64-bit version failed here ], ) def test_ex_gaussian_cdf(self, value, mu, sigma, nu, logcdf):