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).