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)
end
ArgumentError: `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)
DependenciesSpec: DeltaFnunder DeltaApproximation{<:CVIProjection, Nothing}
m[:out]q[:out]q[(:in,)]outDeltaFnm[:in][k]q[(:in,)]in[k]DeltaFn
━▶ target─▶ message m┄▶ marginal q
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),)),
)
RuleResult: marginal of DeltaFn over (:in,)marginal
outin[1]q(in)DeltaFn
Result
valueFactorizedJoint{Tuple{NormalMeanVariance{Float64}}}((ExponentialFamily.NormalMeanVariance{Float64}(μ=-0.119498, v=0.472109),))
typeBayesBase.FactorizedJoint{Tuple{ExponentialFamily.NormalMeanVariance{Float64}}}
Inputs
edgevalue
outmDistributions.Gamma{Float64}(α=2.0, θ=1.0)
in[1]mExponentialFamily.NormalMeanVariance{Float64}(μ=0.0, v=1.0)
Rule
declared inputsm[:out]::Any, m[:in...]::Any
algorithmDeltaApproximation{CVIProjection{Int64, Nothing, Nothing, ProposalDistributionContainer{Any}, FullSampling}, Nothing}(C…
servicesnode, rng
definedext/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,),
)
RuleResult: message of DeltaFn towards :outmessages and marginals
outin…outDeltaFn
Result
valueDivisionOf{Gamma{Float64}, Gamma{Float64}}(Distributions.Gamma{Float64}(α=2.43227, θ=0.421246), Distributions.Gamma{Float64}(α=2.0, θ=1.0))
typeDeltaMessagePassingRulesProjectionExt.DivisionOf{Distributions.Gamma{Float64}, Distributions.Gamma{Float64}}
log scaleundefined: the message rule for DeltaFn towards :out under DeltaApproximation{<:CVIProjection} declares no `logscale`
Inputs
edgevalue
outmDistributions.Gamma{Float64}(α=2.0, θ=1.0) its own edge
in…qFactorizedJoint{Tuple{NormalMeanVariance{Float64}}}((ExponentialFamily.NormalMeanVariance{Float64}(μ=-0.119498, v=0.472… q[(:in,)]
Rule
declared inputsm[:out]::Any, q[:out]::Any, q[(:in,)]::BayesBase.FactorizedJoint
algorithmDeltaApproximation{CVIProjection{Int64, Nothing, Nothing, ProposalDistributionContainer{Any}, FullSampling}, Nothing}(C…
servicesnode, rng
log scalenone
definedext/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 out reads q(out) and the joint over the inputs, which depend on the node's own messages; a model whose out is not observed needs initial marginals for them.
  • One CVIProjection value 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 towards out. Default 100.
  • out_prjparams: the family of the message towards out. nothing, the default, projects onto the family of q(out) with the default projection parameters; a ProjectedTo is used as it is; ProjectionParameters project onto the family of q(out) with those parameters.
  • in_prjparams: the families of the inputs' marginals, a NamedTuple keyed in_1, in_2, …, each as for out_prjparams, or nothing, the default, for every input. A missing key, or nothing, projects input k onto the family of its message.
  • proposal_distribution: the ProposalDistributionContainer the 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, FullSampling or MeanBased. Default FullSampling(10).

The rules

  • Towards out: samples of the joint over the inputs, q[(:in,)], a FactorizedJoint, pushed through the function and projected; the message is that projection divided by the message that arrived from out, 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 of out and 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()))
source

How the other inputs are sampled while one input of a node with several is projected:

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.

source