Algorithms and dependencies
Every rule belongs to an algorithm. The algorithm and the graph's factorisation together decide what each rule consumes. An algorithm is not an inference scheme: it selects which rules run and carries their parameters. Almost every node runs under the default algorithm and declares nothing.
MessagePassingRulesBase.AbstractAlgorithm — Type
AbstractAlgorithmSupertype of every algorithm. An algorithm selects which rules run and carries their parameters, which a rule reads through its algo slot. It is not an inference scheme: belief propagation, variational message passing and their structured forms all come from the factorisation, under one DefaultAlgorithm.
A custom algorithm is either a rule switcher, when someone wants a different set of rules, or a node's own algorithm, when the node needs one. It comes in two kinds:
- a direct subtype of
AbstractAlgorithmstands alone: only its own rules and dependencies apply to it; - a subtype of
DefaultAlgorithmExtensionextends the default: where it defines no rule or dependencies of its own, those ofDefaultAlgorithmapply.
An algorithm with parameters stores them as fields, and a rule reads them from its algo slot.
struct MyVMP{T} <: AbstractAlgorithm
iterations::Int
tolerance::T
end
@define_message_update_rule(
node = MyNode, target = :out, algorithm = MyVMP,
args = (q[:in]::Any,),
body = (algo, args) -> solve(args.q[:in]; maxiter = algo.iterations, tol = algo.tolerance),
)See also ispure, @define_dependencies.
The default scheme
Under DefaultAlgorithm, the factorisation alone decides whether a rule is belief propagation, variational message passing or structured variational message passing. The rule itself does not decide. DefaultAlgorithm minimises the Bethe free energy.
The factorisation splits a node's interfaces into clusters. An engine gives the rule for a target two kinds of input:
- the messages on the other interfaces of the target's own cluster;
- the marginals of the other clusters, a joint marginal for a cluster of several interfaces.
This is the default scheme. For NormalMeanVariance, with the interfaces out, μ and v, it gives:
| factorisation | the rule towards out takes | which is |
|---|---|---|
q(out, μ, v) | m[:μ], m[:v] | belief propagation |
q(out) q(μ) q(v) | q[:μ], q[:v] | mean-field variational message passing |
q(out, μ) q(v) | m[:μ], q[:v] | structured variational message passing |
A marginal rule over a cluster of several interfaces, q(out, μ) here, takes the messages on the cluster's members and the marginals of the other clusters. An average energy takes one marginal per cluster. A deterministic node's clusters are always its output and the joint over its inputs, whatever the factorisation.
A rule package defines a rule for each combination of inputs it supports. Among rules with the same inputs, the types of the inputs select one. Your first node writes the rules of one node for each row of the table.
MessagePassingRulesBase.DefaultAlgorithm — Type
DefaultAlgorithm()The algorithm every node runs under unless it declares its own: Bethe free energy minimisation. Whether a rule behaves as belief propagation, variational message passing or their structured form depends on the factorisation, through the engine's default dependency scheme, not on the algorithm. A rule that omits algorithm belongs to its node's default, default_algorithm(node), which is this unless the node declares otherwise.
MessagePassingRulesBase.default_algorithm — Function
default_algorithm(node) -> AbstractAlgorithmThe algorithm value rules for node run under unless a call or a graph gives another: the node's declared algorithm, instantiated, and DefaultAlgorithm() for a node that declares none. A rule that omits algorithm is defined for its type.
Extending the default
A subtype of DefaultAlgorithmExtension overrides some rules, or some dependencies, of the default, and inherits the rest. Resolution looks for the extension's own rule first. When there is none, it falls back to the default's rule. That rule then runs with DefaultAlgorithm() in its algo slot, the algorithm it was written for.
julia> using MessagePassingRulesBase
julia> struct Shift end
julia> @define_factor_node(node = Shift, type = Deterministic, interfaces = [:out, :in])
julia> @define_message_update_rule(node = Shift, target = :out, args = (m[:in]::Real,), body = (args) -> args.m[:in] + 1)
julia> @define_message_update_rule(node = Shift, target = :in, args = (m[:out]::Real,), body = (args) -> args.m[:out] - 1)
julia> struct Doubled <: DefaultAlgorithmExtension end
julia> @define_message_update_rule(
node = Shift, target = :out, algorithm = Doubled, args = (m[:in]::Real,),
body = (args) -> 2 * (args.m[:in] + 1),
)
julia> getresult(@call_message_update_rule(node = Shift, target = :out, m = (in = 1.0,), algorithm = Doubled()))
4.0
julia> MessagePassingRulesBase.getalgorithm(@call_message_update_rule(node = Shift, target = :in, m = (out = 1.0,), algorithm = Doubled()))
DefaultAlgorithm()Doubled has its own rule towards out, which the first call runs. It has no rule towards in, so the second call runs the default's rule, under DefaultAlgorithm().
A direct subtype of AbstractAlgorithm stands alone instead: only its own rules and dependencies apply to it.
MessagePassingRulesBase.DefaultAlgorithmExtension — Type
DefaultAlgorithmExtensionSupertype of the algorithms that extend DefaultAlgorithm. Resolution looks for the extension's own rule first and falls back to the default's, and likewise for dependencies. A rule reached through that fallback receives DefaultAlgorithm() in its algo slot, the algorithm it was written for. The fallback is a second lookup, not dispatch on a supertype: if default rules dispatched on an abstract type, an extension's rule with broader inputs than a default one would be ambiguous with it, and resolution must never throw.
struct MyRules <: DefaultAlgorithmExtension end # override some rules, inherit the restAlgorithms with parameters
An algorithm's fields are its parameters, and a rule reads them from its algo slot. A rule declared with algorithm = T, for a parametric type T, matches every T{…}. A call or a graph gives the value.
julia> struct Damped{T} <: DefaultAlgorithmExtension
factor::T
end
julia> @define_message_update_rule(
node = Shift, target = :out, algorithm = Damped, args = (m[:in]::Real,),
body = (algo, args) -> algo.factor * (args.m[:in] + 1),
)
julia> getresult(@call_message_update_rule(node = Shift, target = :out, m = (in = 1.0,), algorithm = Damped(0.5)))
1.0A node may name a parametric algorithm as its own, algorithm = T. A rule that omits algorithm is then bound to the type of the node's default instance, T{Nothing} say, not to T. Rules and dependencies meant for every variant declare algorithm = T themselves. A node with its own algorithm builds such a node step by step.
Purity
A rule is pure unless it is declared otherwise. Pure means that a call has no effect anyone outside it can observe, apart from its result: running the rule leaves nothing changed that its caller did not hand it to change. It does not mean that the rule writes nothing.
A pure rule may
- allocate whatever intermediate arrays it needs;
- write its output into the buffer an in-place rule is given, since that buffer is handed over to hold the result;
- reuse its own scratch, working memory that nothing else reads;
- draw random numbers from
ctx.rng, a generator its caller owns and passes in; - warn or log, for a degenerate input say, which changes no state another computation reads.
A rule is impure if it
- mutates an input, a message or a marginal it reads;
- mutates its algorithm, a cache kept in one of its fields for instance;
- writes a global variable, a file or any other state that later code reads;
- carries its own random number generator in its algorithm.
An algorithm that carries state of its own is impure, and it says so with a method of ispure. A rule overrides its algorithm's purity with pure = false or pure = true.
MessagePassingRulesBase.ispure — Function
ispure(algorithm::Type{<:AbstractAlgorithm}) -> Bool
ispure(algorithm::AbstractAlgorithm) -> BoolWhether rules under this algorithm are pure unless they say otherwise. An impure algorithm adds a method, MessagePassingRulesBase.ispure(::Type{<:MyAlgorithm}) = false, which covers each MyAlgorithm{T} of a parametric one as well.
Pure means that a call has no effect anyone outside it can observe, apart from its result; it does not mean that the rule writes nothing. A pure rule may allocate intermediate arrays, write the output buffer an in-place rule is given (the buffer is handed over to hold the result), reuse its own scratch, which nothing else reads, draw from ctx.rng, which its caller owns, and warn or log. A rule is impure if it mutates an input, mutates its algorithm (a cache kept in a field, say), writes a global variable, a file or other state that later code reads, or carries its own random number generator in its algorithm.
Purity is declared, not proved: the default is true. A rule may override its algorithm with pure = false, and an engine auditing purity must read the rule's own flag, so the override cannot hide impurity. Purity is an audit policy, for threading and for finding side effects; it says nothing about whether a rule can be differentiated, which is tested separately.
A node's own algorithm
A node whose rules ignore the factorisation declares an algorithm of its own. It also declares the dependencies of each target: what the rule for that target consumes. The mixtures do this. NormalMixture runs under NormalMixtureVMP and is always variational. Mixture runs under MixtureBP and always reads messages.
The node below has the interfaces of NormalMixture, with the groups m and p for the components' means and precisions. Every rule takes marginals:
using MessagePassingRulesBase
struct Blend end
struct BlendVMP <: AbstractAlgorithm end
@define_factor_node(
node = Blend,
type = Stochastic,
interfaces = [:out, :switch, :m..., :p...],
algorithm = BlendVMP,
matched_groups = [(:m, :p)],
dependencies = [
:out => (q[:switch], q[:p...], q[:m...]),
:switch => (q[:out], q[:p...], q[:m...]),
(:m, k) => (q[:out], q[:switch], q[:p][k]),
(:p, k) => (q[:out], q[:switch], q[:m][k]),
],
)
MessagePassingRulesBase.dependencies_spec(Blend, BlendVMP())| free-energy partition | from the factorisation |
|---|
The declaration draws itself as a table of targets and their inputs. An engine subscribes to a target's inputs in the order they are declared, and in variational message passing that order is the update schedule. Here each component's precision is updated before its mean.
Every target a graph connects must be declared. A target that follows the default scheme is declared as target => (default,).
@define_dependencies declares the same for another algorithm of an existing node:
struct Link end
struct LinkVMP <: AbstractAlgorithm end
@define_factor_node(node = Link, type = Stochastic, interfaces = [:out, :in])
@define_dependencies(node = Link, algorithm = LinkVMP, dependencies = [:out => (q[:in],), :in => (q[:out],)])
MessagePassingRulesBase.dependencies_spec(Link, LinkVMP())| free-energy partition | from the factorisation |
|---|
Link runs under DefaultAlgorithm unless a call or a graph selects LinkVMP. Under LinkVMP, each rule reads the marginal of the other interface.
MessagePassingRulesBase.@define_dependencies — Macro
@define_dependencies(node = ..., algorithm = ..., dependencies = [...], free_energy_partition = [...])Declare what node's rules consume under algorithm, target by target, in place of the default scheme, where the inputs follow the factorisation. It is for an algorithm whose rules ignore the factorisation, such as a node's own or one of its variants. The macro takes keyword arguments only; node, algorithm and dependencies are required. An unknown or repeated keyword is an error at definition time, naming the valid ones.
A node declares the same for its default algorithm with @define_factor_node's own dependencies keyword, in the same vocabulary. dependencies_spec returns what was declared.
Required keywords
node: the node, as declared with@define_factor_node.algorithm: the algorithm the declaration is for, a type, or a value whose type is used. With a parametric typeT,algorithm = Tcovers everyT{…}.dependencies: what each target's rule consumes.A vector of
target => (inputs...)pairs, one per target whose inputs are declared:dependencies = [:out => (m[:μ], q[:v]), :μ => (m[:out], q[:v])]. A target is:out, or(:m, k)for every member of the groupm, bindingkfor the inputs to select by. The inputs are written as in a rule'sargs, without types:m[:μ],q[:μ]: the message or the marginal of a single interface;q[:y, :x], orq[(:y, :x)]: the joint marginal of a cluster, its members in interface order;q[(:in,)]is the joint over the groupin;- for a group:
m[:in...](every member),m[:in][k](the target's own member),m[:in][!k](all but it), orm[:in][select_group_members(f; arity)](the membersf(k)returns, alwaysarityof them, seeselect_group_members); default: the default scheme's inputs for that target, which follow the factorisation (the messages of the target's own cluster, the marginals of the others).:a => (default, q[:a])is the default scheme's inputs plusq(a), and:y => (default,)the default scheme alone. An input listed besidedefaultis a single interface's message or marginal, placed among the default scheme's in interface order, and consumed without being scored.
A target with no inputs is
target => (). Every target a graph connects must be declared: one left out is an error when an engine activates the node, so writetarget => (default,)for one that follows the default scheme. The inputs are subscribed to in the order written, which under variational message passing is the update schedule: it changes how fast a node converges, not where to. The declaration is checked against the node's interfaces when it is loaded: unknown names, a group written as a single interface or the other way round, a target or an input given twice, and a cluster out of interface order are errors.
Optional keywords
free_energy_partition: the clusters the free energy is computed over, a vector of tuples of interface names covering every interface exactly once:[(:out, :μ), (:τ,)]. A group's name stands for all its members. What a rule consumes need not be a block of it: a marginal consumed but outside the partition is never scored. The engine refuses a graph whose factorisation is not this partition, block for block. Default: none, and the partition is the graph's factorisation.
Example
# Declared on the parametric `MixtureBP`, so it covers every product strategy:
@define_dependencies(
node = Mixture,
algorithm = MixtureBP,
dependencies = [
:out => (m[:switch], m[:inputs...]),
:switch => (m[:out], m[:inputs...]),
(:inputs, k) => (m[:out], m[:switch]),
],
)MessagePassingRulesBase.DependenciesSpec — Type
DependenciesSpecWhat a node's rules consume under one algorithm, target by target, and optionally the partition the free energy is computed over, as @define_dependencies or @define_factor_node's dependencies keyword declares it and dependencies_spec returns it. Fields:
node,algorithm: the node, and the algorithm type the declaration is for;targets: oneTargetDependenciesper declared target;partition: the free-energy partition, a tuple of member tuples, ornothing, and the engine derives it from the factorisation. Read it withfree_energy_partition.
The two are separate: a rule may consume a marginal that is not a block of the partition, and that marginal is never scored. Read a target's inputs with target_dependencies and extends_default_scheme. It shows itself as a table of targets and their inputs.
MessagePassingRulesBase.dependencies_spec — Function
dependencies_spec(node, algorithm) -> Union{DependenciesSpec, Nothing}The DependenciesSpec declared for node under algorithm, a value, or nothing when none is declared and the engine's default scheme applies. A DefaultAlgorithmExtension that declares none gets the declaration for DefaultAlgorithm, if any.
MessagePassingRulesBase.free_energy_partition — Function
free_energy_partition(spec::DependenciesSpec) -> Union{Tuple, Nothing}The partition the free energy is computed over, as declared with free_energy_partition: a tuple of blocks, each a tuple of interface names, covering every interface once, a group's name standing for all its members. nothing when none is declared, and the partition is the graph's factorisation. An engine refuses a graph whose factorisation is not this partition.
Extending the default scheme
A rule may need one input that the factorisation does not give it, while its other inputs follow the factorisation as usual. default among a target's inputs stands for the inputs of the default scheme, and the inputs beside it are added to them. Take a transition y ~ f(x; a) whose function is linearised around the mean of a. Its rule towards a needs q(a), the expansion point, under every factorisation, including one where the default scheme gives it the message on a instead. The node below declares that:
using MessagePassingRulesBase
struct Transition end # y ~ f(x; a)
struct TransitionVMP <: AbstractAlgorithm end
@define_factor_node(node = Transition, type = Stochastic, interfaces = [:y, :x, :a])
@define_dependencies(
node = Transition, algorithm = TransitionVMP,
dependencies = [:y => (default,), :x => (default,), :a => (default, q[:a])],
)
MessagePassingRulesBase.dependencies_spec(Transition, TransitionVMP())| free-energy partition | from the factorisation |
|---|
An added input is a single interface's message or marginal. The engine places it among the inputs of the default scheme in interface order. It adds nothing the default scheme already gives.
A marginal added this way is consumed and never scored: the free energy is computed over the factorisation. check_rules checks that a rule for such a target reads the added inputs. It does not check the other inputs, which depend on the factorisation, and a rule does not know the factorisation.
How an engine reads a declaration
An engine reads a declaration through its parts. Each target has its inputs, a TargetDependencies. Each input selects one of these:
- an interface;
- a whole group;
- a group's member aligned with the target;
- every member but the target's;
- the members a function picks.
spec = MessagePassingRulesBase.dependencies_spec(Link, LinkVMP())
MessagePassingRulesBase.target_dependencies(spec, MessagePassingRulesBase.Target(:out))(q[:in],)MessagePassingRulesBase.TargetDependencies — Type
TargetDependenciesThe inputs one target consumes, one entry of a DependenciesSpec. Fields:
edge::Symbol: the target interface, or the group for an indexed target;indexed::Bool:truefor a group's members, declared(:m, k) => (...), whose inputs may select byk;falsefor a single interface, declared:out => (...);inputs: theDependencys, in the order declared, which is the order an engine subscribes to them;default::Bool:truewhen the target was declared withdefaultamong its inputs: it consumes the default scheme's inputs, which follow the factorisation, andinputsare added to them.
MessagePassingRulesBase.target_dependencies — Function
target_dependencies(declaration::DependenciesSpec, target) -> Union{Tuple, Nothing}The Dependencys target, a Target or an IndexedTarget, consumes under declaration, in the order declared, or nothing when the declaration does not list the target. For a target declared with default, only the inputs added to the default scheme's (extends_default_scheme).
MessagePassingRulesBase.extends_default_scheme — Function
extends_default_scheme(declaration::DependenciesSpec, target) -> BoolWhether target was declared with default, so that it consumes the engine's default scheme's inputs plus those target_dependencies lists. false for a target the declaration does not list.
MessagePassingRulesBase.Dependency — Type
DependencyOne input a target consumes, as a dependency declaration lists it. Fields:
container::Symbol::mfor a message,:qfor a marginal;key: the interface's or group's name, or, for a cluster's joint, the tuple of its members, each an interface's name or a group member by its index,(:out, (:T, 1));selector::DependencySelector: what it takes of the interface, aDependencySelector;SingleInterfacefor an interface or a cluster.
MessagePassingRulesBase.DependencySelector — Type
DependencySelectorSupertype of how a Dependency selects from its interface: the interface itself (SingleInterface), or, for a group, all members (AllGroupMembers), the member aligned with the target (AlignedGroupMember), all but that one (AllGroupMembersButSelf), or the members a function picks (CustomGroupSelector, made by select_group_members). An engine reads a group selection through selected_indices and selection_arity.
MessagePassingRulesBase.SingleInterface — Type
SingleInterface()The DependencySelector of a single interface, m[:μ], or of a cluster's joint, q[:y, :x]: the whole of what the key names.
MessagePassingRulesBase.AllGroupMembers — Type
AllGroupMembers()The DependencySelector of every member of a group, m[:in...]: all n of a group of n, whatever the target.
MessagePassingRulesBase.AlignedGroupMember — Type
AlignedGroupMember()The DependencySelector of the member with the target's index, m[:in][k] for the indexed target (:m, k): one member. Needs an indexed target.
MessagePassingRulesBase.AllGroupMembersButSelf — Type
AllGroupMembersButSelf()The DependencySelector of every member but the one with the target's index, m[:in][!k]: n - 1 of a group of n, none of a group of one. Needs an indexed target.
MessagePassingRulesBase.CustomGroupSelector — Type
CustomGroupSelector{F}The DependencySelector select_group_members(f; arity) makes: the members f(k) returns for target index k, always arity of them. Fields: f and arity.
MessagePassingRulesBase.select_group_members — Function
select_group_members(f; arity::Integer) -> CustomGroupSelectorA custom group selector, written m[:in][select_group_members(f; arity)] in a dependency declaration: f(k) returns the member indices, a tuple or anything Tuple converts, for target index k, always arity of them. The arity is static, so an engine knows how many inputs to wait for before it calls f.
Throws
selected_indices throws an ArgumentError when f returns another number of indices.
julia> using MessagePassingRulesBase: select_group_members, selected_indices
julia> previous = select_group_members(k -> (k - 1,); arity = 1);
julia> selected_indices(previous, 3, 4)
(2,)MessagePassingRulesBase.selected_indices — Function
selected_indices(selector::DependencySelector, k, n) -> TupleThe member indices selector picks from a group of n for target index k, in order. A selection of no members is an empty tuple; an engine must treat it as satisfied, never wait on it. Not defined for SingleInterface, which selects no members.
Throws
ArgumentError for a CustomGroupSelector whose function returns other than its arity.
Examples
julia> using MessagePassingRulesBase: selected_indices, AllGroupMembersButSelf, select_group_members
julia> selected_indices(AllGroupMembersButSelf(), 2, 4)
(1, 3, 4)
julia> selected_indices(AllGroupMembersButSelf(), 1, 1)
()
julia> selected_indices(select_group_members(k -> (mod1(k - 1, 3),); arity = 1), 1, 3)
(3,)MessagePassingRulesBase.selection_arity — Function
selection_arity(selector::DependencySelector, n) -> IntHow many members selector picks from a group of n, for any target index: n for AllGroupMembers, 1 for AlignedGroupMember, n - 1 for AllGroupMembersButSelf, the declared arity for a CustomGroupSelector. Not defined for SingleInterface.
Initial messages
Some rules read the message on their own edge, as an expectation propagation rule does. In a graph with a loop through that edge, such a rule has no message to start from. The node may therefore declare an initial message for the edge. Probit declares one for in, and the node below does the same:
using MessagePassingRulesBase, ExponentialFamily
struct Threshold end # out = (in > 0)
struct ThresholdEP <: AbstractAlgorithm end
@define_factor_node(
node = Threshold, type = Stochastic, interfaces = [:out, :in], algorithm = ThresholdEP,
dependencies = [:out => (m[:in],), :in => (m[:out], m[:in])],
initial_messages = [:in => NormalMeanPrecision(0.0, 100.0)],
)
MessagePassingRulesBase.nodespec(Threshold)| interface | aliases | |
|---|---|---|
out | ||
in |
| default algorithm | ThresholdEP() |
|---|---|
| static inputs | none |
| initial messages | in => ExponentialFamily.NormalMeanPrecision{Float64}(μ=0.0, w=100.0) |
| defined | algorithms.md:295 |
The rule towards in reads m[:in], the message on its own edge. The engine sets the initial message on the node's inbound message at activation, where nothing was set, so a model's own initialisation wins.
An initial message is a default for starting, not a dependency: the algorithm still decides which inputs a rule reads. No rule computed an initial message, so its log scale is undefined.
MessagePassingRulesBase.initial_messages — Function
initial_messages(node) -> TupleThe messages node seeds its interfaces with, as name => message pairs: for a rule that reads the message on its own edge, the value it starts from. The engine sets each on the node's inbound message of that interface at activation, unless one was set there already, so a user's initialisation wins. Empty unless declared.