-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Stabilize Negative Binomial with large mu/alpha #8394
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -699,7 +699,7 @@ def dist(cls, mu=None, alpha=None, p=None, n=None, *args, **kwargs): | |
| return super().dist([n, p], *args, **kwargs) | ||
|
|
||
| @classmethod | ||
| def get_n_p(cls, mu=None, alpha=None, p=None, n=None): | ||
| def get_n_p(cls, mu=None, alpha=None, p=None, n=None, math=pt): | ||
| if n is None: | ||
| if alpha is not None: | ||
| n = alpha | ||
|
|
@@ -710,7 +710,9 @@ def get_n_p(cls, mu=None, alpha=None, p=None, n=None): | |
|
|
||
| if p is None: | ||
| if mu is not None: | ||
| p = n / (mu + n) | ||
| # n / (mu + n) in logit space, so a mu = exp(x) that overflows still | ||
| # yields finite logp and dlogp: log(mu) cancels back to x | ||
| p = math.sigmoid(math.log(n) - math.log(mu)) | ||
| else: | ||
| raise ValueError("Incompatible parametrization. Must specify either mu or p.") | ||
| elif mu is not None: | ||
|
|
@@ -725,7 +727,10 @@ def support_point(rv, size, n, p): | |
| return mu | ||
|
|
||
| def logp(value, n, p): | ||
| mu = n * (1 - p) / p | ||
| # (1 - p) / p in log space: a p = sigmoid(w) that saturates at 1.0 carries no | ||
| # information, while the rewritten log terms still see w. Spelled log(1 - p) | ||
| # because the sigmoid stabilization rewrites do not recognize log1p(-p) | ||
| mu = n * pt.exp(pt.log(1 - p) - pt.log(p)) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is addressed by pymc-devs/pytensor#2328 |
||
|
|
||
| # binomln subtracts gammaln(value + n) - gammaln(n), whose difference falls below | ||
| # their shared ulp once n is large, so fall back on the Poisson(mu) limit there. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1420,14 +1420,14 @@ def logcdf_fn(value, psi, mu): | |
|
|
||
| def test_zeroinflatednegativebinomial_logp(self): | ||
| def logp_fn(value, psi, mu, alpha): | ||
| n, p = NegativeBinomial.get_n_p(mu=mu, alpha=alpha) | ||
| n, p = alpha, alpha / (mu + alpha) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why don't we want to use the helper anymore?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was just convenience for the test, there's no numpy.sigmoid so can't be used anymore for bon symbolic inputs |
||
| if value == 0: | ||
| return np.log((1 - psi) * st.nbinom.pmf(0, n, p)) | ||
| else: | ||
| return np.log(psi * st.nbinom.pmf(value, n, p)) | ||
|
|
||
| def logcdf_fn(value, psi, mu, alpha): | ||
| n, p = NegativeBinomial.get_n_p(mu=mu, alpha=alpha) | ||
| n, p = alpha, alpha / (mu + alpha) | ||
| return np.log((1 - psi) + psi * st.nbinom.cdf(value, n, p)) | ||
|
|
||
| check_logp( | ||
|
|
@@ -1537,10 +1537,12 @@ def test_zero_inflated_binomial_support_point(self, psi, n, p, size, expected): | |
| (0.2, 10, 4, 5, np.full(5, 2)), | ||
| ( | ||
| 0.4, | ||
| np.arange(1, 5), | ||
| # keep the means off whole numbers, where the floor in the support | ||
| # point is sensitive to how p was built | ||
| np.arange(1, 5) + 0.5, | ||
| np.arange(2, 6), | ||
| None, | ||
| np.array([0, 1, 1, 2] if pytensor.config.floatX == "float64" else [0, 0, 1, 1]), | ||
| np.array([0, 1, 1, 2]), | ||
| ), | ||
| ( | ||
| np.linspace(0.2, 0.6, 3), | ||
|
|
@@ -1691,7 +1693,7 @@ def logp_fn(value, psi, mu): | |
|
|
||
| def test_hurdle_negativebinomial_logp(self): | ||
| def logp_fn(value, psi, mu, alpha): | ||
| n, p = NegativeBinomial.get_n_p(mu=mu, alpha=alpha) | ||
| n, p = alpha, alpha / (mu + alpha) | ||
| if value == 0: | ||
| return np.log(1 - psi) | ||
| else: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the story with the math argument?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tensor and xtensor are using the helper