fix(distributions): keep Categorical entropy finite when a category has zero probability - #2276
Conversation
…as zero probability CategoricalProbs.entropy computed -(probs * log(probs)).sum(-1), which is NaN as soon as one category has probability exactly zero, even though such a probs vector is in the simplex and the category contributes nothing to the entropy. Use xlogy, as BernoulliProbs.entropy already does in the same module.
Benchmark reportthis PR - run time: 3 slower, 1 faster
compile time: unchanged across 32 benchmarksSignificant changes (4) ──────── run time ─────── ─────── compile time ───────
benchmark baseline this PR Δ baseline this PR Δ
────────────────────────────────────────────────────────────────────────────────────────
+ categorical_log_prob 1.8 ms 1.7 ms -6.5% 67.6 ms 69.3 ms +2.4%
- stick_breaking_transform 6.3 ms 7.2 ms +14.3% 210.1 ms 210.0 ms -0.0%
- truncated_normal_log_prob 1.0 ms 1.1 ms +7.8% 51.1 ms 52.6 ms +3.1%
- nested_handler_stack 1.3 ms 1.4 ms +6.2% 482 µs 604 µs (+25.2%)Red is slower, green is faster; a row is coloured by the worse of its two columns. A delta in parentheses cleared the threshold on a measurement below the resolution floor, so it is shown without being called a change. † marks a benchmark that could not be compared — see below. Full results
|
| baseline | this PR | |
|---|---|---|
| ref | master |
fix-categorical-entropy-zero-probs |
| commit | 0adc509b |
73599fe1 |
| numpyro | 0.21.0 | 0.21.0 |
| jax | 0.11.1 | 0.11.1 |
| backend | cpu | cpu |
| python | 3.14.7 | 3.14.7 |
Runner: Linux-6.17.0-1022-azure-x86_64-with-glibc2.39, 4 CPUs.
Produced by this benchmark run.
While reading the entropy methods in
numpyro/distributions/discrete.pyI noticed thatCategoricalProbs.entropyis the only one in that file that multiplies by a bare logarithm:probsis constrained to the simplex, and a simplex point may contain an exact zero. When it does, that term is0 * -inf = nanand the whole sum becomes NaN, even though a category that never occurs contributes nothing to the entropy under the usual0 log 0 = 0convention:BernoulliProbs.entropy, a few hundred lines above, already handles the same edge case withxlogy, andPoisson.log_probwas moved toxlogyfor the zero-rate case in #2176. I applied the same treatment here; the change is one line and leaves every strictly positiveprobsvector bit-for-bit identical, sincexlogy(x, x) == x * log(x)away from zero.I added the case to the existing categorical entropy test, checked against
scipy.stats.entropybecausescipy.stats.multinomialcannot be used with a zero-probability category. On my machine (CPU jax 0.11.1) the new test fails on master withACTUAL: nan / DESIRED: 0.562335and passes with the fix;pytest test/test_distributions.py -k entropygives 327 passed, 826 skipped afterwards.I deliberately left
CategoricalLogits.entropyalone: it hits the same0 * -infshape only when a logit is-inf, which is outside its declaredreal_vectorconstraint, so that felt like a separate decision rather than part of this fix.Found by a defect-hunting pipeline I build and run (Dev-next-gen), using Claude Code with Anthropic's Claude Opus 5.