Factor nodes

A factor node is one factor of a factorised model. The engine creates a FactorNode for each factor and connects it to its variables through interfaces. When you activate the graph, the engine wires each of the node's outbound messages to the rule that computes it.

The engine defines no node. You declare a node with @define_factor_node and its rules with the other macros of MessagePassingRulesBase. The standard nodes, the distributions and the arithmetic, come from StandardMessagePassingRules, and the others from packages of their own (The ecosystem).

The examples use the Gaussian node of The example node.

Creating a node

factornode takes the node type, the variables it connects and a factorisation. Each interface is a (name, variable) pair. The factorisation is a tuple of clusters, each a tuple of interface names:

x, y = randomvar(label = :x), randomvar(label = :y)
node = factornode(Gaussian, [(:v, constvar(1.0)), (:out, y), (:μ, x)], ((:out, :μ), (:v,)))
FactorNode Gaussian (stochastic)
  out ── y    random
  μ   ── x    random
  v   ── 1.0  constant
  clusters: (out, μ) (v)

The node draws its interfaces, the variables they connect to and its clusters. The engine puts interfaces and clusters in declaration order, whatever order you give them in.

Without a factorisation, a node has one cluster over every interface, and its rules are those of belief propagation. With one cluster per interface, ((:out,), (:μ,), (:v,)), they are those of mean-field variational message passing. Every interface belongs to exactly one cluster.

Each cluster has a local marginal, keyed by its members. A joint cluster's key is its member tuple, (:out, :μ):

ReactiveMP.get_node_local_marginals(ReactiveMP.getlocalclusters(node))
(FactorNodeLocalMarginal((:out, :μ)), FactorNodeLocalMarginal(:v))

A node with a group, an interface with any number of members, takes each member as ((group, k), variable). A cluster may hold the whole group, keyed by its name, (:in,), or some of its members, each keyed with its index, (:out, (:in, 1)). The static inputs example below creates a node with a group.

ReactiveMP.factornode — Function
factornode(fform, interfaces, factorisation = nothing; nodefn = nothing) -> FactorNode

Create a factor node of type fform connected to the variables in interfaces. Each connection allocates the variable's stream for the node's messages; nothing is computed until the node is activated with ReactiveMP.activate!.

Arguments

  • fform: the node type, declared with @define_factor_node;
  • interfaces: a collection of (key, variable) pairs, key being the interface's name, :out, or (:m, k) for member k of a group m; a name may be an alias. Every declared interface is given, a group's members as 1:n, in any order;
  • factorisation: the clusters of the node's local marginals, a tuple of tuples of keys, ((:out, :μ), (:v,)). nothing means one cluster over every interface. Every interface is in exactly one cluster. A deterministic node ignores it: its clusters are its output alone and the joint over its inputs.

Keywords

  • nodefn: the function the node computes, which a rule reaches with getnodefn(ctx.node, Target(:out)). Default nothing, none. A node declared with static_inputs = :fold needs it: the members of its group connected to a constant or to data are folded into it (see ReactiveMP.StaticFold), and the others are numbered 1:n in their order.

The interfaces and the clusters are kept in declaration order, whatever order they are given in, so a cluster, a dependency and an emission never depend on it.

Returns

The FactorNode.

Throws

ArgumentError, naming the node, when:

  • fform is not a declared node;
  • an interface is missing, unknown, given twice, or a group's members are not 1:n;
  • the node's declaration is not met: its matched_groups must have as many members as each other, each group at least min_group_length, and a node declared factorisation = :meanfield accepts only clusters of one interface each;
  • the factorisation names an unknown interface, puts one in two clusters, or leaves one out;
  • a node that folds its static inputs has no nodefn, or every member of its group is static.

Examples

using ReactiveMP, StandardMessagePassingRules

x, y = randomvar(), datavar()
node = factornode(NormalMeanVariance, [(:out, y), (:μ, x), (:v, constvar(1.0))], ((:out,), (:μ,), (:v,)))
source
ReactiveMP.FactorNodeLocalMarginal — Type
ReactiveMP.FactorNodeLocalMarginal(key)

The marginal of one cluster of a factor node's factorisation, and the stream it arrives on once the node is activated. Its key is what a rule reads it by: the interface name for a cluster of one interface, :μ, and the tuple of member names for a joint, (:out, :μ) (see ReactiveMP.clusterkey). A cluster of one interface shares the stream of its variable's marginal; a joint's stream is computed by the node's marginal rule.

source

Interfaces

Every edge of a factor node, its connection to one variable, is a ReactiveMP.NodeInterface:

getinterfaces(node)
3-element Vector{ReactiveMP.NodeInterface}:
 Interface(out)
 Interface(μ)
 Interface(v)

Creating an interface allocates a stream for the node's messages among the variable's inbound messages. So the interface's outbound message is the variable's inbound one, and the interface's inbound message is the variable's outbound one. The members of a group are ReactiveMP.IndexedNodeInterfaces, which add the member's index.

interface = ReactiveMP.getinterface(node, 3)
ReactiveMP.name(interface), ReactiveMP.getvariable(interface)
(:v, ConstVariable(1.0))
ReactiveMP.NodeInterface — Type
ReactiveMP.NodeInterface(name::Symbol, variable)

One edge of a factor node: the interface name and the variable it connects to. Creating it calls ReactiveMP.create_new_stream_of_inbound_messages! on the variable, and keeps the stream it returns as the interface's outbound message stream: the node's message towards the variable is the variable's inbound message on this connection. variable is converted to an AbstractVariable when it is not one.

The streams are lazy until the node is activated, which connects the outbound stream to the rule's messages with ReactiveMP.set_stream_of_outbound_messages!.

See also ReactiveMP.IndexedNodeInterface, ReactiveMP.get_stream_of_outbound_messages, ReactiveMP.get_stream_of_inbound_messages.

source
ReactiveMP.name — Function
ReactiveMP.name(interface::NodeInterface) -> Symbol
ReactiveMP.name(interface::IndexedNodeInterface) -> Symbol
ReactiveMP.name(localmarginal::FactorNodeLocalMarginal)

The name of an interface, :out or a group's name for a member of the group; the key of a cluster's local marginal, a name or a tuple of member names. A Symbol is its own name.

source

Activation

Activation connects the node's lazy message and marginal streams into a live network. You activate a node after its variables. For each interface on a random or a data variable, the engine finds the inputs its rule needs, from the dependencies the node's algorithm declares or from the default scheme. It subscribes to them in declaration order, which in variational message passing is the update schedule.

x, y = randomvar(label = :x), randomvar(label = :y)
prior = factornode(Gaussian, [(:out, x), (:μ, constvar(0.0)), (:v, constvar(10.0))])
likelihood = factornode(Gaussian, [(:out, y), (:μ, x), (:v, constvar(1.0))])
foreach(v -> activate!(v, RandomVariableActivationOptions()), (x, y))
foreach(n -> activate!(n, FactorNodeActivationOptions()), (prior, likelihood))

subscription = subscribe!(get_stream_of_marginals(y), (q) -> println("q(y) = ", q))
q(y) = Marginal(ExponentialFamily.NormalMeanVariance{Float64}(μ=0.0, v=11.0))

The variables are new ones, since a variable waits for a message from every node connected to it. Here y is latent, and its marginal is the prior predictive, $\mathcal{N}(0, 10 + 1)$. The node runs with its ReactiveMP.FactorNodeActivationOptions: the algorithm, callbacks, annotations, diagnostics, services, a rule fallback and log scales, described on Activation options.

ReactiveMP.activate! — Method
ReactiveMP.activate!(factornode::FactorNode, options::FactorNodeActivationOptions)

Wire the message and marginal streams of a factor node, after its variables are activated.

  1. Clusters: each cluster of the factorisation gets a ReactiveMP.FactorNodeLocalMarginal. A cluster of one interface shares the variable's own marginal stream; a joint, keyed by a tuple, is computed by the node's marginal rule (ReactiveMP.MarginalMapping).
  2. Initial messages: the option initial_messages sets its messages on the node's inbound messages, in place of anything set before; then a node that declares initial_messages has them set on each interface where nothing is set yet.
  3. Outbound messages: for every interface connected to a random or a data variable, the inputs its rule needs are combined, and each update gives a DeferredMessage computed by a ReactiveMP.MessageMapping, which resolves and runs the rule. An interface on a constant sends no message.

What a rule needs is what the node's algorithm declares (dependencies_spec), or else the engine's default scheme (see ReactiveMP.default_dependencies). Its inputs are subscribed to in the order they are declared, which in variational message passing is the update schedule. A group reaches a rule as one tuple in member order, with nothing for the members it does not depend on.

Throws

  • ArgumentError when the algorithm declares a free-energy partition the factorisation is not, or declares no dependencies for a target, or names a cluster the factorisation does not have.

See also ReactiveMP.FactorNodeActivationOptions.

source

The Internals page describes how the engine chooses and labels a node's inputs.

Static inputs

A deterministic node declared with static_inputs = :fold folds the inputs whose values are known, constants and data, into its node function. Its rules see only the random inputs. You give the function when you create the node, factornode(…; nodefn = f), and a rule reaches it with getnodefn. The Delta node of DeltaMessagePassingRules works this way.

The node below computes out = f(ins...) for an affine f. Its rule pushes a normal message through the folded function:

struct Affine end

@define_factor_node(node = Affine, type = Deterministic, interfaces = [:out, :ins...], static_inputs = :fold)

@define_message_update_rule(
    node = Affine, target = :out, args = (m[:ins...]::NormalMeanVariance,), ctx = (:node,),
    body = (ctx, args) -> begin
        f = getnodefn(ctx.node, MessagePassingRulesBase.Target(:out))
        x = only(args.m[:ins])
        slope = f(mean(x) + 1) - f(mean(x))
        NormalMeanVariance(f(mean(x)), slope^2 * var(x))
    end,
)

a, b = randomvar(label = :a), randomvar(label = :b)
source = factornode(Gaussian, [(:out, a), (:μ, constvar(0.0)), (:v, constvar(1.0))])
affine = factornode(Affine, [(:out, b), ((:ins, 1), a), ((:ins, 2), constvar(3.0))]; nodefn = (a, k) -> k * a + 1)
FactorNode Affine (deterministic)
  out    ── b  random
  ins[1] ── a  random
  clusters: (out) (ins[1])

The constant 3.0 is folded into the function, so the node shows one member of ins, and the rule sees one input. The function it reaches is a -> 3.0 * a + 1, and it sends $\mathcal{N}(3 \cdot 0 + 1, 3^2 \cdot 1)$ towards b:

foreach(v -> activate!(v, RandomVariableActivationOptions()), (a, b))
foreach(n -> activate!(n, FactorNodeActivationOptions()), (source, affine))
affine_subscription = subscribe!(get_stream_of_marginals(b), (q) -> println("q(b) = ", q))
q(b) = Marginal(ExponentialFamily.NormalMeanVariance{Float64}(μ=1.0, v=9.0))

The Internals page describes ReactiveMP.StaticFold, which holds the folded function.

Node kinds

Each factor node is either deterministic or stochastic. A stochastic node is a density over its interfaces. A deterministic node computes its output as a function of its inputs. The kind decides how the node enters the free energy: a deterministic node has no average energy, and its clusters are always its output and the joint over its inputs.

isstochastic(sdtype(Gaussian)), isdeterministic(sdtype(Affine))
(true, true)
ReactiveMP.sdtype — Function
sdtype(fform) -> Union{Stochastic, Deterministic}
sdtype(factornode::FactorNode) -> Union{Stochastic, Deterministic}

The kind of a node type, or of a factor node, as its @define_factor_node declaration says: Stochastic() for a distribution, Deterministic() for a function of the inputs. A deterministic node's clusters are its output and the joint over its inputs, and its contribution to the free energy has no average energy.

See also isdeterministic, isstochastic.

source
ReactiveMP.isdeterministic — Function
isdeterministic(kind::Union{Deterministic, Stochastic}) -> Bool
isdeterministic(node) -> Bool

Whether a node is Deterministic: given its kind, as sdtype returns it, or the kind's type, or a node, a factor node or what a node is of (NormalMeanVariance, +), asked through its sdtype.

Examples

julia> isdeterministic(Deterministic()), isdeterministic(Stochastic)
(true, false)
source
ReactiveMP.isstochastic — Function
isstochastic(kind::Union{Stochastic, Deterministic}) -> Bool
isstochastic(node) -> Bool

Whether a node is Stochastic: given its kind, as sdtype returns it, or the kind's type, or a node, a factor node or what a node is of, asked through its sdtype.

source