-
-
Notifications
You must be signed in to change notification settings - Fork 10
Add Exp and Softplus #67
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?
Changes from all commits
98b5be0
e96843c
3fb633b
d4bf5e0
1e5aed5
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,34 @@ | ||
| import jax.numpy as jnp | ||
| from jaxtyping import Array | ||
|
|
||
| from ._bijector import ( | ||
| AbstractBijector, | ||
| AbstractForwardInverseBijector, | ||
| AbstractFwdLogDetJacBijector, | ||
| AbstractInvLogDetJacBijector, | ||
| ) | ||
|
|
||
|
|
||
| class Exp( | ||
| AbstractForwardInverseBijector, | ||
| AbstractInvLogDetJacBijector, | ||
| AbstractFwdLogDetJacBijector, | ||
| ): | ||
| """Exponential bijector: y = exp(x).""" | ||
|
|
||
| _is_constant_jacobian: bool = False | ||
| _is_constant_log_det: bool = False | ||
|
|
||
| def forward_and_log_det(self, x: Array) -> tuple[Array, Array]: | ||
| """Computes y = exp(x) and log|det J(f)(x)| = x.""" | ||
| return jnp.exp(x), x | ||
|
|
||
| def inverse_and_log_det(self, y: Array) -> tuple[Array, Array]: | ||
| """Computes x = log(y) and log|det J(f^{-1})(y)| = -log(y).""" | ||
| x = jnp.log(y) | ||
| # Optimization: since x = log(y), the log det is simply -x | ||
| return x, -x | ||
|
|
||
| def same_as(self, other: AbstractBijector) -> bool: | ||
| """Returns True if this bijector is guaranteed to be the same as `other`.""" | ||
| return type(other) is Exp |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import jax.nn as jnn | ||
|
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. I prefer just import jax then later do jax.nn |
||
| import jax.numpy as jnp | ||
| from jaxtyping import Array | ||
|
|
||
| from ._bijector import ( | ||
| AbstractBijector, | ||
| AbstractForwardInverseBijector, | ||
| AbstractFwdLogDetJacBijector, | ||
| AbstractInvLogDetJacBijector, | ||
| ) | ||
|
|
||
|
|
||
| class Softplus( | ||
| AbstractForwardInverseBijector, | ||
| AbstractInvLogDetJacBijector, | ||
| AbstractFwdLogDetJacBijector, | ||
| ): | ||
| """ | ||
| Transforms the real line to the positive domain using | ||
| softplus y = log(1 + exp(x)). | ||
|
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. can be math mode here? |
||
| """ | ||
|
|
||
| _is_constant_jacobian: bool = False | ||
| _is_constant_log_det: bool = False | ||
|
|
||
| def forward_and_log_det(self, x: Array) -> tuple[Array, Array]: | ||
| """Computes y = softplus(x) and log|det J(f)(x)|.""" | ||
| y = jnn.softplus(x) | ||
| logdet = -jnn.softplus(-x) | ||
| return y, logdet | ||
|
|
||
| def inverse_and_log_det(self, y: Array) -> tuple[Array, Array]: | ||
| """Computes x = softplus^{-1}(y) and log|det J(f^{-1})(y)|.""" | ||
| x = jnp.log(-jnp.expm1(-y)) + y | ||
| logdet = jnn.softplus(-x) | ||
| return x, logdet | ||
|
|
||
| def same_as(self, other: AbstractBijector) -> bool: | ||
| """Returns True if this bijector is guaranteed to be the same as `other`.""" | ||
| return type(other) is Softplus | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # Exp Bijector | ||
|
|
||
| ::: distreqx.bijectors.Exp | ||
| options: | ||
| members: false | ||
| --- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # Softplus Bijector | ||
|
|
||
| ::: distreqx.bijectors.Softplus | ||
| options: | ||
| members: false | ||
| --- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| from unittest import TestCase | ||
|
|
||
| import equinox as eqx | ||
| import jax | ||
| import jax.numpy as jnp | ||
| import numpy as np | ||
| from parameterized import parameterized # type: ignore | ||
|
|
||
| from distreqx.bijectors import Exp, Tanh | ||
|
|
||
|
|
||
| class ExpTest(TestCase): | ||
| def setUp(self): | ||
| self.bij = Exp() | ||
|
|
||
| def assertion_fn(self, rtol=1e-5): | ||
| return lambda x, y: np.testing.assert_allclose(x, y, rtol=rtol) | ||
|
|
||
| @parameterized.expand([("float32", jnp.float32), ("float64", jnp.float64)]) | ||
| def test_forward_and_log_det(self, name, dtype): | ||
| x = jnp.array([-2.5, 0.0, 1.0, 3.5], dtype=dtype) | ||
| y, log_det = self.bij.forward_and_log_det(x) | ||
|
|
||
| self.assertion_fn()(y, jnp.exp(x)) | ||
| self.assertion_fn()(log_det, x) | ||
| self.assertEqual(y.dtype, dtype) | ||
| self.assertEqual(log_det.dtype, dtype) | ||
|
|
||
| @parameterized.expand([("float32", jnp.float32), ("float64", jnp.float64)]) | ||
| def test_inverse_and_log_det(self, name, dtype): | ||
| # We must use strictly positive numbers for the domain of log(y) | ||
| y = jnp.array([0.1, 1.0, jnp.e, 10.0], dtype=dtype) | ||
| x, log_det = self.bij.inverse_and_log_det(y) | ||
|
|
||
| self.assertion_fn()(x, jnp.log(y)) | ||
| self.assertion_fn()(log_det, -jnp.log(y)) | ||
| self.assertEqual(x.dtype, dtype) | ||
| self.assertEqual(log_det.dtype, dtype) | ||
|
|
||
|
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. I like these round trip tests, e.g. the |
||
| @parameterized.expand([("float32", jnp.float32), ("float64", jnp.float64)]) | ||
| def test_forward_and_inverse(self, name, dtype): | ||
| # Round-trip: inverse(forward(x)) == x, matching the softplus tests. | ||
| x = jnp.array([-5.0, 0.0, 5.0], dtype=dtype) | ||
| y, log_det_fwd = self.bij.forward_and_log_det(x) | ||
|
|
||
| x_rec, log_det_inv = self.bij.inverse_and_log_det(y) | ||
| self.assertion_fn()(x_rec, x) | ||
| self.assertion_fn()(log_det_inv, -log_det_fwd) | ||
|
|
||
| def test_jittable(self): | ||
| @eqx.filter_jit | ||
| def f(bij, x): | ||
| return bij.forward_and_log_det(x) | ||
|
|
||
| x = jnp.array([1.0, 2.0]) | ||
| y, log_det = f(self.bij, x) | ||
| self.assertIsInstance(y, jax.Array) | ||
| self.assertIsInstance(log_det, jax.Array) | ||
|
|
||
| def test_same_as(self): | ||
| same_bij = Exp() | ||
| diff_bij = Tanh() | ||
|
|
||
| self.assertTrue(self.bij.same_as(same_bij)) | ||
| self.assertFalse(self.bij.same_as(diff_bij)) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| from unittest import TestCase | ||
|
|
||
| import equinox as eqx | ||
| import jax | ||
| import jax.numpy as jnp | ||
| import numpy as np | ||
|
|
||
| from distreqx.bijectors import Softplus | ||
|
|
||
|
|
||
| class SoftplusTest(TestCase): | ||
| def setUp(self): | ||
| self.bij = Softplus() | ||
|
|
||
| def assertion_fn(self, rtol=1e-5): | ||
| return lambda x, y: np.testing.assert_allclose(x, y, rtol=rtol) | ||
|
|
||
| def test_forward_and_inverse(self): | ||
| x = jnp.array([-5.0, 0.0, 5.0]) | ||
| y, log_det_fwd = self.bij.forward_and_log_det(x) | ||
|
|
||
| expected_y = jax.nn.softplus(x) | ||
| self.assertion_fn()(y, expected_y) | ||
| self.assertion_fn()(log_det_fwd, -jax.nn.softplus(-x)) | ||
|
|
||
| x_rec, log_det_inv = self.bij.inverse_and_log_det(y) | ||
| self.assertion_fn()(x_rec, x) | ||
| self.assertion_fn()(log_det_inv, -log_det_fwd) | ||
|
|
||
| def test_jittable(self): | ||
| @eqx.filter_jit | ||
| def f(bij, x): | ||
| return bij.forward_and_log_det(x) | ||
|
|
||
| y, log_det = f(self.bij, jnp.array(1.0)) | ||
| self.assertIsInstance(y, jax.Array) |
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.
this is an interesting issue, I copied this code from the original distrax so I'm surprised they didn't run into this issue. Did you encounter this in your work?
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.
@lockwo yes I did actually! I can't remember where though, I think it was during an optimization problem using the bijector for constraints, but I definitely remember bumping into it and having to fix it