MultinomialPolya
Overview
MultinomialPolya is a multinomial over K categories whose probabilities come from K - 1 log-odds ψ by logistic stick-breaking: the first category takes σ(ψ₁) of the stick, the second σ(ψ₂) of what is left, and the last the rest. The multinomial then factors into K - 1 binomials, each Pólya-Gamma augmented as the overview shows, so the message towards ψ is normal. With ψ a linear function of covariates it is multinomial regression.
Definition
\[p(x \mid N, \psi) = \frac{N!}{\prod_{k=1}^{K} x_k!} \prod_{k=1}^{K} p_k^{x_k}, \qquad p_k = \sigma(\psi_k) \prod_{j<k} \big(1 - \sigma(\psi_j)\big), \quad p_K = \prod_{j<K} \big(1 - \sigma(\psi_j)\big)\]
The first factor is the multinomial coefficient; p_k is the share of the stick that category k takes. The k-th binomial is of x_k out of N_k = N - Σ_{j<k} x_j trials, with log-odds ψ_k. logistic_stick_breaking computes the p_k and compose_Nks the N_k.
Interfaces
using PolyaMessagePassingRules, MessagePassingRulesBase, BayesBase, ExponentialFamily
MessagePassingRulesBase.nodespec(MultinomialPolya)| interface | aliases | |
|---|---|---|
x | ||
N | ||
ψ |
| default algorithm | MultinomialPolyaApproximation(21) |
|---|---|
| static inputs | none |
| defined | src/multinomial_polya.jl:95 |
The node has three interfaces. The table lists the marginals and messages the rules read on each.
| name | meaning | messages and marginals the rules take |
|---|---|---|
x | the counts of the K categories | any marginal with a vector mean, PointMass or Multinomial for the energy |
N | the number of trials | PointMass, Poisson, Binomial or Categorical, by its mode; PointMass for the energy |
ψ | the K - 1 log-odds | normal message towards ψ; any marginal with a mean towards x; normal or PointMass for the energy |
The rule towards ψ sends a normal in weighted-mean and precision form, with a diagonal precision, univariate when K = 2; the rule towards x sends a Multinomial. There is no rule towards N.
Algorithm
PolyaMessagePassingRules.MultinomialPolyaApproximation — Type
MultinomialPolyaApproximation(; points = 21)The algorithm of MultinomialPolya, and its default: a model names it only to change points.
Keywords
points: the number of Gauss–Hermite points with which the average energy computes each⟨softplus(ψ_k)⟩. Default21. The messages do not depend on it.
Examples
julia> MultinomialPolyaApproximation().points
21MultinomialPolyaApproximation() is the node's default algorithm, so a model names it only to change the number of cubature points of the average energy.
The node declares its dependencies, the inputs each target's rule takes:
MessagePassingRulesBase.dependencies_spec(MultinomialPolya, MultinomialPolyaApproximation())| free-energy partition | from the factorisation |
|---|
Every target takes the inputs of the default scheme. The rule towards ψ also reads the message on ψ, the current estimate at which it takes the Pólya-Gamma means.
Supported rules
MessagePassingRulesBase.rule_coverage(MultinomialPolya)| MultinomialPolyaApproximation | |
|---|---|
| → x | ✓ |
| → N | |
| → ψ | ✓ |
| average energy | ✓ |
The rules towards x and ψ read marginals and, towards ψ, the message on ψ. They fit a mean-field factorisation. With the average energy, the Bethe free energy is available.
Example
The message towards ψ for ten trials over three categories, at a message on ψ centred on zero:
@call_message_update_rule(
node = MultinomialPolya, target = :ψ,
q = (x = PointMass([2, 3, 5]), N = PointMass(10)),
m = (ψ = MvNormalWeightedMeanPrecision(zeros(2), [1.0 0.0; 0.0 1.0]),),
)Result
| value | MvNormalWeightedMeanPrecision( xi: [-3.0, -1.0] Λ: Diagonal([2.5, 2.0]) ) |
|---|---|
| type | ExponentialFamily.MvNormalWeightedMeanPrecision{Float64, Vector{Float64}, LinearAlgebra.Diagonal{Float64, Vector{Float64}}} |
| log scale | undefined: the message rule for MultinomialPolya towards :ψ under MultinomialPolyaApproximation declares no `logscale` |
Inputs
| edge | value | |
|---|---|---|
| x | q | PointMass{Vector{Int64}}([2, 3, 5]) |
| N | q | PointMass{Int64}(10) |
Rule
| declared inputs | q[:x]::Any, q[:N]::Union{BayesBase.PointMass, Distributions.Binomial, Distributions.Poisson, Distributions.Categorical{P} where P<:Real}, m[:ψ]::Union{ExponentialFamily.NormalDistributionsFamily{T}, ExponentialFamily.GaussianDistributionsFamily{T}} where T |
|---|---|
| algorithm | MultinomialPolyaApproximation(21) |
| log scale | none |
| defined | src/multinomial_polya.jl:165 |
| body | args->begin
x = mean(args.q[:x])
K = length(x)
Nks = compose_Nks(x, mode(args.q[:N]))
ω = map(((n, c)->mean(PolyaGammaHybridSampler(n, c))), Nks, mean(args.m[:ψ]))
η = map(((d, n)->d - n / 2), view(x, 1:K - 1), Nks)
Λ = if length(η) == 1
ω[1]
else
Diagonal(ω)
end
η = if length(η) == 1
η[1]
else
η
end
convert(promote_variate_type(typeof(η), NormalWeightedMeanPrecision), η, Λ)
end |
@call_message_update_rule runs the rule and draws its inputs. The stick's two breaks leave N_k = 10 and 8 trials, so the weighted means are x_k - N_k/2 and the precisions N_k/4, which getresult confirms:
julia> using PolyaMessagePassingRules, MessagePassingRulesBase, BayesBase, ExponentialFamily
julia> result = @call_message_update_rule(
node = MultinomialPolya, target = :ψ,
q = (x = PointMass([2, 3, 5]), N = PointMass(10)),
m = (ψ = MvNormalWeightedMeanPrecision(zeros(2), [1.0 0.0; 0.0 1.0]),),
);
julia> weightedmean(getresult(result)) ≈ [2 - 10 / 2, 3 - 8 / 2]
true
julia> precision(getresult(result)) ≈ [10/4 0.0; 0.0 8/4]
trueLimitations
- The rule towards
ψneeds an initial message onψ. - There is no rule towards
N, and the average energy takes aPointMassq(N)only. - The messages use the mean of
ψonly: its variance enters the average energy, not them. - The average energy computes each
⟨softplus(ψ_k)⟩by Gauss–Hermite cubature withMultinomialPolyaApproximation(points = …)points, 21 by default. It is exact to rounding whileψ_kis narrow, a variance up to about 1, and loses accuracy as it broadens, since softplus bends sharply on the cubature's scale: about1e-4relative at a variance of 25 and1e-2at 400, and more points help slowly. The free energy of a model with broad priors is approximate in its first iterations; the messages do not use the cubature.
API
The node, and the two exported helpers its rules are built on: the stick-breaking probabilities and the trials left at each break.
PolyaMessagePassingRules.MultinomialPolya — Type
MultinomialPolyaThe stochastic node of a multinomial through logistic stick-breaking,
\[p(x \mid N, \psi) = \frac{N!}{\prod_{k=1}^{K} x_k!} \prod_{k=1}^{K} p_k^{x_k}, \qquad p_k = \sigma(\psi_k) \prod_{j<k} \big(1 - \sigma(\psi_j)\big), \quad p_K = \prod_{j<K} \big(1 - \sigma(\psi_j)\big),\]
for K categories and K - 1 log-odds ψ, Pólya-Gamma augmented so that the message towards ψ is normal. The multinomial factors into K - 1 binomials, the k-th of x_k out of the N_k = N - Σ_{j<k} x_j trials left, with log-odds ψ_k. With ψ a linear function of covariates it is multinomial regression.
Interfaces
x: the counts of theKcategories, a vector: observed (PointMass) or aMultinomial;N: the number of trials, aPointMass, or aPoisson,BinomialorCategorical, whose mode the rules use;ψ: theK - 1log-odds,MvNormal, or a univariateNormalforK = 2.
Augmentation
For a count y out of b trials with log-odds ψ, the Pólya-Gamma identity
\[\frac{(e^{\psi})^{y}}{(1 + e^{\psi})^{b}} = 2^{-b} e^{\kappa \psi} \int_0^\infty e^{-\omega \psi^2 / 2} \, \mathrm{PG}(\omega \mid b, 0) \, d\omega, \qquad \kappa = y - b / 2,\]
makes the likelihood Gaussian in ψ given the auxiliary ω ~ PG(b, ψ): proportional to exp(κψ - ωψ²/2). The rules replace ω by its expectation, E[ω] = b / (2c) · tanh(c / 2) for PG(b, c), at the current estimate c of ψ, so the message towards the weights is a normal in weighted-mean and precision form.
Applied to each binomial of the stick with b = N_k, the message towards ψ has the weighted mean x_k - N_k / 2 and the diagonal precision ω_k, the Pólya-Gamma mean at the k-th entry of the mean of the message on ψ. The message towards x is the multinomial at logistic_stick_breaking of the mean of q(ψ).
The average energy is that of the multinomial itself, -⟨log C(x)⟩ - Σ_{k<K} ⟨x_k⟩⟨ψ_k⟩ + Σ_{k<K} N_k ⟨softplus(ψ_k)⟩, with each ⟨softplus(ψ_k)⟩ by Gauss–Hermite cubature over the mean and variance of ψ_k, with the points of MultinomialPolyaApproximation. For a Multinomial q(x), the coefficient's expectation is log N! - Σ_k ⟨log x_k!⟩ over its binomial marginals.
Limitations
- The rule towards
ψreads the message onψitself, besides the marginals its factorisation gives it, so a model must initialise that message. - There is no rule towards
N, and the average energy takes aPointMassq(N)only. - The rules use the mean of
ψonly: its variance enters the average energy, not the messages. - The Gauss–Hermite cubature of
⟨softplus(ψ)⟩is exact to rounding whileψis narrow, a variance up to about 1, and loses accuracy asψbroadens, since softplus bends sharply on the cubature's scale: about1e-4relative at a variance of 25 and1e-2at 400 with 32 points. Morepointshelp slowly. The free energy of a model with broad priors is therefore approximate in its first iterations; the messages do not use the cubature.
Examples
julia> result = @call_message_update_rule(
node = MultinomialPolya, target = :ψ,
q = (x = PointMass([2, 3, 5]), N = PointMass(10)),
m = (ψ = MvNormalWeightedMeanPrecision(zeros(2), [1.0 0.0; 0.0 1.0]),),
);
julia> weightedmean(getresult(result)) ≈ [-3.0, -1.0]
true
julia> precision(getresult(result)) ≈ [2.5 0.0; 0.0 2.0]
trueThe stick's two breaks leave N_k = 10 and 8 trials, so the weighted means are 2 - 5 and 3 - 4 and the precisions, at ψ = 0, N_k / 4.
See also MultinomialPolyaApproximation, logistic_stick_breaking, compose_Nks, BinomialPolya.
PolyaMessagePassingRules.logistic_stick_breaking — Function
logistic_stick_breaking(m) -> Vector{Float64}The K probabilities of logistic stick-breaking with the K - 1 log-odds m: p_k = σ(m_k) Π_{j<k} (1 - σ(m_j)) for k < K, and the rest of the stick, Π_{j<K} (1 - σ(m_j)), for the last. The probabilities of MultinomialPolya at ψ = m.
Examples
julia> using PolyaMessagePassingRules
julia> logistic_stick_breaking([0.0, 0.0])
3-element Vector{Float64}:
0.5
0.25
0.25PolyaMessagePassingRules.compose_Nks — Function
compose_Nks(x, N) -> VectorThe number of trials left at each of the first K - 1 breaks of the stick, for the K counts x out of N: N_k = N - Σ_{j<k} x_j, of the element type of x. The trials of the binomials MultinomialPolya factors into.
Examples
julia> using PolyaMessagePassingRules
julia> compose_Nks([2, 3, 5], 10)
2-element Vector{Int64}:
10
8