Projection
CVIProjection handles the functions and message families that the Gaussian methods cannot. It draws samples of the inputs, pushes the samples through f, and projects the result onto an exponential family with ExponentialFamilyProjection. Projection here means finding the member of a chosen family, such as the normals or the gamma distributions, that is closest to the result.
Loading the rules
The rules of CVIProjection are in a package extension. Until ExponentialFamilyProjection is loaded, DeltaApproximation refuses the method, and its error says which package to load:
using DeltaMessagePassingRules, MessagePassingRulesBase, ExponentialFamily, BayesBase
try
DeltaApproximation(method = CVIProjection())
catch err
showerror(stdout, err)
endArgumentError: `CVIProjection(…)` is not an approximation method of the Delta node. It takes `Unscented()` and `Linearization()` from MessagePassingRulesApproximations, and `CVIProjection()` once ExponentialFamilyProjection is loaded. `CVIProjection`'s rules are in an extension: load it with `using ExponentialFamilyProjection`.Loading the package loads the rules:
using ExponentialFamilyProjection
algorithm = DeltaApproximation(method = CVIProjection())The method reads different inputs from the Gaussian methods. Towards out it reads the message on out itself, the marginal q(out) and the joint over the inputs:
MessagePassingRulesBase.dependencies_spec(DeltaFn, algorithm)| free-energy partition | (:out,), (:in,) |
|---|
Example
Take out = exp(in), a standard normal message on in, and a gamma message on out, a positive quantity. The joint over the single input is the input's message reweighted by the likelihood of out, projected back onto the normals:
using Random: Xoshiro
struct WithFunction{F}
f::F
end
MessagePassingRulesBase.getnodefn(node::WithFunction, ::MessagePassingRulesBase.Target{:out}) = node.f
ctx = MessagePassingRulesBase.RuleContext(node = WithFunction(exp), rng = Xoshiro(42))
joint = @call_marginal_update_rule(
node = DeltaFn, target = (:in,), algorithm = algorithm, ctx = ctx,
m = (out = Gamma(2.0, 1.0), in = (NormalMeanVariance(0.0, 1.0),)),
)Result
| value | FactorizedJoint{Tuple{NormalMeanVariance{Float64}}}((ExponentialFamily.NormalMeanVariance{Float64}(μ=-0.119498, v=0.472109),)) |
|---|---|
| type | BayesBase.FactorizedJoint{Tuple{ExponentialFamily.NormalMeanVariance{Float64}}} |
Inputs
| edge | value |
|---|
Rule
| declared inputs | m[:out]::Any, m[:in...]::Any |
|---|---|
| algorithm | DeltaApproximation{CVIProjection{Int64, Nothing, Nothing, ProposalDistributionContainer{Any}, FullSampling}, Nothing}(C… |
| services | node, rng |
| defined | ext/DeltaMessagePassingRulesProjectionExt.jl:93 |
| body | (algo, ctx, args)->begin
g = getnodefn(ctx.node, Target(:out))
(method, m_out, m_ins) = (algo.method, args.m[:out], args.m[:in])
if length(m_ins) == 1
m_in = only(m_ins)
logp = generic_logpdf(m_in, (z->logpdf(m_out, g(z))))
FactorizedJoint((project_to(create_project_to_ins(method, m_in, 1), logp, m_in),))
else
(proposal, strategy) = (method.proposal_distribution, method.sampling_strategy)
columns = Any[input_samples(ctx.rng, source, strategy) for source = sampling_sources(proposal.distribution, m_ins)]
loglikelihood(z, i) = mean((sample->logpdf(m_out, g(replace_at(sample, i, z)...))), zip(columns...))
projections = ntuple(length(m_ins)) do i
m_in = m_ins[i]
prj = create_project_to_ins(method, m_in, i)
matches = ExponentialFamilyProjection.get_projected_to_type(prj) === ExponentialFamily.exponential_family_typetag(m_in) && ExponentialFamilyProjection.get_projected_to_dims(prj) == size(m_in)
projection = if matches
project_to(prj, generic_logpdf(m_in, (z->loglikelihood(z, i))), m_in)
else
project_to(prj, generic_logpdf(m_in, (z->loglikelihood(z, i) + logpdf(m_in, z))))
end
columns[i] = input_samples(ctx.rng, projection, strategy)
projection
end
result = FactorizedJoint(projections)
proposal.distribution = result
result
end
end |
Other rules for this target (1)
rules/gaussian.jl:14| ✗ | algorithm DeltaApproximation{<:Union{MessagePassingRulesApproximations.Linearization, MessagePassingRulesApproximations.Unscented}} |
| ✗ | m[:out]::Union{ExponentialFamily.NormalDistributionsFamily{T}, ExponentialFamily.GaussianDistributionsFamily{T}} where T got Distributions.Gamma{Float64} |
| ✓ | m[:in...]::Union{ExponentialFamily.NormalDistributionsFamily{T}, ExponentialFamily.GaussianDistributionsFamily{T}} where T got Tuple{ExponentialFamily.NormalMeanVariance{Float64}} |
The rules draw their samples from the rule context's generator, ctx.rng, which an engine owns and a caller sets here. The node object is the same stand-in as on the overview. The message towards out samples that joint, pushes the samples through exp, and projects them onto the family of q(out), here the gamma distributions:
q_in = getresult(joint)
@call_message_update_rule(
node = DeltaFn, target = :out, algorithm = algorithm, ctx = ctx,
m = (out = Gamma(2.0, 1.0),), q = (out = Gamma(3.0, 1.0),), clusters = ((:in,) => q_in,),
)Result
| value | DivisionOf{Gamma{Float64}, Gamma{Float64}}(Distributions.Gamma{Float64}(α=2.43227, θ=0.421246), Distributions.Gamma{Float64}(α=2.0, θ=1.0)) |
|---|---|
| type | DeltaMessagePassingRulesProjectionExt.DivisionOf{Distributions.Gamma{Float64}, Distributions.Gamma{Float64}} |
| log scale | undefined: the message rule for DeltaFn towards :out under DeltaApproximation{<:CVIProjection} declares no `logscale` |
Inputs
| edge | value | |
|---|---|---|
| in… | q | FactorizedJoint{Tuple{NormalMeanVariance{Float64}}}((ExponentialFamily.NormalMeanVariance{Float64}(μ=-0.119498, v=0.472… q[(:in,)] |
Rule
| declared inputs | m[:out]::Any, q[:out]::Any, q[(:in,)]::BayesBase.FactorizedJoint |
|---|---|
| algorithm | DeltaApproximation{CVIProjection{Int64, Nothing, Nothing, ProposalDistributionContainer{Any}, FullSampling}, Nothing}(C… |
| services | node, rng |
| log scale | none |
| defined | ext/DeltaMessagePassingRulesProjectionExt.jl:71 |
| body | (algo, ctx, args)->begin
f = getnodefn(ctx.node, Target(:out))
method = algo.method
samples = map((q_in->cvilinearize(rand(ctx.rng, sampling_optimized(q_in), method.outsamples))), components(args.q[(:in,)]))
out_samples = map((x->f(x...)), zip(samples...))
estimate = project_to(create_project_to(method, args.q[:out], out_samples), out_samples)
DivisionOf(estimate, args.m[:out])
end |
Other rules for this target (1)
rules/gaussian.jl:6| ✗ | algorithm DeltaApproximation{<:Union{MessagePassingRulesApproximations.Linearization, MessagePassingRulesApproximations.Unscented}} |
| ✗ | m[:in...]::Union{ExponentialFamily.NormalDistributionsFamily{T}, ExponentialFamily.GaussianDistributionsFamily{T}} where T not provided |
| ✗ | m[:out]::Distributions.Gamma{Float64} provided but not consumed |
| ✗ | q[:out]::Distributions.Gamma{Float64} provided but not consumed |
| ✗ | q[(:in,)]::BayesBase.FactorizedJoint{Tuple{ExponentialFamily.NormalMeanVariance{Float64}}} provided but not consumed |
The result is the projection divided by the message that arrived on out, left unevaluated: its product with that message, at the variable, is the projection.
For a node with several inputs, the rule for the joint projects each input in turn, against samples of the others. A CVISamplingStrategy decides how those samples are drawn:
DeltaApproximation(method = CVIProjection(outsamples = 200, sampling_strategy = MeanBased()))The rule for the joint keeps its result as the next call's proposal, in a ProposalDistributionContainer, so the method carries state and that rule is impure.
Limitations
- The rules need ExponentialFamilyProjection loaded.
- Sampling makes the messages random: two runs agree only with the same generator.
- The message towards
outreadsq(out)and the joint over the inputs, which depend on the node's own messages; a model whoseoutis not observed needs initial marginals for them. - One
CVIProjectionvalue given to several nodes shares one proposal among them. - A known inverse is not used, and is ignored with a warning.
API
The method:
DeltaMessagePassingRules.CVIProjection — Type
CVIProjection(; outsamples = 100, out_prjparams = nothing, in_prjparams = nothing,
proposal_distribution = ProposalDistributionContainer{Any}(nothing),
sampling_strategy = FullSampling(10))The Delta node's projection method, for DeltaApproximation(method = CVIProjection()): it samples the inputs, pushes the samples through the function, and projects the result onto an exponential family with ExponentialFamilyProjection. It takes messages of any family the projection can target, where Unscented and Linearization take normals only.
Its rules are in the package extension DeltaMessagePassingRulesProjectionExt, loaded with using ExponentialFamilyProjection; until then DeltaApproximation does not accept the method, and its error says which package to load.
Keywords
outsamples: the number of samples drawn from each input's marginal for the message towardsout. Default100.out_prjparams: the family of the message towardsout.nothing, the default, projects onto the family ofq(out)with the default projection parameters; aProjectedTois used as it is;ProjectionParametersproject onto the family ofq(out)with those parameters.in_prjparams: the families of the inputs' marginals, aNamedTuplekeyedin_1,in_2, …, each as forout_prjparams, ornothing, the default, for every input. A missing key, ornothing, projects inputkonto the family of its message.proposal_distribution: theProposalDistributionContainerthe inputs are sampled from, for a node with several inputs. Default: an empty one, so the first call samples the inputs' messages.sampling_strategy: how those inputs are sampled,FullSamplingorMeanBased. DefaultFullSampling(10).
The rules
- Towards
out: samples of the joint over the inputs,q[(:in,)], aFactorizedJoint, pushed through the function and projected; the message is that projection divided by the message that arrived fromout, left unevaluated, so its product with that message is the projection. - The joint over the inputs, from
m[:out]and the inputs' messages: for one input, its message reweighted by the likelihood ofoutand projected; for several, each input in turn, against samples of the others' latest projections. The result becomes the next proposal. - Towards
(:in, k): the input's component of the joint divided by its message, left unevaluated.
The samples are drawn from the rule context's generator, ctx.rng, which the engine owns. The joint rule writes its result into proposal_distribution, so the method carries state, its rule is impure, and one CVIProjection value given to several nodes shares one proposal among them. A known inverse is not used, and is ignored with a warning.
Examples
using DeltaMessagePassingRules, ExponentialFamilyProjection
algorithm = DeltaApproximation(method = CVIProjection(sampling_strategy = MeanBased()))How the other inputs are sampled while one input of a node with several is projected:
DeltaMessagePassingRules.CVISamplingStrategy — Type
CVISamplingStrategyHow CVIProjection samples the other inputs while it projects one input of a node with several: FullSampling or MeanBased. A node with one input does not sample for its joint.
DeltaMessagePassingRules.FullSampling — Type
FullSampling(samples::Int = 10)The CVISamplingStrategy that draws samples samples from each input's distribution, the proposal's or, before there is one, the input's message. The default strategy of CVIProjection.
DeltaMessagePassingRules.MeanBased — Type
MeanBased()The CVISamplingStrategy that uses each input's mean as its only sample: much faster than FullSampling, and less accurate for a strongly nonlinear function or a wide distribution.
The proposal the inputs are sampled from:
DeltaMessagePassingRules.ProposalDistributionContainer — Type
ProposalDistributionContainer(distribution)
ProposalDistributionContainer{Any}(nothing)A mutable holder of the proposal CVIProjection samples the inputs from, for a node with several inputs. distribution is nothing, and the inputs' messages are sampled, or a FactorizedJoint of the inputs, whose components are sampled. The rule for the joint over the inputs replaces it with its result on every call.