Marginals
A marginal is a belief. The marginal of a variable is the normalised product of the messages that arrive at it. The marginal of a cluster of a factor node is the node's local joint marginal. You run inference for the marginals, variational rules read them, and the free energy is computed from them.
The examples use the Gaussian node of The example node.
The marginal type
Every marginal is a Marginal. Like a Message, it holds its data and forwards the statistics to it, and it records whether it is clamped and whether it is initial:
marginal = Marginal(NormalMeanPrecision(0.0, 1.0), false, true)
mean(marginal), precision(marginal), is_initial(marginal)(0.0, 1.0, true)ReactiveMP.Marginal — Type
Marginal(data, is_clamped::Bool, is_initial::Bool)
Marginal(data, is_clamped::Bool, is_initial::Bool, annotations::AnnotationDict)
Marginal(data, is_clamped::Bool, is_initial::Bool, annotations::AnnotationDict, logscale)A marginal, the belief about a variable or a cluster of a factor node's variables: its data, usually a distribution, with the flags and the metadata the engine tracks for it.
Arguments
data: the marginal itself, usually a distribution; the joint of a cluster may be aFactorizedCluster, and a marginal with amissinginput ismissing;is_clamped: whether the marginal comes from constants and observations alone. A clamped value is the result of computations on constants and observations alone: it never
changes during inference.
is_initial: whether the marginal was set before inference or computed from initial values only. An initial value was set before inference, withReactiveMP.set_initial_message!or
ReactiveMP.set_initial_marginal!, or computed from clamped and initial values only, at least one of them initial.
annotations: theReactiveMP.AnnotationDictof optional metadata. By default the engine's shared empty annotations, which nothing may write to: a marginal meant to be annotated is given its ownAnnotationDict();logscale: the log scale of the product of messages the marginal was formed from (seegetlogscale), ornothing, the default.
The statistics of the data are forwarded: mean, median, mode, var, std, cov, invcov, precision, entropy, params, mean_cov, mean_var, mean_invcov, mean_precision, weightedmean and its variants, shape, scale, rate, probvec, logdetcov, length, ndims, size, pdf and logpdf.
== compares the data and the is_clamped and is_initial flags, not the annotations or the log scale: those describe how a value was computed, not the belief it holds. Compare getannotations and getlogscale explicitly where they matter.
Examples
julia> marginal = Marginal(Gamma(10.0, 2.0), false, true)
Marginal(Distributions.Gamma{Float64}(α=10.0, θ=2.0))
julia> mean(marginal)
20.0
julia> is_clamped(marginal), is_initial(marginal)
(false, true)See also Message, as_marginal.
ReactiveMP.getdata — Method
getdata(marginal::Marginal)The data of marginal, usually a distribution, or missing.
ReactiveMP.is_clamped — Method
is_clamped(marginal::Marginal) -> BoolWhether marginal comes from constants and observations alone. A clamped value is the result of computations on constants and observations alone: it never changes during inference.
ReactiveMP.is_initial — Method
is_initial(marginal::Marginal) -> BoolWhether marginal was set before inference or computed from initial values only. An initial value was set before inference, with ReactiveMP.set_initial_message! or ReactiveMP.set_initial_marginal!, or computed from clamped and initial values only, at least one of them initial.
MessagePassingRulesBase.getannotations — Method
getannotations(marginal::Marginal) -> AnnotationDictThe ReactiveMP.AnnotationDict of marginal: a variable's marginal keeps the annotations of the product it was formed from; the joint marginal of a cluster has none. Where nothing may write to it, it is the shared, frozen empty value, which cannot be annotated afterwards.
MessagePassingRulesBase.getlogscale — Method
getlogscale(marginal::Marginal)The log scale of marginal: that of the product of the messages a random variable formed it from, a number or an UndefinedLogScale saying why it is not known. For a variable's marginal in a tree-shaped model inferred exactly by belief propagation, it is the log evidence of the data.
A data variable's marginal, an observation, has log scale zero, and so does a constant's.
Throws
ArgumentErrorwhere the marginal carries no log scale: when log scales are not tracked, and always for an initial marginal set withReactiveMP.set_initial_marginal!and for the joint marginal of a factor node's cluster, which carry none even withlogscales = true.
ReactiveMP.as_marginal — Function
as_marginal(message::Message) -> Marginal
as_marginal(marginal::Marginal) -> MarginalA message as a Marginal, with its flags, annotations and log scale, its data in its public type: an efficient working type such as WishartFast becomes the type users expect (public_equivalent). A marginal is returned as it is. A variable forms its marginal from the product of its messages with it.
ReactiveMP.skip_initial — Function
ReactiveMP.skip_initial()The stream operator that drops initial values, those ReactiveMP.set_initial_marginal! and ReactiveMP.set_initial_message! seed, so that what follows only sees computed ones: get_stream_of_marginals(v) |> skip_initial(). The engine's free energy streams use it.
A variable's marginal
A random variable's marginal is the product of its inbound messages, formed with as_marginal in its public type. A rule may compute in an efficient working type, such as WishartFast, which public_equivalent turns into the type users expect, Wishart, before the marginal leaves the product. The marginal keeps the product's annotations. When log scales are tracked, it also keeps the product's log scale: in a tree-shaped model inferred exactly by belief propagation, the log evidence of the data.
A data variable's marginal is its latest observation, and a constant's is the constant, both with log scale zero.
Joint marginals
A cluster of more than one interface of a factor node has a joint marginal. A ReactiveMP.MarginalMapping computes it with the node's marginal rule, from the messages on the cluster's interfaces and the marginals of the other clusters. A structured variational rule reads it as q[:out, :μ], and the free energy uses it.
x, y = randomvar(label = :x), datavar(label = :y)
prior = factornode(Gaussian, [(:out, x), (:μ, constvar(0.0)), (:v, constvar(10.0))])
likelihood = factornode(Gaussian, [(:out, y), (:μ, x), (:v, constvar(1.0))])
activate!(x, RandomVariableActivationOptions())
activate!(y, DataVariableActivationOptions())
foreach(n -> activate!(n, FactorNodeActivationOptions()), (prior, likelihood))
joint = only(ReactiveMP.get_node_local_marginals(ReactiveMP.getlocalclusters(likelihood)))
joints = Marginal[]
subscription = subscribe!(get_stream_of_marginals(joint), (q) -> push!(joints, q))
new_observation!(y, 2.0)
last(joints)Marginal(FactorizedCluster((:out,) => BayesBase.PointMass{Float64}(2.0), (:μ,) => ExponentialFamily.NormalWeightedMeanPrecision{Float64}(xi=2.0, w=1.1), (:v,) => BayesBase.PointMass{Float64}(1.0)))The likelihood's cluster is (out, μ, v). Its out is observed and its v constant, so the joint factorises: a FactorizedCluster of blocks, which rules read block by block. A joint marginal carries no annotations and no log scale.
ReactiveMP.MarginalMapping — Type
MarginalMappingWhat computes the joint marginal of one cluster of a factor node, called with the latest messages and marginals the node's marginal rule depends on. A node builds one for each joint cluster at activation.
A call returns a missing marginal, and runs no rule, when an input is missing, or a static input folded into the node's function is data observed as missing. Otherwise it resolves the marginal rule with find_marginal_rule under the node's algorithm, checks it against the ReactiveMP.EngineDiagnostics and the services the node's context supplies (check_services), and runs it. Where no rule matches, it throws a RuleNotFoundError: a rule fallback does not apply to a marginal.
The marginal is clamped when every input is, and initial when it is not clamped and every input is clamped or initial. It carries no annotations and no log scale.
The callbacks receive ReactiveMP.BeforeMarginalRuleCallEvent and ReactiveMP.AfterMarginalRuleCallEvent around each call, the latter also for a missing marginal.
Marginal streams
Like messages, marginals live as streams, ReactiveMP.MarginalObservables. A stream emits a new belief whenever the messages or marginals it is computed from change. Every variable holds one, which ReactiveMP.get_stream_of_marginals returns, and a factor node holds one per joint cluster. The stream is lazy until activation connects it. Before that, ReactiveMP.set_initial_marginal! can seed it, so that a rule that depends on the marginal at the start has something to read. The stream keeps its latest marginal: a subscriber that joins late receives it at once.
ReactiveMP.MarginalObservable — Type
MarginalObservable()The stream of a variable's marginals, or of a factor node's joint marginal of a cluster. Every subscriber shares one upstream subscription, and the latest marginal is kept, so Rocket.getrecent returns it and a late subscriber receives it at once.
The stream is lazy: activation connects it to its source. Before that, ReactiveMP.set_initial_marginal! can seed it with an initial marginal, which is what a rule reads before any marginal has been computed.
See also ReactiveMP.MessageObservable, ReactiveMP.get_stream_of_marginals.