Skip to content
learn
  1. Learn
  2. Machine Learning
  3. Unsupervised Learning

Machine Learning ML-07.03

Gaussian mixture models and EM

Soft clustering with overlapping bell-shaped clusters, fitted by expectation-maximization: guess memberships, refit clusters, repeat until stable.

Advanced 12 min Last reviewed

The big idea

k-means gives each point exactly one cluster, and assumes every cluster is a round ball. Real groups overlap and stretch. A customer who is 60 percent "bargain hunter" and 40 percent "weekend splurger" is not well described by a single label.

A Gaussian mixture modelA probability model in which data are generated by first choosing one of k Gaussian components with given weights, then drawing a point from that component's normal distribution. (GMM) imagines the data was made by a two-step recipe: pick one of k bell-shaped clouds at random (with some weights), then draw a point from that cloud. Each cloud has its own centre, its own spread and its own tilt. Clustering means working backwards from the points to the clouds, and the answer for each point is a soft clusteringClustering in which each point gets a probability of belonging to each cluster instead of a single label.: a probability for each cloud.

The fitting algorithm, expectation-maximizationExpectation-maximization: an iterative algorithm for models with hidden variables that alternates estimating the hidden variables given the parameters (E-step) and the parameters given those estimates (M-step). (EM), is a chicken-and-egg loop. If you knew which cloud each point came from, fitting the clouds would be easy. If you knew the clouds, computing each point's membership would be easy. So guess one, compute the other, and alternate.

How it works

EM fitting two Gaussian components 80 data points in two overlapping clouds: a long cloud tilted along the diagonal near (2, 2) and a narrow, almost vertical cloud near (5, 2). Glowing ellipses mark two standard deviations of each component. Step 1: two circles at (3, 4) and (4, 0). Step 2, after one iteration: two wide ellipses covering most points. Step 3, after eight: one tilted ellipse on the diagonal cloud, one on the right cloud. Step 4, converged after 26 iterations: tight ellipses matching each cloud's shape and tilt. 02460246feature 1feature 2 Figure 1. Snapshots of the EM loop from the code below (ellipses span two standard deviations; one point in five is drawn). Each component can stretch and tilt, which k-means cannot.
  1. Start: two round guesses Two Gaussians start as circles centred at (3, 4) and (4, 0), with equal weights. Neither fits the data.
  2. After 1 EM iteration Every point has been shared between the two guesses in proportion to how likely each made it. Both ellipses stretch over most of the data.
  3. After 8 iterations The ellipses separate: one tilts along the long diagonal cloud, the other settles on the narrow cloud on the right.
  4. Converged after 26 iterations Means (2.16, 2.11) and (5.04, 1.93). The left component is long and tilted at about 43 degrees; the right one is narrow and nearly vertical. Log-likelihood stopped improving.

Step 1: The model

Each component j has a mixing weight pi_j (the share of points it generates), a mean vector mu_j and a covariance matrix Sigma_j (spread and tilt). The probability density of a point is a weighted sum of bells:

Fitting means choosing the parameters that make the observed data most likely, called maximum likelihoodChoosing model parameters that maximise the probability (likelihood) of the observed data under the model.. There is no closed-form solution, because we do not know which component made which point. That unknown is a latent variableA variable that is part of the model but never observed, such as which mixture component generated each point..

Step 2: E-step, compute responsibilities

Given the current parameters, compute for every point i and component j the responsibilityIn a mixture model, the posterior probability that a given point was generated by a given component., the probability that component j produced point i, with Bayes' rule:

Each row of responsibilities sums to 1. In the code, the point (3.6, 2.0), which sits between the clouds, ends up with responsibilities 0.391 and 0.609.

Step 3: M-step, refit each component with weighted points

Treat responsibilities as fractional memberships. Each component's new weight is its share of total responsibility, its mean is the responsibility-weighted average of all points, and its covariance is the weighted covariance around that mean. A point with responsibility 0.6 counts as 0.6 of a point for that component.

Step 4: Repeat until the log-likelihood stops improving

The EM loop Three stages around a ring: E-step computes responsibilities from the current clouds, M-step refits weights, means and covariances, and a check asks whether the log-likelihood is still rising. The E-step glows. The centre reads: likelihood never falls. E-step responsibilities from current clouds M-step refit weights, means, covariances check log-likelihood still rising? EM loop likelihood never falls Figure 2. EM alternates two easy problems instead of solving one hard one. Each round is guaranteed not to decrease the data's log-likelihood, so it converges, though possibly to a local optimum.

In the code the log-likelihood rises from -1908.0 after the first iteration to -1134.5 at convergence after 26. k-means is the special case of EM where every covariance is the same tiny circle and responsibilities are rounded to 0 or 1, which is why it has the same assign-update rhythm.

Going deeper

Code: EM from scratch, soft membership and choosing k

gmm.pyruns with python3 + numpy + scipy + scikit-learn
import numpy as np
from scipy.stats import multivariate_normal as mvn
from sklearn.cluster import KMeans
from sklearn.metrics import adjusted_rand_score
from sklearn.mixture import GaussianMixture

rng = np.random.default_rng(1)
# Two elongated, tilted clusters that overlap a little
A = rng.multivariate_normal([2, 2], [[1.6, 1.2], [1.2, 1.2]], 200)
B = rng.multivariate_normal([5, 2], [[0.3, -0.25], [-0.25, 0.9]], 200)
X = np.vstack([A, B]); truth = np.repeat([0, 1], 200)

# EM from scratch for k = 2
k = 2
mu = np.array([[3.0, 4.0], [4.0, 0.0]]); cov = np.array([np.eye(2)] * k); w = np.full(k, 1 / k)
prev = -np.inf
for it in range(1, 200):
    # E-step: responsibilities r[i, j] = P(cluster j | x_i)
    dens = np.column_stack([w[j] * mvn(mu[j], cov[j]).pdf(X) for j in range(k)])
    r = dens / dens.sum(1, keepdims=True)
    ll = np.log(dens.sum(1)).sum()
    # M-step: weighted means, covariances and mixing weights
    nk = r.sum(0)
    w = nk / len(X)
    mu = (r.T @ X) / nk[:, None]
    cov = np.array([((r[:, j, None] * (X - mu[j])).T @ (X - mu[j])) / nk[j] for j in range(k)])
    if it in (1, 2, 5) or ll - prev < 1e-6:
        print(f"iter {it:>2}: log-likelihood {ll:8.1f}, means {np.round(mu, 2).tolist()}")
    if ll - prev < 1e-6:
        break
    prev = ll
print("final covariances:", np.round(cov, 2).tolist())

# Soft membership of a point between the clusters
x = np.array([3.6, 2.0])
d = np.array([w[j] * mvn(mu[j], cov[j]).pdf(x) for j in range(k)])
print(f"point {x.tolist()}: P(cluster) = {np.round(d / d.sum(), 3).tolist()}")

hard = r.argmax(1)
print(f"\nARI  GMM (EM above) {adjusted_rand_score(truth, hard):.3f}   "
      f"k-means {adjusted_rand_score(truth, KMeans(2, n_init=10, random_state=0).fit_predict(X)):.3f}")

# Choosing the number of components with BIC (lower is better)
print("\nk   BIC")
for kk in range(1, 6):
    print(f"{kk}   {GaussianMixture(kk, n_init=3, random_state=0).fit(X).bic(X):.1f}")

Output

iter  1: log-likelihood  -1908.0, means [[3.26, 2.67], [3.98, 1.31]]
iter  2: log-likelihood  -1323.3, means [[3.2, 2.64], [4.04, 1.36]]
iter  5: log-likelihood  -1304.5, means [[2.82, 2.48], [4.44, 1.53]]
iter 26: log-likelihood  -1134.5, means [[2.16, 2.11], [5.04, 1.93]]
final covariances: [[[1.17, 0.96], [0.96, 1.02]], [[0.31, -0.23], [-0.23, 1.04]]]
point [3.6, 2.0]: P(cluster) = [0.391, 0.609]

ARI  GMM (EM above) 0.874   k-means 0.739

k   BIC
1   2721.9
2   2335.0
3   2371.8
4   2409.5
5   2441.5

On these two tilted, overlapping clouds the GMM recovers the true groups with an adjusted Rand index of 0.874, while k-means, limited to round clusters, gets 0.739.

Choosing the number of components

Likelihood always improves with more components, so compare models with a penalty for parameters. The BICBayesian information criterion: minus twice the log-likelihood plus the number of parameters times the log of the number of points. Lower is better. is the usual choice. In the code it is lowest (2335.0) at k = 2, the true number, and rises for k = 3, 4 and 5.

Practical issues

  • Covariance type: full covariances (any tilt) need d(d+1)/2 parameters per component, which explodes in high dimensions. covariance_type="diag", "tied" or "spherical" constrain them.
  • Collapse: a component can shrink onto a single point, sending its likelihood to infinity. Libraries add a small value to the diagonal (reg_covar).
  • Initialization: EM finds local optima; scikit-learn initializes from k-means and supports several restarts (n_init).
  • Density estimation: a fitted GMM gives p(x) for any point, so low-density points can be flagged as anomalies ( Anomaly detection ), and you can sample new points from it.
  • EM is general: the same E and M steps fit hidden Markov models, handle missing data and train many latent-variable models.

Trade-offs

OptionWins whenLoses when
k-means Speed, round similar-sized clusters, hard labels are enough Elongated or overlapping clusters; no probabilities
GMM, full covariance Tilted, stretched, overlapping clusters; soft membership; density Many parameters in high dimensions; slower; local optima
GMM, diagonal covariance Many features with limited data Cannot capture tilt (correlation within a cluster)
DBSCAN Arbitrary non-elliptical shapes and noise No probabilities; struggles with overlapping clusters

Common mistakes

  • Reading responsibilities as certainty: 0.61 versus 0.39 means the point is genuinely ambiguous.
  • Choosing k by the highest likelihood, which always prefers more components; use BIC or held-out likelihood.
  • Fitting full covariances with 100 features and a few hundred points.
  • Forgetting that EM can converge to a poor local optimum; run several initializations.
  • Expecting a GMM to model crescent or ring shapes; it can only approximate them with many ellipses.

In the wild

Gaussian mixtures were for decades the acoustic model in speech recognition, modelling how each speech sound is spread in feature space, before neural networks took over. Today they appear in background subtraction for video (each pixel's colour as a small mixture), in density-based anomaly detection, in customer segmentation where soft membership is useful, and as a teaching ground for EM, which itself powers hidden Markov models and many missing-data methods.

Check yourself

  1. What does the E-step compute?

    Show answer

    B. Each point's responsibility: the probability that each component generated it Given current parameters, Bayes' rule gives each point's probability of belonging to each component.

  2. A point has responsibilities 0.5 and 0.5. How does it affect the M-step?

    Show answer

    B. It counts as half a point for each component The M-step uses responsibility-weighted averages, so the point contributes equally to both components.

  3. Why can a GMM separate two long, tilted, overlapping clouds that k-means splits badly?

    Show answer

    B. Each component has its own covariance, so it can stretch and tilt A full covariance matrix describes spread and correlation, letting each ellipse match its cloud's shape.

  4. Log-likelihood keeps increasing as you add components. How do you choose k?

    Show answer

    B. Use BIC (or held-out likelihood), which penalises extra parameters More components always fit the training data better. BIC trades fit against parameter count; it picked k = 2 in the code.

Recap

  • A GMM models data as a weighted mixture of Gaussian components, each with its own mean, covariance and weight.
  • It gives soft clustering: each point has a probability of belonging to each component.
  • EM alternates the E-step (responsibilities via Bayes' rule) and the M-step (responsibility-weighted refits).
  • Each EM round never lowers the likelihood, but it can converge to a local optimum; restart and initialize well.
  • Choose the number of components with BIC; constrain covariances in high dimensions.

Get new lessons by email

Get new posts on backend and AI internals. No spam, unsubscribe anytime.

Your email is only used to send new posts. Privacy