Mixtures

A mixture node models a variable out drawn from one of several components, with a one-hot switch that selects the component. Models use it for clustering and regime switching. The message towards switch says which component explains out best, and the message towards a component treats out as that component's data, weighted by the probability of the switch.

The page has three mixture nodes. Unlike the distribution nodes, each declares an algorithm of its own, because its rules ignore the factorisation. NormalMixture and GammaMixture are always variational: their rules take marginals and use expectations such as E[log p]. Mixture is always belief propagation: its rules take messages and their log scales, which weigh the components by their evidence. The algorithm is the node's default, so a model need not name it.

nodecomponentsalgorithmrulesaverage energy
NormalMixturenormals with means m and precisions pNormalMixtureVMPvariational, over marginalsyes
GammaMixtureGammas with shapes a and rates bGammaMixtureVMPvariational, over marginalsyes
Mixtureany, the messages of inputsMixtureBPbelief propagation, over messages and their log scalesno

The components are groups, declared with ... (:m...). The number of components is the number of members a model connects, at least two. NormalMixture and GammaMixture need the mean-field factorisation, and as many members in one group as in the other. The engine's factornode checks both.

Example

With both components known, the switch probabilities of a point near the first component's mean favour that component:

julia> using StandardMessagePassingRules, MessagePassingRulesBase, BayesBase, Distributions

julia> switch = getresult(@call_message_update_rule(
           node = NormalMixture, target = :switch,
           q = (out = PointMass(0.5), m = (PointMass(0.0), PointMass(4.0)), p = (PointMass(1.0), PointMass(1.0))),
       ));

julia> probvec(switch)[1] > 0.99
true

NormalMixture

\[p(\mathrm{out} \mid \mathrm{switch}, m, p) = \prod_{k=1}^K \mathcal{N}(\mathrm{out} \mid m_k, p_k^{-1})^{\mathrm{switch}_k}\]

MessagePassingRulesBase.nodespec(NormalMixture)
NodeSpec: NormalMixture
switchm…p…outNormalMixturestochastic
interfacealiases
out
switch
ma group of any number of members
pa group of any number of members
default algorithmNormalMixtureVMP()
static inputsnone
matched groupsm = p
min group length2
factorisationmeanfield
definednodes/normal_mixture.jl:81

The declaration draws the two groups, m for the means and p for the precisions. It also lists the requirements that the engine checks: matched groups, at least two members and the mean-field factorisation.

MessagePassingRulesBase.rule_coverage(NormalMixture)
Rule coverage for NormalMixture
NormalMixtureVMP
→ out✓
→ switch✓
→ (m, k)✓×2
→ (p, k)✓×2
average energy✓

The message towards switch compares the expected log-density of out under each component:

@call_message_update_rule(
    node = NormalMixture, target = :switch,
    q = (out = PointMass(0.5), m = (PointMass(0.0), PointMass(4.0)), p = (PointMass(1.0), PointMass(1.0))),
)
RuleResult: message of NormalMixture towards :switchmarginals
outm[1]m[2]p[1]p[2]switchNormalMixture
Result
valueDistributions.Categorical{Float64, Vector{Float64}}(support=Base.OneTo(2), p=[0.997527, 0.00247262])
typeDistributions.Categorical{Float64, Vector{Float64}}
log scaleundefined: the message rule for NormalMixture towards :switch under NormalMixtureVMP declares no `logscale`
Inputs
edgevalue
outqPointMass{Float64}(0.5)
m[1]qPointMass{Float64}(0.0)
m[2]qPointMass{Float64}(4.0)
p[1]qPointMass{Float64}(1.0)
p[2]qPointMass{Float64}(1.0)
Rule
declared inputsq[:out]::Any, q[:m...]::Any, q[:p...]::Any
algorithmNormalMixtureVMP()
log scalenone
definednormal_mixture/switch.jl:3
body
args->begin
        U = [-(mixture_component_energy(args.q[:out], m, p)) for (m, p) = zip(args.q[:m], args.q[:p])]
        Categorical(clamp!(softmax!(U), tiny, one(eltype(U)) - tiny))
    end

The message towards the first mean, (:m, 1), is a normal around E[out]. Its precision is E[p₁], weighted by the switch's probability of the first component, 0.9 × 2. The rule reads only the first precision, q[:p][k], so the call passes nothing for the second, as a graph does:

@call_message_update_rule(
    node = NormalMixture, target = (:m, 1),
    q = (out = PointMass(0.5), switch = Categorical([0.9, 0.1]), p = (GammaShapeRate(2.0, 1.0), nothing)),
)
RuleResult: message of NormalMixture towards (:m, 1)marginals
outswitchp[1]p[2]m[1]NormalMixture
Result
valueExponentialFamily.NormalMeanPrecision{Float64}(μ=0.5, w=1.8)
typeExponentialFamily.NormalMeanPrecision{Float64}
log scaleundefined: the message rule for NormalMixture towards (:m, k) under NormalMixtureVMP declares no `logscale`
Inputs
edgevalue
outqPointMass{Float64}(0.5)
switchqDistributions.Categorical{Float64, Vector{Float64}}(support=Base.OneTo(2), p=[0.9, 0.1])
p[1]qExponentialFamily.GammaShapeRate{Float64}(a=2.0, b=1.0)
p[2]unusedunused
Rule
declared inputsq[:out]::Any, q[:switch]::Union{BayesBase.PointMass{<:AbstractVector}, Distributions.Bernoulli, Distributions.Categorical{P} where P<:Real}, q[:p][k]::Any
algorithmNormalMixtureVMP()
log scalenone
definednormal_mixture/m.jl:3
body
args->begin
        pv = probvec(args.q[:switch])
        z = clamp(pv[k], tiny, one(eltype(pv)) - tiny)
        normal = promote_variate_type(variate_form(typeof(args.q[:out])), NormalMeanPrecision)
        normal(mean(args.q[:out]), z * mean((args.q[:p])[k]))
    end
Other rules for this target (1)
normal_mixture/m.jl:14
✓algorithm NormalMixtureVMP
✓q[:out]::Any got BayesBase.PointMass{Float64}
✗q[:switch]::BayesBase.PointMass{<:Real} got Distributions.Categorical{Float64, Vector{Float64}}
✓q[:p][k]::Any got Tuple{ExponentialFamily.GammaShapeRate{Float64}, Nothing}

The two rules towards each component are the variational one and one for an integer PointMass switch, which throws an error.

StandardMessagePassingRules.NormalMixture — Type
NormalMixture

A mixture of normals: out is drawn from the component the one-hot switch selects, with means m and precisions p, both groups with one member per component,

\[p(\mathrm{out} \mid \mathrm{switch}, m, p) = \prod_{k=1}^K \mathcal{N}(\mathrm{out} \mid m_k, p_k^{-1})^{\mathrm{switch}_k}.\]

A component is a NormalMeanPrecision, with a Gamma precision, or, when out is a vector, an MvNormalMeanPrecision, with a Wishart precision. The number of components K is the groups' length, not a type parameter. Its interfaces are out, switch and the groups m and p; GaussianMixture is an alias.

A node needs at least two components, as many members in one group as in the other, and a mean-field factorisation; factornode checks all three. The switch's marginal must be a Categorical, a Bernoulli or a one-hot vector PointMass: an integer PointMass switch is an error, which says to one-hot encode it.

Its rules run under its own algorithm, NormalMixtureVMP, and are always variational. Towards out the message is a normal in weighted-mean form, towards switch a Categorical of the components' responsibilities, towards (:m, k) a normal and towards (:p, k) a GammaShapeRate or a Wishart likelihood. It has an average energy, each component's weighted by the switch.

The :out and :switch dependencies list the precisions before the means. 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: each component's precision is updated before its mean. Both orders reach the same optimum, but precisions first converges faster, in about 7 iterations against about 13 on a measured mixture model. The order of the members within a group changes nothing, since they do not depend on each other.

Examples

julia> switch = getresult(@call_message_update_rule(
           node = NormalMixture, target = :switch,
           q = (out = PointMass(0.0), m = (PointMass(0.0), PointMass(5.0)), p = (PointMass(1.0), PointMass(1.0))),
       ));

julia> probvec(switch)[1] > 0.99
true
source

GammaMixture

\[p(\mathrm{out} \mid \mathrm{switch}, a, b) = \prod_{k=1}^K \mathrm{Gamma}(\mathrm{out} \mid a_k, b_k)^{\mathrm{switch}_k},\]

with shapes a[k] and rates b[k], as in GammaShapeRate.

MessagePassingRulesBase.rule_coverage(GammaMixture)
Rule coverage for GammaMixture
GammaMixtureVMP
→ out✓
→ switch✓
→ (a, k)✓×2
→ (b, k)✓×2
average energy✓

As for NormalMixture, the second rule towards each component throws the error for an integer switch. The rules towards out, switch and the shapes a need a Gamma marginal of each rate b[k], and they refuse a PointMass rate.

StandardMessagePassingRules.GammaMixture — Type
GammaMixture

A mixture of Gamma distributions: out is drawn from the component the one-hot switch selects, a GammaShapeRate with shape a and rate b, both groups with one member per component,

\[p(\mathrm{out} \mid \mathrm{switch}, a, b) = \prod_{k=1}^K \mathrm{Gamma}(\mathrm{out} \mid a_k, b_k)^{\mathrm{switch}_k}.\]

The number of components K is the groups' length. Its interfaces are out, switch and the groups a and b.

A node needs at least two components, as many members in one group as in the other, and a mean-field factorisation; factornode checks all three. The switch's marginal must be a Categorical, a Bernoulli or a one-hot vector PointMass: an integer PointMass switch is an error, which says to one-hot encode it. The rates' marginals must be Gamma distributions.

Its rules run under its own algorithm, GammaMixtureVMP, and are always variational. Towards out the message is a GammaShapeRate, towards switch a Categorical, towards (:a, k) a GammaShapeLikelihood and towards (:b, k) a GammaShapeRate. It has an average energy, each component's weighted by the switch. The :out and :switch dependencies list the rates before the shapes, which is the update schedule; the order within a group changes nothing.

source

Mixture

\[p(\mathrm{out} \mid \mathrm{switch}, \mathrm{inputs}) = \prod_{k=1}^K \mathrm{inputs}_k(\mathrm{out})^{\mathrm{switch}_k}\]

MessagePassingRulesBase.nodespec(Mixture)
NodeSpec: Mixture
switchinputs…outMixturestochastic
interfacealiases
out
switch
inputsa group of any number of members
default algorithmMixtureBP{BayesBase.GenericProd}(GenericProd())
static inputsnone
definednodes/mixture.jl:52
MessagePassingRulesBase.rule_coverage(Mixture)
Rule coverage for Mixture
MixtureBP
→ out✓
→ switch✓
→ (inputs, k)✓

The rules are declared on MixtureBP itself, so that they apply for every product strategy, the default MixtureBP{GenericProd} included.

The message towards switch weighs each component by the evidence of out under it: the log scale of the product of the message on out and the component's message, plus the incoming log scales. A call passes the incoming log scales with the logscale keyword:

@call_message_update_rule(
    node = Mixture, target = :switch,
    m = (out = NormalMeanVariance(0.5, 0.1), inputs = (NormalMeanVariance(0.0, 1.0), NormalMeanVariance(4.0, 1.0))),
    logscale = (out = 0.0, inputs = (0.0, 0.0)),
)
RuleResult: message of Mixture towards :switchmessages
outinputs[1]inputs[2]switchMixture
Result
valueDistributions.Categorical{Float64, Vector{Float64}}(support=Base.OneTo(2), p=[0.995741, 0.00425861])
typeDistributions.Categorical{Float64, Vector{Float64}}
log scale-1.0759622859974485 (computed by the body)
Inputs
edgevalue
outmExponentialFamily.NormalMeanVariance{Float64}(μ=0.5, v=0.1)
inputs[1]mExponentialFamily.NormalMeanVariance{Float64}(μ=0.0, v=1.0)
inputs[2]mExponentialFamily.NormalMeanVariance{Float64}(μ=4.0, v=1.0)
incoming log scale inputs(0.0, 0.0)
incoming log scale out0.0
Rule
declared inputsm[:out]::Any, m[:inputs...]::Any
algorithmMixtureBP{GenericProd}(GenericProd())
log scalefrom the body, reads the incoming ones
definedmixture/switch.jl:4
body
(algo, args)->begin
        out = incoming_logscale(args.logscale.m[:out])
        logscales = [out + incoming_logscale(l) + product_logscale(algo.prod, args.m[:out], m) for (m, l) = zip(args.m[:inputs], args.logscale.m[:inputs])]
        with_logscale(Categorical(softmax(logscales)), logsumexp(logscales))
    end

Limitations. Mixture has no average energy, so the free energy of a model with a Mixture is an error. The graph must track log scales, and an unknown incoming log scale is an error.

StandardMessagePassingRules.Mixture — Type
Mixture

A mixture of arbitrary components: out is the member of the group inputs that the one-hot switch selects,

\[p(\mathrm{out} \mid \mathrm{switch}, \mathrm{inputs}) = \prod_{k=1}^K δ(\mathrm{out} - \mathrm{inputs}_k)^{\mathrm{switch}_k}.\]

Its interfaces are out, switch and the group inputs, one member per component.

Its rules are belief propagation over the incoming messages, whatever the factorisation, under its own algorithm, MixtureBP. Towards out the message is a MixtureDistribution of the inputs' messages, towards switch a Categorical of each component's evidence, and towards (:inputs, k) the message from out, scaled by the probability of k. The switch's message must have a probvec, a Categorical or a Bernoulli; the inputs' messages may be of any type whose product with the message from out has a log scale under the algorithm's product strategy.

The rules read the log scales of their incoming messages (reads_logscale = true), so a graph with a Mixture must track log scales (the engine's activation option logscales = true); an unknown log scale is an error. The node has no average energy: the free energy of a model with one is an error.

source
StandardMessagePassingRules.MixtureBP — Type
MixtureBP(; prod = GenericProd())

Mixture's own algorithm: belief propagation over the incoming messages, regardless of the factorisation. MixtureBP(GenericProd()) is the node's default, so a model need not name it.

Keywords

  • prod: the product strategy under which the rule towards switch multiplies the message from out with each input's, for the log scale of their product, which is component k's evidence. Any BayesBase product strategy. Default GenericProd().

It stands alone, a direct subtype of AbstractAlgorithm. Its dependencies and rules are declared on MixtureBP itself, so they apply for every product strategy.

source