Soft clustering with overlapping bell curves.
On this page
k-Means Clustering gives every point one cluster and every cluster a circle. A Gaussian mixture gives each point a share in every cluster and lets clusters be tilted ellipses.
A Gaussian mixture model (GMM) describes the data as a weighted sum of Gaussian bumps, gives each point a share in every bump, fits the bumps with the EM loop from Maximum Likelihood and EM, and picks the number of bumps with a score that charges for extra parameters.
Each Gaussian, a component, has a center, a covariance and a weight, and EM picks the values that maximize the likelihood of the data. EM runs once over the training set; after that, scoring a new point is one pass over the K components.
Which component made a point? Bayes rule answers with shares: each component’s height at the point, times its weight, rescaled to add to 1.
The shares need the components and the components need the shares, the loop EM solves. The E step computes every rik; the M step refits each component from the points counted at their shares.
Figure 4. Toy data like the Old Faithful geyser, eruption length against waiting time, in minutes.
This snippet runs one EM round in NumPy on two toy blobs and prints the new weights and means; after the starting guess, its lines follow the card from top to bottom.
import numpy as np
from scipy.stats import multivariate_normal as mvn
rng = np.random.default_rng(0)
blob_a = rng.normal([-1, -1], 0.5, (100, 2))
blob_b = rng.normal([1, 1], 0.5, (150, 2))
X = np.vstack([blob_a, blob_b])
w = np.array([0.5, 0.5])
mu = np.array([[-0.5, 0.0], [0.5, 0.0]])
Sigma = np.array([np.eye(2), np.eye(2)])
K = len(w)
components = [mvn(mu[k], Sigma[k]) for k in range(K)]
a = np.column_stack([w[k] * components[k].pdf(X) for k in range(K)])
r = a / a.sum(axis=1, keepdims=True)
N_k = r.sum(axis=0)
mu = r.T @ X / N_k[:, None]
covs = [np.cov(X.T, aweights=r[:, k], bias=True) for k in range(K)]
Sigma = np.stack(covs)
w = N_k / len(X)
print(w.round(2), mu.round(2))
Give every component the same round covariance, with its standard deviation shrinking toward zero. Each share becomes 0 or 1 for the nearest center, and the center update becomes k-means’. k-means never estimates a covariance, hence its circles.
Figure 5. Narrow components give the dark point to the nearer center, like k-means.
A full covariance lets every component tilt and stretch freely, at three numbers per component in 2D. Cheaper options constrain it; scikit-learn names them with covariance_type.
The long axes of each ellipse are the directions PCA (principal component analysis) would find inside that cluster.
More components always raise the likelihood, so the likelihood alone cannot pick K. The Bayesian information criterion (BIC) can.
This snippet fits 1 to 6 components to the same two blobs and prints each BIC; look for the smallest.
import numpy as np
from sklearn.mixture import GaussianMixture
rng = np.random.default_rng(0)
blob_a = rng.normal([-1, -1], 0.5, (100, 2))
blob_b = rng.normal([1, 1], 0.5, (150, 2))
X = np.vstack([blob_a, blob_b])
for K in range(1, 7):
gm = GaussianMixture(n_components=K, random_state=0).fit(X)
print(K, gm.bic(X)) # lowest wins
With two separate blobs, expect the smallest BIC at 2.
In scikit-learn a mixture sits after a scaler; fit runs EM once on the training points, and predict_proba or score reads new points.
import numpy as np
from sklearn.mixture import GaussianMixture
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
rng = np.random.default_rng(0)
short_eruptions = rng.normal([2.0, 55], [0.3, 6], (100, 2))
long_eruptions = rng.normal([4.3, 80], [0.4, 6], (172, 2))
X = np.vstack([short_eruptions, long_eruptions])
gm = GaussianMixture(n_components=2, covariance_type="full", random_state=0)
model = make_pipeline(StandardScaler(), gm)
model.fit(X)
new = [[2.1, 54.0], [4.0, 79.0]]
print(model.predict_proba(new).round(2)) # shares r_ik
print(model.score(new), gm.n_iter_) # mean log-likelihood, rounds
StandardScaler with a two-component GaussianMixture.tol.| step | when, on what, and its cost |
|---|---|
fit | Once, in EM rounds over all N points; each round evaluates K component densities at every point (Bishop 2006, sec. 9.2). |
predict_proba, score | Per new point, K component densities, no refit. |
| state | wk, μk, Σk per component (weights_, means_, covariances_). |
| setting | raise it, lower it | default |
|---|---|---|
n_components, K in Σk | Up always raises the likelihood and soon overfits; K = 1 is one component for the whole cloud. BIC picks it. | 1 |
covariance_type, the shape of Σk | "full" fits any tilt at the most parameters; "diag", "spherical" and "tied" constrain it. | "full" |
reg_covar, added to the diagonal of Σk | Up keeps components wide and stable; 0 lets a component collapse onto one point. | 1e-6 |
tol, max_iter | EM stops when a round raises the average log-likelihood per point by less than tol, or after max_iter rounds. A smaller tol runs more rounds and lands closer to the peak; too large a tol stops early. Hitting max_iter stops with a convergence warning. | 1e-3, 100 |
n_init, init_params | More starts, best kept, fewer poor local peaks. The first guess comes from k-means; "k-means++", "random" and "random_from_data" are the other options, and a poor start lands on a poorer local peak. | 1, "kmeans" |
Defaults are from the GaussianMixture docs. Gotchas are in Section 10.
In speaker verification, the classic use, Reynolds et al. (2000) fit a large background GMM to speech from many people and adapt each speaker’s model from it. Each short frame becomes a vector of MFCCs, numbers describing its spectrum shape (see MFCC and Other Audio Features), and both models score the frames.
A fitted mixture scores every new point (score_samples); a very low score flags an anomaly. Classic speech recognizers used a mixture per hidden state to score each frame (Rabiner 1989, eq. 49), as in Hidden Markov Models.
reg_covar prevents it.n_init to try several starts.When the hidden cluster follows a chain over time, as words in speech or a casino’s dice do, the model is a hidden Markov model.
sklearn.mixture.GaussianMixture documentation.