4. Mixtures and discrete latents¶
When a model has a discrete latent variable, you can sample it, which incurs score-function variance when pathwise gradients are unavailable, or marginalize it over its finite support. Marginalization produces a deterministic contribution from that latent and often improves gradients, but its cost grows with the support and the body of the block.
QVR makes finite marginalization a first-class block. The body runs once per
value in an enumerable support; the runtime combines the resulting scope
weights with the prior by logsumexp. This operation exactly sums out a
finite discrete latent and is the categorical-prior form of
Rao–Blackwellization (Casella & Robert, 1996).
It does not numerically integrate a continuous latent: a marginalize whose
family has no finite support follows the ordinary sampling path, and an
explicit reduction on such a family is rejected.
A two-component Gaussian mixture¶
Each observation comes from one of two Gaussian clusters; we don't know which.
object Item : FinSet 500
object Component : FinSet 2
object Weights : Real 2
program gmm : Item -> Weights [effects=[Sample, Score, Marginal]]
sample probs <- Dirichlet(concentration=1.0) [over=Component]
sample mu_k : Component <- Normal(loc=0.0, scale=5.0)
sample sd_k : Component <- HalfNormal(scale=1.0)
marginalize z : Component <- Categorical(probs=probs) [over=Item]
observe y : Item <- Normal(loc=mu_k[z], scale=sd_k[z]) [via=item_idx]
return probs
export gmm
@config_enumerate
def model(data):
probs = pyro.sample("probs", dist.Dirichlet(torch.ones(2)))
mu_k = pyro.sample("mu_k", dist.Normal(0., 5.).expand([2]).to_event(1))
sd_k = pyro.sample("sd_k", dist.HalfNormal(1.).expand([2]).to_event(1))
with pyro.plate("data", len(data)):
z = pyro.sample("z", dist.Categorical(probs),
infer={"enumerate": "parallel"})
pyro.sample("y", dist.Normal(mu_k[z], sd_k[z]), obs=data)
data { int N; vector[N] y; }
parameters {
simplex[2] probs;
ordered[2] mu_k;
vector<lower=0>[2] sd_k;
}
model {
probs ~ dirichlet([1, 1]');
mu_k ~ normal(0, 5);
sd_k ~ normal(0, 1);
for (n in 1:N) {
vector[2] lp;
for (k in 1:2)
lp[k] = log(probs[k])
+ normal_lpdf(y[n] | mu_k[k], sd_k[k]);
target += log_sum_exp(lp);
}
}
probs is one point on the component simplex; the annotation on mu_k and
sd_k instead draws one scalar for each component. The
marginalize ... [over=Item] block gives each row its own latent assignment;
the host supplies item_idx = arange(500), the identity fibration from rows to
those groups. This implements the Stan log_sum_exp pattern. The Marginal
entry in the optional effect summary records the finite reduction.
Fitting the mixture¶
The gallery's
mixture_model.qvr uses the
closed-form MixtureNormal family instead. The code below shows the explicit
finite-marginalization form so that the latent support and grouping rule remain
visible.
import torch
from quivers.dsl import loads
from quivers.inference import AutoNormalGuide, ELBO, SVI
GMM_SRC = """
object Item : FinSet 500
object Component : FinSet 2
object Weights : Real 2
program gmm : Item -> Weights
sample probs <- Dirichlet(concentration=1.0) [over=Component]
sample mu_k : Component <- Normal(loc=0.0, scale=5.0)
sample sd_k : Component <- HalfNormal(scale=1.0)
marginalize z : Component <- Categorical(probs=probs) [over=Item]
observe y : Item <- Normal(loc=mu_k[z], scale=sd_k[z]) [via=item_idx]
return probs
export gmm
"""
program = loads(GMM_SRC)
model = program.morphism
torch.manual_seed(0)
true_mu = torch.tensor([-2.0, 2.0])
true_sd = torch.tensor([0.5, 0.7])
z_true = torch.bernoulli(torch.full((500,), 0.6)).long()
y_data = (torch.randn(500) * true_sd[z_true] + true_mu[z_true]).unsqueeze(0)
guide = AutoNormalGuide(model, observed_names={"y", "item_idx"})
elbo = ELBO(num_particles=1)
optimizer = torch.optim.Adam(
list(model.parameters()) + list(guide.parameters()), lr=1e-2,
)
svi = SVI(model, guide, optimizer, elbo)
x_tensor = torch.zeros(1, 1)
observations = {
"y": y_data,
"item_idx": torch.arange(500, dtype=torch.long),
}
for _ in range(20): # bump to ~3000 for real fits
svi.step(x_tensor, observations)
The marginalize block is integrated out exactly at every SVI step, so the gradients on mu_k, sd_k, and probs flow through a smooth logsumexp. No discrete-sampling variance contaminates the ELBO.
Hierarchical mixtures with grouping¶
Suppose each observation belongs to one of G groups, and the categorical mixture proportions vary by group. The marginalization has to respect group membership: the log-likelihood over the discrete latent gets aggregated per group, not per row. The marginalize header declares the grouping plate (over G); each observe inside the body carries its own via <idx> clause naming the fibration from its response plate into the grouping plate.
object Item : FinSet 1000
object Group : FinSet 20
object Component : FinSet 3
object Weights : Real 3
program grouped_mixture : Item -> Weights [effects=[Sample, Score, Marginal]]
sample probs : Group <- Dirichlet(concentration=1.0) [over=Component, iid_over=Group]
sample mu_k : Component <- Normal(loc=0.0, scale=5.0)
sample sd_k : Component <- HalfNormal(scale=1.0)
marginalize z : Component <- Categorical(probs=probs) [over=Group]
observe y : Item <- Normal(loc=mu_k[z], scale=sd_k[z]) [via=group_idx]
return probs
export grouped_mixture
The [over=Group] entry declares the grouping plate. The free host-data name
group_idx is the integer-valued fibration from observation rows into that
plate; [via=group_idx] gathers each row into its group before the reduction.
It is data, not a sampled HalfNormal latent. The block contributes
to the log-density, which is the right Kan extension along the fibration Item -> G and matches Stan's target += log_mix(probs[g], ll_item[i]) accumulation. A grouped block can contain multiple observes, each with its own [via=<idx>] entry, when several heterogeneous response axes share the same per-group class indicator; the per-axis log-likelihoods scatter-sum into the same (|G|, K) accumulator before the log-sum-exp.
When to marginalize vs sample¶
The decision is a straight cost-benefit:
marginalizecosts roughlyK × (body cost)per evaluation. The reward is exact gradients with respect to the discrete-prior parameters and zero Monte Carlo variance on the discrete latent.- Sampling the discrete latent with
ScoreFunction(REINFORCE) evaluates one sampled branch but introduces Monte Carlo variance. The variance depends on the prior, the component likelihoods, and any control variate.
There is no repository-wide support-size threshold at which one method wins. Profile the full model. For larger supports, possible alternatives include score-function estimators with baselines or a relaxed proxy such as the Concrete distribution (Maddison, Mnih & Teh, 2017); each changes the computational or statistical tradeoff.
| Discrete support per row | Recommendation |
|---|---|
Small finite K and cheap body |
Start with marginalize. |
Larger finite K or expensive body |
Profile marginalization against a sampled estimator. |
| Very large or unbounded support | Use a sampled estimator or a model-specific approximation. |
| Continuous-discrete mixture | marginalize the discrete part, reparameterize the continuous part. |
flowchart LR
A["Program reaches<br/>discrete latent z : K"] --> CHOICE{"How is z handled?"}
CHOICE -->|sample z| SAMPLE["One sampled branch"] --> SCORE["Stochastic score"]
CHOICE -->|marginalize z| ENUM["Run body once<br/>for every z ∈ K"]
ENUM --> REDUCE["logsumexp across K"] --> SCORE
SCORE --> ELBO["Add result to ELBO"]
class A qv-input
class CHOICE qv-decision
class SAMPLE,ENUM,REDUCE,SCORE qv-checked
class ELBO qv-output
Try this¶
- Change the model to four components and inspect the recovered means.
Mixture models have a label-switching symmetry
(Stephens, 2000); sorting draws
after inference can summarize exchangeable components, but a deterministic
sort(mu_k)inside the likelihood is not the same as an ordered prior. - Convert the grouped mixture to a
marginalizewithout theover/viaclauses and observe the difference: per-row marginalization versus per-group. - Combine with chapter 3's plate-draws: a hierarchical mixture where each group has its own
mu_kdrawn from a hyperprior.
Next¶
Chapter 5 examines sequence-shaped models: HMMs, state-space models, and chart-based weighted deduction.
References¶
- Chris J. Maddison, Andriy Mnih, and Yee Whye Teh. 2017. The Concrete distribution: A continuous relaxation of discrete random variables. arXiv preprint arXiv:1611.00712.
- George Casella and Christian P. Robert. 1996. Rao-Blackwellisation of sampling schemes. Biometrika, 83(1):81–94.
- Matthew Stephens. 2000. Dealing with label switching in mixture models. Journal of the Royal Statistical Society Series B: Statistical Methodology, 62(4):795–809.