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.

Note

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             streams
Note

A 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:

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

Every 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

PhaseWhat happensKey functions
ConstructionVariables and nodes are created and connected; the streams are allocated, lazyrandomvar, datavar, constvar, factornode
ActivationThe streams are wired into a live network; initial values are set; subscriptions are madeReactiveMP.activate!, ReactiveMP.FactorNodeActivationOptions, ReactiveMP.get_stream_of_marginals
ObservationData arrives, messages propagate, marginals updatenew_observation!

Next steps