FlowMessagePassingRules
The Flow node of the ReactiveMP ecosystem: a deterministic, invertible transformation $y = f(x)$ of a multivariate variable, with $f$ a normalising-flow model built from layers. Use it to give a Gaussian latent variable a flexible, non-Gaussian image, or to learn a nonlinear map whose inverse and Jacobian are cheap. The flow models themselves, and their layers, live in this package too: see Flow models.
FlowMessagePassingRules — Module
FlowMessagePassingRulesThe Flow node, the deterministic node $y = f(x)$ for an invertible flow model f, and the flow models it is built from. A model is a tuple of layers, FlowModel, given its parameters by compile; the layers are InputLayer, AdditiveCouplingLayer and PermutationLayer, and a coupling layer holds a coupling flow, PlanarFlow or RadialFlow. PermutationMatrix is the permutation a PermutationLayer applies.
The node's rules push a multivariate normal through the compiled model, forwards towards out and backwards, through the inverse, towards in, by linearisation or by the unscented transform. They run under FlowApproximation, which carries the model, and which the model must give: under DefaultAlgorithm no rule exists.
Examples
julia> model = compile(FlowModel(2, (AdditiveCouplingLayer(PlanarFlow(); permute = false),)), [1.0, 2.0, 3.0]);
julia> result = @call_message_update_rule(
node = Flow, target = :out, algorithm = FlowApproximation(model),
m = (in = MvNormalMeanCovariance([1.0, 2.0], [1.0 0.0; 0.0 1.0]),),
);
julia> μ, Σ = mean_cov(getresult(result));
julia> μ ≈ [1.0, 3.0 + tanh(5.0)] # y₂ = x₂ + x₁ + tanh(2x₁ + 3) at the mean
true
julia> J = [1.0 0.0; 1 + 2 * (1 - tanh(5.0)^2) 1.0]; # the Jacobian at the mean
julia> Σ ≈ J * J'
trueReactiveMP runs these rules on a factor graph, which RxInfer builds from a GraphPPL model. The examples here call the rules directly, as a test does.
The site has three pages: this one, the node; Flow models, the models and their layers; and Internals, the helpers the rules are built on.
Overview
Flow is a deterministic node: it connects an input in and an output out of the same dimension through a compiled flow model. A message arriving on either side is pushed through the model, forwards through $f$ or backwards through $f^{-1}$. It leaves as a multivariate normal, by linearisation with the model's own Jacobians or by the unscented transform. Both directions are exact for an affine flow and approximate otherwise.
Definition
\[p(y \mid x) = \delta\big(y - f(x)\big), \qquad x, y \in \mathbb{R}^d,\]
where $f = f_L \circ \dots \circ f_1$ is the composition of the model's layers, each invertible with a known Jacobian. The Dirac delta puts all the probability on $y = f(x)$: the output is a function of the input, with no noise.
Interfaces
using FlowMessagePassingRules, MessagePassingRulesBase, MessagePassingRulesApproximations, BayesBase, ExponentialFamily
MessagePassingRulesBase.nodespec(Flow)| interface | aliases | |
|---|---|---|
out | ||
in |
| default algorithm | MessagePassingRulesBase.DefaultAlgorithm() |
|---|---|
| static inputs | none |
| defined | src/node.jl:118 |
The node has two interfaces.
| name | aliases | meaning | messages the rules take |
|---|---|---|---|
out | the output $y = f(x)$ | multivariate normal | |
in | the input $x$ | multivariate normal |
Under Linearization the rules take MvNormalMeanCovariance, MvNormalMeanPrecision and MvNormalWeightedMeanPrecision, and return an MvNormalMeanCovariance for the first and an MvNormalMeanPrecision for the others; under Unscented they take any multivariate normal and return an MvNormalMeanCovariance. No rule reads a marginal.
Algorithm
The node's algorithm, FlowApproximation(model; method = Linearization()), carries the CompiledFlowModel and the method, Linearization() or Unscented(). A model must name it for every Flow node: the node has no default algorithm, so under DefaultAlgorithm no rule exists. The node declares no dependencies, so each rule takes the inputs of the default scheme: the message on the other interface.
Supported rules
MessagePassingRulesBase.rule_coverage(Flow)| DefaultAlgorithm | FlowApproximation{<:AbstractCompiledFlowModel, <:Linearization} | FlowApproximation{<:AbstractCompiledFlowModel, <:Unscented} | |
|---|---|---|---|
| → out | ✓×3 | ✓ | |
| → in | ✓×3 | ✓ |
The two FlowApproximation columns are its two methods. The first is linearisation, with three rules per edge, one per normal form; the second is the unscented transform. Every rule is a belief propagation message. There is no marginal rule, since the marginal of in is the product of its two messages. There is no average energy, since a deterministic node needs none: its contribution to the Bethe free energy is minus the entropy of q(in), as good as the approximation behind it.
Example
A two-dimensional model of one coupling layer with a planar flow, given its three parameters, and the message towards out from a standard normal centred at [1, 2]:
model = compile(FlowModel(2, (AdditiveCouplingLayer(PlanarFlow(); permute = false),)), [1.0, 2.0, 3.0])
@call_message_update_rule(
node = Flow, target = :out, algorithm = FlowApproximation(model),
m = (in = MvNormalMeanCovariance([1.0, 2.0], [1.0 0.0; 0.0 1.0]),),
)Result
| value | MvNormalMeanCovariance( μ: [1.0, 3.99991] Σ: [1.0 1.00036; 1.00036 2.00073] ) |
|---|---|
| type | ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}} |
| log scale | undefined: the message rule for Flow towards :out under FlowApproximation{<:FlowMessagePassingRules.AbstractCompiledFlowModel, <:MessagePassingRulesApproximations.Linearization} declares no `logscale` |
Inputs
| edge | value |
|---|
Rule
| declared inputs | m[:in]::ExponentialFamily.MvNormalMeanCovariance |
|---|---|
| algorithm | FlowApproximation{CompiledFlowModel{1, Tuple{AdditiveCouplingLayer{Tuple{PlanarFlow{Float64, Float64}}}}}, Linearizatio… |
| log scale | none |
| defined | src/rules.jl:24 |
| body | (algo, args)->begin
(μ_in, Σ_in) = mean_cov(args.m[:in])
(μ_out, J) = forward_jacobian(getmodel(algo), μ_in)
MvNormalMeanCovariance(μ_out, J * Σ_in * J')
end |
Other rules for this target (3)
src/rules.jl:48| ✓ | algorithm FlowApproximation{<:FlowMessagePassingRules.AbstractCompiledFlowModel, <:MessagePassingRulesApproximations.Linearization} |
| ✗ | m[:in]::ExponentialFamily.MvNormalMeanPrecision got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}} |
src/rules.jl:53| ✓ | algorithm FlowApproximation{<:FlowMessagePassingRules.AbstractCompiledFlowModel, <:MessagePassingRulesApproximations.Linearization} |
| ✗ | m[:in]::ExponentialFamily.MvNormalWeightedMeanPrecision got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}} |
src/rules.jl:123| ✗ | algorithm FlowApproximation{<:FlowMessagePassingRules.AbstractCompiledFlowModel, <:MessagePassingRulesApproximations.Unscented} |
| ✓ | m[:in]::Union{ExponentialFamily.MultivariateNormalDistributionsFamily{T}, ExponentialFamily.MultivariateGaussianDistributionsFamily{T}} where T got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}} |
@call_message_update_rule runs the rule and draws the message it read. Under linearisation the mean of the result is the image of the input's mean, FlowMessagePassingRules.forward of it. The unscented transform gives a mean close to it. getresult extracts both for the comparison:
julia> model = compile(FlowModel(2, (AdditiveCouplingLayer(PlanarFlow(); permute = false),)), [1.0, 2.0, 3.0]);
julia> m_in = MvNormalMeanCovariance([1.0, 2.0], [1.0 0.0; 0.0 1.0]);
julia> linearised = getresult(@call_message_update_rule(node = Flow, target = :out, m = (in = m_in,), algorithm = FlowApproximation(model)));
julia> mean(linearised) ≈ FlowMessagePassingRules.forward(model, [1.0, 2.0])
true
julia> unscented = getresult(@call_message_update_rule(node = Flow, target = :out, m = (in = m_in,), algorithm = FlowApproximation(model; method = Unscented())));
julia> isapprox(mean(unscented), mean(linearised); atol = 1e-2)
trueLimitations
- Multivariate normal messages only, in and out; no rule takes a marginal.
- No average energy, as for every deterministic node, and a free energy only as good as the linearisation or the unscented transform behind the messages.
- The model's parameters are fixed once compiled: the node does not learn them.
- Under
Linearization, the rules for a message in precision form evaluate the Jacobian at the message's mean read as a point on the other side of the flow, where the covariance form evaluates it at the mean itself: the two forms agree for a two-dimensional model of one coupling layer, and differ in general for a nonlinear flow.
API
The node and its algorithm, and the algorithm's accessors, called qualified:
FlowMessagePassingRules.Flow — Type
FlowThe deterministic node $y = f(x)$, for an invertible flow model $f$, a CompiledFlowModel of dimension $d$:
\[p(y \mid x) = \delta\big(y - f(x)\big), \qquad x, y \in \mathbb{R}^d.\]
Interfaces
out: the output $y$, multivariate;in: the input $x$, multivariate, of the model's dimension.
Algorithm
The rules run under FlowApproximation, which carries the model and the method, and which the model must give. The node declares no algorithm of its own, so under DefaultAlgorithm no rule exists and a call throws a RuleNotFoundError.
Rules
Belief propagation only, one message rule per edge and per method, each taking a multivariate normal message on the other edge and returning a multivariate normal:
- towards
out, the message oninpushed forwards through $f$; - towards
in, the message onoutpulled back through $f^{-1}$.
Under Linearization the model's own Jacobian $J$ gives $\mathcal{N}(f(\mu), J \Sigma J^\top)$; the rules take MvNormalMeanCovariance, MvNormalMeanPrecision and MvNormalWeightedMeanPrecision, and return an MvNormalMeanCovariance for the first and an MvNormalMeanPrecision for the others. Under Unscented the sigma points give the moments; the rules take any multivariate normal and return an MvNormalMeanCovariance.
The node has no marginal rule, since the marginal of in is the product of its two messages, and no average energy, since a deterministic node needs none: its contribution to the Bethe free energy is minus the entropy of q(in), which is exact only as far as the approximation is.
Examples
julia> model = compile(FlowModel(2, (AdditiveCouplingLayer(PlanarFlow(); permute = false),)), [1.0, 2.0, 3.0]);
julia> y = [1.0, 3.0 + tanh(5.0)]; # f([1.0, 2.0])
julia> result = @call_message_update_rule(
node = Flow, target = :in, algorithm = FlowApproximation(model),
m = (out = MvNormalMeanPrecision(y, [1.0 0.0; 0.0 1.0]),),
);
julia> mean(getresult(result)) ≈ [1.0, 2.0]
trueFlowMessagePassingRules.FlowApproximation — Type
FlowApproximation(model::CompiledFlowModel; method = Linearization())
FlowApproximation(model::CompiledFlowModel, method)Flow's algorithm: the compiled flow model, and the method that pushes a normal through it. A model using the node must give it; the node has no default algorithm.
Arguments
model: the compiled flow, aCompiledFlowModelfromcompile.
Keywords
method:Linearization(), which uses the model's own Jacobians, orUnscented, which uses sigma points.Unscented()takes its dimension from each input;Unscented(dim)fixes it, and a rule throws aDimensionMismatchwhendimis not the model's dimension. DefaultLinearization().
Examples
julia> model = compile(FlowModel(2, (AdditiveCouplingLayer(PlanarFlow()),)), [1.0, 2.0, 3.0]);
julia> FlowApproximation(model).method isa Linearization
true
julia> FlowApproximation(model; method = Unscented(2)).method isa Unscented
trueFlowMessagePassingRules.getmodel — Function
FlowMessagePassingRules.getmodel(algo::FlowApproximation) -> CompiledFlowModelThe compiled flow model of a FlowApproximation. Public, not exported: call it qualified, since RxInfer exports a getmodel of its own.
FlowMessagePassingRules.getmethod — Function
FlowMessagePassingRules.getmethod(algo::FlowApproximation)The method of a FlowApproximation, a Linearization or an Unscented. Public, not exported: call it qualified.