Gaussian Mixture Models and Expectation-Maximization

Michael BrenndoerferOctober 29, 202656 min read

Gaussian mixture models cluster with soft assignments. EM derivations and NumPy code explain covariance geometry, stable fitting, and component selection.

Machine Learning from ScratchAll chapters
Part 9 of 15Unsupervised Learning
Chapter 66Gaussian Mixture Models and Expectation-Maximization
Reading the full chapter · 10 sections · 56 min10 sections · 56 min

All 10 sections · 56 min read

Gaussian Mixture Models and Expectation-MaximizationFull chapter · Sections in this chapter1 / 10 ↓
Reading tools Definitions, links & notes

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Notes & comments

Gaussian Mixture Models and Expectation-Maximization

In K-means Clustering every observation belonged to exactly one cluster, and the algorithm committed to that answer the moment it assigned the point. That commitment is convenient, but it hides two things we often care about. The first is doubt: a point sitting between two clusters is not really "80% cluster A and 20% cluster B" to k-means; it is entirely cluster A or entirely cluster B depending on which side of the boundary it happens to fall. The second is shape: k-means measures similarity with a plain Euclidean distance, so its nearest-centroid decision regions have straight boundaries. The squared-distance objective favours compact, roughly isotropic groups, but the regions themselves need not be spherical or equally sized.

A Gaussian mixture model (GMM) replaces that hard machinery with a probability model. We imagine that nature first picked one of KK hidden groups for each observation, then drew the observation from a Gaussian distribution belonging to that group. Because the group labels are never observed, they are latent variables. The model does not need to be told the labels; it infers a probability distribution over them, which is exactly the soft assignment that k-means refused to give us.

The price of that richer model is that the likelihood we want to maximize contains the log of a sum, so the general multi-component maximum likelihood equations do not yield a direct closed-form joint parameter estimate like the simpler examples in Maximum Likelihood Estimation. The Expectation-Maximization (EM) algorithm solves this by repeatedly pretending the latent labels are known, taking their expected values under the current parameters, and then re-estimating the parameters as if those expectations were counts. An exact E-step followed by an M-step that increases the auxiliary objective cannot decrease the observed-data likelihood. That guarantee requires the stated updates and valid parameters; the diagonal-addition safeguard used in our practical code does not automatically inherit it. Even exact nondecrease is weaker than finding a global maximum.

By the end of this chapter we will have derived the E-step and M-step from scratch, worked a four-point example entirely by hand, built a numerically stable implementation that handles the underflow problems a naive version would hit, and compared its soft assignments against k-means on clusters that are deliberately overlapping and elliptical.

Advantages

The most immediate benefit is that the model reports uncertainty. The fitted quantity γik\gamma_{ik}, called the responsibility of component kk for observation ii, is a posterior probability under the fitted mixture: it is the model's chance that this observation was generated by component kk, conditional on its features and the current parameter estimates. We can threshold it to obtain clusters, keep it as a soft feature for a downstream model, or use it to flag observations that sit between groups and therefore deserve a human look. K-means supplies distances and hard assignments rather than these model-based posterior probabilities. A responsibility is not, by itself, evidence of calibration against real-world groups or a measure of uncertainty in the estimated parameters.

The second benefit is geometric flexibility. Each component carries its own full covariance matrix, so it can be elongated, rotated, and scaled differently from its neighbours. It can also carry its own mixing weight, so one component can be large and diffuse while another is small and tight. Data that k-means would either split in the wrong place or merge into one blob (think of a long diagonal ridge next to a compact ball) is handled naturally. Restricted covariance structures (shared, diagonal, or spherical) let us dial that flexibility back down when data are scarce, which gives us a principled continuum from k-means-like fits to fully general ones.

The third benefit is the fitted density itself. Once fitted, p(x)p(\mathbf{x}) is a proper probability distribution we can evaluate, sample from, and compare across models. That makes GMMs useful far beyond clustering: for density estimation, for anomaly detection (a point with very low mixture density is unusual), for generative sampling, and as a flexible component inside larger probabilistic models. Because the likelihood is a well-defined criterion, we can select the number of components with information criteria such as BIC rather than eyeballing a scree plot.

For a specified mixture, we can marginalize unobserved coordinates: each component's marginal is Gaussian, and their weighted sum is still a Gaussian mixture. Evaluating this marginal does not require filling in those coordinates. Our code and scikit-learn's GaussianMixture expect complete feature rows; fitting mixtures to incomplete training observations needs an additional missing-data procedure.

Disadvantages

The likelihood surface is difficult to optimize. With multiple components the log-likelihood is not concave, so EM can settle near different stationary points. Convergence of parameters requires additional regularity; likelihoods can also approach singular boundary configurations. Different initializations can produce visibly different answers with different likelihoods, and there is no certificate telling us that the best one was found. Multiple restarts and a structured initialization such as k-means are useful safeguards, although neither guarantees the best fit.

For an unrestricted mixture with at least two components, the sample likelihood is unbounded above. Put one component mean on an observation and shrink its covariance to zero while another nondegenerate component gives the remaining observations positive density. The first contribution diverges, even though this is a degenerate density fit. This is not a numerical quirk that better code removes; it is a property of the objective. The standard remedy is to add a small constant ϵ\epsilon to the diagonal of every covariance matrix, that is, replace Σk\boldsymbol{\Sigma}_k with Σk+ϵI\boldsymbol{\Sigma}_k + \epsilon\mathbf{I}, where ϵ>0\epsilon>0 is a positive regularization strength (often called reg_covar in code) and I\mathbf{I} is the d×dd \times d identity matrix. For a positive-semidefinite scatter matrix, this imposes a positive covariance eigenvalue floor and caps the density. It prevents singular factorization but does not establish global optimality or exact EM monotonicity. The size of ϵ\epsilon is therefore a real modelling decision, not a rounding detail.

There are further costs. The method assumes each component is Gaussian. Within a fixed feature space, approximating strongly skewed or heavy-tailed groups may require extra Gaussian components, which can make interpretation harder. Components are not the same thing as real-world groups: a single natural group with a non-Gaussian shape may be split across two fitted components, and two natural groups may share one. Component labels are arbitrary: permuting the component indices gives exactly the same likelihood, so comparisons across runs require matching components before comparing parameters. Finally, the computational cost grows with the square of the dimension: evaluating the full-covariance densities costs O(nKd2)O(nKd^2) after O(Kd3)O(Kd^3) factorizations, and forming the weighted covariance scatters costs O(nKd2)O(nKd^2). Here nn is the observation count, dd the feature count, and KK the number of components. For a thousand well-populated clusters in high dimensions, k-means remains far cheaper.

Formula

This section derives the mixture density, introduces the latent-variable formulation, and develops the E- and M-step updates.

Mixture densities and latent assignments

The derivation starts with the smallest object we need: a single Gaussian bump. In one dimension, with mean μ\mu, positive standard deviation σ>0\sigma>0 and variance σ2\sigma^2, its density is

N(x∣μ,σ2)=1σ2πexp⁡ ⁣(−(x−μ)22σ2).\mathcal{N}(x \mid \mu, \sigma^2) = \frac{1}{\sigma\sqrt{2\pi}} \exp\!\left(-\frac{(x-\mu)^2}{2\sigma^2}\right).

The fraction in front is not decoration. It is the exact constant that makes the area under the curve equal one, so the result can be treated as a probability density. The exponent measures how far xx sits from μ\mu in units of standard deviations, then squares that distance; the negative sign turns distance into a penalty, so points near the mean get high density and points far away get low density. A single Gaussian therefore encodes one centre and one spread. That is useful, but it cannot describe data with two separated modes.

A mixture removes that limitation by averaging several Gaussian densities together:

p(x∣θ)=∑k=1Kπk N(x∣μk,σk2),p(x \mid \boldsymbol{\theta}) = \sum_{k=1}^{K} \pi_k \, \mathcal{N}(x \mid \mu_k, \sigma_k^2),

where:

  • θ\boldsymbol{\theta}: the full parameter set collecting all means μk\mu_k, variances σk2\sigma_k^2, and mixing weights πk\pi_k
  • πk\pi_k: the mixing weight of component kk, satisfying πk≥0\pi_k \ge 0 and ∑k=1Kπk=1\sum_{k=1}^K \pi_k = 1, so the weights behave like probabilities
  • μk\mu_k: the mean (centre) of component kk
  • σk2\sigma_k^2: the variance of component kk (one-dimensional case; the multivariate case replaces it with a covariance matrix Σk\boldsymbol{\Sigma}_k below)
  • N(x∣μk,σk2)\mathcal{N}(x \mid \mu_k, \sigma_k^2): the Gaussian density of component kk evaluated at xx

The mixing weights describe prior component membership within the model: before seeing an observation's features, πk\pi_k is its chance of coming from component kk. These are mixture probabilities, not a Bayesian prior on the fitted parameters. The sum says that the overall density at xx is a weighted average of the component densities at xx: each component contributes in proportion to how much prior mass it owns.

This weighted sum is also a generative recipe. To produce a point, first choose a component kk with probability πk\pi_k, then draw the point from that component's Gaussian. The recipe contains a hidden step, and naming that step is what makes inference possible. Let zi∈{1,…,K}z_i \in \{1, \dots, K\} record which component produced observation ii. We never observe ziz_i, which is why it is called a latent variable. Writing the model jointly over the observation and its hidden label gives

p(xi,zi=k∣θ)=πk N(xi∣μk,Σk),p(\mathbf{x}_i, z_i = k \mid \boldsymbol{\theta}) = \pi_k \, \mathcal{N}(\mathbf{x}_i \mid \boldsymbol{\mu}_k, \boldsymbol{\Sigma}_k),

and the law of total probability recovers the marginal density we use:

p(xi∣θ)=∑k=1Kp(xi,zi=k∣θ).p(\mathbf{x}_i \mid \boldsymbol{\theta}) = \sum_{k=1}^{K} p(\mathbf{x}_i, z_i = k \mid \boldsymbol{\theta}).

That identity is the reason the latent variable earns its place. The marginal likelihood contains a sum over hidden states, and that sum is exactly what blocks a direct closed-form maximum. If we could condition on ziz_i, the sum would disappear; the latent variable lets us separate the difficult marginal problem into a sequence of easier complete-data problems.

Bayes' rule then turns the generative model into an inference procedure. Given the current parameters, the posterior probability that observation ii belongs to component kk is

γik=p(zi=k∣xi,θ)=πk N(xi∣μk,Σk)∑j=1Kπj N(xi∣μj,Σj).\gamma_{ik} = p(z_i = k \mid \mathbf{x}_i, \boldsymbol{\theta}) = \frac{\pi_k \, \mathcal{N}(\mathbf{x}_i \mid \boldsymbol{\mu}_k, \boldsymbol{\Sigma}_k)}{\sum_{j=1}^{K} \pi_j \, \mathcal{N}(\mathbf{x}_i \mid \boldsymbol{\mu}_j, \boldsymbol{\Sigma}_j)}.

The denominator is the total density at xi\mathbf{x}_i contributed by all components, and the numerator is the piece of that total contributed by component kk. Their ratio is the share of the density that component kk explains, which is precisely a probability. These γik\gamma_{ik} are the soft assignments. They replace the missing labels with graded beliefs instead of forcing each point into one cluster.

Before generalizing, it is worth seeing why the obvious optimization strategy fails. The log-likelihood of nn independent observations is

ℓ(θ)=∑i=1nlog⁡ ⁣(∑k=1Kπk N(xi∣μk,Σk)).\ell(\boldsymbol{\theta}) = \sum_{i=1}^{n} \log\!\left( \sum_{k=1}^{K} \pi_k \, \mathcal{N}(\mathbf{x}_i \mid \boldsymbol{\mu}_k, \boldsymbol{\Sigma}_k) \right).

We take logs because the likelihood itself is a product of nn densities, and products of many small numbers underflow quickly on a computer; logs turn products into sums without moving the maximum, since log⁡\log is strictly increasing. The obstacle is the outer logarithm sitting on top of a sum. Differentiating with respect to μk\boldsymbol{\mu}_k and setting the result to zero produces

∑i=1nγik Σk−1(xi−μk)=0,\sum_{i=1}^{n} \gamma_{ik} \, \boldsymbol{\Sigma}_k^{-1} (\mathbf{x}_i - \boldsymbol{\mu}_k) = \mathbf{0},

because the ratio πkN(xi∣μk,Σk)/∑jπjN(xi∣μj,Σj)\pi_k \mathcal{N}(\mathbf{x}_i \mid \boldsymbol{\mu}_k, \boldsymbol{\Sigma}_k) / \sum_j \pi_j \mathcal{N}(\mathbf{x}_i \mid \boldsymbol{\mu}_j, \boldsymbol{\Sigma}_j) is precisely γik\gamma_{ik}. Solving for the mean gives

μk=∑i=1nγik xi∑i=1nγik.\boldsymbol{\mu}_k = \frac{\sum_{i=1}^{n} \gamma_{ik}\, \mathbf{x}_i}{\sum_{i=1}^{n} \gamma_{ik}}.

This looks like a closed-form answer, but it is not: the responsibilities γik\gamma_{ik} on the right-hand side are themselves functions of the whole parameter set θ\boldsymbol{\theta}, including μk\boldsymbol{\mu}_k. We have a fixed-point equation, not a formula. EM's insight is to freeze the right-hand side at its current value, recompute the left-hand side, and repeat. The E-step supplies the current beliefs about the hidden labels; the M-step turns those beliefs into new parameters.

Now the multivariate case, which is what we will implement. For an observation x∈Rd\mathbf{x} \in \mathbb{R}^d the component density is

N(x∣μ,Σ)=(2π)−d/2∣Σ∣−1/2exp⁡ ⁣(−12(x−μ)⊤Σ−1(x−μ)),\mathcal{N}(\mathbf{x} \mid \boldsymbol{\mu}, \boldsymbol{\Sigma}) = (2\pi)^{-d/2} \lvert \boldsymbol{\Sigma} \rvert^{-1/2} \exp\!\left(-\tfrac{1}{2} (\mathbf{x} - \boldsymbol{\mu})^{\top} \boldsymbol{\Sigma}^{-1} (\mathbf{x} - \boldsymbol{\mu})\right),

where μ\boldsymbol{\mu} is a dd-vector, Σ\boldsymbol{\Sigma} is a d×dd \times d symmetric positive definite covariance matrix, the superscript ⊤\top means transpose (turn the column vector into a row vector so the matrix product is defined), Σ−1\boldsymbol{\Sigma}^{-1} is the matrix inverse, and ∣Σ∣\lvert \boldsymbol{\Sigma} \rvert is the determinant. The quadratic form (x−μ)⊤Σ−1(x−μ)(\mathbf{x}-\boldsymbol{\mu})^{\top}\boldsymbol{\Sigma}^{-1}(\mathbf{x}-\boldsymbol{\mu}) is the squared Mahalanobis distance: it rescales and de-rotates space so that distance is measured in units of the component's own spread. The factor ∣Σ∣−1/2\lvert \boldsymbol{\Sigma} \rvert^{-1/2} does the same job the 1/σ1/\sigma factor did in one dimension: it divides by the volume the Gaussian occupies, so the density integrates to one.

Expectation and maximization steps

EM alternates between two problems, each of which is easy on its own. The first problem is inference: given parameters, what do we believe about the hidden labels? The second is estimation: given beliefs about the labels, what parameters are most likely?

Both problems are phrased in terms of the complete-data log-likelihood, the quantity we would maximize if the labels were observed. With zikz_{ik} a one-hot indicator equal to one when observation ii comes from component kk,

ℓc(θ)=∑i=1n∑k=1Kzik[log⁡πk+log⁡N(xi∣μk,Σk)].\ell_c(\boldsymbol{\theta}) = \sum_{i=1}^{n} \sum_{k=1}^{K} z_{ik} \Big[ \log \pi_k + \log \mathcal{N}(\mathbf{x}_i \mid \boldsymbol{\mu}_k, \boldsymbol{\Sigma}_k) \Big].

where:

  • ℓc(θ)\ell_c(\boldsymbol{\theta}): the complete-data log-likelihood, the log-likelihood we would maximize if the latent labels were observed
  • zikz_{ik}: a one-hot indicator equal to 11 when observation ii came from component kk, and 00 otherwise
  • πk\pi_k: the mixing weight of component kk
  • N(xi∣μk,Σk)\mathcal{N}(\mathbf{x}_i \mid \boldsymbol{\mu}_k, \boldsymbol{\Sigma}_k): the Gaussian density of component kk evaluated at xi\mathbf{x}_i
  • nn: the number of observations, KK: the number of components

Because the indicators select exactly one term per observation, each component's mean and covariance can be fitted from its assigned observations. The mixing weights still share their sum-to-one constraint and are estimated from the component counts. That is the structure the marginal likelihood destroyed.

Since zikz_{ik} is unknown, we replace it with its conditional expectation, which is the responsibility: E[zik∣xi,θ]=γik\mathbb{E}[z_{ik} \mid \mathbf{x}_i, \boldsymbol{\theta}] = \gamma_{ik}. This produces the auxiliary function

Q(θ∣θt)=∑i=1n∑k=1Kγik[log⁡πk+log⁡N(xi∣μk,Σk)],Q(\boldsymbol{\theta} \mid \boldsymbol{\theta}^{t}) = \sum_{i=1}^{n} \sum_{k=1}^{K} \gamma_{ik} \Big[ \log \pi_k + \log \mathcal{N}(\mathbf{x}_i \mid \boldsymbol{\mu}_k, \boldsymbol{\Sigma}_k) \Big],

where θt\boldsymbol{\theta}^{t} denotes the parameters at iteration tt. The E-step consists of computing the γik\gamma_{ik} from the current parameters using Bayes' rule above. Each γik\gamma_{ik} depends on all components through the denominator. Holding responsibilities fixed separates the mean and covariance estimation by component, while the mixing weights retain their shared normalization.

The M-step maximizes QQ over θ\boldsymbol{\theta}. For the means, only the quadratic term depends on μk\boldsymbol{\mu}_k:

log⁡N(xi∣μk,Σk)=−d2log⁡(2π)−12log⁡∣Σk∣−12(xi−μk)⊤Σk−1(xi−μk).\log \mathcal{N}(\mathbf{x}_i \mid \boldsymbol{\mu}_k, \boldsymbol{\Sigma}_k) = -\tfrac{d}{2}\log(2\pi) - \tfrac{1}{2}\log\lvert \boldsymbol{\Sigma}_k \rvert - \tfrac{1}{2}(\mathbf{x}_i - \boldsymbol{\mu}_k)^{\top} \boldsymbol{\Sigma}_k^{-1} (\mathbf{x}_i - \boldsymbol{\mu}_k).

Differentiating the quadratic form with respect to the vector μk\boldsymbol{\mu}_k uses the chain rule on the squared Mahalanobis distance,

∂∂μk(xi−μk)⊤Σk−1(xi−μk)=−2 Σk−1(xi−μk),\frac{\partial}{\partial \boldsymbol{\mu}_k} (\mathbf{x}_i - \boldsymbol{\mu}_k)^{\top} \boldsymbol{\Sigma}_k^{-1} (\mathbf{x}_i - \boldsymbol{\mu}_k) = -2\,\boldsymbol{\Sigma}_k^{-1}(\mathbf{x}_i - \boldsymbol{\mu}_k),

where the minus sign appears because μk\boldsymbol{\mu}_k enters with a negative sign inside the parenthesis. Setting ∂Q/∂μk=0\partial Q / \partial \boldsymbol{\mu}_k = \mathbf{0} gives

∑i=1nγik Σk−1(xi−μk)=0.\sum_{i=1}^{n} \gamma_{ik} \, \boldsymbol{\Sigma}_k^{-1} (\mathbf{x}_i - \boldsymbol{\mu}_k) = \mathbf{0}.

Because Σk−1\boldsymbol{\Sigma}_k^{-1} is invertible, we may multiply both sides on the left by Σk\boldsymbol{\Sigma}_k to remove it, leaving ∑iγik(xi−μk)=0\sum_i \gamma_{ik}(\mathbf{x}_i - \boldsymbol{\mu}_k) = \mathbf{0}. Splitting the sum and solving,

μk=1Nk∑i=1nγik xi,Nk=∑i=1nγik.\boldsymbol{\mu}_k = \frac{1}{N_k} \sum_{i=1}^{n} \gamma_{ik} \, \mathbf{x}_i, \qquad N_k = \sum_{i=1}^{n} \gamma_{ik}.

where:

  • Nk=∑i=1nγikN_k = \sum_{i=1}^{n} \gamma_{ik}: the effective number of observations assigned to component kk, formed by summing the responsibilities
  • γik\gamma_{ik}: the responsibility of component kk for observation ii, its posterior probability of membership
  • xi\mathbf{x}_i: the ii-th observation

The mean update requires Nk>0N_k>0. When a component has zero effective count, this weighted-mean formula is undefined. We can stop with an error, reinitialize or remove the component, or retain its parameters as an inactive zero-weight component and skip its update. Our implementation below stops with an error. The effective count need not be an integer, because responsibilities are fractional: a point with γik=0.3\gamma_{ik} = 0.3 contributes 0.3xi0.3\mathbf{x}_i to the weighted feature sum, then division by NkN_k converts that sum to the mean. This is the soft-count idea in its simplest form.

For the covariance, we need two matrix-calculus facts. First, ∂log⁡∣Σ∣/∂Σ=Σ−1\partial \log\lvert \boldsymbol{\Sigma} \rvert / \partial \boldsymbol{\Sigma} = \boldsymbol{\Sigma}^{-1} for a symmetric positive definite matrix, which is the matrix analogue of ∂log⁡σ2/∂σ2=1/σ2\partial \log \sigma^2 / \partial \sigma^2 = 1/\sigma^2. Second, ∂(a⊤Σ−1a)/∂Σ=−Σ−1aa⊤Σ−1\partial (\mathbf{a}^{\top}\boldsymbol{\Sigma}^{-1}\mathbf{a})/\partial \boldsymbol{\Sigma} = -\boldsymbol{\Sigma}^{-1}\mathbf{a}\mathbf{a}^{\top}\boldsymbol{\Sigma}^{-1}, which follows from differentiating the inverse. Applying both,

∂Q∂Σk=∑i=1nγik[−12Σk−1+12Σk−1(xi−μk)(xi−μk)⊤Σk−1]=0.\frac{\partial Q}{\partial \boldsymbol{\Sigma}_k} = \sum_{i=1}^{n} \gamma_{ik} \left[ -\tfrac{1}{2}\boldsymbol{\Sigma}_k^{-1} + \tfrac{1}{2}\boldsymbol{\Sigma}_k^{-1}(\mathbf{x}_i - \boldsymbol{\mu}_k)(\mathbf{x}_i - \boldsymbol{\mu}_k)^{\top}\boldsymbol{\Sigma}_k^{-1} \right] = \mathbf{0}.

Multiply on the left and right by Σk\boldsymbol{\Sigma}_k (which is legal since it is invertible) and use Nk=∑iγikN_k = \sum_i \gamma_{ik}:

−NkΣk+∑i=1nγik(xi−μk)(xi−μk)⊤=0⟹Σk=1Nk∑i=1nγik(xi−μk)(xi−μk)⊤.-N_k \boldsymbol{\Sigma}_k + \sum_{i=1}^{n} \gamma_{ik}(\mathbf{x}_i - \boldsymbol{\mu}_k)(\mathbf{x}_i - \boldsymbol{\mu}_k)^{\top} = \mathbf{0} \quad\Longrightarrow\quad \boldsymbol{\Sigma}_k = \frac{1}{N_k} \sum_{i=1}^{n} \gamma_{ik}(\mathbf{x}_i - \boldsymbol{\mu}_k)(\mathbf{x}_i - \boldsymbol{\mu}_k)^{\top}.

where:

  • Σk\boldsymbol{\Sigma}_k: the d×dd \times d covariance matrix of component kk
  • γik\gamma_{ik}: the responsibility of component kk for observation ii
  • xi−μk\mathbf{x}_i - \boldsymbol{\mu}_k: the centered observation, its deviation from component kk's mean
  • NkN_k: the effective count for component kk

For this to be a valid nondegenerate Gaussian covariance we need Nk>0N_k>0 and a positive-definite scatter. This is the weighted scatter matrix of component kk, with each observation's outer product (xi−μk)(xi−μk)⊤(\mathbf{x}_i - \boldsymbol{\mu}_k)(\mathbf{x}_i - \boldsymbol{\mu}_k)^{\top} counted γik\gamma_{ik} times. It is the same estimator we would use with known labels, except that the counts are fractional.

The mixing weights carry a constraint, ∑kπk=1\sum_k \pi_k = 1, so we cannot differentiate and set to zero naively; we add the constraint times a Lagrange multiplier λ\lambda and differentiate the augmented expression:

L=∑i=1n∑k=1Kγiklog⁡πk+λ(∑k=1Kπk−1),∂L∂πk=Nkπk+λ=0.\mathcal{L} = \sum_{i=1}^{n}\sum_{k=1}^{K} \gamma_{ik} \log \pi_k + \lambda\left(\sum_{k=1}^{K}\pi_k - 1\right), \qquad \frac{\partial \mathcal{L}}{\partial \pi_k} = \frac{N_k}{\pi_k} + \lambda = 0.

The first term's derivative is Nk/πkN_k/\pi_k because ∂log⁡πk/∂πk=1/πk\partial \log \pi_k/\partial \pi_k = 1/\pi_k, and the Lagrange multiplier λ\lambda is a constant; note that although γik\gamma_{ik} itself depends on πk\pi_k through the Bayes formula, in Q(θ∣θt)Q(\boldsymbol{\theta} \mid \boldsymbol{\theta}^t) the responsibilities are held fixed at their current E-step values, so within this maximization they act as constants on the πk\pi_k derivative. Rearranging gives πk=Nk/(−λ)\pi_k = N_k / (-\lambda). Summing both sides over kk and using ∑kπk=1\sum_k \pi_k = 1 and ∑kNk=∑i∑kγik=n\sum_k N_k = \sum_i \sum_k \gamma_{ik} = n (each observation's responsibilities sum to one),

1=∑k=1Kπk=∑k=1KNk−λ=n−λ⟹−λ=n⟹πk=Nkn.1 = \sum_{k=1}^{K}\pi_k = \frac{\sum_{k=1}^{K} N_k}{-\lambda} = \frac{n}{-\lambda} \quad\Longrightarrow\quad -\lambda = n \quad\Longrightarrow\quad \pi_k = \frac{N_k}{n}.

where:

  • πk\pi_k: the mixing weight of component kk
  • NkN_k: the effective count for component kk
  • nn: the number of observations
  • λ\lambda: the Lagrange multiplier enforcing the constraint ∑kπk=1\sum_k \pi_k = 1

So each mixing weight is just the share of the effective count that component kk holds.

Collecting everything, one full EM iteration is:

γik=πkN(xi∣μk,Σk)∑jπjN(xi∣μj,Σj)(E-step)\gamma_{ik} = \frac{\pi_k \mathcal{N}(\mathbf{x}_i \mid \boldsymbol{\mu}_k, \boldsymbol{\Sigma}_k)}{\sum_j \pi_j \mathcal{N}(\mathbf{x}_i \mid \boldsymbol{\mu}_j, \boldsymbol{\Sigma}_j)} \qquad\text{(E-step)} Nk=∑iγik,μk=1Nk∑iγikxi,Σk=1Nk∑iγik(xi−μk)(xi−μk)⊤,πk=Nkn(M-step)N_k = \sum_i \gamma_{ik}, \qquad \boldsymbol{\mu}_k = \frac{1}{N_k}\sum_i \gamma_{ik}\mathbf{x}_i, \qquad \boldsymbol{\Sigma}_k = \frac{1}{N_k}\sum_i \gamma_{ik}(\mathbf{x}_i - \boldsymbol{\mu}_k)(\mathbf{x}_i - \boldsymbol{\mu}_k)^{\top}, \qquad \pi_k = \frac{N_k}{n} \qquad\text{(M-step)}

The matrix form of the M-step makes the computation obvious. Gather the responsibilities into an n×Kn \times K matrix R\mathbf{R} whose entry (i,k)(i,k) is γik\gamma_{ik}, and the data into an n×dn \times d matrix X\mathbf{X} whose rows are the observations. Let 1n\mathbf{1}_n be a column vector of ones and n=R⊤1n\mathbf{n} = \mathbf{R}^{\top}\mathbf{1}_n the column vector of effective counts. Then

M=diag⁡(n)−1R⊤X,Σk=1nk(X−1nμk⊤)⊤diag⁡(rk)(X−1nμk⊤),\mathbf{M} = \operatorname{diag}(\mathbf{n})^{-1} \mathbf{R}^{\top}\mathbf{X}, \qquad \boldsymbol{\Sigma}_k = \frac{1}{n_k} (\mathbf{X} - \mathbf{1}_n \boldsymbol{\mu}_k^{\top})^{\top} \operatorname{diag}(\mathbf{r}_k) (\mathbf{X} - \mathbf{1}_n \boldsymbol{\mu}_k^{\top}),

where:

  • R\mathbf{R}: the n×Kn \times K responsibility matrix with entry (i,k)=γik(i,k) = \gamma_{ik}
  • X\mathbf{X}: the n×dn \times d data matrix whose rows are the observations
  • 1n\mathbf{1}_n: a column vector of ones of length nn
  • n=R⊤1n\mathbf{n} = \mathbf{R}^{\top}\mathbf{1}_n: the column vector of effective counts whose kk-th entry is NkN_k
  • nkn_k: the kk-th entry of n\mathbf{n} (same as NkN_k), the effective count of component kk
  • rk\mathbf{r}_k: the kk-th column of R\mathbf{R} (the responsibilities for component kk)
  • M\mathbf{M}: the K×dK \times d matrix whose kk-th row is μk⊤\boldsymbol{\mu}_k^{\top}
  • μk\boldsymbol{\mu}_k: the mean of component kk
  • diag⁡(⋅)\operatorname{diag}(\cdot): turns a vector into a diagonal matrix

Multiplying R⊤\mathbf{R}^{\top} by X\mathbf{X} sums the rows of X\mathbf{X} weighted by the responsibilities (each column accumulates a weighted sum), and dividing by n\mathbf{n} turns those weighted sums into weighted averages, giving the K×dK \times d matrix M\mathbf{M} whose kk-th row is μk⊤\boldsymbol{\mu}_k^{\top}. The covariance expression is the same operation applied to the centered data: subtracting 1nμk⊤\mathbf{1}_n\boldsymbol{\mu}_k^{\top} centers every row, the diagonal matrix reweights the rows, and the product A⊤WA\mathbf{A}^{\top}\mathbf{W}\mathbf{A} accumulates the weighted outer products. In code this is a handful of matrix multiplications rather than nn nested loops, which matters because the E-step is repeated until convergence.

The reason EM works at all is that it is coordinate ascent on a lower bound. For a probability distribution q(z)q(z) that is positive on the joint model's positive support, multiplying and dividing inside the log by q(z)q(z) and applying Jensen's inequality (which says that for a concave function like log⁡\log, the average of the function is at most the function of the average) gives

log⁡p(x∣θ)=log⁡∑zq(z)p(x,z∣θ)q(z)  ≥  ∑zq(z)log⁡p(x,z∣θ)q(z).\log p(\mathbf{x}\mid\boldsymbol{\theta}) = \log \sum_{z} q(z)\frac{p(\mathbf{x}, z \mid \boldsymbol{\theta})}{q(z)} \;\ge\; \sum_{z} q(z) \log \frac{p(\mathbf{x}, z \mid \boldsymbol{\theta})}{q(z)}.

The gap between the two sides is the Kullback-Leibler divergence from qq to the posterior p(z∣x,θ)p(z \mid \mathbf{x}, \boldsymbol{\theta}), which is non-negative and equals zero exactly when qq is the posterior. The E-step sets qq to that posterior, closing the gap so the bound touches the log-likelihood at the current parameters. The M-step then pushes the bound up by maximizing it over θ\boldsymbol{\theta}. Since the log-likelihood is at least the bound, and the bound has increased, the log-likelihood cannot decrease. This proves likelihood nondecrease for the exact bound-increasing updates. It does not prove parameter convergence, a finite maximum, or a global optimum, and it does not apply automatically after an arbitrary covariance modification.

Mathematical Properties

A few properties follow directly from the derivation and shape how the method is used.

  • Monotone under exact updates. With valid parameters and an exact E-step followed by a bound-increasing M-step, ℓ(θt+1)≥ℓ(θt)\ell(\boldsymbol{\theta}^{t+1}) \ge \ell(\boldsymbol{\theta}^{t}). The likelihood is non-concave; finite interior limits under suitable regularity are stationary, and they need not be global maxima. Singular boundary behaviour remains possible.
  • Unbounded above for unrestricted mixtures with K≥2K\ge2. A component can collapse onto one observation while another covers the remaining points, driving the sample likelihood to +∞+\infty. A single Gaussian whose empirical centered covariance is positive definite does have a finite maximum-likelihood fit; the collapse claim is not a statement about every Gaussian model.
  • Label switching. Any permutation of the component indices leaves the likelihood unchanged, so its component labels are arbitrary. Comparing parameter estimates across runs requires matching components first.
  • Affine transformation of full-covariance models. For x′=Ax+b\mathbf{x}'=A\mathbf{x}+\mathbf{b} with invertible AA, transform each mean to Aμk+bA\boldsymbol{\mu}_k+\mathbf{b} and covariance to AΣkA⊤A\boldsymbol{\Sigma}_k A^{\top}. The density divides by ∣det⁡A∣|\det A|, so responsibilities remain unchanged and total log-likelihood shifts by −nlog⁡∣det⁡A∣-n\log|\det A|. This relates corresponding unrestricted fits, not arbitrary numerical runs with fixed regularization. Diagonal models are closed under axis scaling but not general rotations; spherical models permit common uniform scaling and rotations, not arbitrary axis scaling.
  • Asymptotic relation to k-means. For fixed observations and centroids, suppose all components have equal fixed weights and common spherical covariance σ2I\sigma^2 \mathbf{I}. As the externally specified common variance tends to zero, responsibilities at observations with a unique nearest centroid approach nearest-centroid indicators. This varies a prescribed variance across models rather than estimating it in the M-step. For each component with a positive limiting hard-assignment count, its mean update approaches the k-means update. Empty limiting clusters require a separate handling rule. This is the small-variance connection between the mixture and k-means.
  • Parameter counts. A full-covariance mixture has K(d+d(d+1)/2)+(K−1)K\big(d + d(d+1)/2\big) + (K-1) free parameters: KdKd means, KK covariance matrices with d(d+1)/2d(d+1)/2 distinct entries each, and K−1K-1 free mixing weights. Diagonal covariances cost 2Kd+(K−1)2Kd + (K-1), spherical ones cost K(d+1)+(K−1)K(d+1) + (K-1), and a tied covariance uses Kd+d(d+1)/2+(K−1)Kd+d(d+1)/2+(K-1) parameters by sharing one covariance across components.

Example

We can carry a full EM iteration by hand on four one-dimensional observations, x={1.0, 2.5, 4.0, 6.0}x = \{1.0,\ 2.5,\ 4.0,\ 6.0\}, with two components. To keep the arithmetic transparent we fix the variances at σ12=σ22=1.52=2.25\sigma_1^2 = \sigma_2^2 = 1.5^2 = 2.25 and the weights at π1=π2=0.5\pi_1 = \pi_2 = 0.5 for the first E-step; the M-step then updates everything.

The normal density at xx is 1σ2πexp⁡ ⁣(−(x−μ)22σ2)\frac{1}{\sigma\sqrt{2\pi}}\exp\!\left(-\frac{(x-\mu)^2}{2\sigma^2}\right), where μ\mu is the mean and σ2\sigma^2 is the variance, here μ1=2.0\mu_1 = 2.0 and μ2=5.0\mu_2 = 5.0 with σ=1.5\sigma = 1.5. With σ=1.5\sigma = 1.5 the leading constant is 11.52π≈0.26596\frac{1}{1.5\sqrt{2\pi}} \approx 0.26596. Starting means are μ1=2.0\mu_1 = 2.0 and μ2=5.0\mu_2 = 5.0. For x=2.5x = 2.5 the squared distances are (2.5−2)2=0.25(2.5-2)^2 = 0.25 and (2.5−5)2=6.25(2.5-5)^2 = 6.25; dividing by 2σ2=4.52\sigma^2 = 4.5 gives approximately 0.05560.0556 and 1.38891.3889. Evaluating the densities with the unrounded constant and exponents gives approximately 0.251590.25159 and 0.066320.06632. The responsibilities follow from the ratio:

γ2,1=0.5×0.251590.5×0.25159+0.5×0.06632=0.251590.31791=0.7914,γ2,2=1−0.7914=0.2086.\gamma_{2,1} = \frac{0.5 \times 0.25159}{0.5 \times 0.25159 + 0.5 \times 0.06632} = \frac{0.25159}{0.31791} = 0.7914, \qquad \gamma_{2,2} = 1 - 0.7914 = 0.2086.

Repeating for all four points gives the E-step below. The two outer points are almost certain, while x=4.0x = 4.0 has a mixed posterior assignment, sitting at γ3,1=0.3392\gamma_{3,1} = 0.3392 (that is, γi1\gamma_{i1} is close to but below one half, so component 2 is slightly favoured). That contested point is useful because a nearest-centroid method still assigns it deterministically, while the mixture retains both possibilities under its fitted probability model.

Responsibilities for the four-point example after the first E-step.
xix_iN(xi∣μ1,2.25)\mathcal{N}(x_i \mid \mu_1, 2.25)N(xi∣μ2,2.25)\mathcal{N}(x_i \mid \mu_2, 2.25)γi1\gamma_{i1}γi2\gamma_{i2}
1.00.212970.007600.96560.0344
2.50.251590.066320.79140.2086
4.00.109340.212970.33920.6608
6.00.007600.212970.03440.9656

The log-likelihood at this parameter value is the sum of the logs of the mixture densities. For x=1.0x = 1.0 the mixture density is π1N(1.0∣μ1,2.25)+π2N(1.0∣μ2,2.25)≈0.11028133\pi_1 \mathcal{N}(1.0 \mid \mu_1, 2.25) + \pi_2 \mathcal{N}(1.0 \mid \mu_2, 2.25) \approx 0.11028133 using unrounded densities, whose natural logarithm is approximately −2.20472063-2.20472063. Summing the four contributions gives ℓ(θ0)=−8.074\ell(\boldsymbol{\theta}^{0}) = -8.074. That is our baseline. The exact M-step cannot lower it; equality is possible at a stationary configuration. In this example we can check the strict increase numerically.

Now the M-step. Effective counts are the column sums: N1=0.9656+0.7914+0.3392+0.0344=2.1306N_1 = 0.9656 + 0.7914 + 0.3392 + 0.0344 = 2.1306 and N2=4−2.1306=1.8694N_2 = 4 - 2.1306 = 1.8694. These are not integer counts. They are the total fractional membership each component has earned and provide the denominators for the M-step's weighted averages. The new mean of component 1 is the responsibility-weighted average of the observations,

μ1=0.9656(1.0)+0.7914(2.5)+0.3392(4.0)+0.0344(6.0)2.1306=4.50732.1306≈2.116,\mu_1 = \frac{0.9656(1.0) + 0.7914(2.5) + 0.3392(4.0) + 0.0344(6.0)}{2.1306} = \frac{4.5073}{2.1306} \approx 2.116,

and similarly μ2=(13.5−4.5073)/1.8694=4.810\mu_2 = (13.5 - 4.5073)/1.8694 = 4.810, where we used the fact that the two weighted sums must add to the total ∑ixi=13.5\sum_i x_i = 13.5. The variances use the same weights applied to squared deviations:

σ12=0.9656(1.0−2.116)2+0.7914(2.5−2.116)2+0.3392(4.0−2.116)2+0.0344(6.0−2.116)22.1306=1.428,\sigma_1^2 = \frac{0.9656(1.0 - 2.116)^2 + 0.7914(2.5-2.116)^2 + 0.3392(4.0-2.116)^2 + 0.0344(6.0-2.116)^2}{2.1306} = 1.428,

giving σ1=1.195\sigma_1 = 1.195, and the same calculation for component 2 gives σ22=1.826\sigma_2^2 = 1.826, so σ2=1.351\sigma_2 = 1.351. The weights become π1=2.1306/4=0.533\pi_1 = 2.1306/4 = 0.533 and π2=0.467\pi_2 = 0.467.

The test of the whole procedure is whether the likelihood improved. Recomputing the mixture density at the new parameters gives ℓ(θ1)=−7.897\ell(\boldsymbol{\theta}^{1}) = -7.897, which is larger than −8.074-8.074. The log-likelihood increases by about 0.1770.177 in this step, consistent with the nondecrease guarantee for this exact update. The contested point at x=4.0x = 4.0 has also moved: its responsibility for component 2 increases from about 0.6610.661 to 0.6920.692. Both means move closer to this point. The posterior change depends on the updated weights and variances as well as the means; distances to the means alone do not determine it.

The cell below reproduces the hand calculation with the same formulas, so we can confirm the arithmetic and inspect the responsibilities directly.

PythonIn [3]
CollapseExpand
x_ex = np.array([1.0, 2.5, 4.0, 6.0])
mu_ex = np.array([2.0, 5.0])
var_ex = np.array([2.25, 2.25])
pi_ex = np.array([0.5, 0.5])

# log component density for every (point, component) pair
log_dens = -0.5 * np.log(2 * np.pi * var_ex)[None, :] - (
    x_ex[:, None] - mu_ex[None, :]
) ** 2 / (2 * var_ex[None, :])
log_joint = log_dens + np.log(pi_ex)[None, :]
ll_ex = logsumexp(log_joint, axis=1)
resp_ex = np.exp(log_joint - ll_ex[:, None])
ConsoleOut [4]
CollapseExpand
responsibilities (rows = observations):
[[0.9656 0.0344]
 [0.7914 0.2086]
 [0.3392 0.6608]
 [0.0344 0.9656]]
total log-likelihood before the M-step: -8.074
PythonIn [5]
CollapseExpand
# one M-step
Nk = resp_ex.sum(axis=0)
mu_new = (resp_ex * x_ex[:, None]).sum(axis=0) / Nk
var_new = (resp_ex * (x_ex[:, None] - mu_new) ** 2).sum(axis=0) / Nk
pi_new = Nk / x_ex.size

log_dens_new = -0.5 * np.log(2 * np.pi * var_new)[None, :] - (
    x_ex[:, None] - mu_new[None, :]
) ** 2 / (2 * var_new[None, :])
ll_new = logsumexp(log_dens_new + np.log(pi_new)[None, :], axis=1).sum()
ConsoleOut [6]
CollapseExpand
effective counts:        [2.1306 1.8694]
updated means:           [2.116 4.81 ]
updated variances:       [1.428 1.826]
updated weights:         [0.533 0.467]
log-likelihood after:    -7.897
improvement:             +0.177

The printed numbers agree with the rounded hand calculation to three decimal places, and the log-likelihood strictly increased. Further exact EM steps cannot lower it, but that alone does not guarantee a finite limiting fit. Our later implementation uses a small-change stopping rule and explicitly checks for a material likelihood decrease. The example therefore demonstrates the full rhythm of EM: infer soft labels, turn them into weighted estimates, and check that the objective moved in the promised direction.

Implementation in NumPy and SciPy

We now build the algorithm from primitives, keeping every step visible. We use NumPy for a Cholesky factorization (to evaluate multivariate normal densities without explicitly forming an inverse) and logsumexp from SciPy (to compute the normalizing constant of the responsibilities in a numerically safe way). The linear algebra functions np.linalg.cholesky and np.linalg.solve are standard NumPy.

PythonIn [7]
CollapseExpand
def log_normal_pdf(X, mean, cov):
    """Log density of a multivariate normal, evaluated row-wise on X."""
    n, d = X.shape
    L = np.linalg.cholesky(cov)  # cov = L @ L.T
    diff = X - mean  # (n, d)
    sol = np.linalg.solve(L, diff.T)  # (d, n), solves L @ w = diff_i
    maha = np.sum(sol**2, axis=0)  # squared Mahalanobis distance
    log_det = 2.0 * np.sum(
        np.log(np.diag(L))
    )  # log|cov| from the Cholesky factor
    return -0.5 * (d * np.log(2.0 * np.pi) + log_det + maha)


def log_component_density(X, weights, means, covs, reg_covar):
    """log(pi_k) + log N(x_i | mu_k, Sigma_k) for every point and component."""
    n, d = X.shape
    K = means.shape[0]
    out = np.empty((n, K))
    for k in range(K):
        cov = covs[k] + reg_covar * np.eye(d)  # regularize before factorizing
        out[:, k] = np.log(weights[k]) + log_normal_pdf(X, means[k], cov)
    return out


def gmm_e_step(X, weights, means, covs, reg_covar):
    """Return responsibilities and the total log-likelihood."""
    log_prob = log_component_density(X, weights, means, covs, reg_covar)
    log_norm = logsumexp(
        log_prob, axis=1
    )  # log of the mixture density per point
    if not np.isfinite(log_norm).all():
        raise FloatingPointError(
            "Non-finite log density; inspect scale, weights and covariances."
        )
    resp = np.exp(log_prob - log_norm[:, None])
    return resp, float(log_norm.sum())


def gmm_m_step(X, resp):
    """Weighted mean, weighted scatter, and weight updates."""
    n, d = X.shape
    K = resp.shape[1]
    Nk = resp.sum(axis=0)  # effective counts
    if np.any(Nk <= np.finfo(float).tiny):
        raise FloatingPointError(
            "A component lost its effective weight; reinitialize or reduce K."
        )
    means = (resp.T @ X) / Nk[:, None]  # weighted averages
    covs = np.empty((K, d, d))
    for k in range(K):
        diff = X - means[k]
        covs[k] = (
            (resp[:, [k]] * diff).T @ diff / Nk[k]
        )  # weighted outer products
    weights = Nk / n
    return weights, means, covs

The log_normal_pdf function is worth reading line by line. Factorizing Σ=LL⊤\boldsymbol{\Sigma} = \mathbf{L}\mathbf{L}^{\top} avoids explicitly computing its inverse, and it gives both quantities we need at once: log⁡∣Σ∣=2∑ilog⁡Lii\log\lvert\boldsymbol{\Sigma}\rvert = 2\sum_i \log L_{ii}, and the squared Mahalanobis distance as ∥L−1(x−μ)∥2\lVert \mathbf{L}^{-1}(\mathbf{x} - \boldsymbol{\mu}) \rVert^2, which we obtain by solving the triangular system Lw=x−μ\mathbf{L}\mathbf{w} = \mathbf{x} - \boldsymbol{\mu} for every point at once. The transpose inside np.linalg.solve exists because solve expects the right-hand side as columns; each column of sol is one point's whitened residual.

PythonIn [8]
CollapseExpand
def gmm_init(X, K, rng, method="kmeans", reg_covar=1e-6):
    """Initial parameters from k-means, or from random observations."""
    n, d = X.shape
    if method == "kmeans":
        km = KMeans(
            n_clusters=K, n_init=10, random_state=int(rng.integers(1_000_000))
        )
        labels = km.fit_predict(X)
        means = km.cluster_centers_.copy()
    else:
        means = X[rng.choice(n, size=K, replace=False)].copy()
        dist = ((X[:, None, :] - means[None, :, :]) ** 2).sum(axis=2)
        labels = dist.argmin(axis=1)

    counts = np.bincount(labels, minlength=K).astype(float)
    weights = (
        np.maximum(counts, 1.0) / n
    )  # floor avoids log(0) for empty clusters
    weights = weights / weights.sum()

    global_cov = np.atleast_2d(np.cov(X.T, bias=True)) + reg_covar * np.eye(d)
    covs = np.empty((K, d, d))
    for k in range(K):
        mask = labels == k
        if mask.sum() > d:
            covs[k] = np.atleast_2d(np.cov(X[mask].T, bias=True))
        else:
            covs[k] = global_cov.copy()
    return weights, means, covs


def fit_gmm(X, K, init="kmeans", n_iter=300, tol=1e-6, reg_covar=1e-6, seed=0):
    """Alternate E and M steps until the log-likelihood stops improving."""
    X = np.asarray(X, dtype=float)
    if X.ndim != 2 or not X.shape[1] or not np.isfinite(X).all():
        raise ValueError("X must be a finite two-dimensional feature matrix.")
    if (
        not isinstance(K, (int, np.integer))
        or isinstance(K, bool)
        or not 1 <= K <= len(X)
    ):
        raise ValueError(
            "K must be an integer between one and the number of rows."
        )
    if (
        not isinstance(n_iter, (int, np.integer))
        or isinstance(n_iter, bool)
        or n_iter < 1
    ):
        raise ValueError("n_iter must be a positive integer.")
    if (
        init not in {"kmeans", "random"}
        or not np.isfinite([tol, reg_covar]).all()
        or tol <= 0
        or reg_covar <= 0
    ):
        raise ValueError(
            "Use a supported initialization and finite positive tolerance and covariance floor."
        )
    rng = np.random.default_rng(seed)
    weights, means, covs = gmm_init(X, K, rng, method=init, reg_covar=reg_covar)
    history, prev_ll, converged = [], -np.inf, False
    for _ in range(n_iter):
        resp, ll = gmm_e_step(X, weights, means, covs, reg_covar)
        history.append(ll)
        if np.isfinite(prev_ll):
            gain = ll - prev_ll
            if gain < -1e-8 * (1.0 + abs(prev_ll)):
                raise FloatingPointError(
                    "Log-likelihood decreased materially; inspect regularization and initialization."
                )
            if abs(gain) < tol:
                converged = True
                break
        prev_ll = ll
        weights, means, covs = gmm_m_step(X, resp)
    if not converged:  # the loop ran out of iterations
        resp, ll = gmm_e_step(X, weights, means, covs, reg_covar)
        if ll - prev_ll < -1e-8 * (1.0 + abs(prev_ll)):
            raise FloatingPointError(
                "Log-likelihood decreased materially; inspect regularization and initialization."
            )
        history.append(ll)
    return {
        "weights": weights,
        "means": means,
        "covs": covs,
        "resp": resp,
        "log_likelihood": ll,
        "history": np.array(history),
        "n_iter": len(history),
        "converged": converged,
        "reg_covar": reg_covar,
    }

The convergence test uses the absolute change in total log-likelihood, not a relative change and not distance to an optimum. A material decrease raises an error instead of being labelled convergence; changes within the stated roundoff allowance can still be small. The diagonal-addition update is a practical safeguard, not the exact unconstrained M-step proved earlier. A small change or an exhausted iteration budget does not certify a stationary point or a global optimum.

Now we generate two Gaussian components with different spreads and orientations. Their overlap makes component membership uncertain from the observed features alone, so we can compare hard assignments with posterior responsibilities.

PythonIn [9]
CollapseExpand
rng = np.random.default_rng(7)


def rotated_cov(sd, angle_deg):
    """Covariance matrix with given axis standard deviations, rotated by angle."""
    t = np.deg2rad(angle_deg)
    R = np.array([[np.cos(t), -np.sin(t)], [np.sin(t), np.cos(t)]])
    return R @ np.diag(np.square(sd)) @ R.T


true_means = np.array([[1.6, 1.2], [4.1, 2.4]])
true_covs = np.stack(
    [rotated_cov([1.55, 0.55], 35.0), rotated_cov([1.25, 0.85], -25.0)]
)
true_weights = np.array([0.55, 0.45])

n_obs = 400
z_true = rng.choice(2, size=n_obs, p=true_weights)
X = np.empty((n_obs, 2))
for i in range(n_obs):
    X[i] = rng.multivariate_normal(true_means[z_true[i]], true_covs[z_true[i]])

x_lo, x_hi = X[:, 0].min() - 0.6, X[:, 0].max() + 0.6
y_lo, y_hi = X[:, 1].min() - 0.6, X[:, 1].max() + 0.6
PythonIn [10]
CollapseExpand
def fit_gmm_best(X, K, n_init=5, **kwargs):
    """Run EM from several starting points and keep the best log-likelihood."""
    if (
        not isinstance(n_init, (int, np.integer))
        or isinstance(n_init, bool)
        or n_init < 1
    ):
        raise ValueError("n_init must be a positive integer.")
    runs = [fit_gmm(X, K, seed=s, **kwargs) for s in range(n_init)]
    return max(runs, key=lambda m: m["log_likelihood"]), runs


model, runs = fit_gmm_best(X, 2, n_init=5, reg_covar=1e-6)
ConsoleOut [11]
CollapseExpand
converged: True, iterations: 46
log-likelihood: -1231.566
weights: [0.535 0.465]
means:
[[1.865 1.455]
 [4.224 2.276]]
covariances:
[[[ 2.174  1.161]
  [ 1.161  0.931]]

 [[ 1.227 -0.348]
  [-0.348  0.784]]]

The recovered means sit close to the generating values, and the covariance matrices show the differing orientations we built in. Sort the components by their first coordinate before printing if you want to compare runs directly, since the fitting order is arbitrary.

To summarize ambiguity under the fitted model, we can count the observations whose responsibility falls in the middle of the range. K-means still assigns these points by their nearest centroid; it does not return a posterior ambiguity measure.

PythonIn [12]
CollapseExpand
amb = ((model["resp"][:, 0] > 0.3) & (model["resp"][:, 0] < 0.7)).sum()
km = KMeans(n_clusters=2, n_init=10, random_state=0).fit(X)
ours = model["resp"].argmax(axis=1)
agreement = max(np.mean(ours == km.labels_), np.mean(ours != km.labels_))
ConsoleOut [13]
CollapseExpand
observations with responsibility between 0.3 and 0.7: 91 of 400
hard-label agreement between GMM and k-means: 0.863

The printed agreement summarizes the two hard partitions after allowing their binary labels to swap. It does not measure accuracy against the generating labels, and agreement alone does not locate the disagreements; the plots let us inspect their geometry.

A reference implementation is the natural cross-check. Scikit-learn's GaussianMixture also uses posterior responsibilities and diagonal covariance regularization. We compare log densities on the same coordinates, but initialization, stopping conventions and covariance bookkeeping can differ; close results are a useful check rather than a guarantee.

PythonIn [14]
CollapseExpand
sk_model = GaussianMixture(
    n_components=2,
    covariance_type="full",
    reg_covar=1e-6,
    n_init=5,
    max_iter=300,
    tol=1e-9,
    random_state=0,
).fit(X)
sk_ll = sk_model.score(X) * len(X)
sk_labels = sk_model.predict(X)
sk_agree = max(np.mean(ours == sk_labels), np.mean(ours != sk_labels))
ConsoleOut [15]
CollapseExpand
our log-likelihood:      -1231.566
sklearn log-likelihood:  -1231.566
assignment agreement:    1.000
sklearn BIC:             2529.039

Small differences are expected: the two implementations initialize differently, stop at slightly different points, and handle the regularization term in slightly different places. A large gap could reflect different local fits, stopping behaviour, regularization, or an implementation error, which is a reminder to use several restarts whichever library is in play.

Stable implementation and initialization

The formulas above are mathematically correct and numerically fragile if taken literally. Two things must be changed before they will survive real data.

The first is the responsibility calculation. Forming πkN(xi∣μk,Σk)\pi_k \mathcal{N}(\mathbf{x}_i \mid \boldsymbol{\mu}_k, \boldsymbol{\Sigma}_k) directly and then dividing produces underflow for points far from every component: an exponential of −800-800 is exactly zero in double precision, and dividing zero by zero yields nan. The fix is to compute everything in log space and use the log-sum-exp identity,

log⁡∑j=1Kexp⁡(aj)=log⁡ ⁣(em∑j=1Kexp⁡(aj−m))(collect the common factor)=m+log⁡∑j=1Kexp⁡(aj−m)(use log⁡(em⋅s)=m+log⁡s)\begin{aligned} \log \sum_{j=1}^{K} \exp(a_j) &= \log\!\left( e^{m} \sum_{j=1}^{K} \exp(a_j - m) \right) && \text{(collect the common factor)} \\ &= m + \log \sum_{j=1}^{K} \exp(a_j - m) && \text{(use } \log(e^m \cdot s) = m + \log s \text{)} \end{aligned}

where aja_j are finite real log-joint values and m=max⁡jajm = \max_j a_j.

Factoring out the largest term is a legal algebraic rearrangement: we divide each term by eme^m and multiply the total back by eme^m. It guarantees that the largest exponent is zero, so no term overflows and at least one term is exactly 11. The responsibility then becomes exp⁡(ak−logsumexp(a))\exp(a_k - \text{logsumexp}(\mathbf{a})), which is always between zero and one even when the raw densities are unrepresentable. SciPy's logsumexp supplies this stable finite-input calculation. An all-infinite or nan input still needs explicit handling; the identity does not make invalid model parameters meaningful.

The second issue is covariance singularity. A weighted scatter is positive definite when its positive-weight observations span all dd feature directions after centring. For a hard-assigned component that requires at least d+1d+1 affinely independent points. The responsibility sum NkN_k is not a rank test: many tiny positive weights can still give a full-rank scatter. Adding ϵI\epsilon\mathbf{I} with ϵ>0\epsilon>0 bounds the eigenvalues away from zero and makes factorization possible. It also bounds the density of these regularized fits, although adding it after an unconstrained M-step does not guarantee ordinary-likelihood monotonicity.

Initialization deserves as much care as the numerics. Exact bound-increasing updates guarantee likelihood nondecrease, not global optimality. Choose starting values and a restart budget together:

  • Random means chosen from the data are simple; some runs land in poor fits.
  • K-means partitioning often gives a useful structured start. Fit k-means, use its centroids as the means, its labels to compute initial covariances, and its cluster proportions as initial weights. It can still miss an elongated or overlapping component.
  • Multiple restarts with the best final likelihood retained are the pragmatic default; n_init controls the number of starts in scikit-learn.

A useful diagnostic is to track the log-likelihood of every restart: if converged runs have substantially different final values, initialization matters. If some runs have not converged, more iterations may explain part of the spread.

Two further safeguards are worth knowing. Component weights can collapse toward zero when a component becomes responsible for almost nothing, which can reflect a redundant component or a poor initialization but produces log⁡πk→−∞\log \pi_k \to -\infty if it reaches exactly zero; flooring the weights at a small positive value keeps the arithmetic finite. And the convergence threshold should be applied to the log-likelihood, not to the parameters, since parameters can drift while the likelihood is flat along a ridge.

Key Parameters

The parameters of a Gaussian mixture fit control capacity, shape, and computational effort, and each has a practical consequence.

  • n_components (KK): the number of mixture components. The best attainable fit in nested model families cannot worsen as components are added, but finite EM runs may return worse local fits. Training likelihood alone does not penalize complexity.
  • covariance_type: "full" gives each component its own unrestricted matrix (Kd(d+1)/2K d(d+1)/2 covariance parameters), "tied" shares one matrix across components, "diag" restricts each component to axis-aligned spreads, and "spherical" gives each a single variance. Constraining covariance is the main defence against overfitting when the data are limited, and a shared spherical variance with equal weights has the nearest-centroid hard-assignment rule. Finite-variance soft EM remains a different procedure.
  • reg_covar: the ϵ\epsilon added to the diagonal. Scikit-learn defaults to 10−610^{-6}, the starting value used in this example. Inspect that setting relative to feature scales and degeneracy warnings; a larger value may be needed for an unstable fit.
  • init_params and n_init: how the starting point is chosen and how many restarts are attempted. K-means initialization with several restarts is the sensible default.
  • max_iter and tol: the iteration cap and the stopping threshold on the log-likelihood. Scikit-learn applies tol to the average lower-bound gain, whereas our teaching implementation checks total log-likelihood change. Inspect the convergence flag and increase the budget if it is exhausted.
  • weights_init, means_init, precisions_init: optional user-supplied starting values, useful when there is prior knowledge about where the components should be, or when a specific initialization must be reproduced.

Key Methods

Once fitted, a GaussianMixture object exposes the different ways of using the model, and the distinction between them matters.

  • fit(X) estimates the parameters; fit_predict(X) additionally returns the hard labels.
  • predict(X) returns the component index with the highest responsibility, which is the hard clustering. It discards the uncertainty that the model went to the trouble of computing.
  • predict_proba(X) returns the full responsibility matrix, one row per observation and one column per component. This is usually the more useful output.
  • score_samples(X) returns the log mixture density at each observation, which is the natural anomaly score.
  • score(X) returns the average log density per observation, the quantity we compared against our own implementation after multiplying by nn.
  • bic(X) and aic(X) return penalized likelihood criteria that can be compared across different values of n_components, since a lower value indicates a better trade-off between fit and complexity.
  • sample(n_samples) draws new observations from the fitted density, which is the generative use of the model.

The distinction between predict and predict_proba is central to using the model in practice: the first produces hard GMM labels, which can differ from k-means labels, while the second gives the information that made the mixture model worth fitting.

Visualizing Gaussian Mixture Models

We begin with the data as generated, coloured by the true component. In a real problem those colours would be unavailable (the whole difficulty is that they are), but seeing them establishes what the model is trying to recover.

VisualizationOut [16]
CollapseExpand
Scatter plot of two clusters on a plane, the first a wide tilted ellipse running lower-left to upper-right, the second a smaller tilted ellipse overlapping it.
Four hundred observations drawn from two Gaussian components with different weights, sizes, and orientations. Colours show the generating component, which is unobserved in practice; the two clouds overlap in the middle with axes pointing in different directions.

The two clouds overlap in the middle and their axes point in different directions, so no single distance threshold separates them cleanly. A circular cluster boundary is visibly the wrong shape for either cloud.

The fitted model replaces the truth with a density. The contours below show the total mixture density; the dashed contours show how each component alone would be shaped.

PythonIn [17]
CollapseExpand
xx, yy = np.meshgrid(np.linspace(x_lo, x_hi, 300), np.linspace(y_lo, y_hi, 300))
grid = np.column_stack([xx.ravel(), yy.ravel()])
log_joint_grid = log_component_density(
    grid, model["weights"], model["means"], model["covs"], model["reg_covar"]
)
dens = np.exp(logsumexp(log_joint_grid, axis=1)).reshape(xx.shape)
levels = np.exp(
    np.linspace(np.log(dens.max() * 0.02), np.log(dens.max() * 0.9), 8)
)
VisualizationOut [18]
CollapseExpand
Scatter plot with grey density contours overlaid, plus two dashed ellipses of different orientations centred on two marked means.
Fitted two-component mixture. Muted contours show the total density, dashed ellipses mark one standard deviation of each component, and crosses mark the fitted means. The ellipses capture the differing orientation and aspect ratio of each component's covariance.

Each dashed ellipse is a radius-one Mahalanobis contour, not a universal 68% probability region. In two dimensions it encloses about 39% of its Gaussian component's probability. It has the orientation and aspect ratio of that component's covariance matrix: one is long and tilted steeply, the other is rounder and tilted the other way. Those shapes are estimated from the data, not supplied, and they are what a diagonal or spherical covariance type would be unable to represent.

The real output of the fit is not the contours but the responsibilities. Colouring each point by its posterior probability of belonging to component 1 produces a gradual transition through the overlap region instead of a step.

VisualizationOut [19]
CollapseExpand
Scatter plot where points are coloured on a continuous scale from blue to red according to the posterior probability of the first component.
Posterior probability of component 1 for each observation, with points coloured on a continuous scale from low to high responsibility. Points in the overlap region fade between the two components rather than being assigned to one, and the colour gradient is broad around the overlap. Observations strongly favoured by either component have saturated colours.

The colour gradient is broad around the overlap, while observations strongly favoured by either component have saturated colours. Points in the pale region have ambiguous component assignments under this fitted model; a downstream decision that treats them as firmly assigned is throwing away information the model has already computed.

K-means, fitted to the same data, has no such region. It produces a boundary and commits.

VisualizationOut [20]
CollapseExpand
Scatter plot of the same data with points coloured in two solid colours by k-means cluster label.
Hard assignments from k-means on the same data, with every point fully assigned by colour. Points the mixture model places near a responsibility of one half are forced into a cluster, and the nearest-centroid rule cuts through the overlap with a straight decision boundary.

Comparing this with the previous figure shows where the two methods diverge: near the interface, k-means draws a straight line through points that the mixture model declines to commit on. The pictures summarize different fitted models. The mixture displays its conditional uncertainty; its visual smoothness does not establish that the model is correct.

Finally, inspect the optimization itself. This seeded run records increasing log-likelihood values that flatten; we check that observed behaviour rather than assuming it from the diagonal-addition update.

VisualizationOut [21]
CollapseExpand
Line chart of log-likelihood against EM iteration, increasing and then levelling off.
Log-likelihood recorded at each EM iteration for the fitted two-component mixture. The displayed run rises and flattens; the flat right-hand end is the practical stopping signal, but it does not show whether this plateau is the highest one available.

The exact EM proof explains why likelihood monitoring matters. Here diagonal regularization means we must also check for decreases; a flat right-hand end is a practical stopping signal. Note what the curve does not show: whether this plateau is the highest one available.

Different starting points give a practical check on that question, although finitely many starts cannot settle it. The figure below runs the same algorithm from five random initializations and overlays the trajectories.

PythonIn [22]
CollapseExpand
random_runs = [
    fit_gmm(X, 2, init="random", seed=s, reg_covar=1e-6) for s in range(5)
]
VisualizationOut [23]
CollapseExpand
Five line charts of log-likelihood against iteration, rising and flattening at different final heights.
Five independent EM runs from random initializations on the same data, with final log-likelihoods reported in the legend. Different final heights show initialization dependence under this stopping rule; they do not by themselves certify stationary points or a global optimum.

The legend reports each run's final log-likelihood. Where the converged curves separate, the lower plateaus are poorer fitted solutions. The finite trajectories do not establish whether a plateau is an exact local optimum. When the curves do coincide, the data happened to be forgiving, and nothing in the algorithm guarantees that it will stay that way. This is the practical argument for n_init.

Practical Implications

Gaussian mixtures are a reasonable choice when observations are continuous and the groups you expect are roughly elliptical (elongated, rotated, or differently scaled), and when graded membership is more useful than a single label. For a segmentation task we can retain overlapping membership probabilities. The fitted density also supplies log-density scores for anomaly detection, supports generative sampling, and can serve as a density component in a larger probabilistic model. Full covariance is the version that can use correlations between features and can let each component have its own orientation and aspect ratio, which is the flexibility k-means lacks. When the scientific question only needs a partition, Euclidean feature distance is meaningful, and the clusters are compact, roughly spherical, and of comparable spread, k-means remains the better default.

Cost and sample size determine whether that flexibility is affordable. A GMM iteration costs O(nKd2)O(nKd^2) plus O(Kd3)O(Kd^3) for covariance factorizations, compared with O(nKd)O(nKd) for k-means, and a full-covariance model has KdKd means and Kd(d+1)/2Kd(d+1)/2 covariance entries to estimate. Each component therefore needs a meaningful number of effective observations: a small NkN_k is a warning about statistical stability, but nonsingularity depends on the affine span of positive-weight observations, not on Nk>d+1N_k>d+1. Full covariance estimation can still be unstable with few well-supported observations, especially under overlap or noisy features. When counts fall short, constrain the covariance, reduce dimensionality before fitting (the subject of Dimensionality Reduction), or reduce KK. Large nn and high dd with nearly spherical groups are the regime where k-means is usually preferable. The cost of a full-covariance fit also grows quickly with dimension, so a full model in dozens of dimensions should be justified by both the sample size and evidence that correlated, non-spherical components are present.

The data requirements concern the conditional distributions within components. Features should be continuous or transformed to a scale on which a Gaussian is a plausible local approximation; strong skew, heavy tails, or discrete counts are better handled by transforming the features, adding components, or choosing a different model. Outliers can pull means and inflate covariances, and near-duplicates can encourage degenerate components, so inspect fitted responsibilities and effective counts rather than trusting a single likelihood value. Scaling has a precise role. For corresponding full-covariance parameters under an invertible affine transformation, responsibilities are unchanged but density values change by the Jacobian factor. Diagonal models remain closed under feature-wise rescaling; spherical models generally do not. Fixed isotropic regularization, initialization and stopping can change even a full-covariance numerical fit. Scaling is therefore a modelling and numerical choice. Fit any learned preprocessing on training observations and apply it unchanged to held-out observations; compare density scores only in the same coordinates.

A practical workflow starts from a structured initialization and several restarts. K-means centroids are one useful starting point, and trying n_init between 5 and 10 is a practical initial budget rather than a guarantee; keep the run with the highest final log-likelihood. Our example uses reg_covar=1e-6, allows 300 iterations, and uses a total-log-likelihood tolerance of 1e-6. Other data and the scikit-learn average-gain convention may require different settings. After fitting, check that:

  • no component has a vanishing weight or an effective count near zero;
  • no covariance eigenvalue is sitting on the regularization floor;
  • the log-likelihood history is monotone and has plateaued;
  • the responsibilities reflect the expected component overlap; nearly hard assignments can be appropriate for well-separated components;
  • independent restarts produce similar parameters after matching components.

These checks are diagnostic, not proof of correctness: EM can converge cleanly to a poor local optimum, and a well-behaved finite history does not establish stationarity or global optimality.

Deployment adds concerns that are easy to overlook during exploratory fitting. Store the fitted weights, means, covariances, the preprocessing used for the features, and the random seed, because a later refit without them may produce a different labelling of the components. Monitor held-out average log density rather than training likelihood, and refit or warm-start when the distribution drifts. Component labels are not stable across fits (permuting them leaves the likelihood unchanged), so compare components only after matching them by location, shape, or predictive role. If the model is used for anomaly detection, set the threshold from a validation sample and recheck it when the model is updated. Missing values deserve a note of caution: the Gaussian marginal over observed coordinates is well defined, but not every software implementation accepts incomplete rows, so a separate imputation or marginalization step may be required.

Covariance choices, model selection, and degeneracy

The covariance type is the first structural decision because it controls both flexibility and the number of parameters that must be estimated. Full covariance is the most flexible and the most expensive; it can represent any elliptical shape, at d(d+1)/2d(d+1)/2 parameters per component. Tied covariance shares a single shape across all components, which imposes the same covariance size, shape and orientation on every component while allowing different means and weights, and it reduces the number of covariance parameters. Diagonal covariance restricts ellipses to be axis-aligned, a strong assumption that a global principal component rotation does not automatically justify: removing marginal correlation need not remove correlation within every component. Spherical covariance gives every component a single variance and is the closest mixture analogue to k-means. Choose the most constrained structure that captures the visible structure, and relax it only when the constrained fit leaves systematic correlation or curvature in the residuals or when held-out density clearly improves.

Choosing the number of components is a model selection problem, and the training likelihood cannot do it: the best attainable likelihood within nested model families cannot decrease when components are added, but a finite EM fit can return a worse local solution. The standard criteria penalize complexity. The Bayesian information criterion is

BIC=−2ℓ(θ^)+plog⁡n,\mathrm{BIC} = -2\ell(\hat{\boldsymbol{\theta}}) + p \log n,

where:

  • ℓ(θ^)\ell(\hat{\boldsymbol{\theta}}): the maximized log-likelihood when a finite maximum-likelihood fit exists
  • pp: the number of free parameters
  • nn: the number of observations

The numerical comparison below evaluates this expression at the finite parameters returned by EM; it does not certify that the likelihood was maximized. The penalty per parameter grows with sample size. The Akaike criterion replaces log⁡n\log n with 22. Here n=400n=400, so BIC penalizes each parameter more heavily because log⁡400>2\log 400>2; that ordering does not hold for every sample size. These conventional criteria are useful model-selection heuristics, and their regular asymptotic justification needs care for singular mixture models; neither is a guarantee. A practical approach is to use these criteria as a guide across a manageable grid of plausible candidate values. Cross-validated log-likelihood is a reasonable alternative, provided it is computed on held-out data with the density evaluated there rather than on the training set. Because each candidate fit can land in a different local optimum, compare models only after giving each KK several restarts and retaining its best likelihood.

PythonIn [24]
CollapseExpand
selection = []
for k in range(1, 7):
    gm = GaussianMixture(
        n_components=k,
        covariance_type="full",
        reg_covar=1e-6,
        n_init=5,
        random_state=0,
    ).fit(X)
    selection.append((k, gm.bic(X), gm.aic(X), gm.score(X) * len(X)))
k_best = min(selection, key=lambda row: row[1])[0]
ConsoleOut [25]
CollapseExpand
 K    log-lik        BIC        AIC
 1    -1304.1     2638.2     2618.3
 2    -1232.2     2530.3     2486.4
 3    -1225.4     2552.7     2484.8
 4    -1224.8     2587.4     2495.5
 5    -1222.1     2618.0     2502.3
 6    -1219.3     2648.2     2508.5

BIC selects K = 2

The table separates fit from complexity. In this seeded comparison the fitted log-likelihood rises as components are added, while the penalized criteria eventually increase. That finite result is observed, not guaranteed by local EM, and the curves alone cannot distinguish real structure from noise. Read the minimum as a guide rather than a theorem: if BIC and AIC disagree and held-out log density is similar, prefer the more parsimonious model; if the selected KK changes across seeds or resamples, report that instability instead of presenting one value as definitive. The printed value is the criterion's answer for this sample, and refitting with different random states checks how sensitive the selected component count is to initialization.

The same selection result is clearer as a curve: the fitted log-likelihood trend improves, while the parameter penalties can make the criteria turn upward.

PythonIn [26]
CollapseExpand
k_vals = np.array([row[0] for row in selection])
bic_vals = np.array([row[1] for row in selection])
aic_vals = np.array([row[2] for row in selection])
VisualizationOut [27]
CollapseExpand
BIC and AIC curves across candidate component counts, with one curve reaching its minimum at a small K value.
BIC and AIC across candidate component counts from 1 to 6 for the fitted two-dimensional data. The penalized criteria trade likelihood against parameter count, the BIC curve reaches a minimum at the selected model, and the AIC curve penalizes less; the minimum is a guide rather than a theorem, so check that the choice is stable across seeds.

Degeneracy is the failure mode to watch for. The log-likelihood is unbounded above: if one component keeps a positive weight and collapses its covariance onto an observation, while another covers the rest, the total sample log-likelihood can grow without limit. The figure below shows this directly, on a deliberately tiny dataset with three coincident observations.

PythonIn [28]
CollapseExpand
x_deg = np.array([[0.0], [0.0], [0.0], [3.8], [4.0], [4.2]])
means_deg = np.array([[0.0], [4.0]])
covs_deg = np.stack([np.eye(1), np.array([[0.04]])])
weights_deg = np.array([0.5, 0.5])

sig_grid = np.logspace(-4, 0, 60)
deg_ll = []
for s in sig_grid:
    covs_deg[0] = np.array([[s**2]])
    _, ll_k = gmm_e_step(x_deg, weights_deg, means_deg, covs_deg, reg_covar=0.0)
    deg_ll.append(ll_k)
VisualizationOut [29]
CollapseExpand
Line chart of log-likelihood against the logarithm of the component standard deviation, rising steadily as the standard deviation approaches zero.
Log-likelihood of a two-component model on six observations, three of which coincide, as the first component's standard deviation shrinks from 1 down to 10^-4. The likelihood increases without bound as the standard deviation approaches zero, so no maximum likelihood estimate exists without regularization.

The three coincident observations at zero contribute a term proportional to −log⁡σ-\log \sigma, which diverges as σ→0\sigma \to 0; the other three observations are covered by the second component and do not resist the collapse. The example holds both weights at 0.50.5: covariance collapse does not require a vanishing weight. Exact duplicates make the plot easy to read, but one observation is enough for the unrestricted-mixture collapse construction when other components cover the remaining data. Near-singular component scatters can also make numerical fitting unstable. Our fitting routine requires a positive ϵ\epsilon and adds ϵI\epsilon\mathbf{I} before factorization to maintain a covariance eigenvalue floor. Its magnitude is a modelling choice: too small and the fit remains unstable, too large and the covariances are pulled toward spherical and the density is biased. When degeneracy persists, increase reg_covar, remove exact duplicates if they are artifacts, constrain the covariance type, or reduce the number of components rather than accepting a component that exists only to fit a handful of coincident points.

Summary

A Gaussian mixture model describes data as a weighted sum of Gaussian densities and treats the component membership of each observation as a latent variable. The model is fitted by maximizing a likelihood that contains the log of a sum, which has no closed-form solution, so we use Expectation-Maximization: compute the posterior probability of each component for each observation (the E-step), then re-estimate the means, covariances, and weights as if those posterior probabilities were fractional counts (the M-step). The updates are weighted versions of the ordinary maximum likelihood estimators, with responsibilities standing in for the unknown indicators, and exact bound-increasing EM updates cannot decrease the objective because the E-step makes the lower bound tight. Our practical diagonal-addition safeguard requires a separate decrease check.

The soft assignments are the point of the exercise. Where k-means commits, the mixture reports a probability, and that probability carries information about ambiguity that a hard boundary discards. The same posterior drives the covariance updates, which is what allows each component to take on its own orientation and aspect ratio, and the resulting density can be evaluated, sampled, and compared across models.

For exact EM the likelihood guarantee is nondecrease, not global optimality or parameter convergence. The likelihood is non-concave, so initialization and restarts determine the answer, and unrestricted mixtures with K≥2K\ge2 have an unbounded sample likelihood, so a covariance constraint or regularization is needed to prevent collapse. Components are terms in a density, not discovered truths about the world, and the number of components is a model selection decision to be made with penalized criteria rather than training likelihood. For continuous data with roughly elliptical components, a Gaussian mixture gives us a probability density and model-based soft assignments alongside a hard partition.

Quiz

Ready to test your understanding of Gaussian mixtures and Expectation-Maximization? Take this quick quiz to reinforce what you've learned about the model, the EM updates, and their practical pitfalls.

Join the discussionAsk a question or share what you learned.

Comments

Add to the conversation

A question, a correction, or an example from your own work.

No comments yet. Be the first to share your thoughts!

A different angle
All writing
Machine Learning · Vol. 3

DBSCAN: Density-Based Clustering for Arbitrary Shapes

Covers DBSCAN (Density-Based Spatial Clustering of Applications with Noise), the algorithm.

58 min read
Machine Learning · Vol. 3

HDBSCAN Clustering: Hierarchical Density-Based Clustering

Covers HDBSCAN clustering algorithm covering density-based clustering, automatic cluster selection, noise detection.

37 min read
Machine Learning · Vol. 3

Hierarchical Clustering: Dendrograms, Linkage Criteria

Covers hierarchical clustering, including dendrograms, linkage criteria (single, complete, average, Ward), and scikit-learn implementation.

51 min read

About the author

Reference

Citation details

Cite or share this article.

BIBTEXAcademic
@misc{brenndoerfer2026gaussianmixture, author = {Michael Brenndoerfer}, title = {Gaussian Mixture Models and Expectation-Maximization}, year = {2026}, url = {https://mbrenndoerfer.com/writing/gaussian-mixture-models-from-scratch}, organization = {mbrenndoerfer.com}, note = {Accessed: 2026-10-30} }
APAAcademic
Michael Brenndoerfer (2026). Gaussian Mixture Models and Expectation-Maximization. Retrieved from https://mbrenndoerfer.com/writing/gaussian-mixture-models-from-scratch
MLAAcademic
Michael Brenndoerfer. "Gaussian Mixture Models and Expectation-Maximization." 2026. Web. October 30, 2026. <https://mbrenndoerfer.com/writing/gaussian-mixture-models-from-scratch>.
CHICAGOAcademic
Michael Brenndoerfer. "Gaussian Mixture Models and Expectation-Maximization." Accessed October 30, 2026. https://mbrenndoerfer.com/writing/gaussian-mixture-models-from-scratch.
HARVARDAcademic
Michael Brenndoerfer (2026) 'Gaussian Mixture Models and Expectation-Maximization'. Available at: https://mbrenndoerfer.com/writing/gaussian-mixture-models-from-scratch (Accessed: October 30, 2026).
SimpleBasic
Michael Brenndoerfer (2026). Gaussian Mixture Models and Expectation-Maximization. https://mbrenndoerfer.com/writing/gaussian-mixture-models-from-scratch
Newsletter

Stay up to date

Get new articles, book updates, and notes on AI and technology.

Unsubscribe at any time. Privacy policy