This page runs belief propagation on a small model by hand. You compute every message with the rules RxInfer runs, multiply the messages at a variable to get its posterior, and check the posterior against the closed form. Then infer runs the same model and returns the same posteriors.
A message is a function of one variable, sent along an edge of a factor graph. A rule computes one message of one factor node from the messages on the node's other edges. Message passing introduces the algorithm; this page computes it step by step.
This is the sum-product rule of belief propagation. The posterior of a variable is the normalized product of the messages that arrive at it. In a chain, every variable sits between two nodes, so its posterior is the product of a forward and a backward message.
The node + relates three variables, $\mathrm{out} = \mathrm{in}_1 + \mathrm{in}_2$. Its node function is a Dirac delta, $f(\mathrm{out}, \mathrm{in}_1, \mathrm{in}_2) = \delta(\mathrm{out} - \mathrm{in}_1 - \mathrm{in}_2)$, and the function + itself is the node:
MessagePassingRulesBase.nodespec(+)
NodeSpec: +
interface
aliases
out
in
a group of any number of members
default algorithm
DefaultAlgorithm()
static inputs
none
min group length
2
defined
nodes/addition.jl:3
The interfaces are the node's edges: out, and in, a group with one member per term. A model creates this node when it writes s := z + u: z is the member (:in, 1) and u the member (:in, 2).
The message towards out integrates the delta out, which leaves a convolution of the two terms' messages:
For two normal messages $\mathcal{N}(m_1, v_1)$ and $\mathcal{N}(m_2, v_2)$, the convolution is $\mathcal{N}(m_1 + m_2, v_1 + v_2)$. @call_message_update_rule finds the rule for a node, a target and the inputs you give, and runs it. The messages on a group are a tuple, one per member:
@call_message_update_rule(
node = +, target = :out,
m = (in = (NormalMeanVariance(1.0, 1.0), NormalMeanVariance(2.0, 1.0)),),
)
RuleResult: message of + towards :outbelief propagation
Result
value
NormalMeanVariance{Float64}(μ=3.0, v=2.0)
type
NormalMeanVariance{Float64}
log scale
0 (declared)
Inputs
edge
value
in[1]
m
NormalMeanVariance{Float64}(μ=1.0, v=1.0)
in[2]
m
NormalMeanVariance{Float64}(μ=2.0, v=1.0)
Rule
declared inputs
m[:in...]::Union{NormalDistributionsFamily{T}, GaussianDistributionsFamily{T}} where T
The result draws the node. The input messages are solid arrows into the node, and the target is the double arrow out of it. The message is $\mathcal{N}(3, 2)$: the means add and the variances add. The value is a RuleResult, and getresult returns the message itself.
The message towards a term runs the sum backwards. Towards the second, (:in, 2), the delta sets $\mathrm{in}_2 = \mathrm{out} - \mathrm{in}_1$, and the message is $\mathcal{N}(m_{\mathrm{out}} - m_1, v_{\mathrm{out}} + v_1)$. The rule reads the other terms only, so the tuple holds nothing at the target's own position, as a graph delivers it:
@call_message_update_rule(
node = +, target = (:in, 2),
m = (out = NormalMeanVariance(5.0, 1.0), in = (NormalMeanVariance(1.0, 1.0), nothing)),
)
RuleResult: message of + towards (:in, 2)belief propagation
The node * relates $\mathrm{out} = A \cdot \mathrm{in}$. When the gain $A = a$ is known, its message is a point mass, PointMass(a), a distribution with all its mass at $a$. The message towards out scales the input: a normal $\mathcal{N}(m, v)$ becomes $\mathcal{N}(a m, a^2 v)$.
@call_message_update_rule(
node = *, target = :out,
m = (A = PointMass(2.0), in = NormalMeanVariance(1.0, 1.0)),
)
RuleResult: message of * towards :outmessages
Result
value
NormalMeanVariance{Float64}(μ=2.0, v=4.0)
type
NormalMeanVariance{Float64}
log scale
0 (declared)
Inputs
edge
value
A
m
PointMass{Float64}(2.0)
in
m
NormalMeanVariance{Float64}(μ=1.0, v=1.0)
Rule
declared inputs
m[:A]::PointMass{<:Real}, m[:in]::Union{NormalDistributionsFamily{T}, GaussianDistributionsFamily{T}} where T
Towards in, the delta evaluates the message on out at $a \cdot \mathrm{in}$:
\[\mu(\mathrm{in}) = \int \delta(\mathrm{out} - a \cdot \mathrm{in})\, \mu_{\mathrm{out}}(\mathrm{out}) \, \mathrm{d}\mathrm{out}
= \mathcal{N}(a \cdot \mathrm{in} \mid m, v) .\]
As a function of $\mathrm{in}$, this is a normal with precision $w = a^2 / v$ and weighted mean $\xi = a m / v$, the precision times the mean. It integrates to $1 / |a|$, not to one.
@call_message_update_rule(
node = *, target = :in,
m = (out = NormalMeanVariance(1.0, 1.0), A = PointMass(2.0)),
)
The message is $\mathcal{N}(0.5, 0.25)$ in weighted-mean form. Its log scale, $-\log 2$, is the logarithm of the constant $1/|a|$ that the message carries beyond a normalized distribution. The last section adds such constants up to the model's evidence. The card also lists matrix_correction, a service that the rule reads to keep a precision matrix invertible; its default serves here.
The observation enters as a point mass on the likelihood's out. The message towards μ, the mean, is the likelihood of the observation as a function of $s$:
likelihood_s = @call_message_update_rule(
node = NormalMeanVariance, target = :μ,
m = (out = PointMass(y), v = PointMass(0.5)),
)
RuleResult: message of NormalMeanVariance towards :μbelief propagation
The posterior of $x$ is the product of its forward and backward messages. BayesBase's prod(ClosedProd(), left, right) multiplies two densities in closed form, and returns a normalized distribution:
The closed form agrees. Given $x$, the observation is $y \sim \mathcal{N}(a x + 1, 1.5)$, since the variances of $u$ and of the sensor noise add. The posterior precision of $x$ is the prior's plus $a^2 / 1.5$, and its mean is $a (y - 1) / 1.5$ divided by that precision:
w = 1 / 10 + a^2 / 1.5
(mean = a * (y - 1) / 1.5 / w, var = 1 / w)
(mean = 0.963855421686747, var = 0.3614457831325301)
The model below is the same chain. := defines a deterministic relationship, so z and s are the outputs of * and +:
@model function tracking_step(y, a)
x ~ NormalMeanVariance(0.0, 10.0)
u ~ NormalMeanVariance(1.0, 1.0)
z := a * x
s := z + u
y ~ NormalMeanVariance(s, 0.5)
end
result = infer(model = tracking_step(a = a), data = (y = y,), free_energy = true)
Inference results:
Posteriors | available for (s, z, u, x)
Free Energy: | [2.82998]
infer runs the rules you ran, in the order their inputs arrive, and multiplies the messages at each variable:
(
x = (by_hand = mean_var(q_x), rxinfer = mean_var(result.posteriors[:x])),
s = (by_hand = mean_var(q_s), rxinfer = mean_var(result.posteriors[:s])),
)
RxInfer gives the rules of a stochastic node its observations and constants as point-mass marginals, q = (…), rather than as messages. For a point mass the two are the same distribution, and the rules return the same message.
The model's evidence is the probability of the observation, $p(y) = \mathcal{N}(y \mid 1, a^2 \cdot 10 + 1 + 0.5)$. On a graph without loops, belief propagation is exact, and the Bethe free energy that free_energy = true computes equals $-\log p(y)$:
The log scales of the messages add up to the same number. With logscales = true, every message and posterior carries its log scale, and the log scale of a posterior is $\log p(y)$:
Variational message passing by hand runs a model where belief propagation has no closed form, and minimizes the free energy with a factorized posterior.