JAX implementation of the CatNat parameterization for categorical random variables, introduced in “Beyond Softmax: A Natural Parameterization for Categorical Random Variables” (ICML 2026).
PyTorch implementation available at allemanenti/catnat-torch.
CatNat is a drop-in alternative to softmax for parameterizing categorical distributions over K outcomes. It is designed to improve optimization by using a natural parameterization whose Fisher Information Matrix (FIM) is diagonal.
While softmax maps K unconstrained logits to K probabilities by exponentiating and normalizing them, CatNat uses only K - 1 unconstrained scores. These scores are assigned to the internal nodes of a binary tree whose leaves correspond to the K categorical outcomes.
Each score

Figure 1. Each score s_i is placed at an
internal node. The activation a(s_i) gives the probability of
taking the left branch, while 1 - a(s_i) gives the probability of
taking the right branch. A leaf probability is the product of the branch
probabilities along its path from the root.
This package provides two entry points:
catnat(scores, activation="sigmoid", use_logprobs=True)— returns categorical log-probabilities by default (numerically stable, gradient-friendly), or plain probabilities whenuse_logprobs=False.CatNatCategorical— adistrax-compatible distribution wrapping the above. Since the advantage ofCatNatis in the diagonal FIM, it is recommended to useCatNatCategoricalas it does not re-normalise the probabilities.
From git:
pip install git+https://github.com/allemanenti/catnat-jax.gitimport jax.numpy as jnp
from catnat_jax import catnat
# K - 1 = 4 internal scores -> K = 5 categorical log-probabilities
scores = jnp.array([0.1, -0.2, 0.3, 0.4])
logprobs = catnat(scores) # default: sigmoid, log-space
logprobs_nat = catnat(scores, "natural") # alternative built-in activation
# to get probabilities:
probs = jnp.exp(catnat(scores)) # Stable way, uses log-space in the binary tree
# or
probs = catnat(scores, use_logprobs=False) # May be less stable for extreme scores,
# as it uses probabilities in the treeYou can also pass a custom callable instead of a string. In log mode the callable must return
(log_p_left, log_p_right):
import jax
def log_sigmoid_split(x):
return jax.nn.log_sigmoid(x), jax.nn.log_sigmoid(-x)
logprobs = catnat(scores, log_sigmoid_split)Set use_logprobs=False to get normalised probabilities. The callable signature
becomes x -> p (Bernoulli probability):
probs = catnat(scores, use_logprobs=False) # shape (5,), sums to 1.0
probs = catnat(scores, "natural", use_logprobs=False)
probs = catnat(scores, jax.nn.sigmoid, use_logprobs=False)CatNatCategorical does not re-normalise its inputs, so feeding it logits
directly preserves the diagonal FIM that motivates CatNat:
import jax
from catnat_jax import catnat, CatNatCategorical
scores = jnp.array([0.1, -0.2, 0.3, 0.4])
dist = CatNatCategorical(logits=catnat(scores)) # default log-prob path
# Get samples
key = jax.random.PRNGKey(0)
samples = dist.sample(seed=key, sample_shape=(8,)) # sample 8 outcomes
# Get log-probabilities of samples
log_probs = dist.log_prob(samples) # log-prob of each sample
# Get entropy of the distribution
entropy = dist.entropy()CatNatCategoricalimports private modules fromdistrax(distrax._src.*). The dependency is pinned todistrax>=0.1.5; future distrax releases may move these paths.- This package depends on
tfp-nightly(the JAX-compatible build of TFP) rather than the stabletensorflow-probabilitywheel, which lags JAX compatibility. If you havetensorflow-probabilityalready installed in your environment, uninstall it before installingcatnat-jaxto avoid file-overlap conflicts.
If you use catnat-jax in your research, please cite the original paper:
@inproceedings{manenti2026beyond,
title={Beyond Softmax: A Natural Parameterization for Categorical Random Variables},
author={Manenti, Alessandro and Alippi, Cesare},
booktitle={International Conference on Machine Learning},
year={2026},
organization={PMLR}
}
MIT License.