Other than the logpdf_with_trans methods, the package also provides a more composable interface through the Bijector types. Consider for example the one from above with Beta(2, 2).
julia> using Random;
+
+
+
+
+
Random.seed!(42);
julia> using Bijectors;
@@ -21,3 +478,4 @@
transform: Bijectors.Logit{Float64, Float64}(0.0, 1.0)
) julia> tdist isa UnivariateDistributiontrue
We can the then compute the logpdf for the resulting distribution:
julia> # Some example values
x = rand(dist)0.2909654089284631 julia> y = tdist.transform(x)-0.890699923433023 julia> logpdf(tdist, y)-1.3650442380572652
Settings
This document was generated with Documenter.jl version 0.27.25 on Wednesday 8 January 2025. Using Julia version 1.11.2.
But the real utility of TransformedDistribution becomes more apparent when using transformed(dist, b) for any bijector b. To get the transformed distribution corresponding to the Beta(2, 2), we called transformed(dist) before. This is simply an alias for transformed(dist, bijector(dist)). Remember bijector(dist) returns the constrained-to-constrained bijector for that particular Distribution. But we can of course construct a TransformedDistribution using different bijectors with the same dist. This is particularly useful in something called Automatic Differentiation Variational Inference (ADVI).[2] An important part of ADVI is to approximate a constrained distribution, e.g. Beta, as follows:
Sample x from a Normal with parameters μ and σ, i.e. x ~ Normal(μ, σ).
Transform x to y s.t. y ∈ support(Beta), with the transform being a differentiable bijection with a differentiable inverse (a "bijector")
This then defines a probability density with same support as Beta! Of course, it's unlikely that it will be the same density, but it's an approximation. Creating such a distribution becomes trivial with Bijector and TransformedDistribution:
It's worth noting that support(Beta) is the closed interval [0, 1], while the constrained-to-unconstrained bijection, Logit in this case, is only well-defined as a map (0, 1) → ℝ for the open interval (0, 1). This is of course not an implementation detail. ℝ is itself open, thus no continuous bijection exists from a closed interval to ℝ. But since the boundaries of a closed interval has what's known as measure zero, this doesn't end up affecting the resulting density with support on the entire real line. In practice, this means that
This package implements a set of functions for transforming constrained random variables (e.g. simplexes, intervals) to Euclidean space. The 3 main functions implemented in this package are the link, invlink and logpdf_with_trans for a number of distributions.
julia> using Bijectors
+
+
+
+
+
julia> d = LogNormal() # support is (0, Inf)
LogNormal{Float64}(μ=0.0, σ=1.0)
@@ -38,3 +495,4 @@
julia> # The difference between the two is due to the Jacobian
logabsdetjac(bijector(LogNormal()), ℯ)
-1
PDMatDistribution: Union{InverseWishart, Wishart}, and
TransformDistribution: Union{T, Truncated{T}} where T<:ContinuousUnivariateDistribution.
All exported names from the Distributions.jl package are reexported from Bijectors.
Bijectors.jl also provides a nice interface for working with these maps: composition, inversion, etc. The following table lists mathematical operations for a bijector and the corresponding code in Bijectors.jl.
Operation
Method
Automatic
b ↦ b⁻¹
inverse(b)
✓
(b₁, b₂) ↦ (b₁ ∘ b₂)
b₁ ∘ b₂
✓
(b₁, b₂) ↦ [b₁, b₂]
stack(b₁, b₂)
✓
x ↦ b(x)
b(x)
×
y ↦ b⁻¹(y)
inverse(b)(y)
×
x ↦ log|det J(b, x)|
logabsdetjac(b, x)
AD
x ↦ b(x), log|det J(b, x)|
with_logabsdet_jacobian(b, x)
✓
p ↦ q := b_* p
q = transformed(p, b)
✓
y ∼ q
y = rand(q)
✓
p ↦ b such that support(b_* p) = ℝᵈ
bijector(p)
✓
(x ∼ p, b(x), log|det J(b, x)|, log q(y))
forward(q)
✓
In this table, b denotes a Bijector, J(b, x) denotes the Jacobian of b evaluated at x, b_* denotes the push-forward of p by b, and x ∼ p denotes x sampled from the distribution with density p.
The "Automatic" column in the table refers to whether or not you are required to implement the feature for a custom Bijector. "AD" refers to the fact that it can be implemented "automatically" using automatic differentiation.
Settings
This document was generated with Documenter.jl version 0.27.25 on Wednesday 8 January 2025. Using Julia version 1.11.2.
Some transformations are well-defined for different types of inputs, e.g. exp can also act elementwise on an N-dimensional Array{<:Real,N}. To specify that a transformation should act elementwise, we use the elementwise method:
julia> x = ones(2, 2)2×2 Matrix{Float64}:
+
+
+
+
+
1.0 1.0
1.0 1.0 julia> transform(elementwise(exp), x)2×2 Matrix{Float64}:
2.71828 2.71828
@@ -122,3 +579,4 @@
julia> (a = x.a, b = (x.a + x.c) * x.b, c = x.c)
(a = 1.0, b = 8.0, c = 3.0)