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.
| node | components | algorithm | rules | average energy |
|---|---|---|---|---|
NormalMixture | normals with means m and precisions p | NormalMixtureVMP | variational, over marginals | yes |
GammaMixture | Gammas with shapes a and rates b | GammaMixtureVMP | variational, over marginals | yes |
Mixture | any, the messages of inputs | MixtureBP | belief propagation, over messages and their log scales | no |
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
trueNormalMixture
\[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)| interface | aliases | |
|---|---|---|
out | ||
switch | ||
m | a group of any number of members | |
p | a group of any number of members |
| default algorithm | NormalMixtureVMP() |
|---|---|
| static inputs | none |
| matched groups | m = p |
| min group length | 2 |
| factorisation | meanfield |
| defined | nodes/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)| 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))),
)Result
| value | Distributions.Categorical{Float64, Vector{Float64}}(support=Base.OneTo(2), p=[0.997527, 0.00247262]) |
|---|---|
| type | Distributions.Categorical{Float64, Vector{Float64}} |
| log scale | undefined: the message rule for NormalMixture towards :switch under NormalMixtureVMP declares no `logscale` |
Inputs
| edge | value | |
|---|---|---|
| out | q | PointMass{Float64}(0.5) |
| m[1] | q | PointMass{Float64}(0.0) |
| m[2] | q | PointMass{Float64}(4.0) |
| p[1] | q | PointMass{Float64}(1.0) |
| p[2] | q | PointMass{Float64}(1.0) |
Rule
| declared inputs | q[:out]::Any, q[:m...]::Any, q[:p...]::Any |
|---|---|
| algorithm | NormalMixtureVMP() |
| log scale | none |
| defined | normal_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)),
)Result
| value | ExponentialFamily.NormalMeanPrecision{Float64}(μ=0.5, w=1.8) |
|---|---|
| type | ExponentialFamily.NormalMeanPrecision{Float64} |
| log scale | undefined: the message rule for NormalMixture towards (:m, k) under NormalMixtureVMP declares no `logscale` |
Inputs
| edge | value | |
|---|---|---|
| out | q | PointMass{Float64}(0.5) |
| switch | q | Distributions.Categorical{Float64, Vector{Float64}}(support=Base.OneTo(2), p=[0.9, 0.1]) |
| p[1] | q | ExponentialFamily.GammaShapeRate{Float64}(a=2.0, b=1.0) |
| p[2] | unused | unused |
Rule
| declared inputs | q[:out]::Any, q[:switch]::Union{BayesBase.PointMass{<:AbstractVector}, Distributions.Bernoulli, Distributions.Categorical{P} where P<:Real}, q[:p][k]::Any |
|---|---|
| algorithm | NormalMixtureVMP() |
| log scale | none |
| defined | normal_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
NormalMixtureA 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
trueStandardMessagePassingRules.GaussianMixture — Type
GaussianMixtureAn alias of NormalMixture.
StandardMessagePassingRules.NormalMixtureVMP — Type
NormalMixtureVMP()NormalMixture's own algorithm. Variational message passing, regardless of the factorisation: every rule takes the marginals of out, switch and the components, never their messages. It is the node's default, so a model need not name it. It stands alone, a direct subtype of AbstractAlgorithm, and inherits nothing from DefaultAlgorithm, which has no mixture rules.
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)| 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
GammaMixtureA 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.
StandardMessagePassingRules.GammaMixtureVMP — Type
GammaMixtureVMP()GammaMixture's own algorithm. Variational message passing, regardless of the factorisation: every rule takes the marginals of out, switch and the components, never their messages. It is the node's default, so a model need not name it. It stands alone, a direct subtype of AbstractAlgorithm, and inherits nothing from DefaultAlgorithm, which has no mixture rules.
Mixture
\[p(\mathrm{out} \mid \mathrm{switch}, \mathrm{inputs}) = \prod_{k=1}^K \mathrm{inputs}_k(\mathrm{out})^{\mathrm{switch}_k}\]
MessagePassingRulesBase.nodespec(Mixture)| interface | aliases | |
|---|---|---|
out | ||
switch | ||
inputs | a group of any number of members |
| default algorithm | MixtureBP{BayesBase.GenericProd}(GenericProd()) |
|---|---|
| static inputs | none |
| defined | nodes/mixture.jl:52 |
MessagePassingRulesBase.rule_coverage(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)),
)Result
| value | Distributions.Categorical{Float64, Vector{Float64}}(support=Base.OneTo(2), p=[0.995741, 0.00425861]) |
|---|---|
| type | Distributions.Categorical{Float64, Vector{Float64}} |
| log scale | -1.0759622859974485 (computed by the body) |
Inputs
| edge | value |
|---|
| incoming log scale inputs | (0.0, 0.0) |
|---|---|
| incoming log scale out | 0.0 |
Rule
| declared inputs | m[:out]::Any, m[:inputs...]::Any |
|---|---|
| algorithm | MixtureBP{GenericProd}(GenericProd()) |
| log scale | from the body, reads the incoming ones |
| defined | mixture/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
MixtureA 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.
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 towardsswitchmultiplies the message fromoutwith each input's, for the log scale of their product, which is componentk's evidence. Any BayesBase product strategy. DefaultGenericProd().
It stands alone, a direct subtype of AbstractAlgorithm. Its dependencies and rules are declared on MixtureBP itself, so they apply for every product strategy.