The VectorBijectors module
The Bijectors.VectorBijectors module contains functionality that is very similar to that in the core Bijectors modules, but is specifically focused on converting random samples from distributions to and from vectors.
It assumes that there are three forms of samples from a distribution d that we are interested in:
The original form, which is what
rand(d)returns.A vectorised form, which is a vector that contains a flattened version of the original form.
A linked vectorised form, which is a vector in which:
- each element is independent; and
- each element is unconstrained (can take any value in ℝ).
Note that because of the independence requirement, the linked vectorised form may have a different dimension to the vectorised form. For example, when sampling from a Dirichlet distribution, the original form is a vector that always sums to 1. The linked vectorised form will have one element less than the original form, because this constraint is eliminated.
The Bijectors.VectorBijectors module provides functionality to convert between these three forms, via the following functions. Assuming that x = rand(d) for some distribution d:
to_vec(d)is a function which convertsxto the vectorised formfrom_vec(d)is the inverse ofto_vec(d)vec_length(d)returns the length ofto_vec(d)(x)optic_vec(d)returns a vector of optics that describes how each element ofto_vec(d)(x)is accessed fromxto_linked_vec(d)is a function which convertsxto the linked vectorised formfrom_linked_vec(d)is the inverse ofto_linked_vec(d)linked_vec_length(d)returns the length ofto_linked_vec(d)(x)linked_optic_vec(d)returns a vector of optics that describes how each element ofto_linked_vec(d)(x)is accessed fromx(if possible)
For example:
julia> using Bijectors.VectorBijectors, Distributions
julia> d = Beta(2, 2);
x = rand(d); # x is between 0 and 1
0.5602086057097567
julia> to_vec(d)(x)
1-element Vector{Float64}:
0.5602086057097567
julia> to_linked_vec(d)(x)
1-element Vector{Float64}:
0.24200871395677753The bijectors here will also implement ChangesOfVariables.with_logabsdet_jacobian as well as InverseFunctions.inverse. See the main Bijectors documentation for more details of this interface.
When would you use this?
This module is intended primarily for use with probabilistic programming, e.g. DynamicPPL.jl, where vectorised samples are required to satisfy the LogDensityProblems.jl interface.
The core Bijectors.jl interface does indeed contain very similar functionality, but it does not guarantee that Bijectors.bijector(d)(x) will always return a vector (in general it can be a scalar or an array of any dimension). Thus, there is often extra overhead introduced when converting to and from vectorised forms. It also makes it difficult to correctly handle edge cases, especially when dealing with recursive calls to bijector for nested distributions (such as product_distribution). See e.g. https://github.com/TuringLang/DynamicPPL.jl/issues/1142.
Docstrings
Bijectors.VectorBijectors.from_vec — Function
VectorBijectors.from_vec(d::Distribution)Returns a function that can be used to convert a vectorised sample from d back to its original form.
Bijectors.VectorBijectors.to_vec — Function
VectorBijectors.to_vec(d::Distribution)Returns a function that can be used to vectorise a sample from d.
Bijectors.VectorBijectors.from_linked_vec — Function
VectorBijectors.from_linked_vec(d::Distribution)Returns a function that can be used to convert an unconstrained vector back to a sample from d.
Bijectors.VectorBijectors.to_linked_vec — Function
VectorBijectors.to_linked_vec(d::Distribution)Returns a function that can be used to convert a sample from d to an unconstrained vector.
Bijectors.VectorBijectors.vec_length — Function
VectorBijectors.vec_length(d::Distribution)Returns the length of the vector representation of a sample from d, i.e., length(to_vec(d)(rand(d))). However, it does this without actually drawing a sample.
Bijectors.VectorBijectors.linked_vec_length — Function
VectorBijectors.linked_vec_length(d::Distribution)Returns the length of the unconstrained vector representation of a sample from d, i.e., length(to_linked_vec(d)(rand(d))). However, it does this without actually drawing a sample.
Bijectors.VectorBijectors.optic_vec — Function
VectorBijectors.optic_vec(d::Distribution)Returns a vector of optics (from AbstractPPL.jl), which describe how each element in the vectorised sample from d can be accessed from the original sample.
For example, if d = MvNormal(zeros(3), I), then optic_vec(d) would return a vector of [@opticof(_[1]), @opticof(_[2]), @opticof(_[3])]. The length of this vector would be equal to vec_length(d).
If optics = optic_vec(d), then for any sample x ~ d and its vectorised form v = to_vec(d)(x), it should hold that v[i] == optics[i](x) for all i.
Bijectors.VectorBijectors.linked_optic_vec — Function
VectorBijectors.linked_optic_vec(d::Distribution)Returns a vector of optics (from AbstractPPL.jl), which describe how each element in the unconstrained vector representation of a sample from d is related to the original sample.
This is not always well-defined. For example, consider a Dirichlet distribution, with three components. The unconstrained vector representation would have two elements. However, these two elements do not correspond to specific components of the original sample, since all three components are interdependent (they must sum to one). In such cases, this function should return a vector of two nothings.
However, for a distribution like MvNormal, this function would return a vector of [@opticof(_[1]), @opticof(_[2]), @opticof(_[3])], similar to optic_vec. That is because the first element of the unconstrained vector is solely determined by the first component of the original sample, the second element by the second component, and so on.
Note that, unlike optic_vec, the first element of the linked vector does not necessarily have to be exactly equal to the first component of the original sample, as there may have been a transformation applied. It merely needs to be determined by the first component (and only the first component).
Additional utilities
The VectorBijectors module also contains some functions that can be extended to provide better performance for distributions defined outside of Bijectors.jl / Distributions.jl.
Bijectors.VectorBijectors.has_constant_vec_bijector — Function
Bijectors.VectorBijectors.has_constant_vec_bijector(::Type{T}) where {T}Return true if the vector bijector for each element of a collection of distributions is determined solely by the type of the distribution, and not by any runtime parameter values.
This is slightly confusing, so is best explained by example. Consider
d = product_distribution(array_of_dists)If it can be inferred from typeof(array_of_dists) that each distribution inside array_of_dists has the same vector bijector, then has_constant_vec_bijector(typeof(array_of_dists)) should return true.
For example, if array_of_dists is a FillArrays.Fill of some distribution type, then we know that each distribution inside is the same, and so they all have the same vector bijector. Thus, we have that
has_constant_vec_bijector(::Type{<:FillArrays.Fill}) == trueFor generic AbstractArrays or Tuples, this will dispatch on the element type of the array. That means that if a dist::D (where D<:Distribution) has a constant vector bijector, we can simply mark has_constant_vec_bijector(::Type{D}) == true.
For example, Beta has a constant vector bijector, because its support is always between 0 and 1, regardless of its parameters.
On the other hand, Uniform does not have a constant vector bijector, because its support depends on its parameters.