Variables
A variable is an edge of the factor graph: a quantity that the factor nodes connected to it share. The engine has three kinds of variable, all subtypes of ReactiveMP.AbstractVariable.
The examples use the Gaussian node of The example node.
Choosing the right variable type
| Kind | Constructor | Holds | Changes by |
|---|---|---|---|
RandomVariable | randomvar | a latent quantity, whose posterior you infer | inference, which updates its marginal |
DataVariable | datavar | an observed quantity | new_observation! |
ConstVariable | constvar | a fixed value | nothing: it is set at creation |
Use a random variable for every quantity you want a posterior for. Use a data variable for an observation, especially one that changes between runs, as in streaming inference. Use a constant for a hyperparameter or any other value that never changes.
x = randomvar(label = :x)
y = datavar(label = :y)
noise = constvar(1.0, label = :noise)
ReactiveMP.israndom(x), ReactiveMP.isdata(y), ReactiveMP.isconst(noise)(true, true, true)The label names the variable in displays, callback events and error messages.
Variables as reactive streams
A variable holds no single value. It holds reactive streams, which emit values once the graph is live:
- a marginal stream, a
ReactiveMP.MarginalObservable, which emits the variable's marginal whenever it changes; - one message stream per connected node, a
ReactiveMP.MessageObservable, which carries the messages between the variable and that node.
Connecting a node to a variable creates its message streams:
prior = factornode(Gaussian, [(:out, x), (:μ, constvar(0.0)), (:v, constvar(10.0))])
likelihood = factornode(Gaussian, [(:out, y), (:μ, x), (:v, noise)])
ReactiveMP.degree(x), ReactiveMP.degree(y), ReactiveMP.degree(noise)(2, 1, 1)The ReactiveMP.degree of a variable is its number of connections: x connects to both nodes. The streams stay lazy while the graph is built. ReactiveMP.activate! wires them into a live network, variables first, then nodes:
activate!(x, RandomVariableActivationOptions())
activate!(y, DataVariableActivationOptions())
activate!(prior, FactorNodeActivationOptions())
activate!(likelihood, FactorNodeActivationOptions())
posteriors = Marginal[]
subscription = subscribe!(get_stream_of_marginals(x), (q) -> push!(posteriors, q))
new_observation!(y, 2.0)
last(posteriors)Marginal(ExponentialFamily.NormalWeightedMeanPrecision{Float64}(xi=2.0, w=1.1))The observation propagates through the graph: the likelihood sends a message to x, and x multiplies it with the prior's message. Inference lifecycle describes the order of construction, activation and observation.
ReactiveMP.AbstractVariable — Type
AbstractVariableThe supertype of the variables of a factor graph: RandomVariable, a latent quantity; DataVariable, an observed one; and ConstVariable, a constant.
A subtype implements:
ReactiveMP.degree, the number of connections to factor nodes;ReactiveMP.israndom,ReactiveMP.isdataandReactiveMP.isconst;ReactiveMP.create_new_stream_of_inbound_messages!, which a node's interface calls once per connection;ReactiveMP.get_stream_of_inbound_messages(variable, index), the stream of the messages the node on connectionindexsends to the variable, andReactiveMP.get_stream_of_outbound_messages(variable, index), that of the messages the variable sends to it;ReactiveMP.get_stream_of_marginalsandReactiveMP.set_stream_of_marginals!,ReactiveMP.get_stream_of_predictionsandReactiveMP.set_stream_of_predictions!;ReactiveMP.activate!(variable, options), when it has streams to wire.
A variable has a label field, which the callback events show.
ReactiveMP.activate! — Function
ReactiveMP.activate!(randomvar::RandomVariable, options::RandomVariableActivationOptions)
ReactiveMP.activate!(datavar::DataVariable, options::DataVariableActivationOptions)
ReactiveMP.activate!(factornode::FactorNode, options::FactorNodeActivationOptions)Wire the lazy streams of a built graph into a live one. A graph is activated in order, once every node is created: its random and data variables first, then the initial marginals and messages, then its factor nodes, whose rules read the variables' streams. A constant needs no activation. After it, each new observation propagates through the graph.
See each method: ReactiveMP.activate!(::RandomVariable, ::RandomVariableActivationOptions), ReactiveMP.activate!(::DataVariable, ::DataVariableActivationOptions), ReactiveMP.activate!(::FactorNode, ::ReactiveMP.FactorNodeActivationOptions).
Common variable API
Every kind of variable answers the same questions: which kind it is, how many nodes it connects to, and what its streams are.
Type predicates
ReactiveMP.israndom — Function
ReactiveMP.israndom(variable::AbstractVariable) -> Bool
ReactiveMP.israndom(variables::AbstractArray{<:AbstractVariable}) -> BoolWhether variable is a RandomVariable; for an array, whether every element is. A node interface answers for its variable.
ReactiveMP.isdata — Function
ReactiveMP.isdata(variable::AbstractVariable) -> Bool
ReactiveMP.isdata(variables::AbstractArray{<:AbstractVariable}) -> BoolWhether variable is a DataVariable; for an array, whether every element is. A node interface answers for its variable.
ReactiveMP.isconst — Function
ReactiveMP.isconst(variable::AbstractVariable) -> Bool
ReactiveMP.isconst(variables::AbstractArray{<:AbstractVariable}) -> BoolWhether variable is a ConstVariable; for an array, whether every element is. A node interface answers for its variable.
ReactiveMP.degree — Function
ReactiveMP.degree(variable::AbstractVariable) -> IntThe number of factor node connections of variable: for a random or a data variable the number of its inbound message streams, for a constant the number of nodes connected to it.
Marginal and message streams
ReactiveMP.get_stream_of_marginals returns a variable's marginal stream, and activation connects it with ReactiveMP.set_stream_of_marginals!. A late subscriber receives the latest marginal at once.
Some graphs need a value before inference computes one. In a variational graph, a rule may read a marginal that depends, through other rules, on the rule's own message. You break the cycle with an initial marginal, ReactiveMP.set_initial_marginal!, or an initial message, ReactiveMP.set_initial_message!. The chain below has a mean-field node between x1 and x2:
x1, x2, y2 = randomvar(label = :x1), randomvar(label = :x2), datavar(label = :y2)
chain = (
factornode(Gaussian, [(:out, x1), (:μ, constvar(0.0)), (:v, constvar(10.0))]),
factornode(Gaussian, [(:out, x2), (:μ, x1), (:v, constvar(1.0))], ((:out,), (:μ,), (:v,))),
factornode(Gaussian, [(:out, y2), (:μ, x2), (:v, constvar(1.0))]),
)
foreach(v -> activate!(v, RandomVariableActivationOptions()), (x1, x2))
activate!(y2, DataVariableActivationOptions())
foreach(n -> activate!(n, FactorNodeActivationOptions()), chain)
set_initial_marginal!(x1, NormalMeanVariance(0.0, 1.0))
q1 = Marginal[]
chain_subscription = subscribe!(get_stream_of_marginals(x1), (q) -> push!(q1, q))
for iteration in 1:20
new_observation!(y2, 2.0)
end
mean(last(q1)), 2.0 * 10 / 12(1.6666664304039827, 1.6666666666666667)Each observation runs one iteration. The mean of q(x1) converges to the exact posterior mean, $2 \cdot 10 / 12$. Without the initial marginal, the middle node's rule towards out waits for q(x1). That marginal waits for the node's message towards μ, which waits for q(x2), which waits for the rule towards out. Nothing is computed.
ReactiveMP.get_stream_of_marginals — Function
ReactiveMP.get_stream_of_marginals(variable::AbstractVariable) -> MarginalObservableThe stream of the marginals of variable, a ReactiveMP.MarginalObservable: the posterior of a random variable, the latest observation of a data variable, the constant of a constant. Subscribe to it to receive every update:
subscribe!(ReactiveMP.get_stream_of_marginals(x), (q) -> println(mean(q)))See also ReactiveMP.set_initial_marginal!, ReactiveMP.set_stream_of_marginals!.
ReactiveMP.set_stream_of_marginals! — Function
ReactiveMP.set_stream_of_marginals!(variable::AbstractVariable, stream)Connect the marginal stream of variable to stream, which activation does.
Throws
ErrorExceptionfor aConstVariable, whose marginal is fixed.
See also ReactiveMP.get_stream_of_marginals.
ReactiveMP.set_initial_marginal! — Function
ReactiveMP.set_initial_marginal!(variable::AbstractVariable, marginal)
ReactiveMP.set_initial_marginal!(variables::AbstractArray{<:AbstractVariable}, marginals)Seed the marginal stream of variable with marginal, a distribution, as an initial Marginal: what a rule that depends on it reads before inference computes one. For an array, a single PointMass or Distribution seeds every variable, and a collection seeds each variable with its element.
The initial marginal carries no log scale, even where log scales are tracked.
Throws
AssertionErrorwhen an array of variables and a collection of marginals differ in length.
See also ReactiveMP.set_initial_message!.
ReactiveMP.set_initial_message! — Function
ReactiveMP.set_initial_message!(variable::RandomVariable, message)
ReactiveMP.set_initial_message!(variables::AbstractArray{<:AbstractVariable}, messages)Seed every message variable sends to its nodes with message, a distribution, as an initial Message: what a rule that depends on it reads before inference computes one. For an array, a single PointMass or Distribution seeds every variable, and a collection seeds each variable with its element.
A random variable's outbound message streams exist once it is activated, so this is called after ReactiveMP.activate! of the variable and before that of its nodes. The initial message's log scale is undefined.
Throws
ArgumentErrorfor a random variable not activated yet, and for aDataVariableor aConstVariable, whose messages are their observations and their value;AssertionErrorwhen an array of variables and a collection of messages differ in length.
See also ReactiveMP.set_initial_marginal!.
Prediction streams
A data variable's prediction is the product of the messages its nodes send to it: what the model predicts for the observation without the observation itself. You ask for it when you activate the variable, with the first field of DataVariableActivationOptions:
xp, yp = randomvar(label = :x), datavar(label = :y)
nodes = (
factornode(Gaussian, [(:out, xp), (:μ, constvar(0.0)), (:v, constvar(10.0))]),
factornode(Gaussian, [(:out, yp), (:μ, xp), (:v, constvar(1.0))]),
)
activate!(xp, RandomVariableActivationOptions())
activate!(yp, DataVariableActivationOptions(true, false, nothing, nothing))
foreach(n -> activate!(n, FactorNodeActivationOptions()), nodes)
predictions = Marginal[]
prediction_subscription = subscribe!(get_stream_of_predictions(yp), (p) -> push!(predictions, p))
new_observation!(yp, 2.0)
last(predictions)Marginal(ExponentialFamily.NormalMeanVariance{Float64}(μ=0.0, v=11.0))The prediction is the prior predictive, $\mathcal{N}(0, 10 + 1)$. A random variable's prediction stream is its marginal stream.
ReactiveMP.get_stream_of_predictions — Function
ReactiveMP.get_stream_of_predictions(variable::AbstractVariable)The stream of the predictions of variable, a ReactiveMP.MarginalObservable:
- for a
DataVariable, the product of the messages the nodes send to it, what the model predicts for the observation without it; built only when the variable is activated withprediction = true(seeDataVariableActivationOptions), and never emitting otherwise; - for a
RandomVariableand aConstVariable, its marginal stream.
See also ReactiveMP.set_stream_of_predictions!.
ReactiveMP.set_stream_of_predictions! — Function
ReactiveMP.set_stream_of_predictions!(variable::DataVariable, stream)Connect the prediction stream of a data variable to stream, which activation does.
Throws
ErrorExceptionfor aRandomVariableand aConstVariable, whose predictions are their marginals.
See also ReactiveMP.get_stream_of_predictions.
Random variables
A random variable is a latent quantity. Its marginal is the product of all the messages its nodes send to it, and the message it sends to one node is the product of the messages from all the others. With more than one connection, it computes those products along an equality chain, which shares the partial products between them (see Internals).
ReactiveMP.RandomVariable — Type
RandomVariable <: AbstractVariableA latent variable, the quantity inference computes a posterior for. It has a stream of inbound messages per connected factor node, as many outbound ones once it is activated, and the stream of its marginal, the product of its inbound messages. Create one with randomvar.
See also ConstVariable, DataVariable.
ReactiveMP.randomvar — Function
randomvar(; label = nothing) -> RandomVariableCreate a RandomVariable, with no connections yet. label names it in the callback events and in error messages, and is any value.
Examples
julia> x = randomvar(label = :x);
julia> ReactiveMP.israndom(x), ReactiveMP.degree(x)
(true, 0)Its activation options say how it multiplies messages: two ReactiveMP.MessageProductContexts, one for its outbound messages and one for its marginal, and a stream postprocessor. The product contexts carry the form constraints and the product callbacks.
ReactiveMP.RandomVariableActivationOptions — Type
RandomVariableActivationOptions(stream_postprocessor, prod_context_for_message_computation::MessageProductContext, prod_context_for_marginal_computation::MessageProductContext)
RandomVariableActivationOptions()What activating a RandomVariable needs, given positionally. With no arguments, no postprocessor and two default ReactiveMP.MessageProductContexts.
Fields
stream_postprocessor: the stream postprocessor applied to the streams of the variable'sReactiveMP.EqualityChainand to its marginal stream, ornothing(seeReactiveMP.AbstractStreamPostprocessor);prod_context_for_message_computation: how an outbound message, the product of every inbound message but the one on its own connection, is computed;prod_context_for_marginal_computation: how the marginal, the product of every inbound message, is computed. Its callbacks also receiveReactiveMP.BeforeMarginalComputationEventandReactiveMP.AfterMarginalComputationEvent, and the variable's free-energy events,ReactiveMP.BeforeVariableBoundEntropyEventandReactiveMP.AfterVariableBoundEntropyEvent.
See also ReactiveMP.activate!.
ReactiveMP.activate! — Method
ReactiveMP.activate!(randomvar::RandomVariable, options::RandomVariableActivationOptions)Wire the streams of a random variable, after every factor node it connects to has been created.
- Outbound messages: one stream per connection. With more than one connection, the message to connection
iis the product of the inbound messages on every other connection, computed along aReactiveMP.EqualityChain, which reuses the partial products. With one connection, the outbound message never emits: there is nothing to multiply. - Marginal: the product of all inbound messages, formed with
as_marginal. It is computed once every inbound message has a value, and again whenever each has updated since.
A product that is initial does not consume its inputs, so the next update of any of them computes it again.
Throws
ArgumentErrorfor a variable with no connections.
See also RandomVariableActivationOptions.
Data variables
A data variable is an observed quantity. It has no value until you give it one with new_observation!, and each new value propagates through the graph. The observation reaches the nodes as a point mass, a PointMass from BayesBase.
The value missing says that an observation is not available. A rule that depends on a missing message gives missing in turn, and a product ignores a missing side. Here the posterior of x falls back to its prior:
new_observation!(y, missing)
last(posteriors)Marginal(ExponentialFamily.NormalMeanVariance{Float64}(μ=0.0, v=10.0))An observation must be a real number, an array of real numbers or a UniformScaling. Anything else is an error that names the variable:
try
new_observation!(y, Normal(0.0, 1.0))
catch err
showerror(stdout, err)
endInvalid observation for `y`: `Distributions.Normal{Float64}` cannot be used as observed data.
Observations must be a real number, an array of real numbers, or a `UniformScaling`. Got a value of type `Distributions.Normal{Float64}`.
If you meant to indicate that this observation is not available, pass `missing` instead.
Passing a distribution as data is not supported: data variables hold observed point values, not beliefs. To place a prior on a quantity, make it a random variable in the model instead.ReactiveMP.DataVariable — Type
DataVariable <: AbstractVariableAn observed variable, whose value arrives after the graph is built, with new_observation!, and may change: each observation propagates through the graph. Every connected node shares its one outbound stream, of the observations, and has a stream of the messages it sends back, whose product is the variable's prediction. Its marginal is its latest observation. Create one with datavar.
See also RandomVariable, ConstVariable.
ReactiveMP.datavar — Function
datavar(; label = nothing) -> DataVariableCreate a DataVariable, with no observation and no connections yet. label names it in the callback events and in the error of an invalid observation.
Examples
julia> y = datavar(label = :y);
julia> ReactiveMP.isdata(y), ReactiveMP.degree(y)
(true, 0)ReactiveMP.new_observation! — Function
new_observation!(datavar::DataVariable, data)
new_observation!(datavar::DataVariable, data::PointMass)
new_observation!(datavar::DataVariable, ::Missing)
new_observation!(datavars::AbstractArray{<:DataVariable}, data::AbstractArray)
new_observation!(datavars::AbstractArray{<:DataVariable}, ::Missing)Observe data on a data variable, and propagate it through the graph: the variable sends it to its nodes as a PointMass message, with log scale zero.
- A real number, an array of real numbers or a
UniformScalingis wrapped in aPointMass: these are the values for whichPointMassdefines avariate_form, and so amean. - A
PointMassis sent as it is, without validation: this is how an observation of another kind, such as text a custom node reads withBayesBase.getpointmass, is given. See Non-standard observations. missingsays the observation is not available: the message ismissing, and the rules that depend on it givemissingin turn.- An array of data variables observes an array of values of the same size, element by element, or
missingon every variable.
Throws
ErrorExceptionfor any other value, a distribution included, naming the variable and the type;AssertionErrorwhen an array of data variables and an array of values differ in size.
Examples
julia> y = datavar();
julia> new_observation!(y, 1.5)
julia> new_observation!([datavar(), datavar()], [1.0, 2.0])Non-standard observations
A node of your own may take observations that are not numbers, such as text. You pass such a value wrapped in a PointMass yourself, which new_observation! sends unchecked. The node's rules dispatch on the concrete PointMass type and read the payload with BayesBase.getpointmass.
The node below observes a text through its number of words, a Poisson count with rate θ. As a function of the rate, the likelihood of n words is $\theta^n e^{-\theta}$, a Gamma density with shape $n + 1$ and rate 1:
struct WordCount end
@define_factor_node(node = WordCount, type = Stochastic, interfaces = [:out, :θ])
@define_message_update_rule(
node = WordCount, target = :θ,
args = (m[:out]::PointMass{<:AbstractString},),
body = (args) -> GammaShapeRate(length(split(BayesBase.getpointmass(args.m[:out]))) + 1, 1.0),
)
θ, text = randomvar(label = :θ), datavar(label = :text)
counter = factornode(WordCount, [(:out, text), (:θ, θ)])
activate!(θ, RandomVariableActivationOptions())
activate!(text, DataVariableActivationOptions())
activate!(counter, FactorNodeActivationOptions())
rates = Marginal[]
rate_subscription = subscribe!(get_stream_of_marginals(θ), (q) -> push!(rates, q))
new_observation!(text, PointMass("the quick brown fox"))
last(rates)Marginal(ExponentialFamily.GammaShapeRate{Float64}(a=5.0, b=1.0))A PointMass of a non-numeric payload has limits you must respect:
- It has no
variate_form, and therefore nomean,varorlogpdf. A rule reads it withBayesBase.getpointmass, which is not exported. Callingmeanon it recurses until the stack overflows. - Only nodes written for the payload can connect to the variable. The numeric nodes of the rule packages compute with the moments of their inputs.
- The Bethe free energy is not defined for such an edge, since it needs log-densities.
new_observation! does not validate a PointMass: PointMass(Beta(1, 1)) goes through as readily as PointMass("text"). You make sure that only rules which understand the payload ever receive it. @define_message_update_rule describes how to write them.
Activation
A data variable's activation options ask for its prediction stream, or link its observations to other variables through a function.
ReactiveMP.DataVariableActivationOptions — Type
DataVariableActivationOptions(prediction::Bool, linked::Bool, transform, args)
DataVariableActivationOptions()What activating a DataVariable needs, given positionally. With no arguments, no prediction and no link.
Fields
prediction: whether to build the prediction stream, the product of the messages the nodes send to the variable (seeReactiveMP.get_stream_of_predictions);linked: whether the variable's observations are a function of other variables, rather than given withnew_observation!;transform: for a linked variable, the function its observation is of;args: for a linked variable, the constants, variables and arrays of variablestransformtakes, in order.transformis given their point values: a constant, a data variable's observation (the payload of itsPointMass, numeric or not) or, for an array, the vector of its variables' observations.
See also ReactiveMP.activate!.
ReactiveMP.activate! — Method
ReactiveMP.activate!(datavar::DataVariable, options::DataVariableActivationOptions)Wire the streams of a data variable, after every factor node it connects to has been created.
- Prediction, with
options.prediction: the product of the messages the nodes send to the variable, computed once every one has a value and again whenever each has updated since. - Link, with
options.linked: whenever each ofoptions.argshas a value, and again when one updates,options.transformof their values is observed withnew_observation!. Linking to a random variable fails at the first update, since its marginal is not a point value. - Marginal: always the latest observation, as a
Marginal.
See also DataVariableActivationOptions.
Constant variables
A constant holds a fixed value as a clamped PointMass, whose log scale is zero. Its streams are complete when you create it, so it needs no activation:
constant_subscription = subscribe!(get_stream_of_marginals(noise), (q) -> println(q))Marginal(BayesBase.PointMass{Float64}(1.0)) with logscale = 0A constant receives no messages, and its marginal cannot be rewired.
ReactiveMP.ConstVariable — Type
ConstVariable <: AbstractVariableA constant of the model, fixed when it is created. Its message and its marginal are a clamped PointMass of the constant, with log scale zero, wired at creation: a constant needs no activation, and every node connected to it shares its one message stream. A node's interface on a constant sends it no message. Create one with constvar.
See also RandomVariable, DataVariable.
ReactiveMP.constvar — Function
constvar(constant; label = nothing) -> ConstVariableCreate a ConstVariable holding constant, a number, an array or any other value a PointMass holds. label names it in the callback events and in error messages.
Examples
julia> c = constvar(2.0);
julia> ReactiveMP.isconst(c), ReactiveMP.degree(c)
(true, 0)How the streams are built
This section is for contributors who implement a variable or read the engine's code.
A node's interface calls ReactiveMP.create_new_stream_of_inbound_messages! once per connection. The stream it returns carries the node's messages to the variable: the interface's outbound message is the variable's inbound one.
- A random variable allocates a new inbound stream per connection. Activation creates as many outbound streams, fed by the equality chain, and connects the marginal stream to the product of the inbound messages. With one connection, its outbound message never emits.
- A data variable also allocates an inbound stream per connection; their product is its prediction. Every connected node reads the same outbound stream, of its observations.
- A constant counts its connections and returns the same stream, of its one message, to each of them. It has no inbound streams: asking for one, or setting its marginal or prediction stream, is an error.
ReactiveMP.create_new_stream_of_inbound_messages! — Function
ReactiveMP.create_new_stream_of_inbound_messages!(variable::AbstractVariable) -> (observable, index)Allocate the stream of the messages a new factor node connection sends to variable, and return it with the connection's index. A ReactiveMP.NodeInterface calls it when it is created, and keeps the stream as its outbound message stream: the node's outbound message is the variable's inbound one. The stream is a lazy ReactiveMP.MessageObservable until the node is activated.
A random and a data variable allocate a new stream per connection, numbered from 1. A constant counts the connection and returns its one shared stream, of the constant's message, with index 1 every time.