Migration Guide
This page describes the major changes between GraphPPL v3
and v4
. The v4
introduced many changes to the language and the API. The changes are designed to make the language more consistent and easier to use, but also are tested better and provides extra features for the future releases. These changes are, however, not backward compatible with the previous versions of GraphPPL, such as v3
. In this guide, we will describe the major changes between the two versions and provide examples to help you migrate your code to the new version.
The @model
macro
The @model
macro is no longer exported from the library and is only provided for interactive purposes (e.g. plotting). The downstream package must define their own @model
macro and use the GraphPPL.model_macro_interior
function with a custom backend.
Model definition
Model definition in version 4
is similar to version 3
. The main difference is the deletion of the datavar
, randomvar
and constvar
syntax. Previously the random variables and data variables needed to be specified in advance. This is no longer possible but also is unnecessary. GraphPPL
is able to infer the type of the variable based on the way in which it is used. This greatly trims down the amount of code to be written in the model definition.
The following example is a simple model definition in version 3
:
@model function SSM(n, x0, A, B, Q, P)
x = randomvar(n)
y = datavar(Vector{Float64}, n)
x_prior ~ MvNormal(μ = mean(x0), Σ = cov(x0))
x_prev = x_prior
for i in 1:n
x[i] ~ MvNormal(μ = A * x_prev, Σ = Q)
y[i] ~ MvNormal(μ = B * x[i], Σ = P)
x_prev = x[i]
end
end
The equivalent model definition in version 4
is as follows:
@model function SSM(y, prior_x, A, B, Q, P)
x_prev ~ prior_x
for i in eachindex(y)
x[i] ~ MvNormal(μ = A * x_prev, Σ = Q)
y[i] ~ MvNormal(μ = B * x[i], Σ = P)
x_prev = x[i]
end
end
As you can see, variable creation still requires the ~
operator. However, there are a couple of subtle changes compared to the old version of GraphPPL:
- The
randomvar
anddatavar
syntax is no longer needed.GraphPPL
is able to infer the type of the variable based on the way in which it is used. - The data
y
is an explicit parameter of the model function. - The
n
parameter is no longer needed. The size of the variablex
is inferred from the size of the variabley
. - We are no longer required to extract the mean and covariance of our prior distribution using the
MvNormal(μ = mean(x0), Σ = cov(x0))
pattern. Instead, we can pass a prior and callx_prev ~ prior_x
to assign it to an edge in the factor graph. - The data
y
is passed as an argument to the model. This is because of the support of nested models in version4
. In the Nested models we elaborate more on this design choice.
Vectors and arrays
As seen in the example above, we can assign x[i]
without explicitly defining x
first. GraphPPL
is able to infer that x
is a vector of random variables, and will grow the internal representation of x
accordingly to accomodate i
elements. Note that this works recursively, so z[i, j]
will define a matrix of random variables. GraphPPL does check that the index [i,j]
is compatible with the shape of the variable z
.
eachindex(y)
works only if y
has a static data associated with it. Read the documentation for GraphPPL.create_model
for more information.
Factor aliases
In version 4
, we can define factor aliases to define different implementations of the same factor. For example, in RxInfer.jl
, there are multiple implentations of the Normal
distribution. Previously, we would have to explicitly call NormalMeanVariance
or NormalMeanPrecision
. In version 4
, we can define factor aliases to default to certain implementations when specific keyword arguments are used on construction. For example: Normal(μ = 0, σ² = 1)
will default to NormalMeanVariance
and Normal(μ = 0, τ = 1)
will default to NormalMeanPrecision
. This allows users to quickly toggle between different implementations of the same factor, while keeping an implementation agnostic model definition.
This feature works only in the combination with the ~
operator, which creates factor nodes. Therefore it cannot be used to instantiate a distribution object in a regular Julia code.
Nested models
The major difference between versions 3
and 4
is the support for nested models. In version 4
, models can be nested within each other. This allows for more complex models to be built in a modular way. The following example demonstrates how to nest models in version 4
:
@model function kalman_filter_step(y, prev_x, new_x, A, B, Q, P)
new_x ~ MvNormal(μ = A * prev_x, Σ = Q)
y ~ MvNormal(μ = B * new_x, Σ = P)
end
@model function state_space_model(y, A, B, Q, P)
x[1] ~ MvNormal(zeros(2), diagm(ones(2)))
for i in eachindex(y)
y[i] ~ kalman_filter_step(prev_x = x[i], new_x = new(x[i + 1]), A=A, B=B, Q=Q, P=P)
end
end
Note that we reuse the kalman_filter_step
model in the state_space_model
model. In the argument list of any GraphPPL
model, we have to specify the Markov Blanket of the model we are defining. This means that all interfaces with the outside world have to be passed as arguments to the model. For the kalman_filter_step
model, we pass the previous state prev_x
, the new state new_x
, the observation y
as well as the parameters A
, B
, Q
and P
. This means that, when invoking a submodel in a larger model, we can specify all components of the Markov Blanket. Note that, in the state_space_model
model, we do not pass y
as an argument to the kalman_filter_step
model. GraphPPL
will infer that y
is missing from the argument list and assign it to whatever is left of the ~
operator. Note that we also use the new(x[i + 1])
syntax to create a new variable in the position of x[i + 1]
. Since y
is also passed in the argument list of the state_space_model
model, we could have written this line with the equivalen statement x[i + 1] ~ kalman_filter_step(prev_x = x[i], y = y[i], A=A, B=B, Q=Q, P=P)
. However, to respect the generative direction of the model and to make the code more readable, we use the new(x[i + 1])
syntax. Note, however, that the underlaying representation of the models in GraphPPL
are still undirected.
Constraint specification
With the introduction of nested models, the specification of variational constraints becomes more difficult. In version 3
, variable names were uniquely defined in the model, which made it easy to specify constraints on variables. In version 4
, nested models can contain variables with the same name as their parents, even though they are distinct random variables. Therefore, we need to specify constraints on submodel level in the constraints macro. This is done with the for q in _submodel_
syntax.
For example:
@constraints begin
for q in kalman_filter_step
q(new_x, prev_x, y) = q(new_x, prev_x)q(y)
end
end
This specification reuses the same constraint to all instances of the kalman_filter_step
submodel. Of course, we'd like to have more flexibility in the constraints we can specify. Therefore, we can also specify constraints on a specific instance of the submodel. For example:
@constraints begin
for q in (kalman_filter_step, 1)
q(new_x, prev_x, y) = q(new_x, prev_x)q(y)
q(new_x) :: Normal
end
end
This pushes the constraint to the first instance of the kalman_filter_step
submodel. With this syntax, we can specify constraints on any instance of a submodel.
The function syntax for constraints is still supported. For example:
@constraints function ssm_constraints(factorize)
for q in kalman_filter_step
if factorize
q(new_x, prev_x, y) = MeanField()
else
q(new_x, prev_x, y) = q(new_x, prev_x)q(y)
end
end
end
Meta specification
The meta specification follows exactly the same structure as the constraints specification. Nested models in the @meta
macro are specified in the same way as in the @constraints
macro.
For example:
@meta begin
for meta in some_submodel
GCV(x, k, w) -> GCVMetadata(GaussHermiteCubature(20))
end
y -> SomeMetaData()
end
Additionally, we can pass arbitrary metadata to the inference backend. For example:
@meta begin
GCV(x, k, w) -> GCVMetadata(GaussHermiteCubature(20))
x -> (prod_constraint = SomeProdConstraint(), )
end
By passing a NamedTuple
in the @model
macro, we can pass arbitrary metadata to the inference backend that we would previously have to specify in the where
clause of a node. With the added functionality of the @meta
macro, we can pass metadata to the inference backend in a more structured way, and detach metadata definition from model definition.
Dispatch
In the new version of GraphPPL.jl
it is no longer possible to use Julia's multiple dispatch within the @model
function definition. The workaround is to use nested models instead, e.g. users can pass submodels as an extra argument, therefore change the structure of the model based on the input arguments.
Positional arguments
In the new version of GraphPPL.jl
positional arguments within model construction is no longer supported and all arguments must be named explicitly. For example:
@model function beta_bernoulli(y, a, b)
t ~ Beta(a, b)
for i in eachindex(y)
y[i] ~ Bernoulli(t)
end
end
model = beta_bernoulli(1.0, 2.0) # ambigous and unsupported
model = beta_bernoulli(a = 1.0, b = 2.0) # explicitly defined via keyword arguments
The same recipe applies when nesting models within each other, e.g.
@model function bigger_model(y)
a ~ Uniform(0.0, 10.0)
b ~ Uniform(0.0, 10.0)
y ~ beta_bernoulli(a = a, b = b)
end
Mixture Nodes
The interface of the mixture nodes does no longer require a tuple of inputs. Instead, the inputs can be passed as a vector as well. For example:
@model function mixture_model(y)
# specify two models
m_1 ~ Beta(2.0, 7.0)
m_2 ~ Beta(7.0, 2.0)
# specify prior on switch param
s ~ Bernoulli(0.7)
# specify mixture prior Distribution
θ ~ Mixture(switch = s, inputs = [m_1, m_2])
end
Overview of the removed functionality
- The
datavar
,randomvar
andconstvar
syntax is removed.GraphPPL
is able to infer the type of the variable based on the way in which it is used. - Specifying factorization constraints in the
where
clause of a node is no longer possible. Thewhere
syntax can still be used to specify metadata for factor nodes, but factorization constraints can only be specified with the@constraints
macro. - Dispatch in the
@model
macro is not supported. - Positional arguments in the
@model
macro are not supporeted.