Inference lifecycle
Every inference run goes through three phases: construction, activation and observation. This page runs the three phases on one small graph, block by block. Getting started runs the same phases and adds the free energy.
With RxInfer.jl, its infer function manages these phases. This page is for working with the engine directly.
The graph is one factor, the Gaussian node of the example node, a normal density with a known variance. It relates an observation y to a latent mean x.
using ReactiveMP, MessagePassingRulesBase, BayesBase, ExponentialFamily, Rocket
import ReactiveMP: activate!, FactorNodeActivationOptions, get_stream_of_marginals
include(joinpath(pkgdir(ReactiveMP), "docs", "nodes.jl"))Phase 1: Construction
You create the variables and factor nodes of the model, and connect them. Each variable is created by its role (see Variables): randomvar for a latent quantity you infer, datavar for an observation, and constvar for a value that never changes.
x = randomvar(label = :x)
y = datavar(label = :y)
v = constvar(2.0)A factornode connects a node type to the variables on its interfaces. An optional third argument gives the factorisation of the node's local marginals; without it, all interfaces form one cluster.
node = factornode(Gaussian, [(:out, y), (:μ, x), (:v, v)])FactorNode Gaussian (stochastic)
out ── y data
μ ── x random
v ── 2.0 constant
clusters: (out, μ, v)Each connection allocates the variable's stream of messages for that edge, a ReactiveMP.MessageObservable. All streams are lazy: they exist, but compute nothing.
[datavar: y] ──── [factor: node] ──── (randomvar: x)
unconnected unconnected
streams streamsA variable's connections are fixed during construction: a variable does not see a node created after the variable is activated.
Phase 2: Activation
Activation wires the lazy streams into a live network. You call ReactiveMP.activate! on each variable, then on each factor node, with an options object:
- A random variable takes
RandomVariableActivationOptions: theReactiveMP.MessageProductContexts its outbound messages and its marginal are computed with, and a stream postprocessor. - A data variable takes
DataVariableActivationOptions: whether to compute its predictions, and whether its values are a function of other variables. - A factor node takes
ReactiveMP.FactorNodeActivationOptions: the algorithm its rules run under, callbacks, annotations, diagnostics, services for its rules, a rule fallback, and whether to track log scales (see Activation options). - A constant needs no activation.
activate!(x, RandomVariableActivationOptions())
activate!(y, DataVariableActivationOptions())
activate!(node, FactorNodeActivationOptions())Variational message passing and loopy graphs need initial marginals or messages to start. You set them between the two steps, with ReactiveMP.set_initial_marginal! and ReactiveMP.set_initial_message!. This graph is a tree under belief propagation and needs none.
[datavar: y] ──── [factor: node] ──── (randomvar: x) ──► marginal q(x)
▲ rules streams
(waiting for connected connected
observations)After activation, every edge carries a stream subscribed to its sources. The marginal of x, a ReactiveMP.MarginalObservable, emits a new belief whenever the messages it is formed from change. Nothing is computed until something subscribes: to a marginal, with ReactiveMP.get_stream_of_marginals, or to the free energy, with bethe_free_energy.
subscription = subscribe!(get_stream_of_marginals(x), (q) -> println("q(x) updated: ", getdata(q)))Phase 3: Observation
Observations drive inference. You give them to the data variables with new_observation!:
new_observation!(y, 3.14)q(x) updated: ExponentialFamily.NormalMeanVariance{Float64}(μ=3.14, v=2.0)The observation is a PointMass(3.14) message on the data variable's outbound stream. It propagates through the connected node, whose rule computes its message towards x, and on to x, whose marginal updates. Without a prior, the marginal of x is that message, $\mathcal{N}(x \mid 3.14, 2)$.
new_observation!(y, 3.14)
│
▼
[datavar: y] ──► message ──► [factor: node] ──► message ──► (randomvar: x)
│
▼
marginal q(x) emitsEvery call to new_observation! propagates again. In streaming inference, each call is a new data point. In variational inference, the same data is given once per iteration, and each iteration updates the marginals and the free energy.
new_observation!(y, 1.0)q(x) updated: ExponentialFamily.NormalMeanVariance{Float64}(μ=1.0, v=2.0)A value that PointMass can represent is observed this way: a real number, an array of real numbers or a UniformScaling. Anything else is an error. Data that is deliberately not numeric, read by a custom node, is wrapped explicitly (see Non-standard observations).
When the run is over, unsubscribe:
unsubscribe!(subscription)Summary
| Phase | What happens | Key functions |
|---|---|---|
| Construction | Variables and nodes are created and connected; the streams are allocated, lazy | randomvar, datavar, constvar, factornode |
| Activation | The streams are wired into a live network; initial values are set; subscriptions are made | ReactiveMP.activate!, ReactiveMP.FactorNodeActivationOptions, ReactiveMP.get_stream_of_marginals |
| Observation | Data arrives, messages propagate, marginals update | new_observation! |
Next steps
- Factor nodes: how nodes are created and activated.
- Variables: the streams of each kind of variable.
- Callbacks: observing every rule call and product.
- Form constraints: constraining the form of marginals.