-
-
Notifications
You must be signed in to change notification settings - Fork 9
Add LogNormal #84
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
base: main
Are you sure you want to change the base?
Add LogNormal #84
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 |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| """LogNormal distribution.""" | ||
|
|
||
| import math | ||
|
|
||
| import jax | ||
| import jax.numpy as jnp | ||
| from jaxtyping import Array, Key | ||
|
|
||
| from ._distribution import AbstractProbDistribution | ||
|
|
||
| _half_log2pi = 0.5 * math.log(2 * math.pi) | ||
|
|
||
|
|
||
| class LogNormal(AbstractProbDistribution): | ||
| """ | ||
| LogNormal distribution parameterized by | ||
| `loc` and `scale` of the underlying Normal. | ||
| """ | ||
|
|
||
| loc: Array | ||
| scale: Array | ||
|
|
||
| def __init__(self, loc: Array, scale: Array): | ||
| """Initializes a LogNormal distribution. | ||
|
|
||
| **Arguments:** | ||
|
|
||
| - `loc`: Mean of the underlying Normal distribution. | ||
| - `scale`: Standard deviation of the underlying Normal distribution. | ||
| """ | ||
| self.loc = jnp.array(loc) | ||
| self.scale = jnp.array(scale) | ||
|
|
||
| @property | ||
| def event_shape(self) -> tuple[int, ...]: | ||
| """Shape of event of distribution samples.""" | ||
| return self.loc.shape | ||
|
|
||
| @property | ||
| def support(self) -> tuple[Array, Array]: | ||
| """See `Distribution.support`.""" | ||
| dtype = jnp.result_type(self.loc, self.scale) | ||
| return (jnp.array(0.0, dtype=dtype), jnp.array(jnp.inf, dtype=dtype)) | ||
|
|
||
| def _sample_from_std_normal(self, key: Key[Array, ""]) -> Array: | ||
| dtype = jnp.result_type(self.loc, self.scale) | ||
| return jax.random.normal(key, shape=self.event_shape, dtype=dtype) | ||
|
|
||
| def sample(self, key: Key[Array, ""]) -> Array: | ||
|
Owner
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. event_shape is loc.shape, but sample() later broadcasts with scale. e.g. LogNormal(0, jnp.ones(3)).event_shape == (), while sample() returns shape (3,) with the same random draw repeated across all entries. Either use the broadcasted parameter shape for sampling, or reject unequal parameter shapes |
||
| """See `Distribution.sample`.""" | ||
| rnd = self._sample_from_std_normal(key) | ||
| normal_samples = self.scale * rnd + self.loc | ||
| return jnp.exp(normal_samples) | ||
|
|
||
| def sample_and_log_prob(self, key: Key[Array, ""]) -> tuple[Array, Array]: | ||
| """See `Distribution.sample_and_log_prob`.""" | ||
| rnd = self._sample_from_std_normal(key) | ||
| normal_samples = self.scale * rnd + self.loc | ||
| samples = jnp.exp(normal_samples) | ||
|
|
||
| # Change of variables: log(P(Y=y)) = log(P(X=x)) - log(y), where x = log(y) | ||
| normal_log_prob = -0.5 * jnp.square(rnd) - _half_log2pi - jnp.log(self.scale) | ||
| log_prob = normal_log_prob - normal_samples | ||
| return samples, log_prob | ||
|
|
||
| def log_prob(self, value: Array) -> Array: | ||
| """See `Distribution.log_prob`.""" | ||
| log_value = jnp.log(value) | ||
| log_unnormalized = -0.5 * jnp.square(self._standardize(log_value)) | ||
| log_normalization = _half_log2pi + jnp.log(self.scale) + log_value | ||
| return log_unnormalized - log_normalization | ||
|
|
||
| def icdf(self, value: Array) -> Array: | ||
| """See `Distribution.icdf`.""" | ||
| return jnp.exp(jax.scipy.special.ndtri(value) * self.scale + self.loc) | ||
|
|
||
| def cdf(self, value: Array) -> Array: | ||
| """See `Distribution.cdf`.""" | ||
| return jax.scipy.special.ndtr(self._standardize(jnp.log(value))) | ||
|
|
||
| def log_cdf(self, value: Array) -> Array: | ||
| """See `Distribution.log_cdf`.""" | ||
| return jax.scipy.special.log_ndtr( | ||
| self._standardize(jnp.log(value)) | ||
| ) # pyright: ignore[reportGeneralTypeIssues] | ||
|
|
||
| def survival_function(self, value: Array) -> Array: | ||
| """See `Distribution.survival_function`.""" | ||
| return jax.scipy.special.ndtr(-self._standardize(jnp.log(value))) | ||
|
|
||
| def log_survival_function(self, value: Array) -> Array: | ||
| """See `Distribution.log_survival_function`.""" | ||
| return jax.scipy.special.log_ndtr( | ||
| -self._standardize(jnp.log(value)) | ||
| ) # pyright: ignore[reportGeneralTypeIssues] | ||
|
|
||
| def _standardize(self, value: Array) -> Array: | ||
| return (value - self.loc) / self.scale | ||
|
|
||
| def entropy(self) -> Array: | ||
| """Calculates the Shannon entropy (in nats).""" | ||
| log_normalization = _half_log2pi + jnp.log(self.scale) | ||
| entropy = 0.5 + log_normalization + self.loc | ||
| return entropy | ||
|
|
||
| def mean(self) -> Array: | ||
| """Calculates the mean.""" | ||
| return jnp.exp(self.loc + 0.5 * jnp.square(self.scale)) | ||
|
|
||
| def variance(self) -> Array: | ||
| """Calculates the variance.""" | ||
| var_scale = jnp.square(self.scale) | ||
| return jnp.expm1(var_scale) * jnp.exp(2.0 * self.loc + var_scale) | ||
|
|
||
| def stddev(self) -> Array: | ||
| """Calculates the standard deviation.""" | ||
| return jnp.sqrt(self.variance()) | ||
|
|
||
| def mode(self) -> Array: | ||
| """Calculates the mode.""" | ||
| return jnp.exp(self.loc - jnp.square(self.scale)) | ||
|
|
||
| def median(self) -> Array: | ||
| """Calculates the median.""" | ||
| return jnp.exp(self.loc) | ||
|
|
||
| def kl_divergence(self, other_dist, **kwargs) -> Array: | ||
| """Calculates the KL divergence to another distribution. | ||
|
|
||
| **Arguments:** | ||
|
|
||
| - `other_dist`: A compatible disteqx LogNormal distribution. | ||
| - `kwargs`: Additional kwargs. | ||
|
|
||
| **Returns:** | ||
|
|
||
| The KL divergence `KL(self || other_dist)`. | ||
| """ | ||
| # KL divergence is invariant under strictly monotonic transformations. | ||
| # Thus, KL(LogNormal(m1, s1) || LogNormal(m2, s2)) is identical to | ||
| # KL(Normal(m1, s1) || Normal(m2, s2)). | ||
| dist1 = self | ||
| dist2 = other_dist | ||
| diff_log_scale = jnp.log(dist1.scale) - jnp.log(dist2.scale) | ||
| return ( | ||
| 0.5 * jnp.square(dist1.loc / dist2.scale - dist2.loc / dist2.scale) | ||
| + 0.5 * jnp.expm1(2.0 * diff_log_scale) | ||
| - diff_log_scale | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # LogNormal | ||
|
|
||
| ::: distreqx.distributions.LogNormal | ||
| options: | ||
| members: | ||
| - __init__ | ||
| - entropy | ||
| --- |
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.
can type as Float[Array]? also can do the converter trick mentioned in another PR to remove init